tui.js:模型管理面板改状态机,每层带「← 返回上级」;恢复出厂设置=删除厂商并二次确认
This commit is contained in:
+356
-203
@@ -411,6 +411,49 @@ function toastResult(context, res, actionLabel) {
|
||||
// 而 TUI 插件 ctx 的 client.location 一直可用(本日志大量 location.reload OK)。
|
||||
const MM_OUTBOX = join(homedir(), ".config", "opencode", "model-manager.outbox.jsonl")
|
||||
const MM_FLUSH = join(homedir(), ".config", "opencode", "model-manager.last-flush.json")
|
||||
const MM_CFG = join(homedir(), ".config", "opencode", "model-manager.cfg.json")
|
||||
const MM_REMOVED = join(homedir(), ".config", "opencode", "model-manager.removed.json")
|
||||
const MM_PRISTINE = join(homedir(), ".config", "opencode", "model-manager.pristine.json")
|
||||
|
||||
// 读 cfg.json 的「以下是停用列表」(键名大小写不敏感,其它键全部忽略)
|
||||
function mmReadStopList() {
|
||||
try {
|
||||
if (!existsSync(MM_CFG)) return []
|
||||
const j = JSON.parse(readFileSync(MM_CFG, "utf-8"))
|
||||
const target = "以下是停用列表".toLowerCase()
|
||||
for (const k of Object.keys(j || {})) {
|
||||
if (String(k).toLowerCase() === target) {
|
||||
const v = j[k]
|
||||
if (Array.isArray(v)) return v.filter((x) => typeof x === "string" && x.trim())
|
||||
}
|
||||
}
|
||||
return []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
// 读「已彻底删除」的厂商列表(remove_provider 标记,供面板显示与找回)
|
||||
function mmReadRemovedList() {
|
||||
try {
|
||||
if (!existsSync(MM_REMOVED)) return []
|
||||
const j = JSON.parse(readFileSync(MM_REMOVED, "utf-8"))
|
||||
return Array.isArray(j) ? j.filter((x) => typeof x === "string") : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
// 读 pristine(出厂 catalog)→ 用于显示「被停用/删除的厂商」还能找回多少模型
|
||||
function mmReadPristineBody() {
|
||||
try {
|
||||
if (!existsSync(MM_PRISTINE)) return {}
|
||||
const j = JSON.parse(readFileSync(MM_PRISTINE, "utf-8"))
|
||||
const b = j && j.body
|
||||
if (typeof b === "string") return JSON.parse(b)
|
||||
return b && typeof b === "object" ? b : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function mmEnqueue(op, payload) {
|
||||
try {
|
||||
@@ -521,220 +564,330 @@ function mmStartFlushPoller(context) {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// ── /manage 模型管理面板(状态机:每层都有「← 返回上级」,不靠 ESC)────────
|
||||
// 说明:OC 原生 dialog 的 ESC 直接关整个面板,无法逐层回退,
|
||||
// 所以每层菜单第一项固定是「← 返回上级」,由我们自己维护层级栈。
|
||||
const MM_BACK = "__mm_back__"
|
||||
const MM_CLOSE = "__mm_close__"
|
||||
|
||||
// 收集「已连接 provider」(config provider + catalog∧credential 精确匹配)
|
||||
async function mmCollectProviders() {
|
||||
const connectedProviders = []
|
||||
const seen = new Set()
|
||||
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:opencode.json 的 provider 字段(cc-switch 自定义加的,如 id=1)
|
||||
try {
|
||||
const cfgPath = join(homedir(), ".config", "opencode", "opencode.json")
|
||||
if (existsSync(cfgPath)) {
|
||||
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"))
|
||||
const prov = (cfg && cfg.provider) || {}
|
||||
for (const pid of Object.keys(prov)) {
|
||||
const pv = prov[pid] || {}
|
||||
addProvider(pid, pv.name || pid, Object.keys((pv.models && pv.models) || {}), "config")
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log("[oc-session-all] /manage 读 config provider 失败: " + e.message)
|
||||
}
|
||||
|
||||
// 来源 B:catalog 里「已连接内置 provider」。
|
||||
// ★ 精确匹配(前缀匹配已废弃——会把没订阅的 Xiaomi Token Plan * 变体也拖进来):
|
||||
// ① credential.label 精确等于 provider 显示名 ② 回退 cid === integration_id
|
||||
const cat = await mmReadCatalog()
|
||||
if (cat && cat.catalog && cat.credentials) {
|
||||
let body = {}
|
||||
try {
|
||||
// 坑:models-dev:catalog 的 value 是外层包装 {updatedAt, digest, body},
|
||||
// body 才是真正的 provider JSON 字符串 —— 必须二次解包
|
||||
const outer = JSON.parse(cat.catalog)
|
||||
if (outer && typeof outer.body === "string") body = JSON.parse(outer.body)
|
||||
else if (outer && outer.body && typeof outer.body === "object") body =outer.body
|
||||
else body = outer
|
||||
} catch (e) {
|
||||
log("[oc-session-all] /manage catalog body parse 失败: " + e.message)
|
||||
}
|
||||
const credentials = Array.isArray(cat.credentials) ? cat.credentials : []
|
||||
const hits = []
|
||||
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)
|
||||
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] || {}
|
||||
addProvider(cid, cv.name || cid, Object.keys(cv.models || {}), "catalog")
|
||||
}
|
||||
log("[oc-session-all] /manage catalog 命中: " + JSON.stringify(hits))
|
||||
}
|
||||
return connectedProviders
|
||||
}
|
||||
|
||||
// 已停用/已删除的厂商(可找回):cfg 停用列表 + 已删除列表,去掉仍在线的
|
||||
function mmFindDisabled(connected) {
|
||||
const online = new Set(connected.map((p) => p.id))
|
||||
const stop = mmReadStopList()
|
||||
const removed = mmReadRemovedList()
|
||||
const pristine = mmReadPristineBody()
|
||||
const ids = []
|
||||
for (const id of stop.concat(removed)) {
|
||||
if (!id || online.has(id) || ids.includes(id)) continue
|
||||
ids.push(id)
|
||||
}
|
||||
return ids.map((id) => {
|
||||
const p = pristine[id] || {}
|
||||
return {
|
||||
id,
|
||||
name: p.name || id,
|
||||
models: Object.keys(p.models || {}),
|
||||
deleted: removed.includes(id),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function openModelManager(context) {
|
||||
try {
|
||||
// ── 收集「已连接 provider」,两个来源都读,合并去重 ──────────────────
|
||||
// 来源 A:opencode.json 的 provider 字段(cc-switch 自定义加的,如 id=1)
|
||||
// 来源 B:opencode.db catalog 里「当前 OC 真正启用的内置 provider」(如 Xiaomi)
|
||||
// —— catalog 全量 232 个不是已连接,要按 OC 当前实际启用集合过滤
|
||||
let connectedProviders = []
|
||||
const seen = new Set()
|
||||
// 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 })
|
||||
}
|
||||
// 层级状态机:view = list | scope | provider | model | action | find
|
||||
let view = "list"
|
||||
let target = null // 当前厂商 {id,name,models}
|
||||
let pendingModel = null
|
||||
|
||||
// 来源 A:config provider
|
||||
try {
|
||||
const cfgPath = join(homedir(), ".config", "opencode", "opencode.json")
|
||||
if (existsSync(cfgPath)) {
|
||||
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"))
|
||||
const prov = (cfg && cfg.provider) || {}
|
||||
for (const pid of Object.keys(prov)) {
|
||||
const pv = prov[pid] || {}
|
||||
const name = pv.name || pid
|
||||
const models = Object.keys((pv.models && pv.models) || {})
|
||||
addProvider(pid, name, models, "config")
|
||||
for (;;) {
|
||||
// ── 第 1 层:选厂商 ────────────────────────────────────────────────
|
||||
if (view === "list") {
|
||||
const connectedProviders = await mmCollectProviders()
|
||||
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 }))),
|
||||
)
|
||||
const disabled = mmFindDisabled(connectedProviders)
|
||||
const options = [
|
||||
{ title: "✕ 关闭面板", value: MM_CLOSE },
|
||||
...connectedProviders.map((p) => ({
|
||||
title: p.name + "(" + p.models.length + " 个模型 · " + p.source + ")",
|
||||
value: p.id,
|
||||
})),
|
||||
]
|
||||
if (disabled.length) {
|
||||
options.push({ title: "♻️ 已停用/已删除的厂商(" + disabled.length + ")", value: "__find__" })
|
||||
}
|
||||
}
|
||||
log("[oc-session-all] /manage 来源A(config): " + connectedProviders.map((p) => p.name).join(", "))
|
||||
} catch (e) {
|
||||
log("[oc-session-all] /manage 读 config provider 失败: " + e.message)
|
||||
}
|
||||
|
||||
// 来源 B:catalog 里「已连接内置 provider」。
|
||||
// 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 = {}
|
||||
try {
|
||||
// 坑:models-dev:catalog 的 value 是外层包装 {updatedAt, digest, body},
|
||||
// body 才是真正的 provider JSON 字符串(provider→models)——必须二次解包,
|
||||
// 否则 Object.keys 只有 3 个(updatedAt/digest/body),循环命不中任何 provider。
|
||||
const outer = JSON.parse(cat.catalog)
|
||||
if (outer && typeof outer.body === "string") body = JSON.parse(outer.body)
|
||||
else if (outer && outer.body && typeof outer.body === "object") body = outer.body
|
||||
else body = outer
|
||||
} catch (e) {
|
||||
log("[oc-session-all] /manage catalog body parse 失败: " + e.message)
|
||||
}
|
||||
const credentials = Array.isArray(cat.credentials) ? cat.credentials : []
|
||||
log("[oc-session-all] /manage 来源B: credentials=" + JSON.stringify(credentials) + " catalogProviders=" + Object.keys(body).length)
|
||||
if (credentials.length === 0) {
|
||||
log("[oc-session-all] /manage credential 表无 active=1 记录(没 /connect 过内置 provider)")
|
||||
}
|
||||
const hits = []
|
||||
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] || {}
|
||||
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) => ({ 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",
|
||||
variant: "warning",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 第一层:选 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)",
|
||||
value: "__reset_all__",
|
||||
},
|
||||
...connectedProviders.map((p) => ({
|
||||
title: p.name + "(" + p.models.length + " 个模型 · " + p.source + ")",
|
||||
value: p.id,
|
||||
})),
|
||||
]
|
||||
const pickProvider = await context.ui.dialog.select({
|
||||
title: "模型管理 · 选已连接 provider",
|
||||
placeholder: "选一个已连接 provider 管理其模型…",
|
||||
options: providerOptions,
|
||||
})
|
||||
if (!pickProvider) return
|
||||
if (pickProvider === "__reset_all__") {
|
||||
for (const p of connectedProviders) mmEnqueue("reset", { provider: p.id })
|
||||
context.ui.toast.show({
|
||||
message: "恢复出厂已入队(" + connectedProviders.length + " 个 provider),daemon 写完自动热重载,/models 随即生效",
|
||||
variant: "info",
|
||||
})
|
||||
return
|
||||
}
|
||||
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 整个厂商 vs 恢复出厂(格式化)
|
||||
const scope = await context.ui.dialog.select({
|
||||
title: "模型管理 · " + dispName + " · 选择粒度",
|
||||
placeholder: "管单个模型、整个厂商,还是格式化恢复出厂?",
|
||||
options: [
|
||||
{ title: "🧩 单个模型(停用/启用其中一个)", value: "model" },
|
||||
{ title: "🏭 整个厂商(全部停用 / 全部启用)", value: "provider" },
|
||||
{ title: "♻️ 恢复出厂(格式化该厂商 → 出厂模型集)", value: "factory" },
|
||||
],
|
||||
})
|
||||
if (!scope) return
|
||||
|
||||
if (scope === "factory") {
|
||||
// 格式化 = 整厂重置回出厂 pristine(含把已停用的全部拉回),并从 cfg 停用列表摘除
|
||||
mmEnqueue("reset", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:格式化 " + dispName + "(恢复出厂模型集),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (scope === "provider") {
|
||||
// 厂商粒度:停用=整厂一条 disable_all 下架(持久化进 cfg 停用列表);启用=reset 拉回 pristine
|
||||
const pAct = await context.ui.dialog.select({
|
||||
title: "对整个 " + dispName + " 执行",
|
||||
placeholder: "停用整个厂商,还是恢复全部模型?",
|
||||
options: [
|
||||
{ title: "⛔ 停用整个厂商(全部模型下架 · 持久化进 cfg 停用列表)", 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" })
|
||||
if (connectedProviders.length === 0 && disabled.length === 0) {
|
||||
context.ui.toast.show({
|
||||
message: "没有已连接 provider(config 和 catalog 都为空),先 /connect 一个再 /manage",
|
||||
variant: "warning",
|
||||
})
|
||||
return
|
||||
}
|
||||
mmEnqueue("disable_all", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:停用整个 " + dispName + "(" + models.length + " 个模型 · 一条搞定 · 持久化),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
} else {
|
||||
mmEnqueue("reset", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:启用整个 " + dispName + "(恢复全部模型),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "模型管理 · 选厂商",
|
||||
placeholder: "选一个厂商管理其模型…",
|
||||
options,
|
||||
})
|
||||
if (!pick || pick === MM_CLOSE) return
|
||||
if (pick === "__find__") {
|
||||
view = "find"
|
||||
continue
|
||||
}
|
||||
const t = connectedProviders.find((p) => p.id === pick)
|
||||
if (!t) {
|
||||
context.ui.toast.show({ message: "未找到该 provider", variant: "warning" })
|
||||
continue
|
||||
}
|
||||
target = t
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
|
||||
// ── 第 2 层:选粒度(进厂后的第一层,带返回上级)────────────────────
|
||||
if (view === "scope") {
|
||||
const dispName = target.name
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "模型管理 · " + dispName + " · 选择操作",
|
||||
placeholder: "选操作(← 返回上一级)",
|
||||
options: [
|
||||
{ title: "← 返回上级(选厂商)", value: MM_BACK },
|
||||
{ title: "🧩 单个模型(停用/启用其中一个)", value: "model" },
|
||||
{ title: "🏭 整个厂商(全部停用 / 全部启用)", value: "provider" },
|
||||
{ title: "♻️ 恢复出厂设置(彻底删除该厂商)", value: "factory" },
|
||||
],
|
||||
})
|
||||
if (!pick || pick === MM_BACK) {
|
||||
view = "list"
|
||||
target = null
|
||||
continue
|
||||
}
|
||||
if (pick === "model") {
|
||||
view = "model"
|
||||
continue
|
||||
}
|
||||
if (pick === "provider") {
|
||||
view = "provider"
|
||||
continue
|
||||
}
|
||||
if (pick === "factory") {
|
||||
// 恢复出厂设置 = 彻底删除该厂商(/models 里整厂消失),二次确认
|
||||
const ok = await context.ui.dialog.select({
|
||||
title: "确认恢复出厂设置(删除 " + dispName + ")",
|
||||
placeholder: "该厂商将从 /models 彻底移除(可再找回)",
|
||||
options: [
|
||||
{ title: "← 返回上级(不删)", value: MM_BACK },
|
||||
{ title: "🗑 确定删除 " + dispName, value: "yes" },
|
||||
],
|
||||
})
|
||||
if (!ok || ok === MM_BACK) {
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
mmEnqueue("remove_provider", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:删除 " + dispName + "(恢复出厂设置),daemon 写完自动热重载,/models 里该厂消失",
|
||||
variant: "info",
|
||||
})
|
||||
view = "list"
|
||||
target = null
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// ── 第 3 层:整厂操作 ─────────────────────────────────────────────
|
||||
if (view === "provider") {
|
||||
const dispName = target.name
|
||||
const n = (target.models || []).length
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "对整个 " + dispName + " 执行",
|
||||
placeholder: "选操作(← 返回上一级)",
|
||||
options: [
|
||||
{ title: "← 返回上级", value: MM_BACK },
|
||||
{ title: "⛔ 停用整个厂商(全部模型下架 · 持久化)", value: "disable_all" },
|
||||
{ title: "✅ 启用整个厂商(恢复全部模型)", value: "enable_all" },
|
||||
],
|
||||
})
|
||||
if (!pick || pick === MM_BACK) {
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
if (pick === "disable_all") {
|
||||
if (n === 0) {
|
||||
context.ui.toast.show({ message: dispName + " 的模型已全部停用", variant: "warning" })
|
||||
} else {
|
||||
mmEnqueue("disable_all", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:停用整个 " + dispName + "(" + n + " 个模型 · 持久化),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
mmEnqueue("reset", { provider: target.id })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:启用整个 " + dispName + "(恢复全部模型),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
}
|
||||
view = "list"
|
||||
target = null
|
||||
continue
|
||||
}
|
||||
|
||||
// ── 第 4 层:选模型 ───────────────────────────────────────────────
|
||||
if (view === "model") {
|
||||
const dispName = target.name
|
||||
const models = target.models || []
|
||||
if (models.length === 0) {
|
||||
context.ui.toast.show({
|
||||
message: dispName + " 下没有可管理的模型(已全部停用?用「整个厂商 → 启用」恢复)",
|
||||
variant: "warning",
|
||||
})
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "模型管理 · " + dispName + " · 选模型",
|
||||
placeholder: "选一个模型(← 返回上一级)",
|
||||
options: [
|
||||
{ title: "← 返回上级", value: MM_BACK },
|
||||
...models.map((m) => ({ title: m, value: m })),
|
||||
],
|
||||
})
|
||||
if (!pick || pick === MM_BACK) {
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
pendingModel = pick
|
||||
view = "action"
|
||||
continue
|
||||
}
|
||||
|
||||
// ── 第 5 层:对某模型执行 ─────────────────────────────────────────
|
||||
if (view === "action") {
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "对 " + pendingModel + " 执行",
|
||||
placeholder: "选操作(← 返回上一级)",
|
||||
options: [
|
||||
{ title: "← 返回上级", value: MM_BACK },
|
||||
{ title: "⛔ 停用(不在 /models 显示)", value: "disable" },
|
||||
{ title: "✅ 启用(恢复显示)", value: "enable" },
|
||||
],
|
||||
})
|
||||
if (!pick || pick === MM_BACK) {
|
||||
view = "model"
|
||||
continue
|
||||
}
|
||||
if (pick === "disable") mmEnqueue("disable", { provider: target.id, modelID: pendingModel })
|
||||
else mmEnqueue("enable", { provider: target.id, modelID: pendingModel })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:" + pick + " " + target.name + "/" + pendingModel + ",daemon 写完自动热重载,/models 随即更新",
|
||||
variant: "info",
|
||||
})
|
||||
view = "scope"
|
||||
continue
|
||||
}
|
||||
|
||||
// ── 找回:已停用/已删除的厂商 ─────────────────────────────────────
|
||||
if (view === "find") {
|
||||
const connected = await mmCollectProviders()
|
||||
const disabled = mmFindDisabled(connected)
|
||||
if (!disabled.length) {
|
||||
context.ui.toast.show({ message: "没有已停用/已删除的厂商", variant: "info" })
|
||||
view = "list"
|
||||
continue
|
||||
}
|
||||
const pick = await context.ui.dialog.select({
|
||||
title: "已停用/已删除的厂商",
|
||||
placeholder: "选一个恢复(← 返回上一级)",
|
||||
options: [
|
||||
{ title: "← 返回上级", value: MM_BACK },
|
||||
...disabled.map((d) => ({
|
||||
title: d.name + "(" + d.models.length + " 个出厂模型 · " + (d.deleted ? "已删除" : "已停用") + ")",
|
||||
value: d.id,
|
||||
})),
|
||||
],
|
||||
})
|
||||
if (!pick || pick === MM_BACK) {
|
||||
view = "list"
|
||||
continue
|
||||
}
|
||||
mmEnqueue("reset", { provider: pick })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:恢复 " + pick + "(从出厂集拉回),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
view = "list"
|
||||
continue
|
||||
}
|
||||
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: "模型管理 · " + dispName + " · 选模型",
|
||||
placeholder: "选一个模型 停用/启用…",
|
||||
options: modelOptions,
|
||||
})
|
||||
if (!pickModel) return
|
||||
const actOptions = [
|
||||
{ title: "⛔ 停用(不在 /models 显示)", value: "disable" },
|
||||
{ title: "✅ 启用(恢复显示)", value: "enable" },
|
||||
]
|
||||
const action = await context.ui.dialog.select({
|
||||
title: "对 " + pickModel + " 执行",
|
||||
options: actOptions,
|
||||
})
|
||||
if (!action) return
|
||||
if (action === "disable") mmEnqueue("disable", { provider: target.id, modelID: pickModel })
|
||||
else mmEnqueue("enable", { provider: target.id, modelID: pickModel })
|
||||
context.ui.toast.show({
|
||||
message: "已入队:" + action + " " + dispName + "/" + pickModel + ",daemon 写完自动热重载,/models 随即更新",
|
||||
variant: "info",
|
||||
})
|
||||
} catch (e) {
|
||||
log("[oc-session-all] openModelManager ERROR: " + (e.stack || e.message))
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user