213 lines
5.3 KiB
TypeScript
213 lines
5.3 KiB
TypeScript
"use server";
|
||
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 * as crypto from "node:crypto";
|
||
|
||
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;
|
||
|
||
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}`;
|
||
|
||
const body = await req.text();
|
||
const fetchOptions: RequestInit = {
|
||
headers: {
|
||
...getHeader(body),
|
||
},
|
||
method: req.method,
|
||
body,
|
||
redirect: "manual",
|
||
// @ts-ignore
|
||
duplex: "half",
|
||
signal: controller.signal,
|
||
};
|
||
|
||
try {
|
||
const res = await fetch(fetchUrl, fetchOptions);
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
|
||
// 使用 SHA-256 和 secret 进行 HMAC 加密
|
||
function sha256(message: any, secret = "", encoding?: string) {
|
||
return crypto.createHmac("sha256", secret).update(message).digest(encoding);
|
||
}
|
||
|
||
// 使用 SHA-256 进行哈希
|
||
function getHash(message: any, encoding = "hex") {
|
||
return crypto.createHash("sha256").update(message).digest(encoding);
|
||
}
|
||
|
||
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 contentType = "application/json";
|
||
const canonicalUri = "/";
|
||
const canonicalQueryString = "";
|
||
const canonicalHeaders =
|
||
`content-type:${contentType}\n` +
|
||
"host:" +
|
||
endpoint +
|
||
"\n" +
|
||
"x-tc-action:" +
|
||
action.toLowerCase() +
|
||
"\n";
|
||
const signedHeaders = "content-type;host;x-tc-action";
|
||
|
||
const canonicalRequest = [
|
||
httpRequestMethod,
|
||
canonicalUri,
|
||
canonicalQueryString,
|
||
canonicalHeaders,
|
||
signedHeaders,
|
||
hashedRequestPayload,
|
||
].join("\n");
|
||
|
||
// ************* 步骤 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": contentType,
|
||
Host: endpoint,
|
||
"X-TC-Action": action,
|
||
"X-TC-Timestamp": timestamp.toString(),
|
||
"X-TC-Version": version,
|
||
"X-TC-Region": region,
|
||
};
|
||
}
|