소스 검색

Merge remote-tracking branch 'source/main'

YeungYeah 1 년 전
부모
커밋
a3d4a7253f

+ 12 - 6
.env.template

@@ -1,21 +1,20 @@
-
 # Your openai api key. (required)
 OPENAI_API_KEY=sk-xxxx
 
 # Access password, separated by comma. (optional)
 CODE=your-password
 
-# You can start service behind a proxy
+# You can start service behind a proxy. (optional)
 PROXY_URL=http://localhost:7890
 
 # (optional)
 # Default: Empty
-# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
+# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
 GOOGLE_API_KEY=
 
 # (optional)
 # Default: https://generativelanguage.googleapis.com/
-# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
+# Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
 GOOGLE_URL=
 
 # Override openai api request base url. (optional)
@@ -47,6 +46,15 @@ ENABLE_BALANCE_QUERY=
 # If you want to disable parse settings from url, set this value to 1.
 DISABLE_FAST_LINK=
 
+# (optional)
+# Default: Empty
+# To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma.
+CUSTOM_MODELS=
+
+# (optional)
+# Default: Empty
+# Change default model
+DEFAULT_MODEL=
 
 # anthropic claude Api Key.(optional)
 ANTHROPIC_API_KEY=
@@ -54,8 +62,6 @@ ANTHROPIC_API_KEY=
 ### anthropic claude Api version. (optional)
 ANTHROPIC_API_VERSION=
 
-
-
 ### anthropic claude Api url (optional)
 ANTHROPIC_URL=
 

+ 41 - 10
README.md

@@ -1,5 +1,8 @@
 <div align="center">
-<img src="./docs/images/head-cover.png" alt="icon"/>
+
+<a href='#企业版'>
+  <img src="./docs/images/ent.svg" alt="icon"/>
+</a>
 
 <h1 align="center">NextChat (ChatGPT Next Web)</h1>
 
@@ -14,9 +17,9 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
 [![MacOS][MacOS-image]][download-url]
 [![Linux][Linux-image]][download-url]
 
-[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/NextChatDev)
+[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)
 
-[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
+[网页版](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)
 
 [web-url]: https://app.nextchat.dev/
 [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
@@ -25,15 +28,37 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
 [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 
-[![Deploy with Vercel](https://vercel.com/button)](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://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)
 
-[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
+</div>
 
-[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
+## Enterprise Edition
 
-![cover](./docs/images/cover.png)
+Meeting Your Company's Privatization and Customization Deployment Requirements:
+- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
+- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
+- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
+- **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
+- **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
+- **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
+- **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
 
-</div>
+For enterprise inquiries, please contact: **business@nextchat.dev**
+
+## 企业版
+
+满足企业用户私有化部署和个性化定制需求:
+- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
+- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
+- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
+- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
+- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
+- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
+- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
+
+企业版咨询: **business@nextchat.dev**
+
+<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
 
 ## Features
 
@@ -49,6 +74,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
 - Automatically compresses chat history to support long conversations while also saving your tokens
 - I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
 
+<div align="center">
+   
+![主界面](./docs/images/cover.png)
+
+</div>
+
 ## Roadmap
 
 - [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
@@ -180,8 +211,7 @@ Specify OpenAI organization ID.
 
 ### `AZURE_URL` (optional)
 
-> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
-> if you config deployment name in `CUSTOM_MODELS`, you can remove `{deploy-name}` in `AZURE_URL`
+> Example: https://{azure-resource-url}/openai
 
 Azure deploy url.
 
@@ -276,6 +306,7 @@ 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.
 > 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.
 
 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.

+ 28 - 10
README_CN.md

@@ -1,21 +1,33 @@
 <div align="center">
-<img src="./docs/images/icon.svg" alt="预览"/>
+
+<a href='#企业版'>
+  <img src="./docs/images/ent.svg" alt="icon"/>
+</a>
 
 <h1 align="center">NextChat</h1>
 
 一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
 
-[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
+[企业版](#%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)
 
-[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=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)
 
-[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
+</div>
 
-[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
+## 企业版
 
-![主界面](./docs/images/cover.png)
+满足您公司私有化部署和定制需求
+- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
+- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
+- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
+- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
+- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
+- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
+- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
 
-</div>
+企业版咨询: **business@nextchat.dev**
+
+<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
 
 ## 开始使用
 
@@ -25,6 +37,12 @@
 3. 部署完毕后,即可开始使用;
 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
 
+<div align="center">
+   
+![主界面](./docs/images/cover.png)
+
+</div>
+
 ## 保持更新
 
 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
@@ -94,8 +112,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 
 ### `AZURE_URL` (可选)
 
-> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
-> 如果你已经在`CUSTOM_MODELS`中参考`displayName`的方式配置了{deploy-name},那么可以从`AZURE_URL`中移除`{deploy-name}`
+> 形如:https://{azure-resource-url}/openai
 
 Azure 部署地址。
 
@@ -186,7 +203,8 @@ ByteDance Api Url.
 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 
 在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)`
 
 在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项

+ 310 - 0
README_JA.md

@@ -0,0 +1,310 @@
+<div align="center">
+<img src="./docs/images/ent.svg" alt="プレビュー"/>
+
+<h1 align="center">NextChat</h1>
+
+ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
+
+[企業版](#企業版) / [デモ](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)
+
+
+</div>
+
+## 企業版
+
+あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
+- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
+- **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
+- **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
+- **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
+- **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
+- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
+- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
+
+企業版のお問い合わせ: **business@nextchat.dev**
+
+
+## 始めに
+
+1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
+2. 右側のボタンをクリックしてデプロイを開始:
+   [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 、GitHubアカウントで直接ログインし、環境変数ページにAPI Keyと[ページアクセスパスワード](#設定ページアクセスパスワード) CODEを入力してください;
+3. デプロイが完了したら、すぐに使用を開始できます;
+4. (オプション)[カスタムドメインをバインド](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercelが割り当てたドメインDNSは一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
+
+<div align="center">
+   
+![メインインターフェース](./docs/images/cover.png)
+
+</div>
+
+
+## 更新を維持する
+
+もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
+
+以下の手順に従って再デプロイすることをお勧めします:
+
+- 元のリポジトリを削除する
+- ページ右上の fork ボタンを使って、本プロジェクトを fork する
+- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
+
+
+### 自動更新を開く
+
+> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
+
+プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
+
+![自動更新](./docs/images/enable-actions.jpg)
+
+![自動更新を有効にする](./docs/images/enable-actions-sync.jpg)
+
+
+### 手動でコードを更新する
+
+手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
+
+このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
+
+
+
+## ページアクセスパスワードを設定する
+
+> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
+
+> **警告**:パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
+
+このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
+
+```
+code1,code2,code3
+```
+
+この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
+
+
+## 環境変数
+
+> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
+
+### `OPENAI_API_KEY` (必須)
+
+OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
+
+### `CODE` (オプション)
+
+アクセスパスワード。カンマで区切って複数設定可能。
+
+**警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
+
+### `BASE_URL` (オプション)
+
+> デフォルト: `https://api.openai.com`
+
+> 例: `http://your-openai-proxy.com`
+
+OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
+
+> SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
+
+### `OPENAI_ORG_ID` (オプション)
+
+OpenAI の組織 ID を指定します。
+
+### `AZURE_URL` (オプション)
+
+> 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
+> `CUSTOM_MODELS` で `displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
+
+Azure のデプロイ URL。
+
+### `AZURE_API_KEY` (オプション)
+
+Azure の API キー。
+
+### `AZURE_API_VERSION` (オプション)
+
+Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
+
+### `GOOGLE_API_KEY` (オプション)
+
+Google Gemini Pro API キー。
+
+### `GOOGLE_URL` (オプション)
+
+Google Gemini Pro API の URL。
+
+### `ANTHROPIC_API_KEY` (オプション)
+
+Anthropic Claude API キー。
+
+### `ANTHROPIC_API_VERSION` (オプション)
+
+Anthropic Claude API バージョン。
+
+### `ANTHROPIC_URL` (オプション)
+
+Anthropic Claude API の URL。
+
+### `BAIDU_API_KEY` (オプション)
+
+Baidu API キー。
+
+### `BAIDU_SECRET_KEY` (オプション)
+
+Baidu シークレットキー。
+
+### `BAIDU_URL` (オプション)
+
+Baidu API の URL。
+
+### `BYTEDANCE_API_KEY` (オプション)
+
+ByteDance API キー。
+
+### `BYTEDANCE_URL` (オプション)
+
+ByteDance API の URL。
+
+### `ALIBABA_API_KEY` (オプション)
+
+アリババ(千问)API キー。
+
+### `ALIBABA_URL` (オプション)
+
+アリババ(千问)API の URL。
+
+### `HIDE_USER_API_KEY` (オプション)
+
+ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
+
+### `DISABLE_GPT4` (オプション)
+
+ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
+
+### `ENABLE_BALANCE_QUERY` (オプション)
+
+バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
+
+### `DISABLE_FAST_LINK` (オプション)
+
+リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
+
+### `WHITE_WEBDEV_ENDPOINTS` (オプション)
+
+アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
+- 各アドレスは完全なエンドポイントでなければなりません。
+> `https://xxxx/xxx`
+- 複数のアドレスは `,` で接続します。
+
+### `CUSTOM_MODELS` (オプション)
+
+> 例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` は `qwen-7b-chat` と `glm-6b` をモデルリストに追加し、`gpt-3.5-turbo` を削除し、`gpt-4-1106-preview` のモデル名を `gpt-4-turbo` として表示します。
+> すべてのモデルを無効にし、特定のモデルを有効にしたい場合は、`-all,+gpt-3.5-turbo` を使用します。これは `gpt-3.5-turbo` のみを有効にすることを意味します。
+
+モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
+
+Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
+> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
+
+ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
+> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
+
+### `DEFAULT_MODEL` (オプション)
+
+デフォルトのモデルを変更します。
+
+### `DEFAULT_INPUT_TEMPLATE` (オプション)
+
+『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
+
+
+## 開発
+
+下のボタンをクリックして二次開発を開始してください:
+
+[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
+
+コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
+
+```
+OPENAI_API_KEY=<your api key here>
+```
+
+
+### ローカル開発
+
+1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
+2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
+3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
+
+
+## デプロイ
+
+### コンテナデプロイ(推奨)
+
+> Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
+
+> ⚠️ 注意:Docker バージョンは最新バージョンより 1~2 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
+
+```shell
+docker pull yidadaa/chatgpt-next-web
+
+docker run -d -p 3000:3000 \
+   -e OPENAI_API_KEY=sk-xxxx \
+   -e CODE=ページアクセスパスワード \
+   yidadaa/chatgpt-next-web
+```
+
+プロキシを指定することもできます:
+
+```shell
+docker run -d -p 3000:3000 \
+   -e OPENAI_API_KEY=sk-xxxx \
+   -e CODE=ページアクセスパスワード \
+   --net=host \
+   -e PROXY_URL=http://127.0.0.1:7890 \
+   yidadaa/chatgpt-next-web
+```
+
+ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
+
+```shell
+-e PROXY_URL="http://127.0.0.1:7890 user password"
+```
+
+他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
+
+
+### ローカルデプロイ
+
+コンソールで以下のコマンドを実行します:
+
+```shell
+bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
+```
+
+⚠️ 注意:インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
+
+
+## 謝辞
+
+### 寄付者
+
+> 英語版をご覧ください。
+
+### 貢献者
+
+[プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
+
+### 関連プロジェクト
+
+- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
+
+
+## オープンソースライセンス
+
+[MIT](https://opensource.org/license/mit/)

+ 66 - 50
app/api/google/[...path]/route.ts

@@ -1,7 +1,15 @@
 import { NextRequest, NextResponse } from "next/server";
 import { auth } from "../../auth";
 import { getServerSideConfig } from "@/app/config/server";
-import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
+import {
+  ApiPath,
+  GEMINI_BASE_URL,
+  Google,
+  ModelProvider,
+} from "@/app/constant";
+import { prettyObject } from "@/app/utils/format";
+
+const serverConfig = getServerSideConfig();
 
 async function handle(
   req: NextRequest,
@@ -13,32 +21,6 @@ async function handle(
     return NextResponse.json({ body: "OK" }, { status: 200 });
   }
 
-  const controller = new AbortController();
-
-  const serverConfig = getServerSideConfig();
-
-  let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
-
-  if (!baseUrl.startsWith("http")) {
-    baseUrl = `https://${baseUrl}`;
-  }
-
-  if (baseUrl.endsWith("/")) {
-    baseUrl = baseUrl.slice(0, -1);
-  }
-
-  let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
-
-  console.log("[Proxy] ", path);
-  console.log("[Base Url]", baseUrl);
-
-  const timeoutId = setTimeout(
-    () => {
-      controller.abort();
-    },
-    10 * 60 * 1000,
-  );
-
   const authResult = auth(req, ModelProvider.GeminiPro);
   if (authResult.error) {
     return NextResponse.json(authResult, {
@@ -49,9 +31,9 @@ async function handle(
   const bearToken = req.headers.get("Authorization") ?? "";
   const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 
-  const key = token ? token : serverConfig.googleApiKey;
+  const apiKey = token ? token : serverConfig.googleApiKey;
 
-  if (!key) {
+  if (!apiKey) {
     return NextResponse.json(
       {
         error: true,
@@ -62,10 +44,63 @@ async function handle(
       },
     );
   }
+  try {
+    const response = await request(req, apiKey);
+    return response;
+  } catch (e) {
+    console.error("[Google] ", e);
+    return NextResponse.json(prettyObject(e));
+  }
+}
 
-  const fetchUrl = `${baseUrl}/${path}?key=${key}${
-    req?.nextUrl?.searchParams?.get("alt") == "sse" ? "&alt=sse" : ""
+export const GET = handle;
+export const POST = handle;
+
+export const runtime = "edge";
+export const preferredRegion = [
+  "bom1",
+  "cle1",
+  "cpt1",
+  "gru1",
+  "hnd1",
+  "iad1",
+  "icn1",
+  "kix1",
+  "pdx1",
+  "sfo1",
+  "sin1",
+  "syd1",
+];
+
+async function request(req: NextRequest, apiKey: string) {
+  const controller = new AbortController();
+
+  let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
+
+  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
+
+  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}?key=${apiKey}${
+    req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
   }`;
+
+  console.log("[Fetch Url] ", fetchUrl);
   const fetchOptions: RequestInit = {
     headers: {
       "Content-Type": "application/json",
@@ -97,22 +132,3 @@ async function handle(
     clearTimeout(timeoutId);
   }
 }
-
-export const GET = handle;
-export const POST = handle;
-
-export const runtime = "edge";
-export const preferredRegion = [
-  "bom1",
-  "cle1",
-  "cpt1",
-  "gru1",
-  "hnd1",
-  "iad1",
-  "icn1",
-  "kix1",
-  "pdx1",
-  "sfo1",
-  "sin1",
-  "syd1",
-];

+ 1 - 1
app/client/platforms/alibaba.ts

@@ -21,7 +21,7 @@ import {
 } from "@fortaine/fetch-event-source";
 import { prettyObject } from "@/app/utils/format";
 import { getClientConfig } from "@/app/config/client";
-import { getMessageTextContent, isVisionModel } from "@/app/utils";
+import { getMessageTextContent } from "@/app/utils";
 
 export interface OpenAIListModelResponse {
   object: string;

+ 7 - 2
app/client/platforms/anthropic.ts

@@ -3,7 +3,6 @@ import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
 import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 import { getClientConfig } from "@/app/config/client";
 import { DEFAULT_API_HOST } from "@/app/constant";
-import { RequestMessage } from "@/app/typing";
 import {
   EventStreamContentType,
   fetchEventSource,
@@ -12,6 +11,7 @@ import {
 import Locale from "../../locales";
 import { prettyObject } from "@/app/utils/format";
 import { getMessageTextContent, isVisionModel } from "@/app/utils";
+import { preProcessImageContent } from "@/app/utils/chat";
 import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 
 export type MultiBlockContent = {
@@ -93,7 +93,12 @@ export class ClaudeApi implements LLMApi {
       },
     };
 
-    const messages = [...options.messages];
+    // try get base64image from local cache image_url
+    const messages: ChatOptions["messages"] = [];
+    for (const v of options.messages) {
+      const content = await preProcessImageContent(v.content);
+      messages.push({ role: v.role, content });
+    }
 
     const keys = ["system", "user"];
 

+ 42 - 37
app/client/platforms/google.ts

@@ -1,4 +1,4 @@
-import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
+import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
 import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
 import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 import { getClientConfig } from "@/app/config/client";
@@ -14,8 +14,37 @@ import {
   getMessageImages,
   isVisionModel,
 } from "@/app/utils";
+import { preProcessImageContent } from "@/app/utils/chat";
 
 export class GeminiProApi implements LLMApi {
+  path(path: string): string {
+    const accessStore = useAccessStore.getState();
+
+    let baseUrl = "";
+    if (accessStore.useCustomConfig) {
+      baseUrl = accessStore.googleUrl;
+    }
+
+    if (baseUrl.length === 0) {
+      const isApp = !!getClientConfig()?.isApp;
+      baseUrl = isApp
+        ? DEFAULT_API_HOST + `/api/proxy/google?key=${accessStore.googleApiKey}`
+        : ApiPath.Google;
+    }
+    if (baseUrl.endsWith("/")) {
+      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
+    }
+    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
+      baseUrl = "https://" + baseUrl;
+    }
+
+    console.log("[Proxy Endpoint] ", baseUrl, path);
+
+    let chatPath = [baseUrl, path].join("/");
+
+    chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
+    return chatPath;
+  }
   extractMessage(res: any) {
     console.log("[Response] gemini-pro response: ", res);
 
@@ -28,7 +57,14 @@ export class GeminiProApi implements LLMApi {
   async chat(options: ChatOptions): Promise<void> {
     const apiClient = this;
     let multimodal = false;
-    const messages = options.messages.map((v) => {
+
+    // try get base64image from local cache image_url
+    const _messages: ChatOptions["messages"] = [];
+    for (const v of options.messages) {
+      const content = await preProcessImageContent(v.content);
+      _messages.push({ role: v.role, content });
+    }
+    const messages = _messages.map((v) => {
       let parts: any[] = [{ text: getMessageTextContent(v) }];
       if (isVisionModel(options.config.model)) {
         const images = getMessageImages(v);
@@ -111,30 +147,13 @@ export class GeminiProApi implements LLMApi {
       ],
     };
 
-    let baseUrl = "";
-
-    if (accessStore.useCustomConfig) {
-      baseUrl = accessStore.googleUrl;
-    }
-
-    const isApp = !!getClientConfig()?.isApp;
-
     let shouldStream = !!options.config.stream;
     const controller = new AbortController();
     options.onController?.(controller);
     try {
-      if (!baseUrl) {
-        baseUrl = isApp
-          ? DEFAULT_API_HOST + "/api/proxy/google/"
-          : this.path("");
-      }
-      baseUrl = `${baseUrl}/${Google.ChatPath(modelConfig.model)}`.replaceAll(
-        "//",
-        "/",
-      );
-      if (isApp) {
-        baseUrl += `?key=${accessStore.googleApiKey}`;
-      }
+      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
+      const chatPath = this.path(Google.ChatPath(modelConfig.model));
+
       const chatPayload = {
         method: "POST",
         body: JSON.stringify(requestPayload),
@@ -184,10 +203,6 @@ export class GeminiProApi implements LLMApi {
 
         controller.signal.onabort = finish;
 
-        // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
-        const chatPath =
-          baseUrl.replace("generateContent", "streamGenerateContent") +
-          (baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse");
         fetchEventSource(chatPath, {
           ...chatPayload,
           async onopen(res) {
@@ -262,7 +277,7 @@ export class GeminiProApi implements LLMApi {
           openWhenHidden: true,
         });
       } else {
-        const res = await fetch(baseUrl, chatPayload);
+        const res = await fetch(chatPath, chatPayload);
         clearTimeout(requestTimeoutId);
         const resJson = await res.json();
         if (resJson?.promptFeedback?.blockReason) {
@@ -288,14 +303,4 @@ export class GeminiProApi implements LLMApi {
   async models(): Promise<LLMModel[]> {
     return [];
   }
-  path(path: string): string {
-    return "/api/google/" + path;
-  }
-}
-
-function ensureProperEnding(str: string) {
-  if (str.startsWith("[") && !str.endsWith("]")) {
-    return str + "]";
-  }
-  return str;
 }

+ 8 - 4
app/client/platforms/openai.ts

@@ -11,6 +11,7 @@ import {
 } from "@/app/constant";
 import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 import { collectModelsWithDefaultModel } from "@/app/utils/model";
+import { preProcessImageContent } from "@/app/utils/chat";
 import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 
 import {
@@ -105,10 +106,13 @@ export class ChatGPTApi implements LLMApi {
 
   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 messages: ChatOptions["messages"] = [];
+    for (const v of options.messages) {
+      const content = visionModel
+        ? await preProcessImageContent(v.content)
+        : getMessageTextContent(v);
+      messages.push({ role: v.role, content });
+    }
 
     const modelConfig = {
       ...useAppConfig.getState().modelConfig,

+ 3 - 3
app/components/chat.tsx

@@ -61,7 +61,7 @@ import {
   isVisionModel,
 } from "../utils";
 
-import { compressImage } from "@/app/utils/chat";
+import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
 
 import dynamic from "next/dynamic";
 
@@ -1167,7 +1167,7 @@ function _Chat() {
               ...(await new Promise<string[]>((res, rej) => {
                 setUploading(true);
                 const imagesData: string[] = [];
-                compressImage(file, 256 * 1024)
+                uploadImageRemote(file)
                   .then((dataUrl) => {
                     imagesData.push(dataUrl);
                     setUploading(false);
@@ -1209,7 +1209,7 @@ function _Chat() {
           const imagesData: string[] = [];
           for (let i = 0; i < files.length; i++) {
             const file = event.target.files[i];
-            compressImage(file, 256 * 1024)
+            uploadImageRemote(file)
               .then((dataUrl) => {
                 imagesData.push(dataUrl);
                 if (

+ 1 - 1
app/config/server.ts

@@ -21,7 +21,7 @@ declare global {
       ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
       DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
       CUSTOM_MODELS?: string; // to control custom models
-      DEFAULT_MODEL?: string; // to cnntrol default model in every new chat window
+      DEFAULT_MODEL?: string; // to control default model in every new chat window
 
       // azure only
       AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}

+ 10 - 2
app/constant.ts

@@ -21,6 +21,9 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
 
 export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
 
+export const CACHE_URL_PREFIX = "/api/cache";
+export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
+
 export enum Path {
   Home = "/",
   Chat = "/chat",
@@ -127,7 +130,8 @@ export const Azure = {
 
 export const Google = {
   ExampleEndpoint: "https://generativelanguage.googleapis.com/",
-  ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
+  ChatPath: (modelName: string) =>
+    `v1beta/models/${modelName}:streamGenerateContent`,
 };
 
 export const Baidu = {
@@ -191,6 +195,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
   "gpt-4-turbo-preview": "2023-12",
   "gpt-4o": "2023-10",
   "gpt-4o-2024-05-13": "2023-10",
+  "gpt-4o-mini": "2023-10",
+  "gpt-4o-mini-2024-07-18": "2023-10",
   "gpt-4-vision-preview": "2023-04",
   // After improvements,
   // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
@@ -210,6 +216,8 @@ const openaiModels = [
   "gpt-4-turbo-preview",
   "gpt-4o",
   "gpt-4o-2024-05-13",
+  "gpt-4o-mini",
+  "gpt-4o-mini-2024-07-18",
   "gpt-4-vision-preview",
   "gpt-4-turbo-2024-04-09",
   "gpt-4-1106-preview",
@@ -243,7 +251,7 @@ const baiduModels = [
   "ernie-speed-128k",
   "ernie-speed-8k",
   "ernie-lite-8k",
-  "ernie-tiny-8k"
+  "ernie-tiny-8k",
 ];
 
 const bytedanceModels = [

+ 0 - 2
app/store/chat.ts

@@ -9,8 +9,6 @@ import {
   DEFAULT_MODELS,
   DEFAULT_SYSTEM_TEMPLATE,
   KnowledgeCutOffDate,
-  ServiceProvider,
-  ModelProvider,
   StoreKey,
   SUMMARIZE_MODEL,
   GEMINI_SUMMARIZE_MODEL,

+ 3 - 2
app/store/prompt.ts

@@ -154,7 +154,7 @@ export const usePromptStore = createPersistStore(
       fetch(PROMPT_URL)
         .then((res) => res.json())
         .then((res) => {
-          let fetchPrompts = [res.en, res.cn];
+          let fetchPrompts = [res.en, res.tw, res.cn];
           if (getLang() === "cn") {
             fetchPrompts = fetchPrompts.reverse();
           }
@@ -175,7 +175,8 @@ export const usePromptStore = createPersistStore(
           const allPromptsForSearch = builtinPrompts
             .reduce((pre, cur) => pre.concat(cur), [])
             .filter((v) => !!v.title && !!v.content);
-          SearchService.count.builtin = res.en.length + res.cn.length;
+          SearchService.count.builtin =
+            res.en.length + res.cn.length + res.tw.length;
           SearchService.init(allPromptsForSearch, userPrompts);
         });
     },

+ 1 - 0
app/utils.ts

@@ -256,6 +256,7 @@ export function isVisionModel(model: string) {
     "gemini-1.5-pro",
     "gemini-1.5-flash",
     "gpt-4o",
+    "gpt-4o-mini",
   ];
   const isGpt4Turbo =
     model.includes("gpt-4-turbo") && !model.includes("preview");

+ 99 - 9
app/utils/chat.ts

@@ -1,6 +1,7 @@
-import heic2any from "heic2any";
+import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
+import { RequestMessage } from "@/app/client/api";
 
-export function compressImage(file: File, maxSize: number): Promise<string> {
+export function compressImage(file: Blob, maxSize: number): Promise<string> {
   return new Promise((resolve, reject) => {
     const reader = new FileReader();
     reader.onload = (readerEvent: any) => {
@@ -40,15 +41,104 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
     reader.onerror = reject;
 
     if (file.type.includes("heic")) {
-      heic2any({ blob: file, toType: "image/jpeg" })
-        .then((blob) => {
-          reader.readAsDataURL(blob as Blob);
-        })
-        .catch((e) => {
-          reject(e);
-        });
+      try {
+        const heic2any = require("heic2any");
+        heic2any({ blob: file, toType: "image/jpeg" })
+          .then((blob: Blob) => {
+            reader.readAsDataURL(blob);
+          })
+          .catch((e: any) => {
+            reject(e);
+          });
+      } catch (e) {
+        reject(e);
+      }
     }
 
     reader.readAsDataURL(file);
   });
 }
+
+export async function preProcessImageContent(
+  content: RequestMessage["content"],
+) {
+  if (typeof content === "string") {
+    return content;
+  }
+  const result = [];
+  for (const part of content) {
+    if (part?.type == "image_url" && part?.image_url?.url) {
+      try {
+        const url = await cacheImageToBase64Image(part?.image_url?.url);
+        result.push({ type: part.type, image_url: { url } });
+      } catch (error) {
+        console.error("Error processing image URL:", error);
+      }
+    } else {
+      result.push({ ...part });
+    }
+  }
+  return result;
+}
+
+const imageCaches: Record<string, string> = {};
+export function cacheImageToBase64Image(imageUrl: string) {
+  if (imageUrl.includes(CACHE_URL_PREFIX)) {
+    if (!imageCaches[imageUrl]) {
+      const reader = new FileReader();
+      return fetch(imageUrl, {
+        method: "GET",
+        mode: "cors",
+        credentials: "include",
+      })
+        .then((res) => res.blob())
+        .then(
+          async (blob) =>
+            (imageCaches[imageUrl] = await compressImage(blob, 256 * 1024)),
+        ); // compressImage
+    }
+    return Promise.resolve(imageCaches[imageUrl]);
+  }
+  return Promise.resolve(imageUrl);
+}
+
+export function base64Image2Blob(base64Data: string, contentType: string) {
+  const byteCharacters = atob(base64Data);
+  const byteNumbers = new Array(byteCharacters.length);
+  for (let i = 0; i < byteCharacters.length; i++) {
+    byteNumbers[i] = byteCharacters.charCodeAt(i);
+  }
+  const byteArray = new Uint8Array(byteNumbers);
+  return new Blob([byteArray], { type: contentType });
+}
+
+export function uploadImage(file: File): Promise<string> {
+  if (!window._SW_ENABLED) {
+    // if serviceWorker register error, using compressImage
+    return compressImage(file, 256 * 1024);
+  }
+  const body = new FormData();
+  body.append("file", file);
+  return fetch(UPLOAD_URL, {
+    method: "post",
+    body,
+    mode: "cors",
+    credentials: "include",
+  })
+    .then((res) => res.json())
+    .then((res) => {
+      console.log("res", res);
+      if (res?.code == 0 && res?.data) {
+        return res?.data;
+      }
+      throw Error(`upload Error: ${res?.msg}`);
+    });
+}
+
+export function removeImage(imageUrl: string) {
+  return fetch(imageUrl, {
+    method: "DELETE",
+    mode: "cors",
+    credentials: "include",
+  });
+}

+ 1 - 1
app/utils/hooks.ts

@@ -1,6 +1,6 @@
 import { useMemo } from "react";
 import { useAccessStore, useAppConfig } from "../store";
-import { collectModels, collectModelsWithDefaultModel } from "./model";
+import { collectModelsWithDefaultModel } from "./model";
 
 export function useAllModels() {
   const accessStore = useAccessStore();

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 5 - 0
docs/images/ent.svg


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 3 - 0
public/prompts.json


+ 45 - 1
public/serviceWorker.js

@@ -1,10 +1,13 @@
 const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
+const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file";
+let a="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let nanoid=(e=21)=>{let t="",r=crypto.getRandomValues(new Uint8Array(e));for(let n=0;n<e;n++)t+=a[63&r[n]];return t};
 
 self.addEventListener("activate", function (event) {
   console.log("ServiceWorker activated.");
 });
 
 self.addEventListener("install", function (event) {
+  self.skipWaiting();  // enable new version
   event.waitUntil(
     caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
       return cache.addAll([]);
@@ -12,4 +15,45 @@ self.addEventListener("install", function (event) {
   );
 });
 
-self.addEventListener("fetch", (e) => {});
+async function upload(request, url) {
+  const formData = await request.formData()
+  const file = formData.getAll('file')[0]
+  let ext = file.name.split('.').pop()
+  if (ext === 'blob') {
+    ext = file.type.split('/').pop()
+  }
+  const fileUrl = `${url.origin}/api/cache/${nanoid()}.${ext}`
+  // console.debug('file', file, fileUrl, request)
+  const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
+  await cache.put(new Request(fileUrl), new Response(file, {
+    headers: {
+      'content-type': file.type,
+      'content-length': file.size,
+      'cache-control': 'no-cache', // file already store in disk
+      'server': 'ServiceWorker',
+    }
+  }))
+  return Response.json({ code: 0, data: fileUrl })
+}
+
+async function remove(request, url) {
+  const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
+  const res = await cache.delete(request.url)
+  return Response.json({ code: 0 })
+}
+
+self.addEventListener("fetch", (e) => {
+  const url = new URL(e.request.url);
+  if (/^\/api\/cache/.test(url.pathname)) {
+    if ('GET' == e.request.method) {
+      e.respondWith(caches.match(e.request))
+    }
+    if ('POST' == e.request.method) {
+      e.respondWith(upload(e.request, url))
+    }
+    if ('DELETE' == e.request.method) {
+      e.respondWith(remove(e.request, url))
+    }
+  }
+});
+

+ 20 - 2
public/serviceWorkerRegister.js

@@ -1,9 +1,27 @@
 if ('serviceWorker' in navigator) {
-  window.addEventListener('load', function () {
+  window.addEventListener('DOMContentLoaded', function () {
     navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
       console.log('ServiceWorker registration successful with scope: ', registration.scope);
+      const sw = registration.installing || registration.waiting
+      if (sw) {
+        sw.onstatechange = function() {
+          if (sw.state === 'installed') {
+            // SW installed.  Reload for SW intercept serving SW-enabled page.
+            console.log('ServiceWorker installed reload page');
+            window.location.reload();
+          }
+        }
+      }
+      registration.update().then(res => {
+        console.log('ServiceWorker registration update: ', res);
+      });
+      window._SW_ENABLED = true
     }, function (err) {
       console.error('ServiceWorker registration failed: ', err);
     });
+    navigator.serviceWorker.addEventListener('controllerchange', function() {
+      console.log('ServiceWorker controllerchange ');
+      window.location.reload(true);
+    });
   });
-}
+}

+ 26 - 5
scripts/fetch-prompts.mjs

@@ -6,11 +6,13 @@ const MIRRORF_FILE_URL = "http://raw.fgit.ml/";
 
 const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
 const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL;
+const RAW_TW_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json";
+const TW_URL = MIRRORF_FILE_URL + RAW_TW_URL;
 const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv";
 const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL;
 const FILE = "./public/prompts.json";
 
-const ignoreWords = ["涩涩", "魅魔"];
+const ignoreWords = ["涩涩", "魅魔", "澀澀"];
 
 const timeoutPromise = (timeout) => {
   return new Promise((resolve, reject) => {
@@ -39,6 +41,25 @@ async function fetchCN() {
   }
 }
 
+async function fetchTW() {
+  console.log("[Fetch] fetching tw prompts...");
+  try {
+    const response = await Promise.race([fetch(TW_URL), timeoutPromise(5000)]);
+    const raw = await response.json();
+    return raw
+      .map((v) => [v.act, v.prompt])
+      .filter(
+        (v) =>
+          v[0] &&
+          v[1] &&
+          ignoreWords.every((w) => !v[0].includes(w) && !v[1].includes(w)),
+      );
+  } catch (error) {
+    console.error("[Fetch] failed to fetch tw prompts", error);
+    return [];
+  }
+}
+
 async function fetchEN() {
   console.log("[Fetch] fetching en prompts...");
   try {
@@ -61,13 +82,13 @@ async function fetchEN() {
 }
 
 async function main() {
-  Promise.all([fetchCN(), fetchEN()])
-    .then(([cn, en]) => {
-      fs.writeFile(FILE, JSON.stringify({ cn, en }));
+  Promise.all([fetchCN(), fetchTW(), fetchEN()])
+    .then(([cn, tw, en]) => {
+      fs.writeFile(FILE, JSON.stringify({ cn, tw, en }));
     })
     .catch((e) => {
       console.error("[Fetch] failed to fetch prompts");
-      fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] }));
+      fs.writeFile(FILE, JSON.stringify({ cn: [], tw: [], en: [] }));
     })
     .finally(() => {
       console.log("[Fetch] saved to " + FILE);

+ 1 - 1
src-tauri/tauri.conf.json

@@ -9,7 +9,7 @@
   },
   "package": {
     "productName": "NextChat",
-    "version": "2.13.0"
+    "version": "2.13.1"
   },
   "tauri": {
     "allowlist": {

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.