@4399ywkf/editor-mcp 0.1.0 → 0.2.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 +27 -3
- package/bin/hub.mjs +61 -4
- package/dist/{chunk-TUKXSXAN.js → chunk-P3AOHTTC.js} +36 -2
- package/dist/chunk-P3AOHTTC.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/tools.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TUKXSXAN.js.map +0 -1
package/README.md
CHANGED
|
@@ -74,7 +74,8 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
|
|
|
74
74
|
|
|
75
75
|
默认只听 `127.0.0.1`。`--token` 可选但建议开:这个口的权限是「以用户身份重写他正在
|
|
76
76
|
编辑的文档」,不开的话本机任何进程都能调。`--static <dir>` 可以顺便托管一个页面,
|
|
77
|
-
|
|
77
|
+
本地调试方便。`--out-dir <dir>` 指定 docx 的读写目录(默认 hub 的工作目录),
|
|
78
|
+
`doc_export_docx` / `doc_import_docx` 只能碰这个目录里的文件。
|
|
78
79
|
|
|
79
80
|
**3. 注册 MCP**
|
|
80
81
|
|
|
@@ -93,7 +94,7 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
|
|
|
93
94
|
> codex `exec` 下需要 `--dangerously-bypass-approvals-and-sandbox`:`approval: never`
|
|
94
95
|
> 会把 MCP 调用直接判成 "user cancelled"。交互式 `codex` 里可以逐次批准。
|
|
95
96
|
|
|
96
|
-
## 工具(
|
|
97
|
+
## 工具(10 个)
|
|
97
98
|
|
|
98
99
|
| 工具 | 用途 |
|
|
99
100
|
|---|---|
|
|
@@ -105,13 +106,36 @@ npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)
|
|
|
105
106
|
| `doc_format_text` | 加/去 bold·italic·underline·strike·code·sup·sub(改格式首选) |
|
|
106
107
|
| `doc_modify_nodes` | 结构化 insert / remove / modify |
|
|
107
108
|
| `doc_table_edit` | 表格:增删行列、合并拆分、批量写值 |
|
|
109
|
+
| `doc_export_docx` | 导出成 .docx 落盘,返回路径 + 统计 + 警告 |
|
|
110
|
+
| `doc_import_docx` | 读一份 .docx 灌进编辑器(**整篇替换**) |
|
|
108
111
|
|
|
109
112
|
刻意保持小。参照系:BlockNote AI 只有 3 个工具;把 tiptap 每个 command 都包成工具的
|
|
110
|
-
`tiptap-apcore` 做到 79 个,GitHub 1 star
|
|
113
|
+
`tiptap-apcore` 做到 79 个,GitHub 1 star。后两个之所以够格占位置:没有它们,
|
|
114
|
+
模型能把文档改好却交付不出去 —— 「改完了,然后呢」是个死胡同。
|
|
111
115
|
|
|
112
116
|
工具描述里的可用标签清单是从 runtime 的标签表**生成**的,不是手抄的 —— 编辑器加一个
|
|
113
117
|
节点类型,description 自动跟着变(`src/tools.test.ts` 守这条)。
|
|
114
118
|
|
|
119
|
+
### docx 的字节不进模型上下文
|
|
120
|
+
|
|
121
|
+
`doc_export_docx` / `doc_import_docx` 的参数和返回值里**没有文件内容**,只有路径:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
模型 ──path──▶ hub ──base64──▶ 浏览器(真正的 serializeDocx / parseDocx)
|
|
125
|
+
模型 ◀──path── hub ◀──base64── 浏览器
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
一份带图的 docx 动辄几 MB,base64 之后更大,塞进上下文会直接把窗口撑爆。所以字节
|
|
129
|
+
只在 hub ↔ 浏览器之间走一趟,hub 负责落盘 / 读盘。这是 hub 唯一一处不"纯中继"的地方。
|
|
130
|
+
|
|
131
|
+
文件钉在 `--out-dir`(默认 hub 的工作目录)里,`../` 和绝对路径一律拒 —— 那两个
|
|
132
|
+
路径参数是**模型给的**,「能读写用户整块磁盘」和「能改用户正在编辑的文档」是两个
|
|
133
|
+
量级的权限。
|
|
134
|
+
|
|
135
|
+
前提:编辑器实例得注册了 `ExportDocx` / `ImportDocx` 扩展(从
|
|
136
|
+
`@4399ywkf/editor/docx` 引入,通过 `<NotionEditor extensions={[...]}>` 注入)。
|
|
137
|
+
没注册时工具会报错并说明怎么装,而不是静默失败。
|
|
138
|
+
|
|
115
139
|
## 两条硬规则
|
|
116
140
|
|
|
117
141
|
LiteXML 对 ProseMirror JSON 是**有损压缩**(省 1.55x 以上 token),所以:
|
package/bin/hub.mjs
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
* 结果送回来。文档活在浏览器那个 editor 实例里(这也带来两个已知限制,见 README)。
|
|
12
12
|
*
|
|
13
13
|
* ywkf-editor-hub [--port 4399] [--host 127.0.0.1] [--static ./dist] [--token xxx]
|
|
14
|
+
* [--out-dir .]
|
|
15
|
+
*
|
|
16
|
+
* 唯一一处不"纯"的地方:doc_export_docx / doc_import_docx 的文件内容由 hub 落盘 /
|
|
17
|
+
* 读盘,模型两头只看到路径。docx 是二进制,base64 进上下文会把窗口撑爆。
|
|
14
18
|
*/
|
|
15
19
|
import { createServer } from "node:http"
|
|
16
|
-
import { readFile } from "node:fs/promises"
|
|
17
|
-
import { extname, join, normalize, resolve } from "node:path"
|
|
20
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
21
|
+
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path"
|
|
18
22
|
import { WebSocketServer } from "ws"
|
|
19
23
|
|
|
20
24
|
/* ------------------------------ 参数 ------------------------------ */
|
|
@@ -34,6 +38,13 @@ const HOST = flag("host", process.env.HOST ?? "127.0.0.1")
|
|
|
34
38
|
const STATIC = flag("static", process.env.EDITOR_MCP_STATIC ?? null)
|
|
35
39
|
const TOKEN = flag("token", process.env.EDITOR_MCP_TOKEN ?? null)
|
|
36
40
|
const TIMEOUT = Number(flag("timeout", process.env.EDITOR_MCP_TIMEOUT ?? 15_000))
|
|
41
|
+
/**
|
|
42
|
+
* doc_export_docx / doc_import_docx 的文件落在这儿。
|
|
43
|
+
*
|
|
44
|
+
* 这两个工具的路径参数是**模型给的**,所以一律钉在这个目录里 —— 越界直接拒。
|
|
45
|
+
* 模型能读写用户整块磁盘和「能改用户正在编辑的文档」是两个量级的权限。
|
|
46
|
+
*/
|
|
47
|
+
const OUT_DIR = resolve(flag("out-dir", process.env.EDITOR_MCP_OUT_DIR ?? process.cwd()))
|
|
37
48
|
|
|
38
49
|
const MIME = {
|
|
39
50
|
".html": "text/html; charset=utf-8",
|
|
@@ -84,6 +95,50 @@ function callBrowser(tool, args, timeoutMs) {
|
|
|
84
95
|
})
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
/* --------------------------- 文件旁路 --------------------------- *
|
|
99
|
+
* docx 是二进制,而 MCP 的返回值会原样进模型上下文 —— 一份带图的文档几 MB,
|
|
100
|
+
* base64 之后更大,塞进去直接把窗口撑爆。所以字节只在 hub ↔ 浏览器之间走,
|
|
101
|
+
* 模型两头看到的都只是路径。
|
|
102
|
+
* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
/** 把模型给的名字钉在 OUT_DIR 里;越界(../、绝对路径)一律拒 */
|
|
105
|
+
function insideOutDir(name) {
|
|
106
|
+
const raw = String(name ?? "").trim()
|
|
107
|
+
if (!raw) throw new Error("缺少文件名")
|
|
108
|
+
const rel = relative(OUT_DIR, resolve(OUT_DIR, raw))
|
|
109
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
110
|
+
throw new Error(`路径越界:只能读写 hub 输出目录(${OUT_DIR})下的文件,收到 "${raw}"`)
|
|
111
|
+
}
|
|
112
|
+
return join(OUT_DIR, rel)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 调用前:把 doc_import_docx 的 path 读成 base64,浏览器那头只认字节 */
|
|
116
|
+
async function beforeCall(tool, args) {
|
|
117
|
+
if (tool !== "doc_import_docx") return args
|
|
118
|
+
if (!args?.path) throw new Error("doc_import_docx 缺少 path")
|
|
119
|
+
const file = insideOutDir(args.path)
|
|
120
|
+
let buf
|
|
121
|
+
try {
|
|
122
|
+
buf = await readFile(file)
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(`读不到文件:${file}`)
|
|
125
|
+
}
|
|
126
|
+
const { path: _dropped, ...rest } = args ?? {}
|
|
127
|
+
return { ...rest, base64: buf.toString("base64") }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 调用后:把 doc_export_docx 回来的 base64 落盘,只把路径交给模型 */
|
|
131
|
+
async function afterCall(tool, result) {
|
|
132
|
+
if (tool !== "doc_export_docx" || !result || typeof result !== "object") return result
|
|
133
|
+
const { base64, ...rest } = result
|
|
134
|
+
if (!base64) return result
|
|
135
|
+
const file = insideOutDir(rest.filename ?? "document.docx")
|
|
136
|
+
await mkdir(dirname(file), { recursive: true })
|
|
137
|
+
await writeFile(file, Buffer.from(base64, "base64"))
|
|
138
|
+
console.error(`[hub] 已写出 ${file}(${Buffer.from(base64, "base64").length} 字节)`)
|
|
139
|
+
return { ...rest, path: file }
|
|
140
|
+
}
|
|
141
|
+
|
|
87
142
|
function readBody(req) {
|
|
88
143
|
return new Promise((resolve, reject) => {
|
|
89
144
|
const chunks = []
|
|
@@ -128,8 +183,9 @@ const server = createServer(async (req, res) => {
|
|
|
128
183
|
}
|
|
129
184
|
try {
|
|
130
185
|
const { tool, args, timeout } = JSON.parse(await readBody(req))
|
|
131
|
-
const
|
|
132
|
-
|
|
186
|
+
const prepared = await beforeCall(tool, args)
|
|
187
|
+
const raw = await callBrowser(tool, prepared, Number(timeout) || TIMEOUT)
|
|
188
|
+
json(200, { ok: true, result: await afterCall(tool, raw) })
|
|
133
189
|
} catch (e) {
|
|
134
190
|
// 故意返回 200 + ok:false:调用方是 MCP 进程,它要的是错误文本本身
|
|
135
191
|
json(200, { ok: false, error: String(e?.message ?? e) })
|
|
@@ -213,5 +269,6 @@ server.listen(PORT, HOST, () => {
|
|
|
213
269
|
console.error(`[hub] 浏览器桥 ws://${HOST}:${port}/bridge`)
|
|
214
270
|
console.error(`[hub] 转发口 POST http://${HOST}:${port}/api/call`)
|
|
215
271
|
if (staticRoot) console.error(`[hub] 静态页 ${staticRoot}`)
|
|
272
|
+
console.error(`[hub] docx 读写目录 ${OUT_DIR}(--out-dir 改)`)
|
|
216
273
|
if (!TOKEN) console.error("[hub] 未开鉴权:本机任何进程都能改文档。要收紧就加 --token")
|
|
217
274
|
})
|
|
@@ -161,6 +161,40 @@ modify 的 litexml 必须带上要改的那个块的 id 属性。你没写进 XM
|
|
|
161
161
|
required: ["tableId", "action"],
|
|
162
162
|
additionalProperties: false
|
|
163
163
|
}
|
|
164
|
+
},
|
|
165
|
+
/* ---------------------------- 导入 / 导出 ---------------------------- *
|
|
166
|
+
* 这两个工具的返回值和参数里**不出现文件内容**。docx 是二进制,一份带图的
|
|
167
|
+
* 动辄几 MB,base64 进上下文会直接把窗口撑爆。所以约定:模型只说路径,
|
|
168
|
+
* 字节在 hub ↔ 浏览器之间走,不经过模型。
|
|
169
|
+
* ------------------------------------------------------------------ */
|
|
170
|
+
{
|
|
171
|
+
name: "doc_export_docx",
|
|
172
|
+
description: "把当前文档导出成 .docx 落到磁盘,返回文件路径与导出统计。文件写在 hub 的输出目录下(启动时 --out-dir 指定,默认 hub 的工作目录),只接受文件名、不接受路径 —— 想放别处请让用户改 hub 的 --out-dir。\n会一并写回导入时保留的页眉页脚与页面设置(前提是这篇文档是本次会话里用 doc_import_docx 或界面导入进来的)。返回的 warnings 是**导出时丢了什么**,非空就如实转述给用户,别当成功报。",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
filename: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "文件名,如 `报告.docx`。省略则用 document.docx。不要带目录"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
additionalProperties: false
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "doc_import_docx",
|
|
186
|
+
description: "读一份 .docx 灌进编辑器,**整篇替换**当前内容(不是追加,也不可撤销 —— 文档里已有内容时先跟用户确认)。path 相对 hub 的输出目录解析。\n返回 recovered(各类样式各救回多少项)和 warnings。导入完要接着编辑的话,先调 doc_read 拿新的 id —— 旧 id 全部失效。",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
path: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "docx 文件名或相对 hub 输出目录的路径"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
required: ["path"],
|
|
196
|
+
additionalProperties: false
|
|
197
|
+
}
|
|
164
198
|
}
|
|
165
199
|
];
|
|
166
200
|
var DOC_TOOL_NAMES = DOC_TOOLS.map((t) => t.name);
|
|
@@ -169,5 +203,5 @@ function getDocTool(name) {
|
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool };
|
|
172
|
-
//# sourceMappingURL=chunk-
|
|
173
|
-
//# sourceMappingURL=chunk-
|
|
206
|
+
//# sourceMappingURL=chunk-P3AOHTTC.js.map
|
|
207
|
+
//# sourceMappingURL=chunk-P3AOHTTC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool } from './chunk-
|
|
1
|
+
export { DOC_TOOLS, DOC_TOOL_NAMES, getDocTool } from './chunk-P3AOHTTC.js';
|
|
2
2
|
export { attachBridge } from './chunk-AFXUKOKO.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/tools.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4399ywkf/editor-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.
|
|
34
|
+
"@4399ywkf/editor": "^0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.10.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools.ts"],"names":[],"mappings":"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","file":"chunk-TUKXSXAN.js","sourcesContent":["/**\n * 工具面:8 个工具的 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 * 为什么只有 8 个:参照系是 BlockNote AI 的 3 个工具,和把 tiptap 每个 command\n * 都包成工具的 `tiptap-apcore`(79 个,GitHub 1 star)。工具面大了模型反而选不准。\n * `doc_stats` 那类对账工具刻意不在这里 —— 它是给人调试用的,`DocRuntime.call`\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\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"]}
|