fix: preserve source event IDs across all feedback

This commit is contained in:
2026-10-10 15:02:30 +08:00
parent b41dc76ac4
commit c2ee8a5a4b
26 changed files with 553 additions and 54 deletions
+2
View File
@@ -7,6 +7,8 @@ SaaS 开发人员与本项目共同维护业务规范、Schema、MQ 拓扑和正
本项目的 `contracts/schema` 是 Git submodule,只记录消费的确定提交,不维护另一份本地定义。
原 `contracts/local` 已移除;`docs/thirds/saas-dispatcher.md` 只提供导航。
内部 `proto/agent/agent.proto` 仍属于本项目,不移入 SaaS 业务契约。
2026-10-10 原 `event_id` 全链路反馈规则已同步至共享提交 `ef3009d`;本项目变更、核验和 SaaS 开发同步说明见 [event_id 生命周期核验](evidence/event-id-lifecycle-20261010.md)。具体消息定义仍只以共享合同为准;此次本地改动未部署,不代表线上反馈已切换。
任务控制回执及逐字段拒绝反馈规则统一见共享仓库的 `mq-task-control-receipt.schema.json`、`saas-dispatcher.md` 和 `examples/mq-control-{ack,rejected}.json`;SaaS 须同步采用这些入口,不以历史归档提案或旧的仅 status 回执作为现行结构。
| 位置 | 用途 |
@@ -0,0 +1,82 @@
# event_id 生命周期一致性:本地修复与核验
## 范围与来源
使用者确认:所有有源事件的反馈原样保留下发 `event_id`,包括其中的 numberID;其他事件同步检查。不修改真实 SaaS 代码,不部署、不拨号、不调用真实 AI/OSS,不迁移、重置或清理现存业务数据。
- 项目基线:`b41dc76`;独立分支:`fix/event-id-lifecycle`。
- 开始前工作区干净,已 fetch 并快进核对;合同更新由 `make contracts-update` 获取共享 `main`。
- 共享合同:`ef3009df3a90f6e941ca0b24e46134aef17a374d`,已推送并核对远端 `main`;本项目固定对应 submodule 指针。
- 唯一业务定义仍在 [`contracts/schema/saas-dispatcher.md`](../../contracts/schema/saas-dispatcher.md) 及其 Schema。本记录只说明改动与证据,不维护另一份业务规范。
## 全链路逐项核对
| 路径 | 核对结论与处理 |
| --- | --- |
| MQ `call.execute` 接收、inbox、选线、冻结执行与 Agent `CallSpec` | 已保留原 ID,无需改号;重复或重启不重新发起呼叫。带 numberID 的特殊大小写、前缀、标点、Unicode 和前后空格原值均通过测试。 |
| `call.execute` 派发/拒绝回执 | 已保留原 ID;回执查询限定 `call.execute`,不再可能读到相同 ID 的最终结果。 |
| `call.execute.result` 无录音及已上传录音 | 原实现将源 ID 哈希为 `result-…`;已删除该生成函数,结果原样使用源 ID,首次结果正文保持不可变。 |
| 外发保存、查询、确认、重发与恢复 | 保存身份改为 Dispatcher、事件类型及原 ID;两种反馈分别确认,一种反馈的确认不会吞掉另一种。最终结果重发和重启恢复保持原 ID 与原正文,矛盾内容仍拒绝。 |
| 录音授权及上传结果 | 录音查询只判断对应 `call.execute.result` 是否存在;已发送派发回执不会阻断录音授权,真正最终结果存在后仍拒绝新建录音。原上传身份、目标与未知 PUT 屏障未变。 |
| 任务 start/pause/resume/stop/edit 回执 | 已保留原 ID;保存和重复回执查询、重新排队明确限定 `task.control`,保留原处理状态。 |
| `call.execute` / `task.control` / `sip.config` 结构拒绝及拒绝重放 | 已保留可核实的原 ID;按事件类型查询及重新排队,不重新排队相同 ID 的其他反馈。缺少有效身份时仍不伪造 ID。 |
| SIP 通知持久来源与应用完成 | 原通知身份已原样保存;成功加载的完成标记不额外生成业务反馈事件,不新增未定义的成功回执。 |
| Agent 结束回报、录音、结果日记及重启恢复 | `SourceEventID` 已保持原值;结束、上传、结果等 RPC 动作编号仍独立,动作编号不替换源事件 ID。 |
| 本项目测试专用 SaaS Mock 单通结果等待 | 发现旧 `expectedResultEventID` 哈希判定;已删除,直接核对原 ID。它是隔离测试辅助工具,不是修改真实 SaaS 业务代码。 |
| 文件名、录音对象键、上传身份、会话和请求身份 | 不等同于业务 event_id;其独立身份与必要的文件名摘要保留,不改变源事件字段,也不做无关重构。 |
活动代码中的两个旧派生 ID 函数及其算法引用均已删除。历史证据、归档及独立负例保留原意,不作为运行回退或兼容规则。
## 保存结构与现存数据
SQLite 布局由 3 改为 **4**,`dispatcher_outbox` 主键为 `(dispatcher_id,event_type,event_id)`。启动先验证版本及这三个主键字段;旧布局明确报错且拒绝打开,不自动迁移、清库或用新空目录绕过。
`TestSourceEventIdentityRejectsVersionThreeWithoutWriting` 核实拒绝后旧版本号、表结构和既有反馈正文没有变化。本次仅在临时测试数据库中验证;未打开或改写线上数据库、Agent 恢复文件、录音或其他业务数据。**当前运行环境尚未切换此版本;不能直接用旧布局启动新版。** 后续若需要使用现存数据,必须另行确认数据处置方案,不能把本地通过当作迁移授权。
## TDD 与验证
原实现上先运行 `TestSourceEventIdentity*`:源 ID 被哈希、相同 ID 的多类型反馈冲突及旧布局误接纳均实际失败。测试专用 Mock 的原 ID 结果测试亦先失败,再修复验证器。日志留在未提交的 `.local/event-id-lifecycle/{red-tests,mock-red-tests}.log`。
| 项目 | 本次结果 |
| --- | --- |
| 原 ID、不同类型同 ID、独立确认、反序确认、错误类型确认 | `TestSourceEventIdentitySurvivesResultAndRestart`、`TestSourceEventIdentityAllowsIndependentFeedback`、`TestSourceEventIdentityConfirmationCannotConsumeAnotherType` 通过。 |
| 录音、最终结果和拒绝重放隔离 | `TestSourceEventIdentityUploadedResultRetainsOriginal`、`TestSourceEventIdentityUploadUsesOnlyFinalResult`、`TestSourceEventIdentityRejectionReplayOnlyRequeuesItsType` 通过。 |
| 确认丢失、不重复执行、原正文重发 | `TestOriginalEventIDSurvivesExecuteResultAndLostPublishConfirmation`、`TestExecuteTimeoutAndMQFailureDoNotRedial`、`TestFinalResultRestartResumesSameOutboxBody` 通过。 |
| 结束早于派发回执、矛盾内容、事务回滚 | `TestFinalResultBeforeOriginateAckDoesNotCreateSuccessReceipt`、`TestFinalResultRejectsChangedFactsAndUnboundRecording`、`TestFinalResultOutboxFailureRollsBackAndCanResume` 通过。 |
| 合同正常示例关联 | `TestCurrentCallExamplesRetainOriginalEventIdentity` 验证命令、派发回执及两种最终结果使用相同 ID;全部正反例通过。 |
| 实际隔离 RabbitMQ 结果收件 | `TestRuntimeIsolatedControlBacklogExecuteAndSharedResult` 实际核对同 ID 的回执/结果、AMQP MessageId 与正文;重复命令不重拨。 |
| 实际隔离 Agent/Dispatcher、双向 TLS 与 HTTPS 上传 | `TestDispatcherCommandStartsWithIsolatedMQHTTPAndAgent` 使用带 numberID 的原 ID,实际从共享队列读取回执及上传后的最终结果;一次模拟 HTTPS PUT,重启后重复命令不执行第二通。允许两种反馈乱序,分别核验收件。 |
| 全量本地检查 | `make check` 通过:格式、合同与 Proto 来源检查、`go vet ./...`、`go test -race ./...`、构建和实际隔离 MQ/HTTPS/mTLS 链路。Python 26 项通过。 |
| 业务单元覆盖率 | `make coverage` 通过;排除生成代码后的覆盖率 **72.8%**,不以集成测试提高该单元覆盖率。 |
最终检查日志:`.local/event-id-lifecycle/make-check-final.log`;覆盖率日志与原始 profile:同目录 `coverage-final.log`、`coverage.out`、`coverage-business.out`。这些是本地测试材料,不是线上证据。
## A01–A12 / K01–K16 本次对照
历史清单见 [P08 本地验收](saas-dispatcher-p08-acceptance.md);其旧合同位置、旧修订接纳规则和历史结果不代替当前共享合同。
| 编号 | 本次影响及核对 |
| --- | --- |
| A01/A04 | 直接影响事件身份语义与共享示例;共享提交、submodule、Schema 正反例与 Proto 检查通过,未改变 Proto。 |
| A02/A03 | 不新增运行入口或通信协议代次,MQ/HTTP 名称与拓扑不变;当前拓扑测试及实际隔离 MQ 回归通过。 |
| A05/A07 | 配置来源、关准入、任务控制和 SIP 屏障不放宽;既有全量测试通过,重复控制回执按类型独立保存。 |
| A06/A08 | 直接影响反馈确认;原消息身份重发和重复命令不重拨验证通过,未知执行及占用规则不变。 |
| A09/A10 | 直接影响最终结果、录音查询及重启恢复;上述结果/录音测试通过,旧布局拒绝且不改数据。 |
| A11/A12 | TDD、完整检查与 72.8% 覆盖率通过;仅本地,不扩大部署或真实业务授权。 |
| K01 | 拓扑与队列所有权不变,实际隔离共享队列收件通过。 |
| K02/K06 | 直接影响身份关联;原 ID 从执行到不同类型反馈一致,派发回执不等于通话结果。 |
| K03/K04/K05/K07 | 分页、停止不可恢复、暂停处理和 SIP 加载屏障不变;全量测试通过。 |
| K08/K09 | AI 授权与关键词行为未变;全量测试通过,未发真实供应商请求。 |
| K10 | 恢复间隔、48 小时起点与到期保留不变;录音失败/未知 PUT、确认丢失及重启测试通过。 |
| K11/K12/K13 | SIP 事实、外呼规则及单号拒绝语义不变;全量测试通过,不编造 SIP 码或放开授权。 |
| K14/K15/K16 | 通话结果、录音生成失败及双文件保存失败语义不变;相关测试通过,不伪造结果、不自动释放未知占用。 |
## SaaS 开发同步说明
共享合同 `main` 已包含本次变更,入口是 `ef3009d` 及正常 MQ 示例。最终结果返回下发的原 `event_id`,因此源 ID 中的 numberID 无需另加字段或解析哈希即可保留;派发回执与最终结果仍是不同事件类型。字段范围没有增加旧执行字段或兼容通道。
本说明用于交给 SaaS 开发人员同步现行合同;**尚无对方已读取、应用已修改或线上已收到新结果的事实,不代签这些事项。** 当前项目源码仅在本地分支提交,不推送本项目远端、不合入 main。
## 发布检查补充
固定源码提交后的本地发布检查结果另行记录;不代表部署或生产批准。