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 ""
ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|chore|revert"
# 允许的范围(常见模块)
ALLOWED_SCOPES="material|product|plan|user|auth|api|ui|config|build|ci|release|husky|chore"
ALLOWED_SCOPES="material|product|plan|user|auth|api|ui|config|build|ci|release|husky|chore|to-parse"
# 检查格式 - 使用简单的正则
PATTERN="^([a-z]+)\\(([a-z]+\\))?: .{1,50}"
# 支持: type(scope): subject,subject 允许中文、英文、数字、标点
# 范围支持连字符: to-parse, test-tabs 等
PATTERN="^([a-z]+)\(([a-z-]+)\): .{1,50}"
# 检查格式
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
......
......@@ -52,6 +52,9 @@ pnpm lint
-**回跳路径统一** - 统一保存登录回跳路径,确保权限拦截后可恢复
-**搜索页测试** - 搜索页测试对齐当前实现并补充接口 Mock
### 文档解析
-**配置生成修复** - 修复文档解析生成配置的 form_sn 前缀、category 位置与插入稳定性
## 🆕 最新更新(2026-02-12)
### 计划书功能优化
......@@ -305,6 +308,11 @@ export default {
3. **微信支付**`src/utils/wechatPay.js`
4. **时间选择器**`src/components/time-picker-data/`
## ✅ 优化建议
- 建议将文档解析脚本接入真实 AI 解析服务以替代 mock 配置
- 建议为 parse:docs 增加一键校验配置合法性的脚本输出
## 📚 相关文档
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
......
## [2026-02-13] - 文档解析配置生成修复
### 修复
- 修复文档解析生成的 form_sn 前缀不匹配模板映射问题
- 修复储蓄型产品 category 写入位置错误导致模板识别异常
- 修复配置插入位置不稳定导致写入结构损坏的问题
- 修复测试时脚本自动执行导致进程退出的问题
### 新增
- 增加文档解析脚本的生成逻辑测试覆盖
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js
- **技术栈**: Node.js, Vitest
- **测试状态**: 已通过(pnpm test scripts/parse-docs.test.js)
- **备注**: lint 存在历史 warning 未处理
---
## [2026-02-13] - 消息详情页布局与状态优化
### 优化
......
# 保险知识库
> 本文档提供保险产品解析所需的行业知识,AI 解析器可以引用此内容来正确识别和提取产品信息。
## 📚 保险产品分类
### 1. 储蓄型保险(Savings Insurance)
**特点**
- 具有储蓄功能,保单有现金价值
- 通常支持灵活提取现金
- 保险期间较长(终身或至指定年龄如 100 岁)
**常见产品名关键词**
- 储蓄计划
- 传承计划
- 累积计划
- 增值计划
- 万能险
- 投连险
**关键提取字段**
| 字段 | 说明 | 示例 |
|------|------|------|
| 产品名称 | 保险产品全称 | 宏挚传承保障计划 |
| 币种 | 支持的货币 | HKD, USD, CNY |
| 缴费年期 | 可选择的缴费年限 | 整交、5年、10年 |
| 保险期间 | 保障期限 | 终身、至 100 岁 |
| 最低/最高投保年龄 | 年龄限制 | 15 日 - 75 岁 |
| 提取方式 | 现金提取方式 | 灵活提取、固定提取 |
| 提取周期 | 提取频率 | 每年、每半年、每季、每月 |
---
### 2. 人寿保险(Life Insurance)
**特点**
- 提供身故保障
- 保费相对较低
- 保障期间固定
**常见产品名关键词**
- 人寿保险
- 定期寿险
- 终身寿险
- 保障计划
**关键提取字段**
| 字段 | 说明 | 示例 |
|------|------|------|
| 身故保险金 | 被人身故赔付金额 | 基本保额 × 倍数 |
| 保费形式 | 缴费方式 | 年缴、季缴、月缴 |
---
### 3. 重疾保险(Critical Illness Insurance)
**特点**
- 保障重大疾病
- 确诊即赔付
- 可选保费豁免
**常见产品名关键词**
- 重疾险
- 危疾保障
- 重大疾病保险
- 健康保障
**关键提取字段**
| 字段 | 说明 | 示例 |
|------|------|------|
| 保障疾病数量 | 覆盖病种数量 | 100+ 种 |
| 赔付次数 | 可赔付次数 | 单次、多次 |
| 等待期 | 观察期 | 90 天、180 天 |
---
## 📋 保险专业术语对照表
| 中文术语 | 英文术语 | 说明 |
|---------|---------|------|
| 投保人 | Applicant | 购买保险的人 |
| 被保险人 | Insured | 受保障的人 |
| 受益人 | Beneficiary | 接受保险金的人 |
| 保额 | Sum Assured | 保险公司承担的最高赔偿额 |
| 保费 | Premium | 投保人支付的费用 |
| 缴费年期 | Payment Period | 保费缴纳的年限 |
| 保险期间 | Policy Term | 保险合同的有效期 |
| 现金价值 | Cash Value | 退保时可获得的价值 |
| 保单年度 | Policy Year | 保单生效的年数 |
| 宽限期 | Grace Period | 逾期未缴保费但保障仍有效的期间 |
| 观察期/等待期 | Waiting Period | 保险生效后需等待的一段时间 |
| 免责期 | Exclusion Period | 保险公司不承担责任的时间 |
---
## 🏦 币种识别
| 币种代码 | 币种名称 | 符号 |
|---------|---------|------|
| HKD | 港元 | $, HK$ |
| USD | 美元 | US$, $ |
| CNY | 人民币 | ¥, RMB |
| MOP | 澳门元 | MOP$ |
---
## 📊 缴费年期识别规则
| 文案表述 | 标准化值 |
|---------|---------|
| 整交、趸交 | lump_sum |
| 5年、5年缴 | 5 |
| 10年、10年缴 | 10 |
| 15年、15年缴 | 15 |
| 20年、20年缴 | 20 |
| 25年、25年缴 | 25 |
| 30年、30年缴 | 30 |
| 终身缴费、至终身 | to_age_100 |
---
## 📅 保险期间识别规则
| 文案表述 | 标准化值 |
|---------|---------|
| 终身、100岁 |终身 |
| 至 85 岁 | to_85 |
| 至 88 岁 | to_88 |
| 至 100 岁 | to_100 |
| 1 年、1 年期 | 1_year |
| 20 年、20 年期 | 20_years |
---
## 🎯 年龄范围识别
| 文案表述 | 最低年龄 | 最高年龄 |
|---------|---------|---------|
| 出生 15 天起 | 0 | - |
| 15 日 - 75 岁 | 0 | 75 |
| 18 岁 - 65 岁 | 18 | 65 |
| 30 日 - 70 岁 | 0 | 70 |
**注意**:年龄通常按周岁计算。
---
## 💰 提取功能识别
### 提取方式(Withdrawal Modes)
| 文案表述 | 标准化值 |
|---------|---------|
| 灵活提取 | flexible |
| 固定提取 | fixed |
| 定期提取 | regular |
| 终身年金 | lifetime_annuity |
### 提取周期(Withdrawal Periods)
| 文案表述 | 标准化值 |
|---------|---------|
| 每年 | yearly |
| 每半年 | half_yearly |
| 每季 | quarterly |
| 每月 | monthly |
| 终身 | lifetime |
---
## 🔍 产品名称关键词识别
### 储蓄型关键词
- 传承、累积、增值、储蓄、分红
- 宏挚、迈达、创富、丰誉
- 5G、6G、世代
### 人寿保险关键词
- 人寿、寿险、身故保障
- 定期、终身、保额
### 重疾保险关键词
- 重疾、危疾、重大疾病
- 疾病保障、健康保障
- 守护、守护
---
## 📐 配置生成规则
### form_sn 生成规则
```
{产品类型}-{产品代号}-{币种}
```
**示例**
- `savings-hc77-hkd` - 宏挚 77(储蓄型,港币)
- `life-term-20-usd` - 定期寿险 20 年期(人寿,美元)
- `ci-essential-cny` - 基础重疾险(重疾,人民币)
### 组件选择规则
| 产品类型 | 对应组件 |
|---------|----------|
| 储蓄型 | SavingsTemplate |
| 人寿保险 | LifeInsuranceTemplate |
| 重疾保险 | CriticalIllnessTemplate |
---
## ⚠️ 常见解析错误及修正
### 错误 1:币种识别错误
- **问题**:将 "USD$" 识别为港币
- **修正**:检查符号,$ 后面跟 USD 才是美元
### 错误 2:年龄范围解析错误
- **问题**:将 "15 日 - 75 岁" 的最低年龄识别为 15
- **修正**:15 日应转换为 0 岁
### 错误 3:缴费年期单位错误
- **问题**:将 "20 年期" 识别为 20 年缴费
- **修正**:"年期"通常指保险期间,"缴费"才是缴费年期
### 错误 4:提取方式缺失
- **问题**:储蓄型产品未识别到提取功能
- **修正**:查找"现金价值"、"提取"、"红利"等关键词
---
## 📖 参考规范
### 产品说明书标准结构
1. **产品概述** - 产品定位和特点
2. **投保条件** - 年龄、职业限制
3. **保障内容** - 具体保障项目
4. **缴费方式** - 缴费年期和币种
5. **红利分配**(如有)- 红利政策和领取方式
6. **提取功能**(如有)- 现金价值提取规则
7. **费用说明** - 各项费用标准
8. **案例演示** - 实际投保示例
### 文档解析优先级
1. **表格数据** > 文本描述
2. **明确的数字** > 模糊的表达(如"左右"、"约")
3. **章节标题** > 正文内容
4. **示例数据** > 说明文字
---
## 🔄 更新记录
| 日期 | 更新内容 | 更新人 |
|------|---------|--------|
| 2026-02-13 | 创建初始版本,添加保险产品分类和术语表 | Claude Code |
......@@ -16,11 +16,8 @@
* # 查看待处理文档
* npm run parse:docs -- --list
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import readline from 'readline'
// ========== 配置区 ==========
......@@ -81,6 +78,76 @@ function getDocsToParse() {
}
/**
* 生成 form_sn
*/
export function generateFormSn(config) {
const product_type = config?.product_type || 'product'
const timestamp = Date.now().toString(36)
const name_slug = (config?.product_name || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
return `${product_type}-${name_slug || 'product'}-${timestamp}`
}
/**
* 生成配置代码
*/
export function generateConfigCode(config) {
const formSn = generateFormSn(config)
const isSavings = config.is_savings || config.product_type === 'savings'
const productType = config.product_type || 'life-insurance'
const componentName = isSavings
? 'SavingsTemplate'
: (productType === 'critical-illness' ? 'CriticalIllnessTemplate' : 'LifeInsuranceTemplate')
let code = " /**\n"
code += " * " + config.product_name + "\n"
code += " * @added " + new Date().toISOString() + "\n"
code += " * @source docs/to-parse/" + config.source_file + "\n"
code += " */\n"
code += " '" + formSn + "': {\n"
code += " name: '" + config.product_name + "',\n"
code += " component: '" + componentName + "',\n"
if (isSavings) {
code += " category: 'savings',\n"
}
code += " config: {\n"
if (isSavings) {
code += " currency: '" + config.currency + "',\n"
code += " payment_periods: " + JSON.stringify(config.payment_periods || []) + ",\n"
code += " age_range: { min: " + (config.age_range?.min || 0) + ", max: " + (config.age_range?.max || 75) + " },\n"
code += " insurance_period: '" + (config.insurance_period || '终身') + "',\n"
code += " withdrawal_plan: {\n"
code += " enabled: true,\n"
code += " currencies: ['HKD', 'USD', 'CNY'],\n"
code += " default_currency: '" + config.currency + "',\n"
code += " withdrawal_modes: " + JSON.stringify(config.withdrawal_modes || []) + ",\n"
code += " withdrawal_periods: " + JSON.stringify(config.withdrawal_periods || []) + "\n"
code += " }\n"
} else {
code += " currency: '" + config.currency + "',\n"
code += " payment_periods: " + JSON.stringify(config.payment_periods || []) + ",\n"
code += " age_range: { min: " + (config.age_range?.min || 0) + ", max: " + (config.age_range?.max || 75) + " },\n"
code += " insurance_period: '" + (config.insurance_period || '终身') + "'\n"
}
code += " }\n"
code += " }\n\n"
return { formSn, code }
}
function formatSize(size) {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
/**
* 调用 AI 服务解析文档
*
* 这里使用 skill 工具调用实际的 AI 解析服务
......@@ -90,12 +157,11 @@ async function parseDocumentWithAI(docPath) {
console.log(`\n🤖 正在解析: ${path.basename(docPath)}`)
try {
// 方式 1: 使用 PDF 转 base64
const imageBuffer = fs.readFileSync(docPath)
const base64 = `data:application/pdf;base64,${imageBuffer.toString('base64')}`
// 读取文档内容
const content = fs.readFileSync(docPath, 'utf-8')
// 这里应该调用 AI service(通过 skill 或 API)
// 暂时返回模拟数据用于演示
// 模拟解析:从文档内容中提取配置
// 实际使用时可以调用 AI 服务
const mockConfig = {
product_name: path.basename(docPath, path.extname(docPath)),
product_type: 'savings',
......@@ -117,90 +183,19 @@ async function parseDocumentWithAI(docPath) {
}
/**
* 生成 form_sn
*/
function generateFormSn(config) {
const typePrefix = {
'life-insurance': 'life',
'critical-illness': 'ci',
'savings': 'sav'
}
const prefix = typePrefix[config.product_type] || 'prod'
const timestamp = Date.now().toString(36)
// 从产品名称生成简化的英文标识
const nameSlug = config.product_name
.replace(/[^\w\u4e00-\u9fa5]/g, '')
.replace(/\s+/g, '-')
.toLowerCase()
.substring(0, 20)
return `${prefix}-${nameSlug}-${timestamp}`
}
/**
* 生成配置代码
*/
function generateConfigCode(config) {
const formSn = generateFormSn(config)
const isSavings = config.is_savings || config.product_type === 'savings'
let code = `
/**
* ${config.product_name}
* @added ${new Date().toISOString()}
* @source docs/to-parse/${config.source_file}
*/
'${formSn}': {
name: '${config.product_name}',
component: '${isSavings ? 'SavingsTemplate' : 'InsuranceTemplate'}',
`
if (isSavings) {
code += ` category: 'savings',
config: {
currency: '${config.currency}',
payment_periods: ${JSON.stringify(config.payment_periods || [])},
age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75 } },
insurance_period: '${config.insurance_period || '终身'}',
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: '${config.currency}',
withdrawal_modes: ${JSON.stringify(config.withdrawal_modes || [])},
withdrawal_periods: ${JSON.stringify(config.withdrawal_periods || [])}
}
}
}`
} else {
code += ` config: {
currency: '${config.currency}',
payment_periods: ${JSON.stringify(config.payment_periods || [])},
age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75} },
insurance_period: '${config.insurance_period || '终身'}'
}`
}
code += ` }\n`
return { formSn, code }
}
/**
* 解析单个文档
*/
async function parseSingleFile(filePath) {
const fileName = path.basename(filePath)
console.log(`\n${'='.repeat(60)}`)
console.log(`📄 处理文件: ${fileName}`)
console.log(`${'='.repeat(60)}`)
console.log("\n" + "=".repeat(60))
console.log("📄 处理文件: " + fileName)
console.log("=".repeat(60))
// 解析文档
const config = await parseDocumentWithAI(filePath)
if (!config) {
console.log(`⏭️ 跳过文件: ${fileName} (解析失败)`)
console.log("⏭️ 跳过文件: " + fileName + " (解析失败)")
return { success: false, file: fileName }
}
......@@ -210,63 +205,62 @@ async function parseSingleFile(filePath) {
// 生成配置代码
const { formSn, code } = generateConfigCode(config)
console.log(`\n📝 生成 form_sn: ${formSn}`)
console.log(`📋 生成配置代码:\n${code}`)
console.log("\n📝 生成 form_sn: " + formSn)
console.log("📋 生成配置代码:\n" + code)
return { success: true, formSn, code, file: fileName, config }
}
/**
* 更新配置文件
* @description 使用简单的字符串搜索找到正确的插入位置
*/
export function updateConfigContent(existingContent, newConfigs) {
const templatesStart = existingContent.indexOf('export const PLAN_TEMPLATES')
const templatesEndMarker = '\n}\n\nexport const FEATURE_FLAGS'
const templatesEnd = existingContent.indexOf(templatesEndMarker, templatesStart)
if (templatesStart === -1 || templatesEnd === -1) {
return null
}
const insertContent = newConfigs.map((item, index) => {
const code = item.code.trimEnd()
return index === newConfigs.length - 1 ? code : code + ','
}).join('\n\n')
const before = existingContent.substring(0, templatesEnd)
const after = existingContent.substring(templatesEnd)
const beforeTrimmed = before.replace(/\s+$/, '')
const needsComma = !beforeTrimmed.endsWith(',')
const comma = needsComma ? ',' : ''
return `${beforeTrimmed}${comma}\n\n${insertContent}${after}`
}
function updateConfigFile(newConfigs) {
console.log(`\n${'='.repeat(60)}`)
console.log(`📝 更新配置文件: ${CONFIG_FILE}`)
console.log(`${'='.repeat(60)}`)
console.log("\n" + "=".repeat(60))
console.log("📝 更新配置文件: " + CONFIG_FILE)
console.log("=".repeat(60))
// 备份现有配置
if (fs.existsSync(CONFIG_FILE)) {
ensureDir(BACKUP_DIR)
const backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
fs.copyFileSync(CONFIG_FILE, backupFile)
console.log(`💾 已备份到: ${backupFile}`)
console.log("💾 已备份到: " + backupFile)
}
// 读取现有配置
const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')
const updatedContent = updateConfigContent(existingContent, newConfigs)
// 找到插入位置(在 PLAN_TEMPLATES = { 之后)
const insertPosition = existingContent.indexOf('PLAN_TEMPLATES = {')
if (insertPosition === -1) {
console.error('❌ 找不到 PLAN_TEMPLATES 定义')
return
}
// 在 PLAN_TEMPLATES 对象结束前插入
const endPosition = existingContent.indexOf('}', existingContent.lastIndexOf('}'))
// 计算插入位置:倒数第二个 } 之后(PLAN_TEMPLATES 对象结束)
let insertAfter = existingContent.lastIndexOf('}')
// 找到 PLAN_TEMPLATES 的最后一个 }
const lastObjectEnd = existingContent.lastIndexOf('}', insertAfter - 1)
if (lastObjectEnd === -1) {
console.error('❌ 找不到插入位置')
if (!updatedContent) {
console.error('❌ 无法定位 PLAN_TEMPLATES 插入位置')
return
}
// 生成要插入的内容
const insertContent = newConfigs.map(c => c.code).join(',\n')
// 插入新配置
const updatedContent =
existingContent.slice(0, lastObjectEnd + 1) +
',\n' + insertContent +
existingContent.slice(lastObjectEnd + 1)
// 写入文件
writeFile(CONFIG_FILE, updatedContent)
console.log(`✅ 已更新配置文件,新增 ${newConfigs.length} 个产品`)
console.log("✅ 已更新配置文件,新增 " + newConfigs.length + " 个产品")
}
/**
......@@ -278,9 +272,9 @@ async function parseAllDocs(docs) {
return
}
console.log(`\n${'='.repeat(60)}`)
console.log(`📚 发现 ${docs.length} 个待处理文档`)
console.log(`${'='.repeat(60)}`)
console.log("\n" + "=".repeat(60))
console.log("📚 发现 " + docs.length + " 个待处理文档")
console.log("=".repeat(60))
const results = []
const successResults = []
......@@ -294,24 +288,26 @@ async function parseAllDocs(docs) {
}
// 汇总
console.log(`\n${'='.repeat(60)}`)
console.log(`📊 解析结果汇总`)
console.log(`${'='.repeat(60)}`)
console.log(`总计: ${docs.length} 个文档`)
console.log(`成功: ${successResults.length} 个`)
console.log(`失败: ${results.length - successResults.length} 个`)
console.log("\n" + "=".repeat(60))
console.log("📊 解析结果汇总")
console.log("=".repeat(60))
console.log("总计: " + docs.length + " 个文档")
console.log("成功: " + successResults.length + " 个")
console.log("失败: " + (results.length - successResults.length) + " 个")
// 显示成功的产品
if (successResults.length > 0) {
console.log(`\n✅ 成功解析的产品:`)
console.log("\n✅ 成功解析的产品:")
successResults.forEach(r => {
console.log(` - ${r.formSn}: ${r.config.product_name}`)
console.log(" - " + r.formSn + ": " + r.config.product_name)
})
}
// 更新配置文件
// 更新配置文件
if (successResults.length > 0) {
updateConfigFile(successResults)
} else {
console.log(`\n❌ 没有成功解析的文档,配置文件未更新`)
console.log("\n❌ 没有成功解析的文档,配置文件未更新")
}
}
......@@ -320,57 +316,52 @@ async function parseAllDocs(docs) {
*/
async function main() {
const args = process.argv.slice(2)
const docs = getDocsToParse()
// 检查模式
const listMode = args.includes('--list')
const fileMode = args.find(arg => arg.startsWith('--file='))
console.log('\n🚀 文档解析工具')
console.log(` 文档目录: ${DOCS_DIR}`)
console.log(` 配置文件: ${CONFIG_FILE}`)
console.log(" 文档目录: " + DOCS_DIR)
console.log(" 配置文件: " + CONFIG_FILE)
if (listMode) {
// 列出模式
const docs = getDocsToParse()
console.log(`\n📋 待处理文档列表:`)
console.log("\n📋 待处理文档列表:")
if (docs.length === 0) {
console.log(' (无文档)')
console.log(' (无文档)')
} else {
docs.forEach((doc, index) => {
console.log(` ${index + 1}. ${doc.name} (${formatSize(doc.size)})`)
console.log(" " + (index + 1) + ". " + doc.name + " (" + formatSize(doc.size) + ")")
})
}
} else if (fileMode) {
// 单文件模式
const fileName = fileMode.split('=')[1]
const docs = getDocsToParse()
const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName))
if (targetDoc) {
await parseSingleFile(targetDoc.fullPath)
const result = await parseSingleFile(targetDoc.fullPath)
if (result.success) {
updateConfigFile([result])
}
} else {
console.log(`❌ 找不到文件: ${fileName}`)
console.log("❌ 找不到文件: " + fileName)
}
} else {
// 批量处理模式
const docs = getDocsToParse()
await parseAllDocs(docs)
}
console.log('\n✨ 处理完成!')
}
/**
* 格式化文件大小
*/
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
const isDirectRun = import.meta.url === `file://${process.argv[1]}`
if (isDirectRun) {
main().catch(error => {
console.error('❌ 执行失败:', error)
process.exit(1)
})
}
// 运行
main().catch(error => {
console.error('❌ 执行失败:', error)
process.exit(1)
})
......
import { describe, it, expect } from 'vitest'
import { generateFormSn, generateConfigCode, updateConfigContent } from './parse-docs'
describe('parse-docs 生成逻辑', () => {
it('generateFormSn 使用产品类型前缀', () => {
const form_sn = generateFormSn({
product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
product_type: 'life-insurance'
})
expect(form_sn.startsWith('life-insurance-')).toBe(true)
})
it('generateConfigCode 储蓄配置包含顶层 category', () => {
const result = generateConfigCode({
product_name: '宏挚传承保障计划',
product_type: 'savings',
currency: 'USD',
payment_periods: ['整付'],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
is_savings: true,
withdrawal_modes: ['年龄指定金额'],
withdrawal_periods: ['1年']
})
expect(result.code.includes("component: 'SavingsTemplate'")).toBe(true)
expect(result.code.includes("category: 'savings'")).toBe(true)
expect(result.code.includes("config: {\n category")).toBe(false)
})
it('updateConfigContent 插入到 PLAN_TEMPLATES 末尾', () => {
const base_content = `export const PLAN_TEMPLATES = {
'a': {
name: 'A',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: [],
age_range: { min: 0, max: 1 },
insurance_period: '终身'
}
}
}
export const FEATURE_FLAGS = {}`
const result = updateConfigContent(base_content, [
{
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 }"
}
])
expect(result).toMatch(/'a'[\s\S]*},\n\s+'b'/)
expect(result).toMatch(/'b'[\s\S]*}\n\nexport const FEATURE_FLAGS/)
})
})