@gobi-starweaver/ws-cli 0.3.1
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/LICENSE +53 -0
- package/README-en.md +52 -0
- package/README.md +52 -0
- package/assets/assembly-orders/README.md +29 -0
- package/assets/asset-catalog/index.json +11 -0
- package/assets/asset-catalog/snapshots/catalog-2026-08-15.json +3122 -0
- package/assets/card-templates/README.md +166 -0
- package/assets/card-templates/collator.card.json +117 -0
- package/assets/card-templates/deep-reader.card.json +108 -0
- package/assets/card-templates/grant-editor.card.json +107 -0
- package/assets/card-templates/grant-writer.card.json +111 -0
- package/assets/card-templates/paper-review-rechecker.card.json +108 -0
- package/assets/card-templates/paper-reviewer.card.json +106 -0
- package/assets/card-templates/translation-reviewer.card.json +149 -0
- package/assets/card-templates/translator.card.json +149 -0
- package/assets/card-templates/visual-general.card.json +130 -0
- package/assets/card-templates/writing-author.card.json +137 -0
- package/assets/card-templates/writing-editor.card.json +118 -0
- package/assets/card-templates//346/234/233/350/210/222/344/270/223/345/277/203/351/251/276/351/251/266/350/257/264/346/230/216_/345/206/231/344/275/234/347/273/204.md +93 -0
- package/assets/card-templates//346/234/233/350/210/222/344/270/223/345/277/203/351/251/276/351/251/266/350/257/264/346/230/216_/345/256/241/347/250/277/347/273/204.md +70 -0
- package/assets/card-templates//346/234/233/350/210/222/344/270/223/345/277/203/351/251/276/351/251/266/350/257/264/346/230/216_/347/224/263/346/212/245/347/273/204.md +92 -0
- package/assets/card-templates//346/234/233/350/210/222/344/270/223/345/277/203/351/251/276/351/251/266/350/257/264/346/230/216_/347/277/273/350/257/221/347/273/204.md +70 -0
- package/assets/card-templates//346/234/233/350/210/222/350/275/254/345/221/210/345/275/242/346/200/201/350/247/204/350/214/203.md +58 -0
- package/assets/expressions/ascii-art.json +152 -0
- package/assets/expressions/emoji-full.json +11558 -0
- package/assets/expressions/emoji.json +415 -0
- package/bundled/plan/SKILL.md +133 -0
- package/chunks/chunk-BUBNMVFA.js +77 -0
- package/chunks/chunk-O2SA6W2I.js +37899 -0
- package/chunks/chunk-S7EGXUII.js +49 -0
- package/chunks/devtools-V5WI5QLO.js +3882 -0
- package/cli.js +81427 -0
- package/mcp/servers/doc-toolchain-server.js +1516 -0
- package/mcp/servers/ws-vision-ocr-server.js +32 -0
- package/package.json +26 -0
- package/templates/prompts/init_command.md.ejs +44 -0
|
@@ -0,0 +1,1516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
killProcessTree
|
|
4
|
+
} from "../../chunks/chunk-S7EGXUII.js";
|
|
5
|
+
import {
|
|
6
|
+
__name,
|
|
7
|
+
init_esbuild_shims
|
|
8
|
+
} from "../../chunks/chunk-BUBNMVFA.js";
|
|
9
|
+
|
|
10
|
+
// packages/core/src/mcp/servers/doc-toolchain-server.ts
|
|
11
|
+
init_esbuild_shims();
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import * as fs2 from "node:fs";
|
|
15
|
+
import * as path2 from "node:path";
|
|
16
|
+
|
|
17
|
+
// packages/core/src/mcp/servers/doc-toolchain/ooxml.ts
|
|
18
|
+
init_esbuild_shims();
|
|
19
|
+
|
|
20
|
+
// packages/core/src/mcp/servers/doc-toolchain/zip.ts
|
|
21
|
+
init_esbuild_shims();
|
|
22
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
23
|
+
var EOCD_SIGNATURE = 101010256;
|
|
24
|
+
var CENTRAL_SIGNATURE = 33639248;
|
|
25
|
+
var LOCAL_SIGNATURE = 67324752;
|
|
26
|
+
var ZIP64_EOCD_SIGNATURE = 101075792;
|
|
27
|
+
var METHOD_STORED = 0;
|
|
28
|
+
var METHOD_DEFLATE = 8;
|
|
29
|
+
var FLAG_UTF8 = 1 << 11;
|
|
30
|
+
var ZipFormatError = class extends Error {
|
|
31
|
+
static {
|
|
32
|
+
__name(this, "ZipFormatError");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function findEocdOffset(buffer) {
|
|
36
|
+
const earliest = Math.max(0, buffer.length - 22 - 65535);
|
|
37
|
+
for (let offset = buffer.length - 22; offset >= earliest; offset -= 1) {
|
|
38
|
+
if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
|
|
39
|
+
return offset;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new ZipFormatError("not-a-zip:\u672A\u627E\u5230 EOCD \u8BB0\u5F55");
|
|
43
|
+
}
|
|
44
|
+
__name(findEocdOffset, "findEocdOffset");
|
|
45
|
+
function readZipEntries(buffer) {
|
|
46
|
+
const eocdOffset = findEocdOffset(buffer);
|
|
47
|
+
if (eocdOffset >= 4 && buffer.readUInt32LE(eocdOffset - 20) === ZIP64_EOCD_SIGNATURE) {
|
|
48
|
+
throw new ZipFormatError("zip64-unsupported:ZIP64 \u6863\u6848\u6682\u4E0D\u652F\u6301");
|
|
49
|
+
}
|
|
50
|
+
const entryCount = buffer.readUInt16LE(eocdOffset + 10);
|
|
51
|
+
let centralOffset = buffer.readUInt32LE(eocdOffset + 16);
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
54
|
+
if (centralOffset + 46 > buffer.length || buffer.readUInt32LE(centralOffset) !== CENTRAL_SIGNATURE) {
|
|
55
|
+
throw new ZipFormatError(`not-a-zip:\u4E2D\u592E\u76EE\u5F55\u7B2C ${index} \u6761\u7B7E\u540D\u9519`);
|
|
56
|
+
}
|
|
57
|
+
const method = buffer.readUInt16LE(centralOffset + 10);
|
|
58
|
+
const compressedSize = buffer.readUInt32LE(centralOffset + 20);
|
|
59
|
+
const uncompressedSize = buffer.readUInt32LE(centralOffset + 24);
|
|
60
|
+
const nameLength = buffer.readUInt16LE(centralOffset + 28);
|
|
61
|
+
const extraLength = buffer.readUInt16LE(centralOffset + 30);
|
|
62
|
+
const commentLength = buffer.readUInt16LE(centralOffset + 32);
|
|
63
|
+
const localOffset = buffer.readUInt32LE(centralOffset + 42);
|
|
64
|
+
const name = buffer.subarray(centralOffset + 46, centralOffset + 46 + nameLength).toString("utf8");
|
|
65
|
+
if (compressedSize === 4294967295 || uncompressedSize === 4294967295 || localOffset === 4294967295) {
|
|
66
|
+
throw new ZipFormatError("zip64-unsupported:ZIP64 \u6863\u6848\u6682\u4E0D\u652F\u6301");
|
|
67
|
+
}
|
|
68
|
+
if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) {
|
|
69
|
+
throw new ZipFormatError(`not-a-zip:\u6761\u76EE ${name} \u672C\u5730\u5934\u7B7E\u540D\u9519`);
|
|
70
|
+
}
|
|
71
|
+
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
72
|
+
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
73
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
74
|
+
if (dataStart + compressedSize > buffer.length) {
|
|
75
|
+
throw new ZipFormatError(`not-a-zip:\u6761\u76EE ${name} \u6570\u636E\u8D8A\u754C`);
|
|
76
|
+
}
|
|
77
|
+
entries.push({
|
|
78
|
+
name,
|
|
79
|
+
method,
|
|
80
|
+
compressed: buffer.subarray(dataStart, dataStart + compressedSize),
|
|
81
|
+
compressedSize,
|
|
82
|
+
uncompressedSize
|
|
83
|
+
});
|
|
84
|
+
centralOffset += 46 + nameLength + extraLength + commentLength;
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
__name(readZipEntries, "readZipEntries");
|
|
89
|
+
function inflateZipEntry(entry) {
|
|
90
|
+
if (entry.method === METHOD_STORED) {
|
|
91
|
+
return Buffer.from(entry.compressed);
|
|
92
|
+
}
|
|
93
|
+
if (entry.method === METHOD_DEFLATE) {
|
|
94
|
+
const inflated = inflateRawSync(entry.compressed);
|
|
95
|
+
if (inflated.length !== entry.uncompressedSize) {
|
|
96
|
+
throw new ZipFormatError(`bad-inflate:\u6761\u76EE ${entry.name} \u89E3\u538B\u957F\u5EA6\u4E0E\u76EE\u5F55\u4E0D\u7B26`);
|
|
97
|
+
}
|
|
98
|
+
return inflated;
|
|
99
|
+
}
|
|
100
|
+
throw new ZipFormatError(`method-unsupported:\u6761\u76EE ${entry.name} \u538B\u7F29\u65B9\u6CD5 ${entry.method} \u4E0D\u652F\u6301`);
|
|
101
|
+
}
|
|
102
|
+
__name(inflateZipEntry, "inflateZipEntry");
|
|
103
|
+
function readZipText(entries, name) {
|
|
104
|
+
const entry = entries.find((candidate) => candidate.name === name);
|
|
105
|
+
if (!entry) return null;
|
|
106
|
+
return inflateZipEntry(entry).toString("utf8");
|
|
107
|
+
}
|
|
108
|
+
__name(readZipText, "readZipText");
|
|
109
|
+
var CRC_TABLE = (() => {
|
|
110
|
+
const table = new Uint32Array(256);
|
|
111
|
+
for (let n = 0; n < 256; n += 1) {
|
|
112
|
+
let c = n;
|
|
113
|
+
for (let k = 0; k < 8; k += 1) {
|
|
114
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
115
|
+
}
|
|
116
|
+
table[n] = c >>> 0;
|
|
117
|
+
}
|
|
118
|
+
return table;
|
|
119
|
+
})();
|
|
120
|
+
function crc32(data) {
|
|
121
|
+
let crc = 4294967295;
|
|
122
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
123
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 255] ^ crc >>> 8;
|
|
124
|
+
}
|
|
125
|
+
return (crc ^ 4294967295) >>> 0;
|
|
126
|
+
}
|
|
127
|
+
__name(crc32, "crc32");
|
|
128
|
+
function writeZip(inputs) {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
const central = [];
|
|
131
|
+
let offset = 0;
|
|
132
|
+
for (const input of inputs) {
|
|
133
|
+
const nameBytes = Buffer.from(input.name, "utf8");
|
|
134
|
+
const compressed = deflateRawSync(input.data, { level: 9 });
|
|
135
|
+
const crc = crc32(input.data);
|
|
136
|
+
const local = Buffer.alloc(30);
|
|
137
|
+
local.writeUInt32LE(LOCAL_SIGNATURE, 0);
|
|
138
|
+
local.writeUInt16LE(20, 4);
|
|
139
|
+
local.writeUInt16LE(FLAG_UTF8, 6);
|
|
140
|
+
local.writeUInt16LE(METHOD_DEFLATE, 8);
|
|
141
|
+
local.writeUInt16LE(0, 10);
|
|
142
|
+
local.writeUInt16LE(33, 12);
|
|
143
|
+
local.writeUInt32LE(crc, 14);
|
|
144
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
145
|
+
local.writeUInt32LE(input.data.length, 22);
|
|
146
|
+
local.writeUInt16LE(nameBytes.length, 26);
|
|
147
|
+
local.writeUInt16LE(0, 28);
|
|
148
|
+
chunks.push(local, nameBytes, compressed);
|
|
149
|
+
const centralHeader = Buffer.alloc(46);
|
|
150
|
+
centralHeader.writeUInt32LE(CENTRAL_SIGNATURE, 0);
|
|
151
|
+
centralHeader.writeUInt16LE(20, 4);
|
|
152
|
+
centralHeader.writeUInt16LE(20, 6);
|
|
153
|
+
centralHeader.writeUInt16LE(FLAG_UTF8, 8);
|
|
154
|
+
centralHeader.writeUInt16LE(METHOD_DEFLATE, 10);
|
|
155
|
+
centralHeader.writeUInt16LE(0, 12);
|
|
156
|
+
centralHeader.writeUInt16LE(33, 14);
|
|
157
|
+
centralHeader.writeUInt32LE(crc, 16);
|
|
158
|
+
centralHeader.writeUInt32LE(compressed.length, 20);
|
|
159
|
+
centralHeader.writeUInt32LE(input.data.length, 24);
|
|
160
|
+
centralHeader.writeUInt16LE(nameBytes.length, 28);
|
|
161
|
+
centralHeader.writeUInt16LE(0, 30);
|
|
162
|
+
centralHeader.writeUInt16LE(0, 32);
|
|
163
|
+
centralHeader.writeUInt16LE(0, 34);
|
|
164
|
+
centralHeader.writeUInt16LE(0, 36);
|
|
165
|
+
centralHeader.writeUInt32LE(0, 38);
|
|
166
|
+
centralHeader.writeUInt32LE(offset, 42);
|
|
167
|
+
central.push(centralHeader, nameBytes);
|
|
168
|
+
offset += 30 + nameBytes.length + compressed.length;
|
|
169
|
+
}
|
|
170
|
+
const centralBuffer = Buffer.concat(central);
|
|
171
|
+
const eocd = Buffer.alloc(22);
|
|
172
|
+
eocd.writeUInt32LE(EOCD_SIGNATURE, 0);
|
|
173
|
+
eocd.writeUInt16LE(0, 4);
|
|
174
|
+
eocd.writeUInt16LE(0, 6);
|
|
175
|
+
eocd.writeUInt16LE(inputs.length, 8);
|
|
176
|
+
eocd.writeUInt16LE(inputs.length, 10);
|
|
177
|
+
eocd.writeUInt32LE(centralBuffer.length, 12);
|
|
178
|
+
eocd.writeUInt32LE(offset, 16);
|
|
179
|
+
eocd.writeUInt16LE(0, 20);
|
|
180
|
+
return Buffer.concat([...chunks, centralBuffer, eocd]);
|
|
181
|
+
}
|
|
182
|
+
__name(writeZip, "writeZip");
|
|
183
|
+
|
|
184
|
+
// packages/core/src/mcp/servers/doc-toolchain/ooxml.ts
|
|
185
|
+
var XML_DECLARATION = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`;
|
|
186
|
+
var OoxmlFormatError = class extends Error {
|
|
187
|
+
static {
|
|
188
|
+
__name(this, "OoxmlFormatError");
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function localName(name) {
|
|
192
|
+
const colon = name.indexOf(":");
|
|
193
|
+
return colon >= 0 ? name.slice(colon + 1) : name;
|
|
194
|
+
}
|
|
195
|
+
__name(localName, "localName");
|
|
196
|
+
function parseAttributes(source) {
|
|
197
|
+
const attrs = {};
|
|
198
|
+
const pattern = /([A-Za-z_][\w:.-]*)\s*=\s*"([^"]*)"/g;
|
|
199
|
+
let match;
|
|
200
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
201
|
+
attrs[match[1]] = unescapeXml(match[2]);
|
|
202
|
+
}
|
|
203
|
+
return attrs;
|
|
204
|
+
}
|
|
205
|
+
__name(parseAttributes, "parseAttributes");
|
|
206
|
+
function unescapeXml(value) {
|
|
207
|
+
return value.replace(/&(amp|lt|gt|quot|apos|#x[0-9A-Fa-f]+|#\d+);/g, (whole, entity) => {
|
|
208
|
+
switch (entity) {
|
|
209
|
+
case "amp":
|
|
210
|
+
return "&";
|
|
211
|
+
case "lt":
|
|
212
|
+
return "<";
|
|
213
|
+
case "gt":
|
|
214
|
+
return ">";
|
|
215
|
+
case "quot":
|
|
216
|
+
return '"';
|
|
217
|
+
case "apos":
|
|
218
|
+
return "'";
|
|
219
|
+
default: {
|
|
220
|
+
const code = entity.startsWith("#x") ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
221
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
__name(unescapeXml, "unescapeXml");
|
|
227
|
+
function escapeXml(value) {
|
|
228
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
229
|
+
}
|
|
230
|
+
__name(escapeXml, "escapeXml");
|
|
231
|
+
function tokenizeXml(xml) {
|
|
232
|
+
const tokens = [];
|
|
233
|
+
let cursor = 0;
|
|
234
|
+
const length = xml.length;
|
|
235
|
+
while (cursor < length) {
|
|
236
|
+
const lt = xml.indexOf("<", cursor);
|
|
237
|
+
if (lt < 0) {
|
|
238
|
+
tokens.push({ type: "text", text: unescapeXml(xml.slice(cursor)), start: cursor, end: length });
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (lt > cursor) {
|
|
242
|
+
tokens.push({ type: "text", text: unescapeXml(xml.slice(cursor, lt)), start: cursor, end: lt });
|
|
243
|
+
}
|
|
244
|
+
if (xml.startsWith("<?", lt)) {
|
|
245
|
+
const end = xml.indexOf("?>", lt + 2);
|
|
246
|
+
cursor = end < 0 ? length : end + 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (xml.startsWith("<!--", lt)) {
|
|
250
|
+
const end = xml.indexOf("-->", lt + 4);
|
|
251
|
+
cursor = end < 0 ? length : end + 3;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (xml.startsWith("<![CDATA[", lt)) {
|
|
255
|
+
const end = xml.indexOf("]]>", lt + 9);
|
|
256
|
+
const textEnd = end < 0 ? length : end;
|
|
257
|
+
tokens.push({ type: "text", text: xml.slice(lt + 9, textEnd), start: lt, end: end < 0 ? length : end + 3 });
|
|
258
|
+
cursor = end < 0 ? length : end + 3;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (xml.startsWith("<!", lt)) {
|
|
262
|
+
const end = xml.indexOf(">", lt + 2);
|
|
263
|
+
cursor = end < 0 ? length : end + 1;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (xml.startsWith("</", lt)) {
|
|
267
|
+
const end = xml.indexOf(">", lt + 2);
|
|
268
|
+
if (end < 0) throw new OoxmlFormatError("bad-xml:\u95ED\u5408\u6807\u7B7E\u672A\u6536\u5C3E");
|
|
269
|
+
const name2 = xml.slice(lt + 2, end).trim();
|
|
270
|
+
tokens.push({ type: "close", name: name2, start: lt, end: end + 1 });
|
|
271
|
+
cursor = end + 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
let scan = lt + 1;
|
|
275
|
+
let inQuote = false;
|
|
276
|
+
while (scan < length) {
|
|
277
|
+
const ch = xml[scan];
|
|
278
|
+
if (ch === '"') inQuote = !inQuote;
|
|
279
|
+
else if (ch === ">" && !inQuote) break;
|
|
280
|
+
scan += 1;
|
|
281
|
+
}
|
|
282
|
+
if (scan >= length) throw new OoxmlFormatError("bad-xml:\u5F00\u653E\u6807\u7B7E\u672A\u6536\u5C3E");
|
|
283
|
+
const inner = xml.slice(lt + 1, scan);
|
|
284
|
+
const selfClosing = /\/\s*$/.test(inner);
|
|
285
|
+
const body = selfClosing ? inner.replace(/\/\s*$/, "") : inner;
|
|
286
|
+
const nameMatch = /^([^\s]+)/.exec(body.trim());
|
|
287
|
+
if (!nameMatch) throw new OoxmlFormatError("bad-xml:\u6807\u7B7E\u540D\u4E3A\u7A7A");
|
|
288
|
+
const name = nameMatch[1];
|
|
289
|
+
const attrs = parseAttributes(body.slice(body.indexOf(name) + name.length));
|
|
290
|
+
tokens.push({ type: "open", name, attrs, selfClosing, start: lt, end: scan + 1 });
|
|
291
|
+
cursor = scan + 1;
|
|
292
|
+
}
|
|
293
|
+
return tokens;
|
|
294
|
+
}
|
|
295
|
+
__name(tokenizeXml, "tokenizeXml");
|
|
296
|
+
function subtreeEndIndex(tokens, index) {
|
|
297
|
+
const token = tokens[index];
|
|
298
|
+
if (token.selfClosing) return index;
|
|
299
|
+
const name = token.name;
|
|
300
|
+
let depth = 1;
|
|
301
|
+
for (let cursor = index + 1; cursor < tokens.length; cursor += 1) {
|
|
302
|
+
const current = tokens[cursor];
|
|
303
|
+
if (current.type === "open" && !current.selfClosing && current.name === name) depth += 1;
|
|
304
|
+
else if (current.type === "close" && current.name === name) depth -= 1;
|
|
305
|
+
else if (current.type === "open" && current.selfClosing) {
|
|
306
|
+
}
|
|
307
|
+
if (depth === 0) return cursor;
|
|
308
|
+
}
|
|
309
|
+
throw new OoxmlFormatError(`bad-xml:\u6807\u7B7E ${name} \u672A\u914D\u5E73`);
|
|
310
|
+
}
|
|
311
|
+
__name(subtreeEndIndex, "subtreeEndIndex");
|
|
312
|
+
function paragraphTextFromTokens(tokens, startIndex, endIndex) {
|
|
313
|
+
let text = "";
|
|
314
|
+
let insideText = false;
|
|
315
|
+
let delDepth = 0;
|
|
316
|
+
for (let cursor = startIndex; cursor <= endIndex; cursor += 1) {
|
|
317
|
+
const token = tokens[cursor];
|
|
318
|
+
if (token.type === "open") {
|
|
319
|
+
const local = localName(token.name);
|
|
320
|
+
if (local === "del") {
|
|
321
|
+
if (!token.selfClosing) delDepth += 1;
|
|
322
|
+
} else if (local === "t" && delDepth === 0) {
|
|
323
|
+
if (!token.selfClosing) insideText = true;
|
|
324
|
+
} else if ((local === "tab" || local === "br" || local === "cr") && delDepth === 0) {
|
|
325
|
+
text += local === "tab" ? " " : "\n";
|
|
326
|
+
} else if (local === "noBreakHyphen" && delDepth === 0) {
|
|
327
|
+
text += "-";
|
|
328
|
+
}
|
|
329
|
+
} else if (token.type === "close") {
|
|
330
|
+
const local = localName(token.name);
|
|
331
|
+
if (local === "del" && delDepth > 0) delDepth -= 1;
|
|
332
|
+
else if (local === "t") insideText = false;
|
|
333
|
+
} else if (insideText && delDepth === 0) {
|
|
334
|
+
text += token.text;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return text;
|
|
338
|
+
}
|
|
339
|
+
__name(paragraphTextFromTokens, "paragraphTextFromTokens");
|
|
340
|
+
function paragraphStyleFromTokens(tokens, startIndex, endIndex) {
|
|
341
|
+
for (let cursor = startIndex; cursor <= endIndex; cursor += 1) {
|
|
342
|
+
const token = tokens[cursor];
|
|
343
|
+
if (token.type === "open" && localName(token.name) === "pStyle") {
|
|
344
|
+
const value = token.attrs["w:val"] ?? token.attrs.val;
|
|
345
|
+
if (value) return value;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
__name(paragraphStyleFromTokens, "paragraphStyleFromTokens");
|
|
351
|
+
function walkBody(tokens) {
|
|
352
|
+
const bodyOpenIndex = tokens.findIndex((token) => token.type === "open" && localName(token.name) === "body");
|
|
353
|
+
if (bodyOpenIndex < 0) throw new OoxmlFormatError("bad-docx:\u672A\u627E\u5230 w:body");
|
|
354
|
+
const bodyEndIndex = subtreeEndIndex(tokens, bodyOpenIndex);
|
|
355
|
+
const blocks = [];
|
|
356
|
+
const paragraphSpans = [];
|
|
357
|
+
let sectPrSpan = null;
|
|
358
|
+
let tableCount = 0;
|
|
359
|
+
let cursor = bodyOpenIndex + 1;
|
|
360
|
+
while (cursor < bodyEndIndex) {
|
|
361
|
+
const token = tokens[cursor];
|
|
362
|
+
if (token.type !== "open") {
|
|
363
|
+
cursor += 1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const local = localName(token.name);
|
|
367
|
+
if (local === "p") {
|
|
368
|
+
const endIndex = token.selfClosing ? cursor : subtreeEndIndex(tokens, cursor);
|
|
369
|
+
const start = token.start;
|
|
370
|
+
const end = tokens[endIndex].end;
|
|
371
|
+
const text = token.selfClosing ? "" : paragraphTextFromTokens(tokens, cursor + 1, endIndex - 1);
|
|
372
|
+
const style = token.selfClosing ? null : paragraphStyleFromTokens(tokens, cursor + 1, endIndex - 1);
|
|
373
|
+
paragraphSpans.push({ start, end });
|
|
374
|
+
blocks.push({ type: "paragraph", paragraphIndex: paragraphSpans.length - 1, style, text });
|
|
375
|
+
cursor = endIndex + 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (local === "tbl") {
|
|
379
|
+
const endIndex = subtreeEndIndex(tokens, cursor);
|
|
380
|
+
const rows = [];
|
|
381
|
+
let rowDepth = 0;
|
|
382
|
+
let currentRow = null;
|
|
383
|
+
let currentCell = null;
|
|
384
|
+
let cellParagraphStart = -1;
|
|
385
|
+
for (let inner = cursor + 1; inner < endIndex; inner += 1) {
|
|
386
|
+
const innerToken = tokens[inner];
|
|
387
|
+
if (innerToken.type === "open") {
|
|
388
|
+
const innerLocal = localName(innerToken.name);
|
|
389
|
+
if (innerLocal === "tr") {
|
|
390
|
+
if (innerToken.selfClosing) {
|
|
391
|
+
if (rowDepth === 0) rows.push([]);
|
|
392
|
+
} else {
|
|
393
|
+
if (rowDepth === 0) currentRow = [];
|
|
394
|
+
rowDepth += 1;
|
|
395
|
+
}
|
|
396
|
+
} else if (innerLocal === "tc" && rowDepth === 1 && currentRow) {
|
|
397
|
+
if (innerToken.selfClosing) currentRow.push([]);
|
|
398
|
+
else currentCell = [];
|
|
399
|
+
} else if (innerLocal === "p" && currentCell) {
|
|
400
|
+
if (innerToken.selfClosing) currentCell.push("");
|
|
401
|
+
else cellParagraphStart = inner;
|
|
402
|
+
}
|
|
403
|
+
} else if (innerToken.type === "close") {
|
|
404
|
+
const innerLocal = localName(innerToken.name);
|
|
405
|
+
if (innerLocal === "p" && currentCell && cellParagraphStart >= 0) {
|
|
406
|
+
currentCell.push(paragraphTextFromTokens(tokens, cellParagraphStart + 1, inner - 1));
|
|
407
|
+
cellParagraphStart = -1;
|
|
408
|
+
} else if (innerLocal === "tc" && rowDepth === 1 && currentRow && currentCell) {
|
|
409
|
+
currentRow.push(currentCell);
|
|
410
|
+
currentCell = null;
|
|
411
|
+
} else if (innerLocal === "tr") {
|
|
412
|
+
if (rowDepth === 1 && currentRow) {
|
|
413
|
+
rows.push(currentRow.map((cell) => cell.join("\n")));
|
|
414
|
+
currentRow = null;
|
|
415
|
+
}
|
|
416
|
+
rowDepth -= 1;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
blocks.push({ type: "table", tableIndex: tableCount, rows });
|
|
421
|
+
tableCount += 1;
|
|
422
|
+
cursor = endIndex + 1;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (local === "sectPr") {
|
|
426
|
+
const endIndex = token.selfClosing ? cursor : subtreeEndIndex(tokens, cursor);
|
|
427
|
+
sectPrSpan = { start: token.start, end: tokens[endIndex].end };
|
|
428
|
+
cursor = endIndex + 1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
cursor = token.selfClosing ? cursor + 1 : subtreeEndIndex(tokens, cursor) + 1;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
blocks,
|
|
435
|
+
paragraphSpans,
|
|
436
|
+
sectPrSpan,
|
|
437
|
+
bodyCloseStart: tokens[bodyEndIndex].start
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
__name(walkBody, "walkBody");
|
|
441
|
+
function notesFromXml(xml, elementLocal) {
|
|
442
|
+
if (!xml) return [];
|
|
443
|
+
const tokens = tokenizeXml(xml);
|
|
444
|
+
const notes = [];
|
|
445
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
446
|
+
const token = tokens[index];
|
|
447
|
+
if (token.type !== "open" || localName(token.name) !== elementLocal || token.selfClosing) continue;
|
|
448
|
+
const id = token.attrs["w:id"] ?? token.attrs.id ?? "";
|
|
449
|
+
if (id === "-1" || id === "0") continue;
|
|
450
|
+
const endIndex = subtreeEndIndex(tokens, index);
|
|
451
|
+
notes.push({ id, text: paragraphTextFromTokens(tokens, index + 1, endIndex - 1) });
|
|
452
|
+
index = endIndex;
|
|
453
|
+
}
|
|
454
|
+
return notes;
|
|
455
|
+
}
|
|
456
|
+
__name(notesFromXml, "notesFromXml");
|
|
457
|
+
function readDocx(buffer, options = {}) {
|
|
458
|
+
const entries = readZipEntries(buffer);
|
|
459
|
+
const documentXml = readZipText(entries, "word/document.xml");
|
|
460
|
+
if (documentXml === null) throw new OoxmlFormatError("bad-docx:\u7F3A word/document.xml");
|
|
461
|
+
const walk = walkBody(tokenizeXml(documentXml));
|
|
462
|
+
const textParts = [];
|
|
463
|
+
let characters = 0;
|
|
464
|
+
for (const block of walk.blocks) {
|
|
465
|
+
if (block.type === "paragraph") {
|
|
466
|
+
textParts.push(block.text);
|
|
467
|
+
characters += block.text.replace(/\s/g, "").length;
|
|
468
|
+
} else {
|
|
469
|
+
textParts.push(block.rows.map((row) => row.join(" | ")).join("\n"));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const result = {
|
|
473
|
+
blocks: walk.blocks,
|
|
474
|
+
paragraphCount: walk.paragraphSpans.length,
|
|
475
|
+
tableCount: walk.blocks.filter((block) => block.type === "table").length,
|
|
476
|
+
text: textParts.join("\n"),
|
|
477
|
+
stats: {
|
|
478
|
+
characters,
|
|
479
|
+
paragraphs: walk.paragraphSpans.length,
|
|
480
|
+
tables: walk.blocks.filter((block) => block.type === "table").length
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
if (options.includeNotes) {
|
|
484
|
+
result.notes = {
|
|
485
|
+
footnotes: notesFromXml(readZipText(entries, "word/footnotes.xml"), "footnote"),
|
|
486
|
+
endnotes: notesFromXml(readZipText(entries, "word/endnotes.xml"), "endnote"),
|
|
487
|
+
comments: notesFromXml(readZipText(entries, "word/comments.xml"), "comment")
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
__name(readDocx, "readDocx");
|
|
493
|
+
var CONTENT_TYPES_XML = `${XML_DECLARATION}
|
|
494
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>`;
|
|
495
|
+
var ROOT_RELS_XML = `${XML_DECLARATION}
|
|
496
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`;
|
|
497
|
+
var DOCUMENT_RELS_XML = `${XML_DECLARATION}
|
|
498
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/></Relationships>`;
|
|
499
|
+
var STYLES_XML = `${XML_DECLARATION}
|
|
500
|
+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Times New Roman" w:eastAsia="\u5B8B\u4F53" w:hAnsi="Times New Roman" w:cs="Times New Roman"/><w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr></w:rPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style><w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="240" w:after="120"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:b/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="200" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="160" w:after="80"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:b/><w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:pPr><w:ind w:left="720"/></w:pPr></w:style></w:styles>`;
|
|
501
|
+
var NUMBERING_XML = `${XML_DECLARATION}
|
|
502
|
+
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:abstractNum w:abstractNumId="0"><w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="\xB7"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl></w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num></w:numbering>`;
|
|
503
|
+
var SECT_PR_XML = `<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="851" w:footer="992" w:gutter="0"/></w:sectPr>`;
|
|
504
|
+
function runXmlForText(text) {
|
|
505
|
+
const lines = text.split("\n");
|
|
506
|
+
const segments = lines.map((line) => `<w:t xml:space="preserve">${escapeXml(line)}</w:t>`);
|
|
507
|
+
return `<w:r>${segments.join("<w:br/>")}</w:r>`;
|
|
508
|
+
}
|
|
509
|
+
__name(runXmlForText, "runXmlForText");
|
|
510
|
+
function paragraphXml(text, style) {
|
|
511
|
+
const pPr = style ? `<w:pPr><w:pStyle w:val="${escapeXml(style)}"/></w:pPr>` : "";
|
|
512
|
+
if (!text) {
|
|
513
|
+
return `<w:p>${pPr}</w:p>`;
|
|
514
|
+
}
|
|
515
|
+
return `<w:p>${pPr}${runXmlForText(text)}</w:p>`;
|
|
516
|
+
}
|
|
517
|
+
__name(paragraphXml, "paragraphXml");
|
|
518
|
+
function listItemXml(text) {
|
|
519
|
+
return `<w:p><w:pPr><w:pStyle w:val="ListParagraph"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>${runXmlForText(text)}</w:p>`;
|
|
520
|
+
}
|
|
521
|
+
__name(listItemXml, "listItemXml");
|
|
522
|
+
function isCjk(codePoint) {
|
|
523
|
+
return codePoint >= 19968 && codePoint <= 40959 || codePoint >= 13312 && codePoint <= 19903 || codePoint >= 12288 && codePoint <= 12351 || codePoint >= 65280 && codePoint <= 65519;
|
|
524
|
+
}
|
|
525
|
+
__name(isCjk, "isCjk");
|
|
526
|
+
function markdownToBodyXml(markdown) {
|
|
527
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
528
|
+
const parts = [];
|
|
529
|
+
let paragraphBuffer = [];
|
|
530
|
+
const flushParagraph = /* @__PURE__ */ __name(() => {
|
|
531
|
+
if (paragraphBuffer.length === 0) return;
|
|
532
|
+
let merged = paragraphBuffer[0];
|
|
533
|
+
for (let index = 1; index < paragraphBuffer.length; index += 1) {
|
|
534
|
+
const previous = merged[merged.length - 1] ?? "";
|
|
535
|
+
const next = paragraphBuffer[index][0] ?? "";
|
|
536
|
+
const glue = isCjk(previous.codePointAt(0) ?? 0) || isCjk(next.codePointAt(0) ?? 0) ? "" : " ";
|
|
537
|
+
merged += glue + paragraphBuffer[index];
|
|
538
|
+
}
|
|
539
|
+
parts.push(paragraphXml(merged));
|
|
540
|
+
paragraphBuffer = [];
|
|
541
|
+
}, "flushParagraph");
|
|
542
|
+
for (const rawLine of lines) {
|
|
543
|
+
const line = rawLine.trimEnd();
|
|
544
|
+
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
545
|
+
const listItem = /^[-•]\s+(.*)$/.exec(line);
|
|
546
|
+
if (heading) {
|
|
547
|
+
flushParagraph();
|
|
548
|
+
parts.push(paragraphXml(heading[2], `Heading${heading[1].length}`));
|
|
549
|
+
} else if (listItem) {
|
|
550
|
+
flushParagraph();
|
|
551
|
+
parts.push(listItemXml(listItem[1]));
|
|
552
|
+
} else if (line.trim() === "") {
|
|
553
|
+
flushParagraph();
|
|
554
|
+
} else {
|
|
555
|
+
paragraphBuffer.push(line.trim());
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
flushParagraph();
|
|
559
|
+
return parts.join("");
|
|
560
|
+
}
|
|
561
|
+
__name(markdownToBodyXml, "markdownToBodyXml");
|
|
562
|
+
function buildDocx(markdown) {
|
|
563
|
+
const documentXml = `${XML_DECLARATION}
|
|
564
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${markdownToBodyXml(markdown)}${SECT_PR_XML}</w:body></w:document>`;
|
|
565
|
+
return writeZip([
|
|
566
|
+
{ name: "[Content_Types].xml", data: Buffer.from(CONTENT_TYPES_XML, "utf8") },
|
|
567
|
+
{ name: "_rels/.rels", data: Buffer.from(ROOT_RELS_XML, "utf8") },
|
|
568
|
+
{ name: "word/_rels/document.xml.rels", data: Buffer.from(DOCUMENT_RELS_XML, "utf8") },
|
|
569
|
+
{ name: "word/document.xml", data: Buffer.from(documentXml, "utf8") },
|
|
570
|
+
{ name: "word/styles.xml", data: Buffer.from(STYLES_XML, "utf8") },
|
|
571
|
+
{ name: "word/numbering.xml", data: Buffer.from(NUMBERING_XML, "utf8") }
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
574
|
+
__name(buildDocx, "buildDocx");
|
|
575
|
+
function editDocx(buffer, ops) {
|
|
576
|
+
if (!Array.isArray(ops) || ops.length === 0) {
|
|
577
|
+
throw new OoxmlFormatError("invalid-ops:ops \u4E3A\u7A7A");
|
|
578
|
+
}
|
|
579
|
+
const entries = readZipEntries(buffer);
|
|
580
|
+
const documentEntry = entries.find((entry) => entry.name === "word/document.xml");
|
|
581
|
+
if (!documentEntry) throw new OoxmlFormatError("bad-docx:\u7F3A word/document.xml");
|
|
582
|
+
const documentXml = inflateZipEntry(documentEntry).toString("utf8");
|
|
583
|
+
const tokens = tokenizeXml(documentXml);
|
|
584
|
+
const walk = walkBody(tokens);
|
|
585
|
+
const replacements = [];
|
|
586
|
+
const insertions = [];
|
|
587
|
+
const paragraphXmlAt = /* @__PURE__ */ __name((index) => {
|
|
588
|
+
const span = walk.paragraphSpans[index];
|
|
589
|
+
if (!span) {
|
|
590
|
+
throw new OoxmlFormatError(`invalid-ops:\u6BB5\u843D\u7F16\u53F7 ${index} \u8D8A\u754C\uFF08\u5171 ${walk.paragraphSpans.length} \u6BB5\uFF09`);
|
|
591
|
+
}
|
|
592
|
+
return span;
|
|
593
|
+
}, "paragraphXmlAt");
|
|
594
|
+
const extractPPr = /* @__PURE__ */ __name((start, end) => {
|
|
595
|
+
const slice = documentXml.slice(start, end);
|
|
596
|
+
const pPrMatch = /<w:pPr>[\s\S]*?<\/w:pPr>/.exec(slice);
|
|
597
|
+
return pPrMatch ? pPrMatch[0] : "";
|
|
598
|
+
}, "extractPPr");
|
|
599
|
+
for (const op of ops) {
|
|
600
|
+
if (op.op === "replace_paragraph_text") {
|
|
601
|
+
const span = paragraphXmlAt(op.index);
|
|
602
|
+
if (typeof op.text !== "string") throw new OoxmlFormatError("invalid-ops:replace_paragraph_text \u7F3A text");
|
|
603
|
+
const pPr = extractPPr(span.start, span.end);
|
|
604
|
+
replacements.push({
|
|
605
|
+
start: span.start,
|
|
606
|
+
end: span.end,
|
|
607
|
+
xml: `<w:p>${pPr}${op.text ? runXmlForText(op.text) : ""}</w:p>`
|
|
608
|
+
});
|
|
609
|
+
} else if (op.op === "delete_paragraph") {
|
|
610
|
+
const span = paragraphXmlAt(op.index);
|
|
611
|
+
replacements.push({ start: span.start, end: span.end, xml: "" });
|
|
612
|
+
} else if (op.op === "insert_paragraph") {
|
|
613
|
+
const span = paragraphXmlAt(op.index);
|
|
614
|
+
if (typeof op.text !== "string") throw new OoxmlFormatError("invalid-ops:insert_paragraph \u7F3A text");
|
|
615
|
+
const xml = paragraphXml(op.text, op.style ?? null);
|
|
616
|
+
if (op.position === "before") insertions.push({ at: span.start, xml });
|
|
617
|
+
else insertions.push({ at: span.end, xml });
|
|
618
|
+
} else if (op.op === "append_paragraph") {
|
|
619
|
+
if (typeof op.text !== "string") throw new OoxmlFormatError("invalid-ops:append_paragraph \u7F3A text");
|
|
620
|
+
const at = walk.sectPrSpan ? walk.sectPrSpan.start : walk.bodyCloseStart;
|
|
621
|
+
insertions.push({ at, xml: paragraphXml(op.text, op.style ?? null) });
|
|
622
|
+
} else {
|
|
623
|
+
throw new OoxmlFormatError(`invalid-ops:\u672A\u77E5\u64CD\u4F5C ${op.op}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const touchedSpans = /* @__PURE__ */ new Set();
|
|
627
|
+
for (const op of ops) {
|
|
628
|
+
if (op.op === "replace_paragraph_text" || op.op === "delete_paragraph") {
|
|
629
|
+
if (touchedSpans.has(op.index)) {
|
|
630
|
+
throw new OoxmlFormatError(`invalid-ops:\u6BB5\u843D ${op.index} \u88AB replace/delete \u91CD\u590D\u951A\u5B9A\uFF08\u51B2\u7A81\uFF09`);
|
|
631
|
+
}
|
|
632
|
+
touchedSpans.add(op.index);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const edits = [
|
|
636
|
+
...replacements.map((entry, seq) => ({ start: entry.start, end: entry.end, xml: entry.xml, kind: 0, seq })),
|
|
637
|
+
...insertions.map((entry, seq) => ({
|
|
638
|
+
start: entry.at,
|
|
639
|
+
end: entry.at,
|
|
640
|
+
xml: entry.xml,
|
|
641
|
+
kind: 1,
|
|
642
|
+
seq: replacements.length + seq
|
|
643
|
+
}))
|
|
644
|
+
].sort((a, b) => b.start - a.start || a.kind - b.kind || b.seq - a.seq);
|
|
645
|
+
let nextXml = documentXml;
|
|
646
|
+
for (const edit of edits) {
|
|
647
|
+
nextXml = nextXml.slice(0, edit.start) + edit.xml + nextXml.slice(edit.end);
|
|
648
|
+
}
|
|
649
|
+
const after = walkBody(tokenizeXml(nextXml));
|
|
650
|
+
const nextEntries = entries.map(
|
|
651
|
+
(entry) => entry.name === "word/document.xml" ? { name: entry.name, data: Buffer.from(nextXml, "utf8") } : { name: entry.name, data: inflateZipEntry(entry) }
|
|
652
|
+
);
|
|
653
|
+
return {
|
|
654
|
+
buffer: writeZip(nextEntries),
|
|
655
|
+
applied: ops.length,
|
|
656
|
+
paragraphCountBefore: walk.paragraphSpans.length,
|
|
657
|
+
paragraphCountAfter: after.paragraphSpans.length
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
__name(editDocx, "editDocx");
|
|
661
|
+
|
|
662
|
+
// packages/core/src/mcp/servers/doc-toolchain/convert.ts
|
|
663
|
+
init_esbuild_shims();
|
|
664
|
+
import { spawn } from "node:child_process";
|
|
665
|
+
import * as fs from "node:fs";
|
|
666
|
+
import * as os from "node:os";
|
|
667
|
+
import * as path from "node:path";
|
|
668
|
+
import { pathToFileURL } from "node:url";
|
|
669
|
+
var OUTPUT_CAPTURE_LIMIT = 64 * 1024;
|
|
670
|
+
function appendCapped(previous, chunk) {
|
|
671
|
+
if (previous.length >= OUTPUT_CAPTURE_LIMIT) return previous;
|
|
672
|
+
return (previous + chunk).slice(0, OUTPUT_CAPTURE_LIMIT);
|
|
673
|
+
}
|
|
674
|
+
__name(appendCapped, "appendCapped");
|
|
675
|
+
var defaultRunProcess = /* @__PURE__ */ __name((command, args, options) => new Promise((resolve3) => {
|
|
676
|
+
let settled = false;
|
|
677
|
+
let child;
|
|
678
|
+
try {
|
|
679
|
+
child = spawn(command, args, {
|
|
680
|
+
env: options.env,
|
|
681
|
+
shell: false,
|
|
682
|
+
windowsHide: true
|
|
683
|
+
});
|
|
684
|
+
} catch (error) {
|
|
685
|
+
resolve3({
|
|
686
|
+
exitCode: null,
|
|
687
|
+
stdout: "",
|
|
688
|
+
stderr: "",
|
|
689
|
+
timedOut: false,
|
|
690
|
+
spawnError: error instanceof Error ? error.message : String(error)
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
let stdout = "";
|
|
695
|
+
let stderr = "";
|
|
696
|
+
let timedOut = false;
|
|
697
|
+
const finish = /* @__PURE__ */ __name((exitCode) => {
|
|
698
|
+
if (settled) return;
|
|
699
|
+
settled = true;
|
|
700
|
+
clearTimeout(timer);
|
|
701
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
702
|
+
resolve3({ exitCode, stdout: stdout.trim(), stderr: stderr.trim(), timedOut });
|
|
703
|
+
}, "finish");
|
|
704
|
+
const kill = /* @__PURE__ */ __name(() => {
|
|
705
|
+
if (child.pid) killProcessTree(child.pid);
|
|
706
|
+
}, "kill");
|
|
707
|
+
const timer = setTimeout(() => {
|
|
708
|
+
timedOut = true;
|
|
709
|
+
kill();
|
|
710
|
+
}, options.timeoutMs);
|
|
711
|
+
const onAbort = /* @__PURE__ */ __name(() => {
|
|
712
|
+
kill();
|
|
713
|
+
}, "onAbort");
|
|
714
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
715
|
+
child.stdout?.on("data", (chunk) => {
|
|
716
|
+
stdout = appendCapped(stdout, chunk.toString("utf8"));
|
|
717
|
+
});
|
|
718
|
+
child.stderr?.on("data", (chunk) => {
|
|
719
|
+
stderr = appendCapped(stderr, chunk.toString("utf8"));
|
|
720
|
+
});
|
|
721
|
+
child.on("error", (error) => {
|
|
722
|
+
if (settled) return;
|
|
723
|
+
settled = true;
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
726
|
+
resolve3({ exitCode: null, stdout: "", stderr: "", timedOut, spawnError: error.message });
|
|
727
|
+
});
|
|
728
|
+
child.on("close", (code) => finish(code));
|
|
729
|
+
}), "defaultRunProcess");
|
|
730
|
+
var SOFFICE_ENV = "WS_SOFFICE";
|
|
731
|
+
var PDFTOPPM_ENV = "WS_PDFTOPPM";
|
|
732
|
+
var PYTHON_ENV = "WS_PYTHON";
|
|
733
|
+
function searchPathDirs(env, platform) {
|
|
734
|
+
const raw = env.PATH ?? env.Path ?? env.path ?? "";
|
|
735
|
+
return raw.split(platform === "win32" ? ";" : ":").filter((entry) => entry.length > 0);
|
|
736
|
+
}
|
|
737
|
+
__name(searchPathDirs, "searchPathDirs");
|
|
738
|
+
function findOnPath(names, env, platform, pathExists) {
|
|
739
|
+
for (const dir of searchPathDirs(env, platform)) {
|
|
740
|
+
for (const name of names) {
|
|
741
|
+
const candidate = path.join(dir, name);
|
|
742
|
+
if (pathExists(candidate)) return candidate;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
__name(findOnPath, "findOnPath");
|
|
748
|
+
function wellKnownSofficePaths(env, platform) {
|
|
749
|
+
if (platform === "win32") {
|
|
750
|
+
const roots = [
|
|
751
|
+
env.ProgramFiles,
|
|
752
|
+
env["ProgramFiles(x86)"],
|
|
753
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs")
|
|
754
|
+
].filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
755
|
+
return roots.map((root) => path.join(root, "LibreOffice", "program", "soffice.exe"));
|
|
756
|
+
}
|
|
757
|
+
if (platform === "darwin") {
|
|
758
|
+
return ["/Applications/LibreOffice.app/Contents/MacOS/soffice"];
|
|
759
|
+
}
|
|
760
|
+
return [
|
|
761
|
+
"/usr/bin/soffice",
|
|
762
|
+
"/usr/local/bin/soffice",
|
|
763
|
+
"/snap/bin/soffice",
|
|
764
|
+
"/usr/bin/libreoffice",
|
|
765
|
+
"/usr/local/bin/libreoffice"
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
__name(wellKnownSofficePaths, "wellKnownSofficePaths");
|
|
769
|
+
function wellKnownPdftoppmPaths(platform) {
|
|
770
|
+
if (platform === "win32") return [];
|
|
771
|
+
if (platform === "darwin") return ["/opt/homebrew/bin/pdftoppm", "/usr/local/bin/pdftoppm"];
|
|
772
|
+
return ["/usr/bin/pdftoppm", "/usr/local/bin/pdftoppm", "/snap/bin/pdftoppm"];
|
|
773
|
+
}
|
|
774
|
+
__name(wellKnownPdftoppmPaths, "wellKnownPdftoppmPaths");
|
|
775
|
+
async function probeVersion(run, env, command, args, pattern) {
|
|
776
|
+
const result = await run(command, args, { env, timeoutMs: 2e4 });
|
|
777
|
+
const haystack = `${result.stdout}
|
|
778
|
+
${result.stderr}`;
|
|
779
|
+
const match = pattern.exec(haystack);
|
|
780
|
+
return match ? match[0].trim() : null;
|
|
781
|
+
}
|
|
782
|
+
__name(probeVersion, "probeVersion");
|
|
783
|
+
async function probeSofficeVersion(run, env, candidate, platform, pathExists) {
|
|
784
|
+
const version = await probeVersion(run, env, candidate, ["--version"], /LibreOffice[\s\d.]+/i);
|
|
785
|
+
if (version || platform !== "win32") return version;
|
|
786
|
+
const consoleVariant = path.join(path.dirname(candidate), "soffice.com");
|
|
787
|
+
if (consoleVariant === candidate || !pathExists(consoleVariant)) return version;
|
|
788
|
+
return probeVersion(run, env, consoleVariant, ["--version"], /LibreOffice[\s\d.]+/i);
|
|
789
|
+
}
|
|
790
|
+
__name(probeSofficeVersion, "probeSofficeVersion");
|
|
791
|
+
async function detectSoffice(deps = {}) {
|
|
792
|
+
const env = deps.env ?? process.env;
|
|
793
|
+
const platform = deps.platform ?? process.platform;
|
|
794
|
+
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
795
|
+
const run = deps.run ?? defaultRunProcess;
|
|
796
|
+
const probed = [];
|
|
797
|
+
const explicit = env[SOFFICE_ENV]?.trim();
|
|
798
|
+
if (explicit) {
|
|
799
|
+
const exists = pathExists(explicit);
|
|
800
|
+
const version = exists ? await probeSofficeVersion(run, env, explicit, platform, pathExists) : null;
|
|
801
|
+
return { tool: "soffice", found: true, path: explicit, source: "env", version, exists, probed: [explicit] };
|
|
802
|
+
}
|
|
803
|
+
const names = platform === "win32" ? ["soffice.exe", "soffice.com", "soffice"] : ["soffice", "libreoffice"];
|
|
804
|
+
const onPath = findOnPath(names, env, platform, pathExists);
|
|
805
|
+
if (onPath) {
|
|
806
|
+
const version = await probeSofficeVersion(run, env, onPath, platform, pathExists);
|
|
807
|
+
return { tool: "soffice", found: true, path: onPath, source: "path", version, probed: [onPath] };
|
|
808
|
+
}
|
|
809
|
+
probed.push(`PATH(${names.join("/")})`);
|
|
810
|
+
for (const candidate of wellKnownSofficePaths(env, platform)) {
|
|
811
|
+
if (pathExists(candidate)) {
|
|
812
|
+
const version = await probeSofficeVersion(run, env, candidate, platform, pathExists);
|
|
813
|
+
return {
|
|
814
|
+
tool: "soffice",
|
|
815
|
+
found: true,
|
|
816
|
+
path: candidate,
|
|
817
|
+
source: "wellknown",
|
|
818
|
+
version,
|
|
819
|
+
probed: [...probed, candidate]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
probed.push(candidate);
|
|
823
|
+
}
|
|
824
|
+
return { tool: "soffice", found: false, path: null, source: null, version: null, probed };
|
|
825
|
+
}
|
|
826
|
+
__name(detectSoffice, "detectSoffice");
|
|
827
|
+
async function detectPdftoppm(deps = {}) {
|
|
828
|
+
const env = deps.env ?? process.env;
|
|
829
|
+
const platform = deps.platform ?? process.platform;
|
|
830
|
+
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
831
|
+
const run = deps.run ?? defaultRunProcess;
|
|
832
|
+
const probed = [];
|
|
833
|
+
const explicit = env[PDFTOPPM_ENV]?.trim();
|
|
834
|
+
if (explicit) {
|
|
835
|
+
const exists = pathExists(explicit);
|
|
836
|
+
const version = exists ? await probeVersion(run, env, explicit, ["-v"], /pdftoppm version [\d.]+/i) : null;
|
|
837
|
+
return { tool: "pdftoppm", found: true, path: explicit, source: "env", version, exists, probed: [explicit] };
|
|
838
|
+
}
|
|
839
|
+
const names = platform === "win32" ? ["pdftoppm.exe", "pdftoppm"] : ["pdftoppm"];
|
|
840
|
+
const onPath = findOnPath(names, env, platform, pathExists);
|
|
841
|
+
if (onPath) {
|
|
842
|
+
const version = await probeVersion(run, env, onPath, ["-v"], /pdftoppm version [\d.]+/i);
|
|
843
|
+
return { tool: "pdftoppm", found: true, path: onPath, source: "path", version, probed: [onPath] };
|
|
844
|
+
}
|
|
845
|
+
probed.push(`PATH(${names.join("/")})`);
|
|
846
|
+
for (const candidate of wellKnownPdftoppmPaths(platform)) {
|
|
847
|
+
if (pathExists(candidate)) {
|
|
848
|
+
const version = await probeVersion(run, env, candidate, ["-v"], /pdftoppm version [\d.]+/i);
|
|
849
|
+
return {
|
|
850
|
+
tool: "pdftoppm",
|
|
851
|
+
found: true,
|
|
852
|
+
path: candidate,
|
|
853
|
+
source: "wellknown",
|
|
854
|
+
version,
|
|
855
|
+
probed: [...probed, candidate]
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
probed.push(candidate);
|
|
859
|
+
}
|
|
860
|
+
return { tool: "pdftoppm", found: false, path: null, source: null, version: null, probed };
|
|
861
|
+
}
|
|
862
|
+
__name(detectPdftoppm, "detectPdftoppm");
|
|
863
|
+
var FITZ_PROBE_SCRIPT = "import fitz,sys;print(getattr(fitz,'VersionBind','unknown'))";
|
|
864
|
+
async function detectPyMuPDF(deps = {}) {
|
|
865
|
+
const env = deps.env ?? process.env;
|
|
866
|
+
const platform = deps.platform ?? process.platform;
|
|
867
|
+
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
868
|
+
const run = deps.run ?? defaultRunProcess;
|
|
869
|
+
const probed = [];
|
|
870
|
+
const candidates = [];
|
|
871
|
+
const explicit = env[PYTHON_ENV]?.trim();
|
|
872
|
+
if (explicit) candidates.push({ command: explicit, source: "env" });
|
|
873
|
+
const pythonNames = platform === "win32" ? ["python.exe", "python3.exe"] : ["python3", "python"];
|
|
874
|
+
const onPath = findOnPath(pythonNames, env, platform, pathExists);
|
|
875
|
+
if (onPath) candidates.push({ command: onPath, source: "path" });
|
|
876
|
+
if (candidates.length === 0) {
|
|
877
|
+
probed.push(explicit ?? `PATH(${pythonNames.join("/")})`);
|
|
878
|
+
return { tool: "pymupdf", found: false, path: null, source: null, version: null, probed };
|
|
879
|
+
}
|
|
880
|
+
for (const candidate of candidates) {
|
|
881
|
+
probed.push(candidate.command);
|
|
882
|
+
const result = await run(candidate.command, ["-c", FITZ_PROBE_SCRIPT], {
|
|
883
|
+
env,
|
|
884
|
+
timeoutMs: 2e4
|
|
885
|
+
});
|
|
886
|
+
if (result.exitCode === 0 && result.stdout.trim().length > 0) {
|
|
887
|
+
return {
|
|
888
|
+
tool: "pymupdf",
|
|
889
|
+
found: true,
|
|
890
|
+
path: candidate.command,
|
|
891
|
+
source: candidate.source,
|
|
892
|
+
version: `PyMuPDF ${result.stdout.trim()}`,
|
|
893
|
+
probed
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return { tool: "pymupdf", found: false, path: null, source: null, version: null, probed };
|
|
898
|
+
}
|
|
899
|
+
__name(detectPyMuPDF, "detectPyMuPDF");
|
|
900
|
+
function unavailableResult(tool, message, installHint, probed) {
|
|
901
|
+
return {
|
|
902
|
+
ok: false,
|
|
903
|
+
error: { type: "tool_unavailable", tool, message, install_hint: installHint, probed }
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
__name(unavailableResult, "unavailableResult");
|
|
907
|
+
function isInsidePath(parentDir, target) {
|
|
908
|
+
const relative2 = path.relative(parentDir, target);
|
|
909
|
+
return relative2 !== "" && !relative2.startsWith("..") && !path.isAbsolute(relative2);
|
|
910
|
+
}
|
|
911
|
+
__name(isInsidePath, "isInsidePath");
|
|
912
|
+
function libreOfficeInstallRoot(sofficePath) {
|
|
913
|
+
return path.dirname(path.dirname(sofficePath));
|
|
914
|
+
}
|
|
915
|
+
__name(libreOfficeInstallRoot, "libreOfficeInstallRoot");
|
|
916
|
+
async function convertDocxToPdf(inputPath, options = {}, deps = {}) {
|
|
917
|
+
const env = deps.env ?? process.env;
|
|
918
|
+
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
919
|
+
const run = deps.run ?? defaultRunProcess;
|
|
920
|
+
const tmpRoot = deps.tmpRoot ?? os.tmpdir();
|
|
921
|
+
const startedAt = Date.now();
|
|
922
|
+
const resolvedInput = path.resolve(inputPath);
|
|
923
|
+
if (!pathExists(resolvedInput)) {
|
|
924
|
+
return { ok: false, error: { type: "input_not_found", message: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${resolvedInput}` } };
|
|
925
|
+
}
|
|
926
|
+
const soffice = await detectSoffice(deps);
|
|
927
|
+
if (!soffice.found || !soffice.path) {
|
|
928
|
+
return unavailableResult(
|
|
929
|
+
"libreoffice",
|
|
930
|
+
"\u672C\u673A\u672A\u627E\u5230 LibreOffice\uFF08soffice\uFF09\uFF0Cdocx\u2192PDF \u8F6C\u6362\u4E0D\u53EF\u7528\u3002",
|
|
931
|
+
"\u5B89\u88C5 LibreOffice\uFF08https://www.libreoffice.org/download/\uFF09\u540E\u91CD\u8BD5\uFF1B\u6216\u8BBE WS_SOFFICE \u6307\u5411 soffice \u53EF\u6267\u884C\u6587\u4EF6\u3002\u8BF7\u52FF\u73B0\u573A\u5199\u811A\u672C\u786C\u603C\u3002",
|
|
932
|
+
soffice.probed
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (soffice.exists === false) {
|
|
936
|
+
return {
|
|
937
|
+
ok: false,
|
|
938
|
+
error: {
|
|
939
|
+
type: "tool_unavailable",
|
|
940
|
+
message: `${SOFFICE_ENV} \u6307\u5411\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${soffice.path}`
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const installRoot = libreOfficeInstallRoot(soffice.path);
|
|
945
|
+
if (isInsidePath(installRoot, resolvedInput)) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
error: {
|
|
949
|
+
type: "libreoffice_install_dir_forbidden",
|
|
950
|
+
message: `\u8F93\u5165\u6587\u4EF6\u4F4D\u4E8E LibreOffice \u5B89\u88C5\u76EE\u5F55\u5185\uFF0C\u6309\u7EAA\u5F8B\u62D2\u7EDD\u8F6C\u6362\uFF1A${resolvedInput}`
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const outputDir = path.resolve(options.outputDir ?? path.dirname(resolvedInput));
|
|
955
|
+
if (isInsidePath(installRoot, outputDir)) {
|
|
956
|
+
return {
|
|
957
|
+
ok: false,
|
|
958
|
+
error: {
|
|
959
|
+
type: "libreoffice_install_dir_forbidden",
|
|
960
|
+
message: `\u8F93\u51FA\u76EE\u5F55\u4F4D\u4E8E LibreOffice \u5B89\u88C5\u76EE\u5F55\u5185\uFF0C\u6309\u7EAA\u5F8B\u62D2\u7EDD\uFF1A${outputDir}`
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
965
|
+
const resolvedTmpRoot = path.resolve(tmpRoot);
|
|
966
|
+
const profile = fs.mkdtempSync(path.join(resolvedTmpRoot, "ws-lo-profile-"));
|
|
967
|
+
if (!isInsidePath(resolvedTmpRoot, profile)) {
|
|
968
|
+
return { ok: false, error: { type: "profile_outside_tmp", message: `LibreOffice profile \u8D8A\u51FA\u4E34\u65F6\u6839\uFF1A${profile}` } };
|
|
969
|
+
}
|
|
970
|
+
const configDir = path.join(profile, "xdg_config");
|
|
971
|
+
const cacheDir = path.join(profile, "xdg_cache");
|
|
972
|
+
const tempDir = path.join(profile, "tmp");
|
|
973
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
974
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
975
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
976
|
+
const childEnv = {
|
|
977
|
+
...env,
|
|
978
|
+
HOME: profile,
|
|
979
|
+
XDG_CONFIG_HOME: configDir,
|
|
980
|
+
XDG_CACHE_HOME: cacheDir,
|
|
981
|
+
TEMP: tempDir,
|
|
982
|
+
TMP: tempDir
|
|
983
|
+
};
|
|
984
|
+
const timeoutMs = Math.min(Math.max((options.timeoutSeconds ?? 180) * 1e3, 1e4), 6e5);
|
|
985
|
+
const args = [
|
|
986
|
+
"--headless",
|
|
987
|
+
"--invisible",
|
|
988
|
+
"--norestore",
|
|
989
|
+
`-env:UserInstallation=${pathToFileURL(profile).href}`,
|
|
990
|
+
"--convert-to",
|
|
991
|
+
"pdf",
|
|
992
|
+
"--outdir",
|
|
993
|
+
outputDir,
|
|
994
|
+
resolvedInput
|
|
995
|
+
];
|
|
996
|
+
try {
|
|
997
|
+
const result = await run(soffice.path, args, { env: childEnv, timeoutMs, signal: deps.signal });
|
|
998
|
+
const expectedPdf = path.join(outputDir, `${path.parse(resolvedInput).name}.pdf`);
|
|
999
|
+
if (!result.timedOut && result.exitCode === 0 && pathExists(expectedPdf) && fs.statSync(expectedPdf).size > 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
ok: true,
|
|
1002
|
+
pdfPath: expectedPdf,
|
|
1003
|
+
soffice: { path: soffice.path, version: soffice.version },
|
|
1004
|
+
durationMs: Date.now() - startedAt
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
error: {
|
|
1010
|
+
type: result.timedOut ? "conversion_timeout" : "conversion_failed",
|
|
1011
|
+
message: result.timedOut ? `LibreOffice \u8F6C\u6362\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09` : `LibreOffice \u8F6C\u6362\u5931\u8D25\uFF08exit=${result.exitCode ?? "spawn-error"}\uFF09`,
|
|
1012
|
+
exitCode: result.exitCode,
|
|
1013
|
+
stderr: result.stderr || result.spawnError || void 0,
|
|
1014
|
+
stdout: result.stdout || void 0
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
} finally {
|
|
1018
|
+
fs.rmSync(profile, { recursive: true, force: true });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
__name(convertDocxToPdf, "convertDocxToPdf");
|
|
1022
|
+
var PYMUPDF_RENDER_SCRIPT = [
|
|
1023
|
+
"import sys, os, fitz",
|
|
1024
|
+
"pdf, outdir, dpi = sys.argv[1], sys.argv[2], float(sys.argv[3])",
|
|
1025
|
+
"first, last = int(sys.argv[4]), int(sys.argv[5])",
|
|
1026
|
+
"doc = fitz.open(pdf)",
|
|
1027
|
+
"zoom = dpi / 72.0",
|
|
1028
|
+
"mat = fitz.Matrix(zoom, zoom)",
|
|
1029
|
+
"total = doc.page_count",
|
|
1030
|
+
"lo = max(first, 1)",
|
|
1031
|
+
"hi = min(last, total) if last > 0 else total",
|
|
1032
|
+
"count = 0",
|
|
1033
|
+
"for pno in range(lo - 1, hi):",
|
|
1034
|
+
" pix = doc.load_page(pno).get_pixmap(matrix=mat)",
|
|
1035
|
+
" pix.save(os.path.join(outdir, f'page-{pno + 1}.png'))",
|
|
1036
|
+
" count += 1",
|
|
1037
|
+
"print(f'pages={count}')"
|
|
1038
|
+
].join("\n");
|
|
1039
|
+
function collectPngs(dir) {
|
|
1040
|
+
return fs.readdirSync(dir).filter((name) => /^page-\d+\.png$/i.test(name)).sort((a, b) => parseInt(/\d+/.exec(a)[0], 10) - parseInt(/\d+/.exec(b)[0], 10)).map((name) => path.join(dir, name));
|
|
1041
|
+
}
|
|
1042
|
+
__name(collectPngs, "collectPngs");
|
|
1043
|
+
async function convertPdfToPng(inputPath, options = {}, deps = {}) {
|
|
1044
|
+
const env = deps.env ?? process.env;
|
|
1045
|
+
const platform = deps.platform ?? process.platform;
|
|
1046
|
+
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
1047
|
+
const run = deps.run ?? defaultRunProcess;
|
|
1048
|
+
const tmpRoot = deps.tmpRoot ?? os.tmpdir();
|
|
1049
|
+
const resolvedInput = path.resolve(inputPath);
|
|
1050
|
+
if (!pathExists(resolvedInput)) {
|
|
1051
|
+
return { ok: false, error: { type: "input_not_found", message: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${resolvedInput}` } };
|
|
1052
|
+
}
|
|
1053
|
+
const dpi = Math.min(Math.max(Math.round(options.dpi ?? 150), 50), 400);
|
|
1054
|
+
const firstPage = Math.max(1, Math.round(options.firstPage ?? 1));
|
|
1055
|
+
const lastPage = Math.round(options.lastPage ?? 0);
|
|
1056
|
+
const outputDir = path.resolve(
|
|
1057
|
+
options.outputDir ?? path.join(path.dirname(resolvedInput), `${path.parse(resolvedInput).name}-pages`)
|
|
1058
|
+
);
|
|
1059
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1060
|
+
const pdftoppm = await detectPdftoppm(deps);
|
|
1061
|
+
if (pdftoppm.found && pdftoppm.path && pdftoppm.exists !== false) {
|
|
1062
|
+
const pageArgs = [
|
|
1063
|
+
...firstPage > 1 ? ["-f", String(firstPage)] : [],
|
|
1064
|
+
...lastPage > 0 ? ["-l", String(lastPage)] : []
|
|
1065
|
+
];
|
|
1066
|
+
if (platform === "win32") {
|
|
1067
|
+
const workDir = fs.mkdtempSync(path.join(path.resolve(tmpRoot), "ws-poppler-"));
|
|
1068
|
+
try {
|
|
1069
|
+
const workPdf = path.join(workDir, "input.pdf");
|
|
1070
|
+
fs.copyFileSync(resolvedInput, workPdf);
|
|
1071
|
+
const result2 = await run(
|
|
1072
|
+
pdftoppm.path,
|
|
1073
|
+
["-png", "-r", String(dpi), ...pageArgs, workPdf, path.join(workDir, "page")],
|
|
1074
|
+
{
|
|
1075
|
+
env,
|
|
1076
|
+
timeoutMs: 3e5,
|
|
1077
|
+
signal: deps.signal
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
const produced2 = collectPngs(workDir);
|
|
1081
|
+
if (!result2.timedOut && result2.exitCode === 0 && produced2.length > 0) {
|
|
1082
|
+
const pngPaths = [];
|
|
1083
|
+
for (const producedPath of produced2) {
|
|
1084
|
+
const pageNo = /\d+/.exec(path.basename(producedPath))[0];
|
|
1085
|
+
const target = path.join(outputDir, `page-${parseInt(pageNo, 10)}.png`);
|
|
1086
|
+
fs.copyFileSync(producedPath, target);
|
|
1087
|
+
pngPaths.push(target);
|
|
1088
|
+
}
|
|
1089
|
+
return { ok: true, pngPaths, pageCount: pngPaths.length, engine: "pdftoppm", dpi, outputDir };
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
error: {
|
|
1094
|
+
type: result2.timedOut ? "conversion_timeout" : "conversion_failed",
|
|
1095
|
+
message: `pdftoppm \u6E32\u67D3\u5931\u8D25\uFF08exit=${result2.exitCode ?? "spawn-error"}\uFF09`,
|
|
1096
|
+
engine: "pdftoppm",
|
|
1097
|
+
exitCode: result2.exitCode,
|
|
1098
|
+
stderr: result2.stderr || result2.spawnError || void 0
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
} finally {
|
|
1102
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const result = await run(
|
|
1106
|
+
pdftoppm.path,
|
|
1107
|
+
["-png", "-r", String(dpi), ...pageArgs, resolvedInput, path.join(outputDir, "page")],
|
|
1108
|
+
{
|
|
1109
|
+
env,
|
|
1110
|
+
timeoutMs: 3e5,
|
|
1111
|
+
signal: deps.signal
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
const produced = collectPngs(outputDir);
|
|
1115
|
+
if (!result.timedOut && result.exitCode === 0 && produced.length > 0) {
|
|
1116
|
+
return { ok: true, pngPaths: produced, pageCount: produced.length, engine: "pdftoppm", dpi, outputDir };
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
ok: false,
|
|
1120
|
+
error: {
|
|
1121
|
+
type: result.timedOut ? "conversion_timeout" : "conversion_failed",
|
|
1122
|
+
message: `pdftoppm \u6E32\u67D3\u5931\u8D25\uFF08exit=${result.exitCode ?? "spawn-error"}\uFF09`,
|
|
1123
|
+
engine: "pdftoppm",
|
|
1124
|
+
exitCode: result.exitCode,
|
|
1125
|
+
stderr: result.stderr || result.spawnError || void 0
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const pymupdf = await detectPyMuPDF(deps);
|
|
1130
|
+
if (pymupdf.found && pymupdf.path) {
|
|
1131
|
+
const result = await run(
|
|
1132
|
+
pymupdf.path,
|
|
1133
|
+
["-c", PYMUPDF_RENDER_SCRIPT, resolvedInput, outputDir, String(dpi), String(firstPage), String(lastPage)],
|
|
1134
|
+
{ env, timeoutMs: 3e5, signal: deps.signal }
|
|
1135
|
+
);
|
|
1136
|
+
const produced = collectPngs(outputDir);
|
|
1137
|
+
if (!result.timedOut && result.exitCode === 0 && produced.length > 0) {
|
|
1138
|
+
return { ok: true, pngPaths: produced, pageCount: produced.length, engine: "pymupdf", dpi, outputDir };
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
ok: false,
|
|
1142
|
+
error: {
|
|
1143
|
+
type: result.timedOut ? "conversion_timeout" : "conversion_failed",
|
|
1144
|
+
message: `PyMuPDF \u6E32\u67D3\u5931\u8D25\uFF08exit=${result.exitCode ?? "spawn-error"}\uFF09`,
|
|
1145
|
+
engine: "pymupdf",
|
|
1146
|
+
exitCode: result.exitCode,
|
|
1147
|
+
stderr: result.stderr || result.spawnError || void 0
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
return unavailableResult(
|
|
1152
|
+
"pdftoppm+pymupdf",
|
|
1153
|
+
"\u672C\u673A\u65E2\u672A\u627E\u5230 pdftoppm\uFF08poppler\uFF09\uFF0C\u4E5F\u672A\u627E\u5230\u88C5\u4E86 PyMuPDF \u7684 python\uFF0CPDF\u2192PNG \u6E32\u67D3\u4E0D\u53EF\u7528\u3002",
|
|
1154
|
+
"\u88C5 poppler\uFF08Windows \u53D1\u884C\u5305\u89E3\u538B\u540E\u628A bin \u52A0 PATH\xB7macOS `brew install poppler`\xB7Linux `apt install poppler-utils`\uFF09\uFF0C\u6216 `pip install pymupdf`\uFF1B\u4EA6\u53EF\u7528 WS_PDFTOPPM / WS_PYTHON \u663E\u5F0F\u6307\u8DEF\u3002\u8BF7\u52FF\u73B0\u573A\u5199\u811A\u672C\u786C\u603C\u3002",
|
|
1155
|
+
[...pdftoppm.probed, ...pymupdf.probed]
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
__name(convertPdfToPng, "convertPdfToPng");
|
|
1159
|
+
async function probeEnvironment(deps = {}) {
|
|
1160
|
+
const platform = deps.platform ?? process.platform;
|
|
1161
|
+
const [soffice, pdftoppm, pymupdf] = await Promise.all([
|
|
1162
|
+
detectSoffice(deps),
|
|
1163
|
+
detectPdftoppm(deps),
|
|
1164
|
+
detectPyMuPDF(deps)
|
|
1165
|
+
]);
|
|
1166
|
+
const notes = [];
|
|
1167
|
+
if (!soffice.found || soffice.exists === false) {
|
|
1168
|
+
notes.push("docx\u2192PDF \u4E0D\u53EF\u7528\uFF1A\u672A\u627E\u5230 LibreOffice\u2014\u2014\u88C5 LibreOffice \u6216\u8BBE WS_SOFFICE \u540E\u91CD\u8BD5\uFF1B\u4E0D\u8981\u73B0\u573A\u5199\u4EE3\u7801\u786C\u603C\u3002");
|
|
1169
|
+
}
|
|
1170
|
+
if (!pdftoppm.found && !pymupdf.found) {
|
|
1171
|
+
notes.push(
|
|
1172
|
+
"PDF\u2192PNG \u4E0D\u53EF\u7528\uFF1Apdftoppm \u4E0E PyMuPDF \u53CC\u53CC\u7F3A\u5E2D\u2014\u2014\u88C5 poppler \u6216 `pip install pymupdf`\uFF1B\u4E0D\u8981\u73B0\u573A\u5199\u4EE3\u7801\u786C\u603C\u3002"
|
|
1173
|
+
);
|
|
1174
|
+
} else if (!pdftoppm.found) {
|
|
1175
|
+
notes.push("pdftoppm \u7F3A\u5E2D\uFF0CPDF\u2192PNG \u8D70 PyMuPDF fallback\uFF08\u53EF\u7528\uFF09\u3002");
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
platform,
|
|
1179
|
+
node: process.version,
|
|
1180
|
+
tools: { soffice, pdftoppm, pymupdf },
|
|
1181
|
+
capabilities: {
|
|
1182
|
+
read_docx: true,
|
|
1183
|
+
write_docx: true,
|
|
1184
|
+
edit_docx: true,
|
|
1185
|
+
docx_to_pdf: soffice.found && soffice.exists !== false,
|
|
1186
|
+
pdf_to_png: pdftoppm.found && pdftoppm.exists !== false || pymupdf.found
|
|
1187
|
+
},
|
|
1188
|
+
honest_notes: notes
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
__name(probeEnvironment, "probeEnvironment");
|
|
1192
|
+
|
|
1193
|
+
// packages/core/src/mcp/servers/doc-toolchain-server.ts
|
|
1194
|
+
var DOC_TOOLCHAIN_SERVER_NAME = "doc_toolchain";
|
|
1195
|
+
var DOC_TOOLCHAIN_TOOL_PREFIX = "mcp__doc_toolchain__";
|
|
1196
|
+
var EDIT_OPS = ["replace_paragraph_text", "insert_paragraph", "delete_paragraph", "append_paragraph"];
|
|
1197
|
+
var DOC_TOOLCHAIN_TOOL_DEFINITIONS = [
|
|
1198
|
+
{
|
|
1199
|
+
name: "probe_environment",
|
|
1200
|
+
description: "\u63A2\u6D4B\u672C\u673A\u6587\u6863\u5DE5\u5177\u94FE\u73AF\u5883\uFF1ALibreOffice\uFF08docx\u2192PDF\uFF09\u3001pdftoppm/PyMuPDF\uFF08PDF\u2192PNG\uFF09\u5728\u573A\u4E0E\u5426\u3001\u8DEF\u5F84\u4E0E\u7248\u672C\u3002docx \u8BFB/\u5199/\u6539\u4E3A\u7EAF\u5185\u7F6E\u80FD\u529B\u6052\u53EF\u7528\u3002\u8F6C\u6362\u7C7B\u4EFB\u52A1\u524D\u53EF\u5148\u63A2\u4E00\u6B21\uFF1B\u7F3A\u5931\u65F6\u5DE5\u5177\u4F1A\u8BDA\u5B9E\u964D\u7EA7\uFF0C\u4E0D\u8981\u73B0\u573A\u5199\u4EE3\u7801\u786C\u603C\u3002",
|
|
1201
|
+
inputSchema: {
|
|
1202
|
+
type: "object",
|
|
1203
|
+
properties: {},
|
|
1204
|
+
additionalProperties: false
|
|
1205
|
+
},
|
|
1206
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
name: "read_docx",
|
|
1210
|
+
description: "\u8BFB docx\uFF1AOOXML \u89E3\u6790\u51FA\u6B63\u6587\u5757\u5E8F\u5217\uFF08\u6BB5\u843D\u951A paragraphIndex + \u6837\u5F0F + \u6587\u672C\uFF0C\u8868\u683C\u6309\u884C/\u683C\uFF09\u4E0E\u5B57\u6570\u7EDF\u8BA1\uFF1BincludeNotes=true \u65F6\u9644\u811A\u6CE8/\u5C3E\u6CE8/\u6279\u6CE8\u3002\u8868\u683C\u5185\u6BB5\u843D\u53EA\u8BFB\uFF08\u7F16\u8F91\u951A\u4E0D\u8986\u76D6\uFF09\u3002",
|
|
1211
|
+
inputSchema: {
|
|
1212
|
+
type: "object",
|
|
1213
|
+
properties: {
|
|
1214
|
+
path: { type: "string", description: "docx \u6587\u4EF6\u8DEF\u5F84" },
|
|
1215
|
+
includeNotes: { type: "boolean", default: false, description: "\u662F\u5426\u9644\u811A\u6CE8/\u5C3E\u6CE8/\u6279\u6CE8" }
|
|
1216
|
+
},
|
|
1217
|
+
required: ["path"],
|
|
1218
|
+
additionalProperties: false
|
|
1219
|
+
},
|
|
1220
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
name: "write_docx",
|
|
1224
|
+
description: "\u4ECE markdown \u5B50\u96C6\u65B0\u5EFA docx\uFF1A# / ## / ### \u6807\u9898\u3001- \u5217\u8868\u3001\u7A7A\u884C\u5206\u6BB5\uFF08\u6BB5\u5185\u6362\u884C\u6309 CJK \u53CB\u597D\u89C4\u5219\u62FC\u63A5\uFF09\u3002A4 \u7248\u5F0F\u3001Times New Roman/\u5B8B\u4F53\u57FA\u7EBF\u3002\u5DF2\u5B58\u5728\u6587\u4EF6\u987B overwrite=true \u624D\u8986\u76D6\u3002\u884C\u5185\u5B57\u7B26\u683C\u5F0F\uFF08\u7C97\u659C\u4F53\u7B49\uFF09v1 \u4E0D\u652F\u6301\uFF0C\u5982\u5B9E\u77E5\u6089\u3002",
|
|
1225
|
+
inputSchema: {
|
|
1226
|
+
type: "object",
|
|
1227
|
+
properties: {
|
|
1228
|
+
path: { type: "string", description: "\u8F93\u51FA docx \u8DEF\u5F84" },
|
|
1229
|
+
markdown: { type: "string", description: "markdown \u5B50\u96C6\u6B63\u6587" },
|
|
1230
|
+
overwrite: { type: "boolean", default: false }
|
|
1231
|
+
},
|
|
1232
|
+
required: ["path", "markdown"],
|
|
1233
|
+
additionalProperties: false
|
|
1234
|
+
},
|
|
1235
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
name: "edit_docx",
|
|
1239
|
+
description: "\u6BB5\u843D\u951A\u5B9A\u7F16\u8F91\u65E2\u6709 docx\uFF1Aops \u5168\u90E8\u951A\u5B9A\u300C\u7F16\u8F91\u524D\u300D\u6BB5\u843D\u7F16\u53F7\uFF08read_docx \u7684 paragraphIndex\uFF09\uFF0C\u4E00\u6B21\u6027\u5957\u7528\u3002\u64CD\u4F5C\uFF1Areplace_paragraph_text\uFF08\u6362\u6587\xB7\u4FDD\u7559\u6BB5\u843D\u7EA7\u683C\u5F0F\xB7run \u7EA7\u5B57\u7B26\u683C\u5F0F\u5F52\u4E00\uFF09/ insert_paragraph\uFF08before|after\uFF09/ delete_paragraph / append_paragraph\u3002\u53EA\u91CD\u5199 document.xml\uFF0C\u5176\u4F59\u5305\u5185\u5BB9\u5B57\u8282\u539F\u6837\u4FDD\u7559\u3002\u9ED8\u8BA4\u539F\u5730\u8986\u76D6\u5E76\u7559 .bak-\u65F6\u95F4\u6233 \u5907\u4EFD\uFF1B\u7ED9 outputPath \u5219\u53E6\u5B58\u3002\u8868\u683C\u5185\u6BB5\u843D\u3001\u4FEE\u8BA2/\u6279\u6CE8\u751F\u6210 v1 \u4E0D\u652F\u6301\u3002",
|
|
1240
|
+
inputSchema: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
properties: {
|
|
1243
|
+
path: { type: "string", description: "\u8F93\u5165 docx \u8DEF\u5F84" },
|
|
1244
|
+
ops: {
|
|
1245
|
+
type: "array",
|
|
1246
|
+
description: "\u7F16\u8F91\u64CD\u4F5C\u5E8F\u5217\uFF08\u951A\u5B9A\u7F16\u8F91\u524D\u6BB5\u843D\u7F16\u53F7\uFF09",
|
|
1247
|
+
items: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
properties: {
|
|
1250
|
+
op: { type: "string", enum: [...EDIT_OPS] },
|
|
1251
|
+
index: { type: "number" },
|
|
1252
|
+
position: { type: "string", enum: ["before", "after"] },
|
|
1253
|
+
text: { type: "string" },
|
|
1254
|
+
style: { type: "string" }
|
|
1255
|
+
},
|
|
1256
|
+
required: ["op"]
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
outputPath: { type: "string", description: "\u53E6\u5B58\u8DEF\u5F84\uFF08\u7F3A\u7701=\u539F\u5730\u8986\u76D6+\u81EA\u52A8\u5907\u4EFD\uFF09" }
|
|
1260
|
+
},
|
|
1261
|
+
required: ["path", "ops"],
|
|
1262
|
+
additionalProperties: false
|
|
1263
|
+
},
|
|
1264
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "docx_to_pdf",
|
|
1268
|
+
description: "docx \u8F6C PDF\uFF1ALibreOffice headless \u96C6\u6210\uFF08soffice \u63A2\u6D4B\uFF1AWS_SOFFICE > PATH > \u5468\u77E5\u5B89\u88C5\u4F4D\uFF1B\u72EC\u7ACB -env:UserInstallation profile \u843D\u4E34\u65F6\u76EE\u5F55\uFF1BLibreOffice \u5B89\u88C5\u76EE\u5F55\u7EDD\u5BF9\u7981\u5199\uFF09\u3002\u73AF\u5883\u7F3A\u5931\u8FD4\u56DE tool_unavailable \u8BDA\u5B9E\u964D\u7EA7+\u5B89\u88C5\u6307\u5F15\uFF0C\u6B64\u65F6\u5982\u5B9E\u544A\u77E5\u7528\u6237\uFF0C\u4E0D\u8981\u73B0\u573A\u5199\u4EE3\u7801\u786C\u603C\u3002\u8F93\u51FA\u9ED8\u8BA4\u4E0E\u8F93\u5165\u540C\u76EE\u5F55\u540C\u540D .pdf\u3002",
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
type: "object",
|
|
1271
|
+
properties: {
|
|
1272
|
+
path: { type: "string", description: "\u8F93\u5165 docx \u8DEF\u5F84" },
|
|
1273
|
+
outputDir: { type: "string", description: "PDF \u8F93\u51FA\u76EE\u5F55\uFF08\u7F3A\u7701=\u8F93\u5165\u6587\u4EF6\u6240\u5728\u76EE\u5F55\uFF09" },
|
|
1274
|
+
timeoutSeconds: { type: "number", description: "\u8F6C\u6362\u8D85\u65F6\u79D2\u6570\uFF08\u9ED8\u8BA4 180\xB7\u4E0A\u9650 600\uFF09" }
|
|
1275
|
+
},
|
|
1276
|
+
required: ["path"],
|
|
1277
|
+
additionalProperties: false
|
|
1278
|
+
},
|
|
1279
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
name: "pdf_to_png",
|
|
1283
|
+
description: "PDF \u6E32\u67D3\u4E3A\u9010\u9875 PNG\uFF1Apdftoppm \u4F18\u5148\u3001PyMuPDF fallback\uFF08\u63A2\u6D4B\uFF1AWS_PDFTOPPM/WS_PYTHON > PATH > \u5468\u77E5\u4F4D\uFF1Bwin32 \u4E00\u5F8B\u5728 ASCII \u4E34\u65F6\u76EE\u5F55\u8DD1 poppler \u518D\u62F7\u56DE\uFF09\u3002\u4EA7\u7269 page-<\u9875\u7801>.png \u8DEF\u5F84\u5217\u8868\u76F4\u63A5\u7ED9\u89C6\u89C9\u901A\u9053\u770B\u56FE\u9A8C\u6536\u3002\u53CC\u7F3A\u5E2D\u8FD4\u56DE tool_unavailable \u8BDA\u5B9E\u964D\u7EA7+\u5B89\u88C5\u6307\u5F15\uFF0C\u4E0D\u8981\u73B0\u573A\u5199\u4EE3\u7801\u786C\u603C\u3002",
|
|
1284
|
+
inputSchema: {
|
|
1285
|
+
type: "object",
|
|
1286
|
+
properties: {
|
|
1287
|
+
path: { type: "string", description: "\u8F93\u5165 PDF \u8DEF\u5F84" },
|
|
1288
|
+
outputDir: { type: "string", description: "PNG \u8F93\u51FA\u76EE\u5F55\uFF08\u7F3A\u7701=<PDF\u76EE\u5F55>/<\u539F\u540D>-pages/\uFF09" },
|
|
1289
|
+
dpi: { type: "number", description: "\u6E32\u67D3 DPI\uFF08\u9ED8\u8BA4 150\xB7\u8303\u56F4 50-400\uFF09" },
|
|
1290
|
+
firstPage: { type: "number", description: "\u8D77\u59CB\u9875\uFF08\u9ED8\u8BA4 1\uFF09" },
|
|
1291
|
+
lastPage: { type: "number", description: "\u622A\u6B62\u9875\uFF08\u9ED8\u8BA4 0=\u5230\u672B\u9875\uFF09" }
|
|
1292
|
+
},
|
|
1293
|
+
required: ["path"],
|
|
1294
|
+
additionalProperties: false
|
|
1295
|
+
},
|
|
1296
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1297
|
+
}
|
|
1298
|
+
];
|
|
1299
|
+
function textResult(text, isError = false) {
|
|
1300
|
+
return {
|
|
1301
|
+
content: [{ type: "text", text }],
|
|
1302
|
+
...isError ? { isError: true } : {}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
__name(textResult, "textResult");
|
|
1306
|
+
function asObject(value) {
|
|
1307
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1308
|
+
}
|
|
1309
|
+
__name(asObject, "asObject");
|
|
1310
|
+
function asString(value) {
|
|
1311
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
1312
|
+
}
|
|
1313
|
+
__name(asString, "asString");
|
|
1314
|
+
function asNumber(value) {
|
|
1315
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1316
|
+
}
|
|
1317
|
+
__name(asNumber, "asNumber");
|
|
1318
|
+
function errorResult(error) {
|
|
1319
|
+
if (error instanceof OoxmlFormatError || error instanceof ZipFormatError) {
|
|
1320
|
+
return textResult(JSON.stringify({ error: { type: "bad_document", message: error.message } }), true);
|
|
1321
|
+
}
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
return textResult(JSON.stringify({ error: { type: "internal_error", message } }), true);
|
|
1324
|
+
}
|
|
1325
|
+
__name(errorResult, "errorResult");
|
|
1326
|
+
function backupPathFor(target) {
|
|
1327
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1328
|
+
return `${target}.bak-${stamp}`;
|
|
1329
|
+
}
|
|
1330
|
+
__name(backupPathFor, "backupPathFor");
|
|
1331
|
+
async function executeDocToolchainTool(name, args, deps = {}) {
|
|
1332
|
+
const record = asObject(args) ?? {};
|
|
1333
|
+
try {
|
|
1334
|
+
if (name === "probe_environment") {
|
|
1335
|
+
return textResult(JSON.stringify(await probeEnvironment(deps)));
|
|
1336
|
+
}
|
|
1337
|
+
if (name === "read_docx") {
|
|
1338
|
+
const target = asString(record.path);
|
|
1339
|
+
if (!target)
|
|
1340
|
+
return textResult(JSON.stringify({ error: { type: "invalid_arguments", message: "\u7F3A path" } }), true);
|
|
1341
|
+
const resolved = path2.resolve(target);
|
|
1342
|
+
if (!fs2.existsSync(resolved)) {
|
|
1343
|
+
return textResult(
|
|
1344
|
+
JSON.stringify({ error: { type: "input_not_found", message: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${resolved}` } }),
|
|
1345
|
+
true
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
const result = readDocx(fs2.readFileSync(resolved), {
|
|
1349
|
+
includeNotes: record.includeNotes === true
|
|
1350
|
+
});
|
|
1351
|
+
return textResult(JSON.stringify(result));
|
|
1352
|
+
}
|
|
1353
|
+
if (name === "write_docx") {
|
|
1354
|
+
const target = asString(record.path);
|
|
1355
|
+
const markdown = typeof record.markdown === "string" ? record.markdown : null;
|
|
1356
|
+
if (!target || markdown === null) {
|
|
1357
|
+
return textResult(
|
|
1358
|
+
JSON.stringify({ error: { type: "invalid_arguments", message: "\u7F3A path \u6216 markdown" } }),
|
|
1359
|
+
true
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const resolved = path2.resolve(target);
|
|
1363
|
+
if (fs2.existsSync(resolved) && record.overwrite !== true) {
|
|
1364
|
+
return textResult(
|
|
1365
|
+
JSON.stringify({
|
|
1366
|
+
error: { type: "already_exists", message: `\u6587\u4EF6\u5DF2\u5B58\u5728\uFF08\u987B overwrite=true \u624D\u8986\u76D6\uFF09\uFF1A${resolved}` }
|
|
1367
|
+
}),
|
|
1368
|
+
true
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
fs2.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
1372
|
+
fs2.writeFileSync(resolved, buildDocx(markdown));
|
|
1373
|
+
return textResult(JSON.stringify({ ok: true, path: resolved, bytes: fs2.statSync(resolved).size }));
|
|
1374
|
+
}
|
|
1375
|
+
if (name === "edit_docx") {
|
|
1376
|
+
const target = asString(record.path);
|
|
1377
|
+
if (!target || !Array.isArray(record.ops)) {
|
|
1378
|
+
return textResult(JSON.stringify({ error: { type: "invalid_arguments", message: "\u7F3A path \u6216 ops" } }), true);
|
|
1379
|
+
}
|
|
1380
|
+
const ops = record.ops;
|
|
1381
|
+
const resolved = path2.resolve(target);
|
|
1382
|
+
if (!fs2.existsSync(resolved)) {
|
|
1383
|
+
return textResult(
|
|
1384
|
+
JSON.stringify({ error: { type: "input_not_found", message: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${resolved}` } }),
|
|
1385
|
+
true
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
const result = editDocx(fs2.readFileSync(resolved), ops);
|
|
1389
|
+
const output = asString(record.outputPath) ? path2.resolve(record.outputPath) : resolved;
|
|
1390
|
+
fs2.mkdirSync(path2.dirname(output), { recursive: true });
|
|
1391
|
+
let backup = null;
|
|
1392
|
+
if (output === resolved) {
|
|
1393
|
+
backup = backupPathFor(resolved);
|
|
1394
|
+
fs2.copyFileSync(resolved, backup);
|
|
1395
|
+
}
|
|
1396
|
+
fs2.writeFileSync(output, result.buffer);
|
|
1397
|
+
return textResult(
|
|
1398
|
+
JSON.stringify({
|
|
1399
|
+
ok: true,
|
|
1400
|
+
path: output,
|
|
1401
|
+
backup,
|
|
1402
|
+
applied: result.applied,
|
|
1403
|
+
paragraphCountBefore: result.paragraphCountBefore,
|
|
1404
|
+
paragraphCountAfter: result.paragraphCountAfter
|
|
1405
|
+
})
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (name === "docx_to_pdf") {
|
|
1409
|
+
const target = asString(record.path);
|
|
1410
|
+
if (!target)
|
|
1411
|
+
return textResult(JSON.stringify({ error: { type: "invalid_arguments", message: "\u7F3A path" } }), true);
|
|
1412
|
+
const result = await convertDocxToPdf(
|
|
1413
|
+
target,
|
|
1414
|
+
{ outputDir: asString(record.outputDir) ?? void 0, timeoutSeconds: asNumber(record.timeoutSeconds) },
|
|
1415
|
+
deps
|
|
1416
|
+
);
|
|
1417
|
+
return textResult(JSON.stringify(result), !result.ok);
|
|
1418
|
+
}
|
|
1419
|
+
if (name === "pdf_to_png") {
|
|
1420
|
+
const target = asString(record.path);
|
|
1421
|
+
if (!target)
|
|
1422
|
+
return textResult(JSON.stringify({ error: { type: "invalid_arguments", message: "\u7F3A path" } }), true);
|
|
1423
|
+
const result = await convertPdfToPng(
|
|
1424
|
+
target,
|
|
1425
|
+
{
|
|
1426
|
+
outputDir: asString(record.outputDir) ?? void 0,
|
|
1427
|
+
dpi: asNumber(record.dpi),
|
|
1428
|
+
firstPage: asNumber(record.firstPage),
|
|
1429
|
+
lastPage: asNumber(record.lastPage)
|
|
1430
|
+
},
|
|
1431
|
+
deps
|
|
1432
|
+
);
|
|
1433
|
+
return textResult(JSON.stringify(result), !result.ok);
|
|
1434
|
+
}
|
|
1435
|
+
return textResult(JSON.stringify({ error: { type: "unknown_tool", message: name } }), true);
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
return errorResult(error);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
__name(executeDocToolchainTool, "executeDocToolchainTool");
|
|
1441
|
+
var activeToolCalls = /* @__PURE__ */ new Map();
|
|
1442
|
+
function send(value) {
|
|
1443
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
1444
|
+
`);
|
|
1445
|
+
}
|
|
1446
|
+
__name(send, "send");
|
|
1447
|
+
async function handleRpc(message) {
|
|
1448
|
+
if (typeof message.method !== "string") return;
|
|
1449
|
+
if (message.method === "notifications/cancelled") {
|
|
1450
|
+
const params = asObject(message.params);
|
|
1451
|
+
activeToolCalls.get(params?.requestId)?.abort();
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (message.id === void 0) return;
|
|
1455
|
+
let result;
|
|
1456
|
+
if (message.method === "initialize") {
|
|
1457
|
+
result = {
|
|
1458
|
+
protocolVersion: "2025-03-26",
|
|
1459
|
+
capabilities: { tools: {} },
|
|
1460
|
+
serverInfo: { name: DOC_TOOLCHAIN_SERVER_NAME, version: "1.0.0" }
|
|
1461
|
+
};
|
|
1462
|
+
} else if (message.method === "tools/list") {
|
|
1463
|
+
result = { tools: DOC_TOOLCHAIN_TOOL_DEFINITIONS };
|
|
1464
|
+
} else if (message.method === "tools/call") {
|
|
1465
|
+
const params = asObject(message.params);
|
|
1466
|
+
const controller = new AbortController();
|
|
1467
|
+
activeToolCalls.set(message.id, controller);
|
|
1468
|
+
try {
|
|
1469
|
+
result = await executeDocToolchainTool(String(params?.name ?? ""), params?.arguments, {
|
|
1470
|
+
signal: controller.signal
|
|
1471
|
+
});
|
|
1472
|
+
} finally {
|
|
1473
|
+
activeToolCalls.delete(message.id);
|
|
1474
|
+
}
|
|
1475
|
+
} else if (message.method === "prompts/list") {
|
|
1476
|
+
result = { prompts: [] };
|
|
1477
|
+
} else if (message.method === "resources/list") {
|
|
1478
|
+
result = { resources: [] };
|
|
1479
|
+
} else {
|
|
1480
|
+
send({
|
|
1481
|
+
jsonrpc: "2.0",
|
|
1482
|
+
id: message.id,
|
|
1483
|
+
error: { code: -32601, message: "Method not found" }
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
send({ jsonrpc: "2.0", id: message.id, result });
|
|
1488
|
+
}
|
|
1489
|
+
__name(handleRpc, "handleRpc");
|
|
1490
|
+
function runDocToolchainMcpServer() {
|
|
1491
|
+
const reader = createInterface({ input: process.stdin });
|
|
1492
|
+
reader.on("line", (line) => {
|
|
1493
|
+
let parsed;
|
|
1494
|
+
try {
|
|
1495
|
+
parsed = JSON.parse(line);
|
|
1496
|
+
} catch {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1500
|
+
void handleRpc(parsed);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
__name(runDocToolchainMcpServer, "runDocToolchainMcpServer");
|
|
1505
|
+
var invokedPath = process.argv[1] ? path2.resolve(process.argv[1]) : "";
|
|
1506
|
+
var invokedBasename = invokedPath ? path2.basename(invokedPath).replace(/\.(?:[cm]?js|ts)$/, "") : "";
|
|
1507
|
+
if (invokedPath && (path2.resolve(fileURLToPath(import.meta.url)) === invokedPath || invokedBasename === "doc-toolchain-server")) {
|
|
1508
|
+
runDocToolchainMcpServer();
|
|
1509
|
+
}
|
|
1510
|
+
export {
|
|
1511
|
+
DOC_TOOLCHAIN_SERVER_NAME,
|
|
1512
|
+
DOC_TOOLCHAIN_TOOL_DEFINITIONS,
|
|
1513
|
+
DOC_TOOLCHAIN_TOOL_PREFIX,
|
|
1514
|
+
executeDocToolchainTool,
|
|
1515
|
+
runDocToolchainMcpServer
|
|
1516
|
+
};
|