From 7ab1b9a9b0aea29df253a7e943ad7397e369d297 Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Mon, 19 Oct 2026 12:34:02 +0800 Subject: [PATCH] =?UTF-8?q?tui.js=EF=BC=9A/manage=20=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E6=9B=BF=E6=8D=A2=E5=89=8D=E7=BC=80=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E9=80=89=E5=8E=82=E5=90=8E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B/=E6=95=B4=E5=8E=82=E7=B2=92=E5=BA=A6?= =?UTF-8?q?=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tui-plugins/oc-session-all.js | 127 +++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index 54b31a1..6cad864 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -427,13 +427,14 @@ function mmReadCatalog() { try { const py = process.platform === "win32" ? "python" : "python3" // 读 catalog + credential(credential 表存 /connect 传 key 的内置 provider,如 xiaomi) + // label = /connect 时选的 service 名(如 "Xiaomi"),用于精确区分同 integration 的不同 service // 坑:① Python -c 单行里 if row: import sys 非法 ② Windows GBK 编码 → 全走 buffer.write utf-8 const script = "import sqlite3,os,sys,json;" + "db=os.path.join(os.path.expanduser('~'),'.local','share','opencode','opencode.db');" + "con=sqlite3.connect(db);" + "row=con.execute(\"SELECT value FROM kv WHERE key='models-dev:catalog'\").fetchone();" + - "creds=[r[0] for r in con.execute(\"SELECT integration_id FROM credential WHERE active=1\").fetchall()];" + + "creds=[{'i':r[0],'l':r[1]} for r in con.execute(\"SELECT integration_id,label FROM credential WHERE active=1\").fetchall()];" + "con.close();" + "obj={'catalog':(row[0] if row else None),'credentials':creds};" + "sys.stdout.buffer.write(json.dumps(obj, ensure_ascii=False).encode('utf-8'))" @@ -476,10 +477,12 @@ async function openModelManager(context) { // —— catalog 全量 232 个不是已连接,要按 OC 当前实际启用集合过滤 let connectedProviders = [] const seen = new Set() - const addProvider = (name, models, source) => { - if (!name || seen.has(name)) return - seen.add(name) - connectedProviders.push({ name, models: models || [], source }) + // id = providerID(catalog key / config key,daemon 端 body.get(provider) 用它索引) + // name = 显示名(cv.name,如 "Xiaomi"),只用于界面展示 + const addProvider = (id, name, models, source) => { + if (!id || seen.has(id)) return + seen.add(id) + connectedProviders.push({ id, name: name || id, models: models || [], source }) } // 来源 A:config provider @@ -492,7 +495,7 @@ async function openModelManager(context) { const pv = prov[pid] || {} const name = pv.name || pid const models = Object.keys((pv.models && pv.models) || {}) - addProvider(name, models, "config") + addProvider(pid, name, models, "config") } } log("[oc-session-all] /manage 来源A(config): " + connectedProviders.map((p) => p.name).join(", ")) @@ -501,9 +504,10 @@ async function openModelManager(context) { } // 来源 B:catalog 里「已连接内置 provider」。 - // credential 表存的是"集成名"(如 xiaomi),catalog 存的是"具体 providerID"(xiaomi、 - // xiaomi-token-plan-cn、xiaomi-token-plan-sgp、xiaomi-token-plan-ams 等)。 - // 匹配规则:catalog providerID 以 credential.integration_id 开头(前缀匹配)就算已连接。 + // credential 表:integration_id=集成名(如 xiaomi),label=/connect 时选的 service 名(如 "Xiaomi")。 + // ★ 精确匹配规则(前缀匹配已废弃——它会把没订阅的 Xiaomi Token Plan * 变体也拖进来): + // ① label 精确等于 provider 显示名 → 就是 /connect 选的那个 service + // ② 匹配不到再回退 cid === integration_id(label 缺失/被改名的兜底) const cat = await mmReadCatalog() if (cat && cat.catalog && cat.credentials) { let body = {} @@ -524,23 +528,33 @@ async function openModelManager(context) { log("[oc-session-all] /manage credential 表无 active=1 记录(没 /connect 过内置 provider)") } const hits = [] - for (const cid of Object.keys(body)) { + const hitSet = new Set() + for (const c of credentials) { + const integ = (c && c.i) || "" + const label = (c && c.l) || "" + let matched = label + ? Object.keys(body).filter((cid) => (((body[cid] || {}).name) || cid) === label) + : [] + if (matched.length === 0 && integ) { + matched = Object.keys(body).filter((cid) => cid === integ) + } + log("[oc-session-all] /manage 来源B credential=" + JSON.stringify(c) + " → " + JSON.stringify(matched)) + for (const cid of matched) { + if (hitSet.has(cid)) continue + hitSet.add(cid) + hits.push(cid) + } + } + for (const cid of hits) { const cv = body[cid] || {} - const models = Object.keys((cv && cv.models) || {}) - if (models.length === 0) continue - // 前缀匹配:providerID 以某个 credential.integration_id 开头 - const isConnected = credentials.some((integ) => cid === integ || cid.startsWith(integ + "-") || cid.startsWith(integ + "_")) - if (!isConnected) continue - const name = cv.name || cid - hits.push(cid) - addProvider(name, models, "catalog") + addProvider(cid, cv.name || cid, Object.keys(cv.models || {}), "catalog") } log("[oc-session-all] /manage 来源B 命中: " + JSON.stringify(hits)) } else { log("[oc-session-all] /manage 来源B 跳过: cat=" + (cat ? "有值但catalog/credentials缺一" : "mmReadCatalog 返回 null")) } - log("[oc-session-all] /manage 最终列表(" + connectedProviders.length + "): " + JSON.stringify(connectedProviders.map((p) => ({ n: p.name, m: p.models.length, s: p.source })))) + log("[oc-session-all] /manage 最终列表(" + connectedProviders.length + "): " + JSON.stringify(connectedProviders.map((p) => ({ id: p.id, n: p.name, m: p.models.length, s: p.source })))) if (connectedProviders.length === 0) { context.ui.toast.show({ message: "没有已连接 provider(config 和 catalog 都为空),先 /connect 一个再 /manage", @@ -550,6 +564,7 @@ async function openModelManager(context) { } // 第一层:选 provider(顶置「恢复出厂」全局操作) // 注意:OC dialog.select 的 options 字段是 title/value(不是 label/value,label 会 undefined→崩溃) + // value 用 providerID(daemon 端 body.get(provider) 按 catalog key 索引,显示名 "Xiaomi"≠key "xiaomi") const providerOptions = [ { title: "🏭 恢复出厂(全部已连接 provider)", @@ -557,7 +572,7 @@ async function openModelManager(context) { }, ...connectedProviders.map((p) => ({ title: p.name + "(" + p.models.length + " 个模型 · " + p.source + ")", - value: p.name, + value: p.id, })), ] const pickProvider = await context.ui.dialog.select({ @@ -567,27 +582,77 @@ async function openModelManager(context) { }) if (!pickProvider) return if (pickProvider === "__reset_all__") { - for (const p of connectedProviders) mmEnqueue("reset", { provider: p.name }) + for (const p of connectedProviders) mmEnqueue("reset", { provider: p.id }) context.ui.toast.show({ message: "恢复出厂已入队(" + connectedProviders.length + " 个 provider),等 daemon 消费后 /models 生效", variant: "info", }) return } - // 该 provider 的当前 models - const target = connectedProviders.find((p) => p.name === pickProvider) - const models = (target && target.models) || [] + const target = connectedProviders.find((p) => p.id === pickProvider) + if (!target) { + context.ui.toast.show({ message: "未找到该 provider", variant: "warning" }) + return + } + const dispName = target.name + const models = target.models || [] + + // 第二层:管理粒度 —— 单个模型 vs 整个厂商 + const scope = await context.ui.dialog.select({ + title: "模型管理 · " + dispName + " · 选择粒度", + placeholder: "管单个模型,还是管整个厂商?", + options: [ + { title: "🧩 单个模型(停用/启用其中一个)", value: "model" }, + { title: "🏭 整个厂商(全部停用 / 全部启用)", value: "provider" }, + ], + }) + if (!scope) return + + if (scope === "provider") { + // 厂商粒度:停用=逐个下架现存模型;启用=恢复该厂商全部模型(reset 拉回 pristine) + const pAct = await context.ui.dialog.select({ + title: "对整个 " + dispName + " 执行", + placeholder: "停用整个厂商,还是恢复全部模型?", + options: [ + { title: "⛔ 停用整个厂商(全部模型下架)", value: "disable_all" }, + { title: "✅ 启用整个厂商(恢复全部模型)", value: "enable_all" }, + ], + }) + if (!pAct) return + if (pAct === "disable_all") { + if (models.length === 0) { + context.ui.toast.show({ message: dispName + " 的模型已全部停用", variant: "warning" }) + return + } + for (const m of models) mmEnqueue("disable", { provider: target.id, modelID: m }) + context.ui.toast.show({ + message: "已入队:停用整个 " + dispName + "(" + models.length + " 个模型),daemon 5s 内消费", + variant: "info", + }) + } else { + mmEnqueue("reset", { provider: target.id }) + context.ui.toast.show({ + message: "已入队:启用整个 " + dispName + "(恢复全部模型),daemon 5s 内消费", + variant: "info", + }) + } + return + } + + // scope === "model":单模型粒度 if (models.length === 0) { - context.ui.toast.show({ message: pickProvider + " 下没有模型", variant: "warning" }) + context.ui.toast.show({ + message: dispName + " 下没有可管理的模型(已全部停用?可用「整个厂商 → 启用」恢复)", + variant: "warning", + }) return } - // 第二层:逐模型选 停用/启用 const modelOptions = models.map((m) => ({ title: m, value: m, })) const pickModel = await context.ui.dialog.select({ - title: "模型管理 · " + pickProvider, + title: "模型管理 · " + dispName + " · 选模型", placeholder: "选一个模型 停用/启用…", options: modelOptions, }) @@ -595,18 +660,16 @@ async function openModelManager(context) { const actOptions = [ { title: "⛔ 停用(不在 /models 显示)", value: "disable" }, { title: "✅ 启用(恢复显示)", value: "enable" }, - { title: "🏭 恢复该 provider 出厂", value: "reset" }, ] const action = await context.ui.dialog.select({ title: "对 " + pickModel + " 执行", options: actOptions, }) if (!action) return - if (action === "disable") mmEnqueue("disable", { provider: pickProvider, modelID: pickModel }) - else if (action === "enable") mmEnqueue("enable", { provider: pickProvider, modelID: pickModel }) - else mmEnqueue("reset", { provider: pickProvider }) + if (action === "disable") mmEnqueue("disable", { provider: target.id, modelID: pickModel }) + else mmEnqueue("enable", { provider: target.id, modelID: pickModel }) context.ui.toast.show({ - message: "已入队:" + action + " " + pickProvider + "/" + pickModel + "(daemon 5s 内消费,/models 将更新)", + message: "已入队:" + action + " " + dispName + "/" + pickModel + "(daemon 5s 内消费,/models 将更新)", variant: "info", }) } catch (e) {