feat(parse): 增强文档解析工具链和智能字段提取
主要改进: - 优化 smartExtractList() 智能字段提取器 - 增强产品边界检测逻辑 - 完善 MCP 解析切换功能 - 优化 mockData 产品列表数据结构 - 更新计划书模板配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
7 changed files
with
315 additions
and
20 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"} | ... | ... |
This diff is collapsed. Click to expand it.
| ... | @@ -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) | ... | ... |
This diff is collapsed. Click to expand it.
| ... | @@ -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++) { | ... | ... |
-
Please register or login to post a comment