Compare commits

...

95 Commits

Author SHA1 Message Date
k88936 8b2d0cb9df 统一错误事件同步留痕到 AppData 应用日志
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 6m44s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m28s
Project CI / Frontend tests (pull_request) Successful in 4m9s
Project CI / Native shell tests (pull_request) Successful in 8m24s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m37s
- 新增 agent_runtime_error_app_log_lines,把统一错误事件的同一份已脱敏诊断投影成应用日志两行:agent.runtime.error 身份行与 agent.runtime.error.detail 详情行
- 身份行含 eventId/source/stage/code/retryable/clientTurnId/elapsedMs/detailRef/summary,详情行含 hint/detail/metadata;detail 与 metadata 各按 1200/200 字符预算先脱敏再截断,字段仍只来自 sidecar 那份 diagnosis
- 拆两行的原因:整行命中凭据标记会被 sanitize_diagnostic_message 整体替换成脱敏占位,详情行被吃掉时身份行仍能定位 eventId 与 detailRef
- app_log! 先于 sidecar 写入,sidecar 写失败也留下可提交的诊断;各字段先压平换行,保证应用日志逐行读取且 stderr 输出不拆行
- 新增用例覆盖身份字段、凭据脱敏、单行口径与实际 sanitize_diagnostic_message 落盘边界(含超长诊断)
- 同步文件【技术方案】AGC错误报告与诊断上传与【技术方案】AI游戏创作智能体App实施计划,共享记忆 decision-log 与 pitfalls 明确项目内 .agent/runtime/errors sidecar 与进程内错误报告事件池是两套东西,本次只写日志行
- 验证:cargo test runtime_error(7 passed)、cargo fmt --check、npm run check:encoding、git diff --check
2026-09-21 21:34:45 +08:00
kdletters 4d313a44eb 收口后台模板管理里程碑:回填验收证据并删除临时实施计划
Project CI / AI game creator shell Rust crates (push) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m58s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m32s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m26s
Project CI / Backend tests (push) Successful in 8m33s
Project CI / Repository checks (push) Successful in 4m50s
Project CI / Native shell tests (push) Successful in 9m20s
Project CI / Frontend tests (push) Successful in 5m57s
Project CI / AI game creator shell web tests (push) Successful in 3m19s
- 里程碑 Status 从 approved 更新为 implemented-awaiting-runtime-acceptance,并注明按验收口径未对真实 OSS 写入
- 新增验收证据:合同/权限/领域/存储/CLI 五层用例结果,本地 API 健康检查与未授权读取写入失败关闭,授权读取 9 个模板快照,受控浏览器桌面与 390×844 移动端 smoke
- 新增复审记录:写路径顺序与 409 语义、后台 DTO 结构上无法改动 ZIP/版本、封面公共域名与单 bucket 合同一致、迟到响应判据的覆盖方式
- 按实施计划第 4 步删除临时实施计划文件,里程碑文档保留为唯一验收依据
- 验证:check:doc-index(192 份)、check:encoding、git diff --check
2026-09-21 14:21:25 +08:00
kdletters 01ed9fadc6 修复手工 release 发布未透传 AGC 渠道
Project CI / AI game creator shell Rust crates (push) Successful in 2m45s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 7m54s
Project CI / Backend tests (push) Successful in 11m10s
Project CI / Repository checks (push) Successful in 6m17s
Project CI / Frontend tests (push) Successful in 8m0s
Project CI / Native shell tests (push) Successful in 11m32s
Project CI / AI game creator shell web tests (push) Has been cancelled
补充手工发布目标到 AGC 更新渠道的踩坑记录

补充 release 客户端包分区与补发流程说明
2026-09-21 14:04:02 +08:00
kdletters 02a7abeb3f 新增后台 AGC 模板管理与模板库内容寻址落库
Project CI / AI game creator shell Rust crates (push) Successful in 2m53s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m42s
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
- 后台新增「模板管理」页:按权限查看并编辑模板名称/简介/标签/封面,支持上架与下架;路由、侧栏、tab 权限、API 客户端与 DTO 同步接入
- api-server 新增 /admin/api/agc-templates 读取与更新路由,复用现有后台鉴权与 tab 权限校验,权限不足按既有失败关闭口径拒绝
- module-assets 收敛模板库纯规则(active/inactive 投影、索引 schema 与重复项校验、ZIP 大小与摘要核对),platform-oss 承担对象存储读写、不可变对象复用与发布互斥锁
- shared-contracts 补后台模板 DTO;spacetime-module 账号存储适配保持既有 schema 不变
- AGC 壳内置模板索引与建项读取改为内容寻址对象,补模板安装、越界归档、索引不匹配等用例
- 模板库发布脚本支持定向发布与上架/下架合并、内容地址复用与锁语义,CLI 守卫用例同步;jenkins 两条客户端打包管线统一触发邮件通知 Job 并传递首装包链接
- 内置 5 个 Cocos 官方模板载荷与索引 fixture(内容地址对象,共 90 个文件)
- 补「后台模板管理」里程碑与实施计划,更新模板库技术方案、开发运维文档与共享记忆
- 验证:admin-web 39 项用例与 typecheck、发布脚本 25 项用例、api-server 与 AGC 壳编译、模板库定向 Rust 用例(AGC 壳 18 项、module-assets/platform-oss 16 项、后台权限 1 项)、check:encoding / check:doc-index / check:production-ops / rustfmt 全部通过
2026-09-21 13:56:20 +08:00
suzmii 6845cdcbc5 修复 Mac 包校验脚本对合法随包 npm 的误判
Project CI / AI game creator shell Rust smoke (push) Successful in 2m14s
Project CI / AI game creator shell Rust crates (push) Successful in 1m26s
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- check-macos-bundle 的「包里不得出现 node_modules」门禁只放行了 game-runtime/node/node_modules/npm,但 recursive readdir 会把目录项 game-runtime/node/node_modules 也列出来,于是合法包必然断言失败
- 这条门禁是 #439 引入的,之后每次构建都死在更早的阶段(staging/测试),直到 2026-09-21 arm64 单架构跑通才第一次走到它:12:58、13:24、13:43 三轮都挂在这里
- 修复:放行 npm 目录自身、其子树与它的上级 node_modules 容器目录;已用真实 arm64 产物跑通完整 smoke(资源、架构、摘要、权限、Codex 正式查找、app-server 握手、缺组件拒绝全部通过)
2026-09-21 13:50:32 +08:00
suzmii 4044ffd89b 修复 Mac 发布 Job 手工触发时 Checkout 因未定义参数失败
- 手工「立即构建」不会注入可选参数(COMMIT_HASH / SKIP_IF_SUPERSEDED 未定义而非空值),set -u 下 `[ -n "$COMMIT_HASH" ]` 直接以 unbound variable 退出,Job 2026-09-21 12:54 那一轮就是这样在 Checkout 失败的
- 两段 Checkout 脚本开头统一兜底:SOURCE_BRANCH 默认 master、COMMIT_HASH 默认空、SKIP_IF_SUPERSEDED 默认 false,调度器显式传参时行为不变(仍按 pinned commit 构建并判让位)
2026-09-21 13:50:32 +08:00
suzmii 8d0c021126 修正 Mac 发布文档口径:自动触发与描述原文
- 运维文档原写「不设置 trigger;不接入现有每小时版本调度」,与现状矛盾:调度 Job 会先发号再把 SOURCE_BRANCH/COMMIT_HASH/AGC_UPDATE_CHANNEL/AGC_RELEASE_VERSION/SKIP_IF_SUPERSEDED 透传给 Mac Job,正常发布不需要手工点构建
- 把 Jenkins Job 描述与节点描述的当前原文(arm64 单架构 + 由调度器自动触发 + dry-run 默认关)写进文档,并要求每次改口径同步 Jenkins 侧描述,避免描述停留在 universal/手动触发的旧口径
2026-09-21 13:50:32 +08:00
kdletters 1cb91f9444 修复画布绑定查询与生成失败归因
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
- 画布绑定前置的项目列表查询改为 view=summary,站内与外部路由统一支持摘要视图(缺省 full 兼容,未知取值 400)
- 项目摘要投影收敛到 editor_project,外部 API 与 MCP 复用同一份实现,摘要不做内联媒体修复也不回传画布与全量资源
- 外部编辑器请求失败文案补 kind 语义与底层因链,区分请求超时、正文截断与非法 JSON,不拼接 URL
- 远端资源编辑终态文案带出稳定失败码并指向移出恢复队列这一唯一出口,上游原文仍不写入账本
- 补齐站内摘要视图与画布测试夹具的请求断言用例
- 更新共享决策记录与踩坑记录(图集切片上限条目归上游 #442,本提交不重复)
2026-09-21 13:33:23 +08:00
kdletters 2e31a485d8 修复 Stdb publish 事务确认超时被误判为失败
Project CI / AI game creator shell Rust crates (push) Successful in 3m0s
Project CI / AI game creator shell Rust smoke (push) Successful in 4m0s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m41s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m44s
Project CI / Backend tests (push) Successful in 8m52s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- production-stdb-publish 新增 run_spacetime_publish:仅在出现 timeout waiting for transaction confirmation 时重试一次同版本 publish(SpacetimeDB 对已生效的同版本为幂等),重试成功即视为模块已在位,不再把生产留在维护态
- 其它失败原因保持原有失败关闭语义;生产运维门禁断言补上该重试口径
2026-09-21 13:27:37 +08:00
suzmii 4ad131d74d Merge pull request 'AGC 画布:锚点让位标题栏、生成浮层贴边、拖动误触点击、读秒 100ms 与图集参考口径' (#448) from fix/agc-canvas-bugfix into master
Project CI / AI game creator shell Rust smoke (push) Successful in 3m58s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m26s
Project CI / AI game creator shell Rust crates (push) Successful in 1m40s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m43s
Project CI / Frontend tests (push) Failing after 5m41s
Project CI / Backend tests (push) Successful in 7m29s
Project CI / Repository checks (push) Successful in 4m50s
Project CI / Native shell tests (push) Successful in 9m32s
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #448
2026-09-21 13:04:11 +08:00
kdletters 886bdc06ed 去掉渠道环境变量用例里重复的 stageRuntime
Project CI / AI game creator shell Rust crates (push) Successful in 2m44s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m37s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 7m58s
Project CI / Backend tests (push) Successful in 6m33s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
- 两次独立修复(#444 与本仓库 55cb04765)在同一处各加了一个 stageRuntime,合并后成为重复对象键,Web-Build 的 ESLint(no-dupe-keys) 失败
- 保留一条带说明的 stageRuntime stub,用例语义不变
2026-09-21 12:49:34 +08:00
suzmii 562e1ba586 「生成任务」侧栏读秒改为 100ms 粒度:与对话侧共用同一份时钟
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 4m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m15s
Project CI / Repository checks (pull_request) Failing after 20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m16s
Project CI / Frontend tests (pull_request) Successful in 7m30s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m1s
Project CI / Native shell tests (pull_request) Successful in 11m1s
- ResourceCanvasAssetGenerationTasksPanelView:删掉面板自带的 1000ms interval 与 nowMillis 状态;两条任务行的「已耗时」下沉为叶子组件(AssetGenerationElapsedLabel / ResourceEditElapsedLabel),各自订阅共享 hook useLiveNow(LIVE_TIMER_TICK_MS = 100)。
- 为什么不是面板级时钟:面板上通常同时挂着多条任务,100ms 的 tick 落在面板上等于每 0.1 秒重建整个侧栏;useLiveNow 的注释明确要求调用方是叶子节点。终态行不订阅,已耗时定格在账本给的结束时间上。
- 行为:不足一分钟的耗时本来就显示到 0.1 秒(formatElapsedDuration),现在每 100ms 真的更新一次,不再是「带小数却 1 秒跳一格」。
- 用例:新增「读秒按 100ms 刷新(0.0 → 0.1 → 0.5 秒)」「终态行不再走表,定格在结束时间」「秒表只订在运行中的那一行上(setInterval 只被调用一次)」;把 LIVE_TIMER_TICK_MS 临时改回 1000 时第一条用例即红,粒度确实被钉住。
2026-09-21 12:48:21 +08:00
suzmii e18067e028 Merge pull request 'macOS 发布改为只出 arm64 单架构(修复 Mac 管线 #7 起连续失败)' (#447) from feat/agc-macos-arm64-only into master
Project CI / AI game creator shell Rust smoke (push) Successful in 2m11s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Reviewed-on: #447
2026-09-21 12:38:03 +08:00
suzmii 613d62fdb2 同步里程碑计划:macOS 清单只登记 darwin-aarch64
Project CI / AI game creator shell Rust crates (pull_request) Failing after 53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m52s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 7m40s
Project CI / Repository checks (pull_request) Failing after 2m0s
Project CI / Backend tests (pull_request) Successful in 7m42s
Project CI / Native shell tests (pull_request) Successful in 8m47s
Project CI / Frontend tests (pull_request) Successful in 6m54s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m1s
- docs/project-memory/plans 的渠道化里程碑仍写着 dev-mac universal 同时写两个平台键,与 2026-09-21 的单架构决策冲突,按当前口径更新并指向决策记录
2026-09-21 12:35:36 +08:00
suzmii b4ce6877c8 修掉「拖动占位卡仍触发点击」的真因:点击抑制的计时起点必须在松手那一刻
- useResourceCanvasGenerationPlaceholders:抑制状态在越过拖动阈值时只登记、不起计时器,改到 pointerup 才起 0ms 计时(deferDragClickSuppressionCleanup);pointercancel 与切项目走 clearDragClickSuppression 立刻收干净。
- 原实现(越过阈值时就起 0ms 计时器)在真实鼠标下不成立:一次拖动里 pointermove 横跨多轮宏任务,计时器早在松手前就把抑制清掉,收尾那次 click 照样生效——jsdom 同步发 move/up 看不见这一档,所以上一版用例是假绿。
- 用例补上真实时序:拖动过程中显式让时钟走一轮宏任务再松手;宿主用例还补上浏览器在 pointerup 之后补发的 lostpointercapture,并断言它不能把这次拖动的抑制一起清掉。
- 敏感性已双向验证:把计时器搬回「越过阈值那一刻」、以及让 lostpointercapture 清抑制,两条用例都会红;改用例前的同步版本对两种错法都会绿(所以上一版没抓住)。
- 注释写明计时起点为什么是松手那一刻,并与资源卡链路的 deferSkippedResourceCardClickCleanup 对齐口径。
2026-09-21 12:34:47 +08:00
suzmii e53992fdc2 macOS 发布改为只出 arm64 单架构
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (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 smoke (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 / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- build-macos-ci 固定 aarch64-apple-darwin:单架构构建、只跑 arm64 隔离 smoke、首装包命名 <产品名>_<版本>_aarch64.dmg,构建清单记录 smokes 与实际清单平台键
- stage-node-runtime 的 universal-apple-darwin 恢复失败关闭并说明原因:随包 Node 只有单架构官方发行版,按宿主架构放行会让 Intel 上这份侧车不可执行,且通用包自检按 process.arch 校验抓不到
- 守卫用例改为断言入口固定 arm64 目标与 _aarch64.dmg 后缀、universal 目标必须抛错
- Jenkinsfile 与三份技术/运维文档同步单架构口径:清单只登记 darwin-aarch64,Intel 非目标,恢复 universal 的前置条件写在文档里
- 共享记忆补 2026-09-21 决策记录,pitfalls 记下「只带宿主架构的通用包」陷阱,避免再次用过渡实现糊过去
2026-09-21 12:30:56 +08:00
suzmii a5ee06a4c7 Merge pull request '修复macOS主机上AGC发布测试未stub Node运行时staging' (#444) from fix/agc-test-node-runtime-stub into master
Project CI / AI game creator shell Rust smoke (push) Successful in 4m16s
Project CI / AI game creator shell Rust crates (push) Successful in 1m6s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m51s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m45s
Project CI / Repository checks (push) Failing after 1m55s
Project CI / Backend tests (push) Successful in 6m33s
Project CI / Native shell tests (push) Successful in 7m47s
Project CI / Frontend tests (push) Successful in 5m49s
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #444
2026-09-21 12:23:52 +08:00
suzmii 7b57cba14d 补齐「图集(生成图标素材)为什么没有 @」的当面说明与依据
- 生成浮层:不给 @ 选择器的入口新增一段说明行(`data-resource-canvas-generation-reference-free`),讲清「该入口走平台图集通道,参考只有一张权威规范图(图标规范),不支持另挑参考图」——验收现场那条「生成图标素材没法 @」读起来像漏了按钮,原因其实在通道上。
- 样式:说明行允许折行、沿用次要文字色;与既有的「参考图 n/5」计数行区分开。
- 用例:图集入口断言说明行在场;逐条入口断言「说明行」与「@ 选择器」严格互补;原「不呈现参考选择」的判据从按词匹配改成按计数元素/计数值匹配(说明行本身会提到「参考图」三个字)。
- 文档:入口矩阵补「图集为什么没有 @」一条,写明图集走 `icon-spritesheets` 通道、提交体只有 referenceId + iconDescriptions(无 referenceImageSrcs)、结果绑定硬校验参考恰好一张,以及「平台 OpenAPI 该请求 schema 有 referenceImageSrcs 字段,放开属于产品 + 平台口径变更」。
2026-09-21 12:23:32 +08:00
kdletters 55cb047654 支持 macOS 通用包构建时的 Node 运行时暂存
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- stage-node-runtime 的 targetRuntime 支持 universal-apple-darwin:在 macOS 宿主上按宿主架构暂存官方 Node(通用包自检按 process.arch 校验),非 darwin 宿主仍失败关闭
- 同步 stage-node-runtime 测试期望:darwin 宿主可用、linux 宿主仍拒绝,Universal Node 合并(lipo)留作未完成项
- 共享记忆记录该限制:当前通用包侧车仍是宿主架构,Intel Mac 需等待真正的通用 Node
2026-09-21 12:15:28 +08:00
suzmii beebcca4de 修复 AGC 画布四项验收问题:锚点让位标题栏、浮层不再撞画布边缘、拖动不再触发点击、图标规范参考口径对齐
- 生成任务锚点新增「资源总览」档:资源总览页仍贴画布顶边内缩 0.85rem,资源栏目画布页让开顶部钉住的整宽栏目标题栏(42px + 0.5rem),两档切换走 margin-top 过渡(减动效下关掉)。
- 生成浮层的顶边与可用高度改为按占位卡的真实屏幕位置算(卡下沿到安全底边),不再假设卡片贴在安全带上沿——占位落到下半屏时浮层底边不再越过画布下沿被切掉。
- 生成浮层新增水平收口:按「浮层左右各留 12px」把锚点收进画布可视范围,卡片靠边时浮层不再有一半落到画布之外;宽度口径与面板 CSS 同源(min(560px, 100% - 24px))。
- 新增画布卡片手势的共享判据 resourceCanvasGestureExceededDragThreshold(阈值仍取 RESOURCE_CANVAS_DRAG_THRESHOLD):资源卡与生成占位卡共用同一份「拖动还是点击」判断。
- 生成占位卡修掉拖动收尾误触点击:越过阈值才算拖动、登记一次性点击抑制(consumeDragClick)由宿主消费,「拖动中」样式也从越过阈值那一刻起才亮。
- 生成占位卡的删除按钮起手手势不再被当成「点卡片」:按起手点判定,松手落回卡片时不触发开合生成浮层。
- 图标规范面板的参考选择注释按现行判据改写(只有图集 icon-spritesheet 不接受用户参考),入口矩阵文档同步补「参考图(@ 引用)」口径并修掉「本地 IPC 没有参考图入参」的旧说法。
- 补齐回归用例:手势模型判据、占位卡拖动/点击与阈值、宿主级「拖完不弹浮层且普通点击仍开合」、浮层顶边与水平收口、锚点分档与跨文件标题栏高度守卫、逐条生成入口的 @ 与判据一致、图标规范面板的 @ 与提交带引用。
- 共享记忆 pitfalls.md 记录两条排障口径:卡类手势必须只有一份阈值与点击抑制判据;画布浮层几何只能按真实屏幕位置算、分页锚点必须让开钉死的标题栏。
2026-09-21 11:58:13 +08:00
kdletters 9bf35e8745 修复 macOS 构建上的渠道环境变量用例宿主依赖
Project CI / AI game creator shell Rust crates (push) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m50s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m0s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 8m5s
Project CI / Backend tests (push) Successful in 7m2s
Project CI / Native shell tests (push) Successful in 8m12s
Project CI / Repository checks (push) Successful in 4m8s
Project CI / Frontend tests (push) Successful in 5m15s
Project CI / AI game creator shell web tests (push) Failing after 6m13s
- build-release.test.mjs 的「packaged renderer receives the same channel as the updater manifest」用 windows target 却未注入 stageRuntime,真实 staging 要求宿主 Node 平台/架构与目标一致,导致 macOS(arm64)/Linux 上必然失败
- 该用例只断言传给 spawn 的 VITE_AGC_PLATFORM_CHANNEL,改为注入空 stageRuntime,与同文件其它用例一致
2026-09-21 11:53:32 +08:00
kdletters 90207bc1d5 minimal 备份增加 freeze 副本路径
Project CI / AI game creator shell Rust smoke (push) Successful in 2m35s
Project CI / AI game creator shell Rust crates (push) Successful in 1m14s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 7m28s
Project CI / Backend tests (push) Successful in 7m25s
Project CI / Frontend tests (push) Successful in 7m18s
Project CI / Repository checks (push) Successful in 5m51s
Project CI / Native shell tests (push) Successful in 9m16s
Project CI / AI game creator shell web tests (push) Successful in 3m41s
- database-backup-to-oss.mjs 新增 --freeze-dir:minimal 模式先把选中子集 rsync 成冻结副本(保持原始相对路径)再从副本上传,避免热备时最新 commitlog 持续追加触发 stat 漂移失败
- files-minimal systemd 模板与发布前 minimal 备份默认传入 freeze 目录
- 生产运维门禁同步 freeze 口径断言
2026-09-21 11:37:14 +08:00
kdletters 2f248ea0c3 新增 minimal 备份口径并切换 release 定时备份
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
- database-backup-to-oss.mjs 新增 --minimal/--retain-snapshots:按上游 retention 语义只备份最近 N 份 snapshot、覆盖最老保留 snapshot 的 commitlog 边界段及其后全部段,外加身份/配置/控制库/模块字节,且不停止任何服务
- 备份检查脚本补充 minimal 计划用例(保留次新+最新 snapshot、丢弃更早 snapshot 与 clog 段、状态路径齐全、保留数校验)
- 发布前备份默认走 minimal 热备(GENARRATIVE_STDB_PUBLISH_BACKUP_MINIMAL=0 可回到冷备),不再需要 44.7GiB 冷备空间
- Server-Provision 新增 DATABASE_BACKUP_PROFILE=files-minimal 与 systemd drop-in,release 推荐使用并继续禁止 files-history
- 运维文档与共享记忆记录上游 retention 依据、实测 40G→3.6G 与不停服热备口径
2026-09-21 11:33:39 +08:00
kdletters 117d482f93 发布前备份空间预检前置并支持自动降级
Project CI / AI game creator shell Rust smoke (push) Successful in 2m13s
Project CI / AI game creator shell Rust crates (push) Successful in 1m7s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 7m9s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m57s
Project CI / Native shell tests (push) Successful in 7m58s
Project CI / Repository checks (push) Successful in 5m13s
Project CI / Frontend tests (push) Successful in 7m19s
Project CI / Backend tests (push) Successful in 10m10s
Project CI / AI game creator shell web tests (push) Successful in 4m47s
- database-backup-to-oss.mjs 新增 --check-space-only:archive 按 data×1.1、files 按 max(data×0.05, 2GiB) 计算门槛,空间不足以退出码 3 返回
- production-stdb-publish.sh 在进入维护与停服务之前先做空间预检,archive 不足时自动降级 files(files 不支持 defer,async 收敛为 sync),可用环境变量关闭
- 尚未开始 publish 的失败自动恢复本次停掉的 API/controller/worker 并退出维护,只有真正开始发布之后的失败才保持维护态
- 备份检查脚本与生产运维门禁补上新口径与执行顺序断言,运维文档与共享记忆记录事故与规则
2026-09-21 10:57:29 +08:00
suzmii 9f15c1c572 修复非Windows主机上AGC发布测试未stub Node运行时staging
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m19s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 7m16s
Project CI / AI game creator shell web tests (pull_request) Successful in 7m2s
Project CI / Frontend tests (pull_request) Successful in 9m46s
Project CI / Native shell tests (pull_request) Successful in 11m46s
build-release.test.mjs的渠道注入用例漏注入stageRuntime,真实staging会用宿主平台与默认Windows目标做一致性校验
该缺陷在macOS主机上必然失败,卡住macOS渠道构建的测试阶段
补上stageRuntime stub,本机(darwin/arm64)验证37项全通过
2026-09-21 10:35:43 +08:00
suzmii 959d367ed4 Merge pull request '图集切片上限对齐平台契约:客户端结果门 64 → 256' (#442) from fix/spritesheet-slice-limit-256 into master
Project CI / AI game creator shell Rust smoke (push) Successful in 2m30s
Project CI / AI game creator shell Rust crates (push) Successful in 1m8s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 5m47s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m38s
Project CI / Repository checks (push) Successful in 7m17s
Project CI / Frontend tests (push) Successful in 9m56s
Project CI / Backend tests (push) Successful in 11m6s
Project CI / Native shell tests (push) Successful in 11m58s
Project CI / AI game creator shell web tests (push) Successful in 5m20s
Reviewed-on: #442
2026-09-21 10:27:39 +08:00
suzmii 56dcb06fb8 Merge remote-tracking branch 'origin/master' into fix/spritesheet-slice-limit-256
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m50s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 7m57s
Project CI / Backend tests (pull_request) Successful in 8m54s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 8m14s
Project CI / Repository checks (pull_request) Successful in 7m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m56s
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-21 10:12:01 +08:00
kdletters f947807fba 修复手动发布的发号 Job Copy Artifact 授权
Project CI / AI game creator shell Rust crates (push) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 7m17s
Project CI / Backend tests (push) Successful in 6m48s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 9m58s
Project CI / Native shell tests (push) Successful in 8m17s
Project CI / Frontend tests (push) Successful in 5m58s
Project CI / AI game creator shell web tests (push) Successful in 3m35s
Project CI / Repository checks (push) Successful in 4m1s
- 发号 Job 的 copyArtifactPermission 补齐手动发布消费者,用户触发的构建不再报 Unable to find project for artifact copy
- 同步总版本号技术方案与共享记忆排障记录,写明 SYSTEM 定时构建短路、用户触发按认证用户判权的差异与回写 Job property 的步骤
2026-09-21 08:54:17 +08:00
suzmii 1997dfeb80 图集切片上限对齐平台契约:客户端结果门 64 → 256
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m55s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m56s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m42s
Project CI / Backend tests (pull_request) Successful in 5m19s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m39s
Project CI / Native shell tests (pull_request) Successful in 7m24s
Project CI / Repository checks (pull_request) Successful in 3m54s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m32s
Project CI / Frontend tests (pull_request) Successful in 4m54s
- 现场(gameagent-6e53c9e8)「AI 生成图标素材」失败:平台已生成并切完图,客户端在绑定结果这一步把整条结果判失败(assetId 为空、付费产物被丢)。
- 根因:同一条链路存在两个上限——平台切分 / Agent 工具 schema sliceCount / 持久化产物批次都是 256,客户端两处结果绑定门仍是 64(external_generation_state.rs、canvas_generation.rs)。
- 修复:两处门统一到 PLATFORM_ART_SPRITESHEET_MAX_SLICES = 256,判据与拒绝文案各只留一份(数字由常量插值),注释点名三处同值权威。
- 用例:新增上限值/边界/文案用例与「64/65/256 片必须能落盘、257 片必须拒」的回归用例;两条都做过变异验证(常量改回 64 即红)。
- 文档:decision-log 与 pitfalls 各补一条(客户端门不得比平台产品上限更严;上限引同一常量)。
2026-09-21 08:31:39 +08:00
kdletters 0ac86f941c 优化 Gitea CI Rust 分片并发
Project CI / AI game creator shell Rust crates (push) Successful in 53s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m58s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m40s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 9m54s
Project CI / Frontend tests (push) Failing after 5m2s
Project CI / Backend tests (push) Successful in 8m9s
Project CI / Repository checks (push) Successful in 4m7s
Project CI / Native shell tests (push) Successful in 8m58s
Project CI / AI game creator shell web tests (push) Successful in 3m9s
将四个 AGC Rust shard 合并为两条 lane,减少重复预热与排队

同步 workflow 契约测试、分片说明和运维文档
2026-09-21 07:56:06 +08:00
suzmii 3815d00c84 Merge pull request 'AGC 画布三项:生成任务侧栏右上角 / 拖到对话批量引用 / 替换面板画布点选' (#441) from feat/agc-canvas-ux-followups into master
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #441
2026-09-21 07:51:07 +08:00
suzmii 4172534bab Merge branch 'master' into feat/agc-canvas-ux-followups
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 7m14s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 7m44s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 7m46s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m2s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 9m41s
Project CI / Repository checks (pull_request) Successful in 3m44s
Project CI / Backend tests (pull_request) Successful in 6m4s
Project CI / Frontend tests (pull_request) Successful in 5m6s
Project CI / Native shell tests (pull_request) Successful in 9m0s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m20s
2026-09-21 07:51:00 +08:00
kdletters ab9820990c 补充 Windows 构建节点 Python 安装口径
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 7m28s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m55s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 8m2s
Project CI / AI game creator shell Rust crates (push) Successful in 1m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m21s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 10m4s
Project CI / Frontend tests (push) Failing after 4m17s
Project CI / Repository checks (push) Successful in 4m21s
Project CI / Backend tests (push) Successful in 6m49s
Project CI / Native shell tests (push) Successful in 7m48s
Project CI / AI game creator shell web tests (push) Successful in 2m58s
- 排障记录写明新节点静默安装 Python 3 的确切命令,并说明 CMake 用 Build Tools 自带版本
2026-09-21 06:58:45 +08:00
kdletters 510e550f19 补充 Windows 构建节点修复的实跑证据
- Godot 编辑器插件接入文档记录 Jenkins Windows 节点在本修复分支上的 dry-run 实跑结果:preflight、原生载荷 staging、NSIS 安装包与渠道清单
2026-09-21 06:58:39 +08:00
kdletters 6dadd76c1b 修复 Windows AGC 构建节点的 Godot 原生扩展工具链
- AGC_WINDOWS_PATH 增加 Build Tools 自带 CMake 与 Python 3.12 目录,阶段 PATH 不再漏掉 Godot 原生扩展所需工具
- Toolchain preflight 校验 CMake 3.25 及以上、Python 3 和 Visual Studio 17 2022 生成器,缺失时在构建前失败关闭
- 同步 Godot 编辑器插件接入文档与共享记忆排障记录
2026-09-21 06:41:03 +08:00
suzmii 1a507dbf2d Merge remote-tracking branch 'origin/master' into feat/agc-canvas-ux-followups
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 6m50s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 5m29s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 41s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 10m19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 5m17s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m2s
Project CI / Repository checks (pull_request) Successful in 4m57s
Project CI / Frontend tests (pull_request) Successful in 6m13s
Project CI / Backend tests (pull_request) Successful in 7m40s
Project CI / Native shell tests (pull_request) Successful in 8m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m57s
2026-09-21 06:21:38 +08:00
suzmii 5467ed6d7b 自查修正:拖到对话落点逐值比较、替换面板锚进画布容器并与任务面板互斥
- 拖动落点改逐值比较 sameResourceCardReferenceDrop(条数 + 对话栏矩形四值),指针在对话栏上移动不再每帧 setState、不再每帧重渲染工作台。
- 替换面板从 stage 顶层移进画布容器 .game-resource-book-manager(本身 position: relative),top 3.2rem 排在任务开关下方;相对 stage 写死的偏移会在工具条换行时压上去,与刚修的任务开关是同一类隐患。
- 打开替换会话时收起「生成任务」面板:画布右上角同一时刻只留一块浮层(任务照旧在账本里推进)。
- 面板上方补一行「替换源素材:<显示名>」:非模态之后画布上的源素材光环会被空白点击清掉,源身份得在面板这一侧留一份可读说法。
- 测试:替换用例新增「面板锚在画布容器里、写明源素材、并与任务面板互斥」(20 passed)、落点模型新增逐值比较用例(6 passed)。
- 文档:decision-log 新增本批自查条目、待办文档补登记、验收用例 C9 注脚改为 nonModal 形态。
2026-09-21 06:21:19 +08:00
kdletters 4b788fa73a 修复 Gitea CI 镜像构建并刷新到 1.98.1 工具链 (#438)
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 7m25s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m39s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 7m48s
Project CI / AI game creator shell Rust crates (push) Successful in 1m11s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 9m55s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m29s
Project CI / Repository checks (push) Successful in 4m9s
Project CI / Backend tests (push) Successful in 6m18s
Project CI / Frontend tests (push) Successful in 6m17s
Project CI / Native shell tests (push) Successful in 9m22s
Project CI / AI game creator shell web tests (push) Successful in 5m21s
修复 CI job 镜像无法构建的问题,并换用 Rust 1.98.1 工具链镜像,跑通 PR 门禁。

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/438
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-09-21 06:15:57 +08:00
suzmii 7148ff9eaf 验收修正:任务锚点改挂画布那一格,展开后开关让位
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 7m22s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 7m32s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 7m46s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 7m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m10s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m8s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web 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
- 重叠:右上角锚点不再用 position: absolute + 写死的 top(工具条是 stage 第一行、高度随按钮换行变化),改为 stage 网格里与工作面同一个单元格的条目:grid-row 按 placement 分档 + grid-column 1 + justify-self end + align-self start + margin;同格的三处容器也显式钉住第 1 列,避免画布被自动列放置挤进隐式第二列。
- 并排:开关只在完全收起时渲染(收起动画期间也不画),展开态画布右上角只留面板,收起走面板头部 × 或点画布外部。
- 测试:面板用例新增「展开时开关让位」、计数用例拆成收起查开关 / 展开查面板;样式用例改判网格条目坐标与窄屏 justify-self: stretch;dismiss 用例改走面板 × 收起、收起后开关回来能再打开。
- 文档:PRD 3.10 与更新时间、decision-log 新增 2026-09-21 验收修正条目、待办文档补登记该修正。
2026-09-21 04:33:26 +08:00
suzmii b20d980ba7 替换点选判据的注释口径跟上新流程
- resolveResourceReplacementPick 的注释里「点选态」「点选替换」改为「画布点选」「面板里的当前替换目标」,与退役掉的按钮 / 提示条口径一致;只改注释,行为不变。
2026-09-21 03:53:07 +08:00
suzmii de502e9bc3 替换面板改为非模态浮层,支持在画布上点选替换目标
- 共享选择组件新增 opt-in nonModal:不铺遮罩、不做焦点陷阱、限高 + 内部滚动,Esc 在 document 阶段截断后取消,关掉即卸载;网页端美术画布不传,弹窗行为逐字不变。
- 新增 initialSelectionRevision:面板开着时宿主换了目标才重同步当前选择,面板里的搜索词与分类筛选不被重置。
- AGC 侧把候选面板锚到画布右上角(任务开关下方),壳样式放共享样式表、宿主只负责锚定;面板开着时画布照常可点,点中合法候选落成面板当前选择,写入仍只由「确认」发起。
- 退役旧的「点选替换」按钮、画布提示条与 resourceReplacementPickMode:同一个功能不留两条 UI 路径;合法性判据仍复用 resolveResourceReplacementPick,写入仍是同一个 confirm 函数。
- 确认时用画布点选结果兜底,避免点完当帧按确认误报「请选择一个替换素材」。
- 测试:替换用例 4 条点选路径改写为非模态 + 面板点选(含四类非法目标在面板内报因、Esc 只收面板),新增「画布点选不清掉面板搜索/分类」;共享组件用例新增 nonModal 形态与浮层壳声明级契约。
- 文档:PRD 5.3 / 7.8 第 8 条改写、验收用例 S15a 改写、decision-log 新增 2026-09-21 条目、待办文档登记三项并勾掉对应需求。
2026-09-21 03:48:40 +08:00
suzmii 9639a080c6 资源卡拖到对话栏实现批量 @ 引用
- 新增落点模型 resourceCardReferenceDropModel:对话栏矩形量取(0 x 0 判成没有落点,避免画布内拖动被误判)、命中判据、引用构造与落点提示文案。
- 引用构造与工具条「引用」按钮逐字一致(只认已登记 manifest 素材、source 为 resource-card、kind/分类/标签取自资源投影),一条都构造不出来时用提示条说明原因。
- 拖动中指针落到对话栏上即从排版切到引用:卡片不再跟指针走,改铺虚线落点浮层并提示「松手即可 @ 引用 N 项素材」;拖回画布内松手仍是原来的排版语义。
- 批量范围与拖动位移同一个集合(多选整批、单选只引用按住的那张);新增批量事件 RESOURCE_REFERENCE_INSERT_MANY_EVENT,App 侧一次插入整批并只聚焦一次,空批次不派发。
- 对话栏挂 ref(不 document.querySelector)、落点浮层按对话栏当时的屏幕矩形铺在根节点上,不改对话栏自身的定位与排版。
- 测试:新增落点模型用例 5 条与画布集成用例 4 条(单卡/多选/拖回画布/pointercancel),resourceReferenceInput 新增批量事件与一次插入整批 chip 两条,共 39 条定向全绿。
- 文档:功能说明新增「拖拽引用」一节与完成清单条目、PRD 更新时间、decision-log 新增 2026-09-21 条目。
2026-09-21 03:37:43 +08:00
suzmii d56b5ff15a AGC「生成任务」侧栏移到画布右上角:开关常驻、面板贴着它展开
- 新增右上角锚点 game-resource-generation-tasks-anchor,挂在 game-workbench-stage 内;资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口。
- 开关改由 ResourceCanvasAssetGenerationTasksPanelView 自己渲染(保留 data-resource-generation-task-toggle 与计数属性、可访问名唯一),收起态只剩这一枚开关;工具条上两处重复入口与宿主那份 resourceAssetGenerationInFlightCount 一并删除。
- 面板自己不再定位,改由 max-height 封顶 + 内部滚动;进场 / 退场动画位移方向跟着锚点翻到正 X。
- 开关沿用原工具条那枚次级胶囊外观,颜色 / 圆角 / 字重 / 阴影继续全部走平台 token。
- 测试:面板用例新增「折叠只剩右上角开关」「开关计数与面板头部同源」「锚点按工作面分档」;样式用例改为锚点坐标 / 运行档下移 / 面板不再自定位 / 开关走 token;dismiss 用例改为右上角入口且工具条不再有生成任务按钮。
- 文档:PRD 3.10 与更新时间、decision-log 新增 2026-09-21 条目、画布验收待办登记第四批并勾掉该条。
2026-09-21 03:25:53 +08:00
kdletters 485ed50b26 AGC 七项交付效率优化并升级捆绑 Codex 到 0.155.1 (#439)
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
## 背景

净月潭案例(2026-09-19 21:53 → 09-20 01:26,3 小时 33 分)的问题不是模型慢,而是环境未就绪、验收靠模型自述、预算只约束单个工具入口、工具调用被 SDK 全局串行闸门卡住。本 PR 落地确认后的七项交付效率合同,并把捆绑 Codex 升到当前 npm latest。

## 结果

- 新建 Web 游戏在正式生成前由宿主自动预检:捆绑 Node/npm、真实 Vite 构建、受限浏览器桌面/移动截图;失败不启动生成或付费素材。
- 首个副作用前冻结交付合同,宿主保存权威证据与预算;证据齐全后先封口、排空并取得执行器完整退出证明,再产出交付报告,模型回复不再当作验收。
- 原生 shell、内置浏览器与托管命令、第三方 MCP 共用同一执行许可和累计执行时间;`validation.maxRuns` 改为执行/返修批次语义,另设 `maxTurnSeconds` 墙钟上限。
- 所有工具可并发:移除 SDK 全局串行的 `apply_patch`/`update_plan` 注册,改由宿主 `agc_apply_patch`(官方 parser + 当前回合写许可 + 受控进程树 + 短项目事务)和 `agc_update_plan` 提供等价能力;真实请求目录里已无串行注册,长 MCP 与补丁/计划实测在同一响应内重叠。
- 付费提交与本地写入绑定原回合原租约:容量与同动作锁等待可取消,封口、取消或预算耗尽后零新增提交;已越过提交边界的请求保留 operation ID 走 GET 对账,不自动重放。
- 新增请求与工具分段计时、有界并行批读和首轮上下文预取,未知耗时不补零。
- 捆绑 Codex 0.147.0 → 0.155.1:固定版本收敛到 `build_support/codex_bundle.rs` 单一声明,vendor 解析源码按 `rust-v0.155.1` 逐字节重取并更新 UPSTREAM 证据,适配 0.155 统一 exec(`exec_command`/`write_stdin`);macOS 侧车最小系统版本仍为 15.0。

## 验证

- 生产 CLI → 捆绑 Codex 0.155.1 → 本地 Responses/MCP 夹具 9/9:补丁、完成收尾、批次、只读/可写 MCP 并发、原生命令并发、原生资源并发、统一 exec 会话、期限终止(真实重叠 775 / 764 / 999 ms)。
- 真实模型目录 2/2;vendor 上游库 111 项;宿主定向与回归 106 项;真实 Windows 进程与取消 21 项;Node 侧门禁 52 项。
- 发行载荷:artifact-only 重建 NSIS,解包验证侧车清单 `codex-cli 0.155.1`、6 个组件哈希、打包后 `bin/codex.exe --version`、Node 运行时 2130 文件与双端 PNG、`--environment-check` ready。
- `check-config`、TypeScript、编码(4991 文件)、文档索引、`git diff --check`、`cargo fmt --check` 全部通过。

## 未覆盖

- 真实陶泥儿登录态 Provider 生成尚未执行(本机 `--llm-status` 返回 `authentication-required`)。
- macOS 侧车只在本机做静态 Mach-O 与清单解析,未在 macOS 上跑 `check-macos-bundle.mjs`。
- 全仓库聚合套件在本机仍因负载敏感的后台 mock-provider 用例而红,与本次改动无关:改动前的旧二进制同样失败,换单线程后相关用例 3/3 通过。

---------

Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/439
2026-09-21 02:29:29 +08:00
suzmii 1655a2eab4 Merge pull request '画布验收遗留问题与后续需求' (#419) from fix/agc-acceptance-followup into master
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 6m39s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 8m5s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 5m15s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m40s
Project CI / AI game creator shell Rust crates (push) Successful in 1m3s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 5m2s
Project CI / Backend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
2026-09-21 02:02:19 +08:00
kdletters df39d5a16f 修复客户端渠道与错误报告展示
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
客户端 debug 保留服务器选择,正式包按发布渠道连接服务

修复错误报告时间解析与超时路由脱敏

重做后台错误详情面板并补齐定向测试

同步渠道、诊断与后台展示文档
2026-09-21 01:52:35 +08:00
kdletters 914791d63c 修复项目列表状态互相污染
保留已确认项目的独立状态

隔离项目检查代次和迟到结果

增加坏项目刷新回归测试并同步项目文档
2026-09-21 01:48:19 +08:00
suzmii 5574f41129 文档同步:PR #419 合并前复核结论与全量测试口径
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 6m51s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 7m11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 7m23s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 8m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m49s
Project CI / Repository checks (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Successful in 6m11s
Project CI / Backend tests (pull_request) Successful in 7m45s
Project CI / Native shell tests (pull_request) Successful in 10m11s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m21s
- 待办文档补「合并前复核(2026-09-21,PR #419)」:仓库根全量 373 文件 / 4512 通过 / 34 跳过 / 0 失败、No Conflicts、范围核对(后端与契约零改动)
- decision-log 的验证段补同一口径;同时记下未复验的两类(真实客户端观感、远程 CI)都不阻塞合并
2026-09-21 01:45:18 +08:00
suzmii cd98e0603e 合并 master:音频后台化与 Mac 构建管线进分支,冲突按新口径解
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
- index.tsx 的 audio/video 提交:取 master 侧(#437 已把它改成后台任务队列 + 项目内账本),
  卡片浮层那套本地提交记录在这条链路上不再需要——队列本身就是它的可见性与幂等来源
- PRD 更新时间两边的两条事实合成一行;decision-log 保留 master 的音频章节与我们的
  派生任务/提交即关章节,并把「appSurface 16 条既有失败」更正为 cwd 假红的口径
- 样式守卫里撤掉「生图面板」这条探针:它走 AGC 自有类名,本就不吃
  `image-canvas-editor__*` 那一族(守卫自身加的空集断言把这条假探针暴露了出来)
- 合并后从仓库根验证:appSurface 538 通过 / 17 跳过 / 0 失败;AGC 定向 84 条、typecheck、
  check:encoding、git diff --check 全通过
2026-09-21 01:33:06 +08:00
suzmii 4bc0f04d74 补齐画布验收待办:review 修复与生图浮层美化的进展记录
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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 / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (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
- 记下在途拦截补齐生成动画、样式守卫扩到三面板、窄屏差异显式放行、生图浮层紧凑化与基类 token
- 更正「appSurface 16 条既有失败」的口径:那是从 apps 目录跑造成的 cwd 假红,仓库根跑为 0 失败
2026-09-21 01:24:56 +08:00
suzmii 329dd749eb 美化生图浮层:紧凑排布不再出现滚动条,画布卡片外形收敛到基类 token
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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 / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (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
- 字段行改两列(标签在左、控件在右),比例与尺寸并排一行,提示词 6 行降到 3 行
- 润色入口、参考图计数与取消 / 生成两个按钮收进同一行动作:原先它们各占一行把内容顶到
  几何上界之外,面板因此出现滚动条;重排后面板高度回落到上界以内(overflow 仅作兜底)
- 新增画布卡片基类 token(.game-project-workbench 上的 --game-canvas-card-*),取值照抄
  网页端美术画布的面板;占位卡与生成浮层/弹窗从此共用一套圆角、描边、底色与投影,各卡只表达差异
- 用例补一条结构回归:动作行同时装下润色 / 参考计数 / 两个动作,比例与尺寸同处一格
2026-09-21 01:24:03 +08:00
suzmii 2a2e4c1cea 按 review 修:动画在途拦截、样式守卫扩到三面板、窄屏差异显式放行、用例不再依赖 cwd
- 在途拦截补齐生成动画:提交记录的「属于哪张资源」从 draftKey 拆成 resourcePath,
  `resourceCanvasResourceEditSubmissionBlocksResource` 被快速编辑与生成动画共用;
  此前动画这条键对不上、拦不住,重开面板再提交会复用同一 operationId 卡在原生锁上,
  或(改了提示词)直接多出一笔付费生成
- 样式缺口守卫扩到三块面板(动画面板 / 快速编辑面板 / 生图面板),判据改为按
  「选择器分支 + 声明」逐条比对;两个面板的实测缺口一并照抄补上(快速编辑面板的
  `overflow: auto → visible` 与窄屏底栏两条按宿主差异显式登记,不静默漂移)
- 窄屏只搬动画面板那一支:网页端 ≤760px 会把快速编辑浮层也变成整宽底栏,AGC 的
  快速编辑是「居中贴资源卡」的绝对定位,跟随它会压住栏目工具栏与任务侧栏
- 用例路径不再依赖 cwd:新增 tests/repoPath.ts(按 import.meta.url 反推仓库根),
  19 个用例文件的 `resolve(process.cwd(), …)` 统一改用它;顺手修掉
  `HOST_STYLE_SHEETS.filter(existsSync)` 把仓库相对路径交给 cwd 解析的假红
- 清掉两处死代码:不可达的 `quickEditPanel.status === 'generating'` 判据与
  未被使用的 prune 导出
2026-09-21 01:23:35 +08:00
suzmii 12c1e5f3f8 Merge pull request '接入 Mac universal 构建管线与 Jenkins 归档 Job' (#430) from feat/jenkins-mac-build into master
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m37s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 8m0s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 8m1s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 8m51s
Project CI / AI game creator shell Rust crates (push) Successful in 1m20s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m15s
Project CI / Repository checks (push) Successful in 4m4s
Project CI / Frontend tests (push) Successful in 5m11s
Project CI / Backend tests (push) Successful in 6m35s
Project CI / Native shell tests (push) Successful in 8m41s
Project CI / AI game creator shell web tests (push) Successful in 4m44s
Reviewed-on: #430
2026-09-21 01:02:04 +08:00
suzmii b178431a57 Merge branch 'master' into feat/jenkins-mac-build
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 21s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 21s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 21s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 21s
Project CI / Native shell tests (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
2026-09-21 00:54:24 +08:00
suzmii 65b7ae0e2b 修正Mac构建入口的个人路径、超时余量与过期文档口径
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
Jenkinsfile的PATH改为按$HOME展开,移除写死的节点用户名
构建超时90分钟提高到150分钟并补失败/中断诊断,避免冷构建被中断在链接阶段
同步更新检查技术方案里dry-run默认值与调度接入的过期描述
新增回归断言:禁止个人Home路径、锁定超时余量与诊断文案
2026-09-21 00:48:55 +08:00
k88936 4be95f50ae Merge pull request 'Fix/let bgm and sfx async' (#437) from fix/let-bgm-and-sfx-async into master
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 15s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 12s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 16s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 16s
Project CI / AI game creator shell Rust smoke (push) Failing after 17s
Project CI / AI game creator shell Rust crates (push) Failing after 13s
Project CI / Frontend tests (push) Failing after 8s
Project CI / Backend tests (push) Failing after 16s
Project CI / Native shell tests (push) Failing after 16s
Project CI / Repository checks (push) Failing after 14s
Project CI / AI game creator shell web tests (push) Failing after 12s
Reviewed-on: #437
2026-09-21 00:48:49 +08:00
k88936 f19a003dec Merge branch 'master' into fix/let-bgm-and-sfx-async
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 17s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / Native shell tests (pull_request) Failing after 17s
Project CI / Frontend tests (pull_request) Failing after 8s
Project CI / AI game creator shell web tests (pull_request) Failing after 15s
Project CI / Repository checks (pull_request) Failing after 15s
2026-09-21 00:48:26 +08:00
suzmii 8cffbf6112 回退合并时被钩子误格式化的无关文件
过程中发现并修复:本地 pre-commit 钩子在提交合并时pipeline里prettier会重排无关文件,导致本PR携带与客户端无关的上下文
本提交把20个纯格式化文件恢复为主线内容,PR只保留macOS构建管线和发布链路相关改动
后续提交不再把这些文件纳入范围
2026-09-21 00:47:05 +08:00
suzmii eed72ba39a Merge branch 'master' into fix/agc-acceptance-followup
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
2026-09-21 00:38:08 +08:00
suzmii aa3e29f04e 修掉生图弹窗点输入框就弹素材选择框:label 转发点击到 @ 按钮
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (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
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
- 现象:点生成提示词输入框任意位置都会弹出「选择素材」。根因是这一格用 `<label>` 包住,而 `<label>` 会把点击转发给内部第一个可标注控件——正好是引用输入区里的「插入素材引用」按钮
- 处理:该格改为 `<div>` 包裹;可访问名仍由控件自身的 aria-label 提供(引用输入区 `ariaLabel` 与 `PlatformTextField` 的 `aria-label`),不依赖 label 关联
- 用例:`resourceCanvasAssetGenerationReferences` 新增「点提示词输入区不该自己弹出素材选择框,只有 @ 按钮才开」——先用旧代码复现出红灯再修
- 顺带扫过资源工作台其余 `<label>`:都只包纯 input / textarea(音频与视频生成面板用纯 textarea,没有 @ 按钮),无同类隐患
- 待办文档补「2026-09-21 第三批」:卡片浮层提交即关、动画面板样式整档照抄与守卫升级、本条 label 修复
2026-09-21 00:37:08 +08:00
k88936 04db93faab 给生成面板的「提交被拒 + 草稿归属」加 TODO(只留标记,不改行为)
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 8s
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / AI game creator shell web tests (pull_request) Failing after 16s
- `ResourceCanvasGenerationPanelView.submit()` 加 `TODO(resource-generation-panel-contract)`:说明
  `onSubmit` 只返回 `void` 导致宿主同步拒绝时草稿没人接走的缺口,以及「`onSubmit` 回报受理结果、
  只有受理成功才置位 `draftReleased` 并关闭」的修法
- `ResourceCanvasAssetGenerationPanelView.submit()` 加同名 TODO:图片类面板与音频面板是同一份
  契约,要改一起改
- `index.tsx` 的 `submitResourceCanvasGeneration` 同步拒绝分支加同名 TODO:指向面板侧修法与
  「这里改成 `return false`」的宿主侧动作
2026-09-21 00:29:55 +08:00
suzmii 55259683b8 删除角色动画面板里重复回显提示词的「动画描述」行
- 共享组件 ImageCanvasCharacterAnimationPanelView 不再渲染只读的生成文本回显(与上方输入区内容重复,且两个宿主都没有为它定义样式)
- 用例同步:原断言「生成文本:…」改为钉住该节点不再渲染,并保留「输入区确实收下这段文本」的检查
2026-09-21 00:28:26 +08:00
suzmii 5b4f9e961d 按美术画布原样补齐动画面板样式:关闭键落位、提示词态与 760px 整档
- 补 `.image-canvas-editor__generation-close` 的第二条同名规则(浮层内绝对定位):上一版只搬了基础外观,关闭键因此掉进网格流、独自占一行并居中
- 补提示词输入区的聚焦态与 aria-invalid 两档,以及 composer 内参数簇的透明底色与 hover / 展开态
- 新增 `@media (max-width: 760px)` 整档照抄(浮层落底部整宽、composer 定宽、动作竖排、参数与提交整宽),替换上一版按 720px 手写的那一小段,选择器与断点都与网页端逐字一致
- 守卫升级为「选择器 + 声明」逐条比对:同名规则只搬第一条不再判过;范围限定在 `image-canvas-editor__` 命名空间(共享 chrome 类与全局 reset 不归宿主 chrome 表照抄)
2026-09-21 00:26:31 +08:00
k88936 98e42e6dae 文档同步:音频无源生成的源媒体类型由 editKind 推出,不作为入参
- `docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`:音频入口复用无源生成链路的措辞
  从「传 `sourceMediaType: 'audio/mpeg'`」改成「源快照媒体类型由 `editKind` 推为 `audio/mpeg`
  (Create 模式不读该入参)」
- `docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` 第 7.9 节第 6 条同口径修正,避免文档继续
  描述一个代码已经不传、也不会被读的参数
2026-09-21 00:21:07 +08:00
suzmii 973e965f4f 卡片浮层改为提交即关:快速编辑 / 生成动画只负责交任务
- 快速编辑与生成动画点提交当帧即关面板(不等 IPC、不留等待态),进度、阶段与结果只由「生成任务」侧栏承载,与图片类生成面板同一口径
- 受理之后的失败不再重开面板:侧栏那条收口为失败 + 提示条,草稿回落成「未完成编辑」等用户重开;只有点击瞬间就失败(名称预检、源素材不在画布、无客户端)留在面板里
- 派生请求身份改为按「入口 + 资源路径」记忆:重开面板再提交时提示词没变就沿用同一 operationId / 幂等键,命中同一 operation 账本;派生成成功后清除
- 删除 canDismissResourceCanvasQuickEdit 与宿主里只为它服务的两块面板状态 ref,clearResourceCanvasFocus 回到「清选中 + 关两块浮层」的直线逻辑
- 三条既有用例按新口径改写为「重开面板再重试,身份不变」;PRD §3.10 与 decision-log 同步
2026-09-21 00:19:42 +08:00
k88936 f2a3ba7aca 音频提交上下文按任务 id 各自留存,重叠提交不再互相覆盖
- `resourceGenerationAudioSubmissionRef` 从单槽改成按任务 id 索引的 Map(键是 task id,值里
  不再重复存 taskId)
- 提交写入改成 `set(task.taskId, …)`,收口读取改成 `get(settlement.taskId)` + `delete`,切项目
  改成 `clear()`
- 原先两条音频提交重叠时,后一次提交会覆盖前一条的提交上下文,前一条「后端从未受理」的即时
  失败因此找不到自己的恢复上下文,只走通用失败提示、丢掉「面板连原草稿带回来」
- 新增宿主用例「两条音频提交重叠:第一条未被受理时,它的面板仍会被带回来」:给测试宿主加
  `holdFirstAssetGenerationStart` 开关,把第一次派发挂在受理前,复现两条提交重叠
2026-09-21 00:17:05 +08:00
k88936 7b1bc1d3e8 生成任务先登记 live 再落账本,关掉被判成中断残留的窗口
- `start_local_project_asset_generation` 的音频分支改为一律先登记 live、再写排队记录(原先顺序
  相反,中间窗口里并发 `list_local_project_asset_generations` 会把这条排队记录当成上次运行的
  残留收口成失败)
- 图片类分支同一处顺序问题一并改正:同一条命令的两条通道共用同一条顺序约束
- 新增 `register_live_task_id`:登记并返回「本轮是否真的插入了新 id」,落账失败时只回滚本轮插入
  的那条登记,避免删掉同 id 那个正在运行任务的 live 登记
- 新增用例 `a_failed_ledger_write_only_takes_back_the_live_registration_it_inserted` 覆盖该回滚判据
2026-09-21 00:14:11 +08:00
k88936 ae0ab0ddf4 音频后台生成不再传形同虚设的 sourceMediaType
- `run_local_project_audio_generation_at` 提交 `derive_local_project_resource_at` 时把
  `source_media_type` 从 `Some("audio/mpeg")` 改成 `None`(`generation_mode: Create` 下源快照的
  媒体类型由 `edit_kind` 推出,这条通道根本不读该入参,留着会让读者以为它有作用)
- 该函数文档补一句说明「Create 模式不读 `input.source_media_type`」,避免后续又被加回去
2026-09-21 00:11:31 +08:00
k88936 fca111239c AGC 音频账本 kind 判定改成显式穷举,去掉静默兜底
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 8s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
- `begin_local_project_audio_generation_task` 的 `edit_kind → 账本 kind` 映射显式列出
  `SoundEffect` 与 `BackgroundMusic` 两个成员,不再用 `_ =>` 兜底
- 其余 `LocalProjectResourceEditKind` 变体在此处直接返回错误,避免新增音频变体(或上游白名单被
  放宽)时被静默记成音效
2026-09-21 00:05:44 +08:00
suzmii 573f9f447a 补齐 AGC「生成动画」面板的宿主样式并加样式缺口守卫
- resourceCanvasChrome.css 逐条照抄 src/index.css 的角色动画面板规则:面板 / 提示词 textarea / 动作预设 / 底部动作行 / 提交按钮,以及关闭键、素材缩略图与 reference-chip 全族
- 窄屏(≤720px)与网页端同口径:面板落到底部整宽、动作竖排、提交按钮整宽
- 新增 tests/resourceCanvasCharacterAnimationPanelStyle.test.tsx:渲染共享动画面板,按 src/main.tsx 的真实 import 关系解析 AGC 实际加载的样式表,断言「网页端给出规则的类 AGC 必须也有一条」并抽查撑开布局的关键声明
- 变异验证:临时移除本次补的样式块,守卫用例列出缺规则的 15 个类并红灯
2026-09-20 23:48:53 +08:00
k88936 9a6875b4ce Merge remote-tracking branch 'origin/master' 2026-09-20 23:45:36 +08:00
k88936 99fb6c38c1 AGC 音频生成并入图片类那份后台任务账本
- 原生:`start_local_project_asset_generation` 新增可选入参 `idempotencyKey`,并在音频 kind 上分叉;图片类载荷与分支逐字未改
- 原生:新增音频提交期收口 `prepare_local_project_audio_generation`(kind / 提示词上限 / 素材名 / operation 身份 / 幂等键)
- 原生:新增 `run_local_project_audio_generation_at`,在派发时刻读项目 revision 并复用既有音频无源生成实现,不复制生成逻辑
- 原生:新增 `begin_local_project_audio_generation_task` / `run_local_project_audio_generation_task`,音频任务落同一份项目内账本并写 running → completed / failed,跑完没登记素材按失败收口
- 原生:补两条用例——被拒绝的提交零写入账本、音频任务落在同一账本且 kind 正确
- 前端任务模型:新增音频任务与 `idempotencyKey` 字段,恢复出来的历史任务不带它也不承接重试,入口文案扩到音频栏目
- 前端队列:按 kind 分流派发载荷,音频只发任务身份(任务 id 即 operation id)与幂等键
- 前端面板:音频生成面板改为点「生成」同步提交并立即关闭,删除「生成中…」「后台运行并关闭」与输入锁定
- 前端宿主:音频提交改为同步入队并展开「生成任务」侧栏,只有「后端从未受理」才连原草稿与原请求身份重开面板
- 前端清理:删除随本次改动失效的 `resourceCanvasGenerationSourceId` 与宿主里不再使用的 import
- 测试:面板 / 队列 / 宿主生命周期 / 落点 / appSurface 改按后台账本口径断言,并补「未受理即时失败重开」用例(已用移除重开逻辑的变异验证其非空)
- 文档:PRD §3.10 / §7.9、AGC 底部工具栏入口矩阵、V3 端到端验收 S11a 同步为音频后台化口径
- 文档:新增里程碑与实施计划(含验收证据矩阵),并在 decision-log 记下「音频并入后台任务账本」这条长期约定
2026-09-20 23:32:09 +08:00
suzmii 5b73082ad0 补齐画布验收第二批:派生任务进生成任务侧栏、提交中让出草稿与重复提交防护
- 新增 resourceCanvasResourceEditTaskModel:原生资源编辑阶段到面板状态/文案的映射,本地提交记录与原生待办按 operationId 合并,派生任务的终态与耗时判据
- 「生成任务」侧栏同时收派生/修改类任务:与图片类生成任务同一条列表、同一套分栏与状态徽标,多一行提示词,样式沿用既有卡片类
- 快速编辑、生成动画、视频/音效/背景音乐三条派生提交:按下提交当帧写入本地记录并在侧栏可见,成功后收口为已完成、失败带原因并重读原生待办
- 「管理未完成编辑」不再列出正在提交的那一笔草稿,失败后自动回落,提示词与 @ 引用不丢
- 同一张素材存在在途提交时,重新打开面板再提交会被拦下并给出原因,避免重复派生与重复计费
- 侧栏与工具条入口的在途计数把派生任务一并计入
- 新增 tests/resourceCanvasResourceEditTasks.test.ts(9 条);resourceCanvasQuickEditDraft.test.tsx 补两组宿主用例(提交当帧进侧栏并收口、在途重复提交被拦),两处新判据均做过变异验证
- 同步 PRD 派生任务侧栏口径、decision-log 与画布验收待办文档
2026-09-20 23:04:13 +08:00
suzmii d354e5e7c3 修复发号Job未授权调度器读取归档导致整轮调度失败
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 9s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell web tests (pull_request) Failing after 13s
发号Jenkinsfile显式copyArtifactPermission授权调度管线
生产门禁新增该授权规则避免回归
运维文档补充生产者到调度器的Copy Artifact映射与失败后果
2026-09-20 22:58:39 +08:00
suzmii 62bffebf8e Merge remote-tracking branch 'origin/master' into feat/jenkins-mac-build
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 20s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 21s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-20 22:54:29 +08:00
suzmii dbbbef8a0d 把macOS渠道纳入定时发布并共用总版本号
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
调度器在发号后同时触发macOS渠道构建,透传同一个总版本号与固定revision
macOS Job改为默认真实发布,演练需显式勾选
新增SKIP_IF_SUPERSEDED:节点离线期间排队的旧构建在恢复后自行让位
补充生产门禁规则与回归断言
同步调度与运维文档口径
2026-09-20 22:47:01 +08:00
suzmii 88090fcc6e Merge remote-tracking branch 'origin/master' into feat/jenkins-mac-build
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 17s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-20 21:34:08 +08:00
suzmii a0d432b63d Merge remote-tracking branch 'origin/master' into fix/agc-acceptance-followup
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
2026-09-20 21:27:23 +08:00
suzmii d4de81c128 记录Mac Job与节点描述的维护口径
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 17s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
补充Job描述与节点描述属Jenkins侧元数据、不随仓库同步的说明
记录该Job的config.xml POST返回500、须用脚本接口原地改描述以保留历史
同步并行度默认值与描述口径
2026-09-20 21:19:23 +08:00
suzmii c0154c2aed 优化Mac构建管线并行度与入口健壮性
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 12s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 8s
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
并行度参数默认改为8以吃满节点,并在说明里记录其作为rustc jobserver令牌上限的影响
产品名统一从Tauri配置推导,校验脚本从Info.plist读取可执行名,改产品名不再静默失效
隔离smoke改用ditto --clone复制副本,实测整轮6.8秒
Agent工作区守卫改为按目录约定匹配,节点改名后仍成立
签到同时取master以便解析上次发布commit,失败仅降级更新摘要
补充上述行为的回归断言
2026-09-20 21:00:05 +08:00
suzmii c4b391cb84 记录Mac构建管线首次端到端验收结果
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 17s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 12s
回填build3成功的产物、验签、dry-run与耗时证据
补充--no-sign连带跳过更新包签名、残留产物不幂等两处踩坑
记录主crate重复编译这一剩余耗时优化点
2026-09-20 20:44:12 +08:00
suzmii bd0f9f481a 修复Mac构建产物不幂等导致的DMG重建失败
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 17s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 17s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
构建前删除本次将写出的DMG、更新包与签名,避免同名文件让hdiutil失败
hdiutil create显式覆盖,杜绝残留对象影响本次产物
补充回归断言,防止旧签名让验签门禁误通过
2026-09-20 19:58:30 +08:00
suzmii 6fb17de9db Merge remote-tracking branch 'origin/master' into fix/agc-acceptance-followup
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 15s
2026-09-20 19:25:45 +08:00
suzmii 57017ee046 修复Mac入口误用--no-sign导致更新包无签名
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
移除build-macos-ci的--no-sign:该标志会连带跳过updater的minisign签名
改为剥离APPLE_*凭据跳过Apple签名,保持更新包签名能力
构建清单按codesign实测记录appleSigned与签名类型
补充回归断言与运维、更新规范口径
2026-09-20 19:11:30 +08:00
suzmii bdf6d0f556 Mac构建并行度改为可调参数并记录首次缓存表现
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Failing after 11s
CARGO_BUILD_JOBS从写死4改为Job参数默认6,适配8核共用机器
补充并行度与sccache首次全未命中的运维口径
增加Jenkinsfile并行度参数的回归断言
2026-09-20 18:51:36 +08:00
suzmii bb1df3c6da Merge remote-tracking branch 'origin/master' into feat/jenkins-mac-build
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Native shell tests (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
# Conflicts:
#	docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md
2026-09-20 17:45:38 +08:00
suzmii 1e17d2c852 补齐画布验收第一批:润色回包提示、未提交草稿恢复与派生名预检
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m57s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m44s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m21s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m27s
Project CI / Frontend tests (pull_request) Successful in 7m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m22s
Project CI / Native shell tests (pull_request) Successful in 10m13s
AI 润色回包与原文相同时给出明确提示,不再落下点了等于没点的「恢复原文」假入口
润色「与原文相同」的判据改为规范化之后要写回宿主的文本,回包被长度上限截回原文同样算没变化
共享聊天输入区显示润色提示,并让提示随用户手改提示词失效
快速编辑未提交草稿改按资源路径归属,收起面板或被旁路重投影后都不再静默丢掉
恢复入口按路径与本会话草稿对号,并区分「本会话未提交」与原生账本条目
账本读取失败时入口仍报出本会话草稿条数,「继续编辑」重开面板回填提示词与 @ 引用
派生资源名预检接入快速编辑与角色动画两条提交路径,早于正规化写盘
名称预检自身抛错时回写面板失败态,不再留下「点了没反应」的提交按钮
浮层关闭判定改在捕获阶段冻结,避免浮层内按钮卸载自己时整块面板被自己人收掉
恢复入口动作按钮收进统一容器,补移动端纵向排布与禁用态样式
空草稿表改工厂函数,跨语言对表用例按模块路径解析 Rust 源码
补回归用例:旁路重投影后的草稿、原样回包提示、提交前名称拦截、草稿表读写与跨卡隔离
同步 decision-log 与画布验收待办的进展说明
2026-09-20 17:36:02 +08:00
suzmii b1cadd0cc8 Mac 渠道接入更新包签名、OSS 上传与渠道清单
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m25s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m34s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m17s
Project CI / Backend tests (pull_request) Successful in 11m30s
Project CI / Repository checks (pull_request) Successful in 11m36s
Project CI / Frontend tests (pull_request) Successful in 13m46s
Project CI / Native shell tests (pull_request) Successful in 16m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m31s
问题:mac 侧此前只有 archive-only 构建,dev-mac 渠道不产出更新包、不上传、无渠道
清单,客户端拿不到 macOS 更新(里程碑曾因签名/公证凭据未就绪而暂缓)。

改动:
- build-macos-ci.mjs 改为 dev-mac 发布入口:开启 createUpdaterArtifacts 产出
  .app.tar.gz + .sig,生成 universal DMG 与渠道清单 latest.json,构建后用产物内
  烘焙的公钥复核签名,再按 AGC_RELEASE_DRY_RUN 决定是否上传 OSS
- 新增 verify-updater-signature.mjs(minisign ED 预哈希校验)+ 单测:验签失败或
  keyId 不一致立即失败关闭,绝不写 OSS
- Jenkinsfile:新增 AGC_RELEASE_VERSION / AGC_RELEASE_DRY_RUN(默认开启)/
  AGC_UPDATE_RELEASE_NOTES / OSSUTIL_BIN 参数;withCredentials 注入
  AgcUpdaterSigningKey(+密码) 与 AliyunAccessKeyId/Secret;归档补 latest.json、
  .sig 与更新摘要
- 既有单测口径更新:archive-only → 发布型,并新增「必须先验签再上传」守卫
- 文档:技术方案、里程碑、运维文档同步;Apple 签发与公证暂缺显式记录为
  未验证项(--no-sign、appleSigned=false、notarized=false)

验证:单测 11/11 + 42/42;Jenkinsfile 四个 sh 块语法通过;伪造 bundle 端到端
验证清单(两平台同 URL/同签名、universal 首装包被正确选中)、真实 Tauri 签名
验签通过、错钥匙报 keyId 不一致、篡改报校验失败、dry-run 上传计划顺序正确。
2026-09-19 17:49:25 +08:00
suzmii 261228ed3f 修复 universal 首装 DMG 选择以兼容主线新增校验
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m38s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m46s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m49s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m20s
Project CI / Repository checks (pull_request) Successful in 7m8s
Project CI / Native shell tests (pull_request) Successful in 13m34s
Project CI / Frontend tests (pull_request) Successful in 12m58s
Project CI / Backend tests (pull_request) Successful in 14m59s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m8s
合并 origin/master 后,e3682fd06 新增的 selectFirstInstallArtifact 对
universal-apple-darwin 会推导出架构 x64,匹配不到本管线产出的
陶泥儿_<version>_universal.dmg,createUpdateManifest 直接抛
「首装 DMG 必须唯一匹配本次版本 x 和架构 x64,找到 0 个」。

- build-release.mjs:为 universal-apple-darwin 增加分支,按
  _<version>_universal.dmg 唯一匹配(与 build-macos-ci.mjs 的命名一致)
- build-release.test.mjs:createDmgFixture 支持 universal 命名;universal 用例
  补首装包断言,锁定两个平台键共用同一个 DMG

验证:管线同款 node --test 33/33 通过(合并前 32/33);prepare-macos-codex
与 release-oss 用例同跑通过。
2026-09-19 17:03:18 +08:00
suzmii 8c17e40d7d 合并 origin/master 并解决 pitfalls.md 冲突
- 冲突仅 1 处:踩坑文档顶部双方各新增条目(本次合并相对 master 落后 88 个提交)
- 解决方式:保留 master 的 2026-09-19 / 2026-09-17 两条在前,分支的
  「universal 主程序必须配套双架构原生依赖」在后;另补齐标题前缺失的空行
- 本次提交内容为上游已合入 master 的文件,跳过 lint-staged/Husky,
  避免对 master 侧 99 个 js/ts 文件重新格式化产生无意义改动
2026-09-19 17:01:57 +08:00
suzmii 237e440057 Mac Jenkins节点接入sccache编译缓存
原因:macOS universal 构建此前在 Jenkinsfile 与 build-macos-ci.mjs 两处把
RUSTC_WRAPPER 清空,依赖 crate 每次从零编译且双架构各编译一遍,逼近 90 分钟超时。

改动:
- Jenkinsfile 对齐 Linux 生产管线:RUSTC_WRAPPER=sccache、CARGO_INCREMENTAL=0、
  SCCACHE_CACHE_SIZE=20G,SCCACHE_DIR 固定到稳定缓存根
  ${HOME}/caches/genarrative-jenkins/agc-macos/sccache(可用
  GENARRATIVE_AGC_MACOS_CACHE_ROOT 覆盖),并输出 sccache --show-stats 供判断命中率。
- build-macos-ci.mjs 不再清空 RUSTC_WRAPPER,wrapper 由管线显式声明。
- 保留 CARGO_TARGET_DIR 指向 workspace 内 target,跨构建继续复用增量产物。
- 文档补充缓存根、覆盖变量与回退语义。

验证:sh -n/bash -n 通过;管线同款 node --test 34 项全通过;本地探针证明空缓存
首次 miss 编译、清空 target 后再次构建命中缓存不再编译。
2026-09-19 16:55:00 +08:00
suzmii fc46cabb75 补充Mac Jenkins节点工具链路径
为LaunchAgent构建环境注入Node、npm、Cargo和Homebrew路径
2026-09-18 22:34:49 +08:00
suzmii 762f037150 修复Mac Jenkins节点工作区根目录回退
移除不兼容的节点环境变量配置依赖
在Jenkinsfile中使用专用Agent根目录默认值
避免节点分配阶段环境变量属性解析失败
2026-09-18 21:57:52 +08:00
suzmii 48985d3447 接入Mac通用构建与Jenkins归档管线
补齐macOS universal双架构Codex资源与构建校验
统一发布清单和检查脚本支持universal目标
新增锁定原生依赖完整性校验与隔离构建smoke
新增Mac Jenkins Agent归档构建Job与本机构建接入规范
2026-09-18 19:34:23 +08:00
suzmii ca80cb28d2 登记首批合并后的画布验收遗留问题
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m9s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m2s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / Backend tests (pull_request) Successful in 11m35s
Project CI / Frontend tests (pull_request) Successful in 11m42s
Project CI / Repository checks (pull_request) Successful in 11m40s
Project CI / Native shell tests (pull_request) Successful in 14m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 9m39s
从master已合并的PR412建立后续修复范围。
区分动画与编辑等缺陷、卡片与任务浮层需求及未验证风险,不宣称已修复。
2026-09-18 15:57:32 +08:00
387 changed files with 62774 additions and 3320 deletions
+2
View File
@@ -4,3 +4,5 @@
src/components/AdventurePanel.tsx
src/data/customWorldCharacterLoadout.ts
dist_check_monster_position/**
# 固定上游UTF-8测试刻意包含U+FFFDupstream-integrity.test.mjs逐字节验证来源hash。
apps/ai-game-creator-shell/src-tauri/vendor/codex-utils-path-uri/src/api_path_string_tests.rs
+12 -74
View File
@@ -31,28 +31,26 @@ env:
RUSTC_WRAPPER: ''
CARGO_BUILD_RUSTC_WRAPPER: ''
# job 声明顺序就是 runner 领取顺序,因此把最长尾的客户端 Rust 门禁排在前面,
# job 声明顺序就是 runner 领取顺序,因此把两条最长尾的客户端 Rust lane 排在前面,
# 让它在最少的等待下占用并发槽位;其余 job 按时长递减排列。
#
# 客户端(微信壳 / Expo 移动壳 / Tauri 桌面壳 / AI 游戏创作壳)门禁原先全部串在
# `Native shell tests` 一个 job 里,实测 18 分 37 秒。现在按门禁组拆成一个分组一个 job:
# `Native shell tests`(契约 + H5 / 微信 / 移动 / 桌面壳门禁 + 发布构建 smoke)、
# `AI game creator shell web tests`typecheck + 壳内测试)、`AI game creator shell
# Rust shard 1/4` 到 `4/4`AGC 壳 bin 单测按名单分 4 片)、`AI game creator shell
# Rust smoke`agent-run smoke)与 `AI game creator shell Rust crates`AGC 壳依赖的
# Rust lane 1/2`、`lane 2/2`(每条 lane 顺序运行两片 AGC 壳 bin 单测)、`AI game creator
# shell Rust smoke`agent-run smoke)与 `AI game creator shell Rust crates`AGC 壳依赖的
# 共享 / 平台 crate 测试)。各自的命令与拆分前逐一对应,本地
# `npm run check:native-shells` 仍是同一条串行序列。
#
# AGC 壳的 bin 单测(2466 条)按名单分 4 片、一片一个 job片内保持 `--test-threads=1`
# (当年线程并行会互相干扰的是进程内后台锁与异步终态),片与片之间靠 job 级并发摊开。
# 不要改回「一个 job 里多进程并行这几片」:同一容器内它们会争抢共享 HOME、target 与固定
# 临时路径,实测比整套串行还慢。每个分片 job 都会自校验「片并集等于全集且互斥」。
# AGC 壳的 bin 单测(2466 条)按名单分 4 片、片内保持 `--test-threads=1`;两条 lane
# 各自顺序运行两片,让一次 job 只做一次依赖预热,同时避免同一容器内多进程并行争抢
# 共享 HOME、target 与固定临时路径。每个分片调用仍会自校验「片并集等于全集且互斥」。
jobs:
# AGC 壳自身的 Rust bin 单测分片,4 片各自独立 job 并发执行、片内仍保持
# `--test-threads=1`。这里不装 npm 依赖:壳 Rust 门禁只用 cargo 与 node 内建模块,
# 也只需要 AGC 壳自己那份锁定依赖。
ai-game-creator-shell-rust-shard-1:
name: AI game creator shell Rust shard 1/4
# AGC 壳自身的 Rust bin 单测由两条 lane 承载,每条 lane 顺序运行两片;这里不装 npm
# 依赖:壳 Rust 门禁只用 cargo 与 node 内建模块,也只需要 AGC 壳自己那份锁定依赖。
ai-game-creator-shell-rust-lane-1:
name: AI game creator shell Rust lane 1/2
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
@@ -87,41 +85,11 @@ jobs:
- name: Run AI game creator shell Rust shard 1/4
run: npm run check:native-shells:agc-rust-shard-1
ai-game-creator-shell-rust-shard-2:
name: AI game creator shell Rust shard 2/4
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Prepare AI game creator shell Rust dependencies
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
exit 1
fi
sleep $((attempt * 2))
done
- name: Run AI game creator shell Rust shard 2/4
run: npm run check:native-shells:agc-rust-shard-2
ai-game-creator-shell-rust-shard-3:
name: AI game creator shell Rust shard 3/4
ai-game-creator-shell-rust-lane-2:
name: AI game creator shell Rust lane 2/2
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
@@ -153,36 +121,6 @@ jobs:
- name: Run AI game creator shell Rust shard 3/4
run: npm run check:native-shells:agc-rust-shard-3
ai-game-creator-shell-rust-shard-4:
name: AI game creator shell Rust shard 4/4
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Prepare AI game creator shell Rust dependencies
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
exit 1
fi
sleep $((attempt * 2))
done
- name: Run AI game creator shell Rust shard 4/4
run: npm run check:native-shells:agc-rust-shard-4
+4
View File
@@ -47,12 +47,16 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-arm64/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-x64/
/plugins/agc-cocos-editor/native/payload/
/plugins/agc-unity-editor/dotnet/**/bin/
/plugins/agc-unity-editor/dotnet/**/obj/
/plugins/agc-unity-editor/dotnet/publish/
/plugins/agc-unity-editor/dotnet/native-build/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
/apps/mobile-shell/.expo/
@@ -3,12 +3,14 @@ import { afterEach, expect, test, vi } from 'vitest';
import {
createAdminAccount,
executeAdminRechargeRefund,
getAdminAgcTemplates,
getAdminFeatureGateConfig,
getAdminUserDetail,
listAdminRechargeOrders,
reconcileAdminUserConsumption,
resolveAdminRechargeRefundManualReview,
updateAdminAccount,
updateAdminAgcTemplate,
uploadAdminEditorShowcaseCampaignImage,
upsertAdminFeatureGateConfig,
upsertProfileWalletConfig,
@@ -18,6 +20,51 @@ afterEach(() => {
vi.unstubAllGlobals();
});
test('模板管理读取和更新复用认证封装,提交 revision 和封面但不提交 ZIP 或版本', async () => {
const library = { revision: 'revision-new', writable: true, templates: [] };
const fetchMock = vi.fn().mockImplementation(
async () =>
new Response(JSON.stringify({ ok: true, data: library }), {
status: 200,
}),
);
vi.stubGlobal('fetch', fetchMock);
const controller = new AbortController();
expect(await getAdminAgcTemplates('admin-token', controller.signal)).toEqual(
library,
);
const update = {
expectedRevision: 'revision-old',
title: '空白模板',
summary: '简介',
tags: ['2D'],
enabled: true,
cover: { contentType: 'image/png', dataBase64: 'aW1hZ2U=' },
};
expect(
await updateAdminAgcTemplate('admin-token', 'template/1', update),
).toEqual(library);
expect(fetchMock.mock.calls[0]).toEqual([
'/admin/api/agc-templates',
expect.objectContaining({
method: 'GET',
signal: controller.signal,
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
}),
]);
expect(fetchMock.mock.calls[1]).toEqual([
'/admin/api/agc-templates/template%2F1',
expect.objectContaining({
method: 'PUT',
headers: expect.objectContaining({
Authorization: 'Bearer admin-token',
'Content-Type': 'application/json',
}),
body: JSON.stringify(update),
}),
]);
});
test('后台账号创建和更新同时携带 Tab 与独立操作权限', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
+20
View File
@@ -1,5 +1,6 @@
import type {
AdminAccountListResponse,
AdminAgcTemplateLibraryResponse,
AdminConfirmEditorShowcaseCampaignImageUploadRequest,
AdminCreateAccountRequest,
AdminCreateAccountResponse,
@@ -47,6 +48,7 @@ import type {
AdminTrackingEventListResponse,
AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
AdminUpdateAgcTemplateRequest,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
@@ -1176,3 +1178,21 @@ export function saveAgcModelCatalog(
{ token, method: 'PUT', body },
);
}
export function getAdminAgcTemplates(token: string, signal?: AbortSignal) {
return request<AdminAgcTemplateLibraryResponse>('/admin/api/agc-templates', {
token,
signal,
});
}
export function updateAdminAgcTemplate(
token: string,
id: string,
body: AdminUpdateAgcTemplateRequest,
) {
return request<AdminAgcTemplateLibraryResponse>(
`/admin/api/agc-templates/${encodeURIComponent(id)}`,
{ token, method: 'PUT', body },
);
}
+34
View File
@@ -144,6 +144,8 @@ export interface AdminErrorReportListResponse {
}
export interface AdminErrorReportDetail extends AdminErrorReportEntry {
firstFingerprint?: string;
firstSource?: string;
note?: string;
events: Array<Record<string, unknown>>;
logNames: string[];
@@ -1031,3 +1033,35 @@ export interface AdminAgcModelCatalog {
defaultModelId: string;
models: AdminAgcModel[];
}
export interface AdminAgcTemplatePayload {
id: string;
title: string;
summary: string;
tags: string[];
runtime: string;
engine: string;
engineVersion: string;
templateVersion: string;
enabled: boolean;
coverUrl: string;
zipSizeBytes: number;
}
export interface AdminAgcTemplateLibraryResponse {
revision: string;
writable: boolean;
templates: AdminAgcTemplatePayload[];
}
export interface AdminUpdateAgcTemplateRequest {
expectedRevision: string;
title: string;
summary: string;
tags: string[];
enabled: boolean;
cover?: {
contentType: string;
dataBase64: string;
};
}
+7
View File
@@ -19,6 +19,7 @@ import {
} from '../auth/adminAuthStore';
import { AdminAccountsPage } from '../pages/AdminAccountsPage';
import { AdminAgcModelsPage } from '../pages/AdminAgcModelsPage';
import { AdminAgcTemplatesPage } from '../pages/AdminAgcTemplatesPage';
import { AdminDashboardPage } from '../pages/AdminDashboardPage';
import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage';
import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
@@ -294,6 +295,12 @@ export function AdminApp() {
{activeRouteId === 'agc-models' ? (
<AdminAgcModelsPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{activeRouteId === 'agc-templates' ? (
<AdminAgcTemplatesPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'editor-showcase' ? (
<AdminEditorShowcaseReviewPage
token={token}
+1
View File
@@ -53,6 +53,7 @@ const routeIcons = {
'project-snapshots': FolderArchive,
accounts: Users,
'agc-models': ListChecks,
'agc-templates': Images,
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
export function AdminShell({
@@ -147,3 +147,30 @@ test('项目工程入口对 owner 与已授权 member 开放且可分配权限',
}),
).not.toContainEqual(route);
});
test('模板管理只对 owner 或具有 agc-templates 权限的 member 可见', () => {
expect(adminRoutes).toContainEqual({
id: 'agc-templates',
label: '模板管理',
hash: '#agc-templates',
});
expect(resolveAdminRoute('#agc-templates')).toBe('agc-templates');
expect(routeHash('agc-templates')).toBe('#agc-templates');
expect(
getAccessibleAdminRoutes({ accountRole: 'owner', tabPermissions: [] }).some(
(route) => route.id === 'agc-templates',
),
).toBe(true);
expect(
getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: ['agc-templates'],
}).map((route) => route.id),
).toEqual(['agc-templates']);
expect(
getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: ['editor-assets'],
}).some((route) => route.id === 'agc-templates'),
).toBe(false);
});
+2
View File
@@ -18,6 +18,7 @@ export type AdminRouteId =
| 'editor-assets'
| 'project-snapshots'
| 'agc-models'
| 'agc-templates'
| 'accounts';
export type AdminTabPermission = Exclude<
@@ -53,6 +54,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
hash: '#editor-generation-pricing',
},
{ id: 'agc-models', label: 'AGC 模型', hash: '#agc-models', ownerOnly: true },
{ id: 'agc-templates', label: '模板管理', hash: '#agc-templates' },
{ id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' },
{ id: 'editor-assets', label: '素材查询', hash: '#editor-assets' },
{ id: 'project-snapshots', label: '项目工程', hash: '#project-snapshots' },
+1
View File
@@ -1,3 +1,4 @@
import '@genarrative/shared/styles.css';
import './styles/admin.css';
import { StrictMode } from 'react';
@@ -0,0 +1,498 @@
// @vitest-environment jsdom
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import {
AdminApiError,
getAdminAgcTemplates,
updateAdminAgcTemplate,
} from '../api/adminApiClient';
import type {
AdminAgcTemplateLibraryResponse,
AdminAgcTemplatePayload,
} from '../api/adminApiTypes';
import { AdminAgcTemplatesPage } from './AdminAgcTemplatesPage';
vi.mock('../api/adminApiClient', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/adminApiClient')>()),
getAdminAgcTemplates: vi.fn(),
updateAdminAgcTemplate: vi.fn(),
}));
const template: AdminAgcTemplatePayload = {
id: 'cocos-empty-2d',
title: '空白 2D',
summary: '二维项目',
tags: ['2D', '入门'],
runtime: 'cocos',
engine: 'Cocos Creator',
engineVersion: '3.8.8',
templateVersion: '1',
enabled: true,
coverUrl: 'https://example.test/cover.png',
zipSizeBytes: 2048,
};
const library: AdminAgcTemplateLibraryResponse = {
revision: 'rev-1',
writable: true,
templates: [
template,
{
...template,
id: 'godot-starter',
title: 'Godot 起步',
runtime: 'godot',
engine: 'Godot',
engineVersion: '4',
tags: ['3D'],
enabled: false,
},
],
};
beforeEach(() => {
vi.mocked(getAdminAgcTemplates)
.mockReset()
.mockImplementation(async () => structuredClone(library));
vi.mocked(updateAdminAgcTemplate)
.mockReset()
.mockImplementation(async (_token, id, update) => ({
...structuredClone(library),
revision: 'rev-2',
templates: library.templates.map((entry) =>
entry.id === id
? {
...entry,
title: update.title,
summary: update.summary,
tags: update.tags,
enabled: update.enabled,
}
: entry,
),
}));
vi.stubGlobal(
'URL',
Object.assign(class extends URL {}, {
createObjectURL: vi.fn(() => 'blob:template-cover'),
revokeObjectURL: vi.fn(),
}),
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
async function openEditor() {
fireEvent.click(await screen.findByRole('button', { name: '编辑 空白 2D' }));
return screen.getByRole('dialog', { name: '编辑模板' });
}
async function confirmWrite() {
const confirmation = await screen.findByRole('dialog', { name: '确认操作' });
fireEvent.click(within(confirmation).getByRole('button', { name: '确认' }));
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((next, fail) => {
resolve = next;
reject = fail;
});
return { promise, resolve, reject };
}
test('按名称、ID、标签、运行时和上下架状态筛选,展示版本与封面', async () => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
await screen.findByText('空白 2D');
expect(screen.getByText('Cocos Creator 3.8.8')).not.toBeNull();
expect(screen.getAllByText('模板 1')).toHaveLength(2);
expect(screen.getAllByText('2.0 KiB')).toHaveLength(2);
expect(
screen.getByRole('img', { name: '空白 2D封面' }).getAttribute('src'),
).toBe(template.coverUrl);
for (const value of ['空白', 'cocos-empty-2d', '入门']) {
fireEvent.change(screen.getByLabelText('搜索模板'), { target: { value } });
expect(screen.getByText('空白 2D')).not.toBeNull();
expect(screen.queryByText('Godot 起步')).toBeNull();
}
fireEvent.change(screen.getByLabelText('搜索模板'), {
target: { value: '' },
});
fireEvent.change(screen.getByLabelText('运行时'), {
target: { value: 'godot' },
});
expect(screen.queryByText('空白 2D')).toBeNull();
fireEvent.change(screen.getByLabelText('上架状态'), {
target: { value: 'enabled' },
});
expect(screen.getByText('没有符合筛选条件的模板')).not.toBeNull();
});
test('独立弹窗保存经写确认,只提交展示字段和冻结 revision', async () => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
const editor = await openEditor();
expect(editor.closest('.admin-agc-templates')).toBeNull();
fireEvent.change(within(editor).getByLabelText('名称'), {
target: { value: ' 新名称 ' },
});
fireEvent.change(within(editor).getByLabelText('简介'), {
target: { value: '新简介' },
});
fireEvent.change(within(editor).getByLabelText('标签'), {
target: { value: '2D, 新手,2D' },
});
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
expect(updateAdminAgcTemplate).not.toHaveBeenCalled();
await confirmWrite();
await waitFor(() =>
expect(updateAdminAgcTemplate).toHaveBeenCalledWith('token', template.id, {
expectedRevision: 'rev-1',
title: '新名称',
summary: '新简介',
tags: ['2D', '新手'],
enabled: true,
}),
);
await waitFor(() =>
expect(screen.queryByRole('dialog', { name: '编辑模板' })).toBeNull(),
);
expect(screen.getByText('新名称')).not.toBeNull();
expect(screen.getByText('已保存')).not.toBeNull();
});
test('上下架复用确认且只用服务端结果更新列表', async () => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
fireEvent.click(await screen.findByRole('button', { name: '下架 空白 2D' }));
await confirmWrite();
await waitFor(() =>
expect(updateAdminAgcTemplate).toHaveBeenCalledWith('token', template.id, {
expectedRevision: 'rev-1',
title: template.title,
summary: template.summary,
tags: template.tags,
enabled: false,
}),
);
expect(
await screen.findByRole('button', { name: '上架 空白 2D' }),
).not.toBeNull();
});
test('只读模式禁用字段、保存与上下架', async () => {
vi.mocked(getAdminAgcTemplates).mockResolvedValue({
...library,
writable: false,
});
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
expect(
await screen.findByText('当前为只读模式,无法保存或上下架'),
).not.toBeNull();
expect(
(screen.getByRole('button', { name: '下架 空白 2D' }) as HTMLButtonElement)
.disabled,
).toBe(true);
const editor = await openEditor();
expect(
(within(editor).getByLabelText('名称') as HTMLInputElement).disabled,
).toBe(true);
expect(
(within(editor).getByRole('button', { name: '保存' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(updateAdminAgcTemplate).not.toHaveBeenCalled();
});
test('409 保留草稿,显式刷新后重新编辑才采用新 revision,不自动重试', async () => {
vi.mocked(updateAdminAgcTemplate).mockRejectedValueOnce(
new AdminApiError({ message: '模板库已被修改', status: 409 }),
);
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
let editor = await openEditor();
fireEvent.change(within(editor).getByLabelText('名称'), {
target: { value: '未保存草稿' },
});
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
await confirmWrite();
await screen.findByText('模板库已被修改');
expect((screen.getByLabelText('名称') as HTMLInputElement).value).toBe(
'未保存草稿',
);
expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1);
expect(
(screen.getByRole('button', { name: '保存' }) as HTMLButtonElement)
.disabled,
).toBe(true);
vi.mocked(getAdminAgcTemplates).mockResolvedValueOnce({
...library,
revision: 'rev-3',
templates: [{ ...template, title: '最新名称' }],
});
fireEvent.click(screen.getByRole('button', { name: '刷新后重新编辑' }));
await screen.findByDisplayValue('最新名称');
editor = screen.getByRole('dialog', { name: '编辑模板' });
fireEvent.change(within(editor).getByLabelText('名称'), {
target: { value: '重新编辑' },
});
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
await confirmWrite();
await waitFor(() =>
expect(updateAdminAgcTemplate).toHaveBeenLastCalledWith(
'token',
template.id,
expect.objectContaining({ expectedRevision: 'rev-3', title: '重新编辑' }),
),
);
});
test('防止重复确认和保存中重复提交', async () => {
const pending = deferred<AdminAgcTemplateLibraryResponse>();
vi.mocked(updateAdminAgcTemplate).mockReturnValue(pending.promise);
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
const editor = await openEditor();
const form = within(editor).getByLabelText('名称').closest('form')!;
fireEvent.submit(form);
fireEvent.submit(form);
expect(screen.getAllByRole('dialog', { name: '确认操作' })).toHaveLength(1);
await confirmWrite();
await waitFor(() => expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1));
fireEvent.submit(form);
expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1);
await act(async () => pending.resolve(library));
});
test.each([409, 503])(
'滚到表单底部后出现 HTTP %s 写入错误,聚焦并仅滚动弹窗到提示区',
async (status) => {
vi.mocked(updateAdminAgcTemplate).mockRejectedValueOnce(
new AdminApiError({ message: '本次保存失败', status }),
);
render(
<div data-testid="background-page">
<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />
</div>,
);
const background = screen.getByTestId('background-page');
background.scrollTop = 240;
const editor = await openEditor();
const viewport = editor.querySelector<HTMLElement>(
'.genarrative-ui-modal__body',
)!;
viewport.scrollTop = 400;
const bounds = (top: number, bottom: number) =>
({
top,
bottom,
height: bottom - top,
left: 0,
right: 300,
width: 300,
x: 0,
y: top,
toJSON: () => ({}),
}) as DOMRect;
vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue(
bounds(100, 450),
);
const originalBounds = HTMLElement.prototype.getBoundingClientRect;
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
function (this: HTMLElement) {
return this.classList.contains('admin-agc-template-feedback')
? bounds(-280, -120)
: originalBounds.call(this);
},
);
const windowScroll = vi
.spyOn(window, 'scrollTo')
.mockImplementation(() => {});
const focus = vi.spyOn(HTMLElement.prototype, 'focus');
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
await confirmWrite();
const message = await screen.findByRole('alert');
const feedback = message.parentElement!;
expect(document.activeElement).toBe(feedback);
expect(feedback.tabIndex).toBe(-1);
expect(focus).toHaveBeenCalledWith({ preventScroll: true });
expect(viewport.scrollTop).toBe(20);
expect(background.scrollTop).toBe(240);
expect(windowScroll).not.toHaveBeenCalled();
if (status === 409) {
expect(
within(feedback).getByRole('button', { name: '刷新后重新编辑' }),
).not.toBeNull();
}
},
);
test('token 切换后忽略旧列表和未确认写操作', async () => {
const pending = deferred<AdminAgcTemplateLibraryResponse>();
vi.mocked(getAdminAgcTemplates).mockReturnValueOnce(pending.promise);
const props = { onUnauthorized: vi.fn() };
const view = render(<AdminAgcTemplatesPage token="old" {...props} />);
view.rerender(<AdminAgcTemplatesPage token="new" {...props} />);
await screen.findByText('空白 2D');
await act(async () =>
pending.resolve({
...library,
templates: [{ ...template, title: '过期列表' }],
}),
);
expect(screen.queryByText('过期列表')).toBeNull();
const editor = await openEditor();
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
view.rerender(<AdminAgcTemplatesPage token="third" {...props} />);
await screen.findByText('空白 2D');
expect(screen.queryByRole('dialog')).toBeNull();
expect(updateAdminAgcTemplate).not.toHaveBeenCalled();
});
test.each(['success', 'unauthorized'])(
'token 切换后忽略旧保存 %s,不污染新会话',
async (result) => {
const pending = deferred<AdminAgcTemplateLibraryResponse>();
vi.mocked(updateAdminAgcTemplate).mockReturnValueOnce(pending.promise);
const onUnauthorized = vi.fn();
const view = render(
<AdminAgcTemplatesPage token="old" onUnauthorized={onUnauthorized} />,
);
const editor = await openEditor();
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
await confirmWrite();
await waitFor(() =>
expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1),
);
view.rerender(
<AdminAgcTemplatesPage token="new" onUnauthorized={onUnauthorized} />,
);
await screen.findByText('空白 2D');
await act(async () => {
if (result === 'success') {
pending.resolve({
...library,
templates: [{ ...template, title: '旧保存结果' }],
});
} else {
pending.reject(
new AdminApiError({ status: 401, message: '旧会话失效' }),
);
}
});
expect(onUnauthorized).not.toHaveBeenCalled();
expect(screen.queryByText('旧会话失效')).toBeNull();
expect(screen.queryByText('旧保存结果')).toBeNull();
expect(screen.queryByText('已保存')).toBeNull();
},
);
test('封面在草稿中转 base64,保存携带,取消释放预览 URL 且不串到下一条', async () => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
let editor = await openEditor();
const file = new File(['cover'], 'cover.png', { type: 'image/png' });
fireEvent.change(within(editor).getByLabelText('更换封面'), {
target: { files: [file] },
});
await waitFor(() => expect(screen.queryByText('正在读取封面')).toBeNull());
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
await confirmWrite();
await waitFor(() =>
expect(updateAdminAgcTemplate).toHaveBeenCalledWith(
'token',
template.id,
expect.objectContaining({
cover: { contentType: 'image/png', dataBase64: 'Y292ZXI=' },
}),
),
);
await waitFor(() =>
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:template-cover'),
);
editor = await openEditor();
fireEvent.change(within(editor).getByLabelText('更换封面'), {
target: { files: [file] },
});
fireEvent.click(within(editor).getByRole('button', { name: '取消' }));
fireEvent.click(screen.getByRole('button', { name: '编辑 Godot 起步' }));
expect(
screen.getByRole('img', { name: '模板封面预览' }).getAttribute('src'),
).toBe(template.coverUrl);
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
});
test.each([
['bad.svg', 'image/svg+xml', 1, '封面仅支持 PNG、JPEG 或 WebP'],
[
'large.png',
'image/png',
5 * 1024 * 1024 + 1,
'封面文件须大于 0 且不超过 5 MiB',
],
])('拒绝无效封面 %s,不上传', async (name, type, size, message) => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
const editor = await openEditor();
const file = new File(['x'], name, { type });
Object.defineProperty(file, 'size', { value: size });
fireEvent.change(within(editor).getByLabelText('更换封面'), {
target: { files: [file] },
});
expect(screen.getByText(message)).not.toBeNull();
expect(
(screen.getByRole('button', { name: '保存' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(URL.createObjectURL).not.toHaveBeenCalled();
expect(updateAdminAgcTemplate).not.toHaveBeenCalled();
});
test('名称和标签校验失败时不进入写确认', async () => {
render(<AdminAgcTemplatesPage token="token" onUnauthorized={vi.fn()} />);
const editor = await openEditor();
fireEvent.change(within(editor).getByLabelText('名称'), {
target: { value: ' ' },
});
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
expect(screen.getByText('名称须为 180 个字符')).not.toBeNull();
fireEvent.change(within(editor).getByLabelText('名称'), {
target: { value: '有效名称' },
});
fireEvent.change(within(editor).getByLabelText('标签'), {
target: {
value: Array.from({ length: 17 }, (_, index) => `标签${index}`).join(','),
},
});
fireEvent.click(within(editor).getByRole('button', { name: '保存' }));
expect(
screen.getByText('最多 16 个标签,每个标签不超过 32 个字符'),
).not.toBeNull();
expect(updateAdminAgcTemplate).not.toHaveBeenCalled();
});
test('读取失败显示真实错误,401 交给会话处理且不显示空库', async () => {
const onUnauthorized = vi.fn();
vi.mocked(getAdminAgcTemplates).mockRejectedValueOnce(
new AdminApiError({ status: 503, message: '模板服务不可用' }),
);
render(
<AdminAgcTemplatesPage token="token" onUnauthorized={onUnauthorized} />,
);
await screen.findByText('模板服务不可用');
expect(screen.queryByText('暂无模板')).toBeNull();
vi.mocked(getAdminAgcTemplates).mockRejectedValueOnce(
new AdminApiError({ status: 401, message: '会话失效' }),
);
fireEvent.click(screen.getByRole('button', { name: '刷新' }));
await waitFor(() =>
expect(onUnauthorized).toHaveBeenCalledWith('登录状态已失效'),
);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';
import {
formatAdminTimestamp,
parseAdminTimestamp,
} from './AdminErrorReportsPage';
describe('错误报告时间格式化', () => {
it('解析 SpacetimeDB seconds.microsZ 时间', () => {
expect(parseAdminTimestamp('1778207451.731746Z')).toBe(1778207451731);
});
it('解析微秒字符串并拒绝无效时间', () => {
expect(parseAdminTimestamp('1778207451731746')).toBe(1778207451731);
expect(formatAdminTimestamp('not-a-date')).toBe('-');
expect(formatAdminTimestamp('999999999999999999999999')).toBe('-');
});
});
@@ -17,6 +17,57 @@ type Props = { token: string; onUnauthorized: (message?: string) => void };
const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const;
const ERROR_REPORT_PAGE_SIZE = 50;
const ERROR_REPORT_STATUS_LABELS: Record<string, string> = {
new: '待处理',
'in-progress': '处理中',
resolved: '已解决',
};
export function parseAdminTimestamp(value: string | null | undefined) {
const normalized = value?.trim() ?? '';
if (/^-?\d+\.\d{6}Z$/u.test(normalized)) {
const [secondsText, microsText] = normalized.slice(0, -1).split('.');
const seconds = Number(secondsText);
const micros = Number(microsText);
return Number.isFinite(seconds) && Number.isFinite(micros)
? seconds * 1000 + Math.floor(micros / 1000)
: Number.NaN;
}
if (/^-?\d+$/u.test(normalized)) {
const numeric = Number(normalized);
if (!Number.isFinite(numeric)) return Number.NaN;
if (Math.abs(numeric) >= 1e14) return Math.floor(numeric / 1000);
if (Math.abs(numeric) >= 1e11) return numeric;
return numeric * 1000;
}
return Date.parse(normalized);
}
export function formatAdminTimestamp(value: string | null | undefined) {
const timestamp = parseAdminTimestamp(value);
if (!Number.isFinite(timestamp)) return '-';
const date = new Date(timestamp);
if (!Number.isFinite(date.getTime())) return '-';
try {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(date);
} catch {
return '-';
}
}
function eventText(event: Record<string, unknown>, key: string) {
const value = event[key];
return typeof value === 'string' || typeof value === 'number'
? String(value)
: '';
}
export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
const [reports, setReports] = useState<AdminErrorReportEntry[]>([]);
@@ -29,6 +80,24 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false });
const openReportRequestId = useRef(0);
const loadRequestId = useRef(0);
const detailCloseButtonRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (!selected) return;
const previousFocus = document.activeElement as HTMLElement | null;
const focusFrame = window.requestAnimationFrame(() => {
detailCloseButtonRef.current?.focus();
});
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setSelected(null);
};
window.addEventListener('keydown', handleEscape);
return () => {
window.cancelAnimationFrame(focusFrame);
window.removeEventListener('keydown', handleEscape);
previousFocus?.focus?.();
};
}, [selected]);
const load = useCallback(async () => {
const requestId = ++loadRequestId.current;
@@ -184,9 +253,15 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
<td>{report.batchId}</td>
<td>{report.eventCount}</td>
<td>{report.source ?? '-'}</td>
<td>{report.status}</td>
<td>
<span
className={`admin-error-report-status is-${report.status}`}
>
{ERROR_REPORT_STATUS_LABELS[report.status] ?? report.status}
</span>
</td>
<td>{report.userId}</td>
<td>{new Date(report.createdAt).toLocaleString()}</td>
<td>{formatAdminTimestamp(report.createdAt)}</td>
</tr>
))}
</tbody>
@@ -224,66 +299,173 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
className="admin-detail-modal"
role="dialog"
aria-label="错误报告详情"
aria-modal="true"
onMouseDown={(event) => {
if (event.target === event.currentTarget) setSelected(null);
}}
>
<div className="admin-detail-modal__panel">
<header>
<h2>{selected.batchId}</h2>
<button type="button" onClick={() => setSelected(null)}>
<div className="admin-detail-modal__panel admin-error-report-detail">
<header className="admin-error-report-detail__header">
<div className="admin-error-report-detail__heading">
<span className="admin-error-report-detail__eyebrow">
</span>
<h2>{selected.batchId}</h2>
<div className="admin-error-report-detail__badges">
<span
className={`admin-error-report-status is-${selected.status}`}
>
{ERROR_REPORT_STATUS_LABELS[selected.status] ??
selected.status}
</span>
<span className="admin-error-report-detail__badge">
{selected.firstSource ?? selected.source ?? '未知来源'}
</span>
</div>
</div>
<button
type="button"
className="admin-error-report-detail__close"
ref={detailCloseButtonRef}
onClick={() => setSelected(null)}
aria-label="关闭错误报告详情"
>
×
</button>
</header>
<p>
{selected.userId} · {selected.eventCount} ·
{selected.logCount}
</p>
<pre>{JSON.stringify(selected.events.slice(0, 20), null, 2)}</pre>
{selected.userDescription ? (
<p>{selected.userDescription}</p>
) : null}
<label className="admin-detail-modal__note">
<textarea
value={selected.note ?? ''}
onChange={(event) =>
setSelected((current) =>
current
? { ...current, note: event.target.value }
: current,
)
}
maxLength={2000}
rows={4}
placeholder="记录处理结论或后续跟进事项"
disabled={busy}
/>
</label>
<div className="admin-detail-modal__actions">
<select
value={selected.status}
onChange={(event) => void saveStatus(event.target.value)}
disabled={busy}
>
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<button
type="button"
onClick={() => void saveStatus(selected.status)}
disabled={busy}
>
</button>
<button
type="button"
onClick={() => void download(selected.batchId)}
disabled={downloading}
>
{downloading ? '下载中…' : '下载诊断包'}
</button>
<div className="admin-error-report-detail__summary">
<div>
<span></span>
<strong>{selected.userId}</strong>
</div>
<div>
<span></span>
<strong>{formatAdminTimestamp(selected.createdAt)}</strong>
</div>
<div>
<span></span>
<strong>{selected.eventCount}</strong>
</div>
<div>
<span></span>
<strong>{selected.logCount}</strong>
</div>
</div>
<div className="admin-error-report-detail__body">
<section className="admin-error-report-detail__section">
<div className="admin-error-report-detail__section-heading">
<div>
<h3></h3>
<span> 20 </span>
</div>
<span className="admin-error-report-detail__count">
{selected.events.length}
</span>
</div>
<div className="admin-error-report-event-list">
{selected.events.length === 0 ? (
<p className="admin-error-report-detail__empty">
</p>
) : null}
{selected.events.slice(0, 20).map((event, index) => {
const message = eventText(event, 'message') || '未知错误';
const stack = eventText(event, 'stack');
const occurredAt = eventText(event, 'occurredAt');
return (
<article
className="admin-error-report-event"
key={`${index}-${message}`}
>
<div className="admin-error-report-event__topline">
<span className="admin-error-report-event__index">
#{index + 1}
</span>
<span>{eventText(event, 'source') || 'client'}</span>
<span>{eventText(event, 'count') || '1'} </span>
<time>{formatAdminTimestamp(occurredAt)}</time>
</div>
<p className="admin-error-report-event__message">
{message}
</p>
{eventText(event, 'fingerprint') ? (
<code>{eventText(event, 'fingerprint')}</code>
) : null}
{stack ? (
<details>
<summary></summary>
<pre>{stack}</pre>
</details>
) : null}
</article>
);
})}
</div>
</section>
{selected.userDescription ? (
<section className="admin-error-report-detail__section">
<div className="admin-error-report-detail__section-heading">
<div>
<h3></h3>
</div>
</div>
<p className="admin-error-report-detail__description">
{selected.userDescription}
</p>
</section>
) : null}
<section className="admin-error-report-detail__section admin-error-report-detail__section--note">
<label className="admin-detail-modal__note">
<span></span>
<textarea
value={selected.note ?? ''}
onChange={(event) =>
setSelected((current) =>
current
? { ...current, note: event.target.value }
: current,
)
}
maxLength={2000}
rows={4}
placeholder="记录处理结论或后续跟进事项"
disabled={busy}
/>
</label>
</section>
</div>
<footer className="admin-error-report-detail__footer">
<label className="admin-error-report-detail__status-field">
<select
value={selected.status}
onChange={(event) => void saveStatus(event.target.value)}
disabled={busy}
>
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
<option key={value} value={value}>
{ERROR_REPORT_STATUS_LABELS[value]}
</option>
))}
</select>
</label>
<div className="admin-error-report-detail__footer-actions">
<button
type="button"
onClick={() => void saveStatus(selected.status)}
disabled={busy}
>
{busy ? '保存中…' : '保存备注'}
</button>
<button
type="button"
onClick={() => void download(selected.batchId)}
disabled={downloading}
>
{downloading ? '下载中…' : '下载诊断包'}
</button>
</div>
</footer>
</div>
</div>
) : null}
+452
View File
@@ -13,6 +13,87 @@
text-rendering: optimizeLegibility;
}
.admin-agc-templates {
min-width: 0;
}
.admin-agc-template-filters {
display: grid;
grid-template-columns: minmax(180px, 1fr) repeat(2, minmax(130px, 190px));
gap: 14px;
}
.admin-agc-template-table {
min-width: 850px;
}
.admin-agc-template-table td:nth-child(2) {
min-width: 230px;
max-width: 380px;
}
.admin-agc-template-cover {
display: block;
width: 88px;
height: 62px;
border-radius: 8px;
object-fit: cover;
background: #f8efe7;
}
.admin-agc-template-summary {
display: -webkit-box;
overflow: hidden;
margin: 8px 0;
color: #866954;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.admin-agc-template-tags,
.admin-agc-template-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.admin-agc-template-tags span {
padding: 3px 7px;
border-radius: 5px;
background: #f8efe7;
font-size: 12px;
}
.admin-agc-template-dialog {
border-radius: 10px;
}
.admin-agc-template-editor,
.admin-agc-template-conflict {
display: grid;
gap: 14px;
}
.admin-agc-template-cover-preview {
display: block;
width: min(100%, 300px);
max-height: 180px;
border-radius: 8px;
object-fit: contain;
background: #f8efe7;
}
.admin-agc-template-dialog .admin-confirm-backdrop {
z-index: 1100;
}
@media (max-width: 680px) {
.admin-agc-template-filters {
grid-template-columns: minmax(0, 1fr);
}
}
* {
box-sizing: border-box;
}
@@ -3213,6 +3294,377 @@ button:disabled {
padding: 12px;
border-radius: 8px;
}
.admin-error-report-detail {
width: min(980px, 100%);
padding: 0;
overflow: hidden;
border: 1px solid #eadfd6;
background: #fffdfb;
box-shadow: 0 24px 80px rgb(57 31 18 / 22%);
}
.admin-error-report-detail__header {
align-items: flex-start;
padding: 24px 28px 20px;
border-bottom: 1px solid #eee2d8;
background: linear-gradient(135deg, #fffaf5, #fffdfb 65%);
}
.admin-error-report-detail__heading {
min-width: 0;
}
.admin-error-report-detail__eyebrow {
display: block;
margin-bottom: 7px;
color: #a4775d;
font-size: 12px;
font-weight: 750;
letter-spacing: 0.08em;
}
.admin-error-report-detail__heading h2 {
margin: 0;
overflow-wrap: anywhere;
color: #3d2a20;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 19px;
line-height: 1.35;
}
.admin-error-report-detail__badges,
.admin-error-report-detail__footer-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.admin-error-report-detail__badges {
margin-top: 12px;
}
.admin-error-report-detail__badge,
.admin-error-report-detail__count {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 9px;
border: 1px solid #eadfd6;
border-radius: 999px;
color: #785b49;
background: #fff;
font-size: 12px;
font-weight: 700;
}
.admin-error-report-detail__close {
display: inline-flex;
width: 34px;
height: 34px;
align-items: center;
justify-content: center;
flex: 0 0 auto;
border: 1px solid #e5d5c8;
border-radius: 9px;
color: #765848;
background: #fff;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.admin-error-report-detail__close:hover,
.admin-error-report-detail__close:focus-visible {
border-color: #b6623f;
color: #9b4f31;
outline: none;
}
.admin-error-report-status {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 9px;
border-radius: 999px;
font-size: 12px;
font-weight: 750;
}
.admin-error-report-status.is-new {
color: #a14e20;
background: #fff0e5;
}
.admin-error-report-status.is-in-progress {
color: #72551b;
background: #fff7d9;
}
.admin-error-report-status.is-resolved {
color: #28704d;
background: #e9f7ee;
}
.admin-error-report-detail__summary {
display: grid;
grid-template-columns: minmax(0, 1.7fr) repeat(3, minmax(100px, 1fr));
gap: 1px;
border-bottom: 1px solid #eee2d8;
background: #eee2d8;
}
.admin-error-report-detail__summary > div {
display: grid;
min-width: 0;
gap: 5px;
padding: 14px 18px;
background: #fffdfb;
}
.admin-error-report-detail__summary span,
.admin-error-report-detail__section-heading span,
.admin-error-report-detail__status-field {
color: #997d6a;
font-size: 12px;
font-weight: 650;
}
.admin-error-report-detail__summary strong {
overflow-wrap: anywhere;
color: #4d3326;
font-size: 13px;
}
.admin-error-report-detail__body {
display: grid;
gap: 16px;
max-height: min(62vh, 640px);
overflow: auto;
padding: 20px 28px;
}
.admin-error-report-detail__section {
display: grid;
gap: 12px;
}
.admin-error-report-detail__section-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.admin-error-report-detail__section-heading h3 {
margin: 0 0 4px;
color: #4d3326;
font-size: 15px;
}
.admin-error-report-detail__section-heading span {
line-height: 1.5;
}
.admin-error-report-detail__count {
flex: 0 0 auto;
}
.admin-error-report-event-list {
display: grid;
gap: 10px;
}
.admin-error-report-detail__empty {
margin: 0;
padding: 16px;
border: 1px dashed #e4d2c5;
border-radius: 10px;
color: #997d6a;
background: #fffaf6;
text-align: center;
}
.admin-error-report-event {
display: grid;
gap: 9px;
padding: 14px 16px;
border: 1px solid #eee2d8;
border-radius: 12px;
background: #fff;
}
.admin-error-report-event__topline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
color: #927663;
font-size: 12px;
}
.admin-error-report-event__topline time {
margin-left: auto;
color: #aa9282;
}
.admin-error-report-event__index {
color: #b6623f;
font-weight: 750;
}
.admin-error-report-event__message {
margin: 0;
color: #4a3024;
font-size: 14px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.admin-error-report-event code {
overflow-wrap: anywhere;
color: #957a69;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 11px;
}
.admin-error-report-event details {
border-top: 1px solid #f0e7df;
padding-top: 9px;
}
.admin-error-report-event summary {
color: #a15d3e;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.admin-error-report-event pre {
max-height: 180px;
margin: 9px 0 0;
padding: 10px;
border: 1px solid #f0e7df;
background: #fffaf6;
color: #72594a;
font-size: 11px;
line-height: 1.5;
}
.admin-error-report-detail__description {
margin: 0;
padding: 13px 15px;
border-radius: 10px;
color: #634b3c;
background: #fff8f2;
line-height: 1.6;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.admin-error-report-detail__section--note {
padding-top: 4px;
}
.admin-error-report-detail__section--note .admin-detail-modal__note {
display: grid;
gap: 8px;
color: #624938;
font-size: 13px;
font-weight: 700;
}
.admin-error-report-detail__section--note textarea {
width: 100%;
min-height: 100px;
box-sizing: border-box;
resize: vertical;
border: 1px solid #e4d2c5;
border-radius: 10px;
padding: 11px 12px;
color: #4d3326;
background: #fff;
font: inherit;
line-height: 1.5;
}
.admin-error-report-detail__section--note textarea:focus {
border-color: #b6623f;
box-shadow: 0 0 0 3px rgb(182 98 63 / 14%);
outline: none;
}
.admin-error-report-detail__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 16px 28px 20px;
border-top: 1px solid #eee2d8;
background: #fffaf6;
}
.admin-error-report-detail__status-field {
display: grid;
gap: 6px;
}
.admin-error-report-detail__status-field select {
min-width: 132px;
min-height: 38px;
border: 1px solid #e4d2c5;
border-radius: 8px;
padding: 0 10px;
color: #4d3326;
background: #fff;
font: inherit;
}
.admin-error-report-detail__footer-actions button {
min-height: 38px;
border: 1px solid #d9c3b4;
border-radius: 8px;
padding: 0 13px;
color: #6d4b3a;
background: #fff;
font-weight: 700;
cursor: pointer;
}
.admin-error-report-detail__footer-actions button:last-child {
border-color: #a96442;
color: #fff;
background: #a96442;
}
.admin-error-report-detail__footer-actions button:hover,
.admin-error-report-detail__footer-actions button:focus-visible {
border-color: #a96442;
outline: none;
}
@media (max-width: 720px) {
.admin-error-report-detail__header,
.admin-error-report-detail__body,
.admin-error-report-detail__footer {
padding-right: 18px;
padding-left: 18px;
}
.admin-error-report-detail__summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-error-report-detail__footer {
align-items: stretch;
flex-direction: column;
}
.admin-error-report-detail__footer-actions button {
flex: 1;
}
}
.admin-agc-models {
min-width: 0;
}
+2 -2
View File
@@ -66,15 +66,15 @@
"zustand": "^5.0.14"
},
"devDependencies": {
"@openai/codex": "0.147.0",
"@openai/codex": "0.155.1",
"@tailwindcss/vite": "^4.1.14",
"@tauri-apps/cli": "^2.11.2",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.184.1",
"@types/react-window": "^1.8.8",
"@types/three": "^0.184.1",
"tailwindcss": "^4.1.14",
"typescript": "~5.8.2",
"vitest": "^0.34.6"
@@ -0,0 +1,285 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
generateUpdateManifest,
prepareReleaseVersion,
resolveManifestPlatformKeys,
resolveReleaseContext,
resolveReleasePartition,
runTauriBuild,
} from './build-release.mjs';
import { readReleaseDryRun, uploadReleaseArtifacts } from './release-oss.mjs';
import {
readUpdaterPubkey,
verifyUpdaterSignature,
} from './verify-updater-signature.mjs';
/**
* AGC macOS 分区(`<channel>-mac`)发布入口:构建 arm64 单架构包 → arm64 隔离 smoke → 生成 arm64 DMG
* → 生成分区清单 latest.json → 用产物内烘焙的公钥验签 → 按 dry-run 决定是否上传 OSS。
*
* 边界:
* - 只出 Apple Siliconarm64)单架构:清单只登记 `darwin-aarch64`。Intel 侧要可用,前提是随包 Node
* 也能按架构各带一份(`stage-node-runtime.mjs` 对 universal 目标失败关闭);在实现之前**不得**
* 把 arm64 产物登记成 `darwin-x86_64`,否则 Intel 客户端会装到跑不起来的包。
* - Apple 签名与公证暂缺:本入口剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名,但**不能传
* `--no-sign`** —— 该标志同时会跳过 updater 的 minisign 签名,产物就没有 `.sig`
* 未签名 + 未公证必须显式记录而非静默通过;
* - 更新包签名(TAURI_SIGNING_PRIVATE_KEYminisign)是硬需求:缺了客户端一律拒绝安装,
* 因此构建前要求凭据存在,构建后用内置公钥复核 `.sig` 才允许继续上传;
* - 未通过验签绝不写 OSS:上传顺序为更新包、签名、首装包,全部成功后才覆盖渠道清单指针。
*/
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
/**
* 产品名只从 Tauri 配置读取:它同时决定 `*.app` 目录名、updater 归档名与 DMG 卷名。
* 写死会在改名后让入口静默找错对象(清理、打包、归档三处一起失效)。
*/
function readProductName() {
const read = (file) =>
JSON.parse(fs.readFileSync(path.join(appRoot, 'src-tauri', file), 'utf8'));
const base = read('tauri.conf.json');
const macosPath = path.join(appRoot, 'src-tauri', 'tauri.macos.conf.json');
const productName = fs.existsSync(macosPath)
? (read('tauri.macos.conf.json').productName ?? base.productName)
: base.productName;
assert.ok(
typeof productName === 'string' && productName.trim().length > 0,
'Tauri 配置缺少 productName',
);
return productName;
}
const productName = readProductName();
const appBundleName = `${productName}.app`;
const updaterArtifactName = `${productName}.app.tar.gz`;
assert.equal(process.platform, 'darwin', '只能在 macOS Agent 执行');
assert.equal(
process.env.JENKINS_URL?.length > 0,
true,
'此入口仅用于 Jenkins 独立工作区',
);
assert.equal(
fs.realpathSync(process.env.WORKSPACE || '.'),
fs.realpathSync(repoRoot),
'必须在 Jenkins workspace 根目录执行',
);
const space = fs.statfsSync(repoRoot);
assert.ok(
space.bavail * space.bsize >= 8 * 1024 ** 3,
'构建前至少需要 8 GiB 可用空间;禁止自动清理开发缓存',
);
// 仅剥离 Apple 签名/公证变量:本节点没有证书,误用只会让构建失败;
// 更新包签名与 OSS 凭据必须保留,它们是本入口发布能力的组成部分。
for (const key of Object.keys(process.env)) {
if (/^APPLE_/u.test(key)) delete process.env[key];
}
assert.ok(
process.env.TAURI_SIGNING_PRIVATE_KEY?.length > 0 ||
process.env.TAURI_SIGNING_PRIVATE_KEY_PATH?.length > 0,
'缺少更新包签名私钥(TAURI_SIGNING_PRIVATE_KEY / _PATH):无签名的更新包会被客户端拒绝,禁止继续',
);
const bucket = process.env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
const endpoint =
process.env.AGC_OSS_ENDPOINT?.trim() || 'oss-rg-china-mainland.aliyuncs.com';
if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
throw new Error('OSS bucket 或 endpoint 配置无效');
}
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const dryRun = readReleaseDryRun();
process.env.CARGO_TARGET_DIR = path.join(appRoot, 'src-tauri/target');
// 单架构目标:清单侧 `resolveManifestPlatformKeys` 只为它登记 darwin-aarch64。
const macTarget = 'aarch64-apple-darwin';
const context = resolveReleaseContext([`--target=${macTarget}`]);
const partition = resolveReleasePartition(context.channel, context.target);
const version = await prepareReleaseVersion(context);
// 首装包名必须让清单侧的单架构分支唯一匹配:`<产品名>_<版本>_<架构>.dmg`
// 架构段用 Tauri 的 aarch64 口径(不是 updater 平台键的 arm64 / x86_64)。
const firstInstallName = `${productName}_${version}_aarch64.dmg`;
// 幂等边界:workspace 会保留上一轮产物。先删掉本次将要写出的对象,否则
// 1) hdiutil 会因同名 DMG 已存在直接失败(首次实跑即命中);
// 2) 上一轮遗留的 `.sig` 会让验签门禁把「本轮其实没签」判成通过。
// 只删本次要写出的确切路径,不动其它版本产物与编译缓存。
const macosBundle = path.join(context.bundleRoot, 'macos');
for (const stale of [
path.join(macosBundle, updaterArtifactName),
path.join(macosBundle, `${updaterArtifactName}.sig`),
path.join(macosBundle, `${firstInstallName}`),
path.join(macosBundle, `${firstInstallName}.sha256`),
path.join(context.bundleRoot, 'latest.json'),
path.join(context.bundleRoot, 'release-notes.txt'),
]) {
fs.rmSync(stale, { force: true });
}
const args = [
`--target=${macTarget}`,
'--bundles',
'app',
'--ci',
// 刻意不传 `--no-sign`:它会连带跳过 updater 签名,而客户端强制校验更新包签名。
// Apple 侧改为剥离 APPLE_* 凭据,未配置身份时 Tauri 不签名也不失败。
// 基础配置已开启;这里显式声明,避免被其它配置来源关掉后静默失去更新能力。
'--config',
'{"bundle":{"createUpdaterArtifacts":true}}',
];
const command = (binary, argv, options = {}) =>
execFileSync(binary, argv, { cwd: repoRoot, stdio: 'inherit', ...options });
runTauriBuild(args, context);
const app = path.join(context.bundleRoot, 'macos', appBundleName);
command(process.execPath, [
path.join(appRoot, 'scripts/check-macos-bundle.mjs'),
app,
'arm64',
]);
// DMG 放在 bundle 根目录下:渠道清单的首装包选择会扫描该目录,命名必须匹配 `_<version>_aarch64.dmg`。
const dmgDirectory = path.join(context.bundleRoot, 'macos');
fs.mkdirSync(dmgDirectory, { recursive: true });
const dmg = path.join(dmgDirectory, firstInstallName);
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-ci-dmg-'));
try {
command('ditto', [app, path.join(stage, appBundleName)]);
fs.symlinkSync('/Applications', path.join(stage, 'Applications'));
command('hdiutil', [
'create',
// 前面已删除同名对象;这里再要求显式覆盖,避免残留文件让构建以「文件已存在」失败。
'-ov',
'-volname',
productName,
'-srcfolder',
stage,
'-format',
'UDZO',
dmg,
]);
command('hdiutil', ['verify', dmg]);
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
const release = await generateUpdateManifest(context);
assert.equal(
path.resolve(release.downloadArtifact),
path.resolve(dmg),
'首装包必须锁定本次生成的 arm64 DMG',
);
// 上传前门禁:用产物里烘焙的公钥复核更新包签名。验不过就停在这里,绝不写 OSS。
const signature = verifyUpdaterSignature({
artifactPath: release.artifact,
signaturePath: `${release.artifact}.sig`,
pubkey: readUpdaterPubkey(),
});
console.log(
`[agc-macos] 更新包签名校验通过:alg=${signature.algorithm}keyId=${signature.keyId}`,
);
const artifacts = path.join(repoRoot, 'artifacts');
// 只清理本 Job 的归档输出,不能把上次 DMG 当成本次成功产物。
fs.rmSync(artifacts, { recursive: true, force: true });
fs.mkdirSync(artifacts, { recursive: true });
const sha256 = (file) => {
const hash = createHash('sha256');
hash.update(fs.readFileSync(file));
return hash.digest('hex');
};
const dmgHash = sha256(dmg);
fs.writeFileSync(`${dmg}.sha256`, `${dmgHash} ${path.basename(dmg)}\n`);
const uploadPlan = uploadReleaseArtifacts(release, {
bucket,
endpoint,
binary: process.env.OSSUTIL_BIN?.trim() || 'ossutil',
accessKeyId: process.env.AGC_OSS_ACCESS_KEY_ID?.trim(),
accessKeySecret: process.env.AGC_OSS_ACCESS_KEY_SECRET,
dryRun,
});
const archived = [
dmg,
`${dmg}.sha256`,
release.manifestPath,
release.notesPath,
`${release.artifact}.sig`,
];
for (const file of archived) {
fs.copyFileSync(file, path.join(artifacts, path.basename(file)));
}
const commit = execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: repoRoot,
encoding: 'utf8',
}).trim();
// Apple 签名状态必须实测:剥离 APPLE_* 后 Tauri 通常跳过签名,但节点若装了 Developer ID
// 证书仍可能签上,硬编码 appleSigned=false 会把「其实签了」写成假事实。
const signatureProbe = spawnSync('codesign', ['-dv', '--verbose=2', app], {
encoding: 'utf8',
});
const signatureText = `${signatureProbe.stdout ?? ''}${signatureProbe.stderr ?? ''}`;
const appleSigned = /Authority=Developer ID Application/u.test(signatureText);
const appleSignatureKind = appleSigned
? 'developer-id'
: /Signature=adhoc/u.test(signatureText)
? 'adhoc'
: 'unsigned';
fs.writeFileSync(
path.join(artifacts, 'build-manifest.json'),
`${JSON.stringify(
{
version,
commit,
target: context.target,
channel: context.channel,
// Apple 签名与公证暂缺:显式记录为未验证项,不静默通过。
appleSigned,
appleSignatureKind,
notarized: false,
dryRun,
uploaded: !dryRun,
updaterSignature: {
algorithm: signature.algorithm,
keyId: signature.keyId,
verified: true,
},
oss: {
bucket,
endpoint,
partition,
latest: `oss://${bucket}/agc/${partition}/latest.json`,
objects: uploadPlan.map(({ destination }) => destination),
},
artifacts: {
updater: path.basename(release.artifact),
updaterSha256: sha256(release.artifact),
updaterBytes: fs.statSync(release.artifact).size,
updaterSignature: path.basename(`${release.artifact}.sig`),
firstInstall: path.basename(dmg),
firstInstallSha256: dmgHash,
manifest: 'latest.json',
},
// 单架构发布:只跑 arm64 隔离 smokeIntel 未支持(清单里没有 darwin-x86_64 键)。
smokes: ['arm64'],
manifestPlatformKeys: resolveManifestPlatformKeys(context.target),
},
null,
2,
)}\n`,
);
console.log(
dryRun
? `[agc-macos] dry-run 完成${partition} 分区产物与清单已生成未写入 OSS`
: `[agc-macos] ${partition} 分区更新包签名首装包与清单已上传 OSS`,
);
@@ -13,6 +13,7 @@ import {
defaultEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
import { stageNodeRuntime } from './stage-node-runtime.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
@@ -46,16 +47,12 @@ function explicitBuildTarget(args) {
}
function validateReleaseTarget(target) {
if (target === 'universal-apple-darwin') {
throw new Error(
'内置 Codex 资源仅支持 macOS 单架构构建,请使用 aarch64-apple-darwin 或 x86_64-apple-darwin',
);
}
if (
![
'x86_64-pc-windows-msvc',
'aarch64-apple-darwin',
'x86_64-apple-darwin',
'universal-apple-darwin',
].includes(target)
) {
throw new Error(`不支持的发布目标:${target}`);
@@ -200,10 +197,12 @@ export function updateManifestUrl(
}
/**
* 单架构产物只登记实际目标,不能把同一原生资源映射为另一架构
* universal 主程序与双目录原生资源共用一个更新包;单架构只登记实际目标。
*/
export function resolveManifestPlatformKeys(target = defaultTarget()) {
validateReleaseTarget(target);
if (target === 'universal-apple-darwin')
return ['darwin-aarch64', 'darwin-x86_64'];
if (target === 'aarch64-apple-darwin') return ['darwin-aarch64'];
if (target === 'x86_64-apple-darwin') return ['darwin-x86_64'];
if (target.includes('windows')) {
@@ -442,22 +441,25 @@ export function createChannelConfig(
};
}
function writeChannelConfigFile(channel, target) {
function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
const configPath = path.join(
os.tmpdir(),
`agc-tauri-channel-${channel}-${target}.json`,
);
fs.writeFileSync(
configPath,
`${JSON.stringify(createChannelConfig(channel, target), null, 2)}\n`,
);
const config = createChannelConfig(channel, target);
// 普通 cargo test/dev 不要求发行资源;只有完成 staging 的发行构建加入映射。
if (includeNodeRuntime)
config.bundle = {
resources: { 'resources/node-runtime': 'game-runtime/node' },
};
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
return configPath;
}
export function runTauriBuild(
args = [],
context = resolveReleaseContext(args),
{ spawn = spawnSync } = {},
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
) {
if (
explicitBuildTarget(args) &&
@@ -467,7 +469,12 @@ export function runTauriBuild(
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel, target } = context;
const configPath = writeChannelConfigFile(channel, target);
if (!args.includes('--no-bundle')) stageRuntime(target);
const configPath = writeChannelConfigFile(
channel,
target,
!args.includes('--no-bundle'),
);
console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
);
@@ -483,7 +490,17 @@ export function runTauriBuild(
const result = spawn(
npmCommand,
['--prefix', '../..', 'exec', 'tauri', '--', ...tauriArguments],
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
{
cwd: appRoot,
stdio: 'inherit',
shell: process.platform === 'win32',
env: {
...process.env,
// Vite embeds the platform API origin in the packaged renderer. The
// release channel and updater channel therefore cannot drift apart.
VITE_AGC_PLATFORM_CHANNEL: channel,
},
},
);
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
@@ -543,6 +560,18 @@ export function selectFirstInstallArtifact(
if (!selected?.endsWith('.exe')) {
throw new Error('Windows 首装包必须复用本次 NSIS .exe 更新包');
}
} else if (target === 'universal-apple-darwin') {
// universal 主程序只产出一个 DMGaarch64 与 x86_64 首装共用它(命名见 build-macos-ci.mjs)。
const suffix = `_${version}_universal.dmg`;
const candidates = files.filter((file) =>
path.basename(file).endsWith(suffix),
);
if (candidates.length !== 1) {
throw new Error(
`首装 DMG 必须唯一匹配本次版本 ${version} 的 universal 产物,找到 ${candidates.length}`,
);
}
selected = candidates[0];
} else {
// Tauri DMG 文件名使用 aarch64 / x64,而 updater 的 Intel 平台键是 x86_64。
const architecture = target.startsWith('aarch64') ? 'aarch64' : 'x64';
@@ -45,19 +45,22 @@ const packageVersion = JSON.parse(
).version;
function createDmgFixture(root, target, version = packageVersion) {
const architecture = target.startsWith('aarch64') ? 'aarch64' : 'x64';
const architecture = target.startsWith('aarch64')
? 'aarch64'
: target === universalTarget
? 'universal'
: 'x64';
const dmg = path.join(root, `陶泥儿_${version}_${architecture}.dmg`);
writeFileSync(dmg, 'first installation disk image');
return dmg;
}
test('native sidecar builds reject universal targets and accept each macOS architecture', () => {
assert.throws(() => buildTauriBuildArguments([], universalTarget), /单架构/);
assert.throws(
() => buildTauriBuildArguments(['--target=universal-apple-darwin']),
/单架构/,
);
for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) {
test('native sidecar builds accept universal and each macOS architecture', () => {
for (const target of [
universalTarget,
'aarch64-apple-darwin',
'x86_64-apple-darwin',
]) {
assert.deepEqual(buildTauriBuildArguments([], target), [
'build',
'--target',
@@ -201,8 +204,29 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
});
});
test('macOS manifests only advertise the architecture actually built', () => {
assert.throws(() => resolveManifestPlatformKeys(universalTarget), /单架构/);
test('packaged renderer receives the same channel as the updater manifest', () => {
const context = resolveReleaseContext([], {
AGC_BUILD_TARGET: windowsTarget,
AGC_UPDATE_CHANNEL: 'release',
});
let spawnOptions;
runTauriBuild([], context, {
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
stageRuntime: () => {},
spawn: (_binary, _args, options) => {
spawnOptions = options;
return { status: 0 };
},
});
assert.equal(spawnOptions?.env?.VITE_AGC_PLATFORM_CHANNEL, 'release');
});
test('macOS manifests advertise exactly the architectures actually built', () => {
assert.deepEqual(resolveManifestPlatformKeys(universalTarget), [
'darwin-aarch64',
'darwin-x86_64',
]);
assert.deepEqual(resolveManifestPlatformKeys('aarch64-apple-darwin'), [
'darwin-aarch64',
]);
@@ -246,7 +270,6 @@ test('release context resolves explicit targets before environment/default and f
['--target='],
['--target', '--no-bundle'],
['--target', windowsTarget, '--target=aarch64-apple-darwin'],
['--target', universalTarget],
['--target', 'unknown'],
])
assert.throws(() => resolveReleaseContext(args, {}));
@@ -279,6 +302,7 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
build: (args, context) => {
seenContexts.push(context);
runTauriBuild(args, context, {
stageRuntime: () => {},
spawn: (_binary, command) => {
const configIndex = command.lastIndexOf('--config');
const config = JSON.parse(
@@ -461,8 +485,8 @@ test('invalid target or platform used as channel fails before any release side e
},
};
await assert.rejects(
() => buildRelease(['--target', universalTarget], sideEffects),
/单架构/,
() => buildRelease(['--target', 'unknown'], sideEffects),
/不支持的发布目标/,
);
await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () =>
assert.rejects(
@@ -473,6 +497,43 @@ test('invalid target or platform used as channel fails before any release side e
assert.equal(touched, false);
});
test('universal uses the Mac channel and the same signed artifact for both architectures', () => {
const context = resolveReleaseContext(['--target', universalTarget], {
AGC_BUILD_TARGET: windowsTarget,
});
// 渠道本身不含系统:分区由渠道 + 目标推导,二者不能混为一谈。
assert.equal(context.channel, 'dev');
assert.equal(
resolveReleasePartition(context.channel, context.target),
'dev-mac',
);
assert.ok(context.bundleRoot.includes(universalTarget));
withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => {
const manifest = createUpdateManifest(artifact, {
...context,
downloadArtifact: createDmgFixture(
path.dirname(artifact),
universalTarget,
),
});
assert.deepEqual(Object.keys(manifest.platforms), [
'darwin-aarch64',
'darwin-x86_64',
]);
assert.deepEqual(
manifest.platforms['darwin-aarch64'],
manifest.platforms['darwin-x86_64'],
);
assert.match(manifest.platforms['darwin-aarch64'].url, /\/dev-mac\//);
// 两个平台键共用同一个 universal 首装包,不能要求出两份架构 DMG。
assert.deepEqual(
manifest.downloads['darwin-aarch64'].url,
manifest.downloads['darwin-x86_64'].url,
);
assert.match(manifest.downloads['darwin-aarch64'].url, /_universal\.dmg$/u);
});
});
test('Windows remains the default and explicit Windows overrides macOS environment', () => {
const files = ['/tmp/mac.app.tar.gz', '/tmp/windows.exe', '/tmp/mac.dmg'];
for (const context of [
@@ -490,6 +551,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
['--target', windowsTarget, '--config', 'user-config.json'],
context,
{
stageRuntime: () => {},
spawn: (_binary, command) => {
assert.ok(
command.includes(
@@ -528,6 +590,52 @@ test('no-bundle smoke skips version writes and manifest generation', async () =>
assert.deepEqual(steps, ['dev']);
});
test('release stages Node before Tauri and injects its resource mapping only for bundles', () => {
const context = resolveReleaseContext(['--target', windowsTarget]);
const events = [];
runTauriBuild(['--target', windowsTarget], context, {
stageRuntime(target) {
assert.equal(target, windowsTarget);
events.push('stage');
},
spawn(_binary, args) {
events.push('build');
const config = JSON.parse(
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
);
assert.deepEqual(config.bundle.resources, {
'resources/node-runtime': 'game-runtime/node',
});
return { status: 0 };
},
});
assert.deepEqual(events, ['stage', 'build']);
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
stageRuntime() {
assert.fail('no-bundle must not stage resources');
},
spawn(_binary, args) {
const config = JSON.parse(
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
);
assert.equal(config.bundle, undefined);
return { status: 0 };
},
});
assert.throws(
() =>
runTauriBuild(['--target', windowsTarget], context, {
stageRuntime() {
throw new Error('missing runtime');
},
spawn() {
assert.fail('invalid runtime must prevent build');
},
}),
/missing runtime/,
);
});
test('channel manifest carries version, platform keys and signature', () => {
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
@@ -631,6 +739,7 @@ for (const channel of ['release', 'beta-2']) {
'2.3.4',
);
runTauriBuild([`--target=${target}`], context, {
stageRuntime: () => {},
spawn: (_binary, command) => {
const config = JSON.parse(
readFileSync(
@@ -4,9 +4,25 @@ import { EventEmitter } from 'node:events';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
// 固定解析源码保留上游测试中的替换字符;必须同时核对原始字节与许可。
execFileSync(
process.execPath,
[
'--test',
fileURLToPath(
new URL(
'../src-tauri/vendor/codex-patch-parser/upstream-integrity.test.mjs',
import.meta.url,
),
),
],
{ stdio: 'inherit' },
);
import {
appIdentifier,
defaultRealSwarmTestTask,
@@ -1313,6 +1329,18 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
...Object.fromEntries(
[
'codex-patch-parser',
'codex-utils-path-uri',
'codex-utils-absolute-path',
].flatMap((name) =>
['LICENSE', 'NOTICE'].map((file) => [
`vendor/${name}/${file}`,
`licenses/${name}/${file}`,
]),
),
),
};
const expectedBundledWindowsResources = {
'resources/codex/win-x64/bin/codex.exe': 'coding-agent/win-x64/bin/codex.exe',
@@ -1336,7 +1364,7 @@ assert.deepEqual(
'AI game creator shell base Tauri config must bundle the design-agent resource pack',
);
for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
if (String(key).includes('codex')) {
if (String(key).startsWith('resources/codex/')) {
throw new Error(
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
);
@@ -1365,18 +1393,20 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
assert.deepEqual(
macosTauriConfig.bundle?.resources,
Object.fromEntries([
...[
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
'NOTICE.md',
'manifest.json',
].map((file) => [
`resources/codex/mac-native/${file}`,
`coding-agent/mac-native/${file}`,
]),
...['darwin-arm64', 'darwin-x64'].flatMap((arch) =>
[
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
'NOTICE.md',
'manifest.json',
].map((file) => [
`resources/codex/mac-native/${arch}/${file}`,
`coding-agent/mac-native/${arch}/${file}`,
]),
),
['resources/plugins', 'plugins'],
]),
'macOS must bundle the complete native Codex layout and plugin workspace',
@@ -8,6 +8,13 @@ import path from 'node:path';
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
const source = path.resolve(process.argv[2] || '');
const architecture =
process.argv[3] || (process.arch === 'arm64' ? 'arm64' : 'x86_64');
assert.ok(
['arm64', 'x86_64'].includes(architecture),
'架构只接受 arm64 / x86_64',
);
const requireUniversal = process.argv.includes('--universal');
assert.ok(
source.endsWith('.app') && fs.statSync(source).isDirectory(),
'请传入 .app 绝对路径',
@@ -15,7 +22,28 @@ assert.ok(
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'agc-macos-bundle-')),
);
const app = path.join(root, '陶泥儿 隔离测试.app');
// 产品名从传入的 .app 推导,不在校验脚本里写死;改名后校验对象仍指向同一个包。
const appBundleName = path.basename(source);
const app = path.join(root, `隔离-${appBundleName}`);
// 侧车清单版本必须等于锁定的 @openai/codex 版本,避免两处固定版本漂移。
const appPackage = JSON.parse(
fs.readFileSync(
path.join(
path.dirname(new URL(import.meta.url).pathname),
'../package.json',
),
'utf8',
),
);
const pinnedCodexVersion =
appPackage.dependencies?.['@openai/codex'] ??
appPackage.devDependencies?.['@openai/codex'] ??
appPackage.optionalDependencies?.['@openai/codex'];
assert.match(
pinnedCodexVersion,
/^\d+\.\d+\.\d+$/u,
'package.json 必须锁定精确的 @openai/codex 版本',
);
const home = path.join(root, 'home');
const config = path.join(root, 'config');
const tmp = path.join(root, 'tmp');
@@ -31,17 +59,58 @@ const env = {
};
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
env,
encoding: 'utf8',
timeout: 30_000,
maxBuffer: 1024 * 1024,
});
// 只强制被测应用切片;本机 Xcode 检查工具可能仅提供宿主架构。
const useSlice = command.startsWith(`${app}${path.sep}`);
const result = spawnSync(
useSlice ? '/usr/bin/arch' : command,
useSlice ? [`-${architecture}`, command, ...args] : args,
{
cwd: root,
env,
encoding: 'utf8',
timeout: 120_000,
maxBuffer: 1024 * 1024,
},
);
assert.ifError(result.error);
return result;
}
/**
* APFS 上优先用 `ditto --clone`:整包按区块克隆,秒级完成且几乎不占额外空间。
* 跨卷或非 APFS 时回退到真实复制;两种路径都必须产出可独立改动的副本,
* 因为「缺组件拒绝」用例会在副本里改名文件。
*/
function copyBundle(from, to) {
const cloned = spawnSync('/usr/bin/ditto', ['--clone', from, to], {
encoding: 'utf8',
});
if (
cloned.status === 0 &&
fs.existsSync(path.join(to, 'Contents/Info.plist'))
) {
return 'clone';
}
fs.cpSync(from, to, { recursive: true });
return 'copy';
}
/** 可执行名以包内 Info.plist 为准:它是稳定契约,但没必要在校验脚本里重复硬编码。 */
function readBundleExecutable(appPath) {
const plist = path.join(appPath, 'Contents/Info.plist');
const result = spawnSync(
'/usr/libexec/PlistBuddy',
['-c', 'Print :CFBundleExecutable', plist],
{ encoding: 'utf8' },
);
const name = (result.stdout ?? '').trim();
assert.ok(
name.length > 0,
`无法从 Info.plist 读取 CFBundleExecutable${plist}`,
);
return name;
}
async function hashFile(file) {
const hash = createHash('sha256');
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
@@ -60,7 +129,7 @@ async function handshake(executable) {
await new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('app-server 初始化超时')),
15_000,
120_000,
);
const finish = (error) => {
clearTimeout(timer);
@@ -127,23 +196,39 @@ async function handshake(executable) {
}
try {
fs.cpSync(source, app, { recursive: true });
const copiedWith = copyBundle(source, app);
const resources = path.join(app, 'Contents/Resources');
const bundle = path.join(resources, 'coding-agent/mac-native');
const platform = architecture === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
const bundle = path.join(resources, 'coding-agent/mac-native', platform);
const executable = path.join(bundle, 'bin/codex');
const main = path.join(
app,
'Contents/MacOS/genarrative-ai-game-creator-shell',
);
const main = path.join(app, 'Contents/MacOS', readBundleExecutable(app));
const mainArchitectures = run('/usr/bin/lipo', ['-archs', main]);
assert.equal(mainArchitectures.status, 0);
assert.ok(mainArchitectures.stdout.split(/\s+/).includes(architecture));
if (requireUniversal) {
assert.deepEqual(mainArchitectures.stdout.trim().split(/\s+/).sort(), [
'arm64',
'x86_64',
]);
for (const platform of ['darwin-arm64', 'darwin-x64']) {
assert.ok(
fs.existsSync(
path.join(
resources,
'coding-agent/mac-native',
platform,
'manifest.json',
),
),
);
}
}
const manifest = JSON.parse(
fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'),
);
assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2');
assert.equal(
manifest.platform,
process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
);
assert.equal(manifest.version, 'codex-cli 0.147.0');
assert.equal(manifest.platform, platform);
assert.equal(manifest.version, `codex-cli ${pinnedCodexVersion}`);
const components = [
'bin/codex',
'bin/codex-code-mode-host',
@@ -159,14 +244,41 @@ try {
fs.accessSync(file, fs.constants.X_OK);
const arch = run('/usr/bin/lipo', ['-archs', file]);
assert.equal(arch.status, 0, component);
assert.equal(
arch.stdout.trim(),
process.arch === 'arm64' ? 'arm64' : 'x86_64',
component,
);
assert.equal(arch.stdout.trim(), architecture, component);
}
}
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
const nodeRoot = path.join(resources, 'game-runtime/node');
const nodeManifest = JSON.parse(
fs.readFileSync(path.join(nodeRoot, 'manifest.json'), 'utf8'),
);
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
assert.equal(nodeManifest.platform, 'darwin');
assert.equal(nodeManifest.arch, process.arch);
const runtimeFiles = fs
.readdirSync(nodeRoot, { recursive: true })
.filter(
(file) =>
fs.statSync(path.join(nodeRoot, file)).isFile() &&
file !== 'manifest.json',
);
assert.deepEqual(runtimeFiles.sort(), Object.keys(nodeManifest.files).sort());
for (const [file, digest] of Object.entries(nodeManifest.files)) {
assert.equal(await hashFile(path.join(nodeRoot, file)), digest, file);
}
assert.ok(nodeManifest.files['NODE-LICENSE']);
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
assert.equal(
run(path.join(nodeRoot, 'node'), ['--version']).stdout.trim(),
nodeManifest.nodeVersion,
);
assert.equal(
run(path.join(nodeRoot, 'node'), [
path.join(nodeRoot, 'node_modules/npm/bin/npm-cli.js'),
'--version',
]).stdout.trim(),
nodeManifest.npmVersion,
);
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
for (const file of [
'plugin.json',
@@ -175,12 +287,21 @@ try {
]) {
assert.ok(fs.existsSync(path.join(plugin, file)), file);
}
// 随包 Node 的 npm 是包里唯一允许出现的 node_modules:除了 npm 目录自身与它的子项,
// 还要放行它的上级目录 `game-runtime/node/node_modules`recursive readdir 会列出目录项,
// 少了这一条会让整个门禁对合法包失败——#439 引入后一直没被跑到,直到 2026-09-21 才暴露)。
const allowedNodeModules = (file) =>
file === 'game-runtime/node/node_modules' ||
file === 'game-runtime/node/node_modules/npm' ||
file.startsWith('game-runtime/node/node_modules/npm/');
const packageFiles = fs.readdirSync(resources, { recursive: true });
assert.ok(
!packageFiles.some((file) =>
/(^|\/)(\.env[^/]*|auth\.json|node_modules|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
),
!packageFiles.some(
(file) =>
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
) ||
(/(^|\/)node_modules(\/|$)/.test(file) && !allowedNodeModules(file)),
),
);
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
@@ -212,7 +333,7 @@ try {
assert.notEqual(broken.status, 0);
assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/);
console.log(
'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝',
`PASS (${architecture}, 副本=${copiedWith}): 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝`,
);
console.log(
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,134 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const platforms = {
arm64: 'aarch64-apple-darwin',
x64: 'x86_64-apple-darwin',
};
export function lockedMacPackage(lock, arch, version) {
assert.ok(Object.hasOwn(platforms, arch), '未知 macOS 架构');
const alias = `@openai/codex-darwin-${arch}`;
const entry = lock.packages?.[`node_modules/${alias}`];
assert.equal(
entry?.version,
`${version}-darwin-${arch}`,
'原生依赖必须与应用锁定版本一致',
);
assert.deepEqual(entry.os, ['darwin']);
assert.deepEqual(entry.cpu, [arch]);
const url = new URL(entry.resolved);
assert.equal(url.protocol, 'https:');
assert.equal(
url.hostname,
'registry.npmjs.org',
'只下载锁定的官方 npm 原生包',
);
assert.equal(url.username + url.password + url.search + url.hash, '');
assert.match(entry.integrity, /^sha512-[A-Za-z0-9+/]+={0,2}$/);
return { alias, target: platforms[arch], ...entry };
}
export function verifyPackageIntegrity(bytes, expected) {
const actual = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
assert.equal(actual, expected, 'Codex 下载包 lockfile integrity 不匹配');
}
export function validateArchiveListing(listing) {
const files = listing.trim().split(/\r?\n/u);
assert.ok(files.length > 0);
for (const file of files) {
assert.ok(file.startsWith('package/'), '原生包必须只有 package 根目录');
assert.ok(
!file.split('/').includes('..') && !file.includes('\\'),
'压缩包路径不安全',
);
}
}
export async function prepareMacosCodex() {
assert.equal(process.platform, 'darwin', '该入口仅用于 macOS 构建机');
const lock = JSON.parse(
fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'),
);
const app = JSON.parse(
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
);
const version = app.devDependencies['@openai/codex'];
assert.match(version, /^\d+\.\d+\.\d+$/u, 'Codex 必须锁定精确版本');
const cache = path.join(appRoot, 'src-tauri/target/.macos-native-cache');
fs.mkdirSync(cache, { recursive: true });
for (const arch of Object.keys(platforms)) {
const entry = lockedMacPackage(lock, arch, version);
const archive = path.join(cache, `codex-${entry.version}.tgz`);
if (!fs.existsSync(archive)) {
const response = await fetch(entry.resolved, {
signal: AbortSignal.timeout(300_000),
});
assert.ok(response.ok, `原生包下载失败 HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
verifyPackageIntegrity(bytes, entry.integrity);
const partial = `${archive}.${process.pid}.tmp`;
fs.writeFileSync(partial, bytes);
fs.renameSync(partial, archive);
}
verifyPackageIntegrity(fs.readFileSync(archive), entry.integrity);
validateArchiveListing(
execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }),
);
// 拒绝链接、设备及其它特殊条目,不能让 tar 在包目录之外写入。
const entries = execFileSync('tar', ['-tvzf', archive], {
encoding: 'utf8',
});
assert.ok(
entries
.trim()
.split(/\r?\n/u)
.every((line) => /^[-d]/u.test(line)),
'原生包禁止链接或特殊文件',
);
const parent = path.join(repoRoot, 'node_modules/@openai');
fs.mkdirSync(parent, { recursive: true });
const stage = fs.mkdtempSync(path.join(parent, '.mac-native-'));
try {
execFileSync(
'tar',
['-xzf', archive, '-C', stage, '--strip-components=1'],
{ stdio: 'pipe' },
);
const metadata = JSON.parse(
fs.readFileSync(
path.join(stage, 'vendor', entry.target, 'codex-package.json'),
'utf8',
),
);
assert.equal(metadata.version, version);
assert.equal(metadata.target, entry.target);
assert.equal(metadata.entrypoint, 'bin/codex');
const destination = path.join(repoRoot, 'node_modules', entry.alias);
assert.ok(
!fs.existsSync(destination) ||
!fs.lstatSync(destination).isSymbolicLink(),
'拒绝覆盖链接依赖',
);
fs.rmSync(destination, { recursive: true, force: true });
fs.renameSync(stage, destination);
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
console.log(`[macOS Codex] ${entry.version}: lockfile integrity 已验证`);
}
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
await prepareMacosCodex();
}
@@ -0,0 +1,201 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import { test } from 'node:test';
import {
lockedMacPackage,
validateArchiveListing,
verifyPackageIntegrity,
} from './prepare-macos-codex.mjs';
const lock = JSON.parse(
fs.readFileSync(new URL('../../../package-lock.json', import.meta.url)),
);
const version = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url)),
).devDependencies['@openai/codex'];
test('both macOS dependencies resolve from the lockfile without floating versions', () => {
assert.equal(
lockedMacPackage(lock, 'arm64', version).target,
'aarch64-apple-darwin',
);
assert.equal(
lockedMacPackage(lock, 'x64', version).target,
'x86_64-apple-darwin',
);
assert.throws(() => lockedMacPackage(lock, 'other', version));
assert.throws(() => lockedMacPackage(lock, 'x64', '0.0.0'));
});
test('native package integrity rejects tampering', () => {
const bytes = Buffer.from('pinned package');
const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
verifyPackageIntegrity(bytes, integrity);
assert.throws(() =>
verifyPackageIntegrity(Buffer.from('modified'), integrity),
);
});
test('archive traversal and non-package entries fail closed', () => {
validateArchiveListing(
'package/package.json\npackage/vendor/target/bin/codex\n',
);
for (const listing of [
'',
'/tmp/payload',
'package/../private',
'other/file',
'package/..\\file',
]) {
assert.throws(() => validateArchiveListing(listing));
}
});
test('CI pipeline is manual, publishes the macOS partition and never reuses a developer workspace', () => {
const pipeline = fs.readFileSync(
new URL(
'../../../jenkins/Jenkinsfile.ai-game-creator-shell-macos-build',
import.meta.url,
),
'utf8',
);
for (const required of [
'genarrative-agc-macos',
'disableConcurrentBuilds()',
'$AGC_AGENT_ROOT',
'StrictHostKeyChecking=yes',
'git merge-base --is-ancestor',
'allowEmptyArchive: false',
"string(name: 'AGC_UPDATE_CHANNEL', defaultValue: 'dev'",
'AGC_UPDATE_CHANNEL=${params.AGC_UPDATE_CHANNEL}',
"string(credentialsId: 'AgcUpdaterSigningKey'",
"string(credentialsId: 'AgcUpdaterSigningKeyPassword'",
"string(credentialsId: 'AliyunAccessKeyId'",
"string(credentialsId: 'AliyunaccessKeySecret'",
'AGC_RELEASE_VERSION',
'OSSUTIL_BIN',
// 并行度必须可调:节点是共用机器,写死容易把整机压满或反过来浪费一半核心。
"string(name: 'CARGO_BUILD_JOBS', defaultValue: '8'",
'CARGO_BUILD_JOBS=${params.CARGO_BUILD_JOBS}',
// Agent 工作区按约定匹配,不写死节点名:节点改名(-local → -01)后守卫仍成立。
'"$HOME"/Library/Jenkins/agents/*/workspace/*',
// 上一次发布的 commit 落在 master 上,取到它更新摘要才不会退化成「最近提交」。
'refs/heads/master:refs/remotes/origin/master',
]) {
assert.ok(pipeline.includes(required), required);
}
assert.ok(
!pipeline.includes('genarrative-agc-macos-local'),
'Jenkinsfile 不得写死具体节点名',
);
// 这条管线是正式发布入口(与 Windows 对称):默认真发布,演练需显式勾选。
assert.match(
pipeline,
/booleanParam\(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false/u,
'Channel 发布默认必须是真发布,演练只能显式勾选',
);
// 节点是办公机:离线期间排队的旧构建必须自行让位,且跳过要覆盖后续全部阶段。
assert.match(
pipeline,
/booleanParam\(name: 'SKIP_IF_SUPERSEDED', defaultValue: false/u,
);
// 仓库文件不得出现节点用户名/个人 Home 路径:换机或改名后必须仍然可用。
assert.ok(
!pipeline.includes('/Users/'),
'Jenkinsfile 不得写死个人 Home 路径,工具链位置应按 $HOME 展开',
);
assert.ok(
pipeline.includes('export PATH="$HOME/'),
'PATH 必须在 shell 步骤里按 $HOME 展开',
);
// 超时必须高于实测最慢(78 分钟冷构建 + 共用机器),否则会被中断在链接阶段。
assert.ok(
pipeline.includes('timeout(time: 150'),
'构建超时上限必须留出冷构建余量',
);
for (const diagnostic of ['macOS 发布失败', '被中断']) {
assert.ok(pipeline.includes(diagnostic), diagnostic);
}
assert.ok(
pipeline.includes('.jenkins-superseded-by'),
'必须记录被推进的标记供后续阶段判定',
);
assert.equal(
(pipeline.match(/env\.AGC_BUILD_SUPERSEDED != 'true'/gu) ?? []).length,
3,
'Toolchain / Package / Archive 三个阶段都必须按跳过标记收口',
);
for (const forbidden of [
'triggers {',
'cron(',
'pollSCM(',
'git clean -fdx',
// release:upload 会重新触发一次完整构建,既翻倍耗时也绕过本 Job 的验签门禁。
'release:upload',
]) {
assert.ok(!pipeline.includes(forbidden), forbidden);
}
});
test('macOS release entry verifies the updater signature before uploading', () => {
const entry = fs.readFileSync(
new URL('./build-macos-ci.mjs', import.meta.url),
'utf8',
);
const verifyIndex = entry.indexOf('verifyUpdaterSignature({');
const uploadIndex = entry.indexOf('uploadReleaseArtifacts(release');
assert.ok(verifyIndex > 0, '必须调用更新包验签');
assert.ok(uploadIndex > 0, '必须调用 OSS 上传');
assert.ok(verifyIndex < uploadIndex, '必须先验签再上传,验不过不得写 OSS');
// 无签名私钥时禁止构建:未签名的更新包会被客户端一律拒绝。
assert.ok(entry.includes('TAURI_SIGNING_PRIVATE_KEY'));
// `--no-sign` 会连带跳过 updater 的 minisign 签名,产物将没有 .sig,入口不得传它。
assert.ok(
!entry.includes("'--no-sign'"),
'--no-sign 会同时跳过 updater 签名,产物缺少 .sig',
);
// workspace 会跨构建保留产物:必须先删本次要写的对象,否则会因同名 DMG 失败,
// 或让上一轮遗留的 .sig 让验签门禁误通过。
for (const required of [
// 清理对象用派生的产品名算出来,而不是写死某个名字。
'${updaterArtifactName}.sig',
'${firstInstallName}.sha256',
'fs.rmSync(stale, { force: true })',
"'-ov'",
]) {
assert.ok(entry.includes(required), required);
}
});
test('macOS release entry and smoke script derive product names from config and the bundle', () => {
const entry = fs.readFileSync(
new URL('./build-macos-ci.mjs', import.meta.url),
'utf8',
);
// 产品名决定 *.app、updater 归档与 DMG 卷名:写死会在改名后静默找错对象。
assert.ok(entry.includes('readProductName'), '入口必须从 Tauri 配置读产品名');
assert.ok(!entry.includes('陶泥儿'), 'macOS 发布入口不得写死产品名');
assert.ok(
entry.includes("const macTarget = 'aarch64-apple-darwin'"),
'macOS 发布入口必须固定单架构目标',
);
assert.ok(
entry.includes('_${version}_aarch64.dmg'),
'首装包名必须保留清单侧单架构分支唯一匹配所需的后缀(Tauri 口径 aarch64',
);
const smoke = fs.readFileSync(
new URL('./check-macos-bundle.mjs', import.meta.url),
'utf8',
);
assert.ok(!smoke.includes('陶泥儿'), '校验脚本不得写死产品名');
for (const required of [
'path.basename(source)',
'Print :CFBundleExecutable',
"'--clone'",
]) {
assert.ok(smoke.includes(required), required);
}
});

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