diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index 68a59f5..dfbc02d 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -97,7 +97,7 @@ const persistedExtensionResultVersions = new Map(); let taskCreateInProgress = false; const AUTO_HANDOFF_RETRY_MS = 30_000; -const REQUIRED_EXTENSION_VERSION = '0.5.164'; +const REQUIRED_EXTENSION_VERSION = '0.5.165'; const MANUAL_HANDOFF_LABEL = '确认并提交到 ERP 插件'; const RETRY_HANDOFF_LABEL = '继续提交到 ERP 插件'; const RECONCILE_LABEL = '只读回查 ERP 现有结果'; diff --git a/agent设计规范/businesses/shared_plan_create.md b/agent设计规范/businesses/shared_plan_create.md index b2f72ae..d56c988 100644 --- a/agent设计规范/businesses/shared_plan_create.md +++ b/agent设计规范/businesses/shared_plan_create.md @@ -69,7 +69,7 @@ - `planned_capacity=40` 是母团计划的计划收客数;它与可选的拼团实际人数不是同一个字段。 - `room_counts` 属于计划表整体字段;`split_order.customer` 与 `split_order.passenger_counts` 是 ERP 选填字段,可以独立提交,缺少哪一个就省略哪一个。 - 产品关键字可以是业务侧缩写,但必须保留原始 `keyword`。Skill 不写 ERP 全称、产品 ID、客户隐藏 ID 或团号。 -- `广东衡阳6D` 只是业务搜索关键词;产品名称和天数分开匹配。若当前散拼候选无法唯一解析,插件先用空条件重新加载候选,再本地匹配;如果最终 0 个或多个候选命中,必须提示用户并停止。拼团客户与产品分别唯一解析,来源地关键字不再作为两者之间的强制匹配门禁。 +- `广东衡阳6D` 只是业务搜索关键词;产品名称和天数分开匹配。若当前散拼候选无法唯一解析,插件先通过 `S_chanpinming` 发起原生非空产品名检索,再对返回候选做本地唯一匹配;尾部数字时长不发送给产品名检索,最后仅保留一次空条件兼容重载。如果最终 0 个或多个候选命中,必须提示用户并停止。拼团客户与产品分别唯一解析,来源地关键字不再作为两者之间的强制匹配门禁。 ## 2. ERP 操作系统流程 @@ -77,7 +77,7 @@ 1. 进入“散拼团计划表”,点击“新增计划”,打开 `/System/Business/plan_add.asp`。 2. 填写 `Riqi1=2026-09-01`、`Riqi2=2026-09-30`。 -3. 先在散拼团产品列表的产品名称/天数单元格中按 `广东衡阳6D` 做连续或有序唯一匹配并点选 `cp_id`。若当前候选无法解析,不把复合缩写直接提交给 `S_chanpinming`,先用空条件重新加载候选后再本地匹配。点选后的 `set_th` 必须自动带入团号前缀/后缀。 +3. 先在散拼团产品列表的产品名称/天数单元格中按 `广东衡阳6D` 做连续或有序唯一匹配并点选 `cp_id`。若当前候选无法解析,调用 `S_chanpinming` 的原生非空检索:完整产品名原样搜索,`10D`/`8天` 等尾部数字时长先剥离后只搜索产品名;再对返回候选本地唯一匹配,必要时以空条件做一次兼容重载。点选后的 `set_th` 必须自动带入团号前缀/后缀。 4. 填写 `jihuashu=40`。 5. 填写房型:`frenshu0=1`(SGL)、`frenshu1=8`(TWN),其余明确未提供房型为 0;调用 `sum_fang()`,确认 `frenshu6=9`。 6. 调用 `ShowFoucsBox()` 生成日期枚举,只勾选 `2026-09-07`、`2026-09-15`、`2026-09-26` 各一个 `zhidingzhouqi` 节点。 @@ -90,7 +90,7 @@ | 用户/系统事实 | ERP 字段 | |---|---| | 发团日期范围 | `Riqi1`, `Riqi2` | -| 产品搜索关键字 | `S_chanpinming`;复合产品名+天数关键字不直接提交,必要时空条件重载后本地匹配 | +| 产品搜索关键字 | `S_chanpinming`;先原生检索完整产品名或剥离尾部数字时长后的产品名,再本地唯一匹配;必要时空条件兼容重载 | | 产品选择 | `cp_id` | | 计划收客数 | `jihuashu` | | 房型 SGL/TWN/TRP/DBL | `frenshu0`/`frenshu1`/`frenshu2`/`frenshu3` | diff --git a/agent设计规范/businesses/team_order_batch_create.md b/agent设计规范/businesses/team_order_batch_create.md index 5776383..12c77f2 100644 --- a/agent设计规范/businesses/team_order_batch_create.md +++ b/agent设计规范/businesses/team_order_batch_create.md @@ -97,7 +97,7 @@ 1. 在已登录的 ERP 主页面进入独立团计划表,打开“批量下单”功能,进入 `/System/Business/orders_adds.asp`。 2. 填写发团日期范围,对应页面字段 `Riqi1`、`Riqi2`;范围用于生成可选日期集合。 3. 在预订客户字段 `zutuanshe` 中使用用户关键词匹配当前 SelectBox 数据集的可搜索字段。必须唯一匹配后通过页面自身映射点选,不能把关键词当作最终客户全称或隐藏 ID。 -4. 在当前产品候选中按用户关键词匹配产品名和天数并点选唯一 `cp_id`;当前候选无法唯一解析时以空条件重载候选,再在插件本地匹配,不能把“产品名+天数”复合关键字直接发送给 ERP 的 `S_chanpinming`。点选后执行页面产品联动,等待团号设置等必填字段自动带入;不能自行生成团号前后缀。 +4. 在当前产品候选中按用户关键词匹配产品名和天数并点选唯一 `cp_id`;当前候选无法唯一解析时,通过 `S_chanpinming` 发起原生非空检索:完整产品名原样搜索,尾部数字时长先剥离后只搜索产品名。插件再对返回候选做本地唯一匹配,必要时以空条件做一次兼容重载。点选后执行页面产品联动,等待团号设置等必填字段自动带入;不能自行生成团号前后缀。 5. 填写预估人数,对应成人和领队/工作人员数量字段(当前页面为 `darenshu`、`frenshu1`)。 6. 填写用房数量,只提交明确的房型数量,例如 `TWN=8`、`SGL=1`;不能把总房间数当作房型分配。 7. 在发团周期中选择实际日期:显式日期通过 `zhidingzhouqi` 勾选;星期、单日/双日等表达通过页面旁的快捷标签转换后批量勾选。所有选中日期必须落在发团日期范围内。 diff --git a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md index 9dbc77c..5cee395 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.164` +- Chrome 插件:`0.5.165` - Agent Prompt:`ltjt-agent-prompt-v1.8-independent-headcount-categories` - 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08` - 五个 Skill:`0.5.125`;运营 DOCX:`0.5.125` -- 发布状态:核心窄能力已有真实证据;一笔名单附件 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 任务默认不执行。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。 +- 发布状态:核心窄能力已有真实证据;一笔名单附件 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` 让散拼新增计划和独立团批量下单在本地候选未命中时先使用表单原生非空产品检索,实测两类表单均能由完整关键词唯一返回目标产品;本轮未勾选或保存,自动选择与写入仍由原门禁保护。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。 运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。 diff --git a/archive/releases/2026-09-02/README.md b/archive/releases/2026-09-02/README.md index f34bff3..9ee04a9 100644 --- a/archive/releases/2026-09-02/README.md +++ b/archive/releases/2026-09-02/README.md @@ -4,3 +4,5 @@ - `release-manifest-0.5.163.json`:引入账号、渠道、云电脑 worker 与 ERP 身份一致性门禁前的历史清单快照。 - `ltjt-order-assistant-0.5.163.zip`:被 `0.5.164` 的员工账号路由和 ERP 身份核验替代;保留用于恢复和追溯。 +- `release-manifest-0.5.164.json`:启用表单原生非空产品检索前的历史清单快照。 +- `ltjt-order-assistant-0.5.164.zip`:被 `0.5.165` 的散拼新增计划/独立团批量下单产品检索修复替代;保留用于恢复和追溯。 diff --git a/dist/ltjt-order-assistant-0.5.164.zip b/archive/releases/2026-09-02/ltjt-order-assistant-0.5.164.zip similarity index 100% rename from dist/ltjt-order-assistant-0.5.164.zip rename to archive/releases/2026-09-02/ltjt-order-assistant-0.5.164.zip diff --git a/archive/releases/2026-09-02/release-manifest-0.5.164.json b/archive/releases/2026-09-02/release-manifest-0.5.164.json new file mode 100644 index 0000000..40194d7 --- /dev/null +++ b/archive/releases/2026-09-02/release-manifest-0.5.164.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-09-02", + "baselines": { + "chrome_extension": "0.5.164", + "skills": "0.5.125", + "business_instruction_docx": "0.5.125", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.164", + "path": "dist/ltjt-order-assistant-0.5.164.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "5a59b616aa7ea2cfcc02b2c3242bfc0b424dcd6ddd28fff7ed9bcfb5b367191a" + }, + { + "kind": "skill", + "name": "lwlt-arrangement", + "version": "0.5.125", + "path": "dist/lwlt-arrangement-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-arrangement", + "sha256": "c729bebec84695b8642a6d31cb4dc0d963135b6c6620c2173be095826cd3976d" + }, + { + "kind": "skill", + "name": "lwlt-confirmation", + "version": "0.5.125", + "path": "dist/lwlt-confirmation-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-confirmation", + "sha256": "659f6aca7918a03a29fc78f0b4b938dfa9084ccdbb44ebd71f6399556cfa4609" + }, + { + "kind": "skill", + "name": "lwlt-lifecycle", + "version": "0.5.125", + "path": "dist/lwlt-lifecycle-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-lifecycle", + "sha256": "d964c0a9e482f82757fea25d4713f0c81feb2704486583dfa84f2336ecd930c0" + }, + { + "kind": "skill", + "name": "lwlt-newbooking", + "version": "0.5.125", + "path": "dist/lwlt-newbooking-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-newbooking", + "sha256": "bb610c50dd659fc172aabfa469b3028de98704c249cfd4890463deea0c88cd0c" + }, + { + "kind": "skill", + "name": "lwlt-updating", + "version": "0.5.125", + "path": "dist/lwlt-updating-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-updating", + "sha256": "c6f5bc9008e2f63d170f3f140140003d99411ce5232632c48b38891da1ceee33" + }, + { + "kind": "business_instruction_docx", + "version": "0.5.125", + "path": "dist/老挝联泰AI指令表-0.5.125.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "a0c2fd5f808e3d3f2ac211a652a59f2e435d3824df49c51f763ca4beb7d9b97d", + "source_sha256": "27600fe9bcb0de3898a99550428a9890dd350c701ba704fb3940ca966088dcd9", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/chrome-extension/ltjt-order-assistant/README.md b/chrome-extension/ltjt-order-assistant/README.md index 7515c86..f09b077 100644 --- a/chrome-extension/ltjt-order-assistant/README.md +++ b/chrome-extension/ltjt-order-assistant/README.md @@ -11,9 +11,11 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过 ## 当前版本 -当前源码版本为 `0.5.164`。版本化 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.165`。版本化 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.164 当前重点: +0.5.165 当前重点: + +- 散拼新增计划和独立团批量下单在已加载候选无法唯一匹配时,会真正触发 `S_chanpinming` 的原生非空检索并等待 Ajax 返回,再对产品名/时长做本地唯一匹配。完整产品名原样查询;只有尾部 `10D`、`8天` 等数字时长会先剥离。最后仍保留一次空条件兼容重载,零候选或多候选继续在写前阻断。 - 平台会把当前员工账号绑定的 ERP 账号随只读 PING 交给扩展核验;扩展只返回是否匹配,不回传页面中的其他账号文本。 - 只有平台确认的唯一在线云电脑、匹配的 ERP 登录身份和任务归属账号三者一致时,任务才可领取或回传结果。 diff --git a/chrome-extension/ltjt-order-assistant/inpage.js b/chrome-extension/ltjt-order-assistant/inpage.js index 5978c26..d0cd1a4 100644 --- a/chrome-extension/ltjt-order-assistant/inpage.js +++ b/chrome-extension/ltjt-order-assistant/inpage.js @@ -920,6 +920,15 @@ return { matched: false, rule: 'product_lookup_helper_missing' }; } + function nativeProductSearchQueries(query) { + const helper = window.LTJTProductLookup; + if (typeof helper?.nativeProductSearchQueries === 'function') { + return helper.nativeProductSearchQueries(query); + } + const keyword = String(query || '').trim(); + return keyword ? [keyword, ''] : ['']; + } + function parseErpResponseText(value) { return String(value || '') .replace(//gi, '\n') @@ -1117,17 +1126,16 @@ .join('|'); } - async function reloadProductsForLocalMatch(scopeDocument, productSearchInput, keyword) { + async function reloadProductsForLocalMatch(scopeDocument, productSearchInput, keyword, nativeQuery = '') { const previousSignature = productCandidateSignature(scopeDocument); const ownerWindow = scopeDocument.defaultView || window; const jq = ownerWindow.jQuery || ownerWindow.$; const startedAt = Date.now(); - // The ERP endpoint can search only a contiguous product-name fragment. - // An input such as “江西老挝10D” is a business shorthand composed of a - // product name and a duration; sending it to S_chanpinming makes the ERP - // SQL LIKE filter return zero rows. Reload with an empty native query, - // then resolve the shorthand against the returned candidate fields. - setDomField(scopeDocument, 'S_chanpinming', '', { optional: true }); + // Dispatch the same blur-driven AjaxLoadData(true) flow used by the ERP + // form. The shared lookup helper removes only a trailing numeric duration + // shorthand; complete product names, including Chinese words such as + // “八天”, are sent unchanged. + setDomField(scopeDocument, 'S_chanpinming', nativeQuery, { optional: true }); const refreshed = await waitForCondition(() => { const currentSignature = productCandidateSignature(scopeDocument); const active = jq && Number.isFinite(Number(jq.active)) ? Number(jq.active) : 0; @@ -1139,8 +1147,8 @@ }, 8000, 200); productSearchInput.value = String(keyword || ''); return { - strategy: 'native_empty_then_local', - native_query: '', + strategy: nativeQuery ? 'native_query_then_local' : 'native_empty_then_local', + native_query: nativeQuery, previous_candidate_count: previousSignature ? previousSignature.split('|').length : 0, refreshed: Boolean(refreshed), candidate_count: scopeDocument.querySelectorAll('input[name="cp_id"], input[name="cpid"]').length @@ -3898,20 +3906,24 @@ setDomField(formDocument, 'Riqi2', dateEnd); setDomField(formDocument, 'jihuashu', data.planned_capacity); - // The ERP search input fires AjaxLoadData(true) on blur. A shorthand such - // as "老挝广东8D" is intentionally matched against the loaded product - // name/duration fields first. If the current table cannot resolve it, - // reload the broad ERP candidate set with an empty native query and keep - // the shorthand matching local; never send a compound name+duration token - // to the ERP's contiguous S_chanpinming LIKE filter. + // Match the currently loaded candidates first. If they do not contain a + // unique result, invoke the ERP's native non-empty product-name search and + // match the returned name/duration fields locally. A trailing numeric + // duration shorthand is excluded from the native LIKE query; a bounded + // empty-query reload remains the final compatibility fallback. const productKeyword = data.product.keyword || data.product.name; const productSearchInput = formDocument.querySelector('#S_chanpinming, [name="S_chanpinming"]'); if (productSearchInput) productSearchInput.value = String(productKeyword || ''); let product = selectProductRadio(formDocument, data); let productSearch = { strategy: 'loaded_candidates', native_query: null, refreshed: false }; + const productSearchAttempts = []; if (!product.ok && !data.product?.cpid && !data.product?.id && productSearchInput) { - productSearch = await reloadProductsForLocalMatch(formDocument, productSearchInput, productKeyword); - product = selectProductRadio(formDocument, data); + for (const nativeQuery of nativeProductSearchQueries(productKeyword)) { + productSearch = await reloadProductsForLocalMatch(formDocument, productSearchInput, productKeyword, nativeQuery); + productSearchAttempts.push(productSearch); + product = selectProductRadio(formDocument, data); + if (product.ok) break; + } } lookupChecks.push({ name: 'product', @@ -3923,7 +3935,9 @@ search_strategy: productSearch.strategy, native_query: productSearch.native_query, refreshed: productSearch.refreshed, - refreshed_candidate_count: productSearch.candidate_count + refreshed_candidate_count: productSearch.candidate_count, + search_attempt_count: productSearchAttempts.length, + search_attempt_strategies: productSearchAttempts.map((attempt) => attempt.strategy) }); if (!product.ok) { closeDialogDocument(formDocument); @@ -6077,7 +6091,7 @@ } window.LTJTOrderAssistant = { - version: '0.5.164', + version: '0.5.165', resolveNativeListSearchValues, lookupKeywordMatchesText, inspectLifecycleSearchCriteria: lifecycleSearchCriteria, diff --git a/chrome-extension/ltjt-order-assistant/manifest.json b/chrome-extension/ltjt-order-assistant/manifest.json index 181ccce..1ff1c19 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.164", + "version": "0.5.165", "description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。", "permissions": [ "activeTab", diff --git a/chrome-extension/ltjt-order-assistant/product-lookup.js b/chrome-extension/ltjt-order-assistant/product-lookup.js index d38189a..f076a06 100644 --- a/chrome-extension/ltjt-order-assistant/product-lookup.js +++ b/chrome-extension/ltjt-order-assistant/product-lookup.js @@ -62,6 +62,21 @@ }; } + function nativeProductSearchQueries(query) { + const keyword = collapseWhitespace(query); + if (!keyword) return ['']; + + // S_chanpinming is a contiguous product-name LIKE filter; duration is a + // separate ERP field. Preserve a complete product name verbatim, but trim + // a trailing Arabic duration shorthand before issuing the native query. + // Keep one bounded empty-query fallback for legacy shorthand whose name + // fragments are not contiguous in the ERP product name. + const productNameQuery = keyword + .replace(/\s*\d+(?:\.\d+)?\s*(?:D|天)(?:\s*\d+(?:\.\d+)?\s*(?:N|晚|夜))?\s*$/iu, '') + .trim(); + return productNameQuery ? [productNameQuery, ''] : ['']; + } + function productCandidateFromRow(row, input = null) { const label = productLabelFromRow(row); const parts = productNameAndDuration(label); @@ -142,6 +157,7 @@ isOrderedSubsequence, productLabelFromRow, productNameAndDuration, + nativeProductSearchQueries, productCandidateFromRow, productQueryTokens, productQueryMatch, diff --git a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js index d485b3c..26784a1 100644 --- a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js +++ b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js @@ -279,14 +279,25 @@ }; } - async function reloadProductsForLocalMatch(form, input, keyword) { + function nativeProductSearchQueries(keyword) { + if (typeof productLookup.nativeProductSearchQueries === 'function') { + return productLookup.nativeProductSearchQueries(keyword); + } + const normalized = String(keyword || '').trim(); + return normalized ? [normalized, ''] : ['']; + } + + async function reloadProductsForLocalMatch(form, input, keyword, nativeQuery = '') { const previousSignature = productCandidates(form) .map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`) .join('|'); const ownerWindow = form.ownerDocument.defaultView || window; const jq = ownerWindow.jQuery || ownerWindow.$; const startedAt = Date.now(); - setField(form, 'S_chanpinming', ''); + // Trigger the form's native blur-driven AjaxLoadData(true) search. The + // shared helper preserves complete product names and removes only a + // trailing numeric duration shorthand from this contiguous LIKE query. + setField(form, 'S_chanpinming', nativeQuery); const candidates = await waitFor(() => { const current = productCandidates(form); const signature = current @@ -302,8 +313,8 @@ input.value = String(keyword || ''); return { candidates, - strategy: 'native_empty_then_local', - native_query: '', + strategy: nativeQuery ? 'native_query_then_local' : 'native_empty_then_local', + native_query: nativeQuery, previous_candidate_count: previousSignature ? previousSignature.split('|').length : 0, refreshed: candidates.length > 0 && candidates .map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`) @@ -320,14 +331,24 @@ input.value = keyword; let candidates = await waitForProductList(form); let search = { strategy: 'loaded_candidates', native_query: null, refreshed: false }; + const searchAttempts = []; let result = resolveProductCandidates(candidates, keyword); if (!result.ok) { if (typeof ownerWindow.AjaxLoadData !== 'function') return { ...result, blocker: result.blocker || 'ERP 产品 AjaxLoadData 函数不存在。' }; - search = await reloadProductsForLocalMatch(form, input, keyword); - candidates = search.candidates; - result = resolveProductCandidates(candidates, keyword); + for (const nativeQuery of nativeProductSearchQueries(keyword)) { + search = await reloadProductsForLocalMatch(form, input, keyword, nativeQuery); + searchAttempts.push(search); + candidates = search.candidates; + result = resolveProductCandidates(candidates, keyword); + if (result.ok) break; + } } - if (!result.ok) return { ...result, ...search }; + if (!result.ok) return { + ...result, + ...search, + search_attempt_count: searchAttempts.length, + search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy) + }; const selected = result.candidate.input; selected.click(); selected.checked = true; @@ -342,6 +363,8 @@ search_strategy: search.strategy, native_query: search.native_query, refreshed: search.refreshed, + search_attempt_count: searchAttempts.length, + search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy), selected_label_length: String(result.candidate.label || '').length, selected_value_present: Boolean(String(selected.value || '').trim()), group_prefix_present: Boolean(fieldValue(form, 'tuanxuhao1').trim()), @@ -613,7 +636,9 @@ search_strategy: productResult.search_strategy || 'unknown', native_query: productResult.native_query, refreshed: productResult.refreshed === true, - candidate_count: productResult.candidate_count || 0 + candidate_count: productResult.candidate_count || 0, + search_attempt_count: productResult.search_attempt_count || 0, + search_attempt_strategies: productResult.search_attempt_strategies || [] }); if (!productResult.ok) blockers.push(productResult.blocker || '业务产品未能唯一匹配。'); const requestedOrderSuffix = String(data.order_number?.suffix || '').trim(); @@ -919,7 +944,7 @@ window.LTJTOrderAssistant = { ...(window.LTJTOrderAssistant || {}), - version: '0.5.164', + version: '0.5.165', openTeamBatchForm, pingTeamBatchFrame, preflightTeamBatchNative, diff --git a/dist/ltjt-order-assistant-0.5.165.zip b/dist/ltjt-order-assistant-0.5.165.zip new file mode 100644 index 0000000..4e7a3de Binary files /dev/null and b/dist/ltjt-order-assistant-0.5.165.zip differ diff --git a/dist/release-manifest.json b/dist/release-manifest.json index 40194d7..bfdd98d 100644 --- a/dist/release-manifest.json +++ b/dist/release-manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "generated_on": "2026-09-02", "baselines": { - "chrome_extension": "0.5.164", + "chrome_extension": "0.5.165", "skills": "0.5.125", "business_instruction_docx": "0.5.125", "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" @@ -10,10 +10,10 @@ "artifacts": [ { "kind": "chrome_extension", - "version": "0.5.164", - "path": "dist/ltjt-order-assistant-0.5.164.zip", + "version": "0.5.165", + "path": "dist/ltjt-order-assistant-0.5.165.zip", "source": "chrome-extension/ltjt-order-assistant", - "sha256": "5a59b616aa7ea2cfcc02b2c3242bfc0b424dcd6ddd28fff7ed9bcfb5b367191a" + "sha256": "14f3150ab26d99131e32321ddc805a85550438bc2810a1e20921f7922b25aaac" }, { "kind": "skill", diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json index 9c282c9..3cd3c15 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.164", + "current_extension_version": "0.5.165", "historical_test_marker": "TEST-202609", "scope": "/System/Business/", "erp_session_keepalive": { diff --git a/mappings/orders_adds.mapping.json b/mappings/orders_adds.mapping.json index da25746..5ee9125 100644 --- a/mappings/orders_adds.mapping.json +++ b/mappings/orders_adds.mapping.json @@ -1,7 +1,7 @@ { "$schema": "https://local.lwltapi/schemas/ltjt_form_mapping.schema.json", - "mapping_version": "2026-08-11.team_order_batch_create.orders_adds.v0.2", - "status": "live_batch_requery_and_suffix_override_repeatedly_validated_0.5.81_candidate", + "mapping_version": "2026-09-02.team_order_batch_create.orders_adds.v0.3", + "status": "native_nonempty_product_search_live_verified_release_gated_0.5.165", "source": { "schema": "schemas/standard_system_operation.schema.json", "action": "team_order_batch_create" @@ -20,7 +20,7 @@ { "step": 1, "operation": "open_native_batch_entry", "description": "在独立团订单列表打开批量下单功能。" }, { "step": 2, "operation": "set_date_range", "source": "data.recurrence.start_date/end_date", "targets": ["Riqi1", "Riqi2"] }, { "step": 3, "operation": "resolve_booking_customer", "source": "data.customer.keyword || data.customer.name", "target": "zutuanshe", "setval_targets": ["zutuansheid", "zutuanshe", "lianxiren", "zutuanshegzr"], "rule": "exact/unique normalized matching first; if those fail, all recognizable source-region tokens may select only one current ERP customer name", "unique_required": true }, - { "step": 4, "operation": "resolve_and_select_product", "source": "data.product.keyword || data.product.name", "search_target": "S_chanpinming", "radio_target": "cp_id", "side_effect": "page set_th fills tuanxuhao1/tuanxuhao2", "unique_required": true }, + { "step": 4, "operation": "resolve_and_select_product", "source": "data.product.keyword || data.product.name", "search_target": "S_chanpinming", "radio_target": "cp_id", "side_effect": "page set_th fills tuanxuhao1/tuanxuhao2", "unique_required": true, "fallback": "match loaded candidates first; if unresolved, issue the native non-empty product-name query after stripping only a trailing numeric duration, then match locally; retain one empty-query compatibility reload; never choose the first row" }, { "step": 5, "operation": "conditionally_apply_explicit_order_suffix", "source": "data.order_number.suffix", "target": "tuanxuhao2", "rule": "ordinary Agent output leaves this absent; when a host/business system explicitly supplies it, apply only after product set_th and require exact persistence before payload approval" }, { "step": 6, "operation": "record_source_region_context", "source": "the final uniquely resolved ERP product/customer candidate names plus optional source_region context", "rule": "source-region context is report-only; product and customer candidates are resolved independently and a mismatch never blocks DoInfoJHs" }, { "step": 7, "operation": "set_passenger_counts", "targets": ["darenshu", "xiaorenshu", "ertrenshu", "yingrenshu", "quanrenshu"], "side_effect": "GetVisitorsHtml()" }, @@ -42,7 +42,8 @@ "product": { "search_field": "S_chanpinming", "radio_name": "cp_id", - "match_order": ["exact_row_text", "unique_contiguous", "unique_tokenized_contains"], + "match_order": ["unique_contiguous", "unique_name_duration_tokens", "unique_ordered_tokens"], + "native_query_policy": "submit the complete product name unchanged, or strip a trailing numeric duration shorthand before querying; await native Ajax and match returned candidates locally; use one empty query only as the final compatibility reload", "ambiguous_or_empty": "block", "context": "customer selection must be applied before product query" }, diff --git a/mappings/plan_add.mapping.json b/mappings/plan_add.mapping.json index 74ff1dc..ecb4cb9 100644 --- a/mappings/plan_add.mapping.json +++ b/mappings/plan_add.mapping.json @@ -1,7 +1,7 @@ { "$schema": "https://local.lwltapi/schemas/ltjt_form_mapping.schema.json", - "mapping_version": "2026-08-18.shared_plan_create.plan_add.v0.8", - "status": "multi_date_split_facts_repeated_live_validated_test_context_release_gated_0.5.81", + "mapping_version": "2026-09-02.shared_plan_create.plan_add.v0.9", + "status": "native_nonempty_product_search_live_verified_release_gated_0.5.165", "source": { "schema": "schemas/standard_system_operation.schema.json", "action": "shared_plan_create" @@ -18,7 +18,7 @@ }, "ordered_steps": [ { "step": 1, "operation": "set_date_range", "source": "data.recurrence.start_date/end_date", "targets": ["Riqi1", "Riqi2"] }, - { "step": 2, "operation": "resolve_and_select_product", "source": "data.product.keyword || data.product.name", "search_target": "S_chanpinming", "radio_target": "cp_id", "side_effect": "set_th fills tuanxuhao1/tuanxuhao2", "unique_required": true, "fallback": "match loaded candidates by product-name/duration fields; if unresolved, reload with an empty native query and match locally; never send a compound name+duration keyword or choose the first row" }, + { "step": 2, "operation": "resolve_and_select_product", "source": "data.product.keyword || data.product.name", "search_target": "S_chanpinming", "radio_target": "cp_id", "side_effect": "set_th fills tuanxuhao1/tuanxuhao2", "unique_required": true, "fallback": "match loaded candidates first; if unresolved, issue the native non-empty product-name query and match the returned name/duration fields locally; strip only a trailing numeric duration before the native query; retain one empty-query compatibility reload; never choose the first row" }, { "step": 3, "operation": "conditionally_apply_explicit_order_suffix", "source": "data.order_number.suffix", "target": "tuanxuhao2", "rule": "ordinary Agent output leaves this absent; when a host/business system explicitly supplies it, apply only after product set_th and require exact persistence before payload approval" }, { "step": 4, "operation": "set_planned_capacity", "source": "data.planned_capacity", "target": "jihuashu" }, { "step": 5, "operation": "set_room_counts", "source": "data.room_counts", "targets": ["frenshu0", "frenshu1", "frenshu2", "frenshu3", "frenshu4", "frenshu5", "frenshu6"], "side_effect": "sum_fang()" }, @@ -35,7 +35,7 @@ "radio_name": "cp_id", "match_order": ["unique_contiguous", "unique_name_duration_tokens", "unique_ordered_tokens"], "label_scope": "product-name-and-duration cell, not full remarks", - "native_query_policy": "S_chanpinming supports contiguous product-name LIKE only; use empty query for broad reload when a compound keyword cannot be resolved", + "native_query_policy": "S_chanpinming supports contiguous product-name LIKE only; submit the complete product name unchanged, or strip a trailing numeric duration shorthand before querying; await native Ajax and match returned candidates locally; use one empty query only as the final compatibility reload", "ambiguous_or_empty": "block" }, "customer": { diff --git a/tools/lifecycle-contract.test.mjs b/tools/lifecycle-contract.test.mjs index cec9471..13052bc 100644 --- a/tools/lifecycle-contract.test.mjs +++ b/tools/lifecycle-contract.test.mjs @@ -1296,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\.164'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.165'/); }); test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => { @@ -1926,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.164'); + assert.equal(mapping.current_extension_version, '0.5.165'); assert.equal( mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'], 'ertrenshu' @@ -2309,10 +2309,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.164'); - assert.match(inpage, /version: '0\.5\.164'/); - assert.match(teamBatchInpage, /version: '0\.5\.164'/); - assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.164'/); + assert.equal(extensionManifest.version, '0.5.165'); + assert.match(inpage, /version: '0\.5\.165'/); + assert.match(teamBatchInpage, /version: '0\.5\.165'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.165'/); 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/product-lookup.test.mjs b/tools/product-lookup.test.mjs index 3c84e32..a957a6d 100644 --- a/tools/product-lookup.test.mjs +++ b/tools/product-lookup.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; +import { readFileSync } from 'node:fs'; const require = createRequire(import.meta.url); const lookup = require('../chrome-extension/ltjt-order-assistant/product-lookup.js'); @@ -36,6 +37,38 @@ test('preserves numeric product names while matching a duration suffix', () => { assert.equal(result.rule, 'unique_contiguous'); }); +test('uses the complete ERP product name as the first native search query', () => { + const keyword = '全国散--八天--版纳动车拉邦进万象出'; + assert.deepEqual(lookup.nativeProductSearchQueries(keyword), [keyword, '']); + const matches = lookup.matchProductCandidates([ + candidate('live-target', '全国散--八天--版纳动车拉邦进万象出 8D7N') + ], keyword); + assert.equal(matches.length, 1); + assert.equal(matches[0].cpid, 'live-target'); +}); + +test('removes only a trailing numeric duration from the native name query', () => { + assert.deepEqual(lookup.nativeProductSearchQueries('江西老挝10D'), ['江西老挝', '']); + assert.deepEqual(lookup.nativeProductSearchQueries('老挝广东8天'), ['老挝广东', '']); + assert.deepEqual( + lookup.nativeProductSearchQueries('全国散--八天--版纳动车拉邦进万象出 8D7N'), + ['全国散--八天--版纳动车拉邦进万象出', ''] + ); + assert.deepEqual(lookup.nativeProductSearchQueries('尊享老挝3.10'), ['尊享老挝3.10', '']); +}); + +test('both S_chanpinming order adapters execute the planned native queries', () => { + const sources = [ + readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'), + readFileSync(new URL('../chrome-extension/ltjt-order-assistant/team-batch-inpage.js', import.meta.url), 'utf8') + ]; + for (const source of sources) { + assert.match(source, /for \(const nativeQuery of nativeProductSearchQueries\(/u); + assert.match(source, /reloadProductsForLocalMatch\([^\n]+nativeQuery\)/u); + assert.match(source, /native_query_then_local/u); + } +}); + test('keeps unique-match safety when multiple candidates satisfy a shorthand', () => { const matches = lookup.matchProductCandidates([ candidate('401', '老挝广东 8D7N'),