@aalis/plugin-office 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,640 @@
1
+ import { AlignmentType, BorderStyle, convertInchesToTwip, Document, Footer, Header, HeadingLevel, ImageRun, LevelFormat, Packer, PageBreak, Paragraph, Table, TableCell, TableOfContents, TableRow, TextRun, WidthType, } from 'docx';
2
+ import { loadImage } from '../utils.js';
3
+ /**
4
+ * 文档排版预设。一键应用整套样式,避免 agent 逐项配置。
5
+ * - chinese-report: 中文报告(微软雅黑、12pt、1.5 倍行距、首行缩进 2 字符、段后 6pt)
6
+ * - chinese-academic: 中文学术(宋体、12pt、1.5 倍行距、首行缩进 2 字符、段后 0pt)
7
+ * - minimal: 极简(无首行缩进、1.15 倍行距)
8
+ */
9
+ const DOC_PRESETS = {
10
+ 'chinese-report': {
11
+ defaultFontFamily: 'Microsoft YaHei',
12
+ defaultFontSize: 12,
13
+ paragraphDefaults: {
14
+ firstLineIndentChars: 2,
15
+ lineHeight: 1.5,
16
+ spaceBefore: 0,
17
+ spaceAfter: 6,
18
+ },
19
+ },
20
+ 'chinese-academic': {
21
+ defaultFontFamily: 'SimSun',
22
+ defaultFontSize: 12,
23
+ paragraphDefaults: {
24
+ firstLineIndentChars: 2,
25
+ lineHeight: 1.5,
26
+ spaceBefore: 0,
27
+ spaceAfter: 0,
28
+ },
29
+ },
30
+ minimal: {
31
+ defaultFontFamily: undefined,
32
+ defaultFontSize: 11,
33
+ paragraphDefaults: {
34
+ firstLineIndentChars: 0,
35
+ lineHeight: 1.15,
36
+ spaceBefore: 0,
37
+ spaceAfter: 6,
38
+ },
39
+ },
40
+ };
41
+ /**
42
+ * 把含 \n 的纯文本拆成多个 TextRun,行间用 break: 1(软换行,保持同一段落)。
43
+ * 修复 agent 传 "第一行\n第二行" 时 Word 渲染为同一行的问题。
44
+ */
45
+ function splitTextToRuns(text, runOpts) {
46
+ const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
47
+ const lines = normalized.split('\n');
48
+ return lines.map((line, i) => new TextRun({
49
+ ...runOpts,
50
+ text: line,
51
+ break: i > 0 ? 1 : undefined,
52
+ }));
53
+ }
54
+ const headingMap = {
55
+ '1': HeadingLevel.HEADING_1,
56
+ '2': HeadingLevel.HEADING_2,
57
+ '3': HeadingLevel.HEADING_3,
58
+ '4': HeadingLevel.HEADING_4,
59
+ '5': HeadingLevel.HEADING_5,
60
+ '6': HeadingLevel.HEADING_6,
61
+ };
62
+ const alignMap = {
63
+ left: AlignmentType.LEFT,
64
+ center: AlignmentType.CENTER,
65
+ right: AlignmentType.RIGHT,
66
+ justify: AlignmentType.JUSTIFIED,
67
+ };
68
+ export function registerDocxTools(tools, sessions, storage, outputUri) {
69
+ function joinUri(base, rel) {
70
+ const b = base.endsWith('/') ? base : `${base}/`;
71
+ return `${b}${rel.replace(/^\/+/, '')}`;
72
+ }
73
+ // ---- doc_create ----
74
+ tools.register({
75
+ definition: {
76
+ type: 'function',
77
+ function: {
78
+ name: 'doc_create',
79
+ description: '创建一个新的 Word 文档会话,返回 docId 用于后续操作。该 docId 全局共享,可传递给子任务实现并行协作编辑。\n' +
80
+ '【排版建议】中文文档强烈建议传 preset="chinese-report"(自动应用:微软雅黑、12pt、1.5 倍行距、首行缩进 2 字符、段后 6pt)。' +
81
+ '不设 preset 时 Word 会用 Times New Roman + Calibri 默认,中文混排显示不佳。',
82
+ parameters: {
83
+ type: 'object',
84
+ properties: {
85
+ filename: { type: 'string', description: '文件名(如 report.docx)' },
86
+ title: { type: 'string', description: '文档标题(元数据)' },
87
+ author: { type: 'string', description: '作者' },
88
+ preset: {
89
+ type: 'string',
90
+ enum: ['chinese-report', 'chinese-academic', 'minimal'],
91
+ description: '排版预设:chinese-report=中文报告(微软雅黑/1.5行距/首行缩进2字符/段后6pt,推荐);chinese-academic=中文学术(宋体/1.5行距/首行缩进2字符);minimal=极简(无缩进/1.15行距)。不传则保留 Word 空默认。',
92
+ },
93
+ defaultFont: { type: 'string', description: '默认字体(覆盖 preset 设置)' },
94
+ defaultFontSize: { type: 'number', description: '默认字号(磅,覆盖 preset 设置)' },
95
+ },
96
+ required: ['filename'],
97
+ },
98
+ },
99
+ },
100
+ async handler(args) {
101
+ const filename = String(args.filename || 'document.docx');
102
+ const presetName = args.preset ? String(args.preset) : undefined;
103
+ const preset = presetName && presetName in DOC_PRESETS ? DOC_PRESETS[presetName] : undefined;
104
+ const state = {
105
+ doc: null,
106
+ sections: [],
107
+ styles: {
108
+ defaultFontFamily: args.defaultFont ? String(args.defaultFont) : preset?.defaultFontFamily,
109
+ defaultFontSize: args.defaultFontSize ? Number(args.defaultFontSize) : preset?.defaultFontSize,
110
+ defaultColor: args.defaultColor ? String(args.defaultColor) : undefined,
111
+ paragraphDefaults: { ...(preset?.paragraphDefaults ?? {}) },
112
+ },
113
+ };
114
+ // 存储元数据,最终 save 时构建 Document
115
+ const meta = {
116
+ title: args.title ? String(args.title) : undefined,
117
+ author: args.author ? String(args.author) : undefined,
118
+ };
119
+ const docId = sessions.create('docx', filename, { state, meta });
120
+ return JSON.stringify({
121
+ docId,
122
+ filename,
123
+ preset: presetName,
124
+ message: `Word 文档已创建${presetName ? `(预设:${presetName})` : ''},使用 docId="${docId}" 进行后续操作`,
125
+ });
126
+ },
127
+ });
128
+ // ---- doc_add_heading ----
129
+ tools.register({
130
+ definition: {
131
+ type: 'function',
132
+ function: {
133
+ name: 'doc_add_heading',
134
+ description: '向 Word 文档添加标题。',
135
+ parameters: {
136
+ type: 'object',
137
+ properties: {
138
+ docId: { type: 'string', description: '文档会话 ID' },
139
+ text: { type: 'string', description: '标题文本' },
140
+ level: { type: 'number', description: '标题级别 1-6,默认 1' },
141
+ alignment: { type: 'string', enum: ['left', 'center', 'right'], description: '对齐方式' },
142
+ },
143
+ required: ['docId', 'text'],
144
+ },
145
+ },
146
+ },
147
+ async handler(args) {
148
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
149
+ const level = headingMap[String(args.level || '1')] || HeadingLevel.HEADING_1;
150
+ const para = new Paragraph({
151
+ heading: level,
152
+ alignment: args.alignment ? alignMap[String(args.alignment)] : undefined,
153
+ children: splitTextToRuns(String(args.text), {
154
+ font: state.styles.defaultFontFamily ? { name: state.styles.defaultFontFamily } : undefined,
155
+ }),
156
+ });
157
+ state.sections.push(para);
158
+ return JSON.stringify({ success: true, message: `标题已添加: "${args.text}"` });
159
+ },
160
+ });
161
+ // ---- doc_add_paragraph ----
162
+ tools.register({
163
+ definition: {
164
+ type: 'function',
165
+ function: {
166
+ name: 'doc_add_paragraph',
167
+ description: '向 Word 文档添加段落。支持富文本(粗体、斜体、颜色等)。\n' +
168
+ '【换行】text 或 runs[].text 中的 \\n 会自动转为软换行(同段落内的换行)。多个段落请多次调用本工具。\n' +
169
+ '【排版】未传 alignment/indent/spacing 时,会自动应用 doc_create 的 preset 或 doc_set_style 设置的默认值。' +
170
+ '中文正文建议:firstLineIndentChars=2(首行缩进 2 字符)、lineHeight=1.5(1.5 倍行距)、段后 6pt。',
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ docId: { type: 'string', description: '文档会话 ID' },
175
+ text: { type: 'string', description: '纯文本内容(与 runs 二选一)。支持 \\n 软换行。' },
176
+ runs: {
177
+ type: 'array',
178
+ description: '富文本片段数组(与 text 二选一)。每个片段的 text 也支持 \\n 软换行。',
179
+ items: {
180
+ type: 'object',
181
+ properties: {
182
+ text: { type: 'string' },
183
+ bold: { type: 'boolean' },
184
+ italics: { type: 'boolean' },
185
+ underline: { type: 'boolean' },
186
+ color: { type: 'string', description: '十六进制颜色如 "FF0000"' },
187
+ size: { type: 'number', description: '字号(磅)' },
188
+ font: { type: 'string', description: '字体名' },
189
+ highlight: { type: 'string', description: '高亮颜色' },
190
+ strike: { type: 'boolean' },
191
+ superScript: { type: 'boolean' },
192
+ subScript: { type: 'boolean' },
193
+ },
194
+ required: ['text'],
195
+ },
196
+ },
197
+ alignment: { type: 'string', enum: ['left', 'center', 'right', 'justify'] },
198
+ firstLineIndentChars: {
199
+ type: 'number',
200
+ description: '首行缩进字符数(中文习惯,1 字符≈字号磅值;中文正文常用 2)。覆盖全局默认。',
201
+ },
202
+ lineHeight: {
203
+ type: 'number',
204
+ description: '行距倍数(1.0=单倍,1.5=1.5 倍,2.0=双倍)。覆盖全局默认。',
205
+ },
206
+ indent: { type: 'number', description: '首行缩进(磅,已废弃,建议改用 firstLineIndentChars)' },
207
+ spacing: {
208
+ type: 'object',
209
+ description: '段落间距与行距(磅),覆盖全局默认。',
210
+ properties: {
211
+ before: { type: 'number', description: '段前距(磅)' },
212
+ after: { type: 'number', description: '段后距(磅,中文正文常用 6)' },
213
+ line: { type: 'number', description: '行距倍数(与顶层 lineHeight 等价)' },
214
+ },
215
+ },
216
+ },
217
+ required: ['docId'],
218
+ },
219
+ },
220
+ },
221
+ async handler(args) {
222
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
223
+ const runs = [];
224
+ const defaultFontOpt = state.styles.defaultFontFamily ? { name: state.styles.defaultFontFamily } : undefined;
225
+ const defaultSizeHalf = state.styles.defaultFontSize ? state.styles.defaultFontSize * 2 : undefined;
226
+ if (args.runs && Array.isArray(args.runs)) {
227
+ for (const r of args.runs) {
228
+ const runOpts = {
229
+ bold: r.bold,
230
+ italics: r.italics,
231
+ underline: r.underline ? {} : undefined,
232
+ color: r.color ? String(r.color) : state.styles.defaultColor,
233
+ size: r.size ? Number(r.size) * 2 : defaultSizeHalf,
234
+ font: r.font ? { name: String(r.font) } : defaultFontOpt,
235
+ strike: r.strike,
236
+ superScript: r.superScript,
237
+ subScript: r.subScript,
238
+ highlight: r.highlight ? String(r.highlight) : undefined,
239
+ };
240
+ runs.push(...splitTextToRuns(String(r.text ?? ''), runOpts));
241
+ }
242
+ }
243
+ else if (args.text !== undefined && args.text !== null) {
244
+ runs.push(...splitTextToRuns(String(args.text), {
245
+ font: defaultFontOpt,
246
+ size: defaultSizeHalf,
247
+ color: state.styles.defaultColor,
248
+ }));
249
+ }
250
+ // 计算行距 / 首行缩进 / 段距:调用时显式参数 > paragraphDefaults > 不设置
251
+ const paraDef = state.styles.paragraphDefaults;
252
+ const spacing = (args.spacing ?? {});
253
+ const effLineHeight = typeof args.lineHeight === 'number'
254
+ ? Number(args.lineHeight)
255
+ : typeof spacing.line === 'number'
256
+ ? Number(spacing.line)
257
+ : paraDef.lineHeight;
258
+ const effSpaceBefore = typeof spacing.before === 'number' ? Number(spacing.before) : paraDef.spaceBefore;
259
+ const effSpaceAfter = typeof spacing.after === 'number' ? Number(spacing.after) : paraDef.spaceAfter;
260
+ const effFirstLineChars = typeof args.firstLineIndentChars === 'number'
261
+ ? Number(args.firstLineIndentChars)
262
+ : args.indent
263
+ ? undefined
264
+ : paraDef.firstLineIndentChars;
265
+ // 首行缩进:字符数 × 字号磅值 = 磅;转 twip(1 磅 = 20 twip)
266
+ const baseFontSize = state.styles.defaultFontSize ?? 12;
267
+ const firstLineTwip = effFirstLineChars && effFirstLineChars > 0
268
+ ? Math.round(effFirstLineChars * baseFontSize * 20)
269
+ : args.indent
270
+ ? convertInchesToTwip(Number(args.indent) / 72)
271
+ : undefined;
272
+ // docx 的 spacing.line 单位是二十分之一磅;1.0 倍行距对应字号 × 20。这里以 240(12pt 单倍)为基准乘以倍数。
273
+ const lineTwip = effLineHeight ? Math.round(effLineHeight * 240) : undefined;
274
+ const para = new Paragraph({
275
+ children: runs,
276
+ alignment: args.alignment ? alignMap[String(args.alignment)] : undefined,
277
+ indent: firstLineTwip ? { firstLine: firstLineTwip } : undefined,
278
+ spacing: effSpaceBefore !== undefined || effSpaceAfter !== undefined || lineTwip !== undefined
279
+ ? {
280
+ before: effSpaceBefore !== undefined ? effSpaceBefore * 20 : undefined,
281
+ after: effSpaceAfter !== undefined ? effSpaceAfter * 20 : undefined,
282
+ line: lineTwip,
283
+ }
284
+ : undefined,
285
+ });
286
+ state.sections.push(para);
287
+ return JSON.stringify({ success: true, message: '段落已添加' });
288
+ },
289
+ });
290
+ // ---- doc_add_table ----
291
+ tools.register({
292
+ definition: {
293
+ type: 'function',
294
+ function: {
295
+ name: 'doc_add_table',
296
+ description: '向 Word 文档添加表格。',
297
+ parameters: {
298
+ type: 'object',
299
+ properties: {
300
+ docId: { type: 'string', description: '文档会话 ID' },
301
+ headers: { type: 'array', items: { type: 'string' }, description: '表头行' },
302
+ rows: { type: 'array', items: { type: 'array', items: { type: 'string' } }, description: '数据行' },
303
+ columnWidths: { type: 'array', items: { type: 'number' }, description: '列宽百分比数组' },
304
+ headerBold: { type: 'boolean', description: '表头加粗,默认 true' },
305
+ borders: { type: 'boolean', description: '显示边框,默认 true' },
306
+ },
307
+ required: ['docId', 'rows'],
308
+ },
309
+ },
310
+ },
311
+ async handler(args) {
312
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
313
+ const headers = args.headers || [];
314
+ const rows = args.rows;
315
+ const headerBold = args.headerBold !== false;
316
+ const showBorders = args.borders !== false;
317
+ const borderStyle = showBorders
318
+ ? { style: BorderStyle.SINGLE, size: 1, color: '000000' }
319
+ : { style: BorderStyle.NONE, size: 0 };
320
+ const border = { top: borderStyle, bottom: borderStyle, left: borderStyle, right: borderStyle };
321
+ const makeRow = (cells, bold = false) => new TableRow({
322
+ children: cells.map(text => new TableCell({
323
+ children: [
324
+ new Paragraph({
325
+ children: splitTextToRuns(text, {
326
+ bold,
327
+ font: state.styles.defaultFontFamily ? { name: state.styles.defaultFontFamily } : undefined,
328
+ }),
329
+ }),
330
+ ],
331
+ borders: border,
332
+ })),
333
+ });
334
+ const tableRows = [];
335
+ if (headers.length > 0)
336
+ tableRows.push(makeRow(headers, headerBold));
337
+ for (const row of rows)
338
+ tableRows.push(makeRow(row));
339
+ state.sections.push(new Table({
340
+ rows: tableRows,
341
+ width: { size: 100, type: WidthType.PERCENTAGE },
342
+ }));
343
+ return JSON.stringify({
344
+ success: true,
345
+ message: `表格已添加 (${headers.length ? `${headers.length} 列` : `${rows[0]?.length} 列`}, ${rows.length} 行)`,
346
+ });
347
+ },
348
+ });
349
+ // ---- doc_add_image ----
350
+ tools.register({
351
+ definition: {
352
+ type: 'function',
353
+ function: {
354
+ name: 'doc_add_image',
355
+ description: '向 Word 文档添加图片。支持 URL 或本地文件路径。',
356
+ parameters: {
357
+ type: 'object',
358
+ properties: {
359
+ docId: { type: 'string', description: '文档会话 ID' },
360
+ source: { type: 'string', description: '图片 URL 或本地路径' },
361
+ width: { type: 'number', description: '宽度(像素),默认 400' },
362
+ height: { type: 'number', description: '高度(像素),默认按比例' },
363
+ alignment: { type: 'string', enum: ['left', 'center', 'right'] },
364
+ },
365
+ required: ['docId', 'source'],
366
+ },
367
+ },
368
+ },
369
+ async handler(args) {
370
+ sessions.require(String(args.docId), 'docx');
371
+ const { state } = sessions.get(String(args.docId)).doc;
372
+ const { buffer } = await loadImage(storage, String(args.source), outputUri);
373
+ const width = Number(args.width || 400);
374
+ const height = Number(args.height || 300);
375
+ const para = new Paragraph({
376
+ alignment: args.alignment ? alignMap[String(args.alignment)] : AlignmentType.CENTER,
377
+ children: [new ImageRun({ data: buffer, transformation: { width, height }, type: 'png' })],
378
+ });
379
+ state.sections.push(para);
380
+ return JSON.stringify({ success: true, message: `图片已添加 (${width}×${height})` });
381
+ },
382
+ });
383
+ // ---- doc_add_list ----
384
+ tools.register({
385
+ definition: {
386
+ type: 'function',
387
+ function: {
388
+ name: 'doc_add_list',
389
+ description: '向 Word 文档添加有序或无序列表。',
390
+ parameters: {
391
+ type: 'object',
392
+ properties: {
393
+ docId: { type: 'string', description: '文档会话 ID' },
394
+ items: { type: 'array', items: { type: 'string' }, description: '列表项' },
395
+ ordered: { type: 'boolean', description: '是否有序列表,默认 false' },
396
+ level: { type: 'number', description: '缩进级别 0-4,默认 0' },
397
+ },
398
+ required: ['docId', 'items'],
399
+ },
400
+ },
401
+ },
402
+ async handler(args) {
403
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
404
+ const items = args.items;
405
+ const ordered = !!args.ordered;
406
+ const level = Number(args.level || 0);
407
+ const reference = ordered ? 'aalis-ordered-list' : 'aalis-unordered-list';
408
+ for (const item of items) {
409
+ state.sections.push(new Paragraph({
410
+ children: splitTextToRuns(item, {
411
+ font: state.styles.defaultFontFamily ? { name: state.styles.defaultFontFamily } : undefined,
412
+ size: state.styles.defaultFontSize ? state.styles.defaultFontSize * 2 : undefined,
413
+ }),
414
+ numbering: { reference, level },
415
+ }));
416
+ }
417
+ return JSON.stringify({ success: true, message: `${ordered ? '有序' : '无序'}列表已添加 (${items.length} 项)` });
418
+ },
419
+ });
420
+ // ---- doc_add_page_break ----
421
+ tools.register({
422
+ definition: {
423
+ type: 'function',
424
+ function: {
425
+ name: 'doc_add_page_break',
426
+ description: '在 Word 文档中插入分页符。',
427
+ parameters: {
428
+ type: 'object',
429
+ properties: {
430
+ docId: { type: 'string', description: '文档会话 ID' },
431
+ },
432
+ required: ['docId'],
433
+ },
434
+ },
435
+ },
436
+ async handler(args) {
437
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
438
+ state.sections.push(new Paragraph({ children: [new PageBreak()] }));
439
+ return JSON.stringify({ success: true, message: '分页符已插入' });
440
+ },
441
+ });
442
+ // ---- doc_add_toc ----
443
+ tools.register({
444
+ definition: {
445
+ type: 'function',
446
+ function: {
447
+ name: 'doc_add_toc',
448
+ description: '向 Word 文档添加目录(Table of Contents)。需在 Word 中打开后更新域。',
449
+ parameters: {
450
+ type: 'object',
451
+ properties: {
452
+ docId: { type: 'string', description: '文档会话 ID' },
453
+ title: { type: 'string', description: '目录标题,默认"目录"' },
454
+ maxLevel: { type: 'number', description: '最大标题级别,默认 3' },
455
+ },
456
+ required: ['docId'],
457
+ },
458
+ },
459
+ },
460
+ async handler(args) {
461
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
462
+ const title = String(args.title || '目录');
463
+ state.sections.push(new Paragraph({
464
+ heading: HeadingLevel.HEADING_1,
465
+ children: [new TextRun({ text: title })],
466
+ }));
467
+ state.sections.push(new TableOfContents('TOC', {
468
+ hyperlink: true,
469
+ headingStyleRange: `1-${args.maxLevel || 3}`,
470
+ }));
471
+ return JSON.stringify({ success: true, message: '目录已添加(在 Word 中打开后请更新域以显示页码)' });
472
+ },
473
+ });
474
+ // ---- doc_set_header_footer ----
475
+ tools.register({
476
+ definition: {
477
+ type: 'function',
478
+ function: {
479
+ name: 'doc_set_header_footer',
480
+ description: '设置 Word 文档的页眉和/或页脚。',
481
+ parameters: {
482
+ type: 'object',
483
+ properties: {
484
+ docId: { type: 'string', description: '文档会话 ID' },
485
+ header: { type: 'string', description: '页眉文本' },
486
+ footer: { type: 'string', description: '页脚文本' },
487
+ headerAlignment: { type: 'string', enum: ['left', 'center', 'right'] },
488
+ footerAlignment: { type: 'string', enum: ['left', 'center', 'right'] },
489
+ showPageNumber: { type: 'boolean', description: '页脚显示页码,默认 false' },
490
+ },
491
+ required: ['docId'],
492
+ },
493
+ },
494
+ },
495
+ async handler(args) {
496
+ const internal = sessions.require(String(args.docId), 'docx').doc;
497
+ // 存储 header/footer 信息,在 save 时构建
498
+ internal.meta.headerText = args.header ? String(args.header) : undefined;
499
+ internal.meta.footerText = args.footer ? String(args.footer) : undefined;
500
+ internal.meta.headerAlignment = args.headerAlignment ? String(args.headerAlignment) : 'center';
501
+ internal.meta.footerAlignment = args.footerAlignment ? String(args.footerAlignment) : 'center';
502
+ internal.meta.showPageNumber = !!args.showPageNumber;
503
+ return JSON.stringify({ success: true, message: '页眉/页脚设置已保存' });
504
+ },
505
+ });
506
+ // ---- doc_set_style ----
507
+ tools.register({
508
+ definition: {
509
+ type: 'function',
510
+ function: {
511
+ name: 'doc_set_style',
512
+ description: '设置 Word 文档的全局默认样式。影响后续添加的内容;已添加的段落不受影响。\n' +
513
+ '【典型用法】中文报告:defaultFont="Microsoft YaHei", defaultFontSize=12, defaultFirstLineIndentChars=2, defaultLineHeight=1.5, defaultSpaceAfter=6。',
514
+ parameters: {
515
+ type: 'object',
516
+ properties: {
517
+ docId: { type: 'string', description: '文档会话 ID' },
518
+ defaultFont: { type: 'string', description: '默认字体(中文常用 "Microsoft YaHei"/"SimSun"/"SimHei")' },
519
+ defaultFontSize: { type: 'number', description: '默认字号(磅,中文正文常用 12=小四 或 10.5=五号)' },
520
+ defaultColor: { type: 'string', description: '默认文字颜色(十六进制如 "000000")' },
521
+ defaultFirstLineIndentChars: {
522
+ type: 'number',
523
+ description: '默认首行缩进字符数(中文正文常用 2;0=不缩进)',
524
+ },
525
+ defaultLineHeight: { type: 'number', description: '默认行距倍数(1.0/1.5/2.0;中文常用 1.5)' },
526
+ defaultSpaceBefore: { type: 'number', description: '默认段前距(磅,常用 0)' },
527
+ defaultSpaceAfter: { type: 'number', description: '默认段后距(磅,中文正文常用 6)' },
528
+ },
529
+ required: ['docId'],
530
+ },
531
+ },
532
+ },
533
+ async handler(args) {
534
+ const { state } = sessions.require(String(args.docId), 'docx').doc;
535
+ if (args.defaultFont)
536
+ state.styles.defaultFontFamily = String(args.defaultFont);
537
+ if (args.defaultFontSize)
538
+ state.styles.defaultFontSize = Number(args.defaultFontSize);
539
+ if (args.defaultColor)
540
+ state.styles.defaultColor = String(args.defaultColor);
541
+ const pd = state.styles.paragraphDefaults;
542
+ if (typeof args.defaultFirstLineIndentChars === 'number') {
543
+ pd.firstLineIndentChars = Number(args.defaultFirstLineIndentChars);
544
+ }
545
+ if (typeof args.defaultLineHeight === 'number')
546
+ pd.lineHeight = Number(args.defaultLineHeight);
547
+ if (typeof args.defaultSpaceBefore === 'number')
548
+ pd.spaceBefore = Number(args.defaultSpaceBefore);
549
+ if (typeof args.defaultSpaceAfter === 'number')
550
+ pd.spaceAfter = Number(args.defaultSpaceAfter);
551
+ return JSON.stringify({ success: true, message: '样式已更新', styles: { ...state.styles } });
552
+ },
553
+ });
554
+ // ---- doc_save ----
555
+ tools.register({
556
+ definition: {
557
+ type: 'function',
558
+ function: {
559
+ name: 'doc_save',
560
+ description: '保存 Word 文档到文件并释放文档会话。如果使用了子任务协作编辑,请确保所有子任务完成后再调用此工具保存。',
561
+ parameters: {
562
+ type: 'object',
563
+ properties: {
564
+ docId: { type: 'string', description: '文档会话 ID' },
565
+ },
566
+ required: ['docId'],
567
+ },
568
+ },
569
+ },
570
+ async handler(args) {
571
+ const session = sessions.require(String(args.docId), 'docx');
572
+ const { state, meta } = session.doc;
573
+ // 构建 header/footer
574
+ const headers = {};
575
+ const footers = {};
576
+ if (meta.headerText) {
577
+ headers.default = new Header({
578
+ children: [
579
+ new Paragraph({
580
+ alignment: alignMap[String(meta.headerAlignment || 'center')],
581
+ children: [new TextRun({ text: String(meta.headerText), italics: true, size: 18 })],
582
+ }),
583
+ ],
584
+ });
585
+ }
586
+ if (meta.footerText || meta.showPageNumber) {
587
+ const footerChildren = [];
588
+ if (meta.footerText)
589
+ footerChildren.push(new TextRun({ text: String(meta.footerText), size: 18 }));
590
+ footers.default = new Footer({
591
+ children: [
592
+ new Paragraph({
593
+ alignment: alignMap[String(meta.footerAlignment || 'center')],
594
+ children: footerChildren,
595
+ }),
596
+ ],
597
+ });
598
+ }
599
+ const doc = new Document({
600
+ title: meta.title ? String(meta.title) : undefined,
601
+ creator: meta.author ? String(meta.author) : 'Aalis',
602
+ numbering: {
603
+ config: [
604
+ {
605
+ reference: 'aalis-ordered-list',
606
+ levels: Array.from({ length: 5 }, (_, i) => ({
607
+ level: i,
608
+ format: LevelFormat.DECIMAL,
609
+ text: `%${i + 1}.`,
610
+ alignment: AlignmentType.LEFT,
611
+ })),
612
+ },
613
+ {
614
+ reference: 'aalis-unordered-list',
615
+ levels: Array.from({ length: 5 }, (_, i) => ({
616
+ level: i,
617
+ format: LevelFormat.BULLET,
618
+ text: '\u2022',
619
+ alignment: AlignmentType.LEFT,
620
+ })),
621
+ },
622
+ ],
623
+ },
624
+ sections: [
625
+ {
626
+ headers,
627
+ footers,
628
+ children: state.sections,
629
+ },
630
+ ],
631
+ });
632
+ const buffer = await Packer.toBuffer(doc);
633
+ const fileUri = joinUri(outputUri, session.filename);
634
+ await storage.writeFile(fileUri, buffer);
635
+ sessions.remove(session.id);
636
+ return JSON.stringify({ success: true, path: fileUri, size: buffer.length, message: `文档已保存: ${fileUri}` });
637
+ },
638
+ });
639
+ }
640
+ //# sourceMappingURL=docx.js.map