判断修改

This commit is contained in:
andy committed 2026-09-14 12:47:15 +08:00
1 parent e06afc8c33
commit 4194a200c1
65 files changed
+9617 -109

No files matched your search

@@ -11,9 +11,13 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.177`。版本化 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.178`。版本化 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.177 当前重点:
0.5.178 当前重点:
- 酒店新增取消固定 150 个可序列化字段门槛,改为检查当前已连接表单、有效 MaxI、连续酒店行及每行必要控件;控件唯一且可用、页面加载完成、无 loading/未完成 Ajax,并实际观察结构稳定至少 1 秒后才通过。76 字段两行与较长表单采用同一结构检查;字段总数只作诊断。资源匹配、空槽、付款审核、身份归属及保存回查仍执行。酒店修改/清空和其他业务的门槛不变。真实 ERP 验收待部署方完成;本项运行修复只需替换插件,无迁移。
继续保留 0.5.177 的车票说明过滤:
- 程序模式“安排大交通 / 安排大交通变更”新增选填车票说明(别名:车票说明搜索)。与结算单位搜索共同筛选同一条原生候选,填写后仅接受唯一结果;省略沿用首条匹配行为。完整说明与价格等字段仍由 ERP 原生选择联动。原有“车票/控位说明”兼容字段不变。需同步更新服务端,无新增迁移。
@@ -26,9 +30,7 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
- 名单 TSV 在派发校验、领队提取和原生名单合并时保留末尾空列的制表符,只统一换行并移除首尾空行。最后一条电话或备注为空时仍按完整 13 列处理;真正少列、多列及领队不一致仍阻断。本次需同步部署服务端的统一 operation 校验修复,不涉及数据库变更。
继续保留 0.5.172 的酒店门槛修正:
- 按维护方要求,将酒店新增 `DoInfo_jiudian` 的最少可序列化字段数从 195 调整为 150,恢复原有页面预检流程。154 字段页面可以通过数量检查;关键字段、loading、稳定 1 秒、资源、空槽、财务状态、归属和保存后回查检查保持原样。酒店修改/清空仍为 150,其他业务阈值不变。低于 150 的页面仍会阻断;真实 ERP 验收由部署方完成。
0.5.172 的酒店新增 195 → 150 门槛修正已由 0.5.178 的结构检查取代;酒店修改/清空仍保留原门槛。
继续保留 0.5.170 的批量子单执行能力:
@@ -6518,7 +6518,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.177',
version: '0.5.178',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
@@ -155,7 +155,6 @@
const FORM_READINESS_MINIMUMS = Object.freeze({
DoInfo_daoyou: 12,
DoInfo_cheliang: 90,
DoInfo_jiudian: 150,
DoInfo_piao: 120,
DoInfo_qita: 120,
DoInfoJH: 800,
@@ -688,7 +687,102 @@
});
}
// Hotel pages vary with the number of native slots. A successful-control
// total cannot prove completeness: short complete forms and large partial
// forms can fall on either side of the same threshold.
function hotelFormStructure(form) {
const required = FORM_READINESS_REQUIRED_FIELDS.DoInfo_jiudian;
const rowPrefixes = required.filter((name) => name.endsWith('0')).map((name) => name.slice(0, -1));
const controls = Array.from(form?.elements || []);
const byName = new Map();
const indexes = new Set();
const disabled = (control) => control.disabled === true || control.matches?.(':disabled') === true;
for (const control of controls) {
for (const key of new Set([control.name, control.id].filter(Boolean))) {
if (!byName.has(key)) byName.set(key, []);
byName.get(key).push(control);
const match = key.match(/^([a-zA-Z_]+)(\d+)$/);
if (match && rowPrefixes.includes(match[1])) indexes.add(Number(match[2]));
}
}
const maxIText = controlValue(getControl(form, 'MaxI'));
const maxI = /^[1-9]\d*$/.test(maxIText) && Number.isSafeInteger(Number(maxIText)) ? Number(maxIText) : null;
const slots = [...indexes].sort((a, b) => a - b);
const blockers = [];
const missing = [];
const unavailable = [];
if (!form || form.isConnected !== true || findForm(CONTRACTS.arrangement_hotel, form.ownerDocument) !== form) {
blockers.push('lifecycle_complete_form_missing');
}
if (form?.ownerDocument?.readyState !== 'complete') blockers.push('lifecycle_document_not_ready');
if (maxI === null) blockers.push('lifecycle_hotel_max_i_invalid');
// Native MaxI is the slot count (zero-based slots 0..MaxI-1). Enumerate
// actual slots, never loop up to an untrusted marker value.
if (maxI !== null && (slots.length !== maxI || slots.some((slot, index) => slot !== index))) {
blockers.push(`lifecycle_hotel_rows_incomplete:${slots.length}:${maxI}`);
}
const names = [...new Set([...required, ...slots.flatMap((slot) => rowPrefixes.map((prefix) => `${prefix}${slot}`))])];
for (const name of names) {
const matches = byName.get(name) || [];
if (!matches.length) { missing.push(name); continue; }
const control = matches[0];
const submit = name === 'SubmitButton';
const tag = String(control.tagName || '').toLowerCase();
const type = String(control.type || '').toLowerCase();
if (matches.length !== 1 || control.isConnected === false || disabled(control)
|| (!submit && (control.name !== name || !['input', 'select', 'textarea'].includes(tag)
|| NON_SERIALIZABLE_CONTROL_TYPES.has(type)))
|| (submit && !['input', 'button'].includes(tag))) unavailable.push(name);
}
if (missing.length) blockers.push(`lifecycle_required_fields_missing:${missing.join(',')}`);
if (unavailable.length) blockers.push(`lifecycle_required_fields_unavailable:${unavailable.join(',')}`);
const signature = JSON.stringify([maxIText, controls.map((control) => [
control.name || '', control.id || '', control.tagName || '', control.type || '',
disabled(control), control.readOnly === true, control.options?.length ?? null
])]);
return { controls, signature, blockers, missing_required_fields: missing,
unavailable_required_fields: unavailable, native_max_i: maxI, hotel_row_count: slots.length };
}
async function waitForHotelCreateReadiness(form, timeoutMs) {
const deadline = Date.now() + timeoutMs;
let previous = null;
let stableSince = null;
let latest;
let stable = false;
do {
const structure = hotelFormStructure(form);
const loading = form ? visibleLoadingSignals(form.ownerDocument.defaultView) : [];
const count = form ? serializedFieldCount(form) : 0;
const ajaxActive = Number(form?.ownerDocument?.defaultView?.jQuery?.active || 0) > 0;
const blockers = [...structure.blockers];
if (loading.length || ajaxActive) blockers.push('lifecycle_loading_overlay_visible');
const signature = JSON.stringify([structure.signature, count, loading, ajaxActive]);
const sameNodes = previous && structure.controls.length === previous.controls.length
&& structure.controls.every((control, index) => control === previous.controls[index]);
const now = Date.now();
if (blockers.length) stableSince = null;
else if (!previous || signature !== previous.signature || !sameNodes || stableSince === null) stableSince = now;
stable = !blockers.length && now - stableSince >= 1000;
latest = { ready: stable, strategy: 'native_hotel_structure', native_action: 'DoInfo_jiudian',
minimum_successful_controls: null, required_fields: FORM_READINESS_REQUIRED_FIELDS.DoInfo_jiudian,
successful_control_count: count, native_max_i: structure.native_max_i,
hotel_row_count: structure.hotel_row_count, missing_required_fields: structure.missing_required_fields,
unavailable_required_fields: structure.unavailable_required_fields, visible_loading_signals: loading,
native_ajax_active: ajaxActive, stable_ms: stableSince === null ? 0 : now - stableSince,
blockers, value_redacted: true };
previous = { signature, controls: structure.controls };
if (stable || now >= deadline) break;
await sleep(Math.min(250, deadline - now));
} while (true);
if (!stable && !latest.blockers.length) latest.blockers.push('lifecycle_form_not_stable');
return latest;
}
async function waitForFormReadiness(operation, contract, form, timeoutMs = 12000) {
if (operation.action === 'arrangement_hotel' && operation.data?.arrangement?.mode === 'create') {
return waitForHotelCreateReadiness(form, timeoutMs);
}
const target = formReadinessTarget(operation, contract, form);
const profile = readinessProfile(operation, contract);
if (!profile.minimum_successful_controls) return {
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.177",
"version": "0.5.178",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",
@@ -944,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.177',
version: '0.5.178',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,