]> acesimba.cloud Git - codebuddy-web.git/commitdiff
feat: 默认模型固定为 hy3;非黏贴上传保留原始文件名
authorCodebuddy <codebuddy@localhost>
Tue, 18 Aug 2026 13:26:19 +0000 (21:26 +0800)
committerCodebuddy <codebuddy@localhost>
Tue, 18 Aug 2026 13:26:19 +0000 (21:26 +0800)
backend/app.py
frontend/index.html

index 261bb18f6ca62a1610fe406bc34705897b3a0d1c..5177e492f97827fbe8dae484fccac11ebdf40cd3 100644 (file)
@@ -134,27 +134,11 @@ def parse_model_list(help_text: str) -> list:
     return [x.strip() for x in m.group(1).split(",") if x.strip()]
 
 
-def latest_glm(models: list) -> str:
-    """在 glm-* 模型里挑出版本号最大的(即最新 GLM)。"""
-    best = None
-    best_key = None
-    for name in models:
-        if not name.startswith("glm-"):
-            continue
-        base = name[len("glm-"):].split("-")[0]  # 5.2 / 5v / 4.7
-        mm = re.match(r"(\d+)(?:\.(\d+))?", base)
-        if not mm:
-            continue
-        major = int(mm.group(1))
-        minor = int(mm.group(2)) if mm.group(2) else 0
-        key = (major, minor)
-        if best_key is None or key > best_key:
-            best_key, best = key, name
-    return best or ""
-
-
 def scan_models(force: bool = False) -> dict:
-    """扫描模型列表并写入缓存。返回 {models, default, scanned_at}。"""
+    """扫描模型列表并写入缓存。返回 {models, default, scanned_at}。
+
+    系统默认模型固定为 hy3:所有未显式指定模型的会话(含既有与新建)在
+    启动 / 重启时均默认使用 hy3。"""
     global MODELS_CACHE
     now = time.time()
     need_scan = (
@@ -163,6 +147,10 @@ def scan_models(force: bool = False) -> dict:
         or now - MODELS_CACHE.get("scanned_at", 0) > MODELS_SCAN_INTERVAL
     )
     if not need_scan:
+        # 命中缓存:强制默认模型为 hy3,并确保 hy3 在可选列表中
+        MODELS_CACHE["default"] = "hy3"
+        if "hy3" not in MODELS_CACHE.get("models", []):
+            MODELS_CACHE["models"] = ["hy3"] + list(MODELS_CACHE.get("models", []))
         return MODELS_CACHE
 
     models = []
@@ -181,7 +169,10 @@ def scan_models(force: bool = False) -> dict:
         else:
             models = list(FALLBACK_MODELS)
 
-    default = latest_glm(models) or (models[0] if models else "")
+    # 系统默认模型固定为 hy3
+    default = "hy3"
+    if default not in models:
+        models = [default] + models
     MODELS_CACHE = {"models": models, "default": default, "scanned_at": now}
     # 持久化缓存,避免重启后立刻再扫
     try:
@@ -192,7 +183,7 @@ def scan_models(force: bool = False) -> dict:
         os.replace(tmp, MODELS_CACHE_FILE)
     except Exception as e:
         logger.warning(f"写入模型缓存失败: {e}")
-    logger.info(f"模型列表已扫描: 共 {len(models)} 个,默认(最新GLM)={default}")
+    logger.info(f"模型列表已扫描: 共 {len(models)} 个,默认(hy3)={default}")
     return MODELS_CACHE
 
 
@@ -679,7 +670,7 @@ async def api_resume_task(task_id: str, request: Request):
 
 @app.post("/api/tasks/{task_id}/model")
 async def api_set_model(task_id: str, request: Request):
-    """设置任务的模型。model 为空字符串表示跟随默认(最新 GLM)。"""
+    """设置任务的模型。model 为空字符串表示跟随默认(hy3)。"""
     if not _auth_ok(request):
         return JSONResponse({"ok": False, "error": "未授权"}, status_code=401)
     try:
@@ -699,7 +690,7 @@ async def api_set_model(task_id: str, request: Request):
             return JSONResponse({"ok": False, "error": f"未知模型: {model}"}, status_code=400)
         t["model"] = model
     else:
-        # 空字符串:清除显式选择,跟随默认最新 GLM
+        # 空字符串:清除显式选择,跟随默认 hy3
         t["model"] = ""
     save_tasks(data)
     logger.info(f"设置模型: id={task_id}, name={t.get('name')}, model='{t['model']}'")
@@ -763,6 +754,19 @@ def save_uploads_index(idx: dict):
         logger.warning(f"保存上传索引失败: {e}")
 
 
+def _safe_basename(name: str) -> str:
+    """从上传文件名中提取安全的基础名:去掉目录、控制字符与危险字符
+    (含路径分隔符、Windows 非法字符 :*?\"<>|),并去除开头的点/下划线
+    (避免与‘禁止删除以 ._ 开头文件’的保护逻辑冲突)。保留 Unicode 原文件名。"""
+    base = (name or "").replace("\\", "/").rsplit("/", 1)[-1]
+    bad = set('/\\:*?"<>|\x00')
+    base = "".join(c for c in base if ord(c) >= 0x20 and c not in bad)
+    base = base.lstrip("._").strip()
+    if len(base) > 120:
+        base = base[:120]
+    return base
+
+
 @app.post("/api/upload-image")
 async def upload_image(request: Request):
     if not _auth_ok(request):
@@ -773,6 +777,7 @@ async def upload_image(request: Request):
         return JSONResponse({"error": "invalid json body"}, status_code=400)
     mime = (body.get("mime") or "").lower()
     name = body.get("name") or ""
+    pasted = bool(body.get("pasted"))
     b64 = body.get("data") or ""
     import base64
     try:
@@ -783,14 +788,25 @@ async def upload_image(request: Request):
         return JSONResponse({"error": "empty file"}, status_code=400)
     if len(raw) > MAX_UPLOAD_SIZE:
         return JSONResponse({"error": f"file too large (max {MAX_UPLOAD_SIZE // 1024 // 1024}MB)"}, status_code=400)
-    # 扩展名:优先用原文件名,其次按 mime 推断,否则 bin
-    ext = ""
-    if name and "." in name:
-        ext = name.rsplit(".", 1)[1].lower()[:12]
-    if not ext:
-        ext = _MIME_EXT.get(mime, "bin")
     PASTES_DIR.mkdir(parents=True, exist_ok=True)
-    fname = f"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_{secrets.token_hex(4)}.{ext}"
+    if pasted or not name.strip():
+        # 黏贴进来的图片(或缺失原名):沿用自动生成文件名
+        ext = ""
+        if name and "." in name:
+            ext = name.rsplit(".", 1)[1].lower()[:12]
+        if not ext:
+            ext = _MIME_EXT.get(mime, "bin")
+        fname = f"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_{secrets.token_hex(4)}.{ext}"
+    else:
+        # 其余(附件 / 拖拽):保留原始文件名(sanitize 防路径穿越,并处理同名冲突)
+        base = _safe_basename(name)
+        if not base:
+            base = "file"
+        fname = base
+        if (PASTES_DIR / fname).exists():
+            stem, _, suf = base.rpartition(".")
+            suffix = secrets.token_hex(4)
+            fname = f"{stem}_{suffix}.{suf}" if suf else f"{base}_{suffix}"
     fpath = PASTES_DIR / fname
     fpath.write_bytes(raw)
     # 记录所属会话任务(前端上传时带 task_id;列表页据此显示“所属会话任务”)
@@ -1297,7 +1313,7 @@ async def websocket_terminal(websocket: WebSocket):
         await websocket.close()
         return
 
-    # 决定生效模型:任务显式设置 > 默认最新 GLM
+    # 决定生效模型:任务显式设置 > 默认 hy3
     sid = task["session_id"]
     with MODELS_CACHE_LOCK:
         _default_model = MODELS_CACHE.get("default") or (MODELS_CACHE.get("models") or [""])[0] or "glm-5.2"
index d6de32e28c35e8888095645123baf456176757f6..7e9d2a1beb132ea62e5f5b4860cb93e50589e4ae 100644 (file)
@@ -314,7 +314,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
       <span class="task-name" id="task-title"></span>
       <span class="task-cwd" id="task-cwd"></span>
       <select id="model-select" class="model-select" title="切换模型(切换将重启会话)">
-        <option value="">默认 · 最新 GLM</option>
+        <option value="">默认 · hy3</option>
       </select>
     </div>
     <div class="actions">
@@ -767,11 +767,11 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
       if (!data.ok) return;
       const models = data.models || [];
       const def = data.default || '';
-      // 重建选项:第一项“默认·最新 GLM”
+      // 重建选项:第一项为“默认·hy3”
       modelSelect.innerHTML = '';
       const optDef = document.createElement('option');
       optDef.value = '';
-      optDef.textContent = def ? ('默认 · 最新 GLM (' + def + ')') : '默认 · 最新 GLM';
+      optDef.textContent = def ? ('默认 · ' + def) : '默认';
       modelSelect.appendChild(optDef);
       models.forEach(m => {
         const o = document.createElement('option');
@@ -859,7 +859,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
   let pendingFiles = [];
 
   // 上传任意文件到后端,返回 {path, name} 或抛错(上限 20MB)
-  async function uploadFile(blob) {
+  async function uploadFile(blob, pasted) {
     const MAX = 20 * 1024 * 1024;
     if (blob.size > MAX) {
       const mb = Math.round(blob.size / 1024 / 1024 * 10) / 10;
@@ -878,7 +878,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
     const r = await fetch(API + '/api/upload-image', {
       method: 'POST',
       headers: Object.assign({}, authHeaders(), { 'Content-Type': 'application/json' }),
-      body: JSON.stringify({ mime: blob.type, name: name, data: b64, task_id: TASK_ID || "" })
+      body: JSON.stringify({ mime: blob.type, name: name, data: b64, task_id: TASK_ID || "", pasted: !!pasted })
     });
     if (!r.ok) {
       let msg = 'HTTP ' + r.status;
@@ -954,7 +954,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
     }
     if (fileBlob) {
       e.preventDefault();
-      try { const f = await uploadFile(fileBlob); pendingFiles.push(f); renderPasteChips(); }
+      try { const f = await uploadFile(fileBlob, true); pendingFiles.push(f); renderPasteChips(); }
       catch (err) { term.writeln('\x1b[31m✗ 文件上传失败: ' + err.message + '\x1b[0m'); }
       return;
     }
@@ -969,7 +969,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
     if (!files.length) return;
     e.preventDefault();
     for (const f of files) {
-      try { const uf = await uploadFile(f); pendingFiles.push(uf); }
+      try { const uf = await uploadFile(f, false); pendingFiles.push(uf); }
       catch (err) { term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); }
     }
     renderPasteChips();
@@ -980,7 +980,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
   pasteFileInput.onchange = async () => {
     if (!pasteFileInput.files || !pasteFileInput.files.length) return;
     for (const f of pasteFileInput.files) {
-      try { const uf = await uploadFile(f); pendingFiles.push(uf); }
+      try { const uf = await uploadFile(f, false); pendingFiles.push(uf); }
       catch (err) { term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); }
     }
     pasteFileInput.value = '';