From b74d87cfbb08fff3257ba6b2751591f449f4db0f Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Wed, 23 Sep 2026 00:05:43 +0800 Subject: [PATCH] =?UTF-8?q?v6:=20MCP=E6=94=B9config=E7=A6=81=E7=94=A8+?= =?UTF-8?q?=E6=8F=92=E4=BB=B6mcp.connect=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E5=90=AF=E7=94=A8=EF=BC=9Bskill=E7=9B=AE=E5=BD=955s=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E7=83=AD=E5=8A=A0=E8=BD=BD=E5=85=9C=E5=BA=95=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=9B=AE=E5=BD=95=E7=9B=B2=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tui-plugins/oc-session-all.js | 166 +++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 5 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index a97e313..81e41e2 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -1,5 +1,13 @@ import { Plugin } from "@opencode/plugin/tui" -import { appendFileSync, mkdirSync, existsSync } from "fs" +import { + appendFileSync, + mkdirSync, + existsSync, + readFileSync, + writeFileSync, + readdirSync, + statSync, +} from "fs" import { join } from "path" import { homedir } from "os" @@ -38,12 +46,17 @@ function projectDisplayName(session, projectMap) { return session.projectID ? session.projectID.slice(0, 8) : "unknown" } +// MCP server 名单:opencode.json 里 enabled:false 的项(启动无 toast), +// 由本插件运行时 connect 启用(官方语义:overriding disabled config until restart)。 +// 新增被禁用的 MCP server 时把名字加进来即可。 +const MCP_NAMES = ["baota-mcp"] + export default Plugin.define({ id: "oc-session-all", async setup(context) { - log("[oc-session-all] v5 loaded — official slot({append}) + layer(()=>({})) pattern") + log("[oc-session-all] v6 loaded — sessions picker + MCP runtime connect + skill hot-reload") - // ── MCP error toast 拦截(monkey-patch toast.show,无未验证 API)── + // ── MCP error toast 拦截(monkey-patch 兜底;主方案已改为 config 禁用)── try { const toast = context.ui.toast const orig = toast.show.bind(toast) @@ -68,11 +81,154 @@ export default Plugin.define({ } return orig(opts) } - log("[oc-session-all] MCP toast interceptor installed") + log("[oc-session-all] MCP toast interceptor installed (backup layer)") } catch (e) { log("[oc-session-all] toast interceptor install ERROR: " + e.message) } + // ── MCP 运行时启用(config enabled:false + 插件 connect)──────────── + const enableMcp = async () => { + // 等服务端 MCP 子系统初始化完成 + await delay(1500) + try { + // 1) dump 现状(供后续动态化:字段名/状态一并写日志) + try { + const list = await context.client.mcp.list({ location: context.location }) + const data = list && list.data !== undefined ? list.data : list + log("[oc-session-all] mcp.list dump: " + JSON.stringify(data).substring(0, 800)) + } catch (e) { + log("[oc-session-all] mcp.list ERROR: " + e.message) + } + // 2) 逐个 connect(POST /api/experimental/mcp/:server/connect) + for (const name of MCP_NAMES) { + try { + await context.client.mcp.connect({ server: name, location: context.location }) + log("[oc-session-all] mcp.connect(" + name + ") OK") + } catch (e) { + log("[oc-session-all] mcp.connect(" + name + ") ERROR: " + e.message) + } + } + // 3) connect 后再拉一次 list(服务端状态已变,日志记录结果) + try { + const after = await context.client.mcp.list({ location: context.location }) + const data = after && after.data !== undefined ? after.data : after + log("[oc-session-all] mcp.list after connect: " + JSON.stringify(data).substring(0, 800)) + } catch (e) { + log("[oc-session-all] mcp.list(after) ERROR: " + e.message) + } + } catch (e) { + log("[oc-session-all] enableMcp ERROR: " + e.message) + } + } + void enableMcp() + + // ── Skill 目录热加载(原生 watch 不覆盖"新增 skill 目录",此处轮询兜底)── + // 标准目录(逆向 ConfigSkillPlugin):每个 config entry 下的 skill/ 与 skills/ + const projDir = (context.location && context.location.directory) || process.cwd() + const skillDirs = [ + join(LOG_DIR, "skill"), + join(LOG_DIR, "skills"), + join(projDir, "skill"), + join(projDir, "skills"), + ] + + const skillSignature = () => { + const parts = [] + for (const d of skillDirs) { + if (!existsSync(d)) { + parts.push(d + ":absent") + continue + } + try { + for (const ent of readdirSync(d, { withFileTypes: true })) { + const p = join(d, ent.name) + try { + parts.push(p + ":" + statSync(p).mtimeMs) + } catch (e) {} + if (ent.isDirectory()) { + // 第二层:skills//SKILL.md 或 skills/.md 的兄弟文件 + try { + for (const f of readdirSync(p)) { + const fp = join(p, f) + try { + parts.push(fp + ":" + statSync(fp).mtimeMs) + } catch (e) {} + } + } catch (e) {} + } + } + } catch (e) { + parts.push(d + ":err") + } + } + return parts.join("|") + } + + const presentDirs = skillDirs.filter((d) => existsSync(d)) + let lastSkillSig = skillSignature() + log( + "[oc-session-all] skill watcher armed (5s poll), present dirs: " + + (presentDirs.length ? presentDirs.join(", ") : "(none yet)"), + ) + + const forceSkillReload = async (reason) => { + log("[oc-session-all] skill change detected (" + reason + ") -> forcing reload") + let triggered = "none" + try { + // a) 优先尝试 PATCH /api/experimental/config(触发 config.updated → + // ConfigSkillPlugin 全量 rescan → skill.reload() 推送客户端 invalidate) + try { + const maybePatch = + (context.client.experimental && + context.client.experimental.config && + context.client.experimental.config.update) || + (context.client.config && context.client.config.experimentalUpdate) || + null + if (typeof maybePatch === "function") { + await maybePatch({}) + triggered = "patch" + } + } catch (e) { + log("[oc-session-all] config patch attempt ERROR: " + e.message) + } + // b) 回退:同内容重写 opencode.json(write 事件触发 config watcher) + if (triggered === "none") { + const cfg = join(LOG_DIR, "opencode.json") + if (existsSync(cfg)) { + const raw = readFileSync(cfg, "utf8") + writeFileSync(cfg, raw) + triggered = "rewrite" + } + } + // c) 等服务端 rescan 完成后拉新列表 + await delay(600) + const r = await context.client.skill.list({ location: context.location }) + const arr = (r && r.data) || r + const n = Array.isArray(arr) ? arr.length : -1 + log("[oc-session-all] skill reload via " + triggered + " -> skill.list = " + n + " skills") + try { + context.ui.toast.show({ + message: "Skills reloaded (" + n + ") — " + triggered, + variant: "info", + duration: 3000, + }) + } catch (e) {} + } catch (e) { + log("[oc-session-all] skill reload ERROR: " + e.message) + } + } + + setInterval(() => { + try { + const sig = skillSignature() + if (sig === lastSkillSig) return + lastSkillSig = sig + void forceSkillReload("poll") + } catch (e) { + log("[oc-session-all] skill watcher tick ERROR: " + e.message) + } + }, 5000) + // ── Session 选择器(移植自 v1,改用 v2 dialog.select/router API)── const openPicker = async () => { log("[oc-session-all] openPicker called") @@ -222,7 +378,7 @@ export default Plugin.define({ await delay(3000) try { context.ui.toast.show({ - message: "oc-session-all v5 loaded — /sessions-all or Ctrl+O", + message: "oc-session-all v6 loaded — /sessions-all or Ctrl+O", variant: "info", duration: 20000, })