合并 origin/master:保留 3D 契约与 provider checkpoint,旧玩法表随主线退役

- server-rs/crates/shared-contracts/src/lib.rs:采用主线的模块裁剪(删掉 cfg(any()) 遗留模块、补上 agc_analytics 与 game_distribution),同时保留本分支的 editor_canvas、model3d 模块与 EDITOR_GENERATION_OPERATION_KINDS 导出
- server-rs/crates/spacetime-module/src/migration.rs:主线删除的 410 行旧玩法表 normalize 段保持删除,保留本分支的 provider_kind / provider_task_id 兼容段与对应用例
- docs/【开发运维】本地开发验证与生产运维-2026-05-15.md:校验清单同时保留 Tripo 3D 生成任务与 editor_background_music_generation / model3d_text_to_model / model3d_image_to_model
- .gitignore:补回本分支新增的 3D 模型文件忽略规则(*.glb / *.gltf 等 12 行),压测数据段随主线一并删除
- 共享记忆:本分支的 3D 决策与踩坑条目保留在主线重排后的 decision-log.md 与 pitfalls.md 中
This commit is contained in:
2026-09-23 19:39:30 +08:00
997 changed files with 56264 additions and 80302 deletions
+6 -2
View File
@@ -19,12 +19,14 @@
## 当前产品与平台
- [当前产品与工程约束](./【项目基线】当前产品与工程约束-2026-05-15.md):现役入口、账号钱包、UI 和后端分层。
- [平台入口与玩法链路](./【玩法创作】平台入口与玩法链路-2026-05-15.md):只描述现役平台壳与旧模板退役边界。
- [平台入口与玩法链路](./【玩法创作】平台入口与玩法链路-2026-05-15.md):只描述现役平台壳与图片画布编辑器链路。
- [外部 OpenAPI 与 API Key 接入方案](./【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md)
- [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json):公开 HTTP 契约唯一机器可读来源。
## AI 游戏创作与 Agent Runtime
- [客户端本地埋点与主站入库契约](./technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md):本地 12 类事件采集、每 15 分钟上传、私有事件表、确认后清理与后台明细查询已完成隔离环境验收;不扩充采集范围、不做加密,未部署生产。配置要求及验证边界见第 13 节。
- [AGC 资源 kind 枚举化契约](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-09-15-gamecreationapp-资源-kind-枚举化当前权威口径):GameCreationApp 资源 kind 的 Rust enum、ts-rs 绑定、Unknown 可观测性和 shell 内重构边界。
- [策划 Agent 生产迁移与工作区浏览](./technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md):已完成;当前策划入口统一使用 Design Agent,采用阶段审批与用户工作区文件浏览。旧 V1/V2 会话、命令、专用展示和测试不再作为兼容目标。
@@ -43,6 +45,8 @@
- [DirectProject Codex 原始历史与异常恢复](<./technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md>):原始 Responses item 持久化、线程注入与异常回合收尾。
- [DirectProject 对话历史单一事实源](./adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md):AGC 项目开发对话只以项目对话历史与运行态事件为真相源,聊天投影不落盘。
- [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。
- [退役 AGC 项目对话斜杠命令](./adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md):AGC 项目对话与终端 swarm chat 均不再解析斜杠命令,终端聊天入口一并退役;实现、测试、门禁与文档承诺全部删除,命令 id 与权限位作为项目策略词汇表保留。
- [引用候选由宿主注入](./adr/【ADR】引用候选由宿主注入-2026-09-22.md):引用输入区只接受宿主注入的引用 provider,素材选择面板独立成组件,附件芯片成为本轮附件唯一事实源。
- [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
- [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。
@@ -135,5 +139,5 @@
- 文档现行/历史/待复核状态以[文档生命周期与现状索引](./【协作规范】文档生命周期与现状索引-2026-09-12.md)为准;未列入当前入口的文档不能直接作为实现依据。
- `docs/project-memory/shared-memory/` 只保存长期有效的概览、决策、流程和踩坑;`plans/`、`todos/` 仅保存仍开放且有明确下一门禁的事项。
- 修改 `/api/external/v1` 必须同步 OpenAPI 和契约测试;修改 SpacetimeDB schema 必须同步 migration、表目录、绑定和 schema 检查。
- 旧模板、旧公开作品、旧运行态和旧后端路线不因历史源码或数据表仍存在而恢复入口。
- 当前入口、公开契约和数据对象只以现役代码、路由树、shared-contracts 与 SpacetimeDB schema / bindings 为准;未登记对象不得作为兼容目标恢复。
- 新增 Markdown 使用 `【标签名】中文标题-YYYY-MM-DD.md` 命名;不要把个人配置、密钥、Token、日志或构建产物写入文档。
@@ -51,5 +51,7 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
- 旧项目磁盘上遗留的 `turn-stream.jsonl` / `tool-calls.jsonl` 保留不动,不迁移、不清理、不再由 DirectProject 聊天框读取。
- 工具卡片的脱敏与截断必须在读取期执行一次,不能因为"原始条目已在磁盘"就把未脱敏内容直接渲染到界面。
- 回合结束语义务必由 `turn.completed` 判定;缺少该事件的残留回合不得被渲染成运行中。
- 「活动回合的唯一判据」约束的是**原生回合**:界面上的「本地已发出、原生还没认领」是投影的展示态(`DirectChatTurn.state = 'awaiting-start'`),由本地在途用户条目身份派生,不构成第二套原生生命周期,也不参与 `turnRunning` 的判定。
- 三层数据流、变量归属与一次发送的时序写在代码里:`apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts` 的模块注释;回合三态的定义与判据真值表在 `apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnPresentation.ts` 的 `DirectChatTurnState`。改判据时同步这两处与对应测试。
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致;文件系统不得再新增 `turn-stream.jsonl` / `tool-calls.jsonl`。
- id 空间已用源码核对:codex-rs `app-server-protocol/src/protocol/thread_history.rs` 中所有工具 item 都是 `id: payload.call_id.clone()`,而 `project.jsonl` 落盘的是原始 response item。真实 app-server 会话核对仍列为运行时验收项。
@@ -0,0 +1,36 @@
# 引用候选由宿主注入
状态:已接受
引用输入区(`ResourceReferenceInput`)不再自己去拿候选。素材清单、Skill 目录、缩略图预览这些项目级或应用级事实全部改由宿主注入:输入区只接受一组「引用 provider」(每种引用一份,暴露触发符、候选、身份解析与正文文本形态;需要为菜单开合拉一次数据的 provider 另有 `onMenuQueryChange`,由输入区在 effect 里回调),宿主按需选择性注入——只注入资源 provider 就只存在 `@`,再注入 Skill provider 才出现 `$`。素材选择面板同时拆成独立组件(`ResourceReferencePicker`),自己拿数据、由宿主渲染,确认后经输入区句柄的 `insertReferences` 交回。
这么改的理由是双向的。留在组件里的读取属于后端副作用,越过了「共享表现组件不拥有后端副作用与正式业务状态」的边界;而「组件自己查 Skill 目录」又让所有宿主无差别获得 `$` 候选,可只有 DirectProject 那条路径会把 `agc_skill_reference` 解析成真 Skill,其余宿主只把它退化成字面文本,形成误导入口。注入之后「没注入就没有这类引用」成为默认,可见性不再需要额外的开关。
## 备选与取舍
- 只把 Skill 目录外移、素材继续留在组件内部:改动更小,但输入区仍要知道资源种类的字段(显示名、版本 scope、可提及过滤),「只认接口」不成立。
- 一个总装 builder 统一产出全部候选:调用方接线更短,但所有种类被焊死在同一层,无法只注入资源、也无法单独测试某一种引用。
- 附件继续留在正文之外(待发送列表):改动更小,但本轮附件会有两份事实源(正文芯片与列表条目),提交时还要再拼一遍;收敛成一份之后,移除语义、上限口径与排队路径都自然归位。
## 影响
- 输入区删除 `versions` / `activeVersionId` / `showTriggerButton` / `onReferencePickerOpen` / `skills`,`assets` 被注入的 provider 取代;`projectPath` 只保留给输入区自己的润色链路。
- 输入区与宿主之间只留三个通用接缝:`providers`(引用来源)、`inputActions`(操作排里的宿主控件,例如 `@` 触发钮)、`submitSuppressed`(宿主浮层打开时 Enter 让位)。引用种类一个都不进输入区。
- `provider.fuzzyLookup` 必须是纯函数(输入区在渲染阶段调它取候选);懒加载走 provider 的可选 `onMenuQueryChange(query)`,由输入区在 `useEffect` 里回调,菜单关闭时收到 `null`。Skill 目录因此第一次敲出 `$` 时才读,渲染期不再有 invokes 或 ref 写入。
- 附件并入 `ChatReference`,编辑器收敛为单一引用节点类型,附件 chip 的 DOM 契约逐字保留;附件导入成功后以芯片进入正文,失败不插入;控制器不再持有附件数组,`MAX_CHAT_COMPOSER_ATTACHMENTS` 改为按草稿中的附件芯片数计算,导入进行中禁止发送。
- 用户可见行为保持不变,唯一例外是已裁决的缺陷修复:非 DirectProject 宿主不再出现 `$` Skill 候选。
## 修订(2026-09-22):粘贴解析需要第二道只读缝
粘贴进来的纯文本要按同一套引用文本语法反解析回正文芯片,因此 provider 的两个查询能力按「模糊 / 精确」分开命名,各自说清自己的语义:
- `fuzzyLookup(query)`:候选菜单那条路——按 query 做包含匹配、大小写不敏感,并在 provider 内部截断到候选上限。名字写明它是模糊的,避免被拿去反查 token。
- `lookup()`:精确查找那条路——某一刻 provider 真正能解析出的全部引用,不做模糊过滤、不截断,与菜单共用同一份候选来源;仍是纯函数,由输入区在粘贴事件里同步调用,数据没到就是空数组。
这不推翻本 ADR 的懒加载结论:`onMenuQueryChange` 仍是唯一的懒加载入口,粘贴解析**只用此刻就绪的候选**,不等待、不补读。Skill 目录因此还是「用户第一次敲出 `$` 才读」——冷启动时粘贴 `$名称` 就按字面文本保留(看得见、不是猜错),不为了粘贴去提前读盘。
附件与运行画面区域仍是静默 provider:它们没有候选,所以粘贴解析不认 `@附件名` / `@区域标签`,这两类 token 粘贴时逐字保留(附件与运行区域的身份来自文件与 run,纯文本重建不出来)。
歧义口径:同一个 token 对应多条引用身份(同名素材)时一律按文本保留;解析只认显示名逐字一致(不认扩展名、resourceId、大小写变体),未命中的 token 与其余文字逐字保留。
(本次把上一条同名决策里的 `match` / `candidates` 改名为 `fuzzyLookup` / `lookup`,语义不变;旧名不再保留。)
@@ -0,0 +1,34 @@
# 【ADR】退役AGC项目对话斜杠命令与终端swarm入口-2026-09-22
状态:已接受
## 背景
AGC(`apps/ai-game-creator-shell`)的项目对话曾把用户能力挂在「聊天输入 `/<cmd>`」上:除真正带执行语义的 `/history` 外,还累积了几十个只生成后续草稿或只读摘要的命令(`/brief`、`/status`、`/read`、`/trace`、`/art`、`/export`、`/cover`、`/remember` 等)。无 GUI 的终端 swarm chat 入口 `--swarm-chat` 另有一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。
这些命令的实际状态是:正式对话面已经换成 DirectProject 单容器,斜杠命令列表不再有渲染入口,`/history` 之外没有任何现役调用方;但命令字面量仍分散在控制器分支、`chatPromptPolish` 的 `/` 前缀绕过、命令参数校验文案、`projectSummaryConstants` 的命令清单、只服务已退役摘要面板的 `project-summary/*Summaries.ts`、构建期门禁 `scripts/check-config.mjs`、无人调用的 Tauri 能力清单命令、`swarm_cli` 的终端输入解析与帮助输出,以及多份权威文档的承诺里。
保留它的代价持续存在:每次调整对话形态都要同步维护这套死词汇表和它的门禁,而且「命令」一词在 AGC 里同时指用户斜杠命令与项目权限命令 id(`GAME_CREATION_APP_COMMANDS`)两件事,术语歧义会直接误导后续改动。
## 决策
- 斜杠命令语义整体退役,按「从未存在」处理:不保留入口、不做兼容提示、不写 tombstone,实现、专属测试、构建期门禁条目与文档承诺一并删除,历史由 Git 保存。
- 删除项:Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`;`chatPromptPolish` 的 `/` 前缀绕过;`chatCommandMetadata`、`projectSummaryConstants.chatCommandHelp`、`memoryCommands.parseRememberInput` 等命令清单与参数解析;只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`;草稿回填死链(前端 `agentPresentation.ts` 的草稿推导与 Rust `suggested_canvas_tool_call`);无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` 与 `get_limited_local_commands`;钉住上述字符串的门禁条目与专属测试。
- 终端 swarm chat 入口连带其命令层整体退役:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit` 的解析、帮助输出、turn 派发、观察器、报告与专属测试)一并删除;`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务终端命令的 `agent.compact` / `agent.resume` / `agent.run_status` 校验(`swarm_cli/input.rs` 内那份)与 `print_runtime_response_stream_status` 也随之消失;同名权限 id 在现役 Tauri 命令与 Runtime 生命周期上的门禁保持不动。只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`)同样删除;其上仅存的自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at` 在收尾复查后一并删除(见「影响」的复查收尾)。
- 保留项(它们不是斜杠命令):命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(Rust 运行期项目权限策略的词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计日志粒度,数组本体由 Rust 策略路径与跨语言一致性门禁消费)、`needsInitializedChatProject` 与项目权限策略链路、Rust 侧路径与路由的 `/` 前缀校验、`--agent-*` CLI 控制命令(Goal、steer、cancel、retry、context compact、resume、状态查询)与 Tauri IPC 注册。
- 应用内项目对话的输入只剩自然语言回合(外加 `@` 素材引用与附件);需要动作时由 Runtime 工具、确认卡和既有 CLI 控制命令承接,不由聊天文本解析控制词。
- 需要显式控制时改用现有 `--agent-*` CLI 命令:手动压缩是 `--agent-context-compact`,恢复扫描是 `--agent-resume`,Goal 生命周期是 `--agent-goal-*`。
## 备选方案与取舍
1. **只删正式用户窗口的入口,保留解析层**:看似省事,但命令字面量与分支继续存在,新对话形态仍要绕过它们,正是本次要消除的持续维护成本。
2. **保留 `/history` 作为唯一命令**:它确实是唯一有执行语义的入口,但保留一个精确匹配的 `/` 语法就要求保留前缀绕过判断、命令被发现与文档承诺的整套口径;DirectProject 的历史重读改由重新进入对话/重新订阅自然完成,不需要用户输入控制词。
3. **加兼容层(识别到已知命令时给提示或忽略)**:等于把死词汇表永久固化在解析层,与「按从未存在处理」相反,且会长期占据用户可见面。
## 影响
- 术语收敛:AGC 里的「命令」此后指项目内部命令 id 与权限位,「斜杠命令」作为已退役说法不再出现在权威文档与代码注释中。
- 应用内项目对话的可见行为不变:正式对话面本来就不渲染命令列表,`/history` 之外没有可执行路径;删除后唯一的用户可见差异是输入以 `/` 开头时按普通文本处理。终端侧不再有 swarm chat 入口,也没有任何斜杠命令面。
- 删除范围包含构建期门禁条目,因此不得为退役概念新增守卫测试或断言残留字符串的 check 条目;防止概念回归依靠架构边界(没有解析层可写)而不是字符串钉桩。
- 复查收尾(同一决定的后续提交):配置向导里指向已删 npm 脚本的 `test:chat` 调用与失去含义的 `--configure-only` 开关、App 内 `请先用 /project …` 用户文案、`interaction.json` 中只服务已删交互内核的 7 个 prompt 键、harness 里因调用方被删而零引用的死导出,以及过期注释一并清理;无前端调用方的 Tauri 命令 `start_game_creator_agent_runtime_task` 随本次清理删除(`main.rs` 注册与 `check-config.mjs` 条目同步移除);`steer_game_creator_agent_runtime_task` 同时是 `decide_game_creator_agent_runtime_steer_at` 的唯一非测试入口,删掉该命令后整条 LLM steer 判定链(`agent/interaction.rs`、`runtime.interrupt_for_steer_decision`、`agent.runtime.steer_decision` 持久记录、`steer_decision_*` prompt 键与只测该链的专属用例)一并退役。
- `.agent/logs/command.log`、项目权限确认卡与 `commandRuns` 属于项目命令审计,继续保留,不受本次退役影响。
@@ -10,7 +10,7 @@
- 允许修改:
- `apps/ai-game-creator-shell/scripts/channel-identity.mjs`(新增,渠道身份单点定义)
- `apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`、`agent-swarm-test-chat.mjs`
- `apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`
- `apps/ai-game-creator-shell/src-tauri/src/main.rs`、`src-tauri/src/windows.rs`、`src-tauri/src/config.rs`
- 对应测试:`build-release.test.mjs`、`prepare-macos-codex.test.mjs`
- 文档:AGC 更新主规范、共享记忆与本计划对
@@ -47,3 +47,10 @@
- 风险:非默认渠道首次以新身份安装,老 `dev` 用户不会自动迁移本地数据。回滚点:渠道身份只影响非默认渠道构建,撤销该渠道的构建产物即可,仓库侧无数据迁移。
- 风险:窗口标题改为构建期产品名后,标题不再等于配置里的字面量。回滚点:去掉 `main.rs` 的标题覆盖调用,行为回到配置标题。
- 风险:ACL managed 识别放宽到前缀族。回滚点:`is_game_creator_packaged_app_data_leaf` 收紧回单一直线值,但非默认渠道的提权修复会重新失败关闭。
## 2026-09-23 追加:dev 渠道展示名统一
- `dev` 渠道继续复用 `world.genarrative.ai-game-creator`,保证既有安装、升级链和 AppData 路径不变;展示名统一为 `陶泥儿开发版`。
- `channel-identity.mjs` 是展示名单一来源。发布构建将同一 `productName` 同时注入 Tauri 安装配置、原生窗口标题和 `VITE_AGC_PRODUCT_NAME`;React 自绘标题栏及关于/运行时配置展示从该注入值读取。
- 因此 Windows NSIS 默认生成的快捷方式、开始菜单/卸载注册表展示名随 Tauri `productName` 变为 `陶泥儿开发版`;未新增自定义注册表或快捷方式实现。
- 静态 Tauri 基线配置同步为 `陶泥儿开发版`,本地壳与正式 `dev` 包的显示名保持一致。
@@ -38,6 +38,6 @@ Parent Milestone: `【里程碑】AGC项目定时快照上传-2026-09-17.md`
## 风险与回滚
- 上传体积与带宽:首轮全量可能很大,先设单文件与单次同步总量上限并把超限项记入跳过清单;不静默截断。
- 数据出境边界:只上传项目目录内普通文件,排除 `.agent/runtime`、`.agent/logs`、`.git`、构建产物与临时文件;凭据类文件不在白名单内。
- 数据出境边界(2026-09-22 修订):只上传项目目录内普通文件。`.agent` 承载项目身份与 Agent 状态,整目录上传(含 conversations、logs、runtime、checkpoint、`agent.db`);`.git`、构建产物、临时文件与凭据类文件仍不在白名单内。该修订只作用于快照同步,项目索引与 checkpoint 继续排除整个 `.agent`。
- 服务端未配置 bucket 时客户端必须失败关闭,不能把本地索引推进成"已同步",否则后续同步会漏传。
- 回滚:客户端可停用触发接线(保留模块与测试)即可回到无上传行为;服务端路由与配置项可单独移除,不影响既有 OSS 前缀与错误报告链路。
@@ -0,0 +1,63 @@
# 引用粘贴解析实施计划
对应:[引用候选由宿主注入](../../adr/【ADR】引用候选由宿主注入-2026-09-22.md)(含 2026-09-22 修订节);决策记录见 `docs/project-memory/shared-memory/decision-log.md` 的 2026-09-22 条目。上一步的重构计划见 [引用输入区重构与宿主注入](【实施计划】引用输入区重构与宿主注入-2026-09-22.md),那份明确不含本项。
## 一句话交付与验收判据
把从用户消息气泡(或任何同口径文本)复制出来的 `@显示名` / `$名称` 粘贴进引用输入区时,原位重建同顺序的引用芯片;其余文字逐字保留。
验收判据:
1. 粘贴文本 → 引用:`@显示名` 与 `$名称` 逐字命中当前宿主注入的 provider 候选时原位换成芯片,一次 Ctrl+Z 整体回退,token 之外的每个字符(含换行与空白)原样保留。
2. 逐字一致、不做兼容别名:不认 `@hero.png`、`resourceId`、大小写变体、全角 `@`;token 前后必须是行首 / 行尾或空白。
3. 宁可不成芯片也不能认错:同名多候选(同一个 token 对应多条引用身份)一律按文本保留;未命中的 token 静默保留,不提示、不猜路径或文件名。
4. 只有真的解析出引用时才接管:同 namespace 的 `application/x-lexical-editor` 负载、不含 token 的纯文本、图片文件粘贴一律放行编辑器默认导入,现有粘贴行为逐字不变。
5. 附件与运行画面区域不参与粘贴解析(静默 provider 没有候选),它们的 token 粘贴时按文本保留。
6. Skill 目录冷启动不阻塞粘贴:`lookup()` 是纯函数,目录没到就是空数组——冷启动时粘贴 `$名称` 保留为文本(不等待、不补读),用户敲过一次 `$` 后即可解析。
## 流程判定
本次是共享组件的一处行为增量 + provider 契约加两个可选能力,不动 schema、不动公开 API/DTO、不动后端;按轻量流程只建本实施计划,不新建主规范与里程碑规范(与上一步重构同一判定)。
## 提交切分
1. **反解析口径**:`resourceReferences.ts` 新增 `buildContentFromPastedText(text, references)`(粘贴侧唯一反解析;与 `buildContentFromTextTokens` 同一套边界规则),配规则矩阵单测。
2. **provider 契约**:`reference-source/types.ts` 把菜单查询改名为 `fuzzyLookup(query)`、新增精确查找 `lookup()`(两者共用同一份候选来源);`resourceReferenceProvider` 给全量可提及候选,`skillReferenceProvider` 给去重后的 Skill 候选。
3. **输入区接管**:`ResourceReferenceInput` 注册 `COMMAND_PRIORITY_CRITICAL` 的 `PASTE_COMMAND`,只在解析出引用时 `preventDefault` 并在一次 `editor.update`(`PASTE_TAG`)内按选区插入;配集成用例。
4. **文档**:`CONTEXT.md` 术语、本计划、ADR 修订节、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md` 与决策记录。
## 验证
定向:`npx vitest run apps/ai-game-creator-shell/tests/resourceReferences.test.ts apps/ai-game-creator-shell/tests/referenceSourceProviders.test.ts apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`;全量:`npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。真实客户端手感(粘贴时的候选菜单位置、大段文本粘贴观感、Tauri 剪贴板只带图片时的行为)留待真机验收。
## 风险与回滚
- 解析接管会绕过 Lexical 的 `text/html` 富文本导入:只在文本里真的解析出引用时接管,取舍已在上一条判据里限定;要完全避开富文本场景可以后续按 `clipboardData.types` 再收窄。
- 气泡里若出现与素材同名的 `@区域标签` / `@附件名`,粘贴会被认成素材引用——纯文本无法区分,属已知取舍。
- 回滚按提交粒度 revert;canonical content 形状、provider 的既有能力与出站文本口径都不变。
## 执行状态(2026-09-22)
已完成:`buildContentFromPastedText` 与规则矩阵单测(命中 / 未命中 / 相邻中文 / 扩展名 / 大小写 / 全角 / 同名歧义 / 重复出现 / 换行 / 与显示口径互为逆运算);provider 的 `fuzzyLookup` / `lookup` 及用例;输入区 `PASTE_COMMAND` 接管与 5 条集成用例(粘贴重建芯片、未命中保持字面、纯文本走默认导入、Lexical 负载让位、Skill 冷启动保持字面且敲过 `$` 后可解析);文档同步。
未做(本次范围外):斜杠命令 `/` 解析、拖拽文本(drop)、附件 / 运行画面区域 / 文件路径 / URL / 剪贴板图片的解析、复制侧 `text/plain` 形态调整、扩展安装卸载后的目录即时失效。
## 追加执行状态(2026-09-23):前缀重叠按「引用名无空白」收口
自动评审留下的唯一破坏性项(`@hero` 与 `@hero v2` 互为前缀时粘贴会多插一枚短名芯片)不改反解析,改为把不变量前移到引用名:
- `resourceReferences.ts` 新增共享 `normalizeMentionName(value)`(内部空白折 `-`、裁首尾),素材显示名(`resourceDisplayName`)、Skill 名(目录读入与 `toReference` / `mentionToken`)、附件名(导入映射与 `toReference` / `mentionToken`)与两个 token 投影(`chatReferenceMentionToken`、`directCodexContentToPromptText`)统一过这一份;token 层重复归一化是幂等的。
- 名称里不做 `resourceId` 兜底这一条按评审校正:词干为空(`.env` / `.gitignore` 这类整名就是扩展名的文件)时 `resourceDisplayName` 回退 `asset.id`,回退值同样过 `normalizeMentionName`;`normalizeMentionName` 自己仍只做归一化。
- 代价与取舍:`hero v2` 与 `hero-v2` 归一化后同名时走既有的「同名多候选按文本保留」;改动前生成的旧文本里的 `@hero v2` 不再解析。
- 验证:`normalizeMentionName` / 显示名 / token 口径单测 + 前缀重叠回归用例(归一化后粘贴只剩正确的那一枚芯片)+ provider 两处用例;把 `normalizeMentionName` 变异成恒等后新增用例全红。
## 追加执行状态(2026-09-23 第二轮自动评审):空名字与插入兜底收口
第二轮自动评审的四个问题逐个提交处理,落点都在 `apps/ai-game-creator-shell`:
- 显示名不再可能为空:`resourceDisplayName` 在文件名词干为空(`.env` / `.gitignore`)时回退 `asset.id`,回退值同样过 `normalizeMentionName`(提交 `4e90465c4`)。这条校正了上一节「不做兜底」的写法,决策记录与功能说明同步。
- 粘贴解析跳过退化 token:`buildContentFromPastedText` 组装候选时跳过 `token.length <= 1` 的条目,裸 `@` / `$` 不再认领正文里的触发符(提交 `3f9c698a6`)。
- 粘贴接管改成「插入真的发生之后」才 `preventDefault`:`$insertContentAtSelection` 返回「插进去没有」,插入为空时放行默认粘贴;编辑器已在更新中(回调被排队)时按原口径先接管(提交 `3391ecf7d`)。
- 插入兜底不再留空:provider 的 `toReference` 与 `mentionToken` 都答不出来的 part 退到新增的 `contentPartText`(通用文本形态,资源落 `@resourceId`)。此前这一支会什么都不插,是「粘贴内容逐字保留」唯一的例外分支;现在没有例外。新增单测覆盖四类 part 的文本形态,新增输入区集成用例证明被破坏的 provider 契约下这段粘贴仍按文本落下,去掉兜底即变红。
- 归一化撞名(`hero v2` 与 `hero-v2` 折成同一个 token)只在解析侧兜住、候选菜单不提示冲突,这一条涉及「哪些素材能被 @ 到」的产品取舍,未改代码,留给下一轮决定。
- 润色回写与粘贴共用同一条落点兜底:新增 `mentionTokenOrText`(provider 的 `mentionToken`,拿不到就退 `contentPartText` 的通用文本形态),润色回写的候选扫描、整根替换的落点与粘贴插入的兜底都走它。由此润色回写不再有「provider 答不出 token 的 part 直接消失」和「整根替换时 part 解析不出引用就整条吃掉」两条静默丢弃路径;初始草稿(`applyContent`)与润色回写(`applyPolishedTextToRoot`)共用 `applyContentToRoot`,所以恢复出来的草稿里已解析不出的引用现在落成 `@resourceId` 文本而不是被吃掉。
@@ -0,0 +1,63 @@
# 引用输入区重构与宿主注入实施计划
对应:[引用候选由宿主注入](../../adr/【ADR】引用候选由宿主注入-2026-09-22.md);决策记录见 `docs/project-memory/shared-memory/decision-log.md` 的 2026-09-22 条目。本计划不含粘贴解析。
## 一句话交付与验收判据
把 `ResourceReferenceInput` 拆成「纯输入区 + 宿主注入的引用 provider + 独立的选择器面板」,并让附件芯片成为本轮附件的唯一事实源。
验收判据:
1. 输入区不 import 任何具体引用种类,也不判断 `part.type` / `reference.type`:候选、身份解析、正文文本形态都经注入的 provider。
2. 宿主按需选择性注入:只注入资源 → 只有 `@`;资源 + Skill → `@` 与 `$`;未注入的宿主连触发符都不存在,不需要额外的可见性开关。
3. 素材选择器面板独立成组件、自己拿数据、由宿主渲染;输入区不再有 `versions` / `activeVersionId` / `showTriggerButton` / `onReferencePickerOpen` / `skills`。
4. 附件导入成功后以芯片进入正文,失败或异常不插入;控制器不再持有附件数组,「待发送附件列表」删除;上限 8 按草稿中的附件芯片数计算;导入进行中禁止发送。
5. 行为零变化:改名后芯片显示名自动刷新、打开面板前重读清单、`@`/`$` 候选与键盘交互、上限与失败提示文案逐条一致;唯一例外是已裁决的 Skill 可见性缺陷修复(非 DirectProject 宿主不再出现 `$`)。
## 流程判定
本次是无行为变化重构加两条已裁决缺陷修复(Skill 可见性、附件单一事实源),按轻量流程只建本实施计划,不新建主规范与里程碑规范;若评审要求补 SDD 里程碑,再单独补。
## 提交切分
1. **接口与 provider 工厂**:新增 `features/project-workspace/reference-source/types.ts`(`ReferenceProvider`)与四个工厂(资源 / Skill / 附件 / 运行画面区域),配规则矩阵单测:触发符有无、`match` 过滤、`toReference`、`refresh`(改名 / 删除 / 恒等)、`mentionToken`。
2. **输入区接入注入**:输入区改为按 `providers` 派生候选菜单(不再写死两个触发符)、按数组顺序取第一个非空回答做草稿回填与改名刷新,删除 `skills` prop;测试夹具(22 处 `assets={assets}`)同批迁移为注入 provider。
3. **面板外移**:素材选择器面板与缩略图预览搬进独立组件,宿主渲染并接线(`onConfirm → insertReferences`),输入区删除相关 props 与其死 CSS 规则(`styles.css` 里 `resource-reference-*` 共 78 条,逐条核对哪些随面板离开而失效)。
4. **附件并入联合**:附件成为 `ChatReference` 成员,节点层收敛为单一节点类型,附件 chip 的 DOM 契约逐字保留(`data-attachment-reference`、`data-attachment-status`、`title`、移除按钮 `aria-label`)。
5. **附件单一事实源**:`uploadFiles` 改为返回导入结果,composer 在导入成功后插入芯片、失败不插入;删除控制器附件状态、`ComposerPendingAttachments` 与提交时的附件 parts 拼接;上限改为按草稿芯片数计算,导入进行中禁止发送。
6. **显示口径与文档**:`directCodexContentToPromptText` 的 token 前后补空白(不重复)并加 `// TODO we will rewrite this with ref as component later.`,显示名反查改由调用方注入;同步更新 `docs/【功能说明】AGC聊天素材引用-2026-09-08.md`(`ResourceReferenceInput` 共用口径、`@`/`$` 可见性、附件入口与上限口径都会变)。
`resource-reference-*` 的 CSS 类名重命名(78 条规则 / 15 个文件 / 22 处测试断言)留作最后一个独立机械提交,必须与 `RESOURCE_REFERENCE_OVERLAY_SELECTOR` 常量同批修改。
## 验证
每个提交后跑定向用例:`npx vitest run apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`,加上该提交触达的宿主用例(聊天 composer / 策划输入盒 / 画布生成面板 / 资源卡快速编辑)。全部完成后跑 `npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`,再按验收判据第 5 条逐条对照行为。真实客户端手感(候选菜单位置、面板内滚轮、附件插入位置)留待真机验收,未执行前按未验证项记录。
## 风险与回滚
- 漏注入某个宿主会让该宿主静默失去候选;fail-closed 语义正确,但必须在用例里逐宿主覆盖。
- `LexicalTypeaheadMenuPlugin` 由两个写死实例改为按 provider 派生后,Enter 让位与 Escape 关闭口径要重新覆盖(`mentionMenuOpenRef` 需聚合成「任一菜单开着」)。
- 附件插入位置在异步导入期间可能漂移:导入失败一律不插入;文档或选区已改动时退到草稿末尾。
- 删除 `ComposerPendingAttachments` 会连带影响样式与既有断言,属于预期内的行为收敛,需在提交说明里写清。
- 回滚按提交粒度 revert;无数据迁移,canonical content 形状不变。
## 执行状态(2026-09-22)
已落地的提交内容:
1. 接口与 provider 工厂:`features/project-workspace/reference-source/types.ts` + 四个工厂(资源 / Skill / 附件 / 运行画面区域),规则矩阵单测落在 `apps/ai-game-creator-shell/tests/referenceSourceProviders.test.ts`(触发符有无、`match` 过滤 / 去重 / 截断、`toReference`、`refresh`(改名 / 删除 / 恒等)、`mentionToken`、`isReady`、Skill 目录「首次 `match` 才读、失败可重试」)。
2. 输入区接入注入:`providers` / `inputActions` / `submitSuppressed` 三个通用接缝;`skills`、`assets`、`versions`、`activeVersionId`、`showTriggerButton`、`openPicker` 全部删除;`ProviderMentionMenu` 按带触发符的 provider 派生(Enter 让位按 `mentionMenuOpenRef` 聚合)。
3. 面板外移:`ResourceReferencePicker` + `ResourceReferencePickerAction` 由宿主渲染并接线(`onInsert → insertReferences`、`onOpenChange → submitSuppressed`),三个宿主的 `@` 触发钮位置逐处保持原样。
4. 附件并入联合:附件成为 `ChatReference` 成员,编辑器只剩 `ResourceReferenceNode` 一种节点,附件 chip 的 DOM 契约逐字保留。
5. 附件单一事实源:`uploadFiles(files, draftAttachmentCount)` 只返回导入成功的附件、失败不插入;控制器附件状态、`ComposerPendingAttachments`、提交时的附件 parts 拼接都删除;上限按草稿芯片数计算;导入进行中禁止发送。
6. 显示口径与文档:`directCodexContentToPromptText(content, resolver)` 的 token 前后各补一个空白(不重复)并加 `// TODO we will rewrite this with ref as component later.`;显示名反查改由 `resourceLabelResolver(assets)` 注入;同步更新功能说明、ADR 与决策记录。
顺带清理:`ProjectChatComponentProps.activeVersionId` 在策划输入盒不再有 `@` 触发钮之后已无消费方,随本次删除(工作台壳自己的那份仍归 `ProjectDevelopmentView`)。
验证结果:`npx tsc -p tsconfig.json --noEmit`、`npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 全通过;定向用例(`referenceSourceProviders`、`resourceReferenceInput`、`chatPromptPolish`、`resourceReferences`、画布生成面板、快速编辑、资源卡实时集成、`appSurface` 215 项、工作台壳清单合并 / 窗口同步、资源标签统计刷新、版本切换 / 替换)全绿。整仓 `npx vitest run` 仍有与本改动无关的既有环境失败(jsdom 缺 `localStorage`:`clientApi` / `clientAuthStorage` / `clientHttp` / `projectCreationDirectory` / `recentProjectsHook` 与 `src/components/image-editor/*` 若干),失败集合与本改动触及的模块不相交。
未完成项:
- `resource-reference-*` CSS 类名重命名(78 条规则 / 15 个文件 / 22 处断言)仍是独立机械提交,须与 `RESOURCE_REFERENCE_OVERLAY_SELECTOR` 同批修改;本次未动,输入区与面板的既有类名逐字保留。
- 真实客户端手感(候选菜单位置、面板内滚轮、附件插入位置、`@` 触发钮落点)未做真机验收,按未验证项记录。
- 粘贴解析仍不在本次范围(未来接入点是 provider 的 `mentionToken` 与既有 `buildContentFromTextTokens`)。
@@ -0,0 +1,128 @@
# 【实施计划】游戏分发阶段A领域合同
| 字段 | 值 |
| --- | --- |
| Milestone | `docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md` 阶段 A |
| Status | in_progress |
| Owner | Codex |
## 修改边界
- 允许修改:`module-game-distribution` 新领域 crate、workspace 声明、Rust shared-contracts DTO、领域单测及文档证据。
- 明确不修改:发行域名与 CDN、旧 public work/runtime、AGC native 上传命令、网页发布表单。
## 行为范围
- 定义游戏身份、版本状态、包摘要、owner 权限、幂等 key 冲突和 publication revision CAS。
- 领域服务先以纯内存仓储验证状态合同,再接入已冻结的 SpacetimeDB 游戏/版本/幂等收据表;不把内存仓储作为生产事实源。
- 阶段 A 只提供身份、版本、真实包确认和状态回读基础,不提供“发布成功”或公开可玩状态,不绕过人工审核。
## 验证命令
1. `cargo test -p module-game-distribution --manifest-path server-rs/Cargo.toml`
2. `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check`
3. `npm run check:encoding`
4. `git diff --check`
## 风险与回滚点
- API 接入前端前必须完成 SpacetimeDB facade 和 schema 门禁;任何失败都保留旧版本数据,不覆盖已有业务表。
- 内存仓储只用于行为合同测试,不得被前端当作正式数据源。
## 已完成证据(当前切片)
- `module-game-distribution` 已落地游戏/版本状态机、owner 校验、幂等摘要冲突、上传确认、验证失败重试、审核 CAS、撤回和管理员暂停的纯领域服务与内存测试仓储。
- 同一 crate 已加入不执行上传代码的 ZIP 包校验:根 `index.html`、路径穿越/大小写冲突、符号链接、加密文件、敏感文件、嵌套压缩包、单文件/总展开量/压缩比上限和逐文件 SHA-256 清单;并新增按白名单内容类型取单个发行资源的读取层。
- Rust/TypeScript 跨端 DTO、SpacetimeDB 三张持久表、migration 白名单、生成 bindings、typed facade 和游戏分发 HTTP 路由已接入,覆盖创建、上传、送审、审核、下架、公开目录与详情。
- `api-server` 新增发行网关 `GET /api/game-distribution/releases/{gameId}/{assetPath}`:只服务当前已公开版本,未知扩展名 404,附带 nosniff / CORP / HTML CSP,拒绝带 Cookie 请求,并按对象键做有界包缓存。
- 重复发布复用游戏身份:游戏表末尾新增可空 `local_project_id`,`create_game_distribution_game` 在 owner + local_project_id 命中时复用既有 `gameId` 并只新增版本;`localProjectId` 经 shared-contracts(Rust/TS)透传,AGC 发布链路写入并在缺失时于发请求前失败关闭,api-server 对短标识做路径分隔符与控制字符校验。
- 后台新增 `#game-distribution` 游戏审核页:待审列表、通过(要求 HTTPS 发行入口)、拒绝(要求理由)、幂等键与列表刷新;对应 `editor-showcase` Tab 权限映射、API client 与页面测试已加入。
- 路由级测试证明发行网关、公开目录与登录发布路由确实挂载:带 Cookie 的发行请求 403、未知扩展名 404 且不触达对象存储、无 Bearer 的发布请求 401、目录在无数据库时返回 502 而不是 404。
- 自动化证据:`cargo test -p module-game-distribution`(11 passed)、`cargo test -p api-server` 游戏分发模块(9 passed)与全量 `cargo test -p api-server`(1069 passed / 6 ignored)、`cargo test -p shared-contracts`、admin-web Vitest(149 passed,含新增游戏审核页与 client 用例)、前端游戏定向 Vitest(15 passed)、AGC `typecheck` 与 `local_project_export` Rust 测试(7 passed)、Vite build。
- 版本回读、撤回与管理员安全下架闭环:新增 SpacetimeDB `get_game_distribution_version_and_return` / `cancel_game_distribution_version_and_return`(幂等收据 + 公开修订号 CAS)、`spacetime-client` façade、`GET /api/game-distribution/versions/{versionId}`(作者,未知与非 owner 一律 404)、`GET /admin/api/game-distribution/versions/{versionId}`(管理员)与 `POST /api/game-distribution/versions/{versionId}/cancel`(作者,`Idempotency-Key` + `expectedPublicationRevision`);版本私有投影新增服务端派生的 `recoveryAction`(upload/submit/wait/none/reupload/fix_package/fix_metadata)。后台游戏审核页补齐「安全下架」入口(原因输入 + 二次确认弹窗 + 幂等键)。
- 幂等响应语义修正:写操作 procedure 现在把「命中既有幂等收据」如实回传为 `replayed: true`(此前所有写操作都固定返回 false),覆盖创建游戏/版本、确认包、上传失败、送审、审核、撤回、下架与暂停。
- 网页端恢复与撤回:`/games/publish` 在创建版本后写入带 owner 的发布草稿,窗口关闭或上传中断后同一账号可见「继续上传/继续送审」并复用原 `versionId`,换账号只忽略不读取;`/games/mine` 对可撤回状态提供二次确认的「撤回审核」,成功后刷新状态。
- AGC 发布真实链路回归测试:新增 `apps/ai-game-creator-shell/tests/gameDistributionPublishLive.test.ts`(默认跳过,设置 `GENARRATIVE_AGC_PUBLISH_E2E_BASE_URL` 指向运行中的 api-server 才执行)。它不 mock 请求层,而是走真实的 `clientApi`/`clientHttp` 调用 `publishLocalProjectGame`,覆盖本地导出包读取、创建游戏、同 `localProjectId` 复用游戏身份、真实 ZIP 上传、送审、版本回读与 `my-games` 聚合,把「AGC 一键发布」从请求组装单测提升到真实后端链路证据。
- AGC 客户端回归修复:导出试玩包后自动打开的发布面板是焦点陷阱模态,会让既有 `appSurface` 导出快捷操作用例静默失效;用例改为先断言面板出现、关闭后再继续,`appSurface.test.ts` 552 项(535 passed / 17 skipped)恢复全绿,并顺手补上该面板此前缺失的行为断言。同一轮修掉 `gameDistributionPublish.test.ts` 的模块 mock 缺少 `getClientServerBaseUrl` 导出的既有失败。
- 发行来源(每游戏独立 origin)部署工件:新增 `deploy/nginx/genarrative-release-origin.conf` 与 `scripts/check-release-origin-config.mjs`(`npm run check:release-origin-config`)。模板按命名捕获 `game_id` 把 `https://<gameId>.games.<域>/` 映射到该游戏的 `index.html`、其余路径映射到发行网关前缀,边缘拒绝并清空 Cookie,不代理平台 API/后台/SPA;门禁逐条校验模板约束、交叉检查发行网关仍在设置 `nosniff`/CORP/CORS/CSP 与 Cookie 403,并在本机渲染临时配置跑 `nginx -t`。
- AGC 发布面板组件测试:新增 `apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx`,覆盖打开时预填资料、发行包摘要、提交入参(项目路径/发行包/资料/幂等键)、连续点击只提交一次、服务端失败保留面板与无 Tauri 宿主/缺包时失败关闭。
- 视觉与交互巡检修复:真实栈 + headless Chromium 逐页巡检 `/games`、`/games/detail`、`/games/play`、`/games/mine`、`/games/publish`(桌面 1440×900 与移动 390×844)。修掉两处:① 详情页「游玩方式」只读公开投影里恒为空的 `version.controls`,作者声明的 `inputModes` 被忽略,现在优先展示「键盘 · 鼠标 · 触屏」再退回自由文本;② 桌面端 hero 过高导致首屏看不到任何游戏卡片,改为仅桌面压缩 hero 高度,首排卡片进入 900px 视口。
- 容量与限额边界(阶段 D 证据):新增真实栈脚本覆盖 5 类包——① 99.0 MiB(两个 50/49 MiB 文件 + `index.html`,均在单文件/展开量/压缩比限制内)上传 200、耗时 11.8s、api-server RSS 81.6→378.0 MB(峰值 +296 MB)后回落 83.2 MB,随后送审 202 进入 `pending_review`;② 声明的 `packageBytes` 为 101 MiB 时在创建版本即被拒 413 `PAYLOAD_TOO_LARGE`("发行包大小超出限制",不落版本);③ 声明合法但请求体 101 MiB 时被请求体限制拒绝 413(0.03s、内存零增长、版本保持 `awaiting_upload`/`upload`,可原版本重传);④ 60 MiB 全零文件压到 61 KB(压缩比约 1000)拒绝 422 `PACKAGE_VALIDATION_FAILED`(0.01s,`upload_failed`/`reupload`);⑤ 单文件 65 MiB 与 10,001 个文件同样 422(0.05s / 0.02s)。失败包都不出现在公开目录。
- 发行包 PUT 受控重试:`platform-oss` 新增 `put_internal_object_with_retry`(复用既有 `oss_error_is_retryable` 分类:传输/超时/connect、408、429、5xx 与 400+RequestTimeout 可重试,确定性 4xx 不重试;body 只转一次 `Bytes` 供各 attempt 复用),发行包上传接入 3 次尝试 + 250/500ms 退避。触发原因:本机实测 99 MiB 单次 PUT 三次里出现过一次 `UPSTREAM_ERROR`(`请求 OSS 失败:error sending request`),客户端需要白传整包。
- 网页端「为既有游戏发布新版本」:发布页新增 `updateGameId` 更新模式(按作者中心的最近版本回读冻结资料与公开修订号预填,提交时跳过创建游戏、直接在既有 `gameId` 下创建不可变新版本),作者中心新增「发布新版本」入口,壳层支持 `/games/publish?game=<gameId>`。浏览器真实链路:从 `/games/mine` 点「发布新版本」→ `/games/publish?game=…` 预填并显示「为《…》发布新版本 v2」→ 选包提交 → 该 `gameId` 下出现 `versions [(2, pending_review), (1, published)]`,公开入口仍指向并返回 v1 内容。此前网页端只能新建游戏,更新无法沿用 `gameId`,与里程碑「更新沿用相同 gameId」不符。
- 全生命周期回归(真实栈,11 步):待审公开不可见(目录不返回 + 详情 404)→ 管理员通过 v1 后目录/详情/网关可玩 V1 → v2 待审期间网关仍是 V1 → v2 被拒后仍是 V1 → 重复批准 `rejected` 版本返回 409(`rejected` 是终态,必须新建版本,与主规范一致)→ 新建 v3 通过后公开入口切换为 V3 → 作者回读被拒版本为 `rejected`/`fix_metadata` → 作者下架后目录/详情/网关全部关闭。同一轮修掉 5 条既有的壳层导航用例失败(游戏分发新增「游戏」页签与移动底栏后,测试仍断言旧导航集合)。
- 边界、越权与隔离运行时证据(真实栈):① 跨作者越权——作者 B 对作者 A 的版本上传 403、送审 403、撤回 404、回读 404,且 A 的版本状态未被改变;② 过期 CAS——管理员用错误 `publicationRevision` 批准返回 409,正确修订号才通过;③ 包边界——符号链接条目、`.env` 凭据文件、嵌套 ZIP 分别返回 422 `PACKAGE_VALIDATION_FAILED`,版本落到 `upload_failed`/`reupload`;④ 私有对象直取——按对象键直接 GET 私有 OSS 地址返回 **403**,发行文件只能经网关读取。
- 浏览器沙箱隔离实测:发布一个自检探测包(内联脚本主动探测并 `postMessage` 回传结果),在 `/games/play` 里点「开始游戏」后收到 `{ origin: "http://127.0.0.1:10001", cookie: "throw:SecurityError", storage: "throw:SecurityError", parentDom: "throw:SecurityError", externalFetch: "throw:TypeError" }`——主站 Cookie、localStorage、父页面 DOM 全部不可访问,外站 fetch 被 CSP `connect-src 'self'` 阻断。键盘可达抽查:桌面端 Tab 顺序覆盖导航 → 搜索 → 下载客户端 → 账户,游戏分发自己的控件(发布游戏/我的游戏/分类页签/游戏卡片)聚焦时都有可见的浏览器默认焦点环。同一探测包还验证了音频链路:沙箱内 `fetch('assets/beep.wav')` 返回 `status:200 type:audio/wav`,WebAudio `decodeAudioData` 成功解出 `0.40s / 48000Hz`(`AudioContext.state = suspended`,与浏览器“首次播放需帧内用户手势”的策略一致,玩家进入游戏后的第一次交互即提供该手势)。游戏内真实触屏输入也已验证(见下一条)。
- 上传中断与进程重启恢复(真实栈,两次独立实验):60 MiB 包 PUT 进行到 ~2.5s 时 `SIGKILL` api-server,客户端拿到 `RemoteDisconnected`(等价于响应丢失);重启后该版本仍是 `awaiting_upload`/`upload`,没有半确认状态。① 用不同字节重传返回 422 `PACKAGE_VALIDATION_FAILED`(`InvalidArchive`)并转 `upload_failed`/`reupload`;② 用固定时间戳构造的确定性 60 MiB 包重传**相同字节**返回 200(7.64s),版本转 `uploaded`/`submit`,随后送审 202 进入 `pending_review`。两次实验中游戏都未出现在公开目录。
- 发布开关(回滚/事故能力):新增 `game-distribution:publish` 灰度 gate(复用现役灰度配置机制与后台入口)。默认开放;`enabled=true` 时按白名单/用户标签/灰度百分比放行,`rolloutPercent=0` 且无白名单即全部关闭。关闭时作者写入与管理员批准返回 `503 GAME_DISTRIBUTION_PUBLISH_DISABLED`,而目录、详情、版本回读、发行网关、`/my-games`、审核队列读取、拒绝审核与安全下架继续可用;开关读取失败按关闭处理。路由级测试覆盖「默认放行 / 全关拦截 / 读取不受影响 / 白名单放行」,真实栈验证同口径:关闭后作者写入 503、目录/详情/网关/my-games 全 200、管理员批准 503 而拒绝 200、白名单用户可发布、重新开放后恢复。
- 游戏内真实触屏输入(本机 Chromium + CDP `Input.dispatchTouchEvent`,390×844 视口):在 `/games/play` 点「开始游戏」后,向 iframe 可视区域坐标派发 `touchStart`/`touchEnd`;`sandbox="allow-scripts"` 且 `ready` 的沙箱内探测包回传 `{touchstart: 1, touchend: 1, pointerdown: 1, pointerTypes: ["touch"], click: 1, target: "pad", touches: 1}`,证明真实触摸事件经平台进入游戏容器并命中目标元素。注意:触摸模拟必须在 iframe 文档创建之前启用,否则文档不会注册 touch 事件支持(首次实测只收到 pointerdown,重载后 touchstart/touchend 才出现)。
- 可观测性(阶段 D「上传失败、校验耗时、审核积压、撤销传播」):`api-server` 的游戏分发模块补齐结构化事件,均带 `request_id`(可 join 访问日志)与 `operation`,不记录 Token、signed URL、完整文件内容或本地路径。事件与字段:`package_confirmed`(game_id/version_id/package_bytes/file_count/sha256_prefix/oss_put_skipped/elapsed_ms)、`package_rejected`(code/reason/uploaded_bytes/elapsed_ms)、`version_submitted`、`version_cancelled`、`review_backlog_listed`(pending_versions/limit)、`review_decided`(decision/admin_user_id/publication_revision)、`game_unpublished`(visibility/publication_revision/active_version_id)、`game_suspended`(admin/reason/publication_revision)、`publish_switch_blocked`/`publish_switch_unavailable`、发行网关 `release_rejected`(debug 级:reason=cookie_present/unsupported_extension/not_public/no_active_version/version_not_published)。真实栈复跑一次完整链路后,8 类事件各出现 1-2 次,字段与耗时均可用(样例:`package_rejected … reason=InvalidArchive uploaded_bytes=9 elapsed_ms=6`、`game_unpublished … visibility=unpublished publication_revision=2 active_version_id=""`)。
- 工程门禁:`npm run check:spacetime-schema`(147 tables)、`npm run check:server-rs-ddd`、`cargo fmt --all -- --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 运行时证据(2026-09-20,真实本地栈)
`npm run dev` 在无冲突端口段启动(spacetime `127.0.0.1:10004`、api-server `127.0.0.1:10001`、真实 OSS bucket `xushi-dev`),用 HTTP 全链路脚本验证:
- 管理员登录 + 用户口令注册/登录均走真实路由。
- 创建游戏后,用相同 `localProjectId` 再次创建返回**同一个 `gameId`**(去重在真实数据库上生效)。
- 真实 ZIP(根 `index.html` + `assets/app.js`)上传私有 OSS,服务端重算 SHA-256 / 字节数 / 文件数并确认 `uploaded`。
- 送审返回 `202` 与 `pending_review`;此时公开目录、公开详情都不返回该游戏。
- 管理员用 HTTPS `entryUrl` 审核通过后,目录与详情出现该游戏并带当前版本 `entryUrl`。
- 发行网关返回 `text/html` 与 `text/javascript`;带 `Cookie` 的发行请求 403,未知扩展名 404。
- 作者下架后公开详情回到 404。
- 更新链路:v2 送审期间与 v2 被拒后,公开入口始终停留在 v1 且 v1 内容仍可读取;v3 通过审核后才替换公开版本。
- 真实浏览器(headless Chromium)加载发行网关的 `index.html`:文档渲染、同源 `assets/app.js` 在现役 CSP 下执行成功(`booted=1`、`window.__gd=1`),控制台只剩 favicon 404。
- 浏览器打开 `http://127.0.0.1:10000/games` 能看到已发布游戏卡片,`/games/detail?id=<id>` 渲染标题与「立即玩」。
- 平台页面内嵌游玩:浏览器打开 `/games/play?id=<id>`,点「开始游戏」后 iframe 以 `sandbox="allow-scripts"` 挂载发行网关地址,`game-player-frame--ready` 置位,控制台无 CSP/CORP 拦截,api-server 访问日志显示该版本 `assets/app.js` 返回 200。
- 非生产环境允许 http 回环 `entryUrl`(与前端 `normalizeGameEntryUrl` 同口径),生产仍只接受 HTTPS;这样本地无需 TLS 即可验证内嵌游玩。
- 作者中心 `/games/mine`:SpacetimeDB 新增 `list_owner_game_distribution_games_and_return`(owner 只从认证主体派生,每游戏最多回 10 个最近版本),`api-server` 暴露 `GET /api/game-distribution/my-games`;浏览器里用真实作者 token 打开该页,能看到「已公开」状态与「下架」按钮,点击后状态变为「未公开」、按钮消失,公开目录同步不再返回该游戏。
- 网页端发布入口 `/games/publish`:浏览器里用真实文件选择框上传 ZIP(根 `index.html` + `assets/app.js`),api-server 访问日志记录 `POST /api/game-distribution/games` 200 → `POST /api/game-distribution/games/{gameId}/versions` 200 → `PUT /api/game-distribution/versions/{versionId}/package` 200 → `POST /api/game-distribution/versions/{versionId}/submit` 202;随后同一账号在 `/games/mine` 看到该游戏为「未公开 / 版本 v1 · 审核中」,公开目录不返回它。
- 版本回读/撤回真实链路(`npm run dev`,api-server `127.0.0.1:10001`、SpacetimeDB `127.0.0.1:10004`):作者送审后 `GET /versions/{id}` 返回 `pending_review` + `recoveryAction=wait`;其他账号与未知版本都是 404;过期修订号撤回 409;正常撤回 200 且状态变 `cancelled`、`recoveryAction=none`;同 key 同请求重放返回 `replayed=true`,同 key 不同摘要 409;撤回后公开目录不返回该游戏。
- 主链路回归(同一真实栈):正式发布 `replayed=false`、重复送审 `replayed=true`;待审期间目录不可见;管理员读版本 200、审核通过后状态 `published` 且目录与详情返回 `currentVersion.entryUrl`;发行网关 `index.html` 200、带 Cookie 403、未知扩展名 404。
- AGC 发布真实链路(同一真实栈):`GENARRATIVE_AGC_PUBLISH_E2E_BASE_URL=http://127.0.0.1:10001 npx vitest run apps/ai-game-creator-shell/tests/gameDistributionPublishLive.test.ts` 通过——AGC 发布函数依次完成创建游戏、创建版本、上传 ZIP、送审,返回 `pending_review`;版本回读为 `pending_review`/`wait`;不带旧幂等键的第二次发布复用同一 `gameId` 并生成 v2;`/my/games` 的 `latestVersion` 指向 v2。
- 非法包真实链路(同一真实栈):缺根 `index.html` 且含 `../` 条目的 ZIP 上传返回 422(包校验错误),版本进入 `upload_failed` 且 `recoveryAction=reupload`,此时送审被拒(409)、公开目录不返回该游戏,作者仍可撤回该版本重新出包。
- 发行来源真实边缘验证(同一真实栈 + 本机 nginx 1.28.3,模板渲染到 `~/data/tmp` 后监听高位端口):`Host: <gameId>.games.example.com` 时根路径 200 `text/html`(游戏 `index.html`)、`/assets/app.js` 200 `text/javascript`;带 `Cookie` 403;`/api/auth/me` 404(平台命名空间未暴露);未知 gameId 404;http 301 到 https;响应头经边缘透传后仍是 `nosniff` + CORP `cross-origin` + 无凭据 CORS + HTML CSP + `Cache-Control: public, max-age=60, must-revalidate`。
- 移动视口真实游玩:headless Chromium 以 `390x844` 打开已发布游戏 `/games/play?id=<id>`,页面显示「横屏设计,旋转设备」提示与移动端底部导航,点击「开始游戏」后 iframe 以 `sandbox="allow-scripts"` + `allow="fullscreen"` 挂载发行网关地址并 `ready`,`index.html` 与 `assets/app.js` 均返回 200,控制台无 CSP/CORP 报错;截图存于本轮验证记录(不入库)。
本轮同时修掉三个真实缺陷:Vite dev 代理缺少 `/api/game-distribution` 前缀(本地全部 404);详情页在「已发布但没有 controls」时误显示「暂未发布可玩版本」;发行网关的 `Cross-Origin-Resource-Policy: same-origin` 会让 opaque origin 沙箱内的游戏加载不了自己的脚本(改为 `cross-origin` + 无凭据 CORS,详见 `pitfalls.md`)。
- 资料冻结与展示闭环(封面 + 截图):游戏表末尾新增可空 `cover_object_key` / `screenshots_json`,版本表末尾新增可空 `metadata_json`;创建版本时 api-server 校验「必需封面、≤6 张截图、素材属于当前作者且 `content_type` 为 `image/`」,并从素材记录派生对象键生成冻结快照,审核通过时整体生效到游戏行。
- 公开素材读授权:只有 `published` 且存在有效 `active_version_id` 的游戏,其封面/截图素材才在 `/api/assets/read-url` 获得匿名读授权;其余素材仍按 owner 校验。
- 作者续发复用:版本回读(作者本人)与审核回读(管理员)的版本投影新增 `frozenMetadata`,带回 `coverAssetId` / `screenshots[].assetId`;`/games/publish` 更新模式据此预填封面与截图,不要求作者为沿用封面重新上传;快照缺素材 ID 的旧版本明确要求重新选择封面。公开投影仍只暴露对象键。
- 网页发布资料入口:`/games/publish` 新增「封面与截图」区(封面必需、截图 ≤6、可逐张移除、上传中禁用提交),复用平台图片直传 + confirm 通道;本地校验与服务端口径对齐(缺封面/超 6 张在发请求前拦截)。
- 网页展示:游戏广场卡片与详情页 hero 用 `useResolvedAssetReadUrl` 换签展示真实封面,详情页在存在截图时给出可点击缩略图条(封面 + 截图,选中态 + 键盘可达),换签失败或无素材时静默回退原有渐变占位,不出现空框。
- 本切片验证:`cargo check -p api-server -p spacetime-module -p spacetime-client`;`cargo test -p api-server game_distribution`(17 passed,含新增 `version_detail_payload_exposes_frozen_metadata_to_owner`);`npm run typecheck`;`npx vitest run src/components/game-distribution src/services/gameDistributionClient.test.ts`(46 passed);改动文件 `eslint --max-warnings 0` 与 `npm run check:encoding`。
- AGC 发布面板资料入口:新增 `apps/ai-game-creator-shell/src/services/assetDirectUpload.ts`(凭证 → 直传 → confirm,直传固定走 Tauri HTTP 插件并校验目标主机必须是平台素材存储),面板支持封面必选 + 截图 ≤6、本地预览、逐张移除、上传中禁用发布;缺封面时在创建游戏前失败关闭,服务端「封面」类错误原样展示;同一文件重复提交复用素材 ID 不重复直传。为支持直传,`src-tauri/capabilities/main.json` 的 http 作用域新增 `https://*.aliyuncs.com/*`(配合客户端主机白名单,避免把本地文件发给任意主机)。
- AGC 测试证据:`npx vitest run apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx apps/ai-game-creator-shell/tests/clientApi.test.ts`(27 passed);`npm --prefix apps/ai-game-creator-shell run typecheck`;改动文件 `eslint --max-warnings 0`。
- 真实本地栈媒体验证(2026-09-20,`npm run dev:api-server` + `dev:web`,api-server `127.0.0.1:12401`、SpacetimeDB `127.0.0.1:12402`、真实 dev OSS 桶):`E2E_ADMIN_USER=… E2E_ADMIN_PASSWORD=… npm run check:game-distribution-media-e2e`(`scripts/check-game-distribution-media-e2e.mjs`)27 项全部通过——真实直传封面与 2 张截图素材、缺封面 400、截图 7 张 400、不存在素材 400(创建游戏与创建版本两处)、他人素材 403、创建游戏/版本/上传/送审(202 + `pending_review`)、作者回读拿到 `frozenMetadata` 且素材 ID 与顺序一致、待审期间公开目录不含该游戏且封面匿名读返回 404、管理员审核通过后公开投影带封面与截图对象键且不含素材 ID、匿名 `read-url` 对封面与截图都返回签名地址、发行网关 `index.html` 与 `assets/app.js` 均 200 且带 nosniff。
- 真实浏览器展示验证(同一栈,headless Chromium `390x844` 与 `1280x900`):`/games` 卡片渲染真实封面签名地址(无占位回退),`/games/detail?id=…` hero 显示封面、缩略图条渲染「封面 + 2 张截图」,点击第 3 张后 `aria-pressed` 与 hero 图片同步切换;移动端布局正常。截图存于 `~/data/tmp/gd3/`(不入库)。
- 网页发布页真实上传验证(同一栈 + headless Chromium):在 `/games/publish` 里用作者会话填写资料、选择真实 PNG 封面与 ZIP 并提交,页面返回「已提交审核」;随后 `my-games` 显示该游戏 `pending_review`,版本回读的 `frozenMetadata.coverAssetId` / `coverObjectKey` 正是本次浏览器上传的素材,证明网页端封面直传(凭证 → OSS → confirm)与冻结链路真实可用。
- 发布资料入口交互打磨:原生 file 控件在上传后清空 value 时会显示「未选择任何文件」,与「已选择」提示互相矛盾;网页发布页与 AGC 面板都改成透明 input 覆盖自定义胶囊按钮(点击命中原生控件,文案显示「选择/更换封面图片」「添加截图」),并在验证中用 `elementFromPoint` 确认点击命中的是 `input[type=file]`。
- AGC 直传能力作用域静态验证:用 `tauri-plugin-http` 同一套 `urlpattern` 解析逻辑验证 `capabilities/main.json` 新增的 `https://*.aliyuncs.com/*` 命中 dev 桶(`xushi-dev.oss-cn-beijing.aliyuncs.com/…`)与生产桶(`genarrative-assets.oss-cn-shanghai.aliyuncs.com/…`)、拒绝 `evil.example.com`;直传失败(含作用域拒绝/网络不可达)统一转成中文可操作文案,并有单测覆盖。桌面端真实执行仍需在装有 AGC 的机器上跑一次。
- 创建游戏同步校验素材归属:此前 `POST /api/game-distribution/games` 只校验「封面必填 / 截图 ≤6 / ID 非空」,素材是否存在与是否属于当前作者要等到创建版本才失败,游戏行会先落一个无效素材 ID;现在创建游戏与创建版本都走 `resolve_owned_game_media`,不存在返回 400、他人素材返回 403。
- 合并 master(`81e0c41f1`,含 `429f991bd` 定向回滚、Godot 原生绑定迁移与策划 Agent 模型控件):回滚提交把混入 master 的游戏分发代码整体删掉,其中 25 个文件在本次合并里属于“双方都改动同一区域之外”的自动删除,直接合并会静默丢功能。处理方式是按“分支补回功能、master 保留迁移”逐类归位:模块注册(`spacetime-module/src/active.rs`)、api-server 路由与权限映射、后台审核页、AGC 发布命令与 payload 类型、`SelectionStage` 的 games 阶段、`ProjectSupervisorView` 的 `overlay` 挂载点、主站路由/标题、Cargo 依赖与锁、vite/vitest 代理与 tailwind source 全部取回分支版本;master 的 Godot C++ 迁移(删除旧 vendored GDExtension、新的 composer 控件样式)保留。合并后用“按文件对比 game-distribution 引用数不得下降”的脚本复核 85 个相关文件,无残留丢失。
- 顺带修掉 master 自带的一处红灯:`src/config/viteProxyConfig.test.ts` 断言 `/api/creation-entry` 会被代理,但 master 的 `vite.config.ts` 并无该代理项,且 api-server 已把 `/api/creation-entry/config` 列入 retired 路由测试(dev 中间件按退役路径返回 404)。测试改为断言“退役路径不进入代理”,与 `isRetiredApiPath` 事实一致。
- 第二次合并 master(`81e0c41f1` → `500835407`,304 个提交:DirectProject 聊天容器重构、Project Supervisor 退役、策划附件导入、CI 隔离编译缓存等):冲突 9 个文件,其中真正需要集成决策的是 AGC 侧——master 把 `ProjectSupervisorView` / `SupervisorChatOnlyView` 整体退役(含我们此前挂 overlay 的挂载点),并且前端不再调用 `export_local_project_package`(`check-config.mjs` 把它登记为 native-only)。处理方式:接受 master 的退役与删除;把发布入口重新接到新架构上——`DirectProjectChatHeader` 新增「发布到游戏广场」按钮(没有回调时不渲染、忙态禁用),`DirectProjectChatView` 透传 `onRequestGamePublish`,`App.tsx` 用工作台壳持有试玩包导出与 `GameDistributionPublishPanel`(沿用 `project.export_package` 权限确认队列),并把该命令从 `check-config.mjs` 的 native-only 清单移回 App invoke。
- 同轮修掉 master 自带的红灯断言:`src/config/viteProxyConfig.test.ts` 曾断言 `/api/creation-entry` 会被代理,但配置无该代理项且 api-server 已把 `/api/creation-entry/config` 列为退役路由,测试改为断言退役路径不进入代理。
- 覆盖率与门禁(合并后):全量 `npm test` 392 文件 / 4371 用例通过(appSurface 重构后 215 用例)、两端与 admin-web typecheck、`cargo check`(api-server / spacetime-module / spacetime-client)、`cargo test`(api-server 游戏分发 17、module-game-distribution 11)、`check:encoding`、`check:doc-index`、`check:rustfmt`、SpacetimeDB schema guard(对比 `origin/master`)。
- 第三次合并 master(`500835407` → `fdc14404b`:DirectProject 回合三态、投影 memo、宿主崩溃后的回合收口、策划对话布局修复、AGC release 每日调度):git 自动合并无冲突,但产生了**静默拼接缺陷**——新加的聊天头 CSS 被并进了 master 策划态分组选择器中间,导致 `.project-chat-topbar-status` 在策划态丢失 `font-size`(`chatDialogFrameLayout` 用例抓到)。修复方式是按 master 原文重建分组规则、把发布入口规则独立成块,并把重复的状态规则删掉。
- 合并后复核:全量 `npm test` 393 文件 / 4374 用例通过,root / AGC / admin-web 三端 typecheck 通过,发布入口(聊天头「发布到游戏广场」→ 试玩包导出 → 发布面板)在新回合三态下保持接线。
- 发布入口灰度下发:`GET /api/runtime/frontend-config` 新增 `gameDistributionPublishEnabled`,复用既有 `is_game_distribution_publish_enabled_for_user`(未配置 `game-distribution:publish` 或 `enabled=false` 时对已登录作者默认开放,显式收紧后只放行白名单/灰度命中,匿名恒为 false),避免前端入口与写入口出现两套判据。网页端 `PlatformEntryActiveFlowShell` 据此隐藏「发布游戏 / 发布新版本」入口,`/games/publish` 直接访问时渲染「发布功能正在灰度中」并提供重新检查;AGC 端 `readGamePublishAvailability` 同样读该字段,只有命中才把发布回调交给 DirectProject 聊天头。
- 灰度验证:`cargo test -p api-server frontend_runtime_config`(6 passed,含新增的 `frontend_runtime_config_game_distribution_publish_is_scoped_to_authenticated_gate`:无 gate 行 → 登录作者 true/匿名 false;`enabled=true` 无白名单 → false;白名单命中 → true;`deny_user_ids` → false;`enabled=false` → true;`rolloutPercent=100` → true)、网页发布页 15 用例(含灰度未命中隐藏表单与「重新检查」放行)、平台壳 18 用例(含广场入口按灰度隐藏/显示)、AGC 发布服务 6 用例(含字段缺失与读取失败按不开放处理)。
- Phaser 一键发布闭环(作者不构建、不打 ZIP):`export_local_project_package` 改为发布前构建——已有可玩入口直接打包,否则解析 `game/` 或项目根的 npm `build` 脚本(`resolve_publish_build_plan`),缺 `game/node_modules` 时先跑 `project.bootstrap`,再走 `project.verify` 的受控 npm 运行器执行 build,最后校验入口并打包;构建或安装失败返回带日志尾部的可操作错误。真实 Phaser 4.2.1 + Vite 7 工程验证:构建产物使用相对引用(`./assets/...`),ZIP 370,969 B 经真实素材直传 + 创建游戏/版本/上传/送审/审核通过后,发行网关 `index.html` 200(323 B)与 `assets/index-DZGg_tPs.js` 200(1,388,719 B),网页播放页在 `allow-scripts` 沙箱 iframe 内渲染出 `PHASER-PUBLISH-OK` 与可点击按钮。
- 发行网关根路径:`GET /api/game-distribution/releases/{gameId}` 与带尾斜杠的同一路径等价于 `index.html`(生产由每游戏 origin 映射根路径,本地直连网关或入口直接填网关地址时同样可玩);路由级用例覆盖 Cookie 拒绝门与根路径。
- 发布灰度改为**默认关闭**并修掉客户端“看得到点不动”:`is_game_distribution_publish_enabled_for_user` 现在要求 gate 行存在且 `enabled=true`(未登录、无行、`enabled=false` 一律 false),因此没配灰度时 `gameDistributionPublishEnabled=false`,AGC 不再渲染「发布到游戏广场」按钮、网页入口也不出现;AGC 侧新增 `announcePublishMessage`,把「已构建并打包试玩包」「先打开一个项目再发布」等提示通过 DirectProject 聊天容器的 `announce` 出口回话(普通项目不渲染工作台状态行,之前只写 workspaceStatus 才会表现为点击无反应)。后台「灰度发布配置」新增「可配置开关」列表:预设开关在未创建行时也可见并可一键配置(不再需要先猜 gate key)。
## 尚未完成
- 真实独立发行域名、通配 TLS 与 CDN 仍属部署侧:边缘模板与门禁已就绪,本地已用真实 nginx 验证按主机映射、Cookie 403 与命名空间隔离,但仍需在真实域名/证书下跑一次“审核通过 → 游玩 → 换版 → 下架”并确认 CDN TTL 不超过 60 秒窗口。
- 版本回读、撤回与管理员安全下架已实现;主规范 HTTP 表中不再有待落地路由。容量与限额边界、发行包 PUT 重试已有真实栈证据;仍未做的是 CDN purge 失败行为、回滚演练与清理策略(不删除仍被公开版本引用的对象)的上线验收。
- 生产调用依赖已初始化的 editor generation runtime service identity;未初始化时 procedure 拒绝写入,不会退回 API 进程内存状态。
@@ -0,0 +1,47 @@
# 实施计划:游戏分发阶段 C · AGC 发布资料 AI 生成
- 状态:`in_progress`
- 日期:`2026-09-23`
- 上游:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 的“AGC 游戏分发与在线游玩合同”
- 里程碑:`docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md` 阶段 C
## 1. 交付结果
AGC 发布面板完成三项收敛:
1. 移除 ZIP 路径、文件数、体积和“只上传字节”等技术摘要。
2. 打开面板时基于有界、脱敏的项目上下文免费生成一句话简介和七类白名单分类;失败保留本地兜底,不阻断发布,生成结果可编辑。
3. 游戏封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。
## 2. 实现边界
- 只改 AGC 发布面板、AGC 发布 service、api-server 内部发布资料建议路由、shared DTO、定向测试和文档。
- 不改网页发布表单、游戏分发审核 API、发行包上传/审核状态机、SpacetimeDB schema 或 `/api/external/v1` OpenAPI。
- 简介/分类生成不写用户泥点账本;封面生成继续由现役 `execute_billable_asset_operation_with_cost` 负责预扣、幂等、失败退款和结果登记。
- 传给文本模型的上下文只包含项目名称、创作目标、任务标题/状态、素材 kind/相对路径、运行状态和最近编辑提示;不包含绝对路径、聊天记录、凭据、Token 或完整 manifest。
## 3. 实现步骤
1. `packages/shared` 与 `shared-contracts` 增加发布资料建议请求/响应 DTO,分类继续使用 `GAME_DISTRIBUTION_CATEGORIES`。
2. api-server 增加 `POST /api/game-distribution/publish-metadata/suggestions`,经 Bearer 鉴权后使用内部文本模型生成严格 JSON;解析失败或模型不可用时返回本地确定性兜底,不触发钱包扣费。
3. AGC `GameDistributionPublishPanel` 打开时调用建议接口;用户未修改字段时回填,用户已编辑或迟到响应不得覆盖,失败不影响发布。
4. AGC 封面生成复用 `POST /api/editor/images/generations` 的 `publication-material`、`gpt-image-2`、`16:9`、`2K` 合同;按钮和确认弹窗从 `/api/editor/generation-pricing` 读取并显示具体泥点数。若响应进入队列,轮询 `/api/runtime/external-generation/jobs/{operationId}`,完成后直接使用 `assetObjectId`。
5. 发布截图同批并行上传;成功缩略图悬浮显示“删除|预览”,失败缩略图内部显示错误并在下方保留删除按钮。失败项不参与发布,不阻断同批其它截图。
6. 定向测试覆盖面板删除技术摘要、免费资料回填、封面价格/确认/生成/`coverAssetId` 直用、截图并行与单张失败跳过、服务端 parser/route 与输入边界。
## 4. 验收判据
- 发布面板不出现 `发行包摘要`、ZIP 相对路径或文件数/体积。
- 建议接口成功时简介和分类自动回填;分类只可能是七类之一;接口失败时保留原创作目标或通用兜底。
- 建议请求不会调用钱包、不会写 `asset_operation_consume` 或 LLM Router 额度账本。
- 封面按钮和确认弹窗显示后端运行时定价对应的具体泥点数;确认后调用现役图片生成接口,生成结果直接成为发布 `coverAssetId`,没有第二次上传或素材身份分叉。
- 同批截图并行上传;成功图悬浮显示删除/预览,失败图内部显示错误并有独立删除按钮;失败项不参与发布且不影响其它截图。
- 生成失败、余额不足和队列失败均在面板内可见且可重试,不进入聊天历史。
- 定向 vitest、api-server Rust 测试、AGC typecheck、`check:encoding`、`git diff --check` 通过。
## 5. 非目标
- 不做标题 AI 改写。
- 不做网页发布页的自动生成。
- 不新增发布草稿持久化、生成历史、重试队列或 SpacetimeDB 表。
- 不改变封面上传入口和手动选择封面的能力。
@@ -17,7 +17,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev`
- 目标 bucket 配置:`GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_*`,默认 `agc-dev`。
- 契约:`shared-contracts::agc_project_snapshots` 新增请求/响应 DTO 与项目 ID、相对路径、摘要校验函数。
- 客户端增量索引:`<AppData>/project-snapshots/<projectId>/index.json`,按用户身份判等,换号后按冷启动全量重算。
- 排除口径:复用 `should_skip_project_snapshot_path`(整个 `.agent`、`.git`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点)。
- 排除口径:快照同步使用 `should_skip_project_snapshot_sync_path`(2026-09-22 起)。`.agent` 承载项目身份与 Agent 状态,整目录同步;`.git`、构建与依赖目录、凭据目录、敏感后缀、符号链接与重解析点仍然排除。项目索引、checkpoint、Agent 上下文与 git 检查继续沿用 `should_skip_project_snapshot_path` 的整个 `.agent` 排除口径。
## 不做
@@ -32,7 +32,7 @@ AGC 在项目打开期间按周期把用户项目增量上传到 OSS `agc-dev`
1. 首次同步上传项目内全部符合条件的普通文件;再次同步在无改动时上传 0 个文件。
2. 只修改一个文件时,差异集合恰好包含一个修改项;删除一个文件时上传集合为空且清单中不再包含该文件。
3. `(字节数, 修改时间)` 未变的文件复用已存摘要,不重复读取内容计算摘要。
4. 排除规则命中项(`.agent/runtime`、`.agent/logs`、`.git`、`node_modules`、构建产物、临时文件、符号链接)与超限文件进入跳过清单,不进入上传集合。
4. 项目内 `.agent` 的全部普通文件(`manifest.json`、`agent.db` 与其 WAL/SHM、`.manifest.json.lock`、`project.lock`、conversations、logs、runtime、checkpoint、workbench)进入上传集合;`.git`、`node_modules`、构建产物、凭据目录与敏感后缀仍不进入;超限文件进入跳过或延后清单,不静默丢弃。
5. 任一次同步失败(非鉴权类)不推进本地索引,下一次触发重算并重试;鉴权/权限类失败不自动重试。
6. 同一项目的并发触发串行执行,不产生两路重复上传。
7. 工作区窗口关闭与应用退出都会触发一次同步,且关闭路径不因同步失败而阻塞退出超过超时上限。
@@ -42,7 +42,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
- 未知 item 类型由 Rust 原样透传(只有类型与身份,Rust 侧留 TODO),当前由前端投影丢弃。
- 前端聊天卡片的工具形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`;`tool-calls.jsonl` 的持久化形状与 DirectRuntime 的写入保持不变。
- 「可显示」的判据取**前端回合反馈**:一次翻页操作连拉到「合并后聊天投影的回合数增加」为止。工具卡片与思考文本虽然能通过 `projectDirectThreadItem`,但可能整页落进已渲染回合的折叠「执行过程」,不构成用户可见反馈;口径只在 `directHistoryPaging.ts` 里实现一份,首屏与「显示更早」共用。
- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,`/history` 手动重读保持按当前文件尾取尾屏的恢复语义。
- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,手动重读保持按当前文件尾取尾屏的恢复语义。
## 依赖与前置条件
@@ -0,0 +1,139 @@
# 【里程碑】游戏分发、发布与在线游玩
| 字段 | 值 |
| --- | --- |
| Version | 0.2 |
| Status | accepted(用户“继续”确认按既定假设进入阶段 A;A 未验收) |
| Date | 2026-09-18 |
| Parent Spec | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` |
## 交付与评审边界
本文件拆分主规范“AGC 游戏分发与在线游玩合同”的完整业务:真实上传、持久化发行、审核、隔离托管、AGC 发布、网页上传与游玩、上线运维。当前只授权阶段 A 实现,阶段 B/C/D 仍为 `proposed`,没有已上线/已验收结论。
先完成主规范与本文件评审,再为一个获准阶段创建单独实施计划;未经该阶段验收,不进入依赖它的阶段。里程碑只规定行为与证据,不预先写代码步骤。
| 阶段 | 行为切片 | 依赖 | 状态 |
| --- | --- | --- | --- |
| A | 真实包、游戏身份与可恢复发行 | 主规范评审及持久化合同冻结 | accepted(实现中) |
| B | 人工审核、公开投影与隔离托管 | A 验收;发行站点/私有存储/审核策略确认 | proposed |
| C | AGC 与网页发布、现代游戏目录和跨端游玩 | B 验收;导航与交互稿评审 | proposed |
| D | 端到端验收、容量与撤销、上线准备 | C 验收;实际部署资源可用 | proposed |
## 实现进度(2026-09-20,未验收)
以下是已落地的实现与本地运行时证据,**不等于阶段验收**:B/C 的隔离托管、域名与生产验收仍缺少真实发行域名、TLS/CDN 与生产账号。
- 阶段 A:真实 ZIP 上传、游戏身份、owner/幂等/CAS、状态机、DTO 与 schema 门禁已完成;真实 SpacetimeDB + 私有 OSS 的创建/上传/确认/重启恢复已有证据。
- 阶段 B:人工审核(后台列表、通过需 HTTPS 入口、拒绝需理由)、公开投影、发行网关(按公开版本服务、扩展名白名单、`nosniff`/CORP/CSP、带 Cookie 403)与作者下架已实现;每游戏独立来源已有可执行工件 `deploy/nginx/genarrative-release-origin.conf` 与门禁 `npm run check:release-origin-config`,并已在本机用真实 nginx + 真实网关验证按主机映射、Cookie 403 与平台命名空间 404;生产域名、通配证书与 CDN TTL 仍需上线环境确认。
- 阶段 C:AGC 客户端「发布到平台」面板与发布链路(dist 归一化根 `index.html`、摘要/字节数声明、幂等键、`localProjectId` 复用)已实现并有请求组装与 Rust 导出测试;网页 `/games/publish` 走同一服务端管道,本地已用真实文件选择验证;AGC GUI 自身的端到端发布仍待客户端环境验收。目录(关键词/分类/设备筛选、滚动与筛选恢复)、详情、游玩页(主动作后加载、超时重试、旋转提示、全屏、移动端门槛)与作者中心(状态、驳回理由、撤回、下架)已实现;本地已在桌面与 `390x844` 移动视口真实游玩。
- 阶段 D:容量/额度、重启恢复、CDN 撤销与回滚演练尚未开始,依赖生产资源。
细节与命令级证据见[实施计划【游戏分发阶段A领域合同】](【实施计划】游戏分发阶段A领域合同-2026-09-19.md)的「已完成证据」「运行时证据」「尚未完成」。
## 共通范围与不做项
- 正式状态来自后端,素材仍复用平台上传与归属能力;前端和 AGC 不另建公开游戏状态、owner 事实或审核结果。
- 真实发行包具有不可变版本和 SHA-256,AGC dist 归一化为发行根 `index.html`;网页 ZIP 与 AGC 共用一条服务管道。
- 不恢复退役玩法 API、公开作品表或专属 runtime;不把私有项目源码镜像公开。
- 首版不包含原生/Wasm 游戏、任意外网依赖、服务端进程、多人联机、云存档、评论/评分/关注、排行榜、推荐算法和收益结算。
- 本文件只协调本业务;不顺带改造图片编辑器、Agent Runtime 执行模型或无关项目数据。
## 阶段 A:真实包、身份与可恢复发行
### 前置条件
- 主规范的包类型、限额、身份/幂等/CAS、状态机和内部 API 已评审。
- 游戏、版本、审核与操作账本的完整字段、索引、唯一约束、受信服务身份及清理策略已经冻结;若涉及已有表破坏性变更,另有已确认迁移计划。
### 行为与验收
- [ ] 登录用户创建服务端分配的游戏,owner 不能由请求伪造;其他账号不能读取私有版本、上传、提交或撤销。
- [ ] 服务端接收真实 ZIP 字节,重算摘要/字节数并建立展开清单;只有 metadata 的请求不能获得已上传或已发布状态。
- [ ] 缺入口、越界/重复/大小写冲突路径、符号链接、压缩炸弹、敏感内容和额度超限均失败关闭,原私有对象和公开状态保持一致。
- [ ] 一份版本只接受一份已确认内容;同 key 同请求重放无重复游戏/版本,不同请求冲突;同版本并发上传不混写。
- [ ] 校验可异步恢复,响应丢失、服务进程退出和客户端重试均回到原版本;确定失败和未知结果在响应中可区分。
- [ ] 正常及失败状态、私有查询和错误 envelope 在 Rust 与 TypeScript DTO 中一致;新增 schema、迁移、表目录与绑定一致。
### 证据要求
- 自动化:领域状态机、owner、幂等/CAS、包读取与错误路径、DTO 和 schema 定向测试。
- 运行时:真实 SpacetimeDB 与私有对象存储完成上传/回读/重启恢复;`/healthz` 正常。
- 边界:服务端记录的 ZIP 摘要与测试上传字节一致;未审核目录不能匿名读取;无凭据或本地路径泄漏。
## 阶段 B:审核、公开投影与隔离托管
### 前置条件
- A 已验收;人工审核角色、资料检查标准与拒绝/封禁行为已确认。
- 已确定不同可注册站点的发行域名、每游戏独立 origin、私有存储和网关能力;同源临时路径不能作为验收替代。
### 行为与验收
- [ ] 自动校验通过只进入待审,管理员可查看真实待审游戏并批准/拒绝;审核记录可追溯且普通作者不能提交审核动作。
- [ ] 新游戏审核通过并核验发行文件可读后才公开;更新待审或失败不改变旧版资料、URL 与可玩性。
- [ ] 审核激活与下架使用 `publicationRevision` CAS;过期审核、重复批准、并发更新和下架不会恢复本应关闭的游戏。
- [ ] 游客目录、详情和启动接口只返回已公开投影;未公开和已下架状态均不可见,不返回私有快照地址。
- [ ] 每游戏在独立 HTTPS origin 上,iframe sandbox、网关 CSP/CORS/MIME/禁止 Worker 等策略与主规范一致。
- [ ] 实际 npm/Vite 模块和同包资源在 opaque sandbox 下可载入;外站 fetch/WebSocket、平台 Cookie/storage/DOM、顶层跳转、弹窗和敏感权限被阻断。
- [ ] 作者下架及管理员安全下架会关闭新启动和发行读取;不能绕过网关直取公开 OSS 对象;撤销传播符合最大缓存窗口。
### 证据要求
- 自动化:审核权限、版本切换事务、公开投影与入口 allowlist、响应头和缓存策略测试。
- 运行时:真实独立域名内运行至少一个代表性游戏;真实审核、更新失败、并发下架和旧 URL 回读证据。
- 边界:外部请求阻断、匿名私有对象拒绝、跨游戏 origin 隔离及 60 秒以内缓存撤销(以获批值为准)。
## 阶段 C:双端发布与现代游戏体验
- AGC 发布资料生成切片实施计划:`docs/project-memory/plans/【实施计划】游戏分发阶段C-AGC发布资料AI生成-2026-09-23.md`。
### 前置条件
- B 已验收;网页根入口、桌面/移动导航、暖色主题交互稿及首版设备范围已评审。
- AGC 现有 npm 构建/导出能提供完整 dist,不需要把源码同步管道改作发行管道。
### 行为与验收
- [ ] AGC 从已构建 dist 生成根入口为 `index.html` 的真实包,一次提交动作完成检查、资料确认、上传和送审;状态及失败原因与服务端回读一致。
- [ ] AGC 发布面板隐藏发行包技术摘要;打开时基于有界、脱敏的项目上下文免费生成一句话简介与白名单分类,失败保留本地兜底且不阻断发布;作者始终可以直接编辑生成结果。
- [ ] AGC 发布封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。
- [ ] 网页可选 ZIP、提交封面和必需资料,进入相同上传/校验/审核流程;任一客户端可以查看同账号游戏状态,更新沿用相同 `gameId`。
- [ ] 上传中断、双击、登录失效、窗口关闭后恢复原操作;换账号不能恢复前账号私有状态;待审不能显示为已发布。
- [ ] 目录支持真实数据、关键词/分类/设备筛选、空/错/加载态;详情提供明确主动作,搜索与返回恢复上下文。
- [ ] 游客从目录/分享链接进入详情再启动真实已发布游戏;开始操作后才加载 iframe,加载失败可重试,退出不会自动重启游戏。
- [ ] 桌面与移动导航、详情、上传/发布面板、旋转提示、全屏、安全区和焦点可达符合主规范;不适配移动端的游戏有明确门槛。
- [ ] 视觉沿用现有 warm token,通用交互复用共享组件;没有固定假统计、本地演示兜底或同源 iframe 放宽。
### 证据要求
- 自动化:AGC 打包与操作恢复定向 Rust 测试、两端客户端/组件/路由/状态测试及类型检查;发布资料免费生成必须验证不写入钱包账本,封面生成必须验证扣费幂等、失败退款与 `assetObjectId` 直用。
- 运行时:AGC 一次真实发布、网页一次真实 ZIP 上传,分别审核后从桌面和手机游玩;浏览器覆盖游戏模块、素材、音频、触屏及横竖屏。
- 边界:未构建/失效 dist、资料缺失、换账号、迟到响应、审核拒绝、非移动游戏及真实空态。
## 阶段 D:端到端、运维与上线准备
### 前置条件
- C 已验收;生产域名/TLS、CDN、私有存储、审核账号及保留/清理周期可用且已确认。
### 行为与验收
- [ ] 真实环境中完整跑通“首次上传 → 校验 → 审核 → 公开 → 游客游玩 → 更新待审旧版在线 → 新版切换 → 下架撤销”。
- [ ] 100 MiB 包与获批文件数/展开量边界有可复核耗时、内存和失败证据;校验不会执行上传代码,服务资源有界。
- [ ] 校验执行器重启可恢复,审核积压与失败可观测,清理不删除仍被公开版本引用的文件。
- [ ] CDN purge 失败时仍在获批缓存 TTL 内拒绝新资源;明确已下载脚本无法远程抹除的边界。
- [ ] 发布/回滚步骤保留当前公开版本,能关闭新提交和新版本激活;部署路由、缓存、响应头、日志脱敏和告警完成检查。
- [ ] 主规范逐条证据矩阵齐全,未验证项明确列出;有任何核心路径未验证时不标记上线完成。
### 证据要求
- 自动化:范围匹配前后端、schema/契约、编码与文档索引门禁,部署配置检查。
- 运行时:真实发行域名、真实存储、真实账号和两类客户端的完整链路证据,桌面及移动视口记录。
- 边界:压力/额度、重启恢复、缓存撤销、误删防护和回滚演练。
## 当前待审项与下一门禁
主规范仍待确认人工审核策略、导航、首版能力/额度、对象保留、发行域名及运营责任;当前没有主规范与里程碑通过评审的记录,也没有任何阶段验收证据。先评审这些决策,再冻结 A 的持久化与 DTO 明细并创建仅覆盖 A 的实施计划。不得把该文档状态改为 accepted 以代替评审。
所有阶段完成并验收后,稳定事实回写主规范,删除本临时里程碑与各阶段实施计划;过程记录不进入产品代码、用户文案或长期共享记忆。
@@ -0,0 +1,85 @@
# 【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22
状态:已完成
父规范:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 目标与判据
一句话目标:AGC 应用内不再存在任何斜杠命令语义,终端 swarm chat CLI 入口(`--swarm-chat`)连同其命令层整体退役,且不再残留只服务这两者的实现、测试、文案与构建期门禁;行为上等同于它们从未存在。
验收判据:
- AGC 应用内(`apps/ai-game-creator-shell/src/**`)除路径/路由字面量外,不存在形如 `'/xxx'` 的命令字面量,也不存在对 `/` 开头输入的命令分支。
- 斜杠命令专属模块与其导出全部消失,`projectSummary` 桶文件只保留现役导出。
- 权威文档不再承诺「聊天输入 `/<cmd>`」能力,终端章节点也不再把控制命令列为入口。
- 终端侧不再存在 swarm chat 入口与其命令层;`--agent-run`、`--agent-enqueue`、`--agent-steer`、`--agent-resume`、`--agent-context-compact`、`--preview-serve` 等运维类控制命令保持现役。
- 依赖终端入口的真实 E2E 与脚本(`user-input`、`supervisor-autonomous-playable-lane-defense`、三个 supervisor-swarm 混合套件、`agc:test`、`agc:test:chat*`、`agc:chat`、`agc:swarm`)与交互式 CLI harness 管道一并消失。
- 不新增任何守卫测试或 check 脚本条目。
## 范围
范围内(删除):
- `features/project-summary/projectSummaryConstants.ts` 的 `chatCommandHelp` 与全部命令清单常量。
- `features/project-summary/chatCommandMetadata.ts`(斜杠命令参数校验文案)。
- Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`。
- `chatPromptPolish` 的 `/` 前缀绕过分支。
- `memoryCommands.ts` 的 `parseRememberInput`(`/remember` 参数解析器)。
- 只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`。
- `/sync-canvas-project`、`/read <path>`、`/trace` 的草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)。
- 无人调用的 Tauri 命令 `get_game_creation_agent_capabilities`、`get_limited_local_commands`。
- 钉住上述字符串的构建期门禁条目与专属测试。
- 终端 swarm chat 入口与整个命令层:`cli.rs` 的 `SwarmChat` 变体、`--swarm-chat` 解析与派发、`src/swarm_cli.rs` 与 `src/swarm_cli/` 整个目录(输入解析与帮助输出、`/agents`、`/status` 打印器、`/goal` 引擎、`/compact`、`/resume`、`/quit` 退出分支、观察器、报告、turn 派发与等待)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`,以及 `swarm_cli/input.rs` 中那份只为终端命令存在的 `agent.compact` / `agent.resume` / `agent.run_status` 权限校验(同名权限 id 在现役 Tauri 命令与 Runtime 生命周期上的门禁保持不动)。
- 只服务终端交互内核的 `agent/interaction.rs` 整层:`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`;其上的自然语言 steer 决策 LLM 路径 `decide_game_creator_agent_runtime_steer_at` 在收尾复查后一并删除,见「收尾清理」。
- 只能由命令触达的打印器:`print_runtime_response_stream_status` 及其专属测试。
- 依赖终端入口的脚本与套件:`scripts/agent-swarm-test-chat.mjs`、`scripts/agent-runtime-deterministic-playable-e2e.mjs`、`scripts/deterministic-lane-defense-provider.mjs`,套件 `user-input`、`supervisor-autonomous-playable-lane-defense`、`supervisor-swarm-autonomous-chat`、`supervisor-swarm-static-isolated-autonomous-chat`、`supervisor-swarm-collaboration-policy-mixed-recovery`,以及 harness 里的交互式 CLI 管道(`startInteractiveCli`、`writeInteractiveCliLine`、`waitForInteractiveCli*`、`closeInteractiveCli`、`activeInteractiveCliSessions`、`answerRemainingInteractiveQuestions` 等)与只服务这些套件的混合套件分支、状态字段、sentinel 与 npm 脚本。 同时清掉只被这些套件调用的 harness 残件:`collaboration-assertions.mjs` 的混合/静态隔离断言族(`supervisorSwarmMixed*`、`observeSupervisorSwarmStaticIsolatedProviderOverlap`)、`repair-recovery.mjs` 的静态隔离观测调用点、evidence 模板与校验里的 `mixed*` / `staticIsolated*` / `initialBatchRecovery*` 占位字段,以及 `self-test.mjs` 中对应的合成用例与汇总字段。
范围外(保留):
- 命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)。
- `needsInitializedChatProject` 与项目权限策略链路。
- Rust 侧路径/路由的 `/` 前缀校验。
- `--agent-run`、`--agent-enqueue`、`--agent-task`、`--agent-steer`、`--agent-retry`、`--agent-cancel`、`--agent-confirm`、`--agent-resume`、`--agent-runtime-status`、`--agent-goal-*`、`--agent-context-compact`、`--runner-status`、`--runner-shutdown-if-idle`、`--llm-status`、`--preview-serve`、`--environment-check`、`--direct-codex-chat` 等运维与开发 CLI 控制命令。
- 保留的真实 E2E 套件:`supervisor-swarm`、`supervisor-swarm-transient-retry`、`supervisor-swarm-final-reply-transient-retry`、`supervisor-swarm-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。
## 检查点
1. 斜杠语义层:Direct `/history`、润色绕过、`chatCommandHelp`/`chatCommandMetadata`、`parseRememberInput`、桶文件与门禁、专属测试。
2. 死链与死模块:草稿回填链(前端 + Rust + 断言)、零调用摘要模块、`agentTrace.ts`。
3. 无人调用的能力清单 Tauri 命令。
4. 文档收口:AGC 主专题命令承诺、ADR、`decision-log.md`、`CONTEXT.md`。
5. 终端 swarm chat 命令层与只服务它的权限门禁、打印器、专属测试。
6. 终端 swarm chat 入口本体、交互内核、`--swarm-chat` 派发与其专属 Rust 测试。
7. 依赖终端入口的 e2e 套件、deterministic wrapper/provider、npm 脚本、构建期门禁条目、harness 交互式 CLI 管道、混合套件分支,以及随之失效的混合/静态隔离断言族与 evidence 占位字段。
8. 文档收口:终端入口退役后主实施计划、Runtime 文档、ADR、decision-log、pitfalls、里程碑计划的最终口径。
## 验证
- `npm run --workspace apps/ai-game-creator-shell typecheck`(含 `skill-pack:check` 与 `check-config.mjs`):通过。
- 定向用例 `npx vitest run tests/appSurface.test.ts`:211 tests(202 passed / 9 skipped);`chatPromptPolish`、`rememberCommand`、`ChatMarkdownMessage`、`agentRuntimeModel` 等相关用例集通过。
- `cargo check --tests`:0 error;删改文件无新增 `dead_code` 告警(按「父提交 vs 本次」引用数逐条比对告警标识符确认)。
- `npm run check:encoding`、`npm run check:doc-index`、`git diff --check`:通过。
- 真实 E2E harness 自检 `node scripts/agent-runtime-real-e2e.mjs --self-test`:`status: PASS`;harness 相对与具名 import 全部可解析。
- 收尾清理(2026-09-23,LLM steer 判定链):`cargo check --tests` 0 error,告警与改动前基线一致(无新增 `dead_code`);`npm --workspace apps/ai-game-creator-shell run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过;幂等复核 `steer_decision` / `interrupt_for_steer_decision` / `decide_game_creator_agent_runtime_steer_at` 在 `apps/`、`packages/` 源码中零命中。
## 文档收口
- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`:删除全部「聊天输入 `/<cmd>`」能力条目,保留并改写其中的非命令事实;`/compact`、`/mcp`、`/goal`、`/resume` 的终端承诺改为对应 `--agent-*` 入口。
- `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`:删除终端 swarm chat 入口整节,控制面统一记为 `--agent-*` CLI(`/goal` → `--agent-goal-*`、`/compact` → `--agent-context-compact`、`/mcp` → 开发配置面板与真实 E2E 核验);终端不再承担任何交互式聊天职责。
- `docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md`:本次决策与影响边界。
- `docs/project-memory/shared-memory/decision-log.md`:新增 2026-09-22 决策条目,并清理已被本次退役取代的历史命令条目。
- `docs/project-memory/shared-memory/pitfalls.md`、`docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md`:移除 `/history` 入口表述,改为「显式重新加载对话」。
- `CONTEXT.md`:新增「项目对话输入」「项目命令 id」术语,把「斜杠命令」标为已退役说法;不再出现 `/history` 重读入口。
- `docs/README.md`:登记新 ADR。
## 收尾清理
复查(2026-09-23)发现退役残留与随之产生的零引用代码,按同一口径清理:
- 配置向导 `scripts/game-creator-config-wizard.mjs`:删除指向已删脚本的 `npm run test:chat` 调用、失去含义的 `--configure-only` 开关,以及只服务该分支的 `askYesNo` 与 `npmCommand`。
- 用户文案:App 内 `请先用 /project 设置本地项目。` 改为 `请先打开本地项目。`。
- 注释与 prompt:`chatPromptPolish` 头部注释的 `/` 命令表述、`harness/process.mjs` 与 `scripts/check-config.mjs` 中指向已退役入口的说明,以及 `interaction.json` 中只服务已删交互内核的 7 个键(`execute_description`、`resume_description`、`project_location_description`、`protocol`、`system`、`user`、`user_with_context`)。
- harness 死导出:`runtime-state.mjs` 的 4 个 schema 常量、`assertions/runtime.mjs`、`harness/project.mjs`、`collaboration-policy.mjs`、`persistence.mjs` 中因调用方被删而零引用的函数,连同因此失去用途的 import。
- 无前端调用方的 Tauri 命令 `start_game_creator_agent_runtime_task` 与 `steer_game_creator_agent_runtime_task` 全部删除(`main.rs` 注册与 `check-config.mjs` 条目同步移除)。删掉 steer 命令后其唯一非测试入口的整条 LLM steer 判定链一并退役:`agent/interaction.rs`(`runtime_steer_decision` 工具、判定请求构建、响应解析、`decide_game_creator_agent_runtime_steer_at`)、`runtime.interrupt_for_steer_decision` RPC 与派发分支、持久 decision 记录(`agent.runtime.steer_decision`)、`AgentRuntimeProviderInterrupt::applied_steer_cursor`、`AgentRuntimeSteerResult` 的 `assistantReply / interruptDecision / decisionReason` 字段、`steer_decision_*` prompt 键,以及只测该链条的 provider / runner / runtime_state 用例。
## 未做与边界
- 不为退役概念新增守卫测试或断言残留字符串的 check 条目。
- 不为终端入口与已删套件补充替代实现、兼容别名或迁移提示。
@@ -25,6 +25,7 @@
- [ ] 未单独配置快照目标时仍使用 agc-dev,只复用资源存储凭据;显式快照目标保持有效,不迁移现存对象。
- [ ] 后台列表按部署渠道查询,显示项目名/ID、用户(昵称 + 陶泥号)、同步时间、文件数、体积和完整性,支持刷新与游标分页(每页 20/50/100 + 上一页/下一页)。
- [ ] ZIP 按清单还原相对路径;不含对象存储摘要目录;空文件可上传与导出。
- [ ] `.agent` 承载项目身份与 Agent 状态,整目录随快照上传,并在后台 ZIP 中按原相对路径还原;归档因此可用于还原项目身份与 AGC 对话历史。凭据、版本库与构建产物仍不进快照。
- [ ] 清单名称/完整性变化在无内容差异时也提交,partial 可恢复 ready,历史缺字段不冒充 ready。
- [ ] 缺失、损坏、越界路径和非完整清单失败关闭;历史未声明完整性的清单明确标记,允许导出已有文件但不称为完整工程。
- [ ] 无后台权限不能读取项目或 ZIP;不泄漏凭据;ZIP 构建有体积、并发和临时文件清理边界。
@@ -39,6 +40,10 @@
| 层次 | 结果 |
| --- | --- |
| 客户端 Rust `project_snapshot` | 22 通过、1 忽略(写入式真实上传 smoke 未运行);含排队退出等待回归 |
| `.agent` 全量上传口径(2026-09-22 修订) | `project_snapshot` 定向 24 通过、1 忽略;新增扫描纳入 `.agent` 与策略单测,覆盖 `.agent` 内凭据、版本库、`.env*` 继续排除,以及项目索引 / checkpoint 口径不变 |
| `.agent` 真实样本复核 | 对 15 个本地 AppData 项目的 487 个 `.agent` 普通文件按新规则复算,0 个仍落在排除集;该复算是脚本复刻规则,不是 Rust 运行时证据 |
| `.agent` 客户端链路运行时 smoke(2026-09-22) | 用真实项目副本(`.agent` 164 个文件 / 5.61 MiB,项目 `gameagent-agentsmoke1`)走真实差异引擎 → 本地 api-server → 真实 OSS:首轮 `synced`、`uploaded=179` / `18,991,590` 字节、跳过与失败均为 0;紧接着第二轮 `no-op` / 上传 0 个文件。`uploaded=179` 恰好等于原口径的 15 个项目文件加 164 个 `.agent` 文件 |
| `.agent` 存储与后台归档(2026-09-22) | 只读 GET 真实 OSS 清单对象(`agc/project-snapshots/v1/<user>/gameagent-agentsmoke1/manifest.json`,26,356 字节):`files=179`、其中 `.agent/**` 164 条、`pendingFiles=0`、合计 18,991,590 字节;按清单里的 `大小-摘要` 构造对象键 HEAD 命中 `.agent/manifest.json`(18,319)、`.agent/agent.db`(9,353)、`.agent/conversations/project.jsonl`(1,422,226) 与 `game/package.json`(227)。后台 `download` 返回的 ZIP 解压出 179 条目、164 条在 `.agent/` 下,抽样 6 个文件(含 `agent.db`、会话、运行态文件)与本地 SHA-256 完全一致;正在运行的 dev api-server 列表显示同一项目 `files=179 status=ready` |
| 客户端前端生命周期与启动器 | 3 文件、10 测试通过;完整 AGC typecheck、skill-pack、check-config 通过 |
| 后台页面、API client、路由与样式 | 44 测试通过;admin-web typecheck/build 通过 |
| 后端快照与权限 | 14 定向测试通过;未认证路由矩阵与页签映射 2 测试通过 |
@@ -53,3 +58,5 @@
`npm run dev:api-server -- --api-port 4198 --bgfilter-worker-port 4199 --api-timeout-seconds 600` 编译成功,但当前工作区配置的本地数据库 `xushi-p4wfr` 在 `127.0.0.1:3101` 返回 404,启动认证投影无法完成,故 `/healthz` 及完整 HTTP smoke 未通过。仅本任务启动的 API/worker 已停止;没有清库、迁移数据库、改 `.env` 或替换其它项目的验证目标。
尚未替换安装版、构建新客户端发布包或部署;提交推送按用户本轮授权执行。当前条款已有上述自动化与只读存储证据,最终验收仍等待当前工作区数据库就绪后的 HTTP 联调,以及新客户端的隔离实机自动上传验证;完成后再关闭里程碑并清理两份计划。
`.agent` 全量口径已在上表取得真实上传、真实 OSS 清单与后台 ZIP 三层运行时证据。两点边界仍需记住:① 本次 smoke 用的是真实项目的可丢弃副本,其 `projectId` 改成 `gameagent-agentsmoke1`、走 dev 专用账号上传,历史真实项目在下一个同步周期前仍是旧口径清单(不含 `.agent`);② 本地 `api-server.exe` 构建于 2026-09-21 14:06,早于渠道分区提交 `4951b71d7`(2026-09-21 16:57),因此本次对象键仍是 `agc/project-snapshots/v1/` 布局、`GET /admin/api/project-snapshots/channels` 在本地返回 404;升级到 v2 键布局需要重建并重启本地 api-server。
File diff suppressed because it is too large Load Diff
@@ -92,10 +92,24 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m
## Jenkins 定时版本调度
定时与版本比较只保留在 `Genarrative-Scheduled-Revision-Trigger` 一处:每小时用 `git ls-remote` 解析 `SOURCE_BRANCH` 远端 HEAD,与上一次触发过的 revision 比较,变化时才把同一个 `COMMIT_HASH` 传给 `Genarrative-Full-Build-And-Deploy`,并先经 `Genarrative-Agc-Global-Version-Issue` 发号、再把同一个总版本号透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,保证两个客户端的平台分区发布同一个版本。这三个下游 Job 都不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住这两类回退。macOS 节点是日常办公机,调度触发它时置 `SKIP_IF_SUPERSEDED=true`:节点离线期间排队的旧构建在恢复后会自行让位,不发布过期版本。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
定时与版本比较收口到两条调度器:`Genarrative-Scheduled-Revision-Trigger` 每小时处理 dev 渠道,变化时触发 Full Build、AGC Windows/macOS dev,并让两个客户端平台共用同一个总版本号;`Genarrative-Scheduled-Release-Trigger` 每天 04:00 按服务端与客户端两条独立 scope 处理 release,服务端变化时用 `DEPLOY_TARGET=release` 触发正式 Full Build,客户端变化时经 `Genarrative-Agc-Global-Version-Issue` 发同一个总号并触发 AGC Windows/macOS release。两条调度都等待并汇总下游,按成功 lane 推进 revision;失败 lane 下一轮单独补发。各下游 Job 都不得自带 `triggers` / `cron`,也不得在管线内再做一套版本去重;`npm run check:production-ops` 会拦住回退。macOS 节点是日常办公机,两条调度触发它时都置 `SKIP_IF_SUPERSEDED=true`:节点离线期间排队的旧构建在恢复后会自行让位,不发布过期版本。调度状态与生效步骤见 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
## Gitea CI 依赖闭合
AGC Rust 两条 lane、crates、smoke、Backend 和桌面壳测试使用镜像内可信 sccache 对象快照;Native shell release step 显式清空双 wrapper,前端/repository checks 不启用。维护者通过 `scripts/build-gitea-rust-cache.sh` 从远端 master 在限额、无宿主挂载的临时容器中按实际 cwd/profile/目标预热全部测试组,仅编译、不执行测试/应用;后端 workspace 与 spacetime-module 保持独立,AGC 的三个 cwd 入口之间清理预热 target,防止 fresh 判断漏产缓存键。最终镜像只追加 sccache、对象和来源元数据,不包含源码或 target。容量上限 4 GiB,不替代宿主旧镜像/归档清理。PR 只写当前容器层、不回传,不开放 Docker API/发布权限;继续禁用 incremental。`ci-rust-cache.sh` 在快照缺失、工具链不符或 wrapper 探测失败时直接编译,并隔离远程缓存配置和 daemon。分片日志记录编译耗时,收尾输出命中统计;两个 lane 的测试和前置检查不同,耗时差不是严格 A/B。线上存在活跃 CI 时不得重启 runner 或切换标签;全组启用前须刷新完整快照并逐组验证,详见开发运维文档。
缓存维护下载必须核对 artifact 元数据大小与实际响应,并立即检查 ZIP 完整性;有长度上限不等于能检测短读。网络或归档损坏最多重试 3 次且重新取签名链接。sccache 同 key 的不同 ZIP 成员排列会改变整个对象 SHA;新增对象去重仅在成员内容 SHA、权限和 ZIP 元数据均一致时接受排列差异,真实内容及继承对象冲突仍拒绝。禁止通过任取一份冲突对象绕过完整性契约。
Buildx 0.30.1 的 `inspect` 不支持 `--format`,builder 驱动校验读取普通输出的 `Driver:` 字段。相关命令须在宿主真实插件上验证;测试替身应拒绝不支持的参数,避免把模拟命令成功误当兼容性证据。
BuildKit 会把 Dockerfile 中的裸 `FROM sha256:<Image ID>` 当作远程镜像名;快照组装须在维护锁内把可信基础 Image ID 绑定到专用临时 tag,并显式使用能读取宿主镜像的 `default` Docker builder。成功或失败后去 tag,元数据和 runner 仍固定完整 Image ID;不要复用受管 `base_tag` 给历史基础镜像打别名,以免改变自动清理归属。
Gitea 基础镜像通过专用 `genarrative-ci-images` Buildx builder 持久复用 Cargo/npm 下载缓存;稳定 cache mount 与 commit、lock 哈希无关,以 `sharing=locked` 隔离并发写入,仅供可信宿主构建、不开放给 PR。最终镜像显式物化当前依赖下载快照,仍不包含 node_modules/target 或上一版 sccache 层。首次可用 `seed-downloads` 从可信完整 Image ID 提取包缓存,操作账号须与维护服务一致;部署要求及 builder GC 空间目标见 `deploy/container/README.md`。构建上下文必须覆盖 AGC vendor 与编辑器 bridge 的全部本地 path manifest,普通源码变化不应使依赖层失效。维护 journal 提供阶段耗时和失败 build.log 定位。
Gitea Rust 缓存自动维护由宿主 `genarrative-ci-cache.timer` 收集同一 master push run 六个 Rust job 的原生 V4 缓存产物,不重复执行 Cargo 预热。只传本轮新 key,命中对象只传使用时间;宿主与真实来源镜像对象合并、去重、按新近使用时间裁剪到 4 GiB,从无对象缓存基础镜像重新组装。源 run 不要求全绿,但取消、缺组、旧 attempt、未完成上传或混用来源镜像不得采用。网关暂停新 FetchTask、在途领取结束、持久化任务账本清空且内层活动容器为空才切换,不打断运行中的 CI。首次接入/升级网关需空闲窗口;Token 只需普通仓库 `write:repository`,不查管理员 API。候选装载后清理已收集 artifact,遗留项保留 7 天;真实 master CI 验证后才清理旧镜像,保留当前、一个回滚版、基础镜像及容器引用。部署入口见 `deploy/container/README.md`,合并代码不等于服务启用。
修改 Gitea workflow 的 job 显示名称、ID 或缓存导出组时,必须同步维护器的 `JOBS` / `RUST_JOB_IDS`;`test_gitea_cache_maintenance.py` 直接对照实际 workflow 检查全集和导出映射,避免自动刷新或镜像验收因名单漂移长期等待。维护器 `Api.request` 的 `method` 是必填关键字参数,GET 也必须显式指定,不根据 body 推断请求方法。
Gitea 缓存部署必须区分网络:runner 的 RPC 走 `gitea-runner-fetch-gate:8080`;内层 job 的 checkout/上传走映射到 `172.30.0.3` 的 `http://genarrative-station/git`;宿主专用 clone 走 `http://127.0.0.1:3003`。不要把 runner 可达的 `gitea:3000` 配给 job。内层 Docker 使用 `10.240.0.0/16`、每 job `/24` 的默认地址池,避开外层 `172.30/172.31` 网段;恢复领取前必须在真实 job 网络里验证 checkout 与 Gitea API,不能只验证 FetchTask。具体配置与遗留空网络处理见 `deploy/container/README.md`。
AGC Rust 两条 lane、crates、smoke、Backend 和桌面壳测试使用镜像内可信 sccache 对象快照;Native shell release step 显式清空双 wrapper,前端/repository checks 不启用。仅首次人工 bootstrap 时,维护者通过 `scripts/build-gitea-rust-cache.sh` 从远端 master 在限额、无宿主挂载的临时容器中按实际 cwd/profile/目标预热全部测试组,仅编译、不执行测试/应用;后端 workspace 与 spacetime-module 保持独立,AGC 的三个 cwd 入口之间清理预热 target,防止 fresh 判断漏产缓存键。最终镜像只追加 sccache、对象和来源元数据,不包含源码或 target。容量上限 4 GiB,不替代宿主旧镜像/归档清理。PR 只写当前容器层、不回传,不开放 Docker API/发布权限;继续禁用 incremental。`ci-rust-cache.sh` 在快照缺失、工具链不符或 wrapper 探测失败时直接编译,并隔离远程缓存配置和 daemon。分片日志记录编译耗时,收尾输出命中统计;两个 lane 的测试和前置检查不同,耗时差不是严格 A/B。线上存在活跃 CI 时不得重启 runner 或切换标签;全组启用前须刷新完整快照并逐组验证,详见开发运维文档。
`.gitea/workflows/project-ci.yml` 的客户端门禁拆成 lane 与功能 job,每个 job 只预热自己会构建的那几份依赖:`AI game creator shell Rust lane 1/2`、`lane 2/2` 各自预取一次 AGC 壳 manifest,并顺序运行两片 Rust bin 单测;`AI game creator shell Rust smoke` 同样只预取 AGC 壳 manifest(`agent-run` smoke 会用 `src-tauri/Cargo.toml` spawn `cargo run`),`AI game creator shell Rust crates` 预取 `server-rs/Cargo.toml` 与独立 crate,`Native shell tests` 预取桌面壳与 AGC 壳 manifest,`AI game creator shell web tests` 不触碰 Cargo,不预热。两条 Rust lane、smoke job 与 crates job 只用 cargo 与 node 内建模块,因此不执行 `npm ci`。两个被 `server-rs/Cargo.toml` 排除、且没有提交 `Cargo.lock` 的独立 crate(`agent-runtime-core`、`agent-runtime-orchestration`)只能在 `AI game creator shell Rust crates` 里用不带锁标志的 fetch。AGC 壳的 bin target 单测(约 2466 条)由 `apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs` 编译后按 `--list` 名单分 4 片:每次分片调用用 `--shard-index=<i>` 只跑自己那片,片内保持 `--test-threads=1` 并使用独立 `TMPDIR`;两条 lane 之间并发,lane 内顺序运行两片,避免重复依赖预热和同一容器内多进程争抢。不要改回「一个 job 内多进程并行这几片」——同一容器里它们会争抢共享 `HOME`、target 目录与固定临时路径,实测比整套串行还慢。每个分片调用都会自校验「片并集等于全集且互斥」,因此改分片规则不会静默漏跑。Backend host workspace tests 使用 `cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`,避免 `spacetime-module` 的 `spacetime-types` feature 统一污染普通领域 crate 的 host 测试;随后单独执行 `cargo test --locked -p spacetime-module --no-fail-fast`,由 `spacetime-module/src/active.rs` 在 host 测试构建期间提供仅测试期的 SpacetimeDB ABI 链接支持,使该 crate 的纯单元测试也纳入 Backend 门禁。`spacetime-module` 的 reducer / procedure 运行时行为仍必须通过真实 SpacetimeDB runtime/integration harness 验证,host 链接支持不得被当作运行时替身。Backend 另外执行 `cargo check --locked -p spacetime-module` 验证模块源码。AGC 壳检查还会运行 `platform-llm` 与 `shared-contracts` 的 server-rs workspace 测试,这些命令以及 AGC 壳测试必须带 `--locked`,避免在测试阶段重新解析 registry index;锁文件发生变化时应先更新受信任 CI 镜像缓存,再重跑门禁。
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
## 一句话定位
Genarrative / 陶泥儿当前主站聚焦图片画布创作、编辑器项目与素材管理,以及账号、钱包、公共设置、后台管理和小程序 WebView 外壳。AI Game Creator(AGC)是独立的桌面创作 App,按 DirectProject Runtime 合同执行,不复用主站旧玩法入口。
Genarrative / 陶泥儿当前主站聚焦图片画布创作、编辑器项目与素材管理,以及账号、钱包、公共设置、后台管理和小程序 WebView 外壳。AI Game Creator(AGC)是独立的桌面创作 App,按 DirectProject Runtime 合同执行,与主站创作入口、后端契约和运行态保持独立。
产品 IP / 产品形象统一为 `public/branding/taonier-product-ip.png`:陶罐中探出的橙色耳朵形象。
@@ -70,9 +70,9 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
- `ui-prototype`(设计图片)与 UI 编辑器 `UI` JSON 是不同资源。白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`,由 provider-backed 识别、合并和组件绑定持久化 State/revision,并把 `reference-ready → structure-ready → merge-ready → binding-ready → application-ready → completed` 投影到 manifest。Provider 缺失、请求失败、工具缺失、结果不匹配或仍有待审节点时保留真实阶段并返回 blocker,不得用 deterministic seed 伪造完成。
- UI workflow 的资源桥接与 Runtime 边界以 `docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md` 和 AGC 实施计划的 2026-08-24 覆盖段为准;只生成图片、登记空 JSON 或进入普通图片画布都不构成 workflow 完成。
## 退役边界
## 当前产品边界
全部旧创作模板及其创作、生成、发布、公开业务详情与专属运行态已于 `2026-07-17` 退役。历史表和迁移仅按最小 schema 数据壳保留;旧前端、API、worker、procedure / reducer 和纯业务 crate 不属于当前编译或运行能力,不能重新作为兼容目标。
当前主站只提供图片画布创作、编辑器项目与素材、账号与钱包、公共设置、后台管理及小程序 WebView 外壳;AGC 独立承载桌面端游戏创作。不在当前路由树、Cargo workspace 和 SpacetimeDB schema / bindings 中的入口与数据对象,不属于当前产品能力。
## 当前文档入口
@@ -43,6 +43,8 @@
- 画布卡片类型与信息角标共用 `CanvasCardCornerActions`;菜单收纳共用 `OverflowActions`,宿主决定展示数量和资源命令。AGC 选中菜单前 5 项直显,Web 默认不折叠;浮层 portal 继续接入现有画布关闭与滚轮归属判据。
- 修改范围保持聚焦;优先扩展现有系统、页面、组件、DTO 和脚本。
- Agent 可见内容直接描述当前任务、输入和成功条件,细节按调用需要提供。
- AGC 外置智能体提示词只写模型必须遵守的指令与契约(工具名与参数、调用顺序、禁止项、失败处理、产物要求),不写客户端/宿主怎么实现:内部预算字段与限额、内部状态机与枚举、执行器锁与沙箱技术栈、进程/插件部署、注入与投影管线都属于宿主实现,一律改写成对应行为要求或删除。
- 修改提示词文本时保留原有 `{...}`/`{{...}}` 占位符;确需增删占位符必须同步改对应 Rust 调用点与断言该文本的测试。
- UI 开发优先复用现有公共组件;跨页面或跨端重复的视觉/交互模式应沉淀到 `packages/shared`,由现有页面迁移使用。共享组件承载通用表现与交互,领域规则、后端副作用和正式业务状态由后端负责。
- AGC 当前 Agent 与策划 Agent 的消息层级共用 `packages/shared` 的 `AgentMessageContent`:正文使用 `body`,思考、中间输出与工具调用使用 `process`;过程字号和颜色由共享组件统一定义,错误状态保留语义色。
- 后端遵循 `module-*`、`spacetime-module`、`spacetime-client`、`api-server`、`platform-*`、`shared-contracts` 的现役边界。
@@ -1,13 +1,10 @@
# 外部生成 Worker 化方案
> 文档状态:`historical`
本文仅用于历史追溯,不作为当前实现依据。
> 2026-07-18 退役覆盖:旧创作模板 job 类型、玩法写回和玩法恢复链路均已退出现役 worker。当前 worker 只领取 `source_module = editor-canvas` 的任务;本文涉及拼图、跳一跳、拼消消、敲木鱼等玩法的内容仅作为历史设计记录,历史队列行不得被领取或改写。
本文记录 external generation worker 的稳定设计边界;当前实现细节以代码和现行架构文档为准。
> 2026-07-21 已实施、待生产压测专题:BgFilter 作为受限内部资源,仍遵守“单用户动作一个外部生成 job”;用户可见层与调度层都只有父 `external_generation_job`。父 future 保持原 lease 和 attempt,在当前调用栈内同步请求唯一 `bgfilter-worker` 的内部 HTTP,成功图片字节直接返回父流程。首版不新增 SpacetimeDB 子任务表、父 checkpoint / continuation 或 raw 中间结果 OSS。完整边界见 [`BgFilter 受限资源调度方案(同步内部 HTTP 原地等待版)`](./【后端架构】BgFilter受限资源调度方案-2026-07-21.md)。
更新时间:`2026-07-31`
更新时间:`2026-09-23`
## 背景
@@ -20,8 +17,8 @@ VectorEngine `gpt-image-2`、音频、LLM 等外部生成不能由面向外部
- 多个 worker 进程通过 SpacetimeDB 任务表抢占任务,依赖 lease 超时恢复,支持按进程数和单进程并发动态缩扩容。
- 本地或小流量站内同步排查可显式启用 `inline` 模式,由站内 HTTP handler 复用同一 worker executor 同步执行并返回 `completed`;该模式不创建队列任务,也不具备 worker 横向扩容能力。External v1 不继承此例外,始终异步入队。
- SpacetimeDB reducer / procedure 只做任务状态流转,不做网络、文件系统或外部 provider I/O。
- 已接入拼图 `compile_puzzle_draft`、结果页 `generate_puzzle_images` 与结果页 `generate_puzzle_ui_background`,跳一跳、拼消消和敲木鱼的外部图片生成动作,以及图片画布编辑器的图片、改图、手动去背景、图标 spritesheet、UI 素材提取、角色动作、视频、音效和背景音乐生成。后续玩法和编辑器生成入口继续复用同一队列 Module,不再为每个入口发明独立队列。
- 第一版外部生成队列粒度固定为“单个用户动作对应单个 job”。例如草稿编译、结果页单槽重生、图集重生都各自入一个 job;job 内部可以串行或并行调用 provider、OSS、SpacetimeDB 写回,但不再拆成“提示词 / 生图 / 切图 / 去背景 / 持久化 / 回写”等阶段 job。用户可见执行阶段通过现有任务行及摘要投影的轻量 `phase` 保存,不作为队列调度单位,也不写回大 payload。
- 已接入图片画布编辑器的图片、改图、手动去背景、图标 spritesheet、UI 素材提取、角色动作、视频、音效和背景音乐生成。外部生成入口继续复用同一队列 Module,不再为每个入口发明独立队列。
- 外部生成队列粒度固定为“单个用户动作对应单个 job”。例如一次图片生成、改图、视频生成或角色动作生成各自入一个 job;job 内部可以串行或并行调用 provider、OSS 和 SpacetimeDB 写回,但不再拆成“提示词 / 生图 / 处理 / 持久化 / 回写”等阶段 job。用户可见执行阶段通过现有任务行及摘要投影的轻量 `phase` 保存,不作为队列调度单位,也不写回大 payload。
- 不调用外部图片 / 音频 / LLM provider 的动作继续 inline 执行,不为了统一排队而进入 `external_generation_job`。
## Module 与 Interface
@@ -45,19 +42,19 @@ External API job 复用同一个 `result_payload_json` 列,但只额外保存
不带 `summary / summaries` 的旧 `get / list / acknowledge_external_generation_job*` procedure 只保留给受控内部兼容,不是 BFF 正式读取入口。
这个 Module 的 **Seam** 在 SpacetimeDB procedure + `spacetime-client` facade;`api-server` HTTP role 和 worker role 都只依赖这个 Interface。外部 provider、OSS、计费补偿、玩法草稿回写仍留在 `api-server` worker implementation 内,不进入 SpacetimeDB reducer。
这个 Module 的 **Seam** 在 SpacetimeDB procedure + `spacetime-client` facade;`api-server` HTTP role 和 worker role 都只依赖这个 Interface。外部 provider、OSS、计费补偿和编辑器业务结果回写仍留在 `api-server` worker implementation 内,不进入 SpacetimeDB reducer。
## BFF 状态接口
队列状态对前端只通过 `api-server` BFF 暴露,不允许前端直接查询 SpacetimeDB private table:
- `GET /api/runtime/external-generation/queue-overview`:当前账号队列概览,用于兼容旧展示和轻量状态读取。返回 pending、running、未确认终态数量和更新时间。
- `GET /api/runtime/external-generation/queue-overview`:当前账号队列概览,用于轻量状态读取。返回 pending、running、未确认终态数量和更新时间。
- `GET /api/runtime/external-generation/jobs?limit=20&includeAcknowledgedTerminal=false`:当前账号正式生成任务列表,用于 `我的` 页签任务列表和完成 / 失败提示。返回每个任务的 job id、kind、source、可展示 label、状态、进度、错误、可选 `warning`、`priceMudPoints`、`refundLedgerId`、`notificationAcknowledgedAt` 和时间戳。默认不返回已确认的终态任务;需要拆分活跃和完成列表时可追加 `statuses=running,queued` 或 `statuses=completed,failed`,BFF 仍只返回当前账号任务。
- 任务被 claim 后默认处于 `generating`,BFF 显示“正在生成”;真实进入 BgFilter、逐帧抠图或手动去背景时切换为 `processing`,BFF 显示“正在处理”。旧任务 `phase=None` 按 `generating` 兼容,前端不得按耗时或 job kind 推断阶段。
- 任务被 claim 后默认处于 `generating`,BFF 显示“正在生成”;真实进入 BgFilter、逐帧抠图或手动去背景时切换为 `processing`,BFF 显示“正在处理”。`phase=None` 按 `generating` 兼容,前端不得按耗时或 job kind 推断阶段。
- `POST /api/runtime/external-generation/jobs/acknowledge`:生成完成 / 失败提示展示后由前端后台调用,BFF 只传当前账号 job ids,后端只确认属于当前账号且已终态的任务。
- `GET /api/runtime/external-generation/jobs/{jobId}`:单 job 状态,用于生成页轮询某次动作。返回 `operationId`(即任务 ID)、`status`、`phaseLabel`、`phaseDetail`、`progress`、`error`、`updatedAtMicros`,以及可选、可直接展示的 `warning` 完整文案。生成页轮询只依赖状态、阶段、进度、错误和警告;`jobKind`、source 和完整时间信息继续由任务列表接口或业务快照提供。`attempt` / `maxAttempts` 属于 worker 调度事实,不向该前端契约暴露;若未来需要面向用户展示,必须单独完成产品、契约和摘要投影设计。
BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、lease、执行和计费事实仍以 `external_generation_job` 为准,用户可见任务列表、单任务状态、执行阶段和通知确认的正式读取事实源为 `external_generation_job_summary`,业务结果仍以玩法 session / work profile 为准。生成页 / 进度页只展示当前玩法业务进度;用户可见任务列表放在 `我的` 页签,必要时再用单 job 状态补充排障信息,并继续按原玩法 session/detail 接口收敛到 ready 或 failed。队列接口不替代玩法恢复接口,也不把 private `request_payload_json` 原样传给前端。终态提示的弹出与否以后端 `notification_acknowledged_at` 为准;前端在提示展示后后台调用 acknowledge 接口,关闭按钮只负责收起本地弹窗,不能只靠本地 dismiss 永久吞掉任务。
BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、lease、执行和计费事实仍以 `external_generation_job` 为准,用户可见任务列表、单任务状态、执行阶段和通知确认的正式读取事实源为 `external_generation_job_summary`,编辑器业务结果以 `editor_project`、`editor_canvas`、`editor_project_resource` 与 `editor_asset` 为准。用户可见任务列表放在 `我的` 页签,单 job 状态用于补充轮询和排障信息,最终结果仍以编辑器项目快照收敛。队列接口不替代编辑器结果接口,也不把 private `request_payload_json` 原样传给前端。终态提示的弹出与否以后端 `notification_acknowledged_at` 为准;前端在提示展示后后台调用 acknowledge 接口,关闭按钮只负责收起本地弹窗,不能只靠本地 dismiss 永久吞掉任务。
## 任务表
@@ -66,11 +63,11 @@ BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、
| 字段 | 说明 |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `job_id` | 主键,`extgen-` 前缀 UUID |
| `dedupe_key` | 唯一键,建议为 `play/action/session/scope` |
| `job_kind` | 执行类型,当前覆盖 `puzzle_compile_draft`、`puzzle_generate_images`、`puzzle_generate_ui_background`、跳一跳 / 拼消消 / 敲木鱼生成动作,以及 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation`、`editor_background_music_generation` |
| `dedupe_key` | 唯一键,按 owner、job kind 与稳定业务键派生 |
| `job_kind` | 执行类型,当前覆盖 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation`、`editor_background_music_generation` |
| `owner_user_id` | 触发用户 |
| `source_module` | 玩法或能力名,例如 `puzzle` |
| `source_entity_id` | session/profile/work 等作用域 |
| `source_module` | 来源模块,当前为 `editor-canvas` |
| `source_entity_id` | 编辑器项目等稳定业务作用域 |
| `request_label` | 排障标签 |
| `request_payload_json` | worker 执行入参 JSON |
| `status` | `pending/running/completed/failed/cancelled` |
@@ -91,7 +88,7 @@ BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、
新增私有审计表 `external_generation_job_event`,记录 `enqueued/claimed/lease_renewed/completed/failed/acknowledged` 等事件。事件表只追加状态转换事实,不作为当前状态源;排障时先看 `external_generation_job` 当前状态,再按 `job_id` 追 `external_generation_job_event` 时间线。
另新增私有 `editor_generation_operation` durable commit receipt。它不与 `external_generation_job` 争抢任务状态:job 仍负责队列、lease、计费和通知,receipt 只固化某个 owner/kind/operation 的 request fingerprint、整笔 commit SHA-256、可选 project 以及 queue 的 job/worker/lease/result 绑定。inline 虽没有 job,也必须写 receipt;否则 API 进程重启后无法安全区分“完整提交”与“稳定 ID 巧合/历史部分记录”。
另新增私有 `editor_generation_operation` durable commit receipt。它不与 `external_generation_job` 争抢任务状态:job 仍负责队列、lease、计费和通知,receipt 只固化某个 owner/kind/operation 的 request fingerprint、整笔 commit SHA-256、可选 project 以及 queue 的 job/worker/lease/result 绑定。inline 虽没有 job,也必须写 receipt;否则 API 进程重启后无法安全区分“完整提交”与“稳定 ID 巧合或不完整记录”。
索引:
@@ -111,7 +108,7 @@ pending/running -> cancelled (预留)
`claim` 只领取 `pending` 且 `available_at <= now` 的任务,或 `running` 且 `lease_expires_at <= now` 的任务。领取时递增 `attempt`、写入 `worker_id`、`started_at`、新的 `lease_expires_at` 和 `lease_token`。SpacetimeDB procedure 使用 `ctx.timestamp` 作为状态流转时间,只从 worker 入参读取“时长差值”,不信任 worker 本机绝对时间。worker 每次执行只处理自己 claim 到的任务;续租、完成或失败时必须带同一个 `worker_id + lease_token`,且当前 lease 尚未过期,防止过期 worker 覆盖新 lease。
玩法业务写回也必须在 SpacetimeDB 同一事务里校验 lease fencing。拼图的 `compile_puzzle_agent_draft` worker 调用、`save_puzzle_generated_images`、`save_puzzle_ui_background`、`mark_puzzle_draft_generation_failed` 和 `mark_puzzle_level_generation_failed` 在 `queue` 模式下会带 `external_generation_job_id / worker_id / lease_token`,并校验 job 仍为 `running`、token 未过期、`job_kind`、`owner_user_id`、`source_module` 和 `source_entity_id` 均匹配后才写 session / work profile。`inline` 模式不创建 `external_generation_job`,因此这三个 guard 字段必须同时为空;transaction 只把三项全空识别为 api-server 受控同步写回,三项半空仍按非法请求拒绝。worker 路径的核心业务写回失败不能返回内存快照并把 job 标为 `completed`;失败态业务回写成功后才允许把队列 job 标为 `failed`,失败态仍未写回时保留当前租约并等待后续 lease 过期重领,避免队列状态和真实 session 脱节。api-server 的资产扣费包装遇到这类 stale worker lease guard 错误时不执行补偿退款,避免旧 worker 冲掉后续合法 worker 的同一账本扣费。
编辑器业务写回也必须在 SpacetimeDB 同一事务里校验 lease fencing。`queue` 模式下,`persist_editor_generation_result_and_return` 会携带 `external_generation_job_id / worker_id / lease_token`,并校验 job 仍为 `running`、token 未过期、`job_kind`、`owner_user_id`、`source_module` 和 `source_entity_id` 均匹配后才写正式结果。`inline` 模式不创建 `external_generation_job`,这三个 guard 字段必须同时为空;transaction 只把三项全空识别为 api-server 受控同步写回,三项半空仍按非法请求拒绝。worker 路径的结果写回失败不能返回内存快照并把 job 标为 `completed`;失败态业务回写成功后才允许把队列 job 标为 `failed`,失败态仍未写回时保留当前租约并等待后续 lease 过期重领。api-server 的资产扣费包装遇到 stale worker lease guard 错误时不执行补偿退款,避免过期 worker 冲掉后续合法 worker 的同一账本扣费。
## 执行模式与进程角色
@@ -155,51 +152,7 @@ controller 配置:
动态缩扩容方式:生产默认由 `deploy/systemd/genarrative-external-generation-controller.service` 启动 `GENARRATIVE_PROCESS_ROLE=external-generation-controller`,controller 读取 `get_external_generation_queue_stats_and_return` 后对 `genarrative-external-generation-worker@N.service` 执行精确 `systemctl start/stop`;无需改变 HTTP 进程数。controller 只操作 `@1..@MAX` 中的缺口或最高编号多余实例,保留 `@1` 作为保底 worker。缩容或发布重启 worker 时,进程收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务会在 lease 过期后被其它 worker 重新领取。VectorEngine 图片链路会先于整个 job 执行预算停止 provider 发送 / 重试,以便 worker 在有效 lease 内完成终态写回;若其他业务 future 仍长时间无返回,执行预算到期后 worker 会停止续租并释放槽位,在途 future 继续运行至租约仲裁窗口;有效租约内的写回仍可完成,租约过期后才会由其它 worker 重新认领,避免客户端取消与服务端写回竞态。本次预算收口不改变 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽收口和原子退款语义。容器链路已有独立 `external-generation-worker` compose service;扩 worker 必须扩这个 worker service,不能只扩 `api-server` HTTP service。
## 已接入的拼图纵切
### 拼图
`compile_puzzle_draft`:
1. HTTP handler 保存拼图表单草稿;`queue` 模式下 `queued/running` 的持久事实源是 `external_generation_job`,不把 HTTP 进程变成外部生成执行者。
2. `queue` 模式下 HTTP handler 入队 `puzzle_compile_draft`,返回 `operation.status = queued` 和当前 session。拼图 dedupe key 包含本次 `extgen-` job id,只保证同一任务行唯一,不把同一 session 后续重新生成吞掉。`inline` 模式下 HTTP handler 复用同一 executor 同步执行,成功后直接返回 `completed` 和最新 session。
3. 前端保持 `puzzle-generating`,继续轮询 `getPuzzleAgentSession`;首期不把 `queued/running` 写回 `puzzle_agent_session`,因此刷新或跨设备恢复生成中状态仍是后续 read model 工作。
4. worker claim 后执行原有 `compile_puzzle_draft_with_initial_cover` 或 `compile_puzzle_draft_with_uploaded_cover`;前置 `compile_puzzle_agent_draft` 也必须携带本次 `job_id / worker_id / lease_token`,防止过期 worker 先把草稿卡和 session 写到 ready。
5. 成功后沿原有 SpacetimeDB 拼图会话/作品写回,前端轮询看到 `progressPercent >= 94/96/100` 和 ready 草稿。
6. 失败后调用 `mark_puzzle_draft_generation_failed`,拼图首期业务失败直接进入 failed;只有失败态写回成功才把队列 job 标为 failed,失败态写回失败则保留租约等待重领。队列仍保留 lease 过期后的崩溃重领,避免 worker 退款后再次成功导致钱包账本漂移。前端通过现有失败草稿/弹窗机制展示来源错误。
`generate_puzzle_images`:
1. HTTP handler 校验本次 `levelsJson` 快照;`queue` 模式下入队 `puzzle_generate_images` 并返回 `operation.status = queued/running/completed/failed`,`inline` 模式下同步执行原 worker executor 并在成功后返回 `completed`。
2. worker 执行原结果页关卡图链路:自动命名、VectorEngine / 上传图直用、关卡场景图、UI spritesheet、关卡背景资产包、OSS 持久化和 SpacetimeDB 回写。
3. 成功后 `save_puzzle_generated_images` 写回目标关卡和草稿卡;失败后 `mark_puzzle_level_generation_failed` 只标记目标关卡 `failed`,不污染已 ready 的其它关卡。队列 job 只有在目标关卡失败态写回成功后才进入 failed。
4. 前端结果页对 `queued/running` 操作继续轮询 `getPuzzleAgentSession`,目标关卡变为 ready 或 failed 后收敛。
`generate_puzzle_ui_background`:
1. HTTP handler 校验本次 `levelsJson` 快照;`queue` 模式下入队 `puzzle_generate_ui_background` 并返回 `operation.status = queued/running/completed/failed`,`inline` 模式下同步执行原 worker executor 并在成功后返回 `completed`。
2. worker 执行原结果页 UI 背景链路:归一化提示词、VectorEngine 生成、OSS 持久化和 `save_puzzle_ui_background` 写回。
3. 成功后目标关卡写入 `uiBackgroundPrompt/uiBackgroundImageSrc/uiBackgroundImageObjectKey`;失败后复用 `mark_puzzle_level_generation_failed` 标记目标关卡 `failed`,并在失败态写回成功后才终结队列 job,让前端轮询能收敛。
### 跳一跳、拼消消和敲木鱼扩展范围
以下动作按同一 worker 模式迁移。命名以现有玩法 action 为准,队列 `job_kind` 采用后端稳定 snake_case,不新增平行队列:
- 跳一跳 `jump-hop`
- `compile-draft`:草稿编译阶段需要生成地块 / 视觉资产时入队,例如 `jump_hop_compile_draft`。
- `regenerate-tiles`:结果页地块图集重生入队,例如 `jump_hop_regenerate_tiles`。
- 拼消消 `puzzle-clear`
- `compile-draft`:草稿编译阶段需要生成场地底图和卡片 atlas 时入队,例如 `puzzle_clear_compile_draft`。
- `regenerate-atlas`:结果页素材 atlas 重生入队,例如 `puzzle_clear_regenerate_atlas`。
- 敲木鱼 `wooden-fish`
- `compile-draft`:草稿编译阶段需要生成背景、敲击物或其它图片资产时入队,例如 `wooden_fish_compile_draft`。
- `regenerate-hit-object`:结果页敲击物图片重生入队,例如 `wooden_fish_regenerate_hit_object`。
这些动作首版都保持“单动作单 job”:一次 `compile-draft` 或一次 `regenerate-*` 请求只创建一个 job,worker 内部负责该动作所需的 provider 调用、素材处理、OSS 持久化、失败态写回和业务成功写回。非外部图片生成动作,例如纯元信息保存、标签编辑、发布、试玩启动、运行态动作、删除和公开 read model 读取,继续 inline 执行。
每个玩法迁移时必须同时接入业务写回 lease guard:worker 路径带 `external_generation_job_id / worker_id / lease_token`,inline 路径三项同时为空。过期 worker 不得写 session / work profile;业务失败态写回成功后才允许 job 进入 `failed`。
### 图片画布编辑器
## 图片画布编辑器
图片画布 `/editor/canvas` 下所有会调用外部生成 provider 的入口在 `queue` 模式下入 `external_generation_job`,HTTP handler 只返回 `queueState`:
@@ -253,8 +206,6 @@ cargo test -p spacetime-module level_generation_failure --manifest-path server-r
cargo test -p api-server external_generation_worker --manifest-path server-rs/Cargo.toml
cargo test -p api-server external_editor_generation --manifest-path server-rs/Cargo.toml
cargo test -p api-server external_mcp --manifest-path server-rs/Cargo.toml
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx -t "keeps generation progress visible"
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "compile_puzzle_draft"
```
本地 smoke:
@@ -266,7 +217,7 @@ curl -f http://127.0.0.1:<api-port>/healthz
本地 `npm run dev` 与 `npm run dev:api-server` 默认注入 `GENARRATIVE_PROCESS_ROLE=all`,同一 Rust 进程同时监听 HTTP 并消费外部生成队列;显式设置 `GENARRATIVE_PROCESS_ROLE` 时保留显式值。需要验证生产式拆分角色、lease 重领或扩缩容时,再分别启动 `api`、`external-generation-worker` 和 `external-generation-controller`,也可以使用隔离容器 smoke。
生产 smoke 需要保持 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue`,并至少启动一个 `api` 角色、一个 `external-generation-worker` 角色和一个 `external-generation-controller` 角色;发布脚本会在默认 worker pattern 下自动启用并启动 `genarrative-external-generation-worker@1.service`,重启并验活 `genarrative-external-generation-controller.service`。`genarrative-api.service` 还通过 systemd `Wants=genarrative-external-generation-controller.service` 弱依赖覆盖只启动 API 的现场兜底;controller 仍是独立进程,不由 HTTP 进程内执行 `systemctl`。若 worker 数量归零,生成任务会保持 `queued/running`,不会由 HTTP 进程偷偷执行。部署验证除 `/healthz` / `/readyz` 外,还要确认任务列表 BFF 可读、未确认终态任务会弹出提示、提示展示后后台 acknowledge 且刷新后不再弹出,单 job 状态能从 `queued/running` 收敛到业务 session/detail 的 ready 或 failed。External smoke 还必须证明生成 POST 返回 `202`、同幂等键不重复创建任务、统一查询能读到 compact completed result、跨 owner 返回 `404`,并通过托管 MCP 调用同一提交/查询工具链。
生产 smoke 需要保持 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue`,并至少启动一个 `api` 角色、一个 `external-generation-worker` 角色和一个 `external-generation-controller` 角色;发布脚本会在默认 worker pattern 下自动启用并启动 `genarrative-external-generation-worker@1.service`,重启并验活 `genarrative-external-generation-controller.service`。`genarrative-api.service` 还通过 systemd `Wants=genarrative-external-generation-controller.service` 弱依赖覆盖只启动 API 的现场兜底;controller 仍是独立进程,不由 HTTP 进程内执行 `systemctl`。若 worker 数量归零,生成任务会保持 `queued/running`,不会由 HTTP 进程偷偷执行。部署验证除 `/healthz` / `/readyz` 外,还要确认任务列表 BFF 可读、未确认终态任务会弹出提示、提示展示后后台 acknowledge 且刷新后不再弹出,单 job 状态能从 `queued/running` 收敛到编辑器项目快照中的 completed 或 failed。External smoke 还必须证明生成 POST 返回 `202`、同幂等键不重复创建任务、统一查询能读到 compact completed result、跨 owner 返回 `404`,并通过托管 MCP 调用同一提交/查询工具链。
systemd 生产 controller 与手动兜底示例:
@@ -77,6 +77,7 @@ EditorGenerationResultPersistInput {
## api-server 接入
- 旧分段持久化、worker 独立完成及其重复结果序列化实现,在失去现役调用方后直接删除;不保留只供旧测试调用的生产副本。废弃实现的专属测试随实现删除,不因清理而将旧用例改接到现役函数;现有现役行为测试保持原有覆盖。手动拆分、上传等现役路径使用的底层 helper 继续保留,HTTP / DTO、inline 模式和历史任务解析兼容不受清理影响。
- 通用持久化改为 `prepare -> build canvas candidate -> atomic commit`。prepare 阶段只生成稳定 ID、上传/验证对象和构造候选 DTO,不创建 resource/asset。
- api-server 继续复用现有画布 completion / replacement 逻辑计算候选 `layers_json` 和 `expected_revision`;统一 procedure 在最终事务内重新执行既有 layout 校验和 CAS。
- CAS 冲突只刷新当前 project、重新计算 layout 并重试 prepared commit;相同 operation、slot、对象和记录候选保持不变,禁止重跑 Provider。
@@ -1,6 +1,6 @@
# AGC 客户端更新检查与下载
更新时间:`2026-09-21`
更新时间:`2026-09-22`
本文件是 AGC 客户端自动更新的主规范:更新能力由 Tauri 官方插件 `tauri-plugin-updater` 承担,并按下文渠道分发。
@@ -27,6 +27,7 @@
- 数据不跨渠道共享:本地项目、工程快照、模板、登录态、诊断日志与 Runner/项目锁按渠道身份分目录。切渠道等于换一个客户端,不迁移、不合并本地数据;渠道内的 origin 隔离规则不变。
- 主窗口与工作区/启动器窗口标题取构建期产品名,让同机并存的渠道客户端在任务栏与 Alt-Tab 中可区分;默认渠道标题仍是「陶泥儿」。
- 首装包与更新包的对象名包含产品名(如 `陶泥儿 Release_0.1.96_x64-setup.exe`、`陶泥儿 Release_0.1.96_aarch64.dmg`)。清单 `downloads` 地址由发布脚本按本次真实产物派生,禁止写死产品名;首装包选择按 `<版本>_<架构>.dmg` 唯一匹配,不依赖产品名字面量。
- 产品名中的空格在清单 URL 中必须使用标准百分号编码(如 `%20`);网站服务端允许这种合法文件名编码,但仍拒绝控制字符、路径分隔符、DEL 和非法百分号编码,不得通过改写真实产物名绕过校验。
- Windows 提权 ACL 修复助手按目录名识别安装身份:`<基线>` 与 `<基线>.<渠道>` 都在 AGC 自有的 managed 范围内;相似前缀(例如 `world.genarrative.ai-game-creator-backup`)不在范围内,落回 user-selected 范围或直接拒绝。
## 非目标
@@ -136,9 +137,9 @@
- 发布入口只解析一次目标,优先级为 CLI `--target value` / `--target=value` / `-t value`、`AGC_BUILD_TARGET`、Windows 默认值;重复/空目标与不支持目标失败关闭。版本高水位、构建 feature/渠道端点、bundle 路径、产物后缀、清单平台键及摘要必须消费同一个发布上下文,不能分别回读默认目标。
- 渠道由 `AGC_UPDATE_CHANNEL` 显式指定,默认 dev;Windows 与 macOS 目标均支持 dev、release 和自定义渠道,目标校验独立进行。
- 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier` 与 updater 端点:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。
- 定时调度只在本轮到达的提交包含 AGC 相关路径(客户端、共享包、`server-rs/crates`、AGC 插件、桌面壳图标、根依赖清单)时才触发渠道发布;纯文档或流水线自身的提交只跑 Full Build,不推高客户端版本号。判定失败或勾选强制触发时按"需要发布"处理。
- 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题(短 SHA)`,最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。锚点仍不可得(清单读取失败或没有 CI 锚点)时降级为「最近客户端改动」列表并注明可能与上一版重复 —— 摘要属于附注,任何情况下都不允许因为它让发布失败。
- 清单里的 `commit` 是非标准字段:更新插件忽略未知字段,发布脚本用它定位下一次摘要的起点。
- 定时调度分别判断服务端与客户端 scope:dev 小时调度在提交含 AGC 相关路径时发布对应渠道,纯文档或流水线自身的提交仍只跑 Full Build;release 每日调度在服务端相关路径变化时发布正式 Full Build,在 AGC 相关路径变化时发布 release 客户端,并在同一调度内等待、汇总各 lane 结果,失败 lane 下一轮补发。判定失败或勾选强制触发时按"需要发布"处理。
- 更新摘要不再自动生成:发布脚本不读取提交记录生成 `notes`;只有 `AGC_UPDATE_RELEASE_NOTES` 非空时,才把显式手动文案写入渠道清单和旧协议清单的 `releaseNotes`。未设置时清单不携带更新说明,归档文件 `release-notes.txt` 记录“本次没有可用的更新摘要”。
- 清单里的 `commit` 是非标准字段:更新插件忽略未知字段;发布脚本只为线上排障保留源码 revision,不驱动更新摘要。
- 上传:安装包与 `.sig` 上传到 `agc/<channel>-win|mac/<version>/`,清单以 `--force` 覆盖上传到对应分区的 `latest.json`,保证 latest 指针与清单内 URL 指向已存在的对象。
- 首装发布:发布脚本生成 `downloads`,Windows 复用已选 NSIS `.exe`,Mac 选择本次版本和目标架构匹配的非空 `.dmg`;缺失、歧义或版本/架构不匹配时失败,不发布带悬空地址的清单。上传顺序为更新包、签名及首装包全部成功后再更新渠道清单,Windows 相同对象只上传一次。`dry-run` 不写 OSS。各渠道独立写自己的清单,由 BFF 汇总,Windows 与 Mac 发布不会覆盖彼此的下载项;Mac 跨架构合并仍遵循现有单架构发布约束。
- Jenkins 流水线需要新增渠道参数与签名凭据;签名私钥与密码只以受保护凭据注入当前进程,不写入 workspace、日志或归档产物。
@@ -176,7 +177,7 @@
| 安装包与清单登记一致 | 下载安装包实算 SHA-256 与尺寸后与迁移桥清单比对 | 通过(size `104678031`、sha256 `1f67…4fd0` 一致) |
| 旧协议迁移桥 | 公网读取 `agc/latest.json` | 通过(0.1.48,`downloadUrl` 指向同一对象,含 `sha256` / `size`) |
| 真实更新闭环(含升级后重启) | 0.1.47 客户端按提示下载安装并重启 | 通过(2026-09-17 用户实测:提示 → 下载 → 安装 → 关于页显示新版本,再次检查为已是最新) |
| 更新摘要端到端展示 | 公网读取渠道清单 `notes` 与客户端更新提示 | 通过(2026-09-17 用户实测:0.1.62 清单带 8 条自动摘要,客户端提示正常显示多行内容) |
| 更新摘要按显式配置写入 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过(未设置 `AGC_UPDATE_RELEASE_NOTES` 时渠道清单不写入 `notes`) |
渠道安装身份隔离已于 `2026-09-21` 完成源码验收:
@@ -1,6 +1,6 @@
# AGC 总版本号与发号
更新时间:`2026-09-20`
更新时间:`2026-09-22`
## 背景
@@ -18,7 +18,7 @@ AGC 客户端此前按「渠道各自比高水位自增」发号:`dev-win` 与
- `next = 总号 + 1`;总号不存在时先一次性播种,见下节。
- 顺序固定为「先写总号 → 再构建 → 再发渠道清单」;任何一步失败都不回滚,只烧号。
- 统一构建:发一次号,通过 `AGC_RELEASE_VERSION` 同时传给 `dev-win`、`dev-mac`(以及后续渠道),各渠道共用同一个号。
- 统一构建:发一次号,通过 `AGC_RELEASE_VERSION` 同时传给同一渠道组的两个平台;dev 调度写 `dev-win` / `dev-mac`,每日 release 调度写 `release-win` / `release-mac`,组内共用同一个号。
- 单渠道热修:发一次号,只传给该渠道;其他渠道清单保持原值。
- 显式传入 `AGC_RELEASE_VERSION` 的构建不再自行发号,只做高水位断言。
@@ -39,8 +39,8 @@ AGC 客户端此前按「渠道各自比高水位自增」发号:`dev-win` 与
- 发号模块:`apps/ai-game-creator-shell/scripts/agc-global-version.mjs`(读总号 / 播种 / 发号 / 写后回读 / 渠道高水位断言 / dry-run 预览)。
- 发号入口:`apps/ai-game-creator-shell/scripts/issue-global-version.mjs`(CI 与本地共用,输出固定为 `AGC_GLOBAL_VERSION=<version>`)。
- 构建侧:`build-release.mjs` 的 `prepareReleaseVersion()` 优先采用传入总号,未传入时现场发号;`resolveRemoteHighWaterVersion()` 降级为断言来源,只用于「请求号低于本渠道清单版本即失败关闭」。
- CI:`jenkins/Jenkinsfile.agc-global-version-issue` + `jenkins/agc-global-version-issue-job-config.xml`;调度管线 `jenkins/Jenkinsfile.scheduled-revision-trigger` 与手动管线先调发号 Job,再把号透传给 AGC Build。
- 发号 Job 的 Copy Artifact 采用生产权限模式,`options` 里的 `copyArtifactPermission(...)` 必须显式列出**全部**消费者:定时调度 `Genarrative-Scheduled-Revision-Trigger` 与手动发布 `Genarrative-Manual-Build-And-Deploy`。用户触发的构建按「认证用户」判权(SYSTEM 定时构建短路放行),漏列时手动发布会报 `Unable to find project for artifact copy: Genarrative-Agc-Global-Version-Issue`,且失败点在发号之后。改完该 `options` 后必须先单独跑一次发号 Job,让 Declarative Pipeline 把 Job property 写回 Jenkins。
- CI:`jenkins/Jenkinsfile.agc-global-version-issue` + `jenkins/agc-global-version-issue-job-config.xml`;dev 调度管线 `jenkins/Jenkinsfile.scheduled-revision-trigger`、每日 release 调度管线 `jenkins/Jenkinsfile.scheduled-release-trigger` 与手动管线先调发号 Job,再把号透传给 AGC Build;release 调度的服务端 Full Build 不使用 AGC 总号,只使用固定 `COMMIT_HASH` 与 Full Build 自身版本。
- 发号 Job 的 Copy Artifact 采用生产权限模式,`options` 里的 `copyArtifactPermission(...)` 必须显式列出**全部**消费者:小时 dev 调度 `Genarrative-Scheduled-Revision-Trigger`、每日 release 调度 `Genarrative-Scheduled-Release-Trigger` 与手动发布 `Genarrative-Manual-Build-And-Deploy`。用户触发的构建按「认证用户」判权(SYSTEM 定时构建短路放行),漏列时手动发布会报 `Unable to find project for artifact copy: Genarrative-Agc-Global-Version-Issue`,且失败点在发号之后。改完该 `options` 后必须先单独跑一次发号 Job,让 Declarative Pipeline 把 Job property 写回 Jenkins。
## 不改的东西
@@ -116,7 +116,10 @@ templates/
- `src/features/template-library/templateLibraryModel.ts`:清单类型、搜索(空白分隔多关键词「与」)、标签/运行时/已下载筛选、标签选项聚合、体积格式化等纯函数。
- `src/features/template-library/useTemplateLibrary.ts`:一次拉清单,暴露筛选状态、下载与「用模板建项目」;下载成功后只就地更新该条目的已下载状态。
- `src/view/template-library/index.tsx`:模板库全屏页(返回、刷新、搜索、运行时/标签筛选、仅看已下载、卡片显示封面与已下载徽标、下载/使用模板)。
- 筛选区拆两行:第一行是共享筛选条 `PlatformResourceFilterBar`(搜索框 + 运行时分段)与靠右的「仅看已下载 / 清除筛选」,第二行是标签 chip(带命中数量,换行排布、`max-h-[20vh]` 上限内滚动)。
- 筛选控件的两态口径(模板库、资源画布、参考图弹窗共用):**未选 = 浅底描边 + 常规文字,选中 = 实心品牌填充 + 反白文字**,两态填充对比 ≥ 3:1、标签文字在选中填充上 ≥ 4.5:1(由 `tests/workbenchThemeContrast.test.ts` 钉住)。运行时分段用 `PlatformSegmentedTabs` 的 `tone="accent"`,与标签 chip 共用同一组 `--platform-chip-active-*` 语义色,不再出现「选中只换了一点点暖色」的弱状态。
- 卡片动作按安装状态收口:已下载且版本一致时**不再显示下载入口**,只留「使用模板」;版本落后才显示「更新」;缺包显示「下载」。
- 忙状态写在**触发它的那个按钮**上(下载中 / 创建中,按钮就地换图标与文案),动作行里不额外塞状态文本:最小卡宽(250px)下「使用模板 + 下载/更新 + 状态文本」三个元素会把按钮文案挤成两行并顶出卡片。
- 过程提示(下载完成、开始建项目)走浮层 toast(复用 `packages/shared` 的 `PlatformRuntimeStatusToast`,`document.body` 浮层 + 2.6 秒自动消失),不再占用页面内位置;页面内只保留可操作的错误与空态。
- 首页「灵感推荐」替换为「模板库」推荐位(`src/view/home/TemplateRecommendations.tsx`):只展示封面、标题、运行时与已下载徽标,点击进入模板库页面;首页不再直接触发建项目。
- 左侧导航新增模板库入口(`LauncherView = 'template-library'`)。
@@ -164,14 +167,15 @@ AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT=300 AGC_DEV_CARGO_FEATURES=template-library
### 卡片列表虚拟滚动(react-window)
- 列表改用 workspace 里已有的 `react-window@1.8.11` 的 `FixedSizeGrid`(`react-arborist` 已在用同一版本,不引入新包;类型来自 devDependency `@types/react-window`)。
- 布局契约收在纯函数 `templateLibraryGrid.ts`(单测覆盖):列数 = `floor((容器宽 + gap) / (最小卡宽 + gap))`、列宽 = 容器宽 / 列数、行高 = `卡片宽 × 9/16 + 文字区 150 + gap`;`buildTemplateRows` 按行切分并在行尾补 `null` 占位。
- 布局契约收在纯函数 `templateLibraryGrid.ts`(单测覆盖):列数 = `floor((容器宽 + gap) / (最小卡宽 + gap))`、列宽 = 容器宽 / 列数、行高 = `卡片宽 × 9/16 + 文字区 174 + gap`;`buildTemplateRows` 按行切分并在行尾补 `null` 占位。
- **卡片文字区行高契约**:文字区 174 = 内边距 12×2 + 标题 20(`h-5`)+ 元信息 16(`h-4`)+ 简介 32(`h-8`,两行)+ 标签 22(`h-5.5`)+ 按钮行 28(`h-7`)+ 行距 8×4;这些分项在 `templateLibraryGrid.ts` 里各有一个常量,`TemplateCard` 用同一组固定高度 + `shrink-0` 渲染。**不要**再把文字区写成 `grid` 的 auto 行:auto 行按 max-content 计高,多行文字只按一行算,标题 / 简介 / 标签会被逐行裁掉。
- 卡片抽成 `TemplateCard`(`memo`),网格只渲染可视行 + 2 行 overscan;筛选条件(关键词/标签/运行时/仅看已下载)变化时把滚动位置复位到顶部,避免"从筛选切回全量后停在空白处"。
- **页面高度契约**:页面根节点的高度按**父级 `.launcher-main` 的实测高度**内联设置,既不用百分比也不用 `100vh`。原因:外壳样式 `.launcher-main > .platform-theme { height: 100% }` 特异性高于 Tailwind 工具类,而这条百分比在 `.launcher-shell { min-height: 100vh }` 链路上是不定高,页面会退化成内容高度(虚拟网格视口高度 0、卡片区整片空白);`100vh` 又比真实舞台高一个标题栏高度(窗口 100vh=800 / 舞台 750),底部会被裁掉。
- 筛选区(运行时/标签)改成可独立滚动的区块(`max-h-[24vh]`),标签数量随库量增长时不再把卡片区挤出窗口。
- 筛选区(运行时/标签)在窗口变窄时整体换行,标签行单独限高(`max-h-[20vh]` 内滚动),标签数量随库量增长时不再把卡片区挤出窗口。
- 回归:`templateLibraryGrid.test.ts` 覆盖列数/行高/行数/切行;页面测试用固定视口断言「1000 条只渲染 ≤ 40 张卡片,滚动高度仍按 250 行计算」。
- 页面能正常渲染 1000 张卡片(头部显示「共 1000 个模板 · 已下载 335 个」),并且滚动容器生效(窗口高度压到 430px 时右侧出现滚动条,页面内容被裁切而不是溢出到窗口外)。
- 需要后续收口的两点(本次未改):① 标签筛选条随库量膨胀——1000 条时聚合出 35 个标签、占三行;② 一次性渲染 1000 个卡片节点并触发 1000 次封面请求。建议标签只展示 Top N + 「更多」,卡片列表加分页或虚拟滚动。
- 仍需关注:① 标签筛选条随库量膨胀——1000 条时聚合出 35 个标签;现为「换行 + `max-h-[20vh]` 滚动」兜底,量大时仍建议只展示 Top N + 「更多」;② 一次性渲染 1000 个卡片节点并触发 1000 次封面请求,建议加封面懒加载上限或分页。
- 前端回归:1000 条渲染 + 已安装过滤(334)/标签过滤(50)/关键词过滤数量自洽,见 `apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx`。
```bash

Some files were not shown because too many files have changed in this diff Show More