修复bug

This commit is contained in:
brother7 committed 2026-08-30 00:13:22 +08:00
1 parent c65f00c941
commit 98282d127c
33 files changed
+1104 -167

No files matched your search

@@ -11,9 +11,15 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.158`。版本化 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.163`。版本化 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.158 当前重点:
0.5.163 当前重点:
- 保留 `0.5.162` 的稳定态处理:独立团单个下单只有在表单结构、客户/跟单人/销售人三个原生 SelectBox lookup 数据以及页面 jQuery Ajax 全部就绪后才进入预检;`GetProduct` 外层完成后还会等待页面自身 `ajaxStop`,再应用最终业务字段。该门禁使用原生完成事件和失败超时,不使用固定 sleep,也不在 live gate 静默回填。
- 按生产操作方明确要求,独立团单个下单的受保护字段范围恢复为 `0.5.156` 的实际效果:日期、客户、产品、人数、房型、跟单人、销售人等核心字段继续执行写前漂移阻断;带编号的 `ys_danwei*`、`ys_danweiid*`、`ys_bizhong*`、`ys_shuliang*`、`ys_jine*` 不再进入该哈希门禁,允许 ERP 原生表单在预检后调整这些动态应收行。漂移诊断仍只报告字段名,不保存字段值。
- 修复 ERP 链路恢复在 Chrome MV3 服务工作线程中断后残留 `running`、导致操作台永久显示“已连接,有告警”并阻断新任务的问题。真实内存恢复仍在执行时继续保持 `running`;Promise 已不存在时收敛为 `interrupted`,后续普通只读保活成功后转为 `idle`。该修复不重放业务任务,也不放宽 ERP 写入门禁。
- 正式控制面 `https://lwlt.nianxx.cn` 现在进入精确业务系统白名单;Popup、后台标签页发现、Manifest host permission 和 content script 注入保持同一 origin。相似域名、子域名及其他 HTTPS 站点仍不允许注入。
@@ -826,15 +826,27 @@ async function erpSessionKeepaliveEnabled() {
return saved.erpAutomationEnabled !== false && saved.erpKeepaliveEnabled !== false;
}
function normalizeErpKeepaliveState(state = {}, recoveryInFlight = false) {
const recoveryStatus = String(state.recovery_status || 'not_started');
if (recoveryStatus !== 'running' || recoveryInFlight) return { ...state };
return {
...state,
last_status: String(state.last_status || '') === 'running' ? 'interrupted' : state.last_status,
recovery_status: 'interrupted',
recovery_error: 'erp_link_recovery_interrupted'
};
}
async function readErpKeepaliveState() {
const saved = await chrome.storage.local.get([
'erpAutomationEnabled',
'erpKeepaliveEnabled',
ERP_KEEPALIVE_STATE_KEY
]);
const state = saved[ERP_KEEPALIVE_STATE_KEY] && typeof saved[ERP_KEEPALIVE_STATE_KEY] === 'object'
const persistedState = saved[ERP_KEEPALIVE_STATE_KEY] && typeof saved[ERP_KEEPALIVE_STATE_KEY] === 'object'
? saved[ERP_KEEPALIVE_STATE_KEY]
: {};
const state = normalizeErpKeepaliveState(persistedState, Boolean(erpLinkRecoveryInFlight));
const automationEnabled = saved.erpAutomationEnabled !== false;
const userEnabled = saved.erpKeepaliveEnabled !== false;
return {
@@ -1216,7 +1228,12 @@ async function runErpSessionKeepalive(trigger = 'alarm') {
last_error: success ? '' : errorMessage,
last_response_status: Number(probe.status || 0),
last_duration_ms: Number(probe.duration_ms || (Date.now() - startedAt)),
...(success ? { last_success_at: nowIso() } : {})
...(success ? { last_success_at: nowIso() } : {}),
...(success && !erpLinkRecoveryInFlight ? {
recovery_status: 'idle',
recovery_completed_at: '',
recovery_error: ''
} : {})
});
if (!success && shouldRecoverErpLink(status, probe)) {
const recovery = requestErpLinkRecovery(`keepalive_${status}`, 'keepalive');
@@ -1660,12 +1677,23 @@ function lightweightOrderFrameProbe() {
} catch (error) {
visible = false;
}
const missing = ['zutuanshe', 'gendanren', 'xiaoshouren']
.filter((name) => {
const input = form.querySelector(`#${name}`);
return !input || !String(input.getAttribute('data') || '').length;
})
.map((name) => `${name}.data`);
const pageJq = window.jQuery || window.$;
const pendingAjaxCount = Math.max(0, Number(pageJq?.active || 0));
if (pendingAjaxCount) missing.push(`jquery.ajax.pending:${pendingAjaxCount}`);
return withTiming({
status: form.elements.length >= 800 ? 'order_frame_ready' : 'order_frame_loading',
status: form.elements.length >= 800 && !missing.length ? 'order_frame_ready' : 'order_frame_loading',
url: location.href,
title: document.title,
entry_stamp: new URL(location.href).searchParams.get('_') || '',
form_element_count: form.elements.length,
pending_ajax_count: pendingAjaxCount,
missing,
visible
});
}
+128 -8
View File
@@ -4802,8 +4802,7 @@
]);
function isProtectedSubmitField(name) {
return PROTECTED_SUBMIT_FIELD_NAMES.has(name)
|| /^ys_(danwei|danweiid|bizhong|shuliang|jine)\\d*$/.test(name);
return PROTECTED_SUBMIT_FIELD_NAMES.has(name);
}
function protectedSubmitProjection(serializedForm) {
@@ -4818,6 +4817,69 @@
]));
}
function protectedSubmitProjectionChanges(approvedProjection, currentProjection) {
const approved = new Map(JSON.parse(approvedProjection));
const current = new Map(JSON.parse(currentProjection));
return [...new Set([...approved.keys(), ...current.keys()])]
.sort()
.filter((name) => JSON.stringify(approved.get(name) || []) !== JSON.stringify(current.get(name) || []));
}
function protectedSubmitFieldChanges(approvedSerializedForm, currentSerializedForm) {
return protectedSubmitProjectionChanges(
protectedSubmitProjection(approvedSerializedForm),
protectedSubmitProjection(currentSerializedForm)
);
}
function jqueryPendingAjaxCount(jq) {
return Math.max(0, Number(jq?.active || 0));
}
async function waitForJQueryAjaxIdle(jq, timeoutMs = 12000) {
const startedAt = performance.now();
const initialPending = jqueryPendingAjaxCount(jq);
if (!initialPending) {
return {
status: 'idle',
initial_pending_ajax_count: 0,
final_pending_ajax_count: 0,
duration_ms: performance.now() - startedAt
};
}
const eventTarget = typeof jq === 'function' ? jq(document) : null;
if (!eventTarget || typeof eventTarget.one !== 'function') {
return {
status: 'event_unavailable',
initial_pending_ajax_count: initialPending,
final_pending_ajax_count: jqueryPendingAjaxCount(jq),
duration_ms: performance.now() - startedAt
};
}
return new Promise((resolve) => {
const eventName = 'ajaxStop.ltjtOrderPreflightIdle';
let finished = false;
let timeoutId = 0;
const finish = (status) => {
if (finished) return;
finished = true;
if (timeoutId) window.clearTimeout(timeoutId);
if (typeof eventTarget.off === 'function') eventTarget.off(eventName);
resolve({
status,
initial_pending_ajax_count: initialPending,
final_pending_ajax_count: jqueryPendingAjaxCount(jq),
duration_ms: performance.now() - startedAt
});
};
eventTarget.one(eventName, () => finish(
jqueryPendingAjaxCount(jq) === 0 ? 'idle' : 'still_pending'
));
timeoutId = window.setTimeout(() => finish('timeout'), Math.max(1, Number(timeoutMs) || 12000));
if (jqueryPendingAjaxCount(jq) === 0) finish('idle');
});
}
function liveSubmitPrewriteBlocked(blockers, extra = {}) {
return {
status: 'live_submit_blocked',
@@ -4918,12 +4980,23 @@
async function pingOrderFrame() {
const form = getOrderForm();
if (!form) return { status: 'not_order_frame', url: location.href, title: document.title };
const missing = ['zutuanshe', 'gendanren', 'xiaoshouren']
.filter((name) => {
const input = form.querySelector(`#${name}`);
return !input || !String(input.getAttribute('data') || '').length;
})
.map((name) => `${name}.data`);
const jq = window.jQuery || window.$;
const pendingAjaxCount = jqueryPendingAjaxCount(jq);
if (pendingAjaxCount) missing.push(`jquery.ajax.pending:${pendingAjaxCount}`);
return {
status: form.elements.length >= 800 ? 'order_frame_ready' : 'order_frame_loading',
status: form.elements.length >= 800 && !missing.length ? 'order_frame_ready' : 'order_frame_loading',
url: location.href,
title: document.title,
entry_stamp: new URL(location.href).searchParams.get('_') || '',
form_element_count: form.elements.length,
pending_ajax_count: pendingAjaxCount,
missing,
visible: getFrameVisibility()
};
}
@@ -5131,6 +5204,12 @@
let beforeProduct = snapshotForm(form);
let productEffectCompleted = false;
let nativeAjaxIdle = {
status: 'not_checked',
initial_pending_ajax_count: 0,
final_pending_ajax_count: 0,
duration_ms: 0
};
const productEffectStartedAt = performance.now();
if (!blockers.length) {
setValue(form, blockers, setResults, 'chufa_ri', dateYyyyMD(data.departure_dates[0]), 'raw_instruction_departure_date');
@@ -5154,6 +5233,10 @@
} else {
await productEffectCompletion;
productEffectCompleted = ajaxRecords.some((record) => record.completed);
nativeAjaxIdle = await waitForJQueryAjaxIdle(jq);
if (nativeAjaxIdle.status !== 'idle') {
blockers.push(`ERP native ajax did not become idle after GetProduct: ${nativeAjaxIdle.status}`);
}
}
}
}
@@ -5167,6 +5250,15 @@
completion_event_count: productEffectCompleted ? 1 : 0
}
});
timingDetails.push({
step: 'preflight_native_ajax_idle',
status: nativeAjaxIdle.status === 'idle' ? 'completed' : 'blocked',
duration_ms: nativeAjaxIdle.duration_ms,
counters: {
initial_pending_ajax_count: nativeAjaxIdle.initial_pending_ajax_count,
final_pending_ajax_count: nativeAjaxIdle.final_pending_ajax_count
}
});
const afterProduct = snapshotForm(form);
const productChanges = compareSnapshots(beforeProduct, afterProduct);
@@ -5413,6 +5505,13 @@
// baselines: the post-call browser form and the exact native request.
const postInterceptSerializedForm = serializeForm(form);
const postInterceptParams = new URLSearchParams(postInterceptSerializedForm);
const postInterceptProtectedProjection = protectedSubmitProjection(postInterceptSerializedForm);
const preflightGuard = jq?.ajax;
if (preflightGuard?.__ltjt_preflight_guard === true) {
// Keep values only inside the live page guard. Persisted diagnostics
// report changed field names, never the approved or current values.
preflightGuard.__ltjt_approved_form_protected_projection = postInterceptProtectedProjection;
}
const receivableAmountTotal = receivableRows.reduce((sum, row) => sum + row.amount, 0);
const output = {
@@ -5451,7 +5550,7 @@
protected_fields_sha256: await sha256(protectedSubmitProjection(serializedForm)),
post_intercept_serialized_field_count: Array.from(postInterceptParams.keys()).length,
post_intercept_serialized_form_sha256: await sha256(`Act=DoInfoJH&${postInterceptSerializedForm}`),
post_intercept_protected_fields_sha256: await sha256(protectedSubmitProjection(postInterceptSerializedForm)),
post_intercept_protected_fields_sha256: await sha256(postInterceptProtectedProjection),
serialized_form_redacted: true,
required_missing: requiredMissing
},
@@ -5591,7 +5690,8 @@
const serializedForm = serializeForm(form);
const requestBody = `Act=DoInfoJH&${serializedForm}`;
const currentHash = await sha256(requestBody);
const currentProtectedFieldsHash = await sha256(protectedSubmitProjection(serializedForm));
const currentProtectedProjection = protectedSubmitProjection(serializedForm);
const currentProtectedFieldsHash = await sha256(currentProtectedProjection);
const params = new URLSearchParams(serializedForm);
const blockers = [];
const guardedAjax = jq.ajax;
@@ -5603,8 +5703,26 @@
if (!hasPreflightGuard) blockers.push('preflight submit guard is missing or expired');
if (hasPreflightGuard && preflightSubmitCount !== 1) blockers.push('preflight guard did not retain exactly one intercepted DoInfoJH submit');
if (approvedFormFieldCount && Array.from(params.keys()).length !== approvedFormFieldCount) blockers.push('current browser field count does not match approved post-intercept field count');
let protectedFieldDrift = null;
if (currentProtectedFieldsHash !== approvedFormProtectedFieldsSha256) {
blockers.push('current protected business fields do not match the approved preflight');
const approvedProjection = typeof guardedAjax?.__ltjt_approved_form_protected_projection === 'string'
? guardedAjax.__ltjt_approved_form_protected_projection
: '';
const baselineMatchesApprovedHash = approvedProjection
? await sha256(approvedProjection) === approvedFormProtectedFieldsSha256
: false;
const changedFieldNames = baselineMatchesApprovedHash
? protectedSubmitProjectionChanges(approvedProjection, currentProtectedProjection)
: [];
protectedFieldDrift = {
baseline_available: Boolean(approvedProjection),
baseline_matches_approved_hash: baselineMatchesApprovedHash,
changed_field_count: changedFieldNames.length,
changed_field_names: changedFieldNames,
values_redacted: true
};
const changedFieldSuffix = changedFieldNames.length ? ` (changed fields: ${changedFieldNames.join(', ')})` : '';
blockers.push(`current protected business fields do not match the approved preflight${changedFieldSuffix}`);
}
if (blockers.length) {
return liveSubmitPrewriteBlocked(blockers, {
@@ -5613,7 +5731,8 @@
protected_fields_sha256: approvedFormProtectedFieldsSha256,
field_count: approvedFormFieldCount,
value_redacted: true
}
},
...(protectedFieldDrift ? { protected_field_drift: protectedFieldDrift } : {})
});
}
@@ -5958,7 +6077,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.158',
version: '0.5.163',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
@@ -5988,6 +6107,7 @@
inspectCustomerRestoreReadiness(actualValues, expectedValues) {
return customerRestoreReadiness(actualValues, expectedValues);
},
inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,
inspectArrangementLookupReadiness(form, action, scopeDocument = null, operation = {}) {
return arrangementLookupReadiness(form, action, scopeDocument, operation);
},
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.158",
"version": "0.5.163",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",
@@ -919,7 +919,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.158',
version: '0.5.163',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,