@4399ywkf/editor-mcp 0.2.6 → 0.3.0

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
@@ -1,7 +1,7 @@
1
1
  # @4399ywkf/editor-mcp
2
2
 
3
3
  把 [`@4399ywkf/editor`](../editor) 的结构化编辑面接出去,让 AI 操作**浏览器里那个真实的
4
- tiptap 实例** —— AI 改的和用户看的是同一个。
4
+ 编辑器实例**(文档是 tiptap,表格是 Univer)—— AI 改的和用户看的是同一个。
5
5
 
6
6
  ## 分层
7
7
 
@@ -10,12 +10,16 @@ MCP 只是最外面那一层的一种传输。中间两层跟它没关系:
10
10
  | 层 | 在哪 | 知道 MCP 吗 |
11
11
  |---|---|---|
12
12
  | runtime:LiteXML 编解码 + 8 类操作 | `@4399ywkf/editor/doc-runtime` | 不知道 |
13
+ | runtime:表格快照读 + facade 写 | `@4399ywkf/editor/sheet-runtime` | 不知道 |
13
14
  | 工具面:name / JSON Schema / description | 本包 `./tools` | 不知道 |
14
15
  | 传输:浏览器桥 / hub / stdio MCP | 本包 `./bridge` 与 `bin/` | 知道 |
15
16
 
16
17
  runtime 住在编辑器包里而不是这儿,是因为 `NODE_TAG` 镜像的是**那个编辑器自己的
17
18
  schema**。跨包之后每加一个节点类型,序列化就静默降级成 `<node type="..."/>` 占位符 ——
18
- 不报错、不丢数据、也不告诉你。
19
+ 不报错、不丢数据、也不告诉你。表格同理:对 Univer facade 的探测全钉在
20
+ `sheet-runtime/facade.ts`,Univer 版本一漂只改那一个文件。
21
+
22
+ 两个 runtime 的 `call(tool, args)` 是同构的,桥不需要知道自己面对的是文档还是表格。
19
23
 
20
24
  ## 用法一:不走 MCP(推荐给已有 agent loop 的项目)
21
25
 
@@ -37,6 +41,19 @@ const result = await runtime.call(toolName, args)
37
41
  (`getPageContent` / `modifyNodes` / `replaceText` / `setEditor` / `isReady`),
38
42
  两套编辑器可以被同一套工具面驱动。
39
43
 
44
+ 表格换成 `SHEET_TOOLS` + `SheetRuntime`,形状一模一样:
45
+
46
+ ```ts
47
+ import { SHEET_TOOLS } from "@4399ywkf/editor-mcp/tools"
48
+ import { SheetRuntime } from "@4399ywkf/editor/sheet-runtime"
49
+
50
+ const runtime = new SheetRuntime()
51
+ // <DocumentView format="sheet" onReady={(univerAPI) => runtime.setUniverAPI(univerAPI)} />
52
+ ```
53
+
54
+ 两个都挂就喂 `ALL_TOOLS`(= `DOC_TOOLS` + `SHEET_TOOLS`),
55
+ 执行侧用 `composeRuntimes` 按前缀分发(见下)。
56
+
40
57
  ## 用法二:走 MCP
41
58
 
42
59
  ```
@@ -66,6 +83,24 @@ useEffect(
66
83
 
67
84
  样式记得引一次 `@4399ywkf/editor/styles`(AI 改动块的闪烁提示 `.ai-flash` 在里面)。
68
85
 
86
+ 同一个页面上既有文档又有表格时,用 `composeRuntimes` 把两个运行时并成一个挂上去 ——
87
+ 按工具名前缀路由,`doc_*` 去 tiptap,`sheet_*` 去 Univer:
88
+
89
+ ```tsx
90
+ import { attachBridge, composeRuntimes } from "@4399ywkf/editor-mcp/bridge"
91
+
92
+ const doc = useMemo(() => new DocRuntime(), [])
93
+ const sheet = useMemo(() => new SheetRuntime(), [])
94
+
95
+ useEffect(
96
+ () => attachBridge({ runtime: composeRuntimes({ doc, sheet }), url: "ws://127.0.0.1:4399/bridge" }),
97
+ [doc, sheet],
98
+ )
99
+ ```
100
+
101
+ 只挂一个也行 —— 打到没挂的前缀时报的是「本页面没有挂 sheet_\* 的运行时,可用的工具面:
102
+ doc_\*」,而不是「未知工具」。差别在于前者不会让模型换个工具名反复重试。
103
+
69
104
  **2. 起 hub**
70
105
 
71
106
  ```bash
@@ -74,8 +109,8 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
74
109
 
75
110
  默认只听 `127.0.0.1`。`--token` 可选但建议开:这个口的权限是「以用户身份重写他正在
76
111
  编辑的文档」,不开的话本机任何进程都能调。`--static <dir>` 可以顺便托管一个页面,
77
- 本地调试方便。`--out-dir <dir>` 指定 docx 的读写目录(默认 hub 的工作目录),
78
- `doc_export_docx` / `doc_import_docx` 只能碰这个目录里的文件。
112
+ 本地调试方便。`--out-dir <dir>` 指定文档的读写目录(默认 hub 的工作目录),
113
+ `doc_export_docx` / `doc_import_docx` / `sheet_export_xlsx` 只能碰这个目录里的文件。
79
114
 
80
115
  **3. 注册 MCP**
81
116
 
@@ -94,7 +129,9 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
94
129
  > codex `exec` 下需要 `--dangerously-bypass-approvals-and-sandbox`:`approval: never`
95
130
  > 会把 MCP 调用直接判成 "user cancelled"。交互式 `codex` 里可以逐次批准。
96
131
 
97
- ## 工具(10 个)
132
+ ## 工具(doc 10 个 + sheet 6 个)
133
+
134
+ ### 文档(tiptap)
98
135
 
99
136
  | 工具 | 用途 |
100
137
  |---|---|
@@ -116,25 +153,53 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
116
153
  工具描述里的可用标签清单是从 runtime 的标签表**生成**的,不是手抄的 —— 编辑器加一个
117
154
  节点类型,description 自动跟着变(`src/tools.test.ts` 守这条)。
118
155
 
119
- ### docx 的字节不进模型上下文
156
+ ### 表格(Univer)
157
+
158
+ | 工具 | 用途 |
159
+ |---|---|
160
+ | `sheet_read` | 工作簿概览 + 一张表的 CSV 视图与公式清单。写之前必须先调它 |
161
+ | `sheet_find` | 全簿检索(值和公式都搜),返回可直接回填的 `sheet` + A1 `ref` |
162
+ | `sheet_set_values` | 从某格起写一块二维值,`"=…"` 是公式、`null` 清空 |
163
+ | `sheet_edit_rows_cols` | 插入 / 删除整行整列(0-based) |
164
+ | `sheet_manage_sheets` | 工作表 add / rename / delete / activate |
165
+ | `sheet_export_xlsx` | 导出成 .xlsx 落盘,返回路径 + 统计 + 警告 |
166
+
167
+ 定位一律用 A1 引用,不用 node id —— 表格的坐标系本来就是稳定的,不需要 doc 那套
168
+ 「读了才有 id」的仪式。但**先读后写**这条纪律不变,而且比 doc 更要紧:A1 引用不读也能
169
+ 编出来,模型会照着想象中的表结构盲写。所以写工具的 description 里都写死了「先 sheet_read」。
170
+
171
+ 读走快照、写走 facade 是有意分的两条路:读是纯数据(Univer 版本漂移伤不到),
172
+ 且 CSV 文本的 token 成本只有 JSON 单元格矩阵的几分之一;写必须进 Univer 的 command
173
+ 体系才有撤销栈和协同广播,直接改快照是死路。
120
174
 
121
- `doc_export_docx` / `doc_import_docx` 的参数和返回值里**没有文件内容**,只有路径:
175
+ `sheet_manage_sheets` 的 `delete` 不允许省略 `sheet` —— 「默认删活动表」是脚枪。
176
+
177
+ ### 文档字节不进模型上下文
178
+
179
+ `doc_export_docx` / `doc_import_docx` / `sheet_export_xlsx` 的参数和返回值里**没有文件内容**,
180
+ 只有路径:
122
181
 
123
182
  ```
124
- 模型 ──path──▶ hub ──base64──▶ 浏览器(真正的 serializeDocx / parseDocx)
183
+ 模型 ──path──▶ hub ──base64──▶ 浏览器(真正的 serializeDocx / parseDocx / serializeXlsx)
125
184
  模型 ◀──path── hub ◀──base64── 浏览器
126
185
  ```
127
186
 
128
- 一份带图的 docx 动辄几 MB,base64 之后更大,塞进上下文会直接把窗口撑爆。所以字节
187
+ 一份带图的 docx / xlsx 动辄几 MB,base64 之后更大,塞进上下文会直接把窗口撑爆。所以字节
129
188
  只在 hub ↔ 浏览器之间走一趟,hub 负责落盘 / 读盘。这是 hub 唯一一处不"纯中继"的地方。
130
189
 
131
- 文件钉在 `--out-dir`(默认 hub 的工作目录)里,`../` 和绝对路径一律拒 —— 那两个
190
+ 文件钉在 `--out-dir`(默认 hub 的工作目录)里,`../` 和绝对路径一律拒 —— 那几个
132
191
  路径参数是**模型给的**,「能读写用户整块磁盘」和「能改用户正在编辑的文档」是两个
133
192
  量级的权限。
134
193
 
135
194
  前提:编辑器实例得注册了 `ExportDocx` / `ImportDocx` 扩展(从
136
195
  `@4399ywkf/editor/docx` 引入,通过 `<NotionEditor extensions={[...]}>` 注入)。
137
- 没注册时工具会报错并说明怎么装,而不是静默失败。
196
+ 没注册时工具会报错并说明怎么装,而不是静默失败。表格侧没有这道手续 ——
197
+ `sheet_export_xlsx` 直接读 Univer 快照拼 OOXML,但它返回的 `warnings` 是
198
+ **导出时丢了什么**(图片、条件格式等插件数据),非空就得如实转述,别当成功报。
199
+
200
+ 表格**导入**不在工具面里:`.xlsx / .csv / .tsv` 走 `<DocumentView source={file}>`
201
+ (实现在 `@4399ywkf/editor/xlsx` 的 `importSheetSource`)—— 那是用户打开文件的动作,
202
+ 不是 AI 的动作。
138
203
 
139
204
  ## 两条硬规则
140
205
 
@@ -158,11 +223,12 @@ LiteXML 对 ProseMirror JSON 是**有损压缩**(省 1.55x 以上 token),
158
223
  `onStatus` 会回 `observer`,请在界面上显示出来。接了 yjs 之后自然消失。
159
224
  3. **没有 AI 编辑的审计与回滚。** ProseMirror 的 Step 可 invert,应该持久化 inverted
160
225
  steps 做 `doc_revert_turn`,目前没做 —— 想兜底的话用 `attachBridge` 的 `onCall`
161
- 钩子自己存快照。
226
+ 钩子自己存快照。表格侧写操作都走 Univer 的 command 体系,用户按 Ctrl+Z 撤得掉,
227
+ 但同样没有「这一轮 AI 改了哪些格子」的审计记录。
162
228
 
163
229
  ## 开发
164
230
 
165
231
  ```bash
166
- pnpm --filter @4399ywkf/editor build # tools.ts 依赖它的标签表
167
- pnpm --filter @4399ywkf/editor-mcp test # hub 端到端 + 工具面形状
232
+ pnpm --filter @4399ywkf/editor build # 测试要读它的构建产物(标签表 / SheetRuntime)
233
+ pnpm --filter @4399ywkf/editor-mcp test # hub 端到端 + 工具面形状 + 两个 runtime 的不漂移
168
234
  ```
package/bin/hub.mjs CHANGED
@@ -13,8 +13,9 @@
13
13
  * ywkf-editor-hub [--port 4399] [--host 127.0.0.1] [--static ./dist] [--token xxx]
14
14
  * [--out-dir .]
15
15
  *
16
- * 唯一一处不"纯"的地方:doc_export_docx / doc_import_docx 的文件内容由 hub 落盘 /
17
- * 读盘,模型两头只看到路径。docx 是二进制,base64 进上下文会把窗口撑爆。
16
+ * 唯一一处不"纯"的地方:导入导出工具(doc_export_docx / doc_import_docx /
17
+ * sheet_export_xlsx)的文件内容由 hub 落盘 / 读盘,模型两头只看到路径。
18
+ * 文档是二进制,base64 进上下文会把窗口撑爆。
18
19
  */
19
20
  import { createServer } from "node:http"
20
21
  import { mkdir, readFile, writeFile } from "node:fs/promises"
@@ -127,12 +128,18 @@ async function beforeCall(tool, args) {
127
128
  return { ...rest, base64: buf.toString("base64") }
128
129
  }
129
130
 
130
- /** 调用后:把 doc_export_docx 回来的 base64 落盘,只把路径交给模型 */
131
+ /** 调用后:把导出工具回来的 base64 落盘,只把路径交给模型 */
132
+ const EXPORT_TOOLS = new Map([
133
+ ["doc_export_docx", "document.docx"],
134
+ ["sheet_export_xlsx", "工作簿.xlsx"],
135
+ ])
136
+
131
137
  async function afterCall(tool, result) {
132
- if (tool !== "doc_export_docx" || !result || typeof result !== "object") return result
138
+ const fallback = EXPORT_TOOLS.get(tool)
139
+ if (!fallback || !result || typeof result !== "object") return result
133
140
  const { base64, ...rest } = result
134
141
  if (!base64) return result
135
- const file = insideOutDir(rest.filename ?? "document.docx")
142
+ const file = insideOutDir(rest.filename ?? fallback)
136
143
  await mkdir(dirname(file), { recursive: true })
137
144
  await writeFile(file, Buffer.from(base64, "base64"))
138
145
  console.error(`[hub] 已写出 ${file}(${Buffer.from(base64, "base64").length} 字节)`)
package/bin/stdio.mjs CHANGED
@@ -15,7 +15,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"
15
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
16
16
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
17
17
 
18
- import { DOC_TOOLS } from "../dist/tools.js"
18
+ import { ALL_TOOLS } from "../dist/tools.js"
19
19
 
20
20
  const argv = process.argv.slice(2)
21
21
  const flag = (name, fallback) => {
@@ -53,7 +53,7 @@ const server = new Server(
53
53
  { capabilities: { tools: {} } },
54
54
  )
55
55
 
56
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: DOC_TOOLS }))
56
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: ALL_TOOLS }))
57
57
 
58
58
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
59
59
  const { name, arguments: args } = req.params
package/dist/bridge.d.ts CHANGED
@@ -1,20 +1,37 @@
1
- import { DocRuntime } from '@4399ywkf/editor/doc-runtime';
2
-
3
1
  /**
4
- * 浏览器侧的桥:把一个 `DocRuntime` 挂到 hub 的 WebSocket 上,
5
- * 让 hub 转来的工具调用落在**用户此刻正在看的那个编辑器实例**上。
2
+ * 浏览器侧的桥:把一个运行时(`DocRuntime` / `SheetRuntime` / 两者的组合)挂到
3
+ * hub 的 WebSocket 上,让 hub 转来的工具调用落在**用户此刻正在看的那个实例**上。
6
4
  *
7
5
  * 这是整套架构的关键约束:AI 改的和用户看的必须是同一个实例。形状照抄腾讯
8
6
  * WorkBuddy 的 editor_sdk(一个进程同时提供编辑器页面和 MCP 端点),
9
7
  * 区别是不用付它那 193MB 原生二进制的赎金。
10
8
  *
11
9
  * ```tsx
12
- * const runtime = useMemo(() => new DocRuntime(), [])
13
- * useEffect(() => attachBridge({ runtime, url: "ws://127.0.0.1:4399/bridge" }), [runtime])
14
- * <NotionEditor onEditorReady={(e) => runtime.setEditor(e)} />
10
+ * const doc = useMemo(() => new DocRuntime(), [])
11
+ * useEffect(() => attachBridge({ runtime: doc, url: "ws://127.0.0.1:4399/bridge" }), [doc])
12
+ * <NotionEditor onEditorReady={(e) => doc.setEditor(e)} />
13
+ * ```
14
+ *
15
+ * 同页有文档 + 表格两个编辑器时,用 `composeRuntimes` 按工具名前缀路由:
16
+ *
17
+ * ```tsx
18
+ * attachBridge({ runtime: composeRuntimes({ doc, sheet }) })
15
19
  * ```
16
20
  */
17
-
21
+ /**
22
+ * 桥能挂的运行时的最小面:一个 `call`。
23
+ *
24
+ * 刻意不 import `DocRuntime` 的类型 —— 桥只需要分发口,import 具体类会把
25
+ * tiptap 的类型面拖进传输层,而 `SheetRuntime`(Univer)就永远接不进来了。
26
+ */
27
+ interface BridgeTarget {
28
+ call(tool: string, args?: Record<string, unknown>): unknown;
29
+ }
30
+ /**
31
+ * 多运行时按工具名前缀路由:`doc_*` → `targets.doc`,`sheet_*` → `targets.sheet`。
32
+ * 打到没挂的前缀时报可读错误(说清这个页面上挂了什么),而不是「未知工具」。
33
+ */
34
+ declare function composeRuntimes(targets: Record<string, BridgeTarget>): BridgeTarget;
18
35
  type BridgeStatus =
19
36
  /** 正在连 hub */
20
37
  "connecting"
@@ -25,7 +42,8 @@ type BridgeStatus =
25
42
  /** 断开 */
26
43
  | "disconnected";
27
44
  interface AttachBridgeOptions {
28
- runtime: DocRuntime;
45
+ /** 工具调用的执行体:`DocRuntime`、`SheetRuntime`,或 `composeRuntimes` 的组合 */
46
+ runtime: BridgeTarget;
29
47
  /** hub 的 WS 地址。默认 `ws://<当前 host>/bridge`。 */
30
48
  url?: string;
31
49
  /** hub 开了 --token 时必须给,会作为 query 参数带上。 */
@@ -58,4 +76,4 @@ interface AttachBridgeOptions {
58
76
  */
59
77
  declare function attachBridge(options: AttachBridgeOptions): () => void;
60
78
 
61
- export { type AttachBridgeOptions, type BridgeStatus, attachBridge };
79
+ export { type AttachBridgeOptions, type BridgeStatus, type BridgeTarget, attachBridge, composeRuntimes };
package/dist/bridge.js CHANGED
@@ -1,3 +1,3 @@
1
- export { attachBridge } from './chunk-AFXUKOKO.js';
1
+ export { attachBridge, composeRuntimes } from './chunk-AHF24VXT.js';
2
2
  //# sourceMappingURL=bridge.js.map
3
3
  //# sourceMappingURL=bridge.js.map
@@ -1,4 +1,17 @@
1
1
  // src/bridge.ts
2
+ function composeRuntimes(targets) {
3
+ return {
4
+ call(tool, args) {
5
+ const prefix = tool.split("_")[0] ?? "";
6
+ const target = targets[prefix];
7
+ if (!target) {
8
+ const available = Object.keys(targets).map((p) => `${p}_*`).join(" 和 ");
9
+ throw new Error(`本页面没有挂 ${prefix}_* 的运行时,可用的工具面:${available}`);
10
+ }
11
+ return target.call(tool, args);
12
+ }
13
+ };
14
+ }
2
15
  function attachBridge(options) {
3
16
  const { runtime, token, onStatus, onCall, reconnect = true } = options;
4
17
  if (typeof WebSocket === "undefined") {
@@ -69,6 +82,6 @@ function defaultUrl() {
69
82
  return `${proto}//${location.host}/bridge`;
70
83
  }
71
84
 
72
- export { attachBridge };
73
- //# sourceMappingURL=chunk-AFXUKOKO.js.map
74
- //# sourceMappingURL=chunk-AFXUKOKO.js.map
85
+ export { attachBridge, composeRuntimes };
86
+ //# sourceMappingURL=chunk-AHF24VXT.js.map
87
+ //# sourceMappingURL=chunk-AHF24VXT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bridge.ts"],"names":[],"mappings":"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","file":"chunk-AHF24VXT.js","sourcesContent":["/**\n * 浏览器侧的桥:把一个运行时(`DocRuntime` / `SheetRuntime` / 两者的组合)挂到\n * hub 的 WebSocket 上,让 hub 转来的工具调用落在**用户此刻正在看的那个实例**上。\n *\n * 这是整套架构的关键约束:AI 改的和用户看的必须是同一个实例。形状照抄腾讯\n * WorkBuddy 的 editor_sdk(一个进程同时提供编辑器页面和 MCP 端点),\n * 区别是不用付它那 193MB 原生二进制的赎金。\n *\n * ```tsx\n * const doc = useMemo(() => new DocRuntime(), [])\n * useEffect(() => attachBridge({ runtime: doc, url: \"ws://127.0.0.1:4399/bridge\" }), [doc])\n * <NotionEditor onEditorReady={(e) => doc.setEditor(e)} />\n * ```\n *\n * 同页有文档 + 表格两个编辑器时,用 `composeRuntimes` 按工具名前缀路由:\n *\n * ```tsx\n * attachBridge({ runtime: composeRuntimes({ doc, sheet }) })\n * ```\n */\n\n/**\n * 桥能挂的运行时的最小面:一个 `call`。\n *\n * 刻意不 import `DocRuntime` 的类型 —— 桥只需要分发口,import 具体类会把\n * tiptap 的类型面拖进传输层,而 `SheetRuntime`(Univer)就永远接不进来了。\n */\nexport interface BridgeTarget {\n call(tool: string, args?: Record<string, unknown>): unknown\n}\n\n/**\n * 多运行时按工具名前缀路由:`doc_*` → `targets.doc`,`sheet_*` → `targets.sheet`。\n * 打到没挂的前缀时报可读错误(说清这个页面上挂了什么),而不是「未知工具」。\n */\nexport function composeRuntimes(targets: Record<string, BridgeTarget>): BridgeTarget {\n return {\n call(tool: string, args?: Record<string, unknown>) {\n const prefix = tool.split(\"_\")[0] ?? \"\"\n const target = targets[prefix]\n if (!target) {\n const available = Object.keys(targets)\n .map((p) => `${p}_*`)\n .join(\" 和 \")\n throw new Error(`本页面没有挂 ${prefix}_* 的运行时,可用的工具面:${available}`)\n }\n return target.call(tool, args)\n },\n }\n}\n\nexport type BridgeStatus =\n /** 正在连 hub */\n | \"connecting\"\n /** 已连上且持有桥 —— 工具调用会打到这个实例 */\n | \"primary\"\n /** 已连上但另一个标签页持有桥 —— 本实例只旁观 */\n | \"observer\"\n /** 断开 */\n | \"disconnected\"\n\nexport interface AttachBridgeOptions {\n /** 工具调用的执行体:`DocRuntime`、`SheetRuntime`,或 `composeRuntimes` 的组合 */\n runtime: BridgeTarget\n /** hub 的 WS 地址。默认 `ws://<当前 host>/bridge`。 */\n url?: string\n /** hub 开了 --token 时必须给,会作为 query 参数带上。 */\n token?: string\n /** 状态变化回调,用来在界面上显示「● MCP 桥已连接」这类提示。 */\n onStatus?: (status: BridgeStatus, detail?: string) => void\n /**\n * 每次工具调用后触发,给业务侧做审计 / 埋点用。\n *\n * 说明:本桥**不提供 AI 编辑的撤销与回滚**。ProseMirror 的 Step 可以 invert,\n * 真要做 `doc_revert_turn` 应该持久化 inverted steps —— 目前没做,\n * 想兜底的话在这个钩子里自己存快照。\n */\n onCall?: (event: { tool: string; args: unknown; result?: unknown; error?: string }) => void\n /** 断线自动重连,默认 true。 */\n reconnect?: boolean\n}\n\ninterface BridgeRequest {\n id?: number\n type?: string\n role?: BridgeStatus\n tool?: string\n args?: Record<string, unknown>\n}\n\n/**\n * 连上 hub,返回 detach 函数。\n *\n * 多标签页:**第一个连上的实例持有桥**,后开的降级为旁观者。这不是偷懒,是这套\n * 架构的固有性质 —— 浏览器执行器模式下「文档」等于「某个页面里的那个 editor 实例」,\n * 两个标签页就是两份互不相干的文档。若让后连的抢走桥,agent 就会对着一个和用户\n * 正在看的不同的实例干活。接了 yjs(文档活在浏览器之外)之后这个问题自然消失。\n */\nexport function attachBridge(options: AttachBridgeOptions): () => void {\n const { runtime, token, onStatus, onCall, reconnect = true } = options\n\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\"attachBridge 只能在浏览器里用:当前环境没有 WebSocket\")\n }\n\n const url = options.url ?? defaultUrl()\n const target = token\n ? `${url}${url.includes(\"?\") ? \"&\" : \"?\"}token=${encodeURIComponent(token)}`\n : url\n\n let socket: WebSocket | null = null\n let retry = 0\n let closed = false\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const status = (s: BridgeStatus, detail?: string) => onStatus?.(s, detail)\n\n const connect = () => {\n if (closed) return\n status(\"connecting\")\n socket = new WebSocket(target)\n\n socket.onopen = () => {\n retry = 0\n status(\"primary\")\n }\n\n socket.onclose = () => {\n status(\"disconnected\")\n if (!reconnect || closed) return\n retry += 1\n timer = setTimeout(connect, Math.min(3000, 300 * retry))\n }\n\n socket.onmessage = (ev: MessageEvent) => {\n let req: BridgeRequest\n try {\n req = JSON.parse(String(ev.data))\n } catch {\n return\n }\n\n if (req.type === \"role\") {\n status(req.role === \"primary\" ? \"primary\" : \"observer\")\n return\n }\n\n const { id, tool, args } = req\n if (id == null || !tool) return\n\n const send = (payload: unknown) => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ id, result: payload }))\n }\n }\n\n // 必须 await:有些操作是异步的,Promise 被 JSON.stringify 会变成 {},\n // 调用方拿到空结果还以为成功了。\n Promise.resolve()\n .then(() => runtime.call(tool, args ?? {}))\n .then((result) => {\n onCall?.({ tool, args, result })\n send(result)\n })\n .catch((e: unknown) => {\n const error = String((e as Error)?.message ?? e)\n onCall?.({ tool, args, error })\n send({ error })\n })\n }\n }\n\n connect()\n\n return () => {\n closed = true\n if (timer) clearTimeout(timer)\n socket?.close()\n socket = null\n }\n}\n\nfunction defaultUrl(): string {\n if (typeof location === \"undefined\") {\n throw new Error(\"没有 location,请显式传 url(如 ws://127.0.0.1:4399/bridge)\")\n }\n const proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\"\n return `${proto}//${location.host}/bridge`\n}\n"]}
@@ -201,7 +201,108 @@ var DOC_TOOL_NAMES = DOC_TOOLS.map((t) => t.name);
201
201
  function getDocTool(name) {
202
202
  return DOC_TOOLS.find((t) => t.name === name);
203
203
  }
204
+ var SHEET_ADDRESSING_NOTE = "定位一律用 A1 引用(如 B2、A1:D20),行列都从 A/1 开始数。写之前先用 sheet_read 看清目标区域现状 —— 盲写会覆盖你没看见的数据。";
205
+ var SHEET_TOOLS = [
206
+ {
207
+ name: "sheet_read",
208
+ description: "读取工作簿概览(每张表的名字和已用范围)+ 指定表的单元格内容(CSV 文本)与公式清单。要写之前必须先调它。区域超过 300×100 会截断并提示,用 range 分段读。" + SHEET_ADDRESSING_NOTE,
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ sheet: { type: "string", description: "工作表 id 或表名,省略用活动表" },
213
+ range: { type: "string", description: 'A1 区域(如 "B2:F40"),省略读整个已用范围' },
214
+ includeFormulas: { type: "boolean", description: "是否附带公式清单,默认 true" }
215
+ },
216
+ additionalProperties: false
217
+ }
218
+ },
219
+ {
220
+ name: "sheet_find",
221
+ description: "全簿检索(单元格的值和公式都搜,大小写不敏感),返回 {sheet, ref, value, formula?},ref 可直接回填给写工具。",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ query: { type: "string", description: "要查找的文本(字面量,不是正则)" },
226
+ limit: { type: "number", description: "最多返回几条,默认 20" }
227
+ },
228
+ required: ["query"],
229
+ additionalProperties: false
230
+ }
231
+ },
232
+ {
233
+ name: "sheet_set_values",
234
+ description: '从 start 起写入一块二维值,批量写入首选(不要拆成很多次单格调用)。以 "=" 开头的字符串按公式写入(如 "=SUM(A1:A9)"),null 清空该格,各行必须等长 —— 整块区域会被覆盖,不想动的格子请缩小区域分多次写。' + SHEET_ADDRESSING_NOTE,
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ sheet: { type: "string", description: "工作表 id 或表名,省略用活动表" },
239
+ start: { type: "string", description: '写入区域左上角,单格 A1 形式(如 "B2")' },
240
+ values: {
241
+ type: "array",
242
+ description: "二维数组,外层是行。元素:字符串/数字/布尔/null",
243
+ items: {
244
+ type: "array",
245
+ items: { type: ["string", "number", "boolean", "null"] }
246
+ }
247
+ }
248
+ },
249
+ required: ["start", "values"],
250
+ additionalProperties: false
251
+ }
252
+ },
253
+ {
254
+ name: "sheet_edit_rows_cols",
255
+ description: "插入 / 删除整行整列。index 是 0-based:insert 在该位置**之前**插入,delete 从该位置开始删 count 个。删除不可撤销地移除数据 —— 删之前先 sheet_read 确认内容。",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: {
259
+ sheet: { type: "string", description: "工作表 id 或表名,省略用活动表" },
260
+ action: {
261
+ type: "string",
262
+ enum: ["insert_rows", "delete_rows", "insert_cols", "delete_cols"]
263
+ },
264
+ index: { type: "number", description: "0-based 行号/列号" },
265
+ count: { type: "number", description: "行数/列数,默认 1" }
266
+ },
267
+ required: ["action", "index"],
268
+ additionalProperties: false
269
+ }
270
+ },
271
+ {
272
+ name: "sheet_manage_sheets",
273
+ description: "工作表管理:add(新建)/ rename(重命名)/ delete(删除,**整表数据一并消失**,必须显式指定 sheet,删之前先跟用户确认)/ activate(切活动表)。返回最新的表清单。",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ action: { type: "string", enum: ["add", "rename", "delete", "activate"] },
278
+ sheet: { type: "string", description: "rename/delete/activate 的目标(id 或表名)" },
279
+ name: { type: "string", description: "add 的表名 / rename 的新名" }
280
+ },
281
+ required: ["action"],
282
+ additionalProperties: false
283
+ }
284
+ },
285
+ {
286
+ name: "sheet_export_xlsx",
287
+ description: "把当前工作簿导出成 .xlsx 落到磁盘,返回文件路径与导出统计。文件写在 hub 的输出目录下,只接受文件名、不接受路径。返回的 warnings 是**导出时丢了什么**(图片、条件格式等插件数据),非空就如实转述给用户,别当成功报。",
288
+ inputSchema: {
289
+ type: "object",
290
+ properties: {
291
+ filename: {
292
+ type: "string",
293
+ description: "文件名,如 `报表.xlsx`。省略则用 工作簿.xlsx。不要带目录"
294
+ }
295
+ },
296
+ additionalProperties: false
297
+ }
298
+ }
299
+ ];
300
+ var SHEET_TOOL_NAMES = SHEET_TOOLS.map((t) => t.name);
301
+ function getSheetTool(name) {
302
+ return SHEET_TOOLS.find((t) => t.name === name);
303
+ }
304
+ var ALL_TOOLS = [...DOC_TOOLS, ...SHEET_TOOLS];
204
305
 
205
- export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool };
206
- //# sourceMappingURL=chunk-P3AOHTTC.js.map
207
- //# sourceMappingURL=chunk-P3AOHTTC.js.map
306
+ export { ALL_TOOLS, DOC_TOOLS, DOC_TOOL_NAMES, SHEET_TOOLS, SHEET_TOOL_NAMES, getDocTool, getSheetTool };
307
+ //# sourceMappingURL=chunk-THBSF3IO.js.map
308
+ //# sourceMappingURL=chunk-THBSF3IO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools.ts"],"names":[],"mappings":"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","file":"chunk-THBSF3IO.js","sourcesContent":["/**\n * 工具面:doc 10 个 + sheet 6 个工具的 name / description / JSON Schema。\n *\n * **这里没有任何传输**。是 MCP、是 HTTP、还是像 jizhi_ai 那样 agent loop 直接调\n * executor,都由上层决定 —— 想自己接的只 import 这个模块就够了:\n *\n * ```ts\n * import { DOC_TOOLS, SHEET_TOOLS, ALL_TOOLS } from \"@4399ywkf/editor-mcp/tools\"\n * ```\n *\n * 为什么这么少:参照系是 BlockNote AI 的 3 个工具,和把 tiptap 每个 command\n * 都包成工具的 `tiptap-apcore`(79 个,GitHub 1 star)。工具面大了模型反而选不准。\n * `doc_stats` 那类对账工具刻意不在这里 —— 它是给人调试用的,`DocRuntime.call`\n * 支持,但不该占模型的注意力。\n *\n * 8 个读写工具 + 2 个导入导出。后两个之所以够格占位置:没有它们,模型能把文档\n * 改好却交付不出去 —— 「改完了,然后呢」是个死胡同。\n */\nimport { FORMATTABLE, MARK_TAG, NODE_TAG, READONLY_TAGS } from \"@4399ywkf/editor/doc-runtime\"\n\nexport interface DocToolSchema {\n type: \"object\"\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n}\n\nexport interface DocTool {\n name: string\n description: string\n inputSchema: DocToolSchema\n}\n\n/**\n * 定位纪律。写进 description 而不是 README —— 和实现同生共死,不会漂。\n *\n * 后半句是必需的:模型带着 Google Docs / 腾讯文档那套 API 的条件反射\n * (改一处就失效一次索引),不明说它会自作聪明地倒序处理、反复重读。\n */\nconst ADDRESSING_NOTE =\n \"定位一律用 node id(读工具返回的 id 属性),逐字复制。\" +\n \"注意:与 Google Docs / 腾讯文档那类 API 不同,本工具面的定位在执行的那一瞬间才解析,\" +\n \"所以你不需要从文档尾部往前处理,也不需要每改一处就重新读一遍。\"\n\n/**\n * 可用标签清单从 doc-runtime 的标签表**生成**,不是手抄的。\n * 编辑器加一个节点类型,这段 description 自动跟着变。\n */\nconst BLOCK_TAGS = [...new Set(Object.values(NODE_TAG))]\n .filter((t) => !READONLY_TAGS.has(t))\n .sort()\n .join(\" \")\nconst INLINE_TAGS = Object.values(MARK_TAG).join(\" \")\n\nexport const DOC_TOOLS: readonly DocTool[] = [\n {\n name: \"doc_read\",\n description:\n \"读取整篇文档,返回 LiteXML(每个块带稳定 id)。要编辑之前必须先调它拿 id。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n format: {\n type: \"string\",\n enum: [\"xml\", \"json\", \"both\"],\n description:\n \"默认 xml。json 是 ProseMirror 原生结构(无损但费 token,约 1.5-2.8 倍),\" +\n \"只在你怀疑 LiteXML 丢了信息时才用。\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"doc_schema\",\n description:\n \"返回本编辑器支持的全部节点类型 / 标记类型 / LiteXML 标签映射 / 哪些类型带稳定 id。\" +\n \"不确定某个结构能不能写、该用什么标签时先查它。这份数据是从运行时 schema 反射出来的,\" +\n \"永远不会和实现漂移。\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"doc_find\",\n description:\n \"全文检索,返回可直接回填给写工具的地址:{nodeId, nodeType, contextBefore, match, contextAfter}。\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"要查找的文本(字面量,不是正则)\" },\n limit: { type: \"number\", description: \"最多返回几条,默认 20\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_get_selection\",\n description:\n \"读取用户此刻在编辑器里选中了什么。返回 selection:null 时表示本轮无选区,不得沿用历史选区。\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"doc_replace_text\",\n description:\n \"块内文本替换,最推荐的细粒度改法。只改命中的那一段,块内其余的格式、链接、\" +\n \"mention 都会保住。可用 nodeIds 限定只在某些块里替换。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n searchText: { type: \"string\" },\n newText: { type: \"string\" },\n nodeIds: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"限定生效范围;省略则全文\",\n },\n replaceAll: {\n type: \"boolean\",\n description: \"默认 false,只替换文档里第一处\",\n },\n },\n required: [\"searchText\", \"newText\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_format_text\",\n description:\n \"给某个块里的一段文字加/去格式(加粗、斜体、下划线、删除线、行内代码、上下标)。\" +\n \"**要改格式请优先用它,不要用 doc_modify_nodes 整块重写** —— \" +\n \"整块重写要求你重述整块内容,你没看见的属性和内联节点会在重述中丢失。\\n\" +\n \"先用 doc_read / doc_find 拿到块的 id 和它的文本,再指定要施加格式的那段文字。\",\n inputSchema: {\n type: \"object\",\n properties: {\n nodeId: { type: \"string\", description: \"目标块的 id\" },\n searchText: { type: \"string\", description: \"块内要施加格式的那段文字(精确匹配)\" },\n marks: {\n type: \"array\",\n items: { type: \"string\", enum: [...FORMATTABLE] },\n description: \"要施加的标记,可多个\",\n },\n remove: { type: \"boolean\", description: \"true 则移除这些标记而不是施加,默认 false\" },\n occurrence: { type: \"number\", description: \"块内第几处匹配,从 1 开始,默认 1\" },\n },\n required: [\"nodeId\", \"searchText\", \"marks\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_modify_nodes\",\n description:\n \"结构化编辑:一次提交多个 insert / remove / modify 操作。\" +\n \"批量写入优先用它,不要拆成很多次单点调用。\\n\" +\n `LiteXML 块标签:${BLOCK_TAGS} h1-h6;未知节点用 <node type=\"x\"/>。\\n` +\n `内联:${INLINE_TAGS} a[href] mark[color] color[v] br。\\n` +\n '例:<code lang=\"ts\">…</code>、<img src=\"…\"/>、<tasks><task checked=\"true\">…</task></tasks>。\\n' +\n \"modify 的 litexml 必须带上要改的那个块的 id 属性。\" +\n \"你没写进 XML 的属性会以原节点为底保留下来(不会被抹掉),所以不必重述你没看见的状态。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n operations: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n action: { type: \"string\", enum: [\"insert\", \"remove\", \"modify\"] },\n afterId: { type: \"string\", description: \"insert:插到这个块之后\" },\n beforeId: { type: \"string\", description: \"insert:插到这个块之前\" },\n id: { type: \"string\", description: \"remove:要删除的块 id\" },\n litexml: {\n type: \"string\",\n description: \"insert/modify 的内容。modify 时必须带 id 属性\",\n },\n },\n required: [\"action\"],\n },\n },\n },\n required: [\"operations\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_table_edit\",\n description:\n \"表格的**结构性**改动(增删行列、合并拆分、切表头)和批量写值。\" +\n \"用 (tableId, row, col) 这种结构坐标定位,row/col 都是 0-based,表头是第 0 行。\\n\" +\n \"注意:单元格里的段落是有 id 的,所以「只改某个格子的文字」直接用 \" +\n \"doc_replace_text / doc_format_text 打那个段落的 id 通常更简单。\",\n inputSchema: {\n type: \"object\",\n properties: {\n tableId: { type: \"string\" },\n action: {\n type: \"string\",\n enum: [\n \"set_cells\",\n \"insert_row_after\",\n \"insert_row_before\",\n \"insert_col_after\",\n \"insert_col_before\",\n \"delete_row\",\n \"delete_col\",\n \"toggle_header_row\",\n \"merge_cells\",\n \"split_cell\",\n ],\n },\n row: {\n type: \"number\",\n description: \"0-based 行号(表头是第 0 行)。除 set_cells 外**必填**,作为锚点单元格\",\n },\n col: { type: \"number\", description: \"0-based 列号。除 set_cells 外**必填**\" },\n values: {\n type: \"array\",\n description: \"set_cells 用:[{row, col, text}]\",\n items: {\n type: \"object\",\n properties: {\n row: { type: \"number\" },\n col: { type: \"number\" },\n text: { type: \"string\" },\n },\n required: [\"row\", \"col\", \"text\"],\n },\n },\n },\n required: [\"tableId\", \"action\"],\n additionalProperties: false,\n },\n },\n\n /* ---------------------------- 导入 / 导出 ---------------------------- *\n * 这两个工具的返回值和参数里**不出现文件内容**。docx 是二进制,一份带图的\n * 动辄几 MB,base64 进上下文会直接把窗口撑爆。所以约定:模型只说路径,\n * 字节在 hub ↔ 浏览器之间走,不经过模型。\n * ------------------------------------------------------------------ */\n {\n name: \"doc_export_docx\",\n description:\n \"把当前文档导出成 .docx 落到磁盘,返回文件路径与导出统计。\" +\n \"文件写在 hub 的输出目录下(启动时 --out-dir 指定,默认 hub 的工作目录),\" +\n \"只接受文件名、不接受路径 —— 想放别处请让用户改 hub 的 --out-dir。\\n\" +\n \"会一并写回导入时保留的页眉页脚与页面设置(前提是这篇文档是本次会话里用 \" +\n \"doc_import_docx 或界面导入进来的)。返回的 warnings 是**导出时丢了什么**,\" +\n \"非空就如实转述给用户,别当成功报。\",\n inputSchema: {\n type: \"object\",\n properties: {\n filename: {\n type: \"string\",\n description: \"文件名,如 `报告.docx`。省略则用 document.docx。不要带目录\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"doc_import_docx\",\n description:\n \"读一份 .docx 灌进编辑器,**整篇替换**当前内容(不是追加,也不可撤销 —— \" +\n \"文档里已有内容时先跟用户确认)。path 相对 hub 的输出目录解析。\\n\" +\n \"返回 recovered(各类样式各救回多少项)和 warnings。导入完要接着编辑的话,\" +\n \"先调 doc_read 拿新的 id —— 旧 id 全部失效。\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: {\n type: \"string\",\n description: \"docx 文件名或相对 hub 输出目录的路径\",\n },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n]\n\nexport const DOC_TOOL_NAMES: readonly string[] = DOC_TOOLS.map((t) => t.name)\n\n/** 单个工具的定义,找不到返回 undefined。 */\nexport function getDocTool(name: string): DocTool | undefined {\n return DOC_TOOLS.find((t) => t.name === name)\n}\n\n/* ================================ sheet ================================ */\n\n/**\n * 表格工具面。执行体是 `@4399ywkf/editor/sheet-runtime` 的 `SheetRuntime`,\n * 和 doc 工具面同一套纪律:面刻意小(6 个)、导出不给模型看字节、\n * 丢失/降级如实进 warnings。\n *\n * 定位用 A1 引用而不是 node id —— 表格的坐标系本来就是稳定的,\n * 不需要 doc 那套「读了才有 id」的仪式;但**写之前先 sheet_read 看清现状**\n * 这条纪律不变:盲写会覆盖你没看见的数据。\n */\nconst SHEET_ADDRESSING_NOTE =\n \"定位一律用 A1 引用(如 B2、A1:D20),行列都从 A/1 开始数。\" +\n \"写之前先用 sheet_read 看清目标区域现状 —— 盲写会覆盖你没看见的数据。\"\n\nexport const SHEET_TOOLS: readonly DocTool[] = [\n {\n name: \"sheet_read\",\n description:\n \"读取工作簿概览(每张表的名字和已用范围)+ 指定表的单元格内容(CSV 文本)与公式清单。\" +\n \"要写之前必须先调它。区域超过 300×100 会截断并提示,用 range 分段读。\" +\n SHEET_ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n sheet: { type: \"string\", description: \"工作表 id 或表名,省略用活动表\" },\n range: { type: \"string\", description: 'A1 区域(如 \"B2:F40\"),省略读整个已用范围' },\n includeFormulas: { type: \"boolean\", description: \"是否附带公式清单,默认 true\" },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"sheet_find\",\n description:\n \"全簿检索(单元格的值和公式都搜,大小写不敏感),返回 {sheet, ref, value, formula?},\" +\n \"ref 可直接回填给写工具。\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"要查找的文本(字面量,不是正则)\" },\n limit: { type: \"number\", description: \"最多返回几条,默认 20\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"sheet_set_values\",\n description:\n \"从 start 起写入一块二维值,批量写入首选(不要拆成很多次单格调用)。\" +\n '以 \"=\" 开头的字符串按公式写入(如 \"=SUM(A1:A9)\"),null 清空该格,' +\n \"各行必须等长 —— 整块区域会被覆盖,不想动的格子请缩小区域分多次写。\" +\n SHEET_ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n sheet: { type: \"string\", description: \"工作表 id 或表名,省略用活动表\" },\n start: { type: \"string\", description: '写入区域左上角,单格 A1 形式(如 \"B2\")' },\n values: {\n type: \"array\",\n description: \"二维数组,外层是行。元素:字符串/数字/布尔/null\",\n items: {\n type: \"array\",\n items: { type: [\"string\", \"number\", \"boolean\", \"null\"] },\n },\n },\n },\n required: [\"start\", \"values\"],\n additionalProperties: false,\n },\n },\n {\n name: \"sheet_edit_rows_cols\",\n description:\n \"插入 / 删除整行整列。index 是 0-based:insert 在该位置**之前**插入,\" +\n \"delete 从该位置开始删 count 个。删除不可撤销地移除数据 —— 删之前先 sheet_read 确认内容。\",\n inputSchema: {\n type: \"object\",\n properties: {\n sheet: { type: \"string\", description: \"工作表 id 或表名,省略用活动表\" },\n action: {\n type: \"string\",\n enum: [\"insert_rows\", \"delete_rows\", \"insert_cols\", \"delete_cols\"],\n },\n index: { type: \"number\", description: \"0-based 行号/列号\" },\n count: { type: \"number\", description: \"行数/列数,默认 1\" },\n },\n required: [\"action\", \"index\"],\n additionalProperties: false,\n },\n },\n {\n name: \"sheet_manage_sheets\",\n description:\n \"工作表管理:add(新建)/ rename(重命名)/ delete(删除,**整表数据一并消失**,\" +\n \"必须显式指定 sheet,删之前先跟用户确认)/ activate(切活动表)。返回最新的表清单。\",\n inputSchema: {\n type: \"object\",\n properties: {\n action: { type: \"string\", enum: [\"add\", \"rename\", \"delete\", \"activate\"] },\n sheet: { type: \"string\", description: \"rename/delete/activate 的目标(id 或表名)\" },\n name: { type: \"string\", description: \"add 的表名 / rename 的新名\" },\n },\n required: [\"action\"],\n additionalProperties: false,\n },\n },\n {\n name: \"sheet_export_xlsx\",\n description:\n \"把当前工作簿导出成 .xlsx 落到磁盘,返回文件路径与导出统计。\" +\n \"文件写在 hub 的输出目录下,只接受文件名、不接受路径。\" +\n \"返回的 warnings 是**导出时丢了什么**(图片、条件格式等插件数据),\" +\n \"非空就如实转述给用户,别当成功报。\",\n inputSchema: {\n type: \"object\",\n properties: {\n filename: {\n type: \"string\",\n description: \"文件名,如 `报表.xlsx`。省略则用 工作簿.xlsx。不要带目录\",\n },\n },\n additionalProperties: false,\n },\n },\n]\n\nexport const SHEET_TOOL_NAMES: readonly string[] = SHEET_TOOLS.map((t) => t.name)\n\n/** 单个 sheet 工具的定义,找不到返回 undefined。 */\nexport function getSheetTool(name: string): DocTool | undefined {\n return SHEET_TOOLS.find((t) => t.name === name)\n}\n\n/** 全量工具面:宿主同时挂了 DocRuntime 和 SheetRuntime 时用它注册。 */\nexport const ALL_TOOLS: readonly DocTool[] = [...DOC_TOOLS, ...SHEET_TOOLS]\n"]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
- export { AttachBridgeOptions, BridgeStatus, attachBridge } from './bridge.js';
2
- export { DOC_TOOLS, DOC_TOOL_NAMES, DocTool, DocToolSchema, getDocTool } from './tools.js';
3
- import '@4399ywkf/editor/doc-runtime';
1
+ export { AttachBridgeOptions, BridgeStatus, BridgeTarget, attachBridge, composeRuntimes } from './bridge.js';
2
+ export { ALL_TOOLS, DOC_TOOLS, DOC_TOOL_NAMES, DocTool, DocToolSchema, SHEET_TOOLS, SHEET_TOOL_NAMES, getDocTool, getSheetTool } from './tools.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool } from './chunk-P3AOHTTC.js';
2
- export { attachBridge } from './chunk-AFXUKOKO.js';
1
+ export { ALL_TOOLS, DOC_TOOLS, DOC_TOOL_NAMES, SHEET_TOOLS, SHEET_TOOL_NAMES, getDocTool, getSheetTool } from './chunk-THBSF3IO.js';
2
+ export { attachBridge, composeRuntimes } from './chunk-AHF24VXT.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
package/dist/tools.d.ts CHANGED
@@ -13,5 +13,11 @@ declare const DOC_TOOLS: readonly DocTool[];
13
13
  declare const DOC_TOOL_NAMES: readonly string[];
14
14
  /** 单个工具的定义,找不到返回 undefined。 */
15
15
  declare function getDocTool(name: string): DocTool | undefined;
16
+ declare const SHEET_TOOLS: readonly DocTool[];
17
+ declare const SHEET_TOOL_NAMES: readonly string[];
18
+ /** 单个 sheet 工具的定义,找不到返回 undefined。 */
19
+ declare function getSheetTool(name: string): DocTool | undefined;
20
+ /** 全量工具面:宿主同时挂了 DocRuntime 和 SheetRuntime 时用它注册。 */
21
+ declare const ALL_TOOLS: readonly DocTool[];
16
22
 
17
- export { DOC_TOOLS, DOC_TOOL_NAMES, type DocTool, type DocToolSchema, getDocTool };
23
+ export { ALL_TOOLS, DOC_TOOLS, DOC_TOOL_NAMES, type DocTool, type DocToolSchema, SHEET_TOOLS, SHEET_TOOL_NAMES, getDocTool, getSheetTool };
package/dist/tools.js CHANGED
@@ -1,3 +1,3 @@
1
- export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool } from './chunk-P3AOHTTC.js';
1
+ export { ALL_TOOLS, DOC_TOOLS, DOC_TOOL_NAMES, SHEET_TOOLS, SHEET_TOOL_NAMES, getDocTool, getSheetTool } from './chunk-THBSF3IO.js';
2
2
  //# sourceMappingURL=tools.js.map
3
3
  //# sourceMappingURL=tools.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4399ywkf/editor-mcp",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "把 @4399ywkf/editor 的结构化编辑面接出去:工具定义(transport 无关)+ 浏览器桥 + hub + stdio MCP server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.30.0",
33
33
  "ws": "^8.21.3",
34
- "@4399ywkf/editor": "^0.6.0"
34
+ "@4399ywkf/editor": "^0.7.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/bridge.ts"],"names":[],"mappings":"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","file":"chunk-AFXUKOKO.js","sourcesContent":["/**\n * 浏览器侧的桥:把一个 `DocRuntime` 挂到 hub 的 WebSocket 上,\n * 让 hub 转来的工具调用落在**用户此刻正在看的那个编辑器实例**上。\n *\n * 这是整套架构的关键约束:AI 改的和用户看的必须是同一个实例。形状照抄腾讯\n * WorkBuddy 的 editor_sdk(一个进程同时提供编辑器页面和 MCP 端点),\n * 区别是不用付它那 193MB 原生二进制的赎金。\n *\n * ```tsx\n * const runtime = useMemo(() => new DocRuntime(), [])\n * useEffect(() => attachBridge({ runtime, url: \"ws://127.0.0.1:4399/bridge\" }), [runtime])\n * <NotionEditor onEditorReady={(e) => runtime.setEditor(e)} />\n * ```\n */\nimport type { DocRuntime } from \"@4399ywkf/editor/doc-runtime\"\n\nexport type BridgeStatus =\n /** 正在连 hub */\n | \"connecting\"\n /** 已连上且持有桥 —— 工具调用会打到这个实例 */\n | \"primary\"\n /** 已连上但另一个标签页持有桥 —— 本实例只旁观 */\n | \"observer\"\n /** 断开 */\n | \"disconnected\"\n\nexport interface AttachBridgeOptions {\n runtime: DocRuntime\n /** hub 的 WS 地址。默认 `ws://<当前 host>/bridge`。 */\n url?: string\n /** hub 开了 --token 时必须给,会作为 query 参数带上。 */\n token?: string\n /** 状态变化回调,用来在界面上显示「● MCP 桥已连接」这类提示。 */\n onStatus?: (status: BridgeStatus, detail?: string) => void\n /**\n * 每次工具调用后触发,给业务侧做审计 / 埋点用。\n *\n * 说明:本桥**不提供 AI 编辑的撤销与回滚**。ProseMirror 的 Step 可以 invert,\n * 真要做 `doc_revert_turn` 应该持久化 inverted steps —— 目前没做,\n * 想兜底的话在这个钩子里自己存快照。\n */\n onCall?: (event: { tool: string; args: unknown; result?: unknown; error?: string }) => void\n /** 断线自动重连,默认 true。 */\n reconnect?: boolean\n}\n\ninterface BridgeRequest {\n id?: number\n type?: string\n role?: BridgeStatus\n tool?: string\n args?: Record<string, unknown>\n}\n\n/**\n * 连上 hub,返回 detach 函数。\n *\n * 多标签页:**第一个连上的实例持有桥**,后开的降级为旁观者。这不是偷懒,是这套\n * 架构的固有性质 —— 浏览器执行器模式下「文档」等于「某个页面里的那个 editor 实例」,\n * 两个标签页就是两份互不相干的文档。若让后连的抢走桥,agent 就会对着一个和用户\n * 正在看的不同的实例干活。接了 yjs(文档活在浏览器之外)之后这个问题自然消失。\n */\nexport function attachBridge(options: AttachBridgeOptions): () => void {\n const { runtime, token, onStatus, onCall, reconnect = true } = options\n\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\"attachBridge 只能在浏览器里用:当前环境没有 WebSocket\")\n }\n\n const url = options.url ?? defaultUrl()\n const target = token\n ? `${url}${url.includes(\"?\") ? \"&\" : \"?\"}token=${encodeURIComponent(token)}`\n : url\n\n let socket: WebSocket | null = null\n let retry = 0\n let closed = false\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const status = (s: BridgeStatus, detail?: string) => onStatus?.(s, detail)\n\n const connect = () => {\n if (closed) return\n status(\"connecting\")\n socket = new WebSocket(target)\n\n socket.onopen = () => {\n retry = 0\n status(\"primary\")\n }\n\n socket.onclose = () => {\n status(\"disconnected\")\n if (!reconnect || closed) return\n retry += 1\n timer = setTimeout(connect, Math.min(3000, 300 * retry))\n }\n\n socket.onmessage = (ev: MessageEvent) => {\n let req: BridgeRequest\n try {\n req = JSON.parse(String(ev.data))\n } catch {\n return\n }\n\n if (req.type === \"role\") {\n status(req.role === \"primary\" ? \"primary\" : \"observer\")\n return\n }\n\n const { id, tool, args } = req\n if (id == null || !tool) return\n\n const send = (payload: unknown) => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ id, result: payload }))\n }\n }\n\n // 必须 await:有些操作是异步的,Promise 被 JSON.stringify 会变成 {},\n // 调用方拿到空结果还以为成功了。\n Promise.resolve()\n .then(() => runtime.call(tool, args ?? {}))\n .then((result) => {\n onCall?.({ tool, args, result })\n send(result)\n })\n .catch((e: unknown) => {\n const error = String((e as Error)?.message ?? e)\n onCall?.({ tool, args, error })\n send({ error })\n })\n }\n }\n\n connect()\n\n return () => {\n closed = true\n if (timer) clearTimeout(timer)\n socket?.close()\n socket = null\n }\n}\n\nfunction defaultUrl(): string {\n if (typeof location === \"undefined\") {\n throw new Error(\"没有 location,请显式传 url(如 ws://127.0.0.1:4399/bridge)\")\n }\n const proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\"\n return `${proto}//${location.host}/bridge`\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tools.ts"],"names":[],"mappings":"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","file":"chunk-P3AOHTTC.js","sourcesContent":["/**\n * 工具面:10 个工具的 name / description / JSON Schema。\n *\n * **这里没有任何传输**。是 MCP、是 HTTP、还是像 jizhi_ai 那样 agent loop 直接调\n * executor,都由上层决定 —— 想自己接的只 import 这个模块就够了:\n *\n * ```ts\n * import { DOC_TOOLS } from \"@4399ywkf/editor-mcp/tools\"\n * ```\n *\n * 为什么这么少:参照系是 BlockNote AI 的 3 个工具,和把 tiptap 每个 command\n * 都包成工具的 `tiptap-apcore`(79 个,GitHub 1 star)。工具面大了模型反而选不准。\n * `doc_stats` 那类对账工具刻意不在这里 —— 它是给人调试用的,`DocRuntime.call`\n * 支持,但不该占模型的注意力。\n *\n * 8 个读写工具 + 2 个导入导出。后两个之所以够格占位置:没有它们,模型能把文档\n * 改好却交付不出去 —— 「改完了,然后呢」是个死胡同。\n */\nimport { FORMATTABLE, MARK_TAG, NODE_TAG, READONLY_TAGS } from \"@4399ywkf/editor/doc-runtime\"\n\nexport interface DocToolSchema {\n type: \"object\"\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n}\n\nexport interface DocTool {\n name: string\n description: string\n inputSchema: DocToolSchema\n}\n\n/**\n * 定位纪律。写进 description 而不是 README —— 和实现同生共死,不会漂。\n *\n * 后半句是必需的:模型带着 Google Docs / 腾讯文档那套 API 的条件反射\n * (改一处就失效一次索引),不明说它会自作聪明地倒序处理、反复重读。\n */\nconst ADDRESSING_NOTE =\n \"定位一律用 node id(读工具返回的 id 属性),逐字复制。\" +\n \"注意:与 Google Docs / 腾讯文档那类 API 不同,本工具面的定位在执行的那一瞬间才解析,\" +\n \"所以你不需要从文档尾部往前处理,也不需要每改一处就重新读一遍。\"\n\n/**\n * 可用标签清单从 doc-runtime 的标签表**生成**,不是手抄的。\n * 编辑器加一个节点类型,这段 description 自动跟着变。\n */\nconst BLOCK_TAGS = [...new Set(Object.values(NODE_TAG))]\n .filter((t) => !READONLY_TAGS.has(t))\n .sort()\n .join(\" \")\nconst INLINE_TAGS = Object.values(MARK_TAG).join(\" \")\n\nexport const DOC_TOOLS: readonly DocTool[] = [\n {\n name: \"doc_read\",\n description:\n \"读取整篇文档,返回 LiteXML(每个块带稳定 id)。要编辑之前必须先调它拿 id。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n format: {\n type: \"string\",\n enum: [\"xml\", \"json\", \"both\"],\n description:\n \"默认 xml。json 是 ProseMirror 原生结构(无损但费 token,约 1.5-2.8 倍),\" +\n \"只在你怀疑 LiteXML 丢了信息时才用。\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"doc_schema\",\n description:\n \"返回本编辑器支持的全部节点类型 / 标记类型 / LiteXML 标签映射 / 哪些类型带稳定 id。\" +\n \"不确定某个结构能不能写、该用什么标签时先查它。这份数据是从运行时 schema 反射出来的,\" +\n \"永远不会和实现漂移。\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"doc_find\",\n description:\n \"全文检索,返回可直接回填给写工具的地址:{nodeId, nodeType, contextBefore, match, contextAfter}。\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"要查找的文本(字面量,不是正则)\" },\n limit: { type: \"number\", description: \"最多返回几条,默认 20\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_get_selection\",\n description:\n \"读取用户此刻在编辑器里选中了什么。返回 selection:null 时表示本轮无选区,不得沿用历史选区。\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"doc_replace_text\",\n description:\n \"块内文本替换,最推荐的细粒度改法。只改命中的那一段,块内其余的格式、链接、\" +\n \"mention 都会保住。可用 nodeIds 限定只在某些块里替换。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n searchText: { type: \"string\" },\n newText: { type: \"string\" },\n nodeIds: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"限定生效范围;省略则全文\",\n },\n replaceAll: {\n type: \"boolean\",\n description: \"默认 false,只替换文档里第一处\",\n },\n },\n required: [\"searchText\", \"newText\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_format_text\",\n description:\n \"给某个块里的一段文字加/去格式(加粗、斜体、下划线、删除线、行内代码、上下标)。\" +\n \"**要改格式请优先用它,不要用 doc_modify_nodes 整块重写** —— \" +\n \"整块重写要求你重述整块内容,你没看见的属性和内联节点会在重述中丢失。\\n\" +\n \"先用 doc_read / doc_find 拿到块的 id 和它的文本,再指定要施加格式的那段文字。\",\n inputSchema: {\n type: \"object\",\n properties: {\n nodeId: { type: \"string\", description: \"目标块的 id\" },\n searchText: { type: \"string\", description: \"块内要施加格式的那段文字(精确匹配)\" },\n marks: {\n type: \"array\",\n items: { type: \"string\", enum: [...FORMATTABLE] },\n description: \"要施加的标记,可多个\",\n },\n remove: { type: \"boolean\", description: \"true 则移除这些标记而不是施加,默认 false\" },\n occurrence: { type: \"number\", description: \"块内第几处匹配,从 1 开始,默认 1\" },\n },\n required: [\"nodeId\", \"searchText\", \"marks\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_modify_nodes\",\n description:\n \"结构化编辑:一次提交多个 insert / remove / modify 操作。\" +\n \"批量写入优先用它,不要拆成很多次单点调用。\\n\" +\n `LiteXML 块标签:${BLOCK_TAGS} h1-h6;未知节点用 <node type=\"x\"/>。\\n` +\n `内联:${INLINE_TAGS} a[href] mark[color] color[v] br。\\n` +\n '例:<code lang=\"ts\">…</code>、<img src=\"…\"/>、<tasks><task checked=\"true\">…</task></tasks>。\\n' +\n \"modify 的 litexml 必须带上要改的那个块的 id 属性。\" +\n \"你没写进 XML 的属性会以原节点为底保留下来(不会被抹掉),所以不必重述你没看见的状态。\" +\n ADDRESSING_NOTE,\n inputSchema: {\n type: \"object\",\n properties: {\n operations: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n action: { type: \"string\", enum: [\"insert\", \"remove\", \"modify\"] },\n afterId: { type: \"string\", description: \"insert:插到这个块之后\" },\n beforeId: { type: \"string\", description: \"insert:插到这个块之前\" },\n id: { type: \"string\", description: \"remove:要删除的块 id\" },\n litexml: {\n type: \"string\",\n description: \"insert/modify 的内容。modify 时必须带 id 属性\",\n },\n },\n required: [\"action\"],\n },\n },\n },\n required: [\"operations\"],\n additionalProperties: false,\n },\n },\n {\n name: \"doc_table_edit\",\n description:\n \"表格的**结构性**改动(增删行列、合并拆分、切表头)和批量写值。\" +\n \"用 (tableId, row, col) 这种结构坐标定位,row/col 都是 0-based,表头是第 0 行。\\n\" +\n \"注意:单元格里的段落是有 id 的,所以「只改某个格子的文字」直接用 \" +\n \"doc_replace_text / doc_format_text 打那个段落的 id 通常更简单。\",\n inputSchema: {\n type: \"object\",\n properties: {\n tableId: { type: \"string\" },\n action: {\n type: \"string\",\n enum: [\n \"set_cells\",\n \"insert_row_after\",\n \"insert_row_before\",\n \"insert_col_after\",\n \"insert_col_before\",\n \"delete_row\",\n \"delete_col\",\n \"toggle_header_row\",\n \"merge_cells\",\n \"split_cell\",\n ],\n },\n row: {\n type: \"number\",\n description: \"0-based 行号(表头是第 0 行)。除 set_cells 外**必填**,作为锚点单元格\",\n },\n col: { type: \"number\", description: \"0-based 列号。除 set_cells 外**必填**\" },\n values: {\n type: \"array\",\n description: \"set_cells 用:[{row, col, text}]\",\n items: {\n type: \"object\",\n properties: {\n row: { type: \"number\" },\n col: { type: \"number\" },\n text: { type: \"string\" },\n },\n required: [\"row\", \"col\", \"text\"],\n },\n },\n },\n required: [\"tableId\", \"action\"],\n additionalProperties: false,\n },\n },\n\n /* ---------------------------- 导入 / 导出 ---------------------------- *\n * 这两个工具的返回值和参数里**不出现文件内容**。docx 是二进制,一份带图的\n * 动辄几 MB,base64 进上下文会直接把窗口撑爆。所以约定:模型只说路径,\n * 字节在 hub ↔ 浏览器之间走,不经过模型。\n * ------------------------------------------------------------------ */\n {\n name: \"doc_export_docx\",\n description:\n \"把当前文档导出成 .docx 落到磁盘,返回文件路径与导出统计。\" +\n \"文件写在 hub 的输出目录下(启动时 --out-dir 指定,默认 hub 的工作目录),\" +\n \"只接受文件名、不接受路径 —— 想放别处请让用户改 hub 的 --out-dir。\\n\" +\n \"会一并写回导入时保留的页眉页脚与页面设置(前提是这篇文档是本次会话里用 \" +\n \"doc_import_docx 或界面导入进来的)。返回的 warnings 是**导出时丢了什么**,\" +\n \"非空就如实转述给用户,别当成功报。\",\n inputSchema: {\n type: \"object\",\n properties: {\n filename: {\n type: \"string\",\n description: \"文件名,如 `报告.docx`。省略则用 document.docx。不要带目录\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"doc_import_docx\",\n description:\n \"读一份 .docx 灌进编辑器,**整篇替换**当前内容(不是追加,也不可撤销 —— \" +\n \"文档里已有内容时先跟用户确认)。path 相对 hub 的输出目录解析。\\n\" +\n \"返回 recovered(各类样式各救回多少项)和 warnings。导入完要接着编辑的话,\" +\n \"先调 doc_read 拿新的 id —— 旧 id 全部失效。\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: {\n type: \"string\",\n description: \"docx 文件名或相对 hub 输出目录的路径\",\n },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n]\n\nexport const DOC_TOOL_NAMES: readonly string[] = DOC_TOOLS.map((t) => t.name)\n\n/** 单个工具的定义,找不到返回 undefined。 */\nexport function getDocTool(name: string): DocTool | undefined {\n return DOC_TOOLS.find((t) => t.name === name)\n}\n"]}