瀏覽代碼

merge origin/main

lloydzhou 1 年之前
父節點
當前提交
fc31d8e5d1

+ 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=
 

+ 80 - 0
.github/ISSUE_TEMPLATE/1_bug_report.yml

@@ -0,0 +1,80 @@
+name: '🐛 Bug Report'
+description: 'Report an bug'
+title: '[Bug] '
+labels: ['bug']
+body:
+  - type: dropdown
+    attributes:
+      label: '📦 Deployment Method'
+      multiple: true
+      options:
+        - 'Official installation package'
+        - 'Vercel'
+        - 'Zeabur'
+        - 'Sealos'
+        - 'Netlify'
+        - 'Docker'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 Version'
+    validations:
+      required: true
+  
+  - type: dropdown
+    attributes:
+      label: '💻 Operating System'
+      multiple: true
+      options:
+        - 'Windows'
+        - 'macOS'
+        - 'Ubuntu'
+        - 'Other Linux'
+        - 'iOS'
+        - 'iPad OS'
+        - 'Android'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 System Version'
+    validations:
+      required: true
+  - type: dropdown
+    attributes:
+      label: '🌐 Browser'
+      multiple: true
+      options:
+        - 'Chrome'
+        - 'Edge'
+        - 'Safari'
+        - 'Firefox'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 Browser Version'
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '🐛 Bug Description'
+      description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '📷 Recurrence Steps'
+      description: A clear and concise description of how to recurrence.
+  - type: textarea
+    attributes:
+      label: '🚦 Expected Behavior'
+      description: A clear and concise description of what you expected to happen.
+  - type: textarea
+    attributes:
+      label: '📝 Additional Information'
+      description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.

+ 80 - 0
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml

@@ -0,0 +1,80 @@
+name: '🐛 反馈缺陷'
+description: '反馈一个问题/缺陷'
+title: '[Bug] '
+labels: ['bug']
+body:
+  - type: dropdown
+    attributes:
+      label: '📦 部署方式'
+      multiple: true
+      options:
+        - '官方安装包'
+        - 'Vercel'
+        - 'Zeabur'
+        - 'Sealos'
+        - 'Netlify'
+        - 'Docker'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 软件版本'
+    validations:
+      required: true
+
+  - type: dropdown
+    attributes:
+      label: '💻 系统环境'
+      multiple: true
+      options:
+        - 'Windows'
+        - 'macOS'
+        - 'Ubuntu'
+        - 'Other Linux'
+        - 'iOS'
+        - 'iPad OS'
+        - 'Android'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 系统版本'
+    validations:
+      required: true
+  - type: dropdown
+    attributes:
+      label: '🌐 浏览器'
+      multiple: true
+      options:
+        - 'Chrome'
+        - 'Edge'
+        - 'Safari'
+        - 'Firefox'
+        - 'Other'
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: '📌 浏览器版本'
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '🐛 问题描述'
+      description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '📷 复现步骤'
+      description: 请提供一个清晰且简洁的描述,说明如何复现问题。
+  - type: textarea
+    attributes:
+      label: '🚦 期望结果'
+      description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
+  - type: textarea
+    attributes:
+      label: '📝 补充信息'
+      description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。

+ 21 - 0
.github/ISSUE_TEMPLATE/2_feature_request.yml

@@ -0,0 +1,21 @@
+name: '🌠 Feature Request'
+description: 'Suggest an idea'
+title: '[Feature Request] '
+labels: ['enhancement']
+body:
+  - type: textarea
+    attributes:
+      label: '🥰 Feature Description'
+      description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '🧐 Proposed Solution'
+      description: Describe the solution you'd like in a clear and concise manner.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '📝 Additional Information'
+      description: Add any other context about the problem here.

+ 21 - 0
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml

@@ -0,0 +1,21 @@
+name: '🌠 功能需求'
+description: '提出需求或建议'
+title: '[Feature Request] '
+labels: ['enhancement']
+body:
+  - type: textarea
+    attributes:
+      label: '🥰 需求描述'
+      description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '🧐 解决方案'
+      description: 请清晰且简洁地描述您想要的解决方案。
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: '📝 补充信息'
+      description: 在这里添加关于问题的任何其他背景信息。

+ 0 - 146
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -1,146 +0,0 @@
-name: Bug report
-description: Create a report to help us improve
-title: "[Bug] "
-labels: ["bug"]
-
-body:
-  - type: markdown
-    attributes:
-      value: "## Describe the bug"
-  - type: textarea
-    id: bug-description
-    attributes:
-      label: "Bug Description"
-      description: "A clear and concise description of what the bug is."
-      placeholder: "Explain the bug..."
-    validations:
-      required: true
-
-  - type: markdown
-    attributes:
-      value: "## To Reproduce"
-  - type: textarea
-    id: steps-to-reproduce
-    attributes:
-      label: "Steps to Reproduce"
-      description: "Steps to reproduce the behavior:"
-      placeholder: |
-        1. Go to '...'
-        2. Click on '....'
-        3. Scroll down to '....'
-        4. See error
-    validations:
-      required: true
-
-  - type: markdown
-    attributes:
-      value: "## Expected behavior"
-  - type: textarea
-    id: expected-behavior
-    attributes:
-      label: "Expected Behavior"
-      description: "A clear and concise description of what you expected to happen."
-      placeholder: "Describe what you expected to happen..."
-    validations:
-      required: true
-
-  - type: markdown
-    attributes:
-      value: "## Screenshots"
-  - type: textarea
-    id: screenshots
-    attributes:
-      label: "Screenshots"
-      description: "If applicable, add screenshots to help explain your problem."
-      placeholder: "Paste your screenshots here or write 'N/A' if not applicable..."
-    validations:
-      required: false
-
-  - type: markdown
-    attributes:
-      value: "## Deployment"
-  - type: checkboxes
-    id: deployment
-    attributes:
-      label: "Deployment Method"
-      description: "Please select the deployment method you are using."
-      options:
-        - label: "Docker"
-        - label: "Vercel"
-        - label: "Server"
-
-  - type: markdown
-    attributes:
-      value: "## Desktop (please complete the following information):"
-  - type: input
-    id: desktop-os
-    attributes:
-      label: "Desktop OS"
-      description: "Your desktop operating system."
-      placeholder: "e.g., Windows 10"
-    validations:
-      required: false
-  - type: input
-    id: desktop-browser
-    attributes:
-      label: "Desktop Browser"
-      description: "Your desktop browser."
-      placeholder: "e.g., Chrome, Safari"
-    validations:
-      required: false
-  - type: input
-    id: desktop-version
-    attributes:
-      label: "Desktop Browser Version"
-      description: "Version of your desktop browser."
-      placeholder: "e.g., 89.0"
-    validations:
-      required: false
-
-  - type: markdown
-    attributes:
-      value: "## Smartphone (please complete the following information):"
-  - type: input
-    id: smartphone-device
-    attributes:
-      label: "Smartphone Device"
-      description: "Your smartphone device."
-      placeholder: "e.g., iPhone X"
-    validations:
-      required: false
-  - type: input
-    id: smartphone-os
-    attributes:
-      label: "Smartphone OS"
-      description: "Your smartphone operating system."
-      placeholder: "e.g., iOS 14.4"
-    validations:
-      required: false
-  - type: input
-    id: smartphone-browser
-    attributes:
-      label: "Smartphone Browser"
-      description: "Your smartphone browser."
-      placeholder: "e.g., Safari"
-    validations:
-      required: false
-  - type: input
-    id: smartphone-version
-    attributes:
-      label: "Smartphone Browser Version"
-      description: "Version of your smartphone browser."
-      placeholder: "e.g., 14"
-    validations:
-      required: false
-
-  - type: markdown
-    attributes:
-      value: "## Additional Logs"
-  - type: textarea
-    id: additional-logs
-    attributes:
-      label: "Additional Logs"
-      description: "Add any logs about the problem here."
-      placeholder: "Paste any relevant logs here..."
-    validations:
-      required: false

+ 0 - 53
.github/ISSUE_TEMPLATE/feature_request.yml

@@ -1,53 +0,0 @@
-name: Feature request
-description: Suggest an idea for this project
-title: "[Feature Request]: "
-labels: ["enhancement"]
-
-body:
-  - type: markdown
-    attributes:
-      value: "## Is your feature request related to a problem? Please describe."
-  - type: textarea
-    id: problem-description
-    attributes:
-      label: Problem Description
-      description: "A clear and concise description of what the problem is. Example: I'm always frustrated when [...]"
-      placeholder: "Explain the problem you are facing..."
-    validations:
-      required: true
-
-  - type: markdown
-    attributes:
-      value: "## Describe the solution you'd like"
-  - type: textarea
-    id: desired-solution
-    attributes:
-      label: Solution Description
-      description: A clear and concise description of what you want to happen.
-      placeholder: "Describe the solution you'd like..."
-    validations:
-      required: true
-
-  - type: markdown
-    attributes:
-      value: "## Describe alternatives you've considered"
-  - type: textarea
-    id: alternatives-considered
-    attributes:
-      label: Alternatives Considered
-      description: A clear and concise description of any alternative solutions or features you've considered.
-      placeholder: "Describe any alternative solutions or features you've considered..."
-    validations:
-      required: false
-
-  - type: markdown
-    attributes:
-      value: "## Additional context"
-  - type: textarea
-    id: additional-context
-    attributes:
-      label: Additional Context
-      description: Add any other context or screenshots about the feature request here.
-      placeholder: "Add any other context or screenshots about the feature request here..."
-    validations:
-      required: false

+ 28 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,28 @@
+#### 💻 变更类型 | Change Type
+
+<!-- For change type, change [ ] to [x]. -->
+
+- [ ] feat    <!-- 引入新功能 | Introduce new features -->
+- [ ] fix    <!-- 修复 Bug | Fix a bug -->
+- [ ] refactor    <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
+- [ ] perf    <!-- 提升性能的代码变更 | A code change that improves performance -->
+- [ ] style    <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
+- [ ] test    <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
+- [ ] docs    <!-- 仅文档更新 | Documentation only changes -->
+- [ ] ci    <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
+- [ ] chore    <!-- 其他不修改 src 或 test 文件的变更 | Other changes that don’t modify src or test files -->
+- [ ] build    <!-- 进行架构变更 | Make architectural changes -->
+
+#### 🔀 变更说明 | Description of Change
+
+<!-- 
+感谢您的 Pull Request ,请提供此 Pull Request 的变更说明
+Thank you for your Pull Request. Please provide a description above.
+-->
+
+#### 📝 补充信息 | Additional Information
+
+<!-- 
+请添加与此 Pull Request 相关的补充信息
+Add any other context about the Pull Request here.
+-->

+ 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/)

+ 11 - 9
app/api/anthropic/[...path]/route.ts

@@ -11,6 +11,7 @@ import { prettyObject } from "@/app/utils/format";
 import { NextRequest, NextResponse } from "next/server";
 import { auth } from "../../auth";
 import { isModelAvailableInServer } from "@/app/utils/model";
+import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 
 const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 
@@ -114,7 +115,8 @@ async function request(req: NextRequest) {
     10 * 60 * 1000,
   );
 
-  const fetchUrl = `${baseUrl}${path}`;
+  // try rebuild url, when using cloudflare ai gateway in server
+  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
 
   const fetchOptions: RequestInit = {
     headers: {
@@ -164,17 +166,17 @@ async function request(req: NextRequest) {
       console.error(`[Anthropic] filter`, e);
     }
   }
-  console.log("[Anthropic request]", fetchOptions.headers, req.method);
+  // console.log("[Anthropic request]", fetchOptions.headers, req.method);
   try {
     const res = await fetch(fetchUrl, fetchOptions);
 
-    console.log(
-      "[Anthropic response]",
-      res.status,
-      "   ",
-      res.headers,
-      res.url,
-    );
+    // console.log(
+    //   "[Anthropic response]",
+    //   res.status,
+    //   "   ",
+    //   res.headers,
+    //   res.url,
+    // );
     // to prevent browser prompt for credentials
     const newHeaders = new Headers(res.headers);
     newHeaders.delete("www-authenticate");

+ 4 - 2
app/api/common.ts

@@ -7,6 +7,7 @@ import {
   ServiceProvider,
 } from "../constant";
 import { isModelAvailableInServer } from "../utils/model";
+import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
 
 const serverConfig = getServerSideConfig();
 
@@ -37,7 +38,7 @@ export async function requestOpenai(req: NextRequest) {
   );
 
   let baseUrl =
-    serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
+    (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
 
   if (!baseUrl.startsWith("http")) {
     baseUrl = `https://${baseUrl}`;
@@ -95,7 +96,8 @@ export async function requestOpenai(req: NextRequest) {
     }
   }
 
-  const fetchUrl = `${baseUrl}/${path}`;
+  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
+  console.log("fetchUrl", fetchUrl);
   const fetchOptions: RequestInit = {
     headers: {
       "Content-Type": "application/json",

+ 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;

+ 10 - 3
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,8 @@ 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 = {
   type: "image" | "text";
@@ -92,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"];
 
@@ -375,7 +381,8 @@ export class ClaudeApi implements LLMApi {
 
     baseUrl = trimEnd(baseUrl, "/");
 
-    return `${baseUrl}/${path}`;
+    // try rebuild url, when using cloudflare ai gateway in client
+    return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
   }
 }
 

+ 48 - 43
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);
@@ -70,6 +106,9 @@ export class GeminiProApi implements LLMApi {
     // if (visionModel && messages.length > 1) {
     //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
     // }
+
+    const accessStore = useAccessStore.getState();
+
     const modelConfig = {
       ...useAppConfig.getState().modelConfig,
       ...useChatStore.getState().currentSession().mask.modelConfig,
@@ -91,50 +130,30 @@ export class GeminiProApi implements LLMApi {
       safetySettings: [
         {
           category: "HARM_CATEGORY_HARASSMENT",
-          threshold: "BLOCK_ONLY_HIGH",
+          threshold: accessStore.googleSafetySettings,
         },
         {
           category: "HARM_CATEGORY_HATE_SPEECH",
-          threshold: "BLOCK_ONLY_HIGH",
+          threshold: accessStore.googleSafetySettings,
         },
         {
           category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
-          threshold: "BLOCK_ONLY_HIGH",
+          threshold: accessStore.googleSafetySettings,
         },
         {
           category: "HARM_CATEGORY_DANGEROUS_CONTENT",
-          threshold: "BLOCK_ONLY_HIGH",
+          threshold: accessStore.googleSafetySettings,
         },
       ],
     };
 
-    const accessStore = useAccessStore.getState();
-
-    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 {
-      // let baseUrl = accessStore.googleUrl;
-
-      if (!baseUrl) {
-        baseUrl = isApp
-          ? DEFAULT_API_HOST +
-            "/api/proxy/google/" +
-            Google.ChatPath(modelConfig.model)
-          : this.path(Google.ChatPath(modelConfig.model));
-      }
+      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
+      const chatPath = this.path(Google.ChatPath(modelConfig.model));
 
-      if (isApp) {
-        baseUrl += `?key=${accessStore.googleApiKey}`;
-      }
       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;
 }

+ 11 - 5
app/client/platforms/openai.ts

@@ -11,6 +11,8 @@ 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 {
   ChatOptions,
@@ -94,7 +96,8 @@ export class ChatGPTApi implements LLMApi {
 
     console.log("[Proxy Endpoint] ", baseUrl, path);
 
-    return [baseUrl, path].join("/");
+    // try rebuild url, when using cloudflare ai gateway in client
+    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
   }
 
   extractMessage(res: any) {
@@ -103,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,

+ 8 - 8
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";
 
@@ -245,11 +245,11 @@ function useSubmitHandler() {
   };
 }
 
-export type RenderPompt = Pick<Prompt, "title" | "content">;
+export type RenderPrompt = Pick<Prompt, "title" | "content">;
 
 export function PromptHints(props: {
-  prompts: RenderPompt[];
-  onPromptSelect: (prompt: RenderPompt) => void;
+  prompts: RenderPrompt[];
+  onPromptSelect: (prompt: RenderPrompt) => void;
 }) {
   const noPrompts = props.prompts.length === 0;
   const [selectIndex, setSelectIndex] = useState(0);
@@ -727,7 +727,7 @@ function _Chat() {
 
   // prompt hints
   const promptStore = usePromptStore();
-  const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
+  const [promptHints, setPromptHints] = useState<RenderPrompt[]>([]);
   const onSearch = useDebouncedCallback(
     (text: string) => {
       const matchedPrompts = promptStore.search(text);
@@ -812,7 +812,7 @@ function _Chat() {
     setAutoScroll(true);
   };
 
-  const onPromptSelect = (prompt: RenderPompt) => {
+  const onPromptSelect = (prompt: RenderPrompt) => {
     setTimeout(() => {
       setPromptHints([]);
 
@@ -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 (

+ 394 - 410
app/components/settings.tsx

@@ -57,6 +57,7 @@ import {
   ByteDance,
   Alibaba,
   Google,
+  GoogleSafetySettingsThreshold,
   OPENAI_BASE_URL,
   Path,
   RELEASE_URL,
@@ -657,6 +658,389 @@ export function Settings() {
   const clientConfig = useMemo(() => getClientConfig(), []);
   const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
 
+  const accessCodeComponent = showAccessCode && (
+    <ListItem
+      title={Locale.Settings.Access.AccessCode.Title}
+      subTitle={Locale.Settings.Access.AccessCode.SubTitle}
+    >
+      <PasswordInput
+        value={accessStore.accessCode}
+        type="text"
+        placeholder={Locale.Settings.Access.AccessCode.Placeholder}
+        onChange={(e) => {
+          accessStore.update(
+            (access) => (access.accessCode = e.currentTarget.value),
+          );
+        }}
+      />
+    </ListItem>
+  );
+
+  const useCustomConfigComponent = // Conditionally render the following ListItem based on clientConfig.isApp
+    !clientConfig?.isApp && ( // only show if isApp is false
+      <ListItem
+        title={Locale.Settings.Access.CustomEndpoint.Title}
+        subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
+      >
+        <input
+          type="checkbox"
+          checked={accessStore.useCustomConfig}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.useCustomConfig = e.currentTarget.checked),
+            )
+          }
+        ></input>
+      </ListItem>
+    );
+
+  const openAIConfigComponent = accessStore.provider ===
+    ServiceProvider.OpenAI && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.OpenAI.Endpoint.Title}
+        subTitle={Locale.Settings.Access.OpenAI.Endpoint.SubTitle}
+      >
+        <input
+          type="text"
+          value={accessStore.openaiUrl}
+          placeholder={OPENAI_BASE_URL}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.openaiUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.OpenAI.ApiKey.Title}
+        subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.openaiApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.openaiApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+    </>
+  );
+
+  const azureConfigComponent = accessStore.provider ===
+    ServiceProvider.Azure && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Azure.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.Azure.Endpoint.SubTitle + Azure.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.azureUrl}
+          placeholder={Azure.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.azureUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Azure.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.azureApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Azure.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.azureApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Azure.ApiVerion.Title}
+        subTitle={Locale.Settings.Access.Azure.ApiVerion.SubTitle}
+      >
+        <input
+          type="text"
+          value={accessStore.azureApiVersion}
+          placeholder="2023-08-01-preview"
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.azureApiVersion = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+    </>
+  );
+
+  const googleConfigComponent = accessStore.provider ===
+    ServiceProvider.Google && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Google.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.Google.Endpoint.SubTitle +
+          Google.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.googleUrl}
+          placeholder={Google.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.googleUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Google.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.googleApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.googleApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Google.ApiVersion.Title}
+        subTitle={Locale.Settings.Access.Google.ApiVersion.SubTitle}
+      >
+        <input
+          type="text"
+          value={accessStore.googleApiVersion}
+          placeholder="2023-08-01-preview"
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.googleApiVersion = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Google.GoogleSafetySettings.Title}
+        subTitle={Locale.Settings.Access.Google.GoogleSafetySettings.SubTitle}
+      >
+        <Select
+          value={accessStore.googleSafetySettings}
+          onChange={(e) => {
+            accessStore.update(
+              (access) =>
+                (access.googleSafetySettings = e.target
+                  .value as GoogleSafetySettingsThreshold),
+            );
+          }}
+        >
+          {Object.entries(GoogleSafetySettingsThreshold).map(([k, v]) => (
+            <option value={v} key={k}>
+              {k}
+            </option>
+          ))}
+        </Select>
+      </ListItem>
+    </>
+  );
+
+  const anthropicConfigComponent = accessStore.provider ===
+    ServiceProvider.Anthropic && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Anthropic.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
+          Anthropic.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.anthropicUrl}
+          placeholder={Anthropic.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.anthropicUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Anthropic.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Anthropic.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.anthropicApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Anthropic.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.anthropicApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
+        subTitle={Locale.Settings.Access.Anthropic.ApiVerion.SubTitle}
+      >
+        <input
+          type="text"
+          value={accessStore.anthropicApiVersion}
+          placeholder={Anthropic.Vision}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.anthropicApiVersion = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+    </>
+  );
+
+  const baiduConfigComponent = accessStore.provider ===
+    ServiceProvider.Baidu && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Baidu.Endpoint.Title}
+        subTitle={Locale.Settings.Access.Baidu.Endpoint.SubTitle}
+      >
+        <input
+          type="text"
+          value={accessStore.baiduUrl}
+          placeholder={Baidu.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.baiduUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Baidu.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.baiduApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Baidu.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.baiduApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Baidu.SecretKey.Title}
+        subTitle={Locale.Settings.Access.Baidu.SecretKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.baiduSecretKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Baidu.SecretKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.baiduSecretKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+    </>
+  );
+
+  const byteDanceConfigComponent = accessStore.provider ===
+    ServiceProvider.ByteDance && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.ByteDance.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.ByteDance.Endpoint.SubTitle +
+          ByteDance.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.bytedanceUrl}
+          placeholder={ByteDance.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.bytedanceUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.ByteDance.ApiKey.Title}
+        subTitle={Locale.Settings.Access.ByteDance.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.bytedanceApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.ByteDance.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.bytedanceApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+    </>
+  );
+
+  const alibabaConfigComponent = accessStore.provider ===
+    ServiceProvider.Alibaba && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Alibaba.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.Alibaba.Endpoint.SubTitle +
+          Alibaba.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.alibabaUrl}
+          placeholder={Alibaba.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.alibabaUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Alibaba.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Alibaba.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.alibabaApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Alibaba.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.alibabaApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+    </>
+  );
+
   return (
     <ErrorBoundary>
       <div className="window-header" data-tauri-drag-region>
@@ -903,46 +1287,12 @@ export function Settings() {
         </List>
 
         <List id={SlotID.CustomModel}>
-          {showAccessCode && (
-            <ListItem
-              title={Locale.Settings.Access.AccessCode.Title}
-              subTitle={Locale.Settings.Access.AccessCode.SubTitle}
-            >
-              <PasswordInput
-                value={accessStore.accessCode}
-                type="text"
-                placeholder={Locale.Settings.Access.AccessCode.Placeholder}
-                onChange={(e) => {
-                  accessStore.update(
-                    (access) => (access.accessCode = e.currentTarget.value),
-                  );
-                }}
-              />
-            </ListItem>
-          )}
+          {accessCodeComponent}
 
           {!accessStore.hideUserApiKey && (
             <>
-              {
-                // Conditionally render the following ListItem based on clientConfig.isApp
-                !clientConfig?.isApp && ( // only show if isApp is false
-                  <ListItem
-                    title={Locale.Settings.Access.CustomEndpoint.Title}
-                    subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
-                  >
-                    <input
-                      type="checkbox"
-                      checked={accessStore.useCustomConfig}
-                      onChange={(e) =>
-                        accessStore.update(
-                          (access) =>
-                            (access.useCustomConfig = e.currentTarget.checked),
-                        )
-                      }
-                    ></input>
-                  </ListItem>
-                )
-              }
+              {useCustomConfigComponent}
+
               {accessStore.useCustomConfig && (
                 <>
                   <ListItem
@@ -967,379 +1317,13 @@ export function Settings() {
                     </Select>
                   </ListItem>
 
-                  {accessStore.provider === ServiceProvider.OpenAI && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.OpenAI.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.OpenAI.Endpoint.SubTitle
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.openaiUrl}
-                          placeholder={OPENAI_BASE_URL}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.openaiUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.OpenAI.ApiKey.Title}
-                        subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
-                      >
-                        <PasswordInput
-                          value={accessStore.openaiApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.OpenAI.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.openaiApiKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                    </>
-                  )}
-                  {accessStore.provider === ServiceProvider.Azure && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.Azure.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.Azure.Endpoint.SubTitle +
-                          Azure.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.azureUrl}
-                          placeholder={Azure.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.azureUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Azure.ApiKey.Title}
-                        subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
-                      >
-                        <PasswordInput
-                          value={accessStore.azureApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Azure.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.azureApiKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Azure.ApiVerion.Title}
-                        subTitle={
-                          Locale.Settings.Access.Azure.ApiVerion.SubTitle
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.azureApiVersion}
-                          placeholder="2023-08-01-preview"
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.azureApiVersion =
-                                  e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                    </>
-                  )}
-                  {accessStore.provider === ServiceProvider.Google && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.Google.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.Google.Endpoint.SubTitle +
-                          Google.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.googleUrl}
-                          placeholder={Google.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.googleUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Google.ApiKey.Title}
-                        subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
-                      >
-                        <PasswordInput
-                          value={accessStore.googleApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Google.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.googleApiKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Google.ApiVersion.Title}
-                        subTitle={
-                          Locale.Settings.Access.Google.ApiVersion.SubTitle
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.googleApiVersion}
-                          placeholder="2023-08-01-preview"
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.googleApiVersion =
-                                  e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                    </>
-                  )}
-                  {accessStore.provider === ServiceProvider.Anthropic && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.Anthropic.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
-                          Anthropic.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.anthropicUrl}
-                          placeholder={Anthropic.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.anthropicUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Anthropic.ApiKey.Title}
-                        subTitle={
-                          Locale.Settings.Access.Anthropic.ApiKey.SubTitle
-                        }
-                      >
-                        <PasswordInput
-                          value={accessStore.anthropicApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Anthropic.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.anthropicApiKey =
-                                  e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
-                        subTitle={
-                          Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.anthropicApiVersion}
-                          placeholder={Anthropic.Vision}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.anthropicApiVersion =
-                                  e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                    </>
-                  )}
-                  {accessStore.provider === ServiceProvider.Baidu && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.Baidu.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
-                          Baidu.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.baiduUrl}
-                          placeholder={Baidu.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.baiduUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Baidu.ApiKey.Title}
-                        subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle}
-                      >
-                        <PasswordInput
-                          value={accessStore.baiduApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Baidu.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.baiduApiKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Baidu.SecretKey.Title}
-                        subTitle={
-                          Locale.Settings.Access.Baidu.SecretKey.SubTitle
-                        }
-                      >
-                        <PasswordInput
-                          value={accessStore.baiduSecretKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Baidu.SecretKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.baiduSecretKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                    </>
-                  )}
-
-                  {accessStore.provider === ServiceProvider.ByteDance && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.ByteDance.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.ByteDance.Endpoint.SubTitle +
-                          ByteDance.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.bytedanceUrl}
-                          placeholder={ByteDance.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.bytedanceUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.ByteDance.ApiKey.Title}
-                        subTitle={
-                          Locale.Settings.Access.ByteDance.ApiKey.SubTitle
-                        }
-                      >
-                        <PasswordInput
-                          value={accessStore.bytedanceApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.ByteDance.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.bytedanceApiKey =
-                                  e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                    </>
-                  )}
-
-                  {accessStore.provider === ServiceProvider.Alibaba && (
-                    <>
-                      <ListItem
-                        title={Locale.Settings.Access.Alibaba.Endpoint.Title}
-                        subTitle={
-                          Locale.Settings.Access.Alibaba.Endpoint.SubTitle +
-                          Alibaba.ExampleEndpoint
-                        }
-                      >
-                        <input
-                          type="text"
-                          value={accessStore.alibabaUrl}
-                          placeholder={Alibaba.ExampleEndpoint}
-                          onChange={(e) =>
-                            accessStore.update(
-                              (access) =>
-                                (access.alibabaUrl = e.currentTarget.value),
-                            )
-                          }
-                        ></input>
-                      </ListItem>
-                      <ListItem
-                        title={Locale.Settings.Access.Alibaba.ApiKey.Title}
-                        subTitle={
-                          Locale.Settings.Access.Alibaba.ApiKey.SubTitle
-                        }
-                      >
-                        <PasswordInput
-                          value={accessStore.alibabaApiKey}
-                          type="text"
-                          placeholder={
-                            Locale.Settings.Access.Alibaba.ApiKey.Placeholder
-                          }
-                          onChange={(e) => {
-                            accessStore.update(
-                              (access) =>
-                                (access.alibabaApiKey = e.currentTarget.value),
-                            );
-                          }}
-                        />
-                      </ListItem>
-                    </>
-                  )}
+                  {openAIConfigComponent}
+                  {azureConfigComponent}
+                  {googleConfigComponent}
+                  {anthropicConfigComponent}
+                  {baiduConfigComponent}
+                  {byteDanceConfigComponent}
+                  {alibabaConfigComponent}
                 </>
               )}
             </>

+ 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
 
       // stability only
       STABILITY_URL?: string;

+ 29 - 3
app/constant.ts

@@ -23,7 +23,8 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
 
 export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
 
-export const UPLOAD_URL = "/api/cache/upload";
+export const CACHE_URL_PREFIX = "/api/cache";
+export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
 
 export enum Path {
   Home = "/",
@@ -41,6 +42,7 @@ export enum ApiPath {
   Azure = "/api/azure",
   OpenAI = "/api/openai",
   Anthropic = "/api/anthropic",
+  Google = "/api/google",
   Baidu = "/api/baidu",
   ByteDance = "/api/bytedance",
   Alibaba = "/api/alibaba",
@@ -95,6 +97,15 @@ export enum ServiceProvider {
   Alibaba = "Alibaba",
 }
 
+// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
+// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
+export enum GoogleSafetySettingsThreshold {
+  BLOCK_NONE = "BLOCK_NONE",
+  BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
+  BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
+  BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
+}
+
 export enum ModelProvider {
   Stability = "Stability",
   GPT = "GPT",
@@ -131,7 +142,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 = {
@@ -147,6 +159,12 @@ export const Baidu = {
     if (modelName === "ernie-3.5-8k") {
       endpoint = "completions";
     }
+    if (modelName === "ernie-speed-128k") {
+      endpoint = "ernie-speed-128k";
+    }
+    if (modelName === "ernie-speed-8k") {
+      endpoint = "ernie_speed";
+    }
     return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
   },
 };
@@ -179,7 +197,7 @@ Latex inline: \\(x^2\\)
 Latex block: $$e=mc^2$$
 `;
 
-export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
+export const SUMMARIZE_MODEL = "gpt-4o-mini";
 export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
 
 export const KnowledgeCutOffDate: Record<string, string> = {
@@ -189,6 +207,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.
@@ -208,6 +228,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",
@@ -238,6 +260,10 @@ const baiduModels = [
   "ernie-4.0-8k-latest",
   "ernie-3.5-8k",
   "ernie-3.5-8k-0205",
+  "ernie-speed-128k",
+  "ernie-speed-8k",
+  "ernie-lite-8k",
+  "ernie-tiny-8k",
 ];
 
 const bytedanceModels = [

+ 7 - 3
app/locales/cn.ts

@@ -346,21 +346,25 @@ const cn = {
           Title: "API 版本(仅适用于 gemini-pro)",
           SubTitle: "选择一个特定的 API 版本",
         },
+        GoogleSafetySettings: {
+          Title: "Google 安全过滤级别",
+          SubTitle: "设置内容过滤级别",
+        },
       },
       Baidu: {
         ApiKey: {
-          Title: "接口密钥",
+          Title: "API Key",
           SubTitle: "使用自定义 Baidu API Key",
           Placeholder: "Baidu API Key",
         },
         SecretKey: {
-          Title: "接口密钥",
+          Title: "Secret Key",
           SubTitle: "使用自定义 Baidu Secret Key",
           Placeholder: "Baidu Secret Key",
         },
         Endpoint: {
           Title: "接口地址",
-          SubTitle: "样例:",
+          SubTitle: "不支持自定义前往.env配置",
         },
       },
       ByteDance: {

+ 9 - 5
app/locales/en.ts

@@ -326,7 +326,7 @@ const en: LocaleType = {
 
         Endpoint: {
           Title: "Endpoint Address",
-          SubTitle: "Example:",
+          SubTitle: "Example: ",
         },
 
         ApiVerion: {
@@ -347,7 +347,7 @@ const en: LocaleType = {
         },
         Endpoint: {
           Title: "Endpoint Address",
-          SubTitle: "Example:",
+          SubTitle: "not supported, configure in .env",
         },
       },
       ByteDance: {
@@ -358,7 +358,7 @@ const en: LocaleType = {
         },
         Endpoint: {
           Title: "Endpoint Address",
-          SubTitle: "Example:",
+          SubTitle: "Example: ",
         },
       },
       Alibaba: {
@@ -369,7 +369,7 @@ const en: LocaleType = {
         },
         Endpoint: {
           Title: "Endpoint Address",
-          SubTitle: "Example:",
+          SubTitle: "Example: ",
         },
       },
       CustomModel: {
@@ -385,13 +385,17 @@ const en: LocaleType = {
 
         Endpoint: {
           Title: "Endpoint Address",
-          SubTitle: "Example:",
+          SubTitle: "Example: ",
         },
 
         ApiVersion: {
           Title: "API Version (specific to gemini-pro)",
           SubTitle: "Select a specific API version",
         },
+        GoogleSafetySettings: {
+          Title: "Google Safety Settings",
+          SubTitle: "Select a safety filtering level",
+        },
       },
     },
 

+ 42 - 42
app/locales/tw.ts

@@ -4,11 +4,11 @@ import { SubmitKey } from "../store/config";
 const isApp = !!getClientConfig()?.isApp;
 
 const tw = {
-  WIP: "功能仍在開發中……",
+  WIP: "功能仍在開發中……",
   Error: {
     Unauthorized: isApp
-      ? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否設定正確。"
-      : "存取密碼不正確或未填寫,請前往[登入](/#/auth)頁輸入正確的存取密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
+      ? "偵測到無效的 API Key,請前往[設定](/#/settings)頁面檢查 API Key 是否設定正確。"
+      : "存取密碼不正確或未填寫,請前往[登入](/#/auth)頁輸入正確的存取密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
   },
 
   Auth: {
@@ -159,7 +159,7 @@ const tw = {
     },
     InputTemplate: {
       Title: "使用者輸入預處理",
-      SubTitle: "使用者最新的一訊息會填充到此範本",
+      SubTitle: "使用者最新的一訊息會填充到此範本",
     },
 
     Update: {
@@ -194,19 +194,19 @@ const tw = {
         },
         SyncType: {
           Title: "同步類型",
-          SubTitle: "選擇喜愛的同步伺服器",
+          SubTitle: "選擇偏好的同步伺服器",
         },
         Proxy: {
-          Title: "啟用代理",
-          SubTitle: "在瀏覽器中同步時,必須啟用代理以避免跨域限制",
+          Title: "啟用代理伺服器",
+          SubTitle: "在瀏覽器中同步時,啟用代理伺服器以避免跨域限制",
         },
         ProxyUrl: {
-          Title: "代理地址",
-          SubTitle: "僅適用於本專案自帶的跨域代理",
+          Title: "代理伺服器位置",
+          SubTitle: "僅適用於本專案內建的跨域代理",
         },
 
         WebDav: {
-          Endpoint: "WebDAV 地址",
+          Endpoint: "WebDAV 位置",
           UserName: "使用者名稱",
           Password: "密碼",
         },
@@ -218,9 +218,9 @@ const tw = {
         },
       },
 
-      LocalState: "本資料",
+      LocalState: "本資料",
       Overview: (overview: any) => {
-        return `${overview.chat} 次對話,${overview.message} 訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`;
+        return `${overview.chat} 次對話,${overview.message} 訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`;
       },
       ImportFailed: "匯入失敗",
     },
@@ -239,13 +239,13 @@ const tw = {
         Title: "停用提示詞自動補齊",
         SubTitle: "在輸入框開頭輸入 / 即可觸發自動補齊",
       },
-      List: "自定義提示詞列表",
+      List: "自提示詞列表",
       ListCount: (builtin: number, custom: number) =>
-        `內建 ${builtin} 條,使用者定義 ${custom} 條`,
+        `內建 ${builtin} 條,使用者自訂 ${custom} 條`,
       Edit: "編輯",
       Modal: {
         Title: "提示詞列表",
-        Add: "新增一",
+        Add: "新增一",
         Search: "搜尋提示詞",
       },
       EditModal: {
@@ -278,40 +278,40 @@ const tw = {
         Placeholder: "請輸入存取密碼",
       },
       CustomEndpoint: {
-        Title: "自定義介面 (Endpoint)",
-        SubTitle: "是否使用自定義 Azure 或 OpenAI 服務",
+        Title: "自訂 API 端點 (Endpoint)",
+        SubTitle: "是否使用自 Azure 或 OpenAI 服務",
       },
       Provider: {
-        Title: "模型服務商",
-        SubTitle: "切換不同的服務商",
+        Title: "模型供應商",
+        SubTitle: "切換不同的服務供應商",
       },
       OpenAI: {
         ApiKey: {
           Title: "API Key",
-          SubTitle: "使用自定義 OpenAI Key 繞過密碼存取限制",
+          SubTitle: "使用自 OpenAI Key 繞過密碼存取限制",
           Placeholder: "OpenAI API Key",
         },
 
         Endpoint: {
-          Title: "介面(Endpoint) 地址",
-          SubTitle: "除預設址外,必須包含 http(s)://",
+          Title: "API 端點 (Endpoint) 位址",
+          SubTitle: "除預設址外,必須包含 http(s)://",
         },
       },
       Azure: {
         ApiKey: {
-          Title: "介面金鑰",
-          SubTitle: "使用自定義 Azure Key 繞過密碼存取限制",
+          Title: "API 金鑰",
+          SubTitle: "使用自 Azure Key 繞過密碼存取限制",
           Placeholder: "Azure API Key",
         },
 
         Endpoint: {
-          Title: "介面(Endpoint) 地址",
-          SubTitle: "例:",
+          Title: "API 端點 (Endpoint) 位址",
+          SubTitle: "例:",
         },
 
         ApiVerion: {
-          Title: "介面版本 (azure api version)",
-          SubTitle: "選擇指定的部分版本",
+          Title: "API 版本 (azure api version)",
+          SubTitle: "指定一個特定的 API 版本",
         },
       },
       Anthropic: {
@@ -322,13 +322,13 @@ const tw = {
         },
 
         Endpoint: {
-          Title: "終端地址",
+          Title: "端點位址",
           SubTitle: "範例:",
         },
 
         ApiVerion: {
           Title: "API 版本 (claude api version)",
-          SubTitle: "選擇一個特定的 API 版本輸入",
+          SubTitle: "指定一個特定的 API 版本",
         },
       },
       Google: {
@@ -339,7 +339,7 @@ const tw = {
         },
 
         Endpoint: {
-          Title: "終端地址",
+          Title: "端點位址",
           SubTitle: "範例:",
         },
 
@@ -349,8 +349,8 @@ const tw = {
         },
       },
       CustomModel: {
-        Title: "自定義模型名",
-        SubTitle: "增加自定義模型可選項,使用英文逗號隔開",
+        Title: "自訂模型名稱",
+        SubTitle: "增加自訂模型可選擇項目,使用英文逗號隔開",
       },
     },
 
@@ -400,7 +400,7 @@ const tw = {
   Context: {
     Toast: (x: any) => `已設定 ${x} 條前置上下文`,
     Edit: "前置上下文和歷史記憶",
-    Add: "新增一",
+    Add: "新增一",
     Clear: "上下文已清除",
     Revert: "恢復上下文",
   },
@@ -425,16 +425,16 @@ const tw = {
     EditModal: {
       Title: (readonly: boolean) =>
         `編輯預設角色範本 ${readonly ? "(唯讀)" : ""}`,
-      Download: "下載預設",
-      Clone: "複製預設",
+      Download: "下載預設",
+      Clone: "以此預設值建立副本",
     },
     Config: {
       Avatar: "角色頭像",
       Name: "角色名稱",
       Sync: {
-        Title: "使用全域設定",
-        SubTitle: "目前對話是否使用全域模型設定",
-        Confirm: "目前對話的自定義設定將會被自動覆蓋,確認啟用全域性設定?",
+        Title: "使用全域設定",
+        SubTitle: "目前對話是否使用全域模型設定",
+        Confirm: "目前對話的自訂設定將會被自動覆蓋,確認啟用全域設定?",
       },
       HideContext: {
         Title: "隱藏預設對話",
@@ -450,15 +450,15 @@ const tw = {
   NewChat: {
     Return: "返回",
     Skip: "跳過",
-    NotShow: "不再呈現",
+    NotShow: "不再顯示",
     ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。",
     Title: "挑選一個角色範本",
     SubTitle: "現在開始,與角色範本背後的靈魂思維碰撞",
     More: "搜尋更多",
   },
   URLCommand: {
-    Code: "測到連結中已經包含存取密碼,是否自動填入?",
-    Settings: "測到連結中包含了預設設定,是否自動填入?",
+    Code: "測到連結中已經包含存取密碼,是否自動填入?",
+    Settings: "測到連結中包含了預設設定,是否自動填入?",
   },
   UI: {
     Confirm: "確認",

+ 34 - 14
app/store/access.ts

@@ -1,6 +1,7 @@
 import {
   ApiPath,
   DEFAULT_API_HOST,
+  GoogleSafetySettingsThreshold,
   ServiceProvider,
   StoreKey,
 } from "../constant";
@@ -12,15 +13,33 @@ import { DEFAULT_CONFIG } from "./config";
 
 let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
 
-const DEFAULT_OPENAI_URL =
-  getClientConfig()?.buildMode === "export"
-    ? DEFAULT_API_HOST + "/api/proxy/openai"
-    : ApiPath.OpenAI;
+const isApp = getClientConfig()?.buildMode === "export";
 
-const DEFAULT_AZURE_URL =
-  getClientConfig()?.buildMode === "export"
-    ? DEFAULT_API_HOST + "/api/proxy/azure/{resource_name}"
-    : ApiPath.Azure;
+const DEFAULT_OPENAI_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/openai"
+  : ApiPath.OpenAI;
+
+const DEFAULT_GOOGLE_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/google"
+  : ApiPath.Google;
+
+const DEFAULT_ANTHROPIC_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/anthropic"
+  : ApiPath.Anthropic;
+
+const DEFAULT_BAIDU_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/baidu"
+  : ApiPath.Baidu;
+
+const DEFAULT_BYTEDANCE_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/bytedance"
+  : ApiPath.ByteDance;
+
+const DEFAULT_ALIBABA_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/alibaba"
+  : ApiPath.Alibaba;
+
+console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL);
 
 const DEFAULT_ACCESS_STATE = {
   accessCode: "",
@@ -33,31 +52,32 @@ const DEFAULT_ACCESS_STATE = {
   openaiApiKey: "",
 
   // azure
-  azureUrl: DEFAULT_AZURE_URL,
+  azureUrl: "",
   azureApiKey: "",
   azureApiVersion: "2023-08-01-preview",
 
   // google ai studio
-  googleUrl: "",
+  googleUrl: DEFAULT_GOOGLE_URL,
   googleApiKey: "",
   googleApiVersion: "v1",
+  googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH,
 
   // anthropic
+  anthropicUrl: DEFAULT_ANTHROPIC_URL,
   anthropicApiKey: "",
   anthropicApiVersion: "2023-06-01",
-  anthropicUrl: "",
 
   // baidu
-  baiduUrl: "",
+  baiduUrl: DEFAULT_BAIDU_URL,
   baiduApiKey: "",
   baiduSecretKey: "",
 
   // bytedance
+  bytedanceUrl: DEFAULT_BYTEDANCE_URL,
   bytedanceApiKey: "",
-  bytedanceUrl: "",
 
   // alibaba
-  alibabaUrl: "",
+  alibabaUrl: DEFAULT_ALIBABA_URL,
   alibabaApiKey: "",
 
   // server config

+ 1 - 3
app/store/chat.ts

@@ -9,8 +9,6 @@ import {
   DEFAULT_MODELS,
   DEFAULT_SYSTEM_TEMPLATE,
   KnowledgeCutOffDate,
-  ServiceProvider,
-  ModelProvider,
   StoreKey,
   SUMMARIZE_MODEL,
   GEMINI_SUMMARIZE_MODEL,
@@ -92,7 +90,7 @@ function createEmptySession(): ChatSession {
 }
 
 function getSummarizeModel(currentModel: string) {
-  // if it is using gpt-* models, force to use 3.5 to summarize
+  // if it is using gpt-* models, force to use 4o-mini to summarize
   if (currentModel.startsWith("gpt")) {
     const configStore = useAppConfig.getState();
     const accessStore = useAccessStore.getState();

+ 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 - 1
app/styles/globals.scss

@@ -118,7 +118,7 @@ body {
 }
 
 ::-webkit-scrollbar {
-  --bar-width: 5px;
+  --bar-width: 10px;
   width: var(--bar-width);
   height: var(--bar-width);
 }

+ 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");

+ 62 - 10
app/utils/chat.ts

@@ -1,7 +1,7 @@
-import { UPLOAD_URL } from "@/app/constant";
-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) => {
@@ -41,19 +41,67 @@ 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);
@@ -65,6 +113,10 @@ export function base64Image2Blob(base64Data: string, contentType: string) {
 }
 
 export function uploadImage(file: Blob): 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, {

+ 26 - 0
app/utils/cloudflare.ts

@@ -0,0 +1,26 @@
+export function cloudflareAIGatewayUrl(fetchUrl: string) {
+  // rebuild fetchUrl, if using cloudflare ai gateway
+  // document: https://developers.cloudflare.com/ai-gateway/providers/openai/
+
+  const paths = fetchUrl.split("/");
+  if ("gateway.ai.cloudflare.com" == paths[2]) {
+    // is cloudflare.com ai gateway
+    // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15'
+    if ("azure-openai" == paths[6]) {
+      // is azure gateway
+      return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url
+    }
+    // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions
+    if ("openai" == paths[6]) {
+      // is openai gateway
+      return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url
+    }
+    // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \
+    if ("anthropic" == paths[6]) {
+      // is anthropic gateway
+      return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url
+    }
+    // TODO: Amazon Bedrock, Groq, HuggingFace...
+  }
+  return fetchUrl;
+}

+ 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();

+ 14 - 7
app/utils/model.ts

@@ -1,9 +1,9 @@
 import { DEFAULT_MODELS } from "../constant";
 import { LLMModel } from "../client/api";
 
-const customProvider = (modelName: string) => ({
-  id: modelName,
-  providerName: "Custom",
+const customProvider = (providerName: string) => ({
+  id: providerName.toLowerCase(),
+  providerName: providerName,
   providerType: "custom",
 });
 
@@ -71,10 +71,17 @@ export function collectModelTable(
         }
         // 2. if model not exists, create new model with available value
         if (count === 0) {
-          const provider = customProvider(name);
-          modelTable[`${name}@${provider?.id}`] = {
-            name,
-            displayName: displayName || name,
+          let [customModelName, customProviderName] = name.split("@");
+          const provider = customProvider(
+            customProviderName || customModelName,
+          );
+          // swap name and displayName for bytedance
+          if (displayName && provider.providerName == "ByteDance") {
+            [customModelName, displayName] = [displayName, customModelName];
+          }
+          modelTable[`${customModelName}@${provider?.id}`] = {
+            name: customModelName,
+            displayName: displayName || customModelName,
             available,
             provider, // Use optional chaining
           };

File diff suppressed because it is too large
+ 5 - 0
docs/images/ent.svg


File diff suppressed because it is too large
+ 3 - 0
public/prompts.json


+ 1 - 1
public/serviceWorker.js

@@ -7,7 +7,7 @@ self.addEventListener("activate", function (event) {
 });
 
 self.addEventListener("install", function (event) {
-  self.skipWaiting();  // 立即启用新的版本
+  self.skipWaiting();  // enable new version
   event.waitUntil(
     caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
       return cache.addAll([]);

+ 12 - 1
public/serviceWorkerRegister.js

@@ -1,10 +1,21 @@
 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);
     });

+ 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);

二進制
src-tauri/icons/icon.icns


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

@@ -9,7 +9,7 @@
   },
   "package": {
     "productName": "NextChat",
-    "version": "2.12.4"
+    "version": "2.13.1"
   },
   "tauri": {
     "allowlist": {
@@ -112,4 +112,4 @@
       }
     ]
   }
-}
+}

Some files were not shown because too many files changed in this diff