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:
@@ -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 一样生效。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user