修复bug
This commit is contained in:
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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user