Files
2026-07-13 19:57:46 +08:00

276 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <message.txt> [--audit-dir <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,
};