想在 JetBrains 里用 ZCode,有三条路:照着界面自己写一个插件;用外部浏览器打开官方 Web 端; 或者把官方 Web UI 直接嵌进 IDE。第三条听起来最省事,实际做下来, 难点全在边界上:窗口归谁、端口归谁、项目路径归谁、登录态归谁。
它不是仿制品
zcode-ide 的定位不是「仿 ZCode 的插件」,而是给官方产品换一层 IDE 外壳。
界面直接用官方的 @zcode/ui,后端直接跑官方的 @zcode/server,
数据和桌面客户端共享同一个 ~/.zcode 目录。
这带来一个很实际的好处:同一套 Agent、同一套会话、同一套界面。 上游更新时,大部分能力自动跟上。代价也很明确:必须跟着上游的启动参数、通信协议和目录结构走, 任何一处假设变化,插件都要跟着改。
不打包 Electron,不等于没有运行时。它只是把 Electron 换成了 IDE 自带的 JCEF,把独立进程换成了被插件托管的本地 server。
三层,而不是两层
整个链路可以画成这样:
IntelliJ (JDK 21)
└── ToolWindow / JBCefBrowser(每个项目窗口一个,共用同一个 server)
└── http://127.0.0.1:<port>/?token=..&ideWorkspace=<项目>&ideHost=1
└── packages/web-ide/dist(官方 Root + IDE 版 IPlatformService)
└── WS /ws → @zcode/server(Node 24,local 宿主形态)
└── 按 workspaceKey 各自 spawn agent
插件层只做三件事:创建工具窗口、启动并连接本地 server、把 Web 需要的能力桥接给 IDE。 真正的业务逻辑仍然在 Web 和 server 里。这个分层决定了后面所有设计: 凡是 IDEA 能做的,走桥;凡是 ZCode 原有的,走官方链路,插件不插手。
一个 server,多个窗口
整个 IDE 只启动一个 server 进程,端口固定。这是我最开始不太理解、 后来觉得最关键的一个决定。
原因是 JCEF 的 localStorage 按 origin 隔离。如果每个窗口起一个随机端口的 server,
那么每个窗口都是不同的 origin,草稿、最近使用的模型、界面偏好都会各存一份,
窗口一换或者 IDE 一重启就丢。端口固定之后,所有窗口同源,状态自然共享。
多窗口的问题用另一个办法解决:项目路径由 URL 的 ideWorkspace 参数传入,
server 本身不绑定项目。每个窗口锁定自己的 workspace,agent 按 workspaceKey
分别 spawn。server 是共享的,项目是隔离的。
两条桥,一条不经过插件
页面里有三条通信路径,只有两条和插件有关:
| 路径 | 链路 | 用途 |
|---|---|---|
| 业务 RPC | UI → HTTP / WS → server → agent | 官方链路,插件不参与 |
| Web → IDE | JBCefJSQuery → IdeBridge | 文件对话框、打开文件、系统通知 |
| IDE → Web | executeJavaScript → __zcodeIdeMessage | 右键发送引用、选区插入 |
业务 RPC 完全不经过插件,是这套设计能保持薄的关键。插件越少介入业务, 上游升级时的破坏面就越小。两条桥只传递两类东西:本机能力调用,和「用户在 IDE 里选中了什么」。
比如右键「添加到 ZCode 对话」,实际发送的只有文件路径和行号,不传选中原文。
选中代码后发送选区,传的是 路径#Lx-Ly。这样做既避免了大段内容在桥上来回,
也让模型自己决定要读哪一段。
共享 ~/.zcode 的代价
与桌面客户端共享数据目录,意味着会话列表天然互通:在 IDE 里开的对话, 回到桌面客户端还能继续。但 SQLite 的锁不会因为使用场景不同就变得宽容, 所以有一个必须写清楚的限制:不要和桌面客户端同时运行。
测试时可以用 ZCODE_DATA_BASE_DIR 和 ZCODE_DESKTOP_HOME_DIR
隔离数据,避免把正式会话弄乱。这类隔离变量在集成项目里比功能本身更能节省时间。
薄插件与 runtime 按需下载
插件 zip 默认约 3.8MB,不包含 ZCode runtime。第一次打开工具窗口时才按需下载, 之后可以独立更新和回滚。这样做的好处是插件本体很小,IDE 的插件市场安装体验好; 代价是首次打开需要网络,离线或内网环境要打完整包。
# 薄包
./gradlew buildPlugin
# 离线 / 内网:把 runtime 一起打进去
./gradlew buildPlugin -PbundleRuntime=true
Node 24 也是按需供给的:优先用本机的,没有就从镜像下载到
~/.zcode/ide/runtime-node。插件不假设用户的 IDE 环境里一定有合适的 Node,
但也不强行塞一个几百 MB 的运行时进插件包。
同步上游,而不是向上游提 PR
这个项目维护的是官方源码的 fork,分支叫 ide-integration。
我们不向上游提 PR,而是定期 fetch + rebase。
原因很实际:IDE 适配的补丁面太大,不是一个能被上游接受的小改动;
而 fork 的好处是可以随时调整,不被上游的评审节奏阻塞。
可复现构建靠两样东西:vendor/zcode.lock 锁定 fork 的 commit,
以及 scripts/patch-budget.mjs 控制官方源码的补丁面。
后者是一个预算检查:改动官方代码的地方越多,未来同步的成本越高,
所以补丁要有配额,不能随手改。
已知的坑
- runIde 不可用:IntelliJ Platform Gradle Plugin 解析 IDEA 2025.1 的
product-info.json时会失败,只能装 zip 验证。 - Web 模式的「新建项目」不可用:官方对话框依赖桌面端能力,改成启动时自动创建或绑定当前项目。
- 「审查」按钮也会在 IDE 打开文件:patch 源和真实文件共用同一条打开规则,面板内看 diff 暂不支持。
- 欢迎页没有右上角的加号:上游在无活动任务时不渲染 header,插件无法凭空补一个。
- 源码版本略低于桌面客户端:等上游公开对应版本后再同步。
这些问题大多不是「做不了」,而是「不值得为它增加补丁面」。集成项目的核心判断是边界, 不是功能清单。想清楚哪些能力归 IDE、哪些归 Web、哪些归 server, 剩下的就都是普通工程问题。