From cd25c0a3d090072109640f1daa7479f4e5ad6735 Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Mon, 19 Oct 2026 18:38:40 +0800 Subject: [PATCH] =?UTF-8?q?tui.js=EF=BC=9A=E8=BF=9B=E5=8E=82=E8=A1=A5?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=87=BA=E5=8E=82(=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96)=E5=85=A5=E5=8F=A3=E3=80=81=E6=95=B4=E5=8E=82?= =?UTF-8?q?=E5=81=9C=E7=94=A8=E6=94=B9=E5=8D=95=E6=9D=A1=20disable=5Fall?= =?UTF-8?q?=E3=80=81flush=20=E6=94=B9=E5=B8=B8=E9=A9=BB=E8=BD=AE=E8=AF=A2?= =?UTF-8?q?=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 | 119 +++++++++++++++------------------- 1 file changed, 52 insertions(+), 67 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index a25d5b4..4456737 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -474,61 +474,40 @@ 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() +// ── 常驻 flush 轮询:daemon(server 插件)每次把队列/对账写完会落 flush 标记, +// 这里每 2s 看一眼,发现新标记就执行 location.reload(TUI ctx 的 client.location +// 是本环境唯一实测可用的 reload 通道;server 插件 ctx 无 client/serverUrl)。 +// 覆盖两类来源:① /manage 入队操作消费完 ② 后台对账压制 catalog 回潮后。 +// 启动时以当前 flush.at 为基线,不重放历史标记。 +let mmFlushSeenAt = 0 +function mmStartFlushPoller(context) { + try { + if (existsSync(MM_FLUSH)) { + try { + mmFlushSeenAt = JSON.parse(readFileSync(MM_FLUSH, "utf-8")).at || 0 + } catch {} + } + } catch {} + log("[oc-session-all] flush 常驻轮询已启动(2s,基线 at=" + mmFlushSeenAt + ")") + setInterval(async () => { 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) + if (!existsSync(MM_FLUSH)) return + let f = null + try { + f = JSON.parse(readFileSync(MM_FLUSH, "utf-8")) + } catch { + return } - await delay(500) // 最后一条 DB 写的可见性缓冲 + if (!f || typeof f.at !== "number" || f.at <= mmFlushSeenAt) return + mmFlushSeenAt = f.at + if (f.serverReload) { + log("[oc-session-all] flush 新标记 tag=" + f.lastOp + "(server 侧已 reload,TUI 免重复触发)") + return + } + log("[oc-session-all] flush 新标记 tag=" + f.lastOp + " → 执行 TUI 热重载") + 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", - ) + log("[oc-session-all] 模型目录热重载 " + (ok ? "OK" : "ERROR") + " tag=" + f.lastOp) try { context.ui.toast.show({ message: ok ? "模型目录已热重载,/models 立即生效" : "热重载失败(详见 oc-session-all.log)", @@ -537,11 +516,9 @@ function mmReloadAfterFlush(context, tag) { }) } catch {} } catch (e) { - log("[oc-session-all] mmReloadAfterFlush ERROR: " + (e.stack || e.message)) - } finally { - if (mmWatch === w) mmWatch = null + log("[oc-session-all] flush poller ERROR: " + (e.stack || e.message)) } - })() + }, 2000) } async function openModelManager(context) { @@ -658,7 +635,6 @@ 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 随即生效", variant: "info", @@ -673,24 +649,35 @@ async function openModelManager(context) { const dispName = target.name const models = target.models || [] - // 第二层:管理粒度 —— 单个模型 vs 整个厂商 + // 第二层:管理粒度 —— 单个模型 vs 整个厂商 vs 恢复出厂(格式化) const scope = await context.ui.dialog.select({ title: "模型管理 · " + dispName + " · 选择粒度", - placeholder: "管单个模型,还是管整个厂商?", + 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") { - // 厂商粒度:停用=逐个下架现存模型;启用=恢复该厂商全部模型(reset 拉回 pristine) + // 厂商粒度:停用=整厂一条 disable_all 下架(持久化进 cfg 停用列表);启用=reset 拉回 pristine const pAct = await context.ui.dialog.select({ title: "对整个 " + dispName + " 执行", placeholder: "停用整个厂商,还是恢复全部模型?", options: [ - { title: "⛔ 停用整个厂商(全部模型下架)", value: "disable_all" }, + { title: "⛔ 停用整个厂商(全部模型下架 · 持久化进 cfg 停用列表)", value: "disable_all" }, { title: "✅ 启用整个厂商(恢复全部模型)", value: "enable_all" }, ], }) @@ -700,15 +687,13 @@ async function openModelManager(context) { context.ui.toast.show({ message: dispName + " 的模型已全部停用", variant: "warning" }) return } - for (const m of models) mmEnqueue("disable", { provider: target.id, modelID: m }) - mmReloadAfterFlush(context, "disable_all:" + target.id) + mmEnqueue("disable_all", { provider: target.id }) context.ui.toast.show({ - message: "已入队:停用整个 " + dispName + "(" + models.length + " 个模型),daemon 写完自动热重载", + 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 写完自动热重载", variant: "info", @@ -746,7 +731,6 @@ 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 写完自动热重载,/models 随即更新", variant: "info", @@ -762,7 +746,8 @@ async function openModelManager(context) { export default Plugin.define({ id: "oc-session-all", async setup(context) { - log("[oc-session-all] v10 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 推送兜底(GET+merge补shell) + 热重载 + skill热加载") + log("[oc-session-all] v11 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 推送兜底(GET+merge补shell) + flush常驻热重载 + 模型停用持久化(cfg) + skill热加载") + mmStartFlushPoller(context) // 常驻轮询 flush 标记:/manage 操作与后台对账回潮都靠它热重载 // ── 读取 mcpcfg.txt(键值小白配置)──────────────────────────────── const mcfg = readMcpCfg()