飞书、钉钉与企业微信研发通知闭环实战
告警已经发进群,故障却仍在扩大
凌晨的订单延迟告警成功发进研发群,机器人接口返回成功,群里也确实出现了一张红色卡片。值班同学以为发布负责人会处理,发布负责人以为值班同学已经接管;半小时后,审批单仍停在“已通过”,部署平台却已自动回滚。第二天复盘时,聊天记录里有“我看一下”,文档里有旧方案,工单里仍显示处理中,没有任何一个系统能回答谁在什么时刻确认、采取了什么动作、最终结果是否写回。
这类事故不是少发了一条消息,而是把“通知送达”误当成“协作完成”。飞书、钉钉和企业微信都能把 HTTP 请求变成群消息,但 Webhook 返回成功通常只能证明平台受理了请求,不能证明消息被某个人阅读,更不能证明审批、发布或故障状态已经改变。
可靠链路需要保存五类证据:业务事件、投递尝试、平台受理结果、人的确认动作、源系统终态。群聊负责吸引注意力,文档保存可复核材料,审批表达授权,工单或发布平台仍然是状态事实源。三者接在一起,才是一条研发通知闭环。
先认清五种对象
把平台名从设计图上暂时拿掉,协作系统最少有这些对象:
CollaborationEvent
├── event_id # 全局事件身份,例如 incident:INC-1842:opened
├── dedupe_key # 同一业务变化的幂等键
├── source # monitor / ci / issue / release
├── severity # info / warning / critical
├── owner_role # oncall-order,不使用易腐烂的个人姓名
├── action_url # 回到事实源处理,不把聊天当数据库
├── document_url # 评审材料或运行手册
├── approval_ref # 审批定义、实例或任务身份
├── data_classification # public / internal / restricted
└── expires_at # 超过时限后升级或停止操作
DeliveryAttempt
├── provider # feishu / dingtalk / wecom
├── target_id # 逻辑目标,不保存群名作为唯一身份
├── attempt
├── requested_at
├── response_code
├── provider_request_id
└── next_retry_at
Acknowledgement
├── event_id
├── actor_id # 平台稳定用户 ID 映射到企业身份
├── action # acknowledge / reject / resolve / escalate
├── occurred_at
└── callback_id # 用于事件去重状态机应明确区分“已入队”和“人已确认”:
群 Webhook 往往只能让状态走到 ACCEPTED。只有应用机器人回调、审批事件,或卡片里的链接回到自有服务并完成企业身份认证,才能可信地写入 ACKED。若平台没有提供逐消息送达回执,就不要凭空增加 DELIVERED 状态。
选对入口:群 Webhook 还是应用身份
三个平台都有“很快发一条群消息”和“以应用身份参与业务流程”两条路线,但名称和能力并不完全对应。
| 需要解决的问题 | 群 Webhook / 消息推送 | 自建应用或应用机器人 |
|---|---|---|
| 固定群接收 CI、监控摘要 | 合适 | 能做,但运维成本更高 |
| 用户点击确认并回写 | 通常不足 | 合适,需要事件回调和身份权限 |
| 跨多个群或单聊指定用户 | 能力受限 | 合适,受应用可用范围约束 |
| 读取用户、群、文档或审批数据 | 不应具备 | 按权限申请并经管理员授权 |
| 快速撤销 | 删除机器人或轮换 Webhook | 停用应用、撤销权限与凭证 |
| 可审计的长期业务集成 | 只能做投递末端 | 应作为主路径 |
飞书机器人概述 明确区分应用机器人和群自定义机器人:自定义机器人只能在所在群单向推送,不能读取用户或租户数据;需要接收消息、管理群或调用文档等 OpenAPI 时,应创建应用并开启机器人能力。企业微信“消息推送”配置说明 是原群机器人的当前入口;它同样以带 key 的 Webhook 向固定群推送。钉钉自定义机器人入口 会随产品线演进调整路径,旧教程中的机器人类型、创建页面和下线提示不能直接当作新接入依据,上线前要从当前机器人总览重新确认可用类型。
简单判断是:只想让一个固定群看到构建结果,用群 Webhook;需要确认人身份、接收交互、驱动审批、访问组织资源或跨群路由,用应用身份。不要为了少走管理员审批,把高权限业务伪装成几十个散落的群机器人。
在三个平台启用最小通知入口
飞书:群内添加自定义机器人
由目标群的群主或管理员进入群设置,添加自定义机器人,复制 V2 Webhook,并至少启用签名校验;有固定出口时再叠加 IP 白名单,关键词只作为误发保护,不是认证。机器人与群一一绑定,不能拿同一个 Webhook 跨群复用。官方自定义机器人使用指南 还给出了当前消息体限制、频率限制和删除入口。
先只发送合成数据:
curl -X POST \
-H "Content-Type: application/json; charset=utf-8" \
-d '{"msg_type":"text","content":{"text":"[SANDBOX] build demo-42 passed"}}' \
"$FEISHU_WEBHOOK"配置签名后,请求体还要带 timestamp 和 sign。FEISHU_WEBHOOK 与签名密钥是两项独立秘密,都只能从密钥管理服务注入。应用机器人则在开放平台创建自建应用、开启机器人能力、申请最小消息或事件权限、设置可用范围、发布版本,再由管理员审核;调用发送消息 API 时使用应用访问令牌,而不是群 Webhook。
钉钉:从当前机器人产品入口创建
从目标群的机器人管理入口或钉钉开放平台当前指引创建适用的机器人,记录它属于自定义机器人、企业内部应用机器人还是其他机器人类型。不同类型的凭证、消息接口和回调能力不同,不能把旧版自定义机器人示例直接套到应用机器人。
自定义 Webhook 启用安全设置时优先选择加签,有稳定出口再叠加 IP 白名单;关键词适合阻止发错群,却无法证明调用者身份。官方自定义机器人安全设置 是签名算法与入口变化的准绳。测试消息保持可识别和可删除:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"[SANDBOX] release demo-42 ready"}}' \
"$DINGTALK_SIGNED_WEBHOOK"需要收取消息、事件或以应用访问凭据调用 OpenAPI 时,改用企业内部应用机器人并完成发布授权。不要把 accessToken、应用密钥和自定义机器人 secret 混成一个配置项;它们的权限面、轮换方式和泄漏后果不同。
企业微信:创建消息推送并保护 key
在目标群的管理界面创建“消息推送”,复制形如 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=... 的地址。key 本身就是调用凭证,官方页面明确要求不要把地址发布到 GitHub、博客或其他公开位置。最小测试为:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"[SANDBOX] incident demo-42 opened"}}' \
"$WECOM_WEBHOOK"企业微信消息推送没有与飞书、钉钉自定义机器人完全对等的“发送端 HMAC 加签”配置。安全边界主要是 Webhook key、HTTPS、秘密保管和受控出口;若需要更强身份和交互,创建自建应用,通过发送应用消息接口调用,并按回调配置设置 Token 与 EncodingAESKey。回调的 msg_signature 验证和消息解密是接收端安全机制,不能拿来声称群 Webhook 已有发送签名。
三组 curl 的预期结果都是 HTTP 成功且群里出现唯一的 demo-42 消息。平台业务响应体仍要解析:HTTP 200 里可能包含非零业务错误码。失败时保存脱敏后的状态码、业务码、请求 ID、目标逻辑名和 payload 摘要;绝不记录完整 Webhook 或访问令牌。
签名验签最容易错在“算法相同,输入不同”
飞书与钉钉都使用 HMAC-SHA256 和 Base64,但密钥、消息、时间戳单位以及 URL 编码规则不同。为了“统一”而共用一段含糊的 sign(secret, timestamp),通常会在生产中得到稳定的签名错误。
下面用 Node.js 标准库展示两种发送签名:
import crypto from "node:crypto";
export function signFeishu(timestampSeconds, secret) {
const key = `${timestampSeconds}\n${secret}`;
return crypto.createHmac("sha256", key).update("").digest("base64");
}
export function signDingTalk(timestampMilliseconds, secret) {
const message = `${timestampMilliseconds}\n${secret}`;
const base64 = crypto
.createHmac("sha256", secret)
.update(message)
.digest("base64");
return encodeURIComponent(base64);
}飞书把 timestamp + "\n" + secret 作为 HMAC key,对空字节串计算;钉钉把 secret 作为 HMAC key,对 timestamp + "\n" + secret 计算,并把 Base64 结果放入 URL 时再编码。时间戳还分别使用秒与毫秒。生产实现应直接对照各自官方文档和 SDK 测试向量,不要凭算法名称推断参数顺序。
接收应用事件又是另一条链。飞书请求地址回调的 X-Lark-Signature 不是上面的 HMAC:服务端把 X-Lark-Request-Timestamp + X-Lark-Request-Nonce + Encrypt Key 编码为字节,再直接拼接原始请求体,计算 SHA-256 十六进制摘要。若先把 JSON 解析后重新序列化,空格、转义或字段顺序变化都会让验签失败。飞书官方接收事件说明 同时区分了请求地址与长连接:请求地址模式由服务端验签和解密;长连接模式由 SDK 建连鉴权,不应再套 HTTP 请求头算法。
可以先把飞书请求地址模式的原始字节校验封装成独立入口,验签通过后才解析或解密:
import crypto from "node:crypto";
export function verifyFeishuCallback({ timestamp, nonce, rawBody, encryptKey, signature }) {
const prefix = Buffer.from(`${timestamp}${nonce}${encryptKey}`, "utf8");
const expected = crypto
.createHash("sha256")
.update(Buffer.concat([prefix, rawBody]))
.digest("hex");
const actual = Buffer.from(signature, "ascii");
const wanted = Buffer.from(expected, "ascii");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}企业微信回调则按官方回调配置处理:msg_signature 由 Token、timestamp、nonce 和加密消息体共同参与计算,随后用 EncodingAESKey 解密,并校验解密结果中的接收方身份。优先使用官方 WXBizMsgCrypt 实现,避免自行处理排序、PKCS#7 填充和接收方校验。这里的 Token、EncodingAESKey 与发送应用消息使用的 Secret 是三种不同凭证。
无论平台是哪一家,HTTP 回调都按同一安全顺序收口:读取原始字节,拒绝超出允许窗口的时间戳,验签,解密并检查租户或应用接收方,按平台事件 ID 或稳定业务组合键去重,在同一事务内提交业务变化与 callback_id,最后才返回成功。若业务提交失败就返回平台规定的失败响应,让重投继续;若已经提交过,重复回调直接返回成功但不再触发发布、审批或升级。签名正确只证明请求来源与完整性,不证明事件新鲜,也不证明操作者有权改变源系统。
用正反实验留下可诊断证据
先在隔离测试群建立一个机器人,以合成事件 incident:demo-42:opened 发送消息。投递服务记录以下结果:
{
"event_id": "incident:demo-42:opened",
"provider": "feishu",
"target": "sandbox-oncall",
"attempt": 1,
"transport_status": 200,
"provider_code": 0,
"state": "ACCEPTED"
}群消息应展示事件 ID、级别、逻辑 owner、摘要、事实源链接和确认截止时间。点击“确认”时,不直接相信卡片中提交的用户字段,而是由应用回调或企业登录后的自有确认页识别操作者;成功后,源工单出现 acknowledged_by 和相对时间,机器人再更新原卡片或发送低噪声终态消息。这样才证明通知、身份、动作和回写都接通。
然后制造三个稳定反例:
把飞书签名的时间戳单位改成毫秒,预期收到签名或时间戳错误,投递状态不得进入 ACCEPTED。对同一个 dedupe_key 连续提交两次,预期 outbox 只有一个逻辑事件;若平台侧无法幂等,第二次投递也必须标记为重发并更新同一事实记录,不能产生两个独立处置任务。让确认回调重复到达,预期第一次写入 ACKED,后续相同 callback_id 返回成功但不重复执行升级、审批或发布动作。
故障日志应像这样指向机制,而不是只写“机器人失败”:
delivery rejected provider=feishu target=sandbox-oncall
event_hash=sha256:7f... attempt=1 transport=200 provider_code=sign_invalid
secret_version=collab/feishu/sandbox@3 payload_bytes=428 retryable=false日志保留密钥版本,不保留密钥值;保留事件哈希,不保留受限正文。将错误分成 retryable、credential、permission、payload 和 permanent,值班人员才能决定重试、轮换、扩权还是修消息。
把通知闭环接进项目
不要让 CI、监控、工单和发布平台各自直接拼三家 payload。它们只向内部协作网关写统一事件,网关通过 outbox 和适配器投递:
CI / Monitor / Issue / Release
|
v
Collaboration API
|
transaction + outbox
|
dispatcher queue
/ | \
Feishu DingTalk WeCom
\ | /
callback gateway
|
identity + dedupe + policy
|
Issue / Release / Incident source of truth项目仓库只保存逻辑配置和变量名:
collaboration:
routes:
- match: { source: ci, severity: warning }
target: team-build
template: build-summary-v3
acknowledgement: none
- match: { source: incident, severity: critical }
target: oncall-primary
template: incident-critical-v5
acknowledgement: required
escalate_after: PT10M
providers:
feishu:
webhook_secret_ref: collab/feishu/team-build
dingtalk:
webhook_secret_ref: collab/dingtalk/team-build
signing_secret_ref: collab/dingtalk/team-build-sign
wecom:
webhook_secret_ref: collab/wecom/team-buildtarget 是服务目录中的逻辑目标,再由部署环境映射到具体群或应用接收者。群迁移时只改受控映射,不要求几十个仓库提交新 Webhook。模板也要有版本:消息字段变化、链接权限变化和卡片交互变化都可追踪,旧事件重放仍能解释当时渲染结果。
数据库事务只提交业务状态和 outbox,不在业务请求内同步调用办公平台。dispatcher 用 event_id + provider + target 建唯一键,保证应用内部不会创建第二条处置任务;这并不等于三家发送接口都承诺平台侧幂等。
一次投递必须区分三种结果。连接建立前就失败,可以进入退避重试;收到明确业务拒绝,按凭证、权限或 payload 错误停止;请求已发出却在响应前超时,只能标为 UNKNOWN。UNKNOWN 先查询平台消息状态或按返回的消息 ID 对账,平台没有查询入口时,再按业务时限决定是否补发,并让补发消息携带同一 event_id 与“重发”标记。直接把超时当失败会在群里制造双消息,直接把超时当成功又会漏掉告警。
若平台返回消息 ID 并支持更新,就保存它来更新卡片;若不支持,终态消息必须引用同一事件 ID,不能假装修改成功。每次尝试仍单独保留 attempt、开始与结束时间、脱敏响应摘要和凭证版本,逻辑事件则始终只有一条。
限流、重试与消息风暴
三家的限流维度不同,而且会调整。飞书官方自定义机器人指南当前给出单租户单机器人每分钟与每秒限制,并提醒整点附近可能出现限流;应用消息接口还有接口级和同一用户、同一群级限制。企业微信当前规定每个消息推送每分钟不超过 20 条。钉钉历史自定义机器人规则也采用每机器人分钟级限制,并可能在超限后进入更长冷却;新机器人类型必须从当前接口文档读取限额。
不要把这些数字写成队列容量。可靠调度器应从配置加载供应商策略,并同时执行:
以 provider + tenant + credential + target 为键的令牌桶。对超时、5xx 和明确限流码使用带抖动的指数退避,并尊重 Retry-After 或官方重试提示。对签名错误、凭证失效、无权限、目标不存在和 payload 非法停止盲目重试。
相同故障在时间窗内聚合成一条摘要,严重级别上升或状态改变时才突破抑制。队列按 critical、warning、info 分级,低级别消息不能挤占事故确认通道。超过业务时限的通知进入死信并升级其他渠道,不能在故障结束后补发一百条过期告警。
容量规划要从事件突发算起。假设一次区域故障会让 300 个构建同时失败,正确设计不是准备 300 次并发 POST,而是先按根因、服务、环境聚合,再给群发一条可展开摘要。观测 queue_age、accepted_rate、throttle_rate、dead_letter_count、ack_latency、duplicate_suppressed 和每目标消息量;只看 HTTP 成功率会掩盖无人确认。
文档与审批承担不同证据
群消息适合短摘要和入口链接,不适合保存完整评审结论。设计评审材料放入团队空间或知识库,至少包含稳定 owner、评审版本、关联工单、决策与验证结果。权限继承要经过实际账号验证:机器人能贴出链接,不代表群中每个人都有读权限;外部群更不能继承内部文档可见性。
审批表达的是“谁被授权对哪个版本的对象作出什么决定”。发布审批至少绑定制品摘要、环境、变更工单和风险版本;审批通过后若制品摘要变化,原批准应失效。飞书开放平台把审批定义、审批实例、审批任务和审批动态分成不同资源,审批概述 也明确三方审批接入本质上是数据同步,外部系统仍需接收结果并继续自己的状态流转。钉钉和企业微信接入时也要按各自当前审批 API、事件权限和可用版本建立同样映射,不能只保存一个可点击的审批页面 URL。
一次完整发布应能从 release_id 查询到:候选制品摘要、评审文档版本、审批实例、批准人企业身份、平台事件 ID、部署执行 ID和最终结果。聊天中的“同意”可以是讨论证据,却不能自动替代受控审批;审批通过也不代表部署已经成功。
凭证、权限与敏感数据
Webhook URL 是 bearer secret:拿到它的人通常就能向目标群发消息。把它放在密钥管理系统,按环境、团队和用途拆分,运行时注入;禁止进入 Git、工单、文档、截图、Shell 历史、错误追踪请求 URL和普通日志。自建应用的 App Secret、Corp Secret、访问令牌、回调 Token、Encrypt Key 与签名 secret 分开保存和轮换。
应用权限遵循“能力需要什么才申请什么”:只发群消息就不申请通讯录全量读取,只处理审批事件就不申请文档全库访问。一次调用实际要连续通过三道闸门:应用是否申请并获批该 API scope,目标用户是否处于应用可用范围,当前应用或用户身份是否拥有目标群、文档或审批对象的资源权限。任一闸门失败都不能靠扩大另两项掩盖。
这三道闸门也解释了“接口成功但有人没收到”。企业微信发送应用消息在部分接收人无权限或不存在时仍可能执行发送,并在响应中返回 invaliduser、invalidparty、invalidtag 或 unlicenseduser;全部接收人无效时才整体失败。投递器必须把这些字段落成部分失败证据,不能只判断 errcode 或 HTTP 状态。飞书和钉钉也应按各自接口返回的无效接收者、权限码和请求 ID 建立同样的逐目标结果,而不是把租户级 token 当成“可访问租户内全部资源”。
测试账号应位于隔离租户或沙箱群,不把生产客户、手机号、邮箱、订单正文、源代码片段、漏洞细节和真实访问令牌发到测试消息。应用发布、权限扩展和可用范围变更都要经过管理员复核,并用一个允许用户和一个拒绝用户做正反验证;前者应收到消息或完成审批,后者应留下明确权限错误且不得被自动重试。
消息卡片采用分级披露:群里只放事件 ID、脱敏摘要、owner 角色和受权限控制的深链接;受限数据留在事实源。自由文本在发送前经过 DLP 与密钥扫描,链接使用稳定 HTTPS 域名并避免把 token 放进 query。回调日志只记录 schema、摘要和平台请求 ID,原始请求体如因审计必须留存,应加密、限权并设置短保留期。
留存、离职回收与审计
即时消息的可搜索时长、导出、会话内容存档、审计日志、文档容量、审批高级能力和 SSO 等,可能受产品版本、企业认证、管理员策略、法规地域与另购能力影响。不要在架构文档里承诺“永久保存”,也不要根据某个免费测试租户推断生产权益。采购与上线前从三家官方版本权益页、管理后台和合同核对当前能力,不写死价格。
协作证据应按数据类型建立保留表:投递元数据可以长期保存,消息正文按最低需要保留,审批与发布证据按合规周期归档,原始敏感回调尽快删除。真正需要长期审计的结论同步到受控工单、ADR 或发布记录,不能依赖聊天搜索。
员工离职或团队调整时,至少执行这些回收动作:
把个人名下文档和自动化所有权转给角色或团队空间,验证旧链接仍可访问。从群、应用管理员、审批节点、服务目录和 on-call 轮值中移除离职身份。撤销用户授权令牌与个人创建的测试应用;检查机器人、回调服务和密钥是否由个人账号持有。
轮换可能被个人查看过的 Webhook、应用密钥和签名密钥,并记录新版本生效时间。用已离职身份做一次负向验证,确认其不能读文档、批准审批、调用接口或接收事故通知。
仅在通讯录里停用账号并不够。个人可能仍是文档 owner、群主、应用负责人、审批节点或外部自动化的密钥保管人,这些都是离职后会爆炸的隐性依赖。
从故障现象反推失败层
HTTP 200,但群里没有消息
先解析业务响应码,再检查目标机器人是否仍在群内、消息是否命中关键词、签名时间戳是否正确、payload 是否超限以及机器人是否被限流。保存请求 ID 与目标逻辑名后,在隔离群用最小文本复测。不要用无限重试掩盖权限或格式错误。
群里有两条相同事故
检查业务写入与消息发送是否处在同一同步调用中,以及超时后是否无条件重发。查询 outbox 唯一键、attempt 记录和平台消息 ID。修复点通常是内部幂等和状态持久化,不是给文本加“请忽略重复”。
点击确认后,源工单没有变化
沿 event_id -> callback_id -> actor_id -> source update 查四段证据。若平台回调不存在,Webhook 卡片可能只支持打开链接;确认动作就必须落到自有认证页面。若回调收到却更新失败,区分验签、解密、重复事件、身份映射、源系统权限和并发版本冲突。
审批已通过,发布仍然拒绝
比较审批绑定的制品摘要、环境和变更版本。批准旧制品后重新构建,新制品不应继承旧批准。再检查审批事件是否只在办公平台更新,却没有回写发布事实源;同步延迟与权限错误都应有明确状态,不能靠值班同学截图放行。
更换群或平台后,所有链接失效
说明业务系统保存了群名、消息 URL 或平台用户 ID作为核心身份。把稳定事件 ID、企业身份 ID和事实源 URL留在内部模型,平台消息只保存为可替换的投影。迁移时双写验证一段时间,再切换路由和回调,不要一次性删除旧入口。
清理、回滚与退出演练
测试结束后,在三个平台删除测试消息或整个测试机器人,撤销测试应用权限,轮换曾出现在终端历史中的 Webhook 和 secret,清空死信中的合成 payload,并确认没有定时任务继续投递。删除群消息并不能让已泄漏的 Webhook 失效,必须删除机器人或轮换凭证。
生产发布回滚按层处理:模板有问题,回退模板版本;某一平台适配器异常,暂停该 provider 并保留 outbox;凭证泄漏,先禁用或轮换,再恢复投递;回调处理有副作用,关闭动作执行但继续接收、验签和去重,避免平台反复重投。切换到备用渠道时沿用同一 event_id,并标记路由变化,防止两个群各自形成一套处置现场。
平台退出不能等合同结束才设计。每季度演练导出机器人清单、逻辑目标映射、文档与审批索引、应用 scope、所有者、回调订阅和投递元数据;确认核心事实仍能从自有工单、发布和事故系统重建。卡片模板应由中立视图模型生成,三家适配器只负责语法转换;业务代码若直接依赖某家的卡片 JSON、用户 ID和审批节点结构,迁移成本会随每条自动化增长。
架构选型与长期治理
小团队、低频构建摘要、固定内部群,可以从每群一个受控 Webhook 开始,但仍要有逻辑目标、秘密轮换和消息聚合。需要确认、跨群路由、指定用户、审批联动或审计时,直接采用应用身份和回调网关。多业务线或同时使用多个办公平台的组织,应建设薄协作网关,而不是追求一个包含所有办公能力的“大一统平台”:网关拥有事件模型、幂等、限流、审计和身份映射,平台适配器保持可替换。
成本不能只看接口调用。还包括应用开发与管理员审批、席位或增值权益、会话与文档存储、审计归档、消息风暴干扰、凭证轮换、故障值守、跨平台身份映射和退出迁移。三家版本与商业权益会变化,飞书可从官方定价版本说明核对当前版本差异;钉钉与企业微信则从各自官网版本页、管理后台和采购合同确认,不把历史网页中的金额或额度写成架构常量。
长期台账至少记录:应用与机器人 owner、使用群、业务源、权限 scope、数据分类、密钥位置与轮换周期、限流策略、回调 URL、留存周期、费用归属、替代渠道和退出负责人。每季度做一次正向通知和一次反向验签实验,每半年做离职回收与平台故障切换演练。指标同时观察平台受理率和人工确认时延,避免“机器人全绿、事故无人处理”的假健康。
最终检验很朴素:给定一个 event_id,能够找到它为何产生、发给谁、平台是否受理、谁确认、审批对应哪个对象、源系统何时结束;任一平台不可用或合同变化时,仍能从自有事实源恢复过程并切换渠道。做到这些,群聊才从消息终点变成可靠协作链路的一环。
