# @epoch-agent/plugin-lsp 中文文档

规范地址：https://npmcn.com/package/%40epoch-agent/plugin-lsp/

版本：6\.2\.0
npm 元数据更新时间：2026-11-13T08:28:29.080Z
中文译文更新时间：2026-11-13T19:50:35.600Z
软件包许可：Apache-2\.0

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40epoch-agent/plugin-lsp
原文对照：https://npmcn.com/package/%40epoch-agent/plugin-lsp/?lang=original
项目主页：https://github.com/Ddbor/epoch-agent#readme
源码仓库：https://github.com/Ddbor/epoch-agent

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：80.8 kB，5 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.0\.0
- 运行依赖：4 个
- 首次发布：2026-08-19T10:31:53.698Z
- 最新版本发布：2026-11-13T08:28:28.863Z
- 近 30 天下载量：4258（2026-10-14 至 2026-11-12，npm 官方下载统计）
- 已发布版本数：57（近 12 个月 57 个）

## 中文简介

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

## 安装

```sh
npm install @epoch-agent/plugin-lsp
```

也可以使用：`pnpm add @epoch-agent/plugin-lsp`、`yarn add @epoch-agent/plugin-lsp`、`bun add @epoch-agent/plugin-lsp`

## README

# @epoch-agent/plugin-lsp

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

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

## 工具参数

```json
{ "paths": ["src/a.ts", "src/b.tsx"] }
```

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `paths` | ✅ | 文件路径数组，相对工作目录或绝对路径。一次最多 50 个文件 |

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

当前认得 8 个扩展名：`.cjs` `.cts` `.js` `.jsx` `.mjs` `.mts` `.ts` `.tsx` （唯一真源是 [servers.ts](https://npmcn.com/package/%40epoch-agent/plugin-lsp/src/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](https://npmcn.com/docs/verify/VERIFY_RECORD-38-lsp-diagnostics.md)）。
2.  **输出是排好版的文本而不是 `[{file, line, ...}]`**，理由见上一节。

## 三级探测，一个 server 都不捆绑

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

和 [infra 的 ripgrep 探测](https://npmcn.com/package/infra/src/ripgrep.ts)反着来：那边捆绑一个 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](https://npmcn.com/package/%40epoch-agent/plugin-lsp/__tests__/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`

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

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

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

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

[servers.ts](https://npmcn.com/package/%40epoch-agent/plugin-lsp/src/servers.ts) 的 `SERVERS` 是**唯一**需要改的地方，client / pool / diagnose 一行都不用动。别让语言判定漏到别处去。Python / Rust / Go 是方案 38 的 PR-3，**刻意砍掉的**（不是做到一半放弃）—— 砍的判据和真要做时的两条成本写在 [验收记录 8.1](https://npmcn.com/docs/verify/VERIFY_RECORD-38-lsp-diagnostics.md)。

## 文件

| 文件 | 内容 |
| --- | --- |
| `index.ts` | `lspPlugin` + 模块级池 + `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` 我们没想到的形态。别复用别人的段 —— 用户报错时那四位数是唯一能一眼定位到哪个包的东西。

## 开发

```bash
npx vitest run --project=plugin-lsp
```

**八个**用例文件（2026-11-03 核：上一版写的是「七个」，漏了 `orphan-reap`）。 两处值得先看：

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

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

## 常见问题

### @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 和提交记录判断。

