From a7c9cc358a7a949da409e056dd7d22caf1c204ba Mon Sep 17 00:00:00 2001 From: andy Date: Tue, 22 Sep 2026 18:44:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=9E=E6=89=A7=E6=8E=92?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20260922-wechat-receipt-layout-01a0858b.md | 29 +++++++++ agent设计规范/agentbus-reply-contract.md | 3 + .../wechat-receipt-layout-01a0858b/README.md | 19 ++++++ .../receipt-preview.txt | 63 +++++++++++++++++++ control-plane/src/reply-contract.ts | 34 ++++++---- control-plane/src/team-progress.ts | 39 +++++++----- control-plane/test/reply-contract.test.ts | 17 ++--- 7 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260922-wechat-receipt-layout-01a0858b.md create mode 100644 archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/README.md create mode 100644 archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/receipt-preview.txt diff --git a/.project-docs/30-worklog/tasks/20260922-wechat-receipt-layout-01a0858b.md b/.project-docs/30-worklog/tasks/20260922-wechat-receipt-layout-01a0858b.md new file mode 100644 index 0000000..f3880a1 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260922-wechat-receipt-layout-01a0858b.md @@ -0,0 +1,29 @@ +# 微信回执文本分段 + +## Scope / 授权 + +- 用户要求回执增加空格和换行,适合微信聊天框阅读。 +- 调整成功回执、团队进度和安排明细的纯文本排版;不改变业务记录、账号归属、ERP 写入或投递时机。 +- 不部署、不重启、不修改插件或外发真实消息。单线程实施。 + +## Worktree Gate + +- Branch: main;Worktree: /Users/andy/IdeaProjects/LWLT-AIBOT。 +- Base: 1797c713af179e33364ac22ce2956a8d232aa06c;唯一 worktree。 +- 既有未跟踪 `.idea/`、`dist/ltjt-order-assistant-0.5.189.zip` 保留,源码无未提交修改。 +- Overlap: Clear;Feature 模式,仅写本任务项目记忆。 + +## Progress + +- 已完成:本次操作标题与正文留空行,多团号/子单号/文件逐行缩进;酒店英文和中文模板之间留空行。 +- 团队基本信息、办理进度、每个子单、每类安排独立分段;记录时间单独一行,修改字段与各条安排字段逐行显示,不再通过竖线挤成一行。 +- 安排名细仅资源名称已知时仍显示该名称;完全缺失明细继续显示“明细未记录”。其余业务值、统计规则、附件、投递时机不变。 +- 保持纯文本,平台与 AgentBus 共用格式。更新既有回执测试的期望空行;未新增排版专用测试。 +- 未更新插件、数据库迁移或发布包,未部署、重启或发送真实微信消息。 + +## Validation / Promotion Candidates + +- `check`、`build` 通过;控制面 293/293、旧功能 439/439 通过。 +- 工作树 `check:repo` 8/10,失败仅为既有 `.idea/`、`.DS_Store` 和被忽略的历史日志,原文件均保留。当前源码独立快照 10/10 通过。 +- 使用合成数据生成并检查完整纯文本回执,段落、字段、多个安排均清晰分开;未进行真实微信外发测试。[验证与样例](../../../archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/README.md)。 +- Promotion:后续 Integration 可在回执展示相关 canonical 说明中加入“纯文本段落、逐字段换行与缩进”约定;业务与架构规则不变,无待决产品问题。 diff --git a/agent设计规范/agentbus-reply-contract.md b/agent设计规范/agentbus-reply-contract.md index 4cc4fef..364f8f4 100644 --- a/agent设计规范/agentbus-reply-contract.md +++ b/agent设计规范/agentbus-reply-contract.md @@ -88,6 +88,8 @@ 平台保存完整回执。AgentBus 最终文本超过现有 20,000 字符限制时,按行截取并明确提示“完整内容请在平台任务回执中查看”,不静默截断;阶段消息和附件交付规则不变。 +微信回执使用纯文本分段:本次结果标题与正文之间、办理进度与安排明细之间、各子单及各类安排之间留空行。多团号、多文件和多条安排逐条换行,安排字段及记录时间缩进显示;不依赖 Markdown 表格、HTML 或空格对齐列。排版不改变字段值、统计范围或附件内容。 + ## 酒店安排客户模板 酒店新增成功且执行结果包含完整酒店字段时,使用以下格式。变量全部由真实回查结果填充;英文和中文日期使用同一组实际日期。 @@ -98,6 +100,7 @@ GROUP CODE: {group_code} IN. {in_date_dd_mm_yyyy} OUT {out_date_dd_mm_yyyy} {nights}Night(s). {room_count}{room_type_en} + ②新订单 团号:{group_code}{customer_suffix} 入住日期:{in_date_yyyy_mm_dd_with_dots} diff --git a/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/README.md b/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/README.md new file mode 100644 index 0000000..8349011 --- /dev/null +++ b/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/README.md @@ -0,0 +1,19 @@ +# 微信回执排版验证 + +日期:2026-09-22。此目录仅保存历史验证证据,不定义当前业务规则。 + +当前规则:[AgentBus 用户回复契约](../../../../agent设计规范/agentbus-reply-contract.md)。 + +## 验证结果 + +- `node --run check`、`node --run build`:通过。 +- `node --run test:control-plane`:293/293 通过;既有精确文本断言同步新的空行。 +- `node --run test:legacy`:439/439 通过;在批准的本机临时 HTTP 测试环境执行,无 ERP 或外部发送。 +- `node --run check:repo`:工作树受原有 IDE 元数据、`.DS_Store` 和被忽略历史日志影响为 8/10;未删除这些文件,当前源码独立快照为 10/10。 +- `git diff --check`:通过。 + +## 文本样例 + +[完整样例回执](receipt-preview.txt) 由当前回执构造函数生成,全部内容为合成数据;已检查酒店双语段落、办理进度、记录时间、修改字段及两条酒店安排的分段与缩进。仅资源名称已知时不再隐藏名称,其他字段和统计语义不变。 + +不使用 Markdown 表格、HTML 或空格对齐列;通过真实换行和空行分段。微信客户端会自行换行,未进行真实微信发送或截图验收。本次仅修改控制面文本渲染和契约,插件、数据库 Schema、ERP 执行、附件与投递时机未改动。 diff --git a/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/receipt-preview.txt b/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/receipt-preview.txt new file mode 100644 index 0000000..26b0ea5 --- /dev/null +++ b/archive/evidence/2026-09-22/wechat-receipt-layout-01a0858b/receipt-preview.txt @@ -0,0 +1,63 @@ +①NEW BOOKING +GROUP CODE: LW-SAMPLE-A +IN. 01/10/2026 +OUT 02/10/2026 +1Night. 10Twin + +②新订单 +团号:LW-SAMPLE-A +入住日期:2026.10.01 +退房日期:2026.10.02 +几 晚:1晚 +房型/房数:标间 10间 + +本系统团队办理进度(当前账号) + +团号:LW-SAMPLE-A +客户:示例客户 +产品:示例行程 +出发日期:2026-10-01 + +【办理进度】 +订单状态:已创建 +下单:已完成 + 时间:2026-09-22 14:00 + +名单:已导入,最近一次20人 + 时间:2026-09-22 14:00 + +最近修改: + 标间改为10 + 大床房改为2 + 时间:2026-09-22 14:00 + +文件:暂无本系统导出记录 + +【安排情况与明细】 + +导游:已安排,共1条 + 1. 示例导游 + 电话:示例电话 + +用车:未安排 + +酒店:已安排,共2条 + 1. 示例酒店甲 + 房型:TWN + 入住日期:2026-10-01 + 离店日期:2026-10-02 + 房数:10间 + + 2. 示例酒店乙 + 房型:DBL + 入住日期:2026-10-02 + 离店日期:2026-10-03 + 房数:2间 + +大交通:未安排 + +其他/备案:未安排 + +仅统计当前账号通过本系统成功完成的操作。 +未记录不代表 ERP 中没有。 +已安排表示至少一条,不代表全行程已安排齐全。 diff --git a/control-plane/src/reply-contract.ts b/control-plane/src/reply-contract.ts index 74d4349..22a336e 100644 --- a/control-plane/src/reply-contract.ts +++ b/control-plane/src/reply-contract.ts @@ -48,6 +48,17 @@ function firstText(...values: unknown[]): string { return texts(...values)[0] || ''; } +function businessText(lines: string[]): string { + const [title, ...details] = lines.filter(Boolean); + return [title, details.join('\n')].filter(Boolean).join('\n\n'); +} + +function labelledList(label: string, values: string[]): string { + return values.length > 1 + ? `${label}:\n${values.map(value => ` ${value}`).join('\n')}` + : `${label}:${values[0]}`; +} + function firstNumber(...values: unknown[]): number | null { for (const value of values) { if (value === undefined || value === null || value === '') continue; @@ -144,7 +155,7 @@ function identifierLine( || refs.kind === 'shared_child_order'; if (child && orderNumbers.length) { return { - text: `子单号:${orderNumbers.join('、')}`, + text: labelledList('子单号', orderNumbers), identifier: orderNumbers[0], groupNumbers, orderNumbers @@ -152,7 +163,7 @@ function identifierLine( } if (groupNumbers.length) { return { - text: `团号:${groupNumbers.join('、')}`, + text: labelledList('团号', groupNumbers), identifier: groupNumbers[0], groupNumbers, orderNumbers @@ -160,7 +171,7 @@ function identifierLine( } if (orderNumbers.length) { return { - text: `订单号:${orderNumbers.join('、')}`, + text: labelledList('订单号', orderNumbers), identifier: orderNumbers[0], groupNumbers, orderNumbers @@ -303,6 +314,7 @@ function hotelNewBookingReply( `IN. ${englishDate(values.start)}`, `OUT ${englishDate(values.end)}`, `${nights}${nightLabel} ${values.roomCount}${englishRoom}`, + '', '②新订单', `团号:${identifier}${customerSuffix}`, `入住日期:${chineseDate(values.start)}`, @@ -413,7 +425,7 @@ function arrangementReply( const id = identifier ? `团号:${identifier}` : ''; if (mode === 'clear') { return { - text: [`${actionTitle(action).replace('成功', '已清除')}`, id].filter(Boolean).join('\n'), + text: businessText([`${actionTitle(action).replace('成功', '已清除')}`, id]), profile: 'standard-arrangement-v1', fields: { identifier, mode: 'clear' } }; @@ -448,7 +460,7 @@ function arrangementReply( else if (date) lines.push(`日期:${date}`); if (quantity !== null) lines.push(action === 'arrangement_hotel' ? `房数:${quantity}间` : `数量:${quantity}`); return { - text: lines.filter(Boolean).join('\n'), + text: businessText(lines), profile: 'standard-arrangement-v1', fields: { identifier, resource: text(ref.name), item, start_date: start, end_date: end, date, quantity, mode: mode || 'create' } }; @@ -506,7 +518,7 @@ export function buildCustomerSuccessReply( fields = { ...fields, ...reply.fields }; } else if (action.startsWith('order_update_')) { const lines = [actionTitle(action), identity.text, ...updateLines(operationValue, result, context)].filter(Boolean); - textValue = lines.join('\n'); + textValue = businessText(lines); profile = 'standard-update-v1'; fields = { ...fields, updated_fields: updateLines(operationValue, result, context) }; } else if (action === 'passenger_list_import') { @@ -520,7 +532,7 @@ export function buildCustomerSuccessReply( object(data.passenger_list).row_count, requery.expected_persisted_count ); - textValue = [actionTitle(action), identity.text, count === null ? '' : `本次导入:${count}人`].filter(Boolean).join('\n'); + textValue = businessText([actionTitle(action), identity.text, count === null ? '' : `本次导入:${count}人`]); profile = 'standard-passenger-import-v1'; fields = { ...fields, imported_count: count }; } else if (action === 'order_cancel' || action === 'order_restore') { @@ -531,19 +543,19 @@ export function buildCustomerSuccessReply( && result.side_effects.some((value) => /child|子单/iu.test(String(value))) ? '子单状态已同步' : ''; - textValue = [actionTitle(action), identity.text, targetStatus ? `目标状态:${targetStatus}` : '', sideEffect].filter(Boolean).join('\n'); + textValue = businessText([actionTitle(action), identity.text, targetStatus ? `目标状态:${targetStatus}` : '', sideEffect]); profile = 'standard-lifecycle-v1'; fields = { ...fields, target_status: targetStatus, side_effect: sideEffect }; } else if (action === 'confirmation_export') { const names = attachments.map((item) => item.name); - textValue = [actionTitle(action), identity.text, names.length ? `文件:${names.join('、')}` : '文件已保存到任务附件'].filter(Boolean).join('\n'); + textValue = businessText([actionTitle(action), identity.text, names.length ? labelledList('文件', names) : '文件已保存到任务附件']); profile = 'standard-file-delivery-v1'; fields = { ...fields, file_names: names, attachment_count: attachments.length }; } else if (['team_order_create', 'team_order_batch_create', 'shared_plan_create', 'shared_child_order_create', 'shared_child_order_batch_create'].includes(action)) { - textValue = [actionTitle(action), identity.text].filter(Boolean).join('\n'); + textValue = businessText([actionTitle(action), identity.text]); profile = 'standard-newbooking-v1'; } else { - textValue = [actionTitle(action), identity.text].filter(Boolean).join('\n'); + textValue = businessText([actionTitle(action), identity.text]); } if (!textValue) return null; diff --git a/control-plane/src/team-progress.ts b/control-plane/src/team-progress.ts index ab13728..f803295 100644 --- a/control-plane/src/team-progress.ts +++ b/control-plane/src/team-progress.ts @@ -146,16 +146,18 @@ function displayTime(value: string): string { hour: '2-digit', minute: '2-digit' }).format(new Date(value)); } function arrangementLine(action: string, details: Json): string { - const fields = [txt(details.resource_name), txt(details.item), - details.start_date ? `${txt(details.start_date)} 至 ${txt(details.end_date) || '离店/结束日未记录'}` : txt(details.date), - details.quantity === undefined ? '' : `${txt(details.quantity)}${action === 'arrangement_hotel' ? '间' : '(数量)'}`, + const hotel = action === 'arrangement_hotel'; + const fields = [txt(details.resource_name), details.item ? `${hotel ? '房型' : '项目'}:${txt(details.item)}` : '', + details.start_date ? `${hotel ? '入住日期' : '开始日期'}:${txt(details.start_date)}` : details.date ? `日期:${txt(details.date)}` : '', + details.start_date ? `${hotel ? '离店日期' : '结束日期'}:${txt(details.end_date) || '未记录'}` : '', + details.quantity === undefined ? '' : `${hotel ? '房数' : '数量'}:${txt(details.quantity)}${hotel ? '间' : ''}`, details.phone ? `${action === 'arrangement_vehicle' ? '司机电话' : '电话'}:${txt(details.phone)}` : '', details.vehicle_number ? `车号:${txt(details.vehicle_number)}` : '', details.grade ? `等级:${txt(details.grade)}` : '', details.filing_number ? `备案号:${txt(details.filing_number)}` : '', details.filing_date ? `备案日期:${txt(details.filing_date)}` : '', details.filing_entry_port ? `入境口岸:${txt(details.filing_entry_port)}` : '', details.filing_exit_port ? `出境口岸:${txt(details.filing_exit_port)}` : '', details.remark ? `备注:${txt(details.remark)}` : ''].filter(Boolean); if (!details.resource_name) fields.unshift('资源名称未记录'); - return fields.length > 1 ? fields.join('|') : '已安排,明细未记录'; + return fields.length > 1 || details.resource_name ? fields.join('\n ') : '已安排,明细未记录'; } export function teamProgressText(value: unknown): string { @@ -172,49 +174,52 @@ export function teamProgressText(value: unknown): string { if (!facts.product && facts.product_condition) lines.push(`产品条件:${txt(facts.product_condition)}`); const status = latest(team, e => e.action === 'order_cancel' || e.action === 'order_restore' || ['team_order_create','team_order_batch_create','shared_plan_create'].includes(e.action)); - lines.push(`订单状态:${status ? txt(status.details.order_status) || '已创建' : '暂无本系统状态记录'}`); + lines.push('', '【办理进度】', `订单状态:${status ? txt(status.details.order_status) || '已创建' : '暂无本系统状态记录'}`); const subjects = [...new Set(events.filter(e => e.subject_kind === 'child').map(e => e.subject_number))].sort(); const shared = facts.team_kind === 'shared_plan' || subjects.length > 0; const creation = latest(team, e => ['team_order_create','team_order_batch_create','shared_plan_create'].includes(e.action)); - lines.push(`${shared ? '建计划' : '下单'}:${creation ? `已完成(${displayTime(creation.effective_at)})` : '暂无本系统成功记录'}`); + lines.push(`${shared ? '建计划' : '下单'}:${creation ? `已完成\n 时间:${displayTime(creation.effective_at)}` : '暂无本系统成功记录'}`); const subjectProgress = (subset: ProgressEvent[], label: string, parent = false): void => { const roster = latest(subset, e => e.action === 'passenger_list_import'); - if (!parent) lines.push(`${label}名单:${roster ? `已导入,最近一次${roster.details.roster_count === undefined ? '人数未记录' : `${roster.details.roster_count}人`}(${displayTime(roster.effective_at)})` : '暂无本系统导入记录'}`); + if (!parent) lines.push('', `${label}名单:${roster ? `已导入,最近一次${roster.details.roster_count === undefined ? '人数未记录' : `${roster.details.roster_count}人`}\n 时间:${displayTime(roster.effective_at)}` : '暂无本系统导入记录'}`); const update = latest(subset, e => e.action.startsWith('order_update_')); - lines.push(`${label}最近修改:${update ? `${list(update.details.updates).map(u => `${txt(u.label)}${u.mode === 'append' ? '追加' : '改为'}${txt(u.value)}`).join(';') || '修改成功,明细未记录'}(${displayTime(update.effective_at)})` : '暂无本系统修改记录'}`); + lines.push('', `${label}最近修改:${update ? `\n ${list(update.details.updates).map(u => `${txt(u.label)}${u.mode === 'append' ? '追加' : '改为'}${txt(u.value)}`).join('\n ') || '修改成功,明细未记录'}\n 时间:${displayTime(update.effective_at)}` : '暂无本系统修改记录'}`); const exports = new Map(); for (const e of subset) if (e.action === 'confirmation_export') for (const f of strings(e.details.files)) exports.set(f, e); const unknownExport = latest(subset, e => e.action === 'confirmation_export' && !strings(e.details.files).length); - if (!exports.size && !unknownExport) lines.push(`${label}文件:暂无本系统导出记录`); - if (unknownExport) lines.push(`${label}文件:已导出,文件类型未记录(${displayTime(unknownExport.effective_at)})`); + if (!exports.size && !unknownExport) lines.push('', `${label}文件:暂无本系统导出记录`); + if (unknownExport) lines.push('', `${label}文件:已导出,文件类型未记录\n 时间:${displayTime(unknownExport.effective_at)}`); for (const [file, e] of exports) { const changed = events.some(change => change.action !== 'confirmation_export' && change.effective_at > e.effective_at && (change.subject_kind === 'team' || subset.includes(change) || (file === 'visitor-list' && shared))); - lines.push(`${label}${parent && file === 'visitor-list' ? '整团游客信息' : FILES[file] || '团队文件'}:已导出(${displayTime(e.effective_at)})${changed ? ';之后有业务变更,可能需要重新导出' : ''}`); + lines.push('', `${label}${parent && file === 'visitor-list' ? '整团游客信息' : FILES[file] || '团队文件'}:已导出\n 时间:${displayTime(e.effective_at)}${changed ? '\n 之后有业务变更,可能需要重新导出' : ''}`); } }; if (!shared) subjectProgress(team, ''); else { - lines.push(`本系统记录子单:${subjects.length}个(不代表母团全部子单)`); + lines.push('', `本系统记录子单:${subjects.length}个`, '(不代表母团全部子单)'); // Parent modifications and whole-group exports belong to the parent. subjectProgress(team, '母团', true); for (const child of subjects) { const subset = events.filter(e => e.subject_kind === 'child' && e.subject_number === child); const created = subset.some(e => ['shared_child_order_create','shared_child_order_batch_create'].includes(e.action)); const state = latest(subset, e => ['order_cancel','order_restore','shared_child_order_create','shared_child_order_batch_create'].includes(e.action)); - lines.push(`子单 ${child}:${created ? '已新增' : '暂无本系统新增记录'};状态:${state ? txt(state.details.order_status) || '已创建' : '未记录'}`); + lines.push('', `子单 ${child}:${created ? '已新增' : '暂无本系统新增记录'};状态:${state ? txt(state.details.order_status) || '已创建' : '未记录'}`); subjectProgress(subset, ` ${child} `); } } - lines.push('【安排情况与明细】'); + lines.push('', '【安排情况与明细】'); for (const [action, label] of Object.entries(ARRANGEMENT_LABELS)) { const items = entry.arrangements.filter(a => a.action === action); - lines.push(`${label}:${items.length ? `已安排,共${items.length}条` : '未安排'}`); - items.forEach((a,i) => lines.push(` ${i+1}. ${arrangementLine(action, a.details)}`)); + lines.push('', `${label}:${items.length ? `已安排,共${items.length}条` : '未安排'}`); + items.forEach((a,i) => { + if (i > 0) lines.push(''); + lines.push(` ${i+1}. ${arrangementLine(action, a.details)}`); + }); } } if (!snap.groups.length || snap.unlinked_subjects.length) lines.push(`\n${snap.unlinked_subjects.length ? `对象:${snap.unlinked_subjects.join('、')}。` : ''}暂无法关联团号,未推断安排状态。`); - lines.push('\n仅统计当前账号通过本系统成功完成的操作;未记录不代表 ERP 中没有。已安排表示至少一条,不代表全行程已安排齐全。'); + lines.push('\n仅统计当前账号通过本系统成功完成的操作。', '未记录不代表 ERP 中没有。', '已安排表示至少一条,不代表全行程已安排齐全。'); return lines.join('\n'); } diff --git a/control-plane/test/reply-contract.test.ts b/control-plane/test/reply-contract.test.ts index 2a622ed..2bf58dd 100644 --- a/control-plane/test/reply-contract.test.ts +++ b/control-plane/test/reply-contract.test.ts @@ -43,6 +43,7 @@ test('hotel arrangement uses the customer bilingual booking template', () => { 'IN. 03/06/2026', 'OUT 04/06/2026', '1Night. 6Twin', + '', '②新订单', '团号:LW-260603Z-A 老挝联泰', '入住日期:2026.06.03', @@ -65,7 +66,7 @@ test('hotel clear and update replies stay business-readable when the booking tem } ); assert.ok(clearReply); - assert.equal(clearReply.text, '酒店安排已清除\n团号:LW-260603Z-A'); + assert.equal(clearReply.text, '酒店安排已清除\n\n团号:LW-260603Z-A'); const updateReply = buildCustomerSuccessReply( { success: true, group_number: 'LW-260603Z-A' }, @@ -79,7 +80,7 @@ test('hotel clear and update replies stay business-readable when the booking tem } ); assert.ok(updateReply); - assert.equal(updateReply.text, '酒店安排成功\n团号:LW-260603Z-A\n退房日期:2026-06-05\n房数:9间'); + assert.equal(updateReply.text, '酒店安排成功\n\n团号:LW-260603Z-A\n退房日期:2026-06-05\n房数:9间'); }); test('hotel replacement reply describes the new hotel and count while retaining the original dates', () => { @@ -100,7 +101,7 @@ test('hotel replacement reply describes the new hotel and count while retaining } } ); - assert.equal(reply?.text, '酒店安排成功\n团号:LW-TEST-A\n资源:测试新酒店\n入住日期:2026-09-15\n退房日期:2026-09-16\n房数:9间'); + assert.equal(reply?.text, '酒店安排成功\n\n团号:LW-TEST-A\n资源:测试新酒店\n入住日期:2026-09-15\n退房日期:2026-09-16\n房数:9间'); assert.equal(reply?.fields.resource, '测试新酒店'); assert.equal(reply?.fields.quantity, 9); }); @@ -112,7 +113,7 @@ test('batch newbooking receipt preserves every group number', () => { { action: 'team_order_batch_create', data: {} } ); assert.ok(reply); - assert.equal(reply.text, '批量下单成功\n团号:LW-260603A-A、LW-260605A-A'); + assert.equal(reply.text, '批量下单成功\n\n团号:\n LW-260603A-A\n LW-260605A-A'); }); test('update and file replies expose business fields without technical evidence', () => { @@ -128,7 +129,7 @@ test('update and file replies expose business fields without technical evidence' } ); assert.ok(updateReply); - assert.equal(updateReply.text, '散拼母团信息修改成功\n团号:LW-260603A-A\n计划收客数:45'); + assert.equal(updateReply.text, '散拼母团信息修改成功\n\n团号:LW-260603A-A\n计划收客数:45'); assert.doesNotMatch(updateReply.text, /sha256|verification|blocker|execution/iu); const baseReceipt = buildCustomerSuccessReceipt( @@ -146,7 +147,7 @@ test('update and file replies expose business fields without technical evidence' }, { action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-260603A-A' } } } ); - assert.equal(replyTextFromReceipt(baseReceipt), '团队文件已准备\n团号:LW-260603A-A\n文件:liantai-confirm.docx'); + assert.equal(replyTextFromReceipt(baseReceipt), '团队文件已准备\n\n团号:LW-260603A-A\n文件:liantai-confirm.docx'); assert.deepEqual(replyAttachmentsFromReceipt(baseReceipt), [{ name: 'liantai-confirm.docx', content_type: 'application/msword', @@ -181,7 +182,7 @@ test('visitor export reply exposes only the mobile XLSX delivery artifact', () = }, { action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-260816Z-A' } } } ); - assert.equal(replyTextFromReceipt(receipt!), '团队文件已准备\n团号:LW-260816Z-A\n文件:LW-260816Z-A Visitor.xlsx'); + assert.equal(replyTextFromReceipt(receipt!), '团队文件已准备\n\n团号:LW-260816Z-A\n文件:LW-260816Z-A Visitor.xlsx'); assert.deepEqual(replyAttachmentsFromReceipt(receipt!), [{ name: 'LW-260816Z-A Visitor.xlsx', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', @@ -206,7 +207,7 @@ test('legacy ERP mojibake filenames are repaired when a stored receipt is read', }, { action: 'confirmation_export', data: { existing_refs: { identifier: 'LW-261202A-A' } } } ); - assert.equal(replyTextFromReceipt(receipt), '团队文件已准备\n团号:LW-261202A-A\n文件:LLW-261202A-A 客户确认书.doc'); + assert.equal(replyTextFromReceipt(receipt), '团队文件已准备\n\n团号:LW-261202A-A\n文件:LLW-261202A-A 客户确认书.doc'); assert.deepEqual(replyAttachmentsFromReceipt(receipt), [{ name: 'LLW-261202A-A 客户确认书.doc', content_type: 'application/msword',