| # | Finding | 复核结果 |
|---|---|---|
| P1-1 | 任务基于一个不在基线的提交 | 成立,我的错。git merge-base --is-ancestor d9a9f6835 HEAD 返回否;分支内 grep ActorTaskID / warnUnboundAttachmentIDs / actor_task_id 全部为空 |
| P1-2 | 新 capability 没有取代已发布的 Work 附件契约 | 成立,最严重(见 02) |
| P1-3 | 「同名文件 latest」语义不符 | 成立,我误导了你(见 03) |
| P1-4 | 移除 URL 改写扩大可读范围 | 机制成立。对我们自己的部署大概率不触发(CloudFront 签名),对「公开 CDN + 无签名」这类受支持部署成立 |
| P1-5 | 生命周期规范自相矛盾 | 成立,是我 spec 自己的矛盾 |
| P2 | CLI 换端点回归 GIF 支持 | 成立。而且它反过来证实了你的判断(见 04) |
| 范围 | 基线分支选错 | 成立。我基于 origin/xisheng,集成分支是 origin/develop(领先 12 个 commit;xisheng 另带 4 个无关 release-note commit) |
fix/attachment-bind-authz-parity 上做了 d9a9f6835,后来从 origin/xisheng 全新建了 feat/attachment-model-upstream-realignment,那个 commit 根本没进新分支 —— 但我写 tasks.md 时当它在。于是任务 4.1–4.7 一半是「删不存在的代码」,一半是「保留不存在的能力」。
已发布的 rollica-work-surface 里有这么一条硬性要求:
openspec/changes/rollica-work-surface/specs/work-navigation-and-direct-runtime/spec.md:637
### Requirement: Work attachments remain creator-private across every access path
"across every access path" —— 这正是决策 A(附件读取只校验 workspace 成员)要去掉的东西。两者不可能同时成立。
rollica-work-surface 的 spec。| 选项 | 含义 | 代价 |
|---|---|---|
| A′ 坚持 A 并显式撤销该契约 | 把 Work attachments remain creator-private 标为 REMOVED / superseded,写清「对同 workspace 成员在已知 UUID 时可读」 | Work 私密性从「每条访问路径」降级;私密文件夹(MIA-249)那条线的产品叙事要跟着改 |
| B 回到容器继承 (我仍然推荐) | 附件属于 Work/chat 会话时,读取要求会话可见性;其余五样照删 | 比 A 多一处判断、一条查询。TTL / GC / 触发器 / URL 改写 / uploader 等值仍然全删 |
attachment_latest_head 描述成「同名文件最新版本」,你回「对,这是我要的产品行为」。我的描述是错的。
代码事实:
attachment_latest_head 主键是 attachment_id(迁移 236)。ListWorkSessionAttachments 只按 chat_session_id 取 chat_message_id IS NOT NULL 的行,没有任何按文件名合并。已发布契约(同一份 spec :1491)写的是另一回事:
Every cloud Session attachment SHALL retain its immutable message-bound original. Editable supported formats SHALL maintain at most one CAS-protected latest head. The product SHALL expose onlyOriginal / 原版andLatest / 最新版.
即:每个附件保留不可变原版 + 最多一个 CAS 保护的最新版,最新版来自「编辑」而不是「重传同名」,UI 只暴露 原版 / 最新版 两档。
| 选项 | 开发量 |
|---|---|
| 1 · 保持已发布语义(每附件 original + 编辑产生 latest)—— 推荐 | 零。它已经在跑,spec 只需把我写错的措辞改回来 |
| 2 · 真要按「会话 + 文件名」合并(重传同名推进 head) | 新功能:新身份定义、新查询、迁移、冲突规则(同名不同类型?谁覆盖谁?),要单独立 change |
POST /api/avatar-upload + UploadAvatar + isAllowedAvatarContentType,来自 e1549f5e8(2026-07-29)。origin/main:没有 UploadAvatar、没有 avatar-upload 路由。上游 avatar.go 注释原话:
avatar_url columns (user / agent / squad / workspace) store the raw storage object URL the upload returned.
也就是上游就是走普通附件上传接口拿到存储 URL、直接存进 avatar_url。
/api/upload-file 就又返回可渲染的存储 URL,头像走通用附件接口自然恢复。届时:
/api/avatar-upload、UploadAvatar、isAllowedAvatarContentType 全部变成死代码 → 删掉。上游后来发现:在私有 bucket、无公开 CDN 的部署上,原始存储 URL 在浏览器里必然 403(upstream #6024)。上游的解法不是回到专用上传端点,而是加了一个签名的公开头像读取路由:
server/internal/handler/avatar.go # MUL-5393 / #6024
r.Get("/api/avatars/{sig}/*", h.ServeAvatar)
Rollica 没有这个文件 —— 上游是在我们快照之后加的。而我们的部署看起来正是「需要它」那一类:helm values 有 cloudfrontDomain / cloudfrontKeyPairId,本地 .env 有 CLOUDFRONT_DOMAIN / CLOUDFRONT_KEY_PAIR_ID / ATTACHMENT_DOWNLOAD_MODE / S3_BUCKET。CloudFront 签名一旦启用,storageURLIsPubliclyReadable 恒为 false,原始 URL 不可直接渲染。
⚠️ 我只看了配置项名字,没有读生产环境的实际取值(也不该读)。所以这是强指示、不是证据。
avatar.go,否则再挂一次。
d9a9f6835 不在分支里,所以 4.1–4.6 从「回退」改成「按 develop 现状新增上游形态的改动」;4.7 从「保留 CLI 告警」改成新增 warnUnboundAttachmentIDs 与服务端 dropped-id 告警(spec 要求它们存在,基线里没有)。origin/develop 重开分支,只 cherry-pick spec commit 0a8a295b0,甩掉 4 个无关 release-note commit。同时修掉会话记忆里「基于 origin/xisheng」那句错误规则。| # | 问题 | 答复 | 已落地 |
|---|---|---|---|
| 1 | A 撤销已发布的 Work creator-private 契约:坚持 A′ 还是回 B? | 坚持 A。理由:非 creator 成员事实上拿不到附件 id,私密性仍在 | 已在代码层验证前提(GetWorkSessionForCreator 404 / ListTaskMessagesByUser 403);写成 MODIFIED Requirement + 强制测试 |
| 2 | latest 语义:保持已发布,还是做同名合并? | 保持已发布语义 | spec 已改回「每附件 original + 编辑产生 latest」,重传同名产生独立附件 |
| 3 | 生产是否启用 CloudFront 签名?(决定头像方案与 P1-4) | 并入 MIA-279 一起决定 | URL 轴整体移交 MIA-279;本 change 保留 attachmentRequiresCreatorScope 的 URL 用途以便并行 |
| 4 | Work/Chat 会话删除要不要连带删 blob? | 不删 | 已写进 spec 为「明确接受的永久 orphan」,并与「禁止基于时间的回收」拆开 |
| 5 | 基线换到 origin/develop? | 是,但保留分支与 worktree | 用 rebase --onto 只重放 spec commit,甩掉 4 个无关 release-note commit;2026-08-13 再次 rebase 到最新 tip |
| 6 | GIF 头像继续支持? | 继续 | 随 URL 轴一起处理:删掉专用端点后格式白名单消失,问题自动解决 |
tasks.md 从第 0 组(停 sweeper)开始动代码,每组一个 checkpoint commit。