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 { 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(),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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: "接口密钥",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue