Browse Source

hotfix for tencent sign

lloydzhou 1 year ago
parent
commit
f85ec95877

+ 19 - 116
app/api/tencent/[...path]/route.ts

@@ -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(),

+ 32 - 18
app/client/platforms/tencent.ts

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

+ 53 - 0
app/components/settings.tsx

@@ -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}
                 </>
               )}

+ 16 - 0
app/locales/cn.ts

@@ -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: "接口密钥",

+ 16 - 0
app/locales/en.ts

@@ -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",

+ 5 - 1
app/store/access.ts

@@ -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: "",