优化回执排序

This commit is contained in:
andy
2026-09-22 18:44:41 +08:00
parent 1797c713af
commit a7c9cc358a
7 changed files with 168 additions and 36 deletions

View File

@@ -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 说明中加入“纯文本段落、逐字段换行与缩进”约定;业务与架构规则不变,无待决产品问题。

View File

@@ -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}

View File

@@ -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 执行、附件与投递时机未改动。

View File

@@ -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 中没有。
已安排表示至少一条,不代表全行程已安排齐全。

View File

@@ -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;

View File

@@ -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<string, ProgressEvent>();
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');
}

View File

@@ -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',