excel唯一逻辑修复
This commit is contained in:
1 parent
ede1d47c47
commit
af85ea22ec
24 files changed
+784
-34
No files matched your search
@@ -11,9 +11,13 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
|
||||
|
||||
## 当前版本
|
||||
|
||||
当前源码版本为 `0.5.184`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
|
||||
当前源码版本为 `0.5.185`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
|
||||
|
||||
0.5.184 当前重点:
|
||||
0.5.185 当前重点:
|
||||
|
||||
- 目标只读查询遇到 Chrome 主框架移除时,在同一执行仍处于写入前运行状态下,最多等待 8 秒确认同一主文档连续两次加载完成,再重新注入适配器并只读重查一次。持续切换、权限错误、取消或写入边界变化时停止;业务无匹配和多匹配不重试,原生保存不重试。异常按实际查询、路由准备或预检阶段记录;查询失败保留步骤和当时自动恢复时间快照,不记录业务值或恢复正常心跳刷屏。本地合成回归覆盖,真实 ERP 验收待部署方执行。需更新插件及平台最低版本,无数据库迁移。
|
||||
|
||||
继续保留 0.5.184 的列表及回执修复:
|
||||
|
||||
- ERP 列表就绪等待跟随当前 iframe 文档,仍要求加载完成、原生搜索函数与可用按钮,切换期间不在旧文档中搜索;失败回执明确区分页面未就绪,并记录不含业务值的条件与耗时。平台接单等待为 10 秒;明确未写入的迟到检索失败可在同执行编号、原连接范围内同步回平台。需同步更新平台/服务端与插件,无数据库迁移;不重新下发任务,真实写入不确定结果继续人工核验。
|
||||
|
||||
|
||||
@@ -870,6 +870,8 @@ async function readErpKeepaliveState() {
|
||||
recovery_trigger: String(state.recovery_trigger || ''),
|
||||
recovery_reason: String(state.recovery_reason || ''),
|
||||
recovery_attempt_at: String(state.recovery_attempt_at || ''),
|
||||
recovery_reload_requested_at: String(state.recovery_reload_requested_at || ''),
|
||||
recovery_tab_id: Number(state.recovery_tab_id || 0),
|
||||
recovery_completed_at: String(state.recovery_completed_at || ''),
|
||||
recovery_error: String(state.recovery_error || ''),
|
||||
recovery_count: Number(state.recovery_count || 0),
|
||||
@@ -1147,6 +1149,8 @@ async function performErpLinkRecovery(reason, trigger) {
|
||||
recovery_completed_at: '',
|
||||
recovery_error: '',
|
||||
recovery_count: state.recovery_count + 1,
|
||||
recovery_reload_requested_at: '',
|
||||
recovery_tab_id: tab.id,
|
||||
last_skip_reason: '',
|
||||
last_tab_url: tab.url || ''
|
||||
});
|
||||
@@ -1164,7 +1168,11 @@ async function performErpLinkRecovery(reason, trigger) {
|
||||
return { ok: false, status: 'skipped', reason: 'active_erp_execution' };
|
||||
}
|
||||
if (typeof chrome.tabs?.reload !== 'function') throw new Error('erp_link_refresh_api_unavailable');
|
||||
await chrome.tabs.reload(tab.id, { bypassCache: true });
|
||||
const reloadRequestedAt = nowIso();
|
||||
await Promise.all([
|
||||
chrome.tabs.reload(tab.id, { bypassCache: true }),
|
||||
tryUpdateErpKeepaliveState({ recovery_reload_requested_at: reloadRequestedAt })
|
||||
]);
|
||||
const ready = await waitForTabLoad(tab.id, ERP_LINK_RECOVERY_TIMEOUT_MS);
|
||||
if (ready.status && ready.status !== 'complete') throw new Error('erp_link_refresh_timeout');
|
||||
if (!isApprovedErpPageUrl(ready.url)) throw erpPageAccessError(ready.url, null);
|
||||
@@ -1635,6 +1643,14 @@ async function canReusePageActionScripts(tabId, action, scriptTarget, taskId = '
|
||||
}
|
||||
}
|
||||
|
||||
function pageActionError(error, actionCode, step) {
|
||||
const annotated = new Error(error?.message || String(error), { cause: error });
|
||||
annotated.code = error?.code;
|
||||
annotated.page_action = actionCode;
|
||||
annotated.page_step = step;
|
||||
return annotated;
|
||||
}
|
||||
|
||||
async function runPageAction(tabId, action, args = [], target = {}, taskId = '') {
|
||||
throwIfTaskCancelled(taskId);
|
||||
const scriptTarget = target.frameIds
|
||||
@@ -1664,7 +1680,7 @@ async function runPageAction(tabId, action, args = [], target = {}, taskId = '')
|
||||
duration_ms: performance.now() - injectionStartedAt,
|
||||
counters: { call_count: 1, failure_count: 1 }
|
||||
}]);
|
||||
throw error;
|
||||
throw pageActionError(error, actionCode, 'script_injection');
|
||||
}
|
||||
|
||||
const roundtripStartedAt = performance.now();
|
||||
@@ -1711,7 +1727,7 @@ async function runPageAction(tabId, action, args = [], target = {}, taskId = '')
|
||||
duration_ms: performance.now() - roundtripStartedAt,
|
||||
counters: { call_count: 1, failure_count: 1 }
|
||||
}]);
|
||||
throw error;
|
||||
throw pageActionError(error, actionCode, `action_roundtrip.${actionCode}`);
|
||||
}
|
||||
|
||||
queueOperationTimingDetails(taskId, [{
|
||||
@@ -2298,8 +2314,124 @@ function withoutResolvedOperation(report = {}) {
|
||||
return publicReport;
|
||||
}
|
||||
|
||||
function isRemovedResolutionFrame(error) {
|
||||
return /^Frame with ID 0 was removed\.?$/i.test(String(error?.message || error));
|
||||
}
|
||||
|
||||
async function resolutionRecoverySnapshot(tabId) {
|
||||
try {
|
||||
const state = await readErpKeepaliveState();
|
||||
const code = (value) => /^[a-z_:\d-]{1,80}$/i.test(String(value || '')) ? String(value) : '';
|
||||
const timestamp = (value) => /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(String(value || '')) ? String(value) : '';
|
||||
return {
|
||||
recovery_status: code(state.recovery_status),
|
||||
recovery_reason: code(state.recovery_reason),
|
||||
recovery_trigger: code(state.recovery_trigger),
|
||||
recovery_attempt_at: timestamp(state.recovery_attempt_at),
|
||||
recovery_reload_requested_at: timestamp(state.recovery_reload_requested_at),
|
||||
recovery_completed_at: timestamp(state.recovery_completed_at),
|
||||
same_tab: state.recovery_tab_id ? state.recovery_tab_id === tabId : null
|
||||
};
|
||||
} catch (error) {
|
||||
return { unavailable: true };
|
||||
}
|
||||
}
|
||||
|
||||
async function assertResolutionRetrySafe(taskId, originalExecution) {
|
||||
throwIfTaskCancelled(taskId);
|
||||
const current = await getExecutionRecord(taskId);
|
||||
throwIfTaskCancelled(taskId);
|
||||
if (!originalExecution?.execution_id || originalExecution.state !== 'running'
|
||||
|| current?.execution_id !== originalExecution.execution_id || current.state !== 'running'
|
||||
|| current.write_started_at || originalExecution.write_started_at) {
|
||||
throw new Error('erp_resolution_retry_not_safe');
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForResolutionDocument(tabId, taskId, originalExecution) {
|
||||
const deadline = Date.now() + 8000;
|
||||
let previousDocument = '';
|
||||
// Read-only probes only. Never use this retry around a save or an entire task.
|
||||
while (Date.now() < deadline) {
|
||||
await assertResolutionRetrySafe(taskId, originalExecution);
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (!isApprovedErpPageUrl(tab.url) || (tab.pendingUrl && !isApprovedErpPageUrl(tab.pendingUrl))) {
|
||||
throw new Error('erp_resolution_retry_page_not_allowed');
|
||||
}
|
||||
let documentId = '';
|
||||
if (tab.status === 'complete' && !tab.pendingUrl) {
|
||||
try {
|
||||
const results = await withDeadline(chrome.scripting.executeScript({
|
||||
target: { tabId, frameIds: [0] },
|
||||
world: 'MAIN',
|
||||
func: () => ({ ready: document.readyState === 'complete', url: location.href })
|
||||
}), Math.max(1, deadline - Date.now()), 'erp_resolution_document_not_stable');
|
||||
const top = results?.find((item) => item.frameId === 0);
|
||||
if (top?.result?.ready === true && isApprovedErpPageUrl(top.result.url)) {
|
||||
documentId = String(top.documentId || '');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isRemovedResolutionFrame(error)) throw error;
|
||||
}
|
||||
}
|
||||
if (Date.now() < deadline && documentId && documentId === previousDocument) {
|
||||
await assertResolutionRetrySafe(taskId, originalExecution);
|
||||
return;
|
||||
}
|
||||
previousDocument = documentId;
|
||||
await delay(250, taskId);
|
||||
}
|
||||
throw new Error('erp_resolution_document_not_stable');
|
||||
}
|
||||
|
||||
function readonlyResolutionException(error, navigationRetry = null) {
|
||||
return normalizeLifecycleReport({
|
||||
status: 'erp_resolution_blocked',
|
||||
blockers: [`erp_resolution_navigation_failed:${error?.message || String(error)}`],
|
||||
preflight: {
|
||||
stage: 'erp_readonly_resolution', no_erp_write: true, write_attempted: false,
|
||||
...(navigationRetry ? { navigation_retry: navigationRetry } : {})
|
||||
},
|
||||
no_erp_write: true,
|
||||
write_attempted: false,
|
||||
manual_review_required: true,
|
||||
side_effects: ['no_procurement', 'no_payment', 'no_notification', 'no_external_send']
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveOperationInErp(tabId, taskId, operation) {
|
||||
const results = await runPageAction(tabId, 'resolveLifecycleOperation', [operation], { allFrames: false }, taskId);
|
||||
const originalExecution = await getExecutionRecord(taskId);
|
||||
const navigationRetry = { attempts: 0, max_attempts: 2, failures: [] };
|
||||
let results;
|
||||
let failure;
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
navigationRetry.attempts += 1;
|
||||
try {
|
||||
results = await runPageAction(tabId, 'resolveLifecycleOperation', [operation], { allFrames: false }, taskId);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error?.code === 'task_cancelled') throw error;
|
||||
failure = error;
|
||||
navigationRetry.failures.push({
|
||||
attempt: attempt + 1,
|
||||
observed_at: nowIso(),
|
||||
main_frame_removed: isRemovedResolutionFrame(error),
|
||||
step: error?.page_step || 'resolve_lifecycle_operation',
|
||||
recovery: await resolutionRecoverySnapshot(tabId)
|
||||
});
|
||||
if (attempt !== 0 || !isRemovedResolutionFrame(error)) break;
|
||||
try {
|
||||
await waitForResolutionDocument(tabId, taskId, originalExecution);
|
||||
} catch (waitError) {
|
||||
if (waitError?.code === 'task_cancelled') throw waitError;
|
||||
failure = waitError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!results) {
|
||||
return { ok: false, report: readonlyResolutionException(failure, navigationRetry), operation: null };
|
||||
}
|
||||
const report = normalizeLifecycleReport(results[0]?.result || {
|
||||
status: 'erp_resolution_blocked',
|
||||
blockers: ['erp_resolution_missing_result'],
|
||||
@@ -2307,6 +2439,9 @@ async function resolveOperationInErp(tabId, taskId, operation) {
|
||||
write_attempted: false,
|
||||
manual_review_required: true
|
||||
});
|
||||
if (navigationRetry.failures.length) {
|
||||
report.preflight = { ...report.preflight, navigation_retry: navigationRetry };
|
||||
}
|
||||
const resolvedOperation = report.resolved_operation;
|
||||
if (report.status !== 'erp_resolution_ready' || !resolvedOperation || typeof resolvedOperation !== 'object') {
|
||||
return { ok: false, report: withoutResolvedOperation(report), operation: null };
|
||||
@@ -2334,6 +2469,12 @@ function erpResolutionFailure(operation, report = {}) {
|
||||
&& operation.data.departure_dates.length === 1;
|
||||
const targetLabel = identifier ? `单号“${identifier}”` : '当前业务条件';
|
||||
const blockers = Array.isArray(report.blockers) ? report.blockers.map(String) : [];
|
||||
if (blockers.some((item) => /Frame with ID 0 was removed|erp_resolution_document_not_stable/.test(item))) {
|
||||
return {
|
||||
errorCode: 'erp_resolution_page_changed',
|
||||
message: 'ERP 主页面在目标查询时失效,查询未能完成;未写入 ERP。请待页面加载稳定后再操作。'
|
||||
};
|
||||
}
|
||||
if (operation?.action === 'shared_child_order_batch_create') {
|
||||
if (blockers.includes('shared_child_batch_no_matching_parent')) {
|
||||
return {
|
||||
@@ -2472,6 +2613,7 @@ async function executeLifecycleOperation(taskId, operation, operationPlan) {
|
||||
|
||||
let tab;
|
||||
let preflight;
|
||||
let preparationStage = 'erp_resolution';
|
||||
try {
|
||||
tab = await findOrOpenErpTab(taskId);
|
||||
const resolution = await resolveOperationInErp(tab.id, taskId, executionOperation);
|
||||
@@ -2493,6 +2635,7 @@ async function executeLifecycleOperation(taskId, operation, operationPlan) {
|
||||
return { ok: false, status: 'blocked', task_id: taskId, report: resolution.report };
|
||||
}
|
||||
executionOperation = resolution.operation;
|
||||
preparationStage = 'lifecycle_route_preparation';
|
||||
await setResult(taskId, {
|
||||
stage: 'lifecycle_route_preparation',
|
||||
status: 'running',
|
||||
@@ -2556,9 +2699,30 @@ async function executeLifecycleOperation(taskId, operation, operationPlan) {
|
||||
return { ok: false, status: 'blocked', task_id: taskId, report: strictGate };
|
||||
}
|
||||
await cacheResolvedExecutionOperation(taskId, executionOperation);
|
||||
preparationStage = 'lifecycle_preflight';
|
||||
preflight = await preflightLifecycleAfterRoute(tab.id, executionOperation, taskId, routeToken);
|
||||
} catch (error) {
|
||||
if (error?.code === 'task_cancelled') throw error;
|
||||
if (preparationStage !== 'lifecycle_preflight') {
|
||||
const report = preparationStage === 'erp_resolution'
|
||||
? readonlyResolutionException(error)
|
||||
: normalizeLifecycleReport({
|
||||
status: 'lifecycle_route_blocked', blockers: [error?.message || String(error)],
|
||||
no_erp_write: true, write_attempted: false, manual_review_required: true
|
||||
});
|
||||
const failure = preparationStage === 'erp_resolution'
|
||||
? erpResolutionFailure(executionOperation, report)
|
||||
: lifecycleRouteFailure(executionOperation, report);
|
||||
await chrome.storage.local.set({
|
||||
[preparationStage === 'erp_resolution' ? 'lastErpResolution' : 'lastLifecycleRoutePreparation']: report
|
||||
});
|
||||
await publish(taskId, executionOperation, preparationStage, report, {
|
||||
status: 'blocked', error_code: failure.errorCode, failure_stage: preparationStage,
|
||||
failure_source: 'plugin_executor', failure_message: failure.message, message: failure.message,
|
||||
operation_plan: publicPlan, no_erp_write: true, write_attempted: false, blockers: report.blockers
|
||||
});
|
||||
return { ok: false, status: 'blocked', task_id: taskId, report };
|
||||
}
|
||||
preflight = normalizeLifecycleReport({
|
||||
status: 'lifecycle_preflight_blocked',
|
||||
blockers: [error.message || String(error)],
|
||||
|
||||
@@ -6600,7 +6600,7 @@
|
||||
}
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
version: '0.5.184',
|
||||
version: '0.5.185',
|
||||
resolveNativeListSearchValues,
|
||||
lookupKeywordMatchesText,
|
||||
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "联泰下单助手",
|
||||
"version": "0.5.184",
|
||||
"version": "0.5.185",
|
||||
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
...(window.LTJTOrderAssistant || {}),
|
||||
version: '0.5.184',
|
||||
version: '0.5.185',
|
||||
openTeamBatchForm,
|
||||
pingTeamBatchFrame,
|
||||
preflightTeamBatchNative,
|
||||
|
||||
Reference in new issue
Block a user