导入表头修复

This commit is contained in:
andy
2026-09-16 17:11:16 +08:00
parent 8a0f02c9be
commit a3505f3921
28 changed files with 281 additions and 45 deletions

View File

@@ -0,0 +1,38 @@
# 名单附件兼容缺少电话列
## 范围与授权
- 用户授权:兼容修复已提供的 XLSX 名单因缺少“电话”表头而被拒绝的问题。
- 仅修改名单附件标准化和相关测试、当前业务契约;缺少电话列时输出空电话,已有电话列的内容继续保留。
- 不修改原始客户附件,不将真实旅客资料保存为测试夹具,不执行 ERP 写入、部署或重启。
## Worktree 检查
- Branch: `main`
- Worktree: `/Users/andy/IdeaProjects/LWLT-AIBOT`
- Base commit: `8a0f02c9be4141feb9aecfdeb0051ea6c0748bf3`
- Existing changes: 未跟踪 `.idea/`,与本任务无关,保留原样。
- Other worktrees: 无。
- Overlap: Clear。使用当前工作区Feature 模式只写本任务的项目记忆记录。
## 已知证据
- 原附件 11269 字节SHA-256 `a109854555d0cc9343fa7e416cf469418cde6748c4baf4fb3bd6627a89a4de3c`,与用户生产日志一致。
- 当前 v1.6.0 复现 `roster_workbook_header_not_found`;第 2 行缺少电话表头。
- 仅在内存增加空电话列后,原附件 8 行通过标准化。未改动原文件。
## 结果与验证
- 已完成:名单标准化器升级为 `ltjt-passenger-roster-workbook-v1.7.0`。电话表头改为可选;缺列输出空电话,存在时照常读取、检查重复表头及非法值。其他 11 个业务字段仍必需,输出仍为 canonical 13 列。
- 不从无表头的数字推断电话;原表无表头的数字不导入。明确标记为“领队”的记录仍受姓名和电话必填规则约束。
- 增加缺列、删列、重排列名、合并备注、XLS 转换路径及旧电话校验回归。真实原附件只在内存读取8 行、8 个空电话Program Parser 返回 `agent_parse_passed`,原始 SHA-256 未变。
- 同步名单输入契约、生命周期 mapping、业务入口和操作说明。输入契约及 DOCX 为 `0.5.135`;五个 Skill 统一发布为 `0.5.128`,旧发布物按原哈希归档。插件保持 `0.5.186`,没有数据库迁移。
- 验证:定向名单测试 35/35`check:repo` 10/10`check` 通过;`test:control-plane` 271/271`test:legacy` 417/417`build` 通过;`git diff --check` 通过。
- 五个 Skill 官方校验和 ZIP/源码逐文件比较通过当前发布清单、源文件哈希及旧发布物哈希通过。DOCX 渲染后逐页检查全部 16 页,中文正常、无裁切;第 5 页为新名单说明。
- 完整仓库检查在临时干净源码快照运行,保留工作区既有 `.idea/``.DS_Store` 和旧忽略日志,不删除或收编。快照中的 2742 个源/文档/发布文件与工作区逐文件哈希一致。首次卫生检查仅因本次 DOCX 预览目录尚未清理失败;清理本次预览目录后 `check:repo``test:legacy` 全部通过。
- 主线程最终只读复核PASS未启用多 Agent。无真实 ERP 写入、部署、服务重启、提交或推送。生效需用户更新并重启服务端。
- 脱敏验证结论:[证据说明](../../../archive/evidence/2026-09-16/roster-optional-phone-01a0858b/README.md)。
## Promotion Candidates
- 后续 Integration 可将“名单原文件电话列可选;缺列留空;领队电话仍必填;不推断无表头数字”同步到 canonical 项目记忆。本次 Feature 未修改 canonical 项目记忆。

View File

@@ -20,7 +20,7 @@
| `team_order_create` | 独立团单个下单 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `team_order_create` | [业务入口](businesses/team_order_create.md) | [ERP 流程](businesses/team_order_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 已真实验证 |
| `shared_plan_create` | 散拼团新增计划 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `shared_plan_create` | [业务入口](businesses/shared_plan_create.md) | [ERP 流程](businesses/shared_plan_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 单个/批量母团已重复验证E2E3/E2E4 均证明批量每日期具体子单、客户与 15+1统一发布仍待批准 |
| `shared_child_order_create` | 散拼团单个新增子单/计划 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `shared_child_order_create` | [业务入口](businesses/shared_child_order_create.md) | [ERP 流程](businesses/shared_child_order_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 已重复真实创建并完成后续全生命周期;仍受统一发布门槛约束 |
| `passenger_list_import` | 独立团/散拼子单名单导入/补充 | [`lwlt-lifecycle`](skills/lwlt-lifecycle/SKILL.md) / `passenger_list_import`Program-only | [统一输入模板 §6—7](templates/business-input-templates.md);文字定位 + 单个 `.xls/.xlsx` 附件;程序在前 100 行自动定位唯一 ERP 字段表头,按精确语义从任意表头行和列顺序中选择必需字段,并读取其下连续名单数据;身份证、年龄、源证件类型和其他普通额外列忽略且不输出 | 识别 route 后等待附件 → 程序规范化为加密 13 列 TSV并从唯一严格`领队`备注行提取姓名/电话 → 读取 ERP 游客位 → 未确认的占用序号要求覆盖确认;确认后所有指定序号强制覆写(同值也写)→ `DaoRuDones` → 原生游客位不足时返回“名单 N 人超过 ERP 最多 M 人”且不保存 → 恢复未指定行 → 投影领队联系人 → 完整写前投影 → 父页原生保存ERP 明确返回成功即完成,不再逐行写后回查 | 用户模板 `.xls` 已只读通过 25 行规范化及单一领队行结构核验;解析阶段不以初始 16/31 行作统一静态上限5000 为技术上限;执行时以精确 ERP 页面实际物化游客位为准。2026-08-28 一笔散拼子单 25 行及领队联系人已持久化全等;另一次独立团 25 人只物化 16 位并在保存前安全阻断 | 名单与领队联系人真实写入已有一笔证据;`0.5.155` 起确认覆盖同值也写,`0.5.156` 起容量不足使用人数/上限业务提示并仅在技术详情保留原始 blocker其他对象/边界继续按发布门槛分级 |
| `passenger_list_import` | 独立团/散拼子单名单导入/补充 | [`lwlt-lifecycle`](skills/lwlt-lifecycle/SKILL.md) / `passenger_list_import`Program-only | [统一输入模板 §6—7](templates/business-input-templates.md);文字定位 + 单个 `.xls/.xlsx` 附件;程序在前 100 行自动定位唯一 ERP 字段表头,按精确语义从任意表头行和列顺序中选择 11 个必需字段及可选电话列(缺少时输出空值,存在时仍须唯一并校验),并读取其下连续名单数据;身份证、年龄、源证件类型和其他普通额外列忽略且不输出 | 识别 route 后等待附件 → 程序规范化为加密 13 列 TSV并从唯一严格`领队`备注行提取姓名/电话 → 读取 ERP 游客位 → 未确认的占用序号要求覆盖确认;确认后所有指定序号强制覆写(同值也写)→ `DaoRuDones` → 原生游客位不足时返回“名单 N 人超过 ERP 最多 M 人”且不保存 → 恢复未指定行 → 投影领队联系人 → 完整写前投影 → 父页原生保存ERP 明确返回成功即完成,不再逐行写后回查 | 用户模板 `.xls` 已只读通过 25 行规范化及单一领队行结构核验;解析阶段不以初始 16/31 行作统一静态上限5000 为技术上限;执行时以精确 ERP 页面实际物化游客位为准。2026-08-28 一笔散拼子单 25 行及领队联系人已持久化全等;另一次独立团 25 人只物化 16 位并在保存前安全阻断 | 名单与领队联系人真实写入已有一笔证据;`0.5.155` 起确认覆盖同值也写,`0.5.156` 起容量不足使用人数/上限业务提示并仅在技术详情保留原始 blocker其他对象/边界继续按发布门槛分级 |
| `order_update_shared_plan` / `order_update_shared_child` | 散拼母团容量/散拼子单住宿说明 | [`lwlt-updating`](skills/lwlt-updating/SKILL.md) | [统一输入模板:修改](templates/business-input-templates.md) | `DoInfoSP` / `DoInfo_order`;写前读取当前值 | 母团容量、子单住宿说明通过;用户只填目标值;无年份定位日期取任务日后的下一次发生日 | Skill 全量解析;仅已验证字段自动执行,其他字段保留并转人工复核 |
| `order_update_independent` | 独立团信息修改 | [`lwlt-updating`](skills/lwlt-updating/SKILL.md) / `order_update_independent` | [统一输入模板 §15](templates/business-input-templates.md) | `DoInfoJH`写前读取当前值SGL/TWN 分别映射 `frenshu0`/`frenshu1`,成人/占床儿童/不占床儿童/领队分别映射 `darenshu`/`xiaorenshu`/`ertrenshu`/`quanrenshu` | 标间数 `8→9→8` 已有明确响应和 fresh GET无年份定位/目标日期取下一次发生日SGL/TWN 与四类人数待新版本授权 ERP 实写复测 | Skill 全量解析当前插件执行标间数、SGL/TWN 与四类人数,其他目标保留并转人工复核 |
| `arrangement_guide` / `arrangement_vehicle` / `arrangement_hotel` / `arrangement_transport` / `arrangement_other` | 安排导游/用车/酒店/大交通/其他备案 | [`lwlt-arrangement`](skills/lwlt-arrangement/SKILL.md) | [统一输入模板 §8—12](templates/business-input-templates.md) | 五类 action-specific `DoInfo_*`,全类 `create|clear`;酒店另有窄 `update`,程序模式支持五类创建指令加“变更”;用车新增/变更支持选填用车类型,与结算单位搜索共同筛选唯一候选;大交通新增/变更支持选填车票说明,与结算单位共同筛选唯一候选;原日期仅定位已有安排,支持 MM-DD 由指定团队中唯一原记录确定年份YYYY-MM-DD 精确匹配,歧义列候选并停止;导游按团队唯一已有导游定位 | 两类团队共 10 create + 10 clear酒店资源/房型由唯一 ERP 模糊匹配结果带入;大交通省略车票说明时及其他/备案的结算单位组合候选按下拉顺序首条原生联动,其他/备案数量可选,省略时保留 ERP 默认值;散拼母团无子单时 ERP 禁止安排酒店并隐藏提交入口,插件在三项页面证据同时成立时提示先新增子单 | create/clear 与酒店窄 update 已有真实证据;程序模式五类资源替换变更已完成本地回归,待真实 ERP 验收;状态和直接修改联动字段继续阻断 |

View File

@@ -11,7 +11,7 @@ description: 定义 LTJT 的独立团或散拼子单名单导入、按序号补
1. 从首行和正文识别名单导入、取消或恢复。
2. 按 [input-contract.md](references/input-contract.md) 检查用户可见必填字段。
3. 名单 route 先进入 `awaiting_attachment`;只接收一份 `.xls`/`.xlsx`。控制面程序在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义从任意列顺序中选择 12 个必需源字段,再把表头下方的连续名单数据生成内部 13 列 canonical TSV。性别、出生日期采用受支持的身份证模板公式时程序仅为验证公式读取同一行身份证并校验缓存结果身份证不进入输出。年龄、源证件类型和其他普通额外列仍忽略。附件到齐前不启动解析或执行。具体公式范围见 `references/input-contract.md`
3. 名单 route 先进入 `awaiting_attachment`;只接收一份 `.xls`/`.xlsx`。控制面程序在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义从任意列顺序中选择 11 个必需源字段和可选的电话列;缺少电话列时电话输出为空,有电话列时按原规则读取和校验,再把表头下方的连续名单数据生成内部 13 列 canonical TSV。性别、出生日期采用受支持的身份证模板公式时程序仅为验证公式读取同一行身份证并校验缓存结果身份证不进入输出。年龄、源证件类型和其他普通额外列仍忽略。附件到齐前不启动解析或执行。具体公式范围见 `references/input-contract.md`
4. Program Parser 校验内部 13 列表头、每行完整性、正整数唯一序号;允许用户只提供本次需要补录的一个或多个序号。备注去空格后严格等于`领队`的唯一行会生成结构化 `passenger_list.leader_contact`;姓名或电话为空、或出现多行领队时失败关闭。
5. 按 [operation-contract.md](references/operation-contract.md) 生成一个 operation。
6. 返回 `agent_parse_passed``agent_parse_needs_input``agent_parse_blocked`

View File

@@ -60,7 +60,7 @@
示例同时附一份符合 ERP 字段规则的名单工作簿。
附件必须且只能有一个可见工作表。程序在前 100 行中自动定位唯一表头;表头可直接位于第一行,也可位于团信息或说明行之后,列顺序任意,表头下一行起的名单数据必须连续。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/电话/备注`。程序选择和校验这些必需字段;`年龄`、源`证件类型`以及其他普通额外列均被忽略。`身份证号码(或身份证)`仅在性别或出生日期使用下述固定公式时作为校验来源,其余情况下忽略;身份证原值不会进入 ERP 或内部 canonical TSV。程序把英文姓名映射为 `NAME`、护照号码映射为“证件号码”、固定输出“证件类型=护照”,并把签发日期映射为“签发日”。
附件必须且只能有一个可见工作表。程序在前 100 行中自动定位唯一表头;表头可直接位于第一行,也可位于团信息或说明行之后,列顺序任意,表头下一行起的名单数据必须连续。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/备注`电话列可选:缺少时在内部 13 列 TSV 中输出空电话,不从无表头数字或其他列推断;存在时只能有一列,继续读取和校验其内容。程序选择和校验上述必需字段及存在的电话列`年龄`、源`证件类型`以及其他普通额外列均被忽略。`身份证号码(或身份证)`仅在性别或出生日期使用下述固定公式时作为校验来源,其余情况下忽略;身份证原值不会进入 ERP 或内部 canonical TSV。程序把英文姓名映射为 `NAME`、护照号码映射为“证件号码”、固定输出“证件类型=护照”,并把签发日期映射为“签发日”。
顶部团信息或说明区的 Excel 错误值(如 `#VALUE!`)不作为表头标签;名单字段中的错误值仍阻断。性别、出生日期兼容以下固定身份证推算公式(示例 `E3` 必须对应唯一身份证列的同一行;列重排后按实际位置匹配):
@@ -73,7 +73,7 @@
若某位游客是领队,该行`备注`去除首尾空格后必须严格等于`领队`,并同时填写该行`姓名``电话`。一份名单最多标记一位领队;标记成功后程序会把这两个值写入 ERP `联系人/领队人``电话`,该行仍作为普通游客导入。`领队备注`等其他文字不触发写入;没有严格标记时保留 ERP 原联系人信息。正文里的`领队`字段仍只是订单定位的选填补充关键词,不是联系人写入来源。
只接收一个 `.xls`/`.xlsx` 文件;旧版 `.xls` 必须经隔离转换得到真实 XLSX不能回退原始字节。多工作表、隐藏的必需名单字段数据、宏、外部链接、必需字段中的非模板公式、必需字段断行、空名单、非正整数/重复序号均失败关闭。普通额外列可以有值、公式或被隐藏,但外部公式、超链接及其他主动内容仍按全工作簿安全边界阻断。最多 5000 行且最高序号 5000。附件错误不得把单元格值、文件正文或明文文件名写入日志或 API 错误。
只接收一个 `.xls`/`.xlsx` 文件;旧版 `.xls` 必须经隔离转换得到真实 XLSX不能回退原始字节。多工作表、隐藏的已选名单字段数据、宏、外部链接、已选字段中的非模板公式、必需字段断行、空名单、非正整数/重复序号均失败关闭。普通额外列可以有值、公式或被隐藏,但外部公式、超链接及其他主动内容仍按全工作簿安全边界阻断。最多 5000 行且最高序号 5000。附件错误不得把单元格值、文件正文或明文文件名写入日志或 API 错误。
用户可提供一个或多个游客序号。规范化后的每个内部数据行必须完整包含 13 列未提供的序号不代表删除或清空。ERP 独立团默认 16 行、散拼子单默认 31 行只是初始行基线,不是导入上限;执行端按现有行数与最高序号动态扩行。

View File

@@ -1,6 +1,6 @@
# 运营业务 AI 输入模板总表
交付版本0.5.134
交付版本0.5.135
适用范围:当前 23 条业务路由,按 19 项运营场景分组;执行能力以生命周期发布门槛为准。
本文件用于向系统提交 ERP 业务需求。复制对应模板填写;示例只供参考,不要直接用于生产业务。
@@ -154,7 +154,7 @@
名单只通过一份 `.xls``.xlsx` 附件提交,不再粘贴到文字输入框。系统识别名单业务后会等待附件,附件到齐后只走程序解析。
系统会在工作表前 100 行中自动定位唯一的名单表头;表头可以直接在第一行,也可以位于团信息或说明行之后,字段顺序任意,表头下一行开始识别连续名单数据。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/电话/备注`。系统读取和校验这些必需字段;`年龄`、源`证件类型`以及其他普通额外列会被忽略,不进入 ERP。每次只保留一个可见工作表最多 5000 行;宏、外部链接等主动内容仍会被安全阻断。
系统会在工作表前 100 行中自动定位唯一的名单表头;表头可以直接在第一行,也可以位于团信息或说明行之后,字段顺序任意,表头下一行开始识别连续名单数据。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/备注`电话列可以省略,省略时电话留空;如需导入电话号码,请保留一列“电话”表头,系统不会把无表头数字猜作电话。系统读取和校验这些必需字段及存在的电话列`年龄`、源`证件类型`以及其他普通额外列会被忽略,不进入 ERP。每次只保留一个可见工作表最多 5000 行;宏、外部链接等主动内容仍会被安全阻断。
名单模板中按同一行身份证自动推算性别和出生日期的固定公式可以保留。请保留唯一的`身份证号码``身份证`18 位号码使用文本格式保存;系统会核对公式计算结果,身份证本身不导入 ERP。公式结果缺失、错误或与身份证不一致时需要先在表格中修正。直接填写性别和出生日期时身份证列仍被忽略。顶部说明区的错误值不影响识别旅客资料中的错误值需要修正。

View File

@@ -6,7 +6,7 @@
- `0.5.186` 修复车票说明中车次与时刻的数字边界,明确供应商与说明联合唯一匹配及多候选提示;新增/变更本地合成回归通过,真实 ERP 验收待部署方完成。见[任务记录](../../../.project-docs/30-worklog/tasks/20260916-transport-description-match-01a0858b.md)。
- Agent Prompt`ltjt-agent-prompt-v1.8-independent-headcount-categories`
- 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08`
- 五个 Skill`0.5.127`;运营 DOCX`0.5.134`
- 五个 Skill`0.5.128`;运营 DOCX`0.5.135`
- 发布状态:核心窄能力已有真实证据;一笔名单附件 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 原生回调在预检后调整这些字段。`0.5.164` 新增平台账号、AgentBus 渠道、唯一云电脑 worker 与 ERP 登录身份四方一致门禁;旧未绑定渠道和历史未归属 AgentBus 任务默认不执行。`0.5.165` 让散拼新增计划和独立团批量下单在本地候选未命中时先使用表单原生非空产品检索,实测两类表单均能由完整关键词唯一返回目标产品;本轮未勾选或保存,自动选择与写入仍由原门禁保护。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。
- `0.5.166` 在散拼母团列表入口加入立即检查、100ms 条件轮询和 15 秒总超时;每次探测都重新取得当前主 iframe document按钮一旦可用便立即继续。该等待只发生在点击前不固定拖慢快速页面也不触发业务写入重试。
@@ -16,7 +16,7 @@
- `0.5.172` 按维护方要求将酒店新增 `DoInfo_jiudian` 的固定字段门槛从 195 调整为 150恢复原来的预检流程`0.5.171` 的结构判定方案已撤回并归档。其他模式/业务原门槛及后续执行检查保留,低于 150 的页面仍会阻断。尚未部署或进行真实 ERP 写入验收;验证范围见[任务记录](../../../.project-docs/30-worklog/tasks/20260909-hotel-form-readiness-01a0858b.md)。
- `0.5.173` 修复名单 TSV 末尾空列被整体 `trim()` 删除的问题,覆盖服务端统一 operation 校验、插件派发及原生名单合并。13 列完整性、序号、覆盖确认及领队约束继续执行;服务端与插件需同步更新,数据库无需变更。该版本的本地验证与真实 ERP 验收边界见[任务记录](../../../.project-docs/30-worklog/tasks/20260910-roster-trailing-empty-columns-01a0858b.md)。
- 运营输入/DOCX `0.5.127` 补齐第 19 项`散拼团多个新增子单`的可复制模板与示例;该入口仍固定为 Program-only不进入 Agent Prompt 或 Skill。
- 名单工作簿 normalizer `v1.6.0` 继续选择 12 个必需源字段;新增固定身份证推算性别/生日公式的同一行来源缓存一致性校验,顶部说明区错误值不作为表头。身份证用于公式校验,不进入 canonical 输出;其余普通额外列仍忽略。必需字段错误、宏/外链、全局超链接和外部公式等安全门槛保持失败关闭。验证范围见[验证记录](../../../archive/evidence/2026-09-15/roster-template-formulas-01a0858b/README.md)未执行真实 ERP 导入。
- 名单工作簿 normalizer `v1.7.0` 选择 11 个必需源字段及可选电话列;电话列缺少时输出空值,不推断无表头数字,存在时仍须唯一并接受原有校验;标记领队仍须有姓名和电话。保留固定身份证推算性别/生日公式的同一行来源缓存一致性校验,顶部说明区错误值不作为表头。身份证用于公式校验,其余普通额外列仍忽略。已选字段错误、宏/外链、全局超链接和外部公式等安全门槛保持失败关闭。缺少电话列的验证见[当前任务](../../../.project-docs/30-worklog/tasks/20260916-roster-optional-phone-01a0858b.md),此前公式验证见[验证记录](../../../archive/evidence/2026-09-15/roster-template-formulas-01a0858b/README.md)未执行真实 ERP 导入。
运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。

View File

@@ -0,0 +1,31 @@
# 名单缺少电话列兼容验证
日期2026-09-16。此文件保存本次验证的历史结论不作为当前业务规则或运行输入。未保存真实名单、旅客资料、生产日志或秘密配置。
## 行为与真实附件
- 标准化器:`ltjt-passenger-roster-workbook-v1.7.0`
- 原始 XLSX11269 字节SHA-256 `a109854555d0cc9343fa7e416cf469418cde6748c4baf4fb3bd6627a89a4de3c`;处理前后哈希一致。
- 第 2 行识别出 11 个必需业务字段,电话表头缺失;成功标准化 8 行电话均为空canonical TSV 仍为 13 列。
- Program Parser 返回 `agent_parse_passed`。验证仅在内存执行,未创建任务或提交 ERP。
- 无表头数字不作为电话;有电话表头时保留原有校验;领队仍要求姓名和电话。
## 验证结果
| 检查 | 结果 |
| --- | --- |
| 名单定向测试 | 35/35 |
| `node --run check:repo` | 10/10 |
| `node --run check` | PASS |
| `node --run test:control-plane` | 271/271 |
| `node --run test:legacy` | 417/417 |
| `node --run build` | PASS |
| 五个 Skill 官方校验 | PASS |
| 五个 Skill ZIP 与源码逐文件核对 | PASS |
| 当前发布清单、源码及构建器 SHA-256 | PASS |
| 旧版本归档 SHA-256 | PASS |
| DOCX 全部 16 页 PNG 目视检查 | PASS |
仓库检查在临时干净源码快照执行,避开工作区原有 IDE、系统文件及忽略日志未清理这些原有文件。快照与工作区 2742 个文件逐文件哈希一致。本次预览输出清理后重新运行卫生及 legacy 检查通过。
交付版本为 Skill `0.5.128`、操作 DOCX/输入契约 `0.5.135`;插件仍为 `0.5.186`。当前发布物以 `dist/release-manifest.json` 为准。本次未部署、未重启服务、未操作 ERP。

View File

@@ -0,0 +1,3 @@
# 旧名单输入契约发布物
只读历史,不能作为当前业务规则。此目录按原 SHA-256 保留五个 0.5.127 Skill、0.5.134 操作 DOCX 和更新前发布清单;当前交付物以根目录 dist/release-manifest.json 为准。

View File

@@ -0,0 +1,69 @@
{
"manifest_version": 1,
"generated_on": "2026-09-16",
"baselines": {
"chrome_extension": "0.5.186",
"skills": "0.5.127",
"business_instruction_docx": "0.5.134",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
},
"artifacts": [
{
"kind": "chrome_extension",
"version": "0.5.186",
"path": "dist/ltjt-order-assistant-0.5.186.zip",
"source": "chrome-extension/ltjt-order-assistant",
"sha256": "c79d108ce4cbed85a856b336e1643fd990d81f21d16a0bbd66be27510d1c2568"
},
{
"kind": "skill",
"name": "lwlt-arrangement",
"version": "0.5.127",
"path": "dist/lwlt-arrangement-0.5.127.skill",
"source": "agent设计规范/skills/lwlt-arrangement",
"sha256": "ae3fc66965d2a7968aeaa2ecf371d75e5365ef4cc4dfd8ef080f9343e0d0f09c"
},
{
"kind": "skill",
"name": "lwlt-confirmation",
"version": "0.5.127",
"path": "dist/lwlt-confirmation-0.5.127.skill",
"source": "agent设计规范/skills/lwlt-confirmation",
"sha256": "c2f3689d8a0b4d6b250bb2cfce572fe3a9fca3f5519ea5fae5ef0d382b44dae4"
},
{
"kind": "skill",
"name": "lwlt-lifecycle",
"version": "0.5.127",
"path": "dist/lwlt-lifecycle-0.5.127.skill",
"source": "agent设计规范/skills/lwlt-lifecycle",
"sha256": "f0b5cdea11e65657fb9b10e4f7d020769d5477cc8ceddfcfd6a284513345f6bc"
},
{
"kind": "skill",
"name": "lwlt-newbooking",
"version": "0.5.127",
"path": "dist/lwlt-newbooking-0.5.127.skill",
"source": "agent设计规范/skills/lwlt-newbooking",
"sha256": "b64593d88145966cd4e702d2532ffe3ea85052e8d56fbe75d503a19ababa1715"
},
{
"kind": "skill",
"name": "lwlt-updating",
"version": "0.5.127",
"path": "dist/lwlt-updating-0.5.127.skill",
"source": "agent设计规范/skills/lwlt-updating",
"sha256": "54f10dd6bd6d2ddf47c700f1bca4b1fcd3346dc6143bf27118a23587b1b11736"
},
{
"kind": "business_instruction_docx",
"version": "0.5.134",
"path": "dist/老挝联泰AI指令表-0.5.134.docx",
"source": "agent设计规范/templates/business-input-templates.md",
"builder": "tools/build_business_instruction_docx.py",
"sha256": "b6c1f677b93cb4f7356cfb26c6af01860587be1288ffe9e8aff8295929074490",
"source_sha256": "7446e4560e2eaaf6f490c8ed7523d6aadcee6f94f58cb620f7e71db674404471",
"builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1"
}
]
}

View File

@@ -10,7 +10,7 @@
- 业务页面通过 REST 创建任务Agent 返回结构化结果后,任务归属人在自己的任务上一次点击“确认并提交到 ERP 插件”,再通过 SSE 或轮询读取服务端状态。普通用户与组长的任务 API、SSE、附件和插件执行权限只覆盖分配给本人的人工任务与 AgentBus 任务;管理员是纯管理面身份,不能创建、查看、修改、领取、接收事件或提交任何业务任务。
- 手工与 AgentBus 的每个新任务都会按同一组织、同一 19 项业务路由固化解析策略及配置 revision来源不能覆盖模式。其中两项名单 route 与散拼批量子单 route 固定为 `program_only`,其余 16 项可配置 `ai / shadow / auto / program`。全消息中的唯一已登记指令可以确定 route没有指令时只有全部标签都属于唯一 route、至少两个不同标签且业务定位必填项完整的字段签名才可确定 route。未知、冲突或多 route 输入不猜测。后续补充轮次沿用原任务快照,设置变化只影响新任务和新会话。
- 组长和普通用户使用逐账号白名单,新账号默认没有任何可执行任务类型;管理员自身的任务白名单固定为空,只能在 `/accounts` 为员工逐项授权。已登记但未授权的业务返回 `business_not_authorized`;无法唯一确认 route 的员工输入返回 `business_type_unresolved`。两种拒绝都发生在解析器和 ERP 插件之前,并记录不含明文的授权拒绝审计。权限在补充输入、人工确认、全自动确认和插件领取前再次校验;取消授权后的任务不会进入 ERP 队列。AgentBus 入站使用渠道绑定员工的同一白名单,管理员账号不得成为员工渠道归属人。
- 名单 route 创建后先进入 `awaiting_attachment`,只接收一份 `.xls/.xlsx`;控制面在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义从任意表头行和列顺序中选择 12 个必需源字段,并把表头下方连续名单数据在内存中规范化为 13 列 canonical TSV。身份证、年龄、源证件类型和其他普通额外列不参与名单行识别、字段校验或输出必需字段缺失/重复、多个候选表头、必需字段中的非法公式及宏、外链等主动内容仍失败关闭。原始工作簿不入库,文件名和 canonical TSV 使用字段加密,解析通过或终止后清除 canonical 中间文本。附件到齐前不会领取解析任务,也不会进入 ERP。独立团初始 16 行、散拼子单初始 31 行仅为 ERP 动态扩行基线5000 为技术上限。
- 名单 route 创建后先进入 `awaiting_attachment`,只接收一份 `.xls/.xlsx`;控制面在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义从任意表头行和列顺序中选择 11 个必需源字段及可选的电话列(缺少时输出空值,不推断无表头数字;存在时仍须唯一并接受原有校验),并把表头下方连续名单数据在内存中规范化为 13 列 canonical TSV。身份证、年龄、源证件类型和其他普通额外列不参与名单行识别、字段校验或输出必需字段缺失/重复、多个候选表头、已选字段中的非法公式及宏、外链等主动内容仍失败关闭。原始工作簿不入库,文件名和 canonical TSV 使用字段加密,解析通过或终止后清除 canonical 中间文本。附件到齐前不会领取解析任务,也不会进入 ERP。独立团初始 16 行、散拼子单初始 31 行仅为 ERP 动态扩行基线5000 为技术上限。
- 平台的任务 ID、Agent 会话 ID、确认状态、重要摘要、事件和用户通讯内容只存在于平台任务/会话/结果信封中,不回写到 Agent `operation`,也不成为 ERP 业务字段。
- Agent `operation` 只保存解析态业务事实。插件领取已确认任务后先做无需 ERP 查询的前门禁,再在 ERP 内只读唯一解析对象、资源和当前状态,最后对内部 execution operation 执行严格写前门禁。
- Chrome 插件仍在当前平台账号已登录的浏览器/ERP 会话中工作;每个员工平台账号必须配置唯一 ERP 账号,同一平台账号在 90 秒新鲜期内只允许一台云电脑保持执行 worker。心跳携带扩展对期望 ERP 账号的只读匹配结果;账号不匹配、心跳过期、第二台并发云电脑或任务归属不一致都会在领取和回执两端失败关闭。`chrome.storage.local` 只是临时缓存。

View File

@@ -80,7 +80,7 @@ export function effectiveParserModeForRoute(route: BusinessRouteDefinition, conf
}
const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.12';
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.134';
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.135';
const lookupFields = ({ customerRequired = true } = {}): ProgramFieldDefinition[] => [
{ key: 'order_no', label: '单号', aliases: ['团号', '订单号'], kind: 'text' },

View File

@@ -13,22 +13,23 @@ const ABSOLUTE_MAX_DATA_ROWS = 5_000;
const MAX_PASSENGER_SEQUENCE = 5_000;
const MAX_HEADER_ROW = 100;
export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.6.0';
export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.7.0';
const REQUIRED_SOURCE_FIELDS = [
'序号', '姓名', '英文姓名', '性别', '出生日期', '出生地', '护照号码',
'签发地', '签发日期', '有效期', '电话', '备注'
'签发地', '签发日期', '有效期', '备注'
] as const;
type RequiredSourceField = typeof REQUIRED_SOURCE_FIELDS[number];
type SourceColumnMap = Record<RequiredSourceField, number>;
type SourceField = RequiredSourceField | '电话';
type SourceColumnMap = Record<RequiredSourceField, number> & { 电话?: number };
/**
* Header recognition is intentionally exact and finite. It accepts the
* required customer-facing labels, canonical ERP labels, and approved aliases.
* Non-import columns are deliberately absent from this map and are ignored.
*/
const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, RequiredSourceField]> = [
const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, SourceField]> = [
['序号', '序号'],
['姓名', '姓名'],
['英文姓名', '英文姓名'],
@@ -48,7 +49,7 @@ const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, RequiredSourceField]
['备注', '备注']
];
const SOURCE_FIELD_BY_HEADER = new Map<string, RequiredSourceField>(HEADER_FIELD_ENTRIES);
const SOURCE_FIELD_BY_HEADER = new Map<string, SourceField>(HEADER_FIELD_ENTRIES);
export const CANONICAL_PASSENGER_HEADERS = [
'序号', '姓名', 'NAME', '性别', '出生日期', '出生地', '证件类型', '证件号码',
@@ -402,7 +403,7 @@ function rowHasData(row: ExcelJS.Row, sourceColumns: readonly number[]): boolean
// These two legacy template formulas derive gender/birth date from a deleted
// identity-card column. Recognize the exact broken templates, not arbitrary
// formulas or their cached results, when deciding whether a tail row is unused.
const BROKEN_TEMPLATE_FORMULAS: Partial<Record<RequiredSourceField, string>> = {
const BROKEN_TEMPLATE_FORMULAS: Partial<Record<SourceField, string>> = {
: 'IF(OR(LEN(#REF!)=15,LEN(#REF!)=18),IF(MOD(MID(#REF!,15,3)*1,2),"男","女"),NA())',
: 'DATE(MID(#REF!,7,VLOOKUP(LEN(#REF!),{15,2;18,4},2,0)),MID(#REF!,VLOOKUP(LEN(#REF!),{15,9;18,11},2,0),2),MID(#REF!,VLOOKUP(LEN(#REF!),{15,11;18,13},2,0),2))'
};
@@ -434,11 +435,11 @@ function unusedTemplateRow(row: ExcelJS.Row, headerRow: number, sourceColumns: S
if (sequenceCell.type === ExcelJS.ValueType.Formula) return false;
const sequence = normalizedText(scalarValue(sequenceCell));
if (sequence && (!/^[1-9]\d*$/.test(sequence) || Number(sequence) > MAX_PASSENGER_SEQUENCE)) return false;
for (const field of REQUIRED_SOURCE_FIELDS) {
for (const [field, column] of Object.entries(sourceColumns)) {
if (field === '序号') continue;
const cell = row.getCell(sourceColumns[field]);
const cell = row.getCell(column);
if (cell.type === ExcelJS.ValueType.Formula) {
const template = BROKEN_TEMPLATE_FORMULAS[field];
const template = BROKEN_TEMPLATE_FORMULAS[field as SourceField];
if (!formulaAllowed(cell, headerRow, sourceColumns, identityColumn)
&& (!template || normalizedFormula(cell.formula) !== normalizedFormula(template))) return false;
} else if (normalizedText(scalarValue(cell)) !== '') {
@@ -516,7 +517,7 @@ function validateFormulasAndHiddenData(
}
function headerColumnsForRow(sheet: ExcelJS.Worksheet, rowNumber: number): SourceColumnMap | null {
const columns = new Map<RequiredSourceField, number>();
const columns = new Map<SourceField, number>();
const row = sheet.getRow(rowNumber);
for (let columnNumber = 1; columnNumber <= sheet.columnCount; columnNumber += 1) {
const cell = row.getCell(columnNumber);
@@ -530,11 +531,9 @@ function headerColumnsForRow(sheet: ExcelJS.Worksheet, rowNumber: number): Sourc
}
if (!REQUIRED_SOURCE_FIELDS.every((field) => columns.has(field))) return null;
const result: Partial<SourceColumnMap> = {};
for (const field of REQUIRED_SOURCE_FIELDS) {
result[field] = columns.get(field);
}
return result as SourceColumnMap;
// Keep absent optional fields out of the map: all downstream row/formula
// checks operate on actual selected columns, never a guessed neighbour.
return Object.fromEntries(columns) as SourceColumnMap;
}
function findHeaderLayout(sheet: ExcelJS.Worksheet): { row: number; columns: SourceColumnMap } {
@@ -739,7 +738,7 @@ function canonicalRow(
canonicalTextCell(cell('签发地')),
canonicalDate(issueDateCell, date1904, issueDateCell.type === ExcelJS.ValueType.Formula),
canonicalDate(expiryCell, date1904, expiryCell.type === ExcelJS.ValueType.Formula),
canonicalIdentifier(cell('电话')),
sourceColumns['电话'] === undefined ? '' : canonicalIdentifier(row.getCell(sourceColumns['电话'])),
canonicalTextCell(cell('备注'))
];
}

View File

@@ -136,6 +136,102 @@ test('XLSX is deterministically normalized to the canonical 13-column TSV', asyn
].join('\n'));
});
test('an absent phone header produces blank phones without guessing unheaded values', async () => {
const baseline = await normalizeSynthetic(await syntheticWorkbook());
const expected = baseline.canonicalTsv.split('\n').map((line, index) => {
if (index === 0) return line;
const fields = line.split('\t');
fields[11] = '';
return fields.join('\t');
}).join('\n');
for (const removeColumn of [false, true]) {
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
if (removeColumn) sheet.spliceColumns(13, 1);
else sheet.getCell('M2').value = null; // The original number below remains unheaded.
}));
assert.equal(result.canonicalTsv, expected);
assert.equal(result.rowCount, 2);
const parsed = await parseProgramInput({
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
receivedAt: new Date('2026-09-16T00:00:00Z')
});
assert.equal(parsed.status, 'agent_parse_passed');
}
});
test('phone-free rosters support reordered alias headers, merged remarks and XLS conversion', async () => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('无电话模板');
sheet.mergeCells('B1:K1');
sheet.getCell('B1').value = '脱敏团队信息';
sheet.addRow(['序号', '姓名', '英文姓名', '护照号码', '性别', '出生日期', '年龄', '出生地点', '签发地', '签发日期', '有效期至', '备注']);
for (let sequence = 1; sequence <= 8; sequence++) {
sheet.addRow([sequence, '测试游客', 'TEST', `P0000000${sequence}`, '女', '1990-01-02', 36, '测试地', '测试地', '2025-01-02', '2035-01-01', '普通备注']);
}
sheet.mergeCells('L3:L10');
sheet.getCell('M10').value = 13800000000; // Extra unheaded content must not become a phone.
const xlsx = Buffer.from(await workbook.xlsx.writeBuffer());
const expected = await normalizeSynthetic(xlsx);
assert.equal(expected.rowCount, 8);
for (const row of expected.canonicalTsv.split('\n').slice(1)) {
assert.equal(row.split('\t').length, 13);
assert.deepEqual(row.split('\t').slice(11), ['', '普通备注']);
}
assert.doesNotMatch(expected.canonicalTsv, /13800000000/);
const legacy = await normalizePassengerRosterWorkbook({
content: Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]),
fileName: 'synthetic.xls', contentType: 'application/vnd.ms-excel'
}, { convertLegacyXls: async () => xlsx });
assert.equal(legacy.canonicalTsv, expected.canonicalTsv);
});
test('phone is the only optional semantic and duplicate phone headers stay blocked', async () => {
for (const column of [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 14]) {
const content = await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
sheet.getCell('M2').value = null;
sheet.getCell(2, column).value = null;
});
await expectRosterError(() => normalizeSynthetic(content), 'roster_workbook_header_not_found');
}
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('O2').value = '电话';
}).then(normalizeSynthetic), 'roster_workbook_header_not_found');
});
test('present phone columns retain formula, value, hidden-data and row checks', async () => {
for (const [value, code] of [
[{ formula: '1+1', result: 2 }, 'roster_workbook_formula_not_allowed'],
[{ error: '#VALUE!' }, 'roster_workbook_invalid_cell'],
[1.5, 'roster_workbook_invalid_identifier']
] as const) {
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('M3').value = value;
}).then(normalizeSynthetic), code, { row: 3, column: 13 });
}
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getColumn(13).hidden = true;
}).then(normalizeSynthetic), 'roster_workbook_hidden_data');
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('M5').value = '13800000000';
}).then(normalizeSynthetic), 'roster_workbook_invalid_sequence', { row: 5, column: 1 });
});
test('a marked leader still requires a phone when the source phone column is absent', async () => {
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
sheet.getCell('M2').value = null;
sheet.getCell('N3').value = '领队';
}));
const parsed = await parseProgramInput({
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
receivedAt: new Date('2026-09-16T00:00:00Z')
});
assert.notEqual(parsed.status, 'agent_parse_passed');
assert.match(JSON.stringify(parsed), /姓名和电话都必须填写/);
});
test('a merged title error is skipped, while header and passenger errors stay blocked', async () => {
const baseline = await normalizeSynthetic(await syntheticWorkbook());
const titleError = await normalizeSynthetic(await syntheticWorkbook(workbook => {

BIN
dist/lwlt-arrangement-0.5.128.skill vendored Normal file

Binary file not shown.

BIN
dist/lwlt-confirmation-0.5.128.skill vendored Normal file

Binary file not shown.

BIN
dist/lwlt-lifecycle-0.5.128.skill vendored Normal file

Binary file not shown.

BIN
dist/lwlt-newbooking-0.5.128.skill vendored Normal file

Binary file not shown.

BIN
dist/lwlt-updating-0.5.128.skill vendored Normal file

Binary file not shown.

View File

@@ -3,8 +3,8 @@
"generated_on": "2026-09-16",
"baselines": {
"chrome_extension": "0.5.186",
"skills": "0.5.127",
"business_instruction_docx": "0.5.134",
"skills": "0.5.128",
"business_instruction_docx": "0.5.135",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
},
"artifacts": [
@@ -18,51 +18,51 @@
{
"kind": "skill",
"name": "lwlt-arrangement",
"version": "0.5.127",
"path": "dist/lwlt-arrangement-0.5.127.skill",
"version": "0.5.128",
"path": "dist/lwlt-arrangement-0.5.128.skill",
"source": "agent设计规范/skills/lwlt-arrangement",
"sha256": "ae3fc66965d2a7968aeaa2ecf371d75e5365ef4cc4dfd8ef080f9343e0d0f09c"
},
{
"kind": "skill",
"name": "lwlt-confirmation",
"version": "0.5.127",
"path": "dist/lwlt-confirmation-0.5.127.skill",
"version": "0.5.128",
"path": "dist/lwlt-confirmation-0.5.128.skill",
"source": "agent设计规范/skills/lwlt-confirmation",
"sha256": "c2f3689d8a0b4d6b250bb2cfce572fe3a9fca3f5519ea5fae5ef0d382b44dae4"
},
{
"kind": "skill",
"name": "lwlt-lifecycle",
"version": "0.5.127",
"path": "dist/lwlt-lifecycle-0.5.127.skill",
"version": "0.5.128",
"path": "dist/lwlt-lifecycle-0.5.128.skill",
"source": "agent设计规范/skills/lwlt-lifecycle",
"sha256": "f0b5cdea11e65657fb9b10e4f7d020769d5477cc8ceddfcfd6a284513345f6bc"
"sha256": "fcf23bb0e12e7026f03dc3670fe35db77c740a9902a7a415e2efa464d4eceb1e"
},
{
"kind": "skill",
"name": "lwlt-newbooking",
"version": "0.5.127",
"path": "dist/lwlt-newbooking-0.5.127.skill",
"version": "0.5.128",
"path": "dist/lwlt-newbooking-0.5.128.skill",
"source": "agent设计规范/skills/lwlt-newbooking",
"sha256": "b64593d88145966cd4e702d2532ffe3ea85052e8d56fbe75d503a19ababa1715"
},
{
"kind": "skill",
"name": "lwlt-updating",
"version": "0.5.127",
"path": "dist/lwlt-updating-0.5.127.skill",
"version": "0.5.128",
"path": "dist/lwlt-updating-0.5.128.skill",
"source": "agent设计规范/skills/lwlt-updating",
"sha256": "54f10dd6bd6d2ddf47c700f1bca4b1fcd3346dc6143bf27118a23587b1b11736"
},
{
"kind": "business_instruction_docx",
"version": "0.5.134",
"path": "dist/老挝联泰AI指令表-0.5.134.docx",
"version": "0.5.135",
"path": "dist/老挝联泰AI指令表-0.5.135.docx",
"source": "agent设计规范/templates/business-input-templates.md",
"builder": "tools/build_business_instruction_docx.py",
"sha256": "b6c1f677b93cb4f7356cfb26c6af01860587be1288ffe9e8aff8295929074490",
"source_sha256": "7446e4560e2eaaf6f490c8ed7523d6aadcee6f94f58cb620f7e71db674404471",
"sha256": "55dcd03b7ddb43cf3333497faeddc105aff3d57729e06cff200a7e190cc95ea2",
"source_sha256": "a28ecf96a09c9c9dfdeed590ccff44999ecd019abd3f30102c98b6cbb66c5f62",
"builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1"
}
]

Binary file not shown.

View File

@@ -221,7 +221,7 @@
"passenger_list.marker is exactly TEST-202609 only for an explicit historical test context",
"every canonical row has implicit 证件类型 exactly 护照; any source 证件类型 column is ignored",
"each TSV row retains exactly 13 columns, including empty final phone or remark cells; normalization removes only boundary newlines and never trims tab delimiters",
"exactly one workbook row within the first 100 rows maps to the required ERP passenger fields; the header may be row 1 or follow metadata rows, source order is arbitrary, approved source/ERP aliases map to one semantic field each, and ordinary non-import columns are ignored and excluded from canonical output; contiguous roster data is read below the detected header",
"exactly one workbook row within the first 100 rows maps to the required ERP passenger fields; the header may be row 1 or follow metadata rows, source order is arbitrary, approved source/ERP aliases map to one semantic field each, and ordinary non-import columns are ignored and excluded from canonical output; contiguous roster data is read below the detected header; 电话 is the only optional source header, absent phones become empty canonical cells without guessing unheaded values, and a present phone column must be unique and pass the same selected-field validation",
"at most one row has trimmed 备注 exactly 领队; that row requires nonblank 姓名 and 电话 and must match passenger_list.leader_contact"
],
"native_defects": [

View File

@@ -304,7 +304,7 @@ test('single-source and generated-output boundaries remain explicit', async () =
const diagnoseServer = await readFile(path.join(ROOT, 'infra/diagnose-server.sh'), 'utf8');
const gitignore = await readFile(path.join(ROOT, '.gitignore'), 'utf8');
assert.match(template, /^交付版本0\.5\.134\s*$/mu);
assert.match(template, /^交付版本0\.5\.135\s*$/mu);
assert.match(template, /^适用范围:当前 23 条业务路由,按 19 项运营场景分组;执行能力以生命周期发布门槛为准。\s*$/mu);
assert.match(template, /^### 5\. 散拼团多个新增子单\s*$/mu);
assert.doesNotMatch(template, /^事实基线:/mu);