@epoch-agent/plugin-lsp

中文文档
6.2.0发布于 2026/11/13Apache-2.0TypeScriptES Module近 30 天下载 4258

LSP 诊断插件 — 探测项目自带的 language server,按文件取语义诊断

原始使用文档
文档目录10 节
  1. 工具参数
  2. 输出是文本,不是 JSON 数组
  3. 两处与方案 38 原文的偏差
  4. 三级探测,一个 server 都不捆绑
  5. 诊断是 推 过来的,所以「收全了没」要自己判
  6. 进程池:懒启动 / 10 分钟回收 / 崩一次重启一次
  7. 为什么是 file_read 而不是 command
  8. 加一门语言 = 往 SERVERS 里加一条
  9. 文件
  10. 开发

@epoch-agent/plugin-lsp

LSP 诊断插件。一个工具:lsp_diagnostics —— 用项目自己的 language server 回答「我刚改的这几个文件,编译器怎么说」。

  • ✅ 做:探测 language server(项目 node_modules → PATH)、起一个常驻连接、 didOpen / didChange 后收 publishDiagnostics、判「收全了没」、排版给模型看
  • ❌ 不做:不捆绑任何 server(探不到就如实报「跳过 + 装法」)、不做补全 / 跳转 / 重命名、不做项目级构建、不自己 spawn(进程一律走 infra 的 startLongLivedProcess)
  • 依赖:protocol + infra + vscode-jsonrpc / vscode-languageserver-protocol

工具参数

{ "paths": ["src/a.ts", "src/b.tsx"] }
参数必填说明
paths✅文件路径数组,相对工作目录或绝对路径。一次最多 50 个文件

单个路径写成字符串("paths": "src/a.ts")也收 —— 那是很自然的手误, 为它回一条参数错误等于白烧一轮。

当前认得 8 个扩展名:.cjs .cts .js .jsx .mjs .mts .ts .tsx (唯一真源是 servers.ts 的 supportedExtensions(), 工具描述和报错提示都从那儿取)。不认的扩展名进 unsupported, 工作目录外的路径进 rejected,都不会让这次调用失败。

输出是文本,不是 JSON 数组

2 条诊断(1 error · 1 warning) · 已检查 2 个文件

src/a.ts:12:5  error typescript(2322): 不能将类型 "string" 分配给类型 "number"
src/b.tsx:3:10  warning typescript(6133): "unused" 已声明但从未读取

core 的 tool-executor 对非字符串 output 一律 JSON.stringify,那一行 JSON 里 每条诊断都要重复七个键名 —— 对 30 条诊断是白烧一倍多的 token,而模型从 src/a.ts:12:5 error … 里读到的信息一模一样(它每天在 tsc / eslint 的输出里 见的就是这个)。字段一个没少,只是换了排布。

三件事一定在输出里:检查了哪些文件(没这行,「没发现问题」和「一个文件都没 检查」长得一样)、跳过的语言 + 安装命令、被截断了多少(上限 100 条, 按 severity 优先保留 error)。

两处与方案 38 原文的偏差

  1. paths 是必填的。 方案里「不传就检查本轮改过的文件」要拿 CheckpointManager 那份本轮改动清单,而插件的 ToolContext 只有 {sessionId, workDir, extraRoots, permissionLevel, signal, onOutput} —— 补这条得改 core 的 checkpoint 接口。先做成必填(模型知道自己刚改了哪几个文件),那条留给后续 PR (验收记录 8.2)。
  2. 输出是排好版的文本而不是 [{file, line, ...}],理由见上一节。

三级探测,一个 server 都不捆绑

1. 项目本地  node_modules/<pkg>   ← TypeScript 那条「版本跟项目走」的硬要求
2. PATH
3. 都没有 → 该语言 skipped + 安装命令,其它语言不受影响

和 infra 的 ripgrep 探测反着来:那边捆绑一个 6MB 单文件,这边一个都不捆。language server 是一堆文件,而且强绑定用户的项目配置 —— 拿全局的 tsc 去检查一个 pin 了 5.8 的项目,报出来的错是假的。所以还额外把项目 自己那份 node_modules/typescript/lib/tsserver.js 显式传进 initializationOptions.tsserver.path:server 自己也会找 workspace 版本, 但那是它内部的一条兜底顺序,它升一版换个顺序我们就悄悄换了 TS 版本报错。

故意不碰 node_modules/.bin。 Windows 上那儿躺的是 npm 生成的 .cmd 垫片, Node 的 spawn 不过 shell 就执行不了;而 pnpm 下它是符号链接,跟到哪儿要看安装 布局。走 node_modules/<pkg>/package.json 的 bin 字段拿真实入口、用 process.execPath 跑它,两个平台同一条代码路径,且必然是项目里那一份。 PATH 命中 .cmd / .bat 时才走 cmd.exe /d /s /c "<整条命令>" + verbatim。

⚠️ 项目本地那一档带 ELECTRON_RUN_AS_NODE=1(launch.env,值来自 infra 的 EXEC_PATH_AS_NODE_ENV)。嵌进 Electron 主进程的宿主里 process.execPath 不是 node 而是宿主自己的可执行文件,少了那一格,起 language server 等于 启动第二个 app 实例。PATH 那两档起的是真可执行文件,不需要。 全仓「谁把 execPath 当 node 使」由根 __tests__/exec-path-as-node.test.ts 数着。

PATH 扫描保留绝对路径去 spawn,而不是像 ripgrep 那样丢掉路径 spawn 裸名字。 因为这个工具的输入正是「别人的仓库」:shell: true + 裸名字在 Windows 上, 目标仓库根目录里放一个同名 .cmd 就能劫持。我们的扫描列表里没有 cwd。

诊断是推过来的,所以「收全了没」要自己判

LSP 里没有「诊断请求」这种东西 —— typescript-language-server 4.4 也不支持 3.17 的 pull 模式(initialize 的结果里没有 diagnosticProvider)。server 想发就发, 而 tsserver 一个文件会分三次发(语法 / 语义 / 建议,各自 50ms 去抖), 先到的那一份是不全的。

判据是静默:请求的文件都答过了,且最近一次动静距今超过 settleMs(默认 400ms, fallbackMs 1.5s,硬超时 15s)。

还有一条更阴的:内容没变时 server 干脆不发。 FileDiagnostics.update() 里有 一句「这一类上次是空、这次还是空就不推送」,于是一个干净的文件被诊断第二次时 一条消息都收不到。按「没答复 = 没结果」处理就会把最正常的那种情况报成 「server 没回答」。所以有第二条判据:没答复但有上一次的结果,就用上一次的。 e2e.test.ts 拿真 server 盯着这一条。

进程池:懒启动 / 10 分钟回收 / 崩一次重启一次

策略值
启动时机懒启动 —— 第一次 lsp_diagnostics 才起
空闲回收10 分钟没用就关(大项目的 tsserver 吃几个 G)
并发每种语言一个实例,按项目根隔离
初始化超时每种 server 自己给(TypeScript 是 30s)
崩溃下次调用重启一次;再崩就本会话内不再试

池是模块级单例:工具每次调用都是一次新的 execute,而 language server 的意义 全在于「活着、缓存着上次的语法树」。挂在调用上等于每次冷启一个 tsserver, 那比让模型自己跑 tsc 还慢。

「不可用」的每一条路径 —— 没装、起不来、初始化超时、崩了 —— 交出来的都是 { ok: false, reason } 而不是 throw。一个 server 起不来只该让它那门语言的文件被 跳过。「没装」不计入失败次数:那不是「试了没成」,计进去的话用户装完 server 还得重启 epoch 才能用上。

退出时宿主什么都不用做:进程在 infra 的进程级兜底表里,最后一个 runtime dispose() 时一并收掉。disposeLspServers() 是给「想早点收、且要走 LSP shutdown 礼节」的宿主用的。

⚠️ 落在兜底表而不是某个 runtime 名下是刻意的(方案 60 §2.6):这个池按项目根 共享,两个 runtime 开在同一个项目上时它们本来就该共用同一个 tsserver —— 它不是任何一个 runtime 的私产。代价写下来不掩饰:同进程里 A 和 B 并存时, B 的 dispose() 收不掉这些 server,要等 A 也走。

为什么是 file_read 而不是 command

annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: false }
operation: 'file_read'

这个工具对外的语义就是「读几个文件、报问题」。底下确实起了子进程,但那个进程是 我们表里写死的(servers.ts),参数里没有任何一段来自模型。 归到 command 只会让每次诊断都弹一次审批 —— 而 core 的 system prompt 恰恰要求 模型每轮改完都调它。idempotentHint: false 是因为文件内容会变:同一批路径 这一秒和下一秒的诊断可以不一样。

describeTarget 给的是文件清单,不给就退回 JSON.stringify(args), 换个顺序传同样几个文件就不命中审批缓存了。

加一门语言 = 往 SERVERS 里加一条

servers.ts 的 SERVERS 是唯一需要改的地方,client / pool / diagnose 一行都不用动。别让语言判定漏到别处去。Python / Rust / Go 是方案 38 的 PR-3,刻意砍掉的(不是做到一半放弃)—— 砍的判据和真要做时的两条成本写在 验收记录 8.1。

文件

文件内容
index.tslspPlugin + 模块级池 + disposeLspServers()
servers.ts支持哪些语言、怎么起、tsserver.path 怎么找
detect.ts三级探测(不 spawn,纯文件系统问题)
client.ts一个 server 连接:五条消息 + 静默判据 + 崩溃回调 + 优雅退出
pool.ts懒启动 / 空闲回收 / 并发去重 / 失败计数
diagnose.ts编排:路径归一 → 边界校验 → 按 server 分组 → 收结果
format.ts渲染成模型读的那段文字(截断 + 按 severity 优先)
find-up.ts逐级往上找一个相对路径
types.ts本包的类型

错误码占 8xxx 段:8001 参数不对、8002 我们没想到的形态。别复用别人的段 —— 用户报错时那四位数是唯一能一眼定位到哪个包的东西。

开发

npx vitest run --project=plugin-lsp

八个用例文件(2026-11-03 核:上一版写的是「七个」,漏了 orphan-reap)。 两处值得先看:

  • fixtures/fake-server.mjs 是一个手写分帧 的假 LSP server(故意不用 vscode-jsonrpc,那样才是一份独立的参照实现)。 process.argv[2] 选行为:normal / three-kinds / silent / once / hang-init / crash-on-open
  • e2e.test.ts 用真的 typescript-language-server, 被检查的文件写在临时目录里 —— 故意报错的 fixture 不该进本仓库的 tsc / lint 范围

vitest.config.ts 的 testTimeout 是 60s:起一个真 tsserver 加初始化, 默认的 5000ms 不够。

版本与发布记录

@epoch-agent/plugin-lsp 共发布 57 个版本,首次发布于 2026/08/19;过去 12 个月发布了 57 个版本。

版本发布日期距今
6.2.0latest2026/11/13今天
6.1.02026/11/112 天前
6.0.02026/11/112 天前
5.0.02026/11/103 天前
4.1.12026/11/067 天前
4.1.02026/11/067 天前

关于 @epoch-agent/plugin-lsp 的常见问题

@epoch-agent/plugin-lsp 是什么?

LSP 诊断插件 — 探测项目自带的 language server,按文件取语义诊断

如何安装 @epoch-agent/plugin-lsp?

在项目目录运行 npm install @epoch-agent/plugin-lsp,或使用 pnpm add / yarn add / bun add。

@epoch-agent/plugin-lsp 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@epoch-agent/plugin-lsp 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@epoch-agent/plugin-lsp 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@epoch-agent/plugin-lsp 可以商用吗?

@epoch-agent/plugin-lsp 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@epoch-agent/plugin-lsp 的下载量有多少?

根据 npm 官方下载统计,@epoch-agent/plugin-lsp 在 2026/10/14 – 2026/11/12 这 30 天内被下载 4,258 次,日均约 142 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@epoch-agent/plugin-lsp 还在维护吗?

最新版本 6.2.0 发布于 2026/11/13(今天);至今共发布 57 个版本,其中过去 12 个月发布了 57 个;首次发布于 2026/08/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。