mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-02-23 17:59:49 +08:00
wip: tencent
This commit is contained in:
parent
7218f13783
commit
d726c71141
309
app/api/tencent/[...path]/route.ts
Normal file
309
app/api/tencent/[...path]/route.ts
Normal file
@ -0,0 +1,309 @@
|
||||
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 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";
|
||||
|
||||
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 = "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();
|
||||
|
||||
// tencent just use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(
|
||||
ApiPath.Tencent + "/" + Tencent.ChatPath,
|
||||
"",
|
||||
);
|
||||
|
||||
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
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 fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
...getHeader(body),
|
||||
},
|
||||
method: req.method,
|
||||
body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME
|
||||
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");
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 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();
|
||||
}
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
|
||||
function getHeader(payload: any) {
|
||||
// https://cloud.tencent.com/document/api/1729/105701
|
||||
// 密钥参数
|
||||
const SECRET_ID = serverConfig.tencentSecretId;
|
||||
const SECRET_KEY = serverConfig.tencentSecretKey;
|
||||
|
||||
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 canonicalUri = "/";
|
||||
const canonicalQueryString = "";
|
||||
const canonicalHeaders =
|
||||
"content-type:application/json; charset=utf-8\n" +
|
||||
"host:" +
|
||||
endpoint +
|
||||
"\n" +
|
||||
"x-tc-action:" +
|
||||
action.toLowerCase() +
|
||||
"\n";
|
||||
const signedHeaders = "content-type;host;x-tc-action";
|
||||
|
||||
const canonicalRequest =
|
||||
httpRequestMethod +
|
||||
"\n" +
|
||||
canonicalUri +
|
||||
"\n" +
|
||||
canonicalQueryString +
|
||||
"\n" +
|
||||
canonicalHeaders +
|
||||
"\n" +
|
||||
signedHeaders +
|
||||
"\n" +
|
||||
hashedRequestPayload;
|
||||
|
||||
// ************* 步骤 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": "application/json; charset=utf-8",
|
||||
Host: endpoint,
|
||||
"X-TC-Action": action,
|
||||
"X-TC-Timestamp": timestamp.toString(),
|
||||
"X-TC-Version": version,
|
||||
"X-TC-Region": region,
|
||||
};
|
||||
}
|
@ -9,6 +9,8 @@ import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
import { ClaudeApi } from "./platforms/anthropic";
|
||||
import { HunyuanApi } from "./platforms/tencent";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
@ -104,6 +106,9 @@ export class ClientApi {
|
||||
case ModelProvider.Claude:
|
||||
this.llm = new ClaudeApi();
|
||||
break;
|
||||
case ModelProvider.Hunyuan:
|
||||
this.llm = new HunyuanApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
@ -220,6 +225,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
return new ClientApi(ModelProvider.GeminiPro);
|
||||
case ServiceProvider.Anthropic:
|
||||
return new ClientApi(ModelProvider.Claude);
|
||||
case ServiceProvider.Tencent:
|
||||
return new ClientApi(ModelProvider.Hunyuan);
|
||||
default:
|
||||
return new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
259
app/client/platforms/tencent.ts
Normal file
259
app/client/platforms/tencent.ts
Normal file
@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
Tencent,
|
||||
} 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";
|
||||
|
||||
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;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export class HunyuanApi implements LLMApi {
|
||||
path(path: string): 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/bytedance"
|
||||
: 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, path].join("/");
|
||||
}
|
||||
|
||||
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 = {
|
||||
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);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(Tencent.ChatPath);
|
||||
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 [];
|
||||
}
|
||||
}
|
@ -35,6 +35,11 @@ declare global {
|
||||
// google tag manager
|
||||
GTM_ID?: string;
|
||||
|
||||
// tencent only
|
||||
TENCENT_URL?: string;
|
||||
TENCENT_SECRET_KEY?: string;
|
||||
TENCENT_SECRET_ID?: string;
|
||||
|
||||
// custom template for preprocessing user input
|
||||
DEFAULT_INPUT_TEMPLATE?: string;
|
||||
}
|
||||
@ -92,6 +97,7 @@ export const getServerSideConfig = () => {
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
const isTencent = !!process.env.TENCENT_API_KEY;
|
||||
|
||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
@ -124,6 +130,11 @@ export const getServerSideConfig = () => {
|
||||
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
|
||||
anthropicUrl: process.env.ANTHROPIC_URL,
|
||||
|
||||
isTencent,
|
||||
tencentUrl: process.env.TENCENT_URL,
|
||||
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
|
||||
tencentSecretId: process.env.TENCENT_SECRET_ID,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
|
@ -14,6 +14,8 @@ export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||
|
||||
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
@ -28,6 +30,7 @@ export enum ApiPath {
|
||||
Azure = "/api/azure",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
Tencent = "/api/tencent",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@ -71,12 +74,14 @@ export enum ServiceProvider {
|
||||
Azure = "Azure",
|
||||
Google = "Google",
|
||||
Anthropic = "Anthropic",
|
||||
Tencent = "Tencent",
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
Hunyuan = "Hunyuan",
|
||||
}
|
||||
|
||||
export const Anthropic = {
|
||||
@ -104,6 +109,10 @@ export const Google = {
|
||||
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
||||
};
|
||||
|
||||
export const Tencent = {
|
||||
ChatPath: "chat/completions",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
@ -173,6 +182,16 @@ const anthropicModels = [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
];
|
||||
|
||||
const tencentModels = [
|
||||
"hunyuan-pro",
|
||||
"hunyuan-standard",
|
||||
"hunyuan-lite",
|
||||
"hunyuan-role",
|
||||
"hunyuan-functioncall",
|
||||
"hunyuan-code",
|
||||
"hunyuan-vision",
|
||||
];
|
||||
|
||||
export const DEFAULT_MODELS = [
|
||||
...openaiModels.map((name) => ({
|
||||
name,
|
||||
@ -210,6 +229,15 @@ export const DEFAULT_MODELS = [
|
||||
providerType: "anthropic",
|
||||
},
|
||||
})),
|
||||
...tencentModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
provider: {
|
||||
id: "tencent",
|
||||
providerName: "Tencent",
|
||||
providerType: "tencent",
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
|
@ -47,6 +47,11 @@ const DEFAULT_ACCESS_STATE = {
|
||||
anthropicApiVersion: "2023-06-01",
|
||||
anthropicUrl: "",
|
||||
|
||||
// tencent
|
||||
tencentUrl: "",
|
||||
tencentSecretKey: "",
|
||||
tencentSecretId: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
@ -83,6 +88,10 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["anthropicApiKey"]);
|
||||
},
|
||||
|
||||
isValidTencent() {
|
||||
return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
@ -92,6 +101,7 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
this.isValidAnthropic() ||
|
||||
this.isValidTencent ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
|
146
package.json
146
package.json
@ -1,73 +1,77 @@
|
||||
{
|
||||
"name": "nextchat",
|
||||
"private": false,
|
||||
"license": "mit",
|
||||
"scripts": {
|
||||
"mask": "npx tsx app/masks/build.ts",
|
||||
"mask:watch": "npx watch 'yarn mask' app/masks",
|
||||
"dev": "yarn run mask:watch & next dev",
|
||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev",
|
||||
"app:dev": "yarn mask:watch & yarn tauri dev",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortaine/fetch-event-source": "^3.0.6",
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"emoji-picker-react": "^4.9.2",
|
||||
"fuse.js": "^7.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"html-to-image": "^1.11.11",
|
||||
"mermaid": "^10.6.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"next": "^14.1.1",
|
||||
"node-fetch": "^3.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.59.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react": "^18.2.70",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/spark-md5": "^3.0.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"tsx": "^4.16.0",
|
||||
"typescript": "5.2.2",
|
||||
"watch": "^1.0.2",
|
||||
"webpack": "^5.88.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
"name": "nextchat",
|
||||
"private": false,
|
||||
"license": "mit",
|
||||
"scripts": {
|
||||
"mask": "npx tsx app/masks/build.ts",
|
||||
"mask:watch": "npx watch 'yarn mask' app/masks",
|
||||
"dev": "yarn run mask:watch & next dev",
|
||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev",
|
||||
"app:dev": "yarn mask:watch & yarn tauri dev",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortaine/fetch-event-source": "^3.0.6",
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"emoji-picker-react": "^4.9.2",
|
||||
"fuse.js": "^7.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"html-to-image": "^1.11.11",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^10.6.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"next": "^14.1.1",
|
||||
"node-fetch": "^3.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.59.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react": "^18.2.70",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/spark-md5": "^3.0.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"tsx": "^4.16.0",
|
||||
"typescript": "5.2.2",
|
||||
"watch": "^1.0.2",
|
||||
"webpack": "^5.88.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
22
yarn.lock
22
yarn.lock
@ -1617,6 +1617,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
|
||||
|
||||
"@types/crypto-js@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
|
||||
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
|
||||
|
||||
"@types/d3-scale-chromatic@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954"
|
||||
@ -1697,6 +1702,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
||||
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.6"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543"
|
||||
integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
||||
@ -2447,6 +2464,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||
|
||||
css-box-model@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||
|
Loading…
Reference in New Issue
Block a user