avatar.go、下载按钮、GIF)已移出本 change,归入 MIA-279。
净效果是负分歧:与上游的差异变小。
server/cmd/server/attachment_sweeper.go,上游 Multica 完全没有,是我们自己加的(0432ba618,2026-07-14 11:37)。
每 1 小时一轮、每批 100 行、循环抽干。找出 expires_at 到期且 issue_id / comment_id / chat_message_id 三个外键全为 NULL 的附件行,删库行并调用 store.DeleteKeys 删 S3 对象 —— 不可恢复。
agent.avatar_url 这个字符串字段,不是外键。于是正在被渲染的头像被判成垃圾删掉。
245_preserve_avatar_attachments.up.sql自己的注释:
"Migration 176 classified those rows as staged and could therefore expire an image that was still actively rendered by Web, Desktop, and Mobile."
而且迁移 176 把所有既存未绑定附件回填成 created_at + 7 days —— 上线那一刻,所有超过 7 天的老头像立即到期。迁移 248 结尾的 RAISE NOTICE '... matching attachment rows were missing' 说明当时已有一批 S3 对象被删且找不回。
之后为救它又加了:sweeper 里 4 条 URL 排除、迁移 245 的 1 个 PL/pgSQL 函数 + 4 个数据库触发器、迁移 248 的正则回写。起点只是想清理临时文件。
2878abf43、fccd4bda6、62dae5966、e1549f5e8、迁移 245、迁移 248,以及已废弃分支上的 d9a9f6835。248 自证)。multica agent avatar 走 /api/upload-file,把不可渲染的 /api/attachments/{id}/download 写进 avatar_url;该值与 attachment.url 不相等,迁移 245 的触发器不生效,7 天后仍会被删。attachment.sql 比上游多 9 条 fork 查询。| # | 决策 | 结论 |
|---|---|---|
| 1 | Work 附件读取授权 | 只校验 workspace 成员。以 MODIFIED Requirement 显式收窄已发布的 Work attachments remain creator-private across every access path |
| 2 | attachment_latest_head | 保持已发布语义:每附件不可变 original + 编辑产生的至多一个 CAS latest,只暴露 原版/最新版。不做同名合并 |
| 3 | 孤儿 blob | 不自动删除(仅禁止基于时间的后台回收) |
| 4 | 会话删除是否连带删对象 | 不删,对象成为明确接受的永久 orphan |
| 5 | URL / 可渲染性轴 | 移出本 change,归入 MIA-279;277 / 278 已在 issue 树上挂到 279 之下 |
| 6 | 基线分支 | origin/develop(不是 xisheng) |
loadWorkSession → GetWorkSessionForCreator(带 CreatorID,非 creator 直接 404);会话云盘 ListWorkSessionAttachments 的 handler 就走这个门禁。ListTaskMessagesByUser 对 creator-private 任务返回 403。所以私密性移到会话与消息读取层之后仍成立。但它现在是唯一一层,因此该门禁本身已写进 spec 要求,并有强制测试锁住(tasks 6.5)。
buildMarkdownURL 会直接返回公开、无签名、不过期的对象 URL,暴露面会从 workspace 成员扩大到任意持链接者。该形态的 URL 策略归 MIA-279 决定。| 删除项 | 来源 |
|---|---|
expires_at 列 + 索引 + sweeper 进程 + StagedAttachmentTTL + staged_ttl_secs | 0432ba618 / 迁移 176 |
迁移 245 的函数 promote_avatar_attachment() + 4 个触发器 | 迁移 245 |
附件级读授权:CanTaskReadAttachment、authorizeAttachmentRead、authorizeAttachmentForTask、authorizeAttachmentForUser | a21ae23a3 |
绑定的 uploader 等值判断(LinkAttachmentsToIssue 上的两行) | 0432ba618 |
ListWorkSessionAttachments、attachment_latest_head 及其 4 条查询、GetAttachmentForUpdate。GetRunningNativeWorkTaskForAttachmentUpload。GetWorkSessionForCreator 404、ListTaskMessagesByUser 403。attachmentRequiresCreatorScope:只摘掉它在授权上的用途,保留 URL 策略用途 —— 这正是两条轴能并行的关键。warnUnboundAttachmentIDs:请求的 --attachment-id 与响应 attachments 对账,缺失者 warn 到 stderr。linkAttachments 里「哪些 id 未绑定」的 slog.Warn。d9a9f6835 不在本分支基线内(只存在于已废弃的 fix/attachment-bind-authz-parity)。已在基线核实 ActorTaskID / resolveActorTaskID / actor_task_id / warnUnboundAttachmentIDs 全部不存在,而 LinkAttachmentsToIssue 仍带两行 uploader 条件。这是 code review 的 P1-1。
staged 附件 URL 改写是否移除及按部署形态的 URL 策略、是否删除 fork 专用 POST /api/avatar-upload、是否移植上游 avatar.go + /api/avatars/{sig}/*(MUL-5393 / #6024)、GIF 头像支持、下载按钮本体。
这是 code review 指出的一处自相矛盾(P1-5),已修正。
| 行为 | 结论 |
|---|---|
| 基于时间的后台回收(sweeper) | 禁止。不存在任何按时长自动删除的路径 |
| 删除 issue / comment / 显式删附件 | 保留删对象(基线行为,deleteS3Object(s)) |
| 删除 Work / chat 会话 | 只级联数据库行、不删对象;产生的 blob 是明确接受的永久 orphan |
| 未被任何容器引用的上传(孤儿 blob) | 永久保留,与上游一致 |
expires_at、删除附件级读授权、绑定回归上游、新增失败告警。expires_at 列还在但无人读写,迁移 245 的触发器还在但无害(只把该列置 NULL,已无消费者)。回滚 = 回滚代码,不碰数据库。
attachment.sql 从 28 条查询回到接近上游 19 条 + 云盘所需数条;attachment_sweeper.go 整体消失;file.go、service/issue.go 的 fork 分支大幅减少。
issue create 新增 stderr 告警)。Web / Desktop / Mobile-PWA / Shared frontend 不触及。attachment.sql、file.go、service/issue.go 是上游热文件,本 change 把它们朝上游收敛,下次导入冲突面下降。MIA-282 图片/附件文件存储与显示总入口(epic)
├── MIA-279 下载按钮无响应(urgent)——「URL / 可渲染性」轴的驱动 issue
│ └── MIA-278 头像方案用错需重设计
│ └── MIA-277 出厂默认头像丢失
└── MIA-284 Agent 建单附件挂到评论 ——「授权」轴,由本 change 覆盖
feat/attachment-model-upstream-realignment,基线 origin/develop(已于 2026-08-13 rebase 到最新 tip),2 个 commit,未推送。file.go / attachment.sql / service/issue.go / attachment_sweeper.go 的 diff 为空)。tasks.md 从第 0 组(停 sweeper)开始,每组一个 checkpoint commit。xisheng 不动。openspec/changes/attachment-model-upstream-realignment/ —— proposal.md / design.md / tasks.md / specs/{attachment-storage-model, work-session-attachment-drive}/spec.md。本页与之对齐,冲突时以仓库为准。