# 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`: ```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 同步)