Merge pull request #5157 from ConnectAI-E/feature/tencent
Feature/tencent
This commit is contained in:
commit
b3219f57c8
|
@ -0,0 +1,124 @@
|
||||||
|
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 { getHeader } from "@/app/utils/tencent";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
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();
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = baseUrl;
|
||||||
|
|
||||||
|
const body = await req.text();
|
||||||
|
const headers = await getHeader(
|
||||||
|
body,
|
||||||
|
serverConfig.tencentSecretId as string,
|
||||||
|
serverConfig.tencentSecretKey as string,
|
||||||
|
);
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { ClaudeApi } from "./platforms/anthropic";
|
||||||
import { ErnieApi } from "./platforms/baidu";
|
import { ErnieApi } from "./platforms/baidu";
|
||||||
import { DoubaoApi } from "./platforms/bytedance";
|
import { DoubaoApi } from "./platforms/bytedance";
|
||||||
import { QwenApi } from "./platforms/alibaba";
|
import { QwenApi } from "./platforms/alibaba";
|
||||||
|
import { HunyuanApi } from "./platforms/tencent";
|
||||||
import { MoonshotApi } from "./platforms/moonshot";
|
import { MoonshotApi } from "./platforms/moonshot";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
|
@ -117,6 +118,8 @@ export class ClientApi {
|
||||||
break;
|
break;
|
||||||
case ModelProvider.Qwen:
|
case ModelProvider.Qwen:
|
||||||
this.llm = new QwenApi();
|
this.llm = new QwenApi();
|
||||||
|
case ModelProvider.Hunyuan:
|
||||||
|
this.llm = new HunyuanApi();
|
||||||
break;
|
break;
|
||||||
case ModelProvider.Moonshot:
|
case ModelProvider.Moonshot:
|
||||||
this.llm = new MoonshotApi();
|
this.llm = new MoonshotApi();
|
||||||
|
@ -275,6 +278,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||||
return new ClientApi(ModelProvider.Doubao);
|
return new ClientApi(ModelProvider.Doubao);
|
||||||
case ServiceProvider.Alibaba:
|
case ServiceProvider.Alibaba:
|
||||||
return new ClientApi(ModelProvider.Qwen);
|
return new ClientApi(ModelProvider.Qwen);
|
||||||
|
case ServiceProvider.Tencent:
|
||||||
|
return new ClientApi(ModelProvider.Hunyuan);
|
||||||
case ServiceProvider.Moonshot:
|
case ServiceProvider.Moonshot:
|
||||||
return new ClientApi(ModelProvider.Moonshot);
|
return new ClientApi(ModelProvider.Moonshot);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
"use client";
|
||||||
|
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||||
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatOptions,
|
||||||
|
getHeaders,
|
||||||
|
LLMApi,
|
||||||
|
LLMModel,
|
||||||
|
MultimodalContent,
|
||||||
|
} from "../api";
|
||||||
|
import Locale from "../../locales";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
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;
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
root: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestPayload {
|
||||||
|
Messages: {
|
||||||
|
Role: "system" | "user" | "assistant";
|
||||||
|
Content: string | MultimodalContent[];
|
||||||
|
}[];
|
||||||
|
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 {
|
||||||
|
path(): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.tencentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
baseUrl = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||||
|
: ApiPath.Tencent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl);
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
return res.Choices?.at(0)?.Message?.Content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions) {
|
||||||
|
const visionModel = isVisionModel(options.config.model);
|
||||||
|
const messages = options.messages.map((v) => ({
|
||||||
|
role: v.role,
|
||||||
|
content: visionModel ? v.content : getMessageTextContent(v),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = capitalizeKeys({
|
||||||
|
model: modelConfig.model,
|
||||||
|
messages,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
stream: options.config.stream,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Request] Tencent payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path();
|
||||||
|
const chatPayload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
// animate response to make it looks smooth
|
||||||
|
function animateResponseText() {
|
||||||
|
if (finished || controller.signal.aborted) {
|
||||||
|
responseText += remainText;
|
||||||
|
console.log("[Response Animation] finished");
|
||||||
|
if (responseText?.length === 0) {
|
||||||
|
options.onError?.(new Error("empty response from server"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
options.onUpdate?.(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start animaion
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
options.onFinish(responseText + remainText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
|
fetchEventSource(chatPath, {
|
||||||
|
...chatPayload,
|
||||||
|
async onopen(res) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log(
|
||||||
|
"[Tencent] request response content type: ",
|
||||||
|
contentType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
responseText = await res.clone().text();
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [responseText];
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]" || finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.Choices as Array<{
|
||||||
|
Delta: { Content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.Delta?.Content;
|
||||||
|
if (delta) {
|
||||||
|
remainText += delta;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
options.onError?.(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = this.extractMessage(resJson);
|
||||||
|
options.onFinish(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Request] failed to make a chat request", e);
|
||||||
|
options.onError?.(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async usage() {
|
||||||
|
return {
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async models(): Promise<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ import {
|
||||||
Anthropic,
|
Anthropic,
|
||||||
Azure,
|
Azure,
|
||||||
Baidu,
|
Baidu,
|
||||||
|
Tencent,
|
||||||
ByteDance,
|
ByteDance,
|
||||||
Alibaba,
|
Alibaba,
|
||||||
Moonshot,
|
Moonshot,
|
||||||
|
@ -965,6 +966,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.tencentSecretId}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Tencent.ApiKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.tencentSecretId = 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 && (
|
||||||
<>
|
<>
|
||||||
|
@ -1404,6 +1456,7 @@ export function Settings() {
|
||||||
{baiduConfigComponent}
|
{baiduConfigComponent}
|
||||||
{byteDanceConfigComponent}
|
{byteDanceConfigComponent}
|
||||||
{alibabaConfigComponent}
|
{alibabaConfigComponent}
|
||||||
|
{tencentConfigComponent}
|
||||||
{moonshotConfigComponent}
|
{moonshotConfigComponent}
|
||||||
{stabilityConfigComponent}
|
{stabilityConfigComponent}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -57,6 +57,11 @@ declare global {
|
||||||
ALIBABA_URL?: string;
|
ALIBABA_URL?: string;
|
||||||
ALIBABA_API_KEY?: string;
|
ALIBABA_API_KEY?: string;
|
||||||
|
|
||||||
|
// tencent only
|
||||||
|
TENCENT_URL?: string;
|
||||||
|
TENCENT_SECRET_KEY?: string;
|
||||||
|
TENCENT_SECRET_ID?: string;
|
||||||
|
|
||||||
// moonshot only
|
// moonshot only
|
||||||
MOONSHOT_URL?: string;
|
MOONSHOT_URL?: string;
|
||||||
MOONSHOT_API_KEY?: string;
|
MOONSHOT_API_KEY?: string;
|
||||||
|
@ -120,6 +125,7 @@ export const getServerSideConfig = () => {
|
||||||
const isAzure = !!process.env.AZURE_URL;
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
const isTencent = !!process.env.TENCENT_API_KEY;
|
||||||
|
|
||||||
const isBaidu = !!process.env.BAIDU_API_KEY;
|
const isBaidu = !!process.env.BAIDU_API_KEY;
|
||||||
const isBytedance = !!process.env.BYTEDANCE_API_KEY;
|
const isBytedance = !!process.env.BYTEDANCE_API_KEY;
|
||||||
|
@ -173,6 +179,11 @@ export const getServerSideConfig = () => {
|
||||||
alibabaUrl: process.env.ALIBABA_URL,
|
alibabaUrl: process.env.ALIBABA_URL,
|
||||||
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
|
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
|
||||||
|
|
||||||
|
isTencent,
|
||||||
|
tencentUrl: process.env.TENCENT_URL,
|
||||||
|
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
|
||||||
|
tencentSecretId: process.env.TENCENT_SECRET_ID,
|
||||||
|
|
||||||
isMoonshot,
|
isMoonshot,
|
||||||
moonshotUrl: process.env.MOONSHOT_URL,
|
moonshotUrl: process.env.MOONSHOT_URL,
|
||||||
moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
|
moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
|
||||||
|
|
|
@ -22,6 +22,9 @@ export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`;
|
||||||
export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
|
export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
|
||||||
|
|
||||||
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
||||||
|
|
||||||
|
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||||
|
|
||||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||||
|
|
||||||
export const CACHE_URL_PREFIX = "/api/cache";
|
export const CACHE_URL_PREFIX = "/api/cache";
|
||||||
|
@ -48,6 +51,7 @@ export enum ApiPath {
|
||||||
Baidu = "/api/baidu",
|
Baidu = "/api/baidu",
|
||||||
ByteDance = "/api/bytedance",
|
ByteDance = "/api/bytedance",
|
||||||
Alibaba = "/api/alibaba",
|
Alibaba = "/api/alibaba",
|
||||||
|
Tencent = "/api/tencent",
|
||||||
Moonshot = "/api/moonshot",
|
Moonshot = "/api/moonshot",
|
||||||
Stability = "/api/stability",
|
Stability = "/api/stability",
|
||||||
Artifacts = "/api/artifacts",
|
Artifacts = "/api/artifacts",
|
||||||
|
@ -102,6 +106,7 @@ export enum ServiceProvider {
|
||||||
Baidu = "Baidu",
|
Baidu = "Baidu",
|
||||||
ByteDance = "ByteDance",
|
ByteDance = "ByteDance",
|
||||||
Alibaba = "Alibaba",
|
Alibaba = "Alibaba",
|
||||||
|
Tencent = "Tencent",
|
||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
}
|
}
|
||||||
|
@ -123,6 +128,7 @@ export enum ModelProvider {
|
||||||
Ernie = "Ernie",
|
Ernie = "Ernie",
|
||||||
Doubao = "Doubao",
|
Doubao = "Doubao",
|
||||||
Qwen = "Qwen",
|
Qwen = "Qwen",
|
||||||
|
Hunyuan = "Hunyuan",
|
||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +193,10 @@ export const Alibaba = {
|
||||||
ChatPath: "v1/services/aigc/text-generation/generation",
|
ChatPath: "v1/services/aigc/text-generation/generation",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Tencent = {
|
||||||
|
ExampleEndpoint: TENCENT_BASE_URL,
|
||||||
|
};
|
||||||
|
|
||||||
export const Moonshot = {
|
export const Moonshot = {
|
||||||
ExampleEndpoint: MOONSHOT_BASE_URL,
|
ExampleEndpoint: MOONSHOT_BASE_URL,
|
||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
|
@ -298,6 +308,16 @@ const alibabaModes = [
|
||||||
"qwen-max-longcontext",
|
"qwen-max-longcontext",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tencentModels = [
|
||||||
|
"hunyuan-pro",
|
||||||
|
"hunyuan-standard",
|
||||||
|
"hunyuan-lite",
|
||||||
|
"hunyuan-role",
|
||||||
|
"hunyuan-functioncall",
|
||||||
|
"hunyuan-code",
|
||||||
|
"hunyuan-vision",
|
||||||
|
];
|
||||||
|
|
||||||
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
|
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
|
||||||
|
|
||||||
export const DEFAULT_MODELS = [
|
export const DEFAULT_MODELS = [
|
||||||
|
@ -364,6 +384,15 @@ export const DEFAULT_MODELS = [
|
||||||
providerType: "alibaba",
|
providerType: "alibaba",
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
...tencentModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
provider: {
|
||||||
|
id: "tencent",
|
||||||
|
providerName: "Tencent",
|
||||||
|
providerType: "tencent",
|
||||||
|
},
|
||||||
|
})),
|
||||||
...moonshotModes.map((name) => ({
|
...moonshotModes.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
|
|
@ -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_MOONSHOT_URL = isApp
|
const DEFAULT_MOONSHOT_URL = isApp
|
||||||
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
||||||
: ApiPath.Moonshot;
|
: ApiPath.Moonshot;
|
||||||
|
@ -94,6 +98,11 @@ const DEFAULT_ACCESS_STATE = {
|
||||||
stabilityUrl: DEFAULT_STABILITY_URL,
|
stabilityUrl: DEFAULT_STABILITY_URL,
|
||||||
stabilityApiKey: "",
|
stabilityApiKey: "",
|
||||||
|
|
||||||
|
// tencent
|
||||||
|
tencentUrl: DEFAULT_TENCENT_URL,
|
||||||
|
tencentSecretKey: "",
|
||||||
|
tencentSecretId: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
|
@ -142,6 +151,10 @@ export const useAccessStore = createPersistStore(
|
||||||
return ensure(get(), ["alibabaApiKey"]);
|
return ensure(get(), ["alibabaApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidTencent() {
|
||||||
|
return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
|
||||||
|
},
|
||||||
|
|
||||||
isValidMoonshot() {
|
isValidMoonshot() {
|
||||||
return ensure(get(), ["moonshotApiKey"]);
|
return ensure(get(), ["moonshotApiKey"]);
|
||||||
},
|
},
|
||||||
|
@ -158,6 +171,7 @@ export const useAccessStore = createPersistStore(
|
||||||
this.isValidBaidu() ||
|
this.isValidBaidu() ||
|
||||||
this.isValidByteDance() ||
|
this.isValidByteDance() ||
|
||||||
this.isValidAlibaba() ||
|
this.isValidAlibaba() ||
|
||||||
|
this.isValidTencent ||
|
||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { createHash, createHmac } from "node:crypto";
|
||||||
|
// 使用 SHA-256 和 secret 进行 HMAC 加密
|
||||||
|
function sha256(message: any, secret = "", encoding?: string) {
|
||||||
|
return createHmac("sha256", secret)
|
||||||
|
.update(message)
|
||||||
|
.digest(encoding as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 SHA-256 进行哈希
|
||||||
|
function getHash(message: any, encoding = "hex") {
|
||||||
|
return createHash("sha256")
|
||||||
|
.update(message)
|
||||||
|
.digest(encoding as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHeader(
|
||||||
|
payload: any,
|
||||||
|
SECRET_ID: string,
|
||||||
|
SECRET_KEY: string,
|
||||||
|
) {
|
||||||
|
// https://cloud.tencent.com/document/api/1729/105701
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -28,6 +28,7 @@
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^10.6.1",
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react": "^18.2.70",
|
"@types/react": "^18.2.70",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1697,6 +1697,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
||||||
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
|
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
|
||||||
|
|
||||||
|
"@types/lodash-es@^4.17.12":
|
||||||
|
version "4.17.12"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||||
|
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.17.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612"
|
||||||
|
integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
"@types/mdast@^3.0.0":
|
||||||
version "3.0.11"
|
version "3.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
||||||
|
|
Loading…
Reference in New Issue