hookehuyr

docs(parse): 完善文档解析改造文档与测试验证

### 新增
- 文档解析改造任务清单说明
- 文本抽取管线、结构化校验、写入稳态化等模块说明
- 解析摘要输出与审计日志功能说明
- 计划书模块定位与优化建议

### 修复
- 修复 ESLint 警告

### 测试
- 补充解析流程集成测试与边界测试
- 新增 fixtures 文档样本说明

---

**详细信息**:
- **影响文件**: README.md, docs/CHANGELOG.md, docs/PLAN/plan-form-schema-usage.md, docs/to-parse/README.md, scripts/parse-docs.js, scripts/parse-docs.test.js
- **技术栈**: Node.js, Vitest, 文档维护
- **测试状态**: 已通过 (pnpm test),ESLint 存在现有警告
- **备注**: 每次解析都有可追溯审计记录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -70,6 +70,14 @@ pnpm lint ...@@ -70,6 +70,14 @@ pnpm lint
70 -**新人指南更新** - 入口文档从工具生成器调整为业务上手流程 70 -**新人指南更新** - 入口文档从工具生成器调整为业务上手流程
71 -**文档导航同步** - docs/README 快速导航修正与补充 71 -**文档导航同步** - docs/README 快速导航修正与补充
72 72
73 +### 文档解析改造
74 +-**任务清单** - 输出文档解析改造任务清单,便于跟踪与回顾
75 +-**文本抽取管线** - 接入 PDF/Docx 文本抽取与统一结构输出
76 +-**结构化校验** - 接入 JSON Schema 校验并阻断非法配置写入
77 +-**写入稳态化** - 结构化插入、重复检测与 dry-run 预览已接入
78 +-**输出结构补齐** - 解析输出 JSON 结构与稳定 form_sn 规则已明确
79 +-**审计与摘要** - 解析摘要与审计日志输出已接入
80 +
73 ### 计划书模块定位 81 ### 计划书模块定位
74 -**配置与入口整理** - 补充计划书模块入口、配置与 API 位置说明 82 -**配置与入口整理** - 补充计划书模块入口、配置与 API 位置说明
75 -**优化建议** - 新增产品时优先补齐 form_sn 与 plan_config,避免模板缺失 83 -**优化建议** - 新增产品时优先补齐 form_sn 与 plan_config,避免模板缺失
...@@ -365,6 +373,8 @@ export default { ...@@ -365,6 +373,8 @@ export default {
365 373
366 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 374 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
367 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) 375 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
376 +- **[文档解析待处理说明](docs/to-parse/README.md)** - 文档解析样本与脚本使用方式
377 +- **[文档解析改造任务](docs/tasks/文档解析改造-tasks.md)** - 解析链路改造进度与验收
368 - [Taro 官方文档](https://docs.taro.zone/) 378 - [Taro 官方文档](https://docs.taro.zone/)
369 - [NutUI 文档](https://nutui.jd.com/taro/) 379 - [NutUI 文档](https://nutui.jd.com/taro/)
370 - [Vue 3 文档](https://cn.vuejs.org/) 380 - [Vue 3 文档](https://cn.vuejs.org/)
......
1 +## [2026-02-14] - 运营与审计完善
2 +
3 +### 新增
4 +- 解析摘要输出(成功/失败/耗时)并生成审计日志与变更摘要
5 +- 使用说明补充解析摘要与审计日志位置
6 +
7 +---
8 +
9 +**详细信息**
10 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/to-parse/README.md, docs/tasks/文档解析改造-tasks.md, README.md
11 +- **技术栈**: Node.js, Vitest, 文档维护
12 +- **测试状态**: pnpm test 通过;pnpm lint 30 warnings
13 +- **备注**: 每次解析都有可追溯审计记录
14 +
15 +---
16 +
17 +## [2026-02-14] - 测试与验证完善
18 +
19 +### 新增
20 +- 补充解析流程集成测试与 updateConfigContent 边界测试
21 +- 新增 fixtures 文档样本说明并补齐相关文档入口
22 +
23 +---
24 +
25 +**详细信息**
26 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/to-parse/README.md, docs/tasks/文档解析改造-tasks.md, README.md
27 +- **技术栈**: Node.js, Vitest, 文档维护
28 +- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
29 +- **备注**: 解析流程测试可重复运行,覆盖冲突与插入边界路径
30 +
31 +---
32 +
33 +## [2026-02-14] - 生成与写入稳态化
34 +
35 +### 新增
36 +- 结构化定位 PLAN_TEMPLATES 插入位置并支持 dry-run 变更预览
37 +- 增加重复 form_sn 冲突检测与阻断写入
38 +- 完善备份记录并支持回滚入口
39 +
40 +---
41 +
42 +**详细信息**
43 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/tasks/文档解析改造-tasks.md, README.md
44 +- **技术栈**: Node.js, Vitest
45 +- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
46 +- **备注**: 解析写入路径更稳定,新增冲突保护与预览模式
47 +
48 +---
49 +
50 +## [2026-02-14] - 结构化解析校验接入
51 +
52 +### 新增
53 +- 接入 JSON Schema 校验并输出缺失字段报告
54 +- 校验失败阻断解析结果写入配置
55 +- 单测覆盖校验通过与失败路径
56 +
57 +---
58 +
59 +**详细信息**
60 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, package.json, docs/tasks/文档解析改造-tasks.md, README.md
61 +- **技术栈**: Node.js, Ajv, Vitest
62 +- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
63 +- **备注**: 校验规则覆盖核心字段并保留扩展字段
64 +
65 +---
66 +
67 +## [2026-02-14] - 文本抽取管线接入
68 +
69 +### 新增
70 +- 接入 PDF 文本抽取与页数元信息
71 +- 接入 Docx 文本抽取并输出警告信息
72 +- 统一抽取结果结构并增加抽取失败回退
73 +
74 +---
75 +
76 +**详细信息**
77 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, package.json, docs/tasks/文档解析改造-tasks.md, README.md
78 +- **技术栈**: Node.js, Vitest
79 +- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
80 +- **备注**: .doc 文件提示转换为 .docx,OCR 预留未启用
81 +
82 +---
83 +
84 +## [2026-02-14] - 文档解析输出定义完善
85 +
86 +### 更新
87 +- 明确解析输出 JSON 结构并补齐示例与约束
88 +- 生成 form_sn 改为稳定的 slug + hash 规则
89 +- 配置生成支持 form_schema 与 submit_mapping 输出
90 +
91 +---
92 +
93 +**详细信息**
94 +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/plan/plan-form-schema-usage.md, docs/tasks/文档解析改造-tasks.md, README.md
95 +- **技术栈**: Node.js, Vitest, 文档维护
96 +- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
97 +- **备注**: 解析输出结构对齐 Schema 与提交映射配置
98 +
99 +---
100 +
101 +## [2026-02-14] - 文档解析改造任务清单
102 +
103 +### 新增
104 +- 新增文档解析改造任务清单,细化步骤与验收标准
105 +
106 +---
107 +
108 +**详细信息**
109 +- **影响文件**: docs/tasks/文档解析改造-tasks.md, README.md
110 +- **技术栈**: 文档维护
111 +- **测试状态**: 不适用
112 +- **备注**: 任务完成后按清单勾选便于回顾
113 +
114 +---
115 +
1 ## [2026-02-14] - 优化计划书字段配置管理 116 ## [2026-02-14] - 优化计划书字段配置管理
2 117
3 ### 新增 118 ### 新增
......
...@@ -85,7 +85,37 @@ const submit_mapping = { ...@@ -85,7 +85,37 @@ const submit_mapping = {
85 } 85 }
86 ``` 86 ```
87 87
88 -## 8. 使用示例 88 +## 8. 解析输出结构
89 +解析脚本输出 JSON 用于生成 `plan-templates` 配置,字段结构与 `form_schema``submit_mapping` 对齐:
90 +
91 +```javascript
92 +{
93 + product_name: '宏挚传承保障计划',
94 + product_type: 'savings',
95 + form_sn: 'savings-hong-zhi-chuan-cheng-abcdef12',
96 + currency: 'USD',
97 + payment_periods: ['整付', '3年', '5年'],
98 + age_range: { min: 0, max: 75 },
99 + insurance_period: '终身',
100 + is_savings: true,
101 + withdrawal_modes: ['年龄指定金额', '最高固定金额'],
102 + withdrawal_periods: ['1年', '3年', '5年', '10年'],
103 + form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
104 + submit_mapping: { coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' } },
105 + source_file: '产品说明书.pdf',
106 + warnings: []
107 +}
108 +```
109 +
110 +字段约束与可选项:
111 +- 必填:`product_name``product_type``currency``payment_periods``age_range``insurance_period`
112 +- 可选:`form_sn``is_savings``withdrawal_modes``withdrawal_periods``form_schema``submit_mapping``source_file``warnings`
113 +- `form_sn` 若未传入,按规则自动生成稳定值
114 +- `payment_periods` 必须为非空数组
115 +- `age_range.min``age_range.max`
116 +- 储蓄产品需提供 `withdrawal_modes``withdrawal_periods`
117 +
118 +## 9. 使用示例
89 ```vue 119 ```vue
90 <!-- 储蓄型模板使用示例 --> 120 <!-- 储蓄型模板使用示例 -->
91 <template> 121 <template>
...@@ -111,7 +141,7 @@ const template_config = { ...@@ -111,7 +141,7 @@ const template_config = {
111 </script> 141 </script>
112 ``` 142 ```
113 143
114 -## 8.1 人寿/重疾模板使用示例 144 +## 9.1 人寿/重疾模板使用示例
115 ```vue 145 ```vue
116 <template> 146 <template>
117 <LifeInsuranceTemplate v-model="form_data" :config="template_config" /> 147 <LifeInsuranceTemplate v-model="form_data" :config="template_config" />
...@@ -129,14 +159,14 @@ const template_config = { ...@@ -129,14 +159,14 @@ const template_config = {
129 </script> 159 </script>
130 ``` 160 ```
131 161
132 -## 9. 新增保险类型流程 162 +## 10. 新增保险类型流程
133 1.`src/config/plan-templates.js` 新增产品项(配置 form_sn) 163 1.`src/config/plan-templates.js` 新增产品项(配置 form_sn)
134 2. 为该产品选择已有模板组件或新增模板组件 164 2. 为该产品选择已有模板组件或新增模板组件
135 3. 定义 `form_schema``submit_mapping` 165 3. 定义 `form_schema``submit_mapping`
136 4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑) 166 4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑)
137 5. 验证校验与提交映射 167 5. 验证校验与提交映射
138 168
139 -## 10. 新增产品配置示例 169 +## 11. 新增产品配置示例
140 ```javascript 170 ```javascript
141 // 示例:新增储蓄类产品配置 171 // 示例:新增储蓄类产品配置
142 'savings-new': { 172 'savings-new': {
...@@ -171,20 +201,20 @@ const template_config = { ...@@ -171,20 +201,20 @@ const template_config = {
171 } 201 }
172 ``` 202 ```
173 203
174 -## 11. 常见扩展点 204 +## 12. 常见扩展点
175 - 新字段:仅在 form_schema 增加字段并补充 submit_mapping 205 - 新字段:仅在 form_schema 增加字段并补充 submit_mapping
176 - 新联动:在 show_when 与 reset_map 中定义条件 206 - 新联动:在 show_when 与 reset_map 中定义条件
177 - 新模板:复用现有字段组件,保持 schema 结构一致 207 - 新模板:复用现有字段组件,保持 schema 结构一致
178 208
179 -## 12. 计划书模块入口与配置地图 209 +## 13. 计划书模块入口与配置地图
180 -### 12.1 页面入口 210 +### 13.1 页面入口
181 - 产品详情:`src/pages/product-detail/index.vue`(按钮打开计划书弹窗) 211 - 产品详情:`src/pages/product-detail/index.vue`(按钮打开计划书弹窗)
182 - 产品中心:`src/pages/product-center/index.vue`(列表内“计划书”按钮) 212 - 产品中心:`src/pages/product-center/index.vue`(列表内“计划书”按钮)
183 - 搜索页:`src/pages/search/index.vue`(搜索结果卡片“计划书”按钮) 213 - 搜索页:`src/pages/search/index.vue`(搜索结果卡片“计划书”按钮)
184 - 计划书列表:`src/pages/plan/index.vue`(查看/删除计划书) 214 - 计划书列表:`src/pages/plan/index.vue`(查看/删除计划书)
185 - 提交结果页:`src/pages/plan-submit-result/index.vue` 215 - 提交结果页:`src/pages/plan-submit-result/index.vue`
186 216
187 -### 12.2 组件与模板 217 +### 13.2 组件与模板
188 - 弹窗容器:`src/components/plan/PlanPopupNew.vue` 218 - 弹窗容器:`src/components/plan/PlanPopupNew.vue`
189 - 计划书容器:`src/components/plan/PlanFormContainer.vue` 219 - 计划书容器:`src/components/plan/PlanFormContainer.vue`
190 - 模板组件: 220 - 模板组件:
...@@ -193,7 +223,7 @@ const template_config = { ...@@ -193,7 +223,7 @@ const template_config = {
193 - `src/components/plan/PlanTemplates/SavingsTemplate.vue` 223 - `src/components/plan/PlanTemplates/SavingsTemplate.vue`
194 - 字段组件:`src/components/plan/PlanFields/*` 224 - 字段组件:`src/components/plan/PlanFields/*`
195 225
196 -### 12.3 配置与数据处理 226 +### 13.3 配置与数据处理
197 - 模板映射:`src/config/plan-templates.js` 227 - 模板映射:`src/config/plan-templates.js`
198 - 字段定义与映射:`src/config/plan-fields.js` 228 - 字段定义与映射:`src/config/plan-fields.js`
199 - 字段转换函数:`src/utils/planFieldTransformers.js` 229 - 字段转换函数:`src/utils/planFieldTransformers.js`
...@@ -202,18 +232,18 @@ const template_config = { ...@@ -202,18 +232,18 @@ const template_config = {
202 - 字段校验工具:`src/utils/planFieldValidation.js` 232 - 字段校验工具:`src/utils/planFieldValidation.js`
203 - 订单状态常量:`src/config/constants/orderStatus.js` 233 - 订单状态常量:`src/config/constants/orderStatus.js`
204 234
205 -### 12.4 API 入口 235 +### 13.4 API 入口
206 - 计划书 API:`src/api/plan.js` 236 - 计划书 API:`src/api/plan.js`
207 - 新增:`addAPI` 237 - 新增:`addAPI`
208 - 列表:`listAPI` 238 - 列表:`listAPI`
209 - 删除:`deleteAPI` 239 - 删除:`deleteAPI`
210 - 查看:`viewAPI` 240 - 查看:`viewAPI`
211 241
212 -### 12.5 技术书/附件预览关联 242 +### 13.5 技术书/附件预览关联
213 - 产品详情附件列表:`src/pages/product-detail/index.vue` 243 - 产品详情附件列表:`src/pages/product-detail/index.vue`
214 - 文件预览能力:`src/composables/useFileOperation.js` 244 - 文件预览能力:`src/composables/useFileOperation.js`
215 245
216 -## 13. 计划书模块使用流程 246 +## 14. 计划书模块使用流程
217 1. 产品详情/产品中心/搜索页获取产品对象(至少包含 `id``form_sn`,可选 `plan_config` 247 1. 产品详情/产品中心/搜索页获取产品对象(至少包含 `id``form_sn`,可选 `plan_config`
218 2. 打开 `PlanFormContainer` 并传入 `product` 248 2. 打开 `PlanFormContainer` 并传入 `product`
219 3. `PlanFormContainer` 根据 `form_sn``plan-templates` 选择模板并合并 `plan_config` 249 3. `PlanFormContainer` 根据 `form_sn``plan-templates` 选择模板并合并 `plan_config`
...@@ -222,7 +252,7 @@ const template_config = { ...@@ -222,7 +252,7 @@ const template_config = {
222 6. 提交完成后通过 `usePlanSubmit` 跳转到提交结果页 252 6. 提交完成后通过 `usePlanSubmit` 跳转到提交结果页
223 7. 在计划书列表中用 `listAPI` 拉取数据,使用 `viewAPI` 标记为已查看 253 7. 在计划书列表中用 `listAPI` 拉取数据,使用 `viewAPI` 标记为已查看
224 254
225 -## 14. 计划书容器使用示例 255 +## 15. 计划书容器使用示例
226 ```vue 256 ```vue
227 <template> 257 <template>
228 <PlanFormContainer 258 <PlanFormContainer
......
1 +# 文档解析改造任务清单
2 +
3 +> **创建时间**: 2026-02-14
4 +> **分支**: 当前分支
5 +> **目标**: 文档解析从 mock 走向可用链路
6 +
7 +---
8 +
9 +## 📊 总体进度
10 +
11 +- [x] **第 1 步**: 目标与输出定义
12 +- [ ] **第 2 步**: 文本抽取管线
13 +- [ ] **第 3 步**: 结构化解析与校验
14 +- [x] **第 4 步**: 生成与写入稳态化
15 +- [x] **第 5 步**: 测试与验证
16 +- [x] **第 6 步**: 运营与审计
17 +
18 +---
19 +
20 +## 📝 任务详情
21 +
22 +### 第 1 步:目标与输出定义
23 +
24 +**目标**: 明确解析输出结构与计划书配置的对齐规则
25 +
26 +**文件**:
27 +- `docs/plan/plan-form-schema-usage.md`
28 +- `scripts/parse-docs.js`
29 +
30 +**子任务**:
31 +- [x] 定义解析输出 JSON 结构(字段、类型、必填/可选)
32 +- [x] 对齐 form_schema 与 submit_mapping 规范
33 +- [x] 明确 form_sn 可复现生成规则
34 +- [x] 补齐输出示例与边界约束说明
35 +
36 +**验收标准**:
37 +- [x] 输出结构在文档中完整可查
38 +- [x] form_sn 规则具备稳定性与可追溯性
39 +- [x] 解析输出可直接用于配置生成
40 +
41 +---
42 +
43 +### 第 2 步:文本抽取管线
44 +
45 +**目标**: 建立 PDF/Word 文本抽取基础能力
46 +
47 +**文件**:
48 +- `scripts/parse-docs.js`
49 +- `package.json`
50 +
51 +**子任务**:
52 +- [x] 选择 PDF 文本抽取方案并完成接入
53 +- [x] 选择 Doc/Docx 文本抽取方案并完成接入
54 +- [x] 为扫描文档预留 OCR 接口与降级策略
55 +- [x] 统一抽取结果结构(text/meta/warnings)
56 +- [x] 增加抽取失败的错误提示与回退逻辑
57 +
58 +**验收标准**:
59 +- [x] PDF 与 Docx 均可输出可用文本
60 +- [x] 抽取失败可定位原因并不写入配置
61 +- [x] 日志记录包含文件名与失败原因
62 +
63 +---
64 +
65 +### 第 3 步:结构化解析与校验
66 +
67 +**目标**: 将文本解析成结构化配置并进行校验
68 +
69 +**文件**:
70 +- `scripts/parse-docs.js`
71 +- `scripts/parse-docs.test.js`
72 +
73 +**子任务**:
74 +- [x] 定义 JSON Schema 校验规则
75 +- [x] 接入结构化解析结果校验
76 +- [x] 校验失败输出清晰报告
77 +- [x] 校验失败阻断写入配置
78 +- [x] 增加最小覆盖单测与示例
79 +
80 +**验收标准**:
81 +- [x] 不合法配置不会写入 plan-templates
82 +- [x] 校验错误可一眼定位缺失字段
83 +- [x] 单测覆盖关键异常路径
84 +
85 +---
86 +
87 +### 第 4 步:生成与写入稳态化
88 +
89 +**目标**: 输出稳定可控、支持 diff 与回滚
90 +
91 +**文件**:
92 +- `scripts/parse-docs.js`
93 +- `src/config/plan-templates.js`
94 +
95 +**子任务**:
96 +- [x] form_sn 改为 slug + hash 的稳定规则
97 +- [x] 插入位置改为锚点块或结构化写入
98 +- [x] 增加重复 form_sn 检测与冲突提示
99 +- [x] 支持 dry-run 输出变更 diff
100 +- [x] 备份与回滚记录完善
101 +
102 +**验收标准**:
103 +- [x] 重复解析不会产生随机 form_sn
104 +- [x] 插入位置稳定可靠
105 +- [x] dry-run 能清晰展示新增/修改内容
106 +
107 +---
108 +
109 +### 第 5 步:测试与验证
110 +
111 +**目标**: 保证解析流程可回归验证
112 +
113 +**文件**:
114 +- `scripts/parse-docs.test.js`
115 +- `docs/to-parse/README.md`
116 +
117 +**子任务**:
118 +- [x] 新增 fixtures 文档样本说明
119 +- [x] 增加解析流程集成测试
120 +- [x] 补充 updateConfigContent 边界测试
121 +- [x] 运行测试并记录结果
122 +
123 +**验收标准**:
124 +- [x] 解析流程有稳定测试兜底
125 +- [x] 关键边界路径有覆盖
126 +- [x] 测试可重复运行
127 +
128 +---
129 +
130 +### 第 6 步:运营与审计
131 +
132 +**目标**: 便于长期维护与复盘
133 +
134 +**文件**:
135 +- `scripts/parse-docs.js`
136 +- `docs/to-parse/README.md`
137 +
138 +**子任务**:
139 +- [x] 输出解析摘要(成功/失败/耗时)
140 +- [x] 生成审计日志与变更摘要
141 +- [x] 更新使用说明与注意事项
142 +
143 +**验收标准**:
144 +- [x] 每次解析均可追踪结果
145 +- [x] 文档能指导新成员完成解析
146 +
147 +---
148 +
149 +## 🔍 快速跳转
150 +
151 +- [解析脚本](./../../scripts/parse-docs.js)
152 +- [解析测试](./../../scripts/parse-docs.test.js)
153 +- [待解析说明](./../../docs/to-parse/README.md)
154 +- [计划书配置](./../../src/config/plan-templates.js)
155 +- [Schema 使用文档](./../../docs/plan/plan-form-schema-usage.md)
156 +
157 +---
158 +
159 +## 📝 备注
160 +
161 +- 每完成一个子任务,就在对应的 [ ] 中打勾 ✓
162 +- 任务执行过程中的问题与结论直接补充在对应任务下
...@@ -39,6 +39,29 @@ pnpm run parse:docs:file -- --file="产品说明书.pdf" ...@@ -39,6 +39,29 @@ pnpm run parse:docs:file -- --file="产品说明书.pdf"
39 - ✅ Word (.doc, .docx) 39 - ✅ Word (.doc, .docx)
40 - ✅ 纯本文档 (.txt, .md) 40 - ✅ 纯本文档 (.txt, .md)
41 41
42 +## 🧪 Fixtures 文档样本说明
43 +
44 +用于测试的样本文档建议放在此目录,命名规则建议包含产品名与类型,便于回归验证:
45 +
46 +```
47 +docs/to-parse/
48 +├── fixtures-life-insurance-sample.pdf
49 +├── fixtures-critical-illness-sample.docx
50 +└── fixtures-savings-sample.txt
51 +```
52 +
53 +执行测试前请确认样本文档内容完整且可被抽取为文本。
54 +
55 +## 📊 解析摘要与审计日志
56 +
57 +每次解析都会输出成功/失败/耗时摘要,并在以下位置记录审计日志:
58 +
59 +```
60 +docs/parsed-backup/parse-audit.jsonl
61 +```
62 +
63 +日志包含解析汇总与本次变更摘要,便于回溯与排查。
64 +
42 ## 🔧 配置 AI 服务 65 ## 🔧 配置 AI 服务
43 66
44 脚本使用 skill 工具调用 AI 服务,支持: 67 脚本使用 skill 工具调用 AI 服务,支持:
......
...@@ -96,9 +96,12 @@ ...@@ -96,9 +96,12 @@
96 "eslint-plugin-vue": "^8.0.0", 96 "eslint-plugin-vue": "^8.0.0",
97 "happy-dom": "^14.12.0", 97 "happy-dom": "^14.12.0",
98 "husky": "^9.1.7", 98 "husky": "^9.1.7",
99 + "ajv": "^8.17.1",
99 "js-yaml": "^4.1.1", 100 "js-yaml": "^4.1.1",
100 "less": "^4.2.0", 101 "less": "^4.2.0",
101 "lint-staged": "^16.2.7", 102 "lint-staged": "^16.2.7",
103 + "mammoth": "^1.9.1",
104 + "pdf-parse": "^2.2.0",
102 "postcss": "^8.5.6", 105 "postcss": "^8.5.6",
103 "sass": "^1.78.0", 106 "sass": "^1.78.0",
104 "standard-version": "^9.5.0", 107 "standard-version": "^9.5.0",
......
...@@ -120,6 +120,9 @@ importers: ...@@ -120,6 +120,9 @@ importers:
120 '@vue/test-utils': 120 '@vue/test-utils':
121 specifier: ^2.4.6 121 specifier: ^2.4.6
122 version: 2.4.6 122 version: 2.4.6
123 + ajv:
124 + specifier: ^8.17.1
125 + version: 8.17.1
123 autoprefixer: 126 autoprefixer:
124 specifier: ^10.4.21 127 specifier: ^10.4.21
125 version: 10.4.23(postcss@8.5.6) 128 version: 10.4.23(postcss@8.5.6)
...@@ -159,6 +162,12 @@ importers: ...@@ -159,6 +162,12 @@ importers:
159 lint-staged: 162 lint-staged:
160 specifier: ^16.2.7 163 specifier: ^16.2.7
161 version: 16.2.7 164 version: 16.2.7
165 + mammoth:
166 + specifier: ^1.9.1
167 + version: 1.11.0
168 + pdf-parse:
169 + specifier: ^2.2.0
170 + version: 2.4.5
162 postcss: 171 postcss:
163 specifier: ^8.5.6 172 specifier: ^8.5.6
164 version: 8.5.6 173 version: 8.5.6
...@@ -1487,6 +1496,75 @@ packages: ...@@ -1487,6 +1496,75 @@ packages:
1487 '@leichtgewicht/ip-codec@2.0.5': 1496 '@leichtgewicht/ip-codec@2.0.5':
1488 resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} 1497 resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
1489 1498
1499 + '@napi-rs/canvas-android-arm64@0.1.80':
1500 + resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==}
1501 + engines: {node: '>= 10'}
1502 + cpu: [arm64]
1503 + os: [android]
1504 +
1505 + '@napi-rs/canvas-darwin-arm64@0.1.80':
1506 + resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==}
1507 + engines: {node: '>= 10'}
1508 + cpu: [arm64]
1509 + os: [darwin]
1510 +
1511 + '@napi-rs/canvas-darwin-x64@0.1.80':
1512 + resolution: {integrity: sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==}
1513 + engines: {node: '>= 10'}
1514 + cpu: [x64]
1515 + os: [darwin]
1516 +
1517 + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80':
1518 + resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==}
1519 + engines: {node: '>= 10'}
1520 + cpu: [arm]
1521 + os: [linux]
1522 +
1523 + '@napi-rs/canvas-linux-arm64-gnu@0.1.80':
1524 + resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==}
1525 + engines: {node: '>= 10'}
1526 + cpu: [arm64]
1527 + os: [linux]
1528 + libc: [glibc]
1529 +
1530 + '@napi-rs/canvas-linux-arm64-musl@0.1.80':
1531 + resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==}
1532 + engines: {node: '>= 10'}
1533 + cpu: [arm64]
1534 + os: [linux]
1535 + libc: [musl]
1536 +
1537 + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80':
1538 + resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==}
1539 + engines: {node: '>= 10'}
1540 + cpu: [riscv64]
1541 + os: [linux]
1542 + libc: [glibc]
1543 +
1544 + '@napi-rs/canvas-linux-x64-gnu@0.1.80':
1545 + resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==}
1546 + engines: {node: '>= 10'}
1547 + cpu: [x64]
1548 + os: [linux]
1549 + libc: [glibc]
1550 +
1551 + '@napi-rs/canvas-linux-x64-musl@0.1.80':
1552 + resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==}
1553 + engines: {node: '>= 10'}
1554 + cpu: [x64]
1555 + os: [linux]
1556 + libc: [musl]
1557 +
1558 + '@napi-rs/canvas-win32-x64-msvc@0.1.80':
1559 + resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==}
1560 + engines: {node: '>= 10'}
1561 + cpu: [x64]
1562 + os: [win32]
1563 +
1564 + '@napi-rs/canvas@0.1.80':
1565 + resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==}
1566 + engines: {node: '>= 10'}
1567 +
1490 '@napi-rs/triples@1.2.0': 1568 '@napi-rs/triples@1.2.0':
1491 resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==} 1569 resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
1492 1570
...@@ -2581,6 +2659,10 @@ packages: ...@@ -2581,6 +2659,10 @@ packages:
2581 '@webassemblyjs/wast-printer@1.14.1': 2659 '@webassemblyjs/wast-printer@1.14.1':
2582 resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} 2660 resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
2583 2661
2662 + '@xmldom/xmldom@0.8.11':
2663 + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
2664 + engines: {node: '>=10.0.0'}
2665 +
2584 '@xtuc/ieee754@1.2.0': 2666 '@xtuc/ieee754@1.2.0':
2585 resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 2667 resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
2586 2668
...@@ -2719,6 +2801,9 @@ packages: ...@@ -2719,6 +2801,9 @@ packages:
2719 arg@5.0.2: 2801 arg@5.0.2:
2720 resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 2802 resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
2721 2803
2804 + argparse@1.0.10:
2805 + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
2806 +
2722 argparse@2.0.1: 2807 argparse@2.0.1:
2723 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 2808 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
2724 2809
...@@ -2896,6 +2981,9 @@ packages: ...@@ -2896,6 +2981,9 @@ packages:
2896 bl@4.1.0: 2981 bl@4.1.0:
2897 resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} 2982 resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
2898 2983
2984 + bluebird@3.4.7:
2985 + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
2986 +
2899 body-parser@1.20.4: 2987 body-parser@1.20.4:
2900 resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} 2988 resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
2901 engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 2989 engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
...@@ -3660,6 +3748,9 @@ packages: ...@@ -3660,6 +3748,9 @@ packages:
3660 dijkstrajs@1.0.3: 3748 dijkstrajs@1.0.3:
3661 resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} 3749 resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
3662 3750
3751 + dingbat-to-unicode@1.0.1:
3752 + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
3753 +
3663 dingtalk-jsapi@2.15.6: 3754 dingtalk-jsapi@2.15.6:
3664 resolution: {integrity: sha512-804mFz2AFV/H9ysmo7dLqMjSGOQgREsgQIuep+Xg+yNQeQtnUOYntElEzlB798Sj/691e4mMKz9mtQ7v9qdjuA==} 3755 resolution: {integrity: sha512-804mFz2AFV/H9ysmo7dLqMjSGOQgREsgQIuep+Xg+yNQeQtnUOYntElEzlB798Sj/691e4mMKz9mtQ7v9qdjuA==}
3665 3756
...@@ -3738,6 +3829,9 @@ packages: ...@@ -3738,6 +3829,9 @@ packages:
3738 resolution: {integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==} 3829 resolution: {integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==}
3739 engines: {node: '>=6'} 3830 engines: {node: '>=6'}
3740 3831
3832 + duck@0.1.12:
3833 + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==}
3834 +
3741 dunder-proto@1.0.1: 3835 dunder-proto@1.0.1:
3742 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 3836 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
3743 engines: {node: '>= 0.4'} 3837 engines: {node: '>= 0.4'}
...@@ -4629,6 +4723,9 @@ packages: ...@@ -4629,6 +4723,9 @@ packages:
4629 engines: {node: '>=0.10.0'} 4723 engines: {node: '>=0.10.0'}
4630 hasBin: true 4724 hasBin: true
4631 4725
4726 + immediate@3.0.6:
4727 + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
4728 +
4632 immutable@5.1.4: 4729 immutable@5.1.4:
4633 resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} 4730 resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
4634 4731
...@@ -5015,6 +5112,9 @@ packages: ...@@ -5015,6 +5112,9 @@ packages:
5015 resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} 5112 resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
5016 engines: {node: '>=4.0'} 5113 engines: {node: '>=4.0'}
5017 5114
5115 + jszip@3.10.1:
5116 + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
5117 +
5018 keyv@3.0.0: 5118 keyv@3.0.0:
5019 resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} 5119 resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==}
5020 5120
...@@ -5066,6 +5166,9 @@ packages: ...@@ -5066,6 +5166,9 @@ packages:
5066 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 5166 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
5067 engines: {node: '>= 0.8.0'} 5167 engines: {node: '>= 0.8.0'}
5068 5168
5169 + lie@3.3.0:
5170 + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
5171 +
5069 lightningcss-android-arm64@1.30.2: 5172 lightningcss-android-arm64@1.30.2:
5070 resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} 5173 resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
5071 engines: {node: '>= 12.0.0'} 5174 engines: {node: '>= 12.0.0'}
...@@ -5324,6 +5427,9 @@ packages: ...@@ -5324,6 +5427,9 @@ packages:
5324 resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 5427 resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
5325 hasBin: true 5428 hasBin: true
5326 5429
5430 + lop@0.4.2:
5431 + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
5432 +
5327 loupe@2.3.7: 5433 loupe@2.3.7:
5328 resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} 5434 resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
5329 5435
...@@ -5370,6 +5476,11 @@ packages: ...@@ -5370,6 +5476,11 @@ packages:
5370 resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} 5476 resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
5371 engines: {node: '>=8'} 5477 engines: {node: '>=8'}
5372 5478
5479 + mammoth@1.11.0:
5480 + resolution: {integrity: sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==}
5481 + engines: {node: '>=12.0.0'}
5482 + hasBin: true
5483 +
5373 map-obj@1.0.1: 5484 map-obj@1.0.1:
5374 resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} 5485 resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
5375 engines: {node: '>=0.10.0'} 5486 engines: {node: '>=0.10.0'}
...@@ -5719,6 +5830,9 @@ packages: ...@@ -5719,6 +5830,9 @@ packages:
5719 resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} 5830 resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
5720 engines: {node: '>=12'} 5831 engines: {node: '>=12'}
5721 5832
5833 + option@0.2.4:
5834 + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
5835 +
5722 optionator@0.9.4: 5836 optionator@0.9.4:
5723 resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 5837 resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
5724 engines: {node: '>= 0.8.0'} 5838 engines: {node: '>= 0.8.0'}
...@@ -5806,6 +5920,9 @@ packages: ...@@ -5806,6 +5920,9 @@ packages:
5806 resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} 5920 resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==}
5807 engines: {node: '>=8'} 5921 engines: {node: '>=8'}
5808 5922
5923 + pako@1.0.11:
5924 + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
5925 +
5809 param-case@2.1.1: 5926 param-case@2.1.1:
5810 resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} 5927 resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
5811 5928
...@@ -5904,6 +6021,15 @@ packages: ...@@ -5904,6 +6021,15 @@ packages:
5904 pathval@1.1.1: 6021 pathval@1.1.1:
5905 resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} 6022 resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
5906 6023
6024 + pdf-parse@2.4.5:
6025 + resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
6026 + engines: {node: '>=20.16.0 <21 || >=22.3.0'}
6027 + hasBin: true
6028 +
6029 + pdfjs-dist@5.4.296:
6030 + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
6031 + engines: {node: '>=20.16.0 || >=22.3.0'}
6032 +
5907 pend@1.2.0: 6033 pend@1.2.0:
5908 resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 6034 resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
5909 6035
...@@ -6913,6 +7039,9 @@ packages: ...@@ -6913,6 +7039,9 @@ packages:
6913 resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} 7039 resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
6914 engines: {node: '>= 0.4'} 7040 engines: {node: '>= 0.4'}
6915 7041
7042 + setimmediate@1.0.5:
7043 + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
7044 +
6916 setprototypeof@1.2.0: 7045 setprototypeof@1.2.0:
6917 resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 7046 resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
6918 7047
...@@ -7046,6 +7175,9 @@ packages: ...@@ -7046,6 +7175,9 @@ packages:
7046 split@1.0.1: 7175 split@1.0.1:
7047 resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} 7176 resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
7048 7177
7178 + sprintf-js@1.0.3:
7179 + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
7180 +
7049 stackback@0.0.2: 7181 stackback@0.0.2:
7050 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 7182 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
7051 7183
...@@ -7483,6 +7615,9 @@ packages: ...@@ -7483,6 +7615,9 @@ packages:
7483 unbzip2-stream@1.4.3: 7615 unbzip2-stream@1.4.3:
7484 resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} 7616 resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
7485 7617
7618 + underscore@1.13.7:
7619 + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
7620 +
7486 undici-types@7.16.0: 7621 undici-types@7.16.0:
7487 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 7622 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
7488 7623
...@@ -7886,6 +8021,10 @@ packages: ...@@ -7886,6 +8021,10 @@ packages:
7886 resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 8021 resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
7887 engines: {node: '>=18'} 8022 engines: {node: '>=18'}
7888 8023
8024 + xmlbuilder@10.1.1:
8025 + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
8026 + engines: {node: '>=4.0'}
8027 +
7889 xmlchars@2.2.0: 8028 xmlchars@2.2.0:
7890 resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 8029 resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
7891 8030
...@@ -9313,6 +9452,49 @@ snapshots: ...@@ -9313,6 +9452,49 @@ snapshots:
9313 9452
9314 '@leichtgewicht/ip-codec@2.0.5': {} 9453 '@leichtgewicht/ip-codec@2.0.5': {}
9315 9454
9455 + '@napi-rs/canvas-android-arm64@0.1.80':
9456 + optional: true
9457 +
9458 + '@napi-rs/canvas-darwin-arm64@0.1.80':
9459 + optional: true
9460 +
9461 + '@napi-rs/canvas-darwin-x64@0.1.80':
9462 + optional: true
9463 +
9464 + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80':
9465 + optional: true
9466 +
9467 + '@napi-rs/canvas-linux-arm64-gnu@0.1.80':
9468 + optional: true
9469 +
9470 + '@napi-rs/canvas-linux-arm64-musl@0.1.80':
9471 + optional: true
9472 +
9473 + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80':
9474 + optional: true
9475 +
9476 + '@napi-rs/canvas-linux-x64-gnu@0.1.80':
9477 + optional: true
9478 +
9479 + '@napi-rs/canvas-linux-x64-musl@0.1.80':
9480 + optional: true
9481 +
9482 + '@napi-rs/canvas-win32-x64-msvc@0.1.80':
9483 + optional: true
9484 +
9485 + '@napi-rs/canvas@0.1.80':
9486 + optionalDependencies:
9487 + '@napi-rs/canvas-android-arm64': 0.1.80
9488 + '@napi-rs/canvas-darwin-arm64': 0.1.80
9489 + '@napi-rs/canvas-darwin-x64': 0.1.80
9490 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.80
9491 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.80
9492 + '@napi-rs/canvas-linux-arm64-musl': 0.1.80
9493 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.80
9494 + '@napi-rs/canvas-linux-x64-gnu': 0.1.80
9495 + '@napi-rs/canvas-linux-x64-musl': 0.1.80
9496 + '@napi-rs/canvas-win32-x64-msvc': 0.1.80
9497 +
9316 '@napi-rs/triples@1.2.0': {} 9498 '@napi-rs/triples@1.2.0': {}
9317 9499
9318 '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': 9500 '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
...@@ -10562,6 +10744,8 @@ snapshots: ...@@ -10562,6 +10744,8 @@ snapshots:
10562 '@webassemblyjs/ast': 1.14.1 10744 '@webassemblyjs/ast': 1.14.1
10563 '@xtuc/long': 4.2.2 10745 '@xtuc/long': 4.2.2
10564 10746
10747 + '@xmldom/xmldom@0.8.11': {}
10748 +
10565 '@xtuc/ieee754@1.2.0': {} 10749 '@xtuc/ieee754@1.2.0': {}
10566 10750
10567 '@xtuc/long@4.2.2': {} 10751 '@xtuc/long@4.2.2': {}
...@@ -10675,6 +10859,10 @@ snapshots: ...@@ -10675,6 +10859,10 @@ snapshots:
10675 10859
10676 arg@5.0.2: {} 10860 arg@5.0.2: {}
10677 10861
10862 + argparse@1.0.10:
10863 + dependencies:
10864 + sprintf-js: 1.0.3
10865 +
10678 argparse@2.0.1: {} 10866 argparse@2.0.1: {}
10679 10867
10680 array-buffer-byte-length@1.0.2: 10868 array-buffer-byte-length@1.0.2:
...@@ -10905,6 +11093,8 @@ snapshots: ...@@ -10905,6 +11093,8 @@ snapshots:
10905 inherits: 2.0.4 11093 inherits: 2.0.4
10906 readable-stream: 3.6.2 11094 readable-stream: 3.6.2
10907 11095
11096 + bluebird@3.4.7: {}
11097 +
10908 body-parser@1.20.4: 11098 body-parser@1.20.4:
10909 dependencies: 11099 dependencies:
10910 bytes: 3.1.2 11100 bytes: 3.1.2
...@@ -11792,6 +11982,8 @@ snapshots: ...@@ -11792,6 +11982,8 @@ snapshots:
11792 11982
11793 dijkstrajs@1.0.3: {} 11983 dijkstrajs@1.0.3: {}
11794 11984
11985 + dingbat-to-unicode@1.0.1: {}
11986 +
11795 dingtalk-jsapi@2.15.6: 11987 dingtalk-jsapi@2.15.6:
11796 dependencies: 11988 dependencies:
11797 promise-polyfill: 7.1.2 11989 promise-polyfill: 7.1.2
...@@ -11895,6 +12087,10 @@ snapshots: ...@@ -11895,6 +12087,10 @@ snapshots:
11895 p-event: 2.3.1 12087 p-event: 2.3.1
11896 pify: 3.0.0 12088 pify: 3.0.0
11897 12089
12090 + duck@0.1.12:
12091 + dependencies:
12092 + underscore: 1.13.7
12093 +
11898 dunder-proto@1.0.1: 12094 dunder-proto@1.0.1:
11899 dependencies: 12095 dependencies:
11900 call-bind-apply-helpers: 1.0.2 12096 call-bind-apply-helpers: 1.0.2
...@@ -13085,6 +13281,8 @@ snapshots: ...@@ -13085,6 +13281,8 @@ snapshots:
13085 image-size@0.5.5: 13281 image-size@0.5.5:
13086 optional: true 13282 optional: true
13087 13283
13284 + immediate@3.0.6: {}
13285 +
13088 immutable@5.1.4: {} 13286 immutable@5.1.4: {}
13089 13287
13090 import-fresh@3.3.1: 13288 import-fresh@3.3.1:
...@@ -13481,6 +13679,13 @@ snapshots: ...@@ -13481,6 +13679,13 @@ snapshots:
13481 object.assign: 4.1.7 13679 object.assign: 4.1.7
13482 object.values: 1.2.1 13680 object.values: 1.2.1
13483 13681
13682 + jszip@3.10.1:
13683 + dependencies:
13684 + lie: 3.3.0
13685 + pako: 1.0.11
13686 + readable-stream: 2.3.8
13687 + setimmediate: 1.0.5
13688 +
13484 keyv@3.0.0: 13689 keyv@3.0.0:
13485 dependencies: 13690 dependencies:
13486 json-buffer: 3.0.0 13691 json-buffer: 3.0.0
...@@ -13544,6 +13749,10 @@ snapshots: ...@@ -13544,6 +13749,10 @@ snapshots:
13544 prelude-ls: 1.2.1 13749 prelude-ls: 1.2.1
13545 type-check: 0.4.0 13750 type-check: 0.4.0
13546 13751
13752 + lie@3.3.0:
13753 + dependencies:
13754 + immediate: 3.0.6
13755 +
13547 lightningcss-android-arm64@1.30.2: 13756 lightningcss-android-arm64@1.30.2:
13548 optional: true 13757 optional: true
13549 13758
...@@ -13758,6 +13967,12 @@ snapshots: ...@@ -13758,6 +13967,12 @@ snapshots:
13758 dependencies: 13967 dependencies:
13759 js-tokens: 4.0.0 13968 js-tokens: 4.0.0
13760 13969
13970 + lop@0.4.2:
13971 + dependencies:
13972 + duck: 0.1.12
13973 + option: 0.2.4
13974 + underscore: 1.13.7
13975 +
13761 loupe@2.3.7: 13976 loupe@2.3.7:
13762 dependencies: 13977 dependencies:
13763 get-func-name: 2.0.2 13978 get-func-name: 2.0.2
...@@ -13801,6 +14016,19 @@ snapshots: ...@@ -13801,6 +14016,19 @@ snapshots:
13801 dependencies: 14016 dependencies:
13802 semver: 6.3.1 14017 semver: 6.3.1
13803 14018
14019 + mammoth@1.11.0:
14020 + dependencies:
14021 + '@xmldom/xmldom': 0.8.11
14022 + argparse: 1.0.10
14023 + base64-js: 1.5.1
14024 + bluebird: 3.4.7
14025 + dingbat-to-unicode: 1.0.1
14026 + jszip: 3.10.1
14027 + lop: 0.4.2
14028 + path-is-absolute: 1.0.1
14029 + underscore: 1.13.7
14030 + xmlbuilder: 10.1.1
14031 +
13804 map-obj@1.0.1: {} 14032 map-obj@1.0.1: {}
13805 14033
13806 map-obj@4.3.0: {} 14034 map-obj@4.3.0: {}
...@@ -14134,6 +14362,8 @@ snapshots: ...@@ -14134,6 +14362,8 @@ snapshots:
14134 is-docker: 2.2.1 14362 is-docker: 2.2.1
14135 is-wsl: 2.2.0 14363 is-wsl: 2.2.0
14136 14364
14365 + option@0.2.4: {}
14366 +
14137 optionator@0.9.4: 14367 optionator@0.9.4:
14138 dependencies: 14368 dependencies:
14139 deep-is: 0.1.4 14369 deep-is: 0.1.4
...@@ -14227,6 +14457,8 @@ snapshots: ...@@ -14227,6 +14457,8 @@ snapshots:
14227 registry-url: 5.1.0 14457 registry-url: 5.1.0
14228 semver: 6.3.1 14458 semver: 6.3.1
14229 14459
14460 + pako@1.0.11: {}
14461 +
14230 param-case@2.1.1: 14462 param-case@2.1.1:
14231 dependencies: 14463 dependencies:
14232 no-case: 2.3.2 14464 no-case: 2.3.2
...@@ -14313,6 +14545,15 @@ snapshots: ...@@ -14313,6 +14545,15 @@ snapshots:
14313 14545
14314 pathval@1.1.1: {} 14546 pathval@1.1.1: {}
14315 14547
14548 + pdf-parse@2.4.5:
14549 + dependencies:
14550 + '@napi-rs/canvas': 0.1.80
14551 + pdfjs-dist: 5.4.296
14552 +
14553 + pdfjs-dist@5.4.296:
14554 + optionalDependencies:
14555 + '@napi-rs/canvas': 0.1.80
14556 +
14316 pend@1.2.0: {} 14557 pend@1.2.0: {}
14317 14558
14318 perfect-debounce@1.0.0: {} 14559 perfect-debounce@1.0.0: {}
...@@ -15423,6 +15664,8 @@ snapshots: ...@@ -15423,6 +15664,8 @@ snapshots:
15423 es-errors: 1.3.0 15664 es-errors: 1.3.0
15424 es-object-atoms: 1.1.1 15665 es-object-atoms: 1.1.1
15425 15666
15667 + setimmediate@1.0.5: {}
15668 +
15426 setprototypeof@1.2.0: {} 15669 setprototypeof@1.2.0: {}
15427 15670
15428 shallow-clone@3.0.1: 15671 shallow-clone@3.0.1:
...@@ -15571,6 +15814,8 @@ snapshots: ...@@ -15571,6 +15814,8 @@ snapshots:
15571 dependencies: 15814 dependencies:
15572 through: 2.3.8 15815 through: 2.3.8
15573 15816
15817 + sprintf-js@1.0.3: {}
15818 +
15574 stackback@0.0.2: {} 15819 stackback@0.0.2: {}
15575 15820
15576 standard-version@9.5.0: 15821 standard-version@9.5.0:
...@@ -16064,6 +16309,8 @@ snapshots: ...@@ -16064,6 +16309,8 @@ snapshots:
16064 buffer: 5.7.1 16309 buffer: 5.7.1
16065 through: 2.3.8 16310 through: 2.3.8
16066 16311
16312 + underscore@1.13.7: {}
16313 +
16067 undici-types@7.16.0: {} 16314 undici-types@7.16.0: {}
16068 16315
16069 unescape-js@1.1.4: 16316 unescape-js@1.1.4:
...@@ -16563,6 +16810,8 @@ snapshots: ...@@ -16563,6 +16810,8 @@ snapshots:
16563 16810
16564 xml-name-validator@5.0.0: {} 16811 xml-name-validator@5.0.0: {}
16565 16812
16813 + xmlbuilder@10.1.1: {}
16814 +
16566 xmlchars@2.2.0: {} 16815 xmlchars@2.2.0: {}
16567 16816
16568 xst-solar2lunar@2.1.0: 16817 xst-solar2lunar@2.1.0:
......
...@@ -16,8 +16,12 @@ ...@@ -16,8 +16,12 @@
16 * # 查看待处理文档 16 * # 查看待处理文档
17 * npm run parse:docs -- --list 17 * npm run parse:docs -- --list
18 */ 18 */
19 +import crypto from 'crypto'
19 import fs from 'fs' 20 import fs from 'fs'
20 import path from 'path' 21 import path from 'path'
22 +import { PDFParse } from 'pdf-parse'
23 +import mammoth from 'mammoth'
24 +import Ajv from 'ajv'
21 25
22 // ========== 配置区 ========== 26 // ========== 配置区 ==========
23 27
...@@ -31,6 +35,21 @@ const SUPPORTED_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.md'] ...@@ -31,6 +35,21 @@ const SUPPORTED_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.md']
31 // AI 解析服务选择(通过 skill 调用) 35 // AI 解析服务选择(通过 skill 调用)
32 const AI_SERVICE = 'openai' // 'openai' | 'anthropic' | 'openrouter' 36 const AI_SERVICE = 'openai' // 'openai' | 'anthropic' | 'openrouter'
33 37
38 +const ajv = new Ajv({ allErrors: true, strict: false })
39 +const parseConfigSchema = {
40 + type: 'object',
41 + required: ['product_name', 'product_type', 'currency', 'form_schema', 'submit_mapping'],
42 + properties: {
43 + product_name: { type: 'string', minLength: 1 },
44 + product_type: { type: 'string', enum: ['savings', 'life-insurance', 'critical-illness'] },
45 + currency: { type: 'string', minLength: 1 },
46 + form_schema: { type: 'object' },
47 + submit_mapping: { type: 'object' }
48 + },
49 + additionalProperties: true
50 +}
51 +const validateParsedConfigSchema = ajv.compile(parseConfigSchema)
52 +
34 // ========== 工具函数 ========== 53 // ========== 工具函数 ==========
35 54
36 /** 55 /**
...@@ -50,6 +69,105 @@ function readFile(filePath) { ...@@ -50,6 +69,105 @@ function readFile(filePath) {
50 return fs.readFileSync(filePath, 'utf-8') 69 return fs.readFileSync(filePath, 'utf-8')
51 } 70 }
52 71
72 +function getFileMeta(filePath, extraMeta = {}) {
73 + const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : { size: 0 }
74 + return {
75 + file_name: path.basename(filePath),
76 + ext: path.extname(filePath).toLowerCase(),
77 + size: stats.size,
78 + ocr: {
79 + enabled: false,
80 + provider: null,
81 + reason: 'not_configured'
82 + },
83 + ...extraMeta
84 + }
85 +}
86 +
87 +function buildExtractResult(filePath, text, warnings = [], extraMeta = {}) {
88 + return {
89 + text,
90 + warnings,
91 + meta: getFileMeta(filePath, extraMeta)
92 + }
93 +}
94 +
95 +async function extractTextFromPdf(filePath) {
96 + const buffer = fs.readFileSync(filePath)
97 + const parser = new PDFParse({ data: buffer })
98 + let result
99 + try {
100 + result = await parser.getText()
101 + } finally {
102 + await parser.destroy()
103 + }
104 + return buildExtractResult(filePath, result?.text || '', [], {
105 + total_pages: result?.total || 0
106 + })
107 +}
108 +
109 +async function extractTextFromDocx(filePath) {
110 + const buffer = fs.readFileSync(filePath)
111 + const result = await mammoth.extractRawText({ buffer })
112 + const warnings = (result.messages || []).map(item => `${item.type || 'warning'}:${item.message}`)
113 + return buildExtractResult(filePath, result.value || '', warnings)
114 +}
115 +
116 +function extractTextFromDoc(filePath) {
117 + return buildExtractResult(filePath, '', ['暂不支持 .doc,请转换为 .docx'])
118 +}
119 +
120 +function extractTextFromPlainFile(filePath) {
121 + return buildExtractResult(filePath, readFile(filePath), [])
122 +}
123 +
124 +export async function extractDocumentText(filePath) {
125 + const ext = path.extname(filePath).toLowerCase()
126 + let result
127 +
128 + if (ext === '.pdf') {
129 + result = await extractTextFromPdf(filePath)
130 + } else if (ext === '.docx') {
131 + result = await extractTextFromDocx(filePath)
132 + } else if (ext === '.doc') {
133 + result = extractTextFromDoc(filePath)
134 + } else if (ext === '.txt' || ext === '.md') {
135 + result = extractTextFromPlainFile(filePath)
136 + } else {
137 + result = buildExtractResult(filePath, '', [`不支持的文件类型: ${ext}`])
138 + }
139 +
140 + if (!result.text || !result.text.trim()) {
141 + result.warnings.push('抽取文本为空,可能是扫描件')
142 + result.meta.ocr = {
143 + enabled: false,
144 + provider: null,
145 + reason: 'text_empty'
146 + }
147 + }
148 +
149 + return result
150 +}
151 +
152 +export function validateParsedConfig(config) {
153 + const valid = validateParsedConfigSchema(config)
154 + if (valid) {
155 + return { valid: true, errors: [] }
156 + }
157 +
158 + const errors = (validateParsedConfigSchema.errors || []).map(error => {
159 + if (error.keyword === 'required' && error.params?.missingProperty) {
160 + return `${error.instancePath || '/'} 缺少字段 ${error.params.missingProperty}`
161 + }
162 + if (error.message) {
163 + return `${error.instancePath || '/'} ${error.message}`.trim()
164 + }
165 + return `${error.instancePath || '/'} 校验失败`
166 + })
167 +
168 + return { valid: false, errors }
169 +}
170 +
53 /** 171 /**
54 * 写入文件内容 172 * 写入文件内容
55 */ 173 */
...@@ -81,14 +199,20 @@ function getDocsToParse() { ...@@ -81,14 +199,20 @@ function getDocsToParse() {
81 * 生成 form_sn 199 * 生成 form_sn
82 */ 200 */
83 export function generateFormSn(config) { 201 export function generateFormSn(config) {
202 + if (config?.form_sn) {
203 + return config.form_sn
204 + }
205 +
84 const product_type = config?.product_type || 'product' 206 const product_type = config?.product_type || 'product'
85 - const timestamp = Date.now().toString(36) 207 + const raw_name = (config?.product_name || '').trim()
86 - const name_slug = (config?.product_name || '') 208 + const name_slug = raw_name
87 .toLowerCase() 209 .toLowerCase()
88 .replace(/[^a-z0-9]+/g, '-') 210 .replace(/[^a-z0-9]+/g, '-')
89 .replace(/^-+|-+$/g, '') 211 .replace(/^-+|-+$/g, '')
212 + const base_value = `${product_type}|${name_slug || 'product'}|${raw_name}`
213 + const hash = crypto.createHash('sha1').update(base_value).digest('hex').slice(0, 8)
90 214
91 - return `${product_type}-${name_slug || 'product'}-${timestamp}` 215 + return `${product_type}-${name_slug || 'product'}-${hash}`
92 } 216 }
93 217
94 /** 218 /**
...@@ -126,12 +250,28 @@ export function generateConfigCode(config) { ...@@ -126,12 +250,28 @@ export function generateConfigCode(config) {
126 code += " default_currency: '" + config.currency + "',\n" 250 code += " default_currency: '" + config.currency + "',\n"
127 code += " withdrawal_modes: " + JSON.stringify(config.withdrawal_modes || []) + ",\n" 251 code += " withdrawal_modes: " + JSON.stringify(config.withdrawal_modes || []) + ",\n"
128 code += " withdrawal_periods: " + JSON.stringify(config.withdrawal_periods || []) + "\n" 252 code += " withdrawal_periods: " + JSON.stringify(config.withdrawal_periods || []) + "\n"
129 - code += " }\n" 253 + code += " },\n"
254 + if (config.form_schema) {
255 + const form_schema_code = JSON.stringify(config.form_schema, null, 2).replace(/\n/g, '\n ')
256 + code += " form_schema: " + form_schema_code + ",\n"
257 + }
258 + if (config.submit_mapping) {
259 + const submit_mapping_code = JSON.stringify(config.submit_mapping, null, 2).replace(/\n/g, '\n ')
260 + code += " submit_mapping: " + submit_mapping_code + "\n"
261 + }
130 } else { 262 } else {
131 code += " currency: '" + config.currency + "',\n" 263 code += " currency: '" + config.currency + "',\n"
132 code += " payment_periods: " + JSON.stringify(config.payment_periods || []) + ",\n" 264 code += " payment_periods: " + JSON.stringify(config.payment_periods || []) + ",\n"
133 code += " age_range: { min: " + (config.age_range?.min || 0) + ", max: " + (config.age_range?.max || 75) + " },\n" 265 code += " age_range: { min: " + (config.age_range?.min || 0) + ", max: " + (config.age_range?.max || 75) + " },\n"
134 - code += " insurance_period: '" + (config.insurance_period || '终身') + "'\n" 266 + code += " insurance_period: '" + (config.insurance_period || '终身') + "',\n"
267 + if (config.form_schema) {
268 + const form_schema_code = JSON.stringify(config.form_schema, null, 2).replace(/\n/g, '\n ')
269 + code += " form_schema: " + form_schema_code + ",\n"
270 + }
271 + if (config.submit_mapping) {
272 + const submit_mapping_code = JSON.stringify(config.submit_mapping, null, 2).replace(/\n/g, '\n ')
273 + code += " submit_mapping: " + submit_mapping_code + "\n"
274 + }
135 } 275 }
136 276
137 code += " }\n" 277 code += " }\n"
...@@ -157,8 +297,20 @@ async function parseDocumentWithAI(docPath) { ...@@ -157,8 +297,20 @@ async function parseDocumentWithAI(docPath) {
157 console.log(`\n🤖 正在解析: ${path.basename(docPath)}`) 297 console.log(`\n🤖 正在解析: ${path.basename(docPath)}`)
158 298
159 try { 299 try {
160 - // 读取文档内容 300 + const extract_result = await extractDocumentText(docPath)
161 - const content = fs.readFileSync(docPath, 'utf-8') 301 +
302 + if (extract_result.warnings.length > 0) {
303 + extract_result.warnings.forEach(message => {
304 + console.log(`⚠️ 抽取警告: ${message}`)
305 + })
306 + }
307 +
308 + if (!extract_result.text || !extract_result.text.trim()) {
309 + console.error(`❌ 文本抽取失败 (${docPath})`)
310 + return null
311 + }
312 +
313 + const content = extract_result.text
162 314
163 // 模拟解析:从文档内容中提取配置 315 // 模拟解析:从文档内容中提取配置
164 // 实际使用时可以调用 AI 服务 316 // 实际使用时可以调用 AI 服务
...@@ -171,7 +323,9 @@ async function parseDocumentWithAI(docPath) { ...@@ -171,7 +323,9 @@ async function parseDocumentWithAI(docPath) {
171 insurance_period: '终身', 323 insurance_period: '终身',
172 is_savings: true, 324 is_savings: true,
173 withdrawal_modes: ['年龄指定金额', '最高固定金额'], 325 withdrawal_modes: ['年龄指定金额', '最高固定金额'],
174 - withdrawal_periods: ['1年', '3年', '5年', '10年'] 326 + withdrawal_periods: ['1年', '3年', '5年', '10年'],
327 + form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
328 + submit_mapping: {}
175 } 329 }
176 330
177 console.log('✅ 解析成功') 331 console.log('✅ 解析成功')
...@@ -196,7 +350,16 @@ async function parseSingleFile(filePath) { ...@@ -196,7 +350,16 @@ async function parseSingleFile(filePath) {
196 350
197 if (!config) { 351 if (!config) {
198 console.log("⏭️ 跳过文件: " + fileName + " (解析失败)") 352 console.log("⏭️ 跳过文件: " + fileName + " (解析失败)")
199 - return { success: false, file: fileName } 353 + return { success: false, file: fileName, reason: 'parse_failed' }
354 + }
355 +
356 + const validation = validateParsedConfig(config)
357 + if (!validation.valid) {
358 + console.error("❌ 校验失败: " + fileName)
359 + validation.errors.forEach(message => {
360 + console.error(" - " + message)
361 + })
362 + return { success: false, file: fileName, reason: 'validation_failed', errors: validation.errors }
200 } 363 }
201 364
202 // 添加源文件信息 365 // 添加源文件信息
...@@ -216,11 +379,8 @@ async function parseSingleFile(filePath) { ...@@ -216,11 +379,8 @@ async function parseSingleFile(filePath) {
216 * @description 使用简单的字符串搜索找到正确的插入位置 379 * @description 使用简单的字符串搜索找到正确的插入位置
217 */ 380 */
218 export function updateConfigContent(existingContent, newConfigs) { 381 export function updateConfigContent(existingContent, newConfigs) {
219 - const templatesStart = existingContent.indexOf('export const PLAN_TEMPLATES') 382 + const range = getPlanTemplatesRange(existingContent)
220 - const templatesEndMarker = '\n}\n\nexport const FEATURE_FLAGS' 383 + if (!range) {
221 - const templatesEnd = existingContent.indexOf(templatesEndMarker, templatesStart)
222 -
223 - if (templatesStart === -1 || templatesEnd === -1) {
224 return null 384 return null
225 } 385 }
226 386
...@@ -229,8 +389,8 @@ export function updateConfigContent(existingContent, newConfigs) { ...@@ -229,8 +389,8 @@ export function updateConfigContent(existingContent, newConfigs) {
229 return index === newConfigs.length - 1 ? code : code + ',' 389 return index === newConfigs.length - 1 ? code : code + ','
230 }).join('\n\n') 390 }).join('\n\n')
231 391
232 - const before = existingContent.substring(0, templatesEnd) 392 + const before = existingContent.substring(0, range.endIndex)
233 - const after = existingContent.substring(templatesEnd) 393 + const after = existingContent.substring(range.endIndex)
234 const beforeTrimmed = before.replace(/\s+$/, '') 394 const beforeTrimmed = before.replace(/\s+$/, '')
235 const needsComma = !beforeTrimmed.endsWith(',') 395 const needsComma = !beforeTrimmed.endsWith(',')
236 const comma = needsComma ? ',' : '' 396 const comma = needsComma ? ',' : ''
...@@ -238,40 +398,389 @@ export function updateConfigContent(existingContent, newConfigs) { ...@@ -238,40 +398,389 @@ export function updateConfigContent(existingContent, newConfigs) {
238 return `${beforeTrimmed}${comma}\n\n${insertContent}${after}` 398 return `${beforeTrimmed}${comma}\n\n${insertContent}${after}`
239 } 399 }
240 400
241 -function updateConfigFile(newConfigs) { 401 +function getPlanTemplatesRange(content) {
402 + const startToken = 'export const PLAN_TEMPLATES = {'
403 + const startIndex = content.indexOf(startToken)
404 + if (startIndex === -1) {
405 + return null
406 + }
407 +
408 + const openIndex = startIndex + startToken.length - 1
409 + let depth = 1
410 + let inSingle = false
411 + let inDouble = false
412 + let inTemplate = false
413 + let escape = false
414 +
415 + for (let i = openIndex + 1; i < content.length; i += 1) {
416 + const ch = content[i]
417 + if (escape) {
418 + escape = false
419 + continue
420 + }
421 + if (ch === '\\') {
422 + if (inSingle || inDouble || inTemplate) {
423 + escape = true
424 + }
425 + continue
426 + }
427 + if (inSingle) {
428 + if (ch === "'") {
429 + inSingle = false
430 + }
431 + continue
432 + }
433 + if (inDouble) {
434 + if (ch === '"') {
435 + inDouble = false
436 + }
437 + continue
438 + }
439 + if (inTemplate) {
440 + if (ch === '`') {
441 + inTemplate = false
442 + }
443 + continue
444 + }
445 + if (ch === "'") {
446 + inSingle = true
447 + continue
448 + }
449 + if (ch === '"') {
450 + inDouble = true
451 + continue
452 + }
453 + if (ch === '`') {
454 + inTemplate = true
455 + continue
456 + }
457 + if (ch === '{') {
458 + depth += 1
459 + continue
460 + }
461 + if (ch === '}') {
462 + depth -= 1
463 + if (depth === 0) {
464 + return { startIndex, endIndex: i }
465 + }
466 + }
467 + }
468 +
469 + return null
470 +}
471 +
472 +function readQuotedKey(content, startIndex) {
473 + const quote = content[startIndex]
474 + let value = ''
475 + let escape = false
476 + for (let i = startIndex + 1; i < content.length; i += 1) {
477 + const ch = content[i]
478 + if (escape) {
479 + value += ch
480 + escape = false
481 + continue
482 + }
483 + if (ch === '\\') {
484 + escape = true
485 + continue
486 + }
487 + if (ch === quote) {
488 + return { value, endIndex: i }
489 + }
490 + value += ch
491 + }
492 + return null
493 +}
494 +
495 +function extractPlanTemplateKeys(content) {
496 + const range = getPlanTemplatesRange(content)
497 + if (!range) {
498 + return []
499 + }
500 + const block = content.slice(range.startIndex, range.endIndex + 1)
501 + const blockStart = block.indexOf('{') + 1
502 + const blockContent = block.slice(blockStart, block.length - 1)
503 +
504 + const keys = []
505 + let depth = 0
506 + let inSingle = false
507 + let inDouble = false
508 + let inTemplate = false
509 + let escape = false
510 +
511 + for (let i = 0; i < blockContent.length; i += 1) {
512 + const ch = blockContent[i]
513 + if (escape) {
514 + escape = false
515 + continue
516 + }
517 + if (ch === '\\') {
518 + if (inSingle || inDouble || inTemplate) {
519 + escape = true
520 + }
521 + continue
522 + }
523 + if (inSingle) {
524 + if (ch === "'") {
525 + inSingle = false
526 + }
527 + continue
528 + }
529 + if (inDouble) {
530 + if (ch === '"') {
531 + inDouble = false
532 + }
533 + continue
534 + }
535 + if (inTemplate) {
536 + if (ch === '`') {
537 + inTemplate = false
538 + }
539 + continue
540 + }
541 + if (ch === "'") {
542 + inSingle = true
543 + if (depth === 0) {
544 + const keyResult = readQuotedKey(blockContent, i)
545 + if (keyResult) {
546 + const nextIndex = keyResult.endIndex + 1
547 + const rest = blockContent.slice(nextIndex)
548 + const match = rest.match(/^\s*:/)
549 + if (match) {
550 + keys.push(keyResult.value)
551 + }
552 + i = keyResult.endIndex
553 + inSingle = false
554 + }
555 + }
556 + continue
557 + }
558 + if (ch === '"') {
559 + inDouble = true
560 + if (depth === 0) {
561 + const keyResult = readQuotedKey(blockContent, i)
562 + if (keyResult) {
563 + const nextIndex = keyResult.endIndex + 1
564 + const rest = blockContent.slice(nextIndex)
565 + const match = rest.match(/^\s*:/)
566 + if (match) {
567 + keys.push(keyResult.value)
568 + }
569 + i = keyResult.endIndex
570 + inDouble = false
571 + }
572 + }
573 + continue
574 + }
575 + if (ch === '`') {
576 + inTemplate = true
577 + continue
578 + }
579 + if (ch === '{') {
580 + depth += 1
581 + continue
582 + }
583 + if (ch === '}') {
584 + depth -= 1
585 + }
586 + }
587 +
588 + return keys
589 +}
590 +
591 +export function detectFormSnConflicts(existingContent, newConfigs) {
592 + const existingKeys = extractPlanTemplateKeys(existingContent)
593 + const existingSet = new Set(existingKeys)
594 + const conflicts = []
595 + newConfigs.forEach(item => {
596 + if (existingSet.has(item.formSn)) {
597 + conflicts.push(item.formSn)
598 + }
599 + })
600 + return conflicts
601 +}
602 +
603 +export function buildDryRunDiff(newConfigs) {
604 + const insertContent = newConfigs.map((item, index) => {
605 + const code = item.code.trimEnd()
606 + return index === newConfigs.length - 1 ? code : code + ','
607 + }).join('\n\n')
608 + const lines = insertContent.split('\n').map(line => `+ ${line}`)
609 + return ['--- plan-templates.js', '+++ plan-templates.js', ...lines].join('\n')
610 +}
611 +
612 +export function buildConfigUpdateResult(existingContent, newConfigs, options = {}) {
613 + const conflicts = detectFormSnConflicts(existingContent, newConfigs)
614 + if (conflicts.length > 0) {
615 + return { ok: false, conflicts, updatedContent: null, diff: null }
616 + }
617 +
618 + const updatedContent = updateConfigContent(existingContent, newConfigs)
619 + if (!updatedContent) {
620 + return { ok: false, conflicts: [], updatedContent: null, diff: null }
621 + }
622 +
623 + const diff = options.dry_run ? buildDryRunDiff(newConfigs) : null
624 + return { ok: true, conflicts: [], updatedContent, diff }
625 +}
626 +
627 +export function buildParseSummary(results, duration_ms) {
628 + const summary = {
629 + total: results.length,
630 + success: 0,
631 + failed: 0,
632 + duration_ms,
633 + success_list: [],
634 + failed_list: []
635 + }
636 +
637 + results.forEach(result => {
638 + if (result.success) {
639 + summary.success += 1
640 + summary.success_list.push({
641 + form_sn: result.formSn,
642 + product_name: result.config?.product_name,
643 + file: result.file
644 + })
645 + } else {
646 + summary.failed += 1
647 + summary.failed_list.push({
648 + file: result.file,
649 + reason: result.reason || 'unknown',
650 + errors: result.errors || []
651 + })
652 + }
653 + })
654 +
655 + return summary
656 +}
657 +
658 +function buildChangeSummary(update_result) {
659 + if (!update_result) {
660 + return null
661 + }
662 + const summary = {
663 + ok: update_result.ok,
664 + dry_run: update_result.dry_run || false,
665 + updated_count: update_result.updated_count || 0,
666 + form_sn_list: update_result.form_sn_list || [],
667 + conflicts: update_result.conflicts || [],
668 + reason: update_result.reason || null
669 + }
670 +
671 + if (update_result.diff) {
672 + summary.diff_preview = update_result.diff.split('\n').slice(0, 60).join('\n')
673 + }
674 +
675 + return summary
676 +}
677 +
678 +function buildAuditRecord(summary, options = {}, update_result = null, mode = 'batch') {
679 + return {
680 + at: new Date().toISOString(),
681 + mode,
682 + options: {
683 + dry_run: !!options.dry_run
684 + },
685 + summary,
686 + change_summary: buildChangeSummary(update_result)
687 + }
688 +}
689 +
690 +function writeBackupLog(record) {
691 + ensureDir(BACKUP_DIR)
692 + const logFile = path.join(BACKUP_DIR, 'backup-log.jsonl')
693 + const line = JSON.stringify(record)
694 + fs.appendFileSync(logFile, `${line}\n`, 'utf-8')
695 +}
696 +
697 +function writeAuditLog(record) {
698 + ensureDir(BACKUP_DIR)
699 + const logFile = path.join(BACKUP_DIR, 'parse-audit.jsonl')
700 + const line = JSON.stringify(record)
701 + fs.appendFileSync(logFile, `${line}\n`, 'utf-8')
702 +}
703 +
704 +function rollbackConfigFile(backupFile) {
705 + if (!backupFile || !fs.existsSync(backupFile)) {
706 + console.error("❌ 找不到备份文件: " + backupFile)
707 + return false
708 + }
709 + fs.copyFileSync(backupFile, CONFIG_FILE)
710 + writeBackupLog({
711 + action: 'rollback',
712 + backup_file: backupFile,
713 + target_file: CONFIG_FILE,
714 + at: new Date().toISOString()
715 + })
716 + console.log("✅ 已回滚配置文件: " + backupFile)
717 + return true
718 +}
719 +
720 +function updateConfigFile(newConfigs, options = {}) {
242 console.log("\n" + "=".repeat(60)) 721 console.log("\n" + "=".repeat(60))
243 console.log("📝 更新配置文件: " + CONFIG_FILE) 722 console.log("📝 更新配置文件: " + CONFIG_FILE)
244 console.log("=".repeat(60)) 723 console.log("=".repeat(60))
245 724
246 - // 备份现有配置 725 + const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')
726 + const updateResult = buildConfigUpdateResult(existingContent, newConfigs, options)
727 + if (!updateResult.ok && updateResult.conflicts.length > 0) {
728 + console.error("❌ 检测到重复 form_sn: " + updateResult.conflicts.join(', '))
729 + return { ok: false, reason: 'conflict', conflicts: updateResult.conflicts }
730 + }
731 +
732 + if (!updateResult.ok) {
733 + console.error('❌ 无法定位 PLAN_TEMPLATES 插入位置')
734 + return { ok: false, reason: 'insert_not_found', conflicts: [] }
735 + }
736 +
737 + if (options.dry_run) {
738 + console.log("\n🧪 dry-run 变更预览:\n" + updateResult.diff)
739 + return {
740 + ok: true,
741 + dry_run: true,
742 + diff: updateResult.diff,
743 + form_sn_list: newConfigs.map(item => item.formSn),
744 + updated_count: newConfigs.length
745 + }
746 + }
747 +
748 + let backupFile = null
247 if (fs.existsSync(CONFIG_FILE)) { 749 if (fs.existsSync(CONFIG_FILE)) {
248 ensureDir(BACKUP_DIR) 750 ensureDir(BACKUP_DIR)
249 - const backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`) 751 + backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
250 fs.copyFileSync(CONFIG_FILE, backupFile) 752 fs.copyFileSync(CONFIG_FILE, backupFile)
251 console.log("💾 已备份到: " + backupFile) 753 console.log("💾 已备份到: " + backupFile)
252 } 754 }
253 755
254 - const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8') 756 + writeFile(CONFIG_FILE, updateResult.updatedContent)
255 - const updatedContent = updateConfigContent(existingContent, newConfigs) 757 + writeBackupLog({
256 - 758 + action: 'update',
257 - if (!updatedContent) { 759 + backup_file: backupFile,
258 - console.error('❌ 无法定位 PLAN_TEMPLATES 插入位置') 760 + target_file: CONFIG_FILE,
259 - return 761 + form_sn_list: newConfigs.map(item => item.formSn),
260 - } 762 + at: new Date().toISOString()
261 - 763 + })
262 - writeFile(CONFIG_FILE, updatedContent)
263 console.log("✅ 已更新配置文件,新增 " + newConfigs.length + " 个产品") 764 console.log("✅ 已更新配置文件,新增 " + newConfigs.length + " 个产品")
765 + return {
766 + ok: true,
767 + dry_run: false,
768 + backup_file: backupFile,
769 + form_sn_list: newConfigs.map(item => item.formSn),
770 + updated_count: newConfigs.length
771 + }
264 } 772 }
265 773
266 /** 774 /**
267 * 处理所有文档 775 * 处理所有文档
268 */ 776 */
269 -async function parseAllDocs(docs) { 777 +async function parseAllDocs(docs, options = {}) {
270 if (docs.length === 0) { 778 if (docs.length === 0) {
271 console.log('📭 没有待处理的文档') 779 console.log('📭 没有待处理的文档')
272 return 780 return
273 } 781 }
274 782
783 + const start_time = Date.now()
275 console.log("\n" + "=".repeat(60)) 784 console.log("\n" + "=".repeat(60))
276 console.log("📚 发现 " + docs.length + " 个待处理文档") 785 console.log("📚 发现 " + docs.length + " 个待处理文档")
277 console.log("=".repeat(60)) 786 console.log("=".repeat(60))
...@@ -294,6 +803,8 @@ async function parseAllDocs(docs) { ...@@ -294,6 +803,8 @@ async function parseAllDocs(docs) {
294 console.log("总计: " + docs.length + " 个文档") 803 console.log("总计: " + docs.length + " 个文档")
295 console.log("成功: " + successResults.length + " 个") 804 console.log("成功: " + successResults.length + " 个")
296 console.log("失败: " + (results.length - successResults.length) + " 个") 805 console.log("失败: " + (results.length - successResults.length) + " 个")
806 + const summary = buildParseSummary(results, Date.now() - start_time)
807 + console.log("耗时: " + summary.duration_ms + "ms")
297 808
298 // 显示成功的产品 809 // 显示成功的产品
299 if (successResults.length > 0) { 810 if (successResults.length > 0) {
...@@ -302,13 +813,22 @@ async function parseAllDocs(docs) { ...@@ -302,13 +813,22 @@ async function parseAllDocs(docs) {
302 console.log(" - " + r.formSn + ": " + r.config.product_name) 813 console.log(" - " + r.formSn + ": " + r.config.product_name)
303 }) 814 })
304 } 815 }
816 + if (summary.failed_list.length > 0) {
817 + console.log("\n⚠️ 失败明细:")
818 + summary.failed_list.forEach(item => {
819 + console.log(" - " + item.file + " (" + item.reason + ")")
820 + })
821 + }
305 822
306 // 更新配置文件 823 // 更新配置文件
824 + let update_result = null
307 if (successResults.length > 0) { 825 if (successResults.length > 0) {
308 - updateConfigFile(successResults) 826 + update_result = updateConfigFile(successResults, options)
309 } else { 827 } else {
310 console.log("\n❌ 没有成功解析的文档,配置文件未更新") 828 console.log("\n❌ 没有成功解析的文档,配置文件未更新")
311 } 829 }
830 + const audit_record = buildAuditRecord(summary, options, update_result, 'batch')
831 + writeAuditLog(audit_record)
312 } 832 }
313 833
314 /** 834 /**
...@@ -321,12 +841,17 @@ async function main() { ...@@ -321,12 +841,17 @@ async function main() {
321 // 检查模式 841 // 检查模式
322 const listMode = args.includes('--list') 842 const listMode = args.includes('--list')
323 const fileMode = args.find(arg => arg.startsWith('--file=')) 843 const fileMode = args.find(arg => arg.startsWith('--file='))
844 + const dryRunMode = args.includes('--dry-run')
845 + const rollbackMode = args.find(arg => arg.startsWith('--rollback='))
324 846
325 console.log('\n🚀 文档解析工具') 847 console.log('\n🚀 文档解析工具')
326 console.log(" 文档目录: " + DOCS_DIR) 848 console.log(" 文档目录: " + DOCS_DIR)
327 console.log(" 配置文件: " + CONFIG_FILE) 849 console.log(" 配置文件: " + CONFIG_FILE)
328 850
329 - if (listMode) { 851 + if (rollbackMode) {
852 + const backupFile = rollbackMode.split('=')[1]
853 + rollbackConfigFile(backupFile)
854 + } else if (listMode) {
330 // 列出模式 855 // 列出模式
331 const docs = getDocsToParse() 856 const docs = getDocsToParse()
332 console.log("\n📋 待处理文档列表:") 857 console.log("\n📋 待处理文档列表:")
...@@ -343,16 +868,28 @@ async function main() { ...@@ -343,16 +868,28 @@ async function main() {
343 const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName)) 868 const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName))
344 869
345 if (targetDoc) { 870 if (targetDoc) {
871 + const start_time = Date.now()
346 const result = await parseSingleFile(targetDoc.fullPath) 872 const result = await parseSingleFile(targetDoc.fullPath)
873 + const summary = buildParseSummary([result], Date.now() - start_time)
874 + console.log("\n📊 解析结果汇总")
875 + console.log("总计: " + summary.total + " 个文档")
876 + console.log("成功: " + summary.success + " 个")
877 + console.log("失败: " + summary.failed + " 个")
878 + console.log("耗时: " + summary.duration_ms + "ms")
347 if (result.success) { 879 if (result.success) {
348 - updateConfigFile([result]) 880 + const update_result = updateConfigFile([result], { dry_run: dryRunMode })
881 + const audit_record = buildAuditRecord(summary, { dry_run: dryRunMode }, update_result, 'single')
882 + writeAuditLog(audit_record)
883 + } else {
884 + const audit_record = buildAuditRecord(summary, { dry_run: dryRunMode }, null, 'single')
885 + writeAuditLog(audit_record)
349 } 886 }
350 } else { 887 } else {
351 console.log("❌ 找不到文件: " + fileName) 888 console.log("❌ 找不到文件: " + fileName)
352 } 889 }
353 } else { 890 } else {
354 // 批量处理模式 891 // 批量处理模式
355 - await parseAllDocs(docs) 892 + await parseAllDocs(docs, { dry_run: dryRunMode })
356 } 893 }
357 894
358 console.log('\n✨ 处理完成!') 895 console.log('\n✨ 处理完成!')
......
1 import { describe, it, expect } from 'vitest' 1 import { describe, it, expect } from 'vitest'
2 -import { generateFormSn, generateConfigCode, updateConfigContent } from './parse-docs' 2 +import fs from 'fs'
3 +import os from 'os'
4 +import path from 'path'
5 +import { generateFormSn, generateConfigCode, updateConfigContent, extractDocumentText, validateParsedConfig, detectFormSnConflicts, buildDryRunDiff, buildConfigUpdateResult, buildParseSummary } from './parse-docs'
3 6
4 describe('parse-docs 生成逻辑', () => { 7 describe('parse-docs 生成逻辑', () => {
5 - it('generateFormSn 使用产品类型前缀', () => { 8 + it('generateFormSn 使用稳定规则生成', () => {
6 const form_sn = generateFormSn({ 9 const form_sn = generateFormSn({
7 product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版', 10 product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
8 product_type: 'life-insurance' 11 product_type: 'life-insurance'
9 }) 12 })
13 + const form_sn_repeat = generateFormSn({
14 + product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
15 + product_type: 'life-insurance'
16 + })
10 17
18 + expect(form_sn).toBe(form_sn_repeat)
11 expect(form_sn.startsWith('life-insurance-')).toBe(true) 19 expect(form_sn.startsWith('life-insurance-')).toBe(true)
20 + expect(form_sn).toMatch(/^life-insurance-[a-z0-9-]+-[a-f0-9]{8}$/)
12 }) 21 })
13 22
14 it('generateConfigCode 储蓄配置包含顶层 category', () => { 23 it('generateConfigCode 储蓄配置包含顶层 category', () => {
...@@ -54,4 +63,117 @@ export const FEATURE_FLAGS = {}` ...@@ -54,4 +63,117 @@ export const FEATURE_FLAGS = {}`
54 expect(result).toMatch(/'a'[\s\S]*},\n\s+'b'/) 63 expect(result).toMatch(/'a'[\s\S]*},\n\s+'b'/)
55 expect(result).toMatch(/'b'[\s\S]*}\n\nexport const FEATURE_FLAGS/) 64 expect(result).toMatch(/'b'[\s\S]*}\n\nexport const FEATURE_FLAGS/)
56 }) 65 })
66 +
67 + it('updateConfigContent 无模板时返回 null', () => {
68 + const base_content = `export const OTHER = {}`
69 + const result = updateConfigContent(base_content, [
70 + { code: " 'b': {\n name: 'B'\n }" }
71 + ])
72 +
73 + expect(result).toBe(null)
74 + })
75 +
76 + it('extractDocumentText 统一抽取结构', async () => {
77 + const temp_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-parse-'))
78 + const temp_file = path.join(temp_dir, 'sample.txt')
79 + fs.writeFileSync(temp_file, 'hello parse')
80 + const result = await extractDocumentText(temp_file)
81 +
82 + expect(result.text).toBe('hello parse')
83 + expect(result.meta.ext).toBe('.txt')
84 + expect(Array.isArray(result.warnings)).toBe(true)
85 + })
86 +
87 + it('validateParsedConfig 能识别缺失字段', () => {
88 + const invalid = validateParsedConfig({
89 + product_type: 'savings',
90 + currency: 'USD'
91 + })
92 + const valid = validateParsedConfig({
93 + product_name: '宏挚传承保障计划',
94 + product_type: 'savings',
95 + currency: 'USD',
96 + form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
97 + submit_mapping: {}
98 + })
99 +
100 + expect(invalid.valid).toBe(false)
101 + expect(invalid.errors.length).toBeGreaterThan(0)
102 + expect(valid.valid).toBe(true)
103 + })
104 +
105 + it('detectFormSnConflicts 能识别重复 form_sn', () => {
106 + const base_content = `export const PLAN_TEMPLATES = {
107 + 'a': {
108 + name: 'A',
109 + component: 'LifeInsuranceTemplate',
110 + config: {
111 + currency: 'USD',
112 + payment_periods: [],
113 + age_range: { min: 0, max: 1 },
114 + insurance_period: '终身'
115 + }
116 + }
117 +}
118 +
119 +export const FEATURE_FLAGS = {}`
120 + const conflicts = detectFormSnConflicts(base_content, [
121 + { formSn: 'a', code: ' ' },
122 + { formSn: 'b', code: ' ' }
123 + ])
124 +
125 + expect(conflicts).toEqual(['a'])
126 + })
127 +
128 + it('buildDryRunDiff 输出新增内容', () => {
129 + const diff = buildDryRunDiff([
130 + { formSn: 'b', code: " 'b': {\n name: 'B'\n }" }
131 + ])
132 +
133 + expect(diff.includes('--- plan-templates.js')).toBe(true)
134 + expect(diff.includes("+++ plan-templates.js")).toBe(true)
135 + expect(diff.includes("+ 'b': {")).toBe(true)
136 + })
137 +
138 + it('buildConfigUpdateResult 覆盖冲突与 dry-run', () => {
139 + const base_content = `export const PLAN_TEMPLATES = {
140 + 'a': {
141 + name: 'A',
142 + component: 'LifeInsuranceTemplate',
143 + config: {
144 + currency: 'USD',
145 + payment_periods: [],
146 + age_range: { min: 0, max: 1 },
147 + insurance_period: '终身'
148 + }
149 + }
150 +}
151 +
152 +export const FEATURE_FLAGS = {}`
153 + const conflict_result = buildConfigUpdateResult(base_content, [
154 + { formSn: 'a', code: " 'a': {\n name: 'A'\n }" }
155 + ])
156 + const dry_run_result = buildConfigUpdateResult(base_content, [
157 + { formSn: 'b', code: " 'b': {\n name: 'B'\n }" }
158 + ], { dry_run: true })
159 +
160 + expect(conflict_result.ok).toBe(false)
161 + expect(conflict_result.conflicts).toEqual(['a'])
162 + expect(dry_run_result.ok).toBe(true)
163 + expect(dry_run_result.diff).toContain('+ \'b\': {')
164 + })
165 +
166 + it('buildParseSummary 汇总成功失败与耗时', () => {
167 + const summary = buildParseSummary([
168 + { success: true, formSn: 'a', file: 'a.pdf', config: { product_name: 'A' } },
169 + { success: false, file: 'b.pdf', reason: 'parse_failed' }
170 + ], 1200)
171 +
172 + expect(summary.total).toBe(2)
173 + expect(summary.success).toBe(1)
174 + expect(summary.failed).toBe(1)
175 + expect(summary.duration_ms).toBe(1200)
176 + expect(summary.success_list[0].form_sn).toBe('a')
177 + expect(summary.failed_list[0].file).toBe('b.pdf')
178 + })
57 }) 179 })
......