fix: require exact ERP account identity

This commit is contained in:
inman
2026-09-07 18:18:31 +08:00
parent 6bdaa2a6e6
commit 8f70cbae4d
17 changed files with 201 additions and 31 deletions

View File

@@ -100,7 +100,7 @@ const persistedExtensionResultVersions = new Map();
let taskCreateInProgress = false;
const AUTO_HANDOFF_RETRY_MS = 30_000;
const REQUIRED_EXTENSION_VERSION = '0.5.167';
const REQUIRED_EXTENSION_VERSION = '0.5.168';
const MANUAL_HANDOFF_LABEL = '确认并提交到 ERP 插件';
const RETRY_HANDOFF_LABEL = '继续提交到 ERP 插件';
const RECONCILE_LABEL = '只读回查 ERP 现有结果';

View File

@@ -2,7 +2,7 @@
## 当前基线
- Chrome 插件:`0.5.167`
- Chrome 插件:`0.5.168`
- Agent Prompt`ltjt-agent-prompt-v1.8-independent-headcount-categories`
- 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08`
- 五个 Skill`0.5.125`;运营 DOCX`0.5.125`
@@ -10,6 +10,7 @@
- `0.5.166` 在散拼母团列表入口加入立即检查、100ms 条件轮询和 15 秒总超时;每次探测都重新取得当前主 iframe document按钮一旦可用便立即继续。该等待只发生在点击前不固定拖慢快速页面也不触发业务写入重试。
- `0.5.167` 保留插件侧的空闲证明和受控重载安全协议,并继续包含 `0.5.166` 的散拼入口自适应等待。中央服务私有 OSS/ECS 云助手编排及迁移 019 已按用户决定从主线撤回,因此这些插件更新消息在当前主线没有服务端触发方,属于休眠能力;插件仍按现有人工发布/加载流程使用。
- `0.5.168` 将 ERP 身份门禁从整页子串搜索收紧为唯一 `#Lable_UserName` 登录节点的规范化精确全等;缺失、重复、空白、子串或其他页面文字命中均不再让云电脑进入可执行状态,观察到的账号文本不离开扩展。
运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。

View File

@@ -0,0 +1,6 @@
# 2026-09-07 发布归档
本目录只保存被新版本替代的历史发布物,不定义当前运行规则。当前制品文件名和 SHA-256 只以根目录 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json) 为准。
- `release-manifest-0.5.167.json`:收紧 ERP 登录账号身份识别前的历史清单快照。
- `ltjt-order-assistant-0.5.167.zip`:被 `0.5.168` 的唯一登录节点精确账号匹配替代;保留用于恢复和追溯。

View File

@@ -0,0 +1,69 @@
{
"manifest_version": 1,
"generated_on": "2026-09-03",
"baselines": {
"chrome_extension": "0.5.167",
"skills": "0.5.125",
"business_instruction_docx": "0.5.125",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
},
"artifacts": [
{
"kind": "chrome_extension",
"version": "0.5.167",
"path": "dist/ltjt-order-assistant-0.5.167.zip",
"source": "chrome-extension/ltjt-order-assistant",
"sha256": "6822ec660097a8889da8ea4fa22d4091467aa022067bff3bee2ac5406864b679"
},
{
"kind": "skill",
"name": "lwlt-arrangement",
"version": "0.5.125",
"path": "dist/lwlt-arrangement-0.5.125.skill",
"source": "agent设计规范/skills/lwlt-arrangement",
"sha256": "c729bebec84695b8642a6d31cb4dc0d963135b6c6620c2173be095826cd3976d"
},
{
"kind": "skill",
"name": "lwlt-confirmation",
"version": "0.5.125",
"path": "dist/lwlt-confirmation-0.5.125.skill",
"source": "agent设计规范/skills/lwlt-confirmation",
"sha256": "659f6aca7918a03a29fc78f0b4b938dfa9084ccdbb44ebd71f6399556cfa4609"
},
{
"kind": "skill",
"name": "lwlt-lifecycle",
"version": "0.5.125",
"path": "dist/lwlt-lifecycle-0.5.125.skill",
"source": "agent设计规范/skills/lwlt-lifecycle",
"sha256": "d964c0a9e482f82757fea25d4713f0c81feb2704486583dfa84f2336ecd930c0"
},
{
"kind": "skill",
"name": "lwlt-newbooking",
"version": "0.5.125",
"path": "dist/lwlt-newbooking-0.5.125.skill",
"source": "agent设计规范/skills/lwlt-newbooking",
"sha256": "bb610c50dd659fc172aabfa469b3028de98704c249cfd4890463deea0c88cd0c"
},
{
"kind": "skill",
"name": "lwlt-updating",
"version": "0.5.125",
"path": "dist/lwlt-updating-0.5.125.skill",
"source": "agent设计规范/skills/lwlt-updating",
"sha256": "c6f5bc9008e2f63d170f3f140140003d99411ce5232632c48b38891da1ceee33"
},
{
"kind": "business_instruction_docx",
"version": "0.5.125",
"path": "dist/老挝联泰AI指令表-0.5.125.docx",
"source": "agent设计规范/templates/business-input-templates.md",
"builder": "tools/build_business_instruction_docx.py",
"sha256": "a0c2fd5f808e3d3f2ac211a652a59f2e435d3824df49c51f763ca4beb7d9b97d",
"source_sha256": "27600fe9bcb0de3898a99550428a9890dd350c701ba704fb3940ca966088dcd9",
"builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1"
}
]
}

View File

@@ -11,9 +11,14 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.167`。版本化 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.168`。版本化 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.167 当前重点:
0.5.168 当前重点:
- ERP 登录身份只从当前主页面唯一的 `#Lable_UserName` 节点读取,并与平台绑定身份做 Unicode、空白和大小写规范化后的精确全等比较。缺失、重复、空白或仅为子串时都不再执行不再用整页业务文字猜测账号。
- 扩展仍只向平台返回账号是否存在、是否唯一和是否匹配,不回传 ERP 页面显示的账号文本。
继续保留 0.5.167 的空闲证明和受控重载协议:
- 插件 PING 会返回当前运行任务、持久化写入边界和待回查结果汇总后的 `extension_update.safe`。只有内存与持久化状态都完全空闲,服务端才允许同一 ECS 实例进入文件部署。
- 中央服务完成共享目录替换后,平台通过受控消息请求后台 `chrome.runtime.reload()`,并刷新平台页以重新注入 content script执行中、已跨写边界或待回查状态会拒绝重载。

View File

@@ -1,6 +1,6 @@
'use strict';
importScripts('source-region.js', 'operation-plans.js', 'execution-guard.js', 'lifecycle-result.js', 'operation-timing.js');
importScripts('source-region.js', 'operation-plans.js', 'execution-guard.js', 'lifecycle-result.js', 'operation-timing.js', 'erp-account-identity.js');
const runningTasks = new Set();
const executionWorkerKeepalives = new Map();
@@ -9,6 +9,7 @@ const operationPlans = self.LTJTOperationPlans;
const executionGuard = self.LTJTExecutionGuard;
const lifecycleResult = self.LTJTLifecycleResult;
const operationTiming = self.LTJTOperationTiming;
const erpAccountIdentity = self.LTJTErpAccountIdentity;
const EXTENSION_VERSION = chrome.runtime.getManifest().version;
const operationTimingDetailBuffers = new Map();
const erpTabCache = new Map();
@@ -1406,7 +1407,7 @@ async function findOrOpenErpTab(taskId = '') {
}
async function readErpSessionStatus(expectedErpAccount = '') {
const normalizedExpectedAccount = String(expectedErpAccount || '').trim();
const normalizedExpectedAccount = erpAccountIdentity.normalizeErpAccountIdentity(expectedErpAccount);
const permission = await erpHostPermissionStatus();
const keepalive = await readErpKeepaliveState();
const tabs = await chrome.tabs.query({ url: `${ERP_ORIGIN}/*` });
@@ -1446,25 +1447,44 @@ async function readErpSessionStatus(expectedErpAccount = '') {
try {
const [inspection] = await executeErpScript(tab.id, {
target: { tabId: tab.id },
func: (expectedAccount) => {
func: () => {
const bodyText = document.body?.innerText || '';
const accountNodes = Array.from(document.querySelectorAll('#Lable_UserName'));
const accountNode = accountNodes.length === 1 ? accountNodes[0] : null;
return {
url: location.href,
title: document.title,
account_configured: Boolean(expectedAccount),
account_matched: Boolean(expectedAccount) && bodyText.includes(expectedAccount),
observed_account_identity: accountNode?.textContent || '',
account_identity_node_count: accountNodes.length,
login_or_permission_error: Boolean(document.querySelector('input[type="password"]'))
|| /login|无权限|权限不足|permission denied/i.test(`${location.href}\n${document.title}\n${bodyText.slice(0, 400)}`)
};
},
args: [normalizedExpectedAccount]
args: []
});
const inspectionResult = inspection?.result || {
const rawInspectionResult = inspection?.result || {
url: tab.url || '',
title: '',
account_matched: false,
observed_account_identity: '',
account_identity_node_count: 0,
login_or_permission_error: true
};
const {
observed_account_identity: observedAccountIdentity,
...safeInspectionResult
} = rawInspectionResult;
const inspectionResult = {
...safeInspectionResult,
account_configured: Boolean(normalizedExpectedAccount),
account_identity_present: Boolean(
erpAccountIdentity.normalizeErpAccountIdentity(observedAccountIdentity)
),
account_matched: Number(safeInspectionResult.account_identity_node_count) === 1
&& erpAccountIdentity.exactErpAccountIdentityMatch(
normalizedExpectedAccount,
observedAccountIdentity
)
};
const recoveryRequest = (shouldRecoverFromSessionStatus(inspectionResult)
|| keepalive.last_status === 'session_expired')
? requestErpLinkRecovery(

View File

@@ -0,0 +1,26 @@
(function installLTJTErpAccountIdentity(root, factory) {
const api = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = api;
if (root) root.LTJTErpAccountIdentity = api;
}(typeof self !== 'undefined' ? self : globalThis, () => {
'use strict';
function normalizeErpAccountIdentity(value) {
return String(value || '')
.normalize('NFKC')
.replace(/\s+/gu, ' ')
.trim()
.toLocaleLowerCase();
}
function exactErpAccountIdentityMatch(expectedAccount, observedAccount) {
const expected = normalizeErpAccountIdentity(expectedAccount);
const observed = normalizeErpAccountIdentity(observedAccount);
return Boolean(expected && observed && expected === observed);
}
return {
normalizeErpAccountIdentity,
exactErpAccountIdentityMatch
};
}));

View File

@@ -6118,7 +6118,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.167',
version: '0.5.168',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.167",
"version": "0.5.168",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",

View File

@@ -944,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.167',
version: '0.5.168',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,

BIN
dist/ltjt-order-assistant-0.5.168.zip vendored Normal file

Binary file not shown.

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 1,
"generated_on": "2026-09-03",
"generated_on": "2026-09-07",
"baselines": {
"chrome_extension": "0.5.167",
"chrome_extension": "0.5.168",
"skills": "0.5.125",
"business_instruction_docx": "0.5.125",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
@@ -10,10 +10,10 @@
"artifacts": [
{
"kind": "chrome_extension",
"version": "0.5.167",
"path": "dist/ltjt-order-assistant-0.5.167.zip",
"version": "0.5.168",
"path": "dist/ltjt-order-assistant-0.5.168.zip",
"source": "chrome-extension/ltjt-order-assistant",
"sha256": "6822ec660097a8889da8ea4fa22d4091467aa022067bff3bee2ac5406864b679"
"sha256": "2432a882f2fbdcf28bfb6bb8a7fead55986297f86d4cf4ab8e12ea373c72518c"
},
{
"kind": "skill",

View File

@@ -1,7 +1,7 @@
{
"contract_version": "ltjt-lifecycle-v2.9-roster-leader-contact-2026-08",
"current_agent_parse_prompt_version": "ltjt-agent-prompt-v1.8-independent-headcount-categories",
"current_extension_version": "0.5.167",
"current_extension_version": "0.5.168",
"historical_test_marker": "TEST-202609",
"scope": "/System/Business/",
"erp_session_keepalive": {
@@ -31,6 +31,17 @@
"failure_policy": "record structured recovery status and require manual login/inspection; an interrupted MV3 service worker recovery is normalized out of running and the next successful read-only probe returns the recovery state to idle; never retry a business operation"
}
},
"erp_account_identity": {
"source": "the unique #Lable_UserName node in the existing ERP Mainlt.asp document",
"comparison": "NFKC-normalized, whitespace-normalized, case-insensitive exact equality with the platform account's configured ERP identity",
"forbidden_fallbacks": [
"whole-page substring matching",
"matching an expected account against unrelated ERP business text",
"accepting a missing or duplicate login identity node"
],
"privacy": "only matched/present/node-count booleans are returned to the platform; the observed ERP account text remains inside the extension session probe",
"failure_policy": "missing, duplicate, blank, or non-equal ERP identity is not execution-ready"
},
"operation_timing": {
"schema_version": "ltjt-operation-timing-v1",
"scope": "all plugin business executions through the shared result state machine",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://local.lwltapi/schemas/ltjt_form_mapping.schema.json",
"mapping_version": "2026-09-03.shared_plan_create.plan_add.v0.10",
"status": "adaptive_list_entry_readiness_release_gated_0.5.167",
"status": "adaptive_list_entry_readiness_release_gated_0.5.168",
"source": {
"schema": "schemas/standard_system_operation.schema.json",
"action": "shared_plan_create"

View File

@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import test from 'node:test';
const require = createRequire(import.meta.url);
const identity = require('../chrome-extension/ltjt-order-assistant/erp-account-identity.js');
test('ERP account identity comparison accepts only normalized exact equality', () => {
assert.equal(identity.exactErpAccountIdentityMatch('测试AI员工账号', ' 测试ai员工账号 '), true);
assert.equal(identity.exactErpAccountIdentityMatch('-01', 'erp-01'), true);
assert.equal(identity.exactErpAccountIdentityMatch('测试 AI', '测试 ai'), true);
});
test('ERP account identity comparison rejects substrings and blank identities', () => {
assert.equal(identity.exactErpAccountIdentityMatch('测试', '测试ai员工账号'), false);
assert.equal(identity.exactErpAccountIdentityMatch('admin', 'erp-admin-01'), false);
assert.equal(identity.exactErpAccountIdentityMatch('', ''), false);
assert.equal(identity.exactErpAccountIdentityMatch('employee', ''), false);
});
test('ERP session probe reads the unique login label and never matches against whole-page text', () => {
const background = readFileSync(
new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url),
'utf8'
);
assert.match(background, /document\.querySelectorAll\('#Lable_UserName'\)/u);
assert.match(background, /accountNodes\.length === 1/u);
assert.match(background, /exactErpAccountIdentityMatch/u);
assert.doesNotMatch(background, /bodyText\.includes\(expectedAccount\)/u);
});

View File

@@ -1318,7 +1318,7 @@ test('passenger explicit server success is terminal without a post-save row requ
assert.doesNotMatch(passengerBranch, /passengerRequery|verifyLifecycleOperation/);
assert.doesNotMatch(background, /attemptAutomaticPassengerReconciliation/);
assert.match(background, /名单已取得 ERP 明确成功响应,按业务规则确认录入成功/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.167'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.168'/);
});
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
@@ -1525,7 +1525,7 @@ test('lifecycle execution keeps the MV3 worker alive until the durable task sett
assert.match(background, /arrangement_resource_candidate_data_missing/);
});
test('extension 0.5.167 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.168 keeps reload dormant unless its durable execution state is fully idle', async () => {
const [background, bridge] = await Promise.all([
readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'),
readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8')
@@ -1964,7 +1964,7 @@ test('delete guard distinguishes independent, shared child, and shared parent ro
for (const operation of operations) assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; '));
const mapping = JSON.parse(await readFile(new URL('../mappings/lifecycle.mapping.json', import.meta.url), 'utf8'));
assert.equal(mapping.current_extension_version, '0.5.167');
assert.equal(mapping.current_extension_version, '0.5.168');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2333,8 +2333,9 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov
assert.match(background, /if \(report\.manual_review_required === true\) return 'execution_uncertain'/);
assert.match(background, /async function readErpSessionStatus/);
assert.match(background, /message\?\.type === 'LTJT_ERP_SESSION_STATUS'/);
assert.match(background, /account_matched: Boolean\(expectedAccount\) && bodyText\.includes\(expectedAccount\)/);
assert.doesNotMatch(background, /bodyText\.includes\('测试ai员工账号'\)/);
assert.match(background, /document\.querySelectorAll\('#Lable_UserName'\)/);
assert.match(background, /erpAccountIdentity\.exactErpAccountIdentityMatch/);
assert.doesNotMatch(background, /bodyText\.includes\(expectedAccount\)/);
assert.match(background, /lifecycle_preflight_ambiguous/);
assert.match(background, /lifecycle_frame_discovery_retry_exhausted/);
assert.match(background, /lifecycleFrameDiscoveryOnly/);
@@ -2347,10 +2348,10 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov
assert.match(inpage, /`ys_danweiid\$\{index\}`, resolvedCustomerId/);
assert.doesNotMatch(inpage, /product_customer_source_region|sourceRegionCheck|source_reference/);
assert.doesNotMatch(teamBatchInpage, /product_customer_source_region|sourceRegionCheck/);
assert.equal(extensionManifest.version, '0.5.167');
assert.match(inpage, /version: '0\.5\.167'/);
assert.match(teamBatchInpage, /version: '0\.5\.167'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.167'/);
assert.equal(extensionManifest.version, '0.5.168');
assert.match(inpage, /version: '0\.5\.168'/);
assert.match(teamBatchInpage, /version: '0\.5\.168'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.168'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);