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>
Showing
9 changed files
with
1299 additions
and
48 deletions
| ... | @@ -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 | ... | ... |
docs/tasks/文档解析改造-tasks.md
0 → 100644
| 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 | }) | ... | ... |
-
Please register or login to post a comment