Files
LWLT-AIBOT/chrome-extension/ltjt-order-assistant/business-bridge.js
T

360 lines
12 KiB
JavaScript

(() => {
'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 = {}) {
let erpSession = {
ok: false,
tab_present: false,
account_matched: false,
url: ''
};
try {
erpSession = await chrome.runtime.sendMessage({ type: 'LTJT_ERP_SESSION_STATUS' }) || erpSession;
} catch (error) {
erpSession = { ...erpSession, 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,
...(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());
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 || '<missing>'}`
});
} 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(() => {});
})();