385 lines
13 KiB
JavaScript
385 lines
13 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 = {}, 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 || '<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(() => {});
|
|
})();
|