'use strict'; importScripts('operation-plans.js', 'execution-guard.js'); const runningTasks = new Set(); const cancelledTasks = new Set(); const operationPlans = self.LTJTOperationPlans; const executionGuard = self.LTJTExecutionGuard; let storageMutationQueue = Promise.resolve(); function nowIso() { return new Date().toISOString(); } async function delay(ms, taskId = '') { await new Promise((resolve) => setTimeout(resolve, ms)); throwIfTaskCancelled(taskId); } function taskRootId(taskId) { return String(taskId || '').split('#')[0]; } function isTaskCancelled(taskId) { const normalized = String(taskId || ''); return Boolean(normalized) && (cancelledTasks.has(normalized) || cancelledTasks.has(taskRootId(normalized))); } function cancellationError(taskId) { const error = new Error(`任务 ${taskId} 已取消。`); error.code = 'task_cancelled'; return error; } function throwIfTaskCancelled(taskId) { if (isTaskCancelled(taskId)) throw cancellationError(taskId); } function isBusinessSystemUrl(url) { return /^http:\/\/(localhost|127\.0\.0\.1)(?::8765)?\//i.test(url || ''); } function toNumber(value) { const normalized = String(value ?? '').replace(/[,,]/g, '').trim(); const num = Number(normalized); return Number.isFinite(num) ? num : 0; } function dateYyyyMD(value) { if (typeof value !== 'string') return 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 validateOperation(operation) { return operationPlans.validateOperation(operation).blockers; } function summaryFromOperation(operation) { const operationSummary = operationPlans.operationSummary(operation); const data = operation.data || {}; const counts = data.passenger_counts || {}; const rooms = data.room_counts || {}; const prices = data.prices || {}; const passengerTotal = ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader'].reduce((sum, key) => sum + toNumber(counts[key]), 0); const roomTotal = ['SGL', 'TWN', 'TRP', 'DBL', 'HNM', 'TL'].reduce((sum, key) => sum + toNumber(rooms[key]), 0); const amount = (toNumber(counts.adult) * toNumber(prices.adult)) + (toNumber(counts.child_bed) * toNumber(prices.child_bed)) + (toNumber(counts.child_no_bed) * toNumber(prices.child_no_bed)) + (toNumber(counts.infant) * toNumber(prices.infant)) + (toNumber(counts.leader) * toNumber(prices.leader)); return { action: operationSummary.action, operation_label: operationSummary.label, route: operationSummary.route, order_nature: operation.order_nature, task_id: operation.source?.task_id || operation.task_id || '', product: data.product?.name || '', departure_date: data.departure_dates?.[0] || '', test_marker: data.test_marker || '', passenger_total: passengerTotal, room_total: roomTotal, receivable_amount_total: amount, op_user: data.op_user?.name || '', sales_user: data.sales_user?.name || '' }; } function erpReceiptFromReport(report) { if (report?.erp_receipt) return report.erp_receipt; const alerts = Array.isArray(report?.alerts) ? report.alerts : []; return alerts.map((alert) => alert.erp_receipt).find(Boolean) || null; } function summarizeReportForBusiness(operation, stage, report) { const status = report?.status || 'unknown'; const erpReceipt = erpReceiptFromReport(report); return { stage, status, reason: report?.reason || '', blockers: Array.isArray(report?.blockers) ? report.blockers : [], warnings: Array.isArray(report?.warnings) ? report.warnings : [], operation_plan: report?.operation_plan || undefined, no_erp_write: report?.no_erp_write, write_attempted: report?.write_attempted === true, test_marker: report?.test_markers?.suffix_marker || operation?.data?.test_marker || '', payload_sha256: report?.current_payload?.payload_sha256 || report?.final_form?.submit_payload_sha256 || report?.submit_intercept?.intercepted_submits?.[0]?.payload_sha256 || '', lookup_checks: Array.isArray(report?.lookup_checks) ? report.lookup_checks.map((check) => ({ name: check.name, exact_match_count: check.exact_match_count, contains_match_count: check.contains_match_count, match_rule: check.match_rule })) : [], erp_response_summary: report?.ajax_records?.[0] ? { success_hint: Boolean(report.ajax_records[0].response_contains_success_hint), login_timeout: Boolean(report.ajax_records[0].response_contains_login_timeout), permission_error: Boolean(report.ajax_records[0].response_contains_permission_text), error_hint: Boolean(report.ajax_records[0].response_contains_error_hint) } : undefined, erp_receipt: erpReceipt || undefined, verification: report?.response ? { status, marker_occurrences: report.response.marker_occurrences || 0, login_timeout: Boolean(report.response.contains_login_timeout_text), permission_error: Boolean(report.response.contains_permission_text) } : undefined }; } function withStorageMutation(callback) { const run = storageMutationQueue.then(callback, callback); storageMutationQueue = run.then(() => undefined, () => undefined); return run; } async function getExecutionRecord(taskId) { const rootId = executionGuard.rootTaskId(taskId); const saved = await chrome.storage.local.get('businessTaskExecutions'); return saved.businessTaskExecutions?.[rootId] || null; } async function acceptExecution(taskId, executionId, taskPayload) { const rootId = executionGuard.rootTaskId(taskId); return withStorageMutation(async () => { const saved = await chrome.storage.local.get(['businessTaskExecutions', 'businessTaskResults', 'businessTasks']); const executions = saved.businessTaskExecutions || {}; const results = saved.businessTaskResults || {}; const tasks = saved.businessTasks || {}; const existing = executions[rootId] || null; const decision = executionGuard.acceptanceDecision(existing, executionId); if (!decision.accepted) { return { ok: true, accepted: false, task_id: rootId, execution_id: executionId, status: decision.status, message: decision.reason, result: results[rootId] || null }; } const acceptedAt = nowIso(); executions[rootId] = { task_id: rootId, execution_id: executionId, state: 'running', accepted_at: acceptedAt, updated_at: acceptedAt }; const acceptedTask = { ...(taskPayload || {}), task_id: rootId, execution_id: executionId, received_at: taskPayload?.received_at || acceptedAt }; if (!acceptedTask.operation) { throw new Error('唯一执行领取成功前缺少 task.operation,已停止执行。'); } tasks[rootId] = acceptedTask; results[rootId] = { ...(results[rootId] || {}), task_id: rootId, execution_id: executionId, stage: 'auto_executor', status: 'accepted', updated_at: acceptedAt, message: '后台自动执行器已取得唯一执行权。' }; await chrome.storage.local.set({ businessTaskExecutions: executions, businessTaskResults: results, businessTasks: tasks, currentBusinessTask: acceptedTask, currentBusinessTaskId: rootId, lastOperation: acceptedTask.operation, lastRawText: JSON.stringify(acceptedTask.operation, null, 2) }); return { ok: true, accepted: true, task_id: rootId, execution_id: executionId, status: 'accepted', message: '后台自动执行器已取得唯一执行权。' }; }); } async function setExecutionState(taskId, executionId, state, details = {}) { const rootId = executionGuard.rootTaskId(taskId); return withStorageMutation(async () => { const saved = await chrome.storage.local.get('businessTaskExecutions'); const executions = saved.businessTaskExecutions || {}; const existing = executions[rootId] || null; if (!executionGuard.canTransition(existing, executionId)) { throw new Error(`任务 ${rootId} 的持久化执行闸门与当前执行编号不一致。`); } if (executionGuard.TERMINAL_STATES.has(existing.state) && existing.state !== state) { return existing; } const updated = { ...existing, ...details, state, updated_at: nowIso() }; executions[rootId] = updated; await chrome.storage.local.set({ businessTaskExecutions: executions }); return updated; }); } async function transitionCurrentExecution(taskId, state, details = {}) { const execution = await getExecutionRecord(taskId); if (!execution?.execution_id) { throw new Error(`任务 ${executionGuard.rootTaskId(taskId)} 没有持久化执行编号。`); } return setExecutionState(taskId, execution.execution_id, state, details); } async function setResult(taskId, result, { allowCancelled = false } = {}) { if (!allowCancelled && isTaskCancelled(taskId)) return; const rootId = executionGuard.rootTaskId(taskId); return withStorageMutation(async () => { const saved = await chrome.storage.local.get(['businessTaskResults', 'businessTaskExecutions']); const resultMap = saved.businessTaskResults || {}; const executions = saved.businessTaskExecutions || {}; const existingResult = resultMap[taskId] || {}; const execution = executions[rootId] || null; const executionId = result.execution_id || existingResult.execution_id || execution?.execution_id || ''; let nextResult = { ...existingResult, ...result, task_id: taskId, execution_id: executionId, updated_at: nowIso() }; const currentStatus = String(existingResult.status || ''); const nextStatus = String(nextResult.status || ''); const terminalResult = /^(completed|dry_run|blocked|cancelled|saved_unverified|execution_uncertain|reconciliation_pending)$/; if (terminalResult.test(currentStatus) && currentStatus !== nextStatus) return existingResult; if (taskId === rootId && execution && execution.execution_id === executionId) { if (nextStatus === 'completed' || nextStatus === 'dry_run') { executions[rootId] = { ...execution, state: 'completed', updated_at: nowIso() }; } else if (/uncertain|saved_unverified|reconciliation/.test(nextStatus)) { executions[rootId] = { ...execution, state: 'uncertain', updated_at: nowIso() }; } else if (/blocked|failed|error|cancelled|incomplete/.test(nextStatus)) { const failureState = executionGuard.failureState(execution); executions[rootId] = { ...execution, state: failureState, updated_at: nowIso() }; if (failureState === 'uncertain') { nextResult = { ...nextResult, status: 'execution_uncertain', execution_phase: execution.state, write_attempted: true, no_erp_write: false, message: 'ERP 写入已发起但未取得确定回执,已停止执行且不会自动重试。' }; } } } resultMap[taskId] = nextResult; await chrome.storage.local.set({ businessTaskResults: resultMap, businessTaskExecutions: executions }); return nextResult; }); } async function publish(taskId, operation, stage, report, extra = {}) { const businessSummary = summarizeReportForBusiness(operation, stage, report || {}); await setResult(taskId, { ...businessSummary, ...extra, report }); } async function failTask(taskId, operation, stage, message, blockers = []) { await publish(taskId, operation, stage, { status: 'auto_execution_blocked', blockers: blockers.length ? blockers : [message] }, { status: 'blocked', message, blockers: blockers.length ? blockers : [message], no_erp_write: true, write_attempted: false }); } async function erpAutomationEnabled() { const saved = await chrome.storage.local.get('erpAutomationEnabled'); return saved.erpAutomationEnabled !== false; } async function waitForTabLoad(tabId, timeoutMs = 20000, taskId = '') { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { throwIfTaskCancelled(taskId); const tab = await chrome.tabs.get(tabId); if (tab.status === 'complete') return tab; await delay(500, taskId); } throwIfTaskCancelled(taskId); return chrome.tabs.get(tabId); } async function findOrOpenErpTab(taskId = '') { throwIfTaskCancelled(taskId); const tabs = await chrome.tabs.query({ url: 'https://ltjt.yunzhi.run/*' }); const existing = tabs.find((tab) => /\/System\/Mainlt\.asp/i.test(tab.url || '')) || tabs[0]; if (existing?.id) return waitForTabLoad(existing.id, 20000, taskId); throwIfTaskCancelled(taskId); const tab = await chrome.tabs.create({ url: 'https://ltjt.yunzhi.run/System/Mainlt.asp', active: false }); return waitForTabLoad(tab.id, 20000, taskId); } async function injectBridgeIntoBusinessTabs() { const tabs = await chrome.tabs.query({ url: [ 'http://localhost/*', 'http://127.0.0.1/*' ] }); for (const tab of tabs) { if (!tab.id) continue; if (!isBusinessSystemUrl(tab.url)) continue; try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['business-bridge.js'] }); } catch (error) { console.warn('LTJT bridge injection skipped', tab.id, error); } } } async function runPageAction(tabId, action, args = [], target = {}, taskId = '') { throwIfTaskCancelled(taskId); const scriptTarget = target.frameIds ? { tabId, frameIds: target.frameIds } : { tabId, allFrames: Boolean(target.allFrames) }; await chrome.scripting.executeScript({ target: scriptTarget, world: 'MAIN', files: ['inpage.js'] }); const results = await chrome.scripting.executeScript({ target: scriptTarget, world: 'MAIN', func: async (actionName, actionArgs) => { const assistant = window.LTJTOrderAssistant; if (!assistant || typeof assistant[actionName] !== 'function') { return { status: 'assistant_not_injected' }; } return assistant[actionName](...(actionArgs || [])); }, args: [action, args] }); throwIfTaskCancelled(taskId); return results; } function pickResult(results, predicate) { for (const item of results || []) { if (predicate(item.result)) return item; } return null; } async function openOrderForm(tabId, operation, taskId = '') { const fabudanwei = operation?.data?.system_defaults?.fabudanwei || '老挝联泰'; const results = await runPageAction(tabId, 'openOrderForm', [{ fabudanwei }], { allFrames: false }, taskId); const result = results[0]?.result; if (!result || result.status !== 'order_form_open_requested') { throw new Error(result?.blockers?.join('; ') || result?.status || '打开下单页失败。'); } return result; } async function waitForOrderFrame(tabId, entryStamp, taskId = '') { const deadline = Date.now() + 30000; let lastResults = []; while (Date.now() < deadline) { throwIfTaskCancelled(taskId); const results = await runPageAction(tabId, 'pingOrderFrame', [], { allFrames: true }, taskId); lastResults = results; const exact = pickResult(results, (result) => result?.status === 'order_frame_ready' && (!entryStamp || result.entry_stamp === entryStamp)); const visible = entryStamp ? null : pickResult(results, (result) => result?.status === 'order_frame_ready' && result.visible); const anyReady = entryStamp ? null : pickResult(results, (result) => result?.status === 'order_frame_ready'); const selected = exact || visible || anyReady; if (selected) return selected; await delay(600, taskId); } throwIfTaskCancelled(taskId); throw new Error(`未等到本次新打开的完整下单表单。entry_stamp=${entryStamp || ''};最近状态:${JSON.stringify(lastResults.map((item) => item.result).slice(0, 8))}`); } async function getTask(taskId) { const saved = await chrome.storage.local.get(['businessTasks', 'currentBusinessTask', 'currentBusinessTaskId']); const indexedTask = taskId ? saved.businessTasks?.[taskId] : null; if (indexedTask?.operation) return indexedTask; const task = saved.currentBusinessTask || null; if (!task?.operation) throw new Error('没有找到待执行业务任务。'); if (taskId && task.task_id !== taskId) throw new Error(`当前任务 ${task.task_id} 与请求任务 ${taskId} 不一致。`); return task; } async function publishOperationPlan(taskId, operation, plan) { const publicPlan = operationPlans.publicPlan(plan); const label = publicPlan.label || publicPlan.action || 'ERP 业务'; await publish(taskId, operation, 'capability_check', { status: 'operation_plan_ready', operation_plan: publicPlan, blockers: publicPlan.blockers, warnings: publicPlan.warnings, no_erp_write: true, }, { status: 'dry_run', reason: 'operation_not_connected', message: `已识别${label}任务,当前完成规则校验和执行规划,暂未写入 ERP。`, operation_plan: publicPlan, no_erp_write: true, next_step: publicPlan.fallback ? `后续接入 ${publicPlan.fallback} 浏览器执行路径。` : '后续接入该业务的浏览器页面操作、提交和回查路径。', }); return { ok: true, status: 'dry_run', task_id: taskId, operation_plan: publicPlan }; } function selectReadOnlyProbeResult(results = []) { const preferred = [ 'login_required', 'erp_order_candidate_unique', 'erp_order_candidate_ambiguous', 'erp_order_candidate_not_found', 'split_parent_page_detected', 'split_child_page_detected', 'erp_page_mismatch', ]; for (const status of preferred) { const match = (results || []).find((item) => item?.result?.status === status); if (match) return match.result; } return (results || []).map((item) => item?.result).find(Boolean) || null; } async function executeReadOnlyOperationPlan(taskId, operation, operationPlan) { throwIfTaskCancelled(taskId); const publicPlan = operationPlans.publicPlan(operationPlan); let browserProbe = null; let probeError = ''; try { const tab = await findOrOpenErpTab(taskId); const results = await runPageAction(tab.id, 'inspectOperationContext', [operation], { allFrames: true }, taskId); browserProbe = selectReadOnlyProbeResult(results); } catch (error) { if (error?.code === 'task_cancelled') throw error; probeError = error.message || String(error); } const candidate = browserProbe?.candidates?.length === 1 ? browserProbe.candidates[0] : {}; const confirmationPlan = operation.action === 'confirmation_export' ? operationPlans.buildConfirmationExportPlan(operation, candidate) : undefined; const warnings = [...(operationPlan.warnings || [])]; if (probeError) warnings.push(`浏览器只读预检未完成:${probeError}`); if (browserProbe?.status === 'login_required') warnings.push('ERP 当前需要人工登录;插件没有尝试绕过登录或验证码。'); if (browserProbe?.status === 'erp_page_mismatch') warnings.push('当前 ERP 标签页不是目标业务列表页;本次仅保留结构化计划。'); const report = { status: 'operation_dry_run_completed', reason: 'read_only_browser_probe', operation_plan: publicPlan, browser_probe: browserProbe || undefined, confirmation_plan: confirmationPlan, blockers: operationPlan.blockers || [], warnings, no_erp_write: true, write_attempted: false, }; await publish(taskId, operation, 'browser_dry_run', report, { status: 'dry_run', reason: probeError ? 'read_only_browser_probe_unavailable' : 'read_only_browser_probe', message: `已完成${publicPlan.label || 'ERP 业务'}的业务校验和只读浏览器预检,未写入 ERP。`, operation_plan: publicPlan, browser_probe: browserProbe || undefined, confirmation_plan: confirmationPlan, no_erp_write: true, warnings, }); return { ok: true, status: 'dry_run', task_id: taskId, operation_plan: publicPlan, browser_probe: browserProbe, confirmation_plan: confirmationPlan, }; } function browserExecutionAction(execution) { return { browser_live_split_parent: 'createSplitParentLive', browser_live_split_child: 'createSplitChildLive', browser_export_source: 'exportConfirmationSources' }[execution] || ''; } function browserExecutionStatus(report = {}) { if (/completed$/i.test(String(report.status || '')) || report.status === 'export_source_completed') return 'completed'; if (/uncertain/i.test(String(report.status || ''))) return 'execution_uncertain'; return 'blocked'; } async function executeConnectedBrowserOperation(taskId, operation, operationPlan) { throwIfTaskCancelled(taskId); const publicPlan = operationPlans.publicPlan(operationPlan); const action = browserExecutionAction(operationPlan.execution); if (!action) return publishOperationPlan(taskId, operation, operationPlan); await setResult(taskId, { stage: 'browser_execution', status: 'running', message: `已通过规则校验,正在执行${publicPlan.label || 'ERP 浏览器操作'}。`, operation_plan: publicPlan, no_erp_write: publicPlan.no_erp_write, summary: summaryFromOperation(operation) }); let report; try { const tab = await findOrOpenErpTab(taskId); if (publicPlan.no_erp_write === false) { await transitionCurrentExecution(taskId, 'write_started', { write_started_at: nowIso(), current_task_id: taskId, browser_action: action }); } const results = await runPageAction(tab.id, action, [operation], { allFrames: false }, taskId); report = results.map((item) => item?.result).find((result) => result && result.status !== 'assistant_not_injected') || results[0]?.result || { status: 'browser_action_missing', blockers: ['浏览器执行器没有返回结果。'] }; } catch (error) { if (error?.code === 'task_cancelled') throw error; report = { status: 'browser_execution_error', blockers: [error.message || String(error)], no_erp_write: publicPlan.no_erp_write }; } let finalStatus = browserExecutionStatus(report); if (publicPlan.no_erp_write === false && finalStatus !== 'completed') { finalStatus = 'execution_uncertain'; } if (publicPlan.no_erp_write === false && finalStatus === 'completed') { await transitionCurrentExecution(taskId, 'submitted', { submitted_at: nowIso(), current_task_id: taskId, erp_receipt: report.erp_receipt || undefined }); } const warnings = [...(operationPlan.warnings || []), ...(Array.isArray(report.warnings) ? report.warnings : [])]; const message = finalStatus === 'completed' ? (report.no_erp_write ? `${publicPlan.label || 'ERP 导出'}源文件回执已确认;未重保存订单,浏览器未将文件落盘状态误报为已交付。` : `${publicPlan.label || 'ERP 业务'}已提交并完成回查。`) : (finalStatus === 'execution_uncertain' ? `${publicPlan.label || 'ERP 业务'}已发起但回执不确定,已停止,不会自动重试。` : `${publicPlan.label || 'ERP 业务'}未完成,已停止后续写入。`); await publish(taskId, operation, 'browser_execution', { ...report, operation_plan: publicPlan, warnings, no_erp_write: publicPlan.no_erp_write === true, write_attempted: publicPlan.no_erp_write === false }, { status: finalStatus, message, operation_plan: publicPlan, no_erp_write: publicPlan.no_erp_write === true, erp_receipt: report.erp_receipt, execution_phase: publicPlan.no_erp_write === false ? (finalStatus === 'completed' ? 'submitted' : 'write_started') : 'read_only', write_attempted: publicPlan.no_erp_write === false, warnings, blockers: report.blockers || [] }); return { ok: finalStatus === 'completed', status: finalStatus, task_id: taskId, operation_plan: publicPlan, report }; } async function executeTeamSingleTask(taskId, operation, options = {}) { throwIfTaskCancelled(taskId); const batchLabel = options.batchIndex ? `批量 fallback 第 ${options.batchIndex}/${options.batchTotal} 个日期:` : ''; await setResult(taskId, { stage: 'queued', status: 'running', message: `${batchLabel}自动执行器已接单,正在寻找已登录 ERP 页面。`, summary: summaryFromOperation(operation) }); const tab = await findOrOpenErpTab(taskId); await setResult(taskId, { stage: 'open_order_form', status: 'running', message: `${batchLabel}已找到 ERP 页面,正在打开团队下单表单。` }); const opened = await openOrderForm(tab.id, operation, taskId); await setResult(taskId, { stage: 'wait_order_form', status: 'running', message: `${batchLabel}正在等待 ERP 下单表单加载完成。`, open: opened }); const frame = await waitForOrderFrame(tab.id, opened.entry_stamp, taskId); await chrome.storage.local.set({ lastFrameId: frame.frameId }); await setResult(taskId, { stage: 'preflight', status: 'running', message: `${batchLabel}正在校验选择项、触发产品联动并拦截提交载荷。` }); const preflightResults = await runPageAction(tab.id, 'preflightRawInstruction', [operation, { interceptSubmit: true }], { frameIds: [frame.frameId] }, taskId); const preflight = preflightResults[0]?.result || { status: 'missing_result', blockers: ['预检没有返回结果。'] }; await chrome.storage.local.set({ lastPreflight: preflight }); await publish(taskId, operation, 'preflight', preflight, { status: preflight.status === 'raw_instruction_test_passed' ? 'running' : 'blocked', message: preflight.status === 'raw_instruction_test_passed' ? `${batchLabel}预检通过,准备自动真实保存测试订单。` : `${batchLabel}预检阻断,未保存。` }); if (preflight.status !== 'raw_instruction_test_passed') { return { ok: false, status: 'preflight_blocked', task_id: taskId, blockers: preflight.blockers || [] }; } const intercepted = preflight.submit_intercept?.intercepted_submits?.[0]; if (preflight.submit_intercept?.status !== 'submit_intercept_captured' || !intercepted?.payload_sha256) { await failTask(taskId, operation, 'preflight', `${batchLabel}预检未拿到可批准的提交哈希,未保存。`, ['submit_intercept_missing_or_invalid']); return { ok: false, status: 'submit_intercept_missing', task_id: taskId }; } await setResult(taskId, { stage: 'live_submit', status: 'running', message: `${batchLabel}提交哈希已校验,正在自动真实保存测试订单。` }); throwIfTaskCancelled(taskId); await transitionCurrentExecution(taskId, 'write_started', { write_started_at: nowIso(), current_task_id: taskId, payload_sha256: intercepted.payload_sha256 }); const liveSubmitResults = await runPageAction(tab.id, 'liveSubmitApproved', [{ expectedPayloadSha256: intercepted.payload_sha256, expectedFieldCount: intercepted.payload_summary?.field_count || preflight.final_form?.serialized_field_count || 0 }], { frameIds: [frame.frameId] }, taskId); const liveSubmit = liveSubmitResults[0]?.result || { status: 'missing_result', blockers: ['真实保存没有返回结果。'] }; const erpReceipt = erpReceiptFromReport(liveSubmit); await chrome.storage.local.set({ lastLiveSubmit: liveSubmit }); if (liveSubmit.status === 'live_submit_completed') { await transitionCurrentExecution(taskId, 'submitted', { submitted_at: nowIso(), current_task_id: taskId, erp_receipt: erpReceipt || undefined }); } await publish(taskId, operation, 'live_submit', liveSubmit, { status: liveSubmit.status === 'live_submit_completed' ? 'running' : 'execution_uncertain', message: erpReceipt?.group_number ? `${batchLabel}ERP 已返回保存回执,团号:${erpReceipt.group_number}。正在回查测试标记。` : (liveSubmit.status === 'live_submit_completed' ? `${batchLabel}ERP 已返回保存完成,正在回查测试标记。` : `${batchLabel}ERP 保存失败或状态不确定。`), erp_receipt: erpReceipt || undefined, execution_phase: liveSubmit.status === 'live_submit_completed' ? 'submitted' : 'write_started', write_attempted: true }); if (liveSubmit.status !== 'live_submit_completed') { return { ok: false, status: 'live_submit_blocked', task_id: taskId, blockers: liveSubmit.blockers || [] }; } await setResult(taskId, { stage: 'verification', status: 'running', message: `${batchLabel}正在按测试标记回查订单。` }); const marker = operation.data?.test_marker; const date = operation.data?.departure_dates?.[0]; const verifyResults = await runPageAction(tab.id, 'verifyOrderMarker', [{ marker, dateFrom: date, dateTo: date }], { allFrames: false }, taskId); const verification = verifyResults[0]?.result || { status: 'missing_result', blockers: ['回查没有返回结果。'] }; await chrome.storage.local.set({ lastVerification: verification }); let returnToList = null; if (verification.status === 'order_marker_found') { const returnResults = await runPageAction(tab.id, 'returnToOrderList', [{ marker, dateFrom: date, dateTo: date }], { frameIds: [frame.frameId] }, taskId); returnToList = returnResults[0]?.result || null; await chrome.storage.local.set({ lastReturnToOrderList: returnToList }); } await publish(taskId, operation, 'verification', verification, { status: verification.status === 'order_marker_found' ? 'completed' : 'saved_unverified', message: erpReceipt?.group_number ? `${batchLabel}测试订单已保存,团号:${erpReceipt.group_number}。${verification.status === 'order_marker_found' ? '回查已命中。' : '回查暂未命中。'}` : (verification.status === 'order_marker_found' ? `${batchLabel}测试订单已保存并回查命中。` : `${batchLabel}测试订单已提交,但回查暂未命中。`), erp_receipt: erpReceipt || undefined, return_to_order_list: returnToList }); return { ok: verification.status === 'order_marker_found', status: verification.status === 'order_marker_found' ? 'completed' : 'saved_unverified', task_id: taskId, erp_receipt: erpReceipt || undefined, }; } async function runAutoTask(taskId, executionId) { const rootId = executionGuard.rootTaskId(taskId); if (isTaskCancelled(taskId)) { return { ok: false, status: 'cancelled', task_id: taskId }; } let operation = null; try { const durableExecution = await getExecutionRecord(rootId); if (!executionGuard.canTransition(durableExecution, executionId) || durableExecution.state !== 'running') { return { ok: false, status: 'duplicate_blocked', task_id: rootId }; } throwIfTaskCancelled(taskId); const task = await getTask(taskId); if (String(task.execution_id || '') !== String(executionId || '')) { throw new Error('任务载荷与持久化执行编号不一致。'); } throwIfTaskCancelled(taskId); operation = operationPlans.normalizeOperation(task.operation); const normalizedTaskId = task.task_id || operation.source?.task_id || taskId; if (!(await erpAutomationEnabled())) { throwIfTaskCancelled(normalizedTaskId); await publish(normalizedTaskId, operation, 'automation_disabled', { status: 'erp_automation_disabled', blockers: ['插件已关闭 ERP 操作。'], no_erp_write: true, write_attempted: false }, { status: 'blocked', message: '插件已关闭 ERP 操作,未打开或填写 ERP。', blockers: ['插件开关处于关闭状态。'], no_erp_write: true, write_attempted: false }); return { ok: false, status: 'automation_disabled', task_id: normalizedTaskId }; } const operationPlan = operationPlans.validateOperation(operation); throwIfTaskCancelled(normalizedTaskId); const blockers = operationPlan.blockers; if (blockers.length) { await failTask(normalizedTaskId, operation, 'validation', '任务未通过自动执行安全校验。', blockers); return { ok: false, status: 'validation_blocked', blockers }; } if (operationPlan.execution === 'browser_team_single_fallback') { const expanded = operationPlans.expandTeamBatch(operation); const batchResults = []; throwIfTaskCancelled(normalizedTaskId); await setResult(normalizedTaskId, { stage: 'batch_fallback', status: 'running', message: `原生批量接口未启用,将按日期串行复用 team_single:共 ${expanded.length} 个日期。`, operation_plan: operationPlans.publicPlan(operationPlan), no_erp_write: false, }); for (let index = 0; index < expanded.length; index += 1) { throwIfTaskCancelled(normalizedTaskId); const childTaskId = `${normalizedTaskId}#${index + 1}`; const childOperation = expanded[index]; try { const childResult = await executeTeamSingleTask(childTaskId, childOperation, { batchIndex: index + 1, batchTotal: expanded.length, }); batchResults.push({ task_id: childTaskId, date: childOperation.data?.departure_dates?.[0] || '', status: childResult.status, erp_receipt: childResult.erp_receipt, }); if (childResult.status !== 'completed') { await publish(normalizedTaskId, operation, 'batch_fallback', { status: 'batch_fallback_incomplete', operation_plan: operationPlans.publicPlan(operationPlan), blockers: childResult.blockers || ['批量 fallback 中某个日期未完成回查。'], no_erp_write: false, }, { status: childResult.status === 'saved_unverified' ? 'saved_unverified' : 'blocked', message: `批量 fallback 已停止在第 ${index + 1} 个日期,未继续后续日期。`, batch_results: batchResults, no_erp_write: false, }); return { ok: false, status: 'batch_fallback_incomplete', task_id: normalizedTaskId, batch_results: batchResults }; } } catch (error) { if (error?.code === 'task_cancelled') { await setResult(normalizedTaskId, { stage: 'cancelled', status: 'cancelled', message: '批量任务已取消,已停止后续日期。' }); return { ok: false, status: 'cancelled', task_id: normalizedTaskId, batch_results: batchResults }; } await failTask(childTaskId, childOperation, 'batch_fallback_error', error.message || String(error)); batchResults.push({ task_id: childTaskId, date: childOperation.data?.departure_dates?.[0] || '', status: 'blocked', error: error.message || String(error), }); await publish(normalizedTaskId, operation, 'batch_fallback', { status: 'batch_fallback_incomplete', operation_plan: operationPlans.publicPlan(operationPlan), blockers: [error.message || String(error)], no_erp_write: false, }, { status: 'blocked', message: `批量 fallback 在第 ${index + 1} 个日期发生异常,已停止后续日期。`, batch_results: batchResults, no_erp_write: false, }); return { ok: false, status: 'batch_fallback_incomplete', task_id: normalizedTaskId, batch_results: batchResults }; } } await publish(normalizedTaskId, operation, 'batch_fallback', { status: 'batch_fallback_completed', operation_plan: operationPlans.publicPlan(operationPlan), warnings: operationPlan.warnings, no_erp_write: false, }, { status: 'completed', message: `批量 fallback 已按 ${batchResults.length} 个日期逐个完成并回查。`, batch_results: batchResults, no_erp_write: false, }); return { ok: true, status: 'completed', task_id: normalizedTaskId, batch_results: batchResults }; } if (['browser_live_split_parent', 'browser_live_split_child', 'browser_export_source'].includes(operationPlan.execution)) { return executeConnectedBrowserOperation(normalizedTaskId, operation, operationPlan); } if (operationPlan.execution === 'browser_dry_run') { return executeReadOnlyOperationPlan(normalizedTaskId, operation, operationPlan); } if (operationPlan.execution !== 'browser_team_single') { return publishOperationPlan(normalizedTaskId, operation, operationPlan); } return executeTeamSingleTask(normalizedTaskId, operation); } catch (error) { if (error?.code === 'task_cancelled') { await setResult(taskId, { stage: 'cancelled', status: 'cancelled', message: '任务已取消,已停止后续自动执行。' }); return { ok: false, status: 'cancelled', task_id: taskId }; } await failTask(taskId, operation, 'extension_error', error.message || String(error)); return { ok: false, status: 'extension_error', task_id: taskId, message: error.message || String(error) }; } finally { runningTasks.delete(rootId); } } async function cancelAutoTask(taskId) { const normalizedTaskId = String(taskId || '').trim(); if (!normalizedTaskId) { return { ok: false, status: 'cancel_failed', message: '缺少 task_id。' }; } const execution = await getExecutionRecord(normalizedTaskId); if (execution && executionGuard.WRITE_STATES.has(execution.state)) { await setResult(normalizedTaskId, { stage: 'reconciliation', status: 'execution_uncertain', execution_phase: execution.state, write_attempted: true, no_erp_write: false, message: 'ERP 写入已开始,取消不能作为回滚;任务已保留并等待人工回查。' }, { allowCancelled: true }); return { ok: false, status: 'cancel_blocked_after_write', task_id: normalizedTaskId, cancelled: false, message: 'ERP 写入已开始,不能通过删除或取消回滚。' }; } cancelledTasks.add(normalizedTaskId); const running = Array.from(runningTasks).some((runningTaskId) => ( runningTaskId === normalizedTaskId || taskRootId(runningTaskId) === normalizedTaskId )); await setResult(normalizedTaskId, { stage: 'cancelled', status: 'cancelled', message: running ? '已收到取消请求,正在停止后续自动执行。' : '任务已取消,未继续自动执行。' }, { allowCancelled: true }); if (execution?.execution_id) { await setExecutionState(normalizedTaskId, execution.execution_id, 'cancelled', { cancelled_at: nowIso() }); } return { ok: true, status: 'cancelled', task_id: normalizedTaskId, cancelled: true, running }; } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message?.type === 'LTJT_CANCEL_TASK') { cancelAutoTask(message.task_id || '') .then((result) => sendResponse(result)) .catch((error) => sendResponse({ ok: false, status: 'cancel_failed', message: error.message || String(error) })); return true; } if (message?.type !== 'LTJT_AUTO_EXECUTE_TASK') return false; const taskId = message.task_id || ''; const executionId = message.execution_id || ''; acceptExecution(taskId, executionId, message.task) .then((acceptance) => { if (acceptance.accepted) { runningTasks.add(executionGuard.rootTaskId(taskId)); runAutoTask(taskId, executionId).catch((error) => { console.error('LTJT auto task failed', error); }); } sendResponse(acceptance); }) .catch((error) => sendResponse({ ok: false, accepted: false, task_id: taskId, execution_id: executionId, status: 'execution_guard_error', message: error.message || String(error) })); return true; }); chrome.runtime.onInstalled.addListener(() => { injectBridgeIntoBusinessTabs().catch((error) => { console.warn('LTJT bridge injection failed after install', error); }); }); chrome.runtime.onStartup.addListener(() => { injectBridgeIntoBusinessTabs().catch((error) => { console.warn('LTJT bridge injection failed after startup', error); }); });