opencode-idea 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,179 +2,327 @@
2
2
 
3
3
  OpenCode 插件,面向 JetBrains 项目 (IntelliJ IDEA / PyCharm / WebStorm 等),做两件事:
4
4
 
5
- 1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务 (60 个工具:
6
- 检索、读、改、构建、重构、调试、数据库……)注册进 OpenCode。
7
- 2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入"项目内操作优先使用 IDEA MCP"的系统提示引导 + 能力映射;IDE 不可用时回退原生工具
8
- (正常回退,无需声明)。
9
-
10
- > **项目命令走 IDE 终端**:跑 `java` / `python` / `npm` 这类项目命令时,引导模型用 `idea_execute_terminal_command`
11
- > (`executeInShell: true`)而不是原生 shell —— 它在 IDE 集成终端里执行,**天然继承用户在 IDE 里配好的 JDK / Node / Python 版本**
12
- > (含 fnm / SDKMAN 这类靠 shell init 切版本的场景)。详见「项目命令执行」。
13
-
14
- 非 JetBrains 项目完全不受影响。 **手动接入**:插件启动不探测、不连接 IDE,只有跑 `/open-in-idea` 才接入。跑过一次后,该**项目**
15
- 被「标记」(持久化在 `ctx.storage`,重启 OpenCode 后仍在)。之后**没有任何后台轮询**——恢复完全靠请求驱动:插件发现"该有 `idea_*`
16
- 工具却没有"(新实例)或"`idea_*` 调用失败"(掉线)时,**在下一条消息发出前静默接回**(用户无感)。
17
- `/close-in-idea` 注销 IDE MCP + 删除标记。没跑过 `/open-in-idea` 的项目保持全手动、零自主动作。
5
+ 1. **接入 IDE MCP** —— 把项目在**它自己的 IDE** 里打开,并把这些 IDE 的 MCP 工具注册进 OpenCode。一个 OpenCode 会话可以
6
+ 同时接入**多个项目**:root 本身,以及 root 下任意层级的**子项目**;子项目可以用与 root **不同**的 IDE 打开
7
+ (root 在 IDEA、`modelserver` 在 PyCharm 是常态)。每个项目注册成独立的 MCP server,**工具名前缀就是它**:
8
+ root 是 `ide_*`,子项目是 `<目录名>_*`(例如 `modelserver_search_symbol`)。
9
+ 2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入**逐项目**的引导:在哪个项目里工作,就用哪个项目前缀的工具;IDE 不可用时
10
+ 回退原生工具(正常回退,无需声明)。
11
+
12
+ > **需要项目环境的命令走 Run Configuration**:跑 `java` / `python` / `npm` 这类依赖项目配好的 JDK / Node / Python 版本的
13
+ > 命令时,引导模型用 `ide_execute_run_configuration`(配置**由用户**先在 IDE 里建好),而不是原生 shell;不需要项目
14
+ > 环境的命令照常走原生 shell。**IDE 内置终端 (`ide_execute_terminal_command`) 已被隐藏** —— 实测它并不继承项目环境,
15
+ > 对原生 shell 没有任何优势。详见「项目命令执行」。
16
+
17
+ 非 JetBrains 项目完全不受影响。**手动接入**:插件启动不探测、不连接任何 IDE。三个命令管全部:
18
+
19
+ | 命令 | 作用 |
20
+ | ------------------------------- | ------------------------------------------------------------------------------- |
21
+ | `/open-in-idea [ide]` | 接入 root 及其全部已登记子项目;第一次**没有配置**时由 AI 问用户 root 用哪个 IDE |
22
+ | `/add-sub-module-in-ide <路径>` | 登记一个子项目(相对 root 的路径);随后由 AI 问用户它用哪个 IDE |
23
+ | `/delete-in-ide` | 删掉**整个**配置(root + 全部子项目)并注销全部 IDE MCP |
24
+
25
+ 配置持久化在 `ctx.storage`(`project/<root 路径>`),重启 OpenCode 后仍在。之后**没有任何后台轮询**——恢复完全靠请求驱动:插件发现"该有
26
+ IDE 工具却没有"(新实例)或"IDE 工具调用失败"(掉线)时,**在下一条消息发出前静默接回**(用户无感)。没跑过 `/open-in-idea` 的 root
27
+ 保持全手动、零自主动作。
18
28
 
19
29
  > **为什么改成请求驱动**:OpenCode 把一个 Location(项目)的全部服务缓存在 `LayerMap` 里,空闲 **60 分钟**
20
- > (`idleTimeToLive`)后整体驱逐——插件实例连同 `idea` MCP 注册一起消失。这一点两种传输都躲不过,所以与其后台探活
30
+ > (`idleTimeToLive`)后整体驱逐——插件实例连同全部 IDE MCP 注册一起消失。这一点两种传输都躲不过,所以与其后台探活
21
31
  > (永远晚一步、还刷 IDE 日志),不如在请求前发现并把连接接回来。
22
32
 
23
33
  ## 工作原理
24
34
 
25
35
  ```text
26
- setup(启动, 每项目一次):
27
- a. 读项目标记(ctx.storage: project/<项目路径>; OpenCode 重启后仍生效)
28
- b. 注册 /open-in-idea 与 /close-in-idea 命令 + session context 钩子 + tool execute.after 钩子
29
- 不探测、不注册 MCP
36
+ setup(启动, 每 root 一次):
37
+ a. 读配置(ctx.storage: project/<root 路径>; OpenCode 重启后仍生效)
38
+ —— root 用哪个 IDE,登记了哪些子项目、各自用哪个 IDE
39
+ b. 注册 3 个命令 + session context/prompt 钩子 + tool 钩子 + configure_ide_project 工具
40
+ 不探测、不注册任何 MCP
30
41
 
31
42
  /open-in-idea(手动触发, 阻塞到就绪):
32
- 1. 写项目标记 = true + 同步内存缓存;注册 `idea-run-config` skill
33
- 2. 探测本地 JetBrains IDE MCP 服务(/stream)
34
- - 已就绪 → 再问 IDE「当前项目是否已打开」(`get_project_modules`,带项目路径)
35
- · 已打开 → 不重复 open;未打开 → open -a 打开
36
- - 未就绪(冷启动)→ open -a 拉起,再轮询等到就绪(上限 60s)
37
- 4. **先注销旧的 idea MCP,再重新注册**(`type: "remote"` 指向 `http://127.0.0.1:<port>/stream`,项目路径走请求头)
38
- —— 每次执行都重建:OpenCode 的 reconcile 会跳过配置未变的 re-`set`,不先拆就换不掉一个已经死掉的注册
39
- 5. 等 `idea_*` 工具进入注册表;读 IDE 终端环境更新 shell 环境;启用引导
40
- 6. 发结果通知(用户看到干净气泡,模型只回"收到")
41
-
42
- /close-in-idea(手动触发):
43
- 1. 删项目标记 + 同步内存缓存 + 注销 IDE MCP + 清空注入的环境
43
+ 1. 还没有配置时:
44
+ - 带了 ide 参数 → 直接写入 root 的 IDE(直通逃生口,不经过模型)
45
+ - 没带参数 → 发给模型一条**任务**:请它问用户 root 用 IDEA 还是 PyCharm,拿到答复后调用
46
+ configure_ide_project 回写;本次到此为止。**只有第一次要问**
47
+ 2. 有配置时,对**每个**已登记项目:
48
+ - 在**该项目自己 IDE 的固定端口**上问「你认得这个项目吗」(`get_project_modules`,带项目路径)
49
+ · 认得 → 就用那个端口(这就是"哪个 IDE 打开着它"的判定)
50
+ · 不认得 → `open -a <该项目自己的 IDE> <项目目录>`,再轮询等到认得(上限 60s)
51
+ 3. **先注销全部旧注册,再按项目各注册一个 MCP server**
52
+ (server 名 = 工具前缀:root 是 `ide`,子项目是目录名;`type: "remote"` 指向
53
+ `http://127.0.0.1:<port>/stream`,项目路径走请求头)
54
+ 4. 等 IDE 工具进入注册表;按每个项目实际提供的工具同步 skill;启用逐项目引导
55
+ 5. 发结果通知(用户看到干净气泡,模型只回"收到")
56
+
57
+ /add-sub-module-in-ide <路径>(手动触发):
58
+ 1. 没有配置 → 直接报错,提示先执行 /open-in-idea(不提问、不写任何东西)
59
+ 2. 有配置 → 校验路径:必须在 root 下、目录存在、目录名合法、前缀不与已登记项目重名、不占用保留名
60
+ 3. 通过 → 发给模型一条**任务**:请它问用户这个子项目用哪个 IDE,答复后调用
61
+ configure_ide_project 回写。(带多个参数 = 报错,不猜)
62
+
63
+ /delete-in-ide(手动触发):
64
+ 1. 删除**整个**配置(root + 全部子项目) + 注销全部 IDE MCP + 清空注入的环境
44
65
  2. 发结果通知
45
66
 
46
- 工具失败触发(仅在被标记的项目里;需先 /open-in-idea 过一次):
47
- 1. `idea_*` 工具调用失败 → tool execute.after 钩子**分类**:
67
+ 工具失败触发(仅在有配置的 root 里):
68
+ 1. IDE 工具调用失败 → tool execute.after 钩子**分类**:
48
69
  - IDE MCP 不可用(掉了)→ 重跑接入流程
49
- - IDE 在、但项目没打开("Unable to determine the target project" / "doesn't correspond to any open project")→ 让 IDEA 重新打开该项目
70
+ - IDE 在、但项目没打开("Unable to determine the target project" / "doesn't correspond to any open project")
71
+ → 让**这个工具所属项目**的 IDE 重新打开它(不会去动别的项目)
50
72
  - 普通业务错误(File not found 等)→ 不动作
51
73
  2. 触发成功 → 把该错误改写为"已重连 / 已重新打开,请重试",模型同一回合重试;失败 → 保持原错误
52
74
 
53
75
  请求驱动的恢复(没有后台轮询;每个 Location(项目)各一份插件实例):
54
- 1. `prompt` 钩子(消息进入时,OpenCode 会 await 它)——「已标记 + 本实例没有任何 `idea` 注册」→
55
- 探测端口,**只注册、绝不 `open -a`**;命中就重建,于是**这条消息本身**就带上了 `idea_*` 工具
56
- 2. `context` 钩子——某条请求"曾经有 `idea_*`、现在没有" → 立刻锁存并在后台重建(不等下一条消息)
57
- 3. `execute.after` 钩子——`idea_*` 调用以"不可用"告终(`MCP server "idea" is not available` /
76
+ 1. `prompt` 钩子(消息进入时,OpenCode 会 await 它)——「已配置 + 本实例没有任何 MCP 注册」→
77
+ 探测端口,**只注册、绝不 `open -a`**;命中就重建,于是**这条消息本身**就带上了 IDE 工具
78
+ 2. `context` 钩子——某条请求"曾经有 IDE 工具、现在没有" → 立刻锁存并在后台重建(不等下一条消息)
79
+ 3. `execute.after` 钩子——IDE 工具调用以"不可用"告终(`MCP server "<名>" is not available` /
58
80
  `Error POSTing to endpoint`)→ 重跑接入流程,并把错误改写成"已重连,请重试";
59
81
  普通业务错误(文件不存在等)不动作
60
82
  ※ 三者都只在**确实有事发生**时动;没人用这个项目时,插件完全静止
61
83
  ```
62
84
 
63
- ## 在 IDE 中打开项目 (`/open-in-idea`)
85
+ ## 接入 (`/open-in-idea` 与 `/add-sub-module-in-ide`)
86
+
87
+ ### `/open-in-idea [ide]`
88
+
89
+ 输入 `/open-in-idea`,把 root 及其**全部已登记子项目**接入。每个项目都用**它自己配置的 IDE** 打开:
90
+
91
+ 1. **第一次(还没有配置)** —— 插件不猜:
92
+ - 不带参数 → 插件先按**安装目录**探一次本机装了哪些 IDE(macOS 是 `/Applications` 与 `~/Applications`,
93
+ Windows / Linux 按各自的常见位置),把结论写进任务文本,再让模型问用户。**不让模型自己去翻进程** ——
94
+ `ps aux | grep` 只看得见**正在运行**的 IDE,装了没开就漏(实测踩过:PyCharm 明明装着,却被报成"未发现"),
95
+ 而且慢。**只有这一次**需要用户回答;
96
+ - 带参数(`/open-in-idea pycharm`)→ 直通写入,不经过模型(模型不配合时的逃生口)。
64
97
 
65
- 在会话里输入 `/open-in-idea`:
98
+ 探测结果只用来"帮用户做选择",不决定"哪个 IDE 正开着这个项目" —— 后者始终由用户回答。探不到时
99
+ (非 mac、或路径未知)那句提示干脆不出现,照常问用户。
66
100
 
67
- > **非 macOS**:第 1、3 步的自动打开被跳过(没有对应的启动命令),请**先手动打开 IDE 和当前项目**,再执行本命令 ——
68
- > 端口在监听就会正常接入,并同样等到 `idea_*` 工具就绪才返回。
101
+ 2. **有配置后** —— 对每个已登记项目,在**它自己 IDE 的固定端口**上问「你认得这个项目吗」(`get_project_modules`,带项目路径 header):
102
+ - 认得 → 用那个端口。这同时就是"这个项目被哪个 IDE 打开着"的判定(IDEA 与 PyCharm 是两个进程、两个端口);
103
+ - 不认得 → `open -a "<该项目自己的 IDE>" <项目目录>`(**复用已运行实例**),再轮询等到认得(冷启动数十秒,上限 60s)。
104
+ 3. 就绪后 **先注销全部旧注册 → 再按项目各注册一个 MCP server**(server 名 = 工具前缀),然后重读 IDE 环境、启用逐项目引导,
105
+ 并**等到 IDE 工具进入注册表**才返回。
69
106
 
70
- 1. **让运行中的 IDEA 实例打开/激活当前项目**:`open -a "IntelliJ IDEA" <项目目录>` —— **复用已运行的实例**(不会重复开新实例),
71
- 项目已打开时只把它切到前台。**无论 MCP 是否已就绪都会执行这一步**:端点能应答只说明 IDEA 在运行(可能开的是**别的**项目),
72
- 不代表当前项目已打开;
73
- 2. 再探测 `/stream`;已就绪 → 直接进入下一步;
74
- 3. 未就绪 → 说明 IDEA 是冷启动,继续用 `open -a` 拉起并**轮询等到 MCP 就绪**(冷启动约数十秒,上限 60s);
75
- 4. 等不到(例如 IDE 里 MCP 服务未开启)→ 提示在 IDEA 的 `Settings → MCP Server` 开启 MCP 服务并启用 **Brave Mode**,用户设置好后
76
- 再次执行 `/open-in-idea`;
77
- 5. 就绪后 **先注销旧的 `idea` MCP → 再重新注册**(每次执行都重建:配置未变的 re-`set` 会被 reconcile 跳过),然后重读
78
- IDE 环境、启用系统引导,并**等到 `idea_*` 工具进入注册表**才返回。
107
+ > **非 macOS**:第 2 步的自动打开被跳过(没有对应的启动命令),请**先手动打开 IDE 和项目**,再执行本命令 ——
108
+ > 端口在监听、项目被认得就会正常接入。
79
109
 
80
110
  > **通知不触发 AI 干活**:默认 `feedback: "message"` 把结果作为一条 **标注清楚的通知**发到会话里 —— 用户看到干净的通知气泡
81
111
  > (经 `metadata.displayText`),模型只看到"这是通知,请只回复『收到』"的指令,因此不会执行任何操作、不会调用工具。
82
- > `feedback: false` 则完全静默。
112
+ > `feedback: false` 则完全静默。**唯一例外**是上面"问用户用哪个 IDE"那条:它是**任务不是通知**,模型必须问完并调用工具才算完成。
83
113
 
84
- **可重入 / 启动保护**:IDEA 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并
85
- fast-fail,不会在后台轮询;重复触发共享同一次打开请求,按目录维护 `lastSpawnAt` / `attempts`,避免重复 spawn。开启 MCP +
86
- Brave Mode 后再次执行 `/open-in-idea` 即可接上。
114
+ **可重入 / 启动保护**:IDE 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并
115
+ fast-fail,不会在后台轮询;重复触发共享同一次打开请求,**每个项目各自维护** `lastSpawnAt` / `attempts`,避免重复 spawn。
87
116
 
88
- **热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放,`activePort` 归零。这是手动模式的预期行为 ——
89
- **改完插件后重跑一次 `/open-in-idea`** 即可恢复。
117
+ ### `/add-sub-module-in-ide <路径>`
90
118
 
91
- ## 关闭接入 (`/close-in-idea`)
119
+ 把一个子项目登记进配置。参数是**相对 root 的路径**(例如 `modelserver`),而且**只接受一个参数**:
92
120
 
93
- 输入 `/close-in-idea`:**注销当前项目的 IDE MCP** + 删除项目标记(持久化 + 内存缓存一并清),并清空注入的环境变量。
94
- 之后 `idea_*` 工具不再可用,也不会再因失败自动接入。想重新接入,再次执行 `/open-in-idea`。
121
+ 1. 没有配置 → 直接报错,提示先执行 `/open-in-idea`(不提问、不写任何东西);
122
+ 2. 校验:必须位于 root 之下、目录真实存在、**目录名合法**、前缀不与已登记项目重名、不占用保留名 `ide`;
123
+ 3. 通过后发给模型一条**任务**:请它问用户这个子项目用哪个 IDE,答复后调用 `configure_ide_project` 回写。
95
124
 
96
- ## IDE 工具优先 (直接调用引导)
125
+ **目录名的硬规则**(它就是工具前缀,所以不给猜的余地):必须匹配 `^[a-z0-9][a-z0-9_-]*$` —— 小写字母或数字开头,
126
+ 只含小写字母、数字、`-`、`_`。不合规(大写、点号、空格、中文……)或与已登记项目重名,插件**一律拒绝**并说明原因,
127
+ 由用户改目录名。插件**绝不**自动改名或自动消歧。
128
+
129
+ ### 让模型回写:`configure_ide_project`
130
+
131
+ 插件注册的一个**自己的**工具,是"用户选的 IDE"回到配置里的**唯一通道**(插件不能弹选择框,也不能挂起等回答):
132
+
133
+ ```text
134
+ configure_ide_project({ ide: "<idea|pycharm>" }) // 配置 root
135
+ configure_ide_project({ path: "modelserver", ide: "pycharm" }) // 配置一个子项目
136
+ ```
97
137
 
98
- IDE MCP 可用时,插件通过 **系统提示注入**提高模型主动使用 IDEA MCP 的概率;不修改任何工具 (原生或 IDEA)
138
+ 它常驻在工具列表里(一个工具的 schema 只有几百 token,不值得为它做一套注册/撤销的状态机);
139
+ 该不该被调用由工具描述约束 —— "仅当用户明确回答了用哪个 IDE 之后调用"。
140
+
141
+ ## 删除接入 (`/delete-in-ide`)
142
+
143
+ 输入 `/delete-in-ide`:**注销全部 IDE MCP** + 删除**整个**配置(root 与全部子项目,持久化 + 内存缓存一并清),
144
+ 并清空注入的环境变量。之后 IDE 工具不再可用,也不会再因失败自动接入。想重新接入,再次执行 `/open-in-idea`。
145
+
146
+ **热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放。**改完插件后重跑一次 `/open-in-idea`** 即可恢复。
147
+
148
+ ## 按能力选工具 (直接调用引导)
149
+
150
+ IDE MCP 可用时,插件通过 **系统提示注入**告诉模型**哪些事该走 IDE**;不修改任何工具 (原生或 IDEA)
99
151
  的定义、描述或可用性:
100
152
 
101
- 1. **系统提示注入**:通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段「项目内操作优先使用 IDEA
102
- MCP」的直接调用引导 + 能力映射表 (`src/ide-guidance.js`),含工具名 (`idea_<name>`)与关键参数名:
103
- - 检索 → `idea_search_text` / `idea_search_regex` / `idea_search_file`(参数 `q`,可带 `paths` 收窄)
104
- - 读文件 → `idea_read_file`(`file_path` / `offset` / `limit`);目录 → `idea_list_directory_tree`
105
- - 改文件 → `idea_apply_patch`(`input`);新建 → `idea_create_new_file`(`pathInProject`)
106
- - 校验 → `idea_lint_files` / `idea_get_file_problems`
107
- - 重构/格式化 → `idea_rename_refactoring` / `idea_reformat_file`
108
- - 数据库 → `idea_list_database_connections` / `idea_execute_sql_query` / `idea_preview_table_data` 等;没有数据源时先建议用户在
109
- IDEA 里配置
110
- - 边界:只约束 **代码与文件操作**;未暴露的领域 (版本控制、shell/终端等) 工具表里没有 `idea_*` 选项,模型自然走原生
153
+ 1. **系统提示注入**:通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段引导 + 能力映射表。**文本本身在
154
+ `src/ide-guidance.template.md`**——它是一份要给人 review 的提示词文档,条件直接用 `{{#if …}}` 写在那几行外面,所以打开模板
155
+ 就能看到最终渲染成什么样;`src/template.js` 是渲染器(支持 `{{var}}` / `{{#if}}` / `{{else}}`,**不做 HTML 转义**——
156
+ 输出是 Markdown,`<jar>!/<entry>` 和反引号必须原样过)。含工具名 (`<项目前缀>_<name>`;root 是 `ide_*`,
157
+ 子项目是 `<目录名>_*`)与关键参数名:
158
+ - **只有 IDE 才有的能力才写进去** —— 符号级检索、项目模型、检查与构建、重构与格式化、运行配置:
159
+ - 符号 → `ide_search_symbol` / `ide_get_symbol_info` / `ide_analyze_calls`
160
+ - 项目与编辑器 → `ide_get_project_modules` / `ide_get_project_dependencies` / `ide_list_directory_tree` /
161
+ `ide_get_all_open_file_paths` / `ide_open_file_in_editor`
162
+ - 重构/格式化 → `ide_rename_refactoring` / `ide_reformat_file`
163
+ - 校验 → `ide_lint_files` / `ide_get_file_problems` / `ide_build_project`
164
+ - 运行配置 → `ide_get_run_configurations` / `ide_execute_run_configuration`(+ 配套插件的启停工具)
165
+ - **读、写、找文本、找文件用原生工具**(`read` / `write` / `edit` / `grep` / `glob`)—— 这几件事走 IDE 只是多一跳,
166
+ 所以映射表里**不出现** `ide_search_text` / `ide_search_regex` / `ide_search_file` / `ide_apply_patch` /
167
+ `ide_create_new_file`,也不再禁止它们的原生对应物。
168
+ - **唯一例外是 `ide_read_file`**:它是读**包内文件**的唯一手段(`<jar>!/<entry>` 的依赖 jar 内文件、`jrt://` 的
169
+ JDK 镜像),引导只在这个用途上点名它,并写明普通文件一律用 `read`。
111
170
 
112
171
  不会把整份工具目录重复塞进 system prompt,也不要求模型先写编排代码。IDEA MCP 工具自身的
113
172
  description/schema 仍是参数使用的权威来源;工具返回业务错误时先修正参数,不要误判成工具不可用。MCP 不可用时立即
114
173
  提示用户执行 `/open-in-idea`,不重复探测或等待。
115
174
 
175
+ **总则里最靠前的两条是 Code Mode 规则**,而且第一条是**通用**的,不是 `ide_*` 特例:`工具列表里已经直接列出的原生工具,
176
+ 直接调用,不需要也不应该用 Code Mode 的 `search` 去找`;第二条才落到 `ide_*`(它就是原生工具,且不在 Code Mode 目录里)。
177
+ 放在所有路由规则之前,因为它们是前提 —— 模型拿不到工具,后面写什么都没用。理由是实测过的失效:工具明明在原生工具
178
+ 列表里,模型却进 Code Mode 跑了一次工具搜索(Code Mode 自己的指令「用 `search` 找工具」读起来像是访问任何东西的通用
179
+ 姿势,而原文没有任何一句说明这些工具在哪),白绕一圈之后还可能得出"工具不可用"的错误结论。同一条约束也写在
180
+ 「工具已消失」的恢复提示里,避免掉线时又去 Code Mode 里找。
181
+
116
182
  引导只在 **IDE MCP 可用**时生效;MCP 不可用时仅保留 fast-fail 恢复提示。`injectGuidance: false` 可关闭系统提示注入。
117
183
 
118
- ## Run Configuration 管理 (skill)
184
+ **条件注入**:映射表里只有内置 MCP Server 自带的那批工具是「永远写死」的。如果 IDE 里另外装了配套的
185
+ [`idea-mcp-enhance`](https://github.com/syllr/idea-mcp-enhance) 插件,它提供的 run configuration 启停工具也会被加进
186
+ 下面的「运行配置」段。判据是**这次请求的工具快照里真的有这些工具** —— 没装的 IDE 上这段一个字都不会出现,模型不会去调
187
+ 一个不存在的工具。
119
188
 
120
- IDE MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_configuration`)run configuration, **没有增删改**
121
- 。插件在**被标记的项目**里注册一个 `idea-run-config` skill 来补上这块:
189
+ ## 工具参数 (schema 收敛 + `execute.before`)
122
190
 
123
- - 只操作 **项目级** `.run/<name>.run.xml`(可提交 git、团队共享), **不碰** `.idea/workspace.xml`;
124
- - 不内置任何按类型的模板 —— 先找项目里的 **真实样本**(`**/*.run.xml`,或让用户在 IDEA 里勾 "Store as project file"
125
- 建一个)当参照,再写;
126
- - 写完必须用 `idea_get_run_configurations` **校验它能被列出**、并用 `idea_execute_run_configuration` **实跑确认**;
127
- - 改配置用 `idea_apply_patch` **定向改 option**,不整文件重写。
191
+ 有些 IDEA 工具**不传某个参数就会回答一个你没问的问题**。这种参数由本插件完全掌管,所以要**两半一起做** —— 只做一半都是错的:
128
192
 
129
- **skill 的生命周期跟随项目标记**:`/open-in-idea` 打标记时注册、`/close-in-idea` 清标记时注销;曾被标记过的项目在
130
- `setup` 时(读回标记)就会注册。未标记的项目不受影响。文档放在 `skills/<id>/SKILL.md`(每个 skill 一个目录,`name` /
131
- `description` 走 YAML frontmatter),由 `src/skill/loader.js` 扫描加载 —— **加新 skill 只需加一个目录,不用改代码**。
193
+ | 一半 | 做法 | 不做会怎样 |
194
+ | ------------------------------ | ------------------------------------ | ------------------------------------------ |
195
+ | 从模型的工具 schema 里**删掉** | `ctx.tool.transform` 改 input schema | 模型会设它、会据此推理,而那个值被无声丢弃 |
196
+ | 每次调用**设成固定值** | `ctx.tool.hook("execute.before")` | 模型不传时拿到的是那个可能出错的默认值本身 |
132
197
 
133
- ## Standalone 模式纪律 (skill)
198
+ 两半由 `src/mcp/idea-tool-defaults.js` 的**同一张表** `FORCED_ARGUMENTS` 驱动,所以不会漂移。目前一条:
199
+
200
+ | 工具 | 参数 | 值 | 为什么 |
201
+ | ------------------- | --------- | ------ | -------------------------------------------------------------------------------------------------------------- |
202
+ | `ide_build_project` | `rebuild` | `true` | 增量构建比对的是 **IDE 自己那份文件新鲜度**;IDE 外部改的代码可能被判成"没有脏文件",构建于是变成 no-op 并报成功 |
203
+
204
+ - **强制**而不是"缺省才补":`rebuild: false` 请求的正是那个可能出错的答案,而且调用方从结果里分辨不出来;`rebuild: true`
205
+ 唯一的代价是时间。
206
+ - 依据是实测(IntelliJ 2026.2.3):`build_project` 落到
207
+ `ProjectTaskManager.createAllModulesBuildTask(project, isIncrementalBuild = !rebuild)`;用 `rebuild: true` 跑的 6 个场景
208
+ (改已有文件 / 新增文件、任意包、在编辑器里打开与否)**全部**识别到外部改动。
209
+ - 调用方自己也传了非空 `filesToRebuild` 时,IDE 走的是另一条分支、根本不读 `rebuild`,所以设不设都无副作用。
210
+ - **引导文本里一个字都不提 `rebuild`** —— schema 都不暴露了,再写进提示词等于把刚拿掉的旋钮又放回去。
211
+ - 只对**点名的工具**生效,其余工具的参数一字不改(测试里专门有一条:另一个恰好也有 `rebuild` 属性的工具不受影响)。
134
212
 
135
- 本机直跑应用的**项目级纪律**由 `idea-standalone` skill 固化:它为项目生成/维护一份 rule,落在项目根
136
- **`.opencode/rules/idea-standalone.md`**,四段结构固定 ——
213
+ 安全性照着 OpenCode 源码确认过:一次工具调用**先**按该请求发布出去的 schema 校验,**然后**才进 `execute`,而
214
+ `execute.before` 在真正的工具逻辑之前触发。所以"schema 里没有、钩子里补上"这个组合不会撞校验。
137
215
 
138
- 1. **什么是 standalone 模式**:**standalone = 通过 IDEA 启动**(每个应用的入口是 IDEA run configuration),应用在本机进程
139
- 直跑(不容器化、不部署),只启动其中一部分是常态;启停一律由用户在 IDEA 里手动完成,AI 不得代为启停(含经
140
- `idea_execute_run_configuration`),判断需要重启时先取得用户明确授权;
141
- 2. **启动哪些应用**:一行一个应用,启动路径**只能是 IDEA run configuration**(配置名必须能在 `idea_get_run_configurations`
142
- 里查到;缺配置先走 `idea-run-config` skill 建 `.run/*.run.xml`),端口与被依赖的先后顺序一并写明;
216
+ **这是对早先「绝不改工具定义」那条规则的一次有意例外**(见 `141b1bd`),且刻意收得很窄:这里**只做减法,不发明、不解释**。
217
+ IDE 自己的 `build_project` 工具描述本来就没提这个参数(只有属性级描述提),所以删掉属性之后 schema 依然自洽、也依然完全是
218
+ IDE 的。schema 变换**绝不能**用来加建议 —— 那是引导文本的职责。
219
+
220
+ 两半都**不依赖项目配置**:它们修的是工具调用本身,与"这个项目是否接入 IDE"无关。
221
+
222
+ ## Run Configuration 全套 (配套插件 `idea-mcp-enhance`)
223
+
224
+ IDE 内置的 MCP Server 只能 **启动已有**的 run configuration(`execute_run_configuration`),**不能新建、不能停止、不能重启、
225
+ 也查不到运行状态**。配套的 IDEA 插件 [`idea-mcp-enhance`](https://github.com/syllr/idea-mcp-enhance)(IDEA 2026.2+,另装)
226
+ 补齐这四件事 —— 正好是「建 → 看 → 停 → 重启」一条链:
227
+
228
+ | 工具 (OpenCode 里叫 `ide_*`) | 作用 |
229
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
230
+ | `ide_create_shell_run_configuration` | 新建**项目级** Shell 配置(写 `.run/<name>.run.xml`);只支持 Shell 类型。**重操作 —— 只给固定流程建,且必须先取得用户同意** |
231
+ | `ide_list_running_configurations` | **当前真正在跑**的配置:几个实例 / 各自 pid / 退出码。`>1` 个实例就是端口漂移的信号 |
232
+ | `ide_stop_run_configuration` | 停掉该配置的**全部**实例并等它们退出;没在跑也返回成功(幂等) |
233
+ | `ide_rerun_run_configuration` | 等同 IDE 的 **Rerun** 按钮:在跑就重启、没在跑就启动,读 `started` 判断是否真起来了 |
234
+
235
+ opencode-idea 对这一组工具做三件事:
236
+
237
+ 1. **透传**:IDE MCP server 以 `codemode: false` 注册,工具目录整体转发,所以这四个工具自动以 `ide_*` 出现在模型的
238
+ 工具表里 —— 插件侧不需要单独声明(也因此**装不装这个 IDEA 插件,插件侧的代码都不用改**);
239
+ 2. **条件引导**:只有这次请求的工具快照里真的有它们,才在「运行配置(配套插件 … 提供,装了才有)」**自己那一节**里逐个点名 ——
240
+ 和 IDE 自带的 `### 运行配置` 分开:两个来源,两节,读者要能分清"IDE 给我的"和"我额外装的"。没装插件时**整节连同标题一起消失**。
241
+ 两个地方会跟着变:
242
+ - 「运行配置」段写明**改完代码要重启无热重载的进程就用 `ide_rerun_run_configuration`,不要用 `pkill` / `kill` / shell**
243
+ —— 这正是它要解决的问题:外部杀掉的进程 IDE 并不知道,Run 窗口仍显示运行中,下次启动就会撞端口;
244
+ - 「命令执行」段把**新建 run configuration 定性成重操作**:一个项目通常只建几个(固定要跑的构建 / 装依赖 / dev server),
245
+ **不是随手一条命令就建一个**,而且要**先告诉用户建什么、为什么,取得同意再动手**。只有最后"用哪条路建"才按工具是否
246
+ 分成两条**无条件 + 有条件**:「配置一律不由你写」永远在;只有「Shell 例外,用 `ide_create_shell_run_configuration`」那一行
247
+ 按工具是否存在分流,没有就换成「缺配置请用户在 IDEA 里建」。**没装插件时绝不会提到那个工具。**
248
+ 3. **工具名清单**写在 `src/mcp/enhance.js`(与原生 IDE 的 `src/mcp/idea.js` 分开放:原生走原生,插件的走插件的),
249
+ 与该仓库的 `RunLifecycleSchemas.kt` 保持同步。工具按**存在的子集**逐个点名(而不是「有就全写」),这样旧版本的配套
250
+ 插件少给一两个工具时,引导也不会提到它们。
251
+
252
+ > 没装 `idea-mcp-enhance` 的 IDE 一切照旧:工具不存在,引导也不提,模型继续按原来的方式工作。
253
+
254
+ ## Run Configuration:能建就用工具,不能建就交给用户
255
+
256
+ 内置 MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_configuration`)run configuration;新建靠配套插件的
257
+ `ide_create_shell_run_configuration`(只有 Shell 类型,见上面的「Run Configuration 全套」);**修改和删除永远没有工具**。
258
+
259
+ 所以插件的立场是:
260
+
261
+ - **配置一律不由 AI 动手** —— 不手写 `.run/*.run.xml`,也不手搓启动命令。这条无条件;
262
+ - **Shell 类型**且装了配套插件 → 用 `ide_create_shell_run_configuration`(新建是重操作,**必须先取得用户同意**);
263
+ - **其他类型,或没装插件** → **请用户在 IDEA 里建**,AI 只负责说明要建什么、为什么。
264
+
265
+ > **曾经有一个 `idea-run-config` skill 教模型手写 `.run/*.run.xml`,已经删掉。** 理由:手写那条路很脆 —— `type` 写错 IDE 会
266
+ > **静默忽略整个文件、日志里也不报**,模型的唯一判据只有"`ide_get_run_configurations` 里看不看得到";而它想补的"新建"能力现在
267
+ > 由配套插件提供。要配置但没有工具,就**交给用户**,比让模型猜 option 名可靠。
268
+
269
+ **skill 的生命周期跟随两道门,缺一不可**:
270
+
271
+ 1. **项目配置** —— `/open-in-idea` 写入配置时注册、`/delete-in-ide` 删配置时注销;以前配置过的 root 在 `setup` 时(读回配置)
272
+ 也会走这一步。未配置的 root 不受影响。
273
+ 2. **IDE 真的在提供这个 skill 依赖的工具** —— skill 在自己的 frontmatter 里用 `requires:` 声明它绕着哪个工具写,而且写的是**后缀**
274
+ (例如 `idea-standalone` 声明 `rerun_run_configuration`,真实工具名是 `<项目前缀>rerun_run_configuration`)。**没这个工具就不注册这个 skill**:
275
+ 一个教模型去调"它根本看不到的工具"的 skill,不是知识,是一次注定的失败调用。工具的可见性是双向跟随的 —— `setup` 时还没
276
+ 连过 IDE、目录未知,所以那时**不注册**;等 IDE 应答(`/open-in-idea` 或重连)后目录才算已知,该出现的才出现;反过来,某个
277
+ 请求里工具没了,skill 也跟着撤(不需要等重连)。
278
+ `requires` 必须写 **OpenCode 侧的名字**(IDE 工具带 `ide_` 服务器前缀),因为门就是拿它跟 `ctx.tool.list()` 比;
279
+ 写成 IDE 的原始名(`rerun_run_configuration`)会让这个 skill 在任何机器上都不出现,而且没有任何报错。
280
+
281
+ 文档放在 `skills/<id>/SKILL.md`(每个 skill 一个目录,`name` / `description` / `requires` 走 YAML frontmatter,`requires` 由
282
+ loader 消费、**不会进入注册给 OpenCode 的定义**),由 `src/skill/loader.js` 扫描加载 —— **加新 skill 只需加一个目录,不用改代码**。
283
+
284
+ ## Standalone 模式纪律 (skill)
285
+
286
+ 本机直跑应用的**项目级纪律**由 `idea-standalone` skill 固化:它把这份纪律写进**项目根 `AGENTS.md`** 里由
287
+ `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 包裹的**标记块**(根 `AGENTS.md` 本来就是 AI 在本项目
288
+ 工作时必读的文件,所以不需要另建规则文件、也不需要 always-apply 开关;标记块是"以后改没改过"的判据)。骨架与各段写作规范在
289
+ `skills/idea-standalone/assets/AGENTS.template.md`,**SKILL.md 只写流程、指向模板**,不重复骨架。三段固定 ——
290
+
291
+ 1. **什么是 standalone 模式**:**standalone = 启动收口只有 IDEA run configuration**(每个应用的启动入口是 run configuration,不是手搓命令行;
292
+ 至于配置内部怎么起 —— 本机进程 / 容器 / 部署到远端 —— 由它自己决定,本段不预设),只启动其中一部分是常态;
293
+ **跑应用只用 `ide_rerun_run_configuration`**(在跑就重启、没在跑就启动)
294
+ —— 不必分辨启动还是重启,直接 rerun 就完事,常驻进程化 / 服务化的配置一律走这条;
295
+ **`ide_execute_run_configuration` 不用于这里**(它是给命令 / Job 类型的配置跑的,对常驻进程只会起重复实例);也不绕开 IDEA 用
296
+ `go run` / `pnpm dev` 这类裸命令或 `kill` / `pkill`;**重启不必先问** —— 改完代码要重启就直接 rerun,这条链是无人化的;
297
+ 2. **启动哪些应用**:一行一个应用,启动路径**只能是 IDEA run configuration**(配置名必须能在 `ide_get_run_configurations`
298
+ 里查到;**缺配置就交给用户**去 IDEA 里建),端口与被依赖的先后顺序一并写明;
143
299
  3. **热重载矩阵**:逐应用标"是 / 否" —— 带 HMR 的 dev server 改动后**不用**提示重启;`go run .` 这类无热重载的进程改动后
144
300
  **必须**显式点名「需要重启 <应用>」,不得在未提示的情况下宣称"改完可验证";
145
301
  4. **项目补充条款**:文件写入位置、测试纪律、访问归属、dev 模式特有限制等,按用户给的条款逐条续写、保留原意。
146
302
 
147
- 事实一律先查后写:配置名来自 `idea_get_run_configurations`,热重载列来自运行方式,端口读不到就问用户 —— 不猜。
148
-
149
- ## 项目命令执行 (IDE 终端)
303
+ 事实一律先查后写:配置名来自 `ide_get_run_configurations`,热重载列来自运行方式,端口读不到就问用户 —— 不猜。
304
+ **只动自己的标记块**:根 `AGENTS.md` 是共享文件(`project-scaffold` 的宪法、项目自己的内容都在里面),标记之外一字不改。
150
305
 
151
- 跑项目相关的命令时,模型使用 **`idea_execute_terminal_command`**,而不是 OpenCode 原生 shell。判据是**白名单**:命令用于本项目
152
- 自身的构建 / 测试 / 运行,且首个可执行文件命中下表,才走 IDE 终端;其余一律走原生 shell。
306
+ ## 项目命令执行 (Run Configuration)
153
307
 
154
- - **Node / 前端**(`package.json`):`node` / `npm` / `npx` / `pnpm` / `yarn` / `vite` / `webpack` / `esbuild` / `tsc` /
155
- `jest` / `vitest`。
156
- - **Java**(`pom.xml` / `build.gradle`):`java` / `javac` / `mvn` / `mvnw` / `gradle` / `gradlew`。
157
- - **Python**(`pyproject.toml` / `requirements.txt`):`python` / `python3` / `pip` / `pip3` / `uv` / `poetry` / `pytest`。
158
- - **Go**(`go.mod`):`go` / `gofmt` / `golangci-lint`。
159
- - 白名单未覆盖的语言暂走原生 shell,遇到后再补进上表。
308
+ 需要**项目环境**的命令(依赖用户在 IDE 里配好的 JDK / Node / Python 版本等),模型使用 **`ide_execute_run_configuration`**
309
+ 执行 —— 走 run configuration,不用原生 shell。
160
310
 
161
- 原因:
311
+ - 判据是:这条命令**依赖项目的运行时版本**。是 → run configuration;不是(看文件、git、临时脚本)→ 原生 shell。
312
+ - run configuration 会继承开发时具体配的那套运行时,或者干脆是你那个应用配好的运行时,所以这就是个应用。
313
+ - 配置必须**先在 IDEA 里存在**(项目级 `.run/*.run.xml`,**由用户建**)。没有就请用户建一个,再跑;
162
314
 
163
- - **`executeInShell: true`**(必开):命令在用户真实 shell(`zsh`/`bash`)里执行,**继承 IDE 集成终端的环境** —— 也就是用户在
164
- IDE 里配好的 JDK / Node / Python / Go / Maven / uv 等。原生 shell 未必能拿到同一套版本,尤其是 fnm / SDKMAN / pyenv 这类
165
- "选择写在 shell init 里"的版本管理器,靠几个环境变量复现不出来。
166
- - `timeout`(毫秒)按预期耗时给足;`maxLinesCount` 控制返回行数,超 2000 行会截断。
167
- - `reuseExistingTerminalWindow: true` 复用同一个 IDE 终端窗口,不刷屏。
168
- - 返回 `command_exit_code` + `command_output`;`cd` **不跨调用保留**,多步操作用 `&&` 串在一条命令里。
169
- - 需要 IDE 里开启 **Brave Mode**,否则每条命令都要用户确认一次。
315
+ > 为什么不再用 IDE 终端:`ide_execute_terminal_command` 之前被当成"自动继承 IDE 项目环境"的通道而保留暴露,但实测
316
+ > **它拿不到项目配好的 JDK / Node / Python 版本**,和原生 shell 没有区别。既然没有优势,就把它从模型可见的工具表里
317
+ > 隐藏掉(`HIDDEN_IDE_TOOL_SUFFIXES`),命令执行的路由改由 run configuration 承担。
170
318
 
171
- 引导文本由 `src/ide-guidance.js` 注入(`### 终端` 段),只在 IDE MCP 可用时生效。
319
+ 引导文本由 `src/ide-guidance.js` 注入(`### 命令执行` 段),只在 IDE MCP 可用时生效。
172
320
 
173
321
  ## 前置条件
174
322
 
175
323
  - JetBrains IDE 已开启 MCP 服务 (IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点
176
324
  `/stream`);若关闭,`/open-in-idea` 会立即提示如何重新开启。
177
- - 要读 IDE 终端环境,需要在 IDE 里开启 **Brave Mode**(否则 `execute_terminal_command` 会等确认);MCP 未就绪时通知会同时提醒开启它。
325
+ - IDE 侧需开启 **Brave Mode**(否则需要确认的 MCP 工具调用会停下来等用户确认);MCP 未就绪时通知会同时提醒开启它。
178
326
  - 插件注册给 OpenCode 的 MCP 配置(**只支持 Streamable HTTP**):
179
327
 
180
328
  ```jsonc
@@ -193,9 +341,11 @@ IDE MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_conf
193
341
  > HTTP 至少把"端点没了"变成一次普通的调用错误,插件能分类并自愈。OpenCode 2.0.19+ 也已无 SSE 客户端传输,当初选 stdio 的理由不再成立。
194
342
  >
195
343
  > **为什么不用 `/sse`**:OpenCode V2 的远程 MCP 客户端**只支持 Streamable HTTP**,没有旧版 SSE 传输。IDE 的 `/sse` 是旧版
196
- > SSE (POST 返回 405),V2 连不上。插件对 IDE 的**探测**仍走 `/stream`(一次 `initialize`),用来确认 IDE MCP 已就绪并拿到端口。
344
+ > SSE (POST 返回 405),V2 连不上。插件对 IDE 的**探测**也走 `/stream`(一次项目作用域的 `get_project_modules` 调用),
345
+ > 用来确认"这个 IDE 里打开着这个项目"。
197
346
 
198
- 插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342]`。
347
+ **端口是产品常量,不开放配置**:IDEA 的 MCP 在 **64342**,PyCharm 在 **64462**(见 `src/ide-projects.js` 的 `IDE_PORTS`)。
348
+ "哪个 IDE 在哪个端口"跨版本不变,所以插件不需要扫描候选端口 —— 探哪个端口,由项目的 IDE 唯一决定,用户不需要填任何东西。
199
349
 
200
350
  ## 安装
201
351
 
@@ -254,13 +404,12 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
254
404
  {
255
405
  "package": "opencode-idea",
256
406
  "options": {
257
- "ports": [64342], // IDE MCP 端口;默认 [64342]
258
- "injectGuidance": true, // IDE 可用时向系统提示注入「优先使用 IDEA MCP」引导
259
- "openInIde": "idea", // false 关闭;或任意 macOS 应用名(如 "IntelliJ IDEA")
260
- "launchCooldownMs": 120000, // 启动保护冷却
407
+ "injectGuidance": true, // IDE 可用时注入逐项目的「优先使用 IDE 工具」引导
408
+ "openInIde": true, // 是否允许自动拉起 IDE;false 只连已运行的端点
409
+ "launchCooldownMs": 120000, // 启动保护冷却(按项目各自计)
261
410
  "launchMaxAttempts": 5,
262
- "mcpProbeTimeoutMs": 1000, // 单个 MCP 端口的短探测超时
263
- "executionTimeoutMs": 600000, // 单次 idea_* 工具调用硬上限;默认 10 分钟(OpenCode 默认 12 小时)
411
+ "mcpProbeTimeoutMs": 1000, // 单次「这个 IDE 里打开着这个项目吗」探测的超时
412
+ "executionTimeoutMs": 600000, // 单次 IDE 工具调用硬上限;默认 10 分钟(OpenCode 默认 12 小时)
264
413
  "feedback": "message", // "message"(默认,会话通知,AI 只回"收到")| false(静默)
265
414
  },
266
415
  },
@@ -270,46 +419,46 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
270
419
 
271
420
  不传 options 时全部使用下表默认值。
272
421
 
273
- | 选项 | 类型 | 默认 | 说明 |
274
- | -------------------- | -------------------- | ----------- | ---------------------------------------------------------------------------------- |
275
- | `ports` | `number[]` | `[64342]` | 依次探测的 IDE MCP 端口 |
276
- | `injectGuidance` | `boolean` | `true` | IDE 可用时是否向系统提示注入「优先使用 IDEA MCP」引导 |
277
- | `openInIde` | `boolean \| string` | `"idea"` | 拉起哪个 IDE;`false` 关闭,`"idea"` 映射到 IntelliJ IDEA |
278
- | `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn |
279
- | `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试 |
280
- | `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
281
- | `executionTimeoutMs` | `number` | `600000` | 单次 `idea_*` 工具调用的硬上限(毫秒);防止端点卡住后无限等待(OpenCode 默认 12 小时) |
282
- | `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
422
+ | 选项 | 类型 | 默认 | 说明 |
423
+ | -------------------- | -------------------- | ----------- | ---------------------------------------------------------------------------------------------- |
424
+ | `injectGuidance` | `boolean` | `true` | IDE 可用时是否注入逐项目的「优先使用 IDE 工具」引导 |
425
+ | `openInIde` | `boolean` | `true` | 是否允许自动拉起 IDE;`false` 时仍会连接**已经运行**的端点。**用哪个 IDE 由每个项目的配置决定** |
426
+ | `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn(每个项目各算各的) |
427
+ | `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试(每个项目各算各的) |
428
+ | `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
429
+ | `executionTimeoutMs` | `number` | `600000` | 单次 IDE 工具调用的硬上限(毫秒);防止端点卡住后无限等待(OpenCode 默认 12 小时) |
430
+ | `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
283
431
 
284
432
  ## 行为
285
433
 
286
- - **被标记的项目**:启动读回标记后挂上 shell 环境钩子 (环境值待接入后填充)、注册 `idea-run-config` skill;IDE MCP 由
287
- `/open-in-idea` 注册。
288
- - **未标记的项目**:不改环境、不注册、不注入任何 IDEA 提示 (每请求零开销;setup 时只做一次项目标记读取)。
289
- - **手动接入 + 就绪即返回**:`/open-in-idea` 会 `open -a` 拉起 IDEA 并**等 IDE MCP 就绪**(冷启动数十秒,上限 60s);就绪后
290
- **先注销旧的 `idea` → 再重新注册**(远程 Streamable HTTP),等 `idea_*` 工具就绪,最后才通知。**每次执行都重建**。命令返回即可用;
291
- `/close-in-idea` 注销 `idea` + 删标记 + 清环境。
292
- - **`context` 钩子是请求的纯函数**:按「每次模型调用」触发 (含工具续跑),只看本次请求自带的工具快照 —— 有 `idea_*` 就剥离隐藏
293
- 工具并注入引导;没有则只在项目被标记时给恢复提示。该钩子本身**不做任何 I/O**(只看内存缓存),也不订阅任何事件。
434
+ - **已配置的 root**:启动读回配置后挂上 shell 环境钩子 (环境值待接入后填充)、按 IDE 实际提供的工具同步 skill
435
+ (IDE 还没应答时依赖配套工具的 skill 不注册);全部 IDE MCP 由 `/open-in-idea` 注册。
436
+ - **未配置的 root**:不改环境、不注册、不注入任何 IDE 提示 (每请求零开销;setup 时只做一次配置读取)。
437
+ - **手动接入 + 就绪即返回**:`/open-in-idea` 会按**每个项目自己的 IDE** 分别 `open -a` 拉起并**等 IDE MCP 就绪**(冷启动数十秒,
438
+ 上限 60s);就绪后 **先注销全部旧注册 → 再按项目各注册一个 server**(远程 Streamable HTTP),等 IDE 工具就绪,最后才通知。
439
+ **每次执行都重建**。命令返回即可用;`/delete-in-ide` 注销全部注册 + 删配置 + 清环境。
440
+ - **`context` 钩子是请求的纯函数**:按「每次模型调用」触发 (含工具续跑),只看本次请求自带的工具快照 —— 有任一项目前缀的工具就
441
+ **按每个前缀**剥离隐藏工具并注入**逐项目**引导;没有则只在已配置时给恢复提示。该钩子本身**不做任何 I/O**(只看内存缓存)。
294
442
  - **请求驱动的恢复(没有后台轮询)**:
295
- - **`prompt` 钩子**:「已标记 + 本实例没有任何 `idea` 注册」时**探测端口并重新注册**(只探测、绝不 `open -a`)。OpenCode 会
296
- await `prompt` 才构建请求,所以**触发它的那条消息本身**就带上了 `idea_*` 工具,没有空窗。这正是 Location 被空闲驱逐
443
+ - **`prompt` 钩子**:「已配置 + 本实例没有任何 MCP 注册」时**探测端口并重新注册**(只探测、绝不 `open -a`)。OpenCode 会
444
+ await `prompt` 才构建请求,所以**触发它的那条消息本身**就带上了 IDE 工具,没有空窗。这正是 Location 被空闲驱逐
297
445
  (60 分钟 `idleTimeToLive`)或 OpenCode 重启后,新实例自动接回连接的路径。
298
- - **`context` 钩子**:发现「本会话见过 `idea_*` 工具、本请求却没有」→ 立刻锁存并在后台重建(不等下一条消息)。工具缺失时注入的
299
- 恢复引导会**明确禁止**调用 `idea_*`(此前复用"重试一次"的文案,反而诱导模型去调一个不在工具表里的工具,得到
300
- `No tool named ... available`)。
446
+ - **`context` 钩子**:发现「本会话见过 IDE 工具、本请求却没有」→ 立刻锁存并在后台重建(不等下一条消息)。工具缺失时注入的
447
+ 恢复引导会**明确禁止**调用任何 IDE 工具(此前复用"重试一次"的文案,反而诱导模型去调一个不在工具表里的工具,
448
+ 得到 `No tool named ... available`)。
301
449
  - **`execute.after` 钩子**:见下一条。
302
- 恢复是**每个 Location(项目)各一份**;没有任何定时器,插件在不使用时完全静止。
303
- - **失败触发的自动接入(仅被标记项目)**:`tool.execute.after` 钩子监听 `idea_*` 工具报错并**分类**:
450
+ 恢复是**每个 Location(项目)各一份**;没有任何定时器,插件在不使用时完全静止。
451
+ - **失败触发的自动接入(仅已配置的 root)**:`tool.execute.after` 钩子监听 IDE 工具报错并**分类**:
304
452
  - IDE MCP 不可用(掉了)→ 重跑 `/open-in-idea` 的接入流程;
305
- - IDE 在、但项目没打开(`Unable to determine the target project` / `doesn't correspond to any open project`)→ 让 IDEA 重新打开该项目;
306
- - 普通业务错误(`File not found` 等)→ **不动作**。判定只认**传输层专有标记**(`MCP server "idea" is not available` /
453
+ - IDE 在、但项目没打开(`Unable to determine the target project` / `doesn't correspond to any open project`)→ 让**这个工具
454
+ 所属项目**的 IDE 重新打开它(**不会**去动别的项目);
455
+ - 普通业务错误(`File not found` 等)→ **不动作**。判定只认**传输层专有标记**(`MCP server "<名>" is not available` /
307
456
  `MCP server is not connected` / `Error POSTing to endpoint`),**不认**工具输出里出现的通用串
308
457
  (例如 SQL 工具报的 `Connection refused`) —— MCP 工具错误的 message 就是工具自己的输出,认通用串会把正常业务失败误判成掉线并重建。
309
- 触发成功会把该工具错误改写为"已重连 / 已重新打开,请重试",模型在同一回合内重试;失败则保持原错误。非 `idea_*` 工具、未标记
310
- 项目都不受影响。启动保护 (`launchCooldownMs` / `launchMaxAttempts`) 限制重复拉起 IDE。
311
- - **`/open-in-idea`**:手动打开当前项目;**等 IDE MCP 就绪 → 先注销旧的 `idea` → 再重新注册 → 等 `idea_*` 工具就绪**才返回。
312
- **`/close-in-idea`**:注销 IDE MCP + 删项目标记 + 清空注入的环境。
458
+ 触发成功会把该工具错误改写为"已重连 / 已重新打开,请重试",模型在同一回合内重试;失败则保持原错误。非 IDE 工具、未配置
459
+ 的 root 都不受影响。启动保护 (`launchCooldownMs` / `launchMaxAttempts`) 限制重复拉起 IDE,**按项目各自计**。
460
+ - **`/open-in-idea`**:手动接入 root 与全部子项目;**等每个项目的 IDE 就绪 → 先注销全部旧注册 → 再按项目各注册一个 server →
461
+ 等 IDE 工具就绪**才返回。**`/delete-in-ide`**:注销全部 IDE MCP + 删整个配置 + 清空注入的环境。
313
462
 
314
463
  ## 限制
315
464
 
@@ -319,15 +468,32 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
319
468
  - skill 定义的字段名**以 schema 为准**,不要照官方文档:`Skill.Info` 需要 `path` (AbsolutePath),文档里写的 `location`
320
469
  不存在 —— 用错会让 `ctx.skill.transform` 抛 `SchemaError`,而**任何 transform 失败都会禁用整个插件**(0.0.6 就是这样丢了
321
470
  `/open-in-idea`)。同理,`Skill.Info` 的完整字段是 `id` / `name` / `description?` / `autoinvoke?` / `path` / `content`。
322
- - 项目命令走 `idea_execute_terminal_command` + **Brave Mode**;没开 Brave Mode 时每条命令都要用户确认一次。
323
- - 一批用不到的 IDEA MCP 工具被硬编码隐藏 (`src/plugin.js` 的 `HIDDEN_IDEA_TOOLS`,共 31 个):VCS (`idea_git_status` /
324
- `idea_get_repositories`,版本控制走原生 `git`)、Router 派发工具 (`idea_execute_tool`,未启用 router-only 时冗余)、Debugger
325
- (全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、解释器环境 MCP、以及数据库的建/改数据源 (数据源由用户在
326
- IDEA 里配置,AI 只读和查询)。**`idea_execute_terminal_command` 不隐藏**:项目命令要在 IDE 环境里跑,模型需要它。
327
- - 探测只确认「MCP 服务是否在监听」;**「当前项目是否已在 IDE 打开」靠一次项目作用域的 MCP 调用判断**
328
- (`get_project_modules`,带 `IJ_MCP_SERVER_PROJECT_PATH`):返回模块 = 已打开 → 跳过 `open -a`;报
329
- `Unable to determine the target project` = 未打开 → `open -a` 打开。MCP 关闭时会用一次短探测并立即给出设置提示。
330
- 项目刚打开时的短暂"项目未就绪"由工具失败分类兜底(「IDE 在、但项目没打开」→ 重新打开并让模型重试)。
471
+ - 需要项目环境的命令走 `ide_execute_run_configuration`(run configuration 要先在 IDEA 里配好),不用原生 shell。
472
+ - 一批用不到的 IDE MCP 工具被硬编码隐藏 (`src/plugin.js` 的 `HIDDEN_IDE_TOOL_SUFFIXES`,**共 48 个后缀**):
473
+ VCS (`git_status` / `get_repositories`,版本控制走原生 `git`)、Router 派发工具 (`execute_tool`,未启用 router-only 时冗余)、
474
+ Debugger (全部 `xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、解释器环境 MCP、**IDE 内置终端
475
+ (`execute_terminal_command`:实测不继承项目环境,对原生 shell 没有优势)**、**整个 Database 组**(数据源由用户在
476
+ IDE 里配置,而 IDE 也不是本插件设想的数据库客户端),以及**有原生对应的文件读写**
477
+ (`search_text` / `search_regex` / `search_file` / `apply_patch` / `create_new_file`
478
+ 分别对应 `grep` / `grep -E` / `glob` / `edit` / `write`)。
479
+
480
+ 存的是**后缀**,清洗时按当前每个项目前缀**各删一遍** —— 只处理 `ide_` 会让子项目那几份原样送到模型面前。
481
+ 实测数字(拿运行中的 IDE MCP 拉 `tools/list`,单个项目):IDE 暴露 **64** 个 → 隐藏 48 → **模型可见 16 个**,
482
+ 而且这 16 个**恰好等于引导里点名的 16 个** —— 没有"能看见但没人告诉它用途"的残留。多项目就是 N × 16:
483
+ 暴露面完全由插件决定,不需要 OpenCode 提供任何白名单能力,也不需要惰性注册。
484
+
485
+ - **端口是产品常量,不开放配置**:IDEA 的 MCP 在 **64342**,PyCharm 在 **64462**(`src/ide-projects.js` 的 `IDE_PORTS`),
486
+ 跨版本不变。所以"探哪个端口"由项目的 IDE 唯一决定,不需要扫描候选端口,也不需要用户填任何东西。
487
+ - 探测只回答一个问题:**「这个 IDE 里打开着这个项目吗」** —— 一次项目作用域的 MCP 调用
488
+ (`get_project_modules`,带 `IJ_MCP_SERVER_PROJECT_PATH`):返回模块 = 认得 → 绑定;报
489
+ `Unable to determine the target project` = 不认得 → `open -a` 打开它并继续轮询到认得(上限 60s)。
490
+ 项目刚打开时的短暂"未就绪"由工具失败分类兜底(「IDE 在、但项目没打开」→ 重新打开并让模型重试)。
491
+ - **IDE 的「目录包含」语义(实测)**:请求路径落在某个**已打开项目的目录树内**时,IDE 会把它解析到
492
+ 那个项目,而不是报错。所以「子项目与 root 配同一个 IDE」时,只要那个 IDE 打开了 root,子项目就被判成
493
+ "已打开" —— 它确实是 root 项目的一部分,工具能操作它的文件,但语言支持按 **root 项目**走(例如
494
+ Python 子项目被 root 的 Web 模块解析,符号检索与检查会不准)。这是 IDE 的语义,绕过不了;要让子项目
495
+ 按自己的模块解析,就得让它在那个 IDE 里**单独打开**成一个项目,或者(正常做法)给它配它真正被打开的
496
+ 那个 IDE。完全不在任何已打开项目下的路径(`/tmp`、不存在的路径)两个 IDE 都会正确报错。
331
497
  - **项目路径按 IDE 的写法归一化**:IDE 收的是系统无关形式(正斜杠),其自带配置示例在 Windows 上也是
332
498
  `"E:/gitea/secondev-demo"`。OpenCode 的 `location` 在 Windows 上是原生反斜杠,所以 `src/mcp/idea.js` 的
333
499
  `ideProjectPath()` 把它转成正斜杠,**注册与探测走同一个函数**,两者必须一致,否则会用一种写法注册、又用另一种
@@ -335,23 +501,28 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
335
501
  - IDE **2026.2+** 提供 Streamable HTTP 端点 `/stream`,这是插件**唯一**使用的传输(`type: "remote"`)。注册本身不启动任何子进程,
336
502
  所以没有"桥进程被 IDE 拖死后变成僵尸"这一类问题。`/open-in-idea` 仍要求 IDE MCP 已在监听(冷启动约数十秒),因此会先等就绪再注册。
337
503
  - **空闲驱逐(已实测)**:OpenCode 把一个 Location 的服务缓存在 `LayerMap` 里,`idleTimeToLive` = **60 分钟**;空闲到期即整体驱逐,
338
- 插件实例连同 `idea` MCP 注册一起消失。**两种传输都躲不过**,所以对策不是换传输,而是请求驱动的恢复(见「行为」):
504
+ 插件实例连同**全部** IDE MCP 注册一起消失。**两种传输都躲不过**,所以对策不是换传输,而是请求驱动的恢复(见「行为」):
339
505
  - 新实例上第一条用户消息的 `prompt` 钩子会把连接接回来 —— 常态下**不需要用户手动做什么**;
340
- - 手动重建仍可用:执行 `/open-in-idea`(先注销旧 `idea`、再重新注册);
341
- - 插件给 `idea` server 配 `executionTimeoutMs`(默认 **10 分钟**)作为硬上限:兜住端点卡住时的单次调用,不再沿用 OpenCode 默认的 12 小时;
342
- - "IDE 被关掉"这一类会被下一次 `idea_*` 调用暴露(HTTP 直接报错),由 `execute.after` 钩子分类后自愈;不产生 error 的场景(例如
506
+ - 手动重建仍可用:执行 `/open-in-idea`(先注销全部旧注册、再重新注册);
507
+ - 插件给每个 server 配 `executionTimeoutMs`(默认 **10 分钟**)作为硬上限:兜住端点卡住时的单次调用,不再沿用 OpenCode 默认的 12 小时;
508
+ - "IDE 被关掉"这一类会被下一次 IDE 工具调用暴露(HTTP 直接报错),由 `execute.after` 钩子分类后自愈;不产生 error 的场景(例如
343
509
  IDE 进程还在但项目没打开)由「IDE 在、项目没打开」分支处理。
344
- - 项目标记存在插件名下 KV (`plugin:opencode-idea:project/<项目根路径>`)。插件实例按 **Location**(项目/打开目录)创建,标记按
345
- **项目根路径**共享;但两个不同 Location(例如项目根与某子目录)各有自己的内存缓存 —— 单目录打开项目的常见用法不受影响。
346
- - `/close-in-idea` 会**注销 IDE MCP** 并删标记 + 清空注入的环境。
347
- - 插件**完全不再检查 `.idea` 目录**:是否是 IDEA 项目只由项目标记(`/open-in-idea` 设置、`/close-in-idea` 清除)决定。
348
- (`/open-in-idea` 会让 IDEA 打开并导入该项目,`.idea` 由 IDEA 自己生成,插件无需也不应据此判断。)
510
+ - 配置存在插件名下 KV (`plugin:opencode-idea:project/<root 路径>`),内容是
511
+ `{ root: { path, app }, children: [{ path, app }] }` —— root 与每个子项目**各自的 IDE** 都记在里面。插件实例按
512
+ **Location**(项目/打开目录)创建,配置按 **root 路径**共享;旧版存过的布尔 `true` 会按"root 用 idea 的 v1 配置"读入。
513
+ - `/delete-in-ide` 会**注销全部 IDE MCP** 并删**整个**配置 + 清空注入的环境。
514
+ - **目录名就是工具前缀,不合规一律拒绝**:必须匹配 `^[a-z0-9][a-z0-9_-]*$`(小写字母或数字开头,只含小写字母、数字、`-`、`_`)、
515
+ 不与已登记项目重名、不占用保留名 `ide`。插件**绝不**自动改名或自动消歧 —— 静默改一个名字,等于用户的目录和模型看到的工具名
516
+ 对不上,而且没人会发现。
517
+ - 插件**完全不再检查 `.idea` 目录**:是不是 IDE 项目只由配置(`/open-in-idea` 设置、`/delete-in-ide` 清除)决定。
518
+ (`/open-in-idea` 会让对应 IDE 打开并导入该项目,`.idea` 由 IDE 自己生成,插件无需也不应据此判断。)
349
519
  - **自动拉起 IDE 仅实现 macOS** (`open -a`)。其他平台(包括 Windows)没有对应的启动命令,**请手动打开 IDE 和项目**,再执行
350
- `/open-in-idea` —— 端口在监听就会正常接入,其余能力(注册、探测、自愈、工具暴露)与 macOS 完全一致。
520
+ `/open-in-idea` —— 端口在监听、项目被认得就会正常接入,其余能力(注册、探测、自愈、工具暴露)与 macOS 完全一致。
351
521
  IDE 里打开 MCP 服务(2026.2+)并启用 Brave Mode;首次打开若弹 Trust 对话框,需确认后重试。
352
522
  - IDE MCP 没有「打开项目」工具,因此打开动作走 OS/CLI,而非 MCP。
353
523
  - 插件热重载会断开 IDE 连接,需重跑 `/open-in-idea`。
354
- - 插件不修改任何工具的定义、描述或可用性;IDE MCP 工具以 `idea_*` 直接暴露给模型 (`codemode: false`),不用 `execute` 包裹。
524
+ - 插件不修改任何**它不接管**的工具定义或描述;IDE 工具以 `<项目前缀>_*` 直接暴露给模型 (`codemode: false`),不用 `execute` 包裹。
525
+ 唯一的例外是 `rebuild` 参数(见「工具参数」),它对每个项目前缀各处理一遍。
355
526
 
356
527
  ## License
357
528