From 6e0ed1743aac38848da0dbbbeed1a3f835e97a33 Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Wed, 23 Sep 2026 15:31:55 +0800 Subject: [PATCH] =?UTF-8?q?TUI=E6=8F=92=E4=BB=B6v9=EF=BC=9A=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Ddirname=E5=AF=BC=E5=85=A5=E7=BC=BA=E5=A4=B1=E8=87=B4?= =?UTF-8?q?=E8=87=AA=E6=84=88=E9=9D=99=E9=BB=98=E5=A4=B1=E6=95=88=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9Ereload=E5=89=8Dmcp=E9=85=8D=E7=BD=AE=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E5=85=9C=E5=BA=95=EF=BC=88=E5=8F=8C=E4=BF=9D=E9=99=A9?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补回 node:path 的 dirname 导入:healShadowPath 引用它,缺导入时 ReferenceError 被自身 catch 吞掉,影子路径自愈静默失效 - 新增 pushMcpConfig:swap+flag 之后、reload 之前把活配置 mcp 推进 运行中 OC(SDK config.update → body 形参 → experimental 三连), 推送成功+reload 双保险,失败降级原单通道 - toast 区分双保险/单通道文案;log/toast 版本标识升级 v9 - 单测新增 S0 回归断言:源文件 path 导入必须含 dirname (prelude 复制的导入曾掩盖源码缺失,15/15 假绿 -> 16/16) --- tui-plugins/oc-session-all.js | 70 ++++++++++++++++++++++++++++++--- tui-plugins/readmcpcfg.test.mjs | 11 ++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index 420699a..a76541e 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -9,7 +9,7 @@ import { statSync, rmSync, } from "fs" -import { join, basename } from "path" +import { join, basename, dirname } from "path" import { homedir } from "os" // ── 日志(跨平台:Windows C:\Users\... / WSL /root)──────────────────── @@ -265,6 +265,53 @@ async function doReload(context) { return false } +// P2 兜底:reload 之前先把 mcp 配置 PATCH 进运行中的 OC(PATCH /config)。 +// 这样「配置就位」不再依赖 reload 的读盘时序 —— 推送 + reload 双保险, +// 保证 MCP 百分百加载;推送失败也只降级为原有 reload 单通道,不影响对调。 +async function pushMcpConfig(context, activePath) { + try { + const j = JSON.parse(readFileSync(activePath, "utf8")) + const mcp = j && j.mcp + if (!mcp || !mcp.servers || !Object.keys(mcp.servers).length) return false + const errs = [] + const cfgApi = context.client && context.client.config + if (cfgApi && typeof cfgApi.update === "function") { + // SDK 签名:parameters.config -> body(PATCH /config) + try { + await cfgApi.update({ config: mcp }) + log("[oc-session-all] mcp 配置已推送进 OC(client.config.update → PATCH /config)") + return true + } catch (e) { + errs.push("config.update: " + e.message) + } + try { + await cfgApi.update({ body: { mcp } }) + log("[oc-session-all] mcp 配置已推送进 OC(body 变体)") + return true + } catch (e) { + errs.push("config.update(body): " + e.message) + } + } else { + errs.push("client.config.update 不存在") + } + const exp = context.client && context.client.experimental && context.client.experimental.config + if (exp && typeof exp.update === "function") { + try { + await exp.update({ mcp }) + log("[oc-session-all] mcp 配置已推送进 OC(experimental.config.update)") + return true + } catch (e) { + errs.push("experimental: " + e.message) + } + } + log("[oc-session-all] mcp 配置推送未成功(继续走 reload 兜底): " + errs.join("; ")) + return false + } catch (e) { + log("[oc-session-all] mcp 配置推送异常(继续走 reload 兜底): " + e.message) + return false + } +} + // 切换 MCP 开/关(对调 + reload)。返回 { ok, unchanged?, reloaded?, why } async function setMcp(context, wantOn) { const cfg = readMcpCfg() @@ -289,16 +336,29 @@ async function setMcp(context, wantOn) { try { if (wantOn) writeFileSync(ON_FLAG, String(process.pid)) else if (existsSync(ON_FLAG)) rmSync(ON_FLAG) - } catch (e) {} + } catch (e) { + log("[oc-session-all] mcp.on.flag 写入失败: " + e.message) + } log("[oc-session-all] config swapped -> " + (wantOn ? "WITH mcp" : "QUIET (no mcp)")) + // P2 兜底:开启时先推送再 reload(双保险;关闭无需推送) + const pushed = wantOn ? await pushMcpConfig(context, cfg.activePath) : undefined const reloaded = await doReload(context) - return { ok: true, reloaded, why: reloaded ? null : "配置已对调,但热重载调用失败(重启 OC 生效)" } + return { + ok: true, + reloaded, + pushed, + why: reloaded ? null : "配置已对调,但热重载调用失败(重启 OC 生效)", + } } function toastResult(context, res, actionLabel) { let message if (!res.ok) message = actionLabel + "失败:" + res.why else if (res.unchanged) message = res.why + else if (res.reloaded && res.pushed === true) + message = actionLabel + "成功(mcp 配置已先送入 OC + 对调 + 热重载,双保险)" + else if (res.reloaded && res.pushed === false) + message = actionLabel + "成功(已对调并热重载;配置推送未生效,走 reload 兜底)" else if (res.reloaded) message = actionLabel + "成功(配置已对调并热重载)" else message = actionLabel + "成功,但热重载未完成:" + res.why try { @@ -310,7 +370,7 @@ function toastResult(context, res, actionLabel) { export default Plugin.define({ id: "oc-session-all", async setup(context) { - log("[oc-session-all] v8 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 热重载 + skill热加载") + log("[oc-session-all] v9 loaded — 会话选择器 + MCP配置对调(mcpcfg.txt) + 推送兜底 + 热重载 + skill热加载") // ── 读取 mcpcfg.txt(键值小白配置)──────────────────────────────── const mcfg = readMcpCfg() @@ -724,7 +784,7 @@ export default Plugin.define({ try { context.ui.toast.show({ message: - "oc-session-all v8 已加载 — /sessions-all 浏览全部会话(Ctrl+O)" + + "oc-session-all v9 已加载 — /sessions-all 浏览全部会话(Ctrl+O)" + (featureOn ? ",/mcpon 开启 MCP、/mcpoff 关闭 MCP" : ""), variant: "info", duration: 20000, diff --git a/tui-plugins/readmcpcfg.test.mjs b/tui-plugins/readmcpcfg.test.mjs index 01d6c28..5bd0758 100644 --- a/tui-plugins/readmcpcfg.test.mjs +++ b/tui-plugins/readmcpcfg.test.mjs @@ -50,6 +50,17 @@ function check(name, cond, extra) { } } +// ---- 回归:源文件 path 导入必须含 dirname ---- +// healShadowPath 用了 dirname;源文件缺导入时 ReferenceError 会被它自己的 +// catch 吞掉 → 影子自愈静默失效。prelude 里复制的导入会掩盖这个缺失 +// (曾导致 15/15 假绿),所以必须对源文件本体断言。 +const pathImportLine = (text.match(/^import\s*\{[^}]*\}\s*from\s*["']path["']/m) || [""])[0] +check( + "S0 源文件 path 导入含 dirname", + /dirname/.test(pathImportLine), + "import line=" + pathImportLine, +) + // ---- 场景1:键值格式(注释+中文冒号)---- const active = join(work, "opencode.json") const shadow = join(work, "opencode.json.2")