(function installLTJTOperationPlans(root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (root) root.LTJTOperationPlans = api; }(typeof self !== 'undefined' ? self : globalThis, () => { const OPERATION_DEFINITIONS = { team_order_create: { label: '团队-单个下单', route: 'team_single', execution: 'browser_team_single', liveSubmit: 'test-only', source: 'root-browser-validated' }, team_order_batch_create: { label: '团队-批量下单', route: 'team_batch', execution: 'browser_team_single_fallback', nativeStatus: 'deferred', fallback: 'team_single_per_date', source: 'handoff-fallback-rule' }, shared_plan_create: { label: '散拼-创建母团计划', route: 'split_parent', execution: 'browser_live_split_parent', liveSubmit: 'test-only', source: 'handoff-browser-validated' }, shared_child_order_create: { label: '散拼-拼单/录入子单', route: 'split_child', execution: 'browser_live_split_child', liveSubmit: 'test-only', source: 'handoff-browser-validated' }, order_update: { label: '已有订单更新', route: 'update_order', execution: 'browser_dry_run', dryRunAdapter: 'inspect_existing_order_update', supportedTargets: ['rooms', 'remark', 'pax', 'prices'], source: 'handoff-update-rule' }, passenger_list_import: { label: '旅客名单导入/补充', route: 'update_order', execution: 'browser_dry_run', dryRunAdapter: 'inspect_existing_order_traveler', supportedTargets: ['travelerList'], source: 'handoff-traveler-rule' }, confirmation_export: { label: '确认件导出/恢复', route: 'export_confirmation', execution: 'browser_export_source', dryRunAdapter: 'inspect_confirmation_sources', source: 'handoff-browser-validated' } }; const ACTION_ALIASES = { create_order: { team_single: 'team_order_create', team_batch: 'team_order_batch_create', split_parent: 'shared_plan_create', split_child: 'shared_child_order_create' }, update_order: 'order_update', export_confirmation: 'confirmation_export' }; const EXPORT_TYPE_ALIASES = { confirmation: 'xingyou-confirm', confirm: 'xingyou-confirm', xingyou: 'xingyou-confirm', 'xingyou-confirm': 'xingyou-confirm', liantai: 'liantai-confirm', 'liantai-confirm': 'liantai-confirm', job: 'job-order', beian: 'job-order', 'job-order': 'job-order', }; function toNumber(value) { const normalized = String(value ?? '').replace(/[,,]/g, '').trim(); const number = Number(normalized); return Number.isFinite(number) ? number : 0; } function dateYyyyMD(value) { if (typeof value !== 'string') return String(value || ''); const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); if (!match) return value; return `${match[1]}-${Number(match[2])}-${Number(match[3])}`; } function compactDate(value) { const match = String(value || '').match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); if (!match) return String(value || '').replace(/-/g, ''); return `${match[1]}${match[2].padStart(2, '0')}${match[3].padStart(2, '0')}`; } function passengerTotal(counts = {}) { return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader'] .reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0); } function actionForOperation(operation = {}) { const direct = String(operation.action || '').trim(); if (OPERATION_DEFINITIONS[direct]) return direct; const legacy = String(operation.operation || '').trim(); if (legacy === 'create_order') { return ACTION_ALIASES.create_order[String(operation.route || '').trim()] || ''; } return ACTION_ALIASES[legacy] || ''; } function identifierForOperation(operation = {}, data = {}) { if (operation.identifier) return String(operation.identifier).trim(); const refs = data.existing_refs || {}; return String( refs.order_no || refs.group_no || refs.child_order_no || refs.parent_group_no || refs.identifier || '' ).trim(); } function exportTypesForOperation(operation = {}, data = {}) { const source = Array.isArray(operation.exportTypes) && operation.exportTypes.length ? operation.exportTypes : (Array.isArray(data.exportTypes || data.export_types) && (data.exportTypes || data.export_types).length ? (data.exportTypes || data.export_types) : [data.confirmation?.type || 'xingyou-confirm']); return [...new Set(source.map((value) => { const key = String(value || '').trim().toLowerCase(); return EXPORT_TYPE_ALIASES[key] || key; }))]; } function normalizePax(value = {}) { if (typeof value === 'number' || typeof value === 'string') return { adult: toNumber(value) }; return { adult: toNumber(value.adult), child_bed: toNumber(value.child_bed ?? value.childBed), child_no_bed: toNumber(value.child_no_bed ?? value.childNoBed), infant: toNumber(value.infant), leader: toNumber(value.leader), expected_total: value.expected_total ?? value.expectedTotal, }; } function normalizeCreateFields(fields = {}) { const departureDates = Array.isArray(fields.departureDates) ? fields.departureDates : (fields.departureDate ? [fields.departureDate] : []); const orderNature = fields.orderNature || fields.order_nature || ''; const parentGroupNo = fields.parentGroupNo || fields.parent_group_no || ''; const productName = fields.productName || fields.product || fields.productRoute || ''; const bookingCustomer = fields.bookingCustomer || fields.channelCustomer || fields.customer || ''; const pax = fields.pax || fields.passengerCounts || fields.defaultPax || {}; return { order_mode: fields.orderMode || fields.order_mode || '', product: { name: productName, route: fields.productRoute || fields.route || productName, cpid: fields.cpid || '' }, customer: { name: bookingCustomer, id: fields.customerId || fields.bookingCustomerId || '', contact: fields.contact || fields.bookingContact || '', origin: fields.customerOrigin || fields.origin || '' }, route: { name: fields.route || fields.productRoute || productName }, departure_dates: departureDates, passenger_counts: normalizePax(pax), room_counts: fields.rooms || fields.roomCounts || fields.defaultRooms || {}, prices: fields.prices || {}, op_user: { name: fields.op || fields.opUser || fields.followOp || fields.follow_op || '' }, sales_user: { name: fields.salesperson || fields.salesUser || '' }, special_requests: fields.remark || fields.specialRequests || '', test_marker: fields.testMarker || '', order_nature: orderNature, parent_group_no: parentGroupNo, child_order_no: fields.childOrderNo || fields.child_order_no || '', planned_capacity: fields.plannedGuests || fields.plannedPax || fields.planned_capacity || 0, groups_per_date: fields.parentPlansPerDate || fields.groupsPerDate || fields.groups_per_date || 1, cycle: fields.cycle || '', date_adjustments: fields.dateAdjustments || fields.adjustments || {}, supplemental: fields.supplemental || {}, system_defaults: fields.system_defaults || fields.systemDefaults || {}, }; } function updateActionsFromSource(source = {}) { const plan = source.updatePlan || source.update_plan || source.data?.updatePlan || {}; if (Array.isArray(plan.actions)) return plan.actions; if (Array.isArray(source.actions)) return source.actions; if (Array.isArray(source.data?.updates?.actions)) return source.data.updates.actions; return []; } function hasTravelerAction(actions = []) { return actions.some((item) => /^travelerList(?:\.|$)/i.test(String(item?.target || '')) || String(item?.operation || '').toLowerCase() === 'traveler_upsert'); } function normalizeOperation(input = {}) { const source = input.task && typeof input.task === 'object' ? input.task : input; const directAction = String(source.action || '').trim(); if (OPERATION_DEFINITIONS[directAction]) return source; const operation = String(source.operation || '').trim(); if (operation === 'create_order') { const data = normalizeCreateFields(source.fields || {}); return { ...source, action: ACTION_ALIASES.create_order[String(source.route || '').trim()] || '', order_nature: data.order_nature || source.orderNature || '', submit_mode: source.submit_mode || 'dry_run', data: { ...data, original_text: source.originalText || source.original_text || '', attachments: Array.isArray(source.attachments) ? source.attachments : [], }, originalText: source.originalText || source.original_text || '', }; } if (operation === 'update_order') { const actions = updateActionsFromSource(source); const action = hasTravelerAction(actions) && actions.every((item) => /^travelerList(?:\.|$)/i.test(String(item?.target || '')) || String(item?.operation || '').toLowerCase() === 'traveler_upsert') ? 'passenger_list_import' : 'order_update'; return { ...source, action, identifier: source.identifier || source.data?.existing_refs?.identifier || '', data: { ...(source.data || {}), existing_refs: { ...(source.data?.existing_refs || {}), identifier: source.identifier || source.data?.existing_refs?.identifier || '' }, updates: source.updatePlan || source.data?.updates || { actions }, updatePlan: source.updatePlan || source.data?.updatePlan || { actions }, attachments: Array.isArray(source.attachments) ? source.attachments : [], }, }; } if (operation === 'export_confirmation') { return { ...source, action: 'confirmation_export', exportTypes: Array.isArray(source.exportTypes) && source.exportTypes.length ? source.exportTypes : (Array.isArray(source.data?.exportTypes || source.data?.export_types) ? (source.data.exportTypes || source.data.export_types) : undefined), identifier: source.identifier || source.data?.existing_refs?.identifier || '', data: { ...(source.data || {}), existing_refs: { ...(source.data?.existing_refs || {}), identifier: source.identifier || source.data?.existing_refs?.identifier || '' }, confirmation: { type: Array.isArray(source.exportTypes) && source.exportTypes.length ? source.exportTypes[0] : 'xingyou-confirm', }, }, }; } return source; } function addMissing(blockers, label, value) { if (value === undefined || value === null || String(value).trim() === '') blockers.push(label); } function validateCreate(operation, action, data, blockers, warnings) { const counts = data.passenger_counts || {}; if (!data.product?.name) blockers.push('缺少产品名称。'); if (!data.op_user?.name) blockers.push('缺少计调 OP。'); if (!data.sales_user?.name) blockers.push('缺少销售人。'); if (passengerTotal(counts) <= 0) blockers.push('人数合计必须大于 0。'); if (counts.expected_total != null && toNumber(counts.expected_total) !== passengerTotal(counts)) { blockers.push('expected_total 与人数合计不一致。'); } if (action === 'team_order_create') { if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1 || !data.departure_dates[0]) { blockers.push('团队单必须有且只有一个出发日期。'); } addMissing(blockers, '缺少 data.test_marker。', data.test_marker); if (operation.submit_mode !== 'dry_run') { blockers.push('submit_mode 必须保持 dry_run,真实保存由插件安全门单独控制。'); } if (operation.order_nature !== 'test') { blockers.push('当前插件自动保存只允许 order_nature=test 的测试订单。'); } return; } if (!Array.isArray(data.departure_dates) || data.departure_dates.length < 2) { blockers.push('团队批量下单至少需要两个出发日期。'); } warnings.push('原生 DoInfoJHs 尚未验证;当前业务规则要求按日期使用 team_single fallback。'); if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) { warnings.push('批量 fallback 当前仅允许带测试性质、dry_run 和测试标记的任务;本任务只生成计划,不写 ERP。'); } } function validateOperation(operation = {}) { const action = actionForOperation(operation); const definition = OPERATION_DEFINITIONS[action] || null; const data = operation.data || {}; const blockers = []; const warnings = []; if (!definition) { return { ok: false, action, definition: null, execution: 'blocked', blockers: [`不支持的 ERP 业务类型:${operation.action || operation.operation || '未填写'}。`], warnings, }; } if (action === 'team_order_create' || action === 'team_order_batch_create') { validateCreate(operation, action, data, blockers, warnings); } else if (action === 'shared_plan_create') { addMissing(blockers, '缺少产品名称。', data.product?.name); if (!Array.isArray(data.departure_dates) || !data.departure_dates.length) blockers.push('缺少母团出发日期。'); if (Array.isArray(data.departure_dates) && data.departure_dates.length !== 1) blockers.push('当前母团浏览器执行器一次只允许一个出发日期。'); if (toNumber(data.planned_capacity) <= 0) blockers.push('计划收客数必须大于 0。'); addMissing(blockers, '缺少跟团/接待 OP。', data.op_user?.name); addMissing(blockers, '缺少预订客户。', data.customer?.name); if (toNumber(data.groups_per_date) <= 0) blockers.push('每天母团计划数必须大于 0。'); if (toNumber(data.groups_per_date || 1) !== 1) blockers.push('当前母团浏览器执行器每天只允许一个计划。'); if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) { blockers.push('散拼母团真实保存只允许带测试性质、dry_run 和测试标记的任务。'); } warnings.push('散拼母团当前使用已验证的浏览器提交路径,仅允许测试订单。'); } else if (action === 'shared_child_order_create') { addMissing(blockers, '缺少渠道客户。', data.customer?.name); if (passengerTotal(data.passenger_counts || {}) <= 0) blockers.push('人数合计必须大于 0。'); addMissing(blockers, '缺少计调 OP。', data.op_user?.name); addMissing(blockers, '缺少销售人。', data.sales_user?.name); const parentGroupNo = data.parent_group_no || data.existing_refs?.parent_group_no || ''; if (!parentGroupNo && !(data.product?.name && Array.isArray(data.departure_dates) && data.departure_dates.length)) { blockers.push('缺少母团/子单已有编号,或产品与出发日期。'); } if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1) blockers.push('子单必须有且只有一个出发日期。'); if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) { blockers.push('散拼子单真实保存只允许带测试性质、dry_run 和测试标记的任务。'); } warnings.push('散拼子单当前使用已验证的浏览器提交路径,仅允许测试订单。'); } else if (action === 'order_update') { addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data)); const plan = operation.updatePlan || data.updatePlan || data.updates; const actions = Array.isArray(plan?.actions) ? plan.actions : []; if (!actions.length && !data.updates) blockers.push('缺少结构化修改内容。'); if (actions.some((item) => !item || !item.target || !item.operation)) blockers.push('修改动作必须包含 target 和 operation。'); const unsupportedTargets = actions.filter((item) => !isSupportedUpdateTarget(item?.target)); if (unsupportedTargets.length) blockers.push(`包含未接入的修改目标:${unsupportedTargets.map((item) => item.target).join('、')}。`); if (actions.some((item) => /^prices\.|^pax\./.test(String(item?.target || '')))) { warnings.push('价格/人数跨路线更新仍属于 optional/unverified,本次只记录计划,不承诺等价执行。'); } warnings.push('当前只做订单定位和修改预览,实际编辑页提交仍需独立预检与显式授权。'); } else if (action === 'passenger_list_import') { addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data)); const attachments = Array.isArray(data.attachments) ? data.attachments : []; const passengerList = data.passenger_list || data.travelerList; const plan = operation.updatePlan || data.updatePlan || data.updates || {}; const actions = Array.isArray(plan?.actions) ? plan.actions : []; if (!attachments.length && !passengerList && !hasTravelerAction(actions)) blockers.push('缺少旅客名单附件或 passenger_list 描述。'); if (actions.length && actions.some((item) => !/^travelerList(?:\.|$)/i.test(String(item?.target || '')) && String(item?.operation || '').toLowerCase() !== 'traveler_upsert')) { blockers.push('旅客名单任务不能混入未声明的非旅客修改动作。'); } warnings.push('当前只定位订单并校验旅客导入计划;文件解析、粘贴导入和行数回查仍需独立执行器。'); } else if (action === 'confirmation_export') { addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data)); if (data.route === 'split_parent' || operation.route === 'split_parent') blockers.push('母团计划不能直接导出客户确认件。'); const exportTypes = exportTypesForOperation(operation, data); const unsupported = exportTypes.filter((type) => !['xingyou-confirm', 'liantai-confirm', 'job-order'].includes(type)); if (unsupported.length) blockers.push(`包含不支持的确认件类型:${unsupported.join('、')}。`); if (operation.recovery?.exportOnly && operation.recovery?.neverResave !== true) { blockers.push('恢复导出任务必须同时声明 neverResave=true。'); } warnings.push('导出路径只读取 ERP 源文件并回报响应摘要;插件不会重保存订单,也不会把未落盘文件误报为已交付。'); } let execution = definition.execution; if ( action === 'team_order_batch_create' && (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) ) { execution = 'planned_only'; } return { ok: blockers.length === 0, action, definition, execution: blockers.length ? 'blocked' : execution, blockers, warnings, identifier: identifierForOperation(operation, data), exportTypes: action === 'confirmation_export' ? exportTypesForOperation(operation, data) : [], noErpWrite: !['browser_team_single', 'browser_team_single_fallback', 'browser_live_split_parent', 'browser_live_split_child'].includes(execution), }; } function expandTeamBatch(operation = {}) { const data = operation.data || {}; const dates = Array.isArray(data.departure_dates) ? data.departure_dates : []; const marker = String(data.test_marker || 'TEAM-BATCH-TEST').trim(); return dates.map((date, index) => ({ ...operation, action: 'team_order_create', data: { ...data, departure_dates: [dateYyyyMD(date)], test_marker: `${marker}-D${compactDate(date)}`, }, source: { ...(operation.source || {}), batch_index: index + 1, batch_total: dates.length, }, })); } function publicPlan(result = {}) { return { action: result.action || '', label: result.definition?.label || '', route: result.definition?.route || '', execution: result.execution || 'blocked', native_status: result.definition?.nativeStatus || '', fallback: result.definition?.fallback || '', dry_run_adapter: result.definition?.dryRunAdapter || '', identifier: result.identifier || '', export_types: Array.isArray(result.exportTypes) ? result.exportTypes : [], no_erp_write: result.noErpWrite !== false, blockers: Array.isArray(result.blockers) ? result.blockers : [], warnings: Array.isArray(result.warnings) ? result.warnings : [], }; } function operationSummary(operation = {}) { const action = actionForOperation(operation); const definition = OPERATION_DEFINITIONS[action] || {}; const data = operation.data || {}; return { action, label: definition.label || action || '未知 ERP 业务', route: definition.route || operation.route || '', product: data.product?.name || data.productName || '', departure_dates: Array.isArray(data.departure_dates) ? data.departure_dates : [], identifier: identifierForOperation(operation, data), test_marker: data.test_marker || '', }; } function isSupportedUpdateTarget(target = '') { const value = String(target || '').trim(); return value === 'remark' || value === 'xiadanbeizhu' || /^rooms\.(SGL|TWN|TRP|DBL|HNM|TL)$/i.test(value) || /^pax\.(adult|child_bed|childBed|child_no_bed|childNoBed|infant|leader)$/i.test(value) || /^prices\.(adult|child_bed|childBed|child_no_bed|childNoBed|infant|leader)$/i.test(value) || /^travelerList(?:\.|$)/i.test(value); } function canonicalUpdateTarget(target = '') { return String(target || '').replace(/\.(childBed|childNoBed)$/i, (match, key) => `.${key.toLowerCase() === 'childbed' ? 'child_bed' : 'child_no_bed'}`); } function clone(value) { if (value === undefined) return undefined; return JSON.parse(JSON.stringify(value)); } function snapshotValue(snapshot = {}, target = '') { if (target === 'remark' || target === 'xiadanbeizhu') return snapshot.remark || ''; const match = String(target).match(/^(rooms|pax|prices)\.(.+)$/); if (!match) return undefined; return snapshot[match[1]]?.[match[2]]; } function setSnapshotValue(snapshot, target, value) { const match = String(target).match(/^(rooms|pax|prices)\.(.+)$/); if (target === 'remark' || target === 'xiadanbeizhu') { snapshot.remark = String(value ?? ''); return; } if (!match) return; if (!snapshot[match[1]]) snapshot[match[1]] = {}; snapshot[match[1]][match[2]] = value; } function applyUpdatePlanToSnapshot(updatePlan = {}, currentSnapshot = {}) { const snapshot = clone(currentSnapshot) || {}; const actions = Array.isArray(updatePlan.actions) ? updatePlan.actions : []; const changes = []; for (const action of actions) { const target = canonicalUpdateTarget(String(action?.target || '').trim()); const operation = String(action?.operation || '').trim().toLowerCase(); if (!isSupportedUpdateTarget(target) || /^travelerList(?:\.|$)/i.test(target)) { return { status: 'blocked', reason: `unsupported_update_target:${target}` }; } const before = snapshotValue(snapshot, target); let after; if (operation === 'set') { after = target === 'remark' || target === 'xiadanbeizhu' ? String(action.value ?? '') : toNumber(action.value); } else if (operation === 'delta') { if (before === undefined || before === null || before === '') return { status: 'blocked', reason: `delta_baseline_missing:${target}` }; after = toNumber(before) + toNumber(action.value); } else if (operation === 'append') { if (target !== 'remark' && target !== 'xiadanbeizhu') return { status: 'blocked', reason: `append_target_invalid:${target}` }; after = [String(before || '').trim(), String(action.value || '').trim()].filter(Boolean).join('\n'); } else { return { status: 'blocked', reason: `unsupported_update_operation:${operation}` }; } if (['rooms', 'pax', 'prices'].includes(String(target).split('.')[0]) && after < 0) { return { status: 'blocked', reason: `negative_update_result:${target}` }; } setSnapshotValue(snapshot, target, after); changes.push({ target, operation, before: clone(before), after: clone(after) }); } return { status: 'ready', snapshot, changes }; } function buildUpdatePreview(operation = {}, currentSnapshot = {}) { const data = operation.data || {}; const updatePlan = operation.updatePlan || data.updatePlan || data.updates || {}; const applied = applyUpdatePlanToSnapshot(updatePlan, currentSnapshot); return { status: applied.status, identifier: identifierForOperation(operation, data), changes: applied.changes || [], snapshot: applied.snapshot, write_targets: (applied.changes || []).map((item) => item.target), no_erp_write: true, reason: applied.reason || '' }; } function buildConfirmationExportPlan(operation = {}, record = {}, basePath = '/System/Business') { const data = operation.data || {}; const types = exportTypesForOperation(operation, data); const ddid = String(record.ddid || record.did || data.existing_refs?.ddid || '').trim(); const tid = String(record.tid || data.existing_refs?.tid || '').trim(); const identifier = identifierForOperation(operation, data); const artifacts = types.map((type) => { if (type === 'xingyou-confirm') return { type, path: `${basePath}/orders_confirm_news.asp`, params: { did: ddid, tid } }; if (type === 'liantai-confirm') return { type, path: `${basePath}/orders_confirm_new.asp`, params: { did: ddid, tid } }; if (type === 'job-order') return { type, path: `${basePath}/teams_beian1.asp`, params: { did: ddid } }; return { type, path: '', params: {} }; }); return { identifier, export_only: true, never_resave: true, types, artifacts, ready_for_browser_lookup: Boolean(identifier), no_erp_write: true }; } return { OPERATION_DEFINITIONS, actionForOperation, dateYyyyMD, expandTeamBatch, applyUpdatePlanToSnapshot, buildConfirmationExportPlan, buildUpdatePreview, isSupportedUpdateTarget, normalizeOperation, operationSummary, publicPlan, validateOperation, }; }));