From fb7015ff6cae84a657203b116e9bd7852310559a Mon Sep 17 00:00:00 2001 From: youpaishidifu <1@ypshidifu.cn> Date: Mon, 19 Oct 2026 22:29:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=87=BA=E5=8E=82=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=3D=E7=9C=9F=E5=88=A0=E9=99=A4=E5=8E=82=E5=95=86(remov?= =?UTF-8?q?e=5Fprovider)=EF=BC=8C=E5=81=9C=E7=94=A8=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=91=BD=E4=B8=AD=E6=94=B9=E7=9C=9F=E5=88=A0=E9=99=A4=E5=8E=8B?= =?UTF-8?q?=E5=88=B6=EF=BC=8Cpristine=20=E6=8F=90=E5=89=8D=E5=88=B0=20setu?= =?UTF-8?q?p=20=E6=8B=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model-manager.ts | 85 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/model-manager.ts b/model-manager.ts index 2d35992..4e64b1a 100644 --- a/model-manager.ts +++ b/model-manager.ts @@ -78,6 +78,8 @@ const CFG_TEMPLATE = `{ }` // modelID 级镜像清单(单模型停用的持久化;整厂级走 cfg 的停用列表) const DISABLED_MIRROR = join(homedir(), ".config", "opencode", "model-manager.disabled.json") +// 已彻底删除的厂商列表(remove_provider 标记;面板据此显示「已删除·可找回」) +const REMOVED_LIST = join(homedir(), ".config", "opencode", "model-manager.removed.json") // 数据库路径 const DB_PATH = join(homedir(), ".local", "share", "opencode", "opencode.db") // SQLite 写执行器(Python,OC 外部子进程跑,绕 OC 自身 DB 写锁) @@ -243,6 +245,24 @@ function mirrorUnmark(provider: string, modelID: string): void { fileLog(`停用镜像已移除 ${provider}/${modelID}`) } +// 已删除厂商列表(面板显示「已删除·可找回」用) +function loadRemoved(): string[] { + try { + if (!existsSync(REMOVED_LIST)) return [] + const j = JSON.parse(readFileSync(REMOVED_LIST, "utf-8")) + return Array.isArray(j) ? j.filter((x: any) => typeof x === "string") : [] + } catch { + return [] + } +} +function saveRemoved(list: string[]): void { + try { + writeFileSync(REMOVED_LIST, JSON.stringify(list, null, 2), "utf-8") + } catch (e: any) { + fileLog("已删除厂商列表写入失败: " + (e && e.message ? e.message : String(e))) + } +} + // 读 catalog(只读 SQLite,OC 自身持锁但只读不冲突;用 Python 子进程读保证一致) async function readCatalog(): Promise { try { @@ -376,8 +396,8 @@ async function processOutbox(ctx: any): Promise { } fileLog(`outbox 命中 op=${op} payload=${JSON.stringify(payload).slice(0, 120)}`) - // reset 前确保有 pristine 备份 - if (op === "reset") { + // reset 前确保有 pristine 备份(恢复厂商要靠它把出厂模型拉回来) + if (op === "reset" || op === "remove_provider") { if (!existsSync(PRISTINE)) { const cat = await readCatalog() if (cat) writeFileSync(PRISTINE, JSON.stringify(cat)) @@ -408,8 +428,22 @@ async function processOutbox(ctx: any): Promise { mirrorUnmark(prov, payload.modelID) } else if (op === "enable" && s.startsWith("model not in pristine")) { mirrorUnmark(prov, payload.modelID) + } else if (op === "remove_provider" && (s.startsWith("removed_provider ") || s.startsWith("provider not found"))) { + // 恢复出厂设置(格式化)= 彻底删除该厂 → 记进 cfg 停用列表,OC 刷新 catalog 回潮时自动再压 + const list = readStopList() + if (!list.some((p) => patMatch(p, prov))) { + list.push(prov) + writeStopList(list) + fileLog(`已删除厂商 ${prov},并记入 cfg 停用列表(cfg 共 ${list.length} 项,持久化)`) + } + const rm = loadRemoved() + if (!rm.includes(prov)) { + rm.push(prov) + saveRemoved(rm) + fileLog(`已删除厂商列表已记录 ${prov}(面板可显示「已删除·可找回」)`) + } } else if (op === "reset" && (s.startsWith("reset ") || s.startsWith("provider not in pristine"))) { - // 恢复出厂/格式化:整厂从停用列表摘除(含命中它的通配项),单模型镜像清空 + // 恢复该厂商(撤销删除/停用):从停用列表摘除,单模型镜像清空 const list = readStopList() const kept = list.filter((p) => !patMatch(p, prov)) const removed = list.length - kept.length @@ -423,6 +457,11 @@ async function processOutbox(ctx: any): Promise { saveMirror(map) fileLog(`恢复出厂 ${prov}:单模型停用镜像已清空`) } + const rm = loadRemoved() + if (rm.includes(prov)) { + saveRemoved(rm.filter((x) => x !== prov)) + fileLog(`恢复出厂 ${prov}:已从「已删除厂商」列表移除(可再被 /manage 看到)`) + } } } catch (e: any) { fileLog("停用状态维护异常(已吞掉): " + (e && e.message ? e.message : String(e))) @@ -553,6 +592,19 @@ elif op == "reset": else: out("provider not in pristine: " + provider) +elif op == "remove_provider": + # 真删除:把整个 provider 节点从 catalog body 里摘掉(/models 里该厂彻底消失) + # 恢复靠 reset(pristine 里有完整节点) + cat = load() + body = json.loads(cat["body"]) + if provider not in body: + out("provider not found: " + provider); sys.exit(0) + n = len(body[provider].get("models", {})) + body.pop(provider, None) + cat["body"] = json.dumps(body, ensure_ascii=False, separators=(",", ":")) + save(cat) + out("removed_provider " + provider + " models=" + str(n)) + else: out("unknown op: " + op) ` @@ -569,6 +621,24 @@ function ensureDbExec(): void { } } +// 出厂备份:setup 时就拍一次(幂等)。 +// 为什么提前拍:若等到第一次 reset/remove_provider 才拍,那时的 catalog 可能已被 +// 改过(模型被删过),拍到的就不是「出厂态」,恢复出来的会缺模型。 +async function ensurePristine(): Promise { + try { + if (existsSync(PRISTINE)) return + const cat = await readCatalog() + if (!cat || !cat.body) { + fileLog("pristine 暂未建立(catalog 读不到,稍后重试)") + return + } + writeFileSync(PRISTINE, JSON.stringify(cat)) + fileLog("pristine 出厂备份已建立(setup 阶段)") + } catch (e: any) { + fileLog("pristine 建立失败(已吞掉): " + (e && e.message ? e.message : String(e))) + } +} + // 读 catalog 的 time_updated(单值,便宜;用于判断 OC 是否刷新覆盖过 catalog) async function readCatalogTime(): Promise { return new Promise((resolve) => { @@ -626,9 +696,13 @@ async function reconcile(ctx: any): Promise { const p = body[id] || {} const name = p.name || "" const models = p.models || {} - if (stopList.length && stopListHits(stopList, id, name) && Object.keys(models).length) { - plan.push({ op: "disable_all", payload: { provider: id } }) + // cfg 停用列表命中 → 真删除厂商节点(不能只清空 models:OC 刷新 catalog 会把 + // provider 节点连同模型一起加回来,只有摘掉节点才算真删除) + if (stopList.length && stopListHits(stopList, id, name)) { + plan.push({ op: "remove_provider", payload: { provider: id } }) + continue } + // modelID 级镜像命中 → 只摘掉那一个模型 const ids = mirror[id] if (ids && ids.length) { for (const mid of ids) { @@ -671,6 +745,7 @@ export default { } ensureDbExec() ensureCfg() // 防御:OC 更新删掉 cfg 时立即释放新配置(含停用列表镜像恢复) + await ensurePristine() // 出厂备份提前拍,避免首次操作时 catalog 已被改 // 后台 daemon:每 5s 轮询 outbox(普通文件读写,不碰 DB 锁)+ 对账压制回潮 let stopped = false