ChatGPT-Next-Web/app/api/tencent/[...path]/route.ts

310 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}