youpaishidifu b56f4df500 修复 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 死循环
2026-10-20 23:13:03 +08:00

model-manager

模型目录管理插件(OpenCode TUI):停用/恢复/恢复出厂。

背景

OpenCode 的 /models 命令读取模型目录,新版目录存在 SQLite 数据库里:

  • DB 文件:~/.local/share/opencode/opencode.db(WAL 模式)
  • 表:kv
  • key:models-dev:catalog
  • value:JSON 字符串 {updatedAt, digest, body},其中 body 本身又是嵌套 JSON 字符串:
    { providerID: { id, npm, api, models: { modelID: entry, ... }, ... }, ... }
    

/models 渲染的就是这份 catalog。要"停用某模型"(不在 /models 显示)或"恢复出厂"(还原所有被停用的模型),就得改这份 DB。

难点:OC 进程持有 SQLite 写锁

OC 的 main 进程常驻打开这个 DB(WAL 模式)。插件跑在 OC 内部(Bun 运行时),如果在插件进程里直接 better-sqlite3 写同一个文件,会撞上 OC 主进程的写锁 → SQLITE_BUSY / 死锁。

解法:插件不直接写 DB,而是走 outbox 队列 + fork 外部 Python 子进程 执行 DB 写:

  1. 插件收到操作请求(CLI / 外部脚本触发)→ 写一行 JSON 到 ~/.config/opencode/model-manager.outbox.jsonl(outbox 队列)
  2. 插件后台 daemon(setup 里 spawn,每 5s 轮询)发现新行 → fork python 子进程执行 model-manager-db-exec.py(OC setup 时自动落盘到 ~/.config/opencode/)
  3. Python 子进程用 sqlite3 标准库(Python 对 WAL 模式无独占锁问题)读写 DB
  4. 写完后调 POST http://127.0.0.1:4096/api/location/reload 让 OC 热重载目录
  5. 结果回写到 outbox 的响应字段,daemon 记录日志

三种操作

操作 含义 实现
disable 停用某模型(不在 /models 显示) 在 body 的对应 provider.models 里把该 modelID 条目标记 disabled:true(或移出 models 字典,视 catalog schema)
enable 恢复某被停用的模型 清除 disabled 标记 / 恢复出厂 entry
reset 恢复出厂(全部模型还原) 用首次启用时自动拍的 ~/.config/opencode/model-manager.pristine.json 备份覆盖 DB catalog

触发方式

方式一:TUI 命令 /manage(推荐)

OC TUI 里输入 /manage(别名 /managent)打开模型管理面板,操作不直接改 DB,而是写 outbox 队列,由本插件 daemon 消费:

  • 第一层:选 provider(顶置「恢复出厂」全局项)
  • 第二层:对该 provider 选 停用 / 启用 / 恢复出厂
  • 选中模型 → 选 停用 / 启用

命令注册在 tui-plugins/oc-session-all.js(model.manage 命令),写的是同一个 outbox 文件,daemon 统一消费。

方式二:外部脚本写 outbox

直接写一行 JSON 到 ~/.config/opencode/model-manager.outbox.jsonl:

{"op":"disable","payload":{"provider":"anthropic","modelID":"claude-opus-4.6"},"ts":1700000000}
{"op":"enable","payload":{"provider":"anthropic","modelID":"claude-opus-4.6"},"ts":1700000000}
{"op":"reset","payload":{"provider":"anthropic"},"ts":1700000000}

(daemon 5s 轮询,操作有最多 5s 延迟。)

日志

~/.config/opencode/model-manager.log

关键行:

  • OUTBOX 收到 op=disable provider=anthropic model=claude-opus-4.6
  • DB 写成功,digest 更新
  • reload 200 热重载完成
  • reset 已还原 pristine 备份
  • pristine 备份已建立(首次启用时)

部署

端 路径
Windows 本机 ~/.config/opencode/plugins/model-manager.ts
WSL /root/.config/opencode/plugins/model-manager.ts

依赖:Python 3(用 sqlite3 标准库,无需 pip install)。

由 tools/sync_wsl.py 的 ITEMS 清单管理。

已知取舍

  1. 不直接写 DB:OC 主进程持写锁,插件进程内直接 better-sqlite3 会 SQLITE_BUSY。outbox + fork Python 是绕开锁的可靠路径。
  2. reset 依赖 pristine 备份:首次启用时自动拍 model-manager.pristine.json;若该文件被手动删了,reset 会失败(日志报 pristine not found),需重新拍一份。
  3. 5s 轮询:daemon 每 5s 扫一次 outbox,操作有最多 5s 延迟。实为 fire-and-forget,不影响 OC 主流程。
  4. 目录 reload 用 4096 端口:假设 OC serve 跑在默认端口;若用了 --port 自定义端口,reload 调用需带对端口(插件默认 4096,改 OPENCODE_PORT 环境变量可覆盖)。

文件

  • model-manager.ts — 插件主文件(TypeScript)
  • model-manager-db-exec.py — DB 写执行器(Python,OC setup 时自动落盘到 ~/.config/opencode/,本仓库也保留一份供 sync_wsl 同步)
S
Description
No description provided
Readme
16 MiB
Languages
JavaScript 57.9%
Python 35.9%
TypeScript 6.2%