修改备注没有带入的问题

This commit is contained in:
andy
2026-09-29 15:43:15 +08:00
parent c2c71d35af
commit 2f0e6582d5
25 changed files with 5104 additions and 30 deletions

View File

@@ -0,0 +1,36 @@
# 酒店新增保留原生备注
## Scope
- 用户授权修复新增酒店时默认备注被空值覆盖:未填写备注保留选中酒店原生备注,明确填写时覆盖;酒店变更继续保留未指定的原记录备注。
- 单条与同团批量共用执行路径。无真实 ERP 写入、部署、重启、扩展重载或外部发送。
## Worktree gate
- Branch: main;worktree: `/Users/andy/IdeaProjects/LWLT-AIBOT`;base: `c2c71d35afbb7c1b9d28bda1b28926a9681414b6`。
- 唯一 worktree;既有改动仅暂存 `.idea/` 文件,与本任务无重叠,保留原状。Overlap: Clear。
- Feature 模式;单智能体;canonical 项目记忆只读。
## Plan
1. 捕获所选酒店原生备注,防止日期/数量回调清空;用户明确备注优先。
2. 将冻结后的备注纳入保存前保护和持久回查证据,旧版历史回查兼容。
3. 增加单条/批量及异常回归,更新插件版本、最低版本、mapping、ZIP 和清单,完成仓库检查。
## Results / Validation
- 完成默认酒店备注捕获与冻结;显式备注覆盖;资源/日期/数量回调不能静默清空待保存备注,其他行与已有酒店变更语义保持。
- 默认备注创建证据 v2 绑定原始操作与备注摘要,保存前 guard 和写后 fresh GET/只读回查验证;缺失或不一致时阻断/不确定,历史 v1 回查兼容。批量按条独立生效。
- 本地控制面 299/299 通过;插件/兼容 452 项中沙箱通过 450 项,另 2 项 HTTP 监听受限,经批准后定向重跑 2/2 通过。类型检查、构建及干净同内容源码副本仓库检查 10/10 通过。
- 原工作目录仅有两项既有卫生检查失败(.idea、.DS_Store、历史日志),未删除或认领;.env 未读取。
- 插件、平台最低版本、mapping、测试与发布清单同步 0.5.191。ZIP 21 文件逐字节匹配源码;原 0.5.190 ZIP 验证原清单哈希后归档。扫描仅清单引用旧 ZIP,无其他活动链接需修复。
- 无新迁移;解析器/Skill/运营模板/DOCX 不变。当前未提交 Git,未部署或真实 ERP 验收。
- [验证证据](../../../archive/evidence/2026-09-29/hotel-native-remark-01a0858b/README.md);[发布清单](../../../dist/release-manifest.json)。
## Follow-ups
- 更新插件至 0.5.191 并同步平台最低版本,刷新平台/ERP 后在授权环境验收原生备注联动;历史已保存空备注不自动回填。
## Promotion Candidates
- 目标:canonical 安排规则。建议登记酒店新增未指定备注继承所选资源默认备注,显式值覆盖,变更保留未指定原记录值;v2 证据用于新增默认备注持久核验。用户已授权方向,本地验证通过,生产真实验收待执行。

View File

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

View File

@@ -2,7 +2,8 @@
## 当前基线
- Chrome 插件:`0.5.190`
- Chrome 插件:`0.5.191`
- `0.5.191` 酒店新增默认备注保留:原生选择后立即冻结备注,用户明确填写时覆盖;保存前保护及持久回查核对,单条/批量共用。旧 v1 回查与已有安排变更保持。仅本地合成回归,真实 ERP 验收待部署方完成;需更新插件/平台最低版本,无迁移。见[验证证据](../../../archive/evidence/2026-09-29/hotel-native-remark-01a0858b/README.md)及[任务记录](../../../.project-docs/30-worklog/tasks/20260929-hotel-native-remark-01a0858b.md)。
- `0.5.190` 同团批量酒店新增:2—10 个完整酒店块先整批验证,串行逐条复用原生保存/回查,首次失败或不确定停止,保留成功前缀和未执行清单;只读核验不自动续写。需同步控制面/平台和插件,无新增迁移;本地合成验证,真实 ERP 验收待部署方完成。见[验证证据](../../../archive/evidence/2026-09-24/hotel-batch-01a0858b/README.md)和[任务记录](../../../.project-docs/30-worklog/tasks/20260924-hotel-batch-01a0858b.md)。
- `0.5.189` 仅修复大交通与其他/备案新增的默认日期空行误判:行日期等于页面有效发团日期且数量为原生空/0/1 时允许进入原有空行校验,资源、说明、备注、费用、付款、审核与独立备案保护仍保留;其他安排、非默认日期和变更/清除不放宽。需更新插件、平台最低版本并刷新 ERP 页面,无服务端逻辑/迁移变更;真实 ERP 验收待部署方完成。见[本地验证](../../../archive/evidence/2026-09-22/arrangement-default-date-01a0b77e/README.md)及[任务记录](../../../.project-docs/30-worklog/tasks/20260922-arrangement-default-date-01a0b77e.md)。
- `0.5.188` 与程序解析器 `v1.0.14` 修复单个下单的产品格式匹配:比较双方统一 NFKC/空白,保留标点及业务限定词,唯一候选使用 ERP 原名联动;keyword 保留输入原始格式。初始失败跳过后续依赖校验,零/多产品候选提供独立中文原因。真实公共预检入口的合成回归通过,生产 ERP 验收待部署方完成;需同步服务端、插件和平台,无迁移。见[验证证据](../../../archive/evidence/2026-09-19/product-name-normalization-01a0b77e/README.md)及[任务记录](../../../.project-docs/30-worklog/tasks/20260919-product-name-normalization-01a0b77e.md)。

View File

@@ -0,0 +1,20 @@
# 酒店新增默认备注本地验证
此目录保存 2026-09-29 修复的不可变本地证据,不定义当前业务规则,也不是运行时输入。
## 结果
- 酒店原生选择后、资源/日期/数量事件前冻结所选行备注;省略备注继承该值,明确填写覆盖。其他行保持,已有酒店变更未填备注保持原记录。
- 默认备注保存前校验,v2 创建证据绑定原操作与备注摘要,fresh GET/只读回查验证持久值;缺失证据或保存后变化不报成功。历史 v1 回查保持原语义。单条与批量逐条均覆盖。
- 控制面 299/299 通过;插件/兼容测试共 452 项,沙箱中 450 项通过,2 项仅因本机监听 `EPERM` 失败,经批准解除该限制后定向重跑 2/2 通过。完整日志和定向结果均保留,没有隐藏失败日志。
- 类型检查、构建通过;同内容干净源码副本仓库检查 10/10 通过,ZIP 的 21 个文件已与当前插件源码逐字节比对。
- 原工作目录的仓库检查有两项既有卫生失败:`.idea/`、`.DS_Store`、历史日志。均保留不动;干净副本只排除这些已有杂项及秘密配置,未读取 `.env`。
- 发布插件/平台最低版本/mapping/ZIP 为 0.5.191。无 Schema、解析器、Skill、运营模板/DOCX 或数据库迁移变更。
- 未访问/写入真实 ERP,未部署、重启、扩展重载或外部发送。生产验收仍需更新后执行。
## 日志
- [控制面](control-plane-tests.txt)
- [插件/兼容完整沙箱结果](legacy-tests-sandbox.txt)、[HTTP 两项定向重跑](http-tests.txt)
- [干净副本仓库检查](repository-clean-tests.txt)、[原目录既有残留](repository-local-residue.txt)
- [类型检查](typecheck.txt)、[构建](build.txt)(成功时无输出)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
▶ business-system HTTP boundary
✔ returns the real AI connectivity probe from /api/status (19.389709ms)
✔ exposes /api/parse and no longer exposes /api/agent routes (4.931792ms)
✔ business-system HTTP boundary (25.077166ms)
ℹ tests 2
ℹ suites 1
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 135.24725

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
TAP version 13
# Subtest: root contains only governed long-lived entries
ok 1 - root contains only governed long-lived entries
---
duration_ms: 92.746625
type: 'test'
...
# Subtest: project docs are the sole active project memory
ok 2 - project docs are the sole active project memory
---
duration_ms: 19.769834
type: 'test'
...
# Subtest: temporary residue and report outputs do not pollute the repository
ok 3 - temporary residue and report outputs do not pollute the repository
---
duration_ms: 338.834166
type: 'test'
...
# Subtest: dist is an exact versioned release set with valid hashes
ok 4 - dist is an exact versioned release set with valid hashes
---
duration_ms: 21.956
type: 'test'
...
# Subtest: extension and release baselines agree
ok 5 - extension and release baselines agree
---
duration_ms: 5.963541
type: 'test'
...
# Subtest: packaged Skills and extension runtime match their project sources
ok 6 - packaged Skills and extension runtime match their project sources
---
duration_ms: 1081.545417
type: 'test'
...
# Subtest: active Markdown links resolve and current docs do not depend on rolling history
ok 7 - active Markdown links resolve and current docs do not depend on rolling history
---
duration_ms: 347.337041
type: 'test'
...
# Subtest: archive README indexes remain navigable
ok 8 - archive README indexes remain navigable
---
duration_ms: 145.497375
type: 'test'
...
# Subtest: single-source and generated-output boundaries remain explicit
ok 9 - single-source and generated-output boundaries remain explicit
---
duration_ms: 39.299333
type: 'test'
...
# Subtest: runtime image includes both LibreOffice document converters
ok 10 - runtime image includes both LibreOffice document converters
---
duration_ms: 1.320125
type: 'test'
...
1..10
# tests 10
# suites 0
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2460.424584

View File

@@ -0,0 +1,132 @@
TAP version 13
# Subtest: root contains only governed long-lived entries
not ok 1 - root contains only governed long-lived entries
---
duration_ms: 5.146542
type: 'test'
location: '/Users/andy/IdeaProjects/LWLT-AIBOT/tools/repository-hygiene.test.mjs:111:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
+ [
+ '.DS_Store',
+ '.idea'
+ ]
- []
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual:
0: '.DS_Store'
1: '.idea'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///Users/andy/IdeaProjects/LWLT-AIBOT/tools/repository-hygiene.test.mjs:114:10)
async Test.run (node:internal/test_runner/test:1054:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
...
# Subtest: project docs are the sole active project memory
ok 2 - project docs are the sole active project memory
---
duration_ms: 3.319083
type: 'test'
...
# Subtest: temporary residue and report outputs do not pollute the repository
not ok 3 - temporary residue and report outputs do not pollute the repository
---
duration_ms: 45.544084
type: 'test'
location: '/Users/andy/IdeaProjects/LWLT-AIBOT/tools/repository-hygiene.test.mjs:149:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
+ [
+ '.DS_Store',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/build.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/control-plane-tests.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/focused-tests.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/legacy-tests.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/repository-clean.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/repository-local-metadata.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/runtime-layout-smoke.log',
+ 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/typecheck.log',
+ 'chrome-extension/.DS_Store'
+ ]
- []
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
actual:
0: '.DS_Store'
1: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/build.log'
2: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/control-plane-tests.log'
3: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/focused-tests.log'
4: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/legacy-tests.log'
5: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/repository-clean.log'
6: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/repository-local-metadata.log'
7: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/runtime-layout-smoke.log'
8: 'archive/evidence/2026-09-14/arrangement-original-date-year-01a0858b/typecheck.log'
9: 'chrome-extension/.DS_Store'
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///Users/andy/IdeaProjects/LWLT-AIBOT/tools/repository-hygiene.test.mjs:153:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: dist is an exact versioned release set with valid hashes
ok 4 - dist is an exact versioned release set with valid hashes
---
duration_ms: 5.366667
type: 'test'
...
# Subtest: extension and release baselines agree
ok 5 - extension and release baselines agree
---
duration_ms: 2.476417
type: 'test'
...
# Subtest: packaged Skills and extension runtime match their project sources
ok 6 - packaged Skills and extension runtime match their project sources
---
duration_ms: 137.733208
type: 'test'
...
# Subtest: active Markdown links resolve and current docs do not depend on rolling history
ok 7 - active Markdown links resolve and current docs do not depend on rolling history
---
duration_ms: 48.791875
type: 'test'
...
# Subtest: archive README indexes remain navigable
ok 8 - archive README indexes remain navigable
---
duration_ms: 27.249208
type: 'test'
...
# Subtest: single-source and generated-output boundaries remain explicit
ok 9 - single-source and generated-output boundaries remain explicit
---
duration_ms: 2.113625
type: 'test'
...
# Subtest: runtime image includes both LibreOffice document converters
ok 10 - runtime image includes both LibreOffice document converters
---
duration_ms: 0.471583
type: 'test'
...
1..10
# tests 10
# suites 0
# pass 8
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 377.6575

View File

@@ -0,0 +1,5 @@
# 2026-09-29 发布归档
此处为历史资料,不定义当前业务规则。
- [酒店默认备注修复前发布物](hotel-native-remark-01a0858b/README.md)

View File

@@ -0,0 +1,3 @@
# 酒店备注修复前发布物
只读历史发布物,不定义当前业务规则。0.5.190 ZIP 在归档前核对原发布清单哈希,原字节保留。当前版本与哈希以 [发布清单](../../../../dist/release-manifest.json) 为准。

View File

@@ -11,9 +11,14 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.190`。版本化 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.191`。版本化 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.190 当前重点:
0.5.191 当前重点:
- 酒店新增未填备注时保留选中酒店原生联动备注,明确填写时使用用户备注;在资源选择后、日期/数量回调前冻结,保存前校验及写后回查均验证。默认备注回查使用 v2 证据,旧 v1 回查保持兼容。单条与批量逐条适用;变更未填备注仍保留原记录。仅本地合成验证,真实 ERP 验收待部署方完成。
- 需更新插件和平台最低版本至 0.5.191 并刷新平台/ERP 页面;无新增数据库迁移。
继续保留 0.5.190 的同团批量酒店新增:
- 同团 2—10 条酒店新增由程序解析为批量信封,插件按输入顺序复用单条原生保存/回查;首次失败或不确定即停止,逐条保存成功前缀、失败/待核验项与未执行项。每条确认成功才进入账号隔离的安排账本和团队进度;整批成功回执包含逐条结果及当前安排。只读回查只核验不确定项,不自动继续剩余安排。禁止跨团、混合动作、批量变更、整批自动重试。
- 需同步更新控制面、平台和插件 0.5.190,无新增数据库迁移。单条输入及原生资源匹配规则保持;本地合成验证通过,真实 ERP 验收待部署方完成。

View File

@@ -6677,7 +6677,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.190',
version: '0.5.191',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,

View File

@@ -651,6 +651,13 @@
return sha256(JSON.stringify([operation.action, operation.data.existing_refs, arrangement]));
}
function usesNativeHotelRemark(operation) {
// Public input only permits a nonempty explicit remark. Resolution also
// normalizes an omitted remark to '', so both forms mean inherit here.
return operation.action === 'arrangement_hotel' && operation.data?.arrangement?.mode === 'create'
&& !normalize(operation.data.arrangement.remark);
}
async function arrangementCreateEvidence(form, operation) {
const rows = [];
for (const index of arrangementRowIndexes(form)) {
@@ -660,7 +667,7 @@
const preserved = createPreservedFields(form, operation);
const preservedProof = await Promise.all(preserved.map(async (field) => ({ name: field.name, date: Boolean(field.date),
sha256: await sha256(field.date ? dateYyyyMD(field.expected) : normalize(field.expected)) })));
return { version: 1, slot_index: operation.data.arrangement.target.slot_index,
return { version: usesNativeHotelRemark(operation) ? 2 : 1, slot_index: operation.data.arrangement.target.slot_index,
operation_key: await arrangementCreateOperationKey(operation), existing_rows: rows,
...(operation.action === 'arrangement_other' ? { preserved_fields: preservedProof } : {}), value_redacted: true };
}
@@ -2318,11 +2325,13 @@
}
const linkedFields = ['danwei0', 'danweiid0', 'fangxing0', 'diqu0', 'dianhua0', 'bizhong0', 'danjia0', 'fangshi0'].map((name) => arrangementFieldName(operation, name));
const before = Object.fromEntries(linkedFields.map((name) => [name, controlValue(getControl(form, name))]));
let nativeRemark;
try {
// SetValToObj is the same ERP-native mapping used by a real hotel-row
// click. It fills the read-only room type and all linked fields.
selectBox.SetVal = setVal;
selectBox.SetValToObj(selected.candidate.raw || selected.candidate.row.join('◆'));
nativeRemark = controlValue(getArrangementControl(form, operation, 'beizhu0'));
dispatchNativeFieldEvents(control);
} catch (error) {
blockers.push(`arrangement_hotel_native_selectbox_failed:${error?.message || String(error)}`);
@@ -2340,7 +2349,7 @@
linked_fields_changed: linkedFields.filter((name) => before[name] !== after[name]),
value_redacted: true
});
return selected;
return { ...selected, nativeRemark };
}
function applyNativeSettlementSelection(form, supplier, changes, blockers, action = 'arrangement_transport', operation = {}) {
@@ -2793,9 +2802,18 @@
const preserved = createPreservedFields(form, operation);
const field = (name) => arrangementFieldName(operation, name);
const action = operation.action;
if (action === 'arrangement_hotel') applyNativeHotelSelection(form, value.resource, changes, blockers, operation);
let hotelSelection;
if (action === 'arrangement_hotel') hotelSelection = applyNativeHotelSelection(form, value.resource, changes, blockers, operation);
else applyNativeSettlementSelection(form, value.supplier, changes, blockers, action, operation);
if (blockers.length) return { changes, blockers, arrangementSlot: slot };
// Freeze the selected resource's note before date/quantity callbacks can
// clear it. Never inherit another existing row or read it back after save.
const nativeRemark = usesNativeHotelRemark(operation)
? hotelSelection.nativeRemark : undefined;
const effectiveRemark = nativeRemark ?? value.remark;
const projectionOperation = nativeRemark === undefined ? operation : {
...operation, data: { ...operation.data, arrangement: { ...value, remark: effectiveRemark } }
};
if (action === 'arrangement_hotel' || action === 'arrangement_vehicle') {
applyExact(form, field('riqi0'), 'set', value.start_date, changes, blockers);
applyExact(form, field('riqis0'), 'set', value.end_date, changes, blockers);
@@ -2820,15 +2838,16 @@
// Native date/room callbacks may reset status and remark. Set and assert
// the requested final values only after those callbacks have settled.
applyExact(form, field('zt0'), 'set', value.status, changes, blockers, { optional: action === 'arrangement_other' });
applyExact(form, field('beizhu0'), 'set', value.remark, changes, blockers);
applyExact(form, field('beizhu0'), 'set', effectiveRemark, changes, blockers);
if (action === 'arrangement_other') {
for (const [key, name] of Object.entries({ number: 'beianhao', date: 'beianri', entry_port: 'rujingkou', exit_port: 'chujingkou' })) {
if (value.filing?.[key] !== undefined) applyExact(form, name, 'set', value.filing[key], changes, blockers);
}
}
const writeGuard = () => arrangementCreateWriteGuard(form, operation, signature, controls, preserved);
const writeGuard = () => arrangementCreateWriteGuard(form, projectionOperation, signature, controls, preserved);
blockers.push(...writeGuard());
return { changes, blockers, arrangementSlot: slot, writeGuard, linkageWait };
return { changes, blockers, arrangementSlot: slot, writeGuard, linkageWait,
...(nativeRemark !== undefined ? { nativeRemarkSha256: await sha256(normalize(nativeRemark)) } : {}) };
}
async function applyArrangement(form, operation) {
@@ -4077,7 +4096,7 @@
async function indexedCreateRequeryProjection(form, operation, proof) {
const blockers = [];
if (!proof || proof.version !== 1 || !Array.isArray(proof.existing_rows)
if (!proof || ![1, 2].includes(proof.version) || !Array.isArray(proof.existing_rows)
|| !Number.isInteger(proof.slot_index) || proof.slot_index < 0
|| proof.operation_key !== await arrangementCreateOperationKey(operation)) {
return { fields: [], blockers: ['arrangement_create_before_evidence_missing_or_mismatched'], operation };
@@ -4097,6 +4116,16 @@
if (newRows.length !== 1) blockers.push(`arrangement_create_new_row_count:${newRows.length}`);
if (blockers.length) return { fields: [], blockers: [...new Set(blockers)], operation };
const projectedOperation = arrangementCreateAtSlot(operation, newRows[0]);
if (proof.version === 2) {
if (!usesNativeHotelRemark(operation) || !/^(?:[a-f0-9]{64}|fallback-[a-f0-9]{1,8})$/.test(proof.native_remark_sha256 || '')) {
return { fields: [], blockers: ['arrangement_create_native_remark_evidence_missing'], operation };
}
const nativeRemark = controlValue(getArrangementControl(form, projectedOperation, 'beizhu0'));
if (await sha256(normalize(nativeRemark)) !== proof.native_remark_sha256) {
return { fields: [], blockers: ['arrangement_create_native_remark_changed_after_save'], operation };
}
projectedOperation.data.arrangement.remark = nativeRemark;
}
const projection = arrangementExpectedFields(form, projectedOperation);
if (operation.action === 'arrangement_other') {
const expectedNames = createPreservedFields(form, projectedOperation).map((field) => field.name);
@@ -4526,6 +4555,9 @@
applyBlockers = applied.blockers;
writeGuard = applied.writeGuard || null;
if (applied.linkageWait) preflight.preflight.arrangement_linkage = applied.linkageWait;
if (applied.nativeRemarkSha256 && preflight.preflight.hotel_create?.version === 2) {
preflight.preflight.hotel_create.native_remark_sha256 = applied.nativeRemarkSha256;
}
} else if (operation.action.startsWith('order_update_')) {
const applied = applyUpdates(form, operation);
const receivable = applyReceivableFixture(form, operation);

View File

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

View File

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

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

Binary file not shown.

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 1,
"generated_on": "2026-09-24",
"generated_on": "2026-09-29",
"baselines": {
"chrome_extension": "0.5.190",
"chrome_extension": "0.5.191",
"skills": "0.5.129",
"business_instruction_docx": "0.5.137",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
@@ -10,10 +10,10 @@
"artifacts": [
{
"kind": "chrome_extension",
"version": "0.5.190",
"path": "dist/ltjt-order-assistant-0.5.190.zip",
"version": "0.5.191",
"path": "dist/ltjt-order-assistant-0.5.191.zip",
"source": "chrome-extension/ltjt-order-assistant",
"sha256": "092838d79cf5864ec3894035b60b6dde5bbeb979c6bf9bce0ad656dc9b7c7f69"
"sha256": "97ea457b40c164b5f015a051b450aed8e3617da950608eb293f18e3ae5e41d69"
},
{
"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.190",
"current_extension_version": "0.5.191",
"original_arrangement_date_selection": "Hotel, vehicle, transport and other/filing update selection accepts MM-DD or YYYY-MM-DD; omitted year matches original ERP month/day within the exact group only. Require exactly one persisted row, then freeze its actual full dates and id. Full year is exact; mixed/cross-year endpoints are matched independently with actual range order checked. No match or ambiguity blocks before mutation; show up to five original candidates and ask to resubmit complete dates. Creation and legacy target-date changes retain existing semantics.",
"historical_test_marker": "TEST-202609",
"scope": "/System/Business/",
@@ -333,6 +333,7 @@
}
},
"arrangement_hotel": {
"create_remark_policy": "0.5.191: omitted/normalized-empty remark inherits the selected native hotel note captured before field callbacks; explicit nonempty remark overrides. Freeze final note for the pre-POST guard. Hotel create proof v2 binds the inherited note hash before POST and verifies it on fresh requery/reconciliation; missing/mismatched proof cannot complete. Historical v1 and existing-row updates retain prior semantics. Applies independently to each batch item.",
"page": "/System/Business/teams_jiudian.asp",
"action": "DoInfo_jiudian",
"create_form_readiness": {

View File

@@ -2,6 +2,12 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { webcrypto } from 'node:crypto';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const plans = require('../chrome-extension/ltjt-order-assistant/operation-plans.js');
const hotelBatch = require('../chrome-extension/ltjt-order-assistant/hotel-batch.js');
const inpage = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8')
@@ -15,7 +21,7 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
teamSummaryRequery = async (operation, subpage) => subpage;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {} } = {}) {
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {}, onFieldEvent = () => {}, crypto = webcrypto } = {}) {
let now = 10000;
let posts = 0;
let selections = 0;
@@ -25,13 +31,13 @@ function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, on
const fields = new Map();
const document = { querySelector: () => form, querySelectorAll: () => [], readyState: 'complete', body: { innerText: '' } };
const location = { href: 'https://erp.invalid/System/Business/teams_jiudian.asp?tdID=123', pathname: '/System/Business/teams_jiudian.asp' };
const window = { document, Event: class {}, location, testValidate: () => onValidate(fields), getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' }) };
const window = { document, crypto, Event: class {}, location, testValidate: () => onValidate(fields), getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' }) };
window.parent = window;
document.defaultView = window;
form.ownerDocument = document;
const add = (name, value) => {
const control = { name, id: name, value: String(value), type: name === 'SubmitButton' ? 'submit' : 'text', tagName: 'INPUT', disabled: false,
ownerDocument: document, dispatchEvent() {},
ownerDocument: document, dispatchEvent() { onFieldEvent(fields, name); },
getAttribute: (key) => key.toLowerCase() === 'data' ? '270◆测试新酒店◆TWN H◆CNY◆120' : key.toLowerCase() === 'setval' ? name : null };
fields.set(name, control); form.elements.push(control);
};
@@ -204,3 +210,100 @@ test('requery cannot report an old row, duplicate new rows, changed existing row
assert.equal(f.posts(), 1);
}
});
test('omitted hotel remarks survive selection/date callbacks, save and read-only requery', async () => {
for (const omitted of [undefined, '']) for (const crypto of [webcrypto, null]) {
const f = fixture({ sort: true, crypto,
onSelection(fields, index) { fields.get(`beizhu${index}`).value = '6月21号起 按照包房12间\n有房损另计'; },
onFieldEvent(fields, name) { if (/^(danwei|riqi|riqis|jianshu)1$/.test(name)) fields.get('beizhu1').value = ''; }
});
if (omitted === undefined) delete f.operation.data.arrangement.remark;
const before = JSON.stringify(f.operation);
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result));
assert.equal(f.fields.get('beizhu1').value, '6月21号起 按照包房12间\n有房损另计');
assert.equal(f.fields.get('beizhu0').value, '保留备注0');
assert.equal(JSON.stringify(f.operation), before);
assert.equal(result.preflight.hotel_create.version, 2);
const checked = result.requery.checks.find(check => check.field === 'beizhu0');
assert.equal(checked.actual, f.fields.get('beizhu1').value);
assert.equal(checked.matched, true);
const verified = await f.api.verifyLifecycleOperation(f.operation, { hotel_create: result.preflight.hotel_create });
assert.equal(verified.status, 'lifecycle_requery_matched', JSON.stringify(verified));
assert.equal(f.posts(), 1);
}
});
test('explicit hotel remark overrides the native default, while missing native notes remain empty', async () => {
const f = fixture({ onSelection(fields, index) { fields.get(`beizhu${index}`).value = '酒店默认备注'; } });
f.operation.data.arrangement.remark = '用户备注:晚到保留';
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result));
assert.equal(f.fields.get('beizhu1').value, '用户备注:晚到保留');
assert.equal(result.preflight.hotel_create.version, 1);
const empty = fixture();
const blank = await empty.api.liveSubmitLifecycleOperation(empty.operation);
assert.equal(blank.status, 'lifecycle_completed', JSON.stringify(blank));
assert.equal(empty.fields.get('beizhu1').value, '');
// Old executions stored v1 and intentionally wrote the omitted note as ''.
const oldProof = { ...blank.preflight.hotel_create, version: 1 };
delete oldProof.native_remark_sha256;
assert.equal((await empty.api.verifyLifecycleOperation(empty.operation, { hotel_create: oldProof })).status, 'lifecycle_requery_matched');
});
test('native remark changes at validation block POST; missing or changed saved evidence cannot report success', async () => {
const selection = (fields, index) => { fields.get(`beizhu${index}`).value = '酒店默认备注'; };
const f = fixture({ onSelection: selection, onValidate(fields) { fields.get('beizhu1').value = ''; } });
const blocked = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(blocked.status, 'lifecycle_apply_blocked', JSON.stringify(blocked));
assert.equal(f.posts(), 0);
const changed = fixture({ onSelection: selection, onPost(form) { form.elements.find(control => control.name === 'beizhu1').value = ''; } });
const uncertain = await changed.api.liveSubmitLifecycleOperation(changed.operation);
assert.equal(uncertain.status, 'lifecycle_execution_uncertain');
assert.ok(uncertain.requery.blockers.includes('arrangement_create_native_remark_changed_after_save'));
const good = fixture({ onSelection: selection });
const result = await good.api.liveSubmitLifecycleOperation(good.operation);
const proof = structuredClone(result.preflight.hotel_create);
delete proof.native_remark_sha256;
assert.equal((await good.api.verifyLifecycleOperation(good.operation, { hotel_create: proof })).status, 'lifecycle_requery_not_matched');
good.savedForm().elements.find(control => control.name === 'beizhu1').value = 'ERP后续变动';
assert.equal((await good.api.verifyLifecycleOperation(good.operation, { hotel_create: result.preflight.hotel_create })).status, 'lifecycle_requery_not_matched');
assert.equal(good.posts(), 1);
});
test('batch orchestration preserves each hotel native or explicit remark through actual lifecycle execution', async () => {
const base = fixture().operation;
const arrangements = [5, 6, 7].map(day => ({ mode: 'create', resource: { name: '测试新酒店TWN', keyword: '测试新酒店TWN' },
start_date: `2026-09-0${day}`, end_date: `2026-09-0${day + 1}`, room_count: 12,
...(day === 6 ? { remark: '用户独立备注' } : {}) }));
const operation = { action: 'arrangement_hotel', order_nature: 'formal', submit_mode: 'dry_run',
data: { existing_refs: { identifier: 'LW-TEST-HOTEL' }, arrangements } };
const before = JSON.stringify(operation);
const results = {}, reports = [];
let final;
const context = { operationPlans: plans, hotelBatch, throwIfTaskCancelled() {},
publishHotelBatch: async (_id, _op, batch) => { final = batch; },
chrome: { storage: { local: { get: async () => ({ businessTaskResults: results }) } } },
executeLifecycleOperation: async (id, child) => {
const index = reports.length;
const f = fixture({ onSelection(fields, slot) { fields.get(`beizhu${slot}`).value = `酒店备注${index}`; },
onFieldEvent(fields, name) { if (/^(riqi|riqis|jianshu)1$/.test(name)) fields.get('beizhu1').value = ''; },
onPost(form) { form.elements.find(control => control.name === 'id1').value = String(999 + index); }
});
f.operation.data.arrangement = { ...base.data.arrangement, ...child.data.arrangement, resource: base.data.arrangement.resource };
const report = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(report.status, 'lifecycle_completed', JSON.stringify(report));
report.requery = { matched: report.requery.matched, subpage: report.requery };
reports.push(report);
results[id] = { status: 'completed', report, write_attempted: true, no_erp_write: false };
}
};
const background = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
vm.createContext(context);
vm.runInContext(background.slice(background.indexOf('async function executeHotelBatch('), background.indexOf('async function runAutoTask(')), context);
await context.executeHotelBatch('TASK', operation);
assert.equal(hotelBatch.inspect(final).allCompleted, true);
assert.deepEqual(reports.map(report => report.requery.subpage.checks.find(check => /^beizhu\d+$/.test(check.field)).actual),
['酒店备注0', '用户独立备注', '酒店备注2']);
assert.equal(JSON.stringify(operation), before);
});

View File

@@ -123,6 +123,19 @@ test('fresh ERP MAIN world resolves the reported 2027 hotel update using the act
assert.equal(f.fields.get('riqis1').value, '2027-9-13');
});
test('hotel replacement preserves the existing note unless the user supplies a new one', async () => {
for (const remark of [undefined, '明确的新备注']) {
const f = fixture({ onSelection(fields, slot) { fields.get(`beizhu${slot}`).value = '新酒店默认备注'; } });
if (remark !== undefined) f.input.data.arrangement.changes.remark = remark;
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
const applied = await f.api.testApply(f.form, resolved.operation);
assert.equal(applied.blockers.length, 0, JSON.stringify(applied));
assert.equal(f.fields.get('beizhu1').value, remark ?? '保留备注1');
assert.equal(f.fields.get('beizhu0').value, '保留备注0');
}
});
test('missing page date dependency is diagnosed before matching and never reported as zero ERP records', () => {
const f = fixture({ omitDateMatcher: true });
const before = [...f.fields].map(([key, control]) => [key, control.value]);

View File

@@ -1374,7 +1374,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\.190'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.191'/);
});
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
@@ -1584,7 +1584,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.190 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.191 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')
@@ -2037,7 +2037,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.190');
assert.equal(mapping.current_extension_version, '0.5.191');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2421,10 +2421,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.190');
assert.match(inpage, /version: '0\.5\.190'/);
assert.match(teamBatchInpage, /version: '0\.5\.190'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.190'/);
assert.equal(extensionManifest.version, '0.5.191');
assert.match(inpage, /version: '0\.5\.191'/);
assert.match(teamBatchInpage, /version: '0\.5\.191'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.191'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);