Files
LWLT-AI/chrome-extension/ltjt-order-assistant/background.js
2026-07-13 19:57:46 +08:00

1004 lines
40 KiB
JavaScript
Raw 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.

'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 || '<none>'};最近状态:${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);
});
});