parse-docs.js 10 KB
/**
 * 文档解析脚本
 *
 * @description 扫描 docs/to-parse 文件夹中的文档,调用 AI 服务解析,自动更新配置
 * @module scripts/parse-docs
 * @author Claude Code
 * @created 2026-02-13
 *
 * @usage
 * # 解析所有待处理文档
 * npm run parse:docs
 *
 * # 解析指定文档
 * npm run parse:docs -- --file=产品说明书.pdf
 *
 * # 查看待处理文档
 * npm run parse:docs -- --list
 */

import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import readline from 'readline'

// ========== 配置区 ==========

const DOCS_DIR = path.resolve(process.cwd(), 'docs/to-parse')
const CONFIG_FILE = path.resolve(process.cwd(), 'src/config/plan-templates.js')
const BACKUP_DIR = path.resolve(process.cwd(), 'docs/parsed-backup')

// 支持的文档格式
const SUPPORTED_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.md']

// AI 解析服务选择(通过 skill 调用)
const AI_SERVICE = 'openai' // 'openai' | 'anthropic' | 'openrouter'

// ========== 工具函数 ==========

/**
 * 确保目录存在
 */
function ensureDir(dirPath) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true })
    console.log(`📁 创建目录: ${dirPath}`)
  }
}

/**
 * 读取文件内容
 */
function readFile(filePath) {
  return fs.readFileSync(filePath, 'utf-8')
}

/**
 * 写入文件内容
 */
function writeFile(filePath, content) {
  fs.writeFileSync(filePath, content, 'utf-8')
}

/**
 * 获取所有待处理的文档
 */
function getDocsToParse() {
  if (!fs.existsSync(DOCS_DIR)) {
    console.log('📂 文档夹不存在:', DOCS_DIR)
    return []
  }

  const files = fs.readdirSync(DOCS_DIR)
  return files
    .filter(file => SUPPORTED_EXTENSIONS.includes(path.extname(file).toLowerCase()))
    .map(file => ({
      name: file,
      fullPath: path.join(DOCS_DIR, file),
      ext: path.extname(file).toLowerCase(),
      size: fs.statSync(path.join(DOCS_DIR, file)).size
    }))
}

/**
 * 调用 AI 服务解析文档
 *
 * 这里使用 skill 工具调用实际的 AI 解析服务
 * 可以是:file-url-to-pdf + openai/anthropic skill
 */
async function parseDocumentWithAI(docPath) {
  console.log(`\n🤖 正在解析: ${path.basename(docPath)}`)

  try {
    // 方式 1: 使用 PDF 转 base64
    const imageBuffer = fs.readFileSync(docPath)
    const base64 = `data:application/pdf;base64,${imageBuffer.toString('base64')}`

    // 这里应该调用 AI service(通过 skill 或 API)
    // 暂时返回模拟数据用于演示
    const mockConfig = {
      product_name: path.basename(docPath, path.extname(docPath)),
      product_type: 'savings',
      currency: 'USD',
      payment_periods: ['整付', '3年', '5年'],
      age_range: { min: 0, max: 75 },
      insurance_period: '终身',
      is_savings: true,
      withdrawal_modes: ['年龄指定金额', '最高固定金额'],
      withdrawal_periods: ['1年', '3年', '5年', '10年']
    }

    console.log('✅ 解析成功')
    return mockConfig
  } catch (error) {
    console.error(`❌ 解析失败 (${docPath}):`, error.message)
    return null
  }
}

/**
 * 生成 form_sn
 */
function generateFormSn(config) {
  const typePrefix = {
    'life-insurance': 'life',
    'critical-illness': 'ci',
    'savings': 'sav'
  }

  const prefix = typePrefix[config.product_type] || 'prod'
  const timestamp = Date.now().toString(36)

  // 从产品名称生成简化的英文标识
  const nameSlug = config.product_name
    .replace(/[^\w\u4e00-\u9fa5]/g, '')
    .replace(/\s+/g, '-')
    .toLowerCase()
    .substring(0, 20)

  return `${prefix}-${nameSlug}-${timestamp}`
}

/**
 * 生成配置代码
 */
function generateConfigCode(config) {
  const formSn = generateFormSn(config)
  const isSavings = config.is_savings || config.product_type === 'savings'

  let code = `
  /**
   * ${config.product_name}
   * @added ${new Date().toISOString()}
   * @source docs/to-parse/${config.source_file}
   */
  '${formSn}': {
    name: '${config.product_name}',
    component: '${isSavings ? 'SavingsTemplate' : 'InsuranceTemplate'}',
`

  if (isSavings) {
    code += `    category: 'savings',
    config: {
      currency: '${config.currency}',
      payment_periods: ${JSON.stringify(config.payment_periods || [])},
      age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75 } },
      insurance_period: '${config.insurance_period || '终身'}',
      withdrawal_plan: {
        enabled: true,
        currencies: ['HKD', 'USD', 'CNY'],
        default_currency: '${config.currency}',
        withdrawal_modes: ${JSON.stringify(config.withdrawal_modes || [])},
        withdrawal_periods: ${JSON.stringify(config.withdrawal_periods || [])}
      }
    }
  }`
  } else {
    code += `    config: {
      currency: '${config.currency}',
      payment_periods: ${JSON.stringify(config.payment_periods || [])},
      age_range: { min: ${config.age_range?.min || 0}, max: ${config.age_range?.max || 75} },
      insurance_period: '${config.insurance_period || '终身'}'
    }`
  }

  code += `  }\n`

  return { formSn, code }
}

/**
 * 解析单个文档
 */
async function parseSingleFile(filePath) {
  const fileName = path.basename(filePath)
  console.log(`\n${'='.repeat(60)}`)
  console.log(`📄 处理文件: ${fileName}`)
  console.log(`${'='.repeat(60)}`)

  // 解析文档
  const config = await parseDocumentWithAI(filePath)

  if (!config) {
    console.log(`⏭️  跳过文件: ${fileName} (解析失败)`)
    return { success: false, file: fileName }
  }

  // 添加源文件信息
  config.source_file = fileName

  // 生成配置代码
  const { formSn, code } = generateConfigCode(config)

  console.log(`\n📝 生成 form_sn: ${formSn}`)
  console.log(`📋 生成配置代码:\n${code}`)

  return { success: true, formSn, code, file: fileName, config }
}

/**
 * 更新配置文件
 */
function updateConfigFile(newConfigs) {
  console.log(`\n${'='.repeat(60)}`)
  console.log(`📝 更新配置文件: ${CONFIG_FILE}`)
  console.log(`${'='.repeat(60)}`)

  // 备份现有配置
  if (fs.existsSync(CONFIG_FILE)) {
    ensureDir(BACKUP_DIR)
    const backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
    fs.copyFileSync(CONFIG_FILE, backupFile)
    console.log(`💾 已备份到: ${backupFile}`)
  }

  // 读取现有配置
  const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')

  // 找到插入位置(在 PLAN_TEMPLATES = { 之后)
  const insertPosition = existingContent.indexOf('PLAN_TEMPLATES = {')
  if (insertPosition === -1) {
    console.error('❌ 找不到 PLAN_TEMPLATES 定义')
    return
  }

  // 在 PLAN_TEMPLATES 对象结束前插入
  const endPosition = existingContent.indexOf('}', existingContent.lastIndexOf('}'))

  // 计算插入位置:倒数第二个 } 之后(PLAN_TEMPLATES 对象结束)
  let insertAfter = existingContent.lastIndexOf('}')
  // 找到 PLAN_TEMPLATES 的最后一个 }
  const lastObjectEnd = existingContent.lastIndexOf('}', insertAfter - 1)

  if (lastObjectEnd === -1) {
    console.error('❌ 找不到插入位置')
    return
  }

  // 生成要插入的内容
  const insertContent = newConfigs.map(c => c.code).join(',\n')

  // 插入新配置
  const updatedContent =
    existingContent.slice(0, lastObjectEnd + 1) +
    ',\n' + insertContent +
    existingContent.slice(lastObjectEnd + 1)

  // 写入文件
  writeFile(CONFIG_FILE, updatedContent)
  console.log(`✅ 已更新配置文件,新增 ${newConfigs.length} 个产品`)
}

/**
 * 处理所有文档
 */
async function parseAllDocs(docs) {
  if (docs.length === 0) {
    console.log('📭 没有待处理的文档')
    return
  }

  console.log(`\n${'='.repeat(60)}`)
  console.log(`📚 发现 ${docs.length} 个待处理文档`)
  console.log(`${'='.repeat(60)}`)

  const results = []
  const successResults = []

  for (const doc of docs) {
    const result = await parseSingleFile(doc.fullPath)
    results.push(result)
    if (result.success) {
      successResults.push(result)
    }
  }

  // 汇总
  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} 个`)

  // 显示成功的产品
  if (successResults.length > 0) {
    console.log(`\n✅ 成功解析的产品:`)
    successResults.forEach(r => {
      console.log(`   - ${r.formSn}: ${r.config.product_name}`)
    })

    // 更新配置文件
    updateConfigFile(successResults)
  } else {
    console.log(`\n❌ 没有成功解析的文档,配置文件未更新`)
  }
}

/**
 * CLI 入口
 */
async function main() {
  const args = process.argv.slice(2)

  // 检查模式
  const listMode = args.includes('--list')
  const fileMode = args.find(arg => arg.startsWith('--file='))

  console.log('\n🚀 文档解析工具')
  console.log(`   文档目录: ${DOCS_DIR}`)
  console.log(`   配置文件: ${CONFIG_FILE}`)

  if (listMode) {
    // 列出模式
    const docs = getDocsToParse()
    console.log(`\n📋 待处理文档列表:`)
    if (docs.length === 0) {
      console.log('   (无文档)')
    } else {
      docs.forEach((doc, index) => {
        console.log(`   ${index + 1}. ${doc.name} (${formatSize(doc.size)})`)
      })
    }
  } else if (fileMode) {
    // 单文件模式
    const fileName = fileMode.split('=')[1]
    const docs = getDocsToParse()
    const targetDoc = docs.find(d => d.name === fileName || d.name.includes(fileName))

    if (targetDoc) {
      await parseSingleFile(targetDoc.fullPath)
    } else {
      console.log(`❌ 找不到文件: ${fileName}`)
    }
  } else {
    // 批量处理模式
    const docs = getDocsToParse()
    await parseAllDocs(docs)
  }

  console.log('\n✨ 处理完成!')
}

/**
 * 格式化文件大小
 */
function formatSize(bytes) {
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}

// 运行
main().catch(error => {
  console.error('❌ 执行失败:', error)
  process.exit(1)
})