导入问题修复
This commit is contained in:
1 parent
0282c30a2a
commit
ede1d47c47
26 files changed
+934
-45
No files matched your search
@@ -11,9 +11,13 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
|
||||
|
||||
## 当前版本
|
||||
|
||||
当前源码版本为 `0.5.183`。版本化 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`。版本化 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.183 当前重点:
|
||||
0.5.184 当前重点:
|
||||
|
||||
- ERP 列表就绪等待跟随当前 iframe 文档,仍要求加载完成、原生搜索函数与可用按钮,切换期间不在旧文档中搜索;失败回执明确区分页面未就绪,并记录不含业务值的条件与耗时。平台接单等待为 10 秒;明确未写入的迟到检索失败可在同执行编号、原连接范围内同步回平台。需同步更新平台/服务端与插件,无数据库迁移;不重新下发任务,真实写入不确定结果继续人工核验。
|
||||
|
||||
继续保留既有修复:
|
||||
|
||||
- 安排联动稳定检查将正常采样观察预算与定时回调延迟分开;长间隔后重新观察至少 1.5 秒并要求多个有效样本,30 秒总等待上限内恢复,持续变化、持续延迟、控件失效或原生加载未结束均不保存。新增失败回执区分检测延迟与字段变化,详细证据仅记录字段名、采样次数和时间,不记录业务值。保留原生联动、唯一空行、付款审核、其他行保护及保存前复核;不自动重试保存。合成回归覆盖,真实 ERP 验收待部署方执行。
|
||||
|
||||
|
||||
@@ -2390,6 +2390,12 @@ function erpResolutionFailure(operation, report = {}) {
|
||||
}
|
||||
const navigationFailure = blockers.find((item) => item.startsWith('erp_resolution_navigation_failed:'));
|
||||
if (navigationFailure) {
|
||||
if (/native_list_search_not_ready|native_list_search_document_changed/.test(navigationFailure)) {
|
||||
return {
|
||||
errorCode: 'erp_list_search_not_ready',
|
||||
message: 'ERP 列表搜索页面未就绪或在检索时发生切换,已停止,未写入 ERP。请待页面加载完成后再操作。'
|
||||
};
|
||||
}
|
||||
return {
|
||||
errorCode: 'erp_target_lookup_failed',
|
||||
message: `ERP 检索${targetLabel}失败,请确认 ERP 已登录后重试。`
|
||||
|
||||
@@ -1402,19 +1402,72 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForNativeListSearch(initialDocument, timeoutMs = 15000) {
|
||||
const expectedPath = String(initialDocument?.location?.pathname || '').toLowerCase();
|
||||
const framed = initialDocument !== document || Boolean(mainIframe());
|
||||
const now = () => typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
const startedAt = now();
|
||||
let samples = 0;
|
||||
let replacements = 0;
|
||||
let previousDocument = initialDocument;
|
||||
let readiness;
|
||||
while (true) {
|
||||
// A navigating iframe can expose the destination URL before replacing
|
||||
// its Document. Never continue waiting on that detached old Document.
|
||||
let current = null;
|
||||
let accessible = true;
|
||||
try { current = framed ? mainIframe()?.contentDocument : document; }
|
||||
catch { accessible = false; }
|
||||
if (current && current !== previousDocument) replacements += 1;
|
||||
previousDocument = current;
|
||||
let pathMatched = false;
|
||||
let complete = false;
|
||||
let loaderReady = false;
|
||||
let buttonReady = false;
|
||||
try {
|
||||
pathMatched = Boolean(current && expectedPath && String(current.location.pathname).toLowerCase() === expectedPath);
|
||||
complete = current?.readyState === 'complete';
|
||||
loaderReady = typeof current?.defaultView?.AjaxLoadData === 'function';
|
||||
const button = current?.querySelector('#SearchButton, [name="SearchButton"]');
|
||||
buttonReady = Boolean(button && button.isConnected !== false && button.disabled !== true);
|
||||
} catch { accessible = false; }
|
||||
samples += 1;
|
||||
readiness = {
|
||||
document_accessible: accessible, document_present: Boolean(current),
|
||||
path_matched: pathMatched, document_complete: complete,
|
||||
loader_ready: loaderReady, search_button_ready: buttonReady,
|
||||
document_replacements: replacements, samples,
|
||||
elapsed_ms: Math.round(now() - startedAt)
|
||||
};
|
||||
if (accessible && pathMatched && complete && loaderReady && buttonReady) {
|
||||
return { document: current, readiness };
|
||||
}
|
||||
if (readiness.elapsed_ms >= timeoutMs) {
|
||||
const missing = ['document_accessible', 'document_present', 'path_matched', 'document_complete', 'loader_ready', 'search_button_ready']
|
||||
.filter((key) => !readiness[key]);
|
||||
const error = new Error(`native_list_search_not_ready:${missing.join(',')}:elapsed_ms=${readiness.elapsed_ms}:document_replacements=${replacements}`);
|
||||
error.nativeListReadiness = readiness;
|
||||
throw error;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNativeListDocumentCurrent(scopeDocument) {
|
||||
const current = mainIframe() ? mainIframeDocument() : document;
|
||||
if (current !== scopeDocument) throw new Error('native_list_search_document_changed');
|
||||
}
|
||||
|
||||
async function searchMainPage(scopeDocument, values = {}) {
|
||||
const nativeSearchReady = await waitForCondition(() => (
|
||||
scopeDocument.readyState === 'complete'
|
||||
&& typeof scopeDocument.defaultView?.AjaxLoadData === 'function'
|
||||
&& Boolean(scopeDocument.querySelector('#SearchButton, [name="SearchButton"]'))
|
||||
), 15000, 100);
|
||||
if (!nativeSearchReady) throw new Error('native_list_search_not_ready');
|
||||
const ready = await waitForNativeListSearch(scopeDocument);
|
||||
scopeDocument = ready.document;
|
||||
const searchResolution = resolveNativeListSearchValues(values);
|
||||
const searchValues = searchResolution.values;
|
||||
const jq = scopeDocument.defaultView?.jQuery || scopeDocument.defaultView?.$;
|
||||
if (typeof jq !== 'function' || typeof jq.ajax !== 'function') throw new Error('native_list_search_ajax_not_ready');
|
||||
const priorAjaxIdle = await waitForCondition(() => Number(jq.active || 0) === 0, 12000, 50);
|
||||
if (!priorAjaxIdle) throw new Error('native_list_search_prior_ajax_not_idle');
|
||||
assertNativeListDocumentCurrent(scopeDocument);
|
||||
const setIfPresent = (name, value) => {
|
||||
const element = scopeDocument.querySelector(`#${name}, [name="${name}"]`);
|
||||
if (!element) return false;
|
||||
@@ -1486,6 +1539,7 @@
|
||||
), 3000, 50);
|
||||
if (!statusApplied) throw new Error('native_status_filter_not_applied');
|
||||
const clickRequestStarted = await waitForCondition(() => capturedXhr, 3000, 25);
|
||||
assertNativeListDocumentCurrent(scopeDocument);
|
||||
if (!clickRequestStarted) scopeDocument.defaultView.AjaxLoadData(true);
|
||||
} else {
|
||||
// Invoke the ERP's own list loader directly. The exact ajaxSend/ajaxComplete
|
||||
@@ -1498,10 +1552,12 @@
|
||||
const completed = await waitForCondition(() => requestCompleted, 15000, 50);
|
||||
if (!completed) throw new Error('native_list_search_result_timeout');
|
||||
if (httpStatus < 200 || httpStatus >= 300) throw new Error(`native_list_search_http_${httpStatus || 'error'}`);
|
||||
assertNativeListDocumentCurrent(scopeDocument);
|
||||
} finally {
|
||||
eventTarget.off(eventNamespace);
|
||||
}
|
||||
scopeDocument.__ltjtNativeListSearchEvidence = {
|
||||
readiness: ready.readiness,
|
||||
date_strategy: searchResolution.date_strategy,
|
||||
fallback_applied: searchResolution.fallback_applied,
|
||||
date_range: searchResolution.date_range,
|
||||
@@ -1819,7 +1875,7 @@
|
||||
async function searchLifecycleList(path, pattern, criteria, listKind, statusOverride = '') {
|
||||
const searchValues = lifecycleSearchValues(criteria, listKind, statusOverride);
|
||||
let scopeDocument = await ensureMainPage(path, pattern, searchValues);
|
||||
await searchMainPage(scopeDocument, searchValues);
|
||||
scopeDocument = await searchMainPage(scopeDocument, searchValues);
|
||||
let stable = await waitForCondition(() => {
|
||||
const current = currentMainListDocument(pattern, scopeDocument);
|
||||
return current?.readyState === 'complete' ? current : null;
|
||||
@@ -1839,7 +1895,7 @@
|
||||
if (independentBusinessLookup && (!visibleRows.length || !customerEvidence)) {
|
||||
const primaryEvidence = stable?.__ltjtNativeListSearchEvidence || null;
|
||||
const fallbackValues = { ...searchValues, S_kehuming: '' };
|
||||
await searchMainPage(scopeDocument, fallbackValues);
|
||||
scopeDocument = await searchMainPage(scopeDocument, fallbackValues);
|
||||
stable = await waitForCondition(() => {
|
||||
const current = currentMainListDocument(pattern, scopeDocument);
|
||||
return current?.readyState === 'complete' ? current : null;
|
||||
@@ -2463,6 +2519,7 @@
|
||||
blockers: [`erp_resolution_navigation_failed:${error?.message || String(error)}`],
|
||||
preflight: {
|
||||
stage: 'erp_readonly_resolution',
|
||||
...(error.nativeListReadiness ? { native_list_readiness: error.nativeListReadiness } : {}),
|
||||
lookup_mode: criteria.mode,
|
||||
identifier,
|
||||
supplied_filters: {
|
||||
@@ -2552,7 +2609,7 @@
|
||||
let listDocument;
|
||||
try {
|
||||
listDocument = await ensureMainPage(listPath, listPattern, searchValues);
|
||||
await searchMainPage(listDocument, searchValues);
|
||||
listDocument = await searchMainPage(listDocument, searchValues);
|
||||
} catch (error) {
|
||||
return lifecycleRouteOutcome('lifecycle_route_blocked', operation, {
|
||||
blockers: [`passenger_list_navigation_failed:${error?.message || String(error)}`]
|
||||
@@ -2816,7 +2873,7 @@
|
||||
let listDocument;
|
||||
try {
|
||||
listDocument = await ensureMainPage(listPath, listPattern, searchValues);
|
||||
await searchMainPage(listDocument, searchValues);
|
||||
listDocument = await searchMainPage(listDocument, searchValues);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -3893,7 +3950,7 @@
|
||||
S_lingdui: '',
|
||||
S_zhuangtai: ''
|
||||
};
|
||||
await searchMainPage(scopeDocument, searchValues);
|
||||
scopeDocument = await searchMainPage(scopeDocument, searchValues);
|
||||
const row = rowForIdentifier(scopeDocument, groupNumber);
|
||||
return {
|
||||
found: Boolean(row),
|
||||
@@ -3939,7 +3996,7 @@
|
||||
S_lingdui: '',
|
||||
S_zhuangtai: ''
|
||||
};
|
||||
await searchMainPage(scopeDocument, searchValues);
|
||||
scopeDocument = await searchMainPage(scopeDocument, searchValues);
|
||||
const selectionExpected = options.fact_probe
|
||||
? { ...expected, customer_name: '', passenger_counts: undefined, require_customer_match: false, date }
|
||||
: { ...expected, date };
|
||||
@@ -4584,7 +4641,7 @@
|
||||
S_tuanxuhao: parentGroup,
|
||||
S_zhuangtai: ''
|
||||
});
|
||||
await searchMainPage(listDocument, {
|
||||
listDocument = await searchMainPage(listDocument, {
|
||||
S_chufariqi: dateYyyyMD(dates[0] || ''),
|
||||
S_chufarizhi: dateYyyyMD(dates[0] || ''),
|
||||
S_tuanxuhao: parentGroup,
|
||||
@@ -4945,7 +5002,7 @@
|
||||
S_tuanxuhao: listIdentifier
|
||||
};
|
||||
let listDocument = await ensureMainPage(listPath, listPattern, searchValues);
|
||||
await searchMainPage(listDocument, searchValues);
|
||||
listDocument = await searchMainPage(listDocument, searchValues);
|
||||
const sourceSearchEvidence = listDocument.__ltjtNativeListSearchEvidence || null;
|
||||
const stableSourceDocument = await waitForCondition(() => {
|
||||
const current = currentMainListDocument(listPattern, listDocument);
|
||||
@@ -6543,7 +6600,7 @@
|
||||
}
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
version: '0.5.183',
|
||||
version: '0.5.184',
|
||||
resolveNativeListSearchValues,
|
||||
lookupKeywordMatchesText,
|
||||
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "联泰下单助手",
|
||||
"version": "0.5.183",
|
||||
"version": "0.5.184",
|
||||
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
...(window.LTJTOrderAssistant || {}),
|
||||
version: '0.5.183',
|
||||
version: '0.5.184',
|
||||
openTeamBatchForm,
|
||||
pingTeamBatchFrame,
|
||||
preflightTeamBatchNative,
|
||||
|
||||
Reference in new issue
Block a user