#!/usr/bin/env node const fs = require('node:fs'); const path = require('node:path'); const orderAdapter = require('./erp_wechat_adapter'); const updateOrderParser = require('./erp_update_order_parser'); const travelerLists = require('./erp_traveler_list'); const FIELD_LABELS = [ '操作类型', '订单编号', '修改内容', '下单模式', ]; function normalizeText(input) { return String(input || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); } function cleanValue(value) { return String(value || '').trim().replace(/[。;;]+$/g, '').trim(); } function escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function getLineValue(text, labels) { const labelList = Array.isArray(labels) ? labels : [labels]; for (const line of normalizeText(text).split('\n')) { for (const label of labelList) { const match = line.match(new RegExp(`^\\s*${escapeRegex(label)}\\s*[::]\\s*(.*?)\\s*$`)); if (match) return cleanValue(match[1]); } } return ''; } function getBlockValue(text, labels) { const labelList = Array.isArray(labels) ? labels : [labels]; const normalized = normalizeText(text); for (const label of labelList) { const nextLabels = FIELD_LABELS.filter((item) => item !== label).map(escapeRegex).join('|'); const regex = new RegExp(`${escapeRegex(label)}\\s*[::]\\s*([\\s\\S]*?)(?=\\n\\s*(?:${nextLabels})\\s*[::]|$)`); const match = normalized.match(regex); if (match) return cleanValue(match[1]); } return ''; } function normalizeOperation(value, text = '') { const explicit = cleanValue(value); const normalizedText = normalizeText(text); if (!explicit && /^\s*下单模式\s*[::]/m.test(normalizedText)) return 'create_order'; const source = explicit || normalizedText; if ( !explicit && extractIdentifier(normalizedText) && /(导入|录入|补录|补名单|名单|游客|旅客|护照|附件|excel|traveler|passport|attachment|import)/i.test(normalizedText) ) { return 'update_order'; } if (/补充|修改|变更|更新/i.test(source)) return 'update_order'; if (/导出|确认件|确认单|PDF/i.test(source)) return 'export_confirmation'; return 'create_order'; } function extractIdentifier(text) { const explicit = getLineValue(text, ['订单编号', '团号', '子单号', '母团号']); const match = explicit || ((String(text || '').match(/\bD\d{4,}\b|[A-Z]{1,4}-\d{6,8}[A-Z]?-A\b/i) || [])[0] || ''); return String(match || '').trim().toUpperCase(); } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function writeAudit(result, auditDir) { const targetDir = auditDir || path.join(process.cwd(), 'runtime', 'erp-order-entry', 'audit'); ensureDir(targetDir); const safeOperation = result.operation || 'unknown'; const auditPath = path.join(targetDir, `${new Date().toISOString().replace(/[:.]/g, '-')}_${safeOperation}.json`); const payload = { ...result, auditPath }; fs.writeFileSync(auditPath, JSON.stringify(cloneForAudit(payload), null, 2), 'utf8'); return payload; } function cloneForAudit(value) { const clone = JSON.parse(JSON.stringify(value || {})); const plan = clone.task && clone.task.updatePlan; if (plan && plan.supplemental && plan.supplemental.travelerList) { plan.supplemental.travelerList = travelerLists.sanitizeTravelerListForAudit(plan.supplemental.travelerList); } if (plan && Array.isArray(plan.actions)) { plan.actions = plan.actions.map((action) => { if (action && action.target === 'supplemental.travelerList') { return { ...action, value: travelerLists.sanitizeTravelerListForAudit(action.value), }; } return action; }); } return clone; } function missingIdentifierResult(operation, auditDir, originalText) { return writeAudit({ status: 'needs_clarification', operation, route: '', missingFields: ['订单编号'], task: null, customerMessage: '这条指令还缺订单编号。请补充团号、母团号或子单号后我再继续。', originalText, }, auditDir); } function handleCreateOrder(text, options) { const result = orderAdapter.handleIncomingOrder(text, options); if (result.task) { result.operation = 'create_order'; result.task.operation = 'create_order'; result.task.delivery = { ...(result.task.delivery || {}), deferConfirmationExport: true, sendPdfFollowUp: false, }; } return result; } function normalizeAttachments(value) { return Array.isArray(value) ? value.filter(Boolean).map(String) : []; } function parseTravelerListAttachment(attachments = []) { const source = attachments.find((item) => /\.(?:xls|xlsx|csv|tsv)$/i.test(String(item || ''))); if (!source) return null; return { ...travelerLists.parseTravelerListFile(source), validationStatus: 'pending_order_pax', }; } function onlyMissingUpdateContent(missingFields = []) { return missingFields.length <= 1 && (!missingFields.length || /修改内容|淇敼鍐呭/.test(String(missingFields[0]))); } function attachTravelerListToUpdatePlan(updatePlan, attachments = []) { const list = parseTravelerListAttachment(attachments); if (!list) return updatePlan; const currentActions = Array.isArray(updatePlan && updatePlan.actions) ? updatePlan.actions : []; const mayBecomeReady = updatePlan && updatePlan.status === 'ready' || onlyMissingUpdateContent(updatePlan && updatePlan.missingFields || []); return { ...(updatePlan || {}), status: mayBecomeReady ? 'ready' : updatePlan.status, missingFields: mayBecomeReady ? [] : (updatePlan.missingFields || []), actions: [ ...currentActions.filter((action) => action && action.target !== 'supplemental.travelerList'), { target: 'supplemental.travelerList', operation: 'upsert', value: list, source: 'traveler-list attachment', }, ], supplemental: { ...((updatePlan && updatePlan.supplemental) || {}), travelerList: list, }, customerMessage: mayBecomeReady ? '已收到补充名单附件。' : (updatePlan.customerMessage || ''), }; } function parseExportTypes(text) { const source = String(text || ''); const types = ['xingyou-confirm']; if (/联泰|liantai|orders_confirm_new/i.test(source)) types.push('liantai-confirm'); if (/\bjob\b|JOB|备案|teams_beian/i.test(source)) types.push('job-order'); return [...new Set(types)]; } function handleIncomingMessage(input, options = {}) { const originalText = normalizeText(input); const auditDir = options.auditDir; const attachments = normalizeAttachments(options.attachments); const operation = normalizeOperation(getLineValue(originalText, '操作类型'), originalText); if (operation === 'create_order') return handleCreateOrder(originalText, options); const identifier = extractIdentifier(originalText); if (!identifier) return missingIdentifierResult(operation, auditDir, originalText); const updateText = operation === 'update_order' ? (getBlockValue(originalText, '修改内容') || originalText) : ''; let updatePlan = operation === 'update_order' ? updateOrderParser.parseUpdateOrderText(updateText) : null; if (operation === 'update_order') { updatePlan = attachTravelerListToUpdatePlan(updatePlan, attachments); } if (operation === 'update_order' && updatePlan.status !== 'ready') { return writeAudit({ status: 'needs_clarification', operation, route: '', missingFields: updatePlan.missingFields || ['修改内容'], task: null, customerMessage: updatePlan.customerMessage || '这条修改指令还不够明确,请补充修改内容后我再继续。', originalText, }, auditDir); } const task = { schemaVersion: 'erp-task-v1', operation, route: '', identifier, updateText, updatePlan: updatePlan || undefined, attachments, exportTypes: operation === 'export_confirmation' ? parseExportTypes(originalText) : [], originalText, createdAt: new Date().toISOString(), }; return writeAudit({ status: 'ready', operation, route: '', task, customerMessage: operation === 'update_order' ? `已收到修改指令:${identifier}。` : `已收到确认件导出指令:${identifier}。`, originalText, }, auditDir); } function parseArgs(argv) { const args = { input: '', auditDir: '', attachments: [], json: false }; for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--input') args.input = argv[index += 1] || ''; else if (arg === '--audit-dir') args.auditDir = argv[index += 1] || ''; else if (arg === '--attachment') args.attachments.push(argv[index += 1] || ''); else if (arg.startsWith('--attachment=')) args.attachments.push(arg.slice('--attachment='.length)); else if (arg === '--json') args.json = true; } return args; } function main() { const args = parseArgs(process.argv); if (!args.input) { console.error('Usage: node tools/erp_operation_adapter.js --input [--audit-dir ] [--json]'); process.exitCode = 1; return; } const input = fs.readFileSync(args.input, 'utf8'); const result = handleIncomingMessage(input, { auditDir: args.auditDir || undefined, attachments: args.attachments }); if (args.json) console.log(JSON.stringify(result, null, 2)); else console.log(result.customerMessage || result.status); } if (require.main === module) main(); module.exports = { normalizeText, getLineValue, getBlockValue, normalizeOperation, extractIdentifier, handleIncomingMessage, parseArgs, parseExportTypes, };