You need to sign in or sign up before continuing.
hookehuyr

feat(parse): 支持多产品文档解析

- 新增 product-splitter.js 产品边界检测模块
  - 支持产品代码前缀识别(GS、GC、FA、LV2 等)
  - 支持产品命名模式(以"計劃"、"保障"、"保险"、"壽險"结尾)
  - 自动检测和分割多产品文档

- 增强 parse-docs.js 多产品处理
  - parseSingleFile() 返回数组支持多产品
  - generateAuditFile() 支持产品索引参数
  - 单文件模式 (--file=) 正确处理多产品结果
  - buildParseSummary() 统计多产品数量

- 优化 smart-field-extractor.js
  - 新增 smartExtractFieldsForProduct() 单产品提取
  - 移除重复的函数定义
  - 包装函数兼容新旧调用方式

测试结果:
- 成功解析 计划书模版2.docx 中的 4 个保险产品
- 每个产品生成独立的审核文件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -30,7 +30,8 @@ import {
MARKITDOWN_CONFIG,
AI_SERVICE_CONFIG
} from './parse-config.js'
import { smartExtractFields, generateAuditReport } from './smart-field-extractor.js'
import { smartExtractFields, smartExtractFieldsForProduct, generateAuditReport } from './smart-field-extractor.js'
import { splitByProducts, findProductTitles, generateSplitReport } from './product-splitter.js'
// ========== 配置区 ==========
......@@ -470,7 +471,7 @@ const AI_PARSE_PROMPT = `你是一个保险产品配置专家。请从以下文
*
* @description 使用 markitdown + AI 智能解析文档并提取配置
* @param {string} docPath - 文档路径
* @returns {Promise<Object>} 解析后的配置对象
* @returns {Promise<Object|Array<Object>>} 解析后的配置对象或配置数组(多产品)
*/
async function parseDocumentWithAI(docPath) {
console.log(`\n🤖 正在智能解析: ${path.basename(docPath)}`)
......@@ -493,7 +494,78 @@ async function parseDocumentWithAI(docPath) {
const content = parse_result.text
const fileName = path.basename(docPath)
// 步骤 2: 使用智能字段提取器
// ========== 步骤 2: 检测并分割多产品 ==========
const productTitles = findProductTitles(content)
if (productTitles.length > 1) {
// 多产品文档
console.log(`\n📦 检测到 ${productTitles.length} 个产品:`)
productTitles.forEach((p, i) => {
console.log(` ${i + 1}. [${p.code || '?'}] ${p.name || p.fullTitle?.slice(0, 30)}`)
})
// 分割文档
const products = splitByProducts(content)
const splitReport = generateSplitReport(content, products)
console.log('\n' + splitReport)
// 对每个产品分别提取字段
const configs = []
for (let i = 0; i < products.length; i++) {
const product = products[i]
console.log(`\n${'='.repeat(40)}`)
console.log(`📋 处理产品 ${i + 1}/${products.length}: ${product.name || product.code || '未命名'}`)
console.log('='.repeat(40))
const extractResult = smartExtractFieldsForProduct(
product.content,
fileName,
{
productCode: product.code,
productName: product.name
}
)
// 生成审核报告
const auditReport = generateAuditReport(extractResult)
console.log('\n' + auditReport)
// 构建配置对象
const config = {
...extractResult.config,
is_savings: extractResult.config.product_type === 'savings',
form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
submit_mapping: {}
}
// 保存匹配详情
config._extractDetails = {
matched: extractResult.matchDetails.filter(m => m.matched).map(m => m.field),
unmatched: extractResult.unmatched,
warnings: extractResult.warnings,
productIndex: i,
totalProducts: products.length
}
config.form_sn = generateFormSn(config)
const matchedCount = extractResult.matchDetails.filter(m => m.matched).length
const totalCount = extractResult.matchDetails.length
console.log(`\n✅ 产品 ${i + 1} 解析成功 (智能匹配 ${matchedCount}/${totalCount} 字段)`)
console.log(` 产品名称: ${config.product_name}`)
console.log(` 产品代码: ${product.code || '-'}`)
console.log(` 产品类型: ${config.product_type}`)
console.log(` 币种: ${config.currency}`)
console.log(` 缴费年期: ${JSON.stringify(config.payment_periods)}`)
configs.push(config)
}
return configs // 返回数组
}
// ========== 单产品文档 ==========
console.log('🧠 使用智能字段提取器...')
const extractResult = smartExtractFields(content, fileName)
......@@ -598,6 +670,10 @@ function inferCurrency(content) {
/**
* 解析单个文档
*
* @description 支持单产品和多产品文档解析
* - 单产品文档:返回单个结果对象
* - 多产品文档:返回结果数组(每个产品一个结果)
*/
async function parseSingleFile(filePath) {
const fileName = path.basename(filePath)
......@@ -605,41 +681,90 @@ async function parseSingleFile(filePath) {
console.log("📄 处理文件: " + fileName)
console.log("=".repeat(60))
// 解析文档
const config = await parseDocumentWithAI(filePath)
// 解析文档(可能返回单个 config 或 configs 数组)
const parseResult = await parseDocumentWithAI(filePath)
if (!config) {
if (!parseResult) {
console.log("⏭️ 跳过文件: " + fileName + " (解析失败)")
return { success: false, file: fileName, reason: 'parse_failed' }
}
const validation = validateParsedConfig(config)
if (!validation.valid) {
console.error("❌ 校验失败: " + fileName)
validation.errors.forEach(message => {
console.error(" - " + message)
})
return { success: false, file: fileName, reason: 'validation_failed', errors: validation.errors }
// 统一处理为数组形式
const configs = Array.isArray(parseResult) ? parseResult : [parseResult]
// 多产品提示
if (configs.length > 1) {
console.log("\n📦 检测到多产品文档,共 " + configs.length + " 个产品")
}
// 添加源文件信息
config.source_file = fileName
// 处理每个产品配置
const results = []
for (let i = 0; i < configs.length; i++) {
const config = configs[i]
const productIndex = configs.length > 1 ? ` [${i + 1}/${configs.length}]` : ''
if (configs.length > 1 && config.product_name) {
console.log("\n--- 处理产品: " + config.product_name + " ---")
}
// 生成配置代码
const { formSn, code } = generateConfigCode(config)
const validation = validateParsedConfig(config)
if (!validation.valid) {
console.error("❌ 校验失败" + productIndex + ": " + (config.product_name || fileName))
validation.errors.forEach(message => {
console.error(" - " + message)
})
results.push({
success: false,
file: fileName,
productName: config.product_name || `产品${i + 1}`,
reason: 'validation_failed',
errors: validation.errors
})
continue
}
console.log("\n📝 生成 form_sn: " + formSn)
console.log("📋 生成配置代码:\n" + code)
// 添加源文件信息
config.source_file = fileName
// 生成配置代码
const { formSn, code } = generateConfigCode(config)
console.log("\n📝 生成 form_sn: " + formSn + productIndex)
console.log("📋 生成配置代码:\n" + code)
// 生成待审核文件
const auditFile = await generateAuditFile(fileName, config, code, i, configs.length)
if (auditFile) {
console.log("\n✅ 已生成待审核文件: " + auditFile)
console.log("📋 请审核后手动移动到 src/config/plan-templates.js")
}
results.push({
success: true,
formSn,
code,
file: fileName,
productName: config.product_name || `产品${i + 1}`,
config,
auditFile
})
}
// ✨ 新增:生成待审核文件(不直接写入正式配置)
const auditFile = await generateAuditFile(fileName, config, code)
if (auditFile) {
console.log("\n✅ 已生成待审核文件: " + auditFile)
console.log("📋 请审核后手动移动到 src/config/plan-templates.js")
return { success: true, formSn, code, file: fileName, config, auditFile }
// 单产品时返回单个结果对象(保持向后兼容)
// 多产品时返回数组
if (configs.length === 1) {
return results[0]
}
return { success: true, formSn, code, file: fileName, config, auditFile }
// 多产品返回特殊结构
return {
success: results.some(r => r.success),
file: fileName,
multiProduct: true,
productCount: configs.length,
successCount: results.filter(r => r.success).length,
results // 每个产品的详细结果
}
}
/**
......@@ -649,16 +774,31 @@ async function parseSingleFile(filePath) {
* @param {string} fileName - 原始文件名
* @param {Object} config - 解析的配置对象
* @param {string} code - 生成的配置代码
* @param {number} productIndex - 产品索引(多产品文档时使用,从 0 开始)
* @param {number} totalProducts - 产品总数(多产品文档时使用)
* @returns {Promise<string|null>} 审核文件路径
*/
async function generateAuditFile(fileName, config, code) {
async function generateAuditFile(fileName, config, code, productIndex = 0, totalProducts = 1) {
const AUDIT_PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending')
const AUDIT_APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved')
ensureDir(AUDIT_PENDING_DIR)
ensureDir(AUDIT_APPROVED_DIR)
const date = new Date().toISOString().split('T')[0]
const auditFileName = `${date}-${fileName.replace(/\.[^/.]+$/, '')}.md`
const baseFileName = fileName.replace(/\.[^/.]+$/, '')
// 多产品文档时,为每个产品生成独立文件
let auditFileName
if (totalProducts > 1 && config.product_name) {
// 使用产品名称作为文件名的一部分
const productSlug = config.product_name
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '-') // 保留中文、英文、数字
.replace(/-+/g, '-')
.slice(0, 30) // 限制长度
auditFileName = `${date}-${baseFileName}-${productSlug}.md`
} else {
auditFileName = `${date}-${baseFileName}.md`
}
const auditFilePath = path.join(AUDIT_PENDING_DIR, auditFileName)
const formSn = generateFormSn(config)
const formSchemaPreview = config.form_schema ? JSON.stringify(config.form_schema, null, 2) : '// 请手动补充'
......@@ -857,9 +997,16 @@ export function updateConfigContent(existingContent, newConfigs) {
return null
}
const insertContent = newConfigs.map((item, index) => {
// 过滤掉没有 code 的配置项
const validConfigs = newConfigs.filter(item => item && item.code)
if (validConfigs.length === 0) {
console.warn('⚠️ 没有有效的配置代码可插入')
return null
}
const insertContent = validConfigs.map((item, index) => {
const code = item.code.trimEnd()
return index === newConfigs.length - 1 ? code : code + ','
return index === validConfigs.length - 1 ? code : code + ','
}).join('\n\n')
const before = existingContent.substring(0, range.endIndex)
......@@ -1099,7 +1246,8 @@ export function buildConfigUpdateResult(existingContent, newConfigs, options = {
export function buildParseSummary(results, duration_ms) {
const summary = {
total: results.length,
total_docs: results.length,
total_products: 0,
success: 0,
failed: 0,
duration_ms,
......@@ -1108,20 +1256,49 @@ export function buildParseSummary(results, duration_ms) {
}
results.forEach(result => {
if (result.success) {
summary.success += 1
summary.success_list.push({
form_sn: result.formSn,
product_name: result.config?.product_name,
file: result.file
})
// 处理多产品文档
if (result.multiProduct) {
summary.total_products += result.productCount
if (result.results) {
result.results.forEach(r => {
if (r.success) {
summary.success += 1
summary.success_list.push({
form_sn: r.formSn,
product_name: r.config?.product_name || r.productName,
file: r.file
})
} else {
summary.failed += 1
summary.failed_list.push({
file: r.file,
product_name: r.productName,
reason: r.reason || 'unknown',
errors: r.errors || []
})
}
})
}
} else {
summary.failed += 1
summary.failed_list.push({
file: result.file,
reason: result.reason || 'unknown',
errors: result.errors || []
})
// 单产品文档
summary.total_products += 1
if (result.success) {
summary.success += 1
summary.success_list.push({
form_sn: result.formSn,
product_name: result.config?.product_name,
file: result.file
})
} else {
summary.failed += 1
summary.failed_list.push({
file: result.file,
reason: result.reason || 'unknown',
errors: result.errors || []
})
}
}
})
......@@ -1246,6 +1423,8 @@ function updateConfigFile(newConfigs, options = {}) {
/**
* 处理所有文档
*
* @description 支持单产品和多产品文档的批量处理
*/
async function parseAllDocs(docs, options = {}) {
if (docs.length === 0) {
......@@ -1263,19 +1442,45 @@ async function parseAllDocs(docs, options = {}) {
for (const doc of docs) {
const result = await parseSingleFile(doc.fullPath)
results.push(result)
if (result.success) {
successResults.push(result)
// 处理多产品返回值
if (result.multiProduct) {
// 多产品文档
console.log("\n📦 文档 " + result.file + " 包含 " + result.productCount + " 个产品,成功 " + result.successCount + " 个")
// 添加文档级结果
results.push(result)
// 展开每个成功的产品到 successResults
if (result.results) {
result.results.forEach(r => {
if (r.success && r.code) {
successResults.push(r)
}
})
}
} else {
// 单产品文档
results.push(result)
if (result.success && result.code) {
successResults.push(result)
}
}
}
// 计算实际产品数量
const totalProducts = results.reduce((sum, r) => {
return sum + (r.multiProduct ? r.productCount : 1)
}, 0)
const successProducts = successResults.length
// 汇总
console.log("\n" + "=".repeat(60))
console.log("📊 解析结果汇总")
console.log("=".repeat(60))
console.log("总计: " + docs.length + " 个文档")
console.log("成功: " + successResults.length + " 个")
console.log("失败: " + (results.length - successResults.length) + " 个")
console.log("文档: " + docs.length + " 个")
console.log("产品: " + totalProducts + " 个(成功: " + successProducts + ", 失败: " + (totalProducts - successProducts) + ")")
const summary = buildParseSummary(results, Date.now() - start_time)
console.log("耗时: " + summary.duration_ms + "ms")
......@@ -1283,13 +1488,21 @@ async function parseAllDocs(docs, options = {}) {
if (successResults.length > 0) {
console.log("\n✅ 成功解析的产品:")
successResults.forEach(r => {
console.log(" - " + r.formSn + ": " + r.config.product_name)
const productInfo = r.config?.product_name || r.productName || '未知产品'
console.log(" - " + r.formSn + ": " + productInfo)
})
}
if (summary.failed_list.length > 0) {
console.log("\n⚠️ 失败明细:")
summary.failed_list.forEach(item => {
console.log(" - " + item.file + " (" + item.reason + ")")
// 显示失败信息
const failedResults = results.filter(r => !r.success || (r.multiProduct && r.successCount < r.productCount))
if (failedResults.length > 0) {
console.log("\n⚠️ 失败/部分失败:")
failedResults.forEach(r => {
if (r.multiProduct) {
console.log(" - " + r.file + " (" + r.successCount + "/" + r.productCount + " 成功)")
} else {
console.log(" - " + r.file + " (" + (r.reason || 'unknown') + ")")
}
})
}
......@@ -1298,7 +1511,7 @@ async function parseAllDocs(docs, options = {}) {
if (successResults.length > 0) {
update_result = updateConfigFile(successResults, options)
} else {
console.log("\n❌ 没有成功解析的文档,配置文件未更新")
console.log("\n❌ 没有成功解析的产品,配置文件未更新")
}
const audit_record = buildAuditRecord(summary, options, update_result, 'batch')
writeAuditLog(audit_record)
......@@ -1362,21 +1575,40 @@ async function main() {
if (targetDoc) {
const start_time = Date.now()
const result = await parseSingleFile(targetDoc.fullPath, parserMode)
const result = await parseSingleFile(targetDoc.fullPath)
const summary = buildParseSummary([result], Date.now() - start_time)
// 计算产品数量
const productCount = result.multiProduct ? result.productCount : 1
const successCount = result.multiProduct ? result.successCount : (result.success ? 1 : 0)
console.log("\n📊 解析结果汇总")
console.log("总计: " + summary.total + " 个文档")
console.log("成功: " + summary.success + " 个")
console.log("失败: " + summary.failed + " 个")
console.log("文档: 1 个")
console.log("产品: " + productCount + " 个(成功: " + successCount + ", 失败: " + (productCount - successCount) + ")")
console.log("耗时: " + summary.duration_ms + "ms")
if (result.success) {
const update_result = updateConfigFile([result], { dry_run: dryRunMode })
const audit_record = buildAuditRecord(summary, { dry_run: dryRunMode }, update_result, 'single')
writeAuditLog(audit_record)
// 收集成功的产品配置
let successConfigs = []
if (result.multiProduct) {
// 多产品文档:展开子结果
if (result.results) {
successConfigs = result.results.filter(r => r.success && r.code)
}
} else if (result.success && result.code) {
// 单产品文档
successConfigs = [result]
}
// 更新配置文件
let update_result = null
if (successConfigs.length > 0) {
update_result = updateConfigFile(successConfigs, { dry_run: dryRunMode })
} else {
const audit_record = buildAuditRecord(summary, { dry_run: dryRunMode }, null, 'single')
writeAuditLog(audit_record)
console.log("\n❌ 没有成功解析的产品,配置文件未更新")
}
const audit_record = buildAuditRecord(summary, { dry_run: dryRunMode }, update_result, 'single')
writeAuditLog(audit_record)
} else {
console.log("❌ 找不到文件: " + fileName)
}
......
/**
* 产品分割器
*
* @description 从包含多个保险产品的文档中识别并分割出各个产品
* @module scripts/product-splitter
* @author Claude Code
* @created 2026-02-15
*/
/**
* 产品标题匹配规则
*
* @description 用于识别文档中的产品标题行
* 格式示例:
* - GS宏摯傳承保障計劃 - 性別, 年齡, 出生年月日
* - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
* - FA 宏浚傳承保障計劃
* - LV2 赤霞珠終身壽險計劃2基本人壽保障選項
*/
const PRODUCT_TITLE_PATTERNS = [
// 产品代码 + 产品名称 + 可选后缀
// GS宏摯傳承保障計劃 - 性別, 年齡, 出生年月日
/^([A-Z]{2,4}\d?)\s*([^\n\-]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm,
// 产品代码 + 空格 + 产品名称
// FA 宏浚傳承保障計劃
/^([A-Z]{2,4}\d?)\s+([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险))/gm,
// 纯产品名称(包含"計劃")
// 宏摯傳承保障計劃
/^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm,
// 产品代码开头的行
/^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm
]
/**
* 产品代码前缀列表(用于优先匹配)
*/
const PRODUCT_CODE_PREFIXES = [
'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR',
'TR', 'UR', 'WR', 'XR', 'YR', 'ZR'
]
/**
* 检测文档中包含的产品数量
*
* @param {string} content - 文档内容
* @returns {number} 产品数量
*/
export function detectProductCount(content) {
const matches = findProductTitles(content)
return matches.length
}
/**
* 查找文档中所有产品标题
*
* @param {string} content - 文档内容
* @returns {Array<{index: number, code: string, name: string, fullTitle: string}>} 产品标题列表
*/
export function findProductTitles(content) {
const products = []
const seenCodes = new Set()
// 策略1: 优先匹配产品代码前缀
for (const prefix of PRODUCT_CODE_PREFIXES) {
// 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃"
const regex = new RegExp(
`^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`,
'gm'
)
let match
while ((match = regex.exec(content)) !== null) {
const code = match[1]
const name = match[2].trim()
// 去重
if (seenCodes.has(code)) continue
seenCodes.add(code)
products.push({
index: match.index,
code,
name,
fullTitle: match[0].trim()
})
}
}
// 策略2: 如果没找到,尝试通用模式匹配
if (products.length === 0) {
// 匹配包含"計劃"的产品名称行
const regex = /^([A-Z]{2,4}\d?)?\s*([^\n]*?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm
let match
while ((match = regex.exec(content)) !== null) {
const fullTitle = match[0].trim()
if (fullTitle.length < 5) continue // 过滤太短的匹配
products.push({
index: match.index,
code: match[1] || null,
name: match[2] || fullTitle,
fullTitle
})
}
}
// 按出现位置排序
products.sort((a, b) => a.index - b.index)
return products
}
/**
* 将文档内容按产品分割
*
* @param {string} content - 文档内容
* @returns {Array<{code: string, name: string, content: string, fullTitle: string}>} 分割后的产品列表
*/
export function splitByProducts(content) {
const products = findProductTitles(content)
if (products.length === 0) {
// 没有找到多个产品,返回整个文档作为单个产品
return [{
code: null,
name: null,
content: content,
fullTitle: null
}]
}
if (products.length === 1) {
// 只有一个产品,返回整个文档
return [{
code: products[0].code,
name: products[0].name,
content: content,
fullTitle: products[0].fullTitle
}]
}
// 多个产品,按位置分割
const result = []
for (let i = 0; i < products.length; i++) {
const product = products[i]
const startIndex = product.index
const endIndex = (i < products.length - 1) ? products[i + 1].index : content.length
const productContent = content.slice(startIndex, endIndex).trim()
result.push({
code: product.code,
name: product.name,
content: productContent,
fullTitle: product.fullTitle
})
}
return result
}
/**
* 智能提取产品名称
*
* @description 从产品标题或内容中提取标准化的产品名称
* @param {string} fullTitle - 产品完整标题
* @param {string} content - 产品内容片段
* @returns {string} 产品名称
*/
export function extractProductName(fullTitle, content) {
if (!fullTitle && !content) return null
// 优先从完整标题提取
if (fullTitle) {
// 移除产品代码前缀
let name = fullTitle.replace(/^[A-Z]{2,4}\d?\s*[-::]?\s*/, '')
// 移除后缀说明(如 "- 性別, 年齡, 出生年月日")
name = name.split(/[-—::]/)[0].trim()
if (name && name.length > 2) {
return name
}
}
// 从内容中查找产品名称
const patterns = [
/产品名称[::]\s*([^\n]+)/,
/计划书名称[::]\s*([^\n]+)/,
/([A-Z]{2,4}\d?\s*[\u4e00-\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险))/
]
for (const pattern of patterns) {
const match = content.match(pattern)
if (match) {
// 清理产品名称
let name = match[1] || match[0]
name = name.replace(/^[A-Z]{2,4}\d?\s*[-::]?\s*/, '')
name = name.split(/[-—::]/)[0].trim()
if (name && name.length > 2) {
return name
}
}
}
return null
}
/**
* 生成产品分割报告
*
* @param {string} content - 原始文档内容
* @param {Array} products - 分割后的产品列表
* @returns {string} Markdown 格式的报告
*/
export function generateSplitReport(content, products) {
let report = `## 📊 产品分割报告\n\n`
report += `### 分割统计\n\n`
report += `- 文档总长度: ${content.length} 字符\n`
report += `- 识别产品数: ${products.length} 个\n\n`
report += `### 产品列表\n\n`
report += `| 序号 | 产品代码 | 产品名称 | 内容长度 |\n`
report += `|------|---------|---------|----------|\n`
products.forEach((product, index) => {
const code = product.code || '-'
const name = product.name || product.fullTitle?.slice(0, 20) || '-'
const length = product.content.length
report += `| ${index + 1} | ${code} | ${name.slice(0, 30)} | ${length} 字符 |\n`
})
return report
}
export {
PRODUCT_TITLE_PATTERNS,
PRODUCT_CODE_PREFIXES
}
......@@ -463,80 +463,6 @@ function smartExtractList(content, startPattern, endKeywords, itemFilter) {
}
/**
* 智能提取所有字段
*
* @param {string} content - 文档内容
* @param {string} fileName - 文件名(用于推断产品名称)
* @returns {{config: Object, unmatched: Array, warnings: Array}} 提取结果
*/
export function smartExtractFields(content, fileName) {
const config = {}
const unmatched = []
const warnings = []
const matchDetails = []
// 按优先级提取字段
const sortedFields = Object.entries(FIELD_RULES).sort((a, b) => a[1].priority - b[1].priority)
for (const [fieldName, rule] of sortedFields) {
const result = extractField(content, fieldName)
// 记录匹配详情
matchDetails.push({
field: fieldName,
matched: result.matched,
pattern: result.pattern,
value: result.value
})
// 如果匹配成功或字段有默认值
if (result.value !== null) {
config[fieldName] = result.value
// 如果使用了默认值,记录警告
if (!result.matched && rule.required) {
warnings.push({
field: fieldName,
message: `未找到字段 "${fieldName}",使用默认值: ${JSON.stringify(rule.fallback)}`,
severity: 'warning'
})
}
} else if (rule.required) {
// 必填字段未匹配
unmatched.push({
field: fieldName,
reason: '未找到匹配内容',
suggestions: generateSuggestions(fieldName, content)
})
}
}
// 产品名称特殊处理:如果未匹配,使用文件名
if (!config.product_name) {
const baseName = fileName.replace(/\.[^/.]+$/, '')
config.product_name = baseName
warnings.push({
field: 'product_name',
message: `未找到产品名称,使用文件名: "${baseName}"`,
severity: 'info'
})
}
// 根据产品类型过滤字段
if (config.product_type !== 'savings') {
delete config.withdrawal_modes
delete config.withdrawal_periods
}
return {
config,
unmatched,
warnings,
matchDetails
}
}
/**
* 生成字段建议值
*
* @param {string} fieldName - 字段名称
......@@ -614,4 +540,131 @@ export function generateAuditReport(result) {
return report
}
/**
* 智能提取所有字段(支持多产品)
*
* @description 从单个产品内容片段中提取字段,优先使用传入的产品名称
* @param {string} content - 产品内容片段
* @param {string} fileName - 文件名
* @param {Object} options - 额外选项
* @param {string} options.productCode - 产品代码(如 GS、GC、FA)
* @param {string} options.productName - 产品名称(从分割器获取)
* @returns {{config: Object, unmatched: Array, warnings: Array, matchDetails: Array}} 提取结果
*/
export function smartExtractFieldsForProduct(content, fileName, options = {}) {
const { productCode, productName } = options
const config = {}
const unmatched = []
const warnings = []
const matchDetails = []
// 按优先级提取字段
const sortedFields = Object.entries(FIELD_RULES).sort((a, b) => a[1].priority - b[1].priority)
for (const [fieldName, rule] of sortedFields) {
// 跳过 product_name,后面特殊处理
if (fieldName === 'product_name') continue
const result = extractField(content, fieldName)
// 记录匹配详情
matchDetails.push({
field: fieldName,
matched: result.matched,
pattern: result.pattern,
value: result.value
})
// 如果匹配成功或字段有默认值
if (result.value !== null) {
config[fieldName] = result.value
// 如果使用了默认值,记录警告
if (!result.matched && rule.required) {
warnings.push({
field: fieldName,
message: `未找到字段 "${fieldName}",使用默认值: ${JSON.stringify(rule.fallback)}`,
severity: 'warning'
})
}
} else if (rule.required) {
// 必填字段未匹配
unmatched.push({
field: fieldName,
reason: '未找到匹配内容',
suggestions: generateSuggestions(fieldName, content)
})
}
}
// ========== 产品名称特殊处理 ==========
// 优先级: 传入的产品名称 > 从内容提取 > 文件名
if (productName) {
// 使用分割器传入的产品名称
config.product_name = productName
matchDetails.unshift({
field: 'product_name',
matched: true,
pattern: 'product_splitter',
value: productName
})
} else {
// 尝试从内容提取
const nameResult = extractField(content, 'product_name')
if (nameResult.matched && nameResult.value) {
config.product_name = nameResult.value
matchDetails.unshift({
field: 'product_name',
matched: true,
pattern: nameResult.pattern,
value: nameResult.value
})
} else {
// 使用文件名
const baseName = fileName.replace(/\.[^/.]+$/, '')
config.product_name = baseName
warnings.push({
field: 'product_name',
message: `未找到产品名称,使用文件名: "${baseName}"`,
severity: 'info'
})
matchDetails.unshift({
field: 'product_name',
matched: false,
pattern: 'filename_fallback',
value: baseName
})
}
}
// 如果有产品代码,添加到配置中
if (productCode) {
config.product_code = productCode
}
// 根据产品类型过滤字段
if (config.product_type !== 'savings') {
delete config.withdrawal_modes
delete config.withdrawal_periods
}
return {
config,
unmatched,
warnings,
matchDetails
}
}
/**
* 智能提取所有字段(原始函数,保持兼容)
*
* @param {string} content - 文档内容
* @param {string} fileName - 文件名(用于推断产品名称)
* @returns {{config: Object, unmatched: Array, warnings: Array}} 提取结果
*/
export function smartExtractFields(content, fileName) {
return smartExtractFieldsForProduct(content, fileName, {})
}
export { FIELD_RULES }
......