tui.js:/manage 精确匹配替换前缀匹配,选厂后加单模型/整厂粒度层
This commit is contained in:
@@ -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 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
|
||||
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)
|
||||
addProvider(name, models, "catalog")
|
||||
}
|
||||
}
|
||||
for (const cid of hits) {
|
||||
const cv = body[cid] || {}
|
||||
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: pickProvider + " 下没有模型", variant: "warning" })
|
||||
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: 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) {
|
||||
|
||||
Reference in New Issue
Block a user