import { getServerSideConfig } from "@/app/config/server"; import { TENCENT_BASE_URL, ApiPath, ModelProvider, ServiceProvider, Tencent, } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/api/auth"; import { isModelAvailableInServer } from "@/app/utils/model"; import CryptoJS from "crypto-js"; import mapKeys from "lodash-es/mapKeys"; import mapValues from "lodash-es/mapValues"; import isArray from "lodash-es/isArray"; import isObject from "lodash-es/isObject"; const serverConfig = getServerSideConfig(); async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { console.log("[Tencent Route] params ", params); if (req.method === "OPTIONS") { return NextResponse.json({ body: "OK" }, { status: 200 }); } const authResult = auth(req, ModelProvider.Hunyuan); if (authResult.error) { return NextResponse.json(authResult, { status: 401, }); } try { const response = await request(req); return response; } catch (e) { console.error("[Tencent] ", e); return NextResponse.json(prettyObject(e)); } } export const GET = handle; export const POST = handle; export const runtime = "edge"; export const preferredRegion = [ "arn1", "bom1", "cdg1", "cle1", "cpt1", "dub1", "fra1", "gru1", "hnd1", "iad1", "icn1", "kix1", "lhr1", "pdx1", "sfo1", "sin1", "syd1", ]; async function request(req: NextRequest) { const controller = new AbortController(); // tencent just use base url or just remove the path let path = `${req.nextUrl.pathname}`.replaceAll( ApiPath.Tencent + "/" + Tencent.ChatPath, "", ); let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; } if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, -1); } console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); const timeoutId = setTimeout( () => { controller.abort(); }, 10 * 60 * 1000, ); const fetchUrl = `${baseUrl}${path}`; let body = null; if (req.body) { const bodyText = await req.text(); console.log( "Dogtiti ~ request ~ capitalizeKeys(JSON.parse(bodyText):", capitalizeKeys(JSON.parse(bodyText)), ); body = JSON.stringify(capitalizeKeys(JSON.parse(bodyText))); } const fetchOptions: RequestInit = { headers: { ...getHeader(body), }, method: req.method, body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME redirect: "manual", // @ts-ignore duplex: "half", signal: controller.signal, }; // #1815 try to refuse some request to some models if (serverConfig.customModels && req.body) { try { const clonedBody = await req.text(); fetchOptions.body = clonedBody; const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false if ( isModelAvailableInServer( serverConfig.customModels, jsonBody?.model as string, ServiceProvider.Tencent as string, ) ) { return NextResponse.json( { error: true, message: `you are not allowed to use ${jsonBody?.model} model`, }, { status: 403, }, ); } } catch (e) { console.error(`[Tencent] filter`, e); } } console.log("[Tencent request]", fetchOptions.headers, req.method); try { const res = await fetch(fetchUrl, fetchOptions); console.log("[Tencent response]", res.status, " ", res.headers, res.url); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); // to disable nginx buffering newHeaders.set("X-Accel-Buffering", "no"); return new Response(res.body, { status: res.status, statusText: res.statusText, headers: newHeaders, }); } finally { clearTimeout(timeoutId); } } function capitalizeKeys(obj: any): any { if (isArray(obj)) { return obj.map(capitalizeKeys); } else if (isObject(obj)) { return mapValues( mapKeys( obj, (value: any, key: string) => key.charAt(0).toUpperCase() + key.slice(1), ), capitalizeKeys, ); } else { return obj; } } // 使用 SHA-256 和 secret 进行 HMAC 加密 function sha256(message: any, secret = "", encoding = "hex") { const hmac = CryptoJS.HmacSHA256(message, secret); if (encoding === "hex") { return hmac.toString(CryptoJS.enc.Hex); } else if (encoding === "base64") { return hmac.toString(CryptoJS.enc.Base64); } else { return hmac.toString(); } } // 使用 SHA-256 进行哈希 function getHash(message: any, encoding = "hex") { const hash = CryptoJS.SHA256(message); if (encoding === "hex") { return hash.toString(CryptoJS.enc.Hex); } else if (encoding === "base64") { return hash.toString(CryptoJS.enc.Base64); } else { return hash.toString(); } } function getDate(timestamp: number) { const date = new Date(timestamp * 1000); const year = date.getUTCFullYear(); const month = ("0" + (date.getUTCMonth() + 1)).slice(-2); const day = ("0" + date.getUTCDate()).slice(-2); return `${year}-${month}-${day}`; } function getHeader(payload: any) { // https://cloud.tencent.com/document/api/1729/105701 // 密钥参数 const SECRET_ID = serverConfig.tencentSecretId; const SECRET_KEY = serverConfig.tencentSecretKey; const endpoint = "hunyuan.tencentcloudapi.com"; const service = "hunyuan"; const region = ""; // optional const action = "ChatCompletions"; const version = "2023-09-01"; const timestamp = Math.floor(Date.now() / 1000); //时间处理, 获取世界时间日期 const date = getDate(timestamp); // ************* 步骤 1:拼接规范请求串 ************* const hashedRequestPayload = getHash(payload); const httpRequestMethod = "POST"; const canonicalUri = "/"; const canonicalQueryString = ""; const canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + endpoint + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; const signedHeaders = "content-type;host;x-tc-action"; const canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload; // ************* 步骤 2:拼接待签名字符串 ************* const algorithm = "TC3-HMAC-SHA256"; const hashedCanonicalRequest = getHash(canonicalRequest); const credentialScope = date + "/" + service + "/" + "tc3_request"; const stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; // ************* 步骤 3:计算签名 ************* const kDate = sha256(date, "TC3" + SECRET_KEY); const kService = sha256(service, kDate); const kSigning = sha256("tc3_request", kService); const signature = sha256(stringToSign, kSigning, "hex"); // ************* 步骤 4:拼接 Authorization ************* const authorization = algorithm + " " + "Credential=" + SECRET_ID + "/" + credentialScope + ", " + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; return { Authorization: authorization, "Content-Type": "application/json; charset=utf-8", Host: endpoint, "X-TC-Action": action, "X-TC-Timestamp": timestamp.toString(), "X-TC-Version": version, "X-TC-Region": region, }; }