aico-cli 2.0.76 → 2.0.77

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.
@@ -14,7 +14,7 @@ import { join, dirname, basename } from 'pathe';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { EventEmitter } from 'node:events';
16
16
 
17
- const version = "2.0.76";
17
+ const version = "2.0.77";
18
18
 
19
19
  function displayBanner(subtitle) {
20
20
  const defaultSubtitle = "\u4E00\u952E\u914D\u7F6E\u4F60\u7684\u5F00\u53D1\u73AF\u5883";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aico-cli",
3
- "version": "2.0.76",
3
+ "version": "2.0.77",
4
4
  "packageManager": "pnpm@9.15.9",
5
5
  "description": "AI CLI",
6
6
  "repository": {
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: tech-spec-analyzer
3
+ description: 技术规格说明书生成指导,指导用户使用 MCP 工具分步生成技术规格说明书
4
+ tools: mcp__tech-spec-generator
5
+ color: blue
6
+ ---
7
+
8
+ # 技术规格说明书生成指导
9
+
10
+ 指导用户使用 MCP 工具分步生成技术规格说明书。
11
+
12
+ ## 🛠️ 可用 MCP 工具
13
+
14
+ ### 项目管理
15
+
16
+ | 工具名 | 说明 |
17
+ | -------------------------- | -------------------------- |
18
+ | `create_tech_spec_project` | 创建项目,返回 projectUuid |
19
+ | `get_project_progress` | 获取项目进度 |
20
+ | `list_tech_spec_projects` | 列出所有项目 |
21
+
22
+ ### 章节保存(可多次调用)
23
+
24
+ | 工具名 | 说明 |
25
+ | ------------------------- | -------------------------- |
26
+ | `save_service_interfaces` | 保存一个服务的接口清单 |
27
+ | `save_interface_design` | 保存一个服务的接口设计详情 |
28
+ | `save_database_table` | 保存一个数据库表结构 |
29
+ | `save_tech_solution` | 保存技术方案 |
30
+ | `save_business_exception` | 保存业务异常处理 |
31
+ | `save_appendix` | 保存附录(异常码、码值) |
32
+
33
+ ### 渲染与分析
34
+
35
+ | 工具名 | 说明 |
36
+ | --------------------------- | -------------------------- |
37
+ | `render_tech_spec_document` | 合并所有章节生成 Word 文档 |
38
+ | `analyze_project_structure` | 分析项目结构 |
39
+ | `analyze_large_project` | 自动分析大型项目 |
40
+
41
+ ## 📋 标准生成流程
42
+
43
+ ### 步骤 1:创建项目
44
+
45
+ ```
46
+ 调用 create_tech_spec_project
47
+ 参数:
48
+ - projectPath: 项目路径
49
+ - projectName: 项目名称
50
+ - moduleName: 模块名称
51
+ 返回:projectUuid(后续步骤必需)
52
+ ```
53
+
54
+ ### 步骤 2:保存服务接口清单
55
+
56
+ 对每个 Controller/Service 调用:
57
+
58
+ ```
59
+ 调用 save_service_interfaces
60
+ 参数:
61
+ - projectUuid: 步骤1返回的UUID
62
+ - serviceType: "REST" 或 "RPC"
63
+ - serviceEnglishName: 类名,如 "UserController"
64
+ - serviceChineseName: 中文名,如 "用户管理"
65
+ - serviceDescription: 服务描述
66
+ - interfaces: JSON字符串,格式:
67
+ [{"functionDescription":"查询用户","serviceName":"UserController","methodName":"getUser"}]
68
+ ```
69
+
70
+ ### 步骤 3:保存接口设计详情
71
+
72
+ 对每个服务调用:
73
+
74
+ ```
75
+ 调用 save_interface_design
76
+ 参数:
77
+ - projectUuid: UUID
78
+ - serviceName: 服务名称
79
+ - serviceDesc: 服务描述
80
+ - designs: JSON字符串,格式:
81
+ [{
82
+ "functionName": "查询用户",
83
+ "interfaceDesc": "根据ID查询用户信息",
84
+ "className": "UserController",
85
+ "methodName": "getUser",
86
+ "parameterList": [{"type":"Long","name":"id","required":"是","desc":"用户ID"}],
87
+ "returnResultList": [{"type":"UserVo","name":"data","desc":"用户信息"}],
88
+ "implementLogic": "1.校验参数 2.查询数据库 3.返回结果"
89
+ }]
90
+ ```
91
+
92
+ ### 步骤 4:保存数据库表结构
93
+
94
+ 对每个表调用:
95
+
96
+ ```
97
+ 调用 save_database_table
98
+ 参数:
99
+ - projectUuid: UUID
100
+ - tableName: 表名,如 "t_user"
101
+ - entityName: 实体名,如 "User"
102
+ - tableDescription: 表描述
103
+ - fields: JSON字符串,格式:
104
+ [{"fieldName":"id","fieldType":"bigint","fieldDescription":"主键","fieldLength":"20"}]
105
+ ```
106
+
107
+ ### 步骤 5:保存技术方案(可选)
108
+
109
+ ```
110
+ 调用 save_tech_solution
111
+ 参数:
112
+ - projectUuid: UUID
113
+ - content: Markdown格式的技术方案内容
114
+ ```
115
+
116
+ ### 步骤 6:保存附录(可选)
117
+
118
+ ```
119
+ 调用 save_appendix
120
+ 参数:
121
+ - projectUuid: UUID
122
+ - exceptionCodes: JSON字符串,格式:
123
+ [{"exceptionCode":"USER_001","exceptionDescription":"用户不存在"}]
124
+ - codeValues: JSON字符串,格式:
125
+ [{"propertyName":"用户状态","codeValue":"1-正常,0-禁用"}]
126
+ ```
127
+
128
+ ### 步骤 7:生成文档
129
+
130
+ ```
131
+ 调用 render_tech_spec_document
132
+ 参数:
133
+ - projectUuid: UUID
134
+ - outputDir: 输出目录(可选)
135
+ 返回:生成的 Word 文档路径
136
+ ```
137
+
138
+ ## ⚡ 快速模式
139
+
140
+ 对于大型项目,可使用自动分析:
141
+
142
+ ```
143
+ 调用 analyze_large_project
144
+ 参数:
145
+ - projectPath: 项目路径
146
+ - projectName: 项目名称(可选)
147
+ - moduleName: 模块名称(可选)
148
+ 返回:projectUuid
149
+
150
+ 然后调用 render_tech_spec_document 生成文档
151
+ ```
152
+
153
+ ## 📝 注意事项
154
+
155
+ 1. **分步保存**:每个服务/表单独调用保存,避免一次传输大量数据
156
+ 2. **JSON 字符串**:复杂参数使用 JSON 字符串格式传递
157
+ 3. **多次调用**:同类型章节可多次调用,系统会自动合并
158
+ 4. **projectUuid**:创建项目后返回,后续所有操作都需要此参数
@@ -0,0 +1,214 @@
1
+ ---
2
+ description: 技术规格说明书生成指挥官,支持单工程和多工程(微服务)合并生成
3
+ allowed-tools: Read(**), Write(.aico/tech/**), mcp__tech-spec-generator
4
+ argument-hint: <工程路径> [工程路径2...] [--name 项目名称] [--exclude 排除模块]
5
+ ---
6
+
7
+ ## 用法
8
+
9
+ `/tech-spec <路径> [选项]`
10
+
11
+ ### 单工程
12
+
13
+ ```bash
14
+ # 分析当前目录
15
+ /tech-spec .
16
+
17
+ # 分析指定工程
18
+ /tech-spec /path/to/project
19
+
20
+ # 指定项目名称
21
+ /tech-spec /path/to/project --name "用户管理系统"
22
+
23
+ # 排除测试模块
24
+ /tech-spec /path/to/project --exclude test,demo
25
+ ```
26
+
27
+ ### 多工程(微服务)
28
+
29
+ 多个工程并行分析,合并生成一本技术规格说明书:
30
+
31
+ ```bash
32
+ # 方式1:给父目录,自动识别子工程
33
+ /tech-spec /path/to/microservices-parent
34
+
35
+ # 方式2:明确指定多个工程路径
36
+ /tech-spec /path/to/user-service /path/to/order-service /path/to/payment-service
37
+
38
+ # 使用别名标识工程(格式:路径:别名)
39
+ /tech-spec /path/to/user-service:用户服务 /path/to/order-service:订单服务
40
+
41
+ # 指定合并后的项目名称
42
+ /tech-spec /path/to/microservices-parent --name "电商微服务系统"
43
+ ```
44
+
45
+ ## 智能体调用
46
+
47
+ ### 单工程模式
48
+
49
+ 调用 `tech-spec-analyzer` 智能体:
50
+
51
+ ```
52
+ tech-spec-analyzer
53
+ ├── projectPath: 工程路径
54
+ ├── projectName: 项目名称
55
+ └── excludeModules: 排除模块
56
+ ```
57
+
58
+ ### 多工程模式(并行分析 → 合并生成)
59
+
60
+ **并行调用多个 `tech-spec-analyzer` 智能体**分析各工程:
61
+
62
+ ```
63
+ ┌─────────────────────────────────────────────────────┐
64
+ │ 并行执行 │
65
+ ├─────────────────────────────────────────────────────┤
66
+ │ tech-spec-analyzer(用户服务) ──┐ │
67
+ │ tech-spec-analyzer(订单服务) ──┼── 等待所有完成 │
68
+ │ tech-spec-analyzer(支付服务) ──┘ │
69
+ └─────────────────────────────────────────────────────┘
70
+
71
+ 合并分析结果
72
+
73
+ 生成一本技术规格说明书
74
+ ```
75
+
76
+ ## 执行流程
77
+
78
+ ### 阶段一:参数解析与工程识别
79
+
80
+ 1. 解析输入路径
81
+ 2. **判断路径类型**:
82
+ - 单个工程(有 pom.xml/package.json/build.gradle)
83
+ - 父目录(包含多个子工程)
84
+ - 多个明确指定的工程路径
85
+ 3. **自动识别子工程**(父目录模式):
86
+ ```
87
+ 扫描子目录,识别包含以下文件的为独立工程:
88
+ - pom.xml(Java Maven)
89
+ - build.gradle(Java Gradle)
90
+ - package.json(Node.js)
91
+ ```
92
+ 4. 解析工程别名和选项参数
93
+
94
+ ### 阶段二:并行分析
95
+
96
+ **单工程**:
97
+
98
+ ```
99
+ 调用 tech-spec-analyzer 智能体
100
+ └── analyze_full_project → 生成文档
101
+ ```
102
+
103
+ **多工程**:
104
+
105
+ ```
106
+ 并行调用 N 个 tech-spec-analyzer 智能体(仅分析,不生成文档)
107
+ ├── 智能体1 → 分析用户服务 → 返回 serviceInterfaceList, tableInfoList
108
+ ├── 智能体2 → 分析订单服务 → 返回 serviceInterfaceList, tableInfoList
109
+ └── 智能体N → 分析支付服务 → 返回 serviceInterfaceList, tableInfoList
110
+ 等待所有智能体完成
111
+ ```
112
+
113
+ ### 阶段三:合并生成(多工程)
114
+
115
+ 收集所有智能体的分析结果,合并后生成一本文档:
116
+
117
+ ```
118
+ 合并规则:
119
+ 1. 服务接口:添加工程前缀 [用户服务] UserController
120
+ 2. 数据表:添加工程前缀 [用户服务] t_user
121
+ 3. 重新编号:合并后统一编号
122
+ 4. 生成统一文档
123
+ ```
124
+
125
+ ### 阶段四:结果展示
126
+
127
+ **单工程结果**:
128
+
129
+ ```markdown
130
+ ## 📄 技术规格说明书生成完成
131
+
132
+ **项目**:{projectName}
133
+ **文档路径**:{outputPath}
134
+
135
+ | 统计项 | 数量 |
136
+ | -------- | -------------- |
137
+ | 服务接口 | {serviceCount} |
138
+ | 数据表 | {tableCount} |
139
+ ```
140
+
141
+ **多工程结果**:
142
+
143
+ ```markdown
144
+ ## 📄 微服务技术规格说明书生成完成
145
+
146
+ **项目**:{projectName}
147
+ **文档路径**:{outputPath}
148
+
149
+ ### 各工程统计
150
+
151
+ | 工程 | 服务接口 | 数据表 |
152
+ | -------- | -------- | ------ |
153
+ | 用户服务 | 12 | 8 |
154
+ | 订单服务 | 15 | 10 |
155
+ | 支付服务 | 8 | 5 |
156
+ | **合计** | **35** | **23** |
157
+
158
+ 已合并生成一本技术规格说明书。
159
+ ```
160
+
161
+ ## 流程可视化
162
+
163
+ ```mermaid
164
+ graph TD
165
+ A[用户输入路径] --> B{路径类型判断}
166
+
167
+ B -->|单工程| C[tech-spec-analyzer]
168
+ C --> D[analyze_full_project]
169
+ D --> E[生成文档]
170
+
171
+ B -->|父目录| F[扫描识别子工程]
172
+ F --> G{发现多个工程?}
173
+ G -->|是| H[并行调用智能体]
174
+ G -->|否| C
175
+
176
+ B -->|多个路径| H
177
+
178
+ H --> I1[智能体1: 分析工程1]
179
+ H --> I2[智能体2: 分析工程2]
180
+ H --> I3[智能体N: 分析工程N]
181
+ I1 --> J[等待所有完成]
182
+ I2 --> J
183
+ I3 --> J
184
+ J --> K[合并分析结果]
185
+ K --> L[生成合并文档]
186
+
187
+ E --> M[展示结果]
188
+ L --> M
189
+ ```
190
+
191
+ ## MCP 工具说明
192
+
193
+ ### analyze_project_structure
194
+
195
+ 扫描工程目录,识别文件类型和模块。
196
+
197
+ ### analyze_full_project
198
+
199
+ 完整分析工程并生成技术规格说明书。
200
+
201
+ **参数**:
202
+
203
+ - `projectPath`: 工程根目录(必填)
204
+ - `projectName`: 项目名称(可选)
205
+ - `moduleName`: 模块名称(可选)
206
+ - `excludeModules`: 排除的模块列表(可选)
207
+
208
+ ## 输出
209
+
210
+ 文档保存到 `.aico/tech/` 目录:
211
+
212
+ ```
213
+ 技术规格说明书_{项目名}_{模块名}_{时间戳}.docx
214
+ ```
@@ -4,6 +4,8 @@
4
4
  "DISABLE_TELEMETRY": "1",
5
5
  "DISABLE_ERROR_REPORTING": "1",
6
6
  "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
7
+ "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000",
8
+ "MAX_THINKING_TOKENS": "31999",
7
9
  "ANTHROPIC_BASE_URL": "http://127.0.0.1:3456",
8
10
  "ANTHROPIC_AUTH_TOKEN": "sk-aico-x-ccr"
9
11
  },
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 五域科技
3
+ Copyright (c) 2025 智能软件星工厂
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 智能软件星工厂
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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
+ }