From 0001fc7d6439b3b309dcbec36425241108a0e0c0 Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Mon, 19 Oct 2026 16:59:21 +0800 Subject: [PATCH] =?UTF-8?q?tui.js=EF=BC=9A=E6=A8=A1=E5=9E=8B=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=85=A5=E9=98=9F=E5=90=8E=E7=AD=89=20daemon=20flush?= =?UTF-8?q?=20=E8=90=BD=E7=9B=98=E5=86=8D=E6=89=A7=E8=A1=8C=20location.rel?= =?UTF-8?q?oad=20=E7=83=AD=E9=87=8D=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tui-plugins/oc-session-all.js | 89 +++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index 6cad864..a25d5b4 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -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) {