server插件v2:reload前mcp配置推送兜底(PATCH /config三连),文案键值化

- 新增 pushMcpConfig:on 对调写 flag 后、reload 前把活配置 mcp 推进
  运行中 OC —— client.config.update({config}) → ({body}) → 原生
  fetch PATCH /config(无 /api 前缀)三连;成功=双保险,失败降级单通道
- SetResult 增 pushed 字段,工具返回文案按双保险/单通道区分
- 用户可见文案「第1行」全部键值化为「模式」键(mcpcfg 已是键值格式)
- 加载日志版本标识升级 v2(含推送兜底);README 补双保险链路说明
This commit is contained in:
ypshidifu
2026-09-23 15:31:55 +08:00
parent ec3e241615
commit a25683591e
2 changed files with 87 additions and 11 deletions
+10 -2
View File
@@ -60,7 +60,7 @@ C:\Users\<你>\.config\opencode\plugins\ai-mcp.ts # Windows
| --- | --- |
| 工具名 | `mcp_switch` |
| 参数 | `action`:`on` / `off` / `status` |
| 效果 | 对调两份配置 → 校验活文件 → `location.reload` 热重载 |
| 效果 | 对调两份配置 → 校验活文件 → **先推送 mcp 配置进 OC** → `location.reload` 热重载 |
`status` 只读,任何时候都允许;`on` / `off` 按 `mcpcfg.txt`「模式」键门控:
@@ -70,8 +70,16 @@ C:\Users\<你>\.config\opencode\plugins\ai-mcp.ts # Windows
| 手动 | ❌ 拒绝,提示用户用 `/mcpon` `/mcpoff` | ✅ 仅此可用 |
| 关闭 | ❌ 拒绝 | ❌ 不注册 |
**开启方向是「推送 + reload」双保险(v2 新增)**:对调并写好 `mcp.on.flag` 之后、
reload 之前,先把活配置里的 `mcp` 推进**运行中的 OC** —— 三连尝试:
`client.config.update({ config: mcp })`(SDK → `PATCH /config`,**无 `/api` 前缀**)
→ `({ body: { mcp } })` → 原生 `fetch PATCH /config`。
推送成功再 reload,MCP **百分百加载**;推送失败只记日志,自动降级为原 reload 单通道,
不影响后续流程。TUI 插件 `/mcpon`(v9)与 CLI `mcpctl on` 是同一套链。
reload 走三连尝试:`client.location.reload({location})` → `({})` → 无参 →
兜底 `POST /api/location/reload`。全部失败时配置**已经对调完成**,重启 OpenCode 一样生效。
兜底 `POST /api/location/reload`。全部失败时配置**已经对调完成**(若推送已成功则
OC 内已是新配置),重启 OpenCode 一样生效。
---
+77 -9
View File
@@ -3,7 +3,7 @@
// 职责:通过 mcp_switch 工具,让大模型自己切换「配置对调」状态,
// 实现 MCP 的热开/热关(对调 opencode.json 两份配置 + reload 重载)。
//
// 模式(mcpcfg.txt 第1行)语义:
// 模式(mcpcfg.txt「模式」键)语义:
// - 自动 auto :大模型可自由调用本工具切换状态;/mcpon /mcpoff 视为手动切换一次
// - 手动 manual:只允许用户用 /mcpon /mcpoff,本工具拒绝切换(status 仍可查)
// - 关闭 off :功能整体禁用,本工具拒绝一切操作
@@ -287,8 +287,71 @@ async function doReload(client: any, serverUrl: any) {
return { ok: false, why: msg }
}
// ---------------------------------------------------------------- P2 兜底:reload 前先把 mcp 配置送进 OC
// 对调只是改了磁盘,reload 读盘时序偶发不稳会导致 MCP 没加载上。
// 这里在 reload 之前先 PATCH /config 把 mcp 配置推进运行中的 OC,
// 「配置就位」不再依赖 reload —— 推送 + reload 双保险,百分百加载。
// 推送失败自动降级为原有 reload 单通道,不影响对调主流程。
async function pushMcpConfig(ctx: any, activePath: string | null): Promise<boolean> {
try {
if (!activePath) return false
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: string[] = []
const c = ctx?.client
if (c && c.config && typeof c.config.update === "function") {
// SDK 签名:parameters.config -> body(PATCH /config,注意无 /api 前缀)
try {
await c.config.update({ config: mcp })
await slog(c, "info", "mcp 配置已推送进 OC(client.config.update → PATCH /config)")
return true
} catch (e: any) {
errs.push("config.update: " + e.message)
}
try {
await c.config.update({ body: { mcp } })
await slog(c, "info", "mcp 配置已推送进 OC(body 变体)")
return true
} catch (e: any) {
errs.push("config.update(body): " + e.message)
}
} else {
errs.push("client.config.update 不存在")
}
if (ctx?.serverUrl) {
try {
const res = await fetch(new URL("/config", ctx.serverUrl), {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ mcp }),
})
if (res.ok) {
await slog(c, "info", "mcp 配置已推送进 OC(HTTP PATCH /config)")
return true
}
errs.push("http PATCH /config -> " + res.status)
} catch (e: any) {
errs.push("http: " + e.message)
}
}
await slog(c, "warn", "mcp 配置推送未成功(继续走 reload 兜底): " + errs.join("; "))
return false
} catch (e: any) {
await slog(ctx?.client, "warn", "mcp 配置推送异常(继续走 reload 兜底): " + e.message)
return false
}
}
// ---------------------------------------------------------------- 切换主逻辑
type SetResult = { ok: boolean; unchanged?: boolean; reloaded?: boolean; why?: string | null; state?: string }
type SetResult = {
ok: boolean
unchanged?: boolean
reloaded?: boolean
pushed?: boolean
why?: string | null
state?: string
}
async function setMcp(ctx: any, wantOn: boolean): Promise<SetResult> {
const cfg = readMcpCfg()
@@ -325,10 +388,13 @@ async function setMcp(ctx: any, wantOn: boolean): Promise<SetResult> {
} catch {}
await slog(ctx?.client, "info", `配置已对调 -> ${wantOn ? "含 MCP" : "安静(无 MCP)"}`)
// P2 兜底:开启时先推送再 reload(双保险;关闭无需推送)
const pushed = wantOn ? await pushMcpConfig(ctx, cfg.activePath) : undefined
const r = await doReload(ctx?.client, ctx?.serverUrl)
return {
ok: true,
reloaded: r.ok,
pushed,
state: wantOn ? "开启" : "关闭",
why: r.ok ? null : "配置已对调,但热重载调用失败(重启 OC 生效):" + (r.why || ""),
}
@@ -379,7 +445,7 @@ async function buildHooks(ctx: any) {
"action=on 把配置对调为含 MCP 的原始版并重载,让 MCP 连接问题暴露出来便于排查;" +
"action=off 对调为不含 MCP 的安静版并重载,恢复零提示的安静状态;" +
"action=status 只查询当前模式与 MCP 状态,不改动任何文件。" +
"仅在 mcpcfg.txt 第1行为「自动」时允许切换;「手动」模式请让用户执行 /mcpon 或 /mcpoff;「关闭」模式直接拒绝。",
"仅在 mcpcfg.txt「模式: 自动」时允许切换;「手动」模式请让用户执行 /mcpon 或 /mcpoff;「关闭」模式直接拒绝。",
args: {
action: toolFn.schema.enum(["on", "off", "status"]),
},
@@ -398,7 +464,7 @@ async function buildHooks(ctx: any) {
return {
title: "MCP 当前状态",
output: [
`模式:${modeTxt}(mcpcfg.txt 第1行)`,
`模式:${modeTxt}(mcpcfg.txt「模式」键)`,
`MCP:${hasMcp ? "已开启(配置含 mcp.servers)" : "已关闭(安静,无 mcp 配置)"}`,
`活配置文件:${cfg.activePath}`,
`对调目标文件:${cfg.onPath === cfg.activePath ? cfg.quietPath : cfg.onPath}`,
@@ -412,7 +478,7 @@ async function buildHooks(ctx: any) {
if (cfg.mode === "off") {
return {
title: "MCP 开关被禁用",
output: "拒绝执行:MCP 开关功能已关闭(mcpcfg.txt 第1行=关闭)。如需使用,请让用户把第1行改成「自动」。",
output: "拒绝执行:MCP 开关功能已关闭(mcpcfg.txt「模式: 关闭」)。如需使用,请让用户把「模式」键改成「自动」。",
}
}
@@ -422,7 +488,7 @@ async function buildHooks(ctx: any) {
output:
"拒绝执行:当前是手动模式,只有用户能切换 MCP 状态。\n" +
"请提醒用户在输入框执行 /mcpon(开启)或 /mcpoff(关闭);" +
"如果确实需要大模型自动切换,让用户把 mcpcfg.txt 第1行改成「自动」。",
"如果确实需要大模型自动切换,让用户把 mcpcfg.txt 的「模式」键改成「自动」。",
}
}
@@ -439,7 +505,9 @@ async function buildHooks(ctx: any) {
return { title: "MCP 状态未变", output: "无需切换。" + res.why }
}
const reloadTxt = res.reloaded
? "配置已对调并完成热重载,MCP 配置立即生效。"
? res.pushed
? "mcp 配置已先推送进 OC,再完成对调 + 热重载(双保险),MCP 百分百加载。"
: "配置已对调并完成热重载,MCP 配置立即生效。(推送兜底未生效,已走 reload 单通道)"
: "配置已对调,但热重载未成功:" + (res.why || "") + "\n需要重启 OpenCode 才能生效。"
return {
title: "MCP 已切换",
@@ -454,7 +522,7 @@ async function buildHooks(ctx: any) {
// ---------------------------------------------------------------- 双导出
// 1) 命名导出:官方文档形式(export const X: Plugin = async (ctx) => ...)
export const AiMcp: Plugin = async (ctx) => {
await slog(ctx?.client, "info", "ai-mcp server 插件已加载(命名导出)")
await slog(ctx?.client, "info", "ai-mcp v2 server 插件已加载(命名导出,含推送兜底)")
return await buildHooks(ctx)
}
@@ -463,7 +531,7 @@ export const AiMcp: Plugin = async (ctx) => {
export default {
id: "ai-mcp",
async setup(ctx: any) {
await slog(ctx?.client, "info", "ai-mcp server 插件已加载(default/setup)")
await slog(ctx?.client, "info", "ai-mcp v2 server 插件已加载(default/setup,含推送兜底)")
return await buildHooks(ctx)
},
}