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)
- 现象:像素规整后的图片虽然保持了源图宽高,放大观察却能看到相邻逻辑块占用的物理列数或行数不同,表现为部分块更宽、部分块更窄;整数倍样例看起来正常,换一张网格数不能整除输入尺寸的图才复现。
@@ -566,6 +566,7 @@ game-project/
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/<taskId>.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,输入状态按文字与附件 token 的顺序保存,附件以文件名 token 内嵌在输入框中而非堆叠在下方;提交时才将 token 转为 LLM 可读的 `{1st attachment}` 引用。发送时弹出原生目录选择,目标目录存在且非空时必须二次确认;确认后调用 `init_local_game_project` 初始化本地项目、`upload_local_asset` 导入附件,再把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,写入最近项目并切到项目开发页。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。本流程不调用 `generate_local_game_draft`、`generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。
- 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。
- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”区块并显示无数据状态,但不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、管理对应 active/archived Session 并读取历史,用户消息和真实 Agent 回复只持久化到 `.agent/conversations/agents/<agentId>/` 下的规范 Session。普通用户窗口不得出现 `Agent 聊天` 导航、picker 或工具台入口。
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。
- 项目开发页保留左侧栏和顶部栏,顶部展示项目名、路径和最近 run 状态;中间只挂载 active `project-supervisor` Session 的正式对话面、Runtime 状态、确认/Needs input 和专业 Agent 协作只读状态,底部保留附件导入结果。真正的项目开发画布仍未落地;专业 Agent picker、完整计划和工具台继续留在开发入口。
@@ -729,7 +729,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorShowcaseAssetLike`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:`陶泥儿精选` 点赞去重表,主键由 `showcase_id:user_id` 组成。点赞 / 取消点赞通过登录接口更新该表,并同步回写 `editor_showcase_asset.like_count`;未通过审核或未展示的精选素材不能点赞。
- 说明:`陶泥儿精选` 点赞去重表,主键由 `showcase_id:user_id` 组成,是“某用户是否点赞”的唯一业务真相;`editor_showcase_asset.like_count` 只是全局聚合,不能反推出当前浏览者状态。点赞 / 取消点赞通过登录接口在同一事务中幂等更新去重行与聚合计数,并返回权威 `viewer_liked + like_count`;写路径只保留返回该 viewer 权威状态的 `set_editor_showcase_asset_like_for_viewer_and_return`,不保留只返回素材聚合快照的旧 toggle procedure。未通过审核或未展示的精选素材不能点赞。公开 `GET /api/editor/showcase/resources` 接受可选 Bearer:匿名不读取私有 like 表并返回 `viewerLiked=false`,有效登录态由 api-server 从 claims 派生 `viewer_user_id`,通过 runtime service identity 调用 viewer procedure,在公开排序 / 截断后按确定性主键读取每项状态。viewer 读取与所有 like 写 procedure 都先在 `ProcedureContext` 捕获 `ctx.sender()`,再在事务内校验现有 runtime service identity;不得信任 procedure 输入中的 `user_id` 作为调用方授权。无效 Bearer 或个性化读取失败不降级匿名。
- 索引:`by_editor_showcase_asset_like_showcase_id`、`by_editor_showcase_asset_like_user_id`。
### `editor_showcase_campaign_config`
@@ -1164,7 +1164,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
### api-server 长期订阅读模型
> 现役覆盖:`spacetime-client` 不再把旧创作入口配置、公开作品聚合表或逐玩法 gallery view 作为连接池必需订阅。`REQUIRED_CACHED_READ_MODEL_QUERIES` 当前为空,`user_account` 仅作为认证兼容所需的可选缓存。新版 `/creation` 的公开内容通过 `GET /api/editor/showcase/resources` 读取编辑器精选 read model;旧作品、入口配置和玩法统计表只保留 schema 数据壳,不再形成订阅 cache、BFF 路由或前端契约。
> 现役覆盖:`spacetime-client` 不再把旧创作入口配置、公开作品聚合表或逐玩法 gallery view 作为连接池必需订阅。`REQUIRED_CACHED_READ_MODEL_QUERIES` 当前为空,`user_account` 仅作为认证兼容所需的可选缓存。新版 `/creation` 的公开内容通过可选鉴权的 `GET /api/editor/showcase/resources` 读取编辑器精选 read model;登录态的 viewer like 投影在请求事务内读取,不进入共享长期订阅 cache,HTTP 响应固定 `private, no-store` 并按 `Authorization` 区分。旧作品、入口配置和玩法统计表只保留 schema 数据壳,不再形成订阅 cache、BFF 路由或前端契约。
#### 退役前历史订阅清单
@@ -79,7 +79,7 @@
精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,审核通过后也不会自动展示;运营可在后台按前台具体 Tab 设置分类(角色、UI、音乐、美宣)并开启展示,未设置分类的素材不会隐藏,会进入前台“全部”。旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才可进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等;批准状态、钱包返还流水和 `refund_completed_at` 必须在同一个 SpacetimeDB 事务中完成,任一步失败都保持待审核,历史已通过但未返还记录重复批准时按同一流水补齐且不得重复入账。已通过、已开启展示且返还完成的精选快照必须按同 owner 的精确 `assetObjectId` 或 `objectKey` 派生匿名读取授权,不能放开整个 `generated-*` 前缀。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
精选素材流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有列表,而不是固定只展示首屏数量。追加、筛选、分类和容器/卡片尺寸变化都必须按当前完整 DOM 顺序重算循环分列;位置未完整时保留正常文档流 fallback,不能让分页 sentinel 因容器短暂零高提前触发。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选;活动卡作为第一张进入第一列,普通素材从第二张继续同一循环列序。活动卡图片继续保持 private:后台直传 OSS 后必须先确认正式 `asset_object`,公开读取只由已启用 `global` 配置对活动卡专用目录中的当前 exact `imageObjectKey` 派生,禁用或替换图片后旧 key 不再可读;历史缺 metadata 的当前活动卡可通过同一 exact 配置授权恢复,但新上传不能跳过 confirm。
精选素材流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条。该 GET 保持公开,但接受可选 Bearer:匿名响应中每项固定 `viewerLiked=false`;有效登录态由 BFF 从已验证 claims 派生用户 ID,并在同一 SpacetimeDB 事务快照中返回该用户的权威 `viewerLiked` 与全局 `likeCount`。只要请求携带无效或过期 Bearer 就返回 `401`,个性化读取失败也显式失败,禁止静默降级成“未点赞”。响应统一发送 `Cache-Control: private, no-store` 与 `Vary: Authorization`。响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有列表,而不是固定只展示首屏数量。追加、筛选、分类和容器/卡片尺寸变化都必须按当前完整 DOM 顺序重算循环分列;位置未完整时保留正常文档流 fallback,不能让分页 sentinel 因容器短暂零高提前触发。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选;活动卡作为第一张进入第一列,普通素材从第二张继续同一循环列序。活动卡图片继续保持 private:后台直传 OSS 后必须先确认正式 `asset_object`,公开读取只由已启用 `global` 配置对活动卡专用目录中的当前 exact `imageObjectKey` 派生,禁用或替换图片后旧 key 不再可读;历史缺 metadata 的当前活动卡可通过同一 exact 配置授权恢复,但新上传不能跳过 confirm。
Tab:
@@ -116,7 +116,7 @@ Tab:
数据来源:
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId`、`assetId`、审核状态、展示状态、`showcaseCategory`、点赞数、生成成本、返还泥点、`authorDisplayName` / `display_name` 和 `authorPublicUserCode` / 陶泥号用于展示;前端默认进入“全部”,展示所有后端已经筛过的公开结果;角色、UI、音乐或美宣 Tab 只展示对应 `showcaseCategory` 的素材。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`。
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId`、`assetId`、审核状态、展示状态、`showcaseCategory`、`likeCount`、当前浏览者的 `viewerLiked`、生成成本、返还泥点、`authorDisplayName` / `display_name` 和 `authorPublicUserCode` / 陶泥号用于展示;前端默认进入“全部”,展示所有后端已经筛过的公开结果;角色、UI、音乐或美宣 Tab 只展示对应 `showcaseCategory` 的素材。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`。
- 全部、角色、UI、音乐、音效、视频和美宣都必须来自用户素材库中已提交并通过审核的生成素材;不要求素材当前仍保留在某个项目资源中,已通过审核的快照在原素材删除后仍可保留公开展示。
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
@@ -133,7 +133,7 @@ Tab:
- 最近项目继续使用编辑器项目接口:`listEditorProjects`、`createEditorProject` 和 `/editor/canvas?projectid=xxx`。
- 最近项目和项目页打开画布时,浏览器 history 只保留带 `projectid` 的最终画布路由;新建画布引导使用 `guide=toolbar` 一次性 query,主站九宫格直达生成器使用 `tool=<intent>` 一次性 query,画布消费后通过 `history.replaceState` 清理这两个参数。
- 项目封面逻辑复用 `/project` 项目卡已有的画布中心缩略图算法。
- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status` 和 `display_enabled` 为准,`showcase_category` 只用于前端具体 Tab 归类,前端只做展示组合、Tab 归类、点赞交互和提交审核的乐观状态回滚。
- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status` 和 `display_enabled` 为准,`showcase_category` 只用于前端具体 Tab 归类。点赞不做乐观图标或计数更新:按钮 pending 期间保持原状态,成功后应用服务端返回的 `viewerLiked + likeCount`,失败时保留原状态并在列表上方给出可访问短错误。登录、退出和账号切换都重载首屏并清空分页 / pending / 错误;首屏、分页和点赞回包必须按账号与请求代际丢弃旧响应。已知用户仍在恢复鉴权时不得先发匿名请求;不要求跨 Tab 实时同步。
- `/creation/<play>` 的玩法工作台、草稿、生成页、结果页、发布、运行态和作品架链路保持原状。
## 路由与导航