Merge remote-tracking branch 'origin/master' into codex/pr140-master-sync
Project CI / Repository checks (pull_request) Successful in 1m33s
Project CI / Frontend tests (pull_request) Successful in 3m24s
Project CI / Backend tests (pull_request) Successful in 4m43s
Project CI / Native shell tests (pull_request) Successful in 16m26s

This commit is contained in:
2026-08-12 14:40:25 +08:00
31 changed files with 1306 additions and 219 deletions
@@ -7078,6 +7078,13 @@
- 安全边界:动态端口不恢复旧 Vite 复用。无法证明 worktree 归属的监听器仍不复用、不主动终止;同用户多 worktree 通过段内漂移并行,不通过共享未知服务并行。
- 验证:公共端口映射、Linux 默认槽位与段内漂移、非 Linux 兼容漂移、Tauri 动态配置、启动前预检、进程树收束、AGC typecheck / 配置门禁、编码检查和差异检查必须通过。
## 2026-08-10 陶泥儿精选点赞状态使用可选鉴权 viewer 投影
- 背景:`editor_showcase_asset_like` 已持久化用户点赞,`editor_showcase_asset.like_count` 也会事务更新,但公开精选 GET 没有返回当前浏览者状态,前端每次挂载都从空本地集合开始,导致刷新后已点赞图标消失。
- 决策:保留同一个公开 `GET /api/editor/showcase/resources` 并接受可选 Bearer。匿名每项返回 `viewerLiked=false`;登录态只从已验证 claims 派生 viewer user ID,通过现有 runtime service identity 调用专用 SpacetimeDB viewer procedure,在同一事务快照中返回 `viewerLiked + likeCount`。无效 Bearer、viewer procedure 或私有读取失败显式失败,不降级匿名。GET 与点赞 POST 固定 `Cache-Control: private, no-store`,并在保留响应已有 `Vary` 字段的基础上追加 `Authorization`。
- 写入与前端:现有 like 去重表继续是用户状态真相,不新增字段、迁移或历史修复。写路径只保留返回 `viewerLiked + likeCount` 的 viewer set procedure,并要求 runtime service identity;删除只返回素材聚合快照的旧 toggle procedure,输入 `user_id` 只表示经 BFF 验证后代入的业务主体,不能充当调用授权。前端不做乐观图标 / 计数更新,成功只采纳服务端权威回包,失败保留原状态并显示可访问短错误;登录、退出、账号切换与鉴权恢复重载列表,并按 viewer scope / request generation 丢弃旧首屏、分页和写回响应。request generation 只在已提交的精选加载 effect 中激活,并在 cleanup 中失效,禁止 render 阶段写 ref 让被放弃的并发渲染污染当前请求。不做跨 Tab 实时同步。
- 影响范围:SpacetimeDB typed procedure 与 bindings、`spacetime-client` mapper / facade、api-server 可选鉴权与 public DTO、`editorProjectClient`、`CreationLandingView` 和对应回归测试;不修改持久 schema、migration 或 External v1 契约。
## 2026-08-10 完美像素最终持久化逻辑分辨率 PNG
- 背景:SpriteFusion Pixel Snapper 上游在网格采样后直接输出 `(列切线数 - 1) × (行切线数 - 1)` 的逻辑图,每个检测单元恰好对应一个输出像素。Genarrative 首版在此后增加了 nearest 输入尺寸恢复;当输入宽高不能被逻辑列数、行数整除时,最近邻只能把同一逻辑像素分配到 `floor / ceil` 数量不等的目标列或行,最终物理像素块宽窄不一。已有 `128 → 64` 的整数倍测试没有覆盖该问题。
@@ -4669,6 +4669,13 @@
- 处理:所有付费编辑器生成在队列 enqueue 前和 worker / inline 执行前复用只读 `preflight_editor_generation_target_and_return`,按认证 owner 校验可选项目及归一化目录;读取失败和归属不匹配一律失败关闭。helper 返回 canonical 项目与目录并覆写后续入队 / worker / 原子准备使用的 payload,不能校验 trim 后的项目却持久化原始空白值。角色图片、角色动作、图标 spritesheet 与 UI 提取省略目录时按实际默认目录预检;默认目录允许尚未创建,自定义目录必须存在且 owned。预检不替代最终 procedure 复验,也不保证跨外部调用的目录锁定。
- 验证:源码顺序回归必须覆盖图片生成、图片修改、图标 spritesheet、UI 设计图提取、视频、角色动作、SFX 与 BGM 的 enqueue / direct 两层,证明纯本地格式和 `data:` / `blob:` 稳定引用门禁先执行,canonical target 在预检后写回 payload,远端引用解析、generation input rebuild、扣费、入队、provider 与 OSS 均留在预检之后;模块侧扫描证明预检只调用 runtime identity、项目、目录只读校验且不含 insert / update / delete,并覆盖带空白项目、`project`、旧 `folder-*`、默认目录 ID、自定义目录与 `None` 归一化。
## 聚合点赞数不能恢复当前浏览者是否点赞(2026-08-10)
- 现象:陶泥儿精选点赞写入成功、总点赞数也正确,但刷新或重新挂载后图标恢复成未点赞;前端再次点击会发出错误意图或让计数体验混乱。
- 原因:`editor_showcase_asset.like_count` 只表达全局聚合,公开列表未携带 `editor_showcase_asset_like(showcase_id:user_id)` 的 viewer 状态;前端用生命周期内的空 `Set` 充当真相,刷新必然丢失。仅靠 `likeCount > 0` 无法判断其中是否包含当前用户。
- 处理:公开列表使用可选鉴权 viewer 投影;登录态从 Bearer claims 派生 user ID,并在公开列表事务内按确定性 like 主键返回 `viewerLiked`,匿名固定 false。个性化响应禁止共享缓存或错误降级,追加 `Vary: Authorization` 时不得覆盖 handler 或内层中间件已有字段。写入采用服务端确认式更新,账号 / 鉴权 scope 变化后重载并丢弃旧请求回包;request generation 的激活与失效必须跟随已提交 effect,不能在 render 阶段修改 ref;输入 user ID 不能代替 runtime service identity 鉴权。
- 验证:覆盖刷新 / remount 保持已点赞、pending 期间不改图标计数、失败保留旧状态并播报错误、分页保留 viewer state、鉴权恢复不发匿名请求、登录 / 退出 / 换号与旧首屏 / 分页 / POST 回包竞态、被 Suspense 放弃的 viewer 渲染不影响当前已提交请求,以及无效 Bearer 返回 `401 + private,no-store + Vary: Authorization`;中间件测试另需证明已有 `Vary` 字段被保留。
## 非整除 nearest 会让逻辑像素块宽窄不一(2026-08-10)
- 现象:像素规整后的图片虽然保持了源图宽高,放大观察却能看到相邻逻辑块占用的物理列数或行数不同,表现为部分块更宽、部分块更窄;整数倍样例看起来正常,换一张网格数不能整除输入尺寸的图才复现。