feat: add Chinese LLM providers support with auto-fill API Base URL

- Add support for DeepSeek, Qwen (Alibaba), Kimi (Moonshot), and GLM (Zhipu)
- Implement auto-fill API Base URL when selecting Chinese LLM providers
- Add smart validation and warnings for missing API endpoints
- Fix session state management in task logging service
- Add comprehensive Chinese setup documentation
- Add database migration for new LLM provider enums

Closes #383
This commit is contained in:
Differ 2025-10-12 19:10:46 +08:00
parent 402039f02f
commit 917cf4f398
9 changed files with 565 additions and 5 deletions

323
docs/chinese-llm-setup.md Normal file
View file

@ -0,0 +1,323 @@
# 国产 LLM 配置指南 | Chinese LLM Setup Guide
本指南将帮助你在 SurfSense 中配置和使用国产大语言模型。
This guide helps you configure and use Chinese LLM providers in SurfSense.
---
## 📋 支持的提供商 | Supported Providers
SurfSense 现已支持以下国产 LLM
- ✅ **DeepSeek** - 国产高性能 AI 模型
- ✅ **阿里通义千问 (Alibaba Qwen)** - 阿里云通义千问大模型
- ✅ **月之暗面 Kimi (Moonshot)** - 月之暗面 Kimi 大模型
- ✅ **智谱 AI GLM (Zhipu)** - 智谱 AI GLM 系列模型
---
## 🚀 快速开始 | Quick Start
### 通用配置步骤 | General Configuration Steps
1. 登录 SurfSense Dashboard
2. 进入 **Settings****API Keys** (或 **LLM Configurations**)
3. 点击 **Add New Configuration**
4. 从 **Provider** 下拉菜单中选择你的国产 LLM 提供商
5. 填写必填字段(见下方各提供商详细配置)
6. 点击 **Save**
---
## 1⃣ DeepSeek 配置 | DeepSeek Configuration
### 获取 API Key
1. 访问 [DeepSeek 开放平台](https://platform.deepseek.com/)
2. 注册并登录账号
3. 进入 **API Keys** 页面
4. 点击 **Create New API Key**
5. 复制生成的 API Key (格式: `sk-xxx`)
### 在 SurfSense 中配置
| 字段 | 值 | 说明 |
|------|-----|------|
| **Configuration Name** | `DeepSeek Chat` | 配置名称(自定义) |
| **Provider** | `DEEPSEEK` | 选择 DeepSeek |
| **Model Name** | `deepseek-chat` | 推荐模型<br>其他选项: `deepseek-coder` |
| **API Key** | `sk-xxx...` | 你的 DeepSeek API Key |
| **API Base URL** | `https://api.deepseek.com` | DeepSeek API 地址 |
| **Parameters** | _(留空)_ | 使用默认参数 |
### 示例配置
```
Configuration Name: DeepSeek Chat
Provider: DEEPSEEK
Model Name: deepseek-chat
API Key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API Base URL: https://api.deepseek.com
```
### 可用模型
- **deepseek-chat**: 通用对话模型(推荐)
- **deepseek-coder**: 代码专用模型
### 定价
- 请访问 [DeepSeek 定价页面](https://platform.deepseek.com/pricing) 查看最新价格
---
## 2⃣ 阿里通义千问 (Alibaba Qwen) 配置
### 获取 API Key
1. 访问 [阿里云百炼平台](https://dashscope.aliyun.com/)
2. 登录阿里云账号
3. 开通 DashScope 服务
4. 进入 **API-KEY 管理**
5. 创建并复制 API Key
### 在 SurfSense 中配置
| 字段 | 值 | 说明 |
|------|-----|------|
| **Configuration Name** | `通义千问 Max` | 配置名称(自定义) |
| **Provider** | `ALIBABA_QWEN` | 选择阿里通义千问 |
| **Model Name** | `qwen-max` | 推荐模型<br>其他选项: `qwen-plus`, `qwen-turbo` |
| **API Key** | `sk-xxx...` | 你的 DashScope API Key |
| **API Base URL** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | 阿里云 API 地址 |
| **Parameters** | _(留空)_ | 使用默认参数 |
### 示例配置
```
Configuration Name: 通义千问 Max
Provider: ALIBABA_QWEN
Model Name: qwen-max
API Key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1
```
### 可用模型
- **qwen-max**: 最强性能,适合复杂任务
- **qwen-plus**: 性价比高,适合日常使用(推荐)
- **qwen-turbo**: 速度快,适合简单任务
### 定价
- 请访问 [阿里云百炼定价](https://help.aliyun.com/zh/model-studio/getting-started/billing) 查看最新价格
---
## 3⃣ 月之暗面 Kimi (Moonshot) 配置
### 获取 API Key
1. 访问 [Moonshot AI 开放平台](https://platform.moonshot.cn/)
2. 注册并登录账号
3. 进入 **API Key 管理**
4. 创建新的 API Key
5. 复制 API Key
### 在 SurfSense 中配置
| 字段 | 值 | 说明 |
|------|-----|------|
| **Configuration Name** | `Kimi` | 配置名称(自定义) |
| **Provider** | `MOONSHOT` | 选择月之暗面 Kimi |
| **Model Name** | `moonshot-v1-32k` | 推荐模型<br>其他选项: `moonshot-v1-8k`, `moonshot-v1-128k` |
| **API Key** | `sk-xxx...` | 你的 Moonshot API Key |
| **API Base URL** | `https://api.moonshot.cn/v1` | Moonshot API 地址 |
| **Parameters** | _(留空)_ | 使用默认参数 |
### 示例配置
```
Configuration Name: Kimi 32K
Provider: MOONSHOT
Model Name: moonshot-v1-32k
API Key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API Base URL: https://api.moonshot.cn/v1
```
### 可用模型
- **moonshot-v1-8k**: 8K 上下文(基础版)
- **moonshot-v1-32k**: 32K 上下文(推荐)
- **moonshot-v1-128k**: 128K 上下文(长文本专用)
### 定价
- 请访问 [Moonshot AI 定价](https://platform.moonshot.cn/pricing) 查看最新价格
---
## 4⃣ 智谱 AI GLM (Zhipu) 配置
### 获取 API Key
1. 访问 [智谱 AI 开放平台](https://open.bigmodel.cn/)
2. 注册并登录账号
3. 进入 **API 管理**
4. 创建新的 API Key
5. 复制 API Key
### 在 SurfSense 中配置
| 字段 | 值 | 说明 |
|------|-----|------|
| **Configuration Name** | `GLM-4` | 配置名称(自定义) |
| **Provider** | `ZHIPU` | 选择智谱 AI |
| **Model Name** | `glm-4` | 推荐模型<br>其他选项: `glm-4-flash`, `glm-3-turbo` |
| **API Key** | `xxx.yyy...` | 你的智谱 API Key |
| **API Base URL** | `https://open.bigmodel.cn/api/paas/v4` | 智谱 API 地址 |
| **Parameters** | _(留空)_ | 使用默认参数 |
### 示例配置
```
Configuration Name: GLM-4
Provider: ZHIPU
Model Name: glm-4
API Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx
API Base URL: https://open.bigmodel.cn/api/paas/v4
```
### 可用模型
- **glm-4**: GLM-4 旗舰模型(推荐)
- **glm-4-flash**: 快速推理版本
- **glm-3-turbo**: 高性价比版本
### 定价
- 请访问 [智谱 AI 定价](https://open.bigmodel.cn/pricing) 查看最新价格
---
## ⚙️ 高级配置 | Advanced Configuration
### 自定义参数 | Custom Parameters
你可以在 **Parameters** 字段中添加自定义参数JSON 格式):
```json
{
"temperature": 0.7,
"max_tokens": 2000,
"top_p": 0.9
}
```
### 常用参数说明
| 参数 | 说明 | 默认值 | 范围 |
|------|------|--------|------|
| `temperature` | 控制输出随机性,越高越随机 | 0.7 | 0.0 - 1.0 |
| `max_tokens` | 最大输出 Token 数 | 模型默认 | 1 - 模型上限 |
| `top_p` | 核采样参数 | 1.0 | 0.0 - 1.0 |
---
## 🔧 故障排除 | Troubleshooting
### 常见问题
#### 1. **错误: "Invalid API Key"**
- ✅ 检查 API Key 是否正确复制(无多余空格)
- ✅ 确认 API Key 是否已激活
- ✅ 检查账户余额是否充足
#### 2. **错误: "Connection timeout"**
- ✅ 确认 API Base URL 是否正确
- ✅ 检查网络连接
- ✅ 确认防火墙是否允许访问
#### 3. **错误: "Model not found"**
- ✅ 确认模型名称是否拼写正确
- ✅ 检查该模型是否已开通
- ✅ 参照上方文档确认可用模型名称
#### 4. **文档处理卡住 (IN_PROGRESS)**
- ✅ 检查模型名称中是否有多余空格
- ✅ 确认 API Key 有效且有额度
- ✅ 查看后端日志: `docker compose logs backend`
### 查看日志
```bash
# 查看后端日志
docker compose logs backend --tail 100
# 实时查看日志
docker compose logs -f backend
# 搜索错误
docker compose logs backend | grep -i "error"
```
---
## 💡 最佳实践 | Best Practices
### 1. 模型选择建议
| 任务类型 | 推荐模型 | 说明 |
|---------|---------|------|
| **文档摘要** | Qwen-Plus, GLM-4 | 平衡性能和成本 |
| **代码分析** | DeepSeek-Coder | 代码专用 |
| **长文本处理** | Kimi 128K | 超长上下文 |
| **快速响应** | Qwen-Turbo, GLM-4-Flash | 速度优先 |
### 2. 成本优化
- 🎯 **Long Context LLM**: 使用 Qwen-Plus 或 GLM-4处理文档摘要
- ⚡ **Fast LLM**: 使用 Qwen-Turbo 或 GLM-4-Flash快速对话
- 🧠 **Strategic LLM**: 使用 Qwen-Max 或 DeepSeek-Chat复杂推理
### 3. API Key 安全
- ❌ 不要在公开代码中硬编码 API Key
- ✅ 定期轮换 API Key
- ✅ 为不同用途创建不同的 Key
- ✅ 设置合理的额度限制
---
## 📚 相关资源 | Resources
### 官方文档
- [DeepSeek 文档](https://platform.deepseek.com/docs)
- [阿里云百炼文档](https://help.aliyun.com/zh/model-studio/)
- [Moonshot AI 文档](https://platform.moonshot.cn/docs)
- [智谱 AI 文档](https://open.bigmodel.cn/dev/api)
### SurfSense 文档
- [安装指南](../README.md)
- [贡献指南](../CONTRIBUTING.md)
- [部署指南](../DEPLOYMENT_GUIDE.md)
---
## 🆘 需要帮助? | Need Help?
如果遇到问题,可以通过以下方式获取帮助:
- 💬 [GitHub Issues](https://github.com/MODSetter/SurfSense/issues)
- 💬 [Discord Community](https://discord.gg/ejRNvftDp9)
- 📧 Email: [项目维护者邮箱]
---
## 🔄 更新日志 | Changelog
- **2025-01-12**: 初始版本,添加 DeepSeek、Qwen、Kimi、GLM 支持
---
**祝你使用愉快Happy coding with Chinese LLMs! 🚀**

View file

@ -20,6 +20,12 @@ from app.db import Base # Assuming your Base is defined in app.db
# access to the values within the .ini file in use.
config = context.config
# Override SQLAlchemy URL from environment variables when available
# 如果环境变量提供了数据库连接字符串,则优先使用该配置
database_url = os.getenv("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:

View file

@ -0,0 +1,118 @@
"""Add Chinese LLM providers to LiteLLMProvider enum
添加国产 LLM 提供商到 LiteLLMProvider 枚举
Revision ID: 26
Revises: 25
"""
from collections.abc import Sequence
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "26"
down_revision: str | None = "25"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""
Add Chinese LLM providers to LiteLLMProvider enum.
添加国产 LLM 提供商到 LiteLLMProvider 枚举
Adds support for:
- DEEPSEEK: DeepSeek AI models
- ALIBABA_QWEN: Alibaba Qwen (通义千问) models
- MOONSHOT: Moonshot AI (月之暗面 Kimi) models
- ZHIPU: Zhipu AI (智谱 GLM) models
"""
# Add DEEPSEEK to the enum if it doesn't already exist
# 如果不存在则添加 DEEPSEEK 到枚举
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumtypid = 'litellmprovider'::regtype
AND enumlabel = 'DEEPSEEK'
) THEN
ALTER TYPE litellmprovider ADD VALUE 'DEEPSEEK';
END IF;
END$$;
"""
)
# Add ALIBABA_QWEN to the enum if it doesn't already exist
# 如果不存在则添加 ALIBABA_QWEN 到枚举
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumtypid = 'litellmprovider'::regtype
AND enumlabel = 'ALIBABA_QWEN'
) THEN
ALTER TYPE litellmprovider ADD VALUE 'ALIBABA_QWEN';
END IF;
END$$;
"""
)
# Add MOONSHOT to the enum if it doesn't already exist
# 如果不存在则添加 MOONSHOT 到枚举
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumtypid = 'litellmprovider'::regtype
AND enumlabel = 'MOONSHOT'
) THEN
ALTER TYPE litellmprovider ADD VALUE 'MOONSHOT';
END IF;
END$$;
"""
)
# Add ZHIPU to the enum if it doesn't already exist
# 如果不存在则添加 ZHIPU 到枚举
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumtypid = 'litellmprovider'::regtype
AND enumlabel = 'ZHIPU'
) THEN
ALTER TYPE litellmprovider ADD VALUE 'ZHIPU';
END IF;
END$$;
"""
)
def downgrade() -> None:
"""
Remove Chinese LLM providers from LiteLLMProvider enum.
LiteLLMProvider 枚举中移除国产 LLM 提供商
Note: PostgreSQL doesn't support removing enum values directly.
This would require recreating the enum type and updating all dependent objects.
For safety, this downgrade is a no-op.
注意PostgreSQL 不支持直接删除枚举值
这需要重建枚举类型并更新所有依赖对象
为了安全起见此降级操作为空操作
"""
# PostgreSQL doesn't support removing enum values directly
# This would require a complex migration recreating the enum
# PostgreSQL 不支持直接删除枚举值
# 这需要复杂的迁移来重建枚举
pass

View file

@ -78,6 +78,10 @@ class ChatType(str, Enum):
class LiteLLMProvider(str, Enum):
"""
Enum for LLM providers supported by LiteLLM.
LiteLLM 支持的 LLM 提供商枚举
"""
OPENAI = "OPENAI"
ANTHROPIC = "ANTHROPIC"
GROQ = "GROQ"
@ -101,6 +105,11 @@ class LiteLLMProvider(str, Enum):
ALEPH_ALPHA = "ALEPH_ALPHA"
PETALS = "PETALS"
COMETAPI = "COMETAPI"
# Chinese LLM Providers (OpenAI-compatible) / 国产 LLM 提供商OpenAI 兼容)
DEEPSEEK = "DEEPSEEK" # DeepSeek
ALIBABA_QWEN = "ALIBABA_QWEN" # 阿里通义千问
MOONSHOT = "MOONSHOT" # 月之暗面 (Kimi)
ZHIPU = "ZHIPU" # 智谱 AI (GLM)
CUSTOM = "CUSTOM"

View file

@ -1070,6 +1070,7 @@ async def process_file_in_background(
},
)
except Exception as e:
await session.rollback()
await task_logger.log_task_failure(
log_entry,
f"Failed to process file: {filename}",

View file

@ -83,11 +83,11 @@ async def get_user_llm_instance(
)
return None
# Build the model string for litellm
# Build the model string for litellm / 构建 LiteLLM 的模型字符串
if llm_config.custom_provider:
model_string = f"{llm_config.custom_provider}/{llm_config.model_name}"
else:
# Map provider enum to litellm format
# Map provider enum to litellm format / 将提供商枚举映射为 LiteLLM 格式
provider_map = {
"OPENAI": "openai",
"ANTHROPIC": "anthropic",
@ -99,6 +99,11 @@ async def get_user_llm_instance(
"AZURE_OPENAI": "azure",
"OPENROUTER": "openrouter",
"COMETAPI": "cometapi",
# Chinese LLM providers (OpenAI-compatible) / 国产 LLMOpenAI 兼容)
"DEEPSEEK": "openai", # DeepSeek uses OpenAI-compatible API
"ALIBABA_QWEN": "openai", # Qwen uses OpenAI-compatible API
"MOONSHOT": "openai", # Moonshot (Kimi) uses OpenAI-compatible API
"ZHIPU": "openai", # Zhipu (GLM) uses OpenAI-compatible API
# Add more mappings as needed
}
provider_prefix = provider_map.get(

View file

@ -73,6 +73,16 @@ class TaskLoggingService:
Returns:
Log: The updated log entry
"""
# Ensure session is in a valid state / 确保 session 处于有效状态
if not self.session.is_active:
await self.session.rollback()
# Refresh log_entry to avoid expired state / 刷新 log_entry 避免过期状态
try:
await self.session.refresh(log_entry)
except Exception:
pass
# Update the existing log entry
log_entry.status = LogStatus.SUCCESS
log_entry.message = message
@ -114,6 +124,17 @@ class TaskLoggingService:
Returns:
Log: The updated log entry
"""
# Ensure session is in a valid state / 确保 session 处于有效状态
if not self.session.is_active:
await self.session.rollback()
# Refresh log_entry to avoid expired state / 刷新 log_entry 避免过期状态
try:
await self.session.refresh(log_entry)
except Exception:
# If refresh fails, the object might be detached / 如果刷新失败,对象可能已分离
pass
# Update the existing log entry
log_entry.status = LogStatus.FAILED
log_entry.level = LogLevel.ERROR
@ -161,6 +182,16 @@ class TaskLoggingService:
Returns:
Log: The updated log entry
"""
# Ensure session is in a valid state / 确保 session 处于有效状态
if not self.session.is_active:
await self.session.rollback()
# Refresh log_entry to avoid expired state / 刷新 log_entry 避免过期状态
try:
await self.session.refresh(log_entry)
except Exception:
pass
log_entry.message = progress_message
if progress_metadata:

View file

@ -90,6 +90,17 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Handle provider change with auto-fill API Base URL / 处理 Provider 变更并自动填充 API Base URL
const handleProviderChange = (providerValue: string) => {
const provider = LLM_PROVIDERS.find((p) => p.value === providerValue);
setFormData((prev) => ({
...prev,
provider: providerValue,
// Auto-fill API Base URL if provider has a default / 如果提供商有默认值则自动填充
api_base: provider?.apiBase || prev.api_base,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) {
@ -468,7 +479,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<Label htmlFor="provider">Provider *</Label>
<Select
value={formData.provider}
onValueChange={(value) => handleInputChange("provider", value)}
onValueChange={handleProviderChange}
>
<SelectTrigger>
<SelectValue placeholder="Select a provider">
@ -537,13 +548,39 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
</div>
<div className="space-y-2">
<Label htmlFor="api_base">API Base URL (Optional)</Label>
<Label htmlFor="api_base">
API Base URL
{selectedProvider?.apiBase && (
<span className="text-xs font-normal text-muted-foreground ml-2">
(Auto-filled for {selectedProvider.label})
</span>
)}
</Label>
<Input
id="api_base"
placeholder="e.g., https://api.openai.com/v1"
placeholder={selectedProvider?.apiBase || "e.g., https://api.openai.com/v1"}
value={formData.api_base}
onChange={(e) => handleInputChange("api_base", e.target.value)}
/>
{selectedProvider?.apiBase && formData.api_base === selectedProvider.apiBase && (
<p className="text-xs text-green-600 flex items-center gap-1">
<CheckCircle className="h-3 w-3" />
Using recommended API endpoint for {selectedProvider.label}
</p>
)}
{selectedProvider?.apiBase && !formData.api_base && (
<p className="text-xs text-amber-600 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
API Base URL is required for {selectedProvider.label}. Click to auto-fill:
<button
type="button"
className="underline font-medium"
onClick={() => handleInputChange("api_base", selectedProvider.apiBase || "")}
>
{selectedProvider.apiBase}
</button>
</p>
)}
</div>
{/* Optional Inference Parameters */}

View file

@ -3,6 +3,7 @@ export interface LLMProvider {
label: string;
example: string;
description: string;
apiBase?: string; // Default API Base URL for the provider / 提供商的默认 API Base URL
}
export const LLM_PROVIDERS: LLMProvider[] = [
@ -90,6 +91,35 @@ export const LLM_PROVIDERS: LLMProvider[] = [
example: "gpt-5-mini, claude-sonnet-4-5",
description: "Access 500+ AI models through one unified API",
},
// Chinese LLM Providers / 国产 LLM 提供商
{
value: "DEEPSEEK",
label: "DeepSeek",
example: "deepseek-chat, deepseek-coder",
description: "Chinese high-performance AI models",
apiBase: "https://api.deepseek.com",
},
{
value: "ALIBABA_QWEN",
label: "Qwen",
example: "qwen-max, qwen-plus, qwen-turbo",
description: "Alibaba Cloud Qwen LLM",
apiBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
{
value: "MOONSHOT",
label: "Kimi",
example: "moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k",
description: "Moonshot AI Kimi models",
apiBase: "https://api.moonshot.cn/v1",
},
{
value: "ZHIPU",
label: "GLM",
example: "glm-4, glm-4-flash, glm-3-turbo",
description: "Zhipu AI GLM series models",
apiBase: "https://open.bigmodel.cn/api/paas/v4",
},
{
value: "CUSTOM",
label: "Custom Provider",