From 9fdf7ef78fc2511358cd961a61d1827fae5fc21f Mon Sep 17 00:00:00 2001 From: yangyongju <1243354771@qq.com> Date: Mon, 12 Aug 2024 17:36:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=20deepseek=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth.ts | 3 + app/api/common.ts | 2 + app/api/deepseek.ts | 132 ++++++++++++++++ app/client/api.ts | 15 +- app/client/platforms/deepseek.ts | 262 +++++++++++++++++++++++++++++++ app/components/model-config.tsx | 1 + app/config/server.ts | 9 +- app/constant.ts | 28 +++- app/layout.tsx | 6 +- app/locales/cn.ts | 3 +- app/store/access.ts | 12 +- app/store/update.ts | 4 +- app/utils/cloudflare.ts | 3 + package.json | 2 +- public/site.webmanifest | 4 +- src-tauri/tauri.conf.json | 8 +- 16 files changed, 473 insertions(+), 21 deletions(-) create mode 100644 app/api/deepseek.ts create mode 100644 app/client/platforms/deepseek.ts diff --git a/app/api/auth.ts b/app/api/auth.ts index 95965ceec..225810256 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -85,6 +85,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { case ModelProvider.Qwen: systemApiKey = serverConfig.alibabaApiKey; break; + case ModelProvider.Deepseek: + systemApiKey = serverConfig.deepseekApiKey; + break; case ModelProvider.Moonshot: systemApiKey = serverConfig.moonshotApiKey; break; diff --git a/app/api/common.ts b/app/api/common.ts index 24453dd96..8ccae9ad6 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -97,6 +97,8 @@ export async function requestOpenai(req: NextRequest) { } const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`); + console.log("baseUrl", baseUrl); + console.log("path", path); console.log("fetchUrl", fetchUrl); const fetchOptions: RequestInit = { headers: { diff --git a/app/api/deepseek.ts b/app/api/deepseek.ts new file mode 100644 index 000000000..4825ca94e --- /dev/null +++ b/app/api/deepseek.ts @@ -0,0 +1,132 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + Deepseek, + DEEPSEEK_BASE_URL, + ApiPath, + ModelProvider, + ServiceProvider, +} 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 type { RequestPayload } from "@/app/client/platforms/openai"; + +const serverConfig = getServerSideConfig(); + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Deepseek Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.Deepseek); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[Deepseek] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +async function request(req: NextRequest) { + const controller = new AbortController(); + + // alibaba use base url or just remove the path + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Deepseek, ""); + console.log("req.nextUrl.pathname", `${req.nextUrl.pathname}`); + + let baseUrl = serverConfig.deepseekApiUrl || DEEPSEEK_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}`; + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + Authorization: req.headers.get("Authorization") ?? "", + "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable", + }, + method: req.method, + body: req.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.Deepseek as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[Deepseek] filter`, e); + } + } + 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); + } +} diff --git a/app/client/api.ts b/app/client/api.ts index 98202c4db..8ca035b24 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -12,6 +12,7 @@ import { ClaudeApi } from "./platforms/anthropic"; import { ErnieApi } from "./platforms/baidu"; import { DoubaoApi } from "./platforms/bytedance"; import { QwenApi } from "./platforms/alibaba"; +import { DeepseekApi } from "./platforms/deepseek"; import { HunyuanApi } from "./platforms/tencent"; import { MoonshotApi } from "./platforms/moonshot"; import { SparkApi } from "./platforms/iflytek"; @@ -123,6 +124,9 @@ export class ClientApi { case ModelProvider.Qwen: this.llm = new QwenApi(); break; + case ModelProvider.Deepseek: + this.llm = new DeepseekApi(); + break; case ModelProvider.Hunyuan: this.llm = new HunyuanApi(); break; @@ -214,6 +218,8 @@ export function getHeaders() { const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; + const isDeepseek = modelConfig.providerName === ServiceProvider.Deepseek; + const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot; const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek; const isEnabledAccessControl = accessStore.enabledAccessControl(); @@ -227,6 +233,8 @@ export function getHeaders() { ? accessStore.bytedanceApiKey : isAlibaba ? accessStore.alibabaApiKey + : isDeepseek + ? accessStore.deepseekApiKey : isMoonshot ? accessStore.moonshotApiKey : isIflytek @@ -241,6 +249,7 @@ export function getHeaders() { isBaidu, isByteDance, isAlibaba, + isDeepseek, isMoonshot, isIflytek, apiKey, @@ -268,7 +277,9 @@ export function getHeaders() { const authHeader = getAuthHeader(); const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic); - + console.log("bearerToken", bearerToken); + // TODO: remove this 更换 Deepseek 的 api key + headers["Authorization"] = `Bearer sk-c8e0505e462f49068758ebcc4331c9ee`; if (bearerToken) { headers[authHeader] = bearerToken; } else if (isEnabledAccessControl && validString(accessStore.accessCode)) { @@ -292,6 +303,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.Doubao); case ServiceProvider.Alibaba: return new ClientApi(ModelProvider.Qwen); + case ServiceProvider.Deepseek: + return new ClientApi(ModelProvider.Deepseek); case ServiceProvider.Tencent: return new ClientApi(ModelProvider.Hunyuan); case ServiceProvider.Moonshot: diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts new file mode 100644 index 000000000..0db844a1b --- /dev/null +++ b/app/client/platforms/deepseek.ts @@ -0,0 +1,262 @@ +"use client"; +import { + ApiPath, + Deepseek, + DEEPSEEK_BASE_URL, + 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 } from "@/app/utils"; + +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + +interface RequestInput { + role: "system" | "user" | "assistant"; + content: string | MultimodalContent[]; +} +[]; +interface RequestParam { + result_format: string; + incremental_output?: boolean; + temperature: number; + repetition_penalty?: number; + top_p: number; + max_tokens?: number; +} +interface RequestPayload { + model: string; + messages: RequestInput[]; + stream?: Boolean; +} + +export class DeepseekApi implements LLMApi { + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.alibabaUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + baseUrl = isApp ? DEEPSEEK_BASE_URL : ApiPath.Deepseek; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Deepseek)) { + baseUrl = "https://" + baseUrl; + } + + console.log("[Proxy Endpoint] ", baseUrl, path); + + return [baseUrl, path].join("/"); + } + + extractMessage(res: any) { + return res?.choices?.at(0)?.message?.content ?? ""; + } + + async chat(options: ChatOptions) { + const messages: RequestInput[] = options.messages.map((v) => ({ + role: v.role, + content: getMessageTextContent(v), + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const shouldStream = !!options.config.stream; + // const shouldStream = false; + const requestPayload: RequestPayload = { + model: modelConfig.model, + messages, + stream: shouldStream, + }; + + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(Deepseek.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( + "[Deepseek] 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) { + console.log("msg", msg); + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + console.log("json", json); + + 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(); + console.log("resJson", resJson); + 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 { + return []; + } +} +export { Deepseek }; diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 6ce25f664..d0abbef0b 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -11,6 +11,7 @@ export function ModelConfigList(props: { updateConfig: (updater: (config: ModelConfig) => void) => void; }) { const allModels = useAllModels(); + const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; return ( diff --git a/app/config/server.ts b/app/config/server.ts index 5bfa2c2df..5e34dc4b5 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -56,7 +56,9 @@ declare global { // alibaba only ALIBABA_URL?: string; ALIBABA_API_KEY?: string; - + // deepseek only + DEEPSEEK_API_KEY?: string; + DEEPSEEK_URL?: string; // tencent only TENCENT_URL?: string; TENCENT_SECRET_KEY?: string; @@ -141,6 +143,7 @@ export const getServerSideConfig = () => { const isBaidu = !!process.env.BAIDU_API_KEY; const isBytedance = !!process.env.BYTEDANCE_API_KEY; const isAlibaba = !!process.env.ALIBABA_API_KEY; + const isDeepseek = !!process.env.DEEPSEEK_API_KEY; const isMoonshot = !!process.env.MOONSHOT_API_KEY; const isIflytek = !!process.env.IFLYTEK_API_KEY; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; @@ -191,6 +194,10 @@ export const getServerSideConfig = () => { alibabaUrl: process.env.ALIBABA_URL, alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), + isDeepseek, + deepseekApiKey: getApiKey(process.env.DEEPSEEK_API_KEY), + deepseekApiUrl: process.env.DEEPSEEK_URL, + isTencent, tencentUrl: process.env.TENCENT_URL, tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY), diff --git a/app/constant.ts b/app/constant.ts index d21f18f5a..0f5360e8d 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -30,7 +30,7 @@ export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com"; export const CACHE_URL_PREFIX = "/api/cache"; export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`; - +export const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; export enum Path { Home = "/", Chat = "/chat", @@ -42,7 +42,7 @@ export enum Path { SdNew = "/sd-new", Artifacts = "/artifacts", } - +// API路径 export enum ApiPath { Cors = "", Azure = "/api/azure", @@ -57,6 +57,7 @@ export enum ApiPath { Iflytek = "/api/iflytek", Stability = "/api/stability", Artifacts = "/api/artifacts", + Deepseek = "/api/deepseek", } export enum SlotID { @@ -99,7 +100,7 @@ export const STORAGE_KEY = "chatgpt-next-web"; export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; - +// 服务提供商 export enum ServiceProvider { OpenAI = "OpenAI", Azure = "Azure", @@ -112,6 +113,7 @@ export enum ServiceProvider { Moonshot = "Moonshot", Stability = "Stability", Iflytek = "Iflytek", + Deepseek = "Deepseek", } // Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings @@ -123,6 +125,7 @@ export enum GoogleSafetySettingsThreshold { BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE", } +// 模型提供商 export enum ModelProvider { Stability = "Stability", GPT = "GPT", @@ -134,6 +137,7 @@ export enum ModelProvider { Hunyuan = "Hunyuan", Moonshot = "Moonshot", Iflytek = "Iflytek", + Deepseek = "Deepseek", } export const Stability = { @@ -200,7 +204,10 @@ export const Alibaba = { ExampleEndpoint: ALIBABA_BASE_URL, ChatPath: "v1/services/aigc/text-generation/generation", }; - +export const Deepseek = { + ExampleEndpoint: DEEPSEEK_BASE_URL, + ChatPath: "v1/chat/completions", +}; export const Tencent = { ExampleEndpoint: TENCENT_BASE_URL, }; @@ -323,7 +330,7 @@ const alibabaModes = [ "qwen-max-0107", "qwen-max-longcontext", ]; - +const DeepseekModels = ["deepseek-chat", "deepseek-coder"]; const tencentModels = [ "hunyuan-pro", "hunyuan-standard", @@ -456,6 +463,17 @@ export const DEFAULT_MODELS = [ sorted: 10, }, })), + ...DeepseekModels.map((name) => ({ + name, + available: true, + sorted: seq++, + provider: { + id: "deepseek", + providerName: "Deepseek", + providerType: "deepseek", + sorted: 11, + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/layout.tsx b/app/layout.tsx index eda5f43dd..1c985ebbf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,10 +10,10 @@ import { GoogleTagManager } from "@next/third-parties/google"; const serverConfig = getServerSideConfig(); export const metadata: Metadata = { - title: "NextChat", - description: "Your personal ChatGPT Chat Bot.", + title: "清明上河图小助手", + description: "清明上河图小助手 description.", appleWebApp: { - title: "NextChat", + title: "清明上河图小助手", statusBarStyle: "default", }, }; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 02b3a9d40..e2ea40725 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -484,7 +484,8 @@ const cn = { }, Store: { DefaultTopic: "新的聊天", - BotHello: "有什么可以帮你的吗", + BotHello: + "我是《清明上河图》智能小助手,您可以从侧边栏选择任务和配置参数开始,有什么可以帮您的?", Error: "出错了,稍后重试吧", Prompt: { History: (content: string) => "这是历史聊天总结作为前情提要:" + content, diff --git a/app/store/access.ts b/app/store/access.ts index b89b080d8..7edb13e47 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -39,6 +39,10 @@ const DEFAULT_ALIBABA_URL = isApp ? DEFAULT_API_HOST + "/api/proxy/alibaba" : ApiPath.Alibaba; +const DEFAULT_DEEPSEEK_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/deepseek" + : ApiPath.Deepseek; + const DEFAULT_TENCENT_URL = isApp ? DEFAULT_API_HOST + "/api/proxy/tencent" : ApiPath.Tencent; @@ -93,7 +97,9 @@ const DEFAULT_ACCESS_STATE = { // alibaba alibabaUrl: DEFAULT_ALIBABA_URL, alibabaApiKey: "", - + // DEEPSEEK + deepseekUrl: DEFAULT_DEEPSEEK_URL, + deepseekApiKey: "", // moonshot moonshotUrl: DEFAULT_MOONSHOT_URL, moonshotApiKey: "", @@ -159,6 +165,9 @@ export const useAccessStore = createPersistStore( isValidAlibaba() { return ensure(get(), ["alibabaApiKey"]); }, + isValidDeepseekApiKey() { + return ensure(get(), ["deepseekApiKey"]); + }, isValidTencent() { return ensure(get(), ["tencentSecretKey", "tencentSecretId"]); @@ -183,6 +192,7 @@ export const useAccessStore = createPersistStore( this.isValidBaidu() || this.isValidByteDance() || this.isValidAlibaba() || + this.isValidDeepseekApiKey() || this.isValidTencent || this.isValidMoonshot() || this.isValidIflytek() || diff --git a/app/store/update.ts b/app/store/update.ts index 7253caffc..f09214459 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -106,7 +106,7 @@ export const useUpdateStore = createPersistStore( if (version === remoteId) { // Show a notification using Tauri window.__TAURI__?.notification.sendNotification({ - title: "NextChat", + title: "清明上河图小助手", body: `${Locale.Settings.Update.IsLatest}`, icon: `${ChatGptIcon.src}`, sound: "Default", @@ -116,7 +116,7 @@ export const useUpdateStore = createPersistStore( Locale.Settings.Update.FoundUpdate(`${remoteId}`); // Show a notification for the new version using Tauri window.__TAURI__?.notification.sendNotification({ - title: "NextChat", + title: "清明上河图小助手", body: updateMessage, icon: `${ChatGptIcon.src}`, sound: "Default", diff --git a/app/utils/cloudflare.ts b/app/utils/cloudflare.ts index 5094640fc..c975166a3 100644 --- a/app/utils/cloudflare.ts +++ b/app/utils/cloudflare.ts @@ -22,5 +22,8 @@ export function cloudflareAIGatewayUrl(fetchUrl: string) { } // TODO: Amazon Bedrock, Groq, HuggingFace... } + if ("api.deepseek.com" == paths[2]) { + return paths.slice(0, 3).concat(paths.slice(-3)).join("/"); + } return fetchUrl; } diff --git a/package.json b/package.json index eb0a5ef67..e62a78f5a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "nextchat", + "name": "111", "private": false, "license": "mit", "scripts": { diff --git a/public/site.webmanifest b/public/site.webmanifest index cf77f68e4..0ff3ca84a 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "NextChat", - "short_name": "NextChat", + "name": "清明上河图小助手", + "short_name": "清明上河图小助手", "icons": [ { "src": "/android-chrome-192x192.png", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 245254eff..f7264b5c6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ "withGlobalTauri": true }, "package": { - "productName": "NextChat", + "productName": "清明上河图小助手", "version": "2.14.1" }, "tauri": { @@ -68,7 +68,7 @@ "icons/icon.ico" ], "identifier": "com.yida.chatgpt.next.web", - "longDescription": "NextChat is a cross-platform ChatGPT client, including Web/Win/Linux/OSX/PWA.", + "longDescription": "清明上河图小助手 is a cross-platform ChatGPT client, including Web/Win/Linux/OSX/PWA.", "macOS": { "entitlements": null, "exceptionDomain": "", @@ -77,7 +77,7 @@ "signingIdentity": null }, "resources": [], - "shortDescription": "NextChat App", + "shortDescription": "清明上河图小助手 App", "targets": "all", "windows": { "certificateThumbprint": null, @@ -105,7 +105,7 @@ "fullscreen": false, "height": 600, "resizable": true, - "title": "NextChat", + "title": "清明上河图小助手", "width": 960, "hiddenTitle": true, "titleBarStyle": "Overlay"