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 = (
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 = []
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:
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
@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:
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']}'")
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):
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:
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;列表页据此显示“所属会话任务”)
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"
<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">
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');
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;
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;
}
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;
}
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();
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 = '';