276 lines
9.5 KiB
JavaScript
276 lines
9.5 KiB
JavaScript
#!/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,
|
||
};
|