ai-mcp:补模块级留痕日志 + v2 注册说明
- 顶层加 module imported 留痕:区分「没被加载」和「加载了但没被调用」
- 新增 README,记录实测结论:
* OpenCode v2 不再自动扫描全局 plugins/ 下的一级文件,必须在 opencode.json
的 plugins 数组用 file:/// 显式注册,否则文件根本不会被 import
* 两份配置(含 MCP 版 / 安静版)都要注册,否则 mcpoff 对调后插件消失
* 双导出的原因:v2 对 default 有 schema 校验(opencode-btw 就因缺 default 反复报错)
* 插件在 session 初始化时才加载,只启动 serve 不建 session 看不到日志
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# ai-mcp — 给大模型操作的 MCP 开关 server 插件
|
||||
|
||||
注册一个 `mcp_switch` 工具,让大模型自己切换 MCP 的开/关状态
|
||||
(配置对调 + `location.reload` 热重载),配合 TUI 插件的 `/mcpon` `/mcpoff` 形成闭环。
|
||||
|
||||
分支名:`ai-mcp` | 文件:`ai-mcp.ts`
|
||||
|
||||
---
|
||||
|
||||
## 一、安装(v2 必须显式注册,这是踩过的坑)
|
||||
|
||||
> **实测结论**:OpenCode **v2 不再自动扫描全局 `~/.config/opencode/plugins/` 下的一级文件**。
|
||||
> v2 只自动发现**项目级** `.opencode/plugins/<name>/index.ts`。
|
||||
> 全局插件必须写进 `opencode.json(c)` 的 **`plugins` 数组**,否则文件放进去**根本不会被 import**。
|
||||
|
||||
把插件复制到全局插件目录:
|
||||
|
||||
```
|
||||
~/.config/opencode/plugins/ai-mcp.ts # Linux / WSL
|
||||
C:\Users\<你>\.config\opencode\plugins\ai-mcp.ts # Windows
|
||||
```
|
||||
|
||||
然后在 `opencode.json` 里注册(**两份配置都要加**,否则 `mcpoff` 对调之后插件会消失):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": [
|
||||
"opencode-btw",
|
||||
"file:///root/.config/opencode/plugins/ai-mcp.ts" // WSL/Linux
|
||||
// Windows 用: "file:///C:/Users/<你>/.config/opencode/plugins/ai-mcp.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
依赖:插件目录下需有 `package.json` 声明 `@opencode-ai/plugin` 并装好
|
||||
(OpenCode 启动时会跑 `bun install`;WSL 没有 bun 时手动 `npm install --omit=dev`)。
|
||||
|
||||
---
|
||||
|
||||
## 二、工具说明
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| 工具名 | `mcp_switch` |
|
||||
| 参数 | `action`:`on` / `off` / `status` |
|
||||
| 效果 | 对调两份配置 → 校验活文件 → `location.reload` 热重载 |
|
||||
|
||||
`status` 只读,任何时候都允许;`on` / `off` 按 `mcpcfg.txt` 第1行的模式门控:
|
||||
|
||||
| 模式 | 大模型调 `mcp_switch` | 用户 `/mcpon` `/mcpoff` |
|
||||
| --- | --- | --- |
|
||||
| 自动 | ✅ 允许 | ✅(视为手动切换一次) |
|
||||
| 手动 | ❌ 拒绝,提示用户用 `/mcpon` `/mcpoff` | ✅ 仅此可用 |
|
||||
| 关闭 | ❌ 拒绝 | ❌ 不注册 |
|
||||
|
||||
reload 走三连尝试:`client.location.reload({location})` → `({})` → 无参 →
|
||||
兜底 `POST /api/location/reload`。全部失败时配置**已经对调完成**,重启 OpenCode 一样生效。
|
||||
|
||||
---
|
||||
|
||||
## 三、格式说明(双导出)
|
||||
|
||||
```ts
|
||||
// 1) 命名导出 —— V1 legacy 形式(loader 直接调用拿 hooks)
|
||||
export const AiMcp: Plugin = async (ctx) => ({ tool: { mcp_switch: tool({...}) } })
|
||||
|
||||
// 2) default 导出 —— V2 校验形式(OpenCode 2.0.x 的硬性要求)
|
||||
export default { id: "ai-mcp", async setup(ctx) { ... } }
|
||||
```
|
||||
|
||||
**为什么两个都要**:OpenCode v2 对 default 导出有 schema 校验,缺失会直接报
|
||||
|
||||
```
|
||||
Plugin must export a default definition with an id and an effect or setup function.
|
||||
(Missing key at ["default"])
|
||||
```
|
||||
|
||||
(`opencode-btw` 就因为缺 default 在日志里反复报这条)。命名导出则是 legacy loader 的入口。
|
||||
两者返回同一套 hooks,无论加载器认哪个,`mcp_switch` 都能注册成功。
|
||||
|
||||
`tool` helper 通过**动态 import** 装载:某端没装依赖时只写错误日志,
|
||||
**不会让整个插件文件加载失败**。
|
||||
|
||||
---
|
||||
|
||||
## 四、排查
|
||||
|
||||
日志:`~/.config/opencode/ai-mcp.log`
|
||||
|
||||
| 日志内容 | 含义 |
|
||||
| --- | --- |
|
||||
| `module imported (pid=...)` | 文件**被 OpenCode import 了**(模块级留痕,最先出现) |
|
||||
| `ai-mcp server 插件已加载(命名导出/default/setup)` | 插件函数**被调用了**(session 初始化时触发) |
|
||||
| 什么都没有 | **没被加载** → 检查 `plugins` 数组是否注册、路径是否写对 |
|
||||
| `无法加载 @opencode-ai/plugin` | 依赖没装,去插件目录 `npm install` |
|
||||
|
||||
> 注意:插件在 **session 初始化时**才加载,不是进程启动时。
|
||||
> 只启动 `opencode serve` 不建 session,是看不到日志的。
|
||||
@@ -20,6 +20,19 @@ const CFG_TXT = join(LOG_DIR, "mcpcfg.txt")
|
||||
const LOG_FILE = join(LOG_DIR, "ai-mcp.log")
|
||||
const MODE_LABEL: Record<string, string> = { auto: "自动", manual: "手动", off: "关闭" }
|
||||
|
||||
// ---------------------------------------------------------------- 模块级留痕
|
||||
// 只要 OC 真的 import 了本文件,这里立刻写一行 ——
|
||||
// 用来区分「插件没被加载」和「加载了但插件函数没被调用」两种情况。
|
||||
try {
|
||||
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true })
|
||||
appendFileSync(
|
||||
LOG_FILE,
|
||||
`[${new Date().toISOString()}] module imported (pid=${process.pid}, node=${process.versions.node})\n`,
|
||||
)
|
||||
} catch {
|
||||
// 留痕失败绝不影响加载
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 本地日志兜底
|
||||
function fileLog(msg: string) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user