hookehuyr

feat(parse): 增强文档解析工具链和智能字段提取

主要改进:
- 优化 smartExtractList() 智能字段提取器
- 增强产品边界检测逻辑
- 完善 MCP 解析切换功能
- 优化 mockData 产品列表数据结构
- 更新计划书模板配置

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -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,14 +106,51 @@ export function findProductTitles(content) { ...@@ -99,14 +106,51 @@ 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 +
118 + products.push({
119 + index: match.index,
120 + code,
121 + name,
122 + fullTitle
123 + })
124 + }
125 + }
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)
102 products.push({ 144 products.push({
103 index: match.index, 145 index: match.index,
104 - code: match[1] || null, 146 + code: null,
105 - name: match[2] || fullTitle, 147 + name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明
106 fullTitle 148 fullTitle
107 }) 149 })
108 } 150 }
109 } 151 }
152 + }
153 + }
110 154
111 // 按出现位置排序 155 // 按出现位置排序
112 products.sort((a, b) => a.index - b.index) 156 products.sort((a, b) => a.index - b.index)
......
...@@ -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: 人寿保险产品
438 + const testProduct2 = {
439 + id: 'life-insurance-3-d8fde07d',
440 + product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)',
441 + cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300',
442 + recommend: 'hot',
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'
450 + }
451 +
452 + // 检查分类和关键词过滤,依次添加测试商品
453 + const testProducts = [testProduct1, testProduct2]
454 +
455 + testProducts.forEach((testProduct, index) => {
436 let shouldInclude = true 456 let shouldInclude = true
437 - if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) { 457 + if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) {
438 shouldInclude = false 458 shouldInclude = false
439 } 459 }
440 - if (keyword && !testProduct1.product_name.includes(keyword)) { 460 + if (keyword && !testProduct.product_name.includes(keyword)) {
441 shouldInclude = false 461 shouldInclude = false
442 } 462 }
443 463
444 if (shouldInclude) { 464 if (shouldInclude) {
445 - list.push(testProduct1) 465 + list.push(testProduct)
446 - console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd') 466 + console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`)
447 } 467 }
468 + })
448 } 469 }
449 470
450 for (let i = 0; i < limit; i++) { 471 for (let i = 0; i < limit; i++) {
......