From bfec192158627b0a0285e62b8e728bce1e8514c1 Mon Sep 17 00:00:00 2001 From: SuperManTouX <93423476+SuperManTouX@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:00:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E4=BB=8E=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8=E5=B9=B6?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=B8=B2=E6=9F=93=E5=AF=B9=E5=BA=94=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E9=80=BB=E8=BE=91=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BA=A4=E4=BA=92=E4=B8=80=E8=87=B4=E6=80=A7=20[?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=9A=E6=8C=89=E9=92=AE=E6=A0=B9=E6=8D=AE?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=94=AF=E6=8C=81=E7=9A=84=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=94=9F=E6=88=90]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG-2026-03-06.md | 72 ++++++++++++++++++++++++- server/adapters/base.py | 11 +++- server/adapters/dashscope_adapter.py | 46 +++++++++++++--- server/adapters/glm_adapter.py | 16 ++++++ server/adapters/openai_adapter.py | 64 ++++++++++++++++++----- src/components/chat/ChatMain.vue | 44 ++++++++++++++-- src/components/input/ChatInput.vue | 69 ++++++++++++++++++++---- src/services/api.ts | 78 +++++++++++++++++----------- 8 files changed, 332 insertions(+), 68 deletions(-) diff --git a/docs/CHANGELOG-2026-03-06.md b/docs/CHANGELOG-2026-03-06.md index b8b1c83..4b2ed74 100644 --- a/docs/CHANGELOG-2026-03-06.md +++ b/docs/CHANGELOG-2026-03-06.md @@ -168,11 +168,74 @@ def _supports_thinking(self, model: str) -> bool: --- +## 功能:模型列表从后端动态获取 + +### 需求 +前端模型选择器中的模型列表改为从后端 API 获取,而非硬编码。 + +### 解决方案 + +#### 前端 `src/services/api.ts` + +修改 `getModels` 方法,调用后端 API: + +```typescript +async getModels(): Promise { + try { + const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.MODELS}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`获取模型列表失败: HTTP ${response.status}`); + } + + const data = await response.json(); + // 后端返回格式: { object: "list", data: [...] } + return data.data || []; + } catch (error) { + console.error("获取模型列表失败:", error); + // 返回默认模型列表作为降级 + return [...]; + } +} +``` + +#### 后端 `server/main.py` + +已有 `/api/chat-ui/models` 端点: + +```python +@app.get("/api/chat-ui/models") +async def get_models(): + """模型列表(聚合所有可用平台的模型)""" + from adapters import get_all_adapters + + all_models = [] + for provider, adapter in get_all_adapters().items(): + if adapter.is_available(): + models = adapter.list_models() + all_models.extend([m.to_dict() for m in models]) + + return {"object": "list", "data": all_models} +``` + +#### 特点 + +- **动态聚合**:自动聚合所有已配置 API Key 的平台模型 +- **按需显示**:只有配置了 API Key 的平台才会显示模型 +- **降级处理**:API 获取失败时返回默认模型列表 + +--- + ## 涉及文件 | 文件 | 修改类型 | |------|----------| -| `src/services/api.ts` | 新增 `StreamChunk` 接口,修改 `streamChat` 方法 | +| `src/services/api.ts` | 新增 `StreamChunk` 接口,修改 `streamChat` 方法,修改 `getModels` 方法 | | `src/components/chat/ChatMain.vue` | 修改流式处理逻辑,支持 `reasoning` 类型 | | `server/adapters/glm_adapter.py` | 修改 `_build_messages` 和 `_resolve_model` 方法 | | `server/adapters/openai_adapter.py` | 添加 DeepSeek 深度思考支持 | @@ -196,4 +259,9 @@ def _supports_thinking(self, model: str) -> bool: - 选择非多模态模型(如 glm-4-flash) - 上传图片或 PDF 文件 - 确认后端日志显示模型切换为 glm-4.6v - - 确认多模态内容正确处理 \ No newline at end of file + - 确认多模态内容正确处理 + +4. **模型列表动态获取测试**: + - 检查前端控制台,确认调用了 `/api/chat-ui/models` + - 确认模型选择器显示后端返回的模型列表 + - 停止后端服务,确认降级模型列表正常显示 \ No newline at end of file diff --git a/server/adapters/base.py b/server/adapters/base.py index e18da2f..a7ea570 100644 --- a/server/adapters/base.py +++ b/server/adapters/base.py @@ -16,6 +16,11 @@ class ModelInfo: description: str max_tokens: int = 4096 provider: str = "unknown" + # 能力标志 + supports_thinking: bool = False # 是否支持深度思考 + supports_web_search: bool = False # 是否支持在线搜索 + supports_vision: bool = False # 是否支持图片识别 + supports_files: bool = False # 是否支持文件附件(PDF、DOCX等) def to_dict(self) -> Dict[str, Any]: return { @@ -24,6 +29,10 @@ class ModelInfo: "description": self.description, "maxTokens": self.max_tokens, "provider": self.provider, + "supports_thinking": self.supports_thinking, + "supports_web_Search": self.supports_web_search, + "supports_vision": self.supports_vision, + "supports_files": self.supports_files, } @@ -123,4 +132,4 @@ class BaseAdapter(ABC): return { "object": "list", "data": [m.to_dict() for m in models], - } \ No newline at end of file + } diff --git a/server/adapters/dashscope_adapter.py b/server/adapters/dashscope_adapter.py index 5df24ce..7728b22 100644 --- a/server/adapters/dashscope_adapter.py +++ b/server/adapters/dashscope_adapter.py @@ -22,6 +22,10 @@ DASHSCOPE_MODELS = [ description="最强大的模型", max_tokens=8192, provider="Aliyun", + supports_thinking=True, + supports_web_search=False, + supports_vision=False, + supports_files=False, ), ModelInfo( id="qwen-plus", @@ -29,6 +33,10 @@ DASHSCOPE_MODELS = [ description="能力均衡", max_tokens=8192, provider="Aliyun", + supports_thinking=False, + supports_web_search=False, + supports_vision=False, + supports_files=False, ), ModelInfo( id="qwen-turbo", @@ -36,6 +44,10 @@ DASHSCOPE_MODELS = [ description="速度更快、成本更低", max_tokens=8192, provider="Aliyun", + supports_thinking=False, + supports_web_search=False, + supports_vision=False, + supports_files=False, ), ModelInfo( id="qwen-vl-max", @@ -43,6 +55,10 @@ DASHSCOPE_MODELS = [ description="支持视觉理解的多模态模型", max_tokens=8192, provider="Aliyun", + supports_thinking=False, + supports_web_search=False, + supports_vision=True, + supports_files=False, ), ModelInfo( id="qwen-vl-plus", @@ -50,6 +66,10 @@ DASHSCOPE_MODELS = [ description="支持视觉理解的多模态模型", max_tokens=8192, provider="Aliyun", + supports_thinking=False, + supports_web_search=False, + supports_vision=True, + supports_files=False, ), ] @@ -84,7 +104,9 @@ class DashScopeAdapter(BaseAdapter): logger.info(f" - temperature: {request.temperature}") logger.info(f" - max_tokens: {request.max_tokens}") logger.info(f" - files: {request.files}") - logger.info(f" - messages: {json.dumps(request.messages, ensure_ascii=False, indent=2)}") + logger.info( + f" - messages: {json.dumps(request.messages, ensure_ascii=False, indent=2)}" + ) # 检测是否包含多模态内容 has_multimodal = self._has_multimodal_content(request) @@ -161,8 +183,10 @@ class DashScopeAdapter(BaseAdapter): if resp.status_code == 200: chunk_count += 1 content = resp.output.choices[0].message.content - if content: - full_content += content + if content and len(content) > len(full_content): + # DashScope 流式响应返回完整内容,计算增量 + delta = content[len(full_content) :] + full_content = content data = { "id": f"chatcmpl-{generate_unique_id()}", "object": "chat.completion.chunk", @@ -171,7 +195,7 @@ class DashScopeAdapter(BaseAdapter): "choices": [ { "index": 0, - "delta": {"content": content}, + "delta": {"content": delta}, "finish_reason": None, } ], @@ -192,7 +216,11 @@ class DashScopeAdapter(BaseAdapter): logger.info(f"[DashScope] 流式文本响应完成:") logger.info(f" - chunks: {chunk_count}") logger.info(f" - content_length: {len(full_content)} 字符") - logger.info(f" - content_preview: {full_content[:200]}..." if len(full_content) > 200 else f" - content: {full_content}") + logger.info( + f" - content_preview: {full_content[:200]}..." + if len(full_content) > 200 + else f" - content: {full_content}" + ) return StreamingResponse(generator(), media_type="text/event-stream") @@ -237,7 +265,11 @@ class DashScopeAdapter(BaseAdapter): # 打印响应结果 logger.info(f"[DashScope] 响应结果:") logger.info(f" - content_length: {len(content)} 字符") - logger.info(f" - content_preview: {content[:200]}..." if len(content) > 200 else f" - content: {content}") + logger.info( + f" - content_preview: {content[:200]}..." + if len(content) > 200 + else f" - content: {content}" + ) if hasattr(resp, "usage") and resp.usage: logger.info(f" - usage: {response['usage']}") @@ -431,4 +463,4 @@ class DashScopeAdapter(BaseAdapter): return JSONResponse( status_code=500, content={"error": f"DashScope Error: {resp.code} - {resp.message}"}, - ) \ No newline at end of file + ) diff --git a/server/adapters/glm_adapter.py b/server/adapters/glm_adapter.py index 77711b5..f7e6897 100644 --- a/server/adapters/glm_adapter.py +++ b/server/adapters/glm_adapter.py @@ -23,6 +23,10 @@ GLM_MODELS = [ description="最新旗舰模型,支持文本/图像/文档/深度思考", max_tokens=128000, provider="ZhipuAI", + supports_thinking=True, + supports_web_search=True, + supports_vision=True, + supports_files=True, ), ModelInfo( id="glm-4-flash", @@ -30,6 +34,10 @@ GLM_MODELS = [ description="高性价比文本模型", max_tokens=128000, provider="ZhipuAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=False, + supports_files=False, ), ModelInfo( id="glm-4v-plus-0111", @@ -37,6 +45,10 @@ GLM_MODELS = [ description="图像 + PDF/DOCX 原生多模态", max_tokens=128000, provider="ZhipuAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=True, + supports_files=True, ), ModelInfo( id="glm-z1-flash", @@ -44,6 +56,10 @@ GLM_MODELS = [ description="深度思考推理模型", max_tokens=128000, provider="ZhipuAI", + supports_thinking=True, + supports_web_search=False, + supports_vision=False, + supports_files=False, ), ] diff --git a/server/adapters/openai_adapter.py b/server/adapters/openai_adapter.py index d1a4dcf..6ff980e 100644 --- a/server/adapters/openai_adapter.py +++ b/server/adapters/openai_adapter.py @@ -22,6 +22,10 @@ OPENAI_MODELS = [ description="最新旗舰多模态模型", max_tokens=128000, provider="OpenAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=True, + supports_files=True, ), ModelInfo( id="gpt-4o-mini", @@ -29,6 +33,10 @@ OPENAI_MODELS = [ description="高性价比多模态模型", max_tokens=128000, provider="OpenAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=True, + supports_files=True, ), ModelInfo( id="gpt-4-turbo", @@ -36,6 +44,10 @@ OPENAI_MODELS = [ description="GPT-4 增强版", max_tokens=128000, provider="OpenAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=True, + supports_files=False, ), ModelInfo( id="gpt-3.5-turbo", @@ -43,6 +55,10 @@ OPENAI_MODELS = [ description="快速经济的选择", max_tokens=16385, provider="OpenAI", + supports_thinking=False, + supports_web_search=True, + supports_vision=False, + supports_files=False, ), ] @@ -54,6 +70,10 @@ DEEPSEEK_MODELS = [ description="Deepseek 对话模型", max_tokens=64000, provider="Deepseek", + supports_thinking=False, + supports_web_search=False, + supports_vision=False, + supports_files=False, ), ModelInfo( id="deepseek-reasoner", @@ -61,6 +81,10 @@ DEEPSEEK_MODELS = [ description="Deepseek 推理模型(支持深度思考)", max_tokens=64000, provider="Deepseek", + supports_thinking=True, + supports_web_search=True, # 注:通过内置检索增强实现 + supports_vision=False, + supports_files=False, ), ] @@ -94,9 +118,7 @@ class OpenAIAdapter(BaseAdapter): if self._provider_type == "deepseek": api_key = os.getenv("DEEPSEEK_API_KEY", "") - base_url = os.getenv( - "DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1" - ) + base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") else: api_key = os.getenv("OPENAI_API_KEY", "") base_url = os.getenv("OPENAI_BASE_URL") # 可选自定义端点 @@ -133,7 +155,9 @@ class OpenAIAdapter(BaseAdapter): # 构建消息 messages = self._build_messages(request) - logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}") + logger.info( + f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}" + ) # 构建请求参数 kwargs = { @@ -150,7 +174,9 @@ class OpenAIAdapter(BaseAdapter): if self._supports_thinking(request.model): extra_body = {"thinking": {"type": "enabled"}} kwargs["extra_body"] = extra_body - logger.info(f"[{provider_name}] 深度思考已启用: extra_body = {extra_body}") + logger.info( + f"[{provider_name}] 深度思考已启用: extra_body = {extra_body}" + ) if request.stream: return self._stream_chat(client, kwargs, extra_body) @@ -184,7 +210,9 @@ class OpenAIAdapter(BaseAdapter): return messages - def _stream_chat(self, client, kwargs: Dict, extra_body: Optional[Dict] = None) -> StreamingResponse: + def _stream_chat( + self, client, kwargs: Dict, extra_body: Optional[Dict] = None + ) -> StreamingResponse: """流式聊天""" provider_name = self._provider_type.upper() logger.info(f"[{provider_name}] 开始流式响应...") @@ -242,11 +270,17 @@ class OpenAIAdapter(BaseAdapter): logger.info(f" - content_length: {len(full_content)} 字符") if full_reasoning: logger.info(f" - reasoning_length: {len(full_reasoning)} 字符") - logger.info(f" - content_preview: {full_content[:200]}..." if len(full_content) > 200 else f" - content: {full_content}") + logger.info( + f" - content_preview: {full_content[:200]}..." + if len(full_content) > 200 + else f" - content: {full_content}" + ) return StreamingResponse(generator(), media_type="text/event-stream") - def _sync_chat(self, client, kwargs: Dict, extra_body: Optional[Dict] = None) -> JSONResponse: + def _sync_chat( + self, client, kwargs: Dict, extra_body: Optional[Dict] = None + ) -> JSONResponse: """非流式聊天""" from utils.helpers import generate_unique_id, get_current_timestamp @@ -273,9 +307,9 @@ class OpenAIAdapter(BaseAdapter): # 添加推理内容(如有) if hasattr(message, "reasoning_content") and message.reasoning_content: - response["choices"][0]["message"]["reasoning_content"] = ( - message.reasoning_content - ) + response["choices"][0]["message"][ + "reasoning_content" + ] = message.reasoning_content if resp.usage: response["usage"] = { @@ -290,7 +324,11 @@ class OpenAIAdapter(BaseAdapter): logger.info(f" - content_length: {len(content)} 字符") if hasattr(message, "reasoning_content") and message.reasoning_content: logger.info(f" - reasoning_length: {len(message.reasoning_content)} 字符") - logger.info(f" - content_preview: {content[:200]}..." if len(content) > 200 else f" - content: {content}") + logger.info( + f" - content_preview: {content[:200]}..." + if len(content) > 200 + else f" - content: {content}" + ) if resp.usage: logger.info(f" - usage: {response['usage']}") @@ -301,4 +339,4 @@ class DeepseekAdapter(OpenAIAdapter): """Deepseek 平台适配器(继承 OpenAI 适配器)""" def __init__(self): - super().__init__(provider_type="deepseek") \ No newline at end of file + super().__init__(provider_type="deepseek") diff --git a/src/components/chat/ChatMain.vue b/src/components/chat/ChatMain.vue index 549053b..a41e7bf 100644 --- a/src/components/chat/ChatMain.vue +++ b/src/components/chat/ChatMain.vue @@ -35,6 +35,10 @@ :is-streaming="isStreaming" :send-on-enter="settings.sendOnEnter" :disabled="false" + :supports_thinking="currentModelCapabilities.supports_thinking" + :supports_web_search="currentModelCapabilities.supports_web_search" + :supports_vision="currentModelCapabilities.supports_vision" + :supports_files="currentModelCapabilities.supports_files" @send="handleSend" @stop="handleStop" /> @@ -44,7 +48,7 @@