|
|
@@ -0,0 +1,309 @@
|
|
|
+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,
|
|
|
+ };
|
|
|
+}
|