设计中心加联调测试台:造入站邮件(带附件)投通道,看真结果

你要的「模拟输入邮件+附件触发起单」。后端**本来就齐了** ——
`ingestMailRoute` 契约里早有 attachments,出参也早就回归属解析与三级消费结果。
所以这是纯前端加一个区:设计中心新增 `testbed`,一个页签「入站邮件」。

走的是与真通道**同一条**入站链路(通道归一 → 归属解析 → 统一事件流 → 三级消费)。
邮件通道是外界,模拟通道是 README 明许的;系统内部机制这里一行都不 mock ——
命中判定、起单、消息兜底全是真的。

## 这一屏回答三个问题

投出去之后就摆三样:归属判到谁(哪一级命中 / 无主的原因 / 默认认领岗位)、
谁接了(任务监听 vs 触发器各命中几个)、没人接落哪儿(消息兜底)。

还有一格**起单前置自检**:投之前就告诉你会不会命中,不用投完了猜。
判据与真实配置同源 —— 通道 id、主题关键词、两个归属键正则,四条逐条给绿灯或说差什么。
预填的样例是一键能起单的那一封;把它改坏,自检立刻指出差哪条。

附件如实说明「只走**登记**」:名字/类型/大小/存证指针进事件信封,内容不解析 ——
解析 Excel/PDF 是活动的事,活动还没实现。不含糊过去。

## 顺手修一个会静默分裂数据的 bug

通道的归属键正则 `RFQ-[A-Z0-9]+` 不含连字符,`RFQ-2607-501` 只抽到 `RFQ-2607` 就停。
而这个键会成为「受理结论」那一行的**主键** —— 截断了,同一单的数据就静默分裂成两行,
网关读到哪一行全看运气。改成分段续接 `RFQ-[A-Z0-9]+(?:-[A-Z0-9]+)*`,
实测 `RFQ-2607-501` / `RFQ-ABC123` 都整个吃下。

## 我自己踩的两个

① 出参类型手搓,`level`/`kind`/`position`/`rationale` **四个字段名全是编的** ——
页面上就摆出两个永远显示「—」的格子。改为直接用契约类型:归属解析真实是按 `outcome`
判别的联合(owned 给 rule+taskId,unowned 给 reason+可选 fallbackPosition)。
为此给契约补了 `IngestMailOutput` 导出(加法)。
② JSX 正文里写 markdown 的 `**` 会显示成字面星号,换 `<b>`。这坑本轮踩过第二次。
另补两个缺失图标(flask-conical / split),不补就是空 SVG。

## 实测

界面上点「投递这封信」→ 归属判**无主 · 三级都没对上 · 默认认领岗位 运营**
(对新询价完全正确:没有在途任务可归)→ 触发器命中 1 个 → 起单。
起单入参 `customerCode: C014` / `rfqRef: RFQ-2607-501` 都抽对了。

一个值得记的观察:事件起单**没有「来源人」**,所以不装载工作组,A1 的 BD 岗位解析到
真正的 BD(冯兵),任务落他工位而不是我的 —— 这是对的,不是 bug。
要在自己工位上看,用「人工登记起单」那条路(登记人即来源)。

typecheck ✓ lint ✓ depcruise 376 模块 ✓ 498 测试 ✓

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 14:02:00 +08:00
co-authored by Claude Opus 5
parent 58d914b8a2
commit a9e9c7babd
12 changed files with 408 additions and 2 deletions
@@ -0,0 +1,19 @@
{
"messageId": "sim-1785218175996",
"from": "buyer@sz-precision.com",
"to": [
"rfq@we.com"
],
"cc": [],
"subject": "【询价】RFQ-2607-501 精密车削件 6 项",
"body": "你好,\n\n我方客户码 C014,本次询价单号 RFQ-2607-501,附件是 BOM 清单(6 项)。\n月需约 2000 套,请两周内回价。\n\n深圳精密件采购部",
"receivedAt": "2026-07-28T05:56:15.996Z",
"attachments": [
{
"name": "BOM-RFQ-2607-501.xlsx",
"contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 18432,
"ref": "sim://attach/BOM-RFQ-2607-501.xlsx"
}
]
}
@@ -0,0 +1,19 @@
{
"messageId": "sim-1785218283747",
"from": "buyer@sz-precision.com",
"to": [
"rfq@we.com"
],
"cc": [],
"subject": "【询价】RFQ-2607-501 精密车削件 6 项",
"body": "你好,\n\n我方客户码 C014,本次询价单号 RFQ-2607-501,附件是 BOM 清单(6 项)。\n月需约 2000 套,请两周内回价。\n\n深圳精密件采购部",
"receivedAt": "2026-07-28T05:58:03.747Z",
"attachments": [
{
"name": "BOM-RFQ-2607-501.xlsx",
"contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 18432,
"ref": "sim://attach/BOM-RFQ-2607-501.xlsx"
}
]
}
@@ -0,0 +1,19 @@
{
"messageId": "sim-verify-1",
"from": "buyer@sz-precision.com",
"to": [
"rfq@we.com"
],
"cc": [],
"subject": "【询价】RFQ-2607-501 精密车削件 6 项",
"body": "我方客户码 C014,本次询价单号 RFQ-2607-501,附件是 BOM 清单。",
"receivedAt": "2026-07-28T06:00:00.000Z",
"attachments": [
{
"name": "BOM.xlsx",
"contentType": "application/vnd.ms-excel",
"size": 18432,
"ref": "sim://a/1"
}
]
}
+8 -1
View File
@@ -470,9 +470,16 @@ export async function seedMvpFlow(container: Container): Promise<void> {
const { positions } = await container.org.listStaff();
const op = positions.find((p) => p.code === 'OP');
const pm = positions.find((p) => p.code === 'PM');
/*
* 归属键提取 —— 正则要能吃**完整**的单号。
*
* `RFQ-[A-Z0-9]+` 不含连字符:遇到 `RFQ-2607-501` 只抽到 `RFQ-2607` 就停(实测)。
* 而这个键会成为「受理结论」那一行的主键 —— 截断了,同一单的数据就静默分裂成两行,
* 网关读到哪一行全看运气。分段续接才能整个吃下:`RFQ-2607-501` / `RFQ-ABC123` 都对。
*/
const extract = [
{ key: 'customerCode', regex: '\\bC\\d{3}\\b', source: 'both' as const },
{ key: 'rfqRef', regex: 'RFQ-[A-Z0-9]+', source: 'both' as const },
{ key: 'rfqRef', regex: 'RFQ-[A-Z0-9]+(?:-[A-Z0-9]+)*', source: 'both' as const },
];
await container.channel.upsert({