- 新建 project/write_lock.rs:取锁、等待分类、持锁方诊断、残留回收与 4 条锁用例整体搬移,逻辑不变 - project/filesystem.rs 只保留项目文件 IO(1533 → 680 行),锁相关常量、结构、进程判据与用例全部移出 - project.rs 注册 mod write_lock 并 pub(crate) use write_lock::*,crate::project:: 与 crate:: 既有路径不变 - windows_metadata_is_reparse_point 提为 pub(crate),供 write_lock 复用同一条 reparse point 判据 - agent_db.rs 与 checkpoint.rs 的 PROJECT_FILE_FLAG_OPEN_REPARSE_POINT 导入路径改为 super::write_lock - 同步修正技术方案、Fast GDD 技术方案、decision-log、pitfalls 中指向锁实现的文件路径,并把“拆锁”从后续事项改为已完成
1024 KiB
踩坑与排障记录
当前口径:本文件保留可复用的排障经验;历史条目的旧路由、旧版本和已删除文档仅作根因背景,不得据此恢复退役入口。当前命令、路由和 schema 以代码与
docs/README.md为准。
2026-09-05 Planning V2 审批和续跑必须等过项目锁瞬时争用
- 现象:策划 V2 在 GDD 审批提交修改意见后提示
项目正在被其他写操作占用:...\\.agent\\project.lock,聊天区再出现项目总控 Agent 执行失败,请稍后重试。 - 原因:V1
decide_plan_gdd_at/ hydrate 已按完整或短窗口等待项目锁。V2 的审批、回合启动、策略落盘和 hydrate 直接acquire_project_write_lock,与 GUI 重灌、刚结束的审批写盘或后台扫描撞车就立刻失败。修订后续跑走continue_planning_session_v2,失败被前端写进总控错误位。这不是锁没释放,也不是 UAC。 - 处理:一次性用户意图(审批、回合启动、策略落盘、失败投影、GDD 认领)走完整等待窗口;V2 hydrate 走短窗口。前端 V2 hydrate 对锁争用保持上一份状态,不把瞬时占用画进审批卡。
- 排查顺序:先看错误是否点名
project.lock且发生在提交修改意见或立刻续跑;不要当成总控 Runtime 或 Provider 失败。锁文件在失败后通常已被 Drop 删掉,现场缺文件不否定争用。 - 验证:Rust 定向覆盖 V2 审批、修订续跑和 hydrate 等过短暂占用的项目锁。
2026-09-05 新建项目锁不要把继承 DACL 当成 UAC 事件
- 现象:策划 V2 在 GDD 审批提交修改意见时弹出权限窗口,目标是
Documents\Genarrative GameAgent\gameagent-*\.agent\project.lock,随后AGC ACL 提权修复未成功(exit code Some(1))。 - 原因:#211 把新建 sidecar 纳入私有 DACL 门禁。父目录已是当前用户独占且禁止继承时,刚
create_new的锁文件仍会短暂带继承 ACE;生产路径把这类 DACL 不合格送进--repair-private-acl。独占句柄还会妨碍本进程SetNamedSecurityInfoW。提权再用Start-Process -ArgumentList数组,含空格路径被拆开,helper 参数个数不对并以 1 退出。这不是 V2 审批协议或 Provider 权限请求。 - 处理:本进程新建对象只在进程内收紧 DACL,不因继承 ACE 自动 UAC。项目锁先写入并释放独占句柄,再 harden,回读内容校验后返回;不再对这把新锁走
prepare_for_read。UAC 仍留给允许范围内的外人本对象;提权命令行改为一条已加引号的 ArgumentList。 - 排查顺序:先看错误是否点名
project.lock且含禁止继承/exit code Some(1);不要当成策划 V2 或 Provider 鉴权问题。含空格的Genarrative GameAgent项目根是复现条件,不是业务失败。 - 验证:Windows 定向覆盖含空格项目根取锁、新锁已满足私有 DACL、Drop 删除,以及提权参数把带空格路径保留为一个 quoted token。
2026-09-04 Planning V2 不可变 GDD 创建后不能当没提交
- 现象:
gdd.vN.json已 create-only 落盘,但 index / Markdown / conversation / session 任一步失败后,session 停在provider_failed且current_artifact_version仍指向旧版本。重试会用新 UUID/时间戳再写同一版本号,命中“已存在且内容不同”。 - 处理:把该文件当作提交点。恢复时只认领 session 指针的下一个连续版本并补投影,不要删文件,也不要重建 GDD 身份。hydrate 和同一回合重试都必须走这条认领路径。
- 排查顺序:先看
.agent/planning-v2/gdd.vN.json是否已存在、再看session.json的currentArtifactVersion是否落后;不要为了重试去覆盖不可变文件。 - 验证:孤儿文件重试后仍是同一
gddId/vN,hydrate 能看到当前产物。
2026-09-04 DeepSeek thinking 不能与 tool_choice=required 同时使用
- 现象:DeepSeek V4(默认 thinking)对
tool_choice=required或指定函数返回 HTTP 400:Thinking mode does not support this tool_choice。 - 处理:策划 V2 协议工具固定
tool_choice=auto,由 Runtime 校验必须恰好调用plan_ask_question或plan_submit_gdd。不要按模型名分支,也不要用 required 强行出稿。 - 验证:请求体含
tools且tool_choice=auto;无工具调用时走既有非法输出重试。
2026-09-09 常用设置跨文件保存失败
主配置与 local overlay 的单文件原子写入不能保证整体成功;覆盖层写入失败会留下混合配置。保存前先序列化全部变更,多文件保存保留原内容,错误时逆序恢复并报告回滚失败;单文件保持原写入路径,成功后不回读、不触发外部诊断。此回滚仅处理可捕获错误,不承诺进程崩溃下的事务恢复。
2026-09-09 npm run agc 的 Ctrl+C 不能只依赖 shell 包装层与端口健康检查
- 现象:
npm run agc按 Ctrl+C 后终端回到提示符,但上个工作树的api-server.exe/ SpacetimeDB 仍在监听8082/8083/3101;切到另一个 worktree 再启动 AGC 时,前端仍然连到上个工作树的后端,在改过数据库 / schema 的工作树上会串库。 - 原因:
- Windows 下所有长驻服务都由 Node
shell: true经cmd.exe /d /s /c包装层启动,Ctrl+C 会先杀掉包装层(退出码0xC000013A)。scripts/dev.mjs的stopProcess见到直接子进程已退出就直接return,start-dev-stack.mjs/start-tauri-dev.mjs对已退出 PID 的taskkill /PID <pid> /T /F只会失败并返回stopped: false,于是更深的cargo → api-server.exe没有任何人收。 - 即使走到按根 PID 遍历进程树,遍历依赖快照里的父子链;中间层(包装层)先消失时链路断开,遍历只能拿到根 PID,深处的后端不可达。
- 复用判据只看
.app/dev-stack.json的 status 与/healthz、/readyz、/v1/ping,从不校验端口上的进程属于哪个工作树;残留后端照样“健康”,因此被当成自己的后端复用。
- Windows 下所有长驻服务都由 Node
- 处理:新增
scripts/dev-windows-process.mjs,同时提供按根 PID 遍历与按身份匹配(server-rs/target/debug/api-server.exe绝对路径、SpacetimeDB--data-dir)两条独立清理路径。dev.mjs在直接子进程已退出时也继续清理,并在退出时按身份兜底清扫本工作树后端(复用他人 standalone 时不清理)。start-dev-stack.mjs在收到信号和finally各清扫一次本工作树api-server.exe(仅限本次自己拉起后端的情况),复用前先校验端口监听进程归属,无法证明归属就不复用、改为启动自己的后端并允许端口漂移。 - 排查顺序:先看
.app/dev-stack.json的 status 与实际监听端口是否一致,再用Get-CimInstance Win32_Process按本工作树server-rs\target\debug\api-server.exe路径与 SpacetimeDB--data-dir核对残留进程;不要因为/healthz返回 200 就认定后端属于当前工作树。 - 验证:
node --check scripts/dev.mjs scripts/dev-windows-process.mjs apps/ai-game-creator-shell/scripts/start-dev-stack.mjs;npx vitest run scripts/dev-windows-process.test.ts apps/ai-game-creator-shell/tests/start-dev-stack.test.ts scripts/dev.test.ts;真机确认 Ctrl+C 后没有匹配本工作树api-server.exe路径的残留进程。 - 关联:
scripts/dev.mjs、scripts/dev-windows-process.mjs、apps/ai-game-creator-shell/scripts/start-dev-stack.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
2026-09-02 Tauri 事件桥在浏览器预览中必须 fail-safe
- 现象:Vitest/jsdom 挂载 AGC 客户端时,错误报告通知调用
@tauri-apps/api/event.listen,因缺少window.__TAURI_INTERNALS__产生未处理拒绝;测试断言虽通过,CI 仍以 unhandled errors 失败。 - 原因:错误报告订阅是非阻塞唤醒通道,不能假定所有渲染环境都已初始化 Tauri IPC;模块级
listen在 API 调用前就会访问transformCallback,仅在调用方包一层.then无法消除该环境差异。 - 处理:订阅桥先复用
window.__TAURI__.event.listen(含 globalTauri/native shim),其次仅在__TAURI_INTERNALS__存在时调用模块 API;浏览器预览或订阅失败统一返回 no-op,并在消费层收口 rejection。错误快照读取、焦点和可见性刷新仍是权威路径。 - 验证:
errorReporting.test.ts覆盖无 Tauri 环境无未处理拒绝;appSurface.test.ts全部 385 条用例通过且无 Vitest unhandled errors。
2026-08-27 Provider 成功 handoff 失败时需要保留本地私有原始响应
- 现象:Provider 已返回响应,但 tool-plan handoff 因绝对路径或其它内容安全校验失败,Runtime 只留下
failureKind、哈希和被压平的 JSON pointer;排障时无法确认实际工具名和完整 arguments。 - 处理:项目
.agent、Agent DB 和公共 event 继续只写安全摘要;额外在应用私有配置目录的diagnostics/provider-reconciliation/<projectHash>/<requestHash>.json保存本次响应、tool calls 和校验错误,供本机人工排障。该文件不参与恢复/重试、不复制到项目、不进入 Git,单文件限制 1 MiB,写入失败不改变 reconciliation 语义。 - 排查顺序:先读 Runtime 状态里的
localDiagnostic相对引用,再在应用私有目录读取诊断,核对 requestId、requestSlot、tool name 和失败 pointer;不要为了取得原文而放宽 handoff 的安全门。
2026-08-27 阶段判定不能在持锁的 Provider builder 中再次获取项目锁
- 现象:GDD 修订取证阶段新增后,重新启动策划时前两步表面成功,但父 Supervisor 在收到
project-planning回执、生成下一轮工具计划时失败:项目正在被其他写操作占用:$PROJECT_ROOT\\.agent\\project.lock。 - 原因:
provider_tool_plan在构建请求前已持有.agent/project.lock;plan_root_supervisor_stage_at又调用会自行取锁的 Acceptance Evidence 包装入口。同一进程的文件锁不可重入,持锁调用被误判为外部竞争,等待约 10 秒后失败。问题与 Provider、代理端口或 GDD 内容无关。 - 处理:所有需要一致快照的状态读取保留在项目锁内;阶段判定提供明确的
*_locked内部入口,外层入口仅供未持锁调用方取得一次锁。Provider builder 显式接收并校验当前锁后调用 locked 阶段判定,不引入可重入锁,也不移除 Acceptance Evidence 门禁。 - 排查顺序:先看失败 Run 的事件顺序是否为
delegate receipt ready → 生成工具计划 → 阶段判定项目锁失败,再检查调用方是否已持有 Provider plan project lock;不要因为错误文案包含“其他写操作”就先扩大锁等待或放宽 Provider usage。 - 验证:
cargo check、cargo fmt --check、planning submit 68 passed、Provider request builder 17 passed;阶段测试同时覆盖未持锁包装入口和持锁 locked 入口。
2026-08-26 GDD 新版本提交后不能沿用“已有委派”工具面
- 现象:
plan_root_supervisor_stage_at只按是否存在 delivery 判定Delegated。用户修订产生的新 GDD 仍未完成当前根 Run 的file.read → agent.acceptance_update取证时,模型会看到agent.delegate,可能重复派发同一条策划链。 - 原因:自然语言 playbook 已规定“证据不足先取证、用户修改后才返工”,但阶段工具白名单没有把这条 durable 状态固化。
- 处理:阶段判定复用现有 acceptance gate 的 GDD/session/delivery/graph identity 检查,增加无副作用的
AwaitingAcceptanceEvidence阶段;PLAN_PROVIDER_USAGE_DEFERRED保持 fail-closed,不通过放宽 Provider 使用量门禁解决。 - 排查顺序:先看最新
gdd.vN.json、session.latestSubmittedRef、delivery 是否ClaimedByParent,再看 Acceptance Graph 是否NeedsEvidence;若仍可见agent.delegate,优先检查 plan root 阶段快照,而不是修改 acceptance gate 或 Provider 门禁。
2026-08-15 把校验往链路前面挪,改的不是严格程度而是作用域
- 现象:CI 全量 5 条失败,看上去毫不相干(两条 Goal 续跑停在
needs-reconciliation、一条交接用例断言错误文案、一条恢复用例把不可读 state 的错误抛了出来、一条 Linux-only 用例错误码对不上),实际只有 3 个根因,且三者是同一个形状:新增或既有的检查被放在了链路更靠前的位置,于是它的语义作用域被悄悄放大或提前,而不是「变严」。 - 形状一(判据上提 → 作用域从同 loop 变成跨 loop):
tool_plan_handoff/identity_order_validation.rs的same_tool_plan_repair_chain含 steer cursor、goal revision/快照与 planning session binding 这些本轮量,只在同 loop 的 repair 之间才必须逐位相等。M1B-2(27c3eb847)把它提到loop_iteration分支之外后,loop-N → loop-N+1的正常续跑必然被判成「身份冲突」,整个 run 进needs-reconciliation。同一次上提还把分支内那句同名检查变成了死代码——编译器不报,测试拿到的是外层文案,于是表现成「断言的错误文案不对」这种看起来无关的症状。模块内本来就有反例可对照:ledger.rs的is_later_repair_identity一直把这条判据显式限定在current_loop == candidate_loop。 - 形状二(探测器的前置条件变成整条链路的 gate):
recovery_scan.rs的missing_plan_submit_anchor_candidate_at用?强读 runtime state,而它被挂在每个 Agent resume 循环的最前面。真正负责处置「state 不可读」的是它下游的resume_game_creator_agent_finalization_at——那里会从 task record 重建并 fail-closed 到needs-reconciliation。前面这一?把整轮 resume 打断,恰好绕过了专门为这种情况写的兜底。这个探测器甚至不适用于出事的 Agent(它只认 planning agent +agent-delegate),读失败纯属前置成本。 - 形状三(通用解析器排在专用校验之前,错误码被压平):
planning_storage.rs把resolve_local_project_path的失败整体映射成PLAN_INVALID_PATH,但该解析器会先于 planning 自己的校验逐组件拒绝符号链接。于是「不可信路径」被报成「非法路径」,与同模块ensure_planning_parent、verify_regular_planning_file的分类自相矛盾。这条自M1B-1(f453c2ca2)写下就没绿过——用例是#[cfg(unix)],Windows 上编译都不参与(0 tests),只有 Linux CI 能看见。 - 处理:形状一改回 loop 分支内,并补一条正向回归(跨 loop 且 steer cursor / goal revision 已前进必须被接受),把边界钉住而不是只钉拒绝;跨 loop 身份由
same_durable_tool_plan_run守,binding 漂移另有provider_retry.rs的 per-request 判据兜底,去掉上提不留缺口。形状二把强读改成「读不到就Ok(None)」,让处置权回到下游兜底。形状三新增resolve_planning_path,先用模块自己的planning_metadata_is_link_or_reparse判链接/重解析点并返回PLAN_UNTRUSTED_PATH,再交给通用解析器;模块内 13 处解析全部改走它,分类统一。 - 定位手法(比二分快得多):失败用例的临时项目目录在 panic 后不会被清理(
fs::remove_dir_all写在用例末尾),直接读里面的.agent/agent.db与.agent/runtime/agents/*.json。本次两条 Goal 失败在 db 里留下failureKind=tool-plan-integrity、errorChars=55、errorSha256=0750f609…,把候选错误文案逐条算 SHA-256 一比即命中,requestSlot从loop-1-repair-0到loop-2-repair-0直接指出是跨 loop 那一跳。错误只留指纹不留原文时,指纹就是可检索的。 - 验证:4 条可在 Windows 复现的用例全部转绿(含新增回归);Linux-only 用例在 WSL 的独立 Linux clone 中验证。Windows worktree 的
.git文件可能记录 Windows 路径,WSL 内应从可访问的主仓库 clone 或真实路径 fetch,不要直接复用不可解析的 worktree 元数据。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/identity_order_validation.rs、.../agent/runtime_driver/recovery_scan.rs、.../agent/runtime_protocol/planning_storage.rs;decision-log 同日条。
2026-08-15 #[cfg(windows)] 里的代码不参与 Linux CI 编译,CI 绿不代表能构建
- 现象:把 master(
9f5c84ee7)合进feat/five_min_design后,cargo check --all-targets在 Windows 上直接error[E0658]: use of unstable library feature 'windows_by_handle',位置是apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs的metadata.number_of_links()。该文件与origin/master逐字节相同,即 master 自身在 Windows 上就构建不过。 - 原因:
std::os::windows::fs::MetadataExt::number_of_links至今未稳定(rust-lang#63010),而rust-toolchain.toml锁的是 stable1.96.0。引入它的提交是578f8019f(优化 AGC 项目入口并识别 Godot 工作区),其中 unix 分支用MetadataExt::nlink()(已稳定)、windows 分支用了未稳定的对应物。Linux CI 上#[cfg(windows)]整块不参与编译,所以 CI 全绿。 - 更普遍的形状:只要一段代码只在某个
#[cfg(target_os)]下编译,它就完全绕过了其它平台的 CI——不只是 unstable feature,还包括类型错误、借用错误、缺失 import。跨平台分支是「双写」,两侧都得有人真的编译过。 - 处理:本仓库对「文件是不是无硬链接普通文件」统一自行声明
ByHandleFileInformation并调用GetFileInformationByHandle,见runner/endpoint.rs、tool_plan_handoff/storage_windows.rs、project/agent_db.rs、git_inspect.rs、image_inspect.rs、agent/generation/canvas_generation.rs。manifest.rs当前已采用同一实现,并保留 fail-closed 语义:无法取得句柄信息或确认存在硬链接时均拒绝,同时拒绝 directory / reparse point。 - 验证:改后
cargo check --offline --all-targets通过、cargo fmt --check通过、project::manifest与 godot 相关定向测试 65 passed / 0 failed。判断「是不是本次合并引入」的通用手法:git diff origin/master -- <file>为空即说明该文件就是 master 原样,问题不在合并。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs;rust-toolchain.toml;master 提交578f8019f。
2026-08-14 planning sidecar 必须区分“可读”与“可写”,canonical bytes 也不等于 typed 指纹
- 现象:如果为了保护 Runtime-owned 事实,直接把
.agent/planning/**加进现有 private-control 读门,planning 子 Agent 的file.read/file.list会一起失败;反过来若只依赖工具面约束,通用file.write、file.patch、file.delete、project.patchset或 checkpoint restore 仍可能覆盖 GDD/session。另一个常见误判是把“能反序列化且语义相同”的 JSON 当成已提交文件,导致尾换行、字段重排或重复键绕过不可变事实的字节身份。 - 原因:
.agent/planning/**是 Runtime 专用 durable sidecar,但 planning Agent 需要只读观察;game/fast_gdd.md又是 Runtime renderer 的人读投影,二者都不能复用“读写一体”的旧 private path 判据。存储 bytes 与 typed fingerprint 是两个门:前者约束磁盘 canonical serialization(Rust struct 字段顺序、compact UTF-8、无 BOM/尾空白、无重复键),后者约束 domain-separated 业务 payload 的完整性;只过其中一门都不能视为 authoritative。 - 处理:保持现有 private-control 读门不含 planning,新增只挡写 predicate;所有通用 mutation 与 restore 路径在推进 revision 前先拒绝
.agent/planning/**/game/fast_gdd.md,专用 writer 再校验project-planning + agent-delegate + standard + project-supervisor身份。GDD/index 等不可变文件走项目锁、同目录临时文件、sync_all、回读与 no-replace 发布;相同 canonical bytes 才是 replay,任何其它内容都是 identity conflict。session 只保留一个.session.json.previous,primary 损坏时 fail closed,不得拿 previous 猜测新旧。 - 验证:先用 planning Agent 的只读 action 验证 sidecar 可列出/读取,再逐项证明
file.write、file.patch、file.delete、project.patchset与 checkpoint restore 均拒绝;storage 测试应覆盖 duplicate key、BOM/尾空白、字段顺序、symlink/目录/硬链接、create-only replay/conflict、session 缺 primary 提升、primary 损坏和合法 successor。第 9.1 节 golden vector 当前为 3857 bytes /sha256-serde-json-v2:a59856de7ef134cf2f49c4dedd2ba10ae4ab2340a9634d402eb792b6ee5458f0。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs、apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs、apps/ai-game-creator-shell/src-tauri/src/patchset.rs、docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md第 8.3~10.2 节。
2026-08-12 在 autonomous-game-build 下试图向用户提问,会让整条工作流永久瘫痪
- 现象:给自主构建链路加「问用户一句」的需求时,最自然的两个想法——让 DAG 节点自己问、或让父 Supervisor 代问——都不成立,而且第二个的失败方式是灾难性的。
- 拦截一(节点自己问):
apps/ai-game-creator-shell/src-tauri/src/user_input.rs:367-394的validate_user_input_action_owner三路 OR 拒绝任何带parent_agent_id / parent_run_id / delegation_id的 run。autonomous 的 ready-task 调度器在apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs:905-907显式给每个 DAG 子节点写 parent,因此必然命中。 - 拦截二(父代问):
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs:198-215只要run_profile == autonomous-game-build就把user.input_request从auto_tools/confirm_tools移除并推进denied_tools;apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs:2604-2616在执行层再拒一次。两处判据都只看 profile、不看agent_id,所以 root Supervisor 自己也在禁令内——「父能问、子不能问」这个中转赖以成立的不对称,在 autonomous 下根本不存在。 - 真正的坑(后果放大):硬闯不是「这次失败」,而是整条流水线停摆。执行层拒绝会走
mark_game_creator_agent_runtime_needs_reconciliation_at,apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs:1157把runtime.status直接写成failed;此后每次调度 ready task 必经的autonomous_game_build_root_task_is_active(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs:685-693)只认pending | running | waiting-for-confirmation | waiting-for-user-input,failed不在其中,于是报「父 Run 已不再活跃」,剩余节点一个都起不来,须人工核对才能恢复。同一类故障本仓库已踩过一次,见下方「自主模式不能保留任何 RequiresConfirmation 漏口」条。 - 也别想着换 profile 绕开:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/run_configuration.rs:264-266明文「子 Run 不能切换父 Run 的 Run Profile」,且 profile 是 run 绑定时 CAS 锁死的终身属性。 - 处理:需要与用户往返的链路必须整体跑在
standardprofile 下。standard的 ready-task 调度器(task_start.rs:566-580,走..._with_source_at而非..._with_link_at)不写 parent,节点是自己的 root run,上述拦截一并不适用。这是 2026-08-12 立项策划 D9 改用「Supervisor + standard 下游工作流节点」的直接原因,见 decision-log 同日条。 - 附带结论:
autonomous_owner_artifact_validation_available_for_run_at(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs:416-441)同样四重绑死 owner 白名单、profile、source,且要求parent_agent_id为 Project Supervisor——standard 节点无 parent,第 436 行即不通过。想给 standard 路径加 owner 产物验证的人不要试图扩展它,必须另建。
2026-08-12 往 trusted supervisor source 里加新 source,等于同时授予 Goal Contract 参与者身份
- 现象:
agent_runtime_supervisor_source_is_trusted(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs:113)读起来像一个「谁能启动 Project Supervisor」的入口白名单,实际早已是多条互不相干的授权判据的共同开关。2026-08-11 合入动态目标验收图后,它的非测试消费者从 2 个文件涨到 10 个文件 18 处调用:run 启动(commands.rs:698)、steer(commands.rs:876、steering.rs:763)、Goal Contract 创建权限(goal_contract.rs:496)、根控制面工具是否被剥离(provider_request_builders.rs:134的root_control_authority)、验收图完成门(acceptance_graph.rs:595)、run configuration、lifecycle_control、task_start、project_gates、autonomous_completion。 - 陷阱:这些判据全都不看 Run Profile。
goal_contract_acceptance_completion_blocker_at_locked(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs:568-611)只要求「agent_id是project-supervisor+ binding 是无 parent 的 root + source 可信」,未冻结 Goal Contract 就返回blocked,并被main_loop.rs:213、main_loop.rs:1803、finalization.rs:398消费。因此给一个用途完全不同的新 source(例如立项策划的 plan chat)加进白名单,会让它的根 Run 立刻背上「必须先调agent.goal_contract」的义务;如果该 source 的工具面按 exact allowlist 设计、不含这个工具,根 Run 就永远无法完成——而且症状是 run 卡在完成门,不是启动失败,排查方向容易跑偏。 - 更坏的一半:把新 source 排除出白名单并不能脱身。同一协议还有一道入口门
validate_root_goal_contract_control_plan_at(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs:171),由provider_tool_plan.rs:434在通用 tool-plan 解析路径上无条件调用,判据只有「agent_id是project-supervisor+ binding 的 root 是自己 + 存在 run profile binding」——连 source 都不看。合同不存在时它强制本轮恰好一个agent.goal_contract动作且plan_update/legacy plan/response全为空,于是「第一轮先问用户一个问题」或「第一轮先回复」的 Agent 会被直接判协议错误。三处判据里只有agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID是共同项,改agent_id是唯一能一次性解耦的做法。 - 处理:新增 trusted source 前,先逐个确认这些调用对新 source 的语义是否成立,尤其是 Goal Contract 创建、验收图完成门与 steer 三处,再单独确认不看 source 的入口门;需要区分时,应当拆出「可信入口」与「Goal Contract 参与者」两条判据,而不是继续复用同一个函数。立项策划已按第 23.1 节裁决进 matcher 并参与 Goal Contract;steer 用独立于 matcher 的显式否决(
reject_supervisor_plan_root_steer),不得用「不进 matcher」实现。复核结论见 decision-log 2026-08-13M1A-1条。 - 双向提问:调用点既是判据又是构造器时,只问「会不会误得不该有的语义」不够,还要问「落到通用兜底会不会丢掉该有的语义」。
resolve_game_creator_agent_runtime_retry_configuration_at因此在M1A-1漏出,由M1A-3补强判据与保源;拒绝继续用弱判据,授予必须用强判据。 - 相关:
requiredEvidence只接受tool:<Runtime 工具名>且必须命中agent_runtime_acceptance_evidence_tools()(apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs:74-96,当前 18 项)。确定性收束的任务和 Runtime 内部产物验证都不产生 Provider 回执,因此无法为验收节点提供证据——不要指望「让 Runtime 自己验一下」能满足验收图。
2026-08-12 给 manifest 加"新鲜度门控"或身份字段的两个陷阱
-
陷阱二(补身份字段只堵一条路):改写
task.status的写入路径有两条互相独立的。除update_manifest_task_status_at(apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs:590-608)外,还有毫无秩序守卫的set_task_status(同文件580-588,直接task.status = status),其调用方record_draft_task_progress(同文件387-413)把code-prototype列进批量置Completed的清单,由用户可随时触发的game.generate_draft命令调用。只给前者补statusRunId/statusSource,后者会原样保留上一次写入的旧身份印记,于是污染写入反而通过校验,比不校验更危险。对照组是apps/ai-game-creator-shell/src-tauri/src/agent/generation/trace.rs:613-637的set_task_status_if_current,它有should_replace_task_status秩序判定——两者的不对称本身也是一个待处理项。 -
处理:本阶段不加机制,manifest 明确降级为 lineage 判定通过后的补充信号(见 decision-log 2026-08-12 条)。将来要做,必须同时覆盖两条写入路径,并统一走毫秒换算 helper。
-
验证:
apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts的「keeps an unbound manifest out of the verdict until the current main is terminal」钉住了现有边界与残余风险;该用例最后一条断言即为已记录的残余风险,改动它就意味着重新裁决,必须同步更新决策记录。 -
现象:前端 Runtime map 以 Agent ID 保存当前记录。新一轮仍会复用
code-prototype、art-director、art-asset-plan这些 Agent ID;若旧 root 已终态但 main/美术 child 或 manifest 仍在收口,直接从 live map 归档会在新 root 接管后丢失旧后代,或把新轮证据误接到旧阶段记录。 -
修复:根进入终态时按完整
agent/session/run身份保存 root-scoped Runtime 快照,后续只合并同一稳定身份的更新;manifest 快照只在该 root 仍为当前 root 时捕获。归档前重新执行严格 lineage、全终态、单 main/单 active art 与 reconciliation 门禁,并用 root run 派生稳定 message ID。
用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
记录格式
## 问题标题
- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue
严格 delegated 授权失败不能把 retry 回落为普通美术权限
- 原因:代码混用了“是否声称动态美术 lineage”和“是否已证明当前首次委派授权”两个事实。retry 使用新 run 和新 delegationId,但没有与之绑定的 durable delivery,结构上无法满足现行 exact lineage;授权失败应表示不可信候选,而不是普通 Agent。
- 验证:覆盖 failed/cancelled child 重试零 successor run、遗留 retry 的 file/patchset/Canvas/command/preview/再委派拒绝、失败回执认领后同 run 同 target 重委派拒绝、下一轮 main 重新审计后同 target 新委派放行且 delivery 全链一致并恢复
assets/**,同时对完整 DAG 与非美术 retry 做非回归。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs、docs/project-memory/shared-memory/decision-log.md。
不能用可玩游戏 smoke 验证 code-prototype 上游的固定文档产物
- 现象:真实新项目的
design-foundation、balance-seed、art-asset-plan或audio-asset-plan已写完自己的固定文件,却始终无法收束;project.verify因项目没有package.json不可用,game.static_smoke又报告缺少活动<canvas>。测试若先调用fake_llm_game_draft(),同一路径却会“通过”。 - 原因:初始化
game/index.html只是无<canvas>的占位页,真正游戏要到下游code-prototype才生成。把所有 mutation verification 都等同于可玩游戏 smoke,会让上游 artifact-only owner 在依赖顺序上自锁;预写 fake game 的夹具提前完成了下游职责,掩盖了真实新项目路径。 - 处理:四个 pre-code 固定 owner 在最终收束门由 Runtime 内部验证 canonical 产物:普通文件有界读取、非空,JSON 可解析,无 incomplete marker,且相对根完成合同 baseline 已变化。该能力不进入 Provider 工具目录,不新增 commandId;凭证类型为
runtime.owner_artifacts_validate,只写普通verifiedRevision,不得写staticSmokeVerifiedRevision或制造 smoke / preview trace。文件路径门与验证必须复用同一 canonical owner 映射。code-prototype和preview-readiness继续执行真实game.static_smoke,preview-playtest继续独立浏览器验收;publish-package不借本修复扩入内部验证。 - 身份与恢复:只允许完整 GUI / CLI 16 任务 DAG 的
agent-ready-task-scheduler确定性直接 child、当前活跃根和正确 parent/binding;错误 source、delegated run、历史/终态根、非当前 root、跨 Agent/run 一律失败关闭。owner 再次 mutation 必须令旧凭证失效;相同身份恢复时可按当前磁盘事实确定性重验。 - 并发恢复补充:自主根任务 journal 写入后建立或重建 completion contract 时,初始 manifest reset 与 continuation reconciliation reset 不能重新使用 fail-fast 项目锁。异步 child finalization 可以合法插入两次取锁之间,使已入 journal 的新根被误记为
completion-contract-failed。这两条 reset 必须使用现有有界等待项目锁,超时仍失败关闭;只验证 scheduler 合同的测试应预占 child Runtime lane,不能真实启动后台 worker 后再手工改 manifest。确定性回归要显式持锁,分别证明初始合同与 continuation 合同等待释放后成功落盘。 - 验证:夹具必须从
init_local_game_project_at开始,先断言无package.json且占位入口 smoke 失败,再证明产物不齐阻断、齐全后内部验证通过、无 smoke trace、再次 mutation 失效;另覆盖四个 owner 路径矩阵、错误身份、恢复、跨 run 凭证、art-director有/无 Key、动态美术借凭证拒绝、code-prototypeproject.verify-only 阻断和试玩 executor 身份。 - 关联:
docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md、docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md。
UI 设计 State 的 strict JSON round-trip 不能混用两种浮点序列化表示(2026-08-19)
- 现象:为节点拖拽/缩放生成非整数 Transform 后,保存报“UI 设计 State 安装后回读与待写内容不一致”;由于读取主文件失败关闭后恢复
.previous,后续回读表现为刚导入的 spirit/sprite 资产丢失。 - 原因:写入使用
serde_json::to_vec_pretty,它对f32输出短十进制(如348.5318);读取端却以serde_json::to_value重建 canonical JSON,重新扩展为精确二进制值(如348.53179931640625)。两者作为serde_json::Value不相等,合法的新主文件被误判为不受支持,然后错误回退到旧恢复副本。 - 处理:严格 envelope 检查必须使用与磁盘写入相同的
serialize_ui_design_document再解析为Value,仍拒绝未知字段/值,却允许合法f32的稳定文件表示;不可再把to_vec_pretty与to_value的数字文本直接比较。 - 验证:持久化回归使用带
spiritsprite、Image.target_graphic引用及拖拽式非整数 Transform 的完整 State,断言保存返回和后续 load 均完整相等;同时保留旧空对象/未知 envelope 字段拒绝、CAS 和.previous恢复测试。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs。
资源管理第二轮修复后不能继续用第一轮文档和弱测试作为验收合同
- 现象:代码已经改成分区内 SVG plane 和卡内媒体,文档仍要求单全局 Overlay 或中央大图;CSS 正则和浅层 AppSurface 测试保持绿色,但真实 Tauri WebView 仍会默认缩放、主动预览请求饥饿、过滤后媒体继续播放或超深布局反复提交非法坐标。
- 原因:第一轮编码时同步编写的 PRD / 技术方案被后续代码修复绕过,第二轮只改实现和局部测试,没有把新验证结论回写正式合同。React 合成 wheel 事件、单例滚动 ref、只按数量限制的 base64 缓存、跨 scope 共用的活动读取计数、无优先级有界队列和前后端不同坐标边界又分别跨越浏览器、会话状态与 IPC 边界,浅层文本断言无法证明运行时行为。
- 处理:每轮验证后按“当前代码 + 最新决策 + 真实运行证据”同步修订 PRD、技术方案、决策记录和回归测试。原生可取消事件要直接断言
defaultPrevented;队列验证主动请求替换预取;预览 data URL 只作临时传输并转为可撤销 Blob URL,LRU 同时限制项目数和总字节;项目 / mode 切换推进 epoch,旧finally不得扣减新 scope;媒体状态同时核对可见集合;滚动分别按内外 scope 保存;坐标合同由共享 TS 与 Rust 同边界维护;暂时 / 永久错误在模型中显式分类。 - 验证:运行定向 hook / AppSurface / 纯布局 / Rust 边界测试,再执行类型检查、编码检查和
git diff --check。 - 关联:
apps/ai-game-creator-shell/src/view/project-development/index.tsx、apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts、apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts、apps/ai-game-creator-shell/src-tauri/src/project/resource_layout.rs、docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md。
预览读取不能像放弃旧 CAS 回调一样直接重置并发槽
- 现象:A 项目的 3 个预览 IPC 仍挂起时切到 B,前端把活动计数归零并立即再发 3 个;界面不会被 A 的迟到结果污染,但原生同时保留 6 个读取。连续项目 / mode / Hook / 窗口切换会继续叠加整文件缓冲、base64 临时字符串和 WebView IPC 载荷,绕过
64 MiB终态缓存预算。 - 原因:布局 CAS 已经发出后只能依靠 revision / 系统锁仲裁,前端放弃回调是正确语义;只读预览却有明确的物理内存和文件读取成本,可以协作取消。把 scope epoch 的逻辑隔离误当成底层取消,又让每个 scope 自行拥有 3 个物理槽,实际并发就不再全局有界。
- 处理:前端继续用 epoch、优先级队列、LRU 和完整资源身份隔离展示状态,但每次挂载 / scope 变化生成不复用的
scopeId,每次 IPC 生成唯一requestId。三个安全读取命令共用 Tauri 进程级 3 permit 管理器;切换和卸载调用窄 scope 取消命令,等待 permit 与固定块读取都检查取消。permit 和 request/scope 清理 guard 必须移入真正的spawn_blocking读取闭包;WebView 卸载或调用方 abort 只会丢弃外层等待,不能让仍在运行的 blocking 读取提前释放物理槽或从取消 registry 消失。取消任务在 base64 前退出,所有终态清理活动 request / scope registry。seen request tombstone 最多保留8192项;非活动 cancelled scope tombstone 的预算为1024项,活动取消 scope 为防复活必须临时钉住并在结束后重新收敛。不得为了追求整个 registry 字面清零而删除防重放 / 防复活记录,也不得让已经结束的 scope 长期占用预算外记录。不得放宽原有项目边界、登记、权限、链接、签名、大小、漂移或安全 SVG 门禁。 - 验证:先让旧 scope 占满 3 个 permit,再连续执行 A → B → A、mode、Hook 和多窗口切换;断言原生活动峰值始终
<= 3,旧等待任务不打开文件,旧在途任务在最近检查点释放,新 scope 随后启动,取消任务不编码 data URL / 不创建 Blob URL,最终活动 request / scope registry 为零。阻塞读取进入后主动 abort 外层 future,必须证明旧 blocking 任务仍占 permit、仍可按 scope 取消且新请求不能提前启动。另分别证明 seen request8192项的硬上限、cancelled scope 超预算时不淘汰活动记录,以及任一活动 scope 结束后非活动 tombstone 立即收敛到1024项预算。前端再断言旧then / catch / finally和取消 ACK 均不写新 scope;内部取消类别只允许精确匹配,不能因真实错误正文恰好包含该标识而静默吞错。 - 关联:
apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts、apps/ai-game-creator-shell/src-tauri/src/resource_preview_scheduler.rs、apps/ai-game-creator-shell/src-tauri/src/resource_inspect.rs、apps/ai-game-creator-shell/src-tauri/src/image_inspect.rs。
依赖线与资源卡不在同一 transform 层时会在滚动和缩放中分离
- 现象:静止时依赖线似乎对齐,触摸板缩放或连续滚动后线段会追赶、漂移或忽隐忽现;某一资源分区的长线还可能出现在相邻分区。
- 原因:资源卡位于各自可滚动、可缩放的 section plane,全局 SVG 却是外层兄弟节点;通过
getBoundingClientRect、RAF 和 React state 重建屏幕端点无法与浏览器合成层 transform 原子同步。把四个 viewport 做成一个 clipPath 并集也不具备“每条线属于哪个分区”的所有权语义。 - 处理:让每个固定分区在自己的
.game-resource-plane中拥有独立 SVG,卡片与路径都直接使用布局逻辑坐标并共享父级 CSS scale / 原生 scroll;viewport 原生 overflow 负责本区裁剪。DOM 测量只换算本区逻辑 viewport,用于完整路径、incoming / outgoing 继续线和两端离屏隐藏,不参与端点身份或主路径坐标。每区 observer 和 RAF 各至多一个,卸载时清理。 - 验证:同时挂载至少两个分区和各自同类型关系,断言每条边只存在于对应分区 SVG;只滚动其中一分区,另一分区的逻辑 viewport 与 path 不变。另覆盖缩放后 SVG / 卡片仍在同一 plane、双向离屏继续线、两端离屏隐藏、marker、自环以及 mode / 项目切换清理。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/ResourceDependencyOverlay.tsx、apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts、docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md。
正式保存不能等待 React state 才取得草稿 CAS 的新 revision
- 现象:用户刚完成编辑就点击“保存到项目”,自动草稿保存已经成功,但正式提交仍携带旧
expectedDraftRevision,于是单窗口也得到 draft revision conflict;快速连续保存时还可能使用不同 commitId 重复 staging。 - 原因:
setDraft(result.value)的 React state 提交晚于当前 Promise 链,正式保存若从闭包或下一次 render 读取 revision,会把 UI 调度时序误当成持久化顺序。相同问题也会出现在选择变化未标脏、父组件每次 render 新建 scope 对象而重复恢复、项目切换后旧 generation 回调继续写 notice。 - 处理:草稿保存队列在 CAS 成功后同步更新
draftRef.current并直接返回权威 draft;正式提交继续使用该返回值的 revision。scope 按 project/draft/intent/source 原始字段稳定化,所有导入、保存、生成和事件回调捕获当前 epoch,选择变化属于草稿合同并必须标脏。首次正式保存冻结 commitId/idempotencyKey,未知结果只重放原请求。 - 验证:用 deferred Promise 证明草稿 CAS 完成后正式 commit 使用新 revision;相同 scope 值重渲染不重复 recover/load;锁定图层选择进入草稿更新;项目切换后迟到 generation/commit/event 均不改变新会话。
- 关联:
apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx、apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx。
共享画布不能用全局 DOM 查询或宿主整包 CSS 作为隐式依赖
- 现象:页面挂两个画布时,第二个画布点击小地图会移动第一个画布;反复挂载后 wheel 触发多次。Tauri 单独引入网站
index.css时还会带入账号、项目页和历史业务样式,或因仓库外源码解析到第二份 React 而出现 Hook 错误。 - 原因:
document.querySelector、body 级 portal、未清理的 listener/observer/animation frame 和不受作用域约束的 CSS 都把组件实例与网站宿主当成全局单例;Tauri Vite 默认根目录又不等于仓库根,React 解析路径可能分叉。 - 处理:共享小地图先从当前 viewport 查询,只有文档中唯一候选时才兼容旧单实例形态;wheel、ResizeObserver 和 animation frame 在 effect cleanup 中逐项释放。portal 支持实例 root,默认 body 只作兼容。共享 CSS 全部限定在
.genarrative-image-canvas,两宿主 alias 同一packages/源码并 dedupe React/ReactDOM,Taurifs.allow覆盖 repo root,禁止导入主站完整index.css。 - 验证:共享 React 测试重复 mount/unmount 后 wheel add/remove 数量相等、ResizeObserver 精确 disconnect,并挂两个含各自小地图的 viewport,确认只更新目标实例;网站与 Tauri 壳分别 typecheck/build。
- 关联:
packages/image-canvas-react/src/useImageCanvasViewportControls.ts、packages/image-canvas-react/src/CanvasPortal.tsx、根目录与apps/ai-game-creator-shell的 Vite 配置。
桌面工作台不要让 Supervisor 内容高度挤掉输入区和 Agent Dock
- 现象:
1280×800或更矮窗口中,Supervisor 的消息、Runtime 状态和错误正文共同按内容高度增长,聊天输入被推到栏外;底部 Dock 使用固定宽度卡片时还会在中等宽度造成页面级横向溢出。中央画布的生成卡和状态栏若同时固定高度并隐藏 overflow,进度、失败、重试、保存或取消动作会被裁掉。 - 原因:四区工作台没有把“主区内部滚动”和“页面级滚动”分开;Supervisor 的长状态没有独立上限,Dock 卡片不能收缩,宿主又用全局按钮/固定行高样式覆盖共享 chrome。
- 处理:桌面工作台使用
100dvh两行网格,第一行minmax(0, 1fr)承载中央区与 Supervisor,第二行承载 Dock;消息和 Runtime 分别内部滚动,composer 保持最后一行并设置明确层级。Dock 卡片使用可收缩 flex 与文本省略。画布 dialog、进度/失败卡和状态栏设置min-height: 0、受限最大高度与内部滚动;宿主不再覆盖所有按钮,只为主要动作和布局提供 token 化薄样式。 - 验证:AppSurface CSS 合同检查
100dvh、Supervisor composer、Runtime 内滚动、Dock 常驻/可收缩;素材画布测试检查生成 dialog、operation card、状态栏和窄屏保存动作不会被裁剪。真实入口在1280×800测量 document/body client 与 scroll 一致;登录门禁不可为视觉测试绕过。 - 关联:
apps/ai-game-creator-shell/src/styles.css、apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css、apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts、apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx。
素材保存区不要把机器 subtype 当普通文本框(2026-08-06)
- 现象:保存区同时显示“画布素材”和裸
asset文本框,普通用户无法判断两者用途;自由修改 kind 会形成无法稳定参与类型布局、Agent 合同和替换兼容性的 subtype。工具栏与保存设置挤在同一行时,主要保存按钮还会被压缩或裁切。 - 原因:把 Host Port 的
name / assetKind / mediaTypeDTO 直接映射成同层输入控件,没有区分用户命名、机器分类和编码格式,也没有为中央区域的真实容器宽度保留主操作列。 - 处理:名称保留编辑;create kind 使用 Runtime 权威四项目录和中文标签,refine 从源 manifest 继承并锁定,未知历史值只透传;格式继续使用有限枚举。工具动作和保存设置显式上下分行,保存列使用
max-content + nowrap,窄容器时按钮独占整行。普通工作区状态只显示项目名称,不把绝对路径作为默认辅助文案。 - 验证:Surface 测试断言四项用途、默认值、精修未知 kind 锁定、最终 commit 参数和保存按钮 CSS;AppSurface 断言普通界面找不到绝对路径,内部 Tauri 调用仍使用原完整路径。
- 关联:
apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx、apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css、apps/ai-game-creator-shell/src/features/project-workspace/。
正式素材提交不能把多文件写入或 Tauri 事件误当成一次原子动作
- 现象:图片已经落到
assets/但 manifest 没有资产,或 manifest 已追加而 project revision/草稿仍是旧值;进程在 emit 前后退出后,用户重试又得到第二份图片、第二个 asset 或重复选中。 - 原因:文件系统只保证单文件原子替换,不能让最终图片、manifest、
.agent/runtime/project-revision.json、commit ledger 和草稿跨文件物理原子;Tauri event 也没有跨崩溃 exactly-once。若先写副作用再临时生成幂等身份,或只凭目标文件存在推断成功,就无法区分未提交、已提交未回包和部分提交。 - 处理:第一次保存前冻结
commitId + idempotencyKey + eventId + requestFingerprint,在项目 write lock 内先写 prepared journal 和 before/after 摘要,再按最终图片、manifest/revision 逻辑原子更新、回读、ledger/草稿提交推进,释放锁后最后 emit。恢复只按 journal stage、精确字节摘要和 ledger 前向完成/安全回滚;矛盾状态进入 reconciliation-required。事件采用至少一次,监听方按 eventId 和 project revision 去重。 - 验证:分别在 prepared、图片安装、manifest 安装、revision 安装、ledger 提交、emit 和投递标记后强杀;确认只有唯一
canvas-<commitId>、revision 最多推进一次、源资产与血缘正确,响应丢失后返回 already-committed,矛盾 fixture 不自动重试。 - 关联:
docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md。
素材保存成功不等于迟到结果仍有权抢占当前焦点
- 现象:用户等待生成/保存时切到另一个项目、run、另一份素材草稿,或主动选择其它资源、修改搜索条件;旧请求完成后界面却切回旧画布、清空筛选并自动选中新资源。
- 原因:异步回调只检查“请求成功”或捕获的旧
isMounted/projectId,没有绑定中央状态 session、draft/intent、selection epoch 和 query epoch;manifest 投影这一数据事实又被错误地与“当前应自动聚焦”的用户意图合并处理。 - 处理:保存开始捕获
projectPath + projectId + centerKind + sessionId + draftId + intent + selectionEpoch + queryEpoch,响应时从当前 ref/store 完整复核。manifest 可以按精确项目身份更新当前上下文或后台缓存,但自动切状态、选择、滚动和聚焦必须等当前 mode 布局 ready 且全部焦点守卫仍相等。新资源被搜索/筛选隐藏时保留条件与选择,提示“新资源已保存,当前筛选条件下不可见”,只提供显式清除/定位动作。 - 验证:使用 deferred commit/layout Promise,依次在请求后切项目、切 run/overview、新开 session、改选择和改筛选;断言 manifest 只更新对应项目,新资源仍进入投影/布局,但所有失效守卫都不切中央状态、不改选择、不清查询。条件未变化且资源可见时才自动定位。
- 关联:
docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md、docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md。
派生 Debug 会让完整配置经应用状态递归进入日志
- 现象:配置和状态当前没有直接日志调用,但新增一行
debug!(?state, ...)或format!("{config:?}")就能把 JWT、后台口令、支付私钥、OSS / provider key 与 SpacetimeDB token 一次性写入日志及 OTel 留存面。 - 原因:
AppConfig、AppState与AppStateInner曾使用派生Debug;状态继续递归格式化多个含配置的 client。即使顶层状态停止下钻,SpacetimeClientConfig及SpacetimeClient的独立手写路径仍会绕过顶层防线。 - 处理:配置和聚合状态只实现封闭的手写安全摘要,不格式化任一自由字符串或含凭据的嵌套 client;
SpacetimeClientConfig独立脱敏,SpacetimeClient只复用该安全摘要。不要以默认info级别或当前零调用点代替代码约束。 - 验证:同一唯一哨兵同时填入全部凭据字段、可能带凭据的 SpacetimeDB URL 和数据库名,逐一格式化
AppConfig、AppStateInner、AppState、SpacetimeClientConfig、SpacetimeClient,断言哨兵零出现且安全运行摘要仍存在。 - 关联:
server-rs/crates/api-server/src/config.rs、server-rs/crates/api-server/src/state.rs、server-rs/crates/spacetime-client/src/active.rs、Issue #148。
Chat 生成预算字段不能按模型名猜测或失败后自动重放
- 现象:同一个 OpenAI-compatible Chat endpoint 调用 reasoning 模型时返回
Unsupported parameter: max_tokens;直接把全局请求字段改成max_completion_tokens后,旧兼容网关又可能拒绝新字段。 - 原因:内部生成预算语义与上游 wire dialect 被混在一起。Chat 当前字段是
max_completion_tokens,旧兼容层仍只接受max_tokens;Responses 和 Anthropic 又分别使用自己的字段。模型名、base URL 和OpenAiCompatible标签都不能证明 endpoint 能力,收到400后重发还可能重复计费。 - 处理:在
LlmConfig上显式声明 Chat token budget field capability;通用兼容配置默认 legacy,已验证的 VectorEngine 专用 client opt-inmax_completion_tokens,每次只发送一个字段。内部max_output_tokens与 AGC 持久指纹键maxOutputTokens保持不变。 - 验证:序列化测试分别断言 modern / legacy Chat 只出现选定字段,请求级 model override 不改变字段;Responses 继续只发
max_output_tokens,Anthropic 继续只发max_tokens;AppState 测试断言 VectorEngine client 已显式启用 modern capability。 - 关联:
server-rs/crates/platform-llm/src/lib.rs、server-rs/crates/api-server/src/state.rs、scripts/test-ve-llm.mjs、Issue #143。
Runtime 状态写失败不能发生在公开失败消息之前
- 现象:用户提交长任务后只看到运行失败或任务直接消失,聊天里一条有用消息都没有;另一些失败又同时出现 Runtime event 和 conversation 两条近似提示。
- 原因:启动确认依赖实际
turn.started,任务只入队或 Runner 在 start transition 前失败时没有公开回执;终态失败先写 task/event/state,最后才由各 main-loop 分支尽力追加 assistant。坏掉的若正是状态文件,流程会在公开消息前返回;散落的let _又无法提供幂等身份。 - 恢复补充:不能把“accepted 还没写完”等同于“用户从未投递”。用户消息已持久时,真实 resume 必须补写 accepted 后才入队;用户消息或 accepted conversation 已存在时,后续审计失败不得留下“正在启动”但永不执行的假状态,但同 message ID 的 role/content 冲突必须把 task 明确收束为
conversation-write-failed。根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。带 parent 的 Supervisor continuation 不得同时产生 Session 终态和 Runtime 事件两条公开消息;秒级时间戳下必须以 task/status message ID 共享的 run 关联摘要排序,不能用不同消息类别的计数猜测顺序。 - 验证:任务 journal 必须显示
preparing -> pending,仅有 accepted 时恢复才可提升;破坏项目 conversation 时断言任务为public-status-write-failed且无可运行 pending;破坏 Runtime state 路径时断言公开失败已经存在;重复写同一 run/status 只有一条 message ID;渲染实际 prompt 断言不包含 Runtime 公开状态;AppSurface 证明根启动/失败事件不重复,专业 Agent 启动仍可见。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs、agent/runtime_driver/task_start.rs、src/features/agent-runtime/model.ts、src/features/project-workspace/SupervisorChatOnlyView.tsx。
manifest 被旧快照写回 Pending 时不能让父 Run 抛下真实运行中的 child
- 现象:
code-prototype已有确定性 child run、running journal 和工具事件,父 Supervisor 却在几秒后以 fixed graph stalled 失败;child 随后完成代码与静态检查,但 completion gate 持续报告task=code-prototype status=pending,最后loop-budget-exhausted。 - 原因:并发 hydration 或其它旧 manifest 快照把 scheduler 已写的 running 覆盖为 pending;父 Run 把“当前不宜调度新 child”错误等同于“没有 child 需要等待”,并只以 manifest 状态判断 DAG 活性。父先终态后,真实 child 也失去正常投影窗口。
- 处理:调度与等待分离。新调度可以被派生视觉修复等门禁阻止,但当前最新活跃根 Run 下,只要 durable child 具有确定性 runId、scheduler source、正确父链接且 journal 仍处于 queued/running,父 Run 就继续等待;同一 child 的完成门可容忍 manifest pending,但仍执行正式产物、revision、静态检查与试玩证据门禁。更新根 Run、GUI/CLI、终态/确认/reconciliation child 或绑定冲突全部失败关闭。
- 验证:人工把当前 child 的 manifest 状态回写 pending,断言父 DAG 仍 in progress、父上下文可持久化为
waiting-for-manifest-tasks、child 可投影 completed;随后创建更新根 Run,断言旧 child 不再保持 DAG 活性且 completion blocker 恢复status=pending。不要靠增加 loop 次数或伪造 completed 掩盖竞态。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs。
固定关键词提示不能代替 Supervisor 选择条件任务图
- 现象:用户只说“现在没有用到任何美术资源”,Graph 就在 Supervisor 输出任何计划前自动打开美术节点;或者用户想复用现有素材,Runtime 直接按关键词预完成节点。Supervisor 无固定计划时随即
fixed-task-graph-stalled,看起来像模型不理解意图,实际上模型根本没有获得决策机会。 - 原因:同一套关键词函数同时承担 prompt hint、Graph reset、baseline 豁免和历史试玩类型继承,启发式信号越过 Supervisor 成为了控制面真相;main loop 又在 Provider 请求前优先调度 ready task。
- 验证:直接使用用户原句,断言 hint 命中但 manifest 全部保持 pending、决策前零 child、Provider request 包含路由工具;决策后只启动 code-prototype,它未完成 asset.list 时不得委派美术;再分别覆盖完整复用、真实缺口和显式重做。
既有正式产物不能同时被快车道视为已完成、被本轮 baseline 门视为未变化
- 现象:增量任务已有完整美术图集,
art-asset-plan每轮都返回零 action 和“已验证交付”,但 completion gate 每轮都报告assets/manifest.art.json(unchanged-from-run-baseline);最终 childloop-budget-exhausted,随后 Graph 和父 Run 失败。日志中没有本轮 Provider request、tool plan 或 action receipt。 - 原因:Graph reset 无差别重新打开稳定的美术 owner 节点;快车道按“当前产物有效”判断完成,owner 完成合同则按“本轮必须修改 baseline 产物”判断完成,两套语义互相冲突。增加 loop 预算、伪造版本号或机械改写 manifest 都不能消除冲突,还会引入 verification loop、字段丢失或错误复用旧主题。
- 处理:关键词和资产探测只作为 Supervisor 的 advisory context,不能直接修改 Graph。根 Run 先以
audit-existing-first持久化用户 intent;即使用户提出整体视觉重做,这也不授权强制重生成。决策后只启动code-prototype,由它在成功asset.list后提交或建立权威覆盖/缺口 delivery。Runtime 验证合同后才允许已有资产复用,或只打开精确缺口 owner;根完成门继续要求主 Agent 认领回执并完成接入、Canvas、私有回执、切片、可见使用和试玩验收。 - 验证:先断言 Supervisor 决策前零 child、固定关键词不会预完成节点,再覆盖完整复用、仅缺图集和明确重做。还要直接经过父完成门,证明合法持久 route/delivery 不再出现 art baseline gap,并证明删除切片后覆盖合同拒绝复用;旧 root、错误 fingerprint、虚构或遗漏缺口、重复委派以及 child 写入
game/**都应失败关闭。
单主 Graph 升级不能只迁移 sidecar,必须同时处理活跃旧 Run
- 现象:v1 decision/route 能迁移,但升级前正在运行的
code-prototype因 prompt 文本变化被 scheduler 判为身份冲突;同时旧 fixed-graph 的art-director / art-asset-plan仍持有 scheduler binding,可以脱离新主 Agent 继续生图。 - 原因:迁移测试只手工构造了非确定性主 Run,未经过真实 ready scheduler;资源 route 的迁移也没有自动让已启动的旧责任链失效。硬截止处理若仍只接受根的直接 child,还会把新的嵌套美术 child 留在 running,而丢失 reconciliation 投影。
- 处理:确定性主 Run 只白名单兼容已知 canonical task 文本版本,所有其它身份字段继续精确校验;旧 fixed-graph 美术 child 及沿 isolated instance 父链可证的历史后代在计划和所有非只读工具入口失败关闭,只允许当前
code-prototype经asset.list后重新委派。新美术 child 同样使用显式只读白名单,写工具只允许可证明落在assets/**的文件/patchset 与canvas.asset_generate,不能借memory.write或task.create/update修改 memory 和 manifest;会认领 delivery 并写 observed 状态的agent.run_status也不是只读。绝对硬截止显式验证根、主 Agent、delegated art child 的完整 task/binding/delegation 链。 - 验证:用真实 scheduler 恢复确定性 v1 主 Run;把旧 scheduler 美术 child 置为 running,断言
canvas.asset_generate、memory.write、task.create、task.update和agent.run_status均被拒绝;再持久化其历史game/**writeScope isolated 后代,断言恢复执行写操作仍失败且项目未变。对当前合法美术 child 同样验证 memory/manifest 零写入,再让它带在途外部生成命中硬截止,断言状态进入needs-reconciliation且 pending/batch/外部生成账本原样保留。
委派幂等与 child 写入测试不能和真实后台 worker 抢状态
- 现象:测试刚建立 static delivery,自动 parent-wake 就抢先恢复并终结父 Run,使随后同 action 重放被“当前 durable task 仍为 running”拒绝;受限美术 child 测试也可能在后台 worker 抢先终态后,让本应允许的
assets/**写入误报 verification failure。 - 原因:测试 fixture 同时手工推进 journal/manifest,又允许真实后台 future 执行同一父子 Run;单测运行时序决定谁最后写入。若为让测试通过而把 active durable task 门禁整体移动到 existing-child 分支之后,该分支仍可能补建 delivery 或投影 Ready,反而允许终态/过期父 Run 发生修复性写入。
- 处理:保留生产门禁顺序,父 Run 终态后的迟到 delivery 继续只允许 suppressed。需要断言回执、claim 与同 action 重放时,测试持有父 Agent execution lane,断言结束后释放再执行 wake;直接测试 child 写工具时持有目标 Agent lane,再把 child 持久推进到
running。所有 lane 均由 RAII 释放,不能依赖后台 future 的调度时机。 - 验证:覆盖父 Run active 时 claimed delivery 的同 action 重放返回 existing、不同 action 的重复缺口仍被拒绝、父终态后的新委派/迟到 delivery 继续失败关闭或 suppressed、合法运行中 child 只可写
assets/**。macOS 直接拼接std::env::temp_dir()的仓库安全测试还应先规范化临时根,避免/var -> /private/var被误当成项目内符号链接。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs、apps/ai-game-creator-shell/src-tauri/src/repository_context.rs。
执行锁移交给未确认启动的异步 future 会制造永久 queued
- 现象:父 Supervisor 与 Runner 一直显示运行中、heartbeat 正常,专业 Agent 已有
background_task.queued和autonomous_ready_task.scheduled,对应执行锁也被 Runner 持有,但该 child 永远没有 running journal、turn.started或后续 Runtime event;其它同批 Agent 可能已经完成。 - 原因:ready scheduler 把 per-Agent 执行锁直接 move 进 fire-and-forget Tokio future,并在 future 首次 poll 前返回成功。锁移交不是启动确认;future 未进入 start transition 时,常规 wake/recovery 又拿不到同一把锁,queued task 因而没有任何接管者。预先占用 child locks 的测试会绕过真实 spawn 路径,无法发现该缺口。
- 处理:实际 Runner 必须在项目写锁外同步完成 pending -> running 与 started journal,成功且 execution worker 已开始轮询后才移交执行锁;启动/接管失败必须持锁完成 child failed 与 manifest Graph failed 投影,再释放锁并让 parent 收到错误。scheduled 诊断审计失败不能阻断 child 启动,external client 只负责 wake Runner。UI 另以 durable
startedAt和父/子最大活动时间显示运行态时长及疑似停滞;task-record fallback 不得把最新 record 时间写成 startedAt,父 Run terminal 后也不得被 child 晚到事件继续增加时长。 - 验证:使用真实空闲 child lanes 一次调度
design-director / art-director / code-director,在有界时间内逐一断言 running/turn.started,并验证幂等重调度不新增逻辑 Run;禁止只断言 scheduled 记录、锁文件或 Runner heartbeat。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_queue.rs、apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx。
Canvas 视觉门不能在没有资产词法候选时运行整套 JavaScript 语义分析
- 现象:Supervisor 已写入
turn.started,但第一条 planning 进度长期不出现;Runner 无 Provider 连接,单个 Tokio worker 持续占满一核。对项目现场复现时,15 KiB 的经典脚本在检查一个根本未被引用的切片文件时,超过 60 秒仍未返回。 - 原因:视觉门先为每个候选切片无条件执行模块依赖分析和函数可达性分析,最后才判断图片
.src或已绑定 DOM 元素是否能指向目标资产。没有import关键字、没有目标文件名且没有已绑定图片元素时,这些全程序分析不可能产生有效视觉证据,属于纯浪费;复杂闭包与 alias 图会把浪费放大成看似 Runtime 停滞。 - 处理:仅做单向安全短路:JavaScript 原文同时没有大小写精确的
import与export字节序列时,跳过模块依赖语义分析;纯export ... from/export * from仍是模块图依赖,不能误跳过。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码\\xNN、\\uNNNN、\\u{...}、简单转义和续行;解码后仍无候选才直接判定没有绘制证据,解码不确定则保守进入 Oxc。这样必须保留 computeds\\x72c、转义资产 URL 与转义getElementById/querySelector,同时不能因 HTML 中存在某个绑定元素让所有无关 JavaScript 单元进入重分析。 - 验证:无候选现场的同一切片检查必须有界返回且保持
missing-visible-art-slice-use结论;同时覆盖相对路径、转义 URL、computed/转义属性与 DOM 方法、绑定 DOM 图片元素、含无关正则转义的脚本单元、路径大小写、纯重导出模块图、动态 import namespace 写入、未调用函数、恒假分支和真实可达drawImage,证明短路只拒绝不可能命中的输入,不扩大验收权限。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs。
ready-task 对账取消后不能让 successor 永久继承 failed Graph
- 原因:取消原 reconciliation Run 只负责安全释放 Agent 队列屏障,并不等于 manifest 任务完成;continuation 完成合同保留既有 Graph 进度,却没有区分“普通失败”和“已经人工核对、保留 cancel tombstone 的 reconciliation 取消”。
- 处理:旧 action 继续禁止重放或伪造 observation;旧 child 与父 Run 先真实终态。新 Supervisor continuation 仅扫描同 Session、同 source、同有效任务合同的历史根 Run,并要求对应 ready-task 同时存在
failed / needs-reconciliation记录、最终cancelled记录和 durable cancel tombstone,才把当前 manifest 的同一 failed 节点恢复为 pending,让 scheduler 创建新 child Run。manifest 的读取、筛选、child 证据重验和写回放在同一项目写锁内;每个 task journal 只读取一次并按 parent Run 建索引。较新的无 child Run 默认阻断旧凭证,只有其 root journal 精确证明为旧 failed Graph 在进入 scheduler 前即失败时才允许向前查找;scheduler 自身失败不得被当成该兼容场景。 - 验证:构造 reconciliation child、人工 cancel tombstone、failed manifest 和终态父 Run,证明同源 continuation 只重排该节点;并列普通 failed 节点保持 failed,完成合同继续继承原任务 SHA 与项目 baseline,旧 pending action 不恢复。追加覆盖“旧 failed Graph 未调度”的中间 Run 可以跨过,而较新的 scheduler failure 即使没有 child journal 也会阻断更老 tombstone。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs。
timeout_at 不能替代显式的预算耗尽预检
- 现象:给完美像素加端点级并发闸后,预算已经耗尽的请求仍然能拿到许可,白占一个名额继续去打几轮全账号 SpacetimeDB 扫描,直到下载那步才失败。
- 原因:
tokio::time::timeout_at会先 poll 一次内层 future 再判超时。信号量有空闲许可时acquire_owned()首次 poll 就绪,于是即使 deadline 早已过去,返回的仍是Ok(Ok(permit))而不是超时。既有acquire_editor_pixel_art_cpu_permit里那句if Instant::now() >= processing_deadline正是为此存在,新写的许可函数漏掉后被单测抓出。 - 处理:所有「先判预算、再等资源」的获取函数都必须在
timeout_at之前显式判一次Instant::now() >= deadline并直接返回超时错误;这句不是冗余防御。同理,进入排队计数之前也要先做这个预检,避免为注定失败的请求占用队列名额。 - 验证:在有空闲许可时用已过期的 deadline 调用获取函数,只断言返回
504而不是许可;504已足以证明显式预检没有被timeout_at的首次 poll 绕过。禁止在该用例里读取进程级队列 Atomic 的 before/after;相对断言同样会被并行测试插入。仅靠「信号量占满时超时」的用例发现不了这个问题。 - 关联:
server-rs/crates/api-server/src/editor_project.rs(acquire_editor_pixel_art_snap_permit、acquire_editor_pixel_art_cpu_permit)。
有界等待队列的计数递减必须写在 Drop 里
- 现象:给同步端点加「最多 N 个等待者」的保险丝时,若把计数递减写在正常返回路径上,客户端断连或超时触发会让等待中的 future 被丢弃而跳过递减;计数只增不减,最终队列永久判定为满,接口对所有人返回
503且不会自愈。 - 原因:Rust 的 async future 可以在任意 await 点被取消,取消时只保证
Drop会跑,不保证后续代码会执行。有界队列的入场与离场天然不对称。 - 处理:把递增封进一个 guard 结构体,递减放在它的
Drop实现里;递增本身用fetch_update的 CAS,不能用「先读后加」——两个线程同时读到max - 1各自加一就会越界。拿到资源后立即drop(guard)让出队列名额,不要让它跟着许可一起活到请求结束。 - 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言
504,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或--test-threads=1掩盖隔离问题。 - 关联:
server-rs/crates/api-server/src/editor_project.rs(try_enter_bounded_queue、EditorPixelArtSnapQueueGuard)。
dependency 不能复用 type 的紧凑间距或让窄层始终顶部对齐
- 现象:相邻卡片间的橙色引用只剩一个箭头,看起来像长度异常;同一个菱形 / 分叉关系中,上半组线很短而下半组线绕很远。
- 原因:type 模式的
16px紧凑行列间距不足以同时容纳 marker 安全距离和可辨认线身;分层布局若只按每层 index 从簇顶向下排,单节点层无法与多节点层的垂直中心对齐。 - 处理:dependency 自动坐标使用独立
48px列间距和40px行间距,type 继续使用16px。相关簇记录最大层行数,每层起始 y 增加(maxRows - layerRows) * dependencySlotHeight / 2的确定性偏移;平局仍用稳定资源 ID,手动坐标仍原样占位。不能通过裁短长线、偏移真实端点或压缩 Rust depth 伪造一致长度。 - 验证:纯模型锁定 type / dependency 间距隔离、四节点菱形的首尾单节点层居中、同层稳定顺序、历史手动坐标和 4096 项性能;Overlay 使用相邻 dependency 槽位证明箭头前保留可辨认线身。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts、ResourceDependencyOverlay.tsx、resourceCanvasLayoutModel.test.ts。
依赖聚类不能把聚合 task-flow 展开为资源两两边
- 现象:为了让 task-flow 的两端资源靠近,若对每个 source × target 构造边,资源多的任务流会迅速放大内存、排序工作和虚假关系;同一图输入还可能随着成员枚举顺序出现不稳定排列。
- 原因:task-flow 的业务语义是任务对的聚合流,不是资源间的完整笛卡尔依赖;dependency depth 也已经由 Rust SCC read model 权威计算,前端不能用布局边重建业务方向。
- 处理:布局分组把每条 flow 作为一个临时流节点,仅与其 source / target 成员相连;中位数扫描读取流另一端成员现有 rank 的中位值。遍历保持迭代式,扫描轮数固定,所有初始序和最终平局都以稳定资源 ID 收口。用于自动重派生的拓扑签名先按固定分类过滤并规范化稳定 ID,再生成固定大小摘要,不能把显示名、卡片大小或浏览器几何加入签名。跨分类 reference 与跨分类-only task-flow 不进入前端布局;所有 task-flow 都不进入 SVG 或画布关系说明。
producerMappingTruncated时继续只消费现有同类型精确引用,不重建 task-flow。 - 验证:纯模型覆盖多入、多出、聚合 task-flow、环、4096 链和重复输入坐标一致;Hook 覆盖仅改邻接、深度不变仍重派生自动坐标。不得把搜索后的可见集传入聚类。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts、useProjectResourceCanvasLayout.ts、resourceDependencyGraphModel.ts。
四分区 SVG 不能把跨类型业务关系当成可绘制几何
- 现象:跨分类资源位于彼此独立滚动和裁剪的 viewport;若仍绘制一条全局 SVG 路径,只会在两个分区中留下没有完整上下文的断线,滚动时还会看似随机出现或消失。同一卡片多边若都锚在中心点,也会让合法的同类型线叠成一束。
- 原因:Rust read model 的业务关系范围大于资源管理画布的展示合同;四分区视图没有跨标题栏的合法连线走廊。几何层直接遍历全部 reference edge 等于把业务真相误当成全部可视关系;单中心端口又忽略了边的稳定身份与对端顺序。
- 处理:保留 Rust 图与权威深度;布局拓扑按资源分类过滤 reference 和 task-flow 超边切片,关系说明与 SVG 则只消费同类型精确引用。相同分区内按对端坐标、稳定边 ID 为同侧精确边分配有界端口。不要通过改变端点、隐藏同类型合法精确边或生成资源笛卡尔积来换取整洁。
- 验证:同时覆盖跨分类精确引用与跨分类-only flow 不聚类 / 不绘制、全部 task-flow 零 SVG / 零画布关系说明、同侧多边端口不重合且重复输入路径一致、同类环 / 自环和 4096 项回归。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/index.tsx、ResourceDependencyOverlay.tsx、resourceCanvasLayoutModel.ts。
Linux 生产脚本门禁不能假设本地也是 GNU userland
- 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现
mv: illegal option -- T、mapfile: command not found、/usr/bin/cp//usr/bin/chmod不存在,以及.rlib明明含有.o却报告“没有可扫描成员”;安全修复计划还会把/var/folders到/private/var/folders的系统别名误判为用户符号链接。 - 原因:生产机是 Linux/GNU,而本地门禁运行在 BSD userland、Bash 3.2 和 BSD ar;测试桩硬编码 Linux 二进制路径与参数,归档解析器没有去掉 BSD 扩展成员名的尾随 NUL,路径校验也直接比较了未规范化字符串。
- 处理:维护 marker 使用同目录临时文件加 POSIX
mv -f,并在替换前拒绝所有符号链接和目录目标,避免mv -f跟随目录链接把临时文件移入链接目标;生产部署测试桩在 macOS 忠实模拟 GNUmv/ln -T的“目标不是目录”语义,并按平台选择系统工具;脚本收集服务使用 Bash 3.2 可用的while read;rlib 解析清理 BSD 成员名 NUL;计划文件只规范化系统临时目录别名,仍拒绝其下用户创建的符号链接组件。 - 验证:运行
npm run check:maintenance-page、npm run check:production-api-deploy、npm run check:server-rs-ddd、npm run test -- scripts/spacetime-repair-editor-canvas-resources.test.ts,并在 Linux CI 保留同一生产脚本语义。 - 关联:
scripts/deploy/maintenance-on.sh、scripts/check-maintenance-page.mjs、scripts/check-production-api-deploy.mjs、scripts/deploy/production-api-deploy.sh、scripts/check-module-runtime-artifact.mjs、scripts/spacetime-repair-editor-canvas-resources.mjs。
分区内部滚动不能只重测分区原点
- 现象:若滚动时只重测分区原点,线会停在旧位置或穿过标题栏;若进一步把“卡片完整位于 viewport”当作关系挂载条件,同一合法关系会在卡片刚触边时突然消失、滚回又出现,箭头也可能恰好落在 clip 外而只剩一截线。
- 原因:全局 SVG 与 section plane 不共享 transform / scroll,
getBoundingClientRect + RAF + state重建屏幕端点只能异步追赶浏览器合成层;卡片可见性又是显示裁剪状态,不是关系身份。SVG marker 贴卡或贴裁剪边界时还可能只剩主 path。 - 处理:每个 section plane 自己持有 SVG,让路径和卡片直接使用同一逻辑坐标与父级 scale / scroll;每区只以一个 Observer 和 RAF 维护逻辑 viewport。精确引用源端或目标端单独离屏时分别绘制 outgoing / incoming 边界继续线,两端离屏才隐藏;目标锚点预留固定箭头间隙,marker 使用
userSpaceOnUse且允许 overflow;自环整体外移避免箭头压卡。搜索隐藏端点仍属于业务可见性过滤,不能与 viewport 裁剪混用。 - 验证:覆盖同帧多次 scroll 只调度一次 RAF、部分离屏时 outgoing / incoming 正确切换、两端离屏隐藏、缩放后路径与卡片仍处于同一 plane、箭头可见、分区互不串线,以及每区单 observer 与卸载清理。搜索隐藏任一精确端点时整条橙线隐藏;task-flow 始终不渲染。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/ResourceDependencyOverlay.tsx、apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts。
External Editor taskId 不能当作本地 manifest taskId
- 现象:Rust read model、资源详情或 dependency 聚类中缺少本应存在的 task-flow;测试用
design-foundation之类字符串时正常,真实生成返回task-1后失败。画布不显示灰色 task-flow 是当前产品决定,不能再用是否出现虚线判断 producer 映射是否正确。 - 原因:
GameCreationAppAssetSource.taskId保存的是 External Editor 生成任务身份,命名空间与本地.agent/manifest.json的 Agent/task 身份不同;前端用taskById.get(source.taskId)会让真实画布资产全部失去 producer。 - 处理:资源依赖图的 Tauri Rust read model 从有界
.agent/agent.db读取agent.runtime.canvas.asset_generate,以assetId -> agentId映射 producer,并要求agentId存在于当前 manifest。记录缺失、多个不同有效 Agent 冲突或读取已截断时失败关闭 producer assignment、task flow 与对应cyclicTaskIds,不回退source.taskId。精确asset-reference仍只依赖 manifest 中外部 resourceId 的唯一匹配;Rust 独立返回的dependencyDepths继续作为 manifest / reference read model 权威结果,前端只过滤未知资源、负数、非整数和非安全整数,不得因 producer 截断把它整体清空。 - 验证:Rust fixture 把
source.taskId固定为task-1 / task-2,只有审计提供art-director / design-foundation后才生成 task-flow read model;移除或截断审计后该 flow 消失但橙色引用保留,合法深度仍为asset:spec=0 / asset:ui=1。前端始终断言 task-flow 零 SVG;AppSurface 使用截断生产数据形状证明深度0 / 1 / 2真实到达卡片布局,并且不会把已有自动坐标持久化成扁平布局。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs、apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts、docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md。
依赖图未就绪时不能先初始化资源布局
- 现象:首次打开 dependency 画布时所有资源短暂按深度 0 排列;Rust 图返回后连线正确,但卡片仍停留在同一列,错误自动坐标还可能已经写入 sidecar。
- 原因:资源图和布局读取独立异步启动,布局 Hook 在图未返回时使用空图资源创建 fallback;后续 reconcile 按旧合同保留全部已有坐标,真实 producer 与 dependency depth 无法纠正首次自动位置。
- 处理:dependency 模式增加按项目与资源输入隔离的图加载屏障,
ready / failed前不启动布局 Hook 的 fallback、读取、协调或保存。Rust read model 返回确定性依赖深度;已有布局只永久保留手动位置,自动位置按最终图重新派生。type 模式不受图加载影响。 - 验证:用 deferred graph Promise 断言终态前 Tauri layout read/update 调用均为 0;图就绪后首次坐标直接按最终深度生成,旧 scope 迟到结果无效,手动坐标不变且相同自动布局不增加 revision。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/index.tsx、apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts、apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs。
等价 Runtime 投影刷新不能清空资源依赖图(2026-08-10)
- 现象:专业 Agent 运行期间,资源画布中的卡片按轮询节奏整批消失并立即恢复;停止产生新的 Runtime 时间戳后闪烁减弱或消失。
- 原因:专业 Agent 轮询会重建结果数组;资源内容虽然相同,前端图读取 effect 仍因数组引用变化重新执行,并先把图和布局置空。布局位置暂时缺失时,所有资源卡都会返回
null。 - 处理:用包含项目与资源输入的语义 scope key 稳定图请求参数,等价输入不重复读取;同一项目的资源集合确实变化时,异步刷新期间保留上一个已解析图和布局,只有首次加载或切换项目才启用空图屏障。刷新失败继续展示旧快照,不能用瞬态失败清空画布。
- 验证:AppSurface 先用全新但内容相同的 Agent 结果数组 rerender,断言图读取仍只有一次且原卡片 DOM 保持连接;再增加真实资源并延迟第二次图响应,断言旧卡片在刷新窗口持续挂载,新图返回后新增卡片正常出现。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/index.tsx、apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts。
Supervisor steer 不能只有内部排队事件(2026-08-10)
- 现象:自主制作期间继续向项目总控发消息,用户消息已进入同一 Run,当前 Provider 也被中断并重新规划,但普通工作台短暂的提交状态消失后一直没有回复,直到整轮制作最终收束。
- 原因:steer 只持久化用户消息与内部
steer.queued事件;公开事件投影又明确排除steer.*。普通工作台提交后会用后端 conversation 覆盖本地消息,因此仅追加临时前端气泡也无法稳定跨刷新显示。 - 处理:根 Project Supervisor 的 steer 进入 durable
queued后,先写“正在判断、当前任务继续”的公开确认,再由独立steer-decisionLLM turn 返回自然语言回复和interruptCurrentProvider。状态询问、解释和不冲突补充默认不中断;明确停止、改向或会使在途方案过期时才允许请求中断。判定和回复按run + steer持久幂等,刷新后仍可见;判定失败时继续当前任务,并在下一安全边界应用 steer。 - 并发边界:steer 入队、Runner
runtime.steer通知都不得直接触发 Provider interrupt。Codex app-server 的判定使用独立节点,不能等待主节点 turn 锁;判定为 true 后也只能中断appliedSteerCursor < steer.sequence的旧 Provider 请求,已经消费该 steer 后启动的新请求不可被误杀。已经开始的工具和外部动作不强杀,完成 observation 后再消费 steer。 - 验证:真实 mock LLM 回归必须覆盖状态询问回复且
interruptCurrentProvider=false;持久重放只保留一条语义回复;steer 入队后旧 Provider 继续运行,判定为 true 后才中断;新规划 Provider 的 cursor 已包含该 steer 时即使旧判定为 true 也不能中断。前端同秒多条消息保持“用户补充 → 判断提示/语义回复”的关联顺序。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs、apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs、apps/ai-game-creator-shell/src/features/agent-runtime/model.ts。
Jenkins 异步备份不能用 nohup 脱离作业
- 现象:Stdb Publish 成功,上传日志只留下“已获取进程锁 / 上传已有备份 / 目标对象”,没有成功或可捕获错误;本地 tar.gz 和
uploadStatus=deferredmanifest 每次发布后继续增长。 - 原因:
nohup只忽略终端 HUP,不会移除 Jenkins/Hudson 进程 Cookie;Job 收尾可清理后台 uploader。原链路只上传当次归档,旧 deferred manifest 没有扫描重试,而files-historytimer 只处理/stdb历史文件。 - 处理:发布退出时用独立
systemd-run --collect --service-type=exectransient unit 执行--upload-deferred-dir,串行处理同库 deferred/pending 归档。启动前拒绝符号链接和非绝对路径;unit 启动失败必须保留 status、archive 和 manifest。补偿扫描不删除上传未验真的文件,也不扫描目录外路径。 - 验证:门禁必须禁止
nohup,要求命名 transient unit、--collect、Type=exec与失败后保留 status;备份测试覆盖稳定顺序、同库过滤、已上传但未清理的归档收敛、归档缺失报告与路径逃逸拒绝。现场最终核对 backup lock、manifest、transient unit/result、根盘、SpacetimeDB/API/worker/controller/Nginx 和公开端点。 - 关联:
scripts/deploy/production-stdb-publish.sh、scripts/database-backup-to-oss.mjs、scripts/check-production-ops-guardrails.mjs、scripts/check-database-backup-to-oss.mjs。
图集切片上限必须早于合并、裁剪和编码
- 现象:透明图集含大量独立碎块或噪点时,接口长时间占用 async worker;最终即使报“超过 64 个切片”,此前仍已完成全量两两合并、裁剪和 PNG 编码。
- 原因:原始连通域无上限,辅助部件合并全量扫描所有 pair,输出限制只在 platform slicer 返回后由 api-server 检查;UI 提取还绕过了该 wrapper。
- 处理:platform slicer 对全部 flood-fill 连通域设置
4096硬上限,用空间网格只查48px邻域候选;单网格最多256个组件、单 source 最多512个候选,避免拥挤网格重新退化为全量 pair。maxOutputSlices与 padding crop 总像素预算在首片 PNG 编码前拒绝。图标自动、手动和 UI 三入口统一在 2 路 CPU semaphore 与 30 秒 / 请求 deadline 保护下 prepare 出共享 RGBA + bounds 计划,不再一次返回最多 64 份 PNG。api-server 只按需编码并用容量 2 的有界管线上传,OSS 连接 / 单请求超时固定为10s / 60s;手动入口在下载最大 32 MiB 来源对象前取得独立内存 admission,同一 admission 覆盖下载、计划与上传生命周期,并在最后一次 HEAD 完成后、数据库调用前释放,排队请求、慢 OSS 或慢数据库都不能绕过内存边界。全部PUT + HEAD成功后,单个 SpacetimeDB procedure 在一个事务中批量确认对象、创建项目资源 / 账号素材并完成 cohort;resource / asset ID 按 owner + task + 序号稳定派生,重放只复用内容一致的素材,来源资源必须存在且同 owner / project;不在上传失败后留下部分数据库批次,也不在不确定结果重放后复制整批素材。 - 验证:覆盖大量独立
4×4块、超过上限的单像素噪点、65 个有效输出和既有高光 / 阴影合并样本;手动超限必须发生在首次持久化前,自动超限不得产生切片 PUT、资源或画布切片。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs、server-rs/crates/api-server/src/editor_project.rs。
Alpha 恢复失败后不能继续持久化原始后处理图
- 现象:BgFilter 返回比例漂移、损坏或低分辨率图片,provider 原图修复性回读又失败时,图标 / UI 仍可能落库透明图与切片,尺寸元数据甚至回退为
512×512。 - 原因:Alpha helper 会同时返回原后处理字节和错误;角色调用方会 source-only 早退,图标 / UI 却只写日志后继续。相同尺寸快路径还只读图片 header,没有完整解码。
- 处理:角色、图标、UI 共用 provider 原图 source-only helper;比例漂移超过
5%、原图回读、Alpha 回贴或透明图完整解码任一失败都立即返回原图、通用 warning、空切片和空sliceWarning,禁止透明图 PUT、派生资源、拆分和透明 / 切片画布层。provider 原图尺寸必须完整解码取得,不得伪造兜底值。 - 验证:覆盖错比例 Alpha、缺失 provider 原图、合法 PNG header 但截断正文;结构断言 source-only helper 不含任何透明持久化、切片或多图层完成调用。
- 关联:
server-rs/crates/api-server/src/editor_project.rs、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
工具 JSON Schema 的条件约束必须覆盖运行时默认值
- 现象:LLM 按工具 schema 生成的参数可以通过结构约束,但参数补默认值后被运行时校验拒绝,白白消耗一次工具修复轮次。例如固定
gpt-image-2的 UI 工具仍暴露0.5K,或视频调用省略model时 schema 允许1080p,运行时却默认成seedance2.0-fast后拒绝。 - 原因:通用枚举 schema 被固定模型工具直接复用;JSON Schema 的
if又用required: ["model"]排除了字段缺失场景,而 Serde 默认值只在 schema 校验之后生效。description 只能提示 LLM,不能替代enum/if/then的结构约束。 - 处理:固定模型工具使用与该模型能力一致的专用枚举;可切换模型的图片工具在对象层复用共享
model + image_size条件约束。条件字段有运行时默认值时,省略字段必须落入默认模型对应的 schema 分支:默认 nanobanana2 的图片工具只在显式选择gpt-image-2时收紧尺寸,所以条件保留required: ["model"];默认 fast 的视频工具则利用字段缺失时properties.model.const条件成立的语义,不额外要求model存在。运行时校验仍保留为最终防线。 - 验证:锁定
generate-ui-design.image_size = ["1K", "2K"],三个可切换图片模型的工具都接入共享gpt-image-2 -> image_size = ["1K", "2K"]条件,以及视频 fast 条件没有内层required、其then.resolution = ["480p", "720p"];同时保留运行时拒绝gpt-image-2 + 0.5K与seedance2.0-fast + 1080p的测试。 - 关联:
server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs、server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs、server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs、docs/【编辑器】画布Agent对话面板-2026-07-03.md。
重复成功的 agent.message 不能被当成新的 Runtime 进展
- 现象:专业 Agent 已把一条定向消息写入目标 Session,却在后续 planning 中反复发送相同正文;目标会话看起来没有重复消息,但 Provider 请求持续增长,run 可能长期不返回自身终态回执。
- 原因:conversation 层的 messageId 幂等只能阻止重复落盘。若每个新 Runtime action 的
status=ok都进入上下文进展指纹,相同 durable no-op 会不断刷新 6 轮停滞窗口;只检查目标会话条数无法证明 action loop 已有界收束。 - 处理:消息语义键必须包含来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256;conversation message、
conversation.message和agent.runtime.agent.message各自 exactly-once。重复调用继续完整记录自己的 action/observation/receipt,但私有 observation 固定返回messageAppended=false,ContextWindowTracker 只忽略这一精确 no-op,不能忽略不同正文的新消息。专业 Agent prompt 同时明确中途消息不能替代自身 final response。 - 验证:
background_agent_runtime_bounds_duplicate_agent_message_livelock必须真实驱动 6 个相同指纹、不同 actionId 的消息动作,证明 action/observation/receipt 各 6 条,目标消息和两类消息审计各 1 条,后 5 次不算进展,第 6 轮保留in_progress计划并进入budget-exhausted,没有第 7 次 Provider 请求、context compaction 或 completed。另保留agent_runtime_context_window_counts_distinct_agent_message_bodies,防止把真正不同的新消息误压成 no-op。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent.rs、apps/ai-game-creator-shell/src-tauri/src/project.rs、apps/ai-game-creator-shell/src-tauri/src/tests.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
Swarm E2E 的隔离 AppData 不能建在正式 AppData 里面
- 现象:真实 suite 自称使用隔离配置,但一次性 AppData 出现在正式 AppData 子目录;源目录 watcher、配置副本计数和清理归属变得含糊,Runner 还可能把临时 endpoint 或运行态写进正式目录树。
- 原因:把
mkdtemp前缀拼在 source config dir 内,只隔离了文件名,没有隔离目录所有权;source-dir guard 无法区分 suite 自己的合法子目录写入与污染,失败清理也可能触碰正式目录边界。 - 处理:需要保护正式配置的 suite 一律在
dirname(realConfigDir)下创建 sentinel 管理的 sibling AppData,并要求 realpath 后与源目录同父、互不包含。配置只使用私有副本或受控 hardlink/overlay,启动 CLI/Runner 全部指向 sibling;清理前核对 sentinel、源配置 inode/hash/link count、source-dir 前缀事件、正式 endpoint 身份和正式 CLI 调用计数,随后只删除拥有明确 token 的临时目录。 - 验证:真实报告必须同时满足
isolatedAppDataUsed=true、sourceAppDataDirectoryUntouched=true、sourceRunnerEndpointUnchanged=true、formalConfigCliCallCount=0、配置副本校验和AppDataCleanupPerformed=true;项目选择--keep-project时也不能改变 AppData 自动清理。 - 关联:
apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
异步 Runtime 测试不能把 child idle 当成终态结果已发布
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
- 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
- 验证:
isolated_agents_with_same_template_run_independently_and_join_once最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails和background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies同样必须经过该 fence 后再断言turn.failed或agent.runtime.completed审计。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/tests.rs、apps/ai-game-creator-shell/src-tauri/src/agent.rs。
CI root 环境不能用文件只读权限注入写失败
- 现象:本地测试把 conversation 文件设为 readonly 后能稳定得到写入失败,Gitea Actions 中同一断言却发现写入成功并继续执行任务。
- 原因:隔离 job 内测试进程可能以 root 运行;root 不受普通 owner write bit 的同等限制,
set_readonly(true)不是跨 runner 身份的确定性故障注入。 - 处理:需要覆盖写失败恢复时使用仅在
cfg(test)生效、一次性消费并限定写入阶段的 marker;生产路径仍走真实持久化函数。测试同时断言 marker 已消费、失败前数据未落盘和恢复后 exactly-once,不依赖 chmod、固定 sleep 或 runner 用户身份。 - 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/project.rs、apps/ai-game-creator-shell/src-tauri/src/agent.rs、apps/ai-game-creator-shell/src-tauri/src/tests.rs。
Ubuntu 容器不能把 chromium-browser 的 Snap 占位包当成 CI 浏览器
- 现象:AI 游戏创作壳的 1132 条 Rust 测试全部通过,尾部
agent-run:smoke却以spawn google-chrome ENOENT失败;直接给 Ubuntu 24.04 job 安装chromium-browser仍拿不到可执行浏览器。 - 原因:Ubuntu 24.04 仓库里的
chromium-browser是 Snap 过渡包,普通 Docker job 没有 snapd 宿主能力;固定 job image 也不预装 Google Chrome。脚本回退到命令名google-chrome后只能在本机通过,在干净 Runner 中必然 ENOENT。 - 处理:Native job 通过 Google 官方签名 APT 源安装
google-chrome-stable,安装后先执行google-chrome --version;smoke 继续真实启动 headless 浏览器验证 DOM / canvas,不允许因 CI 缺浏览器而跳过或降级为静态 HTTP 检查。 - 验证:固定 Ubuntu 24.04 job image 内先确认
apt-cache policy chromium-browser仅为 Snap 占位,再安装官方签名包并运行google-chrome --version;Gitea Native job 最终必须在 1132 passed / 5 ignored 后继续通过agent-run:smoke。 - 关联:
.gitea/workflows/project-ci.yml、apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs。
PTY 测试不能假设输入回显与后续输出必然分行
- 现象:PTY 环境隔离用例偶发得到
你好BRIDGE_ENV:,而不是独立的你好与BRIDGE_ENV:两行;真实私有环境变量并未泄漏,但整行相等断言失败。 - 原因:canonical PTY 的输入回显和目标进程后续输出存在合法调度竞争,读取边界不等于逻辑行边界,回显可能与紧随其后的固定标记合并。
- 处理:对不含秘密的固定标记按语义边界断言,例如要求某行以标记结尾;敏感值仍必须在完整 transcript 和公共持久面执行严格零命中扫描,不能借此放宽泄漏门禁。
- 验证:
process_session_pty_uses_private_environment_and_redacts_public_records对BRIDGE_ENV:使用行尾匹配,并保留真实私有环境变量、stdin 正文与公共记录泄漏扫描。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/process_session.rs、apps/ai-game-creator-shell/src-tauri/src/command_output.rs。
自主 Swarm 验收不能把父 project.verify 当成意外确认动作
- 现象:两个专业 Agent 已完成初始交付,Supervisor 在语义 repair 前合法执行项目宿主验证,但 E2E harness 把所有父 run pending action 一律拒绝,导致真实协作链在业务逻辑正常时提前失败。
- 原因:验收器把“repair 前不允许父 Agent 绕过专业工作”错误实现成“父 run 不能出现任何确认动作”,混淆了 Supervisor 自己的
project.verify与会改变专业交付/文件的意外动作。 - 处理:确认过滤器必须按 owning run 和 tool 精确判断。repair 前允许当前父 run 的
project.verify,仍拒绝其它未列入场景合同的父 pending action;专业 Agent 的修改和验证继续按各自 run、policy 和预期确认集合处理。允许确认不等于通过验收,最终仍由 host oracle、最新 revision verification、delivery/claim/repair 和唯一回复共同裁决。 - 验证:自主 suite 必须出现有效
hostVerificationPassed=true,同时保持恰好 2 个初始 + 1 个 repair delivery、父计划完成、意外 pending 为 0、Runner 强杀恢复和唯一 Supervisor assistant;若放宽后出现额外父写动作,场景必须失败而不是吞掉。 - 关联:
apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
Provider 全成功的真实报告不能证明显式重试可用
- 现象:真实 Swarm 报告显示全部 Provider lifecycle completed,E2E 的 retry validator 也没有报错,于是文档把“支持瞬态重试”一并写成已真实验收。
- 原因:validator 只在实际出现 failed lifecycle 时校验 retry audit;
failed=0 / retry=0会自然通过。随机等待外部网络故障既不可重复,也无法在故障和重试之间证明副作用仍为 0。 - 处理:为重试单独建立 fail-first loopback proxy。在正式 AppData 同级创建 sentinel 管理的一次性目录,只覆盖其中一个目标 Agent 的 base URL 和重试配置;首个 POST 在正文进入 upstream 前断线,第二个请求由 forwarding gate 暂停。gate 内交叉检查 failed lifecycle、retry audit、request slot/identity、action、pending、receipt、delivery、claim、assistant、project revision 和目标产物,再显式放行真实 Provider。代理不能记录 URL、headers 或正文,不能跟随 redirect,必须可幂等清理;启动 CLI/Runner 时同时设置合并后的
NO_PROXY / no_proxy并显式加入 loopback,不能假设开发机已正确配置代理绕过;source-dir guard 禁止本 suite 前缀进入源目录,配置和 endpoint 身份保持只读并逐字复核。不要用源目录 mtime/ctime 归因,正式 Runner heartbeat 会并发改变它。完整链在 checkpoint 后失败时,partial report 也要保留已取得的 identity、slot 和零副作用证据,不能退回模板默认值。 - 验证:
npm run test -- apps/ai-game-creator-shell/tests/llmTransientFaultProxy.test.ts覆盖故障、暂停、base path、流式转发、fallback、隐私和清理;npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e -- --config-dir <AppData>必须得到恰好 1 failed/1 retry、重试前副作用全 0,并继续通过完整 Swarm/Runner 恢复和零泄漏门禁。 - 关联:
apps/ai-game-creator-shell/scripts/llm-transient-fault-proxy.mjs、apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
Agent 真实验收的阶段等待必须同步观察 Runtime 终态
- 现象:真实 Provider 已因 transport、格式修复或其它不可恢复错误把 task/Runtime 写成 failed,专项验收仍在等待某个 pending action、observation 或 receipt,直到 30 分钟总超时才返回。
- 原因:阶段等待只轮询“想看到的成功证据”,没有同时读取 owning Agent/run 的最新 task 与 Runtime phase;外部错误发生在该证据之前时,目标条件永远不会出现。
- 处理:所有分钟级阶段等待都要在每轮先检查 owning task 的 failed/cancelled/budget-exhausted,以及 Runtime 的 needs-reconciliation;命中后立即抛出带阶段前缀的结构化错误。正常 pause 必须保留为可恢复状态,不能被 fail-fast 当失败;Runner 强杀后的 paused 稳定窗口继续按签名零推进单独验证。
- 验证:用正式
goal-runtime观察 Provider repair transport failure,确认部分报告立即保留成功/repair 协议计数、生命周期闭合和零泄漏证据;随后完整复跑仍能通过 Goal edit/pause/Runner kill/resume/finalization,证明 fail-fast 未破坏正常恢复路径。 - 关联:
apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
图片生成的 K 档不能靠回图后缩放实现
- 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。
- 原因:前端占位尺寸、api-server 的模型尺寸映射和 VectorEngine provider 合法尺寸各自维护;同时通用交付恢复与角色去背景恢复会直接缩放整张回图,掩盖了上游请求尺寸错误。
- 处理:
model + imageSize + aspectRatio必须先解析为 provider 可直接生成的真实尺寸,前端占位和后端请求使用同一矩阵。带新尺寸字段的用户生成不执行回图后交付放大;角色、图标和 UI 的去背景服务若降采样,只缩放 alpha 蒙版并应用回模型原始 K 档 RGB。 - 验证:覆盖 nanobanana2 / gpt-image-2 的比例与 K 档尺寸矩阵、VectorEngine 最终请求体、普通图片 / 角色 / 图标 / UI 占位,以及低分辨率去背景结果只贡献 alpha、不贡献被放大的 RGB。
- 关联:
src/components/image-editor/ImageCanvasGenerationModel.ts、server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/platform-image/src/vector_engine/request.rs。
多产物任务的中间原图不能只落资源库
- 现象:角色形象、图标 spritesheet 或 UI 素材提取正常成功后,画布只有透明结果,用户无法直接对照和复用 provider 原图。
- 原因:后端虽然为追溯和后处理失败恢复持久化了 provider 原图,但完成画布只写入透明主结果和拆分素材,把“资源已保存”错误等同为“用户已拿到原图”。
- 处理:provider 原图始终写入 OSS、项目资源和账号素材库;三类任务正常成功时都同时落透明主结果与右侧 provider 原图,生成器
generatedLayerId仍指向透明主结果,图标 / UI 拆分素材从原图右侧继续排列。仅当透明处理最终失败时,才用 provider 原图作为唯一主图完成占位。 - 验证:覆盖三类任务正常成功都落透明结果与原图、原图位于透明图右侧、
generatedLayerId指向透明图、图标 / UI 拆分素材位于原图右侧,以及透明处理失败仍由原图单独完成占位。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
Vite 源码 CSS 清理插件必须早于 Tailwind 执行
- 现象:生产构建通过,但本地 dev 打开主站后全页白屏,
/src/index.css返回 500,Vite 报Unknown word updateStyle。 - 原因:自定义 CSS 插件使用
enforce: 'post',在 Tailwind/Vite 已把 CSS 转为包含updateStyleimport 的 JavaScript 模块后,仍调用postcss.parse。 - 处理:需要改写原始 CSS 的 transform 固定使用
enforce: 'pre';最终构建产物清理继续放在generateBundle,不要混用两个阶段的输入格式。 - 验证:真实启动
npm run dev后请求/src/index.css必须返回 200,并在浏览器确认#root已挂载且控制台无 CSS transform 错误。 - 关联:
vite.config.ts、scripts/vite-retired-css-plugin.test.ts。
phase 上报的业务拒绝与传输失败不能共用字符串错误
- 现象:provider 已经返回并保存原图,worker 上报
processing时一次断连或超时就直接把任务判为失败;或者为了规避误杀而重试所有错误,导致 stale lease 的旧 worker 继续执行后处理。 - 原因:phase procedure 的 lease / fencing 业务拒绝与 SDK 建连、断连、超时错误被压成同一种字符串错误,调用方无法可靠决定是否重试;按中文或 SDK 文案匹配会在错误文本变化后失效。
- 处理:procedure 返回结构化
LeaseFencingRejected/OtherRejected,typed client 再把模块拒绝与 RPC 错误分开。LeaseFencingRejected立即终止,OtherRejected以及 SDK 的Procedure/Runtime错误不重试;只有Build/ConnectDropped/Timeout在同一 job attempt 内重试一次。编辑器 job 固定max_attempts=1,第二次传输失败后进入failed,不回pending、不重新调用 provider。不得让 phase 上报错误落入“后处理失败保留原图”的降级分支。 - 验证:分别覆盖 lease / fencing 拒绝、其它拒绝、建连、断连、超时和第二次失败,确认最多调用两次;同时断言角色、图标和 UI 的原图降级只包住透明背景处理,不包住 phase 上报。
- 关联:
server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/spacetime-client/src/external_generation.rs、server-rs/crates/api-server/src/editor_project.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
禁止 Data URL 持久化时不要漏掉异步任务 JSON
- 现象:工程、素材、图层和元数据都已禁止 Data URL 后,服务器仍在生成高峰出现 SpacetimeDB / api-server 内存急剧膨胀甚至 OOM;读取少量正式生成任务也会造成远大于响应体的瞬时内存增长。
- 原因:同步接口 worker 化时把原请求整体序列化到
external_generation_job.request_payload_json,而前端又把已有objectKey下载成 Data URL 提交。任务表也是正式持久化边界;列表 procedure 若先收集完整任务行再截断,还会把 request/result 大字段在 SpacetimeDB、SDK mapper 和 BFF 多次持有。 - 处理:先在事故涉及的编辑器持久任务 JSON 上由 api-server 与 SpacetimeDB 两层递归拒绝
data:/blob:并限制字节数;已有媒体传objectKey/resourceId/assetId,本地派生图先用强唯一 key 上传。其它玩法若仍以 Data URL 作为正式请求契约,必须先资源化,不能直接扩大门禁造成玩法回归。列表、详情和 acknowledge 只走无 payload 的摘要投影,ack 不能为了同步旧字段重写大任务行;摘要错误文本也必须清除内联媒体并设硬上限,列表只能维护有界 top-N,不能先收集 owner 全量历史再截断。历史只通过迁移操作员的 dry-run + B-tree cursor 分批 procedure 压缩editor-canvas终态任务,cursor 选择读取量必须受 limit 约束,绝不全表扫描、绝不处理 pending / running;dry-run 后 apply 同一批时保持输入 cursor 不变,最后一批即使has_more=false只要仍有命中也必须 apply,只有 apply 成功后才推进到返回 cursor。SpacetimeDB CLI 2.5 的Option<T>非空参数必须使用 SATS sum 编码;维护脚本要统一编码cursor_job_id、owner_user_id和completed_before_micros,否则首批空 cursor 可运行,但第二批或带截止时间的调用会在写入前被拒绝。 - 发布门禁:生产发布入口必须固定
--delete-data=never与 scoped--yes=migrate,break-clients,普通 Jenkins 参数不得暴露清库开关;需要删数据的 schema 冲突必须直接阻断并重新检查 artifact/schema,不能靠裸--yes放行。 - 验证:构造嵌套 Data URL、Blob URL 和超限 JSON 确认入队失败;检查正式 UI procedure / client record 不含 request/result payload;用 dry-run 和 apply 测试确认活动任务不变、终态普通提示词保留且内联媒体被替换;至少带一次非空
--cursor-job-id与--completed-before-micros验证 CLI Option 编码,而不是只测首批空 cursor。 - 关联:
server-rs/crates/api-server/src/editor_generation_queue.rs、server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/api-server/src/external_generation.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
React 测试因内部状态或实现细节正常重构就碎
- 现象:修改组件结构、按钮排序、图标库 class、提示文案或 hook 内部状态名后,React 测试大量失败,但真实用户流程和对外契约没有变化。
- 原因:测试把
data-testid仪表盘、textContent拼接状态、完整对象 / 数组顺序、图标 class 或长文案当成契约;这些断言绑定的是实现形状,不是用户行为或稳定边界。 - 处理:按
React 组件测试准则重写到更稳定的层级。用户流程测试断言 role / label / URL / 弹窗 / callback;hook 逻辑用renderHook直接验证公开返回契约;DTO / payload 使用关键字段或expect.objectContaining(...)。只有产品明确要求的可访问语义、固定顺序或渲染边界才保留精确断言。 - 验证:运行触达文件的定向
vitest,必要时追加npm run typecheck、npm run check:encoding和git diff --check。 - 关联:
docs/technical/【前端测试】React组件测试准则-2026-06-26.md、src/components/image-editor/useCanvasGenerationDialogs.test.tsx、src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx。
Rust 并行测试不要在 await 跨度内修改进程环境变量
- 现象:单独运行的异步测试稳定通过,默认并行运行整个 crate 时却看到临时目录多出其它测试的文件、文件对被拆散,或目录清理与并发写入互相竞争;Gitea Backend CI 可能表现为日志数量断言偶发增加。
- 原因:
std::env::set_var/remove_var修改整个测试进程,不属于当前 async task。测试在await前设置目录、结束后恢复时,同一 test binary 的其它用例会在中间窗口读取该值;只锁修改环境变量的测试也无效,除非所有间接读取方都参与同一把锁。 - 处理:文件、队列、缓存等副作用目录进入实例配置,在构造时一次性解析环境默认值,并允许测试显式注入唯一临时目录。不要靠
--test-threads=1、固定 sleep 或只过滤自己的文件名掩盖错误路由;纯环境解析测试只有在全部相关读写都封闭于同一OnceLock<Mutex<()>>时才使用全局锁。 - 验证:先精确运行目标用例,再以默认并行度重复运行完整 crate;失败类测试同时执行时,各实例目录只能包含自己的输入 / 输出日志,测试结束后临时目录必须清理。
- 关联:
server-rs/crates/platform-llm/src/lib.rs、server-rs/crates/api-server/src/creation_agent_llm_turn.rs、server-rs/crates/api-server/src/custom_world_foundation_draft.rs。
带 objectKey 的画布图片测试要等待换签后可见
- 现象:测试点击“添加素材”后,图层状态已经写入,但立即用
getByAltText('画布图片:...')偶发或稳定找不到图片;前一张图可能通过,紧接着添加的第二张失败。 - 原因:带
objectKey的画布图片通过useResolvedAssetReadUrl异步获取签名 URL,resolvedUrl就绪前不会渲染带alt的<img>。user.click只等待点击交互完成,不等待 effect 内的换签 Promise;前一张图在后续操作期间出现只是调度时机,不是同步契约。 - 处理:每次点击添加后分别用
await screen.findByAltText(...)等待对应图片可见,再执行依赖该图层的下一步操作;不要用固定 sleep,也不要只等待最后一张图而让前面的断言依赖偶然调度。完整前端回归并行负载较高时,可只对明确跨越换签 Promise 的目标查询设置局部、有界的5_000ms超时,不要放宽 Testing Library 全局超时。 - 验证:先精确运行目标用例并连续重复,再运行所在测试文件和完整前端测试;删除场景仍要保留 A/B 都消失、两个删除调用和撤销不恢复已删除素材的断言。
- 关联:
src/hooks/useResolvedAssetReadUrl.ts、src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx。
图片画布素材库删除要匹配 sourceResourceId
- 现象:素材库中删除了已经生成并进入素材库的资源,但画布上对应图层仍然存在,刷新后还可能从已保存布局里恢复。
- 原因:生成素材进入账号级素材库时可能通过
editor_asset.sourceResourceId指向原项目资源;如果前端素材库映射和级联删除只比较sourceAssetId、assetObjectId、objectKey或src,就会漏掉只靠项目资源 ID 关联的历史 / 后端生成图层。 - 处理:
EditorAsset必须保留sourceResourceId;从素材库添加到画布时继续写入图层;删除素材时同时比较layer.resourceId/layer.sourceResourceId与asset.sourceResourceId。 - 验证:
ImageCanvasEditorModel.test.ts覆盖素材库 source resource 保留,useImageCanvasAssetCanvasBridge.test.tsx覆盖资源 ID 级联清理,ImageCanvasEditorAssetsIntegration.test.tsx覆盖删除后保存的新 layout 不再包含被删图层。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/useImageCanvasAssetCanvasBridge.ts、src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx。
图片画布素材选择有效性不要绑定搜索与折叠可见性
- 现象:批量选择多个素材后,搜索、折叠文件夹或展开文件夹会让已选数量下降、Shift 范围锚点丢失,后续批量下载或删除遗漏此前已选素材。
- 原因:搜索结果和文件夹展开状态只描述当前 UI 可见范围,不描述素材是否仍然有效;用
visibleAssetIdsreconcile 全局选择会把暂时隐藏误判为素材失效。 - 处理:由唯一
useImageCanvasAssetSelection持有选择集合、范围锚点、框选和全部选择 mutation;全局选择只按全部selectableAssetIds清理真正删除、上传未完成、上传失败或媒体地址无效的 ID。visibleAssetIds只作为单项切换、Shift 可见区间和当前结果全选 / 取消全选的动作入参,批量下载与删除消费 hook 输出的完整selectedAssets;删除中包含当前未显示选择时,必须明确展示全部数量和未显示数量并二次确认。 - 验证:模型测试覆盖隐藏选择保留、可见范围增量和真正失效 ID 清理;图片画布素材集成测试覆盖搜索、折叠 / 展开后选中数量稳定及当前可见全选不影响隐藏选择。
- 关联:
src/components/image-editor/useImageCanvasAssetSelection.ts、src/components/image-editor/useImageCanvasAssetLibrary.ts、src/components/image-editor/ImageCanvasSidebarView.tsx、src/components/image-editor/ImageCanvasEditorView.tsx。
后台素材查询不要用 SQL 直查 editor_asset
-
后台审核与素材查询的图片预览若要显示像素化原图,应由后台 read model 在
sourceResourceId关联的项目资源上预先透传原图媒体引用,再复用管理员换签;不要让 admin-web 直接查询私有editor_project_resource。 -
现象:后台“素材查询”报
HTTP 400:no such table: editor_asset. If the table exists, it may be marked private.。 -
原因:
editor_asset是私有 SpacetimeDB 表,后台 SQL / schema HTTP 查询面看不到私有表;即使 api-server 有后台身份,也不能把私有表当 Dashboard SQL 表直接查。 -
处理:后台素材查询走
spacetime-module内的admin_list_editor_assets_and_returnprocedure,由spacetime-clienttyped facade 调用后再在api-server映射作者展示名和陶泥号。新增类似后台只读能力时,优先补窄 procedure / read model,不要复用fetch_admin_dashboard_rows直查私有源表。 -
验证:
cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema。 -
关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/spacetime-client/src/editor_project.rs、server-rs/crates/api-server/src/admin.rs。
后台素材查询要在分页前归一用户、游标和派生任务
- 现象:输入
SY-*陶泥号查不到已有素材;无筛选时只显示 80 个任务且没有“读取更多”;手动重拆图集后,素材 N被单独当成零成本父任务,原图集又显示成另一组。 - 原因:
editor_asset.owner_user_id保存内部user_id,不保存公开陶泥号;作者陶泥号在 procedure 返回后才映射,不能直接参与素材表过滤。spacetime-client的统一时间文本是seconds.microsZ,若 cursor 只按 RFC3339 解析,第 80 个任务无法生成nextCursor。手动图集拆分使用独立editor-atlas-split-*任务号,但切片项目资源通过source_resource_id指回原图集资源,只按当前task_id分组会割裂同一条素材生产链。 - 处理:
api-server在调用素材 procedure 前把“用户 ID / 陶泥号”字段或精确SY-*keyword 解析成内部user_id;游标解析同时接受整数微秒、统一seconds.microsZ和 RFC3339,编码失败必须返回服务错误,不能伪装成末页,客户端传入的非法 cursor 必须返回400,不能静默回到第一页。手动拆分素材保留真实task_id,通过editor_asset_group_source_provenance按原 resource、asset object 或 Object Key 查找服务端生成账号素材的可信来源任务,再把来源任务写入editor_asset.group_task_id;跨项目复用时同时携带当前和原始 source resource,首次历史回溯命中后补写 provenance,后续不再扫描账号全量素材。没有可信来源的新批次显式以自己的拆分任务作为group_task_id,不得落回可读取用户资源元数据的 legacy 分支。每片保存group_task_expected_asset_count,全部切片落库后再写不可逆的editor_asset_group_cohort完成事实;read model 依据完成事实判断批次资格,不用当前剩余行数猜测初始是否完整,因此用户后来删除切片不会让批次脱组。部分失败批次没有完成事实,始终保留为独立拆分任务。历史行兼容沿资源链回溯,删除项目时只固化直接引用待删资源且尚未固化的历史切片;固化始终保存真实来源任务,有界展示分组不得反写覆盖来源。read model 只让同根任务的一个完整拆分批次并入原任务,重复批次按真实拆分任务独立分页。带 owner 条件时先走by_editor_asset_owner_user_id,不要为单用户查询扫描全站素材。后台筛选输入使用防抖并取消旧 transport;手动刷新同一查询失败时保留已有结果和游标,筛选已变化时不展示旧查询结果。 - 验证:API 测试覆盖陶泥号组合条件、未知陶泥号、可信来源归组、
seconds.microsZ/ 极值游标和真实 / 归组 Task ID;SpacetimeDB 测试覆盖资源删除后稳定归组、部分失败批次不抢占根任务、重复拆分有界无丢失和 owner 索引分支;后台页面测试覆盖逐字输入防抖、请求取消、刷新失败保留结果、筛选请求乱序、旧分页响应失效和“用户 ID / 陶泥号”请求。再运行cargo test -p api-server admin_editor_asset --manifest-path server-rs/Cargo.toml、cargo test -p spacetime-module admin_editor_asset --manifest-path server-rs/Cargo.toml和npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx。 - 关联:
apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx、server-rs/crates/api-server/src/admin.rs、server-rs/crates/spacetime-module/src/editor_project_storage.rs。
后台素材查询与精选审核缩略图不要在首次挂载时全量换签
- 现象:后台“素材查询”或“精选审核”首批缩略图正常,继续向下滚动、读取更多或一次加载较多审核项后长期显示占位图;api-server journald 中已到达的
/admin/api/assets/read-url可能全部是200。 - 原因:列表一次挂载 80 条私有素材时,每个缩略图同时换签,会在同秒突发请求。production Nginx 的
genarrative_admin_rps为30r/s burst=16,超出部分在进入 api-server 前已返回429,因此仅查 api-server 日志会漏掉失败请求。 - 处理:素材查询与精选审核共用缩略图和预览组件;缩略图使用
IntersectionObserver在进入视口附近时再调用管理端换签;对429使用有上限的退避重试,并在条目卸载后停止更新状态和安排重试。无objectKey的绝对 OSS generated 地址先提取 legacy path 再换签。不得为单页突发放大 Nginx 通用管理端限流,也不得在单次限流失败后永久保留无图占位。 - 验证:前端定向测试覆盖两页共用换签组件、首屏外的后续行进入可见区后才换签、“读取更多”追加行可继续显示缩略图、
429后有限重试恢复、卸载后不再重试、绝对 OSS 地址换签和点击缩略图打开媒体预览;真实浏览器滚动验收时同时核对 Nginx access/error log、api-server journald 和 Network 面板,不以单一日志面判定成功。 - 关联:
apps/admin-web/src/components/AdminEditorAssetMedia.tsx、apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx、apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx及对应测试。
陶泥儿精选重复先查同源同媒体画布副本
- 现象:每次从项目素材中把同一个生成素材拖到画布上,
陶泥儿精选都多出一张看起来相同的素材。 - 原因:素材拖入画布会为图层实例准备
editor_project_resource;如果该素材本来带sourceResourceId指向原始生成资源,而新资源仍按普通 generated 资源公开,精选就会把原件和每次拖拽产生的同源同媒体副本都展示出来。 - 处理:创建项目资源时保留
source_resource_id,并在同项目已有同源同媒体资源时复用已有 resource;确需创建同源同媒体副本时默认public_showcase_enabled = false。公开精选读取和前端精选模型都跳过sourceResourceId指回同一媒体原件的副本,但不要按图片地址全局去重,避免不同生成步骤共享占位图时被误合并。 - 验证:
creationShowcaseModel.test.ts覆盖同源同媒体副本只展示原件;ImageCanvasEditorAssetsIntegration.test.tsx覆盖拖拽生成素材到画布时继续提交sourceResourceId;cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml确认后端资源复用 / 精选过滤逻辑可编译。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、src/components/creation-home/creationShowcaseModel.ts、src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx。
陶泥儿精选作者丢失先查公开作者展示字段
- 现象:
/creation的陶泥儿精选卡片和预览弹窗中,素材下方作者名消失、只显示占位,或错误显示内部用户 ID。 - 原因:精选数据源来自公开
editor_project_resource快照;如果 SpacetimeDB read model、spacetime-clientmapper 或api-serverpayload 任一层漏传authorDisplayName/display_name或authorPublicUserCode/ 陶泥号,前端没有可展示的公开作者字段。owner_user_id/ownerUserId/user_id是内部归属字段,不是公开作者名兜底。 - 处理:
EditorProjectResourceSnapshot、EditorProjectResourceRecord和EditorProjectResourcePayload需要一路保留公开作者展示字段;前端creationShowcaseModel优先显示authorDisplayName/display_name,没有展示名时显示authorPublicUserCode/ 陶泥号,绝不能兜底到内部ownerUserId/user_id。 - 验证:
creationShowcaseModel.test.ts覆盖展示名优先、陶泥号兜底和内部 owner/user id 不展示;editorProjectClient.test.ts覆盖公开精选接口客户端保留公开作者字段;若改动 SpacetimeDB read model,再运行npm run spacetime:generate、cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml和npm run check:spacetime-schema。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/spacetime-client/src/mapper/editor_project.rs、server-rs/crates/api-server/src/editor_project.rs、src/components/creation-home/creationShowcaseModel.ts。
陶泥儿精选顺序分列不要用 multi-column 或共享 Grid 行高
- 现象:
/creation桌面端主内容区明明能放下三张卡,首行却只出现一到两张,后续素材提前回到左侧下一段;活动卡与普通素材高度差较大时尤其明显。 - 原因:
column-count按纵向文章列流入并平衡,三张卡可排成2 + 1 + 0列;标准 CSS Grid 虽会横向先填三张,但整行共用最高卡行轨,短卡下会留下高度差。align-items: start只是不拉伸短卡,不能消除行轨空白;grid-auto-flow: dense也不能填单个网格项内的剩余高度。 - 处理:保留平面 DOM 和
.creation-landing__asset-waterfall旧类名,按当前列数将第index张显式放入index % columns列;三列时第 1/2/3 张分别进入三列,第 4/5/6 张再分别接到三列下方。列数以容器实际宽度和 288px 首选最小列宽计算,不以 viewport 硬切;先设目标卡宽再测真实卡高,列内用 computed gap 紧凑堆叠。 - 动态边界:只有当容器宽度、卡数和所有卡高有效时才进入 absolute Masonry ready;否则保留 Grid fallback。
ResizeObserver + requestAnimationFrame在容器变宽、图片/字体/文本改变高度、筛选重排和 cursor 追加后全量重算;cleanup 必须兼容 StrictMode,避免分页 sentinel 因容器短暂零高提前触发。 - 验证:纯函数测试锁定容器宽度临界值、
index % columns和最高列容器高;样式契约同时锁定 Grid fallback 与 Masonry ready。Playwright 需在同一 viewport 内改容器宽度验证 3/2/1 列,检查每列相邻卡间距等于 gap、容器高等于最高列底、DOM 顺序不变且无重叠/横向溢出。 - 关联:
src/index.css、src/index.test.ts、src/components/creation-home/CreationLandingView.tsx、src/components/creation-home/showcaseMasonryLayout.ts。
画板外部生成排队超时不是失败
- 现象:画板发起付费图片生成后,前端弹出
生成任务仍在队列中,请稍后刷新画布查看结果,但后端任务仍在队列或执行中,后续可能正常完成。 - 原因:画板生成已经接入后端外部生成任务队列,
queued/running是正式任务状态;旧前端轮询等待窗口到期时直接抛错,导致正常排队被提交流程 catch 成失败 UI。 - 处理:
waitForEditorGenerationQueue等待超时只返回“仍在后端继续执行”,调用方停止本次前端等待并保留生成中状态;只有后端任务终态为failed才展示失败。 - 验证:画板生成 workflow 测试覆盖 queueState 持续
running到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。 - 关联:
src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx。
场景队列终态不保证首次项目快照已经收口生成占位
- 现象:游戏场景任务已经显示完成,但画布仍保留
generating占位;场景链路又禁止用本地结果补层,因此当前会话可能一直停在生成中。 - 原因:外部生成任务终态与项目画布投影不是同一个原子观测点。队列轮询先看到
completed后,紧接着的首次项目 GET 仍可能读到同一dialogId的未收口占位;若调用统一回读函数时没有传 completion dialog ID,函数无法识别该快照仍未完成,也不会执行已有的有界延迟重读。 - 处理:游戏场景队列调用要把本次占位
dialogId传给applyQueuedEditorGenerationProject。首个快照中该 ID 仍为 unresolved 时,只按既有间隔补读一次项目;不追加本地图层,也不把任务终态直接等同于画布投影终态。其他生成类型若要补同类保护,必须分别复现其权威回填时序后再改,不能用本条场景结论替代验证。 - 验证:场景 workflow 用两个连续快照复现时序:第一个保留
scene / generating,第二个包含场景结果并把同一占位置为idle。修复前只读一次并超时,修复后依次应用两个权威快照。 - 关联:
src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx、docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md。
图片画布历史不能回退当前权威状态或复活后端已删素材
- 现象:生成占位框移动后开始生成,撤销移动会把仍在运行的生成对象恢复成待生成状态;切换到 2K 或改变比例后撤销位置,旧占位框还可能把当前尺寸回退。上传图层落库后,普通移动撤销可能被提示“可能会使图片消失”并永久卡在栈顶;即使安全检查已放行,直接恢复旧图层快照也会丢失刚回填的资源关联。素材库后端删除关联素材后,更早的移动快照还可能把已删图层重新加入并自动保存;修改素材类型虽然界面提示撤销成功,刷新后却可能从仍指向新类型的 resource 回弹。无稳定 ID 的“修改图片”草稿也可能被 target-null 快照直接关闭。
- 原因:内容消失安全检查和历史快照合并是两道独立边界。图层内容签名若严格比较
sourceAssetId、sourceResourceId等延迟回填的内部关联 ID,会把同一媒体误判为替换;放行后若仍用目标快照整体覆盖同 ID 图层或占位框,又会回退当前权威关联、内容或尺寸。相同 dialog ID 直接恢复整个旧对话框快照还会覆盖当前generating/ 完成态;没有 ID 的 edit 草稿则根本不会进入存在性检查。外部素材删除不写画布历史,若不主动剪除包含关联图层的旧目标快照,target-only 图层会被当作正常撤销删除完整恢复。assetKind的正式事实保存在项目 resource,历史只改内存字段而保留当前resourceId时无法跨刷新成立。 - 处理:图层内容身份按对象存储 key、对象标识和媒体地址的稳定优先级比较,内部关联 ID 的补齐不参与内容消失判断。同 ID 图层以 current 为权威,只从历史覆盖
x、y、zIndex、groupId、assetKind、hidden、locked、flipX、flipY;current 的资源关联、内容、媒体、生成元数据、尺寸和标题全部保留。同 ID generation dialog 从 target 恢复 placeholder 的x/y和 active / inactive 槽位对应的composerOpen,current 的width/height/originalWidth/originalHeight、当前参数、任务生命周期、提示词、参考图和结果保持一致;edit 草稿使用基于来源图层的稳定 ID。current 中不存在对应 ID 时属于撤销完整删除,可从 target 全量恢复对象。素材库删除必须用isLayerLinkedToAssetmatcher 同步过滤 undo / redo 中所有包含关联图层的 entry,即使图层只存在于历史中也要过滤;普通画布删除不调用该接口。assetKind undo / redo 每次重新创建匹配恢复类型的正式 resource,并按 layer 请求版本只接受最新响应。即时生成结果在追加图层前捕获生成历史,自动适合视图不再压入另一条历史。 - 验证:覆盖 idle 生成框移动后进入 generating 再撤销、上传图层异步回填
resourceId/sourceAssetId/sourceResourceId后撤销移动仍保留当前关联值、切换到 2K 或改变比例后撤销位置仍保留当前占位尺寸、非活动生成框被激活并拖动后撤销可恢复原 active / inactive 打开状态、撤销完整删除可以全量恢复对象、即时生成后第一次撤销直接命中生成保护、阈值内指针抖动既不移动也不产生历史、素材库删除后旧 undo / redo 无法复活关联 layer、edit 草稿不会被 target-null 快照吞掉,以及 assetKind 撤销与乱序 resource 响应后刷新仍保持最终类型。 - 关联:
src/components/image-editor/ImageCanvasHistoryModel.ts、src/components/image-editor/useCanvasHistory.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/useImageCanvasStageInteractions.ts、docs/【图片画布】撤销范围与操作提示方案-2026-07-17.md。
画板参考图 objectKey 必须先做归属校验
- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
- 原因:Data URL/Blob URL 只允许停留在浏览器临时态,正式编辑器引用必须先上传并经统一 resolver 校验归属。
- 处理:普通图片、重绘、图标图集附加参考图和 UI 提取等允许 objectKey 的私有对象引用,在读取字节或签发 URL 前统一走
resolve_editor_reference_object_key_for_owner(state, owner_user_id, source),先在当前账号的项目资源、素材库资产或asset_object中匹配 owner / bucket / key。只有确实需要图片字节的入口再走parse_editor_reference_image(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要parse整图。图标规范生成的可选参考图与图标图集的主规范引用是更窄的业务契约:只接受正式referenceId(项目资源 ID / 素材 ID),不得回退 objectKey、URL 或临时 key;图标图集的额外referenceImageSrcs才继续沿用通用 owned objectKey 规则。手动去背景还要恢复源模型时,object key 解析、所有权校验和源模型回溯必须复用同一轮账号项目 / 素材快照,项目与素材快照各最多读取一次;不得先走通用 resolver 全量读取,再为模型回溯重复拉取完整画布和素材库。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。 - 验证:
cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference,并用前端 workflow 测试覆盖referenceImageSrcs进入图标生成请求;若快速编辑重开额外参考图,再补对应请求覆盖。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/spacetime-client/src/assets.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts。
资产换签不能把 generated 前缀当成 objectKey 授权
- 现象:主站或 External API 只要拿到另一个账号的 generated
objectKey就能换签,已登记的私有对象因为 key 同时命中 legacy 前缀而被匿名读取,或者/api/assets/read-url已拒绝但/api/assets/read-bytes仍能读出原始字节;后台资源预览为解决跨账号读取又误把主站入口整体放开。 - 原因:
legacyPublicPath与objectKey代表两种不同信任边界。前者仅用于未登记历史公开作品兼容,后者是正式对象引用;只检查 generated 前缀、或在查询asset_objectmetadata 前直接接受 legacy 白名单,都不能证明对象公开或属于调用方。签名 URL 和 bytes proxy 如果各写一套判断也容易漂移。 - 处理:
read-url与read-bytes必须共用authorize_asset_read_target,先按配置 bucket / 精确 key 查询asset_object;metadata 一旦存在,即使 key 命中 legacy 前缀,也严格执行PublicRead/ owner ACL。只有 metadata 不存在且显式legacyPublicPath命中platform_oss::LEGACY_PUBLIC_PREFIXES时才允许匿名兼容;普通objectKey必须登记。唯一窄例外是历史精选活动卡:global配置已启用、请求 key 位于generated-character-drafts/editor/showcase-campaign/且与当前image_object_key精确匹配时,可在 metadata 缺失期间派生公开读取,禁用或换图后旧 key 立即失效;新上传活动卡仍必须 confirm。External read-url 使用 API Key owner。主站和 External 的 object confirm owner 必须来自认证主体,同 bucket / key 已登记后不能改变 owner,不能让请求体 owner 接管对象。后台跨 owner 换签只用于管理员资源预览,成功后以admin_asset_read_url持久化管理员 subject、请求对象和有效期,且不得记录 signed URL;不能为此把 admin 能力下沉到主站入口。无权访问统一返回不存在,避免泄露对象是否存在。 - 验证:覆盖未登记 curated legacy public path、命中 legacy 前缀但已有私有 metadata、未登记普通 objectKey、活动卡当前/禁用/替换/越目录 exact key、公开对象、本人私有对象、跨 owner、匿名私有、External owner、confirm owner 不可变和 admin-only endpoint;对
read-url与read-bytes使用同一组授权矩阵,并断言 Admin 成功换签会生成不含 signed URL 的管理员主体审计事件。 - 关联:
server-rs/crates/api-server/src/assets.rs、server-rs/crates/api-server/src/external_assets_api.rs、server-rs/crates/api-server/src/admin.rs、server-rs/crates/api-server/src/modules/admin.rs。
精选活动卡上传成功但网站空图先查对象确认和 exact grant
- 现象:后台精选活动卡能保存标题、作者、尺寸和图片地址,
GET /api/editor/showcase/resources也返回已启用 campaign,但网站卡片只有占位区域,没有<img>;Network 中活动卡的/api/assets/read-url?objectKey=...返回404 资源不存在或无权访问。 - 原因:活动卡旧上传链路只完成 signed POST 并保存
imageSrc + imageObjectKey,没有调用 object confirm;同时公开授权只扫描普通editor_showcase_asset,没有识别当前活动卡。前端见到imageObjectKey后会优先走正式 objectKey 换签,失败时按安全规则保持空 src,不回退裸 private 路径。 - 处理:后台上传必须按 ticket -> OSS POST ->
/admin/api/editor-showcase/campaign/image-upload-confirm-> 写回表单执行,confirm 复用统一 OSS HEAD、bucket/长度校验和asset_objectupsert,并由管理员会话绑定 owner、强制 private/固定 asset kind/活动卡专用目录。读取 procedure 在同一事务中对当前 enabled global campaign 的专用目录 exact key 派生授权,使历史未登记当前卡无需重新上传即可恢复;api-server 仅接受 procedure 明确返回的这一 grant,其他未登记 objectKey 继续 404。 - 验证:SpacetimeDB 测试覆盖 current key、disabled、missing key、replaced old key、越目录 key 和普通精选 grant;api-server 测试覆盖 metadata 缺失时 exact grant 可读、无 grant 仍 404、confirm 路径/MIME/大小/固定 private 约束;admin-web 测试锁定 ticket -> OSS -> confirm 顺序。真实浏览器应看到活动卡图片,Network 中 objectKey 换签返回 200,禁用或换图后旧 key 返回 404。
- 关联:
apps/admin-web/src/api/adminApiClient.ts、server-rs/crates/api-server/src/admin.rs、server-rs/crates/api-server/src/assets.rs、server-rs/crates/spacetime-module/src/asset_metadata/objects.rs、server-rs/crates/spacetime-module/src/editor_project_storage.rs。
编辑器生成按钮显示泥点后仍要查真实钱包预扣
- 现象:画板生成按钮显示
N泥点,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。 - 原因:前端展示价和后端价格计算只证明价格能被展示 / 解析;如果 handler 没有包进
execute_billable_asset_operation_with_cost,或异步音频发布目标没有携带本次模型价格,外部 provider 仍会被调用但不会真实扣费。 - 处理:新增或改造编辑器外部生成入口时,确认前端请求不携带
priceMudPoints,后端按运行时模型定价重新计算价格,并用该价格进入资产扣费 wrapper。音频提交 / 发布分离时,把后端计算出的价格写入AudioAssetBindingTarget.billing_points_cost。 - 验证:结构性测试覆盖对应 handler 包含
execute_billable_asset_operation_with_cost和价格变量;音频测试覆盖resolve_creation_audio_points_cost优先读取 editor target 的billing_points_cost。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/api-server/src/character_animation_assets.rs、server-rs/crates/api-server/src/vector_engine_audio_generation/、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts。
本地 dev 启动日志先看成功锚点,不要把非阻断 warning 当失败
- 现象:
npm run dev启动 SpacetimeDB 时可能先打印static max level is off、Skipping tokio metrics,或 SpacetimeDB CLI 提示存在新版本 / 当前版本较旧,看起来像启动异常。 - 原因:这些是 tracing、metrics 或 CLI 更新提示,不代表本地 dev 栈失败;同一段日志后续仍可能已经完成
SpacetimeDB listening on 127.0.0.1:3101、模块 publish、api-server/healthz200、主站 Vite3000和后台 Vite3102ready。 - 处理:排查本地 dev 栈时先确认成功锚点:
[dev:spacetime] actual、Updated database、api-server 已完成 tracing 初始化并开始监听、/healthz200、两个 Viteready。只有缺少这些锚点或进程退出时,再继续查 CLI 权限、端口占用、publish 或 API 编译问题。 - 验证:
http://127.0.0.1:3101/v1/ping可访问、http://127.0.0.1:8082/healthz返回 200、http://127.0.0.1:3000/和http://127.0.0.1:3102/admin/可打开。 - 关联:
scripts/dev.mjs、.app/dev-stack.json、docs/project-memory/shared-memory/development-workflow.md。
私有兑换码不适用先查同手机号重复账号
- 现象:后台把私有兑换码配给某个陶泥号或手机号后,用户用同一手机号登录兑换仍提示
该兑换码不适用于当前账号。 - 原因:认证表里可能存在同一手机号的多条
user_account。如果认证工作集重建phone_to_user_id时让user_account.phone_number_e164后写覆盖前写,当前登录态会漂到没有auth_identity的重复账号,而兑换码白名单仍指向另一个内部user_id。 - 处理:重建认证工作集时以 typed
AuthStoreProjectionView从user_account/auth_identity/refresh_session恢复;手机号索引以auth_identity(provider="phone")指向的账号为权威,user_account.phone_number_e164只补没有 identity 的手机号;auth_store_snapshot表和旧 JSON procedure 已删除,Bearer / refresh session 本进程未命中时不要再从 SpacetimeDB 导出整包状态刷新内存。线上止血先核对失败请求附近的 current sessionuser_id与兑换码allowed_user_ids,不要只看手机号展示值。 - 约束:
auth_identity只保存登录入口身份键;手机号、昵称和头像的正式资料真相在user_account.phone_number_e164/display_name/avatar_url。旧auth_identity.phone_e164/display_name/avatar_url只能作为历史回填来源,不能继续让新写入依赖这些列。 - 验证:
cargo test -p spacetime-module auth_export -- --nocapture应覆盖同手机号重复账号时手机号索引优先指向有 phone identity 的账号;api-server中不应再存在运行期refresh_auth_store_from_spacetime调用。 - 关联:
server-rs/crates/spacetime-module/src/auth/procedures.rs、server-rs/crates/spacetime-module/src/auth/tables.rs、server-rs/crates/module-auth/src/lib.rs。
API Build / Deploy 归档清单不能漏掉随包 Pingora 脚本
- 现象:
Genarrative-Api-Deploy在发布阶段报发布产物缺少 Pingora TLS 证书同步脚本: build/<version>/scripts/deploy/pingora-tls-cert-sync.mjs。 - 原因:
scripts/build-production-release.sh已经把脚本复制进build/<version>/scripts/deploy/,但 Jenkins API Build 的archiveArtifacts和 API Deploy 的copyArtifacts过滤清单仍可能漏掉新增随包脚本,导致 Deploy 工作区拿到的是残缺发布包。 - 处理:新增随包部署脚本时,必须同时更新
jenkins/Jenkinsfile.production-api-build的归档清单、jenkins/Jenkinsfile.production-api-deploy的复制清单和scripts/check-production-ops-guardrails.mjs的字符串门禁;不要在 Deploy Job 里从工作区根目录或源码 checkout 兜底补脚本。 - 验证:运行
npm run check:production-ops、npm run check:production-api-release和npm run check:production-api-deploy,确认构建包、Jenkins 归档链路和 deploy fail-fast 检查口径一致。 - 关联:
jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy、scripts/deploy/production-api-deploy.sh、scripts/check-production-ops-guardrails.mjs。
图片画布角色动作结果主类型是序列帧
- 现象:产品要求画板
生成角色动作返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理。 - 原因:后端仍需要先生成
previewVideoPath再抽帧、绿幕去背和落 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载。 - 处理:角色动作结果图层主
src使用frames[0].imageSrc,mediaType固定为image-sequence,assetKind固定为character-animation,完整帧列表写入imageSequenceFrames,previewVideoPath只作为来源信息保留。生成端确认每帧对象后必须把该帧objectKey与assetObjectId一起写入正式 payload 和generation_inputs_json.characterAnimation.frames。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入sequences/<编号-标题>/frames/。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。 - 验证:
ImageCanvasGenerationLayerModel应断言动作结果src为首帧且mediaType="image-sequence";画布集成测试应出现画布序列帧:角色动作图片播放器,不应出现角色动作<video>;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录;生成测试还应断言首帧与非首帧的稳定引用都被保留。 - 关联:
src/components/image-editor/ImageCanvasGenerationLayerModel.ts、src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasExportModel.ts、server-rs/crates/api-server/src/character_animation_assets.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
OSS 导出兜底必须覆盖响应体读取阶段
- 现象:浏览器控制台显示 OSS
206 Partial Content后紧跟net::ERR_FAILED,画布预览或素材导出失败,但没有出现预期的/api/assets/read-bytes兜底请求。 - 原因:
fetch(signedUrl)可能先返回一个ok的Response,网络、浏览器 Range 缓存或传输错误随后才在arrayBuffer()/blob()消费响应体时暴露;如果直连保护边界只包住fetch和状态码,响应体失败会绕过 fallback。 - 处理:私有素材字节读取必须在直连 OSS 分支内完整消费响应体并重新构造可重复读取的
Response;换签、请求、非成功状态或响应体读取任一阶段失败时统一回退同源/api/assets/read-bytes。Abort 仍应直接上抛,不能转化为额外服务器读取。 - 验证:前端服务测试模拟 OSS 返回
206/ok、但blob()reject,断言随后请求/api/assets/read-bytes并返回 fallback 完整字节;同时保留直连成功、局部分片、直连非成功、请求 reject 和 Abort 边界。 - 关联:
src/services/assetReadUrlService.ts、src/services/assetReadUrlService.test.ts、src/components/image-editor/ImageCanvasExportModel.ts。
图片画布序列帧播放不要复用普通图片淡入样式
- 现象:角色动作序列帧播放时看起来像每帧之间在渐变或闪烁。
- 原因:序列帧播放器每帧切换可低至 40ms,默认约 125ms 一帧;如果帧
<img>复用普通图片的image-canvas-editor__layer-image--loading/--loaded,其中opacity 180ms ease会跨过下一帧切换,形成类似交叉淡入淡出的视觉。 - 处理:
ImageCanvasImageSequenceFrame只使用序列帧专属 class,帧显隐用同步opacity硬切,并显式transition: none;保留“下一帧未加载时继续显示上一帧”的 readiness gate。 - 验证:
ImageCanvasWorldView.test.tsx应断言序列帧<img>不带普通图片 loading/loaded class,且 style 中transition为none。 - 关联:
src/components/image-editor/ImageCanvasWorldView.tsx、src/index.css、src/components/image-editor/ImageCanvasWorldView.test.tsx。
Vidu 文生音频线上网关可能要求 sound 字段
- 现象:画板点击
生成游戏音效后,请求返回Failed to deserialize the JSON body into the target type: missing field sound。 - 原因:VectorEngine Apifox
创建文生音频任务文档仍写/ent/v2/text2audio使用model + prompt + duration,但线上 Vidu 网关曾按sound字段反序列化;只发送prompt会被上游拦截在 JSON 解析阶段。 - 处理:前端和 BFF 对内继续使用用户语义更清晰的
prompt;platform-audio转发到 VectorEngine Vidu 时同时发送prompt与sound,两者值保持一致。不要把 UI 改回 Sunotask: "sound"、type、tempo或 BPM。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml --test vector_engine_audio中音效请求体测试必须同时断言prompt与sound;必要时用线上生成音效 smoke 确认不再出现missing field sound。 - 关联:
server-rs/crates/platform-audio/src/request.rs、server-rs/crates/platform-audio/tests/vector_engine_audio.rs、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
Suno 任务完成或返回 audiopipe 不代表已经拿到稳定下载地址
- 现象:画板生成背景音乐时,前端可能报
音频生成尚未返回可下载地址(requestId:...)、获取 Suno 音效 wav 失败(requestId:...)或读取生成音频内容失败:error decoding response body。上游任务可能已经完成并返回https://audiopipe.suno.ai/?item_id=...,但该地址仍可能以200 + chunked开始响应后不返回完整正文。 - 原因:VectorEngine Suno
/suno/fetch/{task_id}可能先在data中返回歌曲 / 音效 clip id,或返回只携带item_id的 audiopipe 流式中转地址,而不是稳定.wav/.mp3文件 URL;需要再调用/suno/act/wav/{clipId}获取实际文件地址。如果只在“完全没有 URL”时回退 wav,会误把 audiopipe 当最终文件并让 worker 在正文读取阶段卡满请求超时。 - 处理:
platform-audio查询 Suno 结果时保留普通直接音频 URL;遇到 audiopipe 时不直接下载,而是从查询结果的id/clip_id/audioId/songId或 audiopipeitem_id提取 clip id,逐个调用/suno/act/wav/{clipId}。已拿到 clip id 但 wav 地址仍未就绪,或 wav 子请求暂时返回上游错误时,都保持processing让上层继续轮询,不能直接判定为缺少可下载地址或 wav 获取失败。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml;cargo test -p api-server vector_engine_audio_generation --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-audio/src/client.rs、server-rs/crates/platform-audio/src/response.rs、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
图片画布音频卡播放条 0:00 要优先查签名 URL 和嵌套交互
- 现象:画板音效或背景音乐已经生成成功,但卡片里的播放条显示
0:00,点击无法预览。 - 原因:generated 音频资源通常是私有 OSS 路径,直接把
/generated-*或 generated OSS 地址交给<audio>会无鉴权读取失败;如果音频控件嵌在<button>图层里,浏览器还可能因嵌套交互元素阻断 controls 行为。 - 处理:音频图层使用非嵌套交互容器承接画布选择语义,内部
<audio controls preload="metadata">单独阻止 pointer / click 冒泡;generated 音频播放前统一通过useResolvedAssetReadUrl//api/assets/read-url换签。卡片和角标展示时长,后端没返回时长时可用loadedmetadata.duration兜底。 - 验证:
npx vitest run src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts --reporter verbose,并在浏览器确认 generated 音频控件可播放。 - 关联:
src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasMediaModel.ts、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
图片编辑器底部生成按钮不要复用单一画布生成状态
- 现象:图片画布里先新建一个“生成规范”占位,再点击“生成角色形象”或其它底部生成入口,前一个规范占位和面板状态被销毁。
- 原因:底部普通生成、规范、角色和图标素材曾共用单个
generateDialog状态;后一次点击直接覆盖该状态,等同把前一个画布生成对象卸载。 - 处理:底部生成类入口每次点击都创建独立 generation dialog id;当前 active 对象只负责显示编辑面板,旧对象归档为 inactive 后仍保留占位和生成逻辑状态。生成完成 / 失败回写、生成中拖拽和删除都必须按 dialog id 读取 active + inactive 中的最新对象,不能回退到提交瞬间的旧占位快照。
- 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps existing generation placeholders"应断言规范占位和角色占位可同时存在;npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps archived generation logic"应断言旧对象归档后拖动,占位完成回写仍落在最新位置。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器生成中设定面板不要和预览框绑成同一可见性
- 现象:图片编辑器里点击生成后,有时设定面板没收起,有时连画布上的占位预览一起消失,看起来像“生成中界面掉了”。
- 原因:生成中状态只收了 composer 可见性,或把占位框和设定面板共用了同一段条件渲染;面板隐藏后把 placeholder 也一起卸掉,就会丢掉 Lovart 式生成中预览。
- 处理:进入
generating后只隐藏设定面板,保留占位框和生成中状态胶囊;面板外观、预览框和结果图层分开控制,不共用同一个composerOpen条件。 - 验证:对应测试应断言生成按钮点击后
dialog消失但image-canvas-editor__generation-frame--generating仍然存在。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx。
图片画布生成器全体点不开先查卡住的临时交互状态
- 现象:特定操作后,画布中已有生成器点击不再显示设定对话框,而且不是单个生成器坏掉;新建生成器或刷新页面后恢复。
- 原因:旧生成器激活依赖全局交互状态;如果
Shift/ 空格按住态因为窗口失焦漏掉keyup,或“从画布选择参考图”等临时 picking / 菜单状态没有在激活旧生成器时清理,后续点击会被当成多选或选参考图而短路。 - 处理:窗口
blur/ 页面隐藏时释放Shift和空格按住态;激活已有 generation dialog 时同步清理参考图 picking、规格 / 参考菜单和右键菜单;active / inactive 生成器状态的 ref 与 React state 必须同事件周期同步。 - 验证:
npm run test -- src/components/image-editor/useCanvasGenerationDialogs.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx -- --runInBand,并跑ImageCanvasEditorView.test.tsx确认真实组件链路仍能激活生成器。 - 关联:
src/components/image-editor/useCanvasGenerationDialogs.ts、src/components/image-editor/useImageCanvasKeyboardShortcuts.ts、src/components/image-editor/ImageCanvasEditorView.tsx。
图片画布素材多时拖拽卡顿先查等距吸附候选规模
- 现象:画布素材数量增加后,拖拽单个图层或生成占位框时 pointermove 明显卡顿,关闭或绕开吸附后体感恢复。
- 原因:边缘 / 中心线吸附是线性扫描,但等距吸附如果对所有可吸附素材做两两配对,会在素材数量上来后进入 O(n²) 热路径。
- 处理:保留边缘 / 中心线全量线性扫描;等距吸附先过滤跨轴相交素材,再只检查轴向邻近候选,不要为远处或不相交素材生成配对候选。
- 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasInteractionModel.test.ts,并在多素材画布拖拽时确认参考线仍能命中邻近图层且 pointermove 不再明显掉帧。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/ImageCanvasInteractionModel.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布拖动卡顿先查 Stage 合帧和交互期自动保存
- 现象:素材多或序列帧多时,拖动图层、生成占位、小地图视口框或手型平移明显卡顿,像是接口慢或 CSS 动画掉帧,但网络请求不一定异常。
- 原因:高刷新率输入设备会在单个屏幕帧内发出多次
pointermove;每次直接setLayers/setViewport都会触发画布重渲染、吸附或小地图模型重算和工程持久化 effect。即使moveLayersFromDrag保留未移动图层的对象引用,若 WorldView 仍在每帧重建全部图层子树,所有真实位图的 URL hook、加载态、标签和 SVG 操作也会重复执行。持久化链路还会同步serializeCanvasLayout、JSON.stringify并写 sessionStorage,远端 PATCH 有防抖也挡不住本地同步缓存写入。 - 处理:Stage 的图层、生成占位、框选和手型平移统一用单一在途
requestAnimationFrame合并同帧输入,只应用最新坐标;结束拖拽时 flush 最后一帧,主动清理和卸载时 cancel。图层、生成占位、平移和小地图拖动一旦越过拖动阈值就标记为临时交互,拖动中不触发项目保存、session cache 写入或封面快照上传,pointerup/pointercancel后保存最终布局。WorldView 的完整单图层节点必须按稳定 layer 对象浅比较 memo,父回调通过 latest ref 的稳定门面转发,避免未移动图层重渲染或读取陈旧闭包。小地图继续只在 viewport controls 内合帧,不要重复套 rAF。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx --reporter verbose应覆盖只重渲染移动图层、稳定节点调用最新回调、同帧只保留最新坐标、结束前 flush、卸载 cancel、小地图无双重合帧、图层 / 生成占位 / 平移 / 小地图交互边界,以及拖动期间不写 sessionStorage / 不调用saveEditorProjectLayout。浏览器验收必须使用多个独立 raster URL,并区分 rAF 心跳与目标实际位置变化帧;共享 data URI SVG 和包含空闲尾帧的自由 rAF 不能作为拖动流畅证据。 - 关联:
src/components/image-editor/useImageCanvasViewportControls.ts、src/components/image-editor/useImageCanvasStageInteractions.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器宣发素材生成器刷新后不要丢快照
- 现象:图片画布刷新后,宣发素材生成卡片消失,或卡片仍在但游戏名、分类、描述和参考图丢失。
- 原因:画布布局把生成器保存为
itemType: "generation-dialog",但恢复白名单漏掉publication模式和publicationWorkflowId/publicationGameInfo/publicationReferences字段,导致整条生成器快照被当成无效布局项丢弃。 - 处理:
hydrateCanvasGenerationDialog必须把publication视为正式画布生成器模式,并显式恢复宣发素材专属字段;组件层应断言刷新回读项目快照后仍显示卡片类型、字段和参考图。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx --reporter verbose。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/ImageCanvasPublicationMaterialsDemoPanelView.tsx、src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。
图片画布生成请求不要直接提交临时媒体源
- 现象:图片画布快速编辑、参考生成、去背景或角色动画提交 Data URL / Blob URL 时,前端或后端返回“必须先上传 OSS”。
- 原因:Data URL / Blob URL 体积大且不能作为持久引用;如果写入外部生成队列,请求体会膨胀,worker 也无法稳定复用浏览器临时资源。
- 处理:提交前优先复用图层已有
objectKey、项目资源 ID 或素材 ID;未登记的本地图片和普通 public 图片路径先通过resolveEditorGenerationMediaReference(...)读取并上传 OSS,再把稳定引用交给生成接口。Data URL 只允许用于浏览器内压缩、标注等临时处理,不能进入 API 请求、队列载荷或项目持久化。 - 验证:
npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx;cargo test -p api-server inline_data_url --manifest-path server-rs/Cargo.toml。 - 关联:
src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/services/image-editor/editorProjectClient.ts、server-rs/crates/api-server/src/editor_generation_queue.rs、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
专用生成契约不能被通用生成接口和任务摘要绕过
- 现象:专用场景接口要求结构化
sceneContent + stylePreset,但调用方仍可向通用图片接口传kind = scene或assetKind = scene,用任意完整 Prompt 生成并持久化正式场景;合法场景入队后,任务侧栏还可能显示后端完整规则文本和通用“生成图片”标题,空白素材名则可能回退成完整 Prompt。 - 原因:专用 handler 内部复用了通用图片 payload、队列和 Worker,但公开通用 HTTP handler 没有限制专用身份;任务摘要又无条件优先提取 payload 顶层
prompt,素材名默认值只处理了字段省略,没有处理空白字符串。 - 处理:公开通用 handler 拒绝专用
kind / assetKind,专用 handler 仍可直接调用内部共享执行函数;队列投影按kind = scene从权威generationInputs.fields[画面内容]派生标题和摘要,缺字段时失败关闭而不是回退内部 Prompt,并重新计算历史缓存;专用素材名统一把省略和空白收口为产品默认值。 - 验证:路由测试先证明旁路会越过 HTTP 边界,再断言两种旁路均返回
400且指向专用端点;摘要测试覆盖新任务、历史错误缓存和缺少画面内容三种情况;标签测试覆盖省略、空白、自定义和 80 字上限。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/spacetime-module/src/external_generation.rs、docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md。
图片编辑器角色动画必须提交稳定图片引用
- 现象:图片编辑器里对尚未上传的角色图点击
生成动画后,前端或后端返回sourceImageSrc 必须先上传 OSS。 - 原因:角色动画会进入外部生成队列,浏览器 Data URL / Blob URL 既不适合持久任务,也会放大 JSON 请求体。
- 处理:前端统一通过
resolveEditorGenerationMediaReference(...)取得objectKey或画板资源引用;本地临时角色图必须先上传 OSS。后端在入队前同步拒绝内联媒体,不再通过放宽 body limit 兼容 Data URL。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx;cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml。 - 关联:
src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、server-rs/crates/api-server/src/character_animation_assets.rs、server-rs/crates/api-server/src/app.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
图片编辑器角色动画抽帧不要采到视频尾点或逐帧重启 FFmpeg
- 现象:画板角色图点击
生成动画后,Ark 视频已生成并上传 OSS,但后端返回ffmpeg 已执行但未产出动作帧文件(requestId:...)。 - 原因:FFmpeg 在采样时间落到视频尾点附近时可能退出码仍为
0,但实际输出0帧;如果后端按duration - 0.001抽最后一帧,低帧率或短视频很容易踩到不可解码尾点。旧实现还会为32 / 40 / 48个采样点分别启动 FFmpeg、重复解码同一视频,在低配 worker 上形成不必要的多秒 CPU 尖刺。 - 处理:角色动画先按目标帧数计算全部安全采样时刻,例如
32帧·4秒最后一帧采3.875s,不要采3.999s;随后使用单个setpts + split + selectfilter graph 批量输出全部帧,不改用粗粒度fps抽帧。命令返回后逐一检查输出,缺帧时用户主文案保持简短,details 保留首个缺帧的targetSeconds / outputPath、整批missingFrames和 stdout/stderr。 - 验证:
cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml;editor_character_animation_batch_extracts_all_samples_from_short_video必须用一次 FFmpeg 产出整批短视频帧,尾帧测试继续锁定3.875s。 - 关联:
server-rs/crates/api-server/src/character_animation_assets.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
Windows 本地角色动画抽帧找不到 ffmpeg 先查 dev 子进程环境
- 现象:画板角色动画抽帧报
抽取动作视频帧失败:无法启动进程 ffmpeg:program not found(requestId:...),但新开的 PowerShell 里ffmpeg -version正常。 - 原因:长期运行的
api-server可能是在安装 FFmpeg 或更新用户 Path 之前启动的,子进程不会自动继承后续写入的用户环境变量。 - 处理:Windows 本地默认把 FFmpeg 安装到
%LOCALAPPDATA%\Genarrative\ffmpeg\bin,并确保用户 Path 包含该目录;npm run dev/npm run dev:api-server会在启动api-server时自动注入该目录和CHARACTER_ANIMATION_FFMPEG_PATH/CHARACTER_ANIMATION_FFPROBE_PATH绝对路径。修复后需要重启api-server,不能只刷新浏览器。 - 验证:
where ffmpeg、where ffprobe能找到本地安装;npm run test -- scripts/dev.test.ts -t "FFmpeg";重启npm run dev:api-server后访问/healthz。 - 关联:
scripts/dev.mjs、server-rs/crates/api-server/src/config.rs、server-rs/crates/api-server/src/character_animation_assets.rs。
图片编辑器生成长请求完成态必须由后端写入画布
- 现象:画板角色形象等生成请求已经在服务端返回
200,OSS 中也已有generated-character-drafts/.../image.png,但用户刷新或页面重载后仍看到旧生成卡片停在“生成中”。 - 原因:生成是一次长 HTTP 请求,浏览器在请求完成前刷新或重新挂载时会丢失原页面的成功回调;如果完成态只靠前端回调把结果图层写回
editor_canvas.layers_json,服务端虽然已经创建editor_project_resource/editor_asset,但布局里的generation-dialog仍可能停在status="generating"且没有generatedLayerId。如果之后从素材库把同一私有素材加回画布,前端再次创建项目资源时若提交 signed URL / Data URL,还会触发413,进一步阻断资源行绑定。 - 处理:图片生成提交必须在有项目上下文时携带
canvasCompletion(生成器dialogId、标题和占位框);api-server生成成功并创建资源后,直接读取当前项目布局,只有当前布局仍存在对应生成器时才插入轻量结果图层、把生成器改回idle并写入generatedLayerId,再沿用后端当前 viewport 保存 layout 并返回最新项目快照。前端只应用该快照刷新显示,不在加载时根据资源行推断完成态;有项目上下文但后端没有返回快照时也不得本地补结果图层。为已有objectKey的图层创建项目资源时,imageSrc只提交/<objectKey>,不要提交 signed URL / Data URL。 - 验证:
cargo test -p api-server editor_canvas_generation_completion --manifest-path server-rs/Cargo.toml覆盖后端完成态写 layout;npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand覆盖前端提交canvasCompletion、应用后端快照、项目加载不推断完成态和objectKey资源创建不提交大 URL。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器项目和素材 payload 不能持久化内联媒体
- 现象:
/api/editor/projects*、素材库、项目资源或 layout payload 里出现数 MB 的data:image/*、data:video/*、data:audio/*,刷新恢复变慢,发布入口可能 OOM / 413,素材库缩略图还可能只显示文件名。 - 原因:生成、规范图、角色图、图标 / UI spritesheet、音视频或动画帧如果直接把 Data URL / signed URL 写入
editor_project_resource、editor_asset或editor_canvas.layers_json,就把媒体本体塞进了项目快照;signed URL 还会过期,素材库也无法稳定换签。 - 处理:登录态媒体必须先上传 OSS / asset object,持久化只写
imageSrc: "/<objectKey>"、objectKey、assetObjectId;素材库和图层缩略图都通过PlatformMediaFrame -> ResolvedAssetImage传objectKey并调用/api/assets/read-url。layout 序列化和后端保存要递归拒绝data:*/blob:;旧行有objectKey时读出归一成/<objectKey>,没有objectKey的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,必须按用户隔离,而且不能在后端快照回来前自动保存。认证状态变化重跑加载 effect 时,要同步用 ref 关闭写门禁并清除 revision、pending save 和 timer;不能只等isProjectReady=false的下一次 render,否则旧 effect 会先消费 skip 标记,再把公司浏览器的旧缓存无版本 PATCH 到服务端,覆盖另一台设备的新画布布局。现役 Web 与 External layout PATCH 的expectedRevision都必填,三层门禁分别放在 autosave effect、queue 和真正发送前;session cache 即使带 revision 也只有显示权。异步 project resource 创建必须把未发请求队列按用户 / 项目隔离,并记录发起时已接受的权威快照序号;若资源响应前发生认证重载、409 恢复或生成完成快照替换,只把新资源对应图层合并进当前权威布局,禁止用历史snapshotLayers整体覆盖。生成扣费、失败退款或 queue 终态后,右上角泥点余额通过/profile/dashboard回读,不做本地乐观扣减。 - 验证:Network 中
/api/editor/projects*、PATCH /api/editor/projects/{id}、素材库接口不应出现data:image/data:video/data:audio;素材库和图层面板缩略图都能换签显示;npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/common/PlatformMediaFrame.test.tsx src/services/assetReadUrlService.test.ts src/services/image-editor/editorProjectClient.test.ts,后端跑cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、src/components/common/PlatformMediaFrame.tsx、src/services/assetReadUrlService.ts。
图片画布裁扩后刷新或去背景丢图先查项目资源化
- 现象:从规范图裁切 / 裁扩出新图层后立即执行去除背景,去背景完成时原裁扩图从画布消失;刷新后裁扩图仍不在,但去背景占位可能变成结果图。
- 原因:裁扩结果由浏览器 canvas 本地渲染为
data:image/png。如果项目态先把local-resource-*图层加入画布,serializeLayer不会保存src,resolveProjectResourceCreateImageSrc又会跳过内联 Data URL,后续应用后端 project snapshot 或刷新 hydrate 时找不到对应editor_project_resource,该源图层就会被过滤。去背景带canvasCompletion时后端只负责把结果写入生成占位,不会恢复这个未资源化的裁扩源层。 - 处理:项目上下文中的裁扩结果必须在加入画布前先上传 OSS / asset object,再创建
editor_project_resource,并用服务端返回的resourceId/objectKey/assetObjectId创建裁扩图层;随后去背景的sourceResourceId和图片读取都指向正式资源。queue 模式下去背景完成后,如果首次读取的项目快照中对应generation-dialog仍是generating或缺少generatedLayerId,前端短暂等待后再读取一次项目快照。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand应覆盖裁扩先上传再创建项目资源,以及去背景队列完成后对未完成占位进行二次项目读取。 - 关联:
src/components/image-editor/useImageCanvasGenerationWorkflow.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布不能用 local-* 前缀代替资源登记状态
- 现象:刚加入画布、资源登记仍在途的图层被复制后,权威项目快照刷新与资源创建响应交错,副本可能刷新后消失;反过来,历史自包含角色动作序列虽然也使用
local-*,却会被永久禁用并持续提示“素材仍在保存”。 - 原因:
local-*同时覆盖两种不同状态:新素材的临时 ID,以及没有项目资源行、但已凭完整持久化帧成为终态的历史兼容序列。ID 前缀不是状态机;把所有本地 ID 当 pending,或把多个布局调用压进一个临时 ID single-flight Promise,都无法表达每次请求的真实生命周期与恢复上下文。 - 处理:新素材是否 pending 必须读资源登记在途集合;存在明确在途请求时,复制、剪切、创建副本和内部粘贴整体拒绝,正式
resourceId回填后开放。历史序列按结构化持久化的严格自包含谓词识别,不能显示保存中;暂不支持复制时用准确原因失败关闭。既非 pending 又不满足历史谓词的 unresolved local 图层也拒绝,但提示“资源尚未登记”而非“仍在保存”。不要把普通 layout PATCH pending 当资源登记状态,也不要阻止系统剪贴板图片导入。资源创建不再按临时 ID single-flight 合并;布局保存的串行 latest-wins 队列仍保留。 - 验证:
ImageCanvasLayerCommandModel.test.ts、useImageCanvasLayerCommands.test.tsx和ImageCanvasContextMenusView.test.tsx分别覆盖登记在途整体拒绝、正式 ID 回填后恢复、历史 self-contained local sequence 不误报保存中,以及 unsupported / unresolved 的准确提示;useImageCanvasProjectPersistence.test.tsx继续覆盖单个资源响应与权威快照交错恢复。 - 关联:
src/components/image-editor/ImageCanvasLayerCommandModel.ts、src/components/image-editor/useImageCanvasLayerCommands.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、docs/【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md。
图片画布修改图层标签不能通过创建资源和换绑实现
- 现象:用户只修改一个图层的素材类型,项目资源数量却增加且该图层的
resourceId改变;如果修改请求完成前复制,原图层会换绑到新资源,副本仍引用旧资源,刷新后副本类型回退。 - 原因:把资源
assetKind同时当作共享默认值和布局实例标签,只能通过按类型查找 / 创建资源来模拟局部修改。异步响应只知道原layerId,无法自动追踪期间复制出的新布局实例;layout 又没有独立覆盖字段,最终形成资源身份漂移和类型错位。 - 处理:固定双层模型:
editor_project_resource.asset_kind是资源默认类型,editor_canvas_layer.asset_kind_override是可空布局覆盖,effective 值为override ?? resource default。图层标签动作只写 / 清除 override,保持资源行数量和resourceId不变;复制复用resourceId并复制 override。asset_kind_override必须是追加在表末尾、默认None的 typed 字段,不能塞入item_json;schema 同步 migration、表目录、bindings、DTO 和 canonical hash。 - 验证:覆盖“修改标签不新增资源且不换 ID”“同资源两个图层可有不同 override”“复制保留 override 后可独立修改”“清除 override 恢复资源默认值”“刷新与 structured round-trip 不丢覆盖”,并运行
npm run spacetime:generate、npm run check:spacetime-schema、定向 Rust / API / 前端测试。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/spacetime-module/src/migration.rs、src/components/image-editor/useImageCanvasProjectPersistence.ts、src/services/image-editor/editorProjectClient.ts。
图片画布项目封面上传失败要有本地展示兜底
- 现象:画布项目已反复打开、保存或操作,但
/project列表卡片仍只显示“项目”占位,没有封面图。 - 原因:项目封面快照需要先在浏览器生成 Blob,再上传 OSS 并创建
assetKind: "project-cover-snapshot"项目资源;本地 dev 或 OSS CORS 异常时,Blob 生成成功但上传失败,服务端不会产生正式封面资源。 - 处理:服务端
project-cover-snapshot仍是跨设备正式封面;前端在生成封面 Blob 后立即把 Blob 以项目 ID 写入 IndexedDB,仅作为当前浏览器展示兜底。项目列表读取时优先使用服务端封面资源,其次使用本地 IndexedDB 封面,最后才退回可见画布图层或占位。IndexedDB 兜底不得写入项目快照、不得进入editor_project_resource,也不得替代 OSS / asset object 正式持久化。 - 封面是展示派生物,不是 layout 真相。常规编辑只在项目加载和原有 layout 保存触发点采样当前
canvasSize,不监听 ResizeObserver 尺寸变化单独增加保存频率;但用户主动返回项目页时必须先 flush 最新权威 layout,并等待同一视口封面写入 IndexedDB 和正式项目资源后再导航。为避免移动端、窄窗口或首次尺寸尚未稳定时取景过小,以当前视口中心为锚点把取景宽高至少扩大到1280x960;实际值更大时保留更大值。画布存在 drawable 图层但当前取景全部离屏时,要保存纯背景封面,不能因相交列表为空而保留旧缩略图。 - 封面生成不要为同一 OSS 对象发起另一套换签缓存维度:图片、序列帧和 poster 分别复用主画布预览的 refresh key,保证封面取得相同 signed URL,由浏览器合并 in-flight 请求或命中 HTTP 缓存。通用素材上传里的
bypassCache: true只用于上传后立即预览;项目封面不消费该src,应在 confirm 后直接使用 object-only 结果创建项目资源。 - 验证:
npm run test -- src/components/project/ProjectCanvasCover.test.ts src/components/project/ProjectGalleryView.test.tsx src/components/image-editor/ImageCanvasProjectCoverSnapshotModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx覆盖服务端封面优先、本地缓存兜底、上传失败仍保留本地封面缓存、小视口居中扩大到1280x960以及大视口不缩小;浏览器 smoke 可在/project对没有服务端封面的项目写入genarrative-editor-project-coversIndexedDB 记录,刷新后应显示blob:封面图。 - 关联:
src/services/image-editor/editorProjectCoverCache.ts、src/components/project/ProjectGalleryView.tsx、src/components/project/ProjectCanvasCover.tsx、src/components/image-editor/useImageCanvasProjectPersistence.ts。
图片画布框选预览要复用源图换签缓存
- 现象:UI 设计素材提取或快速编辑框选时,画布上的红色框选还在,但底部“框选区域预览”卡片变成空白。
- 原因:预览图从原生
img改成ResolvedAssetImage后,如果没有传入源图同一套objectKey/refreshKey,它会另起一条/api/assets/read-url缓存维度;画布主图已经显示时,预览仍可能处于空签名或失败缓存状态。 - 处理:框选预览继续用
ResolvedAssetImage承接私有资源换签,但必须传源图objectKey,并使用taskId ?? resourceId作为refreshKey,和主画布图片保持同一签名缓存版本。只允许对data:、blob:或已带签名参数的 URL 设置fallbackSrc;不要把裸/generated...私有路径作为 fallback 写进img。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasUiAssetExtractionOverlayView.test.tsx --reporter=dot应断言私有框选预览带objectKey、refreshKey,且裸 generated 路径没有 fallback。 - 关联:
src/components/image-editor/ImageCanvasUiAssetExtractionOverlayView.tsx、src/components/ResolvedAssetImage.tsx、src/hooks/useResolvedAssetReadUrl.ts、src/services/assetReadUrlService.ts。
图片画布发布入口 429 先查自动保存 PATCH 并发
- 现象:发布域名访问画板时出现短时间密集
429,Nginx access log 中PATCH /api/editor/projects/<projectId>、生成接口和资料接口混杂,429 行常见request_time=0.000、upstream_status=-,error log 写limiting connections by zone "genarrative_api_conn"。 - 原因:这类 429 是入口 Nginx
limit_conn在转发前拒绝,不是 api-server、SpacetimeDB、worker 或 VectorEngine 的业务 429。画布自动保存如果只有防抖、没有 in-flight 串行保护,慢PATCH /api/editor/projects/{projectId}未完成时,拖拽生成器、资源回填和后续状态变化会继续发起新的保存请求,同一客户端连接数被长请求撑满后触发入口连接限流。 - 处理:不要先放大 Nginx 限流或把错误归给生成 provider;先看 access log 的
upstream_status/request_time和 error log 的limit_connzone,再查前端保存路径。useImageCanvasProjectPersistence中自动保存和资源创建后的布局保存必须共用串行队列:同一时刻只允许一个saveEditorProjectLayoutin-flight,期间新快照覆盖旧待保存快照,当前保存结束后只发送最新一次。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx -t "serializes project layout saves" --reporter verbose应覆盖慢保存期间不启动第二个 PATCH,首个保存完成后只发送最新待保存快照;排查发布现场时 429 行应从upstream_status=-/ Nginxlimit_conn收敛。 - 关联:
src/components/image-editor/useImageCanvasProjectPersistence.ts、src/components/image-editor/useImageCanvasProjectPersistence.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布发布入口 429 也要查 read-url 换签爆发
- 现象:发布域名刚上线或刷新画板后出现短时间
429,Nginx access log 中集中为同一 IP / 同一editor/canvas?projectid=...referrer 的GET /api/assets/read-url?objectKey=generated-character-drafts/editor/ui-design-assets/.../asset-001.png到几十上百个 UI 设计切片;429 行常见request_time=0.000、upstream_status=-,error log 写limiting requests ... zone "genarrative_api_rps"。 - 原因:这类 429 是入口 Nginx
limit_req在转发前按 RPS burst 快拒,不是 api-server、SpacetimeDB、worker 或 VectorEngine 的业务 429。UI 设计提取、角色动画帧或大量私有素材恢复会让多个ResolvedAssetImage同时挂载;如果/api/assets/read-url只有同 key pending 去重和缓存,没有跨 objectKey 节流,一个页面能在同一秒内发出数百个不同 objectKey 换签请求并打满genarrative_api_rpsburst。 - 处理:不要先放大 Nginx 通用 API 限流;先按 access log 聚合
read-url数量、状态和 referrer,确认是否同一画板页面触发。assetReadUrlService必须统一承接私有 generated 资源换签,并在真实请求前做跨组件轻量节流;画板、素材库、运行态和结果页不得直接绕过该服务调用/api/assets/read-url。 - 验证:
npm run test -- src/services/assetReadUrlService.test.ts --reporter verbose应覆盖大量不同 objectKey 同时换签时首批限量放行、后续按间隔派发;发布现场同类页面刷新时,NginxGET /api/assets/read-url429 应从upstream_status=-/genarrative_api_rps收敛。 - 关联:
src/services/assetReadUrlService.ts、src/hooks/useResolvedAssetReadUrl.ts、src/components/ResolvedAssetImage.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器 Seedance 2.0 参考媒体只提交稳定引用
- 现象:画板生成视频选择 Seedance 2.0 并上传参考视频后,请求体暴涨、可能返回
413或上游拒绝video_url.url;文档示例或测试如果写data:video/mp4;base64,...,后续实现很容易照抄。 - 原因:画板生成会进入持久队列,Base64 / Data URL / Blob URL 会放大请求和任务 JSON;直接签名客户端给出的 objectKey 又会绕过跨账号素材归属校验。项目资源 ID、素材 ID 和 objectKey 必须先解析到当前 owner 的正式对象,公网 URL 与
asset://才能按供应商契约直接透传。 - 处理:本地参考图片 / 视频 / 音频先走
/api/assets/direct-upload-tickets直传 OSS,再/api/assets/objects/confirm确认;前端保留 signed URL 做预览,提交生成时使用objectKey、项目资源 ID 或素材 ID。后端归一化拒绝全部data:*/blob:*,签名 OSS URL 前统一校验 owner;稳定引用字段总长度限制为256KB,非 Seedance 模型携带参考字段也必须拒绝;Ark body 显式带generate_audio:false。 - 验证:
npx vitest run src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/services/image-editor/editorReferenceUploadClient.test.ts --reporter verbose;cargo test -p api-server editor_video --manifest-path server-rs/Cargo.toml;cargo test -p shared-contracts editor_video_request_supports_seedance_multimodal_references --manifest-path server-rs/Cargo.toml。 - 关联:
src/services/image-editor/editorReferenceUploadClient.ts、src/components/image-editor/useImageCanvasUploadWorkflow.ts、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、server-rs/crates/api-server/src/character_animation_assets.rs、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片编辑器生成类菜单要挂到页面级 portal
- 现象:底部
生成规范菜单、角色面板里的角色规范来源菜单点击后像没有弹出来,实际被按钮所在的局部滚动容器挡住了。 - 原因:菜单仍然渲染在底部工具栏或参考图横向滚动行内部,父容器带
overflow,弹层无法越出边界;即便挂到 portal,如果菜单根节点的pointerdown继续冒泡到画布视口,也会先触发画布失焦并卸载面板,导致菜单项click前消失。 - 处理:这类轻量菜单统一用页面级 fixed portal 挂到
document.body,位置根据触发按钮的getBoundingClientRect()计算;PlatformFloatingMenu根节点必须阻止pointerdown冒泡,避免画布清空当前生成面板;底部 AI 工具栏在生成面板打开时仍保持可见,不要整栏隐藏。 - 验证:测试断言菜单不包含在底部工具栏 / 参考图行里,并且生成面板打开时底部
AI画布工具栏仍存在;规范参考图来源菜单应能通过 portal 点击“从画布中选择 / 上传图片”并写回规范参考图。 - 关联:
src/components/common/PlatformFloatingMenu.tsx、src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。
图片编辑器 portal 菜单必须显式继承画板主题 token
- 现象:生成视频参数面板里点击“静音”后,开关轨道和白色滑块一起消失;如果直接把轨道改成
#00ff00,虽然重新可见,却变成与画板主题不一致的荧光绿。相同比例、清晰度、slider、时长文字和模型选中勾选也可能丢失选中态主题。 - 原因:
renderEditorPortal(...)把.image-canvas-editor__portal-menu挂到document.body,它不再是.image-canvas-editor的后代,无法继承只定义在编辑器根节点上的--image-canvas-brand-*自定义属性。浏览器会把依赖缺失变量且没有 fallback 的声明按无效值处理,轨道背景最终为透明。 - 处理:portal 继续挂到
document.body以避免局部overflow裁切,但外层必须通过.image-canvas-editor__portal-theme同步当前platform-theme--light / platform-theme--dark;画板品牌 token 由.image-canvas-editor、主题桥接层与.image-canvas-editor__portal-menu共用同一组声明。控件继续消费主题变量,不使用单点硬编码颜色,也不要只给静音轨道补 fallback 而遗漏同一 portal 内其它 token 消费者。 - 验证:
scripts/image-canvas-portal-theme.test.ts应锁定编辑器根节点、portal 主题桥接层与 portal 菜单共享完整品牌 token,静音 pressed 轨道仍使用var(--image-canvas-brand-accent)且不出现#00ff00;useImageCanvasGenerationSurface.test.tsx应覆盖暗色主题 class 被桥接到document.body下的 portal。真实浏览器从生成视频 -> 视频参数 -> 静音点击后,轨道 computed background 应为非透明当前主题色,portal 内--image-canvas-brand-accent、--image-canvas-brand-border-strong和--image-canvas-brand-soft均应有值。 - 关联:
src/index.css、scripts/image-canvas-portal-theme.test.ts、src/components/image-editor/ImageCanvasEditorPortal.tsx、src/components/image-editor/useImageCanvasGenerationSurface.tsx、src/components/image-editor/ImageCanvasGenerationComposerView.tsx。
图片编辑器规范图片面板不要脱离统一生成 shell
- 现象:生成 UI 设计图或新建图标规范时,面板参考图、输入区和底部生成按钮相对生成图片 / 生成角色 / 生成视频错位;图标规范甚至可能缺少首行参考图入口。
- 原因:规范、UI 设计图等面板虽然都属于生成类入口,但 JSX 和 CSS 曾各自维护
spec-footer、局部 field wrapper 或缺省参考区,导致后续改造只覆盖普通图片 / 角色 / 视频,规范图片类面板结构漂移。 - 处理:生成规范下的角色规范、图标规范、自定义规范,以及生成 UI 设计图,都必须复用
image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image外层 shell;首行统一image-canvas-editor__generation-ref,底部统一image-canvas-editor__generation-composer-footer+image-canvas-editor__generation-submit。多字段内容只在中央字段区保持紧凑,不单独发明 footer 或省略参考区。 - 验证:
npm test -- src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx -t "生成UI设计图|生成规范|visible titles|图标规范|character spec"。 - 关联:
src/components/image-editor/ImageCanvasGenerationComposerView.tsx、src/index.css、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片编辑器生成占位图在生成中也要使用最新拖拽位置
- 现象:用户在图片编辑器里提交生成后继续拖动画布占位图,预览框可以移动,但生成完成后的真实图片仍落回提交瞬间的旧位置。
- 原因:生成提交函数闭包里保存了旧的
dialog.placeholder快照;如果完成回包仍用这个快照创建图层,就会丢失生成中期间的拖拽坐标。若handleGenerationFramePointerDown又按status === 'generating'拦截,则生成中占位图完全不能拖动。 - 处理:生成占位图的 pointer down 不因
generating禁止;普通图片、规范图、角色图和图标素材回包创建图层时,都从当前generateDialogRef.current.placeholder读取最新占位位置,失败后保留的占位图也继续走同一拖拽链路。 - 验证:
npm test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps the generation placeholder draggable while the image is generating"。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布 Lovart 新生成占位必须避让已有图层和占位
- 现象:用户在画布中心已有图片时继续点击“生成图片 / 生成视频 / 生成规范”等入口,新建的待生成占位压在已有图片或其它待生成占位上;生成完成后看起来像图片被覆盖或丢失。
- 原因:入口直接把 placeholder 放在当前视口中心,没有把已有图层、隐藏状态和 inactive generation dialog 的占位统一纳入避让计算,也没有在落点确定后把 viewport 平移到新占位中心。
- 处理:所有会创建 generation dialog 的入口都必须走
ImageCanvasGenerationPlacementModel,避让所有hidden !== true的图层和 active / inactive placeholder;按 32px 世界坐标间距外扩阻挡矩形,在候选点中选择距离当前屏幕中心对应画板位置最近且不重叠的位置,再调用centerViewportOnPlacement(...)保持缩放只平移。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationPlacementModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。 - 关联:
src/components/image-editor/ImageCanvasGenerationPlacementModel.ts、src/components/image-editor/useImageCanvasGenerationWorkflow.ts、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片画布生成类 composer 打开后必须自动进入可见安全区
- 现象:生成器、快速编辑、裁扩或角色动作面板打开后,面板可能在当前画布视口外,或被底部工具栏 / 左下 dock 盖住,用户只看到一部分甚至完全看不到输入框。
- 原因:placement 只负责选择画布世界坐标里的占位落点,面板实际 DOM 宽高、
translateX(-50%)、移动端 fixed 样式和工具栏覆盖区域没有反向修正 viewport。 - 处理:所有画布内 composer / 面板渲染后统一走
resolveViewportForOverlayVisibility(...),用真实 DOM 矩形和工具栏安全边界只平移 viewport;新增入口不要在各自按钮 handler 里写独立偏移。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasGenerationSurface.test.tsx。 - 关联:
src/components/image-editor/ImageCanvasOverlayModel.ts、src/components/image-editor/useImageCanvasGenerationSurface.tsx、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片画布改造按原 action 生成新产物,快速编辑不要新建生成器
- 现象:用户点击图片素材的“快速编辑”后,画布上额外出现
Quick Edit Generator占位,像是新建了一个生成器;但用户预期是在原图下方框选区域、填写一个提示词和模型,然后直接修改当前图。 - 原因:快速编辑入口和提交链路误用了
createQuickEditGenerationDialogDraft(...)/CanvasGenerationDialogState,把“覆盖源图”的快速编辑伪装成会产出新图层的生成器占位。 - 处理:图片快速编辑必须走
QuickEditPanelState,打开时归档当前 active generation dialog 但不创建新的mode="quick-edit"dialog;提交时调用/api/editor/images/edits,主来源始终使用当前图片已登记的resourceId或sourceAssetId。带编号标注的图片上传后只作为辅助referenceImageSrcs,不能替换主来源身份;成功后覆盖源图,失败时保留快速编辑面板。快速编辑任务进入generating后必须移除框选工具和覆盖层,禁止继续新增框选;失败恢复面板后可继续调整框选再重试。image.edit结果不允许改造;其它可改造产物按原action恢复 generation dialog 并生成新产物,去背景等异步入口继续使用各自现役 dialog / placement 链路,不恢复独立 quick-edit 改造面板。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx -- --runInBand,以及按需运行npm run test -- src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx -t "快速编辑|quick edit" -- --runInBand。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/useImageCanvasGenerationWorkflow.ts、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/services/image-editor/editorImageReference.ts。
图片画布快速编辑完成必须按目标图层回写
- 现象:图片快速编辑任务成功后,刷新页面素材库能看到新图,但画布上的源图没有替换。
- 原因:
/api/editor/images/edits只保存生成图、项目资源和素材;没有canvasCompletion时不会写editor_canvas.layers_json。sourceResourceId只能表示溯源,同一资源可出现在多个图层,不能用它来决定替换哪一层。 - 处理:图片快速编辑请求必须传
targetLayerId;后端在没有canvasCompletion的快速编辑完成分支里,用目标 layer id 和生成资源写回项目 layout。 - 验证:
npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand;后端验证至少覆盖editor_image_edit_request_omits_price_mud_points和editor_image_edit_can_complete_by_replacing_target_layer。 - 关联:
src/services/image-editor/editorProjectClient.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、server-rs/crates/api-server/src/editor_project.rs。
图片画布快速编辑尺寸要区分用户目标和 provider 对齐尺寸
- 现象:原图经过快速编辑后 Resolution 变成近似比例的 1K / 2K 预设;原图或框选标记图宽高不是 16 的倍数时,VectorEngine edits 直接拒绝请求。
- 原因:前端已有源图精确
originalWidth/originalHeight,提交时却按最近常用比例和 K 档重新计算size;后端又把非 16 倍数的目标尺寸和原始参考图字节直接放进 multipart,并以 provider 回图宽高落库和覆盖画布图层。 - 处理:画布快速编辑展示与常规图片生成一致的模型、比例和尺寸参数,默认继承来源生成器参数;缺少来源生成器时使用图层模型,并按真实分辨率推导比例和尺寸。用户当前选定的比例和尺寸共同决定业务目标分辨率,允许覆盖源图旧分辨率;api-server 只在 provider 边界向右、向下复制边缘像素,把每张参考图和目标尺寸临时补齐到 16 的倍数,收到回图后裁回业务目标尺寸再持久化。若上游异常返回其他尺寸,先按目标比例裁切缩放;临时对齐尺寸不能进入 OSS 元数据、
editor_project_resource、editor_asset或画布 Resolution。结果覆盖目标图层时更新原始分辨率,并保持图层中心位置不跳动。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx覆盖来源参数继承、模型参数切换、目标尺寸提交和图层回填;cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml覆盖图标类拒绝、provider 尺寸对齐和回图恢复。 - 关联:
src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/ImageCanvasGenerationLayerModel.ts、server-rs/crates/api-server/src/editor_project.rs。
图片画布生成中占位必须同步业务目标尺寸
- 现象:用户选择
2K生成或从 2K 普通图、角色图再次改造时,最终成品仍是 2K,但待生成 / 生成中的灰色框保持 1K 大小,完成后突然放大;UI 素材提取也可能始终显示 512 方框。 - 原因:同来源改造先按默认 1K 创建 draft,再只恢复
imageModel / aspectRatio / imageSize,没有重新计算 placeholder;UI 提取和旧修改入口则分别写死图标展示尺寸与1024x1024。 - 处理:所有共享图片参数恢复和面板比例 / 清晰度切换都经过
resizeGenerationPlaceholderToImageSelection(...),保持占位中心不变并同步width/height/originalWidth/originalHeight;UI 提取按resolveUiAssetExtractionGenerationPlan(...)的 1K / 2K 计划计算占位,旧修改入口使用源图真实 Resolution。快速编辑覆盖源图,不另建生成占位,仍按目标尺寸更新原图层。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx --reporter=dot。 - 关联:
src/components/image-editor/ImageCanvasGenerationModel.ts、src/components/image-editor/ImageCanvasGenerationDialogModel.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布快速编辑模型必须在后端选择正确的 provider 协议
- 现象:快速编辑继承或选择
nanobanana2后,上游返回not supported model for image generation;图集开放快速编辑后尤其容易触发。 - 原因:前端把
gemini-3.1-flash-image-preview正常提交到/api/editor/images/edits,但后端无条件使用只支持gpt-image-2的 VectorEngine/v1/images/editsmultipart 协议。 - 处理:快速编辑请求同时提交
model / aspectRatio / imageSize。api-server 归一模型后分流:nanobanana2使用/v1beta/models/{model}:generateContent,把原图和参考图放入inline_data,并传递generationConfig.imageConfig;gpt-image-2继续使用/v1/images/editsmultipart 和 16 像素 provider 边界对齐。nanobanana2 保留 provider 输出几何尺寸,不套用 GPT edits 的像素恢复。 - 验证:
npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx覆盖前端参数提交;cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml覆盖模型分流、尺寸档位计费与 GPT 对齐恢复。 - 关联:
src/services/image-editor/editorProjectClient.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/api-server/src/openai_image_generation.rs。
图片画布纯尺寸变换必须先在内存决策再单次上传
- 现象:
nanobanana2已返回并成功解码图片,任务随后报“尺寸无效”;普通生图若把尺寸恢复作为硬失败,provider 已成功回图后仍可能没有素材进入素材库和画布。 - 原因:通用尺寸恢复把
nanobanana2的标量清晰度档位512 / 1024 / 2K当成WIDTHxHEIGHT解析;普通图片与快速编辑还把 OSS 持久化放在尺寸恢复之后。同步generateContent返回的是内联 base64,本地兜底 task id 不能用于向 provider 回查原图。 - 处理:
nanobanana2保留 provider 输出几何尺寸;其它模型仍按显式像素目标尝试恢复。普通生图和快速编辑先把 provider 回图留在内存,尺寸恢复成功后只上传变换结果,恢复失败则降级为只上传 provider 原图;每个主结果只执行一次 OSS 持久化并只创建一个素材。角色、图标图集、UI 提取和角色动作的 provider 原始输出按多产物语义单独保留并承载任务模型成本,后续抠图、逐帧处理和切片阶段成本为 0;中间产物沿用character、icon-spritesheet、character-animation等真实类型,不新增“原图类型”。扣费确认仍以 provider 成功为界,不延长到 OSS、后处理或画布回填;后台按任务显示最终产物父行,并把每个中间产物作为独立子行展开,分别展示阶段生成器和阶段成本。 - 验证:
cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml覆盖 nanobanana 标量尺寸、变换失败回落 provider 原图、变换先于单次持久化;cargo test -p api-server character_animation_assets --manifest-path server-rs/Cargo.toml覆盖角色动作原始预览的真实类型和成本归因;npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx覆盖中间产物逐行展开和成本文案。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx。
图片画布快速编辑元数据必须记录原图引用
- 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
- 原因:信息面板直接渲染
generationInputs.references;快速编辑虽然以sourceReferenceId指定原图,但如果buildQuickEditGenerationInputs(...)不把该业务 ID 写成引用,后端资源和画布层都没有可展示的原图引用。 - 处理:快速编辑的
generationInputs.references必须始终包含原图,再追加用户额外参考图;关闭额外参考图入口时也不能删除这条源图引用。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx -- --runInBand。 - 关联:
src/components/image-editor/ImageCanvasGenerationModel.ts、src/components/image-editor/ImageCanvasMetadataModalView.tsx、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts。
图片编辑主来源不能接受 objectKey 或请求类型
- 现象:调用方可把 objectKey、URL 或 Data URL 当作主来源,再用请求
assetKind或另一个允许编辑的目标图层为禁止类型“借壳”;无目标图层时,后端还会扫描账号全部项目和素材库。 - 原因:HTTP DTO 同时承担外部请求与队列载荷,来源身份、存储定位和类型真相混在
sourceImageSrc/sourceResourceId/assetKind中;worker 没有按业务 ID 复核入队后的身份漂移。 - 处理:站内与 External v1 API 调用方只提交必填
sourceReferenceId,且只接受当前账号项目资源 ID 或素材 ID;上传对象必须先登记。后端按两张表主键分别窄查,双表同 ID 时失败关闭,objectKey 仅作为服务端解析结果。目标绑定优先比较双方assetObjectId,缺失才比较 canonical(bucket, objectKey),并校验双方默认类型一致。队列保存版本化解析快照,worker 执行前再次定点解析;旧任务只把既有资源 ID 或旧来源字符串本身作为业务 ID 尝试迁移,禁止 objectKey 反查和旧assetKind真相回退。 - 验证:覆盖资源 ID、素材 ID、双表冲突、跨账号、raw objectKey/URL/Data URL/Blob URL、旧字段、禁止类型、目标对象与类型冲突、快照漂移、旧任务迁移、红框图辅助引用,以及 Canvas Agent 缺少
reference_id。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/api-server/src/external_generation_worker.rs、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、docs/openapi/genarrative-external-v1.openapi.json。
图片画布生成完成应用项目快照后也要刷新素材库
- 现象:部分素材生成成功后画布上已经出现结果,但左侧素材库没有立刻出现新素材,刷新页面后才显示。
- 原因:生成接口带
project快照时,前端只调用applyProjectSnapshot(...)刷新画布布局;左侧素材库状态仍停留在首次loadEditorAssetLibrary()的结果。只有少数图片分支手动upsertGeneratedAsset,图标素材图集、视频、音频、排队完成后重新loadEditorProject等分支不会统一更新素材库。 - 处理:素材库 hook 必须提供显式
refreshAssetLibrary();传给生成工作流的项目快照应用函数应包装为“先应用项目快照,再刷新素材库”。新增生成分支不要在各自分支散落刷新逻辑,除非是无项目快照的本地图层回填,才继续使用generatedAssetSnapshot/upsertGeneratedAsset。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx -t "refreshes the asset library after an icon generation project snapshot is applied" -- --runInBand;npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx -- --runInBand。 - 关联:
src/components/image-editor/useImageCanvasAssetLibrary.ts、src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts。
Windows 本地 dev 不要把 RUSTC_WRAPPER 绕过写成 rustc
- 现象:Windows 上执行
npm run dev:api-server时,api-server 在 Cargo 启动阶段失败,日志出现error: multiple input filenames provided (first two filenames are ... rustc.exe and -),/healthz无法访问。 - 原因:
server-rs/.cargo/config.toml默认配置rustc-wrapper = "sccache";本地 dev 脚本为了绕过损坏的 sccache 需要覆盖 wrapper。Windows 下如果把RUSTC_WRAPPER设置为rustc,Cargo 会按 wrapper 协议调用rustc <真实rustc路径> - ...,真实 rustc 把 wrapper 传入的 rustc 路径和 stdin-都当输入文件。 - 处理:Windows 本地 dev 脚本应把
RUSTC_WRAPPER和CARGO_BUILD_RUSTC_WRAPPER显式设为空字符串,让 Cargo 覆盖项目配置并直连真实 rustc;Linux 保持/usr/bin/env绕过 sccache。 - 验证:
npm run test -- scripts/dev.test.ts -t "Windows 下本地 dev Rust env 用空 wrapper 覆盖项目 sccache",并用npm run dev:api-server拉起后访问实际 api 端口的/healthz返回 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Pingora 直连 80/443 不能只改 env
- 现象:
/etc/genarrative/pingora-gateway.env已把GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN/HTTP_REDIRECT_LISTEN改到0.0.0.0:443/0.0.0.0:80,但genarrative-pingora-gateway.service启动失败,日志出现低端口绑定权限错误。 - 原因:默认 service 用非 root
genarrative用户运行,并且主模板为了保持 shadow 安全边界不带CAP_NET_BIND_SERVICE。低端口直连必须通过显式 systemd drop-in 单独授予 capability;同时 Certbot 私钥默认未必允许genarrative读取,Nginx 也可能仍占用80/443。另一个常见误区是 API release 只带pingora-direct-enable.sh/ rollback 壳脚本,却漏带pingora-current-release-audit.mjs、pingora-direct-rehearsal-status.mjs、check-pingora-direct-preflight.mjs、check-pingora-direct-live.mjs、deploy/systemd/、deploy/env/或deploy/pingora/,导致从/opt/genarrative/current启用时依赖 Jenkins 工作区、源码 checkout 或/etc里某份参考模板;或者 release 已经包含新版pingora-gateway,但已运行的 shadow / canary / direct service 没有随current链接切换重启,仍在跑旧二进制。Jenkins API Build、API Deploy 和 Full Build-And-Deploy 默认要求 Pingora 产物,并用--require-pingora-gateway在部署阶段硬校验;手工本地 API 包仍需显式--include-pingora-gateway才会把二进制、checksum 和 manifest artifact 写入发布包。Server-Provision 安装到/etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf的 drop-in 只用于人工审阅和显式覆盖;直连启用脚本默认必须读取 current release 随包deploy/systemd/genarrative-pingora-gateway-direct-entry.conf,否则旧/etc模板会掩盖发布包缺失。API deploy 脚本本身也不能继续用部署工作区根部的scripts/deploy/production-api-deploy.sh,否则 Jenkins workspace 里的脚本会掩盖build/<version>发布包缺少 deploy / maintenance 同目录脚本的问题;备份脚本、健康巡检脚本和 env 示例目录同样不能从部署工作区兜底,切换命令证据脚本也不能从部署工作区兜底,否则 current release 会和上游构建归档漂移。Pingora 直连依赖、备份脚本、巡检脚本、env 示例目录和 API deploy 执行入口都必须来自上游发布产物;随包api-server.sha256和可选pingora-gateway.sha256也必须复制进 current release,供随包 current release 自审校验二进制;随包deploy/pingora/pingora-gateway.env.example也不能只检查存在,还要保持 gzip-only、不信任 XFF、前置代理确认关闭、接流保护开启和空 probe token 这些生产安全默认值;production-api-deploy.sh发现缺失时应在 current 切换前 fail-fast、清理 staging 并退出本次打开的维护模式,不应从部署工作区兜底补齐;所有 API 发布包都必须携带release-manifest.json且登记api-serverartifact,发布包包含 Pingora 时还必须登记pingora-gatewayartifact,否则 deploy 应在切换 current 前失败;deploy 必须要求 release root、current link 和 api env file 都是绝对路径,release version 以数字或字母开头并拒绝点目录,再先写 staging release,全部复制完成后用非合并语义提升为正式 release,失败时清理 staging 且不留下正式 release,同版本 release 已存在、提升前竞态出现或 current 路径不是符号链接时拒绝覆盖 / 合并,避免旧文件混入 current;发布包包含 Pingora 时,deploy 必须先确认 systemd 最终配置没有 direct-entryCAP_NET_BIND_SERVICE、env 仍是127.0.0.1:18081shadow 且未配置TLS_LISTEN/HTTP_REDIRECT_LISTEN,再提升 release、切换 current 并restartPingora shadow;配置不安全时必须在切换 current 前失败并退出本次打开的维护模式,current 切换后的 readiness / 服务重启失败仍保留维护模式。 - 踩坑补充:Bash 的进程替换
< <(...)不会自动把生产者子进程的失败状态传给消费循环。Pingora systemd 检查若在子进程发现CAP_NET_BIND_SERVICE后直接退出,父函数仍可能继续用空列表输出“缺少 EnvironmentFile”,外层命令替换又继续用空 env 输出“LISTEN 为空”,形成三条互相矛盾的错误。部署前检查必须先捕获并显式检查配置提取命令的退出状态,再解析 EnvironmentFile;首错失败后立即返回。回归用npm run check:production-api-deploy的 direct-entry fixture 同时断言后两条误报不存在。 - 踩坑补充:旧 release 可能没有
pingora-gateway二进制,但 systemd 仍残留历史direct-entry.conf,同时 Nginx 已正常接回80/443、Pingora inactive、env 已是 shadow。此时不要直接执行当前pingora-direct-rollback.sh --apply:脚本删除 drop-in 后会固定重启 Pingora,因 current 二进制不存在而中止,后续 Nginx reload/smoke 不会执行。先确认 current 确实无可执行网关、Pingora inactive、env 已完整恢复 shadow、Nginx 配置与公网 smoke 正常,再以单次 fail-fast 运维命令删除 stale drop-in、daemon-reload、复核 capability/DropInPaths 已清空,随后 reload(若 inactive 则 start)Nginx,并复核 Nginx/API/SpacetimeDB、正式 vhost smoke 和 health patrol nginx 模式;不要伪造--require-pingora-shadow验收。下一次包含 Pingora artifact 的 API Deploy 会在切换 current 后启动新 shadow 网关。 - 处理:确认真实 TLS 证书和 redirect env 已写入
/etc/genarrative/pingora-gateway.env、service 模板和systemctl cat最终配置读取的EnvironmentFile=都包含这份 env、当前执行用户和genarrative-pingora-gateway.service的User=服务用户都能读取证书链 / 私钥、current release 的pingora-gateway已存在且可执行、Nginx 或其它进程已释放80/443后,先用npm run plan:pingora-direct-cutover -- --require-direct ...生成只读 JSON runbook,并逐条审阅 Host 与回退巡检入口确认、current release 自包含自审、current release preflight、启用前基础 readiness、direct enable dry-run、direct enable apply、启用后--require-direct复核、rollback dry-run、rollback apply、回退后 health patrol 切回 Nginx 并恢复切换前 public base URL / Host、回退后 health patrol env 复核;runbook 只用于审阅,不修改系统。正式 runbook 中--direct-redirect-host、--rollback-nginx-smoke-host和--direct-host必须使用同一 hostname,只允许端口不同,避免 redirect 或回退 smoke 各自验证到不同入口;同时必须提供--rollback-health-patrol-public-base-url <切换前Nginx巡检入口>,若切换前 Nginx 巡检需要 Host 覆盖,再追加--rollback-health-patrol-public-host <切换前Host>,确认步骤会展示回退后要恢复的 public base URL / Host,避免回退 runbook 把现场巡检入口覆盖成仓库默认值;如需把回退后 Pingora shadow 探针复核纳入 runbook,追加--rollback-pingora-shadow-probe-url/--rollback-pingora-shadow-probe-token,JSON 输出会隐藏 token 原文。随后先执行/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show,再 dry-run/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --no-status,最后执行/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --preflight-check-cert-readable --preflight-check-service-env-file --preflight-check-service-user-cert-readable --preflight-check-service-binary-executable --preflight-check-ports-free --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,由脚本先跑 direct preflight,再安装 drop-in、reload systemd、重启 Pingora,并用systemctl cat核验 capability 和EnvironmentFile=/etc/genarrative/pingora-gateway.env已生效、用systemctl show ... ExecStart核验最终 service 仍指向随包主 service 模板里的 current releasepingora-gateway、用systemctl is-active确认服务 active,再以 JSON 模式执行 direct live smoke,验证 HTTPS / HTTP redirect / ACME / WSS 101 和 Pingora access log request_id 落盘,并要求direct-access-log结构化结果matchedCount == checked、missingCount=0、mismatchCount=0;如果 direct live 退出 0 但缺少该结构化证据,也必须视为启用失败。直连启用后同步调整/etc/genarrative/health-patrol.env:设置GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct,本机打127.0.0.1时设置GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>,否则巡检会继续按 Nginx 模式误报。验证失败时执行/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>'或npm run deploy:pingora-direct-rollback -- --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>';回退脚本先跑nginx -t,通过后才移除 drop-in、reload systemd、重启 Pingora,并用systemctl cat核验 capability 已移除、用systemctl show ... ExecStart核验最终 service 仍指向随包主 service 模板里的 current releasepingora-gateway,随后 reload Nginx、确认 Nginx service 仍为 active,并用 curl smoke URL 证明公网入口已回到 Nginx;回退 smoke URL/body 必须来自切换前真实 Nginx 入口,不要继续用固定http://127.0.0.1/healthz与"ok":true;回退脚本--apply不允许省略--reload-nginx或--nginx-smoke-url,当 smoke URL 指向本机地址时必须同时提供--nginx-smoke-host <域名>,且 host 值不能包含 URL、路径或查询;回退后把 health patrol gateway mode 改回nginx,恢复切换前 public base URL / Host,并用node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs --env-file /etc/genarrative/health-patrol.env --expected-gateway-mode nginx --expected-public-base-url <切换前Nginx巡检入口> --require-empty-public-host复核;若切换前 Nginx 巡检需要 Host 覆盖,则把--require-empty-public-host换成--expected-public-host <切换前Host>。若 env 已在回退命令前切回 Nginx,也可给 rollback 脚本追加--health-patrol-env-file /etc/genarrative/health-patrol.env --health-patrol-expected-public-base-url <切换前Nginx巡检入口> --health-patrol-require-empty-public-host让它在 Nginx smoke 后自动复核;切换前 Nginx 巡检需要 Host 覆盖时把最后一项换成--health-patrol-expected-public-host <切换前Host>。若要同时证明 Pingora shadow 高端口仍活着,追加--pingora-shadow-probe-url http://127.0.0.1:18081/__genarrative_pingora/healthz --pingora-shadow-probe-token <token>,脚本会隐藏 token 并要求响应为gateway=pingora-shadow。 - 处理补充:不要直接 chmod
/etc/letsencrypt/live或archive来让 Pingora 读取证书;Certbot live 路径通常是 symlink,即使stat -L看起来是普通文件,父目录权限也会让非 rootgenarrative用户不可达。先用随包node -- /opt/genarrative/current/scripts/deploy/pingora-tls-cert-sync.mjs --apply --source-cert-file /etc/letsencrypt/live/<域名>/fullchain.pem --source-key-file /etc/letsencrypt/live/<域名>/privkey.pem --target-dir /etc/genarrative/pingora-tls/<域名>把证书同步到 Pingora 私有目录,再让GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE/TLS_KEY_FILE指向/etc/genarrative/pingora-tls/<域名>/fullchain.pem和privkey.pem。脚本默认 dry-run,--apply才写入,目标目录默认root:genarrative 0750,文件默认root:genarrative 0640,并拒绝符号链接目标目录或目标文件。 - 处理补充:不要在切换窗口手工编辑
/etc/genarrative/health-patrol.env的三项网关变量;使用node -- /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --apply --env-file /etc/genarrative/health-patrol.env --gateway-mode pingora-direct --public-base-url <直连HTTPS入口> --public-host <域名>切到直连,回退前用同一脚本传--gateway-mode nginx --public-base-url <切换前Nginx巡检入口>并按切换前记录选择--clear-public-host或--public-host <切换前Host>。脚本只改 gateway mode / public base URL / public Host,并立即复用随包 env 复核脚本,减少空 Host 和旧值残留;生产巡检、env 复核和 env 切换脚本读取的布尔 env 都必须是明确布尔值,非法值直接失败,不能把拼写错误当成 false;env 复核脚本的--env-file与 env 切换脚本的--env-file/--check-script必须是绝对路径且不能是文件系统根目录,也不能包含换行或 NUL;env 切换脚本写入的 public base URL / Host 同样不能包含换行或 NUL。Node 22 已内置--env-file启动参数,直接用node script.mjs --env-file ...或 shebang 执行.mjs --env-file ...都可能让 Node 抢走业务参数;所有这类命令都必须写成node -- script.mjs --env-file ...,或通过已内置node --的 npm script 执行。 - 踩坑补充:health patrol env 切换脚本必须先复核权限固定为
0600的临时目标 env 再写真实文件,真实 env 原子替换时保持原文件权限和 owner/group;如果随包 env 复核脚本失败,--apply应失败且真实 env 保持原样,避免“切换脚本失败但巡检配置已半改”的状态。--apply的--env-file必须直接指向真实普通文件,不能传符号链接;如果/etc/genarrative/health-patrol.env是链接,先确认真实目标路径后再传给脚本,避免替换链接本身或写入非预期目标。 - 踩坑补充:直连启用脚本的
--preflight-script、--direct-live-script、--current-release-audit-script、--template-path、--service-unit-path、--dropin-path和 env 文件参数都必须使用绝对路径;不要在切换窗口传相对脚本路径,否则会把 current release、Jenkins 工作区或现场 cwd 混在一起。--apply会在安装 direct-entry drop-in 前确认 current release 自审、direct preflight 和 direct live smoke 脚本存在,缺脚本时应先修发布包或复制链路,不要手工改成工作区相对路径绕过。启用脚本还会在任何自审、preflight、drop-in 写入或 systemctl 前拒绝 service、路径、URL、Host、probe token、access log、数据库名、tail 行数和 timeout 参数中的换行或 NUL 字符;遇到这类失败先修 runbook 参数来源或现场 env,不要手工绕过脚本。启用脚本还会拒绝符号链接形式的 drop-in 目录或 drop-in 目标文件,以及已存在但不是普通文件的目标;如果现场 systemd 目录被软链改写,应先修正真实路径,不要让脚本把低端口 capability 写入非预期位置。回退脚本--apply同样会在nginx -t和删除 drop-in 前拒绝 service、路径、Nginx smoke URL / Host / 响应片段、health patrol 复核参数、shadow probe URL / token 和二进制 override 中的换行或 NUL 字符,并拒绝符号链接 drop-in 目录 / 目标以及非普通 drop-in 目标;如果现场路径或参数异常,应先修正 systemd 路径、runbook 参数或现场 env,不要手工删 drop-in、绕过nginx -t或把删除 symlink 当成已回退真实低端口能力。回退脚本覆盖--nginx-binary或--curl-binary时也不要传./nginx、tools/curl这类相对路径;裸命令名可以走PATH,路径形式必须使用绝对路径。--nginx-smoke-url必须带http://或https://,不要只写 host/path,否则脚本会在移除 drop-in 前失败。 - 踩坑补充:回退到 Nginx 后不要只把
curl --fail/ HTTP 200 当作 Nginx 已接回的证据;正式 runbook 必须给 rollback dry-run / apply 显式传切换前真实 Nginx smoke URL 和响应体片段,例如--nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>'。不要继续用固定/healthz与"ok":true,否则要么误卡回退,要么只验证到了错误入口。 - 踩坑修正:上述
/healthz与"ok":true只能算旧示例,不再是正式 runbook 默认。dev 真实直连80/443测试确认回退 smoke 必须从切换前真实 Nginx 入口取样,例如https://dev.genarrative.world/与<!doctype html>;固定http://127.0.0.1/healthz可能返回 301/404 或命中错误 vhost。回退前还必须把/etc/genarrative/pingora-gateway.env从 direct 低端口配置恢复为 shadow 高端口配置,否则回退脚本移除 capability 后重启 Pingora 可能继续按80/443配置失败;恢复 shadow 时不能只清GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN/HTTP_REDIRECT_LISTEN,也必须清空GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE/TLS_KEY_FILE,避免留下证书路径但无 TLS listener 的半直连 env。 - 踩坑补充:直连彩排状态脚本不是修复动作。
node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs --release-root /opt/genarrative/current --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical只读检查公网端口归属、health patrol 模式、Pingora shadow、realpath canary、systemd 和 current release 自审;如果它报CRITICAL,应先修发布包、端口归属、canary 配置、health patrol env 或 systemd 指向,不要把它当成会自动启用 canary、停止 Nginx 或修复 current release 的脚本。 - 踩坑补充:Pingora current release 自审和切换证据链都不是修复动作,
npm run check:pingora-current-release-audit/scripts/ops/pingora-current-release-audit.mjs只负责只读确认发布包自包含、api-server.sha256/pingora-gateway.sha256匹配、release manifest 登记了当前要接流的 Pingora 产物、pingora-gateway可执行和 systemdExecStart指向;npm run check:pingora-cutover-status-snapshot/scripts/ops/pingora-cutover-status-snapshot.mjs只负责输出pre-cutover、post-enable、post-rollback三阶段只读 JSON evidence,并在直连 runbook 中通过--require-pingora-gateway把上述自审结果收录到checks.current-release-audit.details;快照还必须确认systemctl cat genarrative-pingora-gateway.service的EnvironmentFile=精确包含本次--pingora-env-file,否则systemd.pingoraUnit.environmentFileMatchesPingoraEnvFile=false且标记CRITICAL,避免证据包读到一份 env、真实服务读另一份 env。正式切换窗口用scripts/ops/pingora-cutover-evidence-bundle.mjs把快照 JSON、stdout、stderr、命令记录和 manifest 写入--output-root下的新证据目录,证据包 manifest 必须记录已生成 snapshot、direct live、stdout / stderr、命令记录和 parse-error 文件的path、sizeBytes与sha256,便于归档后复核;证据目录生成、复制或归档后必须用随包scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir <bundleDir>做只读验真,确认manifest.files登记的文件未缺失、大小未漂移、sha256 未漂移,且证据目录不是符号链接或非目录;三阶段证据分别验真后,还必须用随包scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback做只读总审计,自动选择每个阶段最新 bundle 并复用 verifier,缺阶段、最新证据损坏、坏 manifest 或符号链接条目都应失败。direct enable apply / rollback apply 必须通过scripts/ops/pingora-cutover-command-evidence.mjs包装真实脚本,单独保存命令 stdout、stderr、退出码、脱敏命令记录和 manifest,runbook 必须显式传绝对路径--output-root,且该路径不能是文件系统根目录、符号链接或包含换行 / NUL 字符;命令记录必须同时保留脱敏后的可读命令和结构化executable/args[],命令证据 manifest 也必须记录command.stdout.txt、command.stderr.txt和command-record.json的sizeBytes与sha256;命令证据生成后也要立即把 stdout 中的bundleDir填入随包 verifier 的<enable-apply-bundle-dir>或<rollback-apply-bundle-dir>占位符做只读验真,不能只等最终根目录总审计才发现 command-record 或 stdout/stderr 归档漂移。启用后证据包必须额外运行随包 direct live smoke,并写入direct-live.json、direct-live.stdout.txt、direct-live.stderr.txt、direct-live-command.json和 manifest summary 的directLiveStatus,让 Pingora access logrequest_id反查结果可复盘;direct-live.json的direct-access-log结果必须保留扫描行数、匹配数量、缺失明细以及 method/path/status 漂移明细,不要只保留 count 或依赖 stderr。证据包 manifest summary 还必须包含directLiveAccessLog摘要;如果 direct live JSON 缺少direct-access-log结构化结果,整包应记为CRITICAL。若 snapshot 或 direct live stdout 解析失败,必须保留snapshot-parse-error.txt或direct-live-parse-error.txt并在 manifest / 最终 stdout 中给出路径;不要只截图或复制pingora-direct-enable.sh/ release readiness 的终端输出当作直连证据。证据阶段名只能使用 ASCII 字母、数字、点、下划线和短横线,非法--phase会直接失败,不会被清洗后继续落盘;自审、状态快照和证据包的--release-root都不能是文件系统根目录,状态快照的--health-patrol-env-file/--pingora-env-file以及证据包所有显式路径参数也不能是文件系统根目录,状态快照自身还必须拒绝带换行或 NUL 的 release/env 路径,并在执行 systemctl、current release 自审、health patrol env 复核或生产巡检子命令前复核子命令参数,证据包执行状态快照或 direct live 子命令前也必须拒绝任何带换行或 NUL 字符的子命令参数,避免污染后的结构化args[]先进入正式证据再等总审计兜底,--output-root及其已存在上级路径不能是符号链接,已存在的--output-root必须是真实目录,路径异常时会在执行状态快照前失败,避免把证据写入非预期软链目标;current release 自审、状态快照和证据包的显式--timeout-ms及对应 env 必须是正整数,生产健康巡检的--timeout-ms、--slow-ms、GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS和GENARRATIVE_HEALTH_PATROL_SLOW_MS,canary / direct live smoke 的--timeout-ms及对应 env,canary access log 对账的--since-lines及对应 env 也必须是正整数,直连 live / release readiness 的布尔 env 也必须是明确布尔值,非法值都会失败,不再静默回退默认值或 false。自审、快照、证据包、证据验真和证据总审计脚本都不修改/etc、systemd、Nginx 或 Pingora;命令证据脚本只执行--后面的真实命令并归档输出,不自行理解 systemd / Nginx;证据包和命令证据目录必须是0750,证据文件必须是0640,且不能覆盖既有文件;probe token 和其他 env 敏感值只能记录是否存在或显示<redacted>,不能把 env 原文写入终端执行日志、gateway smoke / direct live / direct rollback shadow probe 命令日志、stdout、snapshot、manifest、命令记录或子检查 stdout / stderr;如果自审、快照、direct live 或总审计证据里出现CRITICAL,应先修发布包、Jenkins 归档过滤、deploy 复制、env、systemd capability、直连入口、巡检状态或证据归档,再继续下一阶段,不要把自审、快照或证据包当成可自动修复的烟测。 - 踩坑补充:直连 Pingora 后不要让静态缓存头继续依赖框架默认值。HTML、目录 index 和 SPA fallback 必须保持
Cache-Control: no-cache,否则旧入口页可能长期引用已经切换的 chunk;带 Vite 指纹的/assets/*和/admin/assets/*才能使用public, max-age=31536000, immutable;普通非指纹静态和 ACME challenge 继续保守no-cache。如果需要临时覆盖GENARRATIVE_PINGORA_GATEWAY_*_CACHE_CONTROL,值不能包含换行或 NUL,修改后必须跑npm run check:pingora-gateway-smoke确认 HTML、普通静态和指纹资源三类响应头没有漂移。 - 踩坑补充:直连 Pingora 后也不能只验证整文件静态读取。浏览器、媒体探测和线上签名 URL 排障都可能使用
Range: bytes=;Pingora 静态文件必须支持单段 range 的206 + Content-Range和越界 range 的416 + Content-Range: bytes */<len>,同时给静态响应写入Accept-Ranges: bytes。If-Range不能被忽略:日期匹配才继续给局部内容,旧日期或弱 ETag 校验器应回完整200,避免客户端拿旧校验器拼接错误文件片段。206、304、416不应被 gzip 压缩,否则Content-Range指向的字节区间会和实际响应体不一致。多段 range 暂按完整文件处理,不要在切换窗口临时拼 multipart 响应。 - 踩坑补充:直连 Pingora 后不要让静态路由接受非读取方法。
POST /assets/app.js或POST /some/deep/link这类请求不应返回静态内容;命中静态候选时返回405 + Allow: GET, HEAD,缺失文件仍返回404。修改静态路由后跑npm run check:pingora-gateway-smoke,确认 405 没有被压缩或误写成 JSON 代理错误。 - 踩坑补充:静态响应不是代理路径,也必须有 access log 证据。修改静态协商缓存、方法限制或 Range 行为后,smoke 要用固定
X-Request-Id反查 Pingora access log 中同一行的path、status和proxy_target=Local,至少覆盖304、405、206、416;否则直连切换证据包可能只能证明 API / WSS 代理路径,排查浏览器缓存或媒体 Range 问题时缺少本地响应状态证据。 - 踩坑补充:直连 live smoke 不能只证明根 HTML 返回
200。正式发布包的首页通常会引用/assets/或/admin/assets/构建产物,direct live 应自动发现静态资源,验证静态缓存 / 校验头、HEAD头响应、If-None-Match/If-Modified-Since304 和Range: bytes=0-0,并纳入 access log method/path/status 对账;如果首页存在 Vite 指纹资源,还必须额外证明Cache-Control: public, max-age=31536000, immutable以及指纹资源 GET / HEAD / 304 / Range 的 access log method/path/status 证据,避免只验证普通/assets/app.js却漏掉旧 tab chunk 长缓存口径。如果该项显示 skipped,要确认是维护模式、非 HTML,还是发布包首页确实没有资产引用,不要把 skipped 当作已经验证前端静态资源可读。 - 踩坑补充:不要把
direct-live.json当成只有状态码的摘要。静态 GET / HEAD / 304 / Range 检查必须保留白名单headers,至少能复盘cache-control、etag、last-modified、accept-ranges、content-range、content-length和content-encoding;证据包自测要确认普通静态和 Vite 指纹资源的Cache-Control、Content-Range都被归档。API / WSS 检查不要落原始响应头,避免把认证、Cookie 或上游细节带进切换证据。 - 踩坑补充:切流证据包不能只把静态头部藏在
direct-live.json。manifest.summary.directLiveStaticHeaders必须提升普通静态和 Vite 指纹静态的缓存头、校验头、RangeContent-Range和 304 状态摘要,方便切换窗口先扫 manifest 判断证据是否完整;如果 direct live 已输出静态资产结果但摘要缺少缓存头、校验头、Range206 + Content-Range、ETag 304 或 Last-Modified 304 证据,证据包会直接记为CRITICAL。遇到摘要缺失或 diagnostics 非空时,应重新生成启用后证据包或修复 direct live / 静态响应头,不要手工改 manifest。 - 踩坑补充:最终证据根目录总审计必须带
--require-phase-direct-live-access-log post-enable --require-phase-direct-live-static-headers post-enable --require-phase-pingora-env-shadow post-rollback,正式 runbook 已默认生成这些参数。post-enable证据包还必须传--expected-pingora-env-mode direct,post-rollback证据包必须传--expected-pingora-env-mode shadow,否则 health patrol 模式正确也不能证明 active Pingora env 姿态正确。若旧post-enablebundle 虽然manifest.summary.status=OK但没有directLiveAccessLog或directLiveStaticHeaders,或旧post-rollbackbundle 没有manifest.summary.pingoraEnvShadow、没有mode=shadow/shadowReady=true、仍残留tlsCertFile/tlsKeyFile,总审计也应失败;处理方式是用新版 current release 重新生成对应阶段证据包,不要把旧包混进正式归档。 - 踩坑补充:最终证据根目录总审计失败时先看 JSON 顶层
summary,不要直接在长phases[]/commands[]里翻。summary.failedItems[]会聚合失败阶段、命令、根目录或时间线诊断,summary.directLiveEvidence[]会直接给出post-enable的accessLog.ok/reason与staticHeaders.ok/reason;reason 指向缺摘要或字段不完整时,应重新生成启用后证据包,而不是手工补 manifest。 - 踩坑补充:最终证据根目录总审计命令示例也必须包含五条
--require-command-executable,分别绑定 current release 随包pingora-direct-enable.sh、pingora-health-patrol-env-switch.mjs、pingora-gateway-env-shadow-switch.mjs、pingora-health-patrol-env-switch.mjs和pingora-direct-rollback.sh。不要只写--require-command和--require-command-arg --apply,否则只能证明有命令证据和参数,不能证明真实执行的是本次 current release 脚本。发布包级npm run check:production-api-release会同时检查生成 README 和随包 readiness dry-run cutover 输出,若这里失败,先修发布包构建脚本或随包 readiness 脚本,不要只改源码文档。 - 踩坑补充:直连 Pingora 后也不要只看前端页面和 access log 成功。API 上游必须继续收到 Nginx 口径代理头:
Host、X-Forwarded-Host、配置化X-Forwarded-Proto、TCP 对端 IP 的X-Real-IP,以及追加 TCP 对端 IP 的X-Forwarded-For。TRUST_X_FORWARDED_FOR只用于接流保护 client key;如果误以为它会改变上游X-Real-IP或覆盖上游X-Forwarded-For,容易造成回调 URL、鉴权来源或日志归因排查漂移。修改代理头逻辑后先跑npm run check:pingora-gateway-smoke,让 mock 上游回显这些头。 - 踩坑补充:current release 自审开启
--systemd-show时,带换行或 NUL 的--release-root/--systemd-service必须在执行systemctl show前失败,不能把污染参数写进子命令或后续证据链。遇到这类失败先修 runbook 参数来源,不要用--warn-only继续采集。 - 踩坑补充:canary live 的
--base-url、--prefix、--host、--path、--timeout-ms和对应 env 如果包含换行或 NUL,必须在发起 canary 请求前失败,不能让污染参数进入 URL、Host header 或 JSON 输出。遇到这类失败先修目标机 env、runbook 参数来源或手工命令,不要用只看X-Genarrative-Nginx-Handoff的 curl 替代完整npm run check:pingora-canary-live。 - 踩坑补充:真实路径 canary 不能为了“更像生产”而 include 到生产
443server 里写/api、/v1或/assetslocation;那会覆盖当前 Nginx 正式路由。只能把genarrative-pingora-realpath-canary.conf作为独立 loopbackserverinclude 到http上下文,使用127.0.0.1:18083和独立genarrative-pingora-realpath-canary.access.log验证,再用 release readiness 的--require-realpath-live纳入门禁。 - 踩坑补充:direct preflight 的
--env-file、--systemd-service、服务用户和 env 中的 listen / cert / key 值如果包含换行或 NUL,必须在执行systemctl cat、sudo -u ... test -r ...、证书可读检查或端口监听检查前失败。遇到这类失败先修目标机 env 或 runbook 参数来源,不要临时改成手工 systemctl / sudo 命令绕过。 - 踩坑补充:direct live 的
--https-base-url、--http-base-url、--host、--redirect-host、--probe-token、--path、--spacetime-database、--pingora-access-log、--timeout-ms和对应 env 如果包含换行或 NUL,必须在发起 HTTPS / HTTP / WSS 请求前失败,不能让污染参数进入请求头、URL、access log 对账或 direct live JSON。遇到这类失败先修 runbook 参数来源或目标机 env,不要临时删掉 direct live smoke、改用 curl 截图或只看systemctl is-active。 - 踩坑补充:证据包
--run-direct-live透传的 direct URL、Host、probe token、数据库名、Pingora access log 路径和 access log tail 行数也要在证据包配置层先拒绝换行或 NUL,--direct-pingora-access-log还必须是绝对路径且不能是/。遇到这类失败先修 runbook 参数或现场 env,不要把参数污染留给 direct live 子命令兜底,也不要手工改direct-live-command.json或跳过启用后证据包。 - 踩坑补充:不要在证据目录或证据根目录里手工塞
README、截图、压缩包、临时目录、无 manifest 子目录或软链来“辅助说明”。证据 verifier 把manifest.files视为闭集,未登记普通文件、目录和符号链接都会默认失败;证据总审计也默认要求根目录只包含带manifest.json的证据目录。证据 verifier / 总审计的入口路径、verifier 脚本路径和 manifest 登记文件名都不能包含换行或 NUL 字符,避免污染 JSON 证据、终端输出或归档复盘。需要保留人工说明时,应放到证据根目录外部,或重新生成能把该文件纳入 manifest 元数据的正式证据,而不是在正式切换归档上使用--allow-extra-files或--allow-extra-root-entries。 - 踩坑补充:不要把即时证据验真理解成只验 hash。正式 runbook 中
pre-cutover、enable-apply、post-enable、rollback-apply、post-rollback五个即时 verifier 步骤都必须带--require-summary-ok,同时要求manifest.schemaVersion=1、manifest.files未漂移且manifest.summary.status=OK;如果证据包已经记录CRITICAL、缺少 schemaVersion 或缺少 summary,应先修复现场状态、发布包、env、systemd、health patrol 或 direct live 证据并重新归档,不能继续推进到最终总审计。 - 踩坑补充:最终证据根目录总审计复用 verifier 时也必须启用
--require-summary-ok。如果总审计输出里verify.requireSummaryOk不是true,说明脚本或随包 verifier 已经退化成宽松模式,应先修发布包脚本而不是继续切换。 - 踩坑补充:不要用带
manifest.commandName的命令证据目录满足--require-phase。阶段证据必须来自状态快照证据包,命令证据必须通过--require-command单独要求;否则总审计可能把“真实执行过命令”和“某阶段状态已归档”混成一件事。 - 踩坑补充:证据根目录总审计不能只要求三阶段状态快照,也不能只做 sha256 验真。正式 runbook 必须在
--require-phase pre-cutover --require-phase post-enable --require-phase post-rollback之外,再传--require-command enable-apply:pingora-direct-enable-apply --require-command post-enable:pingora-health-patrol-direct-env-switch --require-command rollback-prep:pingora-gateway-shadow-env-switch --require-command rollback-prep:pingora-health-patrol-nginx-env-switch --require-command rollback-apply:pingora-direct-rollback-apply,用manifest.phase + manifest.commandName锁定五条真实切换命令证据;缺命令证据、最新命令证据损坏、阶段或命令manifest.summary.status非OK、命令manifest.summary.exitCode非0、命令名不安全,或顶层manifest.commandName/ 内嵌manifest.command.name任一为空、非法、互不一致时都必须失败。 - 踩坑补充:
commandName只能说明证据分类,不能证明真的跑了 enable / rollback 脚本。正式 runbook 的命令证据必须传--expected-executable绑定 current release 随包脚本绝对路径,并传--require-arg --apply在执行前确认真实命令参数包含--apply;如果真实命令与预期脚本不一致或缺少--apply,命令证据脚本应在创建正式命令证据前失败,避免把错误命令归档成正式切换证据。最终证据根目录总审计还必须传五条--require-command-executable和七条--require-command-arg,覆盖 enable apply、health patrol direct env switch、Pingora gateway shadow env switch、health patrol nginx env switch 和 rollback apply,其中包含enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs、rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh以及对应--apply、pingora-direct、nginx参数要求,复核manifest.expectedExecutable、manifest.command.executable与独立command-record.json的 executable,并要求manifest.commandName与manifest.command.name只要存在就各自是安全非空命令名、两者同时存在时一致、manifest.command.args与独立command-record.json.args都包含--apply,且每个 args 字符串都不含换行或 NUL 字符。--require-command-executable的 executable 段必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符;总审计 JSON 会记录requiredCommandExecutables,便于复盘本次绑定的真实 current release 随包脚本。总审计还会要求manifest.command与command-record.json关键字段一致;其中两份命令记录的schemaVersion都必须是1,stdoutPath/stderrPath必须同时与manifest.files.stdout.path/manifest.files.stderr.path对齐,不能把重新计算过 hash 的 command-record 指向另一份输出文件;args/command也必须一致,不能只保证脚本路径正确却把--apply证据改成--dry-run或其它参数;命令记录时间必须满足finishedAt >= startedAt、durationMs == finishedAt - startedAt,且manifest.generatedAt不能早于命令finishedAt;旧证据缺少 schemaVersion、缺少 expectedExecutable、缺少必需--apply参数、人工同名证据 executable 漂移、manifest 与 command-record 语义漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染、命令时间线漂移或同一命令重复绑定不同脚本路径都必须失败。 - 踩坑补充:命令证据里
expectedExecutable字段不能用空字符串或相对路径表示“未知”。manifest.expectedExecutable和manifest.command.expectedExecutable只要存在就必须是安全绝对路径;否则总审计应把 manifest 判坏,避免坏顶层字段被内嵌字段兜底,或坏内嵌字段被 command-record 里的路径掩盖。生成端--expected-executable也不能填/或带换行 / NUL 的路径,脚本会在执行真实命令前失败,不能用坏 expected path 先生成证据再交给总审计兜底。 - 踩坑补充:不能只检查
manifest.command.executable与command-record.json.executable两边一致;如果命令证据已经声明expectedExecutable,真实executable必须同时等于该预期路径,并且必须是绝对路径。否则人工修改两份命令记录为同一个错误脚本或相对路径,也可能伪造成一致证据。 - 踩坑补充:命令证据的
command字符串是给人读的,不是结构化身份事实。manifest.command.executable和command-record.json.executable都必须存在且是绝对路径;缺少结构化 executable 时,即使command字符串看起来包含正确脚本,也不能作为正式切换证据。 - 踩坑补充:生成命令证据时不要写
-- node script.mjs ...、-- bash script.sh ...或-- pingora-direct-enable.sh ...。pingora-cutover-command-evidence.mjs现在要求-- <command>本身就是绝对路径,且不能是文件系统根目录;真实命令和每个真实命令参数都不能包含换行或 NUL 字符。正式 runbook 应直接执行 current release 随包脚本的绝对路径,让 manifest 与 command-record 的executable/args[]字段从源头就是可审计事实。 - 踩坑补充:不要把 Pingora 日志、env、drop-in、脚本或 release root 路径填成
/来“先跑通参数”。release readiness 的--live-nginx-access-log/--live-pingora-access-log/--direct-pingora-access-log/--direct-health-patrol-env-file/--direct-preflight-env-file,canary 对账脚本的--nginx-log-file/--pingora-log-file,direct live 的--pingora-access-log,direct preflight 的--env-file,以及pingora-direct-enable.sh/pingora-direct-rollback.sh的显式路径参数都必须是绝对文件路径且不能是文件系统根目录;如果现场不确定真实日志、env、drop-in 或脚本文件,先查systemctl cat、logrotate、发布包 manifest 或服务 env,而不是用/占位。 - 踩坑补充:证据根目录总审计选择“每类最新证据”后,还必须证明这些证据来自同一次切换时间线。最新证据选择和标准八段时间线证明只接受
schemaVersion=1且带合法、规范 UTC 毫秒格式manifest.generatedAt的 manifest,命令记录startedAt/finishedAt也必须是new Date().toISOString()形式;缺失、非法、省略毫秒、本地时区或其它宽松可解析格式都会直接失败,不能用目录 mtime 兜底;证据目录被复制、归档或恢复后,也必须以 manifest 时间为准。同一阶段或同一命令如果出现多个候选共享最新manifest.generatedAt,总审计会以AMBIGUOUS_LATEST失败并列出重复目录,不能按目录名排序打平;应重新归档该阶段 / 命令证据,或把旧证据移出正式证据根目录后再审计。标准八段证据都被要求时,每段审计状态都必须是OK,manifest.generatedAt必须满足pre-cutover -> enable-apply -> post-enable:pingora-health-patrol-direct-env-switch -> post-enable -> rollback-prep:pingora-gateway-shadow-env-switch -> rollback-prep:pingora-health-patrol-nginx-env-switch -> rollback-apply -> post-rollback,且默认八段跨度不能超过 24 小时;非 OK、倒序或跨度过大都代表可能混入不同切换窗口遗留证据或现场状态未达标,必须失败后重新归档或清理证据根目录。任何证据 manifest 只要显式写入cutoverRunId字段,就必须是安全非空 ID,不能用空字符串伪装成缺省字段。确需跨更长维护窗口时,只能在生成 runbook 时显式传--cutover-evidence-timeline-max-span-ms <ms>,让最终总审计 JSON 记录本次放宽后的timeline.maxSpanMs与实际timeline.spanMs。 - 踩坑补充:标准八段时间线失败时不要只看顶层
ok=false或diagnostics文本。timeline.failedCount会按具体失败项累计,timeline.failureBreakdown会把非 OK 证据、缺少时间、cutoverRunId 混入、时间倒序和跨度超限拆开计数;同一次审计可能同时暴露多个证据问题,应逐项修复后重新归档。 - 踩坑补充:同一天多次演练或切换时,只靠“最新证据”和 24 小时窗口仍可能把两轮证据拼在一起。正式 runbook 会生成或接受
--cutover-run-id <id>,并把同一manifest.cutoverRunId写入三阶段证据包、五条真实切换命令证据和最终总审计;最终审计必须带--require-cutover-run-id <本次cutoverRunId>,缺少该字段或 ID 不一致时必须失败。即使人工临时总审计忘记带--require-cutover-run-id,标准八段时间线里只要任一证据声明了manifest.cutoverRunId,八段也必须全部声明同一个值,否则总审计失败。 - 验证:先运行
npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free,确认 env、drop-in、service EnvironmentFile 一致性、当前用户证书权限、服务用户证书权限、service 二进制可执行性和 80/443 已释放;systemctl cat genarrative-pingora-gateway.service必须显示AmbientCapabilities=CAP_NET_BIND_SERVICE、CapabilityBoundingSet=CAP_NET_BIND_SERVICE和EnvironmentFile=/etc/genarrative/pingora-gateway.env;启用脚本 apply 必须先通过 current release 自审,失败时不安装 direct-entry drop-in;还必须带 direct HTTPS / HTTP / Host / redirect host / SpacetimeDB database / Pingora access log 参数,并在重启后直接完成 direct live smoke 和 direct-access-log JSON 证据校验;也可用 release readiness--require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable把 HTTPS、HTTP redirect / ACME、正式域名 Host/SNI、redirect Location host、Pingora access log request_id 落盘、env 预检、systemd drop-in、service EnvironmentFile 一致性、当前用户和服务用户证书可读、service 二进制可执行、显式目标库和 WSS 101 一起纳入硬门禁,并拒绝--direct-skip-wss,避免 TLS 证书只按127.0.0.1误测、HTTP redirect Location 指错域名、service 实际读取另一份 env、root / deploy 用户可读但 systemd 服务用户不可读、current release 缺少可执行pingora-gateway,或 WSS subscribe 隐式打到默认 SpacetimeDB 库。check-pingora-release-readiness.mjs --help的正式直连和只生成 runbook 示例也必须带--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,不要让值班人员复制示例后才被--require-direct拦截。current release 自审、状态快照和证据包的布尔 env 必须是明确布尔值,非法值会失败,不得把拼错的 run / require / fail 开关当成 false。npm run plan:pingora-direct-cutover -- --require-direct ...输出必须包含 Host 与回退巡检入口确认、current release preflight、启用前不带--require-direct的基础 readiness、direct enable dry-run/apply、启用后带--require-direct的复核、rollback dry-run/apply、回退后 health patrol 切回 Nginx 并恢复切换前 public base URL / Host、回退后 health patrol env 复核;缺少--require-direct、缺少--rollback-health-patrol-public-base-url、缺少--direct-pingora-access-log、redirect Host 漂移或 rollback smoke Host 漂移时必须失败,避免生成缺少正式直连硬门禁或验证不同入口的切换计划。Host 与回退巡检入口确认步骤必须展示回退后要恢复的 health patrol public base URL / Host。直连后genarrative-health-patrol.service应使用GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct,状态 JSON 中gatewayMode应为pingora-direct,并检查genarrative-pingora-gateway.service而不是nginx.service;public probe 走127.0.0.1时应带GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>。回退后nginx -t必须先通过,systemctl cat genarrative-pingora-gateway.service不应再显示这两条 capability,systemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager必须仍指向 current release 的pingora-gateway,systemctl is-active nginx.service应为active,curl --fail --max-time 5访问--nginx-smoke-url应成功;若 smoke URL 为本机地址必须带--nginx-smoke-host <域名>,证明正式 vhost 已回到 Nginx;随后用node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs ...复核 health patrol env,必须显示GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx且 public base URL / Host 与切换前记录一致,shadow probe 可选复核必须返回gateway=pingora-shadow。本机提交前还要运行npm run check:pingora-direct-enable、npm run check:pingora-direct-rollback、npm run check:production-health-patrol、npm run check:production-api-release、npm run check:pingora-production-release-build和npm run check:production-api-deploy,确保脚本默认 dry-run 不会安装或删除 drop-in、current release 自审失败时启用脚本不会安装 drop-in、direct live 退出 0 但缺少direct-access-log结构化证据时启用失败,API release 布局自包含,真实 Pingora release 二进制能构建并进入发布包,API deploy 从发布产物内执行后 current release 自包含;缺少数据库备份脚本、健康巡检脚本、健康巡检 env 复核脚本、切换命令证据脚本、env 示例目录或 direct live smoke 脚本的发布包都必须在 current 切换前部署失败并退出本次打开的维护模式。正式直连 readiness 必须带--direct-health-patrol-env-file /etc/genarrative/health-patrol.env,并用scripts/check-production-health-patrol-env.mjs阻断 health patrol 仍停在 Nginx 模式或本机 direct probe 缺少正式 Host;发布包包含pingora-gateway时,npm run check:production-api-deploy必须覆盖服务 active / inactive 都会在 shadow 配置安全时执行systemctl restart genarrative-pingora-gateway.service并复核 active,同时覆盖 direct-entry capability 或公网监听 env 下不会提升 release、不会切 current、不会自动 restart。 - 顺序补充:正式 runbook 必须先通过 health patrol env 切换脚本预置回 Nginx 和切换前 public base URL / Host,再执行
rollback apply;回退脚本内置 env 复核和独立 env 复核都会阻断 public base URL / Host 漂移。 - 顺序补充:正式 runbook 还必须在
rollback apply前预置 Pingora shadow env。启用前和--dry-run-cutover要求80/443空闲;启用后--require-direct复核不再要求端口空闲,因为端口应由 Pingora 占用。回退时要先用 current release 随包node -- /opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply --env-file /etc/genarrative/pingora-gateway.env恢复GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081并清空 TLS / HTTP redirect 低端口监听,再移除 direct-entry drop-in 和重启 Pingora。 - 关联:
deploy/systemd/genarrative-pingora-gateway-direct-entry.conf、deploy/env/health-patrol.env.example、deploy/env/pingora-direct-live.env.example、deploy/env/pingora-canary-live.env.example、scripts/deploy/pingora-direct-enable.sh、scripts/deploy/pingora-direct-rollback.sh、scripts/deploy/pingora-tls-cert-sync.mjs、scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、scripts/ops/pingora-cutover-command-evidence.mjs、scripts/ops/pingora-cutover-evidence-verify.mjs、scripts/ops/pingora-cutover-evidence-audit.mjs、scripts/jenkins-server-provision.sh、scripts/build-production-release.sh、scripts/deploy/production-api-deploy.sh、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
外部生成 worker 重领必须按 claim attempt 隔离并持久结算
- 现象:同一个外部生成 job 在 worker 崩溃或 lease 过期后重领,可能出现旧 attempt 和新 attempt 都扣费,或者旧 attempt 已退款后新 attempt 因稳定 ledger 被当成幂等而免费执行。
- 原因:只按业务资源 ID 或 job ID 生成稳定 ledger 无法区分 claim;仅在新 attempt 开始时“先查旧 consume、存在则退款”仍有竞态,旧 consume RPC 可能在检查之后才提交。
- 处理:扣退费 ledger 固定包含
job_id + claim_attempt,每次重领先结算所有旧 attempt,再扣当前 attempt。结算必须在 SpacetimeDBasset_operation_wallet_settlement持久化:旧 consume 已存在时原子退款;尚不存在时写取消 intent。任何迟到 consume 在同一事务内看到 intent 后失败关闭。重复 ledger 必须核对用户、金额和来源,不能只按 ID 存在就返回成功。claim 处理 lease 已过期的runningjob 时还必须先比较attempt与max_attempts:未耗尽才递增并返回 worker;最终 attempt 已耗尽时在同一事务内把 job 置为failed、清空 lease、写完成时间和失败事件,并按当前 attempt 退款或写取消 intent,绝不能再次返回 provider executor。 - 验证:
cargo test -p spacetime-module asset_operation覆盖缺 consume 时写 intent、冲突结算拒绝和退款配对;cargo test -p spacetime-module external_generation::tests::覆盖未耗尽 lease 可重领、最终 attempt 只终态收口且不再递增;cargo test -p spacetime-module wallet_idempotent_replay覆盖冲突重放;cargo test -p api-server asset_billing覆盖崩溃重领、重复结算和当前 attempt 扣费。 - 关联:
server-rs/crates/api-server/src/asset_billing.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
外部生成队列不再由 HTTP 进程兜底执行
- 现象:拼图首关生成接口返回
queued,但生成页长时间不完成,重启genarrative-api.service也没有推进任务。 - 原因:HTTP 角色只入队,不再直接调用外部 provider;如果没有运行
GENARRATIVE_PROCESS_ROLE=external-generation-worker或all的进程,external_generation_job会停留在pending/running,直到有 worker claim。 - 处理:生产用
systemctl enable --now genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service启动保底 worker 和 controller;genarrative-api.service对 controller 使用 systemdWants弱依赖,启动 API 时会尝试一并拉起 controller,但不会让 HTTP 进程自己执行systemctl。首次 API deploy 会在默认 worker pattern 下自动启用并启动@1、等待 worker active,并重启验活 controller。扩容默认交给 controller 按队列统计启动@2.service等实例,手动扩缩容只作为兜底;worker 收到停机信号后会停止 claim 新任务并等待当前任务完成。本地 smoke 可临时用GENARRATIVE_PROCESS_ROLE=all npm run dev;本地若只想同步排查可通过.env.local或本机环境设置GENARRATIVE_EXTERNAL_GENERATION_MODE=inline,但这不会创建 job,也不能验证 worker 扩缩容。 - 验证:
systemctl status genarrative-external-generation-controller.service 'genarrative-external-generation-worker@*.service'能看到 controller 和 worker 实例;queue 模式下任务被 claim 后worker_id与lease_expires_at会更新,完成后 session 进入 ready 或 failed;inline 模式下不应产生新的external_generation_job。 - 关联:
deploy/systemd/genarrative-external-generation-worker@.service、deploy/systemd/genarrative-external-generation-controller.service、deploy/env/external-generation-controller.env.example、server-rs/crates/spacetime-module/src/external_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部生成 worker 不应等待 HTTP 认证投影恢复
- 现象:
genarrative-external-generation-worker@1.service在 systemd 中显示 active,但external_generation_job长时间保持pending;worker 日志每 5 秒出现认证投影或公开 read model 订阅失败。 - 原因:独立 worker / controller 是非 HTTP 角色,不承接用户登录态恢复;如果启动路径复用 HTTP
api-server的认证投影恢复,SpacetimeDB 认证投影或公开 read model 漂移会把 worker claim 循环挡在启动前。 - 处理:
GENARRATIVE_PROCESS_ROLE=external-generation-worker和external-generation-controller启动时只构建空 auth store 的AppState,不调用 SpacetimeDB 认证投影导出;只有api/all这类 HTTP 角色需要在启动时恢复认证投影并在依赖不可用时重试或进入 503 降级。 - 验证:重启 worker 后日志应先出现“非 HTTP 进程跳过 SpacetimeDB 认证投影恢复”,随后出现
external generation worker 已启动;同一时间窗口不应再因为认证投影恢复失败而阻止 job claim。HTTPapi-server的认证恢复日志和 503 降级语义保持不变。 - 关联:
server-rs/crates/api-server/src/main.rs、server-rs/crates/api-server/src/external_generation_worker.rs、server-rs/crates/api-server/src/external_generation_worker_controller.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地旧 external-generation-worker 会抢队列并暴露成 procedure 超时
- 现象:角色 / 画布生成的外部 provider 与 OSS 上传已成功,但 worker 写回
editor_project_resource等业务资源时报SpacetimeDB procedure 调用超时,日志里可能还能看到旧 worker 二进制对 procedure 返回值做 BSATN 反序列化失败。 - 原因:本地
npm run dev/npm run dev:api-server默认GENARRATIVE_PROCESS_ROLE=all,会自己消费队列;如果之前手动启动的同仓库、同 databaseGENARRATIVE_PROCESS_ROLE=external-generation-worker进程没有退出,旧二进制会继续 claim 新 job,schema / binding 已更新的当前进程反而没有拿到这次任务。 - 处理:Linux 本地默认
all角色启动前,scripts/dev.mjs会扫描同仓库、同 SpacetimeDB server / database、同server-rs/target/debug/api-server的遗留external-generation-worker并停止;显式GENARRATIVE_PROCESS_ROLE=api做生产式拆分验证时不清理独立 worker。 - 验证:
ps -eo pid,ppid,lstart,cmd | rg 'server-rs/target/debug/api-server'只应看到当前all或显式拆分下预期的进程;/healthz和/readyz成功后,生成 job 应由当前进程消费并把业务资源写回。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、server-rs/crates/api-server/src/external_generation_worker.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部生成 worker 业务写回必须同事务校验 lease guard
- 现象:worker
complete/fail已校验worker_id + lease_token,但如果玩法 session / work profile 写回在此之前单独调用,过期 worker 仍可能先写入业务状态,随后才在 job complete/fail 阶段失败;带计费包装的旧 worker 还可能因为 stale guard 错误触发补偿退款。 - 原因:队列状态栅栏只保护
external_generation_job自身,不会自动保护玩法 procedure。业务写回必须自己带 claim 后的job_id / worker_id / lease_token,并在同一个 SpacetimeDB transaction 内校验 job 仍为running、lease 未过期、job kind、owner 和 source entity 匹配。 - 处理:拼图首图 worker 的前置
compile_puzzle_agent_draft、save_puzzle_generated_images、save_puzzle_ui_background、mark_puzzle_draft_generation_failed和mark_puzzle_level_generation_failed已接入external_generation_joblease guard;api-server 的资产扣费包装遇到这类 stale worker lease guard 错误时不执行补偿退款,错误文本包含external_generation_job 当前不是 running 状态或external_generation_job 不存在时也按 stale guard 处理。inline 模式只允许job_id / worker_id / lease_token三项同时为空,半空 guard 仍拒绝。后续迁移其它玩法 worker 时必须复用该模式,不能只在 worker 进程内保存一份 token。 - 验证:
cargo test -p api-server external_generation_worker --manifest-path server-rs/Cargo.toml、cargo test -p api-server asset_operation_billing_does_not_refund_stale_worker_lease_errors --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/api-server/src/external_generation_worker.rs、server-rs/crates/api-server/src/asset_billing.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
外部生成 worker 核心业务写回失败不能完成 job
- 现象:worker 已经生成图片并拿到本地合成 session 快照,但 SpacetimeDB 业务写回因连接、旧 wasm 或 lease guard 失败没有真实落库;如果此时仍把
external_generation_job标成completed,前端只会看到队列完成而 session 长时间不变化,后续也没有 worker 会重领修复。 - 原因:同步 HTTP handler 的“外部 provider 已成功但 SpacetimeDB 短暂不可用时返回内存快照”降级语义,不能直接搬进异步 worker。worker 的完成状态必须代表核心业务事实已经持久化。
- 处理:worker 路径的
save_puzzle_generated_images/save_puzzle_ui_background等核心业务写回失败时直接返回错误;只有核心写回已经成功后的非关键投影回写才允许降级记录 warning。业务失败态也必须先写回 session / work profile,写回成功后才允许把队列 job 标为 failed;失败态未写回时保留租约,等待 lease 过期后重领。生产首装和首次 API deploy 都必须至少启用一个 worker 实例,例如systemctl enable --now genarrative-external-generation-worker@1.service。 - 验证:
cargo check -p api-server --manifest-path server-rs/Cargo.toml、cargo test -p api-server asset_operation_billing_does_not_refund_stale_worker_lease_errors --manifest-path server-rs/Cargo.toml,并在 smoke 时确认 queued 任务被 worker 消费后 session 真实更新。 - 关联:
server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/generation.rs、server-rs/crates/api-server/src/external_generation_worker.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
生产冷备份后 API 和外部生成 worker 不能只依赖 SpacetimeDB 自恢复
- 现象:release 机器
03:20冷备份后,spacetimedb.service已恢复,但作品列表、创作入口配置或公开 gallery 继续超时 / 502 / 504,genarrative-api.service保持 stopped;或图片画布生成请求返回队列态后长期显示排队,external_generation_job有 claimable pending,但genarrative-external-generation-worker@1.service/ controller 是 inactive;也可能先看到/var/lib/genarrative/database-backups把根分区写满,gzip: stdout: No space left on device。 - 原因:
genarrative-api.service、genarrative-external-generation-worker@*.service和genarrative-external-generation-controller.service都配置了Requires=spacetimedb.service,冷备份停止spacetimedb.service时这些服务会被 systemd 依赖关系一并停止;如果备份脚本只在打包成功后重启依赖服务,那么 tar/gzip 因空间不足失败时就只会恢复数据库,外部生成队列和 API 仍无人接管。 - 处理:生产冷备份 unit 和发布脚本必须带
--restart-service-after genarrative-api.service、--restart-service-after genarrative-external-generation-worker@1.service和--restart-service-after genarrative-external-generation-controller.service;备份脚本必须在停止 SpacetimeDB 前做工作目录剩余空间预检,并且一旦已经停过 SpacetimeDB,就算打包失败也要先恢复 SpacetimeDB 与这些依赖服务,再返回原始备份错误。genarrative-api.service也保留对 controller 的Wants弱依赖,覆盖“只恢复 API”的现场兜底。仓库用npm run check:production-ops和npm run check:database-backup检查 systemd 模板、脚本失败路径、API build/deploy 归档和健康巡检链路。现场修复后执行systemctl daemon-reload,但不要为了验证而手动触发冷备份。 - 验证:
systemctl cat genarrative-database-backup.service应包含这些参数;systemctl is-active spacetimedb.service genarrative-api.service genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service nginx.service全为active;curl -fsS http://127.0.0.1:3101/v1/ping、/healthz、/readyz和代表性/api/editor/showcase/resources均成功;npm run check:database-backup覆盖空间不足不触碰 systemctl、tar 失败仍恢复依赖服务;get_external_generation_queue_stats_and_return不应长期出现 claimable pending。 - 关联:
deploy/systemd/genarrative-database-backup.service、scripts/database-backup-to-oss.mjs、scripts/ops/production-health-patrol.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Pingora Brotli 不能只看 Content-Encoding
- 现象:在
pingora-gateway中把GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS试验性改成gzip,br后,Accept-Encoding: br, gzip的响应会带Content-Encoding: br,但 NodebrotliDecompressSync(...)报unexpected end of file。 - 原因:Pingora 0.8.1 的 Brotli compressor 路径虽然存在,但端到端输出不能被 Node 按完整 Brotli 流解压;只断言响应头会误判为可用。
- 处理:当前 Pingora shadow 只允许
GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip,在进入 Pingora compression 模块前把下游Accept-Encoding收敛为 gzip。Brotli 继续由 Nginx / 前置代理承担,直到补齐可解压的端到端门禁后再评估迁移。 - 验证:
npm run check:pingora-gateway-smoke必须覆盖小响应不压缩、图片资源不压缩、大响应Accept-Encoding: gzip和Accept-Encoding: br, gzip都返回可解压的 gzip;GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=br必须启动失败,GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=0也必须启动失败。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md、deploy/nginx/README.md。
Pingora 公网直连不能信任 X-Forwarded-For
- 现象:公网直连 Pingora 后接流保护、access log 或
client_ip似乎按用户传入的X-Forwarded-For分散,限流 key 可被客户端伪造。 - 原因:
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true只适合 Pingora 前方还有受控 Nginx / LB 且该前置层会清洗X-Forwarded-For的场景;Pingora 自己监听公网0.0.0.0:80/443时,下游请求头就是用户可控输入,不能拿来作为接流保护 client key。 - 处理:公网直连 env 必须保持
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=false。只有 loopback / 受控前置入口才允许配合GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true使用 XFF。目标机 direct preflight 会在公网监听加TRUST_X_FORWARDED_FOR=true时失败。 - 验证:
npm run check:pingora-direct-enable覆盖公网监听误信任 XFF 负例;切换窗口运行npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env ...,看到该错误时先改 env,再重启 Pingora。该 npm script 内部必须保持node -- scripts/check-pingora-direct-preflight.mjs,避免 Node 22 抢占业务--env-file。 - 关联:
scripts/check-pingora-direct-preflight.mjs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora canary 不能只看 handoff 响应头
- 现象:目标 Nginx 前缀 canary 的
/__genarrative_pingora_canary/healthz和代表性 API 都返回成功,响应也带X-Genarrative-Nginx-Handoff: pingora-canary,但仍无法证明 Nginx 与 Pingora 对同一请求的 method/status/path 完全一致。 - 原因:响应头只能证明请求经过了 canary snippet,不能证明同一
request_id已在 Pingora access log 落盘,也不能发现 healthz exact location 映射、前缀 rewrite 后路径或状态码漂移。 - 处理:本机 / CI 的
check-pingora-canary-docker也必须写临时 Nginx access log,并在 live smoke 后复用scripts/check-pingora-canary-access-log-parity.mjs对账 Docker Nginx 与 Pingora access log。目标机--require-live必须在 live smoke 后继续执行同一脚本,默认读取/var/log/nginx/genarrative.access.log和/var/log/genarrative/pingora-gateway.access.log,按request_id对照/__genarrative_pingora_canary/healthz与/__genarrative_pingora_canary/api/creation-entry/config。Nginx canary exact/healthz映射到 Pingora shadow/__genarrative_pingora/healthz,其它 canary 前缀路径按 rewrite 后路径比对。对账脚本的日志路径、prefix、必需路径和--since-lines/GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES不能包含换行或 NUL;日志行里解析出的 URI / path 含控制字符时也必须失败,避免污染值进入 JSON 对账输出。 - 验证:本机或 CI 执行
node scripts/check-pingora-canary-docker.mjs --require-docker --pull时应同时完成临时 Nginx / Pingora access log 对账。目标机执行node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log;本机执行npm run check:pingora-release-readiness-plan和npm run check:production-ops,确认 live 门禁计划包含真实 access log 对账。 - 关联:
scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-access-log-parity.mjs、deploy/env/pingora-canary-live.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora realpath canary include 要晚于 log_format
- 现象:目标机把
genarrative-pingora-realpath-canary.conf放进/etc/nginx/conf.d/后,nginx -t失败并报unknown log format "genarrative_upstream"。 - 原因:真实路径 canary 是独立
server片段,并使用access_log /var/log/nginx/genarrative-pingora-realpath-canary.access.log genarrative_upstream;。Nginx 会按文件名顺序加载conf.d;如果 canary 文件名早于定义log_format genarrative_upstream的主站配置,access log 行会先被解析而找不到格式。 - 处理:真实路径 canary 启停统一用 current release 随包脚本,不再手工写
/etc/nginx/conf.d/。启用执行/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083,脚本固定写入晚于主站配置加载的/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf,并在nginx -t、reload 或 live smoke 失败时恢复写入前配置。关闭执行/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply,脚本在nginx -t或 reload 失败时恢复删除前配置。另一种长期做法是把log_format放到所有conf.dserver 之前的全局 Nginx 配置。检查配置时不要把 probe token 原文写入记录。 - 验证:提交前运行
npm run check:pingora-realpath-canary-toggle或默认聚合门禁npm run check:pingora-release-readiness,确认启停脚本的 dry-run、apply、失败回滚和 disable 恢复逻辑仍被覆盖。启用脚本通过后,再运行node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>、node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log ...和node -- /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-realpath-live ...。若只启用了真实路径 canary,不要同时传--require-live,否则前缀 canary 未启用时会按正式 Nginx HTTP 入口返回 301。 - 关联:
deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf、deploy/nginx/README.md、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md、scripts/check-pingora-release-readiness.mjs。
Pingora release readiness 脚本不能只存在于源码 checkout
- 现象:本机 runbook 能生成,但目标机切换窗口执行启用前或启用后的 release readiness 复核时,可能命中 Jenkins workspace 或源码 checkout 的
scripts/check-pingora-release-readiness.mjs,而不是当前发布包里的脚本。 - 原因:API release、Jenkins Build 归档、Jenkins Deploy 复制清单都是显式文件列表;只在仓库中新增脚本或只改 runbook 相对路径,不能保证目标机 current release 自包含。另一个误区是在
/opt/genarrative/current上运行默认源码全量 readiness,导致包内脚本依赖 Cargo、npm、Docker 或 Nginx 构建环境。 - 处理:
scripts/build-production-release.sh、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy和scripts/deploy/production-api-deploy.sh必须同时携带scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-live.mjs、canary access log 对账脚本以及 direct preflight / live 子脚本;正式 cutover runbook 的启用前基础门禁和启用后--require-direct复核必须调用/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only。默认不带--release-runtime-only的全量 readiness 只在源码 checkout / CI / 构建环境运行。 - 验证:运行
npm run check:production-api-release、npm run check:production-api-deploy、npm run check:pingora-current-release-audit、npm run check:pingora-release-readiness-plan和npm run check:production-ops,确认发布包、current release、runbook 与 guardrails 都覆盖聚合脚本和check-pingora-canary-live.mjs,且 runbook readiness 参数包含--release-runtime-only。 - 关联:
scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-live.mjs、scripts/build-production-release.sh、scripts/deploy/production-api-deploy.sh、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy。
SpacetimeDB 45 秒超时要看 api-server 记录的阶段
- 现象:release 上 Nginx 能立刻连到
api-server,但/api/runtime/*/gallery、/api/creation-entry/config等请求在约GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS后返回502/504。 - 原因:旧日志只能看到 HTTP 总耗时和最终状态,无法区分卡在连接池、SDK 建连、等待
on_connect、订阅 read model、等待 procedure / reducer 回调还是本地订阅 cache 读取。 - 处理:
spacetime-client内置阶段化健康检查和失败日志;/readyz用GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS短窗口检查 SpacetimeDB 连接租约,业务失败日志包含operation_kind、operation_name、spacetime_stage、elapsed_ms。 - 验证:
/readyz失败时看details.spacetime.stage;业务请求超时时查journalctl -u genarrative-api.service中同一时间窗口的SpacetimeDB client operation failed,优先按pool_acquire、connect_build、connect_handshake、read_model_subscribe、procedure_result、reducer_result、read_cache分阶段处理。 - 关联:
server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/api-server/src/health.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
新建草稿扣费不能和入口卡泥点配置分离
- 现象:后台修改创作入口的
mudPointCost后,入口卡和前置余额提示可能显示新数值,但用户真实钱包流水仍按代码常量扣除。 - 原因:早期约定把
creationTypes[].unifiedCreationSpec.mudPointCost只当展示字段,拼图、抓大鹅和汪汪声浪初始生成各自保留了2、10、三次单图1的硬编码扣费路径。 - 处理:新建草稿初始生成成本必须统一从
GET /api/creation-entry/config的unifiedCreationSpec.mudPointCost解析;前端预校验、拼图首图生成、抓大鹅完整草稿生成和汪汪声浪初始三图生成同源。汪汪声浪结果页单图重新生成仍按单图资产操作成本,不套初始草稿总成本。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "mud points"、npm run test -- src/services/bark-battle-creation/barkBattleCreationClient.test.ts、cargo test -p api-server --manifest-path server-rs/Cargo.toml resolves_mud_point_cost initial_generation_slot_cost_splits_creation_entry_total_cost -- --nocapture。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、server-rs/crates/api-server/src/creation_entry_config.rs、server-rs/crates/api-server/src/puzzle/handlers.rs、server-rs/crates/api-server/src/match3d/draft.rs、server-rs/crates/api-server/src/bark_battle.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
generated 图片重复下载不要改成服务端本地磁盘缓存
- 现象:同一张 OSS generated 图片每次展示都重新从 OSS 拉取,或者完整 OSS 私有 URL 裸请求返回 403。
- 原因:前端输入如果是
https://*.oss-*.aliyuncs.com/generated-*,会被当普通绝对 URL 直连,绕过/api/assets/read-url和 signed URL 本地缓存;旧 OSS 对象如果缺少Cache-Control,浏览器只能依赖ETag/Last-Modified做 304 协商缓存,不会长期强缓存。 - 处理:完整 OSS generated URL 先归一成
/generated-*legacy public path,再走/api/assets/read-url换签;refreshKey是 signed URL 缓存版本号,同一路径、同一版本且未临近过期时必须复用,不要每次渲染都强制重新换签。新上传 generated 私有对象由platform-oss在PostObjectform fields / policy 和服务端PutObject请求头中写入Cache-Control: public, max-age=31536000, immutable。不要把 api-server 变成图片静态代理,也不要把 OSS 内容 fallback 到服务器磁盘。 - 验证:前端测试应看到完整 OSS generated URL 调用
/api/assets/read-url?legacyPublicPath=...,且相同refreshKey不重复换签;cargo test -p platform-oss --manifest-path server-rs/Cargo.toml应覆盖Cache-Controlpolicy、form field、PutObject headers 和 V4AdditionalHeaders;线上旧对象可用curl -I观察是否只有ETag/Last-Modified或已经补齐Cache-Control。 - 关联:
src/services/assetReadUrlService.ts、server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/platform-oss/README.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
小程序 H5 导航不能清掉宿主 query
- 现象:微信小程序首次进入 H5 后,点击需要登录的入口没有返回小程序原生授权页,而是弹出 Web 端登录窗口;充值渠道也可能被误判为普通网页环境。
- 原因:小程序
web-view入口通过clientType=mini_program、clientRuntime=wechat_mini_program、miniProgramEnv标记宿主环境,但 H5 内部pushAppHistoryPath(...)阶段导航会默认清空 query;首点时微信 JS bridge 也可能尚未就绪,导致isWechatMiniProgramWebViewRuntime()和充值平台判断读不到小程序上下文。 - 处理:路由层统一把
clientType、clientRuntime、miniProgramEnv当作 app runtime context,在普通路径归一、显式 query 路由和同一创作流跳转时都跨导航保留;小程序环境识别同时用MicroMessenger + miniProgramUser-Agent 兜底首点 bridge 未就绪场景;创作恢复参数仍只在同玩法创作流内保留,离开创作流时继续清理。 - 验证:
npm exec vitest run src/routing/appPageRoutes.test.ts src/components/auth/AuthGate.test.tsx src/services/authService.test.ts src/services/payment/paymentPlatform.test.ts。 - 关联:
src/routing/appPageRoutes.ts、src/services/authService.ts、src/services/payment/paymentPlatform.ts、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
平台异步错误必须带来源弹窗,不要只显示裸错误
- 现象:用户先后触发多个拼图或草稿生成时,旧请求失败后会在当前页面显示“图片生成失败”等裸错误,容易误判为当前正在看的拼图失败;错误文本也不便复制给开发排查。
- 原因:不同入口、生成页、结果页、作品详情和运行态各自渲染局部错误,没有统一携带草稿、生成会话、作品或游玩来源。
- 处理:跨流程错误统一由
PlatformEntryFlowShellImpl汇总为PlatformErrorDialog,来源使用玩法、草稿 / session / work / run 标识组成;弹窗提供复制按钮。关闭弹窗时只清理可安全清理的错误状态;恢复类错误用 dismiss key 防止反复弹出但不擅自改底层状态。 - 验证:触发任一平台级异步失败时,页面应出现包含“错误来源”和“错误内容”的弹窗;复制内容应包含来源和错误正文;旧页面内错误 banner 不再重复出现。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/platform-entry/PlatformErrorDialog.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
自定义世界旧公开作品不要用 published_at 判断是否存在
- 现象:RPG / 自定义世界作品详情能打开,但点赞时报
custom_world 已发布作品不存在,无法点赞,错误来源是作品详情 CW-*或其它自定义世界历史公开号。 - 原因:部分历史
custom_world_profile已是publication_status=Published,但published_at为空;统一公开详情会用updated_at兜底展示,旧点赞 / 游玩 / Remix 判断却额外要求published_at.is_some()。 - 处理:公开互动存在性统一按
Published + deleted_at=None + visible=true判断;custom_world_gallery_entry同步和公开展示时间在published_at缺失时回退updated_at。 - 验证:
cargo test -p spacetime-module custom_world_public_interactions_accept_legacy_missing_published_at --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/custom_world.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md。
拼图公开推荐不要只按 Published 判断
- 现象:后台把拼图作品隐藏后,作品不在公开列表里显示,但玩家通关其它拼图后的推荐下一作品仍可能出现这条隐藏作品。
- 原因:拼图隐藏只把
puzzle_work_profile.visible置为false,不会把publication_status从Published改走;通关推荐候选曾只通过by_puzzle_work_publication_status().filter(Published)取数,漏掉可见性判断。 - 处理:拼图公开消费路径统一使用
Published + visible=true,范围包括puzzle_gallery_view、puzzle_gallery_card_view、兼容 gallery/detail procedure、公开点赞 / Remix、正式公开 runtime 启动和通关后的recommended_next_works候选。 - 验证:
cargo test -p spacetime-module hidden_published_puzzle_work_is_not_public_visible_candidate --manifest-path server-rs/Cargo.toml,并在需要时用后台隐藏一个已发布拼图后重试通关推荐。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
推荐页 WF 点赞不要落到 RPG / custom-world
- 现象:推荐页里给
WF-*敲木鱼作品点赞时,平台错误弹窗显示custom_world 已发布作品不存在,无法点赞。 - 原因:推荐页点赞统一走
likePublicWork,但敲木鱼尚未接入点赞后端;缺少wooden-fish分支时会落入默认 RPG / custom-world 点赞路径,把敲木鱼的 owner/profile 传给 custom-world reducer。 - 处理:所有公开作品互动必须先按
packages/shared/src/contracts/playTypes.ts中的全局sourceType分流;暂未接入点赞的玩法直接报“该作品类型暂不支持点赞”,禁止显示开放兜底文案,也禁止用默认 RPG / custom-world 分支兜底。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation wooden fish like does not call RPG gallery like"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
暗色创作进度卡不要被 platform-remap-surface 改成深色文字
- 现象:统一创作页里的暗色进度卡背景是深绿 / 深蓝,但“创作进度”、百分比和进度提示显示成深色,移动端几乎看不清。
- 原因:
platform-remap-surface在浅色主题下会把后代[class*='text-white']强制重映射成var(--platform-text-strong),并且使用!important;暗色 hero 卡片如果只写通用text-white*,刷新后仍会被全局 remap 覆盖成深色。早期还混用了text-white/72、text-white/88、border-white/14、bg-white/12等不稳透明度档位,进一步放大了问题。 - 处理:给暗色 hero 加组件专属 class,例如
creation-agent-hero__progress-label、creation-agent-hero__progress-value、creation-agent-hero__progress-hint,并在src/index.css的 remap 规则之后用更具体选择器和!important固定白色透明度、边框和进度条底色。 - 验证:
CreationAgentWorkspace测试应断言进度标题、百分比和提示文本带专属 class;src/index.test.ts应断言这些 class 在 remap surface 内有白色覆盖规则;移动端截图中暗色卡片文字应保持可读。 - 关联:
src/components/creation-agent/CreationAgentWorkspace.tsx、src/components/creation-agent/CreationAgentWorkspace.test.tsx、src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
VectorEngine 图片生成 request_send 传输错误要按可重试网络抖动排查
- 现象:
external_api_call_failure里看到failureStage=request_send、statusCode=null,errorSource可能是client error (SendRequest)、[35] SSL connect error (Recv failure: Connection reset by peer)、[56] Failure when receiving data from the peer (... unexpected eof while reading ...);也可能看到failureStage=upstream_status、statusCode=502、错误体是 Nginx HTML502 Bad Gateway。前端只知道图片生成失败。 - 原因:
request_send表示请求未拿到可归类的 HTTP 响应,不会包含上游 JSON 错误体;upstream_status=502/5xx/429/408表示拿到了上游错误响应但仍属于可重试的过载 / 网关抖动。timeout=true来自超时判定,connect=true会同时覆盖 DNS / connect 失败以及 libcurl 35 SSL 握手、libcurl 56 收包提前 EOF、connection reset 这类临时传输错误。 - 处理:先按
provider/failureStage/statusClass聚合,再用user_id/profile_id和metadata_json.userId/profileId/requestId定位触发者、草稿 / 作品和同一次 HTTP 请求;request_send + timeout/connect=true或upstream_status + statusCode=408/429/5xx优先查 provider 日志的source_chain、请求体大小、参考图数量、出口网络、代理/Nginx、VectorEngine 当时可用性和同一 request_id 日志。当前platform-image对 request_send 的 timeout / connect / SSL connect reset / recv error / unexpected eof / send error,以及 upstream_status 的 408 / 429 / 5xx 最多发送 5 次,multipart/v1/images/edits每次重试都会重新构造 form;看到VectorEngine 图片请求发送失败,准备重试或VectorEngine 图片上游状态可重试,准备重试只是单次 attempt 失败,最终external_api_call_failure才代表该用户请求整体失败。若记录有429 moderation_blocked或明确审核错误,按审核失败另行处理,不要归到网络抖动。 - 拼图关卡资产生成按
level_scene -> ui_spritesheet -> level_background顺序执行,每个资产会输出slot、asset_kind、elapsed_ms;排查拼图草稿失败时优先看同一 request_id 下最后一个失败 slot。 - 验证:
cargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_send_retry_policy -- --nocapture、cargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_image_edit_retries_send_timeout_once_and_succeeds、cargo check -p api-server --manifest-path server-rs/Cargo.toml;查询tracking_event时失败记录应能看到触发者user_id和可用的profile_id。 - 关联:
server-rs/crates/platform-image/src/vector_engine/client.rs、server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳 Three.js 地块 UV 顶面要映射到 Z 轴
- 现象:跳一跳地块使用六面 UV 贴图后,看起来像贴图位置贴歪,顶面显示侧面纹理,或者旧单张地块图被拉到立方体多个面上。
- 原因:运行态以
z作为立方体竖直高度和相机下压方向,但 Three.jsBoxGeometry/RoundedBoxGeometry的默认材质 group 顺序把+Y当 top;如果直接按right / left / top / bottom / front / back写材质,玩法逻辑的top会贴到侧面。旧作品没有完整faceAssets时,把单张旧贴图强行作为 3D 六面 fallback 也会被误认为 UV 贴歪。 - 处理:Three 平台层只在
tileAssets[].faceAssets六面完整时启用;材质数组按 Three group 顺序写入right / left / back / front / top / bottom,把逻辑top映射到+Z顶面,并按每面 UV 方向做翻转校正;旧单图作品继续走 DOM 图片 / 原型兜底层。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖材质顺序、UV 翻转和旧单图不启用 Three 贴面;cargo test -p api-server jump_hop_tile_atlas_slicing --manifest-path server-rs/Cargo.toml -- --nocapture应覆盖 UV 安全边裁切。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、server-rs/crates/api-server/src/jump_hop.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
“我的”页每日任务卡不要硬编码进度,也不要跨日保留旧状态
- 现象:用户完成或领取每日任务后,任务中心弹窗里的任务状态已经变化,但“我的”页卡片仍显示
0 / 1和“去完成”。 - 原因:卡片首版只写了静态展示文案,没有读取
/api/profile/tasks返回的ProfileTaskCenterResponse,领取接口返回的新center也只用于弹窗;后来虽然后端按北京时间 0 点切换业务日,但前端停留在“我的”页时不会跨日刷新,可能继续展示上一日已领取状态。若认证成功后把daily_login当普通埋点写入,或历史profile_task_config仍保留旧profile.login.daily事件键,新业务日也可能写了登录事件却查不到任务进度。 - 处理:进入“我的”页时读取任务中心,卡片用当前可操作任务或已领取任务派生奖励、进度条和操作状态;
claimRpgProfileTaskReward(...)成功后用响应里的center覆盖本地任务中心;停留在“我的”页跨过北京时间 0 点时,先非阻断 refresh 登录态写入新业务日daily_login,再重拉任务中心。后端认证成功统一走SpacetimeClient::record_daily_login_tracking_event(...)与 SpacetimeDB 专用record_daily_login_tracking_event_and_return,默认每日登录任务读取时会把结算字段自愈到 canonicaldaily_login。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx应覆盖卡片从后端任务摘要显示1 / 1、领取后显示已完成,以及北京时间 0 点自动 refresh 后重拉任务中心。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
“我的”页不要恢复旧的填邀请码次级按钮
- 现象:移动端“我的”页在五项常用功能和设置入口下方又出现一个“填邀请码”按钮,看起来像旧入口残留。
- 原因:邀请码流程迁移后仍按新用户窗口保留
canShowReferralRedeemShortcut次级入口;但当前页面口径已经固定为五项常用功能宫格,邀请码填写应由邀请链接 query 或明确引导打开弹窗。 - 处理:移除常驻
次级入口/填邀请码渲染,不删除ProfileReferralModal的redeem面板,也不破坏?inviteCode=/?invite_code=自动打开填写弹窗。 - 验证:新用户账号打开“我的”页时没有
次级入口和填邀请码按钮;带?inviteCode=spring-2026的登录用户仍自动打开邀请码弹窗并预填SPRING2026。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、.codex/skills/genarrative-profile-invite-flow/SKILL.md。
创作卡片点击要直达已有入口表单,别再保留空白入口页
- 现象:创作 Tab 模板卡点击后如果仍然停留在创作大厅,或者先进入“X 创作入口”这种空白页,就会让用户多走一层,还可能被错误的 stage 白名单拉回平台。
- 原因:
/creation/<play>一度被接成空白创作入口页,导致SelectionStage、appPageRoutes和卡片点击分流被旧占位 stage 污染。 - 处理:把
/creation/<play>重新指向已有入口表单 stage,例如agent-workspace、big-fish-agent-workspace、match3d-agent-workspace、square-hole-agent-workspace、jump-hop-workspace、wooden-fish-workspace、puzzle-agent-workspace、bark-battle-workspace、visual-novel-agent-workspace、baby-object-match-workspace;平台壳层和测试同步清理空白入口页相关 helper。 - 验证:点拼图 / 抓大鹅 / 汪汪声浪卡片后,应看到各自既有工作台内容,例如测试中的
拼图工作区:missing-session、抓大鹅工作区:missing-session或汪汪声浪配置表单,并且不再出现“X 创作入口”空白页。 - 关联:
src/components/platform-entry/platformEntryTypes.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
创作流程刷新恢复必须写私有 query
- 现象:创作生成页或结果页刷新后回到空白工作区、平台首页,或者从作品详情返回时错误复用了别的玩法草稿。
- 原因:部分创作流程只把
sessionId/profileId/draftId/workId放在前端内存里,没有写进 URL;也曾把写 URL 放在 stage 切换前,writeCreationUrlState因为还停在非创作路径而直接跳过。若跨玩法或公开详情继续保留私有 query,还会污染/works/detail?work=...。 - 处理:创作页只使用私有 query
sessionId、profileId、draftId、workId做刷新恢复,不复用公开work参数;pushAppHistoryPath只在同一创作流内保留这些 query,离开创作流或切到另一个玩法必须清掉;手动 draft 打开、生成完成和保存回调要在路由已经切到/creation/<play>后再调用writeCreationUrlState。 - 验证:
npm run test -- src/services/creationUrlState.test.ts src/routing/appPageRoutes.test.ts src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx;手测生成页 / 结果页刷新仍恢复同一草稿,打开公开作品详情 URL 不带私有恢复参数。 - 关联:
src/services/creationUrlState.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
草稿作品架打开结果页返回必须回草稿 Tab
- 现象:从草稿 Tab 作品架点击已有草稿进入结果页后,点结果页返回会跳回创作 Tab 模板入口,用户需要重新切回草稿页才能继续找原草稿。
- 原因:平台壳层只按结果页类型硬编码返回创作入口,没有记录本次创作流是从草稿作品架打开;如果来源标记没有在新建入口时重置,还可能污染下一条创作链路。
- 处理:从作品架打开任一玩法草稿时标记返回目标为
draft-shelf;从创作 Tab 新建、打开模板或退出非草稿来源工作区时重置为create;结果页返回和工作区退出统一消费这个返回目标,并在消费后复位。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft result back button returns to draft hub when opened from shelf|agent draft result back button returns to draft hub without syncing result profile"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图生成页轮询不要绑展示 phase 或不稳定 setter
- 现象:拼图创作进入生成中页后,
/api/runtime/puzzle/agent/sessions/{sessionId}会在 0.3 到 0.5 秒内被反复 GET,看起来像轮询风暴,而不是 3 秒一次的正常刷新。 - 原因:轮询
useEffect同时依赖了拼图展示 phase 和会随父组件渲染变化的setSession函数,导致puzzleGenerationState的进度合并或页面重渲染就会重挂 effect;effect 里又会立即先请求一次 session,于是请求被放大成密集循环。 - 处理:拼图轮询只绑定
selectionStage、activePuzzleGenerationSessionId和“是否仍在生成中”这个布尔条件;setSession通过 ref 保持稳定,不让父组件重新渲染改变轮询器身份。进度 phase 变化只更新展示,不重建轮询。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft",并确认恢复生成中草稿后getPuzzleAgentSession不会因为进度刷新继续连发。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/platform-entry/usePlatformCreationAgentFlowController.ts、src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx。
小游戏恢复生成页不要只用请求 busy 判定是否生成中
- 现象:敲木鱼作品架里的生成中草稿点击进入生成页后,页面会显示“重新生成草稿”按钮,而不是继续显示素材生成中的等待态。
- 原因:平台壳恢复
generationStatus=generating草稿时会把isBusy置回 false,只保留MiniGameDraftGenerationState作为生成事实;生成页如果只把请求 busy 传给isGenerating,共用生成页会误判为空闲态并展示重试按钮。 - 处理:小游戏生成页的
isGenerating必须由isBusy || isMiniGameDraftGenerating(generationState)推导;跳一跳、拼消消、敲木鱼等从作品架恢复的生成页都要使用同一口径。 - 验证:
npm run test -- src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts应覆盖busy=false但敲木鱼 generation state 仍在生成中时继续隐藏重试入口。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/unified-creation/UnifiedGenerationPage.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图试玩恢复 query 必须先切到运行态路径再写
- 现象:拼图试玩或正式运行态打开后,刷新会停在“正在进入拼图关卡”,或地址栏只有
runtimeProfileId,缺少草稿runtimeSessionId。 - 原因:
writePuzzleRuntimeUrlState只会在当前路径已经是/runtime/puzzle时写入;如果先触发阶段切换再写 query,或者草稿作品摘要缺少sourceSessionId,就会把恢复参数写丢。App.tsx的 stage 同步也会改 pathname,所以顺序不对时容易只留下部分 query。 - 处理:进入拼图 runtime 时先
pushAppHistoryPath('/runtime/puzzle'),再setSelectionStage('puzzle-runtime'),最后写runtimeProfileId、runtimeSessionId、runtimeLevelId、work、mode;草稿 runtime URL state 允许从profileId反推puzzle-session-*,作为sourceSessionId的兜底。 - 验证:
npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t \"puzzle draft generation auto starts trial and runtime back opens draft result\",确认window.location.pathname === '/runtime/puzzle'且window.location.search同时包含runtimeProfileId和runtimeSessionId。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/services/puzzleRuntimeUrlState.ts、src/routing/appPageRoutes.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼消消草稿试玩不能只测 swap 回调
- 现象:拼消消结果页和 runtime shell 的单测都能通过,但真实页面里卡片只是交换,完全不会消除,顶部准备区还会因为已知的卡背占位路径显示坏图。
- 原因:草稿试玩走的是前端本地 runtime,早期测试只覆盖了
onSwapCards回调和局部状态,没有验证完整的消除、重力补牌、关卡完成和资源兜底链路;同时顶部卡背对puzzle-clear-card-back.webp这类已知缺失资源没有前置回退。 - 处理:草稿试玩的回归测试必须覆盖“交换 -> 完整图案消除 -> 补牌 -> 关卡完成”闭环,并在组件测试里验证真实点击/拖拽序列;顶部准备区卡背遇到已知占位路径时直接回退到
puzzle.webp这类可用参考图,不等图片加载失败后再兜底。 - 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx通过,浏览器 smoke 页实测可完成一次消除并弹出“本关完成”。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消消除过渡不能隐藏已有卡片的最终下沉格
- 现象:消除补牌过程中偶尔看起来下方有空位,但同列上方卡片没有落下来。
- 原因:后端和本地 runtime 的重力补牌已经把已有卡片压到底;真正的问题在前端过渡层。消除动画曾按旧消除坐标隐藏棋盘格,掉落动画也曾隐藏所有 drop 目标格。当某个旧卡下沉到刚被消除的格子时,最终 snapshot 里的真实卡片会被隐藏,视觉上像补牌没有落下。
- 处理:消除 / 掉落覆盖层只负责动画表现,不再隐藏已有场上卡片的最终格;只有从顶部准备区新补入、前一帧棋盘不存在的卡片,才允许临时隐藏底层目标格来配合下落动画。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx -t "已有卡片因重力下沉时目标格不被过渡状态隐藏成空位",并保留领域侧cargo test -p module-puzzle-clear refill --manifest-path server-rs/Cargo.toml。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、server-rs/crates/module-puzzle-clear/src/application.rs、docs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md。
拼消消完整消除反馈不要让补牌抢帧
- 现象:玩家正确拼完整组后,卡片几乎瞬间消失,顶部补牌马上出现或下落,导致“拼对了”的确认反馈很弱。
- 原因:前端一收到新 snapshot 就同时播放消除和掉落叠层,旧消除动画时长较短;新补入卡牌的下落延迟接近 0ms,视觉上会抢在消除反馈之前开始。
- 处理:局部正确拼合但未消除时只给锁定组做一次高光;完整消除时让旧卡片在消除叠层中短暂放大展示再淡出;新补入卡牌的下落延迟到淡出尾段,并继续只隐藏新补入目标格,不隐藏已有场上卡片下沉后的最终格。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx,浏览器里确认局部拼合会闪、完整消除会放大淡出、补牌在淡出后段才开始掉落。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/index.css、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
首页推荐分流参数不能条件性调用 hook
- 现象:桌面首页或移动首页在 HMR、断点切换或重新渲染后直接报 React hook 顺序错误,页面停在“正在加载内容”。
- 原因:
RpgEntryHomeView曾经写成const isDesktopLayout = isDesktopLayoutProp ?? usePlatformDesktopLayout();,当isDesktopLayoutProp存在时会跳过 hook 调用,导致 hook 顺序在不同渲染之间变化。 - 处理:先无条件调用
usePlatformDesktopLayout(),再用isDesktopLayoutProp ?? detectedDesktopLayout合并;不要把 hook 调用藏在条件表达式里。 - 验证:桌面与窄屏各刷新一次首页,控制台不再出现 hook 顺序错误;
npm run typecheck和首页推荐相关测试通过。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/platformEntryResponsive.ts。
泥点不足提示不要把用户退回创作入口
- 现象:拼图 / 抓大鹅 / 汪汪声浪等创作表单点击生成时,如果泥点不足,页面直接回到创作 Tab 玩法模板列表,刚填的表单内容随工作台卸载全部丢失。
- 原因:
PlatformEntryFlowShellImpl.tsx的ensureEnoughDraftGenerationPointsFromServer(...)曾在余额不足或余额读取失败时调用enterCreateTab()并setSelectionStage('platform'),把前置校验失败当作离开工作台处理。 - 处理:泥点前置校验失败只更新独立
UnifiedModal提示,不切换 stage,不清表单;余额读取失败也走同一弹窗口径。需要提示玩法内错误时可以保留局部错误位,但不得因此退出工作台。 - 验证:
npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle form checks mud points before creating a draft|match3d form checks mud points before creating a draft|bark battle form checks mud points before creating image assets"应断言弹窗出现、对应工作台仍在、玩法模板分类不再出现。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
内嵌泥点确认弹窗必须自带平台主题作用域
- 现象:拼图 / 抓大鹅统一创作页点击生成后,“确认消耗泥点”弹窗正文和按钮存在,但弹窗面板背景透明,只剩遮罩和文字。
- 原因:
PlatformMudPointConfirmDialog作为二级确认常以portal={false}内嵌到工作台局部 DOM,局部节点不一定继承.platform-theme;platform-modal-shell依赖--platform-modal-fill等主题变量,变量缺失时面板底色解析为空。 - 处理:共享泥点确认弹窗默认在 overlay 上带
platform-theme platform-theme--<theme>、platform-modal-backdrop和实色遮罩,在 panel 上带platform-modal-shell platform-remap-surface;单按钮状态弹窗也要有默认 light 主题,避免未来独立调用复现。 - 验证:浏览器触发
/creation/puzzle与/creation/match3d的泥点确认弹窗,检查 overlay 最近主题 class 存在、--platform-modal-fill有值且面板为实底;聚焦测试覆盖默认 overlay / panel class。 - 关联:
src/components/common/PlatformMudPointConfirmDialog.tsx、src/components/common/PlatformStatusDialog.tsx、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx。
拼图结果页关卡图不要裁切,嵌套图片预览要高于详情弹窗
- 现象:拼图结果页“拼图关卡”列表里的关卡图底部被裁掉;进入关卡详情后点击画面图,看起来没有打开全屏预览。
- 原因:关卡列表复用
PlatformMediaFrame aspect="standard"默认object-cover,方图或竖向生成图会在 4:3 框内被裁切;关卡详情弹窗自身层级高于CreativeImageInputPanel默认图片预览层级,预览实际打开但被压在详情弹窗后面。 - 处理:结果页关卡缩略图显式传
imageClassName="h-full w-full object-contain"保留完整画面;CreativeImageInputPanel提供mainImagePreviewZIndexClassName,嵌套在高层级弹窗内时由调用方传更高层级。 - 验证:聚焦测试断言关卡缩略图使用
object-contain且没有object-cover,并断言关卡详情内主图预览 overlay 层级高于详情弹窗;浏览器里检查列表完整显示图片,详情内点击画面图能打开可见预览。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.test.tsx。
图片大图预览不要复用白底工具弹窗
- 现象:点击图像输入面板里的参考图或主图预览后,页面只出现白底非全屏弹窗,背后原页面透出,不能缩放或拖拽查看细节。
- 原因:图片查看和工具弹窗共用了
UnifiedModal白底壳层;该壳层适合编辑 / 选择工具,不适合沉浸式看图,也没有图片边界拖拽状态。 - 处理:纯图片预览统一走
PlatformImagePreviewModal,全屏黑底展示,初始 contain 保证完整图片可见,缩放夹在1x-4x,拖拽位移按缩放后的图片边界夹取,避免把图片拖到露出背景。 - 验证:
npm run test -- src/components/common/PlatformImagePreviewModal.test.tsx src/components/common/CreativeImageInputPanel.test.tsx应覆盖黑底全屏、缩放上限、拖拽边界和关闭按钮。 - 关联:
src/components/common/PlatformImagePreviewModal.tsx、src/components/common/CreativeImageInputPanel.tsx。
玩法入口分类字段缺失要前端兜底
- 现象:平台创作入口初始化时,
platformEntryCreationTypes.ts直接对creationTypes[].categoryId/categoryLabel调trim(),一旦后端旧数据、局部 mock 或异常返回里缺字段,整个创作页会在derivePlatformCreationTypes(...)里直接炸掉。 - 处理:
normalizeCategoryId(...)和normalizeCategoryLabel(...)必须接收可空值,并分别回退到recommended/热门推荐;历史recent/最近创作也要归一到推荐分类。最近创作不属于模板分类页签,只能由真实草稿 / 作品架后端数据决定是否展示。 - 验证:
npm test -- src/components/platform-entry/platformEntryCreationTypes.test.ts,再打开本地创作页确认能正常进入创作 Tab。 - 关联:
src/components/platform-entry/platformEntryCreationTypes.ts、src/components/platform-entry/platformEntryCreationTypes.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作入口公告不要恢复前端固定两卡
- 现象:点击底部加号进入的创作入口页只展示固定的拼图 / 抓大鹅主题卡,后台改公告表单后前台没有变化。
- 原因:前端重新硬编码 banner 列表,绕过了
GET /api/creation-entry/config的eventBanners配置。 - 处理:创作入口页公告位优先读取后端
eventBanners数组,多条自动轮播;旧eventBanner只做单条兼容兜底。后台主格式是标题与 HTML 内容表单,保存时序列化为后端eventBannersJson传输字段,只允许受控 HTML 片段经空权限 iframe 展示,不执行 JSX 或直接 DOM 注入。 - 验证:后台保存两条以上公告后,点击底部加号进入创作入口页应自动轮播这些后台配置项;
CustomWorldCreationHub相关测试应断言标题来自后端配置。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、server-rs/crates/module-runtime/src/application.rs、apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx。
创作入口 banner 默认图片路径必须真实存在
- 现象:创作页顶部 banner 返回旧结构化
eventBanner时,前端<img>请求/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png,但public/下没有该文件,导致 banner 背景图加载失败。 - 原因:旧库
event_banners_json=None时,读取层把旧单条结构化 banner 当成eventBanners优先数组下发;同时旧结构化默认coverImageSrc指向已经不存在的品牌素材路径。 - 处理:
module-runtime在event_banners_json缺失或不可解析时回到默认公告数组;默认 HTML 公告和旧结构化默认coverImageSrc都引用public/下真实存在的/creation-type-references/puzzle.webp。 - 验证:
cargo test -p module-runtime creation_entry_event_banners_none_returns_default_announcements --manifest-path server-rs/Cargo.toml;重启本地api-server后GET /api/creation-entry/config的eventBanners[0]不再指向缺失的/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png。 - 关联:
server-rs/crates/module-runtime/src/application.rs、server-rs/crates/module-runtime/src/domain.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
移动端草稿卡不要长按选中文字
- 现象:移动端草稿页长按作品卡标题或摘要时触发系统文字选区,容易误触并打断作品架操作。
- 处理:移动端只对
#platform-tab-panel-saves .creation-work-card禁止user-select和-webkit-touch-callout;输入框、文本域和[contenteditable='true']保留文本选择能力,避免破坏真实编辑场景。 - 验证:移动端草稿页长按普通作品卡文字不出现系统选区;
src/index.test.ts应覆盖 CSS 选择器和可编辑控件例外。 - 关联:
src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
草稿页未读点不要继续用红色 literal
- 现象:草稿页底部 Tab 和作品架的未读点视觉上仍像红点,或 glow 仍带红色阴影,和平台暖棕体系不一致。
- 原因:
platform-nav-unread-dot、creation-work-card__unread-dot直接写了#b64a35和rgba(239, 68, 68, ...),没有收口到统一 token。 - 处理:未读点颜色统一走
--platform-unread-dot-fill/--platform-unread-dot-glow,桌面/移动端共用同一口径;不要把红色 literal 再写回样式。 - 验证:
src/index.test.ts断言两个 unread dot block 都只引用未读点 token,不再出现红色 literal 或红色 glow。 - 关联:
src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作 Tab 模板卡不要复用暗图蒙版参考卡样式
- 现象:创作 Tab 两列玩法卡上图能看到,但标题、描述或预计消耗泥点在白底信息区里看不见,或只剩泥点小图标。
- 原因:旧
platform-creation-reference-card是给暗图蒙版卡用的全局样式,会把卡片及全部子元素强制成白色文字;参考图要求的是“上图 + 下方白底信息区”,继续复用旧类会让白底上的文字消失。 - 处理:创作 Tab 首屏模板卡使用独立
creation-template-card、creation-template-card__body、creation-template-card__title、creation-template-card__subtitle和creation-template-card__cost结构,不挂platform-creation-reference-card;旧弹层如果仍是暗图蒙版卡,可以继续保留旧类。 - 验证:浏览器创作 Tab 中每张开放态卡都应显示标题、描述和后台契约
mudPointCost数量经前端格式化后的泥点消耗文案;旧契约缺字段时兜底显示10泥点数;npm test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx -t "creation start card renders reference-aligned banner and template metadata"应通过。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、src/index.css、src/components/custom-world-home/CustomWorldCreationHub.test.tsx。
创作首屏开放态卡片不要再显示左上状态标签
- 现象:创作 Tab 的开放态玩法卡左上角会重复显示“可创建”或“可创作”,视觉上比其它状态更吵,还会和封面图抢注意力。
- 原因:卡片渲染层默认把
badge当成所有状态都要展示的左上角标签,没有区分开放态与非开放态。 - 处理:开放态卡片不渲染左上标签,仅保留标题、描述和右下角消耗信息;
敬请期待、即将开放等非开放态标签继续保留。 - 验证:创作首屏 HTML 中不应包含
可创建/可创作,但仍应包含即将开放等非开放态状态。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
发现 / 创作 / 草稿页不要把根内容区再包成全局卡片壳
- 现象:发现页、创作页或草稿页根区一旦套回
platform-page-stage,页面边缘会立刻变得更厚,频道标签、列表和模板卡的横向空间都被挤窄,看起来像回到了旧全局卡片壳。 - 原因:
platform-page-stage本身是全局内容卡片壳,适合推荐页、我的页和其它页面,但这三页已经有自己的视觉结构;草稿页顶部筛选若继续用旧platform-tab,还会和发现页频道标签不一致。 - 处理:这三页的根内容区只保留
platform-remap-surface,不要再加platform-page-stage;草稿页顶部筛选复用发现页的platform-mobile-home-channel与platform-mobile-home-channel--active。 - 验证:浏览器里这三页的根区应仍保留
platform-remap-surface,但不再出现platform-page-stage;草稿页顶部筛选样式应和发现页频道标签一致。 - 关联:
src/components/custom-world-home/CustomWorldCreationHub.tsx、src/components/custom-world-home/CustomWorldWorkTabs.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、src/index.css。
统一创作壳现在自己负责页面滚动和四条入口外壳
- 现象:统一创作页最初只包住拼图、抓大鹅和敲木鱼的工作台内容,跳一跳仍然保留独立工作台壳,页面级滚动职责也散落在平台入口 motion wrapper 里,导致移动端不同入口的可见外壳不一致。
- 原因:
UnifiedCreationPage只做了标题和隐藏契约,入口壳还在各自工作台里保留platform-remap-surface/overflow-y-auto,jump-hop也没进入统一 spec。 - 处理:把
jump-hop纳入unifiedCreationSpec,让UnifiedCreationPage自己承担页面级滚动与统一标题栏;JumpHopCreationWorkspace、WoodenFishCreationWorkspace补unifiedChrome/showBackButton,平台壳不再给这几条统一入口套额外滚动壳。 - 验证:
npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx通过后,/creation/puzzle、/creation/match3d、/creation/jump-hop、/creation/wooden-fish都应由同一套统一创作页外壳承载。 - 关联:
src/components/unified-creation/UnifiedCreationPage.tsx、src/components/unified-creation/unifiedCreationSpecs.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
统一创作编排层不要再让平台壳直挂旧工作台
- 现象:平台入口壳已经切到统一创作外壳,但源码里仍直接 lazy import 并渲染四个旧工作台分支,看起来还是四套入口编排。
- 原因:统一创作页只收口了可见外壳,入口层没有再抽一层统一创作编排组件,导致平台壳依旧要认识各玩法旧工作台。
- 处理:新增
UnifiedCreationWorkspace,由它内部按playId选择真实工作台;平台壳只依赖这一层,不再直接挂旧工作台分支。旧工作台已迁入src/components/unified-creation/workspaces/,不再是入口编排事实源。 - 验证:
PlatformEntryFlowShellImpl.tsx中不应再出现四个旧工作台的入口渲染分支,创作 Tab 与/creation/<play>仍能正常进入对应工作台。 - 关联:
src/components/unified-creation/UnifiedCreationWorkspace.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
Jenkinsfile 开头不能带 UTF-8 BOM
- 现象:
Genarrative-Stdb-Module-Publish在Pipeline script from SCM读取jenkins/Jenkinsfile.production-stdb-module-publish后,流水线还未进入任何 stage 就失败,报java.lang.NoSuchMethodError: No such DSL method 'pipeline',堆栈位置是WorkflowScript.run(WorkflowScript:1)。 - 原因:该 Jenkinsfile 文件前三字节是 UTF-8 BOM
EF BB BF,Jenkins/Groovy 把它拼进首个标识符,导致实际调用的是\ufeffpipeline而不是 Declarative Pipeline 的pipeline全局。 - 处理:仓库内
jenkins/Jenkinsfile.production-*保存为 UTF-8 without BOM;不要为了解决 Windows PowerShell 5.1.ps1中文解析问题而给 Jenkinsfile 本身加 BOM。只有 Jenkins helper 临时写出的.ps1才按需要转成 UTF-8 with BOM。 - 验证:检查
jenkins/Jenkinsfile.production-stdb-module-publish文件开头字节不再是EF BB BF,并用 JenkinsvalidateDeclarativePipeline或重放Genarrative-Stdb-Module-Publish,不应再停在No such DSL method 'pipeline'。 - 关联:
jenkins/Jenkinsfile.production-stdb-module-publish、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Full Build 的维护退出节点不得 checkout Git
- 现象:Full Build 的 Stdb、API 和 Web 都已发布成功,
Exit Maintenance进入目标部署 agent 后却先执行checkout scm,用ssh://git@127.0.0.1:2222/...拉仓库并报Connection refused,导致已部署的维护退出脚本根本没有执行。 - 原因:
127.0.0.1:2222只是 Jenkins controller 上的 Gitea SSH 端口,在部署 agent 上代表部署机自身。该次流水线在 Jenkins 重启后恢复,Declarative 的阶段agent路径未继续遵守顶层skipDefaultCheckout(true),在steps前注入了不必要的 SCM checkout。 - 处理:
Exit Maintenance保持agent none,在steps内根据DEPLOY_TARGET用显式node(deployLabel)分配目标机,只从绝对路径执行 current release 已携带的/opt/genarrative/current/scripts/deploy/maintenance-off.sh。不要在这个节点添加 GitSCM、Git SSH 凭据或 Jenkins workspace 相对路径。 - 验证:运行
npm run check:production-ops;重放流水线时,Exit Maintenance的Running on <deploy-agent>之后应直接进入sh,不应出现checkout、GitSCM或 Git 凭据日志。 - 关联:
jenkins/Jenkinsfile.production-full-build-and-deploy、scripts/check-production-ops-guardrails.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Linux 多用户 dev 端口冲突先查系统级端口段注册表
- 现象:同一台 Linux 机器上多个用户同时开发时,
npm run dev报端口段已被其他用户占用、同一用户已有活跃端口段,或 SpacetimeDB 复用记录指向当前用户端口段之外的地址;未手动指定时自动分配应从10000-10099起步。 - 原因:Linux dev 脚本会通过
/var/tmp/genarrative-dev-port-ranges/registry.json做系统级端口段分配,避免两个用户配置相同或重叠端口段;同一用户后续启动会继续复用自己已经占用的固定端口段。注册表会保留该用户的段记录,不会因为多开而要求重新分配。 - 处理:先确认当前用户已经占用的端口段,再让后续
npm run dev/dev:*继续沿用这段;如确实要切换段,手动释放或清掉对应 registry 记录后再重启。需要临时隔离测试时用GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR=<tmp-dir>覆盖注册表目录。不要在 Windows 上按这个注册表排查,Windows 仍走原有端口探测与漂移逻辑。未指定端口段时,系统会从10000-10099开始顺序分配。 - 验证:重新启动后终端应打印
[dev] port-range: <start-end> (<user>)与[dev] port-range-registry: .../registry.json;node node_modules/vitest/vitest.mjs run scripts/dev-stack-port-utils.test.ts scripts/dev.test.ts应通过 Linux registry、自动分配10000-10099与 Windows bypass 用例。 - 关联:
scripts/dev-stack-port-utils.mjs、scripts/dev.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
SpacetimeDB 入口迁移 helper 合并时不要只保留调用
- 现象:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml或 JenkinsGenarrative-Stdb-Module-Build报E0425 cannot find function migrate_rpg_entry_from_old_hidden_default in this scope,位置在server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs的默认入口配置播种流程。 - 原因:分支合并时保留了
seed_creation_entry_config_if_missing(...)中的迁移调用,但漏掉了同文件内的 helper 定义;该 helper 负责把历史默认隐藏的 RPG 入口纠偏为当前开放默认值。 - 处理:恢复缺失的迁移 helper,不要直接删除调用。helper 只能匹配历史默认种子(标题、副标题、badge、图片、visible/open、排序都一致)后再更新,避免覆盖后台入口开关的人工配置。
- 验证:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
抓大鹅新 UI spritesheet 不要回退成中心容器图
- 现象:新素材流程生成后,运行态棋盘中心可能叠出一整张 UI spritesheet,导致按钮素材、方格和空白图集覆盖容器区域。
- 原因:为了兼容旧 DTO,后端可能把
uiSpritesheetImage*同步写入历史containerImage*字段;旧前端只看containerImage*,会误把 UI 图集当透明中心容器。 - 处理:读取中心容器图时先比较归一化后的
containerImage*与uiSpritesheetImage*。两者同源时忽略containerImage*,只把它作为旧数据兼容字段;新流程背景图本身已经保留容器,运行态只需加载背景和解析 UI / 物品 spritesheet。 - 验证:
npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx应覆盖“运行态不把兼容写入的UI spritesheet当中心容器图”。 - 关联:
src/components/match3d-runtime/Match3DRuntimeShell.tsx、server-rs/crates/api-server/src/match3d/mappers.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
通用系列素材图集先看 platform-image,不要先翻 api-server 大文件
- 现象:排查跳一跳、抓大鹅或其它玩法的系列素材图集切片 / 去绿 / 持久化时,最容易先打开
api-server/src/generated_asset_sheets.rs,结果在一个 60KB+ 大文件里找实现、测试和辅助函数,定位很慢。 - 原因:这条通用图片 seam 已经下沉到
server-rs/crates/platform-image/src/generated_asset_sheets/,api-server只剩薄包装和调用方兼容;继续把api-server当真值源会把理解路径拉回旧位置。 - 处理:先看
server-rs/crates/platform-image/src/generated_asset_sheets/mod.rs、prompt.rs、sheet.rs、alpha.rs、persist.rs和error.rs,再看api-server/src/generated_asset_sheets.rs的 AppError / AppState 适配和玩法调用点。 - 验证:
cargo test -p platform-image --test generated_asset_sheets --manifest-path server-rs/Cargo.toml通过,且cargo check -p api-server --manifest-path server-rs/Cargo.toml保持绿灯。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/、server-rs/crates/api-server/src/generated_asset_sheets.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
图片画布 UI 提取素材切片不要把断开的高光阴影当独立图标
- 现象:图片画布提取 UI 素材后,右侧素材库出现很小的废图;主体图标的阴影、反光、高光或小装饰不完整。
- 原因:图标 spritesheet 切片按 alpha 连通域识别素材,模型常把软阴影、高光、小星星等画成与主体断开的透明块;如果直接逐连通域出图,小碎片会抢占图标顺序,主体也会缺边缘装饰。
- 处理:在
platform-image的sheet.rs里先合并靠近主体的辅助连通域,再过滤孤立小碎片,最后给裁剪框保留安全 padding。不要在前端素材卡或画布层里修已经切坏的 PNG。生成图标素材入口只回填扣绿后的整张图集,不再拆分独立图标。 - 验证:
cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml覆盖断开的高光合并和孤立小碎片过滤;调用方补跑cargo test -p api-server editor_icon --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs、server-rs/crates/api-server/src/editor_project.rs。
UI spritesheet 不要依赖模型直接生成透明背景
- 现象:拼图或抓大鹅运行态解析 UI spritesheet 时,把整张背景图、棋盘格、叶子或装饰图也当作 UI 素材区域,按钮映射错乱;截图里常表现为底部按钮区只剩透明棋盘格或素材碎片。
- 原因:前端解析依赖 alpha 连通域检测,透明背景是前提;但生图模型收到“透明背景 spritesheet”提示后仍可能输出带实景背景或伪透明棋盘格的普通不透明 PNG,OSS 中保存的图没有真实 alpha。
- 处理:UI spritesheet 提示词应要求统一单一纯绿色
#00FF00 / RGB(0,255,0)绿幕背景,而不是让模型直接产透明背景;后端在上传 OSS 前复用generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha(...)把绿幕扣成真实透明 PNG,再把透明图写入uiSpritesheetImageSrc/uiSpritesheetImageObjectKey。 - 验证:
cargo test -p api-server puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_level_scene_spritesheet_and_background_requests_use_references --manifest-path server-rs\Cargo.toml、cargo test -p api-server match3d_derived_asset_prompts_match_three_sheet_pipeline --manifest-path server-rs\Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle/generation.rs、server-rs/crates/api-server/src/match3d/works.rs、server-rs/crates/api-server/src/generated_asset_sheets.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼 hit object 不要只相信透明底 prompt
- 现象:苹果等主题试玩时,中央敲击物图带明显黑底;背景图中央还可能出现苹果主体,或背景环境图偶发变成纯绿色底,和“中央只叠加 hitObjectAsset”的运行态设定冲突。
- 原因:gpt-image-2 对“透明底”和“背景只做外围氛围”的遵循不稳定。若 hit object 直接入库,黑底会被当成真实像素展示;若背景 prompt 只有软描述,模型会把主题主体画进中央。第一步为了去背刻意要求绿幕图时,如果第二步参考图或 prompt 没有切断绿幕语义,背景图也可能继承纯绿色画布。
- 处理:敲木鱼 hit object prompt 固定要求先输出
1:1单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景主体图,再由api-server只对绿幕背景做去绿透明化;不要回到黑底 / 白底 / 透明底 prompt 后再做泛抠图。背景生成必须使用第一步抠图完成后的透明图作为参考图,并在 prompt 中显式禁止继承绿色底色、绿幕底色或纯绿色画布;背景 prompt 还要固定要求中央 40% 主体预留区干净,禁止主题主体、局部特写、轮廓影子、重复元素和主题碎片,只允许外围氛围。不要在背景 prompt 写“木鱼预设在屏幕中央位置”或类似中心主体正向描述,运行态敲击物只能由前端叠放。 - 验证:
cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml,并用花朵 / 苹果 / 玉米主题跑试玩图确认绿幕被去除、主体未被抠除、背景中央不出现主题主体,背景环境图不再出现纯绿色底。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼返回按钮不要让模型自由发挥外圈花纹
- 现象:返回按钮试玩图有时会被画成徽章、花盘、浮雕圆牌,甚至出现复杂外圈和装饰花纹,左箭头反而不够突出。
- 原因:prompt 只说“主题化返回按钮”时,image2 会把参考图里的装饰语言一起学进去;如果没有把形状收束到“标准圆形 + 单个居中左箭头”,模型会优先补造型而不是补图标。
- 处理:返回按钮生成 prompt 必须只允许参考图约束圆形底色与箭头配色,明确禁止复杂造型、花纹、浮雕边、异形外框和装饰图案,按钮本体固定为标准圆形,视觉尺寸比当前模板再放大约 50%,圆形外沿需要一圈与主题色搭配的干净外描边。
- 验证:
cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml,并重新试玩确认返回按钮只剩圆形底色和中央左箭头。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md.
敲木鱼历史已发布作品缺返回按钮要补齐,不要靠推荐过滤
- 现象:推荐页或公开列表中的历史敲木鱼作品点击运行态时报
敲木鱼运行态需要完整作品配置,但这类作品的敲击物、背景、音效和飘字都已完整,只是backButtonAsset为空。 - 原因:早期已发布作品缺少统一的默认返回按钮快照;运行态启动时如果仍直接按完整配置校验,就会把可玩的历史作品拒掉。这个问题不应通过推荐流或公开列表过滤解决。
- 处理:
spacetime-module在start_wooden_fish_run_tx和 work snapshot 构建时,若作品已发布且generationStatus=ready,但仅缺backButtonAsset,就补写内置默认返回按钮/UI/11_left_arrow.png,再继续进入运行态。默认返回按钮以bundled-default资产快照写回 work profile,字段保持assetId=wooden-fish-default-back-button、imageObjectKey=public/UI/11_left_arrow.png。 - 验证:历史木鱼作品点击运行态不再报完整作品配置缺失;第一次进入后,work profile 里应补出
backButtonAsset。 - 关联:
server-rs/crates/spacetime-module/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼创作生成不要沿用 15 秒会话超时
- 现象:敲木鱼工作台点击“生成”后,前端直接提示
请求超时:15000ms,但后端和 VectorEngine 未必已经失败。 - 原因:
createCreationAgentClient的createSessionTimeoutMs默认是 15 秒;敲木鱼创作链路会继续进入生成页并执行多次 image2 edits、去绿背景处理和 OSS 写入,单次请求窗口如果继承共享默认值,会早于业务生成完成被前端中断。 - 处理:敲木鱼 client 必须单独配置长等待窗口,同时覆盖
createSessionTimeoutMs与executeActionTimeoutMs;不要修改共享默认值影响其它轻量创作 Agent。 - 验证:
npm run test -- src/services/wooden-fish/woodenFishClient.test.ts,并在本地触发一次木鱼创作确认不再出现 15 秒前端超时。 - 关联:
src/services/wooden-fish/woodenFishClient.ts、src/services/creation-agent/creationAgentClientFactory.ts、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md。
敲木鱼创作“卡住”先查 2xx 慢请求
- 现象:敲木鱼工作台点击生成后长时间停留在生成页,看起来像卡住;
api-server日志可能出现/api/creation/wooden-fish/sessions/{sessionId}/actions的2xx慢请求,耗时可达数分钟,例如latency_ms=525473。 - 原因:当前
compile-draft是同步 action,会串行等待敲击物、背景环境图、返回按钮图三次 image2 edits、去绿处理、OSS 写入和 SpacetimeDB 草稿写回;提示词生成音效已关闭,不应作为生成阶段。 - 处理:先确认日志中该 action 是不是最终 200;若是 200 慢请求,不要优先排查 WebSocket 或 SpacetimeDB procedure。前端生成页进度必须按“整理草稿 -> 生成敲击物 -> 生成背景环境图 -> 生成返回按钮图 -> 写入正式草稿”展示,并在未收到 action 回包前保持等待态,不宣称完成。
- 验证:
npm run test -- src/services/miniGameDraftGenerationProgress.test.ts -t "wooden fish",并观察木鱼生成页在 5 分钟以上等待时仍停留在合理阶段。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
本地 SpacetimeDB procedure 超时或缺失先查版本错配
- 现象:敲木鱼创作时点击“生成”提示
SpacetimeDB procedure 调用超时,或后台 Dashboard 的指标与柱状图同时消失;服务端日志更早出现Failed to BSATN deserialize procedure return value、No such procedure,Dashboard 请求返回502。 - 原因:本机
spacetimeCLI / standalone 版本与server-rs/Cargo.toml锁定的spacetimedb版本不一致时,procedure 返回值会在宿主侧反序列化失败,api-server 继续等待就表现成调用超时。若旧 worktree 已删除但其 orphan standalone 仍监听原端口,API 还可能连到旧 wasm:健康检查正常,新 bindings 对应的 procedure 却尚未发布。 - 处理:先用
spacetime --version和监听端口对应的/proc/<pid>/exe --version分别核对 CLI 与真实宿主,再和server-rs/Cargo.toml的锁定版本对齐;不能把新版本模块硬发布到旧宿主。旧实例仍有需要保留的本地数据时,先用迁移 procedure 导出,在独立端口启动匹配版本、发布当前模块并增量导入,逐表对账后再把本次 API 切到新实例;旧实例在对账前不停止。当前 dev 脚本会对带版本记录的本地实例校验dev-spacetime-tool-version,但显式连接历史端口时仍要核对真实进程和 module schema。 - 验证:CLI、standalone 与 Cargo 锁定版本一致,
/v1/ping正常,spacetime describe可找到调用中的 procedure;Dashboard 接口返回200且包含 4 张图,敲木鱼生成不再卡在 procedure timeout。另执行npm run test -- scripts/dev.test.ts验证本地调度门禁。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、server-rs/Cargo.toml、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
拼图 UI spritesheet 运行态不要二次包圆底或拉伸比例
- 现象:拼图运行态左上返回和右上设置按钮外面出现白色圆圈;底部“提示 / 原图 / 冻结”三枚素材被压扁、拉宽或拉成正圆,和图集原始按钮比例不一致。
- 原因:UI spritesheet 已经包含按钮视觉本体,但运行态仍给顶部按钮套默认圆形 icon 容器;底部三枚素材用
h-full w-full rounded-full铺满按钮格,覆盖了自动检测矩形的真实宽高比。 - 处理:有
uiSpritesheetImage*时,顶部返回 / 设置按钮容器只保留透明点击区和 focus 状态,不再叠加默认圆形底;buildPuzzleUiSpriteBackgroundStyle(...)对检测到的矩形写入aspectRatio,底部三枚素材按原始宽高比和最大尺寸渲染,不强制w-full。 - 验证:
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx、npm run test -- src/services/puzzle-runtime/puzzleUiSpritesheetParser.test.ts。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/services/puzzle-runtime/puzzleUiSpritesheetParser.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
2026-05-22 补充:展示矩形和点击热区要分开处理。puzzleUiSpritesheetParser 的 regions 保留完整视觉裁切矩形,hitRegions 用较高 alpha 阈值只包住实心按钮主体;运行态底部 spritesheet 道具按钮启用 puzzle-runtime-sprite-tool-button--precise-hit,父按钮不吃整块透明留白,内部 puzzle-runtime-ui-sprite-hit-zone 才接收指针事件,避免透明区域成为点击热区。
图像输入组件不要把业务状态藏在页面内联实现里
- 现象:拼图页把参考图上传、缩略图、主图删除确认和 AI 重绘开关内联实现后,后续想复用到其它创作页时,页面级状态和通用 UI 状态混在一起,容易出现多套上传卡和参考图展示口径。
- 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。
- 处理:抽
CreativeImageInputPanel时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。 - 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。
- 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx。
RPG 发布不能只依赖 agent session seed_text
- 现象:RPG 结果页
publish_world返回UPSTREAM_ERROR,details 为custom_world.setting_text 不能为空;同一 session 的result-view日志显示publish_ready=true。 - 原因:前端发布动作只提交
{ action: 'publish_world' },旧 agent 会话的seed_text可能为空;如果后端只从 action payload 或seed_text取setting_text,就会在最终 compile / publish 校验阶段失败。 - 处理:
module-custom-world::resolve_custom_world_publish_setting_text(...)以当前draft_profile_json为草稿真相,优先读取settingText、creatorIntent.rawSettingText、creatorIntent.worldHook、worldHook、anchorContent.worldPromise(.hook)、summary、name/title,最后才回退seed_text。 - 验证:
cargo test -p module-custom-world publish_setting_text --manifest-path server-rs\Cargo.toml;cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml。 - 关联:
server-rs/crates/module-custom-world/src/application.rs、server-rs/crates/spacetime-module/src/custom_world.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 已发布结果页进入世界不能重复 publish_world
- 现象:RPG 草稿发布成功后,按钮文案已变为“进入世界”,但点击仍请求
POST /api/runtime/custom-world/agent/sessions/{sessionId}/actions且 payload 为{"action":"publish_world"},后端返回publish_world is only available during object_refining, visual_refining, long_tail_review or ready_to_publish。 - 原因:按钮文案依据 agent session
stage === 'published'切换,但点击处理仍走发布协调路径;如果前端只依赖草稿同步回包判断是否已发布,回包为空或缺少可进入状态时就会继续重复发送publish_world。 - 处理:进入世界协调器接收当前 agent session stage;当 stage 已为
published时,只调用result-view回读已发布 profile 并启动运行态,不再调用sync_result_profile或publish_world。 - 验证:
npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx;确认已发布场景下syncAgentDraftResultProfile与executePublishWorld均未被调用。 - 关联:
src/components/rpg-entry/useRpgCreationEnterWorld.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 点击启动黑屏 / 默认 profile 先查 profile 归一化和摘要覆盖
- 现象:作品详情点击“启动”后页面切到 RPG runtime,但用户只看到黑屏、空白,或进入默认角色 / 默认 profile;从作品详情点“作品编辑”后开局 CG、封面、角色图、技能动作预览、初始物品图标或场景背景图丢失;DevTools 里可能同时看到旧自动存档
/api/runtime/save/snapshot被主动 cancel。 - 原因:
/custom-world-library//custom-world-gallery详情接口可能返回历史或摘要式profile,缺少playableNpcs、storyNpcs、landmarks、attributeSchema等运行态字段;前端 client 若直接把该对象传给 runtime,角色选择首屏会在buildCustomWorldPlayableCharacters(profile)或后续属性解析处抛错。另一类常见原因是详情接口已回读完整 profile 后,savedCustomWorldEntries里的列表摘要又把selectedDetailEntry覆盖回空 profile,导致启动或编辑时只剩卡片摘要。发布 / 回读 result-view 若返回字段更少的旧视图,也可能把当前结果页已编辑资产降级掉。save/snapshot (canceled)通常是切 runtime 或卸载时AbortController取消旧自动存档,不是黑屏根因。 - 处理:RPG 入口作品库 client 在所有返回
CustomWorldLibraryEntry<CustomWorldProfile>的接口边界统一调用normalizeCustomWorldProfileRecord,并用profileId/worldName/subtitle/summaryText补齐旧数据缺字段;详情页已拿到运行态字段或资产槽位更多的完整 profile 时,不允许列表摘要覆盖当前详情;同一profile.id下,正式进入世界发布 / 回读不得用字段更少的后端旧视图降级当前结果页 profile。normalizeCustomWorldProfileRecord必须近似无损保留cover、openingCg、camp.narrativeResidues、landmark.visualDescription/narrativeResidues、skills[].actionPreviewConfig、initialItems[].iconSrc、attributeSchema、角色attributeProfile和sceneChapterBlueprints[].acts[]的背景与结构字段;只有背景资产的 act 也不能被过滤。角色选择页对角色生成异常或空数组回退默认角色,并保留返回按钮/轻量空态;顶层 runtime 懒加载 fallback 不使用纯null。 - 验证:运行对应入口交互、结果 profile 归一化、创建恢复和类型检查用例,确认列表摘要不会覆盖已加载的完整作品资料。
- 关联:当前作品资料读取、结果回读和运行态入口模块;现行链路见
docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 战后一轮战斗后卡在观察/试探/调息先查 post-battle finalization
- 现象:RPG 一轮战斗胜利后,运行态只显示默认
观察周围迹象 / 主动出声试探 / 原地调息,这些按钮只有文字反馈;点“继续冒险”后又回到同样选项,点探索只播退场/进场动画,场景和剧情不推进。 - 原因:终局战斗 action 如果只走通用
resolve_story_runtime_actionfallback,而没有在后端调用finalize_post_battle_resolution(...),就不会持久写入story_continue_adventure、deferredOptions和下一幕currentSceneActState。另外旧 bootstrap 快照可能只有connectedSceneIds/forwardSceneId、没有connections,战后选项生成若只读connections也会退回idle_explore_forward循环。 - 处理:
module-runtime-story在 story action 投影后统一调用 post-battle finalization;idle_explore_forward清理战斗态并生成下一段遭遇预览;idle_travel_next_scene/camp_travel_home_scene由后端写入新currentScenePreset、场景 act 状态、遭遇预览和runtimeStats.scenesTraveled。前端只负责播放继续、探索和切场景动画,不承接正式剧情推进真相。 - 验证:
cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml battle_tests -- --nocapture应覆盖战斗终局持久化story_continue_adventure、deferredOptions、下一幕 act,以及idle_travel_next_scene真正切换场景。 - 关联:
server-rs/crates/module-runtime-story/src/session_action.rs、server-rs/crates/module-runtime-story/src/post_battle.rs、server-rs/crates/module-runtime-story/src/battle_tests.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 战斗飘字不要只靠低对比红绿文字
- 现象:暗色或棕黑噪声背景下,战斗伤害飘字看起来像背景纹理,尤其是远端敌人头顶的小号红字几乎不可读。
- 原因:旧
CombatFloatingNumber主要依赖text-rose-200/text-emerald-200和 8px 同色 glow;在暗红、棕黑、像素噪声背景上,颜色与背景混在一起,1px 深色描边也不足以形成轮廓。 - 处理:飘字本体使用高亮近白文字、小面积半透明深色底、明显深色描边和多层黑色阴影;只增强瞬时反馈,不新增说明面板,不遮挡主要战斗画面。
- 验证:
npm run test -- src/components/game-canvas/GameCanvasEntityLayer.test.tsx覆盖伤害/治疗飘字样式策略;运行态截图中敌方头顶伤害数字应能在暗场景上辨认。 - 关联:
src/components/game-canvas/GameCanvasEntityLayer.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
弹窗里复用 CreativeImageInputPanel 要保留画面卡高度
- 现象:拼图草稿结果页的关卡详情弹窗中仍能看到“画面图”标题、画面描述和生成按钮,但实际画面图卡片视觉上消失。
- 原因:
CreativeImageInputPanel内部依赖flex-1、h-full和max-h-full撑开正方形画面卡;放进弹窗里的普通section后,父级没有可计算高度,卡片会被压到不可见。 - 处理:通用画面卡
puzzle-image-upload-card保持aspect-square的同时设置稳定min-height,让入口页和关卡详情弹窗都能显示主图/上传区。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx -t "opens an independent level detail dialog"应断言关卡详情中的.puzzle-image-upload-card具备最小高度类;npm run test -- src/components/common/CreativeImageInputPanel.test.tsx应继续通过。 - 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.tsx、src/components/puzzle-result/PuzzleResultView.test.tsx。
Windows provision 下载截断要断点续传而不是回退目标机下载
- 当前状态:已废弃。2026-06-01 起生产 Jenkins 流水线统一切到 Linux agent,
Genarrative-Server-Provision不再维护 Windows 下载阶段。 - 现象:
Genarrative-Server-Provision在Download Provision Tool Archives阶段出现curl: (18) end of response ... bytes missing,常见于otelcol-contrib_0.151.0_linux_amd64.tar.gz等 GitHub release 大文件。 - 原因:这是 Windows Jenkins 节点到 GitHub 的响应体被截断;若每轮都删除
.download临时文件,就会丢掉已下载部分,下一次又从头开始。 - 处理:Windows 下载函数保留
${Output}.download,curl失败时下一轮使用-C -断点续传;最终只以 GitHub release asset 的 SHA256digest作为放行条件,完整返回但 digest 不匹配才删除临时文件重新下载。不要把 SpacetimeDB 或otelcol-contrib下载挪回 Linux 目标机。 - 验证:日志应显示
curl 断点续传 ... resumeBytes=...,最终出现已下载 ... bytes=...;目标 Linux 阶段只消费stash/unstash带过去的下载件。 - 关联:
jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
OTLP 端点只填 Collector HTTP base endpoint
- 现象:生产或容器 env 里把
OTEL_EXPORTER_OTLP_ENDPOINT填成4317、Rider 端口或别的非 HTTP base endpoint 后,api-server 发不出 OTLP,或者链路被错误转发。 - 原因:api-server 当前走 OTLP HTTP,不是 gRPC;Collector 才是接收和转发边界。
- 处理:生产模板用
http://127.0.0.1:4318,容器模板用http://otelcol:4318;需要关闭时显式设GENARRATIVE_OTEL_ENABLED=false,不要通过改 endpoint 绕开 Collector 语义。 - 验证:检查 env 模板和运行态配置都指向 Collector HTTP base endpoint,日志仍通过
journalctl/ 文件日志保留。 - 关联:
deploy/env/api-server.env.example、deploy/container/api-server.env.example、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
tracking outbox 到批量阈值后先封存再异步 flush
- 现象:route tracking 高峰时如果主请求线程要等 SpacetimeDB 批量入库,接口延迟会被 outbox 写入链路拖长。
- 原因:outbox 的职责是把普通 HTTP route tracking 从请求线程切走,不能把 flush 结果回写成同步阻塞。
- 处理:达到
BATCH_SIZE立即封存 active 文件并切新 active,FLUSH_INTERVAL_MS只做兜底封存,后台 worker 异步 flush sealed 文件;成功删文件,失败保留重试,坏文件隔离为corrupt-*,MAX_BYTES只做磁盘保护。 - 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
- 关联:
server-rs/crates/api-server/src/tracking_outbox.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳推荐页匿名直玩要同步放行 runtime 路由和埋点
- 现象:推荐页能看到跳一跳公开卡片,但未登录点击后会被登录门禁拦住,或者进入运行态后没有
work_play_start记录。 - 原因:前端只改了展示层登录门禁,后端 runtime 路由仍要求 bearer auth,或 tracking helper 仍把匿名请求当成无效输入直接丢弃。
- 处理:
/api/runtime/jump-hop/runs、/jump、/restart改为可选鉴权;未登录时直接允许启动、跳跃和重开,同时让work_play_tracking接受Option用户身份并在 metadata 中标记匿名语义,不要伪造 userId。 - 验证:未登录推荐页可以直接进入跳一跳运行态,且
work_play_start事件仍会落库或出现在 outbox 中,metadata 含匿名标记。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/api-server/src/auth.rs、server-rs/crates/api-server/src/work_play_tracking.rs、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx。
跳一跳直接打开空 runtime 路由不能停在加载态
- 现象:直接访问
/runtime/jump-hop时页面看起来一直停在“正在载入游戏 / 正在加载内容”,DOM 内部只有空的跳一跳运行态,没有平台、地块或 run 数据。 - 原因:
appPageRoutes会把该路径解析为jump-hop-runtime,但裸路径没有work=JH-*公开作品码,也没有从详情页启动后写入的jumpHopRun,平台壳仍挂载JumpHopRuntimeShell。 - 处理:平台壳在
jump-hop-runtime且缺少 run 时先看work参数;有JH-*则通过公开 gallery detail 回读 profile 并启动 published run,没有则回到平台首页。全局作品码恢复 effect 在跳一跳 runtime 阶段要跳过,避免和运行态恢复互相抢路由。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop runtime route";浏览器 smoke 分别打开/、/runtime/jump-hop和/runtime/jump-hop?work=JH-*。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/routing/appPageRoutes.ts、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
release tracking outbox 权限错误先查 env 缺失
- 现象:release 机器
journalctl -u genarrative-api.service每秒刷tracking outbox 定时封存 active 文件失败 error=Permission denied (os error 13)和tracking outbox 批量写入 SpacetimeDB 失败。 - 原因:旧
/etc/genarrative/api-server.env没有GENARRATIVE_TRACKING_OUTBOX_DIR时,api-server 会回退到本地开发默认相对路径server-rs/.data/tracking-outbox;systemd 工作目录是只读发布目录/opt/genarrative/releases/<version>,genarrative用户不能在其中创建server-rs。 - 处理:补齐
GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox及 batch/flush/max 配置,创建并授权/var/lib/genarrative/tracking-outbox给genarrative:genarrative,再重启genarrative-api.service。Server-Provision 与 API-Deploy 会保留旧 env 但自动补缺这些运行态路径。 - 验证:
tr '\0' '\n' < /proc/$(systemctl show genarrative-api.service -p MainPID --value)/environ | grep GENARRATIVE_TRACKING_OUTBOX_DIR应指向/var/lib/genarrative/tracking-outbox;重启后当前 PID 不再出现Permission denied (os error 13)。 - 关联:
scripts/deploy/production-api-deploy.sh、scripts/jenkins-server-provision.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
release otelcol 217/USER 和备份 timer inactive 分开处理
- 现象:release 巡检中
otelcol-contrib.service持续activating (auto-restart),日志出现status=217/USER/Failed to determine user credentials;同时genarrative-database-backup.timer显示enabled但inactive/dead,NEXT/Trigger为空。 - 原因:otelcol 的 systemd unit 使用
User=otelcol/Group=otelcol,但目标机缺少该系统用户和/etc/otelcol/genarrative-debug.yaml;备份 timer 在 missed window 后未处于 active waiting 状态,直接重启 Persistent timer 可能在白天立刻补跑冷备份并停止 SpacetimeDB。 - 处理:先创建系统用户 / 组
otelcol,补齐/var/lib/otelcol、/etc/otelcol/genarrative-debug.yaml和/var/log/genarrative,再重启otelcol-contrib.service;修 timer 时先touch /var/lib/systemd/timers/stamp-genarrative-database-backup.timer,再systemctl daemon-reload && systemctl start genarrative-database-backup.timer,避免当前窗口立即补跑冷备份。 - 验证:
otelcol-contrib.service为active (running)且监听127.0.0.1:4317/4318;systemctl list-timers genarrative-database-backup.timer --all显示下一次触发约为次日03:20;/healthz、/readyz、/v1/ping仍通过。 - 关联:
scripts/jenkins-server-provision.sh、deploy/systemd/otelcol-contrib.service、deploy/otelcol/genarrative-debug.yaml、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部 API 失败没法追溯先查 external_api_call_failure
- 现象:VectorEngine 图片生成 / 编辑接口对前端只表现为
502/504或“上游服务请求失败”,但难以区分是请求发送失败、上游 429/5xx、响应解析失败、未返回图片,还是下载图片失败。 - 原因:外部 API 失败如果只靠普通日志,不一定能和 OTLP 指标、trace 与 SpacetimeDB 历史查询稳定关联;重启后也容易丢失上下文。
- 处理:先查 OTLP 指标
genarrative.external_api.failures{provider,failure_stage,status_class,retryable},再查tracking_event中event_key = 'external_api_call_failure'的metadata_json。当前通用 VectorEnginegpt-image-2-all适配器会记录 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、errorSource、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt 和 requestId。 - 验证:
SELECT event_id, scope_id AS provider, metadata_json, occurred_at FROM tracking_event WHERE event_key = 'external_api_call_failure' ORDER BY occurred_at DESC LIMIT 50;;如果查不到同时看 tracking outbox 目录权限和 sealed 文件是否堆积。 - 关联:
server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
VectorEngine 图片协议先看 platform-image,不要先翻 puzzle.rs
- 现象:排查拼图或其它玩法的生图失败时,如果直接在
api-server的大文件里找images/generations、images/edits、base64 解码或下载逻辑,会看到很多历史 helper 和测试桥,看起来像每个玩法都自带一份 provider 实现。 - 原因:旧实现把 VectorEngine 图片 provider 协议、响应解析、下载和日志混在
api-server里,后来虽然迁出到platform-image,但兼容层和测试 helper 仍会让人误判真相源位置。 - 处理:先看
server-rs/crates/platform-image/src/vector_engine/:request.rs查路径和请求体,client.rs查生成 / 编辑编排,transport.rs查 HTTP client 与 reqwest 错误归一,payload.rs查响应字段提取,response.rs查上游状态、解析、缺图和下载分流,image_source.rs查参考图和远端图片下载。再看server-rs/crates/api-server/src/openai_image_generation.rs的兼容桥和external_api_audit.rs的落库映射;puzzle/vector_engine.rs只保留玩法编排,不再作为 provider 协议真相源。 - 验证:
cargo test -p platform-image --manifest-path server-rs/Cargo.toml、cargo test -p platform-image --test vector_engine --manifest-path server-rs/Cargo.toml、cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture通过时,排障先按platform-image的日志字段查 provider / endpoint / failure_stage。 - 关联:
server-rs/crates/platform-image/src/vector_engine/、server-rs/crates/api-server/src/openai_image_generation.rs、server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/puzzle/vector_engine.rs。
音频 provider 协议先看 platform-audio,不要先翻 api-server 大文件
- 现象:排查 Visual Novel 或通用创作音频生成失败时,如果直接打开
api-server/src/vector_engine_audio_generation.rs,会同时看到路由、计费、asset binding、下载、解析和 provider 协议,定位时很容易在同一个文件里来回跳。 - 原因:音频 provider 已经迁到
server-rs/crates/platform-audio/,但api-server仍保留薄 wrapper;如果把 wrapper 当真值源,就会误判边界。 - 处理:先看
server-rs/crates/platform-audio/src/client.rs、request.rs、response.rs、download.rs、persist.rs、error.rs,再看api-server/src/vector_engine_audio_generation.rs的路由、配置、计费、asset object confirm 和 entity binding 包裹。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml通过,且cargo check -p api-server --manifest-path server-rs/Cargo.toml保持绿灯。 - 关联:
server-rs/crates/platform-audio/、server-rs/crates/api-server/src/vector_engine_audio_generation.rs。
Hyper3D 现在只剩后端薄代理,不要再把协议解析写回 api-server
- 现象:排查 Hyper3D/Rodin 时,如果继续在
api-server/src/hyper3d_generation.rs里扩协议解析、请求体构造或下载列表处理,文件会重新变厚。 - 原因:
platform-hyper3d已经承接 Rodin 的提交、状态和下载协议解析;api-server只是薄 wrapper 和错误 envelope 映射。 - 处理:新增或修改 Hyper3D 协议时优先放到
server-rs/crates/platform-hyper3d/的client.rs、request.rs、response.rs、transport.rs和子模块,api-server只保留鉴权、配置校验和错误映射。 - 验证:
cargo test -p platform-hyper3d --manifest-path server-rs/Cargo.toml通过后再看cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-hyper3d/、server-rs/crates/api-server/src/hyper3d_generation.rs。
release 创作接口 413 先查是否还在提交 Data URL
- 现象:release 上
POST /api/runtime/puzzle/agent/sessions/{session_id}/actions携带参考图 Data URL 时返回413 Request Entity Too Large,access log 显示request_time=0.000、upstream_status=-。 - 原因:Nginx 默认
client_max_body_size只有 1 MiB,请求在反代层被拒绝,根本没有到达api-server;即使模板放宽到64m,把图片 base64 放进创作 JSON body 仍会放大请求体并把上限问题推给下一层。 - 处理:长期修复不是继续调大 Nginx,而是让浏览器先走
/api/assets/direct-upload-tickets直传 OSS,再/api/assets/objects/confirm确认asset_object,拼图 action 只提交referenceImageAssetObjectId(s);后端校验 owner / bucket / kind / MIME / size 后签只读 URL 给 VectorEngine。Nginxclient_max_body_size 64m只保留为旧客户端和兼容输入兜底,发布后仍需nginx -t && nginx -s reload。 - 验证:前端 action payload 不应再出现大段
data:image/...;base64;nginx -T 2>/dev/null | grep client_max_body_size可确认反代兜底;再次提交参考图时 access log 应有正常upstream_status,后端测试puzzle_reference_image_sources_prefer_asset_object_ids/puzzle_asset_object_reference_requires_matching_owner应通过。 - 关联:
src/services/puzzle-works/puzzleAssetClient.ts、server-rs/crates/api-server/src/puzzle/vector_engine.rs、deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、deploy/container/nginx.conf、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
汪汪声浪入口不要再回到独立配置阶段
- 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。
- 原因:旧实现把
bark-battle单独挂到bark-battle-configselectionStage,而不是复用创作 Tab 里的模板区。 - 处理:入口点击只设置
activeCreationFormType = 'bark-battle'并回到创作 Tab;BarkBattleConfigEditor作为内嵌表单使用,默认隐藏返回按钮和页面标题;runtimeonExit重新回到创作 Tab 的汪汪声浪模板。 - 验证:点击汪汪声浪后直接看到创作页内嵌表单,不再出现独立配置页;测试应覆盖内嵌表单与 runtime 返回路径。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
汪汪声浪发布态不要丢失结果页最终素材
- 现象:结果页上传或批量生成玩家形象、对手形象、UI 背景后,发布进入正式 runtime 仍可能显示初始草稿素材或兜底视觉。
- 原因:
publish_bark_battle_work如果只把结果页最终状态保存到published_snapshot_json,但正式 runtime 读取的config_json仍来自草稿行旧值,就会丢失结果页局部替换。 - 处理:发布时把最终
publishedSnapshot解析为BarkBattleEditorConfigSnapshot、规范化后同时写入bark_battle_published_config.config_json和published_snapshot_json;首轮自动生成只由bark-battle-generating负责,结果页仅覆盖已接入的玩家形象、对手形象和竞技背景图片槽位,不再提供音频配置入口。 - 验证:发布后 runtime config 应包含结果页最终
playerCharacterImageSrc、opponentCharacterImageSrc和uiBackgroundImageSrc。
汪汪声浪 v1 生成页和正式运行态要分开
- 现象:如果把初始三图自动生成、结果页修补、公开发布和正式运行态混在一页,创作者容易误以为一次生成和正式运行是同一职责。
- 原因:
bark-battle-generating才应该承担玩家形象、对手形象和竞技背景的自动生成;结果页只做单槽修补,正式 runtime 又必须切到真实麦克风和正式统计。 - 处理:表单提交后先进入独立生成页,部分失败仍进结果页;结果页只保留单槽重试、重新生成和上传,不再保留一次生成按钮、音频配置入口、皮肤预设入口或排名配置。发布后先到统一作品详情页,再进正式 runtime;草稿试玩允许 mock,不写正式 run。
- 验证:生成页负责首轮自动产出三图;结果页不出现一次生成按钮、音频配置入口、皮肤预设入口或排名配置;正式 runtime 必须麦克风可用且会写正式 run,草稿试玩不写正式统计。
汪汪声浪生成页不要只停留在前端内存草稿
- 现象:点击“生成草稿”后生成页一直转圈,或刷新 / 回到草稿架后看不到三图素材。
- 原因:生成页只在前端内存里合并玩家形象、对手形象和竞技背景,没有把生成结果写回
bark_battle_draft_config.config_json;另外 BFF 若在刚创建草稿后先读spacetime-client订阅 cache 再保存,cache 可能短暂落后,导致保存失败或返回旧快照。 - 处理:生成页三图完成后调用
POST /api/creation/bark-battle/drafts/{draftId}/config持久化;保存接口直接把请求快照交给 SpacetimeDB procedure,由模块事务校验 owner / work,并在 HTTP 回包用本次请求里的三图字段覆盖,避免订阅 cache 滞后;保存请求必须设置前端超时,保存失败也进入结果页并标记部分失败。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx src/services/bark-battle-creation/barkBattleCreationClient.test.ts src/components/bark-battle-creation/BarkBattleResultView.test.tsx packages/shared/src/contracts/barkBattle.test.ts;npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "bark battle";cargo check --manifest-path server-rs\Cargo.toml -p api-server。 - 关联:
src/components/bark-battle-creation/BarkBattleGeneratingView.tsx、src/services/bark-battle-creation/barkBattleCreationClient.ts、server-rs/crates/api-server/src/bark_battle.rs、server-rs/crates/spacetime-module/src/bark_battle.rs。
汪汪声浪三图不要复用 RPG 场景图链路
- 现象:玩家形象和对手形象看起来走了场景图片 prompt;生成页三个槽位同时转圈,但只有第一个真实生成,首图返回后三个槽位一起停止或只显示首图。
- 原因:前端曾复用
/api/runtime/custom-world/scene-image,三类素材都被当成 RPG landmark scene image;生成页又只用父级 draft 判断 ready,批量 Promise 结束后才一次性合并结果,缺少逐槽状态。 - 处理:Bark Battle 生图统一走
POST /api/creation/bark-battle/images/generate,请求体包含slot和 v1 配置;后端在api-server/src/bark_battle.rs按player-character、opponent-character、ui-background分别拼装正式 prompt,写入generated-bark-battle-assets,并返回prompt/actualPrompt。前端generateAllBarkBattleImageAssets保持三槽Promise.allSettled并通过onSlotComplete逐槽刷新生成页状态。 - 验证:
npm run test -- src/services/bark-battle-creation/barkBattleCreationClient.test.ts src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx packages/shared/src/contracts/barkBattle.test.ts;cargo test -p shared-contracts bark_battle --manifest-path server-rs\Cargo.toml;cargo check --manifest-path server-rs\Cargo.toml -p platform-oss -p api-server。 - 关联:
src/services/bark-battle-creation/barkBattleCreationClient.ts、src/components/bark-battle-creation/BarkBattleGeneratingView.tsx、server-rs/crates/api-server/src/bark_battle.rs、server-rs/crates/platform-oss/src/lib.rs。
抓大鹅批量重新生成物品不要新增 itemId
- 现象:结果页批量重新生成物品后,试玩或正式运行态的物品类型和图片对应关系漂移,或者用户输入一个不存在名称后被当作新物品追加。
- 原因:重新生成和批量新增共用
item-assets接口,如果前端不传mode = "replace",或后端替换时重新分配itemId/ 追加未匹配名称,就会破坏generatedItemAssets顺序和运行态类型映射。 - 处理:批量重新生成只提交当前素材列表中能匹配到的名称,并传
mode = "replace";后端只对同名已有素材生成新图片,合并时保留原itemId、itemName、模型兼容字段、UI 背景和历史音频字段,未匹配名称直接忽略且不计费。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx覆盖前端提交口径,cargo test -p api-server match3d_item_asset --manifest-path server-rs\Cargo.toml和cargo test -p api-server match3d_regenerated_asset --manifest-path server-rs\Cargo.toml覆盖后端替换计划与身份保留。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、server-rs/crates/api-server/src/match3d.rs、packages/shared/src/contracts/match3dWorks.ts、server-rs/crates/shared-contracts/src/match3d_works.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅生成封面图不要覆盖物品素材或配置
- 现象:结果页生成封面图后,
素材配置 > 物品中已有物品素材被清空、回退旧快照,或难度 / 消除次数被改回旧值。 - 原因:封面生成属于定向图片槽位更新;若后端复用草稿编译写回,可能按 session config 重算作品行。即使后端已修正,前端若直接把封面接口返回的整份
item当成最新 profile,也可能用旧回包里的空generatedItemAssets覆盖当前页面素材。 - 处理:
POST /api/creation/match3d/works/{profileId}/cover-image只保存coverImageSrc/coverAssetId等封面字段,保留当前generated_item_assets_json、难度、消除次数、题材和描述;前端收到回包后只合并coverImageSrc,继续保留当前可见generatedItemAssets、clearCount和difficulty。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx覆盖旧回包不覆盖物品素材和配置;cargo test -p api-server match3d_cover --manifest-path server-rs\Cargo.toml覆盖封面提示词与参考图链路。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、server-rs/crates/api-server/src/match3d.rs、server-rs/crates/spacetime-module/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
OSS V4 签名时间和 bucket/object_key 兼容
- 现象:OSS V4 私有读签名在部分时间点失败,可能出现
OSS V4 签名时间格式化失败或服务端判定签名格式错误;排查用例中 bucket 为xushi-dev,object_key 为generated-square-hole-assets/.../image.png。 - 原因:旧逻辑依赖
time::Time::to_string()再去掉冒号,小时小于 10 时输出不稳定补零;同时排查时容易把 bucket 名误当成 object_key 的一部分。 - 处理:OSS V4
x-oss-date使用固定宽度yyyyMMdd'T'HHmmss'Z'格式化;调用读签名或HEAD Object时只传 object_key,不要传bucket/object_key拼接路径。 - 验证:运行
cd server-rs && cargo test -p platform-oss -- --nocapture,并用 bucket=xushi-dev、object_key=generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png覆盖签名生成。 - 关联:
server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/platform-oss/README.md。
generated 音频路径进运行态前要先换签
- 现象:草稿页 audio 控件能播放背景音乐,但拼图或抓大鹅运行态开局后背景音乐不响,Network 可能出现裸
/generated-*-assets/...mp3私有路径 403。 - 原因:生成音乐转存到 OSS 私有对象后,
audioSrc是 generated legacy path;浏览器<audio>不能像公开静态资源一样直接请求裸路径。另一个常见误判是浏览器拒绝自动播放,资源已经进入运行态但开局第一次audio.play()被拦截。 - 处理:结果页试听控件和运行态隐藏
<audio>设置src前,都先通过useResolvedAssetReadUrl或resolveAssetReadUrl换签;签名未就绪时不要回退请求裸 generated 路径。运行态自动播放失败只静默兜底,但玩家首次按下拼图块或点击抓大鹅物品时要重试同一个背景音乐播放函数。拼图读取currentLevel.backgroundMusic.audioSrc,抓大鹅读取generatedItemAssets[].backgroundMusic.audioSrc。 - 验证:结果页试听和运行态
<audio loop>的src为签名 URL 或公开 URL;拼图/抓大鹅运行态首次局内交互后会再次尝试播放背景音乐;npm run typecheck不报契约字段缺失,后端 run response 带backgroundMusic。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md。
抓大鹅背景音乐是作品级字段但暂存在首个物品素材
- 现象:抓大鹅草稿生成日志和 work detail 中已有背景音乐,但结果页
素材配置 > 背景音乐显示“暂无音乐”,点击试玩后局内也不播放生成音乐。 - 原因:当前表结构没有作品级音频字段,背景音乐暂存在
generatedItemAssets[]。如果 action response 的 draft assets 缺音乐,前端又优先用它覆盖 work detail,或音乐落在非首个素材而结果页只读assetDrafts[0].backgroundMusic,就会丢掉已生成音乐。 - 处理:前端统一使用
normalizeMatch3DGeneratedItemAssetsForRuntime/mergeMatch3DGeneratedItemAssetsForRuntime:把任意素材上的backgroundMusic与音乐元信息迁移到首个素材,清空其它素材上的作品级音乐字段;action draft assets 与 work detail assets 按itemId合并,保留详情里的音乐、UI 背景和点击音效。 - 验证:
npm run test -- src\services\match3dGeneratedModelCache.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx;平台推荐流定向跑RpgEntryFlowShell.agent.interaction.test.tsx中的 Match3D runtime assets 用例;npm run typecheck。 - 关联:
src/services/match3dGeneratedModelCache.ts、src/components/match3d-result/Match3DResultView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
中文乱码与编码风险
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
- 原因:Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
- 处理:
- 不要直接沿用乱码文本。
- 不要用英文替换中文,除非用户明确要求翻译。
- 在 PowerShell 5.1 中显式使用 UTF-8。
- 优先用 Python/Node 或
Get-Content -Encoding UTF8核对原文。 - 修改中文文件时优先局部补丁,避免无关内容重写。
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:
AGENTS.md、npm run check:encoding。
SpacetimeDB 运行态查询不要绕过已有索引或用 procedure JSON 回传
- 现象:运行态接口看起来只查当前用户、作品或任务,却在
spacetime-module中使用ctx.db.<table>().iter().filter(...)整表遍历;或者 procedure result 返回items_json/run_json/work_json等 JSON 字符串,spacetime-clientmapper 再反序列化成旧兼容结构。 - 原因:新增索引或 typed snapshot 后,没有同步清理旧 mapper / 测试兼容层,也没有用静态检查拦截回退写法。
- 处理:表上已有主键、unique 或
#[index]覆盖查询前缀时,先用对应 accessor.find(...)/.filter(...),只对索引无法覆盖的条件做内存残余过滤;procedure result 返回 typed snapshot / typed value,不再跨层传*_json: Option<String>作为 payload。 - 验证:执行
npm run check:spacetime-runtime-access、npm run check:server-rs-ddd,涉及绑定变化时先执行npm run spacetime:generate和npm run check:spacetime-schema。 - 关联:
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、scripts/check-spacetime-runtime-access.mjs、server-rs/crates/spacetime-module/src/*、server-rs/crates/spacetime-client/src/mapper.rs。
拼图广场列表不要每次 HTTP 请求调用 SpacetimeDB procedure
- 现象:
/api/runtime/puzzle/gallery每个请求都走spacetime-client.list_puzzle_gallery()调用 SpacetimeDB procedure,导致 SpacetimeDB WASM 侧重复组装全量列表,客户端再映射一遍;历史实现还出现过 procedure JSON 字符串往返。 - 原因:
api-server的服务器端spacetime-client没有订阅可公开读取的 gallery 投影,虽然 SDK 支持 client cache,但请求路径仍把列表读取当作 procedure 调用。 - 处理:
spacetime-module中用 public viewpuzzle_gallery_card_view暴露已发布拼图作品的列表卡片字段,不携带levels/anchor_pack等详情级载荷;spacetime-client建连接后订阅SELECT * FROM puzzle_gallery_card_view和SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'并等待on_applied。HTTP gallery 通过PuzzleGalleryCache缓存最终PuzzleGalleryResponseDTO:items返回前 10 个完整卡片,previewRefs返回后 10 个作品号引用,cache miss / TTL 过期时单飞重建,后台 cleanup task 周期清理旧响应。旧list_puzzle_galleryprocedure 只作兼容,不再作为 HTTP gallery 主路径。 - 验证:搜索
server-rs/crates/spacetime-client/src/puzzle.rs不应再出现 gallery 主路径调用list_puzzle_gallery_then;搜索server-rs/crates/spacetime-client/src/lib.rs应订阅puzzle_gallery_card_view;执行npm run spacetime:generate、cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client、cargo check --manifest-path server-rs/Cargo.toml -p api-server和 schema/runtime access 检查。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/spacetime-client/src/puzzle.rs、server-rs/crates/api-server/src/puzzle_gallery_cache.rs、/api/runtime/puzzle/gallery。
Windows 本地直连高 VU 压测不要误判成业务内存泄漏
- 现象:本地 Windows release
api-server直连 K6 压测时,250 RPS、PREALLOCATED_VUS=300能把进程 private memory 瞬时推到约 7GB;同样配置打/healthz小响应也能复现,压测结束后回落到 100MB 级。 - 原因:高水位主要来自本机直连的 K6 VU / 长连接 / Hyper 发送链路和 Windows 连接缓冲,不是 SpacetimeDB procedure、拼图 JSON 缓存或 OTEL exporter。降低到接近真实并发的 VU 后,同样 250 RPS 拼图广场 p95 约 9ms,峰值约 600MB。
- 处理:本地容量判断时让
PREALLOCATED_VUS/MAX_VUS接近真实并发,不要把过高 VU 预分配当作默认吞吐测试;同时观察process.memory.*、process.windows.handle.count、genarrative.http.server.response_bodies.in_flight、genarrative.http.server.request_permits.available、genarrative.puzzle_gallery.cache.*和genarrative.spacetime.read.*。如果内存高但 body in-flight、背压 permit、cache rebuild 和 SpacetimeDB read 都不显示积压,优先按连接 / 发送链路高水位处理。 - 验证:对照打
/api/runtime/puzzle/gallery与/healthz;对比PREALLOCATED_VUS=300 MAX_VUS=800和PREALLOCATED_VUS=20 MAX_VUS=40;压测结束后继续采样 10 秒确认 private memory 回落。 - 关联:
scripts/loadtest/README.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、server-rs/crates/api-server/src/process_metrics.rs、server-rs/crates/api-server/src/telemetry.rs。
容器高 VU 下 /healthz RSS 尖峰先查 Axum state 深拷贝
- 现象:容器 Linux release
api-server打/healthz,500 HTTP req/s、PREALLOCATED_VUS=100只跑 1 秒也能把 RSS 推到约 1 GiB;同样问题与作品列表、SpacetimeDB procedure、业务 cache 和请求日志等级无关。 - 原因:
AppState曾直接#[derive(Clone)]大结构体,里面包含配置、SpacetimeDB client、平台服务、认证服务和多组 cache。Axum/Hyper 会在 router/service/connection 路径频繁 clone state,高并发 keepalive 下会放大为状态深拷贝高水位。 - 处理:
server-rs/crates/api-server/src/state.rs的AppState必须保持Arc<AppStateInner>浅拷贝壳;新增共享状态字段时放入AppStateInner,不要把外层改回大结构体 clone。 - 验证:用容器内 k6 直连
api-server:8082/healthz,500 HTTP req/s、PREALLOCATED_VUS=100、30 秒压测后采样/proc/$pid/status、/proc/$pid/smaps_rollup和 cgroupmemory.current/memory.peak。2026-05-18 修复后结果为15001请求、http_req_failed=0、dropped_iterations=0,RSS 约 18 MiB -> 52 MiB,cgroup peak 约 47 MiB。 - 关联:
server-rs/crates/api-server/src/state.rs、deploy/container/README.md、deploy/container/api-server.Dockerfile。
Gallery 压测延迟升高先查入口过量放行和 TTL 边界刷新
- 现象:公开作品列表在 500-1000 HTTP req/s 附近可能吞吐没有明显提升,但 p95 变高、VU 上升,甚至出现排队和 dropped iterations。
- 原因:Nginx、Axum 和缓存刷新边界如果同时允许过多请求进入,压力会先堆在连接、service 和 cache rebuild 周围;这类延迟不等同于数据库连接池不足。
- 处理:Nginx 按 endpoint 使用
limit_req快拒绝,api-server 按default/gallery/detail/admin分组 semaphore 快拒绝;拼图广场 TTL 过期时已有缓存先返回 stale 响应,只允许一个后台 refresh 任务重建,冷启动无缓存时才同步构建。 - 验证:OTLP 看
genarrative.http.server.request_permits.available{pool=...}、genarrative.puzzle_gallery.cache.stale_hits、refreshes_started、refreshes_failed,Nginx access log 看request_time与upstream_response_time是否同步收敛;超过容量时应明确 429,而不是长时间排队或新增 502。 - 关联:
deploy/nginx/genarrative.conf、deploy/container/nginx.conf、server-rs/crates/api-server/src/backpressure.rs、server-rs/crates/api-server/src/puzzle_gallery_cache.rs。
多玩法公开广场列表优先订阅 public view / read model
- 现象:抓大鹅、方洞挑战、视觉小说、大鱼吃小鱼等公开列表如果沿用
list_*_worksprocedure,即使只读已发布作品,也会在每个 HTTP 请求里回到 SpacetimeDB WASM 侧扫描、反序列化配置并组装列表,50RPS 以上容易变成热点。 - 原因:个人作品列表和公开广场列表复用了同一套 procedure 输入,导致公开列表为了通过 owner 校验传固定占位 owner,并把可长期同步的公开读模型当成请求期查询。
- 处理:每个公开广场新增或复用专用 public view / public read model:
match_3_d_gallery_view、square_hole_gallery_view、visual_novel_gallery_view、big_fish_gallery_view。spacetime-client建连接后订阅这些 view 和对应public_work_play_daily_statsource_type 桶,HTTP gallery 只读本地 cache。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍走原有 procedure / reducer。 - 验证:搜索
server-rs/crates/spacetime-client/src/{match3d,square_hole,visual_novel,big_fish}.rs,公开 gallery 主路径应读取connection.db().*_gallery_view(),不应调用list_*_works_with_input;执行npm run spacetime:generate、cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema。 - 关联:
server-rs/crates/spacetime-module/src/match3d.rs、server-rs/crates/spacetime-module/src/square_hole.rs、server-rs/crates/spacetime-module/src/visual_novel.rs、server-rs/crates/spacetime-module/src/big_fish/session.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure
- 现象:
/api/runtime/custom-world-gallery每次请求调用list_custom_world_gallery_entriesprocedure;入口熔断中间件每个玩法请求调用get_creation_entry_configprocedure,50RPS 以上会把 SpacetimeDB procedure 调用变成热点。 - 原因:
custom_world_gallery_entry、creation_entry_config和creation_entry_type_config已经是可订阅读模型或配置表,但 HTTP 路径仍按“请求到来再查 procedure”处理。 - 处理:
spacetime-client长连接订阅custom_world_gallery_entry、public_work_play_daily_stat的custom-world桶、creation_entry_config和creation_entry_type_config;custom-world gallery 从本地 cache 排序并聚合 7 日播放数;入口配置优先读订阅 cache,cache 缺失时用最近一次成功内存快照,再兜底调用get_creation_entry_config完成旧库兼容。旧list_custom_world_gallery_entriesprocedure 只允许作为旧库缺少 gallery 行时的一次性同步兜底。 - 验证:搜索
server-rs/crates/spacetime-client/src/custom_world.rs,gallery 主路径应是read_after_connect读取custom_world_gallery_entry();搜索server-rs/crates/spacetime-client/src/runtime.rs,get_creation_entry_config应优先读取creation_entry_config()和creation_entry_type_config()。执行cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/spacetime-client/src/custom_world.rs、server-rs/crates/spacetime-client/src/runtime.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行
- 现象:使用 VectorEngine
gpt-image-2生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回429/当前分组上游负载已饱和;同一批次里后续图片会被前面的慢请求拖住。 - 原因:复杂抽象 logo prompt 同时包含品牌解释、禁用元素、中文结构和多重隐喻时,上游排队与生成时长不稳定;并发或批量运行会放大单条慢请求的影响。
- 处理:先
--dry-run看请求体;真实生成时优先短 prompt、单一造型、单张串行或小批量。失败后不要反复重试同一长 prompt,先压缩到“一个主体 + 一个负形 + 颜色 + 禁用文字/播放键/聊天气泡”再跑。联系表中的中文标签不要通过 PowerShell 管道内联 Python 写入,容易因编码链路显示为问号,可改用英文标签或脚本文件方式。 - 验证:生成文件落在
public/branding/taonier-logo-*/,用 Pillow 检查图片尺寸和非空;执行node --check scripts/generate-taonier-logo-concepts.mjs、npm run check:encoding、git diff --check。 - 关联:
scripts/generate-taonier-logo-concepts.mjs、docs/design/TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md。
忘记密码后仍提示手机号或密码错误先查认证投影同步
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现
Bearer JWT 版本已失效,日志里的 token version 与本地快照不一致。 - 原因:重置/修改密码会更新
password_hash、password_login_enabled和token_version,如果 API 层只更新本地InMemoryAuthStore,没有调用sync_auth_store_tables_to_spacetime(),api-server重启时可能从旧的 SpacetimeDB 正式认证表恢复账号状态。 - 处理:
POST /api/auth/password/change与POST /api/auth/password/reset成功后必须同步正式认证表。2026-07-01 起,auth_store_snapshot表和旧 JSON procedure 已删除;认证工作集只通过 typed projection 同步user_account/auth_identity/refresh_session。认证创建、登录会话、刷新、退出、改密、重置密码、绑定和资料变更等写操作必须在返回客户端前成功同步 SpacetimeDB;同步失败时接口返回错误,不允许把只存在于当前进程内存的账号或会话当成成功结果。新用户注册奖励、邀请码绑定和登录埋点必须排在认证同步成功之后,避免认证没落库时先写出钱包或邀请关系。 - 验证:执行
cargo test -p module-auth password --manifest-path server-rs/Cargo.toml与cargo test -p api-server password --manifest-path server-rs/Cargo.toml;手测时重设密码后旧密码应失败,新密码应成功,重启后仍应保持。 - 关联:
server-rs/crates/api-server/src/password_management.rs、server-rs/crates/api-server/src/state.rs、docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md。
密码登录失败且短信登录提示手机号已存在先查孤儿手机号索引
- 现象:老账号用密码登录提示“手机号或密码错误”,改用短信验证码登录又提示“手机号已存在 / 已注册”,用户卡在既不能登录也不能重新创建的状态。
- 原因:历史版本或停服务时认证同步不完整,可能在 SpacetimeDB
auth_identity(provider=phone)或旧module-auth快照里留下phone_to_user_id映射,但对应user_account/users_by_username用户行已经不存在。密码登录按手机号索引找不到真实用户,短信登录尝试创建新用户时又被孤儿手机号索引挡住。 - 处理:
export_auth_store_projection_from_tables只导出正式认证表 projection;module-auth从 projection 恢复时必须丢弃指向不存在user_account的 identity、union 索引和 refresh session。运行时创建手机号用户前若发现手机号映射指向不存在的用户,应删除孤儿映射后继续创建,避免死锁态继续扩散。 - 验证:
cargo test -p module-auth projection --manifest-path server-rs/Cargo.toml、cargo test -p module-auth phone --manifest-path server-rs/Cargo.toml、cargo test -p api-server phone_login_reuses_existing_user_for_same_phone_number --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/module-auth/src/lib.rs、server-rs/crates/spacetime-module/src/auth/procedures.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
认证快照表和旧 procedure 已删除
- 现象:有些旧代码和生成 bindings 里还会残留
get_auth_store_snapshot、upsert_auth_store_snapshot、import_auth_store_snapshot、import_auth_store_snapshot_json、export_auth_store_snapshot_from_tables,或者把auth-store.json误当成认证恢复源。 - 原因:认证恢复已经彻底收口到 SpacetimeDB 正式表和
module-authtyped projection;本地文件持久化或 JSON 快照会和正式表投影打架,SpacetimeDB 不可用时还可能把旧快照回灌到用户表。 - 处理:先用
npm run spacetime:generate刷新 bindings,确认server-rs/crates/spacetime-client/src/module_bindings.rs里已没有旧 snapshot table / procedure 导出;module-auth只保留内存态和 projection view,不再写本地快照文件。 - 验证:
cargo check -p module-auth --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema、npm run check:encoding。
抓大鹅生成页只显示服务暂不可用先查 reason 和外部服务配置
- 现象:点击生成抓大鹅草稿后,页面只提示“服务暂不可用”,或者本地
npm run dev:api-server看似启动但生成接口不可用。 - 原因:配置缺失类错误通常在后端
error.details.reason中给出具体缺项,前端如果只读details.message会吞掉原因;本地只配置ALIYUN_OSS_BUCKET/ALIYUN_OSS_ENDPOINT时,旧逻辑还会在启动期构造空 AccessKey 的 OSS 客户端并失败。抓大鹅新链路仍是 2D 生图切割,不需要也不应回退 Rodin/GLB。 - 处理:前端 API 错误展示优先读取
details.reason,再读取details.message,避免底层error sending request覆盖真正可操作的配置或网络原因;api-server只有在 OSS 四件套齐全时初始化 OSS 客户端,部分缺失只记 warning 并让具体 generated 上传/换签接口返回OSS 未完成环境变量配置。抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS,并通过details.missingEnv列出缺项;真实生成需补齐VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY和完整ALIYUN_OSS_*四件套。抓大鹅 UI spritesheet 和物品 spritesheet 的提示词必须要求单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景,后端上传 OSS 前统一扣成透明 PNG,避免运行态 alpha 连通域解析失败。 - 验证:
npm run test -- src/services/apiClient.test.ts覆盖details.reason;cargo test -p api-server state --manifest-path server-rs/Cargo.toml覆盖半配置 OSS 不阻断启动;npm run dev:api-server后按实际GENARRATIVE_API_PORT请求/healthz,不要默认打3100。 - 关联:
packages/shared/src/http.ts、server-rs/crates/api-server/src/state.rs、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md、docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md。
2026-05-22 补充:抓大鹅“物品 spritesheet”不再按旧 Gemini generateContent / 5*5 sheet 路径排查;当前链路先用 gpt-image-2 无参考图生成 9:16 关卡整图,再以该关卡整图作为 multipart image 参考并发编辑生成 1K 1:1 UI spritesheet、1K 9:16 背景图和 2K 1:1 物品 spritesheet。UI 与物品 spritesheet 都要求单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕背景,上传 OSS 前通过后端透明化处理写入真实 alpha PNG。
抓大鹅发布按钮要先开发布面板,封面编辑收口到发布面板内
- 现象:抓大鹅结果页发布按钮看起来点不了,或者封面编辑仍然分散在作品信息 Tab 里,和拼图发布体验不一致。
- 原因:发布按钮被
publishReady直接禁用,导致未满足门槛时无法进入发布检查面板;封面编辑仍挂在作品信息 Tab,不能和发布检查一起收口。 - 处理:发布按钮只受忙碌态控制,点击后始终打开独立发布面板;发布面板内先展示阻断项,再承载封面图上传 / AI 重绘 / 参考图编辑,满足条件后再点击
发布到广场。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx;npm run typecheck。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/components/match3d-result/Match3DResultView.test.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
.codex 只放项目工具,不放个人 Codex 配置
- 现象:团队成员误把个人 Codex 配置、会话或密钥复制进仓库。
- 原因:仓库
.codex/与个人~/.codex/名称相似。 - 处理:仓库
.codex/只放可公开的 skills、插件资源、hooks 和配置模板;长期项目知识写入docs/project-memory/,不提交.env、config.toml、sessions/、auth.json。 - 验证:提交前检查
git diff -- .codex,确认没有密钥、会话记录或个人路径敏感信息。 - 关联:
.codex/README.md。
儿童动作 Demo 卡在摄像头不可用或挥手不推进先查 mocap 消费链路
- 现象:
/child-motion-demo打开后即使http://127.0.0.1:8876/已启动,页面仍提示“摄像头暂不可用”,或到“打个招呼”、左右手挥动、站位步骤时真实硬件动作无法检测通过,只能用鼠标拖拽或键盘调试继续。 - 原因:浏览器摄像头视频流只是舞台背景;如果热身关把
getUserMedia状态当成主动作数据源,或只在 gesture 阶段消费useMocapInput,就会错过 mocap 的身体中心、动作名和手部坐标。 - 处理:确认
src/components/child-motion-demo/ChildMotionWarmupDemo.tsx全热身流程启用useMocapInput,页面主提示展示 mocap 动作数据源状态而不是浏览器摄像头状态;确认src/services/useMocapInput.ts能解析/stream包里的general.body.center_norm、actions/action/gesture/gestures/event/name/type、hands[]、leftHand/rightHand、left_hand/right_hand、左右手标记和open_palm/grab状态。/stream是 WebSocket,普通 HTTP 访问返回 404 不能当成服务不可用。 - 验证:运行
npx vitest run src\services\useMocapInput.test.ts src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx,并在本地硬件服务启动后进入/child-motion-demo实测站位、招手、左右手挥动和跳跃阶段。 - 关联:
src/services/useMocapInput.ts、src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 左右手阶段误通过先查身体侧映射和手臂展开阈值
- 现象:热身关“挥动左手 / 挥动右手”阶段,用户只是手自然下垂、横向小幅抖动,或挥了相反侧手,也可能被判定通过。
- 原因:本地 mocap 的 handedness 当前按摄像头视角输出,不能直接当作用户身体左/右;同时左右手阶段的目标是确认现实空间安全,需要验证手臂向外打开和上下摆动角度,不能只看手部
x轨迹范围。 - 处理:热身关中用户左手应消费 camera-right,用户右手应消费 camera-left;左右手阶段只在同侧肩肘腕外展、手腕非自然下垂、连续有效帧、横向范围、上下摆动范围、肩腕角度范围和上下方向变化全部达标时完成,并记录轨迹空间包络、角度范围和最大外展距离。
- 验证:运行
npx vitest run src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx src\components\child-motion-demo\childMotionWarmupModel.test.ts,确认相反侧手、自然下垂、单纯横向轨迹不会完成,真实展开上下摆动可以完成。 - 关联:
src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、src/components/child-motion-demo/childMotionWarmupModel.ts、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 角色轮廓抽搐先查 mocap 坐标防抖和渲染分层
- 现象:
/child-motion-demo中间半透明小人在真实硬件驱动下左右轻微来回摆,移动过程中看起来忽大忽小,用户很难稳定停在目标圆环内。 - 原因:
general.body.center_norm.x原始值逐包直接写入avatarX时,硬件坐标小噪声会直接驱动位置保持判定和 CSS 动画;如果角色外层同时承担横向定位和跳跃transform,半透明 PNG 在移动时也更容易出现重采样抖动观感。 - 处理:mocap 身体中心进入角色位置前必须先 clamp,再经过小幅死区、低通阻尼和单包最大步长限制;键盘 A/D 调试输入仍保持即时。角色 DOM 外层只负责横向定位,内层 sprite 负责轮廓图和跳跃位移,避免同一层
transform同时表达多种运动。 - 验证:运行
npx vitest run src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx src\components\child-motion-demo\childMotionWarmupModel.test.ts src\services\useMocapInput.test.ts src\services\child-motion-demo\childMotionDebugInput.test.ts,并用真实硬件进入站位阶段观察小幅身体晃动不会导致角色频繁左右跳动。 - 关联:
src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、src/index.css、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
宝贝识物选篮误触发先查多套判定和残余轨迹
- 现象:
宝贝识物运行态打开礼物盒或反馈结束后,当前物品被连续送入左侧或右侧篮子,或硬件动作名偶发命中导致未做明确横移动作也触发选篮。 - 原因:选篮如果同时消费
wave_left_hand/wave_right_hand/wave动作名、连续横向轨迹和左右手固定篮子规则,或在correct/wrong反馈阶段继续累计手部状态,会把反馈期间残留移动或未知侧别手部误算成下一次选篮。 - 处理:宝贝识物当前选篮只允许“手先触碰中央物品 UI,物品绑定到该手,随后拖入左侧或右侧篮子区域”这一套路径;侧别为
unknown的手部不参与抓取或选篮;反馈阶段清空持有状态,不在非active阶段累计输入。进入关卡和每次正确反馈结束后自动弹出物品,不再用open_palm -> grab抓握序列激活礼物盒。 - 补充:当前本地 mocap 的 handedness 是摄像头视角,宝贝识物仍需换算为用户身体视角以展示左右手:
rightHand坐标代表玩家左手,leftHand坐标代表玩家右手。换算不再决定只能选择哪侧篮子;任意一只手都可以拖物品到任意篮子。键鼠调试保持鼠标左键=左手位置、右键=右手位置,也必须先触碰中央物品再拖入篮子。 - 验证:运行
npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/services/useMocapInput.test.ts,确认动作名负向测试、未知侧别负向测试、触碰前不能选篮和任意手拖入任意篮子用例通过。 - 关联:
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝爱画左右手反了先查 mocap 摄像头视角换算
- 现象:
宝贝爱画中真实硬件下左手指示器和右手画笔表现反向,用户抬右手却出现左手选色指示器,或抬左手却驱动画笔 / 橡皮。 - 原因:本地 mocap 的 handedness 当前按摄像头视角输出,不能直接当成用户身体左 / 右;宝贝爱画初版直接消费
latestCommand.leftHand/rightHand,漏做摄像头视角到用户身体视角的换算。 - 处理:宝贝爱画运行态消费 mocap 前先换算:
rightHand作为用户左手,用于颜色悬停和左手指示器;leftHand作为用户右手,用于画笔 / 橡皮光标、绘制、擦除和工具切换。键鼠调试输入不做该换算,继续保持鼠标左键为左手、右键为右手。 - 验证:运行
npm run test -- src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx src/components/edutainment-runtime/babyLoveDrawingModel.test.ts,确认 camera-left 驱动用户右手画笔、camera-right 渲染用户左手选色指示器。 - 关联:
src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx、docs/technical/BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md。
宝贝识物创作卡在准备结果页先查长耗时 image-2 请求
- 现象:
/creation/baby-object-match创作生成停在“准备结果页”,约 3 分钟后显示“生成失败 / 请求超时”;后端日志可能出现同一路由status=502 latency_ms=231291,或前端已失败但后端稍后返回 200。 - 原因:宝贝识物创作属于长耗时 image-2 链路。旧前端只等待 180 秒并对长耗时 POST 自动重试,容易在 VectorEngine 仍在生成时先 abort,再重复发起第二次生成;上游某张图超过后端
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS或返回 5xx 时会表现为 502。2026-05-14 后,新链路已从“2 张物品图 + 5 张视觉包装图”收敛为“1 张2x2素材 sheet + 1 张场景背景图”,左右手位置指示器改为运行态默认静态素材,不再每次创作生成,但仍需要按长耗时链路排查。 - 处理:
babyObjectMatchClient对/api/creation/edutainment/baby-object-match/assets使用 10 分钟超时并取消自动重试;后端并发启动2x2素材 sheet 和场景背景生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,按资源类别输出开始、完成和耗时日志。2x2sheet 固定包含物品 A、物品 B、篮子和礼物盒,服务端按格切图并转透明 PNG;ui-frame/smoke-puff/left-hand/right-hand不再作为新生成必需资源。 - 验证:运行
npm run test -- src/services/edutainment-baby-object/babyObjectMatchClient.test.ts src/services/miniGameDraftGenerationProgress.test.ts、cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml和编码检查;真实联调时查看宝贝识物 image-2 2x2 素材 sheet 生成完成、宝贝识物 image-2 场景资源生成完成和整体宝贝识物 image-2 资源生成完成耗时是否小于前端超时,若仍 502 再看VectorEngine 图片生成上游错误的upstreamStatus/raw_excerpt。 - 关联:
src/services/edutainment-baby-object/babyObjectMatchClient.ts、src/services/miniGameDraftGenerationProgress.ts、server-rs/crates/api-server/src/edutainment_baby_object.rs、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝识物篮子手柄白底先查 sheet 切图后处理
- 现象:
宝贝识物新生成的主题篮子在左右手柄、篮口镂空或边缘处仍出现白底块或白色毛边,尤其是 2x2 sheet 背景被抠透明后,封闭镂空区域可能没有被通用边缘连通抠图清理掉。 - 原因:宝贝识物为了降低 image-2 成本,把物品 A、物品 B、篮子和礼物盒放在同一张
2x2sheet。通用背景透明处理主要从单格边缘连通背景开始,封闭在篮子手柄内部的近白区域不一定与边缘连通,因此会残留;如果把强力近白清理应用到物品格,又可能误伤白色物品主体。 - 处理:后端
slice_baby_object_match_sheet只在BabyObjectMatchSheetSlot::Basket编码前执行近白、低饱和 matte 清理;物品格和礼物盒格继续只走通用背景透明处理。sheet prompt 同步要求篮子手柄和篮口镂空处不要留下白底描边或毛边。运行态左右篮子的物品图标和名称 UI 以篮子中心线对齐,避免素材放大后看起来偏移。 - 验证:运行
cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml与npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx;真实联调需要重新生成宝贝识物资源,旧草稿中已保存的 base64 篮子图不会自动被新后处理改写。 - 关联:
server-rs/crates/api-server/src/edutainment_baby_object.rs、src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、src/index.css、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝识物物品框被长条素材拉伸先查固定槽位
- 现象:用户用手机、筷子等长条关键词生成素材后,中央物品 UI 或篮子上方物品图标看起来被拉成长框,圆形 UI 失去固定比例。
- 原因:运行态如果让图片固有宽高或外层自适应内容,就会把长条透明 PNG 的主体比例传导到 UI 容器。
- 处理:中央物品 UI 和篮子物品图标都必须使用固定正方形槽位,外层尺寸由 CSS 变量控制;生成素材图片只在槽位内
object-fit: contain等比缩放,不改变外层圆形 UI 框尺寸。 - 验证:用长条物品草稿进入宝贝识物运行态,中央物品框和篮子图标框仍为正圆,长条主体在框内缩小显示。
- 关联:
src/index.css、src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
寓教于乐作品和宝贝识物模板同时消失先查入口种子
- 现象:发现页“寓教于乐”分类下已发布的宝贝识物作品突然消失,同时创作界面模板选项中也看不到或无法正常展示
宝贝识物。 - 原因:创作入口配置事实源已迁到 SpacetimeDB
creation_entry_type_config;前端用baby-object-match入口可见性同时控制创作模板展示和发现页宝贝识物公开作品合入。若默认种子或后台配置缺少baby-object-match行,两条链路会一起被判定为不可见。 - 处理:确认
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs默认种子包含id=baby-object-match、title=宝贝识物、visible=true、open=true、sort_order=90;api-server 测试降级配置也要同步包含该类型。入口图片路径需指向真实存在资源,避免卡片图片 404。 - 验证:运行
cargo test -p module-runtime default_creation_entry_types_include_baby_object_match --manifest-path server-rs/Cargo.toml、cargo test -p api-server test_creation_entry_config_response_keeps_baby_object_match_visible --manifest-path server-rs/Cargo.toml、cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml和npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts。 - 关联:
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs、server-rs/crates/api-server/src/creation_entry_config.rs、docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md。
儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置
- 现象:
/child-motion-demo已经呈现绘本草地风格,但public/child-motion-demo/picture-book-grass-stage.png、picture-book-grass-floor.png、picture-book-ground-ring.png、picture-book-character-outline.png、picture-book-ui-panel.png或picture-book-ui-button.png不存在,Network 里对应图片返回 404,或运行npm run assets:child-motion-demo -- --live返回缺少 VectorEngine 配置。 - 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine
gpt-image-2生成,脚本只读取VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY和可选VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。 - 处理:在本地私密环境补齐
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai与VECTOR_ENGINE_API_KEY,不要把 key 写入 Git;先运行npm run assets:child-motion-demo -- --dry-run核对 prompt,再运行npm run assets:child-motion-demo -- --live或npm run assets:child-motion-demo -- --live --only ui-panel等小批量命令生成资源。透明资源的品红底源图写入tmp/child-motion-demo-assets/,不要把源图或预览图放入public/child-motion-demo/作为正式资产。 - 验证:生成后确认
public/child-motion-demo/只保留页面引用的最终 PNG,重新打开/child-motion-demo可看到真实绘本草地背景、地面、圆环、角色轮廓和 UI 资源;npm run check:encoding仍通过。 - 关联:
scripts/generate-child-motion-demo-assets.mjs、src/index.css、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 绘本资源变形先查用途拆分和透明后处理
- 现象:
/child-motion-demo背景风格正确,但底部草坪被拉成厚色块、顶部 HUD 或右下状态条像方形面板被横向拉伸,或旧picture-book-ui-panel.png与新资源叠在一起。 - 原因:早期资源中
picture-book-ui-panel.png是接近方形画布,picture-book-grass-floor.png也含大量透明边界;若 CSS 用background-size: 100% 100%把同一资源强行铺成 HUD、状态条、开始面板或底部地板,就会出现变形和层叠观感。 - 处理:使用用途专属资源:
picture-book-foreground-grass-v2.png、picture-book-ground-ring-v3.png、picture-book-character-outline-v4.png、picture-book-hud-strip-v2.png、picture-book-calibration-strip-v2.png、picture-book-start-panel-v2.png、picture-book-ui-button-v2.png;CSS 按资源比例等比缩放,底部草坪只覆盖下沿,HUD / 状态条 / 开始托盘分别引用各自资源。角色指示器使用 v4 更细白色描边资源,内部透明且显示尺寸相对上一版放大 50%;若只需修透明裁切、品红边或纯描边后处理,运行npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>,不重新请求 image-2。 - 验证:用横屏截图检查没有新旧资源叠加、没有方形面板拉成长条、角色和地面指示环不被前景草坪埋住;同时运行
npm run check:encoding。 - 关联:
scripts/generate-child-motion-demo-assets.mjs、src/index.css、public/child-motion-demo/、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 猫咪挥手拆件错位先查动画父级和肩部挂点
- 现象:
/child-motion-demo打个招呼阶段的猫咪图和风格正确,但挥手时左右手臂像漂浮在身体旁边,视频里能看到肢体没有稳定接在肩膀上。 - 原因:猫咪身体和手臂如果分别做上下浮动,或手臂使用透明方形画布的默认中心/底部旋转轴,就会在摆动极值时放大肩点偏差;镜像左臂还需要把资源内部连接点换算到镜像后的坐标。
- 处理:
.child-motion-gesture-guide__wave-cat父级统一承接 bob 动画,身体层保持静态贴底且层级低于手臂;左右手臂作为同一父级下的兄弟层,只做旋转动画并显示在身体前方。身体使用去掉左右小圆点的picture-book-wave-cat-body-guide-v7.png;手臂 v7 资源当前按身体外缘摆放,圆猫爪掌面朝向玩家;左右侧距为12%,左臂使用原图层与60% 78%旋转轴,右臂使用镜像图层与40% 78%旋转轴,动画周期为0.47s,左右手臂不设置错峰延迟;不要把scaleX(...)和 rotate 放在同一个手臂 wrapper 上。 - 验证:用用户录屏关键帧或离线合成预览检查摆动两端的手臂根部仍贴住肩点;再运行儿童动作 Demo 定向组件测试、ESLint 和
npm run check:encoding。 - 关联:
src/index.css、public/child-motion-demo/picture-book-wave-cat-body-guide-v7.png、public/child-motion-demo/picture-book-wave-cat-arm-guide-v7.png、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
GPT-image-2 不再读 APIMart 图片配置
- 现象:配置了
APIMART_BASE_URL/APIMART_API_KEY后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现official_fallback/image_urls。 - 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions
gpt-5.4-mini,APIMart 不再作为当前创意 Agent 来源。 - 处理:为图片生成配置
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai、VECTOR_ENGINE_API_KEY、VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;排查请求体时确认无参考图路径为/v1/images/generations、有参考图路径为/v1/images/edits,业务 / 计费与 provider 首发模型均为gpt-image-2,仅在符合条件的 provider 失败后切到兜底模型gpt-image-2-c。 - 验证:运行
cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。 - 关联:
docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md、server-rs/crates/api-server/src/openai_image_generation.rs。
拼图参考图没有影响生成时先查 action payload 和阶段日志
- 现象:拼图上传参考图后生成出的画面明显不像参考图,或结果页重新生成没有按保存的参考图走图生图。
- 原因:首图生成只通过
compile_puzzle_draft.referenceImageSrc临时传 Data URL,不持久化到 SpacetimeDB;结果页重新生成则要把当前上传图或关卡pictureReference作为generate_puzzle_images.referenceImageSrc继续传给后端。 - 处理:浏览器 Network 里确认 action payload 带
referenceImageSrc;api-server 日志按同一session_id查看拼图参考图解析完成、拼图 VectorEngine 图片生成 HTTP 返回、拼图 VectorEngine 图片下载完成、拼图生成图片已写入 OSS 与资产索引,可定位慢在参考图读取、VectorEngine、下载或 OSS。 - 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的
pictureReference重新生成;后端单测覆盖 VectorEngine 请求体image字段。 - 关联:
src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle.rs。
拼图首图生成后要把入口参考图写回 pictureReference
- 现象:入口页上传图后,首图看着像没吃到参考图;结果页重新生成时默认只沿用关卡旧图,没有继续带入口上传图。
- 原因:首图生成请求虽然已经把
referenceImageSrc传给 VectorEngine,但如果后端只更新cover_image_src/selected_candidate_id而不回写首关pictureReference,结果页后续重绘就会丢失参考图。 - 处理:在
compile_puzzle_draft和generate_puzzle_images的成功与 SpacetimeDB 降级快照路径里,都把本次入口参考图写入首关pictureReference。 - 验证:后端单测覆盖
build_puzzle_levels_with_primary_update和apply_generated_puzzle_candidates_to_session_snapshot;结果页重新生成应在未重新上传时继续带入level.pictureReference。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、src/components/puzzle-result/PuzzleResultView.tsx。
拼图参考图不像时先看 edits multipart image
- 现象:Network payload 已带
referenceImageSrc,但 VectorEngine 生成结果仍明显不像上传图。 - 原因:参考图只在
aiRedraw = true时由后端解析并传给gpt-image-2/v1/images/edits的 multipartimagepart;若前端没传referenceImageSrc、后端解析失败或 prompt 缺少参考图强约束,生成会退化为纯文生图。 - 处理:
referenceImageSrc存在且aiRedraw = true时走 edits multipart,prompt 保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。 - 验证:后端单测应覆盖
/v1/images/edits路由、b64_json响应解码和参考图强提示;真实联调看日志里是否命中拼图 VectorEngine 图片编辑 HTTP 返回。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、src/services/puzzleReferenceImage.ts、docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md。
拼图 edits 报 error sending request 先看网络分类
- 现象:拼图有参考图时返回
拼图图片生成失败:创建拼图 VectorEngine 图片编辑任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/edits),后端没有拼图 VectorEngine 图片编辑 HTTP 返回日志。 - 原因:这是
reqwest在send()阶段失败,尚未收到 VectorEngine HTTP 响应;常见原因是服务器网络 / DNS / 防火墙 / 代理问题,或上游网关中断 multipart 连接。 - 处理:查看错误响应和
拼图 VectorEngine 图片编辑相关日志;若请求发送阶段失败,先查网络出口、DNS、防火墙、代理、参考图大小和VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS。 - 验证:
curl --http1.1 -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2" -F "prompt=test" -F "n=1" -F "size=1024x1024" -F "image=@public/match3d-background-references/pot-fused-reference.png;type=image/png"至少应返回 HTTP401,说明域名、TLS、路径和 multipart 上传可达;执行cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md。
拼图 UI 背景缺失先区分生成失败和消费链路丢字段
- 现象:拼图草稿生成完成后,素材配置页没有展示生成的 UI 背景,或结果页能看到背景但自动试玩 / 结果页“试玩”进入局内仍只显示封面模糊背景。
- 原因:
compile_puzzle_draft设计上会在首图后生成 UI 背景,且缺uiBackgroundImageSrc/uiBackgroundImageObjectKey会让自动草稿失败;若草稿已成功,通常不是“没生成”,而是前端消费链路漏了levels[].uiBackgroundImageObjectKey回退,或本地startLocalPuzzleRun(...)只把coverImageSrc带入currentLevel。 - 处理:结果页预览、运行态和本地运行态统一用
resolvePuzzleUiBackgroundSource,优先uiBackgroundImageSrc,为空时把uiBackgroundImageObjectKey规范成/generated-...路径并交给/api/assets/read-url换签;startLocalPuzzleRun与本地下一关 handoff 都要从PuzzleWorkSummary.levels[]复制uiBackgroundImageSrc/uiBackgroundImageObjectKey/backgroundMusic到currentLevel。结果页UI背景提示词输入框不得把本地兜底 prompt 直接显示成已保存提示词,避免误判为后端已生成。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/services/puzzle-runtime/puzzleLocalRuntime.test.ts src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx,以及npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial";后端用cargo test -p api-server puzzle_ui_background --manifest-path server-rs\Cargo.toml确认生成 / 序列化链路。 - 关联:
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts、src/services/puzzle-runtime/puzzleLocalRuntime.ts、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
拼图草稿生成后音乐/UI 又变空先查结果页回包合并
- 现象:拼图草稿生成完成后,音乐面板曲名有值但音频槽仍显示“暂无音乐”,UI 仍展示默认预览;试玩进入局内也没有生成音乐或 UI 背景。
- 原因:结果页若已有本地
generationStatus = generating编辑态,后端生成完成回包会走mergeDraftEditStateWithIncomingState(...)合并。该合并必须把生成候选图、正式图、uiBackground*和backgroundMusic作为同一批生成资产处理;漏掉backgroundMusic时,随后自动保存会把空音乐写回levels_json。 - 处理:
PuzzleResultView合并生成完成回包时同步保留backgroundMusic,并用回归测试覆盖 UI 预览、音乐试听和试玩 payload 都读取最新levels[]资产。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx,以及自动试玩入口测试npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial"。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md。
自动草稿成功但缺音乐或 UI 先查后端吞错
- 现象:拼图或抓大鹅生成页提示完成,但草稿页仍显示“暂无音乐”,拼图 UI 仍是默认预览,试玩局内也没有生成音乐或 UI 背景。
- 原因:自动草稿阶段如果把 VectorEngine / Suno / OSS / 资产绑定错误记录为 warning 后继续返回成功,前端只能拿到缺关键资产的成功 draft,随后保存和试玩都会消费这份空资产状态。
- 处理:自动草稿必须把必需生成资产当作后端完成条件:拼图首关需同时具备
levels[0].backgroundMusic.audioSrc和levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey;抓大鹅需在generatedItemAssets[]中具备非空backgroundMusic.audioSrc。缺失或上游失败时返回错误并停留在生成页,结果页手动重新生成只作为已有草稿补救入口。 - 验证:
cargo test -p api-server puzzle_initial_draft_assets_must_include_music_and_ui_background match3d_background_music_ready_requires_audio_src match3d_background_music_title_is_required_for_auto_draft --manifest-path server-rs\Cargo.toml,并重启npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、server-rs/crates/api-server/src/match3d.rs、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图草稿生成 180 秒后 502/504 先查 VectorEngine 超时与前端重试
- 现象:点击“生成拼图游戏草稿”后,
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions等待约 180 秒返回502 Bad Gateway或504 Gateway Timeout;钱包流水里同一 session 可能出现连续两组puzzle_initial_image扣费后退款。 - 原因:首图生成走 VectorEngine
gpt-image-2,默认VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000;若上游在该窗口内未返回,后端退款并返回超时错误。旧前端 action 写请求会对 502/503/504 自动重试一次,导致同一次点击重复触发生图与扣退费。 - 处理:拼图/创作 Agent 的
executeAction默认不做前端自动重试;后端将 VectorEngine / 图片请求超时映射为504 Gateway Timeout,error.details.provider=vector-engine且timeout=true。真实排障按日志同一session_id查拼图 VectorEngine 图片生成 HTTP 返回是否缺失,以及钱包流水扣费到退款的时间差是否接近VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS。 - 验证:运行
npm run test -- src/services/creation-agent/creationAgentClientFactory.test.ts src/services/apiClient.test.ts、cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml,真实联调重启npm run dev:api-server后检查/healthz。 - 关联:
src/services/creation-agent/creationAgentClientFactory.ts、server-rs/crates/api-server/src/puzzle.rs、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md。
开局 CG 故事板生图失败先查 VectorEngine 请求预算和旧进程
- 现象:RPG 结果页点击开局 CG 后,
POST /api/runtime/custom-world/opening-cg在较长等待后返回“开局 CG 故事板生成失败:创建图片生成任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/generations)”。 - 原因:该故事板会把角色图和首幕背景图作为参考图一起传给 VectorEngine
gpt-image-2-all,请求体和上游生成耗时都比普通单图更大;若运行中的api-server仍沿用旧VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,或者参考图过大,会在请求发送/等待阶段被 reqwest 截断。日志里timeout=false connect=false request=true body=false source=client error (SendRequest)表示还没拿到上游 HTTP 响应,通常优先怀疑大 JSON 请求体、上游网关中断或 HTTP 协议兼容,而不是业务响应解析失败。直接请求 VectorEngine 若无效 token 可快速返回 401,不能据此判断真实生图不会超时。 - 处理:开局 CG 参考图入参先压到单边 768 的 JPEG;
/v1/images/generations保持 reqwest 默认 HTTP 协商,只有 multipart/v1/images/edits单独强制 HTTP/1.1。后端图片 helper 将timeout/connect/body/source/source_chain/source_chain_depth/endpoint分类写入日志和error.details,失败审计通过metadata_json.errorSource/requestId保留底层错误链和请求标识。修改.env.secrets.local后必须重启api-server,npm run dev终端用rs api-server,否则旧进程仍按旧超时运行。 - 验证:分别运行
cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml和cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml;真实联调重启后再触发开局 CG,若仍失败看返回的details.errorSource/source/timeout/connect/body/endpoint、tracking_event.metadata_json.errorSource/requestId和logs/api-server/同一 request_id。 - 关联:
server-rs/crates/api-server/src/custom_world_ai.rs、server-rs/crates/api-server/src/custom_world_ai/opening_cg.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
开局 CG 成功后又变空白要保留 profile.openingCg
- 现象:RPG 结果页里的开局 CG 成功显示一瞬后,窗口又退回空白占位。
- 原因:
openingCg只存在于结果页 profile 槽位,如果父层在onProfileChange后重新同步了 profile,却经过normalizeCustomWorldProfileRecord或作品库写回时丢掉openingCg,预览就会从视频 / 故事板回退为空白。 - 处理:
src/data/customWorldLibrary.ts的 profile 归一化必须透传openingCg;结果页和父层后续同步都应把它当作受控资产槽位,而不是临时 UI 状态。 - 验证:
npm run test -- src/data/customWorldLibrary.test.ts src/components/CustomWorldResultView.test.tsx,确认生成后即使父层做一次归一化回写,开局 CG 仍继续显示。 - 关联:
src/data/customWorldLibrary.ts、src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx、src/components/CustomWorldEntityCatalog.tsx。
RPG 发布报 legacy_result_profile_json 非法先查 null 兼容
- 现象:RPG 结果页发布动作返回
UPSTREAM_ERROR,SpacetimeDB details 里是custom_world.compile.legacy_result_profile_json 不是合法 JSON object。 - 原因:
publish_world前端契约只要求{ action: 'publish_world' };ExecuteCustomWorldAgentActionRequest.legacy_result_profile是可选字段,经 HTTP / serde / SpacetimeDB payload 传递时可能显式成为 JSONnull。旧的编译器只接受 object 或缺省,把Some("null")当成非法 legacy JSON。 - 处理:
module-custom-world的 optional JSON object 解析要把null视为未提供,仍拒绝数组、字符串、数字和坏 JSON;正式发布继续以 sessiondraft_profile_json为草稿真相。 - 验证:
cargo test -p module-custom-world published_profile_compile --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/module-custom-world/src/application.rs、server-rs/crates/spacetime-module/src/custom_world.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时
- 现象:用 Node
fetch直接请求POST /v1/images/generations,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛AbortError、TypeError: fetch failed或UND_ERR_HEADERS_TIMEOUT;同一 prompt 改用原生https.request可以在较短时间内成功返回图片。 - 原因:Node/Undici 的默认 headers timeout 可能早于业务脚本期望的长生图等待窗口触发,表现上容易被误判成 VectorEngine 上游本身超时。
- 处理:长期脚本优先复用后端 reqwest 或项目已有生成脚本;临时本地工具若必须用 Node,可改用原生
http/https.request并显式设置 socket timeout,或为 Undici 单独配置 headers timeout。仍需隐藏VECTOR_ENGINE_API_KEY,只报告配置是否存在。 - 验证:同一
gpt-image-2请求体、同一环境变量下,原生 HTTP 请求能返回url/b64_json并落盘;失败时错误里能区分请求发送、首部等待、下载和解码阶段。 - 关联:
.codex/skills/gpt-image-2-apimart/SKILL.md、server-rs/crates/api-server/src/openai_image_generation.rs。
旧后端路线文档造成判断漂移
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
- 原因:项目历史文档较多,部分旧方案仍保留作迁移参考。
- 处理:涉及服务端、数据真相、SpacetimeDB、运行时状态时,先看
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md,再看当前代码和具体技术方案。 - 验证:代码改动应落在
server-rs + Axum + SpacetimeDB主线;旧路线只作为迁移参考,不作为兼容目标。 - 关联:
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、AGENTS.md。
SpacetimeDB 表结构变更不能按 PostgreSQL 迁移直觉处理
- 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。
- 原因:SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。
- 处理:变更前阅读
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md;已有表新增字段必须放在 Rust 表结构体最后并设置明确默认值;需要修改字段名时,先询问用户并确认迁移计划;涉及表变化时同步migration.rs、当前表目录和 bindings;必要时走 JSON 导入导出与分片导入迁移流程。 - 验证:发布前运行
npm run check:spacetime-schema,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。 - 关联:
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
SpacetimeDB 持久化 enum 新 variant 只能末尾追加
- 现象:生产发布时 schema 迁移失败,或旧数据中的 enum 判别序号被新代码解释成其它业务枚举值。
- 原因:SpacetimeDB schema 会保存 enum variant 顺序;在已有持久化 enum 中间插入新 variant,会让后续 variant 的判别序号整体移动。即使 Rust 代码能编译,发布到已有数据库也可能炸。
- 处理:给已发布并持久化的 enum 增加 variant 时,只能追加到 enum 末尾;同步运行
npm run spacetime:generate刷新 bindings,不能手工把 generated bindings 改成另一套顺序。需要调整既有 variant 顺序、删除或重命名时,必须先确认数据迁移方案。 - 验证:
npm run check:spacetime-schema应通过;对照本次修改前后的 enum,所有旧 variant 顺序必须完全不变,新 variant 只出现在末尾。 - 关联:
server-rs/crates/module-runtime/src/domain.rs、server-rs/crates/spacetime-client/src/module_bindings/、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
- 现象:发布
spacetime-module时报wasm-bindgen detected,提示wasm-bindgen is only for webassembly modules that target the web platform。 - 原因:SpacetimeDB module 的 wasm32 构建树被间接带入原生/网页依赖;已验证链路是
reqwest -> platform-oss -> shared-contracts -> module-runtime -> spacetime-module,由共享契约默认启用资产 OSS 契约触发。 - 处理:让
shared-contracts的 OSS 资产契约走oss-contractsfeature,workspace 根依赖保持default-features = false;api-server这类原生后端需要资产 DTO 时在自身Cargo.toml显式启用features = ["oss-contracts"]。 - 验证:执行
cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown应显示 nothing to print;再执行cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown。 - 关联:
server-rs/crates/shared-contracts/Cargo.toml、server-rs/crates/api-server/Cargo.toml、docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md。
本地 SpacetimeDB replica identity 不匹配
- 现象:本地 standalone 启动时报
mismatched database identity。 - 原因:本地 SpacetimeDB 数据目录中的 replica 数据残留与当前数据库身份不一致。
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
- 关联:
docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md。
本地 SpacetimeDB publish 403 优先查 CLI 身份和目标库
- 现象:
spacetime publish在Pre-publish check阶段返回403 Forbidden,提示当前 identity 无权对目标 database identity 执行update database。 - 原因:当前 CLI 登录态不是目标数据库的创建者或授权身份,或
.env.local/ publish 命令指向了另一个数据库或 SpacetimeDB 服务。 - 处理:除 CI/CD 脚本内部受控用法外,不再使用
spacetime --root-dir排障或发布。先执行spacetime login show、spacetime server list,再用spacetime list --server http://127.0.0.1:3101或实际--server-url确认当前身份是否能看到目标库;本地开发发布优先使用npm run dev:spacetime或从server-rs目录执行显式--server的spacetime publish。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。 - 验证:
spacetime list --server http://127.0.0.1:3101能看到目标库;重新发布不再使用无权限 identity。 - 关联:
scripts/dev.mjs、docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md。
npm run dev 本地 SpacetimeDB 401 / 403 可重置默认 local 身份
- 现象:
npm run dev启动本地开发栈时,SpacetimeDB 在登录、发布或预检查阶段返回401/403,清理后仍像在使用旧 token 或旧本地库。 - 原因:本机
spacetimeCLI 保存的旧 token、默认 server、正在运行的 standalone 进程或默认 local 数据库与当前发布身份不一致。 - 处理:确认只是本地测试库且数据可丢弃后,先查看并停止本地
spacetimedb-standalone,执行spacetime logout,确认并设置spacetime server set-default local,停 server 后用spacetime server clear -y清空默认本地库,再spacetime start,另开终端执行spacetime login --server-issued-login local,最后用spacetime publish --server local A或项目脚本重新发布。 - 验证:
spacetime server list默认目标为 local;重新登录后发布不再返回401/403;npm run dev可以完成 SpacetimeDB publish 并继续启动api-server。 - 关联:
docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md、scripts/dev.mjs。
本地 SpacetimeDB 联调可按阶段跳过宿主或发布
- 现象:本地
npm run dev因3101已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查spacetime-module语法而被完整联调链路拖慢。 - 原因:
npm run dev默认同时启动 SpacetimeDB standalone、发布server-rs/crates/spacetime-module、启动 Rustapi-server、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。 - 处理:
npm run dev启动时解析实际 SpacetimeDB、api-server、主站 Vite 和后台 Vite 端口,并将同一组运行时地址传给 publish、后端环境变量和前端代理。是否复用既有宿主由启动参数和健康探测决定;修改spacetime-module时重新 publish,未修改时可使用--skip-publish。 - 验证:
--skip-spacetime后脚本复用现有http://127.0.0.1:3101;日志中的[dev] spacetime:不应漂移到没有服务的3102;GET /api/creation-entry/config不应返回连接空端口导致的502。3101或8082被其他进程占用时,脚本使用最近可用端口;--skip-publish后不再进入 publish 阶段;cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml能完成 Rust 语法和类型检查。端口漂移时控制台会打印[dev:ports] ... 不可用,改用 ...,后续[dev] web/admin web/api-server/spacetime地址应与实际端口一致。spacetime-module变更后只应看到重新发布日志,不应看到 standalone 重启日志。 - 关联:
docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md、scripts/dev.mjs。
npm run dev -- --watch 前端无限重启先查外层 watcher
- 现象:开启
npm run dev -- --watch后,后台 Vite 或主站 Vite 反复退出重启,即使没有手动修改源码。 - 原因:Vite 本身会监听源码并写入
node_modules/.vite等缓存;外层调度器如果再递归监听前端目录并重启 dev server,就可能把 Vite 自己的缓存写入当成源码变化,形成循环重启。 - 处理:外层 watcher 只负责后端侧:
spacetime-module改动后重新 publish,api-server改动后重启 Rust 进程。主站 Vite 和后台 Vite 的源码变化交给 Vite HMR;需要进程级重启时在npm run dev终端手动输入rs web或rs admin-web。 - 验证:
npm run dev -- --watch下修改apps/admin-web/src/**应由 Vite HMR 处理,不应出现连续[dev] 重启 admin-web;scripts/dev.test.ts覆盖 web/admin-web 不注册外层 watch。 - 关联:
scripts/dev.mjs、docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md。
根目录 nohup.out 持续写入会触发主站 Vite 刷新循环
- 现象:在仓库根目录用
nohup npm run dev ... &启动完整 dev 栈后,即使没有修改前端源码,主站页面也会反复整页刷新;nohup.out同时持续增长。 - 原因:未显式重定向 stdout / stderr 时,
nohup.out会收集 SpacetimeDB、api-server 和两套 Vite 的整套 dev 栈输出。主站 Vite 的 root 是仓库根目录,若 watcher 未忽略这个持续写入的文件,每次追加日志都会被当成文件变化;后台 Vite root 是apps/admin-web,仓库根日志不在其监听根内。 - 处理:主站
vite.config.ts的server.watch.ignored保持忽略**/nohup.out,Git 同时忽略nohup.out。修改配置后重启主站 Vite。若显式重定向到其它仓库内日志文件,该文件不会自动受保护,应写到 Vite root 之外或补充精确忽略规则。 - 验证:在仓库根目录追加
nohup.out时主站不再刷新,真实源码修改仍正常触发 HMR;git check-ignore nohup.out能命中忽略规则,git status不出现该日志。 - 关联:
vite.config.ts、.gitignore、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地 SpacetimeDB publish 401 可清本地库重发
- 现象:本地
spacetime publish显示401无权限,或重新发布仍像是在更新旧库。 - 原因:本地开发数据目录中保留的数据库、控制库身份或发布身份与当前目标不一致。
- 处理:确认本地开发数据可以丢弃后,停止本地 SpacetimeDB,备份或删除
server-rs/.spacetimedb/local/data,再重新运行npm run dev或本地 publish;不要用--root-dir手工清库。 - 验证:重新发布日志应显示创建新的数据库,而不是更新旧数据库;若仍显示更新或继续
401,继续检查数据目录、库名和 CLI 身份。 - 关联:
docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md、docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md。
SpacetimeDB 模块 publish 报 wasm-bindgen detected
- 现象:
spacetime publish已经完成 Rust 编译,但随后报wasm-bindgen detected,提示依赖树里有面向 Web 平台的 wasm-bindgen。 - 原因:SpacetimeDB 模块是数据库内 WASM,不允许拉入 Web/HTTP client 链路;常见误因是
spacetime-module -> module-* -> shared-contracts -> platform-* -> reqwest -> wasm-bindgen这类反向依赖。 - 处理:执行
cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown找到链路;把平台实现类型从shared-contracts或module-*中移除,只保留公开 DTO,平台响应到 DTO 的转换放回api-server等 adapter 层。 - 验证:上述
cargo tree输出warning: nothing to print;cargo check -p shared-contracts、cargo check -p api-server通过;重新spacetime publish ... --module-path server-rs/crates/spacetime-module不再报 wasm-bindgen。 - 关联:
docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md、server-rs/crates/shared-contracts/src/assets.rs、server-rs/crates/api-server/src/assets.rs。
Vite SPA fallback 吞掉 API 请求
- 现象:本地请求
/api/profile/*等接口时返回 HTML,被前端当 JSON 解析报错。 - 原因:Vite 代理缺少对应
/api/*前缀,API 请求落到 SPA fallback。 - 处理:补齐 Vite 代理,让 API 请求转发到 Rust
api-server。 - 验证:请求返回 JSON,相关页面不再出现 HTML parse 错误。
- 关联:
docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md。
npm run build 因 Vite warning 被 build-gate 判失败
- 现象:主站或后台 Vite 已经输出
built in ...,但根命令最后仍失败并打印Build gate failed because warnings were emitted。 - 原因:
scripts/build-gate.mjs会收集 stdout / stderr 中的 warning 行并作为硬失败;常见触发是产物 chunk 超过vite.config.ts或apps/admin-web/vite.config.ts的chunkSizeWarningLimit。 - 处理:先看 warning 原文确认来源。若是合理的入口级 chunk 体积增长,调整对应 Vite 配置阈值或做真实拆包;不要把这类失败按 Rust / SpacetimeDB 编译错误排查。
- 验证:重新执行
npm run build,主站与后台均构建完成且没有 build-gate warning 汇总。 - 关联:
scripts/build-gate.mjs、vite.config.ts、apps/admin-web/vite.config.ts。
反馈页清空 file input 前必须先拷贝 FileList
- 现象:点击上传凭证会打开文件选择框,但选择图片后页面没有展示预览,提交时也没有携带图片凭证。
- 原因:浏览器传入的
FileList可能跟<input type="file">保持 live 绑定;如果先执行input.value = '',再从参数里的FileList读取文件,列表可能已经为空。 - 处理:在清空 file input 前先执行
const selectedFiles = files ? Array.from(files) : [],后续图片类型、大小、Data URL 读取和预览都基于这个普通数组。 - 验证:
PlatformFeedbackView.test.tsx用 mockFileReader断言选择图片后出现反馈凭证预览,且提交 payload 带evidenceItems[].dataUrl。 - 关联:
src/components/platform-entry/PlatformFeedbackView.tsx、docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md。
拼图 VectorEngine 图片生成密钥不能复用 DashScope / ARK key
- 现象:拼图新手引导或拼图创作点击生成后返回
VectorEngine 图片生成密钥未配置。 - 原因:拼图
gpt-image-2/ 历史nanobanana2图片生成已统一走 VectorEngine;后端只读取VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY、VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,不会用DASHSCOPE_API_KEY、LLM_API_KEY、ARK_API_KEY或APIMART_API_KEY兜底。 - 处理:在本机私密配置
.env.secrets.local或进程环境中配置真实VECTOR_ENGINE_API_KEY,不要提交到 Git;填入后必须重启api-server/npm run dev,运行中的进程不会自动加载新 env。 - 验证:不打印密钥内容,只检查
VECTOR_ENGINE_API_KEY非空;重启后触发拼图生成不再返回本地配置缺失的 503。 - 关联:
docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md、.codex/skills/gpt-image-2-apimart/SKILL.md。
npm run dev:api-server 读取 env 的顺序必须让 .env.secrets.local 最后覆盖
- 现象:
POST /api/assets/hyper3d/text-to-model在本地返回 503,详情里提示HYPER3D_API_KEY 未配置,但开发者明明已经在本地私密文件里写了 key。 - 原因:
scripts/dev-utils.mjs之前按.env.secrets.local → .env.local → .env合并,结果仓库里的.env空示例值会把前面已经设置好的私密 key 覆盖掉。 - 处理:
npm run dev:api-server/npm run dev:spacetime/npm run dev统一按“外层 shell 变量优先,其后.env、.env.local、.env.secrets.local逐层覆盖”的顺序加载;真实密钥优先放.env.secrets.local。本地认证开关例外:SMS_AUTH_ENABLED、SMS_AUTH_PROVIDER等以本地 env 文件为准,避免父进程继承的旧开关值长期压过.env.local。 - 验证:本地加入临时测试后,
HYPER3D_API_KEY应能被.env.secrets.local覆盖,真实密钥 shell 变量仍然最高优先级;mergeApiServerEnv(..., { SMS_AUTH_ENABLED: "false" })在.env.local写SMS_AUTH_ENABLED=true时应返回 true。 - 关联:
scripts/dev-utils.mjs、server-rs/crates/api-server/src/hyper3d_generation.rs、docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md。
OSS 密钥键名不要把字母 O 写成数字 0
- 现象:
.env.secrets.local看起来已经配置 OSS AccessKey Secret,但拼图或抓大鹅生成仍返回OSS 未完成环境变量配置。 - 原因:后端只读取
ALIYUN_OSS_ACCESS_KEY_SECRET。如果写成ALIYUN_0SS_ACCESS_KEY_SECRET,中间是数字0,配置合并检查会显示正确键缺失,api-server不会初始化 OSS 客户端。另一个常见原因是外层 shell / IDE 预置了空的ALIYUN_OSS_*,旧启动脚本会把空值当作最高优先级,导致.env.local或.env.secrets.local的真实值被跳过。 - 处理:只改键名为
ALIYUN_OSS_ACCESS_KEY_SECRET,保留原值;不要在日志、文档或对话里输出密钥内容。本地启动脚本应只保护非空外层环境变量,空字符串或全空白值不得遮蔽本地 env 文件。 - 验证:运行
npm run check:api-server-env,确认VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY、ALIYUN_OSS_BUCKET、ALIYUN_OSS_ENDPOINT、ALIYUN_OSS_ACCESS_KEY_ID、ALIYUN_OSS_ACCESS_KEY_SECRET都是present,再重启npm run dev:api-server或npm run dev。
拼图图片生成 98% 后报 OSS V4 签名时间格式化失败
- 现象:拼图创作表单生成进度卡在 98%,
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions返回502 Bad Gateway,前端提示拼图图片生成失败:OSS V4 签名时间格式化失败。 - 原因:
platform-oss曾用OffsetDateTime::time().to_string()拼接x-oss-date,UTC 小时、分钟或秒为个位数时可能缺少前导零,导致 V4 签名时间不是固定YYYYMMDDTHHMMSSZ。 - 处理:OSS V4 签名日期统一显式补零格式化;签名 scope 用
YYYYMMDD,完整签名时间用YYYYMMDDTHHMMSSZ,不要再依赖time().to_string()。 - 验证:运行
cargo test -p platform-oss和cargo check -p api-server;重启npm run dev:api-server后检查/healthz,再重新触发拼图生成。 - 关联:
server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/api-server/src/assets.rs、docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md。
拼图生成完成后图片只显示破图或 alt 文案
- 现象:拼图结果页生成完成后,“画面图”区域出现破图图标和作品名,图片无法正常预览;但打开历史拼图素材时同一张图可能可以正常预览。
- 原因:拼图正式图保存为
/generated-puzzle-assets/*兼容标识,旧/generated-*直读代理已删除;如果前端没有通过ResolvedAssetImage//api/assets/read-url换签,或收到无前导斜杠的generated-puzzle-assets/*object key 后未识别为 generated 私有资源,浏览器会直接请求裸路径并失败。生成完成后的结果图还会传入refreshKey,它只能作为 signed URL 缓存版本号,不能给 OSS V4 签名 URL 追加_v;OSS 会把 query 纳入签名,额外参数会让签名失效。 - 处理:拼图结果页、发布预览、运行态和历史素材预览都走
ResolvedAssetImage或useResolvedAssetReadUrl;generated 私有资源识别必须同时覆盖/generated-*、generated-*和https://*.oss-*.aliyuncs.com/generated-*;refreshKey变化时重新换签,同一路径同一refreshKey且签名未临近过期时复用已返回的 OSS 签名 URL;禁止恢复/generated-puzzle-assets直读代理。 - 验证:运行
npm run test -- src\services\assetReadUrlService.test.ts src\hooks\useResolvedAssetReadUrl.test.tsx src\components\puzzle-result\PuzzleResultView.test.tsx,再触发一次真实生成确认 Network 中先请求/api/assets/read-url,图片src为未追加_v的签名 URL。 - 关联:
src/services/assetReadUrlService.ts、src/components/ResolvedAssetImage.tsx、docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md。
拼图图片生成失败后不要停在 ImageRefining
- 现象:拼图图片生成失败后,会话仍停留在
PuzzleAgentStage::ImageRefining,用户从作品架或生成页恢复时容易被当成生成中/精修中状态,重试入口和失败承接不清晰。 - 原因:
mark_puzzle_draft_generation_failed_tx只把PuzzleResultDraft.generation_status标成failed,但 session stage 仍沿用旧的row.stage;如果失败前已进入ImageRefining,失败回写不会把会话带回结果草稿态。 - 处理:失败回写后按失败草稿重新解析 session stage:已发布保持
Published,仍满足发布门禁则为ReadyToPublish,否则回到DraftReady;前端生成页文案用“拼图图片生成进度 / 重新生成图片”,避免把失败态误导成还在生成整份草稿。 - 验证:运行
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml、npm run check:encoding,以及拼图生成页恢复相关RpgEntryFlowShell.agent.interaction.test.tsx定向用例。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
本地短信登录页签突然消失
- 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但
LoginScreen中手机号验证码表单仍存在。 - 原因:历史实现曾根据
GET /api/auth/login-options返回的availableLoginMethods渲染页签;接口返回空、失败或只返回["password"]时,AuthGate会降级成只显示密码。- 本地启动脚本没有让
.env.local覆盖.env,SMS_AUTH_ENABLED=true不生效,后端只返回["password"]。 - Rust API 直连已返回
["phone","password"],但 Vite 代理目标指向未监听端口,导致 3000 域名下的login-options返回500,AuthGate降级成["password"]。 - 3000 端口被旧
dev:web占用后,新的完整栈 Vite 自动漂移到 3001/3002;浏览器仍打开旧 3000 页面,旧页面继续代理到已经下线的端口。 - 生成页 UI 改动看起来“完全没变化”时,也要先确认当前浏览器打开的 Vite 进程正在返回最新源码;例如直接请求
http://127.0.0.1:3000/src/components/CustomWorldGenerationView.tsx检查是否包含本次新增类名或关键字。 - 单独
npm run dev:web启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
- 本地启动脚本没有让
- 处理:当前口径是登录弹窗永远展示
短信登录与密码登录两个核心入口;login-options只补充微信等环境相关入口,不能隐藏短信或密码页签。如果“获取验证码”点击后失败,再按短信 provider / API 代理问题排查:优先用npm run dev:api-server、npm run dev:spacetime或npm run dev启动,确认.env.local覆盖.env、RUST_SERVER_TARGET没有指向旧端口,并分别请求 3000 域名和 Rust API 目标。 - 验证:即使
/api/auth/login-options返回空、失败或只返回["password"],登录弹窗也应同时显示短信登录、密码登录、验证码输入和“获取验证码”按钮;短信发送真实可用性再通过POST /api/auth/phone/send-code验证。 - 关联:
src/components/auth/AuthGate.tsx、src/components/auth/LoginScreen.tsx、src/components/auth/AuthGate.test.tsx、scripts/dev-utils.mjs、scripts/dev.mjs。
浏览器自动填充手机号带 +86
- 现象:登录弹窗的手机号被浏览器回填为
+86 1xxxxxxxxxx,点击获取验证码或登录后返回“手机号格式不正确”。 - 原因:
autocomplete="tel"允许浏览器回填含国家码的完整电话号码,inputMode="numeric"只提示软键盘布局,不会过滤自动填充;如果把完整号码和纯号码混在一个phone字段中,微信purePhoneNumber又与countryCode分开传递,后端容易在国家码丢失后把境外号码误判为+86。 - 处理:手机号字段保留
autocomplete="tel";authService在请求前把+86 1xxxxxxxxxx、86 1xxxxxxxxxx拆为countryCode=86 + purePhoneNumber=1xxxxxxxxxx。普通认证请求缺少countryCode时默认86,但微信授权必须使用 provider 真实返回的countryCode + purePhoneNumber,不能默认国家码。module-auth先校验国家码,再用原纯手机号规则校验purePhoneNumber并生成 E.164;数据库仍只保存 E.164。 - 验证:
cargo test -p module-auth --manifest-path server-rs/Cargo.toml、定向api-server认证测试和npm run test -- src/services/authService.test.ts src/components/auth/AuthGate.test.tsx,覆盖省略 / 显式86、境外国家码、浏览器+86自动填充以及微信 provider 国家码路径。 - 关联:
server-rs/crates/module-auth/src/domain.rs、server-rs/crates/module-auth/src/errors.rs、server-rs/crates/api-server/src/phone_auth.rs、server-rs/crates/api-server/src/wechat/auth.rs、src/services/authService.ts、src/components/auth/LoginScreen.tsx。
本地短信收不到验证码先查 provider
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
- 原因:本地
.env.local里如果是SMS_AUTH_PROVIDER="mock",后端不会发真实短信,只会返回固定 mock 验证码;真实阿里云链路已经改为普通短信SendSms,验证码由当前api-server进程本地生成、哈希存储和校验,旧SendSmsVerifyCode/CheckSmsVerifyCode托管验证码参数不再参与真实校验。若接口直接返回“手机号登录暂未启用”,说明当前运行中的api-server进程内sms_auth_enabled=false:常见原因是修改.env.local后没有重启后端,或外层 shell 已经设置了非空SMS_AUTH_ENABLED导致 dotenv 不覆盖。历史上 cmd 里set SMS_AUTH_ENABLED="true"会把引号也传进进程,Rust bool 解析失败后保持默认 false。 - 处理:真实短信联调时把
.env.local的SMS_AUTH_ENABLED=true、SMS_AUTH_PROVIDER=aliyun显式打开,并确认ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com、ALIYUN_SMS_SIGN_NAME=北京亓盒网络科技、ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486、ALIYUN_SMS_TEMPLATE_PARAM_KEY=code后重启api-server;如果只想验证 UI 和账号链路,则保留mock并使用SMS_AUTH_MOCK_VERIFY_CODE。Shell 临时覆盖时 PowerShell 用$env:SMS_AUTH_ENABLED="true",cmd 用set SMS_AUTH_ENABLED=true,不要把引号作为值的一部分。api-server重启会清掉未校验的本地验证码。 - 验证:分别请求浏览器域名和 Rust API 直连的
/api/auth/login-options,都应返回["phone","password"];api-server日志里provider=aliyun才说明真实短信链路已生效。需要直接确认平台层真实调用阿里云时,配置ALIYUN_SMS_ACCESS_KEY_ID、ALIYUN_SMS_ACCESS_KEY_SECRET和ALIYUN_SMS_REAL_TEST_PHONE_NUMBER后手动执行cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms_real_provider_sends_verify_code -- --ignored --nocapture。 - 关联:
server-rs/crates/api-server/src/config.rs、scripts/dev-utils.mjs、docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md、docs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md。
手机验证码登录 500 先查短信 provider 语义
- 现象:登录弹窗手机号验证码登录失败,浏览器看到
POST /api/auth/phone/login 500,后端日志里同时出现阿里云短信UNKNOWN、biz.FREQUENCY或check frequency failed。 - 原因:真实短信 provider 的配置错误或上游失败曾被
module-auth折叠成PhoneAuthError::Store,HTTP 层只能按内部错误返回500,掩盖了 provider 失败。当前验证码校验已经改成本地哈希校验,登录阶段的验证码错误不会再调用阿里云校验接口;若登录前的发送阶段失败,应优先看SendSms返回的Code/Message。 - 处理:保留 provider 错误语义,配置错误映射
503 Service Unavailable,上游短信失败映射502 Bad Gateway;本地只验证 UI/账号链路时可用 shell 临时覆盖SMS_AUTH_PROVIDER=mock后启动npm run dev:api-server。 - 验证:
cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml,真实 provider 频控时接口不再返回500。 - 关联:
server-rs/crates/module-auth/src/errors.rs、server-rs/crates/api-server/src/phone_auth.rs、docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md。
本地短信 smoke 先确认 SMS provider
- 现象:浏览器里短信验证码发送成功,但提交
123456仍然报验证码错误,或者短信登录后又回到未登录态。 - 原因:当前运行中的
api-server如果读取到.env.local里的SMS_AUTH_PROVIDER=aliyun,就会走真实短信 provider 口径;这时 mock 验证码123456不会被接受。之前本地调试时常见的误判是把.env.local改成 mock 了,但没有重启npm run dev,或者旧的scripts/dev.mjs进程还在沿用旧环境。 - 处理:本地只做 UI / 账号链路 smoke 时,把
.env.local显式设为SMS_AUTH_PROVIDER=mock且配置SMS_AUTH_MOCK_VERIFY_CODE=123456,然后重启npm run dev或npm run dev:api-server。要做真实短信联调时,再切回SMS_AUTH_PROVIDER=aliyun并重启。 - 验证:
POST /api/auth/phone/send-code应返回providerRequestId=mock-request-id;POST /api/auth/phone/login用123456应返回200且user.loginMethod=phone。浏览器侧短信登录成功后,会先进入邀请码弹窗或我的页面,不应再提示“验证码错误”。 - 关联:
scripts/dev-utils.mjs、scripts/dev-utils.test.ts、scripts/dev.mjs、server-rs/crates/api-server/src/config.rs。
手机验证码登录成功后又瞬间回到未登录
- 现象:手机号验证码登录先成功,随后 UI 又闪回“未登录”,登录弹窗可能重新出现。
- 原因:
AuthGate首次 hydrate 会异步轮换 refresh cookie 并请求/api/auth/me。如果用户在 hydrate 完成前已经登录,晚到的旧 hydrate 仍可能把刚写入的user覆盖成null。 - 处理:给
AuthGate的 hydrate 增加版本号保护;登录成功、退出登录和全局 auth 事件都会推进版本号,旧 hydrate 结果到达后直接丢弃。 - 验证:
npm run test -- src/components/auth/AuthGate.test.tsx,新增用例应覆盖“旧 guest hydrate 不覆盖新登录态”。 - 关联:
src/components/auth/AuthGate.tsx、src/components/auth/AuthGate.test.tsx、docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md。
刷新网页后登录态失效
- 现象:刷新网页后,用户明明有本地 access token,却回到未登录状态。
- 原因:
AuthGatehydrate 曾先强制调用refreshStoredAccessToken();当 refresh cookie 临时失效、代理错配或后端返回401时,该方法会先清空本地 access token,随后/api/auth/me只能恢复成未登录。 - 处理:
refreshStoredAccessToken()增加clearOnFailure选项;AuthGate在已有本地 access token 时先用/api/auth/me确认用户,确认成功后再后台 refresh 续期与写每日登录埋点,后台 refresh 失败不清 token。 - 追加处理:
/api/auth/refresh只有明确返回401/403时才代表登录态权威失效,可以清本地 access token 并触发全局 auth 变化;服务器重启、Nginx 502/503/504、浏览器Failed to fetch或 refresh 响应契约异常都属于暂时不可用,不能把已有本地 token 清掉,否则重启窗口会把所有打开页面踢成未登录。 - 契约:
/api/auth/refresh成功响应按共享契约RefreshSessionResponse { token }解析;测试 mock 不要额外塞{ ok: true, token }遮住真实恢复路径。 - 验证:
npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx -t "explicit refresh opts out|auth gate keeps a valid local token login"。 - 关联:
src/services/apiClient.ts、src/components/auth/AuthGate.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
登录后推荐页加载出作品又回到未登录
- 现象:前端登录成功后进入推荐页,推荐页自动加载出一个作品,随后瞬间回到未登录;停留在其他页面或推荐页没加载出作品时不复现。
- 原因:推荐页 embedded 运行态会自动发起受保护写请求。若这些卡片级后台请求遇到
401或 refresh 失败,默认请求层曾清空 access token 并广播全局 auth 事件,导致AuthGate重新 hydrate 成未登录态。更隐蔽的是,refreshAccessToken()自身曾在 refresh 失败时静默清 token,即便调用方关闭了clearAuthOnUnauthorized,也可能让后续 hydrate 变成未登录。 - 处理:请求层统一使用
authImpact: 'global' | 'local'区分账号权威请求与局部后台请求;推荐页自动运行态、图片换签、公开拼图运行态和平台 bootstrap 私有投影刷新统一使用BACKGROUND_AUTH_REQUEST_OPTIONS/RUNTIME_BACKGROUND_AUTH_OPTIONS,并等canReadProtectedData为 true 后再启动;用户主动点击的账号动作仍保留默认全局鉴权失败处理。 - 追加处理:推荐页嵌入运行态要按真实身份分流,已登录或已有 access token 时继续走账号 Bearer + local auth impact,不能误带 runtime guest token;只有匿名访客才申请并透传 runtime guest token。
- 追加处理:generated 私有图片换签
/api/assets/read-url也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。 - 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,
startPuzzleRun、通关自动submitPuzzleLeaderboard、下一关advancePuzzleNextLevel和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。 - 追加处理:通关后
refreshSaveArchives()、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。 - 追加处理:未登录推荐页启动任一公开正式玩法时,
/api/runtime/*局内路由必须使用RuntimePrincipal,前端通过PlatformEntryFlowShellImpl的统一 request options helper 给 start / checkpoint / finish / input / drop / click / restart / time-up / leaderboard / next-level 等动作透传 runtime guest token;公开 runtime detail 读取如跳一跳、敲木鱼必须显式skipAuth/skipRefresh,匿名推荐流不能补读受保护创作详情,否则会在真正开局前打出/api/auth/refresh 401。 - 验证:
npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"。 - 关联:
src/services/apiClient.ts、src/services/assetReadUrlService.ts、src/services/puzzle-runtime/puzzleRuntimeClient.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md。
推荐页作品卡一直显示加载中
- 现象:推荐页有公开作品,但主视口一直停在“加载中...”,没有进入作品,也没有显示可操作错误。
- 原因:推荐页自动启动嵌入运行态时先设置
activeRecommendEntryKey/activeRecommendRuntimeKind/isStartingRecommendEntry,但失败或并发切换时外层缺少稳定错误态和请求版本保护,旧启动请求可能晚到覆盖新状态。 - 处理:
selectRecommendRuntimeEntry使用启动请求版本号丢弃旧请求;启动失败统一设置activeRecommendRuntimeError = "作品暂时无法进入,请稍后再试。"并关闭isStartingRecommendEntry。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation surfaces start failure"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
推荐页未登录入口误打开公开详情
- 现象:新用户默认在发现页,但点击推荐页或推荐封面后,如果复用公开作品详情入口,可能绕过推荐页沉浸运行态,打开普通公开详情页。
- 原因:
RpgEntryHomeView曾只有onOpenGalleryDetail一个回调,同时服务发现页公开详情和推荐页作品入口;一旦为发现页保留公开浏览能力,推荐页也会跟着打开详情。 - 处理:公开详情与推荐页入口分离为
onOpenGalleryDetail和onOpenRecommendGalleryDetail。发现页、搜索和排行榜保留公开详情;推荐 Tab、推荐封面、推荐运行态错误重试和桌面推荐模块走推荐运行态入口,不再主动弹登录窗。登录门禁只保留给创作、个人作品、删除、发布、Remix 等账号或所有权动作。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
Rust 冷编译导致 api-server 健康检查误超时
- 现象:旧
npm run dev:rust在 Windows 冷编译/链接阶段误判/healthz等待超时并杀掉cargo run;现入口为npm run dev或npm run dev:api-server。 - 原因:脚本把 SpacetimeDB 与 api-server 等待窗口混在一起,未考虑 Rust 冷编译耗时。
- 处理:按冷编译超时修复文档拆分等待窗口。
- 验证:冷启动时不再误杀仍在编译的 api-server。
- 关联:
docs/technical/API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md。
Windows debug api-server 主线程栈溢出
- 现象:
cargo check -p api-server和build_router测试通过,但npm run dev:api-server在 Windows debug 启动时thread 'main' has overflowed its stack。 - 原因:
api-serverAxum 路由树已经很深,debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。 - 处理:入口
main用显式 16MB 栈线程启动 Tokio runtime,并把实际服务逻辑放入run_server();新增路由时优先用小 router.merge(),避免继续拉长主链。 - 验证:
npm run dev:api-server后/healthz返回 200,相关路由冒烟通过。 - 关联:
server-rs/crates/api-server/src/main.rs、server-rs/crates/api-server/src/app.rs。
Windows debug api-server.exe 锁文件与强杀退出码容易混淆
- 现象:
cargo run -p api-server或npm run dev:api-server报failed to remove file ... target\debug\api-server.exe;清理旧进程后,旧终端可能继续打印process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)。 - 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条
npm run dev:api-server链路仍在运行,进程树为npm run dev:api-server -> node scripts/dev.mjs api-server -> cargo run -> api-server.exe。0xffffffff常见于排障时用Stop-Process -Force强制结束旧api-server.exe后由 Cargo 回显,不一定代表新启动失败。 - 处理:先按目标路径确认并停止本仓库的旧
api-server.exe及其父级cargo/node/cmd启动链路,再重新启动;不要同时开多个npm run dev:api-server。 - 验证:确认没有匹配
C:\Genarrative\server-rs\target\debug\api-server.exe的进程后,Remove-Item能删除旧 exe;随后npm run dev:api-server启动并访问/healthz返回 200。 - 关联:
scripts/dev.mjs、server-rs/crates/api-server/src/main.rs。
dev scheduler 端口被旧进程占用时会误判健康检查
- 现象:旧本地 dev 链路可能输出
Port 3000 is in use, trying another one...,随后api-server.exe报AddrInUse/code: 10048。 - 原因:旧
api-server仍监听默认8082时,脚本的/healthz探测会命中旧进程并误判新服务已就绪;旧 Vite 占住3000时,Vite 默认漂移到新端口,浏览器仍可能打开旧页面。 - 处理:
scripts/dev.mjs已在 publish / 编译前解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并让 Vite 使用--strictPort;遇到端口占用时会自动选择后续可用端口,也可显式传入--api-port/--web-port/--admin-web-port。 - 验证:默认端口被占用时,完整栈应打印
[dev:ports] ... 不可用,改用 ...并把实际端口传给后续 publish、健康检查和 Vite 代理;清理端口后重新启动不再命中旧/healthz。 - 关联:
scripts/dev.mjs、docs/technical/DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md。
Windows debug 长 SSE Future 触发 api-server 断连
- 现象:前端 Vite 代理请求
/api/runtime/creative-agent/sessions/{sessionId}/messages/stream报read ECONNRESET,随后api-server.exe以0xffffffff退出,dev:spacetime回收 SpacetimeDB、Vite 和后台 Vite。 - 原因:单个
async_stream::stream!中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future;真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。 - 处理:长 SSE 路由优先使用
tokio::spawn跑业务流程,通过mpsc+UnboundedReceiverStream向 Axum 返回轻量 stream;失败时更新会话为failed并发送 SSEerror,不要把大段执行逻辑内联到路由返回的 stream future 中。 - 验证:补充实际
collect()SSE body 的路由测试,确认首轮包含stage、puzzle_template_catalog和done,且不会提前发送puzzle_template_selection/puzzle_cost_range;再执行cargo check -p api-server、cargo test -p api-server creative_agent,联调时用npm run dev:api-server检查/healthz。 - 关联:
server-rs/crates/api-server/src/creative_agent.rs、server-rs/crates/api-server/src/app.rs。
creative-agent 过程项不要把历史事件渲染成运行中
- 现象:智能创作页过程中多个阶段从一开始同时转圈,生成结束或进入模板确认后仍有过程项保持转圈。
- 原因:前端把历史
stage、tool_started和thought_summary_delta都按 active 渲染;后端工具开始/完成事件如果toolCallId不一致,也会导致开始事件无法收口。 - 处理:
- 只有最新且仍在执行的 stage 可为 active;等待确认、等待用户、target ready 和 failed 都是静态状态。
- 工具开始事件必须等同一
toolCallId的tool_completed收口;兼容旧流时可按后续同名完成事件兜底。 - 思考摘要只展示用户可见摘要,且流结束或会话进入等待/完成/失败态后必须改成 done。
- 验证:前端测试断言完成后
CreativeAgentProcessItem不再存在tone === 'active';后端测试确认工具开始/完成事件使用相同toolCallId。 - 关联:
src/components/creative-agent/creativeAgentViewModel.ts、server-rs/crates/api-server/src/creative_agent.rs、docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md。
creative-agent 会话切换要清理本地待确认模板
- 现象:用户在一个智能创作会话中点开模板确认面板后,立即切到另一条创作会话,可能看到上一会话的确认面板残留。
- 原因:模板确认面板的
pendingSelection是CreativeAgentWorkspace本地 UI 状态,不属于后端 session 快照;组件复用时如果不监听sessionId清理,会跨会话泄漏。 - 处理:工作区以
session?.sessionId为边界清空pendingSelection;服务端仍以puzzleTemplateSelection/targetBinding作为正式业务状态。 - 验证:前端测试先点开模板确认面板,再 rerender 到另一 session,断言确认面板消失。
- 关联:
src/components/creative-agent/CreativeAgentWorkspace.tsx、src/components/creative-agent/CreativeAgentWorkspace.test.tsx。
创作 Tab 语义迁移后,旧“新建作品”测试要改看智能创作首页
- 现象:把
create从旧创作中心切到CreativeAgentHome后,旧测试仍尝试在创作页找“新建作品”类型卡,导致用例失败或定位不到元素。 - 原因:产品语义已经变成“创作 = 智能创作首页,草稿 = 旧作品架”,但测试夹具和 helper 还沿用旧入口。
- 处理:把这类测试改成验证智能创作首页、快捷胶囊、抽屉与草稿 Tab;同时给
useRpgEntryLibraryDetail这类恢复路径补上setPlatformTabToDraft。 - 验证:定向
vitest、eslint、typecheck、check:encoding都通过。 - 关联:
src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx、src/components/rpg-entry/useRpgEntryLibraryDetail.ts。
server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
- 现象:在
server-rs下无参数cargo build期望同时构建spacetime-module,导致链接或构建范围误判。 - 原因:workspace default-members 当前只包含
crates/api-server;SpacetimeDB module 有独立构建/发布方式。 - 处理:默认 Rust 构建只覆盖原生
api-server;本地模块发布继续走spacetime publish --module-path ... --build-options="--debug"/ bindings 生成流程。 - 验证:查看
server-rs/Cargo.tomldefault-members,并按相关 SpacetimeDB 文档执行模块构建。 - 关联:
server-rs/Cargo.toml、docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md。
Windows 原生 spacetime-module 单测会链接缺失 SpacetimeDB 宿主符号
- 现象:在 Windows 上执行
cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml可能编译到链接阶段后失败,出现LNK2019/LNK1120,缺失datastore_insert_bsatn、procedure_start_mut_tx、console_log等 SpacetimeDB 宿主符号。 - 原因:
spacetime-module依赖的 SpacetimeDB runtime API 面向 wasm 宿主环境,原生 test exe 链接不到这些宿主导出。 - 处理:日常语法和类型验证使用
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml;需要验证模块行为时走 SpacetimeDB publish/dev 或模块域纯 Rust crate 的单测,不把该原生链接错误当作业务测试失败。 - 验证:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml能通过;原生cargo test若仍报上述宿主符号缺失,按当前限制记录为未执行。 - 关联:
server-rs/crates/spacetime-module、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
Rust 构建不要让不可用的 sccache 阻断 rustc
- 现象:Cargo 报
could not execute process sccache ... rustc.exe -vV (never executed)、sccache: error: Timed out waiting for server startup,或sccache: caused by: Failed to send data to or receive data from server / Failed to read response header / failed to fill whole buffer;真实rustc -Vv可以执行,但构建在调用包装器时失败。 - 原因:环境、Jenkinsfile 或
server-rs/.cargo/config.toml启用了sccachewrapper,但当前 agent 没有可执行的sccache、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。Windows 本机若配置了SCCACHE_OSS_*,sccache daemon 冷启动会先经 OSS/本机代理完成缓存读写检查,再监听127.0.0.1:4226;代理或 OSS 链路慢时,Cargo 的sccache rustc -vV可能先超时。 - 处理:保留
server-rs/.cargo/config.toml的rustc-wrapper = "sccache";本地npm run dev/npm run dev:spacetime/npm run dev:api-server由scripts/dev.mjs给 Rust 子进程注入直通 wrapper,自动绕过项目默认 sccache,避免损坏的 daemon 阻断spacetime publish或api-server启动;显式设置的非 sccache 自定义 wrapper 会被保留。Windows 本机优先在%APPDATA%\Mozilla\sccache\config\config写入server_startup_timeout_ms = 60000,拉长 client 等待 daemon 完成 OSS 初始化的时间,然后删除server-rs/target/.rustc_info.json里缓存的失败探测结果并重跑原始 Cargo 命令。冷启动验证优先用sccache --stop-server,不要在另一个cargo/rustc仍在编译时taskkill /F /IM sccache.exe /T,否则 proc-macro crate 可能被打断并表现为serde_derive/spacetimedb-bindings-macro的sccache ... exit code: 1。若只做临时排障,可在 Git Bash 中执行RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build ...,或在 PowerShell 用cargo check -p api-server --config "build.rustc-wrapper=''"一次性绕过 wrapper;生产流水线必须先实际执行sccache --version,失败时移除RUSTC_WRAPPER并回退到直接rustc。 - 验证:
rustc -Vv能输出版本;本地npm run dev能完成spacetime publish、api-server/healthz、主站 Vite 和后台 Vite 启动;冷启动后原始cargo check -p api-server和cargo check -p spacetime-module能通过;sccache --show-stats显示Cache location oss, name: genarrative-sccache,证明原始 Cargo/Jenkins 路径仍可使用 sccache/OSS 缓存;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。 - 关联:
scripts/dev.mjs、jenkins/Jenkinsfile.production-stdb-module-build、docs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md、docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
- 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
- 原因:旧 Jenkins / 旧本地远端部署脚本文档仍作为历史经验保留。
- 处理:生产相关操作先看
PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md,再按需追溯旧文档。 - 验证:发布链路使用当前
deploy/systemd、deploy/nginx、scripts/deploy和jenkins/Jenkinsfile.production-*。 - 关联:
docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
Web Deploy 只从 Jenkins 构建归档取包
- 现象:
Genarrative-Web-Deploy需要发布 Web 时,不应再在构建机或 release agent 的本地缓存目录查找web.tar.gz。 - 原因:Web 发布包已经由
Genarrative-Web-Build归档到 Jenkins 构建产物,deploy 阶段继续读本地缓存或通过rsync回构建机拉包会让 release agent 依赖机器拓扑和本地路径。 - 处理:
Genarrative-Web-Build直接归档build/<version>/web.tar.gz、web.tar.gz.sha256和release-manifest.json;Genarrative-Web-Deploy使用copyArtifacts从指定BUILD_JOB_NAME/BUILD_NUMBER_TO_DEPLOY复制完整产物,不保留WEB_ARTIFACT_ROOT、WEB_ARTIFACT_SYNC_HOST或web-artifact-pointer.txt口径。 - 验证:deploy 工作区应直接出现
build/<version>/web.tar.gz与web.tar.gz.sha256;后续仍由scripts/deploy/production-web-deploy.sh执行 checksum 校验和解压 smoke。 - 关联:
jenkins/Jenkinsfile.production-web-deploy、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Copy Artifact Production 模式下来源 Job 必须显式授权
- 现象:Deploy / Publish / Import 在
copyArtifacts立即报Unable to find project for artifact copy: <job>,但 Jenkins 中的来源 Job、指定构建号和归档产物都存在。 - 原因:Copy Artifact 已启用推荐的
Production模式,但产物生产者的 Jenkinsfile 没有copyArtifactPermission;插件会把权限不足伪装成“找不到项目”。 - 处理:在产物生产者的 Declarative Pipeline
options内精确授权固定消费者:Stdb Build 授权 Stdb Publish,API Build 授权 API Deploy,Web Build 授权 Web Deploy,Database Export 授权 Database Import。不使用*,不通过全局Job/Read扩权,不把插件退回 Migration 模式规避。 - 验证:运行
npm run check:production-ops;上线后先运行一次四个产物生产者中本次需要的 Job,确认 liveconfig.xml出现CopyArtifactPermissionProperty,再重跑消费者。 - 关联:
jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-web-build、jenkins/Jenkinsfile.production-database-export、scripts/check-production-ops-guardrails.mjs。
Jenkins 生产流水线拉 Git 统一走本机 SSH
- 后续更新:2026-07-14 起所有生产 Job 的
Pipeline script from SCM和 Jenkinsfile 内部 checkout 统一使用本机 SSH 地址ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git与凭据genarrative-local-gitea-ssh,不再保留局域网 IP、HTTP 内网地址或公网 fallback。 - 现象:生产发布、数据库导入导出、服务器配置、构建或
Genarrative-Full-Build-And-Deploy流水线执行GitSCM checkout时,如果 Jenkins 生成的 fetch 是+refs/heads/*:refs/remotes/origin/*,公网 Git 链路可能在收包阶段以git-remote-https died of signal 15、curl 56 GnuTLS recv error (-9)、early EOF、invalid index-pack output失败;写死127.0.0.1:3000也会在当前执行 agent 不是 Gitea 所在机器时失败。 - 原因:
127.0.0.1只代表当前执行阶段的 agent 自身,因此 Git checkout 必须收口到同机运行 Gitea SSH、带linux && genarrative-build标签的 Jenkins Built-In Node;公网域名和局域网 IP 会引入额外网络、代理、TLS 与地址漂移。即使使用本机 Git,如果GitSCM没有显式 refspec 并开启CloneOption honorRefspec=true,Jenkins Git 插件仍会拉取所有分支。 - 处理:Full、Web、API、Stdb、Server-Provision 与数据库导入导出的源码准备统一在 Jenkins Built-In Node 使用
GIT_REMOTE_URL=ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git和GIT_REMOTE_CREDENTIAL_ID=genarrative-local-gitea-ssh,GIT_REMOTE_FALLBACK_URL留空。数据库导入导出把经过 commit 校验的必要脚本 stash 给目标 agent,release / dev 目标阶段只 unstash,不再 checkout Git 或挂载 Git SSH 凭据。首次 checkout 保留目标分支 refspec、CloneOption shallow=true depth=1 noTags=true honorRefspec=true,随后由scripts/jenkins-checkout-source.sh复用并在必要时逐步加深。 - 验证:扫描本地 Jenkins live Job
config.xml和所有生产 Jenkinsfile,确认 Git URL 均为ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git,凭据仍为genarrative-local-gitea-ssh且 fallback 为空;确认数据库导入导出在 Prepare 阶段 checkout / stash,目标阶段只 unstash;在 Jenkins 凭据环境运行git ls-remote ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git HEAD,并运行npm run check:production-ops、bash -n scripts/jenkins-checkout-source.sh。 - 关联:
jenkins/Jenkinsfile.production-full-build-and-deploy、jenkins/Jenkinsfile.production-web-build、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-web-deploy、jenkins/Jenkinsfile.production-api-deploy、jenkins/Jenkinsfile.production-stdb-module-publish、jenkins/Jenkinsfile.production-server-provision、jenkins/Jenkinsfile.production-database-export、jenkins/Jenkinsfile.production-database-import、scripts/jenkins-checkout-source.sh、docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
Jenkins 可选参数在 set -u 下不能裸读
- 现象:数据库导入或导出流水线报
INCLUDE_TABLES: unbound variable,或其它可选参数在 Bash 中未定义即退出。 - 原因:Jenkins string/boolean 参数留空时不一定会导出同名环境变量,而生产数据库导入导出脚本块启用了
set -u。 - 处理:进入 Bash 执行块后先使用
${VAR:-}或${VAR:-默认值}收敛成本地变量;必填项使用${VAR:?中文错误}明确失败原因。 - 验证:扫描
jenkins/Jenkinsfile.production-database-export与jenkins/Jenkinsfile.production-database-import,确认INCLUDE_TABLES、CHUNK_SIZE、SERVER_BACKUP_DIRECTORY、SMOKE_HEALTH_URL等可选参数不再裸读。 - 关联:
docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md、jenkins/Jenkinsfile.production-database-export、jenkins/Jenkinsfile.production-database-import。
Jenkins 二次 checkout 后脚本执行位会被 Git 还原
- 现象:
Genarrative-Server-Provision已在 shell 块前面对脚本执行chmod +x,但进入Prepare Provision Tools后仍报scripts/prepare-server-provision-tools.sh: Permission denied/exit code 126。 - 原因:该阶段会先运行
scripts/jenkins-checkout-source.sh,脚本内部执行git reset --hard HEAD和git clean -fd,会把前面临时chmod的执行位还原为 Git 记录的 mode;若被直接执行的脚本在仓库里是100644,二次 checkout 后仍不可执行。 - 处理:需要直接以
scripts/*.sh方式执行的 Jenkins 脚本应提交为 Git100755;如果只想临时授权,必须放在scripts/jenkins-checkout-source.sh完成之后。 - 验证:运行
git ls-files --stage scripts/prepare-server-provision-tools.sh,确认 mode 为100755;重新跑Genarrative-Server-Provision时应进入工具下载/打包日志,而不是停在Permission denied。 - 关联:
jenkins/Jenkinsfile.production-server-provision、scripts/prepare-server-provision-tools.sh、scripts/jenkins-checkout-source.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision 目标机只接收并执行 Jenkins 上传的脚本
- 现象:
Genarrative-Server-Provision选择DEPLOY_TARGET=development/release时,目标阶段仍要求填写SOURCE_GIT_REMOTE_URL,或在目标 dev / release agent 上执行 Git checkout。 - 原因:旧流水线要求目标 agent 自己拉取 provision 脚本,导致服务器初始化依赖目标机到 Git remote 的网络可达性;公网 Git fallback 还会让目标 agent 内网源不可达时悄悄改从公网拉源码,掩盖路由问题。新口径改为 Jenkins 构建节点准备并上传脚本,目标机只接收和执行。
- 处理:
Prepare Provision Files在linux && genarrative-build上使用固定内网 SSH 源和 Jenkins 凭据genarrative-local-gitea-sshcheckout / 校验SOURCE_BRANCH/COMMIT_HASH,并把 provision 脚本、scripts/deploy/**、deploy/**和.jenkins-source-commitstash 给目标 agent。Provision Target下的Receive Provision Files、Prepare Provision Tools和Provision Server必须运行在目标部署 agent:development 使用linux && genarrative-dev-deploy,release 使用linux && genarrative-release-deploy。目标 agent 不再需要SOURCE_GIT_REMOTE_URL,也不再 checkout Git。 - 验证:Jenkins 日志中应先看到
Prepare Provision Files在linux && genarrative-build上完成源码准备和stash 'server-provision-files',再看到Provision Target下的Receive Provision Files、Prepare Provision Tools和Provision Server在目标 dev / release agent 上运行;目标阶段日志不应出现 Git checkout、SOURCE_GIT_REMOTE_URL、Git 主地址拉取失败...改用备用地址或https://git.genarrative.world/GenarrativeAI/Genarrative.git。 - 关联:
jenkins/Jenkinsfile.production-server-provision、scripts/prepare-server-provision-tools.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision 不要无条件下载工具包
- 现象:目标 dev / release 机器已经安装正确版本的 SpacetimeDB 或
otelcol-contrib,但Prepare Provision Tools仍每次下载 release tarball,网络慢或 GitHub 不稳时会把服务器初始化卡在准备阶段。 - 原因:工具准备阶段如果只按“生成交付包”理解,会忽略它已经运行在目标部署 agent 上这一事实;此时目标机本地的
/usr/local/bin/otelcol-contrib与${SPACETIME_ROOT}/bin/current就是可信状态源。 - 处理:
scripts/prepare-server-provision-tools.sh必须先检查目标机状态:otelcol-contrib --version命中OTELCOL_VERSION时复制现有二进制;spacetimedb-cli --version命中SPACETIME_EXPECTED_VERSION或SPACETIME_DOWNLOAD_ROOT推导出的版本且 standalone 同时存在时,复制${SPACETIME_ROOT}/bin并生成 wrapper。只有缺失、不可执行或版本不匹配时,才查PROVISION_DOWNLOADS_DIR或下载源。 - 验证:运行
bash scripts/check-server-provision-tools.sh;Jenkins 日志应先出现“检查目标机 ...”,已有版本命中时出现“复用目标机已有 ...”,且不出现“下载 ...”。 - 关联:
scripts/prepare-server-provision-tools.sh、jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
个人任务 scope 不得扩成 work/site/module
- 现象:个人任务配置为
work/site/module后进度串桶或静默按 0 处理。 - 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
- 处理:Admin 任务配置页不展示范围选择,保存时固定
scopeKind: 'user';API 和领域构造层拒绝非User。 - 验证:非
userscope 返回错误;相关测试覆盖Site/Module/Work被拒绝。 - 关联:
docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md、docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md。
拼图发布 409 不一定是接口故障
- 现象:拼图结果页点击发布后,控制台出现
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions 409 (Conflict),用户只看到发布失败。 - 原因:
publish_puzzle_work是资产操作发布入口,发布前会预扣1枚泥点;余额不足时后端按业务冲突返回409 CONFLICT,details.message为泥点余额不足。 - 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
- 验证:
PuzzleResultView单测覆盖发布弹窗内展示泥点余额不足。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md、docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md。
拼图发布检查阶段会在事件落库时炸 wasm
- 现象:拼图发布在“发布检查”环节直接报
The module instance encountered a fatal error,wasm backtrace 指向spacetime_module::puzzle::publish_puzzle_work,并停在procedure_commit_mut_tx的 commit 阶段。 - 原因:
publish_puzzle_work_tx会无条件调用emit_puzzle_work_published_event写入puzzle_event;该表的event_id是主键,而事件 ID 由profile_id + published_at_micros组成。只要同一发布动作被重复执行、重放,或极端情况下发生时间戳碰撞,commit 时就会因主键冲突触发 fatal error。 - 处理:待修复。发布事件写入需要改成幂等,或在重复发布时显式跳过已存在的
event_id;发布动作本身也应补一层更明确的幂等键,避免把重复提交直接推到事务提交阶段。 - 验证:对同一
session_id/profile_id/published_at_micros重复调用publish_puzzle_work时,不应再在 commit 阶段炸 wasm;正常发布仍应生成作品、更新 session,并可进入公开详情。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/api-server/src/puzzle/handlers.rs、server-rs/crates/spacetime-client/src/module_bindings/puzzle_event_table.rs。
拼图会过早进入待发布态,结果页可能空图但仍显示可发布
- 现象:拼图创作有时刚结束就跳到“待发布”结果页,但结果页里的正式图还是空的,发布检查随后又会拦住,用户会感觉“已经完成了却又不能发布”。
- 原因:拼图的待发布判定太弱,
build_result_preview/validate_publish_requirements和is_puzzle_session_snapshot_publish_ready只检查了作品名、简介、标签、关卡名和 cover 图,没有要求level_scene_image_src、ui_spritesheet_image_src、level_background_image_src等完整资产都齐;历史前端恢复链路里的hasRecoverableGeneratedPuzzleDraft/normalizeRecoveredPuzzleDraftSession也只要有 cover 或候选图就会把草稿当成已完成。 - 处理:前端恢复链路已收口到
platformPuzzleDraftRecoveryModel.ts,只有首图、关卡画面、UI spritesheet 与关卡背景资产包完整时才把恢复草稿抬为完成态;后端build_result_preview/validate_publish_requirements/is_puzzle_session_snapshot_publish_ready也已收紧到同一完整资产包门槛。 - 验证:当某个拼图草稿只补齐首图、但关卡背景或 UI spritesheet 仍缺失时,前端恢复链路不应把它误判为已完成,后端也不应进入
ready_to_publish或返回publishReady=true。 - 关联:
server-rs/crates/module-puzzle/src/application.rs、server-rs/crates/api-server/src/puzzle/tags.rs、server-rs/crates/api-server/src/puzzle/draft.rs、src/components/platform-entry/platformPuzzleDraftRecoveryModel.ts、src/components/puzzle-result/PuzzleResultView.tsx。
WebGL 画布在高 DPR 移动端放大溢出
- 现象:抓大鹅试玩入口进入后,3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
- 原因:
WebGLRenderer.setPixelRatio(...)会把绘图缓冲区乘上设备 DPR;如果没有给renderer.domElement单独设置 CSSwidth/height: 100%和绝对铺满,浏览器可能把高 DPR 缓冲区尺寸当成页面显示尺寸。 - 处理:中心棋盘和托盘预览的 WebGL canvas 统一套用
position:absolute; inset:0; width:100%; height:100%; display:block,renderer.setSize(..., false)只负责同步绘图缓冲区。 - 验证:强制移动端
390x844、DPR 2 截图,确认棋盘左右边界在视口内,canvas CSS 尺寸等于容器尺寸,内部width/height属性可大于 CSS 尺寸。 - 关联:
src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_RUNTIME_3D_GEOMETRY_EXPERIMENT_2026-05-02.md。
Hyper3D subscriptionKey 不要按固定短文本限长
- 现象:抓大鹅生成草稿时,内联 Rodin 图生 3D 模型提交成功后,状态轮询报
subscriptionKey 超过 256 字符,导致/api/creation/match3d/sessions/{sessionId}/actions返回 400。 - 原因:
subscriptionKey是 Hyper3D 返回的 opaque token,长度由上游决定;后端状态查询曾复用普通文本校验,把它限制在 256 字符。 - 处理:
query_task_status对subscriptionKey只做 trim 和非空校验,不做固定长度限制;前端临时任务和 Match3D 草稿响应可继续展示该 token,但不要把它当作可编辑短文本。 - 验证:
cargo test -p api-server accepts_opaque_subscription_key_without_length_cap --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/hyper3d_generation.rs、docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md。
抓大鹅新草稿不要再接回 Rodin 或 GLB 生成
- 现象:修改抓大鹅素材时容易沿用旧 Rodin/GLB 方案,导致新草稿生成耗时变长、进度停在模型阶段,或运行态等待不存在的 GLB。
- 原因:仓库里保留了 Hyper3D 通用代理和历史模型字段,旧文档也曾要求草稿阶段同步生成 GLB。当前产品口径已经改为 2D 多视角素材。
- 处理:新
match3d_compile_draft与批量新增只生成 2D 图片:每个物品 5 个形态,单张2K 1:1物品 spritesheet 固定10*10,每行承载两种物品、每种五个形态,单张最多承载 20 种物品。素材图 prompt 固定要求单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景,上传 OSS 前先把整张 spritesheet 绿幕处理为透明 alpha,再由运行态和编辑器按 alpha 连通域解析;generatedItemAssets[].status使用image_ready,发布校验看imageViews[]、首图引用或可解析的物品 spritesheet。generated-models仅用于历史外部模型链接转存,不能作为新生产链路。 - 验证:
cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml、npm run test -- src\services\miniGameDraftGenerationProgress.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx。 - 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅切图路径不能只用中文物品名
- 现象:草稿页
素材配置 > 物品中多个素材名称不同,但预览图片完全一样。 - 原因:中文物品名经过 OSS 路径段清洗后都可能退化成
item,多张切割图片写到同一个 object key,后写入覆盖先写入。 - 处理:切割图上传路径必须带稳定唯一
itemId前缀,例如items/match3d-item-1-item/views/view-01.png;运行态读取 generated 私有图片时通过同源/api/assets/read-url换签,不直接请求裸 OSS 路径。 - 验证:后端单测覆盖中文名路径唯一,前端运行态测试覆盖 generated 图片源解析。
- 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅生成素材不能只挂在 compile response
- 现象:抓大鹅草稿生成完成后停留在结果页能看到切割好的物品图片;退出后从草稿 Tab 重新进入同一草稿,素材列表变回默认占位或为空,已生成的物品名称和图片丢失。
- 原因:
generatedItemAssets如果只附加在match3d_compile_draft的 HTTP response draft 上,刷新或重进时getMatch3DWorkDetail只能读取 SpacetimeDB 中的match3d_work_profile;旧 mapper 返回空数组,自然无法恢复素材。拼图链路已经通过save_puzzle_generated_images把候选图和 levels 写回 work profile,抓大鹅也必须同样写持久字段。 - 处理:compile 成功时把独立物品图片列表序列化写入
match3d_work_profile.generated_item_assets_json;update_match3d_work/publish_match3d_work保留该字段;API work summary/detail 映射反序列化为generatedItemAssets。前端保持“本次 draft 优先,重进 profile 兜底”的读取顺序。 - 验证:
cargo test -p spacetime-client match3d --manifest-path server-rs/Cargo.toml、cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml、npm run test -- src/components/match3d-result/Match3DResultView.test.tsx。 - 关联:
server-rs/crates/spacetime-module/src/match3d/*、server-rs/crates/spacetime-client/src/mapper.rs、server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅试玩和正式运行态不要只读草稿页本地素材预览
- 现象:结果页能看到生成的物品图片,但点击试玩或从推荐 / 公开作品进入正式抓大鹅时,局内仍显示默认积木素材。
- 原因:结果页本地
assetDrafts和作品 profile 的generatedItemAssets可能不同步;推荐流内嵌运行态若只读卡片摘要,卡片缺素材时会把已持久化 profile 素材丢掉;点击试玩时 React state 异步更新也可能让运行态第一帧读取旧match3dProfile。 - 处理:删除、批量新增、音效生成或封面引用物品素材后,都把当前
generatedItemAssets写回作品 profile;Match3DResultView合并同itemId的 draft/profile 素材,用 profile 已有imageViews[]、首图引用、backgroundMusic或backgroundAsset补齐旧 draft;点击试玩前把试玩可用物品种类通过itemTypeCountOverride降到已生成 2D 素材数量;推荐流内嵌运行态启动前若卡片摘要没有物品图片素材,补读getMatch3DWorkDetail(profileId)并把详情资产传给Match3DRuntimeShell。PlatformEntryFlowShellImpl需要维护match3dRuntimeProfile,在startMatch3DRunFromProfile创建 run 后立即锁定本次完整 profile,runtime 渲染时优先按run.profileId使用这份 profile,而不是等待普通match3dProfilestate 下一轮刷新。同 profile 下已有generatedItemAssets时不能因为图片完整性判断失败就覆盖为空数组。判断是否需要补读详情时只看imageViews[]或imageSrc/imageObjectKey;背景、音乐、容器 UI 是附属运行态资产,不能单独证明物品素材已完整。 - 验证:执行
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx、npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx,并检查历史草稿和公开 M3 作品的 Network 响应里generatedItemAssets[].imageViews/imageSrc/imageObjectKey。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅 UI 背景和容器只在顶层字段时也要传进运行态
- 现象:抓大鹅草稿 / 推荐卡片响应里已有
generatedBackgroundAsset,结果页 UI 预览能看到纯背景图和容器图,但进入试玩或正式局内仍显示默认渐变背景和默认圆形容器。 - 原因:部分链路把 UI 资产只放在作品顶层
generatedBackgroundAsset/backgroundImageObjectKey,没有同步放进首个generatedItemAssets[].backgroundAsset;如果运行态入口只传generatedItemAssets和backgroundImageSrc,Match3DRuntimeShell就拿不到containerImageObjectKey。 - 处理:
PlatformMatch3DGalleryCard、mapPublicWorkDetailToMatch3DWork、resolveMatch3DRuntimeGeneratedBackgroundAsset和Match3DRuntimeShell都必须保留并传递顶层generatedBackgroundAsset;运行态背景读取顺序为backgroundImageSrc/ 顶层generatedBackgroundAsset.image*/generatedItemAssets[].backgroundAsset.image*,容器读取顺序为顶层generatedBackgroundAsset.containerImage*/generatedItemAssets[].backgroundAsset.containerImage*。 - 验证:执行
npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx和npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "Match3D runtime";浏览器 Network 中背景和容器 generated path 应先请求/api/assets/read-url换签,局内出现match3d-background-image和match3d-container-image对应图片。 - 关联:
src/components/match3d-runtime/Match3DRuntimeShell.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/rpgEntryWorldPresentation.ts、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅容器参考图必须进入 edits multipart image 并接管棋盘外观
- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像
pot-fused-reference.png,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。 - 原因:容器参考图必须进入
gpt-image-2/v1/images/editsmultipartimagepart,并配合强 prompt 锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认rounded-full锅壳和overflow-hidden,生成图也会被默认视觉覆盖或裁掉。 - 处理:抓大鹅
1:1容器 UI 图统一调用 VectorEnginePOST /v1/images/edits,参考public/match3d-background-references/pot-fused-reference.png的透明容器图由后端作为imagepart 上传;该参考图属于后端生图协议输入,需通过include_bytes!编译进api-server,不能在运行时按当前工作目录读取public/。Match3DRuntimeShell在容器图换签并成功加载后,把棋盘外壳切为透明和overflow-visible,只在容器缺失或加载失败时使用默认圆形容器。 - 验证:执行
cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml、cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml、npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx;真实联调看容器生成请求是否命中/v1/images/edits,局内match3d-container-image是否渲染且match3d-board不再含默认rounded-full。 - 关联:
server-rs/crates/api-server/src/openai_image_generation.rs、server-rs/crates/api-server/src/match3d.rs、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅结果页音频试听也要先换签
- 现象:抓大鹅草稿生成完成后,背景音乐已写在
generatedItemAssets[0].backgroundMusic.audioSrc,但素材配置 > 背景音乐或物品详情音效<audio>不能播放,Network 可能请求裸/generated-match3d-assets/...mp3并返回 403。 - 原因:结果页试听控件和运行态一样运行在浏览器里,不能直接读取 generated 私有对象;只在运行态换签会造成“运行态可能有声,结果页不能预览”的割裂。
- 处理:结果页音频控件统一通过
useResolvedAssetReadUrl//api/assets/read-url取得签名 URL 后再传给<audio>;换签失败时只显示“音频已绑定”,不要回退请求裸 generated path。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx覆盖背景音乐和点击音效试听使用签名 URL。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/services/assetReadUrlService.ts、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
法律文档弹窗通过 portal 挂载时要显式带平台主题
- 现象:登录弹窗内点击协议链接打开法律文档时,弹窗可能继承不到
platform-theme--light/dark变量,或者层级低于登录遮罩导致不可见。 - 原因:
UnifiedModal默认通过 portal 挂到document.body,不再处于原页面的主题容器内;登录弹窗自身又使用较高 z-index。 - 处理:法律文档弹窗组件应支持传入
platformTheme,overlay 上显式挂platform-theme platform-theme--*,并使用高于登录遮罩的层级。法律内容必须作为独立面板打开,不要在当前个人页或登录面板下方内联展开。 - 验证:登录页协议链接、个人页法律入口均能打开可滚动
LegalDocumentModal,亮色 / 暗色主题文本和按钮可读。
生成页完成回调不能只依赖异步 React state
- 现象:抓大鹅或拼图点击生成后,进度页已经显示 100% / 生成完成,但没有自动进入试玩或结果页。
- 原因:完成回调用
selectionStageRef.current判断用户是否仍在生成页;如果执行 compile 前只调用setSelectionStage('*-generating'),action 很快返回时 ref 仍可能是旧 stage。 - 处理:进入各玩法生成页时同步写
selectionStageRef.current = '*-generating',再调用setSelectionStage('*-generating')。这不是为渲染服务,而是给同一异步链路里的完成回调提供即时事实。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx覆盖抓大鹅和拼图生成后自动试玩 / 返回结果页。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图最后一步到 100% 但不变绿优先看阶段映射
- 现象:拼图草稿生成跑完所有步骤后,总进度仍停在 98%,最后一步“写入正式草稿”显示 100% 但卡片不变绿,视觉上像还在进行中。
- 原因:进度条总进度刻意保留 98% 作为未收到 action 回包前的安全余量,但最后一步的绿色完成态只看步骤状态;如果时间轴已经跑到
puzzle-select-image末尾却还没收到ready回包,最后一步会一直保持 active。 - 处理:
buildMiniGameDraftGenerationProgress需要在拼图最后一步时,把“预计写入时长已耗尽”单独判为 completed,避免出现“进行中 100%”。 - 验证:
npm test -- src/services/miniGameDraftGenerationProgress.test.ts。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、src/services/miniGameDraftGenerationProgress.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
微信支付回调验签不要用商户私钥
- 现象:微信小程序支付下单能返回
prepay_id,但真实支付通知验签失败,或者本地实现误把商户 API 私钥当作回调验签 key。 - 原因:商户私钥只用于商户请求微信支付和生成小程序
paySign;微信支付通知的Wechatpay-Signature需要使用微信支付平台公钥或平台证书公钥验签,并按通知头里的平台序列号匹配。 - 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:
WECHAT_PAY_PRIVATE_KEY_*用于签名,WECHAT_PAY_PLATFORM_PUBLIC_KEY_*与WECHAT_PAY_PLATFORM_SERIAL_NO用于通知验签,WECHAT_PAY_API_V3_KEY只用于解密通知 resource。微信平台PUBLIC KEYPEM 的 DER 内容是 SPKISubjectPublicKeyInfo,初始化时必须解析并提取其中的 PKCS#1RSAPublicKeyDER 后再交给ring::RSA_PKCS1_2048_8192_SHA256;不能把整段 SPKI DER 直接传给ring。支付成功后只通过通知里的out_trade_no确认本地 pending 订单,并保存transaction_id到profile_recharge_order.provider_transaction_id。 - APIv3 通知成功应答使用 HTTP
204 No Content,不要沿用 V2 XML 成功报文;失败仍返回 4XX/5XX 让微信重试。 - 验证:mock 通知测试只能覆盖本地回调推进;
platform-wechat必须用标准 SPKIPUBLIC KEY和匹配私钥生成真实 RSA-SHA256 签名,覆盖 SPKI 到 PKCS#1 的解析与生产验签 helper。真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
微信支付 JSAPI 下单必须显式带 User-Agent
- 现象:调用
/v3/pay/transactions/jsapi失败,微信返回“Http头缺少Accept或User-Agent”。 - 原因:
reqwest请求即使已设置Accept: application/json,也不会默认附带业务侧User-Agent;微信支付网关会校验这两个头。 - 处理:
api-server的 JSAPI 下单请求统一通过with_wechat_pay_jsapi_headers(...)设置Accept: application/json、Content-Type: application/json和User-Agent: Genarrative-WechatPay/1.0。 - 验证:执行
cargo test -p api-server jsapi_order_request_sets_wechat_required_http_headers --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/wechat_pay.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
容器公开列表压测不要靠继续抬并发吃满 CPU
- 现象:2C / 2G 容器压测公开 gallery list 时,
api-serverCPU 仍有余量,看起来像可以继续提高GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS或 Nginxlimit_conn。 - 原因:当前瓶颈不是 Tokio worker 线程数。
/api/runtime/puzzle/gallery和/api/runtime/custom-world-gallery成功响应后会走全局 route tracking,继续向 SpacetimeDB 写record_tracking_event_and_return;入口并发从 320 抬到 336 / 352 时,SpacetimeDB 内存先逼近896m容器上限,200 请求 p95 变差,429 比例没有改善。 - 处理:2C / 2G 容器模拟里公开 gallery list 暂以
limit_conn=320、GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320作为稳定上限。若要继续提升吞吐,优先减少高频公开 GET 的 tracking 写入、做采样或改成批量/异步聚合;不要单纯放大入口并发。 - 验证:宿主机 k6 打
http://127.0.0.1:18080,PEAK_RPS=1000等价约 2000 HTTP req/s;320 档无 dropped iterations、无 5xx、无 OOM,200 请求request_time p95约 0.292s。336 / 352 档 p95 升到约 0.31s / 0.32s,SpacetimeDB 内存尾部可到约880MiB / 896MiB。 - 关联:
deploy/container/nginx.conf、deploy/container/api-server.env.example、deploy/container/README.md、server-rs/crates/api-server/src/tracking.rs。
tracking outbox 成功入库后删除 sealed 文件
- 现象:普通 route tracking 改为本机 outbox 后,容易误以为入库成功只需要清空文件内容。
- 原因:清空文件会扩大崩溃窗口,进程在 truncate 和确认之间异常退出时可能丢失未确认事件。
- 处理:当前 active NDJSON 达到数量或时间阈值后原子 rename 为 sealed 文件;后台批量 flush sealed 文件,SpacetimeDB 返回成功后直接删除该文件,失败则保留文件等待重试。sealed 文件如果出现无法解析的坏行,重命名为
corrupt-*隔离并记录指标,避免阻塞后续批量入库。该路径是至少一次投递,重复事件由tracking_event.event_id幂等跳过。 - 验证:模拟 SpacetimeDB 不可用时 sealed 文件保留;恢复后批量 procedure 成功,sealed 文件消失,
tracking_event与tracking_daily_stat均更新。 - 关联:
docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、server-rs/crates/api-server/src/tracking.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs。
后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码
- 现象:后台“表查询”查看
profile_recharge_order时,kind和status显示为空数组[],例如充值订单原始行里points_60的类型和状态都不可读。 - 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态
[variant_index, []];后台通用 normalizer 曾把任何[0, value]都当作Option::Some(value)展开,导致[0, []]最终只剩[]。 - 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如
profile_recharge_order.kind映射为points/membership,profile_recharge_order.status映射为pending/paid/failed/closed/refunded/expired。 - 验证:执行
cargo test -p api-server admin_database -- --nocapture,并确认后台详情弹层的raw与表格cells都显示业务字符串。 - 关联:
server-rs/crates/api-server/src/admin.rs、docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md。
后台通用表查询不能先按每页条数截断再筛选
- 现象:后台“表查询”填写关键词或 JSON 条件后查不到确定存在的记录;把“条数”从 100 调到 500 只能偶尔缓解,而且页面没有继续翻页的入口。
- 原因:旧实现先执行
SELECT * FROM <table> LIMIT <limit>,再对这批行做内存过滤;目标记录不在首批结果时永远无法命中,同时响应没有页码、匹配总数或扫描上限状态。 - 处理:用户输入继续不进入通用 SQL。API Server 通过单次
SELECT * ... LIMIT 50001读取哨兵行,最多保留前 50,000 条候选,先过滤,再按后端接收的列名 / 方向对完整候选集稳定排序,最后分页;totalMatched、scannedCount和scanLimitReached都从同一份 SQL 结果计算。请求页码超过实际总页数时钳制到末页,零结果固定为第 1 页。响应统一返回page、totalMatched、scannedCount、scanLimit和scanLimitReached,后台翻页栏固定在视口底部;达到扫描上限时明确提示结果可能不完整。32 MiB 是候选 SQL 响应体硬上限,宽表即使每页条数很小也可能整次拒绝,不返回部分结果。实时写入可能改变相邻请求之间的候选快照,精确审计使用专用业务查询。 - 验证:执行
cargo test -p api-server admin_database -- --nocapture,覆盖第 101 行才命中、完整候选集排序后分页、哨兵截断、越界页码和响应体硬上限;前端测试覆盖下一页沿用已应用条件、后端排序参数 / 返回结果与扫描警告,并运行后台类型检查。 - 关联:
server-rs/crates/api-server/src/admin.rs、server-rs/crates/shared-contracts/src/admin.rs、apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx。
充值订单过期补偿不要放进外部生成 worker
- 现象:外部生成 worker/controller 扩容后,微信充值过期查单和关单流量也被同步放大;排查时还会误去外部生成 worker 日志里找支付过期任务。
- 原因:支付过期是账户资金链路,不是外部内容生成队列;旧实现把充值过期轮询 worker 挂在通用后台任务启动函数里,非 HTTP 角色也会启动。
- 处理:充值订单过期由 SpacetimeDB 原生
profile_recharge_order_expiration_timer到点把pending改为expired,只有 HTTPapi-server订阅活跃 timer 表的删除事件,按order_id重新读取订单并仅对expired查微信补偿;支付或主动关闭导致的删除信号会被状态判断忽略,断线窗口由未检查过期订单 catch-up 补齐。未支付终态本地保持expired,不要再改写成closed;微信成功支付通知或补偿查单仍可把Expired -> Paid入账。 - 验证:确认
GENARRATIVE_PROCESS_ROLE=external-generation-worker/external-generation-controller不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生expired,HTTP api-server listener 记录expiration_checked_at或补入账。 - 关联:
server-rs/crates/api-server/src/profile_recharge_expiration_listener.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
充值订单状态枚举不能用字符串 SQL 字面量订阅
- 现象:API 已 ready,但日志每 5 秒出现
profile recharge expiration listener failed to subscribe,并提示pending不能解析为profile_recharge_order.status的枚举类型;scheduled reducer 仍会把订单改成expired,但微信查单补偿监听没有运行。 - 原因:SpacetimeDB 2.6 不会把订阅 SQL 中的
'pending'/'expired'字符串自动转换为生成绑定的 sum-type enum;两个按状态过滤的订阅都在应用阶段失败。 - 处理:不要改成订阅完整
profile_recharge_order历史表。后端订阅只保留活跃五分钟定时器的profile_recharge_order_expiration_timer,监听 timer 删除后按order_id通过 procedure 读取订单,只处理当前状态为expired的记录;支付 / 关闭信号会被忽略,断线窗口继续由未检查过期订单 catch-up 补齐。这样既不依赖不受支持的枚举 SQL,也不会把充值历史常驻 API 客户端缓存。 - 验证:运行
cargo test -p spacetime-client profile_recharge_expiration --manifest-path server-rs/Cargo.toml,发布后确认 API 日志不再出现订阅解析错误,并用真实 pending 订单验证 scheduled reducer 过期后写入expiration_checked_at。
微信 Native 已入账但二维码弹窗不关闭
- 现象:微信支付回调已经返回
204,本地充值订单为paid且泥点已到账,但网页仍停留在“微信扫码支付”,必须点击“我已支付”才刷新。 - 原因:通用页面恢复确认逻辑在存在
nativeWechatPayment时直接跳过,Native 分支创建二维码后也没有订阅订单 SSE,因此服务端回调发布的订单更新没有前端消费者。 - 处理:Native 二维码出现后立即调用
watchWechatRpgProfileRechargeOrder订阅当前订单;收到终态后更新充值中心、关闭二维码、清理 pending ref、刷新全局余额并只展示一次结果。SSE 超时或暂时失败时在二维码过期前重连,手动确认与 SSE 并发时以 pending order ref 保证只有首个终态生效。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx覆盖不点击“我已支付”也会在 SSE 返回paid后自动关闭;再运行根级npm run typecheck、npm run check:encoding和git diff --check。 - 关联:
src/components/platform-entry/usePlatformProfileCenterController.ts、src/services/rpg-entry/rpgProfileClient.ts、server-rs/crates/api-server/src/runtime_profile.rs。
商户平台退款登记不要混淆 refund_id 与 out_refund_no
- 现象:在“登记商户平台退款”里填写
50000000000000000000000000000一类微信退款单号后提示找不到退款。 - 原因:该编号是微信侧
refund_id;V3 单笔退款查询路径只接受商户退款单号out_refund_no。把refund_id放进路径不会自动转换,退款查单会返回RESOURCE_NOT_EXISTS。不过微信没有承诺refund_id固定为50开头的 29 位数字,合法out_refund_no也可能是纯数字,因此形状判断不能代替真实查单。 - 处理:登记表单明确标注
out_refund_no,所有满足官方字符和长度约束的输入都交给服务端真实查询;只有查询确认不存在后,才把50开头的 29 位纯数字作为“疑似 refund*id”给出定向提示。只有refund_id时等待退款回调或 T+1 退款账单建立映射;不要调用异常退款申请接口冒充查询。out_refund_no字符校验须覆盖官方允许的数字、大小写字母和* - | \* @。 - 验证:后台页面测试断言疑似编号仍交给服务端;平台适配器测试锁定
RESOURCE_NOT_EXISTS映射和@字符,并确认真正的out_refund_no仍调用GET /v3/refund/domestic/refunds/{out_refund_no}。 - 关联:
apps/admin-web/src/pages/AdminRechargeOrderPage.tsx、server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/platform-wechat/src/pay.rs。
微信支付查单的 REFUND 不等于已经全额退款
- 现象:一笔 6 元充值在商户平台成功退 3 元并登记
out_refund_no后,本地显示累计已退 3 元、剩余可退 3 元,但后台再次预检仍显示“未核验 / REFUND”并禁止退款。 - 原因:微信支付订单查单的
trade_state=REFUND只说明该支付订单发生过退款,不携带累计退款明细,也不表示已经全额退款。若后台把verified硬编码为trade_state == SUCCESS,任何已成功部分退款的订单都会永久失去继续退款能力;反过来,仅看到REFUND就直接放行又可能漏掉未登记的商户平台退款。 - 处理:预检先查支付订单并校验商户订单号、支付单号和总金额,再主动刷新全部已知
out_refund_no并重读本地退款 settlement。SUCCESS可继续预检;REFUND仅在本地累计成功退款大于 0 且小于订单总额,并且没有PROCESSING / ABNORMAL退款、活动 hold、退款欠账或人工冻结时,允许继续退本地剩余额度。没有本地成功退款能解释REFUND时使用独立原因码阻止并要求登记或对账,不能冒充“订单未支付”。 - 验证:后端策略测试覆盖
SUCCESS + 0/600、REFUND + 0/600、REFUND + 300/600、REFUND + 600/600;后台页面测试覆盖已核验 / REFUND时剩余额度可提交。真实联调核对累计退款、已追回泥点、活动占用和欠账均与退款明细一致。 - 关联:
server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、apps/admin-web/src/pages/AdminRechargeOrderPage.tsx。
抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
- 现象:草稿页预览模型失败并报
GL_INVALID_ENUM: Invalid cap.,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。 - 原因:历史结果页手动
重新生成会把 Hyper3D/Rodin 的外部 CDN 下载链接直接保存到generatedItemAssets[].modelSrc,同时modelObjectKey为空。外部链接可能过期、跨域、返回 HTML 错误页或非 GLB 内容;前端预览和运行态不能把它当作稳定私有资产。 - 处理:该问题只适用于旧数据。结果页发现
status = model_ready、modelSrc = https://...且无modelObjectKey时,可调用POST /api/creation/match3d/works/{profileId}/generated-models做一次性转存;新草稿和批量新增不得继续生成或依赖 GLB。若历史半修复数据同时保留外部modelSrc和平台modelObjectKey,旧模型预览读取层优先用modelObjectKey。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx、npm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsx、npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx、cargo test -p api-server match3d_model_download --manifest-path server-rs\Cargo.toml,并检查修复后响应中的generatedItemAssets[].modelObjectKey不为空。 - 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、src/components/match3d-result/Match3DModelPreview.tsx、src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅难度配置的物品种类和消除次数必须分离
- 现象:历史草稿选择标准 / 硬核难度后,系统可能把
clearCount当成局内物品种类数量,导致标准需要 12 种、硬核需要 20/21 种;或者把第 11 到 20 个物品持久化为第 11 到 20 行,触发“系列素材图集持久化的行列索引必须落在 n*n 范围内”。 - 原因:旧运行态把消除次数和类型数量绑在一起,结果页文案又同时展示“素材图片 / 局内类型”,导致前端、发布校验和 run start 口径不一致。
- 处理:生成和持久化固定使用 20 个物品素材;运行态物品种类口径为轻松 3、标准 9、进阶 15、硬核 20,历史
clearCount=20且难度为硬核的运行态仍可升为 21 组三消,但类型池不超过 20。10*10 sheet 每行两种物品、每种五个形态,持久化行列为row = itemIndex / 2 + 1、col = itemIndex % 2 * 5 + viewIndex + 1。发布前按image_ready且有imageViews[]或imageSrc/imageObjectKey的生成素材数量阻断不足难度;试玩不阻断,但通过itemTypeCountOverride自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx、cargo test -p module-match3d --manifest-path server-rs\Cargo.toml,涉及发布 reducer 时补跑cargo test -p spacetime-module match3d --manifest-path server-rs\Cargo.toml。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/services/match3d-runtime/match3dRuntimeClient.ts、server-rs/crates/module-match3d/src/application.rs、server-rs/crates/spacetime-module/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅标签清洗不要把 3D素材 当编号剥掉
- 现象:AI 或兜底生成的
3D素材标签在后端规范化后变成D素材。 - 原因:标签清洗在去掉编号列表前缀后,又无条件剥离开头数字和标点,把合法标签中的
3D当成列表编号处理。 - 处理:只移除明确的编号列表前缀,例如
1. 标签、1、标签、1) 标签;不要对普通标签开头数字做二次剥离。 - 验证:
cargo test -p api-server match3d_tag_normalization --manifest-path server-rs/Cargo.toml,并保留normalize_match3d_tag("3D素材") == "3D素材"的单测。 - 关联:
server-rs/crates/api-server/src/match3d.rs。
抓大鹅物品切图白边或绿幕残留先查后端透明化
- 现象:抓大鹅生成的物品视角图裁剪后仍带白边,或者整块纯绿色绿幕背景没有被透明化,运行态看到绿色方块。
- 原因:素材 sheet 可能是“每格内部绿幕、整张图外圈近白底”,内部绿幕不一定连通到 sheet 外边缘;旧 flood fill 只从外边缘找背景会漏掉这种绿幕块。白底抗锯齿如果不纳入抠像和边缘去污染,也会随裁剪输出成一圈白边。即使顺序已是先整张 sheet 去绿再裁剪,较厚的半透明或混色软绿边仍可能低于高置信绿幕阈值,被当作前景带进独立 PNG。
- 处理:
api-server的slice_match3d_material_sheet必须先在整张 sheet 上做透明背景后处理:外边缘连通绿幕/近白底清 alpha,非连通但高置信纯绿块也清 alpha,沿整张 sheet 透明背景继续吃掉软绿边,边缘近白和绿幕抗锯齿做透明或去污染;同时保护不够纯的绿色主体像素。不要改成先裁剪单格再去绿。 - 验证:
cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml覆盖非连通绿幕、白边、贴边主体保留和固定10*10切图;cargo test -p api-server match3d_spritesheet_green_screen_postprocess_turns_background_transparent --manifest-path server-rs\Cargo.toml覆盖完整 spritesheet 上传前绿幕透明化。 - 关联:
server-rs/crates/api-server/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅物品详情大方格只做单张大图查看
- 现象:结果页
素材配置 > 物品打开详情后,上方大方格仍显示横向五图带、焦点内框或小缩略图边框,物品本体看起来偏小且像带着素材自带边框。 - 原因:旧预览把上方区域当作横向视角带,当前焦点只是带内缩略图的一张,视觉上不是“详细查看物品形象”的大图。
- 处理:上方方格只渲染当前选中的单张大图,使用
object-contain和少量内边距放大查看;底部缩略图栏负责切换视角,缩略图可以保留选中态边框,但上方大图不渲染焦点内框或缩略图容器边框。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx覆盖上方大图、底部缩略图和视角切换。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
草稿页卡片有真实素材但仍显示黑卡先查摘要字段
- 现象:草稿页拼图卡片没有关卡图背景,抓大鹅卡片没有背景图或物品图背景,甚至兜底视觉也退回黑色面板。
- 原因:拼图列表摘要若不下发
levels,前端拿不到关卡coverImageSrc/ 候选图;抓大鹅列表摘要若只提供公开 URL、不保留generatedBackgroundAsset或generatedItemAssets中的 object key,前端无法换签读取私有生成图。卡片封面组件如果自带暗色默认背景,也会让兜底失败时看起来仍是黑卡。 - 处理:拼图
map_puzzle_work_summary_response必须保留levels;草稿页优先用关卡coverImageSrc,再用候选图。抓大鹅货架封面解析必须读取backgroundImageObjectKey、generatedBackgroundAsset.imageObjectKey/containerImageObjectKey、generatedItemAssets[].imageObjectKey和imageViews[].imageObjectKey。图片渲染统一交给ResolvedAssetImage换签,并给卡片传入玩法参考图与暖色底兜底。 - 验证:执行
npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/hooks/useResolvedAssetReadUrl.test.tsx、cargo test -p api-server puzzle_work_summary_response_keeps_levels_for_shelf_cover --manifest-path server-rs\Cargo.toml、npm run typecheck。 - 关联:
src/components/custom-world-home/creationWorkShelf.ts、src/components/CustomWorldCoverArtwork.tsx、server-rs/crates/api-server/src/puzzle.rs、docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md。
用户标签不要直接外显,SpacetimeDB Vec 字段不要写 default 宏
- 现象:给
user_account.user_tags或邀请码独立标签列写#[default(Vec::<String>::new())]时,SpacetimeDB WASM 构建报destructor of Vec<String> cannot be evaluated at compile-time。 - 原因:SpacetimeDB 的 table default 宏会走编译期常量求值,不能直接使用有析构逻辑的堆分配类型默认值。
- 处理:
user_account.user_tags使用Option<Vec<String>>+#[default(None::<Vec<String>>)]表达数据库默认空,业务层统一把None归一化为空数组;邀请码授予标签复用metadata_json.userTags存储和解析,不再新增独立 Vec 列。用户标签原始值不得进入登录态、个人资料等通用响应,只能在明确业务白名单里投影,例如拼图排行榜visibleTags首版仅允许北科。 - 验证:
npm run spacetime:generate -- --rust-only能通过;user_account旧迁移 JSON 缺字段时能导入,profile_invite_code缺metadata_json时按{}兼容。 - 关联:
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、server-rs/crates/spacetime-module/src/。
公开作品详情深链找不到作品不能停在空详情页
- 现象:直接访问
/works/detail?work=PZ-...,作品不存在或已下架时会弹出“作品不存在或已下架,将返回首页。”;关闭提示后仍可能停在大白屏。 - 原因:旧恢复逻辑只覆盖
/runtime/...,没有覆盖/works/detail。同时selectionStage === 'work-detail'且selectedPublicWorkDetail === null时没有兜底渲染,详情数据为空就只剩空页面。 - 处理:公开详情失效统一走
resolveWorkNotFoundRecoveryAction(...),覆盖/works/detail、/gallery/puzzle/detail和/gallery/visual-novel/detail;搜索失败和拼图详情 404 分支清理详情/运行态临时状态并回首页;work-detail空数据阶段显示轻量读取态,避免异步间隙白屏。 - 验证:
npm run test -- src/routing/runtimeNotFoundRecovery.test.ts、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"。 - 关联:
docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md、src/routing/runtimeNotFoundRecovery.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
拼图 UI 背景只有 objectKey 时不要回退默认 UI
- 现象:拼图草稿页、试玩和正式运行态都显示默认 UI,或者只在结果页看到生成图,进入试玩后又回到默认背景。
- 原因:
uiBackgroundImageSrc可能为空而真实生成结果只写了uiBackgroundImageObjectKey;如果前端和运行态只读src,或者本地试玩 / 正式 run 没把objectKey一起传递,就会丢掉已有背景。 - 处理:统一通过一个解析入口把
uiBackgroundImageSrc || uiBackgroundImageObjectKey归一到可展示路径;本地试玩和正式运行态都要保留uiBackgroundImageObjectKey,并在uiBackgroundImageSrc为空时换签读取。 - 验证:结果页 UI Tab、
startLocalPuzzleRun和PuzzleRuntimeShell都应在仅有objectKey时显示生成背景,不再回落默认 UI。 - 关联:
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts、src/components/puzzle-result/PuzzleResultView.tsx、src/services/puzzle-runtime/puzzleLocalRuntime.ts、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、server-rs/crates/module-puzzle/src/application.rs。
拼图 UI 背景提示词或作品元信息异常先查首关命名契约
- 现象:拼图草稿生成完成后,第一关名称或作品名称变成
levelNam/levelName这类字段名片段,或素材配置 > UI里显示的UI背景提示词像前端或后端模板拼接,而不是 AI 生成的视觉提示词。 - 原因:首关命名 LLM 旧契约只返回
levelName,自动 UI 背景阶段只能用作品名、作品描述、关卡描述和标签拼接确定性兜底提示词;如果模型返回截断 JSON,解析层还可能把levelNam这类字段名片段当作普通英文关卡名归一化通过。 - 处理:首关命名 LLM 契约必须同时返回
{"levelName":"...","workDescription":"...","workTags":["..."],"uiBackgroundPrompt":"..."};解析层必须拒绝levelNam、levelName、workDescription、workTags、uiBackgroundPrompt等字段名片段作为关卡名。草稿自动 UI 背景生成优先使用该 AI 提示词,作品描述和 6 个作品标签默认填入草稿;视觉精修请求若返回新提示词或作品元信息则覆盖文本请求结果,否则保留文本请求结果。前端文本框只展示已保存的uiBackgroundPrompt或用户编辑值,字段为空时不展示本地兜底模板。 - 验证:执行
cargo test -p api-server puzzle_level_naming_parser --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_first_level_name --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_initial --manifest-path server-rs\Cargo.toml、npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx。 - 关联:
server-rs/crates/api-server/src/prompt/puzzle/level_name.rs、server-rs/crates/api-server/src/puzzle.rs、src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
拼图 / 抓大鹅 UI 背景重生成报 No such procedure 先查 SpacetimeDB 版本漂移
- 现象:拼图或抓大鹅结果页点击
重新生成UI 背景时报No such procedure,常见位置是泥点预扣、save_puzzle_ui_background或 Match3D 草稿写回。 - 原因:
api-server和spacetime-client已按新 bindings 调用 procedure,但目标 SpacetimeDB 数据库仍运行旧 wasm,尚未导出钱包扣退费、拼图 UI 背景保存或 Match3D 写回相关 procedure。 - 处理:临时容错是把这类
No such procedure当作后端版本漂移:泥点预扣阶段跳过扣费,图片已经生成但保存失败时返回本次内存快照 / 内存 profile,避免草稿页直接报错。长期修复仍是发布最新spacetime-module、重新生成 bindings,并用spacetime describe或定向 smoke 确认 procedure 已导出。 - 验证:
cargo test -p api-server asset_operation_billing_skips_spacetime_connectivity_errors --manifest-path server-rs\Cargo.toml、cargo test -p api-server match3d_fallback_work_profile_keeps_generated_background_asset --manifest-path server-rs\Cargo.toml、npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/asset_billing.rs、server-rs/crates/api-server/src/match3d.rs、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图合并块拖起后原位置出现红色块先查选中态泄漏
- 现象:拼图运行态中,多个拼图片合并后拖起整体块,原位置会露出一块粉红 / 红色底色。
- 原因:合并块拖拽的可见层来自
mergedGroups绝对定位整体层,但pointerdown会同步写入selectedPieceId;若棋盘格里的底层单块 DOM 先匹配选中态,再匹配合并态,整体层移开后就会露出单块选中填充色。 - 处理:合并格底层 DOM 只作为透明定位占位,
isSelected必须排除isMerged;合并格样式优先级高于单块选中态。 - 验证:运行
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "拖拽合并大块时底层单格不显示选中色块",并确认合并块拖拽时底层[data-piece-id]仍为puzzle-runtime-piece--merged。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
推荐页嵌入拼图通关结算不要放在运行态内部 absolute 层
- 现象:推荐页里玩拼图通关后,结算面板只显示上半部分,排行榜或下一关按钮被截断。
- 原因:推荐页把运行态放在滑动作品卡的视觉区内,
platform-recommend-swipe-page、platform-recommend-swipe-card__visual和platform-recommend-runtime-viewport都是overflow: hidden;拼图通关结算如果仍是运行态内部absolute inset-0弹层,就只能在半屏卡片区域里显示。 - 处理:
PuzzleRuntimeShell在embedded模式下把通关结算层通过 portal 挂到document.body,使用puzzle-runtime-modal-overlay--fixed页面级 fixed 浮层;非嵌入态继续使用运行态内部覆盖层。 - 验证:运行
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "推荐页嵌入拼图通关结算使用页面级浮层避免卡片裁剪",确认弹层不再位于.platform-recommend-runtime-viewport内。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/index.css、src/components/rpg-entry/RpgEntryHomeView.tsx。
拼图历史图片列表不要把账号归属当图片名
- 现象:拼图创作页或结果页打开“选择历史图片”后,历史列表显示
账号 user-1之类归属文案而不是图片名;1713686400.000000Z这类时间显示为未知;选中后预览或生成参考图可能被怀疑不可用。 - 原因:
/api/assets/history?kind=puzzle_cover_image返回的ownerLabel是资产归属账号,不是图片标题;createdAt可能是 SpacetimeDB / shared-kernel 秒级时间字符串,不能只用浏览器new Date(value)解析。历史图的imageSrc是/generated-*私有兼容路径,浏览器预览必须换签。 - 处理:前端标题和选中标签从
imageSrc路径末尾推导,例如image.png;时间解析兼容 ISO 与1713686400.000000Z;创作页主图、历史列表图和结果页参考图继续用ResolvedAssetImage,提交给后端时仍保留原始imageSrc。 - 验证:
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx,并执行npm run check:encoding。 - 关联:
src/services/puzzle-works/puzzleHistoryAsset.ts、src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.tsx、docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md。
拼图历史图关闭 AI 重绘不要强制 Data URL
- 现象:拼图创作页从历史生成图片中选择主图,再关闭 AI 重绘生成草稿时,后端报“上传图必须是图片 Data URL”。
- 原因:历史图
imageSrc是/generated-puzzle-assets/...私有兼容路径;AI 重绘开启时后端参考图分支会解析该路径,但关闭 AI 重绘的“直用上传图”分支旧实现只调用parse_puzzle_image_data_url。 - 处理:关闭 AI 重绘时也复用拼图参考图解析入口,允许 Data URL 与
/generated-*历史路径统一转成PuzzleDownloadedImage后持久化;前端不需要下载历史图再转 base64。 - 验证:
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml、npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/vector_engine.rs、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx。
拼图结果页局部生图不要污染草稿生成态
- 现象:拼图草稿已经生成完成后,在结果页重新生成关卡图片或追加关卡生成图片,草稿页仍显示整卡“生成中”,点击草稿会回到生成过程页,无法查看已有结果;关卡图片生成中还会禁用“新增关卡”和其它关卡详情编辑。
- 原因:结果页局部 action 复用了全局
isPuzzleBusy/ 持久化generationStatus=generating语义,作品架没有区分“初始草稿不可查看”和“已有结果上的局部关卡生成”。 - 处理:作品架只在拼图没有可用封面、首关候选图或任一可查看关卡时才把
generationStatus=generating解释为初始草稿生成;结果页关卡图走 background action,不设置全局 busy,只标记对应关卡局部生成进度;SpacetimeDB/API mapper 读写时把已有图片但状态仍是generating的历史关卡归一为ready。 - 验证:
npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle --manifest-path server-rs\Cargo.toml。 - 关联:
src/components/custom-world-home/creationWorkShelf.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle/mappers.rs、server-rs/crates/spacetime-module/src/puzzle.rs。
2026-05-22 补充:结果页关卡详情的“关卡测试”不能把单关 draft 传给父级再调用 updatePuzzleWork。updatePuzzleWork 会同步 puzzle_work_profile.levels_json 和 source session 草稿,单关快照会把整份多关卡草稿覆盖成一个关卡,退出重进后只剩最后测试的关卡且序号表现为第一关。修复口径是 PuzzleResultView 始终传完整 syncedDraft,额外用 { levelId } 指定起始关卡;父级持久化完整 levels 后调用 startLocalPuzzleRun(item, levelId)。
2026-06-18 补充:结果页点击“新增关卡”只是在本地打开一个空白占位关卡,不应立刻进入自动保存。空白占位如果被写入 /api/runtime/puzzle/works/{profile_id},在作品 profile 投影尚未稳定存在时会触发 update_puzzle_work 404,并且后续 session/draft 回读可能把当前详情弹窗关闭。修复口径是自动保存比较和 payload 过滤掉“后端基线中不存在且完全空白”的本地关卡;用户填写名称、描述、参考图或开始生成后再保存。mergeDraftEditStateWithIncomingState(...) 还要保留本地空白占位,避免 incoming draft 刷新时移除正在编辑的弹窗。
2026-06-18 补充:改造流的 creative_agent 草稿写回会用 puzzle-session-* 派生出的 puzzle-profile-* 调用 update_puzzle_work;如果前置 create_puzzle_agent_session 已写入 puzzle_agent_session,但派生的 puzzle_work_profile 草稿投影缺失,写回会报“拼图作品不存在”。首图生成或结果页保存也可能踩到同一缺口。修复口径是在 SpacetimeDB update_puzzle_work_tx 里只对稳定 puzzle-profile-* 反推同源 puzzle-session-*,确认 owner 匹配、session 未发布且有 draft 后恢复 draft profile,再继续更新;不要在前端重试或凭空创建任意 profile,也不要恢复已发布 session。
拼图上传图关闭 AI 重绘不要走首图生图
- 现象:用户在拼图入口页或结果页关卡详情上传图片并关闭 AI 重绘后,生成页仍显示“生成拼图首图”,或者后端仍调用
generate_puzzle_image_candidates生成第一张 1:1 候选图。 - 原因:上传图直用路径应把 Data URL 或
/generated-*历史图解析后持久化为sourceType=uploaded的正式候选,再继续生成 9:16 关卡画面、UI spritesheet 和纯背景;如果只把aiRedraw=false当作“不参考图片生成”,就会误走首图生成。 - 处理:入口页用 payload 的
aiRedraw写入生成页 metadata,puzzleAiRedraw=false时进度跳过生成拼图首图;后端compile_puzzle_draft和结果页generate_puzzle_images都在aiRedraw=false && referenceImageSrc 非空时走上传图直用候选。结果页关卡详情必须复用CreativeImageInputPanel,不要把正式图当成可重绘参考图;本次上传或历史选择的图才显示 AI 重绘开关并可删除。 - 验证:
npm run test -- src/services/miniGameDraftGenerationProgress.test.ts src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/generation.rs。
Jenkins 数据库导入导出脚本先补 Node 工具链 PATH
- 现象:
Genarrative-Database-Import或Genarrative-Database-Export运行到迁移脚本时,bash报node: command not found,常见在日志里表现为某个sh块内第 61 行直接调用node失败。 - 原因:Jenkins 的非交互 shell 没有自动加载用户的 nvm/profile,数据库导入导出脚本又在 shell 里直接执行
node scripts/spacetime-*.mjs,因此只要 Jenkins agent 没把 Node 的 bin 目录放进 PATH,就会在迁移开始前失败。 - 处理:导入 / 导出流水线在调用迁移脚本前先
source scripts/jenkins-prepare-toolchain-env.sh;该脚本会把GENARRATIVE_JENKINS_TOOL_PATHS、/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin、/var/lib/jenkins/.cargo/bin、/var/lib/jenkins/.local/bin和系统 PATH 前缀统一补齐,并在缺少node时尽早报错。 - 验证:重新跑
Genarrative-Database-Import或Genarrative-Database-Export,日志应先打印jenkins-toolchain的node=...解析结果,而不是在迁移中途报node: command not found。 - 关联:
scripts/jenkins-prepare-toolchain-env.sh、jenkins/Jenkinsfile.production-database-import、jenkins/Jenkinsfile.production-database-export、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Runtime bootstrap secret 原文不能进入 WASM 或发布归档
- 现象:下载 Jenkins Stdb artifact 或检查
spacetime_module.wasm能找到原始 bootstrap secret;Stdb Build / Publish 配了不同 credential,或 Publish 使用的 Secret File 摘要与 release manifest 不一致却仍继续发布;或者 module 已发布、/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt也已更新,但模型定价首次初始化、队列 claim 或钱包调用仍报 identity 未授权。 - 原因:把原文作为 Rust 编译环境变量会进入可下载 WASM;把
migration-bootstrap-secret.txt归档会把构建凭据变成长生命周期 artifact。只把摘要编进 WASM、却不在 release manifest 绑定摘要并让 Publish 重算核对,仍可能把另一份 secret 配给已构建 module。另一方面,AppConfig只在 api-server / worker 进程启动时读取直传值或 FILE,覆盖文件不会更新已运行进程;Full Build 又先发 Stdb、后发 API,不能等待后续 API deploy 才补旧 env。 - 处理:原始 bootstrap secret 固定为 64 位十六进制。WASM 编译只接受
GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256,模块对 procedure 入参原文重新计算 SHA-256 并做常量时间比较。生产 Jenkins Build / Publish 必须使用完全相同的 Secret File credential ID:Build 只计算摘要,WASM、artifact 和copyArtifacts不含原文,Stdb release manifest 记录migration_bootstrap_secret_sha256;Publish 重新读取同一 Secret File、校验 64 位十六进制并重算摘要,与 manifest 强制匹配后才把临时文件路径交给production-stdb-publish.sh。module 发布后安装固定 runtime 文件为root:genarrative 0440、目录root:genarrative 0750,补齐 API / worker env,再在维护模式内重启发布前 active 的 API、controller 和 worker,并执行 API/healthz门禁。人工构建自动生成的原文只放 gitignoredserver-rs/.spacetimedb/build-secrets/<version>.txt,目录0700、文件0600;本地 dev 的 API token 和按 server/database 作用域 secret 也分别持久化为0600文件。日志不得cat或插值打印明文。 - 身份轮换:bootstrap secret 只允许空表首次授权,不能重复接管既有 writer。migration operator 与 runtime writer 必须互斥:operator 不能成为 writer,当前 writer 不能被授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。生产 token 确需轮换时,使用
scripts/deploy/production-runtime-writer-identity-rotate.mjs,由当前已授权 migration operator 登录态双录新 writer identity、填写操作人和原因;procedure 必须拒绝把新 writer 设为当前 writer 或任一 migration operator,并写editor_generation_runtime_identity_rotation审计。成功后先核对审计,再切换 token,不得靠重启 API 隐式改 writer。 - 验证:运行相关部署脚本
bash -n、node --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs scripts/deploy/production-runtime-writer-identity-rotate.mjs、npm run check:production-ops,扫描 artifact 清单和 diff,确认 Build / Publish credential ID 一致、manifest 摘要与 Publish Secret File 匹配,并确认不存在migration-bootstrap-secret.txt、原文编译环境变量、cat或生产 env 明文键,同时验证 64 位十六进制规则及手工 secret / 本地 token 文件权限。 - 关联:
server-rs/crates/spacetime-module/src/migration.rs、scripts/dev.mjs、scripts/build-production-release.sh、scripts/deploy/production-stdb-publish.sh、scripts/deploy/production-runtime-writer-identity-rotate.mjs、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-stdb-module-publish。
Windows Jenkins powershell step 在 Stdb module 构建里曾触发 CreateProcess error=5
- 当前状态:已废弃。
Genarrative-Stdb-Module-Build已切到 Linux agent,不再执行 Windows PowerShell 流程。 - 现象:
Genarrative-Stdb-Module-Build在 Windows Jenkins 节点上报java.io.IOException: Cannot run program "powershell" (in directory "C:\\Users\\DSK\\.jenkins-local\\workspace\\Genarrative-Stdb-Module-Build"): CreateProcess error=5, 拒绝访问。;日志里能看到durable-task已写出powershellWrapper.ps1,但在真正启动裸powershell子进程时失败。 - 原因:Jenkins durable-task 的
powershellstep 依赖一个隐式命令解析/启动路径,在这台 Windows 本地 Jenkins 环境里会被拒绝。powershell.exe本体和 workspace ACL 都是正常的,问题出在 Jenkins step 的启动方式,而不是 PowerShell 脚本内容。修复后若日志能打印[jenkins-powershell] exe:,但随后仅报拒绝访问/script returned exit code 5,通常已经不是 PowerShell 启动失败,而是 Checkout 脚本内部命令在 Windows workspace 里触发权限拒绝。若.jenkins-*.ps1里中文throw '[stdb-build] ...'报MissingArrayIndexExpression,则是 Windows PowerShell 5.1 用-File解析无 BOM UTF-8 脚本时按本地 ANSI 误解码。 - 处理:把
jenkins/Jenkinsfile.production-stdb-module-build的Checkout和Build Stdb Module两处powershellstep 收口成runWindowsPowerShell(...)helper,先用writeFile写出临时.ps1,再用显式powershell.exe把脚本重写成 UTF-8 with BOM,最后通过%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...执行。这个 helper 写在 Groovy GString 里时,PowerShell 的$path/$text/$true必须写成\$path/\$text/\$true,否则 Jenkinsfile 会在 Groovy 编译阶段报unexpected token: true。Checkout 阶段优先复用 Jenkins GitSCM 已完成的工作区结果;COMMIT_HASH为空或已经等于当前HEAD时不再重复git fetch/git checkout/git clean,只有确实要切到另一个指定 commit 时才补 fetch、归属校验和 checkout。 - 验证:检查 Jenkins build log 中是否出现
[jenkins-powershell] user:和[jenkins-powershell] exe:,以及[stdb-checkout] current HEAD:。上游 Full Build 传下来的COMMIT_HASH若已等于当前 GitSCM checkout,日志应显示requested commit already matches Jenkins GitSCM checkout并继续进入构建阶段;同时确认builds/<n>/log不再停在PipelineNodeTreeScanner... Cannot run program "powershell"或 Checkout 内部 exit code 5。 - 关联:
jenkins/Jenkinsfile.production-stdb-module-build、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision Windows 下载 helper 不要原地重写临时 ps1
- 现象:
Genarrative-Server-Provision的 Windows 下载阶段已经打印了[jenkins-powershell] user:和[jenkins-powershell] exe:,但在.ps1原地 BOM 重写前后仍然返回exit code 5/拒绝访问,且下载目录还没创建。 - 原因:Jenkins
writeFile生成的临时.ps1正被同一个 workspace 里的 PowerShell 进程马上重写成 BOM 文件,这个原地改写在本地 Windows Jenkins 环境里比直接脚本执行更容易碰到 workspace 占用或 ACL 拒绝。对这条流水线来说,BOM 不是必须的执行条件。 - 处理:
runWindowsPowerShell(...)改成先writeFile,再由显式powershell.exe读取脚本文本并用ScriptBlock::Create(...)直接在内存中执行,不再对同一个.ps1做 BOM 重写。Windows 下载脚本里先把PROVISION_DOWNLOADS_DIR归一到 workspace 绝对路径,并补Windows workspace/download dir/已创建下载目录三段日志,方便区分是路径问题还是下载问题。 - 验证:Jenkins log 应先出现
[jenkins-powershell] workspace:、[jenkins-powershell] loaded bytes:,再出现[prepare-provision-downloads] Windows workspace:和[prepare-provision-downloads] 已创建下载目录:;如果下载 URL 故意指到不可达地址,应该只在curl 下载失败处结束,而不是卡在 BOM 重写前。 - 关联:
jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
SpacetimeDB update installer 不要按带 host 后缀的下载文件名执行
- 现象:Server-Provision 目标机阶段已经显示“使用已下载的 SpacetimeDB Linux update installer”,随后报
Error: unexpected argument '-y' found或前置unknown command name for spacetimedb-update multicall binary。 - 原因:
spacetimedb-update-*不是当前离线交付的最终形态,GitHub release 页面真正可比较的缓存对象是spacetime-x86_64-unknown-linux-gnu.tar.gz这种 release tarball;GitHub release asset API 暴露的是digest/ SHA256,不是 MD5。 - 处理:Windows 下载阶段应直接缓存 release tarball 和
otelcol-contrib_0.151.0_linux_amd64.tar.gz,目标机scripts/prepare-server-provision-tools.sh只解压本地 tarball 生成bin/current/spacetimedb-cli与bin/current/spacetimedb-standalone,不要再把 update installer 当成最终离线包执行。 - 验证:Jenkins 目标机日志不再出现
unexpected argument '-y'、unknown command name for spacetimedb-update multicall binary,后续应继续检查bin/current/spacetimedb-cli和bin/current/spacetimedb-standalone是否生成。 - 关联:
scripts/prepare-server-provision-tools.sh、jenkins/Jenkinsfile.production-server-provision。
清库重建后先查 schema 兼容再重启
- 现象:
npm run dev -- --clear-database --no-interactive之后,api-server 仍在GET /api/creation-entry/config或订阅恢复阶段报No such procedure/ schema guard 失败。 - 原因:本地重建只会重发当前
spacetime-module,不会自动修正旧迁移 JSON 的字段兼容;如果migration.rs没把新字段补成None/ 默认值,清库后重建仍会卡在 schema 同步。 - 处理:先让
server-rs/crates/spacetime-module/src/migration.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md和生成绑定对齐,再执行清库重建。 - 验证:
npm run check:spacetime-schema先通过,再重启npm run dev -- --clear-database --no-interactive,最后检查/v1/ping、/healthz和GET /api/creation-entry/config。 - 关联:
server-rs/crates/spacetime-module/src/migration.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、scripts/dev.mjs。
QQ 浏览器发现页推荐封面全不显示先查 aspect-ratio 兜底
- 现象:发现页的“推荐”子频道作品卡标题、作者和数据正常,但所有封面图不显示,常见于 QQ 浏览器 / X5 等旧移动内核。
- 原因:公开作品卡封面内部图片是绝对铺满,容器原本主要依赖 Tailwind
aspect-video/ CSSaspect-ratio撑高;旧内核不支持或实现异常时封面容器高度会坍缩为 0。若封面还是/generated-*私有资源,换签失败后没有玩法参考图兜底时会进一步表现成黑卡。 - 处理:
.platform-public-work-card__cover::before使用padding-top: 56.25%保留 16:9 高度,沉浸式卡片单独覆盖比例;公开作品卡通过resolvePlatformWorldFallbackCoverImage(...)给ResolvedAssetImage传入玩法参考图兜底,签名失败或图片加载失败时仍有可见封面。 - 验证:
npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、npm run typecheck、npm run check:encoding。 - 关联:
src/index.css、src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/rpgEntryWorldPresentation.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
公开作品卡作者行不要拼手机号或陶泥号
- 现象:发现页 / 推荐页公开作品卡作者行显示
158****3533 · SY-00000003这类手机号掩码和陶泥号组合,列表卡片看起来像暴露账号标识。 - 原因:
resolvePlatformWorkAuthorDisplayName(...)曾把公开昵称和publicUserCode拼接为昵称 · SY-*,并在无法解析公开昵称时直接回退后端卡片里的authorDisplayName;当后端或旧投影把手机号掩码写进展示名时,卡片会原样外露。 - 处理:公开卡片作者名只取可读公开昵称;识别手机号掩码、单独
SY-*或手机号掩码 · SY-*时回退为玩家。作品号复制、陶泥号搜索和完整身份展示只放在详情页、搜索或明确复制入口,不塞进卡片作者行。 - 验证:
npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformWorkDetailView.test.tsx。 - 关联:
src/components/rpg-entry/rpgEntryWorldPresentation.ts、src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformWorkDetailView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
生成中草稿恢复要按后端时间戳计时
- 现象:拼图或抓大鹅草稿生成中刷新网页后,进入生成页的“已耗时”从
0 秒重新开始;另一类旧问题是后端progressPercent=88时总进度首帧直接跳到88%。 - 原因:生成页恢复曾把展示态
startedAtMs重置为进入页面的当前时间,导致计时不跟随后端真实生成时刻;拼图总进度也曾把后端里程碑当作百分比地板,导致步骤刚切换就抬高总进度。 - 处理:恢复生成中的草稿时,展示起点使用后端 session
updatedAt或作品摘要updatedAt;88/94/96只切换当前步骤,不直接作为总进度地板。总进度按已完成步骤权重加当前步骤内假进度推导,非完成态最多停在98%。 - 验证:
node node_modules/vitest/vitest.mjs run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating"、node node_modules/vitest/vitest.mjs run src/services/miniGameDraftGenerationProgress.test.ts。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、src/services/miniGameDraftGenerationProgress.ts、docs/【玩法创作】拼图生成页进度口径-2026-05-23.md。
生成失败草稿回到作品架不能继续显示生成中
- 现象:拼图生成页已经收到 VectorEngine 图片编辑失败并进入重试态,但用户返回草稿 Tab 后,同一草稿仍显示“生成中”;连续触发多个拼图生成时,失败后还可能只剩一条新增草稿,或者只看到标题为“第1关”的半成品空壳;抓大鹅后台失败时也可能没有任何通知,点击草稿又像重新开始生成。
- 原因:前端失败 notice 只更新生成页局部状态,pending 作品架条目在失败时被清掉或被非
generating状态误映射为ready;后端作品摘要也可能短暂仍是generationStatus=generating。如果失败消息没有写入 notice,用户离开生成页后不会弹出PlatformErrorDialog;如果打开草稿只看持久化generating,就会绕过失败态恢复。 - 处理:失败时按 session 保留 pending 作品架条目并标记
failed,失败 notice 保存错误消息并触发带来源的PlatformErrorDialog;拼图契约没有failed枚举,pending 拼图映射为idle,同时用本地失败 notice 覆盖持久化生成中状态和旧的“正在生成”摘要。点击失败草稿应优先用 notice / 后端 session / fallback payload 组装失败生成页,不能重新从 0 秒启动新进度;失败页点击重新生成必须优先复用当前sessionId执行编译 action,不得因存在表单缓存 payload 就调用 create-session。拼图失败半成品没有有效workTitle时,作品架标题回退为“拼图草稿”。 - 验证:
node node_modules/vitest/vitest.mjs run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed parallel puzzle|background match3d"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/custom-world-home/creationWorkShelf.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
生成失败重试不要走新建草稿
- 现象:拼图或抓大鹅生成失败后,在失败页点击“重新生成”,作品架里多出一份新的草稿,原失败草稿仍留在列表里。
- 原因:重试 handler 曾优先读取缓存的表单 payload 并调用 create-session 路径;失败草稿按 session 留在作品架是正确行为,于是重试动作额外创建了第二份草稿。
- 处理:只要当前失败页还能恢复到原
sessionId,重试就走该 session 的 compile action;只有没有可恢复 session 时,才允许用表单 payload 重新创建草稿。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed .* draft retry reuses current session"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
汪汪声浪草稿试玩不要写正式 run
- 现象:如果草稿结果页试玩和发布后 runtime 共用同一写成绩路径,未发布或未确认资源的草稿试玩会污染正式单局、排行榜和作品统计。
- 原因:
BarkBattleRuntimeShell同时承担草稿预览和发布后运行态,需要由调用方显式传入runtimeMode区分是否写正式 run。 - 处理:草稿结果页试玩保持
runtimeMode=draft,只做本地预览;发布成功后先进入/works/detail?work=BB-xxxxxxxx,再从详情页以runtimeMode=published进入正式 runtime,并在开始/结算时分别调用startBarkBattleRun与finishBarkBattleRun。 - 验证:草稿试玩不触发 start / finish run;正式 runtime 必须先通过麦克风授权,再写 start run 和结算派生指标。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx、src/services/bark-battle-runtime/barkBattleRuntimeClient.ts。
汪汪声浪移动端创作表单不要再套一层纵向滚动
- 现象:移动端创作 Tab 里进入汪汪声浪表单后,页面右侧出现不自然的内层滚动条,最后的形象描述输入框容易被“生成草稿”按钮、键盘或底部 TabBar 挤压 / 遮挡;顶部玩法卡首尾也可能贴边显得被裁。
- 原因:外层
.platform-tab-panel已经是纵向滚动容器,创作页中间又有多层overflow-hidden,旧的BarkBattleConfigEditor根节点再加overflow-y-auto,形成外层 Tab 面板 + 内层表单的套滚动;底部按钮只预留 safe-area,不预留真实操作区距离;顶部玩法卡横向滚动条隐藏且首尾没有 scroll padding。 - 处理:移动端让 Bark Battle 表单跟随父级滚动,
lg以上才恢复表单内滚动;创作页容器移动端使用overflow-visible和 safe-area 底部 padding;顶部模板 tablist 加scroll-px-3/ 横向 padding,移动端卡片宽度收窄,避免首尾 ring 和圆角贴边裁切。
统一创作页不要把竖屏滚动锁进内部内容区
- 现象:竖屏打开拼图、抓大鹅或敲木鱼创作页时,浏览器页面本身无法滚动,生成按钮或右侧表单面板落到视口外;木鱼的敲击音效和功德词条看起来像被塞进单独滑动窗口。
- 原因:平台根壳固定一屏并隐藏溢出,
UnifiedCreationPage又使用h-full min-h-0 overflow-hidden和内容区overflow-y-auto,导致滚动责任落到内部内容窗,而不是整个创作 stage。 - 处理:
UnifiedCreationPage统一负责标题、隐藏字段契约、内容包装和页面级纵向滚动;拼图、抓大鹅、跳一跳和敲木鱼的外层motion.div不再额外包overflow-y-auto。各工作台在unifiedChrome下收起旧h-full overflow-hidden外壳,让表单主体跟随统一页面滚动。 - 验证:用竖屏浏览器视口打开
/creation/wooden-fish、/creation/puzzle、/creation/match3d和/creation/jump-hop,统一创作页应可滚动到生成按钮;.unified-creation-page应包含页面级overflow-y-auto,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。 - 关联:
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
汪汪声浪拟声词不要被默认狗主题锁死
- 现象:创作者把主题或形象改成机甲、猫、骑士等非狗主题后,局内仍播放
轰汪!、汪爆!这类狗叫词,表现像系统强行把主题带回狗。 - 原因:拟声词 textarea 如果一开始就填入默认小狗词池,并且始终作为自定义
onomatopoeia提交,runtime 会优先使用该字段,无法再根据新的themeDescription/playerImageDescription/opponentImageDescription走主题 fallback。 - 处理:
BarkBattleConfigEditor需要区分“系统默认词池”和“创作者已手动编辑”。未手动编辑时随主题 / 形象描述自动重算;手动编辑后才冻结为自定义词池。默认词池只在命中狗相关关键词时加入狗叫词,非狗主题使用科技、幻想或通用高能词。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx,并确认非狗主题的拟声词不含汪。 - 关联:
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/games/bark-battle/application/BarkBattleConfig.ts、src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx。
Jenkins Web 构建公开作品号导出缺失优先补 publicWorkCode
- 现象:
Genarrative-Web-Build在npm run build:production-release -- --component web阶段失败,Rollup 报"buildJumpHopPublicWorkCode" is not exported by "src/services/publicWorkCode.ts",但导入方rpgEntryWorldPresentation.ts或PlatformEntryFlowShellImpl.tsx已经引用该玩法公开码函数。 - 原因:玩法分支合并时容易只带入新玩法的
publicWorkCode.ts导出,覆盖或遗漏另一个玩法的公开码 builder / matcher,Vite 构建会在静态导出检查阶段直接失败。 - 处理:在
src/services/publicWorkCode.ts中保持每个玩法的build<Play>PublicWorkCode与isSame<Play>PublicWorkCode成对导出;跳一跳使用JH-前缀和 profileId 后 8 位规范化后缀。补src/services/publicWorkCode.test.ts覆盖 builder 和 matcher,避免后续合并再次丢失导出。 - 验证:
npm test -- src/services/publicWorkCode.test.ts,并用npm run build:production-release -- --component web --name <临时名>复现 Jenkins web 构建路径。若npm run typecheck仍报 JumpHop 阶段或状态变量缺口,那是远端当前 JumpHop 接线未收齐的独立问题,不等同于该 Rollup 导出失败。 - 关联:
src/services/publicWorkCode.ts、src/components/rpg-entry/rpgEntryWorldPresentation.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳前端壳层接线不要只合渲染分支
- 现象:
npm run typecheck大量报setJumpHopSession、jumpHopRun、jumpHopGalleryEntries、mapJumpHopWorkToPublicWorkDetail不存在,以及"jump-hop-runtime" is not assignable to SelectionStage;即使 typecheck 过了,分享或刷新/runtime/jump-hop?work=...仍可能掉回首页。 - 原因:跳一跳工作台、生成页、结果页、runtime 和推荐流渲染分支已经合入
PlatformEntryFlowShellImpl.tsx,但平台壳层状态、public detail mapper、SelectionStageunion 与appPageRoutes.ts阶段路由映射没有一并合入;发现页卡片分类也没有先判断isJumpHopGalleryEntry,导致 fallback 访问 RPGthemeMode。 - 处理:
platformEntryTypes.ts必须注册jump-hop-workspace/generating/result/runtime/gallery-detail;appPageRoutes.ts必须补/creation/jump-hop/workspace、/creation/jump-hop/generating、/creation/jump-hop/result、/gallery/jump-hop/detail、/runtime/jump-hop;PlatformEntryFlowShellImpl.tsx必须持有 JumpHop session/work/run/gallery/runtimeReturnStage/generationState/error/busy,并提供mapJumpHopWorkToPublicWorkDetail;RpgEntryHomeView.tsx的公开卡片类型描述要给 JumpHop 单独返回跳一跳。 - 验证:
npm run typecheck,并跑npm test -- src/routing/appPageRoutes.test.ts覆盖 JumpHop 阶段路径。 - 关联:
src/components/platform-entry/platformEntryTypes.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳地块图集固定走 18 个 UV 大单元
- 现象:跳一跳初始草稿生成时报
系列素材图集的物品行数不能超过 n。,或者生成完成后只有 atlas 预览路径,地块切片没有真正落盘。 - 原因:旧模板先后尝试过通用系列素材 helper、
2x3六格固定 tileType 和5x5单贴图池,但当前跳一跳已经重设计为“主题 -> 一张1024x1536图集 -> 18 个3列*6行UV 大单元 -> 每格4列*3行六面贴图 -> 无限路径”,旧的物品行数 / 固定类型模型都会把创作链路带偏。 - 处理:跳一跳地块固定只生成一张
1024x1536主题 UV 展开图集,后端先切出 18 个大单元,再从每格固定 UV 网切出 top/front/right/back/left/bottom 六张256x256不透明 PNG,并对 108 张面贴图各自走 OSS 上传、asset_object 确认和 entity bind;不要再恢复2行*3列、5x5单贴图、start / normal / target / finish / bonus / accent六格口径。 - 验证:
jump_hop.rs不应再调用通用物品行数模型处理地块图集;公开结果里应能拿到 18 个独立JumpHopTileAsset且每个新资产包含faceAssets六面贴图,运行态无限路径从地块池随机取材;旧资产没有faceAssets时仍能用imageSrc单贴图 fallback。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳宝可梦主题地块图集 safety rejection 只做专项改写
- 现象:跳一跳草稿使用“宝可梦 / Pokemon / 皮卡丘 / 精灵球”等主题时,背景底图和返回按钮可能已生成成功,但地块图集的 VectorEngine 请求返回
Your request was rejected by the safety system,日志里failure_context="跳一跳地块图集生成失败"、status=429、code="invalid_prompt"。 - 原因:18 个立方体主题物体 UV 展开图集 prompt 会把这些词放进“主题物体图集”语境,容易被上游理解为要求生成具体宝可梦角色或标志道具,触发安全拦截;这不是普通平台造型词、抠图或超时问题。
- 处理:仅在跳一跳图片生成 prompt 文本命中宝可梦相关词时做生成侧替换,把
宝可梦 / 神奇宝贝 / 口袋妖怪 / Pokemon改为“原创幻想萌宠冒险道具”,把精灵球改为“彩色冒险能量球”,把皮卡丘 / Pikachu改为“黄色闪电萌宠符号”;不要把所有主题都加全局 IP 禁止约束,用户草稿标题和主题展示也不改。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml应覆盖宝可梦词专项替换;真实联调时同一草稿重试后,地块图集请求的 prompt 不再包含宝可梦相关词。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳地块切片不要按 tileType 复用资产槽位
- 现象:跳一跳生成完成后,运行态看起来仍像在显示默认几何地块,或者地块图片在加载时频闪;结果页地块池也可能只看到少量重复素材。
- 原因:
tileType只是路径平台的玩法类型标签,18 个 atlas 大单元里会重复出现normal / target / bonus / accent等类型。若后端持久化时用tileType生成 slot/path,同类型切片会写入同一个/generated-jump-hop-assets/<profile>/<slot>/image.png,后上传的切片覆盖先上传的切片,前端换签缓存也会读到重复或旧对象。 - 处理:后端切图后必须按 atlas 单元格写入
tile-01到tile-18的唯一 tile slot,并把六面贴图写入tile-XX-top/front/right/back/left/bottom唯一 face slot;前端结果页和运行态展示生成图时用assetObjectId作为refreshKey,避免重生成后复用旧签名或旧图片缓存。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml -- --nocapture应包含jump_hop_tile_asset_slots_are_unique_for_eighteen_slices;前端运行态测试应断言地块换签带assetObjectId刷新键,并覆盖新 UV 资产会解析六张面贴图。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-result/JumpHopResultView.tsx。
跳一跳落点辅助标识不要再用舞台高度常量拍脑袋投影
- 现象:按住蓄力时落点辅助标识虽然会动,但看起来像静态点位漂移,和真实可落地的位置对不上。
- 原因:辅助标识如果只按
stageSize.height和一个固定比例估算投影距离,再去跟拖拽向量合成,就会和当前地块到目标地块的真实屏幕跨度脱节;三维场景层级过高时还会把辅助点直接盖住。 - 处理:辅助标识必须使用当前地块与目标地块之间的真实屏幕距离和后端
chargeToDistanceRatio做投影,再映射到屏幕坐标;它只作为调参验证层随按下显示、松手或取消隐藏,不参与后端裁决和作品配置;同时把辅助层 z-index 放到三维角色层之上,避免被场景层遮挡。 - 验证:半程蓄力时辅助点应落在当前地块和目标地块之间,完整蓄力时应逼近目标地块中心;运行态截图里辅助点必须始终压在地块与角色之上。
- 关联:
src/services/jump-hop/jumpHopRuntimeModel.ts、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx。
跳一跳长按蓄力不能再消费拖拽方向
- 现象:跳一跳改成长按蓄力后,如果前端或后端仍消费
dragVectorX/dragVectorY,玩家手指轻微移动就会改变跳跃方向,和“始终朝下一块中心跳”的体验不一致。 - 原因:历史弹弓拖拽版本把屏幕拖拽方向作为正式裁决输入,契约字段仍为兼容旧客户端保留,容易被误认为仍是当前玩法规则。
- 处理:前端运行态只用长按时长提交
dragDistance兼容字段,不再发送方向字段;落点预测按当前地块中心到下一块地块中心的方向投影。后端module-jump-hop即使收到旧客户端dragVectorX/dragVectorY也必须忽略,只按当前地块到下一块地块中心的单位向量裁决。 - 验证:前端回归测试覆盖手指移动不改变提交方向、预测落点忽略旧方向字段;后端领域测试覆盖旧客户端传错误方向时仍按下一块中心命中。
- 关联:
src/services/jump-hop/jumpHopRuntimeModel.ts、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、server-rs/crates/module-jump-hop/src/application.rs。
跳一跳创作入口旧文案先查 SpacetimeDB 配置
- 现象:
JumpHopWorkspace已只剩主题输入,但创作 Tab 的跳一跳模板卡仍显示旧的“俯视角跳跃闯关”或拼图参考图。 - 原因:创作入口卡片事实源是 SpacetimeDB
creation_entry_type_config和/api/creation-entry/config,前端只做展示派生;如果只改工作台、PRD 或前端组件,已有库里的旧入口行不会自动变化。当前api-server读取入口配置时优先订阅缓存,缓存命中后不会再走 procedure 播种,所以只把迁移写在get_creation_entry_config里不够。 - 处理:同步更新
module-runtime默认入口种子,并在spacetime-module/src/runtime/creation_entry_config.rs加只命中旧系统默认值的迁移;同时在spacetime-client的入口配置读模型里做同一条旧系统默认行的读路径纠偏。跳一跳当前默认值为subtitle=主题驱动平台跳跃、image_src=/creation-type-references/jump-hop.webp。 - 验证:本地
GET /api/creation-entry/config的jump-hop项应返回新 subtitle 和新 imageSrc;若仍旧,检查本地 SpacetimeDB 是否已发布当前spacetime-module,以及后台是否手动覆盖过入口配置。若缓存路径和 procedure 路径返回不一致,优先怀疑读模型映射没做纠偏,而不是前端展示层。
image2 dry-run 带参考图时不要直接打印 data URL
- 现象:使用 VectorEngine
gpt-image-2-all生成带参考图的概念图时,如果 dry-run 直接打印完整请求体,参考图会被转成超长data:image/png;base64,...,终端日志会被数百万字符淹没。 - 原因:生成请求支持
image数组传入 data URL 参考图;dry-run 如果复用 live 请求体输出,就会把参考图内容完整打印。 - 处理:dry-run 输出摘要,只保留
imageReferenceCount、尺寸、模型和 prompt,不输出完整 base64。live 请求仍按实际需要传image数组。 - 验证:执行
node scripts/generate-edutainment-tv-map-concepts.mjs --dry-run,输出应只显示imageReferenceCount: 1,不出现完整 base64。 - 关联:
scripts/generate-edutainment-tv-map-concepts.mjs、docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md。
生成图资产不能只拼 generated legacy path
- 现象:结果页或运行态拿到
/generated-*-assets/.../image.png后图片不显示;前端ResolvedAssetImage会先调用/api/assets/read-url?legacyPublicPath=...,但换签后的 OSS URL 仍指向不存在对象。 - 原因:后端只写了看起来像生成图的 legacy path,没有真正调用 image2、上传 OSS、登记
asset_object并绑定实体。/api/assets/read-url只负责签名读取,不会凭空生成或补写对象。 - 处理:玩法生成链路必须在
api-server完成外部副作用:调用 VectorEnginegpt-image-2-all,用GeneratedImageAssetAdapter准备PutObject,上传 OSS 私有对象,调用confirm_asset_object和bind_asset_object_to_entity,再把返回的legacyPublicPath写入玩法 profile。 - 验证:
cargo check -p api-server --manifest-path server-rs/Cargo.toml;契约测试应断言前端 JSON 自带的hitObjectAsset会被忽略,spacetime-client 定向测试应断言缺少服务端注入的真实hitObjectAsset时不能编译;浏览器 Network 中 generated 图片应先换签,签名 URL 指向已存在对象。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、server-rs/crates/spacetime-client/src/wooden_fish.rs、src/components/ResolvedAssetImage.tsx、src/services/assetReadUrlService.ts。
生成页背景视频要固定全屏并显式触发播放
- 现象:生成页明明带了
media/create_bg_video.mp4,但移动端或某些内核里只看到静态首帧,或视频层跟着局部容器滚动,被白色面板压住后看起来像没加载。 - 原因:仅靠
autoPlay/loop/muted/playsInline并不稳定;视频如果仍挂在局部容器里,还会被页面面板和遮罩吞掉。某些浏览器初始化后也会停在paused=true。 - 处理:背景视频必须放到
fixed inset-0的全屏底层容器里,外层页面用isolate/ 透明底控制叠层;挂载后显式尝试play(),并在loadeddata、canplay和页面聚焦时再次触发,避免只停首帧。 - 验证:移动端视口检查视频
rect应覆盖整个视口,paused应最终变为false,currentTime应持续前进。 - 关联:
src/components/GenerationProgressHero.tsx、docs/【玩法创作】生成页圆环布局口径-2026-05-23.md。
跳一跳结果页直达时不要把恢复面板当成空白页
- 现象:浏览器直接打开
/creation/jump-hop/result,如果没有sessionId、profileId、draftId或workId,页面以前会看起来像空白,容易误判成结果页坏了。 - 原因:跳一跳结果页恢复原先只盯
jumpHopSession.draft,没有把“缺恢复信息”明确兜成可见恢复面板;直达结果页时也没有优先用profileId -> getWorkDetail补回完整作品。 - 处理:
PlatformEntryFlowShellImpl的跳一跳恢复逻辑改成先尝试profileId -> getWorkDetail,再尝试sessionId -> getSession;两者都没有时显示跳一跳草稿未恢复和返回创作,不再留空白页。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route",并手测/creation/jump-hop/result与/creation/jump-hop/result?profileId=<id>两种情况。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
2026-05-24 补充:GenerationPageBackdrop 不要通过 portal 挂到 document.body。body 级 fixed 背景会逃离生成页自己的 stacking context,即使业务内容有局部 z-10,真实浏览器里也可能把整页 UI 压住。背景视频应作为生成页根容器子节点保留 fixed inset-0 z-0,生成页内容保持 relative z-10;相关测试应同时断言背景容器低层级、生成页根容器高层级,以及视频节点仍在生成页 DOM 内部。视觉调整时还要记住:空心圆环的中心块要抽掉,时间卡与总进度标题都应缩小,不要让生成页再回到“纯色底 + 大字号说明卡”的状态。顶部返回和右上状态也不能沿用 text-lg / sm:text-2xl 这类展示级字号;当前步骤名、步骤状态和底部玩法信息标题要维持普通 UI 字号档位,优先保持 text-xs 到 text-sm 区间。
2026-05-24 补充:生成页“预计等待 / 已耗时”卡片本身已经有标签,传给 GenerationProgressHero 的值只能是纯时间,例如 4 分钟、1 分 15 秒,不要再拼接“预计还需”或“已耗时”;两张时间卡也要和当前步骤卡一样保持半透明。拼图总进度初始帧必须允许显示 0%,不要再用 Math.max(1, nextProgress) 之类的保护把启动态抬到 1%。
2026-05-27 补充:generation-hero-progress-ring-fill 里那个橘黄色小点不是背景噪点,而是 strokeLinecap="round" 在短弧段上的端点;当前圆环口径要求底部 90deg 开口居中对称,因此轨道和填充都应使用 135deg 起点。圆环本体现在固定为 400x400,排查时先看 data-ring-start-degrees、data-ring-fill-start-degrees 和容器尺寸,不要把尺寸伸缩误认成素材渲染问题。
dev:spacetime 启动后 3101 又断开先查 publish 是否被 spacetime.json 干扰
- 现象:浏览器报
Failed to initiate WebSocket connection,目标为ws://127.0.0.1:3101/v1/database/<db>/subscribe,端口检查发现3101没有长期监听;手动运行npm run dev:spacetime可看到 standalone 短暂启动后退出,发布阶段报No database target matches '<db>'。 - 原因:SpacetimeDB CLI 会读取仓库根目录
spacetime.json。如果本地发布命令没有显式--no-config,CLI 可能按配置文件里的 target 解析数据库,覆盖脚本已传入的.env.local数据库名和--server,导致 publish 失败;dev.mjs捕获错误后会清理刚启动的 standalone,于是浏览器看到 3101 被拒绝连接。 - 处理:
scripts/dev.mjs的本地 publish 固定追加--no-config,只使用脚本解析出的数据库名、module path 和实际 SpacetimeDB server。排查时前台运行npm run dev:spacetime -- --no-interactive,若看到该错误,先确认脚本是否仍带--no-config,再查.env.local/spacetime.local.json的数据库名。 - 验证:
npm run test -- scripts/dev.test.ts覆盖 publish 参数包含--no-config;npm run dev:spacetime -- --no-interactive后http://127.0.0.1:3101/v1/ping应保持 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地 api-server 启动订阅 401 先查 Web identity token 注入
- 现象:
npm run dev启动到 api-server 恢复认证投影时,日志出现Failed to initiate WebSocket connection ... /v1/database/<db>/subscribe?compression=Brotli: HTTP error: 401 Unauthorized。 - 原因:SpacetimeDB SDK 订阅需要 Web API identity token;本地
.env.local常把GENARRATIVE_SPACETIME_TOKEN留空,只靠 CLI 登录态 publish 成功并不能让 api-server 的 WebSocket subscribe 获得权限。 - 处理:
scripts/dev.mjs在 SpacetimeDB 就绪后优先读取<spacetimeDataDir>/dev-api-identities/<serverSha256>.json;缺失或不可用时才调用/v1/identity创建专用 Web API identity token,并以普通0600文件持久化。token 只注入api-server,不写.env.local、不传 Web / Vite、也不进日志。若仍报 401,先确认是否使用项目脚本启动、记录文件是否因 server 或权限不匹配被重建,以及GENARRATIVE_SPACETIME_SERVER_URL/ 数据库名是否指向本次启动的实例。 - 验证:
npm run test -- scripts/dev.test.ts;重新运行npm run dev后 api-server 启动日志不再出现上述 subscribe 401,/healthz返回 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
创作作品架或公开列表异常先查本地 SpacetimeDB schema 漂移
- 现象:本地
http://127.0.0.1:3000/启动后,api-server日志反复出现Host returned error when processing subscription query: no such table: puzzle_gallery_card_view;或创作中心草稿 / 已发布作品整块消失,GET /api/creation-entry/config返回502且 details 为No such procedure。 - 原因:本地
.env.local或spacetime.local.json指向的 SpacetimeDB 库没有发布当前spacetime-module,或当前 CLI 身份无权发布该库;例如旧xushi-p4wfr库缺get_creation_entry_config/puzzle_gallery_card_view,但当前代码的spacetime-client启动时会长期订阅这些公开 read model。 - 处理:先用
spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --server http://127.0.0.1:3101确认目标库是否有当前 view;若只是本地验证,可用 gitignored 的spacetime.local.json指向可发布且已包含当前 schema 的库,例如{"database":"genarrative-dev-codex"}。该 JSON 必须无 UTF-8 BOM,否则scripts/dev.mjs会忽略它。修改后用npm run dev:api-server -- --database <database> --spacetime-port 3101 --api-port 8082 --no-interactive重启。 - 验证:
curl.exe -i http://127.0.0.1:8082/healthz返回200;curl.exe -i http://127.0.0.1:8082/api/runtime/puzzle/gallery返回200;浏览器打开http://127.0.0.1:3000/无puzzle_gallery_card_view控制台或后端日志错误。 - 关联:
scripts/dev.mjs、server-rs/crates/spacetime-client/src/lib.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
创作作品架消失先查入口配置 procedure 与本地库权限
- 现象:寓教于乐或创作中心下草稿 / 已发布作品突然整块消失,
GET /api/creation-entry/config返回502,details 中为No such procedure。 - 原因:本地
.env.local或spacetime.local.json指向的 SpacetimeDB 库没有发布当前spacetime-module,或当前 CLI 身份无权发布该库;例如旧xushi-p4wfr库缺get_creation_entry_config时,前端拿不到入口配置就不会渲染作品架。 - 处理:优先切换到拥有目标库权限的 SpacetimeDB 身份后重新运行
npm run dev完成发布;若只是本地验证,可用 gitignored 的spacetime.local.json指向可发布的本地库。debug 构建的api-server对入口配置缺 procedure 会使用后端默认入口配置兜底,避免作品架因本地库漂移整块空白。 - 验证:
curl.exe -i http://127.0.0.1:8082/api/creation-entry/config返回200且包含baby-object-match;前端草稿页作品架重新渲染。 - 关联:
server-rs/crates/api-server/src/state.rs、server-rs/crates/api-server/src/creation_entry_config.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
抓大鹅物品 spritesheet 偏移先查 alpha 连通域切片是否启用
- 现象:抓大鹅物品图集里大多数素材显示不全、被裁碎、位置整体偏移,甚至切出来像拼贴块。
- 原因:旧链路只按
10x10固定格线裁切,遇到模型输出的透明图集稍有偏移、跨格或留白不均时就会把主体切坏。现在后端优先按透明 alpha 连通域识别真实素材矩形,再按原图从上到下、从左到右排序;只有识别数量不足时才回退旧网格切法。 - 处理:优先检查
generated_asset_sheets.rs的 alpha 连通域切片是否生效,再查item_assets.rs是否还在透传旧的固定格线语义。不要只改前端显示比例。 - 验证:定向测试
cargo test -p api-server generated_asset_sheet_two_items_per_row --manifest-path server-rs/Cargo.toml -- --nocapture应通过,且错位透明样本应按连通域切出完整视图。 - 关联:
server-rs/crates/api-server/src/generated_asset_sheets.rs、server-rs/crates/api-server/src/match3d/item_assets.rs。
腾讯云 release 上 VectorEngine SendRequest 超时先查出口链路与重试
- 现象:release 机器调用 VectorEngine
gpt-image-2的/v1/images/generations或/v1/images/edits偶发client error (SendRequest) -> connection error -> Connection timed out (os error 110),应用层表现为 504;本地通常正常。 - 原因:本地 DNS 可能走代理 / 加速出口,而腾讯云 release 直接解析到 VectorEngine 真实边缘节点。实测同一张约 2.37MB PNG、同一 edits 请求,
curl5/5 成功,但reqwest/hyper会间歇性超时;固定40.160.33.47也只能改善,不能根治。 - 处理:不要优先关闭 multipart,也不要直接把
SendRequest解释成上游业务拒绝。VectorEngine 图片generations/edits上游 POST 单独使用libcurl;参考图下载和响应图片 URL 下载仍用reqwest。send 阶段 timeout / connect error 在platform-image内最多重试 5 次,使用指数退避和短抖动;日志字段attempt、max_attempts、retry_delay_ms、reference_image_bytes_total、request_params是定位依据。
api-server libcurl / OpenSSL 3.2 runtime
- 症状:release 部署新
api-server后服务反复exit-code,LD_TRACE_LOADED_OBJECTS=1 /opt/genarrative/current/api-server或ldd报/lib/x86_64-linux-gnu/libssl.so.3: version 'OPENSSL_3.2.0' not found。 - 根因:
platform-image使用libcurl后,Linux release 构建产物可能直接要求OPENSSL_3.2.0符号;Ubuntu 24.04 apt 默认 OpenSSL 仍是3.0.13,不能满足该符号版本。 - 处理:
Genarrative-Server-Provision独立安装 OpenSSL3.2.0到/opt/genarrative/openssl-3.2.0,并只通过genarrative-api.service的LD_LIBRARY_PATH=/opt/genarrative/openssl-3.2.0/lib64:/opt/genarrative/openssl-3.2.0/lib给 api-server 使用,避免替换系统 OpenSSL。
VectorEngine edits multipart image part
- 症状:拼图参考图链路请求
/v1/images/edits返回500 image is required,但应用日志里reference_image_count=1、reference_image_bytes_total>0,request_params.referenceImages[0]也有field=image、文件名、MIME 和 bytes。 - 根因:Rust
curl::easy::Form中contents(...).filename(...)不等价于文件上传 part;VectorEngine 转码层会认为没有收到图片。release 上用 curl CLI-F image=@file可成功,证明字段名和上游接口本身没变。 - 处理:multipart 参考图必须用
Form::buffer(file_name, bytes)并设置content_type(...),让 libcurl 生成真正的name="image"; filename="..."文件 part。 - 验证:release 上先看
journalctl -u genarrative-api.service中VectorEngine 图片请求发送失败,准备重试与最终HTTP 返回;若仍失败,再用同一图片分别跑 curl 与最小 reqwest 探针对照。 - 关联:
server-rs/crates/platform-image/src/vector_engine/client.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
个人中心不再保留直达“存档”按钮入口
- 现象:2026-05-25 起,移动端“我的”页顶部改为品牌行 + 扫码 / 设置按钮,设置区和次级入口不再提供独立的
存档按钮;用户仍可在“玩过”弹窗里查看可继续存档。 - 原因:产品布局收口后,个人中心只保留设置、扫码、常用功能和条件性次级入口,存档恢复继续以后端
/api/profile/save-archives真相为准,但不再作为页面直达入口。 - 处理:后续如果需要重新暴露存档入口,优先评估是否应回到“玩过”或别的独立弹窗流程,不要默认把存档再塞回常用功能宫格或设置列表。
- 验证:
npm test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile profile page matches the reference layout sections|profile scan action opens camera scanner instead of recharge panel"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
旧创作入口先确认是不是旧 worktree 在响应
- 现象:浏览器里明明还看到跳一跳旧入口,比如
俯视角跳跃闯关和puzzle.webp,但当前 worktree 里已经改成了主题驱动平台跳跃和jump-hop.webp。 - 原因:本机常同时存在两个开发栈,旧 worktree 可能还在占用
3000/8082/3101/3102,而当前 worktree 可能跑在另一组端口。只看页面文案就下结论,容易把旧进程误认成当前改动没生效。 - 处理:先用
Get-NetTCPConnection/Get-CimInstance Win32_Process确认端口对应的可执行文件和命令行,再分别请求/api/creation-entry/config比对旧端口与当前 worktree 端口。必要时以当前 worktree 的实际端口为准重新打开页面。 - 验证:旧端口返回旧跳一跳入口,当前 worktree 端口返回新跳一跳入口;两边的
api-server/vite-cli命令行应指向不同仓库路径。 - 关联:
scripts/dev.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
3001 无法访问先查旧 worktree 占端口和 SpacetimeDB 版本
- 现象:
http://127.0.0.1:3001/打不开,但3000 / 3101 / 8082仍有进程;npm run dev直接退出,没有把新栈拉起来。 - 原因:旧 worktree 的
api-server、spacetime-standalone和 Vite 还活着,或者当前 worktree 的本机 SpacetimeDB CLI 默认版本低于仓库锁定版本,scripts/dev.mjs会先校验版本再启动并直接报错退出。 - 处理:先停掉占用端口的旧进程,再执行
spacetime version list,确认本机 CLI/standalone 与server-rs/Cargo.toml锁定版本一致;不一致时先直接升级 / 切换到锁定版本,再重新启动npm run dev -- --no-interactive --web-port 3001 --api-port 8083 --spacetime-port 3103 --admin-web-port 3104。 - 验证:
http://127.0.0.1:3001/、http://127.0.0.1:8083/healthz、http://127.0.0.1:3103/v1/ping都返回 200,且进程命令行指向当前 worktree 路径而不是别的仓库。 - 关联:
scripts/dev.mjs、docs/project-memory/shared-memory/pitfalls.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
微信历史孤儿作品不要让新注册账号顶替
- 现象:清空用户数据或迁移历史数据后,旧作品的
owner_user_id为空或失效,新注册用户会因为顺序号复用或旧 ID 残留顶替作品归属,导致刚注册就看到别人的草稿或已发布作品。 - 原因:作品作者解析曾经把缺失作者简单回退到普通登录用户,且微信新用户用户名 / 内部 ID 都太容易被误认或复用。
- 处理:作品作者找不到真实账号时统一回退到占位作者
wx-openid-placeholder,展示名固定为失效作者;微信新用户用户名改为名字_openid,内部user_id改成不可复用的 UUID 风格;离线回填时先识别真实有效用户,再把孤儿作品表写回占位账号。 - 验证:
cargo test -p module-auth --manifest-path server-rs/Cargo.toml、cargo test -p api-server --manifest-path server-rs/Cargo.toml work_author、npm run test -- scripts/rebind-orphan-work-owners.test.ts。 - 关联:
server-rs/crates/api-server/src/work_author.rs、server-rs/crates/module-auth/src/domain.rs、scripts/rebind-orphan-work-owners.mjs。
访客推荐页上下滑不要绑定登录态
- 现象:访客模式进入移动端推荐页后,推荐内容可展示和点击底部“下一个”,但在作品信息区域上下滑不会切换推荐作品,表现为推荐页不能上下滑动。
- 原因:推荐页滑动切换逻辑
beginRecommendDrag(...)误把isAuthenticated作为启用条件;访客态虽然允许浏览和通过底部按钮切换,却无法触发同一套拖拽切换。 - 处理:推荐页拖拽只校验当前是否有作品、多作品可切换以及是否正在提交动画,不再要求登录;登录态相关操作仍由点赞、改造等按钮自身权限控制。
- 验证:
npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx覆盖访客态纵向滑动不弹登录且触发下一条推荐。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx。
Windows junction worktree 下 Vitest 定向路径失败先切真实路径
- 现象:在 Windows junction 或映射 worktree 中运行前端测试时,Vitest 可能把同一文件解析为另一盘符路径,误报文件不存在。
- 原因:Vite / Vitest 在 Windows 下会把测试入口 realpath 到真实 worktree 路径;如果命令从 junction 路径传入相对文件参数,入口路径和 resolved id 可能跨盘符不一致。
- 处理:前端定向测试优先从
Get-Item <worktree> | Format-List Target显示的真实路径运行,例如F:\DevWorktrees\codex\worktrees\f584\Genarrative;不要把这类文件加载失败误判成组件或路由断言失败。 - 验证:同一命令从真实路径执行应正常收集并运行测试,例如
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。 - 关联:
src/components/puzzle-clear-creation/PuzzleClearWorkspace.test.tsx、src/components/puzzle-clear-result/PuzzleClearResultView.test.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/routing/appPageRoutes.test.ts。
拼消消草稿试玩要和正式 runtime 分流
- 现象:拼消消结果页点击“试玩”后如果仍然调用
/api/runtime/puzzle-clear/runs,草稿试玩会被正式 run 规则和统计约束卡住,公开作品又可能和草稿恢复串台。 - 原因:拼消消既有草稿生成 / 结果页 / 发布闭环,也有正式公开 runtime;如果把结果页试玩和公开运行态复用同一个后端 startRun 入口,
work detail读取路径和统计口径都会混在一起。 - 处理:结果页试玩改走前端本地
runtimeMode=draftsnapshot,只用于草稿试玩和关卡切换,不写正式 run;公开详情和推荐流进入正式 runtime 时才走后端/api/runtime/puzzle-clear/*。客户端读取作品详情时也要区分创作详情/api/creation/puzzle-clear/works/{profileId}与公开运行态详情/api/runtime/puzzle-clear/works/{profileId}。 - 验证:点击拼消消结果页的试玩按钮,不应再请求
/api/runtime/puzzle-clear/runs;公开详情入口仍应能读取后端运行态详情。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/services/puzzle-clear/puzzleClearClient.ts、src/services/puzzle-clear/puzzleClearLocalRuntime.ts、docs/prd/【玩法创作】拼消消玩法模板PRD-2026-05-30.md。
拼消消 runtime 必须继承拼图模板的原生交互基线
- 现象:拼消消卡片在浏览器里会出现原生图片拖拽 / 下载手柄,或窗口拉伸后棋盘和卡片被拉成矩形。
- 原因:拼消消 runtime 早期只继承了“交换 / 消除”的业务逻辑,没有完整继承拼图模板在基础交互上的防护:
touch-none、select-none、aspect-square、draggable={false}、onDragStart(event.preventDefault())、-webkit-user-drag: none。 - 处理:棋盘容器必须保持正方形约束,卡片按钮和内层
<img>都要显式禁用浏览器原生拖拽,样式层也要补user-select: none与-webkit-user-drag: none,不能只靠业务指针逻辑。 - 验证:浏览器中检查棋盘
getBoundingClientRect().width === height,卡片图片draggable="false"且-webkit-user-drag为none;真实拖拽只应进入交换逻辑,不应触发原生图片拖拽。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/index.css、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消拖拽浮层要挂到页面级 portal
- 现象:拼消消拖拽时图片看起来没有贴在鼠标或手指上,尤其是平台壳层本身带有 transform 时更明显。
- 原因:拖拽 ghost 用了
position: fixed,但如果还挂在会被 transform 的局部容器里,浏览器会把 fixed 当成相对该祖先定位;clientX/clientY读到的是视口坐标,两个坐标系一混就会出现肉眼可见的偏移。 - 处理:拖拽浮层必须通过 portal 挂到
document.body这一层,再继续使用clientX/clientY - pointerOffset计算 left/top;不要把 ghost 留在平台壳或任何会参与 transform 的容器里。 - 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx应断言拖拽浮层父节点是document.body,且 left/top 与按下点偏移一致。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消要继承拼图模板的动作语言,不只是规则
- 现象:拼消消如果只实现“交换后裁决”,但没有开局翻牌、按下留空位、被替换卡快速飞回、以及局部拼接块整体拖动,玩家会直觉上觉得比原拼图更笨重。
- 原因:早期实现容易把“规则独立”误读成“动作语言也要重写”,结果只保留了交换逻辑,没有沿用拼图模板里已经验证过的拖拽反馈、空位让位和合并块连续感。
- 处理:拼消消运行态要继承拼图模板的基础手感:只在开局保留入场翻牌,拖起时源位立即呈空,放下时被替换卡要有明确飞向空位的位移感,连通块要作为整体拖动和整体呈现。
- 验证:浏览器拖拽时能看到跟手 ghost、源位空槽、落点飞入和整组拼接层;
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx应覆盖这些行为。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/index.css。
拼消消空格位必须允许落位,不能当成不可交互死格
- 现象:运行到某一关后,棋盘里出现空格位,用户能看见空洞但拖不进去,也点不动。
- 原因:空格位被前端交互或后端裁决误当成“无效目标”,只保留了交换逻辑,没有把“源卡落入空位、源位清空”当成合法移动。
- 处理:空格位必须保留 button 交互态和落点命中逻辑;前端拖拽 / 点击落到空格时直接提交移动,后端和本地 runtime 都要把源卡移动到目标格并清空源格,不再走失败交换。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml player_move_can_drop_card_into_empty_target_cell -- --nocapture。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
拼消消空位落卡后必须立即补位,不能把空洞留成真空格
- 现象:卡牌成功落进空格后,源位仍然留空,玩家会误以为那个格子坏掉了。
- 原因:移动逻辑只处理了“落到空位”,没有在未消除时同步走一遍重力补位,所以源列会短暂或永久留下空洞。
- 处理:只要移动后棋盘存在空位,就立即走补位和可解性修复;这样源位会从顶部准备区补卡,不会留下不可交互空洞。
- 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml player_move_can_drop_card_into_empty_target_cell -- --nocapture。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
拼消消素材错位先查 sheet 质量门禁
- 现象:一张卡牌切片里同时出现两个或多个错位图案,或空白格、相邻编号区域里混入其他图案碎片。
- 原因:provider 生成的
1024x1536 / 4x6工作表可能违反视觉契约;旧流程只校验布局元数据和切片数量,无法发现图像内容已经主体缺失或污染空白格。边界贴边检测容易把正常铺满主体误判成跨格污染,不能作为高可靠硬门禁。 - 处理:先强化 atlas prompt,要求每个
256x256单元独立查看时只能包含一个主体或同一主体单一局部;服务端在 sheet 切片前做像素级质量门禁,硬拦截非空格前景占比过低和空白格污染,严重多边非同组边界贴边只记录 warning 供排查,不直接让创作失败。硬门禁失败的 sheet 最多尝试 4 次,仍失败则拒绝持久化脏 atlas。 - 追加处理:照片式微场景素材必须把每个
256x256单元收束为一张完整的单场景照片裁片;同编号连续格表示同一视觉家族,不是随机独立小图,要求共享同一场景锚点、主色和道具语言。禁止单格内部出现两张照片、两个不同场景、拼接线、内部竖切、内部横切或左右 / 上下两块不同背景;质量门禁只在单格内部强色差直线贯穿大部分高度或宽度,且两侧都像低纹理人工平铺色块时,按“单格内部疑似拼接线”硬失败并重试 sheet,避免把窗框、桌沿、地平线等自然场景强边缘误杀。 - 追加处理:sheet 生成时如果 VectorEngine 返回
retryable=true的502、504、429或请求超时,例如 nginx HTML502 Bad Gateway,不要立刻把草稿置为 failed,应消耗同一 sheet 的下一次 attempt;仍失败再回写失败状态。 - 追加处理:
sheet-03原本唯一空白格容易被模型画入主题主体,导致第 6 行第 4 列反复报“空白格有主体”并消耗多次 image2 请求。该格改为FILL补位格,允许生成主题小图但服务端切片、atlas 合成和运行态全部丢弃;前端拼消消 action 等待窗口同步提高到 40 分钟,避免上游单图慢返回时用户侧 20 分钟超时。 - 验证:
cargo test -p api-server puzzle_clear --manifest-path server-rs/Cargo.toml -- --nocapture、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle_clear.rs、docs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md。
拼消消锁定组覆盖层必须锚定在棋盘本身
- 现象:消除或补牌过程中,局部完成的组图偶尔会看起来从格子里“飘出去”,并且大小会随着窗口和外层面板变化而异常拉伸。
- 原因:锁定组视觉层用了
absolute inset-0,但棋盘容器本身不是position: relative,于是覆盖层实际锚到了更外层的运行态面板,gridColumn/gridRow只能在错误坐标系里排版。 - 处理:棋盘容器必须显式
relative,让锁定组覆盖层、拖拽鬼影和格子坐标都在同一正方形棋盘坐标系内排版;不要把这类覆盖层锚到外层section或整页容器。 - 验证:浏览器里棋盘
getBoundingClientRect()和锁定组覆盖层应共享同一块正方形区域,窗口缩放后组图不应再出现越界或被拉伸的现象;PuzzleClearRuntimeShell.test.tsx需要断言棋盘 class 包含relative。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消中央场地底图必须挂在棋盘内部
- 现象:创作阶段选择了中央场地底图,但运行态消除卡片后只看到浅色格子或空点,看不到底图。
- 原因:底图被渲染成整页氛围背景,并被页面渐变、棋盘面板和格子
bg-white/78遮住;棋盘内部没有静态底图层,空格仍保留不透明卡片底色。 - 处理:
boardBackgroundAsset.imageSrc必须作为puzzle-clear-board内部的absolute inset-0静态底图渲染;空格、消除空位和拖拽源位必须透明或近透明,不能继续使用实体卡片白底。 - 验证:
PuzzleClearRuntimeShell.test.tsx断言puzzle-clear-board-background在棋盘内,/board-bg.png只出现一次,空格 class 包含bg-transparent且不包含bg-white/78。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作入口突然消失先查前后端是否串到不同 worktree
- 现象:
http://127.0.0.1:3000/可访问,但创作 Tab 里新增玩法入口消失;例如puzzle-clear已在代码默认种子中存在,浏览器仍看不到“拼消消”。 - 原因:Vite 可能来自当前 worktree,但代理目标的
api-server仍是另一个 worktree 的旧进程,或者api-server连到旧 SpacetimeDB 模块;此时/api/creation-entry/config会返回旧入口配置。 - 处理:先用
Get-NetTCPConnection -State Listen -LocalPort 3000,8083,3103结合Get-CimInstance Win32_Process确认端口进程路径;停止串线的旧api-server,再用当前 worktree 的npm run dev:spacetime -- --spacetime-port <port> --database <database>和npm run dev:api-server -- --api-port <port> --spacetime-port <port> --database <database>拉起同一套服务。 - 验证:
GET /api/creation-entry/config应包含目标入口,且监听端口的命令行都指向同一个 worktree;浏览器创作 Tab 对应分类应显示入口卡。 - 关联:
scripts/dev.mjs、.codex/skills/genarrative-dev-stack-port-routing/SKILL.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
Windows junction 工作区下 dev.mjs 直接执行入口要用 realpath 判断
- 现象:在 Windows junction 或映射 worktree 中运行
npm run dev:web,进程可能秒退且端口不监听;从真实 worktree 路径启动正常。 - 原因:
scripts/dev.mjs的入口判断只比对process.argv[1]和import.meta.url的字面路径;junction 路径和 realpath 路径不一致时会误判成“不是直接执行”,于是主流程根本不进入。 - 处理:入口判断改成基于
realpathSync(...)的isDirectModuleExecution(...),让 junction 路径和真实 worktree 路径指向同一个模块;同时补回归测试覆盖该场景。 - 验证:
npm run test -- scripts/dev.test.ts scripts/dev-stack-port-utils.test.ts通过后,npm run dev:web -- --web-port 3000 --api-port 8083 --no-interactive应能稳定把0.0.0.0:3000监听起来。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts。
Vitest 定向测试在 Windows junction 工作区要切真实路径
- 现象:同一类 junction 路径问题会让 Vitest 的错误路径和实际工作树不一致,看起来像文件不存在。
- 原因:Vite / Vitest 会把入口 realpath 到真实 worktree 路径;如果命令从 junction 路径传入相对文件参数,入口路径和 resolved id 可能跨盘符不一致。
- 处理:前端定向测试优先从真实路径
F:\DevWorktrees\codex\worktrees\f584\Genarrative运行,不要把这类文件加载失败误判成组件或路由断言失败。 - 验证:同一命令从真实路径执行应正常收集并运行测试。
- 关联:
src/components/puzzle-clear-creation/PuzzleClearWorkspace.test.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/routing/appPageRoutes.test.ts。 - 现象:新增或扩展
*-generating页面后,生成卡只渲染首帧,已耗时/预计等待停在进入页那一刻不动。 - 原因:平台壳层的共享
miniGameGenerationProgressNowMs时钟没有把新生成阶段纳入 tick 条件,或者该阶段的buildMiniGameDraftGenerationProgress(..., nowMs)没有接入同一时钟。 - 处理:任何共享生成页都要通过平台壳层统一的时钟判断和
nowMs传递刷新,新增生成阶段时要同时补selectionStage判定、useEffect依赖和进度调用点。 - 验证:浏览器里进入对应生成页后,
已耗时/预计等待应持续变化,不应停在首帧。
拼消消要用真实可消除判断,不要把“已相邻”当成可解
- 现象:拼消消开局或补牌后会直接出现已完成的图案组,或者
1x2被当成半锁定局部留在场上。 - 原因:早期把可解性写成“场上已经有同组相邻卡”或“只要有一对相邻同组卡就算可解”,这会把已完成盘面误当成合法盘面;同时半锁定规则没有排除
1x2。 - 处理:开局和补牌后的重排必须先排除现成消除,再用真实交换 / 落位模拟判断是否会产生新消除;
1x2永远不进入半锁定组,半锁定只允许1x3、2x2、2x3。 - 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx与cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml -- --nocapture通过后,开局盘面不应直接出现 completed group。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
推荐页作品 key 漏玩法会导致运行内容和标题作者错位
- 现象:移动端推荐页进入跳一跳或敲木鱼等作品时,游戏运行内容已经切到当前作品,但下方标题、作者和头像仍显示第一条拼图或其它推荐作品。
- 原因:平台壳层用
getPlatformPublicGalleryEntryKey(...)写入activeRecommendEntryKey,而RpgEntryHomeView内部的buildPublicGalleryCardKey(...)漏掉新玩法sourceType分支,导致当前 key 查不到条目后回退到推荐列表第一条。 - 处理:推荐页和平台壳层的公开作品 key 规则必须复用
buildPlatformPublicGalleryCardKey(...),覆盖同一批sourceType,至少包括big-fish、puzzle、jump-hop、wooden-fish、match3d、square-hole、visual-novel、bark-battle和edutainment:<templateId>;新增玩法公开推荐流时先补这个共享 helper。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile recommend meta matches active"应覆盖跳一跳和敲木鱼的当前运行内容、标题和作者一致。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳飞行动画不要直接用最新 run 重绘地块窗口
- 现象:跳一跳松手后如果后端很快返回下一帧 run,地块窗口会立刻前移,角色翻腾动画看起来像没播放;若同时刷新图片资产,还可能被误认为地块频闪。
- 原因:后端 run 是规则真相,前端 runtime 又需要低延迟表现。如果 DOM 平台层直接用最新
run.currentPlatformIndex渲染,后端回包会抢在动画前完成视觉切换。 - 处理:前端保留独立
displayRun,松手后先进入isJumpAnimating=true,角色在当前显示窗口内飞向前端预测真实落点;视觉预测必须用当前显示窗口的 current/next 地块作为方向来源,不能拿已经提前返回的后端新 run 目标配旧窗口角色,否则下一跳会朝实际目标反方向飞。飞行动画完成后再把displayRun切到最新后端 run,并进入约1440ms的platformAdvancing表现态。成功后的角色显示必须使用lastJump.landedX/landedY映射出的真实偏移,不要吸附到目标地块中心。推进期间地块层和角色层必须统一包在同一个 camera layer 下移动,旧当前地块先跟随相机偏移离开主视野,之后只保留在屏幕后方;不要给旧地块加独立向上 / 向下飞走 keyframes,也不要因为旧地块还在保留列表里阻塞下一跳。玩家继续向前跳时,已完成旧地块继续被新的相机推进自然带离屏幕,超过离屏阈值后销毁。相机层必须同时设置--jump-hop-camera-shift-x与--jump-hop-camera-shift-y,并以旧窗口真实落点和新窗口真实落点为锚点,避免先横向瞬切居中再纵向推进;运行态相机层当前为约1.3x近距缩放。地块保留当前 / 目标 / 预览的深度尺寸差异,但深度差异必须用固定宽高 + CSS transform scale 缓动实现,不能直接改宽高瞬切;当前态不要额外叠 CSS scale。Three.js Sprite 角色与平台共用同一套屏幕坐标投影,DOM 角色只作为 WebGL 或贴图加载失败 fallback;DOM fallback 在相机推进期间自身不能保留left/toptransition,否则displayRun切换造成的角色局部坐标变更会和父级 camera layer 位移叠加,视觉上像落地后又从屏幕外飞回。正式胜负、成功跳跃次数、时长和排行榜仍以后端 run 为准,前端只延迟显示态。 - 验证:
npm test -- src/services/jump-hop/jumpHopRuntimeModel.test.ts src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖动画期间平台仍停在旧窗口,成功落地保留真实落点偏移,动画结束后进入data-platform-advancing=true,角色 Three 帧沿真实预测落点插值并保留飞行弧线,DOM fallback 角色与地块层同在jump-hop-camera-layer内,通过--jump-hop-camera-shift-x和--jump-hop-camera-shift-y完成相机斜向推进,并校验可见地块按深度保留不同视觉尺寸、运行态平台宽高使用固定基准值、推进态 transform transition 为1440ms、推进态 DOM fallback 角色 transition 不包含left/top、旧地块没有独立jump-hop-platform-exit-driftkeyframes 且下一跳不会被旧地块保留态阻塞。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/services/jump-hop/jumpHopRuntimeModel.ts、server-rs/crates/module-jump-hop/src/application.rs。
跳一跳相机推进不要让地块图片回退到原型方块
- 现象:角色落到下一块后,相机推进时旧地块图片突然消失,或新预览地块先露出浅色原型方块,随后真实 image2 切片才出现。
- 原因:旧地块进入 exiting 状态时如果 React key 从
platformId变成platformId-exiting,图片组件会重新挂载并丢失已加载状态;同时JumpHopTileImage曾在真实图片 URL 已存在但onLoad尚未触发时显示 fallback 原型地块。Three.js 平台层接入后,如果隐藏预加载只让浏览器缓存<img>,但没有把未来platformId的纹理 URL 写入platformTextureUrlsByRenderKey,相机推进时新预览地块会短暂缺 Three 贴图;若旧 blob 贴图在空 URL 回调时先被 revoke,再继续保留在 state 中,也会留下一个看似 ready、实际已失效的贴图地址。 - 处理:exiting 地块继续使用稳定
platformIdkey,让旧图片组件在推进期复用;有真实resolvedUrl且未错误时直接保留真实<img>,只在无 URL 或加载失败时显示 fallback;当前 3 块之外的后续地块通过隐藏预加载图片提前解析签名 URL 和浏览器缓存,并同步按未来platformId发布 Three 纹理 URL。Three 平台层在当前 render items 全部有贴图 URL 后继续承接包含 exiting 地块在内的 3D 渲染;退出地块只随相机推进自然离屏,不播放独立飞走动画,避免退出期露出被放大的平面贴图或重复飞多次;贴图 URL 替换必须等新 URL 到达后再释放旧 parent-owned blob,空 URL 回调不得清空或 revoke 仍在活跃 / 预加载 key 上的旧贴图。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/services/jump-hop/jumpHopRuntimeModel.test.ts应覆盖真实 tile URL 不露出.jump-hop-runtime__fallback-tile,并存在jump-hop-tile-preload-image。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx。
跳一跳 Three.js 平台层不能左右镜像 DOM 坐标
- 现象:视觉上下一块地块在角色右侧,但蓄力引导和角色飞行动画朝左侧;后端回包后地块窗口又闪现摆回正确位置,像是先按反方向飞、再由快照刷新纠正。
- 原因:Three.js 平台层如果把相机
up设置成反向,或在 Three 容器上做左右镜像,会让 WebGL 地块的屏幕 X 轴和角色 / 落点预测的屏幕 X 轴相反。规则层仍沿当前地块中心到下一块中心裁决,所以后端快照会把状态纠正回来,表现为跳后刷新。 - 处理:Three 相机保持
up=(0, 1, 0),再用内部投影公式抵消 45° 下压导致的 Y 轴压缩;不要通过反向camera.up解决上下方向。Three.js Sprite 角色、DOM fallback 角色、蓄力引导、落点预测和 Three 平台层必须共用同向屏幕坐标。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/services/jump-hop/jumpHopRuntimeModel.test.ts应覆盖JUMP_HOP_THREE_CAMERA_UP_Y=1,并断言 Three 投影与 DOM 屏幕坐标同向。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx。
跳一跳 Three.js 角色不要被地块透明排序压住
- 现象:角色已经进 Three.js 场景后,看起来像落在地块内部或只露出头,角色没有站在方块顶面上。
- 原因:地块材质如果设置
transparent=true会进入 Three.js 透明物体排序队列,可能在 Sprite 角色之后绘制;同时角色脚点如果仍用固定 Z 高度,遇到标准1x1x1方块放大后的当前块时会落到顶面后方或方块体内。 - 处理:地块贴图材质只使用
alphaTest裁掉透明边,不放入透明材质队列;角色 Sprite 的renderOrder必须高于平台 mesh,脚点 Z 高度按最近方块半高加顶面偏移计算,确保角色站在当前方块顶面上方。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖平台材质不透明队列、角色 renderOrder 高于地块、角色脚点高度高于方块顶面。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md。
跳一跳立方体贴图不要走透明主体切片
- 现象:水果等主题生成成功后,运行态地块看起来像薄的纯水果 PNG、果切贴纸、透明 cutout;或者反过来六个面都是同一张平铺果皮 / 果肉材质,无法组合成方块苹果 / 方块香蕉这类完整主题对象表达。
- 原因:跳一跳地板已经改为 Three.js 标准
1x1x1等比极小倒角立方体复用几何体,运行态视角固定为近距相机和 45° 下压视角;image2 应生成1024x1536的 18 个 cube object UV unwrap,每个大单元内的 top/front/right/back/left/bottom 六面要共同包装同一个主题物体。只强调 full-bleed 容易让水果主题退化成果皮、果肉、叶脉等表面纹理;如果仍把一张图贴给六个面,模型也不需要理解正反和跨面连续特征。旧切图链路若把洋红 key 转 alpha、裁边、只保留最大 alpha 连通主体并补透明安全边,会把整格贴图重新抠成苹果 / 香蕉 / 果切等居中主体,贴到立方体上后四角和侧面都变透明。 - 处理:跳一跳地板图集 prompt 固定要求
cube object UV unwrap atlas / 立方体主题物体六面展开图集,一张图只生成 18 个大单元,每个大单元固定4列*3行UV 网:第 1 行第 2 列 top,第 2 行 left/front/right/back,第 3 行第 2 列 bottom;水果主题要明确生成能一眼说出名称的方块苹果、方块香蕉、方块橙子、方块西瓜等可识别对象,并要求果柄叶片、剥皮条带、放射切面、红瓤黑籽等身份特征跨面连续。禁止自然圆形水果、自然长条香蕉、非方块化完整水果、果切小贴纸、居中小物体、透明背景和留白,同时也禁止“单纯平铺材质 / 抽象纹理 / 只铺主题颜色 / 纯果皮材质 / 纯果肉纹理 / 纯叶脉纹理”。后端先对图集做洋红去背,再以jump_hop_atlas_slicing.rs的自适应 blob+gradient 算法检测 3x6 大单元和单元内六面区域,输出 108 张256x256不透明面贴图;固定 3x6 / 4x3 切片只作为测试对照和必要 fallback 参考,不作为优先生图切图路径。洋红#FF00FF只作为图集安全缝 / UV 空位 / 外圈 key 色;绿色、白色、雪地、云朵、草地、花朵、果肉粉色和浅黄色等主题颜色必须完整保留。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml -- --nocapture覆盖跳一跳 UV unwrap prompt、18 个大单元、108 张不透明面贴图、绿色 / 白色材质不被透明化、洋红 key 残留不作为透明洞;前端JumpHopRuntimeShell测试覆盖新 UV 资产会解析六张面贴图,旧单贴图资产仍可 fallback。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs、server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs、server-rs/crates/api-server/src/jump_hop.rs。
跳一跳 UV 图集切片要防贴边矩形 u32 中间溢出
- 现象:跳一跳草稿在背景、返回按钮和地板图集 image2 都生成成功后,前端报“执行跳一跳共创操作失败”,Vite 代理日志出现
socket hang up,后端日志出现jump_hop_atlas_slicing.rs内attempt to subtract with overflow。 - 原因:blob gradient 切片的 histogram 最大不透明矩形在计算顶部坐标时写成
by0 + ly - sh + 1。当模型输出的 UV 面内容刚好贴到 cell 顶边,数学结果本应是 0,但u32会先执行中间步骤0 - 1并在 debug 运行时 panic。 - 处理:顶部坐标先在局部坐标内用
ly.saturating_add(1).saturating_sub(sh)计算,再加 block 偏移;不要恢复成连写减法。补充贴顶两行不透明矩形回归测试,保证贴边 UV 面不会打崩共创接口。 - 验证:
RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop_atlas_slicing::tests::max_opaque_rect_handles_content_touching_top_edge;整组再跑RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop。 - 关联:
server-rs/crates/api-server/src/jump_hop_atlas_slicing.rs、server-rs/crates/api-server/src/jump_hop.rs。
跳一跳生图切图主路径不要绕过自适应图集切片
- 现象:拉取
fix/jump-hop-image-gen后,如果又把生成链路切回旧固定坐标裁切,容易和该分支解决的 AI 图集偏移、间距不均、UV 面位置漂移问题互相抵消,导致新生图链路的实际收益无法验证。 - 原因:当前跳一跳 image2 prompt 仍要求 3x6 大单元和 4x3 UV 子网格,这是给模型和算法的结构约束;真实生产切图由自适应
SeedRefinement + blob + gradient + max opaque rectangle链路消化 AI 输出偏差。固定网格切片只能验证理想图集,不适合覆盖新分支的主修复。 - 处理:生产生成链路优先调用
slice_tile_atlas_adaptive(...);旧固定slice_jump_hop_tile_atlas(...)只保留为对照测试、实验和必要 fallback 参考。若自适应切图出现具体误切,应优先修正自适应模块的边界检测、主 blob、透明/安全色处理和回归测试,而不是直接全局切回固定坐标。 - 验证:新生成作品下载
tile-01-top/front/right等面贴图时,单图应基本充满对应主题面内容,不应出现大块空背景、相邻面混入或纯色原型 cube;同时执行RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop_atlas_slicing -- --nocapture。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/api-server/src/jump_hop_atlas_slicing.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md。
含中文 image2 live 验证不要用 PowerShell 管道喂 Node 源码
- 现象:本地用
@'...'@ | node -跑 VectorEngine / gpt-image-2 live 验证时,request.json里的中文 prompt 可能全部变成????,生成图会变成完全不相关的 UI、建筑海报或其它随机内容,容易误判为模型不服从提示词。 - 原因:Windows PowerShell 管道到 Node stdin 时可能按本机非 UTF-8 编码传输脚本文本,JS 源码里的中文字符串在进入 Node 前已经损坏;Rust 后端真实请求不会走这条编码路径。
- 处理:含中文提示词的 live 验证优先写成 UTF-8
.mjs文件再执行,或使用能确认 UTF-8 的运行入口;执行后先检查本次request.json是否保留真实中文,再判断生图质量。不要基于????prompt 生成的图片调整项目提示词。 - 验证:生成前后检查
request.json,其中prompt字段应显示中文而不是问号;同一提示词在 UTF-8 文件脚本下应能得到符合主题的图。 - 关联:
.codex/skills/gpt-image-2-apimart/SKILL.md、server-rs/crates/api-server/src/jump_hop.rs。
Tauri devUrl 不会自动跟随 dev:web 端口漂移
- 现象:运行
npm run desktop-shell:dev时终端显示主站 Vite 实际启动在10000+端口,但 Tauri 窗口仍加载http://127.0.0.1:3000/,桌面壳表现为白屏、连接失败或加载到旧页面。 - 原因:Linux dev 端口段只把 CLI
--web-port视为显式端口;桌面壳 package script 里的WEB_PORT=3000会被端口段映射覆盖。TauridevUrl是静态配置,不会读取scripts/dev.mjs最终解析出的漂移端口。 - 处理:桌面壳
beforeDevCommand必须使用npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port,让 Vite 实际监听端口和 TauridevUrl一致,并在 3000 被占用时直接失败。若 3000 被占用,先释放占用进程再启动桌面壳,不要依赖 Vite 漂移。 - 验证:
npm run test -- scripts/dev.test.ts -t "Linux 桌面壳显式指定 web-port"、npm run desktop-shell:typecheck、实际启动时终端应显示[dev] web: http://127.0.0.1:3000。 - 关联:
apps/desktop-shell/src-tauri/tauri.conf.json、apps/desktop-shell/scripts/check-config.mjs、scripts/dev.mjs。
Tauri 手动创建主窗口时 devUrl 不会自动套到 index.html
- 现象:
npm run desktop-shell:dev启动后窗口地址显示http://tauri.localhost/index.html或tauri://localhost/index.html,即使 Vite 已经在http://127.0.0.1:3000/正常监听。 - 原因:桌面壳为了注册导航、下载、生命周期和托盘行为,把 Tauri 配置里的主窗口设为
create=false,再在 Rustapp.rs中用WebviewWindowBuilder::from_config(...)手动创建窗口。此时如果只读取app.windows[].url = index.html并补 HostBridge query,手动窗口会沿 release 入口走打包资源协议;Tauri CLI 的build.devUrl不会自动替换这份手动克隆后的窗口 URL。 - 处理:
app.rs在 dev build 下必须先把主窗口 URL 替换为config.build.dev_url,再调用desktop_window_config_with_runtime_platform(...)补写宿主上下文;shell/navigation.rs也必须允许 dev build 下的http://127.0.0.1:3000留在 WebView 内,不要把自己的 Vite 首页当外链交给系统浏览器。release build 保持index.html打包入口。 - 验证:
cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml desktop_main_window_config_uses_dev_url_in_dev_builds desktop_webview_navigation_stays_on_packaged_or_same_origin_pages,实际启动时窗口应加载http://127.0.0.1:3000/...而不是tauri.localhost/index.html。 - 关联:
apps/desktop-shell/src-tauri/src/app.rs、apps/desktop-shell/src-tauri/src/shell/url.rs、apps/desktop-shell/src-tauri/src/shell/navigation.rs、apps/desktop-shell/src-tauri/tauri.conf.json。
Tauri release 的 tauri.localhost 不要交给系统浏览器
- 现象:Windows / release 包启动桌面壳时,系统默认浏览器被打开到
http://tauri.localhost/index.html。 - 原因:release 打包资源在 WebView 内可能表现为
tauri://localhost/index.html、https://tauri.localhost/index.html或http://tauri.localhost/index.html;如果导航白名单只允许tauri:和https://*.localhost,http://tauri.localhost会被误判成普通外链并交给opener.open_url。 - 处理:桌面壳导航策略必须把
http/https的*.localhost都视为 Tauri 内部打包资源,只允许真正外部http/https、mailto、tel走系统浏览器。Windows release 入口还必须使用windows_subsystem = "windows",避免正式包额外弹出控制台窗口;dev build 保留控制台日志。 - 验证:
cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml desktop_webview_navigation_stays_on_packaged_or_same_origin_pages、npm run desktop-shell:typecheck、Windows release 启动时不应打开系统浏览器或控制台窗口。 - 关联:
apps/desktop-shell/src-tauri/src/main.rs、apps/desktop-shell/src-tauri/src/shell/navigation.rs、apps/desktop-shell/scripts/check-config.mjs。
自动试玩退出不要回到生成页
- 现象:拼图草稿生成完成后自动进入试玩,用户从试玩退出或使用系统返回时落回生成进度页,页面还暴露“重新生成”按钮。
- 原因:自动试玩前如果没有先把
/creation/puzzle/result写成/runtime/puzzle的浏览器历史前一站,系统返回会命中旧的生成页历史项;仅靠运行态内部returnStage='puzzle-result'只能覆盖运行态按钮返回,不能覆盖浏览器 / WebView 系统返回。 - 处理:所有“生成完成后自动进入草稿试玩”的分支在
openPuzzleRuntimeStage(...)前都必须调用结果页历史写入 helper,把/creation/puzzle/result与当前sessionId/profileId/workId写入历史;运行态按钮返回到puzzle-result时也同步写回创作恢复 query。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial and runtime back opens draft result"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
推荐页 ready 不能只等主图或首次 DOM 图片
- 现象:移动端推荐页卡面遮罩在作品主图加载后就渐隐,但游戏内 UI 图集、背景、道具图或换签中的 generated 图片还没有准备好,用户会看到运行态半成品或资源闪入。
- 原因:推荐页 ready probe 如果只扫描首次挂载时已有的
<img>,就会漏掉 React effect、/api/assets/read-url换签、spritesheet 解析或后续 state 更新才新增的资源。 - 处理:推荐页 runtime 遮罩必须持续观察运行态 DOM 内新增图片、内联
background-image和data-runtime-resource-pending隐藏标记;各玩法对换签中、解析中的资源源头要暴露 pending 标记,失败后释放标记并交给玩法兜底,避免遮罩永久卡住。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile recommend cover waits for async runtime resources beyond the main image|mobile recommend cover waits until runtime images are ready"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/common/RuntimeResourcePendingMarker.tsx、src/components/ResolvedAssetImage.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图文字直创的 compile 回包不等于生成完成
- 现象:只输入文字点击生成拼图时,页面刚进入生成页就弹出“生成任务已完成,可以继续查看草稿。”,随后又提示“请先选择一张正式拼图图片。”,结果页关卡里也没有图。
- 原因:统一创作表单路径把
compile_puzzle_draft的同步回包无条件当成 ready;但后端在 AI 重绘路径会先返回stage=image_refining、progressPercent=88的会话,只表示首关草稿已编译且后台首图 / UI 资产任务已启动,还没有正式封面或候选图。 - 处理:前端必须继续用
isPuzzleCompileActionReady(...)判断回包 session;没有draft.coverImageSrc、首关coverImageSrc或候选图时保持生成中,不弹完成、不把作品架 pending 标 ready、不自动试玩。生成页轮询合并 session 进度时,未进入编译态或进度无变化就返回原 state,避免轮询制造重复 render。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle text-only form stays generating|puzzle draft generation auto starts trial|running puzzle draft opens generation progress"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
CreativeImageInputPanel 主图点击默认预览
- 现象:复用
CreativeImageInputPanel的结果页 / 编辑页已有主图时,用户点击图片却触发上传,无法直接查看大图;不同玩法若各自手写上传按钮会让主图、历史图、AI 重绘和参考图行为再次分叉。 - 原因:旧主图卡整卡是上传 label,缺少主图预览模式和上传 / 历史入口的显式控制参数。
- 处理:通用面板已有主图时默认点击主图打开全屏预览,上传 / 更换收口到右下角
ImagePlus图标按钮;无图时仍允许点击空图卡上传。调用方用canUploadMainImage和canUseImageHistory分别控制上传与历史按钮,不要复制面板或用样式遮挡按钮。 - 验证:
npm run test -- src/components/common/CreativeImageInputPanel.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx。 - 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
项目画布跳转不要先写无参画布路由
- 现象:从
/creation最近项目或/project项目卡进入画布时,浏览器先进入/editor/canvas,随后再进入/editor/canvas?projectid=xxx,导致返回来源页需要点两次。 - 原因:
App传给平台壳的setSelectionStage会按 stage 自动pushAppHistoryPath(resolvePathForSelectionStage(stage));如果项目入口先setSelectionStage('image-editor')再写项目 URL,就会把无参数画布路由塞入 history。 - 处理:项目入口必须先写入最终
/editor/canvas?projectid=xxx,再切image-editor阶段;App的 stage setter 在当前位置已经解析为image-editor时不要再补写基础画布路由。 - 验证:
npm run test -- src/App.test.tsx;浏览器中从最近项目或项目页打开项目后,后退一次应直接回到/creation或/project。 - 关联:
src/App.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md。
统一创作页短表单软键盘打开不要露出黑底
- 现象:小程序 / H5 移动端点击拼图或敲木鱼创作输入框后,输入框和键盘之间出现一大片黑色区域;H5 还会明显弹一下。跳一跳因为按钮区用
mt-auto撑开页面,看起来没有同样问题。 - 原因:旧移动键盘处理会用
--platform-keyboard-focus-offset把.platform-viewport-shell整体上移;但 H5 浏览器和小程序web-view已会自行处理输入框可见性,二次整体上移会造成页面弹跳并露出body或原生page的黑色宿主底色。统一创作短表单若内容区按短内容收缩,也会放大这个黑底暴露。 - 处理:
UnifiedCreationPage根容器必须保留bg-[image:var(--platform-body-fill)]和overscroll-contain,内容区必须用flex-1 min-h-0占满统一页剩余高度;移动端键盘打开时只记录data-mobile-keyboard-open、隐藏底部 dock、设置键盘 inset 和浅色--platform-keyboard-exposed-fill,不要再对.platform-viewport-shell做全局transform;小程序pages/web-view的page和 web-view class 也要用浅色背景。不要只给某个玩法工作台单独加高度补丁。 - 验证:
npm run test -- src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedCreationWorkspace.test.tsx src/mobileViewportKeyboardFocus.test.ts src/index.test.ts miniprogram/pages/web-view/index.style.test.js;移动端点击拼图、敲木鱼、跳一跳输入框时,页面不应整体弹起,键盘上方应持续显示平台浅色背景。 - 关联:
src/components/unified-creation/UnifiedCreationPage.tsx、src/mobileViewportKeyboardFocus.ts、src/index.css、miniprogram/pages/web-view/index.wxml、miniprogram/pages/web-view/index.wxss、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
小程序订阅消息授权不要依赖 web-view bindmessage
2026-07-18:本节及下一节只作为历史记录。生成结果订阅页、H5 service、HostBridge capability 和后端发送链路已随旧创作模板业务退役,不得按这些排障步骤恢复。
- 现象:拼图点击生成后,H5 以为已经请求了生成结果订阅授权,但小程序没有弹出
wx.requestSubscribeMessage授权框。 - 原因:
web-view bindmessage/wx.miniProgram.postMessage不适合承接“当前用户点击后立刻请求授权”的时序,消息可能等到 web-view 后退、分享或销毁时才派发,导致授权请求没有发生在compile_puzzle_draft前。 - 处理:不要在原生页
onLoad自动触发wx.requestSubscribeMessage,真机会闪页返回且不弹授权框。H5 在compile_puzzle_draft前应先进入生成进度态并立即发起生成 action,再通过微信 JS SDKminiProgram.navigateTo非阻塞跳转到小程序原生订阅页尝试请求授权;用户接受、拒绝或返回都不能阻塞生成。原生页不要改写上一页webViewUrl,否则 web-view 可能重新加载首页并丢失进度页状态。后端发送订阅消息仍只允许在拼图资产成功或失败终态后执行。 - 验证:
npm run test -- src/services/wechatMiniProgramSubscribe.test.ts miniprogram/pages/subscribe-message/index.test.js。 - 关联:
src/services/wechatMiniProgramSubscribe.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、miniprogram/pages/subscribe-message/index.shared.js、miniprogram/pages/web-view/index.js。
微信订阅消息 time 字段不能用内部时间戳
2026-07-18:该能力已退役,本节不再作为现役排障入口。
- 现象:dev 服务器拼图资产生成终态后已经调用订阅消息发送,但日志出现
微信订阅消息发送失败:argument invalid! data.time4.value invalid,用户收不到生成结果通知。 - 原因:微信模板
time字段不接受内部微秒时间戳、秒级时间戳或带Z/ 时区后缀的字符串;发送1713686401.234567Z或类似2026-06-08 08:09:18Z会被微信拒绝。 - 处理:
api-server构造生成结果订阅消息时,time4固定格式化为北京时间YYYY-MM-DD HH:mm;不要复用shared_kernel::format_timestamp_micros。 - 验证:
cargo test --manifest-path server-rs\Cargo.toml -p api-server generation_result_template -- --nocapture;dev 日志中不应再出现data.time4.value invalid。 - 关联:
server-rs/crates/api-server/src/wechat_subscribe_message.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
待解决:跳一跳生成超时后可能后台继续成功
- 风险程度:高。
- 现象:跳一跳生成页可能在
98% 写入正式草稿后报“请求超时,请稍后重试”,但后端仍在继续生成,稍后才把同一 session 写成DraftCompiled=100。2026-06-08 排查jump-hop-session-6db8fa7af57c4fa2a71e6430cc808412时,背景底图 image2 成功但耗时约18分25秒,返回按钮约2分44秒,地板图集约1分46秒,总耗时超过前端 20 分钟等待窗口,最终在前端超时后约 3 分钟写草稿成功。 - 原因:跳一跳创作链路仍把背景、返回按钮、地板图集、切片和 OSS 写入串在一次 HTTP 请求里;VectorEngine image2 单步 timeout/connect 失败会在后端重试,单步耗时可能超过前端总等待窗口。中间资产和真实阶段没有落库,session 在完成前仍显示
Collecting、progress_percent=0,前端只能按时间显示假进度;超时后重试同一 session 时,后端还可能因为 session 没有中间素材而重新从背景开始生成。 - 待处理:将跳一跳生成改为后端任务化 / 可轮询真实阶段进度,按背景、返回按钮、图集、切片、持久化、写草稿分阶段落库;统一后端全局生成 deadline、VectorEngine 重试预算、前端等待窗口和失败态回写。超时后再次进入同一 session 应优先恢复正在运行或已完成的任务,不应重复生图。
- 验证:模拟首张 image2 超长耗时或超时重试时,生成页应显示真实阶段和可恢复状态;前端请求超时不应把最终成功草稿标记为失败;刷新
/creation/jump-hop/generating?sessionId=<id>后应能恢复到后端真实状态;同一 session 重试不得重复生成已完成阶段。 - 关联:
src/services/jump-hop/jumpHopClient.ts、src/services/miniGameDraftGenerationProgress.ts、server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/platform-image/src/vector_engine/client.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
画布生成完成态不能被旧 autosave 覆盖
- 现象:release 外部生成 worker 补跑完成后,生成图已进入素材库或项目资源,但画布生成器仍显示
generating;刷新后可能仍看到历史生成框卡住。 - 原因:画布前端在提交生成后会把
generatinglayout 放入 450ms 自动保存队列;worker 完成后后端会写入idle + generatedLayerId + 生成层,但旧的 pending / in-flight layout save 可能晚到并覆盖完成态。另有历史 inline 请求在 api-server 重启时只留下前端已保存的generating框,没有终态任务或生成资源。 - 处理:前端
applyProjectSnapshot必须取消 pending layout save,并跳过一次由后端快照恢复触发的 autosave;后端save_editor_project_layout要保护已完成的 generation dialog,如果传入旧generating且无generatedLayerId,而当前 layout 已有同一 dialog 的完成态,则保留完成态和生成层。线上脏数据只在确认无任务 / 无资源时标成failed并保留原 prompt 供用户重试。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx;cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml editor_project_storage --lib;release 排障用list_editor_projects_and_return/get_editor_project_and_return查generation-dialog状态,不要只看素材库。 - 关联:
src/components/image-editor/useImageCanvasProjectPersistence.ts、server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/api-server/src/external_generation_worker.rs。
Pingora 静态缓存不能只写 Cache-Control
- 现象:直连 Pingora 后,HTML 入口虽然是
Cache-Control: no-cache,但浏览器每次都重新下载完整入口页或普通静态文件;或者 Vite 指纹资源长期缓存正常,但旧标签页刷新时协商缓存行为和 Nginx 直连不同。 - 原因:
Cache-Control只决定缓存策略,不等于条件请求能力。Nginx 静态文件默认会按文件 metadata 提供ETag/Last-Modified,浏览器随后可用If-None-Match/If-Modified-Since得到304;Pingora 自实现静态读取时如果只写 body 和Cache-Control,就会丢掉这层协商缓存。 - 处理:Pingora 静态响应读取文件 metadata,写入弱
ETag和Last-Modified;GET/HEAD命中If-None-Match或If-Modified-Since时直接返回304,不读取或发送 body。HEAD静态响应只读 metadata,仍写正确Content-Length。 - 验证:
npm run check:pingora-gateway-smoke必须覆盖静态HEAD、If-None-Match304、If-Modified-Since304,并用 access log method/path/status 对账证明本地静态边界进入日志证据链;cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml必须覆盖 ETag 构造和匹配 helper。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 静态路径必须按 URL segment 解码
- 现象:dev 页面里部分像素图标加载失败,浏览器直接打开
/Icons/Admurin%27s%20Pixel%20Items/.../499_Iron_Gear.png返回200 text/html,响应体是主站index.html,但服务器磁盘上真实 PNG 文件存在。 - 原因:浏览器请求中的空格和英文撇号会变成
%20/%27;Pingora 静态文件解析如果直接把编码后的 path 当磁盘路径查找,就会错过真实文件,并继续落到 SPA fallback,最终让图片解码看到 HTML。 - 处理:静态路径按
/拆分 URL segment 后逐段 percent-decode;解码后拒绝/、\、NUL、..和非法%编码,既能读取带空格 / 撇号的真实文件,又不重新打开目录穿越边界。 - 验证:
cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml必须覆盖编码空格 / 撇号、%2e%2e、%2f和非法%GG;npm run check:pingora-gateway-smoke必须覆盖编码图标路径返回image/png,并确认危险编码路径仍返回404。dev 切换后用浏览器或 curl 直接验证对应图标 URL 的Content-Type和 PNG magic bytes。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
dev health patrol 不能缺少公网 HTTPS 入口配置
- 现象:dev 上
genarrative-health-patrol.timer正常 active,但genarrative-health-patrol.service最近一次运行失败;Pingora 直连彩排状态脚本只因/etc/genarrative/health-patrol.env缺失或 public probe 命中http://127.0.0.1后被 Nginx 301 而报CRITICAL。 - 原因:health patrol systemd unit 的
EnvironmentFile=-/etc/genarrative/health-patrol.env允许文件缺失,脚本会退回默认 public base URLhttp://127.0.0.1;dev / release 的 Nginx 公开入口会把 HTTP 跳到 HTTPS,巡检按非 2xx 判失败。 - 处理:目标机应创建
/etc/genarrative/health-patrol.env,保持GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx,把GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL指向真实 HTTPS 域名,例如https://dev.genarrative.world;Pingora shadow 巡检同时配置GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL=http://127.0.0.1:18081和与/etc/genarrative/pingora-gateway.env一致的 probe token。不要为了让彩排状态变绿把缺 env 降级成 warning。 - 验证:先运行随包
node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs --env-file /etc/genarrative/health-patrol.env --expected-gateway-mode nginx --expected-public-base-url https://dev.genarrative.world --require-empty-public-host,再systemctl start genarrative-health-patrol.service;最后运行node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs --release-root /opt/genarrative/current --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical。 - 关联:
deploy/env/health-patrol.env.example、scripts/ops/production-health-patrol.mjs、scripts/ops/pingora-direct-rehearsal-status.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 高端口直连演练不要 source env 文件
- 现象:在 dev 上用临时 env 启动高端口 Pingora direct 演练时,shell 报
/tmp/pingora-direct-highport-*.env: line ...: max-age=31536000,: command not found,或者临时演练进程启动后没有按预期监听18443/18080。 - 原因:
pingora-gateway.env是 systemd EnvironmentFile 口径,允许GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutable这类带空格的值;它不是可安全source的 shell 脚本。用 shellsource会把空格后的内容拆成命令或参数。另一个容易误判的点是 Pingora 默认优雅退出窗口较长,停止临时 systemd unit 后可能短暂停在stop-sigterm,即使监听端口已经释放。 - 处理:高端口真实演练优先用临时 systemd unit 启动 current release 的
/opt/genarrative/current/pingora-gateway,通过systemd-run --property=EnvironmentFile=/tmp/<run>.env --property=User=genarrative --property=WorkingDirectory=/opt/genarrative/current ...让 systemd 解析 env;或使用显式安全 env 解析器,禁止直接source。临时 env 要把正式 shadow 端口改到独立 loopback 端口,例如127.0.0.1:18084,HTTPS / HTTP redirect 用127.0.0.1:18443/127.0.0.1:18080,access log 写独立文件。演练结束先systemctl stop <临时unit>,再用ss -ltnp确认高端口已释放;若临时 unit 仍停在deactivating/stop-sigterm且只剩演练进程,可对该临时 unit 执行systemctl kill -s SIGKILL <临时unit>收尾,不要碰正式genarrative-pingora-gateway.service。 - 处理补充:正式
plan:pingora-direct-cutover/check-pingora-release-readiness.mjs --dry-run-cutover --require-direct生成的 runbook 默认读取 active/etc/genarrative/pingora-gateway.env,不会自动使用/tmp候选 env。若只生成了候选 direct env,必须先在维护窗口内把候选 env 提升为 active env,并确认 Nginx 已释放80/443,再执行 runbook 的 direct preflight、enable dry-run 和 enable apply;否则 runbook 第 5 步仍会按 shadow env 报缺TLS_LISTEN、HTTP_REDIRECT_LISTEN、cert/key、FORWARDED_PROTO=https以及 direct-entry capability。不要把候选 env 的 loopback / 高端口预检通过误解为 active env 已满足正式直连门禁。 - 验证:先跑
check-pingora-direct-preflight.mjs --env-file <临时env> --require-live-env --check-cert-readable --check-service-user-cert-readable --check-ports-free --allow-loopback-only;启动临时 unit 后跑check-pingora-direct-live.mjs --https-base-url https://127.0.0.1:18443 --http-base-url http://127.0.0.1:18080 --host <域名> --redirect-host <域名> --redirect-base-url https://<域名> --require-wss-upgrade --pingora-access-log <临时log> --insecure-tls --json,要求OK且direct-access-log matchedCount == checked。收尾后复核80/443仍由 Nginx 监听,正式 Pingora shadow 仍为127.0.0.1:18081。 - 关联:
scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 直连接管同 IP 多域名前先确认 Host 和证书覆盖
- 现象:dev 上准备让 Pingora 直接绑定
0.0.0.0:80/443时,只按dev.genarrative.world配置证书和路由会让同 IP 的git.genarrative.world也进入主站 Pingora 路由,Gitea 可能不可访问;即使补了 Gitea Host 路由,如果仍使用只覆盖dev.genarrative.world的单域名证书,浏览器和 Git 客户端访问git.genarrative.world也会遇到证书域名不匹配。 - 原因:Nginx 原来通过多个
server_namevhost 承载主站和 Gitea;当前 Pingora direct listener 默认只有一组 TLS cert/key,且路径路由本身无法区分同一 IP 上的多个域名。 - 处理:direct env 必须配置
GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.world与GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000;TLS 证书必须同时覆盖dev.genarrative.world和git.genarrative.world,并通过pingora-tls-cert-sync.mjs同步到 Pingora 私有目录后再指向 env。不要通过“临时释放 Gitea vhost”把 Gitea 从切换窗口里牺牲掉。 - 验证:本地
npm run check:pingora-gateway-smoke必须覆盖 Gitea Host 整站转发、维护模式不拦截 Gitea Host 和 access logproxy_target=Gitea;dev 切换后除https://dev.genarrative.world/外,还必须验证https://git.genarrative.world/返回 Gitea,HTTP 到 HTTPS redirect 保留正确 Host,Pingora access log 中有host=git.genarrative.world/proxy_target=Gitea。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
SpacetimeDB 连接池租约必须有 Drop 兜底,acquire 不允许无界自旋
- 现象:release 上 api-server 周期性出现全量
spacetime_stage="pool_acquire" elapsed_ms=45000业务超时,/readyz503(reason=spacetime_unhealthy, stage=pool_acquire),/healthz仍 200,只有重启能恢复,过若干小时复发。 - 原因:旧
PooledConnectionLease只能显式release_connection归还;HTTP 请求方在等待 StDB 回包期间断开时 handler future 被取消,permit 自动归还但槽位in_use永不复位。后续 acquire 在拿到 permit 后进入无界loop + yield_now扫描空闲槽位,泄漏积累到 pool_size 后整池挂死。 - 处理:租约持有
Arc<SpacetimeConnectionPool>并实现Drop统一复位槽位/归还连接;槽位改AtomicBoolCAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。该保证只覆盖本地 lease / slot / permit 回收;RPC 已发出后,handler timeout/drop 不会取消或回滚远端 procedure,结果仍须按 unknown 读取权威事实。 - 验证:
cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib(dropped_lease_releases_slot_and_permit、acquire_times_out_at_pool_acquire_when_pool_is_busy)。 - 关联:
server-rs/crates/spacetime-client/src/active.rs、docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md。
后台灰度配置不能从 SpacetimeDB 本地表缓存读取
- 现象:后台灰度页保存
image-editor:agent-sidebar后当前响应能看到 gate,但刷新后台页列表变空;前台画布 Agent 入口仍显示,0% 灰度没有生效。 - 原因:
feature_gate_config是后台私有事实表,spacetime-client如果优先读 SDK 本地订阅表缓存,可能得到空表并覆盖 procedure 返回后的正确缓存。灰度语义里“未配置 gate”表示不限制访问,所以空列表会让功能继续开放。 - 处理:灰度配置读取必须走
get_feature_gate_configprocedure 的事务快照,成功后再更新进程缓存;缓存只作为 procedure 暂时失败后的兜底。不要订阅或读取feature_gate_config本地表来判断后台配置。 - 验证:
RUSTC_WRAPPER= cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml;RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml frontend_runtime_config_denies_anonymous_agent_sidebar_when_gate_enabled;RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent_api_returns_service_unavailable_when_sidebar_gate_denies_user。 - 关联:
server-rs/crates/spacetime-client/src/runtime.rs、server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/api-server/src/frontend_runtime_config.rs、server-rs/crates/api-server/src/editor_agent.rs。
后台灰度新 target 不能继承旧规则
- 现象:管理员先点开一条已有 gate,再从两段式下拉框选择一个尚不存在的新 target,保存后新 gate 可能带着上一条 gate 的启用状态、灰度比例和黑白名单。
- 原因:新 target 分支如果只更新 gate key,会复用当前 React 表单状态;这些字段对运营不可见地跨 target 泄漏。
- 处理:
applyGateTarget进入不存在的新 target 时必须重置为新建态:enabled=false、rolloutPercent=0、allow / deny 列表为空,并使用 target 默认描述。只有显式点已有 gate 才fillForm复制服务端规则。 - 验证:
npm run test -- apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx。 - 关联:
apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx、apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx。
背景色决策喂 gpt-5-mini 的图不必按阿里云抠图那样归一化
- 现象:担心带图背景色决策把源角色图原样 base64 塞给 gpt-5-mini(
resolve_media_source_as_data_url不做 resize / 字节上限),会像阿里云通用抠图那样因超尺寸 / 超体积被上游拒绝,于是想给决策链路也补一套图片归一化。 - 原因:两条链路的上游限制完全不同。阿里云 SegmentCommonImage 有硬限制(≤3MB、分辨率 <2000×2000、最长边 ≤1999),必须归一化;而 gpt-5-mini(经 VectorEngine
/v1/responses,Responses 协议 +input_image)对图片输入宽松得多,实测远超 App 真实源图范围仍全部 HTTP 200:纯色图到 5000×5000(隔离像素维度)正常识别;噪声图到 base64 请求体 34MB(隔离字节维度,PNG 25.8MB)仍成功返回。App 真实源角色图一般 ≤2048px、几 MB,稳落在安全区。 - 处理:不要给
resolve_editor_screen_background_color的带图路径加图片归一化——那是阿里云抠图链路(platform-matting)专属需求,两者别混。真要加保护也应放在字节 / 像素远高于当前实测通过档(如 base64 >40MB 或长边 >6000px)才截断,避免无谓重编码开销与画质损失。 - 验证:探针脚本
Myscripts/probe_gpt5mini_image_limits.py(本地不入库,逐级放大纯色 / 噪声图打/v1/responses,记录 HTTP 状态与响应)。2026-07-10 实测:solid 512²~5000² 全 200;noise 900²(4.1MB)~2600²(34.4MB) 全 200,无拒绝阈值出现在实用范围内。 - 关联:
server-rs/crates/api-server/src/character_animation_assets.rs(resolve_media_source_as_data_url)、server-rs/crates/api-server/src/editor_screen_background_decision.rs、server-rs/crates/platform-matting/src/lib.rs(对照:阿里云输入归一化)。
不要把 BgFilter segModel 暴露为外部可选参数
- 现象:看到
EditorImageGenerationRequest、EditorIconSpritesheetGenerationRequest和EditorUiDesignAssetExtractionRequest能反序列化segModel,容易认为外部 OpenAPI 也应公开该字段,或让用户在birefnet与anime-seg间自行选择。 - 原因:
segModel是 BgFilter 内部调用链的有效兼容字段,不等于稳定的外部产品契约。当前 BgFilter 服务受进程内存和并发容量约束,不同分割模型的资源消耗不能交给外部调用方控制;任意开放模型切换会让容量规划、超时和故障隔离失去确定性。 - 处理:产品 UI 不提供模型选择,应用内调用固定
birefnet;外部编辑器 OpenAPI 不声明segModel,并保持相关请求 schema 的additionalProperties: false,使外部请求携带该字段时被契约拒绝。只有维护 BgFilter 模型与容量的后端代码可使用该内部字段;若未来需要开放,先完成各模型的内存、并发和超时压测,再明确版本化外部契约。 - 验证:检查
docs/openapi/genarrative-external-v1.openapi.json的图片生成、图标 spritesheet 和 UI 素材提取请求 schema 均未包含segModel,且均保持additionalProperties: false。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/services/image-editor/editorProjectClient.ts、docs/project-memory/shared-memory/decision-log.md。
SPA 路由白名单不能只按一级目录放行
- 现象:
/not-exist已返回 404,但/creation/not-exist、/runtime/not-exist或/puzzle/not-exist仍返回 200 首页,搜索引擎继续判定为 soft 404。 - 原因:Nginx 或 Pingora 使用
/creation/*、/runtime/*等宽前缀作为 SPA fallback,前端对未知路径又回到平台首页;只验收根级未知 URL 无法发现该问题。 - 处理:SPA fallback 必须精确匹配当前真实完整路径,同时允许前端已有的大小写归一和尾部斜杠;最终 catch-all 只提供真实静态文件。浏览器 HTML 导航失败时返回品牌
404.html,但状态码仍为 404;API、探针和非 HTML 请求保持原有 404 响应。路由增删同步三套 Nginx、Pingora、route parity matrix 和路由门禁。 - 验证:除全部真实 SPA 路径外,至少检查
/not-exist、/creation/not-exist、/runtime/not-exist和/puzzle/not-exist均返回 404;带Accept: text/html的未知 Web 路径正文命中品牌页,不带 HTML Accept 的请求不得命中品牌页;维护模式仍保持页面 503 优先语义。 - 关联:
src/routing/appRoutes.tsx、src/routing/appPageRoutes.ts、deploy/nginx/、deploy/container/nginx.conf、server-rs/crates/pingora-gateway/src/main.rs。
Jenkins Job UI 参数会被 SCM Jenkinsfile 覆盖
- 现象:在 Jenkins Job 页面给
MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID配了默认值,下一次加载 Declarative Pipeline 后又变空或恢复旧描述;04:00 Full Job 还可能因默认选择pause-after-stdb且 approvers 为空而失败。 - 原因:这些 Job 使用 Pipeline script from SCM,
parameters {}和triggers {}会作为 Job property 回写现场配置;只改 UI 不是持久修复。构建编排如果不显式关闭下游PUBLISH_AFTER_BUILD,还会受下游默认值漂移影响。 - 处理:credential ID 和参数默认值写回三个 Jenkinsfile;仅供开发使用的 dev 定时 Full Job 默认
STDB_API_ROLLOUT_MODE=normal,三路 Build 调用显式传PUBLISH_AFTER_BUILD=false,再由 Full Job 统一按 Stdb → API → Web 发布。Secret 原文只放 Jenkins Secret File,旧 Secret Text 保留给 Import / Export。 - 验证:推送后让 Full / Stdb Build 用不存在的源码分支在 checkout 阶段 fail-closed,让 Stdb Publish 用空构建版本在 Prepare 阶段 fail-closed,以安全刷新参数 schema;随后只读检查三个 live
config.xml的参数描述和默认值,确认 Full timer 仍为0 4 * * *、rollout 默认值为normal,并确认刷新运行未进入 publish / deploy stage。 - 关联:
jenkins/Jenkinsfile.production-full-build-and-deploy、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-stdb-module-publish、scripts/check-production-ops-guardrails.mjs。
维护模式内网全站放行不能信任 X-Forwarded-For
- 现象:维护期间希望让内网继续访问整站,如果直接按
X-Forwarded-For: 192.168.x.x放行,公网请求可伪造该头绕过维护闸;如果仍按路径只放行后台,又会让内网主站和普通 API 继续返回 503。 - 原因:XFF 是客户端可提交的普通请求头,当前 Nginx 的
$proxy_add_x_forwarded_for还会保留已有前缀;维护放行属于授权判断,必须建立在不可伪造的网络来源边界上,并在路由分类前按来源统一决定是否绕过维护闸。 - 处理:Nginx 按 TCP
$remote_addr判断内网;Pingora 按 TCP peer 判断,只有 peer 为 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的X-Real-IP。可信内网来源绕过整站维护响应,公网应用主站、普通 API、后台和 SpacetimeDB 路由仍保持维护响应;绝不能用X-Forwarded-For做放行判断。 - 验证:Pingora smoke 同时覆盖公网主站、普通 API、后台为 503,以及内网对应路由为 200;Rust 单测覆盖 IPv4 / IPv6 内网、公网和空来源;Nginx 静态门禁反查两份模板的内网来源定义与全局维护变量清零逻辑。
- 限制:如果发布门禁已经停止 api-server,网关放行后普通 API 和后台 API 仍会失败;需要调用后端时应确保对应服务仍运行,不能把维护页绕过误当作服务可用性保证。
Full 结束后保持维护不能只加一个 UI 参数
- 现象:Full Job 参数页没有“完整发布成功后是否退出维护”选项,或者补了选项后 API readiness 一通过仍自动撤掉维护。
- 原因:维护退出发生在随 API artifact 发布的
production-api-deploy.sh内;Full、API Deploy Job 和脚本任一层没有透传,最终都会回到固定执行maintenance-off.sh。Declarative Pipeline 参数还要等 live Job 加载新版 Jenkinsfile 后才会刷新。 - 处理:Full 使用
EXIT_MAINTENANCE_MODE_AFTER_COMPLETION表达产品选择,Stdb Publish 和 API Deploy 全程固定保持维护,Web Deploy 成功后才进入独立最终退出阶段;API Deploy 的独立KEEP_MAINTENANCE_MODE再转换为脚本--keep-maintenance-mode。API deploy 还必须把production-api-deploy.sh、maintenance-on.sh和maintenance-off.sh从同一 build artifact 复制进 current release,否则 Full 最终阶段即使有选项也找不到随包退出脚本。默认值仍在 Full 结束时退出维护,避免定时 dev 发布行为变化。 - 验证:API deploy fixture 必须覆盖成功发布并保留 marker,还要断言 current release 中三个部署 / 维护脚本存在;生产运维静态门禁同时反查 Full 参数、下游透传、API Deploy 参数和脚本 flag。推送后用 fail-closed 首阶段运行刷新 live Job 参数,再核对
config.xml,不能只看仓库文件。
临时维护公告不能提交进版本化默认页
- 现象:现场已恢复通用维护页,但后续 Web Deploy 或下一次进入维护后,又显示昨天的“今天晚上 HH:MM~HH:MM”公告。
- 原因:
public/maintenance.html会被 Vite 复制进web.tar.gz,Web Deploy 解包后把/srv/genarrative/web指向新制品;临时公告一旦进入该源码,就会成为每次发布都恢复的长期内容。旧维护 on / off 只控制 marker,浏览器缓存不是根因。 - 处理:版本化默认页只保留无日期通用文案;临时公告用
maintenance-on.sh --page-file <公告HTML>安装到/var/lib/genarrative/maintenance/page.html。Nginx / Pingora 优先读取运行态公告,退出维护时同步清理;不要再原地编辑/srv/genarrative/web/maintenance.html或提交临时公告到public/。 - 验证:
npm run check:maintenance-page必须拒绝相对日期、具体日期和具体时间,并覆盖公告安装、同窗口保留、退出清理与新窗口清残留;Pingora smoke 必须证明运行态公告优先且删除后回退默认页。 - 关联:
public/maintenance.html、scripts/deploy/maintenance-on.sh、scripts/deploy/maintenance-off.sh、deploy/nginx/snippets/genarrative-maintenance.conf、server-rs/crates/pingora-gateway/src/main.rs。
遮罩点击关闭必须校验完整指针序列
- 现象:在弹窗内容内按下鼠标,拖到弹窗外的遮罩上松开时,弹窗被误关闭。
- 原因:只在
click阶段判断event.target === event.currentTarget不足以确认用户点击了遮罩;跨弹窗边界松开时,浏览器可能把合成点击的目标归到弹窗和遮罩的共同祖先。 - 处理:共享弹窗统一记录
pointerdown与pointerup的目标,只有按下和松开都发生在遮罩自身时才允许关闭。新增弹窗优先复用UnifiedModal,不要继续复制只判断最终click目标的手写遮罩逻辑。 - 验证:回归测试同时覆盖“弹窗内按下、遮罩松开不关闭”和“遮罩按下、遮罩松开正常关闭”。
- 关联:
src/components/common/UnifiedModal.tsx、src/components/common/UnifiedModal.test.tsx、src/components/auth/PlatformAuthModalShell.test.tsx。
公开作品资产不能用 generated 前缀或 PublicRead 批量放行
- 现象:资产 ACL 收紧后,公开页面读取其他作者作品资产集中返回
404;对象在 OSS 中真实存在,但已登记asset_object.access_policy = private。 - 原因:“作品公开”不等于“作者账号下所有 generated 对象永久公开”。只按 profile / session 关联也会误公开同会话的未选候选图、参考图或生成输入;批量改
PublicRead则无法随作品隐藏、删除或取消发布自动撤销。 - 处理:已登记对象继续保持
private,通过public_work_asset_read_grant只派生Published + visible(custom-world还必须未删除)正式发布快照实际使用资产的匿名读授权。API 必须同时校验 grant owner 与资产 owner 一致,以及asset_object_id或精确object_key命中;明确排除参考图、未选候选图和generationInputs。Custom World 只能扫描角色、地标、营地、章节和 opening CG 等正式根,不能遍历 legacy payload 的未知根。历史作品交给 view 现算补齐,不做永久 ACL 数据补丁。 - 权威查询边界:不能从
asset_object或public_work_asset_read_grant的连接级订阅 cache 推断当前 ACL;池连接水位不一致会让刚撤销的 grant 继续签发 URL,也会让刚公开的作品短暂 404。资产定位和公开授权必须通过受 runtime service identity 限制的 procedure 在同一事务快照中计算,失败时拒绝读取;procedure 先按 asset owner 使用各玩法 owner 索引缩小到该作者作品,再匹配候选asset_object_id/ 精确 key,不能每张图都执行全站公开 view,也不要在每个池连接订阅复制全量 private 资产表。公开派生授权、PublicRead和 legacy 兼容读取的签名 URL 最长 600 秒,owner / admin 不受该公开上限影响。 - Remix 边界:拼图、Custom World 和大鱼现有 Remix 会把源资产引用复制到新 owner,但没有持久化不可伪造的资产来源。不得因此放宽跨 owner grant;源作品隐藏后仍公开的 Remix 资产,需要后续通过 Remix 时复制资产或持久化 provenance 解决。
- 验证:资产 owner 本人仍可读;公开可见作品的正式资产可匿名读;跨 owner、只命中前缀、参考图、未选候选图和
generationInputs仍返回不存在;作品隐藏、删除或取消发布后 grant 消失。 - 关联:
server-rs/crates/spacetime-module/src/public_asset_access.rs、server-rs/crates/spacetime-client/src/assets.rs、server-rs/crates/api-server/src/assets.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
持久进程恢复不能重放 start 或按 PID 重连
- 现象:Runner 强杀或重启后,同一个 run 又启动了一份开发服务器,或者新 Runner 根据旧 PID 把宿主上的同号进程误认成原 PTY 会话。
- 原因:把 durable process record 当成活 OS handle,或在 spawn 前没有同步写入
launching,导致恢复逻辑无法区分“尚未启动”和“已经尝试启动”;OS PID 会复用,也不包含项目、Agent、run、action 和 Runner boot 身份。 - 处理:
processIdcreate-once 绑定完整 Runtime 身份,prepared / launching必须先于 OS spawn 持久化。旧 boot 下 prepared / launching / running / terminating 且缺少可信 terminal record 时只进入needs-reconciliation;不重放 start / stdin / terminate,不探测或接管旧 PID / PTY。首版不自动推断旧 prepared 为安全重试。 - 验证:在 launch 前后、running、stdin 写入后和两阶段 terminate 中分别强杀 Runner;fixture 的 launch / stdin 计数保持 1,恢复后没有 PID reconnect、没有 final,
runner.shutdown_if_idle仍报告 busy / reconciliation。 - 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/runner.rs、apps/ai-game-creator-shell/src-tauri/src/command_exec.rs。
PTY 输出与 stdin 正文不能进入公共 Runtime 持久面
- 现象:模型能正常 poll 进程输出,但 task/event、Agent DB、receipt、动作历史、activity/output、UI snapshot 或验收报告里也出现了终端正文;或者 stdin challenge 被确认摘要、inputSummary、错误日志保存为明文。
- 原因:直接复用普通 tool observation / pending action 的通用序列化,或为了排障把 PTY chunk 和 stdin data 整段复制进审计。持久进程正文可能包含密钥、绝对路径、交互输入和第三方进程回显,不能只依赖事后清洗。
- 处理:PTY 原始字节经控制序列、UTF-8、凭据和绝对路径清洗后,必须显式恢复清洗器裁掉的逻辑换行,再只进入 owning Agent 的私有 transcript、observation 和 context。公共持久面只保留 cursor、字节数、SHA-256、截断、状态和退出元数据;stdin 正文只允许存在于执行所需的私有 pending action,终态后删除,审计仅保留
bytesWritten / contentSha256 / stdinOpen / eof,禁止前后缀、摘要和可逆编码。 - 验证:fixture 同时输出唯一 sentinel、绝对路径和诱饵密钥,并发送唯一 stdin challenge;私有 poll 能读取清洗后结果,所有公共文件和报告的正文命中数为 0,stdin 只命中字节数和 SHA-256。
- 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/command_output.rs、apps/ai-game-creator-shell/src-tauri/src/agent.rs。
PTY 会话未终态时不能 final 或关闭 Runner
- 现象:Agent 回复“服务已在后台运行”后 run 被记成 completed,随后
runner.shutdown_if_idle关闭 Runner;或 terminate 只发出一次信号就宣告成功,留下仍存活的 child / grandchild。 - 原因:完成门禁只检查 pending tool action,没有把 live process registry、输出泵、终止任务和 unresolved reconciliation 纳入 active work;或者 terminate 为取状态从 offset 0 偷读一个字符并返回新 cursor,诱导后续 poll 重读 / 跳过;同时把 PTY / process group 错当成完整 OS sandbox 和可靠进程树隔离。
- 处理:launching / running / terminating 与任意 status 上的
needsReconciliation=true全部阻止 final reply、finalization journal、completed 和 idle shutdown。terminate 携带最后一次 poll cursor 并返回同一 cursor 的零消费元数据;Unix 完成完整 graceful wait 后只 force kill 同组残留,再 wait / reap / drain PTY,Windows 首版使用 Job force terminate + wait / reap;任何 signal / Job / wait 阶段无法确认都保持 reconciliation。Linux wrapper 监测 owner PID 并在 Runner 强杀后 kill 当前前台进程组,Windows 使用 kill-on-close Job Object;取消 run 也走同一收束路径。主动setsid/ 外部 service 和 OS sandbox 仍不在承诺内。 - 验证:活会话下 finalization 和
runner.shutdown_if_idle必须失败关闭;分别验证 graceful handler 尾部输出、宽限超时后的 force、忽略 SIGHUP 的 npm 孙进程和 Windows Job 路径,只有 child 已终态、同组残留已处理且 PTY 尾部排空才出现唯一 terminal record。另用允许程序证明代理和固定 cwd 不是文件系统 / 网络沙箱,不得把该现象误写成测试失败或安全能力。 - 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/runner.rs、apps/ai-game-creator-shell/src-tauri/src/agent.rs。
命令环境变量、代理和进程组不能冒充 OS 沙箱
- 现象:命令看似使用隔离 HOME / TMP、离线包管理器和不可达代理,仍能直接读取宿主用户文件、用原始 socket 联网,或由
project.verify的平行 npm spawn 绕开command.exec限制。 - 原因:环境变量和 argv 白名单只约束主动配合的程序,进程组 / Job Object 主要解决生命周期;它们不建立 mount / network namespace,也不能保护
.agentRuntime 控制面。只包command.exec而漏掉command.start或project.verify同样属于 fail-open。 - 处理:Linux 三个入口统一使用受信任系统 bubblewrap;项目根 rw,
.git / .agents / .codexro,.agent以 000 空 mount 隐藏,项目外普通用户路径不挂载,network namespace 默认隔离,嵌套 userns 禁用。全局 namespace canary 与项目 mount preflight 都必须在 revision / processId / 目标 program 前成功;任何失败都不回退宿主执行。Windows 在等价 restricted process / AppContainer 落地前继续标记为固定命令 legacy 边界。 - 验证:不能只断言 bwrap argv。必须运行真实目标和子进程,分别检查工作区写入、宿主 sentinel、四个控制目录、原始 socket、PTY stdin / graceful terminate、Runner SIGKILL 后宿主
/proc无项目 cwd 进程,以及 unavailable 时 marker 为零。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs、command_exec.rs、process_session.rs、project.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
工具链环境根不能把整个用户目录挂进命令沙箱
- 现象:Runner 以
RUSTUP_HOME=$HOME启动,或.rustup符号链接最终 canonicalize 到 HOME;Agent 随后能在 bubblewrap 内读取 SSH、Cookie 或其它用户文件,并把正文带回命令输出。 - 原因:只拒绝字面
/home / root / tmp,没有拒绝/home/<user>,也没有检查 canonicalize 后目录名是否仍与RUSTUP_HOME / JAVA_HOME / GOROOT / DOTNET_ROOT类型匹配。 - 处理:外部工具链环境根 canonicalize 后必须通过窄叶目录校验;用户 HOME、HOME 符号链接目标和类型不匹配目录全部在 mount preflight 阶段失败关闭。不要为了兼容任意自定义环境根放宽成“只读就安全”。
- 验证:直接 HOME、
.rustup -> HOME均返回错误且目标 program 零执行;真实.rustup叶目录仍可只读挂载,Cargo fixture build 继续通过。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs、command_exec.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
沙箱内验收 fixture 不能依赖 Runtime 控制面或宿主 namespace 身份
- 现象:V1.10 process-session 真实验收在 V1.11 后报“缺少精确回显”,但确定性 PTY 和 sandbox 测试均通过;旧 fixture 在 readiness 前写
.agent,还启动 loopback server 并把进程内 PID / 端口交给宿主检查。 - 原因:V1.11 正确地用 0000 空 mount 隐藏
.agent,并隔离 pid / network namespace。沙箱内 PID、loopback 端口和 Runtime 控制目录不再是宿主可观察事实;fixture 在输出 readiness 前即可能失败,统一的 interaction-evidence 错误又掩盖了真实阶段。 - 处理:交互 fixture 只使用 PTY stdin/stdout/signal,不写
.agent、不监听 TCP、不持久化 PID/端口。唯一启动由 process record、start action/fingerprint、start audit 和唯一 readiness marker共同证明;Runner 强杀后的清理由 owner boot、reconciliation record 和宿主/proc/*/cwd项目进程归零证明。 - 验证:独立真实 Node smoke 必须完成 readiness、challenge 单行原样输入、精确 echo、SIGTERM stopped,并确认项目未创建
.agent;E2E 分别报告 readiness / stdin hash / echo / stopped 缺失,严格检查 readiness poll -> stdin -> echo poll -> terminate -> terminal poll。Provider 在零工具计划阶段的 502/TLS 只记外部失败,不得归因到 fixture 或 Runtime。 - 关联:
apps/ai-game-creator-shell/scripts/process-session-real-e2e-fixture.mjs、agent-runtime-real-e2e.mjs、apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
pre-exec 固定 FD 映射不能逐项覆盖源描述符
- 现象:可信 launcher 的单项测试都通过,但并发
project.verify偶发在第二次 launch 被记为 failed;失败项单独重跑又恢复正常。 - 原因:父进程创建 socket/file 后得到的源 FD 数值不固定。若逐项
dup2(source, 0/4/5/6),前一次目标 FD 可能正是后一条映射尚未读取的源 FD,导致 status、block 或 trampoline source 被静默替换。测试并发度改变打开 FD 分布,因此表现为偶发。 - 处理:在父进程进入 spawn 前先用
F_DUPFD_CLOEXEC把所有源复制到 64 以上互不重叠的 owned FD,并保持到 child-created;pre-exec 只把这些稳定高位 FDdup2到固定 0/4/5/6。不能等到 pre-exec 才复制原始源,因为 Command 的 stdin/stdout/stderr 安装可能已覆盖原本占用 0/1/2 的源 FD。 - 验证:并发执行全部 project verification 测试;同时真实运行 bwrap staged marker 用例,确认 child-created、block、ready、commit、exec 和目标退出链均稳定,目标 argv/env/FD 不含 nonce 或控制 socket。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/command_sandbox_trampoline.rs、command_sandbox.rs、command_exec.rs、project.rs。
bwrap 的命令分隔符不能从目标 argv 末尾反查
- 现象:普通命令握手正常,但
cargo test -- --nocapture、npm forwarded args 等包含独立--的合法目标参数可能在 sandbox preflight 或 staged launch 期间提前执行原目标。 - 原因:launcher 用
rposition("--")查找 bwrap 自己插入的命令分隔符,误命中目标 argv 里的最后一个--;截断后原 target executable 仍位于 bwrap COMMAND 位置,trampoline 被追加成目标参数而不是替代目标。 - 处理:构造器保证 bwrap options 与 COMMAND 之间只有第一个独立
--是 launcher 分隔符;preflight 和 stage 都取第一个位置。不要从用户目标 argv 的末尾推断结构边界。 - 验证:真实 staged bwrap 用例必须让目标 argv 带独立
--,同时断言 sandbox-ready/commit 前 marker 为零,commit 后才执行成功。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs、command_exec.rs。
PTY 私有控制 FD 不能假设会被 bubblewrap 透传
- 现象:process-session wrapper 在 portable-pty 之后成功创建 fd 3 控制 socket,但 trampoline 收不到 ready/commit 帧;如果直接改用 fd 0,真实 target 又失去交互 stdin。
- 原因:portable-pty 在 wrapper exec 前关闭全部 fd 3 以上描述符;wrapper 重新创建 fd 3 后,bubblewrap 仍只保留 stdio 和被
--json-status-fd / --block-fd / --ro-bind-fd明确引用的描述符,未引用 fd 3 不是可靠 COMMAND 继承通道。 - 处理:Runner 与 wrapper 先用 abstract Unix socket bridge 传递私有 launch plan;wrapper 内部 gate 继续通过 bwrap 会保留的 fd 0进入 trampoline。process-session trampoline 不把控制 fd 0传给 target,而是确认 fd 1 是 PTY 后复制同一 slave作为 target stdin;一次性命令模式仍显式使用
/dev/null。 - 验证:真实 PTY 测试必须完成 readiness、stdin echo 和 terminate;trampoline 测试同时断言一次性 stdin 为 null、process-session stdin 可读 PTY,target fd 3/4/5/6 不存在,bridge endpoint/nonce/control frame 不出现在 transcript 和公共持久面。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs、process_session.rs、command_sandbox.rs、command_sandbox_trampoline.rs。
graceful terminate 不能先杀承载 target 的 wrapper 进程组
- 现象:target 注册了 SIGTERM 清理逻辑,但
command.terminate只偶尔出现 stopped marker;耗时 300-500ms 的清理经常被提前截断。 - 原因:如果先向 wrapper/bwrap/trampoline/target 共用的外层进程组发送 SIGTERM,wrapper 会先退出,bwrap 的 die-with-parent 随即收走 namespace;名义上的 800ms 宽限并没有真正留给 target。
- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。reader 发现未换行输出超过上限时必须先原子投影
output-limit-exceeded并唤醒 poll,再异步发送终止控制,不能让高负载下的 supervisor 调度延迟把已越界进程继续暴露为running。 - 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。正式
command.exec测试夹具仍必须走允许的npm run等程序,不能为了构造 stdin race 绕过白名单直接解析bash -lc。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收;测试互斥锁在前序 panic 后应恢复 guard 继续报告后续独立结果,不能用PoisonError掩盖真实失败范围。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/process_session.rs、process_session_bridge.rs、command_sandbox_trampoline.rs。
启动记录必须封闭状态组合,child 不能自行猜 durable commit 超时
- 现象:慢磁盘让 sandbox-ready 后的 child 在父侧持久化完成前自行退出,父侧随后把零执行误记为 launch-unknown;损坏的
failed + launch-unknown + needsReconciliation=falserecord 又可能被 active/final/idle 扫描漏掉。 - 原因:child 与父侧各自维护短 timeout,没有统一 commit/abort 决策;record 校验只检查枚举和值存在,没有约束 status、sandbox、target、failure、reconciliation 与时间戳的合法组合。Windows 如果在 action 去重前调用 durable callback,还会在同 action replay 时重复推进 revision。
- 处理:sandbox-ready 后 child 阻塞等待父侧显式 commit 或 abort,父侧失败时发送 abort 并回收树。v3 读取使用封闭状态矩阵和
started <= ready <= exec <= terminal <= updated的逐项可选时间校验;旧 boot prepared/launching 及同 action start replay转成 target unknown。非 Linux durable callback 只放在 existing action miss 分支,started/ready/exec 使用同一时间点。live registry 必须与 durable record、pending reservation 合并参与 capacity/final/idle,不能因 record 缺失失败开放。 - 验证:durable callback 延迟超过旧 3 秒时 target marker 在 callback 内必须仍不存在、commit 后才出现;构造 launch-unknown/start-audit/target-exec 的非法组合均拒绝读取,旧 boot launching 和 same-action replay必须变成可再次读取的 reconciliation,同 action Windows 测试只调用一次 callback;删除 live record 后 final/idle 仍被 registry 阻断。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/process_session.rs、process_session_bridge.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
loopback port 0 也会被临时端口池耗尽阻断
- 现象:旧 Runner 已停止、endpoint 连接拒绝,但新 Runner 在
TcpListener::bind(127.0.0.1:0)直接返回Address already in use,所有 Agent 写命令随后报“Runner 在就绪前退出”;同一主机上的本地预览和 Node HTTP 测试夹具也会以相同方式失败。 - 原因:port 0 仍需要内核从
ip_local_port_range分配监听端口;本机 api-server 与 SpacetimeDB 的约 2.8 万双向连接占满 32768-60999 后,即使目标端口不是旧 endpoint 端口,自动分配也会失败。只看ss -ltn会漏掉占用本地端口的 established client socket。 - 处理:先保留 port 0 正常路径;Linux 只在
AddrInUse后懒读取ip_local_port_range / ip_unprivileged_port_start / ip_local_reserved_ports,把候选限制在 61000-65535 高位段并排除临时范围、实际特权范围和 reserved ranges,再按随机起点尝试。Runner 与本地预览复用同一 loopback binder;必须交给外部测试进程监听时,先用有同等回退能力的测试 listener 预留端口并有限重试交接。不要停止用户 dev 栈,不要扫描常见服务低端口,不要使用非 loopback fallback,也不要用固定公开端口或无 token 协议绕过。 - 验证:除纯 bind 回退、懒加载、非默认特权起点、reserved ranges、候选耗尽和范围解析单测外,还要在端口池真实耗尽的主机上启动 Runner 和本地预览,确认监听端口位于临时范围外、heartbeat 与预览内容可读,并完成真实 Provider 任务及 MCP HTTP fixture 全量回归。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/runner.rs、preview.rs、tests.rs、agent-runtime-real-e2e.mjs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
旧 process record 惰性迁移不能替代 resume 主动投影
- 现象:owning Runner 被 SIGKILL 后,项目 cwd 进程已经清零,新 boot 和同 run / session 也恢复成功,但 process record 仍显示旧 boot 的 running,task 长时间停在旧 planning,真实 Runner-kill 套件等不到 reconciliation。
- 原因:process record 的旧 boot 迁移只在 poll、active scan 等读取路径发生;独立 Runner
runtime.resume原先直接恢复 running task,没有先触发 active process scan,也没有把迁移后的 record 同步投影到 task / state。 - 处理:resume 先全局拒绝指向未知 Agent 的 reconciliation record,再在 Agent lane 锁内处理所属旧 active records;一旦 record 进入 reconciliation,立即把 owning run 的 task / state / queue / event / Agent DB 写成
needs-reconciliation并停止恢复。这里不能用“task phase 已是 reconciliation”作为整体完成标记:task 追加、state / queue 重建、event 和 Agent DB 补齐必须分别幂等。JSONL 追加前先锁内修复截断尾行;event / Agent DB 用原 start action 身份去重,并对 Agent/task/session/run/process/owner boot 做冲突校验,不能只按 run/type 判断存在。 - 安全边界:写投影前逐条核对 process record 与 task 的 Agent、run、task、conversation session 身份;同一 Agent 出现多个不同 owning run 时失败关闭。同 run 多 record 也只能在全部身份一致时聚合。缺 task、记录损坏或身份冲突时不得改写原 task、继续规划、按 PID 重连或自动重启服务。
- 验证:确定性用例除重复 resume 外,还要在首次恢复后把 task、event 和 Agent DB 专用投影改成截断尾行并删除 state,再次 resume 必须修复半行、补齐四者且 reconciliation task 仍只有 1 条;Agent 错归属、task/session/process 同键冲突必须报错。真实套件在 readiness 后 SIGKILL Runner,分别从全量 task、event、Agent DB、runtime state 和 process record证明唯一 reconciliation与零 reconnect;可选文件或目录只容忍
ENOENT,其他读取错误不能吞掉。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent.rs、process_session.rs、tests.rs、agent-runtime-real-e2e.mjs。
私有 PTY 正文不能只靠 prompt 阻止最终回复复述
- 现象:Agent 正确完成唯一持久进程交互,但模型偶发在最终回复中复述一次性 challenge 或 readiness / echo / stopped 行,随后 conversation、event 和 Agent DB 公共投影一起泄漏私有进程正文。
- 原因:
command.poll正文需要进入 owning Agent 私有 observation 才能继续交互;system/task prompt 只能约束模型行为,不能作为持久化安全边界。 - 处理:在 finalization journal 写入前检查当前 run 的成功
command.pollobservation;只要存在非空私有输出,就不再持久化模型原回复,而是写固定安全完成摘要,再计算 fingerprint、写 assistant 和公共终态投影。不能只替换长行或高熵 token,因为模型可能只复述1234等短子串;不要把原始行或 token 写入新的审计记录。 - 验证:定向用例让最终回复包含 challenge、完整 ready/echo/stopped 行和
PIN=1234的短值局部回显,要求统一变为固定摘要;没有私有 poll 正文的普通回复保持原样。真实 Provider 继续扫描 task/event/Agent DB/receipt/conversation/activity/output/runtime state/report,所有正文泄漏必须为 0。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent.rs、tests.rs、agent-runtime-real-e2e.mjs。
Project Supervisor 专业回执不能靠 Agent DB 或新 continuation 收束
- 现象:同一条用户目标在专业 Agent 完成后出现第二个
delegate-receipt-*run 和第二条面向用户的 assistant;或 Runner 重启后回执丢失、被不同 action 重复认领、父 run 永久等待,甚至错配 child 把真实 delivery 误标为 suppressed。另一类实测症状是首条 Supervisor 消息报“Agent Session Runtime 启动跨进程锁超时”,或父 Agent 在两个 child 运行时持续调用agent.run_status,随后被 child 的黑板写入推进 project revision 而误判为旧动作。 - 原因:把
.agent/agent.db诊断投影当成回执协议事实,或直接从 ready 跳到已消费,没有可恢复的 claim 阶段和 observation 门禁。多回执认领若不先按稳定顺序取齐所有 delivery 锁,会死锁或留下部分认领;parent-wake 若每次生成随机 Runner requestId、把“全局扫描成功”当成“目标已推进”或对结构性错误无界重试,会丢 wake、重复唤醒或掩盖损坏状态。持有 Session lane 同步通知 Runner 会让 Runner 反向启动同一 Agent 时自锁;把 claim 型agent.run_status当成项目写动作做 revision/fingerprint 门禁,会被合法的专业 Agent 写入误伤。 - 处理:正式主聊天只路由到
project-supervisoractive Session,活跃期输入继续 same-run steer;同一父 run 最多同时保留 3 个dispatched / ready静态专业委派,已预留的同 action delivery 恢复复用原 target Session/run,不另占名额。同一工具计划完成委派后,Runtime 在下一次 Provider planning 前直接持久化waiting-for-delegate-receipts并释放 lane,不让模型轮询等待。delivery 单向推进dispatched -> ready -> claimed-by-parent / suppressed,claim 单向推进Prepared -> Committed -> Observed;先持有 claim 锁,再对 delegationId 排序去重并按序取齐 delivery 锁,任一锁不可得时零状态推进。delivery / claim journal 与 pending observation 是事实源;Agent DB append 只能 best-effort,失败不得推翻已持久化结果。入队在 Session lane 内完成,Runner 通知在 lane 外发送;agent.run_status保留 claim 身份校验但不绑定全局 project revision/fingerprint。 - 恢复门禁:只有
project-supervisor的 executingagent.delegate / agent.run_status可在项目锁内重验 durable pending、Session/run/action fingerprint、delivery/claim/child 身份和当前 policy 后补交;只有 delivery 预留且无 child 时,拒绝动作必须把该预留 CAS 为 suppressed。其他 executing 动作或副作用身份不明必须进入needs-reconciliation。parent-wake 以 project/Agent/run 做 coalescing singleflight,新信号不能在已有 worker 退出窗口丢失;有界重试接受 lane 竞争、暂时连接、连接中止、broken pipe、unexpected EOF、资源暂不可用和超时类错误。损坏 journal、身份冲突及重启扫描中的损坏 barrier 直接投影 reconciliation。External Runner wake 用项目根、method、Agent、runId 和 loop iteration 派生稳定 requestId,目标未观察到、仍 waiting 或 lane 忙时返回不缓存的可重试错误。 - 身份与收束:子终态发布前同时核对 parent Agent/Session/run/action、delegationId 派生、target Agent/Session/run、child source 和 child 反向 parent/delegation 链接。错配 child 保持原 delivery 不变并记录冲突;父任务先进入 completed / failed / cancelled / budget-exhausted 时,终态写入路径 suppress 尚未认领的匹配 delivery,合法迟到 child 不能重新写 ready。父 run 在 waiting、ready-unclaimed 或 unobserved claim 任一非零时都不得 final;全部清零后仍由原 Supervisor Session/run 的 finalization journal 幂等写入唯一 assistant,不创建新 receipt run。
- 验证:Rust 定向回归使用
project_supervisor_前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executingrun_status续接与 delegate policy 重验;本地 mock Provider 长套件应允许一次短间隔 connectivity 重试,并在断言前同时等待终态投影和 Agent lane 释放,避免端口瞬时波动或后台收尾窗口制造假失败。agent_background_enqueue_notifies_only_after_session_lane_release覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。 - 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/delegation.rs、agent.rs、runner.rs、tests.rs。
父 run 协作策略不能在绑定后继续按全局 live policy 重验
- 现象:同一 Supervisor 父 run 已经持久化合法 collaboration batch,管理员随后修改或损坏
.agent/collaboration-policy.json,后续 spawn、claim、mutation、MCP 或 finalization 却突然改用新策略、进入 reconciliation;或者 snapshot 被删除后,Runtime 又按 live policy 把已有 run 当成未绑定 run。另一类症状是 contractless/v1 batch 被跳过、两个不安全 run ID 经字符替换落到同一 snapshot/锁 key,或旧Prepared / Committedclaim 因 snapshot/binding 不可读而不能重放 observation。 - 原因:把项目级 policy 当成每个动作的 live 执行事实,没有为父 run 设置明确线性化点、不可变策略快照和独立“曾绑定”记录;或者在 v2 batch 完整验真前就用
contract.policy播种 snapshot。只对 run ID 做 lossy 规范化、让锁复用该路径片段,或用通用原子 replace 代替同一身份锁内 CAS,也会制造路径碰撞、并发覆盖和伪合同漂移。 - 处理:V1.38 固定顺序为
v2 batch -> snapshot -> binding sidecar -> action side effects。snapshot 位于.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json,其完整字段必须统一为schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt;snapshot fingerprint 覆盖除snapshotFingerprint / boundAt外的全部稳定字段。独立 binding 位于.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json,固定包含schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。同一 run 并发绑定时,无论 loser 是读取到不同快照还是在 winner 持锁期间耗尽有界等待,都必须返回稳定的“并发绑定冲突”错误分类。Unix 同进程首次并发初始化安全锁路径时,需要短暂串行化mkdirat/openat打开阶段,规避 macOS loser 在最终O_CREAT前观察到瞬时ENOENT;返回后的flock仍承担跨线程、跨进程互斥。 - 路径与恢复:不安全或规范化后变化的 Agent/run ID 使用有界安全前缀加原始 ID 稳定 SHA-256,锁 key 对完整
parentAgentId + NUL + parentRunId计算稳定 SHA-256,不能只做字符替换。恢复顺序为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy;snapshot 缺 binding 可从 snapshot 补写,binding 存在但 snapshot 丢失只能按可信 v2 contract 和首次绑定身份恢复,无可信 v2 时禁止 live policy 重绑。contractless/v1 collaboration batch 必须先失败关闭。legacy-current-project-policy只允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由可信身份和状态明确证明属于pending / running / waiting-for-confirmation / waiting-for-user-input的旧父 run;terminal、needs-reconciliation或身份/状态未知 run 的状态读取不得新建 snapshot。 - 漂移与 Claim:绑定后 global policy 的
matched / drifted / unreadable只报告状态,不能改变后续动作或完成门禁;新 policy 只用于后续新父 run。旧 durable claim、未观察 claim 和 legacy claimed delivery 先按原 action/group 身份恢复且不得取得新 delivery;新的 claim 必须先成功解析 effective snapshot 并核对 binding,再执行 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。 - 真实 E2E 现场:正在运行的正式客户端可能在验收期间启动或重启正式 Runner,导致 source endpoint 身份真实变化。不得关闭
sourceRunnerEndpointUnchanged门禁,也不得杀掉不属于验收器的进程;应把同一配置内容复制到仓库外的大容量磁盘私有目录,目录/文件权限分别为0700/0600,不复制 endpoint、锁、会话或数据库,验收后删除。功能完整但 source endpoint 被外部改变的报告与后续干净清理报告不得拼接。 - 验证:必须覆盖 snapshot 9 个完整字段、首次
abortedbatch 无 snapshot/binding、matching binding 已存在时可用可信abortedv2 contract 恢复缺失 snapshot、双故障窗口零副作用恢复、同内容并发 CAS、snapshot/binding 冲突或丢失、篡改 contract 不得播种、binding 已存在且无可信 v2 时禁止 live 重绑、contractless/v1 协作 batch 先失败关闭且非协作 v1 batch 不误伤、四种 legacy 非终态可迁移而 terminal/needs-reconciliation/身份状态未知读取不建 snapshot、危险 ID 路径/锁不碰撞、四类 global policy 状态,以及旧 claim 可恢复而新 claim 先过 effective snapshot。2026-07-19 上述确定性门禁、E2E self-test、52/52 collaboration 定向回归、终态 snapshot/binding 字节保留回归和 949 passed/4 ignored Rust 全量已完成;真实功能闭合轮受正式 endpoint 外部重启污染,私有配置源轮又连续耗尽 transient Provider retry,不能拼接为 PASS,故当前仍不得声称 V1.38 真实 E2E 已 PASS。 - 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、docs/project-memory/shared-memory/decision-log.md、apps/ai-game-creator-shell/src-tauri/src/collaboration.rs、agent.rs、tests.rs、scripts/agent-runtime-real-e2e.mjs。
单 Agent 持久计划不能靠工具下标或恢复猜进度
- 现象:工具 action 1 成功后第二个计划步骤被自动标成完成,模型仍有 pending / in_progress 步骤却写出最终回复;或 Runner 重启、刷新 UI、same-run steer 后
planRevision回退、已完成步骤消失,legacyplan又覆盖新计划。另一类错误是仅更新计划就触发项目 revision 漂移、verification 失效或权限确认。 - 原因:旧
planSteps由短plan派生,并按 actions 数组下标驱动active / completed,它无法表达跨窗口、恢复和 steer 后的真实任务进度。把 context bundle 当唯一计划事实源、把 v2 缺字段当空计划,或把planUpdate伪装成受策略工具,也会让 Runtime state、恢复快照和项目副作用门禁互相污染。 - 处理:V1.17 的
planUpdate只接受 1 到 8 个唯一pending / in_progress / completed步骤,至多一个in_progress;native function 即使无变化也必须显式传planUpdate: null,只有旧文本 JSON 可省略。当前 run 建立结构化计划后,legacyplan和所有按 action 下标推进的 helper 都只能读不能写。planRevision只在有效变化时单调增加;completed 与历史快照中已有的 failed 终态即使被下一版省略也必须合并保留,completed 回退或合并后超过 8 步时整次拒绝。外层 run 进入failed / budget-exhausted时必须原样保留最后可信进度,不把 pending / in_progress 机械标成 failed。任何非 completed 步骤都阻止成功 final,不能用 response 文本绕过。 - 恢复与 steer:context bundle v3 必须保存并复核 revision、说明、步骤和 active index;与 Runtime state 不一致时失败关闭,不能选“看起来更新”的一份。v2 只能在原身份、task、revision 和 verification gate 校验通过后从当前 state 补齐计划,v1 继续拒绝。same-run steer 只作废旧 Provider actions / 回复并要求重审未完成部分,不能清空终态步骤或重置 revision;Runner 重启同样不得自动勾选。finalization 另以 v2 journal 绑定最终完整计划快照:只有 assistant 已落盘时,state 丢失才可从该快照恢复终态;assistant 未落盘且 state 不可读时必须进入 reconciliation。
- 边界:计划更新是
.agent/runtime私有元数据,不经过项目工具 policy,不推进 project revision 或 verification gate,不改变 pending action fingerprint。开发 UI/CLI 可展示最多 8 步完整计划;普通用户 Supervisor 只能显示完成数、当前步骤、等待、下一步和协作数量,不能把内部 explanation、完整步骤或 currentAction 搬到主聊天。 - 验证:运行
structured_plan_与agent_runtime_context_bundle_migrates_v2_and_rejects_v3_plan_mismatchRust 定向用例,并用appSurface.test.ts覆盖刷新恢复和 Supervisor 紧凑摘要。真实 Provider 必须在无计划配方下多次更新计划,完成一步后接受 same-run steer,再经历 Runner 强杀恢复;最终证明 run/session 不变、revision 不回退、终态不丢、旧动作与副作用不重放、未完成时零 assistant、完成后唯一 assistant,计划更新前后 project revision / policy 不变。未运行该门禁时不得写 V1.17 PASS。
Finalization 只绑定回复会在 Runtime state 丢失后丢计划
- 症状:assistant 已经按稳定 messageId 写入 conversation,进程却在 Runtime completed 投影前退出;重启后 state 文件缺失,系统从 task record 重建出默认或 legacy 计划,最终回复虽然没有重复,结构化计划 revision 和步骤却丢失。反向地,assistant 尚未写入时若也用 journal 单独猜计划,会把过期回复错误提交给用户。
- 原因:task record 不携带完整结构化计划,context bundle 也可能对应 finalization 前的其它 checkpoint;只给 journal 绑定回复和 verification gate,无法证明准备提交时的最终计划快照。
- 处理:
game-creator-runtime-finalization.v2在 prepared 时保存完整planRevision / planExplanation / plan / planSteps / activePlanStepIndex与planSnapshotFingerprint,并将指纹纳入 finalizationId。读取时除校验格式和指纹外,还要再次要求全部结构化步骤 completed 且 active index 为空。assistant 已存在且 task 唯一时,允许从 v2 快照恢复原计划并补齐 Runtime completed;assistant 不存在而 state 缺失或不可读时保留 journal、进入needs-reconciliation,不能自动写回复。已有 state 与 journal 快照冲突时同样失败关闭或让 prepared 回复失效后在同一 run 重规划。 - 验证:
finalization_resume_recovers_persisted_assistant_without_runtime_state必须证明无 Provider 重放、assistant 唯一且恢复后的 revision/说明/步骤与 prepared 快照完全一致;structured_plan_finalization_without_readable_runtime_state_needs_reconciliation必须证明 assistant 未落盘时 missing/corrupt state 都零回复、journal 保留且无 completed 审计;finalization_resume_blocks_internally_consistent_incomplete_plan_snapshot必须证明重算合法指纹和 finalizationId 也不能提交未完成计划。
CLI Runtime JSON 不能暴露项目绝对存储路径
- 症状:真实 E2E 的 task/event/Agent DB/report 均无项目绝对路径,子进程 transcript 扫描却稳定命中 6 次;入队和首次状态读取各返回 3 个路径。
- 原因:开发 CLI 直接序列化
AgentRuntimeResult,把仅供 Tauri/App 定位本地 sidecar 的sessionPath / eventPath / taskPath一并写进runtimeJson。后续 confirm、steer 和 resume 复用同一结果结构,也会重复暴露。 - 处理:保持 Tauri 内部契约不变,只在 CLI JSON 输出视图递归删除三个存储路径;
state、task queue、events、tasks、run/session/action 身份和 steer 状态继续保留,验收器仍能解析必要证据。不要靠 E2E 忽略 CLI stdout,也不要笼统删除所有path字段破坏安全相对产物证据。 - 验证:CLI serializer 单测覆盖顶层、嵌套和数组结果;真实
--agent-runtime-status输出对 disposable 项目路径命中为 0,后续完整llm-runtime报告的projectPathTranscriptLeakCount / projectPathReportLeakCount必须同时为 0。 - 补充现象:
agent.message已把正文安全写入目标 Agent 的私有 tool 会话,但.agent/agent.db的agent.runtime.agent.message.path直接复用了 conversation 返回的绝对路径,导致真实 Supervisor swarm 的公共路径门禁失败。 - 补充处理:会话文件仍由项目内部 API 创建,写公共审计前必须再用项目根做
strip_prefix、路径分隔统一和相对路径规范化,只保存.agent/conversations/agents/<agentId>.jsonl;不能通过 E2E 忽略该 record,也不能笼统删除所有相对path证据。 - 补充验证:
background_agent_runtime_can_write_blackboard_and_message_other_agent同时证明 tool 消息可读、agent.runtime.agent.message唯一存在、path 等于规范项目相对路径且Path::is_absolute=false;正式supervisor-swarm的 Agent DB 项目路径泄漏计数必须为 0。
把模型修复上下文写入公共审计会泄露正文
- 症状:Runtime event 或
.agent/agent.db为了排障直接记录 thinking summary、legacy plan 标题、解析错误、malformed JSON 或 native function arguments;私有任务内容、项目路径或模型调用体因此进入公共审计和 UI 最近事件。 - 原因:格式修复确实需要把上一条输出与错误反馈给同一次 Provider 请求,但“Provider 私有修复上下文”和“持久公共诊断投影”被误当成同一份数据。
- 处理:
thinking_summaryevent 只留正文 SHA-256 与字符数,legacyplanevent 只留步骤数;结构化计划审计只留 explanation 哈希与字符数,以及 step 标题哈希、状态和计数。agent.runtime.tool_plan.repair只留 attempt/maxAttempts、protocol,以及错误、输出或调用体预览、callId/functionName 的哈希与长度。经过过滤和限长的上一条输出与协议错误只可进入当前 planning 的私有 repair 请求,不得落到 event、task 或 Agent DB 正文字段。 - 验证:后台 loop 回归必须断言 thinking event 不含摘要正文、legacy plan event 不含标题;文本与 native repair 回归必须同时证明私有请求仍含足够修复上下文,而 Agent DB 不存在
protocolError / responsePreview / function arguments / callId / functionName原文字段。
每次同步 context bundle 时刷新 planning fingerprint,导致同批旧动作越过仓库规范漂移
- 症状:同一 Provider planning 返回多个 actions;前一个验证动作修改了
AGENTS.md或其它启动上下文来源,后一个写动作仍执行成功,下一轮只看到普通okobservation,没有repositoryContextDrift=true。 - 原因:context bundle v3 在 action 激活和 observation 落盘后都会同步完整计划,同时重新扫描 repository startup context。若 drift gate 从最新 bundle 读取 fingerprint,前一个动作造成的漂移会被同步成新基线,后续动作不再与 Provider planning 真正看到的旧规范比较。
- 处理:Provider request builder 必须把实际渲染的 repository fingerprint 和工具计划一起返回;现行
game-creator-pending-action.v5除plannedRepositoryContextFingerprint外,还绑定 steer cursor 与goalId / goalRevision / goalSnapshotFingerprint。同批自动动作、待确认动作和恢复动作只复核 planning 时持久化的快照;旧 v1-v4 缺少现行完整身份,失败关闭,不能从最新 bundle 或当前 Goal 猜回。 - 验证:
runtime_v11_closure_repository_context_drift_replans_before_auto_mutations必须覆盖 file.write / file.patch / file.delete / project.patchset / project.restore 五种动作,证明前置验证导致规范漂移后旧动作零执行、同 run 收到稳定 drift observation;legacy_context_and_pending_records_fail_closed覆盖 v4 拒绝。 - 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/agent.rs、main.rs、tests.rs、apps/ai-game-creator-shell/src/App.tsx、tests/appSurface.test.ts。
持久 Goal 不能只靠 Runtime state 推断,也不能让旧动作跨 revision 执行
- 现象:Goal 编辑后,旧的自动动作或待确认动作仍按旧目标执行;暂停后重启 Runner 又先恢复 finalization/pending action 并继续调用 Provider;或 assistant 已经可见,但 Goal 先标成 completed、Runtime task/state 仍停在非终态。另一类恢复问题是 pause 已中断 Provider,却没有保存准确 continuation,或 resume 先把 sidecar 写成
active、后续 Runtime 恢复失败,重试却假成功并永久停在 paused。 - 原因:把每 Agent 的 latest Runtime state 当成 Goal 正文事实源,没有独立 Agent/Session Goal sidecar;pending action 未绑定 Goal ID、revision 和快照;恢复扫描把 pause control 放在 finalization/pending action 之后;或 finalization 把 Goal completed 当成 Runtime completed 之前的提交点。只在内存里中断 Provider,也无法保证进程退出后仍有可恢复上下文;只看 sidecar 的
active也不能证明 Runtime 投影和 Runner 唤醒已经提交。旧/半写 Runtime 缺少goalId时吞掉 sidecar 损坏错误,还会把有 Goal 的 run 错当成无 Goal run。 - 处理:Goal 正文只认
.agent/runtime/goals/current/<agentHash>/<sessionHash>.json,终态历史写入.agent/runtime/goals/history/<agentHash>/<goalHash>.json;Runtime state/task 仅作投影。context bundle v4 固定绑定goalId / goalRevision / goalStatus / goalSnapshotFingerprint;Provider 中断边界先保存按active恢复语义构造的 continuation,再把同一 run 收束为 paused。 - 动作与恢复:pending action v5 同时绑定 Goal ID、revision 和 snapshot fingerprint,旧 v1-v4 失败关闭。Goal edit 后,旧自动/确认动作写成稳定
blockedobservation 并在同一 run 重规划;不能执行旧副作用,也不能转成 retry run。重启先处理 cancel / Goal control,pause-requested收束为paused后直接休眠;resume 只做paused -> active,先删除同一 run 的旧 cancel tombstone,再唤醒原 run。若首次 resume 在 sidecar 提交后失败,重试必须继续补 Runtime/Runner;sidecar 损坏或身份冲突时,无论 Runtime 是否已有goalId都进入 reconciliation。 - 完成顺序:
game-creator-runtime-finalization.v3绑定 Goal 快照。assistant 落盘后,先写 Runtime completed task/state,再写 Goal completed 并补齐携带 Goal 终态的 Runtime projection;全部成功后才删除 finalization journal。prepared 且 assistant 未落盘时发现 Goal revision 已更新,必须丢弃旧回复并 same-run 重规划。 - 验证:确定性测试至少覆盖旧自动/确认动作转 blocked、paused 重启零 Provider、同 run resume、旧 cancel tombstone 清理、Provider 中断边界 context、assistant 后 Runtime/Goal completed 顺序和 v1-v4 pending 拒绝。真实 Provider 还必须经历 Goal edit、pause、Runner 强杀和显式 resume,并证明 run/session 不变、旧动作零重放、唯一 assistant;未完成该链路时不得写 V1.18 PASS。
- 关联:
docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md、apps/ai-game-creator-shell/src-tauri/src/goal.rs、agent.rs、runner.rs、tests.rs、apps/ai-game-creator-shell/src/App.tsx、tests/appSurface.test.ts。
iOS 退款问询的 result_code 不是 debug 状态
- 现象:为了先观察真实 iOS 退款通知,回调返回
ErrCode=0 + IosRefundQueryResponse.result_code=1,并把 evidence 写成“调试阶段不执行自动退款决策”,看起来像安全 ACK,实际已经向微信建议拒绝退款。 - 原因:
xpay_subscribe_ios_refund_query_notify只有result_code=0(建议退款)和1(建议拒绝)两种正式决策;evidence必须是可审计的履约或消耗事实,不存在中立调试值。与此同时,解密后的完整 payload 含 OpenID、Apple 交易号、退款原因和票据,不能为了排障直接落日志。 - 处理:未接入真实履约决策时返回非零
ErrCode让微信重试,不携带IosRefundQueryResponse;所有事件只写脱敏结构化摘要,payload、未知事件/字段、标识符和字符串值使用消息 Token 加用途域派生的稳定 HMAC 引用,自由文本只写长度和 HMAC 引用。Android 订阅成功和普通 goods 通知可能同形,payload marker 只能快速分流;只有存在同号wechat_mp_virtual本地充值订单,并在 2.5 秒内通过/xpay/query_order校验订单号、金额、order_type=0/7、支付状态和权威paid_time,才允许入账。Apple 通知缺少WeChatPayInfo.PaidTime时走查单,绝不能用本机时间补齐。 - 验证:
cargo test -p platform-wechat virtual_payment_debug_summary --manifest-path server-rs/Cargo.toml、cargo test -p api-server virtual_payment_debug_routing --manifest-path server-rs/Cargo.toml、cargo test -p api-server virtual_payment_ios_refund_query_has_no_fake_decision_response --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、docs/【技术方案】微信虚拟支付接入-2026-05-26.md。
微信支付 V3 的支付 notify_url 不会自动接收退款结果或发现全部手工退款
- 现象:普通微信支付成功回调已经配置并可达,但在商户平台或代码里发起退款后,
/api/profile/recharge/wechat/notify收不到退款单状态变化;商户平台手工退款也可能没有请求本系统的退款回调入口。 - 原因:V3 支付成功通知与退款结果通知是不同契约;代码发起退款时,退款通知地址来自每次
POST /v3/refund/domestic/refunds请求里的notify_url,支付下单使用的WECHAT_PAY_NOTIFY_URL不会自动复用。商户平台手工退款不能假设会携带本系统按 API 请求传入的回调地址;退款接口返回成功也只表示受理,不能当成退款终态。 - 处理:代码退款显式传入公网
https://<API 域名>/api/profile/recharge/wechat/refund-notify,并用稳定out_refund_no串联申请、重复通知和主动查单。回调先用原始 body 验签、检查正负 5 分钟时间窗,再用 APIv3 密钥解密;校验事件、资源类型、商户号和退款状态后,将 callback observation 写入统一 SpacetimeDB 事务,持久化成功才返回204。正式链路不再是“debug 只记日志”:部分 / 全额退款、泥点回收、欠款冻结和会员人工复核均由事务收口;未知事件、校验或持久化失败返回微信FAIL响应。另开启WECHAT_PAY_REFUND_RECONCILIATION_ENABLED=true,对order_missing / order_not_paid继续等待晚到支付通知,候选退款按分钟轮转分页且错误日志不回显 provider URL;次日 10 点后按分片补扫微信 API 可查询的近 90 天bill_type=REFUND交易账单,并在落账前再主动查单。单行失败不能阻塞其他行或日期,也不能提前写完成 checkpoint;昨日NO_STATEMENT_EXIST至少延迟到次日 10 点后再确认;不要为联调开放未鉴权公网退款或补录接口。 - 验证:
cargo test -p platform-wechat v3_refund_notify --manifest-path server-rs/Cargo.toml、cargo test -p platform-wechat v3_transaction_notify --manifest-path server-rs/Cargo.toml、cargo test -p api-server v3_refund_notify_failure --manifest-path server-rs/Cargo.toml;真实联调后只读核对profile_recharge_refund、profile_recharge_refund_observation、profile_recharge_order_refund_settlement和profile_recharge_refund_bill_checkpoint。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、server-rs/crates/api-server/src/app.rs、docs/【技术方案】微信虚拟支付接入-2026-05-26.md。
已 ACK 的历史退款通知不会因正式落账上线而自动重放
- 现象:微信侧退款已经是
SUCCESS,旧 debug 回调也曾返回204,但部署正式退款表和权益回收事务后,本地充值订单仍为paid,退款表没有记录。 - 原因:微信收到成功应答后会把该次通知视为已送达;服务升级不会让已经 ACK 的历史通知自动重放。主动 reconciliation 只能继续查询本地已经知道
out_refund_no的非终态退款,不能凭空枚举所有历史退款。 - 处理:已知
out_refund_no时,由持有真实商户凭据的受控服务端先调用单笔退款查询,验微信响应签名后写入统一 observation 事务;未知的商户平台退款等待 T+1REFUND交易账单发现,再查单落账。自动账单按分片补扫微信 API 可查询的近 90 天,超过窗口的数据需从商户平台导出候选后逐笔受控查单。禁止用 SQL 直接把订单改为refunded,也禁止直接插入退款表或按账单 CSV 状态扣泥点,这些做法会绕过不可变字段冲突校验、累计部分退款和权益结算。 - 验证:核对退款 observation 的
source、resolution_code与金额,再核对订单级 settlement 的累计退款、recovery_status、unrecovered_points和wallet_frozen;全额退款应保留原订单paid_at,防止错误恢复首充资格。 - 关联:
server-rs/crates/api-server/src/profile_recharge_refund_reconciliation.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
退款请求结果未知时不能立即释放钱包占用
- 现象:后台调用微信退款超时或连接中断,页面提示状态未知;如果服务端立即释放泥点占用,用户可以继续消费,而微信稍后仍可能完成退款,最终形成可避免的退款欠账。反过来,永久保留占用又会让一次明确未创建的退款长期冻结余额。
- 原因:HTTP 错误只能说明客户端没有拿到确定响应,不能证明微信没有受理;单次退款查单
RESOURCE_NOT_EXISTS也可能处于短暂传播窗口。只有使用原out_refund_no主动查单才能继续判定。 - 处理:网络结果未知时保留活动 hold,页面把原
requestId、订单、金额、原因和已知out_refund_no保存到当前后台标签页、当前管理员会话隔离的sessionStorage,有效期 2 小时;刷新后必须与后端 active hold 的金额、原因和退款号对账一致才允许直接复用原请求,服务端明确拒绝时清理上下文。没有原请求上下文、上下文过期或管理员会话已切换时只开放预填out_refund_no的安全查单登记,不生成新 ID 硬撞活动 hold。worker 在占用创建至少 10 分钟后查同一退款号,查到退款就将验签事实写入统一 observation,只有连续 3 次查单收到官方RESOURCE_NOT_EXISTS才释放。进程重启清空连续次数并重新观察;超时、签名、配置、解析等错误一律重置次数并继续占用。 - 补充:退款查单适配器必须保留微信
RESOURCE_NOT_EXISTS业务码,并兼容同类ORDER_NOT_EXIST,不能把所有非 2xx 都抹平成通用上游错误;签名有效的退款申请/查询响应仍须与本次out_refund_no、订单号、交易号和金额做关联校验。已释放 hold 复用旧requestId时必须在调用微信前拒绝,并要求重新预检生成新的请求 ID。 - 关联:
server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/api-server/src/profile_recharge_refund_reconciliation.rs、profile_recharge_refund_hold。
时段 UV 不能伪装成单日趋势
- 现象:访问人数卡显示整段时间有数百人,但趋势图只有终止日一根满柱,其余日期是同样高度的小短柱;横向滚动后还容易误以为后半月突然出现访问。
- 原因:把时段跨日去重 UV 通过单值 series 塞进
anchor_date_key,再对 0 值强制设置最小可见高度。时段 UV、每日 UV 和每日 UV 之和是三个不同指标,不能互相替代。 - 处理:趋势 bucket 按
day_key + user_id每日去重,图头单独使用整个筛选范围跨日去重人数;0 值高度必须为 0。多张同轴图使用同一日期范围并同步横向滚动,快捷范围不生成未来日 bucket。 - 验证:构造同一用户跨两日访问与某日零访问的 fixture,断言每日 bucket、时段去重总数和零值柱分别正确;浏览器核对四图首尾日期窗口一致。
运营聚合不能用固定 LIMIT 的原始事实冒充精确结果
- 现象:Dashboard 出现“达到单次读取上限 50000 行”告警,但模块分布和累计值仍以无“不完整”标识的精确数字展示;数据增长后结果会随任意截断样本漂移。
- 原因:api-server 拉取
SELECT ... LIMIT 50000原始事实再聚合,没有完整分页、稳定排序或数据库侧聚合。提高上限只会推迟错误,并增加响应体与内存压力。 - 处理:精确运营指标通过受 runtime service identity 限制的 SpacetimeDB procedure 在事务内聚合,只返回紧凑统计投影,再由
spacetime-clientfacade 交给 BFF。权威聚合失败时请求必须失败,不能用默认 0 代替未知值;现有索引无法覆盖跨 scope、跨日期统计时先监控事务扫描耗时,数据增长后补日期前缀索引或持久化日聚合事实,不能退回固定LIMIT。若某查询只能采样,契约和 UI 必须明确标为采样,不能展示成精确值。 - 验证:聚合结果不随 HTTP SQL 行上限变化;超过 50,000 条事实时仍无截断告警,并用数据库事实抽样对账每日、时段、累计与分布结果。
后台详情列表的 grid 规则不要命中嵌套身份组件
- 现象:素材查询或精选素材详情弹窗中的作者陶泥号被逐字符竖排,用户详情按钮也被挤到编号旁边;窄屏下图片与详情列继续互相挤压。
- 原因:
.admin-info-list div会命中列表内所有后代div,把字段值内部的.admin-inline-identity和昵称容器也覆盖成双列 grid;陶泥号又允许任意位置换行,最终只剩单字符宽度。素材详情布局若始终固定为220px + 信息列,移动端也没有足够空间。 - 处理:信息列表的行布局只使用直接子选择器
.admin-info-list > div;作者昵称与陶泥号在身份组件内分行,陶泥号保持单行并在真正不足时省略。560px以下的素材详情改为单列,缩略图居中;素材查询与精选审核共用该规则。 - 验证:在桌面、560px、390px 和 320px 浏览器宽度打开素材详情,确认
.admin-inline-identity的 computeddisplay为flex、陶泥号横向显示、详情字段不溢出页面。
Provider 瞬态重试不能只依赖进程内 sleep,也不能让刷新时间进入请求指纹
- 现象:Provider 首次规划请求发生 timeout/connectivity/transport 后,Runner 在 backoff 期间退出会丢失 retry,或重启后清零 attempt、提前补发;另一种偶发现象是 Goal pause/resume 看似保留 sidecar,但只要等待跨过一秒,恢复请求就被判为 context drift,旧
-transient-Nattempt 被删除并改成新 loop 请求。 - 原因:退避 attempt 和到期时间只存在于进程内;或虽然已有 sidecar,请求 prompt 却直接序列化 Runtime 工具策略快照,把每次刷新都会变化的
updatedAt带进 request fingerprint。同一权限内容因此仅因时间变化产生不同请求身份。 - 处理:先闭合物理请求 lifecycle,再原子持久 retry sidecar/.previous,最后投影
waiting-for-provider-retry并释放 lane;Runner 启动扫描并按绝对到期时间恢复。请求指纹只绑定实际 Provider 请求的稳定语义,UI/审计时间戳、剩余等待毫秒和等待态文案不得进入 prompt。Goal pause 保留 sidecar,resume 必须校验同一 Goal/steer/request/config 身份后恢复原 attempt。 - 验证:测试必须故意让 pause/resume 跨秒,捕获失败请求与恢复请求的 HTTP body 并比较 SHA-256,同时断言 lifecycle 使用原
-transient-Nslot而不是新 loop;另覆盖.previous扫描、Runner idle blocker、同 Agent FIFO、跨 Agent 并行、cancel/steer/耗尽清理和重启未到期零请求。真实网络门禁不能只在retryAt前留一个静默窗口后等待请求,代理还要用不含 URL/header/body 的毫秒 metadata 证明第二个请求acceptedAtMs >= retryAtMs。 - 真实验收陷阱:sidecar 按设计早于 task/state 等待投影落盘,验收器看到 sidecar 后必须继续等完整
running / waiting-for-provider-retry,不能把合法提交窗口误判为 torn projection。共享 Runner 强杀会同时中断其它 Agent 的 in-flight Provider 请求;要隔离验证单个持久 retry,应在子请求产生前对父 Agent 首次规划注入故障,恢复后再完成同一 run 的并行协作。首批同批双委派若只依赖自然语言提示会受模型波动影响,真实 suite 应使用正式 collaboration policy/preflight 固定两个指定 static Agent,并保留无正文的 batch 数量诊断。长链路还可能发生额外真实瞬态失败,不能用“全局 failed/retry 必须等于 1”把已正确恢复的网络抖动误判为注入失败;应按 request identity 锁定唯一受控链,额外 failure/retry 独立计数并继续执行全部 lifecycle、后继终态和零残留门禁。 - final-reply 恢复陷阱:不能把当前 Runtime
status / phase / currentAction投影或整份临时 tool-plan 放进要求跨进程稳定的请求指纹。前者在waiting -> planning -> response恢复过程中必然变化,后者的planUpdate/actions不会由 context bundle 原样保存;两者都会让合法-transient-N被误判为 drift。final-reply 必须在请求前同步 response 状态与 context bundle,并只使用可由 bundle 精确恢复的有界收束摘要;恢复 pass 先识别final-reply或final-reply-context-compactionsidecar、恢复原 loop 并跳过新 planning。Agent DB lifecycle 的 requestKind 白名单也必须同步扩展,否则压缩请求会在网络调用前失败并被 planning fallback 掩盖。测试必须制造两种 sidecar-first 窗口、调用恢复扫描、按网络接收时间证明acceptedAtMs >= retryAtMs,并比较失败/恢复 HTTP body 的 SHA-256 和字节一致性;失败输出不得打印正文片段。Provider 成功返回到压缩 sidecar 或 finalization journalprepared之间仍不是 durable 提交点,进程退出可能重发 Provider;finalization journal 清理后才标记 stream committed 的窗口也可能留下 assistant 已落盘但 stream 仍为 ready。在新增成功响应 journal 与可恢复 stream commit 前不得宣称成功请求 exactly-once 或 stream 终态事务。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/provider_retry.rs、agent.rs、runner.rs、tests.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
Provider 成功不等于已交接,stream ready 也不等于 finalization 已完成
- 现象:Provider 已返回完整 final-reply,Runner 在 finalization
prepared前退出后却再次请求;或 assistant/completed 已唯一落盘,response stream 长期停在ready / streaming。更危险的修复是看到 handoff 与 retry 同时存在便择一删除,或因为另一个 Agent 已推进全局 project revision,就把当前固定 run 的 stream 当成不可见缓存并静默跳过提交。 - 原因:Provider 网络 future、成功 handoff、compaction/finalization journal 和 response stream 是连续但不同的 durable owner。只有内存中的成功响应、
startedlifecycle、流式半句或ready展示缓存都不能证明下一 owner 已接管;面向 UI 的 stream 可见性还会读取当前全局 revision,不适合作为 finalization 的提交判据。 - 正确顺序:成功响应先规范化并写入
game-creator-provider-handoff.v1,原子落盘并回读一致后,才用 handoff 保存的真实 requestId 补completedlifecycle。恢复先修复该真实 requestId,再零网络回放。压缩结果先持久化并回读 compaction sidecar 后再清 handoff;final-reply 至少先进入 finalizationprepared,journal 持续负责唯一 assistant、Runtime/Goal completed 和 stream committed,直到 committed 写入后的身份、状态、正文回读全部成功才清理。 - 冲突处理:handoff/retry 只有完整 identity、attempt 和 slot 一致才可把 retry 当作已被成功结果覆盖;冲突时必须零网络进入 reconciliation,并保留两份 sidecar、
.previous和真实 requestId 证据。stream 身份或正文与 journal 冲突时也保留 journal;禁止覆盖冲突 stream、删除 journal、补造 requestId 或靠重复 Provider 调用“刷新”现场。Runner 对存在、备份或损坏 handoff 的 root 都必须报告 busy,不能为 idle shutdown 自动删证据。 - stream 恢复:finalization 使用 journal v4 固定的 Agent/task/Session/run/request slot/steer cursor/response revision 和正文直接读取提交面。缺失或
streaming可由 journal 重建为规范ready再提交;已 committed 且正文一致可幂等清理。即使全局 project revision 已被其它 Agent 推进,也不能跳过这个既定 run 的 stream;写入、回读、固定身份或正文任一不一致,都保持 journal 和可恢复 finalization。 - 排障与验证:先核对 handoff 的
providerRequestId / requestSlot / attempt与 Agent DB lifecycle,再看 retry/handoff/finalization/response-stream sidecar,最后才看 Runtime/UI 投影。用关闭 mock Provider 后恢复证明 handoff 回放零网络;分别覆盖 compaction 与 final-reply 消费窗口、handoff/retry 冲突、stream 缺失/streaming、commit 写失败、committed 后清理前退出、全局 revision 漂移和 Runner busy。日志与断言只公开指纹、字符数、状态和差异字段,不能打印 handoff 正文、请求体、凭据、URL 或绝对路径。 - 保留边界:Runner 若在 Provider 成功后、handoff 原子提交并回读前被硬杀,本地仍只有结果未知的
started,不能安全补发或宣称 exactly-once。handoff 只覆盖无 tool call 的context-compaction / final-reply-context-compaction / final-reply;tool-plan及其 function arguments 不在内,真实外部 Provider 的 final-reply Runner 强杀门禁也需单独完成。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/provider_handoff.rs、provider_retry.rs、agent.rs、project.rs、runner.rs、tests.rs、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
final-reply 强杀门禁不能按请求正文选目标,也不能在协作证据未闭合时动手
- 现象:复用 fail-first 代理后,suite 仍只命中第一个 tool-plan;或者为了定位 final-reply,让代理 selector 读取 URL/header/body;又或者把父 run 的
project.verify误当成注入许可,没有在目标请求 reset/forward 前运行可信宿主检查。最终即使出现 failed/retry 和唯一 assistant,也不能证明 Project Supervisor 收尾边界真实可恢复。 - 原因:网络 sequence 本身不表达请求种类,Provider payload 又包含正文、工具上下文和凭据,不能成为故障选择 API。final-reply 前的
2初始 delivery、1repair delivery、3receipts 和零 assistant 用于证明“父 run 已完成协作、正要唯一收尾”;真正允许故障注入的项目正确性 oracle 是 selector 在 disposable project cwd 同步运行的可信宿主node verify-e2e.mjs成功 marker。父project.verifyaudit/receipt/observation 可能不存在,只能作为诊断计数。 - 处理:fault proxy 只向异步 selector 暴露冻结的
sequence / acceptedAtMs;harness 自己从 Agent DB、delivery/claim sidecar 和 conversation 中选择同一父 Session/run 的唯一 base final-reply,并先验证2+1delivery 已 claim、两次 observed claim 覆盖3receipts、assistant 为0。selector 随后必须在 proxy reset/forward 目标请求前,以可信宿主 Node 同步运行固定的node verify-e2e.mjs;只有 stdout 含real-e2e-command=passed且 stdout/stderr 无失败 marker 才返回允许注入。失败、超时、非零退出或 marker 无效时不得注入,捕获的 stdout/stderr 只能用于内存判定与敏感扫描,不得写入 state、checkpoint、report 或公共日志;父project.verify只记录诊断。候选重复或前提不完整必须失败,禁止回退到首请求或解析正文。 - 恢复门禁:等 base final-reply 形成 failed lifecycle、retry audit、持久 sidecar 和完整
running / waiting-for-provider-retry后,才在30sbackoff 内 pidfdSIGKILLRunner。重启保持 Session/run/request fingerprint/attempt/slot/retryAt,retryAt前零请求,到期后只允许唯一-transient-1;父 tool-plan 数不得增加。task/Runtime 进入终态后还必须显式等待 pending、retry、handoff、finalization、confirmation sidecar 全部清零,并设置10s硬超时;看到唯一回复后立即采样到残留 journal 只能判该轮 FAIL,不能与后续清理或其它轮次拼接。 - 写锁竞争:并行 Agent 的
file.write可能与项目写锁竞争。若失败 observation 原样携带绝对锁路径,后续 pending 持久化会因安全门禁拒绝并进入needs-reconciliation,把原本可恢复的锁竞争扩大为持久状态故障。file.write / file.patch / file.delete应统一使用 Runtime 短等待项目写锁,file.write错误在进入 observation/pending 前脱敏,并以2条 Rust 回归测试固定短等待和脱敏边界。 - 验证:新命令为
npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e -- --config-dir <发布AppData绝对路径>;旧supervisor-swarm-transient-retry只保留首次 tool-plan 证明。fault proxy14/14、E2E self-test PASS、前端308/308及 shell typecheck、platform-llm 41/41、platform-agent 17/17、shared-contracts 7/7等确定性门禁已完成。真实外部 Provider suite 共执行六轮,前五轮均为 FAIL 且不得拼接:第一、二轮沿用既有失败记录,第三轮为终态后过早观察到1个 finalization journal,第四轮为 quality-review 普通 tool-plan 连续 transport/connectivity 失败耗尽重试且未进入目标故障,第五轮为上述写锁竞争与绝对锁路径问题;修复后第六轮在同一轮内完整 PASS。 - 第六轮证据:
gpt-5.5 / openai_chat,2条初始加1条 repair delivery、3条专业 Agent assistant,Project Supervisor base final-reply 目标与可信宿主 verify marker 门禁均成立;受控 Providerfailed=1 / retry=1、incidentalfailure=0 / retry=0,30sbackoff,pidfdclaim=2 / signal=2,Runnerresumed=true / identityStable=true。父 tool-plan 故障前后均为13,parent final-reply 与最终 assistant 唯一,response streamsequence=2 / committed;pending、retry、handoff、finalization、confirmation sidecar、全部重复计数及 API Key、私有正文、项目路径、正式配置路径和公共报告泄漏扫描命中均为0。V1.41 handoff 原子提交前的 unknown-result 和 tool-plan handoff 未覆盖边界继续保留。 - 关联:
apps/ai-game-creator-shell/scripts/llm-transient-fault-proxy.mjs、apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs、apps/ai-game-creator-shell/tests/llmTransientFaultProxy.test.ts、docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md。
SpacetimeDB 历史归档不能按文件名小于 snapshot 就全部删除
- 现象:看到最新
N.snapshot_dir后,把所有起始 offset 小于N的.stdb.log删除,或者只把旧日志上传 OSS 就宣称已有完整增量灾备。 - 原因:segment 文件名只表示该段最早事务;起始 offset 小于等于最新 snapshot 的最后一个 segment 可能跨越 snapshot 边界,重启仍需要它。历史归档也不会及时覆盖 control-db、program bytes、最新 snapshot 和 active segment。
- 处理:latest snapshot 必须是未锁定且存在同 offset
.snapshot_bsatn的完整目录,空目录或同名.lock存在时忽略。每个 replica 独立保留max(segment_start <= latest_snapshot)及全部后缀,只处理更早 segment 对;旧 snapshot 只保留最新一个。--storage-format files必须先发布完整 full catalog;history 对每个候选文件 CAS 对象、history catalog 和 full catalog 执行 HEAD 长度/SHA 验真,再复算边界与 stat fingerprint,最后发布并验真固定latest.json;pointer 失败时不得推进 state 或删除源文件。不要把在线逐文件 full 扫描当成跨文件一致备份,基线必须来自停库目录或已验证冻结副本。SSH 或工具超时后先检查 work-dir PID lock 与原进程,不要直接并发重跑;不要在 history 模式传--stop-service。定时任务通过 Server-Provision 的显式 profile 和仓库 drop-in 管理,启用前 dry-run 验证 baseline,切回 archive 时同时移除托管与现场遗留 drop-in;不要在/etc/systemd/system长期保留手写覆盖,release 必须建立和验证自己的 full baseline 与 work-dir,不能直接复用 dev 的本地 state。files 本地 state 只能保存去重后的 catalog 引用并使用 gzip 原子落盘;本地只保留 latest full catalog 压缩缓存,history/旧 full catalog 和紧凑 result 不得再次复制完整清单。metadata 压缩或清理失败必须早于/stdbhistory 源文件删除。 - 验证:dry-run 输出 replica 的
latestSnapshot、boundarySegment和候选清单;从另一台机器仅凭 OSSlatest.json自动定位 full catalog,创建目录、下载文件并逐项校验长度/SHA,启动隔离 data-dir 验证/v1/ping、snapshot restore、commitlog replay、module launch、代表性 SQL 与 reducer。备份门禁还必须覆盖 v1 JSON 到 v2 gzip 迁移、损坏 gzip 不回退、full 增量复用、本地 catalog SHA 校验、history catalog 清理与紧凑 result。 - 关联:
scripts/database-backup-to-oss.mjs、scripts/check-database-backup-to-oss.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Procedure 事务鉴权必须显式捕获调用者
- 现象:api-server 的 SpacetimeDB token identity 与
editor_generation_pricing_config.writer_identity完全一致,服务启动门禁也通过,但手动拆分图集调用find_editor_asset_group_source_and_return仍返回“当前 identity 无权调用模型生成运行时服务”。 - 原因:当前 workspace 锁定 SpacetimeDB
2.7.0,procedure 的事务闭包身份仍不应成为业务鉴权的隐式来源。把 runtime writer 鉴权写成require_*(tx, tx.sender())会让鉴权边界依赖 SDK 细节,升级后也容易回归。 - 处理:在调用
try_with_tx前通过let caller = ctx.sender()捕获真实调用者,再把caller显式传入事务函数;迁移、后台账号、runtime profile、外部生成等现有 procedure 已采用这一模式。npm run check:spacetime-runtime-access禁止编辑器 runtime writer 鉴权重新直接读取事务ctx.sender()。 - 验证:运行
npm run check:spacetime-runtime-access、cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema;使用当前 2.7.0 模块以 runtime writer identity 重试POST /api/editor/icon-spritesheets/slices,确认来源查询、分片素材写入与 cohort 完成不再返回 identity 403。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、scripts/check-spacetime-runtime-access.mjs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
Portal 弹窗必须在遮罩根节点携带平台主题
- 现象:项目库点击“重命名”后,标题、输入框和按钮仍显示,但弹窗面板及遮罩背景变透明,看起来像“改名界面的背景没了”。
- 原因:
UnifiedModal默认 portal 到document.body;若业务入口只在页面内层继承platform-theme,portal 根节点不会继承该容器的 CSS 变量。此时.platform-modal-shell的background: var(--platform-modal-fill)和.platform-overlay的背景声明都会失效。 - 处理:
UnifiedModal在portal=true时默认把AuthUiContext.platformTheme注入 overlay,共享白底弹窗和直接调用都不应再手工拼接主题 class。完全自绘的黑底预览显式使用portalTheme="none";已明确固定主题的弹窗使用light/dark;局部 CSS 仍固定白底且未完成暗色样式的弹窗,必须暂时显式固定light,否则会出现白底白字或深浅样式混杂;portal=false继续依赖原 DOM 主题作用域。裸createPortal若使用平台或画布 CSS 变量,必须改用相应的主题 portal 壳,不要用硬编码白底掩盖主题变量缺失。 - 验证:在真实
AuthUiContext.platformTheme="dark"Provider 下打开 portal 弹窗,断言 auto 弹窗的 overlay 携带暗色主题类,固定浅色弹窗只携带浅色主题类,panel 与遮罩的 computed background 均非透明;同时断言portalTheme="none"的黑底预览不被平台 remap。 - 关联:
src/components/project/ProjectGalleryView.tsx、src/components/common/PlatformToolModalShell.tsx、src/components/common/UnifiedModal.tsx。
自主试玩失败后的修复责任不能同时落给总控和专业 Agent
- 现象:专业 Agent 已交付新 revision,Project Supervisor 的固定试玩已通过全部业务交互断言,但双视口可见性等外围门禁仍失败;下一轮 Provider 被要求直接
file.patch,随后又被orchestratorOnlyAfterDelegation正确拦截,格式修复耗尽后父 run 失败且没有最终回复。 - 原因:试玩失败活性门只检查“必须出现项目 mutation”,没有区分父 run 是否已经存在 durable 协作事实;它与“进入协作后 Supervisor 只委派、读取、认领和验证”的策略形成互斥合同。
- 处理:同一失败 revision 上,尚无协作事实的兼容 run 可以保留总控直接修复;已有协作事实且总控只编排时,只允许创建新的
code-prototype后续修复委派,明确继承最新preview.validate诊断和game/index.html产物要求,不把它伪装成已有 repair delivery 的二次返工。专业 Agent 推进 revision 后,总控先验证新 revision,再重新试玩。 - 验证:回归测试同时覆盖“无协作时仍可直接修复”“有协作时工具目录只剩
agent.delegate”“专业 Agent 推进新 revision 后总控只能先复验”,并用supervisor-autonomous-playable-lane-defense真实 E2E 检查静态烟雾、桌面/移动浏览器、全部固定试玩断言、唯一 Supervisor 回复和零残留。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent.rs、apps/ai-game-creator-shell/src-tauri/src/tests.rs、apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs。
旧 revision 试玩失败不能越过 ready delivery 触发重复委派
- 现象:父 Supervisor 在较早 revision 的试玩失败后已经收到专业 Agent 推进的新 revision,且 delivery 处于 ready;但下一轮仍按旧失败创建第四次
agent.delegate,随后持续命中activeDelegations=3,即使最新项目已通过浏览器试玩也不能结束。 - 原因:试玩失败 liveness 先执行“协作后必须委派”,没有先检查同一父 run 的 ready 未认领回执和 active delivery 容量;生成合同又只要求提供固定
data-playtest-id,没有明确每个值必须唯一、可见和启用。 - 处理:旧失败仍存在但 ready 回执可认领或 active delivery 已满时,只允许
agent.run_status原子认领/观察既有委派;认领后验证当前 revision,再由父 run 重跑固定试玩。只有当前 revision 自身的新失败且没有待收束交付时才创建后续专业修复。固定试玩控件必须唯一匹配、可见、启用且真实可点击。 - 验证:构造
failed preview@旧 revision + ready delivery@新 revision,断言顺序为agent.run_status -> verification@当前 revision -> preview.validate@当前 revision,委派总数不超过 3;分别以缺失、重复、隐藏和 disabled 的固定控件验证浏览器失败关闭。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs、apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_game_build.rs。
隔离 AppData 的长 TMPDIR 会让 Chrome SingletonSocket 超限
- 现象:静态检查已通过,但
preview.validate在 Chrome 启动阶段以 status134失败;minidump 中是process_singleton_posix.cc的Socket path too long,页面逻辑和截图探针均未开始。 - 原因:真实 E2E 为隔离 Runner 创建的 AppData 路径较长,Chrome 又在
TMPDIR下拼接 SingletonSocket 名称,最终超过 Unix domain socket 路径上限。 - 处理:浏览器进程使用
/tmp/ga-browser-*短临时根,同时把 Profile 放在该根目录并显式传入子进程TMPDIR;项目证据目录不变。用真实长TMPDIR环境运行 Chrome smoke,不能只测临时目录字符串长度。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/browser.rs。
image.inspect 的桌面和移动截图别名必须绑定当前 run
- 现象:真实桌面/移动试玩已经通过并生成截图,Supervisor 随后反复调用
image.inspect的desktop.png / mobile.png,但工具只接受完整项目相对路径,最终耗尽 planning 循环。 - 原因:Provider 能看到固定截图名,却看不到持久证据路径中的 Agent、run 和 revision;让模型猜完整内部路径既不稳定,也会扩大私有路径暴露面。
- 处理:只为精确 basename 提供受控别名,解析到当前 Agent、当前 run 下最新数字 revision;显式路径继续按原规则处理。严禁跨 run 搜索“最近截图”,否则旧轮次成功证据会污染当前验收。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/image_inspect.rs。
Rust 大文件拆成嵌套模块后要同时核对路径、可见性和兼容重导出
- 现象:函数正文原样搬到
foo/bar.rs后,兄弟模块或原父模块突然无法访问;新增runtime_protocol::provider_retry后,原本指向 crate 根provider_retry的相对路径又会被新子模块遮蔽。为尽快通过编译而把全部符号改成pub(crate),或因unused_imports告警删除 facade 上的兼容重导出,都会扩大内部 API 或破坏旧调用面。 - 原因:
pub(super)永远指当前模块的直接父级,源文件下沉一层后原可见范围会随层级缩小;Rust 名称解析又会优先命中更近的同名子模块。facade 重导出是否属于兼容合同,也不能只按当前文件内有没有直接使用来判断。 - 处理:拆分前记录公开符号、可见性和测试路径。仅供同一 facade 下兄弟子模块调用的 helper 使用
pub(super)暴露给直接父级;确实需要供crate::agent兄弟模块调用的符号才最小化使用pub(in crate::agent),不得统一放宽为pub(crate)。访问 crate 根同名模块时显式写crate::provider_retry;入口 facade 保留原公开 API 和兼容重导出,确认为兼容出口但当前未直接消费时只在该重导出上局部添加#[allow(unused_imports)],不要全局 suppress 或机械删除。 - 验证:比对拆分前后的公开 API、测试名和测试路径,运行全 crate 编译与串行测试;同时搜索同名模块的相对路径、跨子模块 helper 和带局部 allow 的兼容重导出,防止后续整理再次回退。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs、apps/ai-game-creator-shell/src-tauri/src/runner.rs、apps/ai-game-creator-shell/src-tauri/src/process_session.rs。
共享工作树并行拆分期间不能启动全 crate 验收
- 现象:真实 E2E 在准备 CLI 阶段以 Rust exit
101退出,任务、run 和 Provider lifecycle 全为 0;同一代码在并行 Agent 停笔后可以正常编译。 - 原因:多个 Agent 虽然拥有互不重叠的写入文件,但全 crate 编译会同时读取所有模块。某个入口刚写入
mod、对应子文件尚未全部落盘时启动构建,会读到合法的中间态半成品。 - 处理:并行 Agent 只做各自 scoped 格式和测试;主 Agent 等所有写入方正式完成并关闭后,再在稳定共享树统一运行 crate fmt、全量测试和真实 E2E。编译前失败且
stdin/provider/task=0的轮次只能算 harness 准备失败,不能归因给 Runtime 行为。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/runner.rs、apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/。
确定性最终回复不能掩盖外部 Provider 的真实失败
- 现象:自主构建已经推进到 revision 5,浏览器试玩
37/37、Supervisor 计划8/8,但image.inspect视觉请求与最终回复命中同一 deserialize fingerprint;114个 Provider request identity 中仍有1个 final-reply failed,且没有 Supervisor assistant。只看项目已完成或后续确定性回复,容易把这轮误写成 PASS。 - 原因:项目 completion gates、用户是否收到收束回复和 Provider lifecycle 是否全成功是三个独立事实。确定性回复可以补齐已完成项目的用户出口,但不能反向证明失败的 Provider 请求成功,也不能覆盖失败 identity。
- 处理:兜底条件必须同时锁定
autonomous-game-build、project-supervisor、当前 revision completion gates 全通过和 final-reply 阶段;优先使用非空plan.response,为空时才生成当前 revision 已完成生成并通过静态、桌面和移动试玩的固定回复。普通 Agent、未收敛、门禁未通过或 reconciliation 一律继续失败关闭,并原样保留 Provider failed lifecycle 证据。 - 验证:把已有外部轮次继续标记为 FAIL。修复后另起独立真实外部 E2E,在同一轮同时证明唯一 Supervisor assistant、完成门禁、Provider lifecycle、零残留、零重复和零泄漏;复验完成前不得宣称外部 Provider 全链路 PASS,也不得与旧失败轮拼接。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/、apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/、docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md。 - 最新复验:新 fallback 已真实命中,父 Supervisor 为
idle / completed,turn.report为settled,唯一 assistant 为44字符;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为0。“完成后不回复”已解决,但该轮仍是 FAIL:105个 Provider identity 只有103 completed / 2 failed,两个原始专业 Agent 失败虽均由 repair 恢复,最终仍因supervisor-swarm-private-body-public-event-leak未通过。 - 泄漏定位:两个专业 Agent 的
149 / 123字符失败正文,分别对应 SHA-256 前缀494ce8 / 3089ad,进入4条event.detail和2条agent.runtime.background_task.failed.error。这些内容是 delivery result,不是 userTask、委派任务或对话正文;不要把该问题误归因到 final-reply fallback。 - 脱敏处理:私有
state.error与私有 delivery 应保留完整诊断;公共 event / agentDb 只投影errorSha256 / errorChars /稳定failureKind。禁止按正文黑名单打补丁,也禁止把失败状态伪装为成功来消除泄漏。 - 复验要求:公共投影修复后必须另起独立真实外部 E2E,重新核对同轮 Provider lifecycle、唯一回复、残留、重复和泄漏;当前仍未 PASS。
- 最终复验:修复后另起的独立真实外部轮次已取得
status=PASS、evidence=complete、privacy scan=complete。此前114identity 和105identity 两个 FAIL 仍是各自独立的历史失败,未与本轮拼接;最终 PASS 仅由这个单个新轮次的完整证据构成,当前状态现已 PASS。 - Provider 与恢复证据:
84个 Provider identity 的started / terminal / completed均为84,failed / retry / open / duplicate均为0。1个原专业任务为budget-exhausted,唯一 repaircompleted且recovered;最终 child 为2 completed + 1 historical failed,全部任务均已终态。 - 收束与项目证据:父 Supervisor 为
idle / completed,turn.report=settled,唯一 assistant 为297字符,completed audit=1,finalization stages 为4。项目 revision0 -> 4,game/index.html为7639bytes 且已变化,static smoke passed;lane-defense-v1的 desktop / mobile 浏览器验证均通过并取得37/37。 - 零值与清理证据:pending / confirmation / user-input / provider batch / retry / handoff / tool-plan handoff / finalization 残留 / reconciliation / duplicate 全为
0;Provider payload / private body / API Key / project path / config path / log / browser report leak 全为0;人工 approve / answer / steer 全为0。Runner 与 AppData 已清,项目因--keep-project暂留后由主线程清理。
第三轮并行拆分要分开处理测试作用域、兼容出口和稳定树验收
- 现象:把父文件中的测试整体下沉到
tests.rs后,原来可直接使用的 helper、validator 或平台 trait 突然无法解析;本轮具体缺失的是response_fingerprint、validate_ledger与 UnixAsRawFd。与此同时,facade 兼容重导出会出现unused_imports,并行写入期间启动全 crate 编译还可能读到其它 Agent 尚未完成的中间态。 - 原因:Rust 子模块不会继承父模块的私有
use作用域;兼容重导出的价值是维持旧调用面,不能用当前 facade 是否直接消费来判断;多个 Agent 即使写入范围互不重叠,全 crate 编译仍会读取全部模块,因而无法避开正在落盘的半成品。 - 处理:测试下沉时显式补齐自身依赖的 import,不把生产可见性为测试统一放宽。已确认属于旧调用面的重导出必须保留,只在精确重导出位置添加局部
#[allow(unused_imports)],不得按 warning 机械删除或全局 suppress。并行阶段禁止启动全 crate 编译、全量测试和真实 E2E;各 Agent 只执行自己边界内的检查,待所有写入方完成后由主线程在稳定共享树统一验收。 - 验证:稳定树统一运行
cargo fmt --check、cargo check、cargo check --tests、三个定向测试组、Linux 串行全量和确定性真实 Runner + Chrome E2E;同时核对原测试名、turn.report字段/顺序、raw JavaScript 哈希和兼容重导出。Windows cross check 若因宿主缺少交叉链接器而未进入项目代码,必须明确记录为残余验证缺口,不能写成项目代码已通过。
未执行的并行 stale 动作不能进入 needs-reconciliation
- 现象:两个专业 Agent 在不同文件上并行工作,一个 Agent 先推进全局 project revision;另一个 Agent 的 pending 写动作尚未执行,却因 revision 与 planning 快照不同进入
failed / needs-reconciliation。Swarm CLI 随即提前结束,父 Supervisor 仍在等待回执,项目 revision 甚至可能尚未包含核心游戏文件。 - 原因:旧实现把“执行前发现计划过期”和“执行后无法证明副作用结果”合并成同一种 reconciliation。全局 revision 会被任何合法项目修改推进,因此它能证明旧计划已过期,却不能证明尚未开始的动作产生了未知副作用。
- 处理:在 pending 标记为 executing 之前检查 revision 漂移;漂移时写入可恢复的 blocked observation,明确旧动作未执行并要求同 run 重新规划。文件写入和 patch 在项目锁内再做一次同样检查,防止预检后的竞态。只有动作可能已落盘、账本身份冲突、审计失败或持久记录损坏时继续失败关闭到 reconciliation。
- 验证:必须覆盖确认后 stale 动作和锁内 stale 动作两条路径,证明目标文件未改变、旧 pending 被收束、下一次 Provider planning 使用同一 run、最终状态可完成,并用单输入真实 external-provider E2E 验证并行专业 Agent 最终生成可试玩项目。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs。
自主模式不能保留任何 RequiresConfirmation 漏口
- 现象:
user.input_request已被拒绝,但模型选择项目权限或 MCP catalog 标记为确认的工具后,整个自主批次仍进入waiting-for-confirmation;重启还会忠实恢复这个等待态,形成永久阻塞。 - 原因:只在本地 command policy 中提升少量 auto-safe 工具,不能覆盖 MCP 动态 approval;只修改新请求策略,也不能处理旧版本已经持久化的 waiting batch。拒绝 observation 若仍带
executionMode=auto,通用续跑还可能把它错误投影为“已执行自动工具”。 - 处理:最终合并后的本地/MCP policy block 必须再次经过持久 Run Profile gate;自主 profile 的所有剩余确认统一转为 deny,混合批次整体零执行并同 run 重规划。恢复迁移以 aborted batch 为提交点、pending 为镜像,并把自动策略拒绝显式审计为
runtime-policy-rejected。 - 验证:同时覆盖 auto-safe 白名单、显式 deny、动态确认失败关闭、标准 profile 不变、完整恢复扫描、前缀副作用未发生、Session/run/profile identity 不变和 replacement planning 已发出;最后必须重新运行确定性与外部 Provider 的单输入可玩 E2E。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_recovery.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs。
只读职责不能用无边界关键词子串判定
- 现象:真实 Provider 已连续返回首批
code-prototype / quality-review委派,但四次格式修复都报“code-prototype 必须是非只读实现任务”;Provider lifecycle 全部正常完成,首批 batch 却始终无法建立,父 run 在 revision 0 失败。 - 原因:只读分类器用
contains("只读")判断任务。模型按修复提示把程序任务写成“非只读实现任务”或“不要只读检查”,否定式文本仍命中“只读”子串,因此同一正确修复会被永久拒绝。 - 处理:只接受明确的“只读审查 / 只读检查 / 只读验收 / 不得修改项目”等正向合同;判定前剥离
非只读 / 不要只读 / 不是只读 / non-read-only / not read-only等否定式标签。首批角色事实继续以 durable delivery 的 target 与 expectedArtifacts 为准,不能只依赖易漂移的任务文案。文本合同还必须落实到 Provider 计划边界:只读 delivery 只允许纯读取和状态观察,写文件、启动命令、推进 revision、修改任务/记忆/黑板/资产或继续委派必须在任何执行前拒绝,并只允许修复成respond_to_user。 - 恢复:给既有持久 batch 增加新职责时必须升级 schema;新 v3 按新合同失败关闭,旧 v2 collaboration batch 与 v1 contractless batch 继续按原 fingerprint 和创建时语义恢复,不能在反序列化时用新规则误杀升级中的无人干预轮次。
- 验证:正向只读与否定式可修改分别做定向回归,并额外让只读 quality Agent 尝试
file.write,证明目标文件和 revision 均不变且下一请求只广告respond_to_user;同内容 v3 batch 必须失败关闭,v2 必须可恢复。真实外部 E2E 必须证明首批两份委派建立、程序 Agent 实际推进game/index.htmlrevision、质量 Agent mutation 为零、父 run 最终 settled,并保持人工输入和终局残留为零。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs。
Gitea PR runner 不能把宿主 Docker socket 当普通 volume
- 现象:
act_runner的container.docker_host留空时,job inspect 会出现/var/run/docker.sock:/var/run/docker.sock;PR 脚本即使valid_volumes: [],仍可直接调用 Docker API读取其它容器、挂宿主目录或取得 runner 配置。另一类迁移故障是 rootless daemon可以启动,但 bwrap 在 job 内报No permissions to create new namespace、Failed to make / slave或Mount too revealing。 - 原因:空
docker_host会自动发现并把控制 socket 传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。Runner 2.0.0 还有一处独立合并缺陷:parseSystemPaths把systempaths=unconfined转成显式空 slice 后,mergo.WithOverride不覆盖 empty value,真实 job 又恢复 Docker 默认 masks,表现为配置文件写了 unconfined、手工docker runcanary 也成功,但 Actions job 仍在--proc /proc返回 EPERM。直接使用--privileged、外层CAP_SYS_ADMIN或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的198.18.0.0/15判断公网还会误拒所有公共依赖。 - 处理:先把 Gitea 升到至少
1.26.4,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无CAP_SYS_ADMIN,内部 Docker 仅 Unix socket,runner 固定docker_host: "-"。若版本仍有上述 empty-slice 缺陷,只做 merge 后保留MaskedPaths=[]/ReadonlyPaths=[]的最小补丁并固定自有镜像 digest,不对整个 HostConfig 启用 overwrite-empty。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用sudo -E,否则 sudoenv_reset会让 apt 丢失 proxy。 - 运维陷阱:从容器内运行 Compose 时,宿主
/opt/gitea-stack必须挂到容器同名绝对路径;挂成/stack会让相对 bind source 被 daemon解析为宿主/stack/...,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。 - 验证:同时检查 Gitea 版本、runner declare、外层
Privileged=false/无 CapAdd/无宿主 socket、inner jobBinds=[]、MaskedPaths=[]、ReadonlyPaths=[]、固定 image digest、/var/run/docker.sock不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。AI 原生壳的共享 Agent Runtime 后台锁 suite 固定单线程执行;并行全量出现锁或异步终态失败、逐项单线程全部通过时,修正 suite 调度口径,不放宽断言。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。 - 关联:
.gitea/workflows/project-ci.yml、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、docs/project-memory/shared-memory/development-workflow.md。
Unix 文件身份复核不能假定 Linux 的 dev_t 类型
- 现象:AI 游戏创作 Tauri 壳在 Linux CI 编译通过,但 macOS 上会在 Agent DB、External Runner owner 和 tool-plan handoff 的
fstatat身份复核中报i32 == u64类型错误;Tauri 失败后配套后端收束,终端还可能短暂出现 SpacetimeDB 订阅连接失败的连锁日志。 - 原因:
libc::stat.st_dev跟随平台dev_t,macOS 为有符号整数,而std::os::unix::fs::MetadataExt::dev()统一返回u64;直接比较会把 Linux 的类型偶合误当成 Unix 通用契约。 - 处理:与 Rust 标准库的 Unix
MetadataExt实现保持一致,先把st_dev / st_ino规范为u64,再与metadata.dev() / metadata.ino()比较;设备号、inode 和文件类型三重检查均必须保留。 - macOS 测试夹具:
std::env::temp_dir()可能返回/var/folders/...,而/var是系统兼容符号链接。需要真实项目根的 Runtime 测试应先 canonicalize 已存在的临时根目录,再创建唯一子目录;不得为了让夹具通过而放宽生产 Runtime 的项目根及祖先符号链接拒绝规则。 - 异步测试隔离:测试触发后台 continuation 后,必须等待对应 Agent lane 完整释放,再删除项目夹具或安装下一项全局 mock 配置;否则前一项后台任务可能抢占后一项的唯一 mock 响应,形成只在全量顺序执行时出现的跨测试污染。
- 验证:macOS 本机运行
cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml,并复跑 Agent DB、project owner 和 tool-plan handoff 的 Unix 相对句柄替换检测;Linux CI 继续覆盖原有安全回归。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project.rs、runner.rs、tool_plan_handoff.rs。
AI 游戏创作壳不能用全局或匿名身份发布本地模块
- 现象:
npm run agc在发布模块时先访问auth.spacetimedb.com并以 401 失败;改成--anonymous后首次可能成功,但再次启动会因匿名 identity 变化而 403。若把 403 当成可忽略警告继续启动,api-server 会连接旧 schema,随后持续输出external_generation_job、profile_recharge_order_expiration_timer等缺表订阅失败,Tauri 也可能在后端就绪前退出或迟迟不弹窗。 - 原因:本地 publish 默认继承开发者全局 SpacetimeDB 云端登录,离线时 standalone 无法校验 issuer;
--anonymous不是可跨进程持久复用的 owner identity;AI 游戏创作壳若再复用主站历史数据目录,还会继承旧数据库归属和旧 schema。 - 处理:AI 游戏创作壳固定使用 gitignored 的独立数据目录;standalone 就绪后先从
/v1/identity获取并按 data dir 而非监听端口持久化同一 API identity,再用数据目录内权限为0600的独立cli.toml执行spacetime login --token和 publish。旧端口作用域记录在同一 data dir 下身份唯一时迁移,存在多个不同身份时失败关闭,不能猜 owner。远程 server 继续使用正常登录配置;本地 publish 403 必须阻断 API/Vite,不得带旧 schema 降级启动。.app/dev-stack.json记录规范化 data dir,独立壳复用后端时必须同时匹配数据库名、专用目录和健康状态;缺少目录字段的旧状态不得复用。POSIX 启动器在spawn后立即监听error / exit、保存 detached leader 的 PGID、向外层登记句柄并用独立进程组收束 npm、Node、Cargo 和子进程;direct leader 先退出后仍向负 PGID 发信号清理后代,ready 前中断、超时或 ENOENT 也走统一清理,退出后确认 3080、8082、3101 均释放。 - macOS 日志:api-server 进程指标当前只实现 Windows API 和 Linux
/proc,macOS 必须跳过 observable callback 注册;不能每轮采集为每个指标重复打印“不支持平台”。Rust/Tauri 既有dead_codewarning 与一次性配置缺失提示不属于长驻重试日志。非 Linuxproject.verify校验npm run参数时必须越过--silent、--ignore-scripts等前置选项定位真实脚本名,不能固定读取run后第一个参数,否则会在 macOS 将合法验证误报为“缺少脚本名”并引发 Runtime 测试级联失败。 - 验证:定向测试覆盖同一 data dir 跨端口复用 identity、不同 data dir 隔离、旧 state/data dir 不匹配拒绝复用、spawn ENOENT 受控失败、direct leader 以 42 退出后同组 descendant 仍收到 TERM,以及后端 ready 前句柄已登记且超时清理。连续运行两次
npm run agc,两次都必须真实完成 module publish、/v1/ping、/healthz、Vite 3080 和 TauriRunning;稳定观察期间不得出现缺表订阅失败或进程指标平台告警,Ctrl-C 后三个端口和主 Tauri 进程均应释放。 - 关联:
scripts/dev.mjs、apps/ai-game-creator-shell/scripts/start-dev-stack.mjs、server-rs/crates/api-server/src/process_metrics.rs。
Swarm 人工测试不能复用正式客户端 Runner AppData
- 现象:
npm run agc:test:chat在进入聊天前报“Agent Runner 版本与当前客户端不一致,但旧 Runner 仍有任务,暂不能重启”;正式客户端仍能看到自己的待确认或委派任务,重复执行测试也持续失败。 - 原因:Runner 复用身份同时绑定协议版本和当前可执行文件 SHA-256。
cargo run重新编译后的 debug 二进制与正在运行的 release Runner 指纹不同,而旧入口只隔离测试项目、仍把正式 AppData 直接传给 CLI,于是测试会向正式 endpoint 发升级探测。正式 Runner 有 pending action、Provider sidecar、进程会话或非终态队列时拒绝退出是正确的安全门禁,不能通过强退或放宽 idle 判定让测试通过。 - 处理:正式 AppData 只作只读配置来源。每次人工测试在系统临时根创建
0700sentinel 隔离目录,只把主配置和可选 local overlay 私有复制为0600普通文件;不得复制 endpoint、lock、.previous或其它状态。LLM 检查与 Swarm CLI 全部使用隔离目录。退出时通过内部 CLI 请求runner.shutdown_if_idle,确认隔离 endpoint 消失后才删除配置;仍有任务或无法确认退出时同时保留测试项目和隔离配置并报告路径。正式 Runner 的 PID、bootId、端口和 executable fingerprint 必须保持不变。 - 验证:单元测试覆盖私有 inode、权限、local overlay、禁止复制 endpoint/lock/备份、符号链接拒绝、sentinel 清理和 endpoint 存在时拒绝删除;真实 smoke 使用隔离 AppData 启动并收束空闲 Runner,前后比较正式 endpoint 身份且确认正式 PID 存活,再检查本轮
/tmp项目和隔离配置均已清理。 - 关联:
apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs、apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts、apps/ai-game-creator-shell/src-tauri/src/runner/client.rs、apps/ai-game-creator-shell/src-tauri/src/cli.rs。
Swarm 队列 busy 不能直接当成 canonical run 可 steer
- 现象:继续已有项目时,Runtime state 仍指向旧的
idle / completed或cancelled / cancelledrun A,但 task ledger 已有更新的pending / queuedrun B;CLI 打印“已投递 B”后却立刻把 A 及其历史子 Agent 的 cancelled/budget-exhausted 报成 B 的失败。 - 原因:旧
runtime_is_busy同时包含当前 state 和队列汇总,调用方看到task_queue.pending > 0后仍从 canonical state 反推 steer、失败扫描和 turn report 的 runId;取消 tombstone 还会让恢复扫描在处理 A 后无条件跳过 B。底层拒绝 terminal steer 和保留 A 的真实失败历史都是正确行为,不能通过放宽门禁或删除历史记录修复。 - 处理:保留 queue busy 用于 Runner 存活判断,另由 Runtime 协议层提供唯一 steerable 判定。start mutation 返回实际
acceptedRunId,CLI 以它建立不可变 turn baseline;失败、reconciliation、用户交互、收束和报告只观察该 run。canonical 已推进到后续 run 时从 task journal 读取目标 run 的最终记录。旧 cancelled canonical 若仍有 pending 且无 running,恢复扫描跳过旧 run 的 pending action 恢复,直接启动队首 pending。若输入与已落盘 pending task 及最后一条 user 消息相同,则只观察原 run。Goal 路径也必须核对同一 Agent、Session、runId、Run Profile 和 steerable 状态。连续 run 的回复必须按确定性 finalization message ID 过滤;历史 specialist 失败必须以(agentId, runId)为键读取完整 journal,不能让滞后的非失败 state 删除 journal 已记录的失败;报告计数也不能退回recent_tasks的 12 条窗口。 - 验证:构造 cancelled run A、保留 A cancel tombstone、pending run B 和单份已落盘用户消息,证明恢复后 B 进入 running 并完成且 conversation 不重复。另覆盖观察 B 时忽略 A 及 A 子任务失败、观察 A 时仍正常失败、B 完成后 canonical 已推进到 C 仍可从 journal 收束 B、
turn.report.parentRunId始终为 baseline,以及 expected Goal runId 不一致时不选中目标。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs、apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs。
2026-07-25 autonomous-game-build 不能只检查 game/index.html 就宣称正式项目完成
- 现象:
agc:test:chat收束后可能只有可玩game/index.html,配置画布 Key 时可能再有一张首版美术图,但memory/、exports/仍为空,设计、数值、美术清单、音频需求和发布包装文件缺失;界面或 CLI 却仍可能显示已完成。 - 原因:“可玩原型”和“正式项目”共用了同一完成信号。首批静态委派只覆盖程序、质量和可选美术,Supervisor 完成门与
agc:test:chat收束检查又主要围绕game/index.html、静态 smoke 和试玩回执;seed manifest 中其他任务的终态和正式文件集没有成为硬门禁。 - 处理:将自主构建改为正式产物 DAG,按“设计打底 -> 数值/美术/音频并行 -> 程序整合 -> 质量/静态/试玩 -> 发布包装”分波调度。基础正式路径必须包含
memory/project.md、game/game_design.md、game/balance.json、assets/manifest.art.json、assets/manifest.audio.json、game/index.html和exports/README.md;配置画布 Key 时再额外要求assets/ui-prototype.png与assets/art-spritesheet.png,并校验非空、JSON 可解析、图片真实可读和 manifest 登记。 - 补充原因:历史 seed 状态未重置、普通 preview / smoke 自动投影和在
agent.run_statusclaim 尚未可靠观察时过早启动 DAG,都会让任务看似完成却没有真实执行;恢复扫描重新发布已经冻结或认领的 delivery,还会刷出重复agent.delegate.result_failed。 - 特殊边界:
assets/manifest.audio.json只是当前阶段的音频需求清单,不能冒充 BGM/SFX 已生成。无画布 API Key 时交付 7 项文本 / JSON / 代码 / 发布产物,并明确图片尚未生成;配置 Key 时额外要求两张受控画布图片,Key 无效或生成失败必须阻塞,不得用占位文件或伪造登记绕过。 - 收束:Supervisor 只有在当前轮所有必需 seed manifest tasks 都为
completed、正式产物齐全且可解析、当前最新 revision 的game.static_smoke和preview.validate通过后才能最终回复。严格agc:test:chat必须精确校验固定 16 个 manifest task 在同一父 Run 下各自唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection,并核对 current revision 静态凭证、桌面 / 移动浏览器 playtest、截图与报告;不能把 delivery 完成、历史 revision 成功、文件存在、非空、JSON 可解析或 PNG magic 命中当成正式完成。PNG 还必须通过 chunk CRC、zlib、scanline、PLTE 和未知 critical chunk 检查;当前脚本已同时绑定 current revision 的 static smoke 与浏览器证据。 - 处理:新自主根 run 重置全部 16 个 seed task;DAG 在
run_statusclaim 可靠观察后启动并等待项目写锁;普通 bookkeeping 不修改自主 DAG;已ready / claimed-by-parent的相同终态 delivery 重放按幂等成功处理,只有真实终态冲突才写失败。 - 验证:确定性
npm run agc:test已通过,16 个 manifest task exactly-once,最终 revision 为11,基础正式产物、两张画布 PNG、静态 smoke 和桌面 / 移动37/37试玩均通过,pending、reconciliation、Provider 失败、重复与泄漏均为0。该证据不冒充独立外部 Provider 验收。
终端真实测试不能混用配置参数、stdin EOF 和持续预览
- 现象:开发者第一次运行
agc:test:chat时必须先打开 GUI 才能配置 Provider;无 TTY 的脚本可能在 stdin 立即 EOF 后零任务成功退出,或者任务已经完成却继续等待 preview 的Ctrl+C,导致自动化看似卡死。若为图省事增加--api-key,密钥还会进入 shell history 和进程列表。 - 原因:把首次配置、手工多轮聊天、单轮真实测试和持续试玩当成同一个交互生命周期;同时让 GUI 与 CLI 使用不同配置入口,或把 EOF 既解释为“提交当前需求”又解释为“没有输入”,会让退出语义随调用环境漂移。
- 处理:GUI 与
npm run agc:config共用系统 AppDatagame-creator.config.json,终端隐藏输入 API Key 并禁止--api-key;更新时保留agentLlm、editorApi、mcpServers等其它配置,POSIX 权限维持目录0700/ 文件0600并原子替换。显式--config-dir必须以world.genarrative.ai-game-creator为独立叶目录,不能让向导对/tmp、AppData 根或共享目录整体 chmod / 重建 DACL。隐藏输入调用stdin.resume()后必须记住原 pause 状态,在成功、取消、异常和SIGINT / SIGTERM / SIGHUP路径恢复 raw mode 并pause(),信号恢复后重发;只移除datalistener 会让--configure-only、配置检查失败或 Ctrl+C 保持活动 stdin。缺配置时仅 TTY 人工会话可询问进入向导,非 TTY 立即失败并提示配置命令。 - Windows 密钥复制:
mode: 0o600和 POSIXchmod在 Windows 上不能代替 DACL。隔离 AppData 目录必须先设置仅当前用户、禁止继承的 DACL;目标配置文件先以空文件创建并收紧 DACL,之后才允许把 API Key 字节写入。先copyFile再依赖 Rust 只读检查或事后收紧会留下密钥暴露窗口,也可能因继承 ACL 不满足 Runtime 合同而在首次--llm-status失败。 - Windows PowerShell 参数:不要把 DACL 目标路径和目录标记直接追加在
powershell.exe -Command <script>后;Windows Nodespawn会让 PowerShell 5.1 把这些值拼入命令文本,带空格的临时路径会被拆分并使GetFullPath($args[0])失败。当前实现只通过子进程私有环境变量传入路径和布尔值,并由真实 Windowsnpm run agc:typecheck覆盖 DACL 回归。 - 跨平台临时路径比较:macOS 的
os.tmpdir()可返回/var/folders/...,而realpath会返回同一目录的/private/var/folders/...;Windows 也可存在驱动器号大小写、junction 或链接解析差异。测试安全路径函数时,fixture 期望值必须基于平台原生realpath后的根目录构造,不能直接与mkdtemp的逻辑路径字符串严格比较。不能只依赖开发机自带的路径别名;CI 应创建真实父目录、指向它的符号链接(Windows 使用 junction)和不存在的叶目录,断言安全函数返回真实父目录下的叶路径。realpath和全部断言都应放在try/finally内,确保失败也能清理 fixture。 - 超时与进程树:
setTimeout后只对直接 Cargo PID 调一次kill()不是硬超时;Cargo 启动的 CLI / Runner 仍可能持有 stdio,使close永远不返回,清理阶段也可能无界等待。POSIX 必须创建独立进程组并按负 PID 终止,Windows 必须使用taskkill /T;宽限期后升级强杀,Runner shutdown 和清理另设短硬超时。重复 Ctrl+C 也必须升级,不能一直被自定义 signal handler 吞掉。 - 模式拆分:自动化使用默认的
agc:test:chat,由脚本投递固定植物塔防需求;正式产物、最新 revision 静态检查和 Runtime 浏览器验收通过后立即收束并清理,不启动持续 preview。agc:test:chat:manual不注入--task,用于多轮 stdin 手工聊天,并保留 preview 直到显式退出;不能用 EOF 或是否存在game/index.html猜测当前模式。 - 验证:覆盖
--api-key拒绝、各平台 AppData 路径、Provider 预设、旧配置节点保留、URL 安全校验、原子写入、POSIX 权限、隐藏输入恢复 pause、Windows 写密钥前 DACL、整棵进程树超时终止、缺配置时 TTY / 非 TTY 分支,以及--task成功后不进入长期 preview、手工模式仍可持续试玩。测试和错误输出只验证“密钥已配置”状态,不读取或打印密钥本体;源码字符串断言和 Linux 上的 Windows mock 不能替代真实 Windows ACL /taskkill复验。 - 关联:
apps/ai-game-creator-shell/scripts/game-creator-config-wizard.mjs、apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs、apps/ai-game-creator-shell/scripts/check-config.mjs、apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts。 - 真实验收状态:外部 Provider 与画布 API 均可调用不等于全链路验收通过。2026-07-27 新起的独立轮次使用
npm run agc:test:chat -- --timeout-minutes 75,约59m50s后以退出码0完整 PASS:同一轮完成固定16个 manifest task exactly-once、七份基础产物、两张真实画布 PNG、当前 revision 静态检查、desktop / mobilelane-defense-v1playtest、唯一终态回复和安全清理;turn.report的 busy / pending / running / confirmation / user-input / reconciliation 均为0。此前失败轮、部分产物、单项接口成功和确定性结果仍不得与本轮拼接。
仓库回退配置模板不能被读取通道私有化锁定
- 现象:Windows 上
apps/ai-game-creator-shell/game-creator.config.json莫名其妙被"加锁"(DACL 被剥成只剩一个陌生 SID,连Get-Acl都 unauthorized),开发 agent 和其他用户无法修改,cargo 也因include_str!读不到文件而不能编译;手动解锁后过一段时间又被锁。 - 原因:无 AppHandle 的开发 CLI(
llm-status、agent-run、agc:test:chat等)经game_creator_config_paths()从 CWD /current_exe向上回溯 8 级探测到 worktree 里的 git 跟踪模板后,读取走了为 AppData 私密凭据设计的私有通道open_project_private_regular_file→prepare_game_creator_private_path_for_read;该函数名为 "for read",在 Windows 上却无条件收紧目标 DACL 为"仅当前进程用户、禁止继承"。沙箱 agent 是其 checkout 文件的 owner,校验通过后被静默私有化,其他账号全部 Access Denied。 - 处理:配置读取按路径归属分流——
read_game_creator_config_file只对位于game_creator_runtime_config_dir()(AppData 托管目录)内的真实凭据走私有加固读取;仓库旁边的回退模板 / local 覆盖一律走open_project_snapshot_regular_file非变异快照通道,读取绝不修改 owner / DACL。这与open_project_private_regular_file注释中"非用户明确选择的文件用 snapshot 读"的既有原则一致。 - 教训:任何名为"读前准备"的函数若附带权限收紧副作用,都必须按路径是否属于本进程托管范围设白名单;共享仓库文件、git 跟踪文件永远不在加固范围内。排查"文件莫名被锁"时优先查 DACL owner 是哪位 SID,再倒推哪个进程以该身份运行过。
- 验证:
tests::configuration::fallback_template_read_stays_on_snapshot_channel_outside_runtime_dir与runtime_config_read_stays_on_private_channel_inside_runtime_dir锁定两条通道的分流;node scripts/check-config.mjs通过。
项目总控空态和持久 Runtime 不能依赖同一份 Session 索引
- 现象:新项目尚未发消息时右侧总控区域只剩整块空白;已有
needs-reconciliationRuntime 的项目重新打开后,也可能看不到失败状态卡。 - 原因:空 Runtime 直接返回
null,没有稳定空态;总控首轮水合又只在 active Session 索引存在时读取单 Agent Runtime。若 Provider 成功响应交接失败并留下 Runtime 文件、但 Session 索引未完成持久化,专业 Agent 列表能读到总控状态,专用总控面板却仍保持runtime=null。 - 处理:总控面板在无 Runtime 时显示“尚未开始”入口;项目级 Runtime 列表中的
project-supervisor作为缺失 Session 索引时的恢复来源,并同步其 Session、响应流和 Runtime 状态。needs-reconciliation显示为“待核对”,不伪装成执行中。 - Windows 根因补充:
tool-plan成功响应在相对目录句柄下原子安装账本时,不能把非空FILE_RENAME_INFO.RootDirectory传给SetFileInformationByHandle(FileRenameInfo);该组合会稳定返回ERROR_INVALID_PARAMETER (87),导致每轮首个 Provider 响应都进入needs-reconciliation。应使用支持相对根目录句柄的NtSetInformationFile(FileRenameInformation),继续保留目录句柄锚定,不能退化成可受路径换绑影响的绝对路径 rename。“按句柄安装”失败应归类为tool-plan-storage,不能落入tool-plan-unknown。 - Responses 协议补充:格式修复会把上一次 Provider 输出作为
assistant消息追加到新请求。OpenAI Responses API 中 system / user 文本使用input_text,assistant 文本必须使用output_text;不区分 role 会收到Invalid value: 'input_text'的 HTTP 400。assistant 图片不能继续序列化为input_image,应在本地请求校验中失败关闭。 - Steer 后审计补充:等待持久 Provider retry 时用户 steer 会让同一 run、同一 loop 重新使用
loop-N-repair-0;tool-plan protocol / repair 审计的幂等身份必须包含appliedSteerCursor,否则新 cursor 的合法响应会与旧响应误报“内容冲突”。旧审计没有该字段时只按 cursor0兼容;不能删库、忽略冲突或改用 response fingerprint 作逻辑槽唯一键。 - 单调用 Provider 协作修复补充:若 Provider 每轮只返回一个 function call,Project Supervisor 的首批协作 repair 必须从触发首次协作缺口的响应开始,跨文本 JSON、OpenAI Chat tool call 与 OpenAI Responses function call 等格式修复轮次累积合法的
agent.delegate / agent.spawn_isolated。同一agentId后出现的 action 覆盖较早 action;agent.spawn_isolated是单批唯一槽位,修正版必须覆盖旧 action,不能因输入变化追加第二个 spawn。每轮再按累计结果计算缺失的静态 Agent,并把下一轮agentIdenum 收窄到明确缺失集合;missingStaticAgents=none表示没有指定 ID 缺口,不得生成enum=["none"],避免已满足的委派被重复生成或首批协作永远无法成批提交。 - Provider action 安全持久化补充:pending / provider action 的泄密检测不能因裸自然语言短语
api key直接拒绝,否则agent.delegate中“不要暴露 External Editor API Key”等安全约束会被误报并阻断首批协作。赋值形式只允许完整匹配受控的“未配置 / 不可用 / 禁止读取”等状态或固定无密钥降级说明,不能用starts_with放行none-but-secret、not configured; actual value ...等安全前缀后的凭据;**API Key**:、`API Key`:、API Key(生产):等装饰或限定标签也必须识别为赋值。结构化字段标记apiKey / api_key、Authorization / Cookie、token / Bearer以及已知 secret token 形状仍必须检测并失败关闭。 - Windows retry 扫描补充:
Path::strip_prefix(root)在 Windows 上得到的相对Path转字符串后使用反斜杠,不能直接传给只接受 portable/的 Runtime JSON sidecar 读取器;否则 Runner 重启或显式--agent-resume扫描已到期 retry 时会报“项目文件路径不能包含反斜杠”,任务持续停在waiting-for-provider-retry。目录扫描应按路径组件重组成/分隔的 UTF-8 相对路径,不要放宽全局路径校验。 - 恢复交互:
needs-reconciliation即使没有pendingToolAction,也必须提供显式“已核对,结束旧任务”;它只取消旧 run,不直接 retry。若取消后仍有 pending task,由 Runner 自动继续;只有队列为空且旧 run 已取消时,才允许创建新的 retry run,避免重复执行同一用户输入。自主构建 Supervisor 的 retry 不能改写为普通agent-background-tasksource,必须从已验证的原 Run Profile 绑定恢复project-supervisor-gui / project-supervisor-cli可信来源;不得只信可追加的 task journal。 - 验证:前端回归同时覆盖零历史、无 Session 的初始空态、无 active Session 索引但存在持久
needs-reconciliation总控 Runtime 的恢复展示,以及“先取消、队列为空后才重试”;真实 Windows 运行全部 tool-plan handoff 测试,确保相对句柄 rename、覆盖安装、回读和清理均通过。Responses 回归覆盖 system / user / assistant 文本分别序列化,并保留 userinput_text + input_image;Runtime 回归覆盖“无效计划 → repair transport 等待 → steer → 新 cursor 再修复”,断言 cursor0 / 1各有一条审计且不冲突。
固定画布产物返工不能变成任意覆盖,design-foundation 不能越权修程序
- 现象:视觉 Agent 发现候选图不合格后,可能先删除
assets/ui-prototype.png或assets/art-spritesheet.png,再用猜测的尺寸、比例或另一条路径重新生成;远端生成期间项目文件又可能被其它 Agent 更新,迟到结果覆盖较新的文件。design-foundation为了让静态或浏览器检查通过,也可能顺手改写game/index.html或自行启动 preview。 - 原因:把“允许一次语义返工”误解成“视觉 Agent 可以任意覆盖”,且只在 prompt 中描述角色职责,没有在 replacement 授权、文件写入、工具策略和提交时 fingerprint 上强制执行。
- 处理:固定 UI 与 spritesheet 路径、比例、尺寸、kind 和 label;普通生成
replaceExisting=false。只有 Project Supervisor 对已认领原 delivery 建立的唯一静态 repair,且父 run、目标 Agent 与expectedArtifacts全部匹配时,才允许replaceExisting=true原位替换;不得先删除固定正式产物,也不得对 repair 再 repair。请求外部生成前记录原路径 SHA-256,取得写锁准备提交时复算;不一致即按 stale fingerprint 失败关闭并保留当前文件。 - 职责隔离(2026-08-11 补充):
design-foundation只写memory/project.md、game/game_design.md和可选固定 UI 原型。Runtime 必须同时在单文件写入、patchset、delete 与工具 policy 层拒绝其修改game/index.html、其它实现文件、启动 preview / playtest、运行进程、执行project.verify、game.static_smoke或整项目恢复。它的两个固定文本产物由 Runtime 在收束门内验证;这不向模型开放验证工具,也不替代preview-readiness的最终静态 smoke 或preview-playtest的独立浏览器验收。 - 画布配置一致性:未配置 External Editor API Key 时,
design-foundation / art-asset-plan的委派合同与 manifest 终态投影必须一起降级为文本产物,不能仍把assets/ui-prototype.png / assets/art-spritesheet.png作为完成条件;配置 Key 时两张固定图片继续是严格必需产物。委派、完成合同和 manifest 投影必须读取同一配置事实,禁止一层降级、另一层仍要求图片。 - 验证与状态:当前回归已覆盖固定合同拒绝漂移、已登记 spritesheet 删除保护、静态 repair 授权、并发修改触发 stale fingerprint、
design-foundation的 write / patchset / delete 和 preview 工具拒绝。2026-07-27 的独立 75 分钟上限外部 E2E 已在同一轮完成两张真实画布图片、固定16任务、当前 revision 静态与双视口试玩并安全清理,当前状态为 PASS;后续改动仍须新轮复验。
manifest 波次不能持项目锁启动 child,终态投影不能依赖静态委派屏障
- 现象:同波多个 ready task 被标为
running后,后排专业 Agent 会在第 1 轮 Provider planning 前,或第 2 轮并行只读结果投影时,等待约 1 秒后直接报“项目正在被其他写操作占用”;另一些专业 Agent 的 Runtime 已completed,manifest 却继续停在running,并留下autonomous_ready_task.projection_failed。 - 原因:scheduler 持有
runtime.autonomous.schedule_ready项目锁时直接启动不同 Agent lane,child 立即申请同一锁构建首轮 Provider request;通用短等待耗尽后被误投影为 terminal failed。终态投影又复用了“开始下一波”校验,把仍在运行的独立静态委派屏障错误当成 child terminal projection 前置条件。 - 处理:scheduler 在项目锁内预占每个 candidate 的 Agent Runtime lane并落 durable journal,释放项目锁后才启动 drain;Runtime 通用项目写锁使用约 10 秒有界等待,统一覆盖 Provider tool-plan、并行只读结果投影及同 run 控制面写入。child terminal projection 只验证父 run 身份和活跃合同,静态屏障继续约束下一波调度与父 run 收束,但不阻止已完成 child 写回 manifest。
- 验证:定向回归至少覆盖 Provider planning和并行只读投影跨过大于 1 秒的 manifest 写锁、静态委派仍 running 时 child terminal 仍能投影、ready scheduler Profile / 幂等身份保持不变;真实验收必须新起父 run,证明同波 child 零锁竞争失败且 16 项均有唯一 terminal projection。
完成合同不能只绑定一个入口摘要,公开资源审计不能保存完整 prompt
- 现象:历史 completion contract 只绑定
game/index.html,可能在其它正式产物沿用旧文件时仍放行;画布生成成功后,asset.register又把完整source.prompt复制进公开 Agent DB,触发 provider payload / private-body 表面泄漏。 - 原因:把“入口变了”误当成“本轮全部正式产物都新鲜”,并直接复用资源 manifest 的完整 source 对象写公开审计。manifest 的本地来源元数据与公开 event / agentDb 的最小身份字段不是同一个安全边界。
- 处理:完成合同升级为
game-creator-autonomous-completion-contract.v2,baselineArtifacts必填并参与指纹;旧 v1、缺基线或提交前身份漂移失败关闭。资源 manifest 可以保留 prompt,但asset.register / asset.update写审计前必须清除source.prompt,只保留资源身份和模型。 - 并发补验:revision 漂移、repository context drift 和项目锁竞争只能在同一 logical run 有界重试;只有明确 blocker / repair 或成功
okobservation 才可重放终态,失败 observation 不能当通过,只读 Agent 不能借补验获得命令权限,completion 统计仍只能增加一次。
tool-plan handoff 不能把计划叙述和源码字段当成配置载荷扫描
- 现象:Provider 已返回 HTTP 200 并计费,tool-plan lifecycle 却只有
started,handoff 账本停在上一 loop,Runtime 进入needs-reconciliation;重启 Runner 或/resume后仍原样被屏障阻断。 - 原因:在解析 function arguments 之前,对整段
response.text和序列化 arguments 统一执行.env、game-creator.config等字面标记扫描。安全叙述如“无需读取.env”,或oldText / newText / content / patch中的普通源码字面量,会在真实路径和内容字段尚未区分时被误判。原始响应未成功交接时不会留下正文,因此现场只能结合 loop 边界和最小复现定位,不能把高概率分支冒充已恢复的原响应证据。 - 处理:计划叙述与规范源码内容字段只检查真实密钥 token 形状、凭据头标记和不安全控制字符;结构化敏感 JSON key、非内容字段的配置痕迹和绝对路径、真实 token、容量、thinking、身份、顺序及账本完整性继续失败关闭。成功 handoff 失败时只在 Runtime event/state 和 Agent DB 保存受控
failureKind、脱敏错误 SHA-256、字符数与 requestId,禁止保存正文、arguments、密钥和绝对路径。 - 验证:必须同时覆盖 narrative 和
oldText / newText / content / html / patch提及.env/game-creator.config可 round-trip,path=.env.local与sk-...真实 token 仍拒绝,全部 handoff 回归通过;诊断审计必须断言不存在error / response / arguments原文。修复后的外部 Provider 重试仍需新起独立轮次,不能与故障轮或确定性回归拼接为 PASS。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/content_validation.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs。
自然语言“继续修复”不能在无活动 Runtime 时落入空 resume
- 现象:旧 run 已取消且
/resume明确报告无可恢复任务,用户随后输入“继续”“继续之前干的事情”或“那就继续修复”,interaction 仍返回resume,宿主反复扫描后不新建任务。 - 原因:interaction 模型能看到会话历史,却不知道宿主已经排除了 active、pending 和可 steer Runtime;宿主又把自然语言
resume与显式/resume当成相同控制动作机械执行。 - 处理:活动 Runtime、排队任务和 Goal 仍在 interaction 前优先 steer/恢复;只有这些门禁全部为空后,自然语言 interaction 返回的
resume才规范化为execute并创建新 run。显式/resume继续保持纯恢复控制,不因无任务而隐式执行。 - 验证:保留自然语言必须进入统一 interaction loop 与显式
/resume命令测试,并新增无活动 Runtime 时Resume -> Execute、普通 reply 不变的回归。
tool-plan 中项目内绝对路径应在成功交接时规范化
- 现象:Provider 已成功返回原生工具调用,但
path / paths / cwd / outputPath等结构化输入使用了当前项目根目录内的绝对路径;交接安全门禁以tool-plan-absolute-path失败,run 进入needs-reconciliation,后续“继续”只能排队。 - 原因:Runtime 工具最终只接受项目相对路径,但 Provider 不一定始终遵守提示;交接层此前只能拒绝全部绝对路径,无法区分“当前项目内、可无损转换”的输入与项目外越界输入。
- 处理:成功响应写入 tool-plan handoff 前,只对内置 Runtime 原生函数和 legacy tool-plan wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做规范化;仅改写完整字符串且位于当前项目根目录内的
file.*.path、project.patchset.changes[*].path、project.git_commit.paths[*]、command.*.cwd、image.inspect.paths[*]与canvas.asset_generate.outputPath等无歧义真实路径字段。不要泛化改写command.*.args[*]:同一个完整绝对路径字符串在rg中可能是搜索 pattern,在其他程序中也可能不是 path operand;无法由固定工具契约确认语义的位置继续拒绝。项目根通过符号链接别名传入时,只解析根身份并保留根内原始相对后缀,使后续 Runtime 仍能拒绝内部 symlink / reparse point;写入目标末端尚不存在可正常处理,越出项目根的 alias 继续失败关闭。源码/叙述字段、动态 MCP arguments、项目外绝对路径、file://、路径加行号、畸形或重复 key JSON、敏感 key 和真实凭据继续拒绝。 - 诊断:拒绝合法 JSON 中剩余的绝对路径时,只公开固定枚举/白名单约束的
functionClass、jsonPointer、pathShape、relationToRoot、duplicateSafeJson和hitCount;数组下标与 object key 分开生成,未知或纯数字 object key 不原样公开,root 关系只做词法分类,不对任意外部路径执行 canonicalize。不得记录 arguments、路径值、正文、前后缀或可逆编码。Provider prompt 同时明确command.exec/command.start的 argv 项目路径必须相对cwd,禁止绝对路径、file URI、路径加行号或嵌入式绝对路径。规范化后的 handoff 同时作为当前进程执行值和重启 replay 值,避免 live/restart 语义漂移。 - 验证:覆盖项目内
path、项目根cwd、paths数组和符号链接根别名的相对化与落账重放,并锁定根 alias 之后的内部 symlink 仍以原相对后缀交给 Runtime 拒绝;覆盖command.exec/command.startargv 不猜测 path 语义,嵌入式 argv、file://、扁平化path、项目外 alias 继续拒绝且只生成安全字段定位;覆盖纯数字 object key、伪造/超长/非法枚举 diagnostic 不进入公开状态。源码content原文保持不变,动态 MCP 和项目外绝对路径仍拒绝,并运行全部 tool-plan handoff 与 Provider reconciliation 回归。 - 关联:
src/components/project/ProjectGalleryView.tsx、src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx、src/components/common/PlatformToolModalShell.tsx、src/components/common/UnifiedModal.tsx。
待用户确认的 Agent 工具不能依赖模型自行结束回合
- 现象:画布 Agent 已生成有效工具规划,却最终只保存
ERROR max turns reached: 3,助手文本和待确认工具卡都消失。 - 原因:八类画布工具的
call()只返回待用户确认的规划结果,但 function-calling runner 在成功工具后仍继续请求 LLM,只靠 prompt 要求模型不再重试;模型连续返回工具调用直到上限后,错误结果又丢弃此前累积的输出。 - 处理:工具通过框架契约显式声明
requires_user_confirmation;当本批全部工具都成功且等待确认时,runner 在处理完整批次后立即返回已有助手文本和工具结果。未知工具、参数错误、hook skip、普通连续工具和不可解析响应仍继续受max_turns门禁保护。不要用单纯提高轮次上限掩盖终止条件缺失。 - 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”“普通连续工具仍会触发 max-turn 门禁”“多工具按数组顺序执行”“request 级 system prompt 真实进入请求”;公共 prompt 在无工具时仍必须包含 runner 所需的 JSON 响应格式,且不得宣称并发执行。
- 关联:
server-rs/crates/platform-agent-harness/src/run.rs、server-rs/crates/platform-agent-harness/src/tool.rs、server-rs/crates/platform-editor-agent/src/agent/tools/。
待确认工具的 prompt 不能使用全局禁止重发话术
- 现象:为防止用户在对话中说“确认 / 可以 / 取消”时重复生成待确认卡片,prompt 加入“不得重新发起相同工具调用”后,模型在用户随后明确提出新生成、修改或重做请求时也拒绝调用工具。
- 原因:LLM 容易把面向“当前确认 / 取消意图 + 特定 pending 卡片”的限制过度泛化为跨回合、跨意图的全局禁止;单看工具名或参数相似度不能区分“重复确认旧卡片”和“用户明确发起新任务”。
- 处理:prompt 只用正向条件句描述当前回合:确认或取消意图确实匹配某条现存 pending 卡片时,引导用户点击该卡片按钮,本条意图不生成新 tool call。不添加全局的“禁止重发相同工具”规则。cancelled 卡片不再处理;用户要求修改、重做或新任务时正常发起新调用,pending 卡片不阻塞无关请求。
- 验证:业务 prompt 契约测试要同时锁定“匹配 pending 时引导确认 / 取消按钮”“cancelled 后可发起新调用”和“pending 不阻塞无关新请求”;模型实测必须另外覆盖同工具名的后续新任务,确认不会因过度泛化而拒绝。
- 关联:
server-rs/crates/platform-editor-agent/src/agent/prompt.rs、docs/【编辑器】画布Agent对话面板-2026-07-03.md。
Agent 终态失败不能吞掉已发生的工具事实
- 现象:同一轮 prompt 中前面工具已经成功生成待确认结果,但后续工具、hook、completion 或
max_turns失败后,API 只保存最后一条ERROR,已执行工具和用户本轮语义从会话历史中消失。 - 原因:runner 只返回单一
PromptError,或者直接向 committed memory 逐步写入,无法区分“尚未发生外部工具事实,整轮可回滚”与“已发生工具事实,只能提交并闭合错误”。工具失败若被压成字符串,调用方还会丢失kind、retryable、fatal和原始output。 - 处理:用
PromptRunError { error, partial_outputs }保留失败前输出,并将本轮 memory 先写入 staged buffer。无工具活动失败时整体回滚 staged 增量;有成功或失败工具活动时提交已发生事实,并追加 terminal error closure。api-server 按partial_outputs顺序先持久化成功工具的not_completed待确认消息,再追加ERROR终态消息;ToolFailed保留给调用方做诊断和流程决策,不伪装成成功确认卡。 - 取消边界:不能在 prompt future 内对
agent.memory.take()后跨 await 持有,也不能用统一VecMemorystaging 绕过自定义 memory 的限长、摘要或脱敏规则。AgentMemory::begin_staged必须产生行为等价、写入隔离的StagedAgentMemory,成功或已有工具活动时显式commit(),直接 drop 才表示回滚。外部 drop 若发生在工具完成后,guard 必须提交结果与取消闭环;若工具仍在执行,至少提交“已启动、结果未知”事实,供后续 reconcile。正式总 deadline 应作为 runner 内部 future 终止 completion;工具开始前检查 deadline,一旦开始则不能中途 drop,必须等待结果后再携带 partial outputs 收口。外层 timeout 只适合作为进程级最后保险,不能承担业务收口。 - 验证:至少覆盖“无工具 completion 失败回滚 staged 用户消息”“非 fatal 工具失败对调用方暴露
kind/retryable/fatal/output”“成功工具后终态失败保留 partial tool output”“有工具活动时 committed memory 末尾存在 error closure”以及“API 增量中待确认工具位于 terminalERROR之前”。 - 关联:
server-rs/crates/platform-agent-harness/src/run.rs、server-rs/crates/platform-agent-harness/src/tool.rs、server-rs/crates/platform-editor-agent/src/agent/prompt.rs、server-rs/crates/api-server/src/editor_agent/api.rs。
画布 Agent 的规划请求不能关闭瞬时失败重试
- 现象:美术 Agent 对话返回红色错误气泡
completion error: LLM 请求超时,累计尝试 1 次;HTTP 本身仍返回 200,前端 20 分钟 transport timeout 没有触发。 - 原因:规划请求虽然有 Agent 专用单次 timeout,但
vector_engine_llm_client把max_retries硬编码为 0;VectorEnginegpt-5.4-mini的偶发长尾、连接超时或可重试上游状态会在第一次失败后直接持久化成 system error。framework 的英文completion error前缀也被原样暴露给用户。 - 处理:120 秒改为前端软提示阈值:POST 仍 pending 时显示不入库的“仍在处理中,请耐心等待”;provider 明确断开/失败才写正式错误。专用 provider 单 attempt 使用 8 分钟 hard timeout,请求发起阶段读取
GENARRATIVE_LLM_MAX_RETRIES,但画布 Agent 最多重试 1 次且重试退避最多 60 秒。不要只计算单次 complete 的最坏时间:runner 还可因非法 JSON/工具校验失败进入后续轮次,必须从 handler 入口开始计算 18 分钟总 deadline,进入agent.prompt(...)时扣除会话锁/上下文准备已用时间,为持久化和前端 20 分钟 timeout 留出余量。响应头后的体读取/解析错误按明确失败收口,必须使用真实 attempt 计数;规划、配置和定价错误对用户统一为中文,原始诊断只记后端日志。重试发生在任何生成工具执行前,不会重复提交生成任务或扣费,不要通过提高前端 timeout 或 runnermax_turns掩盖 provider 重试缺失。 - 验证:
platform-editor-agent测试锁定 8 分钟 hard timeout 与中文错误;前端 fake timer 用例锁定 120 秒前只显示思考动画、到点后显示耐心等待、成功/失败后移除;platform-llm回归用例锁定第二次 attempt 成功响应头后的 body timeout 仍报累计 2 次;api-server测试锁定专用 client retry、18 分钟整体 deadline 与中文直达错误。运行态排障按同一 request id 对齐platform_llmfailure stage 与/messages总耗时,并确认仍 pending 的请求不再在 120 秒形成错误气泡。 - 关联:
server-rs/crates/platform-editor-agent/src/agent/agent.rs、server-rs/crates/platform-agent-harness/src/error.rs、server-rs/crates/api-server/src/state.rs、src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts、src/components/image-editor/EditorAgentConversation/MessageBubble.tsx、src/services/image-editor/editorAgentClient.ts。
前端退役目录不能只靠扫描和 ignore 隔离
- 现象:Tailwind
@source、TypeScript 根include、ESLint ignore 和 Vitest include 都排除了旧创作目录,但干净打开新版页面时,Vite 仍转换services/rpg-entry/index.ts,构建产物也包含旧作品库和旧 profile 逻辑。 - 原因:现役模块的静态 import 会让 Vite、TypeScript 和打包器递归解析依赖;watch ignore 只停止监听,Tailwind source 只控制 class 扫描,tree-shaking 也发生在模块已经加载之后。经 barrel 只取一个公共函数尤其容易把同文件的旧导出一起带回图中。
- 处理:把仍在用的公共账号 / 钱包 / 设置能力迁到明确的现役 client 与 presentation model;Vite
pretransform 对退役模块真实路径直接失败,ESLint 在现役源上增加 restricted imports。每次恢复公共 UI 后用tsc --listFilesOnly和全新浏览器 context 复核,不能用已有 HMR 会话判绿。 - 关联:
vite.config.ts、.eslintrc.cjs、src/services/platform-entry/、docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md。
SpacetimeDB schema guard 的基线不能递归扫描保留源码
- 现象:旧业务按“数据壳保留、业务实现退役”落地后,
check:spacetime-schema报几十个legacy_schema与原路径 accessor 重复;同一提交对自身比较也失败,但cargo实际可以正常编译 module。 - 原因:当前工作树按
Cargo.toml [lib].path的 crate root 可达模块扫描,基线提交却通过git ls-tree -r扫描整个spacetime-module/src。原src/lib.rs和旧业务源码只供追溯、不进入 active crate,但基线全目录扫描仍会把它们与#[path]引入的历史数据壳同时解析。 - 处理:current 与 base 必须各自读取所在快照的 Cargo manifest,并沿各自
mod/#[path]图扫描;base 文件存在性和内容从该 Git tree 读取,不能复用当前工作树。不要忽略legacy_schema、删除历史源码或吞掉 base duplicate,因为历史数据壳正是正式 schema,真实可达重复仍须失败。 - 验证:回归测试同时覆盖“不可达旧源码同 accessor 不报错”和“两个可达模块同 accessor 仍失败”;再运行
npm run check:spacetime-schema -- --base-ref HEAD,确认 self-base 按当前 136 张表通过。 - 关联:
scripts/check-spacetime-schema-guard.mjs、scripts/check-spacetime-schema-guard.test.ts、server-rs/crates/spacetime-module/Cargo.toml、docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md。
VectorEngine 请求超时不能脱离 worker 绝对预算(2026-07-20)
- 现象:VectorEngine 单次请求超时大于 worker job 执行预算时,worker 已停止续租,provider 才超时或开始重试;最终 lease 过期、任务失败并退款,上游却可能继续消耗资源或迟到成功。
- 原因:单 attempt timeout、重试退避、图片下载与 worker / lease 分别使用独立的相对计时,没有共享同一绝对 deadline;只抬高 worker timeout 或单独压低 provider timeout 都无法保证留出终态写回窗口。
- 处理:实际调用 VectorEngine 的四类图片 job 使用
1800slong 预算;从 job 开始的同一起点派生 provider deadline,常规提前60s、短预算提前一半。每次 attempt、退避、下一次 attempt 和图片下载都必须在该 deadline 内;普通 HTTP /inline不伪造 worker deadline。修复时不改动 lease fencing、迟到写回仲裁和原子退款语义。
同一 Rust 二进制的本地双进程不能各自并发 watch 重启(2026-07-21)
- 现象:本地把
api-server与独立bgfilter-worker都用cargo run -p api-server启动后,一次 Rust 源码变更触发两套 watcher 并发停止、编译和链接;Windows 常因另一个实例仍占用api-server.exe而链接失败,或出现 API 已恢复但内部 worker 尚未 ready 的半更新状态。 - 原因:两个进程角色共享同一 crate、target 和可执行文件,却被错误地当成两个互不相关的 dev service。更危险的是先启动
GENARRATIVE_PROCESS_ROLE=all的 API:它会立即消费外部生成队列,可能在内部 BgFilter worker 尚未 ready 时领取任务。 - 处理:
npm run dev与npm run dev:api-server只创建一套 Rust watcher,并把两个进程作为组合重启单元:先停止 API 与 BgFilter worker,再只让 worker 的cargo run完成必要构建,等待 worker/readyz,最后启动并验活 API。交互rs api-server、rs bgfilter-worker在完整栈内也必须走同一组合重启。ProcessRole::All永远不内嵌 BgFilter listener;父子进程共享解析后的内部 base URL / Token,Linux 第五端口固定为端口段start + 4,Windows 把第五端口纳入统一探测和漂移。 - 验证:定向测试断言组合重启顺序为“stop API → stop worker → start/ready worker → start/ready API”,
dev:api-server自动带起同 runner worker,端口解析得到五个互不冲突的端口;再运行node --check scripts/dev.mjs、dev-stack 定向测试和编码检查。 - 关联:
scripts/dev.mjs、scripts/dev-stack-port-utils.mjs、.codex/skills/genarrative-dev-stack-port-routing/SKILL.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
固定 digest 不等于每个 CI job 都要强制拉镜像
- 现象:四个 Gitea Actions job 在约 20 秒内同时失败,checkout 和测试都没开始;setup 日志显示 Docker 对已固定的
docker.gitea.com/runner-images@sha256:...发起 manifest HEAD,随后以net/http: TLS handshake timeout结束。 - 原因:镜像 label 固定 digest 只防止内容漂移;
container.force_pull: true仍会让每个 job 调用 Docker image create/pull 并依赖 registry 即时可用,即使 rootless Docker 本地已有该精确 RepoDigest。并发四个 job 还会同时放大同一外网 TLS 故障。 - 处理:继续使用完整 digest,把 Runner 设为
force_pull: false;首次部署或变更 digest 时,在切换 label 前对精确 digest 执行有界重试拉取,并用内层docker image inspect核对 RepoDigest。保留上一份 runner 配置和已验证镜像备份,切换失败时回滚配置,不改用浮动 tag。 - 验证:重启 runner 后先等待内层
docker info就绪,再 inspect 精确 digest 并确认 runner declare;重跑真实 PR 的四个 job,必须越过原先的启动失败窗口。运行中 job 仍要复核Privileged=false、Binds=[]、MaskedPaths=[]、ReadonlyPaths=[]和独立网络,防止稳定性修正意外放宽隔离。 - 关联:
docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、docs/project-memory/shared-memory/development-workflow.md。
四个 Gitea CI job 不要重复现场安装固定工具链
- 现象:
Repository checks、Frontend tests、Backend tests和Native shell tests都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。 - 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
- 处理:用
deploy/container/gitea-ci-job.Dockerfile预装 Node 22、固定 npm、Rust 1.96、rustfmt、Chrome、bwrap、rg、ffmpeg、clang/lld和 Tauri / 后端系统依赖,并按锁预热唯一根 npm workspace、server-rs、桌面壳与 AI 游戏创作壳 Cargo 四份下载缓存。四个 job 统一runs-on: genarrative-ci,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行scripts/check-gitea-ci-job-image.sh,同时检查四份缓存锁、工具链、完整 bwrap 与 Chrome headless。RUSTUP_AUTO_INSTALL=0;rust-toolchain.toml变更时先重建镜像,不把下载 fallback 放回 job。 - 依赖边界:每个 job 仍必须各自执行
npm ci,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把node_modules或 Cargotarget烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。 - 锁漂移边界:runtime 校验输出任一
*_cache_lock=partial说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实cargo fetch --locked --offline;cargo metadata --no-deps不会证明依赖 archive 可用,不能作为替代。 - 构建网络边界:
CARGO_NET_RETRY只覆盖部分 crate 下载,registryconfig.json/ index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个cargo fetch --locked再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。 - 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按四份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的单次根
npm ci和原有测试门禁仍全部执行。
Gitea Actions HTTPS CONNECT 隧道必须双向收束 socket(2026-08-07)
- 现象:CI 的
npm ci高频出现ECONNRESET / network aborted,Cargo 则出现 crates.io TLS EOF、连接超时或下载失败;同一出口 gateway 容器看似健康,却累计自动重启数百次,日志反复出现Socket.ondata -> Writable.write -> write EPIPE -> Unhandled 'error' event。 - 原因:HTTPS CONNECT 建立后使用
upstreamSocket.pipe(clientSocket)与反向 pipe,但只监听 upstreamerror;客户端在 DNS 等待、下载或 job 清理期间关闭连接时,pipe 继续向已断开的 client socket 写入,未处理的 EPIPE 会让 Node 进程退出。unless-stopped自动拉起和浅层 healthcheck 会掩盖崩溃,所有并发 npm / Cargo 隧道同时被 reset。 - 处理:CONNECT 一开始就为 client socket 注册
error / close,解析完成后为 upstream socket注册同样的双向销毁处理;DNS 返回、写 200 和开始 pipe 前都检查 client 是否已销毁。任一端 error、close 或 timeout 都幂等 destroy 两端,不把普通客户端 reset 写成错误日志。不要用进程级uncaughtException吞掉问题,也不要只增加 npm/Cargo 重试掩盖 gateway 崩溃。 - 验证:在独立 canary 和正式 gateway 上分别并发制造至少 500 次“CONNECT 后立即断开”,随后确认容器仍运行、restart count 不增加、日志无 EPIPE;再通过同一 proxy 对 npm registry 与 crates index 建立完整 TLS 隧道。切换前仍须确认 Gitea 无活跃 run 且 Runner 内层无 job 容器。
统一 npm workspace lock 不能丢失可选 WASM 包的 bundled 依赖节点(2026-08-21)
- 现象:根
npm ci在安装前失败,报告统一 lock 缺少@emnapi/core/@emnapi/runtime;错误版本可能是 registry 当前满足^1.11.1的最新版,而不是原 lock 中曾记录的版本。 - 原因:重写或解决根 workspace lock 冲突时,保留了 AGC 使用的
@tailwindcss/oxide-wasm32-wasi对 bundled@emnapi包的声明,却删掉了对应嵌套 package 节点。npm 会重新解析当前 registry 版本并判定 manifest 与 lock 不同步;这不是单一 npm 版本问题,也不表示应用应直接依赖两个@emnapi包。 - 处理:只在最新目标分支的仓库根执行固定 npm 的
npm install --package-lock-only --ignore-scripts,保留 npm 对全部 workspaces、bundled 节点及peer/optional标记的完整规范化结果;确认各 workspace manifest 没有意外变化,不要手工只补报错中的两个版本。 - 验证:至少用 Jenkins 对应固定 npm 和当前开发环境分别执行干净的根
npm ci,核对 bundled 节点后再运行npm run check:npm-workspaces、AGC typecheck、编码检查和git diff --check;禁止恢复独立 AGC lock 或子目录npm ci。
Windows 专属 Tauri resource 不能写进通用配置(2026-08-21)
- 现象:Linux CI 已完成根 workspace
npm ci,却在 Tauri custom build command 中报resources/codex/win-x64/...exe doesn't exist;Windows 侧车的 Rust staging 受cfg(windows)保护,因此非 Windows 构建不会生成这些文件。 - 原因:Tauri 会在所有平台校验通用
tauri.conf.json的 bundle resource 源路径;把 Windows x64 资源映射写进通用配置,等于要求 Linux / macOS 也预先拥有不属于其安装闭包的 Windows 可执行文件。 - 处理:通用配置只保留跨平台 bundle 项;Windows 原生侧车的完整白名单放入 Tauri 自动合并的
tauri.windows.conf.json。不要提交二进制占位文件,也不要让非 Windows build script 下载或伪造 Windows 资源。 - 验证:配置门禁断言通用配置没有 Windows resource、Windows 平台配置保留完整固定白名单;Linux 运行原生壳门禁必须越过 Tauri resource 校验,Windows release 仍由 build script 对 npm 原生包、SHA-256 清单和目标布局失败关闭。
AGC Skill 指纹与安装内容必须跨平台一致(2026-08-21)
- 现象:内置 Skill 文件集合没有缺失,原生测试却统一报内容指纹不匹配;安装后的 Skill 文件与 manifest 摘要不一致会导致构建校验失败。
- 原因:审核文件定稿后未按最终字节重新生成 manifest SHA-256;安装和原生 Skill loader 必须看到与清单一致的 UTF-8 文件集合。
- 处理:Skill 文件变化与 manifest 指纹更新必须同次提交,并提升审核包版本;references 由 Codex 原生按 Skill 声明的相对路径读取,不再经过 AGC 自定义资源读取器。
- 回归补充:即使 Skill 文件本轮没有变化,也不能从旧提交或旧构建结果复制清单指纹;必须对当前工作树按 UTF-8 读取、将 CRLF 规范为 LF 后现场重算,并在提交前运行原生 Skill Pack 校验。Git 的
eol=lf不能阻止编辑器在干净工作树里留下少量混合 CRLF,而 Cargoinclude_bytes!会读取这些原始字节;因此运行时计算与安装也必须使用同一规范化函数。运行时只报告排序后的首个不匹配项,不能据此假定其余 Skill 已通过。 - 验证:逐项按排序后的
relativePath + NUL + canonical UTF-8 LF bytes + NUL重算并核对 manifest;Rust 单测覆盖 LF / CRLF 指纹等价和安装结果只含 LF,并确认隔离 HOME 中的根 Skill 与 references 可由 Codex 原生读取。
Gitea CI 预构建镜像不能只靠 tag 判断内容
- 现象:宿主已重建带日期修订 tag 的
genarrative/gitea-project-ci镜像,但genarrative-cijob 仍跑旧内容,或直接报 image not found;另一种危险操作是只改 runner label,没把对应镜像装入 rootless runner 的内层 Docker。 - 原因:宿主 Docker 和 runner 内层 Docker 是两个镜像库,同名 tag 可指向不同 Image ID。基础镜像 digest 和 Node tarball 哈希能锁定关键输入,但重建后仍必须把最终完整 Image ID 当作 runner 映射的事实源,不能从 tag 名推断二进制内容。
- 处理:使用
scripts/gitea-ci-job-image.sh build/verify,用export在仓库外保存镜像归档与便携 SHA-256 sidecar,再用load-runner将镜像导入内层、比对两侧 Image ID 并执行 bwrap / Chrome canary。确认无活跃 job 后,把当前 config 备份到仓库外受控位置,再将genarrative-ci映射到新的docker://sha256:...并执行docker restart --timeout 660 gitea-runner。保持内层 Docker 持久化和force_pull: false;精确 ID 缺失时失败关闭,不回退浮动 tag。 - 验证与回滚:重启后先跑真实 PR 的四个 job,再清理旧镜像。失败时先把 workflow
runs-on改回ubuntu-latest,再恢复 runner config 备份并重启;不在 Git、共享文档或日志中记录 config 备份路径、注册信息或 token。 - 重启边界:
docker restart --timeout 660只设置容器停止宽限,不能替代 Runner drain。rootless DinD supervisor 可能与 runner 同时停止内层 dockerd,使仍在收尾的 job 因连接关闭被标记失败;切换前必须同时确认 Gitea 没有in_progressrun 且内层docker ps为空。误触发时只重跑受影响的失败 job,不重跑已成功项。 - 关联:
deploy/container/README.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、docs/project-memory/shared-memory/development-workflow.md。
生产 API 发布重装 worker unit 不能丢失自定义路径(2026-07-23)
- 现象:Server-Provision 已按自定义 current link 和 env 路径安装 worker systemd unit,但下一次 API 发布后,worker 可能重新读取
/opt/genarrative/current与/etc/genarrative/*.env;默认路径仍有旧 release 时,服务 active 和部署成功都不能证明新二进制已运行。 - 原因:发布包中的 BgFilter、external-generation worker 和 controller unit 是带默认路径的模板;deploy 若直接
install原文件,会覆盖 provision 已渲染的目标机 unit。external-generation 专属 env 还是可选加载,错误路径可能不会阻止服务进入 active。 - 处理:API deploy 安装三个 unit 前必须按本次 current、API env 和各角色 env 参数渲染临时文件,安装后保留 release 内原始模板不变;controller 自定义 env 由
--controller-env-file显式传入。API Deploy 与 Full Job 必须同步暴露并透传 controller/BgFilter env,不能让流水线回退默认路径。自定义服务名表示沿用目标机自管 unit,不进入默认 unit 安装分支。 - 验证:部署 guard 使用临时自定义绝对路径,直接读取实际安装目录中的三个 unit,核对
WorkingDirectory、ExecStart、共享 API env 与角色 env,不能只用 fakesystemctl is-active判绿。 - 关联:
scripts/deploy/production-api-deploy.sh、scripts/check-production-api-deploy.mjs、scripts/jenkins-server-provision.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
通用灰度后台页不能依赖已退役业务配置
- 现象:通用
feature_gate_config、/admin/api/feature-gates和现役功能 gate 仍在,但后台“灰度发布”Tab 随旧创作模板入口一起消失;Rust 权限仍可授予gray-release,前端却没有对应路由。 - 原因:灰度页同时请求通用 gate 与旧
/admin/api/creation-entry/config,并把creation-entry:*动态目标和现役固定目标混在同一页面;按页面清理旧入口时连带摘除了通用控制面。 - 处理:灰度页只能以
/admin/api/feature-gates为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧creation-entry:*目标、接口和页面保持退役。 - 验证:
adminRoutes必须包含gray-release,admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。 - 关联:
apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx、apps/admin-web/src/app/adminRoutes.ts、server-rs/crates/api-server/src/modules/admin.rs、docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md。
角色动作不能靠素材主图或通用生成输入恢复
- 现象:角色动作在整画布导出时正常,但从素材库单项下载只得到第一帧 PNG,拖回画布也成为普通静态图片。
- 原因:
editor_asset.image_src只指向首帧;若 worker 把完整帧集塞进generation_inputs_json,素材 DTO、用户输入清洗或画布布局任一层丢字段,就会退化成 PNG。再增加一个mediaType只能掩盖结果字段没有落到正式资源的问题。 - 处理:worker 只把完整帧集与图片序列毫秒时长写入
editor_project_resource/editor_asset的image_sequence_frames_json、image_sequence_duration_ms;数组位置是唯一帧序,不保存frameIndex,帧数和 FPS 均按需派生。assetKind=character-animation决定序列渲染。素材映射、单项下载和拖回画布只读取这两个正式字段,项目 resource 在保存 / 刷新后继续作为主真相;动作 layout 只保留资源引用和 placement,不再复制正式媒体结果。账号素材提交精选审核时,editor_showcase_asset必须冻结复制相同字段,公开 read model 只返回正式字段。外部 helper 只调用一次动作生成接口并直接使用响应resource/asset。 - 画布回填:角色动作会形成“原角色资源 → 预览视频资源 → 最终序列资源”的血缘链。生成响应必须返回已经持久化的最终 resource,前端图层直接使用其
resourceId;不能继续构造local-resource-character-animation-*,否则appendCanvasLayersWithResources会再次创建重复资源。新图层的sourceResourceId同时使用最终 resource 的直接来源(预览视频 resource),不能继续沿用请求中的原角色 resource;否则结构化保存会在已生成并计费后因血缘不一致而拒绝。修复时只替换资源关联与血缘字段,不要顺带把动作图层显示尺寸从生成占位尺寸改成原始帧分辨率。 - 历史处理:不要再在 read mapper 增加
generationInputs/ layout fallback。使用 migration operator procedure 按asset → project-resource → showcase → canvas迁移;只有动作身份已由assetKind、正式字段、嵌套characterAnimation或权威对象证明后,才解释顶层frames/durationSeconds,否则会把无关任意 JSON 误分类。同一 task 可能同时存在误标为动作的预览 MP4 和最终首帧 PNG,候选查找必须先按权威对象类型做计划态分类,排除视频并要求唯一正式图片序列,不能按原始assetKind计数。账号素材仍有旧帧、但后来拖入画布的 project-resource 只剩清洗后fields/references时,project-resource dry-run 必须按同 owner / task / 首帧对象精确消费 asset 计划态结果;apply 仍要求前置 asset scope 已物理完成。canvas 判断已有 resource 是否为动作时也必须消费 project-resource 的计划态类型:旧库误标为动作、但权威对象证明为 preview MP4 且 layout 本身是 video 的图层直接跳过动作清理;layout 明确为image-sequence却指向该视频时继续形成 blocker,资源规划本身有 blocker 时也不得静默跳过。正式序列还要逐帧用稳定对象路径匹配同 owner / task 的已登记图片对象并补齐objectKey/assetObjectId。迁移不得验证 layout 复制的sourceResourceId:历史 layer 可能仍指向原角色,而最终素材已指向预览资源;清理副本后采用最终素材的 DB 血缘即可,新生成链路仍保持严格校验。正式与旧版结果冲突、候选为零或多个均形成 blocker;脚本诊断应直接打印 scope、ID、原因、owner/project/task、对象身份和来源资源,不能只报 blocker ID。普通 layer 顶层mediaType在迁移和响应清洗时删除,但嵌套生成参考的mediaType保留。 - 新写入与验证:动作
generationInputs出现characterAnimation/frames/previewVideoPath/frameCount/fps/durationSeconds/screenColorHex,或正式帧出现frameIndex,HTTP 与 storage 双层拒绝;其它 asset kind 的任意 JSON 不受该动作门禁影响。测试覆盖两种历史 JSON、无关顶层同名字段、正式/旧版相等与冲突、可选帧引用合并、screen color 和 frameIndex 清理、预览 MP4 重分类、幂等、blocker/hash apply、画布 placement 清理/资源补建、正式字段缺失失败关闭和 helper 单请求。
图片序列时长不要复用通用媒体秒数
- 现象:把角色动作、视频、音频和上传媒体都写进通用
duration_seconds,随后又尝试用持久化frame_count/fps/duration_seconds互相校验,造成取整口径、生成参数和实际播放时长彼此污染。 - 原因:角色动作需要的是一组图片完整播放一次的精确时长;视频 / 音频的
durationSeconds是生成请求或临时运行态参数。帧数已经由数组长度唯一确定,FPS 也可按需要推导,无需维护三份可冲突真相。 - 处理:资源 / 素材只保存
image_sequence_frames_json与image_sequence_duration_ms,精选审核快照只冻结复制这两个正式字段。角色动作要求至少两帧且毫秒时长大于 0;播放器按时长毫秒 / 数组长度计算间隔,Spine 导出时再换算秒数并推导 FPS。音频 / 视频durationSeconds不映射到这两个字段。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasExportModel.ts。
精选角色动作显示首帧还要检查前端 renderer 与逐帧授权
- 现象:精选接口已经返回
imageSequenceFrames和正确的 5 / 6 秒成本,但创作主页或后台审核仍只显示首帧;接入播放器后又可能只有第一帧成功、后续帧换签返回 404。 - 原因:快照字段、展示 renderer 和私有对象授权是三道独立边界。公开
imageSrc/objectKey只代表首帧,不能让前端自动获得完整帧集;顶层精选 exact grant 也不会自动覆盖其它帧对象。 - 处理:公开精选模型必须把
assetKind=character-animation映射到序列 renderer,并携带完整帧与毫秒时长;后台素材查询和精选审核共同透传同一字段并复用AdminEditorAssetMedia。生成端不能在ProcessedEditorCharacterAnimationFrame → EditorCharacterAnimationFramePayload收口时丢弃逐帧assetObjectId/objectKey,正式序列 JSON 必须保留已确认对象的稳定引用。公开授权在 SpacetimeDB 同一事务快照中只按有效精选动作的同 owner 逐帧assetObjectId/objectKey匹配,不能放宽 generated 前缀。列表未交互时只读首帧,打开或激活动作预览后也只挂载当前帧和有界预读窗口,避免再次制造换签突发;单帧换签或解码失败时跳过该帧、暂停全帧失败的序列并提供显式重试,不能长期显示空白或旧帧。卡片 hover 与 focus 分别跟踪,只要任一状态仍成立就继续播放,系统请求prefers-reduced-motion时卡片和弹窗默认暂停,用户仍可在弹窗中手动播放。 - 验证:模型 / 组件测试覆盖 4 / 5 / 6 秒动作、损坏序列不回退 PNG、后台两页共用播放器和未激活列表不逐帧请求;SpacetimeDB 测试覆盖主对象、每帧对象、无关对象、跨 owner 与取消展示后的授权撤销。真实浏览器和端到端验收由人工单独执行,不把 unit / component 结果写成 E2E PASS。
可复用资源回填必须保持时间戳单调
- 现象:延迟重试携带比既有行更旧的调用方时间,回填图片序列字段时若无条件写入,会使
updated_at倒退,导致基于时间戳的同步看不到更新或排序错误。 - 处理:同源图片序列字段只允许
None → Some,非空冲突失败关闭;发生回填时updated_at = max(existing.updated_at, request_timestamp)。legacy 音频 repair 不派生资源级图片序列或通用时长,重放继续精确匹配。
历史钱包消费不能从最近流水或通用订单快照推算
-
现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的
list_profile_wallet_ledger,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。 -
原因:最近流水会低估历史总额;通用钱包快照又会被订单列表反复构造,把一次按用户聚合放大为
订单数 × 流水数的重复扫描。 -
处理:历史花费只累计
asset_operation_consume负向流水绝对值,退款不冲减;通过profile_wallet_consumption_total在已有投影时按主键 O(1) 累加。首次上线必须在停写维护窗口由 owner 执行全量初始化,为每个已有钱包流水的用户建立投影,不能让所有存量用户的首次正常消费各自扫描历史;维护遗漏或新用户缺行时才在首次消费或详情读取中按用户索引兜底重建一次。手动对账扫描是独立高风险操作,member 必须单独持有profile-wallet-consumption-reconcile,不能因为能打开共享用户详情就自动获得。 -
验证:构造消费、退款、充值退款追回和赠送混合流水,断言只累计消费;维护初始化后正常消费只按主键累加;重复详情读取不得重复扫描或重复累计;任意 Tab 权限不能调用手动对账,同时确认充值订单列表的通用钱包快照没有新增历史流水扫描。
-
症状:
code-prototype首次完成后.agent/logs/command.log已出现permission.confirm preview.start,但客户端没有 iframe,.agent/logs/preview.log也没有新的 running 记录;后续即使父 run 完成也不再启动。 -
根因:旧实现调用
start_local_game_preview前就把“项目 + parent run”的授权加入 attempted 集合并清空;首版完成投影与后续专业任务仍在写项目时,启动恰逢项目写锁竞争,catch 只显示错误却无法重试。 -
约束:一次性语义应按“成功或确定性终态”消费,不按“函数调用次数”消费。项目写锁竞争保留同一授权并轮询重试;成功、显式 deny 与非瞬时失败才清除。授权需持久化项目路径和 accepted runId,重启恢复时仍必须逐项匹配,切换项目不得继承。
-
回归:AppSurface 模拟第一次
start_local_game_preview返回项目正在被其他写操作占用、第二次成功,断言最终渲染游戏区域且启动调用恰为两次;完整 AppSurface 仍需覆盖显式 deny、停止隐藏与项目切换隔离。 -
CI 时序:生产预览状态每
1000ms轮询一次,回归若也使用waitFor默认1000ms上限,会在 CI 负载下于首次 interval 回调附近竞争超时。验证“授权保留期间仍继续轮询”应使用明确3000ms上限,不改生产轮询周期。合并长测试文件后还要运行全量 ESLint;单纯 autofix 只会排序、不会消除两个分支同时引入的重复 import。
跨窗口 CAS 锁不能用 mtime stale 删除模拟系统互斥(2026-07-30)
- 现象:两个窗口基于同一 revision 保存资源布局时,正常测试看似只有一个成功;锁文件超过 stale 阈值或两个竞争者同时判断过期时,却可能各自删除 / 重建锁并同时进入 read-check-write,击穿“同 revision 最多一个成功”。无效绝对路径还会在 manifest 报错前遗留
.agent/workbench/resource-layouts。 - 原因:
create_new只保证某一时刻创建文件原子,不保证“判断过期 → 删除 → 重建”整体原子;mtime 不能证明 owner 已退出,token 文本也不能阻止另一个竞争者删除新锁。先获取锁再读 manifest 又把目录创建副作用提前到了项目身份验证之前。 - 处理:锁文件作为持久入口永不由应用删除;Unix 用文件描述符持有
flock(LOCK_EX | LOCK_NB),Windows 用share_mode(0)独占句柄,Drop / 进程退出让操作系统释放锁。安全打开逐级拒绝符号链接 / reparse point,Unix 还核对 owner、硬链接数、inode 和0600。更新携带只用于校验的expectedProjectId,先只读验证 manifest,再获取系统锁并在锁内复核 projectId;不存在根、非项目根、损坏 manifest 和路径复用后的旧窗口都不能创建 workbench。revision 必须 checked increment,耗尽时不能饱和成功。 - 验证:必须覆盖活锁 mtime 被设为 epoch 后竞争者仍拿不到锁、释放后同一 inode 可重新获取、同 revision 并发双写仍恰好一个 updated / 一个 conflict,三类无效根和旧 projectId 零 workbench 副作用,以及
u64::MAXrevision 保持原文件。锁等待超时只能返回可重试错误,不得转为 stale 删除。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/resource_layout.rs、docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md。
旧 scope 的卡死请求不能占住新资源画布队列(2026-07-30)
- 现象:用户在 dependency 布局保存尚未返回时切到 type 或另一个项目,新 scope 已完成读取且拖动已进入队列,但因为全局活动请求引用仍指向旧 scope,新的保存会无限等待旧请求结束。
- 原因:epoch 只阻止迟到响应覆盖新状态,不会自动释放前端单写者槽;把“不能取消已经发出的请求”误写成“所有后续 scope 都必须等待它”,会把一个网络或 IPC 卡死扩大到整个 Hook 生命周期。
- 处理:FIFO 和单写者只约束同一
projectPath + projectId + modescope。切换 scope 或卸载时立即放弃旧活动槽并清空旧队列,旧 Promise 仍可在后台结束,但其结果由 epoch 丢弃,finally 也只能按意图身份清理自己,不能清掉新 scope 的活动请求。后端继续用expectedProjectId、CAS revision 和系统锁仲裁已经发出的旧写入。同 scope 在途 CAS 不取消;其后相同资源与 section 的排队拖动只保留最后坐标,避免连续输入造成无界队列。 - 验证:让旧 mode 更新 Promise 永不先 resolve,切换 mode 后应立即发送并完成新 mode CAS;随后再 resolve 旧请求,新布局、saving 状态和请求数均不得变化。另以百次同资源拖动证明在途请求之后只追加一笔、坐标为最后一次输入。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts、apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts。
资源协调冲突清除排队拖动时不能静默重试(2026-07-31)
- 现象:资源协调 CAS 在途期间,用户拖动资源形成排队 manual intent;协调请求随后 conflict 并基于权威 revision 自动重试成功,布局正确保留另一窗口结果,但界面没有提示本地拖动已经被丢弃。
- 原因:冲突分支虽然清除了当前 scope 的全部 manual intent,却只按“当前 intent 是否为 manual”或“资源协调是否停止重试”决定提示;当前 intent 为 resources 且可重试时,排队拖动的丢弃事实没有进入提示条件。
- 处理:过滤队列前记录本次是否实际清除了 manual intent。只要当前 manual 发生冲突或清除了任何排队 manual,就必须提示用户重新拖动;冲突前坐标不得自动重放,后续资源协调继续使用冲突响应的权威 revision,并且成功响应不能静默清除提示。
- 验证:定向 Hook 测试固定“resource sync revision 1 在途、manual 排队、权威 revision 2 conflict、resource retry 成功”时序,断言 retry 使用 revision 2、权威坐标保留、旧 manual 不重放且 notice 仍存在。
- 关联:
apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts、apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts。
Rust u64 revision 不能直接穿过 JavaScript number 边界(2026-07-31)
- 现象:资源布局 sidecar 的 revision 在 Rust 中可增长到完整
u64,但经 JSON / Tauri 返回 TypeScript 后只能用number表示;超过9_007_199_254_740_991时相邻整数会折叠为同一值,窗口可能持续 conflict,甚至用失真的 expectedRevision 破坏 CAS 判等语义。 - 原因:Rust 的
checked_add只防止u64溢出,不能证明序列化后的整数仍能被 JavaScript 精确表示;纯 Rustu64::MAX测试没有经过真实跨 JSON 合同。 - 处理:保留 Rust
u64存储类型,但把共享合同合法域冻结为0..=Number.MAX_SAFE_INTEGER。共享 DTO 对 revision 自定义 serde 校验,Tauri 更新在任何项目或锁副作用前验证 expectedRevision,sidecar 读取拒绝超限值,前端在 IPC 读取、更新响应和请求发送前重复验证非负安全整数;达到上限时写入失败且 sidecar 字节不变。 - 验证:Rust 与 TypeScript 合同测试分别覆盖最大安全值往返、最大值加一拒绝;Tauri 持久层覆盖超限 expectedRevision 零 workbench 副作用、超限 sidecar 原字节保留和最大安全值递增失败;Hook 覆盖不可信读写响应不能进入 CAS。
- 关联:
server-rs/crates/shared-contracts/src/game_creation_app.rs、packages/shared/src/contracts/gameCreationApp.ts、apps/ai-game-creator-shell/src-tauri/src/project/resource_layout.rs、apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts。
抽通用 Runtime 时不要把产品持久文件直接变成公共 ABI
- 现象:为了快速“抽 crate”,直接把 Tauri package 内的
AgentRuntimeState、sidecar struct 或 Runner protocol 改成pub,第二个消费者虽然能编译,却同时绑定游戏 schema、UI 投影、文件路径和未稳定恢复顺序。 - 原因:代码可见性被误当成领域解耦;产品私有 DTO 中仍混有
game-creator-*schema、固定 profile、Provider 类型和本地持久化细节,公开后只会把后续迁移变成 breaking change。 - 处理:先从纯值对象和宿主注入契约抽取,公共 core 不依赖 Tauri、Provider DTO 或游戏 crate;产品通过 adapter 注册 capability、Agent、profile 和 completion policy。Store/Runner 等只有在事实源、事务和迁移协议单独稳定后再抽接口,不能双写或复制 sidecar。
- 验证:必须存在完全不含游戏语义的 conformance fixture,并让至少一个现役生产入口真实消费公共契约;只新增未被调用的 crate、
include!、路径搬家或旧类型 re-export 都不算完成。
Runtime 不能在外部工具返回后才首次记录执行意图
- 现象:Runtime 调用工具成功后准备写 observation,但进程在写入前崩溃;重启后只看到 queued action,于是再执行一次外部副作用。
- 根因:把“调用返回”当成 durable 事实,缺少工具调用前的持久 executing checkpoint;网络、文件、命令和外部 API 都不能因为“看起来幂等”就自动重放。
- 处理:先以 CAS 单独 commit
queued -> executing,成功后才调 ToolHost;调用返回后再 commit observation。恢复见到 executing 或 ToolHost 返回 Unknown 时只能进入 reconciliation,不得自动重执行。重复 resume 不得继续增 revision 或重复 event。 - 验证:在“ToolHost 已调用、observation commit 失败”处注入故障,序列化快照并用新 engine 重载;断言重复 resume 后 ToolHost 计数仍为 1,且只有显式 reconcile observation 才恢复 running。
Runtime 后台执行不能让大型 async frame 共用默认 worker 栈(2026-08-03)
- 现象:Supervisor collaboration durable isolated spawn 恢复测试或普通
agent.delegate后台委派测试在默认 Tokio worker 栈下稳定stack overflow;单独运行同样失败,提高RUST_MIN_STACK后通过。 - 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queue、Agent 主循环,以及 Provider、Codex CLI、Codex app-server 组合模式分发的最大分支状态都会形成大型 async poll frame;恢复路径直接进入下一层状态机、普通后台任务把完整主循环放回默认 worker,或组合 future 进入泛型 helper,都会超过默认栈。
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后
spawn,调用方 async frame 仍会把它保留在默认 worker 栈上。普通后台任务、静态委派子任务和 manifest ready-task 的首次执行统一复用 16 MiB 专用 Runtime worker,并在 worker 已启动后交接 Agent 任务锁;worker 创建或交接失败要持久化当前 run 失败。Provider 物理请求必须在持久重试 helper 与非持久压缩路径构造完整请求后、进入下层泛型 control/lifecycle helper 前装箱,不能等到底层 helper 才装箱。pending 边界继续保留结构化取消语义,父 continuation 被丢弃时同步 abort 子任务。不得逐个扩大 queue worker 栈,也不得增大 CI 的RUST_MIN_STACK掩盖问题,否则生产路径仍可能崩溃。(2026-08-15 修订)判据从「逐个列举入口」改为不变量:所有会进入 Agent 主循环的 future 必须在agent-runtime-worker-*专用线程上轮询。 原文按入口枚举(普通后台任务、静态委派子任务、manifest ready-task 首次执行),但恢复重启是第四个入口,从未被列进去——recovery_scan.rs手写tauri::async_runtime::spawn直接跑drain_game_creator_agent_background_tasks,把与 started 入口同样深的 poll 链放在默认 2 MiB worker 上;队列 drain(spawn_next_..._with_lock)同样留在默认栈。两条当时都还塞得下,直到M1B-2往主循环与恢复扫描加分支把余量吃穿才暴露。枚举法漏掉一个入口不会产生任何信号,因此改为统一常量AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES加单一 spawn helper;承载主循环的路径一律不得再手写tauri::async_runtime::spawn。 - 验证:失败用例必须在未设置
RUST_MIN_STACK时通过;同时覆盖普通后台委派、policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明任务锁只交接一次、恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。(2026-08-15 补)只断言「默认栈下没崩」不够——余量仅剩几百字节时它依然是绿的,这次崩溃前全部用例都通过,master 侧drain_next_*只剩 512~768 KiB 余量也毫无信号。必须同时断言线程名:drain 入口在#[cfg(test)]下记录std::thread::current().name(),用例断言其全部以agent-runtime-worker-开头。该断言与栈余量无关,已用变异验证:把recovery_scan.rs改回手写 spawn 并把RUST_MIN_STACK抬到 16 MiB(因而不会溢出),用例仍以["tokio-rt-worker", "tokio-rt-worker"]失败。修复后的验收标准是「压到 1 MiB 默认栈仍通过」,而不是「默认栈下没崩」。另需运行background_agent_runtime_can_delegate_task_to_other_agent、provider_retry_、provider_handoff_、response_stream_与 Native shell 完整门禁,全部以默认 worker 栈通过。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_queue.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs。
Provider 可扩展不能用一个全局 protocol 枚举代替实例隔离
- 现象:把
openai_chat / openai_responses / anthropic直接当 Provider 身份,注册第二个同协议 endpoint 时发生 ID 冲突;或为方便调用把 API Key、base URL、raw-log 目录放进全局状态,并行请求后日志串目录。 - 原因:wire protocol 是 adapter 能力,Provider instance 才是配置与资源所有者;两者被闭集枚举合并后,无法表达同协议多租户/多 endpoint。
- 处理:core 同时校验
ProviderInstanceId + ProviderProtocolId,registry 只以 instance ID 索引 adapter;adapter 内持有独立LlmClient。新协议通过实现 trait 注册,新实例通过自定义 instance ID 注册,都不得修改 core match。 - 验证:至少同时注册两个同 protocol 实例,证明 descriptor/lookup 互不污染;对每项未声明能力断言 adapter 调用计数为 0;并行 raw-log 测试必须使用两个显式临时目录,不用串行化掩盖错误路由。
Provider 适配器不能重新实现一份 HTTP/SSE parser
- 现象:为了让 Runtime 调用中立 trait,在 core 或 AGC 内再拼一次 URL/header/request body,或自己消费 SSE;它会与
platform-llm的重试、脱敏、工具分片和错误分类迅速漂移。 - 处理:adapter 只做 core DTO 与现有
Llm*DTO 转换,网络调用唯一落到LlmClient::run/stream_run。流式 sink 保留累计文本、当前增量和 finish reason;tool calls 继续从最终 response 读取。 - 验证:三种 descriptor/capability、request/response round-trip、stream callback 和稳定 error kind 单测后,仍必须运行
platform-llm全量 parser 测试;只有 adapter fake 通过不能证明 wire 协议没有回归。
内部处理模型的可见性过滤是标题精确匹配,不是语义识别
- 现象:读文档以为「内部处理模型不会展示给普通用户」是全覆盖保证,实际历史素材的图片信息弹窗和画布 ZIP 导出里仍能看到抠图模型,例如标题“抠图模型”、取值
动漫风格 anime-seg。 - 原因:
isEditorUserVisibleGenerationInputField的实现是field.title.trim() !== '处理模型',只按这一个标题字符串精确排除,既不识别语义也不探测取值。历史数据里存在标题不同但语义相同的字段,直接穿过过滤器;服务端 User/Public mapper 只清理generationInputs顶层的screenColorHex/mattingProvider/mattingModel,不遍历fields数组,所以两侧都不会拦。 - 处理:本条目前不修——历史素材不迁移、不回溯清理是明确的产品决策,不得据此判定为缺陷或提交「修复」。约束只对新写入生效:新产生的
generationInputs.fields不得再写入任何内部处理模型字段,无论标题叫什么。若将来要扩大过滤范围,先对生产generation_inputs_json做一次标题去重查询枚举真实存在的历史标题,不要仅凭测试夹具推断清单。 - 验证:
ImageCanvasMetadataModalView与ImageCanvasExportModel共用同一过滤口径,改动其一必须同时覆盖另一侧;新增过滤标题时需同时确认图片信息弹窗与画布 ZIP 两条路径。 - 关联:
src/components/image-editor/ImageCanvasGenerationModel.ts(isEditorUserVisibleGenerationInputField)、src/components/image-editor/ImageCanvasExportModel.ts、src/components/image-editor/ImageCanvasMetadataModalView.tsx、src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
外部 OpenAPI v1 的「无调用方」豁免不是受控状态
- 现象:认为外部 v1 契约可以随内部脱敏需要直接改,因为「反正没人接」;
docs/openapi/genarrative-external-v1.openapi.json被当成内部文档同步,不走版本流程。 - 原因:API Key 由用户在个人中心
我的 → 开发者 API Key自助发放,/api/external/v1/openapi.json又是该批路由里唯一不要求鉴权的端点,任何登录用户都能拉规格并生成客户端。因此「无外部调用方」随时可能在无人决策的情况下变为假,不能当作长期前提。 - 处理:改外部 v1 响应前先确认
external_api_key是否已有非内部账号的活跃密钥。仍无调用方时可按现行豁免直接改,但必须同步更新接入方案的「版本与兼容策略」;已有调用方时按该节规则择一处理(兼容值 / 弃用期 / 升 v2),只改 JSON 不构成合规变更。 - 验证:
external_editor_api.rs的 openapi 断言只校验 schema 形状,不校验兼容性,通过不等于契约安全;判定 breaking 与否以「删字段、移出 required、收窄类型、改语义、新增必填」为准。 - 关联:
docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md、docs/openapi/genarrative-external-v1.openapi.json、server-rs/crates/api-server/src/external_editor_api.rs、server-rs/crates/api-server/src/modules/external_api.rs。
“继续”不能成为新游戏主题或触发首版整文件覆盖(2026-08-03)
-
现象:原根 run 已经写出并验证目标玩法,但父 Runtime 因预算、上下文或 Provider 失败;用户在同一项目输入“继续”后,页面标题变成“继续”,玩法被默认收集/点击模板替换,美术规范总览图被直接铺进游戏画面。
-
处理:严格继续意图必须在同一 Supervisor Session、同一持久 source 内继承最近失败根 run 的原始目标和 baseline,但保持新的 run/Provider/sidecar 身份;纯继续词表只能有一个权威实现,中英文短语都走同一入口,真正新需求仍独立 reset。非占位入口禁止 fallback 整体覆盖,也不能反复运行只读 smoke;当前
code-prototype必须先读取并实际 patch,取得本人 mutation 后才能验证和交付。占位 fallback 只支持具备真实语义的显式模板,俄罗斯方块必须实际实现棋盘、下落、旋转、锁定和消行,未知玩法失败关闭。art-spec.png只作规范参考,核心运行时位图必须来自独立派生的透明art-spritesheet.png及其iconImageSrcs本地切片;切片清单绑定当前图集 resourceId,Canvas 分别使用玩家、目标、场景和反馈四类素材。不得猜测图集是 2×2 等分、把规范板塞进画面或以纯代码核心实体绕过派生素材。 -
验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、连续 successor、跨 Session、跨 source、正常完成后新输入、带具体新需求、既有非占位入口先 patch 后 smoke、初始化占位的俄罗斯方块真实语义、未知玩法失败关闭、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后
sequence与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。 -
现象:用户要求把已有美术资源接入游戏时,固定
code-director -> art-director / art-asset-plan -> code-prototype图会在缺少主 Agent 审计的情况下启动美术生成,或把“整体重做”错误实现为无条件生图;美术完成后又换了 Run,代码接入、静态检查和试玩无法形成连续责任链。 -
原因:固定节点把“是否需要美术”的语义判断编码为 Runtime 前置流程,
code-director成为另一个主控,而不是让真正接入游戏的code-prototype基于权威资产事实决策;如果再把固定审计策略塞进用户意图字段,Supervisor 的理解也会被 Runtime 规则覆盖。两个素材槽都缺失时若先消耗不可重试的art-asset-plan委派,其 child 又必然因缺规范图失败,整个 Run 会进入无法补救的死路。 -
处理:Supervisor 用
intentSummary持久化自己对用户意图的理解,固定audit-existing-first只作安全执行策略,随后只启动code-prototype。主 Agent 先asset.list,完整覆盖就直接使用;仅在事实证明缺少规范图或核心图集时,才建立一个写入范围受限为assets/**的美术 durable delivery。两槽都缺失时必须先完成并认领art-director的EvidenceReadydelivery,再委派art-asset-plan;回执返回同一主 Run 后再接入素材、原玩法语义检查、静态检查和双视口试玩。读取旧 v1 决策时必须复核其旧 fingerprint,并从完成合同绑定的有效任务迁移 intent;旧code-directorcoverage/route 只能触发当前主 Agent 重新审计和原位替换,不能直接成为新完成证据。整体视觉重做意图同样必须经过这次审计,不能成为绕过资产复用或强制重生成的固定规则。完整 GUI / CLI DAG 不使用该例外。 -
验证:正反向测试同时证明
intentSummary非空且不被固定策略代替、v1 决策与旧 route 同根恢复、完整资产零委派、真实缺口精确委派、两槽缺失时规范图优先、单个活跃 child、game/**写拒绝、assets/**写允许、回执恢复同一主 Run 和最终主 Agent 自验收;不能只凭 Prompt 出现关键词或 manifest 状态投影判通过。
Tauri beforeDevCommand 失败不等于已启动客户端会自动退出(2026-08-03)
- 原因:Tauri 的字符串
beforeDevCommand默认wait=false。只要固定devUrl上已有可访问页面,Tauri CLI 可以在配套启动脚本完成前创建原生窗口;旧实现又直接从 npm 启动 Tauri CLI,没有在 CLI leader 退出后继续持有其 PGID / Windows 进程树。start-dev-stack.mjs虽会在后端 ready 后识别 marker/API 错配,但检查时机已经晚于窗口创建,且只清理自己登记的后端和 Vite。 - 2026-08-08 后续统一:上述
3080是事故发生时的历史实现,不再是当前 Linux 启动口径。AGC Vite 已纳入系统级用户端口段,首选start + 5,占用时只在本用户段内漂移;外层启动器把最终端口写入 Tauri CLI 动态build.devUrl和子进程GENARRATIVE_AGC_VITE_PORT,并用 Vite CLI--port启动严格监听。beforeDevCommand、配套后端预留、WebView 与 VitestrictPort必须使用同一值。Windows / macOS 仅把3080保留为兼容首选并允许统一漂移。未知归属监听器仍不得复用或主动终止,但其它用户固定3080不再阻塞 Linux 当前用户启动。 - Linux 容器边界:最小化 CI 容器的 PID 1 可能不回收孤儿后代,进程组在所有可执行成员退出后仍只剩
Z僵尸;此时kill(-pgid, 0)仍成功,不能据此把已经完成的收束误报为失败。Linux 等待逻辑在 signal 探活后必须核对/proc/<pid>/stat,只把同 PGID 的非Z / X成员视为存活;/proc不可读时继续使用原保守判断,macOS 等其它 POSIX 平台仍只走 signal 探活。 - 验证:定向测试必须覆盖用户段
start + 5映射、同段占用漂移、父子启动器严格复用最终端口、动态 Tauri--config、marker 与预检地址一致、未知归属监听器拒绝复用、CLI leader 先退出后同 PGID 客户端仍收到 TERM、忽略 TERM 时升级 KILL,以及 Windows taskkill 的/PID /T /F参数。正常启动后退出,确认 Tauri 客户端、Runner 和本轮自有后端 / Vite 均按生命周期收束。 - 关联:
apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs、apps/ai-game-creator-shell/scripts/start-dev-stack.mjs、apps/ai-game-creator-shell/tests/start-tauri-dev.test.ts、apps/ai-game-creator-shell/tests/start-dev-stack.test.ts。
Git 忽略的 AGC dist 会让 Windows 继续运行旧 Linux 路径交互(2026-08-14)
- 现象:源码已经移除项目页常驻路径输入框,Windows 客户端却仍显示
/tmp/genarrative-ai-game-draft,新“打开项目 / 新建项目”交互也没有出现。 - 原因:
apps/ai-game-creator-shell/dist/是 Git 忽略的本地构建产物,可能跨提交保留旧 JS;复用旧dist、旧 EXE 或旧安装包时,Tauri 会继续嵌入旧前端。测试模式曾把/tmp同时当作产品初值,也让旧构建和测试夹具的边界难以辨认。 - 验证:运行 frontend dist guard 定向测试、AGC AppSurface 的双主按钮 / picker 防重复 / 首页回车自动创建回归、Tauri release
--no-bundlesmoke,并确认新dist不含旧路径;Windows 实机项目组不得出现常驻路径框或/tmp,原生 picker 从系统默认位置打开。视觉验收检查1280×720最小横屏与1280×800默认窗口的紧凑项目表格和原生 picker。 - 关联:
apps/ai-game-creator-shell/src/app/constants.ts、apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts、apps/ai-game-creator-shell/src-tauri/src/commands.rs、apps/ai-game-creator-shell/src-tauri/build.rs、apps/ai-game-creator-shell/src-tauri/tauri.conf.json。
参考成熟项目管理器不能变成品牌复刻或伪数据列(2026-08-15)
-
现象:根据外部产品截图重做项目页时,直接照搬其 Logo、深色皮肤、收藏 / 云图标、修改时间或编辑器版本列,页面看似成熟却展示 AGC 没有的数据真相,录屏也只剩静态摆拍。
-
原因:把参考截图当成完整产品合同,没有先核对当前目录检查、manifest 和 Runtime 真正提供的字段,也没有定义视频必须证明的交互结果。
-
处理:只借鉴标题、搜索、主操作、紧凑表头 / 项目行和行尾菜单的信息层级;继续使用 Genarrative theme/token,只展示项目名称、工作区路径、GameAgent / Godot 类型、Godot 相对根和真实状态。次要操作收进行尾菜单,搜索只做本地过滤。没有权威来源的列直接不做,不用占位或推断补齐。
-
验证:DOM 与截图不得出现外部品牌或 unsupported 列;AppSurface 覆盖 populated / invalid / empty、搜索与菜单;Playwright 在
1280×720测量无页面级溢出。视频控制在有用时长内,清楚展示搜索、清除、菜单、状态反馈和项目打开结果,每一段都有可观察变化。 -
关联:
apps/ai-game-creator-shell/src/features/app-shell/ProjectCreation.tsx、apps/ai-game-creator-shell/src/features/app-shell/model.ts、apps/ai-game-creator-shell/src/features/app-shell/useRecentProjects.ts、apps/ai-game-creator-shell/tests/appSurface/home.suite.ts。 -
现象:首波从单个美术任务扩展为三个 Director 后,hydration 若仍只容忍 seed lane 的第一个任务在 manifest 短暂恢复
Pending时收束,另外两个已启动 Director 会被卡住。另外默认llm.stream=false下的专业 final reply 虽已由 finalization 提交,但后续阶段推进项目 revision 后,早期回复会从 Runtime 查询中消失。 -
原因:hydration 例外把“首波”错误收窄成了单个固定或数组第一项任务;
visible_game_creator_agent_runtime_response_stream_at又把未提交流的 revision 新鲜度门误用到了已终态提交的 durable final reply。 -
处理:从当前 root source 的 seed lane 动态解析全部零依赖首波任务,只对这些 child 容忍 hydration
Pending,后续 code prototype / preview 仍严格要求 Running/Completed。streaming / ready仍要求当前 revision,committed回复改为依据 finalization 的稳定身份查询,不随后续项目 revision 失效。 -
验证:覆盖
design-director / art-director / code-director三个 Pending 首波 child 均可投影 Completed、code-prototypePending 仍被拒绝;非流式专业 Agent 在 finalization 前无 stream,提交后形成 committed stream,再推进项目 revision 后仍可查询且正文不变。 -
关联:
apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/response_stream.rs。
异步生成结果未知时不能换幂等键重提(2026-07-31)
- 现象:生成提交发生客户端超时、连接中断或响应丢失后,调用方创建新的
Idempotency-Key再提交一次;原任务其实已经入队,最终造成重复生成、重复扣费和重复画布 / 素材库写入。 - 原因:把“客户端没有收到结果”误判为“服务端没有受理”,又没有持久保留逻辑请求的幂等键和服务端返回的
operationId。托管 MCP 若绕过 External REST router 直接调用 worker 或 SpacetimeDB,也会形成第二套去重与状态语义。 - 补充:不能把“accepted 分支里没有生成 POST”误当成 GET-only 恢复。若读取账本前仍重做项目/素材目录准备、输出路径预检或请求正文构造,恢复仍可能创建远端资源或在查询 operation 前失败。恢复必须直接使用 durable snapshot;清理必须最后删除 pending 身份锚点,活动 orphan 不得自动删除。完整恢复 future 还要在默认 Tokio worker 栈下验证,不能靠测试环境调大
RUST_MIN_STACK掩盖栈溢出。 - 加固:durable snapshot 必须绑定不含明文凭据的规范 base URL 服务身份指纹;服务地址漂移时恢复 POST 和 GET 都必须阻断,Developer API Key 轮换则必须继续原 operation。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧
200durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。只有首次提交直接返回契约明确的400 / 401 / 403才可证明未入队并清理 prepared 账本;首次结果已经未知后,恢复请求的临时鉴权错误、超时、冲突、限流、网关错误及其它意外状态均保留同一账本。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。 - 代理 DNS:Clash 等透明代理可能把公网对象存储域名解析到 RFC 2544 的
198.18.0.0/15fake-IP。下载器只对已通过鉴权objectKey或受控 legacy path 换签得到的 URL 接受“全部地址均位于该 benchmark 段”的窄例外;直接 URL、其它本机/私网地址、公私混合解析和重定向仍必须失败关闭,不能为了兼容代理整体移除 SSRF 校验。 - 验证:覆盖“服务端已入队但提交响应丢失”后两次 POST 的 endpoint、正文 bytes 与
Idempotency-Key完全相同,原键重试仍返回同一 operation,最终只出现一份 completed result 和一次计费 / 写回;恢复再次 transport 失败或临时鉴权失败仍保留同一账本;换 owner 不可见;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。 - 关联:
server-rs/crates/api-server/src/external_generation.rs、server-rs/crates/api-server/src/external_mcp.rs、docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md。
MCP 列表不能透传完整项目快照(2026-08-07)
- 现象:账号项目数量增长后,
list_editor_projects把每个项目的canvas / layers / resources全量透传,REST 响应超过 MCP 4 MiB 上限,Agent 因整批失败而无法展示、查重或安全选择项目;缺少必填请求体时,内部 Axum JSON extractor 的文本415又会被泛化成“非 JSON 响应”。 - 处理:项目列表 REST 保持默认
view=full兼容,并提供view=summary;MCP 固定使用 summary 且不向 Agent 暴露或接受view=full。摘要只返回projectId / title / updatedAt / cover,封面取最新且存在稳定objectKey的project-cover-snapshot,展示时再调用/assets/read-url,不在列表内嵌图片或签名 URL。MCP 在构造内部 REST 请求前按 OpenAPI schema 校验 required body;缺正文和缺字段分别返回结构化错误,不进入写入、上传票据或计费路径。 - 验证:用 19 个完整序列化后超过 4 MiB 的项目 fixture 证明摘要仍低于上限且不含大型布局;覆盖四个历史
415工具的缺正文、空对象和非对象输入,并断言项目列表工具固定 summary、调用方不能通过 query 覆盖。 - 关联:
server-rs/crates/api-server/src/external_mcp.rs、server-rs/crates/api-server/src/external_editor_api.rs、docs/openapi/genarrative-external-v1.openapi.json、docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md。
api-server 嵌入仓库外资源时必须同步容器构建上下文(2026-07-31)
- 现象:本地
cargo test可以编译 MCP 与 Skill 下载模块,但 api-server 镜像在 Rust 编译阶段报include_str!找不到 OpenAPI 或 Skill 文件。 - 原因:本地工作树包含完整仓库,而容器 Rust builder 原先只复制
server-rs/和public/;crate 中向上引用的docs/openapi/、.codex/skills/不会自动进入镜像构建文件系统。 - 处理:凡 api-server 通过
include_str!使用仓库根目录资源,都要在deploy/container/api-server.Dockerfile的 builder 阶段显式复制对应权威目录;不要再复制一份内容到 crate 内形成平行事实源。 - 验证:除本地 Cargo 测试外,检查 Dockerfile 构建上下文覆盖所有
include_str!相对路径;新增或移动嵌入资源时同步更新容器 COPY 和接入文档。 - 关联:
deploy/container/api-server.Dockerfile、server-rs/crates/api-server/src/external_mcp.rs、server-rs/crates/api-server/src/external_skill_api.rs、docs/openapi/genarrative-external-v1.openapi.json。
权威画布快照不能清掉本地待保存或在途布局(2026-08-03)
- 现象:用户拖动、缩放、改层序、背景色或 viewport 后,生成完成回包立即覆盖画布;450ms 防抖尚未触发或布局保存仍在途时,编辑静默丢失,undo 也可能被生成保护项阻断。
- 原因:服务端 revision 只能排序已提交事实,本地未落库布局没有 revision;直接清空 pending save 并整体应用权威快照等同于把“服务端更新更晚”误判成“服务端知道本地编辑”。
- 处理:保留同项目最新本地 dirty snapshot,权威回包先更新资源和生成终态,再按稳定 item ID 合并本地布局字段并基于新 revision 保存。旧权威项在新快照缺失表示后端删除,不能从 pending 或在途旧输入复活;新权威项必须合入,本地删除的旧项不能从权威回包复活。
- 生成器边界:
composerOpen与status / generatedLayerId / errorMessage一样属于后端生命周期事实;生成完成快照要求保持面板关闭时,不得被本地在途快照重新展开。提示词、参数和占位位置等本地布局编辑继续保留。集成测试夹具必须模拟后端真实完成快照:既有布局保持原位,完成结果层追加到末尾。同项目权威刷新还必须保留仍有效的单选、多选、生成占位选择或空选,只过滤已删除目标,不得无条件降成第一张图层的单选;首次载入 / 项目切换才设置默认选择。不要只跑 persistence Hook 单测,必须同时运行图片画布生成集成测试,覆盖完成后面板关闭、显式选择结果、背景清选和合并后 CAS 保存。 - 验证:分别覆盖防抖 pending、真实在途成功与 409、后端新增、后端删除、本地删除、viewport、背景色和生成面板完成态;运行
npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。
Provider schema 能力不能从统一工具标记直接推断(2026-08-03)
- 现象:把 OpenAI 风格
strict原样透传给完整 Anthropic 工具目录,单个 schema 不支持的约束或全请求工具 / optional / union 上限会让整次 planning 返回 400。 - 处理:能力不能从
apiKind=anthropic推断;只对已验证 endpoint/model 显式开启,AGC 当前仅自动识别官方 HTTPS endpoint 与 Claude 4.5+ 版本化 model id,旧模型、未知别名和第三方兼容网关默认关闭。协议适配层用官方支持关键词白名单生成 Anthropic 专用传输 schema,对已知不支持约束仅从传输副本剔除,未知关键词、不可解析 / 递归$ref和复杂度超限均失败关闭为 non-strict,不删工具或修改调用方原 schema。真实 live 样例应包含$defs/$ref嵌套 schema,并使用官方 Anthropic endpoint,第三方兼容网关不能替代官方能力证据。
可选 MCP server 的坏目录不能拖垮全部工具(2026-08-03)
- 现象:可选 server 已成功连接,但返回超限 schema、重复 tool identity 或要求未支持 task-mode 时,整个 MCP catalog 和本轮 Agent planning 一起失败。
- 处理:连接、tools/list、工具归一化与聚合容量都使用同一 required / optional 边界。optional 将该 server 投影为
connected=false + error + tool_count=0,required 保持失败关闭;被包入action.input的$ref只重定位当前 document 根的#/#/...JSON Pointer,命名 anchor、外部 URI 与带$id的 schema resource 内 fragment 不得改写。
React 异步读取必须在组件卸载时中止并失效(2026-08-04)
- 现象:单个 Vitest 文件全部通过,全量 CI 却在 jsdom 环境销毁后出现
ReferenceError: window is not defined;栈指向请求 Promise 的finally中调用 ReactsetState。 - 原因:测试触发了与断言无关的账户读取,较快环境中请求会在用例结束前失败,较慢 CI 中请求延迟到组件和 jsdom 均已销毁后才收束。仅用 revision 丢弃旧请求而不在卸载时推进 revision,最后一个在途请求仍会被误认作当前请求。
- 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配
AbortController,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有then / catch / finally在更新状态前都要检查 signal 与 revision。 - 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。
账号级轮询和并发 bootstrap 必须中止整条旧生命周期(2026-08-06)
- 现象:任务列表
Promise.all一侧失败后,另一侧请求可能跨过重试和卸载继续悬挂;微信充值第一次确认返回 pending 后切换账号,旧订单的延迟重试可能使用新账号 Token 再次请求,401 路径还会影响新账号登录态。 - 原因:只用 React state 或最终回调里的 owner 判断,无法阻止已安排的 timer、下一次 HTTP 请求和同轮未完成分支继续执行;每轮重试覆盖单个 controller ref,也会遗失更早的悬挂请求。
- 处理:并发 bootstrap 每次 attempt 使用独立
AbortController,任一分支失败时先中止同轮 controller 再安排有界重试,卸载时中止当前 attempt。充值订单从创建成功起持有同一个 owner、账号 revision 和AbortController;每次 delay、confirm 和 SSE watch 前后都校验生命周期,并把同一 signal 传到请求层;账号切换和卸载先 abort,再清理 ref、state 与旧支付回调 hash。 - 验证:bootstrap 用例覆盖“一侧 reject、另一侧 pending、重试后卸载”,并断言每轮 signal 都已中止;充值用 fake timer 证明首次确认 pending 后切换账号会中止 signal,推进全部退避时间也不会产生第二个确认请求或清理新账号 Token。
中止 refresh 等待不等于隔离 token 发布(2026-08-07)
- 现象:A 账号的写请求 401 后开始共享 refresh,随后切换到 B。A 的
AbortSignal虽然让业务请求立即结束且不再重放 POST,但底层 refresh 为了其它共享等待者不会被中止;A 的成功回包晚到时仍可能覆盖 B 的 token。 - 原因:只对
await叠加 abort 保护了调用链,没有给共享 Promise 的归属和最终 token 写入加账号栅栏;单一全局 Promise 还会让 B 加入 A 已在途的 refresh。 - 处理:公开 token setter / clearer 每次都推进 auth generation;refresh 按
generation + 发起时 access token共享、并以该快照 CAS 发布成功 token。快照已过期时成功回包转为失效结果,401/403 也不得清理新代际 token;新代际建立自己的 refresh Promise,旧 Promise 收尾时不得清掉新尝试。 - 验证:
src/services/apiClient.test.ts要等旧 refresh 完整收束后断言 B token 不变,并用两个独立 deferred response 证明 B 会发起第二个/api/auth/refresh;另覆盖旧 refresh 401 晚到不清 B token。
下游 manifest 回调测试不能冒充实时数据源(2026-08-05)
- 现象:工作台的资源、任务与版本重投影单测保持绿色,但后台 Agent 已更新
.agent/manifest.json后,打开中的工作台仍长期显示旧快照,只有重开项目才更新。 - 原因:测试 Supervisor 直接调用
onManifestChange,只证明App manifest -> WorkspaceLauncher -> ProjectDevelopmentView的下游桥接;真实 Runtime event 没有失效字段,监听器也没有重读 manifest。External Runner 又与 GUI 分属不同进程,Runner 内无法使用 GUIAppHandle,只补普通 Tauri event 仍不能形成生产链路。 - 处理:后台 manifest mutation 收敛到共用 Runtime emitter;GUI 内进程用带
manifestInvalidated的 Runtime update,External Runner 通过 GUI owner attach 登记的受令牌保护 loopback sink 转发专用失效事件。App 对当前项目做 single-flight manifest 重读,并以 mounted、项目路径和 scope version 丢弃迟到结果;WorkspaceLauncher 继续只消费完整 manifest 快照,不新增平行状态或轮询。 - 验证:集成测试必须渲染真实
App + WorkspaceLauncher、捕获真实 Tauri listener,让get_local_game_manifest从旧快照切换到新快照,并由非 Supervisor Agent 事件驱动资产、completed 任务、运行入口和版本卡出现;另测项目切换时旧请求迟到。测试夹具必须先等待目标 Tauri listener 注册完成,并等待项目写入最近列表后触发的只读目录状态刷新完成,再清空调用记录和发送失效事件;对“事件 -> manifest 重读 -> 工作台重投影”使用局部、有界的5_000ms等待,避免并行全量回归把合法后台检查、监听注册或异步投影调度误判为功能失败。旧的直接onManifestChange测试只能标记为下游桥接证据。
React 资源详情焦点不能依赖重建对象身份(2026-08-05)
- 现象:音频 / 视频播放器、文档链接或收起按钮正在获得焦点时,后台 manifest 更新会把焦点突然移回详情 region;若当前资源被删除,详情虽然消失,stale focused ID 和焦点可能残留到
body。 - 原因:资源投影每次生成新对象,
useLayoutEffect([focusedResource])把同一资源的内容更新误判为重新进入详情;删除路径没有显式恢复状态和可聚焦 fallback,项目 / 运行视图切换也可能沿用旧 trigger。 - 处理:焦点状态机只比较稳定
resourceId:null -> id与idA -> idB聚焦详情,idA -> idA保持当前 active element。显式收起 / Escape 才恢复原卡片与滚动;后台删除清理 focused / matching selected ID 并聚焦搜索框;项目或运行视图切换清空 trigger / restore。媒体预览副作用依赖稳定 ID、路径和类别,不因同 ID 对象重建先卸载控件。 - 验证:媒体控件获得焦点后用同 ID 新 manifest 重渲染并断言 active element 不变;删除资源后断言详情关闭、选中清理且搜索框获得焦点;既有收起、Escape、项目切换和运行切换测试继续通过。
manifest 与 revision 必须作为同一一致快照发布(2026-08-05)
- 现象:旧 manifest 的 React effect 在正式素材提交后才读取项目 revision,可能把“旧内容 + 新 revision”发给父级;若它先到,真正的 commit manifest 会被误判为同 revision 分叉并失败关闭。
- 原因:manifest 和 mutation revision 分开读取,却把其中任意时刻的两个值拼成一个权威快照;单独比较 callback 到达顺序无法修复这种身份错配。
- 处理:普通 Supervisor 投影固定执行“revision 前读 -> manifest -> revision 后读”,两次 revision 相同才发布,漂移时有界重试。素材 command/event 直接使用事务返回的完整 manifest 与对应 revision。父级按
projectPath + projectId单调接受更高 revision,同 revision 只允许内容一致的重复,低 revision 和分叉都不覆盖。 - 验证:分别覆盖 command/event 两种先后、成功后旧轮询和同 revision 不同 manifest;不能只用 eventId 去重而跳过 revision 防倒灌。
新资源自动聚焦不能把投影、布局和 DOM 当成同一时刻(2026-08-05)
- 现象:保存回调已经带回 manifest,但新卡片可能尚无 dependency/type 坐标或尚未提交 DOM;立即选择会得到空画布、错误滚动,迟到回调还会抢走用户后来选择的资源。
- 原因:把 durable commit、资源投影、关系图 ready、两份布局协调和 React DOM commit 压成一个“保存成功”布尔值,缺少保存尝试身份和用户意图 generation。
- 处理:保存开始记录
saveAttemptId + sessionId + draftId + commitId + focusGeneration。自动定位依次等待资源投影存在、dependency/type 两份布局 settled 且都有位置、搜索条件可见和稳定data-resource-idDOM 存在;按 commitId 只执行一次。切项目、切 mode、改选择/搜索、取消或开始新 flow 都推进 generation;迟到结果仍可合并权威 manifest,但不能改变选择。隐藏时保留搜索,只由显式“清除搜索并定位”建立新 generation。 - 验证:覆盖 manifest 已更新但布局未完成、DOM 后只聚焦一次、搜索隐藏、保存中切项目/改选择和连续保存;测试不得用 reload 或重开项目绕过阶段边界。
不要用自然语言精确 .replace() 维护 Runtime Prompt
- 现象:Prompt 文案稍作改写、增删空格或调整段落后,替换静默失效,代码中出现难以审阅的链式
.replace()。 - 原因:把自然语言全文同时当内容和结构锚点,没有稳定 section 身份。
- 处理:稳定片段拆为版本化 Bundle section,由 Rust 显式按角色、平台和配置组合;
agent_runtime_native_executable_tools()是原生可执行工具的权威源列表,同时供 Prompt 工具目录与 native capability registry 使用,mcp.call只服从当前请求的动态 MCP catalog。最终 Provider 请求构建器同样必须使用显式 section 与条件组合,不能以后置自然语言精确.replace()注入工具合同、平台规则或角色规则。安全规则保留在代码中。 - 验证:manifest 覆盖所有嵌入资源、版本一致、源列表中的原生工具全部进入 Prompt 与 native capability registry、
mcp.call不进入静态目录、Supervisor section 顺序和关键角色合同保持不变,并扫描prompt.rs与最终 Provider 请求构建器不再出现自然语言链式.replace();对最终 Provider 请求直接断言各角色、平台和配置分支的合同内容。
动态 MCP 函数参数不能只依赖 Provider schema
- 现象:动态 MCP 函数虽然带 catalog
inputSchema,Runtime 却只检查arguments.input是 object;非 strict 或兼容 Provider 可以返回缺 required、类型错误、enum 外值或 schema 外隐藏字段,并把它们原样送到外部工具。 - 风险:Provider 工具约束不是本地安全边界;特别是
writes + readOnlyHint=true自动放行的工具,schema 外字段可能改变外部副作用而不进入预期确认路径。 - 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified
arguments-schemarepair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件$ref解析,schema 无法安全编译时不广告或不执行。serde类型错误会包含实际字符串值,catalog miss 也会包含模型提交的 server/tool,因此这两类错误同样只能返回稳定类别,不能拼接原始错误、参数值或 schema 内容。 - 验证:覆盖 required、additionalProperties、type、enum、本地
$defs/$ref、HTTP/file 外部引用、无效 schema、错误脱敏,证明 legacy wrapper 在注入 fingerprint 前进入 repair,并证明带旧有效 fingerprint 的历史 pending 在实际调用前仍被 schema 拒绝。
2026-08-05 不要把 static smoke 当作完整专业交付
- 现象:code-prototype 已通过
game.static_smoke,但完成门明确报告missing-visible-art-slice-use;随后每轮 thinking summary 都是“已取得验证证据”,没有新 action,最终 loop-budget-exhausted。 - 处理:确定性交付与自动 plan completion 都必须先通过完整 completion gate,并要求当前 Run 最后一条同 mutation 工具调用与 Agent DB 中严格绑定当前身份的
status=okreceipt 一致;pending action 的 steer cursor fingerprint 也必须一致,失败 patch 或旧 Run receipt 不能取得交付资格。新 blocker 不回退旧 completed 步骤:已有非终态步骤时用明确 repair step 替换首个非终态步骤,其余保持 pending;只有全 completed 且仍有容量时才追加。8 步已满时进入外部 repair lane;计划已有 failed 步骤时立即失败关闭。回归同时覆盖失败 patch、跨 Run receipt、非零 steer cursor、8 个 completed 与 blocker,以及 failed plan 在 ownership/blocker 不同组合下都不会继续空转。
2026-08-05 Runtime 时间戳必须验证 Date 范围并保持来源身份
- 现象:极大但有限的持久时间值会让
toISOString()抛RangeError,或让界面显示Invalid Date;实时回复又借用其它 Runtime 的最近活动时间,文字继续流入时仍显示几分钟前,缺失时还随前端定时器漂移。 - 处理:秒/毫秒归一化后必须再检查
Date#getTime();不可表示的值统一显示“时间未知”并省略datetime。实时回复只使用 response stream 自己的updatedAt,不能借父/子 Runtime 活动时间或Date.now()。
2026-08-05 Pending manifest 容错必须覆盖真实终态时序
- 现象:手工把内存 state 改为 Completed 的测试通过,但真实 finalization 先写 durable Completed、再投影 manifest 时仍被 Pending 状态门拒绝;或 stale manifest 全 Completed 后,父 Run 忽略仍在运行的真实 child。
- 根因:测试没有写 durable terminal record,Pending 容错只验证了非终态 journal;DAG 又把 manifest
completed=true放在 active child 之前。终态投影和收束前检查使用了不同事实时序。 - 处理:测试必须按真实顺序分别写 durable Running 和 durable Completed。Pending 漂移只允许 state/journal 的 Running-Running 或 Completed-Completed 对;queued/waiting/failed/reconciliation 一律拒绝。active child 在无 Failed 时独立保持 DAG 活跃,项目 mutation gate 在写锁内核对当前 root,防止旧 child 污染新根 Run。
2026-08-05 Canvas 可达性不能在扇入调用图中回退 visited
- 根因:大 classic script 虽使用了 bounded direct-call graph,但
javascript_named_function_is_reachable在递归返回时删除 visited,只阻止当前环,不记忆已经遍历的祖先。render/update 图的大量重复调用让同一节点指数重算;父完成门又在 code-prototype 未完成时提前深验四个 Canvas 切片,使第一次 wake 就同步阻塞,200 次外层重试预算完全没有机会推进。 - 处理:单次可达性查询每个 function node 最多访问一次;全
Nonealias 历史直接返回,稳定外层初始化使用有调用前置证明的快路。父完成门只深验 Completed seed task,wake 预算耗尽写入 reconciliation。格子游戏的符号坐标只在唯一数值COLS / ROWS / CELL与画布范围能共同证明时接受,普通无界动态坐标继续拒绝。 - 验证:永久 fixture 至少包含 48 层重复扇入调用、IIFE 外层素材初始化、格子常量绘制、无界坐标反例和整画布尺寸引用;真实项目的全部四个切片还要在同一轮秒级返回 true。禁止用延长 queued timeout、Tokio timeout 或 synthetic 小脚本通过来替代真实大脚本复验。
2026-08-05 Canvas clamp 与 parent wake 不能走字符串或易失兜底
- 根因:Canvas owner 收紧后正确禁用了含尺寸成员的字符串兜底,但 AST 数值区间器尚不认识嵌套
Math.min / Math.maxclamp。若只查源码包含canvas.width,无法证明该 Canvas 创建了当前 context,也无法排除局部伪造Math。 - 处理:只在 semantic 证明未遮蔽全局
Math、上界读取当前 context 所属 Canvas、下界为0时生成有限区间;加入错误 Canvas、遮蔽 Math 和无界坐标负向回归。不要用字符串包含、变量名白名单或把未知动态值当0。 - 现象:parent wake 的 200 次瞬态预算耗尽后 Runtime 仍长期显示 running,或 lane 忙、取消、child 前进、manifest 损坏时 reconciliation 被静默丢弃或覆盖新状态。
- 处理:预算耗尽错误必须向上传递;lane 忙先持久化 deferred signal,再在 lane + 项目锁内重检最新事实。结构损坏路径使用不依赖 manifest hydration 的专用 journal/state 写入,CAS 失败转为继续对账,绝不覆写并发取消或 DAG 进展。可解析的空对象/空 runId 仍是损坏身份,只有完整有效的新 Run 才能阻止旧 marker;event/audit 的同键记录必须完整比对并拒绝冲突或重复。旧 task 已终态、Runtime 非 waiting 或新 Run 接管时,deferred signal 必须写 resolved/superseded,不能留给后续 wake 永久重复 settle。
- 测试注意:autonomous child fixture 先 linked Pending、后正式 Running;终态 runId 必须拒绝复用。判断 Completed-only 诊断时按每个 seed task 的实际状态分析,不能因为
code-prototypePending 就忽略已经 Completed 的art-asset-plan深验。
macOS 安全路径测试必须使用规范化临时目录(2026-08-05)
- 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发
WouldBlock。 - 原因:
tempfile::tempdir()默认返回/var/folders/...,而 macOS 的/var是指向/private/var的符号链接,生产安全校验会按设计拒绝该祖先;恢复测试的服务端读超时若仅为 2 秒,也会与完整测试负载下约 2 秒的首次请求形成窄竞态。 - 处理:凡测试会进入仓库可信路径校验,统一使用
crate::tests::canonical_test_tempdir(...),不得削弱生产符号链接拒绝规则;loopback 夹具保留有界超时,但为完整 CI 负载留足稳定裕量。 - 验证:在 macOS 上定向运行 provider request、pending recovery、autonomous continuation 与 generation recovery 用例,再运行完整
npm run check:native-shells。
Mach-O 文件头校验必须覆盖反字节序魔数(2026-08-05)
- 现象:macOS arm64 的 Tauri release 已成功构建且
file明确认定为 Mach-O,产物 staging 仍报“must be an executable Mach-O file”。 - 原因:脚本用
Buffer.readUInt32BE(0)读取文件头,却只比较0xfeedfacf等正序数值;arm64 常见头字节是cf fa ed fe,读取结果为0xcffaedfe。 - 处理:文件头白名单同时覆盖 32/64 位与 fat Mach-O 的正序和反字节序合法魔数,并由桌面配置门禁同时反查 staging 脚本和根级产物检查,不能改成只按扩展名或构建退出码判断。
- 验证:在 macOS 上构建真实 desktop-shell release,运行
npm run desktop-shell:stage-release-binary,再由npm run check:native-shells校验 staged 产物。
托管 MCP 新增公开域名时不能只更新网关路由(2026-08-05)
- 现象:
https://dev.genarrative.world/api/external/v1/mcp的 manifest、OpenAPI 和 Bearer 鉴权都正常,但鉴权后的initialize返回403 FORBIDDEN;通过 SSH 隧道访问同一 api-server 的 loopback 地址却可以正常列出 tools/resources。 - 原因:
rmcpStreamable HTTP transport 自带 DNS rebinding 防护。公网网关已经接入 dev 域名,但external_mcp::service()的allowed_hosts/allowed_origins仍只登记正式域名和 localhost,因此请求在 MCP 协议处理前被 transport 拒绝。 - 处理:新增公开 MCP 环境时,同批登记对应 Host 与 HTTPS Origin;不要通过客户端伪造
Host、关闭防护或改走内部 SpacetimeDB MCP 规避。allowlist 变更属于 api-server 发布内容,必须随正常 API release 部署到目标环境。 - 验证:自动测试使用真实公开 Host/Origin 执行
initialize;部署后再从公网域名完成带 Key 的initialize、tools/list、resources/list、Skill resource 读取和至少一个只读业务 tool 调用。loopback 成功只能证明 MCP 实现和 Key 可用,不能替代公网 Host 验收。 - 关联:
server-rs/crates/api-server/src/external_mcp.rs、docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md。
异步任务接受后的刷新回调不能统一套用 dialog 所有权(2026-08-05)
- 现象:正式生成任务已被后端接受,用户随后删除 dialog 或切换项目,任务仍继续并可能扣费,但钱包和任务列表没有刷新;反向问题是账号切换时若 project ID 暂时相同,旧任务可能刷新新账号的任务列表。
- 原因:把 dialog / canvas 的完整 UI 所有权同时用于账号级钱包和账号内项目级任务列表,或者任务列表只比较 project ID,没有校验账号。
- 处理:按副作用分层校验。钱包只比较账号;任务列表比较账号加项目;dialog、canvas、asset 和 layer 写回继续比较账号、项目、scope version 与原 dialog。正式请求已接受后,删除 UI 状态不等于取消后端任务。
- 验证:分别覆盖删除 dialog、同账号切项目、账号 A 切到账号 B 且 project ID 保持相同,以及原账号原项目原 dialog 仍有效的正常回写。
GUI owner 锁不能替代逐 boot 的事件接收端登记(2026-08-05)
- 现象:GUI 首次启动后 manifest 事件转发正常,但 Runner 被替换为新 boot 后只剩 owner 锁和 endpoint 可用,后台更新不再到达 GUI;或者 attach 响应只确认 owner,客户端却误记当前 boot 已完整登记,后续 ensure 不再重试。
- 原因:把 OS owner 生命周期约束与进程内事件 sink attachment 混成同一状态,或在
ensure_external_agent_runner之外执行一次性 attach;测试若用 actionId 等无关字段代替真实 sink port/token,也无法证明新 boot 重放的是可用接收端。 - 处理:GUI 按规范化 AppData 私有登记真实 sink port/token,
ensure_external_agent_runner的 endpoint 复用和新 Runner 就绪两条成功路径都按bootId重放。同 boot 成功后幂等,新 boot 必须重挂;RPC、attached或eventSinkAttached任一失败或缺失都不得记录成功 boot,并允许同 boot 后续重试。不同 AppData 不共享登记,未登记 CLI 不触发 attach;sink token 不进入日志、错误或公共状态。 - 验证:分别覆盖真实 port/token 跨 boot 原样重放、同 boot 幂等、新 boot 重挂、普通 attach 失败、
eventSinkAttached缺失与 false 后同 boot 重试、AppData 隔离和未登记 CLI 零副作用。
manifest relay 测试不能并行覆盖同一个全局 sink(2026-08-05)
- 现象:crate 根 relay 测试在配置全局 sink 后阻塞等待
TcpListener::accept(),同时 Runner GUI owner attach 测试通过另一条路径覆盖并清空 sink;事件可能被发往另一端口,原 listener 随后永久等待。断言或expect提前失败时,成功路径末尾的手动 clear 也不会执行。 - 原因:两个跨模块测试读写同一进程全局状态,却没有共用隔离边界;只给 accept 后取得的 stream 设置 read timeout 无法约束 accept 本身,payload 读取也缺少总 deadline。
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在
Drop中无条件清空;测试统一使用manifest_invalidation_sink_isolation_前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。 - 验证:用
--test-threads=2重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
远端图片 completed 不能冒充本地资源创建成功(2026-08-05)
- 现象:External operation 已返回 completed,但稳定引用缺失、下载失败、正式资产事务中断或 manifest 已提交而 UI 事件丢失时,界面仍可能提前显示“资源创建成功”,重复回调还可能再次下载、写文件或登记资源。
- 原因:把远端生成、媒体传输、本地 durability、manifest 投影、布局和选择压成一个 completed 布尔值;同时把 External idempotencyKey、operationId 或 taskId 暴露到公开草稿,导致恢复逻辑从非权威状态重建请求或误绑本地 task graph。
- 处理:使用私有 generation ledger 保存原请求、External 身份、稳定远端引用、固定 staging token 与本地 commit 身份;公开面只投影不可逆阶段。启动时先恢复阶段三事务,再恢复原 generation;重复 completed 先检查远端引用、staging 和 committed ledger,只有
committed | already-committed才进入 manifest 投影。用户取消等待只推进 focus generation,不删除账本或伪装远端取消。 - 精修补充:
sourceImageSrc是可下载的稳定媒体引用,sourceResourceId是资源身份,二者不能因为都可表现为字符串就填同一个 objectKey。本地local-asset:*只保留在本地 manifest 血缘;没有真实 External resourceId 时省略sourceResourceId。 - 验证:覆盖确认前零调用、同 key 连点、accepted 重启 GET-only、重复 completed、取消后迟到、下载后本地事务恢复、事件丢失、切项目/改选择、旧轮询隔离、实时布局与选择、精修血缘及敏感字段零泄漏。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs、apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx、docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md。
可恢复生成账本不能持久化 direct-upload ticket(2026-08-05)
- 现象:为支持参考图上传中断恢复,把完整 upload ticket 放进 generation ledger;账本随之包含 Provider host、formFields、policy、signature 或临时 Authorization,项目目录泄露即可复用临时凭证。
- 原因:把“恢复所需的稳定远端身份”和“仅供一次上传的临时授权材料”当成同一种持久状态。原子 sidecar 只能保证写入完整,不能让敏感字段变安全。
- 处理:ticket 结构不实现 Serialize/Deserialize,host/formFields 只在本次内存调用中使用。账本在上传前只保存稳定 bucket/objectKey;重启先用这组身份调用 object confirm,确认成功后只保留 objectKey/assetObjectId 并清掉上传中间态。账本测试必须直接序列化完整 ledger,扫描 Provider URL、Authorization、policy、signature、API Key 和 ticket 字段名。
- 验证:运行
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_generation_ledger_never_serializes_upload_credentials_or_provider_url,并继续检查公开草稿、manifest、事件和普通错误不含 prompt、operationId、Key、绝对路径或媒体正文。
客户端内部用途目录不能直接作为 legacyPrefix(2026-08-10)
- 现象:本地图片精修或视频、音频等全类型资源编辑点击生成后立即提示参考资源上传失败;私有账本的
uploadBucket/uploadObjectKey/operationId/requestBodyJson全为空,服务端也没有 OSS、confirm、生成或扣费记录。 - 原因:direct-upload ticket 的
legacyPrefix不是任意业务目录,而是platform-oss::LegacyAssetPrefix的权威白名单值。把asset-canvas-references或resource-editor-references直接放在该字段会被 api-server 在签名之前以400拒绝;客户端若把票据、OSS 和 confirm 全折叠成一个错误码,还会掩盖真正失败阶段。 - 处理:客户端编辑器统一使用合法私有
legacyPrefix=generated-character-drafts,把业务用途放入pathSegments:图片画布为editor/asset-canvas-references/<projectId>/<draftId>/<generationId>,全类型资源编辑为editor/resource-editor-references/<projectId>/<operationId>。仍严格执行 ticket → OSS form POST → object confirm,只有 confirm 返回自洽稳定objectKey/assetObjectId后才允许提交生成;不要为内部目录扩白名单或新建上传接口。图片路径按本地校验、票据、对象上传、对象确认分别使用安全错误码,票据材料继续只驻留内存。 - 验证:客户端端到端测试必须断言 confirm 早于生成 POST、请求使用精确前缀与 pathSegments、账本只持久化稳定对象身份;票据失败时断言
operationId/requestBodyJson为空且 manifest 只有源资产。api-server 测试应断言生成的 key 位于generated-character-drafts/editor/...且 access 为 private。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs、apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs、server-rs/crates/api-server/src/assets.rs。
Tauri 有平台登录 Token 不代表应调用主站画布 API(2026-08-10)
- 现象:客户端素材画布和全类型资源编辑从 WebView 读取平台 Access Token,把它传给 Tauri command,再调用
/api/editor/*、/api/assets/*和/api/runtime/external-generation/jobs/*;代码同时保留 External 分支,导致真实 UI、Runtime 和测试使用不同路径,发布客户端还错误依赖网页画布登录态。 - 原因:把“客户端壳有账号登录能力”误当成“客户端画布属于主站网页宿主”。主站和 Tauri 虽复用相同请求 DTO 与后端生成服务,但对外边界不同:主站使用站内认证路由,Tauri 远端媒体能力使用 Developer API Key 和 External v1 路由。
- 处理:Tauri 前端不读取、透传或持久化站内 Access Token;Rust 只从发布 AppData 私有
editorApi.baseUrl/apiKey解析 External 凭据。图片、视频、音效、BGM 的项目/素材库、上传、确认、生成、轮询与换签全部留在/api/external/v1,不访问内部 job 查询或账号/profile 接口。账本只绑定 External 配置身份指纹,升级前遗留的站内 endpoint 必须进入待对账状态,不能拿 External Key 自动重放。Key 缺失或无权限只返回安全配置错误,不打印 Key、Authorization、Provider 正文或私有路径。 - 验证:前端测试断言 command input 不含
accessToken/apiKey;Rust mock 服务器拒绝任何/api/editor/*、/api/assets/*和/api/runtime/external-generation/jobs/*请求,并覆盖 External202、原 operation 轮询、换签、非破坏性本地提交和账本零凭据。主站路由与 OpenAPI 未发生契约变化时不得为了客户端切换修改后端接口。 - 关联:
apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts、apps/ai-game-creator-shell/src/view/project-development/index.tsx、apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs、apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs。
prepared journal 之前同样存在正式事务崩溃窗口(2026-08-05)
- 现象:事务依次安装 before/after 快照后才写 journal;若进程在首个快照、全部快照或 journal 已写但 ledger 未写时退出,重启扫描看到 transaction 目录却无法进入原先只覆盖 prepared 之后的恢复状态机,可能留下孤儿目录或阻塞项目后续提交。
- 原因:把
prepared当成事务的第一个可观察持久阶段,忽略了构造 prepared 证据本身也由多次原子文件安装组成。 - 处理:把首个快照、全部快照和 journal 后/ledger 前加入故障矩阵。无 ledger 时只允许清理受控快照与本模块临时文件;若 journal 已存在,还必须证明正式目标不存在、manifest 和 project revision 精确等于 before。未知文件、正式文件存在或权威状态漂移全部失败关闭,不能递归猜测清理。清理后同步 transaction 父目录,并允许同 commit/idempotency 身份安全重放。
- 验证:故障矩阵逐阶段恢复;额外用同一幂等身份在快照残留清理后提交两次,必须得到一次 committed、一次 already-committed,manifest 仍只有一个 canvas asset。
宿主事件接线不能顺手复制共享 history 栈(2026-08-05)
- 现象:Tauri Surface 已复用共享 viewport/transform/renderer 数学,却另外维护 undo/redo refs、快照克隆和恢复逻辑;网站共享 hook 后续增加内容安全或字段恢复时,两端会静默分叉。
- 原因:把 Pointer 事件接线、宿主生命周期胶水和可复用 history 算法放在同一组件中,误以为没有复制整个画布目录就已经满足共享源码边界。
- 处理:两宿主直接消费共享
useCanvasHistory;共享 snapshot 统一覆盖 viewport、selection、图层位置和 width/height,宿主只声明本地媒体是否允许安全移除/重做。Tauri 仍可保留 Pointer capture/epoch/host callback 接线,但选择、平移、缩放、变换、renderer 和 history 状态机不得在宿主重写。 - 验证:主站 history 定向测试覆盖 resize undo/redo;Tauri 新建、导入、编辑、撤销重做和 durable commit 用例必须在同一共享 hook 下通过。
编辑器生成不能把传输重试、参考图截断和客户端 provenance 当成独立小问题(2026-08-05)
- 现象:生成 POST 首次已经入队但响应丢失时,客户端自动重试产生第二个任务;第 6 张或更多参考图仍显示在 UI / 元数据里,却没有送给 provider;直接构造请求还能把任意资源 ID 写成最终素材引用。
- 原因:客户端虽在重试中复用
x-request-id,队列入口却用随机 job id 生成 dedupe key;前端允许无限追加,api-server 和 provider 用.take(...)静默截断;generationInputs.references被当成可信持久 provenance。 - 处理:主站生成 POST 禁止自动重试,把显式复用的稳定 request id 接到队列唯一键并校验 replay payload;所有边界显式拒绝超限,前端还要预留主图槽位、统计在途上传,并在上传完成前拒绝模型切换、画布选图、提交生成、关联源图删除 / 剪切 / 素材删除和面板切换 / 关闭;reservation 必须绑定原面板上下文,批量部分失败时不能丢弃已经持久化的成功项。入队、完美像素及直接创建资源 / 素材时删除客户端 references,执行时按真实参考源和 owner 资源记录重建权威引用。历史任务比较必须兼容仅差已删除 references 的旧 payload,不能只保留旧 hash 却让 payload 比较误报冲突。
- 验证:覆盖同键同 payload / 不同 payload、普通图片第 6 张、带主图的 GPT-image-2 第 5 张额外引用、provider 6 / 15 张边界、伪造引用删除和 owned 资源 / 素材重建。
共享音频 Composer 架构冲突不能按单行选边(2026-08-06)
- 现象:master 的音频 composer 同时承载 SFX 与 BGM,并在组件内定义
isSoundEffect;功能分支把 BGM 拆成独立组件后,原组件变成 SFX-only。合并时只把 master 的条件占位表达式带回 SFX-only 组件,没有带回变量定义,最终在测试渲染阶段报isSoundEffect is not defined。 - 原因:冲突两侧代表不同组件架构,逐行保留看似有用的 JSX 会把一个架构中的局部条件拼进另一个架构。import 排序、格式检查和只覆盖单一 mode 的测试都不能证明这种组合成立。
- 处理:先确定权威组件边界,再按完整调用链解决冲突。图片画布音频入口当前决策是恢复一个共享
ImageCanvasAudioGenerationComposerView,由组件内isSoundEffect分流;BGM/SFX 的 validator、写回、锁和提交契约仍分别保持。不要只补一个常量后继续维持已经废弃的双 composer 边界。 - 验证:同时渲染
audio-sound-effect与audio-background-music,覆盖两个 mode 的正向控件和互斥负向断言、dialog / mode 切换、BGM 稳定 ID 与 controller 缺失的失败关闭,并运行ImageCanvasGenerationComposerView.test.tsx与 typecheck。
SFX Worker 不能只靠分层单测证明退款和零副作用(2026-08-07)
- 现象:LLM、ElevenLabs adapter、OSS 和 metadata 各自测试都通过,但无法直接证明余额不足时外部调用为零、翻译失败不会调用 provider、OSS / DB 失败只退款一次,或项目资源 / 素材 / 画布使用同一份权威 metadata。
- 原因:正式 SFX handler 把计费、翻译、provider、持久化和写回内联在一个 future 中;分层测试只能证明单个 helper,不能证明组合顺序和“失败后不继续”。同时若把 mock 流程另写一遍,它本身又可能与生产逻辑漂移。
- 处理:抽出单一 Worker 编排函数和计费 / stage adapter。生产 adapter 代理现有正式实现:OSS 后只准备 asset object / binding 候选,项目资源、账号素材、画布和 job 终态通过同一原子提交落库;测试 adapter 逐段记录调用与注入失败。组合矩阵同时断言 charge / refund、LLM / provider / OSS / writeback 计数、稳定 reason code 和权威值等值。ElevenLabs 二进制、MIME、大小、timeout 和 MP3 仍由 loopback adapter 测试负责,组合 mock 不替代协议测试。
- 验证:自动 / 手动时长 × Loop 四组合成功;余额不足;翻译、HTTP、无效音频、时长探测、OSS PUT / HEAD、asset confirm / bind、项目资源、账号素材和画布写回逐点失败;所有 job provider POST
<= 1,预扣后失败 refund= 1。
生成结果的稳定 ID 和 job 终态都不能代替 durable receipt(2026-08-06)
- 现象:Provider / OSS 已成功,但项目资源、账号素材、binding、画布和 job 只完成一部分;不确定结果重放时,有时又复制一批素材或重复推进 canvas revision。inline 路径在进程重启后尤其无法判断前一次提交是否整笔完成。
- 原因:把“请求已入队”、“某个稳定 ID 已存在”或“job 已 completed”误当成整批业务记录已原子提交的证据。request fingerprint 只证明用户请求,不绑定最终 slot、派生记录、画布候选和 compact result;仅比较资源 ID 也无法发现内容漂移。
- 处理:用
editor_generation_operation记录 durable receipt,分开 request fingerprint 与整笔 commit SHA-256。首次调用在同一 SpacetimeDB 事务中校验 lease 并写 object/resource/asset/binding/canvas/job/receipt;重放先查 receipt,再读回逐 slot 权威事实精确比较。receipt 缺失但 resource/asset/binding 已存在时失败关闭,不得补写 receipt;事务前已确认的 asset object 只能在 ID、bucket/key、owner、策略、媒体、来源和实体字段全部相等时复用。 - 时间与并发:
completed_at_micros必须为正数,object/resource/asset/binding/canvas 候选原时间字段与它一起纳入 commit SHA-256,不能在每次重放时重新取时;job 终态和完成事件只用 SpacetimeDBctx.timestamp。canvas CAS 冲突后只刷新 project 并重算布局,不重跑 Provider / OSS。OSS 尚未进入该事务,无引用 object 仍是需另行清理的边界,不要宣称跨 OSS exactly-once。 - queue completion 不能把 inline 完整响应无条件同时复制到
result和editor-agent-tool-call-result。图集/UI 最多 64 个切片会重复携带 resource/asset/prompt/generationInputs,容易超过 job payload 512 KiB 上限并让整个原子提交回滚。必须先按普通 UI、Editor Agent、External API 的消费方契约裁剪,再把最终 JSON 交给统一 procedure。 - 消费方身份不能在提交前重新读取 summary 兼容快照来判断:该快照按设计清空 dedupe key 并删除 generationInputs,Editor Agent / External API 会因此被误判成普通 UI。应在 worker 持有完整 claimed job 时把安全的 consumer kind 与 source identity 固化到调用上下文。
- procedure future 超时或连接断开不能直接映射为业务失败,远端事务可能已经提交。必须有界重放同一 prepared commit;明确 CAS 后才刷新 layout,且刷新 layout 应使用新时间,不能把项目
updated_at回拨。receipt 不复制 queue payload,只存摘要并从 job 权威行回读;跨记录 object/project 一致性必须在事务内验证,不能依赖当前 builder 通常会携带完整 candidate。 - job 的 owner/kind/fingerprint/lease 都正确仍不够:
source_entity_id还必须绑定结果项目,来源资源必须另查存在性与 owner/project 归属;否则同 owner 的 job 可以误写别的项目,或伪造跨用户/跨项目血缘。 - Provider 成功时计费 guard 已解除,后续原子持久化失败不会自动退款。但也不能在 api-server 先独立退款再尝试 fail job:过期 worker、fail 断线或原子提交已成功但回包丢失时,会变成「结果成功且已退款」。正确边界是在同一 SpacetimeDB 事务内先 fencing 当前 lease,再同步写退款账本和失败终态;不得期待
max_attempts = 1的编辑器任务再走租约耗尽路径补退。 - compact result 只能删除大 payload,不能删除消费方 DTO 必填字段或定位正式结果的稳定引用。角色动作/视频缺
ok、音效/BGM 缺prompt都会让 Editor Agent 把已完成 job 判成不可重试的回填失败;External 角色动作/视频如果创建了账号素材,completed 结果还必须保留assetId。 project_resource.source_resource_id校验不会自动覆盖editor_asset.source_resource_id;asset-only 结果可以没有项目资源候选,必须另查来源是本事务候选或已登记资源且属于同 owner;若本次结果有 project,还必须同 project。- inline 模式不会走 queue
fail_job,若计费 wrapper 在 Provider 成功时立即 disarm,后续的上传/原子持久化明确失败会扣费无结果。应在全部 inline owner handler 外统一延迟已成功 billing guard 到 durable commit;明确失败退款,但传输未知结果不退,否则远端已成功时又会变成「结果 + 退款」。 - 消费契约不能只测上游 builder:External v1 在 durable job 入库前还有一层 allowlist compactor,必须对最终 JSON 断言
ok / prompt / actualPrompt及稳定 resource/asset 引用。 - 计费 guard 的取消补偿必须区分 procedure dispatch 边界:
Build / PoolAcquire / ConnectBuild / ConnectHandshake等未发出阶段可确定退款;dispatch 后回包前的 future 取消与断连必须视为结果未知并保留扣款,等 durable receipt 对账。只在 error 返回后再标记 unknown 会留下取消窗口;必须在真正调用 procedure 前同步设置 task-local 标记,并在Procedure结果或确定未发出的失败后清除。 - compact DTO 的可选字段必须用最终 consumer payload 回归:Editor Agent 图片生成/修改的
provider会被脱敏删除,必须是可选字段;图标/UI 正常与 source-only fallback 则必须保留ok / prompt / actualPrompt。fallback 不得从可选 project resource 反推必填字段,否则无projectId任务会持久prompt/model=null、尺寸为零且图标/UI 丢失priceMudPoints。 - receipt 存在不等于引用 object 仍然可信:省略 candidate 的已登记 object 在重放时也要回读 owner/key/task/kind/媒体身份。同时先查同 operation ID job,存在 job 却漏传 completion 必须整笔回滚,否则会得到 receipt 成功而 job 仍 running 的永久分裂。resource/asset/binding 也不得仅核对 object ID/key,必须按 operation 合法 tuple 交叉验证业务元数据。
- 验证:故障注入覆盖 resource 后 asset/binding 失败、canvas CAS 冲突、过期 lease、同 operation 异 fingerprint / 异 commit、receipt 缺失的部分既有记录、精确既有 object 复用与 object 内容漂移;成功重放必须证明记录数、时间、binding/job 事件数和 canvas revision 全部不变。
- 关联:
docs/technical/【后端架构】编辑器生成结果原子提交与幂等重放方案-2026-08-06.md、Issue #134。
付费生成不能把素材目录归属校验留到 provider 之后(2026-08-07)
- 现象:登录用户给自己的合法
projectId搭配不存在或属于其他账号的assetFolderId,图片、改图、图集、UI 提取、视频、角色动作或音频生成会先扣泥点并调用付费 provider / OSS,直到创建editor_asset才拒绝目录;失败退款让用户成本归零,平台侧 provider 和存储成本不可逆。 - 原因:
normalize_generated_asset_folder_id只处理project、旧folder-*和默认目录 ID 的兼容映射,不读取 SpacetimeDB;真正的require_owned_asset_folder位于生成结果持久化末端。把“失败会退款”误当成副作用补偿,漏掉退款不能撤销 provider 请求与 OSS PUT。 - 处理:所有付费编辑器生成在队列 enqueue 前和 worker / inline 执行前复用只读
preflight_editor_generation_target_and_return,按认证 owner 校验可选项目及归一化目录;读取失败和归属不匹配一律失败关闭。helper 返回 canonical 项目与目录并覆写后续入队 / worker / 原子准备使用的 payload,不能校验 trim 后的项目却持久化原始空白值。角色图片、角色动作、图标 spritesheet 与 UI 提取省略目录时按实际默认目录预检;默认目录允许尚未创建,自定义目录必须存在且 owned。预检不替代最终 procedure 复验,也不保证跨外部调用的目录锁定。 - 验证:源码顺序回归必须覆盖图片生成、图片修改、图标 spritesheet、UI 设计图提取、视频、角色动作、SFX 与 BGM 的 enqueue / direct 两层,证明纯本地格式和
data:/blob:稳定引用门禁先执行,canonical target 在预检后写回 payload,远端引用解析、generation input rebuild、扣费、入队、provider 与 OSS 均留在预检之后;模块侧扫描证明预检只调用 runtime identity、项目、目录只读校验且不含 insert / update / delete,并覆盖带空白项目、project、旧folder-*、默认目录 ID、自定义目录与None归一化。
聚合点赞数不能恢复当前浏览者是否点赞(2026-08-10)
- 现象:陶泥儿精选点赞写入成功、总点赞数也正确,但刷新或重新挂载后图标恢复成未点赞;前端再次点击会发出错误意图或让计数体验混乱。
- 原因:
editor_showcase_asset.like_count只表达全局聚合,公开列表未携带editor_showcase_asset_like(showcase_id:user_id)的 viewer 状态;前端用生命周期内的空Set充当真相,刷新必然丢失。仅靠likeCount > 0无法判断其中是否包含当前用户。 - 处理:公开列表使用可选鉴权 viewer 投影;登录态从 Bearer claims 派生 user ID,并在公开列表事务内按确定性 like 主键返回
viewerLiked,匿名固定 false。个性化响应禁止共享缓存或错误降级,追加Vary: Authorization时不得覆盖 handler 或内层中间件已有字段。写入采用服务端确认式更新,账号 / 鉴权 scope 变化后重载并丢弃旧请求回包;request generation 的激活与失效必须跟随已提交 effect,不能在 render 阶段修改 ref;输入 user ID 不能代替 runtime service identity 鉴权。 - 验证:覆盖刷新 / remount 保持已点赞、pending 期间不改图标计数、失败保留旧状态并播报错误、分页保留 viewer state、鉴权恢复不发匿名请求、登录 / 退出 / 换号与旧首屏 / 分页 / POST 回包竞态、被 Suspense 放弃的 viewer 渲染不影响当前已提交请求,以及无效 Bearer 返回
401 + private,no-store + Vary: Authorization;中间件测试另需证明已有Vary字段被保留。
非整除 nearest 会让逻辑像素块宽窄不一(2026-08-10)
- 现象:像素规整后的图片虽然保持了源图宽高,放大观察却能看到相邻逻辑块占用的物理列数或行数不同,表现为部分块更宽、部分块更窄;整数倍样例看起来正常,换一张网格数不能整除输入尺寸的图才复现。
- 原因:逻辑图宽高为检测后的列数、行数。把
C × R的逻辑图用 nearest 恢复到W × H时,只要W % C != 0或H % R != 0,目标栅格就只能在不同逻辑像素间分配floor / ceil数量的列或行;nearest 能避免混色,却不能让非整数缩放后的块严格等大。只用128 × 128 → 64 × 64 → 128 × 128这类整数倍测试会掩盖问题。 - 处理:采样仍是一格一像素;编码前只允许横纵同一整数 N 的 nearest 放大,N 取最接近规整输入尺寸的正整数,禁止非整数拉回精确
W × H。成品宽高比等于逻辑图,尺寸接近但不保证等于源图或占位。普通图片和角色的前置 Lanczos 交付尺寸归一仍用于确定检测输入;平底网格源与透明 RGBA 源仍必须同尺寸。 - 验证:整数倍样例与非整除样例都不得出现宽窄不一的逻辑块;响应、project resource、账号素材和结果 layer 都记录最终 PNG 实际尺寸,且只持久化一个最终 PNG,没有未放大逻辑图、输入尺寸恢复版、诊断图或额外资源。失败降级用例继续验证 Alpha / 交付尺寸守卫。
- 关联:
server-rs/crates/platform-image/src/pixel_art_snapper.rs、server-rs/crates/api-server/src/editor_project.rs、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
Codex CLI 节点不能把进程终态当成 Runtime 提交证据(2026-08-10)
- 现象:
codex exec已启动或退出码为0,但 JSONL 没有turn.completed;或者已出现thread.started,Runner 随后退出,重启时误以为节点已完成。GUI 进程的 PATH、安装或登录状态与交互终端不同时,还可能在默认模式下静默落回 HTTP Provider。 - 原因:CLI 进程、Provider lifecycle、retry/handoff 和 Runtime finalization 是连续但不同的 durable owner;本地进程退出不能代替现有账本的原子提交与回读。模式切换若不进入配置指纹,还会把另一执行模式的 retry/handoff 当成当前结果。
- 处理:Codex CLI 必须只作为新的节点推理适配器,继续经过原 Runtime 的 lifecycle、retry、handoff、receipt、revision、verification 和 reconciliation。只接受完整 JSONL
turn.completed与可验证的最终消息;started无completed、超时、异常退出或输出破损都继续走原失败/核对边界。agentMode、CLI 版本和影响输出的固定参数必须纳入配置指纹;CLI 不可用时显式报错,不得静默切回 Provider。 - 验证:确定性回归覆盖 stdin prompt、空临时 cwd、read-only/no-shell/ephemeral 参数、structured tool call 转换、缺失终态、超时进程组回收和 stderr 不泄漏;真实 smoke 必须显式 opt-in,并分开报告 CLI 协议成功与本机认证/网络结果。
模拟 Provider 的测试不能继承生产默认 Agent 模式(2026-08-11)
- 现象:生产默认切到
codex_app_server后,HTTP mock Provider 测试不再收到请求并超时;只验证 retry/handoff identity 的纯单测还会在未安装 Codex CLI 的 CI Runner 上直接失败。本机安装了 Codex 时,相关遗漏可能被掩盖。 - 原因:测试只构造
agentLlm或直接调用读取全局配置的 identity helper,没有显式固定agentMode;缺省配置按正式产品合同选择codex_app_server,测试因此意外依赖本机 CLI 和认证环境。 - 处理:测试若验证 HTTP Provider 协议或 mock 请求,必须在测试配置中显式写入
agentMode: provider;只验证 Provider retry/handoff 数据结构的纯单测应调用显式接收模式的 identity helper。不得把生产默认模式改回 Provider,也不得仅为单测向通用 CI 镜像安装 Codex CLI。 - 验证:在 PATH 不含 Codex CLI 的环境运行 response-stream identity、MCP Runtime 和平台素材 mock 回归;同时保留独立的 Codex CLI/app-server 可用性与协议测试,防止 Provider 测试替代正式模式覆盖。
空 MCP 覆盖不会清除 Codex 用户配置中的 MCP(2026-08-10)
- 现象:以
codex app-server -c 'mcp_servers={}'启动后,thread/start仍发出用户配置中各 MCP server 的 startup 事件;若直接把这种进程当 AGC 节点 Agent,会出现 Codex 与 AGC 两套 ToolHost、副作用和审批边界。 - 原因:Codex
-c对 table 做配置合并,空 table 不是“删除已有所有条目”。长期 app-server 与一次性codex exec --ignore-user-config的配置隔离能力不同,不能照搬参数后假设用户配置已清空。 - 处理:为 AGC app-server 创建权限受限的临时
CODEX_HOME,只桥接已有auth.json,不带入用户config.toml、MCP、skills、hooks 或项目 rules;model/provider/effort 全部由 AGC 显式传入。server→client 请求一律拒绝,原生工具 item 一律协议失败。临时 HOME 和认证桥接不得写入项目、日志或持久账本。 - 验证:协议 smoke 必须观察
thread/start后没有 MCP startup 事件;fake server 还要断言 API Key 不在 argv、初始化只一次、结构化输出回到 AGC ToolHost。只看到initialize成功不能证明安全隔离成立。
只隔离 CODEX_HOME 仍会加载用户 Skill,且 Codex 原生工具默认不全关闭(2026-08-10)
- 现象:app-server 虽然使用临时
CODEX_HOME和 read-only turn,仍可能发现$HOME/.agents/skills,并默认提供 cached web search、multi-agent 及其它稳定原生能力;等item/completed后再拒绝已经太晚,工具调用和额外模型成本可能已发生。 - 原因:Codex 的用户 Skill 发现根是 OS HOME,不是
CODEX_HOME;dynamicTools=[]也只清空宿主动态工具,不会移除 Codex 内建工具。read-only/network off 是副作用防线,不等于从模型工具目录删除能力。 - 处理:同时隔离
HOME / USERPROFILE / APPDATA / LOCALAPPDATA,并在临时 workspace 创建空.git作为仓库发现边界,防止继续向父目录(例如/tmp)发现.codex/.agents;启动前设置web_search="disabled"、agents.enabled=false,并关闭 shell/unified exec/browser/plugin/image/workspace dependency 等原生 feature;接收item/started时只允许消息、计划、推理和压缩等被动 item,其余立即 interrupt。配置中的webSearchEnabled=true必须失败关闭并提示切provider。 - 验证:fake app-server 检查 argv 不含 Key、专用 Key 只在环境、继承
CODEX_API_KEY被移除、HOME 指向临时目录、web/multi-agent/shell 关闭;另覆盖 turn-start 回包前 drop 最终只发一次对应 interrupt。
多 Agent 共享一个 Codex app-server 会放大单点终态丢失(2026-08-10)
- 现象:多个节点最初已有
started -> completed,随后一个 app-server stdio 连接关闭,同一秒多个仍在途节点一起进入needs-reconciliation;单看 threadId 不同会误以为节点已经进程隔离。 - 原因:pool 只按 LLM 凭据和路由复用进程,节点身份只用于进程内 thread map。任一 stdout framing、子进程退出或连接故障都会 drain 整个进程的 pending/turn router,使所有共享节点同时失去可信终态。
- 处理:pool key 必须包含
projectId + agentId + sessionId + runId,每个权威节点直接持有独立 app-server 子进程;同节点 turn 还要串行,不能向同一 thread 并发turn/start。只发送当前 CLI schema 定义的字段;stderr 使用有界内存尾部并先脱敏再进入 Runner 诊断。 - 验证:至少两个节点并发各跑多轮,确认存在两个 app-server PID;终止其中一个后只有对应节点进入 reconciliation,另一个仍能收到
turn/completed。旧 AppData 缺agentMode且含非 Responses 路由时必须保留provider,不能在项目自动恢复时批量失败。
模拟 Provider 的隔离 AppData 测试必须显式固定执行模式(2026-08-11)
- 现象:测试已经写入本地 mock
baseUrl / apiKey / model,却收不到任何 HTTP 请求,日志反而显示 Codex app-server 启动或退出;Native shell 全量中多个后台 Agent 用例一起超时。 - 原因:新安装和没有迁移上下文的隔离 AppData 默认使用
codex_app_server。只写agentLlm不能表达测试要走 HTTP Provider;直接切换 runtime config dir 的 fixture 也不会经过会自动补agentMode的测试 helper。 - 处理:任何要断言模拟 HTTP Provider 请求的配置都必须显式写
agentMode=provider。测试 helper 可以统一补齐,但直接写隔离 AppData 的 fixture 仍须在自身 JSON 中声明,不能依赖仓库.env、用户 AppData 或历史迁移。 - 验证:先单跑失败用例确认请求命中 mock server,再执行完整
npm run check:native-shells;日志中不得出现该用例启动 Codex CLI/app-server,所有 Provider/MCP 请求数量和顺序按 fixture 闭合。
Tauri 生成与资源编辑恢复不能依赖 UI 快照、旧 Key 指纹或队列首项(2026-08-11)
- 现象:应用重启后,任务视频、项目版本或 refine 草稿无法恢复;轮换 Developer API Key 后已有 operation 被误判为配置变化,已受理任务一次 401/403 还可能永久进入对账;文本 Provider 已成功但尚未 staging 时崩溃会重复调用。目录中放入大量无关文件还能绕过 pending 扫描上限。一条远端已明确失败的老 operation 会持续占据队列首项,挡住后续已受理或已下载任务;manifest 已写而 project revision 未写时,又可能被误标为 committed,或者 journal 已证明提交后因项目继续合法修改而无法补 ledger。durable committed 后遗留 staging 可能因一次删除失败而被误报为提交失败,也可能在正式媒体或 manifest 身份已经漂移时被直接删除;manifest 已有派生子版本而 journal 缺失,或旧 journal 没有 revision 身份时,也可能被猜成已经提交。派生视频再次编辑时若把
assetObjectId当远端引用,生成会失败或指向错误身份。 - 原因:早期账本只保存显示层资源 ID,恢复时又依赖当前页面资源对象;refine
draftId只在组件 Map;配置指纹混入 Key 并把认证错误写成状态机终态;Provider 正文从内存直接进入解析/staging;扫描计数只在识别出 pending JSON 后递增;本地登记 ID 与 External generation 接受的稳定objectKey没有分层。恢复 UI 只选排序后第一项,而账本又没有远端终态失败/归档阶段;资产提交恢复把整个历史 after manifest 当作永久相等条件,没有区分目标事务事实与后续合法提交;旧 version journal 只保存 base/target 数值,不能证明完整 project revision before/after 身份。 - 处理:新账本冻结完整源快照,旧账本从权威 manifest、完成任务和版本记录有界恢复;refine 从正式 sidecar 按项目、意图、源素材和 active 状态唯一发现。服务身份用
service-origin-v1哈希规范化 External base URL,确认 UI 只展示去除路径与凭据的服务 origin;旧 Key-bound 指纹由快照绑定的显式挑战迁移,确认前零网络动作,已受理任务换 Key 后只 GET 原 operation。Provider 调用前先持久化 request-issued,成功正文再写 durable handoff 后解析/staging;issued 无 handoff 只能对账。扫描在读取每个目录条目时先计数,任何文件都消耗预算。提交前复验源摘要,远端请求只使用账本已确认的稳定objectKey,恢复始终复用原 operation 和请求字节。 - 队列与事务:独立恢复面板必须展示后端权威队列的所有 operation,读取失败不能伪装为空。
remote-failed不再重放,只能显式标为archived并保留账本;reconciliation-required不能归档。派生 asset 使用prepared -> media-installed -> manifest-written -> revision-written -> committedjournal,只对可证明状态前向恢复;尚未证明目标写入时严格核对 before/after,已证明目标 asset/media 与 target revision 后允许 manifest/revision 被后续合法提交继续推进,并补齐同一 ledger。committed 后只有 staging 与正式媒体摘要一致、manifest 按 ID 或路径唯一精确匹配 journal asset 时才尽力清理;删除 I/O 失败保持 durable committed,身份或媒体漂移保留 staging 并进入对账。version journal 同样冻结 project revision before/after 身份;manifest 已有子版本但 journal 缺失,或旧 journal 面对已推进 revision 无法补证时都失败关闭。 - 验证:覆盖跨进程唯一 refine 草稿发现和多候选失败关闭、文本 Provider 成功到 staging 崩溃后零重复调用、任务视频/版本旧账本恢复、所有目录条目上限、Key 轮换与旧 Key 无法验证时的显式确认、Accepted 后 401/403 再换 Key 只 GET 原 operation、远端明确失败只归档且零新网络/扣费、三条乱序恢复队列、项目切换迟到结果、asset transaction 各崩溃阶段、revision 后项目继续合法修改仍补齐 ledger、committed 后 staging 清理成功/删除 I/O 失败/媒体或 manifest 漂移保留、源摘要漂移拒绝、committed 视频二次派生,以及 manifest 子版本缺 journal、旧 version journal 无法证明 revision 推进与 version journal exactly-once。
子 Agent 澄清不能直接穿透用户输入权限(2026-08-12)
- 现象:child 需要产品取舍时若直接调用
user.input_request会被 owner gate 拒绝;若把它误走needs-repair,Supervisor 会错误返工而永远不向用户提问。 - 正确路径:child 返回短小的
AGC_NEEDS_USER_INPUT_V1envelope;Runtime 生成needs-user-inputdelivery,父 Supervisor 认领后创建自己的 durableuser.input_request。回答仍绑定原父 run,续建 child 由稳定 delegation identity 幂等控制。 - 验证:重复 wake / Runner 重启不得创建第二个用户输入 action;问题数量、字段长度、问题 SHA 和答案 SHA 不匹配时必须 fail-closed。child 直接请求用户输入仍应保持拒绝。
- 恢复加固:正常 completed child 的最终回复也必须进入 envelope 解析;回答后 pending 会被下一轮动作替换,因此 continuation 不能读取 current pending 作为证据,必须读取原 delivery 上的 durable request/answer 绑定。多个 child 各用一条用户请求逐一收束,禁止把不同 delivery 的问题和答案指纹拍平混用。
- 协议演进:
agent.delegate的澄清 continuation 字段虽然在 strict schema 中是 required nullable,但 Runtime 解析器仍必须接受完全未携带这三个字段的既有调用;只允许三者全缺失、全null或全为合法字符串,部分出现、部分字符串和非法 SHA 均失败关闭。新增 schema 字段时要同步原生函数目录断言与旧调用回归,避免协议修复轮次打乱 Supervisor 协作计划。 - 门禁优先级:已经存在真实
preview.validate失败 observation 或 durablefailed_playtest_revision时,具体试玩修复与新 revision 重新验证门禁必须先于通用“首次 mutation”门禁;否则 Runtime 会把明确的试玩修复错误收窄成普通 pre-mutation repair,导致 Supervisor 无法选择正确的协作动作。
无限画布延迟草稿与零位移不能制造新状态(2026-08-11)
- 现象:r5 的
loadDraft比 r6 更晚回包时会把草稿回退;pointerdown 后没有任何移动,pointerup 仍增加一条空 undo 并触发 CAS 保存;Tauri 自行维护 Shift toggle 后,Shift 单击唯一选中图层会意外清空选择。同一canvas.failed视图还可能把草稿保存或提交故障显示成生成重试。 - 原因:延迟回包只与发起时 revision 比较,没有在落地时复核当前最高 revision;指针按下就 capture history,而不是等首次真实几何变化;宿主复制了共享 selection 规则;失败状态没有携带发生故障的 operation 类别。
- 处理:generation progress、保存队列、生成/提交回包和延迟
loadDraft统一用当前 scope、触发最低 revision 与回包当下草稿的单调门禁;同 revision 只允许完整相等回包。Tauri 指针和键盘选择复用resolveLayerPointerSelection。pointerdown 只冻结快照,首次真实 move/resize/pan 才 capture 一次;零位移、未变选择和锁定图层不增加 undo、documentVersion 或草稿保存。 - 失败边界:
canvas.failed必须携带generation / draft-save / asset-commit / recovery / cancellation,只有generation失败显示“返回修改/重新确认”。保存/CAS 只重试或重载,提交/恢复只安全恢复或对账,取消故障只保留草稿继续编辑;初始恢复失败也不得进入生成重试。 - 验证:用 deferred Promise 覆盖 r5/r6 逆序、保存与 progress 交错和 scope 切换;同时覆盖 Shift 单选自身、多选拖动、指针完整序列、零位移、首次有效移动只一条 history,以及五类失败的可访问名称与按钮集。
Windows MSVC 测试不要依赖系统 OpenSSL(2026-08-17)
- 现象:Windows 上执行 Rust 测试时,
openssl-sys构建脚本因找不到 OpenSSL 开发目录或 vcpkg 而失败;机器即使带有 Strawberry Perl 的openssl.exe和libssl.a,也不能直接供x86_64-pc-windows-msvc链接。 - 原因:测试代码仅为动态生成 RSA fixture 引入
openssldev-dependency,从而把原本使用纯 Rust 加密实现的 crate 额外绑定到本机原生 OpenSSL 工具链;Strawberry 附带的库面向 MinGW,不等于可用的 MSVC OpenSSL SDK。 - 处理:测试优先使用明确标记、只供测试的固定 PEM fixture,并继续通过项目自身的密钥解析与签名验证路径覆盖真实行为;不要仅为生成 fixture 引入系统原生库,也不要把 MinGW OpenSSL 路径写入
OPENSSL_DIR冒充 MSVC 依赖。 - 验证:运行目标 crate 的
cargo tree -i openssl-sys --target all确认依赖已退出,再执行包含测试目标的cargo test --tests,不能只用不会编译 dev-dependency 的cargo check --lib代替。
图片编辑请求与资源卡几何不能依赖宽松 DTO 或可淘汰预览(2026-08-20)
- 现象一:Game Agent 快速编辑返回“平台明确拒绝”,远端没有创建任务。原因是
/api/external/v1/editor/images/edits使用deny_unknown_fields,客户端却把创建接口的assetKind以及本地来源字段一起发送;统一 400 文案又掩盖了真实参数错误。处理时必须按 create/edit 各自权威 DTO 组装请求,Game Agent kind 先映射为编辑器 canonical kind,并把安全业务错误分类,不能回显远端敏感 body。 - 现象二:图片预览已经读取真实宽高,但大量资源触发 LRU 淘汰后,卡片又退回
180x128,布局和连线随之跳动。原因是布局尺寸直接从可淘汰的 Blob/Object URL 预览缓存派生。处理时预览二进制仍按预算淘汰,但已验证的轻量pixelWidth/pixelHeight必须在当前项目、模式和资源 identity 作用域内独立保留;identity 或 scope 变化时再清理。 - 验证:分别覆盖严格 edit body、错误分类、生成占位恢复,以及超过预览缓存条目上限后首张图片仍保持真实比例、布局碰撞和依赖端点不退化。
2026-08-21 已有资源 ID 不等于可用于快速编辑的 canonical 来源
- manifest 中的
source.resourceId可能指向历史按game-background等 Game Agent 私有 kind 登记的远端资源。只检查 ID 前缀并直接传给sourceReferenceId会在 External v1 入队前得到unsupported-source-kind;本地 kind 映射只有在真正重新登记来源时才生效。 - 隐藏文件 input、逐张写
draft-media、前端追加图层再等待 autosave 的组合不是导入事务。失败可能表现为“按钮没反应”、留下孤立媒体或让图片出现在不可见的固定坐标。Tauri 正式链路应由原生多选和后端批量草稿更新闭环。 - 批量导入不能在安装首个媒体后继续执行带
?的 ID、路径、层序或 revision 计算;这些步骤必须先完成。原子草稿写入返回错误后,回读失败属于提交结果未知,必须保留媒体并报对账错误,不能把回读错误压成“未提交”后删除可能已被草稿引用的文件。所有回滚删除失败也必须显式上报。 - 失败 generation 同时存在私有 ledger 和 draft 投影,只在 React state 中
filter会在重启后复活。删除 UI 必须调用只允许明确失败任务的后端归档操作,结果未知任务不能删除。
运行中 generation 与 refine 来源身份不能按 create 链路处理(2026-08-21)
- 草稿 hydrate 后同步等待远端 generation recovery,会让生命周期长期停在 recovering,连平移和选图也被 inert。关键事务恢复与任务恢复必须拆开:前者先完成,后者后台推进且失败只进入任务/notice。
- refine 提交若无条件校验
source.resourceId == local-asset:<assetId>,会拒绝本来合法的editor-resource-*已登记来源,并可能把事务卡在revision-installed。创建与未登记本地资产才补 local identity;已登记 refine 必须保留并按解析后的来源身份回读。 - 画布内部 absolute overlay 只能覆盖宿主网格,嵌在左右分栏时不会遮住整个窗口。阻断性失败必须 portal 到
document.body,并用 fixed inset 覆盖整个 WebView;测试应验证 portal 的直接宿主和 fullscreen modifier。
自由画板 viewport 与资源 extent 分离(2026-08-24)
- 非空资源栏目使用无限画布:普通平移的
x / y不按资源 extent 夹取,窗口 resize、媒体测量和资源 extent 变化也不得把用户 viewport 拉回内容边界;共享MIN_SCALE/MAX_SCALE只约束缩放比例。只有首次进入组合或用户显式复位时,才用真实卡片包围盒计算 fit。 - 持久化布局坐标仍受
-1_000_000..=1_000_000合同限制,负坐标必须进入真实内容包围盒和显式 fit;这与 viewport 能否继续平移是两层独立语义。搜索只控制卡片与连线可见性,不删除、压缩或重排布局坐标,也不能让 viewport 随visibleResources收缩跳动。
素材画布旧提交不能只按当前状态猜测恢复(2026-08-22)
- 现象:同一 refine 资源的旧事务停在
reconciliation-required,后续另一笔事务已成功并更新当前 manifest/revision。恢复器只对比旧事务自己的 before/after 快照时,会把“已被后续提交取代”永久误报为需要人工对账。 - 处理:只有后继事务 committed、项目/草稿/资源身份一致、旧 after 与后继 before 精确衔接、后继 after 与当前 manifest/revision 精确一致、两份候选文件与 manifest 唯一引用都验证通过时,才把旧事务标为
superseded;候选文件和审计记录都保留。任何证据不完整继续失败关闭。 - 门禁:同一资源存在 prepared 或 reconciliation 事务时,新的“设为正式图”提交必须先被拒绝并引导安全恢复,不能继续制造另一笔可能覆盖旧结果的提交。
committed/rolled-back/superseded是可继续后续提交的终态。 - 关联坑:正式文件路径会追加 commitId,重新打开 refine 时不能把整个文件 stem 当作下一次素材名,否则多次精修后超过 80 字符并阻塞提交。路径只推导显示名,且必须循环剥离历史
--<uuid>后缀;确定性名称/用途校验要发生在读取候选与 staging 前,并按输入错误处理而不是进入事务恢复。
素材画布候选确认不能复用普通草稿保存(2026-08-23)
- 现象:候选挂载后用 fire-and-forget
updateDraft确认时,候选本身不增加前端 document version;用户立即设为最终图、导入、生成、归档或放弃草稿会携带旧 revision 与隐藏确认写入竞态。重新打开含candidate-ready的草稿还会重复保存同一画布、无意义推进 revision,并制造多窗口冲突。 - 处理:候选确认使用独立幂等宿主操作,在现有草稿锁内只更新匹配当前项目、草稿、generation、仍存在权威图层且尚未确认的私有 ledger,记录当前草稿 revision,但不改写草稿或推进 revision。普通草稿 update 只保护未确认候选,不再顺带确认。
- 前端门禁:确认任务进入 autosave FIFO,连续候选 ID 合并并串行处理;所有消费 draft revision 的提交、导入、生成、归档和 discard 必须先等待确认屏障。确认失败不得继续 revision-sensitive 操作,重复 hydrate 可以重发但后端必须零写入。
- 验证:前端覆盖确认 pending 时立即设为最终图和删除,断言提交 / update 在确认完成前均未发生;重复打开不调用
updateDraft且 revision 不变。Rust 覆盖首次确认、重复确认、未知或非候选 ID、普通 update 不确认,以及确认后显式删除。
隔离 Codex app-server 会误吃代理的 ChatGPT 额度头(2026-08-20)
- 现象:同一自定义 Responses endpoint 和 API Key 直接 HTTP 为 200,普通用户 HOME 下的 smoke 也完成,但隔离
CODEX_HOME/HOME的 app-server 在真正发请求前返回usageLimitExceeded,并投影 credits balance 0。 - 原因:开发网关把
X-Codex-*ChatGPT 账户额度头附在 API Key Provider 响应上;隔离进程没有用户 ChatGPT 额度状态覆盖,Codex 0.147 将这些头当作本地账户限制。模型、Key、MCP 和 Skill 均不是根因。 - 处理:只为 Direct conversation 启动随机 loopback
/responses流式代理;它不注入 Authorization,只转发请求自带 Bearer,拒绝其它方法/路径并剥离X-Codex-*账户头。不要复制用户auth.json来掩盖问题,也不要把 Provider 切换当根因修复。 - 验证:同时记录上游直接 200、未过滤时 credits=0/usage-limit、过滤后真实 turn completed;代理测试必须证明无 Bearer 拒绝、路径收窄、正文流式保留和额度头不下传。
Codex MCP 子进程不适合直接启动桌面浏览器(2026-08-20)
- 现象:同一
agc_browser_playtest在普通进程中能返回双视口截图,但从 Codex 启动的 STDIO MCP 子进程调用时 Chrome 启动超时。 - 原因:MCP 子进程继承隔离 HOME/AppData 和 Codex 进程约束;把真实浏览器或 GUI 登录态硬塞给子进程既不稳定,也扩大凭据边界。
- 处理:STDIO MCP 只做 schema 与协议适配;浏览器和付费美术通过随机 loopback 工具桥回到持有项目、登录态和正常桌面环境的客户端主进程。桥只绑定当前项目、限制请求大小和审核工具名,返回脱敏文本与有界 PNG。
- 验证:必须从真实 Codex thread 发起 MCP 调用并观察 desktop/mobile
readyState=complete与两张截图;直接运行 MCP 二进制成功不能替代该链路。
独立 Cargo workspace 的测试增量缓存会吞噬数百 GiB(2026-08-22)
- 现象:
server-rs/target与 AGCsrc-tauri/target合计超过231 GiB;其中两个debug/incremental约158.5 GiB,server-rs 累积1298个增量会话目录,AGC 累积100个。 - 原因:两个 Cargo workspace 拥有独立 target 和锁文件,AGC 又以 path dependency 复用若干 server-rs crate;更主要的是全量 / 分组
cargo test继承增量编译,每组 crate / feature / profile hash 都可以留下新会话,Cargo 不会按仓库期望自动收缩这些历史目录。 - 处理:保留两个 workspace 的产品 / 发布边界;两边
[profile.test]关闭 incremental 并固定debug=1,AGC dev profile 与 server-rs 对齐调试信息级别。日常用npm run audit:rust-build-cache只读核对;需要回收时先停止 Cargo / rustc,再显式运行npm run clean:rust-incremental -- --apply,只删两个固定增量目录。 - 验证:清理前后各跑一次只读审计并核对磁盘可用空间;分别运行 server-rs 与 AGC 定向
cargo test,确认 test profile 不再生成持久debug/incremental堆积。共享CARGO_TARGET_DIR必须另做并发启动基准,不得为节省磁盘直接改变生产产物路径。
BuildKit secret 不等于镜像内 secrets 不可提取(2026-08-22)
- 现象:构建时使用 BuildKit secret mount,日志和普通 build context 都没有出现明文,于是误以为最终镜像也能不可提取地保存 secrets,随后将镜像 push 或导出给不同信任域。
- 原因:BuildKit secret mount 只避免秘密作为
ARG/COPY进入构建上下文和中间指令;一旦 Dockerfile 把 mount 的内容安装到最终 rootfs,任何能读取、保存或运行该镜像的主体都可以提取它。 - 处理:预览固定
.env.local与 secrets 只从 Jenkins 宿主受控路径读取,严格校验目录0700、文件0600、owner、普通文件与非链接边界;只将它们安装到api-runtime:/srv/genarrative/.env.local与/srv/genarrative/.env.secrets.local并设为0400,明确排除 Nginx、Web、artifact 和其它镜像。镜像禁止推送或导出到跨信任边界。 - 更新与验证:任一源文件变更不会改动已存镜像,必须重建并替换 API 与 worker 容器;不能用重启代替。验收同时扫描 transcript/context/artifact 零泄漏,检查只有 API 与 worker 最终 rootfs 存在目标文件,并验证容器显式运行 env 优先覆盖内置值。
SpacetimeDB ping 健康不代表完整模块能在内存上限内实例化(2026-08-22)
- 现象:空库
/v1/ping已成功且容器显示 healthy,但spacetime publish在Publishing module...后连接提前关闭,紧接着端口拒绝连接。 - 原因:当前完整模块 init 的 RSS 会超过基础 Compose 旧
896mcgroup 上限;内核 OOM kill SpacetimeDB,客户端只看到传输错误,容易被误判为网络竞态。 - 处理:先查 kernel journal 的
Memory cgroup out of memory和目标容器 ID,再把本地/预发完整容器 SpacetimeDB 上限统一为2g;保留 page pool 限制。不要只增加 publish 重试,也不要把/healthz或首页改成数据库就绪探针。 - 验证:用新空卷完成模块 publish、五服务启动和 Web/API smoke,并确认容器未 OOM、SpacetimeDB 与 API/Nginx 最终 healthy。
同一本地项目切换账号后不能继续信任 manifest 远端 ID(2026-08-23)
- 现象:本地资源文件仍存在,但账号 A 生成后切到账号 B,快速编辑、GIF/视频派生、art-spec 下游生成或直连恢复提示画布不存在、无权限或资源不属于当前项目;原请求重试仍失败。
- 原因:本地 manifest 只有一组
source.canvasProjectId / resourceId / assetObjectId,旧实现把它同时当作历史来源和当前账号可编辑引用。新的 operation 虽已绑定 B,却会把 A 的 project/resource/object 身份配合 B Token 发出;另一部分链路又把 A 的画布 ID 与 B 按标题选出的项目严格比较。 - 处理:manifest 远端字段只保留历史 provenance。所有远端编辑和派生先解析
.agent/runtime/external-editor-bindings/中当前 principal 的项目/资源 binding;不存在时从本地正式文件按 asset ID、SHA-256、媒体类型和 canonical kind 在当前账号重登记。禁止按标题、manifest ID 或其他账号 committed ledger 自动采纳远端对象。A 的在途 operation 继续留在 A,B 只能开始自己的新操作。 - 并发与升级陷阱:首次 binding 不能用无锁的“先查后建”,否则两个 generation 会各自创建远端项目/目录;进程锁只能压住当前存活客户端,远端创建还必须携带由 binding key 派生的稳定
Idempotency-Key,关闭“响应已返回但 sidecar 未落盘”时重启重复创建的窗口。canonical ID 变更也不能只改新请求指纹;必须用本地 asset、路径和内容摘要白名单恢复旧 accepted/committed 账本,避免升级后已付费结果永久无法恢复。 - binding wire 升级陷阱:在仍标 v1 的 struct 上直接新增必填指纹,会让所有已有本地项目的 binding 反序列化失败,重现“画布项目丢失”。必须升为新 schema,用独立严格 legacy wire 只迁移完整通过现役不变量校验的旧文档;不能用 default 缺失字段或在身份校验前回写。
- 在途切号陷阱:只在首次 POST 前校验 session 不足以锁定 principal。获得
operationId后必须先落盘,后续每次 poll、download 和本地 commit 都要复验原 session;同步 commit 的“先校验再写入”仍有切号竞态,必须用会话租约把校验与本地安装线性化。手工 Tauri 入口若使用临时幂等键,也会在 202 后切号时丢失恢复身份。切号后继续用 A Token 轮询或安装 A 结果同样是账号边界缺失。 - renderer / native 两阶段陷阱:只用一个 generation 同时表示 UI 转换和 native CAS,或在 Rust 确认前先替换 committed Token,会让迟到 install / clear 把新账号覆盖回旧账号。正确做法是 auth generation 与 native 只增 generation 分离,所有 native mutation 串行,入队前冻结 token + origin + user,并持续以显式 account /
null期望权威对账迟到完成。对账失败不能恢复未确认候选会话,而要清空 committed 会话与 Token。 - auth origin 竞态陷阱:只在登录前持久化服务器选择不等于冻结事务 origin;请求 A 在途时若 UI 改为 B,返回的 A Token 可能被安装到 B origin。登录、hydrate 与 refresh 必须在首个请求前冻结 origin,让 HTTP 链与 native commit 共用该快照;同时按 origin 隔离 refresh singleflight,不能让 A 的 Promise 被 B 复用。
- stale refresh 陷阱:请求在入队前写入候选 Token 后,若 queued commit 直接因 generation 过期返回却不恢复 renderer authority,本地请求会继续携带错账号 Token。另外,A refresh 失败晚于 B 登录成功时,若仍向全局发布 failed,
AuthenticatedClient会把 B 误登出。所有 stale early return 先恢复当前 committed / desired Token;旧 owner 的迟到 refresh failure 只返回stale,不 clear、不发布 failed。 - 账本写入租约陷阱:只在进入命令时读一次当前账号,仍可能在 owner 绑定、服务身份指纹 / 挑战或确认落盘前切到 B,从而向 A 账本写入 B 身份。这些写入必须持同一冻结 platform session 租约并精确校验 ledger owner;公开的 request / confirm 命令也必须自身完成 owner 门禁,不能依赖调用方曾经走过恢复流程。
- 恢复列表泄漏陷阱:项目相同不代表账本对当前账号可见。如果列表只按 project / phase 扫描,切 B 后会展示 A 的 operation,迟到回包还可能把已清空的列表重新写回。扫描必须持当前 session 租约并按完整 owner
userId + api origin过滤;未绑 / 不完整的远端账本隐藏,纯本地编辑保留。renderer 还要用 auth generation 使恢复 read epoch 失效并清空相关操作状态。 - 追加审计回滚陷阱:Direct 恢复中
asset.register是 append-only 审计,返回错误不能证明 append 未持久。若 file / manifest 已落盘且审计成功或结果未知,删文件或回滚 manifest 会创造“审计已存在、资源却消失”的第二种不一致,重试还可能复制审计或重新扣费。应保留 file + manifest + audit 现场并标记reconciliation-required;后续 binding 失败也使用同一语义。 - Runner 跨 GUI 陷阱:WebView 的
authGeneration会随 GUI 进程重启从较低值重新开始,但 busy Runner 可能仍持有旧 GUI 的高 generation。只用 generation CAS 会把新账号安装误判为过期;只把 OS owner 锁当作授权,或保留独立platform.session.install/clear入口,又会让 Runner 在单次 IPC 丢失后继续使用旧账号。Runner 协议 v7 由 owner 锁创建随机 epoch,每次会话变更先推进 durable revision claim,并且只允许与 claim 完全匹配的runner.attach_gui_owner安装会话。Runner 要持续比对 claim,失配立即清空平台会话并拒绝 Runtime 请求;GUI 同步失败还要隔离或停止旧 Runner,不能只向前端报错。 - 验证:用真实平台会话 fixture 覆盖 A→B→重启→A,逐个断言 B 的 URL、请求体和稳定引用中没有 A 的 project/resource/object ID;另测同名 localProjectId 隔离、改名不漂移、源摘要变化、非 refine 参考、视频 committed objectKey、art-spec 派生以及在途 operation 切号零网络。Runner 回归还要覆盖高 generation 旧 epoch 被低 generation 新 epoch 正确替换、迟到旧 epoch/revision attach 失败关闭、claim 改写或同步失败后旧 Runner 零 Runtime 请求。Developer Key fixture 不能替代平台账号隔离证据。
- 关联:
apps/ai-game-creator-shell/src-tauri/src/project/external_editor_bindings.rs、project/resource_editor.rs、project/asset_canvas/generation.rs、agent/generation/canvas_generation.rs、agent/direct_runtime.rs。
Direct 美术工具不能把“包存在”当成“本次已生成”(2026-08-23)
- 现象:用户明确要求重做美术或切换游戏主题,工具仍立即返回
assets/art-spec.png、assets/direct-game-background.png、assets/art-spritesheet.png;新需求没有 Provider operation,游戏继续使用旧图。切片虽然已经落盘,也可能不出现在资源管理或工具结果中。 - 原因:旧 Direct 工具只有
brief,完整包校验成功后无条件短路;固定阶段账本恢复又未比较本次生成 prompt。切片只写文件和切片清单,未作为顶层 manifest asset 投影;工具桥只返回三条主路径并丢失切片与 warning。 - 处理:显式重做使用
mode=regenerate,普通请求使用reuse-or-create。重生成必须由当前最新 User 消息明确授权并绑定客户端稳定clientTurnId。授权先对完整原文做 Unicode NFKC 与撇号规范化,随后整串必须完整匹配审核过的独立立即执行指令,只允许句号/感叹号收尾;不得剥离引号、方括号或代码片段,动作前后也不得携带 brief、条件、否定、选择、确认、费用、延迟或其它文本。风格需求先单独描述,再由下一条独立“请重新生成美术”消息确认;不要靠扩充 deny 同义词推断付费同意。同一调用完成回包丢失只从completed持久结果等值重放,不能因重试再次扣费。App 必须在 Direct 调用前落盘原始 User 消息和回合 ID,Tauri 必须在成功返回前幂等落盘同 ID assistant 终态;同进程重复水合若命中“回合仍在运行”,只能显示瞬时占用提示,不得以稳定 assistant messageId 写成终态并抢占原执行的成功回复。恢复扫描与启动前置恢复必须发现resetting / compensating / anchored in-progress并在专用锁内恢复,重开项目只续跑真正未回答的原身份。整条付费链必须持有专用跨进程执行锁;换新回合时先持久化resetting再清理旧阶段账本,不得通过删除 workflow 留出无主窗口。崩溃补偿只恢复旧文件并清 replacement CAS 锚点,已prepared / accepted阶段账本、原Idempotency-Key / operationId必须保留,同冻结意图续跑复用旧请求;未知账本在文件 mutation 前失败关闭。只有没有任何阶段账本和替换锚点的孤立 workflow 空壳可原子接管;旧 schema 和其余冲突失败关闭。遇到 prompt 或当前 art-spec 身份不一致的未决账本必须保留原 operation 并返回对账错误。Direct app-server 可写边界只限真实 canonicalgame/,canonical 项目根的原生 OS 路径字节与权威 manifestprojectId经域标签和独立长度前缀编码后共同绑定连接池和 thread 身份,不得写项目根、assets/、.agent/,也不得获得网络、命令、MCP 或权限扩权;受控工具如果需要项目级客户端状态,只能从同一真实game/cwd 经相同校验内部反查项目根,不能扩大模型可写根。标准图集首次创建和重生成都要求四张透明、可见、像素及平台身份唯一的 canonical 切片;工具只回传通过私有回执、公开清单、源图和顶层登记交叉验证的slicePaths与安全resources。部分/opaque/重复/缺回执切片必须告警,不能把公开清单或顶层自述身份当作 Canvas 权威。 - 同进程恢复补充:命中“同一 stable turn 仍在运行”后除禁止写 assistant 终态外,还必须删除当前 App 实例的恢复 claim。这样原调用随后成功时显式刷新能读取其终态,随后失败时也能按相同
clientTurnId再次续跑;不要靠重载 WebView 清理进程内 claim,也不要用无界定时轮询制造并发调用。 - 严格图集崩溃补充:规范图和背景图的两文件 rollback 不覆盖严格图集事务已经整体修改的
.agent/manifest.json、私有回执、公开清单、主图集、四切片和切片清单。必须在严格调用前持久化 pending 及九项旧合同身份;重启恢复先对账底层严格事务,完整新合同直接收口完成,完整旧合同才补偿前两阶段,混合或漂移状态失败关闭。不要在严格提交成功后局部恢复前两张图。 - 部分旧包补充:rollback 的规范图/背景图必须保存旧字节与旧 manifest entry,不能把这两项缺失隐式当成空内容;显式
regenerate因此只在这两项可信可回滚时开放。历史主图集、私有回执、公开清单或 canonical 切片可以缺失,但八个严格路径与受管顶层 asset identity 必须逐项冻结其真实Present/Some或Missing/None状态,补偿也必须恢复相同存在性。不要因为旧美术包缺切片而阻断重生成,也不要把本轮新建的严格文件误记成旧文件。 - 对话扫描与 claim 补充:历史中出现
User A / User B / Assistant B时,B 已回答不代表 A 已回答,扫描必须继续寻找 A。成功 Direct 回复在 Rust 返回前已经落盘,前端冗余 append 失败不能据此重跑;普通错误回复的显式落盘失败时,恢复 claim 要保持到 React fallback writer 的同一 messageId append 明确收敛。writer 成功或明确失败后才释放;失败路径要停止该消息的自动迟到重试,再由显式/history复用原 stable turn。终态后及时删除 claim,避免 Set 无界增长。
GDD 历史审批回执误触发当前恢复提示(2026-08-27)
- 现象:修改 GDD 后新版本标题和内容已正确落盘,但审批卡一直显示“审批状态正在恢复”。
- 原因:
approval pending是当前 lineage 最新 GDD 的单例投影;恢复扫描却让每个历史 receipt 都拿它做 identity 比对。旧 receipt 与新 pending 不同并不表示损坏。 - 处理:历史 receipt 只修复自身投影;只有最新 GDD 的 receipt 才能校验、更新或清理当前 approval pending。不要在前端隐藏
recoveryPending,也不要取消最新版本的 identity fail-closed 检查。
Native shell CI 不能在测试阶段重新解析 Cargo registry(2026-08-26)
- 现象:原生壳 job 的依赖预取成功后,AGC 检查仍在
platform-llm测试阶段重新更新 registry index,并因symphonia下载的 TLS EOF 失败。 - 原因:native job 没有显式预取
server-rs/Cargo.toml;ai-game-creator-shell:check的 server-rs workspace 命令没有--locked。 - 处理:native job 预取 server-rs、桌面壳和 AGC 壳三份 lockfile;
platform-llm、shared-contracts与 AGC 壳测试统一使用--locked,不降低原生测试门禁。 - 验证:workflow 回归测试、锁定的 Rust 测试和原生壳门禁均需运行;若本地 EAS CLI 版本漂移,应单独报告环境阻塞,不把它误判为本次 Cargo CI 修复失败。
Tauri Windows NSIS 工具缓存不能依赖 Jenkins systemprofile AppData(2026-09-02)
- 现象:AGC Windows 构建已完成 Rust release binary,Tauri 下载并解压 NSIS 后,在
Running makensis报Unable to start child process, error 0x2。 - 原因:Tauri Windows bundler 执行自己的
<tauri_tools_path>\NSIS\makensis.exe,默认位于当前用户%LOCALAPPDATA%\tauri,不使用 PATH 中预装的makensis.exe;Jenkins LocalSystem/systemprofile 的 AppData 可能无法启动该缓存程序。 - 处理:Windows 专用 Tauri 配置设置
bundle.useLocalToolsDir: true,把工具缓存到src-tauri/target/.tauri/NSIS;Jenkins 预检验证实际用户、项目工具目录可写,并在构建失败时打印实际缓存路径和绝对路径执行结果。 - 验证:不要把 PATH 中
makensis可发现当作 Tauri bundler 工具可执行的充分证据;需要在 Windows Agent 上检查target/.tauri/NSIS/makensis.exe、ACL、EDR/Defender 和直接-VERSION结果。
AGC 前端等待超时与 worker 端口冲突
backend模式需要同时探测 API、worker 和必要的 SpacetimeDB 端口。只让 API 漂移会遗漏仍被旧进程占用的 worker 端口。- AGC Vite 在配套后端全部就绪后才启动。Tauri 的前端等待超时及随后的
code=143可能是 worker 先失败导致的连带退出,应先检查.app/dev-stack.json各服务状态和监听进程,不能直接归因于 Vite 或数据库。 - 外层
start-tauri-dev.mjs应在启动 Tauri 前完成配套开发服务准备,并统一收束自有服务进程树;不要让冷编译和数据库发布挤占 Tauri 的前端就绪等待。自动发布必须保留数据库,不能靠清库解决启动问题。 - CLI 与 standalone 可能是两个独立软链接。必须同时核对
spacetime --version和spacetimedb-standalone --version,不能把 CLI 的版本记录当作宿主版本证明;PATH 中存在宿主时启动器检查两者一致。更换宿主前停机备份数据,按原目录启动,不通过清库处理版本错配。 - Router 配置缺失不应只在首次请求时报错。API/All 启动必须先校验官方地址、固定模型、provisioning secret、管理员 Token 和凭据加密密钥;否则服务看似 healthy,但登录后的 provisioning/模型调用才延迟失败。
共享画布框选需要识别 world 的真实后代命中
- 现象:共享
CanvasWorld在 world 外层增加.genarrative-image-canvas__world-content后,点击或拖拽空白画布时事件target是内层 div;框选逻辑若只检查外层 world 本身,就会只清除焦点而不创建选框。 - 原因:viewport 上的 pointer 事件通过冒泡接收,
event.currentTarget是 viewport,空白区域的event.target可能是 world 的任意后代,不保证命中外层元素。 - 处理:框选命中判断使用
Element.closest('.genarrative-image-canvas__world'),并保留 viewport 自身命中路径;回归测试通过真实CanvasWorldDOM 的pointerdown冒泡覆盖内层 world content。 - 验证:
src/components/image-editor/useImageCanvasStageInteractions.test.tsx覆盖内层 world content 命中,定向交互测试通过。 - 关联:
packages/image-canvas-react/src/useImageCanvasStageInteractions.ts、packages/image-canvas-react/src/CanvasWorld.tsx。
跨平台“死进程 PID” fixture 必须落在 Unix 有符号 32 位范围内(2026-09-09)
- 现象:
project_lock_recovery::project_write_lock_reclaims_dead_owner_pid在 Windows 本地通过,在 Linux CI 报“死进程残留锁未被回收,实际错误:项目正在被其他写操作占用”。 - 原因:fixture 用
0xFFFF_FFF0当死 PID;Unix 的pid_t是有符号 32 位,i32::try_from直接失败,存活判定返回None(无法判定)而不是Some(false),于是落回 600 秒保守分支,残留锁不再被回收。 - 处理:实现层把“平台不可能分配出的进程号”(0 或超出平台 pid 宽度)判为持有者不存在并直接回收;fixture 改用
i32::MAX as u64 - 1,另加u64::MAX非法进程号用例。 - 验证:WSL Ubuntu 上
cargo test --bin genarrative-ai-game-creator-shell project_lock_recovery7 条全过;Windows 上把可表示性判据临时回退到 HEAD 后,只有project_write_lock_reclaims_unrepresentable_owner_pid失败,说明该用例确实覆盖这条分支;Linux CI 的原始失败记录覆盖越界 PID 分支。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs、apps/ai-game-creator-shell/src-tauri/src/tests/project_lock_recovery.rs。
锁文件回收的判定与删除必须基于同一份快照(2026-09-09)
- 现象:两个实例同时恢复同一个崩溃项目时,后判定的一方可能删掉另一方刚装上的活锁;
remove_file的NotFound还会被当成硬失败,直接报“清理失效项目写锁失败”。 - 原因:
project_write_lock_can_be_reclaimed只是快照观察,调用方拿到 true 后无条件 unlink;helper 还分别重读createdAt/pid/processStartedAt,并发替换会拼出“旧 inode 的死 PID + 新 inode 的启动身份”。 - 处理:payload 只解析一次并连同字节一起快照;删除前重新核对字节,只有内容仍是判定时的内容才 unlink;文件已消失或被替换时返回 false 并重试
create_new,不报错。 - 补充:
project_write_lock_file_modified_seconds读不到 mtime 时不要返回0——纪元 0 会被算成极大年龄,把保守判定反转成“立刻回收”,甚至把活持有者当 PID 复用抢走;要用Option区分“mtime 未知”和“mtime 等于纪元 0”。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs。
2026-09-10 Direct 写通道零等待取锁把毫秒级竞争放大成整轮阻断
- 现象:AGC 新建项目后第一轮 Direct 对话里,唯一的项目写入通道
agc_write_file每次都返回项目正在被其他写操作占用:<项目根>\.agent\project.lock;同一轮 15 次写入全部status=failed且durationMs只有 24-42ms,而只读工具(agc_list_project_files、agc_list_registered_assets、client.session.info)全部正常,整轮无法写入任何项目文件。 - 原因:
.agent/project.lock是create_new存在性锁,Direct 通道却调零等待的acquire_project_write_lock,与 App 自身其它写通道(美术 lane、revision、conversation、预览等)撞车就直接判死;而file.write / file.patch / file.delete等入口走的是约 10 秒有界等待。失败耗时本身就是判据:几十毫秒说明这个入口根本没等,同等争用在其它通道会被等待窗口吸收。此外错误文案不带commandId / pid / createdAt / ownerIsSelf,又把 ACL 拒绝、delete-pending 和真实跨进程争用压成同一句话,现场很容易被误判成“残留锁”。 - 处理:Direct 写路径改用统一的有界等待;
create_new失败按可重试 / 权限 / 其它分三类并给不同文案;争用错误与等待日志都带持锁方身份,ownerIsSelf区分“自己人”和“别人”。 - 补充:重试性不能由一次 metadata 观察决定。Windows 上
create_new在目标被删除的拆链窗口里会返回ACCESS_DENIED(5),而此刻exists()往往已经报 false——本机 6 万次建锁 / 删锁竞争实测 396-538 例命中“5 + 目标不可见”。用path.exists()当场判成权限拒绝,等待层会立刻失败关闭,把同一个问题换成更误导的 ACL 文案。正确形状是:重试性只看错误码(ACCESS_DENIED(5)/ sharing violation(32) / lock violation(33) / 已存在都可重试),终态改判放到等满预算之后——真的等过、目标此刻仍不存在,才改判成权限拒绝。分类判据把平台作为参数传入,Linux CI 才能覆盖 Windows 分支(CI 没有 Windows runner,#[cfg(windows)]用例在 CI 里一次都不跑)。 - 排查顺序:① 先看失败耗时——几十毫秒说明该入口没等,是等待窗口缺失,不是锁没释放。② 看错误里的
ownerIsSelf:true指向同进程另一条写通道,false指向外部进程;该字段只比 PID,PID 复用会把外人报成自己人,只当线索、不当判据(回收判据另有processStartedAt兜底)。③ 锁文件在失败后通常已被 Drop 删掉,现场缺文件不否定争用;同理agc_list_registered_assets的pendingOperations: []只表示没有在跑的付费生成,与项目写锁无关,不构成“锁没有持有者”的证据。④.agent/.manifest.json.lock是 manifest 的持久 OS 文件锁(Windows 不共享写句柄 / Unixflock),0 字节长期存在是设计如此,不是残留锁,也不要用项目写锁的回收判据去处理它。⑤ 看到“项目写锁路径权限被拒绝”时注意它的含义:这是等满等待窗口后的终态改判(Windows 上真实 ACL 拒绝就走这条路),不是某一瞬间的 metadata 观察;反过来,项目正在被其他写操作占用:…(持锁方身份不可读:锁文件此刻不存在…)是零等待入口无法区分拆链窗口与 ACL 拒绝时的并列表述,两者不要互相否定。 - 验证:Rust 定向覆盖同进程重叠写等待、同轮并行写、活外部进程持锁带身份、权限拒绝不投影成争用,以及两条平台无关判据用例(重试性只由错误码决定、终态改判三条件);
runtime_project_write_lock_waits_for_delete_pending_target继续覆盖带句柄的 delete-pending 必须等到成功。 - 关联:
apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs、apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs、apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs。