hotfix for tencent sign

This commit is contained in:
lloydzhou 2024-08-01 15:33:48 +08:00
parent a024980c03
commit f85ec95877
6 changed files with 141 additions and 135 deletions

View File

@ -1,3 +1,4 @@
"use server";
import { getServerSideConfig } from "@/app/config/server"; import { getServerSideConfig } from "@/app/config/server";
import { import {
TENCENT_BASE_URL, TENCENT_BASE_URL,
@ -10,11 +11,7 @@ import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth"; import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model"; import { isModelAvailableInServer } from "@/app/utils/model";
import CryptoJS from "crypto-js"; import * as crypto from "node:crypto";
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(); const serverConfig = getServerSideConfig();
@ -47,27 +44,6 @@ async function handle(
export const GET = handle; export const GET = handle;
export const POST = 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) { async function request(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
@ -99,63 +75,22 @@ async function request(req: NextRequest) {
const fetchUrl = `${baseUrl}${path}`; const fetchUrl = `${baseUrl}${path}`;
let body = null; const body = await req.text();
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 = { const fetchOptions: RequestInit = {
headers: { headers: {
...getHeader(body), ...getHeader(body),
}, },
method: req.method, method: req.method,
body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME body,
redirect: "manual", redirect: "manual",
// @ts-ignore // @ts-ignore
duplex: "half", duplex: "half",
signal: controller.signal, 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 { try {
const res = await fetch(fetchUrl, fetchOptions); const res = await fetch(fetchUrl, fetchOptions);
console.log("[Tencent response]", res.status, " ", res.headers, res.url);
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate"); newHeaders.delete("www-authenticate");
@ -172,45 +107,16 @@ async function request(req: NextRequest) {
} }
} }
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 加密 // 使用 SHA-256 和 secret 进行 HMAC 加密
function sha256(message: any, secret = "", encoding = "hex") { function sha256(message: any, secret = "", encoding?: string) {
const hmac = CryptoJS.HmacSHA256(message, secret); return crypto.createHmac("sha256", secret).update(message).digest(encoding);
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 进行哈希 // 使用 SHA-256 进行哈希
function getHash(message: any, encoding = "hex") { function getHash(message: any, encoding = "hex") {
const hash = CryptoJS.SHA256(message); return crypto.createHash("sha256").update(message).digest(encoding);
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) { function getDate(timestamp: number) {
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
const year = date.getUTCFullYear(); const year = date.getUTCFullYear();
@ -238,10 +144,11 @@ function getHeader(payload: any) {
const hashedRequestPayload = getHash(payload); const hashedRequestPayload = getHash(payload);
const httpRequestMethod = "POST"; const httpRequestMethod = "POST";
const contentType = "application/json";
const canonicalUri = "/"; const canonicalUri = "/";
const canonicalQueryString = ""; const canonicalQueryString = "";
const canonicalHeaders = const canonicalHeaders =
"content-type:application/json; charset=utf-8\n" + `content-type:${contentType}\n` +
"host:" + "host:" +
endpoint + endpoint +
"\n" + "\n" +
@ -250,18 +157,14 @@ function getHeader(payload: any) {
"\n"; "\n";
const signedHeaders = "content-type;host;x-tc-action"; const signedHeaders = "content-type;host;x-tc-action";
const canonicalRequest = const canonicalRequest = [
httpRequestMethod + httpRequestMethod,
"\n" + canonicalUri,
canonicalUri + canonicalQueryString,
"\n" + canonicalHeaders,
canonicalQueryString + signedHeaders,
"\n" + hashedRequestPayload,
canonicalHeaders + ].join("\n");
"\n" +
signedHeaders +
"\n" +
hashedRequestPayload;
// ************* 步骤 2拼接待签名字符串 ************* // ************* 步骤 2拼接待签名字符串 *************
const algorithm = "TC3-HMAC-SHA256"; const algorithm = "TC3-HMAC-SHA256";
@ -299,7 +202,7 @@ function getHeader(payload: any) {
return { return {
Authorization: authorization, Authorization: authorization,
"Content-Type": "application/json; charset=utf-8", "Content-Type": contentType,
Host: endpoint, Host: endpoint,
"X-TC-Action": action, "X-TC-Action": action,
"X-TC-Timestamp": timestamp.toString(), "X-TC-Timestamp": timestamp.toString(),

View File

@ -22,6 +22,10 @@ import {
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { getMessageTextContent, isVisionModel } from "@/app/utils";
import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -33,17 +37,29 @@ export interface OpenAIListModelResponse {
} }
interface RequestPayload { interface RequestPayload {
messages: { Messages: {
role: "system" | "user" | "assistant"; Role: "system" | "user" | "assistant";
content: string | MultimodalContent[]; Content: string | MultimodalContent[];
}[]; }[];
stream?: boolean; Stream?: boolean;
model: string; Model: string;
temperature: number; Temperature: number;
presence_penalty: number; TopP: number;
frequency_penalty: number; }
top_p: number;
max_tokens?: number; 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.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
),
capitalizeKeys,
);
} else {
return obj;
}
} }
export class HunyuanApi implements LLMApi { export class HunyuanApi implements LLMApi {
@ -76,7 +92,7 @@ export class HunyuanApi implements LLMApi {
} }
extractMessage(res: any) { extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? ""; return res.Choices?.at(0)?.Message?.Content ?? "";
} }
async chat(options: ChatOptions) { async chat(options: ChatOptions) {
@ -94,15 +110,13 @@ export class HunyuanApi implements LLMApi {
}, },
}; };
const requestPayload: RequestPayload = { const requestPayload: RequestPayload = capitalizeKeys({
messages, messages,
stream: options.config.stream, stream: options.config.stream,
model: modelConfig.model, model: modelConfig.model,
temperature: modelConfig.temperature, temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p, top_p: modelConfig.top_p,
}; });
console.log("[Request] Tencent payload: ", requestPayload); console.log("[Request] Tencent payload: ", requestPayload);
@ -213,10 +227,10 @@ export class HunyuanApi implements LLMApi {
const text = msg.data; const text = msg.data;
try { try {
const json = JSON.parse(text); const json = JSON.parse(text);
const choices = json.choices as Array<{ const choices = json.Choices as Array<{
delta: { content: string }; Delta: { Content: string };
}>; }>;
const delta = choices[0]?.delta?.content; const delta = choices[0]?.Delta?.Content;
if (delta) { if (delta) {
remainText += delta; remainText += delta;
} }

View File

@ -54,6 +54,7 @@ import {
Anthropic, Anthropic,
Azure, Azure,
Baidu, Baidu,
Tencent,
ByteDance, ByteDance,
Alibaba, Alibaba,
Google, Google,
@ -964,6 +965,57 @@ export function Settings() {
</> </>
); );
const tencentConfigComponent = accessStore.provider ===
ServiceProvider.Tencent && (
<>
<ListItem
title={Locale.Settings.Access.Tencent.Endpoint.Title}
subTitle={Locale.Settings.Access.Tencent.Endpoint.SubTitle}
>
<input
type="text"
value={accessStore.tencentUrl}
placeholder={Tencent.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.tencentUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Tencent.ApiKey.Title}
subTitle={Locale.Settings.Access.Tencent.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.tencentApiKey}
type="text"
placeholder={Locale.Settings.Access.Tencent.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.tencentApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Tencent.SecretKey.Title}
subTitle={Locale.Settings.Access.Tencent.SecretKey.SubTitle}
>
<PasswordInput
value={accessStore.tencentSecretKey}
type="text"
placeholder={Locale.Settings.Access.Tencent.SecretKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.tencentSecretKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);
const byteDanceConfigComponent = accessStore.provider === const byteDanceConfigComponent = accessStore.provider ===
ServiceProvider.ByteDance && ( ServiceProvider.ByteDance && (
<> <>
@ -1364,6 +1416,7 @@ export function Settings() {
{baiduConfigComponent} {baiduConfigComponent}
{byteDanceConfigComponent} {byteDanceConfigComponent}
{alibabaConfigComponent} {alibabaConfigComponent}
{tencentConfigComponent}
{stabilityConfigComponent} {stabilityConfigComponent}
</> </>
)} )}

View File

@ -371,6 +371,22 @@ const cn = {
SubTitle: "不支持自定义前往.env配置", SubTitle: "不支持自定义前往.env配置",
}, },
}, },
Tencent: {
ApiKey: {
Title: "API Key",
SubTitle: "使用自定义腾讯云API Key",
Placeholder: "Tencent API Key",
},
SecretKey: {
Title: "Secret Key",
SubTitle: "使用自定义腾讯云Secret Key",
Placeholder: "Tencent Secret Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "不支持自定义前往.env配置",
},
},
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "接口密钥", Title: "接口密钥",

View File

@ -354,6 +354,22 @@ const en: LocaleType = {
SubTitle: "not supported, configure in .env", SubTitle: "not supported, configure in .env",
}, },
}, },
Tencent: {
ApiKey: {
Title: "Tencent API Key",
SubTitle: "Use a custom Tencent API Key",
Placeholder: "Tencent API Key",
},
SecretKey: {
Title: "Tencent Secret Key",
SubTitle: "Use a custom Tencent Secret Key",
Placeholder: "Tencent Secret Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "not supported, configure in .env",
},
},
ByteDance: { ByteDance: {
ApiKey: { ApiKey: {
Title: "ByteDance API Key", Title: "ByteDance API Key",

View File

@ -39,6 +39,10 @@ const DEFAULT_ALIBABA_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/alibaba" ? DEFAULT_API_HOST + "/api/proxy/alibaba"
: ApiPath.Alibaba; : ApiPath.Alibaba;
const DEFAULT_TENCENT_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/tencent"
: ApiPath.Tencent;
const DEFAULT_STABILITY_URL = isApp const DEFAULT_STABILITY_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/stability" ? DEFAULT_API_HOST + "/api/proxy/stability"
: ApiPath.Stability; : ApiPath.Stability;
@ -87,7 +91,7 @@ const DEFAULT_ACCESS_STATE = {
stabilityApiKey: "", stabilityApiKey: "",
// tencent // tencent
tencentUrl: "", tencentUrl: DEFAULT_TENCENT_URL,
tencentSecretKey: "", tencentSecretKey: "",
tencentSecretId: "", tencentSecretId: "",