hotfix for tencent sign
This commit is contained in:
parent
a024980c03
commit
f85ec95877
|
@ -1,3 +1,4 @@
|
|||
"use server";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
TENCENT_BASE_URL,
|
||||
|
@ -10,11 +11,7 @@ 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";
|
||||
import * as crypto from "node:crypto";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
|
@ -47,27 +44,6 @@ async function handle(
|
|||
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();
|
||||
|
||||
|
@ -99,63 +75,22 @@ async function request(req: NextRequest) {
|
|||
|
||||
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 body = await req.text();
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
...getHeader(body),
|
||||
},
|
||||
method: req.method,
|
||||
body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME
|
||||
body,
|
||||
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");
|
||||
|
@ -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 加密
|
||||
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();
|
||||
}
|
||||
function sha256(message: any, secret = "", encoding?: string) {
|
||||
return crypto.createHmac("sha256", secret).update(message).digest(encoding);
|
||||
}
|
||||
|
||||
// 使用 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();
|
||||
}
|
||||
return crypto.createHash("sha256").update(message).digest(encoding);
|
||||
}
|
||||
|
||||
function getDate(timestamp: number) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const year = date.getUTCFullYear();
|
||||
|
@ -238,10 +144,11 @@ function getHeader(payload: any) {
|
|||
|
||||
const hashedRequestPayload = getHash(payload);
|
||||
const httpRequestMethod = "POST";
|
||||
const contentType = "application/json";
|
||||
const canonicalUri = "/";
|
||||
const canonicalQueryString = "";
|
||||
const canonicalHeaders =
|
||||
"content-type:application/json; charset=utf-8\n" +
|
||||
`content-type:${contentType}\n` +
|
||||
"host:" +
|
||||
endpoint +
|
||||
"\n" +
|
||||
|
@ -250,18 +157,14 @@ function getHeader(payload: any) {
|
|||
"\n";
|
||||
const signedHeaders = "content-type;host;x-tc-action";
|
||||
|
||||
const canonicalRequest =
|
||||
httpRequestMethod +
|
||||
"\n" +
|
||||
canonicalUri +
|
||||
"\n" +
|
||||
canonicalQueryString +
|
||||
"\n" +
|
||||
canonicalHeaders +
|
||||
"\n" +
|
||||
signedHeaders +
|
||||
"\n" +
|
||||
hashedRequestPayload;
|
||||
const canonicalRequest = [
|
||||
httpRequestMethod,
|
||||
canonicalUri,
|
||||
canonicalQueryString,
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
hashedRequestPayload,
|
||||
].join("\n");
|
||||
|
||||
// ************* 步骤 2:拼接待签名字符串 *************
|
||||
const algorithm = "TC3-HMAC-SHA256";
|
||||
|
@ -299,7 +202,7 @@ function getHeader(payload: any) {
|
|||
|
||||
return {
|
||||
Authorization: authorization,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Type": contentType,
|
||||
Host: endpoint,
|
||||
"X-TC-Action": action,
|
||||
"X-TC-Timestamp": timestamp.toString(),
|
||||
|
|
|
@ -22,6 +22,10 @@ import {
|
|||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
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 {
|
||||
object: string;
|
||||
|
@ -33,17 +37,29 @@ export interface OpenAIListModelResponse {
|
|||
}
|
||||
|
||||
interface RequestPayload {
|
||||
messages: {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | MultimodalContent[];
|
||||
Messages: {
|
||||
Role: "system" | "user" | "assistant";
|
||||
Content: string | MultimodalContent[];
|
||||
}[];
|
||||
stream?: boolean;
|
||||
model: string;
|
||||
temperature: number;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
Stream?: boolean;
|
||||
Model: string;
|
||||
Temperature: number;
|
||||
TopP: 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 {
|
||||
|
@ -76,7 +92,7 @@ export class HunyuanApi implements LLMApi {
|
|||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
return res.Choices?.at(0)?.Message?.Content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
|
@ -94,15 +110,13 @@ export class HunyuanApi implements LLMApi {
|
|||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
const requestPayload: RequestPayload = capitalizeKeys({
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
};
|
||||
});
|
||||
|
||||
console.log("[Request] Tencent payload: ", requestPayload);
|
||||
|
||||
|
@ -213,10 +227,10 @@ export class HunyuanApi implements LLMApi {
|
|||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: { content: string };
|
||||
const choices = json.Choices as Array<{
|
||||
Delta: { Content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.delta?.content;
|
||||
const delta = choices[0]?.Delta?.Content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import {
|
|||
Anthropic,
|
||||
Azure,
|
||||
Baidu,
|
||||
Tencent,
|
||||
ByteDance,
|
||||
Alibaba,
|
||||
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 ===
|
||||
ServiceProvider.ByteDance && (
|
||||
<>
|
||||
|
@ -1364,6 +1416,7 @@ export function Settings() {
|
|||
{baiduConfigComponent}
|
||||
{byteDanceConfigComponent}
|
||||
{alibabaConfigComponent}
|
||||
{tencentConfigComponent}
|
||||
{stabilityConfigComponent}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -371,6 +371,22 @@ const cn = {
|
|||
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: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
|
|
|
@ -354,6 +354,22 @@ const en: LocaleType = {
|
|||
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: {
|
||||
ApiKey: {
|
||||
Title: "ByteDance API Key",
|
||||
|
|
|
@ -39,6 +39,10 @@ const DEFAULT_ALIBABA_URL = isApp
|
|||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||
: ApiPath.Alibaba;
|
||||
|
||||
const DEFAULT_TENCENT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
|
||||
const DEFAULT_STABILITY_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||
: ApiPath.Stability;
|
||||
|
@ -87,7 +91,7 @@ const DEFAULT_ACCESS_STATE = {
|
|||
stabilityApiKey: "",
|
||||
|
||||
// tencent
|
||||
tencentUrl: "",
|
||||
tencentUrl: DEFAULT_TENCENT_URL,
|
||||
tencentSecretKey: "",
|
||||
tencentSecretId: "",
|
||||
|
||||
|
|
Loading…
Reference in New Issue