mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			97c972b21b
			...
			sgm/cicd2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					571b6fdef7 | 
@@ -1,97 +1,8 @@
 | 
				
			|||||||
# Logs
 | 
					 | 
				
			||||||
logs
 | 
					 | 
				
			||||||
*.log
 | 
					 | 
				
			||||||
npm-debug.log*
 | 
					 | 
				
			||||||
yarn-debug.log*
 | 
					 | 
				
			||||||
yarn-error.log*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Runtime data
 | 
					 | 
				
			||||||
pids
 | 
					 | 
				
			||||||
*.pid
 | 
					 | 
				
			||||||
*.seed
 | 
					 | 
				
			||||||
*.pid.lock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Directory for instrumented libs generated by jscoverage/JSCover
 | 
					 | 
				
			||||||
lib-cov
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Coverage directory used by tools like istanbul
 | 
					 | 
				
			||||||
coverage
 | 
					 | 
				
			||||||
*.lcov
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# nyc test coverage
 | 
					 | 
				
			||||||
.nyc_output
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
 | 
					 | 
				
			||||||
.grunt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Node.js dependencies
 | 
					 | 
				
			||||||
/node_modules
 | 
					 | 
				
			||||||
/jspm_packages
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# TypeScript v1 declaration files
 | 
					 | 
				
			||||||
typings
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Optional npm cache directory
 | 
					 | 
				
			||||||
.npm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Optional eslint cache
 | 
					 | 
				
			||||||
.eslintcache
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Optional REPL history
 | 
					 | 
				
			||||||
.node_repl_history
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Output of 'npm pack'
 | 
					 | 
				
			||||||
*.tgz
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Yarn Integrity file
 | 
					 | 
				
			||||||
.yarn-integrity
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# dotenv environment variable files
 | 
					 | 
				
			||||||
.env
 | 
					 | 
				
			||||||
.env.test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# local env files
 | 
					# local env files
 | 
				
			||||||
.env*.local
 | 
					.env*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Next.js build output
 | 
					# docker-compose env files
 | 
				
			||||||
.next
 | 
					.env
 | 
				
			||||||
out
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Nuxt.js build output
 | 
					 | 
				
			||||||
.nuxt
 | 
					 | 
				
			||||||
dist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Gatsby files
 | 
					 | 
				
			||||||
.cache/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Vuepress build output
 | 
					 | 
				
			||||||
.vuepress/dist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Serverless directories
 | 
					 | 
				
			||||||
.serverless/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# FuseBox cache
 | 
					 | 
				
			||||||
.fusebox/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# DynamoDB Local files
 | 
					 | 
				
			||||||
.dynamodb/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Temporary folders
 | 
					 | 
				
			||||||
tmp
 | 
					 | 
				
			||||||
temp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# IDE and editor directories
 | 
					 | 
				
			||||||
.idea
 | 
					 | 
				
			||||||
.vscode
 | 
					 | 
				
			||||||
*.swp
 | 
					 | 
				
			||||||
*.swo
 | 
					 | 
				
			||||||
*~
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# OS generated files
 | 
					 | 
				
			||||||
.DS_Store
 | 
					 | 
				
			||||||
Thumbs.db
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# secret key
 | 
					 | 
				
			||||||
*.key
 | 
					*.key
 | 
				
			||||||
*.key.pub
 | 
					*.key.pub
 | 
				
			||||||
@@ -1,28 +1,21 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Your openai api key. (required)
 | 
					# Your openai api key. (required)
 | 
				
			||||||
OPENAI_API_KEY=sk-xxxx
 | 
					OPENAI_API_KEY=sk-xxxx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# DeepSeek Api Key. (Optional)
 | 
					# Access passsword, separated by comma. (optional)
 | 
				
			||||||
DEEPSEEK_API_KEY=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Access password, separated by comma. (optional)
 | 
					 | 
				
			||||||
CODE=your-password
 | 
					CODE=your-password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# You can start service behind a proxy. (optional)
 | 
					# You can start service behind a proxy
 | 
				
			||||||
PROXY_URL=http://localhost:7890
 | 
					PROXY_URL=http://localhost:7890
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Enable MCP functionality (optional)
 | 
					 | 
				
			||||||
# Default: Empty (disabled)
 | 
					 | 
				
			||||||
# Set to "true" to enable MCP functionality
 | 
					 | 
				
			||||||
ENABLE_MCP=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# (optional)
 | 
					# (optional)
 | 
				
			||||||
# Default: Empty
 | 
					# Default: Empty
 | 
				
			||||||
# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
 | 
					# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
 | 
				
			||||||
GOOGLE_API_KEY=
 | 
					GOOGLE_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# (optional)
 | 
					# (optional)
 | 
				
			||||||
# Default: https://generativelanguage.googleapis.com/
 | 
					# Default: https://generativelanguage.googleapis.com/
 | 
				
			||||||
# Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
 | 
					# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
 | 
				
			||||||
GOOGLE_URL=
 | 
					GOOGLE_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Override openai api request base url. (optional)
 | 
					# Override openai api request base url. (optional)
 | 
				
			||||||
@@ -54,30 +47,3 @@ ENABLE_BALANCE_QUERY=
 | 
				
			|||||||
# If you want to disable parse settings from url, set this value to 1.
 | 
					# If you want to disable parse settings from url, set this value to 1.
 | 
				
			||||||
DISABLE_FAST_LINK=
 | 
					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=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### anthropic claude Api version. (optional)
 | 
					 | 
				
			||||||
ANTHROPIC_API_VERSION=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### anthropic claude Api url (optional)
 | 
					 | 
				
			||||||
ANTHROPIC_URL=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### (optional)
 | 
					 | 
				
			||||||
WHITE_WEBDAV_ENDPOINTS=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### siliconflow Api key (optional)
 | 
					 | 
				
			||||||
SILICONFLOW_API_KEY=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### siliconflow Api url (optional)
 | 
					 | 
				
			||||||
SILICONFLOW_URL=
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1 @@
 | 
				
			|||||||
public/serviceWorker.js
 | 
					public/serviceWorker.js
 | 
				
			||||||
app/mcp/mcp_config.json
 | 
					 | 
				
			||||||
app/mcp/mcp_config.default.json
 | 
					 | 
				
			||||||
@@ -1,7 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "extends": "next/core-web-vitals",
 | 
					  "extends": "next/core-web-vitals",
 | 
				
			||||||
  "plugins": ["prettier", "unused-imports"],
 | 
					  "plugins": ["prettier"]
 | 
				
			||||||
  "rules": {
 | 
					 | 
				
			||||||
    "unused-imports/no-unused-imports": "warn"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,80 +0,0 @@
 | 
				
			|||||||
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
									
								
								.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,80 +0,0 @@
 | 
				
			|||||||
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
									
								
								.github/ISSUE_TEMPLATE/2_feature_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,21 +0,0 @@
 | 
				
			|||||||
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
									
								
								.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,21 +0,0 @@
 | 
				
			|||||||
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: 在这里添加关于问题的任何其他背景信息。
 | 
					 | 
				
			||||||
							
								
								
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Bug report
 | 
				
			||||||
 | 
					about: Create a report to help us improve
 | 
				
			||||||
 | 
					title: "[Bug] "
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the bug**
 | 
				
			||||||
 | 
					A clear and concise description of what the bug is.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**To Reproduce**
 | 
				
			||||||
 | 
					Steps to reproduce the behavior:
 | 
				
			||||||
 | 
					1. Go to '...'
 | 
				
			||||||
 | 
					2. Click on '....'
 | 
				
			||||||
 | 
					3. Scroll down to '....'
 | 
				
			||||||
 | 
					4. See error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Expected behavior**
 | 
				
			||||||
 | 
					A clear and concise description of what you expected to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Screenshots**
 | 
				
			||||||
 | 
					If applicable, add screenshots to help explain your problem.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Deployment**
 | 
				
			||||||
 | 
					- [ ] Docker
 | 
				
			||||||
 | 
					- [ ] Vercel
 | 
				
			||||||
 | 
					- [ ] Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Desktop (please complete the following information):**
 | 
				
			||||||
 | 
					 - OS: [e.g. iOS]
 | 
				
			||||||
 | 
					 - Browser [e.g. chrome, safari]
 | 
				
			||||||
 | 
					 - Version [e.g. 22]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Smartphone (please complete the following information):**
 | 
				
			||||||
 | 
					 - Device: [e.g. iPhone6]
 | 
				
			||||||
 | 
					 - OS: [e.g. iOS8.1]
 | 
				
			||||||
 | 
					 - Browser [e.g. stock browser, safari]
 | 
				
			||||||
 | 
					 - Version [e.g. 22]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional Logs**
 | 
				
			||||||
 | 
					Add any logs about the problem here.
 | 
				
			||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Feature request
 | 
				
			||||||
 | 
					about: Suggest an idea for this project
 | 
				
			||||||
 | 
					title: "[Feature] "
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Is your feature request related to a problem? Please describe.**
 | 
				
			||||||
 | 
					A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the solution you'd like**
 | 
				
			||||||
 | 
					A clear and concise description of what you want to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe alternatives you've considered**
 | 
				
			||||||
 | 
					A clear and concise description of any alternative solutions or features you've considered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional context**
 | 
				
			||||||
 | 
					Add any other context or screenshots about the feature request here.
 | 
				
			||||||
							
								
								
									
										24
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: 功能建议
 | 
				
			||||||
 | 
					about: 请告诉我们你的灵光一闪
 | 
				
			||||||
 | 
					title: "[Feature] "
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**这个功能与现有的问题有关吗?**
 | 
				
			||||||
 | 
					如果有关,请在此列出链接或者描述问题。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**你想要什么功能或者有什么建议?**
 | 
				
			||||||
 | 
					尽管告诉我们。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**有没有可以参考的同类竞品?**
 | 
				
			||||||
 | 
					可以给出参考产品的链接或者截图。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**其他信息**
 | 
				
			||||||
 | 
					可以说说你的其他考虑。
 | 
				
			||||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: 反馈问题
 | 
				
			||||||
 | 
					about: 请告诉我们你遇到的问题
 | 
				
			||||||
 | 
					title: "[Bug] "
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**反馈须知**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					请在下方中括号内输入 x 来表示你已经知晓相关内容。
 | 
				
			||||||
 | 
					- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
 | 
				
			||||||
 | 
					- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
 | 
				
			||||||
 | 
					- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**描述问题**
 | 
				
			||||||
 | 
					请在此描述你遇到了什么问题。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**如何复现**
 | 
				
			||||||
 | 
					请告诉我们你是通过什么操作触发的该问题。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**截图**
 | 
				
			||||||
 | 
					请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**一些必要的信息**
 | 
				
			||||||
 | 
					 - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
 | 
				
			||||||
 | 
					 - 浏览器: [比如 chrome, safari]
 | 
				
			||||||
 | 
					 - 版本: [填写设置页面的版本号]
 | 
				
			||||||
 | 
					 - 部署方式:[比如 vercel、docker 或者服务器部署]
 | 
				
			||||||
							
								
								
									
										28
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							@@ -1,28 +0,0 @@
 | 
				
			|||||||
#### 💻 变更类型 | 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.
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
							
								
								
									
										15
									
								
								.github/workflows/app.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/app.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,9 +43,12 @@ jobs:
 | 
				
			|||||||
          - os: ubuntu-latest
 | 
					          - os: ubuntu-latest
 | 
				
			||||||
            arch: x86_64
 | 
					            arch: x86_64
 | 
				
			||||||
            rust_target: x86_64-unknown-linux-gnu
 | 
					            rust_target: x86_64-unknown-linux-gnu
 | 
				
			||||||
 | 
					          - os: macos-latest
 | 
				
			||||||
 | 
					            arch: x86_64
 | 
				
			||||||
 | 
					            rust_target: x86_64-apple-darwin
 | 
				
			||||||
          - os: macos-latest
 | 
					          - os: macos-latest
 | 
				
			||||||
            arch: aarch64
 | 
					            arch: aarch64
 | 
				
			||||||
            rust_target: x86_64-apple-darwin,aarch64-apple-darwin
 | 
					            rust_target: aarch64-apple-darwin
 | 
				
			||||||
          - os: windows-latest
 | 
					          - os: windows-latest
 | 
				
			||||||
            arch: x86_64
 | 
					            arch: x86_64
 | 
				
			||||||
            rust_target: x86_64-pc-windows-msvc
 | 
					            rust_target: x86_64-pc-windows-msvc
 | 
				
			||||||
@@ -57,14 +60,13 @@ jobs:
 | 
				
			|||||||
        uses: actions/setup-node@v3
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 18
 | 
					          node-version: 18
 | 
				
			||||||
          cache: 'yarn'
 | 
					 | 
				
			||||||
      - name: install Rust stable
 | 
					      - name: install Rust stable
 | 
				
			||||||
        uses: dtolnay/rust-toolchain@stable
 | 
					        uses: dtolnay/rust-toolchain@stable
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          targets: ${{ matrix.config.rust_target }}
 | 
					          targets: ${{ matrix.config.rust_target }}
 | 
				
			||||||
      - uses: Swatinem/rust-cache@v2
 | 
					      - uses: Swatinem/rust-cache@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          key: ${{ matrix.config.os }}
 | 
					          key: ${{ matrix.config.rust_target }}
 | 
				
			||||||
      - name: install dependencies (ubuntu only)
 | 
					      - name: install dependencies (ubuntu only)
 | 
				
			||||||
        if: matrix.config.os == 'ubuntu-latest'
 | 
					        if: matrix.config.os == 'ubuntu-latest'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
@@ -77,15 +79,8 @@ jobs:
 | 
				
			|||||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
          TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
 | 
					          TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
 | 
				
			||||||
          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
 | 
					          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
 | 
				
			||||||
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
 | 
					 | 
				
			||||||
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
 | 
					 | 
				
			||||||
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
 | 
					 | 
				
			||||||
          APPLE_ID: ${{ secrets.APPLE_ID }}
 | 
					 | 
				
			||||||
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
 | 
					 | 
				
			||||||
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
 | 
					 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          releaseId: ${{ needs.create-release.outputs.release_id }}
 | 
					          releaseId: ${{ needs.create-release.outputs.release_id }}
 | 
				
			||||||
          args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  publish-release:
 | 
					  publish-release:
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										27
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,16 +1,17 @@
 | 
				
			|||||||
name: VercelPreviewDeployment
 | 
					name: VercelPreviewDeployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  pull_request_target:
 | 
					  workflow_dispatch:
 | 
				
			||||||
    types:
 | 
					    inputs:
 | 
				
			||||||
      - review_requested
 | 
					      branchName:
 | 
				
			||||||
 | 
					        description: 'Branch to deploy'
 | 
				
			||||||
 | 
					        required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
 | 
					 | 
				
			||||||
  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
 | 
					  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
 | 
				
			||||||
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
 | 
					  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
 | 
				
			||||||
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
					  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
				
			||||||
  VERCEL_PR_DOMAIN_SUFFIX: ${{ secrets.VERCEL_PR_DOMAIN_SUFFIX }}
 | 
					  VERCEL_DOMAIN_SUFFIX: ".pr.nextchat.dev"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
  contents: read
 | 
					  contents: read
 | 
				
			||||||
@@ -23,7 +24,7 @@ jobs:
 | 
				
			|||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v2
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.inputs.branchName }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Extract branch name
 | 
					      - name: Extract branch name
 | 
				
			||||||
        shell: bash
 | 
					        shell: bash
 | 
				
			||||||
@@ -39,15 +40,15 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: Set Environment Variables
 | 
					      - name: Set Environment Variables
 | 
				
			||||||
        id: set_env
 | 
					        id: set_env
 | 
				
			||||||
        if: github.event_name == 'pull_request_target'
 | 
					        if: github.event_name == 'pull_request'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_PR_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
 | 
					          echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install Vercel CLI
 | 
					      - name: Install Vercel CLI
 | 
				
			||||||
        run: npm install --global vercel@latest
 | 
					        run: npm install --global vercel@latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Cache dependencies
 | 
					      - name: Cache dependencies
 | 
				
			||||||
        uses: actions/cache@v4
 | 
					        uses: actions/cache@v2
 | 
				
			||||||
        id: cache-npm
 | 
					        id: cache-npm
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          path: ~/.npm
 | 
					          path: ~/.npm
 | 
				
			||||||
@@ -62,16 +63,18 @@ jobs:
 | 
				
			|||||||
        env:
 | 
					        env:
 | 
				
			||||||
          META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
 | 
					          META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          set -e
 | 
					          env
 | 
				
			||||||
          vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
 | 
					          vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
 | 
				
			||||||
          vercel build --token=${VERCEL_TOKEN}
 | 
					          vercel build --token=${VERCEL_TOKEN}
 | 
				
			||||||
          vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
 | 
					          vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          DEFAULT_URL=$(vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }})
 | 
					          vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }} &> vercel-output
 | 
				
			||||||
          ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} --scope ${VERCEL_TEAM}| awk '{print $3}')
 | 
					          DEFAULT_URL=$(cat vercel-output | grep http | awk '{print $2}')
 | 
				
			||||||
 | 
					          ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} | awk '{print $3}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          echo "New preview URL: ${DEFAULT_URL}"
 | 
					          echo "New preview URL: ${DEFAULT_URL}"
 | 
				
			||||||
          echo "New alias URL: ${ALIAS_URL}"
 | 
					          echo "New alias URL: ${ALIAS_URL}"
 | 
				
			||||||
 | 
					          echo "META_TAG=${META_TAG}"
 | 
				
			||||||
          echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
 | 
					          echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: mshick/add-pr-comment@v2
 | 
					      - uses: mshick/add-pr-comment@v2
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/remove_deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/remove_deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							@@ -11,7 +11,7 @@ env:
 | 
				
			|||||||
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
					  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  pull_request_target:
 | 
					  pull_request:
 | 
				
			||||||
    types:
 | 
					    types:
 | 
				
			||||||
      - closed
 | 
					      - closed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,39 +0,0 @@
 | 
				
			|||||||
name: Run Tests
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
on:
 | 
					 | 
				
			||||||
  push:
 | 
					 | 
				
			||||||
    branches:
 | 
					 | 
				
			||||||
      - main
 | 
					 | 
				
			||||||
    tags:
 | 
					 | 
				
			||||||
      - "!*"
 | 
					 | 
				
			||||||
  pull_request:
 | 
					 | 
				
			||||||
    types:
 | 
					 | 
				
			||||||
      - review_requested
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
jobs:
 | 
					 | 
				
			||||||
  test:
 | 
					 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    steps:
 | 
					 | 
				
			||||||
      - name: Checkout repository
 | 
					 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Set up Node.js
 | 
					 | 
				
			||||||
        uses: actions/setup-node@v3
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          node-version: 18
 | 
					 | 
				
			||||||
          cache: "yarn"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Cache node_modules
 | 
					 | 
				
			||||||
        uses: actions/cache@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          path: node_modules
 | 
					 | 
				
			||||||
          key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
 | 
					 | 
				
			||||||
          restore-keys: |
 | 
					 | 
				
			||||||
            ${{ runner.os }}-node_modules-
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Install dependencies
 | 
					 | 
				
			||||||
        run: yarn install
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Run Jest tests
 | 
					 | 
				
			||||||
        run: yarn test:ci
 | 
					 | 
				
			||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -44,8 +44,3 @@ dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
*.key
 | 
					*.key
 | 
				
			||||||
*.key.pub
 | 
					*.key.pub
 | 
				
			||||||
 | 
					 | 
				
			||||||
masks.json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# mcp config
 | 
					 | 
				
			||||||
app/mcp/mcp_config.json
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,20 +34,16 @@ ENV PROXY_URL=""
 | 
				
			|||||||
ENV OPENAI_API_KEY=""
 | 
					ENV OPENAI_API_KEY=""
 | 
				
			||||||
ENV GOOGLE_API_KEY=""
 | 
					ENV GOOGLE_API_KEY=""
 | 
				
			||||||
ENV CODE=""
 | 
					ENV CODE=""
 | 
				
			||||||
ENV ENABLE_MCP=""
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY --from=builder /app/public ./public
 | 
					COPY --from=builder /app/public ./public
 | 
				
			||||||
COPY --from=builder /app/.next/standalone ./
 | 
					COPY --from=builder /app/.next/standalone ./
 | 
				
			||||||
COPY --from=builder /app/.next/static ./.next/static
 | 
					COPY --from=builder /app/.next/static ./.next/static
 | 
				
			||||||
COPY --from=builder /app/.next/server ./.next/server
 | 
					COPY --from=builder /app/.next/server ./.next/server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
 | 
					 | 
				
			||||||
COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
EXPOSE 3000
 | 
					EXPOSE 3000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CMD if [ -n "$PROXY_URL" ]; then \
 | 
					CMD if [ -n "$PROXY_URL" ]; then \
 | 
				
			||||||
    export HOSTNAME="0.0.0.0"; \
 | 
					    export HOSTNAME="127.0.0.1"; \
 | 
				
			||||||
    protocol=$(echo $PROXY_URL | cut -d: -f1); \
 | 
					    protocol=$(echo $PROXY_URL | cut -d: -f1); \
 | 
				
			||||||
    host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
 | 
					    host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
 | 
				
			||||||
    port=$(echo $PROXY_URL | cut -d: -f3); \
 | 
					    port=$(echo $PROXY_URL | cut -d: -f3); \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
MIT License
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Copyright (c) 2023-2025 NextChat
 | 
					Copyright (c) 2023 Zhang Yifei
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
of this software and associated documentation files (the "Software"), to deal
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										293
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										293
									
								
								README.md
									
									
									
									
									
								
							@@ -1,79 +1,41 @@
 | 
				
			|||||||
<div align="center">
 | 
					<div align="center">
 | 
				
			||||||
 | 
					<img src="./docs/images/head-cover.png" alt="icon"/>
 | 
				
			||||||
<a href='https://nextchat.club'>
 | 
					 | 
				
			||||||
  <img src="https://github.com/user-attachments/assets/83bdcc07-ae5e-4954-a53a-ac151ba6ccf3" width="1000" alt="icon"/>
 | 
					 | 
				
			||||||
</a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h1 align="center">NextChat (ChatGPT Next Web)</h1>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<h1 align="center">NextChat</h1>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
English / [简体中文](./README_CN.md)
 | 
					English / [简体中文](./README_CN.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<a href="https://trendshift.io/repositories/5973" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5973" alt="ChatGPTNextWeb%2FChatGPT-Next-Web | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
					One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
✨ Light and Fast AI Assistant,with Claude, DeepSeek, GPT4 & Gemini Pro support. 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[![Saas][Saas-image]][saas-url]
 | 
					 | 
				
			||||||
[![Web][Web-image]][web-url]
 | 
					[![Web][Web-image]][web-url]
 | 
				
			||||||
[![Windows][Windows-image]][download-url]
 | 
					[![Windows][Windows-image]][download-url]
 | 
				
			||||||
[![MacOS][MacOS-image]][download-url]
 | 
					[![MacOS][MacOS-image]][download-url]
 | 
				
			||||||
[![Linux][Linux-image]][download-url]
 | 
					[![Linux][Linux-image]][download-url]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[NextChatAI](https://nextchat.club?utm_source=readme) / [iOS APP](https://apps.apple.com/us/app/nextchat-ai/id6743085599) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Enterprise Edition](#enterprise-edition) 
 | 
					[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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[saas-url]: https://nextchat.club?utm_source=readme
 | 
					[web-url]: https://chatgpt.nextweb.fun
 | 
				
			||||||
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
 | 
					 | 
				
			||||||
[web-url]: https://app.nextchat.dev/
 | 
					 | 
				
			||||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
 | 
					[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
 | 
				
			||||||
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
 | 
					[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
 | 
				
			||||||
[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
 | 
					[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
 | 
				
			||||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 | 
					[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 | 
				
			||||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 | 
					[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://vercel.com/button" alt="Deploy on Vercel" 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://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/ChatGPTNextWeb/NextChat) 
 | 
					[](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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="50" width="" >](https://monica.im/?utm=nxcrp)
 | 
					[](https://zeabur.com/templates/ZBUEFA)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 🥳 Cheer for NextChat iOS Version Online!
 | 
					 | 
				
			||||||
> [👉 Click Here to Install Now](https://apps.apple.com/us/app/nextchat-ai/id6743085599)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> [❤️ Source Code Coming Soon](https://github.com/ChatGPTNextWeb/NextChat-iOS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
					 | 
				
			||||||
## 🫣 NextChat Support MCP  ! 
 | 
					 | 
				
			||||||
> Before build, please set env ENABLE_MCP=true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Enterprise Edition
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Screenshots
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **Deploy for free with one-click** on Vercel in under 1 minute
 | 
					- **Deploy for free with one-click** on Vercel in under 1 minute
 | 
				
			||||||
@@ -88,12 +50,6 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
 | 
					- 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
 | 
					- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div align="center">
 | 
					 | 
				
			||||||
   
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Roadmap
 | 
					## Roadmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
					- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
				
			||||||
@@ -102,25 +58,52 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
					- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
				
			||||||
- [x] Desktop App with tauri
 | 
					- [x] Desktop App with tauri
 | 
				
			||||||
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 | 
					- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 | 
				
			||||||
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
					- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
				
			||||||
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
					 | 
				
			||||||
  - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
					 | 
				
			||||||
- [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
					 | 
				
			||||||
- [ ] local knowledge base
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What's New
 | 
					## What's New
 | 
				
			||||||
- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
					
 | 
				
			||||||
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
 | 
					 | 
				
			||||||
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
 | 
					 | 
				
			||||||
- 🚀 v2.14.0 Now supports  Artifacts & SD 
 | 
					 | 
				
			||||||
- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
					- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
				
			||||||
- 🚀 v2.9.11 you can use azure endpoint now.
 | 
					- 🚀 v2.9.11 you can use azure endpoint now.
 | 
				
			||||||
- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
					- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
				
			||||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
 | 
					- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
 | 
				
			||||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
					- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 主要功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 在 1 分钟内使用 Vercel **免费一键部署**
 | 
				
			||||||
 | 
					- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
 | 
				
			||||||
 | 
					- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
 | 
				
			||||||
 | 
					- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
 | 
				
			||||||
 | 
					- 极快的首屏加载速度(~100kb),支持流式响应
 | 
				
			||||||
 | 
					- 隐私安全,所有数据保存在用户浏览器本地
 | 
				
			||||||
 | 
					- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
 | 
				
			||||||
 | 
					- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
 | 
				
			||||||
 | 
					- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
 | 
				
			||||||
 | 
					- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
 | 
				
			||||||
 | 
					- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 开发计划
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
				
			||||||
 | 
					- [x] 允许用户自行编辑内置 Prompt 列表
 | 
				
			||||||
 | 
					- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
 | 
				
			||||||
 | 
					- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
				
			||||||
 | 
					- [x] 使用 tauri 打包桌面应用
 | 
				
			||||||
 | 
					- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
 | 
				
			||||||
 | 
					- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 最新动态
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
 | 
				
			||||||
 | 
					- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
 | 
				
			||||||
 | 
					- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
 | 
				
			||||||
 | 
					- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
 | 
				
			||||||
 | 
					- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Get Started
 | 
					## Get Started
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
					1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
				
			||||||
2. Click
 | 
					2. Click
 | 
				
			||||||
   [](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), remember that `CODE` is your page password;
 | 
					   [](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), remember that `CODE` is your page password;
 | 
				
			||||||
@@ -128,10 +111,14 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## FAQ
 | 
					## FAQ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[简体中文 > 常见问题](./docs/faq-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[English > FAQ](./docs/faq-en.md)
 | 
					[English > FAQ](./docs/faq-en.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Keep Updated
 | 
					## Keep Updated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
					If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
We recommend that you follow the steps below to re-deploy:
 | 
					We recommend that you follow the steps below to re-deploy:
 | 
				
			||||||
@@ -142,7 +129,7 @@ We recommend that you follow the steps below to re-deploy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Enable Automatic Updates
 | 
					### Enable Automatic Updates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
 | 
					> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
					After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -158,6 +145,8 @@ You can star or watch this project or follow author to get release notifications
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Access Password
 | 
					## Access Password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
					This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
@@ -168,6 +157,8 @@ After adding or modifying this environment variable, please redeploy the project
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Environment Variables
 | 
					## Environment Variables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `CODE` (optional)
 | 
					### `CODE` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Access password, separated by comma.
 | 
					Access password, separated by comma.
 | 
				
			||||||
@@ -190,7 +181,7 @@ Specify OpenAI organization ID.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### `AZURE_URL` (optional)
 | 
					### `AZURE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Example: https://{azure-resource-url}/openai
 | 
					> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Azure deploy url.
 | 
					Azure deploy url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -210,74 +201,6 @@ Google Gemini Pro Api Key.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Google Gemini Pro Api Url.
 | 
					Google Gemini Pro Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `ANTHROPIC_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ANTHROPIC_API_VERSION` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ANTHROPIC_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_SECRET_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Secret Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BYTEDANCE_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ByteDance Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BYTEDANCE_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ByteDance Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ALIBABA_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Alibaba Cloud Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ALIBABA_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Alibaba Cloud Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_URL` (Optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
iflytek Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_API_KEY` (Optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
iflytek Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_API_SECRET` (Optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
iflytek Api Secret.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CHATGLM_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ChatGLM Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CHATGLM_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ChatGLM Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEEPSEEK_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DeepSeek Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEEPSEEK_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DeepSeek Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `HIDE_USER_API_KEY` (optional)
 | 
					### `HIDE_USER_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Default: Empty
 | 
					> Default: Empty
 | 
				
			||||||
@@ -294,7 +217,7 @@ If you do not want users to use GPT-4, set this value to 1.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
> Default: Empty
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If you do want users to query balance, set this value to 1.
 | 
					If you do want users to query balance, set this value to 1, or you should set it to 0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `DISABLE_FAST_LINK` (optional)
 | 
					### `DISABLE_FAST_LINK` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -311,62 +234,13 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
User `-all` to disable all default models, `+all` to enable all default models.
 | 
					User `-all` to disable all default models, `+all` to enable all default models.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
 | 
					 | 
				
			||||||
> 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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEFAULT_MODEL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Change default model
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `VISION_MODELS` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> Default: Empty
 | 
					 | 
				
			||||||
> Example: `gpt-4-vision,claude-3-opus,my-custom-model` means add vision capabilities to these models in addition to the default pattern matches (which detect models containing keywords like "vision", "claude-3", "gemini-1.5", etc).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Add additional models to have vision capabilities, beyond the default pattern matching. Multiple models should be separated by commas.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `WHITE_WEBDAV_ENDPOINTS` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
 | 
					 | 
				
			||||||
- Each address must be a complete endpoint 
 | 
					 | 
				
			||||||
> `https://xxxx/yyy`
 | 
					 | 
				
			||||||
- Multiple addresses are connected by ', '
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEFAULT_INPUT_TEMPLATE` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `STABILITY_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Stability API key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `STABILITY_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Customize Stability API url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ENABLE_MCP` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Enable MCP(Model Context Protocol)Feature
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `SILICONFLOW_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SiliconFlow API Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `SILICONFLOW_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SiliconFlow API URL.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Requirements
 | 
					## Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NodeJS >= 18, Docker >= 20
 | 
					NodeJS >= 18, Docker >= 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -391,6 +265,7 @@ yarn dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Deployment
 | 
					## Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Docker (Recommended)
 | 
					### Docker (Recommended)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -419,16 +294,6 @@ If your proxy needs password, use:
 | 
				
			|||||||
-e PROXY_URL="http://127.0.0.1:7890 user pass"
 | 
					-e PROXY_URL="http://127.0.0.1:7890 user pass"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If enable MCP, use:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					 | 
				
			||||||
   -e OPENAI_API_KEY=sk-xxxx \
 | 
					 | 
				
			||||||
   -e CODE=your-password \
 | 
					 | 
				
			||||||
   -e ENABLE_MCP=true \
 | 
					 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Shell
 | 
					### Shell
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
@@ -449,7 +314,11 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
 | 
				
			|||||||
- [How to use Vercel (No English)](./docs/vercel-cn.md)
 | 
					- [How to use Vercel (No English)](./docs/vercel-cn.md)
 | 
				
			||||||
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
 | 
					- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Translation
 | 
					## Translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -461,7 +330,37 @@ If you want to add a new translation, read this [document](./docs/translation.md
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Special Thanks
 | 
					## Special Thanks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Sponsor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 仅列出捐赠金额 >= 100RMB 的用户。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[@mushan0x0](https://github.com/mushan0x0)
 | 
				
			||||||
 | 
					[@ClarenceDan](https://github.com/ClarenceDan)
 | 
				
			||||||
 | 
					[@zhangjia](https://github.com/zhangjia)
 | 
				
			||||||
 | 
					[@hoochanlon](https://github.com/hoochanlon)
 | 
				
			||||||
 | 
					[@relativequantum](https://github.com/relativequantum)
 | 
				
			||||||
 | 
					[@desenmeng](https://github.com/desenmeng)
 | 
				
			||||||
 | 
					[@webees](https://github.com/webees)
 | 
				
			||||||
 | 
					[@chazzhou](https://github.com/chazzhou)
 | 
				
			||||||
 | 
					[@hauy](https://github.com/hauy)
 | 
				
			||||||
 | 
					[@Corwin006](https://github.com/Corwin006)
 | 
				
			||||||
 | 
					[@yankunsong](https://github.com/yankunsong)
 | 
				
			||||||
 | 
					[@ypwhs](https://github.com/ypwhs)
 | 
				
			||||||
 | 
					[@fxxxchao](https://github.com/fxxxchao)
 | 
				
			||||||
 | 
					[@hotic](https://github.com/hotic)
 | 
				
			||||||
 | 
					[@WingCH](https://github.com/WingCH)
 | 
				
			||||||
 | 
					[@jtung4](https://github.com/jtung4)
 | 
				
			||||||
 | 
					[@micozhu](https://github.com/micozhu)
 | 
				
			||||||
 | 
					[@jhansion](https://github.com/jhansion)
 | 
				
			||||||
 | 
					[@Sha1rholder](https://github.com/Sha1rholder)
 | 
				
			||||||
 | 
					[@AnsonHyq](https://github.com/AnsonHyq)
 | 
				
			||||||
 | 
					[@synwith](https://github.com/synwith)
 | 
				
			||||||
 | 
					[@piksonGit](https://github.com/piksonGit)
 | 
				
			||||||
 | 
					[@ouyangzhiping](https://github.com/ouyangzhiping)
 | 
				
			||||||
 | 
					[@wenjiavv](https://github.com/wenjiavv)
 | 
				
			||||||
 | 
					[@LeXwDeX](https://github.com/LeXwDeX)
 | 
				
			||||||
 | 
					[@Licoy](https://github.com/Licoy)
 | 
				
			||||||
 | 
					[@shangmin2009](https://github.com/shangmin2009)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Contributors
 | 
					### Contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										181
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								README_CN.md
									
									
									
									
									
								
							@@ -1,35 +1,22 @@
 | 
				
			|||||||
<div align="center">
 | 
					<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>
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。
 | 
					一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[NextChatAI](https://nextchat.club?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
 | 
					[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[](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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://zeabur.com/templates/ZBUEFA)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 企业版
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
满足您公司私有化部署和定制需求
 | 
					 | 
				
			||||||
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
 | 
					 | 
				
			||||||
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
 | 
					 | 
				
			||||||
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
 | 
					 | 
				
			||||||
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
 | 
					 | 
				
			||||||
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
 | 
					 | 
				
			||||||
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
 | 
					 | 
				
			||||||
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
企业版咨询: **business@nextchat.dev**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 开始使用
 | 
					## 开始使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
					1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
				
			||||||
@@ -38,12 +25,6 @@
 | 
				
			|||||||
3. 部署完毕后,即可开始使用;
 | 
					3. 部署完毕后,即可开始使用;
 | 
				
			||||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
 | 
					4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div align="center">
 | 
					 | 
				
			||||||
   
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 保持更新
 | 
					## 保持更新
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
 | 
					如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
 | 
				
			||||||
@@ -55,7 +36,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### 打开自动更新
 | 
					### 打开自动更新
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)!
 | 
					> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
					当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,7 +70,7 @@ code1,code2,code3
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### `OPENAI_API_KEY` (必填项)
 | 
					### `OPENAI_API_KEY` (必填项)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OpenAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
 | 
					OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `CODE` (可选)
 | 
					### `CODE` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,7 +94,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### `AZURE_URL` (可选)
 | 
					### `AZURE_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 形如:https://{azure-resource-url}/openai
 | 
					> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Azure 部署地址。
 | 
					Azure 部署地址。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,83 +106,14 @@ Azure 密钥。
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
 | 
					Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `GOOGLE_API_KEY` (可选)
 | 
					### `GOOGLE_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Google Gemini Pro 密钥.
 | 
					Google Gemini Pro 密钥.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `GOOGLE_URL` (可选)
 | 
					### `GOOGLE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Google Gemini Pro Api Url.
 | 
					Google Gemini Pro Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `ANTHROPIC_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ANTHROPIC_API_VERSION` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ANTHROPIC_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
anthropic claude Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_SECRET_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Secret Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BAIDU_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Baidu Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BYTEDANCE_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ByteDance Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `BYTEDANCE_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ByteDance Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ALIBABA_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
阿里云(千问)Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ALIBABA_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
阿里云(千问)Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
讯飞星火Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
讯飞星火Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `IFLYTEK_API_SECRET` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
讯飞星火Api Secret.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CHATGLM_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ChatGLM Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CHATGLM_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ChatGLM Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEEPSEEK_API_KEY` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DeepSeek Api Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEEPSEEK_URL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DeepSeek Api Url.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `HIDE_USER_API_KEY` (可选)
 | 
					### `HIDE_USER_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
					如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
				
			||||||
@@ -218,13 +130,6 @@ DeepSeek Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
					如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `WHITE_WEBDAV_ENDPOINTS` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
					 | 
				
			||||||
- 每一个地址必须是一个完整的 endpoint
 | 
					 | 
				
			||||||
> `https://xxxx/xxx`
 | 
					 | 
				
			||||||
- 多个地址以`,`相连
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CUSTOM_MODELS` (可选)
 | 
					### `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`。
 | 
					> 示例:`+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`。
 | 
				
			||||||
@@ -232,49 +137,6 @@ DeepSeek Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 | 
					用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
					 | 
				
			||||||
> 示例:`+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)`的选项
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEFAULT_MODEL` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
更改默认模型
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `VISION_MODELS` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> 默认值:空
 | 
					 | 
				
			||||||
> 示例:`gpt-4-vision,claude-3-opus,my-custom-model` 表示为这些模型添加视觉能力,作为对默认模式匹配的补充(默认会检测包含"vision"、"claude-3"、"gemini-1.5"等关键词的模型)。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
在默认模式匹配之外,添加更多具有视觉能力的模型。多个模型用逗号分隔。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEFAULT_INPUT_TEMPLATE` (可选)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `STABILITY_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Stability API密钥
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `STABILITY_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
自定义的Stability API请求地址
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `ENABLE_MCP` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
启用MCP(Model Context Protocol)功能
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `SILICONFLOW_API_KEY` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SiliconFlow API Key.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `SILICONFLOW_URL` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SiliconFlow API URL.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 开发
 | 
					## 开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
点击下方按钮,开始二次开发:
 | 
					点击下方按钮,开始二次开发:
 | 
				
			||||||
@@ -298,9 +160,6 @@ BASE_URL=https://b.nextweb.fun/api/proxy
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## 部署
 | 
					## 部署
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 宝塔面板部署
 | 
					 | 
				
			||||||
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 容器部署 (推荐)
 | 
					### 容器部署 (推荐)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
					> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
				
			||||||
@@ -327,16 +186,6 @@ docker run -d -p 3000:3000 \
 | 
				
			|||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如需启用 MCP 功能,可以使用:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```shell
 | 
					 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					 | 
				
			||||||
   -e OPENAI_API_KEY=sk-xxxx \
 | 
					 | 
				
			||||||
   -e CODE=页面访问密码 \
 | 
					 | 
				
			||||||
   -e ENABLE_MCP=true \
 | 
					 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
如果你的本地代理需要账号密码,可以使用:
 | 
					如果你的本地代理需要账号密码,可以使用:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										317
									
								
								README_JA.md
									
									
									
									
									
								
							
							
						
						
									
										317
									
								
								README_JA.md
									
									
									
									
									
								
							@@ -1,317 +0,0 @@
 | 
				
			|||||||
<div align="center">
 | 
					 | 
				
			||||||
<img src="./docs/images/ent.svg" alt="プレビュー"/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<h1 align="center">NextChat</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[NextChatAI](https://nextchat.club?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 企業版
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
 | 
					 | 
				
			||||||
- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
 | 
					 | 
				
			||||||
- **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
 | 
					 | 
				
			||||||
- **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
 | 
					 | 
				
			||||||
- **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
 | 
					 | 
				
			||||||
- **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
 | 
					 | 
				
			||||||
- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
 | 
					 | 
				
			||||||
- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
企業版のお問い合わせ: **business@nextchat.dev**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 始めに
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
 | 
					 | 
				
			||||||
2. 右側のボタンをクリックしてデプロイを開始:
 | 
					 | 
				
			||||||
   [](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">
 | 
					 | 
				
			||||||
   
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 更新を維持する
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
以下の手順に従って再デプロイすることをお勧めします:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 元のリポジトリを削除する
 | 
					 | 
				
			||||||
- ページ右上の fork ボタンを使って、本プロジェクトを fork する
 | 
					 | 
				
			||||||
- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 自動更新を開く
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 手動でコードを更新する
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
手動で即座に更新したい場合は、[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_WEBDAV_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` (オプション)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
デフォルトのモデルを変更します。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `VISION_MODELS` (オプション)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> デフォルト:空
 | 
					 | 
				
			||||||
> 例:`gpt-4-vision,claude-3-opus,my-custom-model` は、これらのモデルにビジョン機能を追加します。これはデフォルトのパターンマッチング("vision"、"claude-3"、"gemini-1.5"などのキーワードを含むモデルを検出)に加えて適用されます。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
デフォルトのパターンマッチングに加えて、追加のモデルにビジョン機能を付与します。複数のモデルはカンマで区切ります。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `DEFAULT_INPUT_TEMPLATE` (オプション)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 開発
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
下のボタンをクリックして二次開発を開始してください:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[](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/)
 | 
					 | 
				
			||||||
@@ -1,82 +0,0 @@
 | 
				
			|||||||
import { ApiPath } from "@/app/constant";
 | 
					 | 
				
			||||||
import { NextRequest } from "next/server";
 | 
					 | 
				
			||||||
import { handle as openaiHandler } from "../../openai";
 | 
					 | 
				
			||||||
import { handle as azureHandler } from "../../azure";
 | 
					 | 
				
			||||||
import { handle as googleHandler } from "../../google";
 | 
					 | 
				
			||||||
import { handle as anthropicHandler } from "../../anthropic";
 | 
					 | 
				
			||||||
import { handle as baiduHandler } from "../../baidu";
 | 
					 | 
				
			||||||
import { handle as bytedanceHandler } from "../../bytedance";
 | 
					 | 
				
			||||||
import { handle as alibabaHandler } from "../../alibaba";
 | 
					 | 
				
			||||||
import { handle as moonshotHandler } from "../../moonshot";
 | 
					 | 
				
			||||||
import { handle as stabilityHandler } from "../../stability";
 | 
					 | 
				
			||||||
import { handle as iflytekHandler } from "../../iflytek";
 | 
					 | 
				
			||||||
import { handle as deepseekHandler } from "../../deepseek";
 | 
					 | 
				
			||||||
import { handle as siliconflowHandler } from "../../siliconflow";
 | 
					 | 
				
			||||||
import { handle as xaiHandler } from "../../xai";
 | 
					 | 
				
			||||||
import { handle as chatglmHandler } from "../../glm";
 | 
					 | 
				
			||||||
import { handle as proxyHandler } from "../../proxy";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { provider: string; path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  const apiPath = `/api/${params.provider}`;
 | 
					 | 
				
			||||||
  console.log(`[${params.provider} Route] params `, params);
 | 
					 | 
				
			||||||
  switch (apiPath) {
 | 
					 | 
				
			||||||
    case ApiPath.Azure:
 | 
					 | 
				
			||||||
      return azureHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Google:
 | 
					 | 
				
			||||||
      return googleHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Anthropic:
 | 
					 | 
				
			||||||
      return anthropicHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Baidu:
 | 
					 | 
				
			||||||
      return baiduHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.ByteDance:
 | 
					 | 
				
			||||||
      return bytedanceHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Alibaba:
 | 
					 | 
				
			||||||
      return alibabaHandler(req, { params });
 | 
					 | 
				
			||||||
    // case ApiPath.Tencent: using "/api/tencent"
 | 
					 | 
				
			||||||
    case ApiPath.Moonshot:
 | 
					 | 
				
			||||||
      return moonshotHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Stability:
 | 
					 | 
				
			||||||
      return stabilityHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.Iflytek:
 | 
					 | 
				
			||||||
      return iflytekHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.DeepSeek:
 | 
					 | 
				
			||||||
      return deepseekHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.XAI:
 | 
					 | 
				
			||||||
      return xaiHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.ChatGLM:
 | 
					 | 
				
			||||||
      return chatglmHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.SiliconFlow:
 | 
					 | 
				
			||||||
      return siliconflowHandler(req, { params });
 | 
					 | 
				
			||||||
    case ApiPath.OpenAI:
 | 
					 | 
				
			||||||
      return openaiHandler(req, { params });
 | 
					 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      return proxyHandler(req, { params });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
@@ -1,129 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ALIBABA_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Alibaba Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Qwen);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Alibaba] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
      "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.Alibaba as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[Alibaba] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,170 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ANTHROPIC_BASE_URL,
 | 
					 | 
				
			||||||
  Anthropic,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "./auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Anthropic Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const subpath = params.path.join("/");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!ALLOWD_PATH.has(subpath)) {
 | 
					 | 
				
			||||||
    console.log("[Anthropic Route] forbidden path ", subpath);
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + subpath,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Claude);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Anthropic] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let authHeaderName = "x-api-key";
 | 
					 | 
				
			||||||
  let authValue =
 | 
					 | 
				
			||||||
    req.headers.get(authHeaderName) ||
 | 
					 | 
				
			||||||
    req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
 | 
					 | 
				
			||||||
    serverConfig.anthropicApiKey ||
 | 
					 | 
				
			||||||
    "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl =
 | 
					 | 
				
			||||||
    serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // try rebuild url, when using cloudflare ai gateway in server
 | 
					 | 
				
			||||||
  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      "Cache-Control": "no-store",
 | 
					 | 
				
			||||||
      "anthropic-dangerous-direct-browser-access": "true",
 | 
					 | 
				
			||||||
      [authHeaderName]: authValue,
 | 
					 | 
				
			||||||
      "anthropic-version":
 | 
					 | 
				
			||||||
        req.headers.get("anthropic-version") ||
 | 
					 | 
				
			||||||
        serverConfig.anthropicApiVersion ||
 | 
					 | 
				
			||||||
        Anthropic.Vision,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.Anthropic as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[Anthropic] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // 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,
 | 
					 | 
				
			||||||
    // );
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,73 +0,0 @@
 | 
				
			|||||||
import md5 from "spark-md5";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handle(req: NextRequest, res: NextResponse) {
 | 
					 | 
				
			||||||
  const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
  const storeUrl = () =>
 | 
					 | 
				
			||||||
    `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
 | 
					 | 
				
			||||||
  const storeHeaders = () => ({
 | 
					 | 
				
			||||||
    Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  if (req.method === "POST") {
 | 
					 | 
				
			||||||
    const clonedBody = await req.text();
 | 
					 | 
				
			||||||
    const hashedCode = md5.hash(clonedBody).trim();
 | 
					 | 
				
			||||||
    const body: {
 | 
					 | 
				
			||||||
      key: string;
 | 
					 | 
				
			||||||
      value: string;
 | 
					 | 
				
			||||||
      expiration_ttl?: number;
 | 
					 | 
				
			||||||
    } = {
 | 
					 | 
				
			||||||
      key: hashedCode,
 | 
					 | 
				
			||||||
      value: clonedBody,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
 | 
					 | 
				
			||||||
      if (ttl > 60) {
 | 
					 | 
				
			||||||
        body["expiration_ttl"] = ttl;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const res = await fetch(`${storeUrl()}/bulk`, {
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        ...storeHeaders(),
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      method: "PUT",
 | 
					 | 
				
			||||||
      body: JSON.stringify([body]),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const result = await res.json();
 | 
					 | 
				
			||||||
    console.log("save data", result);
 | 
					 | 
				
			||||||
    if (result?.success) {
 | 
					 | 
				
			||||||
      return NextResponse.json(
 | 
					 | 
				
			||||||
        { code: 0, id: hashedCode, result },
 | 
					 | 
				
			||||||
        { status: res.status },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      { error: true, msg: "Save data error" },
 | 
					 | 
				
			||||||
      { status: 400 },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (req.method === "GET") {
 | 
					 | 
				
			||||||
    const id = req?.nextUrl?.searchParams?.get("id");
 | 
					 | 
				
			||||||
    const res = await fetch(`${storeUrl()}/values/${id}`, {
 | 
					 | 
				
			||||||
      headers: storeHeaders(),
 | 
					 | 
				
			||||||
      method: "GET",
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: res.headers,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return NextResponse.json(
 | 
					 | 
				
			||||||
    { error: true, msg: "Invalid request" },
 | 
					 | 
				
			||||||
    { status: 400 },
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
@@ -57,62 +57,12 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
				
			|||||||
  if (!apiKey) {
 | 
					  if (!apiKey) {
 | 
				
			||||||
    const serverConfig = getServerSideConfig();
 | 
					    const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // const systemApiKey =
 | 
					    const systemApiKey =
 | 
				
			||||||
    //   modelProvider === ModelProvider.GeminiPro
 | 
					      modelProvider === ModelProvider.GeminiPro
 | 
				
			||||||
    //     ? serverConfig.googleApiKey
 | 
					        ? serverConfig.googleApiKey
 | 
				
			||||||
    //     : serverConfig.isAzure
 | 
					        : serverConfig.isAzure
 | 
				
			||||||
    //     ? serverConfig.azureApiKey
 | 
					        ? serverConfig.azureApiKey
 | 
				
			||||||
    //     : serverConfig.apiKey;
 | 
					        : serverConfig.apiKey;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    let systemApiKey: string | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    switch (modelProvider) {
 | 
					 | 
				
			||||||
      case ModelProvider.Stability:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.stabilityApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.GeminiPro:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.googleApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Claude:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.anthropicApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Doubao:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.bytedanceApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Ernie:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.baiduApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Qwen:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.alibabaApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Moonshot:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.moonshotApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Iflytek:
 | 
					 | 
				
			||||||
        systemApiKey =
 | 
					 | 
				
			||||||
          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.DeepSeek:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.deepseekApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.XAI:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.xaiApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.ChatGLM:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.chatglmApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.SiliconFlow:
 | 
					 | 
				
			||||||
        systemApiKey = serverConfig.siliconFlowApiKey;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.GPT:
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        if (req.nextUrl.pathname.includes("azure/deployments")) {
 | 
					 | 
				
			||||||
          systemApiKey = serverConfig.azureApiKey;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          systemApiKey = serverConfig.apiKey;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (systemApiKey) {
 | 
					    if (systemApiKey) {
 | 
				
			||||||
      console.log("[Auth] use system api key");
 | 
					      console.log("[Auth] use system api key");
 | 
				
			||||||
      req.headers.set("Authorization", `Bearer ${systemApiKey}`);
 | 
					      req.headers.set("Authorization", `Bearer ${systemApiKey}`);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +0,0 @@
 | 
				
			|||||||
import { ModelProvider } from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "./auth";
 | 
					 | 
				
			||||||
import { requestOpenai } from "./common";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Azure Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const subpath = params.path.join("/");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.GPT);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    return await requestOpenai(req);
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Azure] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										144
									
								
								app/api/baidu.ts
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								app/api/baidu.ts
									
									
									
									
									
								
							@@ -1,144 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  BAIDU_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
import { getAccessToken } from "@/app/utils/baidu";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Baidu Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Ernie);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 401,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Baidu] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { access_token } = await getAccessToken(
 | 
					 | 
				
			||||||
    serverConfig.baiduApiKey as string,
 | 
					 | 
				
			||||||
    serverConfig.baiduSecretKey as string,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.Baidu as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[Baidu] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,129 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  BYTEDANCE_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[ByteDance Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Doubao);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[ByteDance] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.ByteDance as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[ByteDance] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,19 +1,17 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { getServerSideConfig } from "../config/server";
 | 
					import { getServerSideConfig } from "../config/server";
 | 
				
			||||||
import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
 | 
					import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant";
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
 | 
					import { collectModelTable } from "../utils/model";
 | 
				
			||||||
import { getModelProvider, isModelNotavailableInServer } from "../utils/model";
 | 
					import { makeAzurePath } from "../azure";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function requestOpenai(req: NextRequest) {
 | 
					export async function requestOpenai(req: NextRequest) {
 | 
				
			||||||
  const controller = new AbortController();
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isAzure = req.nextUrl.pathname.includes("azure/deployments");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  var authValue,
 | 
					  var authValue,
 | 
				
			||||||
    authHeaderName = "";
 | 
					    authHeaderName = "";
 | 
				
			||||||
  if (isAzure) {
 | 
					  if (serverConfig.isAzure) {
 | 
				
			||||||
    authValue =
 | 
					    authValue =
 | 
				
			||||||
      req.headers
 | 
					      req.headers
 | 
				
			||||||
        .get("Authorization")
 | 
					        .get("Authorization")
 | 
				
			||||||
@@ -27,10 +25,13 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
    authHeaderName = "Authorization";
 | 
					    authHeaderName = "Authorization";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
 | 
					  let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
 | 
				
			||||||
 | 
					    "/api/openai/",
 | 
				
			||||||
 | 
					    "",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let baseUrl =
 | 
					  let baseUrl =
 | 
				
			||||||
    (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
 | 
					    serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
@@ -42,6 +43,10 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					  // this fix [Org ID] undefined in server side if not using custom point
 | 
				
			||||||
 | 
					  if (serverConfig.openaiOrgId !== undefined) {
 | 
				
			||||||
 | 
					    console.log("[Org ID]", serverConfig.openaiOrgId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
    () => {
 | 
					    () => {
 | 
				
			||||||
@@ -50,46 +55,17 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
    10 * 60 * 1000,
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isAzure) {
 | 
					  if (serverConfig.isAzure) {
 | 
				
			||||||
    const azureApiVersion =
 | 
					    if (!serverConfig.azureApiVersion) {
 | 
				
			||||||
      req?.nextUrl?.searchParams?.get("api-version") ||
 | 
					      return NextResponse.json({
 | 
				
			||||||
      serverConfig.azureApiVersion;
 | 
					        error: true,
 | 
				
			||||||
    baseUrl = baseUrl.split("/deployments").shift() as string;
 | 
					        message: `missing AZURE_API_VERSION in server env vars`,
 | 
				
			||||||
    path = `${req.nextUrl.pathname.replaceAll(
 | 
					 | 
				
			||||||
      "/api/azure/",
 | 
					 | 
				
			||||||
      "",
 | 
					 | 
				
			||||||
    )}?api-version=${azureApiVersion}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Forward compatibility:
 | 
					 | 
				
			||||||
    // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
 | 
					 | 
				
			||||||
    // then using default '{deploy-id}'
 | 
					 | 
				
			||||||
    if (serverConfig.customModels && serverConfig.azureUrl) {
 | 
					 | 
				
			||||||
      const modelName = path.split("/")[1];
 | 
					 | 
				
			||||||
      let realDeployName = "";
 | 
					 | 
				
			||||||
      serverConfig.customModels
 | 
					 | 
				
			||||||
        .split(",")
 | 
					 | 
				
			||||||
        .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
 | 
					 | 
				
			||||||
        .forEach((m) => {
 | 
					 | 
				
			||||||
          const [fullName, displayName] = m.split("=");
 | 
					 | 
				
			||||||
          const [_, providerName] = getModelProvider(fullName);
 | 
					 | 
				
			||||||
          if (providerName === "azure" && !displayName) {
 | 
					 | 
				
			||||||
            const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
 | 
					 | 
				
			||||||
              "deployments/",
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            if (deployId) {
 | 
					 | 
				
			||||||
              realDeployName = deployId;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      if (realDeployName) {
 | 
					 | 
				
			||||||
        console.log("[Replace with DeployId", realDeployName);
 | 
					 | 
				
			||||||
        path = path.replaceAll(modelName, realDeployName);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    path = makeAzurePath(path, serverConfig.azureApiVersion);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
					  const fetchUrl = `${baseUrl}/${path}`;
 | 
				
			||||||
  console.log("fetchUrl", fetchUrl);
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
@@ -111,23 +87,17 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
  // #1815 try to refuse gpt4 request
 | 
					  // #1815 try to refuse gpt4 request
 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      const modelTable = collectModelTable(
 | 
				
			||||||
 | 
					        DEFAULT_MODELS,
 | 
				
			||||||
 | 
					        serverConfig.customModels,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (modelTable[jsonBody?.model ?? ""].available === false) {
 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          [
 | 
					 | 
				
			||||||
            ServiceProvider.OpenAI,
 | 
					 | 
				
			||||||
            ServiceProvider.Azure,
 | 
					 | 
				
			||||||
            jsonBody?.model as string, // support provider-unspecified model
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					        return NextResponse.json(
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            error: true,
 | 
					            error: true,
 | 
				
			||||||
@@ -146,29 +116,12 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Extract the OpenAI-Organization header from the response
 | 
					 | 
				
			||||||
    const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Check if serverConfig.openaiOrgId is defined and not an empty string
 | 
					 | 
				
			||||||
    if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
 | 
					 | 
				
			||||||
      // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
 | 
					 | 
				
			||||||
      console.log("[Org ID]", openaiOrganizationHeader);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      console.log("[Org ID] is not set up.");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
    // to disable nginx buffering
 | 
					    // to disable nginx buffering
 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
 | 
					 | 
				
			||||||
    // Also, this is to prevent the header from being sent to the client
 | 
					 | 
				
			||||||
    if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
 | 
					 | 
				
			||||||
      newHeaders.delete("OpenAI-Organization");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
					    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
				
			||||||
    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
					    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
				
			||||||
    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
					    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,6 @@ const DANGER_CONFIG = {
 | 
				
			|||||||
  hideBalanceQuery: serverConfig.hideBalanceQuery,
 | 
					  hideBalanceQuery: serverConfig.hideBalanceQuery,
 | 
				
			||||||
  disableFastLink: serverConfig.disableFastLink,
 | 
					  disableFastLink: serverConfig.disableFastLink,
 | 
				
			||||||
  customModels: serverConfig.customModels,
 | 
					  customModels: serverConfig.customModels,
 | 
				
			||||||
  defaultModel: serverConfig.defaultModel,
 | 
					 | 
				
			||||||
  visionModels: serverConfig.visionModels,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								app/api/cors/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/api/cors/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [protocol, ...subpath] = params.path;
 | 
				
			||||||
 | 
					  const targetUrl = `${protocol}://${subpath.join("/")}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const method = req.headers.get("method") ?? undefined;
 | 
				
			||||||
 | 
					  const shouldNotHaveBody = ["get", "head"].includes(
 | 
				
			||||||
 | 
					    method?.toLowerCase() ?? "",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      authorization: req.headers.get("authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: shouldNotHaveBody ? null : req.body,
 | 
				
			||||||
 | 
					    method,
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchResult = await fetch(targetUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Any Proxy]", targetUrl, {
 | 
				
			||||||
 | 
					    status: fetchResult.status,
 | 
				
			||||||
 | 
					    statusText: fetchResult.statusText,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return fetchResult;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const OPTIONS = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "nodejs";
 | 
				
			||||||
@@ -1,128 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  DEEPSEEK_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[DeepSeek Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.DeepSeek);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[DeepSeek] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.DeepSeek, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.DeepSeek as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[DeepSeek] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/glm.ts
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								app/api/glm.ts
									
									
									
									
									
								
							@@ -1,129 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  CHATGLM_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[GLM Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.ChatGLM);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[GLM] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  console.log("[Fetch Url] ", fetchUrl);
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.ChatGLM as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[GLM] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,14 +1,11 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "./auth";
 | 
					import { auth } from "../../auth";
 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
 | 
					import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					async function handle(
 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { provider: string; path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  console.log("[Google Route] params ", params);
 | 
					  console.log("[Google Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,64 +13,11 @@ export async function handle(
 | 
				
			|||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const authResult = auth(req, ModelProvider.GeminiPro);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const bearToken =
 | 
					 | 
				
			||||||
    req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
 | 
					 | 
				
			||||||
  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const apiKey = token ? token : serverConfig.googleApiKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!apiKey) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        message: `missing GOOGLE_API_KEY in server env vars`,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 401,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req, apiKey);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Google] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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();
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
 | 
					  let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
@@ -83,6 +27,8 @@ async function request(req: NextRequest, apiKey: string) {
 | 
				
			|||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -92,18 +38,36 @@ async function request(req: NextRequest, apiKey: string) {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    10 * 60 * 1000,
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}${
 | 
					 | 
				
			||||||
    req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
 | 
					 | 
				
			||||||
  }`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("[Fetch Url] ", fetchUrl);
 | 
					  const authResult = auth(req, ModelProvider.GeminiPro);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bearToken = req.headers.get("Authorization") ?? "";
 | 
				
			||||||
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const key = token ? token : serverConfig.googleApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!key) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        message: `missing GOOGLE_API_KEY in server env vars`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 401,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}/${path}?key=${key}`;
 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      "Cache-Control": "no-store",
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
      "x-goog-api-key":
 | 
					 | 
				
			||||||
        req.headers.get("x-goog-api-key") ||
 | 
					 | 
				
			||||||
        (req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    method: req.method,
 | 
					    method: req.method,
 | 
				
			||||||
    body: req.body,
 | 
					    body: req.body,
 | 
				
			||||||
@@ -131,3 +95,27 @@ async function request(req: NextRequest, apiKey: string) {
 | 
				
			|||||||
    clearTimeout(timeoutId);
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
@@ -1,129 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  IFLYTEK_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
// iflytek
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Iflytek Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Iflytek);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Iflytek] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // iflytek use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.Iflytek as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[Iflytek] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,128 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  MOONSHOT_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Moonshot Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Moonshot);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Moonshot] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.Moonshot as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[Moonshot] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -3,30 +3,24 @@ import { getServerSideConfig } from "@/app/config/server";
 | 
				
			|||||||
import { ModelProvider, OpenaiPath } from "@/app/constant";
 | 
					import { ModelProvider, OpenaiPath } from "@/app/constant";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "./auth";
 | 
					import { auth } from "../../auth";
 | 
				
			||||||
import { requestOpenai } from "./common";
 | 
					import { requestOpenai } from "../../common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
 | 
					const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
					function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
				
			||||||
  const config = getServerSideConfig();
 | 
					  const config = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (config.disableGPT4) {
 | 
					  if (config.disableGPT4) {
 | 
				
			||||||
    remoteModelRes.data = remoteModelRes.data.filter(
 | 
					    remoteModelRes.data = remoteModelRes.data.filter(
 | 
				
			||||||
      (m) =>
 | 
					      (m) => !m.id.startsWith("gpt-4"),
 | 
				
			||||||
        !(
 | 
					 | 
				
			||||||
          m.id.startsWith("gpt-4") ||
 | 
					 | 
				
			||||||
          m.id.startsWith("chatgpt-4o") ||
 | 
					 | 
				
			||||||
          m.id.startsWith("o1") ||
 | 
					 | 
				
			||||||
          m.id.startsWith("o3")
 | 
					 | 
				
			||||||
        ) || m.id.startsWith("gpt-4o-mini"),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return remoteModelRes;
 | 
					  return remoteModelRes;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function handle(
 | 
					async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -38,7 +32,7 @@ export async function handle(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const subpath = params.path.join("/");
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!ALLOWED_PATH.has(subpath)) {
 | 
					  if (!ALLOWD_PATH.has(subpath)) {
 | 
				
			||||||
    console.log("[OpenAI Route] forbidden path ", subpath);
 | 
					    console.log("[OpenAI Route] forbidden path ", subpath);
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
@@ -76,3 +70,27 @@ export async function handle(
 | 
				
			|||||||
    return NextResponse.json(prettyObject(e));
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
@@ -1,89 +0,0 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Proxy Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // remove path params from searchParams
 | 
					 | 
				
			||||||
  req.nextUrl.searchParams.delete("path");
 | 
					 | 
				
			||||||
  req.nextUrl.searchParams.delete("provider");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const subpath = params.path.join("/");
 | 
					 | 
				
			||||||
  const fetchUrl = `${req.headers.get(
 | 
					 | 
				
			||||||
    "x-base-url",
 | 
					 | 
				
			||||||
  )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
 | 
					 | 
				
			||||||
  const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
 | 
					 | 
				
			||||||
  const headers = new Headers(
 | 
					 | 
				
			||||||
    Array.from(req.headers.entries()).filter((item) => {
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        item[0].indexOf("x-") > -1 ||
 | 
					 | 
				
			||||||
        item[0].indexOf("sec-") > -1 ||
 | 
					 | 
				
			||||||
        skipHeaders.includes(item[0])
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  // if dalle3 use openai api key
 | 
					 | 
				
			||||||
    const baseUrl = req.headers.get("x-base-url");
 | 
					 | 
				
			||||||
    if (baseUrl?.includes("api.openai.com")) {
 | 
					 | 
				
			||||||
      if (!serverConfig.apiKey) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          { error: "OpenAI API key not configured" },
 | 
					 | 
				
			||||||
          { status: 500 },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers,
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
					 | 
				
			||||||
    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
					 | 
				
			||||||
    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
					 | 
				
			||||||
    // The browser will try to decode the response with brotli and fail
 | 
					 | 
				
			||||||
    newHeaders.delete("content-encoding");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,128 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  SILICONFLOW_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[SiliconFlow Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.SiliconFlow);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[SiliconFlow] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.SiliconFlow, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.siliconFlowUrl || SILICONFLOW_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.SiliconFlow as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[SiliconFlow] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,99 +0,0 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Stability] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Stability Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Stability Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Stability);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const bearToken = req.headers.get("Authorization") ?? "";
 | 
					 | 
				
			||||||
  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const key = token ? token : serverConfig.stabilityApiKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!key) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        message: `missing STABILITY_API_KEY in server env vars`,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 401,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}/${path}`;
 | 
					 | 
				
			||||||
  console.log("[Stability Url] ", fetchUrl);
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
 | 
					 | 
				
			||||||
      Accept: req.headers.get("Accept") || "application/json",
 | 
					 | 
				
			||||||
      Authorization: `Bearer ${key}`,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,117 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { getHeader } from "@/app/utils/tencent";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[Tencent Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.Hunyuan);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[Tencent] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = baseUrl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const body = await req.text();
 | 
					 | 
				
			||||||
  const headers = await getHeader(
 | 
					 | 
				
			||||||
    body,
 | 
					 | 
				
			||||||
    serverConfig.tencentSecretId as string,
 | 
					 | 
				
			||||||
    serverConfig.tencentSecretKey as string,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers,
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,73 +0,0 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { action: string; key: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  const requestUrl = new URL(req.url);
 | 
					 | 
				
			||||||
  const endpoint = requestUrl.searchParams.get("endpoint");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const [...key] = params.key;
 | 
					 | 
				
			||||||
  // only allow to request to *.upstash.io
 | 
					 | 
				
			||||||
  if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + params.key.join("/"),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // only allow upstash get and set method
 | 
					 | 
				
			||||||
  if (params.action !== "get" && params.action !== "set") {
 | 
					 | 
				
			||||||
    console.log("[Upstash Route] forbidden action ", params.action);
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + params.action,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const method = req.method;
 | 
					 | 
				
			||||||
  const shouldNotHaveBody = ["get", "head"].includes(
 | 
					 | 
				
			||||||
    method?.toLowerCase() ?? "",
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      authorization: req.headers.get("authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    body: shouldNotHaveBody ? null : req.body,
 | 
					 | 
				
			||||||
    method,
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Upstash Proxy]", targetUrl, fetchOptions);
 | 
					 | 
				
			||||||
  const fetchResult = await fetch(targetUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Any Proxy]", targetUrl, {
 | 
					 | 
				
			||||||
    status: fetchResult.status,
 | 
					 | 
				
			||||||
    statusText: fetchResult.statusText,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return fetchResult;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const OPTIONS = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
@@ -1,167 +0,0 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
 | 
					 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const config = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mergedAllowedWebDavEndpoints = [
 | 
					 | 
				
			||||||
  ...internalAllowedWebDavEndpoints,
 | 
					 | 
				
			||||||
  ...config.allowedWebDavEndpoints,
 | 
					 | 
				
			||||||
].filter((domain) => Boolean(domain.trim()));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const normalizeUrl = (url: string) => {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    return new URL(url);
 | 
					 | 
				
			||||||
  } catch (err) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const folder = STORAGE_KEY;
 | 
					 | 
				
			||||||
  const fileName = `${folder}/backup.json`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const requestUrl = new URL(req.url);
 | 
					 | 
				
			||||||
  let endpoint = requestUrl.searchParams.get("endpoint");
 | 
					 | 
				
			||||||
  let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Validate the endpoint to prevent potential SSRF attacks
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    !endpoint ||
 | 
					 | 
				
			||||||
    !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
 | 
					 | 
				
			||||||
      const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
 | 
					 | 
				
			||||||
      const normalizedEndpoint = normalizeUrl(endpoint as string);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        normalizedEndpoint &&
 | 
					 | 
				
			||||||
        normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
 | 
					 | 
				
			||||||
        normalizedEndpoint.pathname.startsWith(
 | 
					 | 
				
			||||||
          normalizedAllowedEndpoint.pathname,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "Invalid endpoint",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 400,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!endpoint?.endsWith("/")) {
 | 
					 | 
				
			||||||
    endpoint += "/";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const endpointPath = params.path.join("/");
 | 
					 | 
				
			||||||
  const targetPath = `${endpoint}${endpointPath}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // only allow MKCOL, GET, PUT
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    proxy_method !== "MKCOL" &&
 | 
					 | 
				
			||||||
    proxy_method !== "GET" &&
 | 
					 | 
				
			||||||
    proxy_method !== "PUT"
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + targetPath,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // for MKCOL request, only allow request ${folder}
 | 
					 | 
				
			||||||
  if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + targetPath,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // for GET request, only allow request ending with fileName
 | 
					 | 
				
			||||||
  if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + targetPath,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //   for PUT request, only allow request ending with fileName
 | 
					 | 
				
			||||||
  if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: "you are not allowed to request " + targetPath,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 403,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const targetUrl = targetPath;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const method = proxy_method || req.method;
 | 
					 | 
				
			||||||
  const shouldNotHaveBody = ["get", "head"].includes(
 | 
					 | 
				
			||||||
    method?.toLowerCase() ?? "",
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      authorization: req.headers.get("authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    body: shouldNotHaveBody ? null : req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    method,
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let fetchResult;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    fetchResult = await fetch(targetUrl, fetchOptions);
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    console.log(
 | 
					 | 
				
			||||||
      "[Any Proxy]",
 | 
					 | 
				
			||||||
      targetUrl,
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        method: method,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: fetchResult?.status,
 | 
					 | 
				
			||||||
        statusText: fetchResult?.statusText,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return fetchResult;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const PUT = handle;
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const OPTIONS = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
								
							@@ -1,128 +0,0 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  XAI_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					 | 
				
			||||||
import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function handle(
 | 
					 | 
				
			||||||
  req: NextRequest,
 | 
					 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  console.log("[XAI Route] params ", params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (req.method === "OPTIONS") {
 | 
					 | 
				
			||||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authResult = auth(req, ModelProvider.XAI);
 | 
					 | 
				
			||||||
  if (authResult.error) {
 | 
					 | 
				
			||||||
    return NextResponse.json(authResult, {
 | 
					 | 
				
			||||||
      status: 401,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await request(req);
 | 
					 | 
				
			||||||
    return response;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[XAI] ", e);
 | 
					 | 
				
			||||||
    return NextResponse.json(prettyObject(e));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					 | 
				
			||||||
  const controller = new AbortController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // alibaba use base url or just remove the path
 | 
					 | 
				
			||||||
  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					 | 
				
			||||||
    baseUrl = `https://${baseUrl}`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
    baseUrl = baseUrl.slice(0, -1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log("[Proxy] ", path);
 | 
					 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const timeoutId = setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      controller.abort();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    10 * 60 * 1000,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}`;
 | 
					 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					 | 
				
			||||||
    headers: {
 | 
					 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					 | 
				
			||||||
      Authorization: req.headers.get("Authorization") ?? "",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    method: req.method,
 | 
					 | 
				
			||||||
    body: req.body,
 | 
					 | 
				
			||||||
    redirect: "manual",
 | 
					 | 
				
			||||||
    // @ts-ignore
 | 
					 | 
				
			||||||
    duplex: "half",
 | 
					 | 
				
			||||||
    signal: controller.signal,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // #1815 try to refuse some request to some models
 | 
					 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // not undefined and is false
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        isModelNotavailableInServer(
 | 
					 | 
				
			||||||
          serverConfig.customModels,
 | 
					 | 
				
			||||||
          jsonBody?.model as string,
 | 
					 | 
				
			||||||
          ServiceProvider.XAI as string,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        return NextResponse.json(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            error: true,
 | 
					 | 
				
			||||||
            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            status: 403,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(`[XAI] filter`, e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // to prevent browser prompt for credentials
 | 
					 | 
				
			||||||
    const newHeaders = new Headers(res.headers);
 | 
					 | 
				
			||||||
    newHeaders.delete("www-authenticate");
 | 
					 | 
				
			||||||
    // to disable nginx buffering
 | 
					 | 
				
			||||||
    newHeaders.set("X-Accel-Buffering", "no");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Response(res.body, {
 | 
					 | 
				
			||||||
      status: res.status,
 | 
					 | 
				
			||||||
      statusText: res.statusText,
 | 
					 | 
				
			||||||
      headers: newHeaders,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    clearTimeout(timeoutId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										9
									
								
								app/azure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/azure.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					export function makeAzurePath(path: string, apiVersion: string) {
 | 
				
			||||||
 | 
					  // should omit /v1 prefix
 | 
				
			||||||
 | 
					  path = path.replaceAll("v1/", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // should add api-key to query string
 | 
				
			||||||
 | 
					  path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return path;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,75 +1,31 @@
 | 
				
			|||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ACCESS_CODE_PREFIX,
 | 
					  ACCESS_CODE_PREFIX,
 | 
				
			||||||
 | 
					  Azure,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
} from "../constant";
 | 
					} from "../constant";
 | 
				
			||||||
import {
 | 
					import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
 | 
				
			||||||
  ChatMessageTool,
 | 
					import { ChatGPTApi } from "./platforms/openai";
 | 
				
			||||||
  ChatMessage,
 | 
					 | 
				
			||||||
  ModelType,
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
} from "../store";
 | 
					 | 
				
			||||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 | 
					 | 
				
			||||||
import { GeminiProApi } from "./platforms/google";
 | 
					import { GeminiProApi } from "./platforms/google";
 | 
				
			||||||
import { ClaudeApi } from "./platforms/anthropic";
 | 
					 | 
				
			||||||
import { ErnieApi } from "./platforms/baidu";
 | 
					 | 
				
			||||||
import { DoubaoApi } from "./platforms/bytedance";
 | 
					 | 
				
			||||||
import { QwenApi } from "./platforms/alibaba";
 | 
					 | 
				
			||||||
import { HunyuanApi } from "./platforms/tencent";
 | 
					 | 
				
			||||||
import { MoonshotApi } from "./platforms/moonshot";
 | 
					 | 
				
			||||||
import { SparkApi } from "./platforms/iflytek";
 | 
					 | 
				
			||||||
import { DeepSeekApi } from "./platforms/deepseek";
 | 
					 | 
				
			||||||
import { XAIApi } from "./platforms/xai";
 | 
					 | 
				
			||||||
import { ChatGLMApi } from "./platforms/glm";
 | 
					 | 
				
			||||||
import { SiliconflowApi } from "./platforms/siliconflow";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ROLES = ["system", "user", "assistant"] as const;
 | 
					export const ROLES = ["system", "user", "assistant"] as const;
 | 
				
			||||||
export type MessageRole = (typeof ROLES)[number];
 | 
					export type MessageRole = (typeof ROLES)[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
 | 
					export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
 | 
				
			||||||
export const TTSModels = ["tts-1", "tts-1-hd"] as const;
 | 
					 | 
				
			||||||
export type ChatModel = ModelType;
 | 
					export type ChatModel = ModelType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MultimodalContent {
 | 
					 | 
				
			||||||
  type: "text" | "image_url";
 | 
					 | 
				
			||||||
  text?: string;
 | 
					 | 
				
			||||||
  image_url?: {
 | 
					 | 
				
			||||||
    url: string;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface MultimodalContentForAlibaba {
 | 
					 | 
				
			||||||
  text?: string;
 | 
					 | 
				
			||||||
  image?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface RequestMessage {
 | 
					export interface RequestMessage {
 | 
				
			||||||
  role: MessageRole;
 | 
					  role: MessageRole;
 | 
				
			||||||
  content: string | MultimodalContent[];
 | 
					  content: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LLMConfig {
 | 
					export interface LLMConfig {
 | 
				
			||||||
  model: string;
 | 
					  model: string;
 | 
				
			||||||
  providerName?: string;
 | 
					 | 
				
			||||||
  temperature?: number;
 | 
					  temperature?: number;
 | 
				
			||||||
  top_p?: number;
 | 
					  top_p?: number;
 | 
				
			||||||
  stream?: boolean;
 | 
					  stream?: boolean;
 | 
				
			||||||
  presence_penalty?: number;
 | 
					  presence_penalty?: number;
 | 
				
			||||||
  frequency_penalty?: number;
 | 
					  frequency_penalty?: number;
 | 
				
			||||||
  size?: DalleRequestPayload["size"];
 | 
					 | 
				
			||||||
  quality?: DalleRequestPayload["quality"];
 | 
					 | 
				
			||||||
  style?: DalleRequestPayload["style"];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface SpeechOptions {
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  input: string;
 | 
					 | 
				
			||||||
  voice: string;
 | 
					 | 
				
			||||||
  response_format?: string;
 | 
					 | 
				
			||||||
  speed?: number;
 | 
					 | 
				
			||||||
  onController?: (controller: AbortController) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ChatOptions {
 | 
					export interface ChatOptions {
 | 
				
			||||||
@@ -77,11 +33,9 @@ export interface ChatOptions {
 | 
				
			|||||||
  config: LLMConfig;
 | 
					  config: LLMConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onUpdate?: (message: string, chunk: string) => void;
 | 
					  onUpdate?: (message: string, chunk: string) => void;
 | 
				
			||||||
  onFinish: (message: string, responseRes: Response) => void;
 | 
					  onFinish: (message: string) => void;
 | 
				
			||||||
  onError?: (err: Error) => void;
 | 
					  onError?: (err: Error) => void;
 | 
				
			||||||
  onController?: (controller: AbortController) => void;
 | 
					  onController?: (controller: AbortController) => void;
 | 
				
			||||||
  onBeforeTool?: (tool: ChatMessageTool) => void;
 | 
					 | 
				
			||||||
  onAfterTool?: (tool: ChatMessageTool) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LLMUsage {
 | 
					export interface LLMUsage {
 | 
				
			||||||
@@ -91,22 +45,18 @@ export interface LLMUsage {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface LLMModel {
 | 
					export interface LLMModel {
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  displayName?: string;
 | 
					 | 
				
			||||||
  available: boolean;
 | 
					  available: boolean;
 | 
				
			||||||
  provider: LLMModelProvider;
 | 
					  provider: LLMModelProvider;
 | 
				
			||||||
  sorted: number;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LLMModelProvider {
 | 
					export interface LLMModelProvider {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  providerName: string;
 | 
					  providerName: string;
 | 
				
			||||||
  providerType: string;
 | 
					  providerType: string;
 | 
				
			||||||
  sorted: number;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export abstract class LLMApi {
 | 
					export abstract class LLMApi {
 | 
				
			||||||
  abstract chat(options: ChatOptions): Promise<void>;
 | 
					  abstract chat(options: ChatOptions): Promise<void>;
 | 
				
			||||||
  abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
 | 
					 | 
				
			||||||
  abstract usage(): Promise<LLMUsage>;
 | 
					  abstract usage(): Promise<LLMUsage>;
 | 
				
			||||||
  abstract models(): Promise<LLMModel[]>;
 | 
					  abstract models(): Promise<LLMModel[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -136,46 +86,11 @@ export class ClientApi {
 | 
				
			|||||||
  public llm: LLMApi;
 | 
					  public llm: LLMApi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(provider: ModelProvider = ModelProvider.GPT) {
 | 
					  constructor(provider: ModelProvider = ModelProvider.GPT) {
 | 
				
			||||||
    switch (provider) {
 | 
					    if (provider === ModelProvider.GeminiPro) {
 | 
				
			||||||
      case ModelProvider.GeminiPro:
 | 
					 | 
				
			||||||
      this.llm = new GeminiProApi();
 | 
					      this.llm = new GeminiProApi();
 | 
				
			||||||
        break;
 | 
					      return;
 | 
				
			||||||
      case ModelProvider.Claude:
 | 
					 | 
				
			||||||
        this.llm = new ClaudeApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Ernie:
 | 
					 | 
				
			||||||
        this.llm = new ErnieApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Doubao:
 | 
					 | 
				
			||||||
        this.llm = new DoubaoApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Qwen:
 | 
					 | 
				
			||||||
        this.llm = new QwenApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Hunyuan:
 | 
					 | 
				
			||||||
        this.llm = new HunyuanApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Moonshot:
 | 
					 | 
				
			||||||
        this.llm = new MoonshotApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.Iflytek:
 | 
					 | 
				
			||||||
        this.llm = new SparkApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.DeepSeek:
 | 
					 | 
				
			||||||
        this.llm = new DeepSeekApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.XAI:
 | 
					 | 
				
			||||||
        this.llm = new XAIApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.ChatGLM:
 | 
					 | 
				
			||||||
        this.llm = new ChatGLMApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case ModelProvider.SiliconFlow:
 | 
					 | 
				
			||||||
        this.llm = new SiliconflowApi();
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        this.llm = new ChatGPTApi();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    this.llm = new ChatGPTApi();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config() {}
 | 
					  config() {}
 | 
				
			||||||
@@ -224,165 +139,37 @@ export class ClientApi {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getBearerToken(
 | 
					export function getHeaders() {
 | 
				
			||||||
  apiKey: string,
 | 
					 | 
				
			||||||
  noBearer: boolean = false,
 | 
					 | 
				
			||||||
): string {
 | 
					 | 
				
			||||||
  return validString(apiKey)
 | 
					 | 
				
			||||||
    ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
 | 
					 | 
				
			||||||
    : "";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function validString(x: string): boolean {
 | 
					 | 
				
			||||||
  return x?.length > 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getHeaders(ignoreHeaders: boolean = false) {
 | 
					 | 
				
			||||||
  const accessStore = useAccessStore.getState();
 | 
					  const accessStore = useAccessStore.getState();
 | 
				
			||||||
  const chatStore = useChatStore.getState();
 | 
					  const headers: Record<string, string> = {
 | 
				
			||||||
  let headers: Record<string, string> = {};
 | 
					 | 
				
			||||||
  if (!ignoreHeaders) {
 | 
					 | 
				
			||||||
    headers = {
 | 
					 | 
				
			||||||
    "Content-Type": "application/json",
 | 
					    "Content-Type": "application/json",
 | 
				
			||||||
      Accept: "application/json",
 | 
					    "x-requested-with": "XMLHttpRequest",
 | 
				
			||||||
 | 
					    "Accept": "application/json",
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  }
 | 
					  const modelConfig = useChatStore.getState().currentSession().mask.modelConfig;
 | 
				
			||||||
 | 
					  const isGoogle = modelConfig.model === "gemini-pro";
 | 
				
			||||||
  const clientConfig = getClientConfig();
 | 
					  const isAzure = accessStore.provider === ServiceProvider.Azure;
 | 
				
			||||||
 | 
					  const authHeader = isAzure ? "api-key" : "Authorization";
 | 
				
			||||||
  function getConfig() {
 | 
					 | 
				
			||||||
    const modelConfig = chatStore.currentSession().mask.modelConfig;
 | 
					 | 
				
			||||||
    const isGoogle = modelConfig.providerName === ServiceProvider.Google;
 | 
					 | 
				
			||||||
    const isAzure = modelConfig.providerName === ServiceProvider.Azure;
 | 
					 | 
				
			||||||
    const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
 | 
					 | 
				
			||||||
    const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
 | 
					 | 
				
			||||||
    const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
 | 
					 | 
				
			||||||
    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
					 | 
				
			||||||
    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
					 | 
				
			||||||
    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
 | 
					 | 
				
			||||||
    const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek;
 | 
					 | 
				
			||||||
    const isXAI = modelConfig.providerName === ServiceProvider.XAI;
 | 
					 | 
				
			||||||
    const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
 | 
					 | 
				
			||||||
    const isSiliconFlow =
 | 
					 | 
				
			||||||
      modelConfig.providerName === ServiceProvider.SiliconFlow;
 | 
					 | 
				
			||||||
    const isEnabledAccessControl = accessStore.enabledAccessControl();
 | 
					 | 
				
			||||||
  const apiKey = isGoogle
 | 
					  const apiKey = isGoogle
 | 
				
			||||||
    ? accessStore.googleApiKey
 | 
					    ? accessStore.googleApiKey
 | 
				
			||||||
    : isAzure
 | 
					    : isAzure
 | 
				
			||||||
    ? accessStore.azureApiKey
 | 
					    ? accessStore.azureApiKey
 | 
				
			||||||
      : isAnthropic
 | 
					 | 
				
			||||||
      ? accessStore.anthropicApiKey
 | 
					 | 
				
			||||||
      : isByteDance
 | 
					 | 
				
			||||||
      ? accessStore.bytedanceApiKey
 | 
					 | 
				
			||||||
      : isAlibaba
 | 
					 | 
				
			||||||
      ? accessStore.alibabaApiKey
 | 
					 | 
				
			||||||
      : isMoonshot
 | 
					 | 
				
			||||||
      ? accessStore.moonshotApiKey
 | 
					 | 
				
			||||||
      : isXAI
 | 
					 | 
				
			||||||
      ? accessStore.xaiApiKey
 | 
					 | 
				
			||||||
      : isDeepSeek
 | 
					 | 
				
			||||||
      ? accessStore.deepseekApiKey
 | 
					 | 
				
			||||||
      : isChatGLM
 | 
					 | 
				
			||||||
      ? accessStore.chatglmApiKey
 | 
					 | 
				
			||||||
      : isSiliconFlow
 | 
					 | 
				
			||||||
      ? accessStore.siliconflowApiKey
 | 
					 | 
				
			||||||
      : isIflytek
 | 
					 | 
				
			||||||
      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
					 | 
				
			||||||
        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
					 | 
				
			||||||
        : ""
 | 
					 | 
				
			||||||
    : accessStore.openaiApiKey;
 | 
					    : accessStore.openaiApiKey;
 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      isGoogle,
 | 
					 | 
				
			||||||
      isAzure,
 | 
					 | 
				
			||||||
      isAnthropic,
 | 
					 | 
				
			||||||
      isBaidu,
 | 
					 | 
				
			||||||
      isByteDance,
 | 
					 | 
				
			||||||
      isAlibaba,
 | 
					 | 
				
			||||||
      isMoonshot,
 | 
					 | 
				
			||||||
      isIflytek,
 | 
					 | 
				
			||||||
      isDeepSeek,
 | 
					 | 
				
			||||||
      isXAI,
 | 
					 | 
				
			||||||
      isChatGLM,
 | 
					 | 
				
			||||||
      isSiliconFlow,
 | 
					 | 
				
			||||||
      apiKey,
 | 
					 | 
				
			||||||
      isEnabledAccessControl,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function getAuthHeader(): string {
 | 
					  const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
 | 
				
			||||||
    return isAzure
 | 
					  const validString = (x: string) => x && x.length > 0;
 | 
				
			||||||
      ? "api-key"
 | 
					 | 
				
			||||||
      : isAnthropic
 | 
					 | 
				
			||||||
      ? "x-api-key"
 | 
					 | 
				
			||||||
      : isGoogle
 | 
					 | 
				
			||||||
      ? "x-goog-api-key"
 | 
					 | 
				
			||||||
      : "Authorization";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  // use user's api key first
 | 
				
			||||||
    isGoogle,
 | 
					  if (validString(apiKey)) {
 | 
				
			||||||
    isAzure,
 | 
					    headers[authHeader] = makeBearer(apiKey);
 | 
				
			||||||
    isAnthropic,
 | 
					  } else if (
 | 
				
			||||||
    isBaidu,
 | 
					    accessStore.enabledAccessControl() &&
 | 
				
			||||||
    isByteDance,
 | 
					    validString(accessStore.accessCode)
 | 
				
			||||||
    isAlibaba,
 | 
					  ) {
 | 
				
			||||||
    isMoonshot,
 | 
					    headers[authHeader] = makeBearer(
 | 
				
			||||||
    isIflytek,
 | 
					 | 
				
			||||||
    isDeepSeek,
 | 
					 | 
				
			||||||
    isXAI,
 | 
					 | 
				
			||||||
    isChatGLM,
 | 
					 | 
				
			||||||
    isSiliconFlow,
 | 
					 | 
				
			||||||
    apiKey,
 | 
					 | 
				
			||||||
    isEnabledAccessControl,
 | 
					 | 
				
			||||||
  } = getConfig();
 | 
					 | 
				
			||||||
  // when using baidu api in app, not set auth header
 | 
					 | 
				
			||||||
  if (isBaidu && clientConfig?.isApp) return headers;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const authHeader = getAuthHeader();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const bearerToken = getBearerToken(
 | 
					 | 
				
			||||||
    apiKey,
 | 
					 | 
				
			||||||
    isAzure || isAnthropic || isGoogle,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (bearerToken) {
 | 
					 | 
				
			||||||
    headers[authHeader] = bearerToken;
 | 
					 | 
				
			||||||
  } else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
 | 
					 | 
				
			||||||
    headers["Authorization"] = getBearerToken(
 | 
					 | 
				
			||||||
      ACCESS_CODE_PREFIX + accessStore.accessCode,
 | 
					      ACCESS_CODE_PREFIX + accessStore.accessCode,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return headers;
 | 
					  return headers;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getClientApi(provider: ServiceProvider): ClientApi {
 | 
					 | 
				
			||||||
  switch (provider) {
 | 
					 | 
				
			||||||
    case ServiceProvider.Google:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.GeminiPro);
 | 
					 | 
				
			||||||
    case ServiceProvider.Anthropic:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Claude);
 | 
					 | 
				
			||||||
    case ServiceProvider.Baidu:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Ernie);
 | 
					 | 
				
			||||||
    case ServiceProvider.ByteDance:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Doubao);
 | 
					 | 
				
			||||||
    case ServiceProvider.Alibaba:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Qwen);
 | 
					 | 
				
			||||||
    case ServiceProvider.Tencent:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Hunyuan);
 | 
					 | 
				
			||||||
    case ServiceProvider.Moonshot:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Moonshot);
 | 
					 | 
				
			||||||
    case ServiceProvider.Iflytek:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.Iflytek);
 | 
					 | 
				
			||||||
    case ServiceProvider.DeepSeek:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.DeepSeek);
 | 
					 | 
				
			||||||
    case ServiceProvider.XAI:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.XAI);
 | 
					 | 
				
			||||||
    case ServiceProvider.ChatGLM:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.ChatGLM);
 | 
					 | 
				
			||||||
    case ServiceProvider.SiliconFlow:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.SiliconFlow);
 | 
					 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      return new ClientApi(ModelProvider.GPT);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,277 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import { ApiPath, Alibaba, ALIBABA_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  preProcessImageContentForAlibabaDashScope,
 | 
					 | 
				
			||||||
  streamWithThink,
 | 
					 | 
				
			||||||
} from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
  MultimodalContentForAlibaba,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  getMessageTextContentWithoutThinking,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					 | 
				
			||||||
  object: string;
 | 
					 | 
				
			||||||
  data: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    object: string;
 | 
					 | 
				
			||||||
    root: string;
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RequestInput {
 | 
					 | 
				
			||||||
  messages: {
 | 
					 | 
				
			||||||
    role: "system" | "user" | "assistant";
 | 
					 | 
				
			||||||
    content: string | MultimodalContent[];
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
interface RequestParam {
 | 
					 | 
				
			||||||
  result_format: string;
 | 
					 | 
				
			||||||
  incremental_output?: boolean;
 | 
					 | 
				
			||||||
  temperature: number;
 | 
					 | 
				
			||||||
  repetition_penalty?: number;
 | 
					 | 
				
			||||||
  top_p: number;
 | 
					 | 
				
			||||||
  max_tokens?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
interface RequestPayload {
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  input: RequestInput;
 | 
					 | 
				
			||||||
  parameters: RequestParam;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class QwenApi implements LLMApi {
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.alibabaUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res?.output?.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      const content = (
 | 
					 | 
				
			||||||
        visionModel
 | 
					 | 
				
			||||||
          ? await preProcessImageContentForAlibabaDashScope(v.content)
 | 
					 | 
				
			||||||
          : v.role === "assistant"
 | 
					 | 
				
			||||||
          ? getMessageTextContentWithoutThinking(v)
 | 
					 | 
				
			||||||
          : getMessageTextContent(v)
 | 
					 | 
				
			||||||
      ) as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      input: {
 | 
					 | 
				
			||||||
        messages,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      parameters: {
 | 
					 | 
				
			||||||
        result_format: "message",
 | 
					 | 
				
			||||||
        incremental_output: shouldStream,
 | 
					 | 
				
			||||||
        temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
        // max_tokens: modelConfig.max_tokens,
 | 
					 | 
				
			||||||
        top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const headers = {
 | 
					 | 
				
			||||||
        ...getHeaders(),
 | 
					 | 
				
			||||||
        "X-DashScope-SSE": shouldStream ? "enable" : "disable",
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const chatPath = this.path(Alibaba.ChatPath(modelConfig.model));
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: headers,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return streamWithThink(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          headers,
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.output.choices as Array<{
 | 
					 | 
				
			||||||
              message: {
 | 
					 | 
				
			||||||
                content: string | null | MultimodalContentForAlibaba[];
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
                reasoning_content: string | null;
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!choices?.length) return { isThinking: false, content: "" };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.message?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const reasoning = choices[0]?.message?.reasoning_content;
 | 
					 | 
				
			||||||
            const content = choices[0]?.message?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Skip if both content and reasoning_content are empty or null
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (!reasoning || reasoning.length === 0) &&
 | 
					 | 
				
			||||||
              (!content || content.length === 0)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: "",
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (reasoning && reasoning.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: true,
 | 
					 | 
				
			||||||
                content: reasoning,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            } else if (content && content.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: Array.isArray(content)
 | 
					 | 
				
			||||||
                  ? content.map((item) => item.text).join(",")
 | 
					 | 
				
			||||||
                  : content,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              isThinking: false,
 | 
					 | 
				
			||||||
              content: "",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            requestPayload?.input?.messages?.splice(
 | 
					 | 
				
			||||||
              requestPayload?.input?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export { Alibaba };
 | 
					 | 
				
			||||||
@@ -1,415 +0,0 @@
 | 
				
			|||||||
import { Anthropic, ApiPath } from "@/app/constant";
 | 
					 | 
				
			||||||
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { ANTHROPIC_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
					 | 
				
			||||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type MultiBlockContent = {
 | 
					 | 
				
			||||||
  type: "image" | "text";
 | 
					 | 
				
			||||||
  source?: {
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    media_type: string;
 | 
					 | 
				
			||||||
    data: string;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  text?: string;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type AnthropicMessage = {
 | 
					 | 
				
			||||||
  role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
 | 
					 | 
				
			||||||
  content: string | MultiBlockContent[];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AnthropicChatRequest {
 | 
					 | 
				
			||||||
  model: string; // The model that will complete your prompt.
 | 
					 | 
				
			||||||
  messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
 | 
					 | 
				
			||||||
  max_tokens: number; // The maximum number of tokens to generate before stopping.
 | 
					 | 
				
			||||||
  stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
 | 
					 | 
				
			||||||
  temperature?: number; // Amount of randomness injected into the response.
 | 
					 | 
				
			||||||
  top_p?: number; // Use nucleus sampling.
 | 
					 | 
				
			||||||
  top_k?: number; // Only sample from the top K options for each subsequent token.
 | 
					 | 
				
			||||||
  metadata?: object; // An object describing metadata about the request.
 | 
					 | 
				
			||||||
  stream?: boolean; // Whether to incrementally stream the response using server-sent events.
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ChatRequest {
 | 
					 | 
				
			||||||
  model: string; // The model that will complete your prompt.
 | 
					 | 
				
			||||||
  prompt: string; // The prompt that you want Claude to complete.
 | 
					 | 
				
			||||||
  max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
 | 
					 | 
				
			||||||
  stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
 | 
					 | 
				
			||||||
  temperature?: number; // Amount of randomness injected into the response.
 | 
					 | 
				
			||||||
  top_p?: number; // Use nucleus sampling.
 | 
					 | 
				
			||||||
  top_k?: number; // Only sample from the top K options for each subsequent token.
 | 
					 | 
				
			||||||
  metadata?: object; // An object describing metadata about the request.
 | 
					 | 
				
			||||||
  stream?: boolean; // Whether to incrementally stream the response using server-sent events.
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ChatResponse {
 | 
					 | 
				
			||||||
  completion: string;
 | 
					 | 
				
			||||||
  stop_reason: "stop_sequence" | "max_tokens";
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type ChatStreamResponse = ChatResponse & {
 | 
					 | 
				
			||||||
  stop?: string;
 | 
					 | 
				
			||||||
  log_id: string;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ClaudeMapper = {
 | 
					 | 
				
			||||||
  assistant: "assistant",
 | 
					 | 
				
			||||||
  user: "user",
 | 
					 | 
				
			||||||
  system: "user",
 | 
					 | 
				
			||||||
} as const;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const keys = ["claude-2, claude-instant-1"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ClaudeApi implements LLMApi {
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    console.log("[Response] claude response: ", res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return res?.content?.[0]?.text;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions): Promise<void> {
 | 
					 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 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"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
 | 
					 | 
				
			||||||
    for (let i = 0; i < messages.length - 1; i++) {
 | 
					 | 
				
			||||||
      const message = messages[i];
 | 
					 | 
				
			||||||
      const nextMessage = messages[i + 1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
 | 
					 | 
				
			||||||
        messages[i] = [
 | 
					 | 
				
			||||||
          message,
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            role: "assistant",
 | 
					 | 
				
			||||||
            content: ";",
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ] as any;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const prompt = messages
 | 
					 | 
				
			||||||
      .flat()
 | 
					 | 
				
			||||||
      .filter((v) => {
 | 
					 | 
				
			||||||
        if (!v.content) return false;
 | 
					 | 
				
			||||||
        if (typeof v.content === "string" && !v.content.trim()) return false;
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .map((v) => {
 | 
					 | 
				
			||||||
        const { role, content } = v;
 | 
					 | 
				
			||||||
        const insideRole = ClaudeMapper[role] ?? "user";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!visionModel || typeof content === "string") {
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            role: insideRole,
 | 
					 | 
				
			||||||
            content: getMessageTextContent(v),
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          role: insideRole,
 | 
					 | 
				
			||||||
          content: content
 | 
					 | 
				
			||||||
            .filter((v) => v.image_url || v.text)
 | 
					 | 
				
			||||||
            .map(({ type, text, image_url }) => {
 | 
					 | 
				
			||||||
              if (type === "text") {
 | 
					 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                  type,
 | 
					 | 
				
			||||||
                  text: text!,
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              const { url = "" } = image_url || {};
 | 
					 | 
				
			||||||
              const colonIndex = url.indexOf(":");
 | 
					 | 
				
			||||||
              const semicolonIndex = url.indexOf(";");
 | 
					 | 
				
			||||||
              const comma = url.indexOf(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              const mimeType = url.slice(colonIndex + 1, semicolonIndex);
 | 
					 | 
				
			||||||
              const encodeType = url.slice(semicolonIndex + 1, comma);
 | 
					 | 
				
			||||||
              const data = url.slice(comma + 1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                type: "image" as const,
 | 
					 | 
				
			||||||
                source: {
 | 
					 | 
				
			||||||
                  type: encodeType,
 | 
					 | 
				
			||||||
                  media_type: mimeType,
 | 
					 | 
				
			||||||
                  data,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (prompt[0]?.role === "assistant") {
 | 
					 | 
				
			||||||
      prompt.unshift({
 | 
					 | 
				
			||||||
        role: "user",
 | 
					 | 
				
			||||||
        content: ";",
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestBody: AnthropicChatRequest = {
 | 
					 | 
				
			||||||
      messages: prompt,
 | 
					 | 
				
			||||||
      stream: shouldStream,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      max_tokens: modelConfig.max_tokens,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      // top_k: modelConfig.top_k,
 | 
					 | 
				
			||||||
      top_k: 5,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const path = this.path(Anthropic.ChatPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (shouldStream) {
 | 
					 | 
				
			||||||
      let index = -1;
 | 
					 | 
				
			||||||
      const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
        .getState()
 | 
					 | 
				
			||||||
        .getAsTools(
 | 
					 | 
				
			||||||
          useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      return stream(
 | 
					 | 
				
			||||||
        path,
 | 
					 | 
				
			||||||
        requestBody,
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          ...getHeaders(),
 | 
					 | 
				
			||||||
          "anthropic-version": accessStore.anthropicApiVersion,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        // @ts-ignore
 | 
					 | 
				
			||||||
        tools.map((tool) => ({
 | 
					 | 
				
			||||||
          name: tool?.function?.name,
 | 
					 | 
				
			||||||
          description: tool?.function?.description,
 | 
					 | 
				
			||||||
          input_schema: tool?.function?.parameters,
 | 
					 | 
				
			||||||
        })),
 | 
					 | 
				
			||||||
        funcs,
 | 
					 | 
				
			||||||
        controller,
 | 
					 | 
				
			||||||
        // parseSSE
 | 
					 | 
				
			||||||
        (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
          // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
          let chunkJson:
 | 
					 | 
				
			||||||
            | undefined
 | 
					 | 
				
			||||||
            | {
 | 
					 | 
				
			||||||
                type: "content_block_delta" | "content_block_stop";
 | 
					 | 
				
			||||||
                content_block?: {
 | 
					 | 
				
			||||||
                  type: "tool_use";
 | 
					 | 
				
			||||||
                  id: string;
 | 
					 | 
				
			||||||
                  name: string;
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                delta?: {
 | 
					 | 
				
			||||||
                  type: "text_delta" | "input_json_delta";
 | 
					 | 
				
			||||||
                  text?: string;
 | 
					 | 
				
			||||||
                  partial_json?: string;
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                index: number;
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
          chunkJson = JSON.parse(text);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (chunkJson?.content_block?.type == "tool_use") {
 | 
					 | 
				
			||||||
            index += 1;
 | 
					 | 
				
			||||||
            const id = chunkJson?.content_block.id;
 | 
					 | 
				
			||||||
            const name = chunkJson?.content_block.name;
 | 
					 | 
				
			||||||
            runTools.push({
 | 
					 | 
				
			||||||
              id,
 | 
					 | 
				
			||||||
              type: "function",
 | 
					 | 
				
			||||||
              function: {
 | 
					 | 
				
			||||||
                name,
 | 
					 | 
				
			||||||
                arguments: "",
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (
 | 
					 | 
				
			||||||
            chunkJson?.delta?.type == "input_json_delta" &&
 | 
					 | 
				
			||||||
            chunkJson?.delta?.partial_json
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            runTools[index]["function"]["arguments"] +=
 | 
					 | 
				
			||||||
              chunkJson?.delta?.partial_json;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return chunkJson?.delta?.text;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
          requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
          toolCallMessage: any,
 | 
					 | 
				
			||||||
          toolCallResult: any[],
 | 
					 | 
				
			||||||
        ) => {
 | 
					 | 
				
			||||||
          // reset index value
 | 
					 | 
				
			||||||
          index = -1;
 | 
					 | 
				
			||||||
          // @ts-ignore
 | 
					 | 
				
			||||||
          requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
            0,
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
              role: "assistant",
 | 
					 | 
				
			||||||
              content: toolCallMessage.tool_calls.map(
 | 
					 | 
				
			||||||
                (tool: ChatMessageTool) => ({
 | 
					 | 
				
			||||||
                  type: "tool_use",
 | 
					 | 
				
			||||||
                  id: tool.id,
 | 
					 | 
				
			||||||
                  name: tool?.function?.name,
 | 
					 | 
				
			||||||
                  input: tool?.function?.arguments
 | 
					 | 
				
			||||||
                    ? JSON.parse(tool?.function?.arguments)
 | 
					 | 
				
			||||||
                    : {},
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            ...toolCallResult.map((result) => ({
 | 
					 | 
				
			||||||
              role: "user",
 | 
					 | 
				
			||||||
              content: [
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                  type: "tool_result",
 | 
					 | 
				
			||||||
                  tool_use_id: result.tool_call_id,
 | 
					 | 
				
			||||||
                  content: result.content,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            })),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        options,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const payload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestBody),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          ...getHeaders(), // get common headers
 | 
					 | 
				
			||||||
          "anthropic-version": accessStore.anthropicApiVersion,
 | 
					 | 
				
			||||||
          // do not send `anthropicApiKey` in browser!!!
 | 
					 | 
				
			||||||
          // Authorization: getAuthKey(accessStore.anthropicApiKey),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        controller.signal.onabort = () =>
 | 
					 | 
				
			||||||
          options.onFinish("", new Response(null, { status: 400 }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const res = await fetch(path, payload);
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        console.error("failed to chat", e);
 | 
					 | 
				
			||||||
        options.onError?.(e as Error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async models() {
 | 
					 | 
				
			||||||
    // const provider = {
 | 
					 | 
				
			||||||
    //   id: "anthropic",
 | 
					 | 
				
			||||||
    //   providerName: "Anthropic",
 | 
					 | 
				
			||||||
    //   providerType: "anthropic",
 | 
					 | 
				
			||||||
    // };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-instant-1.2",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-2.0",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-2.1",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-3-opus-20240229",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-3-sonnet-20240229",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
      // {
 | 
					 | 
				
			||||||
      //   name: "claude-3-haiku-20240307",
 | 
					 | 
				
			||||||
      //   available: true,
 | 
					 | 
				
			||||||
      //   provider,
 | 
					 | 
				
			||||||
      // },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl: string = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.anthropicUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // if endpoint is empty, use default endpoint
 | 
					 | 
				
			||||||
    if (baseUrl.trim().length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    baseUrl = trimEnd(baseUrl, "/");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // try rebuild url, when using cloudflare ai gateway in client
 | 
					 | 
				
			||||||
    return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function trimEnd(s: string, end = " ") {
 | 
					 | 
				
			||||||
  if (end.length === 0) return s;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  while (s.endsWith(end)) {
 | 
					 | 
				
			||||||
    s = s.slice(0, -end.length);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,284 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import { ApiPath, Baidu, BAIDU_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					 | 
				
			||||||
import { getAccessToken } from "@/app/utils/baidu";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { getMessageTextContent, getTimeoutMSByModel } from "@/app/utils";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					 | 
				
			||||||
  object: string;
 | 
					 | 
				
			||||||
  data: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    object: string;
 | 
					 | 
				
			||||||
    root: string;
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RequestPayload {
 | 
					 | 
				
			||||||
  messages: {
 | 
					 | 
				
			||||||
    role: "system" | "user" | "assistant";
 | 
					 | 
				
			||||||
    content: string | MultimodalContent[];
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
  stream?: boolean;
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  temperature: number;
 | 
					 | 
				
			||||||
  presence_penalty: number;
 | 
					 | 
				
			||||||
  frequency_penalty: number;
 | 
					 | 
				
			||||||
  top_p: number;
 | 
					 | 
				
			||||||
  max_tokens?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ErnieApi implements LLMApi {
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.baiduUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      // do not use proxy for baidubce api
 | 
					 | 
				
			||||||
      baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages = options.messages.map((v) => ({
 | 
					 | 
				
			||||||
      // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
 | 
					 | 
				
			||||||
      role: v.role === "system" ? "user" : v.role,
 | 
					 | 
				
			||||||
      content: getMessageTextContent(v),
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
 | 
					 | 
				
			||||||
    if (messages.length % 2 === 0) {
 | 
					 | 
				
			||||||
      if (messages.at(0)?.role === "user") {
 | 
					 | 
				
			||||||
        messages.splice(1, 0, {
 | 
					 | 
				
			||||||
          role: "assistant",
 | 
					 | 
				
			||||||
          content: " ",
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        messages.unshift({
 | 
					 | 
				
			||||||
          role: "user",
 | 
					 | 
				
			||||||
          content: " ",
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: shouldStream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] Baidu payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // getAccessToken can not run in browser, because cors error
 | 
					 | 
				
			||||||
      if (!!getClientConfig()?.isApp) {
 | 
					 | 
				
			||||||
        const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
        if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
          if (accessStore.isValidBaidu()) {
 | 
					 | 
				
			||||||
            const { access_token } = await getAccessToken(
 | 
					 | 
				
			||||||
              accessStore.baiduApiKey,
 | 
					 | 
				
			||||||
              accessStore.baiduSecretKey,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            chatPath = `${chatPath}${
 | 
					 | 
				
			||||||
              chatPath.includes("?") ? "&" : "?"
 | 
					 | 
				
			||||||
            }access_token=${access_token}`;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        let responseText = "";
 | 
					 | 
				
			||||||
        let remainText = "";
 | 
					 | 
				
			||||||
        let finished = false;
 | 
					 | 
				
			||||||
        let responseRes: Response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        function animateResponseText() {
 | 
					 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					 | 
				
			||||||
            responseText += remainText;
 | 
					 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // start animaion
 | 
					 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText, responseRes);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          fetch: fetch as any,
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log("[Baidu] request response content type: ", contentType);
 | 
					 | 
				
			||||||
            responseRes = res;
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onmessage(msg) {
 | 
					 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const delta = json?.result;
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = resJson?.result;
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export { Baidu };
 | 
					 | 
				
			||||||
@@ -1,250 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import { ApiPath, ByteDance, BYTEDANCE_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { streamWithThink } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContentWithoutThinking,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					 | 
				
			||||||
  object: string;
 | 
					 | 
				
			||||||
  data: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    object: string;
 | 
					 | 
				
			||||||
    root: string;
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RequestPayloadForByteDance {
 | 
					 | 
				
			||||||
  messages: {
 | 
					 | 
				
			||||||
    role: "system" | "user" | "assistant";
 | 
					 | 
				
			||||||
    content: string | MultimodalContent[];
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
  stream?: boolean;
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  temperature: number;
 | 
					 | 
				
			||||||
  presence_penalty: number;
 | 
					 | 
				
			||||||
  frequency_penalty: number;
 | 
					 | 
				
			||||||
  top_p: number;
 | 
					 | 
				
			||||||
  max_tokens?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DoubaoApi implements LLMApi {
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.bytedanceUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      const content =
 | 
					 | 
				
			||||||
        v.role === "assistant"
 | 
					 | 
				
			||||||
          ? getMessageTextContentWithoutThinking(v)
 | 
					 | 
				
			||||||
          : await preProcessImageContent(v.content);
 | 
					 | 
				
			||||||
      messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayloadForByteDance = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: shouldStream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(ByteDance.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return streamWithThink(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string | null;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
                reasoning_content: string | null;
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!choices?.length) return { isThinking: false, content: "" };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
					 | 
				
			||||||
            const content = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Skip if both content and reasoning_content are empty or null
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (!reasoning || reasoning.length === 0) &&
 | 
					 | 
				
			||||||
              (!content || content.length === 0)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: "",
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (reasoning && reasoning.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: true,
 | 
					 | 
				
			||||||
                content: reasoning,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            } else if (content && content.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: content,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              isThinking: false,
 | 
					 | 
				
			||||||
              content: "",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayloadForByteDance,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export { ByteDance };
 | 
					 | 
				
			||||||
@@ -1,253 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					 | 
				
			||||||
import { ApiPath, DEEPSEEK_BASE_URL, DeepSeek } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { streamWithThink } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  getMessageTextContentWithoutThinking,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DeepSeekApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.deepseekUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.DeepSeek;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      if (v.role === "assistant") {
 | 
					 | 
				
			||||||
        const content = getMessageTextContentWithoutThinking(v);
 | 
					 | 
				
			||||||
        messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const content = getMessageTextContent(v);
 | 
					 | 
				
			||||||
        messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 检测并修复消息顺序,确保除system外的第一个消息是user
 | 
					 | 
				
			||||||
    const filteredMessages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    let hasFoundFirstUser = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const msg of messages) {
 | 
					 | 
				
			||||||
      if (msg.role === "system") {
 | 
					 | 
				
			||||||
        // Keep all system messages
 | 
					 | 
				
			||||||
        filteredMessages.push(msg);
 | 
					 | 
				
			||||||
      } else if (msg.role === "user") {
 | 
					 | 
				
			||||||
        // User message directly added
 | 
					 | 
				
			||||||
        filteredMessages.push(msg);
 | 
					 | 
				
			||||||
        hasFoundFirstUser = true;
 | 
					 | 
				
			||||||
      } else if (hasFoundFirstUser) {
 | 
					 | 
				
			||||||
        // After finding the first user message, all subsequent non-system messages are retained.
 | 
					 | 
				
			||||||
        filteredMessages.push(msg);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // If hasFoundFirstUser is false and it is not a system message, it will be skipped.
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages: filteredMessages,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					 | 
				
			||||||
      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(DeepSeek.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return streamWithThink(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string | null;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
                reasoning_content: string | null;
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
					 | 
				
			||||||
            const content = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Skip if both content and reasoning_content are empty or null
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (!reasoning || reasoning.length === 0) &&
 | 
					 | 
				
			||||||
              (!content || content.length === 0)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: "",
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (reasoning && reasoning.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: true,
 | 
					 | 
				
			||||||
                content: reasoning,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            } else if (content && content.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: content,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              isThinking: false,
 | 
					 | 
				
			||||||
              content: "",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,292 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import { ApiPath, CHATGLM_BASE_URL, ChatGLM } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { stream } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface BasePayload {
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ChatPayload extends BasePayload {
 | 
					 | 
				
			||||||
  messages: ChatOptions["messages"];
 | 
					 | 
				
			||||||
  stream?: boolean;
 | 
					 | 
				
			||||||
  temperature?: number;
 | 
					 | 
				
			||||||
  presence_penalty?: number;
 | 
					 | 
				
			||||||
  frequency_penalty?: number;
 | 
					 | 
				
			||||||
  top_p?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ImageGenerationPayload extends BasePayload {
 | 
					 | 
				
			||||||
  prompt: string;
 | 
					 | 
				
			||||||
  size?: string;
 | 
					 | 
				
			||||||
  user_id?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface VideoGenerationPayload extends BasePayload {
 | 
					 | 
				
			||||||
  prompt: string;
 | 
					 | 
				
			||||||
  duration?: number;
 | 
					 | 
				
			||||||
  resolution?: string;
 | 
					 | 
				
			||||||
  user_id?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type ModelType = "chat" | "image" | "video";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ChatGLMApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private getModelType(model: string): ModelType {
 | 
					 | 
				
			||||||
    if (model.startsWith("cogview-")) return "image";
 | 
					 | 
				
			||||||
    if (model.startsWith("cogvideo-")) return "video";
 | 
					 | 
				
			||||||
    return "chat";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private getModelPath(type: ModelType): string {
 | 
					 | 
				
			||||||
    switch (type) {
 | 
					 | 
				
			||||||
      case "image":
 | 
					 | 
				
			||||||
        return ChatGLM.ImagePath;
 | 
					 | 
				
			||||||
      case "video":
 | 
					 | 
				
			||||||
        return ChatGLM.VideoPath;
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        return ChatGLM.ChatPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private createPayload(
 | 
					 | 
				
			||||||
    messages: ChatOptions["messages"],
 | 
					 | 
				
			||||||
    modelConfig: any,
 | 
					 | 
				
			||||||
    options: ChatOptions,
 | 
					 | 
				
			||||||
  ): BasePayload {
 | 
					 | 
				
			||||||
    const modelType = this.getModelType(modelConfig.model);
 | 
					 | 
				
			||||||
    const lastMessage = messages[messages.length - 1];
 | 
					 | 
				
			||||||
    const prompt =
 | 
					 | 
				
			||||||
      typeof lastMessage.content === "string"
 | 
					 | 
				
			||||||
        ? lastMessage.content
 | 
					 | 
				
			||||||
        : lastMessage.content.map((c) => c.text).join("\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    switch (modelType) {
 | 
					 | 
				
			||||||
      case "image":
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          model: modelConfig.model,
 | 
					 | 
				
			||||||
          prompt,
 | 
					 | 
				
			||||||
          size: options.config.size,
 | 
					 | 
				
			||||||
        } as ImageGenerationPayload;
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          messages,
 | 
					 | 
				
			||||||
          stream: options.config.stream,
 | 
					 | 
				
			||||||
          model: modelConfig.model,
 | 
					 | 
				
			||||||
          temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
          presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
          frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
          top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
        } as ChatPayload;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private parseResponse(modelType: ModelType, json: any): string {
 | 
					 | 
				
			||||||
    switch (modelType) {
 | 
					 | 
				
			||||||
      case "image": {
 | 
					 | 
				
			||||||
        const imageUrl = json.data?.[0]?.url;
 | 
					 | 
				
			||||||
        return imageUrl ? `` : "";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      case "video": {
 | 
					 | 
				
			||||||
        const videoUrl = json.data?.[0]?.url;
 | 
					 | 
				
			||||||
        return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        return this.extractMessage(json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.chatglmUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.ChatGLM;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
    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,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    const modelType = this.getModelType(modelConfig.model);
 | 
					 | 
				
			||||||
    const requestPayload = this.createPayload(messages, modelConfig, options);
 | 
					 | 
				
			||||||
    const path = this.path(this.getModelPath(modelType));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log(`[Request] glm ${modelType} payload: `, requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (modelType === "image" || modelType === "video") {
 | 
					 | 
				
			||||||
        const res = await fetch(path, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        console.log(`[Response] glm ${modelType}:`, resJson);
 | 
					 | 
				
			||||||
        const message = this.parseResponse(modelType, resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return stream(
 | 
					 | 
				
			||||||
          path,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(path, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,128 +1,31 @@
 | 
				
			|||||||
import { ApiPath, Google } from "@/app/constant";
 | 
					import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
 | 
				
			||||||
 | 
					import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  EventStreamContentType,
 | 
				
			||||||
  getHeaders,
 | 
					  fetchEventSource,
 | 
				
			||||||
  LLMApi,
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
  LLMModel,
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
  LLMUsage,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { stream } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { GEMINI_BASE_URL } from "@/app/constant";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import de from "@/app/locales/de";
 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  getMessageImages,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { nanoid } from "nanoid";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class GeminiProApi implements LLMApi {
 | 
					export class GeminiProApi implements LLMApi {
 | 
				
			||||||
  path(path: string, shouldStream = false): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.googleUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      baseUrl = isApp ? GEMINI_BASE_URL : 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("/");
 | 
					 | 
				
			||||||
    if (shouldStream) {
 | 
					 | 
				
			||||||
      chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return chatPath;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
    console.log("[Response] gemini-pro response: ", res);
 | 
					    console.log("[Response] gemini-pro response: ", res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const getTextFromParts = (parts: any[]) => {
 | 
					 | 
				
			||||||
      if (!Array.isArray(parts)) return "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return parts
 | 
					 | 
				
			||||||
        .map((part) => part?.text || "")
 | 
					 | 
				
			||||||
        .filter((text) => text.trim() !== "")
 | 
					 | 
				
			||||||
        .join("\n\n");
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let content = "";
 | 
					 | 
				
			||||||
    if (Array.isArray(res)) {
 | 
					 | 
				
			||||||
      res.map((item) => {
 | 
					 | 
				
			||||||
        content += getTextFromParts(item?.candidates?.at(0)?.content?.parts);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
 | 
					      res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
 | 
				
			||||||
      content || //getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
 | 
					 | 
				
			||||||
      res?.error?.message ||
 | 
					      res?.error?.message ||
 | 
				
			||||||
      ""
 | 
					      ""
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions): Promise<void> {
 | 
					  async chat(options: ChatOptions): Promise<void> {
 | 
				
			||||||
    const apiClient = this;
 | 
					    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);
 | 
					 | 
				
			||||||
        if (images.length > 0) {
 | 
					 | 
				
			||||||
          multimodal = true;
 | 
					 | 
				
			||||||
          parts = parts.concat(
 | 
					 | 
				
			||||||
            images.map((image) => {
 | 
					 | 
				
			||||||
              const imageType = image.split(";")[0].split(":")[1];
 | 
					 | 
				
			||||||
              const imageData = image.split(",")[1];
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                inline_data: {
 | 
					 | 
				
			||||||
                  mime_type: imageType,
 | 
					 | 
				
			||||||
                  data: imageData,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
      role: v.role.replace("assistant", "model").replace("system", "user"),
 | 
					      role: v.role.replace("assistant", "model").replace("system", "user"),
 | 
				
			||||||
        parts: parts,
 | 
					      parts: [{ text: v.content }],
 | 
				
			||||||
      };
 | 
					    }));
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // google requires that role in neighboring messages must not be the same
 | 
					    // google requires that role in neighboring messages must not be the same
 | 
				
			||||||
    for (let i = 0; i < messages.length - 1; ) {
 | 
					    for (let i = 0; i < messages.length - 1; ) {
 | 
				
			||||||
@@ -137,11 +40,6 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
        i++;
 | 
					        i++;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // 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 = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
@@ -164,33 +62,30 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
      safetySettings: [
 | 
					      safetySettings: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_HARASSMENT",
 | 
					          category: "HARM_CATEGORY_HARASSMENT",
 | 
				
			||||||
          threshold: accessStore.googleSafetySettings,
 | 
					          threshold: "BLOCK_ONLY_HIGH",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
					          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
				
			||||||
          threshold: accessStore.googleSafetySettings,
 | 
					          threshold: "BLOCK_ONLY_HIGH",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
					          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
				
			||||||
          threshold: accessStore.googleSafetySettings,
 | 
					          threshold: "BLOCK_ONLY_HIGH",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
					          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
				
			||||||
          threshold: accessStore.googleSafetySettings,
 | 
					          threshold: "BLOCK_ONLY_HIGH",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let shouldStream = !!options.config.stream;
 | 
					    console.log("[Request] google payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
    const controller = new AbortController();
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
 | 
					      const chatPath = this.path(Google.ChatPath);
 | 
				
			||||||
      const chatPath = this.path(
 | 
					 | 
				
			||||||
        Google.ChatPath(modelConfig.model),
 | 
					 | 
				
			||||||
        shouldStream,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					      const chatPayload = {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
@@ -198,99 +93,103 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
        headers: getHeaders(),
 | 
					        headers: getHeaders(),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const isThinking = options.config.model.includes("-thinking");
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					        let responseText = "";
 | 
				
			||||||
          .getState()
 | 
					        let remainText = "";
 | 
				
			||||||
          .getAsTools(
 | 
					        let streamChatPath = chatPath.replace(
 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					          "generateContent",
 | 
				
			||||||
 | 
					          "streamGenerateContent",
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        return stream(
 | 
					        let finished = false;
 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          // @ts-ignore
 | 
					 | 
				
			||||||
          tools.length > 0
 | 
					 | 
				
			||||||
            ? // @ts-ignore
 | 
					 | 
				
			||||||
              [{ functionDeclarations: tools.map((tool) => tool.function) }]
 | 
					 | 
				
			||||||
            : [],
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const chunkJson = JSON.parse(text);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const functionCall = chunkJson?.candidates
 | 
					        let existingTexts: string[] = [];
 | 
				
			||||||
              ?.at(0)
 | 
					        const finish = () => {
 | 
				
			||||||
              ?.content.parts.at(0)?.functionCall;
 | 
					          finished = true;
 | 
				
			||||||
            if (functionCall) {
 | 
					          options.onFinish(existingTexts.join(""));
 | 
				
			||||||
              const { name, args } = functionCall;
 | 
					        };
 | 
				
			||||||
              runTools.push({
 | 
					
 | 
				
			||||||
                id: nanoid(),
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
                type: "function",
 | 
					        function animateResponseText() {
 | 
				
			||||||
                function: {
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
                  name,
 | 
					            responseText += remainText;
 | 
				
			||||||
                  arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
 | 
					            finish();
 | 
				
			||||||
                },
 | 
					            return;
 | 
				
			||||||
              });
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
            return chunkJson?.candidates
 | 
					
 | 
				
			||||||
              ?.at(0)
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
              ?.content.parts?.map((part: { text: string }) => part.text)
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
              .join("\n\n");
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
          },
 | 
					            responseText += fetchText;
 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
          (
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					          }
 | 
				
			||||||
            toolCallMessage: any,
 | 
					
 | 
				
			||||||
            toolCallResult: any[],
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
          ) => {
 | 
					        }
 | 
				
			||||||
            // @ts-ignore
 | 
					
 | 
				
			||||||
            requestPayload?.contents?.splice(
 | 
					        // start animaion
 | 
				
			||||||
              // @ts-ignore
 | 
					        animateResponseText();
 | 
				
			||||||
              requestPayload?.contents?.length,
 | 
					        fetch(streamChatPath, chatPayload)
 | 
				
			||||||
              0,
 | 
					          .then((response) => {
 | 
				
			||||||
              {
 | 
					            const reader = response?.body?.getReader();
 | 
				
			||||||
                role: "model",
 | 
					            const decoder = new TextDecoder();
 | 
				
			||||||
                parts: toolCallMessage.tool_calls.map(
 | 
					            let partialData = "";
 | 
				
			||||||
                  (tool: ChatMessageTool) => ({
 | 
					
 | 
				
			||||||
                    functionCall: {
 | 
					            return reader?.read().then(function processText({
 | 
				
			||||||
                      name: tool?.function?.name,
 | 
					              done,
 | 
				
			||||||
                      args: JSON.parse(tool?.function?.arguments as string),
 | 
					              value,
 | 
				
			||||||
                    },
 | 
					            }): Promise<any> {
 | 
				
			||||||
                  }),
 | 
					              if (done) {
 | 
				
			||||||
                ),
 | 
					                console.log("Stream complete");
 | 
				
			||||||
              },
 | 
					                // options.onFinish(responseText + remainText);
 | 
				
			||||||
              // @ts-ignore
 | 
					                finished = true;
 | 
				
			||||||
              ...toolCallResult.map((result) => ({
 | 
					                return Promise.resolve();
 | 
				
			||||||
                role: "function",
 | 
					              }
 | 
				
			||||||
                parts: [
 | 
					
 | 
				
			||||||
                  {
 | 
					              partialData += decoder.decode(value, { stream: true });
 | 
				
			||||||
                    functionResponse: {
 | 
					
 | 
				
			||||||
                      name: result.name,
 | 
					              try {
 | 
				
			||||||
                      response: {
 | 
					                let data = JSON.parse(ensureProperEnding(partialData));
 | 
				
			||||||
                        name: result.name,
 | 
					
 | 
				
			||||||
                        content: result.content, // TODO just text content...
 | 
					                const textArray = data.reduce(
 | 
				
			||||||
                      },
 | 
					                  (acc: string[], item: { candidates: any[] }) => {
 | 
				
			||||||
                    },
 | 
					                    const texts = item.candidates.map((candidate) =>
 | 
				
			||||||
                  },
 | 
					                      candidate.content.parts
 | 
				
			||||||
                ],
 | 
					                        .map((part: { text: any }) => part.text)
 | 
				
			||||||
              })),
 | 
					                        .join(""),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					                    return acc.concat(texts);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
          options,
 | 
					                  [],
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (textArray.length > existingTexts.length) {
 | 
				
			||||||
 | 
					                  const deltaArray = textArray.slice(existingTexts.length);
 | 
				
			||||||
 | 
					                  existingTexts = textArray;
 | 
				
			||||||
 | 
					                  remainText += deltaArray.join("");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              } catch (error) {
 | 
				
			||||||
 | 
					                // console.log("[Response Animation] error: ", error,partialData);
 | 
				
			||||||
 | 
					                // skip error message when parsing json
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return reader.read().then(processText);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((error) => {
 | 
				
			||||||
 | 
					            console.error("Error:", error);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (resJson?.promptFeedback?.blockReason) {
 | 
					        if (resJson?.promptFeedback?.blockReason) {
 | 
				
			||||||
          // being blocked
 | 
					          // being blocked
 | 
				
			||||||
          options.onError?.(
 | 
					          options.onError?.(
 | 
				
			||||||
@@ -300,8 +199,8 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const message = apiClient.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message, res);
 | 
					        options.onFinish(message);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
@@ -314,4 +213,14 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
  async models(): Promise<LLMModel[]> {
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
    return [];
 | 
					    return [];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    return "/api/google/" + path;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ensureProperEnding(str: string) {
 | 
				
			||||||
 | 
					  if (str.startsWith("[") && !str.endsWith("]")) {
 | 
				
			||||||
 | 
					    return str + "]";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return str;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,253 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  IFLYTEK_BASE_URL,
 | 
					 | 
				
			||||||
  Iflytek,
 | 
					 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class SparkApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.iflytekUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.Iflytek;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      const content = getMessageTextContent(v);
 | 
					 | 
				
			||||||
      messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					 | 
				
			||||||
      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] Spark payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(Iflytek.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        let responseText = "";
 | 
					 | 
				
			||||||
        let remainText = "";
 | 
					 | 
				
			||||||
        let finished = false;
 | 
					 | 
				
			||||||
        let responseRes: Response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Animate response text to make it look smooth
 | 
					 | 
				
			||||||
        function animateResponseText() {
 | 
					 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					 | 
				
			||||||
            responseText += remainText;
 | 
					 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Start animation
 | 
					 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText, responseRes);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          fetch: fetch as any,
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log("[Spark] request response content type: ", contentType);
 | 
					 | 
				
			||||||
            responseRes = res;
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Handle different error scenarios
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                extraInfo = Locale.Error.Unauthorized;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              options.onError?.(
 | 
					 | 
				
			||||||
                new Error(
 | 
					 | 
				
			||||||
                  `Request failed with status ${res.status}: ${extraInfo}`,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onmessage(msg) {
 | 
					 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
                delta: { content: string };
 | 
					 | 
				
			||||||
              }>;
 | 
					 | 
				
			||||||
              const delta = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text);
 | 
					 | 
				
			||||||
              options.onError?.(new Error(`Failed to parse response: ${text}`));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!res.ok) {
 | 
					 | 
				
			||||||
          const errorText = await res.text();
 | 
					 | 
				
			||||||
          options.onError?.(
 | 
					 | 
				
			||||||
            new Error(`Request failed with status ${res.status}: ${errorText}`),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,200 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  MOONSHOT_BASE_URL,
 | 
					 | 
				
			||||||
  Moonshot,
 | 
					 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { stream } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class MoonshotApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.moonshotUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.Moonshot;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      const content = getMessageTextContent(v);
 | 
					 | 
				
			||||||
      messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					 | 
				
			||||||
      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(Moonshot.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return stream(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,49 +1,22 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  OPENAI_BASE_URL,
 | 
					  DEFAULT_API_HOST,
 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
  OpenaiPath,
 | 
					  OpenaiPath,
 | 
				
			||||||
  Azure,
 | 
					 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import {
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  preProcessImageContent,
 | 
					 | 
				
			||||||
  uploadImage,
 | 
					 | 
				
			||||||
  base64Image2Blob,
 | 
					 | 
				
			||||||
  streamWithThink,
 | 
					 | 
				
			||||||
} from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					 | 
				
			||||||
import { ModelSize, DalleQuality, DalleStyle } from "@/app/typing";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  LLMUsage,
 | 
					 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import Locale from "../../locales";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getMessageTextContent,
 | 
					  EventStreamContentType,
 | 
				
			||||||
  isVisionModel,
 | 
					  fetchEventSource,
 | 
				
			||||||
  isDalle3 as _isDalle3,
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
} from "@/app/utils";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					import { makeAzurePath } from "@/app/azure";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -54,114 +27,80 @@ export interface OpenAIListModelResponse {
 | 
				
			|||||||
  }>;
 | 
					  }>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RequestPayload {
 | 
					 | 
				
			||||||
  messages: {
 | 
					 | 
				
			||||||
    role: "system" | "user" | "assistant";
 | 
					 | 
				
			||||||
    content: string | MultimodalContent[];
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
  stream?: boolean;
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  temperature: number;
 | 
					 | 
				
			||||||
  presence_penalty: number;
 | 
					 | 
				
			||||||
  frequency_penalty: number;
 | 
					 | 
				
			||||||
  top_p: number;
 | 
					 | 
				
			||||||
  max_tokens?: number;
 | 
					 | 
				
			||||||
  max_completion_tokens?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface DalleRequestPayload {
 | 
					 | 
				
			||||||
  model: string;
 | 
					 | 
				
			||||||
  prompt: string;
 | 
					 | 
				
			||||||
  response_format: "url" | "b64_json";
 | 
					 | 
				
			||||||
  n: number;
 | 
					 | 
				
			||||||
  size: ModelSize;
 | 
					 | 
				
			||||||
  quality: DalleQuality;
 | 
					 | 
				
			||||||
  style: DalleStyle;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ChatGPTApi implements LLMApi {
 | 
					export class ChatGPTApi implements LLMApi {
 | 
				
			||||||
  private disableListModels = true;
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  path(path: string): string {
 | 
					  path(path: string): string {
 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let baseUrl = "";
 | 
					    const isAzure = accessStore.provider === ServiceProvider.Azure;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isAzure = path.includes("deployments");
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
    if (isAzure && !accessStore.isValidAzure()) {
 | 
					    if (isAzure && !accessStore.isValidAzure()) {
 | 
				
			||||||
      throw Error(
 | 
					      throw Error(
 | 
				
			||||||
        "incomplete azure config, please check it in your settings page",
 | 
					        "incomplete azure config, please check it in your settings page",
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
 | 
					    let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
 | 
					      baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
 | 
				
			||||||
      baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
 | 
				
			||||||
      !baseUrl.startsWith("http") &&
 | 
					 | 
				
			||||||
      !isAzure &&
 | 
					 | 
				
			||||||
      !baseUrl.startsWith(ApiPath.OpenAI)
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					    if (isAzure) {
 | 
				
			||||||
 | 
					      path = makeAzurePath(path, accessStore.azureApiVersion);
 | 
				
			||||||
    // try rebuild url, when using cloudflare ai gateway in client
 | 
					 | 
				
			||||||
    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async extractMessage(res: any) {
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
    if (res.error) {
 | 
					 | 
				
			||||||
      return "```\n" + JSON.stringify(res, null, 4) + "\n```";
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    // dalle3 model return url, using url create image message
 | 
					
 | 
				
			||||||
    if (res.data) {
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
      let url = res.data?.at(0)?.url ?? "";
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
      const b64_json = res.data?.at(0)?.b64_json ?? "";
 | 
					 | 
				
			||||||
      if (!url && b64_json) {
 | 
					 | 
				
			||||||
        // uploadImage
 | 
					 | 
				
			||||||
        url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
      return [
 | 
					
 | 
				
			||||||
        {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
          type: "image_url",
 | 
					    const messages = options.messages.map((v) => ({
 | 
				
			||||||
          image_url: {
 | 
					      role: v.role,
 | 
				
			||||||
            url,
 | 
					      content: v.content,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? res;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    const requestPayload = {
 | 
					 | 
				
			||||||
      model: options.model,
 | 
					 | 
				
			||||||
      input: options.input,
 | 
					 | 
				
			||||||
      voice: options.voice,
 | 
					 | 
				
			||||||
      response_format: options.response_format,
 | 
					 | 
				
			||||||
      speed: options.speed,
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log("[Request] openai speech payload: ", requestPayload);
 | 
					    const requestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
    const controller = new AbortController();
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const speechPath = this.path(OpenaiPath.SpeechPath);
 | 
					      const chatPath = this.path(OpenaiPath.ChatPath);
 | 
				
			||||||
      const speechPayload = {
 | 
					      const chatPayload = {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
        signal: controller.signal,
 | 
					        signal: controller.signal,
 | 
				
			||||||
@@ -174,238 +113,121 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
        REQUEST_TIMEOUT_MS,
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const res = await fetch(speechPath, speechPayload);
 | 
					 | 
				
			||||||
      clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
      return await res.arrayBuffer();
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a speech request", e);
 | 
					 | 
				
			||||||
      throw e;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isDalle3 = _isDalle3(options.config.model);
 | 
					 | 
				
			||||||
    const isO1OrO3 =
 | 
					 | 
				
			||||||
      options.config.model.startsWith("o1") ||
 | 
					 | 
				
			||||||
      options.config.model.startsWith("o3") ||
 | 
					 | 
				
			||||||
      options.config.model.startsWith("o4-mini");
 | 
					 | 
				
			||||||
    if (isDalle3) {
 | 
					 | 
				
			||||||
      const prompt = getMessageTextContent(
 | 
					 | 
				
			||||||
        options.messages.slice(-1)?.pop() as any,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      requestPayload = {
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        prompt,
 | 
					 | 
				
			||||||
        // URLs are only valid for 60 minutes after the image has been generated.
 | 
					 | 
				
			||||||
        response_format: "b64_json", // using b64_json, and save image in CacheStorage
 | 
					 | 
				
			||||||
        n: 1,
 | 
					 | 
				
			||||||
        size: options.config?.size ?? "1024x1024",
 | 
					 | 
				
			||||||
        quality: options.config?.quality ?? "standard",
 | 
					 | 
				
			||||||
        style: options.config?.style ?? "vivid",
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
      const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
      for (const v of options.messages) {
 | 
					 | 
				
			||||||
        const content = visionModel
 | 
					 | 
				
			||||||
          ? await preProcessImageContent(v.content)
 | 
					 | 
				
			||||||
          : getMessageTextContent(v);
 | 
					 | 
				
			||||||
        if (!(isO1OrO3 && v.role === "system"))
 | 
					 | 
				
			||||||
          messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
 | 
					 | 
				
			||||||
      requestPayload = {
 | 
					 | 
				
			||||||
        messages,
 | 
					 | 
				
			||||||
        stream: options.config.stream,
 | 
					 | 
				
			||||||
        model: modelConfig.model,
 | 
					 | 
				
			||||||
        temperature: !isO1OrO3 ? modelConfig.temperature : 1,
 | 
					 | 
				
			||||||
        presence_penalty: !isO1OrO3 ? modelConfig.presence_penalty : 0,
 | 
					 | 
				
			||||||
        frequency_penalty: !isO1OrO3 ? modelConfig.frequency_penalty : 0,
 | 
					 | 
				
			||||||
        top_p: !isO1OrO3 ? modelConfig.top_p : 1,
 | 
					 | 
				
			||||||
        // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					 | 
				
			||||||
        // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
 | 
					 | 
				
			||||||
      if (isO1OrO3) {
 | 
					 | 
				
			||||||
        requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // add max_tokens to vision model
 | 
					 | 
				
			||||||
      if (visionModel && !isO1OrO3) {
 | 
					 | 
				
			||||||
        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !isDalle3 && !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      let chatPath = "";
 | 
					 | 
				
			||||||
      if (modelConfig.providerName === ServiceProvider.Azure) {
 | 
					 | 
				
			||||||
        // find model, and get displayName as deployName
 | 
					 | 
				
			||||||
        const { models: configModels, customModels: configCustomModels } =
 | 
					 | 
				
			||||||
          useAppConfig.getState();
 | 
					 | 
				
			||||||
        const {
 | 
					 | 
				
			||||||
          defaultModel,
 | 
					 | 
				
			||||||
          customModels: accessCustomModels,
 | 
					 | 
				
			||||||
          useCustomConfig,
 | 
					 | 
				
			||||||
        } = useAccessStore.getState();
 | 
					 | 
				
			||||||
        const models = collectModelsWithDefaultModel(
 | 
					 | 
				
			||||||
          configModels,
 | 
					 | 
				
			||||||
          [configCustomModels, accessCustomModels].join(","),
 | 
					 | 
				
			||||||
          defaultModel,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const model = models.find(
 | 
					 | 
				
			||||||
          (model) =>
 | 
					 | 
				
			||||||
            model.name === modelConfig.model &&
 | 
					 | 
				
			||||||
            model?.provider?.providerName === ServiceProvider.Azure,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        chatPath = this.path(
 | 
					 | 
				
			||||||
          (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
 | 
					 | 
				
			||||||
            (model?.displayName ?? model?.name) as string,
 | 
					 | 
				
			||||||
            useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        chatPath = this.path(
 | 
					 | 
				
			||||||
          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let index = -1;
 | 
					        let responseText = "";
 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					        let remainText = "";
 | 
				
			||||||
          .getState()
 | 
					        let finished = false;
 | 
				
			||||||
          .getAsTools(
 | 
					
 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
 | 
					        function animateResponseText() {
 | 
				
			||||||
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
 | 
					            responseText += remainText;
 | 
				
			||||||
 | 
					            console.log("[Response Animation] finished");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // start animaion
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log(
 | 
				
			||||||
 | 
					              "[OpenAI] request response content type: ",
 | 
				
			||||||
 | 
					              contentType,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        // console.log("getAsTools", tools, funcs);
 | 
					
 | 
				
			||||||
        streamWithThink(
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
          chatPath,
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
          requestPayload,
 | 
					              return finish();
 | 
				
			||||||
          getHeaders(),
 | 
					            }
 | 
				
			||||||
          tools as any,
 | 
					
 | 
				
			||||||
          funcs,
 | 
					            if (
 | 
				
			||||||
          controller,
 | 
					              !res.ok ||
 | 
				
			||||||
          // parseSSE
 | 
					              !res.headers
 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					                .get("content-type")
 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					                ?.startsWith(EventStreamContentType) ||
 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					              res.status !== 200
 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					            ) {
 | 
				
			||||||
 | 
					              const responseTexts = [responseText];
 | 
				
			||||||
 | 
					              let extraInfo = await res.clone().text();
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                const resJson = await res.clone().json();
 | 
				
			||||||
 | 
					                extraInfo = prettyObject(resJson);
 | 
				
			||||||
 | 
					              } catch {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (res.status === 401) {
 | 
				
			||||||
 | 
					                responseTexts.push(Locale.Error.Unauthorized);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (extraInfo) {
 | 
				
			||||||
 | 
					                responseTexts.push(extraInfo);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              responseText = responseTexts.join("\n\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onmessage(msg) {
 | 
				
			||||||
 | 
					            if (msg.data === "[DONE]" || finished) {
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const text = msg.data;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const json = JSON.parse(text) as {
 | 
				
			||||||
 | 
					                choices: Array<{
 | 
				
			||||||
                  delta: {
 | 
					                  delta: {
 | 
				
			||||||
                    content: string;
 | 
					                    content: string;
 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
                reasoning_content: string | null;
 | 
					 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                }>;
 | 
					                }>;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
            if (!choices?.length) return { isThinking: false, content: "" };
 | 
					              const delta = json.choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					              if (delta) {
 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					                remainText += delta;
 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					              }
 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					            } catch (e) {
 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					              console.error("[Request] parse error", text);
 | 
				
			||||||
              if (id) {
 | 
					            }
 | 
				
			||||||
                index += 1;
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          onclose() {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onerror(e) {
 | 
				
			||||||
 | 
					            options.onError?.(e);
 | 
				
			||||||
 | 
					            throw e;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          openWhenHidden: true,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
					 | 
				
			||||||
            const content = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Skip if both content and reasoning_content are empty or null
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (!reasoning || reasoning.length === 0) &&
 | 
					 | 
				
			||||||
              (!content || content.length === 0)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: "",
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (reasoning && reasoning.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: true,
 | 
					 | 
				
			||||||
                content: reasoning,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            } else if (content && content.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: content,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              isThinking: false,
 | 
					 | 
				
			||||||
              content: "",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // reset index value
 | 
					 | 
				
			||||||
            index = -1;
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const chatPayload = {
 | 
					 | 
				
			||||||
          method: "POST",
 | 
					 | 
				
			||||||
          body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
          signal: controller.signal,
 | 
					 | 
				
			||||||
          headers: getHeaders(),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // make a fetch request
 | 
					 | 
				
			||||||
        const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
          () => controller.abort(),
 | 
					 | 
				
			||||||
          getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = await this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message, res);
 | 
					        options.onFinish(message);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
@@ -491,26 +313,20 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const resJson = (await res.json()) as OpenAIListModelResponse;
 | 
					    const resJson = (await res.json()) as OpenAIListModelResponse;
 | 
				
			||||||
    const chatModels = resJson.data?.filter(
 | 
					    const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
 | 
				
			||||||
      (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    console.log("[Models]", chatModels);
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!chatModels) {
 | 
					    if (!chatModels) {
 | 
				
			||||||
      return [];
 | 
					      return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
 | 
					 | 
				
			||||||
    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
					 | 
				
			||||||
    return chatModels.map((m) => ({
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
      name: m.id,
 | 
					      name: m.id,
 | 
				
			||||||
      available: true,
 | 
					      available: true,
 | 
				
			||||||
      sorted: seq++,
 | 
					 | 
				
			||||||
      provider: {
 | 
					      provider: {
 | 
				
			||||||
        id: "openai",
 | 
					        id: "openai",
 | 
				
			||||||
        providerName: "OpenAI",
 | 
					        providerName: "OpenAI",
 | 
				
			||||||
        providerType: "openai",
 | 
					        providerType: "openai",
 | 
				
			||||||
        sorted: 1,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,287 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  SILICONFLOW_BASE_URL,
 | 
					 | 
				
			||||||
  SiliconFlow,
 | 
					 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  getMessageTextContentWithoutThinking,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
export interface SiliconFlowListModelResponse {
 | 
					 | 
				
			||||||
  object: string;
 | 
					 | 
				
			||||||
  data: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    object: string;
 | 
					 | 
				
			||||||
    root: string;
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class SiliconflowApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.siliconflowUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.SiliconFlow;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? SILICONFLOW_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      !baseUrl.startsWith("http") &&
 | 
					 | 
				
			||||||
      !baseUrl.startsWith(ApiPath.SiliconFlow)
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      if (v.role === "assistant") {
 | 
					 | 
				
			||||||
        const content = getMessageTextContentWithoutThinking(v);
 | 
					 | 
				
			||||||
        messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const content = visionModel
 | 
					 | 
				
			||||||
          ? await preProcessImageContent(v.content)
 | 
					 | 
				
			||||||
          : getMessageTextContent(v);
 | 
					 | 
				
			||||||
        messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					 | 
				
			||||||
      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(SiliconFlow.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // console.log(chatPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Use extended timeout for thinking models as they typically require more processing time
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return streamWithThink(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string | null;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
                reasoning_content: string | null;
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
					 | 
				
			||||||
            const content = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Skip if both content and reasoning_content are empty or null
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (!reasoning || reasoning.length === 0) &&
 | 
					 | 
				
			||||||
              (!content || content.length === 0)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: "",
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (reasoning && reasoning.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: true,
 | 
					 | 
				
			||||||
                content: reasoning,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            } else if (content && content.length > 0) {
 | 
					 | 
				
			||||||
              return {
 | 
					 | 
				
			||||||
                isThinking: false,
 | 
					 | 
				
			||||||
                content: content,
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              isThinking: false,
 | 
					 | 
				
			||||||
              content: "",
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    if (this.disableListModels) {
 | 
					 | 
				
			||||||
      return DEFAULT_MODELS.slice();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const res = await fetch(this.path(SiliconFlow.ListModelPath), {
 | 
					 | 
				
			||||||
      method: "GET",
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        ...getHeaders(),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const resJson = (await res.json()) as SiliconFlowListModelResponse;
 | 
					 | 
				
			||||||
    const chatModels = resJson.data;
 | 
					 | 
				
			||||||
    console.log("[Models]", chatModels);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!chatModels) {
 | 
					 | 
				
			||||||
      return [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
					 | 
				
			||||||
    return chatModels.map((m) => ({
 | 
					 | 
				
			||||||
      name: m.id,
 | 
					 | 
				
			||||||
      available: true,
 | 
					 | 
				
			||||||
      sorted: seq++,
 | 
					 | 
				
			||||||
      provider: {
 | 
					 | 
				
			||||||
        id: "siliconflow",
 | 
					 | 
				
			||||||
        providerName: "SiliconFlow",
 | 
					 | 
				
			||||||
        providerType: "siliconflow",
 | 
					 | 
				
			||||||
        sorted: 14,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,278 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
import { ApiPath, TENCENT_BASE_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					 | 
				
			||||||
  getTimeoutMSByModel,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import mapKeys from "lodash-es/mapKeys";
 | 
					 | 
				
			||||||
import mapValues from "lodash-es/mapValues";
 | 
					 | 
				
			||||||
import isArray from "lodash-es/isArray";
 | 
					 | 
				
			||||||
import isObject from "lodash-es/isObject";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					 | 
				
			||||||
  object: string;
 | 
					 | 
				
			||||||
  data: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    object: string;
 | 
					 | 
				
			||||||
    root: string;
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RequestPayload {
 | 
					 | 
				
			||||||
  Messages: {
 | 
					 | 
				
			||||||
    Role: "system" | "user" | "assistant";
 | 
					 | 
				
			||||||
    Content: string | MultimodalContent[];
 | 
					 | 
				
			||||||
  }[];
 | 
					 | 
				
			||||||
  Stream?: boolean;
 | 
					 | 
				
			||||||
  Model: string;
 | 
					 | 
				
			||||||
  Temperature: number;
 | 
					 | 
				
			||||||
  TopP: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function capitalizeKeys(obj: any): any {
 | 
					 | 
				
			||||||
  if (isArray(obj)) {
 | 
					 | 
				
			||||||
    return obj.map(capitalizeKeys);
 | 
					 | 
				
			||||||
  } else if (isObject(obj)) {
 | 
					 | 
				
			||||||
    return mapValues(
 | 
					 | 
				
			||||||
      mapKeys(obj, (value: any, key: string) =>
 | 
					 | 
				
			||||||
        key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      capitalizeKeys,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return obj;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class HunyuanApi implements LLMApi {
 | 
					 | 
				
			||||||
  path(): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.tencentUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl);
 | 
					 | 
				
			||||||
    return baseUrl;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.Choices?.at(0)?.Message?.Content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					 | 
				
			||||||
    const messages = options.messages.map((v, index) => ({
 | 
					 | 
				
			||||||
      // "Messages 中 system 角色必须位于列表的最开始"
 | 
					 | 
				
			||||||
      role: index !== 0 && v.role === "system" ? "user" : v.role,
 | 
					 | 
				
			||||||
      content: visionModel ? v.content : getMessageTextContent(v),
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = capitalizeKeys({
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] Tencent payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path();
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        let responseText = "";
 | 
					 | 
				
			||||||
        let remainText = "";
 | 
					 | 
				
			||||||
        let finished = false;
 | 
					 | 
				
			||||||
        let responseRes: Response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        function animateResponseText() {
 | 
					 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					 | 
				
			||||||
            responseText += remainText;
 | 
					 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // start animaion
 | 
					 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText, responseRes);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          fetch: fetch as any,
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[Tencent] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            responseRes = res;
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onmessage(msg) {
 | 
					 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const choices = json.Choices as Array<{
 | 
					 | 
				
			||||||
                Delta: { Content: string };
 | 
					 | 
				
			||||||
              }>;
 | 
					 | 
				
			||||||
              const delta = choices[0]?.Delta?.Content;
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,194 +0,0 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					 | 
				
			||||||
import { ApiPath, XAI_BASE_URL, XAI } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useAccessStore,
 | 
					 | 
				
			||||||
  useAppConfig,
 | 
					 | 
				
			||||||
  useChatStore,
 | 
					 | 
				
			||||||
  ChatMessageTool,
 | 
					 | 
				
			||||||
  usePluginStore,
 | 
					 | 
				
			||||||
} from "@/app/store";
 | 
					 | 
				
			||||||
import { stream } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ChatOptions,
 | 
					 | 
				
			||||||
  getHeaders,
 | 
					 | 
				
			||||||
  LLMApi,
 | 
					 | 
				
			||||||
  LLMModel,
 | 
					 | 
				
			||||||
  SpeechOptions,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { getTimeoutMSByModel } from "@/app/utils";
 | 
					 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { RequestPayload } from "./openai";
 | 
					 | 
				
			||||||
import { fetch } from "@/app/utils/stream";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class XAIApi implements LLMApi {
 | 
					 | 
				
			||||||
  private disableListModels = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  path(path: string): string {
 | 
					 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let baseUrl = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (accessStore.useCustomConfig) {
 | 
					 | 
				
			||||||
      baseUrl = accessStore.xaiUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					 | 
				
			||||||
      const apiPath = ApiPath.XAI;
 | 
					 | 
				
			||||||
      baseUrl = isApp ? XAI_BASE_URL : apiPath;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
 | 
					 | 
				
			||||||
      baseUrl = "https://" + baseUrl;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [baseUrl, path].join("/");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  extractMessage(res: any) {
 | 
					 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
					 | 
				
			||||||
    throw new Error("Method not implemented.");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					 | 
				
			||||||
    for (const v of options.messages) {
 | 
					 | 
				
			||||||
      const content = await preProcessImageContent(v.content);
 | 
					 | 
				
			||||||
      messages.push({ role: v.role, content });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const modelConfig = {
 | 
					 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					 | 
				
			||||||
      ...{
 | 
					 | 
				
			||||||
        model: options.config.model,
 | 
					 | 
				
			||||||
        providerName: options.config.providerName,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					 | 
				
			||||||
      messages,
 | 
					 | 
				
			||||||
      stream: options.config.stream,
 | 
					 | 
				
			||||||
      model: modelConfig.model,
 | 
					 | 
				
			||||||
      temperature: modelConfig.temperature,
 | 
					 | 
				
			||||||
      presence_penalty: modelConfig.presence_penalty,
 | 
					 | 
				
			||||||
      frequency_penalty: modelConfig.frequency_penalty,
 | 
					 | 
				
			||||||
      top_p: modelConfig.top_p,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("[Request] xai payload: ", requestPayload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    options.onController?.(controller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const chatPath = this.path(XAI.ChatPath);
 | 
					 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        getTimeoutMSByModel(options.config.model),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					 | 
				
			||||||
        const [tools, funcs] = usePluginStore
 | 
					 | 
				
			||||||
          .getState()
 | 
					 | 
				
			||||||
          .getAsTools(
 | 
					 | 
				
			||||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        return stream(
 | 
					 | 
				
			||||||
          chatPath,
 | 
					 | 
				
			||||||
          requestPayload,
 | 
					 | 
				
			||||||
          getHeaders(),
 | 
					 | 
				
			||||||
          tools as any,
 | 
					 | 
				
			||||||
          funcs,
 | 
					 | 
				
			||||||
          controller,
 | 
					 | 
				
			||||||
          // parseSSE
 | 
					 | 
				
			||||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
					 | 
				
			||||||
            // console.log("parseSSE", text, runTools);
 | 
					 | 
				
			||||||
            const json = JSON.parse(text);
 | 
					 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
              delta: {
 | 
					 | 
				
			||||||
                content: string;
 | 
					 | 
				
			||||||
                tool_calls: ChatMessageTool[];
 | 
					 | 
				
			||||||
              };
 | 
					 | 
				
			||||||
            }>;
 | 
					 | 
				
			||||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
					 | 
				
			||||||
            if (tool_calls?.length > 0) {
 | 
					 | 
				
			||||||
              const index = tool_calls[0]?.index;
 | 
					 | 
				
			||||||
              const id = tool_calls[0]?.id;
 | 
					 | 
				
			||||||
              const args = tool_calls[0]?.function?.arguments;
 | 
					 | 
				
			||||||
              if (id) {
 | 
					 | 
				
			||||||
                runTools.push({
 | 
					 | 
				
			||||||
                  id,
 | 
					 | 
				
			||||||
                  type: tool_calls[0]?.type,
 | 
					 | 
				
			||||||
                  function: {
 | 
					 | 
				
			||||||
                    name: tool_calls[0]?.function?.name as string,
 | 
					 | 
				
			||||||
                    arguments: args,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // @ts-ignore
 | 
					 | 
				
			||||||
                runTools[index]["function"]["arguments"] += args;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          // processToolMessage, include tool_calls message and tool call results
 | 
					 | 
				
			||||||
          (
 | 
					 | 
				
			||||||
            requestPayload: RequestPayload,
 | 
					 | 
				
			||||||
            toolCallMessage: any,
 | 
					 | 
				
			||||||
            toolCallResult: any[],
 | 
					 | 
				
			||||||
          ) => {
 | 
					 | 
				
			||||||
            // @ts-ignore
 | 
					 | 
				
			||||||
            requestPayload?.messages?.splice(
 | 
					 | 
				
			||||||
              // @ts-ignore
 | 
					 | 
				
			||||||
              requestPayload?.messages?.length,
 | 
					 | 
				
			||||||
              0,
 | 
					 | 
				
			||||||
              toolCallMessage,
 | 
					 | 
				
			||||||
              ...toolCallResult,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const resJson = await res.json();
 | 
					 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					 | 
				
			||||||
        options.onFinish(message, res);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  async usage() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      used: 0,
 | 
					 | 
				
			||||||
      total: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async models(): Promise<LLMModel[]> {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -38,20 +38,16 @@ interface ChatCommands {
 | 
				
			|||||||
  next?: Command;
 | 
					  next?: Command;
 | 
				
			||||||
  prev?: Command;
 | 
					  prev?: Command;
 | 
				
			||||||
  clear?: Command;
 | 
					  clear?: Command;
 | 
				
			||||||
  fork?: Command;
 | 
					 | 
				
			||||||
  del?: Command;
 | 
					  del?: Command;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Compatible with Chinese colon character ":"
 | 
					export const ChatCommandPrefix = ":";
 | 
				
			||||||
export const ChatCommandPrefix = /^[::]/;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useChatCommand(commands: ChatCommands = {}) {
 | 
					export function useChatCommand(commands: ChatCommands = {}) {
 | 
				
			||||||
  function extract(userInput: string) {
 | 
					  function extract(userInput: string) {
 | 
				
			||||||
    const match = userInput.match(ChatCommandPrefix);
 | 
					    return (
 | 
				
			||||||
    if (match) {
 | 
					      userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
 | 
				
			||||||
      return userInput.slice(1) as keyof ChatCommands;
 | 
					    ) as keyof ChatCommands;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return userInput as keyof ChatCommands;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function search(userInput: string) {
 | 
					  function search(userInput: string) {
 | 
				
			||||||
@@ -61,7 +57,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
 | 
				
			|||||||
      .filter((c) => c.startsWith(input))
 | 
					      .filter((c) => c.startsWith(input))
 | 
				
			||||||
      .map((c) => ({
 | 
					      .map((c) => ({
 | 
				
			||||||
        title: desc[c as keyof ChatCommands],
 | 
					        title: desc[c as keyof ChatCommands],
 | 
				
			||||||
        content: ":" + c,
 | 
					        content: ChatCommandPrefix + c,
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,31 +0,0 @@
 | 
				
			|||||||
.artifacts {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  &-header {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    height: 36px;
 | 
					 | 
				
			||||||
    padding: 20px;
 | 
					 | 
				
			||||||
    background: var(--second);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  &-title {
 | 
					 | 
				
			||||||
    flex: 1;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    font-size: 24px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  &-content {
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
    padding: 0 20px 20px 20px;
 | 
					 | 
				
			||||||
    background-color: var(--second);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.artifacts-iframe {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  border-radius: 6px;
 | 
					 | 
				
			||||||
  background-color: var(--gray);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,266 +0,0 @@
 | 
				
			|||||||
import {
 | 
					 | 
				
			||||||
  useEffect,
 | 
					 | 
				
			||||||
  useState,
 | 
					 | 
				
			||||||
  useRef,
 | 
					 | 
				
			||||||
  useMemo,
 | 
					 | 
				
			||||||
  forwardRef,
 | 
					 | 
				
			||||||
  useImperativeHandle,
 | 
					 | 
				
			||||||
} from "react";
 | 
					 | 
				
			||||||
import { useParams } from "react-router";
 | 
					 | 
				
			||||||
import { IconButton } from "./button";
 | 
					 | 
				
			||||||
import { nanoid } from "nanoid";
 | 
					 | 
				
			||||||
import ExportIcon from "../icons/share.svg";
 | 
					 | 
				
			||||||
import CopyIcon from "../icons/copy.svg";
 | 
					 | 
				
			||||||
import DownloadIcon from "../icons/download.svg";
 | 
					 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					 | 
				
			||||||
import LoadingButtonIcon from "../icons/loading.svg";
 | 
					 | 
				
			||||||
import ReloadButtonIcon from "../icons/reload.svg";
 | 
					 | 
				
			||||||
import Locale from "../locales";
 | 
					 | 
				
			||||||
import { Modal, showToast } from "./ui-lib";
 | 
					 | 
				
			||||||
import { copyToClipboard, downloadAs } from "../utils";
 | 
					 | 
				
			||||||
import { Path, ApiPath, REPO_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
import { Loading } from "./home";
 | 
					 | 
				
			||||||
import styles from "./artifacts.module.scss";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type HTMLPreviewProps = {
 | 
					 | 
				
			||||||
  code: string;
 | 
					 | 
				
			||||||
  autoHeight?: boolean;
 | 
					 | 
				
			||||||
  height?: number | string;
 | 
					 | 
				
			||||||
  onLoad?: (title?: string) => void;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type HTMLPreviewHander = {
 | 
					 | 
				
			||||||
  reload: () => void;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
 | 
					 | 
				
			||||||
  function HTMLPreview(props, ref) {
 | 
					 | 
				
			||||||
    const iframeRef = useRef<HTMLIFrameElement>(null);
 | 
					 | 
				
			||||||
    const [frameId, setFrameId] = useState<string>(nanoid());
 | 
					 | 
				
			||||||
    const [iframeHeight, setIframeHeight] = useState(600);
 | 
					 | 
				
			||||||
    const [title, setTitle] = useState("");
 | 
					 | 
				
			||||||
    /*
 | 
					 | 
				
			||||||
     * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
 | 
					 | 
				
			||||||
     * 1. using srcdoc
 | 
					 | 
				
			||||||
     * 2. using src with dataurl:
 | 
					 | 
				
			||||||
     *    easy to share
 | 
					 | 
				
			||||||
     *    length limit (Data URIs cannot be larger than 32,768 characters.)
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(() => {
 | 
					 | 
				
			||||||
      const handleMessage = (e: any) => {
 | 
					 | 
				
			||||||
        const { id, height, title } = e.data;
 | 
					 | 
				
			||||||
        setTitle(title);
 | 
					 | 
				
			||||||
        if (id == frameId) {
 | 
					 | 
				
			||||||
          setIframeHeight(height);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      window.addEventListener("message", handleMessage);
 | 
					 | 
				
			||||||
      return () => {
 | 
					 | 
				
			||||||
        window.removeEventListener("message", handleMessage);
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }, [frameId]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useImperativeHandle(ref, () => ({
 | 
					 | 
				
			||||||
      reload: () => {
 | 
					 | 
				
			||||||
        setFrameId(nanoid());
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const height = useMemo(() => {
 | 
					 | 
				
			||||||
      if (!props.autoHeight) return props.height || 600;
 | 
					 | 
				
			||||||
      if (typeof props.height === "string") {
 | 
					 | 
				
			||||||
        return props.height;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const parentHeight = props.height || 600;
 | 
					 | 
				
			||||||
      return iframeHeight + 40 > parentHeight
 | 
					 | 
				
			||||||
        ? parentHeight
 | 
					 | 
				
			||||||
        : iframeHeight + 40;
 | 
					 | 
				
			||||||
    }, [props.autoHeight, props.height, iframeHeight]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const srcDoc = useMemo(() => {
 | 
					 | 
				
			||||||
      const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
 | 
					 | 
				
			||||||
      if (props.code.includes("<!DOCTYPE html>")) {
 | 
					 | 
				
			||||||
        props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return script + props.code;
 | 
					 | 
				
			||||||
    }, [props.code, frameId]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleOnLoad = () => {
 | 
					 | 
				
			||||||
      if (props?.onLoad) {
 | 
					 | 
				
			||||||
        props.onLoad(title);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <iframe
 | 
					 | 
				
			||||||
        className={styles["artifacts-iframe"]}
 | 
					 | 
				
			||||||
        key={frameId}
 | 
					 | 
				
			||||||
        ref={iframeRef}
 | 
					 | 
				
			||||||
        sandbox="allow-forms allow-modals allow-scripts"
 | 
					 | 
				
			||||||
        style={{ height }}
 | 
					 | 
				
			||||||
        srcDoc={srcDoc}
 | 
					 | 
				
			||||||
        onLoad={handleOnLoad}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ArtifactsShareButton({
 | 
					 | 
				
			||||||
  getCode,
 | 
					 | 
				
			||||||
  id,
 | 
					 | 
				
			||||||
  style,
 | 
					 | 
				
			||||||
  fileName,
 | 
					 | 
				
			||||||
}: {
 | 
					 | 
				
			||||||
  getCode: () => string;
 | 
					 | 
				
			||||||
  id?: string;
 | 
					 | 
				
			||||||
  style?: any;
 | 
					 | 
				
			||||||
  fileName?: string;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  const [loading, setLoading] = useState(false);
 | 
					 | 
				
			||||||
  const [name, setName] = useState(id);
 | 
					 | 
				
			||||||
  const [show, setShow] = useState(false);
 | 
					 | 
				
			||||||
  const shareUrl = useMemo(
 | 
					 | 
				
			||||||
    () => [location.origin, "#", Path.Artifacts, "/", name].join(""),
 | 
					 | 
				
			||||||
    [name],
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const upload = (code: string) =>
 | 
					 | 
				
			||||||
    id
 | 
					 | 
				
			||||||
      ? Promise.resolve({ id })
 | 
					 | 
				
			||||||
      : fetch(ApiPath.Artifacts, {
 | 
					 | 
				
			||||||
          method: "POST",
 | 
					 | 
				
			||||||
          body: code,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
          .then((res) => res.json())
 | 
					 | 
				
			||||||
          .then(({ id }) => {
 | 
					 | 
				
			||||||
            if (id) {
 | 
					 | 
				
			||||||
              return { id };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            throw Error();
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
          .catch((e) => {
 | 
					 | 
				
			||||||
            showToast(Locale.Export.Artifacts.Error);
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <div className="window-action-button" style={style}>
 | 
					 | 
				
			||||||
        <IconButton
 | 
					 | 
				
			||||||
          icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
 | 
					 | 
				
			||||||
          bordered
 | 
					 | 
				
			||||||
          title={Locale.Export.Artifacts.Title}
 | 
					 | 
				
			||||||
          onClick={() => {
 | 
					 | 
				
			||||||
            if (loading) return;
 | 
					 | 
				
			||||||
            setLoading(true);
 | 
					 | 
				
			||||||
            upload(getCode())
 | 
					 | 
				
			||||||
              .then((res) => {
 | 
					 | 
				
			||||||
                if (res?.id) {
 | 
					 | 
				
			||||||
                  setShow(true);
 | 
					 | 
				
			||||||
                  setName(res?.id);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              })
 | 
					 | 
				
			||||||
              .finally(() => setLoading(false));
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {show && (
 | 
					 | 
				
			||||||
        <div className="modal-mask">
 | 
					 | 
				
			||||||
          <Modal
 | 
					 | 
				
			||||||
            title={Locale.Export.Artifacts.Title}
 | 
					 | 
				
			||||||
            onClose={() => setShow(false)}
 | 
					 | 
				
			||||||
            actions={[
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                key="download"
 | 
					 | 
				
			||||||
                icon={<DownloadIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                text={Locale.Export.Download}
 | 
					 | 
				
			||||||
                onClick={() => {
 | 
					 | 
				
			||||||
                  downloadAs(getCode(), `${fileName || name}.html`).then(() =>
 | 
					 | 
				
			||||||
                    setShow(false),
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />,
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                key="copy"
 | 
					 | 
				
			||||||
                icon={<CopyIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                text={Locale.Chat.Actions.Copy}
 | 
					 | 
				
			||||||
                onClick={() => {
 | 
					 | 
				
			||||||
                  copyToClipboard(shareUrl).then(() => setShow(false));
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />,
 | 
					 | 
				
			||||||
            ]}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
              <a target="_blank" href={shareUrl}>
 | 
					 | 
				
			||||||
                {shareUrl}
 | 
					 | 
				
			||||||
              </a>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </Modal>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function Artifacts() {
 | 
					 | 
				
			||||||
  const { id } = useParams();
 | 
					 | 
				
			||||||
  const [code, setCode] = useState("");
 | 
					 | 
				
			||||||
  const [loading, setLoading] = useState(true);
 | 
					 | 
				
			||||||
  const [fileName, setFileName] = useState("");
 | 
					 | 
				
			||||||
  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (id) {
 | 
					 | 
				
			||||||
      fetch(`${ApiPath.Artifacts}?id=${id}`)
 | 
					 | 
				
			||||||
        .then((res) => {
 | 
					 | 
				
			||||||
          if (res.status > 300) {
 | 
					 | 
				
			||||||
            throw Error("can not get content");
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return res;
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .then((res) => res.text())
 | 
					 | 
				
			||||||
        .then(setCode)
 | 
					 | 
				
			||||||
        .catch((e) => {
 | 
					 | 
				
			||||||
          showToast(Locale.Export.Artifacts.Error);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [id]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className={styles["artifacts"]}>
 | 
					 | 
				
			||||||
      <div className={styles["artifacts-header"]}>
 | 
					 | 
				
			||||||
        <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
					 | 
				
			||||||
          <IconButton bordered icon={<GithubIcon />} shadow />
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
        <IconButton
 | 
					 | 
				
			||||||
          bordered
 | 
					 | 
				
			||||||
          style={{ marginLeft: 20 }}
 | 
					 | 
				
			||||||
          icon={<ReloadButtonIcon />}
 | 
					 | 
				
			||||||
          shadow
 | 
					 | 
				
			||||||
          onClick={() => previewRef.current?.reload()}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <div className={styles["artifacts-title"]}>NextChat Artifacts</div>
 | 
					 | 
				
			||||||
        <ArtifactsShareButton
 | 
					 | 
				
			||||||
          id={id}
 | 
					 | 
				
			||||||
          getCode={() => code}
 | 
					 | 
				
			||||||
          fileName={fileName}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div className={styles["artifacts-content"]}>
 | 
					 | 
				
			||||||
        {loading && <Loading />}
 | 
					 | 
				
			||||||
        {code && (
 | 
					 | 
				
			||||||
          <HTMLPreview
 | 
					 | 
				
			||||||
            code={code}
 | 
					 | 
				
			||||||
            ref={previewRef}
 | 
					 | 
				
			||||||
            autoHeight={false}
 | 
					 | 
				
			||||||
            height={"100%"}
 | 
					 | 
				
			||||||
            onLoad={(title) => {
 | 
					 | 
				
			||||||
              setFileName(title as string);
 | 
					 | 
				
			||||||
              setLoading(false);
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,70 +1,12 @@
 | 
				
			|||||||
.auth-page {
 | 
					.auth-page {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: flex-start;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  .top-banner {
 | 
					 | 
				
			||||||
    position: relative;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    padding: 12px 64px;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
    background: var(--second);
 | 
					 | 
				
			||||||
    .top-banner-inner {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      line-height: 150%;
 | 
					 | 
				
			||||||
      span {
 | 
					 | 
				
			||||||
        gap: 8px;
 | 
					 | 
				
			||||||
        a {
 | 
					 | 
				
			||||||
          display: inline-flex;
 | 
					 | 
				
			||||||
          align-items: center;
 | 
					 | 
				
			||||||
          text-decoration: none;
 | 
					 | 
				
			||||||
          margin-left: 8px;
 | 
					 | 
				
			||||||
          color: var(--primary);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .top-banner-close {
 | 
					 | 
				
			||||||
      cursor: pointer;
 | 
					 | 
				
			||||||
      position: absolute;
 | 
					 | 
				
			||||||
      top: 50%;
 | 
					 | 
				
			||||||
      right: 48px;
 | 
					 | 
				
			||||||
      transform: translateY(-50%);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (max-width: 600px) {
 | 
					 | 
				
			||||||
    .top-banner {
 | 
					 | 
				
			||||||
      padding: 12px 24px 12px 12px;
 | 
					 | 
				
			||||||
      .top-banner-close {
 | 
					 | 
				
			||||||
        right: 10px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .top-banner-inner {
 | 
					 | 
				
			||||||
        .top-banner-logo {
 | 
					 | 
				
			||||||
          margin-right: 8px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .auth-header {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: space-between;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
    animation: slide-in-from-top ease 0.3s;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-logo {
 | 
					  .auth-logo {
 | 
				
			||||||
    margin-top: 10vh;
 | 
					 | 
				
			||||||
    transform: scale(1.4);
 | 
					    transform: scale(1.4);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,7 +14,6 @@
 | 
				
			|||||||
    font-size: 24px;
 | 
					    font-size: 24px;
 | 
				
			||||||
    font-weight: bold;
 | 
					    font-weight: bold;
 | 
				
			||||||
    line-height: 2;
 | 
					    line-height: 2;
 | 
				
			||||||
    margin-bottom: 1vh;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-tips {
 | 
					  .auth-tips {
 | 
				
			||||||
@@ -83,10 +24,6 @@
 | 
				
			|||||||
    margin: 3vh 0;
 | 
					    margin: 3vh 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-input-second {
 | 
					 | 
				
			||||||
    margin: 0 0 3vh 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .auth-actions {
 | 
					  .auth-actions {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +1,21 @@
 | 
				
			|||||||
import styles from "./auth.module.scss";
 | 
					import styles from "./auth.module.scss";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import { useState, useEffect } from "react";
 | 
					
 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
import { Path, SAAS_CHAT_URL } from "../constant";
 | 
					import { Path } from "../constant";
 | 
				
			||||||
import { useAccessStore } from "../store";
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import Delete from "../icons/close.svg";
 | 
					 | 
				
			||||||
import Arrow from "../icons/arrow.svg";
 | 
					 | 
				
			||||||
import Logo from "../icons/logo.svg";
 | 
					 | 
				
			||||||
import { useMobileScreen } from "@/app/utils";
 | 
					 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					 | 
				
			||||||
import { getClientConfig } from "../config/client";
 | 
					 | 
				
			||||||
import { PasswordInput } from "./ui-lib";
 | 
					 | 
				
			||||||
import LeftIcon from "@/app/icons/left.svg";
 | 
					 | 
				
			||||||
import { safeLocalStorage } from "@/app/utils";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  trackSettingsPageGuideToCPaymentClick,
 | 
					 | 
				
			||||||
  trackAuthorizationPageButtonToCPaymentClick,
 | 
					 | 
				
			||||||
} from "../utils/auth-settings-events";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const storage = safeLocalStorage();
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AuthPage() {
 | 
					export function AuthPage() {
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const accessStore = useAccessStore();
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const goHome = () => navigate(Path.Home);
 | 
					  const goHome = () => navigate(Path.Home);
 | 
				
			||||||
  const goChat = () => navigate(Path.Chat);
 | 
					  const goChat = () => navigate(Path.Chat);
 | 
				
			||||||
  const goSaas = () => {
 | 
					 | 
				
			||||||
    trackAuthorizationPageButtonToCPaymentClick();
 | 
					 | 
				
			||||||
    window.location.href = SAAS_CHAT_URL;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const resetAccessCode = () => {
 | 
					  const resetAccessCode = () => {
 | 
				
			||||||
    accessStore.update((access) => {
 | 
					    accessStore.update((access) => {
 | 
				
			||||||
      access.openaiApiKey = "";
 | 
					      access.openaiApiKey = "";
 | 
				
			||||||
@@ -48,58 +32,43 @@ export function AuthPage() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["auth-page"]}>
 | 
					    <div className={styles["auth-page"]}>
 | 
				
			||||||
      <TopBanner></TopBanner>
 | 
					      <div className={`no-dark ${styles["auth-logo"]}`}>
 | 
				
			||||||
      <div className={styles["auth-header"]}>
 | 
					 | 
				
			||||||
        <IconButton
 | 
					 | 
				
			||||||
          icon={<LeftIcon />}
 | 
					 | 
				
			||||||
          text={Locale.Auth.Return}
 | 
					 | 
				
			||||||
          onClick={() => navigate(Path.Home)}
 | 
					 | 
				
			||||||
        ></IconButton>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div className={clsx("no-dark", styles["auth-logo"])}>
 | 
					 | 
				
			||||||
        <BotIcon />
 | 
					        <BotIcon />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
					      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
				
			||||||
      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
					      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <PasswordInput
 | 
					      <input
 | 
				
			||||||
        style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
					        className={styles["auth-input"]}
 | 
				
			||||||
        aria={Locale.Settings.ShowPassword}
 | 
					        type="password"
 | 
				
			||||||
        aria-label={Locale.Auth.Input}
 | 
					 | 
				
			||||||
        value={accessStore.accessCode}
 | 
					 | 
				
			||||||
        type="text"
 | 
					 | 
				
			||||||
        placeholder={Locale.Auth.Input}
 | 
					        placeholder={Locale.Auth.Input}
 | 
				
			||||||
 | 
					        value={accessStore.accessCode}
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          accessStore.update(
 | 
					          accessStore.update(
 | 
				
			||||||
            (access) => (access.accessCode = e.currentTarget.value),
 | 
					            (access) => (access.accessCode = e.currentTarget.value),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					 | 
				
			||||||
      {!accessStore.hideUserApiKey ? (
 | 
					      {!accessStore.hideUserApiKey ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
					          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
				
			||||||
          <PasswordInput
 | 
					          <input
 | 
				
			||||||
            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
					            className={styles["auth-input"]}
 | 
				
			||||||
            aria={Locale.Settings.ShowPassword}
 | 
					            type="password"
 | 
				
			||||||
            aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
					 | 
				
			||||||
            value={accessStore.openaiApiKey}
 | 
					 | 
				
			||||||
            type="text"
 | 
					 | 
				
			||||||
            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
					            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            value={accessStore.openaiApiKey}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
              accessStore.update(
 | 
					              accessStore.update(
 | 
				
			||||||
                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
					                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <PasswordInput
 | 
					          <input
 | 
				
			||||||
            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
					            className={styles["auth-input"]}
 | 
				
			||||||
            aria={Locale.Settings.ShowPassword}
 | 
					            type="password"
 | 
				
			||||||
            aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
					 | 
				
			||||||
            value={accessStore.googleApiKey}
 | 
					 | 
				
			||||||
            type="text"
 | 
					 | 
				
			||||||
            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
					            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            value={accessStore.googleApiKey}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
              accessStore.update(
 | 
					              accessStore.update(
 | 
				
			||||||
                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
					                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
				
			||||||
@@ -116,74 +85,13 @@ export function AuthPage() {
 | 
				
			|||||||
          onClick={goChat}
 | 
					          onClick={goChat}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          text={Locale.Auth.SaasTips}
 | 
					          text={Locale.Auth.Later}
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            goSaas();
 | 
					            resetAccessCode();
 | 
				
			||||||
 | 
					            goHome();
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
function TopBanner() {
 | 
					 | 
				
			||||||
  const [isHovered, setIsHovered] = useState(false);
 | 
					 | 
				
			||||||
  const [isVisible, setIsVisible] = useState(true);
 | 
					 | 
				
			||||||
  const isMobile = useMobileScreen();
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    // 检查 localStorage 中是否有标记
 | 
					 | 
				
			||||||
    const bannerDismissed = storage.getItem("bannerDismissed");
 | 
					 | 
				
			||||||
    // 如果标记不存在,存储默认值并显示横幅
 | 
					 | 
				
			||||||
    if (!bannerDismissed) {
 | 
					 | 
				
			||||||
      storage.setItem("bannerDismissed", "false");
 | 
					 | 
				
			||||||
      setIsVisible(true); // 显示横幅
 | 
					 | 
				
			||||||
    } else if (bannerDismissed === "true") {
 | 
					 | 
				
			||||||
      // 如果标记为 "true",则隐藏横幅
 | 
					 | 
				
			||||||
      setIsVisible(false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleMouseEnter = () => {
 | 
					 | 
				
			||||||
    setIsHovered(true);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleMouseLeave = () => {
 | 
					 | 
				
			||||||
    setIsHovered(false);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleClose = () => {
 | 
					 | 
				
			||||||
    setIsVisible(false);
 | 
					 | 
				
			||||||
    storage.setItem("bannerDismissed", "true");
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!isVisible) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      className={styles["top-banner"]}
 | 
					 | 
				
			||||||
      onMouseEnter={handleMouseEnter}
 | 
					 | 
				
			||||||
      onMouseLeave={handleMouseLeave}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <div className={clsx(styles["top-banner-inner"], "no-dark")}>
 | 
					 | 
				
			||||||
        <Logo className={styles["top-banner-logo"]}></Logo>
 | 
					 | 
				
			||||||
        <span>
 | 
					 | 
				
			||||||
          {Locale.Auth.TopTips}
 | 
					 | 
				
			||||||
          <a
 | 
					 | 
				
			||||||
            href={SAAS_CHAT_URL}
 | 
					 | 
				
			||||||
            rel="stylesheet"
 | 
					 | 
				
			||||||
            onClick={() => {
 | 
					 | 
				
			||||||
              trackSettingsPageGuideToCPaymentClick();
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {Locale.Settings.Access.SaasStart.ChatNow}
 | 
					 | 
				
			||||||
            <Arrow style={{ marginLeft: "4px" }} />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {(isHovered || isMobile) && (
 | 
					 | 
				
			||||||
        <Delete className={styles["top-banner-close"]} onClick={handleClose} />
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./button.module.scss";
 | 
					import styles from "./button.module.scss";
 | 
				
			||||||
import { CSSProperties } from "react";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ButtonType = "primary" | "danger" | null;
 | 
					export type ButtonType = "primary" | "danger" | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,48 +16,35 @@ export function IconButton(props: {
 | 
				
			|||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
  tabIndex?: number;
 | 
					  tabIndex?: number;
 | 
				
			||||||
  autoFocus?: boolean;
 | 
					  autoFocus?: boolean;
 | 
				
			||||||
  style?: CSSProperties;
 | 
					 | 
				
			||||||
  aria?: string;
 | 
					 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      className={clsx(
 | 
					      className={
 | 
				
			||||||
        "clickable",
 | 
					        styles["icon-button"] +
 | 
				
			||||||
        styles["icon-button"],
 | 
					        ` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
 | 
				
			||||||
        {
 | 
					          props.className ?? ""
 | 
				
			||||||
          [styles.border]: props.bordered,
 | 
					        } clickable ${styles[props.type ?? ""]}`
 | 
				
			||||||
          [styles.shadow]: props.shadow,
 | 
					      }
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        styles[props.type ?? ""],
 | 
					 | 
				
			||||||
        props.className,
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      onClick={props.onClick}
 | 
					      onClick={props.onClick}
 | 
				
			||||||
      title={props.title}
 | 
					      title={props.title}
 | 
				
			||||||
      disabled={props.disabled}
 | 
					      disabled={props.disabled}
 | 
				
			||||||
      role="button"
 | 
					      role="button"
 | 
				
			||||||
      tabIndex={props.tabIndex}
 | 
					      tabIndex={props.tabIndex}
 | 
				
			||||||
      autoFocus={props.autoFocus}
 | 
					      autoFocus={props.autoFocus}
 | 
				
			||||||
      style={props.style}
 | 
					 | 
				
			||||||
      aria-label={props.aria}
 | 
					 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {props.icon && (
 | 
					      {props.icon && (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          aria-label={props.text || props.title}
 | 
					          className={
 | 
				
			||||||
          className={clsx(styles["icon-button-icon"], {
 | 
					            styles["icon-button-icon"] +
 | 
				
			||||||
            "no-dark": props.type === "primary",
 | 
					            ` ${props.type === "primary" && "no-dark"}`
 | 
				
			||||||
          })}
 | 
					          }
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.icon}
 | 
					          {props.icon}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {props.text && (
 | 
					      {props.text && (
 | 
				
			||||||
        <div
 | 
					        <div className={styles["icon-button-text"]}>{props.text}</div>
 | 
				
			||||||
          aria-label={props.text || props.title}
 | 
					 | 
				
			||||||
          className={styles["icon-button-text"]}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {props.text}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -11,14 +12,13 @@ import {
 | 
				
			|||||||
import { useChatStore } from "../store";
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { useLocation, useNavigate } from "react-router-dom";
 | 
					import { Link, useNavigate } from "react-router-dom";
 | 
				
			||||||
import { Path } from "../constant";
 | 
					import { Path } from "../constant";
 | 
				
			||||||
import { MaskAvatar } from "./mask";
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
import { Mask } from "../store/mask";
 | 
					import { Mask } from "../store/mask";
 | 
				
			||||||
import { useRef, useEffect } from "react";
 | 
					import { useRef, useEffect } from "react";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { useMobileScreen } from "../utils";
 | 
					import { useMobileScreen } from "../utils";
 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ChatItem(props: {
 | 
					export function ChatItem(props: {
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
@@ -40,17 +40,13 @@ export function ChatItem(props: {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [props.selected]);
 | 
					  }, [props.selected]);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { pathname: currentPath } = useLocation();
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
					    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
				
			||||||
      {(provided) => (
 | 
					      {(provided) => (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={clsx(styles["chat-item"], {
 | 
					          className={`${styles["chat-item"]} ${
 | 
				
			||||||
            [styles["chat-item-selected"]]:
 | 
					            props.selected && styles["chat-item-selected"]
 | 
				
			||||||
              props.selected &&
 | 
					          }`}
 | 
				
			||||||
              (currentPath === Path.Chat || currentPath === Path.Home),
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
          onClick={props.onClick}
 | 
					          onClick={props.onClick}
 | 
				
			||||||
          ref={(ele) => {
 | 
					          ref={(ele) => {
 | 
				
			||||||
            draggableRef.current = ele;
 | 
					            draggableRef.current = ele;
 | 
				
			||||||
@@ -64,7 +60,7 @@ export function ChatItem(props: {
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.narrow ? (
 | 
					          {props.narrow ? (
 | 
				
			||||||
            <div className={styles["chat-item-narrow"]}>
 | 
					            <div className={styles["chat-item-narrow"]}>
 | 
				
			||||||
              <div className={clsx(styles["chat-item-avatar"], "no-dark")}>
 | 
					              <div className={styles["chat-item-avatar"] + " no-dark"}>
 | 
				
			||||||
                <MaskAvatar
 | 
					                <MaskAvatar
 | 
				
			||||||
                  avatar={props.mask.avatar}
 | 
					                  avatar={props.mask.avatar}
 | 
				
			||||||
                  model={props.mask.modelConfig.model}
 | 
					                  model={props.mask.modelConfig.model}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,58 +1,8 @@
 | 
				
			|||||||
@import "../styles/animation.scss";
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attach-images {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  left: 30px;
 | 
					 | 
				
			||||||
  bottom: 32px;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.attach-image {
 | 
					 | 
				
			||||||
  cursor: default;
 | 
					 | 
				
			||||||
  width: 64px;
 | 
					 | 
				
			||||||
  height: 64px;
 | 
					 | 
				
			||||||
  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
					 | 
				
			||||||
  border-radius: 5px;
 | 
					 | 
				
			||||||
  margin-right: 10px;
 | 
					 | 
				
			||||||
  background-size: cover;
 | 
					 | 
				
			||||||
  background-position: center;
 | 
					 | 
				
			||||||
  background-color: var(--white);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .attach-image-mask {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    opacity: 0;
 | 
					 | 
				
			||||||
    transition: all ease 0.2s;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .attach-image-mask:hover {
 | 
					 | 
				
			||||||
    opacity: 1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .delete-image {
 | 
					 | 
				
			||||||
    width: 24px;
 | 
					 | 
				
			||||||
    height: 24px;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    border-radius: 5px;
 | 
					 | 
				
			||||||
    float: right;
 | 
					 | 
				
			||||||
    background-color: var(--white);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-actions {
 | 
					.chat-input-actions {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap;
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  gap: 5px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &-end {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    margin-left: auto;
 | 
					 | 
				
			||||||
    gap: 5px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-input-action {
 | 
					  .chat-input-action {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
@@ -70,6 +20,10 @@
 | 
				
			|||||||
    width: var(--icon-width);
 | 
					    width: var(--icon-width);
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      margin-right: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .text {
 | 
					    .text {
 | 
				
			||||||
      white-space: nowrap;
 | 
					      white-space: nowrap;
 | 
				
			||||||
      padding-left: 5px;
 | 
					      padding-left: 5px;
 | 
				
			||||||
@@ -352,12 +306,6 @@
 | 
				
			|||||||
      flex-wrap: nowrap;
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-model-name {
 | 
					 | 
				
			||||||
    font-size: 12px;
 | 
					 | 
				
			||||||
    color: var(--black);
 | 
					 | 
				
			||||||
    margin-left: 6px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-container {
 | 
					.chat-message-container {
 | 
				
			||||||
@@ -401,7 +349,6 @@
 | 
				
			|||||||
      padding: 7px;
 | 
					      padding: 7px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* Specific styles for iOS devices */
 | 
					  /* Specific styles for iOS devices */
 | 
				
			||||||
  @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
 | 
					  @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
 | 
				
			||||||
    @supports (-webkit-touch-callout: none) {
 | 
					    @supports (-webkit-touch-callout: none) {
 | 
				
			||||||
@@ -419,21 +366,6 @@
 | 
				
			|||||||
  margin-top: 5px;
 | 
					  margin-top: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-tools {
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  color: #aaa;
 | 
					 | 
				
			||||||
  line-height: 1.5;
 | 
					 | 
				
			||||||
  margin-top: 5px;
 | 
					 | 
				
			||||||
  .chat-message-tool {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: end;
 | 
					 | 
				
			||||||
    svg {
 | 
					 | 
				
			||||||
      margin-left: 5px;
 | 
					 | 
				
			||||||
      margin-right: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item {
 | 
					.chat-message-item {
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  max-width: 100%;
 | 
					  max-width: 100%;
 | 
				
			||||||
@@ -449,87 +381,6 @@
 | 
				
			|||||||
  transition: all ease 0.3s;
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-audio {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  background-color: rgba(0, 0, 0, 0.05);
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  transition: all ease 0.3s;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  user-select: text;
 | 
					 | 
				
			||||||
  word-break: break-word;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  audio {
 | 
					 | 
				
			||||||
    height: 30px; /* 调整高度 */
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item-image {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item-images {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  display: grid;
 | 
					 | 
				
			||||||
  justify-content: left;
 | 
					 | 
				
			||||||
  grid-gap: 10px;
 | 
					 | 
				
			||||||
  grid-template-columns: repeat(var(--image-count), auto);
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item-image-multi {
 | 
					 | 
				
			||||||
  object-fit: cover;
 | 
					 | 
				
			||||||
  background-size: cover;
 | 
					 | 
				
			||||||
  background-position: center;
 | 
					 | 
				
			||||||
  background-repeat: no-repeat;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item-image,
 | 
					 | 
				
			||||||
.chat-message-item-image-multi {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
  $calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-message-item-image-multi {
 | 
					 | 
				
			||||||
    width: $calc-image-width;
 | 
					 | 
				
			||||||
    height: $calc-image-width;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-message-item-image {
 | 
					 | 
				
			||||||
    max-width: calc(100vw / 3 * 2);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media screen and (min-width: 600px) {
 | 
					 | 
				
			||||||
  $max-image-width: calc(
 | 
					 | 
				
			||||||
    calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  $image-width: calc(
 | 
					 | 
				
			||||||
    calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
 | 
					 | 
				
			||||||
      var(--image-count)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-message-item-image-multi {
 | 
					 | 
				
			||||||
    width: $image-width;
 | 
					 | 
				
			||||||
    height: $image-width;
 | 
					 | 
				
			||||||
    max-width: $max-image-width;
 | 
					 | 
				
			||||||
    max-height: $max-image-width;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-message-item-image {
 | 
					 | 
				
			||||||
    max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-action-date {
 | 
					.chat-message-action-date {
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  opacity: 0.2;
 | 
					  opacity: 0.2;
 | 
				
			||||||
@@ -609,7 +460,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      @include single-line();
 | 
					      @include single-line();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    .hint-content {
 | 
					    .hint-content {
 | 
				
			||||||
      font-size: 12px;
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -624,26 +474,15 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input-panel-inner {
 | 
					.chat-input-panel-inner {
 | 
				
			||||||
  cursor: text;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex: 1;
 | 
					  flex: 1;
 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-panel-inner-attach {
 | 
					 | 
				
			||||||
  padding-bottom: 80px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-panel-inner:has(.chat-input:focus) {
 | 
					 | 
				
			||||||
  border: 1px solid var(--primary);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input {
 | 
					.chat-input {
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  border-radius: 10px;
 | 
					  border-radius: 10px;
 | 
				
			||||||
  border: none;
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
 | 
					  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
 | 
				
			||||||
  background-color: var(--white);
 | 
					  background-color: var(--white);
 | 
				
			||||||
  color: var(--black);
 | 
					  color: var(--black);
 | 
				
			||||||
@@ -656,6 +495,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input:focus {
 | 
					.chat-input:focus {
 | 
				
			||||||
 | 
					  border: 1px solid var(--primary);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input-send {
 | 
					.chat-input-send {
 | 
				
			||||||
@@ -676,78 +516,3 @@
 | 
				
			|||||||
    bottom: 30px;
 | 
					    bottom: 30px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key-container {
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  overflow-y: auto;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key-grid {
 | 
					 | 
				
			||||||
  display: grid;
 | 
					 | 
				
			||||||
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
 | 
					 | 
				
			||||||
  gap: 16px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key-item {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  background-color: var(--white);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key-title {
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  color: var(--black);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key-keys {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  gap: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  border-radius: 8px;
 | 
					 | 
				
			||||||
  padding: 4px;
 | 
					 | 
				
			||||||
  background-color: var(--gray);
 | 
					 | 
				
			||||||
  min-width: 32px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.shortcut-key span {
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  color: var(--black);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-main {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  .chat-body-container {
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    flex: 1;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .chat-side-panel {
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    inset: 0;
 | 
					 | 
				
			||||||
    background: var(--white);
 | 
					 | 
				
			||||||
    overflow: hidden;
 | 
					 | 
				
			||||||
    z-index: 10;
 | 
					 | 
				
			||||||
    transform: translateX(100%);
 | 
					 | 
				
			||||||
    transition: all ease 0.3s;
 | 
					 | 
				
			||||||
    &-show {
 | 
					 | 
				
			||||||
      transform: translateX(0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,27 +6,14 @@ import EmojiPicker, {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { ModelType } from "../store";
 | 
					import { ModelType } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIconDefault from "../icons/llm-icons/default.svg";
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
import BotIconOpenAI from "../icons/llm-icons/openai.svg";
 | 
					import BlackBotIcon from "../icons/black-bot.svg";
 | 
				
			||||||
import BotIconGemini from "../icons/llm-icons/gemini.svg";
 | 
					 | 
				
			||||||
import BotIconGemma from "../icons/llm-icons/gemma.svg";
 | 
					 | 
				
			||||||
import BotIconClaude from "../icons/llm-icons/claude.svg";
 | 
					 | 
				
			||||||
import BotIconMeta from "../icons/llm-icons/meta.svg";
 | 
					 | 
				
			||||||
import BotIconMistral from "../icons/llm-icons/mistral.svg";
 | 
					 | 
				
			||||||
import BotIconDeepseek from "../icons/llm-icons/deepseek.svg";
 | 
					 | 
				
			||||||
import BotIconMoonshot from "../icons/llm-icons/moonshot.svg";
 | 
					 | 
				
			||||||
import BotIconQwen from "../icons/llm-icons/qwen.svg";
 | 
					 | 
				
			||||||
import BotIconWenxin from "../icons/llm-icons/wenxin.svg";
 | 
					 | 
				
			||||||
import BotIconGrok from "../icons/llm-icons/grok.svg";
 | 
					 | 
				
			||||||
import BotIconHunyuan from "../icons/llm-icons/hunyuan.svg";
 | 
					 | 
				
			||||||
import BotIconDoubao from "../icons/llm-icons/doubao.svg";
 | 
					 | 
				
			||||||
import BotIconChatglm from "../icons/llm-icons/chatglm.svg";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
					export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
				
			||||||
  // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
 | 
					  // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
 | 
				
			||||||
  // Old CDN broken, so I had to switch to this one
 | 
					  // Old CDN broken, so I had to switch to this one
 | 
				
			||||||
  // Author: https://github.com/H0llyW00dzZ
 | 
					  // Author: https://github.com/H0llyW00dzZ
 | 
				
			||||||
  return `https://fastly.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
 | 
					  return `https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AvatarPicker(props: {
 | 
					export function AvatarPicker(props: {
 | 
				
			||||||
@@ -34,7 +21,6 @@ export function AvatarPicker(props: {
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <EmojiPicker
 | 
					    <EmojiPicker
 | 
				
			||||||
      width={"100%"}
 | 
					 | 
				
			||||||
      lazyLoadEmojis
 | 
					      lazyLoadEmojis
 | 
				
			||||||
      theme={EmojiTheme.AUTO}
 | 
					      theme={EmojiTheme.AUTO}
 | 
				
			||||||
      getEmojiUrl={getEmojiUrl}
 | 
					      getEmojiUrl={getEmojiUrl}
 | 
				
			||||||
@@ -46,55 +32,14 @@ export function AvatarPicker(props: {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
					export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
				
			||||||
  let LlmIcon = BotIconDefault;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (props.model) {
 | 
					  if (props.model) {
 | 
				
			||||||
    const modelName = props.model.toLowerCase();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      modelName.startsWith("gpt") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("chatgpt") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("dall-e") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("dalle") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("o1") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("o3")
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconOpenAI;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("gemini")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconGemini;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("gemma")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconGemma;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("claude")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconClaude;
 | 
					 | 
				
			||||||
    } else if (modelName.includes("llama")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconMeta;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("mixtral") || modelName.startsWith("codestral")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconMistral;
 | 
					 | 
				
			||||||
    } else if (modelName.includes("deepseek")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconDeepseek;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("moonshot")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconMoonshot;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("qwen")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconQwen;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("ernie")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconWenxin;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("grok")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconGrok;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("hunyuan")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconHunyuan;
 | 
					 | 
				
			||||||
    } else if (modelName.startsWith("doubao") || modelName.startsWith("ep-")) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconDoubao;
 | 
					 | 
				
			||||||
    } else if (
 | 
					 | 
				
			||||||
      modelName.includes("glm") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("cogview-") ||
 | 
					 | 
				
			||||||
      modelName.startsWith("cogvideox-")
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      LlmIcon = BotIconChatglm;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className="no-dark">
 | 
					      <div className="no-dark">
 | 
				
			||||||
        <LlmIcon className="user-avatar" width={30} height={30} />
 | 
					        {props.model?.startsWith("gpt-4") ? (
 | 
				
			||||||
 | 
					          <BlackBotIcon className="user-avatar" />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <BotIcon className="user-avatar" />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
"use client";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
@@ -8,7 +6,6 @@ import { ISSUE_URL } from "../constant";
 | 
				
			|||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { useSyncStore } from "../store/sync";
 | 
					import { useSyncStore } from "../store/sync";
 | 
				
			||||||
import { useChatStore } from "../store/chat";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IErrorBoundaryState {
 | 
					interface IErrorBoundaryState {
 | 
				
			||||||
  hasError: boolean;
 | 
					  hasError: boolean;
 | 
				
			||||||
@@ -31,7 +28,8 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      useSyncStore.getState().export();
 | 
					      useSyncStore.getState().export();
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      useChatStore.getState().clearAllData();
 | 
					      localStorage.clear();
 | 
				
			||||||
 | 
					      location.reload();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,7 +94,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  button {
 | 
					  button {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:not(:last-child) {
 | 
					    &:not(:last-child) {
 | 
				
			||||||
      margin-right: 10px;
 | 
					      margin-right: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -191,59 +190,6 @@
 | 
				
			|||||||
        pre {
 | 
					        pre {
 | 
				
			||||||
          overflow: hidden;
 | 
					          overflow: hidden;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message-image {
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
          margin-top: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message-images {
 | 
					 | 
				
			||||||
          display: grid;
 | 
					 | 
				
			||||||
          justify-content: left;
 | 
					 | 
				
			||||||
          grid-gap: 10px;
 | 
					 | 
				
			||||||
          grid-template-columns: repeat(var(--image-count), auto);
 | 
					 | 
				
			||||||
          margin-top: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
          $image-width: calc(calc(100vw/2)/var(--image-count));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          .message-image-multi {
 | 
					 | 
				
			||||||
            width: $image-width;
 | 
					 | 
				
			||||||
            height: $image-width;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          .message-image {
 | 
					 | 
				
			||||||
            max-width: calc(100vw/3*2);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media screen and (min-width: 600px) {
 | 
					 | 
				
			||||||
          $max-image-width: calc(900px/3*2/var(--image-count));
 | 
					 | 
				
			||||||
          $image-width: calc(80vw/3*2/var(--image-count));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          .message-image-multi {
 | 
					 | 
				
			||||||
            width: $image-width;
 | 
					 | 
				
			||||||
            height: $image-width;
 | 
					 | 
				
			||||||
            max-width: $max-image-width;
 | 
					 | 
				
			||||||
            max-height: $max-image-width;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          .message-image {
 | 
					 | 
				
			||||||
            max-width: calc(100vw/3*2);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message-image-multi {
 | 
					 | 
				
			||||||
          object-fit: cover;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message-image,
 | 
					 | 
				
			||||||
        .message-image-multi {
 | 
					 | 
				
			||||||
          box-sizing: border-box;
 | 
					 | 
				
			||||||
          border-radius: 10px;
 | 
					 | 
				
			||||||
          border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &-assistant {
 | 
					      &-assistant {
 | 
				
			||||||
@@ -267,5 +213,6 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .default-theme {}
 | 
					  .default-theme {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
/* eslint-disable @next/next/no-img-element */
 | 
					/* eslint-disable @next/next/no-img-element */
 | 
				
			||||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
 | 
					import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import styles from "./exporter.module.scss";
 | 
					import styles from "./exporter.module.scss";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -12,17 +12,13 @@ import {
 | 
				
			|||||||
  showToast,
 | 
					  showToast,
 | 
				
			||||||
} from "./ui-lib";
 | 
					} from "./ui-lib";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import {
 | 
					import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
 | 
				
			||||||
  copyToClipboard,
 | 
					 | 
				
			||||||
  downloadAs,
 | 
					 | 
				
			||||||
  getMessageImages,
 | 
					 | 
				
			||||||
  useMobileScreen,
 | 
					 | 
				
			||||||
} from "../utils";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import CopyIcon from "../icons/copy.svg";
 | 
					import CopyIcon from "../icons/copy.svg";
 | 
				
			||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
import ChatGptIcon from "../icons/chatgpt.png";
 | 
					import ChatGptIcon from "../icons/chatgpt.png";
 | 
				
			||||||
import ShareIcon from "../icons/share.svg";
 | 
					import ShareIcon from "../icons/share.svg";
 | 
				
			||||||
 | 
					import BotIcon from "../icons/bot.png";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import DownloadIcon from "../icons/download.svg";
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
import { useEffect, useMemo, useRef, useState } from "react";
 | 
					import { useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
@@ -32,14 +28,12 @@ import dynamic from "next/dynamic";
 | 
				
			|||||||
import NextImage from "next/image";
 | 
					import NextImage from "next/image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { toBlob, toPng } from "html-to-image";
 | 
					import { toBlob, toPng } from "html-to-image";
 | 
				
			||||||
 | 
					import { DEFAULT_MASK_AVATAR } from "../store/mask";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { prettyObject } from "../utils/format";
 | 
					import { prettyObject } from "../utils/format";
 | 
				
			||||||
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
 | 
					import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant";
 | 
				
			||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import { type ClientApi, getClientApi } from "../client/api";
 | 
					import { ClientApi } from "../client/api";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					 | 
				
			||||||
import { MaskAvatar } from "./mask";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
					const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
				
			||||||
  loading: () => <LoadingIcon />,
 | 
					  loading: () => <LoadingIcon />,
 | 
				
			||||||
@@ -118,10 +112,9 @@ function Steps<
 | 
				
			|||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
              className={clsx("clickable", styles["step"], {
 | 
					              className={`${styles["step"]} ${
 | 
				
			||||||
                [styles["step-finished"]]: i <= props.index,
 | 
					                styles[i <= props.index ? "step-finished" : ""]
 | 
				
			||||||
                [styles["step-current"]]: i === props.index,
 | 
					              } ${i === props.index && styles["step-current"]} clickable`}
 | 
				
			||||||
              })}
 | 
					 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                props.onStepChange?.(i);
 | 
					                props.onStepChange?.(i);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
@@ -294,7 +287,7 @@ export function RenderExport(props: {
 | 
				
			|||||||
          id={`${m.role}:${i}`}
 | 
					          id={`${m.role}:${i}`}
 | 
				
			||||||
          className={EXPORT_MESSAGE_CLASS_NAME}
 | 
					          className={EXPORT_MESSAGE_CLASS_NAME}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Markdown content={getMessageTextContent(m)} defaultShow />
 | 
					          <Markdown content={m.content} defaultShow />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      ))}
 | 
					      ))}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -313,7 +306,12 @@ export function PreviewActions(props: {
 | 
				
			|||||||
  const onRenderMsgs = (msgs: ChatMessage[]) => {
 | 
					  const onRenderMsgs = (msgs: ChatMessage[]) => {
 | 
				
			||||||
    setShouldExport(false);
 | 
					    setShouldExport(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const api: ClientApi = getClientApi(config.modelConfig.providerName);
 | 
					    var api: ClientApi;
 | 
				
			||||||
 | 
					    if (config.modelConfig.model === "gemini-pro") {
 | 
				
			||||||
 | 
					      api = new ClientApi(ModelProvider.GeminiPro);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      api = new ClientApi(ModelProvider.GPT);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api
 | 
					    api
 | 
				
			||||||
      .share(msgs)
 | 
					      .share(msgs)
 | 
				
			||||||
@@ -406,6 +404,22 @@ export function PreviewActions(props: {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ExportAvatar(props: { avatar: string }) {
 | 
				
			||||||
 | 
					  if (props.avatar === DEFAULT_MASK_AVATAR) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <img
 | 
				
			||||||
 | 
					        src={BotIcon.src}
 | 
				
			||||||
 | 
					        width={30}
 | 
				
			||||||
 | 
					        height={30}
 | 
				
			||||||
 | 
					        alt="bot"
 | 
				
			||||||
 | 
					        className="user-avatar"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <Avatar avatar={props.avatar} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ImagePreviewer(props: {
 | 
					export function ImagePreviewer(props: {
 | 
				
			||||||
  messages: ChatMessage[];
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
  topic: string;
 | 
					  topic: string;
 | 
				
			||||||
@@ -510,11 +524,11 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
        messages={props.messages}
 | 
					        messages={props.messages}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        className={clsx(styles["preview-body"], styles["default-theme"])}
 | 
					        className={`${styles["preview-body"]} ${styles["default-theme"]}`}
 | 
				
			||||||
        ref={previewRef}
 | 
					        ref={previewRef}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div className={styles["chat-info"]}>
 | 
					        <div className={styles["chat-info"]}>
 | 
				
			||||||
          <div className={clsx(styles["logo"], "no-dark")}>
 | 
					          <div className={styles["logo"] + " no-dark"}>
 | 
				
			||||||
            <NextImage
 | 
					            <NextImage
 | 
				
			||||||
              src={ChatGptIcon.src}
 | 
					              src={ChatGptIcon.src}
 | 
				
			||||||
              alt="logo"
 | 
					              alt="logo"
 | 
				
			||||||
@@ -526,15 +540,12 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <div className={styles["main-title"]}>NextChat</div>
 | 
					            <div className={styles["main-title"]}>NextChat</div>
 | 
				
			||||||
            <div className={styles["sub-title"]}>
 | 
					            <div className={styles["sub-title"]}>
 | 
				
			||||||
              github.com/ChatGPTNextWeb/ChatGPT-Next-Web
 | 
					              github.com/Yidadaa/ChatGPT-Next-Web
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className={styles["icons"]}>
 | 
					            <div className={styles["icons"]}>
 | 
				
			||||||
              <MaskAvatar avatar={config.avatar} />
 | 
					              <ExportAvatar avatar={config.avatar} />
 | 
				
			||||||
              <span className={styles["icon-space"]}>&</span>
 | 
					              <span className={styles["icon-space"]}>&</span>
 | 
				
			||||||
              <MaskAvatar
 | 
					              <ExportAvatar avatar={mask.avatar} />
 | 
				
			||||||
                avatar={mask.avatar}
 | 
					 | 
				
			||||||
                model={session.mask.modelConfig.model}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
@@ -558,54 +569,21 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
        {props.messages.map((m, i) => {
 | 
					        {props.messages.map((m, i) => {
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={clsx(styles["message"], styles["message-" + m.role])}
 | 
					              className={styles["message"] + " " + styles["message-" + m.role]}
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <div className={styles["avatar"]}>
 | 
					              <div className={styles["avatar"]}>
 | 
				
			||||||
                {m.role === "user" ? (
 | 
					                <ExportAvatar
 | 
				
			||||||
                  <Avatar avatar={config.avatar}></Avatar>
 | 
					                  avatar={m.role === "user" ? config.avatar : mask.avatar}
 | 
				
			||||||
                ) : (
 | 
					 | 
				
			||||||
                  <MaskAvatar
 | 
					 | 
				
			||||||
                    avatar={session.mask.avatar}
 | 
					 | 
				
			||||||
                    model={m.model || session.mask.modelConfig.model}
 | 
					 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div className={styles["body"]}>
 | 
					              <div className={styles["body"]}>
 | 
				
			||||||
                <Markdown
 | 
					                <Markdown
 | 
				
			||||||
                  content={getMessageTextContent(m)}
 | 
					                  content={m.content}
 | 
				
			||||||
                  fontSize={config.fontSize}
 | 
					                  fontSize={config.fontSize}
 | 
				
			||||||
                  fontFamily={config.fontFamily}
 | 
					 | 
				
			||||||
                  defaultShow
 | 
					                  defaultShow
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                {getMessageImages(m).length == 1 && (
 | 
					 | 
				
			||||||
                  <img
 | 
					 | 
				
			||||||
                    key={i}
 | 
					 | 
				
			||||||
                    src={getMessageImages(m)[0]}
 | 
					 | 
				
			||||||
                    alt="message"
 | 
					 | 
				
			||||||
                    className={styles["message-image"]}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
                {getMessageImages(m).length > 1 && (
 | 
					 | 
				
			||||||
                  <div
 | 
					 | 
				
			||||||
                    className={styles["message-images"]}
 | 
					 | 
				
			||||||
                    style={
 | 
					 | 
				
			||||||
                      {
 | 
					 | 
				
			||||||
                        "--image-count": getMessageImages(m).length,
 | 
					 | 
				
			||||||
                      } as React.CSSProperties
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {getMessageImages(m).map((src, i) => (
 | 
					 | 
				
			||||||
                      <img
 | 
					 | 
				
			||||||
                        key={i}
 | 
					 | 
				
			||||||
                        src={src}
 | 
					 | 
				
			||||||
                        alt="message"
 | 
					 | 
				
			||||||
                        className={styles["message-image-multi"]}
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                    ))}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
@@ -624,10 +602,8 @@ export function MarkdownPreviewer(props: {
 | 
				
			|||||||
    props.messages
 | 
					    props.messages
 | 
				
			||||||
      .map((m) => {
 | 
					      .map((m) => {
 | 
				
			||||||
        return m.role === "user"
 | 
					        return m.role === "user"
 | 
				
			||||||
          ? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
 | 
					          ? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
 | 
				
			||||||
          : `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
 | 
					          : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
 | 
				
			||||||
              m,
 | 
					 | 
				
			||||||
            ).trim()}`;
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .join("\n\n");
 | 
					      .join("\n\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,21 +137,12 @@
 | 
				
			|||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  padding-top: 20px;
 | 
					  padding-top: 20px;
 | 
				
			||||||
  padding-bottom: 20px;
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  &-narrow {
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-logo {
 | 
					.sidebar-logo {
 | 
				
			||||||
  display: inline-flex;
 | 
					  position: absolute;
 | 
				
			||||||
}
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 18px;
 | 
				
			||||||
.sidebar-title-container {
 | 
					 | 
				
			||||||
  display: inline-flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-title {
 | 
					.sidebar-title {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require("../polyfill");
 | 
					require("../polyfill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useState, useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
@@ -11,39 +12,33 @@ import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			|||||||
import { getCSSVar, useMobileScreen } from "../utils";
 | 
					import { getCSSVar, useMobileScreen } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import dynamic from "next/dynamic";
 | 
					import dynamic from "next/dynamic";
 | 
				
			||||||
import { Path, SlotID } from "../constant";
 | 
					import { ModelProvider, Path, SlotID } from "../constant";
 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { getISOLang, getLang } from "../locales";
 | 
					import { getISOLang, getLang } from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  HashRouter as Router,
 | 
					  HashRouter as Router,
 | 
				
			||||||
  Route,
 | 
					 | 
				
			||||||
  Routes,
 | 
					  Routes,
 | 
				
			||||||
 | 
					  Route,
 | 
				
			||||||
  useLocation,
 | 
					  useLocation,
 | 
				
			||||||
} from "react-router-dom";
 | 
					} from "react-router-dom";
 | 
				
			||||||
import { SideBar } from "./sidebar";
 | 
					import { SideBar } from "./sidebar";
 | 
				
			||||||
import { useAppConfig } from "../store/config";
 | 
					import { useAppConfig } from "../store/config";
 | 
				
			||||||
import { AuthPage } from "./auth";
 | 
					import { AuthPage } from "./auth";
 | 
				
			||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import { type ClientApi, getClientApi } from "../client/api";
 | 
					import { ClientApi } from "../client/api";
 | 
				
			||||||
import { useAccessStore } from "../store";
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Loading(props: { noLogo?: boolean }) {
 | 
					export function Loading(props: { noLogo?: boolean }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={clsx("no-dark", styles["loading-content"])}>
 | 
					    <div className={styles["loading-content"] + " no-dark"}>
 | 
				
			||||||
      {!props.noLogo && <BotIcon />}
 | 
					      {!props.noLogo && <BotIcon />}
 | 
				
			||||||
      <LoadingIcon />
 | 
					      <LoadingIcon />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
 | 
					 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
					const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -60,28 +55,6 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
				
			|||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
 | 
					 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SearchChat = dynamic(
 | 
					 | 
				
			||||||
  async () => (await import("./search-chat")).SearchChatPage,
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    loading: () => <Loading noLogo />,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
					 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const McpMarketPage = dynamic(
 | 
					 | 
				
			||||||
  async () => (await import("./mcp-market")).McpMarketPage,
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    loading: () => <Loading noLogo />,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useSwitchTheme() {
 | 
					export function useSwitchTheme() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -149,23 +122,11 @@ const loadAsyncGoogleFont = () => {
 | 
				
			|||||||
  document.head.appendChild(linkEl);
 | 
					  document.head.appendChild(linkEl);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WindowContent(props: { children: React.ReactNode }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
					 | 
				
			||||||
      {props?.children}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function Screen() {
 | 
					function Screen() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
  const isArtifact = location.pathname.includes(Path.Artifacts);
 | 
					 | 
				
			||||||
  const isHome = location.pathname === Path.Home;
 | 
					  const isHome = location.pathname === Path.Home;
 | 
				
			||||||
  const isAuth = location.pathname === Path.Auth;
 | 
					  const isAuth = location.pathname === Path.Auth;
 | 
				
			||||||
  const isSd = location.pathname === Path.Sd;
 | 
					 | 
				
			||||||
  const isSdNew = location.pathname === Path.SdNew;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
  const shouldTightBorder =
 | 
					  const shouldTightBorder =
 | 
				
			||||||
    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
					    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
				
			||||||
@@ -174,48 +135,34 @@ function Screen() {
 | 
				
			|||||||
    loadAsyncGoogleFont();
 | 
					    loadAsyncGoogleFont();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isArtifact) {
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
      <Routes>
 | 
					    <div
 | 
				
			||||||
        <Route path="/artifacts/:id" element={<Artifacts />} />
 | 
					      className={
 | 
				
			||||||
      </Routes>
 | 
					        styles.container +
 | 
				
			||||||
    );
 | 
					        ` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
 | 
				
			||||||
 | 
					          getLang() === "ar" ? styles["rtl-screen"] : ""
 | 
				
			||||||
 | 
					        }`
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
  const renderContent = () => {
 | 
					    >
 | 
				
			||||||
    if (isAuth) return <AuthPage />;
 | 
					      {isAuth ? (
 | 
				
			||||||
    if (isSd) return <Sd />;
 | 
					 | 
				
			||||||
    if (isSdNew) return <Sd />;
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
        <SideBar
 | 
					          <AuthPage />
 | 
				
			||||||
          className={clsx({
 | 
					        </>
 | 
				
			||||||
            [styles["sidebar-show"]]: isHome,
 | 
					      ) : (
 | 
				
			||||||
          })}
 | 
					        <>
 | 
				
			||||||
        />
 | 
					          <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
				
			||||||
        <WindowContent>
 | 
					
 | 
				
			||||||
 | 
					          <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
				
			||||||
            <Routes>
 | 
					            <Routes>
 | 
				
			||||||
              <Route path={Path.Home} element={<Chat />} />
 | 
					              <Route path={Path.Home} element={<Chat />} />
 | 
				
			||||||
              <Route path={Path.NewChat} element={<NewChat />} />
 | 
					              <Route path={Path.NewChat} element={<NewChat />} />
 | 
				
			||||||
              <Route path={Path.Masks} element={<MaskPage />} />
 | 
					              <Route path={Path.Masks} element={<MaskPage />} />
 | 
				
			||||||
            <Route path={Path.Plugins} element={<PluginPage />} />
 | 
					 | 
				
			||||||
            <Route path={Path.SearchChat} element={<SearchChat />} />
 | 
					 | 
				
			||||||
              <Route path={Path.Chat} element={<Chat />} />
 | 
					              <Route path={Path.Chat} element={<Chat />} />
 | 
				
			||||||
              <Route path={Path.Settings} element={<Settings />} />
 | 
					              <Route path={Path.Settings} element={<Settings />} />
 | 
				
			||||||
            <Route path={Path.McpMarket} element={<McpMarketPage />} />
 | 
					 | 
				
			||||||
            </Routes>
 | 
					            </Routes>
 | 
				
			||||||
        </WindowContent>
 | 
					          </div>
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
    );
 | 
					      )}
 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      className={clsx(styles.container, {
 | 
					 | 
				
			||||||
        [styles["tight-container"]]: shouldTightBorder,
 | 
					 | 
				
			||||||
        [styles["rtl-screen"]]: getLang() === "ar",
 | 
					 | 
				
			||||||
      })}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      {renderContent()}
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -223,8 +170,12 @@ function Screen() {
 | 
				
			|||||||
export function useLoadData() {
 | 
					export function useLoadData() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const api: ClientApi = getClientApi(config.modelConfig.providerName);
 | 
					  var api: ClientApi;
 | 
				
			||||||
 | 
					  if (config.modelConfig.model === "gemini-pro") {
 | 
				
			||||||
 | 
					    api = new ClientApi(ModelProvider.GeminiPro);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    api = new ClientApi(ModelProvider.GPT);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    (async () => {
 | 
					    (async () => {
 | 
				
			||||||
      const models = await api.llm.models();
 | 
					      const models = await api.llm.models();
 | 
				
			||||||
@@ -242,20 +193,6 @@ export function Home() {
 | 
				
			|||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    console.log("[Config] got config from build time", getClientConfig());
 | 
					    console.log("[Config] got config from build time", getClientConfig());
 | 
				
			||||||
    useAccessStore.getState().fetch();
 | 
					    useAccessStore.getState().fetch();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const initMcp = async () => {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const enabled = await isMcpEnabled();
 | 
					 | 
				
			||||||
        if (enabled) {
 | 
					 | 
				
			||||||
          console.log("[MCP] initializing...");
 | 
					 | 
				
			||||||
          await initializeMcpSystem();
 | 
					 | 
				
			||||||
          console.log("[MCP] initialized");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } catch (err) {
 | 
					 | 
				
			||||||
        console.error("[MCP] failed to initialize:", err);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    initMcp();
 | 
					 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!useHasHydrated()) {
 | 
					  if (!useHasHydrated()) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
import styles from "./input-range.module.scss";
 | 
					import styles from "./input-range.module.scss";
 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface InputRangeProps {
 | 
					interface InputRangeProps {
 | 
				
			||||||
  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
					  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
				
			||||||
@@ -10,7 +9,6 @@ interface InputRangeProps {
 | 
				
			|||||||
  min: string;
 | 
					  min: string;
 | 
				
			||||||
  max: string;
 | 
					  max: string;
 | 
				
			||||||
  step: string;
 | 
					  step: string;
 | 
				
			||||||
  aria: string;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function InputRange({
 | 
					export function InputRange({
 | 
				
			||||||
@@ -21,13 +19,11 @@ export function InputRange({
 | 
				
			|||||||
  min,
 | 
					  min,
 | 
				
			||||||
  max,
 | 
					  max,
 | 
				
			||||||
  step,
 | 
					  step,
 | 
				
			||||||
  aria,
 | 
					 | 
				
			||||||
}: InputRangeProps) {
 | 
					}: InputRangeProps) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={clsx(styles["input-range"], className)}>
 | 
					    <div className={styles["input-range"] + ` ${className ?? ""}`}>
 | 
				
			||||||
      {title || value}
 | 
					      {title || value}
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
        aria-label={aria}
 | 
					 | 
				
			||||||
        type="range"
 | 
					        type="range"
 | 
				
			||||||
        title={title}
 | 
					        title={title}
 | 
				
			||||||
        value={value}
 | 
					        value={value}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,24 +6,13 @@ import RehypeKatex from "rehype-katex";
 | 
				
			|||||||
import RemarkGfm from "remark-gfm";
 | 
					import RemarkGfm from "remark-gfm";
 | 
				
			||||||
import RehypeHighlight from "rehype-highlight";
 | 
					import RehypeHighlight from "rehype-highlight";
 | 
				
			||||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
					import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
				
			||||||
import { copyToClipboard, useWindowSize } from "../utils";
 | 
					import { copyToClipboard } from "../utils";
 | 
				
			||||||
import mermaid from "mermaid";
 | 
					import mermaid from "mermaid";
 | 
				
			||||||
import Locale from "../locales";
 | 
					
 | 
				
			||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
import ReloadButtonIcon from "../icons/reload.svg";
 | 
					 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { useDebouncedCallback } from "use-debounce";
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
import { showImageModal, FullScreen } from "./ui-lib";
 | 
					import { showImageModal } from "./ui-lib";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ArtifactsShareButton,
 | 
					 | 
				
			||||||
  HTMLPreview,
 | 
					 | 
				
			||||||
  HTMLPreviewHander,
 | 
					 | 
				
			||||||
} from "./artifacts";
 | 
					 | 
				
			||||||
import { useChatStore } from "../store";
 | 
					 | 
				
			||||||
import { IconButton } from "./button";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useAppConfig } from "../store/config";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Mermaid(props: { code: string }) {
 | 
					export function Mermaid(props: { code: string }) {
 | 
				
			||||||
  const ref = useRef<HTMLDivElement>(null);
 | 
					  const ref = useRef<HTMLDivElement>(null);
 | 
				
			||||||
@@ -58,7 +47,7 @@ export function Mermaid(props: { code: string }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={clsx("no-dark", "mermaid")}
 | 
					      className="no-dark mermaid"
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        cursor: "pointer",
 | 
					        cursor: "pointer",
 | 
				
			||||||
        overflow: "auto",
 | 
					        overflow: "auto",
 | 
				
			||||||
@@ -73,204 +62,65 @@ export function Mermaid(props: { code: string }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function PreCode(props: { children: any }) {
 | 
					export function PreCode(props: { children: any }) {
 | 
				
			||||||
  const ref = useRef<HTMLPreElement>(null);
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
					  const refText = ref.current?.innerText;
 | 
				
			||||||
  const [mermaidCode, setMermaidCode] = useState("");
 | 
					  const [mermaidCode, setMermaidCode] = useState("");
 | 
				
			||||||
  const [htmlCode, setHtmlCode] = useState("");
 | 
					 | 
				
			||||||
  const { height } = useWindowSize();
 | 
					 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const renderArtifacts = useDebouncedCallback(() => {
 | 
					  const renderMermaid = useDebouncedCallback(() => {
 | 
				
			||||||
    if (!ref.current) return;
 | 
					    if (!ref.current) return;
 | 
				
			||||||
    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
					    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
				
			||||||
    if (mermaidDom) {
 | 
					    if (mermaidDom) {
 | 
				
			||||||
      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
					      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const htmlDom = ref.current.querySelector("code.language-html");
 | 
					 | 
				
			||||||
    const refText = ref.current.querySelector("code")?.innerText;
 | 
					 | 
				
			||||||
    if (htmlDom) {
 | 
					 | 
				
			||||||
      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
					 | 
				
			||||||
    } else if (
 | 
					 | 
				
			||||||
      refText?.startsWith("<!DOCTYPE") ||
 | 
					 | 
				
			||||||
      refText?.startsWith("<svg") ||
 | 
					 | 
				
			||||||
      refText?.startsWith("<?xml")
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      setHtmlCode(refText);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 600);
 | 
					  }, 600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const config = useAppConfig();
 | 
					 | 
				
			||||||
  const enableArtifacts =
 | 
					 | 
				
			||||||
    session.mask?.enableArtifacts !== false && config.enableArtifacts;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //Wrap the paragraph for plain-text
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (ref.current) {
 | 
					    setTimeout(renderMermaid, 1);
 | 
				
			||||||
      const codeElements = ref.current.querySelectorAll(
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
        "code",
 | 
					  }, [refText]);
 | 
				
			||||||
      ) as NodeListOf<HTMLElement>;
 | 
					 | 
				
			||||||
      const wrapLanguages = [
 | 
					 | 
				
			||||||
        "",
 | 
					 | 
				
			||||||
        "md",
 | 
					 | 
				
			||||||
        "markdown",
 | 
					 | 
				
			||||||
        "text",
 | 
					 | 
				
			||||||
        "txt",
 | 
					 | 
				
			||||||
        "plaintext",
 | 
					 | 
				
			||||||
        "tex",
 | 
					 | 
				
			||||||
        "latex",
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
      codeElements.forEach((codeElement) => {
 | 
					 | 
				
			||||||
        let languageClass = codeElement.className.match(/language-(\w+)/);
 | 
					 | 
				
			||||||
        let name = languageClass ? languageClass[1] : "";
 | 
					 | 
				
			||||||
        if (wrapLanguages.includes(name)) {
 | 
					 | 
				
			||||||
          codeElement.style.whiteSpace = "pre-wrap";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      setTimeout(renderArtifacts, 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
 | 
					      {mermaidCode.length > 0 && (
 | 
				
			||||||
 | 
					        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      <pre ref={ref}>
 | 
					      <pre ref={ref}>
 | 
				
			||||||
        <span
 | 
					        <span
 | 
				
			||||||
          className="copy-code-button"
 | 
					          className="copy-code-button"
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            if (ref.current) {
 | 
					            if (ref.current) {
 | 
				
			||||||
              copyToClipboard(
 | 
					              const code = ref.current.innerText;
 | 
				
			||||||
                ref.current.querySelector("code")?.innerText ?? "",
 | 
					              copyToClipboard(code);
 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        ></span>
 | 
					        ></span>
 | 
				
			||||||
        {props.children}
 | 
					        {props.children}
 | 
				
			||||||
      </pre>
 | 
					      </pre>
 | 
				
			||||||
      {mermaidCode.length > 0 && (
 | 
					 | 
				
			||||||
        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      {htmlCode.length > 0 && enableArtifacts && (
 | 
					 | 
				
			||||||
        <FullScreen className="no-dark html" right={70}>
 | 
					 | 
				
			||||||
          <ArtifactsShareButton
 | 
					 | 
				
			||||||
            style={{ position: "absolute", right: 20, top: 10 }}
 | 
					 | 
				
			||||||
            getCode={() => htmlCode}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <IconButton
 | 
					 | 
				
			||||||
            style={{ position: "absolute", right: 120, top: 10 }}
 | 
					 | 
				
			||||||
            bordered
 | 
					 | 
				
			||||||
            icon={<ReloadButtonIcon />}
 | 
					 | 
				
			||||||
            shadow
 | 
					 | 
				
			||||||
            onClick={() => previewRef.current?.reload()}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <HTMLPreview
 | 
					 | 
				
			||||||
            ref={previewRef}
 | 
					 | 
				
			||||||
            code={htmlCode}
 | 
					 | 
				
			||||||
            autoHeight={!document.fullscreenElement}
 | 
					 | 
				
			||||||
            height={!document.fullscreenElement ? 600 : height}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </FullScreen>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function CustomCode(props: { children: any; className?: string }) {
 | 
					function escapeDollarNumber(text: string) {
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  let escapedText = "";
 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					 | 
				
			||||||
  const config = useAppConfig();
 | 
					 | 
				
			||||||
  const enableCodeFold =
 | 
					 | 
				
			||||||
    session.mask?.enableCodeFold !== false && config.enableCodeFold;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ref = useRef<HTMLPreElement>(null);
 | 
					  for (let i = 0; i < text.length; i += 1) {
 | 
				
			||||||
  const [collapsed, setCollapsed] = useState(true);
 | 
					    let char = text[i];
 | 
				
			||||||
  const [showToggle, setShowToggle] = useState(false);
 | 
					    const nextChar = text[i + 1] || " ";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					    if (char === "$" && nextChar >= "0" && nextChar <= "9") {
 | 
				
			||||||
    if (ref.current) {
 | 
					      char = "\\$";
 | 
				
			||||||
      const codeHeight = ref.current.scrollHeight;
 | 
					 | 
				
			||||||
      setShowToggle(codeHeight > 400);
 | 
					 | 
				
			||||||
      ref.current.scrollTop = ref.current.scrollHeight;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [props.children]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toggleCollapsed = () => {
 | 
					 | 
				
			||||||
    setCollapsed((collapsed) => !collapsed);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  const renderShowMoreButton = () => {
 | 
					 | 
				
			||||||
    if (showToggle && enableCodeFold && collapsed) {
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          className={clsx("show-hide-button", {
 | 
					 | 
				
			||||||
            collapsed,
 | 
					 | 
				
			||||||
            expanded: !collapsed,
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <code
 | 
					 | 
				
			||||||
        className={clsx(props?.className)}
 | 
					 | 
				
			||||||
        ref={ref}
 | 
					 | 
				
			||||||
        style={{
 | 
					 | 
				
			||||||
          maxHeight: enableCodeFold && collapsed ? "400px" : "none",
 | 
					 | 
				
			||||||
          overflowY: "hidden",
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        {props.children}
 | 
					 | 
				
			||||||
      </code>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {renderShowMoreButton()}
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeBrackets(text: string) {
 | 
					    escapedText += char;
 | 
				
			||||||
  const pattern =
 | 
					 | 
				
			||||||
    /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
 | 
					 | 
				
			||||||
  return text.replace(
 | 
					 | 
				
			||||||
    pattern,
 | 
					 | 
				
			||||||
    (match, codeBlock, squareBracket, roundBracket) => {
 | 
					 | 
				
			||||||
      if (codeBlock) {
 | 
					 | 
				
			||||||
        return codeBlock;
 | 
					 | 
				
			||||||
      } else if (squareBracket) {
 | 
					 | 
				
			||||||
        return `$$${squareBracket}$$`;
 | 
					 | 
				
			||||||
      } else if (roundBracket) {
 | 
					 | 
				
			||||||
        return `$${roundBracket}$`;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return match;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function tryWrapHtmlCode(text: string) {
 | 
					  return escapedText;
 | 
				
			||||||
  // try add wrap html code (fixed: html codeblock include 2 newline)
 | 
					 | 
				
			||||||
  // ignore embed codeblock
 | 
					 | 
				
			||||||
  if (text.includes("```")) {
 | 
					 | 
				
			||||||
    return text;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return text
 | 
					 | 
				
			||||||
    .replace(
 | 
					 | 
				
			||||||
      /([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
 | 
					 | 
				
			||||||
      (match, quoteStart, lang, newLine, doctype) => {
 | 
					 | 
				
			||||||
        return !quoteStart ? "\n```html\n" + doctype : match;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .replace(
 | 
					 | 
				
			||||||
      /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
 | 
					 | 
				
			||||||
      (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
 | 
					 | 
				
			||||||
        return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function _MarkDownContent(props: { content: string }) {
 | 
					function _MarkDownContent(props: { content: string }) {
 | 
				
			||||||
  const escapedContent = useMemo(() => {
 | 
					  const escapedContent = useMemo(
 | 
				
			||||||
    return tryWrapHtmlCode(escapeBrackets(props.content));
 | 
					    () => escapeDollarNumber(props.content),
 | 
				
			||||||
  }, [props.content]);
 | 
					    [props.content],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ReactMarkdown
 | 
					    <ReactMarkdown
 | 
				
			||||||
@@ -287,24 +137,9 @@ function _MarkDownContent(props: { content: string }) {
 | 
				
			|||||||
      ]}
 | 
					      ]}
 | 
				
			||||||
      components={{
 | 
					      components={{
 | 
				
			||||||
        pre: PreCode,
 | 
					        pre: PreCode,
 | 
				
			||||||
        code: CustomCode,
 | 
					 | 
				
			||||||
        p: (pProps) => <p {...pProps} dir="auto" />,
 | 
					        p: (pProps) => <p {...pProps} dir="auto" />,
 | 
				
			||||||
        a: (aProps) => {
 | 
					        a: (aProps) => {
 | 
				
			||||||
          const href = aProps.href || "";
 | 
					          const href = aProps.href || "";
 | 
				
			||||||
          if (/\.(aac|mp3|opus|wav)$/.test(href)) {
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              <figure>
 | 
					 | 
				
			||||||
                <audio controls src={href}></audio>
 | 
					 | 
				
			||||||
              </figure>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              <video controls width="99.9%">
 | 
					 | 
				
			||||||
                <source src={href} />
 | 
					 | 
				
			||||||
              </video>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          const isInternal = /^\/#/i.test(href);
 | 
					          const isInternal = /^\/#/i.test(href);
 | 
				
			||||||
          const target = isInternal ? "_self" : aProps.target ?? "_blank";
 | 
					          const target = isInternal ? "_self" : aProps.target ?? "_blank";
 | 
				
			||||||
          return <a {...aProps} target={target} />;
 | 
					          return <a {...aProps} target={target} />;
 | 
				
			||||||
@@ -323,7 +158,6 @@ export function Markdown(
 | 
				
			|||||||
    content: string;
 | 
					    content: string;
 | 
				
			||||||
    loading?: boolean;
 | 
					    loading?: boolean;
 | 
				
			||||||
    fontSize?: number;
 | 
					    fontSize?: number;
 | 
				
			||||||
    fontFamily?: string;
 | 
					 | 
				
			||||||
    parentRef?: RefObject<HTMLDivElement>;
 | 
					    parentRef?: RefObject<HTMLDivElement>;
 | 
				
			||||||
    defaultShow?: boolean;
 | 
					    defaultShow?: boolean;
 | 
				
			||||||
  } & React.DOMAttributes<HTMLDivElement>,
 | 
					  } & React.DOMAttributes<HTMLDivElement>,
 | 
				
			||||||
@@ -335,7 +169,6 @@ export function Markdown(
 | 
				
			|||||||
      className="markdown-body"
 | 
					      className="markdown-body"
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        fontSize: `${props.fontSize ?? 14}px`,
 | 
					        fontSize: `${props.fontSize ?? 14}px`,
 | 
				
			||||||
        fontFamily: props.fontFamily || "inherit",
 | 
					 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      ref={mdRef}
 | 
					      ref={mdRef}
 | 
				
			||||||
      onContextMenu={props.onContextMenu}
 | 
					      onContextMenu={props.onContextMenu}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ import {
 | 
				
			|||||||
  useAppConfig,
 | 
					  useAppConfig,
 | 
				
			||||||
  useChatStore,
 | 
					  useChatStore,
 | 
				
			||||||
} from "../store";
 | 
					} from "../store";
 | 
				
			||||||
import { MultimodalContent, ROLES } from "../client/api";
 | 
					import { ROLES } from "../client/api";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Input,
 | 
					  Input,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
@@ -37,25 +37,19 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
 | 
				
			|||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import chatStyle from "./chat.module.scss";
 | 
					import chatStyle from "./chat.module.scss";
 | 
				
			||||||
import { useState } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import {
 | 
					import { copyToClipboard, downloadAs, readFromFile } from "../utils";
 | 
				
			||||||
  copyToClipboard,
 | 
					 | 
				
			||||||
  downloadAs,
 | 
					 | 
				
			||||||
  getMessageImages,
 | 
					 | 
				
			||||||
  readFromFile,
 | 
					 | 
				
			||||||
} from "../utils";
 | 
					 | 
				
			||||||
import { Updater } from "../typing";
 | 
					import { Updater } from "../typing";
 | 
				
			||||||
import { ModelConfigList } from "./model-config";
 | 
					import { ModelConfigList } from "./model-config";
 | 
				
			||||||
import { FileName, Path } from "../constant";
 | 
					import { FileName, Path } from "../constant";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  DragDropContext,
 | 
					  DragDropContext,
 | 
				
			||||||
  Droppable,
 | 
					  Droppable,
 | 
				
			||||||
  Draggable,
 | 
					  Draggable,
 | 
				
			||||||
  OnDragEndResponder,
 | 
					  OnDragEndResponder,
 | 
				
			||||||
} from "@hello-pangea/dnd";
 | 
					} from "@hello-pangea/dnd";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// drag and drop helper function
 | 
					// drag and drop helper function
 | 
				
			||||||
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
					function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
				
			||||||
@@ -127,8 +121,6 @@ export function MaskConfig(props: {
 | 
				
			|||||||
            onClose={() => setShowPicker(false)}
 | 
					            onClose={() => setShowPicker(false)}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              tabIndex={0}
 | 
					 | 
				
			||||||
              aria-label={Locale.Mask.Config.Avatar}
 | 
					 | 
				
			||||||
              onClick={() => setShowPicker(true)}
 | 
					              onClick={() => setShowPicker(true)}
 | 
				
			||||||
              style={{ cursor: "pointer" }}
 | 
					              style={{ cursor: "pointer" }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -141,7 +133,6 @@ export function MaskConfig(props: {
 | 
				
			|||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
        <ListItem title={Locale.Mask.Config.Name}>
 | 
					        <ListItem title={Locale.Mask.Config.Name}>
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
            aria-label={Locale.Mask.Config.Name}
 | 
					 | 
				
			||||||
            type="text"
 | 
					            type="text"
 | 
				
			||||||
            value={props.mask.name}
 | 
					            value={props.mask.name}
 | 
				
			||||||
            onInput={(e) =>
 | 
					            onInput={(e) =>
 | 
				
			||||||
@@ -156,7 +147,6 @@ export function MaskConfig(props: {
 | 
				
			|||||||
          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
					          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
            aria-label={Locale.Mask.Config.HideContext.Title}
 | 
					 | 
				
			||||||
            type="checkbox"
 | 
					            type="checkbox"
 | 
				
			||||||
            checked={props.mask.hideContext}
 | 
					            checked={props.mask.hideContext}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
@@ -167,48 +157,12 @@ export function MaskConfig(props: {
 | 
				
			|||||||
          ></input>
 | 
					          ></input>
 | 
				
			||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {globalConfig.enableArtifacts && (
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Mask.Config.Artifacts.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Mask.Config.Artifacts.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <input
 | 
					 | 
				
			||||||
              aria-label={Locale.Mask.Config.Artifacts.Title}
 | 
					 | 
				
			||||||
              type="checkbox"
 | 
					 | 
				
			||||||
              checked={props.mask.enableArtifacts !== false}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateMask((mask) => {
 | 
					 | 
				
			||||||
                  mask.enableArtifacts = e.currentTarget.checked;
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            ></input>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        {globalConfig.enableCodeFold && (
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Mask.Config.CodeFold.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <input
 | 
					 | 
				
			||||||
              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
					 | 
				
			||||||
              type="checkbox"
 | 
					 | 
				
			||||||
              checked={props.mask.enableCodeFold !== false}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateMask((mask) => {
 | 
					 | 
				
			||||||
                  mask.enableCodeFold = e.currentTarget.checked;
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            ></input>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {!props.shouldSyncFromGlobal ? (
 | 
					        {!props.shouldSyncFromGlobal ? (
 | 
				
			||||||
          <ListItem
 | 
					          <ListItem
 | 
				
			||||||
            title={Locale.Mask.Config.Share.Title}
 | 
					            title={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
					            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
              aria={Locale.Mask.Config.Share.Title}
 | 
					 | 
				
			||||||
              icon={<CopyIcon />}
 | 
					              icon={<CopyIcon />}
 | 
				
			||||||
              text={Locale.Mask.Config.Share.Action}
 | 
					              text={Locale.Mask.Config.Share.Action}
 | 
				
			||||||
              onClick={copyMaskLink}
 | 
					              onClick={copyMaskLink}
 | 
				
			||||||
@@ -222,7 +176,6 @@ export function MaskConfig(props: {
 | 
				
			|||||||
            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
					            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              aria-label={Locale.Mask.Config.Sync.Title}
 | 
					 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={props.mask.syncGlobalConfig}
 | 
					              checked={props.mask.syncGlobalConfig}
 | 
				
			||||||
              onChange={async (e) => {
 | 
					              onChange={async (e) => {
 | 
				
			||||||
@@ -291,7 +244,7 @@ function ContextPromptItem(props: {
 | 
				
			|||||||
        </>
 | 
					        </>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      <Input
 | 
					      <Input
 | 
				
			||||||
        value={getMessageTextContent(props.prompt)}
 | 
					        value={props.prompt.content}
 | 
				
			||||||
        type="text"
 | 
					        type="text"
 | 
				
			||||||
        className={chatStyle["context-content"]}
 | 
					        className={chatStyle["context-content"]}
 | 
				
			||||||
        rows={focusingInput ? 5 : 1}
 | 
					        rows={focusingInput ? 5 : 1}
 | 
				
			||||||
@@ -336,18 +289,7 @@ export function ContextPrompts(props: {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateContextPrompt = (i: number, prompt: ChatMessage) => {
 | 
					  const updateContextPrompt = (i: number, prompt: ChatMessage) => {
 | 
				
			||||||
    props.updateContext((context) => {
 | 
					    props.updateContext((context) => (context[i] = prompt));
 | 
				
			||||||
      const images = getMessageImages(context[i]);
 | 
					 | 
				
			||||||
      context[i] = prompt;
 | 
					 | 
				
			||||||
      if (images.length > 0) {
 | 
					 | 
				
			||||||
        const text = getMessageTextContent(context[i]);
 | 
					 | 
				
			||||||
        const newContext: MultimodalContent[] = [{ type: "text", text }];
 | 
					 | 
				
			||||||
        for (const img of images) {
 | 
					 | 
				
			||||||
          newContext.push({ type: "image_url", image_url: { url: img } });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        context[i].content = newContext;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onDragEnd: OnDragEndResponder = (result) => {
 | 
					  const onDragEnd: OnDragEndResponder = (result) => {
 | 
				
			||||||
@@ -445,7 +387,7 @@ export function MaskPage() {
 | 
				
			|||||||
  const maskStore = useMaskStore();
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const filterLang = maskStore.language;
 | 
					  const [filterLang, setFilterLang] = useState<Lang>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allMasks = maskStore
 | 
					  const allMasks = maskStore
 | 
				
			||||||
    .getAll()
 | 
					    .getAll()
 | 
				
			||||||
@@ -552,9 +494,9 @@ export function MaskPage() {
 | 
				
			|||||||
              onChange={(e) => {
 | 
					              onChange={(e) => {
 | 
				
			||||||
                const value = e.currentTarget.value;
 | 
					                const value = e.currentTarget.value;
 | 
				
			||||||
                if (value === Locale.Settings.Lang.All) {
 | 
					                if (value === Locale.Settings.Lang.All) {
 | 
				
			||||||
                  maskStore.setLanguage(undefined);
 | 
					                  setFilterLang(undefined);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                  maskStore.setLanguage(value as Lang);
 | 
					                  setFilterLang(value as Lang);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -589,7 +531,7 @@ export function MaskPage() {
 | 
				
			|||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className={styles["mask-title"]}>
 | 
					                  <div className={styles["mask-title"]}>
 | 
				
			||||||
                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
					                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
				
			||||||
                    <div className={clsx(styles["mask-info"], "one-line")}>
 | 
					                    <div className={styles["mask-info"] + " one-line"}>
 | 
				
			||||||
                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
					                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
				
			||||||
                        ALL_LANG_OPTIONS[m.lang]
 | 
					                        ALL_LANG_OPTIONS[m.lang]
 | 
				
			||||||
                      } / ${m.modelConfig.model}`}
 | 
					                      } / ${m.modelConfig.model}`}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,657 +0,0 @@
 | 
				
			|||||||
@import "../styles/animation.scss";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.mcp-market-page {
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .loading-indicator {
 | 
					 | 
				
			||||||
    font-size: 12px;
 | 
					 | 
				
			||||||
    color: var(--primary);
 | 
					 | 
				
			||||||
    margin-left: 8px;
 | 
					 | 
				
			||||||
    font-weight: normal;
 | 
					 | 
				
			||||||
    opacity: 0.8;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .mcp-market-page-body {
 | 
					 | 
				
			||||||
    padding: 20px;
 | 
					 | 
				
			||||||
    overflow-y: auto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .loading-container,
 | 
					 | 
				
			||||||
    .empty-container {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      min-height: 200px;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      background-color: var(--white);
 | 
					 | 
				
			||||||
      border: var(--border-in-light);
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      animation: slide-in ease 0.3s;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .loading-text,
 | 
					 | 
				
			||||||
    .empty-text {
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      color: var(--black);
 | 
					 | 
				
			||||||
      opacity: 0.5;
 | 
					 | 
				
			||||||
      text-align: center;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .mcp-market-filter {
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      max-width: 100%;
 | 
					 | 
				
			||||||
      margin-bottom: 20px;
 | 
					 | 
				
			||||||
      animation: slide-in ease 0.3s;
 | 
					 | 
				
			||||||
      height: 40px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .search-bar {
 | 
					 | 
				
			||||||
        flex-grow: 1;
 | 
					 | 
				
			||||||
        max-width: 100%;
 | 
					 | 
				
			||||||
        min-width: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .server-list {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      flex-direction: column;
 | 
					 | 
				
			||||||
      gap: 1px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .mcp-market-item {
 | 
					 | 
				
			||||||
      padding: 20px;
 | 
					 | 
				
			||||||
      border: var(--border-in-light);
 | 
					 | 
				
			||||||
      animation: slide-in ease 0.3s;
 | 
					 | 
				
			||||||
      background-color: var(--white);
 | 
					 | 
				
			||||||
      transition: all 0.3s ease;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &.disabled {
 | 
					 | 
				
			||||||
        opacity: 0.7;
 | 
					 | 
				
			||||||
        pointer-events: none;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:not(:last-child) {
 | 
					 | 
				
			||||||
        border-bottom: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:first-child {
 | 
					 | 
				
			||||||
        border-top-left-radius: 10px;
 | 
					 | 
				
			||||||
        border-top-right-radius: 10px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:last-child {
 | 
					 | 
				
			||||||
        border-bottom-left-radius: 10px;
 | 
					 | 
				
			||||||
        border-bottom-right-radius: 10px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &.loading {
 | 
					 | 
				
			||||||
        position: relative;
 | 
					 | 
				
			||||||
        &::after {
 | 
					 | 
				
			||||||
          content: "";
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          top: 0;
 | 
					 | 
				
			||||||
          left: 0;
 | 
					 | 
				
			||||||
          right: 0;
 | 
					 | 
				
			||||||
          bottom: 0;
 | 
					 | 
				
			||||||
          background: linear-gradient(
 | 
					 | 
				
			||||||
            90deg,
 | 
					 | 
				
			||||||
            transparent,
 | 
					 | 
				
			||||||
            rgba(255, 255, 255, 0.2),
 | 
					 | 
				
			||||||
            transparent
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          background-size: 200% 100%;
 | 
					 | 
				
			||||||
          animation: loading-pulse 1.5s infinite;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .operation-status {
 | 
					 | 
				
			||||||
        display: inline-flex;
 | 
					 | 
				
			||||||
        align-items: center;
 | 
					 | 
				
			||||||
        margin-left: 10px;
 | 
					 | 
				
			||||||
        padding: 2px 8px;
 | 
					 | 
				
			||||||
        border-radius: 4px;
 | 
					 | 
				
			||||||
        font-size: 12px;
 | 
					 | 
				
			||||||
        background-color: #16a34a;
 | 
					 | 
				
			||||||
        color: #fff;
 | 
					 | 
				
			||||||
        animation: pulse 1.5s infinite;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &[data-status="stopping"] {
 | 
					 | 
				
			||||||
          background-color: #9ca3af;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &[data-status="starting"] {
 | 
					 | 
				
			||||||
          background-color: #4ade80;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &[data-status="error"] {
 | 
					 | 
				
			||||||
          background-color: #f87171;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .mcp-market-header {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        justify-content: space-between;
 | 
					 | 
				
			||||||
        align-items: flex-start;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .mcp-market-title {
 | 
					 | 
				
			||||||
          flex-grow: 1;
 | 
					 | 
				
			||||||
          margin-right: 20px;
 | 
					 | 
				
			||||||
          max-width: calc(100% - 300px);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .mcp-market-name {
 | 
					 | 
				
			||||||
          font-size: 14px;
 | 
					 | 
				
			||||||
          font-weight: bold;
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          align-items: center;
 | 
					 | 
				
			||||||
          gap: 8px;
 | 
					 | 
				
			||||||
          margin-bottom: 8px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          .server-status {
 | 
					 | 
				
			||||||
            display: inline-flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            margin-left: 10px;
 | 
					 | 
				
			||||||
            padding: 2px 8px;
 | 
					 | 
				
			||||||
            border-radius: 4px;
 | 
					 | 
				
			||||||
            font-size: 12px;
 | 
					 | 
				
			||||||
            background-color: #22c55e;
 | 
					 | 
				
			||||||
            color: #fff;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            &.error {
 | 
					 | 
				
			||||||
              background-color: #ef4444;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            &.stopped {
 | 
					 | 
				
			||||||
              background-color: #6b7280;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            &.initializing {
 | 
					 | 
				
			||||||
              background-color: #f59e0b;
 | 
					 | 
				
			||||||
              animation: pulse 1.5s infinite;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .error-message {
 | 
					 | 
				
			||||||
              margin-left: 4px;
 | 
					 | 
				
			||||||
              font-size: 12px;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .repo-link {
 | 
					 | 
				
			||||||
          color: var(--primary);
 | 
					 | 
				
			||||||
          font-size: 12px;
 | 
					 | 
				
			||||||
          display: inline-flex;
 | 
					 | 
				
			||||||
          align-items: center;
 | 
					 | 
				
			||||||
          gap: 4px;
 | 
					 | 
				
			||||||
          text-decoration: none;
 | 
					 | 
				
			||||||
          opacity: 0.8;
 | 
					 | 
				
			||||||
          transition: opacity 0.2s;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &:hover {
 | 
					 | 
				
			||||||
            opacity: 1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          svg {
 | 
					 | 
				
			||||||
            width: 14px;
 | 
					 | 
				
			||||||
            height: 14px;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .tags-container {
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          gap: 4px;
 | 
					 | 
				
			||||||
          flex-wrap: wrap;
 | 
					 | 
				
			||||||
          margin-bottom: 8px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .tag {
 | 
					 | 
				
			||||||
          background: var(--gray);
 | 
					 | 
				
			||||||
          color: var(--black);
 | 
					 | 
				
			||||||
          padding: 2px 6px;
 | 
					 | 
				
			||||||
          border-radius: 4px;
 | 
					 | 
				
			||||||
          font-size: 10px;
 | 
					 | 
				
			||||||
          opacity: 0.8;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .mcp-market-info {
 | 
					 | 
				
			||||||
          color: var(--black);
 | 
					 | 
				
			||||||
          font-size: 12px;
 | 
					 | 
				
			||||||
          overflow: hidden;
 | 
					 | 
				
			||||||
          text-overflow: ellipsis;
 | 
					 | 
				
			||||||
          white-space: nowrap;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .mcp-market-actions {
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          gap: 12px;
 | 
					 | 
				
			||||||
          align-items: flex-start;
 | 
					 | 
				
			||||||
          flex-shrink: 0;
 | 
					 | 
				
			||||||
          min-width: 180px;
 | 
					 | 
				
			||||||
          justify-content: flex-end;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .array-input {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 12px;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    padding: 16px;
 | 
					 | 
				
			||||||
    border: 1px solid var(--gray-200);
 | 
					 | 
				
			||||||
    border-radius: 10px;
 | 
					 | 
				
			||||||
    background-color: var(--white);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .array-input-item {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      gap: 8px;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      padding: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      input {
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        padding: 8px 12px;
 | 
					 | 
				
			||||||
        background-color: var(--gray-50);
 | 
					 | 
				
			||||||
        border-radius: 6px;
 | 
					 | 
				
			||||||
        transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        font-size: 13px;
 | 
					 | 
				
			||||||
        border: 1px solid var(--gray-200);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:hover {
 | 
					 | 
				
			||||||
          background-color: var(--gray-100);
 | 
					 | 
				
			||||||
          border-color: var(--gray-300);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:focus {
 | 
					 | 
				
			||||||
          background-color: var(--white);
 | 
					 | 
				
			||||||
          border-color: var(--primary);
 | 
					 | 
				
			||||||
          outline: none;
 | 
					 | 
				
			||||||
          box-shadow: 0 0 0 2px var(--primary-10);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &::placeholder {
 | 
					 | 
				
			||||||
          color: var(--gray-300);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :global(.icon-button.add-path-button) {
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      background-color: var(--primary);
 | 
					 | 
				
			||||||
      color: white;
 | 
					 | 
				
			||||||
      padding: 8px 12px;
 | 
					 | 
				
			||||||
      border-radius: 6px;
 | 
					 | 
				
			||||||
      transition: all 0.3s ease;
 | 
					 | 
				
			||||||
      margin-top: 8px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      border: none;
 | 
					 | 
				
			||||||
      height: 36px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:hover {
 | 
					 | 
				
			||||||
        background-color: var(--primary-dark);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      svg {
 | 
					 | 
				
			||||||
        width: 16px;
 | 
					 | 
				
			||||||
        height: 16px;
 | 
					 | 
				
			||||||
        margin-right: 4px;
 | 
					 | 
				
			||||||
        filter: brightness(2);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .path-list {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 10px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .path-item {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      gap: 10px;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      input {
 | 
					 | 
				
			||||||
        flex: 1;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        max-width: 100%;
 | 
					 | 
				
			||||||
        padding: 10px;
 | 
					 | 
				
			||||||
        border: var(--border-in-light);
 | 
					 | 
				
			||||||
        border-radius: 10px;
 | 
					 | 
				
			||||||
        box-sizing: border-box;
 | 
					 | 
				
			||||||
        font-size: 14px;
 | 
					 | 
				
			||||||
        background-color: var(--white);
 | 
					 | 
				
			||||||
        color: var(--black);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:hover {
 | 
					 | 
				
			||||||
          border-color: var(--gray-300);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:focus {
 | 
					 | 
				
			||||||
          border-color: var(--primary);
 | 
					 | 
				
			||||||
          outline: none;
 | 
					 | 
				
			||||||
          box-shadow: 0 0 0 2px var(--primary-10);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .browse-button {
 | 
					 | 
				
			||||||
        padding: 8px;
 | 
					 | 
				
			||||||
        border: var(--border-in-light);
 | 
					 | 
				
			||||||
        border-radius: 10px;
 | 
					 | 
				
			||||||
        background-color: transparent;
 | 
					 | 
				
			||||||
        color: var(--black-50);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:hover {
 | 
					 | 
				
			||||||
          border-color: var(--primary);
 | 
					 | 
				
			||||||
          color: var(--primary);
 | 
					 | 
				
			||||||
          background-color: transparent;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        svg {
 | 
					 | 
				
			||||||
          width: 16px;
 | 
					 | 
				
			||||||
          height: 16px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .delete-button {
 | 
					 | 
				
			||||||
        padding: 8px;
 | 
					 | 
				
			||||||
        border: var(--border-in-light);
 | 
					 | 
				
			||||||
        border-radius: 10px;
 | 
					 | 
				
			||||||
        background-color: transparent;
 | 
					 | 
				
			||||||
        color: var(--black-50);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:hover {
 | 
					 | 
				
			||||||
          border-color: var(--danger);
 | 
					 | 
				
			||||||
          color: var(--danger);
 | 
					 | 
				
			||||||
          background-color: transparent;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        svg {
 | 
					 | 
				
			||||||
          width: 16px;
 | 
					 | 
				
			||||||
          height: 16px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .file-input {
 | 
					 | 
				
			||||||
        display: none;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .add-button {
 | 
					 | 
				
			||||||
      align-self: flex-start;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      gap: 5px;
 | 
					 | 
				
			||||||
      padding: 8px 12px;
 | 
					 | 
				
			||||||
      background-color: transparent;
 | 
					 | 
				
			||||||
      border: var(--border-in-light);
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      color: var(--black);
 | 
					 | 
				
			||||||
      font-size: 12px;
 | 
					 | 
				
			||||||
      margin-top: 5px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:hover {
 | 
					 | 
				
			||||||
        border-color: var(--primary);
 | 
					 | 
				
			||||||
        color: var(--primary);
 | 
					 | 
				
			||||||
        background-color: transparent;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      svg {
 | 
					 | 
				
			||||||
        width: 16px;
 | 
					 | 
				
			||||||
        height: 16px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .config-section {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .config-header {
 | 
					 | 
				
			||||||
      margin-bottom: 12px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .config-title {
 | 
					 | 
				
			||||||
        font-size: 14px;
 | 
					 | 
				
			||||||
        font-weight: 600;
 | 
					 | 
				
			||||||
        color: var(--black);
 | 
					 | 
				
			||||||
        text-transform: capitalize;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .config-description {
 | 
					 | 
				
			||||||
        font-size: 12px;
 | 
					 | 
				
			||||||
        color: var(--gray-500);
 | 
					 | 
				
			||||||
        margin-top: 4px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .array-input {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      flex-direction: column;
 | 
					 | 
				
			||||||
      gap: 12px;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      padding: 16px;
 | 
					 | 
				
			||||||
      border: 1px solid var(--gray-200);
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      background-color: var(--white);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .array-input-item {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        gap: 8px;
 | 
					 | 
				
			||||||
        align-items: center;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        padding: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        input {
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
          padding: 8px 12px;
 | 
					 | 
				
			||||||
          background-color: var(--gray-50);
 | 
					 | 
				
			||||||
          border-radius: 6px;
 | 
					 | 
				
			||||||
          transition: all 0.3s ease;
 | 
					 | 
				
			||||||
          font-size: 13px;
 | 
					 | 
				
			||||||
          border: 1px solid var(--gray-200);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &:hover {
 | 
					 | 
				
			||||||
            background-color: var(--gray-100);
 | 
					 | 
				
			||||||
            border-color: var(--gray-300);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &:focus {
 | 
					 | 
				
			||||||
            background-color: var(--white);
 | 
					 | 
				
			||||||
            border-color: var(--primary);
 | 
					 | 
				
			||||||
            outline: none;
 | 
					 | 
				
			||||||
            box-shadow: 0 0 0 2px var(--primary-10);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &::placeholder {
 | 
					 | 
				
			||||||
            color: var(--gray-300);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :global(.icon-button) {
 | 
					 | 
				
			||||||
          width: 32px;
 | 
					 | 
				
			||||||
          height: 32px;
 | 
					 | 
				
			||||||
          padding: 0;
 | 
					 | 
				
			||||||
          border-radius: 6px;
 | 
					 | 
				
			||||||
          background-color: transparent;
 | 
					 | 
				
			||||||
          border: 1px solid var(--gray-200);
 | 
					 | 
				
			||||||
          flex-shrink: 0;
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          align-items: center;
 | 
					 | 
				
			||||||
          justify-content: center;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &:hover {
 | 
					 | 
				
			||||||
            background-color: var(--gray-100);
 | 
					 | 
				
			||||||
            border-color: var(--gray-300);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          svg {
 | 
					 | 
				
			||||||
            width: 16px;
 | 
					 | 
				
			||||||
            height: 16px;
 | 
					 | 
				
			||||||
            opacity: 0.7;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      :global(.icon-button.add-path-button) {
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        background-color: var(--primary);
 | 
					 | 
				
			||||||
        color: white;
 | 
					 | 
				
			||||||
        padding: 8px 12px;
 | 
					 | 
				
			||||||
        border-radius: 6px;
 | 
					 | 
				
			||||||
        transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        margin-top: 8px;
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        align-items: center;
 | 
					 | 
				
			||||||
        justify-content: center;
 | 
					 | 
				
			||||||
        border: none;
 | 
					 | 
				
			||||||
        height: 36px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        &:hover {
 | 
					 | 
				
			||||||
          background-color: var(--primary-dark);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        svg {
 | 
					 | 
				
			||||||
          width: 16px;
 | 
					 | 
				
			||||||
          height: 16px;
 | 
					 | 
				
			||||||
          margin-right: 4px;
 | 
					 | 
				
			||||||
          filter: brightness(2);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .input-item {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    input {
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      padding: 10px;
 | 
					 | 
				
			||||||
      border: var(--border-in-light);
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      box-sizing: border-box;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      background-color: var(--white);
 | 
					 | 
				
			||||||
      color: var(--black);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:hover {
 | 
					 | 
				
			||||||
        border-color: var(--gray-300);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:focus {
 | 
					 | 
				
			||||||
        border-color: var(--primary);
 | 
					 | 
				
			||||||
        outline: none;
 | 
					 | 
				
			||||||
        box-shadow: 0 0 0 2px var(--primary-10);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &::placeholder {
 | 
					 | 
				
			||||||
        color: var(--gray-300) !important;
 | 
					 | 
				
			||||||
        opacity: 1;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .tools-list {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 16px;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    padding: 20px;
 | 
					 | 
				
			||||||
    max-width: 100%;
 | 
					 | 
				
			||||||
    overflow-x: hidden;
 | 
					 | 
				
			||||||
    word-break: break-word;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .tool-item {
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      box-sizing: border-box;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .tool-name {
 | 
					 | 
				
			||||||
        font-size: 14px;
 | 
					 | 
				
			||||||
        font-weight: 600;
 | 
					 | 
				
			||||||
        color: var(--black);
 | 
					 | 
				
			||||||
        margin-bottom: 8px;
 | 
					 | 
				
			||||||
        padding-left: 12px;
 | 
					 | 
				
			||||||
        border-left: 3px solid var(--primary);
 | 
					 | 
				
			||||||
        box-sizing: border-box;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .tool-description {
 | 
					 | 
				
			||||||
        font-size: 13px;
 | 
					 | 
				
			||||||
        color: var(--gray-500);
 | 
					 | 
				
			||||||
        line-height: 1.6;
 | 
					 | 
				
			||||||
        padding-left: 15px;
 | 
					 | 
				
			||||||
        box-sizing: border-box;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  :global {
 | 
					 | 
				
			||||||
    .modal-content {
 | 
					 | 
				
			||||||
      margin-top: 20px;
 | 
					 | 
				
			||||||
      max-width: 100%;
 | 
					 | 
				
			||||||
      overflow-x: hidden;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .list {
 | 
					 | 
				
			||||||
      padding: 10px;
 | 
					 | 
				
			||||||
      margin-bottom: 10px;
 | 
					 | 
				
			||||||
      background-color: var(--white);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .list-item {
 | 
					 | 
				
			||||||
      border: none;
 | 
					 | 
				
			||||||
      background-color: transparent;
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      padding: 10px;
 | 
					 | 
				
			||||||
      margin-bottom: 10px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      flex-direction: column;
 | 
					 | 
				
			||||||
      gap: 10px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .list-header {
 | 
					 | 
				
			||||||
        margin-bottom: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .list-title {
 | 
					 | 
				
			||||||
          font-size: 14px;
 | 
					 | 
				
			||||||
          font-weight: bold;
 | 
					 | 
				
			||||||
          text-transform: capitalize;
 | 
					 | 
				
			||||||
          color: var(--black);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .list-sub-title {
 | 
					 | 
				
			||||||
          font-size: 12px;
 | 
					 | 
				
			||||||
          color: var(--gray-500);
 | 
					 | 
				
			||||||
          margin-top: 4px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes loading-pulse {
 | 
					 | 
				
			||||||
  0% {
 | 
					 | 
				
			||||||
    background-position: 200% 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  100% {
 | 
					 | 
				
			||||||
    background-position: -200% 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes pulse {
 | 
					 | 
				
			||||||
  0% {
 | 
					 | 
				
			||||||
    opacity: 0.6;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  50% {
 | 
					 | 
				
			||||||
    opacity: 1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  100% {
 | 
					 | 
				
			||||||
    opacity: 0.6;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,755 +0,0 @@
 | 
				
			|||||||
import { IconButton } from "./button";
 | 
					 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					 | 
				
			||||||
import styles from "./mcp-market.module.scss";
 | 
					 | 
				
			||||||
import EditIcon from "../icons/edit.svg";
 | 
					 | 
				
			||||||
import AddIcon from "../icons/add.svg";
 | 
					 | 
				
			||||||
import CloseIcon from "../icons/close.svg";
 | 
					 | 
				
			||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					 | 
				
			||||||
import RestartIcon from "../icons/reload.svg";
 | 
					 | 
				
			||||||
import EyeIcon from "../icons/eye.svg";
 | 
					 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					 | 
				
			||||||
import { List, ListItem, Modal, showToast } from "./ui-lib";
 | 
					 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  addMcpServer,
 | 
					 | 
				
			||||||
  getClientsStatus,
 | 
					 | 
				
			||||||
  getClientTools,
 | 
					 | 
				
			||||||
  getMcpConfigFromFile,
 | 
					 | 
				
			||||||
  isMcpEnabled,
 | 
					 | 
				
			||||||
  pauseMcpServer,
 | 
					 | 
				
			||||||
  restartAllClients,
 | 
					 | 
				
			||||||
  resumeMcpServer,
 | 
					 | 
				
			||||||
} from "../mcp/actions";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  ListToolsResponse,
 | 
					 | 
				
			||||||
  McpConfigData,
 | 
					 | 
				
			||||||
  PresetServer,
 | 
					 | 
				
			||||||
  ServerConfig,
 | 
					 | 
				
			||||||
  ServerStatusResponse,
 | 
					 | 
				
			||||||
} from "../mcp/types";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
import PlayIcon from "../icons/play.svg";
 | 
					 | 
				
			||||||
import StopIcon from "../icons/pause.svg";
 | 
					 | 
				
			||||||
import { Path } from "../constant";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ConfigProperty {
 | 
					 | 
				
			||||||
  type: string;
 | 
					 | 
				
			||||||
  description?: string;
 | 
					 | 
				
			||||||
  required?: boolean;
 | 
					 | 
				
			||||||
  minItems?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function McpMarketPage() {
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
  const [mcpEnabled, setMcpEnabled] = useState(false);
 | 
					 | 
				
			||||||
  const [searchText, setSearchText] = useState("");
 | 
					 | 
				
			||||||
  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
 | 
					 | 
				
			||||||
  const [editingServerId, setEditingServerId] = useState<string | undefined>();
 | 
					 | 
				
			||||||
  const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
 | 
					 | 
				
			||||||
  const [viewingServerId, setViewingServerId] = useState<string | undefined>();
 | 
					 | 
				
			||||||
  const [isLoading, setIsLoading] = useState(false);
 | 
					 | 
				
			||||||
  const [config, setConfig] = useState<McpConfigData>();
 | 
					 | 
				
			||||||
  const [clientStatuses, setClientStatuses] = useState<
 | 
					 | 
				
			||||||
    Record<string, ServerStatusResponse>
 | 
					 | 
				
			||||||
  >({});
 | 
					 | 
				
			||||||
  const [loadingPresets, setLoadingPresets] = useState(true);
 | 
					 | 
				
			||||||
  const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
 | 
					 | 
				
			||||||
  const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
 | 
					 | 
				
			||||||
    {},
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 检查 MCP 是否启用
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const checkMcpStatus = async () => {
 | 
					 | 
				
			||||||
      const enabled = await isMcpEnabled();
 | 
					 | 
				
			||||||
      setMcpEnabled(enabled);
 | 
					 | 
				
			||||||
      if (!enabled) {
 | 
					 | 
				
			||||||
        navigate(Path.Home);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    checkMcpStatus();
 | 
					 | 
				
			||||||
  }, [navigate]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 添加状态轮询
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (!mcpEnabled || !config) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateStatuses = async () => {
 | 
					 | 
				
			||||||
      const statuses = await getClientsStatus();
 | 
					 | 
				
			||||||
      setClientStatuses(statuses);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 立即执行一次
 | 
					 | 
				
			||||||
    updateStatuses();
 | 
					 | 
				
			||||||
    // 每 1000ms 轮询一次
 | 
					 | 
				
			||||||
    const timer = setInterval(updateStatuses, 1000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => clearInterval(timer);
 | 
					 | 
				
			||||||
  }, [mcpEnabled, config]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 加载预设服务器
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const loadPresetServers = async () => {
 | 
					 | 
				
			||||||
      if (!mcpEnabled) return;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        setLoadingPresets(true);
 | 
					 | 
				
			||||||
        const response = await fetch("https://nextchat.club/mcp/list");
 | 
					 | 
				
			||||||
        if (!response.ok) {
 | 
					 | 
				
			||||||
          throw new Error("Failed to load preset servers");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const data = await response.json();
 | 
					 | 
				
			||||||
        setPresetServers(data?.data ?? []);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Failed to load preset servers:", error);
 | 
					 | 
				
			||||||
        showToast("Failed to load preset servers");
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        setLoadingPresets(false);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    loadPresetServers();
 | 
					 | 
				
			||||||
  }, [mcpEnabled]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 加载初始状态
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const loadInitialState = async () => {
 | 
					 | 
				
			||||||
      if (!mcpEnabled) return;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        setIsLoading(true);
 | 
					 | 
				
			||||||
        const config = await getMcpConfigFromFile();
 | 
					 | 
				
			||||||
        setConfig(config);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 获取所有客户端的状态
 | 
					 | 
				
			||||||
        const statuses = await getClientsStatus();
 | 
					 | 
				
			||||||
        setClientStatuses(statuses);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Failed to load initial state:", error);
 | 
					 | 
				
			||||||
        showToast("Failed to load initial state");
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        setIsLoading(false);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    loadInitialState();
 | 
					 | 
				
			||||||
  }, [mcpEnabled]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 加载当前编辑服务器的配置
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (!editingServerId || !config) return;
 | 
					 | 
				
			||||||
    const currentConfig = config.mcpServers[editingServerId];
 | 
					 | 
				
			||||||
    if (currentConfig) {
 | 
					 | 
				
			||||||
      // 从当前配置中提取用户配置
 | 
					 | 
				
			||||||
      const preset = presetServers.find((s) => s.id === editingServerId);
 | 
					 | 
				
			||||||
      if (preset?.configSchema) {
 | 
					 | 
				
			||||||
        const userConfig: Record<string, any> = {};
 | 
					 | 
				
			||||||
        Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
					 | 
				
			||||||
          if (mapping.type === "spread") {
 | 
					 | 
				
			||||||
            // For spread types, extract the array from args.
 | 
					 | 
				
			||||||
            const startPos = mapping.position ?? 0;
 | 
					 | 
				
			||||||
            userConfig[key] = currentConfig.args.slice(startPos);
 | 
					 | 
				
			||||||
          } else if (mapping.type === "single") {
 | 
					 | 
				
			||||||
            // For single types, get a single value
 | 
					 | 
				
			||||||
            userConfig[key] = currentConfig.args[mapping.position ?? 0];
 | 
					 | 
				
			||||||
          } else if (
 | 
					 | 
				
			||||||
            mapping.type === "env" &&
 | 
					 | 
				
			||||||
            mapping.key &&
 | 
					 | 
				
			||||||
            currentConfig.env
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            // For env types, get values from environment variables
 | 
					 | 
				
			||||||
            userConfig[key] = currentConfig.env[mapping.key];
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        setUserConfig(userConfig);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      setUserConfig({});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [editingServerId, config, presetServers]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!mcpEnabled) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 检查服务器是否已添加
 | 
					 | 
				
			||||||
  const isServerAdded = (id: string) => {
 | 
					 | 
				
			||||||
    return id in (config?.mcpServers ?? {});
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 保存服务器配置
 | 
					 | 
				
			||||||
  const saveServerConfig = async () => {
 | 
					 | 
				
			||||||
    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
					 | 
				
			||||||
    if (!preset || !preset.configSchema || !editingServerId) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const savingServerId = editingServerId;
 | 
					 | 
				
			||||||
    setEditingServerId(undefined);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      updateLoadingState(savingServerId, "Updating configuration...");
 | 
					 | 
				
			||||||
      // 构建服务器配置
 | 
					 | 
				
			||||||
      const args = [...preset.baseArgs];
 | 
					 | 
				
			||||||
      const env: Record<string, string> = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
					 | 
				
			||||||
        const value = userConfig[key];
 | 
					 | 
				
			||||||
        if (mapping.type === "spread" && Array.isArray(value)) {
 | 
					 | 
				
			||||||
          const pos = mapping.position ?? 0;
 | 
					 | 
				
			||||||
          args.splice(pos, 0, ...value);
 | 
					 | 
				
			||||||
        } else if (
 | 
					 | 
				
			||||||
          mapping.type === "single" &&
 | 
					 | 
				
			||||||
          mapping.position !== undefined
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          args[mapping.position] = value;
 | 
					 | 
				
			||||||
        } else if (
 | 
					 | 
				
			||||||
          mapping.type === "env" &&
 | 
					 | 
				
			||||||
          mapping.key &&
 | 
					 | 
				
			||||||
          typeof value === "string"
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          env[mapping.key] = value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const serverConfig: ServerConfig = {
 | 
					 | 
				
			||||||
        command: preset.command,
 | 
					 | 
				
			||||||
        args,
 | 
					 | 
				
			||||||
        ...(Object.keys(env).length > 0 ? { env } : {}),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const newConfig = await addMcpServer(savingServerId, serverConfig);
 | 
					 | 
				
			||||||
      setConfig(newConfig);
 | 
					 | 
				
			||||||
      showToast("Server configuration updated successfully");
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      showToast(
 | 
					 | 
				
			||||||
        error instanceof Error ? error.message : "Failed to save configuration",
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      updateLoadingState(savingServerId, null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 获取服务器支持的 Tools
 | 
					 | 
				
			||||||
  const loadTools = async (id: string) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const result = await getClientTools(id);
 | 
					 | 
				
			||||||
      if (result) {
 | 
					 | 
				
			||||||
        setTools(result);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw new Error("Failed to load tools");
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      showToast("Failed to load tools");
 | 
					 | 
				
			||||||
      console.error(error);
 | 
					 | 
				
			||||||
      setTools(null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 更新加载状态的辅助函数
 | 
					 | 
				
			||||||
  const updateLoadingState = (id: string, message: string | null) => {
 | 
					 | 
				
			||||||
    setLoadingStates((prev) => {
 | 
					 | 
				
			||||||
      if (message === null) {
 | 
					 | 
				
			||||||
        const { [id]: _, ...rest } = prev;
 | 
					 | 
				
			||||||
        return rest;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return { ...prev, [id]: message };
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 修改添加服务器函数
 | 
					 | 
				
			||||||
  const addServer = async (preset: PresetServer) => {
 | 
					 | 
				
			||||||
    if (!preset.configurable) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const serverId = preset.id;
 | 
					 | 
				
			||||||
        updateLoadingState(serverId, "Creating MCP client...");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const serverConfig: ServerConfig = {
 | 
					 | 
				
			||||||
          command: preset.command,
 | 
					 | 
				
			||||||
          args: [...preset.baseArgs],
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        const newConfig = await addMcpServer(preset.id, serverConfig);
 | 
					 | 
				
			||||||
        setConfig(newConfig);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 更新状态
 | 
					 | 
				
			||||||
        const statuses = await getClientsStatus();
 | 
					 | 
				
			||||||
        setClientStatuses(statuses);
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        updateLoadingState(preset.id, null);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // 如果需要配置,打开配置对话框
 | 
					 | 
				
			||||||
      setEditingServerId(preset.id);
 | 
					 | 
				
			||||||
      setUserConfig({});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 修改暂停服务器函数
 | 
					 | 
				
			||||||
  const pauseServer = async (id: string) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      updateLoadingState(id, "Stopping server...");
 | 
					 | 
				
			||||||
      const newConfig = await pauseMcpServer(id);
 | 
					 | 
				
			||||||
      setConfig(newConfig);
 | 
					 | 
				
			||||||
      showToast("Server stopped successfully");
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      showToast("Failed to stop server");
 | 
					 | 
				
			||||||
      console.error(error);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      updateLoadingState(id, null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Restart server
 | 
					 | 
				
			||||||
  const restartServer = async (id: string) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      updateLoadingState(id, "Starting server...");
 | 
					 | 
				
			||||||
      await resumeMcpServer(id);
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      showToast(
 | 
					 | 
				
			||||||
        error instanceof Error
 | 
					 | 
				
			||||||
          ? error.message
 | 
					 | 
				
			||||||
          : "Failed to start server, please check logs",
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      console.error(error);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      updateLoadingState(id, null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Restart all clients
 | 
					 | 
				
			||||||
  const handleRestartAll = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      updateLoadingState("all", "Restarting all servers...");
 | 
					 | 
				
			||||||
      const newConfig = await restartAllClients();
 | 
					 | 
				
			||||||
      setConfig(newConfig);
 | 
					 | 
				
			||||||
      showToast("Restarting all clients");
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      showToast("Failed to restart clients");
 | 
					 | 
				
			||||||
      console.error(error);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      updateLoadingState("all", null);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Render configuration form
 | 
					 | 
				
			||||||
  const renderConfigForm = () => {
 | 
					 | 
				
			||||||
    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
					 | 
				
			||||||
    if (!preset?.configSchema) return null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Object.entries(preset.configSchema.properties).map(
 | 
					 | 
				
			||||||
      ([key, prop]: [string, ConfigProperty]) => {
 | 
					 | 
				
			||||||
        if (prop.type === "array") {
 | 
					 | 
				
			||||||
          const currentValue = userConfig[key as keyof typeof userConfig] || [];
 | 
					 | 
				
			||||||
          const itemLabel = (prop as any).itemLabel || key;
 | 
					 | 
				
			||||||
          const addButtonText =
 | 
					 | 
				
			||||||
            (prop as any).addButtonText || `Add ${itemLabel}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <ListItem
 | 
					 | 
				
			||||||
              key={key}
 | 
					 | 
				
			||||||
              title={key}
 | 
					 | 
				
			||||||
              subTitle={prop.description}
 | 
					 | 
				
			||||||
              vertical
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <div className={styles["path-list"]}>
 | 
					 | 
				
			||||||
                {(currentValue as string[]).map(
 | 
					 | 
				
			||||||
                  (value: string, index: number) => (
 | 
					 | 
				
			||||||
                    <div key={index} className={styles["path-item"]}>
 | 
					 | 
				
			||||||
                      <input
 | 
					 | 
				
			||||||
                        type="text"
 | 
					 | 
				
			||||||
                        value={value}
 | 
					 | 
				
			||||||
                        placeholder={`${itemLabel} ${index + 1}`}
 | 
					 | 
				
			||||||
                        onChange={(e) => {
 | 
					 | 
				
			||||||
                          const newValue = [...currentValue] as string[];
 | 
					 | 
				
			||||||
                          newValue[index] = e.target.value;
 | 
					 | 
				
			||||||
                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
					 | 
				
			||||||
                        }}
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                      <IconButton
 | 
					 | 
				
			||||||
                        icon={<DeleteIcon />}
 | 
					 | 
				
			||||||
                        className={styles["delete-button"]}
 | 
					 | 
				
			||||||
                        onClick={() => {
 | 
					 | 
				
			||||||
                          const newValue = [...currentValue] as string[];
 | 
					 | 
				
			||||||
                          newValue.splice(index, 1);
 | 
					 | 
				
			||||||
                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
					 | 
				
			||||||
                        }}
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  icon={<AddIcon />}
 | 
					 | 
				
			||||||
                  text={addButtonText}
 | 
					 | 
				
			||||||
                  className={styles["add-button"]}
 | 
					 | 
				
			||||||
                  bordered
 | 
					 | 
				
			||||||
                  onClick={() => {
 | 
					 | 
				
			||||||
                    const newValue = [...currentValue, ""] as string[];
 | 
					 | 
				
			||||||
                    setUserConfig({ ...userConfig, [key]: newValue });
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </ListItem>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        } else if (prop.type === "string") {
 | 
					 | 
				
			||||||
          const currentValue = userConfig[key as keyof typeof userConfig] || "";
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <ListItem key={key} title={key} subTitle={prop.description}>
 | 
					 | 
				
			||||||
              <input
 | 
					 | 
				
			||||||
                aria-label={key}
 | 
					 | 
				
			||||||
                type="text"
 | 
					 | 
				
			||||||
                value={currentValue}
 | 
					 | 
				
			||||||
                placeholder={`Enter ${key}`}
 | 
					 | 
				
			||||||
                onChange={(e) => {
 | 
					 | 
				
			||||||
                  setUserConfig({ ...userConfig, [key]: e.target.value });
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </ListItem>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return null;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const checkServerStatus = (clientId: string) => {
 | 
					 | 
				
			||||||
    return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getServerStatusDisplay = (clientId: string) => {
 | 
					 | 
				
			||||||
    const status = checkServerStatus(clientId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const statusMap = {
 | 
					 | 
				
			||||||
      undefined: null, // 未配置/未找到不显示
 | 
					 | 
				
			||||||
      // 添加初始化状态
 | 
					 | 
				
			||||||
      initializing: (
 | 
					 | 
				
			||||||
        <span className={clsx(styles["server-status"], styles["initializing"])}>
 | 
					 | 
				
			||||||
          Initializing
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      paused: (
 | 
					 | 
				
			||||||
        <span className={clsx(styles["server-status"], styles["stopped"])}>
 | 
					 | 
				
			||||||
          Stopped
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      active: <span className={styles["server-status"]}>Running</span>,
 | 
					 | 
				
			||||||
      error: (
 | 
					 | 
				
			||||||
        <span className={clsx(styles["server-status"], styles["error"])}>
 | 
					 | 
				
			||||||
          Error
 | 
					 | 
				
			||||||
          <span className={styles["error-message"]}>: {status.errorMsg}</span>
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return statusMap[status.status];
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Get the type of operation status
 | 
					 | 
				
			||||||
  const getOperationStatusType = (message: string) => {
 | 
					 | 
				
			||||||
    if (message.toLowerCase().includes("stopping")) return "stopping";
 | 
					 | 
				
			||||||
    if (message.toLowerCase().includes("starting")) return "starting";
 | 
					 | 
				
			||||||
    if (message.toLowerCase().includes("error")) return "error";
 | 
					 | 
				
			||||||
    return "default";
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 渲染服务器列表
 | 
					 | 
				
			||||||
  const renderServerList = () => {
 | 
					 | 
				
			||||||
    if (loadingPresets) {
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        <div className={styles["loading-container"]}>
 | 
					 | 
				
			||||||
          <div className={styles["loading-text"]}>
 | 
					 | 
				
			||||||
            Loading preset server list...
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!Array.isArray(presetServers) || presetServers.length === 0) {
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        <div className={styles["empty-container"]}>
 | 
					 | 
				
			||||||
          <div className={styles["empty-text"]}>No servers available</div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return presetServers
 | 
					 | 
				
			||||||
      .filter((server) => {
 | 
					 | 
				
			||||||
        if (searchText.length === 0) return true;
 | 
					 | 
				
			||||||
        const searchLower = searchText.toLowerCase();
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          server.name.toLowerCase().includes(searchLower) ||
 | 
					 | 
				
			||||||
          server.description.toLowerCase().includes(searchLower) ||
 | 
					 | 
				
			||||||
          server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .sort((a, b) => {
 | 
					 | 
				
			||||||
        const aStatus = checkServerStatus(a.id).status;
 | 
					 | 
				
			||||||
        const bStatus = checkServerStatus(b.id).status;
 | 
					 | 
				
			||||||
        const aLoading = loadingStates[a.id];
 | 
					 | 
				
			||||||
        const bLoading = loadingStates[b.id];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 定义状态优先级
 | 
					 | 
				
			||||||
        const statusPriority: Record<string, number> = {
 | 
					 | 
				
			||||||
          error: 0, // Highest priority for error status
 | 
					 | 
				
			||||||
          active: 1, // Second for active
 | 
					 | 
				
			||||||
          initializing: 2, // Initializing
 | 
					 | 
				
			||||||
          starting: 3, // Starting
 | 
					 | 
				
			||||||
          stopping: 4, // Stopping
 | 
					 | 
				
			||||||
          paused: 5, // Paused
 | 
					 | 
				
			||||||
          undefined: 6, // Lowest priority for undefined
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Get actual status (including loading status)
 | 
					 | 
				
			||||||
        const getEffectiveStatus = (status: string, loading?: string) => {
 | 
					 | 
				
			||||||
          if (loading) {
 | 
					 | 
				
			||||||
            const operationType = getOperationStatusType(loading);
 | 
					 | 
				
			||||||
            return operationType === "default" ? status : operationType;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (status === "initializing" && !loading) {
 | 
					 | 
				
			||||||
            return "active";
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return status;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
 | 
					 | 
				
			||||||
        const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 首先按状态排序
 | 
					 | 
				
			||||||
        if (aEffectiveStatus !== bEffectiveStatus) {
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            (statusPriority[aEffectiveStatus] ?? 6) -
 | 
					 | 
				
			||||||
            (statusPriority[bEffectiveStatus] ?? 6)
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Sort by name when statuses are the same
 | 
					 | 
				
			||||||
        return a.name.localeCompare(b.name);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .map((server) => (
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          className={clsx(styles["mcp-market-item"], {
 | 
					 | 
				
			||||||
            [styles["loading"]]: loadingStates[server.id],
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
          key={server.id}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <div className={styles["mcp-market-header"]}>
 | 
					 | 
				
			||||||
            <div className={styles["mcp-market-title"]}>
 | 
					 | 
				
			||||||
              <div className={styles["mcp-market-name"]}>
 | 
					 | 
				
			||||||
                {server.name}
 | 
					 | 
				
			||||||
                {loadingStates[server.id] && (
 | 
					 | 
				
			||||||
                  <span
 | 
					 | 
				
			||||||
                    className={styles["operation-status"]}
 | 
					 | 
				
			||||||
                    data-status={getOperationStatusType(
 | 
					 | 
				
			||||||
                      loadingStates[server.id],
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {loadingStates[server.id]}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
                {!loadingStates[server.id] && getServerStatusDisplay(server.id)}
 | 
					 | 
				
			||||||
                {server.repo && (
 | 
					 | 
				
			||||||
                  <a
 | 
					 | 
				
			||||||
                    href={server.repo}
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                    rel="noopener noreferrer"
 | 
					 | 
				
			||||||
                    className={styles["repo-link"]}
 | 
					 | 
				
			||||||
                    title="Open repository"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <GithubIcon />
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
              <div className={styles["tags-container"]}>
 | 
					 | 
				
			||||||
                {server.tags.map((tag, index) => (
 | 
					 | 
				
			||||||
                  <span key={index} className={styles["tag"]}>
 | 
					 | 
				
			||||||
                    {tag}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
              <div
 | 
					 | 
				
			||||||
                className={clsx(styles["mcp-market-info"], "one-line")}
 | 
					 | 
				
			||||||
                title={server.description}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                {server.description}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className={styles["mcp-market-actions"]}>
 | 
					 | 
				
			||||||
              {isServerAdded(server.id) ? (
 | 
					 | 
				
			||||||
                <>
 | 
					 | 
				
			||||||
                  {server.configurable && (
 | 
					 | 
				
			||||||
                    <IconButton
 | 
					 | 
				
			||||||
                      icon={<EditIcon />}
 | 
					 | 
				
			||||||
                      text="Configure"
 | 
					 | 
				
			||||||
                      onClick={() => setEditingServerId(server.id)}
 | 
					 | 
				
			||||||
                      disabled={isLoading}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  )}
 | 
					 | 
				
			||||||
                  {checkServerStatus(server.id).status === "paused" ? (
 | 
					 | 
				
			||||||
                    <>
 | 
					 | 
				
			||||||
                      <IconButton
 | 
					 | 
				
			||||||
                        icon={<PlayIcon />}
 | 
					 | 
				
			||||||
                        text="Start"
 | 
					 | 
				
			||||||
                        onClick={() => restartServer(server.id)}
 | 
					 | 
				
			||||||
                        disabled={isLoading}
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                      {/* <IconButton
 | 
					 | 
				
			||||||
                        icon={<DeleteIcon />}
 | 
					 | 
				
			||||||
                        text="Remove"
 | 
					 | 
				
			||||||
                        onClick={() => removeServer(server.id)}
 | 
					 | 
				
			||||||
                        disabled={isLoading}
 | 
					 | 
				
			||||||
                      /> */}
 | 
					 | 
				
			||||||
                    </>
 | 
					 | 
				
			||||||
                  ) : (
 | 
					 | 
				
			||||||
                    <>
 | 
					 | 
				
			||||||
                      <IconButton
 | 
					 | 
				
			||||||
                        icon={<EyeIcon />}
 | 
					 | 
				
			||||||
                        text="Tools"
 | 
					 | 
				
			||||||
                        onClick={async () => {
 | 
					 | 
				
			||||||
                          setViewingServerId(server.id);
 | 
					 | 
				
			||||||
                          await loadTools(server.id);
 | 
					 | 
				
			||||||
                        }}
 | 
					 | 
				
			||||||
                        disabled={
 | 
					 | 
				
			||||||
                          isLoading ||
 | 
					 | 
				
			||||||
                          checkServerStatus(server.id).status === "error"
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                      <IconButton
 | 
					 | 
				
			||||||
                        icon={<StopIcon />}
 | 
					 | 
				
			||||||
                        text="Stop"
 | 
					 | 
				
			||||||
                        onClick={() => pauseServer(server.id)}
 | 
					 | 
				
			||||||
                        disabled={isLoading}
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                    </>
 | 
					 | 
				
			||||||
                  )}
 | 
					 | 
				
			||||||
                </>
 | 
					 | 
				
			||||||
              ) : (
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  icon={<AddIcon />}
 | 
					 | 
				
			||||||
                  text="Add"
 | 
					 | 
				
			||||||
                  onClick={() => addServer(server)}
 | 
					 | 
				
			||||||
                  disabled={isLoading}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      ));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ErrorBoundary>
 | 
					 | 
				
			||||||
      <div className={styles["mcp-market-page"]}>
 | 
					 | 
				
			||||||
        <div className="window-header">
 | 
					 | 
				
			||||||
          <div className="window-header-title">
 | 
					 | 
				
			||||||
            <div className="window-header-main-title">
 | 
					 | 
				
			||||||
              MCP Market
 | 
					 | 
				
			||||||
              {loadingStates["all"] && (
 | 
					 | 
				
			||||||
                <span className={styles["loading-indicator"]}>
 | 
					 | 
				
			||||||
                  {loadingStates["all"]}
 | 
					 | 
				
			||||||
                </span>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className="window-header-sub-title">
 | 
					 | 
				
			||||||
              {Object.keys(config?.mcpServers ?? {}).length} servers configured
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className="window-actions">
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<RestartIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                onClick={handleRestartAll}
 | 
					 | 
				
			||||||
                text="Restart All"
 | 
					 | 
				
			||||||
                disabled={isLoading}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<CloseIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                onClick={() => navigate(-1)}
 | 
					 | 
				
			||||||
                disabled={isLoading}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className={styles["mcp-market-page-body"]}>
 | 
					 | 
				
			||||||
          <div className={styles["mcp-market-filter"]}>
 | 
					 | 
				
			||||||
            <input
 | 
					 | 
				
			||||||
              type="text"
 | 
					 | 
				
			||||||
              className={styles["search-bar"]}
 | 
					 | 
				
			||||||
              placeholder={"Search MCP Server"}
 | 
					 | 
				
			||||||
              autoFocus
 | 
					 | 
				
			||||||
              onInput={(e) => setSearchText(e.currentTarget.value)}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className={styles["server-list"]}>{renderServerList()}</div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {/*编辑服务器配置*/}
 | 
					 | 
				
			||||||
        {editingServerId && (
 | 
					 | 
				
			||||||
          <div className="modal-mask">
 | 
					 | 
				
			||||||
            <Modal
 | 
					 | 
				
			||||||
              title={`Configure Server - ${editingServerId}`}
 | 
					 | 
				
			||||||
              onClose={() => !isLoading && setEditingServerId(undefined)}
 | 
					 | 
				
			||||||
              actions={[
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  key="cancel"
 | 
					 | 
				
			||||||
                  text="Cancel"
 | 
					 | 
				
			||||||
                  onClick={() => setEditingServerId(undefined)}
 | 
					 | 
				
			||||||
                  bordered
 | 
					 | 
				
			||||||
                  disabled={isLoading}
 | 
					 | 
				
			||||||
                />,
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  key="confirm"
 | 
					 | 
				
			||||||
                  text="Save"
 | 
					 | 
				
			||||||
                  type="primary"
 | 
					 | 
				
			||||||
                  onClick={saveServerConfig}
 | 
					 | 
				
			||||||
                  bordered
 | 
					 | 
				
			||||||
                  disabled={isLoading}
 | 
					 | 
				
			||||||
                />,
 | 
					 | 
				
			||||||
              ]}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <List>{renderConfigForm()}</List>
 | 
					 | 
				
			||||||
            </Modal>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {viewingServerId && (
 | 
					 | 
				
			||||||
          <div className="modal-mask">
 | 
					 | 
				
			||||||
            <Modal
 | 
					 | 
				
			||||||
              title={`Server Details - ${viewingServerId}`}
 | 
					 | 
				
			||||||
              onClose={() => setViewingServerId(undefined)}
 | 
					 | 
				
			||||||
              actions={[
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  key="close"
 | 
					 | 
				
			||||||
                  text="Close"
 | 
					 | 
				
			||||||
                  onClick={() => setViewingServerId(undefined)}
 | 
					 | 
				
			||||||
                  bordered
 | 
					 | 
				
			||||||
                />,
 | 
					 | 
				
			||||||
              ]}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <div className={styles["tools-list"]}>
 | 
					 | 
				
			||||||
                {isLoading ? (
 | 
					 | 
				
			||||||
                  <div>Loading...</div>
 | 
					 | 
				
			||||||
                ) : tools?.tools ? (
 | 
					 | 
				
			||||||
                  tools.tools.map(
 | 
					 | 
				
			||||||
                    (tool: ListToolsResponse["tools"], index: number) => (
 | 
					 | 
				
			||||||
                      <div key={index} className={styles["tool-item"]}>
 | 
					 | 
				
			||||||
                        <div className={styles["tool-name"]}>{tool.name}</div>
 | 
					 | 
				
			||||||
                        <div className={styles["tool-description"]}>
 | 
					 | 
				
			||||||
                          {tool.description}
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  )
 | 
					 | 
				
			||||||
                ) : (
 | 
					 | 
				
			||||||
                  <div>No tools available</div>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </Modal>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </ErrorBoundary>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -7,8 +7,6 @@ import { MaskAvatar } from "./mask";
 | 
				
			|||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./message-selector.module.scss";
 | 
					import styles from "./message-selector.module.scss";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useShiftRange() {
 | 
					function useShiftRange() {
 | 
				
			||||||
  const [startIndex, setStartIndex] = useState<number>();
 | 
					  const [startIndex, setStartIndex] = useState<number>();
 | 
				
			||||||
@@ -72,7 +70,6 @@ export function MessageSelector(props: {
 | 
				
			|||||||
  defaultSelectAll?: boolean;
 | 
					  defaultSelectAll?: boolean;
 | 
				
			||||||
  onSelected?: (messages: ChatMessage[]) => void;
 | 
					  onSelected?: (messages: ChatMessage[]) => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const LATEST_COUNT = 4;
 | 
					 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
  const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
 | 
					  const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
 | 
				
			||||||
@@ -106,9 +103,7 @@ export function MessageSelector(props: {
 | 
				
			|||||||
    const searchResults = new Set<string>();
 | 
					    const searchResults = new Set<string>();
 | 
				
			||||||
    if (text.length > 0) {
 | 
					    if (text.length > 0) {
 | 
				
			||||||
      messages.forEach((m) =>
 | 
					      messages.forEach((m) =>
 | 
				
			||||||
        getMessageTextContent(m).includes(text)
 | 
					        m.content.includes(text) ? searchResults.add(m.id!) : null,
 | 
				
			||||||
          ? searchResults.add(m.id!)
 | 
					 | 
				
			||||||
          : null,
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    setSearchIds(searchResults);
 | 
					    setSearchIds(searchResults);
 | 
				
			||||||
@@ -143,13 +138,15 @@ export function MessageSelector(props: {
 | 
				
			|||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, [startIndex, endIndex]);
 | 
					  }, [startIndex, endIndex]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const LATEST_COUNT = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["message-selector"]}>
 | 
					    <div className={styles["message-selector"]}>
 | 
				
			||||||
      <div className={styles["message-filter"]}>
 | 
					      <div className={styles["message-filter"]}>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          type="text"
 | 
					          type="text"
 | 
				
			||||||
          placeholder={Locale.Select.Search}
 | 
					          placeholder={Locale.Select.Search}
 | 
				
			||||||
          className={clsx(styles["filter-item"], styles["search-bar"])}
 | 
					          className={styles["filter-item"] + " " + styles["search-bar"]}
 | 
				
			||||||
          value={searchInput}
 | 
					          value={searchInput}
 | 
				
			||||||
          onInput={(e) => {
 | 
					          onInput={(e) => {
 | 
				
			||||||
            setSearchInput(e.currentTarget.value);
 | 
					            setSearchInput(e.currentTarget.value);
 | 
				
			||||||
@@ -196,9 +193,9 @@ export function MessageSelector(props: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={clsx(styles["message"], {
 | 
					              className={`${styles["message"]} ${
 | 
				
			||||||
                [styles["message-selected"]]: props.selection.has(m.id!),
 | 
					                props.selection.has(m.id!) && styles["message-selected"]
 | 
				
			||||||
              })}
 | 
					              }`}
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                props.updateSelection((selection) => {
 | 
					                props.updateSelection((selection) => {
 | 
				
			||||||
@@ -221,13 +218,13 @@ export function MessageSelector(props: {
 | 
				
			|||||||
                <div className={styles["date"]}>
 | 
					                <div className={styles["date"]}>
 | 
				
			||||||
                  {new Date(m.date).toLocaleString()}
 | 
					                  {new Date(m.date).toLocaleString()}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className={clsx(styles["content"], "one-line")}>
 | 
					                <div className={`${styles["content"]} one-line`}>
 | 
				
			||||||
                  {getMessageTextContent(m)}
 | 
					                  {m.content}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div className={styles["checkbox"]}>
 | 
					              <div className={styles["checkbox"]}>
 | 
				
			||||||
                <input type="checkbox" checked={isSelected} readOnly></input>
 | 
					                <input type="checkbox" checked={isSelected}></input>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +0,0 @@
 | 
				
			|||||||
.select-compress-model {
 | 
					 | 
				
			||||||
  width: 60%;
 | 
					 | 
				
			||||||
  select {
 | 
					 | 
				
			||||||
    max-width: 100%;
 | 
					 | 
				
			||||||
    white-space: normal;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,52 +1,37 @@
 | 
				
			|||||||
import { ServiceProvider } from "@/app/constant";
 | 
					 | 
				
			||||||
import { ModalConfigValidator, ModelConfig } from "../store";
 | 
					import { ModalConfigValidator, ModelConfig } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { InputRange } from "./input-range";
 | 
					import { InputRange } from "./input-range";
 | 
				
			||||||
import { ListItem, Select } from "./ui-lib";
 | 
					import { ListItem, Select } from "./ui-lib";
 | 
				
			||||||
import { useAllModels } from "../utils/hooks";
 | 
					import { useAllModels } from "../utils/hooks";
 | 
				
			||||||
import { groupBy } from "lodash-es";
 | 
					 | 
				
			||||||
import styles from "./model-config.module.scss";
 | 
					 | 
				
			||||||
import { getModelProvider } from "../utils/model";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ModelConfigList(props: {
 | 
					export function ModelConfigList(props: {
 | 
				
			||||||
  modelConfig: ModelConfig;
 | 
					  modelConfig: ModelConfig;
 | 
				
			||||||
  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
					  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const allModels = useAllModels();
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
  const groupModels = groupBy(
 | 
					 | 
				
			||||||
    allModels.filter((v) => v.available),
 | 
					 | 
				
			||||||
    "provider.providerName",
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
 | 
					 | 
				
			||||||
  const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <ListItem title={Locale.Settings.Model}>
 | 
					      <ListItem title={Locale.Settings.Model}>
 | 
				
			||||||
        <Select
 | 
					        <Select
 | 
				
			||||||
          aria-label={Locale.Settings.Model}
 | 
					          value={props.modelConfig.model}
 | 
				
			||||||
          value={value}
 | 
					 | 
				
			||||||
          align="left"
 | 
					 | 
				
			||||||
          onChange={(e) => {
 | 
					          onChange={(e) => {
 | 
				
			||||||
            const [model, providerName] = getModelProvider(
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) =>
 | 
				
			||||||
 | 
					                (config.model = ModalConfigValidator.model(
 | 
				
			||||||
                  e.currentTarget.value,
 | 
					                  e.currentTarget.value,
 | 
				
			||||||
 | 
					                )),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            props.updateConfig((config) => {
 | 
					 | 
				
			||||||
              config.model = ModalConfigValidator.model(model);
 | 
					 | 
				
			||||||
              config.providerName = providerName as ServiceProvider;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {Object.keys(groupModels).map((providerName, index) => (
 | 
					          {allModels
 | 
				
			||||||
            <optgroup label={providerName} key={index}>
 | 
					            .filter((v) => v.available)
 | 
				
			||||||
              {groupModels[providerName].map((v, i) => (
 | 
					            .map((v, i) => (
 | 
				
			||||||
                <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
					              <option value={v.name} key={i}>
 | 
				
			||||||
                  {v.displayName}
 | 
					                {v.displayName}({v.provider?.providerName})
 | 
				
			||||||
              </option>
 | 
					              </option>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
            </optgroup>
 | 
					 | 
				
			||||||
          ))}
 | 
					 | 
				
			||||||
        </Select>
 | 
					        </Select>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
@@ -54,7 +39,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
					        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
          aria={Locale.Settings.Temperature.Title}
 | 
					 | 
				
			||||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
					          value={props.modelConfig.temperature?.toFixed(1)}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="1" // lets limit it to 0-1
 | 
					          max="1" // lets limit it to 0-1
 | 
				
			||||||
@@ -74,7 +58,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.TopP.SubTitle}
 | 
					        subTitle={Locale.Settings.TopP.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
          aria={Locale.Settings.TopP.Title}
 | 
					 | 
				
			||||||
          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
					          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="1"
 | 
					          max="1"
 | 
				
			||||||
@@ -94,7 +77,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
					        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          aria-label={Locale.Settings.MaxTokens.Title}
 | 
					 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={1024}
 | 
					          min={1024}
 | 
				
			||||||
          max={512000}
 | 
					          max={512000}
 | 
				
			||||||
@@ -110,14 +92,13 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        ></input>
 | 
					        ></input>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {props.modelConfig?.providerName == ServiceProvider.Google ? null : (
 | 
					      {props.modelConfig.model === "gemini-pro" ? null : (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <ListItem
 | 
					          <ListItem
 | 
				
			||||||
            title={Locale.Settings.PresencePenalty.Title}
 | 
					            title={Locale.Settings.PresencePenalty.Title}
 | 
				
			||||||
            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
					            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <InputRange
 | 
					            <InputRange
 | 
				
			||||||
              aria={Locale.Settings.PresencePenalty.Title}
 | 
					 | 
				
			||||||
              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
					              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
				
			||||||
              min="-2"
 | 
					              min="-2"
 | 
				
			||||||
              max="2"
 | 
					              max="2"
 | 
				
			||||||
@@ -139,7 +120,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
					            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <InputRange
 | 
					            <InputRange
 | 
				
			||||||
              aria={Locale.Settings.FrequencyPenalty.Title}
 | 
					 | 
				
			||||||
              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
					              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
				
			||||||
              min="-2"
 | 
					              min="-2"
 | 
				
			||||||
              max="2"
 | 
					              max="2"
 | 
				
			||||||
@@ -161,7 +141,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
					            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              aria-label={Locale.Settings.InjectSystemPrompts.Title}
 | 
					 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
					              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
				
			||||||
              onChange={(e) =>
 | 
					              onChange={(e) =>
 | 
				
			||||||
@@ -179,7 +158,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
					            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              aria-label={Locale.Settings.InputTemplate.Title}
 | 
					 | 
				
			||||||
              type="text"
 | 
					              type="text"
 | 
				
			||||||
              value={props.modelConfig.template}
 | 
					              value={props.modelConfig.template}
 | 
				
			||||||
              onChange={(e) =>
 | 
					              onChange={(e) =>
 | 
				
			||||||
@@ -196,7 +174,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
					        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
          aria={Locale.Settings.HistoryCount.Title}
 | 
					 | 
				
			||||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
					          title={props.modelConfig.historyMessageCount.toString()}
 | 
				
			||||||
          value={props.modelConfig.historyMessageCount}
 | 
					          value={props.modelConfig.historyMessageCount}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
@@ -215,7 +192,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
					        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          aria-label={Locale.Settings.CompressThreshold.Title}
 | 
					 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={500}
 | 
					          min={500}
 | 
				
			||||||
          max={4000}
 | 
					          max={4000}
 | 
				
			||||||
@@ -231,7 +207,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
					      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          aria-label={Locale.Memory.Title}
 | 
					 | 
				
			||||||
          type="checkbox"
 | 
					          type="checkbox"
 | 
				
			||||||
          checked={props.modelConfig.sendMemory}
 | 
					          checked={props.modelConfig.sendMemory}
 | 
				
			||||||
          onChange={(e) =>
 | 
					          onChange={(e) =>
 | 
				
			||||||
@@ -241,33 +216,6 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        ></input>
 | 
					        ></input>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem
 | 
					 | 
				
			||||||
        title={Locale.Settings.CompressModel.Title}
 | 
					 | 
				
			||||||
        subTitle={Locale.Settings.CompressModel.SubTitle}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Select
 | 
					 | 
				
			||||||
          className={styles["select-compress-model"]}
 | 
					 | 
				
			||||||
          aria-label={Locale.Settings.CompressModel.Title}
 | 
					 | 
				
			||||||
          value={compressModelValue}
 | 
					 | 
				
			||||||
          onChange={(e) => {
 | 
					 | 
				
			||||||
            const [model, providerName] = getModelProvider(
 | 
					 | 
				
			||||||
              e.currentTarget.value,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            props.updateConfig((config) => {
 | 
					 | 
				
			||||||
              config.compressModel = ModalConfigValidator.model(model);
 | 
					 | 
				
			||||||
              config.compressProviderName = providerName as ServiceProvider;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {allModels
 | 
					 | 
				
			||||||
            .filter((v) => v.available)
 | 
					 | 
				
			||||||
            .map((v, i) => (
 | 
					 | 
				
			||||||
              <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
					 | 
				
			||||||
                {v.displayName}({v.provider?.providerName})
 | 
					 | 
				
			||||||
              </option>
 | 
					 | 
				
			||||||
            ))}
 | 
					 | 
				
			||||||
        </Select>
 | 
					 | 
				
			||||||
      </ListItem>
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,6 @@ import { MaskAvatar } from "./mask";
 | 
				
			|||||||
import { useCommand } from "../command";
 | 
					import { useCommand } from "../command";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
					function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -25,9 +24,7 @@ function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			|||||||
        avatar={props.mask.avatar}
 | 
					        avatar={props.mask.avatar}
 | 
				
			||||||
        model={props.mask.modelConfig.model}
 | 
					        model={props.mask.modelConfig.model}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div className={clsx(styles["mask-name"], "one-line")}>
 | 
					      <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
 | 
				
			||||||
        {props.mask.name}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,38 +0,0 @@
 | 
				
			|||||||
.plugin-title {
 | 
					 | 
				
			||||||
  font-weight: bolder;
 | 
					 | 
				
			||||||
  font-size: 16px;
 | 
					 | 
				
			||||||
  margin: 10px 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.plugin-content {
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  font-family: inherit;
 | 
					 | 
				
			||||||
  pre code {
 | 
					 | 
				
			||||||
    max-height: 240px;
 | 
					 | 
				
			||||||
    overflow-y: auto;
 | 
					 | 
				
			||||||
    white-space: pre-wrap;
 | 
					 | 
				
			||||||
    min-width: 280px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.plugin-schema {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: flex-end;
 | 
					 | 
				
			||||||
  flex-direction: row;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  input {
 | 
					 | 
				
			||||||
    margin-right: 20px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @media screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
        margin-right: 0px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 5px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    button {
 | 
					 | 
				
			||||||
      padding: 10px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,370 +0,0 @@
 | 
				
			|||||||
import { useDebouncedCallback } from "use-debounce";
 | 
					 | 
				
			||||||
import OpenAPIClientAxios from "openapi-client-axios";
 | 
					 | 
				
			||||||
import yaml from "js-yaml";
 | 
					 | 
				
			||||||
import { PLUGINS_REPO_URL } from "../constant";
 | 
					 | 
				
			||||||
import { IconButton } from "./button";
 | 
					 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import styles from "./mask.module.scss";
 | 
					 | 
				
			||||||
import pluginStyles from "./plugin.module.scss";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import EditIcon from "../icons/edit.svg";
 | 
					 | 
				
			||||||
import AddIcon from "../icons/add.svg";
 | 
					 | 
				
			||||||
import CloseIcon from "../icons/close.svg";
 | 
					 | 
				
			||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					 | 
				
			||||||
import ConfirmIcon from "../icons/confirm.svg";
 | 
					 | 
				
			||||||
import ReloadIcon from "../icons/reload.svg";
 | 
					 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  PasswordInput,
 | 
					 | 
				
			||||||
  List,
 | 
					 | 
				
			||||||
  ListItem,
 | 
					 | 
				
			||||||
  Modal,
 | 
					 | 
				
			||||||
  showConfirm,
 | 
					 | 
				
			||||||
  showToast,
 | 
					 | 
				
			||||||
} from "./ui-lib";
 | 
					 | 
				
			||||||
import Locale from "../locales";
 | 
					 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					 | 
				
			||||||
import { useState } from "react";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function PluginPage() {
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
  const pluginStore = usePluginStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const allPlugins = pluginStore.getAll();
 | 
					 | 
				
			||||||
  const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
 | 
					 | 
				
			||||||
  const [searchText, setSearchText] = useState("");
 | 
					 | 
				
			||||||
  const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // refactored already, now it accurate
 | 
					 | 
				
			||||||
  const onSearch = (text: string) => {
 | 
					 | 
				
			||||||
    setSearchText(text);
 | 
					 | 
				
			||||||
    if (text.length > 0) {
 | 
					 | 
				
			||||||
      const result = allPlugins.filter(
 | 
					 | 
				
			||||||
        (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      setSearchPlugins(result);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      setSearchPlugins(allPlugins);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
 | 
					 | 
				
			||||||
  const editingPlugin = pluginStore.get(editingPluginId);
 | 
					 | 
				
			||||||
  const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
 | 
					 | 
				
			||||||
  const closePluginModal = () => setEditingPluginId(undefined);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
 | 
					 | 
				
			||||||
    const content = e.target.innerText;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const api = new OpenAPIClientAxios({
 | 
					 | 
				
			||||||
        definition: yaml.load(content) as any,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      api
 | 
					 | 
				
			||||||
        .init()
 | 
					 | 
				
			||||||
        .then(() => {
 | 
					 | 
				
			||||||
          if (content != editingPlugin.content) {
 | 
					 | 
				
			||||||
            pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
              plugin.content = content;
 | 
					 | 
				
			||||||
              const tool = FunctionToolService.add(plugin, true);
 | 
					 | 
				
			||||||
              plugin.title = tool.api.definition.info.title;
 | 
					 | 
				
			||||||
              plugin.version = tool.api.definition.info.version;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .catch((e) => {
 | 
					 | 
				
			||||||
          console.error(e);
 | 
					 | 
				
			||||||
          showToast(Locale.Plugin.EditModal.Error);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(e);
 | 
					 | 
				
			||||||
      showToast(Locale.Plugin.EditModal.Error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 100).bind(null, editingPlugin);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [loadUrl, setLoadUrl] = useState<string>("");
 | 
					 | 
				
			||||||
  const loadFromUrl = (loadUrl: string) =>
 | 
					 | 
				
			||||||
    fetch(loadUrl)
 | 
					 | 
				
			||||||
      .catch((e) => {
 | 
					 | 
				
			||||||
        const p = new URL(loadUrl);
 | 
					 | 
				
			||||||
        return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            "X-Base-URL": p.origin,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .then((res) => res.text())
 | 
					 | 
				
			||||||
      .then((content) => {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          return JSON.stringify(JSON.parse(content), null, "  ");
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          return content;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .then((content) => {
 | 
					 | 
				
			||||||
        pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
          plugin.content = content;
 | 
					 | 
				
			||||||
          const tool = FunctionToolService.add(plugin, true);
 | 
					 | 
				
			||||||
          plugin.title = tool.api.definition.info.title;
 | 
					 | 
				
			||||||
          plugin.version = tool.api.definition.info.version;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch((e) => {
 | 
					 | 
				
			||||||
        showToast(Locale.Plugin.EditModal.Error);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ErrorBoundary>
 | 
					 | 
				
			||||||
      <div className={styles["mask-page"]}>
 | 
					 | 
				
			||||||
        <div className="window-header">
 | 
					 | 
				
			||||||
          <div className="window-header-title">
 | 
					 | 
				
			||||||
            <div className="window-header-main-title">
 | 
					 | 
				
			||||||
              {Locale.Plugin.Page.Title}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className="window-header-submai-title">
 | 
					 | 
				
			||||||
              {Locale.Plugin.Page.SubTitle(plugins.length)}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className="window-actions">
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <a
 | 
					 | 
				
			||||||
                href={PLUGINS_REPO_URL}
 | 
					 | 
				
			||||||
                target="_blank"
 | 
					 | 
				
			||||||
                rel="noopener noreferrer"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <IconButton icon={<GithubIcon />} bordered />
 | 
					 | 
				
			||||||
              </a>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<CloseIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                onClick={() => navigate(-1)}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className={styles["mask-page-body"]}>
 | 
					 | 
				
			||||||
          <div className={styles["mask-filter"]}>
 | 
					 | 
				
			||||||
            <input
 | 
					 | 
				
			||||||
              type="text"
 | 
					 | 
				
			||||||
              className={styles["search-bar"]}
 | 
					 | 
				
			||||||
              placeholder={Locale.Plugin.Page.Search}
 | 
					 | 
				
			||||||
              autoFocus
 | 
					 | 
				
			||||||
              onInput={(e) => onSearch(e.currentTarget.value)}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <IconButton
 | 
					 | 
				
			||||||
              className={styles["mask-create"]}
 | 
					 | 
				
			||||||
              icon={<AddIcon />}
 | 
					 | 
				
			||||||
              text={Locale.Plugin.Page.Create}
 | 
					 | 
				
			||||||
              bordered
 | 
					 | 
				
			||||||
              onClick={() => {
 | 
					 | 
				
			||||||
                const createdPlugin = pluginStore.create();
 | 
					 | 
				
			||||||
                setEditingPluginId(createdPlugin.id);
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {plugins.length == 0 && (
 | 
					 | 
				
			||||||
              <div
 | 
					 | 
				
			||||||
                style={{
 | 
					 | 
				
			||||||
                  display: "flex",
 | 
					 | 
				
			||||||
                  margin: "60px auto",
 | 
					 | 
				
			||||||
                  alignItems: "center",
 | 
					 | 
				
			||||||
                  justifyContent: "center",
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                {Locale.Plugin.Page.Find}
 | 
					 | 
				
			||||||
                <a
 | 
					 | 
				
			||||||
                  href={PLUGINS_REPO_URL}
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noopener noreferrer"
 | 
					 | 
				
			||||||
                  style={{ marginLeft: 16 }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <IconButton icon={<GithubIcon />} bordered />
 | 
					 | 
				
			||||||
                </a>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
            {plugins.map((m) => (
 | 
					 | 
				
			||||||
              <div className={styles["mask-item"]} key={m.id}>
 | 
					 | 
				
			||||||
                <div className={styles["mask-header"]}>
 | 
					 | 
				
			||||||
                  <div className={styles["mask-icon"]}></div>
 | 
					 | 
				
			||||||
                  <div className={styles["mask-title"]}>
 | 
					 | 
				
			||||||
                    <div className={styles["mask-name"]}>
 | 
					 | 
				
			||||||
                      {m.title}@<small>{m.version}</small>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div className={clsx(styles["mask-info"], "one-line")}>
 | 
					 | 
				
			||||||
                      {Locale.Plugin.Item.Info(
 | 
					 | 
				
			||||||
                        FunctionToolService.add(m).length,
 | 
					 | 
				
			||||||
                      )}
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div className={styles["mask-actions"]}>
 | 
					 | 
				
			||||||
                  <IconButton
 | 
					 | 
				
			||||||
                    icon={<EditIcon />}
 | 
					 | 
				
			||||||
                    text={Locale.Plugin.Item.Edit}
 | 
					 | 
				
			||||||
                    onClick={() => setEditingPluginId(m.id)}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                  {!m.builtin && (
 | 
					 | 
				
			||||||
                    <IconButton
 | 
					 | 
				
			||||||
                      icon={<DeleteIcon />}
 | 
					 | 
				
			||||||
                      text={Locale.Plugin.Item.Delete}
 | 
					 | 
				
			||||||
                      onClick={async () => {
 | 
					 | 
				
			||||||
                        if (
 | 
					 | 
				
			||||||
                          await showConfirm(Locale.Plugin.Item.DeleteConfirm)
 | 
					 | 
				
			||||||
                        ) {
 | 
					 | 
				
			||||||
                          pluginStore.delete(m.id);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      }}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  )}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            ))}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {editingPlugin && (
 | 
					 | 
				
			||||||
        <div className="modal-mask">
 | 
					 | 
				
			||||||
          <Modal
 | 
					 | 
				
			||||||
            title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
 | 
					 | 
				
			||||||
            onClose={closePluginModal}
 | 
					 | 
				
			||||||
            actions={[
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<ConfirmIcon />}
 | 
					 | 
				
			||||||
                text={Locale.UI.Confirm}
 | 
					 | 
				
			||||||
                key="export"
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                onClick={() => setEditingPluginId("")}
 | 
					 | 
				
			||||||
              />,
 | 
					 | 
				
			||||||
            ]}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <List>
 | 
					 | 
				
			||||||
              <ListItem title={Locale.Plugin.EditModal.Auth}>
 | 
					 | 
				
			||||||
                <select
 | 
					 | 
				
			||||||
                  value={editingPlugin?.authType}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
                      plugin.authType = e.target.value;
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <option value="">{Locale.Plugin.Auth.None}</option>
 | 
					 | 
				
			||||||
                  <option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
 | 
					 | 
				
			||||||
                  <option value="basic">{Locale.Plugin.Auth.Basic}</option>
 | 
					 | 
				
			||||||
                  <option value="custom">{Locale.Plugin.Auth.Custom}</option>
 | 
					 | 
				
			||||||
                </select>
 | 
					 | 
				
			||||||
              </ListItem>
 | 
					 | 
				
			||||||
              {["bearer", "basic", "custom"].includes(
 | 
					 | 
				
			||||||
                editingPlugin.authType as string,
 | 
					 | 
				
			||||||
              ) && (
 | 
					 | 
				
			||||||
                <ListItem title={Locale.Plugin.Auth.Location}>
 | 
					 | 
				
			||||||
                  <select
 | 
					 | 
				
			||||||
                    value={editingPlugin?.authLocation}
 | 
					 | 
				
			||||||
                    onChange={(e) => {
 | 
					 | 
				
			||||||
                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
                        plugin.authLocation = e.target.value;
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <option value="header">
 | 
					 | 
				
			||||||
                      {Locale.Plugin.Auth.LocationHeader}
 | 
					 | 
				
			||||||
                    </option>
 | 
					 | 
				
			||||||
                    <option value="query">
 | 
					 | 
				
			||||||
                      {Locale.Plugin.Auth.LocationQuery}
 | 
					 | 
				
			||||||
                    </option>
 | 
					 | 
				
			||||||
                    <option value="body">
 | 
					 | 
				
			||||||
                      {Locale.Plugin.Auth.LocationBody}
 | 
					 | 
				
			||||||
                    </option>
 | 
					 | 
				
			||||||
                  </select>
 | 
					 | 
				
			||||||
                </ListItem>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
              {editingPlugin.authType == "custom" && (
 | 
					 | 
				
			||||||
                <ListItem title={Locale.Plugin.Auth.CustomHeader}>
 | 
					 | 
				
			||||||
                  <input
 | 
					 | 
				
			||||||
                    type="text"
 | 
					 | 
				
			||||||
                    value={editingPlugin?.authHeader}
 | 
					 | 
				
			||||||
                    onChange={(e) => {
 | 
					 | 
				
			||||||
                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
                        plugin.authHeader = e.target.value;
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  ></input>
 | 
					 | 
				
			||||||
                </ListItem>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
              {["bearer", "basic", "custom"].includes(
 | 
					 | 
				
			||||||
                editingPlugin.authType as string,
 | 
					 | 
				
			||||||
              ) && (
 | 
					 | 
				
			||||||
                <ListItem title={Locale.Plugin.Auth.Token}>
 | 
					 | 
				
			||||||
                  <PasswordInput
 | 
					 | 
				
			||||||
                    type="text"
 | 
					 | 
				
			||||||
                    value={editingPlugin?.authToken}
 | 
					 | 
				
			||||||
                    onChange={(e) => {
 | 
					 | 
				
			||||||
                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
					 | 
				
			||||||
                        plugin.authToken = e.currentTarget.value;
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  ></PasswordInput>
 | 
					 | 
				
			||||||
                </ListItem>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </List>
 | 
					 | 
				
			||||||
            <List>
 | 
					 | 
				
			||||||
              <ListItem title={Locale.Plugin.EditModal.Content}>
 | 
					 | 
				
			||||||
                <div className={pluginStyles["plugin-schema"]}>
 | 
					 | 
				
			||||||
                  <input
 | 
					 | 
				
			||||||
                    type="text"
 | 
					 | 
				
			||||||
                    style={{ minWidth: 200 }}
 | 
					 | 
				
			||||||
                    onInput={(e) => setLoadUrl(e.currentTarget.value)}
 | 
					 | 
				
			||||||
                  ></input>
 | 
					 | 
				
			||||||
                  <IconButton
 | 
					 | 
				
			||||||
                    icon={<ReloadIcon />}
 | 
					 | 
				
			||||||
                    text={Locale.Plugin.EditModal.Load}
 | 
					 | 
				
			||||||
                    bordered
 | 
					 | 
				
			||||||
                    onClick={() => loadFromUrl(loadUrl)}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </ListItem>
 | 
					 | 
				
			||||||
              <ListItem
 | 
					 | 
				
			||||||
                subTitle={
 | 
					 | 
				
			||||||
                  <div
 | 
					 | 
				
			||||||
                    className={clsx(
 | 
					 | 
				
			||||||
                      "markdown-body",
 | 
					 | 
				
			||||||
                      pluginStyles["plugin-content"],
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                    dir="auto"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <pre>
 | 
					 | 
				
			||||||
                      <code
 | 
					 | 
				
			||||||
                        contentEditable={true}
 | 
					 | 
				
			||||||
                        dangerouslySetInnerHTML={{
 | 
					 | 
				
			||||||
                          __html: editingPlugin.content,
 | 
					 | 
				
			||||||
                        }}
 | 
					 | 
				
			||||||
                        onBlur={onChangePlugin}
 | 
					 | 
				
			||||||
                      ></code>
 | 
					 | 
				
			||||||
                    </pre>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              ></ListItem>
 | 
					 | 
				
			||||||
              {editingPluginTool?.tools.map((tool, index) => (
 | 
					 | 
				
			||||||
                <ListItem
 | 
					 | 
				
			||||||
                  key={index}
 | 
					 | 
				
			||||||
                  title={tool?.function?.name}
 | 
					 | 
				
			||||||
                  subTitle={tool?.function?.description}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              ))}
 | 
					 | 
				
			||||||
            </List>
 | 
					 | 
				
			||||||
          </Modal>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </ErrorBoundary>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
export * from "./realtime-chat";
 | 
					 | 
				
			||||||
@@ -1,74 +0,0 @@
 | 
				
			|||||||
.realtime-chat {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  .circle-mic {
 | 
					 | 
				
			||||||
    width: 150px;
 | 
					 | 
				
			||||||
    height: 150px;
 | 
					 | 
				
			||||||
    border-radius: 50%;
 | 
					 | 
				
			||||||
    background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff);
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .icon-center {
 | 
					 | 
				
			||||||
    font-size: 24px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .bottom-icons {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: space-between;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    bottom: 20px;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
    padding: 0 20px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .icon-left,
 | 
					 | 
				
			||||||
  .icon-right {
 | 
					 | 
				
			||||||
    width: 46px;
 | 
					 | 
				
			||||||
    height: 46px;
 | 
					 | 
				
			||||||
    font-size: 36px;
 | 
					 | 
				
			||||||
    background: var(--second);
 | 
					 | 
				
			||||||
    border-radius: 50%;
 | 
					 | 
				
			||||||
    padding: 2px;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    &:hover {
 | 
					 | 
				
			||||||
      opacity: 0.8;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.mobile {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pulse {
 | 
					 | 
				
			||||||
  animation: pulse 1.5s infinite;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes pulse {
 | 
					 | 
				
			||||||
  0% {
 | 
					 | 
				
			||||||
    transform: scale(1);
 | 
					 | 
				
			||||||
    opacity: 0.7;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  50% {
 | 
					 | 
				
			||||||
    transform: scale(1.1);
 | 
					 | 
				
			||||||
    opacity: 1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  100% {
 | 
					 | 
				
			||||||
    transform: scale(1);
 | 
					 | 
				
			||||||
    opacity: 0.7;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,359 +0,0 @@
 | 
				
			|||||||
import VoiceIcon from "@/app/icons/voice.svg";
 | 
					 | 
				
			||||||
import VoiceOffIcon from "@/app/icons/voice-off.svg";
 | 
					 | 
				
			||||||
import PowerIcon from "@/app/icons/power.svg";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import styles from "./realtime-chat.module.scss";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useState, useRef, useEffect } from "react";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useChatStore, createMessage, useAppConfig } from "@/app/store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { IconButton } from "@/app/components/button";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Modality,
 | 
					 | 
				
			||||||
  RTClient,
 | 
					 | 
				
			||||||
  RTInputAudioItem,
 | 
					 | 
				
			||||||
  RTResponse,
 | 
					 | 
				
			||||||
  TurnDetection,
 | 
					 | 
				
			||||||
} from "rt-client";
 | 
					 | 
				
			||||||
import { AudioHandler } from "@/app/lib/audio";
 | 
					 | 
				
			||||||
import { uploadImage } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { VoicePrint } from "@/app/components/voice-print";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RealtimeChatProps {
 | 
					 | 
				
			||||||
  onClose?: () => void;
 | 
					 | 
				
			||||||
  onStartVoice?: () => void;
 | 
					 | 
				
			||||||
  onPausedVoice?: () => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function RealtimeChat({
 | 
					 | 
				
			||||||
  onClose,
 | 
					 | 
				
			||||||
  onStartVoice,
 | 
					 | 
				
			||||||
  onPausedVoice,
 | 
					 | 
				
			||||||
}: RealtimeChatProps) {
 | 
					 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					 | 
				
			||||||
  const config = useAppConfig();
 | 
					 | 
				
			||||||
  const [status, setStatus] = useState("");
 | 
					 | 
				
			||||||
  const [isRecording, setIsRecording] = useState(false);
 | 
					 | 
				
			||||||
  const [isConnected, setIsConnected] = useState(false);
 | 
					 | 
				
			||||||
  const [isConnecting, setIsConnecting] = useState(false);
 | 
					 | 
				
			||||||
  const [modality, setModality] = useState("audio");
 | 
					 | 
				
			||||||
  const [useVAD, setUseVAD] = useState(true);
 | 
					 | 
				
			||||||
  const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const clientRef = useRef<RTClient | null>(null);
 | 
					 | 
				
			||||||
  const audioHandlerRef = useRef<AudioHandler | null>(null);
 | 
					 | 
				
			||||||
  const initRef = useRef(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const temperature = config.realtimeConfig.temperature;
 | 
					 | 
				
			||||||
  const apiKey = config.realtimeConfig.apiKey;
 | 
					 | 
				
			||||||
  const model = config.realtimeConfig.model;
 | 
					 | 
				
			||||||
  const azure = config.realtimeConfig.provider === "Azure";
 | 
					 | 
				
			||||||
  const azureEndpoint = config.realtimeConfig.azure.endpoint;
 | 
					 | 
				
			||||||
  const azureDeployment = config.realtimeConfig.azure.deployment;
 | 
					 | 
				
			||||||
  const voice = config.realtimeConfig.voice;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleConnect = async () => {
 | 
					 | 
				
			||||||
    if (isConnecting) return;
 | 
					 | 
				
			||||||
    if (!isConnected) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        setIsConnecting(true);
 | 
					 | 
				
			||||||
        clientRef.current = azure
 | 
					 | 
				
			||||||
          ? new RTClient(
 | 
					 | 
				
			||||||
              new URL(azureEndpoint),
 | 
					 | 
				
			||||||
              { key: apiKey },
 | 
					 | 
				
			||||||
              { deployment: azureDeployment },
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          : new RTClient({ key: apiKey }, { model });
 | 
					 | 
				
			||||||
        const modalities: Modality[] =
 | 
					 | 
				
			||||||
          modality === "audio" ? ["text", "audio"] : ["text"];
 | 
					 | 
				
			||||||
        const turnDetection: TurnDetection = useVAD
 | 
					 | 
				
			||||||
          ? { type: "server_vad" }
 | 
					 | 
				
			||||||
          : null;
 | 
					 | 
				
			||||||
        await clientRef.current.configure({
 | 
					 | 
				
			||||||
          instructions: "",
 | 
					 | 
				
			||||||
          voice,
 | 
					 | 
				
			||||||
          input_audio_transcription: { model: "whisper-1" },
 | 
					 | 
				
			||||||
          turn_detection: turnDetection,
 | 
					 | 
				
			||||||
          tools: [],
 | 
					 | 
				
			||||||
          temperature,
 | 
					 | 
				
			||||||
          modalities,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        startResponseListener();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        setIsConnected(true);
 | 
					 | 
				
			||||||
        // TODO
 | 
					 | 
				
			||||||
        // try {
 | 
					 | 
				
			||||||
        //   const recentMessages = chatStore.getMessagesWithMemory();
 | 
					 | 
				
			||||||
        //   for (const message of recentMessages) {
 | 
					 | 
				
			||||||
        //     const { role, content } = message;
 | 
					 | 
				
			||||||
        //     if (typeof content === "string") {
 | 
					 | 
				
			||||||
        //       await clientRef.current.sendItem({
 | 
					 | 
				
			||||||
        //         type: "message",
 | 
					 | 
				
			||||||
        //         role: role as any,
 | 
					 | 
				
			||||||
        //         content: [
 | 
					 | 
				
			||||||
        //           {
 | 
					 | 
				
			||||||
        //             type: (role === "assistant" ? "text" : "input_text") as any,
 | 
					 | 
				
			||||||
        //             text: content as string,
 | 
					 | 
				
			||||||
        //           },
 | 
					 | 
				
			||||||
        //         ],
 | 
					 | 
				
			||||||
        //       });
 | 
					 | 
				
			||||||
        //     }
 | 
					 | 
				
			||||||
        //   }
 | 
					 | 
				
			||||||
        //   // await clientRef.current.generateResponse();
 | 
					 | 
				
			||||||
        // } catch (error) {
 | 
					 | 
				
			||||||
        //   console.error("Set message failed:", error);
 | 
					 | 
				
			||||||
        // }
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Connection failed:", error);
 | 
					 | 
				
			||||||
        setStatus("Connection failed");
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        setIsConnecting(false);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      await disconnect();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const disconnect = async () => {
 | 
					 | 
				
			||||||
    if (clientRef.current) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await clientRef.current.close();
 | 
					 | 
				
			||||||
        clientRef.current = null;
 | 
					 | 
				
			||||||
        setIsConnected(false);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Disconnect failed:", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const startResponseListener = async () => {
 | 
					 | 
				
			||||||
    if (!clientRef.current) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      for await (const serverEvent of clientRef.current.events()) {
 | 
					 | 
				
			||||||
        if (serverEvent.type === "response") {
 | 
					 | 
				
			||||||
          await handleResponse(serverEvent);
 | 
					 | 
				
			||||||
        } else if (serverEvent.type === "input_audio") {
 | 
					 | 
				
			||||||
          await handleInputAudio(serverEvent);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      if (clientRef.current) {
 | 
					 | 
				
			||||||
        console.error("Response iteration error:", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleResponse = async (response: RTResponse) => {
 | 
					 | 
				
			||||||
    for await (const item of response) {
 | 
					 | 
				
			||||||
      if (item.type === "message" && item.role === "assistant") {
 | 
					 | 
				
			||||||
        const botMessage = createMessage({
 | 
					 | 
				
			||||||
          role: item.role,
 | 
					 | 
				
			||||||
          content: "",
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        // add bot message first
 | 
					 | 
				
			||||||
        chatStore.updateTargetSession(session, (session) => {
 | 
					 | 
				
			||||||
          session.messages = session.messages.concat([botMessage]);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        let hasAudio = false;
 | 
					 | 
				
			||||||
        for await (const content of item) {
 | 
					 | 
				
			||||||
          if (content.type === "text") {
 | 
					 | 
				
			||||||
            for await (const text of content.textChunks()) {
 | 
					 | 
				
			||||||
              botMessage.content += text;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          } else if (content.type === "audio") {
 | 
					 | 
				
			||||||
            const textTask = async () => {
 | 
					 | 
				
			||||||
              for await (const text of content.transcriptChunks()) {
 | 
					 | 
				
			||||||
                botMessage.content += text;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            const audioTask = async () => {
 | 
					 | 
				
			||||||
              audioHandlerRef.current?.startStreamingPlayback();
 | 
					 | 
				
			||||||
              for await (const audio of content.audioChunks()) {
 | 
					 | 
				
			||||||
                hasAudio = true;
 | 
					 | 
				
			||||||
                audioHandlerRef.current?.playChunk(audio);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            await Promise.all([textTask(), audioTask()]);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          // update message.content
 | 
					 | 
				
			||||||
          chatStore.updateTargetSession(session, (session) => {
 | 
					 | 
				
			||||||
            session.messages = session.messages.concat();
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (hasAudio) {
 | 
					 | 
				
			||||||
          // upload audio get audio_url
 | 
					 | 
				
			||||||
          const blob = audioHandlerRef.current?.savePlayFile();
 | 
					 | 
				
			||||||
          uploadImage(blob!).then((audio_url) => {
 | 
					 | 
				
			||||||
            botMessage.audio_url = audio_url;
 | 
					 | 
				
			||||||
            // update text and audio_url
 | 
					 | 
				
			||||||
            chatStore.updateTargetSession(session, (session) => {
 | 
					 | 
				
			||||||
              session.messages = session.messages.concat();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleInputAudio = async (item: RTInputAudioItem) => {
 | 
					 | 
				
			||||||
    await item.waitForCompletion();
 | 
					 | 
				
			||||||
    if (item.transcription) {
 | 
					 | 
				
			||||||
      const userMessage = createMessage({
 | 
					 | 
				
			||||||
        role: "user",
 | 
					 | 
				
			||||||
        content: item.transcription,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      chatStore.updateTargetSession(session, (session) => {
 | 
					 | 
				
			||||||
        session.messages = session.messages.concat([userMessage]);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      // save input audio_url, and update session
 | 
					 | 
				
			||||||
      const { audioStartMillis, audioEndMillis } = item;
 | 
					 | 
				
			||||||
      // upload audio get audio_url
 | 
					 | 
				
			||||||
      const blob = audioHandlerRef.current?.saveRecordFile(
 | 
					 | 
				
			||||||
        audioStartMillis,
 | 
					 | 
				
			||||||
        audioEndMillis,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      uploadImage(blob!).then((audio_url) => {
 | 
					 | 
				
			||||||
        userMessage.audio_url = audio_url;
 | 
					 | 
				
			||||||
        chatStore.updateTargetSession(session, (session) => {
 | 
					 | 
				
			||||||
          session.messages = session.messages.concat();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // stop streaming play after get input audio.
 | 
					 | 
				
			||||||
    audioHandlerRef.current?.stopStreamingPlayback();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toggleRecording = async () => {
 | 
					 | 
				
			||||||
    if (!isRecording && clientRef.current) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        if (!audioHandlerRef.current) {
 | 
					 | 
				
			||||||
          audioHandlerRef.current = new AudioHandler();
 | 
					 | 
				
			||||||
          await audioHandlerRef.current.initialize();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        await audioHandlerRef.current.startRecording(async (chunk) => {
 | 
					 | 
				
			||||||
          await clientRef.current?.sendAudio(chunk);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        setIsRecording(true);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Failed to start recording:", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (audioHandlerRef.current) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        audioHandlerRef.current.stopRecording();
 | 
					 | 
				
			||||||
        if (!useVAD) {
 | 
					 | 
				
			||||||
          const inputAudio = await clientRef.current?.commitAudio();
 | 
					 | 
				
			||||||
          await handleInputAudio(inputAudio!);
 | 
					 | 
				
			||||||
          await clientRef.current?.generateResponse();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        setIsRecording(false);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error("Failed to stop recording:", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    // 防止重复初始化
 | 
					 | 
				
			||||||
    if (initRef.current) return;
 | 
					 | 
				
			||||||
    initRef.current = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const initAudioHandler = async () => {
 | 
					 | 
				
			||||||
      const handler = new AudioHandler();
 | 
					 | 
				
			||||||
      await handler.initialize();
 | 
					 | 
				
			||||||
      audioHandlerRef.current = handler;
 | 
					 | 
				
			||||||
      await handleConnect();
 | 
					 | 
				
			||||||
      await toggleRecording();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    initAudioHandler().catch((error) => {
 | 
					 | 
				
			||||||
      setStatus(error);
 | 
					 | 
				
			||||||
      console.error(error);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => {
 | 
					 | 
				
			||||||
      if (isRecording) {
 | 
					 | 
				
			||||||
        toggleRecording();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      audioHandlerRef.current?.close().catch(console.error);
 | 
					 | 
				
			||||||
      disconnect();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    let animationFrameId: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isConnected && isRecording) {
 | 
					 | 
				
			||||||
      const animationFrame = () => {
 | 
					 | 
				
			||||||
        if (audioHandlerRef.current) {
 | 
					 | 
				
			||||||
          const freqData = audioHandlerRef.current.getByteFrequencyData();
 | 
					 | 
				
			||||||
          setFrequencies(freqData);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        animationFrameId = requestAnimationFrame(animationFrame);
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      animationFrameId = requestAnimationFrame(animationFrame);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      setFrequencies(undefined);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => {
 | 
					 | 
				
			||||||
      if (animationFrameId) {
 | 
					 | 
				
			||||||
        cancelAnimationFrame(animationFrameId);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, [isConnected, isRecording]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // update session params
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    clientRef.current?.configure({ voice });
 | 
					 | 
				
			||||||
  }, [voice]);
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    clientRef.current?.configure({ temperature });
 | 
					 | 
				
			||||||
  }, [temperature]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleClose = async () => {
 | 
					 | 
				
			||||||
    onClose?.();
 | 
					 | 
				
			||||||
    if (isRecording) {
 | 
					 | 
				
			||||||
      await toggleRecording();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    disconnect().catch(console.error);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className={styles["realtime-chat"]}>
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        className={clsx(styles["circle-mic"], {
 | 
					 | 
				
			||||||
          [styles["pulse"]]: isRecording,
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <VoicePrint frequencies={frequencies} isActive={isRecording} />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className={styles["bottom-icons"]}>
 | 
					 | 
				
			||||||
        <div>
 | 
					 | 
				
			||||||
          <IconButton
 | 
					 | 
				
			||||||
            icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
 | 
					 | 
				
			||||||
            onClick={toggleRecording}
 | 
					 | 
				
			||||||
            disabled={!isConnected}
 | 
					 | 
				
			||||||
            shadow
 | 
					 | 
				
			||||||
            bordered
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div className={styles["icon-center"]}>{status}</div>
 | 
					 | 
				
			||||||
        <div>
 | 
					 | 
				
			||||||
          <IconButton
 | 
					 | 
				
			||||||
            icon={<PowerIcon />}
 | 
					 | 
				
			||||||
            onClick={handleClose}
 | 
					 | 
				
			||||||
            shadow
 | 
					 | 
				
			||||||
            bordered
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,173 +0,0 @@
 | 
				
			|||||||
import { RealtimeConfig } from "@/app/store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Locale from "@/app/locales";
 | 
					 | 
				
			||||||
import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { InputRange } from "@/app/components/input-range";
 | 
					 | 
				
			||||||
import { Voice } from "rt-client";
 | 
					 | 
				
			||||||
import { ServiceProvider } from "@/app/constant";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const models = ["gpt-4o-realtime-preview-2024-10-01"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const voice = ["alloy", "shimmer", "echo"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function RealtimeConfigList(props: {
 | 
					 | 
				
			||||||
  realtimeConfig: RealtimeConfig;
 | 
					 | 
				
			||||||
  updateConfig: (updater: (config: RealtimeConfig) => void) => void;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  const azureConfigComponent = props.realtimeConfig.provider ===
 | 
					 | 
				
			||||||
    ServiceProvider.Azure && (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <ListItem
 | 
					 | 
				
			||||||
        title={Locale.Settings.Realtime.Azure.Endpoint.Title}
 | 
					 | 
				
			||||||
        subTitle={Locale.Settings.Realtime.Azure.Endpoint.SubTitle}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <input
 | 
					 | 
				
			||||||
          value={props.realtimeConfig?.azure?.endpoint}
 | 
					 | 
				
			||||||
          type="text"
 | 
					 | 
				
			||||||
          placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
 | 
					 | 
				
			||||||
          onChange={(e) => {
 | 
					 | 
				
			||||||
            props.updateConfig(
 | 
					 | 
				
			||||||
              (config) => (config.azure.endpoint = e.currentTarget.value),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </ListItem>
 | 
					 | 
				
			||||||
      <ListItem
 | 
					 | 
				
			||||||
        title={Locale.Settings.Realtime.Azure.Deployment.Title}
 | 
					 | 
				
			||||||
        subTitle={Locale.Settings.Realtime.Azure.Deployment.SubTitle}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <input
 | 
					 | 
				
			||||||
          value={props.realtimeConfig?.azure?.deployment}
 | 
					 | 
				
			||||||
          type="text"
 | 
					 | 
				
			||||||
          placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
 | 
					 | 
				
			||||||
          onChange={(e) => {
 | 
					 | 
				
			||||||
            props.updateConfig(
 | 
					 | 
				
			||||||
              (config) => (config.azure.deployment = e.currentTarget.value),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </ListItem>
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <ListItem
 | 
					 | 
				
			||||||
        title={Locale.Settings.Realtime.Enable.Title}
 | 
					 | 
				
			||||||
        subTitle={Locale.Settings.Realtime.Enable.SubTitle}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <input
 | 
					 | 
				
			||||||
          type="checkbox"
 | 
					 | 
				
			||||||
          checked={props.realtimeConfig.enable}
 | 
					 | 
				
			||||||
          onChange={(e) =>
 | 
					 | 
				
			||||||
            props.updateConfig(
 | 
					 | 
				
			||||||
              (config) => (config.enable = e.currentTarget.checked),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ></input>
 | 
					 | 
				
			||||||
      </ListItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {props.realtimeConfig.enable && (
 | 
					 | 
				
			||||||
        <>
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Settings.Realtime.Provider.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Settings.Realtime.Provider.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Select
 | 
					 | 
				
			||||||
              aria-label={Locale.Settings.Realtime.Provider.Title}
 | 
					 | 
				
			||||||
              value={props.realtimeConfig.provider}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateConfig(
 | 
					 | 
				
			||||||
                  (config) =>
 | 
					 | 
				
			||||||
                    (config.provider = e.target.value as ServiceProvider),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {providers.map((v, i) => (
 | 
					 | 
				
			||||||
                <option value={v} key={i}>
 | 
					 | 
				
			||||||
                  {v}
 | 
					 | 
				
			||||||
                </option>
 | 
					 | 
				
			||||||
              ))}
 | 
					 | 
				
			||||||
            </Select>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Settings.Realtime.Model.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Settings.Realtime.Model.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Select
 | 
					 | 
				
			||||||
              aria-label={Locale.Settings.Realtime.Model.Title}
 | 
					 | 
				
			||||||
              value={props.realtimeConfig.model}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateConfig((config) => (config.model = e.target.value));
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {models.map((v, i) => (
 | 
					 | 
				
			||||||
                <option value={v} key={i}>
 | 
					 | 
				
			||||||
                  {v}
 | 
					 | 
				
			||||||
                </option>
 | 
					 | 
				
			||||||
              ))}
 | 
					 | 
				
			||||||
            </Select>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Settings.Realtime.ApiKey.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Settings.Realtime.ApiKey.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <PasswordInput
 | 
					 | 
				
			||||||
              aria={Locale.Settings.ShowPassword}
 | 
					 | 
				
			||||||
              aria-label={Locale.Settings.Realtime.ApiKey.Title}
 | 
					 | 
				
			||||||
              value={props.realtimeConfig.apiKey}
 | 
					 | 
				
			||||||
              type="text"
 | 
					 | 
				
			||||||
              placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateConfig(
 | 
					 | 
				
			||||||
                  (config) => (config.apiKey = e.currentTarget.value),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
          {azureConfigComponent}
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Settings.TTS.Voice.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Settings.TTS.Voice.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Select
 | 
					 | 
				
			||||||
              value={props.realtimeConfig.voice}
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateConfig(
 | 
					 | 
				
			||||||
                  (config) => (config.voice = e.currentTarget.value as Voice),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {voice.map((v, i) => (
 | 
					 | 
				
			||||||
                <option value={v} key={i}>
 | 
					 | 
				
			||||||
                  {v}
 | 
					 | 
				
			||||||
                </option>
 | 
					 | 
				
			||||||
              ))}
 | 
					 | 
				
			||||||
            </Select>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            title={Locale.Settings.Realtime.Temperature.Title}
 | 
					 | 
				
			||||||
            subTitle={Locale.Settings.Realtime.Temperature.SubTitle}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <InputRange
 | 
					 | 
				
			||||||
              aria={Locale.Settings.Temperature.Title}
 | 
					 | 
				
			||||||
              value={props.realtimeConfig?.temperature?.toFixed(1)}
 | 
					 | 
				
			||||||
              min="0.6"
 | 
					 | 
				
			||||||
              max="1"
 | 
					 | 
				
			||||||
              step="0.1"
 | 
					 | 
				
			||||||
              onChange={(e) => {
 | 
					 | 
				
			||||||
                props.updateConfig(
 | 
					 | 
				
			||||||
                  (config) =>
 | 
					 | 
				
			||||||
                    (config.temperature = e.currentTarget.valueAsNumber),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            ></InputRange>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
        </>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,2 +0,0 @@
 | 
				
			|||||||
export * from "./sd";
 | 
					 | 
				
			||||||
export * from "./sd-panel";
 | 
					 | 
				
			||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
.ctrl-param-item {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  min-height: 40px;
 | 
					 | 
				
			||||||
  padding: 10px 0;
 | 
					 | 
				
			||||||
  animation: slide-in ease 0.6s;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .ctrl-param-item-header {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .ctrl-param-item-title {
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      font-weight: bolder;
 | 
					 | 
				
			||||||
      margin-bottom: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .ctrl-param-item-sub-title {
 | 
					 | 
				
			||||||
    font-size: 12px;
 | 
					 | 
				
			||||||
    font-weight: normal;
 | 
					 | 
				
			||||||
    margin-top: 3px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  textarea {
 | 
					 | 
				
			||||||
    appearance: none;
 | 
					 | 
				
			||||||
    border-radius: 10px;
 | 
					 | 
				
			||||||
    border: var(--border-in-light);
 | 
					 | 
				
			||||||
    min-height: 36px;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
    background: var(--white);
 | 
					 | 
				
			||||||
    color: var(--black);
 | 
					 | 
				
			||||||
    padding: 0 10px;
 | 
					 | 
				
			||||||
    max-width: 50%;
 | 
					 | 
				
			||||||
    font-family: inherit;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.ai-models {
 | 
					 | 
				
			||||||
  button {
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,321 +0,0 @@
 | 
				
			|||||||
import styles from "./sd-panel.module.scss";
 | 
					 | 
				
			||||||
import React from "react";
 | 
					 | 
				
			||||||
import { Select } from "@/app/components/ui-lib";
 | 
					 | 
				
			||||||
import { IconButton } from "@/app/components/button";
 | 
					 | 
				
			||||||
import Locale from "@/app/locales";
 | 
					 | 
				
			||||||
import { useSdStore } from "@/app/store/sd";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const params = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.Prompt,
 | 
					 | 
				
			||||||
    value: "prompt",
 | 
					 | 
				
			||||||
    type: "textarea",
 | 
					 | 
				
			||||||
    placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
 | 
					 | 
				
			||||||
    required: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.ModelVersion,
 | 
					 | 
				
			||||||
    value: "model",
 | 
					 | 
				
			||||||
    type: "select",
 | 
					 | 
				
			||||||
    default: "sd3-medium",
 | 
					 | 
				
			||||||
    support: ["sd3"],
 | 
					 | 
				
			||||||
    options: [
 | 
					 | 
				
			||||||
      { name: "SD3 Medium", value: "sd3-medium" },
 | 
					 | 
				
			||||||
      { name: "SD3 Large", value: "sd3-large" },
 | 
					 | 
				
			||||||
      { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.NegativePrompt,
 | 
					 | 
				
			||||||
    value: "negative_prompt",
 | 
					 | 
				
			||||||
    type: "textarea",
 | 
					 | 
				
			||||||
    placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.AspectRatio,
 | 
					 | 
				
			||||||
    value: "aspect_ratio",
 | 
					 | 
				
			||||||
    type: "select",
 | 
					 | 
				
			||||||
    default: "1:1",
 | 
					 | 
				
			||||||
    options: [
 | 
					 | 
				
			||||||
      { name: "1:1", value: "1:1" },
 | 
					 | 
				
			||||||
      { name: "16:9", value: "16:9" },
 | 
					 | 
				
			||||||
      { name: "21:9", value: "21:9" },
 | 
					 | 
				
			||||||
      { name: "2:3", value: "2:3" },
 | 
					 | 
				
			||||||
      { name: "3:2", value: "3:2" },
 | 
					 | 
				
			||||||
      { name: "4:5", value: "4:5" },
 | 
					 | 
				
			||||||
      { name: "5:4", value: "5:4" },
 | 
					 | 
				
			||||||
      { name: "9:16", value: "9:16" },
 | 
					 | 
				
			||||||
      { name: "9:21", value: "9:21" },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.ImageStyle,
 | 
					 | 
				
			||||||
    value: "style",
 | 
					 | 
				
			||||||
    type: "select",
 | 
					 | 
				
			||||||
    default: "3d-model",
 | 
					 | 
				
			||||||
    support: ["core"],
 | 
					 | 
				
			||||||
    options: [
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Anime, value: "anime" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: Locale.SdPanel.Styles.ModelingCompound,
 | 
					 | 
				
			||||||
        value: "modeling-compound",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Origami, value: "origami" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
 | 
					 | 
				
			||||||
      { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "Seed",
 | 
					 | 
				
			||||||
    value: "seed",
 | 
					 | 
				
			||||||
    type: "number",
 | 
					 | 
				
			||||||
    default: 0,
 | 
					 | 
				
			||||||
    min: 0,
 | 
					 | 
				
			||||||
    max: 4294967294,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: Locale.SdPanel.OutFormat,
 | 
					 | 
				
			||||||
    value: "output_format",
 | 
					 | 
				
			||||||
    type: "select",
 | 
					 | 
				
			||||||
    default: "png",
 | 
					 | 
				
			||||||
    options: [
 | 
					 | 
				
			||||||
      { name: "PNG", value: "png" },
 | 
					 | 
				
			||||||
      { name: "JPEG", value: "jpeg" },
 | 
					 | 
				
			||||||
      { name: "WebP", value: "webp" },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const sdCommonParams = (model: string, data: any) => {
 | 
					 | 
				
			||||||
  return params.filter((item) => {
 | 
					 | 
				
			||||||
    return !(item.support && !item.support.includes(model));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const models = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "Stable Image Ultra",
 | 
					 | 
				
			||||||
    value: "ultra",
 | 
					 | 
				
			||||||
    params: (data: any) => sdCommonParams("ultra", data),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "Stable Image Core",
 | 
					 | 
				
			||||||
    value: "core",
 | 
					 | 
				
			||||||
    params: (data: any) => sdCommonParams("core", data),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "Stable Diffusion 3",
 | 
					 | 
				
			||||||
    value: "sd3",
 | 
					 | 
				
			||||||
    params: (data: any) => {
 | 
					 | 
				
			||||||
      return sdCommonParams("sd3", data).filter((item) => {
 | 
					 | 
				
			||||||
        return !(
 | 
					 | 
				
			||||||
          data.model === "sd3-large-turbo" && item.value == "negative_prompt"
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ControlParamItem(props: {
 | 
					 | 
				
			||||||
  title: string;
 | 
					 | 
				
			||||||
  subTitle?: string;
 | 
					 | 
				
			||||||
  required?: boolean;
 | 
					 | 
				
			||||||
  children?: JSX.Element | JSX.Element[];
 | 
					 | 
				
			||||||
  className?: string;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className={clsx(styles["ctrl-param-item"], props.className)}>
 | 
					 | 
				
			||||||
      <div className={styles["ctrl-param-item-header"]}>
 | 
					 | 
				
			||||||
        <div className={styles["ctrl-param-item-title"]}>
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {props.title}
 | 
					 | 
				
			||||||
            {props.required && <span style={{ color: "red" }}>*</span>}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {props.children}
 | 
					 | 
				
			||||||
      {props.subTitle && (
 | 
					 | 
				
			||||||
        <div className={styles["ctrl-param-item-sub-title"]}>
 | 
					 | 
				
			||||||
          {props.subTitle}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ControlParam(props: {
 | 
					 | 
				
			||||||
  columns: any[];
 | 
					 | 
				
			||||||
  data: any;
 | 
					 | 
				
			||||||
  onChange: (field: string, val: any) => void;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      {props.columns?.map((item) => {
 | 
					 | 
				
			||||||
        let element: null | JSX.Element;
 | 
					 | 
				
			||||||
        switch (item.type) {
 | 
					 | 
				
			||||||
          case "textarea":
 | 
					 | 
				
			||||||
            element = (
 | 
					 | 
				
			||||||
              <ControlParamItem
 | 
					 | 
				
			||||||
                title={item.name}
 | 
					 | 
				
			||||||
                subTitle={item.sub}
 | 
					 | 
				
			||||||
                required={item.required}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <textarea
 | 
					 | 
				
			||||||
                  rows={item.rows || 3}
 | 
					 | 
				
			||||||
                  style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
 | 
					 | 
				
			||||||
                  placeholder={item.placeholder}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    props.onChange(item.value, e.currentTarget.value);
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  value={props.data[item.value]}
 | 
					 | 
				
			||||||
                ></textarea>
 | 
					 | 
				
			||||||
              </ControlParamItem>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          case "select":
 | 
					 | 
				
			||||||
            element = (
 | 
					 | 
				
			||||||
              <ControlParamItem
 | 
					 | 
				
			||||||
                title={item.name}
 | 
					 | 
				
			||||||
                subTitle={item.sub}
 | 
					 | 
				
			||||||
                required={item.required}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <Select
 | 
					 | 
				
			||||||
                  aria-label={item.name}
 | 
					 | 
				
			||||||
                  value={props.data[item.value]}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    props.onChange(item.value, e.currentTarget.value);
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {item.options.map((opt: any) => {
 | 
					 | 
				
			||||||
                    return (
 | 
					 | 
				
			||||||
                      <option value={opt.value} key={opt.value}>
 | 
					 | 
				
			||||||
                        {opt.name}
 | 
					 | 
				
			||||||
                      </option>
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  })}
 | 
					 | 
				
			||||||
                </Select>
 | 
					 | 
				
			||||||
              </ControlParamItem>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          case "number":
 | 
					 | 
				
			||||||
            element = (
 | 
					 | 
				
			||||||
              <ControlParamItem
 | 
					 | 
				
			||||||
                title={item.name}
 | 
					 | 
				
			||||||
                subTitle={item.sub}
 | 
					 | 
				
			||||||
                required={item.required}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <input
 | 
					 | 
				
			||||||
                  aria-label={item.name}
 | 
					 | 
				
			||||||
                  type="number"
 | 
					 | 
				
			||||||
                  min={item.min}
 | 
					 | 
				
			||||||
                  max={item.max}
 | 
					 | 
				
			||||||
                  value={props.data[item.value] || 0}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    props.onChange(item.value, parseInt(e.currentTarget.value));
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </ControlParamItem>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          default:
 | 
					 | 
				
			||||||
            element = (
 | 
					 | 
				
			||||||
              <ControlParamItem
 | 
					 | 
				
			||||||
                title={item.name}
 | 
					 | 
				
			||||||
                subTitle={item.sub}
 | 
					 | 
				
			||||||
                required={item.required}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <input
 | 
					 | 
				
			||||||
                  aria-label={item.name}
 | 
					 | 
				
			||||||
                  type="text"
 | 
					 | 
				
			||||||
                  value={props.data[item.value]}
 | 
					 | 
				
			||||||
                  style={{ maxWidth: "100%", width: "100%" }}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    props.onChange(item.value, e.currentTarget.value);
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </ControlParamItem>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return <div key={item.value}>{element}</div>;
 | 
					 | 
				
			||||||
      })}
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getModelParamBasicData = (
 | 
					 | 
				
			||||||
  columns: any[],
 | 
					 | 
				
			||||||
  data: any,
 | 
					 | 
				
			||||||
  clearText?: boolean,
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const newParams: any = {};
 | 
					 | 
				
			||||||
  columns.forEach((item: any) => {
 | 
					 | 
				
			||||||
    if (clearText && ["text", "textarea", "number"].includes(item.type)) {
 | 
					 | 
				
			||||||
      newParams[item.value] = item.default || "";
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // @ts-ignore
 | 
					 | 
				
			||||||
      newParams[item.value] = data[item.value] || item.default || "";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  return newParams;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getParams = (model: any, params: any) => {
 | 
					 | 
				
			||||||
  return models.find((m) => m.value === model.value)?.params(params) || [];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function SdPanel() {
 | 
					 | 
				
			||||||
  const sdStore = useSdStore();
 | 
					 | 
				
			||||||
  const currentModel = sdStore.currentModel;
 | 
					 | 
				
			||||||
  const setCurrentModel = sdStore.setCurrentModel;
 | 
					 | 
				
			||||||
  const params = sdStore.currentParams;
 | 
					 | 
				
			||||||
  const setParams = sdStore.setCurrentParams;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleValueChange = (field: string, val: any) => {
 | 
					 | 
				
			||||||
    setParams({
 | 
					 | 
				
			||||||
      ...params,
 | 
					 | 
				
			||||||
      [field]: val,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  const handleModelChange = (model: any) => {
 | 
					 | 
				
			||||||
    setCurrentModel(model);
 | 
					 | 
				
			||||||
    setParams(getModelParamBasicData(model.params({}), params));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <ControlParamItem title={Locale.SdPanel.AIModel}>
 | 
					 | 
				
			||||||
        <div className={styles["ai-models"]}>
 | 
					 | 
				
			||||||
          {models.map((item) => {
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                text={item.name}
 | 
					 | 
				
			||||||
                key={item.value}
 | 
					 | 
				
			||||||
                type={currentModel.value == item.value ? "primary" : null}
 | 
					 | 
				
			||||||
                shadow
 | 
					 | 
				
			||||||
                onClick={() => handleModelChange(item)}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </ControlParamItem>
 | 
					 | 
				
			||||||
      <ControlParam
 | 
					 | 
				
			||||||
        columns={getParams?.(currentModel, params) as any[]}
 | 
					 | 
				
			||||||
        data={params}
 | 
					 | 
				
			||||||
        onChange={handleValueChange}
 | 
					 | 
				
			||||||
      ></ControlParam>
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,140 +0,0 @@
 | 
				
			|||||||
import { IconButton } from "@/app/components/button";
 | 
					 | 
				
			||||||
import GithubIcon from "@/app/icons/github.svg";
 | 
					 | 
				
			||||||
import SDIcon from "@/app/icons/sd.svg";
 | 
					 | 
				
			||||||
import ReturnIcon from "@/app/icons/return.svg";
 | 
					 | 
				
			||||||
import HistoryIcon from "@/app/icons/history.svg";
 | 
					 | 
				
			||||||
import Locale from "@/app/locales";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Path, REPO_URL } from "@/app/constant";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					 | 
				
			||||||
import dynamic from "next/dynamic";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  SideBarContainer,
 | 
					 | 
				
			||||||
  SideBarBody,
 | 
					 | 
				
			||||||
  SideBarHeader,
 | 
					 | 
				
			||||||
  SideBarTail,
 | 
					 | 
				
			||||||
  useDragSideBar,
 | 
					 | 
				
			||||||
  useHotKey,
 | 
					 | 
				
			||||||
} from "@/app/components/sidebar";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { getParams, getModelParamBasicData } from "./sd-panel";
 | 
					 | 
				
			||||||
import { useSdStore } from "@/app/store/sd";
 | 
					 | 
				
			||||||
import { showToast } from "@/app/components/ui-lib";
 | 
					 | 
				
			||||||
import { useMobileScreen } from "@/app/utils";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SdPanel = dynamic(
 | 
					 | 
				
			||||||
  async () => (await import("@/app/components/sd")).SdPanel,
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    loading: () => null,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function SideBar(props: { className?: string }) {
 | 
					 | 
				
			||||||
  useHotKey();
 | 
					 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					 | 
				
			||||||
  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
  const sdStore = useSdStore();
 | 
					 | 
				
			||||||
  const currentModel = sdStore.currentModel;
 | 
					 | 
				
			||||||
  const params = sdStore.currentParams;
 | 
					 | 
				
			||||||
  const setParams = sdStore.setCurrentParams;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleSubmit = () => {
 | 
					 | 
				
			||||||
    const columns = getParams?.(currentModel, params);
 | 
					 | 
				
			||||||
    const reqParams: any = {};
 | 
					 | 
				
			||||||
    for (let i = 0; i < columns.length; i++) {
 | 
					 | 
				
			||||||
      const item = columns[i];
 | 
					 | 
				
			||||||
      reqParams[item.value] = params[item.value] ?? null;
 | 
					 | 
				
			||||||
      if (item.required) {
 | 
					 | 
				
			||||||
        if (!reqParams[item.value]) {
 | 
					 | 
				
			||||||
          showToast(Locale.SdPanel.ParamIsRequired(item.name));
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let data: any = {
 | 
					 | 
				
			||||||
      model: currentModel.value,
 | 
					 | 
				
			||||||
      model_name: currentModel.name,
 | 
					 | 
				
			||||||
      status: "wait",
 | 
					 | 
				
			||||||
      params: reqParams,
 | 
					 | 
				
			||||||
      created_at: new Date().toLocaleString(),
 | 
					 | 
				
			||||||
      img_data: "",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    sdStore.sendTask(data, () => {
 | 
					 | 
				
			||||||
      setParams(getModelParamBasicData(columns, params, true));
 | 
					 | 
				
			||||||
      navigate(Path.SdNew);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <SideBarContainer
 | 
					 | 
				
			||||||
      onDragStart={onDragStart}
 | 
					 | 
				
			||||||
      shouldNarrow={shouldNarrow}
 | 
					 | 
				
			||||||
      {...props}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      {isMobileScreen ? (
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          className="window-header"
 | 
					 | 
				
			||||||
          data-tauri-drag-region
 | 
					 | 
				
			||||||
          style={{
 | 
					 | 
				
			||||||
            paddingLeft: 0,
 | 
					 | 
				
			||||||
            paddingRight: 0,
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <div className="window-actions">
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<ReturnIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                title={Locale.Sd.Actions.ReturnHome}
 | 
					 | 
				
			||||||
                onClick={() => navigate(Path.Home)}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <SDIcon width={50} height={50} />
 | 
					 | 
				
			||||||
          <div className="window-actions">
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<HistoryIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                title={Locale.Sd.Actions.History}
 | 
					 | 
				
			||||||
                onClick={() => navigate(Path.SdNew)}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      ) : (
 | 
					 | 
				
			||||||
        <SideBarHeader
 | 
					 | 
				
			||||||
          title={
 | 
					 | 
				
			||||||
            <IconButton
 | 
					 | 
				
			||||||
              icon={<ReturnIcon />}
 | 
					 | 
				
			||||||
              bordered
 | 
					 | 
				
			||||||
              title={Locale.Sd.Actions.ReturnHome}
 | 
					 | 
				
			||||||
              onClick={() => navigate(Path.Home)}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          logo={<SDIcon width={38} height={"100%"} />}
 | 
					 | 
				
			||||||
        ></SideBarHeader>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      <SideBarBody>
 | 
					 | 
				
			||||||
        <SdPanel />
 | 
					 | 
				
			||||||
      </SideBarBody>
 | 
					 | 
				
			||||||
      <SideBarTail
 | 
					 | 
				
			||||||
        primaryAction={
 | 
					 | 
				
			||||||
          <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
					 | 
				
			||||||
            <IconButton icon={<GithubIcon />} shadow />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        secondaryAction={
 | 
					 | 
				
			||||||
          <IconButton
 | 
					 | 
				
			||||||
            text={Locale.SdPanel.Submit}
 | 
					 | 
				
			||||||
            type="primary"
 | 
					 | 
				
			||||||
            shadow
 | 
					 | 
				
			||||||
            onClick={handleSubmit}
 | 
					 | 
				
			||||||
          ></IconButton>
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </SideBarContainer>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,53 +0,0 @@
 | 
				
			|||||||
.sd-img-list{
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-wrap: wrap;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  .sd-img-item{
 | 
					 | 
				
			||||||
    width: 48%;
 | 
					 | 
				
			||||||
    .sd-img-item-info{
 | 
					 | 
				
			||||||
      flex:1;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      overflow: hidden;
 | 
					 | 
				
			||||||
      user-select: text;
 | 
					 | 
				
			||||||
      p{
 | 
					 | 
				
			||||||
        margin: 6px;
 | 
					 | 
				
			||||||
        font-size: 12px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .line-1{
 | 
					 | 
				
			||||||
        overflow: hidden;
 | 
					 | 
				
			||||||
        white-space: nowrap;
 | 
					 | 
				
			||||||
        text-overflow: ellipsis;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .pre-img{
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      width: 130px;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      background-color: var(--second);
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .img{
 | 
					 | 
				
			||||||
      width: 130px;
 | 
					 | 
				
			||||||
      height: 130px;
 | 
					 | 
				
			||||||
      border-radius: 10px;
 | 
					 | 
				
			||||||
      overflow: hidden;
 | 
					 | 
				
			||||||
      cursor: pointer;
 | 
					 | 
				
			||||||
      transition: all .3s;
 | 
					 | 
				
			||||||
      &:hover{
 | 
					 | 
				
			||||||
        opacity: .7;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    &:not(:last-child){
 | 
					 | 
				
			||||||
      margin-bottom: 20px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
  .sd-img-list{
 | 
					 | 
				
			||||||
    .sd-img-item{
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,340 +0,0 @@
 | 
				
			|||||||
import chatStyles from "@/app/components/chat.module.scss";
 | 
					 | 
				
			||||||
import styles from "@/app/components/sd/sd.module.scss";
 | 
					 | 
				
			||||||
import homeStyles from "@/app/components/home.module.scss";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { IconButton } from "@/app/components/button";
 | 
					 | 
				
			||||||
import ReturnIcon from "@/app/icons/return.svg";
 | 
					 | 
				
			||||||
import Locale from "@/app/locales";
 | 
					 | 
				
			||||||
import { Path } from "@/app/constant";
 | 
					 | 
				
			||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  copyToClipboard,
 | 
					 | 
				
			||||||
  getMessageTextContent,
 | 
					 | 
				
			||||||
  useMobileScreen,
 | 
					 | 
				
			||||||
} from "@/app/utils";
 | 
					 | 
				
			||||||
import { useNavigate, useLocation } from "react-router-dom";
 | 
					 | 
				
			||||||
import { useAppConfig } from "@/app/store";
 | 
					 | 
				
			||||||
import MinIcon from "@/app/icons/min.svg";
 | 
					 | 
				
			||||||
import MaxIcon from "@/app/icons/max.svg";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { ChatAction } from "@/app/components/chat";
 | 
					 | 
				
			||||||
import DeleteIcon from "@/app/icons/clear.svg";
 | 
					 | 
				
			||||||
import CopyIcon from "@/app/icons/copy.svg";
 | 
					 | 
				
			||||||
import PromptIcon from "@/app/icons/prompt.svg";
 | 
					 | 
				
			||||||
import ResetIcon from "@/app/icons/reload.svg";
 | 
					 | 
				
			||||||
import { useSdStore } from "@/app/store/sd";
 | 
					 | 
				
			||||||
import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
					 | 
				
			||||||
import ErrorIcon from "@/app/icons/delete.svg";
 | 
					 | 
				
			||||||
import SDIcon from "@/app/icons/sd.svg";
 | 
					 | 
				
			||||||
import { Property } from "csstype";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  showConfirm,
 | 
					 | 
				
			||||||
  showImageModal,
 | 
					 | 
				
			||||||
  showModal,
 | 
					 | 
				
			||||||
} from "@/app/components/ui-lib";
 | 
					 | 
				
			||||||
import { removeImage } from "@/app/utils/chat";
 | 
					 | 
				
			||||||
import { SideBar } from "./sd-sidebar";
 | 
					 | 
				
			||||||
import { WindowContent } from "@/app/components/home";
 | 
					 | 
				
			||||||
import { params } from "./sd-panel";
 | 
					 | 
				
			||||||
import clsx from "clsx";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getSdTaskStatus(item: any) {
 | 
					 | 
				
			||||||
  let s: string;
 | 
					 | 
				
			||||||
  let color: Property.Color | undefined = undefined;
 | 
					 | 
				
			||||||
  switch (item.status) {
 | 
					 | 
				
			||||||
    case "success":
 | 
					 | 
				
			||||||
      s = Locale.Sd.Status.Success;
 | 
					 | 
				
			||||||
      color = "green";
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    case "error":
 | 
					 | 
				
			||||||
      s = Locale.Sd.Status.Error;
 | 
					 | 
				
			||||||
      color = "red";
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    case "wait":
 | 
					 | 
				
			||||||
      s = Locale.Sd.Status.Wait;
 | 
					 | 
				
			||||||
      color = "yellow";
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    case "running":
 | 
					 | 
				
			||||||
      s = Locale.Sd.Status.Running;
 | 
					 | 
				
			||||||
      color = "blue";
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      s = item.status.toUpperCase();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
 | 
					 | 
				
			||||||
      <span>
 | 
					 | 
				
			||||||
        {Locale.Sd.Status.Name}: {s}
 | 
					 | 
				
			||||||
      </span>
 | 
					 | 
				
			||||||
      {item.status === "error" && (
 | 
					 | 
				
			||||||
        <span
 | 
					 | 
				
			||||||
          className="clickable"
 | 
					 | 
				
			||||||
          onClick={() => {
 | 
					 | 
				
			||||||
            showModal({
 | 
					 | 
				
			||||||
              title: Locale.Sd.Detail,
 | 
					 | 
				
			||||||
              children: (
 | 
					 | 
				
			||||||
                <div style={{ color: color, userSelect: "text" }}>
 | 
					 | 
				
			||||||
                  {item.error}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          - {item.error}
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </p>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function Sd() {
 | 
					 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
  const location = useLocation();
 | 
					 | 
				
			||||||
  const clientConfig = useMemo(() => getClientConfig(), []);
 | 
					 | 
				
			||||||
  const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
 | 
					 | 
				
			||||||
  const config = useAppConfig();
 | 
					 | 
				
			||||||
  const scrollRef = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
  const sdStore = useSdStore();
 | 
					 | 
				
			||||||
  const [sdImages, setSdImages] = useState(sdStore.draw);
 | 
					 | 
				
			||||||
  const isSd = location.pathname === Path.Sd;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    setSdImages(sdStore.draw);
 | 
					 | 
				
			||||||
  }, [sdStore.currentId]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
 | 
					 | 
				
			||||||
      <WindowContent>
 | 
					 | 
				
			||||||
        <div className={chatStyles.chat} key={"1"}>
 | 
					 | 
				
			||||||
          <div className="window-header" data-tauri-drag-region>
 | 
					 | 
				
			||||||
            {isMobileScreen && (
 | 
					 | 
				
			||||||
              <div className="window-actions">
 | 
					 | 
				
			||||||
                <div className={"window-action-button"}>
 | 
					 | 
				
			||||||
                  <IconButton
 | 
					 | 
				
			||||||
                    icon={<ReturnIcon />}
 | 
					 | 
				
			||||||
                    bordered
 | 
					 | 
				
			||||||
                    title={Locale.Chat.Actions.ChatList}
 | 
					 | 
				
			||||||
                    onClick={() => navigate(Path.Sd)}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              className={clsx(
 | 
					 | 
				
			||||||
                "window-header-title",
 | 
					 | 
				
			||||||
                chatStyles["chat-body-title"],
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <div className={`window-header-main-title`}>Stability AI</div>
 | 
					 | 
				
			||||||
              <div className="window-header-sub-title">
 | 
					 | 
				
			||||||
                {Locale.Sd.SubTitle(sdImages.length || 0)}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div className="window-actions">
 | 
					 | 
				
			||||||
              {showMaxIcon && (
 | 
					 | 
				
			||||||
                <div className="window-action-button">
 | 
					 | 
				
			||||||
                  <IconButton
 | 
					 | 
				
			||||||
                    aria={Locale.Chat.Actions.FullScreen}
 | 
					 | 
				
			||||||
                    icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
 | 
					 | 
				
			||||||
                    bordered
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      config.update(
 | 
					 | 
				
			||||||
                        (config) => (config.tightBorder = !config.tightBorder),
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
              {isMobileScreen && <SDIcon width={50} height={50} />}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div className={chatStyles["chat-body"]} ref={scrollRef}>
 | 
					 | 
				
			||||||
            <div className={styles["sd-img-list"]}>
 | 
					 | 
				
			||||||
              {sdImages.length > 0 ? (
 | 
					 | 
				
			||||||
                sdImages.map((item: any) => {
 | 
					 | 
				
			||||||
                  return (
 | 
					 | 
				
			||||||
                    <div
 | 
					 | 
				
			||||||
                      key={item.id}
 | 
					 | 
				
			||||||
                      style={{ display: "flex" }}
 | 
					 | 
				
			||||||
                      className={styles["sd-img-item"]}
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      {item.status === "success" ? (
 | 
					 | 
				
			||||||
                        <img
 | 
					 | 
				
			||||||
                          className={styles["img"]}
 | 
					 | 
				
			||||||
                          src={item.img_data}
 | 
					 | 
				
			||||||
                          alt={item.id}
 | 
					 | 
				
			||||||
                          onClick={(e) =>
 | 
					 | 
				
			||||||
                            showImageModal(
 | 
					 | 
				
			||||||
                              item.img_data,
 | 
					 | 
				
			||||||
                              true,
 | 
					 | 
				
			||||||
                              isMobileScreen
 | 
					 | 
				
			||||||
                                ? { width: "100%", height: "fit-content" }
 | 
					 | 
				
			||||||
                                : { maxWidth: "100%", maxHeight: "100%" },
 | 
					 | 
				
			||||||
                              isMobileScreen
 | 
					 | 
				
			||||||
                                ? { width: "100%", height: "fit-content" }
 | 
					 | 
				
			||||||
                                : { width: "100%", height: "100%" },
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                          }
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                      ) : item.status === "error" ? (
 | 
					 | 
				
			||||||
                        <div className={styles["pre-img"]}>
 | 
					 | 
				
			||||||
                          <ErrorIcon />
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                      ) : (
 | 
					 | 
				
			||||||
                        <div className={styles["pre-img"]}>
 | 
					 | 
				
			||||||
                          <LoadingIcon />
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                      )}
 | 
					 | 
				
			||||||
                      <div
 | 
					 | 
				
			||||||
                        style={{ marginLeft: "10px" }}
 | 
					 | 
				
			||||||
                        className={styles["sd-img-item-info"]}
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                        <p className={styles["line-1"]}>
 | 
					 | 
				
			||||||
                          {Locale.SdPanel.Prompt}:{" "}
 | 
					 | 
				
			||||||
                          <span
 | 
					 | 
				
			||||||
                            className="clickable"
 | 
					 | 
				
			||||||
                            title={item.params.prompt}
 | 
					 | 
				
			||||||
                            onClick={() => {
 | 
					 | 
				
			||||||
                              showModal({
 | 
					 | 
				
			||||||
                                title: Locale.Sd.Detail,
 | 
					 | 
				
			||||||
                                children: (
 | 
					 | 
				
			||||||
                                  <div style={{ userSelect: "text" }}>
 | 
					 | 
				
			||||||
                                    {item.params.prompt}
 | 
					 | 
				
			||||||
                                  </div>
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                              });
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                          >
 | 
					 | 
				
			||||||
                            {item.params.prompt}
 | 
					 | 
				
			||||||
                          </span>
 | 
					 | 
				
			||||||
                        </p>
 | 
					 | 
				
			||||||
                        <p>
 | 
					 | 
				
			||||||
                          {Locale.SdPanel.AIModel}: {item.model_name}
 | 
					 | 
				
			||||||
                        </p>
 | 
					 | 
				
			||||||
                        {getSdTaskStatus(item)}
 | 
					 | 
				
			||||||
                        <p>{item.created_at}</p>
 | 
					 | 
				
			||||||
                        <div className={chatStyles["chat-message-actions"]}>
 | 
					 | 
				
			||||||
                          <div className={chatStyles["chat-input-actions"]}>
 | 
					 | 
				
			||||||
                            <ChatAction
 | 
					 | 
				
			||||||
                              text={Locale.Sd.Actions.Params}
 | 
					 | 
				
			||||||
                              icon={<PromptIcon />}
 | 
					 | 
				
			||||||
                              onClick={() => {
 | 
					 | 
				
			||||||
                                showModal({
 | 
					 | 
				
			||||||
                                  title: Locale.Sd.GenerateParams,
 | 
					 | 
				
			||||||
                                  children: (
 | 
					 | 
				
			||||||
                                    <div style={{ userSelect: "text" }}>
 | 
					 | 
				
			||||||
                                      {Object.keys(item.params).map((key) => {
 | 
					 | 
				
			||||||
                                        let label = key;
 | 
					 | 
				
			||||||
                                        let value = item.params[key];
 | 
					 | 
				
			||||||
                                        switch (label) {
 | 
					 | 
				
			||||||
                                          case "prompt":
 | 
					 | 
				
			||||||
                                            label = Locale.SdPanel.Prompt;
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          case "negative_prompt":
 | 
					 | 
				
			||||||
                                            label =
 | 
					 | 
				
			||||||
                                              Locale.SdPanel.NegativePrompt;
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          case "aspect_ratio":
 | 
					 | 
				
			||||||
                                            label = Locale.SdPanel.AspectRatio;
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          case "seed":
 | 
					 | 
				
			||||||
                                            label = "Seed";
 | 
					 | 
				
			||||||
                                            value = value || 0;
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          case "output_format":
 | 
					 | 
				
			||||||
                                            label = Locale.SdPanel.OutFormat;
 | 
					 | 
				
			||||||
                                            value = value?.toUpperCase();
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          case "style":
 | 
					 | 
				
			||||||
                                            label = Locale.SdPanel.ImageStyle;
 | 
					 | 
				
			||||||
                                            value = params
 | 
					 | 
				
			||||||
                                              .find(
 | 
					 | 
				
			||||||
                                                (item) =>
 | 
					 | 
				
			||||||
                                                  item.value === "style",
 | 
					 | 
				
			||||||
                                              )
 | 
					 | 
				
			||||||
                                              ?.options?.find(
 | 
					 | 
				
			||||||
                                                (item) => item.value === value,
 | 
					 | 
				
			||||||
                                              )?.name;
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                          default:
 | 
					 | 
				
			||||||
                                            break;
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                        return (
 | 
					 | 
				
			||||||
                                          <div
 | 
					 | 
				
			||||||
                                            key={key}
 | 
					 | 
				
			||||||
                                            style={{ margin: "10px" }}
 | 
					 | 
				
			||||||
                                          >
 | 
					 | 
				
			||||||
                                            <strong>{label}: </strong>
 | 
					 | 
				
			||||||
                                            {value}
 | 
					 | 
				
			||||||
                                          </div>
 | 
					 | 
				
			||||||
                                        );
 | 
					 | 
				
			||||||
                                      })}
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                });
 | 
					 | 
				
			||||||
                              }}
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                            <ChatAction
 | 
					 | 
				
			||||||
                              text={Locale.Sd.Actions.Copy}
 | 
					 | 
				
			||||||
                              icon={<CopyIcon />}
 | 
					 | 
				
			||||||
                              onClick={() =>
 | 
					 | 
				
			||||||
                                copyToClipboard(
 | 
					 | 
				
			||||||
                                  getMessageTextContent({
 | 
					 | 
				
			||||||
                                    role: "user",
 | 
					 | 
				
			||||||
                                    content: item.params.prompt,
 | 
					 | 
				
			||||||
                                  }),
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                              }
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                            <ChatAction
 | 
					 | 
				
			||||||
                              text={Locale.Sd.Actions.Retry}
 | 
					 | 
				
			||||||
                              icon={<ResetIcon />}
 | 
					 | 
				
			||||||
                              onClick={() => {
 | 
					 | 
				
			||||||
                                const reqData = {
 | 
					 | 
				
			||||||
                                  model: item.model,
 | 
					 | 
				
			||||||
                                  model_name: item.model_name,
 | 
					 | 
				
			||||||
                                  status: "wait",
 | 
					 | 
				
			||||||
                                  params: { ...item.params },
 | 
					 | 
				
			||||||
                                  created_at: new Date().toLocaleString(),
 | 
					 | 
				
			||||||
                                  img_data: "",
 | 
					 | 
				
			||||||
                                };
 | 
					 | 
				
			||||||
                                sdStore.sendTask(reqData);
 | 
					 | 
				
			||||||
                              }}
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                            <ChatAction
 | 
					 | 
				
			||||||
                              text={Locale.Sd.Actions.Delete}
 | 
					 | 
				
			||||||
                              icon={<DeleteIcon />}
 | 
					 | 
				
			||||||
                              onClick={async () => {
 | 
					 | 
				
			||||||
                                if (
 | 
					 | 
				
			||||||
                                  await showConfirm(Locale.Sd.Danger.Delete)
 | 
					 | 
				
			||||||
                                ) {
 | 
					 | 
				
			||||||
                                  // remove img_data + remove item in list
 | 
					 | 
				
			||||||
                                  removeImage(item.img_data).finally(() => {
 | 
					 | 
				
			||||||
                                    sdStore.draw = sdImages.filter(
 | 
					 | 
				
			||||||
                                      (i: any) => i.id !== item.id,
 | 
					 | 
				
			||||||
                                    );
 | 
					 | 
				
			||||||
                                    sdStore.getNextId();
 | 
					 | 
				
			||||||
                                  });
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                              }}
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                          </div>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
              ) : (
 | 
					 | 
				
			||||||
                <div>{Locale.Sd.EmptyRecord}</div>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </WindowContent>
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,167 +0,0 @@
 | 
				
			|||||||
import { useState, useEffect, useRef, useCallback } from "react";
 | 
					 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					 | 
				
			||||||
import styles from "./mask.module.scss";
 | 
					 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					 | 
				
			||||||
import { IconButton } from "./button";
 | 
					 | 
				
			||||||
import CloseIcon from "../icons/close.svg";
 | 
					 | 
				
			||||||
import EyeIcon from "../icons/eye.svg";
 | 
					 | 
				
			||||||
import Locale from "../locales";
 | 
					 | 
				
			||||||
import { Path } from "../constant";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useChatStore } from "../store";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Item = {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  content: string;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
export function SearchChatPage() {
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const sessions = chatStore.sessions;
 | 
					 | 
				
			||||||
  const selectSession = chatStore.selectSession;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [searchResults, setSearchResults] = useState<Item[]>([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const previousValueRef = useRef<string>("");
 | 
					 | 
				
			||||||
  const searchInputRef = useRef<HTMLInputElement>(null);
 | 
					 | 
				
			||||||
  const doSearch = useCallback((text: string) => {
 | 
					 | 
				
			||||||
    const lowerCaseText = text.toLowerCase();
 | 
					 | 
				
			||||||
    const results: Item[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sessions.forEach((session, index) => {
 | 
					 | 
				
			||||||
      const fullTextContents: string[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      session.messages.forEach((message) => {
 | 
					 | 
				
			||||||
        const content = message.content as string;
 | 
					 | 
				
			||||||
        if (!content.toLowerCase || content === "") return;
 | 
					 | 
				
			||||||
        const lowerCaseContent = content.toLowerCase();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // full text search
 | 
					 | 
				
			||||||
        let pos = lowerCaseContent.indexOf(lowerCaseText);
 | 
					 | 
				
			||||||
        while (pos !== -1) {
 | 
					 | 
				
			||||||
          const start = Math.max(0, pos - 35);
 | 
					 | 
				
			||||||
          const end = Math.min(content.length, pos + lowerCaseText.length + 35);
 | 
					 | 
				
			||||||
          fullTextContents.push(content.substring(start, end));
 | 
					 | 
				
			||||||
          pos = lowerCaseContent.indexOf(
 | 
					 | 
				
			||||||
            lowerCaseText,
 | 
					 | 
				
			||||||
            pos + lowerCaseText.length,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (fullTextContents.length > 0) {
 | 
					 | 
				
			||||||
        results.push({
 | 
					 | 
				
			||||||
          id: index,
 | 
					 | 
				
			||||||
          name: session.topic,
 | 
					 | 
				
			||||||
          content: fullTextContents.join("... "), // concat content with...
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // sort by length of matching content
 | 
					 | 
				
			||||||
    results.sort((a, b) => b.content.length - a.content.length);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return results;
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const intervalId = setInterval(() => {
 | 
					 | 
				
			||||||
      if (searchInputRef.current) {
 | 
					 | 
				
			||||||
        const currentValue = searchInputRef.current.value;
 | 
					 | 
				
			||||||
        if (currentValue !== previousValueRef.current) {
 | 
					 | 
				
			||||||
          if (currentValue.length > 0) {
 | 
					 | 
				
			||||||
            const result = doSearch(currentValue);
 | 
					 | 
				
			||||||
            setSearchResults(result);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          previousValueRef.current = currentValue;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }, 1000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Cleanup the interval on component unmount
 | 
					 | 
				
			||||||
    return () => clearInterval(intervalId);
 | 
					 | 
				
			||||||
  }, [doSearch]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ErrorBoundary>
 | 
					 | 
				
			||||||
      <div className={styles["mask-page"]}>
 | 
					 | 
				
			||||||
        {/* header */}
 | 
					 | 
				
			||||||
        <div className="window-header">
 | 
					 | 
				
			||||||
          <div className="window-header-title">
 | 
					 | 
				
			||||||
            <div className="window-header-main-title">
 | 
					 | 
				
			||||||
              {Locale.SearchChat.Page.Title}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div className="window-header-submai-title">
 | 
					 | 
				
			||||||
              {Locale.SearchChat.Page.SubTitle(searchResults.length)}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className="window-actions">
 | 
					 | 
				
			||||||
            <div className="window-action-button">
 | 
					 | 
				
			||||||
              <IconButton
 | 
					 | 
				
			||||||
                icon={<CloseIcon />}
 | 
					 | 
				
			||||||
                bordered
 | 
					 | 
				
			||||||
                onClick={() => navigate(-1)}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className={styles["mask-page-body"]}>
 | 
					 | 
				
			||||||
          <div className={styles["mask-filter"]}>
 | 
					 | 
				
			||||||
            {/**搜索输入框 */}
 | 
					 | 
				
			||||||
            <input
 | 
					 | 
				
			||||||
              type="text"
 | 
					 | 
				
			||||||
              className={styles["search-bar"]}
 | 
					 | 
				
			||||||
              placeholder={Locale.SearchChat.Page.Search}
 | 
					 | 
				
			||||||
              autoFocus
 | 
					 | 
				
			||||||
              ref={searchInputRef}
 | 
					 | 
				
			||||||
              onKeyDown={(e) => {
 | 
					 | 
				
			||||||
                if (e.key === "Enter") {
 | 
					 | 
				
			||||||
                  e.preventDefault();
 | 
					 | 
				
			||||||
                  const searchText = e.currentTarget.value;
 | 
					 | 
				
			||||||
                  if (searchText.length > 0) {
 | 
					 | 
				
			||||||
                    const result = doSearch(searchText);
 | 
					 | 
				
			||||||
                    setSearchResults(result);
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            {searchResults.map((item) => (
 | 
					 | 
				
			||||||
              <div
 | 
					 | 
				
			||||||
                className={styles["mask-item"]}
 | 
					 | 
				
			||||||
                key={item.id}
 | 
					 | 
				
			||||||
                onClick={() => {
 | 
					 | 
				
			||||||
                  navigate(Path.Chat);
 | 
					 | 
				
			||||||
                  selectSession(item.id);
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                style={{ cursor: "pointer" }}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                {/** 搜索匹配的文本 */}
 | 
					 | 
				
			||||||
                <div className={styles["mask-header"]}>
 | 
					 | 
				
			||||||
                  <div className={styles["mask-title"]}>
 | 
					 | 
				
			||||||
                    <div className={styles["mask-name"]}>{item.name}</div>
 | 
					 | 
				
			||||||
                    {item.content.slice(0, 70)}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                {/** 操作按钮 */}
 | 
					 | 
				
			||||||
                <div className={styles["mask-actions"]}>
 | 
					 | 
				
			||||||
                  <IconButton
 | 
					 | 
				
			||||||
                    icon={<EyeIcon />}
 | 
					 | 
				
			||||||
                    text={Locale.SearchChat.Item.View}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            ))}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </ErrorBoundary>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user