Files
LWLT-AIBOT/chrome-extension/ltjt-order-assistant/execution-guard.js

167 lines
7.0 KiB
JavaScript

(function initExecutionGuard(root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
root.LTJTExecutionGuard = api;
})(typeof self !== 'undefined' ? self : globalThis, function createExecutionGuard() {
'use strict';
const TERMINAL_STATES = new Set(['completed', 'blocked', 'cancelled', 'uncertain']);
const WRITE_STATES = new Set(['write_started', 'submitted', 'uncertain', 'completed']);
const RESULT_TIMEOUT_MS = 10 * 60 * 1000;
const HISTORY_MAINTENANCE_TIMEOUT_MS = 30 * 60 * 1000;
const LOCAL_TERMINAL_ROOT_LIMIT = 12;
const ACTIVE_EXECUTION_STATES = new Set(['running', 'write_started', 'submitted', 'uncertain']);
const RECONCILABLE_RESULT_STATUSES = new Set(['saved_unverified', 'execution_uncertain', 'reconciliation_pending', 'uncertain']);
function rootTaskId(taskId) {
return String(taskId || '').split('#')[0];
}
function acceptanceDecision(existing, executionId) {
const normalizedExecutionId = String(executionId || '').trim();
if (!normalizedExecutionId) {
return { accepted: false, status: 'missing_execution_id', reason: '缺少服务端执行编号。' };
}
if (!existing) return { accepted: true, status: 'accepted', reason: '' };
if (String(existing.execution_id || '') !== normalizedExecutionId) {
return { accepted: false, status: 'duplicate_blocked', reason: '同一任务出现了不同执行编号。' };
}
return {
accepted: false,
status: TERMINAL_STATES.has(existing.state) ? 'already_finished' : 'already_started',
reason: '该执行编号已经被插件接收,重复消息不会再次执行。'
};
}
function failureState(existing) {
return existing && WRITE_STATES.has(existing.state) ? 'uncertain' : 'blocked';
}
function confirmedPrewriteNoErpWrite(report) {
if (!report || typeof report !== 'object' || Array.isArray(report)) return false;
const submitSafety = report.submit_safety;
const ajaxRecordsValid = report.ajax_records == null || Array.isArray(report.ajax_records);
const sideEffectsValid = report.side_effects == null || Array.isArray(report.side_effects);
const ajaxRecords = Array.isArray(report.ajax_records) ? report.ajax_records : [];
const sideEffects = Array.isArray(report.side_effects) ? report.side_effects : [];
const ajaxStayedBeforeNetwork = ajaxRecords.every((record) => Boolean(
record
&& typeof record === 'object'
&& (record.prevented_from_network === true || record.network_request_attempted === false)
));
return report.status === 'live_submit_blocked'
&& report.execution_phase === 'prewrite_blocked'
&& report.no_erp_write === true
&& report.write_attempted === false
&& submitSafety
&& typeof submitSafety === 'object'
&& !Array.isArray(submitSafety)
&& submitSafety.live_submit_attempted === false
&& !report.native_request
&& !report.server_response
&& !report.requery
&& ajaxRecordsValid
&& sideEffectsValid
&& sideEffects.length === 0
&& ajaxStayedBeforeNetwork;
}
function timeoutDisposition(existing) {
const writeStarted = Boolean(existing && WRITE_STATES.has(existing.state));
return {
state: writeStarted ? 'uncertain' : 'blocked',
result_status: writeStarted ? 'execution_uncertain' : 'failed',
write_attempted: writeStarted,
no_erp_write: !writeStarted,
reconciliation_required: writeStarted
};
}
function canTransition(existing, executionId) {
return Boolean(existing) && String(existing.execution_id || '') === String(executionId || '');
}
function recordTimestamp(value) {
for (const candidate of [value?.updated_at, value?.accepted_at, value?.received_at, value?.created_at]) {
const timestamp = Date.parse(String(candidate || ''));
if (Number.isFinite(timestamp)) return timestamp;
}
return 0;
}
function pruneLocalTaskHistory(state = {}, options = {}) {
const tasks = { ...(state.tasks || {}) };
const results = { ...(state.results || {}) };
const executions = { ...(state.executions || {}) };
const timingStates = { ...(state.timingStates || {}) };
const currentRoot = rootTaskId(options.currentTaskId || '');
const terminalRootLimit = Math.max(0, Math.trunc(Number(options.terminalRootLimit ?? LOCAL_TERMINAL_ROOT_LIMIT)));
const roots = new Set([
...Object.keys(tasks),
...Object.keys(results),
...Object.keys(executions),
...Object.keys(timingStates)
].map(rootTaskId).filter(Boolean));
const protectedRoots = new Set(currentRoot ? [currentRoot] : []);
const lastUpdatedByRoot = new Map();
const collect = (entries) => {
for (const [taskId, value] of Object.entries(entries)) {
const rootId = rootTaskId(taskId);
if (!rootId) continue;
lastUpdatedByRoot.set(rootId, Math.max(lastUpdatedByRoot.get(rootId) || 0, recordTimestamp(value)));
}
};
collect(tasks);
collect(results);
collect(executions);
for (const [taskId, execution] of Object.entries(executions)) {
if (ACTIVE_EXECUTION_STATES.has(String(execution?.state || ''))) protectedRoots.add(rootTaskId(taskId));
}
for (const [taskId, result] of Object.entries(results)) {
if (RECONCILABLE_RESULT_STATUSES.has(String(result?.status || ''))) protectedRoots.add(rootTaskId(taskId));
}
for (const taskId of Object.keys(timingStates)) protectedRoots.add(rootTaskId(taskId));
const terminalRoots = [...roots]
.filter((rootId) => !protectedRoots.has(rootId))
.sort((left, right) => (lastUpdatedByRoot.get(right) || 0) - (lastUpdatedByRoot.get(left) || 0));
const retainedRoots = new Set([...protectedRoots, ...terminalRoots.slice(0, terminalRootLimit)]);
const filter = (entries) => Object.fromEntries(
Object.entries(entries).filter(([taskId]) => retainedRoots.has(rootTaskId(taskId)))
);
const next = {
tasks: filter(tasks),
results: filter(results),
executions: filter(executions),
timingStates: filter(timingStates)
};
return {
...next,
counters: {
retained_root_count: retainedRoots.size,
protected_root_count: protectedRoots.size,
removed_root_count: Math.max(0, roots.size - retainedRoots.size),
removed_task_count: Math.max(0, Object.keys(tasks).length - Object.keys(next.tasks).length),
removed_result_count: Math.max(0, Object.keys(results).length - Object.keys(next.results).length),
removed_execution_count: Math.max(0, Object.keys(executions).length - Object.keys(next.executions).length),
removed_timing_count: Math.max(0, Object.keys(timingStates).length - Object.keys(next.timingStates).length)
}
};
}
return {
TERMINAL_STATES,
WRITE_STATES,
RESULT_TIMEOUT_MS,
HISTORY_MAINTENANCE_TIMEOUT_MS,
LOCAL_TERMINAL_ROOT_LIMIT,
rootTaskId,
acceptanceDecision,
failureState,
confirmedPrewriteNoErpWrite,
timeoutDisposition,
canTransition,
pruneLocalTaskHistory
};
});