From 84a10c4d0774677b76bf57f5a19f90cda08effc0 Mon Sep 17 00:00:00 2001 From: Codebuddy Date: Tue, 18 Aug 2026 21:26:19 +0800 Subject: [PATCH] =?utf8?q?feat:=20=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B?= =?utf8?q?=E5=9B=BA=E5=AE=9A=E4=B8=BA=20hy3=EF=BC=9B=E9=9D=9E=E9=BB=8F?= =?utf8?q?=E8=B4=B4=E4=B8=8A=E4=BC=A0=E4=BF=9D=E7=95=99=E5=8E=9F=E5=A7=8B?= =?utf8?q?=E6=96=87=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- backend/app.py | 80 +++++++++++++++++++++++++++------------------ frontend/index.html | 16 ++++----- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/backend/app.py b/backend/app.py index 261bb18..5177e49 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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" diff --git a/frontend/index.html b/frontend/index.html index d6de32e..7e9d2a1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -314,7 +314,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
@@ -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 = ''; -- 2.43.0