From ee0b7f08a07687a53004b636e858a7759a63de97 Mon Sep 17 00:00:00 2001 From: CJACK Date: Fri, 13 Feb 2026 22:30:55 +0800 Subject: [PATCH 1/4] refactor: centralize account cleanup in API routes and refine login checkbox styling. --- routes/claude.py | 2 +- routes/openai.py | 3 +-- webui/src/components/Login.jsx | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/routes/claude.py b/routes/claude.py index 706c022..e9c7b25 100644 --- a/routes/claude.py +++ b/routes/claude.py @@ -314,7 +314,7 @@ Remember: Output ONLY the JSON, no other text. The response must start with {{ a deepseek_resp.close() except Exception: pass - cleanup_account(request) + # 注意:不在此处调用 cleanup_account,由外层 finally 统一处理 return StreamingResponse( claude_sse_stream(), diff --git a/routes/openai.py b/routes/openai.py index 5ba60f2..f79283f 100644 --- a/routes/openai.py +++ b/routes/openai.py @@ -435,8 +435,7 @@ IMPORTANT: If calling tools, output ONLY the JSON. The response must start with except Exception as e: logger.error(f"[sse_stream] 异常: {e}") - finally: - cleanup_account(request) + # 注意:不在此处调用 cleanup_account,由外层 finally 统一处理 return StreamingResponse( sse_stream(), diff --git a/webui/src/components/Login.jsx b/webui/src/components/Login.jsx index 2ebe5d6..c06c12e 100644 --- a/webui/src/components/Login.jsx +++ b/webui/src/components/Login.jsx @@ -87,8 +87,8 @@ export default function Login({ onLogin, onMessage }) { checked={remember} onChange={e => setRemember(e.target.checked)} /> -
- +
+ {t('login.rememberSession')} From 648b80bb7b636b6a6639c2ef07f8895f7052531c Mon Sep 17 00:00:00 2001 From: CJACK Date: Fri, 13 Feb 2026 22:44:39 +0800 Subject: [PATCH 2/4] feat: Add support for parsing dictionary-based SSE fragments and prevent duplicate stream termination messages. --- core/config.py | 5 +++++ core/sse_parser.py | 20 ++++++++++++++++++++ routes/openai.py | 6 ++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/config.py b/core/config.py index 83e02ca..f215691 100644 --- a/core/config.py +++ b/core/config.py @@ -32,9 +32,14 @@ logger = logging.getLogger("ds2api") # -------------------------- 初始化 tokenizer -------------------------- chat_tokenizer_dir = resolve_path("DS2API_TOKENIZER_DIR", "") +# 抑制 Mistral tokenizer regex 警告(不影响 DeepSeek tokenization) +_tf_logger = logging.getLogger("transformers") +_tf_log_level = _tf_logger.level +_tf_logger.setLevel(logging.ERROR) tokenizer = transformers.AutoTokenizer.from_pretrained( chat_tokenizer_dir, trust_remote_code=True ) +_tf_logger.setLevel(_tf_log_level) # ---------------------------------------------------------------------- # 配置文件的读写函数 diff --git a/core/sse_parser.py b/core/sse_parser.py index 481c872..e1c234e 100644 --- a/core/sse_parser.py +++ b/core/sse_parser.py @@ -255,6 +255,26 @@ def parse_sse_chunk_for_content( return ([], True, new_fragment_type) contents.extend(result) + # 处理字典值(初始响应 chunk,包含 response.fragments) + elif isinstance(v_value, dict): + response_obj = v_value.get("response", v_value) + fragments = response_obj.get("fragments", []) + if isinstance(fragments, list): + for frag in fragments: + if isinstance(frag, dict): + frag_type = frag.get("type", "").upper() + frag_content = frag.get("content", "") + if frag_type == "THINK" or frag_type == "THINKING": + new_fragment_type = "thinking" + if frag_content: + contents.append((frag_content, "thinking")) + elif frag_type == "RESPONSE": + new_fragment_type = "text" + if frag_content: + contents.append((frag_content, "text")) + elif frag_content: + contents.append((frag_content, ptype)) + return (contents, False, new_fragment_type) diff --git a/routes/openai.py b/routes/openai.py index f79283f..2225581 100644 --- a/routes/openai.py +++ b/routes/openai.py @@ -194,6 +194,7 @@ IMPORTANT: If calling tools, output ONLY the JSON. The response must start with last_content_time = time.time() # 最后收到有效内容的时间 keepalive_count = 0 # 连续 keepalive 计数 has_content = False # 是否收到过内容 + stream_finished = False # 是否已发送过结束标记 def process_data(): """处理 DeepSeek SSE 数据流 - 使用 sse_parser 模块""" @@ -343,6 +344,7 @@ IMPORTANT: If calling tools, output ONLY the JSON. The response must start with yield f"data: {json.dumps(finish_chunk, ensure_ascii=False)}\n\n" yield "data: [DONE]\n\n" last_send_time = current_time + stream_finished = True break new_choices = [] @@ -391,8 +393,8 @@ IMPORTANT: If calling tools, output ONLY the JSON. The response must start with except queue.Empty: continue - # 如果是超时退出,也发送结束标记 - if has_content: + # 如果是超时退出且尚未发送结束标记,补发结束标记 + if has_content and not stream_finished: prompt_tokens = len(final_prompt) // 4 thinking_tokens = len(final_thinking) // 4 completion_tokens = len(final_text) // 4 From 85ac0d95a4762902937062627ccbda995a70f802 Mon Sep 17 00:00:00 2001 From: CJACK Date: Fri, 13 Feb 2026 23:11:15 +0800 Subject: [PATCH 3/4] feat: Display Vercel configuration sync status and last sync time, and clear API tester streaming output before new requests. --- routes/admin/vercel.py | 42 ++++++++++++++++++++ static/admin/.gitkeep | 1 - webui/src/components/ApiTester.jsx | 20 ++++++---- webui/src/components/VercelSync.jsx | 61 +++++++++++++++++++++++++---- webui/src/locales/en.json | 6 ++- webui/src/locales/zh.json | 6 ++- 6 files changed, 118 insertions(+), 18 deletions(-) delete mode 100644 static/admin/.gitkeep diff --git a/routes/admin/vercel.py b/routes/admin/vercel.py index 91b03d3..cb365b5 100644 --- a/routes/admin/vercel.py +++ b/routes/admin/vercel.py @@ -2,8 +2,10 @@ """Admin Vercel 模块 - Vercel 同步和部署""" import asyncio import base64 +import hashlib import json import os +import time as _time import httpx from fastapi import APIRouter, HTTPException, Request, Depends @@ -23,6 +25,19 @@ VERCEL_PROJECT_ID = os.getenv("VERCEL_PROJECT_ID", "") VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID", "") +def _compute_config_hash() -> str: + """计算可同步配置的指纹哈希(仅包含 keys 和 accounts)""" + syncable = { + "keys": CONFIG.get("keys", []), + "accounts": [ + {k: v for k, v in acc.items() if k != "token"} + for acc in CONFIG.get("accounts", []) + ], + } + raw = json.dumps(syncable, sort_keys=True, ensure_ascii=False, separators=(",", ":")) + return hashlib.md5(raw.encode("utf-8")).hexdigest() + + # ---------------------------------------------------------------------- # API 测试(通过本地 API) # ---------------------------------------------------------------------- @@ -228,6 +243,10 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)): if deploy_resp.status_code in [200, 201]: deploy_data = deploy_resp.json() + # 记录同步哈希和时间 + CONFIG["_vercel_sync_hash"] = _compute_config_hash() + CONFIG["_vercel_sync_time"] = int(_time.time()) + save_config(CONFIG) result = { "success": True, "message": "配置已同步,正在重新部署...", @@ -240,6 +259,10 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)): result["saved_credentials"] = saved_credentials return JSONResponse(content=result) + # 环境变量已更新,但无法自动触发重新部署 + CONFIG["_vercel_sync_hash"] = _compute_config_hash() + CONFIG["_vercel_sync_time"] = int(_time.time()) + save_config(CONFIG) result = { "success": True, "message": "配置已同步到 Vercel,请手动触发重新部署", @@ -259,6 +282,25 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)): raise HTTPException(status_code=500, detail=str(e)) +# ---------------------------------------------------------------------- +# 同步状态查询 +# ---------------------------------------------------------------------- +@router.get("/vercel/status") +async def get_vercel_sync_status(_: bool = Depends(verify_admin)): + """检查当前配置与上次同步到 Vercel 的配置是否一致""" + last_hash = CONFIG.get("_vercel_sync_hash", "") + last_time = CONFIG.get("_vercel_sync_time", 0) + current_hash = _compute_config_hash() + + synced = bool(last_hash and last_hash == current_hash) + + return JSONResponse(content={ + "synced": synced, + "last_sync_time": last_time if last_time else None, + "has_synced_before": bool(last_hash), + }) + + # ---------------------------------------------------------------------- # 导出配置 # ---------------------------------------------------------------------- diff --git a/static/admin/.gitkeep b/static/admin/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/static/admin/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/webui/src/components/ApiTester.jsx b/webui/src/components/ApiTester.jsx index 87fc7d9..017e7f6 100644 --- a/webui/src/components/ApiTester.jsx +++ b/webui/src/components/ApiTester.jsx @@ -154,6 +154,10 @@ export default function ApiTester({ config, onMessage, authFetch }) { } const sendTest = async () => { + // 清除上次的流式/思考内容,防止残留 + setStreamingContent('') + setStreamingThinking('') + if (selectedAccount) { setLoading(true) setResponse(null) @@ -209,12 +213,12 @@ export default function ApiTester({ config, onMessage, authFetch }) { onClick={() => setConfigExpanded(!configExpanded)} className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors" > -
-
- -
- {t('apiTester.config')} +
+
+
+ {t('apiTester.config')} +
@@ -365,9 +369,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { {/* Input Area */}
-