修「重载了但模型还在」:热重载/启动时从最近使用与收藏历史(model.json)剔除停用条目,toast同步显示清理条数

This commit is contained in:
ypshidifu
2026-10-20 10:32:29 +08:00
parent b911a43940
commit 884d53b619
+79 -3
View File
@@ -455,6 +455,74 @@ function mmReadPristineBody() {
}
}
// ── 「最近使用/收藏」历史清理 ───────────────────────────────────────────
// /models 顶部的 Recent/Favorites 不是 catalog,而是 OC TUI 的使用历史:
// ~/.local/state/opencode/model.json 的 recent/favorite 数组(providerID/modelID)。
// 停用/删除厂商只改 DB catalog,历史不动 → 画面出现「说重载完了,模型还在」
// 的前后矛盾(2026-10-20 实锤:xiaomi 已从 catalog 删除,但 model.json 的
// recent 里还躺着 {providerID:"xiaomi",modelID:"mimo-v2.6-flash"})。
// 每次热重载前/启动时按停用清单把命中条目清出去:
// ① cfg「以下是停用列表」(provider 级,支持 * 通配)
// ② model-manager.disabled.json(modelID 级镜像)
// ③ model-manager.removed.json(已彻底删除的厂商)
// 通配语义与 model-manager.ts 的 patMatch 完全一致(大小写不敏感,* 通配)。
function mmPatMatch(pattern, text) {
const p = String(pattern).toLowerCase()
const t = String(text || "").toLowerCase()
if (!p) return false
if (p === t) return true
if (p.includes("*")) {
const re = new RegExp("^" + p.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i")
return re.test(t)
}
return false
}
function mmPruneModelState(reason) {
try {
const stop = mmReadStopList()
const removed = mmReadRemovedList()
let disabled = {} // modelID 级:provider -> [modelID]
try {
const dp = join(homedir(), ".config", "opencode", "model-manager.disabled.json")
if (existsSync(dp)) {
const j = JSON.parse(readFileSync(dp, "utf-8"))
for (const k of Object.keys(j || {})) {
if (Array.isArray(j[k])) disabled[k] = j[k].filter((x) => typeof x === "string")
}
}
} catch {}
if (!stop.length && !removed.length && !Object.keys(disabled).length) return 0
const mp = join(homedir(), ".local", "state", "opencode", "model.json")
if (!existsSync(mp)) return 0
const st = JSON.parse(readFileSync(mp, "utf-8"))
const hitProv = (pid) => {
if (!pid) return false
if (removed.includes(pid)) return true
return stop.some((pat) => mmPatMatch(pat, pid))
}
const hit = (e) => {
if (!e || typeof e !== "object") return false
if (hitProv(e.providerID)) return true
const arr = disabled[e.providerID]
return Array.isArray(arr) && arr.includes(e.modelID)
}
const recent = Array.isArray(st.recent) ? st.recent : []
const favorite = Array.isArray(st.favorite) ? st.favorite : []
const nextRecent = recent.filter((e) => !hit(e))
const nextFav = favorite.filter((e) => !hit(e))
const n = recent.length - nextRecent.length + (favorite.length - nextFav.length)
if (n <= 0) return 0
st.recent = nextRecent
st.favorite = nextFav
writeFileSync(mp, JSON.stringify(st), "utf-8")
log("[oc-session-all] 历史清理(" + reason + "): 从「最近使用/收藏」剔除停用条目 " + n + " 条 → " + mp)
return n
} catch (e) {
log("[oc-session-all] 历史清理失败: " + (e.message || e))
return 0
}
}
function mmEnqueue(op, payload) {
try {
const line = JSON.stringify({ op, payload: payload || {}, at: Date.now() })
@@ -560,12 +628,19 @@ function mmStartFlushPoller(context) {
return
}
log("[oc-session-all] flush 新标记 tag=" + f.lastOp + " → 执行 TUI 热重载")
// 先清「最近使用/收藏」历史中的停用条目,再 reload —— 让画面与 catalog 一致
// (修「前面说重载完毕,后面模型还在」:那条躺在 model.json 历史里,不归 catalog 管)
const pruned = mmPruneModelState("flush:" + f.lastOp)
await delay(500) // DB 写可见性缓冲
const ok = await doReload(context)
log("[oc-session-all] 模型目录热重载 " + (ok ? "OK" : "ERROR") + " tag=" + f.lastOp)
log(
"[oc-session-all] 模型目录热重载 " + (ok ? "OK" : "ERROR") + " tag=" + f.lastOp + (pruned ? "(历史清理 " + pruned + " 条)" : ""),
)
try {
context.ui.toast.show({
message: ok ? "模型目录已热重载,/models 立即生效" : "热重载失败(详见 oc-session-all.log)",
message: ok
? "模型目录已热重载" + (pruned ? ",清出停用历史 " + pruned + " 条" : "") + ",/models 立即生效"
: "热重载失败(详见 oc-session-all.log)",
variant: "info",
duration: 4000,
})
@@ -960,8 +1035,9 @@ async function openModelManager(context) {
export default Plugin.define({
id: "oc-session-all",
async setup(context) {
log("[oc-session-all] v11 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 推送兜底(GET+merge补shell) + flush常驻热重载 + 模型停用持久化(cfg) + skill热加载")
log("[oc-session-all] v12 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 推送兜底(GET+merge补shell) + flush常驻热重载 + 模型停用持久化(cfg) + skill热加载 + Recent历史清理")
mmStartFlushPoller(context) // 常驻轮询 flush 标记:/manage 操作与后台对账回潮都靠它热重载
mmPruneModelState("startup") // 启动即清「最近使用/收藏」历史中的停用条目(修「重载了但模型还在」)
// ── 读取 mcpcfg.txt(键值小白配置)────────────────────────────────
const mcfg = readMcpCfg()