修复 execDbWrite 写死 python 导致 WSL 停用/恢复全失效 + toast 死循环

- execDbWrite spawn 可执行名跨平台:win32 用 python,其余用 python3(与 readCatalog/readCatalogTime 一致)
  WSL Ubuntu 24.04 无 python,spawn-fail -> 停用/恢复出厂全失效(截图实锤)
- reconcile 重放失败(spawn-fail/unknown op/Traceback/rc>=1)时不写成功 flush:
  此前每 60s force 对账轮发现回潮 -> 重放失败 -> 照写 flush -> TUI 每 60s reload+toast 死循环
This commit is contained in:
youpaishidifu
2026-10-20 23:13:03 +08:00
parent 229359a052
commit b56f4df500
+29 -6
View File
@@ -303,7 +303,12 @@ sys.stdout.buffer.write(row[0].encode("utf-8"))
// 执行 DB 写(fork Python 子进程,独立 SQLite 连接)
async function execDbWrite(op: string, payload: any): Promise<string> {
return new Promise((resolve) => {
const r = spawn("python", [DB_EXEC, op, JSON.stringify(payload)], {
// ★ 跨平台可执行名:WSL(Ubuntu 24.04) 无 "python" 只有 "python3",
// 写死 "python" 会 spawn-fail(Executable not found in $PATH)→ 停用/恢复全失效,
// 且 outbox 条目永不清队 → daemon 每 5s 重试失败 → 反复写 flush → TUI 反复 toast。
// 与 readCatalog / readCatalogTime 的分流保持一致(2026-10-20 截图实锤)。
const py = process.platform === "win32" ? "python" : "python3"
const r = spawn(py, [DB_EXEC, op, JSON.stringify(payload)], {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true, // 防 Windows 弹黑色控制台窗
})
@@ -709,9 +714,10 @@ let recTicks = 0
async function reconcile(ctx: any): Promise<void> {
ensureCfg()
const stopList = readStopList()
const removed = loadRemoved() // 2026-10-20:已删除厂商也纳入压制(修 agnes 残留在 catalog)
const mirror = loadMirror()
const hasMirror = Object.values(mirror).some((ids) => ids.length > 0)
if (stopList.length === 0 && !hasMirror) return
if (stopList.length === 0 && removed.length === 0 && !hasMirror) return
recTicks++
const t = await readCatalogTime()
@@ -733,9 +739,10 @@ async function reconcile(ctx: any): Promise<void> {
const p = body[id] || {}
const name = p.name || ""
const models = p.models || {}
// cfg 停用列表命中 → 真删除厂商节点(不能只清空 models:OC 刷新 catalog 会把
// provider 节点连同模型一起加回来,只有摘掉节点才算真删除)
if (stopList.length && stopListHits(stopList, id, name)) {
// cfg 停用列表 或 removed.json 命中 → 真删除厂商节点(不能只清空 models:
// OC 刷新 catalog 会把 provider 节点连同模型一起加回来,只有摘掉节点才算真删除)
const inRemoved = removed.includes(id)
if ((stopList.length && stopListHits(stopList, id, name)) || inRemoved) {
plan.push({ op: "remove_provider", payload: { provider: id } })
continue
}
@@ -749,10 +756,26 @@ async function reconcile(ctx: any): Promise<void> {
}
if (plan.length === 0) return
fileLog(`回潮检测:OC 刷新 catalog 使 ${plan.length} 项停用失效,开始重放压制(cfg=${stopList.length} 项 provider 级 + 镜像 modelID 级)`)
fileLog(`回潮检测:OC 刷新 catalog 使 ${plan.length} 项压制失效,开始重放(cfg=${stopList.length} + removed=${removed.length} + 镜像 modelID 级)`)
let okCount = 0
for (const step of plan) {
const r = await execDbWrite(step.op, step.payload)
fileLog(`重放 ${step.op} ${JSON.stringify(step.payload)}: ${r}`)
// ★ 堵假成功(2026-10-20 截图实锤):重放失败绝不能写成功 flush。
// 否则 WSL 侧 spawn-fail(无 python)→ catalog 从未真改 → 每 60s force 轮
// 又发现"回潮" → 又失败 → 又 flush → TUI 每 60s reload + toast 死循环。
const rs = String(r)
const stepFailed =
rs.startsWith("unknown op") ||
rs.startsWith("spawn-fail") ||
rs.startsWith("Traceback") ||
/^rc=[1-9]/.test(rs)
if (!stepFailed) okCount++
}
if (okCount === 0) {
fileLog("✗ reconcile 重放全部失败(DB 一个都没改成),不写 flush、不触发 TUI 重载")
recLastTime = await readCatalogTime() // 防止下一轮立刻重试刷屏;等 catalog 真变化再试
return
}
recLastTime = await readCatalogTime()
await finishBatch(ctx, "reconcile")