feat(docs): 添加文档自动解析工具
- 添加 parse-docs.js 自动化脚本 - 添加 AI 提取器 (ai-extractor.js) - 添加配置生成器 (config-generator.js) - 添加文档说明和使用指南 - 更新 package.json 添加解析脚本命令 - 清理 admin 目录遗留文件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
6 changed files
with
365 additions
and
1 deletions
docs/to-parse/README.md
0 → 100644
| 1 | +# 文档解析工具 | ||
| 2 | + | ||
| 3 | +## 📁 文件夹说明 | ||
| 4 | + | ||
| 5 | +此文件夹用于存放需要解析的保险产品文档,脚本将自动读取并生成配置。 | ||
| 6 | + | ||
| 7 | +## 🚀 使用方法 | ||
| 8 | + | ||
| 9 | +### 1. 添加文档 | ||
| 10 | + | ||
| 11 | +将客户提供的 PDF/Word 文档复制到此文件夹: | ||
| 12 | +``` | ||
| 13 | +docs/to-parse/ | ||
| 14 | +├── WIOP3E 产品说明书.pdf | ||
| 15 | +├── 宏挚传承保障计划.docx | ||
| 16 | +└── MBC PRO 保障计划.pdf | ||
| 17 | +``` | ||
| 18 | + | ||
| 19 | +### 2. 执行解析脚本 | ||
| 20 | + | ||
| 21 | +```bash | ||
| 22 | +# 查看待处理的文档 | ||
| 23 | +pnpm run parse:docs:list | ||
| 24 | + | ||
| 25 | +# 解析所有文档 | ||
| 26 | +pnpm run parse:docs | ||
| 27 | + | ||
| 28 | +# 解析指定文档 | ||
| 29 | +pnpm run parse:docs:file -- --file="产品说明书.pdf" | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +### 3. 查看结果 | ||
| 33 | + | ||
| 34 | +解析成功后,配置会自动添加到 `src/config/plan-templates.js` | ||
| 35 | + | ||
| 36 | +## 📋 支持的文档格式 | ||
| 37 | + | ||
| 38 | +- ✅ PDF (.pdf) | ||
| 39 | +- ✅ Word (.doc, .docx) | ||
| 40 | +- ✅ 纯本文档 (.txt, .md) | ||
| 41 | + | ||
| 42 | +## 🔧 配置 AI 服务 | ||
| 43 | + | ||
| 44 | +脚本使用 skill 工具调用 AI 服务,支持: | ||
| 45 | +- OpenAI GPT-4o Vision | ||
| 46 | +- Anthropic Claude 3.5 Sonnet | ||
| 47 | + | ||
| 48 | +你需要配置 API Key(首次使用时脚本会提示) | ||
| 49 | + | ||
| 50 | +## ⚠️ 注意事项 | ||
| 51 | + | ||
| 52 | +1. **文档命名**:建议使用有意义的文件名,方便识别产品 | ||
| 53 | +2. **手动审核**:生成后请检查配置是否正确 | ||
| 54 | +3. **版本控制**:生成的配置会自动备份 |
| ... | @@ -33,7 +33,10 @@ | ... | @@ -33,7 +33,10 @@ |
| 33 | "changelog:check": "bash scripts/check-changelog.sh 7", | 33 | "changelog:check": "bash scripts/check-changelog.sh 7", |
| 34 | "changelog:check:30": "bash scripts/check-changelog.sh 30", | 34 | "changelog:check:30": "bash scripts/check-changelog.sh 30", |
| 35 | "changelog:check:all": "bash scripts/check-changelog.sh 0", | 35 | "changelog:check:all": "bash scripts/check-changelog.sh 0", |
| 36 | - "prepare": "husky" | 36 | + "prepare": "husky", |
| 37 | + "parse:docs": "node scripts/parse-docs.js", | ||
| 38 | + "parse:docs:list": "node scripts/parse-docs.js --list", | ||
| 39 | + "parse:docs:file": "node scripts/parse-docs.js --file=\"产品说明书.pdf\"" | ||
| 37 | }, | 40 | }, |
| 38 | "browserslist": [ | 41 | "browserslist": [ |
| 39 | "last 3 versions", | 42 | "last 3 versions", | ... | ... |
scripts/parse-docs.js
0 → 100644
This diff is collapsed. Click to expand it.
| ... | @@ -5,6 +5,21 @@ | ... | @@ -5,6 +5,21 @@ |
| 5 | * @module config/plan-templates | 5 | * @module config/plan-templates |
| 6 | * @author Claude Code | 6 | * @author Claude Code |
| 7 | * @created 2026-02-06 | 7 | * @created 2026-02-06 |
| 8 | + * @updated 2026-02-13 - 新增文档解析工具入口 | ||
| 9 | + * | ||
| 10 | + * --- 快速添加新产品(开发工具) --- | ||
| 11 | + * 开发环境可使用以下工具快速添加新产品配置: | ||
| 12 | + * 1. 文档解析工具:/admin/document-parser/index (上传 PDF/Word,AI 自动解析) | ||
| 13 | + * 2. API 配置工具:/admin/document-parser/config (配置 AI 服务) | ||
| 14 | + * | ||
| 15 | + * 使用方式: | ||
| 16 | + * - 上传产品文档 → AI 自动提取配置 → 生成配置代码 → 复制到此文件 | ||
| 17 | + * | ||
| 18 | + * --- 手动添加步骤 --- | ||
| 19 | + * 1. 找到对应的产品分类(人寿/重疾/储蓄) | ||
| 20 | + * 2. 复制现有配置作为模板 | ||
| 21 | + * 3. 修改 name, currency, payment_periods, age_range 等字段 | ||
| 22 | + * 4. 确保 form_sn 唯一(建议使用产品英文标识 + 版本号) | ||
| 8 | */ | 23 | */ |
| 9 | 24 | ||
| 10 | /** | 25 | /** | ... | ... |
src/utils/parsers/ai-extractor.js
0 → 100644
File mode changed
src/utils/parsers/config-generator.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书配置生成器 | ||
| 3 | + * | ||
| 4 | + * @description 将提取的数据转换为 plan-templates.js 的配置格式 | ||
| 5 | + * @file utils/parsers/config-generator | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-13 | ||
| 8 | + */ | ||
| 9 | + | ||
| 10 | +import { generateFormSn, validateConfig } from './ai-extractor' | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 生成完整的配置代码 | ||
| 14 | + * | ||
| 15 | + * @description 将提取的配置数据转换为可写入 plan-templates.js 的代码 | ||
| 16 | + * @param {Object} extractedConfig - 从文档提取的配置 | ||
| 17 | + * @param {Object} options - 生成选项 | ||
| 18 | + * @param {string} options.formSn - 自定义 form_sn(可选) | ||
| 19 | + * @param {boolean} options.includeComment - 是否包含注释(默认 true) | ||
| 20 | + * @returns {Object} { formSn: string, configCode: string, insertPosition: string } | ||
| 21 | + * | ||
| 22 | + * @example | ||
| 23 | + * const result = generateConfigCode({ | ||
| 24 | + * product_name: '宏挚传承保障计划', | ||
| 25 | + * product_type: 'savings', | ||
| 26 | + * ... | ||
| 27 | + * }) | ||
| 28 | + * // 返回: { formSn: 'savings-xxx', configCode: '...', insertPosition: '...' } | ||
| 29 | + */ | ||
| 30 | +export function generateConfigCode(extractedConfig, options = {}) { | ||
| 31 | + const { formSn: customFormSn, includeComment = true } = options | ||
| 32 | + | ||
| 33 | + // 验证配置 | ||
| 34 | + const validation = validateConfig(extractedConfig) | ||
| 35 | + if (!validation.valid) { | ||
| 36 | + throw new Error(`配置验证失败:\n${validation.errors.join('\n')}`) | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + // 生成 form_sn | ||
| 40 | + const formSn = customFormSn || generateFormSn( | ||
| 41 | + extractedConfig.product_name, | ||
| 42 | + extractedConfig.product_type | ||
| 43 | + ) | ||
| 44 | + | ||
| 45 | + // 根据产品类型生成配置 | ||
| 46 | + let configCode = '' | ||
| 47 | + | ||
| 48 | + if (extractedConfig.is_savings) { | ||
| 49 | + configCode = generateSavingsConfig(extractedConfig, formSn, includeComment) | ||
| 50 | + } else if (extractedConfig.product_type === 'life-insurance') { | ||
| 51 | + configCode = generateLifeInsuranceConfig(extractedConfig, formSn, includeComment) | ||
| 52 | + } else if (extractedConfig.product_type === 'critical-illness') { | ||
| 53 | + configCode = generateCriticalIllnessConfig(extractedConfig, formSn, includeComment) | ||
| 54 | + } else { | ||
| 55 | + throw new Error(`不支持的产品类型: ${extractedConfig.product_type}`) | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // 生成插入位置提示 | ||
| 59 | + const insertPosition = generateInsertPositionHint(extractedConfig) | ||
| 60 | + | ||
| 61 | + return { | ||
| 62 | + formSn, | ||
| 63 | + configCode, | ||
| 64 | + insertPosition, | ||
| 65 | + validation | ||
| 66 | + } | ||
| 67 | +} | ||
| 68 | + | ||
| 69 | +/** | ||
| 70 | + * 生成储蓄型产品配置 | ||
| 71 | + * | ||
| 72 | + * @private | ||
| 73 | + */ | ||
| 74 | +function generateSavingsConfig(config, formSn, includeComment) { | ||
| 75 | + const comment = includeComment ? ` | ||
| 76 | + // ${config.product_name} | ||
| 77 | + // form_sn: ${formSn} | ||
| 78 | + // 提取方式: ${config.withdrawal_modes.join(', ')} | ||
| 79 | + // 提取周期: ${config.withdrawal_periods.join(', ')}` : '' | ||
| 80 | + | ||
| 81 | + const paymentPeriodsArray = JSON.stringify(config.payment_periods, null, 2) | ||
| 82 | + const withdrawalModesArray = JSON.stringify(config.withdrawal_modes, null, 2) | ||
| 83 | + const withdrawalPeriodsArray = JSON.stringify(config.withdrawal_periods, null, 2) | ||
| 84 | + | ||
| 85 | + return ` '${formSn}': {${comment} | ||
| 86 | + name: '${config.product_name}', | ||
| 87 | + component: 'SavingsTemplate', | ||
| 88 | + category: 'savings', | ||
| 89 | + config: { | ||
| 90 | + currency: '${config.currency}', | ||
| 91 | + payment_periods: ${paymentPeriodsArray}, | ||
| 92 | + age_range: { min: ${config.age_range.min}, max: ${config.age_range.max} }, | ||
| 93 | + insurance_period: '${config.insurance_period}', | ||
| 94 | + withdrawal_plan: { | ||
| 95 | + enabled: true, | ||
| 96 | + currencies: ['HKD', 'USD', 'CNY'], | ||
| 97 | + default_currency: '${config.currency}', | ||
| 98 | + withdrawal_modes: ${withdrawalModesArray}, | ||
| 99 | + withdrawal_periods: ${withdrawalPeriodsArray} | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + }` | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +/** | ||
| 106 | + * 生成人寿保险产品配置 | ||
| 107 | + * | ||
| 108 | + * @private | ||
| 109 | + */ | ||
| 110 | +function generateLifeInsuranceConfig(config, formSn, includeComment) { | ||
| 111 | + const comment = includeComment ? ` | ||
| 112 | + // ${config.product_name} | ||
| 113 | + // form_sn: ${formSn}` : '' | ||
| 114 | + | ||
| 115 | + const paymentPeriodsArray = JSON.stringify(config.payment_periods, null, 2) | ||
| 116 | + | ||
| 117 | + return ` '${formSn}': {${comment} | ||
| 118 | + name: '${config.product_name}', | ||
| 119 | + component: 'LifeInsuranceTemplate', | ||
| 120 | + config: { | ||
| 121 | + currency: '${config.currency}', | ||
| 122 | + payment_periods: ${paymentPeriodsArray}, | ||
| 123 | + age_range: { min: ${config.age_range.min}, max: ${config.age_range.max} }, | ||
| 124 | + insurance_period: '${config.insurance_period}' | ||
| 125 | + } | ||
| 126 | + }` | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +/** | ||
| 130 | + * 生成重疾保险产品配置 | ||
| 131 | + * | ||
| 132 | + * @private | ||
| 133 | + */ | ||
| 134 | +function generateCriticalIllnessConfig(config, formSn, includeComment) { | ||
| 135 | + const comment = includeComment ? ` | ||
| 136 | + // ${config.product_name} | ||
| 137 | + // form_sn: ${formSn}` : '' | ||
| 138 | + | ||
| 139 | + const paymentPeriodsArray = JSON.stringify(config.payment_periods, null, 2) | ||
| 140 | + | ||
| 141 | + return ` '${formSn}': {${comment} | ||
| 142 | + name: '${config.product_name}', | ||
| 143 | + component: 'CriticalIllnessTemplate', | ||
| 144 | + config: { | ||
| 145 | + currency: '${config.currency}', | ||
| 146 | + payment_periods: ${paymentPeriodsArray}, | ||
| 147 | + age_range: { min: ${config.age_range.min}, max: ${config.age_range.max} }, | ||
| 148 | + insurance_period: '${config.insurance_period}' | ||
| 149 | + } | ||
| 150 | + }` | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +/** | ||
| 154 | + * 生成插入位置提示 | ||
| 155 | + * | ||
| 156 | + * @private | ||
| 157 | + * @param {Object} config - 配置对象 | ||
| 158 | + * @returns {string} 插入位置说明 | ||
| 159 | + */ | ||
| 160 | +function generateInsertPositionHint(config) { | ||
| 161 | + if (config.is_savings) { | ||
| 162 | + return '// 插入到 PLAN_TEMPLATES 对象中的储蓄型产品部分(搜索 "// ====== 储蓄型产品")' | ||
| 163 | + } else if (config.product_type === 'life-insurance') { | ||
| 164 | + return '// 插入到 PLAN_TEMPLATES 对象中的人寿保险部分(搜索 "// 人寿保险产品")' | ||
| 165 | + } else if (config.product_type === 'critical-illness') { | ||
| 166 | + return '// 插入到 PLAN_TEMPLATES 对象中的重疾保险部分(搜索 "// 重疾保险产品")' | ||
| 167 | + } | ||
| 168 | + return '// 插入到 PLAN_TEMPLATES 对象中' | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +/** | ||
| 172 | + * 生成完整的导出代码(包含导入和导出) | ||
| 173 | + * | ||
| 174 | + * @param {Object} config - 提取的配置 | ||
| 175 | + * @param {Object} options - 选项 | ||
| 176 | + * @returns {string} 完整的模块代码 | ||
| 177 | + */ | ||
| 178 | +export function generateFullModuleCode(config, options = {}) { | ||
| 179 | + const { configCode, formSn } = generateConfigCode(config, options) | ||
| 180 | + | ||
| 181 | + return `/** | ||
| 182 | + * 新增产品配置 | ||
| 183 | + * @description ${config.product_name} | ||
| 184 | + * @created ${new Date().toISOString()} | ||
| 185 | + */ | ||
| 186 | + | ||
| 187 | +// 在 PLAN_TEMPLATES 中添加以下配置: | ||
| 188 | +${configCode} | ||
| 189 | + | ||
| 190 | +/** | ||
| 191 | + * 如果需要导出,在文件底部的 export 中添加: | ||
| 192 | + */ | ||
| 193 | +export const ${toCamelCase(formSn)} = PLAN_TEMPLATES['${formSn}'] | ||
| 194 | +` | ||
| 195 | +} | ||
| 196 | + | ||
| 197 | +/** | ||
| 198 | + * 转换为驼峰命名 | ||
| 199 | + * | ||
| 200 | + * @private | ||
| 201 | + */ | ||
| 202 | +function toCamelCase(str) { | ||
| 203 | + return str | ||
| 204 | + .replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '') | ||
| 205 | + .replace(/^(.)/, (c) => c.toLowerCase()) | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +/** | ||
| 209 | + * 批量生成配置代码 | ||
| 210 | + * | ||
| 211 | + * @param {Array} configs - 配置数组 | ||
| 212 | + * @returns {string} 批量配置代码 | ||
| 213 | + */ | ||
| 214 | +export function generateBatchConfigCode(configs) { | ||
| 215 | + const result = configs.map((config, index) => { | ||
| 216 | + const { configCode } = generateConfigCode(config, { | ||
| 217 | + formSn: `batch-product-${Date.now()}-${index}`, | ||
| 218 | + includeComment: true | ||
| 219 | + }) | ||
| 220 | + return configCode | ||
| 221 | + }) | ||
| 222 | + | ||
| 223 | + return result.join(',\n\n') | ||
| 224 | +} | ||
| 225 | + | ||
| 226 | +/** | ||
| 227 | + * 预览配置数据(用于确认) | ||
| 228 | + * | ||
| 229 | + * @param {Object} config - 配置对象 | ||
| 230 | + * @returns {Object} 格式化的预览数据 | ||
| 231 | + */ | ||
| 232 | +export function previewConfig(config) { | ||
| 233 | + return { | ||
| 234 | + '产品名称': config.product_name, | ||
| 235 | + '产品类型': getProductTypeLabel(config.product_type), | ||
| 236 | + '币种': config.currency, | ||
| 237 | + '缴费年期': config.payment_periods?.join('、') || '-', | ||
| 238 | + '年龄范围': `${config.age_range?.min || 0} - ${config.age_range?.max || 75} 岁`, | ||
| 239 | + '保险期间': config.insurance_period, | ||
| 240 | + '是否储蓄型': config.is_savings ? '是' : '否', | ||
| 241 | + '提取方式': config.withdrawal_modes?.join('、') || '-', | ||
| 242 | + '提取周期': config.withdrawal_periods?.join('、') || '-' | ||
| 243 | + } | ||
| 244 | +} | ||
| 245 | + | ||
| 246 | +/** | ||
| 247 | + * 获取产品类型标签 | ||
| 248 | + * | ||
| 249 | + * @private | ||
| 250 | + */ | ||
| 251 | +function getProductTypeLabel(type) { | ||
| 252 | + const labels = { | ||
| 253 | + 'life-insurance': '人寿保险', | ||
| 254 | + 'critical-illness': '重疾保险', | ||
| 255 | + 'savings': '储蓄型' | ||
| 256 | + } | ||
| 257 | + return labels[type] || type | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +/** | ||
| 261 | + * 生成差异对比(新配置 vs 现有配置) | ||
| 262 | + * | ||
| 263 | + * @param {Object} newConfig - 新配置 | ||
| 264 | + * @param {Object} existingConfig - 现有配置 | ||
| 265 | + * @returns {Object} 差异对象 | ||
| 266 | + */ | ||
| 267 | +export function compareConfigs(newConfig, existingConfig) { | ||
| 268 | + const differences = { | ||
| 269 | + added: [], | ||
| 270 | + removed: [], | ||
| 271 | + changed: [] | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + // 比较缴费年期 | ||
| 275 | + if (newConfig.payment_periods && existingConfig.payment_periods) { | ||
| 276 | + const newPeriods = newConfig.payment_periods.filter(p => !existingConfig.payment_periods.includes(p)) | ||
| 277 | + const removedPeriods = existingConfig.payment_periods.filter(p => !newConfig.payment_periods.includes(p)) | ||
| 278 | + | ||
| 279 | + if (newPeriods.length > 0) differences.changed.push(`新增缴费年期: ${newPeriods.join(', ')}`) | ||
| 280 | + if (removedPeriods.length > 0) differences.changed.push(`移除缴费年期: ${removedPeriods.join(', ')}`) | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + // 比较年龄范围 | ||
| 284 | + if (newConfig.age_range && existingConfig.age_range) { | ||
| 285 | + if (newConfig.age_range.min !== existingConfig.age_range.min || | ||
| 286 | + newConfig.age_range.max !== existingConfig.age_range.max) { | ||
| 287 | + differences.changed.push(`年龄范围: ${existingConfig.age_range.min}-${existingConfig.age_range.max} → ${newConfig.age_range.min}-${newConfig.age_range.max}`) | ||
| 288 | + } | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + return differences | ||
| 292 | +} |
-
Please register or login to post a comment