Files
Genarrative/docs/project-memory/shared-memory/pitfalls.md
T
kdletters 51740c021b 补齐流水线维护退出选项与停服公告
Full 发布全程保持维护并在最后按选项退出
API Deploy 支持成功后保留维护并补齐回归门禁
新增停服公告维护页并同步运维文档
2026-07-12 21:24:17 +08:00

546 KiB
Raw Blame History

踩坑与排障记录

用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。

记录格式

## 问题标题

- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue

禁止 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 / runningdry-run 后 apply 同一批时保持输入 cursor 不变,最后一批即使 has_more=false 只要仍有命中也必须 apply,只有 apply 成功后才推进到返回 cursor。SpacetimeDB CLI 2.5 的 Option<T> 非空参数必须使用 SATS sum 编码;维护脚本要统一编码 cursor_job_idowner_user_idcompleted_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.rsserver-rs/crates/spacetime-module/src/external_generation.rsserver-rs/crates/api-server/src/external_generation.rssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md

React 测试因内部状态或实现细节正常重构就碎

  • 现象:修改组件结构、按钮排序、图标库 class、提示文案或 hook 内部状态名后,React 测试大量失败,但真实用户流程和对外契约没有变化。
  • 原因:测试把 data-testid 仪表盘、textContent 拼接状态、完整对象 / 数组顺序、图标 class 或长文案当成契约;这些断言绑定的是实现形状,不是用户行为或稳定边界。
  • 处理:按 React 组件测试准则 重写到更稳定的层级。用户流程测试断言 role / label / URL / 弹窗 / callbackhook 逻辑用 renderHook 直接验证公开返回契约;DTO / payload 使用关键字段或 expect.objectContaining(...)。只有产品明确要求的可访问语义、固定顺序或渲染边界才保留精确断言。
  • 验证:运行触达文件的定向 vitest,必要时追加 npm run typechecknpm run check:encodinggit diff --check
  • 关联:docs/technical/【前端测试】React组件测试准则-2026-06-26.mdsrc/components/image-editor/useCanvasGenerationDialogs.test.tsxsrc/components/image-editor/ImageCanvasBottomToolbarView.test.tsx

图片画布素材库删除要匹配 sourceResourceId

  • 现象:素材库中删除了已经生成并进入素材库的资源,但画布上对应图层仍然存在,刷新后还可能从已保存布局里恢复。
  • 原因:生成素材进入账号级素材库时可能通过 editor_asset.sourceResourceId 指向原项目资源;如果前端素材库映射和级联删除只比较 sourceAssetIdassetObjectIdobjectKeysrc,就会漏掉只靠项目资源 ID 关联的历史 / 后端生成图层。
  • 处理:EditorAsset 必须保留 sourceResourceId;从素材库添加到画布时继续写入图层;删除素材时同时比较 layer.resourceId / layer.sourceResourceIdasset.sourceResourceId
  • 验证:ImageCanvasEditorModel.test.ts 覆盖素材库 source resource 保留,useImageCanvasAssetCanvasBridge.test.tsx 覆盖资源 ID 级联清理,ImageCanvasEditorAssetsIntegration.test.tsx 覆盖删除后保存的新 layout 不再包含被删图层。
  • 关联:src/components/image-editor/ImageCanvasEditorModel.tssrc/components/image-editor/useImageCanvasAssetCanvasBridge.tssrc/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx

后台素材查询不要用 SQL 直查 editor_asset

  • 现象:后台“素材查询”报 HTTP 400no 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_return procedure,由 spacetime-client typed facade 调用后再在 api-server 映射作者展示名和陶泥号。新增类似后台只读能力时,优先补窄 procedure / read model,不要复用 fetch_admin_dashboard_rows 直查私有源表。
  • 验证:cargo check -p spacetime-client --manifest-path server-rs/Cargo.tomlcargo check -p api-server --manifest-path server-rs/Cargo.tomlnpm run check:spacetime-schema
  • 关联:server-rs/crates/spacetime-module/src/editor_project_storage.rsserver-rs/crates/spacetime-client/src/editor_project.rsserver-rs/crates/api-server/src/admin.rs

陶泥儿精选重复先查同源同媒体画布副本

  • 现象:每次从项目素材中把同一个生成素材拖到画布上,陶泥儿精选 都多出一张看起来相同的素材。
  • 原因:素材拖入画布会为图层实例准备 editor_project_resource;如果该素材本来带 sourceResourceId 指向原始生成资源,而新资源仍按普通 generated 资源公开,精选就会把原件和每次拖拽产生的同源同媒体副本都展示出来。
  • 处理:创建项目资源时保留 source_resource_id,并在同项目已有同源同媒体资源时复用已有 resource;确需创建同源同媒体副本时默认 public_showcase_enabled = false。公开精选读取和前端精选模型都跳过 sourceResourceId 指回同一媒体原件的副本,但不要按图片地址全局去重,避免不同生成步骤共享占位图时被误合并。
  • 验证:creationShowcaseModel.test.ts 覆盖同源同媒体副本只展示原件;ImageCanvasEditorAssetsIntegration.test.tsx 覆盖拖拽生成素材到画布时继续提交 sourceResourceIdcargo check -p spacetime-module --manifest-path server-rs/Cargo.toml 确认后端资源复用 / 精选过滤逻辑可编译。
  • 关联:server-rs/crates/spacetime-module/src/editor_project_storage.rssrc/components/creation-home/creationShowcaseModel.tssrc/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx

陶泥儿精选作者丢失先查公开作者展示字段

  • 现象:/creation陶泥儿精选 卡片和预览弹窗中,素材下方作者名消失、只显示占位,或错误显示内部用户 ID。
  • 原因:精选数据源来自公开 editor_project_resource 快照;如果 SpacetimeDB read model、spacetime-client mapper 或 api-server payload 任一层漏传 authorDisplayName / display_nameauthorPublicUserCode / 陶泥号,前端没有可展示的公开作者字段。owner_user_id / ownerUserId / user_id 是内部归属字段,不是公开作者名兜底。
  • 处理:EditorProjectResourceSnapshotEditorProjectResourceRecordEditorProjectResourcePayload 需要一路保留公开作者展示字段;前端 creationShowcaseModel 优先显示 authorDisplayName / display_name,没有展示名时显示 authorPublicUserCode / 陶泥号,绝不能兜底到内部 ownerUserId / user_id
  • 验证:creationShowcaseModel.test.ts 覆盖展示名优先、陶泥号兜底和内部 owner/user id 不展示;editorProjectClient.test.ts 覆盖公开精选接口客户端保留公开作者字段;若改动 SpacetimeDB read model,再运行 npm run spacetime:generatecargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.tomlnpm run check:spacetime-schema
  • 关联:server-rs/crates/spacetime-module/src/editor_project_storage.rsserver-rs/crates/spacetime-client/src/mapper/editor_project.rsserver-rs/crates/api-server/src/editor_project.rssrc/components/creation-home/creationShowcaseModel.ts

陶泥儿精选瀑布流变宽先查 multi-column 容器宽度

  • 现象:release /creation 桌面端精选卡片明显变宽,第三列被裁到屏幕外,页面内部可横向滑动;dev 看起来正常。
  • 原因:精选瀑布流使用 column-count,当它作为 grid item 时如果没有显式 width: 100% / min-width: 0Chrome 会用多列内容的 intrinsic width 反向撑开 grid track。线上实测 1920 视口下 section 为 1296pxwaterfall 被撑到约 2048px,单卡宽约 672px
  • 处理:保留 multi-column 瀑布流时,.creation-landing__asset-waterfall 必须显式约束 width: 100%min-width: 0;不要只看单张图片天然尺寸或改卡片宽度。
  • 验证:Playwright / CSSOM 检查 .creation-landing__section.creation-landing__asset-waterfall、首张 .creation-landing__asset-cardgetBoundingClientRect()waterfall 宽度应等于 section 宽度。
  • 关联:src/index.csssrc/components/creation-home/CreationLandingView.tsx

画板外部生成排队超时不是失败

  • 现象:画板发起付费图片生成后,前端弹出 生成任务仍在队列中,请稍后刷新画布查看结果,但后端任务仍在队列或执行中,后续可能正常完成。
  • 原因:画板生成已经接入后端外部生成任务队列,queued / running 是正式任务状态;旧前端轮询等待窗口到期时直接抛错,导致正常排队被提交流程 catch 成失败 UI。
  • 处理:waitForEditorGenerationQueue 等待超时只返回“仍在后端继续执行”,调用方停止本次前端等待并保留生成中状态;只有后端任务终态为 failed 才展示失败。
  • 验证:画板生成 workflow 测试覆盖 queueState 持续 running 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。
  • 关联:src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx

画板参考图 objectKey 必须先做归属校验

  • 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
  • 原因:Data URL 参考图可以直接解析,但 objectKey 是服务端私有对象引用;只校验 generated 前缀、mime 和大小不能证明它属于当前账号。
  • 处理:所有编辑器参考图入口统一走 parse_editor_reference_image(state, owner_user_id, source);objectKey 分支必须先在当前账号的项目资源、素材库资产或 asset_object 中匹配 owner / bucket / key,再读取 OSS。图标素材等额外参考图必须真实传到 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.rsserver-rs/crates/spacetime-client/src/assets.rssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts

资产换签不能把 generated 前缀当成 objectKey 授权

  • 现象:主站或 External API 只要拿到另一个账号的 generated objectKey 就能换签,已登记的私有对象因为 key 同时命中 legacy 前缀而被匿名读取,或者 /api/assets/read-url 已拒绝但 /api/assets/read-bytes 仍能读出原始字节;后台资源预览为解决跨账号读取又误把主站入口整体放开。
  • 原因:legacyPublicPathobjectKey 代表两种不同信任边界。前者仅用于未登记历史公开作品兼容,后者是正式对象引用;只检查 generated 前缀、或在查询 asset_object metadata 前直接接受 legacy 白名单,都不能证明对象公开或属于调用方。签名 URL 和 bytes proxy 如果各写一套判断也容易漂移。
  • 处理:read-urlread-bytes 必须共用 authorize_asset_read_target,先按配置 bucket / 精确 key 查询 asset_objectmetadata 一旦存在,即使 key 命中 legacy 前缀,也严格执行 PublicRead / owner ACL。只有 metadata 不存在且显式 legacyPublicPath 命中 platform_oss::LEGACY_PUBLIC_PREFIXES 时才允许匿名兼容;任意 objectKey 必须登记。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、公开对象、本人私有对象、跨 owner、匿名私有、External owner、confirm owner 不可变和 admin-only endpoint;对 read-urlread-bytes 使用同一组授权矩阵,并断言 Admin 成功换签会生成不含 signed URL 的管理员主体审计事件。
  • 关联:server-rs/crates/api-server/src/assets.rsserver-rs/crates/api-server/src/external_assets_api.rsserver-rs/crates/api-server/src/admin.rsserver-rs/crates/api-server/src/modules/admin.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.rsserver-rs/crates/api-server/src/character_animation_assets.rsserver-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 offSkipping tokio metrics,或 SpacetimeDB CLI 提示存在新版本 / 当前版本较旧,看起来像启动异常。
  • 原因:这些是 tracing、metrics 或 CLI 更新提示,不代表本地 dev 栈失败;同一段日志后续仍可能已经完成 SpacetimeDB listening on 127.0.0.1:3101、模块 publish、api-server /healthz 200、主站 Vite 3000 和后台 Vite 3102 ready。
  • 处理:排查本地 dev 栈时先确认成功锚点:[dev:spacetime] actualUpdated databaseapi-server 已完成 tracing 初始化并开始监听/healthz 200、两个 Vite ready。只有缺少这些锚点或进程退出时,再继续查 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.jsondocs/project-memory/shared-memory/development-workflow.md

私有兑换码不适用先查同手机号重复账号

  • 现象:后台把私有兑换码配给某个陶泥号或手机号后,用户用同一手机号登录兑换仍提示 该兑换码不适用于当前账号
  • 原因:认证表里可能存在同一手机号的多条 user_account。如果认证工作集重建 phone_to_user_id 时让 user_account.phone_number_e164 后写覆盖前写,当前登录态会漂到没有 auth_identity 的重复账号,而兑换码白名单仍指向另一个内部 user_id
  • 处理:重建认证工作集时以 typed AuthStoreProjectionViewuser_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 session user_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.rsserver-rs/crates/spacetime-module/src/auth/tables.rsserver-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-opsnpm run check:production-api-releasenpm run check:production-api-deploy,确认构建包、Jenkins 归档链路和 deploy fail-fast 检查口径一致。
  • 关联:jenkins/Jenkinsfile.production-api-buildjenkins/Jenkinsfile.production-api-deployscripts/deploy/production-api-deploy.shscripts/check-production-ops-guardrails.mjs

图片画布角色动作结果主类型是序列帧

  • 现象:产品要求画板 生成角色动作 返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理。
  • 原因:后端仍需要先生成 previewVideoPath 再抽帧、绿幕去背和落 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载。
  • 处理:角色动作结果图层主 src 使用 frames[0].imageSrcmediaType 固定为 image-sequenceassetKind 固定为 character-animation,完整帧列表写入 imageSequenceFramespreviewVideoPath 只作为来源信息保留。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入 sequences/<编号-标题>/frames/。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。
  • 验证:ImageCanvasGenerationLayerModel 应断言动作结果 src 为首帧且 mediaType="image-sequence";画布集成测试应出现 画布序列帧:角色动作 图片播放器,不应出现角色动作 <video>;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录。
  • 关联:src/components/image-editor/ImageCanvasGenerationLayerModel.tssrc/components/image-editor/ImageCanvasWorldView.tsxsrc/components/image-editor/ImageCanvasExportModel.tsserver-rs/crates/api-server/src/character_animation_assets.rsdocs/【编辑器】画板角色形象生成入口设计-2026-06-15.md

图片画布序列帧播放不要复用普通图片淡入样式

  • 现象:角色动作序列帧播放时看起来像每帧之间在渐变或闪烁。
  • 原因:序列帧播放器每帧切换可低至 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 中 transitionnone
  • 关联:src/components/image-editor/ImageCanvasWorldView.tsxsrc/index.csssrc/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 对内继续使用用户语义更清晰的 promptplatform-audio 转发到 VectorEngine Vidu 时同时发送 promptsound,两者值保持一致。不要把 UI 改回 Suno task: "sound"typetempo 或 BPM。
  • 验证:cargo test -p platform-audio --manifest-path server-rs/Cargo.toml --test vector_engine_audio 中音效请求体测试必须同时断言 promptsound;必要时用线上生成音效 smoke 确认不再出现 missing field sound
  • 关联:server-rs/crates/platform-audio/src/request.rsserver-rs/crates/platform-audio/tests/vector_engine_audio.rsdocs/【编辑器】画板音乐生成入口设计-2026-06-18.md

Suno 任务完成不代表已经拿到 wav 下载地址

  • 现象:画板生成背景音乐时,前端报 音频生成尚未返回可下载地址(requestId:...);画板生成音效时,前端可能报 获取 Suno 音效 wav 失败(requestId:...)。上游任务可能已经完成,但 wav 下载地址还没就绪。
  • 原因:VectorEngine Suno /suno/fetch/{task_id} 可能先在 data 中返回歌曲 / 音效 clip id,而不是直接返回 .wav / .mp3 URL;需要再调用 /suno/act/wav/{clipId} 获取 wav_file_url。如果只兼容 data 是字符串,会漏掉 data 对象 / 数组里的 idclip_idaudioIdsongId
  • 处理:platform-audio 查询 Suno 结果时先提取直接音频 URL;没有 URL 时,从 data 字符串、对象或数组提取 clip id,逐个调用 /suno/act/wav/{clipId}。已拿到 clip id 但 wav 地址仍未就绪,或 wav 子请求暂时返回上游错误时,都保持 processing 让上层继续轮询,不能直接判定为缺少可下载地址或 wav 获取失败。
  • 验证:cargo test -p platform-audio --manifest-path server-rs/Cargo.tomlcargo test -p api-server vector_engine_audio_generation --manifest-path server-rs/Cargo.toml
  • 关联:server-rs/crates/platform-audio/src/client.rsserver-rs/crates/platform-audio/src/response.rsdocs/【编辑器】画板音乐生成入口设计-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.tsxsrc/components/image-editor/ImageCanvasMediaModel.tsdocs/【编辑器】画板音乐生成入口设计-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.tsxsrc/components/image-editor/ImageCanvasEditorView.test.tsxdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片编辑器生成中设定面板不要和预览框绑成同一可见性

  • 现象:图片编辑器里点击生成后,有时设定面板没收起,有时连画布上的占位预览一起消失,看起来像“生成中界面掉了”。
  • 原因:生成中状态只收了 composer 可见性,或把占位框和设定面板共用了同一段条件渲染;面板隐藏后把 placeholder 也一起卸掉,就会丢掉 Lovart 式生成中预览。
  • 处理:进入 generating 后只隐藏设定面板,保留占位框和生成中状态胶囊;面板外观、预览框和结果图层分开控制,不共用同一个 composerOpen 条件。
  • 验证:对应测试应断言生成按钮点击后 dialog 消失但 image-canvas-editor__generation-frame--generating 仍然存在。
  • 关联:src/components/image-editor/ImageCanvasEditorView.tsxsrc/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.tssrc/components/image-editor/useImageCanvasKeyboardShortcuts.tssrc/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.tssrc/components/image-editor/ImageCanvasInteractionModel.tsdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片画布视口拖动卡顿先查自动保存和小地图合帧

  • 现象:素材多或序列帧多时,拖动小地图视口框或手型平移明显卡顿,像是接口慢或 CSS 动画掉帧,但网络请求不一定异常。
  • 原因:pointermove 高频修改 viewport 会触发画布重渲染、小地图模型重算和工程持久化 effect;持久化链路会同步 serializeCanvasLayoutJSON.stringify 并写 sessionStorage。远端 PATCH 有防抖也挡不住本地同步缓存写入。
  • 处理:把 viewport 拖动标记为临时交互;拖动中只更新画布显示,不触发项目保存、session cache 写入或封面快照上传,pointerup / pointercancel 后保存最终 viewport。小地图拖动的 updateViewportFromMinimapDrag 必须用 requestAnimationFrame 合帧,结束拖拽时 flush 最后一帧。
  • 验证:npm run test -- src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx --reporter verbose 应覆盖小地图拖动合帧、平移 / 小地图 viewport 交互边界,以及拖动期间不写 sessionStorage / 不调用 saveEditorProjectLayout
  • 关联:src/components/image-editor/useImageCanvasViewportControls.tssrc/components/image-editor/useImageCanvasStageInteractions.tssrc/components/image-editor/useImageCanvasProjectPersistence.tsdocs/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.tssrc/components/image-editor/ImageCanvasPublicationMaterialsDemoPanelView.tsxsrc/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx

图片画布快速编辑不要直接提交普通图片 URL

  • 现象:图片画布快速编辑站内示例图、历史 generated 图或 OSS generated 图时,后端返回 修改图片参考图必须是图片 Data URL。
  • 原因:快速编辑直接把图层 src 塞进 /api/editor/images/generationsreferenceImageSrcs;默认示例图和部分持久化图层的 src/creation-type-references/*.webp/generated-* 或 OSS URL,而 api-server 的编辑参考图解析只接收 data:image/*;base64,...
  • 处理:前端统一通过 resolveEditorImageReferenceDataUrl(...) 在提交前读取图片字节并转成图片 Data URL;Data URL 原样透传,/generated-* 和 generated OSS URL 先走 /api/assets/read-url 换签后由浏览器直读 OSS,直读失败时才 fallback 到 /api/assets/read-bytes,普通 public 路径直接 fetch。
  • 验证:npm run test -- src/services/image-editor/editorImageReference.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx -t "editorImageReference|converts non-data-url quick edit source images before submitting references"
  • 关联:src/services/image-editor/editorImageReference.tssrc/components/image-editor/ImageCanvasEditorView.tsxdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片编辑器角色动画不要默认提交大图 Data URL

  • 现象:图片编辑器里对角色图点击 生成动画 后,后端返回 Failed to buffer the request body: length limit exceeded,请求还没进入角色动画 handler。
  • 原因:角色动画生成请求曾把角色图片 src 原样作为 sourceImageSrc 放进 JSON;角色图如果是较大的 Data URL,会超过 Axum 默认 2MB body limit,在 Json 提取器阶段被拦截。
  • 处理:前端在角色图已持久化时优先提交 objectKey,只把 Data URL 作为未持久化本地临时图兜底;后端 /api/editor/character-animations/generations 单独配置 12MB body limit 兼容旧请求,但新链路不应依赖传大图 JSON。
  • 验证:npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "only exposes character animation"cargo test -p api-server editor_character_animation_accepts_character_image_body_above_default_limit --manifest-path server-rs/Cargo.toml
  • 关联:src/components/image-editor/ImageCanvasEditorView.tsxserver-rs/crates/api-server/src/modules/play_flow.rsserver-rs/crates/api-server/src/app.rsdocs/【编辑器】画板角色形象生成入口设计-2026-06-15.md

图片编辑器角色动画抽帧不要采到视频尾点

  • 现象:画板角色图点击 生成动画 后,Ark 视频已生成并上传 OSS,但后端返回 ffmpeg 已执行但未产出动作帧文件(requestId:...)
  • 原因:FFmpeg 在 -ss 采样时间落到视频尾点附近时可能退出码仍为 0,但实际输出 0 帧;如果后端按 duration - 0.001 抽最后一帧,低帧率或短视频很容易踩到不可解码尾点。
  • 处理:角色动画抽帧按目标帧数预留一个采样步长,例如 32帧·4秒 最后一帧采 3.875s,不要采 3.999sffmpeg 返回成功但无输出文件时,错误 details 保留 targetSecondsstdoutstderr 和输出路径,用户主文案保持简短。
  • 验证:cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml,其中 editor_character_animation_extracts_final_sample_from_short_video 应覆盖本机 FFmpeg 8 的 0 帧回归。
  • 关联:server-rs/crates/api-server/src/character_animation_assets.rsdocs/【编辑器】画板角色形象生成入口设计-2026-06-15.md

Windows 本地角色动画抽帧找不到 ffmpeg 先查 dev 子进程环境

  • 现象:画板角色动画抽帧报 抽取动作视频帧失败:无法启动进程 ffmpegprogram 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 ffmpegwhere ffprobe 能找到本地安装;npm run test -- scripts/dev.test.ts -t "FFmpeg";重启 npm run dev:api-server 后访问 /healthz
  • 关联:scripts/dev.mjsserver-rs/crates/api-server/src/config.rsserver-rs/crates/api-server/src/character_animation_assets.rs

图片编辑器生成长请求完成态必须由后端写入画布

  • 现象:画板角色形象等生成请求已经在服务端返回 200OSS 中也已有 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 覆盖后端完成态写 layoutnpm 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.rssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tssrc/components/image-editor/useImageCanvasProjectPersistence.tsdocs/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_resourceeditor_asseteditor_canvas.layers_json,就把媒体本体塞进了项目快照;signed URL 还会过期,素材库也无法稳定换签。
  • 处理:登录态媒体必须先上传 OSS / asset object,持久化只写 imageSrc: "/<objectKey>"objectKeyassetObjectId;素材库和图层缩略图都通过 PlatformMediaFrame -> ResolvedAssetImageobjectKey 并调用 /api/assets/read-url。layout 序列化和后端保存要递归拒绝 data:* / blob:;旧行有 objectKey 时读出归一成 /<objectKey>,没有 objectKey 的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,也不能在后端快照回来前自动保存。生成扣费、失败退款或 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.rssrc/components/image-editor/ImageCanvasEditorModel.tssrc/components/image-editor/useImageCanvasProjectPersistence.tssrc/components/common/PlatformMediaFrame.tsxsrc/services/assetReadUrlService.ts

图片画布裁扩后刷新或去背景丢图先查项目资源化

  • 现象:从规范图裁切 / 裁扩出新图层后立即执行去除背景,去背景完成时原裁扩图从画布消失;刷新后裁扩图仍不在,但去背景占位可能变成结果图。
  • 原因:裁扩结果由浏览器 canvas 本地渲染为 data:image/png。如果项目态先把 local-resource-* 图层加入画布,serializeLayer 不会保存 srcresolveProjectResourceCreateImageSrc 又会跳过内联 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.tssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tsdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片画布项目封面上传失败要有本地展示兜底

  • 现象:画布项目已反复打开、保存或操作,但 /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 正式持久化。
  • 验证:npm run test -- src/components/project/ProjectCanvasCover.test.ts src/components/project/ProjectGalleryView.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx 覆盖服务端封面优先、本地缓存兜底、上传失败仍保留本地封面缓存;浏览器 smoke 可在 /project 对没有服务端封面的项目写入 genarrative-editor-project-covers IndexedDB 记录,刷新后应显示 blob: 封面图。
  • 关联:src/services/image-editor/editorProjectCoverCache.tssrc/components/project/ProjectGalleryView.tsxsrc/components/project/ProjectCanvasCover.tsxsrc/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 应断言私有框选预览带 objectKeyrefreshKey,且裸 generated 路径没有 fallback。
  • 关联:src/components/image-editor/ImageCanvasUiAssetExtractionOverlayView.tsxsrc/components/ResolvedAssetImage.tsxsrc/hooks/useResolvedAssetReadUrl.tssrc/services/assetReadUrlService.ts

图片画布发布入口 429 先查自动保存 PATCH 并发

  • 现象:发布域名访问画板时出现短时间密集 429Nginx access log 中 PATCH /api/editor/projects/<projectId>、生成接口和资料接口混杂,429 行常见 request_time=0.000upstream_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_conn zone,再查前端保存路径。useImageCanvasProjectPersistence 中自动保存和资源创建后的布局保存必须共用串行队列:同一时刻只允许一个 saveEditorProjectLayout in-flight,期间新快照覆盖旧待保存快照,当前保存结束后只发送最新一次。
  • 验证:npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx -t "serializes project layout saves" --reporter verbose 应覆盖慢保存期间不启动第二个 PATCH,首个保存完成后只发送最新待保存快照;排查发布现场时 429 行应从 upstream_status=- / Nginx limit_conn 收敛。
  • 关联:src/components/image-editor/useImageCanvasProjectPersistence.tssrc/components/image-editor/useImageCanvasProjectPersistence.test.tsxdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片画布发布入口 429 也要查 read-url 换签爆发

  • 现象:发布域名刚上线或刷新画板后出现短时间 429Nginx 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.000upstream_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_rps burst。
  • 处理:不要先放大 Nginx 通用 API 限流;先按 access log 聚合 read-url 数量、状态和 referrer,确认是否同一画板页面触发。assetReadUrlService 必须统一承接私有 generated 资源换签,并在真实请求前做跨组件轻量节流;画板、素材库、运行态和结果页不得直接绕过该服务调用 /api/assets/read-url
  • 验证:npm run test -- src/services/assetReadUrlService.test.ts --reporter verbose 应覆盖大量不同 objectKey 同时换签时首批限量放行、后续按间隔派发;发布现场同类页面刷新时,Nginx GET /api/assets/read-url 429 应从 upstream_status=- / genarrative_api_rps 收敛。
  • 关联:src/services/assetReadUrlService.tssrc/hooks/useResolvedAssetReadUrl.tssrc/components/ResolvedAssetImage.tsxdocs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md

图片编辑器 Seedance 2.0 参考媒体不要提交视频 Data URL

  • 现象:画板生成视频选择 Seedance 2.0 并上传参考视频后,请求体暴涨、可能返回 413 或上游拒绝 video_url.url;文档示例或测试如果写 data:video/mp4;base64,...,后续实现很容易照抄。
  • 原因:火山 Seedance 2.0 参考视频只支持公网 URL 或 asset:// 素材 ID,项目内画板资源应以 objectKey 由后端换签;视频不支持 Base64 / data:video,且 50MB 视频转 Base64 后会逼近或超过 64MB 请求体上限。参考音频虽然支持 Base64,但也不能单独输入,且大文件同样不应塞进 JSON。
  • 处理:参考视频 / 音频上传先走 /api/assets/direct-upload-tickets 直传 OSS,再 /api/assets/objects/confirm 确认;前端保留 signed URL 做预览,提交生成时优先使用 objectKey。后端归一化必须拒绝 data:video/*,非 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 verbosecargo test -p api-server editor_video --manifest-path server-rs/Cargo.tomlcargo test -p shared-contracts editor_video_request_supports_seedance_multimodal_references --manifest-path server-rs/Cargo.toml
  • 关联:src/services/image-editor/editorReferenceUploadClient.tssrc/components/image-editor/useImageCanvasUploadWorkflow.tssrc/components/image-editor/ImageCanvasGenerationSubmissionModel.tsserver-rs/crates/api-server/src/character_animation_assets.rsdocs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md

图片编辑器生成类菜单要挂到页面级 portal

  • 现象:底部 生成规范 菜单、角色面板里的 角色规范 来源菜单点击后像没有弹出来,实际被按钮所在的局部滚动容器挡住了。
  • 原因:菜单仍然渲染在底部工具栏或参考图横向滚动行内部,父容器带 overflow,弹层无法越出边界;即便挂到 portal,如果菜单根节点的 pointerdown 继续冒泡到画布视口,也会先触发画布失焦并卸载面板,导致菜单项 click 前消失。
  • 处理:这类轻量菜单统一用页面级 fixed portal 挂到 document.body,位置根据触发按钮的 getBoundingClientRect() 计算;PlatformFloatingMenu 根节点必须阻止 pointerdown 冒泡,避免画布清空当前生成面板;底部 AI 工具栏在生成面板打开时仍保持可见,不要整栏隐藏。
  • 验证:测试断言菜单不包含在底部工具栏 / 参考图行里,并且生成面板打开时底部 AI画布工具栏 仍存在;规范参考图来源菜单应能通过 portal 点击“从画布中选择 / 上传图片”并写回规范参考图。
  • 关联:src/components/common/PlatformFloatingMenu.tsxsrc/components/image-editor/ImageCanvasEditorView.tsxsrc/components/image-editor/ImageCanvasEditorGenerationIntegration.test.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.tsxsrc/index.cssdocs/【编辑器】生成类面板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.tsxsrc/components/image-editor/ImageCanvasEditorView.test.tsxdocs/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.tssrc/components/image-editor/useImageCanvasGenerationWorkflow.tsdocs/【编辑器】生成类面板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.tssrc/components/image-editor/useImageCanvasGenerationSurface.tsxdocs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md

图片画布重绘创建独立占位,快速编辑不要新建生成器

  • 现象:用户点击图片素材的“快速编辑”后,画布上额外出现 Quick Edit Generator 占位,像是新建了一个生成器;但用户预期是在原图下方框选区域、填写一个提示词和模型,然后直接修改当前图。
  • 原因:快速编辑入口和提交链路误用了 createQuickEditGenerationDialogDraft(...) / CanvasGenerationDialogState,把“覆盖源图”的快速编辑伪装成会产出新图层的生成器占位。
  • 处理:图片快速编辑必须走 QuickEditPanelState,打开时归档当前 active generation dialog 但不创建新的 mode="quick-edit" dialog;提交时调用 /api/editor/images/edits,把当前图片或带编号标注的图片作为 sourceImageSrc,成功后覆盖源图,失败时保留快速编辑面板。图片重绘、去背景、视频快速编辑等会产出新图层或异步占位的入口仍可走 generation dialog / placement 链路。
  • 验证:npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx -- --runInBand,以及按需运行 npm run test -- src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx -t "快速编辑|quick edit" -- --runInBand
  • 关联:src/components/image-editor/useImageCanvasGenerationWorkflow.tssrc/components/image-editor/ImageCanvasGenerationSubmissionModel.tssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tssrc/services/image-editor/editorImageReference.ts

图片画布快速编辑完成必须按目标图层回写

  • 现象:图片快速编辑任务成功后,刷新页面素材库能看到新图,但画布上的源图没有替换。
  • 原因:/api/editor/images/edits 只保存生成图、项目资源和素材;没有 canvasCompletion 时不会写 editor_canvas.layers_jsonsourceResourceId 只能表示溯源,同一资源可出现在多个图层,不能用它来决定替换哪一层。
  • 处理:图片快速编辑请求必须传 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_pointseditor_image_edit_can_complete_by_replacing_target_layer
  • 关联:src/services/image-editor/editorProjectClient.tssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tsserver-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_resourceeditor_asset 或画布 Resolution。前端 inline 回填也保留源图显示尺寸和 Resolution,避免旧回包再次放大图层。
  • 验证:npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx 覆盖 1537x1025 精确提交和源图回填;cargo test -p api-server editor_image_edit_aligns_provider_images_and_restores_source_dimensions --manifest-path server-rs/Cargo.toml 覆盖 provider 1552x1040、额外参考图独立对齐和回图恢复。
  • 关联:src/components/image-editor/ImageCanvasGenerationSubmissionModel.tssrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.tssrc/components/image-editor/ImageCanvasGenerationLayerModel.tsserver-rs/crates/api-server/src/editor_project.rs

图片画布快速编辑元数据必须记录原图引用

  • 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
  • 原因:信息面板直接渲染 generationInputs.references;快速编辑虽然把原图作为 sourceImageSrc 传给 provider,但如果 buildQuickEditGenerationInputs(...) 不把源图写成引用,后端资源和画布层都没有可展示的原图引用。
  • 处理:快速编辑的 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.tssrc/components/image-editor/ImageCanvasMetadataModalView.tsxsrc/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts

图片画布生成完成应用项目快照后也要刷新素材库

  • 现象:部分素材生成成功后画布上已经出现结果,但左侧素材库没有立刻出现新素材,刷新页面后才显示。
  • 原因:生成接口带 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" -- --runInBandnpm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx -- --runInBand
  • 关联:src/components/image-editor/useImageCanvasAssetLibrary.tssrc/components/image-editor/ImageCanvasEditorView.tsxsrc/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 设置为 rustcCargo 会按 wrapper 协议调用 rustc <真实rustc路径> - ...,真实 rustc 把 wrapper 传入的 rustc 路径和 stdin - 都当输入文件。
  • 处理:Windows 本地 dev 脚本应把 RUSTC_WRAPPERCARGO_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.mjsscripts/dev.test.tsdocs/【开发运维】本地开发验证与生产运维-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.mjspingora-direct-rehearsal-status.mjscheck-pingora-direct-preflight.mjscheck-pingora-direct-live.mjsdeploy/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-server artifact,发布包包含 Pingora 时还必须登记 pingora-gateway artifact,否则 deploy 应在切换 current 前失败;deploy 必须要求 release root、current link 和 api env file 都是绝对路径,release version 以数字或字母开头并拒绝点目录,再先写 staging release,全部复制完成后用非合并语义提升为正式 release,失败时清理 staging 且不留下正式 release,同版本 release 已存在、提升前竞态出现或 current 路径不是符号链接时拒绝覆盖 / 合并,避免旧文件混入 current;发布包包含 Pingora 时,deploy 必须先确认 systemd 最终配置没有 direct-entry CAP_NET_BIND_SERVICE、env 仍是 127.0.0.1:18081 shadow 且未配置 TLS_LISTEN / HTTP_REDIRECT_LISTEN,再提升 release、切换 current 并 restart Pingora 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 则 startNginx,并复核 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.serviceUser= 服务用户都能读取证书链 / 私钥、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-tokenJSON 输出会隐藏 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 release pingora-gateway、用 systemctl is-active 确认服务 active,再以 JSON 模式执行 direct live smoke,验证 HTTPS / HTTP redirect / ACME / WSS 101 和 Pingora access log request_id 落盘,并要求 direct-access-log 结构化结果 matchedCount == checkedmissingCount=0mismatchCount=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 release pingora-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/livearchive 来让 Pingora 读取证书;Certbot live 路径通常是 symlink,即使 stat -L 看起来是普通文件,父目录权限也会让非 root genarrative 用户不可达。先用随包 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.pemprivkey.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 时也不要传 ./nginxtools/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 可执行和 systemd ExecStart 指向;npm run check:pingora-cutover-status-snapshot / scripts/ops/pingora-cutover-status-snapshot.mjs 只负责输出 pre-cutoverpost-enablepost-rollback 三阶段只读 JSON evidence,并在直连 runbook 中通过 --require-pingora-gateway 把上述自审结果收录到 checks.current-release-audit.details;快照还必须确认 systemctl cat genarrative-pingora-gateway.serviceEnvironmentFile= 精确包含本次 --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 文件的 pathsizeBytessha256,便于归档后复核;证据目录生成、复制或归档后必须用随包 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.txtcommand.stderr.txtcommand-record.jsonsizeBytessha256;命令证据生成后也要立即把 stdout 中的 bundleDir 填入随包 verifier 的 <enable-apply-bundle-dir><rollback-apply-bundle-dir> 占位符做只读验真,不能只等最终根目录总审计才发现 command-record 或 stdout/stderr 归档漂移。启用后证据包必须额外运行随包 direct live smoke,并写入 direct-live.jsondirect-live.stdout.txtdirect-live.stderr.txtdirect-live-command.json 和 manifest summary 的 directLiveStatus,让 Pingora access log request_id 反查结果可复盘;direct-live.jsondirect-access-log 结果必须保留扫描行数、匹配数量、缺失明细以及 method/path/status 漂移明细,不要只保留 count 或依赖 stderr。证据包 manifest summary 还必须包含 directLiveAccessLog 摘要;如果 direct live JSON 缺少 direct-access-log 结构化结果,整包应记为 CRITICAL。若 snapshot 或 direct live stdout 解析失败,必须保留 snapshot-parse-error.txtdirect-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-msGENARRATIVE_HEALTH_PATROL_TIMEOUT_MSGENARRATIVE_HEALTH_PATROL_SLOW_MScanary / direct live smoke 的 --timeout-ms 及对应 envcanary 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: bytesIf-Range 不能被忽略:日期匹配才继续给局部内容,旧日期或弱 ETag 校验器应回完整 200,避免客户端拿旧校验器拼接错误文件片段。206304416 不应被 gzip 压缩,否则 Content-Range 指向的字节区间会和实际响应体不一致。多段 range 暂按完整文件处理,不要在切换窗口临时拼 multipart 响应。
  • 踩坑补充:直连 Pingora 后不要让静态路由接受非读取方法。POST /assets/app.jsPOST /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 中同一行的 pathstatusproxy_target=Local,至少覆盖 304405206416;否则直连切换证据包可能只能证明 API / WSS 代理路径,排查浏览器缓存或媒体 Range 问题时缺少本地响应状态证据。
  • 踩坑补充:直连 live smoke 不能只证明根 HTML 返回 200。正式发布包的首页通常会引用 /assets//admin/assets/ 构建产物,direct live 应自动发现静态资源,验证静态缓存 / 校验头、HEAD 头响应、If-None-Match / If-Modified-Since 304 和 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-controletaglast-modifiedaccept-rangescontent-rangecontent-lengthcontent-encoding;证据包自测要确认普通静态和 Vite 指纹资源的 Cache-ControlContent-Range 都被归档。API / WSS 检查不要落原始响应头,避免把认证、Cookie 或上游细节带进切换证据。
  • 踩坑补充:切流证据包不能只把静态头部藏在 direct-live.jsonmanifest.summary.directLiveStaticHeaders 必须提升普通静态和 Vite 指纹静态的缓存头、校验头、Range Content-Range 和 304 状态摘要,方便切换窗口先扫 manifest 判断证据是否完整;如果 direct live 已输出静态资产结果但摘要缺少缓存头、校验头、Range 206 + 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 directpost-rollback 证据包必须传 --expected-pingora-env-mode shadow,否则 health patrol 模式正确也不能证明 active Pingora env 姿态正确。若旧 post-enable bundle 虽然 manifest.summary.status=OK 但没有 directLiveAccessLogdirectLiveStaticHeaders,或旧 post-rollback bundle 没有 manifest.summary.pingoraEnvShadow、没有 mode=shadow / shadowReady=true、仍残留 tlsCertFile / tlsKeyFile,总审计也应失败;处理方式是用新版 current release 重新生成对应阶段证据包,不要把旧包混进正式归档。
  • 踩坑补充:最终证据根目录总审计失败时先看 JSON 顶层 summary,不要直接在长 phases[] / commands[] 里翻。summary.failedItems[] 会聚合失败阶段、命令、根目录或时间线诊断,summary.directLiveEvidence[] 会直接给出 post-enableaccessLog.ok/reasonstaticHeaders.ok/reason;reason 指向缺摘要或字段不完整时,应重新生成启用后证据包,而不是手工补 manifest。
  • 踩坑补充:最终证据根目录总审计命令示例也必须包含五条 --require-command-executable,分别绑定 current release 随包 pingora-direct-enable.shpingora-health-patrol-env-switch.mjspingora-gateway-env-shadow-switch.mjspingora-health-patrol-env-switch.mjspingora-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 口径代理头:HostX-Forwarded-Host、配置化 X-Forwarded-Proto、TCP 对端 IP 的 X-Real-IP,以及追加 TCP 对端 IP 的 X-Forwarded-ForTRUST_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 到生产 443 server 里写 /api/v1/assets location;那会覆盖当前 Nginx 正式路由。只能把 genarrative-pingora-realpath-canary.conf 作为独立 loopback server include 到 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 catsudo -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-cutoverenable-applypost-enablerollback-applypost-rollback 五个即时 verifier 步骤都必须带 --require-summary-ok,同时要求 manifest.schemaVersion=1manifest.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.statusOK、命令 manifest.summary.exitCode0、命令名不安全,或顶层 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.shpost-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjsrollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjsrollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjsrollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh 以及对应 --applypingora-directnginx 参数要求,复核 manifest.expectedExecutablemanifest.command.executable 与独立 command-record.json 的 executable,并要求 manifest.commandNamemanifest.command.name 只要存在就各自是安全非空命令名、两者同时存在时一致、manifest.command.args 与独立 command-record.json.args 都包含 --apply,且每个 args 字符串都不含换行或 NUL 字符。--require-command-executable 的 executable 段必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符;总审计 JSON 会记录 requiredCommandExecutables,便于复盘本次绑定的真实 current release 随包脚本。总审计还会要求 manifest.commandcommand-record.json 关键字段一致;其中两份命令记录的 schemaVersion 都必须是 1stdoutPath / stderrPath 必须同时与 manifest.files.stdout.path / manifest.files.stderr.path 对齐,不能把重新计算过 hash 的 command-record 指向另一份输出文件;args / command 也必须一致,不能只保证脚本路径正确却把 --apply 证据改成 --dry-run 或其它参数;命令记录时间必须满足 finishedAt >= startedAtdurationMs == finishedAt - startedAt,且 manifest.generatedAt 不能早于命令 finishedAt;旧证据缺少 schemaVersion、缺少 expectedExecutable、缺少必需 --apply 参数、人工同名证据 executable 漂移、manifest 与 command-record 语义漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染、命令时间线漂移或同一命令重复绑定不同脚本路径都必须失败。
  • 踩坑补充:命令证据里 expectedExecutable 字段不能用空字符串或相对路径表示“未知”。manifest.expectedExecutablemanifest.command.expectedExecutable 只要存在就必须是安全绝对路径;否则总审计应把 manifest 判坏,避免坏顶层字段被内嵌字段兜底,或坏内嵌字段被 command-record 里的路径掩盖。生成端 --expected-executable 也不能填 / 或带换行 / NUL 的路径,脚本会在执行真实命令前失败,不能用坏 expected path 先生成证据再交给总审计兜底。
  • 踩坑补充:不能只检查 manifest.command.executablecommand-record.json.executable 两边一致;如果命令证据已经声明 expectedExecutable,真实 executable 必须同时等于该预期路径,并且必须是绝对路径。否则人工修改两份命令记录为同一个错误脚本或相对路径,也可能伪造成一致证据。
  • 踩坑补充:命令证据的 command 字符串是给人读的,不是结构化身份事实。manifest.command.executablecommand-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-filecanary 对账脚本的 --nginx-log-file / --pingora-log-filedirect live 的 --pingora-access-logdirect 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 失败并列出重复目录,不能按目录名排序打平;应重新归档该阶段 / 命令证据,或把旧证据移出正式证据根目录后再审计。标准八段证据都被要求时,每段审计状态都必须是 OKmanifest.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=falsediagnostics 文本。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_SERVICECapabilityBoundingSet=CAP_NET_BIND_SERVICEEnvironmentFile=/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.servicepublic probe 走 127.0.0.1 时应带 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>。回退后 nginx -t 必须先通过,systemctl cat genarrative-pingora-gateway.service 不应再显示这两条 capabilitysystemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager 必须仍指向 current release 的 pingora-gatewaysystemctl is-active nginx.service 应为 activecurl --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-enablenpm run check:pingora-direct-rollbacknpm run check:production-health-patrolnpm run check:production-api-releasenpm run check:pingora-production-release-buildnpm 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.confdeploy/env/health-patrol.env.exampledeploy/env/pingora-direct-live.env.exampledeploy/env/pingora-canary-live.env.examplescripts/deploy/pingora-direct-enable.shscripts/deploy/pingora-direct-rollback.shscripts/deploy/pingora-tls-cert-sync.mjsscripts/check-pingora-direct-preflight.mjsscripts/check-pingora-direct-live.mjsscripts/ops/pingora-cutover-command-evidence.mjsscripts/ops/pingora-cutover-evidence-verify.mjsscripts/ops/pingora-cutover-evidence-audit.mjsscripts/jenkins-server-provision.shscripts/build-production-release.shscripts/deploy/production-api-deploy.shdocs/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。结算必须在 SpacetimeDB asset_operation_wallet_settlement 持久化:旧 consume 已存在时原子退款;尚不存在时写取消 intent。任何迟到 consume 在同一事务内看到 intent 后失败关闭。重复 ledger 必须核对用户、金额和来源,不能只按 ID 存在就返回成功。claim 处理 lease 已过期的 running job 时还必须先比较 attemptmax_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.rsserver-rs/crates/spacetime-module/src/runtime/profile.rsdocs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md

外部生成队列不再由 HTTP 进程兜底执行

  • 现象:拼图首关生成接口返回 queued,但生成页长时间不完成,重启 genarrative-api.service 也没有推进任务。
  • 原因:HTTP 角色只入队,不再直接调用外部 provider;如果没有运行 GENARRATIVE_PROCESS_ROLE=external-generation-workerall 的进程,external_generation_job 会停留在 pending/running,直到有 worker claim。
  • 处理:生产用 systemctl enable --now genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service 启动保底 worker 和 controllergenarrative-api.service 对 controller 使用 systemd Wants 弱依赖,启动 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_idlease_expires_at 会更新,完成后 session 进入 ready 或 failedinline 模式下不应产生新的 external_generation_job
  • 关联:deploy/systemd/genarrative-external-generation-worker@.servicedeploy/systemd/genarrative-external-generation-controller.servicedeploy/env/external-generation-controller.env.exampleserver-rs/crates/spacetime-module/src/external_generation.rsdocs/【开发运维】本地开发验证与生产运维-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-workerexternal-generation-controller 启动时只构建空 auth store 的 AppState,不调用 SpacetimeDB 认证投影导出;只有 api / all 这类 HTTP 角色需要在启动时恢复认证投影并在依赖不可用时重试或进入 503 降级。
  • 验证:重启 worker 后日志应先出现“非 HTTP 进程跳过 SpacetimeDB 认证投影恢复”,随后出现 external generation worker 已启动;同一时间窗口不应再因为认证投影恢复失败而阻止 job claim。HTTP api-server 的认证恢复日志和 503 降级语义保持不变。
  • 关联:server-rs/crates/api-server/src/main.rsserver-rs/crates/api-server/src/external_generation_worker.rsserver-rs/crates/api-server/src/external_generation_worker_controller.rsdocs/【开发运维】本地开发验证与生产运维-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,会自己消费队列;如果之前手动启动的同仓库、同 database GENARRATIVE_PROCESS_ROLE=external-generation-worker 进程没有退出,旧二进制会继续 claim 新 jobschema / 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.mjsscripts/dev.test.tsserver-rs/crates/api-server/src/external_generation_worker.rsdocs/【开发运维】本地开发验证与生产运维-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_draftsave_puzzle_generated_imagessave_puzzle_ui_backgroundmark_puzzle_draft_generation_failedmark_puzzle_level_generation_failed 已接入 external_generation_job lease guardapi-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.tomlcargo test -p api-server asset_operation_billing_does_not_refund_stale_worker_lease_errors --manifest-path server-rs/Cargo.tomlcargo check -p api-server --manifest-path server-rs/Cargo.toml
  • 关联:server-rs/crates/spacetime-module/src/external_generation.rsserver-rs/crates/spacetime-module/src/puzzle.rsserver-rs/crates/api-server/src/external_generation_worker.rsserver-rs/crates/api-server/src/asset_billing.rsdocs/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.tomlcargo 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.rsserver-rs/crates/api-server/src/puzzle/generation.rsserver-rs/crates/api-server/src/external_generation_worker.rsdocs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md

生产冷备份后 API 和外部生成 worker 不能只依赖 SpacetimeDB 自恢复

  • 现象:release 机器 03:20 冷备份后,spacetimedb.service 已恢复,但作品列表、创作入口配置或公开 gallery 继续超时 / 502 / 504genarrative-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.servicegenarrative-external-generation-worker@*.servicegenarrative-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-opsnpm 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 全为 activecurl -fsS http://127.0.0.1:3101/v1/ping/healthz/readyz 和代表性 /api/runtime/puzzle/gallery 均成功;npm run check:database-backup 覆盖空间不足不触碰 systemctl、tar 失败仍恢复依赖服务;get_external_generation_queue_stats_and_return 不应长期出现 claimable pending。
  • 关联:deploy/systemd/genarrative-database-backup.servicescripts/database-backup-to-oss.mjsscripts/ops/production-health-patrol.mjsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

Pingora Brotli 不能只看 Content-Encoding

  • 现象:在 pingora-gateway 中把 GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS 试验性改成 gzip,br 后,Accept-Encoding: br, gzip 的响应会带 Content-Encoding: br,但 Node brotliDecompressSync(...)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: gzipAccept-Encoding: br, gzip 都返回可解压的 gzipGENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=br 必须启动失败,GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=0 也必须启动失败。
  • 关联:server-rs/crates/pingora-gateway/src/main.rsscripts/check-pingora-gateway-smoke.mjsdocs/technical/【开发运维】Pingora独立网关试点-2026-06-11.mddeploy/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.mjsdeploy/pingora/pingora-gateway.env.exampledocs/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-plannpm run check:production-ops,确认 live 门禁计划包含真实 access log 对账。
  • 关联:scripts/check-pingora-release-readiness.mjsscripts/check-pingora-canary-access-log-parity.mjsdeploy/env/pingora-canary-live.env.exampledocs/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.d server 之前的全局 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.confdeploy/nginx/README.mddocs/technical/【开发运维】Pingora独立网关试点-2026-06-11.mdscripts/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.shjenkins/Jenkinsfile.production-api-buildjenkins/Jenkinsfile.production-api-deployscripts/deploy/production-api-deploy.sh 必须同时携带 scripts/check-pingora-release-readiness.mjsscripts/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-releasenpm run check:production-api-deploynpm run check:pingora-current-release-auditnpm run check:pingora-release-readiness-plannpm run check:production-ops,确认发布包、current release、runbook 与 guardrails 都覆盖聚合脚本和 check-pingora-canary-live.mjs,且 runbook readiness 参数包含 --release-runtime-only
  • 关联:scripts/check-pingora-release-readiness.mjsscripts/check-pingora-canary-live.mjsscripts/build-production-release.shscripts/deploy/production-api-deploy.shjenkins/Jenkinsfile.production-api-buildjenkins/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 内置阶段化健康检查和失败日志;/readyzGENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS 短窗口检查 SpacetimeDB 连接租约,业务失败日志包含 operation_kindoperation_namespacetime_stageelapsed_ms
  • 验证:/readyz 失败时看 details.spacetime.stage;业务请求超时时查 journalctl -u genarrative-api.service 中同一时间窗口的 SpacetimeDB client operation failed,优先按 pool_acquireconnect_buildconnect_handshakeread_model_subscribeprocedure_resultreducer_resultread_cache 分阶段处理。
  • 关联:server-rs/crates/spacetime-client/src/lib.rsserver-rs/crates/api-server/src/health.rsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

新建草稿扣费不能和入口卡泥点配置分离

  • 现象:后台修改创作入口的 mudPointCost 后,入口卡和前置余额提示可能显示新数值,但用户真实钱包流水仍按代码常量扣除。
  • 原因:早期约定把 creationTypes[].unifiedCreationSpec.mudPointCost 只当展示字段,拼图、抓大鹅和汪汪声浪初始生成各自保留了 210、三次单图 1 的硬编码扣费路径。
  • 处理:新建草稿初始生成成本必须统一从 GET /api/creation-entry/configunifiedCreationSpec.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.tscargo 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.tsxserver-rs/crates/api-server/src/creation_entry_config.rsserver-rs/crates/api-server/src/puzzle/handlers.rsserver-rs/crates/api-server/src/match3d/draft.rsserver-rs/crates/api-server/src/bark_battle.rsdocs/【后端架构】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-ossPostObject form 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-Control policy、form field、PutObject headers 和 V4 AdditionalHeaders;线上旧对象可用 curl -I 观察是否只有 ETag / Last-Modified 或已经补齐 Cache-Control
  • 关联:src/services/assetReadUrlService.tsserver-rs/crates/platform-oss/src/lib.rsserver-rs/crates/platform-oss/README.mddocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

小程序 H5 导航不能清掉宿主 query

  • 现象:微信小程序首次进入 H5 后,点击需要登录的入口没有返回小程序原生授权页,而是弹出 Web 端登录窗口;充值渠道也可能被误判为普通网页环境。
  • 原因:小程序 web-view 入口通过 clientType=mini_programclientRuntime=wechat_mini_programminiProgramEnv 标记宿主环境,但 H5 内部 pushAppHistoryPath(...) 阶段导航会默认清空 query;首点时微信 JS bridge 也可能尚未就绪,导致 isWechatMiniProgramWebViewRuntime() 和充值平台判断读不到小程序上下文。
  • 处理:路由层统一把 clientTypeclientRuntimeminiProgramEnv 当作 app runtime context,在普通路径归一、显式 query 路由和同一创作流跳转时都跨导航保留;小程序环境识别同时用 MicroMessenger + miniProgram User-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.tssrc/services/authService.tssrc/services/payment/paymentPlatform.tsdocs/【项目基线】当前产品与工程约束-2026-05-15.md

平台异步错误必须带来源弹窗,不要只显示裸错误

  • 现象:用户先后触发多个拼图或草稿生成时,旧请求失败后会在当前页面显示“图片生成失败”等裸错误,容易误判为当前正在看的拼图失败;错误文本也不便复制给开发排查。
  • 原因:不同入口、生成页、结果页、作品详情和运行态各自渲染局部错误,没有统一携带草稿、生成会话、作品或游玩来源。
  • 处理:跨流程错误统一由 PlatformEntryFlowShellImpl 汇总为 PlatformErrorDialog,来源使用玩法、草稿 / session / work / run 标识组成;弹窗提供复制按钮。关闭弹窗时只清理可安全清理的错误状态;恢复类错误用 dismiss key 防止反复弹出但不擅自改底层状态。
  • 验证:触发任一平台级异步失败时,页面应出现包含“错误来源”和“错误内容”的弹窗;复制内容应包含来源和错误正文;旧页面内错误 banner 不再重复出现。
  • 关联:src/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/platform-entry/PlatformErrorDialog.tsxdocs/【玩法创作】平台入口与玩法链路-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.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.mddocs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md

拼图公开推荐不要只按 Published 判断

  • 现象:后台把拼图作品隐藏后,作品不在公开列表里显示,但玩家通关其它拼图后的推荐下一作品仍可能出现这条隐藏作品。
  • 原因:拼图隐藏只把 puzzle_work_profile.visible 置为 false,不会把 publication_statusPublished 改走;通关推荐候选曾只通过 by_puzzle_work_publication_status().filter(Published) 取数,漏掉可见性判断。
  • 处理:拼图公开消费路径统一使用 Published + visible=true,范围包括 puzzle_gallery_viewpuzzle_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.rsdocs/【后端架构】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.tsxsrc/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/72text-white/88border-white/14bg-white/12 等不稳透明度档位,进一步放大了问题。
  • 处理:给暗色 hero 加组件专属 class,例如 creation-agent-hero__progress-labelcreation-agent-hero__progress-valuecreation-agent-hero__progress-hint,并在 src/index.css 的 remap 规则之后用更具体选择器和 !important 固定白色透明度、边框和进度条底色。
  • 验证:CreationAgentWorkspace 测试应断言进度标题、百分比和提示文本带专属 class;src/index.test.ts 应断言这些 class 在 remap surface 内有白色覆盖规则;移动端截图中暗色卡片文字应保持可读。
  • 关联:src/components/creation-agent/CreationAgentWorkspace.tsxsrc/components/creation-agent/CreationAgentWorkspace.test.tsxsrc/index.csssrc/index.test.tsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

VectorEngine 图片生成 request_send 传输错误要按可重试网络抖动排查

  • 现象:external_api_call_failure 里看到 failureStage=request_sendstatusCode=nullerrorSource 可能是 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_statusstatusCode=502、错误体是 Nginx HTML 502 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_idmetadata_json.userId/profileId/requestId 定位触发者、草稿 / 作品和同一次 HTTP 请求;request_send + timeout/connect=trueupstream_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 顺序执行,每个资产会输出 slotasset_kindelapsed_ms;排查拼图草稿失败时优先看同一 request_id 下最后一个失败 slot。
  • 验证:cargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_send_retry_policy -- --nocapturecargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_image_edit_retries_send_timeout_once_and_succeedscargo 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.rsserver-rs/crates/api-server/src/external_api_audit.rsserver-rs/crates/api-server/src/openai_image_generation.rsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

跳一跳 Three.js 地块 UV 顶面要映射到 Z 轴

  • 现象:跳一跳地块使用六面 UV 贴图后,看起来像贴图位置贴歪,顶面显示侧面纹理,或者旧单张地块图被拉到立方体多个面上。
  • 原因:运行态以 z 作为立方体竖直高度和相机下压方向,但 Three.js BoxGeometry / 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.tsxserver-rs/crates/api-server/src/jump_hop.rsdocs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.mddocs/【玩法创作】平台入口与玩法链路-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,默认每日登录任务读取时会把结算字段自愈到 canonical daily_login
  • 验证:npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx 应覆盖卡片从后端任务摘要显示 1 / 1、领取后显示已完成,以及北京时间 0 点自动 refresh 后重拉任务中心。
  • 关联:src/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/rpg-entry/RpgEntryHomeView.recharge.test.tsxdocs/【项目基线】当前产品与工程约束-2026-05-15.md

“我的”页不要恢复旧的填邀请码次级按钮

  • 现象:移动端“我的”页在五项常用功能和设置入口下方又出现一个“填邀请码”按钮,看起来像旧入口残留。
  • 原因:邀请码流程迁移后仍按新用户窗口保留 canShowReferralRedeemShortcut 次级入口;但当前页面口径已经固定为五项常用功能宫格,邀请码填写应由邀请链接 query 或明确引导打开弹窗。
  • 处理:移除常驻 次级入口 / 填邀请码 渲染,不删除 ProfileReferralModalredeem 面板,也不破坏 ?inviteCode= / ?invite_code= 自动打开填写弹窗。
  • 验证:新用户账号打开“我的”页时没有 次级入口填邀请码 按钮;带 ?inviteCode=spring-2026 的登录用户仍自动打开邀请码弹窗并预填 SPRING2026
  • 关联:src/components/rpg-entry/RpgEntryHomeView.tsx.hermes/skills/genarrative-profile-invite-flow/SKILL.md

创作卡片点击要直达已有入口表单,别再保留空白入口页

  • 现象:创作 Tab 模板卡点击后如果仍然停留在创作大厅,或者先进入“X 创作入口”这种空白页,就会让用户多走一层,还可能被错误的 stage 白名单拉回平台。
  • 原因:/creation/<play> 一度被接成空白创作入口页,导致 SelectionStageappPageRoutes 和卡片点击分流被旧占位 stage 污染。
  • 处理:把 /creation/<play> 重新指向已有入口表单 stage,例如 agent-workspacebig-fish-agent-workspacematch3d-agent-workspacesquare-hole-agent-workspacejump-hop-workspacewooden-fish-workspacepuzzle-agent-workspacebark-battle-workspacevisual-novel-agent-workspacebaby-object-match-workspace;平台壳层和测试同步清理空白入口页相关 helper。
  • 验证:点拼图 / 抓大鹅 / 汪汪声浪卡片后,应看到各自既有工作台内容,例如测试中的 拼图工作区:missing-session抓大鹅工作区:missing-session汪汪声浪配置表单,并且不再出现“X 创作入口”空白页。
  • 关联:src/components/platform-entry/platformEntryTypes.tssrc/routing/appPageRoutes.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx

创作流程刷新恢复必须写私有 query

  • 现象:创作生成页或结果页刷新后回到空白工作区、平台首页,或者从作品详情返回时错误复用了别的玩法草稿。
  • 原因:部分创作流程只把 sessionId / profileId / draftId / workId 放在前端内存里,没有写进 URL;也曾把写 URL 放在 stage 切换前,writeCreationUrlState 因为还停在非创作路径而直接跳过。若跨玩法或公开详情继续保留私有 query,还会污染 /works/detail?work=...
  • 处理:创作页只使用私有 query sessionIdprofileIddraftIdworkId 做刷新恢复,不复用公开 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.tssrc/routing/appPageRoutes.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【玩法创作】平台入口与玩法链路-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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/【玩法创作】平台入口与玩法链路-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,于是请求被放大成密集循环。
  • 处理:拼图轮询只绑定 selectionStageactivePuzzleGenerationSessionId 和“是否仍在生成中”这个布尔条件;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.tsxsrc/components/platform-entry/usePlatformCreationAgentFlowController.tssrc/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.tsxsrc/components/unified-creation/UnifiedGenerationPage.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

拼图试玩恢复 query 必须先切到运行态路径再写

  • 现象:拼图试玩或正式运行态打开后,刷新会停在“正在进入拼图关卡”,或地址栏只有 runtimeProfileId,缺少草稿 runtimeSessionId
  • 原因:writePuzzleRuntimeUrlState 只会在当前路径已经是 /runtime/puzzle 时写入;如果先触发阶段切换再写 query,或者草稿作品摘要缺少 sourceSessionId,就会把恢复参数写丢。App.tsx 的 stage 同步也会改 pathname,所以顺序不对时容易只留下部分 query。
  • 处理:进入拼图 runtime 时先 pushAppHistoryPath('/runtime/puzzle'),再 setSelectionStage('puzzle-runtime'),最后写 runtimeProfileIdruntimeSessionIdruntimeLevelIdworkmode;草稿 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 同时包含 runtimeProfileIdruntimeSessionId
  • 关联:src/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/services/puzzleRuntimeUrlState.tssrc/routing/appPageRoutes.tsdocs/【玩法创作】平台入口与玩法链路-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.tssrc/services/puzzle-clear/puzzleClearLocalRuntime.test.tssrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsxsrc/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.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxserver-rs/crates/module-puzzle-clear/src/application.rsdocs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md

拼消消完整消除反馈不要让补牌抢帧

  • 现象:玩家正确拼完整组后,卡片几乎瞬间消失,顶部补牌马上出现或下落,导致“拼对了”的确认反馈很弱。
  • 原因:前端一收到新 snapshot 就同时播放消除和掉落叠层,旧消除动画时长较短;新补入卡牌的下落延迟接近 0ms,视觉上会抢在消除反馈之前开始。
  • 处理:局部正确拼合但未消除时只给锁定组做一次高光;完整消除时让旧卡片在消除叠层中短暂放大展示再淡出;新补入卡牌的下落延迟到淡出尾段,并继续只隐藏新补入目标格,不隐藏已有场上卡片下沉后的最终格。
  • 验证:npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx,浏览器里确认局部拼合会闪、完整消除会放大淡出、补牌在淡出后段才开始掉落。
  • 关联:src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsxsrc/index.csssrc/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.tsxsrc/components/platform-entry/platformEntryResponsive.ts

泥点不足提示不要把用户退回创作入口

  • 现象:拼图 / 抓大鹅 / 汪汪声浪等创作表单点击生成时,如果泥点不足,页面直接回到创作 Tab 玩法模板列表,刚填的表单内容随工作台卸载全部丢失。
  • 原因:PlatformEntryFlowShellImpl.tsxensureEnoughDraftGenerationPointsFromServer(...) 曾在余额不足或余额读取失败时调用 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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

内嵌泥点确认弹窗必须自带平台主题作用域

  • 现象:拼图 / 抓大鹅统一创作页点击生成后,“确认消耗泥点”弹窗正文和按钮存在,但弹窗面板背景透明,只剩遮罩和文字。
  • 原因:PlatformMudPointConfirmDialog 作为二级确认常以 portal={false} 内嵌到工作台局部 DOM,局部节点不一定继承 .platform-themeplatform-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.tsxsrc/components/common/PlatformStatusDialog.tsxsrc/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsxsrc/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.tsxsrc/components/common/CreativeImageInputPanel.tsxsrc/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.tsxsrc/components/common/CreativeImageInputPanel.tsx

玩法入口分类字段缺失要前端兜底

  • 现象:平台创作入口初始化时,platformEntryCreationTypes.ts 直接对 creationTypes[].categoryId / categoryLabeltrim(),一旦后端旧数据、局部 mock 或异常返回里缺字段,整个创作页会在 derivePlatformCreationTypes(...) 里直接炸掉。
  • 处理:normalizeCategoryId(...)normalizeCategoryLabel(...) 必须接收可空值,并分别回退到 recommended / 热门推荐;历史 recent / 最近创作 也要归一到推荐分类。最近创作 不属于模板分类页签,只能由真实草稿 / 作品架后端数据决定是否展示。
  • 验证:npm test -- src/components/platform-entry/platformEntryCreationTypes.test.ts,再打开本地创作页确认能正常进入创作 Tab。
  • 关联:src/components/platform-entry/platformEntryCreationTypes.tssrc/components/platform-entry/platformEntryCreationTypes.test.tsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

创作入口公告不要恢复前端固定两卡

  • 现象:点击底部加号进入的创作入口页只展示固定的拼图 / 抓大鹅主题卡,后台改公告表单后前台没有变化。
  • 原因:前端重新硬编码 banner 列表,绕过了 GET /api/creation-entry/configeventBanners 配置。
  • 处理:创作入口页公告位优先读取后端 eventBanners 数组,多条自动轮播;旧 eventBanner 只做单条兼容兜底。后台主格式是标题与 HTML 内容表单,保存时序列化为后端 eventBannersJson 传输字段,只允许受控 HTML 片段经空权限 iframe 展示,不执行 JSX 或直接 DOM 注入。
  • 验证:后台保存两条以上公告后,点击底部加号进入创作入口页应自动轮播这些后台配置项;CustomWorldCreationHub 相关测试应断言标题来自后端配置。
  • 关联:src/components/custom-world-home/CustomWorldCreationStartCard.tsxserver-rs/crates/module-runtime/src/application.rsapps/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-runtimeevent_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-serverGET /api/creation-entry/configeventBanners[0] 不再指向缺失的 /branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png
  • 关联:server-rs/crates/module-runtime/src/application.rsserver-rs/crates/module-runtime/src/domain.rsdocs/【玩法创作】平台入口与玩法链路-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.csssrc/index.test.tsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

草稿页未读点不要继续用红色 literal

  • 现象:草稿页底部 Tab 和作品架的未读点视觉上仍像红点,或 glow 仍带红色阴影,和平台暖棕体系不一致。
  • 原因:platform-nav-unread-dotcreation-work-card__unread-dot 直接写了 #b64a35rgba(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.csssrc/index.test.tsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

创作 Tab 模板卡不要复用暗图蒙版参考卡样式

  • 现象:创作 Tab 两列玩法卡上图能看到,但标题、描述或预计消耗泥点在白底信息区里看不见,或只剩泥点小图标。
  • 原因:旧 platform-creation-reference-card 是给暗图蒙版卡用的全局样式,会把卡片及全部子元素强制成白色文字;参考图要求的是“上图 + 下方白底信息区”,继续复用旧类会让白底上的文字消失。
  • 处理:创作 Tab 首屏模板卡使用独立 creation-template-cardcreation-template-card__bodycreation-template-card__titlecreation-template-card__subtitlecreation-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.tsxsrc/index.csssrc/components/custom-world-home/CustomWorldCreationHub.test.tsx

创作首屏开放态卡片不要再显示左上状态标签

  • 现象:创作 Tab 的开放态玩法卡左上角会重复显示“可创建”或“可创作”,视觉上比其它状态更吵,还会和封面图抢注意力。
  • 原因:卡片渲染层默认把 badge 当成所有状态都要展示的左上角标签,没有区分开放态与非开放态。
  • 处理:开放态卡片不渲染左上标签,仅保留标题、描述和右下角消耗信息;敬请期待即将开放 等非开放态标签继续保留。
  • 验证:创作首屏 HTML 中不应包含 可创建 / 可创作,但仍应包含 即将开放 等非开放态状态。
  • 关联:src/components/custom-world-home/CustomWorldCreationStartCard.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

发现 / 创作 / 草稿页不要把根内容区再包成全局卡片壳

  • 现象:发现页、创作页或草稿页根区一旦套回 platform-page-stage,页面边缘会立刻变得更厚,频道标签、列表和模板卡的横向空间都被挤窄,看起来像回到了旧全局卡片壳。
  • 原因:platform-page-stage 本身是全局内容卡片壳,适合推荐页、我的页和其它页面,但这三页已经有自己的视觉结构;草稿页顶部筛选若继续用旧 platform-tab,还会和发现页频道标签不一致。
  • 处理:这三页的根内容区只保留 platform-remap-surface,不要再加 platform-page-stage;草稿页顶部筛选复用发现页的 platform-mobile-home-channelplatform-mobile-home-channel--active
  • 验证:浏览器里这三页的根区应仍保留 platform-remap-surface,但不再出现 platform-page-stage;草稿页顶部筛选样式应和发现页频道标签一致。
  • 关联:src/components/custom-world-home/CustomWorldCreationHub.tsxsrc/components/custom-world-home/CustomWorldWorkTabs.tsxsrc/components/rpg-entry/RpgEntryHomeView.tsxsrc/index.css

统一创作壳现在自己负责页面滚动和四条入口外壳

  • 现象:统一创作页最初只包住拼图、抓大鹅和敲木鱼的工作台内容,跳一跳仍然保留独立工作台壳,页面级滚动职责也散落在平台入口 motion wrapper 里,导致移动端不同入口的可见外壳不一致。
  • 原因:UnifiedCreationPage 只做了标题和隐藏契约,入口壳还在各自工作台里保留 platform-remap-surface / overflow-y-autojump-hop 也没进入统一 spec。
  • 处理:把 jump-hop 纳入 unifiedCreationSpec,让 UnifiedCreationPage 自己承担页面级滚动与统一标题栏;JumpHopCreationWorkspaceWoodenFishCreationWorkspaceunifiedChrome / 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.tsxsrc/components/unified-creation/unifiedCreationSpecs.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsx

统一创作编排层不要再让平台壳直挂旧工作台

  • 现象:平台入口壳已经切到统一创作外壳,但源码里仍直接 lazy import 并渲染四个旧工作台分支,看起来还是四套入口编排。
  • 原因:统一创作页只收口了可见外壳,入口层没有再抽一层统一创作编排组件,导致平台壳依旧要认识各玩法旧工作台。
  • 处理:新增 UnifiedCreationWorkspace,由它内部按 playId 选择真实工作台;平台壳只依赖这一层,不再直接挂旧工作台分支。旧工作台已迁入 src/components/unified-creation/workspaces/,不再是入口编排事实源。
  • 验证:PlatformEntryFlowShellImpl.tsx 中不应再出现四个旧工作台的入口渲染分支,创作 Tab 与 /creation/<play> 仍能正常进入对应工作台。
  • 关联:src/components/unified-creation/UnifiedCreationWorkspace.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

Jenkinsfile 开头不能带 UTF-8 BOM

  • 现象:Genarrative-Stdb-Module-PublishPipeline 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 BFJenkins/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,并用 Jenkins validateDeclarativePipeline 或重放 Genarrative-Stdb-Module-Publish,不应再停在 No such DSL method 'pipeline'
  • 关联:jenkins/Jenkinsfile.production-stdb-module-publishdocs/【开发运维】本地开发验证与生产运维-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.jsonnode 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.mjsscripts/dev.mjsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

SpacetimeDB 入口迁移 helper 合并时不要只保留调用

  • 现象:cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml 或 Jenkins Genarrative-Stdb-Module-BuildE0425 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.rsdocs/【开发运维】本地开发验证与生产运维-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.tsxserver-rs/crates/api-server/src/match3d/mappers.rsdocs/【玩法创作】平台入口与玩法链路-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.rsprompt.rssheet.rsalpha.rspersist.rserror.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.rsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

图片画布 UI 提取素材切片不要把断开的高光阴影当独立图标

  • 现象:图片画布提取 UI 素材后,右侧素材库出现很小的废图;主体图标的阴影、反光、高光或小装饰不完整。
  • 原因:图标 spritesheet 切片按 alpha 连通域识别素材,模型常把软阴影、高光、小星星等画成与主体断开的透明块;如果直接逐连通域出图,小碎片会抢占图标顺序,主体也会缺边缘装饰。
  • 处理:在 platform-imagesheet.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.rsserver-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.tomlcargo test -p api-server puzzle_level_scene_spritesheet_and_background_requests_use_references --manifest-path server-rs\Cargo.tomlcargo 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.rsserver-rs/crates/api-server/src/match3d/works.rsserver-rs/crates/api-server/src/generated_asset_sheets.rsdocs/【玩法创作】平台入口与玩法链路-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.rsdocs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.mddocs/【玩法创作】平台入口与玩法链路-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.rsdocs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md.

敲木鱼历史已发布作品缺返回按钮要补齐,不要靠推荐过滤

  • 现象:推荐页或公开列表中的历史敲木鱼作品点击运行态时报 敲木鱼运行态需要完整作品配置,但这类作品的敲击物、背景、音效和飘字都已完整,只是 backButtonAsset 为空。
  • 原因:早期已发布作品缺少统一的默认返回按钮快照;运行态启动时如果仍直接按完整配置校验,就会把可玩的历史作品拒掉。这个问题不应通过推荐流或公开列表过滤解决。
  • 处理:spacetime-modulestart_wooden_fish_run_tx 和 work snapshot 构建时,若作品已发布且 generationStatus=ready,但仅缺 backButtonAsset,就补写内置默认返回按钮 /UI/11_left_arrow.png,再继续进入运行态。默认返回按钮以 bundled-default 资产快照写回 work profile,字段保持 assetId=wooden-fish-default-back-buttonimageObjectKey=public/UI/11_left_arrow.png
  • 验证:历史木鱼作品点击运行态不再报完整作品配置缺失;第一次进入后,work profile 里应补出 backButtonAsset
  • 关联:server-rs/crates/spacetime-module/src/wooden_fish.rsdocs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

敲木鱼创作生成不要沿用 15 秒会话超时

  • 现象:敲木鱼工作台点击“生成”后,前端直接提示 请求超时:15000ms,但后端和 VectorEngine 未必已经失败。
  • 原因:createCreationAgentClientcreateSessionTimeoutMs 默认是 15 秒;敲木鱼创作链路会继续进入生成页并执行多次 image2 edits、去绿背景处理和 OSS 写入,单次请求窗口如果继承共享默认值,会早于业务生成完成被前端中断。
  • 处理:敲木鱼 client 必须单独配置长等待窗口,同时覆盖 createSessionTimeoutMsexecuteActionTimeoutMs;不要修改共享默认值影响其它轻量创作 Agent。
  • 验证:npm run test -- src/services/wooden-fish/woodenFishClient.test.ts,并在本地触发一次木鱼创作确认不再出现 15 秒前端超时。
  • 关联:src/services/wooden-fish/woodenFishClient.tssrc/services/creation-agent/creationAgentClientFactory.tsdocs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md

敲木鱼创作“卡住”先查 2xx 慢请求

  • 现象:敲木鱼工作台点击生成后长时间停留在生成页,看起来像卡住;api-server 日志可能出现 /api/creation/wooden-fish/sessions/{sessionId}/actions2xx 慢请求,耗时可达数分钟,例如 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.tsdocs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

敲木鱼点击生成出现 SpacetimeDB procedure 超时先查版本错配

  • 现象:敲木鱼创作时点击“生成”,前端提示 SpacetimeDB procedure 调用超时,但服务端日志更早出现 Failed to BSATN deserialize procedure return value 或类似反序列化错误。
  • 原因:本机 spacetime CLI / standalone 版本与 server-rs/Cargo.toml 锁定的 spacetimedb 版本不一致时,procedure 返回值会在宿主侧反序列化失败,api-server 继续等待就表现成调用超时。若旧 standalone 进程还在复用,也会把这个错配继续带进新一轮创作。
  • 处理:先用 spacetime --version 确认 spacetimedb tool version,再和 server-rs/Cargo.tomlspacetimedb = "..." 对齐;遇到版本不匹配时先直接执行 spacetime version install <version> && spacetime version use <version>,或在目标就是最新版本时执行 spacetime version upgrade,升级后重启 npm run dev:spacetime 再重试。当前 dev 脚本会在启动和复用本地 SpacetimeDB 前写入并校验 dev-spacetime-tool-version,避免继续复用旧宿主。
  • 验证:spacetime --version 输出与 server-rs/Cargo.toml 一致,http://127.0.0.1:3101/v1/ping 正常,npm run test -- scripts/dev.test.ts 通过,敲木鱼创作点击生成不再卡在 procedure timeout。
  • 关联:scripts/dev.mjsscripts/dev.test.tsserver-rs/Cargo.tomldocs/【开发运维】本地开发验证与生产运维-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.tsxnpm run test -- src/services/puzzle-runtime/puzzleUiSpritesheetParser.test.ts
  • 关联:src/components/puzzle-runtime/PuzzleRuntimeShell.tsxsrc/services/puzzle-runtime/puzzleUiSpritesheetParser.tsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

2026-05-22 补充:展示矩形和点击热区要分开处理。puzzleUiSpritesheetParserregions 保留完整视觉裁切矩形,hitRegions 用较高 alpha 阈值只包住实心按钮主体;运行态底部 spritesheet 道具按钮启用 puzzle-runtime-sprite-tool-button--precise-hit,父按钮不吃整块透明留白,内部 puzzle-runtime-ui-sprite-hit-zone 才接收指针事件,避免透明区域成为点击热区。

图像输入组件不要把业务状态藏在页面内联实现里

  • 现象:拼图页把参考图上传、缩略图、主图删除确认和 AI 重绘开关内联实现后,后续想复用到其它创作页时,页面级状态和通用 UI 状态混在一起,容易出现多套上传卡和参考图展示口径。
  • 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。
  • 处理:抽 CreativeImageInputPanel 时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。
  • 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。
  • 关联:src/components/common/CreativeImageInputPanel.tsxsrc/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx

RPG 发布不能只依赖 agent session seed_text

  • 现象:RPG 结果页 publish_world 返回 UPSTREAM_ERRORdetails 为 custom_world.setting_text 不能为空;同一 session 的 result-view 日志显示 publish_ready=true
  • 原因:前端发布动作只提交 { action: 'publish_world' },旧 agent 会话的 seed_text 可能为空;如果后端只从 action payload 或 seed_textsetting_text,就会在最终 compile / publish 校验阶段失败。
  • 处理:module-custom-world::resolve_custom_world_publish_setting_text(...) 以当前 draft_profile_json 为草稿真相,优先读取 settingTextcreatorIntent.rawSettingTextcreatorIntent.worldHookworldHookanchorContent.worldPromise(.hook)summaryname/title,最后才回退 seed_text
  • 验证:cargo test -p module-custom-world publish_setting_text --manifest-path server-rs\Cargo.tomlcargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
  • 关联:server-rs/crates/module-custom-world/src/application.rsserver-rs/crates/spacetime-module/src/custom_world.rsdocs/【玩法创作】平台入口与玩法链路-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_profilepublish_world
  • 验证:npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx;确认已发布场景下 syncAgentDraftResultProfileexecutePublishWorld 均未被调用。
  • 关联:src/components/rpg-entry/useRpgCreationEnterWorld.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

RPG 点击启动黑屏 / 默认 profile 先查 profile 归一化和摘要覆盖

  • 现象:作品详情点击“启动”后页面切到 RPG runtime,但用户只看到黑屏、空白,或进入默认角色 / 默认 profile;从作品详情点“作品编辑”后开局 CG、封面、角色图、技能动作预览、初始物品图标或场景背景图丢失;DevTools 里可能同时看到旧自动存档 /api/runtime/save/snapshot 被主动 cancel。
  • 原因:/custom-world-library / /custom-world-gallery 详情接口可能返回历史或摘要式 profile,缺少 playableNpcsstoryNpcslandmarksattributeSchema 等运行态字段;前端 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 必须近似无损保留 coveropeningCgcamp.narrativeResidueslandmark.visualDescription/narrativeResiduesskills[].actionPreviewConfiginitialItems[].iconSrcattributeSchema、角色 attributeProfilesceneChapterBlueprints[].acts[] 的背景与结构字段;只有背景资产的 act 也不能被过滤。角色选择页对角色生成异常或空数组回退默认角色,并保留返回按钮/轻量空态;顶层 runtime 懒加载 fallback 不使用纯 null
  • 验证:npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "creation hub published work start uses loaded detail profile instead of library summary|creation hub published work edit keeps loaded detail profile assets instead of library summary"npm run test -- src/data/customWorldLibrary.test.ts -t "保留结果页封面和关键图片资产槽位|近似无损保留编辑态和运行态结构字段|保留只有背景资产的场景幕"npm run test -- src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx -t "默认封面和角色编辑结构差异也不能被列表摘要覆盖"npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx -t "正式进入世界回读结果页字段更少时不降级当前完整 profile"npm run typecheck
  • 关联:src/components/rpg-entry/useRpgEntryLibraryDetail.tssrc/components/rpg-entry/useRpgCreationEnterWorld.tssrc/data/customWorldLibrary.tssrc/services/rpg-entry/rpgEntryLibraryClient.tssrc/components/rpg-entry/RpgEntryCharacterSelectView.tsxsrc/App.tsxsrc/components/rpg-runtime-shell/RpgRuntimeShell.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

RPG 战后一轮战斗后卡在观察/试探/调息先查 post-battle finalization

  • 现象:RPG 一轮战斗胜利后,运行态只显示默认 观察周围迹象 / 主动出声试探 / 原地调息,这些按钮只有文字反馈;点“继续冒险”后又回到同样选项,点探索只播退场/进场动画,场景和剧情不推进。
  • 原因:终局战斗 action 如果只走通用 resolve_story_runtime_action fallback,而没有在后端调用 finalize_post_battle_resolution(...),就不会持久写入 story_continue_adventuredeferredOptions 和下一幕 currentSceneActState。另外旧 bootstrap 快照可能只有 connectedSceneIds / forwardSceneId、没有 connections,战后选项生成若只读 connections 也会退回 idle_explore_forward 循环。
  • 处理:module-runtime-story 在 story action 投影后统一调用 post-battle finalizationidle_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_adventuredeferredOptions、下一幕 act,以及 idle_travel_next_scene 真正切换场景。
  • 关联:server-rs/crates/module-runtime-story/src/session_action.rsserver-rs/crates/module-runtime-story/src/post_battle.rsserver-rs/crates/module-runtime-story/src/battle_tests.rsdocs/【玩法创作】平台入口与玩法链路-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.tsxdocs/【项目基线】当前产品与工程约束-2026-05-15.md

弹窗里复用 CreativeImageInputPanel 要保留画面卡高度

  • 现象:拼图草稿结果页的关卡详情弹窗中仍能看到“画面图”标题、画面描述和生成按钮,但实际画面图卡片视觉上消失。
  • 原因:CreativeImageInputPanel 内部依赖 flex-1h-fullmax-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.tsxsrc/components/puzzle-result/PuzzleResultView.tsxsrc/components/puzzle-result/PuzzleResultView.test.tsx

Windows provision 下载截断要断点续传而不是回退目标机下载

  • 当前状态:已废弃。2026-06-01 起生产 Jenkins 流水线统一切到 Linux agentGenarrative-Server-Provision 不再维护 Windows 下载阶段。
  • 现象:Genarrative-Server-ProvisionDownload 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}.downloadcurl 失败时下一轮使用 -C - 断点续传;最终只以 GitHub release asset 的 SHA256 digest 作为放行条件,完整返回但 digest 不匹配才删除临时文件重新下载。不要把 SpacetimeDB 或 otelcol-contrib 下载挪回 Linux 目标机。
  • 验证:日志应显示 curl 断点续传 ... resumeBytes=...,最终出现 已下载 ... bytes=...;目标 Linux 阶段只消费 stash/unstash 带过去的下载件。
  • 关联:jenkins/Jenkinsfile.production-server-provisiondocs/【开发运维】本地开发验证与生产运维-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,不是 gRPCCollector 才是接收和转发边界。
  • 处理:生产模板用 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.exampledeploy/container/api-server.env.exampledocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

tracking outbox 到批量阈值后先封存再异步 flush

  • 现象:route tracking 高峰时如果主请求线程要等 SpacetimeDB 批量入库,接口延迟会被 outbox 写入链路拖长。
  • 原因:outbox 的职责是把普通 HTTP route tracking 从请求线程切走,不能把 flush 结果回写成同步阻塞。
  • 处理:达到 BATCH_SIZE 立即封存 active 文件并切新 activeFLUSH_INTERVAL_MS 只做兜底封存,后台 worker 异步 flush sealed 文件;成功删文件,失败保留重试,坏文件隔离为 corrupt-*MAX_BYTES 只做磁盘保护。
  • 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
  • 关联:server-rs/crates/api-server/src/tracking_outbox.rsdocs/【开发运维】本地开发验证与生产运维-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.rsserver-rs/crates/api-server/src/auth.rsserver-rs/crates/api-server/src/work_play_tracking.rssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/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.tsxsrc/routing/appPageRoutes.tssrc/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-outboxgenarrative: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.shscripts/jenkins-server-provision.shdocs/【开发运维】本地开发验证与生产运维-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 显示 enabledinactive/deadNEXT / 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.serviceactive (running) 且监听 127.0.0.1:4317/4318systemctl list-timers genarrative-database-backup.timer --all 显示下一次触发约为次日 03:20/healthz/readyz/v1/ping 仍通过。
  • 关联:scripts/jenkins-server-provision.shdeploy/systemd/otelcol-contrib.servicedeploy/otelcol/genarrative-debug.yamldocs/【开发运维】本地开发验证与生产运维-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_eventevent_key = 'external_api_call_failure'metadata_json。当前通用 VectorEngine gpt-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.rsserver-rs/crates/api-server/src/openai_image_generation.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.mddocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

VectorEngine 图片协议先看 platform-image,不要先翻 puzzle.rs

  • 现象:排查拼图或其它玩法的生图失败时,如果直接在 api-server 的大文件里找 images/generationsimages/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.tomlcargo test -p platform-image --test vector_engine --manifest-path server-rs/Cargo.tomlcargo 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.rsserver-rs/crates/api-server/src/external_api_audit.rsserver-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.rsrequest.rsresponse.rsdownload.rspersist.rserror.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.rsrequest.rsresponse.rstransport.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 Largeaccess log 显示 request_time=0.000upstream_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。Nginx client_max_body_size 64m 只保留为旧客户端和兼容输入兜底,发布后仍需 nginx -t && nginx -s reload
  • 验证:前端 action payload 不应再出现大段 data:image/...;base64nginx -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.tsserver-rs/crates/api-server/src/puzzle/vector_engine.rsdeploy/nginx/genarrative.confdeploy/nginx/genarrative-dev-http.confdeploy/container/nginx.confdocs/【玩法创作】平台入口与玩法链路-2026-05-15.mddocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

汪汪声浪入口不要再回到独立配置阶段

  • 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。
  • 原因:旧实现把 bark-battle 单独挂到 bark-battle-config selectionStage,而不是复用创作 Tab 里的模板区。
  • 处理:入口点击只设置 activeCreationFormType = 'bark-battle' 并回到创作 TabBarkBattleConfigEditor 作为内嵌表单使用,默认隐藏返回按钮和页面标题;runtime onExit 重新回到创作 Tab 的汪汪声浪模板。
  • 验证:点击汪汪声浪后直接看到创作页内嵌表单,不再出现独立配置页;测试应覆盖内嵌表单与 runtime 返回路径。
  • 关联:src/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/bark-battle-creation/BarkBattleConfigEditor.tsxsrc/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_jsonpublished_snapshot_json;首轮自动生成只由 bark-battle-generating 负责,结果页仅覆盖已接入的玩家形象、对手形象和竞技背景图片槽位,不再提供音频配置入口。
  • 验证:发布后 runtime config 应包含结果页最终 playerCharacterImageSrcopponentCharacterImageSrcuiBackgroundImageSrc

汪汪声浪 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.tsnpm 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.tsxsrc/services/bark-battle-creation/barkBattleCreationClient.tsserver-rs/crates/api-server/src/bark_battle.rsserver-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.rsplayer-characteropponent-characterui-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.tscargo test -p shared-contracts bark_battle --manifest-path server-rs\Cargo.tomlcargo check --manifest-path server-rs\Cargo.toml -p platform-oss -p api-server
  • 关联:src/services/bark-battle-creation/barkBattleCreationClient.tssrc/components/bark-battle-creation/BarkBattleGeneratingView.tsxserver-rs/crates/api-server/src/bark_battle.rsserver-rs/crates/platform-oss/src/lib.rs

抓大鹅批量重新生成物品不要新增 itemId

  • 现象:结果页批量重新生成物品后,试玩或正式运行态的物品类型和图片对应关系漂移,或者用户输入一个不存在名称后被当作新物品追加。
  • 原因:重新生成和批量新增共用 item-assets 接口,如果前端不传 mode = "replace",或后端替换时重新分配 itemId / 追加未匹配名称,就会破坏 generatedItemAssets 顺序和运行态类型映射。
  • 处理:批量重新生成只提交当前素材列表中能匹配到的名称,并传 mode = "replace";后端只对同名已有素材生成新图片,合并时保留原 itemIditemName、模型兼容字段、UI 背景和历史音频字段,未匹配名称直接忽略且不计费。
  • 验证:npm run test -- src\components\match3d-result\Match3DResultView.test.tsx 覆盖前端提交口径,cargo test -p api-server match3d_item_asset --manifest-path server-rs\Cargo.tomlcargo test -p api-server match3d_regenerated_asset --manifest-path server-rs\Cargo.toml 覆盖后端替换计划与身份保留。
  • 关联:src/components/match3d-result/Match3DResultView.tsxserver-rs/crates/api-server/src/match3d.rspackages/shared/src/contracts/match3dWorks.tsserver-rs/crates/shared-contracts/src/match3d_works.rsdocs/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,继续保留当前可见 generatedItemAssetsclearCountdifficulty
  • 验证: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.tsxserver-rs/crates/api-server/src/match3d.rsserver-rs/crates/spacetime-module/src/match3d.rsdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

OSS V4 签名时间和 bucket/object_key 兼容

  • 现象:OSS V4 私有读签名在部分时间点失败,可能出现 OSS V4 签名时间格式化失败 或服务端判定签名格式错误;排查用例中 bucket 为 xushi-devobject_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.rsserver-rs/crates/platform-oss/README.md

generated 音频路径进运行态前要先换签

  • 现象:草稿页 audio 控件能播放背景音乐,但拼图或抓大鹅运行态开局后背景音乐不响,Network 可能出现裸 /generated-*-assets/...mp3 私有路径 403。
  • 原因:生成音乐转存到 OSS 私有对象后,audioSrc 是 generated legacy path;浏览器 <audio> 不能像公开静态资源一样直接请求裸路径。另一个常见误判是浏览器拒绝自动播放,资源已经进入运行态但开局第一次 audio.play() 被拦截。
  • 处理:结果页试听控件和运行态隐藏 <audio> 设置 src 前,都先通过 useResolvedAssetReadUrlresolveAssetReadUrl 换签;签名未就绪时不要回退请求裸 generated 路径。运行态自动播放失败只静默兜底,但玩家首次按下拼图块或点击抓大鹅物品时要重试同一个背景音乐播放函数。拼图读取 currentLevel.backgroundMusic.audioSrc,抓大鹅读取 generatedItemAssets[].backgroundMusic.audioSrc
  • 验证:结果页试听和运行态 <audio loop>src 为签名 URL 或公开 URL;拼图/抓大鹅运行态首次局内交互后会再次尝试播放背景音乐;npm run typecheck 不报契约字段缺失,后端 run response 带 backgroundMusic
  • 关联:src/components/puzzle-runtime/PuzzleRuntimeShell.tsxsrc/components/match3d-runtime/Match3DRuntimeShell.tsxdocs/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.tssrc/components/match3d-result/Match3DResultView.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

中文乱码与编码风险

  • 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
  • 原因:Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
  • 处理:
    • 不要直接沿用乱码文本。
    • 不要用英文替换中文,除非用户明确要求翻译。
    • 在 PowerShell 5.1 中显式使用 UTF-8。
    • 优先用 Python/Node 或 Get-Content -Encoding UTF8 核对原文。
    • 修改中文文件时优先局部补丁,避免无关内容重写。
  • 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
  • 关联:AGENTS.mdnpm run check:encoding

SpacetimeDB 运行态查询不要绕过已有索引或用 procedure JSON 回传

  • 现象:运行态接口看起来只查当前用户、作品或任务,却在 spacetime-module 中使用 ctx.db.<table>().iter().filter(...) 整表遍历;或者 procedure result 返回 items_json/run_json/work_json 等 JSON 字符串,spacetime-client mapper 再反序列化成旧兼容结构。
  • 原因:新增索引或 typed snapshot 后,没有同步清理旧 mapper / 测试兼容层,也没有用静态检查拦截回退写法。
  • 处理:表上已有主键、unique 或 #[index] 覆盖查询前缀时,先用对应 accessor .find(...) / .filter(...),只对索引无法覆盖的条件做内存残余过滤;procedure result 返回 typed snapshot / typed value,不再跨层传 *_json: Option<String> 作为 payload。
  • 验证:执行 npm run check:spacetime-runtime-accessnpm run check:server-rs-ddd,涉及绑定变化时先执行 npm run spacetime:generatenpm run check:spacetime-schema
  • 关联:docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.mdscripts/check-spacetime-runtime-access.mjsserver-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 view puzzle_gallery_card_view 暴露已发布拼图作品的列表卡片字段,不携带 levels / anchor_pack 等详情级载荷;spacetime-client 建连接后订阅 SELECT * FROM puzzle_gallery_card_viewSELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle' 并等待 on_applied。HTTP gallery 通过 PuzzleGalleryCache 缓存最终 PuzzleGalleryResponse DTOitems 返回前 10 个完整卡片,previewRefs 返回后 10 个作品号引用,cache miss / TTL 过期时单飞重建,后台 cleanup task 周期清理旧响应。旧 list_puzzle_gallery procedure 只作兼容,不再作为 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:generatecargo check --manifest-path server-rs/Cargo.toml -p spacetime-clientcargo check --manifest-path server-rs/Cargo.toml -p api-server 和 schema/runtime access 检查。
  • 关联:server-rs/crates/spacetime-module/src/puzzle.rsserver-rs/crates/spacetime-client/src/lib.rsserver-rs/crates/spacetime-client/src/puzzle.rsserver-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.countgenarrative.http.server.response_bodies.in_flightgenarrative.http.server.request_permits.availablegenarrative.puzzle_gallery.cache.*genarrative.spacetime.read.*。如果内存高但 body in-flight、背压 permit、cache rebuild 和 SpacetimeDB read 都不显示积压,优先按连接 / 发送链路高水位处理。
  • 验证:对照打 /api/runtime/puzzle/gallery/healthz;对比 PREALLOCATED_VUS=300 MAX_VUS=800PREALLOCATED_VUS=20 MAX_VUS=40;压测结束后继续采样 10 秒确认 private memory 回落。
  • 关联:scripts/loadtest/README.mddocs/【开发运维】本地开发验证与生产运维-2026-05-15.mdserver-rs/crates/api-server/src/process_metrics.rsserver-rs/crates/api-server/src/telemetry.rs

容器高 VU 下 /healthz RSS 尖峰先查 Axum state 深拷贝

  • 现象:容器 Linux release api-server/healthz500 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.rsAppState 必须保持 Arc<AppStateInner> 浅拷贝壳;新增共享状态字段时放入 AppStateInner,不要把外层改回大结构体 clone。
  • 验证:用容器内 k6 直连 api-server:8082/healthz500 HTTP req/s、PREALLOCATED_VUS=100、30 秒压测后采样 /proc/$pid/status/proc/$pid/smaps_rollup 和 cgroup memory.current/memory.peak。2026-05-18 修复后结果为 15001 请求、http_req_failed=0dropped_iterations=0RSS 约 18 MiB -> 52 MiBcgroup peak 约 47 MiB。
  • 关联:server-rs/crates/api-server/src/state.rsdeploy/container/README.mddeploy/container/api-server.Dockerfile
  • 现象:公开作品列表在 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_hitsrefreshes_startedrefreshes_failedNginx access log 看 request_timeupstream_response_time 是否同步收敛;超过容量时应明确 429,而不是长时间排队或新增 502。
  • 关联:deploy/nginx/genarrative.confdeploy/container/nginx.confserver-rs/crates/api-server/src/backpressure.rsserver-rs/crates/api-server/src/puzzle_gallery_cache.rs

多玩法公开广场列表优先订阅 public view / read model

  • 现象:抓大鹅、方洞挑战、视觉小说、大鱼吃小鱼等公开列表如果沿用 list_*_works procedure,即使只读已发布作品,也会在每个 HTTP 请求里回到 SpacetimeDB WASM 侧扫描、反序列化配置并组装列表,50RPS 以上容易变成热点。
  • 原因:个人作品列表和公开广场列表复用了同一套 procedure 输入,导致公开列表为了通过 owner 校验传固定占位 owner,并把可长期同步的公开读模型当成请求期查询。
  • 处理:每个公开广场新增或复用专用 public view / public read modelmatch_3_d_gallery_viewsquare_hole_gallery_viewvisual_novel_gallery_viewbig_fish_gallery_viewspacetime-client 建连接后订阅这些 view 和对应 public_work_play_daily_stat source_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:generatecargo check -p spacetime-client --manifest-path server-rs/Cargo.tomlcargo check -p api-server --manifest-path server-rs/Cargo.tomlnpm run check:spacetime-schema
  • 关联:server-rs/crates/spacetime-module/src/match3d.rsserver-rs/crates/spacetime-module/src/square_hole.rsserver-rs/crates/spacetime-module/src/visual_novel.rsserver-rs/crates/spacetime-module/src/big_fish/session.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md

自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure

  • 现象:/api/runtime/custom-world-gallery 每次请求调用 list_custom_world_gallery_entries procedure;入口熔断中间件每个玩法请求调用 get_creation_entry_config procedure50RPS 以上会把 SpacetimeDB procedure 调用变成热点。
  • 原因:custom_world_gallery_entrycreation_entry_configcreation_entry_type_config 已经是可订阅读模型或配置表,但 HTTP 路径仍按“请求到来再查 procedure”处理。
  • 处理:spacetime-client 长连接订阅 custom_world_gallery_entrypublic_work_play_daily_statcustom-world 桶、creation_entry_configcreation_entry_type_configcustom-world gallery 从本地 cache 排序并聚合 7 日播放数;入口配置优先读订阅 cache,cache 缺失时用最近一次成功内存快照,再兜底调用 get_creation_entry_config 完成旧库兼容。旧 list_custom_world_gallery_entries procedure 只允许作为旧库缺少 gallery 行时的一次性同步兜底。
  • 验证:搜索 server-rs/crates/spacetime-client/src/custom_world.rsgallery 主路径应是 read_after_connect 读取 custom_world_gallery_entry();搜索 server-rs/crates/spacetime-client/src/runtime.rsget_creation_entry_config 应优先读取 creation_entry_config()creation_entry_type_config()。执行 cargo check -p spacetime-client --manifest-path server-rs/Cargo.tomlcargo check -p api-server --manifest-path server-rs/Cargo.toml
  • 关联:server-rs/crates/spacetime-client/src/lib.rsserver-rs/crates/spacetime-client/src/custom_world.rsserver-rs/crates/spacetime-client/src/runtime.rsdocs/【后端架构】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.mjsnpm run check:encodinggit diff --check
  • 关联:scripts/generate-taonier-logo-concepts.mjsdocs/design/TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md

忘记密码后仍提示手机号或密码错误先查认证投影同步

  • 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 Bearer JWT 版本已失效,日志里的 token version 与本地快照不一致。
  • 原因:重置/修改密码会更新 password_hashpassword_login_enabledtoken_version,如果 API 层只更新本地 InMemoryAuthStore,没有调用 sync_auth_store_tables_to_spacetime()api-server 重启时可能从旧的 SpacetimeDB 正式认证表恢复账号状态。
  • 处理:POST /api/auth/password/changePOST /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.tomlcargo test -p api-server password --manifest-path server-rs/Cargo.toml;手测时重设密码后旧密码应失败,新密码应成功,重启后仍应保持。
  • 关联:server-rs/crates/api-server/src/password_management.rsserver-rs/crates/api-server/src/state.rsdocs/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 只导出正式认证表 projectionmodule-auth 从 projection 恢复时必须丢弃指向不存在 user_account 的 identity、union 索引和 refresh session。运行时创建手机号用户前若发现手机号映射指向不存在的用户,应删除孤儿映射后继续创建,避免死锁态继续扩散。
  • 验证:cargo test -p module-auth projection --manifest-path server-rs/Cargo.tomlcargo test -p module-auth phone --manifest-path server-rs/Cargo.tomlcargo 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.rsserver-rs/crates/spacetime-module/src/auth/procedures.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md

认证快照表和旧 procedure 已删除

  • 现象:有些旧代码和生成 bindings 里还会残留 get_auth_store_snapshotupsert_auth_store_snapshotimport_auth_store_snapshotimport_auth_store_snapshot_jsonexport_auth_store_snapshot_from_tables,或者把 auth-store.json 误当成认证恢复源。
  • 原因:认证恢复已经彻底收口到 SpacetimeDB 正式表和 module-auth typed 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.tomlcargo check -p api-server --manifest-path server-rs/Cargo.tomlnpm run check:spacetime-schemanpm 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_URLVECTOR_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.reasoncargo 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.tsserver-rs/crates/api-server/src/state.rsdocs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.mddocs/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.tsxnpm run typecheck
  • 关联:src/components/match3d-result/Match3DResultView.tsxsrc/components/match3d-result/Match3DResultView.test.tsxdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

.hermes 只放共享内容,不放个人 Hermes 配置

  • 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
  • 原因:仓库 .hermes/ 与个人 ~/.hermes/ 名称相似。
  • 处理:仓库 .hermes/ 只放 Markdown 共享记忆、计划和可公开 skills;不提交 .envconfig.yamlsessions/auth.json
  • 验证:提交前检查 git diff -- .hermes,确认没有密钥、会话记录或个人路径敏感信息。
  • 关联:.hermes/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_normactions/action/gesture/gestures/event/name/typehands[]leftHand/rightHandleft_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.tssrc/components/child-motion-demo/ChildMotionWarmupDemo.tsxdocs/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.tsxsrc/components/child-motion-demo/childMotionWarmupModel.tsdocs/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.tsxsrc/index.cssdocs/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.tsxdocs/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.tsxdocs/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 分钟,按资源类别输出开始、完成和耗时日志。2x2 sheet 固定包含物品 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.tscargo 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.tssrc/services/miniGameDraftGenerationProgress.tsserver-rs/crates/api-server/src/edutainment_baby_object.rsdocs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md

宝贝识物篮子手柄白底先查 sheet 切图后处理

  • 现象:宝贝识物 新生成的主题篮子在左右手柄、篮口镂空或边缘处仍出现白底块或白色毛边,尤其是 2x2 sheet 背景被抠透明后,封闭镂空区域可能没有被通用边缘连通抠图清理掉。
  • 原因:宝贝识物为了降低 image-2 成本,把物品 A、物品 B、篮子和礼物盒放在同一张 2x2 sheet。通用背景透明处理主要从单格边缘连通背景开始,封闭在篮子手柄内部的近白区域不一定与边缘连通,因此会残留;如果把强力近白清理应用到物品格,又可能误伤白色物品主体。
  • 处理:后端 slice_baby_object_match_sheet 只在 BabyObjectMatchSheetSlot::Basket 编码前执行近白、低饱和 matte 清理;物品格和礼物盒格继续只走通用背景透明处理。sheet prompt 同步要求篮子手柄和篮口镂空处不要留下白底描边或毛边。运行态左右篮子的物品图标和名称 UI 以篮子中心线对齐,避免素材放大后看起来偏移。
  • 验证:运行 cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.tomlnpm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx;真实联调需要重新生成宝贝识物资源,旧草稿中已保存的 base64 篮子图不会自动被新后处理改写。
  • 关联:server-rs/crates/api-server/src/edutainment_baby_object.rssrc/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsxsrc/index.cssdocs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md

宝贝识物物品框被长条素材拉伸先查固定槽位

  • 现象:用户用手机、筷子等长条关键词生成素材后,中央物品 UI 或篮子上方物品图标看起来被拉成长框,圆形 UI 失去固定比例。
  • 原因:运行态如果让图片固有宽高或外层自适应内容,就会把长条透明 PNG 的主体比例传导到 UI 容器。
  • 处理:中央物品 UI 和篮子物品图标都必须使用固定正方形槽位,外层尺寸由 CSS 变量控制;生成素材图片只在槽位内 object-fit: contain 等比缩放,不改变外层圆形 UI 框尺寸。
  • 验证:用长条物品草稿进入宝贝识物运行态,中央物品框和篮子图标框仍为正圆,长条主体在框内缩小显示。
  • 关联:src/index.csssrc/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsxdocs/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-matchtitle=宝贝识物visible=trueopen=truesort_order=90api-server 测试降级配置也要同步包含该类型。入口图片路径需指向真实存在资源,避免卡片图片 404。
  • 验证:运行 cargo test -p module-runtime default_creation_entry_types_include_baby_object_match --manifest-path server-rs/Cargo.tomlcargo test -p api-server test_creation_entry_config_response_keeps_baby_object_match_visible --manifest-path server-rs/Cargo.tomlcargo check -p spacetime-module --manifest-path server-rs/Cargo.tomlnpm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
  • 关联:server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rsserver-rs/crates/api-server/src/creation_entry_config.rsdocs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md

儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置

  • 现象:/child-motion-demo 已经呈现绘本草地风格,但 public/child-motion-demo/picture-book-grass-stage.pngpicture-book-grass-floor.pngpicture-book-ground-ring.pngpicture-book-character-outline.pngpicture-book-ui-panel.pngpicture-book-ui-button.png 不存在,Network 里对应图片返回 404,或运行 npm run assets:child-motion-demo -- --live 返回缺少 VectorEngine 配置。
  • 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine gpt-image-2 生成,脚本只读取 VECTOR_ENGINE_BASE_URLVECTOR_ENGINE_API_KEY 和可选 VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。
  • 处理:在本地私密环境补齐 VECTOR_ENGINE_BASE_URL=https://api.vectorengine.aiVECTOR_ENGINE_API_KEY,不要把 key 写入 Git;先运行 npm run assets:child-motion-demo -- --dry-run 核对 prompt,再运行 npm run assets:child-motion-demo -- --livenpm 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.mjssrc/index.cssdocs/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.pngpicture-book-ground-ring-v3.pngpicture-book-character-outline-v4.pngpicture-book-hud-strip-v2.pngpicture-book-calibration-strip-v2.pngpicture-book-start-panel-v2.pngpicture-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.mjssrc/index.csspublic/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.csspublic/child-motion-demo/picture-book-wave-cat-body-guide-v7.pngpublic/child-motion-demo/picture-book-wave-cat-arm-guide-v7.pngdocs/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.aiVECTOR_ENGINE_API_KEYVECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;排查请求体时确认无参考图路径为 /v1/images/generations、有参考图路径为 /v1/images/edits,模型为 gpt-image-2
  • 验证:运行 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.mdserver-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 带 referenceImageSrcapi-server 日志按同一 session_id 查看 拼图参考图解析完成拼图 VectorEngine 图片生成 HTTP 返回拼图 VectorEngine 图片下载完成拼图生成图片已写入 OSS 与资产索引,可定位慢在参考图读取、VectorEngine、下载或 OSS。
  • 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的 pictureReference 重新生成;后端单测覆盖 VectorEngine 请求体 image 字段。
  • 关联:src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsxsrc/components/puzzle-result/PuzzleResultView.tsxserver-rs/crates/api-server/src/puzzle.rs

拼图首图生成后要把入口参考图写回 pictureReference

  • 现象:入口页上传图后,首图看着像没吃到参考图;结果页重新生成时默认只沿用关卡旧图,没有继续带入口上传图。
  • 原因:首图生成请求虽然已经把 referenceImageSrc 传给 VectorEngine,但如果后端只更新 cover_image_src / selected_candidate_id 而不回写首关 pictureReference,结果页后续重绘就会丢失参考图。
  • 处理:在 compile_puzzle_draftgenerate_puzzle_images 的成功与 SpacetimeDB 降级快照路径里,都把本次入口参考图写入首关 pictureReference
  • 验证:后端单测覆盖 build_puzzle_levels_with_primary_updateapply_generated_puzzle_candidates_to_session_snapshot;结果页重新生成应在未重新上传时继续带入 level.pictureReference
  • 关联:server-rs/crates/api-server/src/puzzle.rssrc/components/puzzle-result/PuzzleResultView.tsx

拼图参考图不像时先看 edits multipart image

  • 现象:Network payload 已带 referenceImageSrc,但 VectorEngine 生成结果仍明显不像上传图。
  • 原因:参考图只在 aiRedraw = true 时由后端解析并传给 gpt-image-2 /v1/images/edits 的 multipart image part;若前端没传 referenceImageSrc、后端解析失败或 prompt 缺少参考图强约束,生成会退化为纯文生图。
  • 处理:referenceImageSrc 存在且 aiRedraw = true 时走 edits multipartprompt 保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。
  • 验证:后端单测应覆盖 /v1/images/edits 路由、b64_json 响应解码和参考图强提示;真实联调看日志里是否命中 拼图 VectorEngine 图片编辑 HTTP 返回
  • 关联:server-rs/crates/api-server/src/puzzle.rssrc/services/puzzleReferenceImage.tsdocs/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 返回 日志。
  • 原因:这是 reqwestsend() 阶段失败,尚未收到 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" 至少应返回 HTTP 401,说明域名、TLS、路径和 multipart 上传可达;执行 cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml
  • 关联:server-rs/crates/api-server/src/puzzle.rsdocs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.mddocs/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/backgroundMusiccurrentLevel。结果页 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.tssrc/services/puzzle-runtime/puzzleLocalRuntime.tssrc/components/puzzle-runtime/PuzzleRuntimeShell.tsxsrc/components/puzzle-result/PuzzleResultView.tsxdocs/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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md

自动草稿成功但缺音乐或 UI 先查后端吞错

  • 现象:拼图或抓大鹅生成页提示完成,但草稿页仍显示“暂无音乐”,拼图 UI 仍是默认预览,试玩局内也没有生成音乐或 UI 背景。
  • 原因:自动草稿阶段如果把 VectorEngine / Suno / OSS / 资产绑定错误记录为 warning 后继续返回成功,前端只能拿到缺关键资产的成功 draft,随后保存和试玩都会消费这份空资产状态。
  • 处理:自动草稿必须把必需生成资产当作后端完成条件:拼图首关需同时具备 levels[0].backgroundMusic.audioSrclevels[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.rsserver-rs/crates/api-server/src/match3d.rsdocs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.mddocs/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 Gateway504 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 Timeouterror.details.provider=vector-enginetimeout=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.tscargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml,真实联调重启 npm run dev:api-server 后检查 /healthz
  • 关联:src/services/creation-agent/creationAgentClientFactory.tsserver-rs/crates/api-server/src/puzzle.rsdocs/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-servernpm run dev 终端用 rs api-server,否则旧进程仍按旧超时运行。
  • 验证:分别运行 cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.tomlcargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml;真实联调重启后再触发开局 CG,若仍失败看返回的 details.errorSource/source/timeout/connect/body/endpointtracking_event.metadata_json.errorSource/requestIdlogs/api-server/ 同一 request_id。
  • 关联:server-rs/crates/api-server/src/custom_world_ai.rsserver-rs/crates/api-server/src/custom_world_ai/opening_cg.rsserver-rs/crates/api-server/src/openai_image_generation.rsdocs/【开发运维】本地开发验证与生产运维-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.tssrc/components/rpg-creation-result/RpgCreationResultViewImpl.tsxsrc/components/CustomWorldEntityCatalog.tsx

RPG 发布报 legacy_result_profile_json 非法先查 null 兼容

  • 现象:RPG 结果页发布动作返回 UPSTREAM_ERRORSpacetimeDB details 里是 custom_world.compile.legacy_result_profile_json 不是合法 JSON object
  • 原因:publish_world 前端契约只要求 { action: 'publish_world' }ExecuteCustomWorldAgentActionRequest.legacy_result_profile 是可选字段,经 HTTP / serde / SpacetimeDB payload 传递时可能显式成为 JSON null。旧的编译器只接受 object 或缺省,把 Some("null") 当成非法 legacy JSON。
  • 处理:module-custom-world 的 optional JSON object 解析要把 null 视为未提供,仍拒绝数组、字符串、数字和坏 JSON;正式发布继续以 session draft_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.rsserver-rs/crates/spacetime-module/src/custom_world.rsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时

  • 现象:用 Node fetch 直接请求 POST /v1/images/generations,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛 AbortErrorTypeError: fetch failedUND_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.mdserver-rs/crates/api-server/src/openai_image_generation.rs

旧后端路线文档造成判断漂移

  • 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
  • 原因:项目历史文档较多,部分旧方案仍保留作迁移参考。
  • 处理:涉及服务端、数据真相、SpacetimeDB、运行时状态时,先看 CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md,再看 DDD 总纲和具体技术方案。
  • 验证:代码改动应落在 server-rs + Axum + SpacetimeDB 主线;旧路线只作为迁移参考,不作为兼容目标。
  • 关联:docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.mdAGENTS.md

SpacetimeDB 表结构变更不能按 PostgreSQL 迁移直觉处理

  • 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。
  • 原因:SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。
  • 处理:变更前阅读 SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md;已有表新增字段必须放在 Rust 表结构体最后并设置明确默认值;需要修改字段名时,先询问用户并确认迁移计划;涉及表变化时同步 migration.rsSPACETIMEDB_TABLE_CATALOG.md 和 bindings;必要时走 JSON 导入导出与分片导入迁移流程。
  • 验证:发布前运行 npm run check:spacetime-schema,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
  • 关联:docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.mddocs/technical/SPACETIMEDB_TABLE_CATALOG.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.rsserver-rs/crates/spacetime-client/src/module_bindings/docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.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-contracts featureworkspace 根依赖保持 default-features = falseapi-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.tomlserver-rs/crates/api-server/Cargo.tomldocs/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 publishPre-publish check 阶段返回 403 Forbidden,提示当前 identity 无权对目标 database identity 执行 update database
  • 原因:当前 CLI 登录态不是目标数据库的创建者或授权身份,或 .env.local / publish 命令指向了另一个数据库或 SpacetimeDB 服务。
  • 处理:除 CI/CD 脚本内部受控用法外,不再使用 spacetime --root-dir 排障或发布。先执行 spacetime login showspacetime server list,再用 spacetime list --server http://127.0.0.1:3101 或实际 --server-url 确认当前身份是否能看到目标库;本地开发发布优先使用 npm run dev:spacetime 或从 server-rs 目录执行显式 --serverspacetime publish。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。
  • 验证:spacetime list --server http://127.0.0.1:3101 能看到目标库;重新发布不再使用无权限 identity。
  • 关联:scripts/dev.mjsdocs/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 或旧本地库。
  • 原因:本机 spacetime CLI 保存的旧 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 / 403npm run dev 可以完成 SpacetimeDB publish 并继续启动 api-server
  • 关联:docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.mdscripts/dev.mjs

本地 SpacetimeDB 联调可按阶段跳过宿主或发布

  • 现象:本地 npm run dev3101 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 spacetime-module 语法而被完整联调链路拖慢。
  • 原因:npm run dev 默认同时启动 SpacetimeDB standalone、发布 server-rs/crates/spacetime-module、启动 Rust api-server、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。
  • 处理:npm run dev 启动后会把实际 SpacetimeDB URL 记录到 server-rs/.spacetimedb/local/data/dev-spacetime-url,并同步写旧兼容文件 dev-rust-spacetime-url。下次启动即使没有传 --skip-spacetime,调度器也会先检查该 URL 旁边的 spacetime.pid/v1/ping 是否可复用;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 3101,被占用则选择最近可用端口,保证 publish 与 api-server 都连接同一个实际 SpacetimeDB URL。显式传 --skip-spacetime 时表示复用既有宿主,脚本不再对 SpacetimeDB 端口做可用性漂移;--spacetime-port 3101 就是后端要连接的实际端口,避免被误改到空闲但未启动的 3102api-server、主站 Vite 和后台 Vite 启动前也会解析可用端口。spacetime-module 改动后只重新 publish,不重启 standalone 宿主;未修改 spacetime-module 时使用 npm run dev -- --skip-publish;只查模块语法时执行 cargo check -p spacetime-module --manifest-path server-rs/Cargo.tomlnpm run dev 会在启动前检查 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,不可用时自动寻找后续可用端口,并把实际端口传给 publish、后端环境变量和前端代理目标。
  • 验证:--skip-spacetime 后脚本复用现有 http://127.0.0.1:3101;日志中的 [dev] spacetime: 不应漂移到没有服务的 3102GET /api/creation-entry/config 不应返回连接空端口导致的 50231018082 被其他进程占用时,脚本使用最近可用端口;--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.mdscripts/dev.mjs

npm run dev -- --watch 前端无限重启先查外层 watcher

  • 现象:开启 npm run dev -- --watch 后,后台 Vite 或主站 Vite 反复退出重启,即使没有手动修改源码。
  • 原因:Vite 本身会监听源码并写入 node_modules/.vite 等缓存;外层调度器如果再递归监听前端目录并重启 dev server,就可能把 Vite 自己的缓存写入当成源码变化,形成循环重启。
  • 处理:外层 watcher 只负责后端侧:spacetime-module 改动后重新 publishapi-server 改动后重启 Rust 进程。主站 Vite 和后台 Vite 的源码变化交给 Vite HMR;需要进程级重启时在 npm run dev 终端手动输入 rs webrs admin-web
  • 验证:npm run dev -- --watch 下修改 apps/admin-web/src/** 应由 Vite HMR 处理,不应出现连续 [dev] 重启 admin-webscripts/dev.test.ts 覆盖 web/admin-web 不注册外层 watch。
  • 关联:scripts/dev.mjsdocs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.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.mddocs/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-contractsmodule-* 中移除,只保留公开 DTO,平台响应到 DTO 的转换放回 api-server 等 adapter 层。
  • 验证:上述 cargo tree 输出 warning: nothing to printcargo check -p shared-contractscargo check -p api-server 通过;重新 spacetime publish ... --module-path server-rs/crates/spacetime-module 不再报 wasm-bindgen。
  • 关联:docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.mdserver-rs/crates/shared-contracts/src/assets.rsserver-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.tsapps/admin-web/vite.config.tschunkSizeWarningLimit
  • 处理:先看 warning 原文确认来源。若是合理的入口级 chunk 体积增长,调整对应 Vite 配置阈值或做真实拆包;不要把这类失败按 Rust / SpacetimeDB 编译错误排查。
  • 验证:重新执行 npm run build,主站与后台均构建完成且没有 build-gate warning 汇总。
  • 关联:scripts/build-gate.mjsvite.config.tsapps/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 用 mock FileReader 断言选择图片后出现 反馈凭证预览,且提交 payload 带 evidenceItems[].dataUrl
  • 关联:src/components/platform-entry/PlatformFeedbackView.tsxdocs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md

拼图 VectorEngine 图片生成密钥不能复用 DashScope / ARK key

  • 现象:拼图新手引导或拼图创作点击生成后返回 VectorEngine 图片生成密钥未配置
  • 原因:拼图 gpt-image-2 / 历史 nanobanana2 图片生成已统一走 VectorEngine;后端只读取 VECTOR_ENGINE_BASE_URLVECTOR_ENGINE_API_KEYVECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,不会用 DASHSCOPE_API_KEYLLM_API_KEYARK_API_KEYAPIMART_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_ENABLEDSMS_AUTH_PROVIDER 等以本地 env 文件为准,避免父进程继承的旧开关值长期压过 .env.local
  • 验证:本地加入临时测试后,HYPER3D_API_KEY 应能被 .env.secrets.local 覆盖,真实密钥 shell 变量仍然最高优先级;mergeApiServerEnv(..., { SMS_AUTH_ENABLED: "false" }).env.localSMS_AUTH_ENABLED=true 时应返回 true。
  • 关联:scripts/dev-utils.mjsserver-rs/crates/api-server/src/hyper3d_generation.rsdocs/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_URLVECTOR_ENGINE_API_KEYALIYUN_OSS_BUCKETALIYUN_OSS_ENDPOINTALIYUN_OSS_ACCESS_KEY_IDALIYUN_OSS_ACCESS_KEY_SECRET 都是 present,再重启 npm run dev:api-servernpm 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-osscargo check -p api-server;重启 npm run dev:api-server 后检查 /healthz,再重新触发拼图生成。
  • 关联:server-rs/crates/platform-oss/src/lib.rsserver-rs/crates/api-server/src/assets.rsdocs/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 纳入签名,额外参数会让签名失效。
  • 处理:拼图结果页、发布预览、运行态和历史素材预览都走 ResolvedAssetImageuseResolvedAssetReadUrl;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.tssrc/components/ResolvedAssetImage.tsxdocs/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.tomlnpm run check:encoding,以及拼图生成页恢复相关 RpgEntryFlowShell.agent.interaction.test.tsx 定向用例。
  • 关联:server-rs/crates/spacetime-module/src/puzzle.rssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsx

本地短信登录页签突然消失

  • 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但 LoginScreen 中手机号验证码表单仍存在。
  • 原因:历史实现曾根据 GET /api/auth/login-options 返回的 availableLoginMethods 渲染页签;接口返回空、失败或只返回 ["password"] 时,AuthGate 会降级成只显示密码。
    • 本地启动脚本没有让 .env.local 覆盖 .envSMS_AUTH_ENABLED=true 不生效,后端只返回 ["password"]
    • Rust API 直连已返回 ["phone","password"],但 Vite 代理目标指向未监听端口,导致 3000 域名下的 login-options 返回 500AuthGate 降级成 ["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-servernpm run dev:spacetimenpm run dev 启动,确认 .env.local 覆盖 .envRUST_SERVER_TARGET 没有指向旧端口,并分别请求 3000 域名和 Rust API 目标。
  • 验证:即使 /api/auth/login-options 返回空、失败或只返回 ["password"],登录弹窗也应同时显示 短信登录密码登录验证码 输入和“获取验证码”按钮;短信发送真实可用性再通过 POST /api/auth/phone/send-code 验证。
  • 关联:src/components/auth/AuthGate.tsxsrc/components/auth/LoginScreen.tsxsrc/components/auth/AuthGate.test.tsxscripts/dev-utils.mjsscripts/dev.mjs

本地短信收不到验证码先查 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.localSMS_AUTH_ENABLED=trueSMS_AUTH_PROVIDER=aliyun 显式打开,并确认 ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.comALIYUN_SMS_SIGN_NAME=北京亓盒网络科技ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486ALIYUN_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_IDALIYUN_SMS_ACCESS_KEY_SECRETALIYUN_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.rsscripts/dev-utils.mjsdocs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.mddocs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md

手机验证码登录 500 先查短信 provider 语义

  • 现象:登录弹窗手机号验证码登录失败,浏览器看到 POST /api/auth/phone/login 500,后端日志里同时出现阿里云短信 UNKNOWNbiz.FREQUENCYcheck frequency failed
  • 原因:真实短信 provider 的配置错误或上游失败曾被 module-auth 折叠成 PhoneAuthError::StoreHTTP 层只能按内部错误返回 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.rsserver-rs/crates/api-server/src/phone_auth.rsdocs/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 devnpm run dev:api-server。要做真实短信联调时,再切回 SMS_AUTH_PROVIDER=aliyun 并重启。
  • 验证:POST /api/auth/phone/send-code 应返回 providerRequestId=mock-request-idPOST /api/auth/phone/login123456 应返回 200user.loginMethod=phone。浏览器侧短信登录成功后,会先进入邀请码弹窗或我的页面,不应再提示“验证码错误”。
  • 关联:scripts/dev-utils.mjsscripts/dev-utils.test.tsscripts/dev.mjsserver-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.tsxsrc/components/auth/AuthGate.test.tsxdocs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md

刷新网页后登录态失效

  • 现象:刷新网页后,用户明明有本地 access token,却回到未登录状态。
  • 原因:AuthGate hydrate 曾先强制调用 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.tssrc/components/auth/AuthGate.tsxdocs/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.tsnpm 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.tssrc/services/assetReadUrlService.tssrc/services/puzzle-runtime/puzzleRuntimeClient.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/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.tsxsrc/components/rpg-entry/RpgEntryHomeView.tsxdocs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md

推荐页未登录入口误打开公开详情

  • 现象:新用户默认在发现页,但点击推荐页或推荐封面后,如果复用公开作品详情入口,可能绕过推荐页沉浸运行态,打开普通公开详情页。
  • 原因:RpgEntryHomeView 曾只有 onOpenGalleryDetail 一个回调,同时服务发现页公开详情和推荐页作品入口;一旦为发现页保留公开浏览能力,推荐页也会跟着打开详情。
  • 处理:公开详情与推荐页入口分离为 onOpenGalleryDetailonOpenRecommendGalleryDetail。发现页、搜索和排行榜保留公开详情;推荐 Tab、推荐封面、推荐运行态错误重试和桌面推荐模块走推荐运行态入口,不再主动弹登录窗。登录门禁只保留给创作、个人作品、删除、发布、Remix 等账号或所有权动作。
  • 验证:npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend"
  • 关联:src/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md

Rust 冷编译导致 api-server 健康检查误超时

  • 现象:旧 npm run dev:rust 在 Windows 冷编译/链接阶段误判 /healthz 等待超时并杀掉 cargo run;现入口为 npm run devnpm 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-serverbuild_router 测试通过,但 npm run dev:api-server 在 Windows debug 启动时 thread 'main' has overflowed its stack
  • 原因:api-server Axum 路由树已经很深,debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。
  • 处理:入口 main 用显式 16MB 栈线程启动 Tokio runtime,并把实际服务逻辑放入 run_server();新增路由时优先用小 router .merge(),避免继续拉长主链。
  • 验证:npm run dev:api-server/healthz 返回 200,相关路由冒烟通过。
  • 关联:server-rs/crates/api-server/src/main.rsserver-rs/crates/api-server/src/app.rs

Windows debug api-server.exe 锁文件与强杀退出码容易混淆

  • 现象:cargo run -p api-servernpm run dev:api-serverfailed 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.exe0xffffffff 常见于排障时用 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.mjsserver-rs/crates/api-server/src/main.rs

dev scheduler 端口被旧进程占用时会误判健康检查

  • 现象:旧本地 dev 链路可能输出 Port 3000 is in use, trying another one...,随后 api-server.exeAddrInUse / 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.mjsdocs/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/streamread ECONNRESET,随后 api-server.exe0xffffffff 退出,dev:spacetime 回收 SpacetimeDB、Vite 和后台 Vite。
  • 原因:单个 async_stream::stream! 中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future;真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。
  • 处理:长 SSE 路由优先使用 tokio::spawn 跑业务流程,通过 mpsc + UnboundedReceiverStream 向 Axum 返回轻量 stream;失败时更新会话为 failed 并发送 SSE error,不要把大段执行逻辑内联到路由返回的 stream future 中。
  • 验证:补充实际 collect() SSE body 的路由测试,确认首轮包含 stagepuzzle_template_catalogdone,且不会提前发送 puzzle_template_selection / puzzle_cost_range;再执行 cargo check -p api-servercargo test -p api-server creative_agent,联调时用 npm run dev:api-server 检查 /healthz
  • 关联:server-rs/crates/api-server/src/creative_agent.rsserver-rs/crates/api-server/src/app.rs

creative-agent 过程项不要把历史事件渲染成运行中

  • 现象:智能创作页过程中多个阶段从一开始同时转圈,生成结束或进入模板确认后仍有过程项保持转圈。
  • 原因:前端把历史 stagetool_startedthought_summary_delta 都按 active 渲染;后端工具开始/完成事件如果 toolCallId 不一致,也会导致开始事件无法收口。
  • 处理:
    • 只有最新且仍在执行的 stage 可为 active;等待确认、等待用户、target ready 和 failed 都是静态状态。
    • 工具开始事件必须等同一 toolCallIdtool_completed 收口;兼容旧流时可按后续同名完成事件兜底。
    • 思考摘要只展示用户可见摘要,且流结束或会话进入等待/完成/失败态后必须改成 done。
  • 验证:前端测试断言完成后 CreativeAgentProcessItem 不再存在 tone === 'active';后端测试确认工具开始/完成事件使用相同 toolCallId
  • 关联:src/components/creative-agent/creativeAgentViewModel.tsserver-rs/crates/api-server/src/creative_agent.rsdocs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md

creative-agent 会话切换要清理本地待确认模板

  • 现象:用户在一个智能创作会话中点开模板确认面板后,立即切到另一条创作会话,可能看到上一会话的确认面板残留。
  • 原因:模板确认面板的 pendingSelectionCreativeAgentWorkspace 本地 UI 状态,不属于后端 session 快照;组件复用时如果不监听 sessionId 清理,会跨会话泄漏。
  • 处理:工作区以 session?.sessionId 为边界清空 pendingSelection;服务端仍以 puzzleTemplateSelection / targetBinding 作为正式业务状态。
  • 验证:前端测试先点开模板确认面板,再 rerender 到另一 session,断言确认面板消失。
  • 关联:src/components/creative-agent/CreativeAgentWorkspace.tsxsrc/components/creative-agent/CreativeAgentWorkspace.test.tsx

视觉小说 VN-10 不要绕过平台资产引用

  • 现象:文档、封面、场景背景、角色立绘或音乐为了预览方便被写成 Data URL、裸对象路径、外部 URL 或本地临时文件路径。
  • 原因:前端上传与预览容易混在一起,若不走平台资产对象,SpacetimeDB 和长期草稿会被大文本或大二进制污染。
  • 处理:VN 资产统一用 /api/assets/direct-upload-tickets、OSS 直传、/api/assets/objects/confirm,长期状态只保存 assetObjectId/generated-* 引用;运行时图片用 ResolvedAssetImage 换签。
  • 验证:文档模式 sourceAssetIds 为平台资产 id;草稿中不出现 data:;图片和音乐字段为平台 generated 引用或 null。
  • 关联:docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.mdsrc/services/visual-novel-creation/visualNovelAssetClient.ts

视觉小说 VN-13 交接时不要再回头找旧迁移方案

  • 现象:接手视觉小说的人容易重新打开旧 TXT 迁移文档,把“外部平台工程迁入”误当成当前实现目标。
  • 原因:视觉小说历史资料里保留了很多迁移阶段的讨论,而当前真正的实现口径已经收口到 PRD、表目录、Prompt 工具说明、实现收口文档和负向扫描报告。
  • 处理:维护视觉小说时优先看 AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.mdSPACETIMEDB_TABLE_CATALOG.mdVISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.mdVISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.mdVISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.mdVN11_NEGATIVE_SCAN_REPORT_2026-05-07.md
  • 验证:新开发者只读这组文档即可继续维护,不需要把旧 TXT 迁移方案重新当作编码依据。
  • 关联:docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.mddocs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.mddocs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md

视觉小说公开广场不要触发登录刷新

  • 现象:未登录用户进入平台公开广场或从推荐流读取视觉小说公开作品时,前端可能先尝试 /api/auth/refresh,失败后再读取公开列表,导致无意义的鉴权噪声或 401 状态刷新。
  • 原因:公开只读接口如果复用默认 requestJson 选项,缺少 access token 时会先走静默 refresh。
  • 处理:视觉小说公开广场列表使用 skipAuth: trueskipRefresh: true;鉴权 mutation 仍保持默认鉴权链路。
  • 验证:执行 src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts,确认 /api/runtime/visual-novel/gallery 请求携带 skipAuth / skipRefresh,而 run、重生成和存档 mutation 仍走受保护路由。
  • 关联:src/services/visual-novel-runtime/visualNovelRuntimeClient.tsdocs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md

创作 Tab 语义迁移后,旧“新建作品”测试要改看智能创作首页

  • 现象:把 create 从旧创作中心切到 CreativeAgentHome 后,旧测试仍尝试在创作页找“新建作品”类型卡,导致用例失败或定位不到元素。
  • 原因:产品语义已经变成“创作 = 智能创作首页,草稿 = 旧作品架”,但测试夹具和 helper 还沿用旧入口。
  • 处理:把这类测试改成验证智能创作首页、快捷胶囊、抽屉与草稿 Tab;同时给 useRpgEntryLibraryDetail 这类恢复路径补上 setPlatformTabToDraft
  • 验证:定向 vitesteslinttypecheckcheck:encoding 都通过。
  • 关联:src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxsrc/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsxsrc/components/rpg-entry/useRpgEntryLibraryDetail.ts

server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块

  • 现象:在 server-rs 下无参数 cargo build 期望同时构建 spacetime-module,导致链接或构建范围误判。
  • 原因:workspace default-members 当前只包含 crates/api-serverSpacetimeDB module 有独立构建/发布方式。
  • 处理:默认 Rust 构建只覆盖原生 api-server;本地模块发布继续走 spacetime publish --module-path ... --build-options="--debug" / bindings 生成流程。
  • 验证:查看 server-rs/Cargo.toml default-members,并按相关 SpacetimeDB 文档执行模块构建。
  • 关联:server-rs/Cargo.tomldocs/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_bsatnprocedure_start_mut_txconsole_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-moduledocs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.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 启用了 sccache wrapper,但当前 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.tomlrustc-wrapper = "sccache";本地 npm run dev / npm run dev:spacetime / npm run dev:api-serverscripts/dev.mjs 给 Rust 子进程注入直通 wrapper,自动绕过项目默认 sccache,避免损坏的 daemon 阻断 spacetime publishapi-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-macrosccache ... 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 publishapi-server /healthz、主站 Vite 和后台 Vite 启动;冷启动后原始 cargo check -p api-servercargo check -p spacetime-module 能通过;sccache --show-stats 显示 Cache location oss, name: genarrative-sccache,证明原始 Cargo/Jenkins 路径仍可使用 sccache/OSS 缓存;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。
  • 关联:scripts/dev.mjsjenkins/Jenkinsfile.production-stdb-module-builddocs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.mddocs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md

生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本

  • 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
  • 原因:旧 Jenkins / 旧本地远端部署脚本文档仍作为历史经验保留。
  • 处理:生产相关操作先看 PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md,再按需追溯旧文档。
  • 验证:发布链路使用当前 deploy/systemddeploy/nginxscripts/deployjenkins/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.gzweb.tar.gz.sha256release-manifest.jsonGenarrative-Web-Deploy 使用 copyArtifacts 从指定 BUILD_JOB_NAME / BUILD_NUMBER_TO_DEPLOY 复制完整产物,不保留 WEB_ARTIFACT_ROOTWEB_ARTIFACT_SYNC_HOSTweb-artifact-pointer.txt 口径。
  • 验证:deploy 工作区应直接出现 build/<version>/web.tar.gzweb.tar.gz.sha256;后续仍由 scripts/deploy/production-web-deploy.sh 执行 checksum 校验和解压 smoke。
  • 关联:jenkins/Jenkinsfile.production-web-deploydocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

Jenkins 生产流水线拉 Git 统一走内网 SSH

  • 后续更新:2026-06-19 起常规构建 / 导入导出 / Full Build 流水线的 Jenkinsfile 内部 checkout 统一使用内网 SSH 地址 ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git 和凭据 genarrative-local-gitea-ssh,不再把 https://git.genarrative.world/git/GenarrativeAI/Genarrative.git 作为默认主源或 fallback,也不再配置公网 Git fallbackGenarrative-Server-Provision 仍是服务器初始化专用口径,Job 的 Pipeline script from SCM 和 Jenkinsfile 内部 checkout 都必须使用本机路径或目标 agent 可访问的内网 Git 源。
  • 现象:生产发布、数据库导入导出、服务器配置、构建或 Genarrative-Full-Build-And-Deploy 流水线执行 GitSCM checkout 时,如果 Jenkins 生成的 fetch 是 +refs/heads/*:refs/remotes/origin/*,公网 Git 链路可能在收包阶段以 git-remote-https died of signal 15curl 56 GnuTLS recv error (-9)early EOFinvalid index-pack output 失败;写死 127.0.0.1:3000 也会在当前执行 agent 不是 Gitea 所在机器时失败。
  • 原因:127.0.0.1 只代表当前执行阶段的 agent 自身;公网域名会绕外部链路并受公网代理、TLS、带宽和凭据影响。HTTP 私有仓库入口如果没有配置 Jenkins 凭据,会在 Git 插件日志中显示 No credentials specified 并以 Failed to authenticate user 失败。即使只使用内网 Git,如果 GitSCM 没有显式 refspec 并开启 CloneOption honorRefspec=trueJenkins Git 插件也会拉取所有分支。
  • 处理:运行于 linux && genarrative-buildGenarrative-Full-Build-And-Deploy 源码解析阶段、Genarrative-Web-Build / Genarrative-Api-Build / Genarrative-Stdb-Module-Build checkout 阶段,以及数据库导入导出流水线的首次 checkout([$class: 'GitSCM', ...]) 层统一使用 GIT_REMOTE_URL=ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.gitGIT_REMOTE_CREDENTIAL_ID=genarrative-local-gitea-sshGIT_REMOTE_FALLBACK_URL 留空。这些首次 checkout 都必须使用目标分支 refspec、CloneOption shallow=true depth=1 noTags=true honorRefspec=true。后续统一走 scripts/jenkins-checkout-source.sh,构建类流水线以 GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT=true 复用首次 GitSCM 带凭据浅克隆,只有指定 commit 不在浅克隆里时才通过同一 SSH 凭据继续 fetch 和加深;COMMIT_HASH 为空时继续 --depth=1 --no-tags,指定 commit 时也先保持 depth=1 校验,浅历史无法证明归属时才按 GENARRATIVE_JENKINS_CHECKOUT_DEEPEN_STEPS 逐步加深,最后才展开完整历史。发布流水线不得为了缩短 checkout 时间清空上游构建传入的 COMMIT_HASH
  • 验证:扫描本地 Jenkins live job config.xml,确认 SCM <url> 不再指向 https://git.genarrative.world/GenarrativeAI/Genarrative.git;扫描所有生产 Jenkinsfile 的首次 GitSCM checkout,确认 GIT_REMOTE_URLssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.gitGIT_REMOTE_CREDENTIAL_IDgenarrative-local-gitea-sshGIT_REMOTE_FALLBACK_URL 为空,userRemoteConfigs+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}CloneOptionhonorRefspec: true;重放 Jenkins 时 checkout 日志不应再出现 No credentials specified;扫描发布流水线确认传给 scripts/jenkins-checkout-source.shCOMMIT_HASH 未被硬编码为空;运行 bash -n scripts/jenkins-checkout-source.sh
  • 关联:jenkins/Jenkinsfile.production-full-build-and-deployjenkins/Jenkinsfile.production-web-buildjenkins/Jenkinsfile.production-api-buildjenkins/Jenkinsfile.production-stdb-module-buildjenkins/Jenkinsfile.production-web-deployjenkins/Jenkinsfile.production-api-deployjenkins/Jenkinsfile.production-stdb-module-publishjenkins/Jenkinsfile.production-server-provisionjenkins/Jenkinsfile.production-database-exportjenkins/Jenkinsfile.production-database-importscripts/jenkins-checkout-source.shdocs/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-exportjenkins/Jenkinsfile.production-database-import,确认 INCLUDE_TABLESCHUNK_SIZESERVER_BACKUP_DIRECTORYSMOKE_HEALTH_URL 等可选参数不再裸读。
  • 关联:docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.mdjenkins/Jenkinsfile.production-database-exportjenkins/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 HEADgit clean -fd,会把前面临时 chmod 的执行位还原为 Git 记录的 mode;若被直接执行的脚本在仓库里是 100644,二次 checkout 后仍不可执行。
  • 处理:需要直接以 scripts/*.sh 方式执行的 Jenkins 脚本应提交为 Git 100755;如果只想临时授权,必须放在 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-provisionscripts/prepare-server-provision-tools.shscripts/jenkins-checkout-source.shdocs/【开发运维】本地开发验证与生产运维-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 Fileslinux && genarrative-build 上使用固定内网 SSH 源和 Jenkins 凭据 genarrative-local-gitea-ssh checkout / 校验 SOURCE_BRANCH / COMMIT_HASH,并把 provision 脚本、scripts/deploy/**deploy/**.jenkins-source-commit stash 给目标 agent。Provision Target 下的 Receive Provision FilesPrepare Provision ToolsProvision Server 必须运行在目标部署 agentdevelopment 使用 linux && genarrative-dev-deployrelease 使用 linux && genarrative-release-deploy。目标 agent 不再需要 SOURCE_GIT_REMOTE_URL,也不再 checkout Git。
  • 验证:Jenkins 日志中应先看到 Prepare Provision Fileslinux && genarrative-build 上完成源码准备和 stash 'server-provision-files',再看到 Provision Target 下的 Receive Provision FilesPrepare Provision ToolsProvision Server 在目标 dev / release agent 上运行;目标阶段日志不应出现 Git checkout、SOURCE_GIT_REMOTE_URLGit 主地址拉取失败...改用备用地址https://git.genarrative.world/GenarrativeAI/Genarrative.git
  • 关联:jenkins/Jenkinsfile.production-server-provisionscripts/prepare-server-provision-tools.shdocs/【开发运维】本地开发验证与生产运维-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_VERSIONSPACETIME_DOWNLOAD_ROOT 推导出的版本且 standalone 同时存在时,复制 ${SPACETIME_ROOT}/bin 并生成 wrapper。只有缺失、不可执行或版本不匹配时,才查 PROVISION_DOWNLOADS_DIR 或下载源。
  • 验证:运行 bash scripts/check-server-provision-tools.sh;Jenkins 日志应先出现“检查目标机 ...”,已有版本命中时出现“复用目标机已有 ...”,且不出现“下载 ...”。
  • 关联:scripts/prepare-server-provision-tools.shjenkins/Jenkinsfile.production-server-provisiondocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

个人任务 scope 不得扩成 work/site/module

  • 现象:个人任务配置为 work / site / module 后进度串桶或静默按 0 处理。
  • 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
  • 处理:Admin 任务配置页不展示范围选择,保存时固定 scopeKind: 'user'API 和领域构造层拒绝非 User
  • 验证:非 user scope 返回错误;相关测试覆盖 Site / Module / Work 被拒绝。
  • 关联:docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.mddocs/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 CONFLICTdetails.message泥点余额不足
  • 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
  • 验证:PuzzleResultView 单测覆盖发布弹窗内展示 泥点余额不足
  • 关联:src/components/puzzle-result/PuzzleResultView.tsxdocs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.mddocs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md

拼图发布检查阶段会在事件落库时炸 wasm

  • 现象:拼图发布在“发布检查”环节直接报 The module instance encountered a fatal errorwasm 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.rsserver-rs/crates/api-server/src/puzzle/handlers.rsserver-rs/crates/spacetime-client/src/module_bindings/puzzle_event_table.rs

拼图会过早进入待发布态,结果页可能空图但仍显示可发布

  • 现象:拼图创作有时刚结束就跳到“待发布”结果页,但结果页里的正式图还是空的,发布检查随后又会拦住,用户会感觉“已经完成了却又不能发布”。
  • 原因:拼图的待发布判定太弱,build_result_preview / validate_publish_requirementsis_puzzle_session_snapshot_publish_ready 只检查了作品名、简介、标签、关卡名和 cover 图,没有要求 level_scene_image_srcui_spritesheet_image_srclevel_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.rsserver-rs/crates/api-server/src/puzzle/tags.rsserver-rs/crates/api-server/src/puzzle/draft.rssrc/components/platform-entry/platformPuzzleDraftRecoveryModel.tssrc/components/puzzle-result/PuzzleResultView.tsx

WebGL 画布在高 DPR 移动端放大溢出

  • 现象:抓大鹅试玩入口进入后,3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
  • 原因:WebGLRenderer.setPixelRatio(...) 会把绘图缓冲区乘上设备 DPR;如果没有给 renderer.domElement 单独设置 CSS width/height: 100% 和绝对铺满,浏览器可能把高 DPR 缓冲区尺寸当成页面显示尺寸。
  • 处理:中心棋盘和托盘预览的 WebGL canvas 统一套用 position:absolute; inset:0; width:100%; height:100%; display:blockrenderer.setSize(..., false) 只负责同步绘图缓冲区。
  • 验证:强制移动端 390x844、DPR 2 截图,确认棋盘左右边界在视口内,canvas CSS 尺寸等于容器尺寸,内部 width/height 属性可大于 CSS 尺寸。
  • 关联:src/components/match3d-runtime/Match3DPhysicsBoard.tsxdocs/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_statussubscriptionKey 只做 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.rsdocs/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.tomlnpm 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.rssrc/components/match3d-runtime/Match3DRuntimeShell.tsxdocs/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.rssrc/components/match3d-result/Match3DResultView.tsxdocs/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_jsonupdate_match3d_work / publish_match3d_work 保留该字段;API work summary/detail 映射反序列化为 generatedItemAssets。前端保持“本次 draft 优先,重进 profile 兜底”的读取顺序。
  • 验证:cargo test -p spacetime-client match3d --manifest-path server-rs/Cargo.tomlcargo test -p api-server match3d --manifest-path server-rs/Cargo.tomlnpm run test -- src/components/match3d-result/Match3DResultView.test.tsx
  • 关联:server-rs/crates/spacetime-module/src/match3d/*server-rs/crates/spacetime-client/src/mapper.rsserver-rs/crates/api-server/src/match3d.rssrc/components/match3d-result/Match3DResultView.tsxdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

抓大鹅试玩和正式运行态不要只读草稿页本地素材预览

  • 现象:结果页能看到生成的物品图片,但点击试玩或从推荐 / 公开作品进入正式抓大鹅时,局内仍显示默认积木素材。
  • 原因:结果页本地 assetDrafts 和作品 profile 的 generatedItemAssets 可能不同步;推荐流内嵌运行态若只读卡片摘要,卡片缺素材时会把已持久化 profile 素材丢掉;点击试玩时 React state 异步更新也可能让运行态第一帧读取旧 match3dProfile
  • 处理:删除、批量新增、音效生成或封面引用物品素材后,都把当前 generatedItemAssets 写回作品 profileMatch3DResultView 合并同 itemId 的 draft/profile 素材,用 profile 已有 imageViews[]、首图引用、backgroundMusicbackgroundAsset 补齐旧 draft;点击试玩前把试玩可用物品种类通过 itemTypeCountOverride 降到已生成 2D 素材数量;推荐流内嵌运行态启动前若卡片摘要没有物品图片素材,补读 getMatch3DWorkDetail(profileId) 并把详情资产传给 Match3DRuntimeShellPlatformEntryFlowShellImpl 需要维护 match3dRuntimeProfile,在 startMatch3DRunFromProfile 创建 run 后立即锁定本次完整 profile,runtime 渲染时优先按 run.profileId 使用这份 profile,而不是等待普通 match3dProfile state 下一轮刷新。同 profile 下已有 generatedItemAssets 时不能因为图片完整性判断失败就覆盖为空数组。判断是否需要补读详情时只看 imageViews[]imageSrc/imageObjectKey;背景、音乐、容器 UI 是附属运行态资产,不能单独证明物品素材已完整。
  • 验证:执行 npm run test -- src/components/match3d-result/Match3DResultView.test.tsxnpm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsxnpm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx,并检查历史草稿和公开 M3 作品的 Network 响应里 generatedItemAssets[].imageViews/imageSrc/imageObjectKey
  • 关联:src/components/match3d-result/Match3DResultView.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/match3d-runtime/Match3DPhysicsBoard.tsxdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

抓大鹅 UI 背景和容器只在顶层字段时也要传进运行态

  • 现象:抓大鹅草稿 / 推荐卡片响应里已有 generatedBackgroundAsset,结果页 UI 预览能看到纯背景图和容器图,但进入试玩或正式局内仍显示默认渐变背景和默认圆形容器。
  • 原因:部分链路把 UI 资产只放在作品顶层 generatedBackgroundAsset / backgroundImageObjectKey,没有同步放进首个 generatedItemAssets[].backgroundAsset;如果运行态入口只传 generatedItemAssetsbackgroundImageSrcMatch3DRuntimeShell 就拿不到 containerImageObjectKey
  • 处理:PlatformMatch3DGalleryCardmapPublicWorkDetailToMatch3DWorkresolveMatch3DRuntimeGeneratedBackgroundAssetMatch3DRuntimeShell 都必须保留并传递顶层 generatedBackgroundAsset;运行态背景读取顺序为 backgroundImageSrc / 顶层 generatedBackgroundAsset.image* / generatedItemAssets[].backgroundAsset.image*,容器读取顺序为顶层 generatedBackgroundAsset.containerImage* / generatedItemAssets[].backgroundAsset.containerImage*
  • 验证:执行 npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsxnpm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "Match3D runtime";浏览器 Network 中背景和容器 generated path 应先请求 /api/assets/read-url 换签,局内出现 match3d-background-imagematch3d-container-image 对应图片。
  • 关联:src/components/match3d-runtime/Match3DRuntimeShell.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/rpg-entry/rpgEntryWorldPresentation.tsdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

抓大鹅容器参考图必须进入 edits multipart image 并接管棋盘外观

  • 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 pot-fused-reference.png,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。
  • 原因:容器参考图必须进入 gpt-image-2 /v1/images/edits multipart image part,并配合强 prompt 锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 rounded-full 锅壳和 overflow-hidden,生成图也会被默认视觉覆盖或裁掉。
  • 处理:抓大鹅 1:1 容器 UI 图统一调用 VectorEngine POST /v1/images/edits,参考 public/match3d-background-references/pot-fused-reference.png 的透明容器图由后端作为 image part 上传;该参考图属于后端生图协议输入,需通过 include_bytes! 编译进 api-server,不能在运行时按当前工作目录读取 public/Match3DRuntimeShell 在容器图换签并成功加载后,把棋盘外壳切为透明和 overflow-visible,只在容器缺失或加载失败时使用默认圆形容器。
  • 验证:执行 cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.tomlcargo test -p api-server match3d_background --manifest-path server-rs/Cargo.tomlnpm 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.rsserver-rs/crates/api-server/src/match3d.rssrc/components/match3d-runtime/Match3DRuntimeShell.tsxdocs/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.tsxsrc/services/assetReadUrlService.tsdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

法律文档弹窗通过 portal 挂载时要显式带平台主题

  • 现象:登录弹窗内点击协议链接打开法律文档时,弹窗可能继承不到 platform-theme--light/dark 变量,或者层级低于登录遮罩导致不可见。
  • 原因:UnifiedModal 默认通过 portal 挂到 document.body,不再处于原页面的主题容器内;登录弹窗自身又使用较高 z-index。
  • 处理:法律文档弹窗组件应支持传入 platformThemeoverlay 上显式挂 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.tsxdocs/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.tssrc/services/miniGameDraftGenerationProgress.test.tsdocs/【玩法创作】平台入口与玩法链路-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。支付成功后只通过通知里的 out_trade_no 确认本地 pending 订单,并保存 transaction_idprofile_recharge_order.provider_transaction_id
  • APIv3 通知成功应答使用 HTTP 204 No Content,不要沿用 V2 XML 成功报文;失败仍返回 4XX/5XX 让微信重试。
  • 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
  • 关联:server-rs/crates/api-server/src/wechat_pay.rsdocs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.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/jsonContent-Type: application/jsonUser-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.rsdocs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md

容器公开列表压测不要靠继续抬并发吃满 CPU

  • 现象:2C / 2G 容器压测公开 gallery list 时,api-server CPU 仍有余量,看起来像可以继续提高 GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS 或 Nginx limit_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=320GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320 作为稳定上限。若要继续提升吞吐,优先减少高频公开 GET 的 tracking 写入、做采样或改成批量/异步聚合;不要单纯放大入口并发。
  • 验证:宿主机 k6 打 http://127.0.0.1:18080PEAK_RPS=1000 等价约 2000 HTTP req/s320 档无 dropped iterations、无 5xx、无 OOM200 请求 request_time p95 约 0.292s。336 / 352 档 p95 升到约 0.31s / 0.32sSpacetimeDB 内存尾部可到约 880MiB / 896MiB
  • 关联:deploy/container/nginx.confdeploy/container/api-server.env.exampledeploy/container/README.mdserver-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_eventtracking_daily_stat 均更新。
  • 关联:docs/【开发运维】本地开发验证与生产运维-2026-05-15.mdserver-rs/crates/api-server/src/tracking.rsserver-rs/crates/spacetime-module/src/runtime/profile.rs

后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码

  • 现象:后台“表查询”查看 profile_recharge_order 时,kindstatus 显示为空数组 [],例如充值订单原始行里 points_60 的类型和状态都不可读。
  • 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态 [variant_index, []];后台通用 normalizer 曾把任何 [0, value] 都当作 Option::Some(value) 展开,导致 [0, []] 最终只剩 []
  • 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如 profile_recharge_order.kind 映射为 points / membershipprofile_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.rsdocs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md

充值订单过期补偿不要放进外部生成 worker

  • 现象:外部生成 worker/controller 扩容后,微信充值过期查单和关单流量也被同步放大;排查时还会误去外部生成 worker 日志里找支付过期任务。
  • 原因:支付过期是账户资金链路,不是外部内容生成队列;旧实现把充值过期轮询 worker 挂在通用后台任务启动函数里,非 HTTP 角色也会启动。
  • 处理:充值订单过期由 SpacetimeDB 原生 profile_recharge_order_expiration_timer 到点把 pending 改为 expired,只有 HTTP api-server 订阅 profile_recharge_orderPending -> Expired 更新并查微信补偿。未支付终态本地保持 expired,不要再改写成 closed;微信成功支付通知或补偿查单仍可把 Expired -> Paid 入账。
  • 验证:确认 GENARRATIVE_PROCESS_ROLE=external-generation-worker / external-generation-controller 不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生 expiredHTTP api-server listener 记录 expiration_checked_at 或补入账。
  • 关联:server-rs/crates/api-server/src/profile_recharge_expiration_listener.rsserver-rs/crates/spacetime-module/src/runtime/profile.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md

抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布

  • 现象:草稿页预览模型失败并报 GL_INVALID_ENUM: Invalid cap.,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
  • 原因:历史结果页手动 重新生成 会把 Hyper3D/Rodin 的外部 CDN 下载链接直接保存到 generatedItemAssets[].modelSrc,同时 modelObjectKey 为空。外部链接可能过期、跨域、返回 HTML 错误页或非 GLB 内容;前端预览和运行态不能把它当作稳定私有资产。
  • 处理:该问题只适用于旧数据。结果页发现 status = model_readymodelSrc = https://... 且无 modelObjectKey 时,可调用 POST /api/creation/match3d/works/{profileId}/generated-models 做一次性转存;新草稿和批量新增不得继续生成或依赖 GLB。若历史半修复数据同时保留外部 modelSrc 和平台 modelObjectKey,旧模型预览读取层优先用 modelObjectKey
  • 验证:npm run test -- src\components\match3d-result\Match3DResultView.test.tsxnpm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsxnpm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsxcargo test -p api-server match3d_model_download --manifest-path server-rs\Cargo.toml,并检查修复后响应中的 generatedItemAssets[].modelObjectKey 不为空。
  • 关联:server-rs/crates/api-server/src/match3d.rssrc/components/match3d-result/Match3DResultView.tsxsrc/components/match3d-result/Match3DModelPreview.tsxsrc/components/match3d-runtime/Match3DPhysicsBoard.tsxdocs/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 + 1col = itemIndex % 2 * 5 + viewIndex + 1。发布前按 image_ready且有imageViews[]imageSrc/imageObjectKey的生成素材数量阻断不足难度;试玩不阻断,但通过itemTypeCountOverride 自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。
  • 验证:npm run test -- src\components\match3d-result\Match3DResultView.test.tsxcargo 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.tsxsrc/services/match3d-runtime/match3dRuntimeClient.tsserver-rs/crates/module-match3d/src/application.rsserver-rs/crates/spacetime-module/src/match3d.rsdocs/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-serverslice_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.rsdocs/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.tsxdocs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md

草稿页卡片有真实素材但仍显示黑卡先查摘要字段

  • 现象:草稿页拼图卡片没有关卡图背景,抓大鹅卡片没有背景图或物品图背景,甚至兜底视觉也退回黑色面板。
  • 原因:拼图列表摘要若不下发 levels,前端拿不到关卡 coverImageSrc / 候选图;抓大鹅列表摘要若只提供公开 URL、不保留 generatedBackgroundAssetgeneratedItemAssets 中的 object key,前端无法换签读取私有生成图。卡片封面组件如果自带暗色默认背景,也会让兜底失败时看起来仍是黑卡。
  • 处理:拼图 map_puzzle_work_summary_response 必须保留 levels;草稿页优先用关卡 coverImageSrc,再用候选图。抓大鹅货架封面解析必须读取 backgroundImageObjectKeygeneratedBackgroundAsset.imageObjectKey/containerImageObjectKeygeneratedItemAssets[].imageObjectKeyimageViews[].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.tsxcargo test -p api-server puzzle_work_summary_response_keeps_levels_for_shelf_cover --manifest-path server-rs\Cargo.tomlnpm run typecheck
  • 关联:src/components/custom-world-home/creationWorkShelf.tssrc/components/CustomWorldCoverArtwork.tsxserver-rs/crates/api-server/src/puzzle.rsdocs/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_codemetadata_json 时按 {} 兼容。
  • 关联:docs/technical/USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.mddocs/technical/SPACETIMEDB_TABLE_CATALOG.md

公开作品详情深链找不到作品不能停在空详情页

  • 现象:直接访问 /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.tsnpm 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.mdsrc/routing/runtimeNotFoundRecovery.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsx

拼图 UI 背景只有 objectKey 时不要回退默认 UI

  • 现象:拼图草稿页、试玩和正式运行态都显示默认 UI,或者只在结果页看到生成图,进入试玩后又回到默认背景。
  • 原因:uiBackgroundImageSrc 可能为空而真实生成结果只写了 uiBackgroundImageObjectKey;如果前端和运行态只读 src,或者本地试玩 / 正式 run 没把 objectKey 一起传递,就会丢掉已有背景。
  • 处理:统一通过一个解析入口把 uiBackgroundImageSrc || uiBackgroundImageObjectKey 归一到可展示路径;本地试玩和正式运行态都要保留 uiBackgroundImageObjectKey,并在 uiBackgroundImageSrc 为空时换签读取。
  • 验证:结果页 UI Tab、startLocalPuzzleRunPuzzleRuntimeShell 都应在仅有 objectKey 时显示生成背景,不再回落默认 UI。
  • 关联:src/services/puzzle-runtime/puzzleUiBackgroundSource.tssrc/components/puzzle-result/PuzzleResultView.tsxsrc/services/puzzle-runtime/puzzleLocalRuntime.tssrc/components/puzzle-runtime/PuzzleRuntimeShell.tsxserver-rs/crates/module-puzzle/src/application.rs

拼图 UI 背景提示词或作品元信息异常先查首关命名契约

  • 现象:拼图草稿生成完成后,第一关名称或作品名称变成 levelNam / levelName 这类字段名片段,或 素材配置 > UI 里显示的 UI背景提示词 像前端或后端模板拼接,而不是 AI 生成的视觉提示词。
  • 原因:首关命名 LLM 旧契约只返回 levelName,自动 UI 背景阶段只能用作品名、作品描述、关卡描述和标签拼接确定性兜底提示词;如果模型返回截断 JSON,解析层还可能把 levelNam 这类字段名片段当作普通英文关卡名归一化通过。
  • 处理:首关命名 LLM 契约必须同时返回 {"levelName":"...","workDescription":"...","workTags":["..."],"uiBackgroundPrompt":"..."};解析层必须拒绝 levelNamlevelNameworkDescriptionworkTagsuiBackgroundPrompt 等字段名片段作为关卡名。草稿自动 UI 背景生成优先使用该 AI 提示词,作品描述和 6 个作品标签默认填入草稿;视觉精修请求若返回新提示词或作品元信息则覆盖文本请求结果,否则保留文本请求结果。前端文本框只展示已保存的 uiBackgroundPrompt 或用户编辑值,字段为空时不展示本地兜底模板。
  • 验证:执行 cargo test -p api-server puzzle_level_naming_parser --manifest-path server-rs\Cargo.tomlcargo test -p api-server puzzle_first_level_name --manifest-path server-rs\Cargo.tomlcargo test -p api-server puzzle_initial --manifest-path server-rs\Cargo.tomlnpm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx
  • 关联:server-rs/crates/api-server/src/prompt/puzzle/level_name.rsserver-rs/crates/api-server/src/puzzle.rssrc/components/puzzle-result/PuzzleResultView.tsxdocs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md

拼图 / 抓大鹅 UI 背景重生成报 No such procedure 先查 SpacetimeDB 版本漂移

  • 现象:拼图或抓大鹅结果页点击 重新生成 UI 背景时报 No such procedure,常见位置是泥点预扣、save_puzzle_ui_background 或 Match3D 草稿写回。
  • 原因:api-serverspacetime-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.tomlcargo test -p api-server match3d_fallback_work_profile_keeps_generated_background_asset --manifest-path server-rs\Cargo.tomlnpm run dev:api-server 后检查 /healthz
  • 关联:server-rs/crates/api-server/src/asset_billing.rsserver-rs/crates/api-server/src/match3d.rsdocs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.mddocs/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.tsxsrc/components/puzzle-runtime/PuzzleRuntimeShell.test.tsxdocs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md

推荐页嵌入拼图通关结算不要放在运行态内部 absolute 层

  • 现象:推荐页里玩拼图通关后,结算面板只显示上半部分,排行榜或下一关按钮被截断。
  • 原因:推荐页把运行态放在滑动作品卡的视觉区内,platform-recommend-swipe-pageplatform-recommend-swipe-card__visualplatform-recommend-runtime-viewport 都是 overflow: hidden;拼图通关结算如果仍是运行态内部 absolute inset-0 弹层,就只能在半屏卡片区域里显示。
  • 处理:PuzzleRuntimeShellembedded 模式下把通关结算层通过 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.tsxsrc/index.csssrc/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.tssrc/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.tsxdocs/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.tsxcargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.tomlnpm run dev:api-server 后检查 /healthz
  • 关联:server-rs/crates/api-server/src/puzzle/draft.rsserver-rs/crates/api-server/src/puzzle/vector_engine.rssrc/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.tsxcargo test -p api-server puzzle --manifest-path server-rs\Cargo.toml
  • 关联:src/components/custom-world-home/creationWorkShelf.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/puzzle-result/PuzzleResultView.tsxserver-rs/crates/api-server/src/puzzle/mappers.rsserver-rs/crates/spacetime-module/src/puzzle.rs

2026-05-22 补充:结果页关卡详情的“关卡测试”不能把单关 draft 传给父级再调用 updatePuzzleWorkupdatePuzzleWork 会同步 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 写入生成页 metadatapuzzleAiRedraw=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.tsxcargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml
  • 关联:src/services/miniGameDraftGenerationProgress.tssrc/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsxsrc/components/puzzle-result/PuzzleResultView.tsxserver-rs/crates/api-server/src/puzzle/draft.rsserver-rs/crates/api-server/src/puzzle/generation.rs

Jenkins 数据库导入导出脚本先补 Node 工具链 PATH

  • 现象:Genarrative-Database-ImportGenarrative-Database-Export 运行到迁移脚本时,bashnode: 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-ImportGenarrative-Database-Export,日志应先打印 jenkins-toolchainnode=... 解析结果,而不是在迁移中途报 node: command not found
  • 关联:scripts/jenkins-prepare-toolchain-env.shjenkins/Jenkinsfile.production-database-importjenkins/Jenkinsfile.production-database-exportdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

Runtime bootstrap secret 原文不能进入 WASM 或发布归档

  • 现象:下载 Jenkins Stdb artifact 或检查 spacetime_module.wasm 能找到原始 bootstrap secretStdb 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 IDBuild 只计算摘要,WASM、artifact 和 copyArtifacts 不含原文,Stdb release manifest 记录 migration_bootstrap_secret_sha256Publish 重新读取同一 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 门禁。人工构建自动生成的原文只放 gitignored server-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 -nnode --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs scripts/deploy/production-runtime-writer-identity-rotate.mjsnpm 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.rsscripts/dev.mjsscripts/build-production-release.shscripts/deploy/production-stdb-publish.shscripts/deploy/production-runtime-writer-identity-rotate.mjsjenkins/Jenkinsfile.production-stdb-module-buildjenkins/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 的 powershell step 依赖一个隐式命令解析/启动路径,在这台 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-buildCheckoutBuild Stdb Module 两处 powershell step 收口成 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-builddocs/【开发运维】本地开发验证与生产运维-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-provisiondocs/【开发运维】本地开发验证与生产运维-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 tarballGitHub 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-clibin/current/spacetimedb-standalone,不要再把 update installer 当成最终离线包执行。
  • 验证:Jenkins 目标机日志不再出现 unexpected argument '-y'unknown command name for spacetimedb-update multicall binary,后续应继续检查 bin/current/spacetimedb-clibin/current/spacetimedb-standalone 是否生成。
  • 关联:scripts/prepare-server-provision-tools.shjenkins/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.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md 和生成绑定对齐,再执行清库重建。
  • 验证:npm run check:spacetime-schema 先通过,再重启 npm run dev -- --clear-database --no-interactive,最后检查 /v1/ping/healthzGET /api/creation-entry/config
  • 关联:server-rs/crates/spacetime-module/src/migration.rsdocs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.mdscripts/dev.mjs

QQ 浏览器发现页推荐封面全不显示先查 aspect-ratio 兜底

  • 现象:发现页的“推荐”子频道作品卡标题、作者和数据正常,但所有封面图不显示,常见于 QQ 浏览器 / X5 等旧移动内核。
  • 原因:公开作品卡封面内部图片是绝对铺满,容器原本主要依赖 Tailwind aspect-video / CSS aspect-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.tsxnpm run typechecknpm run check:encoding
  • 关联:src/index.csssrc/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/rpg-entry/rpgEntryWorldPresentation.tsdocs/【玩法创作】平台入口与玩法链路-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.tssrc/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/platform-entry/PlatformWorkDetailView.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

生成中草稿恢复要按后端时间戳计时

  • 现象:拼图或抓大鹅草稿生成中刷新网页后,进入生成页的“已耗时”从 0 秒 重新开始;另一类旧问题是后端 progressPercent=88 时总进度首帧直接跳到 88%
  • 原因:生成页恢复曾把展示态 startedAtMs 重置为进入页面的当前时间,导致计时不跟随后端真实生成时刻;拼图总进度也曾把后端里程碑当作百分比地板,导致步骤刚切换就抬高总进度。
  • 处理:恢复生成中的草稿时,展示起点使用后端 session updatedAt 或作品摘要 updatedAt88/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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxsrc/services/miniGameDraftGenerationProgress.tsdocs/【玩法创作】拼图生成页进度口径-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.tsxsrc/components/custom-world-home/creationWorkShelf.tsdocs/【玩法创作】平台入口与玩法链路-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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

汪汪声浪草稿试玩不要写正式 run

  • 现象:如果草稿结果页试玩和发布后 runtime 共用同一写成绩路径,未发布或未确认资源的草稿试玩会污染正式单局、排行榜和作品统计。
  • 原因:BarkBattleRuntimeShell 同时承担草稿预览和发布后运行态,需要由调用方显式传入 runtimeMode 区分是否写正式 run。
  • 处理:草稿结果页试玩保持 runtimeMode=draft,只做本地预览;发布成功后先进入 /works/detail?work=BB-xxxxxxxx,再从详情页以 runtimeMode=published 进入正式 runtime,并在开始/结算时分别调用 startBarkBattleRunfinishBarkBattleRun
  • 验证:草稿试玩不触发 start / finish run;正式 runtime 必须先通过麦克风授权,再写 start run 和结算派生指标。
  • 关联:src/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/games/bark-battle/ui/BarkBattleRuntimeShell.tsxsrc/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.tsxnpm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。
  • 关联:src/components/bark-battle-creation/BarkBattleConfigEditor.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【玩法创作】平台入口与玩法链路-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.tsxsrc/games/bark-battle/application/BarkBattleConfig.tssrc/games/bark-battle/ui/BarkBattleRuntimeShell.tsx

Jenkins Web 构建公开作品号导出缺失优先补 publicWorkCode

  • 现象:Genarrative-Web-Buildnpm run build:production-release -- --component web 阶段失败,Rollup 报 "buildJumpHopPublicWorkCode" is not exported by "src/services/publicWorkCode.ts",但导入方 rpgEntryWorldPresentation.tsPlatformEntryFlowShellImpl.tsx 已经引用该玩法公开码函数。
  • 原因:玩法分支合并时容易只带入新玩法的 publicWorkCode.ts 导出,覆盖或遗漏另一个玩法的公开码 builder / matcherVite 构建会在静态导出检查阶段直接失败。
  • 处理:在 src/services/publicWorkCode.ts 中保持每个玩法的 build<Play>PublicWorkCodeisSame<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.tssrc/components/rpg-entry/rpgEntryWorldPresentation.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

跳一跳前端壳层接线不要只合渲染分支

  • 现象:npm run typecheck 大量报 setJumpHopSessionjumpHopRunjumpHopGalleryEntriesmapJumpHopWorkToPublicWorkDetail 不存在,以及 "jump-hop-runtime" is not assignable to SelectionStage;即使 typecheck 过了,分享或刷新 /runtime/jump-hop?work=... 仍可能掉回首页。
  • 原因:跳一跳工作台、生成页、结果页、runtime 和推荐流渲染分支已经合入 PlatformEntryFlowShellImpl.tsx,但平台壳层状态、public detail mapper、SelectionStage union 与 appPageRoutes.ts 阶段路由映射没有一并合入;发现页卡片分类也没有先判断 isJumpHopGalleryEntry,导致 fallback 访问 RPG themeMode
  • 处理:platformEntryTypes.ts 必须注册 jump-hop-workspace/generating/result/runtime/gallery-detailappPageRoutes.ts 必须补 /creation/jump-hop/workspace/creation/jump-hop/generating/creation/jump-hop/result/gallery/jump-hop/detail/runtime/jump-hopPlatformEntryFlowShellImpl.tsx 必须持有 JumpHop session/work/run/gallery/runtimeReturnStage/generationState/error/busy,并提供 mapJumpHopWorkToPublicWorkDetailRpgEntryHomeView.tsx 的公开卡片类型描述要给 JumpHop 单独返回 跳一跳
  • 验证:npm run typecheck,并跑 npm test -- src/routing/appPageRoutes.test.ts 覆盖 JumpHop 阶段路径。
  • 关联:src/components/platform-entry/platformEntryTypes.tssrc/routing/appPageRoutes.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/rpg-entry/RpgEntryHomeView.tsxdocs/【玩法创作】平台入口与玩法链路-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.rsdocs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

跳一跳宝可梦主题地块图集 safety rejection 只做专项改写

  • 现象:跳一跳草稿使用“宝可梦 / Pokemon / 皮卡丘 / 精灵球”等主题时,背景底图和返回按钮可能已生成成功,但地块图集的 VectorEngine 请求返回 Your request was rejected by the safety system,日志里 failure_context="跳一跳地块图集生成失败"status=429code="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.rsdocs/【玩法创作】平台入口与玩法链路-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-01tile-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.rssrc/components/jump-hop-runtime/JumpHopRuntimeShell.tsxsrc/components/jump-hop-result/JumpHopResultView.tsx

跳一跳落点辅助标识不要再用舞台高度常量拍脑袋投影

  • 现象:按住蓄力时落点辅助标识虽然会动,但看起来像静态点位漂移,和真实可落地的位置对不上。
  • 原因:辅助标识如果只按 stageSize.height 和一个固定比例估算投影距离,再去跟拖拽向量合成,就会和当前地块到目标地块的真实屏幕跨度脱节;三维场景层级过高时还会把辅助点直接盖住。
  • 处理:辅助标识必须使用当前地块与目标地块之间的真实屏幕距离和后端 chargeToDistanceRatio 做投影,再映射到屏幕坐标;它只作为调参验证层随按下显示、松手或取消隐藏,不参与后端裁决和作品配置;同时把辅助层 z-index 放到三维角色层之上,避免被场景层遮挡。
  • 验证:半程蓄力时辅助点应落在当前地块和目标地块之间,完整蓄力时应逼近目标地块中心;运行态截图里辅助点必须始终压在地块与角色之上。
  • 关联:src/services/jump-hop/jumpHopRuntimeModel.tssrc/components/jump-hop-runtime/JumpHopRuntimeShell.tsx

跳一跳长按蓄力不能再消费拖拽方向

  • 现象:跳一跳改成长按蓄力后,如果前端或后端仍消费 dragVectorX/dragVectorY,玩家手指轻微移动就会改变跳跃方向,和“始终朝下一块中心跳”的体验不一致。
  • 原因:历史弹弓拖拽版本把屏幕拖拽方向作为正式裁决输入,契约字段仍为兼容旧客户端保留,容易被误认为仍是当前玩法规则。
  • 处理:前端运行态只用长按时长提交 dragDistance 兼容字段,不再发送方向字段;落点预测按当前地块中心到下一块地块中心的方向投影。后端 module-jump-hop 即使收到旧客户端 dragVectorX/dragVectorY 也必须忽略,只按当前地块到下一块地块中心的单位向量裁决。
  • 验证:前端回归测试覆盖手指移动不改变提交方向、预测落点忽略旧方向字段;后端领域测试覆盖旧客户端传错误方向时仍按下一块中心命中。
  • 关联:src/services/jump-hop/jumpHopRuntimeModel.tssrc/components/jump-hop-runtime/JumpHopRuntimeShell.tsxserver-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/configjump-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.mjsdocs/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 完成外部副作用:调用 VectorEngine gpt-image-2-all,用 GeneratedImageAssetAdapter 准备 PutObject,上传 OSS 私有对象,调用 confirm_asset_objectbind_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.rsserver-rs/crates/spacetime-client/src/wooden_fish.rssrc/components/ResolvedAssetImage.tsxsrc/services/assetReadUrlService.ts

生成页背景视频要固定全屏并显式触发播放

  • 现象:生成页明明带了 media/create_bg_video.mp4,但移动端或某些内核里只看到静态首帧,或视频层跟着局部容器滚动,被白色面板压住后看起来像没加载。
  • 原因:仅靠 autoPlay/loop/muted/playsInline 并不稳定;视频如果仍挂在局部容器里,还会被页面面板和遮罩吞掉。某些浏览器初始化后也会停在 paused=true
  • 处理:背景视频必须放到 fixed inset-0 的全屏底层容器里,外层页面用 isolate / 透明底控制叠层;挂载后显式尝试 play(),并在 loadeddatacanplay 和页面聚焦时再次触发,避免只停首帧。
  • 验证:移动端视口检查视频 rect 应覆盖整个视口,paused 应最终变为 falsecurrentTime 应持续前进。
  • 关联:src/components/GenerationProgressHero.tsxdocs/【玩法创作】生成页圆环布局口径-2026-05-23.md

跳一跳结果页直达时不要把恢复面板当成空白页

  • 现象:浏览器直接打开 /creation/jump-hop/result,如果没有 sessionIdprofileIddraftIdworkId,页面以前会看起来像空白,容易误判成结果页坏了。
  • 原因:跳一跳结果页恢复原先只盯 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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/planning/【玩法创作】创作流程统一总计划-2026-05-30.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-xstext-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-degreesdata-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-confignpm run dev:spacetime -- --no-interactivehttp://127.0.0.1:3101/v1/ping 应保持 200。
  • 关联:scripts/dev.mjsscripts/dev.test.tsdocs/【开发运维】本地开发验证与生产运维-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.mjsscripts/dev.test.tsdocs/【开发运维】本地开发验证与生产运维-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.localspacetime.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 返回 200curl.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.mjsserver-rs/crates/spacetime-client/src/lib.rsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

创作作品架消失先查入口配置 procedure 与本地库权限

  • 现象:寓教于乐或创作中心下草稿 / 已发布作品突然整块消失,GET /api/creation-entry/config 返回 502details 中为 No such procedure
  • 原因:本地 .env.localspacetime.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.rsserver-rs/crates/api-server/src/creation_entry_config.rsdocs/【开发运维】本地开发验证与生产运维-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.rsserver-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 请求,curl 5/5 成功,但 reqwest/hyper 会间歇性超时;固定 40.160.33.47 也只能改善,不能根治。
  • 处理:不要优先关闭 multipart,也不要直接把 SendRequest 解释成上游业务拒绝。VectorEngine 图片 generations / edits 上游 POST 单独使用 libcurl;参考图下载和响应图片 URL 下载仍用 reqwest。send 阶段 timeout / connect error 在 platform-image 内最多重试 5 次,使用指数退避和短抖动;日志字段 attemptmax_attemptsretry_delay_msreference_image_bytes_totalrequest_params 是定位依据。

api-server libcurl / OpenSSL 3.2 runtime

  • 症状:release 部署新 api-server 后服务反复 exit-codeLD_TRACE_LOADED_OBJECTS=1 /opt/genarrative/current/api-serverldd/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 独立安装 OpenSSL 3.2.0/opt/genarrative/openssl-3.2.0,并只通过 genarrative-api.serviceLD_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=1reference_image_bytes_total>0request_params.referenceImages[0] 也有 field=image、文件名、MIME 和 bytes。
  • 根因:Rust curl::easy::Formcontents(...).filename(...) 不等价于文件上传 partVectorEngine 转码层会认为没有收到图片。release 上用 curl CLI -F image=@file 可成功,证明字段名和上游接口本身没变。
  • 处理:multipart 参考图必须用 Form::buffer(file_name, bytes) 并设置 content_type(...),让 libcurl 生成真正的 name="image"; filename="..." 文件 part。
  • 验证:release 上先看 journalctl -u genarrative-api.serviceVectorEngine 图片请求发送失败,准备重试 与最终 HTTP 返回;若仍失败,再用同一图片分别跑 curl 与最小 reqwest 探针对照。
  • 关联:server-rs/crates/platform-image/src/vector_engine/client.rsdocs/【后端架构】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.tsxdocs/【项目基线】当前产品与工程约束-2026-05-15.mddocs/【玩法创作】平台入口与玩法链路-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.mjsdocs/【开发运维】本地开发验证与生产运维-2026-05-15.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

3001 无法访问先查旧 worktree 占端口和 SpacetimeDB 版本

  • 现象:http://127.0.0.1:3001/ 打不开,但 3000 / 3101 / 8082 仍有进程;npm run dev 直接退出,没有把新栈拉起来。
  • 原因:旧 worktree 的 api-serverspacetime-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/healthzhttp://127.0.0.1:3103/v1/ping 都返回 200,且进程命令行指向当前 worktree 路径而不是别的仓库。
  • 关联:scripts/dev.mjsdocs/project-memory/shared-memory/pitfalls.mddocs/【开发运维】本地开发验证与生产运维-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.tomlcargo test -p api-server --manifest-path server-rs/Cargo.toml work_authornpm run test -- scripts/rebind-orphan-work-owners.test.ts
  • 关联:server-rs/crates/api-server/src/work_author.rsserver-rs/crates/module-auth/src/domain.rsscripts/rebind-orphan-work-owners.mjs

访客推荐页上下滑不要绑定登录态

  • 现象:访客模式进入移动端推荐页后,推荐内容可展示和点击底部“下一个”,但在作品信息区域上下滑不会切换推荐作品,表现为推荐页不能上下滑动。
  • 原因:推荐页滑动切换逻辑 beginRecommendDrag(...) 误把 isAuthenticated 作为启用条件;访客态虽然允许浏览和通过底部按钮切换,却无法触发同一套拖拽切换。
  • 处理:推荐页拖拽只校验当前是否有作品、多作品可切换以及是否正在提交动画,不再要求登录;登录态相关操作仍由点赞、改造等按钮自身权限控制。
  • 验证:npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx 覆盖访客态纵向滑动不弹登录且触发下一条推荐。
  • 关联:src/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx

Windows junction worktree 下 Vitest 定向路径失败先切真实路径

  • 现象:在 C:\Users\...\ .codex\worktrees\... 这类 junction 工作区运行 npm run test -- src/... 时,Vitest 可能报 Failed to load url C:/Users/... (resolved id: F:/DevWorktrees/...),同一测试文件明明存在却被判定找不到。
  • 原因: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.tsxsrc/components/puzzle-clear-result/PuzzleClearResultView.test.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxsrc/routing/appPageRoutes.test.ts

拼消消草稿试玩要和正式 runtime 分流

  • 现象:拼消消结果页点击“试玩”后如果仍然调用 /api/runtime/puzzle-clear/runs,草稿试玩会被正式 run 规则和统计约束卡住,公开作品又可能和草稿恢复串台。
  • 原因:拼消消既有草稿生成 / 结果页 / 发布闭环,也有正式公开 runtime;如果把结果页试玩和公开运行态复用同一个后端 startRun 入口,work detail 读取路径和统计口径都会混在一起。
  • 处理:结果页试玩改走前端本地 runtimeMode=draft snapshot,只用于草稿试玩和关卡切换,不写正式 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.tsxsrc/services/puzzle-clear/puzzleClearClient.tssrc/services/puzzle-clear/puzzleClearLocalRuntime.tsdocs/prd/【玩法创作】拼消消玩法模板PRD-2026-05-30.md

拼消消 runtime 必须继承拼图模板的原生交互基线

  • 现象:拼消消卡片在浏览器里会出现原生图片拖拽 / 下载手柄,或窗口拉伸后棋盘和卡片被拉成矩形。
  • 原因:拼消消 runtime 早期只继承了“交换 / 消除”的业务逻辑,没有完整继承拼图模板在基础交互上的防护:touch-noneselect-noneaspect-squaredraggable={false}onDragStart(event.preventDefault())-webkit-user-drag: none
  • 处理:棋盘容器必须保持正方形约束,卡片按钮和内层 <img> 都要显式禁用浏览器原生拖拽,样式层也要补 user-select: none-webkit-user-drag: none,不能只靠业务指针逻辑。
  • 验证:浏览器中检查棋盘 getBoundingClientRect().width === height,卡片图片 draggable="false"-webkit-user-dragnone;真实拖拽只应进入交换逻辑,不应触发原生图片拖拽。
  • 关联:src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsxsrc/index.csssrc/components/puzzle-runtime/PuzzleRuntimeShell.tsxsrc/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.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx

拼消消要继承拼图模板的动作语言,不只是规则

  • 现象:拼消消如果只实现“交换后裁决”,但没有开局翻牌、按下留空位、被替换卡快速飞回、以及局部拼接块整体拖动,玩家会直觉上觉得比原拼图更笨重。
  • 原因:早期实现容易把“规则独立”误读成“动作语言也要重写”,结果只保留了交换逻辑,没有沿用拼图模板里已经验证过的拖拽反馈、空位让位和合并块连续感。
  • 处理:拼消消运行态要继承拼图模板的基础手感:只在开局保留入场翻牌,拖起时源位立即呈空,放下时被替换卡要有明确飞向空位的位移感,连通块要作为整体拖动和整体呈现。
  • 验证:浏览器拖拽时能看到跟手 ghost、源位空槽、落点飞入和整组拼接层;src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx 应覆盖这些行为。
  • 关联:src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxsrc/index.css

拼消消空格位必须允许落位,不能当成不可交互死格

  • 现象:运行到某一关后,棋盘里出现空格位,用户能看见空洞但拖不进去,也点不动。
  • 原因:空格位被前端交互或后端裁决误当成“无效目标”,只保留了交换逻辑,没有把“源卡落入空位、源位清空”当成合法移动。
  • 处理:空格位必须保留 button 交互态和落点命中逻辑;前端拖拽 / 点击落到空格时直接提交移动,后端和本地 runtime 都要把源卡移动到目标格并清空源格,不再走失败交换。
  • 验证:npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxnpm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.tscargo 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.tsxsrc/services/puzzle-clear/puzzleClearLocalRuntime.tsserver-rs/crates/module-puzzle-clear/src/application.rs

拼消消空位落卡后必须立即补位,不能把空洞留成真空格

  • 现象:卡牌成功落进空格后,源位仍然留空,玩家会误以为那个格子坏掉了。
  • 原因:移动逻辑只处理了“落到空位”,没有在未消除时同步走一遍重力补位,所以源列会短暂或永久留下空洞。
  • 处理:只要移动后棋盘存在空位,就立即走补位和可解性修复;这样源位会从顶部准备区补卡,不会留下不可交互空洞。
  • 验证:npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.tscargo 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.tsserver-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=true502504429 或请求超时,例如 nginx HTML 502 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 -- --nocapturecargo check -p api-server --manifest-path server-rs/Cargo.toml
  • 关联:server-rs/crates/api-server/src/puzzle_clear.rsdocs/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.tsxsrc/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.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxdocs/【玩法创作】平台入口与玩法链路-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.hermes/skills/genarrative-dev-stack-port-routing/SKILL.mddocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

Windows junction 工作区下 dev.mjs 直接执行入口要用 realpath 判断

  • 现象:在 C:\Users\...\ .codex\worktrees\... 这类 junction 路径里运行 npm run dev:web,进程会秒退,3000 不监听,但同一脚本从真实 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.mjsscripts/dev.test.ts

Vitest 定向测试在 Windows junction 工作区要切真实路径

  • 现象:在 C:\Users\...\ .codex\worktrees\... 这类 junction 路径里跑 npm run test -- src/... 时,Vitest 会报 Failed to load url ... (resolved id: F:/DevWorktrees/...),看起来像文件不存在。
  • 原因:Vite / Vitest 会把入口 realpath 到真实 worktree 路径;如果命令从 junction 路径传入相对文件参数,入口路径和 resolved id 可能跨盘符不一致。
  • 处理:前端定向测试优先从真实路径 F:\DevWorktrees\codex\worktrees\f584\Genarrative 运行,不要把这类文件加载失败误判成组件或路由断言失败。
  • 验证:同一命令从真实路径执行应正常收集并运行测试。
  • 关联:src/components/puzzle-clear-creation/PuzzleClearWorkspace.test.tsxsrc/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxsrc/routing/appPageRoutes.test.ts
  • 现象:新增或扩展 *-generating 页面后,生成卡只渲染首帧,已耗时 / 预计等待 停在进入页那一刻不动。
  • 原因:平台壳层的共享 miniGameGenerationProgressNowMs 时钟没有把新生成阶段纳入 tick 条件,或者该阶段的 buildMiniGameDraftGenerationProgress(..., nowMs) 没有接入同一时钟。
  • 处理:任何共享生成页都要通过平台壳层统一的时钟判断和 nowMs 传递刷新,新增生成阶段时要同时补 selectionStage 判定、useEffect 依赖和进度调用点。
  • 验证:浏览器里进入对应生成页后,已耗时 / 预计等待 应持续变化,不应停在首帧。

拼消消要用真实可消除判断,不要把“已相邻”当成可解

  • 现象:拼消消开局或补牌后会直接出现已完成的图案组,或者 1x2 被当成半锁定局部留在场上。
  • 原因:早期把可解性写成“场上已经有同组相邻卡”或“只要有一对相邻同组卡就算可解”,这会把已完成盘面误当成合法盘面;同时半锁定规则没有排除 1x2
  • 处理:开局和补牌后的重排必须先排除现成消除,再用真实交换 / 落位模拟判断是否会产生新消除;1x2 永远不进入半锁定组,半锁定只允许 1x32x22x3
  • 验证:npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsxcargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml -- --nocapture 通过后,开局盘面不应直接出现 completed group。
  • 关联:src/services/puzzle-clear/puzzleClearLocalRuntime.tsserver-rs/crates/module-puzzle-clear/src/application.rs

推荐页作品 key 漏玩法会导致运行内容和标题作者错位

  • 现象:移动端推荐页进入跳一跳或敲木鱼等作品时,游戏运行内容已经切到当前作品,但下方标题、作者和头像仍显示第一条拼图或其它推荐作品。
  • 原因:平台壳层用 getPlatformPublicGalleryEntryKey(...) 写入 activeRecommendEntryKey,而 RpgEntryHomeView 内部的 buildPublicGalleryCardKey(...) 漏掉新玩法 sourceType 分支,导致当前 key 查不到条目后回退到推荐列表第一条。
  • 处理:推荐页和平台壳层的公开作品 key 规则必须复用 buildPlatformPublicGalleryCardKey(...),覆盖同一批 sourceType,至少包括 big-fishpuzzlejump-hopwooden-fishmatch3dsquare-holevisual-novelbark-battleedutainment:<templateId>;新增玩法公开推荐流时先补这个共享 helper。
  • 验证:npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile recommend meta matches active" 应覆盖跳一跳和敲木鱼的当前运行内容、标题和作者一致。
  • 关联:src/components/rpg-entry/RpgEntryHomeView.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxsrc/components/rpg-entry/RpgEntryHomeView.recharge.test.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

跳一跳飞行动画不要直接用最新 run 重绘地块窗口

  • 现象:跳一跳松手后如果后端很快返回下一帧 run,地块窗口会立刻前移,角色翻腾动画看起来像没播放;若同时刷新图片资产,还可能被误认为地块频闪。
  • 原因:后端 run 是规则真相,前端 runtime 又需要低延迟表现。如果 DOM 平台层直接用最新 run.currentPlatformIndex 渲染,后端回包会抢在动画前完成视觉切换。
  • 处理:前端保留独立 displayRun,松手后先进入 isJumpAnimating=true,角色在当前显示窗口内飞向前端预测真实落点;视觉预测必须用当前显示窗口的 current/next 地块作为方向来源,不能拿已经提前返回的后端新 run 目标配旧窗口角色,否则下一跳会朝实际目标反方向飞。飞行动画完成后再把 displayRun 切到最新后端 run,并进入约 1440msplatformAdvancing 表现态。成功后的角色显示必须使用 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 或贴图加载失败 fallbackDOM fallback 在相机推进期间自身不能保留 left/top transition,否则 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-drift keyframes 且下一跳不会被旧地块保留态阻塞。
  • 关联:src/components/jump-hop-runtime/JumpHopRuntimeShell.tsxsrc/services/jump-hop/jumpHopRuntimeModel.tsserver-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 地块继续使用稳定 platformId key,让旧图片组件在推进期复用;有真实 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.tsxsrc/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.tsxsrc/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.tsxdocs/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.rsserver-rs/crates/platform-image/src/generated_asset_sheets/sheet.rsserver-rs/crates/api-server/src/jump_hop.rs

跳一跳 UV 图集切片要防贴边矩形 u32 中间溢出

  • 现象:跳一跳草稿在背景、返回按钮和地板图集 image2 都生成成功后,前端报“执行跳一跳共创操作失败”,Vite 代理日志出现 socket hang up,后端日志出现 jump_hop_atlas_slicing.rsattempt 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.rsserver-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.rsserver-rs/crates/api-server/src/jump_hop_atlas_slicing.rsdocs/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.mdserver-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 会被端口段映射覆盖。Tauri devUrl 是静态配置,不会读取 scripts/dev.mjs 最终解析出的漂移端口。
  • 处理:桌面壳 beforeDevCommand 必须使用 npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port,让 Vite 实际监听端口和 Tauri devUrl 一致,并在 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.jsonapps/desktop-shell/scripts/check-config.mjsscripts/dev.mjs

Tauri 手动创建主窗口时 devUrl 不会自动套到 index.html

  • 现象:npm run desktop-shell:dev 启动后窗口地址显示 http://tauri.localhost/index.htmltauri://localhost/index.html,即使 Vite 已经在 http://127.0.0.1:3000/ 正常监听。
  • 原因:桌面壳为了注册导航、下载、生命周期和托盘行为,把 Tauri 配置里的主窗口设为 create=false,再在 Rust app.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.rsapps/desktop-shell/src-tauri/src/shell/url.rsapps/desktop-shell/src-tauri/src/shell/navigation.rsapps/desktop-shell/src-tauri/tauri.conf.json

Tauri release 的 tauri.localhost 不要交给系统浏览器

  • 现象:Windows / release 包启动桌面壳时,系统默认浏览器被打开到 http://tauri.localhost/index.html
  • 原因:release 打包资源在 WebView 内可能表现为 tauri://localhost/index.htmlhttps://tauri.localhost/index.htmlhttp://tauri.localhost/index.html;如果导航白名单只允许 tauri:https://*.localhosthttp://tauri.localhost 会被误判成普通外链并交给 opener.open_url
  • 处理:桌面壳导航策略必须把 http / https*.localhost 都视为 Tauri 内部打包资源,只允许真正外部 http / httpsmailtotel 走系统浏览器。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_pagesnpm run desktop-shell:typecheck、Windows release 启动时不应打开系统浏览器或控制台窗口。
  • 关联:apps/desktop-shell/src-tauri/src/main.rsapps/desktop-shell/src-tauri/src/shell/navigation.rsapps/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.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

推荐页 ready 不能只等主图或首次 DOM 图片

  • 现象:移动端推荐页卡面遮罩在作品主图加载后就渐隐,但游戏内 UI 图集、背景、道具图或换签中的 generated 图片还没有准备好,用户会看到运行态半成品或资源闪入。
  • 原因:推荐页 ready probe 如果只扫描首次挂载时已有的 <img>,就会漏掉 React effect、/api/assets/read-url 换签、spritesheet 解析或后续 state 更新才新增的资源。
  • 处理:推荐页 runtime 遮罩必须持续观察运行态 DOM 内新增图片、内联 background-imagedata-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.tsxsrc/components/common/RuntimeResourcePendingMarker.tsxsrc/components/ResolvedAssetImage.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

拼图文字直创的 compile 回包不等于生成完成

  • 现象:只输入文字点击生成拼图时,页面刚进入生成页就弹出“生成任务已完成,可以继续查看草稿。”,随后又提示“请先选择一张正式拼图图片。”,结果页关卡里也没有图。
  • 原因:统一创作表单路径把 compile_puzzle_draft 的同步回包无条件当成 ready;但后端在 AI 重绘路径会先返回 stage=image_refiningprogressPercent=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.tsxsrc/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsxdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

CreativeImageInputPanel 主图点击默认预览

  • 现象:复用 CreativeImageInputPanel 的结果页 / 编辑页已有主图时,用户点击图片却触发上传,无法直接查看大图;不同玩法若各自手写上传按钮会让主图、历史图、AI 重绘和参考图行为再次分叉。
  • 原因:旧主图卡整卡是上传 label,缺少主图预览模式和上传 / 历史入口的显式控制参数。
  • 处理:通用面板已有主图时默认点击主图打开全屏预览,上传 / 更换收口到右下角 ImagePlus 图标按钮;无图时仍允许点击空图卡上传。调用方用 canUploadMainImagecanUseImageHistory 分别控制上传与历史按钮,不要复制面板或用样式遮挡按钮。
  • 验证:npm run test -- src/components/common/CreativeImageInputPanel.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx
  • 关联:src/components/common/CreativeImageInputPanel.tsxsrc/components/puzzle-result/PuzzleResultView.tsxdocs/【玩法创作】平台入口与玩法链路-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.tsxsrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxdocs/【玩法创作】创作主页与项目入口改版计划-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-viewpage 和 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.tsxsrc/mobileViewportKeyboardFocus.tssrc/index.cssminiprogram/pages/web-view/index.wxmlminiprogram/pages/web-view/index.wxssdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

小程序订阅消息授权不要依赖 web-view bindmessage

  • 现象:拼图点击生成后,H5 以为已经请求了生成结果订阅授权,但小程序没有弹出 wx.requestSubscribeMessage 授权框。
  • 原因:web-view bindmessage / wx.miniProgram.postMessage 不适合承接“当前用户点击后立刻请求授权”的时序,消息可能等到 web-view 后退、分享或销毁时才派发,导致授权请求没有发生在 compile_puzzle_draft 前。
  • 处理:不要在原生页 onLoad 自动触发 wx.requestSubscribeMessage,真机会闪页返回且不弹授权框。H5 在 compile_puzzle_draft 前应先进入生成进度态并立即发起生成 action,再通过微信 JS SDK miniProgram.navigateTo 非阻塞跳转到小程序原生订阅页尝试请求授权;用户接受、拒绝或返回都不能阻塞生成。原生页不要改写上一页 webViewUrl,否则 web-view 可能重新加载首页并丢失进度页状态。后端发送订阅消息仍只允许在拼图资产成功或失败终态后执行。
  • 验证:npm run test -- src/services/wechatMiniProgramSubscribe.test.ts miniprogram/pages/subscribe-message/index.test.js
  • 关联:src/services/wechatMiniProgramSubscribe.tssrc/components/platform-entry/PlatformEntryFlowShellImpl.tsxminiprogram/pages/subscribe-message/index.shared.jsminiprogram/pages/web-view/index.js

微信订阅消息 time 字段不能用内部时间戳

  • 现象: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 -- --nocapturedev 日志中不应再出现 data.time4.value invalid
  • 关联:server-rs/crates/api-server/src/wechat_subscribe_message.rsdocs/【开发运维】本地开发验证与生产运维-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 在完成前仍显示 Collectingprogress_percent=0,前端只能按时间显示假进度;超时后重试同一 session 时,后端还可能因为 session 没有中间素材而重新从背景开始生成。
  • 待处理:将跳一跳生成改为后端任务化 / 可轮询真实阶段进度,按背景、返回按钮、图集、切片、持久化、写草稿分阶段落库;统一后端全局生成 deadline、VectorEngine 重试预算、前端等待窗口和失败态回写。超时后再次进入同一 session 应优先恢复正在运行或已完成的任务,不应重复生图。
  • 验证:模拟首张 image2 超长耗时或超时重试时,生成页应显示真实阶段和可恢复状态;前端请求超时不应把最终成功草稿标记为失败;刷新 /creation/jump-hop/generating?sessionId=<id> 后应能恢复到后端真实状态;同一 session 重试不得重复生成已完成阶段。
  • 关联:src/services/jump-hop/jumpHopClient.tssrc/services/miniGameDraftGenerationProgress.tsserver-rs/crates/api-server/src/jump_hop.rsserver-rs/crates/platform-image/src/vector_engine/client.rsdocs/【玩法创作】平台入口与玩法链路-2026-05-15.md

画布生成完成态不能被旧 autosave 覆盖

  • 现象:release 外部生成 worker 补跑完成后,生成图已进入素材库或项目资源,但画布生成器仍显示 generating;刷新后可能仍看到历史生成框卡住。
  • 原因:画布前端在提交生成后会把 generating layout 放入 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.tsxcargo test -p spacetime-module --manifest-path server-rs/Cargo.toml editor_project_storage --librelease 排障用 list_editor_projects_and_return / get_editor_project_and_returngeneration-dialog 状态,不要只看素材库。
  • 关联:src/components/image-editor/useImageCanvasProjectPersistence.tsserver-rs/crates/spacetime-module/src/editor_project_storage.rsserver-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,写入弱 ETagLast-ModifiedGET / HEAD 命中 If-None-MatchIf-Modified-Since 时直接返回 304,不读取或发送 body。HEAD 静态响应只读 metadata,仍写正确 Content-Length
  • 验证:npm run check:pingora-gateway-smoke 必须覆盖静态 HEADIf-None-Match 304、If-Modified-Since 304,并用 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.rsscripts/check-pingora-gateway-smoke.mjsdocs/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 和非法 %GGnpm run check:pingora-gateway-smoke 必须覆盖编码图标路径返回 image/png,并确认危险编码路径仍返回 404。dev 切换后用浏览器或 curl 直接验证对应图标 URL 的 Content-Type 和 PNG magic bytes。
  • 关联:server-rs/crates/pingora-gateway/src/main.rsscripts/check-pingora-gateway-smoke.mjsdocs/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 URL http://127.0.0.1dev / 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.worldPingora 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.examplescripts/ops/production-health-patrol.mjsscripts/ops/pingora-direct-rehearsal-status.mjsdocs/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 脚本。用 shell source 会把空格后的内容拆成命令或参数。另一个容易误判的点是 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:18084HTTPS / 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_LISTENHTTP_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,要求 OKdirect-access-log matchedCount == checked。收尾后复核 80/443 仍由 Nginx 监听,正式 Pingora shadow 仍为 127.0.0.1:18081
  • 关联:scripts/check-pingora-direct-preflight.mjsscripts/check-pingora-direct-live.mjsdeploy/pingora/pingora-gateway.env.exampledocs/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_name vhost 承载主站和 Gitea;当前 Pingora direct listener 默认只有一组 TLS cert/key,且路径路由本身无法区分同一 IP 上的多个域名。
  • 处理:direct env 必须配置 GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.worldGENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000TLS 证书必须同时覆盖 dev.genarrative.worldgit.genarrative.world,并通过 pingora-tls-cert-sync.mjs 同步到 Pingora 私有目录后再指向 env。不要通过“临时释放 Gitea vhost”把 Gitea 从切换窗口里牺牲掉。
  • 验证:本地 npm run check:pingora-gateway-smoke 必须覆盖 Gitea Host 整站转发、维护模式不拦截 Gitea Host 和 access log proxy_target=Giteadev 切换后除 https://dev.genarrative.world/ 外,还必须验证 https://git.genarrative.world/ 返回 GiteaHTTP 到 HTTPS redirect 保留正确 HostPingora access log 中有 host=git.genarrative.world / proxy_target=Gitea
  • 关联:server-rs/crates/pingora-gateway/src/main.rsdeploy/pingora/pingora-gateway.env.exampledocs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md

SpacetimeDB 连接池租约必须有 Drop 兜底,acquire 不允许无界自旋

  • 现象:release 上 api-server 周期性出现全量 spacetime_stage="pool_acquire" elapsed_ms=45000 业务超时,/readyz 503reason=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 统一复位槽位/归还连接;槽位改 AtomicBool CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。
  • 验证:cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --libdropped_lease_releases_slot_and_permitacquire_times_out_at_pool_acquire_when_pool_is_busy)。
  • 关联:server-rs/crates/spacetime-client/src/lib.rsdocs/【后端架构】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_config procedure 的事务快照,成功后再更新进程缓存;缓存只作为 procedure 暂时失败后的兜底。不要订阅或读取 feature_gate_config 本地表来判断后台配置。
  • 验证:RUSTC_WRAPPER= cargo check -p spacetime-client --manifest-path server-rs/Cargo.tomlRUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml frontend_runtime_config_denies_anonymous_agent_sidebar_when_gate_enabledRUSTC_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.rsserver-rs/crates/spacetime-client/src/lib.rsserver-rs/crates/api-server/src/frontend_runtime_config.rsserver-rs/crates/api-server/src/editor_agent.rs

后台灰度新 target 不能继承旧规则

  • 现象:管理员先点开一条已有 gate,再从两段式下拉框选择一个尚不存在的新 target,保存后新 gate 可能带着上一条 gate 的启用状态、灰度比例和黑白名单。
  • 原因:新 target 分支如果只更新 gate key,会复用当前 React 表单状态;这些字段对运营不可见地跨 target 泄漏。
  • 处理:applyGateTarget 进入不存在的新 target 时必须重置为新建态:enabled=falserolloutPercent=0、allow / deny 列表为空,并使用 target 默认描述。只有显式点已有 gate 才 fillForm 复制服务端规则。
  • 验证:npm run test -- apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx
  • 关联:apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsxapps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx

背景色决策喂 gpt-5-mini 的图不必按阿里云抠图那样归一化

  • 现象:担心带图背景色决策把源角色图原样 base64 塞给 gpt-5-miniresolve_media_source_as_data_url 不做 resize / 字节上限),会像阿里云通用抠图那样因超尺寸 / 超体积被上游拒绝,于是想给决策链路也补一套图片归一化。
  • 原因:两条链路的上游限制完全不同。阿里云 SegmentCommonImage 有硬限制(≤3MB、分辨率 <2000×2000、最长边 ≤1999),必须归一化;而 gpt-5-mini(经 VectorEngine /v1/responsesResponses 协议 + 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² 全 200noise 900²(4.1MB)~2600²(34.4MB) 全 200,无拒绝阈值出现在实用范围内。
  • 关联:server-rs/crates/api-server/src/character_animation_assets.rsresolve_media_source_as_data_url)、server-rs/crates/api-server/src/editor_screen_background_decision.rsserver-rs/crates/platform-matting/src/lib.rs(对照:阿里云输入归一化)。

不要把 BgFilter segModel 暴露为外部可选参数

  • 现象:看到 EditorImageGenerationRequestEditorIconSpritesheetGenerationRequestEditorUiDesignAssetExtractionRequest 能反序列化 segModel,容易认为外部 OpenAPI 也应公开该字段,或让用户在 birefnetanime-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.rssrc/services/image-editor/editorProjectClient.tsdocs/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 只提供真实静态文件,失败返回 404。路由增删同步三套 Nginx、Pingora、route parity matrix 和路由门禁。
  • 验证:除全部真实 SPA 路径外,至少检查 /not-exist/creation/not-exist/runtime/not-exist/puzzle/not-exist 均返回 404;维护模式仍保持页面 503 优先语义。
  • 关联:src/routing/appRoutes.tsxsrc/routing/appPageRoutes.tsdeploy/nginx/deploy/container/nginx.confserver-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 SCMparameters {}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-deployjenkins/Jenkinsfile.production-stdb-module-buildjenkins/Jenkinsfile.production-stdb-module-publishscripts/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。默认值仍在 Full 结束时退出维护,避免定时 dev 发布行为变化。
  • 验证:API deploy fixture 必须覆盖成功发布并保留 marker;生产运维静态门禁同时反查 Full 参数、下游透传、API Deploy 参数和脚本 flag。推送后用 fail-closed 首阶段运行刷新 live Job 参数,再核对 config.xml,不能只看仓库文件。