Feat/adapt taonier #661

Merged
k88936 merged 97 commits from feat/adapt-taonier-skill into master 2026-10-07 15:45:55 +08:00
Member

注意到现在发布游戏 封面那块用的是画布里的assetid来引用图片,,,,独立出来用oss key

注意到现在发布游戏 封面那块用的是画布里的assetid来引用图片,,,,独立出来用oss key
k88936 added 10 commits 2026-10-07 10:21:25 +08:00
- AGC 实施计划追加「2026-10-06 陶泥儿导出文件草稿化与游戏分发媒体直传」章节
- 玩法链路新增「游戏分发发布媒体直传合同(2026-10-06)」
- 后端数据契约补充 game_distribution_game 的媒体直传与 schema 变更约束
- decision-log 记录文件草稿、build:taonier/dist-taonier、快照桶+新读路由、硬切、exports 全退役、AI 建议/封面退役
- 新增里程碑规范与实施计划两份 plans 文档
- 新增 api-server::object_read:签名读地址、过期收敛、同源字节中转与错误映射
- assets.rs 的 read-url/read-bytes 改为复用该模块,授权判定仍留在素材域
- 移除 assets.rs 内重复的常量、作用域枚举与字节中转实现,行为不变
- 过期收敛单测随实现迁到 object_read
- 契约:创建/编辑/建版请求去掉 `coverAssetId`,改为 `coverObjectKey` 与 `screenshots: (string|null)[]`,Rust 与 TS 同步。
- schema:`game_distribution_game` 删除 `cover_asset_id`;创建输入末尾追加 `cover_object_key`/`screenshots_json`;冻结资料截图改为 objectKey 数组并删除 `GameDistributionFrozenScreenshot`;同步迁移登记与生成绑定。
- 授权:删除按 assetId 的公开读授权,新增按 objectKey 的 `get_game_distribution_media_read_access` procedure;公开可见判定抽成 `game_distribution_game_is_publicly_visible`。
- api-server:三条写路径改收 `multipart/form-data`(`metadata` 文本 + `cover` + 重复 `screenshot`),新图直写项目快照桶、沿用图校验归属与数量;新增匿名 `GET /api/game-distribution/media/read-url` 与 `read-bytes`;幂等摘要覆盖图片字节。
- web:发布/编辑客户端改 FormData;封面与截图读取切到新读路由;移除发布前素材登记与 assetId 状态。
- AGC:发布命令先按 objectKey 做存在性校验以保持编译,multipart 直传改造留待 M3。
- 文档:实施计划与玩法链路合同章节同步新的 multipart 字段口径。
- 12 个 check-game-distribution-* 脚本改用 multipart:metadata + cover/screenshot 二进制 part
- 删除 assetId/assetObjectId 断言与素材库直传前置,公开读改走 media/read-url 与 read-bytes
- media-e2e 负例重写:缺封面、超 6 张、非本作品 objectKey、槽位与 part 数量不匹配
- ratings-e2e 直连 reducer 字段改 cover_object_key/screenshots_json,test-creator-runtime 同步
- .gitattributes 为 export/generated 生成目录豁免行尾空白检查
- 新增 vite-export-taonier skill:vite.config.taonier.mjs 构建到 dist-taonier,pack.mjs 产出存档根含 index.html 的 .export/taonier.zip
- skill pack 锁步登记:EXPECTED_SKILL_NAMES、AGC_SKILL_PACK_FILES、manifest.json sha 与版本、codex mock 名单
- 新增 export/draft/taonier 文件草稿模块:build:taonier 脚本发现、.export/taonier.json 注册表、四个 tauri 命令与产物读取入口
- 发布暂存改读固定 .export/taonier.zip,删除 phaser/vite 发布门与 exports 打包读取链路
- 删除 exports 初始化目录、exports/README.md 生成与渲染、playtest-package 打包/读取/列举命令
- 生成契约与 prompts 去掉 publish-package artifact 声明,run-trace 白名单与五处排除列表同步
- 同步 ts-rs 生成绑定与 export/generated 行尾空白豁免
- 新增 export/state/taonier 状态层:字段校验、四命令失败载体、失败分类与 adapt/repair/fill 指令、600ms 自动保存与 2s 轮询
- 新增 tabs/taonier 面板:ArtifactsPane/FormCard/OnlineValuesCard/ConflictCard 与三个对话框,线上值并排显示与沿用动作
- 发布媒体改直传:草稿本地图读字节发 cover/screenshot part,空路径沿用线上 objectKey,幂等摘要纳入图片字节
- 退役 AI 资料建议/封面生成:删前端调用、Rust 命令与注册、api-server 路由与专属 DTO
- 删除旧 PublishPane、GameDistributionPublishFormView、GamePublishBlockedDialog、useGameDistributionPublishForm
- 新增 tests/taonierFields、taonierContract、taonierExport,check-config 与 dto parity 同步
- vitest.config.ts 增加 setupFiles,在 Node 26 + jsdom 未注入 localStorage 时补同形内存实现
- 新增 tests/jsdomLocalStorageSetup.ts,供所有 jsdom 用例共用
- gameDistributionPublish.test.ts 去掉局部 polyfill,改用共享 setup
- 删除平台素材直传代跑与 coverAssetId 种子,创建游戏/版本改 FormData(metadata + cover part)
- 断言改为创建响应与 frozenMetadata 的 coverObjectKey/screenshots
- 保留分片续传、复用 gameId、重复版本与版本回退等主链证据
- build.rs 保留 512 MiB 读取安全线,另按 shared-contracts 的 GAME_DISTRIBUTION_MAX_PACKAGE_BYTES(200 MiB)做预检
- 构建收尾与发布读包两处都预检,超限给「精简资源后重新导出再发布」,不再等上传吃服务端 413
- 新增平台上限单测,并锁定读取安全线必须高于平台上限
回写陶泥儿导出与发布媒体直传文档并删除临时计划
Project CI / AI game creator shell Rust crates (pull_request) Failing after 57s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m27s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m30s
Project CI / Native shell tests (pull_request) Successful in 6m1s
5e8ecaa35b
- 技术方案/后端契约/玩法链路三份主规范改成当前状态:文件草稿、build:taonier、multipart objectKey 直传与域内公开读
- decision-log 2026-10-06 条目由计划态改写为落地态,含命名约定、硬切、E1、taptap skill-pack 例外
- pitfalls 新增 multipart 幂等摘要、发布账本、skill-pack 红与 zip 存档根入口四条
- 删除临时实施计划与里程碑规范,历史由 Git 保存
k88936 marked the pull request as work in progress 2026-10-07 10:21:30 +08:00
Author
Member
  • 1. [maintainability·low] apps/ai-game-creator-shell/tests/jsdomLocalStorageSetup.ts:17-34 — 内存 Storage 替身重复三份

    • 现状:jsdomLocalStorageSetup.ts、appSurface/harness.ts、chatPromptPolish.test.tsx 各有一份逻辑几乎一致的内存 Storage。
    • 问题:length/key/removeItem 语义以后要改就得改三处,容易漂。
    • 修复:抽出 tests/memoryStorage.ts:createMemoryStorage(),三处统一 import(99d5fd1ea)。
  • 2. [bug·high] apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:448-450 — 图片字节直接拼接算摘要会碰撞

    • 现状:metadata_digest 只 hasher.update(&image.bytes),不带长度或分隔符。
    • 问题:metadata 只记槽位数量、不记字节边界,封面[1,2]+截图[3] 与 封面[1]+截图[2,3] 得到同一摘要;摘要喂给根幂等键,会把改过的媒体当成重放静默丢弃。
    • 修复:逐张先写 u64::to_le_bytes() 长度再写字节,并补分片不变性断言(3832ca4fa)。
  • 3. [performance·medium] game_distribution_publish.rs:1036-1037 — 读草稿图片没有体积上限

    • 现状:read_draft_image 直接 std::fs::read,只在服务端 8MB 处事后拒绝。
    • 问题:草稿指向超大本地文件时,会先把宿主内存读满再失败。
    • 修复:先 std::fs::metadata().len(),超过 8MB(对齐服务端单图上限)直接返回错误;补稀疏大文件测试(3ecd4efcb)。
  • 4. [maintainability·low] game_distribution_publish.rs:1124 — multipart 文件名一律 .png

    • 现状:multipart_image_part 固定叫 cover.png / screenshot-{i}.png,但内容类型可能是 JPEG/WebP/GIF/AVIF。
    • 问题:服务端目前按 MIME part 判定所以能跑,但文件名在说谎,将来有消费者信文件名就会错分类。
    • 修复:新增 multipart_image_file_name,按 content_type 给扩展名(3d6795978)。
  • 5. [maintainability·low] game_distribution_publish.rs:1148 — _package_relative_path 已是死参数

    • 现状:发布包固定读 .export/taonier.zip,命令签名仍要求前端传 package_relative_path,helper 里也保留形参。
    • 问题:名字看起来能控制包路径,实际被忽略,误导后来的调用者。
    • 修复:用户确认清理。stage_publish_package / publish_local_project_game 删除 package_relative_path 形参,前端 publishGameDistribution 与 useTaonierTab 同步不再传,命令契约更新(aa946a930)。
  • 6. [bug·low] apps/ai-game-creator-shell/src/services/gameDistributionPublish.ts:179-180 — 空串截图槽位被 filter 掉会错位

    • 现状:normalizeMetadata 先 trim 再 filter(s => s !== '')。
    • 问题:契约是「槽位与最终截图顺序一一对应」,丢槽位会让后面的截图整体前移;Rust 侧把空串当本地图槽位。
    • 修复:map 里把 null/空串统一归一成 null、不再 filter,测试同步改为按序断言(ab9789d11)。
  • 7. [style·low] .../tabs/taonier/ArtifactsPane.tsx:124-128 — publishLabel 嵌套三元

    • 修复:改成 if/else 三态赋值,行为不变(30b129fcf)。
  • 8. [style·low] ArtifactsPane.tsx:121-122 — online?.nextVersionNumber == null 宽松相等

    • 修复:改为 !online || online.nextVersionNumber === null(229f0580f)。
  • 9. [style·low] .../tabs/taonier/OnlineValuesCard.tsx:79-85 — 模板字符串里嵌三元

    • 修复:先在 JSX 外算 onlineSummary(含版本号后缀),再单点插入(4d72a77ae)。
  • 10. [maintainability·low] .../tabs/taonier/ConflictCard.tsx:65 — as never 关掉类型检查

    • 现状:解冲突时 resolved[meta.field] = conflict.current[meta.field] as never。
    • 问题:联合键赋值被强行压制,将来表单加非 string/string[] 字段会静默通过。
    • 修复:按字段类型分开赋值——screenshotPaths 走数组分支、其余走字符串分支(500c3fda6)。
  • 11. [bug·medium] .../export/draft/taonier/build.rs:185 — 结论阶段无体积门禁就整包读入

    • 现状:let bytes = std::fs::read(&path).ok(); 在 produced 判断之前执行。
    • 问题:构建脚本写出超大产物时先把内存读满;符号链接目标也会被完整读入。
    • 修复:只在 produced 为真时读,且先过 metadata.len() <= ARTIFACT_PACKAGE_READ_MAX_BYTES,超线直接判不合格(和 32 同一提交 d3d7e5a8c)。
  • 12. [performance·low] build.rs:251 — read_package_bytes 用 Vec::new()

    • 现状:调用方已经知道 metadata.len(),函数却从空 Vec 反复扩容。
    • 问题:接近 200MiB 的包会多复制约一倍数据。
    • 修复:函数接收调用方已验证的 expected_bytes,改用 Vec::with_capacity(4acb0e712)。
  • 13. [style·low] .../export/state/useTaonierTab.ts:179 — == null

    • 修复:改 online.nextVersionNumber === null;online 为空已由 !online 挡住(7b521f6f2)。
  • 14. [bug·medium] useTaonierTab.ts:177-178 — 先快照 form 再 await packageAll()

    • 现状:publishGame 用进入时的 form 快照拼 buildTaonierPublishMetadata,但 packageAll() 开头会 refresh(),可能在用户无未保存编辑时采纳更新的注册表表单。
    • 问题:Rust 发布命令读的是磁盘上的 .export/taonier.json 媒体路径,文本字段却来自旧快照,理论上会用过时标题/简介发布。
    • 修复:用户确认做,并核对了「构建/自动保存/是否所有字段都该走 UI」的疑问。useTaonierExport 新增 getEffectiveForm(),packageAll() 落盘成功后记录本次落定的权威表单;publishGame 在 await packageAll() 之后用 getEffectiveForm() ?? form 合成 metadata,补两条测试(27 passed)。核实结论:packageAll 本就会触发 vite 构建与自动落盘,.export/taonier.json 是 UI 自己写的持久化副本、不是构建/skill 写的;XHS 导出有同样的构建/自动保存结构但没有发布到广场的路径,因此没有这个陈旧表单问题,其表单也只有 name/introduction/iconPath。用户「所有字段都改走 UI、不读磁盘文件」的建议只对一半:去掉磁盘读取既不能消除陈旧,又会丢掉 Rust 侧「UI 槽位数 vs 草稿本地图数」的 fail-closed 校验,最小正确修复即本提交(ab2af60d3)。
  • 15. [bug·medium] useTaonierTab.ts:194-195 — packageAll reject 时进度卡在 running

    • 现状:refresh/persist/buildOnce 会把 exportUnavailable 与未识别失败经 throwToErrorPool 抛出,这里的 await 没包 try/catch。
    • 问题:publishGame reject,progress 停在 phase:'running';运行中对话框没有关闭入口,面板一直卡在「正在打包发行包…」。
    • 修复:await 包 try/catch,失败把进度置 failed 并带原因(8d74fa9ab)。
  • 16. [maintainability·low] .../export/state/taonierDownload.ts:30 — 与 xhsMinitoolDownload.ts 近乎逐字重复

    • 现状:保存/下载流程整文件复制了一份。
    • 问题:下载行为(文件名、取消语义、错误文案)以后要改两处。
    • 修复:用户确认抽共享 util。新增 export/state/exportArtifactDownload.ts 承载保存对话框 + save_local_project_asset_file + 文件管理器定位,删除 taonierDownload.ts / xhsMinitoolDownload.ts,两个 ArtifactsPane 改用 saveExportArtifactToDisk / revealExportArtifactInManager(680503d2a)。
  • 17. [bug·medium] ArtifactsPane.tsx:118-121 — 发布按钮没把 taonier.running 算进禁用

    • 现状:禁用条件只有 publish.publishing,taonier.running 未纳入。
    • 问题:先点「打包」、构建进行中再点「打包并发布」,两端会并发触发 packageAll,争抢同一个 .export/taonier.zip。
    • 修复:publishDisabled 增加 taonier.running(7db1fc4ff)。
  • 18. [style·low] .../tabs/taonier/FormCard.tsx:137-138 — 字段控件嵌套三元

    • 修复:options/multiline/input 三态先算 inputSlot,并复用同一份 fieldClassName,去掉三份重复样式(e4d46609c)。
  • 19. [style·low] FormCard.tsx:215-219 — 截图数量文案嵌套三元

    • 修复:三态先算 screenshotSummary(bb7097ce5)。
  • 20. [bug·medium] FormCard.tsx:279-283 — 多选截图只取最后一张

    • 现状:onImport 里 const picked = assets.at(-1),只写回最后一张。
    • 问题:AssetImporter 的本地文件对话框是 multiple(上限 100),其余截图被静默丢弃。
    • 修复:按 MAX_AGC_GAME_SCREENSHOTS - 当前张数 的剩余名额逐张 onAppendScreenshot;测试替身支持返回多张,新增多选导入断言(057b56b24)。
  • 21. [bug·high] game_distribution_publish.rs:1062-1068 — AGC 回读链路没跟着硬切到 objectKey

    • 现状:GameDistributionPublicationImageDraft 只序列化 assetId/previewUrl;build_publication_draft 拿到 cover_object_key / 截图 object_key 只用于换签预览就丢弃,新合同下 assetId 恒为 None。FrozenPublicationMetadata.screenshots 仍按对象数组解析,而服务端 frozenMetadata.screenshots 已是字符串数组。
    • 问题:面板按 objectKey ?? assetId 取沿用键,两者都拿不到 → 沿用线上封面时 coverObjectKey 变空、截图槽位全 null,更新已上架作品会被迫重选封面/截图(或报「请先选择游戏封面」)。
    • 修复:用户确认做。GameDistributionPublicationImageDraft 增加 object_key: Option<String> 并按 objectKey 序列化;FrozenPublicationMetadata.screenshots 从 Vec<FrozenPublicationScreenshot> 改为 Vec<String>,删除该结构体;build_publication_draft 透传 normalize_object_key(cover_object_key) 与字符串截图数组;前端 GameDistributionPublicationImageDraft 同步为 objectKey;补 Rust 断言 frozen_metadata_accepts_string_screenshot_array(800c45680)。
  • 22. [bug·high] server-rs/.../modules/game_distribution.rs:1859-1861 — 三条 multipart 路由缺 DefaultBodyLimit

    • 现状:create_game / create_version / update_owner_game_metadata 收 multipart(1 封面 + 最多 6 截图 × 8MiB),路由都没有显式 DefaultBodyLimit。
    • 问题:Axum 默认 2MiB,真实带图发布会在 handler 之前就被 413。
    • 修复:新增 MAX_PUBLISH_MULTIPART_BYTES = 8MiB × 7 + 2MiB 并给三条路由加 .layer(DefaultBodyLimit::max(...));MAX_SINGLE_MEDIA_BYTES 提为 pub(crate),补口径断言(d24a16ad0)。
  • 23. [bug·high] game_distribution.rs:3749-3750 — 旧 screenshots_json 对象数组按 Vec<String> 解析失败

    • 现状:serde_json::from_str::<Vec<String>> 对旧 [{"assetId","objectKey"}] 返回 Err,unwrap_or_default() 静默变空列表。
    • 问题:若线上确有旧行,已发布作品的截图不显示,作者沿用旧槽位也会被拒。
    • 结论:用户确认「未上线、暂无数据」,不修。本迁移按计划/决策日志是「硬切、无历史数据、不保留旧 JSON 兼容」;上线前若引入旧行,应先做一次性数据迁移而不是长期兼容解析。
  • 24. [bug·medium] game_distribution.rs:1868-1877 — 先上传 OSS 再写 SpacetimeDB,重放/冲突留孤儿对象

    • 现状:媒体带新 UUID 先上传,随后事务里按幂等键去重或校验 revision。
    • 问题:幂等重放或 revision 冲突时,新上传对象无人引用且没有清理路径。
    • 修复:用户确认「先在事务/收据层判重与校验 revision 再上传」。更新资料路径在 resolve_publish_media 之前先按刚读到的游戏行做 revision CAS,过期直接返回同一 409 PUBLICATION_CONFLICT 口径;resolve_publish_media 拆出 inner 并记录本次新传 objectKey,中途失败即补偿删除;事务失败或幂等重放(replayed=true)时 cleanup_uncommitted_publish_media 调 delete_media_objects 清理孤儿。说明:幂等收据只在 reducer 事务内可读,api-server 无法上传前查收据,故重放改为事后补偿,效果同样是不留孤儿(7aeac536e)。
  • 25. [security·medium] game_distribution_media.rs:147-149 — 内容类型完全信客户端 multipart 头

    • 现状:只检查 starts_with("image/"),image/svg+xml 可通过并按该类型存储;bytes 端点回显存储类型,没有 nosniff/attachment。
    • 问题:带脚本的 SVG 可能被同源当主动文档执行。
    • 修复:用户确认做。read_image_field 改按文件头嗅探 PNG/JPEG/WebP/GIF/AVIF,声明为 SVG 等主动内容一律拒绝,落桶 Content-Type 与扩展名以嗅探结果为准;proxy_read_bytes 新增 ObjectReadContentPolicy,游戏媒体只放行栅格类型、其余降级 application/octet-stream + Content-Disposition: attachment,并统一加 X-Content-Type-Options: nosniff(7aeac536e)。
  • 26. [performance·low] server-rs/.../api-server/src/object_read.rs:70-74 — 每次 read-bytes 都新建 reqwest::Client

    • 问题:丢连接池、且没设超时;该代理已被素材与游戏媒体两条读路径共用。
    • 修复:用户确认做。proxy_read_bytes 改为接收调用方传入的共享 client,素材域与游戏媒体域都传 state.editor_oss_http_client()(进程级复用,connect 10s / total 120s),不再每次 reqwest::Client::new()(7aeac536e)。
  • 27. [performance·low] game_distribution_media.rs:133 — upload_media_image 克隆整段最多 8MiB 字节

    • 现状:摘要已在 resolve_publish_media 之前算完,上传仍借用并 clone。
    • 修复:用户确认做。upload_media_image 改为取 UploadedImage 所有权,body: image.bytes 直接移交,去掉整段 clone()(7aeac536e)。
  • 28. [bug·high] spacetime-module/.../game_distribution.rs:4156 — GameDistributionFrozenMetadata::screenshots 从对象数组改为 Vec<String>

    • 问题:旧 metadata_json 会在 parse_game_distribution_frozen_metadata 反序列化失败,审核通过时重新解析旧版本会报「版本冻结资料格式无效」。
    • 结论:同 23,用户确认「未上线、暂无数据」,不修;上线前若有 pending 旧版本,需先迁移 metadata_json 而不是保留旧形状兼容。
  • 29. [bug·high] spacetime-module/.../game_distribution.rs:4225 — 旧 screenshots_json 行按 Vec<String> 解析 .ok() 成 None

    • 问题:已发布作品截图不再获得匿名读授权(封面因 cover_object_key 本就是字符串不受影响)。
    • 结论:同 23/28,用户确认未上线无旧数据,不修;若有旧行需一次性迁移。
  • 30. [performance·low] spacetime-module/.../game_distribution.rs:4216 — game_distribution_media_has_public_read_grant 每次媒体读都 O(n) 扫全表

    • 现状:公开未鉴权路径上逐行反序列化 screenshots_json。
    • 修复:用户确认做,采用不改 schema 的派生查找:媒体 key 形如 {prefix}{game_id}/{file},先按 GAME_DISTRIBUTION_MEDIA_PREFIX 反解 game_id,再按主键 .game_id().find(...) 只读那一行核对封面/截图归属,降为 O(1);同时把该前缀抽到 shared-contracts 供写路径与授权端共用同一常量,补 key 布局单测(08771d39e)。
  • 31. [maintainability·medium] scripts/check-game-distribution-media-e2e.mjs:129-131 — publishFormData/imagePart 在多个 E2E 脚本重复

    • 修复:用户确认做。新增 scripts/game-distribution-e2e-helpers.mjs(COVER_PNG / imagePart / coverImagePart / publishFormData / resolveApiRequest),11 个 E2E 脚本删除各自的重复实现改为 import,multipart 字段名与 api() 的 FormData 分支只留一份(1f239a96c)。
  • 32. [security·medium] .../taonier/build.rs:186-190 — 符号链接产物仍被完整读入

    • 现状:std::fs::read(&path) 发生在 produced 门禁之前,而 produced=false 恰恰可能是「这是符号链接」。
    • 问题:被拒的符号链接目标仍被无上限读进内存。
    • 修复:同 11,读取前先按 produced 与 metadata.len() 双重门禁(d3d7e5a8c)。
  • 33. [bug·medium] scripts/check-game-distribution-validation-restart-e2e.mjs:241-244 — 创建失败后仍解引用 created.data.coverObjectKey

    • 现状:check() 只记失败不中断,created.data 为空时下一行直接取属性。
    • 问题:抛未捕获 TypeError 打崩整个脚本,拿不到正常的 check 结论。
    • 修复:check 后补 if (!gameId) process.exit(1);(8e59e11f0)。
  • 34. [bug·medium] scripts/check-game-distribution-web-a11y-e2e.mjs:226 — 同上,且在 publishFormData 实参里提前解引用

    • 修复:const gameId = created.data?.id 后补 if (!gameId) process.exit(1);(152cdc617)。
  • 35. [bug·low] scripts/check-game-distribution-validation-restart-e2e.mjs:438 — 旧 E2E_STATE_FILE 只有 coverAssetId

    • 问题:state.coverObjectKey 为 undefined 会被 JSON.stringify 丢掉,续跑创建的 v3 没有封面。
    • 修复:读封面键时回退 state.coverObjectKey ?? state.coverAssetId(9c822c098)。
  • 36. [maintainability·low] scripts/check-game-distribution-upload-safety.mjs:125 — 同一批 multipart helper 重复(≥11 份)

    • 修复:同 31,已收纳进 scripts/game-distribution-e2e-helpers.mjs(1f239a96c)。
  • 37. [maintainability·low] scripts/check-game-distribution-media-e2e.mjs:650-662 — 断言硬编码内部存储前缀 agc/project-snapshots/v1/game-distribution/media/

    • 问题:这是服务端内部布局常量,不是 wire 契约;bucket 布局调整会让契约仍有效的 E2E 失败。
    • 修复:用户确认做。断言放宽为结构校验:非空、去空白、含路径分隔符、不是客户端 part 文件名;不再写死内部前缀(009a772eb)。
  • 38. [test·low] scripts/check-game-distribution-ops-rollback-e2e.mjs:537 — 封禁建游请求同时带 objectKey 与 cover 字节

    • 问题:payload 语义含糊,与套件其余「新图」写法(coverObjectKey: null + cover part)不一致。
    • 修复:改为 publishFormData({ ...resolvedMetadata, coverObjectKey: null }, { cover })(9184e9277)。
  • 39. [bug·high] src/components/game-distribution/gamePublishMediaDefaults.ts:43-45 — 旧 {assetId,objectKey} 截图数组会让 objectKey?.trim() 抛错

    • 现状:screenshotSource 声明为 string[],旧数据元素是对象。
    • 问题:若线上存在旧形状,applyVersionMediaDefaults 会 TypeError 中断,版本续做失败且在线默认值被静默丢弃。
    • 结论:同 23/28/29,用户确认未上线无旧数据,不修。若将来确有旧行,应先服务端迁移;如仍想防御,可在这里按 typeof item === 'string' ? item : item?.objectKey 归一。
  • 40. [performance·medium] gamePublishAssets.ts:83-85 — blob 预览 URL 只在显式移除/替换时 revoke

    • 现状:applyVersionMediaDefaults 覆盖 coverImage/screenshotImages、以及组件卸载时,旧 blob URL 没有 URL.revokeObjectURL。
    • 问题:浏览器内存泄漏。
    • 修复:用户确认做。applyVersionMediaDefaults 在用线上默认值覆盖前先 revoke 被替换的本地预览;GamePublishPage / GameWorkMetadataEditor 用 ref 跟踪最新图片,卸载时兜底 revoke;补「覆盖前 revoke」断言测试(5ce5cb006)。
  • 41. [performance·low] GamePublishMediaFieldset.tsx:185 — key 退化成 ${name}-${index}

    • 现状:本地图 objectKey 为 undefined,key 用 name-index。
    • 问题:截图删除/重排时 index 位移,React 会重建节点;同名文件也只能靠 index 区分。
    • 修复:GamePublishImage 新增稳定 id(createGamePublishImageId),本地图与线上沿用图各自铸一次,列表 key 改用 image.id,补同名文件 id 互异单测(12d4e8477)。
  • 42. [maintainability·low] GamePublishPage.tsx:310-315 — 槽位映射与 File part 组装在发布页/编辑页重复

    • 修复:用户确认做。gamePublishAssets 新增 buildGamePublishMediaSubmission,统一「槽位 objectKey 去空白 / 本地新图只进 parts」;GamePublishPage 的 metadata 槽位与 multipart part、GameWorkMetadataEditor 的 payload/media 都改用它,补槽位与 part 同源单测(6f0ea0310)。
  • 43. [bug·medium] .../export/state/useTaonierExport.ts:548-549 — 保存失败先无条件 setDirty(false)

    • 现状:catch 里先清 dirty,再按失败类型分流。
    • 问题:瞬时 exportUnavailable 会丢掉未落盘编辑:dirty=false 让 adoptableRef 变真,宿主恢复后的重读会用陈旧注册表表单覆盖用户最新输入。
    • 修复:分流后再清——saveConflict/未识别/其余失败清 dirty,exportUnavailable 保持 dirty(075078f35)。
  • 44. [bug·medium] useTaonierExport.ts:381-384 — 采纳新表单时无条件 setFailure(null)

    • 现状:函数式清理(只清读来源失败)在上面,采纳路径又无条件清一次,和注释「构建类失败不在这里清」矛盾。
    • 问题:面板后台注册表被改写时,下一次成功重读会把正在展示的构建失败弹窗(commandFailed/artifactMissing 等)静默 dismiss。
    • 修复:删掉这次无条件清空,保留上方只清读来源失败的函数式清理;补「构建失败 + 重读采纳新表单」测试(00448e0b2)。
  • 45. [bug·low] .../export/state/taonierFields.ts:100-101 — 面板按码点、发布预检按 UTF-16 计数

    • 现状:validateTaonierForm 用 Array.from(trimmed).length(码点),gameDistributionPublish.ts:normalizeMetadata 仍用 title.length > 40 / summary.length > 120(UTF-16 码元)。
    • 问题:含 emoji 的压线标题/简介能过面板,却会在发布预检处被误拒;服务端用的是 chars().count()(码点)。
    • 修复:normalizeMetadata 改用 Array.from(...).length,与面板和服务端同口径;补 40 emoji 标题 + 120 emoji 简介测试(42c742df5)。
  • 46. [bug·medium] server-rs/.../modules/game_distribution.rs:1721 — update_owner_game_metadata 幂等摘要不含 game_id

    • 现状:摘要只哈希 payload,而幂等收据键是 (owner_user_id, action, idempotency_key),不按游戏隔离;create_version 已经用 (game_id, &payload)。
    • 问题:同一作者对两部作品用同一个 Idempotency-Key 推送相同资料时,第二次会被当成第一次的重放:跳过写入、返回第二部作品的旧状态(replayed=true),更新被静默吞掉。
    • 修复:摘要改为 (game_id.as_str(), &payload);补跨作品摘要区分断言(fede8cc16)。
  • 47. [bug·medium] src/components/game-distribution/GameDetailPage.tsx:121 — 作者视角媒体换签走了匿名公开路由

    • 现状:GameDetailResolvedAsset 只用 useGameDistributionMediaReadUrl(objectKey),不带 gameId。
    • 问题:公开读只服务 published + active 作品;作者预览自己的未发布/待审/驳回作品时,封面/截图静默退回占位(项目记忆里记过的「作者预览必须走 owner 路由」坑)。
    • 修复:组件加可选 gameId,renderAsset 传 ownerView ? game.id : undefined;测试替身记录换签选项,补作者视角带 gameId 断言(067f02c85)。
  • 1. 【.gitattributes:33】现状:文件末尾同时存在两条完全相同的 apps/ai-game-creator-shell/src/view/project-development/export/generated/** ... 规则(原第 33、35 行)。问题:重复规则无功能影响,但污染文件、误导后续编辑。修复:删除重复行。commit bff0006eb。

  • 3. 【vite.config.taonier.mjs:55】现状:assertH5Root 只用 /\s(?:src|href)="(\/[^"]*)"/ 匹配双引号属性。问题:单引号属性(src='/app.js')与 = 两侧空白漏检,误配 base 的产物可能通过校验、上传后整站 404(Vite 一般输出双引号,但手写/插件 HTML 会漏)。修复:正则改为 /\s(?:src|href)\s*=\s*(["'])(\/[^"']*)\1/。commit 9c2664289。

  • 4. 【vite.config.taonier.mjs:130】现状:defineTaonierConfig 先 mergeConfig(baseConfig, overrides),再补插件;Vite 的 mergeConfig 会用 overrides 的标量覆盖 build.outDir。问题:overrides 改了 outDir 后,pack.mjs 的 --vite-built-dir 默认仍是 dist-taonier,会打包陈旧/缺失目录且无提示。修复:合并后重新钉死 merged.build.outDir = TAONIER_OUT_DIR。commit af752eaa1。

  • 5. 【pack.mjs:162】现状:writeZipEntries 直接 openSync(zipPath, 'w') 截断落点。问题:写盘中途失败(磁盘满、4 GiB 上限、源文件被删)会进入只删落点的 catch,把落点上原有合法 zip 一起删掉。修复:写同目录临时文件 <zip>.tmp-<pid>,成功后 renameSync 覆盖;失败只清理临时文件,makeZip 失败路径不再删落点。commit 5b0c41683。

  • 6. 【pack.mjs:126】现状:collectZipEntries 只处理 isDirectory() 与 isFile()。问题:FIFO/socket/设备文件既非目录也非普通文件,assertNoSymlinks 也不拦,会被静默跳过,产出缺文件的包。修复:其余类型直接抛错。commit bda1bb081。

  • 7. 【preview.rs:1111】现状:导出目录从 exports 改名为 .export 时,预览服务的两处受保护组件列表只删了 exports、漏加 .export。问题:项目根存在 index.html 时,/.export/taonier.json 等注册表 JSON(content-type 非 octet-stream)会被本地预览直接读走。修复:两处列表补回 .export(.zip 仍因 octet-stream 被挡)。commit 5df27afed。

  • 8. 【game_distribution_publish.rs:1127】现状:read_publish_image 先 std::fs::metadata 看体积,再 std::fs::read 按路径读。问题:两次系统调用之间路径可被换成符号链接或超长文件(TOCTOU),超长文件会先被读进内存。修复:改用 OpenOptions + O_NOFOLLOW/FILE_FLAG_OPEN_REPARSE_POINT 打开,取同一句柄 metadata 定上限后 take(cap+1) 限长读取(原 review 里的常量名 MAX_DRAFT_MEDIA_BYTES 不存在,实际是 MAX_PUBLISH_MEDIA_BYTES)。commit 951284205。

  • 9. 【game_distribution_publish.rs:1267,误报】现状:ensure_publish_project_stack 已不存在,发布前只校验 project.export_package 权限并读固定 .export/taonier.zip。问题:这是 b04aceb57 有意删除的 phaser/vite 发布门,新门禁在导出/构建步骤(prepare() 的 BuildScriptMissing),计划文档 D8 与 decision-log 都有记录;按 review 恢复会退回旧产品决策。处理:无需改动。

  • 10. 【repository_context.rs:719】现状:should_ignore_directory 顶层组件只匹配 memory | logs | .app,exports 改名后漏了 .export。问题:scan_repository 会把生成的 .export 打包内容纳入 agent 仓库上下文。修复:匹配加入 .export。commit ad0877219。

  • 11. 【FormCard.tsx:14】现状:封面/截图复用小红书 tab 的 IconPreview,其 alt="图标预览"、失败 title「用『选择图标』导入…」等文案写死为「图标」。问题:封面/截图的读图失败提示对用户说的是错误对象。修复:IconPreview 增加可选 labels(缺省保持小红书文案),FormCard 的封面/截图各自传入对应 alt、title、aria。commit cb57e59ec。

  • 12. 【FormCard.tsx:274】现状:封面槽只保存一个路径,但复用了多选上限 100 的 SPRITE_IMPORTER_SETTINGS,onImport 只取 assets.at(-1)。问题:用户多选时其余图片被静默丢弃。修复:新增 COVER_IMPORTER_SETTINGS(maxItems: 1),封面导入器改用它(截图仍用多选设置)。commit edaf8896e。

  • 13. 【taonier/script.rs:53】现状:find 读取脚本命令时 map(str::trim),而 project/verification.rs 的防漂移比较用 actual_command != expected_command(actual 是 package.json 原始值)。问题:脚本值带首尾空格/换行时永远判「脚本已变化」→ exportUnavailable。修复:比较时两边都 trim()(只比较脚本内容、不比较无意义空白)。commit 3a6758095。

  • 15. 【taonier/build.rs:192】现状:conclude 里 std::fs::read 前只看 512 MiB 读入安全线,平台 200 MiB 上限在读完后的 ensure_within_platform_package_limit 才判。问题:200–512 MiB 产物会先整包读进内存再判不合规。修复:读之前同时按平台 200 MiB 上限过滤。commit a3cf98000。

  • 16. 【taonier/build.rs:262】现状:read_artifact_package 先用 symlink_metadata 拒符号链接,再 File::open 按路径打开 read_package_bytes。问题:检查与打开之间被换成同长度符号链接时仍会被跟随(TOCTOU,低危)。修复:改 OpenOptions + O_NOFOLLOW/FILE_FLAG_OPEN_REPARSE_POINT。commit ba9d70a5b。

  • 17. 【vite.config.taonier.mjs:68】现状:pruneEmptyDirs 用 statSync 判断目录(会跟随符号链接)。问题:链接指向产物目录之外(甚至祖先)时会递归清理/栈溢出。修复:改 lstatSync,符号链接一律跳过。commit 003f7cfe9。

  • 18. 【game-distribution-e2e-helpers.mjs:34】现状:coverImagePart 与 COVER_PNG 被导出,但仓库内无任何脚本导入(10 个脚本各写本地 coverImagePart,2 个 web-publish 脚本各写本地 COVER_PNG)。问题:死导出误导后续维护。修复:删除 coverImagePart,COVER_PNG 收回为模块内部常量。commit 0f127dbc7。

  • 19. 【useTaonierTab.ts:254】现状:hook 挂在面板根、跨项目不卸载;发布链路在 await packageAll/订阅回调/await publishLocalProjectGame 之后无条件 setProgress/setPublishResult。问题:发布 A 项目期间切到 B 项目,A 的进度/结果/失败会写进 B 的面板。修复:新增 projectPathRef,在各 await 之后与上传进度回调里比对当前项目再写状态。commit 5bc52a344。

  • 20. 【useTaonierExport.ts:784】现状:packageAll 用 render 闭包里的 form 做本地校验;refresh()/flush 可能已把更新的落盘表单写进 effectiveFormRef。问题:form 可能是陈旧快照,会放行盘上的非法值,违背「校验不过不触宿主」。修复:改用 effectiveFormRef.current ?? form。commit 8e3f0b892。

  • 21. 【test-creator-runtime.py:194】现状:fixtures 的 game 字典没有 price_mud_points。问题:该字段是非 Option 的 u64,SpacetimeDB SATS 反序列化对缺失字段报 missing_field;recover_row_with_deleted_fields 只忽略未知字段,normalize_migration_row 没有 game_distribution_game 分支,脚本会在导入 fixture 时直接失败。修复:补 price_mud_points=0(根因上更彻底的 normalize_migration_row 回填见第 21 条备注,属数据兼容,未自动做)。commit 28be35239。

  • 22. 【check-game-distribution-validation-restart-e2e.mjs:413】现状:兼容旧 STATE_FILE 时用 state.coverObjectKey ?? state.coverAssetId 组装元数据。问题:旧版存的 coverAssetId 是素材对象 ID,不是 OSS objectKey;建版沿用要求它等于游戏当前 cover_object_key,因此这个兼容分支永远 400。修复:缺 coverObjectKey 时先 GET 游戏详情取回真实 coverObjectKey。commit 11c8c2789。

  • 23. 【taonier/commands.rs:171】现状:run_build 在 async 函数里直接调 project_root(同步文件 IO,Windows 上还会触发 UAC/ACL 修复)。问题:占着 Tokio worker,拖慢同 runtime 的其它异步任务;同模块其它 IO 都走了 offload。修复:project_root 挪进 offload(spawn_blocking)。commit 2ab7717a2。

  • 24. 【OnlineValuesCard.tsx:113,已过时】现状:该文件已在 9a653c838(你的提交)中删除,线上值内联进 FormCard 字段行,仓库内无引用。处理:finding 已不适用,无需改动。

  • 25. 【check-game-distribution-purchase-e2e.mjs:1470】现状:定价边界用例建游失败时,boundsGameId 为 undefined 仍继续请求 /my-games/undefined、/games/undefined/versions。问题:级联报错掩盖根因。修复:记录失败后 if (!boundsGameId) process.exit(1)。commit 3a0d27574。

  • 26. 【useTaonierExport.ts:727】现状:buildOnce 只 setFailure(null),不清 dismissedFailure;visibleFailure 用 JSON 指纹过滤已忽略的失败。问题:点「重新构建」时同一条确定性失败(如固定路径的 artifactMissing)会被永久滤掉,弹窗不再出现。修复:setFailure(null) 前 setDismissedFailure('')。commit 86649e50a。

  • 28. 【gamePublishMediaDefaults.ts:78】现状:截图换签用 for ... await 串行请求(最多 6 次往返)。问题:预览出现慢。修复:改 Promise.all 并发请求后一次性回填。commit 5e5bb19a6。

  • 29. 【useGameDistributionMediaReadUrl.ts:43】现状:effect 只用 cancelled 标志忽略结果,不取消在途 GET;两个 client 函数也不收 AbortSignal。问题:objectKey/gameId 快速切换时旧请求照跑,白占 API 配额。修复:两个 client 函数与 resolveGamePublishImagePreview 增加可选 signal,hook 在清理时 controller.abort()。commit e830034d0。

  • 30. 【gameDistributionClient.ts:408】现状:readGameDistributionMediaSignedUrl 直接 payload.read;requestJson 对空响应体会返回 null as T。问题:空响应触发原始 TypeError,而不是既有的「游戏媒体访问地址缺失」。修复:payload?.read ?? payload 与 read?.signedUrl。commit 3f211f12d。

  • 31. 【api-server game_distribution.rs:2060】现状:create_game 先 resolve_publish_media(...).await? 上传封面/截图,再由 cleanup_uncommitted_publish_media 兜底清理;normalize_local_project_id(...)? 写在 create_game_distribution_game 实参里、在 cleanup 之前求值。问题:用户传入非法 localProjectId 时提前 ? 返回,已上传对象成为孤儿。修复:把 normalize_local_project_id 提到 resolve_publish_media 之前。commit f4582fe5b。

  • 32. 【api-server game_distribution.rs:2139】现状:create_version 同样的顺序问题。问题:同上,上传后提前返回留下孤儿对象。修复:同样提前校验。commit f4582fe5b。

  • 37. 【gamePublishMediaDefaults.ts:80】现状:取消后 if (apply.isCancelled() || !preview) continue; 仍会推进循环、继续发剩余截图请求。问题:取消语义失效。修复:并发版本在 Promise.all 后统一 if (apply.isCancelled()) return;,取消后不再补写也不再发请求。commit 5e5bb19a6。

  • 39. 【GamePublishPage.tsx:335】现状:buildPublishGameMetadata() 与 buildPublishMediaParts() 各自调用一次 buildGamePublishMediaSubmission。问题:当前纯函数结果一致,但「单一来源」只是口头约束,未来改动可能让元数据槽位与上传 part 分叉。修复:两个 builder 改为接收调用方算好的 submission,两处提交流程各算一次。commit 53750ac31。

二、逐条处理结果(按你的决定)

  • 2. 【pack.mjs:175】现状:每个文件 readFileSync 整读 + deflateRawSync 产出一份压缩副本,峰值约 2× 文件大小;注释宣称的「流式」只在归档层面成立。问题:大型 H5 资源(音频/贴图/WASM)可能顶爆内存或导出中断。处理:按你的决定不重写,已在整读+压缩处补 TODO(perf),说明峰值与 ZIP data descriptor(通用标志位 bit 3)流式改法。commit 7d8b966d1(含 agc-skills manifest 版本/哈希同步)。
  • 14. 【taonier/build.rs:86】现状:run_project_verification_with_commit_at 的项目侧预检失败(不支持的包管理器、项目 .npmrc、脚本超长/含换行、脚本漂移、脚本缺失)过去和沙箱/日志故障一起返回 String,被 map_err(TaonierExportUnavailable::new)? 一律收成 exportUnavailable。问题:项目事实问题被当成宿主故障,用户被引导去重试一个只有改项目才能解决的问题。修复:verification.rs 拆出「按已解析规格运行」的入口;陶泥儿与小红书构建先单独跑项目侧预检,失败映射为新变体 projectUnavailable。前端把该变体归为 code agent 可修、文案写死,ts-rs 绑定与测试同步更新。commit 06e6cf852(陶泥儿测试用例的两行覆盖并入了你当时的并发提交 61acaf6ce)。
  • 27. 【spacetime-module game_distribution.rs:4249】现状:公开读授权用 serde_json::from_str::<Vec<String>> 解析 screenshots_json,解析失败 .ok() 静默变 None;旧格式(master 4a2b27071 写出的 [{"assetId","objectKey"}])会解析失败,老作品截图匿名读 404。处理:按你的决定不修——线上还没有这类数据,暂无兼容压力。等真有旧数据时再按持久化兼容流程加双形状 helper 或做一次性行重写。
  • 33. 【GamePublishPage.tsx:517】要不要修、怎么修由你定。现状(大白话):首次发布新游戏时,前端先把封面/截图上传一遍(发生在「建作品」这一步),紧接着「建第一个版本」时又把同一批图重新上传了一遍;同一张图因此在存储里存在两份,服务端记录的是第二份,第一份不再被任何地方引用、也不会被自动清掉。问题:每次首次发布都会多留一份没人用的图片,长期占存储,排查时也容易误会。为什么不能简单地把第二次上传删掉:「建作品」那一步交给「建版本」的资料里,图片位置已经被填成「用线上的」(空值);如果建版本时不再带图,会被判定成没有图而校验失败。建议做法(会改行为):建作品成功后,用它的返回结果里两张图的实际地址,去组装「建版本」要用的资料,让建版本走「沿用已有图」、不再带本次上传;同时核对服务端「沿用」的判定,并改发布流程的自动化测试。属行为变更,本次未做。
  • 34. 【spacetime-module game_distribution.rs:4154】现状:GameDistributionFrozenMetadata.screenshots 已是 Vec<String>,旧 metadata_json 里存对象数组时,管理员审批这类历史待审版本会直接失败。处理:按你的决定不修——线上还没有这类数据。等真有历史待审版本时,再给该字段加兼容对象数组的反序列化并补测试。
  • 35. 【api-server game_distribution.rs:3911】现状:game_payload、game_media_object_keys(292-294)与 resolve_publish_media_inner 的 current_screenshots(4105-4106)都用 Vec<String> 解析截图,旧格式下会丢图 / 作者读不到 / 「沿用截图」永远不匹配。处理:按你的决定不修——线上还没有这类数据。将来若出现旧格式,与第 27 条一起用同一个双形状 helper 替换这三处。
  • 36. 【api-server game_distribution_media.rs:178】现状:field.bytes().await 把整个 multipart part 收成一个 Bytes,随后 bytes.to_vec() 再完整复制一份,峰值约 2× part;单个超大 part 会被完整缓冲后才 413。处理:按你的决定不改写,已在读取处补 TODO(perf),说明峰值与 field.chunk() 循环流式限长改法。commit 0119a329a。
  • 38. 【gamePublishMediaDefaults.ts:51】现状:revokeGamePublishImagePreview(URL.revokeObjectURL)原先在 setCover/setScreenshots 的 updater 里执行。问题:副作用写进 state updater,StrictMode 双调用/重放可能撤销仍被渲染引用的 blob URL,updater 也不再是纯计算。修复:VersionMediaApplyTarget 增加 getCover/getScreenshots,在 dispatch 之前先 revoke;两个调用点用各自已有的 ref 提供当前媒体,updater 只做纯计算。commit dca848103。
- [x] 1. `[maintainability·low]` `apps/ai-game-creator-shell/tests/jsdomLocalStorageSetup.ts:17-34` — 内存 Storage 替身重复三份 - 现状:`jsdomLocalStorageSetup.ts`、`appSurface/harness.ts`、`chatPromptPolish.test.tsx` 各有一份逻辑几乎一致的内存 `Storage`。 - 问题:`length`/`key`/`removeItem` 语义以后要改就得改三处,容易漂。 - 修复:抽出 `tests/memoryStorage.ts:createMemoryStorage()`,三处统一 import(`99d5fd1ea`)。 - [x] 2. `[bug·high]` `apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:448-450` — 图片字节直接拼接算摘要会碰撞 - 现状:`metadata_digest` 只 `hasher.update(&image.bytes)`,不带长度或分隔符。 - 问题:metadata 只记槽位数量、不记字节边界,`封面[1,2]+截图[3]` 与 `封面[1]+截图[2,3]` 得到同一摘要;摘要喂给根幂等键,会把改过的媒体当成重放静默丢弃。 - 修复:逐张先写 `u64::to_le_bytes()` 长度再写字节,并补分片不变性断言(`3832ca4fa`)。 - [x] 3. `[performance·medium]` `game_distribution_publish.rs:1036-1037` — 读草稿图片没有体积上限 - 现状:`read_draft_image` 直接 `std::fs::read`,只在服务端 8MB 处事后拒绝。 - 问题:草稿指向超大本地文件时,会先把宿主内存读满再失败。 - 修复:先 `std::fs::metadata().len()`,超过 8MB(对齐服务端单图上限)直接返回错误;补稀疏大文件测试(`3ecd4efcb`)。 - [x] 4. `[maintainability·low]` `game_distribution_publish.rs:1124` — multipart 文件名一律 `.png` - 现状:`multipart_image_part` 固定叫 `cover.png` / `screenshot-{i}.png`,但内容类型可能是 JPEG/WebP/GIF/AVIF。 - 问题:服务端目前按 MIME part 判定所以能跑,但文件名在说谎,将来有消费者信文件名就会错分类。 - 修复:新增 `multipart_image_file_name`,按 `content_type` 给扩展名(`3d6795978`)。 - [x] 5. `[maintainability·low]` `game_distribution_publish.rs:1148` — `_package_relative_path` 已是死参数 - 现状:发布包固定读 `.export/taonier.zip`,命令签名仍要求前端传 `package_relative_path`,helper 里也保留形参。 - 问题:名字看起来能控制包路径,实际被忽略,误导后来的调用者。 - 修复:用户确认清理。`stage_publish_package` / `publish_local_project_game` 删除 `package_relative_path` 形参,前端 `publishGameDistribution` 与 `useTaonierTab` 同步不再传,命令契约更新(`aa946a930`)。 - [x] 6. `[bug·low]` `apps/ai-game-creator-shell/src/services/gameDistributionPublish.ts:179-180` — 空串截图槽位被 filter 掉会错位 - 现状:`normalizeMetadata` 先 trim 再 `filter(s => s !== '')`。 - 问题:契约是「槽位与最终截图顺序一一对应」,丢槽位会让后面的截图整体前移;Rust 侧把空串当本地图槽位。 - 修复:`map` 里把 `null`/空串统一归一成 `null`、不再 filter,测试同步改为按序断言(`ab9789d11`)。 - [x] 7. `[style·low]` `.../tabs/taonier/ArtifactsPane.tsx:124-128` — `publishLabel` 嵌套三元 - 修复:改成 if/else 三态赋值,行为不变(`30b129fcf`)。 - [x] 8. `[style·low]` `ArtifactsPane.tsx:121-122` — `online?.nextVersionNumber == null` 宽松相等 - 修复:改为 `!online || online.nextVersionNumber === null`(`229f0580f`)。 - [x] 9. `[style·low]` `.../tabs/taonier/OnlineValuesCard.tsx:79-85` — 模板字符串里嵌三元 - 修复:先在 JSX 外算 `onlineSummary`(含版本号后缀),再单点插入(`4d72a77ae`)。 - [x] 10. `[maintainability·low]` `.../tabs/taonier/ConflictCard.tsx:65` — `as never` 关掉类型检查 - 现状:解冲突时 `resolved[meta.field] = conflict.current[meta.field] as never`。 - 问题:联合键赋值被强行压制,将来表单加非 `string`/`string[]` 字段会静默通过。 - 修复:按字段类型分开赋值——`screenshotPaths` 走数组分支、其余走字符串分支(`500c3fda6`)。 - [x] 11. `[bug·medium]` `.../export/draft/taonier/build.rs:185` — 结论阶段无体积门禁就整包读入 - 现状:`let bytes = std::fs::read(&path).ok();` 在 `produced` 判断之前执行。 - 问题:构建脚本写出超大产物时先把内存读满;符号链接目标也会被完整读入。 - 修复:只在 `produced` 为真时读,且先过 `metadata.len() <= ARTIFACT_PACKAGE_READ_MAX_BYTES`,超线直接判不合格(和 32 同一提交 `d3d7e5a8c`)。 - [x] 12. `[performance·low]` `build.rs:251` — `read_package_bytes` 用 `Vec::new()` - 现状:调用方已经知道 `metadata.len()`,函数却从空 Vec 反复扩容。 - 问题:接近 200MiB 的包会多复制约一倍数据。 - 修复:函数接收调用方已验证的 `expected_bytes`,改用 `Vec::with_capacity`(`4acb0e712`)。 - [x] 13. `[style·low]` `.../export/state/useTaonierTab.ts:179` — `== null` - 修复:改 `online.nextVersionNumber === null`;`online` 为空已由 `!online` 挡住(`7b521f6f2`)。 - [x] 14. `[bug·medium]` `useTaonierTab.ts:177-178` — 先快照 `form` 再 `await packageAll()` - 现状:`publishGame` 用进入时的 `form` 快照拼 `buildTaonierPublishMetadata`,但 `packageAll()` 开头会 `refresh()`,可能在用户无未保存编辑时采纳更新的注册表表单。 - 问题:Rust 发布命令读的是磁盘上的 `.export/taonier.json` 媒体路径,文本字段却来自旧快照,理论上会用过时标题/简介发布。 - 修复:用户确认做,并核对了「构建/自动保存/是否所有字段都该走 UI」的疑问。`useTaonierExport` 新增 `getEffectiveForm()`,`packageAll()` 落盘成功后记录本次落定的权威表单;`publishGame` 在 `await packageAll()` 之后用 `getEffectiveForm() ?? form` 合成 metadata,补两条测试(27 passed)。核实结论:`packageAll` 本就会触发 vite 构建与自动落盘,`.export/taonier.json` 是 UI 自己写的持久化副本、不是构建/skill 写的;XHS 导出有同样的构建/自动保存结构但没有发布到广场的路径,因此没有这个陈旧表单问题,其表单也只有 `name/introduction/iconPath`。用户「所有字段都改走 UI、不读磁盘文件」的建议只对一半:去掉磁盘读取既不能消除陈旧,又会丢掉 Rust 侧「UI 槽位数 vs 草稿本地图数」的 fail-closed 校验,最小正确修复即本提交(`ab2af60d3`)。 - [x] 15. `[bug·medium]` `useTaonierTab.ts:194-195` — `packageAll` reject 时进度卡在 running - 现状:`refresh`/`persist`/`buildOnce` 会把 `exportUnavailable` 与未识别失败经 `throwToErrorPool` 抛出,这里的 `await` 没包 try/catch。 - 问题:`publishGame` reject,`progress` 停在 `phase:'running'`;运行中对话框没有关闭入口,面板一直卡在「正在打包发行包…」。 - 修复:`await` 包 try/catch,失败把进度置 `failed` 并带原因(`8d74fa9ab`)。 - [x] 16. `[maintainability·low]` `.../export/state/taonierDownload.ts:30` — 与 `xhsMinitoolDownload.ts` 近乎逐字重复 - 现状:保存/下载流程整文件复制了一份。 - 问题:下载行为(文件名、取消语义、错误文案)以后要改两处。 - 修复:用户确认抽共享 util。新增 `export/state/exportArtifactDownload.ts` 承载保存对话框 + `save_local_project_asset_file` + 文件管理器定位,删除 `taonierDownload.ts` / `xhsMinitoolDownload.ts`,两个 `ArtifactsPane` 改用 `saveExportArtifactToDisk` / `revealExportArtifactInManager`(`680503d2a`)。 - [x] 17. `[bug·medium]` `ArtifactsPane.tsx:118-121` — 发布按钮没把 `taonier.running` 算进禁用 - 现状:禁用条件只有 `publish.publishing`,`taonier.running` 未纳入。 - 问题:先点「打包」、构建进行中再点「打包并发布」,两端会并发触发 `packageAll`,争抢同一个 `.export/taonier.zip`。 - 修复:`publishDisabled` 增加 `taonier.running`(`7db1fc4ff`)。 - [x] 18. `[style·low]` `.../tabs/taonier/FormCard.tsx:137-138` — 字段控件嵌套三元 - 修复:`options`/`multiline`/`input` 三态先算 `inputSlot`,并复用同一份 `fieldClassName`,去掉三份重复样式(`e4d46609c`)。 - [x] 19. `[style·low]` `FormCard.tsx:215-219` — 截图数量文案嵌套三元 - 修复:三态先算 `screenshotSummary`(`bb7097ce5`)。 - [x] 20. `[bug·medium]` `FormCard.tsx:279-283` — 多选截图只取最后一张 - 现状:`onImport` 里 `const picked = assets.at(-1)`,只写回最后一张。 - 问题:`AssetImporter` 的本地文件对话框是 `multiple`(上限 100),其余截图被静默丢弃。 - 修复:按 `MAX_AGC_GAME_SCREENSHOTS - 当前张数` 的剩余名额逐张 `onAppendScreenshot`;测试替身支持返回多张,新增多选导入断言(`057b56b24`)。 - [x] 21. `[bug·high]` `game_distribution_publish.rs:1062-1068` — AGC 回读链路没跟着硬切到 objectKey - 现状:`GameDistributionPublicationImageDraft` 只序列化 `assetId`/`previewUrl`;`build_publication_draft` 拿到 `cover_object_key` / 截图 `object_key` 只用于换签预览就丢弃,新合同下 `assetId` 恒为 `None`。`FrozenPublicationMetadata.screenshots` 仍按对象数组解析,而服务端 `frozenMetadata.screenshots` 已是字符串数组。 - 问题:面板按 `objectKey ?? assetId` 取沿用键,两者都拿不到 → 沿用线上封面时 `coverObjectKey` 变空、截图槽位全 `null`,更新已上架作品会被迫重选封面/截图(或报「请先选择游戏封面」)。 - 修复:用户确认做。`GameDistributionPublicationImageDraft` 增加 `object_key: Option<String>` 并按 objectKey 序列化;`FrozenPublicationMetadata.screenshots` 从 `Vec<FrozenPublicationScreenshot>` 改为 `Vec<String>`,删除该结构体;`build_publication_draft` 透传 `normalize_object_key(cover_object_key)` 与字符串截图数组;前端 `GameDistributionPublicationImageDraft` 同步为 `objectKey`;补 Rust 断言 `frozen_metadata_accepts_string_screenshot_array`(`800c45680`)。 - [x] 22. `[bug·high]` `server-rs/.../modules/game_distribution.rs:1859-1861` — 三条 multipart 路由缺 `DefaultBodyLimit` - 现状:`create_game` / `create_version` / `update_owner_game_metadata` 收 multipart(1 封面 + 最多 6 截图 × 8MiB),路由都没有显式 `DefaultBodyLimit`。 - 问题:Axum 默认 2MiB,真实带图发布会在 handler 之前就被 413。 - 修复:新增 `MAX_PUBLISH_MULTIPART_BYTES = 8MiB × 7 + 2MiB` 并给三条路由加 `.layer(DefaultBodyLimit::max(...))`;`MAX_SINGLE_MEDIA_BYTES` 提为 `pub(crate)`,补口径断言(`d24a16ad0`)。 - [x] 23. `[bug·high]` `game_distribution.rs:3749-3750` — 旧 `screenshots_json` 对象数组按 `Vec<String>` 解析失败 - 现状:`serde_json::from_str::<Vec<String>>` 对旧 `[{"assetId","objectKey"}]` 返回 Err,`unwrap_or_default()` 静默变空列表。 - 问题:若线上确有旧行,已发布作品的截图不显示,作者沿用旧槽位也会被拒。 - 结论:用户确认「未上线、暂无数据」,不修。本迁移按计划/决策日志是「硬切、无历史数据、不保留旧 JSON 兼容」;上线前若引入旧行,应先做一次性数据迁移而不是长期兼容解析。 - [x] 24. `[bug·medium]` `game_distribution.rs:1868-1877` — 先上传 OSS 再写 SpacetimeDB,重放/冲突留孤儿对象 - 现状:媒体带新 UUID 先上传,随后事务里按幂等键去重或校验 revision。 - 问题:幂等重放或 revision 冲突时,新上传对象无人引用且没有清理路径。 - 修复:用户确认「先在事务/收据层判重与校验 revision 再上传」。更新资料路径在 `resolve_publish_media` 之前先按刚读到的游戏行做 revision CAS,过期直接返回同一 409 `PUBLICATION_CONFLICT` 口径;`resolve_publish_media` 拆出 inner 并记录本次新传 objectKey,中途失败即补偿删除;事务失败或幂等重放(`replayed=true`)时 `cleanup_uncommitted_publish_media` 调 `delete_media_objects` 清理孤儿。说明:幂等收据只在 reducer 事务内可读,api-server 无法上传前查收据,故重放改为事后补偿,效果同样是不留孤儿(`7aeac536e`)。 - [x] 25. `[security·medium]` `game_distribution_media.rs:147-149` — 内容类型完全信客户端 multipart 头 - 现状:只检查 `starts_with("image/")`,`image/svg+xml` 可通过并按该类型存储;bytes 端点回显存储类型,没有 `nosniff`/attachment。 - 问题:带脚本的 SVG 可能被同源当主动文档执行。 - 修复:用户确认做。`read_image_field` 改按文件头嗅探 PNG/JPEG/WebP/GIF/AVIF,声明为 SVG 等主动内容一律拒绝,落桶 Content-Type 与扩展名以嗅探结果为准;`proxy_read_bytes` 新增 `ObjectReadContentPolicy`,游戏媒体只放行栅格类型、其余降级 `application/octet-stream` + `Content-Disposition: attachment`,并统一加 `X-Content-Type-Options: nosniff`(`7aeac536e`)。 - [x] 26. `[performance·low]` `server-rs/.../api-server/src/object_read.rs:70-74` — 每次 read-bytes 都新建 `reqwest::Client` - 问题:丢连接池、且没设超时;该代理已被素材与游戏媒体两条读路径共用。 - 修复:用户确认做。`proxy_read_bytes` 改为接收调用方传入的共享 client,素材域与游戏媒体域都传 `state.editor_oss_http_client()`(进程级复用,connect 10s / total 120s),不再每次 `reqwest::Client::new()`(`7aeac536e`)。 - [x] 27. `[performance·low]` `game_distribution_media.rs:133` — `upload_media_image` 克隆整段最多 8MiB 字节 - 现状:摘要已在 `resolve_publish_media` 之前算完,上传仍借用并 clone。 - 修复:用户确认做。`upload_media_image` 改为取 `UploadedImage` 所有权,`body: image.bytes` 直接移交,去掉整段 `clone()`(`7aeac536e`)。 - [x] 28. `[bug·high]` `spacetime-module/.../game_distribution.rs:4156` — `GameDistributionFrozenMetadata::screenshots` 从对象数组改为 `Vec<String>` - 问题:旧 `metadata_json` 会在 `parse_game_distribution_frozen_metadata` 反序列化失败,审核通过时重新解析旧版本会报「版本冻结资料格式无效」。 - 结论:同 23,用户确认「未上线、暂无数据」,不修;上线前若有 pending 旧版本,需先迁移 `metadata_json` 而不是保留旧形状兼容。 - [x] 29. `[bug·high]` `spacetime-module/.../game_distribution.rs:4225` — 旧 `screenshots_json` 行按 `Vec<String>` 解析 `.ok()` 成 `None` - 问题:已发布作品截图不再获得匿名读授权(封面因 `cover_object_key` 本就是字符串不受影响)。 - 结论:同 23/28,用户确认未上线无旧数据,不修;若有旧行需一次性迁移。 - [x] 30. `[performance·low]` `spacetime-module/.../game_distribution.rs:4216` — `game_distribution_media_has_public_read_grant` 每次媒体读都 O(n) 扫全表 - 现状:公开未鉴权路径上逐行反序列化 `screenshots_json`。 - 修复:用户确认做,采用不改 schema 的派生查找:媒体 key 形如 `{prefix}{game_id}/{file}`,先按 `GAME_DISTRIBUTION_MEDIA_PREFIX` 反解 `game_id`,再按主键 `.game_id().find(...)` 只读那一行核对封面/截图归属,降为 O(1);同时把该前缀抽到 `shared-contracts` 供写路径与授权端共用同一常量,补 key 布局单测(`08771d39e`)。 - [x] 31. `[maintainability·medium]` `scripts/check-game-distribution-media-e2e.mjs:129-131` — `publishFormData`/`imagePart` 在多个 E2E 脚本重复 - 修复:用户确认做。新增 `scripts/game-distribution-e2e-helpers.mjs`(`COVER_PNG` / `imagePart` / `coverImagePart` / `publishFormData` / `resolveApiRequest`),11 个 E2E 脚本删除各自的重复实现改为 import,multipart 字段名与 `api()` 的 FormData 分支只留一份(`1f239a96c`)。 - [x] 32. `[security·medium]` `.../taonier/build.rs:186-190` — 符号链接产物仍被完整读入 - 现状:`std::fs::read(&path)` 发生在 `produced` 门禁之前,而 `produced=false` 恰恰可能是「这是符号链接」。 - 问题:被拒的符号链接目标仍被无上限读进内存。 - 修复:同 11,读取前先按 `produced` 与 `metadata.len()` 双重门禁(`d3d7e5a8c`)。 - [x] 33. `[bug·medium]` `scripts/check-game-distribution-validation-restart-e2e.mjs:241-244` — 创建失败后仍解引用 `created.data.coverObjectKey` - 现状:`check()` 只记失败不中断,`created.data` 为空时下一行直接取属性。 - 问题:抛未捕获 `TypeError` 打崩整个脚本,拿不到正常的 check 结论。 - 修复:`check` 后补 `if (!gameId) process.exit(1);`(`8e59e11f0`)。 - [x] 34. `[bug·medium]` `scripts/check-game-distribution-web-a11y-e2e.mjs:226` — 同上,且在 `publishFormData` 实参里提前解引用 - 修复:`const gameId = created.data?.id` 后补 `if (!gameId) process.exit(1);`(`152cdc617`)。 - [x] 35. `[bug·low]` `scripts/check-game-distribution-validation-restart-e2e.mjs:438` — 旧 `E2E_STATE_FILE` 只有 `coverAssetId` - 问题:`state.coverObjectKey` 为 `undefined` 会被 `JSON.stringify` 丢掉,续跑创建的 v3 没有封面。 - 修复:读封面键时回退 `state.coverObjectKey ?? state.coverAssetId`(`9c822c098`)。 - [x] 36. `[maintainability·low]` `scripts/check-game-distribution-upload-safety.mjs:125` — 同一批 multipart helper 重复(≥11 份) - 修复:同 31,已收纳进 `scripts/game-distribution-e2e-helpers.mjs`(`1f239a96c`)。 - [x] 37. `[maintainability·low]` `scripts/check-game-distribution-media-e2e.mjs:650-662` — 断言硬编码内部存储前缀 `agc/project-snapshots/v1/game-distribution/media/` - 问题:这是服务端内部布局常量,不是 wire 契约;bucket 布局调整会让契约仍有效的 E2E 失败。 - 修复:用户确认做。断言放宽为结构校验:非空、去空白、含路径分隔符、不是客户端 part 文件名;不再写死内部前缀(`009a772eb`)。 - [x] 38. `[test·low]` `scripts/check-game-distribution-ops-rollback-e2e.mjs:537` — 封禁建游请求同时带 objectKey 与 cover 字节 - 问题:payload 语义含糊,与套件其余「新图」写法(`coverObjectKey: null` + `cover` part)不一致。 - 修复:改为 `publishFormData({ ...resolvedMetadata, coverObjectKey: null }, { cover })`(`9184e9277`)。 - [x] 39. `[bug·high]` `src/components/game-distribution/gamePublishMediaDefaults.ts:43-45` — 旧 `{assetId,objectKey}` 截图数组会让 `objectKey?.trim()` 抛错 - 现状:`screenshotSource` 声明为 `string[]`,旧数据元素是对象。 - 问题:若线上存在旧形状,`applyVersionMediaDefaults` 会 `TypeError` 中断,版本续做失败且在线默认值被静默丢弃。 - 结论:同 23/28/29,用户确认未上线无旧数据,不修。若将来确有旧行,应先服务端迁移;如仍想防御,可在这里按 `typeof item === 'string' ? item : item?.objectKey` 归一。 - [x] 40. `[performance·medium]` `gamePublishAssets.ts:83-85` — blob 预览 URL 只在显式移除/替换时 revoke - 现状:`applyVersionMediaDefaults` 覆盖 `coverImage`/`screenshotImages`、以及组件卸载时,旧 blob URL 没有 `URL.revokeObjectURL`。 - 问题:浏览器内存泄漏。 - 修复:用户确认做。`applyVersionMediaDefaults` 在用线上默认值覆盖前先 revoke 被替换的本地预览;`GamePublishPage` / `GameWorkMetadataEditor` 用 ref 跟踪最新图片,卸载时兜底 revoke;补「覆盖前 revoke」断言测试(`5ce5cb006`)。 - [x] 41. `[performance·low]` `GamePublishMediaFieldset.tsx:185` — key 退化成 `${name}-${index}` - 现状:本地图 `objectKey` 为 undefined,key 用 `name-index`。 - 问题:截图删除/重排时 index 位移,React 会重建节点;同名文件也只能靠 index 区分。 - 修复:`GamePublishImage` 新增稳定 `id`(`createGamePublishImageId`),本地图与线上沿用图各自铸一次,列表 key 改用 `image.id`,补同名文件 id 互异单测(`12d4e8477`)。 - [x] 42. `[maintainability·low]` `GamePublishPage.tsx:310-315` — 槽位映射与 File part 组装在发布页/编辑页重复 - 修复:用户确认做。`gamePublishAssets` 新增 `buildGamePublishMediaSubmission`,统一「槽位 objectKey 去空白 / 本地新图只进 parts」;`GamePublishPage` 的 metadata 槽位与 multipart part、`GameWorkMetadataEditor` 的 payload/media 都改用它,补槽位与 part 同源单测(`6f0ea0310`)。 - [x] 43. `[bug·medium]` `.../export/state/useTaonierExport.ts:548-549` — 保存失败先无条件 `setDirty(false)` - 现状:catch 里先清 `dirty`,再按失败类型分流。 - 问题:瞬时 `exportUnavailable` 会丢掉未落盘编辑:`dirty=false` 让 `adoptableRef` 变真,宿主恢复后的重读会用陈旧注册表表单覆盖用户最新输入。 - 修复:分流后再清——`saveConflict`/未识别/其余失败清 `dirty`,`exportUnavailable` 保持 `dirty`(`075078f35`)。 - [x] 44. `[bug·medium]` `useTaonierExport.ts:381-384` — 采纳新表单时无条件 `setFailure(null)` - 现状:函数式清理(只清读来源失败)在上面,采纳路径又无条件清一次,和注释「构建类失败不在这里清」矛盾。 - 问题:面板后台注册表被改写时,下一次成功重读会把正在展示的构建失败弹窗(`commandFailed`/`artifactMissing` 等)静默 dismiss。 - 修复:删掉这次无条件清空,保留上方只清读来源失败的函数式清理;补「构建失败 + 重读采纳新表单」测试(`00448e0b2`)。 - [x] 45. `[bug·low]` `.../export/state/taonierFields.ts:100-101` — 面板按码点、发布预检按 UTF-16 计数 - 现状:`validateTaonierForm` 用 `Array.from(trimmed).length`(码点),`gameDistributionPublish.ts:normalizeMetadata` 仍用 `title.length > 40` / `summary.length > 120`(UTF-16 码元)。 - 问题:含 emoji 的压线标题/简介能过面板,却会在发布预检处被误拒;服务端用的是 `chars().count()`(码点)。 - 修复:`normalizeMetadata` 改用 `Array.from(...).length`,与面板和服务端同口径;补 40 emoji 标题 + 120 emoji 简介测试(`42c742df5`)。 - [x] 46. `[bug·medium]` `server-rs/.../modules/game_distribution.rs:1721` — `update_owner_game_metadata` 幂等摘要不含 `game_id` - 现状:摘要只哈希 `payload`,而幂等收据键是 `(owner_user_id, action, idempotency_key)`,不按游戏隔离;`create_version` 已经用 `(game_id, &payload)`。 - 问题:同一作者对两部作品用同一个 `Idempotency-Key` 推送相同资料时,第二次会被当成第一次的重放:跳过写入、返回第二部作品的旧状态(`replayed=true`),更新被静默吞掉。 - 修复:摘要改为 `(game_id.as_str(), &payload)`;补跨作品摘要区分断言(`fede8cc16`)。 - [x] 47. `[bug·medium]` `src/components/game-distribution/GameDetailPage.tsx:121` — 作者视角媒体换签走了匿名公开路由 - 现状:`GameDetailResolvedAsset` 只用 `useGameDistributionMediaReadUrl(objectKey)`,不带 `gameId`。 - 问题:公开读只服务 `published` + `active` 作品;作者预览自己的未发布/待审/驳回作品时,封面/截图静默退回占位(项目记忆里记过的「作者预览必须走 owner 路由」坑)。 - 修复:组件加可选 `gameId`,`renderAsset` 传 `ownerView ? game.id : undefined`;测试替身记录换签选项,补作者视角带 `gameId` 断言(`067f02c85`)。 - [x] 1. 【.gitattributes:33】现状:文件末尾同时存在两条完全相同的 `apps/ai-game-creator-shell/src/view/project-development/export/generated/** ...` 规则(原第 33、35 行)。问题:重复规则无功能影响,但污染文件、误导后续编辑。修复:删除重复行。commit `bff0006eb`。 - [x] 3. 【vite.config.taonier.mjs:55】现状:`assertH5Root` 只用 `/\s(?:src|href)="(\/[^"]*)"/` 匹配双引号属性。问题:单引号属性(`src='/app.js'`)与 `=` 两侧空白漏检,误配 base 的产物可能通过校验、上传后整站 404(Vite 一般输出双引号,但手写/插件 HTML 会漏)。修复:正则改为 `/\s(?:src|href)\s*=\s*(["'])(\/[^"']*)\1/`。commit `9c2664289`。 - [x] 4. 【vite.config.taonier.mjs:130】现状:`defineTaonierConfig` 先 `mergeConfig(baseConfig, overrides)`,再补插件;Vite 的 `mergeConfig` 会用 overrides 的标量覆盖 `build.outDir`。问题:overrides 改了 `outDir` 后,`pack.mjs` 的 `--vite-built-dir` 默认仍是 `dist-taonier`,会打包陈旧/缺失目录且无提示。修复:合并后重新钉死 `merged.build.outDir = TAONIER_OUT_DIR`。commit `af752eaa1`。 - [x] 5. 【pack.mjs:162】现状:`writeZipEntries` 直接 `openSync(zipPath, 'w')` 截断落点。问题:写盘中途失败(磁盘满、4 GiB 上限、源文件被删)会进入只删落点的 catch,把落点上原有合法 zip 一起删掉。修复:写同目录临时文件 `<zip>.tmp-<pid>`,成功后 `renameSync` 覆盖;失败只清理临时文件,`makeZip` 失败路径不再删落点。commit `5b0c41683`。 - [x] 6. 【pack.mjs:126】现状:`collectZipEntries` 只处理 `isDirectory()` 与 `isFile()`。问题:FIFO/socket/设备文件既非目录也非普通文件,`assertNoSymlinks` 也不拦,会被静默跳过,产出缺文件的包。修复:其余类型直接抛错。commit `bda1bb081`。 - [x] 7. 【preview.rs:1111】现状:导出目录从 `exports` 改名为 `.export` 时,预览服务的两处受保护组件列表只删了 `exports`、漏加 `.export`。问题:项目根存在 `index.html` 时,`/.export/taonier.json` 等注册表 JSON(content-type 非 octet-stream)会被本地预览直接读走。修复:两处列表补回 `.export`(`.zip` 仍因 octet-stream 被挡)。commit `5df27afed`。 - [x] 8. 【game_distribution_publish.rs:1127】现状:`read_publish_image` 先 `std::fs::metadata` 看体积,再 `std::fs::read` 按路径读。问题:两次系统调用之间路径可被换成符号链接或超长文件(TOCTOU),超长文件会先被读进内存。修复:改用 `OpenOptions` + `O_NOFOLLOW`/`FILE_FLAG_OPEN_REPARSE_POINT` 打开,取同一句柄 metadata 定上限后 `take(cap+1)` 限长读取(原 review 里的常量名 `MAX_DRAFT_MEDIA_BYTES` 不存在,实际是 `MAX_PUBLISH_MEDIA_BYTES`)。commit `951284205`。 - [x] 9. 【game_distribution_publish.rs:1267,误报】现状:`ensure_publish_project_stack` 已不存在,发布前只校验 `project.export_package` 权限并读固定 `.export/taonier.zip`。问题:这是 `b04aceb57` 有意删除的 phaser/vite 发布门,新门禁在导出/构建步骤(`prepare()` 的 `BuildScriptMissing`),计划文档 D8 与 decision-log 都有记录;按 review 恢复会退回旧产品决策。处理:无需改动。 - [x] 10. 【repository_context.rs:719】现状:`should_ignore_directory` 顶层组件只匹配 `memory | logs | .app`,`exports` 改名后漏了 `.export`。问题:`scan_repository` 会把生成的 `.export` 打包内容纳入 agent 仓库上下文。修复:匹配加入 `.export`。commit `ad0877219`。 - [x] 11. 【FormCard.tsx:14】现状:封面/截图复用小红书 tab 的 `IconPreview`,其 `alt="图标预览"`、失败 title「用『选择图标』导入…」等文案写死为「图标」。问题:封面/截图的读图失败提示对用户说的是错误对象。修复:`IconPreview` 增加可选 `labels`(缺省保持小红书文案),FormCard 的封面/截图各自传入对应 alt、title、aria。commit `cb57e59ec`。 - [x] 12. 【FormCard.tsx:274】现状:封面槽只保存一个路径,但复用了多选上限 100 的 `SPRITE_IMPORTER_SETTINGS`,`onImport` 只取 `assets.at(-1)`。问题:用户多选时其余图片被静默丢弃。修复:新增 `COVER_IMPORTER_SETTINGS`(`maxItems: 1`),封面导入器改用它(截图仍用多选设置)。commit `edaf8896e`。 - [x] 13. 【taonier/script.rs:53】现状:`find` 读取脚本命令时 `map(str::trim)`,而 `project/verification.rs` 的防漂移比较用 `actual_command != expected_command`(actual 是 package.json 原始值)。问题:脚本值带首尾空格/换行时永远判「脚本已变化」→ `exportUnavailable`。修复:比较时两边都 `trim()`(只比较脚本内容、不比较无意义空白)。commit `3a6758095`。 - [x] 15. 【taonier/build.rs:192】现状:`conclude` 里 `std::fs::read` 前只看 512 MiB 读入安全线,平台 200 MiB 上限在读完后的 `ensure_within_platform_package_limit` 才判。问题:200–512 MiB 产物会先整包读进内存再判不合规。修复:读之前同时按平台 200 MiB 上限过滤。commit `a3cf98000`。 - [x] 16. 【taonier/build.rs:262】现状:`read_artifact_package` 先用 `symlink_metadata` 拒符号链接,再 `File::open` 按路径打开 `read_package_bytes`。问题:检查与打开之间被换成同长度符号链接时仍会被跟随(TOCTOU,低危)。修复:改 `OpenOptions` + `O_NOFOLLOW`/`FILE_FLAG_OPEN_REPARSE_POINT`。commit `ba9d70a5b`。 - [x] 17. 【vite.config.taonier.mjs:68】现状:`pruneEmptyDirs` 用 `statSync` 判断目录(会跟随符号链接)。问题:链接指向产物目录之外(甚至祖先)时会递归清理/栈溢出。修复:改 `lstatSync`,符号链接一律跳过。commit `003f7cfe9`。 - [x] 18. 【game-distribution-e2e-helpers.mjs:34】现状:`coverImagePart` 与 `COVER_PNG` 被导出,但仓库内无任何脚本导入(10 个脚本各写本地 `coverImagePart`,2 个 web-publish 脚本各写本地 `COVER_PNG`)。问题:死导出误导后续维护。修复:删除 `coverImagePart`,`COVER_PNG` 收回为模块内部常量。commit `0f127dbc7`。 - [x] 19. 【useTaonierTab.ts:254】现状:hook 挂在面板根、跨项目不卸载;发布链路在 `await packageAll`/订阅回调/`await publishLocalProjectGame` 之后无条件 `setProgress`/`setPublishResult`。问题:发布 A 项目期间切到 B 项目,A 的进度/结果/失败会写进 B 的面板。修复:新增 `projectPathRef`,在各 await 之后与上传进度回调里比对当前项目再写状态。commit `5bc52a344`。 - [x] 20. 【useTaonierExport.ts:784】现状:`packageAll` 用 render 闭包里的 `form` 做本地校验;`refresh()`/flush 可能已把更新的落盘表单写进 `effectiveFormRef`。问题:`form` 可能是陈旧快照,会放行盘上的非法值,违背「校验不过不触宿主」。修复:改用 `effectiveFormRef.current ?? form`。commit `8e3f0b892`。 - [x] 21. 【test-creator-runtime.py:194】现状:fixtures 的 `game` 字典没有 `price_mud_points`。问题:该字段是非 `Option` 的 `u64`,SpacetimeDB SATS 反序列化对缺失字段报 `missing_field`;`recover_row_with_deleted_fields` 只忽略未知字段,`normalize_migration_row` 没有 `game_distribution_game` 分支,脚本会在导入 fixture 时直接失败。修复:补 `price_mud_points=0`(根因上更彻底的 `normalize_migration_row` 回填见第 21 条备注,属数据兼容,未自动做)。commit `28be35239`。 - [x] 22. 【check-game-distribution-validation-restart-e2e.mjs:413】现状:兼容旧 `STATE_FILE` 时用 `state.coverObjectKey ?? state.coverAssetId` 组装元数据。问题:旧版存的 `coverAssetId` 是素材对象 ID,不是 OSS objectKey;建版沿用要求它等于游戏当前 `cover_object_key`,因此这个兼容分支永远 400。修复:缺 `coverObjectKey` 时先 GET 游戏详情取回真实 `coverObjectKey`。commit `11c8c2789`。 - [x] 23. 【taonier/commands.rs:171】现状:`run_build` 在 async 函数里直接调 `project_root`(同步文件 IO,Windows 上还会触发 UAC/ACL 修复)。问题:占着 Tokio worker,拖慢同 runtime 的其它异步任务;同模块其它 IO 都走了 `offload`。修复:`project_root` 挪进 `offload`(spawn_blocking)。commit `2ab7717a2`。 - [x] 24. 【OnlineValuesCard.tsx:113,已过时】现状:该文件已在 `9a653c838`(你的提交)中删除,线上值内联进 FormCard 字段行,仓库内无引用。处理:finding 已不适用,无需改动。 - [x] 25. 【check-game-distribution-purchase-e2e.mjs:1470】现状:定价边界用例建游失败时,`boundsGameId` 为 undefined 仍继续请求 `/my-games/undefined`、`/games/undefined/versions`。问题:级联报错掩盖根因。修复:记录失败后 `if (!boundsGameId) process.exit(1)`。commit `3a0d27574`。 - [x] 26. 【useTaonierExport.ts:727】现状:`buildOnce` 只 `setFailure(null)`,不清 `dismissedFailure`;`visibleFailure` 用 JSON 指纹过滤已忽略的失败。问题:点「重新构建」时同一条确定性失败(如固定路径的 `artifactMissing`)会被永久滤掉,弹窗不再出现。修复:`setFailure(null)` 前 `setDismissedFailure('')`。commit `86649e50a`。 - [x] 28. 【gamePublishMediaDefaults.ts:78】现状:截图换签用 `for ... await` 串行请求(最多 6 次往返)。问题:预览出现慢。修复:改 `Promise.all` 并发请求后一次性回填。commit `5e5bb19a6`。 - [x] 29. 【useGameDistributionMediaReadUrl.ts:43】现状:effect 只用 `cancelled` 标志忽略结果,不取消在途 GET;两个 client 函数也不收 `AbortSignal`。问题:objectKey/gameId 快速切换时旧请求照跑,白占 API 配额。修复:两个 client 函数与 `resolveGamePublishImagePreview` 增加可选 `signal`,hook 在清理时 `controller.abort()`。commit `e830034d0`。 - [x] 30. 【gameDistributionClient.ts:408】现状:`readGameDistributionMediaSignedUrl` 直接 `payload.read`;`requestJson` 对空响应体会返回 `null as T`。问题:空响应触发原始 TypeError,而不是既有的「游戏媒体访问地址缺失」。修复:`payload?.read ?? payload` 与 `read?.signedUrl`。commit `3f211f12d`。 - [x] 31. 【api-server game_distribution.rs:2060】现状:`create_game` 先 `resolve_publish_media(...).await?` 上传封面/截图,再由 `cleanup_uncommitted_publish_media` 兜底清理;`normalize_local_project_id(...)?` 写在 `create_game_distribution_game` 实参里、在 cleanup 之前求值。问题:用户传入非法 `localProjectId` 时提前 `?` 返回,已上传对象成为孤儿。修复:把 `normalize_local_project_id` 提到 `resolve_publish_media` 之前。commit `f4582fe5b`。 - [x] 32. 【api-server game_distribution.rs:2139】现状:`create_version` 同样的顺序问题。问题:同上,上传后提前返回留下孤儿对象。修复:同样提前校验。commit `f4582fe5b`。 - [x] 37. 【gamePublishMediaDefaults.ts:80】现状:取消后 `if (apply.isCancelled() || !preview) continue;` 仍会推进循环、继续发剩余截图请求。问题:取消语义失效。修复:并发版本在 `Promise.all` 后统一 `if (apply.isCancelled()) return;`,取消后不再补写也不再发请求。commit `5e5bb19a6`。 - [x] 39. 【GamePublishPage.tsx:335】现状:`buildPublishGameMetadata()` 与 `buildPublishMediaParts()` 各自调用一次 `buildGamePublishMediaSubmission`。问题:当前纯函数结果一致,但「单一来源」只是口头约束,未来改动可能让元数据槽位与上传 part 分叉。修复:两个 builder 改为接收调用方算好的 `submission`,两处提交流程各算一次。commit `53750ac31`。 ## 二、逐条处理结果(按你的决定) - [x] 2. 【pack.mjs:175】现状:每个文件 `readFileSync` 整读 + `deflateRawSync` 产出一份压缩副本,峰值约 2× 文件大小;注释宣称的「流式」只在归档层面成立。问题:大型 H5 资源(音频/贴图/WASM)可能顶爆内存或导出中断。处理:按你的决定不重写,已在整读+压缩处补 `TODO(perf)`,说明峰值与 ZIP data descriptor(通用标志位 bit 3)流式改法。commit `7d8b966d1`(含 agc-skills manifest 版本/哈希同步)。 - [x] 14. 【taonier/build.rs:86】现状:`run_project_verification_with_commit_at` 的项目侧预检失败(不支持的包管理器、项目 `.npmrc`、脚本超长/含换行、脚本漂移、脚本缺失)过去和沙箱/日志故障一起返回 `String`,被 `map_err(TaonierExportUnavailable::new)?` 一律收成 `exportUnavailable`。问题:项目事实问题被当成宿主故障,用户被引导去重试一个只有改项目才能解决的问题。修复:`verification.rs` 拆出「按已解析规格运行」的入口;陶泥儿与小红书构建先单独跑项目侧预检,失败映射为新变体 `projectUnavailable`。前端把该变体归为 code agent 可修、文案写死,ts-rs 绑定与测试同步更新。commit `06e6cf852`(陶泥儿测试用例的两行覆盖并入了你当时的并发提交 `61acaf6ce`)。 - [x] 27. 【spacetime-module game_distribution.rs:4249】现状:公开读授权用 `serde_json::from_str::<Vec<String>>` 解析 `screenshots_json`,解析失败 `.ok()` 静默变 `None`;旧格式(master `4a2b27071` 写出的 `[{"assetId","objectKey"}]`)会解析失败,老作品截图匿名读 404。处理:按你的决定不修——线上还没有这类数据,暂无兼容压力。等真有旧数据时再按持久化兼容流程加双形状 helper 或做一次性行重写。 - [ ] 33. 【GamePublishPage.tsx:517】要不要修、怎么修由你定。现状(大白话):首次发布新游戏时,前端先把封面/截图上传一遍(发生在「建作品」这一步),紧接着「建第一个版本」时又把同一批图重新上传了一遍;同一张图因此在存储里存在两份,服务端记录的是第二份,第一份不再被任何地方引用、也不会被自动清掉。问题:每次首次发布都会多留一份没人用的图片,长期占存储,排查时也容易误会。为什么不能简单地把第二次上传删掉:「建作品」那一步交给「建版本」的资料里,图片位置已经被填成「用线上的」(空值);如果建版本时不再带图,会被判定成没有图而校验失败。建议做法(会改行为):建作品成功后,用它的返回结果里两张图的实际地址,去组装「建版本」要用的资料,让建版本走「沿用已有图」、不再带本次上传;同时核对服务端「沿用」的判定,并改发布流程的自动化测试。属行为变更,本次未做。 - [x] 34. 【spacetime-module game_distribution.rs:4154】现状:`GameDistributionFrozenMetadata.screenshots` 已是 `Vec<String>`,旧 `metadata_json` 里存对象数组时,管理员审批这类历史待审版本会直接失败。处理:按你的决定不修——线上还没有这类数据。等真有历史待审版本时,再给该字段加兼容对象数组的反序列化并补测试。 - [x] 35. 【api-server game_distribution.rs:3911】现状:`game_payload`、`game_media_object_keys`(292-294)与 `resolve_publish_media_inner` 的 `current_screenshots`(4105-4106)都用 `Vec<String>` 解析截图,旧格式下会丢图 / 作者读不到 / 「沿用截图」永远不匹配。处理:按你的决定不修——线上还没有这类数据。将来若出现旧格式,与第 27 条一起用同一个双形状 helper 替换这三处。 - [x] 36. 【api-server game_distribution_media.rs:178】现状:`field.bytes().await` 把整个 multipart part 收成一个 `Bytes`,随后 `bytes.to_vec()` 再完整复制一份,峰值约 2× part;单个超大 part 会被完整缓冲后才 413。处理:按你的决定不改写,已在读取处补 `TODO(perf)`,说明峰值与 `field.chunk()` 循环流式限长改法。commit `0119a329a`。 - [x] 38. 【gamePublishMediaDefaults.ts:51】现状:`revokeGamePublishImagePreview`(`URL.revokeObjectURL`)原先在 `setCover`/`setScreenshots` 的 updater 里执行。问题:副作用写进 state updater,StrictMode 双调用/重放可能撤销仍被渲染引用的 blob URL,updater 也不再是纯计算。修复:`VersionMediaApplyTarget` 增加 `getCover`/`getScreenshots`,在 dispatch 之前先 revoke;两个调用点用各自已有的 ref 提供当前媒体,updater 只做纯计算。commit `dca848103`。
k88936 added 46 commits 2026-10-07 13:02:17 +08:00
- 从 5e8ecaa35^ 取回被 M4 删除的【实施计划】与【里程碑】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md
- 本次起不再按计划 §5.4 在收尾删除计划文档,保留理由记录在 decision-log
- 新增 GET /api/game-distribution/my-games/{gameId}/media/read-url 与 .../media/read-bytes,需 bearer
- 判定 = gameId 属于当前登录主体且 objectKey 命中该作品当前行 cover_object_key/screenshots_json
- 抽出 game_media_object_keys 与换签/字节中转共用函数,公开读判定与响应形状不变
- 新增媒体键提取单测与 owner 路由挂载/鉴权断言
- 待审期间断言作者本人可经 owner 路由预览自己的封面
- 断言匿名走 owner 路由被拒(401)、非本作品媒体的 objectKey 换不出来(404)
- resolve_preview_url 改走 /api/game-distribution/my-games/{gameId}/media/read-url,不再走素材库 /api/assets/read-url
- 新增 owner_media_read_url,复用 endpoint 的路径段转义并编码 objectKey
- 新增路由与编码单测,钉住不再回退素材库 ACL
- gameDistributionClient 新增 getOwnerGameDistributionMediaReadUrl,参数与换签解析与公开读共用
- useGameDistributionMediaReadUrl 支持 gameId;GameCover 增 owner 开关,MyGamesPage 封面走 owner
- resolveGamePublishImagePreview 接 gameId,发布/编辑回填未发布作品的封面与截图也能换签
- 补 client 路由/鉴权测试与 MyGamesPage owner 换签测试
- 后端契约/玩法链路/AGC 实施计划补 owner 作用域读路由与作者预览口径(软删仍 404)
- decision-log 2026-10-06 条目记录 owner 读决策,并改「临时计划已删除」为保留计划与里程碑文档
- pitfalls 新增作者预览必须走 owner 读、不能回退素材库 ACL 的排障口径
- 两份文档 Status 由「待评审」改为「已实施(M1–M4 已落地)」
- 实施计划头部注明长期保留,原 §5.4「收尾删除计划文档」不再执行
- 新增 tests/memoryStorage.ts 导出 createMemoryStorage,统一 length/key 语义
- jsdomLocalStorageSetup 全局兜底、appSurface/harness、chatPromptPolish 改为导入同一实现
- chatPromptPolish 断言由 Map.size 改为 Storage.length,语义不变
- normalizeMetadata 不再 filter 掉空串槽位,改为归一成 null(消费草稿本地图)
- 否则后续槽位整体前移,与 .export/taonier.json 的截图顺序错位
- 更新定向测试:空串槽位保留为 null
- metadata_digest 逐张先写 u64 长度再写字节,分片边界进入摘要
- 否则「封面 [1,2]+截图 [3]」与「封面 [1]+截图 [2,3]」摘要相同,本地幂等账本会误判重放
- 补分片不变性断言
- read_draft_image 先看 metadata.len(),超过 8MB(对齐服务端单图上限)直接返回错误
- 避免草稿指向超大本地文件时先把宿主内存读满,再由服务端事后拒绝
- 补稀疏大文件定向测试
- 新增 multipart_image_file_name,JPEG/WebP/GIF/AVIF 不再统一叫 .png
- 服务端仍按 MIME part 判定,文件名只作为未来消费者可依赖的事实保持一致
- 补扩展名映射单测
- publishLabel 改为 if/else 三态赋值,去掉「正在发布/更新/首次发布」的嵌套三元
- 行为不变,仅满足项目禁止嵌套三元的约定
- `online?.nextVersionNumber == null` 改为 `!online || online.nextVersionNumber === null`
- 去掉宽松相等,同时显式挡掉 online 为空的形态
- 「打包并发布」与「打包」共用禁用判据,构建进行中不能再并发触发 packageAll
- 避免两次构建争抢同一个 .export/taonier.zip 并互相覆盖结果
- 模板字符串里的版本号后缀与已发布/未发布两态改为先算 onlineSummary 变量
- 行为不变,去掉嵌套三元
- 截图字段走数组分支,文本字段走字符串分支,不再用 as never 关掉类型检查
- 表单将来加入非 string/string[] 字段时这里会当场编译失败
- conclude 仅在 produced(普通文件、非链接、非空)时才读取产物
- 读取前先看 metadata.len(),超过 ARTIFACT_PACKAGE_READ_MAX_BYTES 直接判不合格
- 避免构建脚本写出超大 zip 时先把宿主内存读满,也堵住符号链接被整包读取
- read_package_bytes 接收调用方已验证的 expected_bytes,用 Vec::with_capacity
- 接近 200 MiB 的包不再因反复扩容多复制约一倍数据
- `online.nextVersionNumber == null` 改为 `=== null`,去掉宽松相等
- online 为空已由 `!online` 显式挡掉,语义不变
- packageAll 在宿主不可用/读失败时会直接抛,await 包一层 try/catch
- 失败时把进度置为 failed 并带上原因,不再停在 running(运行中对话框没有关闭入口)
- options/multiline/input 三态改为 if/else 生成 inputSlot
- 复用一个 fieldClassName,去掉三份重复的边框/错误态样式与嵌套三元
- 本地张数 / 整组沿用线上 / 还没有截图三态先算成 screenshotSummary
- 行为不变,去掉嵌套三元
- AssetImporter 是多选(上限 100),onImport 改为遍历 assets 按 6 张上限截断
- 一次选多张时不再静默丢弃除最后一张外的截图
- 补多选导入定向测试
- create_game / create_version / update_owner_game_metadata 三条 multipart 路由补 DefaultBodyLimit
- 放行封面 + 最多 6 张截图 + metadata/boundary 余量,否则 Axum 默认 2 MiB 会在抽取前 413
- MAX_SINGLE_MEDIA_BYTES 提升为 pub(crate),补口径断言
- update_owner_game_metadata 的 request_digest 与 create_version 同口径,带上 game_id 作用域
- 幂等账本按 (owner, action, key) 归档不含 game_id,摘要不带作用域会把另一部作品的同资料更新当成重放
- 补跨作品摘要区分断言
- 采纳路径去掉无条件 setFailure(null),只保留上方「读来源失败」的函数式清理
- 面板后台注册表被改写时,构建失败弹窗不再被下一次成功重读顺手抹掉
- 补「构建失败 + 重读采纳新表单」定向测试
- 保存失败分流后不再无条件 setDirty(false):exportUnavailable 分支保留 dirty
- dirty 为真时 adoptable 保持 false,宿主恢复后的重读不会用旧注册表表单覆盖用户输入
- 冲突/未知失败仍归零 dirty,维持「同一份输入不反复打宿主」
- normalizeMetadata 用 Array.from(...).length,与面板校验及服务端 chars().count() 同口径
- 含 emoji 的压线标题/简介不再因 UTF-16 码元长度被发布预检误拒
- 补 40 个 emoji 标题 + 120 个 emoji 简介的定向测试
- GameDetailResolvedAsset 新增 gameId 入参,ownerView 时传 game.id
- 公开读只服务已发布 + active 作品,未公开/待审/驳回的作者预览此前只会静默回占位
- 测试替身记录换签选项,补作者视角带 gameId 的定向断言
- 在 check('创建游戏成功') 后补 if (!gameId) process.exit(1)
- check 只记失败不中断,created.data 为空时继续取 coverObjectKey 会抛未捕获 TypeError 打崩脚本
- runResume 读封面键时回退 state.coverAssetId
- 旧版状态文件没有 coverObjectKey,直接读会得到 undefined 且被 JSON.stringify 丢掉,v3 会缺封面
- 在 gameId 取值后补 if (!gameId) process.exit(1)
- 创建失败时 created.data 为空,提前取 coverObjectKey 会抛未捕获 TypeError 打崩脚本
- 带二进制 cover part 时把 metadata.coverObjectKey 显式置 null,与套件其余「新图」语义一致
- 避免同一请求同时携带 objectKey 与 cover 字节,在网关检查顺序变化时结论漂移
- GamePublishImage 新增 id,本地新选图与线上沿用图各自铸一次
- 截图列表 key 改用 image.id,删除/重排时不再因 index 位移重建节点,同名文件也能区分
- 补同名本地图 id 互异的单测
- Rust 命令与 stage_publish_package 去掉该形参,整包固定读 .export/taonier.zip
- 前端服务类型、invoke 载荷与 useTaonierTab 调用同步删除
- 更新单测与 live 测试,去掉对已忽略形参的断言
- GameDistributionPublicationImageDraft 增加 objectKey,封面/截图回读时带上沿用键
- FrozenPublicationMetadata.screenshots 改按字符串数组解析,删除只认 {assetId,objectKey} 的旧结构
- 前端草稿类型补 objectKey;补字符串数组解析的 Rust 单测
- useTaonierExport 维护 effectiveFormRef,refresh 采纳新表单与打包前 flush 成功时同步
- 暴露 getEffectiveForm;publishGame 在 await packageAll() 之后用它,避免旧快照与宿主读盘不一致
- 补两条单测:采纳新表单后权威表单更新、未保存输入先落盘再作为权威表单
- 新增 scripts/game-distribution-e2e-helpers.mjs:COVER_PNG、imagePart、coverImagePart、publishFormData、resolveApiRequest
- 11 个 E2E 脚本删除各自的重复实现,改为 import;api() 的 body 分支收敛到 resolveApiRequest
- 各脚本的封面文件名前缀、envelope 与鉴权头合并顺序、返回形状保持不变
- 「公开投影只暴露服务端派生的媒体 objectKey」改为结构断言:非空去空白、含路径分隔符、不是客户端 part 文件名
- 不再断言 agc/project-snapshots/v1/game-distribution/media/ 这一内部布局常量
- 与服务端返回键的一致性仍由相邻的 owner/公开投影相等断言覆盖
- applyVersionMediaDefaults 用线上默认值覆盖前先 revoke 被替换的本地预览
- GamePublishPage / GameWorkMetadataEditor 用 ref 跟踪最新图片,卸载时兜底 revoke
- 补「线上默认值覆盖本地选择」的 revoke 断言测试
- gamePublishAssets 新增媒体提交合成:槽位 objectKey 去空白、本地新图只进 parts
- GamePublishPage 的 metadata 槽位与 multipart part、GameWorkMetadataEditor 的 payload/media 都改用它
- 补槽位与 part 同源的单测
- 新增 exportArtifactDownload.ts,承载保存对话框 + save_local_project_asset_file + 文件管理器定位
- 删掉逐字重复的 taonierDownload.ts 与 xhsMinitoolDownload.ts
- 两个 ArtifactsPane 改用通用的 saveExportArtifactToDisk / revealExportArtifactInManager
- 图片 part 改按文件头嗅探 PNG/JPEG/WebP/GIF/AVIF,声明 image/svg+xml 之类主动内容一律拒绝
- 同源字节中转新增内容策略:游戏媒体只放行栅格类型,其余降级为 attachment,并统一加 nosniff
- 同源字节中转复用进程级 OSS HTTP 客户端,不再每次 reqwest::Client::new(),连接复用与超时一并生效
- upload_media_image 取 UploadedImage 所有权,去掉最多 8 MiB 的整段克隆
- 更新资料先按刚读到的游戏行做 revision CAS,再决定是否写图;事务失败或幂等重放时补偿删除本次新传对象
- 媒体内部前缀抽到 shared-contracts,供写路径与只读授权端共用同一常量
- 从 GAME_DISTRIBUTION_MEDIA_PREFIX 后的首段反解 game_id,按主键读那一行核对媒体归属
- 不再逐行扫描全部游戏及其 screenshots_json
- 补充 key 布局(非本前缀、缺 gameId、缺文件名、多级路径)的单测
合并 origin/master 到 feat/adapt-taonier-skill:保留陶泥儿退役链路并吸收 master 错误详情改动
Project CI / Backend tests (pull_request) Failing after 1m12s
Project CI / Native shell tests (pull_request) Failing after 3m48s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m39s
Project CI / Repository checks (pull_request) Failing after 1m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 6m51s
Project CI / Frontend tests (pull_request) Successful in 1m58s
Project CI / AI game creator shell web tests (pull_request) Failing after 54s
2fa7923362
- commands/desktop.rs:按本分支「exports 全退役」保留删除,不回填 master 改动的 playtest 打包/读取/列举命令
- project/export.rs:保留本分支删除,旧导出实现已由 export/draft 陶泥儿与小工具链路替换
- useGameDistributionPublishForm.ts:保留本分支删除,发布表单已由 export/state 与 tabs/taonier 面板替换
- game_distribution_publish.rs:保留本分支新增的 owner 媒体读路由与图片直传辅助函数,删除 master 改动的封面生成与 AI 资料建议退役代码
- game_distribution_publish.rs:吸收 master 的错误详情改动(parse_error_payload、response_data、map_http_error、request_json、request_json_for_read、describe_distribution_request_failure、emit_publish_progress 及对应测试)
- resolve_preview_url:合并为本分支 owner_media_read_url 加错误日志
k88936 marked the pull request as ready for review 2026-10-07 14:29:52 +08:00
k88936 added 37 commits 2026-10-07 14:47:09 +08:00
- 发布命令新增 coverPath / screenshotPaths 参数,本地图路径与文字字段都取 packageAll 落定的同一份权威表单
- Rust 侧 resolve_publish_media 去掉 read_form,不再读 .export/taonier.json,按面板路径与张数失败关闭
- resolve_publish_media 单元测试改为直接传面板路径,并新增「面板路径优先于磁盘草稿」用例
- 前端 facade 与 useTaonierTab 透传面板路径,补路径透传与空槽位断言
- 同步技术方案中发布媒体来源说明
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
- 移除与第 33 行完全重复的 apps/ai-game-creator-shell/src/view/project-development/export/generated/** 行
- readGameDistributionMediaSignedUrl 允许 payload 为空,空响应体不再抛原始 TypeError,而是走既有的「游戏媒体访问地址缺失」友好错误
- pruneEmptyDirs 不再跟随符号链接,避免递归走出 dist-taonier 或对指向祖先的链接递归到栈溢出
- mergeConfig 不再允许 overrides 静默改写 build.outDir,保证与 pack.mjs 的 --vite-built-dir 默认值一致
- assertH5Root 的属性正则同时匹配单/双引号并容忍 = 两侧空白,避免手写/插件 HTML 漏检
- collectZipEntries 遇到 FIFO/socket/设备文件时抛错,不再静默跳过导致缺文件的包
- writeZipEntries 先写同目录 <zip>.tmp-<pid>,全部成功后再 renameSync 覆盖落点,失败只清理临时文件
- makeZip 失败路径不再删除落点,避免写盘失败时丢掉落点上原有的 zip
- 删除 coverImagePart:各 E2E 脚本都有自己的本地实现,仓库内无人导入
- COVER_PNG 只作 imagePart 的默认值,收回 export
- preview.rs 两处受保护组件列表补回 .export(导出目录由 exports 改名时遗漏),避免 .export/taonier.json 等导出元数据被本地预览读取返回
- should_ignore_directory 顶层组件匹配补回 .export,避免把生成的导出包/zip 纳入 agent 仓库上下文
- conclude 在 std::fs::read 前同时按平台 200MiB 上限过滤,200MiB~512MiB 的产物直接判 artifactMissing
- read_package_bytes 用 OpenOptions + O_NOFOLLOW / FILE_FLAG_OPEN_REPARSE_POINT,关掉 symlink_metadata 检查后到打开之间被换成符号链接的 TOCTOU 窗口
- run_build 不再在 Tokio worker 上做同步文件 IO 与 Windows ACL 修复,与同模块其它 IO 一致
- resolve_project_verification_spec 对 actual/expected command 都做 trim,避免 package.json 脚本带首尾空格或换行时被误判成「脚本已变化」
- read_publish_image 用 OpenOptions + O_NOFOLLOW 打开,按句柄 metadata 判上限后再 take 限长读取,关掉体积与符号链接 TOCTOU
- create_game 与 create_version 把 normalize_local_project_id 提前到 resolve_publish_media 之前,避免用户输入非法时提前返回、已上传的封面/截图成为孤儿对象
- fixtures 的 game 字典补上必填非 Option 的 price_mud_points=0,避免迁移导入报 missing field
- 旧 STATE_FILE 只有 coverAssetId(素材对象 ID,不是对象存储 key),恢复续跑时先查游戏当前封面 key,不再拿它冒充 coverObjectKey
- 移除独立「打包」按钮与「发行包」卡片,发布入口不套卡片,内部先跑打包
- 线上值逐字段内联到对应输入框下方,退役 OnlineValuesCard
- 线上值进项目自动读取,失败按固定间隔静默自动重试,去掉「重新读取」手动入口
- 读取失败时提示正在自动重试,发布入口保持停住
- 同步陶泥儿面板测试与技术方案/实施计划文档
- boundsGameId 为空时先记录失败再 process.exit(1),避免后续请求 /undefined 级联掩盖根因
- applyVersionMediaDefaults 用 Promise.all 并发请求所有截图读地址,并在取消后直接返回,不再串行等待、也不在取消后继续发起请求
- getGameDistributionMediaReadUrl / getOwnerGameDistributionMediaReadUrl 增加可选 AbortSignal 并透传
- resolveGamePublishImagePreview 转发 signal;useGameDistributionMediaReadUrl 在 effect 清理时 abort 在途请求
- packageAll 的本地校验改用 effectiveFormRef.current ?? form,避免 refresh/flush 后仍用陈旧闭包放行盘上的非法值
- buildOnce 在 setFailure(null) 前 setDismissedFailure(''),显式重跑失败仍会重新弹出弹窗
- useTaonierTab 增加 projectPathRef,在打包/订阅/发布各 await 之后与进度回调里比对当前项目,避免旧项目进度、结果与失败落进新项目面板
- buildPublishGameMetadata / buildPublishMediaParts 改为接收调用方算好的 submission,元数据槽位与上传 part 单一来源
- 新增 COVER_IMPORTER_SETTINGS(maxItems: 1),FormCard 封面导入改用该设置,避免多选后静默丢弃其余图片
- IconPreview 增加可选 labels,缺省仍是小红书「图标」文案
- FormCard 的封面与截图预览各自传入封面/截图语义的 alt、title 与 aria
- pack.mjs 在整读+压缩处补 TODO(perf),说明单文件峰值内存约 2× 与 ZIP data descriptor 流式改法
- 同步 agc-skills manifest.json 版本与技能哈希
- game_distribution_media.rs 在 Field::bytes() 处补 TODO(perf),说明峰值约 2× part 与 field.chunk() 流式限长改法
- 删除左半面板的「下载发行包」按钮与下载失败弹窗接线
- 退役 useTaonierExport 的 artifactReady 状态及其构建收尾写入
- 删除 tabs/taonier/DownloadFailureDialog.tsx
- 同步陶泥儿面板测试与实施计划文档
- verification.rs 拆出按已解析规格运行的入口,项目侧预检错误不再与沙箱/日志故障混成一个字符串
- 陶泥儿与小红书构建把预检失败映射成新的 projectUnavailable 变体(改项目代码可解)
- 前端失败表补 projectUnavailable(归类为 code agent 可修),更新生成绑定与测试
- VersionMediaApplyTarget 增加 getCover/getScreenshots,覆盖前先 revoke 旧的本地预览
- 两个调用点用 ref 提供当前媒体;updater 只做纯计算,StrictMode 重放不再撤销仍在用的 blob URL
Merge remote-tracking branch 'origin/master' into feat/adapt-taonier-skill
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
f69fd339b9
k88936 added 4 commits 2026-10-07 15:35:20 +08:00
- useGameDistributionMediaReadUrl 新增 AbortSignal 第四参数后,旧断言 toHaveBeenCalledWith 失配
- 断言补 expect.any(AbortSignal),与换签请求可取消的语义对齐
- 夹具建在 homedir()/data/tmp;home 自身是仓库时,尚未 init 的夹具被 rev-parse 向上解析成父仓库
- isolatedGitEnvironment 增加 GIT_CEILING_DIRECTORIES 指向夹具临时根,阻止 git 发现 home 仓库
- GameDistributionGame 在原位恢复 cover_asset_id,注明退役、只占位不再写入,避免 SpacetimeDB 破坏性 schema 变更
- 新建与测试行构造补 cover_asset_id: None;migration.rs 注释与实施计划改成「保留退役」口径
- 重新生成 spacetime-client 该表绑定,补回 cover_asset_id 字段
Merge remote-tracking branch 'origin/master' into feat/adapt-taonier-skill
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
23fa91895d
Author
Member

在本地运行ci通过

在本地运行ci通过
k88936 merged commit 93897b88ac into master 2026-10-07 15:45:55 +08:00
k88936 deleted branch feat/adapt-taonier-skill 2026-10-07 15:45:55 +08:00
Sign in to join this conversation.