mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-02-19 04:44:26 +08:00
Compare commits
33 Commits
d830c23dab
...
v2.15.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63ab83c3c8 | ||
|
|
268cf3b606 | ||
|
|
fbc68fa776 | ||
|
|
96273fd75e | ||
|
|
3e63d405c1 | ||
|
|
19b42aac5d | ||
|
|
b67a23200e | ||
|
|
1dac02e4d6 | ||
|
|
acad5b1d08 | ||
|
|
4e9bb51d2f | ||
|
|
c0c8cdbbf3 | ||
|
|
cbdc611b54 | ||
|
|
93ca303b6c | ||
|
|
a925b424a8 | ||
|
|
5b4d423b58 | ||
|
|
6c1cbe120c | ||
|
|
77a58bc4b0 | ||
|
|
8ad63a6c25 | ||
|
|
cd75461f9e | ||
|
|
2bac174e6f | ||
|
|
65f80f81ad | ||
|
|
05e6e4bffb | ||
|
|
fbb66a4a5d | ||
|
|
d51d31a559 | ||
|
|
919ee51dca | ||
|
|
9c577ad9d5 | ||
|
|
953114041b | ||
|
|
cea5b91f96 | ||
|
|
d2984db6e7 | ||
|
|
deb215ccd1 | ||
|
|
0c697e123d | ||
|
|
f5ad51a35e | ||
|
|
4d6b981a54 |
12
README.md
12
README.md
@@ -18,11 +18,11 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
[![MacOS][MacOS-image]][download-url]
|
[![MacOS][MacOS-image]][download-url]
|
||||||
[![Linux][Linux-image]][download-url]
|
[![Linux][Linux-image]][download-url]
|
||||||
|
|
||||||
[NextChatAI](https://nextchat.dev/chat) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
|
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
|
||||||
|
|
||||||
[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||||
|
|
||||||
[saas-url]: https://nextchat.dev/chat
|
[saas-url]: https://nextchat.dev/chat?utm_source=readme
|
||||||
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
|
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
|
||||||
[web-url]: https://app.nextchat.dev/
|
[web-url]: https://app.nextchat.dev/
|
||||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||||
@@ -63,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
|||||||
|
|
||||||
企业版咨询: **business@nextchat.dev**
|
企业版咨询: **business@nextchat.dev**
|
||||||
|
|
||||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -334,9 +334,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
|
|||||||
|
|
||||||
User `-all` to disable all default models, `+all` to enable all default models.
|
User `-all` to disable all default models, `+all` to enable all default models.
|
||||||
|
|
||||||
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
|
For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
|
> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
|
||||||
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
||||||
|
|
||||||
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||||
|
|
||||||
[NextChatAI](https://nextchat.dev/chat) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
@@ -216,9 +216,9 @@ ByteDance Api Url.
|
|||||||
|
|
||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||||
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
||||||
|
|
||||||
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
|
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
|
||||||
|
|
||||||
[NextChatAI](https://nextchat.dev/chat) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
|
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
@@ -207,8 +207,8 @@ ByteDance API の URL。
|
|||||||
|
|
||||||
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
||||||
|
|
||||||
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
||||||
|
|
||||||
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { requestOpenai } from "./common";
|
import { requestOpenai } from "./common";
|
||||||
|
|
||||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
|
||||||
|
|
||||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||||
const config = getServerSideConfig();
|
const config = getServerSideConfig();
|
||||||
@@ -34,7 +34,7 @@ export async function handle(
|
|||||||
|
|
||||||
const subpath = params.path.join("/");
|
const subpath = params.path.join("/");
|
||||||
|
|
||||||
if (!ALLOWD_PATH.has(subpath)) {
|
if (!ALLOWED_PATH.has(subpath)) {
|
||||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||||
const isGoogle = modelConfig.providerName == ServiceProvider.Google;
|
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||||
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
||||||
|
|||||||
@@ -1815,6 +1815,7 @@ function _Chat() {
|
|||||||
{message?.tools?.map((tool) => (
|
{message?.tools?.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
|
title={tool?.errorMsg}
|
||||||
className={styles["chat-message-tool"]}
|
className={styles["chat-message-tool"]}
|
||||||
>
|
>
|
||||||
{tool.isError === false ? (
|
{tool.isError === false ? (
|
||||||
|
|||||||
@@ -207,23 +207,6 @@ function CustomCode(props: { children: any; className?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeDollarNumber(text: string) {
|
|
||||||
let escapedText = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i += 1) {
|
|
||||||
let char = text[i];
|
|
||||||
const nextChar = text[i + 1] || " ";
|
|
||||||
|
|
||||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
|
||||||
char = "\\$";
|
|
||||||
}
|
|
||||||
|
|
||||||
escapedText += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
return escapedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeBrackets(text: string) {
|
function escapeBrackets(text: string) {
|
||||||
const pattern =
|
const pattern =
|
||||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||||
@@ -252,7 +235,7 @@ function tryWrapHtmlCode(text: string) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
|
||||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||||
},
|
},
|
||||||
@@ -261,7 +244,7 @@ function tryWrapHtmlCode(text: string) {
|
|||||||
|
|
||||||
function _MarkDownContent(props: { content: string }) {
|
function _MarkDownContent(props: { content: string }) {
|
||||||
const escapedContent = useMemo(() => {
|
const escapedContent = useMemo(() => {
|
||||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||||
}, [props.content]);
|
}, [props.content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ const tw = {
|
|||||||
Error: {
|
Error: {
|
||||||
Unauthorized: isApp
|
Unauthorized: isApp
|
||||||
? `😆 對話遇到了一些問題,不用慌:
|
? `😆 對話遇到了一些問題,不用慌:
|
||||||
\\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
\\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️`
|
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️`
|
||||||
: `😆 對話遇到了一些問題,不用慌:
|
: `😆 對話遇到了一些問題,不用慌:
|
||||||
\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||||
\ 2️⃣ 如果你正在使用私有部署版本,點擊[這裡](/#/auth)輸入訪問秘鑰 🔑
|
\ 2️⃣ 如果你正在使用私有部署版本,點選[這裡](/#/auth)輸入存取金鑰 🔑
|
||||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️
|
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -25,9 +25,9 @@ const tw = {
|
|||||||
Confirm: "確認",
|
Confirm: "確認",
|
||||||
Later: "稍候再說",
|
Later: "稍候再說",
|
||||||
Return: "返回",
|
Return: "返回",
|
||||||
SaasTips: "配置太麻煩,想要立即使用",
|
SaasTips: "設定太麻煩,想要立即使用",
|
||||||
TopTips:
|
TopTips:
|
||||||
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
|
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
|
||||||
},
|
},
|
||||||
ChatItem: {
|
ChatItem: {
|
||||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||||
@@ -53,8 +53,8 @@ const tw = {
|
|||||||
PinToastAction: "檢視",
|
PinToastAction: "檢視",
|
||||||
Delete: "刪除",
|
Delete: "刪除",
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
RefreshTitle: "刷新標題",
|
RefreshTitle: "重新整理標題",
|
||||||
RefreshToast: "已發送刷新標題請求",
|
RefreshToast: "已傳送重新整理標題請求",
|
||||||
},
|
},
|
||||||
Commands: {
|
Commands: {
|
||||||
new: "新建聊天",
|
new: "新建聊天",
|
||||||
@@ -95,10 +95,10 @@ const tw = {
|
|||||||
IsContext: "預設提示詞",
|
IsContext: "預設提示詞",
|
||||||
ShortcutKey: {
|
ShortcutKey: {
|
||||||
Title: "鍵盤快捷方式",
|
Title: "鍵盤快捷方式",
|
||||||
newChat: "打開新聊天",
|
newChat: "開啟新聊天",
|
||||||
focusInput: "聚焦輸入框",
|
focusInput: "聚焦輸入框",
|
||||||
copyLastMessage: "複製最後一個回覆",
|
copyLastMessage: "複製最後一個回覆",
|
||||||
copyLastCode: "複製最後一個代碼塊",
|
copyLastCode: "複製最後一個程式碼區塊",
|
||||||
showShortcutKey: "顯示快捷方式",
|
showShortcutKey: "顯示快捷方式",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -174,9 +174,9 @@ const tw = {
|
|||||||
SubTitle: "聊天內容的字型大小",
|
SubTitle: "聊天內容的字型大小",
|
||||||
},
|
},
|
||||||
FontFamily: {
|
FontFamily: {
|
||||||
Title: "聊天字體",
|
Title: "聊天字型",
|
||||||
SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
|
SubTitle: "聊天內容的字型,若留空則套用全域預設字型",
|
||||||
Placeholder: "字體名稱",
|
Placeholder: "字型名稱",
|
||||||
},
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "匯入系統提示",
|
Title: "匯入系統提示",
|
||||||
@@ -301,8 +301,8 @@ const tw = {
|
|||||||
Title: "使用 NextChat AI",
|
Title: "使用 NextChat AI",
|
||||||
Label: "(性價比最高的方案)",
|
Label: "(性價比最高的方案)",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
"由 NextChat 官方維護,零配置開箱即用,支持 OpenAI o1、GPT-4o、Claude-3.5 等最新大模型",
|
"由 NextChat 官方維護,無須設定開箱即用,支援 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
|
||||||
ChatNow: "立刻對話",
|
ChatNow: "立刻開始對話",
|
||||||
},
|
},
|
||||||
|
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
@@ -485,18 +485,18 @@ const tw = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
SearchChat: {
|
SearchChat: {
|
||||||
Name: "搜索",
|
Name: "搜尋",
|
||||||
Page: {
|
Page: {
|
||||||
Title: "搜索聊天記錄",
|
Title: "搜尋聊天記錄",
|
||||||
Search: "輸入搜索關鍵詞",
|
Search: "輸入搜尋關鍵詞",
|
||||||
NoResult: "沒有找到結果",
|
NoResult: "沒有找到結果",
|
||||||
NoData: "沒有數據",
|
NoData: "沒有資料",
|
||||||
Loading: "加載中",
|
Loading: "載入中",
|
||||||
|
|
||||||
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||||
},
|
},
|
||||||
Item: {
|
Item: {
|
||||||
View: "查看",
|
View: "檢視",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NewChat: {
|
NewChat: {
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {
|
|||||||
DEFAULT_SYSTEM_TEMPLATE,
|
DEFAULT_SYSTEM_TEMPLATE,
|
||||||
KnowledgeCutOffDate,
|
KnowledgeCutOffDate,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
|
SUMMARIZE_MODEL,
|
||||||
|
GEMINI_SUMMARIZE_MODEL,
|
||||||
|
ServiceProvider,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
import { isDalle3, safeLocalStorage } from "../utils";
|
import { isDalle3, safeLocalStorage } from "../utils";
|
||||||
@@ -23,6 +26,8 @@ import { prettyObject } from "../utils/format";
|
|||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { estimateTokenLength } from "../utils/token";
|
import { estimateTokenLength } from "../utils/token";
|
||||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||||
|
import { useAccessStore } from "./access";
|
||||||
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { createEmptyMask, Mask } from "./mask";
|
import { createEmptyMask, Mask } from "./mask";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
@@ -37,6 +42,7 @@ export type ChatMessageTool = {
|
|||||||
};
|
};
|
||||||
content?: string;
|
content?: string;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
|
errorMsg?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
@@ -102,6 +108,35 @@ function createEmptySession(): ChatSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSummarizeModel(
|
||||||
|
currentModel: string,
|
||||||
|
providerName: string,
|
||||||
|
): string[] {
|
||||||
|
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||||
|
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||||
|
const configStore = useAppConfig.getState();
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
const allModel = collectModelsWithDefaultModel(
|
||||||
|
configStore.models,
|
||||||
|
[configStore.customModels, accessStore.customModels].join(","),
|
||||||
|
accessStore.defaultModel,
|
||||||
|
);
|
||||||
|
const summarizeModel = allModel.find(
|
||||||
|
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||||
|
);
|
||||||
|
if (summarizeModel) {
|
||||||
|
return [
|
||||||
|
summarizeModel.name,
|
||||||
|
summarizeModel.provider?.providerName as string,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentModel.startsWith("gemini")) {
|
||||||
|
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
|
||||||
|
}
|
||||||
|
return [currentModel, providerName];
|
||||||
|
}
|
||||||
|
|
||||||
function countMessages(msgs: ChatMessage[]) {
|
function countMessages(msgs: ChatMessage[]) {
|
||||||
return msgs.reduce(
|
return msgs.reduce(
|
||||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||||
@@ -578,8 +613,14 @@ export const useChatStore = createPersistStore(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerName = modelConfig.compressProviderName;
|
// if not config compressModel, then using getSummarizeModel
|
||||||
const api: ClientApi = getClientApi(providerName);
|
const [model, providerName] = modelConfig.compressModel
|
||||||
|
? [modelConfig.compressModel, modelConfig.compressProviderName]
|
||||||
|
: getSummarizeModel(
|
||||||
|
session.mask.modelConfig.model,
|
||||||
|
session.mask.modelConfig.providerName,
|
||||||
|
);
|
||||||
|
const api: ClientApi = getClientApi(providerName as ServiceProvider);
|
||||||
|
|
||||||
// remove error messages if any
|
// remove error messages if any
|
||||||
const messages = session.messages;
|
const messages = session.messages;
|
||||||
@@ -610,7 +651,7 @@ export const useChatStore = createPersistStore(
|
|||||||
api.llm.chat({
|
api.llm.chat({
|
||||||
messages: topicMessages,
|
messages: topicMessages,
|
||||||
config: {
|
config: {
|
||||||
model: modelConfig.compressModel,
|
model,
|
||||||
stream: false,
|
stream: false,
|
||||||
providerName,
|
providerName,
|
||||||
},
|
},
|
||||||
@@ -674,7 +715,8 @@ export const useChatStore = createPersistStore(
|
|||||||
config: {
|
config: {
|
||||||
...modelcfg,
|
...modelcfg,
|
||||||
stream: true,
|
stream: true,
|
||||||
model: modelConfig.compressModel,
|
model,
|
||||||
|
providerName,
|
||||||
},
|
},
|
||||||
onUpdate(message) {
|
onUpdate(message) {
|
||||||
session.memoryPrompt = message;
|
session.memoryPrompt = message;
|
||||||
@@ -727,7 +769,7 @@ export const useChatStore = createPersistStore(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: StoreKey.Chat,
|
name: StoreKey.Chat,
|
||||||
version: 3.2,
|
version: 3.3,
|
||||||
migrate(persistedState, version) {
|
migrate(persistedState, version) {
|
||||||
const state = persistedState as any;
|
const state = persistedState as any;
|
||||||
const newState = JSON.parse(
|
const newState = JSON.parse(
|
||||||
@@ -783,6 +825,14 @@ export const useChatStore = createPersistStore(
|
|||||||
config.modelConfig.compressProviderName;
|
config.modelConfig.compressProviderName;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// revert default summarize model for every session
|
||||||
|
if (version < 3.3) {
|
||||||
|
newState.sessions.forEach((s) => {
|
||||||
|
const config = useAppConfig.getState();
|
||||||
|
s.mask.modelConfig.compressModel = "";
|
||||||
|
s.mask.modelConfig.compressProviderName = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return newState as any;
|
return newState as any;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
sendMemory: true,
|
sendMemory: true,
|
||||||
historyMessageCount: 4,
|
historyMessageCount: 4,
|
||||||
compressMessageLengthThreshold: 1000,
|
compressMessageLengthThreshold: 1000,
|
||||||
compressModel: "gpt-4o-mini" as ModelType,
|
compressModel: "",
|
||||||
compressProviderName: "OpenAI" as ServiceProvider,
|
compressProviderName: "",
|
||||||
enableInjectSystemPrompts: true,
|
enableInjectSystemPrompts: true,
|
||||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||||
size: "1024x1024" as DalleSize,
|
size: "1024x1024" as DalleSize,
|
||||||
@@ -178,7 +178,7 @@ export const useAppConfig = createPersistStore(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Config,
|
name: StoreKey.Config,
|
||||||
version: 4,
|
version: 4.1,
|
||||||
|
|
||||||
merge(persistedState, currentState) {
|
merge(persistedState, currentState) {
|
||||||
const state = persistedState as ChatConfig | undefined;
|
const state = persistedState as ChatConfig | undefined;
|
||||||
@@ -231,7 +231,7 @@ export const useAppConfig = createPersistStore(
|
|||||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version < 4) {
|
if (version < 4.1) {
|
||||||
state.modelConfig.compressModel =
|
state.modelConfig.compressModel =
|
||||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||||
state.modelConfig.compressProviderName =
|
state.modelConfig.compressProviderName =
|
||||||
|
|||||||
29
app/utils.ts
29
app/utils.ts
@@ -293,36 +293,23 @@ export function fetch(
|
|||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (window.__TAURI__) {
|
if (window.__TAURI__) {
|
||||||
return tauriStreamFetch(url, {
|
return tauriStreamFetch(url, options);
|
||||||
...options,
|
|
||||||
body: (options?.body || options?.data) as any,
|
|
||||||
});
|
|
||||||
// const payload = options?.body || options?.data;
|
|
||||||
// return tauriFetch(url, {
|
|
||||||
// ...options,
|
|
||||||
// body:
|
|
||||||
// payload &&
|
|
||||||
// ({
|
|
||||||
// type: "Text",
|
|
||||||
// payload,
|
|
||||||
// } as any),
|
|
||||||
// timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
|
|
||||||
// responseType:
|
|
||||||
// options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
|
|
||||||
// } as any);
|
|
||||||
}
|
}
|
||||||
return window.fetch(url, options);
|
return window.fetch(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adapter(config: Record<string, unknown>) {
|
export function adapter(config: Record<string, unknown>) {
|
||||||
const { baseURL, url, params, ...rest } = config;
|
const { baseURL, url, params, data: body, ...rest } = config;
|
||||||
const path = baseURL ? `${baseURL}${url}` : url;
|
const path = baseURL ? `${baseURL}${url}` : url;
|
||||||
const fetchUrl = params
|
const fetchUrl = params
|
||||||
? `${path}?${new URLSearchParams(params as any).toString()}`
|
? `${path}?${new URLSearchParams(params as any).toString()}`
|
||||||
: path;
|
: path;
|
||||||
return fetch(fetchUrl as string, { ...rest, responseType: "text" })
|
return fetch(fetchUrl as string, { ...rest, body }).then((res) => {
|
||||||
.then((res) => res.text())
|
const { status, headers, statusText } = res;
|
||||||
.then((data) => ({ data }));
|
return res
|
||||||
|
.text()
|
||||||
|
.then((data: string) => ({ status, statusText, headers, data }));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeLocalStorage(): {
|
export function safeLocalStorage(): {
|
||||||
|
|||||||
@@ -222,7 +222,12 @@ export function stream(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const content = JSON.stringify(res.data);
|
let content = res.data || res?.statusText;
|
||||||
|
// hotfix #5614
|
||||||
|
content =
|
||||||
|
typeof content === "string"
|
||||||
|
? content
|
||||||
|
: JSON.stringify(content);
|
||||||
if (res.status >= 300) {
|
if (res.status >= 300) {
|
||||||
return Promise.reject(content);
|
return Promise.reject(content);
|
||||||
}
|
}
|
||||||
@@ -237,7 +242,11 @@ export function stream(
|
|||||||
return content;
|
return content;
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
options?.onAfterTool?.({ ...tool, isError: true });
|
options?.onAfterTool?.({
|
||||||
|
...tool,
|
||||||
|
isError: true,
|
||||||
|
errorMsg: e.toString(),
|
||||||
|
});
|
||||||
return e.toString();
|
return e.toString();
|
||||||
})
|
})
|
||||||
.then((content) => ({
|
.then((content) => ({
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
|||||||
body = [],
|
body = [],
|
||||||
} = options || {};
|
} = options || {};
|
||||||
let unlisten: Function | undefined;
|
let unlisten: Function | undefined;
|
||||||
let request_id = 0;
|
let setRequestId: Function | undefined;
|
||||||
|
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
|
||||||
const ts = new TransformStream();
|
const ts = new TransformStream();
|
||||||
const writer = ts.writable.getWriter();
|
const writer = ts.writable.getWriter();
|
||||||
|
|
||||||
@@ -47,20 +48,22 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
|||||||
}
|
}
|
||||||
// @ts-ignore 2. listen response multi times, and write to Response.body
|
// @ts-ignore 2. listen response multi times, and write to Response.body
|
||||||
window.__TAURI__.event
|
window.__TAURI__.event
|
||||||
.listen("stream-response", (e: ResponseEvent) => {
|
.listen("stream-response", (e: ResponseEvent) =>
|
||||||
const { request_id: rid, chunk, status } = e?.payload || {};
|
requestIdPromise.then((request_id) => {
|
||||||
if (request_id != rid) {
|
const { request_id: rid, chunk, status } = e?.payload || {};
|
||||||
return;
|
if (request_id != rid) {
|
||||||
}
|
return;
|
||||||
if (chunk) {
|
}
|
||||||
writer.ready.then(() => {
|
if (chunk) {
|
||||||
writer.write(new Uint8Array(chunk));
|
writer.ready.then(() => {
|
||||||
});
|
writer.write(new Uint8Array(chunk));
|
||||||
} else if (status === 0) {
|
});
|
||||||
// end of body
|
} else if (status === 0) {
|
||||||
close();
|
// end of body
|
||||||
}
|
close();
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
.then((u: Function) => (unlisten = u));
|
.then((u: Function) => (unlisten = u));
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -83,15 +86,15 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
|||||||
: [],
|
: [],
|
||||||
})
|
})
|
||||||
.then((res: StreamResponse) => {
|
.then((res: StreamResponse) => {
|
||||||
request_id = res.request_id;
|
const { request_id, status, status_text: statusText, headers } = res;
|
||||||
const { status, status_text: statusText, headers } = res;
|
setRequestId?.(request_id);
|
||||||
const response = new Response(ts.readable, {
|
const response = new Response(ts.readable, {
|
||||||
status,
|
status,
|
||||||
statusText,
|
statusText,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
if (status >= 300) {
|
if (status >= 300) {
|
||||||
setTimeout(close, 50);
|
setTimeout(close, 100);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -56,6 +57,7 @@ pub async fn stream_fetch(
|
|||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.default_headers(_headers)
|
.default_headers(_headers)
|
||||||
.redirect(reqwest::redirect::Policy::limited(3))
|
.redirect(reqwest::redirect::Policy::limited(3))
|
||||||
|
.connect_timeout(Duration::new(3, 0))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|err| format!("failed to generate client: {}", err))?;
|
.map_err(|err| format!("failed to generate client: {}", err))?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user