feat: L0 线协议冻结 + 附件链路修复 + 人/Agent 区分
L0 核心: - 严格解码 Decode(DisallowUnknownFields) 全覆盖 29 个 DecodeBody 调用点 - DecodeLenient 心跳专用:容忍新字段但回报 unknown_fields - 400 消息列出本端点接受的全部字段(jsonFieldNames 反射 tag) - 日历 status 校验(create 补字段 + update 拦非法值) - 新增 strictdecode_test.go 10 例 + blob/list_test.go 6 例 A-4 附件挂载回滚:checkAttachable 在 CreateMail 前校验,失败按 解挂→释放 relay→删邮件→退预算回滚,幽灵邮件这条路堵住了 A-5 反向 GC:blob.Store.List() 枚举磁盘(跳 .upload-*), SweepUnreferencedBlobs 按 attachments + calendar_attachments 反查, 48h 年龄下限兜上传窗口。已接进每小时 sweep 循环 C 人/Agent 区分:四个读路径 + threadCols 补 from_human / to_human (EXISTS users 判定),models.Mail 加 ToHuman。前端判据从 workspace 启发式改成显式布尔,mailCounterpart/sessionCounterpart 从 session_workspace 取 path(修 dsh@dsh 拼接 bug) 契约文档:SSE new_mail 补 4 字段(in_reply_to/from_human/ permission_mode/permission_enforcement),B-5 加 B-5.6 (Agent→Agent 不转发),B-3.4 MUST 改条件式,心跳补 mode_enforcement + unknown_fields,demo 死链修复 + from_human 检查 验收清单加 Agent→Agent 负向对照项
This commit is contained in:
@ -338,7 +338,39 @@ func attachAll(w http.ResponseWriter, r *http.Request, mailID uuid.UUID, ids []u
|
||||
if len(ids) == 0 {
|
||||
return true
|
||||
}
|
||||
err := repo.AttachToMail(r.Context(), mailID, ids, uploader)
|
||||
return writeAttachErr(w, repo.AttachToMail(r.Context(), mailID, ids, uploader))
|
||||
}
|
||||
|
||||
// checkAttachable 在**产生任何副作用之前**校验附件可不可挂。
|
||||
//
|
||||
// 返回 false 表示已写出错误响应,调用方应立即返回。
|
||||
//
|
||||
// # 为什么不能只靠 attachAll
|
||||
//
|
||||
// attachAll 在 CreateMail **之后**调用,于是附件不合法时请求返回 403/409,
|
||||
// 但那封邮件**已经入库、已经通知了收件人、已经扣掉了会话预算**。
|
||||
// 生产实测:两封探针邮件(一封 403「只能附加自己上传的附件」、一封 409
|
||||
// 「附件已随其他邮件发出」)都躺在 mails 表里,used_rounds 也涨了。
|
||||
// 发件方看到 4xx 会重试,收件方于是收到两封。
|
||||
//
|
||||
// 纯输入校验必须在副作用之前做完 —— 与「400 之后会话已建好」是同一个教训。
|
||||
//
|
||||
// 它**不取代** attachAll:两次调用之间仍有竞态窗口(另一个请求把同一个附件
|
||||
// 挂走了),那一次由 attachAll 的原子 UPDATE 拦下、并由调用方回滚。
|
||||
// 双层分工:这里挡住绝大多数(拼错 id、拿别人的附件、重复挂),
|
||||
// attachAll 挡住真正的并发。
|
||||
func checkAttachable(w http.ResponseWriter, r *http.Request, ids []uuid.UUID, uploader string) bool {
|
||||
if len(ids) == 0 {
|
||||
return true
|
||||
}
|
||||
return writeAttachErr(w, repo.EnsureAttachable(r.Context(), ids, uploader))
|
||||
}
|
||||
|
||||
// writeAttachErr 把 repo 层的附件错误映射成 HTTP 响应。
|
||||
//
|
||||
// checkAttachable 与 attachAll 共用一份:同一种错误在两条路径上必须给出同一个
|
||||
// 状态码与同一句话 —— 分开写早晚会分叉,而调用方无法区分自己碰上的是哪一层。
|
||||
func writeAttachErr(w http.ResponseWriter, err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user