aico-cli 2.0.76 → 2.1.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.
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: word-document-processor
3
+ description: Word 文档(.docx)处理技能,支持创建、读取、编辑和模板渲染。使用场景:(1) 创建新的 Word 文档 (2) 读取和提取 .docx 文件内容 (3) 编辑现有文档内容 (4) 使用模板生成文档 (5) 处理文档中的表格、列表、样式等。当用户需要处理 Word 文档、生成报告、填充模板或提取文档内容时触发此技能。
4
+ ---
5
+
6
+ # Word Document Processor
7
+
8
+ Word 文档处理技能,基于 docxtemplater 和 pizzip 库实现。
9
+
10
+ ## 核心依赖
11
+
12
+ ```bash
13
+ npm install docxtemplater pizzip
14
+ ```
15
+
16
+ ## 快速开始
17
+
18
+ ### 读取 Word 文档
19
+
20
+ ```typescript
21
+ import PizZip from "pizzip";
22
+ import Docxtemplater from "docxtemplater";
23
+ import * as fs from "fs/promises";
24
+
25
+ async function readDocx(filePath: string): Promise<string> {
26
+ const content = await fs.readFile(filePath);
27
+ const zip = new PizZip(content);
28
+ const doc = new Docxtemplater(zip, {
29
+ paragraphLoop: true,
30
+ linebreaks: true,
31
+ });
32
+ return doc.getFullText();
33
+ }
34
+ ```
35
+
36
+ ### 使用模板生成文档
37
+
38
+ ```typescript
39
+ async function renderTemplate(
40
+ templatePath: string,
41
+ data: Record<string, any>,
42
+ outputPath: string
43
+ ): Promise<void> {
44
+ const templateContent = await fs.readFile(templatePath);
45
+ const zip = new PizZip(templateContent);
46
+
47
+ const doc = new Docxtemplater(zip, {
48
+ paragraphLoop: true,
49
+ linebreaks: true,
50
+ delimiters: { start: "{", end: "}" },
51
+ });
52
+
53
+ doc.render(data);
54
+
55
+ const output = doc.getZip().generate({
56
+ type: "nodebuffer",
57
+ compression: "DEFLATE",
58
+ });
59
+
60
+ await fs.writeFile(outputPath, output);
61
+ }
62
+ ```
63
+
64
+ ## 模板语法
65
+
66
+ ### 基础占位符
67
+
68
+ ```
69
+ {variableName} - 简单变量替换
70
+ {#items}...{/items} - 循环列表
71
+ {#condition}...{/condition} - 条件渲染
72
+ ```
73
+
74
+ ### 循环示例
75
+
76
+ 模板:
77
+
78
+ ```
79
+ {#users}
80
+ 姓名: {name}
81
+ 邮箱: {email}
82
+ {/users}
83
+ ```
84
+
85
+ 数据:
86
+
87
+ ```typescript
88
+ {
89
+ users: [
90
+ { name: "张三", email: "zhang@example.com" },
91
+ { name: "李四", email: "li@example.com" },
92
+ ];
93
+ }
94
+ ```
95
+
96
+ ### 嵌套循环
97
+
98
+ ```
99
+ {#departments}
100
+ 部门: {deptName}
101
+ {#employees}
102
+ - {empName} ({empRole})
103
+ {/employees}
104
+ {/departments}
105
+ ```
106
+
107
+ ## 高级功能
108
+
109
+ ### 处理表格
110
+
111
+ 表格中的循环会自动复制行:
112
+
113
+ ```
114
+ | 序号 | 名称 | 描述 |
115
+ |------|------|------|
116
+ {#items}
117
+ | {id} | {name} | {desc} |
118
+ {/items}
119
+ ```
120
+
121
+ ### 图片处理
122
+
123
+ 需要额外安装 `docxtemplater-image-module-free`:
124
+
125
+ ```typescript
126
+ import ImageModule from "docxtemplater-image-module-free";
127
+
128
+ const imageOpts = {
129
+ centered: false,
130
+ getImage: (tagValue: string) => fs.readFileSync(tagValue),
131
+ getSize: () => [150, 150],
132
+ };
133
+
134
+ const doc = new Docxtemplater(zip, {
135
+ modules: [new ImageModule(imageOpts)],
136
+ });
137
+ ```
138
+
139
+ ### 错误处理
140
+
141
+ ```typescript
142
+ try {
143
+ doc.render(data);
144
+ } catch (error: any) {
145
+ if (error.properties && error.properties.errors) {
146
+ const errors = error.properties.errors
147
+ .map((e: any) => e.properties?.explanation || e.message)
148
+ .join("; ");
149
+ throw new Error(`模板渲染错误: ${errors}`);
150
+ }
151
+ throw error;
152
+ }
153
+ ```
154
+
155
+ ## 完整示例
156
+
157
+ 参考 `scripts/docx-processor.ts` 获取完整的文档处理工具类实现。
158
+
159
+ ## 最佳实践
160
+
161
+ 1. **模板设计**: 使用清晰的占位符命名,避免特殊字符
162
+ 2. **数据准备**: 确保数据结构与模板占位符完全匹配
163
+ 3. **错误处理**: 始终捕获并处理 docxtemplater 的错误
164
+ 4. **性能优化**: 大文档使用流式处理,避免内存溢出
165
+ 5. **编码问题**: 确保模板文件使用 UTF-8 编码
166
+
167
+ ## 常见问题
168
+
169
+ - **占位符未替换**: 检查数据字段名是否与模板完全匹配
170
+ - **循环不工作**: 确保数据是数组格式
171
+ - **中文乱码**: 确保模板和代码都使用 UTF-8 编码
@@ -0,0 +1,187 @@
1
+ # Word 模板语法参考
2
+
3
+ ## 目录
4
+
5
+ 1. [基础语法](#基础语法)
6
+ 2. [循环语法](#循环语法)
7
+ 3. [条件语法](#条件语法)
8
+ 4. [表格处理](#表格处理)
9
+ 5. [高级用法](#高级用法)
10
+
11
+ ## 基础语法
12
+
13
+ ### 简单变量
14
+
15
+ ```
16
+ {variableName}
17
+ ```
18
+
19
+ 数据: `{ variableName: "Hello World" }`
20
+
21
+ ### 嵌套对象
22
+
23
+ ```
24
+ {user.name}
25
+ {user.address.city}
26
+ ```
27
+
28
+ 数据:
29
+
30
+ ```json
31
+ {
32
+ "user": {
33
+ "name": "张三",
34
+ "address": { "city": "北京" }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## 循环语法
40
+
41
+ ### 基础循环
42
+
43
+ ```
44
+ {#items}
45
+ - {name}: {value}
46
+ {/items}
47
+ ```
48
+
49
+ 数据:
50
+
51
+ ```json
52
+ {
53
+ "items": [
54
+ { "name": "项目A", "value": 100 },
55
+ { "name": "项目B", "value": 200 }
56
+ ]
57
+ }
58
+ ```
59
+
60
+ ### 嵌套循环
61
+
62
+ ```
63
+ {#departments}
64
+ 部门: {deptName}
65
+ {#members}
66
+ 员工: {memberName}
67
+ {/members}
68
+ {/departments}
69
+ ```
70
+
71
+ ### 循环索引
72
+
73
+ 使用 `@index` 获取当前索引(从 0 开始):
74
+
75
+ ```
76
+ {#items}
77
+ {@index}. {name}
78
+ {/items}
79
+ ```
80
+
81
+ ## 条件语法
82
+
83
+ ### 简单条件
84
+
85
+ ```
86
+ {#showSection}
87
+ 这部分内容只在 showSection 为 true 时显示
88
+ {/showSection}
89
+ ```
90
+
91
+ ### 反向条件
92
+
93
+ ```
94
+ {^isEmpty}
95
+ 列表不为空时显示
96
+ {/isEmpty}
97
+ ```
98
+
99
+ ### 条件与循环结合
100
+
101
+ ```
102
+ {#hasItems}
103
+ {#items}
104
+ - {name}
105
+ {/items}
106
+ {/hasItems}
107
+ {^hasItems}
108
+ 暂无数据
109
+ {/hasItems}
110
+ ```
111
+
112
+ ## 表格处理
113
+
114
+ ### 表格行循环
115
+
116
+ 在 Word 表格中,循环标签会自动复制整行:
117
+
118
+ | 序号 | 名称 | 金额 |
119
+ | ----------- | ------ | --------------- |
120
+ | {#rows}{id} | {name} | {amount}{/rows} |
121
+
122
+ 数据:
123
+
124
+ ```json
125
+ {
126
+ "rows": [
127
+ { "id": 1, "name": "商品A", "amount": "¥100" },
128
+ { "id": 2, "name": "商品B", "amount": "¥200" }
129
+ ]
130
+ }
131
+ ```
132
+
133
+ ### 嵌套表格
134
+
135
+ ```
136
+ {#orders}
137
+ 订单号: {orderNo}
138
+ | 商品 | 数量 | 单价 |
139
+ |------|------|------|
140
+ | {#items}{productName} | {quantity} | {price}{/items} |
141
+ {/orders}
142
+ ```
143
+
144
+ ## 高级用法
145
+
146
+ ### 原始 XML 输出
147
+
148
+ 使用 `{@rawXml}` 插入原始 XML:
149
+
150
+ ```
151
+ {@formattedContent}
152
+ ```
153
+
154
+ ### 空值处理
155
+
156
+ 建议在数据准备阶段处理空值:
157
+
158
+ ```typescript
159
+ function normalizeValue(val: any, defaultValue = "不涉及"): string {
160
+ if (!val || val === "" || val === "无") {
161
+ return defaultValue;
162
+ }
163
+ return String(val);
164
+ }
165
+ ```
166
+
167
+ ### 自定义分隔符
168
+
169
+ 如果模板中需要使用 `{}` 字符,可以更改分隔符:
170
+
171
+ ```typescript
172
+ const doc = new Docxtemplater(zip, {
173
+ delimiters: { start: "<<", end: ">>" },
174
+ });
175
+ ```
176
+
177
+ 模板变为: `<<variableName>>`
178
+
179
+ ### 换行处理
180
+
181
+ 启用 `linebreaks: true` 后,数据中的 `\n` 会转换为 Word 换行:
182
+
183
+ ```typescript
184
+ {
185
+ description: "第一行\n第二行\n第三行";
186
+ }
187
+ ```
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Word 文档处理工具类
3
+ * 提供 .docx 文件的读取、创建、编辑和模板渲染功能
4
+ */
5
+
6
+ import * as fs from "fs/promises";
7
+ import * as path from "path";
8
+ import Docxtemplater from "docxtemplater";
9
+ import PizZip from "pizzip";
10
+
11
+ export interface DocxProcessorOptions {
12
+ paragraphLoop?: boolean;
13
+ linebreaks?: boolean;
14
+ delimiters?: { start: string; end: string };
15
+ }
16
+
17
+ export interface RenderResult {
18
+ success: boolean;
19
+ outputPath: string | null;
20
+ error?: string;
21
+ }
22
+
23
+ const DEFAULT_OPTIONS: DocxProcessorOptions = {
24
+ paragraphLoop: true,
25
+ linebreaks: true,
26
+ delimiters: { start: "{", end: "}" },
27
+ };
28
+
29
+ /**
30
+ * 读取 Word 文档内容
31
+ */
32
+ export async function readDocx(filePath: string): Promise<string> {
33
+ const content = await fs.readFile(filePath);
34
+ const zip = new PizZip(content);
35
+ const doc = new Docxtemplater(zip, {
36
+ paragraphLoop: true,
37
+ linebreaks: true,
38
+ });
39
+ return doc.getFullText();
40
+ }
41
+
42
+ /**
43
+ * 获取文档中的所有占位符
44
+ */
45
+ export async function getPlaceholders(templatePath: string): Promise<string[]> {
46
+ const content = await fs.readFile(templatePath);
47
+ const zip = new PizZip(content);
48
+ const doc = new Docxtemplater(zip, {
49
+ paragraphLoop: true,
50
+ linebreaks: true,
51
+ });
52
+
53
+ // 获取模板中的所有标签
54
+ const text = doc.getFullText();
55
+ const placeholderRegex = /\{([^{}#/]+)\}/g;
56
+ const matches = text.match(placeholderRegex) || [];
57
+
58
+ return [...new Set(matches.map((m) => m.slice(1, -1)))];
59
+ }
60
+
61
+ /**
62
+ * 使用模板渲染文档
63
+ */
64
+ export async function renderTemplate(
65
+ templatePath: string,
66
+ data: Record<string, any>,
67
+ outputPath: string,
68
+ options: DocxProcessorOptions = {}
69
+ ): Promise<RenderResult> {
70
+ try {
71
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
72
+
73
+ // 检查模板文件
74
+ try {
75
+ await fs.access(templatePath);
76
+ } catch {
77
+ return {
78
+ success: false,
79
+ outputPath: null,
80
+ error: `模板文件不存在: ${templatePath}`,
81
+ };
82
+ }
83
+
84
+ const templateContent = await fs.readFile(templatePath);
85
+ const zip = new PizZip(templateContent);
86
+
87
+ const doc = new Docxtemplater(zip, {
88
+ paragraphLoop: mergedOptions.paragraphLoop,
89
+ linebreaks: mergedOptions.linebreaks,
90
+ delimiters: mergedOptions.delimiters,
91
+ });
92
+
93
+ doc.render(data);
94
+
95
+ const output = doc.getZip().generate({
96
+ type: "nodebuffer",
97
+ compression: "DEFLATE",
98
+ });
99
+
100
+ // 确保输出目录存在
101
+ const outputDir = path.dirname(outputPath);
102
+ await fs.mkdir(outputDir, { recursive: true });
103
+
104
+ await fs.writeFile(outputPath, output);
105
+
106
+ return {
107
+ success: true,
108
+ outputPath,
109
+ };
110
+ } catch (error: any) {
111
+ // 处理 docxtemplater 错误
112
+ if (error.properties && error.properties.errors) {
113
+ const errorMessages = error.properties.errors
114
+ .map((e: any) => e.properties?.explanation || e.message)
115
+ .join("; ");
116
+ return {
117
+ success: false,
118
+ outputPath: null,
119
+ error: `模板渲染错误: ${errorMessages}`,
120
+ };
121
+ }
122
+
123
+ return {
124
+ success: false,
125
+ outputPath: null,
126
+ error: error.message || "未知错误",
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 批量渲染文档
133
+ */
134
+ export async function batchRender(
135
+ templatePath: string,
136
+ dataList: Record<string, any>[],
137
+ outputDir: string,
138
+ fileNameGenerator: (data: Record<string, any>, index: number) => string
139
+ ): Promise<RenderResult[]> {
140
+ const results: RenderResult[] = [];
141
+
142
+ for (let i = 0; i < dataList.length; i++) {
143
+ const data = dataList[i];
144
+ const fileName = fileNameGenerator(data, i);
145
+ const outputPath = path.join(outputDir, fileName);
146
+
147
+ const result = await renderTemplate(templatePath, data, outputPath);
148
+ results.push(result);
149
+ }
150
+
151
+ return results;
152
+ }
153
+
154
+ /**
155
+ * 合并多个 Word 文档的文本内容
156
+ */
157
+ export async function mergeDocxTexts(filePaths: string[]): Promise<string> {
158
+ const texts: string[] = [];
159
+
160
+ for (const filePath of filePaths) {
161
+ const text = await readDocx(filePath);
162
+ texts.push(text);
163
+ }
164
+
165
+ return texts.join("\n\n---\n\n");
166
+ }
167
+
168
+ /**
169
+ * 验证模板数据完整性
170
+ */
171
+ export async function validateTemplateData(
172
+ templatePath: string,
173
+ data: Record<string, any>
174
+ ): Promise<{ valid: boolean; missingFields: string[] }> {
175
+ const placeholders = await getPlaceholders(templatePath);
176
+ const dataKeys = Object.keys(flattenObject(data));
177
+
178
+ const missingFields = placeholders.filter((p) => !dataKeys.includes(p));
179
+
180
+ return {
181
+ valid: missingFields.length === 0,
182
+ missingFields,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * 扁平化对象(用于验证嵌套数据)
188
+ */
189
+ function flattenObject(
190
+ obj: Record<string, any>,
191
+ prefix = ""
192
+ ): Record<string, any> {
193
+ const result: Record<string, any> = {};
194
+
195
+ for (const [key, value] of Object.entries(obj)) {
196
+ const newKey = prefix ? `${prefix}.${key}` : key;
197
+
198
+ if (value && typeof value === "object" && !Array.isArray(value)) {
199
+ Object.assign(result, flattenObject(value, newKey));
200
+ } else {
201
+ result[newKey] = value;
202
+ }
203
+ }
204
+
205
+ return result;
206
+ }