tui.js:模型操作入队后等 daemon flush 落盘再执行 location.reload 热重载
This commit is contained in:
@@ -404,8 +404,13 @@ function toastResult(context, res, actionLabel) {
|
||||
// 第一层:选 provider(+ 顶部的「恢复出厂」全局项)
|
||||
// 第二层:对该 provider 选 停用 / 启用(逐模型);选「恢复出厂」则整 provider 还原
|
||||
// 操作不直接改 DB(OC 持写锁),而是写 model-manager.outbox.jsonl,
|
||||
// 由 model-manager server 插件的 daemon 消费 → fork Python 子进程写 DB → 热重载。
|
||||
// 由 model-manager server 插件的 daemon 消费 → fork Python 子进程写 DB →
|
||||
// 排空后写 flush 标记 → 本插件(TUI 侧)执行 location.reload 热重载。
|
||||
// ★ 热重载为什么在 TUI 侧:2026-10-19 实测 V2 server 插件 ctx 没有 client.location /
|
||||
// serverUrl(model-manager 与 ai-mcp 都报「没有可用的 reload 通道」),
|
||||
// 而 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")
|
||||
|
||||
function mmEnqueue(op, payload) {
|
||||
try {
|
||||
@@ -469,6 +474,76 @@ function mmReadCatalog() {
|
||||
})
|
||||
}
|
||||
|
||||
// ── 热重载接力:等 daemon 把 outbox 排空并写完 flush 标记,再由 TUI 执行 reload ──
|
||||
// 触发方:每次 mmEnqueue 批次入队后调用(since = 入队时刻,只认之后落盘的 flush,
|
||||
// 避免被上一批的旧标记骗到)。同一时间只跑一个 watcher,后续批次合并进来。
|
||||
// 幂等:flush.serverReload=true 说明 server 进程内通道已成功(将来宿主补通道时),TUI 免重复触发。
|
||||
let mmWatch = null
|
||||
function mmReloadAfterFlush(context, tag) {
|
||||
const since = Date.now()
|
||||
if (mmWatch) {
|
||||
if (since > mmWatch.since) mmWatch.since = since
|
||||
mmWatch.tag = mmWatch.tag + "+" + tag
|
||||
log("[oc-session-all] reload watcher 已在跑,合并批次: " + mmWatch.tag)
|
||||
return
|
||||
}
|
||||
const w = { since, tag }
|
||||
mmWatch = w
|
||||
;(async () => {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
let waiting = false
|
||||
for (;;) {
|
||||
if (Date.now() - t0 > 180000) {
|
||||
log("[oc-session-all] 等 daemon flush 超时(180s),放弃热重载 tag=" + w.tag)
|
||||
try {
|
||||
context.ui.toast.show({ message: "热重载等待超时(daemon 未 flush),详见日志", variant: "error", duration: 5000 })
|
||||
} catch {}
|
||||
return
|
||||
}
|
||||
let flush = null
|
||||
try {
|
||||
if (existsSync(MM_FLUSH)) flush = JSON.parse(readFileSync(MM_FLUSH, "utf-8"))
|
||||
} catch {}
|
||||
if (flush && typeof flush.at === "number" && flush.at >= w.since) {
|
||||
if (flush.serverReload) {
|
||||
log("[oc-session-all] server 侧进程内 reload 已成功,TUI 免重复触发 tag=" + w.tag)
|
||||
return
|
||||
}
|
||||
break // DB 已全部写完 → 出去执行 TUI 侧 reload
|
||||
}
|
||||
if (!waiting) {
|
||||
log("[oc-session-all] 等 daemon 排空队列并落 flush… tag=" + w.tag)
|
||||
waiting = true
|
||||
}
|
||||
await delay(700)
|
||||
}
|
||||
await delay(500) // 最后一条 DB 写的可见性缓冲
|
||||
const ok = await doReload(context)
|
||||
log(
|
||||
"[oc-session-all] 模型目录热重载 " +
|
||||
(ok ? "OK" : "ERROR") +
|
||||
" tag=" +
|
||||
w.tag +
|
||||
" 耗时=" +
|
||||
((Date.now() - t0) / 1000).toFixed(1) +
|
||||
"s",
|
||||
)
|
||||
try {
|
||||
context.ui.toast.show({
|
||||
message: ok ? "模型目录已热重载,/models 立即生效" : "热重载失败(详见 oc-session-all.log)",
|
||||
variant: "info",
|
||||
duration: 4000,
|
||||
})
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
log("[oc-session-all] mmReloadAfterFlush ERROR: " + (e.stack || e.message))
|
||||
} finally {
|
||||
if (mmWatch === w) mmWatch = null
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
async function openModelManager(context) {
|
||||
try {
|
||||
// ── 收集「已连接 provider」,两个来源都读,合并去重 ──────────────────
|
||||
@@ -583,8 +658,9 @@ async function openModelManager(context) {
|
||||
if (!pickProvider) return
|
||||
if (pickProvider === "__reset_all__") {
|
||||
for (const p of connectedProviders) mmEnqueue("reset", { provider: p.id })
|
||||
mmReloadAfterFlush(context, "reset_all")
|
||||
context.ui.toast.show({
|
||||
message: "恢复出厂已入队(" + connectedProviders.length + " 个 provider),等 daemon 消费后 /models 生效",
|
||||
message: "恢复出厂已入队(" + connectedProviders.length + " 个 provider),daemon 写完自动热重载,/models 随即生效",
|
||||
variant: "info",
|
||||
})
|
||||
return
|
||||
@@ -625,14 +701,16 @@ async function openModelManager(context) {
|
||||
return
|
||||
}
|
||||
for (const m of models) mmEnqueue("disable", { provider: target.id, modelID: m })
|
||||
mmReloadAfterFlush(context, "disable_all:" + target.id)
|
||||
context.ui.toast.show({
|
||||
message: "已入队:停用整个 " + dispName + "(" + models.length + " 个模型),daemon 5s 内消费",
|
||||
message: "已入队:停用整个 " + dispName + "(" + models.length + " 个模型),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
} else {
|
||||
mmEnqueue("reset", { provider: target.id })
|
||||
mmReloadAfterFlush(context, "enable_all:" + target.id)
|
||||
context.ui.toast.show({
|
||||
message: "已入队:启用整个 " + dispName + "(恢复全部模型),daemon 5s 内消费",
|
||||
message: "已入队:启用整个 " + dispName + "(恢复全部模型),daemon 写完自动热重载",
|
||||
variant: "info",
|
||||
})
|
||||
}
|
||||
@@ -668,8 +746,9 @@ async function openModelManager(context) {
|
||||
if (!action) return
|
||||
if (action === "disable") mmEnqueue("disable", { provider: target.id, modelID: pickModel })
|
||||
else mmEnqueue("enable", { provider: target.id, modelID: pickModel })
|
||||
mmReloadAfterFlush(context, action + ":" + target.id + "/" + pickModel)
|
||||
context.ui.toast.show({
|
||||
message: "已入队:" + action + " " + dispName + "/" + pickModel + "(daemon 5s 内消费,/models 将更新)",
|
||||
message: "已入队:" + action + " " + dispName + "/" + pickModel + ",daemon 写完自动热重载,/models 随即更新",
|
||||
variant: "info",
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user