feat(parse): 增强文档解析工具链和智能字段提取
主要改进: - 优化 smartExtractList() 智能字段提取器 - 增强产品边界检测逻辑 - 完善 MCP 解析切换功能 - 优化 mockData 产品列表数据结构 - 更新计划书模板配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
7 changed files
with
597 additions
and
60 deletions
| ... | @@ -5,3 +5,4 @@ | ... | @@ -5,3 +5,4 @@ |
| 5 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078080604.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:08:00.605Z"} | 5 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078080604.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:08:00.605Z"} |
| 6 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078351660.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T14:12:31.660Z"} | 6 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078351660.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T14:12:31.660Z"} |
| 7 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771080130974.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-55bcffc2"],"at":"2026-02-14T14:42:10.974Z"} | 7 | {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771080130974.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-55bcffc2"],"at":"2026-02-14T14:42:10.974Z"} |
| 8 | +{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771137003708.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["life-insurance-3-d8fde07d"],"at":"2026-02-15T06:30:03.709Z"} | ... | ... |
| ... | @@ -28,3 +28,11 @@ | ... | @@ -28,3 +28,11 @@ |
| 28 | {"at":"2026-02-15T02:19:44.455Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-15T02:19:44.438Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"8年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | 28 | {"at":"2026-02-15T02:19:44.455Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-15T02:19:44.438Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"8年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} |
| 29 | {"at":"2026-02-15T02:20:31.001Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.982Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} | 29 | {"at":"2026-02-15T02:20:31.001Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.982Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} |
| 30 | {"at":"2026-02-15T03:18:47.647Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":54,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.616Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} | 30 | {"at":"2026-02-15T03:18:47.647Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":54,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.616Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}} |
| 31 | +{"at":"2026-02-15T05:43:33.427Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-4-a204daaf","product_name":"计划书模版4","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-4-a204daaf"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版4\n+ * @added 2026-02-15T05:43:33.409Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'savings-4-a204daaf': {\n+ name: '计划书模版4',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 32 | +{"at":"2026-02-15T05:45:13.034Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-4-a204daaf","product_name":"计划书模版4","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-4-a204daaf"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版4\n+ * @added 2026-02-15T05:45:13.016Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'savings-4-a204daaf': {\n+ name: '计划书模版4',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}} | ||
| 33 | +{"at":"2026-02-15T06:08:21.190Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":51,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:08:21.159Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}} | ||
| 34 | +{"at":"2026-02-15T06:12:50.918Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:12:50.901Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"终身\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}} | ||
| 35 | +{"at":"2026-02-15T06:13:11.114Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:13:11.097Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"终身\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}} | ||
| 36 | +{"at":"2026-02-15T06:17:26.826Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":38,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:17:26.808Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"20年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}} | ||
| 37 | +{"at":"2026-02-15T06:17:45.723Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":38,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:17:45.705Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"20年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}} | ||
| 38 | +{"at":"2026-02-15T06:30:03.709Z","mode":"single","options":{"dry_run":false},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null}} | ... | ... |
| ... | @@ -15,6 +15,12 @@ | ... | @@ -15,6 +15,12 @@ |
| 15 | * | 15 | * |
| 16 | * # 查看待处理文档 | 16 | * # 查看待处理文档 |
| 17 | * npm run parse:docs -- --list | 17 | * npm run parse:docs -- --list |
| 18 | + * | ||
| 19 | + * # 应用审核通过的配置 | ||
| 20 | + * npm run parse:docs -- --apply=计划书模版4 | ||
| 21 | + * | ||
| 22 | + * # 预览应用配置(不实际修改) | ||
| 23 | + * npm run parse:docs -- --apply=计划书模版4 --dry-run | ||
| 18 | */ | 24 | */ |
| 19 | import crypto from 'crypto' | 25 | import crypto from 'crypto' |
| 20 | import fs from 'fs' | 26 | import fs from 'fs' |
| ... | @@ -976,17 +982,27 @@ ${code.trim()} | ... | @@ -976,17 +982,27 @@ ${code.trim()} |
| 976 | 982 | ||
| 977 | ## 📋 审核后操作 | 983 | ## 📋 审核后操作 |
| 978 | 984 | ||
| 979 | -### 确认无误 | 985 | +### 方法 1:自动应用(推荐) |
| 986 | +\`\`\`bash | ||
| 987 | +# 预览变更(不实际修改) | ||
| 988 | +pnpm parse:docs -- --apply=${baseFileName} --dry-run | ||
| 989 | + | ||
| 990 | +# 确认无误后,正式应用 | ||
| 991 | +pnpm parse:docs -- --apply=${baseFileName} | ||
| 992 | + | ||
| 993 | +# 说明: | ||
| 994 | +# 1. 自动提取配置代码并插入到 src/config/plan-templates.js | ||
| 995 | +# 2. 自动创建备份文件(docs/parsed-backup/) | ||
| 996 | +# 3. 自动将审核文件移动到 docs/parse-audit/approved/ | ||
| 997 | +\`\`\` | ||
| 998 | + | ||
| 999 | +### 方法 2:手动操作 | ||
| 980 | \`\`\`bash | 1000 | \`\`\`bash |
| 981 | # 1. 移动到 approved 目录 | 1001 | # 1. 移动到 approved 目录 |
| 982 | mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\ | 1002 | mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\ |
| 983 | docs/parse-audit/approved/ | 1003 | docs/parse-audit/approved/ |
| 984 | 1004 | ||
| 985 | -# 2. 合并到正式配置 | 1005 | +# 2. 手动复制"生成配置片段"到 src/config/plan-templates.js |
| 986 | -# 手动复制或使用工具合并到 src/config/plan-templates.js | ||
| 987 | - | ||
| 988 | -# 3. 删除待审核文件(可选) | ||
| 989 | -rm docs/parse-audit/pending/${baseFileName}/${auditFileName} | ||
| 990 | \`\`\` | 1006 | \`\`\` |
| 991 | 1007 | ||
| 992 | ### 需要修改 | 1008 | ### 需要修改 |
| ... | @@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) { | ... | @@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) { |
| 1402 | return true | 1418 | return true |
| 1403 | } | 1419 | } |
| 1404 | 1420 | ||
| 1421 | +/** | ||
| 1422 | + * 从审核文件应用配置到 plan-templates.js | ||
| 1423 | + * | ||
| 1424 | + * @description 读取审核 markdown 文件,提取配置代码,插入到配置文件中 | ||
| 1425 | + * @param {string} auditFileName - 审核文件名(不含路径,如 "计划书模版4") | ||
| 1426 | + * @param {Object} options - 选项 | ||
| 1427 | + * @param {boolean} options.dry_run - 是否仅预览 | ||
| 1428 | + * @returns {Object} 应用结果 | ||
| 1429 | + */ | ||
| 1430 | +function applyAuditFile(auditFileName, options = {}) { | ||
| 1431 | + const PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending') | ||
| 1432 | + const APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved') | ||
| 1433 | + | ||
| 1434 | + // 1. 查找审核文件 | ||
| 1435 | + let auditFile = null | ||
| 1436 | + let sourceDir = null | ||
| 1437 | + | ||
| 1438 | + // 先在 pending 目录查找 | ||
| 1439 | + const pendingDirs = fs.existsSync(PENDING_DIR) ? fs.readdirSync(PENDING_DIR) : [] | ||
| 1440 | + for (const dir of pendingDirs) { | ||
| 1441 | + const dirPath = path.join(PENDING_DIR, dir) | ||
| 1442 | + if (fs.statSync(dirPath).isDirectory()) { | ||
| 1443 | + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md')) | ||
| 1444 | + for (const file of files) { | ||
| 1445 | + // 匹配文件名或目录名 | ||
| 1446 | + const normalizedName = dir.replace(/\s+/g, '').toLowerCase() | ||
| 1447 | + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase() | ||
| 1448 | + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) { | ||
| 1449 | + auditFile = path.join(dirPath, file) | ||
| 1450 | + sourceDir = PENDING_DIR | ||
| 1451 | + break | ||
| 1452 | + } | ||
| 1453 | + } | ||
| 1454 | + } | ||
| 1455 | + if (auditFile) break | ||
| 1456 | + } | ||
| 1457 | + | ||
| 1458 | + // 如果 pending 没找到,在 approved 目录查找 | ||
| 1459 | + if (!auditFile && fs.existsSync(APPROVED_DIR)) { | ||
| 1460 | + const approvedFiles = fs.readdirSync(APPROVED_DIR).filter(f => f.endsWith('.md')) | ||
| 1461 | + for (const file of approvedFiles) { | ||
| 1462 | + // 从文件名提取产品名(格式:YYYY-MM-DD-产品名.md) | ||
| 1463 | + const match = file.match(/^\d{4}-\d{2}-\d{2}-(.+)\.md$/) | ||
| 1464 | + if (match) { | ||
| 1465 | + const normalizedName = match[1].replace(/\s+/g, '').toLowerCase() | ||
| 1466 | + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase() | ||
| 1467 | + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) { | ||
| 1468 | + auditFile = path.join(APPROVED_DIR, file) | ||
| 1469 | + sourceDir = APPROVED_DIR | ||
| 1470 | + break | ||
| 1471 | + } | ||
| 1472 | + } | ||
| 1473 | + } | ||
| 1474 | + } | ||
| 1475 | + | ||
| 1476 | + if (!auditFile) { | ||
| 1477 | + console.error("❌ 找不到审核文件: " + auditFileName) | ||
| 1478 | + console.log(" 搜索目录:") | ||
| 1479 | + console.log(" - docs/parse-audit/pending/") | ||
| 1480 | + console.log(" - docs/parse-audit/approved/") | ||
| 1481 | + return { ok: false, reason: 'file_not_found' } | ||
| 1482 | + } | ||
| 1483 | + | ||
| 1484 | + console.log("\n📄 找到审核文件: " + auditFile) | ||
| 1485 | + | ||
| 1486 | + // 2. 读取审核文件内容 | ||
| 1487 | + const content = fs.readFileSync(auditFile, 'utf-8') | ||
| 1488 | + | ||
| 1489 | + // 3. 提取配置代码片段 | ||
| 1490 | + const configMatch = content.match(/## 🧩 生成配置片段\s*\n+```javascript\s*\n([\s\S]*?)```/) | ||
| 1491 | + if (!configMatch) { | ||
| 1492 | + console.error("❌ 无法从审核文件中提取配置代码") | ||
| 1493 | + return { ok: false, reason: 'config_not_found' } | ||
| 1494 | + } | ||
| 1495 | + | ||
| 1496 | + const configCode = configMatch[1].trim() | ||
| 1497 | + console.log("\n📝 提取的配置代码:") | ||
| 1498 | + console.log("-".repeat(40)) | ||
| 1499 | + console.log(configCode) | ||
| 1500 | + console.log("-".repeat(40)) | ||
| 1501 | + | ||
| 1502 | + // 4. 提取 form_sn 用于去重检查 | ||
| 1503 | + const formSnMatch = configCode.match(/'([^']+)':\s*\{/) | ||
| 1504 | + const formSn = formSnMatch ? formSnMatch[1] : null | ||
| 1505 | + | ||
| 1506 | + if (!formSn) { | ||
| 1507 | + console.error("❌ 无法从配置代码中提取 form_sn") | ||
| 1508 | + return { ok: false, reason: 'form_sn_not_found' } | ||
| 1509 | + } | ||
| 1510 | + | ||
| 1511 | + console.log("\n🔑 form_sn: " + formSn) | ||
| 1512 | + | ||
| 1513 | + // 5. 读取现有配置文件 | ||
| 1514 | + const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8') | ||
| 1515 | + | ||
| 1516 | + // 检查是否已存在 | ||
| 1517 | + if (existingContent.includes(`'${formSn}':`)) { | ||
| 1518 | + console.error("❌ 配置文件中已存在 form_sn: " + formSn) | ||
| 1519 | + console.log(" 如需更新,请先手动删除旧配置") | ||
| 1520 | + return { ok: false, reason: 'duplicate', formSn } | ||
| 1521 | + } | ||
| 1522 | + | ||
| 1523 | + // 6. 找到插入位置(PLAN_TEMPLATES 对象的结束位置) | ||
| 1524 | + // 查找最后一个产品配置的结束位置 | ||
| 1525 | + const insertPattern = /(\n\s*'\w+[^']+':\s*\{[\s\S]*?\n\s*\}\s*,?\s*)(\n\})/ | ||
| 1526 | + const match = existingContent.match(insertPattern) | ||
| 1527 | + | ||
| 1528 | + if (!match) { | ||
| 1529 | + console.error("❌ 无法定位插入位置") | ||
| 1530 | + return { ok: false, reason: 'insert_not_found' } | ||
| 1531 | + } | ||
| 1532 | + | ||
| 1533 | + // 7. 构建新配置(确保有逗号) | ||
| 1534 | + let newConfigEntry = configCode | ||
| 1535 | + // 确保配置以逗号结尾 | ||
| 1536 | + if (!newConfigEntry.trimEnd().endsWith(',')) { | ||
| 1537 | + newConfigEntry = newConfigEntry.trimEnd() + ',' | ||
| 1538 | + } | ||
| 1539 | + | ||
| 1540 | + // 8. 插入配置 | ||
| 1541 | + const insertPosition = match.index + match[1].length | ||
| 1542 | + const updatedContent = | ||
| 1543 | + existingContent.slice(0, insertPosition) + | ||
| 1544 | + '\n\n' + | ||
| 1545 | + newConfigEntry + | ||
| 1546 | + existingContent.slice(insertPosition) | ||
| 1547 | + | ||
| 1548 | + if (options.dry_run) { | ||
| 1549 | + console.log("\n🧪 dry-run 模式,变更预览:") | ||
| 1550 | + console.log("-".repeat(40)) | ||
| 1551 | + console.log("将插入以下配置:") | ||
| 1552 | + console.log(newConfigEntry) | ||
| 1553 | + console.log("-".repeat(40)) | ||
| 1554 | + return { ok: true, dry_run: true, formSn } | ||
| 1555 | + } | ||
| 1556 | + | ||
| 1557 | + // 9. 备份并写入 | ||
| 1558 | + let backupFile = null | ||
| 1559 | + if (fs.existsSync(CONFIG_FILE)) { | ||
| 1560 | + ensureDir(BACKUP_DIR) | ||
| 1561 | + backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`) | ||
| 1562 | + fs.copyFileSync(CONFIG_FILE, backupFile) | ||
| 1563 | + console.log("\n💾 已备份到: " + backupFile) | ||
| 1564 | + } | ||
| 1565 | + | ||
| 1566 | + writeFile(CONFIG_FILE, updatedContent) | ||
| 1567 | + console.log("\n✅ 配置已更新: " + CONFIG_FILE) | ||
| 1568 | + | ||
| 1569 | + writeBackupLog({ | ||
| 1570 | + action: 'apply_audit', | ||
| 1571 | + backup_file: backupFile, | ||
| 1572 | + target_file: CONFIG_FILE, | ||
| 1573 | + audit_file: auditFile, | ||
| 1574 | + form_sn: formSn, | ||
| 1575 | + at: new Date().toISOString() | ||
| 1576 | + }) | ||
| 1577 | + | ||
| 1578 | + // 10. 移动审核文件到 approved 目录(如果是从 pending 来的) | ||
| 1579 | + if (sourceDir === PENDING_DIR) { | ||
| 1580 | + ensureDir(APPROVED_DIR) | ||
| 1581 | + const fileName = path.basename(auditFile) | ||
| 1582 | + const approvedPath = path.join(APPROVED_DIR, fileName) | ||
| 1583 | + | ||
| 1584 | + // 检查目标是否已存在 | ||
| 1585 | + if (fs.existsSync(approvedPath)) { | ||
| 1586 | + console.log("⚠️ approved 目录已存在同名文件,跳过移动") | ||
| 1587 | + } else { | ||
| 1588 | + fs.renameSync(auditFile, approvedPath) | ||
| 1589 | + console.log("📁 审核文件已移动到: " + approvedPath) | ||
| 1590 | + | ||
| 1591 | + // 删除空的 pending 子目录 | ||
| 1592 | + const pendingSubDir = path.dirname(auditFile) | ||
| 1593 | + const remainingFiles = fs.readdirSync(pendingSubDir).filter(f => !f.startsWith('.')) | ||
| 1594 | + if (remainingFiles.length === 0) { | ||
| 1595 | + fs.rmdirSync(pendingSubDir) | ||
| 1596 | + console.log("🗑️ 已删除空目录: " + pendingSubDir) | ||
| 1597 | + } | ||
| 1598 | + } | ||
| 1599 | + } | ||
| 1600 | + | ||
| 1601 | + return { ok: true, formSn, backupFile } | ||
| 1602 | +} | ||
| 1603 | + | ||
| 1405 | function updateConfigFile(newConfigs, options = {}) { | 1604 | function updateConfigFile(newConfigs, options = {}) { |
| 1406 | console.log("\n" + "=".repeat(60)) | 1605 | console.log("\n" + "=".repeat(60)) |
| 1407 | console.log("📝 更新配置文件: " + CONFIG_FILE) | 1606 | console.log("📝 更新配置文件: " + CONFIG_FILE) |
| ... | @@ -1563,9 +1762,16 @@ async function main() { | ... | @@ -1563,9 +1762,16 @@ async function main() { |
| 1563 | const listMode = args.includes('--list') | 1762 | const listMode = args.includes('--list') |
| 1564 | const fileMode = args.find(arg => arg.startsWith('--file=')) | 1763 | const fileMode = args.find(arg => arg.startsWith('--file=')) |
| 1565 | const writeMode = args.includes('--write-config') | 1764 | const writeMode = args.includes('--write-config') |
| 1566 | - const dryRunMode = args.includes('--dry-run') || !writeMode | ||
| 1567 | const rollbackMode = args.find(arg => arg.startsWith('--rollback=')) | 1765 | const rollbackMode = args.find(arg => arg.startsWith('--rollback=')) |
| 1568 | const statusMode = args.includes('--status') | 1766 | const statusMode = args.includes('--status') |
| 1767 | + const applyMode = args.find(arg => arg.startsWith('--apply=')) | ||
| 1768 | + | ||
| 1769 | + // dry-run 逻辑: | ||
| 1770 | + // 1. 如果显式指定 --dry-run,则 dry-run | ||
| 1771 | + // 2. 如果是 apply 模式,默认不 dry-run(除非显式指定) | ||
| 1772 | + // 3. 如果是解析模式,默认 dry-run(除非显式指定 --write-config) | ||
| 1773 | + const explicitDryRun = args.includes('--dry-run') | ||
| 1774 | + const dryRunMode = applyMode ? explicitDryRun : (!writeMode && !explicitDryRun || explicitDryRun) | ||
| 1569 | 1775 | ||
| 1570 | // 检查解析器选择 | 1776 | // 检查解析器选择 |
| 1571 | const parserModeArg = args.find(arg => arg.startsWith('--parser=')) | 1777 | const parserModeArg = args.find(arg => arg.startsWith('--parser=')) |
| ... | @@ -1586,6 +1792,11 @@ async function main() { | ... | @@ -1586,6 +1792,11 @@ async function main() { |
| 1586 | if (rollbackMode) { | 1792 | if (rollbackMode) { |
| 1587 | const backupFile = rollbackMode.split('=')[1] | 1793 | const backupFile = rollbackMode.split('=')[1] |
| 1588 | rollbackConfigFile(backupFile) | 1794 | rollbackConfigFile(backupFile) |
| 1795 | + } else if (applyMode) { | ||
| 1796 | + // 从审核文件应用配置 | ||
| 1797 | + const auditFileName = applyMode.split('=')[1] | ||
| 1798 | + const applyOptions = { dry_run: dryRunMode } | ||
| 1799 | + applyAuditFile(auditFileName, applyOptions) | ||
| 1589 | } else if (listMode) { | 1800 | } else if (listMode) { |
| 1590 | // 列出模式 | 1801 | // 列出模式 |
| 1591 | const docs = getDocsToParse() | 1802 | const docs = getDocsToParse() | ... | ... |
| ... | @@ -16,6 +16,7 @@ | ... | @@ -16,6 +16,7 @@ |
| 16 | * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 | 16 | * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 |
| 17 | * - FA 宏浚傳承保障計劃 | 17 | * - FA 宏浚傳承保障計劃 |
| 18 | * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項 | 18 | * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項 |
| 19 | + * - LV3 长宁終身壽險計劃3 | ||
| 19 | */ | 20 | */ |
| 20 | const PRODUCT_TITLE_PATTERNS = [ | 21 | const PRODUCT_TITLE_PATTERNS = [ |
| 21 | // 产品代码 + 产品名称 + 可选后缀 | 22 | // 产品代码 + 产品名称 + 可选后缀 |
| ... | @@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [ | ... | @@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [ |
| 31 | /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm, | 32 | /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm, |
| 32 | 33 | ||
| 33 | // 产品代码开头的行 | 34 | // 产品代码开头的行 |
| 34 | - /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm | 35 | + /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm, |
| 36 | + | ||
| 37 | + // 新增:产品代码 + 产品名称 + 数字后缀(如 "LV3 长宁終身壽險計劃3") | ||
| 38 | + /^([A-Z]{2,3}\d?)\s+([^\n]{2,25}?(?:計劃|计划|壽險|壽险)\d?)/gm | ||
| 35 | ] | 39 | ] |
| 36 | 40 | ||
| 37 | /** | 41 | /** |
| 38 | * 产品代码前缀列表(用于优先匹配) | 42 | * 产品代码前缀列表(用于优先匹配) |
| 39 | */ | 43 | */ |
| 40 | const PRODUCT_CODE_PREFIXES = [ | 44 | const PRODUCT_CODE_PREFIXES = [ |
| 41 | - 'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR', | 45 | + 'GS', 'GC', 'FA', 'LV2', 'LV3', 'LV', 'CR', 'HR', 'PR', 'SR', |
| 42 | 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR' | 46 | 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR' |
| 43 | ] | 47 | ] |
| 44 | 48 | ||
| ... | @@ -62,10 +66,11 @@ export function detectProductCount(content) { | ... | @@ -62,10 +66,11 @@ export function detectProductCount(content) { |
| 62 | export function findProductTitles(content) { | 66 | export function findProductTitles(content) { |
| 63 | const products = [] | 67 | const products = [] |
| 64 | const seenCodes = new Set() | 68 | const seenCodes = new Set() |
| 69 | + const seenNames = new Set() | ||
| 65 | 70 | ||
| 66 | // 策略1: 优先匹配产品代码前缀 | 71 | // 策略1: 优先匹配产品代码前缀 |
| 67 | for (const prefix of PRODUCT_CODE_PREFIXES) { | 72 | for (const prefix of PRODUCT_CODE_PREFIXES) { |
| 68 | - // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" | 73 | + // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 或 "LV3 长宁終身壽險計劃3" |
| 69 | const regex = new RegExp( | 74 | const regex = new RegExp( |
| 70 | `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`, | 75 | `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`, |
| 71 | 'gm' | 76 | 'gm' |
| ... | @@ -76,9 +81,11 @@ export function findProductTitles(content) { | ... | @@ -76,9 +81,11 @@ export function findProductTitles(content) { |
| 76 | const code = match[1] | 81 | const code = match[1] |
| 77 | const name = match[2].trim() | 82 | const name = match[2].trim() |
| 78 | 83 | ||
| 79 | - // 去重 | 84 | + // 去重(基于代码或名称) |
| 80 | - if (seenCodes.has(code)) continue | 85 | + const nameKey = name.replace(/\s+/g, '').toLowerCase() |
| 86 | + if (seenCodes.has(code) || seenNames.has(nameKey)) continue | ||
| 81 | seenCodes.add(code) | 87 | seenCodes.add(code) |
| 88 | + seenNames.add(nameKey) | ||
| 82 | 89 | ||
| 83 | products.push({ | 90 | products.push({ |
| 84 | index: match.index, | 91 | index: match.index, |
| ... | @@ -99,15 +106,52 @@ export function findProductTitles(content) { | ... | @@ -99,15 +106,52 @@ export function findProductTitles(content) { |
| 99 | const fullTitle = match[0].trim() | 106 | const fullTitle = match[0].trim() |
| 100 | if (fullTitle.length < 5) continue // 过滤太短的匹配 | 107 | if (fullTitle.length < 5) continue // 过滤太短的匹配 |
| 101 | 108 | ||
| 109 | + const code = match[1] || null | ||
| 110 | + const name = match[2] || fullTitle | ||
| 111 | + | ||
| 112 | + // 去重 | ||
| 113 | + const nameKey = name.replace(/\s+/g, '').toLowerCase() | ||
| 114 | + if (seenNames.has(nameKey)) continue | ||
| 115 | + if (code) seenCodes.add(code) | ||
| 116 | + seenNames.add(nameKey) | ||
| 117 | + | ||
| 102 | products.push({ | 118 | products.push({ |
| 103 | index: match.index, | 119 | index: match.index, |
| 104 | - code: match[1] || null, | 120 | + code, |
| 105 | - name: match[2] || fullTitle, | 121 | + name, |
| 106 | fullTitle | 122 | fullTitle |
| 107 | }) | 123 | }) |
| 108 | } | 124 | } |
| 109 | } | 125 | } |
| 110 | 126 | ||
| 127 | + // 策略3: 新增 - 识别包含"计划"但不包含产品代码的行(纯计划书名称) | ||
| 128 | + // 适用于标题如 "宏挚传承保障计划" 或 "长宁终身寿险计划3" | ||
| 129 | + if (products.length === 0) { | ||
| 130 | + const planNameRegex = /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/gm | ||
| 131 | + let match | ||
| 132 | + | ||
| 133 | + while ((match = planNameRegex.exec(content)) !== null) { | ||
| 134 | + const fullTitle = match[1].trim() | ||
| 135 | + | ||
| 136 | + // 排除太短或包含其他关键词的行 | ||
| 137 | + if (fullTitle.length < 5 || fullTitle.includes('選項') || fullTitle.includes('选项')) continue | ||
| 138 | + | ||
| 139 | + // 检查是否是产品名称(通常包含"保障"、"保险"、"寿险"等关键词) | ||
| 140 | + if (/(?:保障|保险|壽險|壽险|传承|家传)/.test(fullTitle)) { | ||
| 141 | + const nameKey = fullTitle.replace(/\s+/g, '').toLowerCase() | ||
| 142 | + if (!seenNames.has(nameKey)) { | ||
| 143 | + seenNames.add(nameKey) | ||
| 144 | + products.push({ | ||
| 145 | + index: match.index, | ||
| 146 | + code: null, | ||
| 147 | + name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明 | ||
| 148 | + fullTitle | ||
| 149 | + }) | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + | ||
| 111 | // 按出现位置排序 | 155 | // 按出现位置排序 |
| 112 | products.sort((a, b) => a.index - b.index) | 156 | products.sort((a, b) => a.index - b.index) |
| 113 | 157 | ... | ... |
| ... | @@ -8,6 +8,107 @@ | ... | @@ -8,6 +8,107 @@ |
| 8 | */ | 8 | */ |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | + * 保险缴费年期匹配模式 | ||
| 12 | + * | ||
| 13 | + * @description 用于识别各种格式的缴费年期选项 | ||
| 14 | + * @constant {Object} | ||
| 15 | + */ | ||
| 16 | +const PAYMENT_PERIOD_PATTERNS = { | ||
| 17 | + // 一次性缴费关键词 | ||
| 18 | + singlePayment: ['整付', '趸交', '躉繳', 'lump sum', 'single'], | ||
| 19 | + // 数字+年格式(3年、5年、10年等) | ||
| 20 | + yearlyPattern: /^(\d+)\s*年$/, | ||
| 21 | + // 至X岁格式 | ||
| 22 | + untilAgePattern: /^至?\s*(\d+)\s*岁$/, | ||
| 23 | + // 列表项格式(- 3年、• 5年等) | ||
| 24 | + listItemPattern: /^[-•·]\s*(.+)$/ | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * 检查是否为有效的缴费年期选项 | ||
| 29 | + * | ||
| 30 | + * @param {string} text - 待检查文本 | ||
| 31 | + * @returns {boolean} 是否为有效缴费年期 | ||
| 32 | + */ | ||
| 33 | +const isValidPaymentPeriod = (text) => { | ||
| 34 | + const trimmed = text.trim() | ||
| 35 | + | ||
| 36 | + // 排除无效关键字 | ||
| 37 | + if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) { | ||
| 38 | + return false | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // 1. 匹配 "X年" 格式 | ||
| 42 | + if (PAYMENT_PERIOD_PATTERNS.yearlyPattern.test(trimmed)) { | ||
| 43 | + return true | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + // 2. 匹配 "至X岁" 格式 | ||
| 47 | + if (PAYMENT_PERIOD_PATTERNS.untilAgePattern.test(trimmed)) { | ||
| 48 | + return true | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 3. 匹配一次性缴费关键词 | ||
| 52 | + if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw => | ||
| 53 | + trimmed.toLowerCase() === kw.toLowerCase() | ||
| 54 | + )) { | ||
| 55 | + return true | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // 4. 匹配列表项格式(提取内容后递归检查) | ||
| 59 | + const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern) | ||
| 60 | + if (listItemMatch) { | ||
| 61 | + return isValidPaymentPeriod(listItemMatch[1]) | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // 5. 通用匹配:包含"年"或"岁"或"交"的短文本(2-10字符) | ||
| 65 | + if (trimmed.length >= 2 && trimmed.length <= 10) { | ||
| 66 | + if (/年|岁|交|付/.test(trimmed)) { | ||
| 67 | + return true | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + return false | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +/** | ||
| 75 | + * 标准化缴费年期选项 | ||
| 76 | + * | ||
| 77 | + * @param {string} text - 原始文本 | ||
| 78 | + * @returns {string} 标准化后的文本 | ||
| 79 | + */ | ||
| 80 | +const normalizePaymentPeriod = (text) => { | ||
| 81 | + const trimmed = text.trim() | ||
| 82 | + | ||
| 83 | + // 标准化一次性缴费 | ||
| 84 | + if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw => | ||
| 85 | + trimmed.toLowerCase() === kw.toLowerCase() | ||
| 86 | + )) { | ||
| 87 | + return '整付' | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + // 标准化 "X年" 格式 | ||
| 91 | + const yearlyMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.yearlyPattern) | ||
| 92 | + if (yearlyMatch) { | ||
| 93 | + return `${yearlyMatch[1]}年` | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + // 标准化 "至X岁" 格式 | ||
| 97 | + const ageMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.untilAgePattern) | ||
| 98 | + if (ageMatch) { | ||
| 99 | + return `至${ageMatch[1]}岁` | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // 处理列表项格式 | ||
| 103 | + const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern) | ||
| 104 | + if (listItemMatch) { | ||
| 105 | + return normalizePaymentPeriod(listItemMatch[1]) | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + return trimmed | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +/** | ||
| 11 | * 字段提取规则配置 | 112 | * 字段提取规则配置 |
| 12 | * | 113 | * |
| 13 | * @description 定义每个字段的匹配规则、优先级和默认值 | 114 | * @description 定义每个字段的匹配规则、优先级和默认值 |
| ... | @@ -20,13 +121,30 @@ const FIELD_RULES = { | ... | @@ -20,13 +121,30 @@ const FIELD_RULES = { |
| 20 | /产品名称[::]\s*([^\n]+)/, | 121 | /产品名称[::]\s*([^\n]+)/, |
| 21 | /计划书名称[::]\s*([^\n]+)/, | 122 | /计划书名称[::]\s*([^\n]+)/, |
| 22 | /Product\s+Name[::]\s*([^\n]+)/i, | 123 | /Product\s+Name[::]\s*([^\n]+)/i, |
| 23 | - /^#\s+(.+)$/m // Markdown 标题 | 124 | + /^#\s+(.+)$/m, // Markdown 标题 |
| 125 | + // 新增:识别包含"计划"的标题行(如 "LV3 长宁終身壽險計劃3") | ||
| 126 | + // 格式:产品代码 + 中文产品名(包含"計劃/计划") | ||
| 127 | + /^[A-Z]{2,4}\d?\s*[-::]?\s*([^\n]*(?:計劃|计划)[^\n]*)/m, | ||
| 128 | + // 纯产品名称(包含"計劃/计划")- 通常为计划书名称 | ||
| 129 | + /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/m | ||
| 24 | ], | 130 | ], |
| 25 | fallback: null, // 必填,无默认值 | 131 | fallback: null, // 必填,无默认值 |
| 26 | - required: true | 132 | + required: true, |
| 133 | + postProcess: (value) => { | ||
| 134 | + // 处理正则匹配结果(数组) | ||
| 135 | + if (Array.isArray(value) && value.length > 1) { | ||
| 136 | + let name = value[1] || value[0] || '' | ||
| 137 | + // 移除产品代码前缀 | ||
| 138 | + name = name.replace(/^[A-Z]{2,4}\d?\s*[-::]?\s*/, '') | ||
| 139 | + // 移除后缀说明(如 "- 性別, 年齡, 出生年月日") | ||
| 140 | + name = name.split(/[-—::]/)[0].trim() | ||
| 141 | + return name || null | ||
| 142 | + } | ||
| 143 | + return value | ||
| 144 | + } | ||
| 27 | }, | 145 | }, |
| 28 | 146 | ||
| 29 | - // 产品类型 | 147 | + // 产品类型(保险类别) |
| 30 | product_type: { | 148 | product_type: { |
| 31 | priority: 2, | 149 | priority: 2, |
| 32 | patterns: [ | 150 | patterns: [ |
| ... | @@ -34,9 +152,21 @@ const FIELD_RULES = { | ... | @@ -34,9 +152,21 @@ const FIELD_RULES = { |
| 34 | { | 152 | { |
| 35 | type: 'content_match', | 153 | type: 'content_match', |
| 36 | rules: [ | 154 | rules: [ |
| 37 | - { keywords: ['储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' }, | 155 | + // 储蓄型产品(新增"储蓄产品"关键字) |
| 156 | + { keywords: ['储蓄产品', '储蓄型', '储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' }, | ||
| 157 | + // 重疾型产品 | ||
| 38 | { keywords: ['重疾', 'critical', '守护', '严重疾病'], value: 'critical-illness' }, | 158 | { keywords: ['重疾', 'critical', '守护', '严重疾病'], value: 'critical-illness' }, |
| 39 | - { keywords: ['人寿', 'life', '创富', '身故保障'], value: 'life-insurance' } | 159 | + // 人寿型产品 |
| 160 | + { keywords: ['人寿', 'life', '创富', '身故保障', '壽險', '壽险'], value: 'life-insurance' } | ||
| 161 | + ] | ||
| 162 | + }, | ||
| 163 | + // 新增:从标题行推断(如 "LV3 长宁終身壽險計劃3" 中的"壽險") | ||
| 164 | + { | ||
| 165 | + type: 'title_match', | ||
| 166 | + rules: [ | ||
| 167 | + { pattern: /終身壽險|終身寿险|壽險計劃|寿险计划/, value: 'life-insurance' }, | ||
| 168 | + { pattern: /儲蓄計劃|储蓄计划|儲蓄產品|储蓄产品/, value: 'savings' }, | ||
| 169 | + { pattern: /重疾|嚴重疾病/, value: 'critical-illness' } | ||
| 40 | ] | 170 | ] |
| 41 | } | 171 | } |
| 42 | ], | 172 | ], |
| ... | @@ -70,25 +200,12 @@ const FIELD_RULES = { | ... | @@ -70,25 +200,12 @@ const FIELD_RULES = { |
| 70 | priority: 4, | 200 | priority: 4, |
| 71 | patterns: [ | 201 | patterns: [ |
| 72 | // 匹配 "年繳保費繳費年期" 或 "缴费年期" 后面的列表 | 202 | // 匹配 "年繳保費繳費年期" 或 "缴费年期" 后面的列表 |
| 73 | - // 策略:匹配到包含 "年" 或 "整付" 的所有行,直到遇到其他关键字 | 203 | + // 策略:使用通用匹配函数识别各种格式的缴费年期选项 |
| 74 | { | 204 | { |
| 75 | type: 'smart_list_extract', | 205 | type: 'smart_list_extract', |
| 76 | startPattern: /(?:年繳保費)?繳費年期[::\s]*\n/, | 206 | startPattern: /(?:年繳保費)?繳費年期[::\s]*\n/, |
| 77 | endKeywords: ['提取', '保險期間', '保险期间', '投保年龄', '投保年齡', '選是', '選項', 'GC宏', 'FA宏', 'LV2'], | 207 | endKeywords: ['提取', '保險期間', '保险期间', '投保年龄', '投保年齡', '選是', '選項', 'GC宏', 'FA宏', 'LV2'], |
| 78 | - itemFilter: (line) => { | 208 | + itemFilter: (line) => isValidPaymentPeriod(line) |
| 79 | - const trimmed = line.trim() | ||
| 80 | - // 排除包含"投保年龄"等关键字的行 | ||
| 81 | - if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) { | ||
| 82 | - return false | ||
| 83 | - } | ||
| 84 | - // 精确匹配 "整付" 或 "X年" 格式 | ||
| 85 | - return trimmed && ( | ||
| 86 | - /^\d+\s*年$/.test(trimmed) || | ||
| 87 | - trimmed === '整付' || | ||
| 88 | - /^\d+年$/.test(trimmed) || | ||
| 89 | - /^[-•·]\s*\d+\s*年$/.test(trimmed) // 支持列表格式 "- 3年" | ||
| 90 | - ) | ||
| 91 | - } | ||
| 92 | } | 209 | } |
| 93 | ], | 210 | ], |
| 94 | fallback: ['整付', '3年', '5年'], | 211 | fallback: ['整付', '3年', '5年'], |
| ... | @@ -96,24 +213,17 @@ const FIELD_RULES = { | ... | @@ -96,24 +213,17 @@ const FIELD_RULES = { |
| 96 | postProcess: (values) => { | 213 | postProcess: (values) => { |
| 97 | // 过滤并标准化 | 214 | // 过滤并标准化 |
| 98 | const normalized = values | 215 | const normalized = values |
| 99 | - .map(v => v.trim()) | 216 | + .map(v => normalizePaymentPeriod(v)) |
| 100 | - // 排除包含"投保"等无效关键字 | 217 | + .filter(v => v && isValidPaymentPeriod(v)) |
| 101 | - .filter(v => v && !v.includes('投保') && !v.includes('年龄') && !v.includes('年齡')) | ||
| 102 | - .filter(v => v.includes('年') || v.includes('整付')) | ||
| 103 | - .map(v => { | ||
| 104 | - // 提取数字+年格式 | ||
| 105 | - const match = v.match(/(\d+)\s*年|整付/i) | ||
| 106 | - if (match) { | ||
| 107 | - return match[0].includes('整付') ? '整付' : `${match[1]}年` | ||
| 108 | - } | ||
| 109 | - return v | ||
| 110 | - }) | ||
| 111 | 218 | ||
| 112 | - // 去重、排序 | 219 | + // 去重、排序(整付放最前,其他按数字排序) |
| 113 | return [...new Set(normalized)].sort((a, b) => { | 220 | return [...new Set(normalized)].sort((a, b) => { |
| 114 | if (a === '整付') return -1 | 221 | if (a === '整付') return -1 |
| 115 | if (b === '整付') return 1 | 222 | if (b === '整付') return 1 |
| 116 | - return parseInt(a) - parseInt(b) | 223 | + // 提取数字进行排序 |
| 224 | + const numA = parseInt(a.match(/\d+/)?.[0] || '999') | ||
| 225 | + const numB = parseInt(b.match(/\d+/)?.[0] || '999') | ||
| 226 | + return numA - numB | ||
| 117 | }) | 227 | }) |
| 118 | } | 228 | } |
| 119 | }, | 229 | }, |
| ... | @@ -252,6 +362,11 @@ function extractField(content, fieldName) { | ... | @@ -252,6 +362,11 @@ function extractField(content, fieldName) { |
| 252 | patternDesc = `content_match(${pattern.rules.length} rules)` | 362 | patternDesc = `content_match(${pattern.rules.length} rules)` |
| 253 | break | 363 | break |
| 254 | 364 | ||
| 365 | + case 'title_match': | ||
| 366 | + match = matchByTitle(content, pattern.rules) | ||
| 367 | + patternDesc = `title_match(${pattern.rules.length} rules)` | ||
| 368 | + break | ||
| 369 | + | ||
| 255 | case 'count_match': | 370 | case 'count_match': |
| 256 | match = matchByCount(content, pattern.rules) | 371 | match = matchByCount(content, pattern.rules) |
| 257 | patternDesc = `count_match(${pattern.rules.length} rules)` | 372 | patternDesc = `count_match(${pattern.rules.length} rules)` |
| ... | @@ -276,6 +391,11 @@ function extractField(content, fieldName) { | ... | @@ -276,6 +391,11 @@ function extractField(content, fieldName) { |
| 276 | match = extractRange(content, pattern.pattern) | 391 | match = extractRange(content, pattern.pattern) |
| 277 | patternDesc = `range_extract` | 392 | patternDesc = `range_extract` |
| 278 | break | 393 | break |
| 394 | + | ||
| 395 | + case 'options_extract': | ||
| 396 | + match = extractOptionsFields(content, pattern.keyword) | ||
| 397 | + patternDesc = `options_extract(${pattern.keyword})` | ||
| 398 | + break | ||
| 279 | } | 399 | } |
| 280 | } else if (pattern instanceof RegExp) { | 400 | } else if (pattern instanceof RegExp) { |
| 281 | // 正则表达式匹配 | 401 | // 正则表达式匹配 |
| ... | @@ -331,6 +451,120 @@ function matchByContent(content, rules) { | ... | @@ -331,6 +451,120 @@ function matchByContent(content, rules) { |
| 331 | } | 451 | } |
| 332 | 452 | ||
| 333 | /** | 453 | /** |
| 454 | + * 通过标题模式匹配 | ||
| 455 | + * | ||
| 456 | + * @description 从文档标题或产品名称行中匹配产品类型 | ||
| 457 | + * @param {string} content - 文档内容 | ||
| 458 | + * @param {Array} rules - 匹配规则数组 | ||
| 459 | + * @returns {string|null} 匹配的值 | ||
| 460 | + */ | ||
| 461 | +function matchByTitle(content, rules) { | ||
| 462 | + // 提取可能的产品标题行(通常在文档开头) | ||
| 463 | + const lines = content.split('\n').slice(0, 20) // 只检查前20行 | ||
| 464 | + const titleLines = lines.filter(line => { | ||
| 465 | + const trimmed = line.trim() | ||
| 466 | + // 标题行通常包含产品代码、产品名称等 | ||
| 467 | + return trimmed.length > 5 && trimmed.length < 100 && | ||
| 468 | + (/[A-Z]{2,4}\d?/.test(trimmed) || | ||
| 469 | + /計劃|计划|保障|保险|壽險|壽险/.test(trimmed)) | ||
| 470 | + }) | ||
| 471 | + | ||
| 472 | + // 合并标题行进行匹配 | ||
| 473 | + const titleText = titleLines.join('\n') | ||
| 474 | + | ||
| 475 | + for (const rule of rules) { | ||
| 476 | + if (rule.pattern && rule.pattern.test(titleText)) { | ||
| 477 | + return rule.value | ||
| 478 | + } | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + return null | ||
| 482 | +} | ||
| 483 | + | ||
| 484 | +/** | ||
| 485 | + * 从"选项"段落提取字段 | ||
| 486 | + * | ||
| 487 | + * @description 识别 "XXX選項:" 或 "XXX选项:" 格式的段落,提取其中的字段列表 | ||
| 488 | + * 示例: | ||
| 489 | + * - "基本人壽保障選項:" 之后的性别、年龄等字段 | ||
| 490 | + * - "提取選項:" 之后的提取方式 | ||
| 491 | + * @param {string} content - 文档内容 | ||
| 492 | + * @param {string} optionKeyword - 选项关键词(如 "基本人壽保障選項") | ||
| 493 | + * @returns {Array<{name: string, description: string}>|null} 提取的字段列表 | ||
| 494 | + */ | ||
| 495 | +function extractOptionsFields(content, optionKeyword) { | ||
| 496 | + // 匹配 "XXX選項:" 或 "XXX选项:" 格式 | ||
| 497 | + const pattern = new RegExp(`${optionKeyword}[::\\s]*\\n([\\s\\S]*?)(?=\\n\\n|\\n[A-Z]|\\n\\d+\\.\\s|$)`, 'gm') | ||
| 498 | + const match = content.match(pattern) | ||
| 499 | + | ||
| 500 | + if (!match) return null | ||
| 501 | + | ||
| 502 | + const optionContent = match[0] | ||
| 503 | + const fields = [] | ||
| 504 | + | ||
| 505 | + // 按行分割,提取字段名和描述 | ||
| 506 | + const lines = optionContent.split('\n').slice(1) // 跳过标题行 | ||
| 507 | + | ||
| 508 | + for (const line of lines) { | ||
| 509 | + const trimmed = line.trim() | ||
| 510 | + if (!trimmed) continue | ||
| 511 | + | ||
| 512 | + // 常见字段格式: | ||
| 513 | + // 1. "性別" - 纯字段名 | ||
| 514 | + // 2. "年齡 (1-75岁)" - 字段名 + 范围说明 | ||
| 515 | + // 3. "- 出生年月日" - 列表格式 | ||
| 516 | + // 4. "提取金额: 指定金额" - 字段名 + 描述 | ||
| 517 | + | ||
| 518 | + // 提取字段名(去除列表符号和说明) | ||
| 519 | + const fieldMatch = trimmed.match(/^(?:[-•·]\s*)?([^::((]+)/) | ||
| 520 | + if (fieldMatch) { | ||
| 521 | + const fieldName = fieldMatch[1].trim() | ||
| 522 | + if (fieldName && fieldName.length > 0 && fieldName.length < 20) { | ||
| 523 | + fields.push({ | ||
| 524 | + name: fieldName, | ||
| 525 | + description: trimmed // 保留完整描述 | ||
| 526 | + }) | ||
| 527 | + } | ||
| 528 | + } | ||
| 529 | + } | ||
| 530 | + | ||
| 531 | + return fields.length > 0 ? fields : null | ||
| 532 | +} | ||
| 533 | + | ||
| 534 | +/** | ||
| 535 | + * 查找所有"选项"段落 | ||
| 536 | + * | ||
| 537 | + * @description 扫描文档中所有包含"選項"或"选项"的段落标题 | ||
| 538 | + * @param {string} content - 文档内容 | ||
| 539 | + * @returns {Array<{keyword: string, startIndex: number, fields: Array}>} 选项段落列表 | ||
| 540 | + */ | ||
| 541 | +function findAllOptionSections(content) { | ||
| 542 | + const sections = [] | ||
| 543 | + | ||
| 544 | + // 匹配所有 "XXX選項:" 或 "XXX选项:" 格式 | ||
| 545 | + const pattern = /([^\n]*(?:選項|选项|選是)[::]?)/gm | ||
| 546 | + let match | ||
| 547 | + | ||
| 548 | + while ((match = pattern.exec(content)) !== null) { | ||
| 549 | + const keyword = match[1].trim() | ||
| 550 | + const startIndex = match.index | ||
| 551 | + | ||
| 552 | + // 提取该选项下的字段 | ||
| 553 | + const fields = extractOptionsFields(content, keyword.replace(/[::]/g, '')) | ||
| 554 | + | ||
| 555 | + if (fields && fields.length > 0) { | ||
| 556 | + sections.push({ | ||
| 557 | + keyword, | ||
| 558 | + startIndex, | ||
| 559 | + fields | ||
| 560 | + }) | ||
| 561 | + } | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + return sections | ||
| 565 | +} | ||
| 566 | + | ||
| 567 | +/** | ||
| 334 | * 通过统计匹配内容 | 568 | * 通过统计匹配内容 |
| 335 | */ | 569 | */ |
| 336 | function matchByCount(content, rules) { | 570 | function matchByCount(content, rules) { | ... | ... |
| ... | @@ -368,6 +368,24 @@ export const PLAN_TEMPLATES = { | ... | @@ -368,6 +368,24 @@ export const PLAN_TEMPLATES = { |
| 368 | submit_mapping: savingsSubmitMapping | 368 | submit_mapping: savingsSubmitMapping |
| 369 | } | 369 | } |
| 370 | }, | 370 | }, |
| 371 | + | ||
| 372 | + /** | ||
| 373 | + * 长宁終身壽險計劃3 | ||
| 374 | + * @added 2026-02-15T06:30:03.691Z | ||
| 375 | + * @source docs/to-parse/计划书模版4.docx | ||
| 376 | + */ | ||
| 377 | + 'life-insurance-3-d8fde07d': { | ||
| 378 | + name: '长宁終身壽險計劃3', | ||
| 379 | + component: 'LifeInsuranceTemplate', | ||
| 380 | + config: { | ||
| 381 | + currency: 'USD', | ||
| 382 | + payment_periods: ["5年","12年","15年","20年"], | ||
| 383 | + age_range: { min: 0, max: 75 }, | ||
| 384 | + insurance_period: '终身', | ||
| 385 | + form_schema: protectionFormSchema, | ||
| 386 | + submit_mapping: baseSubmitMapping | ||
| 387 | + } | ||
| 388 | + } | ||
| 371 | } | 389 | } |
| 372 | 390 | ||
| 373 | /** | 391 | /** | ... | ... |
| ... | @@ -415,9 +415,11 @@ export async function mockProductListAPI(params) { | ... | @@ -415,9 +415,11 @@ export async function mockProductListAPI(params) { |
| 415 | const list = [] | 415 | const list = [] |
| 416 | const startIndex = page * limit | 416 | const startIndex = page * limit |
| 417 | 417 | ||
| 418 | - // 🔧 测试商品:第一页第一位固定为储蓄产品(form_sn:savings-product-30b41aae) | 418 | + // 🔧 测试商品:第一页前两位固定为测试产品 |
| 419 | if (page === 0) { | 419 | if (page === 0) { |
| 420 | const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1) | 420 | const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1) |
| 421 | + | ||
| 422 | + // 测试商品1: 储蓄产品 | ||
| 421 | const testProduct1 = { | 423 | const testProduct1 = { |
| 422 | id: 'savings-2-148b3acd', | 424 | id: 'savings-2-148b3acd', |
| 423 | product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)', | 425 | product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)', |
| ... | @@ -432,19 +434,38 @@ export async function mockProductListAPI(params) { | ... | @@ -432,19 +434,38 @@ export async function mockProductListAPI(params) { |
| 432 | _test_note: 'form_sn:savings-2-148b3acd' | 434 | _test_note: 'form_sn:savings-2-148b3acd' |
| 433 | } | 435 | } |
| 434 | 436 | ||
| 435 | - // 检查分类和关键词过滤 | 437 | + // 测试商品2: 人寿保险产品 |
| 436 | - let shouldInclude = true | 438 | + const testProduct2 = { |
| 437 | - if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) { | 439 | + id: 'life-insurance-3-d8fde07d', |
| 438 | - shouldInclude = false | 440 | + product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)', |
| 439 | - } | 441 | + cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300', |
| 440 | - if (keyword && !testProduct1.product_name.includes(keyword)) { | 442 | + recommend: 'hot', |
| 441 | - shouldInclude = false | 443 | + form_sn: 'life-insurance-3-d8fde07d', // ✅ 关键字段:对应真实 API 的 form_sn |
| 444 | + created_time: new Date().toISOString(), | ||
| 445 | + categories: [testCategory], // ✅ 符合真实 API 结构:categories 是数组 | ||
| 446 | + tags: [{ id: '1', name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }], | ||
| 447 | + // 测试标识(不影响业务逻辑) | ||
| 448 | + _test: true, | ||
| 449 | + _test_note: 'form_sn:life-insurance-3-d8fde07d' | ||
| 442 | } | 450 | } |
| 443 | 451 | ||
| 444 | - if (shouldInclude) { | 452 | + // 检查分类和关键词过滤,依次添加测试商品 |
| 445 | - list.push(testProduct1) | 453 | + const testProducts = [testProduct1, testProduct2] |
| 446 | - console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd') | 454 | + |
| 447 | - } | 455 | + testProducts.forEach((testProduct, index) => { |
| 456 | + let shouldInclude = true | ||
| 457 | + if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) { | ||
| 458 | + shouldInclude = false | ||
| 459 | + } | ||
| 460 | + if (keyword && !testProduct.product_name.includes(keyword)) { | ||
| 461 | + shouldInclude = false | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + if (shouldInclude) { | ||
| 465 | + list.push(testProduct) | ||
| 466 | + console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`) | ||
| 467 | + } | ||
| 468 | + }) | ||
| 448 | } | 469 | } |
| 449 | 470 | ||
| 450 | for (let i = 0; i < limit; i++) { | 471 | for (let i = 0; i < limit; i++) { | ... | ... |
-
Please register or login to post a comment