(() => { 'use strict'; try { if (window.__LTJT_ORDER_ASSISTANT_BRIDGE_HANDLER__) { window.removeEventListener('message', window.__LTJT_ORDER_ASSISTANT_BRIDGE_HANDLER__); } } catch (error) { // Old handlers can belong to an invalidated extension context after reload. } try { if (window.__LTJT_ORDER_ASSISTANT_STORAGE_HANDLER__) { chrome.storage.onChanged.removeListener(window.__LTJT_ORDER_ASSISTANT_STORAGE_HANDLER__); } } catch (error) { // Re-injection must keep going even when Chrome has invalidated the old context. } window.__LTJT_ORDER_ASSISTANT_BRIDGE_INSTALLED_AT__ = new Date().toISOString(); const BUSINESS_SOURCE = 'LIANSYN_PLATFORM'; const EXTENSION_SOURCE = 'LTJT_ORDER_ASSISTANT_EXTENSION'; const EXECUTION_KEEPALIVE_PORT = 'LTJT_EXECUTION_KEEPALIVE'; const executionKeepalives = new Map(); function nowIso() { return new Date().toISOString(); } function postReply(requestId, type, payload = {}) { window.postMessage({ source: EXTENSION_SOURCE, requestId, type, payload }, window.location.origin); } async function getAutomationState() { const saved = await chrome.storage.local.get(['erpAutomationEnabled', 'erpKeepaliveEnabled']); const enabled = saved.erpAutomationEnabled !== false; return { erp_automation_enabled: enabled, erp_automation_status: enabled ? 'enabled' : 'disabled', erp_keepalive_enabled: enabled && saved.erpKeepaliveEnabled !== false }; } async function bridgePayload(extra = {}, expectedErpAccount = '') { let erpSession = { ok: false, tab_present: false, account_matched: false, url: '' }; try { erpSession = await chrome.runtime.sendMessage({ type: 'LTJT_ERP_SESSION_STATUS', expected_erp_account: String(expectedErpAccount || '').trim() }) || erpSession; } catch (error) { erpSession = { ...erpSession, message: error.message || String(error) }; } let extensionUpdate = { ok: false, safe: false, reason: 'status_unavailable' }; try { extensionUpdate = await chrome.runtime.sendMessage({ type: 'LTJT_EXTENSION_UPDATE_STATUS' }) || extensionUpdate; } catch (error) { extensionUpdate = { ...extensionUpdate, message: error.message || String(error) }; } return { ok: true, extension: 'ltjt-order-assistant', version: chrome.runtime.getManifest().version, bridge_installed_at: window.__LTJT_ORDER_ASSISTANT_BRIDGE_INSTALLED_AT__ || '', erp_session: erpSession, extension_update: extensionUpdate, ...(await getAutomationState()), ...extra }; } async function postBridgeReady() { window.postMessage({ source: EXTENSION_SOURCE, type: 'BRIDGE_READY', payload: await bridgePayload() }, window.location.origin); } async function getResultMap() { const saved = await chrome.storage.local.get('businessTaskResults'); return saved.businessTaskResults || {}; } function taskResultVersion(result) { if (!result || typeof result !== 'object') return ''; return [ result.execution_id || '', result.updated_at || '', result.stage || '', result.status || '' ].join('|'); } function postChangedTaskResults(change) { const previous = change?.oldValue && typeof change.oldValue === 'object' ? change.oldValue : {}; const current = change?.newValue && typeof change.newValue === 'object' ? change.newValue : {}; for (const [taskId, result] of Object.entries(current)) { if (!taskId || !result || typeof result !== 'object') continue; if (taskResultVersion(previous[taskId]) === taskResultVersion(result)) continue; window.postMessage({ source: EXTENSION_SOURCE, type: 'TASK_RESULT_CHANGED', payload: { ok: true, task_id: taskId, result } }, window.location.origin); } } async function removeStoredBusinessTask(taskId) { const saved = await chrome.storage.local.get([ 'businessTasks', 'businessTaskResults', 'businessTaskTimingStates', 'currentBusinessTask', 'currentBusinessTaskId' ]); const belongsToTask = (candidateId) => candidateId === taskId || candidateId.startsWith(`${taskId}#`); const businessTasks = saved.businessTasks || {}; const businessTaskResults = saved.businessTaskResults || {}; const businessTaskTimingStates = saved.businessTaskTimingStates || {}; Object.keys(businessTasks).filter(belongsToTask).forEach((candidateId) => delete businessTasks[candidateId]); Object.keys(businessTaskResults).filter(belongsToTask).forEach((candidateId) => delete businessTaskResults[candidateId]); Object.keys(businessTaskTimingStates).filter(belongsToTask).forEach((candidateId) => delete businessTaskTimingStates[candidateId]); await chrome.storage.local.set({ businessTasks, businessTaskResults, businessTaskTimingStates }); if (saved.currentBusinessTaskId === taskId || saved.currentBusinessTask?.task_id === taskId) { await chrome.storage.local.remove([ 'currentBusinessTask', 'currentBusinessTaskId', 'lastOperation', 'lastRawText' ]); } } async function cancelAndRemoveTask(taskId) { let cancellation = null; try { cancellation = await chrome.runtime.sendMessage({ type: 'LTJT_HARD_DELETE_TASK', task_id: taskId }); } catch (error) { cancellation = { ok: false, status: 'cancel_unreachable', message: error.message || String(error) }; } // The platform's hard-delete request is authoritative. Even when an ERP // write has crossed the cancellation boundary, remove the extension's // local task/cache records so the card can be deleted from the platform. await removeStoredBusinessTask(taskId); return { ok: true, task_id: taskId, status: 'deleted', cancelled: cancellation?.cancelled === true, cancellation_status: cancellation?.status || 'unknown' }; } async function reconcileTask(taskId) { if (!taskId) throw new Error('Missing task_id'); return chrome.runtime.sendMessage({ type: 'LTJT_RECONCILE_TASK', task_id: taskId }); } function resumePrewriteTask(taskId) { if (!taskId) throw new Error('Missing task_id'); // Keep the runtime message Promise alive so the service worker owns the // resumed execution, while acknowledging the UI immediately. Completion is // observed through the existing task-result polling channel. chrome.runtime.sendMessage({ type: 'LTJT_RESUME_PREWRITE_TASK', task_id: taskId }).catch(() => {}); return { ok: true, task_id: taskId, status: 'resume_requested', message: '已请求恢复保存前任务,后续结果通过原任务回执回传。' }; } function dispatchAutoExecutionWithKeepalive(message) { const taskId = String(message?.task_id || ''); if (!taskId) return Promise.reject(new Error('Missing task_id')); const existing = executionKeepalives.get(taskId); if (existing) return Promise.reject(new Error('The same task already has an active execution channel')); return new Promise((resolve, reject) => { const port = chrome.runtime.connect({ name: EXECUTION_KEEPALIVE_PORT }); let accepted = false; let settled = false; const cleanup = () => { const current = executionKeepalives.get(taskId); if (current?.heartbeat) window.clearInterval(current.heartbeat); if (current?.port === port) executionKeepalives.delete(taskId); try { port.disconnect(); } catch (error) { /* already disconnected */ } }; const heartbeat = window.setInterval(() => { try { port.postMessage({ type: 'KEEPALIVE', task_id: taskId }); } catch (error) { cleanup(); } }, 15_000); executionKeepalives.set(taskId, { port, heartbeat }); port.onMessage.addListener((payload) => { if (payload?.type === 'EXECUTION_ACCEPTED') { accepted = true; resolve(payload.acceptance || {}); return; } if (payload?.type === 'EXECUTION_SETTLED') { settled = true; cleanup(); return; } if (payload?.type === 'EXECUTION_ERROR') { const error = new Error(payload.message || 'Extension execution channel failed'); cleanup(); if (!accepted) reject(error); } }); port.onDisconnect.addListener(() => { cleanup(); if (!accepted && !settled) reject(new Error(chrome.runtime.lastError?.message || 'Extension execution channel disconnected before acceptance')); }); port.postMessage({ ...message, type: 'EXECUTE' }); }); } async function createTask(payload) { const task = payload?.task || {}; const operation = task.operation || payload?.operation; const taskId = String(task.task_id || '').trim(); const executionId = String(payload?.execution_id || '').trim(); if (!operation) throw new Error('Missing task.operation'); if (!taskId) throw new Error('Missing task.task_id'); if (!executionId) throw new Error('Missing execution_id'); const currentBusinessTask = { task_id: taskId, operation, raw_text: task.raw_text || '', created_at: task.created_at || nowIso(), received_at: nowIso(), execution_id: executionId, status: 'queued' }; const acceptance = await dispatchAutoExecutionWithKeepalive({ type: 'LTJT_AUTO_EXECUTE_TASK', task_id: taskId, execution_id: executionId, task: currentBusinessTask }); return { ...currentBusinessTask, acceptance }; } const bridgeHandler = async (event) => { if (event.source !== window) return; const message = event.data || {}; if (message.source !== BUSINESS_SOURCE) return; const requestId = message.requestId || ''; try { if (message.type === 'PING') { postReply(requestId, 'PONG', await bridgePayload({}, message.payload?.expected_erp_account)); return; } if (message.type === 'APPLY_EXTENSION_UPDATE') { const result = await chrome.runtime.sendMessage({ type: 'LTJT_APPLY_EXTENSION_UPDATE', target_version: String(message.payload?.target_version || '') }); postReply(requestId, 'EXTENSION_UPDATE_APPLYING', result || { ok: false, accepted: false, status: 'update_reload_failed', message: '插件后台未返回重载结果。' }); return; } if (message.type === 'CREATE_TASK') { const task = await createTask(message.payload || {}); postReply(requestId, 'TASK_CREATED', { ok: true, task_id: task.task_id, execution_id: task.execution_id, accepted: task.acceptance?.accepted === true, status: task.acceptance?.status || 'not_accepted', message: task.acceptance?.message || '' }); return; } if (message.type === 'GET_TASK_RESULT') { const taskId = message.payload?.task_id || ''; const resultMap = await getResultMap(); postReply(requestId, 'TASK_RESULT', { ok: true, task_id: taskId, result: resultMap[taskId] || null }); return; } if (message.type === 'RECONCILE_TASK') { const taskId = message.payload?.task_id || ''; const result = await reconcileTask(taskId); postReply(requestId, 'TASK_RECONCILED', result || { ok: false, status: 'reconciliation_failed', message: '插件未返回回查结果。' }); return; } if (message.type === 'RESUME_PREWRITE_TASK') { const taskId = message.payload?.task_id || ''; postReply(requestId, 'TASK_RESUME_REQUESTED', resumePrewriteTask(taskId)); return; } if (message.type === 'GET_CURRENT_TASK') { const saved = await chrome.storage.local.get('currentBusinessTask'); postReply(requestId, 'CURRENT_TASK', { ok: true, task: saved.currentBusinessTask || null }); return; } if (message.type === 'DELETE_TASK') { const taskId = message.payload?.task_id || ''; if (!taskId) throw new Error('Missing task_id'); const result = await cancelAndRemoveTask(taskId); postReply(requestId, 'TASK_DELETED', result); return; } if (message.type === 'CLEAR_TASKS') { await chrome.storage.local.remove([ 'currentBusinessTask', 'currentBusinessTaskId', 'businessTasks', 'businessTaskResults', 'businessTaskTimingStates' ]); postReply(requestId, 'TASKS_CLEARED', { ok: true }); return; } postReply(requestId, 'ERROR', { ok: false, message: `Unknown bridge message type: ${message.type || ''}` }); } catch (error) { postReply(requestId, 'ERROR', { ok: false, message: error.message || String(error) }); } }; window.__LTJT_ORDER_ASSISTANT_BRIDGE_HANDLER__ = bridgeHandler; window.addEventListener('message', bridgeHandler); window.__LTJT_ORDER_ASSISTANT_STORAGE_HANDLER__ = (changes, areaName) => { if (areaName !== 'local') return; if (changes.businessTaskResults) postChangedTaskResults(changes.businessTaskResults); if (changes.erpAutomationEnabled || changes.erpKeepaliveEnabled) postBridgeReady().catch(() => {}); }; chrome.storage.onChanged.addListener(window.__LTJT_ORDER_ASSISTANT_STORAGE_HANDLER__); postBridgeReady().catch(() => {}); })();