hookehuyr

feat(docs): 优化文档解析工具并添加单元测试

- 重构 generateConfigCode 函数,简化代码生成逻辑
- 导出核心函数以支持单元测试
- 为储蓄产品添加顶层 category 属性
- 添加 parse-docs.test.js 单元测试(3个测试用例全部通过)
- 优化 commit-msg hook 校验逻辑
- 更新 CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -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 ### 优化
......
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 +}
......
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 +})