deepfish-ai 1.0.29 → 2.0.3
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 +195 -344
- package/dist/generate-skill.md +104 -0
- package/dist/generate-tool.md +88 -0
- package/dist/index.js +26831 -0
- package/dist/serve/client/assets/index-DUHYC91l.js +1 -0
- package/dist/serve/client/assets/index-zcrdNLZs.css +1 -0
- package/dist/serve/client/index.html +14 -0
- package/dist/serve/pm2-server.js +24405 -0
- package/dist/serve/server/entry-server.mjs +224 -0
- package/dist/view-learn-cache.md +65 -0
- package/package.json +77 -83
- package/LICENSE +0 -21
- package/README_CN.md +0 -460
- package/src/AgentRobot/AgentRobotFactory/MainAgentRobot.js +0 -88
- package/src/AgentRobot/AgentRobotFactory/SubAgentRobot.js +0 -45
- package/src/AgentRobot/AgentRobotFactory/SubSkillAgentRobot.js +0 -45
- package/src/AgentRobot/AgentRobotFactory/index.js +0 -48
- package/src/AgentRobot/BaseAgentRobot/Brain.js +0 -210
- package/src/AgentRobot/BaseAgentRobot/BrainEvent.js +0 -17
- package/src/AgentRobot/BaseAgentRobot/Hand.js +0 -119
- package/src/AgentRobot/BaseAgentRobot/Heart.js +0 -0
- package/src/AgentRobot/BaseAgentRobot/Logger.js +0 -114
- package/src/AgentRobot/BaseAgentRobot/ScreenPrinter.js +0 -49
- package/src/AgentRobot/BaseAgentRobot/index.js +0 -266
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/AliBailian.js +0 -997
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/doc-transform.js +0 -204
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/docx.js +0 -1286
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/img.js +0 -765
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/pdf.js +0 -736
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/pptx.js +0 -567
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/video.js +0 -36
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/xlsx.js +0 -611
- package/src/AgentRobot/BaseAgentRobot/tools/BaseTools.js +0 -13
- package/src/AgentRobot/BaseAgentRobot/tools/CreateAgentTools.js +0 -158
- package/src/AgentRobot/BaseAgentRobot/tools/FileTools.js +0 -768
- package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +0 -441
- package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +0 -244
- package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +0 -424
- package/src/AgentRobot/BaseAgentRobot/tools/TaskTools.js +0 -211
- package/src/AgentRobot/BaseAgentRobot/tools/TestTools.js +0 -100
- package/src/AgentRobot/BaseAgentRobot/tools/UserTool.js +0 -87
- package/src/AgentRobot/BaseAgentRobot/tools/WebTools.js +0 -257
- package/src/AgentRobot/BaseAgentRobot/utils/AIRequest.js +0 -297
- package/src/AgentRobot/BaseAgentRobot/utils/AIToolManager.js +0 -131
- package/src/AgentRobot/BaseAgentRobot/utils/AgentTree.js +0 -66
- package/src/AgentRobot/BaseAgentRobot/utils/AttachmentToolScanner.js +0 -235
- package/src/AgentRobot/BaseAgentRobot/utils/MessageCompresser.js +0 -236
- package/src/AgentRobot/BaseAgentRobot/utils/aiConsole.js +0 -74
- package/src/AgentRobot/BaseAgentRobot/utils/aiInquirer.js +0 -78
- package/src/AgentRobot/BaseAgentRobot/utils/analyzeReturn.test.js +0 -104
- package/src/AgentRobot/BaseAgentRobot/utils/copilot.js +0 -117
- package/src/AgentRobot/BaseAgentRobot/utils/node-root.js +0 -138
- package/src/AgentRobot/BaseAgentRobot/utils/normal.js +0 -399
- package/src/cli/ConfigManager.js +0 -283
- package/src/cli/DefaultConfig.js +0 -98
- package/src/cli/GlobalVariable.js +0 -8
- package/src/cli/MemoryManager.js +0 -53
- package/src/cli/SkillConfigManager.js +0 -354
- package/src/cli/SkillParser.js +0 -58
- package/src/cli/ai-auth.js +0 -231
- package/src/cli/ai-config.js +0 -225
- package/src/cli/ai-memory.js +0 -22
- package/src/cli/ai-skill.js +0 -65
- package/src/cli/index.js +0 -77
- package/src/index.js +0 -54
|
@@ -1,1286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @Author: Roman 306863030@qq.com
|
|
3
|
-
* @Description: Word 文档处理工具集(mammoth / docx / pizzip / docxtemplater)
|
|
4
|
-
*/
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const fs = require('fs-extra')
|
|
7
|
-
const mammoth = require('mammoth')
|
|
8
|
-
const docx = require('docx')
|
|
9
|
-
const PizZip = require('pizzip')
|
|
10
|
-
const Docxtemplater = require('docxtemplater')
|
|
11
|
-
const cheerio = require('cheerio')
|
|
12
|
-
|
|
13
|
-
// ─── 统一返回结构 ─────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
function ok(data = null) {
|
|
16
|
-
return { success: true, data }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function fail(error, data = null) {
|
|
20
|
-
return { success: false, error: error?.message || String(error), data }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function resolvePath(filePath) {
|
|
24
|
-
return path.resolve(process.cwd(), filePath)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── 格式转换辅助函数 ────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 使用 puppeteer 将 HTML 字符串渲染为 PDF 文件
|
|
31
|
-
*/
|
|
32
|
-
async function htmlStringToPdf(html, outputPath) {
|
|
33
|
-
let puppeteer
|
|
34
|
-
try {
|
|
35
|
-
puppeteer = require('puppeteer')
|
|
36
|
-
} catch {
|
|
37
|
-
throw new Error('puppeteer 未安装,请先执行 npm install puppeteer')
|
|
38
|
-
}
|
|
39
|
-
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] })
|
|
40
|
-
try {
|
|
41
|
-
const page = await browser.newPage()
|
|
42
|
-
await page.setContent(html, { waitUntil: 'networkidle0' })
|
|
43
|
-
fs.ensureDirSync(path.dirname(outputPath))
|
|
44
|
-
await page.pdf({ path: outputPath, format: 'A4', printBackground: true })
|
|
45
|
-
} finally {
|
|
46
|
-
await browser.close()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function escapeHtml(str) {
|
|
51
|
-
return str
|
|
52
|
-
.replace(/&/g, '&')
|
|
53
|
-
.replace(/</g, '<')
|
|
54
|
-
.replace(/>/g, '>')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function unescapeXml(str) {
|
|
58
|
-
return str
|
|
59
|
-
.replace(/&/g, '&')
|
|
60
|
-
.replace(/</g, '<')
|
|
61
|
-
.replace(/>/g, '>')
|
|
62
|
-
.replace(/"/g, '"')
|
|
63
|
-
.replace(/'/g, "'")
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function buildRunXml(rPr, text) {
|
|
67
|
-
if (!text) return ''
|
|
68
|
-
const xmlSp = /^\s|\s$/.test(text) ? ' xml:space="preserve"' : ''
|
|
69
|
-
return `<w:r>${rPr}<w:t${xmlSp}>${escapeHtml(text)}</w:t></w:r>`
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 将 Markdown 文本转换为 HTML 字符串
|
|
74
|
-
*/
|
|
75
|
-
function markdownToHtmlString(md) {
|
|
76
|
-
let html = md
|
|
77
|
-
.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
|
|
78
|
-
`<pre><code class="language-${lang}">${escapeHtml(code.trimEnd())}</code></pre>`)
|
|
79
|
-
.replace(/`([^`]+)`/g, (_, c) => `<code>${escapeHtml(c)}</code>`)
|
|
80
|
-
.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
|
|
81
|
-
.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
|
|
82
|
-
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
|
83
|
-
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
84
|
-
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
85
|
-
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
86
|
-
.replace(/^[-*_]{3,}$/gm, '<hr>')
|
|
87
|
-
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
88
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
89
|
-
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
90
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
91
|
-
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
92
|
-
.replace(/~~(.+?)~~/g, '<del>$1</del>')
|
|
93
|
-
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2">')
|
|
94
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
95
|
-
.replace(/^[ \t]*[-*+] (.+)$/gm, '<li>$1</li>')
|
|
96
|
-
.replace(/^[ \t]*\d+\. (.+)$/gm, '<li>$1</li>')
|
|
97
|
-
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
98
|
-
html = html.replace(/(<li>[\s\S]+?<\/li>)(\n(?!<li>)|$)/g, (_, items) => `<ul>${items}</ul>`)
|
|
99
|
-
html = html.replace(/^(?!<[a-z]|$)(.+)$/gm, '<p>$1</p>')
|
|
100
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
101
|
-
body{font-family:sans-serif;line-height:1.7;max-width:900px;margin:40px auto;padding:0 20px;color:#333}
|
|
102
|
-
h1,h2,h3,h4,h5,h6{margin-top:1.2em}
|
|
103
|
-
pre{background:#f5f5f5;padding:12px;border-radius:4px;overflow:auto}
|
|
104
|
-
code{background:#f0f0f0;padding:2px 4px;border-radius:3px}
|
|
105
|
-
blockquote{border-left:4px solid #ddd;margin:0;padding-left:1em;color:#666}
|
|
106
|
-
table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px 10px}
|
|
107
|
-
</style></head><body>${html}</body></html>`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 将 HTML 字符串转换为 Markdown 文本
|
|
112
|
-
*/
|
|
113
|
-
function htmlStringToMarkdown(html) {
|
|
114
|
-
const $ = cheerio.load(html)
|
|
115
|
-
const body = $('body').length ? $('body') : $.root()
|
|
116
|
-
|
|
117
|
-
function nodeToMd(el) {
|
|
118
|
-
const node = $(el)
|
|
119
|
-
const tag = el.type === 'text' ? '#text' : (el.name || '').toLowerCase()
|
|
120
|
-
if (el.type === 'text') return el.data || ''
|
|
121
|
-
const inner = () => node.contents().toArray().map(nodeToMd).join('')
|
|
122
|
-
switch (tag) {
|
|
123
|
-
case 'h1': return `# ${inner()}\n\n`
|
|
124
|
-
case 'h2': return `## ${inner()}\n\n`
|
|
125
|
-
case 'h3': return `### ${inner()}\n\n`
|
|
126
|
-
case 'h4': return `#### ${inner()}\n\n`
|
|
127
|
-
case 'h5': return `##### ${inner()}\n\n`
|
|
128
|
-
case 'h6': return `###### ${inner()}\n\n`
|
|
129
|
-
case 'p': return `${inner()}\n\n`
|
|
130
|
-
case 'br': return '\n'
|
|
131
|
-
case 'hr': return '---\n\n'
|
|
132
|
-
case 'strong':
|
|
133
|
-
case 'b': return `**${inner()}**`
|
|
134
|
-
case 'em':
|
|
135
|
-
case 'i': return `*${inner()}*`
|
|
136
|
-
case 'del':
|
|
137
|
-
case 's': return `~~${inner()}~~`
|
|
138
|
-
case 'code': return `\`${inner()}\``
|
|
139
|
-
case 'pre': {
|
|
140
|
-
const codeEl = node.find('code')
|
|
141
|
-
const lang = (codeEl.attr('class') || '').replace('language-', '')
|
|
142
|
-
const content = codeEl.length ? codeEl.text() : node.text()
|
|
143
|
-
return `\`\`\`${lang}\n${content}\n\`\`\`\n\n`
|
|
144
|
-
}
|
|
145
|
-
case 'blockquote': return inner().split('\n').map(l => l ? `> ${l}` : '').join('\n') + '\n\n'
|
|
146
|
-
case 'a': return `[${inner()}](${node.attr('href') || ''})`
|
|
147
|
-
case 'img': return ` || ''})`
|
|
148
|
-
case 'ul':
|
|
149
|
-
case 'ol': return inner() + '\n'
|
|
150
|
-
case 'li': return `- ${inner()}\n`
|
|
151
|
-
case 'table': {
|
|
152
|
-
const rows = node.find('tr').toArray()
|
|
153
|
-
if (!rows.length) return ''
|
|
154
|
-
return rows.map((row, i) => {
|
|
155
|
-
const cells = $(row).find('th,td').toArray().map(c => $(c).text().trim())
|
|
156
|
-
const line = `| ${cells.join(' | ')} |`
|
|
157
|
-
return i === 0 ? `${line}\n| ${cells.map(() => '---').join(' | ')} |` : line
|
|
158
|
-
}).join('\n') + '\n\n'
|
|
159
|
-
}
|
|
160
|
-
case 'head':
|
|
161
|
-
case 'style':
|
|
162
|
-
case 'script': return ''
|
|
163
|
-
default: return inner()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return body.contents().toArray().map(nodeToMd).join('').replace(/\n{3,}/g, '\n\n').trim()
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* 将 HTML 字符串解析为 docx sections 数组
|
|
172
|
-
*/
|
|
173
|
-
function htmlStringToDocxSections(html) {
|
|
174
|
-
const $ = cheerio.load(html)
|
|
175
|
-
const sections = []
|
|
176
|
-
|
|
177
|
-
function processNode(el) {
|
|
178
|
-
const node = $(el)
|
|
179
|
-
const tag = (el.name || '').toLowerCase()
|
|
180
|
-
const headingMatch = tag.match(/^h([1-6])$/)
|
|
181
|
-
if (headingMatch) {
|
|
182
|
-
sections.push({ type: 'heading', level: parseInt(headingMatch[1]), text: node.text().trim() })
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
switch (tag) {
|
|
186
|
-
case 'p':
|
|
187
|
-
if (node.text().trim()) sections.push({ type: 'paragraph', text: node.text().trim() })
|
|
188
|
-
break
|
|
189
|
-
case 'ul':
|
|
190
|
-
sections.push({ type: 'list', items: node.find('li').toArray().map(li => $(li).text().trim()) })
|
|
191
|
-
break
|
|
192
|
-
case 'ol':
|
|
193
|
-
sections.push({ type: 'numberedList', items: node.find('li').toArray().map(li => $(li).text().trim()) })
|
|
194
|
-
break
|
|
195
|
-
case 'table': {
|
|
196
|
-
const rows = node.find('tr').toArray().map(row =>
|
|
197
|
-
$(row).find('th,td').toArray().map(c => $(c).text().trim()))
|
|
198
|
-
if (rows.length) sections.push({ type: 'table', rows })
|
|
199
|
-
break
|
|
200
|
-
}
|
|
201
|
-
case 'hr':
|
|
202
|
-
sections.push({ type: 'horizontalRule' })
|
|
203
|
-
break
|
|
204
|
-
case 'pre':
|
|
205
|
-
sections.push({ type: 'paragraph', text: node.text() })
|
|
206
|
-
break
|
|
207
|
-
case 'blockquote':
|
|
208
|
-
node.find('p').each((_, pEl) => {
|
|
209
|
-
const t = $(pEl).text().trim()
|
|
210
|
-
if (t) sections.push({ type: 'paragraph', text: `> ${t}` })
|
|
211
|
-
})
|
|
212
|
-
if (!node.find('p').length && node.text().trim()) {
|
|
213
|
-
sections.push({ type: 'paragraph', text: `> ${node.text().trim()}` })
|
|
214
|
-
}
|
|
215
|
-
break
|
|
216
|
-
default:
|
|
217
|
-
node.children().each((_, child) => processNode(child))
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
$('body').children().each((_, el) => processNode(el))
|
|
222
|
-
return sections
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ─── 内部辅助:将 sections 描述转换为 docx children ──────────────────────────
|
|
226
|
-
/**
|
|
227
|
-
* sections 数组每项结构:
|
|
228
|
-
* { type: 'paragraph'|'heading'|'table'|'list'|'numberedList'|'pageBreak'|'horizontalRule',
|
|
229
|
-
* text, level, rows, items, runs, bold, italic, alignment, color, fontSize }
|
|
230
|
-
* runs 数组每项:{ text, bold, italic, underline, color, fontSize }
|
|
231
|
-
*/
|
|
232
|
-
function buildChildren(sections, docxLib) {
|
|
233
|
-
const {
|
|
234
|
-
Paragraph, TextRun, HeadingLevel,
|
|
235
|
-
Table, TableRow, TableCell, WidthType,
|
|
236
|
-
AlignmentType, ExternalHyperlink,
|
|
237
|
-
} = docxLib
|
|
238
|
-
|
|
239
|
-
const HEADING_MAP = {
|
|
240
|
-
1: HeadingLevel.HEADING_1,
|
|
241
|
-
2: HeadingLevel.HEADING_2,
|
|
242
|
-
3: HeadingLevel.HEADING_3,
|
|
243
|
-
4: HeadingLevel.HEADING_4,
|
|
244
|
-
5: HeadingLevel.HEADING_5,
|
|
245
|
-
6: HeadingLevel.HEADING_6,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function makeRun(r) {
|
|
249
|
-
return new TextRun({
|
|
250
|
-
text: String(r.text ?? ''),
|
|
251
|
-
bold: r.bold || false,
|
|
252
|
-
italics: r.italic || false,
|
|
253
|
-
underline: r.underline ? {} : undefined,
|
|
254
|
-
color: r.color || undefined,
|
|
255
|
-
size: r.fontSize ? r.fontSize * 2 : undefined,
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const alignment = (a) =>
|
|
260
|
-
a ? AlignmentType[a.toUpperCase()] || AlignmentType.LEFT : undefined
|
|
261
|
-
|
|
262
|
-
const children = []
|
|
263
|
-
for (const sec of (sections || [])) {
|
|
264
|
-
switch (sec.type) {
|
|
265
|
-
case 'heading':
|
|
266
|
-
children.push(
|
|
267
|
-
new Paragraph({
|
|
268
|
-
text: String(sec.text || ''),
|
|
269
|
-
heading: HEADING_MAP[sec.level || 1] || HeadingLevel.HEADING_1,
|
|
270
|
-
alignment: alignment(sec.alignment),
|
|
271
|
-
}),
|
|
272
|
-
)
|
|
273
|
-
break
|
|
274
|
-
|
|
275
|
-
case 'paragraph': {
|
|
276
|
-
const runs = Array.isArray(sec.runs)
|
|
277
|
-
? sec.runs.map(makeRun)
|
|
278
|
-
: [new TextRun({
|
|
279
|
-
text: String(sec.text || ''),
|
|
280
|
-
bold: sec.bold || false,
|
|
281
|
-
italics: sec.italic || false,
|
|
282
|
-
color: sec.color || undefined,
|
|
283
|
-
size: sec.fontSize ? sec.fontSize * 2 : undefined,
|
|
284
|
-
})]
|
|
285
|
-
children.push(new Paragraph({ children: runs, alignment: alignment(sec.alignment) }))
|
|
286
|
-
break
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
case 'table': {
|
|
290
|
-
const colCount = Math.max(...(sec.rows || [[]]).map(r => r.length), 1)
|
|
291
|
-
const colWidth = Math.floor(9000 / colCount)
|
|
292
|
-
const rows = (sec.rows || []).map((row) =>
|
|
293
|
-
new TableRow({
|
|
294
|
-
children: (row || []).map((cell) =>
|
|
295
|
-
new TableCell({
|
|
296
|
-
children: [new Paragraph({ text: String(cell ?? '') })],
|
|
297
|
-
width: { size: colWidth, type: WidthType.DXA },
|
|
298
|
-
}),
|
|
299
|
-
),
|
|
300
|
-
}),
|
|
301
|
-
)
|
|
302
|
-
children.push(new Table({ rows, width: { size: 9000, type: WidthType.DXA } }))
|
|
303
|
-
break
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
case 'list':
|
|
307
|
-
for (const item of (sec.items || [])) {
|
|
308
|
-
children.push(new Paragraph({ text: String(item), bullet: { level: sec.level || 0 } }))
|
|
309
|
-
}
|
|
310
|
-
break
|
|
311
|
-
|
|
312
|
-
case 'numberedList': {
|
|
313
|
-
const numRef = sec.numReference || 'default-numbering'
|
|
314
|
-
for (const item of (sec.items || [])) {
|
|
315
|
-
children.push(
|
|
316
|
-
new Paragraph({
|
|
317
|
-
text: String(item),
|
|
318
|
-
numbering: { reference: numRef, level: 0 },
|
|
319
|
-
}),
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
break
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
case 'pageBreak':
|
|
326
|
-
children.push(new Paragraph({ pageBreakBefore: true }))
|
|
327
|
-
break
|
|
328
|
-
|
|
329
|
-
case 'horizontalRule':
|
|
330
|
-
children.push(
|
|
331
|
-
new Paragraph({
|
|
332
|
-
children: [new TextRun({ text: '', break: 1 })],
|
|
333
|
-
border: { bottom: { color: '999999', space: 1, value: 'single', size: 6 } },
|
|
334
|
-
}),
|
|
335
|
-
)
|
|
336
|
-
break
|
|
337
|
-
|
|
338
|
-
default:
|
|
339
|
-
// 未知类型作为普通段落处理
|
|
340
|
-
if (sec.text) {
|
|
341
|
-
children.push(new Paragraph({ text: String(sec.text) }))
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return children
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* 读取 Word 文档为纯文本
|
|
352
|
-
*/
|
|
353
|
-
async function readDocxText(filePath) {
|
|
354
|
-
try {
|
|
355
|
-
const fullPath = resolvePath(filePath)
|
|
356
|
-
if (!fs.existsSync(fullPath)) {
|
|
357
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
358
|
-
}
|
|
359
|
-
const result = await mammoth.extractRawText({ path: fullPath })
|
|
360
|
-
return ok({ filePath: fullPath, text: result.value, messages: result.messages })
|
|
361
|
-
} catch (error) {
|
|
362
|
-
return fail(error, { filePath })
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* 读取 Word 文档为 HTML
|
|
368
|
-
*/
|
|
369
|
-
async function readDocxHtml(filePath) {
|
|
370
|
-
try {
|
|
371
|
-
const fullPath = resolvePath(filePath)
|
|
372
|
-
if (!fs.existsSync(fullPath)) {
|
|
373
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
374
|
-
}
|
|
375
|
-
const result = await mammoth.convertToHtml({ path: fullPath })
|
|
376
|
-
return ok({ filePath: fullPath, html: result.value, messages: result.messages })
|
|
377
|
-
} catch (error) {
|
|
378
|
-
return fail(error, { filePath })
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* 获取 Word 文档基础信息(字数、行数、文件大小等)
|
|
384
|
-
*/
|
|
385
|
-
async function getDocxInfo(filePath) {
|
|
386
|
-
try {
|
|
387
|
-
const fullPath = resolvePath(filePath)
|
|
388
|
-
if (!fs.existsSync(fullPath)) {
|
|
389
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
390
|
-
}
|
|
391
|
-
const result = await mammoth.extractRawText({ path: fullPath })
|
|
392
|
-
const text = result.value || ''
|
|
393
|
-
const stats = fs.statSync(fullPath)
|
|
394
|
-
return ok({
|
|
395
|
-
filePath: fullPath,
|
|
396
|
-
size: stats.size,
|
|
397
|
-
mtime: stats.mtime,
|
|
398
|
-
wordCount: text.trim().split(/\s+/).filter(Boolean).length,
|
|
399
|
-
charCount: text.length,
|
|
400
|
-
lineCount: text.split('\n').length,
|
|
401
|
-
paragraphCount: text.split(/\n\n+/).filter(Boolean).length,
|
|
402
|
-
})
|
|
403
|
-
} catch (error) {
|
|
404
|
-
return fail(error, { filePath })
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* 搜索 Word 文档中是否包含指定文本,返回匹配行列表
|
|
410
|
-
*/
|
|
411
|
-
async function searchDocxText(filePath, keyword) {
|
|
412
|
-
try {
|
|
413
|
-
const readResult = await readDocxText(filePath)
|
|
414
|
-
if (!readResult.success) return readResult
|
|
415
|
-
const lines = readResult.data.text.split('\n')
|
|
416
|
-
const matches = lines
|
|
417
|
-
.map((line, idx) => ({ line: idx + 1, text: line }))
|
|
418
|
-
.filter(({ text }) => text.includes(keyword))
|
|
419
|
-
return ok({
|
|
420
|
-
filePath: resolvePath(filePath),
|
|
421
|
-
keyword,
|
|
422
|
-
matchCount: matches.length,
|
|
423
|
-
matches,
|
|
424
|
-
})
|
|
425
|
-
} catch (error) {
|
|
426
|
-
return fail(error, { filePath, keyword })
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* 将 Word 文档导出为纯文本文件
|
|
432
|
-
*/
|
|
433
|
-
async function docxToText(inputPath, outputPath) {
|
|
434
|
-
try {
|
|
435
|
-
const readResult = await readDocxText(inputPath)
|
|
436
|
-
if (!readResult.success) return readResult
|
|
437
|
-
const fullOutputPath = resolvePath(outputPath)
|
|
438
|
-
fs.ensureDirSync(path.dirname(fullOutputPath))
|
|
439
|
-
fs.writeFileSync(fullOutputPath, readResult.data.text, 'utf8')
|
|
440
|
-
return ok({ inputPath: resolvePath(inputPath), outputPath: fullOutputPath })
|
|
441
|
-
} catch (error) {
|
|
442
|
-
return fail(error, { inputPath, outputPath })
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* 将 Word 文档导出为 HTML 文件
|
|
448
|
-
*/
|
|
449
|
-
async function docxToHtml(inputPath, outputPath) {
|
|
450
|
-
try {
|
|
451
|
-
const readResult = await readDocxHtml(inputPath)
|
|
452
|
-
if (!readResult.success) return readResult
|
|
453
|
-
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${readResult.data.html}</body></html>`
|
|
454
|
-
const fullOutputPath = resolvePath(outputPath)
|
|
455
|
-
fs.ensureDirSync(path.dirname(fullOutputPath))
|
|
456
|
-
fs.writeFileSync(fullOutputPath, html, 'utf8')
|
|
457
|
-
return ok({ inputPath: resolvePath(inputPath), outputPath: fullOutputPath })
|
|
458
|
-
} catch (error) {
|
|
459
|
-
return fail(error, { inputPath, outputPath })
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* 创建 Word 文档(支持段落、标题、表格、列表等)
|
|
465
|
-
*/
|
|
466
|
-
async function createDocx(filePath, sections = []) {
|
|
467
|
-
try {
|
|
468
|
-
const { Document, Packer } = docx
|
|
469
|
-
const fullPath = resolvePath(filePath)
|
|
470
|
-
fs.ensureDirSync(path.dirname(fullPath))
|
|
471
|
-
const children = buildChildren(sections, docx)
|
|
472
|
-
const doc = new Document({ sections: [{ properties: {}, children }] })
|
|
473
|
-
const buffer = await Packer.toBuffer(doc)
|
|
474
|
-
fs.writeFileSync(fullPath, buffer)
|
|
475
|
-
return ok({ filePath: fullPath, sectionCount: sections.length })
|
|
476
|
-
} catch (error) {
|
|
477
|
-
return fail(error, { filePath })
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* 向已有 Word 文档末尾追加内容(以原文本段落 + 新 sections 重建文档)
|
|
483
|
-
*/
|
|
484
|
-
async function appendToDocx(filePath, sections = []) {
|
|
485
|
-
try {
|
|
486
|
-
const readResult = await readDocxText(filePath)
|
|
487
|
-
if (!readResult.success) return readResult
|
|
488
|
-
const existingSections = readResult.data.text
|
|
489
|
-
.split('\n')
|
|
490
|
-
.filter(Boolean)
|
|
491
|
-
.map((line) => ({ type: 'paragraph', text: line }))
|
|
492
|
-
return await createDocx(filePath, [...existingSections, ...sections])
|
|
493
|
-
} catch (error) {
|
|
494
|
-
return fail(error, { filePath })
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* 替换 Word 文档中的文本内容(通过直接修改 XML 实现)
|
|
500
|
-
*/
|
|
501
|
-
async function replaceDocxText(filePath, searchText, replaceText) {
|
|
502
|
-
try {
|
|
503
|
-
const fullPath = resolvePath(filePath)
|
|
504
|
-
if (!fs.existsSync(fullPath)) {
|
|
505
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
506
|
-
}
|
|
507
|
-
const content = fs.readFileSync(fullPath, 'binary')
|
|
508
|
-
const zip = new PizZip(content)
|
|
509
|
-
const targets = ['word/document.xml', 'word/header1.xml', 'word/footer1.xml']
|
|
510
|
-
let replaceCount = 0
|
|
511
|
-
for (const target of targets) {
|
|
512
|
-
const file = zip.file(target)
|
|
513
|
-
if (!file) continue
|
|
514
|
-
let xml = file.asText()
|
|
515
|
-
const before = xml.split(searchText).length - 1
|
|
516
|
-
xml = xml.split(searchText).join(replaceText)
|
|
517
|
-
replaceCount += before
|
|
518
|
-
zip.file(target, xml)
|
|
519
|
-
}
|
|
520
|
-
const buf = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' })
|
|
521
|
-
fs.writeFileSync(fullPath, buf)
|
|
522
|
-
return ok({ filePath: fullPath, searchText, replaceText, replaceCount })
|
|
523
|
-
} catch (error) {
|
|
524
|
-
return fail(error, { filePath, searchText, replaceText })
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* 用数据填充 Word 模板(模板标签格式:{变量名})
|
|
530
|
-
*/
|
|
531
|
-
async function fillDocxTemplate(templatePath, outputPath, data) {
|
|
532
|
-
try {
|
|
533
|
-
const fullTemplatePath = resolvePath(templatePath)
|
|
534
|
-
const fullOutputPath = resolvePath(outputPath)
|
|
535
|
-
if (!fs.existsSync(fullTemplatePath)) {
|
|
536
|
-
return fail(`Template file does not exist: ${fullTemplatePath}`, { templatePath: fullTemplatePath })
|
|
537
|
-
}
|
|
538
|
-
const content = fs.readFileSync(fullTemplatePath, 'binary')
|
|
539
|
-
const zip = new PizZip(content)
|
|
540
|
-
const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true })
|
|
541
|
-
doc.render(data || {})
|
|
542
|
-
const buf = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE' })
|
|
543
|
-
fs.ensureDirSync(path.dirname(fullOutputPath))
|
|
544
|
-
fs.writeFileSync(fullOutputPath, buf)
|
|
545
|
-
return ok({ templatePath: fullTemplatePath, outputPath: fullOutputPath })
|
|
546
|
-
} catch (error) {
|
|
547
|
-
return fail(error, { templatePath, outputPath })
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* 合并多个 Word 文档为一个(以分页符分隔各文档内容)
|
|
553
|
-
*/
|
|
554
|
-
async function mergeDocx(inputPaths, outputPath) {
|
|
555
|
-
try {
|
|
556
|
-
const allSections = []
|
|
557
|
-
for (let i = 0; i < inputPaths.length; i++) {
|
|
558
|
-
const readResult = await readDocxText(inputPaths[i])
|
|
559
|
-
if (!readResult.success) return readResult
|
|
560
|
-
const paras = readResult.data.text
|
|
561
|
-
.split('\n')
|
|
562
|
-
.filter(Boolean)
|
|
563
|
-
.map((line) => ({ type: 'paragraph', text: line }))
|
|
564
|
-
allSections.push(...paras)
|
|
565
|
-
if (i < inputPaths.length - 1) {
|
|
566
|
-
allSections.push({ type: 'pageBreak' })
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return await createDocx(outputPath, allSections)
|
|
570
|
-
} catch (error) {
|
|
571
|
-
return fail(error, { outputPath })
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* 提取 Word 文档中的所有超链接
|
|
577
|
-
*/
|
|
578
|
-
async function extractDocxLinks(filePath) {
|
|
579
|
-
try {
|
|
580
|
-
const fullPath = resolvePath(filePath)
|
|
581
|
-
if (!fs.existsSync(fullPath)) {
|
|
582
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
583
|
-
}
|
|
584
|
-
const content = fs.readFileSync(fullPath, 'binary')
|
|
585
|
-
const zip = new PizZip(content)
|
|
586
|
-
const relsFile = zip.file('word/_rels/document.xml.rels')
|
|
587
|
-
if (!relsFile) return ok({ filePath: fullPath, links: [] })
|
|
588
|
-
const relsXml = relsFile.asText()
|
|
589
|
-
const linkRegex = /Type="http:\/\/schemas\.openxmlformats\.org\/officeDocument\/2006\/relationships\/hyperlink"\s+Target="([^"]+)"/g
|
|
590
|
-
const links = []
|
|
591
|
-
let m
|
|
592
|
-
while ((m = linkRegex.exec(relsXml)) !== null) {
|
|
593
|
-
links.push(m[1])
|
|
594
|
-
}
|
|
595
|
-
return ok({ filePath: fullPath, links })
|
|
596
|
-
} catch (error) {
|
|
597
|
-
return fail(error, { filePath })
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* 统计 Word 文档中各段落字数
|
|
603
|
-
*/
|
|
604
|
-
async function getDocxParagraphStats(filePath) {
|
|
605
|
-
try {
|
|
606
|
-
const readResult = await readDocxText(filePath)
|
|
607
|
-
if (!readResult.success) return readResult
|
|
608
|
-
const paragraphs = readResult.data.text.split('\n').filter(Boolean)
|
|
609
|
-
const stats = paragraphs.map((para, idx) => ({
|
|
610
|
-
index: idx + 1,
|
|
611
|
-
text: para.substring(0, 60) + (para.length > 60 ? '...' : ''),
|
|
612
|
-
wordCount: para.trim().split(/\s+/).filter(Boolean).length,
|
|
613
|
-
charCount: para.length,
|
|
614
|
-
}))
|
|
615
|
-
return ok({
|
|
616
|
-
filePath: resolvePath(filePath),
|
|
617
|
-
paragraphCount: paragraphs.length,
|
|
618
|
-
totalWordCount: stats.reduce((s, p) => s + p.wordCount, 0),
|
|
619
|
-
paragraphs: stats,
|
|
620
|
-
})
|
|
621
|
-
} catch (error) {
|
|
622
|
-
return fail(error, { filePath })
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* 将 Word 文档的全部内容替换为新的 sections 内容
|
|
628
|
-
*/
|
|
629
|
-
async function overwriteDocx(filePath, sections = []) {
|
|
630
|
-
try {
|
|
631
|
-
const fullPath = resolvePath(filePath)
|
|
632
|
-
if (!fs.existsSync(fullPath)) {
|
|
633
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
634
|
-
}
|
|
635
|
-
return await createDocx(filePath, sections)
|
|
636
|
-
} catch (error) {
|
|
637
|
-
return fail(error, { filePath })
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ─── Word 格式转换函数 ───────────────────────────────────────────────────────
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Word 转 PDF
|
|
645
|
-
*/
|
|
646
|
-
async function wordToPdf(inputPath, outputPath) {
|
|
647
|
-
try {
|
|
648
|
-
const fullInput = resolvePath(inputPath)
|
|
649
|
-
const fullOutput = resolvePath(outputPath)
|
|
650
|
-
if (!fs.existsSync(fullInput)) {
|
|
651
|
-
return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
|
|
652
|
-
}
|
|
653
|
-
const result = await mammoth.convertToHtml({ path: fullInput })
|
|
654
|
-
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
655
|
-
body{font-family:sans-serif;line-height:1.7;margin:40px;color:#333}
|
|
656
|
-
table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px 10px}
|
|
657
|
-
</style></head><body>${result.value}</body></html>`
|
|
658
|
-
await htmlStringToPdf(html, fullOutput)
|
|
659
|
-
return ok({ inputPath: fullInput, outputPath: fullOutput })
|
|
660
|
-
} catch (error) {
|
|
661
|
-
return fail(error, { inputPath, outputPath })
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Word 转 HTML
|
|
667
|
-
*/
|
|
668
|
-
async function wordToHtml(inputPath, outputPath) {
|
|
669
|
-
try {
|
|
670
|
-
const fullInput = resolvePath(inputPath)
|
|
671
|
-
const fullOutput = resolvePath(outputPath)
|
|
672
|
-
if (!fs.existsSync(fullInput)) {
|
|
673
|
-
return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
|
|
674
|
-
}
|
|
675
|
-
const result = await mammoth.convertToHtml({ path: fullInput })
|
|
676
|
-
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
677
|
-
body{font-family:sans-serif;line-height:1.7;max-width:900px;margin:40px auto;padding:0 20px;color:#333}
|
|
678
|
-
table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px 10px}
|
|
679
|
-
</style></head><body>${result.value}</body></html>`
|
|
680
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
681
|
-
fs.writeFileSync(fullOutput, html, 'utf8')
|
|
682
|
-
return ok({ inputPath: fullInput, outputPath: fullOutput, messages: result.messages })
|
|
683
|
-
} catch (error) {
|
|
684
|
-
return fail(error, { inputPath, outputPath })
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Word 转 Markdown
|
|
690
|
-
*/
|
|
691
|
-
async function wordToMarkdown(inputPath, outputPath) {
|
|
692
|
-
try {
|
|
693
|
-
const fullInput = resolvePath(inputPath)
|
|
694
|
-
const fullOutput = resolvePath(outputPath)
|
|
695
|
-
if (!fs.existsSync(fullInput)) {
|
|
696
|
-
return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
|
|
697
|
-
}
|
|
698
|
-
const result = await mammoth.convertToHtml({ path: fullInput })
|
|
699
|
-
const md = htmlStringToMarkdown(result.value)
|
|
700
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
701
|
-
fs.writeFileSync(fullOutput, md, 'utf8')
|
|
702
|
-
return ok({ inputPath: fullInput, outputPath: fullOutput })
|
|
703
|
-
} catch (error) {
|
|
704
|
-
return fail(error, { inputPath, outputPath })
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Markdown 转 Word
|
|
710
|
-
*/
|
|
711
|
-
async function markdownToWord(inputPath, outputPath) {
|
|
712
|
-
try {
|
|
713
|
-
const fullInput = resolvePath(inputPath)
|
|
714
|
-
const fullOutput = resolvePath(outputPath)
|
|
715
|
-
if (!fs.existsSync(fullInput)) {
|
|
716
|
-
return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
|
|
717
|
-
}
|
|
718
|
-
const md = fs.readFileSync(fullInput, 'utf8')
|
|
719
|
-
const html = markdownToHtmlString(md)
|
|
720
|
-
const sections = htmlStringToDocxSections(html)
|
|
721
|
-
const { Document, Packer } = docx
|
|
722
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
723
|
-
const children = buildChildren(sections, docx)
|
|
724
|
-
const doc = new Document({ sections: [{ properties: {}, children }] })
|
|
725
|
-
const buffer = await Packer.toBuffer(doc)
|
|
726
|
-
fs.writeFileSync(fullOutput, buffer)
|
|
727
|
-
return ok({ inputPath: fullInput, outputPath: fullOutput, sectionCount: sections.length })
|
|
728
|
-
} catch (error) {
|
|
729
|
-
return fail(error, { inputPath, outputPath })
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* HTML 转 Word
|
|
735
|
-
*/
|
|
736
|
-
async function htmlToWord(inputPath, outputPath) {
|
|
737
|
-
try {
|
|
738
|
-
const fullInput = resolvePath(inputPath)
|
|
739
|
-
const fullOutput = resolvePath(outputPath)
|
|
740
|
-
if (!fs.existsSync(fullInput)) {
|
|
741
|
-
return fail(`File does not exist: ${fullInput}`, { inputPath: fullInput })
|
|
742
|
-
}
|
|
743
|
-
const html = fs.readFileSync(fullInput, 'utf8')
|
|
744
|
-
const sections = htmlStringToDocxSections(html)
|
|
745
|
-
const { Document, Packer } = docx
|
|
746
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
747
|
-
const children = buildChildren(sections, docx)
|
|
748
|
-
const doc = new Document({ sections: [{ properties: {}, children }] })
|
|
749
|
-
const buffer = await Packer.toBuffer(doc)
|
|
750
|
-
fs.writeFileSync(fullOutput, buffer)
|
|
751
|
-
return ok({ inputPath: fullInput, outputPath: fullOutput, sectionCount: sections.length })
|
|
752
|
-
} catch (error) {
|
|
753
|
-
return fail(error, { inputPath, outputPath })
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// ─── 工具描述 ─────────────────────────────────────────────────────────────────
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* 保留原格式修改 Word 文档内容(支持跨 <w:r> run 的文本替换,不破坏其余样式)
|
|
761
|
-
* @param {string} filePath - .docx 文件路径
|
|
762
|
-
* @param {Array<{search:string, replace:string}>} replacements - 替换规则数组
|
|
763
|
-
*/
|
|
764
|
-
async function patchDocxText(filePath, replacements) {
|
|
765
|
-
try {
|
|
766
|
-
const fullPath = resolvePath(filePath)
|
|
767
|
-
if (!fs.existsSync(fullPath)) {
|
|
768
|
-
return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
769
|
-
}
|
|
770
|
-
const content = fs.readFileSync(fullPath, 'binary')
|
|
771
|
-
const zip = new PizZip(content)
|
|
772
|
-
const docFile = zip.file('word/document.xml')
|
|
773
|
-
if (!docFile) return fail('word/document.xml not found', { filePath: fullPath })
|
|
774
|
-
let xml = docFile.asText()
|
|
775
|
-
let totalCount = 0
|
|
776
|
-
|
|
777
|
-
for (const { search, replace: replaceStr } of (replacements || [])) {
|
|
778
|
-
if (!search) continue
|
|
779
|
-
|
|
780
|
-
xml = xml.replace(/<w:p(?:\s[^>]*)?>([\s\S]*?)<\/w:p>/g, (para) => {
|
|
781
|
-
// 提取所有含文本的 <w:r> 元素
|
|
782
|
-
const runRe = /<w:r(?:\s[^>]*)?>([\s\S]*?)<\/w:r>/g
|
|
783
|
-
const runs = []
|
|
784
|
-
let rm
|
|
785
|
-
while ((rm = runRe.exec(para)) !== null) {
|
|
786
|
-
const tM = rm[0].match(/<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/)
|
|
787
|
-
if (!tM) continue // 跳过无文本内容的 run(如字段符号)
|
|
788
|
-
const rPrM = rm[0].match(/<w:rPr>[\s\S]*?<\/w:rPr>/)
|
|
789
|
-
runs.push({
|
|
790
|
-
full: rm[0],
|
|
791
|
-
rPr: rPrM ? rPrM[0] : '',
|
|
792
|
-
text: unescapeXml(tM[1]),
|
|
793
|
-
})
|
|
794
|
-
}
|
|
795
|
-
if (!runs.length) return para
|
|
796
|
-
|
|
797
|
-
const fullText = runs.map(r => r.text).join('')
|
|
798
|
-
if (!fullText.includes(search)) return para
|
|
799
|
-
|
|
800
|
-
// 构建每个 run 的字符偏移区间
|
|
801
|
-
let off = 0
|
|
802
|
-
const offsets = runs.map(r => { const s = off; off += r.text.length; return { start: s, end: off } })
|
|
803
|
-
|
|
804
|
-
const findRunIdx = (pos) => {
|
|
805
|
-
const idx = offsets.findIndex(o => pos >= o.start && pos < o.end)
|
|
806
|
-
return idx >= 0 ? idx : runs.length - 1
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// 找所有非重叠匹配位置
|
|
810
|
-
const occurrences = []
|
|
811
|
-
let from = 0
|
|
812
|
-
while (true) {
|
|
813
|
-
const pos = fullText.indexOf(search, from)
|
|
814
|
-
if (pos === -1) break
|
|
815
|
-
occurrences.push(pos)
|
|
816
|
-
from = pos + search.length
|
|
817
|
-
}
|
|
818
|
-
totalCount += occurrences.length
|
|
819
|
-
|
|
820
|
-
// 按字符位置构建新的 {rPr, text} 片段列表
|
|
821
|
-
const segs = []
|
|
822
|
-
let cursor = 0
|
|
823
|
-
for (const matchPos of occurrences) {
|
|
824
|
-
// 匹配前的文本:按 run 边界拆分,保留各自 rPr
|
|
825
|
-
let p = cursor
|
|
826
|
-
while (p < matchPos) {
|
|
827
|
-
const ri = findRunIdx(p)
|
|
828
|
-
const runEnd = Math.min(offsets[ri].end, matchPos)
|
|
829
|
-
segs.push({ rPr: runs[ri].rPr, text: fullText.slice(p, runEnd) })
|
|
830
|
-
p = runEnd
|
|
831
|
-
}
|
|
832
|
-
// 替换文本:使用匹配起始位置所在 run 的 rPr
|
|
833
|
-
segs.push({ rPr: runs[findRunIdx(matchPos)].rPr, text: replaceStr })
|
|
834
|
-
cursor = matchPos + search.length
|
|
835
|
-
}
|
|
836
|
-
// 匹配后剩余文本
|
|
837
|
-
let p = cursor
|
|
838
|
-
while (p < fullText.length) {
|
|
839
|
-
const ri = findRunIdx(p)
|
|
840
|
-
segs.push({ rPr: runs[ri].rPr, text: fullText.slice(p, offsets[ri].end) })
|
|
841
|
-
p = offsets[ri].end
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// 合并相邻且 rPr 相同的片段
|
|
845
|
-
const merged = []
|
|
846
|
-
for (const seg of segs) {
|
|
847
|
-
if (!seg.text) continue
|
|
848
|
-
if (merged.length && merged[merged.length - 1].rPr === seg.rPr) {
|
|
849
|
-
merged[merged.length - 1].text += seg.text
|
|
850
|
-
} else {
|
|
851
|
-
merged.push({ rPr: seg.rPr, text: seg.text })
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// 构建新的 runs XML
|
|
856
|
-
const newRunsXml = merged.map(s => buildRunXml(s.rPr, s.text)).join('')
|
|
857
|
-
|
|
858
|
-
// 将段落中从第一个到最后一个文本 run 的区间替换为新 runs
|
|
859
|
-
const firstRunFull = runs[0].full
|
|
860
|
-
const lastRunFull = runs[runs.length - 1].full
|
|
861
|
-
const firstIdx = para.indexOf(firstRunFull)
|
|
862
|
-
const lastIdx = para.lastIndexOf(lastRunFull)
|
|
863
|
-
if (firstIdx === -1 || lastIdx === -1) return para
|
|
864
|
-
return para.slice(0, firstIdx) + newRunsXml + para.slice(lastIdx + lastRunFull.length)
|
|
865
|
-
})
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
zip.file('word/document.xml', xml)
|
|
869
|
-
const buf = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' })
|
|
870
|
-
fs.writeFileSync(fullPath, buf)
|
|
871
|
-
return ok({ filePath: fullPath, totalReplacements: totalCount })
|
|
872
|
-
} catch (error) {
|
|
873
|
-
return fail(error, { filePath })
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// ─── 工具描述 ─────────────────────────────────────────────────────────────────
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
// ─── 使用说明 ─────────────────────────────────────────────────────────────────
|
|
881
|
-
|
|
882
|
-
function docxReadme() {
|
|
883
|
-
return `【DOCX 工具使用说明】
|
|
884
|
-
1. 优先使用本工具内置函数完成任务(读取、创建、搜索、替换、模板填充、格式转换、合并等)。
|
|
885
|
-
2. 如果内置函数无法满足需求(如复杂排版保真、特殊宏处理、跨格式高级转换),再尝试使用 LibreOffice 命令行。
|
|
886
|
-
3. 在调用 LibreOffice 前,先检测系统是否已安装 LibreOffice:
|
|
887
|
-
- 已安装:直接使用 LibreOffice 命令行继续处理。
|
|
888
|
-
- 未安装:询问用户是否允许安装。
|
|
889
|
-
4. 若用户同意安装:引导完成安装后继续执行原任务。
|
|
890
|
-
5. 若用户拒绝安装:明确告知当前能力限制,并终止该操作。
|
|
891
|
-
|
|
892
|
-
建议:
|
|
893
|
-
- 常规文档处理优先使用内置函数,速度更快且依赖更少。
|
|
894
|
-
- 仅在确实需要高保真格式转换或复杂排版时才启用 LibreOffice 路径。`
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
const descriptions = [
|
|
898
|
-
{
|
|
899
|
-
type: 'function',
|
|
900
|
-
function: {
|
|
901
|
-
name: 'docxReadme',
|
|
902
|
-
description: '获取 DOCX 工具集的使用说明, 调用函数前必须先查看本说明。',
|
|
903
|
-
parameters: {},
|
|
904
|
-
}
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
type: 'function',
|
|
908
|
-
function: {
|
|
909
|
-
name: 'readDocxText',
|
|
910
|
-
description:
|
|
911
|
-
'读取 Word 文档(.docx)并提取纯文本内容。参数:filePath 为文档路径。返回值:对象,包含 success、data(含 filePath、text)、error。',
|
|
912
|
-
parameters: {
|
|
913
|
-
type: 'object',
|
|
914
|
-
properties: {
|
|
915
|
-
filePath: { type: 'string', description: '要读取的 .docx 文件路径。' },
|
|
916
|
-
},
|
|
917
|
-
required: ['filePath'],
|
|
918
|
-
},
|
|
919
|
-
},
|
|
920
|
-
},
|
|
921
|
-
{
|
|
922
|
-
type: 'function',
|
|
923
|
-
function: {
|
|
924
|
-
name: 'readDocxHtml',
|
|
925
|
-
description:
|
|
926
|
-
'将 Word 文档(.docx)转换为 HTML 字符串,保留格式信息(粗体、斜体、表格等)。参数:filePath 为文档路径。返回值:对象,包含 success、data(含 filePath、html)、error。',
|
|
927
|
-
parameters: {
|
|
928
|
-
type: 'object',
|
|
929
|
-
properties: {
|
|
930
|
-
filePath: { type: 'string', description: '要读取的 .docx 文件路径。' },
|
|
931
|
-
},
|
|
932
|
-
required: ['filePath'],
|
|
933
|
-
},
|
|
934
|
-
},
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
type: 'function',
|
|
938
|
-
function: {
|
|
939
|
-
name: 'getDocxInfo',
|
|
940
|
-
description:
|
|
941
|
-
'获取 Word 文档的基础信息。参数:filePath 为文档路径。返回值:对象,包含 success、data(含 filePath、size、mtime、wordCount、charCount、lineCount、paragraphCount)、error。',
|
|
942
|
-
parameters: {
|
|
943
|
-
type: 'object',
|
|
944
|
-
properties: {
|
|
945
|
-
filePath: { type: 'string', description: '要查询的 .docx 文件路径。' },
|
|
946
|
-
},
|
|
947
|
-
required: ['filePath'],
|
|
948
|
-
},
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
{
|
|
952
|
-
type: 'function',
|
|
953
|
-
function: {
|
|
954
|
-
name: 'searchDocxText',
|
|
955
|
-
description:
|
|
956
|
-
'在 Word 文档中搜索指定关键词,返回所有匹配行。参数:filePath 为文档路径;keyword 为搜索关键词。返回值:对象,包含 success、data(含 keyword、matchCount、matches 数组)、error。',
|
|
957
|
-
parameters: {
|
|
958
|
-
type: 'object',
|
|
959
|
-
properties: {
|
|
960
|
-
filePath: { type: 'string', description: '要搜索的 .docx 文件路径。' },
|
|
961
|
-
keyword: { type: 'string', description: '搜索关键词。' },
|
|
962
|
-
},
|
|
963
|
-
required: ['filePath', 'keyword'],
|
|
964
|
-
},
|
|
965
|
-
},
|
|
966
|
-
},
|
|
967
|
-
{
|
|
968
|
-
type: 'function',
|
|
969
|
-
function: {
|
|
970
|
-
name: 'docxToText',
|
|
971
|
-
description:
|
|
972
|
-
'将 Word 文档导出为纯文本文件(.txt)。参数:inputPath 为源 .docx 路径;outputPath 为输出 .txt 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
|
|
973
|
-
parameters: {
|
|
974
|
-
type: 'object',
|
|
975
|
-
properties: {
|
|
976
|
-
inputPath: { type: 'string', description: '源 .docx 文件路径。' },
|
|
977
|
-
outputPath: { type: 'string', description: '输出 .txt 文件路径。' },
|
|
978
|
-
},
|
|
979
|
-
required: ['inputPath', 'outputPath'],
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
type: 'function',
|
|
985
|
-
function: {
|
|
986
|
-
name: 'docxToHtml',
|
|
987
|
-
description:
|
|
988
|
-
'将 Word 文档导出为 HTML 文件。参数:inputPath 为源 .docx 路径;outputPath 为输出 .html 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
|
|
989
|
-
parameters: {
|
|
990
|
-
type: 'object',
|
|
991
|
-
properties: {
|
|
992
|
-
inputPath: { type: 'string', description: '源 .docx 文件路径。' },
|
|
993
|
-
outputPath: { type: 'string', description: '输出 .html 文件路径。' },
|
|
994
|
-
},
|
|
995
|
-
required: ['inputPath', 'outputPath'],
|
|
996
|
-
},
|
|
997
|
-
},
|
|
998
|
-
},
|
|
999
|
-
{
|
|
1000
|
-
type: 'function',
|
|
1001
|
-
function: {
|
|
1002
|
-
name: 'createDocx',
|
|
1003
|
-
description: `创建一个新的 Word 文档(.docx),支持添加多种内容块。
|
|
1004
|
-
参数:filePath 为输出路径;sections 为内容数组,每项格式如下:
|
|
1005
|
-
- 段落:{ type:'paragraph', text, bold, italic, alignment, color, fontSize, runs:[{text,bold,italic,underline,color,fontSize}] }
|
|
1006
|
-
- 标题:{ type:'heading', text, level(1-6), alignment }
|
|
1007
|
-
- 表格:{ type:'table', rows:[[cell,...]] }
|
|
1008
|
-
- 无序列表:{ type:'list', items:[...], level(0-8) }
|
|
1009
|
-
- 有序列表:{ type:'numberedList', items:[...] }
|
|
1010
|
-
- 分页符:{ type:'pageBreak' }
|
|
1011
|
-
- 分隔线:{ type:'horizontalRule' }
|
|
1012
|
-
返回值:对象,包含 success、data(含 filePath、sectionCount)、error。`,
|
|
1013
|
-
parameters: {
|
|
1014
|
-
type: 'object',
|
|
1015
|
-
properties: {
|
|
1016
|
-
filePath: { type: 'string', description: '输出 .docx 文件路径。' },
|
|
1017
|
-
sections: {
|
|
1018
|
-
type: 'array',
|
|
1019
|
-
description: '内容块数组,支持 paragraph/heading/table/list/numberedList/pageBreak/horizontalRule 类型。',
|
|
1020
|
-
items: { type: 'object' },
|
|
1021
|
-
},
|
|
1022
|
-
},
|
|
1023
|
-
required: ['filePath'],
|
|
1024
|
-
},
|
|
1025
|
-
},
|
|
1026
|
-
},
|
|
1027
|
-
{
|
|
1028
|
-
type: 'function',
|
|
1029
|
-
function: {
|
|
1030
|
-
name: 'appendToDocx',
|
|
1031
|
-
description:
|
|
1032
|
-
'向已有 Word 文档末尾追加新内容块。参数:filePath 为目标 .docx 路径;sections 为要追加的内容块数组(格式同 createDocx)。返回值:对象,包含 success、data(含 filePath)、error。',
|
|
1033
|
-
parameters: {
|
|
1034
|
-
type: 'object',
|
|
1035
|
-
properties: {
|
|
1036
|
-
filePath: { type: 'string', description: '目标 .docx 文件路径。' },
|
|
1037
|
-
sections: { type: 'array', description: '要追加的内容块数组。', items: { type: 'object' } },
|
|
1038
|
-
},
|
|
1039
|
-
required: ['filePath', 'sections'],
|
|
1040
|
-
},
|
|
1041
|
-
},
|
|
1042
|
-
},
|
|
1043
|
-
{
|
|
1044
|
-
type: 'function',
|
|
1045
|
-
function: {
|
|
1046
|
-
name: 'overwriteDocx',
|
|
1047
|
-
description:
|
|
1048
|
-
'用新内容块完全替换已有 Word 文档的内容。参数:filePath 为目标 .docx 路径;sections 为新内容块数组(格式同 createDocx)。返回值:对象,包含 success、data(含 filePath)、error。',
|
|
1049
|
-
parameters: {
|
|
1050
|
-
type: 'object',
|
|
1051
|
-
properties: {
|
|
1052
|
-
filePath: { type: 'string', description: '目标 .docx 文件路径。' },
|
|
1053
|
-
sections: { type: 'array', description: '新内容块数组。', items: { type: 'object' } },
|
|
1054
|
-
},
|
|
1055
|
-
required: ['filePath', 'sections'],
|
|
1056
|
-
},
|
|
1057
|
-
},
|
|
1058
|
-
},
|
|
1059
|
-
{
|
|
1060
|
-
type: 'function',
|
|
1061
|
-
function: {
|
|
1062
|
-
name: 'replaceDocxText',
|
|
1063
|
-
description:
|
|
1064
|
-
'在 Word 文档中进行全局文本替换(包括正文、页眉、页脚)。参数:filePath 为目标 .docx 路径;searchText 为要查找的文本;replaceText 为替换后的文本。返回值:对象,包含 success、data(含 filePath、replaceCount)、error。',
|
|
1065
|
-
parameters: {
|
|
1066
|
-
type: 'object',
|
|
1067
|
-
properties: {
|
|
1068
|
-
filePath: { type: 'string', description: '目标 .docx 文件路径。' },
|
|
1069
|
-
searchText: { type: 'string', description: '要查找的文本。' },
|
|
1070
|
-
replaceText: { type: 'string', description: '替换后的文本。' },
|
|
1071
|
-
},
|
|
1072
|
-
required: ['filePath', 'searchText', 'replaceText'],
|
|
1073
|
-
},
|
|
1074
|
-
},
|
|
1075
|
-
},
|
|
1076
|
-
{
|
|
1077
|
-
type: 'function',
|
|
1078
|
-
function: {
|
|
1079
|
-
name: 'fillDocxTemplate',
|
|
1080
|
-
description: `用数据填充 Word 模板文档,生成新文档。模板中使用 {变量名} 作为占位符。
|
|
1081
|
-
参数:templatePath 为模板 .docx 路径;outputPath 为输出路径;data 为键值对象(如 { name:'张三', date:'2026-01-01' })。
|
|
1082
|
-
返回值:对象,包含 success、data(含 templatePath、outputPath)、error。`,
|
|
1083
|
-
parameters: {
|
|
1084
|
-
type: 'object',
|
|
1085
|
-
properties: {
|
|
1086
|
-
templatePath: { type: 'string', description: '模板 .docx 文件路径。' },
|
|
1087
|
-
outputPath: { type: 'string', description: '输出 .docx 文件路径。' },
|
|
1088
|
-
data: { type: 'object', description: '模板变量键值对,如 { title: "标题" }。' },
|
|
1089
|
-
},
|
|
1090
|
-
required: ['templatePath', 'outputPath', 'data'],
|
|
1091
|
-
},
|
|
1092
|
-
},
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
type: 'function',
|
|
1096
|
-
function: {
|
|
1097
|
-
name: 'mergeDocx',
|
|
1098
|
-
description:
|
|
1099
|
-
'将多个 Word 文档合并为一个,各文档之间以分页符分隔。参数:inputPaths 为源文件路径数组;outputPath 为输出路径。返回值:对象,包含 success、data(含 outputPath)、error。',
|
|
1100
|
-
parameters: {
|
|
1101
|
-
type: 'object',
|
|
1102
|
-
properties: {
|
|
1103
|
-
inputPaths: { type: 'array', items: { type: 'string' }, description: '要合并的 .docx 文件路径数组。' },
|
|
1104
|
-
outputPath: { type: 'string', description: '合并后输出的 .docx 文件路径。' },
|
|
1105
|
-
},
|
|
1106
|
-
required: ['inputPaths', 'outputPath'],
|
|
1107
|
-
},
|
|
1108
|
-
},
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
type: 'function',
|
|
1112
|
-
function: {
|
|
1113
|
-
name: 'extractDocxLinks',
|
|
1114
|
-
description:
|
|
1115
|
-
'提取 Word 文档中所有的超链接 URL。参数:filePath 为 .docx 文件路径。返回值:对象,包含 success、data(含 filePath、links 数组)、error。',
|
|
1116
|
-
parameters: {
|
|
1117
|
-
type: 'object',
|
|
1118
|
-
properties: {
|
|
1119
|
-
filePath: { type: 'string', description: '要提取链接的 .docx 文件路径。' },
|
|
1120
|
-
},
|
|
1121
|
-
required: ['filePath'],
|
|
1122
|
-
},
|
|
1123
|
-
},
|
|
1124
|
-
},
|
|
1125
|
-
{
|
|
1126
|
-
type: 'function',
|
|
1127
|
-
function: {
|
|
1128
|
-
name: 'getDocxParagraphStats',
|
|
1129
|
-
description:
|
|
1130
|
-
'统计 Word 文档各段落的字数与字符数。参数:filePath 为 .docx 文件路径。返回值:对象,包含 success、data(含 paragraphCount、totalWordCount、paragraphs 数组)、error。',
|
|
1131
|
-
parameters: {
|
|
1132
|
-
type: 'object',
|
|
1133
|
-
properties: {
|
|
1134
|
-
filePath: { type: 'string', description: '要统计的 .docx 文件路径。' },
|
|
1135
|
-
},
|
|
1136
|
-
required: ['filePath'],
|
|
1137
|
-
},
|
|
1138
|
-
},
|
|
1139
|
-
},
|
|
1140
|
-
{
|
|
1141
|
-
type: 'function',
|
|
1142
|
-
function: {
|
|
1143
|
-
name: 'wordToPdf',
|
|
1144
|
-
description:
|
|
1145
|
-
'将 Word 文档(.docx)转换为 PDF 文件。依赖 puppeteer,转换时保留基础格式。参数:inputPath 为源 .docx 路径;outputPath 为输出 .pdf 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
|
|
1146
|
-
parameters: {
|
|
1147
|
-
type: 'object',
|
|
1148
|
-
properties: {
|
|
1149
|
-
inputPath: { type: 'string', description: '源 .docx 文件路径。' },
|
|
1150
|
-
outputPath: { type: 'string', description: '输出 .pdf 文件路径。' },
|
|
1151
|
-
},
|
|
1152
|
-
required: ['inputPath', 'outputPath'],
|
|
1153
|
-
},
|
|
1154
|
-
},
|
|
1155
|
-
},
|
|
1156
|
-
{
|
|
1157
|
-
type: 'function',
|
|
1158
|
-
function: {
|
|
1159
|
-
name: 'wordToHtml',
|
|
1160
|
-
description:
|
|
1161
|
-
'将 Word 文档(.docx)转换为 HTML 文件,保留格式信息(粗体、斜体、表格等)。参数:inputPath 为源 .docx 路径;outputPath 为输出 .html 路径。返回值:对象,包含 success、data(含 inputPath、outputPath、messages)、error。',
|
|
1162
|
-
parameters: {
|
|
1163
|
-
type: 'object',
|
|
1164
|
-
properties: {
|
|
1165
|
-
inputPath: { type: 'string', description: '源 .docx 文件路径。' },
|
|
1166
|
-
outputPath: { type: 'string', description: '输出 .html 文件路径。' },
|
|
1167
|
-
},
|
|
1168
|
-
required: ['inputPath', 'outputPath'],
|
|
1169
|
-
},
|
|
1170
|
-
},
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
type: 'function',
|
|
1174
|
-
function: {
|
|
1175
|
-
name: 'wordToMarkdown',
|
|
1176
|
-
description:
|
|
1177
|
-
'将 Word 文档(.docx)转换为 Markdown 文件(.md)。通过 HTML 中间格式进行转换,保留标题、段落、表格、链接等结构。参数:inputPath 为源 .docx 路径;outputPath 为输出 .md 路径。返回值:对象,包含 success、data(含 inputPath、outputPath)、error。',
|
|
1178
|
-
parameters: {
|
|
1179
|
-
type: 'object',
|
|
1180
|
-
properties: {
|
|
1181
|
-
inputPath: { type: 'string', description: '源 .docx 文件路径。' },
|
|
1182
|
-
outputPath: { type: 'string', description: '输出 .md 文件路径。' },
|
|
1183
|
-
},
|
|
1184
|
-
required: ['inputPath', 'outputPath'],
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
{
|
|
1189
|
-
type: 'function',
|
|
1190
|
-
function: {
|
|
1191
|
-
name: 'markdownToWord',
|
|
1192
|
-
description:
|
|
1193
|
-
'将 Markdown 文件(.md)转换为 Word 文档(.docx)。支持标题、段落、表格、列表、代码块、引用等元素。参数:inputPath 为源 .md 路径;outputPath 为输出 .docx 路径。返回值:对象,包含 success、data(含 inputPath、outputPath、sectionCount)、error。',
|
|
1194
|
-
parameters: {
|
|
1195
|
-
type: 'object',
|
|
1196
|
-
properties: {
|
|
1197
|
-
inputPath: { type: 'string', description: '源 .md 文件路径。' },
|
|
1198
|
-
outputPath: { type: 'string', description: '输出 .docx 文件路径。' },
|
|
1199
|
-
},
|
|
1200
|
-
required: ['inputPath', 'outputPath'],
|
|
1201
|
-
},
|
|
1202
|
-
},
|
|
1203
|
-
},
|
|
1204
|
-
{
|
|
1205
|
-
type: 'function',
|
|
1206
|
-
function: {
|
|
1207
|
-
name: 'htmlToWord',
|
|
1208
|
-
description:
|
|
1209
|
-
'将 HTML 文件转换为 Word 文档(.docx),解析标题、段落、表格、列表等元素。参数:inputPath 为源 .html 路径;outputPath 为输出 .docx 路径。返回值:对象,包含 success、data(含 inputPath、outputPath、sectionCount)、error。',
|
|
1210
|
-
parameters: {
|
|
1211
|
-
type: 'object',
|
|
1212
|
-
properties: {
|
|
1213
|
-
inputPath: { type: 'string', description: '源 .html 文件路径。' },
|
|
1214
|
-
outputPath: { type: 'string', description: '输出 .docx 文件路径。' },
|
|
1215
|
-
},
|
|
1216
|
-
required: ['inputPath', 'outputPath'],
|
|
1217
|
-
},
|
|
1218
|
-
},
|
|
1219
|
-
},
|
|
1220
|
-
{
|
|
1221
|
-
type: 'function',
|
|
1222
|
-
function: {
|
|
1223
|
-
name: 'patchDocxText',
|
|
1224
|
-
description: `保留原格式修改 Word 文档内容(支持跨 run 文本替换)。与 replaceDocxText 的区别:本函数在替换时智能处理 Word XML 的 run 拆分问题,替换文本会沿用匹配位置的原有字符样式(字体、字号、颜色等),不重建文档结构,最大程度保留原有格式。
|
|
1225
|
-
参数:filePath 为目标 .docx 路径;replacements 为替换规则数组,每项格式 { search: '查找文本', replace: '替换文本' }。
|
|
1226
|
-
返回值:对象,包含 success、data(含 filePath、totalReplacements)、error。`,
|
|
1227
|
-
parameters: {
|
|
1228
|
-
type: 'object',
|
|
1229
|
-
properties: {
|
|
1230
|
-
filePath: { type: 'string', description: '目标 .docx 文件路径。' },
|
|
1231
|
-
replacements: {
|
|
1232
|
-
type: 'array',
|
|
1233
|
-
description: '替换规则数组,每项包含 search(查找文本)和 replace(替换文本)。',
|
|
1234
|
-
items: {
|
|
1235
|
-
type: 'object',
|
|
1236
|
-
properties: {
|
|
1237
|
-
search: { type: 'string', description: '要查找的文本。' },
|
|
1238
|
-
replace: { type: 'string', description: '替换后的文本。' },
|
|
1239
|
-
},
|
|
1240
|
-
required: ['search', 'replace'],
|
|
1241
|
-
},
|
|
1242
|
-
},
|
|
1243
|
-
},
|
|
1244
|
-
required: ['filePath', 'replacements'],
|
|
1245
|
-
},
|
|
1246
|
-
},
|
|
1247
|
-
},
|
|
1248
|
-
]
|
|
1249
|
-
|
|
1250
|
-
// ─── 导出 ──────────────────────────────────────────────────────────────────────
|
|
1251
|
-
|
|
1252
|
-
const functions = {
|
|
1253
|
-
docxReadme,
|
|
1254
|
-
readDocxText,
|
|
1255
|
-
readDocxHtml,
|
|
1256
|
-
getDocxInfo,
|
|
1257
|
-
searchDocxText,
|
|
1258
|
-
docxToText,
|
|
1259
|
-
docxToHtml,
|
|
1260
|
-
createDocx,
|
|
1261
|
-
appendToDocx,
|
|
1262
|
-
overwriteDocx,
|
|
1263
|
-
replaceDocxText,
|
|
1264
|
-
fillDocxTemplate,
|
|
1265
|
-
mergeDocx,
|
|
1266
|
-
extractDocxLinks,
|
|
1267
|
-
getDocxParagraphStats,
|
|
1268
|
-
wordToPdf,
|
|
1269
|
-
wordToHtml,
|
|
1270
|
-
wordToMarkdown,
|
|
1271
|
-
markdownToWord,
|
|
1272
|
-
htmlToWord,
|
|
1273
|
-
patchDocxText,
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const DocxTool = {
|
|
1277
|
-
name: 'DocxTool',
|
|
1278
|
-
description: '提供 Word 文档(.docx)的创建、读取、搜索、替换、模板填充、格式转换、合并,以及 Word/Markdown/HTML/PDF 格式互转等全面处理能力',
|
|
1279
|
-
// 格式转换:wordToPdf、wordToHtml、wordToMarkdown、markdownToWord、htmlToWord
|
|
1280
|
-
platform: 'all',
|
|
1281
|
-
descriptions,
|
|
1282
|
-
functions,
|
|
1283
|
-
isSystem: true
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
module.exports = DocxTool
|