diff --git a/.project-docs/30-worklog/tasks/20260828-ack-deploy-guide-8c1d.md b/.project-docs/30-worklog/tasks/20260828-ack-deploy-guide-8c1d.md index a284e85..4ee38c7 100644 --- a/.project-docs/30-worklog/tasks/20260828-ack-deploy-guide-8c1d.md +++ b/.project-docs/30-worklog/tasks/20260828-ack-deploy-guide-8c1d.md @@ -16,6 +16,11 @@ - Read-only verification of the existing Docker build, runtime entrypoint, production configuration, persistence, artifact storage, health probes, and ACK/ACR deployment constraints. - Provide a build-and-push command and a minimal middleware inventory; do not deploy, push an image, or read secrets. - Follow up on the production deployment by fixing Chrome extension recognition and bridge injection for the user-confirmed control-plane origin `https://lwlt.nianxx.cn`, then publish a synchronized versioned ZIP. +- Fix the production follow-up where an interrupted Chrome MV3 ERP-link recovery leaves persisted `running`, causing a permanent warning and blocking dispatch; publish a synchronized `0.5.159` ZIP without accessing ERP or mutating tasks. +- Diagnose the Program live-submit blocker reported from production and publish `0.5.160`, which identifies changed protected field names without persisting field values or relaxing the pre-write gate. +- Fix the production-confirmed `xiaoshouren` drift by waiting for native customer, OP and salesperson SelectBox lookup data before team-order preflight, then publish synchronized extension `0.5.161`. +- Follow up on production `0.5.161`, which still drifts `xiaoshouren` and inactive receivable customer/id fields, by waiting for the ERP page's full native jQuery Ajax idle signal and publishing synchronized extension `0.5.162`. +- Follow up on production `0.5.162`, where only numbered dynamic receivable fields still drift, by restoring the effective `0.5.156` protected-field scope at the production operator's explicit request and publishing synchronized extension `0.5.163`. ## Intent And Constraints @@ -25,6 +30,9 @@ - Allow only the exact production control-plane origin; do not broaden content-script injection to arbitrary HTTPS sites. - Do not reload the user's browser extension, deploy services, or access ERP as part of the source/release fix. - Do not create sub-agents. +- Keep the approved protected-field projection only in ERP page memory; persistent diagnostics may contain field names and counts but no field values. Do not retry the failed task. +- Do not remove salesperson protection, silently rewrite it at the live gate, or add a fixed delay; use the ERP page's native lookup readiness signal. +- Through `0.5.162`, preserve corrected dynamic receivable protection and use `jQuery.active/ajaxStop` rather than a fixed quiet sleep. After the production operator explicitly requested the `0.5.156` validation behavior, retain the Ajax stabilization but remove numbered dynamic `ys_*` fields from the protected projection; core order fields remain fail-closed. - Reclaim the same task in Integration mode after the initialized canonical templates caused the Feature-mode drift check to block; the user explicitly approved the forced ownership recovery. ## Outcome @@ -42,6 +50,22 @@ - Archived the superseded `0.5.157` ZIP and manifest snapshot under `archive/releases/2026-08-29/`; generated `dist/ltjt-order-assistant-0.5.158.zip` with SHA-256 `99ba7b29d1d4418cb02fcbb345aeaf8494877dbc397ae8592b4e3fdb68b4cf9d`. - Verified the new archive contains exactly 19 source files and every entry matches the active extension source byte-for-byte. - Repaired Windows-reachable verification gaps encountered by the mandated gates: `.project-docs` is now an allowed governed root, text artifact comparison normalizes CRLF/LF only for explicit text extensions, `ajv` and `ajv-formats` are declared direct dev dependencies, URL fixtures use `fileURLToPath`, and Unix fake-converter success tests are explicitly skipped on Windows while remaining active on Linux/Docker. +- Reproduced the production warning with the real platform state function: `session_ready=true` plus persisted `recovery_status=running` rendered `已连接,有告警` and blocked dispatch. +- Added a behavioral keepalive regression. A real in-memory recovery Promise preserves `running`; a service-worker-interrupted persisted state is normalized to `interrupted`; a later successful read-only keepalive with no recovery in flight sets the recovery state to `idle`. +- Advanced the extension/runtime contract to `0.5.159`, updated the platform cache key, lifecycle mapping, release gate and tests, archived `0.5.158`, and generated `dist/ltjt-order-assistant-0.5.159.zip` with SHA-256 `dcb44f1a6e3336ed1876753c658e664eaf224f55c3d0c792bb2e83b1c8398276`. +- Confirmed from the user's service-worker evidence that the approved preflight form/request protected hashes matched, the live form retained the same 822-field count, and only the live protected hash changed; the blocker remained pre-network with `write_attempted=false` and `no_erp_write=true`. +- Added page-memory-only approved protected projections and structured drift diagnostics containing only changed field names, count, baseline integrity, and `values_redacted=true`; the existing fail-closed blocker and zero-write behavior remain unchanged. +- Fixed the dynamic receivable protection regex so real numeric fields such as `ys_jine0` and `ys_shuliang0` are included in the protected projection. +- Advanced the synchronized extension/runtime contract to `0.5.160`, archived `0.5.159`, and generated `dist/ltjt-order-assistant-0.5.160.zip` with SHA-256 `e331d3d63967ae7278ce0ded35de6210c09555b5e69374c9190e8e7b380c2a70`. +- Production use of `0.5.160` identified `xiaoshouren` as the sole changed protected field. Read-only diagnosis found that the team-order frame probe declares readiness from form element count alone, while the ERP page asynchronously fetches staff data and reinitializes the salesperson SelectBox afterward; the plugin performs no second salesperson write between the approved baseline and live gate. +- A VM harness over the real `lightweightOrderFrameProbe` returned `order_frame_ready` with all three native lookup-data attributes absent, providing a deterministic red signal for the readiness gap. The analogous shared-child flow already waits for `zutuanshe`, `gendanren`, and `xiaoshouren` lookup data. This diagnosis did not change execution code or produce a new release. +- After user authorization, locked that harness into `tools/operation-timing.test.mjs`; it first failed with `actual=order_frame_ready / expected=order_frame_loading`, then passed after both the inline background probe and exported page probe required native `zutuanshe`, `gendanren`, and `xiaoshouren` lookup data. +- Advanced the synchronized extension/runtime contract to `0.5.161`, archived `0.5.160`, and generated `dist/ltjt-order-assistant-0.5.161.zip` with SHA-256 `4a5f680bfe7524f8215ef80b83c681076602e3f59e3a9c7f33005698256809cb`. The archive contains exactly 19 files and matches active extension source byte-for-byte. +- Production handshake confirmed `0.5.161`, but the same route still reported `xiaoshouren` and then `xiaoshouren` plus inactive `ys_danwei*/ys_danweiid*` fields. The user's known-good `0.5.156` comparison showed salesperson protection already existed there, while the old dynamic-receivable regex did not actually match numbered fields. +- A second real-probe VM regression returned `order_frame_ready` with all three lookup data attributes present but two native jQuery Ajax requests still active. The probe now remains loading until `jQuery.active` reaches zero, and preflight waits for the page's native `ajaxStop` after `GetProduct` before applying final business fields. +- Advanced the synchronized extension/runtime contract to `0.5.162`, archived `0.5.161`, and generated `dist/ltjt-order-assistant-0.5.162.zip` with SHA-256 `2b257fd61610e8cb6f21c99441b6072246ce06397e2ea05e96488ef414926f12`. The archive contains exactly 19 files and matches active extension source byte-for-byte. +- Confirmed by archived-source differential that `0.5.156` did not protect real numbered dynamic receivable fields because its regex matched a literal backslash sequence. A deterministic regression first reported `darenshu`, `ys_danwei1`, `ys_danweiid1`, and `ys_jine0`; after restoring the old effective scope it reports only the still-protected `darenshu`. +- Advanced the synchronized extension/runtime contract to `0.5.163`, archived `0.5.162`, and generated `dist/ltjt-order-assistant-0.5.163.zip` with SHA-256 `90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0`. Core protected-field drift still blocks before network; numbered dynamic receivable drift is allowed through the native submit gate by explicit production request. ## Verification @@ -54,11 +78,17 @@ - Cross-checked current ACR build/push, private image pull, ACK ALB/Nginx Ingress, and long-connection guidance against Alibaba Cloud official documentation on 2026-08-28. - The Feature-mode boundary check correctly blocked on initialized canonical templates; after explicit approval, official `release --force` and `start --mode integration --adopt-existing` commands established matching Integration ownership and lock. - No Docker build, image push, Kubernetes mutation, database migration, browser-extension reload, ERP access/write, or deployment was executed. +- The stale-recovery regression failed before the implementation because no state normalizer existed, then passed after the fix; the combined background-normalizer/platform-UI harness now reports `已连接` for an interrupted recovery and retains `已连接,有告警` for a real in-flight recovery. +- `0.5.159` release verification: keepalive tests 6/6; repository hygiene 9/9; TypeScript check; control-plane 123 passed with 4 Windows-only skips; legacy 250/250; build; package/source comparison 19/19; `git diff --check`. The initial legacy rerun exposed only four escaped old-version regex assertions, which were synchronized before the clean rerun. +- `0.5.160` diagnostics verification: focused drift tests 3/3; package/source comparison 19/19; repository hygiene 9/9; TypeScript check; control-plane 123 passed with 4 Windows-only skips; legacy 252/252; build. The first legacy run found one stale `0.5.159` assertion and the PowerShell PATH omitted Git's `unzip`; after synchronizing the assertion and supplying the existing tool path, the clean rerun passed. +- `0.5.161` readiness verification: focused readiness tests 2/2; JavaScript syntax checks; package/source comparison 19/19; repository hygiene 9/9; TypeScript check; control-plane 123 passed with 4 Windows-only skips; legacy 253/253; build. No ERP access/write, failed-task retry, extension reload, service restart, or deployment was performed. +- `0.5.162` verification: the new pending-Ajax probe failed before implementation with `actual=order_frame_ready / expected=order_frame_loading`; afterward operation timing/native-idle tests passed 11/11, drift tests passed 2/2, JavaScript syntax passed, and package/source comparison passed 19/19. Final gates passed: repository hygiene 9/9, TypeScript check, control-plane 123 passed with 4 Windows-only skips, legacy 254/254, and build. +- `0.5.163` verification: the core-scope differential regression failed before implementation with actual changed fields `darenshu, ys_danwei1, ys_danweiid1, ys_jine0` versus expected `darenshu`; afterward focused tests passed 3/3 and Ajax timing tests passed 11/11. Final gates passed: repository hygiene 9/9, TypeScript check, control-plane 123 passed with 4 Windows-only skips, legacy 255/255, build, and release package/source verification. ## Follow-ups - If requested, create task-scoped ACK manifests or a Helm chart containing Namespace, Secret/ConfigMap references, migration Job, single-replica Deployment, ClusterIP Service, Ingress, probes, and ACR pull configuration. -- Load or distribute `dist/ltjt-order-assistant-0.5.158.zip`, then refresh `https://lwlt.nianxx.cn` and confirm the live handshake reports `0.5.158`; runtime reload remains a separate user/operations action. +- Load or distribute `dist/ltjt-order-assistant-0.5.163.zip`, refresh ERP and `https://lwlt.nianxx.cn`, and confirm the live handshake reports `0.5.163`. Then use a new task to verify numbered dynamic receivable drift no longer blocks while core protected-field drift still does; runtime reload and task execution remain separate user/operations actions. - Reconcile stable project facts from the populated root project-memory files into canonical `.project-docs` only through an explicitly scoped architecture-memory integration task. ## Promotion Candidates diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index d7d5c26..70a1846 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -75,7 +75,7 @@ let taskCreateInProgress = false; const AUTO_HANDOFF_RETRY_MS = 30_000; const BROWSER_CONNECTION_ID = `administrator-browser:${location.origin}`; -const REQUIRED_EXTENSION_VERSION = '0.5.158'; +const REQUIRED_EXTENSION_VERSION = '0.5.163'; const MANUAL_HANDOFF_LABEL = '确认并提交到 ERP 插件'; const RETRY_HANDOFF_LABEL = '继续提交到 ERP 插件'; const RECONCILE_LABEL = '只读回查 ERP 现有结果'; diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index 9195a7b..d5c7c05 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -205,6 +205,6 @@ - + diff --git a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md index f709f0f..2bd7815 100644 --- a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md +++ b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md @@ -2,11 +2,11 @@ ## 当前基线 -- Chrome 插件:`0.5.158` +- Chrome 插件:`0.5.163` - 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.123` -- 发布状态:核心窄能力已有真实证据;一笔名单附件 Program-only 任务已把 25 行及唯一领队联系人正确保存到 ERP,后续只读逐字段核验全等。两种写后逐行读取方式都曾在真实成功写入后产生假阴性;名单在全部写前门禁通过后,以父表单 ERP 明确成功响应作为终结证据,不再执行写后逐行回查或自动名单对账。`0.5.155` 规定确认覆盖的指定序号同值也写;`0.5.156` 把原生游客位不足统一映射为包含名单人数和 ERP 实际上限的业务提示,原技术 blocker 仅留技术详情。`0.5.157` 新增散拼母团“整团游客信息”的 `shared_plan + visitor-list + tid-only` 窄分支,静态契约与回归已通过,仍待授权后的新版运行态只读复测。`0.5.158` 把正式控制面 `https://lwlt.nianxx.cn` 同步加入 Popup、后台、Manifest 权限和 content script 精确白名单;其他 HTTPS origin 仍保持阻断。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。 +- 发布状态:核心窄能力已有真实证据;一笔名单附件 Program-only 任务已把 25 行及唯一领队联系人正确保存到 ERP,后续只读逐字段核验全等。两种写后逐行读取方式都曾在真实成功写入后产生假阴性;名单在全部写前门禁通过后,以父表单 ERP 明确成功响应作为终结证据,不再执行写后逐行回查或自动名单对账。`0.5.155` 规定确认覆盖的指定序号同值也写;`0.5.156` 把原生游客位不足统一映射为包含名单人数和 ERP 实际上限的业务提示,原技术 blocker 仅留技术详情。`0.5.157` 新增散拼母团“整团游客信息”的 `shared_plan + visitor-list + tid-only` 窄分支,静态契约与回归已通过,仍待授权后的新版运行态只读复测。`0.5.158` 把正式控制面 `https://lwlt.nianxx.cn` 同步加入 Popup、后台、Manifest 权限和 content script 精确白名单;其他 HTTPS origin 仍保持阻断。`0.5.159` 修复 MV3 后台恢复被中断后持久化 `running` 永久阻断插件派发的问题:仅在内存恢复 Promise 已不存在时收敛为 `interrupted`,真实活动恢复仍保持阻断,下一次成功只读保活把状态归零为 `idle`。`0.5.160` 为独立团单个下单写前漂移增加仅字段名诊断,并把数字后缀动态应收字段纳入保护。`0.5.161` 等待客户、跟单人和销售人原生 lookup 数据就绪后才开始单团预检;生产反馈证明仅检查 lookup 数据仍不足。`0.5.162` 进一步要求页面 jQuery Ajax 在预检前归零,并在 `GetProduct` 后等待原生 `ajaxStop` 再应用最终业务字段。`0.5.163` 按生产操作方明确要求恢复 `0.5.156` 的实际校验范围:核心订单字段继续写前阻断,带编号的动态 `ys_*` 应收行不再纳入保护哈希,允许 ERP 原生回调在预检后调整这些字段。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。 运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。 diff --git a/archive/project-history/2026-08-29/README.md b/archive/project-history/2026-08-29/README.md index edc10bb..16a0a47 100644 --- a/archive/project-history/2026-08-29/README.md +++ b/archive/project-history/2026-08-29/README.md @@ -3,3 +3,4 @@ 本目录保存本日治理压缩前的完整项目记忆,只供追溯,不定义当前规则。活动计划、发现和进度仍以仓库根目录文件为准。 - `task_plan-pre-compression-production-domain.md`:加入正式控制面域名修复任务前的完整 `task_plan.md`,源文件与归档文件 SHA-256 均为 `aceca8635e0e00d5f9ddf6577299d7239ccc8c889d7fec043429d7a5037d3a8c`。 +- `task_plan-pre-compression-erp-recovery-state.md`:加入 ERP 链路恢复状态修复任务前的完整 `task_plan.md`,源文件与归档文件 SHA-256 均为 `7f44217c703ea78cd725040845049dd4eb321735f25170444f50155de5bd2c3e`。 diff --git a/archive/project-history/2026-08-29/task_plan-pre-compression-erp-recovery-state.md b/archive/project-history/2026-08-29/task_plan-pre-compression-erp-recovery-state.md new file mode 100644 index 0000000..026a452 --- /dev/null +++ b/archive/project-history/2026-08-29/task_plan-pre-compression-erp-recovery-state.md @@ -0,0 +1,150 @@ +# 项目文件治理计划 + +## 当前任务:正式控制面域名接入 Chrome 插件(已完成) + +- **目标:** 修复扩展 `0.5.157` 只识别本地控制面、导致 `https://lwlt.nianxx.cn` 显示“未打开业务系统页面”的问题,并发布 `0.5.158`。 +- **安全边界:** 只修改源码、测试、版本和本地发布物;精确允许该生产 origin,不放宽到其他 HTTPS 站点;不重载浏览器扩展、不部署、不访问或写入 ERP。 + +### 阶段与结论 + +- [x] 用 Popup 黑盒把活动标签固定为正式域名,稳定复现正式域名拒绝而 localhost 识别的红灯。 +- [x] 同步 Popup URL 判定、后台标签发现、Manifest host permission/content script,并保留相似域名和其他站点阻断。 +- [x] 扩展、平台最低版本和 lifecycle mapping 升至 `0.5.158`;归档 `0.5.157`,重建版本化 ZIP 与发布清单。 +- [x] ZIP 19/19 文件与源码逐字节一致,SHA-256 为 `99ba7b29d1d4418cb02fcbb345aeaf8494877dbc397ae8592b4e3fdb68b4cf9d`。 +- [x] 修复 Windows 门禁的三项既有可达问题:`.project-docs` 根白名单、文本 CRLF/LF 比较、测试直接依赖与路径/Unix 假转换器边界。 + +### 错误记录 + +- 仓库最初没有安装依赖;锁文件安装需精确批准 `argon2/esbuild` 构建。批准只作用于本地依赖,临时生成的 `pnpm-workspace.yaml` 已移除。 +- 首轮全量门禁暴露 `ajv/ajv-formats` 未声明为直接测试依赖、Windows URL pathname 重复盘符以及 Unix `.sh` 假转换器不可执行;均在测试/依赖边界收敛,未改生产转换逻辑。 + +压缩前完整旧计划见 [2026-08-29 快照](archive/project-history/2026-08-29/task_plan-pre-compression-production-domain.md)。 + +## 当前任务:将运行态切换到整团游客信息新版(已完成) + +- **目标:** 按用户明确授权,优雅重启标准 8786 控制面并重载本地 Chrome 扩展,使 Program parser `v1.0.6`、输入契约 `0.5.123` 与扩展 `0.5.157` 在运行态生效。 +- **安全边界:** 仅做进程/扩展运行态切换和只读健康、版本检查;不创建、确认、重试或派发任务,不访问 ERP 业务数据,不执行 ERP 写入、部署或外发。 + +### 阶段 + +- [x] 确认 8786 live/ready、数据库/Schema/迁移与 4/4 AgentBus 渠道正常,并确认活动任务和 `accepted/running` 在途 attempt 均为 0。 +- [x] 优雅停止旧进程组并从项目标准 `pnpm run dev` 入口启动新控制面。 +- [x] 核验 live/ready、唯一监听、解析器/输入契约版本,并重载扩展后确认 `0.5.157` 握手。 + +### 错误记录 + +- 旧监听停止后首次启动命令未显式传入 bundled Node PATH,`pnpm` 在运行迁移前即因 `node: command not found` 退出;没有数据库或任务变更。已改为在启动进程环境中显式设置 bundled Node/fallback PATH。 +- 旧标准入口在 TERM 后由其会话管理器自动拉起新进程组 `62152`;因此第二次手工启动完成迁移检查后在绑定前以 `EADDRINUSE` 安全退出。该临时实例的 AgentBus listeners 已自行关闭,in-flight tasks 与 pending replies 均为 0;保留自动拉起的新实例作为唯一标准运行态。 +- 收敛记录后的首轮五道门禁由 bundled Node 主进程启动,但其子脚本 PATH 中没有 `node`,所以均在进入检查/测试前退出;显式加入同一 bundled Node 目录后原样重跑并全部通过。 + +### 完成结论 + +- 标准 `pnpm run dev` 进程组 `62152`、服务 PID `62164` 是 8786 的唯一监听;live/ready、数据库、Schema、迁移 `014_task_input_attachments` 与 4/4 AgentBus 渠道均正常。 +- 已通过已登录操作台的只读 API 确认 `confirmation_export` 运行在 `program` 模式,版本为 `ltjt-program-parser-v1.0.6`,输入契约为 `business-input-templates-0.5.123`。 +- 已重载精确匹配的本地“联泰下单助手”扩展;操作台握手为插件 `0.5.157` / 最低版本 `0.5.157` / 兼容,桥接已连接、ERP 会话正常、最近错误为无。诊断字段“ERP 链路恢复”仍显示 `failed`,本轮未扩大授权去访问 ERP 业务页复测。 +- 切换前后活动任务、在途 task 与 `accepted/running` attempt 均为 0;未创建、补充、确认、重试、删除或派发任务,未访问 ERP 业务数据、执行 ERP 写入、部署或外发。 +- 运行态切换后规定门禁再次通过:仓库治理 9/9、TypeScript、控制面 127/127、legacy 248/248、build 与 `git diff --check`。 + +## 当前任务:散拼母团“整团游客信息”导出接入(已完成) + +- **目标:** 保留统一 `confirmation_export` action,在 `文件类型:整团游客信息` 时明确锁定 `shared_plan`,只用母团 `tid` 调用 `orders_Visitor.asp`;现有独立团和散拼具体子单“游客名单”继续使用 `did+tid`,不得混淆范围。 +- **安全边界:** 本轮只修改源码、契约、测试和本地发布物;不访问 ERP,不创建、补充、重试或派发真实任务,不重载扩展、重启、部署或外发。 + +### 阶段 + +- [x] 先补解析、Schema、执行计划和页内导出的失败回归,固定母团 `tid-only` 与子单/独立团 `did+tid` 的分流。 +- [x] 修改 Program parser、解析态/执行态契约、ERP 唯一解析与导出适配器,并保持母团只允许该窄文件类型。 +- [x] 同步运营模板、`lwlt-confirmation` Skill、业务登记、mapping、平台校验与版本。 +- [x] 重建运营 DOCX、Skill 包、Chrome ZIP 和发布清单,逐文件/逐页核验。 +- [x] 运行规定全量门禁并收敛 planning files;运行态切换仍等待另行授权。 + +### 已确定的设计 + +- 历史只读 ERP 证据确认两者共用 `/System/Business/orders_Visitor.asp`,但散拼母团使用 `tid`,独立团/散拼子单使用 `did+tid`。 +- 不新增业务 action;用 `existing_refs.kind` 表达对象范围。`shared_plan` 只对单一 `visitor-list` 开放,不把“全部”或依赖子单 `did` 的其他文件类型放宽到母团。 +- 当前 `program_invalid_value` 属于安全停止,但用户提示应由“不识别文件类型”改为可执行的窄能力或真正的对象定位问题。 + +### 错误记录 + +- 测试先行阶段的四组定向回归均按预期红灯:Program 仍返回 `agent_parse_needs_input`;Agent/插件 strict gate 仍拒绝 `shared_plan`;执行计划仍固定 `did+tid`;页内 exporter 尚无母团分支。这些红灯证明缺口完整覆盖,不属于环境或测试基础设施故障。 +- 核心实现后四组回归中仅一条旧测试仍要求 blocker 包含“母团”;新规则已不再一概禁止母团,但该无类型、未唯一解析的请求仍安全阻断。测试已改为断言真实缺失项,并补充对象范围契约断言。 +- bundled Python 运行官方 Skill 校验器时缺少 `PyYAML`,在生成任何新包前即停止;本机 Python 已确认具备 `PyYAML 6.0.3`,后续用它运行同一个官方校验器。 +- 首次组合定向命令有两项非行为性失败:直接用 Node 执行 TypeScript 测试未加载 `tsx`,以及 lifecycle 中另一个旧位置仍断言平台最低版本 `0.5.156`。前者改用项目规定的 `--import tsx`,后者同步为 `0.5.157`。 +- 首次 `check:repo` 为 8/9;唯一失败是治理测试仍固定运营模板版本 `0.5.122`。已同步为新 DOCX 单一源基线 `0.5.123`,其余目录、哈希、包/源码和链接检查均已通过。 +- 首次 `test:control-plane` 为 126/127;唯一失败是操作台测试仍固定旧 `app.js` 缓存键。已同步为 `20260828-shared-plan-visitor-export-1`;全部业务/解析测试(含新增整团游客信息用例)本轮均已通过。 +- 最后一组补充语法检查首次未显式带 bundled Node PATH,因当前非登录 shell 找不到 `node` 而未启动;随后改用 bundled Node 绝对路径重跑,不影响已通过的五道规定门禁。 + +### 完成结论 + +- `整团游客信息`现在确定性解析为 `confirmation_export / shared_plan / visitor-list`;母团唯一解析后只生成 `orders_Visitor.asp?tid=`,且解析态、执行态、插件门禁和页内 exporter 都拒绝其他类型、混合范围及任何子单 `did/ddid`。普通“游客名单”的独立团/具体子单 `did+tid` 路径未改变。 +- 发布基线已推进为扩展 `0.5.157`、Program parser `v1.0.6`、输入契约/DOCX `0.5.123`、五 Skill `0.5.125`。扩展 ZIP SHA-256 为 `872640668c52cf1ef0135ff51ec33e3c1f5dee304434fbb58ceee4cb3518fcd7`;DOCX 12/12 页视觉检查通过,Skill 与扩展包均逐文件匹配源码。 +- 门禁通过:仓库治理 9/9、TypeScript、控制面 127/127、legacy 248/248、build、JS/JSON/ZIP/Skill、DOCX 与 `git diff --check`。该任务完成时未访问 ERP、处理历史任务、重载扩展、重启服务、部署或外发;后续运行态切换已按用户另行授权完成,见本文件顶部任务。 + +## 当前任务:名单人数超过 ERP 容量时使用业务提示(已完成) + +- **目标:** `passenger_list_import` 在 ERP 原生游客位少于名单所需人数时,不再向用户显示 `passenger_native_*` 等技术码;明确提示本次名单人数和该订单的 ERP 可录入上限。 +- **安全边界:** 原技术 blocker 继续保留在技术详情中;仍在父表单保存前失败关闭并保持 `no_erp_write=true`。不修改或重试历史任务,不访问 ERP,不重载扩展、重启或部署。 + +### 阶段 + +- [x] 先补回归,固定“16 个游客位 / 25 人名单”的业务错误码、中文提示和技术详情保留规则。 +- [x] 修改插件错误映射,并同步版本、平台最低版本、mapping、业务登记与发布门槛。 +- [x] 生成版本化 ZIP、更新发布清单并运行规定全量门禁。 + +### 错误记录 + +- 新回归先按预期红灯,证明旧后台没有容量业务映射;实现后转绿。一次组合门禁因 PATH 只作用于首条命令而未启动后续 Node,改为在受控 shell 内导出 bundled Node PATH 后完整通过。 + +### 完成结论 + +- `passenger_native_target_row_count_not_reached:16:25` 及 DOM 同类码现在归并为 `erp_passenger_count_limit_exceeded`,用户看到“本次名单共 25 人,超过该独立团在 ERP 中最多可录入的 16 人”;原始 blocker 仍在技术详情。 +- 容量失败仍发生在父表单保存前,保持 `no_erp_write=true`。扩展、平台最低版本和 mapping 已推进到 `0.5.156`;新 ZIP SHA-256 为 `9a0d4941728a49a40a8734c560b4dfcb3f5026756ffb46af3f32f38fbbac652a`,旧 `0.5.155` ZIP/清单已归档。 +- 门禁通过:仓库治理 9/9、TypeScript、控制面 126/126、legacy 246/246、build、JS/JSON/ZIP 与 diff 检查。未处理历史任务、访问 ERP、重载扩展、重启或部署。 + +## 当前任务:名单覆盖确认后始终覆写(已完成) + +- **目标:** `passenger_list_import` 收到 `full_replace + confirmed=true` 后,不再根据 ERP 当前游客值判定“无变化”;附件中所有指定序号都执行原生覆写和父表单保存,ERP 明确成功即完成。 +- **安全边界:** 未确认时,任一目标序号已占用(包括同值)仍要求覆盖确认;继续保留附件/Program-only、唯一对象、结构、游客与领队写前投影及 ERP 明确成功响应门禁。不处理历史任务,不访问 ERP,不重载扩展或重启服务。 + +### 阶段 + +- [x] 先补失败回归:确认覆盖的同值行必须进入 changed/write 集合;未确认的同值占用行仍要求确认。 +- [x] 修改合并计划、版本和业务契约,确认覆盖不再产生 `passenger_import_no_changes`。 +- [x] 扩展、平台最低版本和 mapping 推进到 `0.5.155`;旧 `0.5.154` ZIP/清单已归档,新 ZIP 已生成。 +- [x] 运行规定全量门禁并收敛发布 SHA、进度和完成结论。 + +### 错误记录 + +- 首次补充“未确认同值占用行”回归时,一个多文件补丁因上下文未精确命中而整体未应用;拆成精确小补丁后完成,没有产生部分修改。 +- 首轮 `check:repo` 发现根 `task_plan.md` 为 17304 字节,超过 16 KiB。已先完整冻结到 [2026-08-28 计划快照](archive/project-history/2026-08-28/task_plan-pre-compression-confirmed-overwrite.md),再语义压缩本文件;历史未丢失。 + +### 完成结论 + +- 合并计划先区分授权而非“值是否相同”:未确认且目标序号已占用时统一要求覆盖确认;`full_replace + confirmed=true` 后,全部附件指定序号都进入 changed/write 集合,同值也执行 `DaoRuDones`、写前完整投影和父表单保存。 +- 父表单 ERP 明确成功即完成,仍不执行写后逐行名单回查;其他 lifecycle action 的回查规则未改变。 +- 当前扩展为 `0.5.155`,ZIP SHA-256 为 `3159e6700fcb9aea6d18099c5c84bccf2578de6bceaf3b7e2f369ec44ff66574`;`0.5.154` ZIP 与清单已日期化归档。 +- 门禁通过:仓库治理 9/9、TypeScript、控制面 125/125、legacy 245/245、build、JS/JSON/ZIP 与 `git diff --check`。未处理历史任务、访问 ERP、重载扩展、重启或部署。 + +## 最近完成:名单明确成功响应终结与 8786 重启 + +- `passenger_list_import` 在全部写前门禁通过后,只调用一次父表单原生保存;ERP 明确成功即生成 `passenger_list_explicit_server_success` 完成凭据,不执行写后逐行回查或自动名单对账。 +- 控制面只对名单 action 接受上述专用凭据,其他 lifecycle action 仍要求真实 fresh requery。 +- 用户授权后,旧 8786 进程组 `13643` 已优雅退出;新标准入口进程组 `29571`、服务 PID `29588` 成为唯一监听,live/ready、数据库、Schema、迁移 014 与 4/4 AgentBus 渠道正常。 +- 该次运行态仍是 `0.5.154` 最低版本;本次 `0.5.155` 尚未重载扩展或重启服务。 + +## 最近有效里程碑 + +- 两类名单业务固定为 Program-only:先以文字识别业务并等待单个 `.xls/.xlsx`,附件到齐后才解析;第一行忽略、第二行固定 14 列表头,规范化为 13 列 canonical TSV。 +- 独立团 16 行、散拼子单 31 行只是初始合并基线,5000 行是技术上限;未指定行保留,已占用目标行需要覆盖确认。 +- 唯一严格`领队`备注行生成 `passenger_list.leader_contact`,并在父表单保存前投影到 `jj_lianxiren/jj_dianhua`;多行、缺姓名/电话或结构不一致均失败关闭。 +- 当前运营模板单一源为 `agent设计规范/templates/business-input-templates.md`;运营 DOCX、五 Skill 及其发布物保持各自现有基线,本次只改变 Chrome 执行策略。 + +## 相邻待处理项 + +- AgentBus OSS 附件曾因本机 DNS 返回私网/保留地址而被 SSRF 门禁拒绝;与本次平台手工附件和名单覆写规则独立。后续若处理,应采用受控公有 DNS/主机白名单回退,不能全局放宽私网阻断。 + +## 历史与持续边界 + +- 本次压缩前完整计划见 [2026-08-28 快照](archive/project-history/2026-08-28/task_plan-pre-compression-confirmed-overwrite.md);更早快照见同目录 README 和 `archive/project-history/`,均只供追溯,不定义当前规则。 +- 当前发布版本、文件名和 SHA-256 只以 [发布清单](dist/release-manifest.json) 为准。 +- 真实 ERP 写入、任务 mutation、扩展重载、服务重启、部署和外部发送仍需用户另行明确授权。 diff --git a/archive/releases/2026-08-29/README.md b/archive/releases/2026-08-29/README.md index 24e7673..c6ae7e0 100644 --- a/archive/releases/2026-08-29/README.md +++ b/archive/releases/2026-08-29/README.md @@ -4,3 +4,11 @@ - `release-manifest-0.5.157.json`:`0.5.157 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 - `ltjt-order-assistant-0.5.157.zip`:被 `0.5.158` 的正式控制面 `https://lwlt.nianxx.cn` 精确域名白名单支持替代;保留用于恢复和追溯。 +- `release-manifest-0.5.158.json`:`0.5.158 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 +- `ltjt-order-assistant-0.5.158.zip`:被 `0.5.159` 的 ERP 链路恢复状态收敛修复替代;保留用于恢复和追溯。 +- `release-manifest-0.5.159.json`:`0.5.159 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 +- `ltjt-order-assistant-0.5.159.zip`:被 `0.5.160` 的单团写前漂移字段名诊断与动态应收字段保护修复替代;保留用于恢复和追溯。 +- `release-manifest-0.5.160.json`:`0.5.160 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 +- `ltjt-order-assistant-0.5.160.zip`:被 `0.5.161` 的单团客户/员工原生 lookup readiness 门禁替代;保留用于恢复和追溯。 +- `release-manifest-0.5.161.json`:`0.5.161 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 +- `ltjt-order-assistant-0.5.161.zip`:被 `0.5.162` 的单团页面原生 Ajax 稳定态门禁替代;保留用于恢复和追溯。 diff --git a/dist/ltjt-order-assistant-0.5.158.zip b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.158.zip similarity index 100% rename from dist/ltjt-order-assistant-0.5.158.zip rename to archive/releases/2026-08-29/ltjt-order-assistant-0.5.158.zip diff --git a/archive/releases/2026-08-29/ltjt-order-assistant-0.5.159.zip b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.159.zip new file mode 100644 index 0000000..3242316 Binary files /dev/null and b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.159.zip differ diff --git a/archive/releases/2026-08-29/ltjt-order-assistant-0.5.160.zip b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.160.zip new file mode 100644 index 0000000..48ae4aa Binary files /dev/null and b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.160.zip differ diff --git a/archive/releases/2026-08-29/ltjt-order-assistant-0.5.161.zip b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.161.zip new file mode 100644 index 0000000..f449afb Binary files /dev/null and b/archive/releases/2026-08-29/ltjt-order-assistant-0.5.161.zip differ diff --git a/archive/releases/2026-08-29/release-manifest-0.5.158.json b/archive/releases/2026-08-29/release-manifest-0.5.158.json new file mode 100644 index 0000000..27a651c --- /dev/null +++ b/archive/releases/2026-08-29/release-manifest-0.5.158.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-29", + "baselines": { + "chrome_extension": "0.5.158", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.158", + "path": "dist/ltjt-order-assistant-0.5.158.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "99ba7b29d1d4418cb02fcbb345aeaf8494877dbc397ae8592b4e3fdb68b4cf9d" + }, + { + "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": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "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.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/archive/releases/2026-08-29/release-manifest-0.5.159.json b/archive/releases/2026-08-29/release-manifest-0.5.159.json new file mode 100644 index 0000000..4bdd10e --- /dev/null +++ b/archive/releases/2026-08-29/release-manifest-0.5.159.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-29", + "baselines": { + "chrome_extension": "0.5.159", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.159", + "path": "dist/ltjt-order-assistant-0.5.159.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "dcb44f1a6e3336ed1876753c658e664eaf224f55c3d0c792bb2e83b1c8398276" + }, + { + "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": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "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.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/archive/releases/2026-08-29/release-manifest-0.5.160.json b/archive/releases/2026-08-29/release-manifest-0.5.160.json new file mode 100644 index 0000000..392da51 --- /dev/null +++ b/archive/releases/2026-08-29/release-manifest-0.5.160.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-29", + "baselines": { + "chrome_extension": "0.5.160", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.160", + "path": "dist/ltjt-order-assistant-0.5.160.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "e331d3d63967ae7278ce0ded35de6210c09555b5e69374c9190e8e7b380c2a70" + }, + { + "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": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "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.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/archive/releases/2026-08-29/release-manifest-0.5.161.json b/archive/releases/2026-08-29/release-manifest-0.5.161.json new file mode 100644 index 0000000..81a33ae --- /dev/null +++ b/archive/releases/2026-08-29/release-manifest-0.5.161.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-29", + "baselines": { + "chrome_extension": "0.5.161", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.161", + "path": "dist/ltjt-order-assistant-0.5.161.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "4a5f680bfe7524f8215ef80b83c681076602e3f59e3a9c7f33005698256809cb" + }, + { + "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": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "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.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/archive/releases/2026-08-30/README.md b/archive/releases/2026-08-30/README.md new file mode 100644 index 0000000..3b70547 --- /dev/null +++ b/archive/releases/2026-08-30/README.md @@ -0,0 +1,6 @@ +# 2026-08-30 发布归档 + +本目录只保存被新版本替代的历史发布物,不定义当前运行规则。当前制品文件名和 SHA-256 只以根目录 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json) 为准。 + +- `release-manifest-0.5.162.json`:`0.5.162 / 0.5.125 / 0.5.123` 交付基线的历史清单快照。 +- `ltjt-order-assistant-0.5.162.zip`:被 `0.5.163` 的单团写前保护范围调整替代;保留用于恢复和追溯。`0.5.163` 保留原生 Ajax 稳定态处理,但按生产操作方明确要求把带编号的动态 `ys_*` 应收字段移出保护哈希,恢复 `0.5.156` 的实际校验范围。 diff --git a/archive/releases/2026-08-30/ltjt-order-assistant-0.5.162.zip b/archive/releases/2026-08-30/ltjt-order-assistant-0.5.162.zip new file mode 100644 index 0000000..1985141 Binary files /dev/null and b/archive/releases/2026-08-30/ltjt-order-assistant-0.5.162.zip differ diff --git a/archive/releases/2026-08-30/release-manifest-0.5.162.json b/archive/releases/2026-08-30/release-manifest-0.5.162.json new file mode 100644 index 0000000..e47ed1c --- /dev/null +++ b/archive/releases/2026-08-30/release-manifest-0.5.162.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-29", + "baselines": { + "chrome_extension": "0.5.162", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.162", + "path": "dist/ltjt-order-assistant-0.5.162.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "2b257fd61610e8cb6f21c99441b6072246ce06397e2ea05e96488ef414926f12" + }, + { + "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": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "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.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/chrome-extension/ltjt-order-assistant/README.md b/chrome-extension/ltjt-order-assistant/README.md index 05734e5..779374f 100644 --- a/chrome-extension/ltjt-order-assistant/README.md +++ b/chrome-extension/ltjt-order-assistant/README.md @@ -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 站点仍不允许注入。 diff --git a/chrome-extension/ltjt-order-assistant/background.js b/chrome-extension/ltjt-order-assistant/background.js index 4c042fb..98426fd 100644 --- a/chrome-extension/ltjt-order-assistant/background.js +++ b/chrome-extension/ltjt-order-assistant/background.js @@ -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 }); } diff --git a/chrome-extension/ltjt-order-assistant/inpage.js b/chrome-extension/ltjt-order-assistant/inpage.js index e3abf28..570f13d 100644 --- a/chrome-extension/ltjt-order-assistant/inpage.js +++ b/chrome-extension/ltjt-order-assistant/inpage.js @@ -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); }, diff --git a/chrome-extension/ltjt-order-assistant/manifest.json b/chrome-extension/ltjt-order-assistant/manifest.json index eb345e5..a5a80c5 100644 --- a/chrome-extension/ltjt-order-assistant/manifest.json +++ b/chrome-extension/ltjt-order-assistant/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "联泰下单助手", - "version": "0.5.158", + "version": "0.5.163", "description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。", "permissions": [ "activeTab", diff --git a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js index f26c3af..ebc9030 100644 --- a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js +++ b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js @@ -919,7 +919,7 @@ window.LTJTOrderAssistant = { ...(window.LTJTOrderAssistant || {}), - version: '0.5.158', + version: '0.5.163', openTeamBatchForm, pingTeamBatchFrame, preflightTeamBatchNative, diff --git a/control-plane/test/control-plane.test.ts b/control-plane/test/control-plane.test.ts index d00ae8e..fcb2ebb 100644 --- a/control-plane/test/control-plane.test.ts +++ b/control-plane/test/control-plane.test.ts @@ -1177,7 +1177,7 @@ test('operator page has a login gate and uses the durable task API', async () => assert.match(index, /id="loginPanel"/); assert.match(index, /id="workbench"[^>]*hidden/); assert.match(index, /styles\.css\?v=20260827-roster-attachment-1/); - assert.match(index, /app\.js\?v=20260828-shared-plan-visitor-export-1/); + assert.match(index, /app\.js\?v=20260830-team-order-validation-scope-1/); assert.match(index, /id="statusDetailsPopover"/); assert.match(index, /id="statusDetailsRefresh"/); assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/); diff --git a/dist/ltjt-order-assistant-0.5.163.zip b/dist/ltjt-order-assistant-0.5.163.zip new file mode 100644 index 0000000..41f07cc Binary files /dev/null and b/dist/ltjt-order-assistant-0.5.163.zip differ diff --git a/dist/release-manifest.json b/dist/release-manifest.json index 27a651c..70234d5 100644 --- a/dist/release-manifest.json +++ b/dist/release-manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 1, - "generated_on": "2026-08-29", + "generated_on": "2026-08-30", "baselines": { - "chrome_extension": "0.5.158", + "chrome_extension": "0.5.163", "skills": "0.5.125", "business_instruction_docx": "0.5.123", "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" @@ -10,10 +10,10 @@ "artifacts": [ { "kind": "chrome_extension", - "version": "0.5.158", - "path": "dist/ltjt-order-assistant-0.5.158.zip", + "version": "0.5.163", + "path": "dist/ltjt-order-assistant-0.5.163.zip", "source": "chrome-extension/ltjt-order-assistant", - "sha256": "99ba7b29d1d4418cb02fcbb345aeaf8494877dbc397ae8592b4e3fdb68b4cf9d" + "sha256": "90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0" }, { "kind": "skill", diff --git a/findings.md b/findings.md index caaf384..ef98496 100644 --- a/findings.md +++ b/findings.md @@ -1,5 +1,32 @@ # 当前关键发现 +## 2026-08-30 — 0.5.156 校验范围恢复 + +- 用户明确要求恢复 `0.5.156` 的校验方式。归档 ZIP 证明该版本的动态正则写成字面 `\\d*`,所以真实编号 `ys_danwei1/ys_danweiid1/ys_jine0` 等字段并未进入保护;`0.5.160` 修正数字后缀后才新增当前强校验。 +- `0.5.163` 不整体降级:保留正式控制面域名、MV3 链路恢复、lookup readiness 和 `jQuery.active/ajaxStop` 稳定态,只移除编号动态 `ys_*` 的保护投影。日期、客户、产品、人数、房型、跟单人、销售人等静态核心字段继续按摘要漂移失败关闭。 +- 完整 live-gate 回归确认:核心字段变化仍返回 `live_submit_blocked` 且零网络;仅编号动态应收字段变化时,表单和原生请求的保护摘要均匹配并进入原生提交路径。该策略允许 ERP 在预检后调整动态应收行,属于生产操作方明确接受的行为边界。 +- `0.5.163` ZIP SHA-256 为 `90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0`;专项、全量和构建门禁均通过。生产仍需加载后以新任务验证,旧失败任务不重试。 + +## 2026-08-29 — Program 写前受保护字段漂移 + +- `0.5.160` 生产复测已把实际漂移字段定位为 `xiaoshouren`(销售人),仍在网络请求前阻断。活动源码确认单团页面只按 `ListForm` 控件数达到 800 判定就绪,但 ERP 页面会异步请求 `ProDanwei_Yuangong`,并在回调中两次重新初始化 `xiaoshouren`;插件写入预检销售人后到 live gate 之间没有第二次插件写入。 +- 真实 `lightweightOrderFrameProbe` 的离线反馈环稳定证明:`zutuanshe/gendanren/xiaoshouren` 的原生 SelectBox 数据仍为空时,当前实现仍错误返回 `order_frame_ready`。散拼子单路径已等待这三个 lookup 的 `data` 属性,单团路径没有,最可能因此允许 ERP 员工列表回调在预检后改写销售人。由于没有生产页面回调时间戳,这一因果关系属于强证据推断而非直接观测。 +- `0.5.161` 虽已等待客户、跟单人和销售人 SelectBox 的 `data`,生产握手确认该版本后仍先后报告 `xiaoshouren`,以及 `xiaoshouren + ys_danwei*/ys_danweiid*` 漂移。这证明 lookup 数据存在不等于页面所有原生 Ajax 已完成;ERP 表单启动阶段还有收款项目、报位、说明和资源等多组 jQuery Ajax。 +- 用户确认生产 `0.5.156` 没有该阻断。ZIP 差分确认 `0.5.156` 已保护 `xiaoshouren`,但数字后缀正则错误使 `ys_danwei1` 等动态应收字段当时未进入保护;应收行是 `0.5.160` 修正保护范围后新暴露的真实迟到变化。随后用户明确接受该范围调整风险并要求恢复旧校验效果,当前结论见上方 `0.5.163` 记录。 +- 真实 frame probe 的第二个离线反馈环证明:三个 lookup 数据均存在、但 `jQuery.active=2` 时,`0.5.161` 仍错误返回 `order_frame_ready`。`0.5.162` 同时在 frame readiness 要求原生 Ajax 归零,并在 `GetProduct` 外层完成后等待页面自身 `ajaxStop`,再应用最终销售人和应收字段;只使用完成事件和失败超时,不使用固定 sleep。 +- 用户提供的服务工作线程状态证明:预检最终表单与其拦截请求的受保护字段 SHA-256 都是 `fe3cfc2e72e0e8fafad548717a71012228d63b5f5a10ee20a5ff526e1f12c8e5`,实时提交的已批准摘要仍是该值,但当前摘要变为 `fa42ee152b1e4d1920d2f5b4329b8dfe1884900a087c932b3dfb8736d05c1276`;前后字段数均为 822。 +- 因字段数相同且预检前后摘要一致,这不是错误基线或表单字段总数变化,而是预检结束到真实提交之间至少一个受保护字段的名称/值投影发生了漂移。`0.5.159` 只持久化聚合摘要,无法从既有日志反推出具体字段。 +- `0.5.160` 仅在 ERP 页面运行内存保存预检受保护字段投影。写前不一致时回执只包含变化字段名、数量、基线可用性和 `values_redacted=true`,不持久化或输出任何字段值,也不放宽阻断条件。 +- 新回归发现动态应收保护规则原先使用双重转义,实际匹配字面 `\\d`,导致 `ys_jine0` 等带数字后缀的真实字段未进入保护投影;已改为数字后缀匹配并由行为测试覆盖。 +- `0.5.162` ZIP 与 19 个活动源码文件逐字节一致,SHA-256 为 `2b257fd61610e8cb6f21c99441b6072246ce06397e2ea05e96488ef414926f12`;定向回归、仓库治理、TypeScript、控制面、legacy 与 build 均通过。生产运行态仍需运维加载该版本后用新任务验证;不重试旧失败任务。 + +## 2026-08-29 — ERP 链路恢复状态残留 + +- 截图中的插件版本、权限、自动化和 ERP 会话均正常;唯一告警来源是持久化 `recovery_status=running`。操作台把 `scheduled/already_running/running` 视为恢复中,并阻断自动与手工插件派发。 +- MV3 服务工作线程可能在恢复写入 `running` 后被中断;内存 Promise 随工作线程消失,但旧实现没有区分“真实活动恢复”和“只剩持久化状态”,普通保活成功也不清除 `recovery_status`,因此告警可永久残留。 +- `0.5.159` 只在内存恢复 Promise 已不存在时把 `running` 归一化为 `interrupted`;真实活动恢复仍保持 `running`。普通只读保活成功且无恢复在途时把恢复状态转为 `idle`,不会重放任何业务任务。 +- 同一真实页面状态函数的反馈环确认:无在途 Promise 时页面恢复“已连接”,有在途 Promise 时继续“已连接,有告警”。 + ## 2026-08-29 — 正式控制面域名插件接入 - 扩展 `0.5.157` 的 Popup 与后台业务系统 URL 判定只允许本地 HTTP,Manifest 的控制面 content script 匹配也只有 localhost/127.0.0.1;当前 ZIP 与源码一致,因此刷新或重新加载旧包不能识别 `https://lwlt.nianxx.cn`。 diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json index 853ed78..335a0cd 100644 --- a/mappings/lifecycle.mapping.json +++ b/mappings/lifecycle.mapping.json @@ -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.158", + "current_extension_version": "0.5.163", "historical_test_marker": "TEST-202609", "scope": "/System/Business/", "erp_session_keepalive": { @@ -28,7 +28,7 @@ "cooldown_minutes": 2, "max_attempts_per_cooldown": 1, "action": "reload the existing allowlisted ERP tab with bypassCache=true, wait for complete, then repeat the read-only GET probe", - "failure_policy": "record structured recovery status and require manual login/inspection; never retry a business operation" + "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" } }, "operation_timing": { diff --git a/progress.md b/progress.md index aacfa99..85a897d 100644 --- a/progress.md +++ b/progress.md @@ -1,5 +1,38 @@ # 项目进度 +## 2026-08-30 — 恢复 0.5.156 单团写前校验范围(已完成) + +- 已确认当前阻断来自 `0.5.160` 新增的编号动态 `ys_*` 保护,而 `0.5.156` 实际只校验静态核心订单字段。按生产操作方明确要求,`0.5.163` 恢复旧范围,但保留 `0.5.162` 的 jQuery Ajax 稳定态、正式域名和链路恢复修复。 +- 回归先红后绿:核心字段变化仍阻断;仅 `ys_danwei1/ys_danweiid1/ys_jine0` 等编号动态应收字段变化时不再产生受保护字段 blocker。专项 3/3、原生 Ajax 稳定态 11/11 通过。 +- `dist/ltjt-order-assistant-0.5.163.zip` 已生成,SHA-256 为 `90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0`。仓库治理 9/9、TypeScript、控制面 123 通过/4 个 Windows 预期跳过、legacy 255/255 和 build 均通过。 + +## 2026-08-29 — Program 单团原生 Ajax 稳定态修复(已完成) + +- 生产实时握手确认 `0.5.161` 已加载,但仍出现 `xiaoshouren` 及零人数应收行 `ys_danwei*/ys_danweiid*` 漂移;用户确认 `0.5.156` 无同类阻断。版本差分证明旧版已保护销售人,但动态应收数字后缀当时因正则错误未进入保护。 +- 新反馈环先稳定复现 lookup 数据齐全、`jQuery.active=2` 时仍错误返回 ready;修复后 frame 保持 loading,`GetProduct` 后继续等待原生 `ajaxStop` 再应用最终业务字段。定向计时/稳定态 11/11、漂移 2/2 通过。 +- `dist/ltjt-order-assistant-0.5.162.zip` 已生成并与 19 个源码文件逐字节一致,SHA-256 为 `2b257fd61610e8cb6f21c99441b6072246ce06397e2ea05e96488ef414926f12`。仓库治理 9/9、TypeScript、控制面 123 通过/4 个 Windows 预期跳过、legacy 254/254 和 build 均通过。 + +## 2026-08-29 — 0.5.161 lookup readiness(生产复测未通过) + +- 生产加载 `0.5.160` 后报告唯一漂移字段 `xiaoshouren`。真实 probe 回归先稳定复现 lookup 数据为空仍返回 ready,再让 `0.5.161` 只有在 `zutuanshe/gendanren/xiaoshouren` 原生 SelectBox 数据均就绪后进入预检,红灯原样转绿。 +- `dist/ltjt-order-assistant-0.5.161.zip` 已生成并与 19 个源码文件逐字节一致,SHA-256 为 `4a5f680bfe7524f8215ef80b83c681076602e3f59e3a9c7f33005698256809cb`;`0.5.160` ZIP/清单已归档。 +- 本轮门禁通过:定向 readiness 2/2、仓库治理 9/9、TypeScript、控制面 123 通过/4 个 Windows 预期跳过、legacy 253/253、build。未访问 ERP、重试任务、重载扩展、重启服务或部署。 +- 后续生产握手确认 `0.5.161` 已加载,但同一业务仍出现销售人及动态应收结算单位漂移;该版本仅检查三个 lookup 数据,不能作为最终稳定态版本。 +- 已根据用户提供的服务工作线程证据确认:预检表单/请求摘要一致,实时提交字段数仍为 822,但当前受保护字段摘要发生变化;旧任务在网络请求前安全阻断,未尝试 ERP 写入。 +- 扩展 `0.5.160` 在预检后仅把受保护字段投影保留于 ERP 页面内存,漂移回执只暴露字段名和数量并标记 `values_redacted=true`;原有 fail-closed 写前门禁和零写入行为保持不变。 +- 新增行为回归覆盖日期规范化、普通受保护字段、动态应收字段和真实 `liveSubmitApproved` 阻断路径;同时修正 `ys_jine0` 等数字后缀字段未被保护的正则双转义问题。 +- 扩展、平台最低版本、mapping、发布门槛、缓存键和测试已同步到 `0.5.160`;`0.5.159` ZIP/清单已归档。 +- `dist/ltjt-order-assistant-0.5.160.zip` 共 19 个文件并与源码逐字节一致,SHA-256 为 `e331d3d63967ae7278ce0ded35de6210c09555b5e69374c9190e8e7b380c2a70`。 +- 定向回归 3/3、仓库治理 9/9、TypeScript、控制面 123 通过/4 个 Windows 预期跳过、legacy 252/252 和 build 全部通过。未访问 ERP、重试任务、重载扩展、重启服务或部署。 + +## 2026-08-29 — ERP 链路恢复状态残留(已完成) + +- 已先用真实平台状态函数复现“ERP 会话正常但 recovery running 仍告警并阻断派发”,再补活动/中断恢复的最小失败回归并完成红→绿。 +- 后台现在保留真实在途 `running`,把失去内存 Promise 的持久化 `running` 收敛为 `interrupted`;后续普通只读保活成功转为 `idle`。 +- 扩展、平台最低版本、mapping、发布门槛和缓存键已同步到 `0.5.159`;`0.5.158` ZIP/清单已归档。 +- `dist/ltjt-order-assistant-0.5.159.zip` 共 19 个文件并与源码一致,SHA-256 为 `dcb44f1a6e3336ed1876753c658e664eaf224f55c3d0c792bb2e83b1c8398276`。 +- 定向回归 6/6、仓库治理 9/9、TypeScript、控制面 123 passed/4 Windows skips、legacy 250/250 和 build 已通过;最终文档门禁记录在活动任务中。未访问 ERP、重放任务、重载扩展、重启服务或部署。 + ## 2026-08-29 — 正式控制面域名插件接入(已完成) - 已用真实 Popup 代码复现 `https://lwlt.nianxx.cn` 被 `0.5.157` 判定为“未打开业务系统页面”,并将该路径固化为回归测试。 diff --git a/task_plan.md b/task_plan.md index 026a452..a457322 100644 --- a/task_plan.md +++ b/task_plan.md @@ -1,150 +1,46 @@ # 项目文件治理计划 -## 当前任务:正式控制面域名接入 Chrome 插件(已完成) +## 当前任务:恢复 0.5.156 单团写前校验范围(已完成) -- **目标:** 修复扩展 `0.5.157` 只识别本地控制面、导致 `https://lwlt.nianxx.cn` 显示“未打开业务系统页面”的问题,并发布 `0.5.158`。 -- **安全边界:** 只修改源码、测试、版本和本地发布物;精确允许该生产 origin,不放宽到其他 HTTPS 站点;不重载浏览器扩展、不部署、不访问或写入 ERP。 +- **目标:** 按生产操作方明确要求,保留现有正式域名、链路恢复与 Ajax 稳定态修复,只把独立团单个下单的受保护字段范围恢复为 `0.5.156` 的实际效果,并发布扩展 `0.5.163`。 +- **已确认事实:** `0.5.162` 已消除 `xiaoshouren` 漂移,但仍阻断编号动态应收单位字段。ZIP 差分证明 `0.5.156` 的动态 `ys_*` 正则匹配的是字面 `\\d*`,真实编号字段当时没有进入保护;`0.5.160` 修正为数字后缀后才新增这层强校验。 +- **安全边界:** 核心订单字段继续写前阻断;仅编号动态 `ys_*` 应收字段恢复旧范围。诊断不保存字段值;不重试失败任务、不访问或写入 ERP、不重载扩展、不重启服务、不部署。 ### 阶段与结论 -- [x] 用 Popup 黑盒把活动标签固定为正式域名,稳定复现正式域名拒绝而 localhost 识别的红灯。 -- [x] 同步 Popup URL 判定、后台标签发现、Manifest host permission/content script,并保留相似域名和其他站点阻断。 -- [x] 扩展、平台最低版本和 lifecycle mapping 升至 `0.5.158`;归档 `0.5.157`,重建版本化 ZIP 与发布清单。 -- [x] ZIP 19/19 文件与源码逐字节一致,SHA-256 为 `99ba7b29d1d4418cb02fcbb345aeaf8494877dbc397ae8592b4e3fdb68b4cf9d`。 -- [x] 修复 Windows 门禁的三项既有可达问题:`.project-docs` 根白名单、文本 CRLF/LF 比较、测试直接依赖与路径/Unix 假转换器边界。 +- [x] 从归档 `0.5.156` ZIP 读取真实校验实现,确认编号动态 `ys_*` 当时未进入保护。 +- [x] 新回归先稳定失败:当前实际报告 `darenshu, ys_danwei1, ys_danweiid1, ys_jine0`,目标旧范围只报告 `darenshu`。 +- [x] 最小化移除动态 `ys_*` 保护分支,保留所有静态核心字段、页面内存基线和字段名诊断。 +- [x] 增加完整 live-gate 回归:核心字段变化仍在网络前阻断;仅编号应收字段变化时通过保护哈希并进入原生提交路径。 +- [x] 同步扩展、平台最低版本、mapping、发布门槛、缓存键和回归到 `0.5.163`。 +- [x] 归档 `0.5.162`,生成 `dist/ltjt-order-assistant-0.5.163.zip`;19/19 文件与源码一致,SHA-256 为 `90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0`。 +- [x] 专项回归 3/3、原生 Ajax 稳定态 11/11、仓库治理 9/9、TypeScript、控制面 123 通过/4 个 Windows 预期跳过、legacy 255/255 和构建通过。 ### 错误记录 -- 仓库最初没有安装依赖;锁文件安装需精确批准 `argon2/esbuild` 构建。批准只作用于本地依赖,临时生成的 `pnpm-workspace.yaml` 已移除。 -- 首轮全量门禁暴露 `ajv/ajv-formats` 未声明为直接测试依赖、Windows URL pathname 重复盘符以及 Unix `.sh` 假转换器不可执行;均在测试/依赖边界收敛,未改生产转换逻辑。 +- 首次全量门禁只有一处旧版断言仍要求平台最低版本 `0.5.162`;同步为 `0.5.163` 后干净重跑全部通过。首次 PowerShell 串行命令曾让最终 build 退出码覆盖中间测试失败,重跑时已对每个命令单独检查退出码。 +- 完整 live-gate 回归首次通过后被插件自身 30 秒恢复定时器拖住;测试末尾显式清理该定时器后,反馈环恢复为约 1 秒且保持 3/3 通过。 -压缩前完整旧计划见 [2026-08-29 快照](archive/project-history/2026-08-29/task_plan-pre-compression-production-domain.md)。 +## 最近完成:正式控制面域名插件接入 -## 当前任务:将运行态切换到整团游客信息新版(已完成) +- `0.5.158` 将精确 origin `https://lwlt.nianxx.cn` 同步到 Popup、后台、Manifest host permission 和 content script;相似域名、子域名及其他 HTTPS 站点继续拒绝。 +- 旧 `0.5.157` 与对应清单已归档;本次 `0.5.159` 又将 `0.5.158` 日期化归档。 -- **目标:** 按用户明确授权,优雅重启标准 8786 控制面并重载本地 Chrome 扩展,使 Program parser `v1.0.6`、输入契约 `0.5.123` 与扩展 `0.5.157` 在运行态生效。 -- **安全边界:** 仅做进程/扩展运行态切换和只读健康、版本检查;不创建、确认、重试或派发任务,不访问 ERP 业务数据,不执行 ERP 写入、部署或外发。 +## 最近有效业务基线 -### 阶段 +- Program parser `ltjt-program-parser-v1.0.6`;输入契约与运营 DOCX `0.5.123`;五个 Skill `0.5.125`;生命周期契约 `v2.9`。 +- 两类名单固定 Program-only;未确认占用位要求确认,确认后附件指定序号全部覆写;父表单 ERP 明确成功即完成。 +- 散拼母团“整团游客信息”使用 `confirmation_export / shared_plan / visitor-list / tid-only`;独立团和散拼具体子单继续使用 `did+tid`。 +- 新增独立团房型/人数映射仍待授权 ERP 实写;未验证宽能力继续失败关闭。 -- [x] 确认 8786 live/ready、数据库/Schema/迁移与 4/4 AgentBus 渠道正常,并确认活动任务和 `accepted/running` 在途 attempt 均为 0。 -- [x] 优雅停止旧进程组并从项目标准 `pnpm run dev` 入口启动新控制面。 -- [x] 核验 live/ready、唯一监听、解析器/输入契约版本,并重载扩展后确认 `0.5.157` 握手。 +## 持续边界 -### 错误记录 +- 当前版本、文件名和 SHA-256 只以 [发布清单](dist/release-manifest.json) 为准。 +- 真实 ERP 访问或写入、任务 mutation、扩展重载、服务重启、部署和外部发送仍需本轮另行明确授权。 +- `archive/` 只供追溯,不定义当前规则。 -- 旧监听停止后首次启动命令未显式传入 bundled Node PATH,`pnpm` 在运行迁移前即因 `node: command not found` 退出;没有数据库或任务变更。已改为在启动进程环境中显式设置 bundled Node/fallback PATH。 -- 旧标准入口在 TERM 后由其会话管理器自动拉起新进程组 `62152`;因此第二次手工启动完成迁移检查后在绑定前以 `EADDRINUSE` 安全退出。该临时实例的 AgentBus listeners 已自行关闭,in-flight tasks 与 pending replies 均为 0;保留自动拉起的新实例作为唯一标准运行态。 -- 收敛记录后的首轮五道门禁由 bundled Node 主进程启动,但其子脚本 PATH 中没有 `node`,所以均在进入检查/测试前退出;显式加入同一 bundled Node 目录后原样重跑并全部通过。 +## 历史快照 -### 完成结论 - -- 标准 `pnpm run dev` 进程组 `62152`、服务 PID `62164` 是 8786 的唯一监听;live/ready、数据库、Schema、迁移 `014_task_input_attachments` 与 4/4 AgentBus 渠道均正常。 -- 已通过已登录操作台的只读 API 确认 `confirmation_export` 运行在 `program` 模式,版本为 `ltjt-program-parser-v1.0.6`,输入契约为 `business-input-templates-0.5.123`。 -- 已重载精确匹配的本地“联泰下单助手”扩展;操作台握手为插件 `0.5.157` / 最低版本 `0.5.157` / 兼容,桥接已连接、ERP 会话正常、最近错误为无。诊断字段“ERP 链路恢复”仍显示 `failed`,本轮未扩大授权去访问 ERP 业务页复测。 -- 切换前后活动任务、在途 task 与 `accepted/running` attempt 均为 0;未创建、补充、确认、重试、删除或派发任务,未访问 ERP 业务数据、执行 ERP 写入、部署或外发。 -- 运行态切换后规定门禁再次通过:仓库治理 9/9、TypeScript、控制面 127/127、legacy 248/248、build 与 `git diff --check`。 - -## 当前任务:散拼母团“整团游客信息”导出接入(已完成) - -- **目标:** 保留统一 `confirmation_export` action,在 `文件类型:整团游客信息` 时明确锁定 `shared_plan`,只用母团 `tid` 调用 `orders_Visitor.asp`;现有独立团和散拼具体子单“游客名单”继续使用 `did+tid`,不得混淆范围。 -- **安全边界:** 本轮只修改源码、契约、测试和本地发布物;不访问 ERP,不创建、补充、重试或派发真实任务,不重载扩展、重启、部署或外发。 - -### 阶段 - -- [x] 先补解析、Schema、执行计划和页内导出的失败回归,固定母团 `tid-only` 与子单/独立团 `did+tid` 的分流。 -- [x] 修改 Program parser、解析态/执行态契约、ERP 唯一解析与导出适配器,并保持母团只允许该窄文件类型。 -- [x] 同步运营模板、`lwlt-confirmation` Skill、业务登记、mapping、平台校验与版本。 -- [x] 重建运营 DOCX、Skill 包、Chrome ZIP 和发布清单,逐文件/逐页核验。 -- [x] 运行规定全量门禁并收敛 planning files;运行态切换仍等待另行授权。 - -### 已确定的设计 - -- 历史只读 ERP 证据确认两者共用 `/System/Business/orders_Visitor.asp`,但散拼母团使用 `tid`,独立团/散拼子单使用 `did+tid`。 -- 不新增业务 action;用 `existing_refs.kind` 表达对象范围。`shared_plan` 只对单一 `visitor-list` 开放,不把“全部”或依赖子单 `did` 的其他文件类型放宽到母团。 -- 当前 `program_invalid_value` 属于安全停止,但用户提示应由“不识别文件类型”改为可执行的窄能力或真正的对象定位问题。 - -### 错误记录 - -- 测试先行阶段的四组定向回归均按预期红灯:Program 仍返回 `agent_parse_needs_input`;Agent/插件 strict gate 仍拒绝 `shared_plan`;执行计划仍固定 `did+tid`;页内 exporter 尚无母团分支。这些红灯证明缺口完整覆盖,不属于环境或测试基础设施故障。 -- 核心实现后四组回归中仅一条旧测试仍要求 blocker 包含“母团”;新规则已不再一概禁止母团,但该无类型、未唯一解析的请求仍安全阻断。测试已改为断言真实缺失项,并补充对象范围契约断言。 -- bundled Python 运行官方 Skill 校验器时缺少 `PyYAML`,在生成任何新包前即停止;本机 Python 已确认具备 `PyYAML 6.0.3`,后续用它运行同一个官方校验器。 -- 首次组合定向命令有两项非行为性失败:直接用 Node 执行 TypeScript 测试未加载 `tsx`,以及 lifecycle 中另一个旧位置仍断言平台最低版本 `0.5.156`。前者改用项目规定的 `--import tsx`,后者同步为 `0.5.157`。 -- 首次 `check:repo` 为 8/9;唯一失败是治理测试仍固定运营模板版本 `0.5.122`。已同步为新 DOCX 单一源基线 `0.5.123`,其余目录、哈希、包/源码和链接检查均已通过。 -- 首次 `test:control-plane` 为 126/127;唯一失败是操作台测试仍固定旧 `app.js` 缓存键。已同步为 `20260828-shared-plan-visitor-export-1`;全部业务/解析测试(含新增整团游客信息用例)本轮均已通过。 -- 最后一组补充语法检查首次未显式带 bundled Node PATH,因当前非登录 shell 找不到 `node` 而未启动;随后改用 bundled Node 绝对路径重跑,不影响已通过的五道规定门禁。 - -### 完成结论 - -- `整团游客信息`现在确定性解析为 `confirmation_export / shared_plan / visitor-list`;母团唯一解析后只生成 `orders_Visitor.asp?tid=`,且解析态、执行态、插件门禁和页内 exporter 都拒绝其他类型、混合范围及任何子单 `did/ddid`。普通“游客名单”的独立团/具体子单 `did+tid` 路径未改变。 -- 发布基线已推进为扩展 `0.5.157`、Program parser `v1.0.6`、输入契约/DOCX `0.5.123`、五 Skill `0.5.125`。扩展 ZIP SHA-256 为 `872640668c52cf1ef0135ff51ec33e3c1f5dee304434fbb58ceee4cb3518fcd7`;DOCX 12/12 页视觉检查通过,Skill 与扩展包均逐文件匹配源码。 -- 门禁通过:仓库治理 9/9、TypeScript、控制面 127/127、legacy 248/248、build、JS/JSON/ZIP/Skill、DOCX 与 `git diff --check`。该任务完成时未访问 ERP、处理历史任务、重载扩展、重启服务、部署或外发;后续运行态切换已按用户另行授权完成,见本文件顶部任务。 - -## 当前任务:名单人数超过 ERP 容量时使用业务提示(已完成) - -- **目标:** `passenger_list_import` 在 ERP 原生游客位少于名单所需人数时,不再向用户显示 `passenger_native_*` 等技术码;明确提示本次名单人数和该订单的 ERP 可录入上限。 -- **安全边界:** 原技术 blocker 继续保留在技术详情中;仍在父表单保存前失败关闭并保持 `no_erp_write=true`。不修改或重试历史任务,不访问 ERP,不重载扩展、重启或部署。 - -### 阶段 - -- [x] 先补回归,固定“16 个游客位 / 25 人名单”的业务错误码、中文提示和技术详情保留规则。 -- [x] 修改插件错误映射,并同步版本、平台最低版本、mapping、业务登记与发布门槛。 -- [x] 生成版本化 ZIP、更新发布清单并运行规定全量门禁。 - -### 错误记录 - -- 新回归先按预期红灯,证明旧后台没有容量业务映射;实现后转绿。一次组合门禁因 PATH 只作用于首条命令而未启动后续 Node,改为在受控 shell 内导出 bundled Node PATH 后完整通过。 - -### 完成结论 - -- `passenger_native_target_row_count_not_reached:16:25` 及 DOM 同类码现在归并为 `erp_passenger_count_limit_exceeded`,用户看到“本次名单共 25 人,超过该独立团在 ERP 中最多可录入的 16 人”;原始 blocker 仍在技术详情。 -- 容量失败仍发生在父表单保存前,保持 `no_erp_write=true`。扩展、平台最低版本和 mapping 已推进到 `0.5.156`;新 ZIP SHA-256 为 `9a0d4941728a49a40a8734c560b4dfcb3f5026756ffb46af3f32f38fbbac652a`,旧 `0.5.155` ZIP/清单已归档。 -- 门禁通过:仓库治理 9/9、TypeScript、控制面 126/126、legacy 246/246、build、JS/JSON/ZIP 与 diff 检查。未处理历史任务、访问 ERP、重载扩展、重启或部署。 - -## 当前任务:名单覆盖确认后始终覆写(已完成) - -- **目标:** `passenger_list_import` 收到 `full_replace + confirmed=true` 后,不再根据 ERP 当前游客值判定“无变化”;附件中所有指定序号都执行原生覆写和父表单保存,ERP 明确成功即完成。 -- **安全边界:** 未确认时,任一目标序号已占用(包括同值)仍要求覆盖确认;继续保留附件/Program-only、唯一对象、结构、游客与领队写前投影及 ERP 明确成功响应门禁。不处理历史任务,不访问 ERP,不重载扩展或重启服务。 - -### 阶段 - -- [x] 先补失败回归:确认覆盖的同值行必须进入 changed/write 集合;未确认的同值占用行仍要求确认。 -- [x] 修改合并计划、版本和业务契约,确认覆盖不再产生 `passenger_import_no_changes`。 -- [x] 扩展、平台最低版本和 mapping 推进到 `0.5.155`;旧 `0.5.154` ZIP/清单已归档,新 ZIP 已生成。 -- [x] 运行规定全量门禁并收敛发布 SHA、进度和完成结论。 - -### 错误记录 - -- 首次补充“未确认同值占用行”回归时,一个多文件补丁因上下文未精确命中而整体未应用;拆成精确小补丁后完成,没有产生部分修改。 -- 首轮 `check:repo` 发现根 `task_plan.md` 为 17304 字节,超过 16 KiB。已先完整冻结到 [2026-08-28 计划快照](archive/project-history/2026-08-28/task_plan-pre-compression-confirmed-overwrite.md),再语义压缩本文件;历史未丢失。 - -### 完成结论 - -- 合并计划先区分授权而非“值是否相同”:未确认且目标序号已占用时统一要求覆盖确认;`full_replace + confirmed=true` 后,全部附件指定序号都进入 changed/write 集合,同值也执行 `DaoRuDones`、写前完整投影和父表单保存。 -- 父表单 ERP 明确成功即完成,仍不执行写后逐行名单回查;其他 lifecycle action 的回查规则未改变。 -- 当前扩展为 `0.5.155`,ZIP SHA-256 为 `3159e6700fcb9aea6d18099c5c84bccf2578de6bceaf3b7e2f369ec44ff66574`;`0.5.154` ZIP 与清单已日期化归档。 -- 门禁通过:仓库治理 9/9、TypeScript、控制面 125/125、legacy 245/245、build、JS/JSON/ZIP 与 `git diff --check`。未处理历史任务、访问 ERP、重载扩展、重启或部署。 - -## 最近完成:名单明确成功响应终结与 8786 重启 - -- `passenger_list_import` 在全部写前门禁通过后,只调用一次父表单原生保存;ERP 明确成功即生成 `passenger_list_explicit_server_success` 完成凭据,不执行写后逐行回查或自动名单对账。 -- 控制面只对名单 action 接受上述专用凭据,其他 lifecycle action 仍要求真实 fresh requery。 -- 用户授权后,旧 8786 进程组 `13643` 已优雅退出;新标准入口进程组 `29571`、服务 PID `29588` 成为唯一监听,live/ready、数据库、Schema、迁移 014 与 4/4 AgentBus 渠道正常。 -- 该次运行态仍是 `0.5.154` 最低版本;本次 `0.5.155` 尚未重载扩展或重启服务。 - -## 最近有效里程碑 - -- 两类名单业务固定为 Program-only:先以文字识别业务并等待单个 `.xls/.xlsx`,附件到齐后才解析;第一行忽略、第二行固定 14 列表头,规范化为 13 列 canonical TSV。 -- 独立团 16 行、散拼子单 31 行只是初始合并基线,5000 行是技术上限;未指定行保留,已占用目标行需要覆盖确认。 -- 唯一严格`领队`备注行生成 `passenger_list.leader_contact`,并在父表单保存前投影到 `jj_lianxiren/jj_dianhua`;多行、缺姓名/电话或结构不一致均失败关闭。 -- 当前运营模板单一源为 `agent设计规范/templates/business-input-templates.md`;运营 DOCX、五 Skill 及其发布物保持各自现有基线,本次只改变 Chrome 执行策略。 - -## 相邻待处理项 - -- AgentBus OSS 附件曾因本机 DNS 返回私网/保留地址而被 SSRF 门禁拒绝;与本次平台手工附件和名单覆写规则独立。后续若处理,应采用受控公有 DNS/主机白名单回退,不能全局放宽私网阻断。 - -## 历史与持续边界 - -- 本次压缩前完整计划见 [2026-08-28 快照](archive/project-history/2026-08-28/task_plan-pre-compression-confirmed-overwrite.md);更早快照见同目录 README 和 `archive/project-history/`,均只供追溯,不定义当前规则。 -- 当前发布版本、文件名和 SHA-256 只以 [发布清单](dist/release-manifest.json) 为准。 -- 真实 ERP 写入、任务 mutation、扩展重载、服务重启、部署和外部发送仍需用户另行明确授权。 +- 上一任务完整计划:[ERP 恢复状态任务快照](archive/project-history/2026-08-29/task_plan-pre-compression-erp-recovery-state.md),SHA-256 `7f44217c703ea78cd725040845049dd4eb321735f25170444f50155de5bd2c3e`。 +- 正式域名任务前快照:[production-domain 快照](archive/project-history/2026-08-29/task_plan-pre-compression-production-domain.md)。 +- 更早完整历史见 `archive/project-history/` 各日期 README。 diff --git a/tools/erp-session-keepalive.test.mjs b/tools/erp-session-keepalive.test.mjs index 1c62bc2..b3cd472 100644 --- a/tools/erp-session-keepalive.test.mjs +++ b/tools/erp-session-keepalive.test.mjs @@ -25,6 +25,19 @@ function loadPopupBusinessUrlMatcher(popupSource) { return context.isBusinessSystemUrl; } +function loadKeepaliveStateNormalizer(backgroundSource) { + const start = backgroundSource.indexOf('function normalizeErpKeepaliveState'); + const end = backgroundSource.indexOf('async function readErpKeepaliveState', start); + assert.ok(start >= 0 && end > start, 'background must expose the keepalive state normalizer'); + const context = {}; + vm.runInNewContext( + `${backgroundSource.slice(start, end)}\nthis.normalizeErpKeepaliveState = normalizeErpKeepaliveState;`, + context, + { filename: 'background-keepalive-state.js' } + ); + return context.normalizeErpKeepaliveState; +} + test('production control-plane origin is recognized and bridge injection is permitted', async () => { const [manifestText, background, popup] = await Promise.all([ read('chrome-extension/ltjt-order-assistant/manifest.json'), @@ -105,6 +118,34 @@ test('ERP exception recovery refreshes the existing ERP tab once, then performs assert.doesNotMatch(recoveryBody, /captureAjaxSubmit|SubmitInfoForm|DoInfo/); }); +test('an interrupted service worker recovery cannot leave ERP dispatch permanently blocked', async () => { + const background = await read('chrome-extension/ltjt-order-assistant/background.js'); + const normalizeErpKeepaliveState = loadKeepaliveStateNormalizer(background); + const persistedRunning = { + last_status: 'running', + recovery_status: 'running', + recovery_attempt_at: '2026-08-29T12:00:00.000Z', + recovery_completed_at: '', + recovery_error: '' + }; + + const active = normalizeErpKeepaliveState(persistedRunning, true); + assert.equal(active.last_status, 'running'); + assert.equal(active.recovery_status, 'running'); + + const interrupted = normalizeErpKeepaliveState(persistedRunning, false); + assert.equal(interrupted.last_status, 'interrupted'); + assert.equal(interrupted.recovery_status, 'interrupted'); + assert.equal(interrupted.recovery_error, 'erp_link_recovery_interrupted'); + + const keepaliveBody = background.slice( + background.indexOf('async function runErpSessionKeepalive'), + background.indexOf('async function ensureErpSessionKeepaliveAlarm') + ); + assert.match(keepaliveBody, /success && !erpLinkRecoveryInFlight/); + assert.match(keepaliveBody, /recovery_status: 'idle'/); +}); + test('keepalive setting and last probe state are visible without changing the business operation contract', async () => { const [background, bridge, popup, popupHtml] = await Promise.all([ read('chrome-extension/ltjt-order-assistant/background.js'), diff --git a/tools/lifecycle-contract.test.mjs b/tools/lifecycle-contract.test.mjs index 2d085a1..1ca2e4a 100644 --- a/tools/lifecycle-contract.test.mjs +++ b/tools/lifecycle-contract.test.mjs @@ -147,6 +147,166 @@ test('team-order customer restore accepts exact final values and rejects any pro }); }); +test('team-order protected-field drift matches the 0.5.156 core-field scope', async () => { + const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); + const sandbox = { window: {}, URLSearchParams }; + vm.runInNewContext(inpage, sandbox); + const inspect = sandbox.window.LTJTOrderAssistant.inspectProtectedSubmitFieldChanges; + const approved = 'chufa_ri=2026-11-08&darenshu=15&ys_danwei1=customer-a&ys_danweiid1=101&ys_jine0=1500&xiadanbeizhu=approved-note'; + const current = 'chufa_ri=2026-11-8&darenshu=16&ys_danwei1=customer-b&ys_danweiid1=202&ys_jine0=1600&xiadanbeizhu=current-note'; + + assert.deepEqual(Array.from(inspect(approved, current)), ['darenshu']); + assert.match(inpage, /__ltjt_approved_form_protected_projection = postInterceptProtectedProjection/); +}); + +test('team-order live prewrite blocker exposes only changed protected field names', async () => { + const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); + const inpage = source.replace( + 'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,', + 'inspectProtectedSubmitProjection: protectedSubmitProjection,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,' + ); + let serialized = 'chufa_ri=2026-11-8&darenshu=16&quanrenshu=1&frenshu1=8'; + let nativeAjaxCalls = 0; + const form = { elements: { length: 822 } }; + const jq = () => ({ serialize: () => serialized }); + jq.ajax = () => { nativeAjaxCalls += 1; }; + jq.ajax.__ltjt_preflight_guard = true; + jq.ajax.__ltjt_native_ajax = () => { nativeAjaxCalls += 1; }; + jq.ajax.__ltjt_intercepted_submits = [{}]; + const pageWindow = { + jQuery: jq, + $: jq, + SubmitInfoForm() {}, + setTimeout, + clearTimeout, + location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' } + }; + pageWindow.window = pageWindow; + const sandbox = { + window: pageWindow, + document: { querySelector: (selector) => selector === '#ListForm' ? form : null }, + location: pageWindow.location, + crypto: globalThis.crypto, + TextEncoder, + URLSearchParams, + Blob, + FormData, + performance, + setTimeout, + clearTimeout, + console + }; + vm.runInNewContext(inpage, sandbox); + const assistant = pageWindow.LTJTOrderAssistant; + const approved = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8'; + const approvedProjection = assistant.inspectProtectedSubmitProjection(approved); + const approvedHashBytes = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(approvedProjection)); + const approvedHash = Array.from(new Uint8Array(approvedHashBytes), (byte) => byte.toString(16).padStart(2, '0')).join(''); + jq.ajax.__ltjt_approved_form_protected_projection = approvedProjection; + + const result = await assistant.liveSubmitApproved({ + expectedPayloadSha256: 'approved-payload', + expectedFormProtectedFieldsSha256: approvedHash, + expectedRequestProtectedFieldsSha256: approvedHash, + expectedFormFieldCount: 4, + expectedRequestFieldCount: 4 + }); + + assert.equal(result.status, 'live_submit_blocked'); + assert.equal(result.write_attempted, false); + assert.equal(result.no_erp_write, true); + assert.equal(nativeAjaxCalls, 0); + assert.deepEqual(JSON.parse(JSON.stringify(result.protected_field_drift)), { + baseline_available: true, + baseline_matches_approved_hash: true, + changed_field_count: 1, + changed_field_names: ['darenshu'], + values_redacted: true + }); + assert.deepEqual(Array.from(result.blockers), [ + 'current protected business fields do not match the approved preflight (changed fields: darenshu)' + ]); +}); + +test('team-order live gate allows numbered receivable drift like 0.5.156', async () => { + const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); + const inpage = source.replace( + 'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,', + 'inspectProtectedSubmitProjection: protectedSubmitProjection,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,' + ); + const approved = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8&ys_danwei1=customer-a&ys_danweiid1=101'; + const current = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8&ys_danwei1=customer-b&ys_danweiid1=202'; + let serialized = current; + let nativeAjaxCalls = 0; + const form = { elements: { length: 822 } }; + const jq = () => ({ serialize: () => serialized }); + jq.ajax = () => { throw new Error('preflight guard should be replaced during live submit'); }; + jq.ajax.__ltjt_preflight_guard = true; + jq.ajax.__ltjt_native_ajax = (config) => { + nativeAjaxCalls += 1; + config.success?.('下单成功\n团号: TEST-ORDER', 'success'); + config.complete?.({}, 'success'); + return { readyState: 4, status: 200, abort() {} }; + }; + jq.ajax.__ltjt_intercepted_submits = [{}]; + const pageWindow = { + jQuery: jq, + $: jq, + SubmitInfoForm() { + jq.ajax({ + url: '/System/DAT/orders.asp', + type: 'POST', + data: `Act=DoInfoJH&${serialized}` + }); + }, + setTimeout, + clearTimeout, + location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' }, + alert() {} + }; + pageWindow.window = pageWindow; + const sandbox = { + window: pageWindow, + document: { + title: '计划下单/修改', + querySelector: (selector) => selector === '#ListForm' ? form : null, + querySelectorAll: () => [] + }, + location: pageWindow.location, + crypto: globalThis.crypto, + TextEncoder, + URLSearchParams, + Blob, + FormData, + performance, + setTimeout, + clearTimeout, + console + }; + vm.runInNewContext(inpage, sandbox); + const assistant = pageWindow.LTJTOrderAssistant; + const approvedProjection = assistant.inspectProtectedSubmitProjection(approved); + const approvedHashBytes = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(approvedProjection)); + const approvedHash = Array.from(new Uint8Array(approvedHashBytes), (byte) => byte.toString(16).padStart(2, '0')).join(''); + jq.ajax.__ltjt_approved_form_protected_projection = approvedProjection; + + const result = await assistant.liveSubmitApproved({ + expectedPayloadSha256: 'approved-payload-before-native-receivable-drift', + expectedFormProtectedFieldsSha256: approvedHash, + expectedRequestProtectedFieldsSha256: approvedHash, + expectedFormFieldCount: 6, + expectedRequestFieldCount: 6 + }); + + assert.equal(result.status, 'live_submit_completed'); + assert.equal(result.write_attempted, true); + assert.equal(nativeAjaxCalls, 1); + assert.deepEqual(Array.from(result.blockers), []); + assert.equal(result.submit_safety.approved_form_protected_fields_sha256_matched, true); + assert.equal(result.submit_safety.approved_request_protected_fields_sha256_matched, true); + clearTimeout(jq.ajax.__ltjt_restore_timer); +}); + test('split-parent post-write verification retries read-only and falls back after any missed response group', async () => { const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); const createBody = inpage.slice( @@ -1136,7 +1296,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\.158'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.163'/); }); test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => { @@ -1766,7 +1926,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.158'); + assert.equal(mapping.current_extension_version, '0.5.163'); assert.equal( mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'], 'ertrenshu' @@ -2148,10 +2308,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.158'); - assert.match(inpage, /version: '0\.5\.158'/); - assert.match(teamBatchInpage, /version: '0\.5\.158'/); - assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.158'/); + assert.equal(extensionManifest.version, '0.5.163'); + assert.match(inpage, /version: '0\.5\.163'/); + assert.match(teamBatchInpage, /version: '0\.5\.163'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.163'/); assert.match(inpage, /function strictIsoDate\(value\)/); assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/); assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/); diff --git a/tools/operation-timing.test.mjs b/tools/operation-timing.test.mjs index f113e82..878643e 100644 --- a/tools/operation-timing.test.mjs +++ b/tools/operation-timing.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; +import vm from 'node:vm'; const require = createRequire(import.meta.url); const timing = require('../chrome-extension/ltjt-order-assistant/operation-timing.js'); @@ -180,6 +181,8 @@ test('team-order preflight waits on ERP completion events without page-timer pol assert.match(preflight, /await productEffectCompletion/); assert.match(preflight, /preflight_lookup\.parallel_wall/); assert.match(preflight, /preflight_product_linkage/); + assert.match(preflight, /await waitForJQueryAjaxIdle\(jq\)/); + assert.match(preflight, /preflight_native_ajax_idle/); assert.match(preflight, /preflight_customer_restore/); assert.match(preflight, /dispatchNativeSideEffect: false/); assert.match(preflight, /customerRestoreReadiness/); @@ -220,6 +223,89 @@ test('team-order frame discovery targets the exact ERP iframe instead of injecti assert.ok(JSON.parse(manifestText).permissions.includes('webNavigation')); }); +test('team-order frame waits for native customer and staff lookup data before preflight', async () => { + const background = await readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'); + const start = background.indexOf('function lightweightOrderFrameProbe()'); + const end = background.indexOf('function lightweightTeamBatchFrameProbe()', start); + const probeSource = background.slice(start, end); + const runProbe = (readyFields, activeAjaxCount = 0) => { + const fields = Object.fromEntries(['zutuanshe', 'gendanren', 'xiaoshouren'].map((name) => [ + name, + { getAttribute: (attribute) => attribute === 'data' && readyFields.includes(name) ? 'native-options' : '' } + ])); + const form = { + elements: { length: 822 }, + querySelector: (selector) => fields[selector.slice(1)] || null + }; + const sandbox = { + performance, + document: { + title: '计划下单/修改', + querySelector: (selector) => selector === '#ListForm' ? form : null + }, + location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' }, + window: { frameElement: null, jQuery: { active: activeAjaxCount } }, + URL + }; + vm.runInNewContext(`${probeSource};globalThis.result=lightweightOrderFrameProbe();`, sandbox); + return JSON.parse(JSON.stringify(sandbox.result)); + }; + + const loading = runProbe([]); + assert.equal(loading.status, 'order_frame_loading'); + assert.deepEqual(loading.missing, ['zutuanshe.data', 'gendanren.data', 'xiaoshouren.data']); + const nativeAjaxPending = runProbe(['zutuanshe', 'gendanren', 'xiaoshouren'], 2); + assert.equal(nativeAjaxPending.status, 'order_frame_loading'); + assert.deepEqual(nativeAjaxPending.missing, ['jquery.ajax.pending:2']); + assert.equal(runProbe(['zutuanshe', 'gendanren', 'xiaoshouren']).status, 'order_frame_ready'); +}); + +test('team-order preflight waits for the native jQuery Ajax completion event', async () => { + const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); + const inpage = source.replace( + 'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,', + 'inspectWaitForJQueryAjaxIdle: waitForJQueryAjaxIdle,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,' + ); + const handlers = new Map(); + const eventTarget = { + one(eventName, handler) { + handlers.set(eventName, handler); + return this; + }, + off(eventName) { + handlers.delete(eventName); + return this; + } + }; + const jq = () => eventTarget; + jq.active = 2; + const pageWindow = { setTimeout, clearTimeout }; + pageWindow.window = pageWindow; + vm.runInNewContext(inpage, { + window: pageWindow, + document: {}, + performance, + setTimeout, + clearTimeout + }); + + const waiting = pageWindow.LTJTOrderAssistant.inspectWaitForJQueryAjaxIdle(jq, 1000); + setTimeout(() => { + jq.active = 0; + const handler = [...handlers.entries()].find(([eventName]) => eventName.startsWith('ajaxStop'))?.[1]; + handler?.(); + }, 5); + const result = await waiting; + + assert.deepEqual(JSON.parse(JSON.stringify(result)), { + status: 'idle', + initial_pending_ajax_count: 2, + final_pending_ajax_count: 0, + duration_ms: result.duration_ms + }); + assert.ok(result.duration_ms >= 0); +}); + test('extension result changes push immediately while polling remains as a fallback', async () => { const bridge = await readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8'); const app = await readFile(new URL('../LianSyn-platform/app.js', import.meta.url), 'utf8');