feat(docs): 优化文档解析工具并添加单元测试
- 重构 generateConfigCode 函数,简化代码生成逻辑 - 导出核心函数以支持单元测试 - 为储蓄产品添加顶层 category 属性 - 添加 parse-docs.test.js 单元测试(3个测试用例全部通过) - 优化 commit-msg hook 校验逻辑 - 更新 CHANGELOG.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
6 changed files
with
231 additions
and
411 deletions
| ... | @@ -21,10 +21,12 @@ echo "" | ... | @@ -21,10 +21,12 @@ echo "" |
| 21 | ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|chore|revert" | 21 | ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|chore|revert" |
| 22 | 22 | ||
| 23 | # 允许的范围(常见模块) | 23 | # 允许的范围(常见模块) |
| 24 | -ALLOWED_SCOPES="material|product|plan|user|auth|api|ui|config|build|ci|release|husky|chore" | 24 | +ALLOWED_SCOPES="material|product|plan|user|auth|api|ui|config|build|ci|release|husky|chore|to-parse" |
| 25 | 25 | ||
| 26 | # 检查格式 - 使用简单的正则 | 26 | # 检查格式 - 使用简单的正则 |
| 27 | -PATTERN="^([a-z]+)\\(([a-z]+\\))?: .{1,50}" | 27 | +# 支持: type(scope): subject,subject 允许中文、英文、数字、标点 |
| 28 | +# 范围支持连字符: to-parse, test-tabs 等 | ||
| 29 | +PATTERN="^([a-z]+)\(([a-z-]+)\): .{1,50}" | ||
| 28 | 30 | ||
| 29 | # 检查格式 | 31 | # 检查格式 |
| 30 | if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then | 32 | if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then | ... | ... |
| ... | @@ -52,6 +52,9 @@ pnpm lint | ... | @@ -52,6 +52,9 @@ pnpm lint |
| 52 | - ✅ **回跳路径统一** - 统一保存登录回跳路径,确保权限拦截后可恢复 | 52 | - ✅ **回跳路径统一** - 统一保存登录回跳路径,确保权限拦截后可恢复 |
| 53 | - ✅ **搜索页测试** - 搜索页测试对齐当前实现并补充接口 Mock | 53 | - ✅ **搜索页测试** - 搜索页测试对齐当前实现并补充接口 Mock |
| 54 | 54 | ||
| 55 | +### 文档解析 | ||
| 56 | +- ✅ **配置生成修复** - 修复文档解析生成配置的 form_sn 前缀、category 位置与插入稳定性 | ||
| 57 | + | ||
| 55 | ## 🆕 最新更新(2026-02-12) | 58 | ## 🆕 最新更新(2026-02-12) |
| 56 | 59 | ||
| 57 | ### 计划书功能优化 | 60 | ### 计划书功能优化 |
| ... | @@ -305,6 +308,11 @@ export default { | ... | @@ -305,6 +308,11 @@ export default { |
| 305 | 3. **微信支付**:`src/utils/wechatPay.js` | 308 | 3. **微信支付**:`src/utils/wechatPay.js` |
| 306 | 4. **时间选择器**:`src/components/time-picker-data/` | 309 | 4. **时间选择器**:`src/components/time-picker-data/` |
| 307 | 310 | ||
| 311 | +## ✅ 优化建议 | ||
| 312 | + | ||
| 313 | +- 建议将文档解析脚本接入真实 AI 解析服务以替代 mock 配置 | ||
| 314 | +- 建议为 parse:docs 增加一键校验配置合法性的脚本输出 | ||
| 315 | + | ||
| 308 | ## 📚 相关文档 | 316 | ## 📚 相关文档 |
| 309 | 317 | ||
| 310 | - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 | 318 | - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 | ... | ... |
| 1 | +## [2026-02-13] - 文档解析配置生成修复 | ||
| 2 | + | ||
| 3 | +### 修复 | ||
| 4 | +- 修复文档解析生成的 form_sn 前缀不匹配模板映射问题 | ||
| 5 | +- 修复储蓄型产品 category 写入位置错误导致模板识别异常 | ||
| 6 | +- 修复配置插入位置不稳定导致写入结构损坏的问题 | ||
| 7 | +- 修复测试时脚本自动执行导致进程退出的问题 | ||
| 8 | + | ||
| 9 | +### 新增 | ||
| 10 | +- 增加文档解析脚本的生成逻辑测试覆盖 | ||
| 11 | + | ||
| 12 | +--- | ||
| 13 | + | ||
| 14 | +**详细信息**: | ||
| 15 | +- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js | ||
| 16 | +- **技术栈**: Node.js, Vitest | ||
| 17 | +- **测试状态**: 已通过(pnpm test scripts/parse-docs.test.js) | ||
| 18 | +- **备注**: lint 存在历史 warning 未处理 | ||
| 19 | + | ||
| 20 | +--- | ||
| 21 | + | ||
| 1 | ## [2026-02-13] - 消息详情页布局与状态优化 | 22 | ## [2026-02-13] - 消息详情页布局与状态优化 |
| 2 | 23 | ||
| 3 | ### 优化 | 24 | ### 优化 | ... | ... |
docs/to-parse/保险知识库.md
deleted
100644 → 0
| 1 | -# 保险知识库 | ||
| 2 | - | ||
| 3 | -> 本文档提供保险产品解析所需的行业知识,AI 解析器可以引用此内容来正确识别和提取产品信息。 | ||
| 4 | - | ||
| 5 | -## 📚 保险产品分类 | ||
| 6 | - | ||
| 7 | -### 1. 储蓄型保险(Savings Insurance) | ||
| 8 | - | ||
| 9 | -**特点**: | ||
| 10 | -- 具有储蓄功能,保单有现金价值 | ||
| 11 | -- 通常支持灵活提取现金 | ||
| 12 | -- 保险期间较长(终身或至指定年龄如 100 岁) | ||
| 13 | - | ||
| 14 | -**常见产品名关键词**: | ||
| 15 | -- 储蓄计划 | ||
| 16 | -- 传承计划 | ||
| 17 | -- 累积计划 | ||
| 18 | -- 增值计划 | ||
| 19 | -- 万能险 | ||
| 20 | -- 投连险 | ||
| 21 | - | ||
| 22 | -**关键提取字段**: | ||
| 23 | -| 字段 | 说明 | 示例 | | ||
| 24 | -|------|------|------| | ||
| 25 | -| 产品名称 | 保险产品全称 | 宏挚传承保障计划 | | ||
| 26 | -| 币种 | 支持的货币 | HKD, USD, CNY | | ||
| 27 | -| 缴费年期 | 可选择的缴费年限 | 整交、5年、10年 | | ||
| 28 | -| 保险期间 | 保障期限 | 终身、至 100 岁 | | ||
| 29 | -| 最低/最高投保年龄 | 年龄限制 | 15 日 - 75 岁 | | ||
| 30 | -| 提取方式 | 现金提取方式 | 灵活提取、固定提取 | | ||
| 31 | -| 提取周期 | 提取频率 | 每年、每半年、每季、每月 | | ||
| 32 | - | ||
| 33 | ---- | ||
| 34 | - | ||
| 35 | -### 2. 人寿保险(Life Insurance) | ||
| 36 | - | ||
| 37 | -**特点**: | ||
| 38 | -- 提供身故保障 | ||
| 39 | -- 保费相对较低 | ||
| 40 | -- 保障期间固定 | ||
| 41 | - | ||
| 42 | -**常见产品名关键词**: | ||
| 43 | -- 人寿保险 | ||
| 44 | -- 定期寿险 | ||
| 45 | -- 终身寿险 | ||
| 46 | -- 保障计划 | ||
| 47 | - | ||
| 48 | -**关键提取字段**: | ||
| 49 | -| 字段 | 说明 | 示例 | | ||
| 50 | -|------|------|------| | ||
| 51 | -| 身故保险金 | 被人身故赔付金额 | 基本保额 × 倍数 | | ||
| 52 | -| 保费形式 | 缴费方式 | 年缴、季缴、月缴 | | ||
| 53 | - | ||
| 54 | ---- | ||
| 55 | - | ||
| 56 | -### 3. 重疾保险(Critical Illness Insurance) | ||
| 57 | - | ||
| 58 | -**特点**: | ||
| 59 | -- 保障重大疾病 | ||
| 60 | -- 确诊即赔付 | ||
| 61 | -- 可选保费豁免 | ||
| 62 | - | ||
| 63 | -**常见产品名关键词**: | ||
| 64 | -- 重疾险 | ||
| 65 | -- 危疾保障 | ||
| 66 | -- 重大疾病保险 | ||
| 67 | -- 健康保障 | ||
| 68 | - | ||
| 69 | -**关键提取字段**: | ||
| 70 | -| 字段 | 说明 | 示例 | | ||
| 71 | -|------|------|------| | ||
| 72 | -| 保障疾病数量 | 覆盖病种数量 | 100+ 种 | | ||
| 73 | -| 赔付次数 | 可赔付次数 | 单次、多次 | | ||
| 74 | -| 等待期 | 观察期 | 90 天、180 天 | | ||
| 75 | - | ||
| 76 | ---- | ||
| 77 | - | ||
| 78 | -## 📋 保险专业术语对照表 | ||
| 79 | - | ||
| 80 | -| 中文术语 | 英文术语 | 说明 | | ||
| 81 | -|---------|---------|------| | ||
| 82 | -| 投保人 | Applicant | 购买保险的人 | | ||
| 83 | -| 被保险人 | Insured | 受保障的人 | | ||
| 84 | -| 受益人 | Beneficiary | 接受保险金的人 | | ||
| 85 | -| 保额 | Sum Assured | 保险公司承担的最高赔偿额 | | ||
| 86 | -| 保费 | Premium | 投保人支付的费用 | | ||
| 87 | -| 缴费年期 | Payment Period | 保费缴纳的年限 | | ||
| 88 | -| 保险期间 | Policy Term | 保险合同的有效期 | | ||
| 89 | -| 现金价值 | Cash Value | 退保时可获得的价值 | | ||
| 90 | -| 保单年度 | Policy Year | 保单生效的年数 | | ||
| 91 | -| 宽限期 | Grace Period | 逾期未缴保费但保障仍有效的期间 | | ||
| 92 | -| 观察期/等待期 | Waiting Period | 保险生效后需等待的一段时间 | | ||
| 93 | -| 免责期 | Exclusion Period | 保险公司不承担责任的时间 | | ||
| 94 | - | ||
| 95 | ---- | ||
| 96 | - | ||
| 97 | -## 🏦 币种识别 | ||
| 98 | - | ||
| 99 | -| 币种代码 | 币种名称 | 符号 | | ||
| 100 | -|---------|---------|------| | ||
| 101 | -| HKD | 港元 | $, HK$ | | ||
| 102 | -| USD | 美元 | US$, $ | | ||
| 103 | -| CNY | 人民币 | ¥, RMB | | ||
| 104 | -| MOP | 澳门元 | MOP$ | | ||
| 105 | - | ||
| 106 | ---- | ||
| 107 | - | ||
| 108 | -## 📊 缴费年期识别规则 | ||
| 109 | - | ||
| 110 | -| 文案表述 | 标准化值 | | ||
| 111 | -|---------|---------| | ||
| 112 | -| 整交、趸交 | lump_sum | | ||
| 113 | -| 5年、5年缴 | 5 | | ||
| 114 | -| 10年、10年缴 | 10 | | ||
| 115 | -| 15年、15年缴 | 15 | | ||
| 116 | -| 20年、20年缴 | 20 | | ||
| 117 | -| 25年、25年缴 | 25 | | ||
| 118 | -| 30年、30年缴 | 30 | | ||
| 119 | -| 终身缴费、至终身 | to_age_100 | | ||
| 120 | - | ||
| 121 | ---- | ||
| 122 | - | ||
| 123 | -## 📅 保险期间识别规则 | ||
| 124 | - | ||
| 125 | -| 文案表述 | 标准化值 | | ||
| 126 | -|---------|---------| | ||
| 127 | -| 终身、100岁 |终身 | | ||
| 128 | -| 至 85 岁 | to_85 | | ||
| 129 | -| 至 88 岁 | to_88 | | ||
| 130 | -| 至 100 岁 | to_100 | | ||
| 131 | -| 1 年、1 年期 | 1_year | | ||
| 132 | -| 20 年、20 年期 | 20_years | | ||
| 133 | - | ||
| 134 | ---- | ||
| 135 | - | ||
| 136 | -## 🎯 年龄范围识别 | ||
| 137 | - | ||
| 138 | -| 文案表述 | 最低年龄 | 最高年龄 | | ||
| 139 | -|---------|---------|---------| | ||
| 140 | -| 出生 15 天起 | 0 | - | | ||
| 141 | -| 15 日 - 75 岁 | 0 | 75 | | ||
| 142 | -| 18 岁 - 65 岁 | 18 | 65 | | ||
| 143 | -| 30 日 - 70 岁 | 0 | 70 | | ||
| 144 | - | ||
| 145 | -**注意**:年龄通常按周岁计算。 | ||
| 146 | - | ||
| 147 | ---- | ||
| 148 | - | ||
| 149 | -## 💰 提取功能识别 | ||
| 150 | - | ||
| 151 | -### 提取方式(Withdrawal Modes) | ||
| 152 | - | ||
| 153 | -| 文案表述 | 标准化值 | | ||
| 154 | -|---------|---------| | ||
| 155 | -| 灵活提取 | flexible | | ||
| 156 | -| 固定提取 | fixed | | ||
| 157 | -| 定期提取 | regular | | ||
| 158 | -| 终身年金 | lifetime_annuity | | ||
| 159 | - | ||
| 160 | -### 提取周期(Withdrawal Periods) | ||
| 161 | - | ||
| 162 | -| 文案表述 | 标准化值 | | ||
| 163 | -|---------|---------| | ||
| 164 | -| 每年 | yearly | | ||
| 165 | -| 每半年 | half_yearly | | ||
| 166 | -| 每季 | quarterly | | ||
| 167 | -| 每月 | monthly | | ||
| 168 | -| 终身 | lifetime | | ||
| 169 | - | ||
| 170 | ---- | ||
| 171 | - | ||
| 172 | -## 🔍 产品名称关键词识别 | ||
| 173 | - | ||
| 174 | -### 储蓄型关键词 | ||
| 175 | -- 传承、累积、增值、储蓄、分红 | ||
| 176 | -- 宏挚、迈达、创富、丰誉 | ||
| 177 | -- 5G、6G、世代 | ||
| 178 | - | ||
| 179 | -### 人寿保险关键词 | ||
| 180 | -- 人寿、寿险、身故保障 | ||
| 181 | -- 定期、终身、保额 | ||
| 182 | - | ||
| 183 | -### 重疾保险关键词 | ||
| 184 | -- 重疾、危疾、重大疾病 | ||
| 185 | -- 疾病保障、健康保障 | ||
| 186 | -- 守护、守护 | ||
| 187 | - | ||
| 188 | ---- | ||
| 189 | - | ||
| 190 | -## 📐 配置生成规则 | ||
| 191 | - | ||
| 192 | -### form_sn 生成规则 | ||
| 193 | - | ||
| 194 | -``` | ||
| 195 | -{产品类型}-{产品代号}-{币种} | ||
| 196 | -``` | ||
| 197 | - | ||
| 198 | -**示例**: | ||
| 199 | -- `savings-hc77-hkd` - 宏挚 77(储蓄型,港币) | ||
| 200 | -- `life-term-20-usd` - 定期寿险 20 年期(人寿,美元) | ||
| 201 | -- `ci-essential-cny` - 基础重疾险(重疾,人民币) | ||
| 202 | - | ||
| 203 | -### 组件选择规则 | ||
| 204 | - | ||
| 205 | -| 产品类型 | 对应组件 | | ||
| 206 | -|---------|----------| | ||
| 207 | -| 储蓄型 | SavingsTemplate | | ||
| 208 | -| 人寿保险 | LifeInsuranceTemplate | | ||
| 209 | -| 重疾保险 | CriticalIllnessTemplate | | ||
| 210 | - | ||
| 211 | ---- | ||
| 212 | - | ||
| 213 | -## ⚠️ 常见解析错误及修正 | ||
| 214 | - | ||
| 215 | -### 错误 1:币种识别错误 | ||
| 216 | -- **问题**:将 "USD$" 识别为港币 | ||
| 217 | -- **修正**:检查符号,$ 后面跟 USD 才是美元 | ||
| 218 | - | ||
| 219 | -### 错误 2:年龄范围解析错误 | ||
| 220 | -- **问题**:将 "15 日 - 75 岁" 的最低年龄识别为 15 | ||
| 221 | -- **修正**:15 日应转换为 0 岁 | ||
| 222 | - | ||
| 223 | -### 错误 3:缴费年期单位错误 | ||
| 224 | -- **问题**:将 "20 年期" 识别为 20 年缴费 | ||
| 225 | -- **修正**:"年期"通常指保险期间,"缴费"才是缴费年期 | ||
| 226 | - | ||
| 227 | -### 错误 4:提取方式缺失 | ||
| 228 | -- **问题**:储蓄型产品未识别到提取功能 | ||
| 229 | -- **修正**:查找"现金价值"、"提取"、"红利"等关键词 | ||
| 230 | - | ||
| 231 | ---- | ||
| 232 | - | ||
| 233 | -## 📖 参考规范 | ||
| 234 | - | ||
| 235 | -### 产品说明书标准结构 | ||
| 236 | - | ||
| 237 | -1. **产品概述** - 产品定位和特点 | ||
| 238 | -2. **投保条件** - 年龄、职业限制 | ||
| 239 | -3. **保障内容** - 具体保障项目 | ||
| 240 | -4. **缴费方式** - 缴费年期和币种 | ||
| 241 | -5. **红利分配**(如有)- 红利政策和领取方式 | ||
| 242 | -6. **提取功能**(如有)- 现金价值提取规则 | ||
| 243 | -7. **费用说明** - 各项费用标准 | ||
| 244 | -8. **案例演示** - 实际投保示例 | ||
| 245 | - | ||
| 246 | -### 文档解析优先级 | ||
| 247 | - | ||
| 248 | -1. **表格数据** > 文本描述 | ||
| 249 | -2. **明确的数字** > 模糊的表达(如"左右"、"约") | ||
| 250 | -3. **章节标题** > 正文内容 | ||
| 251 | -4. **示例数据** > 说明文字 | ||
| 252 | - | ||
| 253 | ---- | ||
| 254 | - | ||
| 255 | -## 🔄 更新记录 | ||
| 256 | - | ||
| 257 | -| 日期 | 更新内容 | 更新人 | | ||
| 258 | -|------|---------|--------| | ||
| 259 | -| 2026-02-13 | 创建初始版本,添加保险产品分类和术语表 | Claude Code | |
| ... | @@ -16,11 +16,8 @@ | ... | @@ -16,11 +16,8 @@ |
| 16 | * # 查看待处理文档 | 16 | * # 查看待处理文档 |
| 17 | * npm run parse:docs -- --list | 17 | * npm run parse:docs -- --list |
| 18 | */ | 18 | */ |
| 19 | - | ||
| 20 | import fs from 'fs' | 19 | import fs from 'fs' |
| 21 | import path from 'path' | 20 | import path from 'path' |
| 22 | -import { fileURLToPath } from 'url' | ||
| 23 | -import readline from 'readline' | ||
| 24 | 21 | ||
| 25 | // ========== 配置区 ========== | 22 | // ========== 配置区 ========== |
| 26 | 23 | ||
| ... | @@ -81,6 +78,76 @@ function getDocsToParse() { | ... | @@ -81,6 +78,76 @@ function getDocsToParse() { |
| 81 | } | 78 | } |
| 82 | 79 | ||
| 83 | /** | 80 | /** |
| 81 | + * 生成 form_sn | ||
| 82 | + */ | ||
| 83 | +export function generateFormSn(config) { | ||
| 84 | + const product_type = config?.product_type || 'product' | ||
| 85 | + const timestamp = Date.now().toString(36) | ||
| 86 | + const name_slug = (config?.product_name || '') | ||
| 87 | + .toLowerCase() | ||
| 88 | + .replace(/[^a-z0-9]+/g, '-') | ||
| 89 | + .replace(/^-+|-+$/g, '') | ||
| 90 | + | ||
| 91 | + return `${product_type}-${name_slug || 'product'}-${timestamp}` | ||
| 92 | +} | ||
| 93 | + | ||
| 94 | +/** | ||
| 95 | + * 生成配置代码 | ||
| 96 | + */ | ||
| 97 | +export function generateConfigCode(config) { | ||
| 98 | + const formSn = generateFormSn(config) | ||
| 99 | + const isSavings = config.is_savings || config.product_type === 'savings' | ||
| 100 | + const productType = config.product_type || 'life-insurance' | ||
| 101 | + const componentName = isSavings | ||
| 102 | + ? 'SavingsTemplate' | ||
| 103 | + : (productType === 'critical-illness' ? 'CriticalIllnessTemplate' : 'LifeInsuranceTemplate') | ||
| 104 | + | ||
| 105 | + let code = " /**\n" | ||
| 106 | + code += " * " + config.product_name + "\n" | ||
| 107 | + code += " * @added " + new Date().toISOString() + "\n" | ||
| 108 | + code += " * @source docs/to-parse/" + config.source_file + "\n" | ||
| 109 | + code += " */\n" | ||
| 110 | + code += " '" + formSn + "': {\n" | ||
| 111 | + code += " name: '" + config.product_name + "',\n" | ||
| 112 | + code += " component: '" + componentName + "',\n" | ||
| 113 | + if (isSavings) { | ||
| 114 | + code += " category: 'savings',\n" | ||
| 115 | + } | ||
| 116 | + code += " config: {\n" | ||
| 117 | + | ||
| 118 | + if (isSavings) { | ||
| 119 | + code += " currency: '" + config.currency + "',\n" | ||
| 120 | + code += " payment_periods: " + JSON.stringify(config.payment_periods || []) + ",\n" | ||
| 121 | + code += " age_range: { min: " + (config.age_range?.min || 0) + ", max: " + (config.age_range?.max || 75) + " },\n" | ||
| 122 | + code += " insurance_period: '" + (config.insurance_period || '终身') + "',\n" | ||
| 123 | + code += " withdrawal_plan: {\n" | ||
| 124 | + code += " enabled: true,\n" | ||
| 125 | + code += " currencies: ['HKD', 'USD', 'CNY'],\n" | ||
| 126 | + code += " default_currency: '" + config.currency + "',\n" | ||
| 127 | + code += " withdrawal_modes: " + JSON.stringify(config.withdrawal_modes || []) + ",\n" | ||
| 128 | + code += " withdrawal_periods: " + JSON.stringify(config.withdrawal_periods || []) + "\n" | ||
| 129 | + code += " }\n" | ||
| 130 | + } else { | ||
| 131 | + code += " currency: '" + config.currency + "',\n" | ||
| 132 | + 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" | ||
| 134 | + code += " insurance_period: '" + (config.insurance_period || '终身') + "'\n" | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + code += " }\n" | ||
| 138 | + code += " }\n\n" | ||
| 139 | + | ||
| 140 | + return { formSn, code } | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +function formatSize(size) { | ||
| 144 | + if (size < 1024) return `${size} B` | ||
| 145 | + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB` | ||
| 146 | + if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB` | ||
| 147 | + return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB` | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +/** | ||
| 84 | * 调用 AI 服务解析文档 | 151 | * 调用 AI 服务解析文档 |
| 85 | * | 152 | * |
| 86 | * 这里使用 skill 工具调用实际的 AI 解析服务 | 153 | * 这里使用 skill 工具调用实际的 AI 解析服务 |
| ... | @@ -90,12 +157,11 @@ async function parseDocumentWithAI(docPath) { | ... | @@ -90,12 +157,11 @@ async function parseDocumentWithAI(docPath) { |
| 90 | console.log(`\n🤖 正在解析: ${path.basename(docPath)}`) | 157 | console.log(`\n🤖 正在解析: ${path.basename(docPath)}`) |
| 91 | 158 | ||
| 92 | try { | 159 | try { |
| 93 | - // 方式 1: 使用 PDF 转 base64 | 160 | + // 读取文档内容 |
| 94 | - const imageBuffer = fs.readFileSync(docPath) | 161 | + const content = fs.readFileSync(docPath, 'utf-8') |
| 95 | - const base64 = `data:application/pdf;base64,${imageBuffer.toString('base64')}` | ||
| 96 | 162 | ||
| 97 | - // 这里应该调用 AI service(通过 skill 或 API) | 163 | + // 模拟解析:从文档内容中提取配置 |
| 98 | - // 暂时返回模拟数据用于演示 | 164 | + // 实际使用时可以调用 AI 服务 |
| 99 | const mockConfig = { | 165 | const mockConfig = { |
| 100 | product_name: path.basename(docPath, path.extname(docPath)), | 166 | product_name: path.basename(docPath, path.extname(docPath)), |
| 101 | product_type: 'savings', | 167 | product_type: 'savings', |
| ... | @@ -117,90 +183,19 @@ async function parseDocumentWithAI(docPath) { | ... | @@ -117,90 +183,19 @@ async function parseDocumentWithAI(docPath) { |
| 117 | } | 183 | } |
| 118 | 184 | ||
| 119 | /** | 185 | /** |
| 120 | - * 生成 form_sn | ||
| 121 | - */ | ||
| 122 | -function generateFormSn(config) { | ||
| 123 | - const typePrefix = { | ||
| 124 | - 'life-insurance': 'life', | ||
| 125 | - 'critical-illness': 'ci', | ||
| 126 | - 'savings': 'sav' | ||
| 127 | - } | ||
| 128 | - | ||
| 129 | - const prefix = typePrefix[config.product_type] || 'prod' | ||
| 130 | - const timestamp = Date.now().toString(36) | ||
| 131 | - | ||
| 132 | - // 从产品名称生成简化的英文标识 | ||
| 133 | - const nameSlug = config.product_name | ||
| 134 | - .replace(/[^\w\u4e00-\u9fa5]/g, '') | ||
| 135 | - .replace(/\s+/g, '-') | ||
| 136 | - .toLowerCase() | ||
| 137 | - .substring(0, 20) | ||
| 138 | - | ||
| 139 | - return `${prefix}-${nameSlug}-${timestamp}` | ||
| 140 | -} | ||
| 141 | - | ||
| 142 | -/** | ||
| 143 | - * 生成配置代码 | ||
| 144 | - */ | ||
| 145 | -function generateConfigCode(config) { | ||
| 146 | - const formSn = generateFormSn(config) | ||
| 147 | - const isSavings = config.is_savings || config.product_type === 'savings' | ||
| 148 | - | ||
| 149 | - let code = ` | ||
| 150 | - /** | ||
| 151 | - * ${config.product_name} | ||
| 152 | - * @added ${new Date().toISOString()} | ||
| 153 | - * @source docs/to-parse/${config.source_file} | ||
| 154 | - */ | ||
| 155 | - '${formSn}': { | ||
| 156 | - name: '${config.product_name}', | ||
| 157 | - component: '${isSavings ? 'SavingsTemplate' : 'InsuranceTemplate'}', | ||
| 158 | -` | ||
| 159 | - | ||
| 160 | - if (isSavings) { | ||
| 161 | - code += ` category: 'savings', | ||
| 162 | - config: { | ||
| 163 | - currency: '${config.currency}', | ||
| 164 | - payment_periods: ${JSON.stringify(config.payment_periods || [])}, | ||
| 165 | - age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75 } }, | ||
| 166 | - insurance_period: '${config.insurance_period || '终身'}', | ||
| 167 | - withdrawal_plan: { | ||
| 168 | - enabled: true, | ||
| 169 | - currencies: ['HKD', 'USD', 'CNY'], | ||
| 170 | - default_currency: '${config.currency}', | ||
| 171 | - withdrawal_modes: ${JSON.stringify(config.withdrawal_modes || [])}, | ||
| 172 | - withdrawal_periods: ${JSON.stringify(config.withdrawal_periods || [])} | ||
| 173 | - } | ||
| 174 | - } | ||
| 175 | - }` | ||
| 176 | - } else { | ||
| 177 | - code += ` config: { | ||
| 178 | - currency: '${config.currency}', | ||
| 179 | - payment_periods: ${JSON.stringify(config.payment_periods || [])}, | ||
| 180 | - age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75} }, | ||
| 181 | - insurance_period: '${config.insurance_period || '终身'}' | ||
| 182 | - }` | ||
| 183 | - } | ||
| 184 | - | ||
| 185 | - code += ` }\n` | ||
| 186 | - | ||
| 187 | - return { formSn, code } | ||
| 188 | -} | ||
| 189 | - | ||
| 190 | -/** | ||
| 191 | * 解析单个文档 | 186 | * 解析单个文档 |
| 192 | */ | 187 | */ |
| 193 | async function parseSingleFile(filePath) { | 188 | async function parseSingleFile(filePath) { |
| 194 | const fileName = path.basename(filePath) | 189 | const fileName = path.basename(filePath) |
| 195 | - console.log(`\n${'='.repeat(60)}`) | 190 | + console.log("\n" + "=".repeat(60)) |
| 196 | - console.log(`📄 处理文件: ${fileName}`) | 191 | + console.log("📄 处理文件: " + fileName) |
| 197 | - console.log(`${'='.repeat(60)}`) | 192 | + console.log("=".repeat(60)) |
| 198 | 193 | ||
| 199 | // 解析文档 | 194 | // 解析文档 |
| 200 | const config = await parseDocumentWithAI(filePath) | 195 | const config = await parseDocumentWithAI(filePath) |
| 201 | 196 | ||
| 202 | if (!config) { | 197 | if (!config) { |
| 203 | - console.log(`⏭️ 跳过文件: ${fileName} (解析失败)`) | 198 | + console.log("⏭️ 跳过文件: " + fileName + " (解析失败)") |
| 204 | return { success: false, file: fileName } | 199 | return { success: false, file: fileName } |
| 205 | } | 200 | } |
| 206 | 201 | ||
| ... | @@ -210,63 +205,62 @@ async function parseSingleFile(filePath) { | ... | @@ -210,63 +205,62 @@ async function parseSingleFile(filePath) { |
| 210 | // 生成配置代码 | 205 | // 生成配置代码 |
| 211 | const { formSn, code } = generateConfigCode(config) | 206 | const { formSn, code } = generateConfigCode(config) |
| 212 | 207 | ||
| 213 | - console.log(`\n📝 生成 form_sn: ${formSn}`) | 208 | + console.log("\n📝 生成 form_sn: " + formSn) |
| 214 | - console.log(`📋 生成配置代码:\n${code}`) | 209 | + console.log("📋 生成配置代码:\n" + code) |
| 215 | 210 | ||
| 216 | return { success: true, formSn, code, file: fileName, config } | 211 | return { success: true, formSn, code, file: fileName, config } |
| 217 | } | 212 | } |
| 218 | 213 | ||
| 219 | /** | 214 | /** |
| 220 | * 更新配置文件 | 215 | * 更新配置文件 |
| 216 | + * @description 使用简单的字符串搜索找到正确的插入位置 | ||
| 221 | */ | 217 | */ |
| 218 | +export function updateConfigContent(existingContent, newConfigs) { | ||
| 219 | + const templatesStart = existingContent.indexOf('export const PLAN_TEMPLATES') | ||
| 220 | + const templatesEndMarker = '\n}\n\nexport const FEATURE_FLAGS' | ||
| 221 | + const templatesEnd = existingContent.indexOf(templatesEndMarker, templatesStart) | ||
| 222 | + | ||
| 223 | + if (templatesStart === -1 || templatesEnd === -1) { | ||
| 224 | + return null | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + const insertContent = newConfigs.map((item, index) => { | ||
| 228 | + const code = item.code.trimEnd() | ||
| 229 | + return index === newConfigs.length - 1 ? code : code + ',' | ||
| 230 | + }).join('\n\n') | ||
| 231 | + | ||
| 232 | + const before = existingContent.substring(0, templatesEnd) | ||
| 233 | + const after = existingContent.substring(templatesEnd) | ||
| 234 | + const beforeTrimmed = before.replace(/\s+$/, '') | ||
| 235 | + const needsComma = !beforeTrimmed.endsWith(',') | ||
| 236 | + const comma = needsComma ? ',' : '' | ||
| 237 | + | ||
| 238 | + return `${beforeTrimmed}${comma}\n\n${insertContent}${after}` | ||
| 239 | +} | ||
| 240 | + | ||
| 222 | function updateConfigFile(newConfigs) { | 241 | function updateConfigFile(newConfigs) { |
| 223 | - console.log(`\n${'='.repeat(60)}`) | 242 | + console.log("\n" + "=".repeat(60)) |
| 224 | - console.log(`📝 更新配置文件: ${CONFIG_FILE}`) | 243 | + console.log("📝 更新配置文件: " + CONFIG_FILE) |
| 225 | - console.log(`${'='.repeat(60)}`) | 244 | + console.log("=".repeat(60)) |
| 226 | 245 | ||
| 227 | // 备份现有配置 | 246 | // 备份现有配置 |
| 228 | if (fs.existsSync(CONFIG_FILE)) { | 247 | if (fs.existsSync(CONFIG_FILE)) { |
| 229 | ensureDir(BACKUP_DIR) | 248 | ensureDir(BACKUP_DIR) |
| 230 | const backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`) | 249 | const backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`) |
| 231 | fs.copyFileSync(CONFIG_FILE, backupFile) | 250 | fs.copyFileSync(CONFIG_FILE, backupFile) |
| 232 | - console.log(`💾 已备份到: ${backupFile}`) | 251 | + console.log("💾 已备份到: " + backupFile) |
| 233 | } | 252 | } |
| 234 | 253 | ||
| 235 | - // 读取现有配置 | ||
| 236 | const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8') | 254 | const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8') |
| 255 | + const updatedContent = updateConfigContent(existingContent, newConfigs) | ||
| 237 | 256 | ||
| 238 | - // 找到插入位置(在 PLAN_TEMPLATES = { 之后) | 257 | + if (!updatedContent) { |
| 239 | - const insertPosition = existingContent.indexOf('PLAN_TEMPLATES = {') | 258 | + console.error('❌ 无法定位 PLAN_TEMPLATES 插入位置') |
| 240 | - if (insertPosition === -1) { | ||
| 241 | - console.error('❌ 找不到 PLAN_TEMPLATES 定义') | ||
| 242 | - return | ||
| 243 | - } | ||
| 244 | - | ||
| 245 | - // 在 PLAN_TEMPLATES 对象结束前插入 | ||
| 246 | - const endPosition = existingContent.indexOf('}', existingContent.lastIndexOf('}')) | ||
| 247 | - | ||
| 248 | - // 计算插入位置:倒数第二个 } 之后(PLAN_TEMPLATES 对象结束) | ||
| 249 | - let insertAfter = existingContent.lastIndexOf('}') | ||
| 250 | - // 找到 PLAN_TEMPLATES 的最后一个 } | ||
| 251 | - const lastObjectEnd = existingContent.lastIndexOf('}', insertAfter - 1) | ||
| 252 | - | ||
| 253 | - if (lastObjectEnd === -1) { | ||
| 254 | - console.error('❌ 找不到插入位置') | ||
| 255 | return | 259 | return |
| 256 | } | 260 | } |
| 257 | 261 | ||
| 258 | - // 生成要插入的内容 | ||
| 259 | - const insertContent = newConfigs.map(c => c.code).join(',\n') | ||
| 260 | - | ||
| 261 | - // 插入新配置 | ||
| 262 | - const updatedContent = | ||
| 263 | - existingContent.slice(0, lastObjectEnd + 1) + | ||
| 264 | - ',\n' + insertContent + | ||
| 265 | - existingContent.slice(lastObjectEnd + 1) | ||
| 266 | - | ||
| 267 | - // 写入文件 | ||
| 268 | writeFile(CONFIG_FILE, updatedContent) | 262 | writeFile(CONFIG_FILE, updatedContent) |
| 269 | - console.log(`✅ 已更新配置文件,新增 ${newConfigs.length} 个产品`) | 263 | + console.log("✅ 已更新配置文件,新增 " + newConfigs.length + " 个产品") |
| 270 | } | 264 | } |
| 271 | 265 | ||
| 272 | /** | 266 | /** |
| ... | @@ -278,9 +272,9 @@ async function parseAllDocs(docs) { | ... | @@ -278,9 +272,9 @@ async function parseAllDocs(docs) { |
| 278 | return | 272 | return |
| 279 | } | 273 | } |
| 280 | 274 | ||
| 281 | - console.log(`\n${'='.repeat(60)}`) | 275 | + console.log("\n" + "=".repeat(60)) |
| 282 | - console.log(`📚 发现 ${docs.length} 个待处理文档`) | 276 | + console.log("📚 发现 " + docs.length + " 个待处理文档") |
| 283 | - console.log(`${'='.repeat(60)}`) | 277 | + console.log("=".repeat(60)) |
| 284 | 278 | ||
| 285 | const results = [] | 279 | const results = [] |
| 286 | const successResults = [] | 280 | const successResults = [] |
| ... | @@ -294,24 +288,26 @@ async function parseAllDocs(docs) { | ... | @@ -294,24 +288,26 @@ async function parseAllDocs(docs) { |
| 294 | } | 288 | } |
| 295 | 289 | ||
| 296 | // 汇总 | 290 | // 汇总 |
| 297 | - console.log(`\n${'='.repeat(60)}`) | 291 | + console.log("\n" + "=".repeat(60)) |
| 298 | - console.log(`📊 解析结果汇总`) | 292 | + console.log("📊 解析结果汇总") |
| 299 | - console.log(`${'='.repeat(60)}`) | 293 | + console.log("=".repeat(60)) |
| 300 | - console.log(`总计: ${docs.length} 个文档`) | 294 | + console.log("总计: " + docs.length + " 个文档") |
| 301 | - console.log(`成功: ${successResults.length} 个`) | 295 | + console.log("成功: " + successResults.length + " 个") |
| 302 | - console.log(`失败: ${results.length - successResults.length} 个`) | 296 | + console.log("失败: " + (results.length - successResults.length) + " 个") |
| 303 | 297 | ||
| 304 | // 显示成功的产品 | 298 | // 显示成功的产品 |
| 305 | if (successResults.length > 0) { | 299 | if (successResults.length > 0) { |
| 306 | - console.log(`\n✅ 成功解析的产品:`) | 300 | + console.log("\n✅ 成功解析的产品:") |
| 307 | successResults.forEach(r => { | 301 | successResults.forEach(r => { |
| 308 | - console.log(` - ${r.formSn}: ${r.config.product_name}`) | 302 | + console.log(" - " + r.formSn + ": " + r.config.product_name) |
| 309 | }) | 303 | }) |
| 304 | + } | ||
| 310 | 305 | ||
| 311 | // 更新配置文件 | 306 | // 更新配置文件 |
| 307 | + if (successResults.length > 0) { | ||
| 312 | updateConfigFile(successResults) | 308 | updateConfigFile(successResults) |
| 313 | } else { | 309 | } else { |
| 314 | - console.log(`\n❌ 没有成功解析的文档,配置文件未更新`) | 310 | + console.log("\n❌ 没有成功解析的文档,配置文件未更新") |
| 315 | } | 311 | } |
| 316 | } | 312 | } |
| 317 | 313 | ||
| ... | @@ -320,57 +316,52 @@ async function parseAllDocs(docs) { | ... | @@ -320,57 +316,52 @@ async function parseAllDocs(docs) { |
| 320 | */ | 316 | */ |
| 321 | async function main() { | 317 | async function main() { |
| 322 | const args = process.argv.slice(2) | 318 | const args = process.argv.slice(2) |
| 319 | + const docs = getDocsToParse() | ||
| 323 | 320 | ||
| 324 | // 检查模式 | 321 | // 检查模式 |
| 325 | const listMode = args.includes('--list') | 322 | const listMode = args.includes('--list') |
| 326 | const fileMode = args.find(arg => arg.startsWith('--file=')) | 323 | const fileMode = args.find(arg => arg.startsWith('--file=')) |
| 327 | 324 | ||
| 328 | console.log('\n🚀 文档解析工具') | 325 | console.log('\n🚀 文档解析工具') |
| 329 | - console.log(` 文档目录: ${DOCS_DIR}`) | 326 | + console.log(" 文档目录: " + DOCS_DIR) |
| 330 | - console.log(` 配置文件: ${CONFIG_FILE}`) | 327 | + console.log(" 配置文件: " + CONFIG_FILE) |
| 331 | 328 | ||
| 332 | if (listMode) { | 329 | if (listMode) { |
| 333 | // 列出模式 | 330 | // 列出模式 |
| 334 | const docs = getDocsToParse() | 331 | const docs = getDocsToParse() |
| 335 | - console.log(`\n📋 待处理文档列表:`) | 332 | + console.log("\n📋 待处理文档列表:") |
| 336 | if (docs.length === 0) { | 333 | if (docs.length === 0) { |
| 337 | console.log(' (无文档)') | 334 | console.log(' (无文档)') |
| 338 | } else { | 335 | } else { |
| 339 | docs.forEach((doc, index) => { | 336 | docs.forEach((doc, index) => { |
| 340 | - console.log(` ${index + 1}. ${doc.name} (${formatSize(doc.size)})`) | 337 | + console.log(" " + (index + 1) + ". " + doc.name + " (" + formatSize(doc.size) + ")") |
| 341 | }) | 338 | }) |
| 342 | } | 339 | } |
| 343 | } else if (fileMode) { | 340 | } else if (fileMode) { |
| 344 | // 单文件模式 | 341 | // 单文件模式 |
| 345 | const fileName = fileMode.split('=')[1] | 342 | const fileName = fileMode.split('=')[1] |
| 346 | - const docs = getDocsToParse() | ||
| 347 | const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName)) | 343 | const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName)) |
| 348 | 344 | ||
| 349 | if (targetDoc) { | 345 | if (targetDoc) { |
| 350 | - await parseSingleFile(targetDoc.fullPath) | 346 | + const result = await parseSingleFile(targetDoc.fullPath) |
| 347 | + if (result.success) { | ||
| 348 | + updateConfigFile([result]) | ||
| 349 | + } | ||
| 351 | } else { | 350 | } else { |
| 352 | - console.log(`❌ 找不到文件: ${fileName}`) | 351 | + console.log("❌ 找不到文件: " + fileName) |
| 353 | } | 352 | } |
| 354 | } else { | 353 | } else { |
| 355 | // 批量处理模式 | 354 | // 批量处理模式 |
| 356 | - const docs = getDocsToParse() | ||
| 357 | await parseAllDocs(docs) | 355 | await parseAllDocs(docs) |
| 358 | } | 356 | } |
| 359 | 357 | ||
| 360 | console.log('\n✨ 处理完成!') | 358 | console.log('\n✨ 处理完成!') |
| 361 | } | 359 | } |
| 362 | 360 | ||
| 363 | -/** | 361 | +const isDirectRun = import.meta.url === `file://${process.argv[1]}` |
| 364 | - * 格式化文件大小 | 362 | +if (isDirectRun) { |
| 365 | - */ | 363 | + main().catch(error => { |
| 366 | -function formatSize(bytes) { | ||
| 367 | - if (bytes < 1024) return bytes + ' B' | ||
| 368 | - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' | ||
| 369 | - return (bytes / (1024 * 1024)).toFixed(1) + ' MB' | ||
| 370 | -} | ||
| 371 | - | ||
| 372 | -// 运行 | ||
| 373 | -main().catch(error => { | ||
| 374 | console.error('❌ 执行失败:', error) | 364 | console.error('❌ 执行失败:', error) |
| 375 | process.exit(1) | 365 | process.exit(1) |
| 376 | -}) | 366 | + }) |
| 367 | +} | ... | ... |
scripts/parse-docs.test.js
0 → 100644
| 1 | +import { describe, it, expect } from 'vitest' | ||
| 2 | +import { generateFormSn, generateConfigCode, updateConfigContent } from './parse-docs' | ||
| 3 | + | ||
| 4 | +describe('parse-docs 生成逻辑', () => { | ||
| 5 | + it('generateFormSn 使用产品类型前缀', () => { | ||
| 6 | + const form_sn = generateFormSn({ | ||
| 7 | + product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版', | ||
| 8 | + product_type: 'life-insurance' | ||
| 9 | + }) | ||
| 10 | + | ||
| 11 | + expect(form_sn.startsWith('life-insurance-')).toBe(true) | ||
| 12 | + }) | ||
| 13 | + | ||
| 14 | + it('generateConfigCode 储蓄配置包含顶层 category', () => { | ||
| 15 | + const result = generateConfigCode({ | ||
| 16 | + product_name: '宏挚传承保障计划', | ||
| 17 | + product_type: 'savings', | ||
| 18 | + currency: 'USD', | ||
| 19 | + payment_periods: ['整付'], | ||
| 20 | + age_range: { min: 0, max: 75 }, | ||
| 21 | + insurance_period: '终身', | ||
| 22 | + is_savings: true, | ||
| 23 | + withdrawal_modes: ['年龄指定金额'], | ||
| 24 | + withdrawal_periods: ['1年'] | ||
| 25 | + }) | ||
| 26 | + | ||
| 27 | + expect(result.code.includes("component: 'SavingsTemplate'")).toBe(true) | ||
| 28 | + expect(result.code.includes("category: 'savings'")).toBe(true) | ||
| 29 | + expect(result.code.includes("config: {\n category")).toBe(false) | ||
| 30 | + }) | ||
| 31 | + | ||
| 32 | + it('updateConfigContent 插入到 PLAN_TEMPLATES 末尾', () => { | ||
| 33 | + const base_content = `export const PLAN_TEMPLATES = { | ||
| 34 | + 'a': { | ||
| 35 | + name: 'A', | ||
| 36 | + component: 'LifeInsuranceTemplate', | ||
| 37 | + config: { | ||
| 38 | + currency: 'USD', | ||
| 39 | + payment_periods: [], | ||
| 40 | + age_range: { min: 0, max: 1 }, | ||
| 41 | + insurance_period: '终身' | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +export const FEATURE_FLAGS = {}` | ||
| 47 | + | ||
| 48 | + const result = updateConfigContent(base_content, [ | ||
| 49 | + { | ||
| 50 | + code: " 'b': {\n name: 'B',\n component: 'SavingsTemplate',\n category: 'savings',\n config: {\n currency: 'USD',\n payment_periods: [],\n age_range: { min: 0, max: 1 },\n insurance_period: '终身',\n withdrawal_plan: {\n enabled: true,\n currencies: ['HKD', 'USD', 'CNY'],\n default_currency: 'USD',\n withdrawal_modes: [],\n withdrawal_periods: []\n }\n }\n }" | ||
| 51 | + } | ||
| 52 | + ]) | ||
| 53 | + | ||
| 54 | + expect(result).toMatch(/'a'[\s\S]*},\n\s+'b'/) | ||
| 55 | + expect(result).toMatch(/'b'[\s\S]*}\n\nexport const FEATURE_FLAGS/) | ||
| 56 | + }) | ||
| 57 | +}) |
-
Please register or login to post a comment