Skip to main content
Glama
chaosst

doc-fine-tuning-mcp

by chaosst
README.md
# doc-fine-tuning-mcp —— opencode 办公文档精细修改标注器

一个 **opencode 的 MCP server**:当 LLM 需要对你做办公文档的精细修改时,用它在**独立的浏览器应用窗口**里**可视化地打开文档**,
由你**点击整段**(或**拖选连续的字符串**进行更精细的标注)并**输入每个位置的修改提示词**,可一次标注多处;完成后交回 agent,
LLM 依据你的提示词逐处执行修改。标注窗口会**随 agent 的取消/退出自动关闭**,同一文档多轮标注时**复用同一窗口自动重载**。

支持的格式:**`.docx` / `.xlsx` / `.pptx`**(OOXML)。v1 不做旧格式(.doc/.xls/.ppt)。

## 工作流程

```
用户对 opencode 说:把 D:\报告.docx 的第 3 段和标题改得更正式
   │
   ▼
opencode 插件 doc-edit-listener 检测到"文档精细修改意图",注入引导
   │
   ▼
LLM 调用 doc-edit_annotate_document("D:\报告.docx")
   │   → 本地 HTTP 服务启动,独立浏览器应用窗口打开标注页 http://127.0.0.1:<port>/?session=xxx
   │   (同一文档再次标注:复用该窗口自动重载,不重开新窗口)
   ▼
你在页面上:看到文档 → 点击整段/单元格/形状(或拖选连续字符串)→ 输入提示词 → 继续标注下一处 → 点【完成】
   │   (标注以 loc + prompt 形式提交给服务端;完成后 agent 窗口重新聚焦)
   ▼
LLM 调用 doc-edit_wait_for_annotations 拿到全部标注
   │   (若窗口被关闭:返回 status=closed + reason,LLM 据此决定重开或询问你)
   ▼
LLM 对每个标注:doc-edit_read_location 读取原文 → 依据提示词生成新内容
   │   → doc-edit_apply_edit 应用修改(首次自动备份 .bak-<时间戳>)
   ▼
标注窗口自动重载(服务端推送),展示修改后的最新文档供你检查
   │   → LLM 再次 doc-edit_wait_for_annotations 等待下一轮标注(多轮循环)
   ▼
你点【完成】且无标注 / 点【取消】/ 关闭窗口 → 修改流程结束
```

## 架构

```
opencode
 ├─ 插件 doc-edit-listener(监听消息 → 检测修改意图 → 注入引导)
 └─ MCP 客户端 ──stdio──► doc-edit MCP server(Node/TS, @modelcontextprotocol/sdk)
                          ├─ 标注工具:annotate_document / wait_for_annotations / cancel_session
                          ├─ 编辑原语:read_structure / read_location / apply_edit
                          ├─ 本地 HTTP 服务(127.0.0.1:动态端口)→ H5 标注页 + API
                          └─ Python 编辑引擎(python-docx / openpyxl / python-pptx)
```

- **位置定位的一致性**:页面上你点击的位置(`loc`)由 JS 侧遍历 OOXML 生成,Python 引擎用**同一遍历算法**解析定位,
  并有 `tests/parity` 测试保证两侧 loc→文本映射完全一致(尤其 Word 表格内段落、PPT 多形状)。
- **执行者是 LLM**:生成新内容的每一步都由 LLM 基于你的提示词完成;MCP server 只负责"打开页面、收集标注、按位置读写文件"。

## 安装

### 1. 克隆项目并安装依赖

```bash
cd D:\develop\doc-fine-tuning-mcp
npm install
# Python 编辑引擎(创建 .venv 并安装 python-docx/openpyxl/python-pptx)
cmd //c scripts\\setup_venv.bat
```

### 2. 构建

```bash
npm run build          # 编译 src → dist/(MCP server)
cd web && npm install && npm run build   # 构建 H5 标注页 → web/dist/
```

### 3. 注册 MCP server 到 opencode

在 `~/.config/opencode/opencode.jsonc` 的 `mcp` 块添加(开发模式,指向本地构建产物):

```jsonc
"doc-edit": {
  "type": "local",
  "command": ["node", "D:/develop/doc-fine-tuning-mcp/dist/index.js"],
  "enabled": true
}
```

> 发布模式(可选):`npm pack` 生成 tgz 后,改用 `npx -y --package <路径>/doc-fine-tuning-mcp-<版本>.tgz doc-fine-tuning-mcp`,与项目内其他 MCP 一致。

### 4. 安装监听插件

插件分两个文件:`doc-edit-listener.ts` 与其依赖的检测模块 `lib\detect.ts`。
opencode 插件目录会自动扫描其下的 `.ts` 文件作为插件,但**不会递归扫描子目录**,
因此 `detect.ts` 放在 `lib\` 子目录中不会被误当成独立插件加载。

```bash
copy plugin\doc-edit-listener.ts %USERPROFILE%\.config\opencode\plugin\doc-edit-listener.ts
mkdir %USERPROFILE%\.config\opencode\plugin\lib
copy plugin\lib\detect.ts %USERPROFILE%\.config\opencode\plugin\lib\detect.ts
```

> ⚠️ 插件模块**只能导出插件本身**(default)。不要给 `doc-edit-listener.ts` 添加任何具名函数导出,
> 否则 opencode 会把它们当作额外钩子/插件,导致加载失败并报 "Unexpected server error"
> (hooks 被置空后级联到 Provider.defaultModel 崩溃)。检测逻辑一律放在 `lib/detect.ts`。

插件依赖 `@opencode-ai/plugin`(`~/.config/opencode/node_modules` 已内置 1.18.11)。重启 opencode 后生效。

### 5. 端到端自检

```bash
npm test                  # 引擎 / parity / mcp 客户端 / 插件 全部测试
node scripts/e2e-verify.ts   # 输出 PASS 即闭环可用
```

## MCP 工具说明

| 工具 | 入参 | 说明 |
|---|---|---|
| `annotate_document` | `path` | 打开独立标注窗口,创建/复用标注会话,返回 `{session_id, url}` |
| `wait_for_annotations` | `session_id`, `timeout_seconds?` | 阻塞至 `done/cancelled/closed/timeout`,返回标注列表 + `user_actions`(用户回退等操作,供 LLM 总结修改) |
| `cancel_session` | `session_id` | 取消标注会话 |
| `read_structure` | `path`, `max_items?` | 文档大纲(docx 逐段 / xlsx 每 sheet 抽样 / pptx 每页形状) |
| `read_location` | `path`, `loc` | 指定位置原文 + 相邻上下文(docx `loc` 带 `range` 时返回子串) |
| `apply_edit` | `path`, `loc`, `new_content`, `mode?`, `style?` | 应用修改(docx `loc` 带 `range` 时只替换该子串;首次自动备份 `.bak-<时间戳>`) |
| `template_replace` | `path`, `variables` | 批量替换 `{{变量}}` 占位符(仅 docx;跨 run 断裂也能替换;返回 `{matched, replaced, missing_vars, applied}`) |
| `find_replace` | `path`, `find`, `replace`, `match_case?` | 全文查找替换(仅 docx,普通字符串非正则;返回 `{matched, replaced, locations}`) |
| `preview_edits` | `path`, `edits` | 批量预览修改(内存应用不写盘,返回每处 `{loc, before, after}`) |
| `list_versions` | `path` | 列出版本历史(每次 `apply_edit` 前自动快照到 `<path>.versions/`,新版本在前;含操作类型/人类可读描述/时间) |
| `restore_version` | `path`, `index` | 回滚到指定版本(回滚前先快照当前状态,可逆) |

`mode`: `replace` / `append` / `prepend` / `insert_after` / `delete`。
`style`(可选): `{ bold, italic, sizePt, color }`。

> 注:`template_replace` / `find_replace` 的替换采用 **run 级格式保真**——替换区间跨越多个不同格式的 run 时,新文本按源 run 字符权重分段、逐段继承对应 run 的格式(不再退化为只保留首 run 格式)。

## 位置描述符 loc

页面点击生成的 `loc` 是"改哪一处"的唯一凭据,六个类型:

```ts
| { kind: "docx-paragraph",       paraIndex }                  // Word 段落(body 文档序,0-based)
| { kind: "docx-cell",            tableIndex, rowIndex, colIndex, paraIndex }   // Word 表格内段落
| { kind: "xlsx-cell",            sheet, row, col }            // Excel 单元格(1-based,同 A1)
| { kind: "xlsx-range",           sheet, row1, col1, row2, col2 }
| { kind: "pptx-shape",           slideIndex, shapeIndex }     // PPT 形状(1-based)
| { kind: "pptx-shape-paragraph", slideIndex, shapeIndex, paraIndex }
```

## 使用说明与提示

- **LLM 应当**:先 `annotate_document` 让用户标注 → `wait_for_annotations` 取标注 → 对每个标注 `read_location` 核对原文 → 依提示词生成新内容 → `apply_edit`。**不要猜测修改位置**。
- **版本历史纠错**:每次 `apply_edit` 前文档都会快照到 `<path>.versions/`。若修改后发现内容异常(替换结果与提示词不符、用户对某轮结果不满意),用 `list_versions` 查看快照、`restore_version` 回退到修改前再重新生成。**回退后旧标注的 loc 索引可能失效**,应重新 `read_location` 核对或让用户重新标注(第一轮里回退最有价值——标注仍基于原始文档)。标注窗口侧栏的**「历史」Tab** 也可直接查看版本链(每条含操作描述/时间/大小)并一键回退。用户回退的操作会记入会话 `user_actions`,随 `wait_for_annotations` 返回——**LLM 看到 `user_actions` 含 restore 时应意识到文档已回退、之后的修改可能失效**,核对后再继续。
- **等待轮询(永不超时)**:`wait_for_annotations` 缺省**永不超时**(一直等到用户提交 / 取消 / 关闭窗口,或 agent 退出)。部分客户端对单次工具调用有超时上限(约 60s),此时单次调用会被截断——**再次调用该工具即可继续等待**,会话不会被截断而取消。
- **窗口关闭**:若用户关闭了标注窗口,`wait_for_annotations` 返回 `closed` + `reason`(`window_closed` / `page_unload` / `window_lost` / `agent_cancelled` / `agent_exited`)。LLM 应据原因决定重开标注(再次 `annotate_document`)或询问用户。
- **窗口自动重载(推模式)**:每次 `apply_edit` / `template_replace` / `find_replace` / `restore_version` 修改成功后,服务端会**自动向该文档的标注窗口推送重载**(约 2s 防抖,连续修改合并为一次),窗口立即展示修改后的最新内容——**不依赖 LLM 再手动调用 `annotate_document`**。
- **滚动视野保持**:重载后页面会**回到你之前的阅读位置**(视野锚定——记录视口顶部附近的内容并按偏移恢复,而非简单回顶),同时保持 xlsx 当前工作表与 pptx 当前页。
- **多对话共享 server 的窗口归属**:WorkBuddy 等客户端全局共享**同一个 MCP server 进程**,所有对话的标注会话混在同一进程里。为避免修改后重载到**别的对话**的窗口,LLM 应在 `wait_for_annotations` 拿到标注后,把返回的 `session_id` **原样传给 `apply_edit` 等修改工具**(工具已支持该可选参数),服务端据此精确重载本会话的窗口;未传时服务端按"最近活跃会话"兜底,路径有歧义则**不重载**(避免误伤)。同一文档的标注窗口在共享 server 下是复用的——建议同一时刻只在一个对话里操作同一文档。
- **同文档复用**:对同一文档再次 `annotate_document` 会**复用现有窗口**自动重载(页面重新拉取最新文件、清空上轮标注),不会重开新窗口。
- **多轮标注循环**:agent 处理完一轮标注后,窗口自动重载供你检查修改并继续标注;agent 应再次调用 `wait_for_annotations` 等待下一轮。在窗口里点【完成】且**不加任何标注**,即表示本轮无需再修改;点【取消】则**直接关闭窗口**结束本轮(`wait_for_annotations` 据此停止)。
- **字符串级标注**:在 Word 页面里**拖选连续的字符串**,标注即精确到该字符串(`loc.range`);`apply_edit` 只替换它。
- **取消**:用户在页面点【取消】,或 LLM 调用 `cancel_session`,会话置 `cancelled` 且标注窗口**直接关闭**;agent 进程退出时,所有标注窗口自动关闭。
- 修改是**非破坏性**的:首次编辑前自动生成 `文档名.bak-<时间戳>`,且每次修改前自动快照到 `<path>.versions/` 版本链(可用 `list_versions` / `restore_version` 任意步回退)。

## 已知限制

- 仅支持 `.docx` / `.xlsx` / `.pptx`(OOXML)。
- Word:docx-preview 渲染序与 body 遍历序假定 1:1(极端排版元素可能有偏差,见 `web/src/viewers/docxViewer.ts` 顶部注释)。
- PPT:形状定位以文本匹配兜底,形状文本重复时可能不精确。
- Excel:SheetJS + 轻量网格;支持**双击单元格页面内编辑**(修改项作为标注交回 LLM 落地);合并单元格非左上角只读;公式单元格直接编辑会替换公式为普通值。
- 会话状态在内存中,MCP server 重启即失效。

## FAQ

**Q:浏览器没有自动打开?**
A:标注页在 Chrome/Edge 的独立应用窗口打开(无地址栏/工具栏/标签页)。若找不到 Chrome/Edge 或启动失败,工具仍会返回 `url`,LLM 会把链接发给你,手动打开即可(此时窗口关闭检测退化为页面心跳)。

**Q:为什么标注窗口会自己关掉?**
A:当你用 Esc 取消 agent,或 agent/opencode 进程退出时,服务端会主动关闭标注窗口;你在页面上点【完成】或【取消】后窗口会保持只读等待复用(同一文档下一轮直接重载)。

**Q:标注页打不开 / 白屏?**
A:确认已执行 `cd web && npm run build`(服务端在 `web/dist` 缺失时只返回占位提示页);确认文档路径是绝对路径且文件存在。

**Q:为什么需要插件?**
A:插件在消息层检测"精细修改文档"的意图并注入引导,让 LLM 在动手前**先让你标注**,避免它猜位置直接改。注意:插件仅对 opencode 生效;在 WorkBuddy 等其他 MCP 客户端中,LLM 的流程引导来自工具描述与 `wait_for_annotations` 返回的 `next` 字段(已内置多轮循环约定)。

**Q:修改完成后窗口会自动重载,我再标注并点【完成】,agent 会继续处理吗?**
A:**取决于 agent 是否仍在等待**:
- 若 agent 按引导处于"处理完标注 → 再次 `wait_for_annotations`"的循环中,你提交的新标注会被它立刻拿到并继续处理(多轮无缝衔接);
- 若 agent 已经结束回合(不再调用工具),你的标注会暂存在服务端,但**不会自动唤醒 agent**——此时再跟 agent 说一句"标注已提交,请继续处理"即可。
这是"执行者是 LLM"架构的固有边界:服务端无法代替 agent 主动接管下一轮。

**Q:两个对话同时编辑同一文档,窗口会串吗?**
A:WorkBuddy 全局共享同一个 server 进程,同一文档的标注窗口会被复用共享。修改后的自动重载现在**精确绑定会话**:LLM 在 `apply_edit` 等工具中带上 `wait_for_annotations` 返回的 `session_id`,就只会重载本对话的窗口;未带时按最近活跃会话兜底,路径有多个存活会话时宁可**不重载**也不误伤。建议同一时刻只在一个对话里操作同一文档。

TDQS

A4/5.0

Scored across 11 tools

Disambiguation4/5

Most tools have clearly distinct roles in the read-annotate-edit-version workflow. The main ambiguity is among apply_edit, find_replace, and template_replace, which all modify documents, though their descriptions clarify different use cases.

Naming Consistency4/5

Tool names are consistently lowercase snake_case and mostly follow a verb_noun pattern like read_location, apply_edit, and list_versions. template_replace and find_replace deviate slightly from the verb-first style, but the naming remains predictable and readable.

Tool Count5/5

11 tools is well within the ideal range, and each tool covers a necessary part of the fine-tuning workflow: reading, annotating, waiting, editing, previewing, and versioning. There are no obvious filler or redundant tools.

Completeness5/5

The toolset covers the full annotation loop: open a session, wait for annotations, apply edits, preview changes, and roll back via versions. Features like document creation or export are outside the stated fine-tuning purpose, so no significant gaps are apparent.

Maintenance

ActivityMaintained
ResponsivenessNo issues