为 phase procedure 增加结构化 lease/fencing 拒绝结果并同步 typed bindings 仅对 Build、ConnectDropped、Timeout 重试一次,最终失败不回 pending 角色、图标和 UI 后处理最终失败时保留原图并以 completed warning 收口 同步 inline、队列轮询和刷新恢复 warning,保留 sliceWarning 契约 同步 external v1 OpenAPI、权威文档和 external editor skill 更新 BgFilter 服务地址
576 KiB
踩坑与排障记录
用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
记录格式
## 问题标题
- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue
phase 上报的业务拒绝与传输失败不能共用字符串错误
- 现象:provider 已经返回并保存原图,worker 上报
processing时一次断连或超时就直接把任务判为失败;或者为了规避误杀而重试所有错误,导致 stale lease 的旧 worker 继续执行后处理。 - 原因:phase procedure 的 lease / fencing 业务拒绝与 SDK 建连、断连、超时错误被压成同一种字符串错误,调用方无法可靠决定是否重试;按中文或 SDK 文案匹配会在错误文本变化后失效。
- 处理:procedure 返回结构化
LeaseFencingRejected/OtherRejected,typed client 再把模块拒绝与 RPC 错误分开。LeaseFencingRejected立即终止,OtherRejected以及 SDK 的Procedure/Runtime错误不重试;只有Build/ConnectDropped/Timeout在同一 job attempt 内重试一次。编辑器 job 固定max_attempts=1,第二次传输失败后进入failed,不回pending、不重新调用 provider。不得让 phase 上报错误落入“后处理失败保留原图”的降级分支。 - 验证:分别覆盖 lease / fencing 拒绝、其它拒绝、建连、断连、超时和第二次失败,确认最多调用两次;同时断言角色、图标和 UI 的原图降级只包住透明背景处理,不包住 phase 上报。
- 关联:
server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/spacetime-client/src/external_generation.rs、server-rs/crates/api-server/src/editor_project.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
禁止 Data URL 持久化时不要漏掉异步任务 JSON
- 现象:工程、素材、图层和元数据都已禁止 Data URL 后,服务器仍在生成高峰出现 SpacetimeDB / api-server 内存急剧膨胀甚至 OOM;读取少量正式生成任务也会造成远大于响应体的瞬时内存增长。
- 原因:同步接口 worker 化时把原请求整体序列化到
external_generation_job.request_payload_json,而前端又把已有objectKey下载成 Data URL 提交。任务表也是正式持久化边界;列表 procedure 若先收集完整任务行再截断,还会把 request/result 大字段在 SpacetimeDB、SDK mapper 和 BFF 多次持有。 - 处理:先在事故涉及的编辑器持久任务 JSON 上由 api-server 与 SpacetimeDB 两层递归拒绝
data:/blob:并限制字节数;已有媒体传objectKey/resourceId/assetId,本地派生图先用强唯一 key 上传。其它玩法若仍以 Data URL 作为正式请求契约,必须先资源化,不能直接扩大门禁造成玩法回归。列表、详情和 acknowledge 只走无 payload 的摘要投影,ack 不能为了同步旧字段重写大任务行;摘要错误文本也必须清除内联媒体并设硬上限,列表只能维护有界 top-N,不能先收集 owner 全量历史再截断。历史只通过迁移操作员的 dry-run + B-tree cursor 分批 procedure 压缩editor-canvas终态任务,cursor 选择读取量必须受 limit 约束,绝不全表扫描、绝不处理 pending / running;dry-run 后 apply 同一批时保持输入 cursor 不变,最后一批即使has_more=false只要仍有命中也必须 apply,只有 apply 成功后才推进到返回 cursor。SpacetimeDB CLI 2.5 的Option<T>非空参数必须使用 SATS sum 编码;维护脚本要统一编码cursor_job_id、owner_user_id和completed_before_micros,否则首批空 cursor 可运行,但第二批或带截止时间的调用会在写入前被拒绝。 - 发布门禁:生产发布入口必须固定
--delete-data=never与 scoped--yes=migrate,break-clients,普通 Jenkins 参数不得暴露清库开关;需要删数据的 schema 冲突必须直接阻断并重新检查 artifact/schema,不能靠裸--yes放行。 - 验证:构造嵌套 Data URL、Blob URL 和超限 JSON 确认入队失败;检查正式 UI procedure / client record 不含 request/result payload;用 dry-run 和 apply 测试确认活动任务不变、终态普通提示词保留且内联媒体被替换;至少带一次非空
--cursor-job-id与--completed-before-micros验证 CLI Option 编码,而不是只测首批空 cursor。 - 关联:
server-rs/crates/api-server/src/editor_generation_queue.rs、server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/api-server/src/external_generation.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
React 测试因内部状态或实现细节正常重构就碎
- 现象:修改组件结构、按钮排序、图标库 class、提示文案或 hook 内部状态名后,React 测试大量失败,但真实用户流程和对外契约没有变化。
- 原因:测试把
data-testid仪表盘、textContent拼接状态、完整对象 / 数组顺序、图标 class 或长文案当成契约;这些断言绑定的是实现形状,不是用户行为或稳定边界。 - 处理:按
React 组件测试准则重写到更稳定的层级。用户流程测试断言 role / label / URL / 弹窗 / callback;hook 逻辑用renderHook直接验证公开返回契约;DTO / payload 使用关键字段或expect.objectContaining(...)。只有产品明确要求的可访问语义、固定顺序或渲染边界才保留精确断言。 - 验证:运行触达文件的定向
vitest,必要时追加npm run typecheck、npm run check:encoding和git diff --check。 - 关联:
docs/technical/【前端测试】React组件测试准则-2026-06-26.md、src/components/image-editor/useCanvasGenerationDialogs.test.tsx、src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx。
图片画布素材库删除要匹配 sourceResourceId
- 现象:素材库中删除了已经生成并进入素材库的资源,但画布上对应图层仍然存在,刷新后还可能从已保存布局里恢复。
- 原因:生成素材进入账号级素材库时可能通过
editor_asset.sourceResourceId指向原项目资源;如果前端素材库映射和级联删除只比较sourceAssetId、assetObjectId、objectKey或src,就会漏掉只靠项目资源 ID 关联的历史 / 后端生成图层。 - 处理:
EditorAsset必须保留sourceResourceId;从素材库添加到画布时继续写入图层;删除素材时同时比较layer.resourceId/layer.sourceResourceId与asset.sourceResourceId。 - 验证:
ImageCanvasEditorModel.test.ts覆盖素材库 source resource 保留,useImageCanvasAssetCanvasBridge.test.tsx覆盖资源 ID 级联清理,ImageCanvasEditorAssetsIntegration.test.tsx覆盖删除后保存的新 layout 不再包含被删图层。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/useImageCanvasAssetCanvasBridge.ts、src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx。
后台素材查询不要用 SQL 直查 editor_asset
- 现象:后台“素材查询”报
HTTP 400:no such table: editor_asset. If the table exists, it may be marked private.。 - 原因:
editor_asset是私有 SpacetimeDB 表,后台 SQL / schema HTTP 查询面看不到私有表;即使 api-server 有后台身份,也不能把私有表当 Dashboard SQL 表直接查。 - 处理:后台素材查询走
spacetime-module内的admin_list_editor_assets_and_returnprocedure,由spacetime-clienttyped facade 调用后再在api-server映射作者展示名和陶泥号。新增类似后台只读能力时,优先补窄 procedure / read model,不要复用fetch_admin_dashboard_rows直查私有源表。 - 验证:
cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/spacetime-client/src/editor_project.rs、server-rs/crates/api-server/src/admin.rs。
后台素材缩略图不要在首次挂载时全量换签
- 现象:后台“素材查询”首批缩略图正常,继续向下滚动或读取更多后长期显示占位图;api-server journald 中已到达的
/admin/api/assets/read-url可能全部是200。 - 原因:列表一次挂载 80 条私有素材时,每个缩略图同时换签,会在同秒突发请求。production Nginx 的
genarrative_admin_rps为30r/s burst=16,超出部分在进入 api-server 前已返回429,因此仅查 api-server 日志会漏掉失败请求。 - 处理:缩略图使用
IntersectionObserver在进入视口附近时再调用管理端换签;对429使用有上限的退避重试,并在条目卸载后停止更新状态和安排重试。不得为单页突发放大 Nginx 通用管理端限流,也不得在单次限流失败后永久保留无图占位。 - 验证:前端定向测试覆盖首屏外的后续行进入可见区后才换签、“读取更多”追加行可继续显示缩略图、
429后有限重试恢复、卸载后不再重试;真实浏览器滚动验收时同时核对 Nginx access/error log、api-server journald 和 Network 面板,不以单一日志面判定成功。 - 关联:
apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx、apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx。
陶泥儿精选重复先查同源同媒体画布副本
- 现象:每次从项目素材中把同一个生成素材拖到画布上,
陶泥儿精选都多出一张看起来相同的素材。 - 原因:素材拖入画布会为图层实例准备
editor_project_resource;如果该素材本来带sourceResourceId指向原始生成资源,而新资源仍按普通 generated 资源公开,精选就会把原件和每次拖拽产生的同源同媒体副本都展示出来。 - 处理:创建项目资源时保留
source_resource_id,并在同项目已有同源同媒体资源时复用已有 resource;确需创建同源同媒体副本时默认public_showcase_enabled = false。公开精选读取和前端精选模型都跳过sourceResourceId指回同一媒体原件的副本,但不要按图片地址全局去重,避免不同生成步骤共享占位图时被误合并。 - 验证:
creationShowcaseModel.test.ts覆盖同源同媒体副本只展示原件;ImageCanvasEditorAssetsIntegration.test.tsx覆盖拖拽生成素材到画布时继续提交sourceResourceId;cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml确认后端资源复用 / 精选过滤逻辑可编译。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、src/components/creation-home/creationShowcaseModel.ts、src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx。
陶泥儿精选作者丢失先查公开作者展示字段
- 现象:
/creation的陶泥儿精选卡片和预览弹窗中,素材下方作者名消失、只显示占位,或错误显示内部用户 ID。 - 原因:精选数据源来自公开
editor_project_resource快照;如果 SpacetimeDB read model、spacetime-clientmapper 或api-serverpayload 任一层漏传authorDisplayName/display_name或authorPublicUserCode/ 陶泥号,前端没有可展示的公开作者字段。owner_user_id/ownerUserId/user_id是内部归属字段,不是公开作者名兜底。 - 处理:
EditorProjectResourceSnapshot、EditorProjectResourceRecord和EditorProjectResourcePayload需要一路保留公开作者展示字段;前端creationShowcaseModel优先显示authorDisplayName/display_name,没有展示名时显示authorPublicUserCode/ 陶泥号,绝不能兜底到内部ownerUserId/user_id。 - 验证:
creationShowcaseModel.test.ts覆盖展示名优先、陶泥号兜底和内部 owner/user id 不展示;editorProjectClient.test.ts覆盖公开精选接口客户端保留公开作者字段;若改动 SpacetimeDB read model,再运行npm run spacetime:generate、cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml和npm run check:spacetime-schema。 - 关联:
server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/spacetime-client/src/mapper/editor_project.rs、server-rs/crates/api-server/src/editor_project.rs、src/components/creation-home/creationShowcaseModel.ts。
陶泥儿精选瀑布流变宽先查 multi-column 容器宽度
- 现象:release
/creation桌面端精选卡片明显变宽,第三列被裁到屏幕外,页面内部可横向滑动;dev 看起来正常。 - 原因:精选瀑布流使用
column-count,当它作为 grid item 时如果没有显式width: 100%/min-width: 0,Chrome 会用多列内容的 intrinsic width 反向撑开 grid track。线上实测 1920 视口下 section 为1296px,waterfall 被撑到约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-card的getBoundingClientRect(),waterfall 宽度应等于 section 宽度。 - 关联:
src/index.css、src/components/creation-home/CreationLandingView.tsx。
画板外部生成排队超时不是失败
- 现象:画板发起付费图片生成后,前端弹出
生成任务仍在队列中,请稍后刷新画布查看结果,但后端任务仍在队列或执行中,后续可能正常完成。 - 原因:画板生成已经接入后端外部生成任务队列,
queued/running是正式任务状态;旧前端轮询等待窗口到期时直接抛错,导致正常排队被提交流程 catch 成失败 UI。 - 处理:
waitForEditorGenerationQueue等待超时只返回“仍在后端继续执行”,调用方停止本次前端等待并保留生成中状态;只有后端任务终态为failed才展示失败。 - 验证:画板生成 workflow 测试覆盖 queueState 持续
running到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。 - 关联:
src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/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.rs、server-rs/crates/spacetime-client/src/assets.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts。
资产换签不能把 generated 前缀当成 objectKey 授权
- 现象:主站或 External API 只要拿到另一个账号的 generated
objectKey就能换签,已登记的私有对象因为 key 同时命中 legacy 前缀而被匿名读取,或者/api/assets/read-url已拒绝但/api/assets/read-bytes仍能读出原始字节;后台资源预览为解决跨账号读取又误把主站入口整体放开。 - 原因:
legacyPublicPath与objectKey代表两种不同信任边界。前者仅用于未登记历史公开作品兼容,后者是正式对象引用;只检查 generated 前缀、或在查询asset_objectmetadata 前直接接受 legacy 白名单,都不能证明对象公开或属于调用方。签名 URL 和 bytes proxy 如果各写一套判断也容易漂移。 - 处理:
read-url与read-bytes必须共用authorize_asset_read_target,先按配置 bucket / 精确 key 查询asset_object;metadata 一旦存在,即使 key 命中 legacy 前缀,也严格执行PublicRead/ owner ACL。只有 metadata 不存在且显式legacyPublicPath命中platform_oss::LEGACY_PUBLIC_PREFIXES时才允许匿名兼容;任意objectKey必须登记。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-url与read-bytes使用同一组授权矩阵,并断言 Admin 成功换签会生成不含 signed URL 的管理员主体审计事件。 - 关联:
server-rs/crates/api-server/src/assets.rs、server-rs/crates/api-server/src/external_assets_api.rs、server-rs/crates/api-server/src/admin.rs、server-rs/crates/api-server/src/modules/admin.rs。
编辑器生成按钮显示泥点后仍要查真实钱包预扣
- 现象:画板生成按钮显示
N泥点,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。 - 原因:前端展示价和后端价格计算只证明价格能被展示 / 解析;如果 handler 没有包进
execute_billable_asset_operation_with_cost,或异步音频发布目标没有携带本次模型价格,外部 provider 仍会被调用但不会真实扣费。 - 处理:新增或改造编辑器外部生成入口时,确认前端请求不携带
priceMudPoints,后端按运行时模型定价重新计算价格,并用该价格进入资产扣费 wrapper。音频提交 / 发布分离时,把后端计算出的价格写入AudioAssetBindingTarget.billing_points_cost。 - 验证:结构性测试覆盖对应 handler 包含
execute_billable_asset_operation_with_cost和价格变量;音频测试覆盖resolve_creation_audio_points_cost优先读取 editor target 的billing_points_cost。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、server-rs/crates/api-server/src/character_animation_assets.rs、server-rs/crates/api-server/src/vector_engine_audio_generation/、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts。
本地 dev 启动日志先看成功锚点,不要把非阻断 warning 当失败
- 现象:
npm run dev启动 SpacetimeDB 时可能先打印static max level is off、Skipping tokio metrics,或 SpacetimeDB CLI 提示存在新版本 / 当前版本较旧,看起来像启动异常。 - 原因:这些是 tracing、metrics 或 CLI 更新提示,不代表本地 dev 栈失败;同一段日志后续仍可能已经完成
SpacetimeDB listening on 127.0.0.1:3101、模块 publish、api-server/healthz200、主站 Vite3000和后台 Vite3102ready。 - 处理:排查本地 dev 栈时先确认成功锚点:
[dev:spacetime] actual、Updated database、api-server 已完成 tracing 初始化并开始监听、/healthz200、两个 Viteready。只有缺少这些锚点或进程退出时,再继续查 CLI 权限、端口占用、publish 或 API 编译问题。 - 验证:
http://127.0.0.1:3101/v1/ping可访问、http://127.0.0.1:8082/healthz返回 200、http://127.0.0.1:3000/和http://127.0.0.1:3102/admin/可打开。 - 关联:
scripts/dev.mjs、.app/dev-stack.json、docs/project-memory/shared-memory/development-workflow.md。
私有兑换码不适用先查同手机号重复账号
- 现象:后台把私有兑换码配给某个陶泥号或手机号后,用户用同一手机号登录兑换仍提示
该兑换码不适用于当前账号。 - 原因:认证表里可能存在同一手机号的多条
user_account。如果认证工作集重建phone_to_user_id时让user_account.phone_number_e164后写覆盖前写,当前登录态会漂到没有auth_identity的重复账号,而兑换码白名单仍指向另一个内部user_id。 - 处理:重建认证工作集时以 typed
AuthStoreProjectionView从user_account/auth_identity/refresh_session恢复;手机号索引以auth_identity(provider="phone")指向的账号为权威,user_account.phone_number_e164只补没有 identity 的手机号;auth_store_snapshot表和旧 JSON procedure 已删除,Bearer / refresh session 本进程未命中时不要再从 SpacetimeDB 导出整包状态刷新内存。线上止血先核对失败请求附近的 current sessionuser_id与兑换码allowed_user_ids,不要只看手机号展示值。 - 约束:
auth_identity只保存登录入口身份键;手机号、昵称和头像的正式资料真相在user_account.phone_number_e164/display_name/avatar_url。旧auth_identity.phone_e164/display_name/avatar_url只能作为历史回填来源,不能继续让新写入依赖这些列。 - 验证:
cargo test -p spacetime-module auth_export -- --nocapture应覆盖同手机号重复账号时手机号索引优先指向有 phone identity 的账号;api-server中不应再存在运行期refresh_auth_store_from_spacetime调用。 - 关联:
server-rs/crates/spacetime-module/src/auth/procedures.rs、server-rs/crates/spacetime-module/src/auth/tables.rs、server-rs/crates/module-auth/src/lib.rs。
API Build / Deploy 归档清单不能漏掉随包 Pingora 脚本
- 现象:
Genarrative-Api-Deploy在发布阶段报发布产物缺少 Pingora TLS 证书同步脚本: build/<version>/scripts/deploy/pingora-tls-cert-sync.mjs。 - 原因:
scripts/build-production-release.sh已经把脚本复制进build/<version>/scripts/deploy/,但 Jenkins API Build 的archiveArtifacts和 API Deploy 的copyArtifacts过滤清单仍可能漏掉新增随包脚本,导致 Deploy 工作区拿到的是残缺发布包。 - 处理:新增随包部署脚本时,必须同时更新
jenkins/Jenkinsfile.production-api-build的归档清单、jenkins/Jenkinsfile.production-api-deploy的复制清单和scripts/check-production-ops-guardrails.mjs的字符串门禁;不要在 Deploy Job 里从工作区根目录或源码 checkout 兜底补脚本。 - 验证:运行
npm run check:production-ops、npm run check:production-api-release和npm run check:production-api-deploy,确认构建包、Jenkins 归档链路和 deploy fail-fast 检查口径一致。 - 关联:
jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy、scripts/deploy/production-api-deploy.sh、scripts/check-production-ops-guardrails.mjs。
图片画布角色动作结果主类型是序列帧
- 现象:产品要求画板
生成角色动作返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理。 - 原因:后端仍需要先生成
previewVideoPath再抽帧、绿幕去背和落 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载。 - 处理:角色动作结果图层主
src使用frames[0].imageSrc,mediaType固定为image-sequence,assetKind固定为character-animation,完整帧列表写入imageSequenceFrames,previewVideoPath只作为来源信息保留。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入sequences/<编号-标题>/frames/。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。 - 验证:
ImageCanvasGenerationLayerModel应断言动作结果src为首帧且mediaType="image-sequence";画布集成测试应出现画布序列帧:角色动作图片播放器,不应出现角色动作<video>;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录。 - 关联:
src/components/image-editor/ImageCanvasGenerationLayerModel.ts、src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasExportModel.ts、server-rs/crates/api-server/src/character_animation_assets.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
图片画布序列帧播放不要复用普通图片淡入样式
- 现象:角色动作序列帧播放时看起来像每帧之间在渐变或闪烁。
- 原因:序列帧播放器每帧切换可低至 40ms,默认约 125ms 一帧;如果帧
<img>复用普通图片的image-canvas-editor__layer-image--loading/--loaded,其中opacity 180ms ease会跨过下一帧切换,形成类似交叉淡入淡出的视觉。 - 处理:
ImageCanvasImageSequenceFrame只使用序列帧专属 class,帧显隐用同步opacity硬切,并显式transition: none;保留“下一帧未加载时继续显示上一帧”的 readiness gate。 - 验证:
ImageCanvasWorldView.test.tsx应断言序列帧<img>不带普通图片 loading/loaded class,且 style 中transition为none。 - 关联:
src/components/image-editor/ImageCanvasWorldView.tsx、src/index.css、src/components/image-editor/ImageCanvasWorldView.test.tsx。
Vidu 文生音频线上网关可能要求 sound 字段
- 现象:画板点击
生成游戏音效后,请求返回Failed to deserialize the JSON body into the target type: missing field sound。 - 原因:VectorEngine Apifox
创建文生音频任务文档仍写/ent/v2/text2audio使用model + prompt + duration,但线上 Vidu 网关曾按sound字段反序列化;只发送prompt会被上游拦截在 JSON 解析阶段。 - 处理:前端和 BFF 对内继续使用用户语义更清晰的
prompt;platform-audio转发到 VectorEngine Vidu 时同时发送prompt与sound,两者值保持一致。不要把 UI 改回 Sunotask: "sound"、type、tempo或 BPM。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml --test vector_engine_audio中音效请求体测试必须同时断言prompt与sound;必要时用线上生成音效 smoke 确认不再出现missing field sound。 - 关联:
server-rs/crates/platform-audio/src/request.rs、server-rs/crates/platform-audio/tests/vector_engine_audio.rs、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
Suno 任务完成或返回 audiopipe 不代表已经拿到稳定下载地址
- 现象:画板生成背景音乐时,前端可能报
音频生成尚未返回可下载地址(requestId:...)、获取 Suno 音效 wav 失败(requestId:...)或读取生成音频内容失败:error decoding response body。上游任务可能已经完成并返回https://audiopipe.suno.ai/?item_id=...,但该地址仍可能以200 + chunked开始响应后不返回完整正文。 - 原因:VectorEngine Suno
/suno/fetch/{task_id}可能先在data中返回歌曲 / 音效 clip id,或返回只携带item_id的 audiopipe 流式中转地址,而不是稳定.wav/.mp3文件 URL;需要再调用/suno/act/wav/{clipId}获取实际文件地址。如果只在“完全没有 URL”时回退 wav,会误把 audiopipe 当最终文件并让 worker 在正文读取阶段卡满请求超时。 - 处理:
platform-audio查询 Suno 结果时保留普通直接音频 URL;遇到 audiopipe 时不直接下载,而是从查询结果的id/clip_id/audioId/songId或 audiopipeitem_id提取 clip id,逐个调用/suno/act/wav/{clipId}。已拿到 clip id 但 wav 地址仍未就绪,或 wav 子请求暂时返回上游错误时,都保持processing让上层继续轮询,不能直接判定为缺少可下载地址或 wav 获取失败。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml;cargo test -p api-server vector_engine_audio_generation --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-audio/src/client.rs、server-rs/crates/platform-audio/src/response.rs、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
图片画布音频卡播放条 0:00 要优先查签名 URL 和嵌套交互
- 现象:画板音效或背景音乐已经生成成功,但卡片里的播放条显示
0:00,点击无法预览。 - 原因:generated 音频资源通常是私有 OSS 路径,直接把
/generated-*或 generated OSS 地址交给<audio>会无鉴权读取失败;如果音频控件嵌在<button>图层里,浏览器还可能因嵌套交互元素阻断 controls 行为。 - 处理:音频图层使用非嵌套交互容器承接画布选择语义,内部
<audio controls preload="metadata">单独阻止 pointer / click 冒泡;generated 音频播放前统一通过useResolvedAssetReadUrl//api/assets/read-url换签。卡片和角标展示时长,后端没返回时长时可用loadedmetadata.duration兜底。 - 验证:
npx vitest run src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts --reporter verbose,并在浏览器确认 generated 音频控件可播放。 - 关联:
src/components/image-editor/ImageCanvasWorldView.tsx、src/components/image-editor/ImageCanvasMediaModel.ts、docs/【编辑器】画板音乐生成入口设计-2026-06-18.md。
图片编辑器底部生成按钮不要复用单一画布生成状态
- 现象:图片画布里先新建一个“生成规范”占位,再点击“生成角色形象”或其它底部生成入口,前一个规范占位和面板状态被销毁。
- 原因:底部普通生成、规范、角色和图标素材曾共用单个
generateDialog状态;后一次点击直接覆盖该状态,等同把前一个画布生成对象卸载。 - 处理:底部生成类入口每次点击都创建独立 generation dialog id;当前 active 对象只负责显示编辑面板,旧对象归档为 inactive 后仍保留占位和生成逻辑状态。生成完成 / 失败回写、生成中拖拽和删除都必须按 dialog id 读取 active + inactive 中的最新对象,不能回退到提交瞬间的旧占位快照。
- 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps existing generation placeholders"应断言规范占位和角色占位可同时存在;npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps archived generation logic"应断言旧对象归档后拖动,占位完成回写仍落在最新位置。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器生成中设定面板不要和预览框绑成同一可见性
- 现象:图片编辑器里点击生成后,有时设定面板没收起,有时连画布上的占位预览一起消失,看起来像“生成中界面掉了”。
- 原因:生成中状态只收了 composer 可见性,或把占位框和设定面板共用了同一段条件渲染;面板隐藏后把 placeholder 也一起卸掉,就会丢掉 Lovart 式生成中预览。
- 处理:进入
generating后只隐藏设定面板,保留占位框和生成中状态胶囊;面板外观、预览框和结果图层分开控制,不共用同一个composerOpen条件。 - 验证:对应测试应断言生成按钮点击后
dialog消失但image-canvas-editor__generation-frame--generating仍然存在。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx。
图片画布生成器全体点不开先查卡住的临时交互状态
- 现象:特定操作后,画布中已有生成器点击不再显示设定对话框,而且不是单个生成器坏掉;新建生成器或刷新页面后恢复。
- 原因:旧生成器激活依赖全局交互状态;如果
Shift/ 空格按住态因为窗口失焦漏掉keyup,或“从画布选择参考图”等临时 picking / 菜单状态没有在激活旧生成器时清理,后续点击会被当成多选或选参考图而短路。 - 处理:窗口
blur/ 页面隐藏时释放Shift和空格按住态;激活已有 generation dialog 时同步清理参考图 picking、规格 / 参考菜单和右键菜单;active / inactive 生成器状态的 ref 与 React state 必须同事件周期同步。 - 验证:
npm run test -- src/components/image-editor/useCanvasGenerationDialogs.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx -- --runInBand,并跑ImageCanvasEditorView.test.tsx确认真实组件链路仍能激活生成器。 - 关联:
src/components/image-editor/useCanvasGenerationDialogs.ts、src/components/image-editor/useImageCanvasKeyboardShortcuts.ts、src/components/image-editor/ImageCanvasEditorView.tsx。
图片画布素材多时拖拽卡顿先查等距吸附候选规模
- 现象:画布素材数量增加后,拖拽单个图层或生成占位框时 pointermove 明显卡顿,关闭或绕开吸附后体感恢复。
- 原因:边缘 / 中心线吸附是线性扫描,但等距吸附如果对所有可吸附素材做两两配对,会在素材数量上来后进入 O(n²) 热路径。
- 处理:保留边缘 / 中心线全量线性扫描;等距吸附先过滤跨轴相交素材,再只检查轴向邻近候选,不要为远处或不相交素材生成配对候选。
- 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasInteractionModel.test.ts,并在多素材画布拖拽时确认参考线仍能命中邻近图层且 pointermove 不再明显掉帧。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/ImageCanvasInteractionModel.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布视口拖动卡顿先查自动保存和小地图合帧
- 现象:素材多或序列帧多时,拖动小地图视口框或手型平移明显卡顿,像是接口慢或 CSS 动画掉帧,但网络请求不一定异常。
- 原因:
pointermove高频修改viewport会触发画布重渲染、小地图模型重算和工程持久化 effect;持久化链路会同步serializeCanvasLayout、JSON.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.ts、src/components/image-editor/useImageCanvasStageInteractions.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器宣发素材生成器刷新后不要丢快照
- 现象:图片画布刷新后,宣发素材生成卡片消失,或卡片仍在但游戏名、分类、描述和参考图丢失。
- 原因:画布布局把生成器保存为
itemType: "generation-dialog",但恢复白名单漏掉publication模式和publicationWorkflowId/publicationGameInfo/publicationReferences字段,导致整条生成器快照被当成无效布局项丢弃。 - 处理:
hydrateCanvasGenerationDialog必须把publication视为正式画布生成器模式,并显式恢复宣发素材专属字段;组件层应断言刷新回读项目快照后仍显示卡片类型、字段和参考图。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx --reporter verbose。 - 关联:
src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/ImageCanvasPublicationMaterialsDemoPanelView.tsx、src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。
图片画布快速编辑不要直接提交普通图片 URL
- 现象:图片画布快速编辑站内示例图、历史 generated 图或 OSS generated 图时,后端返回
修改图片参考图必须是图片 Data URL。。 - 原因:快速编辑直接把图层
src塞进/api/editor/images/generations的referenceImageSrcs;默认示例图和部分持久化图层的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.ts、src/components/image-editor/ImageCanvasEditorView.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器角色动画不要默认提交大图 Data URL
- 现象:图片编辑器里对角色图点击
生成动画后,后端返回Failed to buffer the request body: length limit exceeded,请求还没进入角色动画 handler。 - 原因:角色动画生成请求曾把角色图片
src原样作为sourceImageSrc放进 JSON;角色图如果是较大的 Data URL,会超过 Axum 默认2MBbody limit,在Json提取器阶段被拦截。 - 处理:前端在角色图已持久化时优先提交
objectKey,只把 Data URL 作为未持久化本地临时图兜底;后端/api/editor/character-animations/generations单独配置12MBbody 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.tsx、server-rs/crates/api-server/src/modules/play_flow.rs、server-rs/crates/api-server/src/app.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
图片编辑器角色动画抽帧不要采到视频尾点
- 现象:画板角色图点击
生成动画后,Ark 视频已生成并上传 OSS,但后端返回ffmpeg 已执行但未产出动作帧文件(requestId:...)。 - 原因:FFmpeg 在
-ss采样时间落到视频尾点附近时可能退出码仍为0,但实际输出0帧;如果后端按duration - 0.001抽最后一帧,低帧率或短视频很容易踩到不可解码尾点。 - 处理:角色动画抽帧按目标帧数预留一个采样步长,例如
32帧·4秒最后一帧采3.875s,不要采3.999s;ffmpeg返回成功但无输出文件时,错误 details 保留targetSeconds、stdout、stderr和输出路径,用户主文案保持简短。 - 验证:
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.rs、docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md。
Windows 本地角色动画抽帧找不到 ffmpeg 先查 dev 子进程环境
- 现象:画板角色动画抽帧报
抽取动作视频帧失败:无法启动进程 ffmpeg:program not found(requestId:...),但新开的 PowerShell 里ffmpeg -version正常。 - 原因:长期运行的
api-server可能是在安装 FFmpeg 或更新用户 Path 之前启动的,子进程不会自动继承后续写入的用户环境变量。 - 处理:Windows 本地默认把 FFmpeg 安装到
%LOCALAPPDATA%\Genarrative\ffmpeg\bin,并确保用户 Path 包含该目录;npm run dev/npm run dev:api-server会在启动api-server时自动注入该目录和CHARACTER_ANIMATION_FFMPEG_PATH/CHARACTER_ANIMATION_FFPROBE_PATH绝对路径。修复后需要重启api-server,不能只刷新浏览器。 - 验证:
where ffmpeg、where ffprobe能找到本地安装;npm run test -- scripts/dev.test.ts -t "FFmpeg";重启npm run dev:api-server后访问/healthz。 - 关联:
scripts/dev.mjs、server-rs/crates/api-server/src/config.rs、server-rs/crates/api-server/src/character_animation_assets.rs。
图片编辑器生成长请求完成态必须由后端写入画布
- 现象:画板角色形象等生成请求已经在服务端返回
200,OSS 中也已有generated-character-drafts/.../image.png,但用户刷新或页面重载后仍看到旧生成卡片停在“生成中”。 - 原因:生成是一次长 HTTP 请求,浏览器在请求完成前刷新或重新挂载时会丢失原页面的成功回调;如果完成态只靠前端回调把结果图层写回
editor_canvas.layers_json,服务端虽然已经创建editor_project_resource/editor_asset,但布局里的generation-dialog仍可能停在status="generating"且没有generatedLayerId。如果之后从素材库把同一私有素材加回画布,前端再次创建项目资源时若提交 signed URL / Data URL,还会触发413,进一步阻断资源行绑定。 - 处理:图片生成提交必须在有项目上下文时携带
canvasCompletion(生成器dialogId、标题和占位框);api-server生成成功并创建资源后,直接读取当前项目布局,只有当前布局仍存在对应生成器时才插入轻量结果图层、把生成器改回idle并写入generatedLayerId,再沿用后端当前 viewport 保存 layout 并返回最新项目快照。前端只应用该快照刷新显示,不在加载时根据资源行推断完成态;有项目上下文但后端没有返回快照时也不得本地补结果图层。为已有objectKey的图层创建项目资源时,imageSrc只提交/<objectKey>,不要提交 signed URL / Data URL。 - 验证:
cargo test -p api-server editor_canvas_generation_completion --manifest-path server-rs/Cargo.toml覆盖后端完成态写 layout;npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand覆盖前端提交canvasCompletion、应用后端快照、项目加载不推断完成态和objectKey资源创建不提交大 URL。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器项目和素材 payload 不能持久化内联媒体
- 现象:
/api/editor/projects*、素材库、项目资源或 layout payload 里出现数 MB 的data:image/*、data:video/*、data:audio/*,刷新恢复变慢,发布入口可能 OOM / 413,素材库缩略图还可能只显示文件名。 - 原因:生成、规范图、角色图、图标 / UI spritesheet、音视频或动画帧如果直接把 Data URL / signed URL 写入
editor_project_resource、editor_asset或editor_canvas.layers_json,就把媒体本体塞进了项目快照;signed URL 还会过期,素材库也无法稳定换签。 - 处理:登录态媒体必须先上传 OSS / asset object,持久化只写
imageSrc: "/<objectKey>"、objectKey、assetObjectId;素材库和图层缩略图都通过PlatformMediaFrame -> ResolvedAssetImage传objectKey并调用/api/assets/read-url。layout 序列化和后端保存要递归拒绝data:*/blob:;旧行有objectKey时读出归一成/<objectKey>,没有objectKey的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,也不能在后端快照回来前自动保存。生成扣费、失败退款或 queue 终态后,右上角泥点余额通过/profile/dashboard回读,不做本地乐观扣减。 - 验证:Network 中
/api/editor/projects*、PATCH /api/editor/projects/{id}、素材库接口不应出现data:image/data:video/data:audio;素材库和图层面板缩略图都能换签显示;npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/common/PlatformMediaFrame.test.tsx src/services/assetReadUrlService.test.ts src/services/image-editor/editorProjectClient.test.ts,后端跑cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/components/image-editor/ImageCanvasEditorModel.ts、src/components/image-editor/useImageCanvasProjectPersistence.ts、src/components/common/PlatformMediaFrame.tsx、src/services/assetReadUrlService.ts。
图片画布裁扩后刷新或去背景丢图先查项目资源化
- 现象:从规范图裁切 / 裁扩出新图层后立即执行去除背景,去背景完成时原裁扩图从画布消失;刷新后裁扩图仍不在,但去背景占位可能变成结果图。
- 原因:裁扩结果由浏览器 canvas 本地渲染为
data:image/png。如果项目态先把local-resource-*图层加入画布,serializeLayer不会保存src,resolveProjectResourceCreateImageSrc又会跳过内联 Data URL,后续应用后端 project snapshot 或刷新 hydrate 时找不到对应editor_project_resource,该源图层就会被过滤。去背景带canvasCompletion时后端只负责把结果写入生成占位,不会恢复这个未资源化的裁扩源层。 - 处理:项目上下文中的裁扩结果必须在加入画布前先上传 OSS / asset object,再创建
editor_project_resource,并用服务端返回的resourceId/objectKey/assetObjectId创建裁扩图层;随后去背景的sourceResourceId和图片读取都指向正式资源。queue 模式下去背景完成后,如果首次读取的项目快照中对应generation-dialog仍是generating或缺少generatedLayerId,前端短暂等待后再读取一次项目快照。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand应覆盖裁扩先上传再创建项目资源,以及去背景队列完成后对未完成占位进行二次项目读取。 - 关联:
src/components/image-editor/useImageCanvasGenerationWorkflow.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布项目封面上传失败要有本地展示兜底
- 现象:画布项目已反复打开、保存或操作,但
/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-coversIndexedDB 记录,刷新后应显示blob:封面图。 - 关联:
src/services/image-editor/editorProjectCoverCache.ts、src/components/project/ProjectGalleryView.tsx、src/components/project/ProjectCanvasCover.tsx、src/components/image-editor/useImageCanvasProjectPersistence.ts。
图片画布框选预览要复用源图换签缓存
- 现象:UI 设计素材提取或快速编辑框选时,画布上的红色框选还在,但底部“框选区域预览”卡片变成空白。
- 原因:预览图从原生
img改成ResolvedAssetImage后,如果没有传入源图同一套objectKey/refreshKey,它会另起一条/api/assets/read-url缓存维度;画布主图已经显示时,预览仍可能处于空签名或失败缓存状态。 - 处理:框选预览继续用
ResolvedAssetImage承接私有资源换签,但必须传源图objectKey,并使用taskId ?? resourceId作为refreshKey,和主画布图片保持同一签名缓存版本。只允许对data:、blob:或已带签名参数的 URL 设置fallbackSrc;不要把裸/generated...私有路径作为 fallback 写进img。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasUiAssetExtractionOverlayView.test.tsx --reporter=dot应断言私有框选预览带objectKey、refreshKey,且裸 generated 路径没有 fallback。 - 关联:
src/components/image-editor/ImageCanvasUiAssetExtractionOverlayView.tsx、src/components/ResolvedAssetImage.tsx、src/hooks/useResolvedAssetReadUrl.ts、src/services/assetReadUrlService.ts。
图片画布发布入口 429 先查自动保存 PATCH 并发
- 现象:发布域名访问画板时出现短时间密集
429,Nginx access log 中PATCH /api/editor/projects/<projectId>、生成接口和资料接口混杂,429 行常见request_time=0.000、upstream_status=-,error log 写limiting connections by zone "genarrative_api_conn"。 - 原因:这类 429 是入口 Nginx
limit_conn在转发前拒绝,不是 api-server、SpacetimeDB、worker 或 VectorEngine 的业务 429。画布自动保存如果只有防抖、没有 in-flight 串行保护,慢PATCH /api/editor/projects/{projectId}未完成时,拖拽生成器、资源回填和后续状态变化会继续发起新的保存请求,同一客户端连接数被长请求撑满后触发入口连接限流。 - 处理:不要先放大 Nginx 限流或把错误归给生成 provider;先看 access log 的
upstream_status/request_time和 error log 的limit_connzone,再查前端保存路径。useImageCanvasProjectPersistence中自动保存和资源创建后的布局保存必须共用串行队列:同一时刻只允许一个saveEditorProjectLayoutin-flight,期间新快照覆盖旧待保存快照,当前保存结束后只发送最新一次。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx -t "serializes project layout saves" --reporter verbose应覆盖慢保存期间不启动第二个 PATCH,首个保存完成后只发送最新待保存快照;排查发布现场时 429 行应从upstream_status=-/ Nginxlimit_conn收敛。 - 关联:
src/components/image-editor/useImageCanvasProjectPersistence.ts、src/components/image-editor/useImageCanvasProjectPersistence.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布发布入口 429 也要查 read-url 换签爆发
- 现象:发布域名刚上线或刷新画板后出现短时间
429,Nginx access log 中集中为同一 IP / 同一editor/canvas?projectid=...referrer 的GET /api/assets/read-url?objectKey=generated-character-drafts/editor/ui-design-assets/.../asset-001.png到几十上百个 UI 设计切片;429 行常见request_time=0.000、upstream_status=-,error log 写limiting requests ... zone "genarrative_api_rps"。 - 原因:这类 429 是入口 Nginx
limit_req在转发前按 RPS burst 快拒,不是 api-server、SpacetimeDB、worker 或 VectorEngine 的业务 429。UI 设计提取、角色动画帧或大量私有素材恢复会让多个ResolvedAssetImage同时挂载;如果/api/assets/read-url只有同 key pending 去重和缓存,没有跨 objectKey 节流,一个页面能在同一秒内发出数百个不同 objectKey 换签请求并打满genarrative_api_rpsburst。 - 处理:不要先放大 Nginx 通用 API 限流;先按 access log 聚合
read-url数量、状态和 referrer,确认是否同一画板页面触发。assetReadUrlService必须统一承接私有 generated 资源换签,并在真实请求前做跨组件轻量节流;画板、素材库、运行态和结果页不得直接绕过该服务调用/api/assets/read-url。 - 验证:
npm run test -- src/services/assetReadUrlService.test.ts --reporter verbose应覆盖大量不同 objectKey 同时换签时首批限量放行、后续按间隔派发;发布现场同类页面刷新时,NginxGET /api/assets/read-url429 应从upstream_status=-/genarrative_api_rps收敛。 - 关联:
src/services/assetReadUrlService.ts、src/hooks/useResolvedAssetReadUrl.ts、src/components/ResolvedAssetImage.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片编辑器 Seedance 2.0 参考媒体不要提交视频 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 verbose;cargo test -p api-server editor_video --manifest-path server-rs/Cargo.toml;cargo test -p shared-contracts editor_video_request_supports_seedance_multimodal_references --manifest-path server-rs/Cargo.toml。 - 关联:
src/services/image-editor/editorReferenceUploadClient.ts、src/components/image-editor/useImageCanvasUploadWorkflow.ts、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、server-rs/crates/api-server/src/character_animation_assets.rs、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片编辑器生成类菜单要挂到页面级 portal
- 现象:底部
生成规范菜单、角色面板里的角色规范来源菜单点击后像没有弹出来,实际被按钮所在的局部滚动容器挡住了。 - 原因:菜单仍然渲染在底部工具栏或参考图横向滚动行内部,父容器带
overflow,弹层无法越出边界;即便挂到 portal,如果菜单根节点的pointerdown继续冒泡到画布视口,也会先触发画布失焦并卸载面板,导致菜单项click前消失。 - 处理:这类轻量菜单统一用页面级 fixed portal 挂到
document.body,位置根据触发按钮的getBoundingClientRect()计算;PlatformFloatingMenu根节点必须阻止pointerdown冒泡,避免画布清空当前生成面板;底部 AI 工具栏在生成面板打开时仍保持可见,不要整栏隐藏。 - 验证:测试断言菜单不包含在底部工具栏 / 参考图行里,并且生成面板打开时底部
AI画布工具栏仍存在;规范参考图来源菜单应能通过 portal 点击“从画布中选择 / 上传图片”并写回规范参考图。 - 关联:
src/components/common/PlatformFloatingMenu.tsx、src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。
图片编辑器规范图片面板不要脱离统一生成 shell
- 现象:生成 UI 设计图或新建图标规范时,面板参考图、输入区和底部生成按钮相对生成图片 / 生成角色 / 生成视频错位;图标规范甚至可能缺少首行参考图入口。
- 原因:规范、UI 设计图等面板虽然都属于生成类入口,但 JSX 和 CSS 曾各自维护
spec-footer、局部 field wrapper 或缺省参考区,导致后续改造只覆盖普通图片 / 角色 / 视频,规范图片类面板结构漂移。 - 处理:生成规范下的角色规范、图标规范、自定义规范,以及生成 UI 设计图,都必须复用
image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image外层 shell;首行统一image-canvas-editor__generation-ref,底部统一image-canvas-editor__generation-composer-footer+image-canvas-editor__generation-submit。多字段内容只在中央字段区保持紧凑,不单独发明 footer 或省略参考区。 - 验证:
npm test -- src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx -t "生成UI设计图|生成规范|visible titles|图标规范|character spec"。 - 关联:
src/components/image-editor/ImageCanvasGenerationComposerView.tsx、src/index.css、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片编辑器生成占位图在生成中也要使用最新拖拽位置
- 现象:用户在图片编辑器里提交生成后继续拖动画布占位图,预览框可以移动,但生成完成后的真实图片仍落回提交瞬间的旧位置。
- 原因:生成提交函数闭包里保存了旧的
dialog.placeholder快照;如果完成回包仍用这个快照创建图层,就会丢失生成中期间的拖拽坐标。若handleGenerationFramePointerDown又按status === 'generating'拦截,则生成中占位图完全不能拖动。 - 处理:生成占位图的 pointer down 不因
generating禁止;普通图片、规范图、角色图和图标素材回包创建图层时,都从当前generateDialogRef.current.placeholder读取最新占位位置,失败后保留的占位图也继续走同一拖拽链路。 - 验证:
npm test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "keeps the generation placeholder draggable while the image is generating"。 - 关联:
src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/ImageCanvasEditorView.test.tsx、docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md。
图片画布 Lovart 新生成占位必须避让已有图层和占位
- 现象:用户在画布中心已有图片时继续点击“生成图片 / 生成视频 / 生成规范”等入口,新建的待生成占位压在已有图片或其它待生成占位上;生成完成后看起来像图片被覆盖或丢失。
- 原因:入口直接把 placeholder 放在当前视口中心,没有把已有图层、隐藏状态和 inactive generation dialog 的占位统一纳入避让计算,也没有在落点确定后把 viewport 平移到新占位中心。
- 处理:所有会创建 generation dialog 的入口都必须走
ImageCanvasGenerationPlacementModel,避让所有hidden !== true的图层和 active / inactive placeholder;按 32px 世界坐标间距外扩阻挡矩形,在候选点中选择距离当前屏幕中心对应画板位置最近且不重叠的位置,再调用centerViewportOnPlacement(...)保持缩放只平移。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationPlacementModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx。 - 关联:
src/components/image-editor/ImageCanvasGenerationPlacementModel.ts、src/components/image-editor/useImageCanvasGenerationWorkflow.ts、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片画布生成类 composer 打开后必须自动进入可见安全区
- 现象:生成器、快速编辑、裁扩或角色动作面板打开后,面板可能在当前画布视口外,或被底部工具栏 / 左下 dock 盖住,用户只看到一部分甚至完全看不到输入框。
- 原因:placement 只负责选择画布世界坐标里的占位落点,面板实际 DOM 宽高、
translateX(-50%)、移动端 fixed 样式和工具栏覆盖区域没有反向修正 viewport。 - 处理:所有画布内 composer / 面板渲染后统一走
resolveViewportForOverlayVisibility(...),用真实 DOM 矩形和工具栏安全边界只平移 viewport;新增入口不要在各自按钮 handler 里写独立偏移。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasGenerationSurface.test.tsx。 - 关联:
src/components/image-editor/ImageCanvasOverlayModel.ts、src/components/image-editor/useImageCanvasGenerationSurface.tsx、docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md。
图片画布重绘创建独立占位,快速编辑不要新建生成器
- 现象:用户点击图片素材的“快速编辑”后,画布上额外出现
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.ts、src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/services/image-editor/editorImageReference.ts。
图片画布快速编辑完成必须按目标图层回写
- 现象:图片快速编辑任务成功后,刷新页面素材库能看到新图,但画布上的源图没有替换。
- 原因:
/api/editor/images/edits只保存生成图、项目资源和素材;没有canvasCompletion时不会写editor_canvas.layers_json。sourceResourceId只能表示溯源,同一资源可出现在多个图层,不能用它来决定替换哪一层。 - 处理:图片快速编辑请求必须传
targetLayerId;后端在没有canvasCompletion的快速编辑完成分支里,用目标 layer id 和生成资源写回项目 layout。 - 验证:
npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand;后端验证至少覆盖editor_image_edit_request_omits_price_mud_points和editor_image_edit_can_complete_by_replacing_target_layer。 - 关联:
src/services/image-editor/editorProjectClient.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、server-rs/crates/api-server/src/editor_project.rs。
图片画布快速编辑尺寸要区分用户目标和 provider 对齐尺寸
- 现象:原图经过快速编辑后 Resolution 变成近似比例的 1K / 2K 预设;原图或框选标记图宽高不是 16 的倍数时,VectorEngine edits 直接拒绝请求。
- 原因:前端已有源图精确
originalWidth/originalHeight,提交时却按最近常用比例和 K 档重新计算size;后端又把非 16 倍数的目标尺寸和原始参考图字节直接放进 multipart,并以 provider 回图宽高落库和覆盖画布图层。 - 处理:画布快速编辑展示与常规图片生成一致的模型、比例和尺寸参数,默认继承来源生成器参数;缺少来源生成器时使用图层模型,并按真实分辨率推导比例和尺寸。用户当前选定的比例和尺寸共同决定业务目标分辨率,允许覆盖源图旧分辨率;api-server 只在 provider 边界向右、向下复制边缘像素,把每张参考图和目标尺寸临时补齐到 16 的倍数,收到回图后裁回业务目标尺寸再持久化。若上游异常返回其他尺寸,先按目标比例裁切缩放;临时对齐尺寸不能进入 OSS 元数据、
editor_project_resource、editor_asset或画布 Resolution。结果覆盖目标图层时更新原始分辨率,并保持图层中心位置不跳动。 - 验证:
npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx覆盖来源参数继承、模型参数切换、目标尺寸提交和图层回填;cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml覆盖图标类拒绝、provider 尺寸对齐和回图恢复。 - 关联:
src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts、src/components/image-editor/ImageCanvasGenerationLayerModel.ts、server-rs/crates/api-server/src/editor_project.rs。
图片画布纯尺寸变换必须先在内存决策再单次上传
- 现象:
nanobanana2已返回并成功解码图片,任务随后报“尺寸无效”;普通生图若把尺寸恢复作为硬失败,provider 已成功回图后仍可能没有素材进入素材库和画布。 - 原因:通用尺寸恢复把
nanobanana2的标量清晰度档位512 / 1024 / 2K当成WIDTHxHEIGHT解析;普通图片与快速编辑还把 OSS 持久化放在尺寸恢复之后。同步generateContent返回的是内联 base64,本地兜底 task id 不能用于向 provider 回查原图。 - 处理:
nanobanana2保留 provider 输出几何尺寸;其它模型仍按显式像素目标尝试恢复。普通生图和快速编辑先把 provider 回图留在内存,尺寸恢复成功后只上传变换结果,恢复失败则降级为只上传 provider 原图;每个主结果只执行一次 OSS 持久化并只创建一个素材。角色、图标图集、UI 提取和角色动作的 provider 原始输出按多产物语义单独保留并承载任务模型成本,后续抠图、逐帧处理和切片阶段成本为 0;中间产物沿用character、icon-spritesheet、character-animation等真实类型,不新增“原图类型”。扣费确认仍以 provider 成功为界,不延长到 OSS、后处理或画布回填;后台按任务显示最终产物父行,并把每个中间产物作为独立子行展开,分别展示阶段生成器和阶段成本。 - 验证:
cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml覆盖 nanobanana 标量尺寸、变换失败回落 provider 原图、变换先于单次持久化;cargo test -p api-server character_animation_assets --manifest-path server-rs/Cargo.toml覆盖角色动作原始预览的真实类型和成本归因;npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx覆盖中间产物逐行展开和成本文案。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx。
图片画布快速编辑元数据必须记录原图引用
- 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
- 原因:信息面板直接渲染
generationInputs.references;快速编辑虽然把原图作为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.ts、src/components/image-editor/ImageCanvasMetadataModalView.tsx、src/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" -- --runInBand;npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx -- --runInBand。 - 关联:
src/components/image-editor/useImageCanvasAssetLibrary.ts、src/components/image-editor/ImageCanvasEditorView.tsx、src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts。
Windows 本地 dev 不要把 RUSTC_WRAPPER 绕过写成 rustc
- 现象:Windows 上执行
npm run dev:api-server时,api-server 在 Cargo 启动阶段失败,日志出现error: multiple input filenames provided (first two filenames are ... rustc.exe and -),/healthz无法访问。 - 原因:
server-rs/.cargo/config.toml默认配置rustc-wrapper = "sccache";本地 dev 脚本为了绕过损坏的 sccache 需要覆盖 wrapper。Windows 下如果把RUSTC_WRAPPER设置为rustc,Cargo 会按 wrapper 协议调用rustc <真实rustc路径> - ...,真实 rustc 把 wrapper 传入的 rustc 路径和 stdin-都当输入文件。 - 处理:Windows 本地 dev 脚本应把
RUSTC_WRAPPER和CARGO_BUILD_RUSTC_WRAPPER显式设为空字符串,让 Cargo 覆盖项目配置并直连真实 rustc;Linux 保持/usr/bin/env绕过 sccache。 - 验证:
npm run test -- scripts/dev.test.ts -t "Windows 下本地 dev Rust env 用空 wrapper 覆盖项目 sccache",并用npm run dev:api-server拉起后访问实际 api 端口的/healthz返回 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Pingora 直连 80/443 不能只改 env
- 现象:
/etc/genarrative/pingora-gateway.env已把GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN/HTTP_REDIRECT_LISTEN改到0.0.0.0:443/0.0.0.0:80,但genarrative-pingora-gateway.service启动失败,日志出现低端口绑定权限错误。 - 原因:默认 service 用非 root
genarrative用户运行,并且主模板为了保持 shadow 安全边界不带CAP_NET_BIND_SERVICE。低端口直连必须通过显式 systemd drop-in 单独授予 capability;同时 Certbot 私钥默认未必允许genarrative读取,Nginx 也可能仍占用80/443。另一个常见误区是 API release 只带pingora-direct-enable.sh/ rollback 壳脚本,却漏带pingora-current-release-audit.mjs、pingora-direct-rehearsal-status.mjs、check-pingora-direct-preflight.mjs、check-pingora-direct-live.mjs、deploy/systemd/、deploy/env/或deploy/pingora/,导致从/opt/genarrative/current启用时依赖 Jenkins 工作区、源码 checkout 或/etc里某份参考模板;或者 release 已经包含新版pingora-gateway,但已运行的 shadow / canary / direct service 没有随current链接切换重启,仍在跑旧二进制。Jenkins API Build、API Deploy 和 Full Build-And-Deploy 默认要求 Pingora 产物,并用--require-pingora-gateway在部署阶段硬校验;手工本地 API 包仍需显式--include-pingora-gateway才会把二进制、checksum 和 manifest artifact 写入发布包。Server-Provision 安装到/etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf的 drop-in 只用于人工审阅和显式覆盖;直连启用脚本默认必须读取 current release 随包deploy/systemd/genarrative-pingora-gateway-direct-entry.conf,否则旧/etc模板会掩盖发布包缺失。API deploy 脚本本身也不能继续用部署工作区根部的scripts/deploy/production-api-deploy.sh,否则 Jenkins workspace 里的脚本会掩盖build/<version>发布包缺少 deploy / maintenance 同目录脚本的问题;备份脚本、健康巡检脚本和 env 示例目录同样不能从部署工作区兜底,切换命令证据脚本也不能从部署工作区兜底,否则 current release 会和上游构建归档漂移。Pingora 直连依赖、备份脚本、巡检脚本、env 示例目录和 API deploy 执行入口都必须来自上游发布产物;随包api-server.sha256和可选pingora-gateway.sha256也必须复制进 current release,供随包 current release 自审校验二进制;随包deploy/pingora/pingora-gateway.env.example也不能只检查存在,还要保持 gzip-only、不信任 XFF、前置代理确认关闭、接流保护开启和空 probe token 这些生产安全默认值;production-api-deploy.sh发现缺失时应在 current 切换前 fail-fast、清理 staging 并退出本次打开的维护模式,不应从部署工作区兜底补齐;所有 API 发布包都必须携带release-manifest.json且登记api-serverartifact,发布包包含 Pingora 时还必须登记pingora-gatewayartifact,否则 deploy 应在切换 current 前失败;deploy 必须要求 release root、current link 和 api env file 都是绝对路径,release version 以数字或字母开头并拒绝点目录,再先写 staging release,全部复制完成后用非合并语义提升为正式 release,失败时清理 staging 且不留下正式 release,同版本 release 已存在、提升前竞态出现或 current 路径不是符号链接时拒绝覆盖 / 合并,避免旧文件混入 current;发布包包含 Pingora 时,deploy 必须先确认 systemd 最终配置没有 direct-entryCAP_NET_BIND_SERVICE、env 仍是127.0.0.1:18081shadow 且未配置TLS_LISTEN/HTTP_REDIRECT_LISTEN,再提升 release、切换 current 并restartPingora shadow;配置不安全时必须在切换 current 前失败并退出本次打开的维护模式,current 切换后的 readiness / 服务重启失败仍保留维护模式。 - 踩坑补充:Bash 的进程替换
< <(...)不会自动把生产者子进程的失败状态传给消费循环。Pingora systemd 检查若在子进程发现CAP_NET_BIND_SERVICE后直接退出,父函数仍可能继续用空列表输出“缺少 EnvironmentFile”,外层命令替换又继续用空 env 输出“LISTEN 为空”,形成三条互相矛盾的错误。部署前检查必须先捕获并显式检查配置提取命令的退出状态,再解析 EnvironmentFile;首错失败后立即返回。回归用npm run check:production-api-deploy的 direct-entry fixture 同时断言后两条误报不存在。 - 踩坑补充:旧 release 可能没有
pingora-gateway二进制,但 systemd 仍残留历史direct-entry.conf,同时 Nginx 已正常接回80/443、Pingora inactive、env 已是 shadow。此时不要直接执行当前pingora-direct-rollback.sh --apply:脚本删除 drop-in 后会固定重启 Pingora,因 current 二进制不存在而中止,后续 Nginx reload/smoke 不会执行。先确认 current 确实无可执行网关、Pingora inactive、env 已完整恢复 shadow、Nginx 配置与公网 smoke 正常,再以单次 fail-fast 运维命令删除 stale drop-in、daemon-reload、复核 capability/DropInPaths 已清空,随后 reload(若 inactive 则 start)Nginx,并复核 Nginx/API/SpacetimeDB、正式 vhost smoke 和 health patrol nginx 模式;不要伪造--require-pingora-shadow验收。下一次包含 Pingora artifact 的 API Deploy 会在切换 current 后启动新 shadow 网关。 - 处理:确认真实 TLS 证书和 redirect env 已写入
/etc/genarrative/pingora-gateway.env、service 模板和systemctl cat最终配置读取的EnvironmentFile=都包含这份 env、当前执行用户和genarrative-pingora-gateway.service的User=服务用户都能读取证书链 / 私钥、current release 的pingora-gateway已存在且可执行、Nginx 或其它进程已释放80/443后,先用npm run plan:pingora-direct-cutover -- --require-direct ...生成只读 JSON runbook,并逐条审阅 Host 与回退巡检入口确认、current release 自包含自审、current release preflight、启用前基础 readiness、direct enable dry-run、direct enable apply、启用后--require-direct复核、rollback dry-run、rollback apply、回退后 health patrol 切回 Nginx 并恢复切换前 public base URL / Host、回退后 health patrol env 复核;runbook 只用于审阅,不修改系统。正式 runbook 中--direct-redirect-host、--rollback-nginx-smoke-host和--direct-host必须使用同一 hostname,只允许端口不同,避免 redirect 或回退 smoke 各自验证到不同入口;同时必须提供--rollback-health-patrol-public-base-url <切换前Nginx巡检入口>,若切换前 Nginx 巡检需要 Host 覆盖,再追加--rollback-health-patrol-public-host <切换前Host>,确认步骤会展示回退后要恢复的 public base URL / Host,避免回退 runbook 把现场巡检入口覆盖成仓库默认值;如需把回退后 Pingora shadow 探针复核纳入 runbook,追加--rollback-pingora-shadow-probe-url/--rollback-pingora-shadow-probe-token,JSON 输出会隐藏 token 原文。随后先执行/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show,再 dry-run/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --no-status,最后执行/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --preflight-check-cert-readable --preflight-check-service-env-file --preflight-check-service-user-cert-readable --preflight-check-service-binary-executable --preflight-check-ports-free --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,由脚本先跑 direct preflight,再安装 drop-in、reload systemd、重启 Pingora,并用systemctl cat核验 capability 和EnvironmentFile=/etc/genarrative/pingora-gateway.env已生效、用systemctl show ... ExecStart核验最终 service 仍指向随包主 service 模板里的 current releasepingora-gateway、用systemctl is-active确认服务 active,再以 JSON 模式执行 direct live smoke,验证 HTTPS / HTTP redirect / ACME / WSS 101 和 Pingora access log request_id 落盘,并要求direct-access-log结构化结果matchedCount == checked、missingCount=0、mismatchCount=0;如果 direct live 退出 0 但缺少该结构化证据,也必须视为启用失败。直连启用后同步调整/etc/genarrative/health-patrol.env:设置GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct,本机打127.0.0.1时设置GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>,否则巡检会继续按 Nginx 模式误报。验证失败时执行/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>'或npm run deploy:pingora-direct-rollback -- --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>';回退脚本先跑nginx -t,通过后才移除 drop-in、reload systemd、重启 Pingora,并用systemctl cat核验 capability 已移除、用systemctl show ... ExecStart核验最终 service 仍指向随包主 service 模板里的 current releasepingora-gateway,随后 reload Nginx、确认 Nginx service 仍为 active,并用 curl smoke URL 证明公网入口已回到 Nginx;回退 smoke URL/body 必须来自切换前真实 Nginx 入口,不要继续用固定http://127.0.0.1/healthz与"ok":true;回退脚本--apply不允许省略--reload-nginx或--nginx-smoke-url,当 smoke URL 指向本机地址时必须同时提供--nginx-smoke-host <域名>,且 host 值不能包含 URL、路径或查询;回退后把 health patrol gateway mode 改回nginx,恢复切换前 public base URL / Host,并用node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs --env-file /etc/genarrative/health-patrol.env --expected-gateway-mode nginx --expected-public-base-url <切换前Nginx巡检入口> --require-empty-public-host复核;若切换前 Nginx 巡检需要 Host 覆盖,则把--require-empty-public-host换成--expected-public-host <切换前Host>。若 env 已在回退命令前切回 Nginx,也可给 rollback 脚本追加--health-patrol-env-file /etc/genarrative/health-patrol.env --health-patrol-expected-public-base-url <切换前Nginx巡检入口> --health-patrol-require-empty-public-host让它在 Nginx smoke 后自动复核;切换前 Nginx 巡检需要 Host 覆盖时把最后一项换成--health-patrol-expected-public-host <切换前Host>。若要同时证明 Pingora shadow 高端口仍活着,追加--pingora-shadow-probe-url http://127.0.0.1:18081/__genarrative_pingora/healthz --pingora-shadow-probe-token <token>,脚本会隐藏 token 并要求响应为gateway=pingora-shadow。 - 处理补充:不要直接 chmod
/etc/letsencrypt/live或archive来让 Pingora 读取证书;Certbot live 路径通常是 symlink,即使stat -L看起来是普通文件,父目录权限也会让非 rootgenarrative用户不可达。先用随包node -- /opt/genarrative/current/scripts/deploy/pingora-tls-cert-sync.mjs --apply --source-cert-file /etc/letsencrypt/live/<域名>/fullchain.pem --source-key-file /etc/letsencrypt/live/<域名>/privkey.pem --target-dir /etc/genarrative/pingora-tls/<域名>把证书同步到 Pingora 私有目录,再让GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE/TLS_KEY_FILE指向/etc/genarrative/pingora-tls/<域名>/fullchain.pem和privkey.pem。脚本默认 dry-run,--apply才写入,目标目录默认root:genarrative 0750,文件默认root:genarrative 0640,并拒绝符号链接目标目录或目标文件。 - 处理补充:不要在切换窗口手工编辑
/etc/genarrative/health-patrol.env的三项网关变量;使用node -- /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --apply --env-file /etc/genarrative/health-patrol.env --gateway-mode pingora-direct --public-base-url <直连HTTPS入口> --public-host <域名>切到直连,回退前用同一脚本传--gateway-mode nginx --public-base-url <切换前Nginx巡检入口>并按切换前记录选择--clear-public-host或--public-host <切换前Host>。脚本只改 gateway mode / public base URL / public Host,并立即复用随包 env 复核脚本,减少空 Host 和旧值残留;生产巡检、env 复核和 env 切换脚本读取的布尔 env 都必须是明确布尔值,非法值直接失败,不能把拼写错误当成 false;env 复核脚本的--env-file与 env 切换脚本的--env-file/--check-script必须是绝对路径且不能是文件系统根目录,也不能包含换行或 NUL;env 切换脚本写入的 public base URL / Host 同样不能包含换行或 NUL。Node 22 已内置--env-file启动参数,直接用node script.mjs --env-file ...或 shebang 执行.mjs --env-file ...都可能让 Node 抢走业务参数;所有这类命令都必须写成node -- script.mjs --env-file ...,或通过已内置node --的 npm script 执行。 - 踩坑补充:health patrol env 切换脚本必须先复核权限固定为
0600的临时目标 env 再写真实文件,真实 env 原子替换时保持原文件权限和 owner/group;如果随包 env 复核脚本失败,--apply应失败且真实 env 保持原样,避免“切换脚本失败但巡检配置已半改”的状态。--apply的--env-file必须直接指向真实普通文件,不能传符号链接;如果/etc/genarrative/health-patrol.env是链接,先确认真实目标路径后再传给脚本,避免替换链接本身或写入非预期目标。 - 踩坑补充:直连启用脚本的
--preflight-script、--direct-live-script、--current-release-audit-script、--template-path、--service-unit-path、--dropin-path和 env 文件参数都必须使用绝对路径;不要在切换窗口传相对脚本路径,否则会把 current release、Jenkins 工作区或现场 cwd 混在一起。--apply会在安装 direct-entry drop-in 前确认 current release 自审、direct preflight 和 direct live smoke 脚本存在,缺脚本时应先修发布包或复制链路,不要手工改成工作区相对路径绕过。启用脚本还会在任何自审、preflight、drop-in 写入或 systemctl 前拒绝 service、路径、URL、Host、probe token、access log、数据库名、tail 行数和 timeout 参数中的换行或 NUL 字符;遇到这类失败先修 runbook 参数来源或现场 env,不要手工绕过脚本。启用脚本还会拒绝符号链接形式的 drop-in 目录或 drop-in 目标文件,以及已存在但不是普通文件的目标;如果现场 systemd 目录被软链改写,应先修正真实路径,不要让脚本把低端口 capability 写入非预期位置。回退脚本--apply同样会在nginx -t和删除 drop-in 前拒绝 service、路径、Nginx smoke URL / Host / 响应片段、health patrol 复核参数、shadow probe URL / token 和二进制 override 中的换行或 NUL 字符,并拒绝符号链接 drop-in 目录 / 目标以及非普通 drop-in 目标;如果现场路径或参数异常,应先修正 systemd 路径、runbook 参数或现场 env,不要手工删 drop-in、绕过nginx -t或把删除 symlink 当成已回退真实低端口能力。回退脚本覆盖--nginx-binary或--curl-binary时也不要传./nginx、tools/curl这类相对路径;裸命令名可以走PATH,路径形式必须使用绝对路径。--nginx-smoke-url必须带http://或https://,不要只写 host/path,否则脚本会在移除 drop-in 前失败。 - 踩坑补充:回退到 Nginx 后不要只把
curl --fail/ HTTP 200 当作 Nginx 已接回的证据;正式 runbook 必须给 rollback dry-run / apply 显式传切换前真实 Nginx smoke URL 和响应体片段,例如--nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>'。不要继续用固定/healthz与"ok":true,否则要么误卡回退,要么只验证到了错误入口。 - 踩坑修正:上述
/healthz与"ok":true只能算旧示例,不再是正式 runbook 默认。dev 真实直连80/443测试确认回退 smoke 必须从切换前真实 Nginx 入口取样,例如https://dev.genarrative.world/与<!doctype html>;固定http://127.0.0.1/healthz可能返回 301/404 或命中错误 vhost。回退前还必须把/etc/genarrative/pingora-gateway.env从 direct 低端口配置恢复为 shadow 高端口配置,否则回退脚本移除 capability 后重启 Pingora 可能继续按80/443配置失败;恢复 shadow 时不能只清GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN/HTTP_REDIRECT_LISTEN,也必须清空GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE/TLS_KEY_FILE,避免留下证书路径但无 TLS listener 的半直连 env。 - 踩坑补充:直连彩排状态脚本不是修复动作。
node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs --release-root /opt/genarrative/current --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical只读检查公网端口归属、health patrol 模式、Pingora shadow、realpath canary、systemd 和 current release 自审;如果它报CRITICAL,应先修发布包、端口归属、canary 配置、health patrol env 或 systemd 指向,不要把它当成会自动启用 canary、停止 Nginx 或修复 current release 的脚本。 - 踩坑补充:Pingora current release 自审和切换证据链都不是修复动作,
npm run check:pingora-current-release-audit/scripts/ops/pingora-current-release-audit.mjs只负责只读确认发布包自包含、api-server.sha256/pingora-gateway.sha256匹配、release manifest 登记了当前要接流的 Pingora 产物、pingora-gateway可执行和 systemdExecStart指向;npm run check:pingora-cutover-status-snapshot/scripts/ops/pingora-cutover-status-snapshot.mjs只负责输出pre-cutover、post-enable、post-rollback三阶段只读 JSON evidence,并在直连 runbook 中通过--require-pingora-gateway把上述自审结果收录到checks.current-release-audit.details;快照还必须确认systemctl cat genarrative-pingora-gateway.service的EnvironmentFile=精确包含本次--pingora-env-file,否则systemd.pingoraUnit.environmentFileMatchesPingoraEnvFile=false且标记CRITICAL,避免证据包读到一份 env、真实服务读另一份 env。正式切换窗口用scripts/ops/pingora-cutover-evidence-bundle.mjs把快照 JSON、stdout、stderr、命令记录和 manifest 写入--output-root下的新证据目录,证据包 manifest 必须记录已生成 snapshot、direct live、stdout / stderr、命令记录和 parse-error 文件的path、sizeBytes与sha256,便于归档后复核;证据目录生成、复制或归档后必须用随包scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir <bundleDir>做只读验真,确认manifest.files登记的文件未缺失、大小未漂移、sha256 未漂移,且证据目录不是符号链接或非目录;三阶段证据分别验真后,还必须用随包scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback做只读总审计,自动选择每个阶段最新 bundle 并复用 verifier,缺阶段、最新证据损坏、坏 manifest 或符号链接条目都应失败。direct enable apply / rollback apply 必须通过scripts/ops/pingora-cutover-command-evidence.mjs包装真实脚本,单独保存命令 stdout、stderr、退出码、脱敏命令记录和 manifest,runbook 必须显式传绝对路径--output-root,且该路径不能是文件系统根目录、符号链接或包含换行 / NUL 字符;命令记录必须同时保留脱敏后的可读命令和结构化executable/args[],命令证据 manifest 也必须记录command.stdout.txt、command.stderr.txt和command-record.json的sizeBytes与sha256;命令证据生成后也要立即把 stdout 中的bundleDir填入随包 verifier 的<enable-apply-bundle-dir>或<rollback-apply-bundle-dir>占位符做只读验真,不能只等最终根目录总审计才发现 command-record 或 stdout/stderr 归档漂移。启用后证据包必须额外运行随包 direct live smoke,并写入direct-live.json、direct-live.stdout.txt、direct-live.stderr.txt、direct-live-command.json和 manifest summary 的directLiveStatus,让 Pingora access logrequest_id反查结果可复盘;direct-live.json的direct-access-log结果必须保留扫描行数、匹配数量、缺失明细以及 method/path/status 漂移明细,不要只保留 count 或依赖 stderr。证据包 manifest summary 还必须包含directLiveAccessLog摘要;如果 direct live JSON 缺少direct-access-log结构化结果,整包应记为CRITICAL。若 snapshot 或 direct live stdout 解析失败,必须保留snapshot-parse-error.txt或direct-live-parse-error.txt并在 manifest / 最终 stdout 中给出路径;不要只截图或复制pingora-direct-enable.sh/ release readiness 的终端输出当作直连证据。证据阶段名只能使用 ASCII 字母、数字、点、下划线和短横线,非法--phase会直接失败,不会被清洗后继续落盘;自审、状态快照和证据包的--release-root都不能是文件系统根目录,状态快照的--health-patrol-env-file/--pingora-env-file以及证据包所有显式路径参数也不能是文件系统根目录,状态快照自身还必须拒绝带换行或 NUL 的 release/env 路径,并在执行 systemctl、current release 自审、health patrol env 复核或生产巡检子命令前复核子命令参数,证据包执行状态快照或 direct live 子命令前也必须拒绝任何带换行或 NUL 字符的子命令参数,避免污染后的结构化args[]先进入正式证据再等总审计兜底,--output-root及其已存在上级路径不能是符号链接,已存在的--output-root必须是真实目录,路径异常时会在执行状态快照前失败,避免把证据写入非预期软链目标;current release 自审、状态快照和证据包的显式--timeout-ms及对应 env 必须是正整数,生产健康巡检的--timeout-ms、--slow-ms、GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS和GENARRATIVE_HEALTH_PATROL_SLOW_MS,canary / direct live smoke 的--timeout-ms及对应 env,canary access log 对账的--since-lines及对应 env 也必须是正整数,直连 live / release readiness 的布尔 env 也必须是明确布尔值,非法值都会失败,不再静默回退默认值或 false。自审、快照、证据包、证据验真和证据总审计脚本都不修改/etc、systemd、Nginx 或 Pingora;命令证据脚本只执行--后面的真实命令并归档输出,不自行理解 systemd / Nginx;证据包和命令证据目录必须是0750,证据文件必须是0640,且不能覆盖既有文件;probe token 和其他 env 敏感值只能记录是否存在或显示<redacted>,不能把 env 原文写入终端执行日志、gateway smoke / direct live / direct rollback shadow probe 命令日志、stdout、snapshot、manifest、命令记录或子检查 stdout / stderr;如果自审、快照、direct live 或总审计证据里出现CRITICAL,应先修发布包、Jenkins 归档过滤、deploy 复制、env、systemd capability、直连入口、巡检状态或证据归档,再继续下一阶段,不要把自审、快照或证据包当成可自动修复的烟测。 - 踩坑补充:直连 Pingora 后不要让静态缓存头继续依赖框架默认值。HTML、目录 index 和 SPA fallback 必须保持
Cache-Control: no-cache,否则旧入口页可能长期引用已经切换的 chunk;带 Vite 指纹的/assets/*和/admin/assets/*才能使用public, max-age=31536000, immutable;普通非指纹静态和 ACME challenge 继续保守no-cache。如果需要临时覆盖GENARRATIVE_PINGORA_GATEWAY_*_CACHE_CONTROL,值不能包含换行或 NUL,修改后必须跑npm run check:pingora-gateway-smoke确认 HTML、普通静态和指纹资源三类响应头没有漂移。 - 踩坑补充:直连 Pingora 后也不能只验证整文件静态读取。浏览器、媒体探测和线上签名 URL 排障都可能使用
Range: bytes=;Pingora 静态文件必须支持单段 range 的206 + Content-Range和越界 range 的416 + Content-Range: bytes */<len>,同时给静态响应写入Accept-Ranges: bytes。If-Range不能被忽略:日期匹配才继续给局部内容,旧日期或弱 ETag 校验器应回完整200,避免客户端拿旧校验器拼接错误文件片段。206、304、416不应被 gzip 压缩,否则Content-Range指向的字节区间会和实际响应体不一致。多段 range 暂按完整文件处理,不要在切换窗口临时拼 multipart 响应。 - 踩坑补充:直连 Pingora 后不要让静态路由接受非读取方法。
POST /assets/app.js或POST /some/deep/link这类请求不应返回静态内容;命中静态候选时返回405 + Allow: GET, HEAD,缺失文件仍返回404。修改静态路由后跑npm run check:pingora-gateway-smoke,确认 405 没有被压缩或误写成 JSON 代理错误。 - 踩坑补充:静态响应不是代理路径,也必须有 access log 证据。修改静态协商缓存、方法限制或 Range 行为后,smoke 要用固定
X-Request-Id反查 Pingora access log 中同一行的path、status和proxy_target=Local,至少覆盖304、405、206、416;否则直连切换证据包可能只能证明 API / WSS 代理路径,排查浏览器缓存或媒体 Range 问题时缺少本地响应状态证据。 - 踩坑补充:直连 live smoke 不能只证明根 HTML 返回
200。正式发布包的首页通常会引用/assets/或/admin/assets/构建产物,direct live 应自动发现静态资源,验证静态缓存 / 校验头、HEAD头响应、If-None-Match/If-Modified-Since304 和Range: bytes=0-0,并纳入 access log method/path/status 对账;如果首页存在 Vite 指纹资源,还必须额外证明Cache-Control: public, max-age=31536000, immutable以及指纹资源 GET / HEAD / 304 / Range 的 access log method/path/status 证据,避免只验证普通/assets/app.js却漏掉旧 tab chunk 长缓存口径。如果该项显示 skipped,要确认是维护模式、非 HTML,还是发布包首页确实没有资产引用,不要把 skipped 当作已经验证前端静态资源可读。 - 踩坑补充:不要把
direct-live.json当成只有状态码的摘要。静态 GET / HEAD / 304 / Range 检查必须保留白名单headers,至少能复盘cache-control、etag、last-modified、accept-ranges、content-range、content-length和content-encoding;证据包自测要确认普通静态和 Vite 指纹资源的Cache-Control、Content-Range都被归档。API / WSS 检查不要落原始响应头,避免把认证、Cookie 或上游细节带进切换证据。 - 踩坑补充:切流证据包不能只把静态头部藏在
direct-live.json。manifest.summary.directLiveStaticHeaders必须提升普通静态和 Vite 指纹静态的缓存头、校验头、RangeContent-Range和 304 状态摘要,方便切换窗口先扫 manifest 判断证据是否完整;如果 direct live 已输出静态资产结果但摘要缺少缓存头、校验头、Range206 + Content-Range、ETag 304 或 Last-Modified 304 证据,证据包会直接记为CRITICAL。遇到摘要缺失或 diagnostics 非空时,应重新生成启用后证据包或修复 direct live / 静态响应头,不要手工改 manifest。 - 踩坑补充:最终证据根目录总审计必须带
--require-phase-direct-live-access-log post-enable --require-phase-direct-live-static-headers post-enable --require-phase-pingora-env-shadow post-rollback,正式 runbook 已默认生成这些参数。post-enable证据包还必须传--expected-pingora-env-mode direct,post-rollback证据包必须传--expected-pingora-env-mode shadow,否则 health patrol 模式正确也不能证明 active Pingora env 姿态正确。若旧post-enablebundle 虽然manifest.summary.status=OK但没有directLiveAccessLog或directLiveStaticHeaders,或旧post-rollbackbundle 没有manifest.summary.pingoraEnvShadow、没有mode=shadow/shadowReady=true、仍残留tlsCertFile/tlsKeyFile,总审计也应失败;处理方式是用新版 current release 重新生成对应阶段证据包,不要把旧包混进正式归档。 - 踩坑补充:最终证据根目录总审计失败时先看 JSON 顶层
summary,不要直接在长phases[]/commands[]里翻。summary.failedItems[]会聚合失败阶段、命令、根目录或时间线诊断,summary.directLiveEvidence[]会直接给出post-enable的accessLog.ok/reason与staticHeaders.ok/reason;reason 指向缺摘要或字段不完整时,应重新生成启用后证据包,而不是手工补 manifest。 - 踩坑补充:最终证据根目录总审计命令示例也必须包含五条
--require-command-executable,分别绑定 current release 随包pingora-direct-enable.sh、pingora-health-patrol-env-switch.mjs、pingora-gateway-env-shadow-switch.mjs、pingora-health-patrol-env-switch.mjs和pingora-direct-rollback.sh。不要只写--require-command和--require-command-arg --apply,否则只能证明有命令证据和参数,不能证明真实执行的是本次 current release 脚本。发布包级npm run check:production-api-release会同时检查生成 README 和随包 readiness dry-run cutover 输出,若这里失败,先修发布包构建脚本或随包 readiness 脚本,不要只改源码文档。 - 踩坑补充:直连 Pingora 后也不要只看前端页面和 access log 成功。API 上游必须继续收到 Nginx 口径代理头:
Host、X-Forwarded-Host、配置化X-Forwarded-Proto、TCP 对端 IP 的X-Real-IP,以及追加 TCP 对端 IP 的X-Forwarded-For。TRUST_X_FORWARDED_FOR只用于接流保护 client key;如果误以为它会改变上游X-Real-IP或覆盖上游X-Forwarded-For,容易造成回调 URL、鉴权来源或日志归因排查漂移。修改代理头逻辑后先跑npm run check:pingora-gateway-smoke,让 mock 上游回显这些头。 - 踩坑补充:current release 自审开启
--systemd-show时,带换行或 NUL 的--release-root/--systemd-service必须在执行systemctl show前失败,不能把污染参数写进子命令或后续证据链。遇到这类失败先修 runbook 参数来源,不要用--warn-only继续采集。 - 踩坑补充:canary live 的
--base-url、--prefix、--host、--path、--timeout-ms和对应 env 如果包含换行或 NUL,必须在发起 canary 请求前失败,不能让污染参数进入 URL、Host header 或 JSON 输出。遇到这类失败先修目标机 env、runbook 参数来源或手工命令,不要用只看X-Genarrative-Nginx-Handoff的 curl 替代完整npm run check:pingora-canary-live。 - 踩坑补充:真实路径 canary 不能为了“更像生产”而 include 到生产
443server 里写/api、/v1或/assetslocation;那会覆盖当前 Nginx 正式路由。只能把genarrative-pingora-realpath-canary.conf作为独立 loopbackserverinclude 到http上下文,使用127.0.0.1:18083和独立genarrative-pingora-realpath-canary.access.log验证,再用 release readiness 的--require-realpath-live纳入门禁。 - 踩坑补充:direct preflight 的
--env-file、--systemd-service、服务用户和 env 中的 listen / cert / key 值如果包含换行或 NUL,必须在执行systemctl cat、sudo -u ... test -r ...、证书可读检查或端口监听检查前失败。遇到这类失败先修目标机 env 或 runbook 参数来源,不要临时改成手工 systemctl / sudo 命令绕过。 - 踩坑补充:direct live 的
--https-base-url、--http-base-url、--host、--redirect-host、--probe-token、--path、--spacetime-database、--pingora-access-log、--timeout-ms和对应 env 如果包含换行或 NUL,必须在发起 HTTPS / HTTP / WSS 请求前失败,不能让污染参数进入请求头、URL、access log 对账或 direct live JSON。遇到这类失败先修 runbook 参数来源或目标机 env,不要临时删掉 direct live smoke、改用 curl 截图或只看systemctl is-active。 - 踩坑补充:证据包
--run-direct-live透传的 direct URL、Host、probe token、数据库名、Pingora access log 路径和 access log tail 行数也要在证据包配置层先拒绝换行或 NUL,--direct-pingora-access-log还必须是绝对路径且不能是/。遇到这类失败先修 runbook 参数或现场 env,不要把参数污染留给 direct live 子命令兜底,也不要手工改direct-live-command.json或跳过启用后证据包。 - 踩坑补充:不要在证据目录或证据根目录里手工塞
README、截图、压缩包、临时目录、无 manifest 子目录或软链来“辅助说明”。证据 verifier 把manifest.files视为闭集,未登记普通文件、目录和符号链接都会默认失败;证据总审计也默认要求根目录只包含带manifest.json的证据目录。证据 verifier / 总审计的入口路径、verifier 脚本路径和 manifest 登记文件名都不能包含换行或 NUL 字符,避免污染 JSON 证据、终端输出或归档复盘。需要保留人工说明时,应放到证据根目录外部,或重新生成能把该文件纳入 manifest 元数据的正式证据,而不是在正式切换归档上使用--allow-extra-files或--allow-extra-root-entries。 - 踩坑补充:不要把即时证据验真理解成只验 hash。正式 runbook 中
pre-cutover、enable-apply、post-enable、rollback-apply、post-rollback五个即时 verifier 步骤都必须带--require-summary-ok,同时要求manifest.schemaVersion=1、manifest.files未漂移且manifest.summary.status=OK;如果证据包已经记录CRITICAL、缺少 schemaVersion 或缺少 summary,应先修复现场状态、发布包、env、systemd、health patrol 或 direct live 证据并重新归档,不能继续推进到最终总审计。 - 踩坑补充:最终证据根目录总审计复用 verifier 时也必须启用
--require-summary-ok。如果总审计输出里verify.requireSummaryOk不是true,说明脚本或随包 verifier 已经退化成宽松模式,应先修发布包脚本而不是继续切换。 - 踩坑补充:不要用带
manifest.commandName的命令证据目录满足--require-phase。阶段证据必须来自状态快照证据包,命令证据必须通过--require-command单独要求;否则总审计可能把“真实执行过命令”和“某阶段状态已归档”混成一件事。 - 踩坑补充:证据根目录总审计不能只要求三阶段状态快照,也不能只做 sha256 验真。正式 runbook 必须在
--require-phase pre-cutover --require-phase post-enable --require-phase post-rollback之外,再传--require-command enable-apply:pingora-direct-enable-apply --require-command post-enable:pingora-health-patrol-direct-env-switch --require-command rollback-prep:pingora-gateway-shadow-env-switch --require-command rollback-prep:pingora-health-patrol-nginx-env-switch --require-command rollback-apply:pingora-direct-rollback-apply,用manifest.phase + manifest.commandName锁定五条真实切换命令证据;缺命令证据、最新命令证据损坏、阶段或命令manifest.summary.status非OK、命令manifest.summary.exitCode非0、命令名不安全,或顶层manifest.commandName/ 内嵌manifest.command.name任一为空、非法、互不一致时都必须失败。 - 踩坑补充:
commandName只能说明证据分类,不能证明真的跑了 enable / rollback 脚本。正式 runbook 的命令证据必须传--expected-executable绑定 current release 随包脚本绝对路径,并传--require-arg --apply在执行前确认真实命令参数包含--apply;如果真实命令与预期脚本不一致或缺少--apply,命令证据脚本应在创建正式命令证据前失败,避免把错误命令归档成正式切换证据。最终证据根目录总审计还必须传五条--require-command-executable和七条--require-command-arg,覆盖 enable apply、health patrol direct env switch、Pingora gateway shadow env switch、health patrol nginx env switch 和 rollback apply,其中包含enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs、rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh以及对应--apply、pingora-direct、nginx参数要求,复核manifest.expectedExecutable、manifest.command.executable与独立command-record.json的 executable,并要求manifest.commandName与manifest.command.name只要存在就各自是安全非空命令名、两者同时存在时一致、manifest.command.args与独立command-record.json.args都包含--apply,且每个 args 字符串都不含换行或 NUL 字符。--require-command-executable的 executable 段必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符;总审计 JSON 会记录requiredCommandExecutables,便于复盘本次绑定的真实 current release 随包脚本。总审计还会要求manifest.command与command-record.json关键字段一致;其中两份命令记录的schemaVersion都必须是1,stdoutPath/stderrPath必须同时与manifest.files.stdout.path/manifest.files.stderr.path对齐,不能把重新计算过 hash 的 command-record 指向另一份输出文件;args/command也必须一致,不能只保证脚本路径正确却把--apply证据改成--dry-run或其它参数;命令记录时间必须满足finishedAt >= startedAt、durationMs == finishedAt - startedAt,且manifest.generatedAt不能早于命令finishedAt;旧证据缺少 schemaVersion、缺少 expectedExecutable、缺少必需--apply参数、人工同名证据 executable 漂移、manifest 与 command-record 语义漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染、命令时间线漂移或同一命令重复绑定不同脚本路径都必须失败。 - 踩坑补充:命令证据里
expectedExecutable字段不能用空字符串或相对路径表示“未知”。manifest.expectedExecutable和manifest.command.expectedExecutable只要存在就必须是安全绝对路径;否则总审计应把 manifest 判坏,避免坏顶层字段被内嵌字段兜底,或坏内嵌字段被 command-record 里的路径掩盖。生成端--expected-executable也不能填/或带换行 / NUL 的路径,脚本会在执行真实命令前失败,不能用坏 expected path 先生成证据再交给总审计兜底。 - 踩坑补充:不能只检查
manifest.command.executable与command-record.json.executable两边一致;如果命令证据已经声明expectedExecutable,真实executable必须同时等于该预期路径,并且必须是绝对路径。否则人工修改两份命令记录为同一个错误脚本或相对路径,也可能伪造成一致证据。 - 踩坑补充:命令证据的
command字符串是给人读的,不是结构化身份事实。manifest.command.executable和command-record.json.executable都必须存在且是绝对路径;缺少结构化 executable 时,即使command字符串看起来包含正确脚本,也不能作为正式切换证据。 - 踩坑补充:生成命令证据时不要写
-- node script.mjs ...、-- bash script.sh ...或-- pingora-direct-enable.sh ...。pingora-cutover-command-evidence.mjs现在要求-- <command>本身就是绝对路径,且不能是文件系统根目录;真实命令和每个真实命令参数都不能包含换行或 NUL 字符。正式 runbook 应直接执行 current release 随包脚本的绝对路径,让 manifest 与 command-record 的executable/args[]字段从源头就是可审计事实。 - 踩坑补充:不要把 Pingora 日志、env、drop-in、脚本或 release root 路径填成
/来“先跑通参数”。release readiness 的--live-nginx-access-log/--live-pingora-access-log/--direct-pingora-access-log/--direct-health-patrol-env-file/--direct-preflight-env-file,canary 对账脚本的--nginx-log-file/--pingora-log-file,direct live 的--pingora-access-log,direct preflight 的--env-file,以及pingora-direct-enable.sh/pingora-direct-rollback.sh的显式路径参数都必须是绝对文件路径且不能是文件系统根目录;如果现场不确定真实日志、env、drop-in 或脚本文件,先查systemctl cat、logrotate、发布包 manifest 或服务 env,而不是用/占位。 - 踩坑补充:证据根目录总审计选择“每类最新证据”后,还必须证明这些证据来自同一次切换时间线。最新证据选择和标准八段时间线证明只接受
schemaVersion=1且带合法、规范 UTC 毫秒格式manifest.generatedAt的 manifest,命令记录startedAt/finishedAt也必须是new Date().toISOString()形式;缺失、非法、省略毫秒、本地时区或其它宽松可解析格式都会直接失败,不能用目录 mtime 兜底;证据目录被复制、归档或恢复后,也必须以 manifest 时间为准。同一阶段或同一命令如果出现多个候选共享最新manifest.generatedAt,总审计会以AMBIGUOUS_LATEST失败并列出重复目录,不能按目录名排序打平;应重新归档该阶段 / 命令证据,或把旧证据移出正式证据根目录后再审计。标准八段证据都被要求时,每段审计状态都必须是OK,manifest.generatedAt必须满足pre-cutover -> enable-apply -> post-enable:pingora-health-patrol-direct-env-switch -> post-enable -> rollback-prep:pingora-gateway-shadow-env-switch -> rollback-prep:pingora-health-patrol-nginx-env-switch -> rollback-apply -> post-rollback,且默认八段跨度不能超过 24 小时;非 OK、倒序或跨度过大都代表可能混入不同切换窗口遗留证据或现场状态未达标,必须失败后重新归档或清理证据根目录。任何证据 manifest 只要显式写入cutoverRunId字段,就必须是安全非空 ID,不能用空字符串伪装成缺省字段。确需跨更长维护窗口时,只能在生成 runbook 时显式传--cutover-evidence-timeline-max-span-ms <ms>,让最终总审计 JSON 记录本次放宽后的timeline.maxSpanMs与实际timeline.spanMs。 - 踩坑补充:标准八段时间线失败时不要只看顶层
ok=false或diagnostics文本。timeline.failedCount会按具体失败项累计,timeline.failureBreakdown会把非 OK 证据、缺少时间、cutoverRunId 混入、时间倒序和跨度超限拆开计数;同一次审计可能同时暴露多个证据问题,应逐项修复后重新归档。 - 踩坑补充:同一天多次演练或切换时,只靠“最新证据”和 24 小时窗口仍可能把两轮证据拼在一起。正式 runbook 会生成或接受
--cutover-run-id <id>,并把同一manifest.cutoverRunId写入三阶段证据包、五条真实切换命令证据和最终总审计;最终审计必须带--require-cutover-run-id <本次cutoverRunId>,缺少该字段或 ID 不一致时必须失败。即使人工临时总审计忘记带--require-cutover-run-id,标准八段时间线里只要任一证据声明了manifest.cutoverRunId,八段也必须全部声明同一个值,否则总审计失败。 - 验证:先运行
npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free,确认 env、drop-in、service EnvironmentFile 一致性、当前用户证书权限、服务用户证书权限、service 二进制可执行性和 80/443 已释放;systemctl cat genarrative-pingora-gateway.service必须显示AmbientCapabilities=CAP_NET_BIND_SERVICE、CapabilityBoundingSet=CAP_NET_BIND_SERVICE和EnvironmentFile=/etc/genarrative/pingora-gateway.env;启用脚本 apply 必须先通过 current release 自审,失败时不安装 direct-entry drop-in;还必须带 direct HTTPS / HTTP / Host / redirect host / SpacetimeDB database / Pingora access log 参数,并在重启后直接完成 direct live smoke 和 direct-access-log JSON 证据校验;也可用 release readiness--require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable把 HTTPS、HTTP redirect / ACME、正式域名 Host/SNI、redirect Location host、Pingora access log request_id 落盘、env 预检、systemd drop-in、service EnvironmentFile 一致性、当前用户和服务用户证书可读、service 二进制可执行、显式目标库和 WSS 101 一起纳入硬门禁,并拒绝--direct-skip-wss,避免 TLS 证书只按127.0.0.1误测、HTTP redirect Location 指错域名、service 实际读取另一份 env、root / deploy 用户可读但 systemd 服务用户不可读、current release 缺少可执行pingora-gateway,或 WSS subscribe 隐式打到默认 SpacetimeDB 库。check-pingora-release-readiness.mjs --help的正式直连和只生成 runbook 示例也必须带--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,不要让值班人员复制示例后才被--require-direct拦截。current release 自审、状态快照和证据包的布尔 env 必须是明确布尔值,非法值会失败,不得把拼错的 run / require / fail 开关当成 false。npm run plan:pingora-direct-cutover -- --require-direct ...输出必须包含 Host 与回退巡检入口确认、current release preflight、启用前不带--require-direct的基础 readiness、direct enable dry-run/apply、启用后带--require-direct的复核、rollback dry-run/apply、回退后 health patrol 切回 Nginx 并恢复切换前 public base URL / Host、回退后 health patrol env 复核;缺少--require-direct、缺少--rollback-health-patrol-public-base-url、缺少--direct-pingora-access-log、redirect Host 漂移或 rollback smoke Host 漂移时必须失败,避免生成缺少正式直连硬门禁或验证不同入口的切换计划。Host 与回退巡检入口确认步骤必须展示回退后要恢复的 health patrol public base URL / Host。直连后genarrative-health-patrol.service应使用GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct,状态 JSON 中gatewayMode应为pingora-direct,并检查genarrative-pingora-gateway.service而不是nginx.service;public probe 走127.0.0.1时应带GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>。回退后nginx -t必须先通过,systemctl cat genarrative-pingora-gateway.service不应再显示这两条 capability,systemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager必须仍指向 current release 的pingora-gateway,systemctl is-active nginx.service应为active,curl --fail --max-time 5访问--nginx-smoke-url应成功;若 smoke URL 为本机地址必须带--nginx-smoke-host <域名>,证明正式 vhost 已回到 Nginx;随后用node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs ...复核 health patrol env,必须显示GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx且 public base URL / Host 与切换前记录一致,shadow probe 可选复核必须返回gateway=pingora-shadow。本机提交前还要运行npm run check:pingora-direct-enable、npm run check:pingora-direct-rollback、npm run check:production-health-patrol、npm run check:production-api-release、npm run check:pingora-production-release-build和npm run check:production-api-deploy,确保脚本默认 dry-run 不会安装或删除 drop-in、current release 自审失败时启用脚本不会安装 drop-in、direct live 退出 0 但缺少direct-access-log结构化证据时启用失败,API release 布局自包含,真实 Pingora release 二进制能构建并进入发布包,API deploy 从发布产物内执行后 current release 自包含;缺少数据库备份脚本、健康巡检脚本、健康巡检 env 复核脚本、切换命令证据脚本、env 示例目录或 direct live smoke 脚本的发布包都必须在 current 切换前部署失败并退出本次打开的维护模式。正式直连 readiness 必须带--direct-health-patrol-env-file /etc/genarrative/health-patrol.env,并用scripts/check-production-health-patrol-env.mjs阻断 health patrol 仍停在 Nginx 模式或本机 direct probe 缺少正式 Host;发布包包含pingora-gateway时,npm run check:production-api-deploy必须覆盖服务 active / inactive 都会在 shadow 配置安全时执行systemctl restart genarrative-pingora-gateway.service并复核 active,同时覆盖 direct-entry capability 或公网监听 env 下不会提升 release、不会切 current、不会自动 restart。 - 顺序补充:正式 runbook 必须先通过 health patrol env 切换脚本预置回 Nginx 和切换前 public base URL / Host,再执行
rollback apply;回退脚本内置 env 复核和独立 env 复核都会阻断 public base URL / Host 漂移。 - 顺序补充:正式 runbook 还必须在
rollback apply前预置 Pingora shadow env。启用前和--dry-run-cutover要求80/443空闲;启用后--require-direct复核不再要求端口空闲,因为端口应由 Pingora 占用。回退时要先用 current release 随包node -- /opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply --env-file /etc/genarrative/pingora-gateway.env恢复GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081并清空 TLS / HTTP redirect 低端口监听,再移除 direct-entry drop-in 和重启 Pingora。 - 关联:
deploy/systemd/genarrative-pingora-gateway-direct-entry.conf、deploy/env/health-patrol.env.example、deploy/env/pingora-direct-live.env.example、deploy/env/pingora-canary-live.env.example、scripts/deploy/pingora-direct-enable.sh、scripts/deploy/pingora-direct-rollback.sh、scripts/deploy/pingora-tls-cert-sync.mjs、scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、scripts/ops/pingora-cutover-command-evidence.mjs、scripts/ops/pingora-cutover-evidence-verify.mjs、scripts/ops/pingora-cutover-evidence-audit.mjs、scripts/jenkins-server-provision.sh、scripts/build-production-release.sh、scripts/deploy/production-api-deploy.sh、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
外部生成 worker 重领必须按 claim attempt 隔离并持久结算
- 现象:同一个外部生成 job 在 worker 崩溃或 lease 过期后重领,可能出现旧 attempt 和新 attempt 都扣费,或者旧 attempt 已退款后新 attempt 因稳定 ledger 被当成幂等而免费执行。
- 原因:只按业务资源 ID 或 job ID 生成稳定 ledger 无法区分 claim;仅在新 attempt 开始时“先查旧 consume、存在则退款”仍有竞态,旧 consume RPC 可能在检查之后才提交。
- 处理:扣退费 ledger 固定包含
job_id + claim_attempt,每次重领先结算所有旧 attempt,再扣当前 attempt。结算必须在 SpacetimeDBasset_operation_wallet_settlement持久化:旧 consume 已存在时原子退款;尚不存在时写取消 intent。任何迟到 consume 在同一事务内看到 intent 后失败关闭。重复 ledger 必须核对用户、金额和来源,不能只按 ID 存在就返回成功。claim 处理 lease 已过期的runningjob 时还必须先比较attempt与max_attempts:未耗尽才递增并返回 worker;最终 attempt 已耗尽时在同一事务内把 job 置为failed、清空 lease、写完成时间和失败事件,并按当前 attempt 退款或写取消 intent,绝不能再次返回 provider executor。 - 验证:
cargo test -p spacetime-module asset_operation覆盖缺 consume 时写 intent、冲突结算拒绝和退款配对;cargo test -p spacetime-module external_generation::tests::覆盖未耗尽 lease 可重领、最终 attempt 只终态收口且不再递增;cargo test -p spacetime-module wallet_idempotent_replay覆盖冲突重放;cargo test -p api-server asset_billing覆盖崩溃重领、重复结算和当前 attempt 扣费。 - 关联:
server-rs/crates/api-server/src/asset_billing.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
外部生成队列不再由 HTTP 进程兜底执行
- 现象:拼图首关生成接口返回
queued,但生成页长时间不完成,重启genarrative-api.service也没有推进任务。 - 原因:HTTP 角色只入队,不再直接调用外部 provider;如果没有运行
GENARRATIVE_PROCESS_ROLE=external-generation-worker或all的进程,external_generation_job会停留在pending/running,直到有 worker claim。 - 处理:生产用
systemctl enable --now genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service启动保底 worker 和 controller;genarrative-api.service对 controller 使用 systemdWants弱依赖,启动 API 时会尝试一并拉起 controller,但不会让 HTTP 进程自己执行systemctl。首次 API deploy 会在默认 worker pattern 下自动启用并启动@1、等待 worker active,并重启验活 controller。扩容默认交给 controller 按队列统计启动@2.service等实例,手动扩缩容只作为兜底;worker 收到停机信号后会停止 claim 新任务并等待当前任务完成。本地 smoke 可临时用GENARRATIVE_PROCESS_ROLE=all npm run dev;本地若只想同步排查可通过.env.local或本机环境设置GENARRATIVE_EXTERNAL_GENERATION_MODE=inline,但这不会创建 job,也不能验证 worker 扩缩容。 - 验证:
systemctl status genarrative-external-generation-controller.service 'genarrative-external-generation-worker@*.service'能看到 controller 和 worker 实例;queue 模式下任务被 claim 后worker_id与lease_expires_at会更新,完成后 session 进入 ready 或 failed;inline 模式下不应产生新的external_generation_job。 - 关联:
deploy/systemd/genarrative-external-generation-worker@.service、deploy/systemd/genarrative-external-generation-controller.service、deploy/env/external-generation-controller.env.example、server-rs/crates/spacetime-module/src/external_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部生成 worker 不应等待 HTTP 认证投影恢复
- 现象:
genarrative-external-generation-worker@1.service在 systemd 中显示 active,但external_generation_job长时间保持pending;worker 日志每 5 秒出现认证投影或公开 read model 订阅失败。 - 原因:独立 worker / controller 是非 HTTP 角色,不承接用户登录态恢复;如果启动路径复用 HTTP
api-server的认证投影恢复,SpacetimeDB 认证投影或公开 read model 漂移会把 worker claim 循环挡在启动前。 - 处理:
GENARRATIVE_PROCESS_ROLE=external-generation-worker和external-generation-controller启动时只构建空 auth store 的AppState,不调用 SpacetimeDB 认证投影导出;只有api/all这类 HTTP 角色需要在启动时恢复认证投影并在依赖不可用时重试或进入 503 降级。 - 验证:重启 worker 后日志应先出现“非 HTTP 进程跳过 SpacetimeDB 认证投影恢复”,随后出现
external generation worker 已启动;同一时间窗口不应再因为认证投影恢复失败而阻止 job claim。HTTPapi-server的认证恢复日志和 503 降级语义保持不变。 - 关联:
server-rs/crates/api-server/src/main.rs、server-rs/crates/api-server/src/external_generation_worker.rs、server-rs/crates/api-server/src/external_generation_worker_controller.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地旧 external-generation-worker 会抢队列并暴露成 procedure 超时
- 现象:角色 / 画布生成的外部 provider 与 OSS 上传已成功,但 worker 写回
editor_project_resource等业务资源时报SpacetimeDB procedure 调用超时,日志里可能还能看到旧 worker 二进制对 procedure 返回值做 BSATN 反序列化失败。 - 原因:本地
npm run dev/npm run dev:api-server默认GENARRATIVE_PROCESS_ROLE=all,会自己消费队列;如果之前手动启动的同仓库、同 databaseGENARRATIVE_PROCESS_ROLE=external-generation-worker进程没有退出,旧二进制会继续 claim 新 job,schema / binding 已更新的当前进程反而没有拿到这次任务。 - 处理:Linux 本地默认
all角色启动前,scripts/dev.mjs会扫描同仓库、同 SpacetimeDB server / database、同server-rs/target/debug/api-server的遗留external-generation-worker并停止;显式GENARRATIVE_PROCESS_ROLE=api做生产式拆分验证时不清理独立 worker。 - 验证:
ps -eo pid,ppid,lstart,cmd | rg 'server-rs/target/debug/api-server'只应看到当前all或显式拆分下预期的进程;/healthz和/readyz成功后,生成 job 应由当前进程消费并把业务资源写回。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、server-rs/crates/api-server/src/external_generation_worker.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部生成 worker 业务写回必须同事务校验 lease guard
- 现象:worker
complete/fail已校验worker_id + lease_token,但如果玩法 session / work profile 写回在此之前单独调用,过期 worker 仍可能先写入业务状态,随后才在 job complete/fail 阶段失败;带计费包装的旧 worker 还可能因为 stale guard 错误触发补偿退款。 - 原因:队列状态栅栏只保护
external_generation_job自身,不会自动保护玩法 procedure。业务写回必须自己带 claim 后的job_id / worker_id / lease_token,并在同一个 SpacetimeDB transaction 内校验 job 仍为running、lease 未过期、job kind、owner 和 source entity 匹配。 - 处理:拼图首图 worker 的前置
compile_puzzle_agent_draft、save_puzzle_generated_images、save_puzzle_ui_background、mark_puzzle_draft_generation_failed和mark_puzzle_level_generation_failed已接入external_generation_joblease guard;api-server 的资产扣费包装遇到这类 stale worker lease guard 错误时不执行补偿退款,错误文本包含external_generation_job 当前不是 running 状态或external_generation_job 不存在时也按 stale guard 处理。inline 模式只允许job_id / worker_id / lease_token三项同时为空,半空 guard 仍拒绝。后续迁移其它玩法 worker 时必须复用该模式,不能只在 worker 进程内保存一份 token。 - 验证:
cargo test -p api-server external_generation_worker --manifest-path server-rs/Cargo.toml、cargo test -p api-server asset_operation_billing_does_not_refund_stale_worker_lease_errors --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/external_generation.rs、server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/api-server/src/external_generation_worker.rs、server-rs/crates/api-server/src/asset_billing.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
外部生成 worker 核心业务写回失败不能完成 job
- 现象:worker 已经生成图片并拿到本地合成 session 快照,但 SpacetimeDB 业务写回因连接、旧 wasm 或 lease guard 失败没有真实落库;如果此时仍把
external_generation_job标成completed,前端只会看到队列完成而 session 长时间不变化,后续也没有 worker 会重领修复。 - 原因:同步 HTTP handler 的“外部 provider 已成功但 SpacetimeDB 短暂不可用时返回内存快照”降级语义,不能直接搬进异步 worker。worker 的完成状态必须代表核心业务事实已经持久化。
- 处理:worker 路径的
save_puzzle_generated_images/save_puzzle_ui_background等核心业务写回失败时直接返回错误;只有核心写回已经成功后的非关键投影回写才允许降级记录 warning。业务失败态也必须先写回 session / work profile,写回成功后才允许把队列 job 标为 failed;失败态未写回时保留租约,等待 lease 过期后重领。生产首装和首次 API deploy 都必须至少启用一个 worker 实例,例如systemctl enable --now genarrative-external-generation-worker@1.service。 - 验证:
cargo check -p api-server --manifest-path server-rs/Cargo.toml、cargo test -p api-server asset_operation_billing_does_not_refund_stale_worker_lease_errors --manifest-path server-rs/Cargo.toml,并在 smoke 时确认 queued 任务被 worker 消费后 session 真实更新。 - 关联:
server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/generation.rs、server-rs/crates/api-server/src/external_generation_worker.rs、docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md。
生产冷备份后 API 和外部生成 worker 不能只依赖 SpacetimeDB 自恢复
- 现象:release 机器
03:20冷备份后,spacetimedb.service已恢复,但作品列表、创作入口配置或公开 gallery 继续超时 / 502 / 504,genarrative-api.service保持 stopped;或图片画布生成请求返回队列态后长期显示排队,external_generation_job有 claimable pending,但genarrative-external-generation-worker@1.service/ controller 是 inactive;也可能先看到/var/lib/genarrative/database-backups把根分区写满,gzip: stdout: No space left on device。 - 原因:
genarrative-api.service、genarrative-external-generation-worker@*.service和genarrative-external-generation-controller.service都配置了Requires=spacetimedb.service,冷备份停止spacetimedb.service时这些服务会被 systemd 依赖关系一并停止;如果备份脚本只在打包成功后重启依赖服务,那么 tar/gzip 因空间不足失败时就只会恢复数据库,外部生成队列和 API 仍无人接管。 - 处理:生产冷备份 unit 和发布脚本必须带
--restart-service-after genarrative-api.service、--restart-service-after genarrative-external-generation-worker@1.service和--restart-service-after genarrative-external-generation-controller.service;备份脚本必须在停止 SpacetimeDB 前做工作目录剩余空间预检,并且一旦已经停过 SpacetimeDB,就算打包失败也要先恢复 SpacetimeDB 与这些依赖服务,再返回原始备份错误。genarrative-api.service也保留对 controller 的Wants弱依赖,覆盖“只恢复 API”的现场兜底。仓库用npm run check:production-ops和npm run check:database-backup检查 systemd 模板、脚本失败路径、API build/deploy 归档和健康巡检链路。现场修复后执行systemctl daemon-reload,但不要为了验证而手动触发冷备份。 - 验证:
systemctl cat genarrative-database-backup.service应包含这些参数;systemctl is-active spacetimedb.service genarrative-api.service genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service nginx.service全为active;curl -fsS http://127.0.0.1:3101/v1/ping、/healthz、/readyz和代表性/api/runtime/puzzle/gallery均成功;npm run check:database-backup覆盖空间不足不触碰 systemctl、tar 失败仍恢复依赖服务;get_external_generation_queue_stats_and_return不应长期出现 claimable pending。 - 关联:
deploy/systemd/genarrative-database-backup.service、scripts/database-backup-to-oss.mjs、scripts/ops/production-health-patrol.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Pingora Brotli 不能只看 Content-Encoding
- 现象:在
pingora-gateway中把GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS试验性改成gzip,br后,Accept-Encoding: br, gzip的响应会带Content-Encoding: br,但 NodebrotliDecompressSync(...)报unexpected end of file。 - 原因:Pingora 0.8.1 的 Brotli compressor 路径虽然存在,但端到端输出不能被 Node 按完整 Brotli 流解压;只断言响应头会误判为可用。
- 处理:当前 Pingora shadow 只允许
GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip,在进入 Pingora compression 模块前把下游Accept-Encoding收敛为 gzip。Brotli 继续由 Nginx / 前置代理承担,直到补齐可解压的端到端门禁后再评估迁移。 - 验证:
npm run check:pingora-gateway-smoke必须覆盖小响应不压缩、图片资源不压缩、大响应Accept-Encoding: gzip和Accept-Encoding: br, gzip都返回可解压的 gzip;GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=br必须启动失败,GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=0也必须启动失败。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md、deploy/nginx/README.md。
Pingora 公网直连不能信任 X-Forwarded-For
- 现象:公网直连 Pingora 后接流保护、access log 或
client_ip似乎按用户传入的X-Forwarded-For分散,限流 key 可被客户端伪造。 - 原因:
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true只适合 Pingora 前方还有受控 Nginx / LB 且该前置层会清洗X-Forwarded-For的场景;Pingora 自己监听公网0.0.0.0:80/443时,下游请求头就是用户可控输入,不能拿来作为接流保护 client key。 - 处理:公网直连 env 必须保持
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=false。只有 loopback / 受控前置入口才允许配合GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true使用 XFF。目标机 direct preflight 会在公网监听加TRUST_X_FORWARDED_FOR=true时失败。 - 验证:
npm run check:pingora-direct-enable覆盖公网监听误信任 XFF 负例;切换窗口运行npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env ...,看到该错误时先改 env,再重启 Pingora。该 npm script 内部必须保持node -- scripts/check-pingora-direct-preflight.mjs,避免 Node 22 抢占业务--env-file。 - 关联:
scripts/check-pingora-direct-preflight.mjs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora canary 不能只看 handoff 响应头
- 现象:目标 Nginx 前缀 canary 的
/__genarrative_pingora_canary/healthz和代表性 API 都返回成功,响应也带X-Genarrative-Nginx-Handoff: pingora-canary,但仍无法证明 Nginx 与 Pingora 对同一请求的 method/status/path 完全一致。 - 原因:响应头只能证明请求经过了 canary snippet,不能证明同一
request_id已在 Pingora access log 落盘,也不能发现 healthz exact location 映射、前缀 rewrite 后路径或状态码漂移。 - 处理:本机 / CI 的
check-pingora-canary-docker也必须写临时 Nginx access log,并在 live smoke 后复用scripts/check-pingora-canary-access-log-parity.mjs对账 Docker Nginx 与 Pingora access log。目标机--require-live必须在 live smoke 后继续执行同一脚本,默认读取/var/log/nginx/genarrative.access.log和/var/log/genarrative/pingora-gateway.access.log,按request_id对照/__genarrative_pingora_canary/healthz与/__genarrative_pingora_canary/api/creation-entry/config。Nginx canary exact/healthz映射到 Pingora shadow/__genarrative_pingora/healthz,其它 canary 前缀路径按 rewrite 后路径比对。对账脚本的日志路径、prefix、必需路径和--since-lines/GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES不能包含换行或 NUL;日志行里解析出的 URI / path 含控制字符时也必须失败,避免污染值进入 JSON 对账输出。 - 验证:本机或 CI 执行
node scripts/check-pingora-canary-docker.mjs --require-docker --pull时应同时完成临时 Nginx / Pingora access log 对账。目标机执行node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log;本机执行npm run check:pingora-release-readiness-plan和npm run check:production-ops,确认 live 门禁计划包含真实 access log 对账。 - 关联:
scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-access-log-parity.mjs、deploy/env/pingora-canary-live.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora realpath canary include 要晚于 log_format
- 现象:目标机把
genarrative-pingora-realpath-canary.conf放进/etc/nginx/conf.d/后,nginx -t失败并报unknown log format "genarrative_upstream"。 - 原因:真实路径 canary 是独立
server片段,并使用access_log /var/log/nginx/genarrative-pingora-realpath-canary.access.log genarrative_upstream;。Nginx 会按文件名顺序加载conf.d;如果 canary 文件名早于定义log_format genarrative_upstream的主站配置,access log 行会先被解析而找不到格式。 - 处理:真实路径 canary 启停统一用 current release 随包脚本,不再手工写
/etc/nginx/conf.d/。启用执行/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083,脚本固定写入晚于主站配置加载的/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf,并在nginx -t、reload 或 live smoke 失败时恢复写入前配置。关闭执行/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply,脚本在nginx -t或 reload 失败时恢复删除前配置。另一种长期做法是把log_format放到所有conf.dserver 之前的全局 Nginx 配置。检查配置时不要把 probe token 原文写入记录。 - 验证:提交前运行
npm run check:pingora-realpath-canary-toggle或默认聚合门禁npm run check:pingora-release-readiness,确认启停脚本的 dry-run、apply、失败回滚和 disable 恢复逻辑仍被覆盖。启用脚本通过后,再运行node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>、node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log ...和node -- /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-realpath-live ...。若只启用了真实路径 canary,不要同时传--require-live,否则前缀 canary 未启用时会按正式 Nginx HTTP 入口返回 301。 - 关联:
deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf、deploy/nginx/README.md、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md、scripts/check-pingora-release-readiness.mjs。
Pingora release readiness 脚本不能只存在于源码 checkout
- 现象:本机 runbook 能生成,但目标机切换窗口执行启用前或启用后的 release readiness 复核时,可能命中 Jenkins workspace 或源码 checkout 的
scripts/check-pingora-release-readiness.mjs,而不是当前发布包里的脚本。 - 原因:API release、Jenkins Build 归档、Jenkins Deploy 复制清单都是显式文件列表;只在仓库中新增脚本或只改 runbook 相对路径,不能保证目标机 current release 自包含。另一个误区是在
/opt/genarrative/current上运行默认源码全量 readiness,导致包内脚本依赖 Cargo、npm、Docker 或 Nginx 构建环境。 - 处理:
scripts/build-production-release.sh、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy和scripts/deploy/production-api-deploy.sh必须同时携带scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-live.mjs、canary access log 对账脚本以及 direct preflight / live 子脚本;正式 cutover runbook 的启用前基础门禁和启用后--require-direct复核必须调用/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only。默认不带--release-runtime-only的全量 readiness 只在源码 checkout / CI / 构建环境运行。 - 验证:运行
npm run check:production-api-release、npm run check:production-api-deploy、npm run check:pingora-current-release-audit、npm run check:pingora-release-readiness-plan和npm run check:production-ops,确认发布包、current release、runbook 与 guardrails 都覆盖聚合脚本和check-pingora-canary-live.mjs,且 runbook readiness 参数包含--release-runtime-only。 - 关联:
scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-live.mjs、scripts/build-production-release.sh、scripts/deploy/production-api-deploy.sh、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-api-deploy。
SpacetimeDB 45 秒超时要看 api-server 记录的阶段
- 现象:release 上 Nginx 能立刻连到
api-server,但/api/runtime/*/gallery、/api/creation-entry/config等请求在约GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS后返回502/504。 - 原因:旧日志只能看到 HTTP 总耗时和最终状态,无法区分卡在连接池、SDK 建连、等待
on_connect、订阅 read model、等待 procedure / reducer 回调还是本地订阅 cache 读取。 - 处理:
spacetime-client内置阶段化健康检查和失败日志;/readyz用GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS短窗口检查 SpacetimeDB 连接租约,业务失败日志包含operation_kind、operation_name、spacetime_stage、elapsed_ms。 - 验证:
/readyz失败时看details.spacetime.stage;业务请求超时时查journalctl -u genarrative-api.service中同一时间窗口的SpacetimeDB client operation failed,优先按pool_acquire、connect_build、connect_handshake、read_model_subscribe、procedure_result、reducer_result、read_cache分阶段处理。 - 关联:
server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/api-server/src/health.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
新建草稿扣费不能和入口卡泥点配置分离
- 现象:后台修改创作入口的
mudPointCost后,入口卡和前置余额提示可能显示新数值,但用户真实钱包流水仍按代码常量扣除。 - 原因:早期约定把
creationTypes[].unifiedCreationSpec.mudPointCost只当展示字段,拼图、抓大鹅和汪汪声浪初始生成各自保留了2、10、三次单图1的硬编码扣费路径。 - 处理:新建草稿初始生成成本必须统一从
GET /api/creation-entry/config的unifiedCreationSpec.mudPointCost解析;前端预校验、拼图首图生成、抓大鹅完整草稿生成和汪汪声浪初始三图生成同源。汪汪声浪结果页单图重新生成仍按单图资产操作成本,不套初始草稿总成本。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "mud points"、npm run test -- src/services/bark-battle-creation/barkBattleCreationClient.test.ts、cargo test -p api-server --manifest-path server-rs/Cargo.toml resolves_mud_point_cost initial_generation_slot_cost_splits_creation_entry_total_cost -- --nocapture。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、server-rs/crates/api-server/src/creation_entry_config.rs、server-rs/crates/api-server/src/puzzle/handlers.rs、server-rs/crates/api-server/src/match3d/draft.rs、server-rs/crates/api-server/src/bark_battle.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
generated 图片重复下载不要改成服务端本地磁盘缓存
- 现象:同一张 OSS generated 图片每次展示都重新从 OSS 拉取,或者完整 OSS 私有 URL 裸请求返回 403。
- 原因:前端输入如果是
https://*.oss-*.aliyuncs.com/generated-*,会被当普通绝对 URL 直连,绕过/api/assets/read-url和 signed URL 本地缓存;旧 OSS 对象如果缺少Cache-Control,浏览器只能依赖ETag/Last-Modified做 304 协商缓存,不会长期强缓存。 - 处理:完整 OSS generated URL 先归一成
/generated-*legacy public path,再走/api/assets/read-url换签;refreshKey是 signed URL 缓存版本号,同一路径、同一版本且未临近过期时必须复用,不要每次渲染都强制重新换签。新上传 generated 私有对象由platform-oss在PostObjectform fields / policy 和服务端PutObject请求头中写入Cache-Control: public, max-age=31536000, immutable。不要把 api-server 变成图片静态代理,也不要把 OSS 内容 fallback 到服务器磁盘。 - 验证:前端测试应看到完整 OSS generated URL 调用
/api/assets/read-url?legacyPublicPath=...,且相同refreshKey不重复换签;cargo test -p platform-oss --manifest-path server-rs/Cargo.toml应覆盖Cache-Controlpolicy、form field、PutObject headers 和 V4AdditionalHeaders;线上旧对象可用curl -I观察是否只有ETag/Last-Modified或已经补齐Cache-Control。 - 关联:
src/services/assetReadUrlService.ts、server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/platform-oss/README.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
小程序 H5 导航不能清掉宿主 query
- 现象:微信小程序首次进入 H5 后,点击需要登录的入口没有返回小程序原生授权页,而是弹出 Web 端登录窗口;充值渠道也可能被误判为普通网页环境。
- 原因:小程序
web-view入口通过clientType=mini_program、clientRuntime=wechat_mini_program、miniProgramEnv标记宿主环境,但 H5 内部pushAppHistoryPath(...)阶段导航会默认清空 query;首点时微信 JS bridge 也可能尚未就绪,导致isWechatMiniProgramWebViewRuntime()和充值平台判断读不到小程序上下文。 - 处理:路由层统一把
clientType、clientRuntime、miniProgramEnv当作 app runtime context,在普通路径归一、显式 query 路由和同一创作流跳转时都跨导航保留;小程序环境识别同时用MicroMessenger + miniProgramUser-Agent 兜底首点 bridge 未就绪场景;创作恢复参数仍只在同玩法创作流内保留,离开创作流时继续清理。 - 验证:
npm exec vitest run src/routing/appPageRoutes.test.ts src/components/auth/AuthGate.test.tsx src/services/authService.test.ts src/services/payment/paymentPlatform.test.ts。 - 关联:
src/routing/appPageRoutes.ts、src/services/authService.ts、src/services/payment/paymentPlatform.ts、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
平台异步错误必须带来源弹窗,不要只显示裸错误
- 现象:用户先后触发多个拼图或草稿生成时,旧请求失败后会在当前页面显示“图片生成失败”等裸错误,容易误判为当前正在看的拼图失败;错误文本也不便复制给开发排查。
- 原因:不同入口、生成页、结果页、作品详情和运行态各自渲染局部错误,没有统一携带草稿、生成会话、作品或游玩来源。
- 处理:跨流程错误统一由
PlatformEntryFlowShellImpl汇总为PlatformErrorDialog,来源使用玩法、草稿 / session / work / run 标识组成;弹窗提供复制按钮。关闭弹窗时只清理可安全清理的错误状态;恢复类错误用 dismiss key 防止反复弹出但不擅自改底层状态。 - 验证:触发任一平台级异步失败时,页面应出现包含“错误来源”和“错误内容”的弹窗;复制内容应包含来源和错误正文;旧页面内错误 banner 不再重复出现。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/platform-entry/PlatformErrorDialog.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
自定义世界旧公开作品不要用 published_at 判断是否存在
- 现象:RPG / 自定义世界作品详情能打开,但点赞时报
custom_world 已发布作品不存在,无法点赞,错误来源是作品详情 CW-*或其它自定义世界历史公开号。 - 原因:部分历史
custom_world_profile已是publication_status=Published,但published_at为空;统一公开详情会用updated_at兜底展示,旧点赞 / 游玩 / Remix 判断却额外要求published_at.is_some()。 - 处理:公开互动存在性统一按
Published + deleted_at=None + visible=true判断;custom_world_gallery_entry同步和公开展示时间在published_at缺失时回退updated_at。 - 验证:
cargo test -p spacetime-module custom_world_public_interactions_accept_legacy_missing_published_at --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/custom_world.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md。
拼图公开推荐不要只按 Published 判断
- 现象:后台把拼图作品隐藏后,作品不在公开列表里显示,但玩家通关其它拼图后的推荐下一作品仍可能出现这条隐藏作品。
- 原因:拼图隐藏只把
puzzle_work_profile.visible置为false,不会把publication_status从Published改走;通关推荐候选曾只通过by_puzzle_work_publication_status().filter(Published)取数,漏掉可见性判断。 - 处理:拼图公开消费路径统一使用
Published + visible=true,范围包括puzzle_gallery_view、puzzle_gallery_card_view、兼容 gallery/detail procedure、公开点赞 / Remix、正式公开 runtime 启动和通关后的recommended_next_works候选。 - 验证:
cargo test -p spacetime-module hidden_published_puzzle_work_is_not_public_visible_candidate --manifest-path server-rs/Cargo.toml,并在需要时用后台隐藏一个已发布拼图后重试通关推荐。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
推荐页 WF 点赞不要落到 RPG / custom-world
- 现象:推荐页里给
WF-*敲木鱼作品点赞时,平台错误弹窗显示custom_world 已发布作品不存在,无法点赞。 - 原因:推荐页点赞统一走
likePublicWork,但敲木鱼尚未接入点赞后端;缺少wooden-fish分支时会落入默认 RPG / custom-world 点赞路径,把敲木鱼的 owner/profile 传给 custom-world reducer。 - 处理:所有公开作品互动必须先按
packages/shared/src/contracts/playTypes.ts中的全局sourceType分流;暂未接入点赞的玩法直接报“该作品类型暂不支持点赞”,禁止显示开放兜底文案,也禁止用默认 RPG / custom-world 分支兜底。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation wooden fish like does not call RPG gallery like"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
暗色创作进度卡不要被 platform-remap-surface 改成深色文字
- 现象:统一创作页里的暗色进度卡背景是深绿 / 深蓝,但“创作进度”、百分比和进度提示显示成深色,移动端几乎看不清。
- 原因:
platform-remap-surface在浅色主题下会把后代[class*='text-white']强制重映射成var(--platform-text-strong),并且使用!important;暗色 hero 卡片如果只写通用text-white*,刷新后仍会被全局 remap 覆盖成深色。早期还混用了text-white/72、text-white/88、border-white/14、bg-white/12等不稳透明度档位,进一步放大了问题。 - 处理:给暗色 hero 加组件专属 class,例如
creation-agent-hero__progress-label、creation-agent-hero__progress-value、creation-agent-hero__progress-hint,并在src/index.css的 remap 规则之后用更具体选择器和!important固定白色透明度、边框和进度条底色。 - 验证:
CreationAgentWorkspace测试应断言进度标题、百分比和提示文本带专属 class;src/index.test.ts应断言这些 class 在 remap surface 内有白色覆盖规则;移动端截图中暗色卡片文字应保持可读。 - 关联:
src/components/creation-agent/CreationAgentWorkspace.tsx、src/components/creation-agent/CreationAgentWorkspace.test.tsx、src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
VectorEngine 图片生成 request_send 传输错误要按可重试网络抖动排查
- 现象:
external_api_call_failure里看到failureStage=request_send、statusCode=null,errorSource可能是client error (SendRequest)、[35] SSL connect error (Recv failure: Connection reset by peer)、[56] Failure when receiving data from the peer (... unexpected eof while reading ...);也可能看到failureStage=upstream_status、statusCode=502、错误体是 Nginx HTML502 Bad Gateway。前端只知道图片生成失败。 - 原因:
request_send表示请求未拿到可归类的 HTTP 响应,不会包含上游 JSON 错误体;upstream_status=502/5xx/429/408表示拿到了上游错误响应但仍属于可重试的过载 / 网关抖动。timeout=true来自超时判定,connect=true会同时覆盖 DNS / connect 失败以及 libcurl 35 SSL 握手、libcurl 56 收包提前 EOF、connection reset 这类临时传输错误。 - 处理:先按
provider/failureStage/statusClass聚合,再用user_id/profile_id和metadata_json.userId/profileId/requestId定位触发者、草稿 / 作品和同一次 HTTP 请求;request_send + timeout/connect=true或upstream_status + statusCode=408/429/5xx优先查 provider 日志的source_chain、请求体大小、参考图数量、出口网络、代理/Nginx、VectorEngine 当时可用性和同一 request_id 日志。当前platform-image对 request_send 的 timeout / connect / SSL connect reset / recv error / unexpected eof / send error,以及 upstream_status 的 408 / 429 / 5xx 最多发送 5 次,multipart/v1/images/edits每次重试都会重新构造 form;看到VectorEngine 图片请求发送失败,准备重试或VectorEngine 图片上游状态可重试,准备重试只是单次 attempt 失败,最终external_api_call_failure才代表该用户请求整体失败。若记录有429 moderation_blocked或明确审核错误,按审核失败另行处理,不要归到网络抖动。 - 拼图关卡资产生成按
level_scene -> ui_spritesheet -> level_background顺序执行,每个资产会输出slot、asset_kind、elapsed_ms;排查拼图草稿失败时优先看同一 request_id 下最后一个失败 slot。 - 验证:
cargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_send_retry_policy -- --nocapture、cargo test -p platform-image --manifest-path server-rs/Cargo.toml vector_engine_image_edit_retries_send_timeout_once_and_succeeds、cargo check -p api-server --manifest-path server-rs/Cargo.toml;查询tracking_event时失败记录应能看到触发者user_id和可用的profile_id。 - 关联:
server-rs/crates/platform-image/src/vector_engine/client.rs、server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳 Three.js 地块 UV 顶面要映射到 Z 轴
- 现象:跳一跳地块使用六面 UV 贴图后,看起来像贴图位置贴歪,顶面显示侧面纹理,或者旧单张地块图被拉到立方体多个面上。
- 原因:运行态以
z作为立方体竖直高度和相机下压方向,但 Three.jsBoxGeometry/RoundedBoxGeometry的默认材质 group 顺序把+Y当 top;如果直接按right / left / top / bottom / front / back写材质,玩法逻辑的top会贴到侧面。旧作品没有完整faceAssets时,把单张旧贴图强行作为 3D 六面 fallback 也会被误认为 UV 贴歪。 - 处理:Three 平台层只在
tileAssets[].faceAssets六面完整时启用;材质数组按 Three group 顺序写入right / left / back / front / top / bottom,把逻辑top映射到+Z顶面,并按每面 UV 方向做翻转校正;旧单图作品继续走 DOM 图片 / 原型兜底层。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖材质顺序、UV 翻转和旧单图不启用 Three 贴面;cargo test -p api-server jump_hop_tile_atlas_slicing --manifest-path server-rs/Cargo.toml -- --nocapture应覆盖 UV 安全边裁切。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、server-rs/crates/api-server/src/jump_hop.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
“我的”页每日任务卡不要硬编码进度,也不要跨日保留旧状态
- 现象:用户完成或领取每日任务后,任务中心弹窗里的任务状态已经变化,但“我的”页卡片仍显示
0 / 1和“去完成”。 - 原因:卡片首版只写了静态展示文案,没有读取
/api/profile/tasks返回的ProfileTaskCenterResponse,领取接口返回的新center也只用于弹窗;后来虽然后端按北京时间 0 点切换业务日,但前端停留在“我的”页时不会跨日刷新,可能继续展示上一日已领取状态。若认证成功后把daily_login当普通埋点写入,或历史profile_task_config仍保留旧profile.login.daily事件键,新业务日也可能写了登录事件却查不到任务进度。 - 处理:进入“我的”页时读取任务中心,卡片用当前可操作任务或已领取任务派生奖励、进度条和操作状态;
claimRpgProfileTaskReward(...)成功后用响应里的center覆盖本地任务中心;停留在“我的”页跨过北京时间 0 点时,先非阻断 refresh 登录态写入新业务日daily_login,再重拉任务中心。后端认证成功统一走SpacetimeClient::record_daily_login_tracking_event(...)与 SpacetimeDB 专用record_daily_login_tracking_event_and_return,默认每日登录任务读取时会把结算字段自愈到 canonicaldaily_login。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx应覆盖卡片从后端任务摘要显示1 / 1、领取后显示已完成,以及北京时间 0 点自动 refresh 后重拉任务中心。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
“我的”页不要恢复旧的填邀请码次级按钮
- 现象:移动端“我的”页在五项常用功能和设置入口下方又出现一个“填邀请码”按钮,看起来像旧入口残留。
- 原因:邀请码流程迁移后仍按新用户窗口保留
canShowReferralRedeemShortcut次级入口;但当前页面口径已经固定为五项常用功能宫格,邀请码填写应由邀请链接 query 或明确引导打开弹窗。 - 处理:移除常驻
次级入口/填邀请码渲染,不删除ProfileReferralModal的redeem面板,也不破坏?inviteCode=/?invite_code=自动打开填写弹窗。 - 验证:新用户账号打开“我的”页时没有
次级入口和填邀请码按钮;带?inviteCode=spring-2026的登录用户仍自动打开邀请码弹窗并预填SPRING2026。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、.hermes/skills/genarrative-profile-invite-flow/SKILL.md。
创作卡片点击要直达已有入口表单,别再保留空白入口页
- 现象:创作 Tab 模板卡点击后如果仍然停留在创作大厅,或者先进入“X 创作入口”这种空白页,就会让用户多走一层,还可能被错误的 stage 白名单拉回平台。
- 原因:
/creation/<play>一度被接成空白创作入口页,导致SelectionStage、appPageRoutes和卡片点击分流被旧占位 stage 污染。 - 处理:把
/creation/<play>重新指向已有入口表单 stage,例如agent-workspace、big-fish-agent-workspace、match3d-agent-workspace、square-hole-agent-workspace、jump-hop-workspace、wooden-fish-workspace、puzzle-agent-workspace、bark-battle-workspace、visual-novel-agent-workspace、baby-object-match-workspace;平台壳层和测试同步清理空白入口页相关 helper。 - 验证:点拼图 / 抓大鹅 / 汪汪声浪卡片后,应看到各自既有工作台内容,例如测试中的
拼图工作区:missing-session、抓大鹅工作区:missing-session或汪汪声浪配置表单,并且不再出现“X 创作入口”空白页。 - 关联:
src/components/platform-entry/platformEntryTypes.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
创作流程刷新恢复必须写私有 query
- 现象:创作生成页或结果页刷新后回到空白工作区、平台首页,或者从作品详情返回时错误复用了别的玩法草稿。
- 原因:部分创作流程只把
sessionId/profileId/draftId/workId放在前端内存里,没有写进 URL;也曾把写 URL 放在 stage 切换前,writeCreationUrlState因为还停在非创作路径而直接跳过。若跨玩法或公开详情继续保留私有 query,还会污染/works/detail?work=...。 - 处理:创作页只使用私有 query
sessionId、profileId、draftId、workId做刷新恢复,不复用公开work参数;pushAppHistoryPath只在同一创作流内保留这些 query,离开创作流或切到另一个玩法必须清掉;手动 draft 打开、生成完成和保存回调要在路由已经切到/creation/<play>后再调用writeCreationUrlState。 - 验证:
npm run test -- src/services/creationUrlState.test.ts src/routing/appPageRoutes.test.ts src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx;手测生成页 / 结果页刷新仍恢复同一草稿,打开公开作品详情 URL 不带私有恢复参数。 - 关联:
src/services/creationUrlState.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
草稿作品架打开结果页返回必须回草稿 Tab
- 现象:从草稿 Tab 作品架点击已有草稿进入结果页后,点结果页返回会跳回创作 Tab 模板入口,用户需要重新切回草稿页才能继续找原草稿。
- 原因:平台壳层只按结果页类型硬编码返回创作入口,没有记录本次创作流是从草稿作品架打开;如果来源标记没有在新建入口时重置,还可能污染下一条创作链路。
- 处理:从作品架打开任一玩法草稿时标记返回目标为
draft-shelf;从创作 Tab 新建、打开模板或退出非草稿来源工作区时重置为create;结果页返回和工作区退出统一消费这个返回目标,并在消费后复位。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft result back button returns to draft hub when opened from shelf|agent draft result back button returns to draft hub without syncing result profile"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图生成页轮询不要绑展示 phase 或不稳定 setter
- 现象:拼图创作进入生成中页后,
/api/runtime/puzzle/agent/sessions/{sessionId}会在 0.3 到 0.5 秒内被反复 GET,看起来像轮询风暴,而不是 3 秒一次的正常刷新。 - 原因:轮询
useEffect同时依赖了拼图展示 phase 和会随父组件渲染变化的setSession函数,导致puzzleGenerationState的进度合并或页面重渲染就会重挂 effect;effect 里又会立即先请求一次 session,于是请求被放大成密集循环。 - 处理:拼图轮询只绑定
selectionStage、activePuzzleGenerationSessionId和“是否仍在生成中”这个布尔条件;setSession通过 ref 保持稳定,不让父组件重新渲染改变轮询器身份。进度 phase 变化只更新展示,不重建轮询。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft",并确认恢复生成中草稿后getPuzzleAgentSession不会因为进度刷新继续连发。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/platform-entry/usePlatformCreationAgentFlowController.ts、src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx。
小游戏恢复生成页不要只用请求 busy 判定是否生成中
- 现象:敲木鱼作品架里的生成中草稿点击进入生成页后,页面会显示“重新生成草稿”按钮,而不是继续显示素材生成中的等待态。
- 原因:平台壳恢复
generationStatus=generating草稿时会把isBusy置回 false,只保留MiniGameDraftGenerationState作为生成事实;生成页如果只把请求 busy 传给isGenerating,共用生成页会误判为空闲态并展示重试按钮。 - 处理:小游戏生成页的
isGenerating必须由isBusy || isMiniGameDraftGenerating(generationState)推导;跳一跳、拼消消、敲木鱼等从作品架恢复的生成页都要使用同一口径。 - 验证:
npm run test -- src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts应覆盖busy=false但敲木鱼 generation state 仍在生成中时继续隐藏重试入口。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/unified-creation/UnifiedGenerationPage.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图试玩恢复 query 必须先切到运行态路径再写
- 现象:拼图试玩或正式运行态打开后,刷新会停在“正在进入拼图关卡”,或地址栏只有
runtimeProfileId,缺少草稿runtimeSessionId。 - 原因:
writePuzzleRuntimeUrlState只会在当前路径已经是/runtime/puzzle时写入;如果先触发阶段切换再写 query,或者草稿作品摘要缺少sourceSessionId,就会把恢复参数写丢。App.tsx的 stage 同步也会改 pathname,所以顺序不对时容易只留下部分 query。 - 处理:进入拼图 runtime 时先
pushAppHistoryPath('/runtime/puzzle'),再setSelectionStage('puzzle-runtime'),最后写runtimeProfileId、runtimeSessionId、runtimeLevelId、work、mode;草稿 runtime URL state 允许从profileId反推puzzle-session-*,作为sourceSessionId的兜底。 - 验证:
npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t \"puzzle draft generation auto starts trial and runtime back opens draft result\",确认window.location.pathname === '/runtime/puzzle'且window.location.search同时包含runtimeProfileId和runtimeSessionId。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/services/puzzleRuntimeUrlState.ts、src/routing/appPageRoutes.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼消消草稿试玩不能只测 swap 回调
- 现象:拼消消结果页和 runtime shell 的单测都能通过,但真实页面里卡片只是交换,完全不会消除,顶部准备区还会因为已知的卡背占位路径显示坏图。
- 原因:草稿试玩走的是前端本地 runtime,早期测试只覆盖了
onSwapCards回调和局部状态,没有验证完整的消除、重力补牌、关卡完成和资源兜底链路;同时顶部卡背对puzzle-clear-card-back.webp这类已知缺失资源没有前置回退。 - 处理:草稿试玩的回归测试必须覆盖“交换 -> 完整图案消除 -> 补牌 -> 关卡完成”闭环,并在组件测试里验证真实点击/拖拽序列;顶部准备区卡背遇到已知占位路径时直接回退到
puzzle.webp这类可用参考图,不等图片加载失败后再兜底。 - 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx通过,浏览器 smoke 页实测可完成一次消除并弹出“本关完成”。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消消除过渡不能隐藏已有卡片的最终下沉格
- 现象:消除补牌过程中偶尔看起来下方有空位,但同列上方卡片没有落下来。
- 原因:后端和本地 runtime 的重力补牌已经把已有卡片压到底;真正的问题在前端过渡层。消除动画曾按旧消除坐标隐藏棋盘格,掉落动画也曾隐藏所有 drop 目标格。当某个旧卡下沉到刚被消除的格子时,最终 snapshot 里的真实卡片会被隐藏,视觉上像补牌没有落下。
- 处理:消除 / 掉落覆盖层只负责动画表现,不再隐藏已有场上卡片的最终格;只有从顶部准备区新补入、前一帧棋盘不存在的卡片,才允许临时隐藏底层目标格来配合下落动画。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx -t "已有卡片因重力下沉时目标格不被过渡状态隐藏成空位",并保留领域侧cargo test -p module-puzzle-clear refill --manifest-path server-rs/Cargo.toml。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、server-rs/crates/module-puzzle-clear/src/application.rs、docs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md。
拼消消完整消除反馈不要让补牌抢帧
- 现象:玩家正确拼完整组后,卡片几乎瞬间消失,顶部补牌马上出现或下落,导致“拼对了”的确认反馈很弱。
- 原因:前端一收到新 snapshot 就同时播放消除和掉落叠层,旧消除动画时长较短;新补入卡牌的下落延迟接近 0ms,视觉上会抢在消除反馈之前开始。
- 处理:局部正确拼合但未消除时只给锁定组做一次高光;完整消除时让旧卡片在消除叠层中短暂放大展示再淡出;新补入卡牌的下落延迟到淡出尾段,并继续只隐藏新补入目标格,不隐藏已有场上卡片下沉后的最终格。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx,浏览器里确认局部拼合会闪、完整消除会放大淡出、补牌在淡出后段才开始掉落。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/index.css、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
首页推荐分流参数不能条件性调用 hook
- 现象:桌面首页或移动首页在 HMR、断点切换或重新渲染后直接报 React hook 顺序错误,页面停在“正在加载内容”。
- 原因:
RpgEntryHomeView曾经写成const isDesktopLayout = isDesktopLayoutProp ?? usePlatformDesktopLayout();,当isDesktopLayoutProp存在时会跳过 hook 调用,导致 hook 顺序在不同渲染之间变化。 - 处理:先无条件调用
usePlatformDesktopLayout(),再用isDesktopLayoutProp ?? detectedDesktopLayout合并;不要把 hook 调用藏在条件表达式里。 - 验证:桌面与窄屏各刷新一次首页,控制台不再出现 hook 顺序错误;
npm run typecheck和首页推荐相关测试通过。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/platformEntryResponsive.ts。
泥点不足提示不要把用户退回创作入口
- 现象:拼图 / 抓大鹅 / 汪汪声浪等创作表单点击生成时,如果泥点不足,页面直接回到创作 Tab 玩法模板列表,刚填的表单内容随工作台卸载全部丢失。
- 原因:
PlatformEntryFlowShellImpl.tsx的ensureEnoughDraftGenerationPointsFromServer(...)曾在余额不足或余额读取失败时调用enterCreateTab()并setSelectionStage('platform'),把前置校验失败当作离开工作台处理。 - 处理:泥点前置校验失败只更新独立
UnifiedModal提示,不切换 stage,不清表单;余额读取失败也走同一弹窗口径。需要提示玩法内错误时可以保留局部错误位,但不得因此退出工作台。 - 验证:
npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle form checks mud points before creating a draft|match3d form checks mud points before creating a draft|bark battle form checks mud points before creating image assets"应断言弹窗出现、对应工作台仍在、玩法模板分类不再出现。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
内嵌泥点确认弹窗必须自带平台主题作用域
- 现象:拼图 / 抓大鹅统一创作页点击生成后,“确认消耗泥点”弹窗正文和按钮存在,但弹窗面板背景透明,只剩遮罩和文字。
- 原因:
PlatformMudPointConfirmDialog作为二级确认常以portal={false}内嵌到工作台局部 DOM,局部节点不一定继承.platform-theme;platform-modal-shell依赖--platform-modal-fill等主题变量,变量缺失时面板底色解析为空。 - 处理:共享泥点确认弹窗默认在 overlay 上带
platform-theme platform-theme--<theme>、platform-modal-backdrop和实色遮罩,在 panel 上带platform-modal-shell platform-remap-surface;单按钮状态弹窗也要有默认 light 主题,避免未来独立调用复现。 - 验证:浏览器触发
/creation/puzzle与/creation/match3d的泥点确认弹窗,检查 overlay 最近主题 class 存在、--platform-modal-fill有值且面板为实底;聚焦测试覆盖默认 overlay / panel class。 - 关联:
src/components/common/PlatformMudPointConfirmDialog.tsx、src/components/common/PlatformStatusDialog.tsx、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx。
拼图结果页关卡图不要裁切,嵌套图片预览要高于详情弹窗
- 现象:拼图结果页“拼图关卡”列表里的关卡图底部被裁掉;进入关卡详情后点击画面图,看起来没有打开全屏预览。
- 原因:关卡列表复用
PlatformMediaFrame aspect="standard"默认object-cover,方图或竖向生成图会在 4:3 框内被裁切;关卡详情弹窗自身层级高于CreativeImageInputPanel默认图片预览层级,预览实际打开但被压在详情弹窗后面。 - 处理:结果页关卡缩略图显式传
imageClassName="h-full w-full object-contain"保留完整画面;CreativeImageInputPanel提供mainImagePreviewZIndexClassName,嵌套在高层级弹窗内时由调用方传更高层级。 - 验证:聚焦测试断言关卡缩略图使用
object-contain且没有object-cover,并断言关卡详情内主图预览 overlay 层级高于详情弹窗;浏览器里检查列表完整显示图片,详情内点击画面图能打开可见预览。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.test.tsx。
图片大图预览不要复用白底工具弹窗
- 现象:点击图像输入面板里的参考图或主图预览后,页面只出现白底非全屏弹窗,背后原页面透出,不能缩放或拖拽查看细节。
- 原因:图片查看和工具弹窗共用了
UnifiedModal白底壳层;该壳层适合编辑 / 选择工具,不适合沉浸式看图,也没有图片边界拖拽状态。 - 处理:纯图片预览统一走
PlatformImagePreviewModal,全屏黑底展示,初始 contain 保证完整图片可见,缩放夹在1x-4x,拖拽位移按缩放后的图片边界夹取,避免把图片拖到露出背景。 - 验证:
npm run test -- src/components/common/PlatformImagePreviewModal.test.tsx src/components/common/CreativeImageInputPanel.test.tsx应覆盖黑底全屏、缩放上限、拖拽边界和关闭按钮。 - 关联:
src/components/common/PlatformImagePreviewModal.tsx、src/components/common/CreativeImageInputPanel.tsx。
玩法入口分类字段缺失要前端兜底
- 现象:平台创作入口初始化时,
platformEntryCreationTypes.ts直接对creationTypes[].categoryId/categoryLabel调trim(),一旦后端旧数据、局部 mock 或异常返回里缺字段,整个创作页会在derivePlatformCreationTypes(...)里直接炸掉。 - 处理:
normalizeCategoryId(...)和normalizeCategoryLabel(...)必须接收可空值,并分别回退到recommended/热门推荐;历史recent/最近创作也要归一到推荐分类。最近创作不属于模板分类页签,只能由真实草稿 / 作品架后端数据决定是否展示。 - 验证:
npm test -- src/components/platform-entry/platformEntryCreationTypes.test.ts,再打开本地创作页确认能正常进入创作 Tab。 - 关联:
src/components/platform-entry/platformEntryCreationTypes.ts、src/components/platform-entry/platformEntryCreationTypes.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作入口公告不要恢复前端固定两卡
- 现象:点击底部加号进入的创作入口页只展示固定的拼图 / 抓大鹅主题卡,后台改公告表单后前台没有变化。
- 原因:前端重新硬编码 banner 列表,绕过了
GET /api/creation-entry/config的eventBanners配置。 - 处理:创作入口页公告位优先读取后端
eventBanners数组,多条自动轮播;旧eventBanner只做单条兼容兜底。后台主格式是标题与 HTML 内容表单,保存时序列化为后端eventBannersJson传输字段,只允许受控 HTML 片段经空权限 iframe 展示,不执行 JSX 或直接 DOM 注入。 - 验证:后台保存两条以上公告后,点击底部加号进入创作入口页应自动轮播这些后台配置项;
CustomWorldCreationHub相关测试应断言标题来自后端配置。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、server-rs/crates/module-runtime/src/application.rs、apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx。
创作入口 banner 默认图片路径必须真实存在
- 现象:创作页顶部 banner 返回旧结构化
eventBanner时,前端<img>请求/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png,但public/下没有该文件,导致 banner 背景图加载失败。 - 原因:旧库
event_banners_json=None时,读取层把旧单条结构化 banner 当成eventBanners优先数组下发;同时旧结构化默认coverImageSrc指向已经不存在的品牌素材路径。 - 处理:
module-runtime在event_banners_json缺失或不可解析时回到默认公告数组;默认 HTML 公告和旧结构化默认coverImageSrc都引用public/下真实存在的/creation-type-references/puzzle.webp。 - 验证:
cargo test -p module-runtime creation_entry_event_banners_none_returns_default_announcements --manifest-path server-rs/Cargo.toml;重启本地api-server后GET /api/creation-entry/config的eventBanners[0]不再指向缺失的/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png。 - 关联:
server-rs/crates/module-runtime/src/application.rs、server-rs/crates/module-runtime/src/domain.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
移动端草稿卡不要长按选中文字
- 现象:移动端草稿页长按作品卡标题或摘要时触发系统文字选区,容易误触并打断作品架操作。
- 处理:移动端只对
#platform-tab-panel-saves .creation-work-card禁止user-select和-webkit-touch-callout;输入框、文本域和[contenteditable='true']保留文本选择能力,避免破坏真实编辑场景。 - 验证:移动端草稿页长按普通作品卡文字不出现系统选区;
src/index.test.ts应覆盖 CSS 选择器和可编辑控件例外。 - 关联:
src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
草稿页未读点不要继续用红色 literal
- 现象:草稿页底部 Tab 和作品架的未读点视觉上仍像红点,或 glow 仍带红色阴影,和平台暖棕体系不一致。
- 原因:
platform-nav-unread-dot、creation-work-card__unread-dot直接写了#b64a35和rgba(239, 68, 68, ...),没有收口到统一 token。 - 处理:未读点颜色统一走
--platform-unread-dot-fill/--platform-unread-dot-glow,桌面/移动端共用同一口径;不要把红色 literal 再写回样式。 - 验证:
src/index.test.ts断言两个 unread dot block 都只引用未读点 token,不再出现红色 literal 或红色 glow。 - 关联:
src/index.css、src/index.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作 Tab 模板卡不要复用暗图蒙版参考卡样式
- 现象:创作 Tab 两列玩法卡上图能看到,但标题、描述或预计消耗泥点在白底信息区里看不见,或只剩泥点小图标。
- 原因:旧
platform-creation-reference-card是给暗图蒙版卡用的全局样式,会把卡片及全部子元素强制成白色文字;参考图要求的是“上图 + 下方白底信息区”,继续复用旧类会让白底上的文字消失。 - 处理:创作 Tab 首屏模板卡使用独立
creation-template-card、creation-template-card__body、creation-template-card__title、creation-template-card__subtitle和creation-template-card__cost结构,不挂platform-creation-reference-card;旧弹层如果仍是暗图蒙版卡,可以继续保留旧类。 - 验证:浏览器创作 Tab 中每张开放态卡都应显示标题、描述和后台契约
mudPointCost数量经前端格式化后的泥点消耗文案;旧契约缺字段时兜底显示10泥点数;npm test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx -t "creation start card renders reference-aligned banner and template metadata"应通过。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、src/index.css、src/components/custom-world-home/CustomWorldCreationHub.test.tsx。
创作首屏开放态卡片不要再显示左上状态标签
- 现象:创作 Tab 的开放态玩法卡左上角会重复显示“可创建”或“可创作”,视觉上比其它状态更吵,还会和封面图抢注意力。
- 原因:卡片渲染层默认把
badge当成所有状态都要展示的左上角标签,没有区分开放态与非开放态。 - 处理:开放态卡片不渲染左上标签,仅保留标题、描述和右下角消耗信息;
敬请期待、即将开放等非开放态标签继续保留。 - 验证:创作首屏 HTML 中不应包含
可创建/可创作,但仍应包含即将开放等非开放态状态。 - 关联:
src/components/custom-world-home/CustomWorldCreationStartCard.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
发现 / 创作 / 草稿页不要把根内容区再包成全局卡片壳
- 现象:发现页、创作页或草稿页根区一旦套回
platform-page-stage,页面边缘会立刻变得更厚,频道标签、列表和模板卡的横向空间都被挤窄,看起来像回到了旧全局卡片壳。 - 原因:
platform-page-stage本身是全局内容卡片壳,适合推荐页、我的页和其它页面,但这三页已经有自己的视觉结构;草稿页顶部筛选若继续用旧platform-tab,还会和发现页频道标签不一致。 - 处理:这三页的根内容区只保留
platform-remap-surface,不要再加platform-page-stage;草稿页顶部筛选复用发现页的platform-mobile-home-channel与platform-mobile-home-channel--active。 - 验证:浏览器里这三页的根区应仍保留
platform-remap-surface,但不再出现platform-page-stage;草稿页顶部筛选样式应和发现页频道标签一致。 - 关联:
src/components/custom-world-home/CustomWorldCreationHub.tsx、src/components/custom-world-home/CustomWorldWorkTabs.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、src/index.css。
统一创作壳现在自己负责页面滚动和四条入口外壳
- 现象:统一创作页最初只包住拼图、抓大鹅和敲木鱼的工作台内容,跳一跳仍然保留独立工作台壳,页面级滚动职责也散落在平台入口 motion wrapper 里,导致移动端不同入口的可见外壳不一致。
- 原因:
UnifiedCreationPage只做了标题和隐藏契约,入口壳还在各自工作台里保留platform-remap-surface/overflow-y-auto,jump-hop也没进入统一 spec。 - 处理:把
jump-hop纳入unifiedCreationSpec,让UnifiedCreationPage自己承担页面级滚动与统一标题栏;JumpHopCreationWorkspace、WoodenFishCreationWorkspace补unifiedChrome/showBackButton,平台壳不再给这几条统一入口套额外滚动壳。 - 验证:
npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx通过后,/creation/puzzle、/creation/match3d、/creation/jump-hop、/creation/wooden-fish都应由同一套统一创作页外壳承载。 - 关联:
src/components/unified-creation/UnifiedCreationPage.tsx、src/components/unified-creation/unifiedCreationSpecs.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
统一创作编排层不要再让平台壳直挂旧工作台
- 现象:平台入口壳已经切到统一创作外壳,但源码里仍直接 lazy import 并渲染四个旧工作台分支,看起来还是四套入口编排。
- 原因:统一创作页只收口了可见外壳,入口层没有再抽一层统一创作编排组件,导致平台壳依旧要认识各玩法旧工作台。
- 处理:新增
UnifiedCreationWorkspace,由它内部按playId选择真实工作台;平台壳只依赖这一层,不再直接挂旧工作台分支。旧工作台已迁入src/components/unified-creation/workspaces/,不再是入口编排事实源。 - 验证:
PlatformEntryFlowShellImpl.tsx中不应再出现四个旧工作台的入口渲染分支,创作 Tab 与/creation/<play>仍能正常进入对应工作台。 - 关联:
src/components/unified-creation/UnifiedCreationWorkspace.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
Jenkinsfile 开头不能带 UTF-8 BOM
- 现象:
Genarrative-Stdb-Module-Publish在Pipeline script from SCM读取jenkins/Jenkinsfile.production-stdb-module-publish后,流水线还未进入任何 stage 就失败,报java.lang.NoSuchMethodError: No such DSL method 'pipeline',堆栈位置是WorkflowScript.run(WorkflowScript:1)。 - 原因:该 Jenkinsfile 文件前三字节是 UTF-8 BOM
EF BB BF,Jenkins/Groovy 把它拼进首个标识符,导致实际调用的是\ufeffpipeline而不是 Declarative Pipeline 的pipeline全局。 - 处理:仓库内
jenkins/Jenkinsfile.production-*保存为 UTF-8 without BOM;不要为了解决 Windows PowerShell 5.1.ps1中文解析问题而给 Jenkinsfile 本身加 BOM。只有 Jenkins helper 临时写出的.ps1才按需要转成 UTF-8 with BOM。 - 验证:检查
jenkins/Jenkinsfile.production-stdb-module-publish文件开头字节不再是EF BB BF,并用 JenkinsvalidateDeclarativePipeline或重放Genarrative-Stdb-Module-Publish,不应再停在No such DSL method 'pipeline'。 - 关联:
jenkins/Jenkinsfile.production-stdb-module-publish、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Linux 多用户 dev 端口冲突先查系统级端口段注册表
- 现象:同一台 Linux 机器上多个用户同时开发时,
npm run dev报端口段已被其他用户占用、同一用户已有活跃端口段,或 SpacetimeDB 复用记录指向当前用户端口段之外的地址;未手动指定时自动分配应从10000-10099起步。 - 原因:Linux dev 脚本会通过
/var/tmp/genarrative-dev-port-ranges/registry.json做系统级端口段分配,避免两个用户配置相同或重叠端口段;同一用户后续启动会继续复用自己已经占用的固定端口段。注册表会保留该用户的段记录,不会因为多开而要求重新分配。 - 处理:先确认当前用户已经占用的端口段,再让后续
npm run dev/dev:*继续沿用这段;如确实要切换段,手动释放或清掉对应 registry 记录后再重启。需要临时隔离测试时用GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR=<tmp-dir>覆盖注册表目录。不要在 Windows 上按这个注册表排查,Windows 仍走原有端口探测与漂移逻辑。未指定端口段时,系统会从10000-10099开始顺序分配。 - 验证:重新启动后终端应打印
[dev] port-range: <start-end> (<user>)与[dev] port-range-registry: .../registry.json;node node_modules/vitest/vitest.mjs run scripts/dev-stack-port-utils.test.ts scripts/dev.test.ts应通过 Linux registry、自动分配10000-10099与 Windows bypass 用例。 - 关联:
scripts/dev-stack-port-utils.mjs、scripts/dev.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
SpacetimeDB 入口迁移 helper 合并时不要只保留调用
- 现象:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml或 JenkinsGenarrative-Stdb-Module-Build报E0425 cannot find function migrate_rpg_entry_from_old_hidden_default in this scope,位置在server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs的默认入口配置播种流程。 - 原因:分支合并时保留了
seed_creation_entry_config_if_missing(...)中的迁移调用,但漏掉了同文件内的 helper 定义;该 helper 负责把历史默认隐藏的 RPG 入口纠偏为当前开放默认值。 - 处理:恢复缺失的迁移 helper,不要直接删除调用。helper 只能匹配历史默认种子(标题、副标题、badge、图片、visible/open、排序都一致)后再更新,避免覆盖后台入口开关的人工配置。
- 验证:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
抓大鹅新 UI spritesheet 不要回退成中心容器图
- 现象:新素材流程生成后,运行态棋盘中心可能叠出一整张 UI spritesheet,导致按钮素材、方格和空白图集覆盖容器区域。
- 原因:为了兼容旧 DTO,后端可能把
uiSpritesheetImage*同步写入历史containerImage*字段;旧前端只看containerImage*,会误把 UI 图集当透明中心容器。 - 处理:读取中心容器图时先比较归一化后的
containerImage*与uiSpritesheetImage*。两者同源时忽略containerImage*,只把它作为旧数据兼容字段;新流程背景图本身已经保留容器,运行态只需加载背景和解析 UI / 物品 spritesheet。 - 验证:
npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx应覆盖“运行态不把兼容写入的UI spritesheet当中心容器图”。 - 关联:
src/components/match3d-runtime/Match3DRuntimeShell.tsx、server-rs/crates/api-server/src/match3d/mappers.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
通用系列素材图集先看 platform-image,不要先翻 api-server 大文件
- 现象:排查跳一跳、抓大鹅或其它玩法的系列素材图集切片 / 去绿 / 持久化时,最容易先打开
api-server/src/generated_asset_sheets.rs,结果在一个 60KB+ 大文件里找实现、测试和辅助函数,定位很慢。 - 原因:这条通用图片 seam 已经下沉到
server-rs/crates/platform-image/src/generated_asset_sheets/,api-server只剩薄包装和调用方兼容;继续把api-server当真值源会把理解路径拉回旧位置。 - 处理:先看
server-rs/crates/platform-image/src/generated_asset_sheets/mod.rs、prompt.rs、sheet.rs、alpha.rs、persist.rs和error.rs,再看api-server/src/generated_asset_sheets.rs的 AppError / AppState 适配和玩法调用点。 - 验证:
cargo test -p platform-image --test generated_asset_sheets --manifest-path server-rs/Cargo.toml通过,且cargo check -p api-server --manifest-path server-rs/Cargo.toml保持绿灯。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/、server-rs/crates/api-server/src/generated_asset_sheets.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
图片画布 UI 提取素材切片不要把断开的高光阴影当独立图标
- 现象:图片画布提取 UI 素材后,右侧素材库出现很小的废图;主体图标的阴影、反光、高光或小装饰不完整。
- 原因:图标 spritesheet 切片按 alpha 连通域识别素材,模型常把软阴影、高光、小星星等画成与主体断开的透明块;如果直接逐连通域出图,小碎片会抢占图标顺序,主体也会缺边缘装饰。
- 处理:在
platform-image的sheet.rs里先合并靠近主体的辅助连通域,再过滤孤立小碎片,最后给裁剪框保留安全 padding。不要在前端素材卡或画布层里修已经切坏的 PNG。生成图标素材入口只回填扣绿后的整张图集,不再拆分独立图标。 - 验证:
cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml覆盖断开的高光合并和孤立小碎片过滤;调用方补跑cargo test -p api-server editor_icon --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs、server-rs/crates/api-server/src/editor_project.rs。
UI spritesheet 不要依赖模型直接生成透明背景
- 现象:拼图或抓大鹅运行态解析 UI spritesheet 时,把整张背景图、棋盘格、叶子或装饰图也当作 UI 素材区域,按钮映射错乱;截图里常表现为底部按钮区只剩透明棋盘格或素材碎片。
- 原因:前端解析依赖 alpha 连通域检测,透明背景是前提;但生图模型收到“透明背景 spritesheet”提示后仍可能输出带实景背景或伪透明棋盘格的普通不透明 PNG,OSS 中保存的图没有真实 alpha。
- 处理:UI spritesheet 提示词应要求统一单一纯绿色
#00FF00 / RGB(0,255,0)绿幕背景,而不是让模型直接产透明背景;后端在上传 OSS 前复用generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha(...)把绿幕扣成真实透明 PNG,再把透明图写入uiSpritesheetImageSrc/uiSpritesheetImageObjectKey。 - 验证:
cargo test -p api-server puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_level_scene_spritesheet_and_background_requests_use_references --manifest-path server-rs\Cargo.toml、cargo test -p api-server match3d_derived_asset_prompts_match_three_sheet_pipeline --manifest-path server-rs\Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle/generation.rs、server-rs/crates/api-server/src/match3d/works.rs、server-rs/crates/api-server/src/generated_asset_sheets.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼 hit object 不要只相信透明底 prompt
- 现象:苹果等主题试玩时,中央敲击物图带明显黑底;背景图中央还可能出现苹果主体,或背景环境图偶发变成纯绿色底,和“中央只叠加 hitObjectAsset”的运行态设定冲突。
- 原因:gpt-image-2 对“透明底”和“背景只做外围氛围”的遵循不稳定。若 hit object 直接入库,黑底会被当成真实像素展示;若背景 prompt 只有软描述,模型会把主题主体画进中央。第一步为了去背刻意要求绿幕图时,如果第二步参考图或 prompt 没有切断绿幕语义,背景图也可能继承纯绿色画布。
- 处理:敲木鱼 hit object prompt 固定要求先输出
1:1单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景主体图,再由api-server只对绿幕背景做去绿透明化;不要回到黑底 / 白底 / 透明底 prompt 后再做泛抠图。背景生成必须使用第一步抠图完成后的透明图作为参考图,并在 prompt 中显式禁止继承绿色底色、绿幕底色或纯绿色画布;背景 prompt 还要固定要求中央 40% 主体预留区干净,禁止主题主体、局部特写、轮廓影子、重复元素和主题碎片,只允许外围氛围。不要在背景 prompt 写“木鱼预设在屏幕中央位置”或类似中心主体正向描述,运行态敲击物只能由前端叠放。 - 验证:
cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml,并用花朵 / 苹果 / 玉米主题跑试玩图确认绿幕被去除、主体未被抠除、背景中央不出现主题主体,背景环境图不再出现纯绿色底。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼返回按钮不要让模型自由发挥外圈花纹
- 现象:返回按钮试玩图有时会被画成徽章、花盘、浮雕圆牌,甚至出现复杂外圈和装饰花纹,左箭头反而不够突出。
- 原因:prompt 只说“主题化返回按钮”时,image2 会把参考图里的装饰语言一起学进去;如果没有把形状收束到“标准圆形 + 单个居中左箭头”,模型会优先补造型而不是补图标。
- 处理:返回按钮生成 prompt 必须只允许参考图约束圆形底色与箭头配色,明确禁止复杂造型、花纹、浮雕边、异形外框和装饰图案,按钮本体固定为标准圆形,视觉尺寸比当前模板再放大约 50%,圆形外沿需要一圈与主题色搭配的干净外描边。
- 验证:
cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml,并重新试玩确认返回按钮只剩圆形底色和中央左箭头。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md.
敲木鱼历史已发布作品缺返回按钮要补齐,不要靠推荐过滤
- 现象:推荐页或公开列表中的历史敲木鱼作品点击运行态时报
敲木鱼运行态需要完整作品配置,但这类作品的敲击物、背景、音效和飘字都已完整,只是backButtonAsset为空。 - 原因:早期已发布作品缺少统一的默认返回按钮快照;运行态启动时如果仍直接按完整配置校验,就会把可玩的历史作品拒掉。这个问题不应通过推荐流或公开列表过滤解决。
- 处理:
spacetime-module在start_wooden_fish_run_tx和 work snapshot 构建时,若作品已发布且generationStatus=ready,但仅缺backButtonAsset,就补写内置默认返回按钮/UI/11_left_arrow.png,再继续进入运行态。默认返回按钮以bundled-default资产快照写回 work profile,字段保持assetId=wooden-fish-default-back-button、imageObjectKey=public/UI/11_left_arrow.png。 - 验证:历史木鱼作品点击运行态不再报完整作品配置缺失;第一次进入后,work profile 里应补出
backButtonAsset。 - 关联:
server-rs/crates/spacetime-module/src/wooden_fish.rs、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
敲木鱼创作生成不要沿用 15 秒会话超时
- 现象:敲木鱼工作台点击“生成”后,前端直接提示
请求超时:15000ms,但后端和 VectorEngine 未必已经失败。 - 原因:
createCreationAgentClient的createSessionTimeoutMs默认是 15 秒;敲木鱼创作链路会继续进入生成页并执行多次 image2 edits、去绿背景处理和 OSS 写入,单次请求窗口如果继承共享默认值,会早于业务生成完成被前端中断。 - 处理:敲木鱼 client 必须单独配置长等待窗口,同时覆盖
createSessionTimeoutMs与executeActionTimeoutMs;不要修改共享默认值影响其它轻量创作 Agent。 - 验证:
npm run test -- src/services/wooden-fish/woodenFishClient.test.ts,并在本地触发一次木鱼创作确认不再出现 15 秒前端超时。 - 关联:
src/services/wooden-fish/woodenFishClient.ts、src/services/creation-agent/creationAgentClientFactory.ts、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md。
敲木鱼创作“卡住”先查 2xx 慢请求
- 现象:敲木鱼工作台点击生成后长时间停留在生成页,看起来像卡住;
api-server日志可能出现/api/creation/wooden-fish/sessions/{sessionId}/actions的2xx慢请求,耗时可达数分钟,例如latency_ms=525473。 - 原因:当前
compile-draft是同步 action,会串行等待敲击物、背景环境图、返回按钮图三次 image2 edits、去绿处理、OSS 写入和 SpacetimeDB 草稿写回;提示词生成音效已关闭,不应作为生成阶段。 - 处理:先确认日志中该 action 是不是最终 200;若是 200 慢请求,不要优先排查 WebSocket 或 SpacetimeDB procedure。前端生成页进度必须按“整理草稿 -> 生成敲击物 -> 生成背景环境图 -> 生成返回按钮图 -> 写入正式草稿”展示,并在未收到 action 回包前保持等待态,不宣称完成。
- 验证:
npm run test -- src/services/miniGameDraftGenerationProgress.test.ts -t "wooden fish",并观察木鱼生成页在 5 分钟以上等待时仍停留在合理阶段。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
本地 SpacetimeDB procedure 超时或缺失先查版本错配
- 现象:敲木鱼创作时点击“生成”提示
SpacetimeDB procedure 调用超时,或后台 Dashboard 的指标与柱状图同时消失;服务端日志更早出现Failed to BSATN deserialize procedure return value、No such procedure,Dashboard 请求返回502。 - 原因:本机
spacetimeCLI / standalone 版本与server-rs/Cargo.toml锁定的spacetimedb版本不一致时,procedure 返回值会在宿主侧反序列化失败,api-server 继续等待就表现成调用超时。若旧 worktree 已删除但其 orphan standalone 仍监听原端口,API 还可能连到旧 wasm:健康检查正常,新 bindings 对应的 procedure 却尚未发布。 - 处理:先用
spacetime --version和监听端口对应的/proc/<pid>/exe --version分别核对 CLI 与真实宿主,再和server-rs/Cargo.toml的锁定版本对齐;不能把新版本模块硬发布到旧宿主。旧实例仍有需要保留的本地数据时,先用迁移 procedure 导出,在独立端口启动匹配版本、发布当前模块并增量导入,逐表对账后再把本次 API 切到新实例;旧实例在对账前不停止。当前 dev 脚本会对带版本记录的本地实例校验dev-spacetime-tool-version,但显式连接历史端口时仍要核对真实进程和 module schema。 - 验证:CLI、standalone 与 Cargo 锁定版本一致,
/v1/ping正常,spacetime describe可找到调用中的 procedure;Dashboard 接口返回200且包含 4 张图,敲木鱼生成不再卡在 procedure timeout。另执行npm run test -- scripts/dev.test.ts验证本地调度门禁。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、server-rs/Cargo.toml、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
拼图 UI spritesheet 运行态不要二次包圆底或拉伸比例
- 现象:拼图运行态左上返回和右上设置按钮外面出现白色圆圈;底部“提示 / 原图 / 冻结”三枚素材被压扁、拉宽或拉成正圆,和图集原始按钮比例不一致。
- 原因:UI spritesheet 已经包含按钮视觉本体,但运行态仍给顶部按钮套默认圆形 icon 容器;底部三枚素材用
h-full w-full rounded-full铺满按钮格,覆盖了自动检测矩形的真实宽高比。 - 处理:有
uiSpritesheetImage*时,顶部返回 / 设置按钮容器只保留透明点击区和 focus 状态,不再叠加默认圆形底;buildPuzzleUiSpriteBackgroundStyle(...)对检测到的矩形写入aspectRatio,底部三枚素材按原始宽高比和最大尺寸渲染,不强制w-full。 - 验证:
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx、npm run test -- src/services/puzzle-runtime/puzzleUiSpritesheetParser.test.ts。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/services/puzzle-runtime/puzzleUiSpritesheetParser.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
2026-05-22 补充:展示矩形和点击热区要分开处理。puzzleUiSpritesheetParser 的 regions 保留完整视觉裁切矩形,hitRegions 用较高 alpha 阈值只包住实心按钮主体;运行态底部 spritesheet 道具按钮启用 puzzle-runtime-sprite-tool-button--precise-hit,父按钮不吃整块透明留白,内部 puzzle-runtime-ui-sprite-hit-zone 才接收指针事件,避免透明区域成为点击热区。
图像输入组件不要把业务状态藏在页面内联实现里
- 现象:拼图页把参考图上传、缩略图、主图删除确认和 AI 重绘开关内联实现后,后续想复用到其它创作页时,页面级状态和通用 UI 状态混在一起,容易出现多套上传卡和参考图展示口径。
- 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。
- 处理:抽
CreativeImageInputPanel时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。 - 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。
- 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx。
RPG 发布不能只依赖 agent session seed_text
- 现象:RPG 结果页
publish_world返回UPSTREAM_ERROR,details 为custom_world.setting_text 不能为空;同一 session 的result-view日志显示publish_ready=true。 - 原因:前端发布动作只提交
{ action: 'publish_world' },旧 agent 会话的seed_text可能为空;如果后端只从 action payload 或seed_text取setting_text,就会在最终 compile / publish 校验阶段失败。 - 处理:
module-custom-world::resolve_custom_world_publish_setting_text(...)以当前draft_profile_json为草稿真相,优先读取settingText、creatorIntent.rawSettingText、creatorIntent.worldHook、worldHook、anchorContent.worldPromise(.hook)、summary、name/title,最后才回退seed_text。 - 验证:
cargo test -p module-custom-world publish_setting_text --manifest-path server-rs\Cargo.toml;cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml。 - 关联:
server-rs/crates/module-custom-world/src/application.rs、server-rs/crates/spacetime-module/src/custom_world.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 已发布结果页进入世界不能重复 publish_world
- 现象:RPG 草稿发布成功后,按钮文案已变为“进入世界”,但点击仍请求
POST /api/runtime/custom-world/agent/sessions/{sessionId}/actions且 payload 为{"action":"publish_world"},后端返回publish_world is only available during object_refining, visual_refining, long_tail_review or ready_to_publish。 - 原因:按钮文案依据 agent session
stage === 'published'切换,但点击处理仍走发布协调路径;如果前端只依赖草稿同步回包判断是否已发布,回包为空或缺少可进入状态时就会继续重复发送publish_world。 - 处理:进入世界协调器接收当前 agent session stage;当 stage 已为
published时,只调用result-view回读已发布 profile 并启动运行态,不再调用sync_result_profile或publish_world。 - 验证:
npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx;确认已发布场景下syncAgentDraftResultProfile与executePublishWorld均未被调用。 - 关联:
src/components/rpg-entry/useRpgCreationEnterWorld.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 点击启动黑屏 / 默认 profile 先查 profile 归一化和摘要覆盖
- 现象:作品详情点击“启动”后页面切到 RPG runtime,但用户只看到黑屏、空白,或进入默认角色 / 默认 profile;从作品详情点“作品编辑”后开局 CG、封面、角色图、技能动作预览、初始物品图标或场景背景图丢失;DevTools 里可能同时看到旧自动存档
/api/runtime/save/snapshot被主动 cancel。 - 原因:
/custom-world-library//custom-world-gallery详情接口可能返回历史或摘要式profile,缺少playableNpcs、storyNpcs、landmarks、attributeSchema等运行态字段;前端 client 若直接把该对象传给 runtime,角色选择首屏会在buildCustomWorldPlayableCharacters(profile)或后续属性解析处抛错。另一类常见原因是详情接口已回读完整 profile 后,savedCustomWorldEntries里的列表摘要又把selectedDetailEntry覆盖回空 profile,导致启动或编辑时只剩卡片摘要。发布 / 回读 result-view 若返回字段更少的旧视图,也可能把当前结果页已编辑资产降级掉。save/snapshot (canceled)通常是切 runtime 或卸载时AbortController取消旧自动存档,不是黑屏根因。 - 处理:RPG 入口作品库 client 在所有返回
CustomWorldLibraryEntry<CustomWorldProfile>的接口边界统一调用normalizeCustomWorldProfileRecord,并用profileId/worldName/subtitle/summaryText补齐旧数据缺字段;详情页已拿到运行态字段或资产槽位更多的完整 profile 时,不允许列表摘要覆盖当前详情;同一profile.id下,正式进入世界发布 / 回读不得用字段更少的后端旧视图降级当前结果页 profile。normalizeCustomWorldProfileRecord必须近似无损保留cover、openingCg、camp.narrativeResidues、landmark.visualDescription/narrativeResidues、skills[].actionPreviewConfig、initialItems[].iconSrc、attributeSchema、角色attributeProfile和sceneChapterBlueprints[].acts[]的背景与结构字段;只有背景资产的 act 也不能被过滤。角色选择页对角色生成异常或空数组回退默认角色,并保留返回按钮/轻量空态;顶层 runtime 懒加载 fallback 不使用纯null。 - 验证:
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.ts、src/components/rpg-entry/useRpgCreationEnterWorld.ts、src/data/customWorldLibrary.ts、src/services/rpg-entry/rpgEntryLibraryClient.ts、src/components/rpg-entry/RpgEntryCharacterSelectView.tsx、src/App.tsx、src/components/rpg-runtime-shell/RpgRuntimeShell.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 战后一轮战斗后卡在观察/试探/调息先查 post-battle finalization
- 现象:RPG 一轮战斗胜利后,运行态只显示默认
观察周围迹象 / 主动出声试探 / 原地调息,这些按钮只有文字反馈;点“继续冒险”后又回到同样选项,点探索只播退场/进场动画,场景和剧情不推进。 - 原因:终局战斗 action 如果只走通用
resolve_story_runtime_actionfallback,而没有在后端调用finalize_post_battle_resolution(...),就不会持久写入story_continue_adventure、deferredOptions和下一幕currentSceneActState。另外旧 bootstrap 快照可能只有connectedSceneIds/forwardSceneId、没有connections,战后选项生成若只读connections也会退回idle_explore_forward循环。 - 处理:
module-runtime-story在 story action 投影后统一调用 post-battle finalization;idle_explore_forward清理战斗态并生成下一段遭遇预览;idle_travel_next_scene/camp_travel_home_scene由后端写入新currentScenePreset、场景 act 状态、遭遇预览和runtimeStats.scenesTraveled。前端只负责播放继续、探索和切场景动画,不承接正式剧情推进真相。 - 验证:
cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml battle_tests -- --nocapture应覆盖战斗终局持久化story_continue_adventure、deferredOptions、下一幕 act,以及idle_travel_next_scene真正切换场景。 - 关联:
server-rs/crates/module-runtime-story/src/session_action.rs、server-rs/crates/module-runtime-story/src/post_battle.rs、server-rs/crates/module-runtime-story/src/battle_tests.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
RPG 战斗飘字不要只靠低对比红绿文字
- 现象:暗色或棕黑噪声背景下,战斗伤害飘字看起来像背景纹理,尤其是远端敌人头顶的小号红字几乎不可读。
- 原因:旧
CombatFloatingNumber主要依赖text-rose-200/text-emerald-200和 8px 同色 glow;在暗红、棕黑、像素噪声背景上,颜色与背景混在一起,1px 深色描边也不足以形成轮廓。 - 处理:飘字本体使用高亮近白文字、小面积半透明深色底、明显深色描边和多层黑色阴影;只增强瞬时反馈,不新增说明面板,不遮挡主要战斗画面。
- 验证:
npm run test -- src/components/game-canvas/GameCanvasEntityLayer.test.tsx覆盖伤害/治疗飘字样式策略;运行态截图中敌方头顶伤害数字应能在暗场景上辨认。 - 关联:
src/components/game-canvas/GameCanvasEntityLayer.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md。
弹窗里复用 CreativeImageInputPanel 要保留画面卡高度
- 现象:拼图草稿结果页的关卡详情弹窗中仍能看到“画面图”标题、画面描述和生成按钮,但实际画面图卡片视觉上消失。
- 原因:
CreativeImageInputPanel内部依赖flex-1、h-full和max-h-full撑开正方形画面卡;放进弹窗里的普通section后,父级没有可计算高度,卡片会被压到不可见。 - 处理:通用画面卡
puzzle-image-upload-card保持aspect-square的同时设置稳定min-height,让入口页和关卡详情弹窗都能显示主图/上传区。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx -t "opens an independent level detail dialog"应断言关卡详情中的.puzzle-image-upload-card具备最小高度类;npm run test -- src/components/common/CreativeImageInputPanel.test.tsx应继续通过。 - 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.tsx、src/components/puzzle-result/PuzzleResultView.test.tsx。
Windows provision 下载截断要断点续传而不是回退目标机下载
- 当前状态:已废弃。2026-06-01 起生产 Jenkins 流水线统一切到 Linux agent,
Genarrative-Server-Provision不再维护 Windows 下载阶段。 - 现象:
Genarrative-Server-Provision在Download Provision Tool Archives阶段出现curl: (18) end of response ... bytes missing,常见于otelcol-contrib_0.151.0_linux_amd64.tar.gz等 GitHub release 大文件。 - 原因:这是 Windows Jenkins 节点到 GitHub 的响应体被截断;若每轮都删除
.download临时文件,就会丢掉已下载部分,下一次又从头开始。 - 处理:Windows 下载函数保留
${Output}.download,curl失败时下一轮使用-C -断点续传;最终只以 GitHub release asset 的 SHA256digest作为放行条件,完整返回但 digest 不匹配才删除临时文件重新下载。不要把 SpacetimeDB 或otelcol-contrib下载挪回 Linux 目标机。 - 验证:日志应显示
curl 断点续传 ... resumeBytes=...,最终出现已下载 ... bytes=...;目标 Linux 阶段只消费stash/unstash带过去的下载件。 - 关联:
jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
OTLP 端点只填 Collector HTTP base endpoint
- 现象:生产或容器 env 里把
OTEL_EXPORTER_OTLP_ENDPOINT填成4317、Rider 端口或别的非 HTTP base endpoint 后,api-server 发不出 OTLP,或者链路被错误转发。 - 原因:api-server 当前走 OTLP HTTP,不是 gRPC;Collector 才是接收和转发边界。
- 处理:生产模板用
http://127.0.0.1:4318,容器模板用http://otelcol:4318;需要关闭时显式设GENARRATIVE_OTEL_ENABLED=false,不要通过改 endpoint 绕开 Collector 语义。 - 验证:检查 env 模板和运行态配置都指向 Collector HTTP base endpoint,日志仍通过
journalctl/ 文件日志保留。 - 关联:
deploy/env/api-server.env.example、deploy/container/api-server.env.example、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
tracking outbox 到批量阈值后先封存再异步 flush
- 现象:route tracking 高峰时如果主请求线程要等 SpacetimeDB 批量入库,接口延迟会被 outbox 写入链路拖长。
- 原因:outbox 的职责是把普通 HTTP route tracking 从请求线程切走,不能把 flush 结果回写成同步阻塞。
- 处理:达到
BATCH_SIZE立即封存 active 文件并切新 active,FLUSH_INTERVAL_MS只做兜底封存,后台 worker 异步 flush sealed 文件;成功删文件,失败保留重试,坏文件隔离为corrupt-*,MAX_BYTES只做磁盘保护。 - 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
- 关联:
server-rs/crates/api-server/src/tracking_outbox.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳推荐页匿名直玩要同步放行 runtime 路由和埋点
- 现象:推荐页能看到跳一跳公开卡片,但未登录点击后会被登录门禁拦住,或者进入运行态后没有
work_play_start记录。 - 原因:前端只改了展示层登录门禁,后端 runtime 路由仍要求 bearer auth,或 tracking helper 仍把匿名请求当成无效输入直接丢弃。
- 处理:
/api/runtime/jump-hop/runs、/jump、/restart改为可选鉴权;未登录时直接允许启动、跳跃和重开,同时让work_play_tracking接受Option用户身份并在 metadata 中标记匿名语义,不要伪造 userId。 - 验证:未登录推荐页可以直接进入跳一跳运行态,且
work_play_start事件仍会落库或出现在 outbox 中,metadata 含匿名标记。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/api-server/src/auth.rs、server-rs/crates/api-server/src/work_play_tracking.rs、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx。
跳一跳直接打开空 runtime 路由不能停在加载态
- 现象:直接访问
/runtime/jump-hop时页面看起来一直停在“正在载入游戏 / 正在加载内容”,DOM 内部只有空的跳一跳运行态,没有平台、地块或 run 数据。 - 原因:
appPageRoutes会把该路径解析为jump-hop-runtime,但裸路径没有work=JH-*公开作品码,也没有从详情页启动后写入的jumpHopRun,平台壳仍挂载JumpHopRuntimeShell。 - 处理:平台壳在
jump-hop-runtime且缺少 run 时先看work参数;有JH-*则通过公开 gallery detail 回读 profile 并启动 published run,没有则回到平台首页。全局作品码恢复 effect 在跳一跳 runtime 阶段要跳过,避免和运行态恢复互相抢路由。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop runtime route";浏览器 smoke 分别打开/、/runtime/jump-hop和/runtime/jump-hop?work=JH-*。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/routing/appPageRoutes.ts、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
release tracking outbox 权限错误先查 env 缺失
- 现象:release 机器
journalctl -u genarrative-api.service每秒刷tracking outbox 定时封存 active 文件失败 error=Permission denied (os error 13)和tracking outbox 批量写入 SpacetimeDB 失败。 - 原因:旧
/etc/genarrative/api-server.env没有GENARRATIVE_TRACKING_OUTBOX_DIR时,api-server 会回退到本地开发默认相对路径server-rs/.data/tracking-outbox;systemd 工作目录是只读发布目录/opt/genarrative/releases/<version>,genarrative用户不能在其中创建server-rs。 - 处理:补齐
GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox及 batch/flush/max 配置,创建并授权/var/lib/genarrative/tracking-outbox给genarrative:genarrative,再重启genarrative-api.service。Server-Provision 与 API-Deploy 会保留旧 env 但自动补缺这些运行态路径。 - 验证:
tr '\0' '\n' < /proc/$(systemctl show genarrative-api.service -p MainPID --value)/environ | grep GENARRATIVE_TRACKING_OUTBOX_DIR应指向/var/lib/genarrative/tracking-outbox;重启后当前 PID 不再出现Permission denied (os error 13)。 - 关联:
scripts/deploy/production-api-deploy.sh、scripts/jenkins-server-provision.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
release otelcol 217/USER 和备份 timer inactive 分开处理
- 现象:release 巡检中
otelcol-contrib.service持续activating (auto-restart),日志出现status=217/USER/Failed to determine user credentials;同时genarrative-database-backup.timer显示enabled但inactive/dead,NEXT/Trigger为空。 - 原因:otelcol 的 systemd unit 使用
User=otelcol/Group=otelcol,但目标机缺少该系统用户和/etc/otelcol/genarrative-debug.yaml;备份 timer 在 missed window 后未处于 active waiting 状态,直接重启 Persistent timer 可能在白天立刻补跑冷备份并停止 SpacetimeDB。 - 处理:先创建系统用户 / 组
otelcol,补齐/var/lib/otelcol、/etc/otelcol/genarrative-debug.yaml和/var/log/genarrative,再重启otelcol-contrib.service;修 timer 时先touch /var/lib/systemd/timers/stamp-genarrative-database-backup.timer,再systemctl daemon-reload && systemctl start genarrative-database-backup.timer,避免当前窗口立即补跑冷备份。 - 验证:
otelcol-contrib.service为active (running)且监听127.0.0.1:4317/4318;systemctl list-timers genarrative-database-backup.timer --all显示下一次触发约为次日03:20;/healthz、/readyz、/v1/ping仍通过。 - 关联:
scripts/jenkins-server-provision.sh、deploy/systemd/otelcol-contrib.service、deploy/otelcol/genarrative-debug.yaml、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
外部 API 失败没法追溯先查 external_api_call_failure
- 现象:VectorEngine 图片生成 / 编辑接口对前端只表现为
502/504或“上游服务请求失败”,但难以区分是请求发送失败、上游 429/5xx、响应解析失败、未返回图片,还是下载图片失败。 - 原因:外部 API 失败如果只靠普通日志,不一定能和 OTLP 指标、trace 与 SpacetimeDB 历史查询稳定关联;重启后也容易丢失上下文。
- 处理:先查 OTLP 指标
genarrative.external_api.failures{provider,failure_stage,status_class,retryable},再查tracking_event中event_key = 'external_api_call_failure'的metadata_json。当前通用 VectorEnginegpt-image-2-all适配器会记录 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、errorSource、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt 和 requestId。 - 验证:
SELECT event_id, scope_id AS provider, metadata_json, occurred_at FROM tracking_event WHERE event_key = 'external_api_call_failure' ORDER BY occurred_at DESC LIMIT 50;;如果查不到同时看 tracking outbox 目录权限和 sealed 文件是否堆积。 - 关联:
server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
VectorEngine 图片协议先看 platform-image,不要先翻 puzzle.rs
- 现象:排查拼图或其它玩法的生图失败时,如果直接在
api-server的大文件里找images/generations、images/edits、base64 解码或下载逻辑,会看到很多历史 helper 和测试桥,看起来像每个玩法都自带一份 provider 实现。 - 原因:旧实现把 VectorEngine 图片 provider 协议、响应解析、下载和日志混在
api-server里,后来虽然迁出到platform-image,但兼容层和测试 helper 仍会让人误判真相源位置。 - 处理:先看
server-rs/crates/platform-image/src/vector_engine/:request.rs查路径和请求体,client.rs查生成 / 编辑编排,transport.rs查 HTTP client 与 reqwest 错误归一,payload.rs查响应字段提取,response.rs查上游状态、解析、缺图和下载分流,image_source.rs查参考图和远端图片下载。再看server-rs/crates/api-server/src/openai_image_generation.rs的兼容桥和external_api_audit.rs的落库映射;puzzle/vector_engine.rs只保留玩法编排,不再作为 provider 协议真相源。 - 验证:
cargo test -p platform-image --manifest-path server-rs/Cargo.toml、cargo test -p platform-image --test vector_engine --manifest-path server-rs/Cargo.toml、cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture通过时,排障先按platform-image的日志字段查 provider / endpoint / failure_stage。 - 关联:
server-rs/crates/platform-image/src/vector_engine/、server-rs/crates/api-server/src/openai_image_generation.rs、server-rs/crates/api-server/src/external_api_audit.rs、server-rs/crates/api-server/src/puzzle/vector_engine.rs。
音频 provider 协议先看 platform-audio,不要先翻 api-server 大文件
- 现象:排查 Visual Novel 或通用创作音频生成失败时,如果直接打开
api-server/src/vector_engine_audio_generation.rs,会同时看到路由、计费、asset binding、下载、解析和 provider 协议,定位时很容易在同一个文件里来回跳。 - 原因:音频 provider 已经迁到
server-rs/crates/platform-audio/,但api-server仍保留薄 wrapper;如果把 wrapper 当真值源,就会误判边界。 - 处理:先看
server-rs/crates/platform-audio/src/client.rs、request.rs、response.rs、download.rs、persist.rs、error.rs,再看api-server/src/vector_engine_audio_generation.rs的路由、配置、计费、asset object confirm 和 entity binding 包裹。 - 验证:
cargo test -p platform-audio --manifest-path server-rs/Cargo.toml通过,且cargo check -p api-server --manifest-path server-rs/Cargo.toml保持绿灯。 - 关联:
server-rs/crates/platform-audio/、server-rs/crates/api-server/src/vector_engine_audio_generation.rs。
Hyper3D 现在只剩后端薄代理,不要再把协议解析写回 api-server
- 现象:排查 Hyper3D/Rodin 时,如果继续在
api-server/src/hyper3d_generation.rs里扩协议解析、请求体构造或下载列表处理,文件会重新变厚。 - 原因:
platform-hyper3d已经承接 Rodin 的提交、状态和下载协议解析;api-server只是薄 wrapper 和错误 envelope 映射。 - 处理:新增或修改 Hyper3D 协议时优先放到
server-rs/crates/platform-hyper3d/的client.rs、request.rs、response.rs、transport.rs和子模块,api-server只保留鉴权、配置校验和错误映射。 - 验证:
cargo test -p platform-hyper3d --manifest-path server-rs/Cargo.toml通过后再看cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-hyper3d/、server-rs/crates/api-server/src/hyper3d_generation.rs。
release 创作接口 413 先查是否还在提交 Data URL
- 现象:release 上
POST /api/runtime/puzzle/agent/sessions/{session_id}/actions携带参考图 Data URL 时返回413 Request Entity Too Large,access log 显示request_time=0.000、upstream_status=-。 - 原因:Nginx 默认
client_max_body_size只有 1 MiB,请求在反代层被拒绝,根本没有到达api-server;即使模板放宽到64m,把图片 base64 放进创作 JSON body 仍会放大请求体并把上限问题推给下一层。 - 处理:长期修复不是继续调大 Nginx,而是让浏览器先走
/api/assets/direct-upload-tickets直传 OSS,再/api/assets/objects/confirm确认asset_object,拼图 action 只提交referenceImageAssetObjectId(s);后端校验 owner / bucket / kind / MIME / size 后签只读 URL 给 VectorEngine。Nginxclient_max_body_size 64m只保留为旧客户端和兼容输入兜底,发布后仍需nginx -t && nginx -s reload。 - 验证:前端 action payload 不应再出现大段
data:image/...;base64;nginx -T 2>/dev/null | grep client_max_body_size可确认反代兜底;再次提交参考图时 access log 应有正常upstream_status,后端测试puzzle_reference_image_sources_prefer_asset_object_ids/puzzle_asset_object_reference_requires_matching_owner应通过。 - 关联:
src/services/puzzle-works/puzzleAssetClient.ts、server-rs/crates/api-server/src/puzzle/vector_engine.rs、deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、deploy/container/nginx.conf、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
汪汪声浪入口不要再回到独立配置阶段
- 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。
- 原因:旧实现把
bark-battle单独挂到bark-battle-configselectionStage,而不是复用创作 Tab 里的模板区。 - 处理:入口点击只设置
activeCreationFormType = 'bark-battle'并回到创作 Tab;BarkBattleConfigEditor作为内嵌表单使用,默认隐藏返回按钮和页面标题;runtimeonExit重新回到创作 Tab 的汪汪声浪模板。 - 验证:点击汪汪声浪后直接看到创作页内嵌表单,不再出现独立配置页;测试应覆盖内嵌表单与 runtime 返回路径。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx。
汪汪声浪发布态不要丢失结果页最终素材
- 现象:结果页上传或批量生成玩家形象、对手形象、UI 背景后,发布进入正式 runtime 仍可能显示初始草稿素材或兜底视觉。
- 原因:
publish_bark_battle_work如果只把结果页最终状态保存到published_snapshot_json,但正式 runtime 读取的config_json仍来自草稿行旧值,就会丢失结果页局部替换。 - 处理:发布时把最终
publishedSnapshot解析为BarkBattleEditorConfigSnapshot、规范化后同时写入bark_battle_published_config.config_json和published_snapshot_json;首轮自动生成只由bark-battle-generating负责,结果页仅覆盖已接入的玩家形象、对手形象和竞技背景图片槽位,不再提供音频配置入口。 - 验证:发布后 runtime config 应包含结果页最终
playerCharacterImageSrc、opponentCharacterImageSrc和uiBackgroundImageSrc。
汪汪声浪 v1 生成页和正式运行态要分开
- 现象:如果把初始三图自动生成、结果页修补、公开发布和正式运行态混在一页,创作者容易误以为一次生成和正式运行是同一职责。
- 原因:
bark-battle-generating才应该承担玩家形象、对手形象和竞技背景的自动生成;结果页只做单槽修补,正式 runtime 又必须切到真实麦克风和正式统计。 - 处理:表单提交后先进入独立生成页,部分失败仍进结果页;结果页只保留单槽重试、重新生成和上传,不再保留一次生成按钮、音频配置入口、皮肤预设入口或排名配置。发布后先到统一作品详情页,再进正式 runtime;草稿试玩允许 mock,不写正式 run。
- 验证:生成页负责首轮自动产出三图;结果页不出现一次生成按钮、音频配置入口、皮肤预设入口或排名配置;正式 runtime 必须麦克风可用且会写正式 run,草稿试玩不写正式统计。
汪汪声浪生成页不要只停留在前端内存草稿
- 现象:点击“生成草稿”后生成页一直转圈,或刷新 / 回到草稿架后看不到三图素材。
- 原因:生成页只在前端内存里合并玩家形象、对手形象和竞技背景,没有把生成结果写回
bark_battle_draft_config.config_json;另外 BFF 若在刚创建草稿后先读spacetime-client订阅 cache 再保存,cache 可能短暂落后,导致保存失败或返回旧快照。 - 处理:生成页三图完成后调用
POST /api/creation/bark-battle/drafts/{draftId}/config持久化;保存接口直接把请求快照交给 SpacetimeDB procedure,由模块事务校验 owner / work,并在 HTTP 回包用本次请求里的三图字段覆盖,避免订阅 cache 滞后;保存请求必须设置前端超时,保存失败也进入结果页并标记部分失败。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx src/services/bark-battle-creation/barkBattleCreationClient.test.ts src/components/bark-battle-creation/BarkBattleResultView.test.tsx packages/shared/src/contracts/barkBattle.test.ts;npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "bark battle";cargo check --manifest-path server-rs\Cargo.toml -p api-server。 - 关联:
src/components/bark-battle-creation/BarkBattleGeneratingView.tsx、src/services/bark-battle-creation/barkBattleCreationClient.ts、server-rs/crates/api-server/src/bark_battle.rs、server-rs/crates/spacetime-module/src/bark_battle.rs。
汪汪声浪三图不要复用 RPG 场景图链路
- 现象:玩家形象和对手形象看起来走了场景图片 prompt;生成页三个槽位同时转圈,但只有第一个真实生成,首图返回后三个槽位一起停止或只显示首图。
- 原因:前端曾复用
/api/runtime/custom-world/scene-image,三类素材都被当成 RPG landmark scene image;生成页又只用父级 draft 判断 ready,批量 Promise 结束后才一次性合并结果,缺少逐槽状态。 - 处理:Bark Battle 生图统一走
POST /api/creation/bark-battle/images/generate,请求体包含slot和 v1 配置;后端在api-server/src/bark_battle.rs按player-character、opponent-character、ui-background分别拼装正式 prompt,写入generated-bark-battle-assets,并返回prompt/actualPrompt。前端generateAllBarkBattleImageAssets保持三槽Promise.allSettled并通过onSlotComplete逐槽刷新生成页状态。 - 验证:
npm run test -- src/services/bark-battle-creation/barkBattleCreationClient.test.ts src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx packages/shared/src/contracts/barkBattle.test.ts;cargo test -p shared-contracts bark_battle --manifest-path server-rs\Cargo.toml;cargo check --manifest-path server-rs\Cargo.toml -p platform-oss -p api-server。 - 关联:
src/services/bark-battle-creation/barkBattleCreationClient.ts、src/components/bark-battle-creation/BarkBattleGeneratingView.tsx、server-rs/crates/api-server/src/bark_battle.rs、server-rs/crates/platform-oss/src/lib.rs。
抓大鹅批量重新生成物品不要新增 itemId
- 现象:结果页批量重新生成物品后,试玩或正式运行态的物品类型和图片对应关系漂移,或者用户输入一个不存在名称后被当作新物品追加。
- 原因:重新生成和批量新增共用
item-assets接口,如果前端不传mode = "replace",或后端替换时重新分配itemId/ 追加未匹配名称,就会破坏generatedItemAssets顺序和运行态类型映射。 - 处理:批量重新生成只提交当前素材列表中能匹配到的名称,并传
mode = "replace";后端只对同名已有素材生成新图片,合并时保留原itemId、itemName、模型兼容字段、UI 背景和历史音频字段,未匹配名称直接忽略且不计费。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx覆盖前端提交口径,cargo test -p api-server match3d_item_asset --manifest-path server-rs\Cargo.toml和cargo test -p api-server match3d_regenerated_asset --manifest-path server-rs\Cargo.toml覆盖后端替换计划与身份保留。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、server-rs/crates/api-server/src/match3d.rs、packages/shared/src/contracts/match3dWorks.ts、server-rs/crates/shared-contracts/src/match3d_works.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅生成封面图不要覆盖物品素材或配置
- 现象:结果页生成封面图后,
素材配置 > 物品中已有物品素材被清空、回退旧快照,或难度 / 消除次数被改回旧值。 - 原因:封面生成属于定向图片槽位更新;若后端复用草稿编译写回,可能按 session config 重算作品行。即使后端已修正,前端若直接把封面接口返回的整份
item当成最新 profile,也可能用旧回包里的空generatedItemAssets覆盖当前页面素材。 - 处理:
POST /api/creation/match3d/works/{profileId}/cover-image只保存coverImageSrc/coverAssetId等封面字段,保留当前generated_item_assets_json、难度、消除次数、题材和描述;前端收到回包后只合并coverImageSrc,继续保留当前可见generatedItemAssets、clearCount和difficulty。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx覆盖旧回包不覆盖物品素材和配置;cargo test -p api-server match3d_cover --manifest-path server-rs\Cargo.toml覆盖封面提示词与参考图链路。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、server-rs/crates/api-server/src/match3d.rs、server-rs/crates/spacetime-module/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
OSS V4 签名时间和 bucket/object_key 兼容
- 现象:OSS V4 私有读签名在部分时间点失败,可能出现
OSS V4 签名时间格式化失败或服务端判定签名格式错误;排查用例中 bucket 为xushi-dev,object_key 为generated-square-hole-assets/.../image.png。 - 原因:旧逻辑依赖
time::Time::to_string()再去掉冒号,小时小于 10 时输出不稳定补零;同时排查时容易把 bucket 名误当成 object_key 的一部分。 - 处理:OSS V4
x-oss-date使用固定宽度yyyyMMdd'T'HHmmss'Z'格式化;调用读签名或HEAD Object时只传 object_key,不要传bucket/object_key拼接路径。 - 验证:运行
cd server-rs && cargo test -p platform-oss -- --nocapture,并用 bucket=xushi-dev、object_key=generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png覆盖签名生成。 - 关联:
server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/platform-oss/README.md。
generated 音频路径进运行态前要先换签
- 现象:草稿页 audio 控件能播放背景音乐,但拼图或抓大鹅运行态开局后背景音乐不响,Network 可能出现裸
/generated-*-assets/...mp3私有路径 403。 - 原因:生成音乐转存到 OSS 私有对象后,
audioSrc是 generated legacy path;浏览器<audio>不能像公开静态资源一样直接请求裸路径。另一个常见误判是浏览器拒绝自动播放,资源已经进入运行态但开局第一次audio.play()被拦截。 - 处理:结果页试听控件和运行态隐藏
<audio>设置src前,都先通过useResolvedAssetReadUrl或resolveAssetReadUrl换签;签名未就绪时不要回退请求裸 generated 路径。运行态自动播放失败只静默兜底,但玩家首次按下拼图块或点击抓大鹅物品时要重试同一个背景音乐播放函数。拼图读取currentLevel.backgroundMusic.audioSrc,抓大鹅读取generatedItemAssets[].backgroundMusic.audioSrc。 - 验证:结果页试听和运行态
<audio loop>的src为签名 URL 或公开 URL;拼图/抓大鹅运行态首次局内交互后会再次尝试播放背景音乐;npm run typecheck不报契约字段缺失,后端 run response 带backgroundMusic。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md。
抓大鹅背景音乐是作品级字段但暂存在首个物品素材
- 现象:抓大鹅草稿生成日志和 work detail 中已有背景音乐,但结果页
素材配置 > 背景音乐显示“暂无音乐”,点击试玩后局内也不播放生成音乐。 - 原因:当前表结构没有作品级音频字段,背景音乐暂存在
generatedItemAssets[]。如果 action response 的 draft assets 缺音乐,前端又优先用它覆盖 work detail,或音乐落在非首个素材而结果页只读assetDrafts[0].backgroundMusic,就会丢掉已生成音乐。 - 处理:前端统一使用
normalizeMatch3DGeneratedItemAssetsForRuntime/mergeMatch3DGeneratedItemAssetsForRuntime:把任意素材上的backgroundMusic与音乐元信息迁移到首个素材,清空其它素材上的作品级音乐字段;action draft assets 与 work detail assets 按itemId合并,保留详情里的音乐、UI 背景和点击音效。 - 验证:
npm run test -- src\services\match3dGeneratedModelCache.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx;平台推荐流定向跑RpgEntryFlowShell.agent.interaction.test.tsx中的 Match3D runtime assets 用例;npm run typecheck。 - 关联:
src/services/match3dGeneratedModelCache.ts、src/components/match3d-result/Match3DResultView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
中文乱码与编码风险
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
- 原因:Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
- 处理:
- 不要直接沿用乱码文本。
- 不要用英文替换中文,除非用户明确要求翻译。
- 在 PowerShell 5.1 中显式使用 UTF-8。
- 优先用 Python/Node 或
Get-Content -Encoding UTF8核对原文。 - 修改中文文件时优先局部补丁,避免无关内容重写。
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:
AGENTS.md、npm run check:encoding。
SpacetimeDB 运行态查询不要绕过已有索引或用 procedure JSON 回传
- 现象:运行态接口看起来只查当前用户、作品或任务,却在
spacetime-module中使用ctx.db.<table>().iter().filter(...)整表遍历;或者 procedure result 返回items_json/run_json/work_json等 JSON 字符串,spacetime-clientmapper 再反序列化成旧兼容结构。 - 原因:新增索引或 typed snapshot 后,没有同步清理旧 mapper / 测试兼容层,也没有用静态检查拦截回退写法。
- 处理:表上已有主键、unique 或
#[index]覆盖查询前缀时,先用对应 accessor.find(...)/.filter(...),只对索引无法覆盖的条件做内存残余过滤;procedure result 返回 typed snapshot / typed value,不再跨层传*_json: Option<String>作为 payload。 - 验证:执行
npm run check:spacetime-runtime-access、npm run check:server-rs-ddd,涉及绑定变化时先执行npm run spacetime:generate和npm run check:spacetime-schema。 - 关联:
docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、scripts/check-spacetime-runtime-access.mjs、server-rs/crates/spacetime-module/src/*、server-rs/crates/spacetime-client/src/mapper.rs。
拼图广场列表不要每次 HTTP 请求调用 SpacetimeDB procedure
- 现象:
/api/runtime/puzzle/gallery每个请求都走spacetime-client.list_puzzle_gallery()调用 SpacetimeDB procedure,导致 SpacetimeDB WASM 侧重复组装全量列表,客户端再映射一遍;历史实现还出现过 procedure JSON 字符串往返。 - 原因:
api-server的服务器端spacetime-client没有订阅可公开读取的 gallery 投影,虽然 SDK 支持 client cache,但请求路径仍把列表读取当作 procedure 调用。 - 处理:
spacetime-module中用 public viewpuzzle_gallery_card_view暴露已发布拼图作品的列表卡片字段,不携带levels/anchor_pack等详情级载荷;spacetime-client建连接后订阅SELECT * FROM puzzle_gallery_card_view和SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'并等待on_applied。HTTP gallery 通过PuzzleGalleryCache缓存最终PuzzleGalleryResponseDTO:items返回前 10 个完整卡片,previewRefs返回后 10 个作品号引用,cache miss / TTL 过期时单飞重建,后台 cleanup task 周期清理旧响应。旧list_puzzle_galleryprocedure 只作兼容,不再作为 HTTP gallery 主路径。 - 验证:搜索
server-rs/crates/spacetime-client/src/puzzle.rs不应再出现 gallery 主路径调用list_puzzle_gallery_then;搜索server-rs/crates/spacetime-client/src/lib.rs应订阅puzzle_gallery_card_view;执行npm run spacetime:generate、cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client、cargo check --manifest-path server-rs/Cargo.toml -p api-server和 schema/runtime access 检查。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/spacetime-client/src/puzzle.rs、server-rs/crates/api-server/src/puzzle_gallery_cache.rs、/api/runtime/puzzle/gallery。
Windows 本地直连高 VU 压测不要误判成业务内存泄漏
- 现象:本地 Windows release
api-server直连 K6 压测时,250 RPS、PREALLOCATED_VUS=300能把进程 private memory 瞬时推到约 7GB;同样配置打/healthz小响应也能复现,压测结束后回落到 100MB 级。 - 原因:高水位主要来自本机直连的 K6 VU / 长连接 / Hyper 发送链路和 Windows 连接缓冲,不是 SpacetimeDB procedure、拼图 JSON 缓存或 OTEL exporter。降低到接近真实并发的 VU 后,同样 250 RPS 拼图广场 p95 约 9ms,峰值约 600MB。
- 处理:本地容量判断时让
PREALLOCATED_VUS/MAX_VUS接近真实并发,不要把过高 VU 预分配当作默认吞吐测试;同时观察process.memory.*、process.windows.handle.count、genarrative.http.server.response_bodies.in_flight、genarrative.http.server.request_permits.available、genarrative.puzzle_gallery.cache.*和genarrative.spacetime.read.*。如果内存高但 body in-flight、背压 permit、cache rebuild 和 SpacetimeDB read 都不显示积压,优先按连接 / 发送链路高水位处理。 - 验证:对照打
/api/runtime/puzzle/gallery与/healthz;对比PREALLOCATED_VUS=300 MAX_VUS=800和PREALLOCATED_VUS=20 MAX_VUS=40;压测结束后继续采样 10 秒确认 private memory 回落。 - 关联:
scripts/loadtest/README.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、server-rs/crates/api-server/src/process_metrics.rs、server-rs/crates/api-server/src/telemetry.rs。
容器高 VU 下 /healthz RSS 尖峰先查 Axum state 深拷贝
- 现象:容器 Linux release
api-server打/healthz,500 HTTP req/s、PREALLOCATED_VUS=100只跑 1 秒也能把 RSS 推到约 1 GiB;同样问题与作品列表、SpacetimeDB procedure、业务 cache 和请求日志等级无关。 - 原因:
AppState曾直接#[derive(Clone)]大结构体,里面包含配置、SpacetimeDB client、平台服务、认证服务和多组 cache。Axum/Hyper 会在 router/service/connection 路径频繁 clone state,高并发 keepalive 下会放大为状态深拷贝高水位。 - 处理:
server-rs/crates/api-server/src/state.rs的AppState必须保持Arc<AppStateInner>浅拷贝壳;新增共享状态字段时放入AppStateInner,不要把外层改回大结构体 clone。 - 验证:用容器内 k6 直连
api-server:8082/healthz,500 HTTP req/s、PREALLOCATED_VUS=100、30 秒压测后采样/proc/$pid/status、/proc/$pid/smaps_rollup和 cgroupmemory.current/memory.peak。2026-05-18 修复后结果为15001请求、http_req_failed=0、dropped_iterations=0,RSS 约 18 MiB -> 52 MiB,cgroup peak 约 47 MiB。 - 关联:
server-rs/crates/api-server/src/state.rs、deploy/container/README.md、deploy/container/api-server.Dockerfile。
Gallery 压测延迟升高先查入口过量放行和 TTL 边界刷新
- 现象:公开作品列表在 500-1000 HTTP req/s 附近可能吞吐没有明显提升,但 p95 变高、VU 上升,甚至出现排队和 dropped iterations。
- 原因:Nginx、Axum 和缓存刷新边界如果同时允许过多请求进入,压力会先堆在连接、service 和 cache rebuild 周围;这类延迟不等同于数据库连接池不足。
- 处理:Nginx 按 endpoint 使用
limit_req快拒绝,api-server 按default/gallery/detail/admin分组 semaphore 快拒绝;拼图广场 TTL 过期时已有缓存先返回 stale 响应,只允许一个后台 refresh 任务重建,冷启动无缓存时才同步构建。 - 验证:OTLP 看
genarrative.http.server.request_permits.available{pool=...}、genarrative.puzzle_gallery.cache.stale_hits、refreshes_started、refreshes_failed,Nginx access log 看request_time与upstream_response_time是否同步收敛;超过容量时应明确 429,而不是长时间排队或新增 502。 - 关联:
deploy/nginx/genarrative.conf、deploy/container/nginx.conf、server-rs/crates/api-server/src/backpressure.rs、server-rs/crates/api-server/src/puzzle_gallery_cache.rs。
多玩法公开广场列表优先订阅 public view / read model
- 现象:抓大鹅、方洞挑战、视觉小说、大鱼吃小鱼等公开列表如果沿用
list_*_worksprocedure,即使只读已发布作品,也会在每个 HTTP 请求里回到 SpacetimeDB WASM 侧扫描、反序列化配置并组装列表,50RPS 以上容易变成热点。 - 原因:个人作品列表和公开广场列表复用了同一套 procedure 输入,导致公开列表为了通过 owner 校验传固定占位 owner,并把可长期同步的公开读模型当成请求期查询。
- 处理:每个公开广场新增或复用专用 public view / public read model:
match_3_d_gallery_view、square_hole_gallery_view、visual_novel_gallery_view、big_fish_gallery_view。spacetime-client建连接后订阅这些 view 和对应public_work_play_daily_statsource_type 桶,HTTP gallery 只读本地 cache。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍走原有 procedure / reducer。 - 验证:搜索
server-rs/crates/spacetime-client/src/{match3d,square_hole,visual_novel,big_fish}.rs,公开 gallery 主路径应读取connection.db().*_gallery_view(),不应调用list_*_works_with_input;执行npm run spacetime:generate、cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema。 - 关联:
server-rs/crates/spacetime-module/src/match3d.rs、server-rs/crates/spacetime-module/src/square_hole.rs、server-rs/crates/spacetime-module/src/visual_novel.rs、server-rs/crates/spacetime-module/src/big_fish/session.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure
- 现象:
/api/runtime/custom-world-gallery每次请求调用list_custom_world_gallery_entriesprocedure;入口熔断中间件每个玩法请求调用get_creation_entry_configprocedure,50RPS 以上会把 SpacetimeDB procedure 调用变成热点。 - 原因:
custom_world_gallery_entry、creation_entry_config和creation_entry_type_config已经是可订阅读模型或配置表,但 HTTP 路径仍按“请求到来再查 procedure”处理。 - 处理:
spacetime-client长连接订阅custom_world_gallery_entry、public_work_play_daily_stat的custom-world桶、creation_entry_config和creation_entry_type_config;custom-world gallery 从本地 cache 排序并聚合 7 日播放数;入口配置优先读订阅 cache,cache 缺失时用最近一次成功内存快照,再兜底调用get_creation_entry_config完成旧库兼容。旧list_custom_world_gallery_entriesprocedure 只允许作为旧库缺少 gallery 行时的一次性同步兜底。 - 验证:搜索
server-rs/crates/spacetime-client/src/custom_world.rs,gallery 主路径应是read_after_connect读取custom_world_gallery_entry();搜索server-rs/crates/spacetime-client/src/runtime.rs,get_creation_entry_config应优先读取creation_entry_config()和creation_entry_type_config()。执行cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/spacetime-client/src/custom_world.rs、server-rs/crates/spacetime-client/src/runtime.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行
- 现象:使用 VectorEngine
gpt-image-2生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回429/当前分组上游负载已饱和;同一批次里后续图片会被前面的慢请求拖住。 - 原因:复杂抽象 logo prompt 同时包含品牌解释、禁用元素、中文结构和多重隐喻时,上游排队与生成时长不稳定;并发或批量运行会放大单条慢请求的影响。
- 处理:先
--dry-run看请求体;真实生成时优先短 prompt、单一造型、单张串行或小批量。失败后不要反复重试同一长 prompt,先压缩到“一个主体 + 一个负形 + 颜色 + 禁用文字/播放键/聊天气泡”再跑。联系表中的中文标签不要通过 PowerShell 管道内联 Python 写入,容易因编码链路显示为问号,可改用英文标签或脚本文件方式。 - 验证:生成文件落在
public/branding/taonier-logo-*/,用 Pillow 检查图片尺寸和非空;执行node --check scripts/generate-taonier-logo-concepts.mjs、npm run check:encoding、git diff --check。 - 关联:
scripts/generate-taonier-logo-concepts.mjs、docs/design/TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md。
忘记密码后仍提示手机号或密码错误先查认证投影同步
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现
Bearer JWT 版本已失效,日志里的 token version 与本地快照不一致。 - 原因:重置/修改密码会更新
password_hash、password_login_enabled和token_version,如果 API 层只更新本地InMemoryAuthStore,没有调用sync_auth_store_tables_to_spacetime(),api-server重启时可能从旧的 SpacetimeDB 正式认证表恢复账号状态。 - 处理:
POST /api/auth/password/change与POST /api/auth/password/reset成功后必须同步正式认证表。2026-07-01 起,auth_store_snapshot表和旧 JSON procedure 已删除;认证工作集只通过 typed projection 同步user_account/auth_identity/refresh_session。认证创建、登录会话、刷新、退出、改密、重置密码、绑定和资料变更等写操作必须在返回客户端前成功同步 SpacetimeDB;同步失败时接口返回错误,不允许把只存在于当前进程内存的账号或会话当成成功结果。新用户注册奖励、邀请码绑定和登录埋点必须排在认证同步成功之后,避免认证没落库时先写出钱包或邀请关系。 - 验证:执行
cargo test -p module-auth password --manifest-path server-rs/Cargo.toml与cargo test -p api-server password --manifest-path server-rs/Cargo.toml;手测时重设密码后旧密码应失败,新密码应成功,重启后仍应保持。 - 关联:
server-rs/crates/api-server/src/password_management.rs、server-rs/crates/api-server/src/state.rs、docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md。
密码登录失败且短信登录提示手机号已存在先查孤儿手机号索引
- 现象:老账号用密码登录提示“手机号或密码错误”,改用短信验证码登录又提示“手机号已存在 / 已注册”,用户卡在既不能登录也不能重新创建的状态。
- 原因:历史版本或停服务时认证同步不完整,可能在 SpacetimeDB
auth_identity(provider=phone)或旧module-auth快照里留下phone_to_user_id映射,但对应user_account/users_by_username用户行已经不存在。密码登录按手机号索引找不到真实用户,短信登录尝试创建新用户时又被孤儿手机号索引挡住。 - 处理:
export_auth_store_projection_from_tables只导出正式认证表 projection;module-auth从 projection 恢复时必须丢弃指向不存在user_account的 identity、union 索引和 refresh session。运行时创建手机号用户前若发现手机号映射指向不存在的用户,应删除孤儿映射后继续创建,避免死锁态继续扩散。 - 验证:
cargo test -p module-auth projection --manifest-path server-rs/Cargo.toml、cargo test -p module-auth phone --manifest-path server-rs/Cargo.toml、cargo test -p api-server phone_login_reuses_existing_user_for_same_phone_number --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/module-auth/src/lib.rs、server-rs/crates/spacetime-module/src/auth/procedures.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
认证快照表和旧 procedure 已删除
- 现象:有些旧代码和生成 bindings 里还会残留
get_auth_store_snapshot、upsert_auth_store_snapshot、import_auth_store_snapshot、import_auth_store_snapshot_json、export_auth_store_snapshot_from_tables,或者把auth-store.json误当成认证恢复源。 - 原因:认证恢复已经彻底收口到 SpacetimeDB 正式表和
module-authtyped projection;本地文件持久化或 JSON 快照会和正式表投影打架,SpacetimeDB 不可用时还可能把旧快照回灌到用户表。 - 处理:先用
npm run spacetime:generate刷新 bindings,确认server-rs/crates/spacetime-client/src/module_bindings.rs里已没有旧 snapshot table / procedure 导出;module-auth只保留内存态和 projection view,不再写本地快照文件。 - 验证:
cargo check -p module-auth --manifest-path server-rs/Cargo.toml、cargo check -p api-server --manifest-path server-rs/Cargo.toml、npm run check:spacetime-schema、npm run check:encoding。
抓大鹅生成页只显示服务暂不可用先查 reason 和外部服务配置
- 现象:点击生成抓大鹅草稿后,页面只提示“服务暂不可用”,或者本地
npm run dev:api-server看似启动但生成接口不可用。 - 原因:配置缺失类错误通常在后端
error.details.reason中给出具体缺项,前端如果只读details.message会吞掉原因;本地只配置ALIYUN_OSS_BUCKET/ALIYUN_OSS_ENDPOINT时,旧逻辑还会在启动期构造空 AccessKey 的 OSS 客户端并失败。抓大鹅新链路仍是 2D 生图切割,不需要也不应回退 Rodin/GLB。 - 处理:前端 API 错误展示优先读取
details.reason,再读取details.message,避免底层error sending request覆盖真正可操作的配置或网络原因;api-server只有在 OSS 四件套齐全时初始化 OSS 客户端,部分缺失只记 warning 并让具体 generated 上传/换签接口返回OSS 未完成环境变量配置。抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS,并通过details.missingEnv列出缺项;真实生成需补齐VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY和完整ALIYUN_OSS_*四件套。抓大鹅 UI spritesheet 和物品 spritesheet 的提示词必须要求单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景,后端上传 OSS 前统一扣成透明 PNG,避免运行态 alpha 连通域解析失败。 - 验证:
npm run test -- src/services/apiClient.test.ts覆盖details.reason;cargo test -p api-server state --manifest-path server-rs/Cargo.toml覆盖半配置 OSS 不阻断启动;npm run dev:api-server后按实际GENARRATIVE_API_PORT请求/healthz,不要默认打3100。 - 关联:
packages/shared/src/http.ts、server-rs/crates/api-server/src/state.rs、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md、docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md。
2026-05-22 补充:抓大鹅“物品 spritesheet”不再按旧 Gemini generateContent / 5*5 sheet 路径排查;当前链路先用 gpt-image-2 无参考图生成 9:16 关卡整图,再以该关卡整图作为 multipart image 参考并发编辑生成 1K 1:1 UI spritesheet、1K 9:16 背景图和 2K 1:1 物品 spritesheet。UI 与物品 spritesheet 都要求单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕背景,上传 OSS 前通过后端透明化处理写入真实 alpha PNG。
抓大鹅发布按钮要先开发布面板,封面编辑收口到发布面板内
- 现象:抓大鹅结果页发布按钮看起来点不了,或者封面编辑仍然分散在作品信息 Tab 里,和拼图发布体验不一致。
- 原因:发布按钮被
publishReady直接禁用,导致未满足门槛时无法进入发布检查面板;封面编辑仍挂在作品信息 Tab,不能和发布检查一起收口。 - 处理:发布按钮只受忙碌态控制,点击后始终打开独立发布面板;发布面板内先展示阻断项,再承载封面图上传 / AI 重绘 / 参考图编辑,满足条件后再点击
发布到广场。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx;npm run typecheck。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/components/match3d-result/Match3DResultView.test.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
.hermes 只放共享内容,不放个人 Hermes 配置
- 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
- 原因:仓库
.hermes/与个人~/.hermes/名称相似。 - 处理:仓库
.hermes/只放 Markdown 共享记忆、计划和可公开 skills;不提交.env、config.yaml、sessions/、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_norm、actions/action/gesture/gestures/event/name/type、hands[]、leftHand/rightHand、left_hand/right_hand、左右手标记和open_palm/grab状态。/stream是 WebSocket,普通 HTTP 访问返回 404 不能当成服务不可用。 - 验证:运行
npx vitest run src\services\useMocapInput.test.ts src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx,并在本地硬件服务启动后进入/child-motion-demo实测站位、招手、左右手挥动和跳跃阶段。 - 关联:
src/services/useMocapInput.ts、src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 左右手阶段误通过先查身体侧映射和手臂展开阈值
- 现象:热身关“挥动左手 / 挥动右手”阶段,用户只是手自然下垂、横向小幅抖动,或挥了相反侧手,也可能被判定通过。
- 原因:本地 mocap 的 handedness 当前按摄像头视角输出,不能直接当作用户身体左/右;同时左右手阶段的目标是确认现实空间安全,需要验证手臂向外打开和上下摆动角度,不能只看手部
x轨迹范围。 - 处理:热身关中用户左手应消费 camera-right,用户右手应消费 camera-left;左右手阶段只在同侧肩肘腕外展、手腕非自然下垂、连续有效帧、横向范围、上下摆动范围、肩腕角度范围和上下方向变化全部达标时完成,并记录轨迹空间包络、角度范围和最大外展距离。
- 验证:运行
npx vitest run src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx src\components\child-motion-demo\childMotionWarmupModel.test.ts,确认相反侧手、自然下垂、单纯横向轨迹不会完成,真实展开上下摆动可以完成。 - 关联:
src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、src/components/child-motion-demo/childMotionWarmupModel.ts、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 角色轮廓抽搐先查 mocap 坐标防抖和渲染分层
- 现象:
/child-motion-demo中间半透明小人在真实硬件驱动下左右轻微来回摆,移动过程中看起来忽大忽小,用户很难稳定停在目标圆环内。 - 原因:
general.body.center_norm.x原始值逐包直接写入avatarX时,硬件坐标小噪声会直接驱动位置保持判定和 CSS 动画;如果角色外层同时承担横向定位和跳跃transform,半透明 PNG 在移动时也更容易出现重采样抖动观感。 - 处理:mocap 身体中心进入角色位置前必须先 clamp,再经过小幅死区、低通阻尼和单包最大步长限制;键盘 A/D 调试输入仍保持即时。角色 DOM 外层只负责横向定位,内层 sprite 负责轮廓图和跳跃位移,避免同一层
transform同时表达多种运动。 - 验证:运行
npx vitest run src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx src\components\child-motion-demo\childMotionWarmupModel.test.ts src\services\useMocapInput.test.ts src\services\child-motion-demo\childMotionDebugInput.test.ts,并用真实硬件进入站位阶段观察小幅身体晃动不会导致角色频繁左右跳动。 - 关联:
src/components/child-motion-demo/ChildMotionWarmupDemo.tsx、src/index.css、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
宝贝识物选篮误触发先查多套判定和残余轨迹
- 现象:
宝贝识物运行态打开礼物盒或反馈结束后,当前物品被连续送入左侧或右侧篮子,或硬件动作名偶发命中导致未做明确横移动作也触发选篮。 - 原因:选篮如果同时消费
wave_left_hand/wave_right_hand/wave动作名、连续横向轨迹和左右手固定篮子规则,或在correct/wrong反馈阶段继续累计手部状态,会把反馈期间残留移动或未知侧别手部误算成下一次选篮。 - 处理:宝贝识物当前选篮只允许“手先触碰中央物品 UI,物品绑定到该手,随后拖入左侧或右侧篮子区域”这一套路径;侧别为
unknown的手部不参与抓取或选篮;反馈阶段清空持有状态,不在非active阶段累计输入。进入关卡和每次正确反馈结束后自动弹出物品,不再用open_palm -> grab抓握序列激活礼物盒。 - 补充:当前本地 mocap 的 handedness 是摄像头视角,宝贝识物仍需换算为用户身体视角以展示左右手:
rightHand坐标代表玩家左手,leftHand坐标代表玩家右手。换算不再决定只能选择哪侧篮子;任意一只手都可以拖物品到任意篮子。键鼠调试保持鼠标左键=左手位置、右键=右手位置,也必须先触碰中央物品再拖入篮子。 - 验证:运行
npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/services/useMocapInput.test.ts,确认动作名负向测试、未知侧别负向测试、触碰前不能选篮和任意手拖入任意篮子用例通过。 - 关联:
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝爱画左右手反了先查 mocap 摄像头视角换算
- 现象:
宝贝爱画中真实硬件下左手指示器和右手画笔表现反向,用户抬右手却出现左手选色指示器,或抬左手却驱动画笔 / 橡皮。 - 原因:本地 mocap 的 handedness 当前按摄像头视角输出,不能直接当成用户身体左 / 右;宝贝爱画初版直接消费
latestCommand.leftHand/rightHand,漏做摄像头视角到用户身体视角的换算。 - 处理:宝贝爱画运行态消费 mocap 前先换算:
rightHand作为用户左手,用于颜色悬停和左手指示器;leftHand作为用户右手,用于画笔 / 橡皮光标、绘制、擦除和工具切换。键鼠调试输入不做该换算,继续保持鼠标左键为左手、右键为右手。 - 验证:运行
npm run test -- src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx src/components/edutainment-runtime/babyLoveDrawingModel.test.ts,确认 camera-left 驱动用户右手画笔、camera-right 渲染用户左手选色指示器。 - 关联:
src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx、docs/technical/BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md。
宝贝识物创作卡在准备结果页先查长耗时 image-2 请求
- 现象:
/creation/baby-object-match创作生成停在“准备结果页”,约 3 分钟后显示“生成失败 / 请求超时”;后端日志可能出现同一路由status=502 latency_ms=231291,或前端已失败但后端稍后返回 200。 - 原因:宝贝识物创作属于长耗时 image-2 链路。旧前端只等待 180 秒并对长耗时 POST 自动重试,容易在 VectorEngine 仍在生成时先 abort,再重复发起第二次生成;上游某张图超过后端
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS或返回 5xx 时会表现为 502。2026-05-14 后,新链路已从“2 张物品图 + 5 张视觉包装图”收敛为“1 张2x2素材 sheet + 1 张场景背景图”,左右手位置指示器改为运行态默认静态素材,不再每次创作生成,但仍需要按长耗时链路排查。 - 处理:
babyObjectMatchClient对/api/creation/edutainment/baby-object-match/assets使用 10 分钟超时并取消自动重试;后端并发启动2x2素材 sheet 和场景背景生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,按资源类别输出开始、完成和耗时日志。2x2sheet 固定包含物品 A、物品 B、篮子和礼物盒,服务端按格切图并转透明 PNG;ui-frame/smoke-puff/left-hand/right-hand不再作为新生成必需资源。 - 验证:运行
npm run test -- src/services/edutainment-baby-object/babyObjectMatchClient.test.ts src/services/miniGameDraftGenerationProgress.test.ts、cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml和编码检查;真实联调时查看宝贝识物 image-2 2x2 素材 sheet 生成完成、宝贝识物 image-2 场景资源生成完成和整体宝贝识物 image-2 资源生成完成耗时是否小于前端超时,若仍 502 再看VectorEngine 图片生成上游错误的upstreamStatus/raw_excerpt。 - 关联:
src/services/edutainment-baby-object/babyObjectMatchClient.ts、src/services/miniGameDraftGenerationProgress.ts、server-rs/crates/api-server/src/edutainment_baby_object.rs、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝识物篮子手柄白底先查 sheet 切图后处理
- 现象:
宝贝识物新生成的主题篮子在左右手柄、篮口镂空或边缘处仍出现白底块或白色毛边,尤其是 2x2 sheet 背景被抠透明后,封闭镂空区域可能没有被通用边缘连通抠图清理掉。 - 原因:宝贝识物为了降低 image-2 成本,把物品 A、物品 B、篮子和礼物盒放在同一张
2x2sheet。通用背景透明处理主要从单格边缘连通背景开始,封闭在篮子手柄内部的近白区域不一定与边缘连通,因此会残留;如果把强力近白清理应用到物品格,又可能误伤白色物品主体。 - 处理:后端
slice_baby_object_match_sheet只在BabyObjectMatchSheetSlot::Basket编码前执行近白、低饱和 matte 清理;物品格和礼物盒格继续只走通用背景透明处理。sheet prompt 同步要求篮子手柄和篮口镂空处不要留下白底描边或毛边。运行态左右篮子的物品图标和名称 UI 以篮子中心线对齐,避免素材放大后看起来偏移。 - 验证:运行
cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml与npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx;真实联调需要重新生成宝贝识物资源,旧草稿中已保存的 base64 篮子图不会自动被新后处理改写。 - 关联:
server-rs/crates/api-server/src/edutainment_baby_object.rs、src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、src/index.css、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
宝贝识物物品框被长条素材拉伸先查固定槽位
- 现象:用户用手机、筷子等长条关键词生成素材后,中央物品 UI 或篮子上方物品图标看起来被拉成长框,圆形 UI 失去固定比例。
- 原因:运行态如果让图片固有宽高或外层自适应内容,就会把长条透明 PNG 的主体比例传导到 UI 容器。
- 处理:中央物品 UI 和篮子物品图标都必须使用固定正方形槽位,外层尺寸由 CSS 变量控制;生成素材图片只在槽位内
object-fit: contain等比缩放,不改变外层圆形 UI 框尺寸。 - 验证:用长条物品草稿进入宝贝识物运行态,中央物品框和篮子图标框仍为正圆,长条主体在框内缩小显示。
- 关联:
src/index.css、src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx、docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md。
寓教于乐作品和宝贝识物模板同时消失先查入口种子
- 现象:发现页“寓教于乐”分类下已发布的宝贝识物作品突然消失,同时创作界面模板选项中也看不到或无法正常展示
宝贝识物。 - 原因:创作入口配置事实源已迁到 SpacetimeDB
creation_entry_type_config;前端用baby-object-match入口可见性同时控制创作模板展示和发现页宝贝识物公开作品合入。若默认种子或后台配置缺少baby-object-match行,两条链路会一起被判定为不可见。 - 处理:确认
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs默认种子包含id=baby-object-match、title=宝贝识物、visible=true、open=true、sort_order=90;api-server 测试降级配置也要同步包含该类型。入口图片路径需指向真实存在资源,避免卡片图片 404。 - 验证:运行
cargo test -p module-runtime default_creation_entry_types_include_baby_object_match --manifest-path server-rs/Cargo.toml、cargo test -p api-server test_creation_entry_config_response_keeps_baby_object_match_visible --manifest-path server-rs/Cargo.toml、cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml和npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts。 - 关联:
server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs、server-rs/crates/api-server/src/creation_entry_config.rs、docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md。
儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置
- 现象:
/child-motion-demo已经呈现绘本草地风格,但public/child-motion-demo/picture-book-grass-stage.png、picture-book-grass-floor.png、picture-book-ground-ring.png、picture-book-character-outline.png、picture-book-ui-panel.png或picture-book-ui-button.png不存在,Network 里对应图片返回 404,或运行npm run assets:child-motion-demo -- --live返回缺少 VectorEngine 配置。 - 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine
gpt-image-2生成,脚本只读取VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY和可选VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。 - 处理:在本地私密环境补齐
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai与VECTOR_ENGINE_API_KEY,不要把 key 写入 Git;先运行npm run assets:child-motion-demo -- --dry-run核对 prompt,再运行npm run assets:child-motion-demo -- --live或npm run assets:child-motion-demo -- --live --only ui-panel等小批量命令生成资源。透明资源的品红底源图写入tmp/child-motion-demo-assets/,不要把源图或预览图放入public/child-motion-demo/作为正式资产。 - 验证:生成后确认
public/child-motion-demo/只保留页面引用的最终 PNG,重新打开/child-motion-demo可看到真实绘本草地背景、地面、圆环、角色轮廓和 UI 资源;npm run check:encoding仍通过。 - 关联:
scripts/generate-child-motion-demo-assets.mjs、src/index.css、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 绘本资源变形先查用途拆分和透明后处理
- 现象:
/child-motion-demo背景风格正确,但底部草坪被拉成厚色块、顶部 HUD 或右下状态条像方形面板被横向拉伸,或旧picture-book-ui-panel.png与新资源叠在一起。 - 原因:早期资源中
picture-book-ui-panel.png是接近方形画布,picture-book-grass-floor.png也含大量透明边界;若 CSS 用background-size: 100% 100%把同一资源强行铺成 HUD、状态条、开始面板或底部地板,就会出现变形和层叠观感。 - 处理:使用用途专属资源:
picture-book-foreground-grass-v2.png、picture-book-ground-ring-v3.png、picture-book-character-outline-v4.png、picture-book-hud-strip-v2.png、picture-book-calibration-strip-v2.png、picture-book-start-panel-v2.png、picture-book-ui-button-v2.png;CSS 按资源比例等比缩放,底部草坪只覆盖下沿,HUD / 状态条 / 开始托盘分别引用各自资源。角色指示器使用 v4 更细白色描边资源,内部透明且显示尺寸相对上一版放大 50%;若只需修透明裁切、品红边或纯描边后处理,运行npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>,不重新请求 image-2。 - 验证:用横屏截图检查没有新旧资源叠加、没有方形面板拉成长条、角色和地面指示环不被前景草坪埋住;同时运行
npm run check:encoding。 - 关联:
scripts/generate-child-motion-demo-assets.mjs、src/index.css、public/child-motion-demo/、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
儿童动作 Demo 猫咪挥手拆件错位先查动画父级和肩部挂点
- 现象:
/child-motion-demo打个招呼阶段的猫咪图和风格正确,但挥手时左右手臂像漂浮在身体旁边,视频里能看到肢体没有稳定接在肩膀上。 - 原因:猫咪身体和手臂如果分别做上下浮动,或手臂使用透明方形画布的默认中心/底部旋转轴,就会在摆动极值时放大肩点偏差;镜像左臂还需要把资源内部连接点换算到镜像后的坐标。
- 处理:
.child-motion-gesture-guide__wave-cat父级统一承接 bob 动画,身体层保持静态贴底且层级低于手臂;左右手臂作为同一父级下的兄弟层,只做旋转动画并显示在身体前方。身体使用去掉左右小圆点的picture-book-wave-cat-body-guide-v7.png;手臂 v7 资源当前按身体外缘摆放,圆猫爪掌面朝向玩家;左右侧距为12%,左臂使用原图层与60% 78%旋转轴,右臂使用镜像图层与40% 78%旋转轴,动画周期为0.47s,左右手臂不设置错峰延迟;不要把scaleX(...)和 rotate 放在同一个手臂 wrapper 上。 - 验证:用用户录屏关键帧或离线合成预览检查摆动两端的手臂根部仍贴住肩点;再运行儿童动作 Demo 定向组件测试、ESLint 和
npm run check:encoding。 - 关联:
src/index.css、public/child-motion-demo/picture-book-wave-cat-body-guide-v7.png、public/child-motion-demo/picture-book-wave-cat-arm-guide-v7.png、docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md。
GPT-image-2 不再读 APIMart 图片配置
- 现象:配置了
APIMART_BASE_URL/APIMART_API_KEY后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现official_fallback/image_urls。 - 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions
gpt-5.4-mini,APIMart 不再作为当前创意 Agent 来源。 - 处理:为图片生成配置
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai、VECTOR_ENGINE_API_KEY、VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS;排查请求体时确认无参考图路径为/v1/images/generations、有参考图路径为/v1/images/edits,模型为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.md、server-rs/crates/api-server/src/openai_image_generation.rs。
拼图参考图没有影响生成时先查 action payload 和阶段日志
- 现象:拼图上传参考图后生成出的画面明显不像参考图,或结果页重新生成没有按保存的参考图走图生图。
- 原因:首图生成只通过
compile_puzzle_draft.referenceImageSrc临时传 Data URL,不持久化到 SpacetimeDB;结果页重新生成则要把当前上传图或关卡pictureReference作为generate_puzzle_images.referenceImageSrc继续传给后端。 - 处理:浏览器 Network 里确认 action payload 带
referenceImageSrc;api-server 日志按同一session_id查看拼图参考图解析完成、拼图 VectorEngine 图片生成 HTTP 返回、拼图 VectorEngine 图片下载完成、拼图生成图片已写入 OSS 与资产索引,可定位慢在参考图读取、VectorEngine、下载或 OSS。 - 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的
pictureReference重新生成;后端单测覆盖 VectorEngine 请求体image字段。 - 关联:
src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle.rs。
拼图首图生成后要把入口参考图写回 pictureReference
- 现象:入口页上传图后,首图看着像没吃到参考图;结果页重新生成时默认只沿用关卡旧图,没有继续带入口上传图。
- 原因:首图生成请求虽然已经把
referenceImageSrc传给 VectorEngine,但如果后端只更新cover_image_src/selected_candidate_id而不回写首关pictureReference,结果页后续重绘就会丢失参考图。 - 处理:在
compile_puzzle_draft和generate_puzzle_images的成功与 SpacetimeDB 降级快照路径里,都把本次入口参考图写入首关pictureReference。 - 验证:后端单测覆盖
build_puzzle_levels_with_primary_update和apply_generated_puzzle_candidates_to_session_snapshot;结果页重新生成应在未重新上传时继续带入level.pictureReference。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、src/components/puzzle-result/PuzzleResultView.tsx。
拼图参考图不像时先看 edits multipart image
- 现象:Network payload 已带
referenceImageSrc,但 VectorEngine 生成结果仍明显不像上传图。 - 原因:参考图只在
aiRedraw = true时由后端解析并传给gpt-image-2/v1/images/edits的 multipartimagepart;若前端没传referenceImageSrc、后端解析失败或 prompt 缺少参考图强约束,生成会退化为纯文生图。 - 处理:
referenceImageSrc存在且aiRedraw = true时走 edits multipart,prompt 保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。 - 验证:后端单测应覆盖
/v1/images/edits路由、b64_json响应解码和参考图强提示;真实联调看日志里是否命中拼图 VectorEngine 图片编辑 HTTP 返回。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、src/services/puzzleReferenceImage.ts、docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md。
拼图 edits 报 error sending request 先看网络分类
- 现象:拼图有参考图时返回
拼图图片生成失败:创建拼图 VectorEngine 图片编辑任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/edits),后端没有拼图 VectorEngine 图片编辑 HTTP 返回日志。 - 原因:这是
reqwest在send()阶段失败,尚未收到 VectorEngine HTTP 响应;常见原因是服务器网络 / DNS / 防火墙 / 代理问题,或上游网关中断 multipart 连接。 - 处理:查看错误响应和
拼图 VectorEngine 图片编辑相关日志;若请求发送阶段失败,先查网络出口、DNS、防火墙、代理、参考图大小和VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS。 - 验证:
curl --http1.1 -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2" -F "prompt=test" -F "n=1" -F "size=1024x1024" -F "image=@public/match3d-background-references/pot-fused-reference.png;type=image/png"至少应返回 HTTP401,说明域名、TLS、路径和 multipart 上传可达;执行cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md。
拼图 UI 背景缺失先区分生成失败和消费链路丢字段
- 现象:拼图草稿生成完成后,素材配置页没有展示生成的 UI 背景,或结果页能看到背景但自动试玩 / 结果页“试玩”进入局内仍只显示封面模糊背景。
- 原因:
compile_puzzle_draft设计上会在首图后生成 UI 背景,且缺uiBackgroundImageSrc/uiBackgroundImageObjectKey会让自动草稿失败;若草稿已成功,通常不是“没生成”,而是前端消费链路漏了levels[].uiBackgroundImageObjectKey回退,或本地startLocalPuzzleRun(...)只把coverImageSrc带入currentLevel。 - 处理:结果页预览、运行态和本地运行态统一用
resolvePuzzleUiBackgroundSource,优先uiBackgroundImageSrc,为空时把uiBackgroundImageObjectKey规范成/generated-...路径并交给/api/assets/read-url换签;startLocalPuzzleRun与本地下一关 handoff 都要从PuzzleWorkSummary.levels[]复制uiBackgroundImageSrc/uiBackgroundImageObjectKey/backgroundMusic到currentLevel。结果页UI背景提示词输入框不得把本地兜底 prompt 直接显示成已保存提示词,避免误判为后端已生成。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/services/puzzle-runtime/puzzleLocalRuntime.test.ts src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx,以及npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial";后端用cargo test -p api-server puzzle_ui_background --manifest-path server-rs\Cargo.toml确认生成 / 序列化链路。 - 关联:
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts、src/services/puzzle-runtime/puzzleLocalRuntime.ts、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
拼图草稿生成后音乐/UI 又变空先查结果页回包合并
- 现象:拼图草稿生成完成后,音乐面板曲名有值但音频槽仍显示“暂无音乐”,UI 仍展示默认预览;试玩进入局内也没有生成音乐或 UI 背景。
- 原因:结果页若已有本地
generationStatus = generating编辑态,后端生成完成回包会走mergeDraftEditStateWithIncomingState(...)合并。该合并必须把生成候选图、正式图、uiBackground*和backgroundMusic作为同一批生成资产处理;漏掉backgroundMusic时,随后自动保存会把空音乐写回levels_json。 - 处理:
PuzzleResultView合并生成完成回包时同步保留backgroundMusic,并用回归测试覆盖 UI 预览、音乐试听和试玩 payload 都读取最新levels[]资产。 - 验证:
npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx,以及自动试玩入口测试npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial"。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md。
自动草稿成功但缺音乐或 UI 先查后端吞错
- 现象:拼图或抓大鹅生成页提示完成,但草稿页仍显示“暂无音乐”,拼图 UI 仍是默认预览,试玩局内也没有生成音乐或 UI 背景。
- 原因:自动草稿阶段如果把 VectorEngine / Suno / OSS / 资产绑定错误记录为 warning 后继续返回成功,前端只能拿到缺关键资产的成功 draft,随后保存和试玩都会消费这份空资产状态。
- 处理:自动草稿必须把必需生成资产当作后端完成条件:拼图首关需同时具备
levels[0].backgroundMusic.audioSrc和levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey;抓大鹅需在generatedItemAssets[]中具备非空backgroundMusic.audioSrc。缺失或上游失败时返回错误并停留在生成页,结果页手动重新生成只作为已有草稿补救入口。 - 验证:
cargo test -p api-server puzzle_initial_draft_assets_must_include_music_and_ui_background match3d_background_music_ready_requires_audio_src match3d_background_music_title_is_required_for_auto_draft --manifest-path server-rs\Cargo.toml,并重启npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/puzzle.rs、server-rs/crates/api-server/src/match3d.rs、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图草稿生成 180 秒后 502/504 先查 VectorEngine 超时与前端重试
- 现象:点击“生成拼图游戏草稿”后,
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions等待约 180 秒返回502 Bad Gateway或504 Gateway Timeout;钱包流水里同一 session 可能出现连续两组puzzle_initial_image扣费后退款。 - 原因:首图生成走 VectorEngine
gpt-image-2,默认VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000;若上游在该窗口内未返回,后端退款并返回超时错误。旧前端 action 写请求会对 502/503/504 自动重试一次,导致同一次点击重复触发生图与扣退费。 - 处理:拼图/创作 Agent 的
executeAction默认不做前端自动重试;后端将 VectorEngine / 图片请求超时映射为504 Gateway Timeout,error.details.provider=vector-engine且timeout=true。真实排障按日志同一session_id查拼图 VectorEngine 图片生成 HTTP 返回是否缺失,以及钱包流水扣费到退款的时间差是否接近VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS。 - 验证:运行
npm run test -- src/services/creation-agent/creationAgentClientFactory.test.ts src/services/apiClient.test.ts、cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml,真实联调重启npm run dev:api-server后检查/healthz。 - 关联:
src/services/creation-agent/creationAgentClientFactory.ts、server-rs/crates/api-server/src/puzzle.rs、docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md。
开局 CG 故事板生图失败先查 VectorEngine 请求预算和旧进程
- 现象:RPG 结果页点击开局 CG 后,
POST /api/runtime/custom-world/opening-cg在较长等待后返回“开局 CG 故事板生成失败:创建图片生成任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/generations)”。 - 原因:该故事板会把角色图和首幕背景图作为参考图一起传给 VectorEngine
gpt-image-2-all,请求体和上游生成耗时都比普通单图更大;若运行中的api-server仍沿用旧VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,或者参考图过大,会在请求发送/等待阶段被 reqwest 截断。日志里timeout=false connect=false request=true body=false source=client error (SendRequest)表示还没拿到上游 HTTP 响应,通常优先怀疑大 JSON 请求体、上游网关中断或 HTTP 协议兼容,而不是业务响应解析失败。直接请求 VectorEngine 若无效 token 可快速返回 401,不能据此判断真实生图不会超时。 - 处理:开局 CG 参考图入参先压到单边 768 的 JPEG;
/v1/images/generations保持 reqwest 默认 HTTP 协商,只有 multipart/v1/images/edits单独强制 HTTP/1.1。后端图片 helper 将timeout/connect/body/source/source_chain/source_chain_depth/endpoint分类写入日志和error.details,失败审计通过metadata_json.errorSource/requestId保留底层错误链和请求标识。修改.env.secrets.local后必须重启api-server,npm run dev终端用rs api-server,否则旧进程仍按旧超时运行。 - 验证:分别运行
cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml和cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml;真实联调重启后再触发开局 CG,若仍失败看返回的details.errorSource/source/timeout/connect/body/endpoint、tracking_event.metadata_json.errorSource/requestId和logs/api-server/同一 request_id。 - 关联:
server-rs/crates/api-server/src/custom_world_ai.rs、server-rs/crates/api-server/src/custom_world_ai/opening_cg.rs、server-rs/crates/api-server/src/openai_image_generation.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
开局 CG 成功后又变空白要保留 profile.openingCg
- 现象:RPG 结果页里的开局 CG 成功显示一瞬后,窗口又退回空白占位。
- 原因:
openingCg只存在于结果页 profile 槽位,如果父层在onProfileChange后重新同步了 profile,却经过normalizeCustomWorldProfileRecord或作品库写回时丢掉openingCg,预览就会从视频 / 故事板回退为空白。 - 处理:
src/data/customWorldLibrary.ts的 profile 归一化必须透传openingCg;结果页和父层后续同步都应把它当作受控资产槽位,而不是临时 UI 状态。 - 验证:
npm run test -- src/data/customWorldLibrary.test.ts src/components/CustomWorldResultView.test.tsx,确认生成后即使父层做一次归一化回写,开局 CG 仍继续显示。 - 关联:
src/data/customWorldLibrary.ts、src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx、src/components/CustomWorldEntityCatalog.tsx。
RPG 发布报 legacy_result_profile_json 非法先查 null 兼容
- 现象:RPG 结果页发布动作返回
UPSTREAM_ERROR,SpacetimeDB details 里是custom_world.compile.legacy_result_profile_json 不是合法 JSON object。 - 原因:
publish_world前端契约只要求{ action: 'publish_world' };ExecuteCustomWorldAgentActionRequest.legacy_result_profile是可选字段,经 HTTP / serde / SpacetimeDB payload 传递时可能显式成为 JSONnull。旧的编译器只接受 object 或缺省,把Some("null")当成非法 legacy JSON。 - 处理:
module-custom-world的 optional JSON object 解析要把null视为未提供,仍拒绝数组、字符串、数字和坏 JSON;正式发布继续以 sessiondraft_profile_json为草稿真相。 - 验证:
cargo test -p module-custom-world published_profile_compile --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/module-custom-world/src/application.rs、server-rs/crates/spacetime-module/src/custom_world.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时
- 现象:用 Node
fetch直接请求POST /v1/images/generations,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛AbortError、TypeError: fetch failed或UND_ERR_HEADERS_TIMEOUT;同一 prompt 改用原生https.request可以在较短时间内成功返回图片。 - 原因:Node/Undici 的默认 headers timeout 可能早于业务脚本期望的长生图等待窗口触发,表现上容易被误判成 VectorEngine 上游本身超时。
- 处理:长期脚本优先复用后端 reqwest 或项目已有生成脚本;临时本地工具若必须用 Node,可改用原生
http/https.request并显式设置 socket timeout,或为 Undici 单独配置 headers timeout。仍需隐藏VECTOR_ENGINE_API_KEY,只报告配置是否存在。 - 验证:同一
gpt-image-2请求体、同一环境变量下,原生 HTTP 请求能返回url/b64_json并落盘;失败时错误里能区分请求发送、首部等待、下载和解码阶段。 - 关联:
.codex/skills/gpt-image-2-apimart/SKILL.md、server-rs/crates/api-server/src/openai_image_generation.rs。
旧后端路线文档造成判断漂移
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
- 原因:项目历史文档较多,部分旧方案仍保留作迁移参考。
- 处理:涉及服务端、数据真相、SpacetimeDB、运行时状态时,先看
CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md,再看 DDD 总纲和具体技术方案。 - 验证:代码改动应落在
server-rs + Axum + SpacetimeDB主线;旧路线只作为迁移参考,不作为兼容目标。 - 关联:
docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md、AGENTS.md。
SpacetimeDB 表结构变更不能按 PostgreSQL 迁移直觉处理
- 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。
- 原因:SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。
- 处理:变更前阅读
SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md;已有表新增字段必须放在 Rust 表结构体最后并设置明确默认值;需要修改字段名时,先询问用户并确认迁移计划;涉及表变化时同步migration.rs、SPACETIMEDB_TABLE_CATALOG.md和 bindings;必要时走 JSON 导入导出与分片导入迁移流程。 - 验证:发布前运行
npm run check:spacetime-schema,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。 - 关联:
docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md、docs/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.rs、server-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-contractsfeature,workspace 根依赖保持default-features = false;api-server这类原生后端需要资产 DTO 时在自身Cargo.toml显式启用features = ["oss-contracts"]。 - 验证:执行
cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown应显示 nothing to print;再执行cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown。 - 关联:
server-rs/crates/shared-contracts/Cargo.toml、server-rs/crates/api-server/Cargo.toml、docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md。
本地 SpacetimeDB replica identity 不匹配
- 现象:本地 standalone 启动时报
mismatched database identity。 - 原因:本地 SpacetimeDB 数据目录中的 replica 数据残留与当前数据库身份不一致。
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
- 关联:
docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md。
本地 SpacetimeDB publish 403 优先查 CLI 身份和目标库
- 现象:
spacetime publish在Pre-publish check阶段返回403 Forbidden,提示当前 identity 无权对目标 database identity 执行update database。 - 原因:当前 CLI 登录态不是目标数据库的创建者或授权身份,或
.env.local/ publish 命令指向了另一个数据库或 SpacetimeDB 服务。 - 处理:除 CI/CD 脚本内部受控用法外,不再使用
spacetime --root-dir排障或发布。先执行spacetime login show、spacetime server list,再用spacetime list --server http://127.0.0.1:3101或实际--server-url确认当前身份是否能看到目标库;本地开发发布优先使用npm run dev:spacetime或从server-rs目录执行显式--server的spacetime publish。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。 - 验证:
spacetime list --server http://127.0.0.1:3101能看到目标库;重新发布不再使用无权限 identity。 - 关联:
scripts/dev.mjs、docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md。
npm run dev 本地 SpacetimeDB 401 / 403 可重置默认 local 身份
- 现象:
npm run dev启动本地开发栈时,SpacetimeDB 在登录、发布或预检查阶段返回401/403,清理后仍像在使用旧 token 或旧本地库。 - 原因:本机
spacetimeCLI 保存的旧 token、默认 server、正在运行的 standalone 进程或默认 local 数据库与当前发布身份不一致。 - 处理:确认只是本地测试库且数据可丢弃后,先查看并停止本地
spacetimedb-standalone,执行spacetime logout,确认并设置spacetime server set-default local,停 server 后用spacetime server clear -y清空默认本地库,再spacetime start,另开终端执行spacetime login --server-issued-login local,最后用spacetime publish --server local A或项目脚本重新发布。 - 验证:
spacetime server list默认目标为 local;重新登录后发布不再返回401/403;npm run dev可以完成 SpacetimeDB publish 并继续启动api-server。 - 关联:
docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md、scripts/dev.mjs。
本地 SpacetimeDB 联调可按阶段跳过宿主或发布
- 现象:本地
npm run dev因3101已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查spacetime-module语法而被完整联调链路拖慢。 - 原因:
npm run dev默认同时启动 SpacetimeDB standalone、发布server-rs/crates/spacetime-module、启动 Rustapi-server、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。 - 处理:
npm run dev启动后会把实际 SpacetimeDB 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就是后端要连接的实际端口,避免被误改到空闲但未启动的3102。api-server、主站 Vite 和后台 Vite 启动前也会解析可用端口。spacetime-module改动后只重新 publish,不重启 standalone 宿主;未修改spacetime-module时使用npm run dev -- --skip-publish;只查模块语法时执行cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml。npm run dev会在启动前检查 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,不可用时自动寻找后续可用端口,并把实际端口传给 publish、后端环境变量和前端代理目标。 - 验证:
--skip-spacetime后脚本复用现有http://127.0.0.1:3101;日志中的[dev] spacetime:不应漂移到没有服务的3102;GET /api/creation-entry/config不应返回连接空端口导致的502。3101或8082被其他进程占用时,脚本使用最近可用端口;--skip-publish后不再进入 publish 阶段;cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml能完成 Rust 语法和类型检查。端口漂移时控制台会打印[dev:ports] ... 不可用,改用 ...,后续[dev] web/admin web/api-server/spacetime地址应与实际端口一致。spacetime-module变更后只应看到重新发布日志,不应看到 standalone 重启日志。 - 关联:
docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md、scripts/dev.mjs。
npm run dev -- --watch 前端无限重启先查外层 watcher
- 现象:开启
npm run dev -- --watch后,后台 Vite 或主站 Vite 反复退出重启,即使没有手动修改源码。 - 原因:Vite 本身会监听源码并写入
node_modules/.vite等缓存;外层调度器如果再递归监听前端目录并重启 dev server,就可能把 Vite 自己的缓存写入当成源码变化,形成循环重启。 - 处理:外层 watcher 只负责后端侧:
spacetime-module改动后重新 publish,api-server改动后重启 Rust 进程。主站 Vite 和后台 Vite 的源码变化交给 Vite HMR;需要进程级重启时在npm run dev终端手动输入rs web或rs admin-web。 - 验证:
npm run dev -- --watch下修改apps/admin-web/src/**应由 Vite HMR 处理,不应出现连续[dev] 重启 admin-web;scripts/dev.test.ts覆盖 web/admin-web 不注册外层 watch。 - 关联:
scripts/dev.mjs、docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md。
根目录 nohup.out 持续写入会触发主站 Vite 刷新循环
- 现象:在仓库根目录用
nohup npm run dev ... &启动完整 dev 栈后,即使没有修改前端源码,主站页面也会反复整页刷新;nohup.out同时持续增长。 - 原因:未显式重定向 stdout / stderr 时,
nohup.out会收集 SpacetimeDB、api-server 和两套 Vite 的整套 dev 栈输出。主站 Vite 的 root 是仓库根目录,若 watcher 未忽略这个持续写入的文件,每次追加日志都会被当成文件变化;后台 Vite root 是apps/admin-web,仓库根日志不在其监听根内。 - 处理:主站
vite.config.ts的server.watch.ignored保持忽略**/nohup.out,Git 同时忽略nohup.out。修改配置后重启主站 Vite。若显式重定向到其它仓库内日志文件,该文件不会自动受保护,应写到 Vite root 之外或补充精确忽略规则。 - 验证:在仓库根目录追加
nohup.out时主站不再刷新,真实源码修改仍正常触发 HMR;git check-ignore nohup.out能命中忽略规则,git status不出现该日志。 - 关联:
vite.config.ts、.gitignore、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地 SpacetimeDB publish 401 可清本地库重发
- 现象:本地
spacetime publish显示401无权限,或重新发布仍像是在更新旧库。 - 原因:本地开发数据目录中保留的数据库、控制库身份或发布身份与当前目标不一致。
- 处理:确认本地开发数据可以丢弃后,停止本地 SpacetimeDB,备份或删除
server-rs/.spacetimedb/local/data,再重新运行npm run dev或本地 publish;不要用--root-dir手工清库。 - 验证:重新发布日志应显示创建新的数据库,而不是更新旧数据库;若仍显示更新或继续
401,继续检查数据目录、库名和 CLI 身份。 - 关联:
docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md、docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md。
SpacetimeDB 模块 publish 报 wasm-bindgen detected
- 现象:
spacetime publish已经完成 Rust 编译,但随后报wasm-bindgen detected,提示依赖树里有面向 Web 平台的 wasm-bindgen。 - 原因:SpacetimeDB 模块是数据库内 WASM,不允许拉入 Web/HTTP client 链路;常见误因是
spacetime-module -> module-* -> shared-contracts -> platform-* -> reqwest -> wasm-bindgen这类反向依赖。 - 处理:执行
cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown找到链路;把平台实现类型从shared-contracts或module-*中移除,只保留公开 DTO,平台响应到 DTO 的转换放回api-server等 adapter 层。 - 验证:上述
cargo tree输出warning: nothing to print;cargo check -p shared-contracts、cargo check -p api-server通过;重新spacetime publish ... --module-path server-rs/crates/spacetime-module不再报 wasm-bindgen。 - 关联:
docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md、server-rs/crates/shared-contracts/src/assets.rs、server-rs/crates/api-server/src/assets.rs。
Vite SPA fallback 吞掉 API 请求
- 现象:本地请求
/api/profile/*等接口时返回 HTML,被前端当 JSON 解析报错。 - 原因:Vite 代理缺少对应
/api/*前缀,API 请求落到 SPA fallback。 - 处理:补齐 Vite 代理,让 API 请求转发到 Rust
api-server。 - 验证:请求返回 JSON,相关页面不再出现 HTML parse 错误。
- 关联:
docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md。
npm run build 因 Vite warning 被 build-gate 判失败
- 现象:主站或后台 Vite 已经输出
built in ...,但根命令最后仍失败并打印Build gate failed because warnings were emitted。 - 原因:
scripts/build-gate.mjs会收集 stdout / stderr 中的 warning 行并作为硬失败;常见触发是产物 chunk 超过vite.config.ts或apps/admin-web/vite.config.ts的chunkSizeWarningLimit。 - 处理:先看 warning 原文确认来源。若是合理的入口级 chunk 体积增长,调整对应 Vite 配置阈值或做真实拆包;不要把这类失败按 Rust / SpacetimeDB 编译错误排查。
- 验证:重新执行
npm run build,主站与后台均构建完成且没有 build-gate warning 汇总。 - 关联:
scripts/build-gate.mjs、vite.config.ts、apps/admin-web/vite.config.ts。
反馈页清空 file input 前必须先拷贝 FileList
- 现象:点击上传凭证会打开文件选择框,但选择图片后页面没有展示预览,提交时也没有携带图片凭证。
- 原因:浏览器传入的
FileList可能跟<input type="file">保持 live 绑定;如果先执行input.value = '',再从参数里的FileList读取文件,列表可能已经为空。 - 处理:在清空 file input 前先执行
const selectedFiles = files ? Array.from(files) : [],后续图片类型、大小、Data URL 读取和预览都基于这个普通数组。 - 验证:
PlatformFeedbackView.test.tsx用 mockFileReader断言选择图片后出现反馈凭证预览,且提交 payload 带evidenceItems[].dataUrl。 - 关联:
src/components/platform-entry/PlatformFeedbackView.tsx、docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md。
拼图 VectorEngine 图片生成密钥不能复用 DashScope / ARK key
- 现象:拼图新手引导或拼图创作点击生成后返回
VectorEngine 图片生成密钥未配置。 - 原因:拼图
gpt-image-2/ 历史nanobanana2图片生成已统一走 VectorEngine;后端只读取VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY、VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,不会用DASHSCOPE_API_KEY、LLM_API_KEY、ARK_API_KEY或APIMART_API_KEY兜底。 - 处理:在本机私密配置
.env.secrets.local或进程环境中配置真实VECTOR_ENGINE_API_KEY,不要提交到 Git;填入后必须重启api-server/npm run dev,运行中的进程不会自动加载新 env。 - 验证:不打印密钥内容,只检查
VECTOR_ENGINE_API_KEY非空;重启后触发拼图生成不再返回本地配置缺失的 503。 - 关联:
docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md、.codex/skills/gpt-image-2-apimart/SKILL.md。
npm run dev:api-server 读取 env 的顺序必须让 .env.secrets.local 最后覆盖
- 现象:
POST /api/assets/hyper3d/text-to-model在本地返回 503,详情里提示HYPER3D_API_KEY 未配置,但开发者明明已经在本地私密文件里写了 key。 - 原因:
scripts/dev-utils.mjs之前按.env.secrets.local → .env.local → .env合并,结果仓库里的.env空示例值会把前面已经设置好的私密 key 覆盖掉。 - 处理:
npm run dev:api-server/npm run dev:spacetime/npm run dev统一按“外层 shell 变量优先,其后.env、.env.local、.env.secrets.local逐层覆盖”的顺序加载;真实密钥优先放.env.secrets.local。本地认证开关例外:SMS_AUTH_ENABLED、SMS_AUTH_PROVIDER等以本地 env 文件为准,避免父进程继承的旧开关值长期压过.env.local。 - 验证:本地加入临时测试后,
HYPER3D_API_KEY应能被.env.secrets.local覆盖,真实密钥 shell 变量仍然最高优先级;mergeApiServerEnv(..., { SMS_AUTH_ENABLED: "false" })在.env.local写SMS_AUTH_ENABLED=true时应返回 true。 - 关联:
scripts/dev-utils.mjs、server-rs/crates/api-server/src/hyper3d_generation.rs、docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md。
OSS 密钥键名不要把字母 O 写成数字 0
- 现象:
.env.secrets.local看起来已经配置 OSS AccessKey Secret,但拼图或抓大鹅生成仍返回OSS 未完成环境变量配置。 - 原因:后端只读取
ALIYUN_OSS_ACCESS_KEY_SECRET。如果写成ALIYUN_0SS_ACCESS_KEY_SECRET,中间是数字0,配置合并检查会显示正确键缺失,api-server不会初始化 OSS 客户端。另一个常见原因是外层 shell / IDE 预置了空的ALIYUN_OSS_*,旧启动脚本会把空值当作最高优先级,导致.env.local或.env.secrets.local的真实值被跳过。 - 处理:只改键名为
ALIYUN_OSS_ACCESS_KEY_SECRET,保留原值;不要在日志、文档或对话里输出密钥内容。本地启动脚本应只保护非空外层环境变量,空字符串或全空白值不得遮蔽本地 env 文件。 - 验证:运行
npm run check:api-server-env,确认VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY、ALIYUN_OSS_BUCKET、ALIYUN_OSS_ENDPOINT、ALIYUN_OSS_ACCESS_KEY_ID、ALIYUN_OSS_ACCESS_KEY_SECRET都是present,再重启npm run dev:api-server或npm run dev。
拼图图片生成 98% 后报 OSS V4 签名时间格式化失败
- 现象:拼图创作表单生成进度卡在 98%,
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions返回502 Bad Gateway,前端提示拼图图片生成失败:OSS V4 签名时间格式化失败。 - 原因:
platform-oss曾用OffsetDateTime::time().to_string()拼接x-oss-date,UTC 小时、分钟或秒为个位数时可能缺少前导零,导致 V4 签名时间不是固定YYYYMMDDTHHMMSSZ。 - 处理:OSS V4 签名日期统一显式补零格式化;签名 scope 用
YYYYMMDD,完整签名时间用YYYYMMDDTHHMMSSZ,不要再依赖time().to_string()。 - 验证:运行
cargo test -p platform-oss和cargo check -p api-server;重启npm run dev:api-server后检查/healthz,再重新触发拼图生成。 - 关联:
server-rs/crates/platform-oss/src/lib.rs、server-rs/crates/api-server/src/assets.rs、docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md。
拼图生成完成后图片只显示破图或 alt 文案
- 现象:拼图结果页生成完成后,“画面图”区域出现破图图标和作品名,图片无法正常预览;但打开历史拼图素材时同一张图可能可以正常预览。
- 原因:拼图正式图保存为
/generated-puzzle-assets/*兼容标识,旧/generated-*直读代理已删除;如果前端没有通过ResolvedAssetImage//api/assets/read-url换签,或收到无前导斜杠的generated-puzzle-assets/*object key 后未识别为 generated 私有资源,浏览器会直接请求裸路径并失败。生成完成后的结果图还会传入refreshKey,它只能作为 signed URL 缓存版本号,不能给 OSS V4 签名 URL 追加_v;OSS 会把 query 纳入签名,额外参数会让签名失效。 - 处理:拼图结果页、发布预览、运行态和历史素材预览都走
ResolvedAssetImage或useResolvedAssetReadUrl;generated 私有资源识别必须同时覆盖/generated-*、generated-*和https://*.oss-*.aliyuncs.com/generated-*;refreshKey变化时重新换签,同一路径同一refreshKey且签名未临近过期时复用已返回的 OSS 签名 URL;禁止恢复/generated-puzzle-assets直读代理。 - 验证:运行
npm run test -- src\services\assetReadUrlService.test.ts src\hooks\useResolvedAssetReadUrl.test.tsx src\components\puzzle-result\PuzzleResultView.test.tsx,再触发一次真实生成确认 Network 中先请求/api/assets/read-url,图片src为未追加_v的签名 URL。 - 关联:
src/services/assetReadUrlService.ts、src/components/ResolvedAssetImage.tsx、docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md。
拼图图片生成失败后不要停在 ImageRefining
- 现象:拼图图片生成失败后,会话仍停留在
PuzzleAgentStage::ImageRefining,用户从作品架或生成页恢复时容易被当成生成中/精修中状态,重试入口和失败承接不清晰。 - 原因:
mark_puzzle_draft_generation_failed_tx只把PuzzleResultDraft.generation_status标成failed,但 session stage 仍沿用旧的row.stage;如果失败前已进入ImageRefining,失败回写不会把会话带回结果草稿态。 - 处理:失败回写后按失败草稿重新解析 session stage:已发布保持
Published,仍满足发布门禁则为ReadyToPublish,否则回到DraftReady;前端生成页文案用“拼图图片生成进度 / 重新生成图片”,避免把失败态误导成还在生成整份草稿。 - 验证:运行
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml、npm run check:encoding,以及拼图生成页恢复相关RpgEntryFlowShell.agent.interaction.test.tsx定向用例。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
本地短信登录页签突然消失
- 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但
LoginScreen中手机号验证码表单仍存在。 - 原因:历史实现曾根据
GET /api/auth/login-options返回的availableLoginMethods渲染页签;接口返回空、失败或只返回["password"]时,AuthGate会降级成只显示密码。- 本地启动脚本没有让
.env.local覆盖.env,SMS_AUTH_ENABLED=true不生效,后端只返回["password"]。 - Rust API 直连已返回
["phone","password"],但 Vite 代理目标指向未监听端口,导致 3000 域名下的login-options返回500,AuthGate降级成["password"]。 - 3000 端口被旧
dev:web占用后,新的完整栈 Vite 自动漂移到 3001/3002;浏览器仍打开旧 3000 页面,旧页面继续代理到已经下线的端口。 - 生成页 UI 改动看起来“完全没变化”时,也要先确认当前浏览器打开的 Vite 进程正在返回最新源码;例如直接请求
http://127.0.0.1:3000/src/components/CustomWorldGenerationView.tsx检查是否包含本次新增类名或关键字。 - 单独
npm run dev:web启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
- 本地启动脚本没有让
- 处理:当前口径是登录弹窗永远展示
短信登录与密码登录两个核心入口;login-options只补充微信等环境相关入口,不能隐藏短信或密码页签。如果“获取验证码”点击后失败,再按短信 provider / API 代理问题排查:优先用npm run dev:api-server、npm run dev:spacetime或npm run dev启动,确认.env.local覆盖.env、RUST_SERVER_TARGET没有指向旧端口,并分别请求 3000 域名和 Rust API 目标。 - 验证:即使
/api/auth/login-options返回空、失败或只返回["password"],登录弹窗也应同时显示短信登录、密码登录、验证码输入和“获取验证码”按钮;短信发送真实可用性再通过POST /api/auth/phone/send-code验证。 - 关联:
src/components/auth/AuthGate.tsx、src/components/auth/LoginScreen.tsx、src/components/auth/AuthGate.test.tsx、scripts/dev-utils.mjs、scripts/dev.mjs。
本地短信收不到验证码先查 provider
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
- 原因:本地
.env.local里如果是SMS_AUTH_PROVIDER="mock",后端不会发真实短信,只会返回固定 mock 验证码;真实阿里云链路已经改为普通短信SendSms,验证码由当前api-server进程本地生成、哈希存储和校验,旧SendSmsVerifyCode/CheckSmsVerifyCode托管验证码参数不再参与真实校验。若接口直接返回“手机号登录暂未启用”,说明当前运行中的api-server进程内sms_auth_enabled=false:常见原因是修改.env.local后没有重启后端,或外层 shell 已经设置了非空SMS_AUTH_ENABLED导致 dotenv 不覆盖。历史上 cmd 里set SMS_AUTH_ENABLED="true"会把引号也传进进程,Rust bool 解析失败后保持默认 false。 - 处理:真实短信联调时把
.env.local的SMS_AUTH_ENABLED=true、SMS_AUTH_PROVIDER=aliyun显式打开,并确认ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com、ALIYUN_SMS_SIGN_NAME=北京亓盒网络科技、ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486、ALIYUN_SMS_TEMPLATE_PARAM_KEY=code后重启api-server;如果只想验证 UI 和账号链路,则保留mock并使用SMS_AUTH_MOCK_VERIFY_CODE。Shell 临时覆盖时 PowerShell 用$env:SMS_AUTH_ENABLED="true",cmd 用set SMS_AUTH_ENABLED=true,不要把引号作为值的一部分。api-server重启会清掉未校验的本地验证码。 - 验证:分别请求浏览器域名和 Rust API 直连的
/api/auth/login-options,都应返回["phone","password"];api-server日志里provider=aliyun才说明真实短信链路已生效。需要直接确认平台层真实调用阿里云时,配置ALIYUN_SMS_ACCESS_KEY_ID、ALIYUN_SMS_ACCESS_KEY_SECRET和ALIYUN_SMS_REAL_TEST_PHONE_NUMBER后手动执行cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms_real_provider_sends_verify_code -- --ignored --nocapture。 - 关联:
server-rs/crates/api-server/src/config.rs、scripts/dev-utils.mjs、docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md、docs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md。
手机验证码登录 500 先查短信 provider 语义
- 现象:登录弹窗手机号验证码登录失败,浏览器看到
POST /api/auth/phone/login 500,后端日志里同时出现阿里云短信UNKNOWN、biz.FREQUENCY或check frequency failed。 - 原因:真实短信 provider 的配置错误或上游失败曾被
module-auth折叠成PhoneAuthError::Store,HTTP 层只能按内部错误返回500,掩盖了 provider 失败。当前验证码校验已经改成本地哈希校验,登录阶段的验证码错误不会再调用阿里云校验接口;若登录前的发送阶段失败,应优先看SendSms返回的Code/Message。 - 处理:保留 provider 错误语义,配置错误映射
503 Service Unavailable,上游短信失败映射502 Bad Gateway;本地只验证 UI/账号链路时可用 shell 临时覆盖SMS_AUTH_PROVIDER=mock后启动npm run dev:api-server。 - 验证:
cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml,真实 provider 频控时接口不再返回500。 - 关联:
server-rs/crates/module-auth/src/errors.rs、server-rs/crates/api-server/src/phone_auth.rs、docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md。
本地短信 smoke 先确认 SMS provider
- 现象:浏览器里短信验证码发送成功,但提交
123456仍然报验证码错误,或者短信登录后又回到未登录态。 - 原因:当前运行中的
api-server如果读取到.env.local里的SMS_AUTH_PROVIDER=aliyun,就会走真实短信 provider 口径;这时 mock 验证码123456不会被接受。之前本地调试时常见的误判是把.env.local改成 mock 了,但没有重启npm run dev,或者旧的scripts/dev.mjs进程还在沿用旧环境。 - 处理:本地只做 UI / 账号链路 smoke 时,把
.env.local显式设为SMS_AUTH_PROVIDER=mock且配置SMS_AUTH_MOCK_VERIFY_CODE=123456,然后重启npm run dev或npm run dev:api-server。要做真实短信联调时,再切回SMS_AUTH_PROVIDER=aliyun并重启。 - 验证:
POST /api/auth/phone/send-code应返回providerRequestId=mock-request-id;POST /api/auth/phone/login用123456应返回200且user.loginMethod=phone。浏览器侧短信登录成功后,会先进入邀请码弹窗或我的页面,不应再提示“验证码错误”。 - 关联:
scripts/dev-utils.mjs、scripts/dev-utils.test.ts、scripts/dev.mjs、server-rs/crates/api-server/src/config.rs。
手机验证码登录成功后又瞬间回到未登录
- 现象:手机号验证码登录先成功,随后 UI 又闪回“未登录”,登录弹窗可能重新出现。
- 原因:
AuthGate首次 hydrate 会异步轮换 refresh cookie 并请求/api/auth/me。如果用户在 hydrate 完成前已经登录,晚到的旧 hydrate 仍可能把刚写入的user覆盖成null。 - 处理:给
AuthGate的 hydrate 增加版本号保护;登录成功、退出登录和全局 auth 事件都会推进版本号,旧 hydrate 结果到达后直接丢弃。 - 验证:
npm run test -- src/components/auth/AuthGate.test.tsx,新增用例应覆盖“旧 guest hydrate 不覆盖新登录态”。 - 关联:
src/components/auth/AuthGate.tsx、src/components/auth/AuthGate.test.tsx、docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md。
刷新网页后登录态失效
- 现象:刷新网页后,用户明明有本地 access token,却回到未登录状态。
- 原因:
AuthGatehydrate 曾先强制调用refreshStoredAccessToken();当 refresh cookie 临时失效、代理错配或后端返回401时,该方法会先清空本地 access token,随后/api/auth/me只能恢复成未登录。 - 处理:
refreshStoredAccessToken()增加clearOnFailure选项;AuthGate在已有本地 access token 时先用/api/auth/me确认用户,确认成功后再后台 refresh 续期与写每日登录埋点,后台 refresh 失败不清 token。 - 追加处理:
/api/auth/refresh只有明确返回401/403时才代表登录态权威失效,可以清本地 access token 并触发全局 auth 变化;服务器重启、Nginx 502/503/504、浏览器Failed to fetch或 refresh 响应契约异常都属于暂时不可用,不能把已有本地 token 清掉,否则重启窗口会把所有打开页面踢成未登录。 - 契约:
/api/auth/refresh成功响应按共享契约RefreshSessionResponse { token }解析;测试 mock 不要额外塞{ ok: true, token }遮住真实恢复路径。 - 验证:
npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx -t "explicit refresh opts out|auth gate keeps a valid local token login"。 - 关联:
src/services/apiClient.ts、src/components/auth/AuthGate.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
登录后推荐页加载出作品又回到未登录
- 现象:前端登录成功后进入推荐页,推荐页自动加载出一个作品,随后瞬间回到未登录;停留在其他页面或推荐页没加载出作品时不复现。
- 原因:推荐页 embedded 运行态会自动发起受保护写请求。若这些卡片级后台请求遇到
401或 refresh 失败,默认请求层曾清空 access token 并广播全局 auth 事件,导致AuthGate重新 hydrate 成未登录态。更隐蔽的是,refreshAccessToken()自身曾在 refresh 失败时静默清 token,即便调用方关闭了clearAuthOnUnauthorized,也可能让后续 hydrate 变成未登录。 - 处理:请求层统一使用
authImpact: 'global' | 'local'区分账号权威请求与局部后台请求;推荐页自动运行态、图片换签、公开拼图运行态和平台 bootstrap 私有投影刷新统一使用BACKGROUND_AUTH_REQUEST_OPTIONS/RUNTIME_BACKGROUND_AUTH_OPTIONS,并等canReadProtectedData为 true 后再启动;用户主动点击的账号动作仍保留默认全局鉴权失败处理。 - 追加处理:推荐页嵌入运行态要按真实身份分流,已登录或已有 access token 时继续走账号 Bearer + local auth impact,不能误带 runtime guest token;只有匿名访客才申请并透传 runtime guest token。
- 追加处理:generated 私有图片换签
/api/assets/read-url也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。 - 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,
startPuzzleRun、通关自动submitPuzzleLeaderboard、下一关advancePuzzleNextLevel和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。 - 追加处理:通关后
refreshSaveArchives()、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。 - 追加处理:未登录推荐页启动任一公开正式玩法时,
/api/runtime/*局内路由必须使用RuntimePrincipal,前端通过PlatformEntryFlowShellImpl的统一 request options helper 给 start / checkpoint / finish / input / drop / click / restart / time-up / leaderboard / next-level 等动作透传 runtime guest token;公开 runtime detail 读取如跳一跳、敲木鱼必须显式skipAuth/skipRefresh,匿名推荐流不能补读受保护创作详情,否则会在真正开局前打出/api/auth/refresh 401。 - 验证:
npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"。 - 关联:
src/services/apiClient.ts、src/services/assetReadUrlService.ts、src/services/puzzle-runtime/puzzleRuntimeClient.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md。
推荐页作品卡一直显示加载中
- 现象:推荐页有公开作品,但主视口一直停在“加载中...”,没有进入作品,也没有显示可操作错误。
- 原因:推荐页自动启动嵌入运行态时先设置
activeRecommendEntryKey/activeRecommendRuntimeKind/isStartingRecommendEntry,但失败或并发切换时外层缺少稳定错误态和请求版本保护,旧启动请求可能晚到覆盖新状态。 - 处理:
selectRecommendRuntimeEntry使用启动请求版本号丢弃旧请求;启动失败统一设置activeRecommendRuntimeError = "作品暂时无法进入,请稍后再试。"并关闭isStartingRecommendEntry。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation surfaces start failure"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
推荐页未登录入口误打开公开详情
- 现象:新用户默认在发现页,但点击推荐页或推荐封面后,如果复用公开作品详情入口,可能绕过推荐页沉浸运行态,打开普通公开详情页。
- 原因:
RpgEntryHomeView曾只有onOpenGalleryDetail一个回调,同时服务发现页公开详情和推荐页作品入口;一旦为发现页保留公开浏览能力,推荐页也会跟着打开详情。 - 处理:公开详情与推荐页入口分离为
onOpenGalleryDetail和onOpenRecommendGalleryDetail。发现页、搜索和排行榜保留公开详情;推荐 Tab、推荐封面、推荐运行态错误重试和桌面推荐模块走推荐运行态入口,不再主动弹登录窗。登录门禁只保留给创作、个人作品、删除、发布、Remix 等账号或所有权动作。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md。
Rust 冷编译导致 api-server 健康检查误超时
- 现象:旧
npm run dev:rust在 Windows 冷编译/链接阶段误判/healthz等待超时并杀掉cargo run;现入口为npm run dev或npm run dev:api-server。 - 原因:脚本把 SpacetimeDB 与 api-server 等待窗口混在一起,未考虑 Rust 冷编译耗时。
- 处理:按冷编译超时修复文档拆分等待窗口。
- 验证:冷启动时不再误杀仍在编译的 api-server。
- 关联:
docs/technical/API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md。
Windows debug api-server 主线程栈溢出
- 现象:
cargo check -p api-server和build_router测试通过,但npm run dev:api-server在 Windows debug 启动时thread 'main' has overflowed its stack。 - 原因:
api-serverAxum 路由树已经很深,debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。 - 处理:入口
main用显式 16MB 栈线程启动 Tokio runtime,并把实际服务逻辑放入run_server();新增路由时优先用小 router.merge(),避免继续拉长主链。 - 验证:
npm run dev:api-server后/healthz返回 200,相关路由冒烟通过。 - 关联:
server-rs/crates/api-server/src/main.rs、server-rs/crates/api-server/src/app.rs。
Windows debug api-server.exe 锁文件与强杀退出码容易混淆
- 现象:
cargo run -p api-server或npm run dev:api-server报failed to remove file ... target\debug\api-server.exe;清理旧进程后,旧终端可能继续打印process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)。 - 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条
npm run dev:api-server链路仍在运行,进程树为npm run dev:api-server -> node scripts/dev.mjs api-server -> cargo run -> api-server.exe。0xffffffff常见于排障时用Stop-Process -Force强制结束旧api-server.exe后由 Cargo 回显,不一定代表新启动失败。 - 处理:先按目标路径确认并停止本仓库的旧
api-server.exe及其父级cargo/node/cmd启动链路,再重新启动;不要同时开多个npm run dev:api-server。 - 验证:确认没有匹配
C:\Genarrative\server-rs\target\debug\api-server.exe的进程后,Remove-Item能删除旧 exe;随后npm run dev:api-server启动并访问/healthz返回 200。 - 关联:
scripts/dev.mjs、server-rs/crates/api-server/src/main.rs。
dev scheduler 端口被旧进程占用时会误判健康检查
- 现象:旧本地 dev 链路可能输出
Port 3000 is in use, trying another one...,随后api-server.exe报AddrInUse/code: 10048。 - 原因:旧
api-server仍监听默认8082时,脚本的/healthz探测会命中旧进程并误判新服务已就绪;旧 Vite 占住3000时,Vite 默认漂移到新端口,浏览器仍可能打开旧页面。 - 处理:
scripts/dev.mjs已在 publish / 编译前解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并让 Vite 使用--strictPort;遇到端口占用时会自动选择后续可用端口,也可显式传入--api-port/--web-port/--admin-web-port。 - 验证:默认端口被占用时,完整栈应打印
[dev:ports] ... 不可用,改用 ...并把实际端口传给后续 publish、健康检查和 Vite 代理;清理端口后重新启动不再命中旧/healthz。 - 关联:
scripts/dev.mjs、docs/technical/DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md。
Windows debug 长 SSE Future 触发 api-server 断连
- 现象:前端 Vite 代理请求
/api/runtime/creative-agent/sessions/{sessionId}/messages/stream报read ECONNRESET,随后api-server.exe以0xffffffff退出,dev:spacetime回收 SpacetimeDB、Vite 和后台 Vite。 - 原因:单个
async_stream::stream!中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future;真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。 - 处理:长 SSE 路由优先使用
tokio::spawn跑业务流程,通过mpsc+UnboundedReceiverStream向 Axum 返回轻量 stream;失败时更新会话为failed并发送 SSEerror,不要把大段执行逻辑内联到路由返回的 stream future 中。 - 验证:补充实际
collect()SSE body 的路由测试,确认首轮包含stage、puzzle_template_catalog和done,且不会提前发送puzzle_template_selection/puzzle_cost_range;再执行cargo check -p api-server、cargo test -p api-server creative_agent,联调时用npm run dev:api-server检查/healthz。 - 关联:
server-rs/crates/api-server/src/creative_agent.rs、server-rs/crates/api-server/src/app.rs。
creative-agent 过程项不要把历史事件渲染成运行中
- 现象:智能创作页过程中多个阶段从一开始同时转圈,生成结束或进入模板确认后仍有过程项保持转圈。
- 原因:前端把历史
stage、tool_started和thought_summary_delta都按 active 渲染;后端工具开始/完成事件如果toolCallId不一致,也会导致开始事件无法收口。 - 处理:
- 只有最新且仍在执行的 stage 可为 active;等待确认、等待用户、target ready 和 failed 都是静态状态。
- 工具开始事件必须等同一
toolCallId的tool_completed收口;兼容旧流时可按后续同名完成事件兜底。 - 思考摘要只展示用户可见摘要,且流结束或会话进入等待/完成/失败态后必须改成 done。
- 验证:前端测试断言完成后
CreativeAgentProcessItem不再存在tone === 'active';后端测试确认工具开始/完成事件使用相同toolCallId。 - 关联:
src/components/creative-agent/creativeAgentViewModel.ts、server-rs/crates/api-server/src/creative_agent.rs、docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md。
creative-agent 会话切换要清理本地待确认模板
- 现象:用户在一个智能创作会话中点开模板确认面板后,立即切到另一条创作会话,可能看到上一会话的确认面板残留。
- 原因:模板确认面板的
pendingSelection是CreativeAgentWorkspace本地 UI 状态,不属于后端 session 快照;组件复用时如果不监听sessionId清理,会跨会话泄漏。 - 处理:工作区以
session?.sessionId为边界清空pendingSelection;服务端仍以puzzleTemplateSelection/targetBinding作为正式业务状态。 - 验证:前端测试先点开模板确认面板,再 rerender 到另一 session,断言确认面板消失。
- 关联:
src/components/creative-agent/CreativeAgentWorkspace.tsx、src/components/creative-agent/CreativeAgentWorkspace.test.tsx。
视觉小说 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.md、src/services/visual-novel-creation/visualNovelAssetClient.ts。
视觉小说 VN-13 交接时不要再回头找旧迁移方案
- 现象:接手视觉小说的人容易重新打开旧 TXT 迁移文档,把“外部平台工程迁入”误当成当前实现目标。
- 原因:视觉小说历史资料里保留了很多迁移阶段的讨论,而当前真正的实现口径已经收口到 PRD、表目录、Prompt 工具说明、实现收口文档和负向扫描报告。
- 处理:维护视觉小说时优先看
AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md、SPACETIMEDB_TABLE_CATALOG.md、VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md、VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md、VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md和VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md。 - 验证:新开发者只读这组文档即可继续维护,不需要把旧 TXT 迁移方案重新当作编码依据。
- 关联:
docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md、docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md、docs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md。
视觉小说公开广场不要触发登录刷新
- 现象:未登录用户进入平台公开广场或从推荐流读取视觉小说公开作品时,前端可能先尝试
/api/auth/refresh,失败后再读取公开列表,导致无意义的鉴权噪声或 401 状态刷新。 - 原因:公开只读接口如果复用默认
requestJson选项,缺少 access token 时会先走静默 refresh。 - 处理:视觉小说公开广场列表使用
skipAuth: true与skipRefresh: 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.ts、docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md。
创作 Tab 语义迁移后,旧“新建作品”测试要改看智能创作首页
- 现象:把
create从旧创作中心切到CreativeAgentHome后,旧测试仍尝试在创作页找“新建作品”类型卡,导致用例失败或定位不到元素。 - 原因:产品语义已经变成“创作 = 智能创作首页,草稿 = 旧作品架”,但测试夹具和 helper 还沿用旧入口。
- 处理:把这类测试改成验证智能创作首页、快捷胶囊、抽屉与草稿 Tab;同时给
useRpgEntryLibraryDetail这类恢复路径补上setPlatformTabToDraft。 - 验证:定向
vitest、eslint、typecheck、check:encoding都通过。 - 关联:
src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx、src/components/rpg-entry/useRpgEntryLibraryDetail.ts。
server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
- 现象:在
server-rs下无参数cargo build期望同时构建spacetime-module,导致链接或构建范围误判。 - 原因:workspace default-members 当前只包含
crates/api-server;SpacetimeDB module 有独立构建/发布方式。 - 处理:默认 Rust 构建只覆盖原生
api-server;本地模块发布继续走spacetime publish --module-path ... --build-options="--debug"/ bindings 生成流程。 - 验证:查看
server-rs/Cargo.tomldefault-members,并按相关 SpacetimeDB 文档执行模块构建。 - 关联:
server-rs/Cargo.toml、docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md。
Windows 原生 spacetime-module 单测会链接缺失 SpacetimeDB 宿主符号
- 现象:在 Windows 上执行
cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml可能编译到链接阶段后失败,出现LNK2019/LNK1120,缺失datastore_insert_bsatn、procedure_start_mut_tx、console_log等 SpacetimeDB 宿主符号。 - 原因:
spacetime-module依赖的 SpacetimeDB runtime API 面向 wasm 宿主环境,原生 test exe 链接不到这些宿主导出。 - 处理:日常语法和类型验证使用
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml;需要验证模块行为时走 SpacetimeDB publish/dev 或模块域纯 Rust crate 的单测,不把该原生链接错误当作业务测试失败。 - 验证:
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml能通过;原生cargo test若仍报上述宿主符号缺失,按当前限制记录为未执行。 - 关联:
server-rs/crates/spacetime-module、docs/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启用了sccachewrapper,但当前 agent 没有可执行的sccache、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。Windows 本机若配置了SCCACHE_OSS_*,sccache daemon 冷启动会先经 OSS/本机代理完成缓存读写检查,再监听127.0.0.1:4226;代理或 OSS 链路慢时,Cargo 的sccache rustc -vV可能先超时。 - 处理:保留
server-rs/.cargo/config.toml的rustc-wrapper = "sccache";本地npm run dev/npm run dev:spacetime/npm run dev:api-server由scripts/dev.mjs给 Rust 子进程注入直通 wrapper,自动绕过项目默认 sccache,避免损坏的 daemon 阻断spacetime publish或api-server启动;显式设置的非 sccache 自定义 wrapper 会被保留。Windows 本机优先在%APPDATA%\Mozilla\sccache\config\config写入server_startup_timeout_ms = 60000,拉长 client 等待 daemon 完成 OSS 初始化的时间,然后删除server-rs/target/.rustc_info.json里缓存的失败探测结果并重跑原始 Cargo 命令。冷启动验证优先用sccache --stop-server,不要在另一个cargo/rustc仍在编译时taskkill /F /IM sccache.exe /T,否则 proc-macro crate 可能被打断并表现为serde_derive/spacetimedb-bindings-macro的sccache ... exit code: 1。若只做临时排障,可在 Git Bash 中执行RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build ...,或在 PowerShell 用cargo check -p api-server --config "build.rustc-wrapper=''"一次性绕过 wrapper;生产流水线必须先实际执行sccache --version,失败时移除RUSTC_WRAPPER并回退到直接rustc。 - 验证:
rustc -Vv能输出版本;本地npm run dev能完成spacetime publish、api-server/healthz、主站 Vite 和后台 Vite 启动;冷启动后原始cargo check -p api-server和cargo check -p spacetime-module能通过;sccache --show-stats显示Cache location oss, name: genarrative-sccache,证明原始 Cargo/Jenkins 路径仍可使用 sccache/OSS 缓存;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。 - 关联:
scripts/dev.mjs、jenkins/Jenkinsfile.production-stdb-module-build、docs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md、docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
- 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
- 原因:旧 Jenkins / 旧本地远端部署脚本文档仍作为历史经验保留。
- 处理:生产相关操作先看
PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md,再按需追溯旧文档。 - 验证:发布链路使用当前
deploy/systemd、deploy/nginx、scripts/deploy和jenkins/Jenkinsfile.production-*。 - 关联:
docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
Web Deploy 只从 Jenkins 构建归档取包
- 现象:
Genarrative-Web-Deploy需要发布 Web 时,不应再在构建机或 release agent 的本地缓存目录查找web.tar.gz。 - 原因:Web 发布包已经由
Genarrative-Web-Build归档到 Jenkins 构建产物,deploy 阶段继续读本地缓存或通过rsync回构建机拉包会让 release agent 依赖机器拓扑和本地路径。 - 处理:
Genarrative-Web-Build直接归档build/<version>/web.tar.gz、web.tar.gz.sha256和release-manifest.json;Genarrative-Web-Deploy使用copyArtifacts从指定BUILD_JOB_NAME/BUILD_NUMBER_TO_DEPLOY复制完整产物,不保留WEB_ARTIFACT_ROOT、WEB_ARTIFACT_SYNC_HOST或web-artifact-pointer.txt口径。 - 验证:deploy 工作区应直接出现
build/<version>/web.tar.gz与web.tar.gz.sha256;后续仍由scripts/deploy/production-web-deploy.sh执行 checksum 校验和解压 smoke。 - 关联:
jenkins/Jenkinsfile.production-web-deploy、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Jenkins 生产流水线拉 Git 统一走本机 SSH
- 后续更新:2026-07-14 起所有生产 Job 的
Pipeline script from SCM和 Jenkinsfile 内部 checkout 统一使用本机 SSH 地址ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git与凭据genarrative-local-gitea-ssh,不再保留局域网 IP、HTTP 内网地址或公网 fallback。 - 现象:生产发布、数据库导入导出、服务器配置、构建或
Genarrative-Full-Build-And-Deploy流水线执行GitSCM checkout时,如果 Jenkins 生成的 fetch 是+refs/heads/*:refs/remotes/origin/*,公网 Git 链路可能在收包阶段以git-remote-https died of signal 15、curl 56 GnuTLS recv error (-9)、early EOF、invalid index-pack output失败;写死127.0.0.1:3000也会在当前执行 agent 不是 Gitea 所在机器时失败。 - 原因:
127.0.0.1只代表当前执行阶段的 agent 自身,因此 Git checkout 必须收口到同机运行 Gitea SSH、带linux && genarrative-build标签的 Jenkins Built-In Node;公网域名和局域网 IP 会引入额外网络、代理、TLS 与地址漂移。即使使用本机 Git,如果GitSCM没有显式 refspec 并开启CloneOption honorRefspec=true,Jenkins Git 插件仍会拉取所有分支。 - 处理:Full、Web、API、Stdb、Server-Provision 与数据库导入导出的源码准备统一在 Jenkins Built-In Node 使用
GIT_REMOTE_URL=ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git和GIT_REMOTE_CREDENTIAL_ID=genarrative-local-gitea-ssh,GIT_REMOTE_FALLBACK_URL留空。数据库导入导出把经过 commit 校验的必要脚本 stash 给目标 agent,release / dev 目标阶段只 unstash,不再 checkout Git 或挂载 Git SSH 凭据。首次 checkout 保留目标分支 refspec、CloneOption shallow=true depth=1 noTags=true honorRefspec=true,随后由scripts/jenkins-checkout-source.sh复用并在必要时逐步加深。 - 验证:扫描本地 Jenkins live Job
config.xml和所有生产 Jenkinsfile,确认 Git URL 均为ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git,凭据仍为genarrative-local-gitea-ssh且 fallback 为空;确认数据库导入导出在 Prepare 阶段 checkout / stash,目标阶段只 unstash;在 Jenkins 凭据环境运行git ls-remote ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git HEAD,并运行npm run check:production-ops、bash -n scripts/jenkins-checkout-source.sh。 - 关联:
jenkins/Jenkinsfile.production-full-build-and-deploy、jenkins/Jenkinsfile.production-web-build、jenkins/Jenkinsfile.production-api-build、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-web-deploy、jenkins/Jenkinsfile.production-api-deploy、jenkins/Jenkinsfile.production-stdb-module-publish、jenkins/Jenkinsfile.production-server-provision、jenkins/Jenkinsfile.production-database-export、jenkins/Jenkinsfile.production-database-import、scripts/jenkins-checkout-source.sh、docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md。
Jenkins 可选参数在 set -u 下不能裸读
- 现象:数据库导入或导出流水线报
INCLUDE_TABLES: unbound variable,或其它可选参数在 Bash 中未定义即退出。 - 原因:Jenkins string/boolean 参数留空时不一定会导出同名环境变量,而生产数据库导入导出脚本块启用了
set -u。 - 处理:进入 Bash 执行块后先使用
${VAR:-}或${VAR:-默认值}收敛成本地变量;必填项使用${VAR:?中文错误}明确失败原因。 - 验证:扫描
jenkins/Jenkinsfile.production-database-export与jenkins/Jenkinsfile.production-database-import,确认INCLUDE_TABLES、CHUNK_SIZE、SERVER_BACKUP_DIRECTORY、SMOKE_HEALTH_URL等可选参数不再裸读。 - 关联:
docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md、jenkins/Jenkinsfile.production-database-export、jenkins/Jenkinsfile.production-database-import。
Jenkins 二次 checkout 后脚本执行位会被 Git 还原
- 现象:
Genarrative-Server-Provision已在 shell 块前面对脚本执行chmod +x,但进入Prepare Provision Tools后仍报scripts/prepare-server-provision-tools.sh: Permission denied/exit code 126。 - 原因:该阶段会先运行
scripts/jenkins-checkout-source.sh,脚本内部执行git reset --hard HEAD和git clean -fd,会把前面临时chmod的执行位还原为 Git 记录的 mode;若被直接执行的脚本在仓库里是100644,二次 checkout 后仍不可执行。 - 处理:需要直接以
scripts/*.sh方式执行的 Jenkins 脚本应提交为 Git100755;如果只想临时授权,必须放在scripts/jenkins-checkout-source.sh完成之后。 - 验证:运行
git ls-files --stage scripts/prepare-server-provision-tools.sh,确认 mode 为100755;重新跑Genarrative-Server-Provision时应进入工具下载/打包日志,而不是停在Permission denied。 - 关联:
jenkins/Jenkinsfile.production-server-provision、scripts/prepare-server-provision-tools.sh、scripts/jenkins-checkout-source.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision 目标机只接收并执行 Jenkins 上传的脚本
- 现象:
Genarrative-Server-Provision选择DEPLOY_TARGET=development/release时,目标阶段仍要求填写SOURCE_GIT_REMOTE_URL,或在目标 dev / release agent 上执行 Git checkout。 - 原因:旧流水线要求目标 agent 自己拉取 provision 脚本,导致服务器初始化依赖目标机到 Git remote 的网络可达性;公网 Git fallback 还会让目标 agent 内网源不可达时悄悄改从公网拉源码,掩盖路由问题。新口径改为 Jenkins 构建节点准备并上传脚本,目标机只接收和执行。
- 处理:
Prepare Provision Files在linux && genarrative-build上使用固定内网 SSH 源和 Jenkins 凭据genarrative-local-gitea-sshcheckout / 校验SOURCE_BRANCH/COMMIT_HASH,并把 provision 脚本、scripts/deploy/**、deploy/**和.jenkins-source-commitstash 给目标 agent。Provision Target下的Receive Provision Files、Prepare Provision Tools和Provision Server必须运行在目标部署 agent:development 使用linux && genarrative-dev-deploy,release 使用linux && genarrative-release-deploy。目标 agent 不再需要SOURCE_GIT_REMOTE_URL,也不再 checkout Git。 - 验证:Jenkins 日志中应先看到
Prepare Provision Files在linux && genarrative-build上完成源码准备和stash 'server-provision-files',再看到Provision Target下的Receive Provision Files、Prepare Provision Tools和Provision Server在目标 dev / release agent 上运行;目标阶段日志不应出现 Git checkout、SOURCE_GIT_REMOTE_URL、Git 主地址拉取失败...改用备用地址或https://git.genarrative.world/GenarrativeAI/Genarrative.git。 - 关联:
jenkins/Jenkinsfile.production-server-provision、scripts/prepare-server-provision-tools.sh、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision 不要无条件下载工具包
- 现象:目标 dev / release 机器已经安装正确版本的 SpacetimeDB 或
otelcol-contrib,但Prepare Provision Tools仍每次下载 release tarball,网络慢或 GitHub 不稳时会把服务器初始化卡在准备阶段。 - 原因:工具准备阶段如果只按“生成交付包”理解,会忽略它已经运行在目标部署 agent 上这一事实;此时目标机本地的
/usr/local/bin/otelcol-contrib与${SPACETIME_ROOT}/bin/current就是可信状态源。 - 处理:
scripts/prepare-server-provision-tools.sh必须先检查目标机状态:otelcol-contrib --version命中OTELCOL_VERSION时复制现有二进制;spacetimedb-cli --version命中SPACETIME_EXPECTED_VERSION或SPACETIME_DOWNLOAD_ROOT推导出的版本且 standalone 同时存在时,复制${SPACETIME_ROOT}/bin并生成 wrapper。只有缺失、不可执行或版本不匹配时,才查PROVISION_DOWNLOADS_DIR或下载源。 - 验证:运行
bash scripts/check-server-provision-tools.sh;Jenkins 日志应先出现“检查目标机 ...”,已有版本命中时出现“复用目标机已有 ...”,且不出现“下载 ...”。 - 关联:
scripts/prepare-server-provision-tools.sh、jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
个人任务 scope 不得扩成 work/site/module
- 现象:个人任务配置为
work/site/module后进度串桶或静默按 0 处理。 - 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
- 处理:Admin 任务配置页不展示范围选择,保存时固定
scopeKind: 'user';API 和领域构造层拒绝非User。 - 验证:非
userscope 返回错误;相关测试覆盖Site/Module/Work被拒绝。 - 关联:
docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md、docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md。
拼图发布 409 不一定是接口故障
- 现象:拼图结果页点击发布后,控制台出现
POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions 409 (Conflict),用户只看到发布失败。 - 原因:
publish_puzzle_work是资产操作发布入口,发布前会预扣1枚泥点;余额不足时后端按业务冲突返回409 CONFLICT,details.message为泥点余额不足。 - 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
- 验证:
PuzzleResultView单测覆盖发布弹窗内展示泥点余额不足。 - 关联:
src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md、docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md。
拼图发布检查阶段会在事件落库时炸 wasm
- 现象:拼图发布在“发布检查”环节直接报
The module instance encountered a fatal error,wasm backtrace 指向spacetime_module::puzzle::publish_puzzle_work,并停在procedure_commit_mut_tx的 commit 阶段。 - 原因:
publish_puzzle_work_tx会无条件调用emit_puzzle_work_published_event写入puzzle_event;该表的event_id是主键,而事件 ID 由profile_id + published_at_micros组成。只要同一发布动作被重复执行、重放,或极端情况下发生时间戳碰撞,commit 时就会因主键冲突触发 fatal error。 - 处理:待修复。发布事件写入需要改成幂等,或在重复发布时显式跳过已存在的
event_id;发布动作本身也应补一层更明确的幂等键,避免把重复提交直接推到事务提交阶段。 - 验证:对同一
session_id/profile_id/published_at_micros重复调用publish_puzzle_work时,不应再在 commit 阶段炸 wasm;正常发布仍应生成作品、更新 session,并可进入公开详情。 - 关联:
server-rs/crates/spacetime-module/src/puzzle.rs、server-rs/crates/api-server/src/puzzle/handlers.rs、server-rs/crates/spacetime-client/src/module_bindings/puzzle_event_table.rs。
拼图会过早进入待发布态,结果页可能空图但仍显示可发布
- 现象:拼图创作有时刚结束就跳到“待发布”结果页,但结果页里的正式图还是空的,发布检查随后又会拦住,用户会感觉“已经完成了却又不能发布”。
- 原因:拼图的待发布判定太弱,
build_result_preview/validate_publish_requirements和is_puzzle_session_snapshot_publish_ready只检查了作品名、简介、标签、关卡名和 cover 图,没有要求level_scene_image_src、ui_spritesheet_image_src、level_background_image_src等完整资产都齐;历史前端恢复链路里的hasRecoverableGeneratedPuzzleDraft/normalizeRecoveredPuzzleDraftSession也只要有 cover 或候选图就会把草稿当成已完成。 - 处理:前端恢复链路已收口到
platformPuzzleDraftRecoveryModel.ts,只有首图、关卡画面、UI spritesheet 与关卡背景资产包完整时才把恢复草稿抬为完成态;后端build_result_preview/validate_publish_requirements/is_puzzle_session_snapshot_publish_ready也已收紧到同一完整资产包门槛。 - 验证:当某个拼图草稿只补齐首图、但关卡背景或 UI spritesheet 仍缺失时,前端恢复链路不应把它误判为已完成,后端也不应进入
ready_to_publish或返回publishReady=true。 - 关联:
server-rs/crates/module-puzzle/src/application.rs、server-rs/crates/api-server/src/puzzle/tags.rs、server-rs/crates/api-server/src/puzzle/draft.rs、src/components/platform-entry/platformPuzzleDraftRecoveryModel.ts、src/components/puzzle-result/PuzzleResultView.tsx。
WebGL 画布在高 DPR 移动端放大溢出
- 现象:抓大鹅试玩入口进入后,3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
- 原因:
WebGLRenderer.setPixelRatio(...)会把绘图缓冲区乘上设备 DPR;如果没有给renderer.domElement单独设置 CSSwidth/height: 100%和绝对铺满,浏览器可能把高 DPR 缓冲区尺寸当成页面显示尺寸。 - 处理:中心棋盘和托盘预览的 WebGL canvas 统一套用
position:absolute; inset:0; width:100%; height:100%; display:block,renderer.setSize(..., false)只负责同步绘图缓冲区。 - 验证:强制移动端
390x844、DPR 2 截图,确认棋盘左右边界在视口内,canvas CSS 尺寸等于容器尺寸,内部width/height属性可大于 CSS 尺寸。 - 关联:
src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_RUNTIME_3D_GEOMETRY_EXPERIMENT_2026-05-02.md。
Hyper3D subscriptionKey 不要按固定短文本限长
- 现象:抓大鹅生成草稿时,内联 Rodin 图生 3D 模型提交成功后,状态轮询报
subscriptionKey 超过 256 字符,导致/api/creation/match3d/sessions/{sessionId}/actions返回 400。 - 原因:
subscriptionKey是 Hyper3D 返回的 opaque token,长度由上游决定;后端状态查询曾复用普通文本校验,把它限制在 256 字符。 - 处理:
query_task_status对subscriptionKey只做 trim 和非空校验,不做固定长度限制;前端临时任务和 Match3D 草稿响应可继续展示该 token,但不要把它当作可编辑短文本。 - 验证:
cargo test -p api-server accepts_opaque_subscription_key_without_length_cap --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/hyper3d_generation.rs、docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md。
抓大鹅新草稿不要再接回 Rodin 或 GLB 生成
- 现象:修改抓大鹅素材时容易沿用旧 Rodin/GLB 方案,导致新草稿生成耗时变长、进度停在模型阶段,或运行态等待不存在的 GLB。
- 原因:仓库里保留了 Hyper3D 通用代理和历史模型字段,旧文档也曾要求草稿阶段同步生成 GLB。当前产品口径已经改为 2D 多视角素材。
- 处理:新
match3d_compile_draft与批量新增只生成 2D 图片:每个物品 5 个形态,单张2K 1:1物品 spritesheet 固定10*10,每行承载两种物品、每种五个形态,单张最多承载 20 种物品。素材图 prompt 固定要求单一纯绿色#00FF00 / RGB(0,255,0)绿幕背景,上传 OSS 前先把整张 spritesheet 绿幕处理为透明 alpha,再由运行态和编辑器按 alpha 连通域解析;generatedItemAssets[].status使用image_ready,发布校验看imageViews[]、首图引用或可解析的物品 spritesheet。generated-models仅用于历史外部模型链接转存,不能作为新生产链路。 - 验证:
cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml、npm run test -- src\services\miniGameDraftGenerationProgress.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx。 - 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅切图路径不能只用中文物品名
- 现象:草稿页
素材配置 > 物品中多个素材名称不同,但预览图片完全一样。 - 原因:中文物品名经过 OSS 路径段清洗后都可能退化成
item,多张切割图片写到同一个 object key,后写入覆盖先写入。 - 处理:切割图上传路径必须带稳定唯一
itemId前缀,例如items/match3d-item-1-item/views/view-01.png;运行态读取 generated 私有图片时通过同源/api/assets/read-url换签,不直接请求裸 OSS 路径。 - 验证:后端单测覆盖中文名路径唯一,前端运行态测试覆盖 generated 图片源解析。
- 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅生成素材不能只挂在 compile response
- 现象:抓大鹅草稿生成完成后停留在结果页能看到切割好的物品图片;退出后从草稿 Tab 重新进入同一草稿,素材列表变回默认占位或为空,已生成的物品名称和图片丢失。
- 原因:
generatedItemAssets如果只附加在match3d_compile_draft的 HTTP response draft 上,刷新或重进时getMatch3DWorkDetail只能读取 SpacetimeDB 中的match3d_work_profile;旧 mapper 返回空数组,自然无法恢复素材。拼图链路已经通过save_puzzle_generated_images把候选图和 levels 写回 work profile,抓大鹅也必须同样写持久字段。 - 处理:compile 成功时把独立物品图片列表序列化写入
match3d_work_profile.generated_item_assets_json;update_match3d_work/publish_match3d_work保留该字段;API work summary/detail 映射反序列化为generatedItemAssets。前端保持“本次 draft 优先,重进 profile 兜底”的读取顺序。 - 验证:
cargo test -p spacetime-client match3d --manifest-path server-rs/Cargo.toml、cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml、npm run test -- src/components/match3d-result/Match3DResultView.test.tsx。 - 关联:
server-rs/crates/spacetime-module/src/match3d/*、server-rs/crates/spacetime-client/src/mapper.rs、server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅试玩和正式运行态不要只读草稿页本地素材预览
- 现象:结果页能看到生成的物品图片,但点击试玩或从推荐 / 公开作品进入正式抓大鹅时,局内仍显示默认积木素材。
- 原因:结果页本地
assetDrafts和作品 profile 的generatedItemAssets可能不同步;推荐流内嵌运行态若只读卡片摘要,卡片缺素材时会把已持久化 profile 素材丢掉;点击试玩时 React state 异步更新也可能让运行态第一帧读取旧match3dProfile。 - 处理:删除、批量新增、音效生成或封面引用物品素材后,都把当前
generatedItemAssets写回作品 profile;Match3DResultView合并同itemId的 draft/profile 素材,用 profile 已有imageViews[]、首图引用、backgroundMusic或backgroundAsset补齐旧 draft;点击试玩前把试玩可用物品种类通过itemTypeCountOverride降到已生成 2D 素材数量;推荐流内嵌运行态启动前若卡片摘要没有物品图片素材,补读getMatch3DWorkDetail(profileId)并把详情资产传给Match3DRuntimeShell。PlatformEntryFlowShellImpl需要维护match3dRuntimeProfile,在startMatch3DRunFromProfile创建 run 后立即锁定本次完整 profile,runtime 渲染时优先按run.profileId使用这份 profile,而不是等待普通match3dProfilestate 下一轮刷新。同 profile 下已有generatedItemAssets时不能因为图片完整性判断失败就覆盖为空数组。判断是否需要补读详情时只看imageViews[]或imageSrc/imageObjectKey;背景、音乐、容器 UI 是附属运行态资产,不能单独证明物品素材已完整。 - 验证:执行
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx、npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx,并检查历史草稿和公开 M3 作品的 Network 响应里generatedItemAssets[].imageViews/imageSrc/imageObjectKey。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅 UI 背景和容器只在顶层字段时也要传进运行态
- 现象:抓大鹅草稿 / 推荐卡片响应里已有
generatedBackgroundAsset,结果页 UI 预览能看到纯背景图和容器图,但进入试玩或正式局内仍显示默认渐变背景和默认圆形容器。 - 原因:部分链路把 UI 资产只放在作品顶层
generatedBackgroundAsset/backgroundImageObjectKey,没有同步放进首个generatedItemAssets[].backgroundAsset;如果运行态入口只传generatedItemAssets和backgroundImageSrc,Match3DRuntimeShell就拿不到containerImageObjectKey。 - 处理:
PlatformMatch3DGalleryCard、mapPublicWorkDetailToMatch3DWork、resolveMatch3DRuntimeGeneratedBackgroundAsset和Match3DRuntimeShell都必须保留并传递顶层generatedBackgroundAsset;运行态背景读取顺序为backgroundImageSrc/ 顶层generatedBackgroundAsset.image*/generatedItemAssets[].backgroundAsset.image*,容器读取顺序为顶层generatedBackgroundAsset.containerImage*/generatedItemAssets[].backgroundAsset.containerImage*。 - 验证:执行
npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx和npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "Match3D runtime";浏览器 Network 中背景和容器 generated path 应先请求/api/assets/read-url换签,局内出现match3d-background-image和match3d-container-image对应图片。 - 关联:
src/components/match3d-runtime/Match3DRuntimeShell.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/rpgEntryWorldPresentation.ts、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅容器参考图必须进入 edits multipart image 并接管棋盘外观
- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像
pot-fused-reference.png,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。 - 原因:容器参考图必须进入
gpt-image-2/v1/images/editsmultipartimagepart,并配合强 prompt 锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认rounded-full锅壳和overflow-hidden,生成图也会被默认视觉覆盖或裁掉。 - 处理:抓大鹅
1:1容器 UI 图统一调用 VectorEnginePOST /v1/images/edits,参考public/match3d-background-references/pot-fused-reference.png的透明容器图由后端作为imagepart 上传;该参考图属于后端生图协议输入,需通过include_bytes!编译进api-server,不能在运行时按当前工作目录读取public/。Match3DRuntimeShell在容器图换签并成功加载后,把棋盘外壳切为透明和overflow-visible,只在容器缺失或加载失败时使用默认圆形容器。 - 验证:执行
cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml、cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml、npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx;真实联调看容器生成请求是否命中/v1/images/edits,局内match3d-container-image是否渲染且match3d-board不再含默认rounded-full。 - 关联:
server-rs/crates/api-server/src/openai_image_generation.rs、server-rs/crates/api-server/src/match3d.rs、src/components/match3d-runtime/Match3DRuntimeShell.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅结果页音频试听也要先换签
- 现象:抓大鹅草稿生成完成后,背景音乐已写在
generatedItemAssets[0].backgroundMusic.audioSrc,但素材配置 > 背景音乐或物品详情音效<audio>不能播放,Network 可能请求裸/generated-match3d-assets/...mp3并返回 403。 - 原因:结果页试听控件和运行态一样运行在浏览器里,不能直接读取 generated 私有对象;只在运行态换签会造成“运行态可能有声,结果页不能预览”的割裂。
- 处理:结果页音频控件统一通过
useResolvedAssetReadUrl//api/assets/read-url取得签名 URL 后再传给<audio>;换签失败时只显示“音频已绑定”,不要回退请求裸 generated path。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx覆盖背景音乐和点击音效试听使用签名 URL。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/services/assetReadUrlService.ts、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
法律文档弹窗通过 portal 挂载时要显式带平台主题
- 现象:登录弹窗内点击协议链接打开法律文档时,弹窗可能继承不到
platform-theme--light/dark变量,或者层级低于登录遮罩导致不可见。 - 原因:
UnifiedModal默认通过 portal 挂到document.body,不再处于原页面的主题容器内;登录弹窗自身又使用较高 z-index。 - 处理:法律文档弹窗组件应支持传入
platformTheme,overlay 上显式挂platform-theme platform-theme--*,并使用高于登录遮罩的层级。法律内容必须作为独立面板打开,不要在当前个人页或登录面板下方内联展开。 - 验证:登录页协议链接、个人页法律入口均能打开可滚动
LegalDocumentModal,亮色 / 暗色主题文本和按钮可读。
生成页完成回调不能只依赖异步 React state
- 现象:抓大鹅或拼图点击生成后,进度页已经显示 100% / 生成完成,但没有自动进入试玩或结果页。
- 原因:完成回调用
selectionStageRef.current判断用户是否仍在生成页;如果执行 compile 前只调用setSelectionStage('*-generating'),action 很快返回时 ref 仍可能是旧 stage。 - 处理:进入各玩法生成页时同步写
selectionStageRef.current = '*-generating',再调用setSelectionStage('*-generating')。这不是为渲染服务,而是给同一异步链路里的完成回调提供即时事实。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx覆盖抓大鹅和拼图生成后自动试玩 / 返回结果页。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图最后一步到 100% 但不变绿优先看阶段映射
- 现象:拼图草稿生成跑完所有步骤后,总进度仍停在 98%,最后一步“写入正式草稿”显示 100% 但卡片不变绿,视觉上像还在进行中。
- 原因:进度条总进度刻意保留 98% 作为未收到 action 回包前的安全余量,但最后一步的绿色完成态只看步骤状态;如果时间轴已经跑到
puzzle-select-image末尾却还没收到ready回包,最后一步会一直保持 active。 - 处理:
buildMiniGameDraftGenerationProgress需要在拼图最后一步时,把“预计写入时长已耗尽”单独判为 completed,避免出现“进行中 100%”。 - 验证:
npm test -- src/services/miniGameDraftGenerationProgress.test.ts。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、src/services/miniGameDraftGenerationProgress.test.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
微信支付回调验签不要用商户私钥
- 现象:微信小程序支付下单能返回
prepay_id,但真实支付通知验签失败,或者本地实现误把商户 API 私钥当作回调验签 key。 - 原因:商户私钥只用于商户请求微信支付和生成小程序
paySign;微信支付通知的Wechatpay-Signature需要使用微信支付平台公钥或平台证书公钥验签,并按通知头里的平台序列号匹配。 - 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:
WECHAT_PAY_PRIVATE_KEY_*用于签名,WECHAT_PAY_PLATFORM_PUBLIC_KEY_*与WECHAT_PAY_PLATFORM_SERIAL_NO用于通知验签,WECHAT_PAY_API_V3_KEY只用于解密通知 resource。微信平台PUBLIC KEYPEM 的 DER 内容是 SPKISubjectPublicKeyInfo,初始化时必须解析并提取其中的 PKCS#1RSAPublicKeyDER 后再交给ring::RSA_PKCS1_2048_8192_SHA256;不能把整段 SPKI DER 直接传给ring。支付成功后只通过通知里的out_trade_no确认本地 pending 订单,并保存transaction_id到profile_recharge_order.provider_transaction_id。 - APIv3 通知成功应答使用 HTTP
204 No Content,不要沿用 V2 XML 成功报文;失败仍返回 4XX/5XX 让微信重试。 - 验证:mock 通知测试只能覆盖本地回调推进;
platform-wechat必须用标准 SPKIPUBLIC KEY和匹配私钥生成真实 RSA-SHA256 签名,覆盖 SPKI 到 PKCS#1 的解析与生产验签 helper。真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
微信支付 JSAPI 下单必须显式带 User-Agent
- 现象:调用
/v3/pay/transactions/jsapi失败,微信返回“Http头缺少Accept或User-Agent”。 - 原因:
reqwest请求即使已设置Accept: application/json,也不会默认附带业务侧User-Agent;微信支付网关会校验这两个头。 - 处理:
api-server的 JSAPI 下单请求统一通过with_wechat_pay_jsapi_headers(...)设置Accept: application/json、Content-Type: application/json和User-Agent: Genarrative-WechatPay/1.0。 - 验证:执行
cargo test -p api-server jsapi_order_request_sets_wechat_required_http_headers --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/wechat_pay.rs、docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md。
容器公开列表压测不要靠继续抬并发吃满 CPU
- 现象:2C / 2G 容器压测公开 gallery list 时,
api-serverCPU 仍有余量,看起来像可以继续提高GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS或 Nginxlimit_conn。 - 原因:当前瓶颈不是 Tokio worker 线程数。
/api/runtime/puzzle/gallery和/api/runtime/custom-world-gallery成功响应后会走全局 route tracking,继续向 SpacetimeDB 写record_tracking_event_and_return;入口并发从 320 抬到 336 / 352 时,SpacetimeDB 内存先逼近896m容器上限,200 请求 p95 变差,429 比例没有改善。 - 处理:2C / 2G 容器模拟里公开 gallery list 暂以
limit_conn=320、GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320作为稳定上限。若要继续提升吞吐,优先减少高频公开 GET 的 tracking 写入、做采样或改成批量/异步聚合;不要单纯放大入口并发。 - 验证:宿主机 k6 打
http://127.0.0.1:18080,PEAK_RPS=1000等价约 2000 HTTP req/s;320 档无 dropped iterations、无 5xx、无 OOM,200 请求request_time p95约 0.292s。336 / 352 档 p95 升到约 0.31s / 0.32s,SpacetimeDB 内存尾部可到约880MiB / 896MiB。 - 关联:
deploy/container/nginx.conf、deploy/container/api-server.env.example、deploy/container/README.md、server-rs/crates/api-server/src/tracking.rs。
tracking outbox 成功入库后删除 sealed 文件
- 现象:普通 route tracking 改为本机 outbox 后,容易误以为入库成功只需要清空文件内容。
- 原因:清空文件会扩大崩溃窗口,进程在 truncate 和确认之间异常退出时可能丢失未确认事件。
- 处理:当前 active NDJSON 达到数量或时间阈值后原子 rename 为 sealed 文件;后台批量 flush sealed 文件,SpacetimeDB 返回成功后直接删除该文件,失败则保留文件等待重试。sealed 文件如果出现无法解析的坏行,重命名为
corrupt-*隔离并记录指标,避免阻塞后续批量入库。该路径是至少一次投递,重复事件由tracking_event.event_id幂等跳过。 - 验证:模拟 SpacetimeDB 不可用时 sealed 文件保留;恢复后批量 procedure 成功,sealed 文件消失,
tracking_event与tracking_daily_stat均更新。 - 关联:
docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、server-rs/crates/api-server/src/tracking.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs。
后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码
- 现象:后台“表查询”查看
profile_recharge_order时,kind和status显示为空数组[],例如充值订单原始行里points_60的类型和状态都不可读。 - 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态
[variant_index, []];后台通用 normalizer 曾把任何[0, value]都当作Option::Some(value)展开,导致[0, []]最终只剩[]。 - 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如
profile_recharge_order.kind映射为points/membership,profile_recharge_order.status映射为pending/paid/failed/closed/refunded/expired。 - 验证:执行
cargo test -p api-server admin_database -- --nocapture,并确认后台详情弹层的raw与表格cells都显示业务字符串。 - 关联:
server-rs/crates/api-server/src/admin.rs、docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md。
后台通用表查询不能先按每页条数截断再筛选
- 现象:后台“表查询”填写关键词或 JSON 条件后查不到确定存在的记录;把“条数”从 100 调到 500 只能偶尔缓解,而且页面没有继续翻页的入口。
- 原因:旧实现先执行
SELECT * FROM <table> LIMIT <limit>,再对这批行做内存过滤;目标记录不在首批结果时永远无法命中,同时响应没有页码、匹配总数或扫描上限状态。 - 处理:用户输入继续不进入通用 SQL。API Server 通过单次
SELECT * ... LIMIT 50001读取哨兵行,最多保留前 50,000 条候选,先过滤,再按后端接收的列名 / 方向对完整候选集稳定排序,最后分页;totalMatched、scannedCount和scanLimitReached都从同一份 SQL 结果计算。请求页码超过实际总页数时钳制到末页,零结果固定为第 1 页。响应统一返回page、totalMatched、scannedCount、scanLimit和scanLimitReached,后台翻页栏固定在视口底部;达到扫描上限时明确提示结果可能不完整。32 MiB 是候选 SQL 响应体硬上限,宽表即使每页条数很小也可能整次拒绝,不返回部分结果。实时写入可能改变相邻请求之间的候选快照,精确审计使用专用业务查询。 - 验证:执行
cargo test -p api-server admin_database -- --nocapture,覆盖第 101 行才命中、完整候选集排序后分页、哨兵截断、越界页码和响应体硬上限;前端测试覆盖下一页沿用已应用条件、后端排序参数 / 返回结果与扫描警告,并运行后台类型检查。 - 关联:
server-rs/crates/api-server/src/admin.rs、server-rs/crates/shared-contracts/src/admin.rs、apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx。
充值订单过期补偿不要放进外部生成 worker
- 现象:外部生成 worker/controller 扩容后,微信充值过期查单和关单流量也被同步放大;排查时还会误去外部生成 worker 日志里找支付过期任务。
- 原因:支付过期是账户资金链路,不是外部内容生成队列;旧实现把充值过期轮询 worker 挂在通用后台任务启动函数里,非 HTTP 角色也会启动。
- 处理:充值订单过期由 SpacetimeDB 原生
profile_recharge_order_expiration_timer到点把pending改为expired,只有 HTTPapi-server订阅活跃 timer 表的删除事件,按order_id重新读取订单并仅对expired查微信补偿;支付或主动关闭导致的删除信号会被状态判断忽略,断线窗口由未检查过期订单 catch-up 补齐。未支付终态本地保持expired,不要再改写成closed;微信成功支付通知或补偿查单仍可把Expired -> Paid入账。 - 验证:确认
GENARRATIVE_PROCESS_ROLE=external-generation-worker/external-generation-controller不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生expired,HTTP api-server listener 记录expiration_checked_at或补入账。 - 关联:
server-rs/crates/api-server/src/profile_recharge_expiration_listener.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
充值订单状态枚举不能用字符串 SQL 字面量订阅
- 现象:API 已 ready,但日志每 5 秒出现
profile recharge expiration listener failed to subscribe,并提示pending不能解析为profile_recharge_order.status的枚举类型;scheduled reducer 仍会把订单改成expired,但微信查单补偿监听没有运行。 - 原因:SpacetimeDB 2.6 不会把订阅 SQL 中的
'pending'/'expired'字符串自动转换为生成绑定的 sum-type enum;两个按状态过滤的订阅都在应用阶段失败。 - 处理:不要改成订阅完整
profile_recharge_order历史表。后端订阅只保留活跃五分钟定时器的profile_recharge_order_expiration_timer,监听 timer 删除后按order_id通过 procedure 读取订单,只处理当前状态为expired的记录;支付 / 关闭信号会被忽略,断线窗口继续由未检查过期订单 catch-up 补齐。这样既不依赖不受支持的枚举 SQL,也不会把充值历史常驻 API 客户端缓存。 - 验证:运行
cargo test -p spacetime-client profile_recharge_expiration --manifest-path server-rs/Cargo.toml,发布后确认 API 日志不再出现订阅解析错误,并用真实 pending 订单验证 scheduled reducer 过期后写入expiration_checked_at。
微信 Native 已入账但二维码弹窗不关闭
- 现象:微信支付回调已经返回
204,本地充值订单为paid且泥点已到账,但网页仍停留在“微信扫码支付”,必须点击“我已支付”才刷新。 - 原因:通用页面恢复确认逻辑在存在
nativeWechatPayment时直接跳过,Native 分支创建二维码后也没有订阅订单 SSE,因此服务端回调发布的订单更新没有前端消费者。 - 处理:Native 二维码出现后立即调用
watchWechatRpgProfileRechargeOrder订阅当前订单;收到终态后更新充值中心、关闭二维码、清理 pending ref、刷新全局余额并只展示一次结果。SSE 超时或暂时失败时在二维码过期前重连,手动确认与 SSE 并发时以 pending order ref 保证只有首个终态生效。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx覆盖不点击“我已支付”也会在 SSE 返回paid后自动关闭;再运行根级npm run typecheck、npm run check:encoding和git diff --check。 - 关联:
src/components/platform-entry/usePlatformProfileCenterController.ts、src/services/rpg-entry/rpgProfileClient.ts、server-rs/crates/api-server/src/runtime_profile.rs。
商户平台退款登记不要混淆 refund_id 与 out_refund_no
- 现象:在“登记商户平台退款”里填写
50000000000000000000000000000一类微信退款单号后提示找不到退款。 - 原因:该编号是微信侧
refund_id;V3 单笔退款查询路径只接受商户退款单号out_refund_no。把refund_id放进路径不会自动转换,退款查单会返回RESOURCE_NOT_EXISTS。不过微信没有承诺refund_id固定为50开头的 29 位数字,合法out_refund_no也可能是纯数字,因此形状判断不能代替真实查单。 - 处理:登记表单明确标注
out_refund_no,所有满足官方字符和长度约束的输入都交给服务端真实查询;只有查询确认不存在后,才把50开头的 29 位纯数字作为“疑似 refund_id”给出定向提示。只有refund_id时等待退款回调或 T+1 退款账单建立映射;不要调用异常退款申请接口冒充查询。out_refund_no字符校验须覆盖官方允许的数字、大小写字母和_ - | * @。 - 验证:后台页面测试断言疑似编号仍交给服务端;平台适配器测试锁定
RESOURCE_NOT_EXISTS映射和@字符,并确认真正的out_refund_no仍调用GET /v3/refund/domestic/refunds/{out_refund_no}。 - 关联:
apps/admin-web/src/pages/AdminRechargeOrderPage.tsx、server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/platform-wechat/src/pay.rs。
微信支付查单的 REFUND 不等于已经全额退款
- 现象:一笔 6 元充值在商户平台成功退 3 元并登记
out_refund_no后,本地显示累计已退 3 元、剩余可退 3 元,但后台再次预检仍显示“未核验 / REFUND”并禁止退款。 - 原因:微信支付订单查单的
trade_state=REFUND只说明该支付订单发生过退款,不携带累计退款明细,也不表示已经全额退款。若后台把verified硬编码为trade_state == SUCCESS,任何已成功部分退款的订单都会永久失去继续退款能力;反过来,仅看到REFUND就直接放行又可能漏掉未登记的商户平台退款。 - 处理:预检先查支付订单并校验商户订单号、支付单号和总金额,再主动刷新全部已知
out_refund_no并重读本地退款 settlement。SUCCESS可继续预检;REFUND仅在本地累计成功退款大于 0 且小于订单总额,并且没有PROCESSING / ABNORMAL退款、活动 hold、退款欠账或人工冻结时,允许继续退本地剩余额度。没有本地成功退款能解释REFUND时使用独立原因码阻止并要求登记或对账,不能冒充“订单未支付”。 - 验证:后端策略测试覆盖
SUCCESS + 0/600、REFUND + 0/600、REFUND + 300/600、REFUND + 600/600;后台页面测试覆盖已核验 / REFUND时剩余额度可提交。真实联调核对累计退款、已追回泥点、活动占用和欠账均与退款明细一致。 - 关联:
server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、apps/admin-web/src/pages/AdminRechargeOrderPage.tsx。
抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
- 现象:草稿页预览模型失败并报
GL_INVALID_ENUM: Invalid cap.,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。 - 原因:历史结果页手动
重新生成会把 Hyper3D/Rodin 的外部 CDN 下载链接直接保存到generatedItemAssets[].modelSrc,同时modelObjectKey为空。外部链接可能过期、跨域、返回 HTML 错误页或非 GLB 内容;前端预览和运行态不能把它当作稳定私有资产。 - 处理:该问题只适用于旧数据。结果页发现
status = model_ready、modelSrc = https://...且无modelObjectKey时,可调用POST /api/creation/match3d/works/{profileId}/generated-models做一次性转存;新草稿和批量新增不得继续生成或依赖 GLB。若历史半修复数据同时保留外部modelSrc和平台modelObjectKey,旧模型预览读取层优先用modelObjectKey。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx、npm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsx、npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx、cargo test -p api-server match3d_model_download --manifest-path server-rs\Cargo.toml,并检查修复后响应中的generatedItemAssets[].modelObjectKey不为空。 - 关联:
server-rs/crates/api-server/src/match3d.rs、src/components/match3d-result/Match3DResultView.tsx、src/components/match3d-result/Match3DModelPreview.tsx、src/components/match3d-runtime/Match3DPhysicsBoard.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅难度配置的物品种类和消除次数必须分离
- 现象:历史草稿选择标准 / 硬核难度后,系统可能把
clearCount当成局内物品种类数量,导致标准需要 12 种、硬核需要 20/21 种;或者把第 11 到 20 个物品持久化为第 11 到 20 行,触发“系列素材图集持久化的行列索引必须落在 n*n 范围内”。 - 原因:旧运行态把消除次数和类型数量绑在一起,结果页文案又同时展示“素材图片 / 局内类型”,导致前端、发布校验和 run start 口径不一致。
- 处理:生成和持久化固定使用 20 个物品素材;运行态物品种类口径为轻松 3、标准 9、进阶 15、硬核 20,历史
clearCount=20且难度为硬核的运行态仍可升为 21 组三消,但类型池不超过 20。10*10 sheet 每行两种物品、每种五个形态,持久化行列为row = itemIndex / 2 + 1、col = itemIndex % 2 * 5 + viewIndex + 1。发布前按image_ready且有imageViews[]或imageSrc/imageObjectKey的生成素材数量阻断不足难度;试玩不阻断,但通过itemTypeCountOverride自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。 - 验证:
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx、cargo test -p module-match3d --manifest-path server-rs\Cargo.toml,涉及发布 reducer 时补跑cargo test -p spacetime-module match3d --manifest-path server-rs\Cargo.toml。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、src/services/match3d-runtime/match3dRuntimeClient.ts、server-rs/crates/module-match3d/src/application.rs、server-rs/crates/spacetime-module/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅标签清洗不要把 3D素材 当编号剥掉
- 现象:AI 或兜底生成的
3D素材标签在后端规范化后变成D素材。 - 原因:标签清洗在去掉编号列表前缀后,又无条件剥离开头数字和标点,把合法标签中的
3D当成列表编号处理。 - 处理:只移除明确的编号列表前缀,例如
1. 标签、1、标签、1) 标签;不要对普通标签开头数字做二次剥离。 - 验证:
cargo test -p api-server match3d_tag_normalization --manifest-path server-rs/Cargo.toml,并保留normalize_match3d_tag("3D素材") == "3D素材"的单测。 - 关联:
server-rs/crates/api-server/src/match3d.rs。
抓大鹅物品切图白边或绿幕残留先查后端透明化
- 现象:抓大鹅生成的物品视角图裁剪后仍带白边,或者整块纯绿色绿幕背景没有被透明化,运行态看到绿色方块。
- 原因:素材 sheet 可能是“每格内部绿幕、整张图外圈近白底”,内部绿幕不一定连通到 sheet 外边缘;旧 flood fill 只从外边缘找背景会漏掉这种绿幕块。白底抗锯齿如果不纳入抠像和边缘去污染,也会随裁剪输出成一圈白边。即使顺序已是先整张 sheet 去绿再裁剪,较厚的半透明或混色软绿边仍可能低于高置信绿幕阈值,被当作前景带进独立 PNG。
- 处理:
api-server的slice_match3d_material_sheet必须先在整张 sheet 上做透明背景后处理:外边缘连通绿幕/近白底清 alpha,非连通但高置信纯绿块也清 alpha,沿整张 sheet 透明背景继续吃掉软绿边,边缘近白和绿幕抗锯齿做透明或去污染;同时保护不够纯的绿色主体像素。不要改成先裁剪单格再去绿。 - 验证:
cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml覆盖非连通绿幕、白边、贴边主体保留和固定10*10切图;cargo test -p api-server match3d_spritesheet_green_screen_postprocess_turns_background_transparent --manifest-path server-rs\Cargo.toml覆盖完整 spritesheet 上传前绿幕透明化。 - 关联:
server-rs/crates/api-server/src/match3d.rs、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
抓大鹅物品详情大方格只做单张大图查看
- 现象:结果页
素材配置 > 物品打开详情后,上方大方格仍显示横向五图带、焦点内框或小缩略图边框,物品本体看起来偏小且像带着素材自带边框。 - 原因:旧预览把上方区域当作横向视角带,当前焦点只是带内缩略图的一张,视觉上不是“详细查看物品形象”的大图。
- 处理:上方方格只渲染当前选中的单张大图,使用
object-contain和少量内边距放大查看;底部缩略图栏负责切换视角,缩略图可以保留选中态边框,但上方大图不渲染焦点内框或缩略图容器边框。 - 验证:
npm run test -- src/components/match3d-result/Match3DResultView.test.tsx覆盖上方大图、底部缩略图和视角切换。 - 关联:
src/components/match3d-result/Match3DResultView.tsx、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
草稿页卡片有真实素材但仍显示黑卡先查摘要字段
- 现象:草稿页拼图卡片没有关卡图背景,抓大鹅卡片没有背景图或物品图背景,甚至兜底视觉也退回黑色面板。
- 原因:拼图列表摘要若不下发
levels,前端拿不到关卡coverImageSrc/ 候选图;抓大鹅列表摘要若只提供公开 URL、不保留generatedBackgroundAsset或generatedItemAssets中的 object key,前端无法换签读取私有生成图。卡片封面组件如果自带暗色默认背景,也会让兜底失败时看起来仍是黑卡。 - 处理:拼图
map_puzzle_work_summary_response必须保留levels;草稿页优先用关卡coverImageSrc,再用候选图。抓大鹅货架封面解析必须读取backgroundImageObjectKey、generatedBackgroundAsset.imageObjectKey/containerImageObjectKey、generatedItemAssets[].imageObjectKey和imageViews[].imageObjectKey。图片渲染统一交给ResolvedAssetImage换签,并给卡片传入玩法参考图与暖色底兜底。 - 验证:执行
npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/hooks/useResolvedAssetReadUrl.test.tsx、cargo test -p api-server puzzle_work_summary_response_keeps_levels_for_shelf_cover --manifest-path server-rs\Cargo.toml、npm run typecheck。 - 关联:
src/components/custom-world-home/creationWorkShelf.ts、src/components/CustomWorldCoverArtwork.tsx、server-rs/crates/api-server/src/puzzle.rs、docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md。
用户标签不要直接外显,SpacetimeDB Vec 字段不要写 default 宏
- 现象:给
user_account.user_tags或邀请码独立标签列写#[default(Vec::<String>::new())]时,SpacetimeDB WASM 构建报destructor of Vec<String> cannot be evaluated at compile-time。 - 原因:SpacetimeDB 的 table default 宏会走编译期常量求值,不能直接使用有析构逻辑的堆分配类型默认值。
- 处理:
user_account.user_tags使用Option<Vec<String>>+#[default(None::<Vec<String>>)]表达数据库默认空,业务层统一把None归一化为空数组;邀请码授予标签复用metadata_json.userTags存储和解析,不再新增独立 Vec 列。用户标签原始值不得进入登录态、个人资料等通用响应,只能在明确业务白名单里投影,例如拼图排行榜visibleTags首版仅允许北科。 - 验证:
npm run spacetime:generate -- --rust-only能通过;user_account旧迁移 JSON 缺字段时能导入,profile_invite_code缺metadata_json时按{}兼容。 - 关联:
docs/technical/USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.md、docs/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.ts、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"。 - 关联:
docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md、src/routing/runtimeNotFoundRecovery.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx。
拼图 UI 背景只有 objectKey 时不要回退默认 UI
- 现象:拼图草稿页、试玩和正式运行态都显示默认 UI,或者只在结果页看到生成图,进入试玩后又回到默认背景。
- 原因:
uiBackgroundImageSrc可能为空而真实生成结果只写了uiBackgroundImageObjectKey;如果前端和运行态只读src,或者本地试玩 / 正式 run 没把objectKey一起传递,就会丢掉已有背景。 - 处理:统一通过一个解析入口把
uiBackgroundImageSrc || uiBackgroundImageObjectKey归一到可展示路径;本地试玩和正式运行态都要保留uiBackgroundImageObjectKey,并在uiBackgroundImageSrc为空时换签读取。 - 验证:结果页 UI Tab、
startLocalPuzzleRun和PuzzleRuntimeShell都应在仅有objectKey时显示生成背景,不再回落默认 UI。 - 关联:
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts、src/components/puzzle-result/PuzzleResultView.tsx、src/services/puzzle-runtime/puzzleLocalRuntime.ts、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、server-rs/crates/module-puzzle/src/application.rs。
拼图 UI 背景提示词或作品元信息异常先查首关命名契约
- 现象:拼图草稿生成完成后,第一关名称或作品名称变成
levelNam/levelName这类字段名片段,或素材配置 > UI里显示的UI背景提示词像前端或后端模板拼接,而不是 AI 生成的视觉提示词。 - 原因:首关命名 LLM 旧契约只返回
levelName,自动 UI 背景阶段只能用作品名、作品描述、关卡描述和标签拼接确定性兜底提示词;如果模型返回截断 JSON,解析层还可能把levelNam这类字段名片段当作普通英文关卡名归一化通过。 - 处理:首关命名 LLM 契约必须同时返回
{"levelName":"...","workDescription":"...","workTags":["..."],"uiBackgroundPrompt":"..."};解析层必须拒绝levelNam、levelName、workDescription、workTags、uiBackgroundPrompt等字段名片段作为关卡名。草稿自动 UI 背景生成优先使用该 AI 提示词,作品描述和 6 个作品标签默认填入草稿;视觉精修请求若返回新提示词或作品元信息则覆盖文本请求结果,否则保留文本请求结果。前端文本框只展示已保存的uiBackgroundPrompt或用户编辑值,字段为空时不展示本地兜底模板。 - 验证:执行
cargo test -p api-server puzzle_level_naming_parser --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_first_level_name --manifest-path server-rs\Cargo.toml、cargo test -p api-server puzzle_initial --manifest-path server-rs\Cargo.toml、npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx。 - 关联:
server-rs/crates/api-server/src/prompt/puzzle/level_name.rs、server-rs/crates/api-server/src/puzzle.rs、src/components/puzzle-result/PuzzleResultView.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
拼图 / 抓大鹅 UI 背景重生成报 No such procedure 先查 SpacetimeDB 版本漂移
- 现象:拼图或抓大鹅结果页点击
重新生成UI 背景时报No such procedure,常见位置是泥点预扣、save_puzzle_ui_background或 Match3D 草稿写回。 - 原因:
api-server和spacetime-client已按新 bindings 调用 procedure,但目标 SpacetimeDB 数据库仍运行旧 wasm,尚未导出钱包扣退费、拼图 UI 背景保存或 Match3D 写回相关 procedure。 - 处理:临时容错是把这类
No such procedure当作后端版本漂移:泥点预扣阶段跳过扣费,图片已经生成但保存失败时返回本次内存快照 / 内存 profile,避免草稿页直接报错。长期修复仍是发布最新spacetime-module、重新生成 bindings,并用spacetime describe或定向 smoke 确认 procedure 已导出。 - 验证:
cargo test -p api-server asset_operation_billing_skips_spacetime_connectivity_errors --manifest-path server-rs\Cargo.toml、cargo test -p api-server match3d_fallback_work_profile_keeps_generated_background_asset --manifest-path server-rs\Cargo.toml、npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/asset_billing.rs、server-rs/crates/api-server/src/match3d.rs、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md、docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md。
拼图合并块拖起后原位置出现红色块先查选中态泄漏
- 现象:拼图运行态中,多个拼图片合并后拖起整体块,原位置会露出一块粉红 / 红色底色。
- 原因:合并块拖拽的可见层来自
mergedGroups绝对定位整体层,但pointerdown会同步写入selectedPieceId;若棋盘格里的底层单块 DOM 先匹配选中态,再匹配合并态,整体层移开后就会露出单块选中填充色。 - 处理:合并格底层 DOM 只作为透明定位占位,
isSelected必须排除isMerged;合并格样式优先级高于单块选中态。 - 验证:运行
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "拖拽合并大块时底层单格不显示选中色块",并确认合并块拖拽时底层[data-piece-id]仍为puzzle-runtime-piece--merged。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx、docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md。
推荐页嵌入拼图通关结算不要放在运行态内部 absolute 层
- 现象:推荐页里玩拼图通关后,结算面板只显示上半部分,排行榜或下一关按钮被截断。
- 原因:推荐页把运行态放在滑动作品卡的视觉区内,
platform-recommend-swipe-page、platform-recommend-swipe-card__visual和platform-recommend-runtime-viewport都是overflow: hidden;拼图通关结算如果仍是运行态内部absolute inset-0弹层,就只能在半屏卡片区域里显示。 - 处理:
PuzzleRuntimeShell在embedded模式下把通关结算层通过 portal 挂到document.body,使用puzzle-runtime-modal-overlay--fixed页面级 fixed 浮层;非嵌入态继续使用运行态内部覆盖层。 - 验证:运行
npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "推荐页嵌入拼图通关结算使用页面级浮层避免卡片裁剪",确认弹层不再位于.platform-recommend-runtime-viewport内。 - 关联:
src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/index.css、src/components/rpg-entry/RpgEntryHomeView.tsx。
拼图历史图片列表不要把账号归属当图片名
- 现象:拼图创作页或结果页打开“选择历史图片”后,历史列表显示
账号 user-1之类归属文案而不是图片名;1713686400.000000Z这类时间显示为未知;选中后预览或生成参考图可能被怀疑不可用。 - 原因:
/api/assets/history?kind=puzzle_cover_image返回的ownerLabel是资产归属账号,不是图片标题;createdAt可能是 SpacetimeDB / shared-kernel 秒级时间字符串,不能只用浏览器new Date(value)解析。历史图的imageSrc是/generated-*私有兼容路径,浏览器预览必须换签。 - 处理:前端标题和选中标签从
imageSrc路径末尾推导,例如image.png;时间解析兼容 ISO 与1713686400.000000Z;创作页主图、历史列表图和结果页参考图继续用ResolvedAssetImage,提交给后端时仍保留原始imageSrc。 - 验证:
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx,并执行npm run check:encoding。 - 关联:
src/services/puzzle-works/puzzleHistoryAsset.ts、src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.tsx、docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md。
拼图历史图关闭 AI 重绘不要强制 Data URL
- 现象:拼图创作页从历史生成图片中选择主图,再关闭 AI 重绘生成草稿时,后端报“上传图必须是图片 Data URL”。
- 原因:历史图
imageSrc是/generated-puzzle-assets/...私有兼容路径;AI 重绘开启时后端参考图分支会解析该路径,但关闭 AI 重绘的“直用上传图”分支旧实现只调用parse_puzzle_image_data_url。 - 处理:关闭 AI 重绘时也复用拼图参考图解析入口,允许 Data URL 与
/generated-*历史路径统一转成PuzzleDownloadedImage后持久化;前端不需要下载历史图再转 base64。 - 验证:
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml、npm run dev:api-server后检查/healthz。 - 关联:
server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/vector_engine.rs、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx。
拼图结果页局部生图不要污染草稿生成态
- 现象:拼图草稿已经生成完成后,在结果页重新生成关卡图片或追加关卡生成图片,草稿页仍显示整卡“生成中”,点击草稿会回到生成过程页,无法查看已有结果;关卡图片生成中还会禁用“新增关卡”和其它关卡详情编辑。
- 原因:结果页局部 action 复用了全局
isPuzzleBusy/ 持久化generationStatus=generating语义,作品架没有区分“初始草稿不可查看”和“已有结果上的局部关卡生成”。 - 处理:作品架只在拼图没有可用封面、首关候选图或任一可查看关卡时才把
generationStatus=generating解释为初始草稿生成;结果页关卡图走 background action,不设置全局 busy,只标记对应关卡局部生成进度;SpacetimeDB/API mapper 读写时把已有图片但状态仍是generating的历史关卡归一为ready。 - 验证:
npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle --manifest-path server-rs\Cargo.toml。 - 关联:
src/components/custom-world-home/creationWorkShelf.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle/mappers.rs、server-rs/crates/spacetime-module/src/puzzle.rs。
2026-05-22 补充:结果页关卡详情的“关卡测试”不能把单关 draft 传给父级再调用 updatePuzzleWork。updatePuzzleWork 会同步 puzzle_work_profile.levels_json 和 source session 草稿,单关快照会把整份多关卡草稿覆盖成一个关卡,退出重进后只剩最后测试的关卡且序号表现为第一关。修复口径是 PuzzleResultView 始终传完整 syncedDraft,额外用 { levelId } 指定起始关卡;父级持久化完整 levels 后调用 startLocalPuzzleRun(item, levelId)。
2026-06-18 补充:结果页点击“新增关卡”只是在本地打开一个空白占位关卡,不应立刻进入自动保存。空白占位如果被写入 /api/runtime/puzzle/works/{profile_id},在作品 profile 投影尚未稳定存在时会触发 update_puzzle_work 404,并且后续 session/draft 回读可能把当前详情弹窗关闭。修复口径是自动保存比较和 payload 过滤掉“后端基线中不存在且完全空白”的本地关卡;用户填写名称、描述、参考图或开始生成后再保存。mergeDraftEditStateWithIncomingState(...) 还要保留本地空白占位,避免 incoming draft 刷新时移除正在编辑的弹窗。
2026-06-18 补充:改造流的 creative_agent 草稿写回会用 puzzle-session-* 派生出的 puzzle-profile-* 调用 update_puzzle_work;如果前置 create_puzzle_agent_session 已写入 puzzle_agent_session,但派生的 puzzle_work_profile 草稿投影缺失,写回会报“拼图作品不存在”。首图生成或结果页保存也可能踩到同一缺口。修复口径是在 SpacetimeDB update_puzzle_work_tx 里只对稳定 puzzle-profile-* 反推同源 puzzle-session-*,确认 owner 匹配、session 未发布且有 draft 后恢复 draft profile,再继续更新;不要在前端重试或凭空创建任意 profile,也不要恢复已发布 session。
拼图上传图关闭 AI 重绘不要走首图生图
- 现象:用户在拼图入口页或结果页关卡详情上传图片并关闭 AI 重绘后,生成页仍显示“生成拼图首图”,或者后端仍调用
generate_puzzle_image_candidates生成第一张 1:1 候选图。 - 原因:上传图直用路径应把 Data URL 或
/generated-*历史图解析后持久化为sourceType=uploaded的正式候选,再继续生成 9:16 关卡画面、UI spritesheet 和纯背景;如果只把aiRedraw=false当作“不参考图片生成”,就会误走首图生成。 - 处理:入口页用 payload 的
aiRedraw写入生成页 metadata,puzzleAiRedraw=false时进度跳过生成拼图首图;后端compile_puzzle_draft和结果页generate_puzzle_images都在aiRedraw=false && referenceImageSrc 非空时走上传图直用候选。结果页关卡详情必须复用CreativeImageInputPanel,不要把正式图当成可重绘参考图;本次上传或历史选择的图才显示 AI 重绘开关并可删除。 - 验证:
npm run test -- src/services/miniGameDraftGenerationProgress.test.ts src/components/puzzle-result/PuzzleResultView.test.tsx、cargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml。 - 关联:
src/services/miniGameDraftGenerationProgress.ts、src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx、src/components/puzzle-result/PuzzleResultView.tsx、server-rs/crates/api-server/src/puzzle/draft.rs、server-rs/crates/api-server/src/puzzle/generation.rs。
Jenkins 数据库导入导出脚本先补 Node 工具链 PATH
- 现象:
Genarrative-Database-Import或Genarrative-Database-Export运行到迁移脚本时,bash报node: command not found,常见在日志里表现为某个sh块内第 61 行直接调用node失败。 - 原因:Jenkins 的非交互 shell 没有自动加载用户的 nvm/profile,数据库导入导出脚本又在 shell 里直接执行
node scripts/spacetime-*.mjs,因此只要 Jenkins agent 没把 Node 的 bin 目录放进 PATH,就会在迁移开始前失败。 - 处理:导入 / 导出流水线在调用迁移脚本前先
source scripts/jenkins-prepare-toolchain-env.sh;该脚本会把GENARRATIVE_JENKINS_TOOL_PATHS、/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin、/var/lib/jenkins/.cargo/bin、/var/lib/jenkins/.local/bin和系统 PATH 前缀统一补齐,并在缺少node时尽早报错。 - 验证:重新跑
Genarrative-Database-Import或Genarrative-Database-Export,日志应先打印jenkins-toolchain的node=...解析结果,而不是在迁移中途报node: command not found。 - 关联:
scripts/jenkins-prepare-toolchain-env.sh、jenkins/Jenkinsfile.production-database-import、jenkins/Jenkinsfile.production-database-export、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Runtime bootstrap secret 原文不能进入 WASM 或发布归档
- 现象:下载 Jenkins Stdb artifact 或检查
spacetime_module.wasm能找到原始 bootstrap secret;Stdb Build / Publish 配了不同 credential,或 Publish 使用的 Secret File 摘要与 release manifest 不一致却仍继续发布;或者 module 已发布、/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt也已更新,但模型定价首次初始化、队列 claim 或钱包调用仍报 identity 未授权。 - 原因:把原文作为 Rust 编译环境变量会进入可下载 WASM;把
migration-bootstrap-secret.txt归档会把构建凭据变成长生命周期 artifact。只把摘要编进 WASM、却不在 release manifest 绑定摘要并让 Publish 重算核对,仍可能把另一份 secret 配给已构建 module。另一方面,AppConfig只在 api-server / worker 进程启动时读取直传值或 FILE,覆盖文件不会更新已运行进程;Full Build 又先发 Stdb、后发 API,不能等待后续 API deploy 才补旧 env。 - 处理:原始 bootstrap secret 固定为 64 位十六进制。WASM 编译只接受
GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256,模块对 procedure 入参原文重新计算 SHA-256 并做常量时间比较。生产 Jenkins Build / Publish 必须使用完全相同的 Secret File credential ID:Build 只计算摘要,WASM、artifact 和copyArtifacts不含原文,Stdb release manifest 记录migration_bootstrap_secret_sha256;Publish 重新读取同一 Secret File、校验 64 位十六进制并重算摘要,与 manifest 强制匹配后才把临时文件路径交给production-stdb-publish.sh。module 发布后安装固定 runtime 文件为root:genarrative 0440、目录root:genarrative 0750,补齐 API / worker env,再在维护模式内重启发布前 active 的 API、controller 和 worker,并执行 API/healthz门禁。人工构建自动生成的原文只放 gitignoredserver-rs/.spacetimedb/build-secrets/<version>.txt,目录0700、文件0600;本地 dev 的 API token 和按 server/database 作用域 secret 也分别持久化为0600文件。日志不得cat或插值打印明文。 - 身份轮换:bootstrap secret 只允许空表首次授权,不能重复接管既有 writer。migration operator 与 runtime writer 必须互斥:operator 不能成为 writer,当前 writer 不能被授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。生产 token 确需轮换时,使用
scripts/deploy/production-runtime-writer-identity-rotate.mjs,由当前已授权 migration operator 登录态双录新 writer identity、填写操作人和原因;procedure 必须拒绝把新 writer 设为当前 writer 或任一 migration operator,并写editor_generation_runtime_identity_rotation审计。成功后先核对审计,再切换 token,不得靠重启 API 隐式改 writer。 - 验证:运行相关部署脚本
bash -n、node --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs scripts/deploy/production-runtime-writer-identity-rotate.mjs、npm run check:production-ops,扫描 artifact 清单和 diff,确认 Build / Publish credential ID 一致、manifest 摘要与 Publish Secret File 匹配,并确认不存在migration-bootstrap-secret.txt、原文编译环境变量、cat或生产 env 明文键,同时验证 64 位十六进制规则及手工 secret / 本地 token 文件权限。 - 关联:
server-rs/crates/spacetime-module/src/migration.rs、scripts/dev.mjs、scripts/build-production-release.sh、scripts/deploy/production-stdb-publish.sh、scripts/deploy/production-runtime-writer-identity-rotate.mjs、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-stdb-module-publish。
Windows Jenkins powershell step 在 Stdb module 构建里曾触发 CreateProcess error=5
- 当前状态:已废弃。
Genarrative-Stdb-Module-Build已切到 Linux agent,不再执行 Windows PowerShell 流程。 - 现象:
Genarrative-Stdb-Module-Build在 Windows Jenkins 节点上报java.io.IOException: Cannot run program "powershell" (in directory "C:\\Users\\DSK\\.jenkins-local\\workspace\\Genarrative-Stdb-Module-Build"): CreateProcess error=5, 拒绝访问。;日志里能看到durable-task已写出powershellWrapper.ps1,但在真正启动裸powershell子进程时失败。 - 原因:Jenkins durable-task 的
powershellstep 依赖一个隐式命令解析/启动路径,在这台 Windows 本地 Jenkins 环境里会被拒绝。powershell.exe本体和 workspace ACL 都是正常的,问题出在 Jenkins step 的启动方式,而不是 PowerShell 脚本内容。修复后若日志能打印[jenkins-powershell] exe:,但随后仅报拒绝访问/script returned exit code 5,通常已经不是 PowerShell 启动失败,而是 Checkout 脚本内部命令在 Windows workspace 里触发权限拒绝。若.jenkins-*.ps1里中文throw '[stdb-build] ...'报MissingArrayIndexExpression,则是 Windows PowerShell 5.1 用-File解析无 BOM UTF-8 脚本时按本地 ANSI 误解码。 - 处理:把
jenkins/Jenkinsfile.production-stdb-module-build的Checkout和Build Stdb Module两处powershellstep 收口成runWindowsPowerShell(...)helper,先用writeFile写出临时.ps1,再用显式powershell.exe把脚本重写成 UTF-8 with BOM,最后通过%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...执行。这个 helper 写在 Groovy GString 里时,PowerShell 的$path/$text/$true必须写成\$path/\$text/\$true,否则 Jenkinsfile 会在 Groovy 编译阶段报unexpected token: true。Checkout 阶段优先复用 Jenkins GitSCM 已完成的工作区结果;COMMIT_HASH为空或已经等于当前HEAD时不再重复git fetch/git checkout/git clean,只有确实要切到另一个指定 commit 时才补 fetch、归属校验和 checkout。 - 验证:检查 Jenkins build log 中是否出现
[jenkins-powershell] user:和[jenkins-powershell] exe:,以及[stdb-checkout] current HEAD:。上游 Full Build 传下来的COMMIT_HASH若已等于当前 GitSCM checkout,日志应显示requested commit already matches Jenkins GitSCM checkout并继续进入构建阶段;同时确认builds/<n>/log不再停在PipelineNodeTreeScanner... Cannot run program "powershell"或 Checkout 内部 exit code 5。 - 关联:
jenkins/Jenkinsfile.production-stdb-module-build、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
Server-Provision Windows 下载 helper 不要原地重写临时 ps1
- 现象:
Genarrative-Server-Provision的 Windows 下载阶段已经打印了[jenkins-powershell] user:和[jenkins-powershell] exe:,但在.ps1原地 BOM 重写前后仍然返回exit code 5/拒绝访问,且下载目录还没创建。 - 原因:Jenkins
writeFile生成的临时.ps1正被同一个 workspace 里的 PowerShell 进程马上重写成 BOM 文件,这个原地改写在本地 Windows Jenkins 环境里比直接脚本执行更容易碰到 workspace 占用或 ACL 拒绝。对这条流水线来说,BOM 不是必须的执行条件。 - 处理:
runWindowsPowerShell(...)改成先writeFile,再由显式powershell.exe读取脚本文本并用ScriptBlock::Create(...)直接在内存中执行,不再对同一个.ps1做 BOM 重写。Windows 下载脚本里先把PROVISION_DOWNLOADS_DIR归一到 workspace 绝对路径,并补Windows workspace/download dir/已创建下载目录三段日志,方便区分是路径问题还是下载问题。 - 验证:Jenkins log 应先出现
[jenkins-powershell] workspace:、[jenkins-powershell] loaded bytes:,再出现[prepare-provision-downloads] Windows workspace:和[prepare-provision-downloads] 已创建下载目录:;如果下载 URL 故意指到不可达地址,应该只在curl 下载失败处结束,而不是卡在 BOM 重写前。 - 关联:
jenkins/Jenkinsfile.production-server-provision、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
SpacetimeDB update installer 不要按带 host 后缀的下载文件名执行
- 现象:Server-Provision 目标机阶段已经显示“使用已下载的 SpacetimeDB Linux update installer”,随后报
Error: unexpected argument '-y' found或前置unknown command name for spacetimedb-update multicall binary。 - 原因:
spacetimedb-update-*不是当前离线交付的最终形态,GitHub release 页面真正可比较的缓存对象是spacetime-x86_64-unknown-linux-gnu.tar.gz这种 release tarball;GitHub release asset API 暴露的是digest/ SHA256,不是 MD5。 - 处理:Windows 下载阶段应直接缓存 release tarball 和
otelcol-contrib_0.151.0_linux_amd64.tar.gz,目标机scripts/prepare-server-provision-tools.sh只解压本地 tarball 生成bin/current/spacetimedb-cli与bin/current/spacetimedb-standalone,不要再把 update installer 当成最终离线包执行。 - 验证:Jenkins 目标机日志不再出现
unexpected argument '-y'、unknown command name for spacetimedb-update multicall binary,后续应继续检查bin/current/spacetimedb-cli和bin/current/spacetimedb-standalone是否生成。 - 关联:
scripts/prepare-server-provision-tools.sh、jenkins/Jenkinsfile.production-server-provision。
清库重建后先查 schema 兼容再重启
- 现象:
npm run dev -- --clear-database --no-interactive之后,api-server 仍在GET /api/creation-entry/config或订阅恢复阶段报No such procedure/ schema guard 失败。 - 原因:本地重建只会重发当前
spacetime-module,不会自动修正旧迁移 JSON 的字段兼容;如果migration.rs没把新字段补成None/ 默认值,清库后重建仍会卡在 schema 同步。 - 处理:先让
server-rs/crates/spacetime-module/src/migration.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md和生成绑定对齐,再执行清库重建。 - 验证:
npm run check:spacetime-schema先通过,再重启npm run dev -- --clear-database --no-interactive,最后检查/v1/ping、/healthz和GET /api/creation-entry/config。 - 关联:
server-rs/crates/spacetime-module/src/migration.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md、scripts/dev.mjs。
QQ 浏览器发现页推荐封面全不显示先查 aspect-ratio 兜底
- 现象:发现页的“推荐”子频道作品卡标题、作者和数据正常,但所有封面图不显示,常见于 QQ 浏览器 / X5 等旧移动内核。
- 原因:公开作品卡封面内部图片是绝对铺满,容器原本主要依赖 Tailwind
aspect-video/ CSSaspect-ratio撑高;旧内核不支持或实现异常时封面容器高度会坍缩为 0。若封面还是/generated-*私有资源,换签失败后没有玩法参考图兜底时会进一步表现成黑卡。 - 处理:
.platform-public-work-card__cover::before使用padding-top: 56.25%保留 16:9 高度,沉浸式卡片单独覆盖比例;公开作品卡通过resolvePlatformWorldFallbackCoverImage(...)给ResolvedAssetImage传入玩法参考图兜底,签名失败或图片加载失败时仍有可见封面。 - 验证:
npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、npm run typecheck、npm run check:encoding。 - 关联:
src/index.css、src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/rpgEntryWorldPresentation.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
公开作品卡作者行不要拼手机号或陶泥号
- 现象:发现页 / 推荐页公开作品卡作者行显示
158****3533 · SY-00000003这类手机号掩码和陶泥号组合,列表卡片看起来像暴露账号标识。 - 原因:
resolvePlatformWorkAuthorDisplayName(...)曾把公开昵称和publicUserCode拼接为昵称 · SY-*,并在无法解析公开昵称时直接回退后端卡片里的authorDisplayName;当后端或旧投影把手机号掩码写进展示名时,卡片会原样外露。 - 处理:公开卡片作者名只取可读公开昵称;识别手机号掩码、单独
SY-*或手机号掩码 · SY-*时回退为玩家。作品号复制、陶泥号搜索和完整身份展示只放在详情页、搜索或明确复制入口,不塞进卡片作者行。 - 验证:
npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformWorkDetailView.test.tsx。 - 关联:
src/components/rpg-entry/rpgEntryWorldPresentation.ts、src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformWorkDetailView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
生成中草稿恢复要按后端时间戳计时
- 现象:拼图或抓大鹅草稿生成中刷新网页后,进入生成页的“已耗时”从
0 秒重新开始;另一类旧问题是后端progressPercent=88时总进度首帧直接跳到88%。 - 原因:生成页恢复曾把展示态
startedAtMs重置为进入页面的当前时间,导致计时不跟随后端真实生成时刻;拼图总进度也曾把后端里程碑当作百分比地板,导致步骤刚切换就抬高总进度。 - 处理:恢复生成中的草稿时,展示起点使用后端 session
updatedAt或作品摘要updatedAt;88/94/96只切换当前步骤,不直接作为总进度地板。总进度按已完成步骤权重加当前步骤内假进度推导,非完成态最多停在98%。 - 验证:
node node_modules/vitest/vitest.mjs run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating"、node node_modules/vitest/vitest.mjs run src/services/miniGameDraftGenerationProgress.test.ts。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、src/services/miniGameDraftGenerationProgress.ts、docs/【玩法创作】拼图生成页进度口径-2026-05-23.md。
生成失败草稿回到作品架不能继续显示生成中
- 现象:拼图生成页已经收到 VectorEngine 图片编辑失败并进入重试态,但用户返回草稿 Tab 后,同一草稿仍显示“生成中”;连续触发多个拼图生成时,失败后还可能只剩一条新增草稿,或者只看到标题为“第1关”的半成品空壳;抓大鹅后台失败时也可能没有任何通知,点击草稿又像重新开始生成。
- 原因:前端失败 notice 只更新生成页局部状态,pending 作品架条目在失败时被清掉或被非
generating状态误映射为ready;后端作品摘要也可能短暂仍是generationStatus=generating。如果失败消息没有写入 notice,用户离开生成页后不会弹出PlatformErrorDialog;如果打开草稿只看持久化generating,就会绕过失败态恢复。 - 处理:失败时按 session 保留 pending 作品架条目并标记
failed,失败 notice 保存错误消息并触发带来源的PlatformErrorDialog;拼图契约没有failed枚举,pending 拼图映射为idle,同时用本地失败 notice 覆盖持久化生成中状态和旧的“正在生成”摘要。点击失败草稿应优先用 notice / 后端 session / fallback payload 组装失败生成页,不能重新从 0 秒启动新进度;失败页点击重新生成必须优先复用当前sessionId执行编译 action,不得因存在表单缓存 payload 就调用 create-session。拼图失败半成品没有有效workTitle时,作品架标题回退为“拼图草稿”。 - 验证:
node node_modules/vitest/vitest.mjs run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed parallel puzzle|background match3d"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/custom-world-home/creationWorkShelf.ts、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
生成失败重试不要走新建草稿
- 现象:拼图或抓大鹅生成失败后,在失败页点击“重新生成”,作品架里多出一份新的草稿,原失败草稿仍留在列表里。
- 原因:重试 handler 曾优先读取缓存的表单 payload 并调用 create-session 路径;失败草稿按 session 留在作品架是正确行为,于是重试动作额外创建了第二份草稿。
- 处理:只要当前失败页还能恢复到原
sessionId,重试就走该 session 的 compile action;只有没有可恢复 session 时,才允许用表单 payload 重新创建草稿。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "failed .* draft retry reuses current session"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
汪汪声浪草稿试玩不要写正式 run
- 现象:如果草稿结果页试玩和发布后 runtime 共用同一写成绩路径,未发布或未确认资源的草稿试玩会污染正式单局、排行榜和作品统计。
- 原因:
BarkBattleRuntimeShell同时承担草稿预览和发布后运行态,需要由调用方显式传入runtimeMode区分是否写正式 run。 - 处理:草稿结果页试玩保持
runtimeMode=draft,只做本地预览;发布成功后先进入/works/detail?work=BB-xxxxxxxx,再从详情页以runtimeMode=published进入正式 runtime,并在开始/结算时分别调用startBarkBattleRun与finishBarkBattleRun。 - 验证:草稿试玩不触发 start / finish run;正式 runtime 必须先通过麦克风授权,再写 start run 和结算派生指标。
- 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx、src/services/bark-battle-runtime/barkBattleRuntimeClient.ts。
汪汪声浪移动端创作表单不要再套一层纵向滚动
- 现象:移动端创作 Tab 里进入汪汪声浪表单后,页面右侧出现不自然的内层滚动条,最后的形象描述输入框容易被“生成草稿”按钮、键盘或底部 TabBar 挤压 / 遮挡;顶部玩法卡首尾也可能贴边显得被裁。
- 原因:外层
.platform-tab-panel已经是纵向滚动容器,创作页中间又有多层overflow-hidden,旧的BarkBattleConfigEditor根节点再加overflow-y-auto,形成外层 Tab 面板 + 内层表单的套滚动;底部按钮只预留 safe-area,不预留真实操作区距离;顶部玩法卡横向滚动条隐藏且首尾没有 scroll padding。 - 处理:移动端让 Bark Battle 表单跟随父级滚动,
lg以上才恢复表单内滚动;创作页容器移动端使用overflow-visible和 safe-area 底部 padding;顶部模板 tablist 加scroll-px-3/ 横向 padding,移动端卡片宽度收窄,避免首尾 ring 和圆角贴边裁切。
统一创作页不要把竖屏滚动锁进内部内容区
- 现象:竖屏打开拼图、抓大鹅或敲木鱼创作页时,浏览器页面本身无法滚动,生成按钮或右侧表单面板落到视口外;木鱼的敲击音效和功德词条看起来像被塞进单独滑动窗口。
- 原因:平台根壳固定一屏并隐藏溢出,
UnifiedCreationPage又使用h-full min-h-0 overflow-hidden和内容区overflow-y-auto,导致滚动责任落到内部内容窗,而不是整个创作 stage。 - 处理:
UnifiedCreationPage统一负责标题、隐藏字段契约、内容包装和页面级纵向滚动;拼图、抓大鹅、跳一跳和敲木鱼的外层motion.div不再额外包overflow-y-auto。各工作台在unifiedChrome下收起旧h-full overflow-hidden外壳,让表单主体跟随统一页面滚动。 - 验证:用竖屏浏览器视口打开
/creation/wooden-fish、/creation/puzzle、/creation/match3d和/creation/jump-hop,统一创作页应可滚动到生成按钮;.unified-creation-page应包含页面级overflow-y-auto,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx、npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。 - 关联:
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
汪汪声浪拟声词不要被默认狗主题锁死
- 现象:创作者把主题或形象改成机甲、猫、骑士等非狗主题后,局内仍播放
轰汪!、汪爆!这类狗叫词,表现像系统强行把主题带回狗。 - 原因:拟声词 textarea 如果一开始就填入默认小狗词池,并且始终作为自定义
onomatopoeia提交,runtime 会优先使用该字段,无法再根据新的themeDescription/playerImageDescription/opponentImageDescription走主题 fallback。 - 处理:
BarkBattleConfigEditor需要区分“系统默认词池”和“创作者已手动编辑”。未手动编辑时随主题 / 形象描述自动重算;手动编辑后才冻结为自定义词池。默认词池只在命中狗相关关键词时加入狗叫词,非狗主题使用科技、幻想或通用高能词。 - 验证:
npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx,并确认非狗主题的拟声词不含汪。 - 关联:
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx、src/games/bark-battle/application/BarkBattleConfig.ts、src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx。
Jenkins Web 构建公开作品号导出缺失优先补 publicWorkCode
- 现象:
Genarrative-Web-Build在npm run build:production-release -- --component web阶段失败,Rollup 报"buildJumpHopPublicWorkCode" is not exported by "src/services/publicWorkCode.ts",但导入方rpgEntryWorldPresentation.ts或PlatformEntryFlowShellImpl.tsx已经引用该玩法公开码函数。 - 原因:玩法分支合并时容易只带入新玩法的
publicWorkCode.ts导出,覆盖或遗漏另一个玩法的公开码 builder / matcher,Vite 构建会在静态导出检查阶段直接失败。 - 处理:在
src/services/publicWorkCode.ts中保持每个玩法的build<Play>PublicWorkCode与isSame<Play>PublicWorkCode成对导出;跳一跳使用JH-前缀和 profileId 后 8 位规范化后缀。补src/services/publicWorkCode.test.ts覆盖 builder 和 matcher,避免后续合并再次丢失导出。 - 验证:
npm test -- src/services/publicWorkCode.test.ts,并用npm run build:production-release -- --component web --name <临时名>复现 Jenkins web 构建路径。若npm run typecheck仍报 JumpHop 阶段或状态变量缺口,那是远端当前 JumpHop 接线未收齐的独立问题,不等同于该 Rollup 导出失败。 - 关联:
src/services/publicWorkCode.ts、src/components/rpg-entry/rpgEntryWorldPresentation.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
跳一跳前端壳层接线不要只合渲染分支
- 现象:
npm run typecheck大量报setJumpHopSession、jumpHopRun、jumpHopGalleryEntries、mapJumpHopWorkToPublicWorkDetail不存在,以及"jump-hop-runtime" is not assignable to SelectionStage;即使 typecheck 过了,分享或刷新/runtime/jump-hop?work=...仍可能掉回首页。 - 原因:跳一跳工作台、生成页、结果页、runtime 和推荐流渲染分支已经合入
PlatformEntryFlowShellImpl.tsx,但平台壳层状态、public detail mapper、SelectionStageunion 与appPageRoutes.ts阶段路由映射没有一并合入;发现页卡片分类也没有先判断isJumpHopGalleryEntry,导致 fallback 访问 RPGthemeMode。 - 处理:
platformEntryTypes.ts必须注册jump-hop-workspace/generating/result/runtime/gallery-detail;appPageRoutes.ts必须补/creation/jump-hop/workspace、/creation/jump-hop/generating、/creation/jump-hop/result、/gallery/jump-hop/detail、/runtime/jump-hop;PlatformEntryFlowShellImpl.tsx必须持有 JumpHop session/work/run/gallery/runtimeReturnStage/generationState/error/busy,并提供mapJumpHopWorkToPublicWorkDetail;RpgEntryHomeView.tsx的公开卡片类型描述要给 JumpHop 单独返回跳一跳。 - 验证:
npm run typecheck,并跑npm test -- src/routing/appPageRoutes.test.ts覆盖 JumpHop 阶段路径。 - 关联:
src/components/platform-entry/platformEntryTypes.ts、src/routing/appPageRoutes.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳地块图集固定走 18 个 UV 大单元
- 现象:跳一跳初始草稿生成时报
系列素材图集的物品行数不能超过 n。,或者生成完成后只有 atlas 预览路径,地块切片没有真正落盘。 - 原因:旧模板先后尝试过通用系列素材 helper、
2x3六格固定 tileType 和5x5单贴图池,但当前跳一跳已经重设计为“主题 -> 一张1024x1536图集 -> 18 个3列*6行UV 大单元 -> 每格4列*3行六面贴图 -> 无限路径”,旧的物品行数 / 固定类型模型都会把创作链路带偏。 - 处理:跳一跳地块固定只生成一张
1024x1536主题 UV 展开图集,后端先切出 18 个大单元,再从每格固定 UV 网切出 top/front/right/back/left/bottom 六张256x256不透明 PNG,并对 108 张面贴图各自走 OSS 上传、asset_object 确认和 entity bind;不要再恢复2行*3列、5x5单贴图、start / normal / target / finish / bonus / accent六格口径。 - 验证:
jump_hop.rs不应再调用通用物品行数模型处理地块图集;公开结果里应能拿到 18 个独立JumpHopTileAsset且每个新资产包含faceAssets六面贴图,运行态无限路径从地块池随机取材;旧资产没有faceAssets时仍能用imageSrc单贴图 fallback。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳宝可梦主题地块图集 safety rejection 只做专项改写
- 现象:跳一跳草稿使用“宝可梦 / Pokemon / 皮卡丘 / 精灵球”等主题时,背景底图和返回按钮可能已生成成功,但地块图集的 VectorEngine 请求返回
Your request was rejected by the safety system,日志里failure_context="跳一跳地块图集生成失败"、status=429、code="invalid_prompt"。 - 原因:18 个立方体主题物体 UV 展开图集 prompt 会把这些词放进“主题物体图集”语境,容易被上游理解为要求生成具体宝可梦角色或标志道具,触发安全拦截;这不是普通平台造型词、抠图或超时问题。
- 处理:仅在跳一跳图片生成 prompt 文本命中宝可梦相关词时做生成侧替换,把
宝可梦 / 神奇宝贝 / 口袋妖怪 / Pokemon改为“原创幻想萌宠冒险道具”,把精灵球改为“彩色冒险能量球”,把皮卡丘 / Pikachu改为“黄色闪电萌宠符号”;不要把所有主题都加全局 IP 禁止约束,用户草稿标题和主题展示也不改。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml应覆盖宝可梦词专项替换;真实联调时同一草稿重试后,地块图集请求的 prompt 不再包含宝可梦相关词。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳地块切片不要按 tileType 复用资产槽位
- 现象:跳一跳生成完成后,运行态看起来仍像在显示默认几何地块,或者地块图片在加载时频闪;结果页地块池也可能只看到少量重复素材。
- 原因:
tileType只是路径平台的玩法类型标签,18 个 atlas 大单元里会重复出现normal / target / bonus / accent等类型。若后端持久化时用tileType生成 slot/path,同类型切片会写入同一个/generated-jump-hop-assets/<profile>/<slot>/image.png,后上传的切片覆盖先上传的切片,前端换签缓存也会读到重复或旧对象。 - 处理:后端切图后必须按 atlas 单元格写入
tile-01到tile-18的唯一 tile slot,并把六面贴图写入tile-XX-top/front/right/back/left/bottom唯一 face slot;前端结果页和运行态展示生成图时用assetObjectId作为refreshKey,避免重生成后复用旧签名或旧图片缓存。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml -- --nocapture应包含jump_hop_tile_asset_slots_are_unique_for_eighteen_slices;前端运行态测试应断言地块换签带assetObjectId刷新键,并覆盖新 UV 资产会解析六张面贴图。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-result/JumpHopResultView.tsx。
跳一跳落点辅助标识不要再用舞台高度常量拍脑袋投影
- 现象:按住蓄力时落点辅助标识虽然会动,但看起来像静态点位漂移,和真实可落地的位置对不上。
- 原因:辅助标识如果只按
stageSize.height和一个固定比例估算投影距离,再去跟拖拽向量合成,就会和当前地块到目标地块的真实屏幕跨度脱节;三维场景层级过高时还会把辅助点直接盖住。 - 处理:辅助标识必须使用当前地块与目标地块之间的真实屏幕距离和后端
chargeToDistanceRatio做投影,再映射到屏幕坐标;它只作为调参验证层随按下显示、松手或取消隐藏,不参与后端裁决和作品配置;同时把辅助层 z-index 放到三维角色层之上,避免被场景层遮挡。 - 验证:半程蓄力时辅助点应落在当前地块和目标地块之间,完整蓄力时应逼近目标地块中心;运行态截图里辅助点必须始终压在地块与角色之上。
- 关联:
src/services/jump-hop/jumpHopRuntimeModel.ts、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx。
跳一跳长按蓄力不能再消费拖拽方向
- 现象:跳一跳改成长按蓄力后,如果前端或后端仍消费
dragVectorX/dragVectorY,玩家手指轻微移动就会改变跳跃方向,和“始终朝下一块中心跳”的体验不一致。 - 原因:历史弹弓拖拽版本把屏幕拖拽方向作为正式裁决输入,契约字段仍为兼容旧客户端保留,容易被误认为仍是当前玩法规则。
- 处理:前端运行态只用长按时长提交
dragDistance兼容字段,不再发送方向字段;落点预测按当前地块中心到下一块地块中心的方向投影。后端module-jump-hop即使收到旧客户端dragVectorX/dragVectorY也必须忽略,只按当前地块到下一块地块中心的单位向量裁决。 - 验证:前端回归测试覆盖手指移动不改变提交方向、预测落点忽略旧方向字段;后端领域测试覆盖旧客户端传错误方向时仍按下一块中心命中。
- 关联:
src/services/jump-hop/jumpHopRuntimeModel.ts、src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、server-rs/crates/module-jump-hop/src/application.rs。
跳一跳创作入口旧文案先查 SpacetimeDB 配置
- 现象:
JumpHopWorkspace已只剩主题输入,但创作 Tab 的跳一跳模板卡仍显示旧的“俯视角跳跃闯关”或拼图参考图。 - 原因:创作入口卡片事实源是 SpacetimeDB
creation_entry_type_config和/api/creation-entry/config,前端只做展示派生;如果只改工作台、PRD 或前端组件,已有库里的旧入口行不会自动变化。当前api-server读取入口配置时优先订阅缓存,缓存命中后不会再走 procedure 播种,所以只把迁移写在get_creation_entry_config里不够。 - 处理:同步更新
module-runtime默认入口种子,并在spacetime-module/src/runtime/creation_entry_config.rs加只命中旧系统默认值的迁移;同时在spacetime-client的入口配置读模型里做同一条旧系统默认行的读路径纠偏。跳一跳当前默认值为subtitle=主题驱动平台跳跃、image_src=/creation-type-references/jump-hop.webp。 - 验证:本地
GET /api/creation-entry/config的jump-hop项应返回新 subtitle 和新 imageSrc;若仍旧,检查本地 SpacetimeDB 是否已发布当前spacetime-module,以及后台是否手动覆盖过入口配置。若缓存路径和 procedure 路径返回不一致,优先怀疑读模型映射没做纠偏,而不是前端展示层。
image2 dry-run 带参考图时不要直接打印 data URL
- 现象:使用 VectorEngine
gpt-image-2-all生成带参考图的概念图时,如果 dry-run 直接打印完整请求体,参考图会被转成超长data:image/png;base64,...,终端日志会被数百万字符淹没。 - 原因:生成请求支持
image数组传入 data URL 参考图;dry-run 如果复用 live 请求体输出,就会把参考图内容完整打印。 - 处理:dry-run 输出摘要,只保留
imageReferenceCount、尺寸、模型和 prompt,不输出完整 base64。live 请求仍按实际需要传image数组。 - 验证:执行
node scripts/generate-edutainment-tv-map-concepts.mjs --dry-run,输出应只显示imageReferenceCount: 1,不出现完整 base64。 - 关联:
scripts/generate-edutainment-tv-map-concepts.mjs、docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md。
生成图资产不能只拼 generated legacy path
- 现象:结果页或运行态拿到
/generated-*-assets/.../image.png后图片不显示;前端ResolvedAssetImage会先调用/api/assets/read-url?legacyPublicPath=...,但换签后的 OSS URL 仍指向不存在对象。 - 原因:后端只写了看起来像生成图的 legacy path,没有真正调用 image2、上传 OSS、登记
asset_object并绑定实体。/api/assets/read-url只负责签名读取,不会凭空生成或补写对象。 - 处理:玩法生成链路必须在
api-server完成外部副作用:调用 VectorEnginegpt-image-2-all,用GeneratedImageAssetAdapter准备PutObject,上传 OSS 私有对象,调用confirm_asset_object和bind_asset_object_to_entity,再把返回的legacyPublicPath写入玩法 profile。 - 验证:
cargo check -p api-server --manifest-path server-rs/Cargo.toml;契约测试应断言前端 JSON 自带的hitObjectAsset会被忽略,spacetime-client 定向测试应断言缺少服务端注入的真实hitObjectAsset时不能编译;浏览器 Network 中 generated 图片应先换签,签名 URL 指向已存在对象。 - 关联:
server-rs/crates/api-server/src/wooden_fish.rs、server-rs/crates/spacetime-client/src/wooden_fish.rs、src/components/ResolvedAssetImage.tsx、src/services/assetReadUrlService.ts。
生成页背景视频要固定全屏并显式触发播放
- 现象:生成页明明带了
media/create_bg_video.mp4,但移动端或某些内核里只看到静态首帧,或视频层跟着局部容器滚动,被白色面板压住后看起来像没加载。 - 原因:仅靠
autoPlay/loop/muted/playsInline并不稳定;视频如果仍挂在局部容器里,还会被页面面板和遮罩吞掉。某些浏览器初始化后也会停在paused=true。 - 处理:背景视频必须放到
fixed inset-0的全屏底层容器里,外层页面用isolate/ 透明底控制叠层;挂载后显式尝试play(),并在loadeddata、canplay和页面聚焦时再次触发,避免只停首帧。 - 验证:移动端视口检查视频
rect应覆盖整个视口,paused应最终变为false,currentTime应持续前进。 - 关联:
src/components/GenerationProgressHero.tsx、docs/【玩法创作】生成页圆环布局口径-2026-05-23.md。
跳一跳结果页直达时不要把恢复面板当成空白页
- 现象:浏览器直接打开
/creation/jump-hop/result,如果没有sessionId、profileId、draftId或workId,页面以前会看起来像空白,容易误判成结果页坏了。 - 原因:跳一跳结果页恢复原先只盯
jumpHopSession.draft,没有把“缺恢复信息”明确兜成可见恢复面板;直达结果页时也没有优先用profileId -> getWorkDetail补回完整作品。 - 处理:
PlatformEntryFlowShellImpl的跳一跳恢复逻辑改成先尝试profileId -> getWorkDetail,再尝试sessionId -> getSession;两者都没有时显示跳一跳草稿未恢复和返回创作,不再留空白页。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route",并手测/creation/jump-hop/result与/creation/jump-hop/result?profileId=<id>两种情况。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/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-xs 到 text-sm 区间。
2026-05-24 补充:生成页“预计等待 / 已耗时”卡片本身已经有标签,传给 GenerationProgressHero 的值只能是纯时间,例如 4 分钟、1 分 15 秒,不要再拼接“预计还需”或“已耗时”;两张时间卡也要和当前步骤卡一样保持半透明。拼图总进度初始帧必须允许显示 0%,不要再用 Math.max(1, nextProgress) 之类的保护把启动态抬到 1%。
2026-05-27 补充:generation-hero-progress-ring-fill 里那个橘黄色小点不是背景噪点,而是 strokeLinecap="round" 在短弧段上的端点;当前圆环口径要求底部 90deg 开口居中对称,因此轨道和填充都应使用 135deg 起点。圆环本体现在固定为 400x400,排查时先看 data-ring-start-degrees、data-ring-fill-start-degrees 和容器尺寸,不要把尺寸伸缩误认成素材渲染问题。
dev:spacetime 启动后 3101 又断开先查 publish 是否被 spacetime.json 干扰
- 现象:浏览器报
Failed to initiate WebSocket connection,目标为ws://127.0.0.1:3101/v1/database/<db>/subscribe,端口检查发现3101没有长期监听;手动运行npm run dev:spacetime可看到 standalone 短暂启动后退出,发布阶段报No database target matches '<db>'。 - 原因:SpacetimeDB CLI 会读取仓库根目录
spacetime.json。如果本地发布命令没有显式--no-config,CLI 可能按配置文件里的 target 解析数据库,覆盖脚本已传入的.env.local数据库名和--server,导致 publish 失败;dev.mjs捕获错误后会清理刚启动的 standalone,于是浏览器看到 3101 被拒绝连接。 - 处理:
scripts/dev.mjs的本地 publish 固定追加--no-config,只使用脚本解析出的数据库名、module path 和实际 SpacetimeDB server。排查时前台运行npm run dev:spacetime -- --no-interactive,若看到该错误,先确认脚本是否仍带--no-config,再查.env.local/spacetime.local.json的数据库名。 - 验证:
npm run test -- scripts/dev.test.ts覆盖 publish 参数包含--no-config;npm run dev:spacetime -- --no-interactive后http://127.0.0.1:3101/v1/ping应保持 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
本地 api-server 启动订阅 401 先查 Web identity token 注入
- 现象:
npm run dev启动到 api-server 恢复认证投影时,日志出现Failed to initiate WebSocket connection ... /v1/database/<db>/subscribe?compression=Brotli: HTTP error: 401 Unauthorized。 - 原因:SpacetimeDB SDK 订阅需要 Web API identity token;本地
.env.local常把GENARRATIVE_SPACETIME_TOKEN留空,只靠 CLI 登录态 publish 成功并不能让 api-server 的 WebSocket subscribe 获得权限。 - 处理:
scripts/dev.mjs在 SpacetimeDB 就绪后优先读取<spacetimeDataDir>/dev-api-identities/<serverSha256>.json;缺失或不可用时才调用/v1/identity创建专用 Web API identity token,并以普通0600文件持久化。token 只注入api-server,不写.env.local、不传 Web / Vite、也不进日志。若仍报 401,先确认是否使用项目脚本启动、记录文件是否因 server 或权限不匹配被重建,以及GENARRATIVE_SPACETIME_SERVER_URL/ 数据库名是否指向本次启动的实例。 - 验证:
npm run test -- scripts/dev.test.ts;重新运行npm run dev后 api-server 启动日志不再出现上述 subscribe 401,/healthz返回 200。 - 关联:
scripts/dev.mjs、scripts/dev.test.ts、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
创作作品架或公开列表异常先查本地 SpacetimeDB schema 漂移
- 现象:本地
http://127.0.0.1:3000/启动后,api-server日志反复出现Host returned error when processing subscription query: no such table: puzzle_gallery_card_view;或创作中心草稿 / 已发布作品整块消失,GET /api/creation-entry/config返回502且 details 为No such procedure。 - 原因:本地
.env.local或spacetime.local.json指向的 SpacetimeDB 库没有发布当前spacetime-module,或当前 CLI 身份无权发布该库;例如旧xushi-p4wfr库缺get_creation_entry_config/puzzle_gallery_card_view,但当前代码的spacetime-client启动时会长期订阅这些公开 read model。 - 处理:先用
spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --server http://127.0.0.1:3101确认目标库是否有当前 view;若只是本地验证,可用 gitignored 的spacetime.local.json指向可发布且已包含当前 schema 的库,例如{"database":"genarrative-dev-codex"}。该 JSON 必须无 UTF-8 BOM,否则scripts/dev.mjs会忽略它。修改后用npm run dev:api-server -- --database <database> --spacetime-port 3101 --api-port 8082 --no-interactive重启。 - 验证:
curl.exe -i http://127.0.0.1:8082/healthz返回200;curl.exe -i http://127.0.0.1:8082/api/runtime/puzzle/gallery返回200;浏览器打开http://127.0.0.1:3000/无puzzle_gallery_card_view控制台或后端日志错误。 - 关联:
scripts/dev.mjs、server-rs/crates/spacetime-client/src/lib.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
创作作品架消失先查入口配置 procedure 与本地库权限
- 现象:寓教于乐或创作中心下草稿 / 已发布作品突然整块消失,
GET /api/creation-entry/config返回502,details 中为No such procedure。 - 原因:本地
.env.local或spacetime.local.json指向的 SpacetimeDB 库没有发布当前spacetime-module,或当前 CLI 身份无权发布该库;例如旧xushi-p4wfr库缺get_creation_entry_config时,前端拿不到入口配置就不会渲染作品架。 - 处理:优先切换到拥有目标库权限的 SpacetimeDB 身份后重新运行
npm run dev完成发布;若只是本地验证,可用 gitignored 的spacetime.local.json指向可发布的本地库。debug 构建的api-server对入口配置缺 procedure 会使用后端默认入口配置兜底,避免作品架因本地库漂移整块空白。 - 验证:
curl.exe -i http://127.0.0.1:8082/api/creation-entry/config返回200且包含baby-object-match;前端草稿页作品架重新渲染。 - 关联:
server-rs/crates/api-server/src/state.rs、server-rs/crates/api-server/src/creation_entry_config.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
抓大鹅物品 spritesheet 偏移先查 alpha 连通域切片是否启用
- 现象:抓大鹅物品图集里大多数素材显示不全、被裁碎、位置整体偏移,甚至切出来像拼贴块。
- 原因:旧链路只按
10x10固定格线裁切,遇到模型输出的透明图集稍有偏移、跨格或留白不均时就会把主体切坏。现在后端优先按透明 alpha 连通域识别真实素材矩形,再按原图从上到下、从左到右排序;只有识别数量不足时才回退旧网格切法。 - 处理:优先检查
generated_asset_sheets.rs的 alpha 连通域切片是否生效,再查item_assets.rs是否还在透传旧的固定格线语义。不要只改前端显示比例。 - 验证:定向测试
cargo test -p api-server generated_asset_sheet_two_items_per_row --manifest-path server-rs/Cargo.toml -- --nocapture应通过,且错位透明样本应按连通域切出完整视图。 - 关联:
server-rs/crates/api-server/src/generated_asset_sheets.rs、server-rs/crates/api-server/src/match3d/item_assets.rs。
腾讯云 release 上 VectorEngine SendRequest 超时先查出口链路与重试
- 现象:release 机器调用 VectorEngine
gpt-image-2的/v1/images/generations或/v1/images/edits偶发client error (SendRequest) -> connection error -> Connection timed out (os error 110),应用层表现为 504;本地通常正常。 - 原因:本地 DNS 可能走代理 / 加速出口,而腾讯云 release 直接解析到 VectorEngine 真实边缘节点。实测同一张约 2.37MB PNG、同一 edits 请求,
curl5/5 成功,但reqwest/hyper会间歇性超时;固定40.160.33.47也只能改善,不能根治。 - 处理:不要优先关闭 multipart,也不要直接把
SendRequest解释成上游业务拒绝。VectorEngine 图片generations/edits上游 POST 单独使用libcurl;参考图下载和响应图片 URL 下载仍用reqwest。send 阶段 timeout / connect error 在platform-image内最多重试 5 次,使用指数退避和短抖动;日志字段attempt、max_attempts、retry_delay_ms、reference_image_bytes_total、request_params是定位依据。
api-server libcurl / OpenSSL 3.2 runtime
- 症状:release 部署新
api-server后服务反复exit-code,LD_TRACE_LOADED_OBJECTS=1 /opt/genarrative/current/api-server或ldd报/lib/x86_64-linux-gnu/libssl.so.3: version 'OPENSSL_3.2.0' not found。 - 根因:
platform-image使用libcurl后,Linux release 构建产物可能直接要求OPENSSL_3.2.0符号;Ubuntu 24.04 apt 默认 OpenSSL 仍是3.0.13,不能满足该符号版本。 - 处理:
Genarrative-Server-Provision独立安装 OpenSSL3.2.0到/opt/genarrative/openssl-3.2.0,并只通过genarrative-api.service的LD_LIBRARY_PATH=/opt/genarrative/openssl-3.2.0/lib64:/opt/genarrative/openssl-3.2.0/lib给 api-server 使用,避免替换系统 OpenSSL。
VectorEngine edits multipart image part
- 症状:拼图参考图链路请求
/v1/images/edits返回500 image is required,但应用日志里reference_image_count=1、reference_image_bytes_total>0,request_params.referenceImages[0]也有field=image、文件名、MIME 和 bytes。 - 根因:Rust
curl::easy::Form中contents(...).filename(...)不等价于文件上传 part;VectorEngine 转码层会认为没有收到图片。release 上用 curl CLI-F image=@file可成功,证明字段名和上游接口本身没变。 - 处理:multipart 参考图必须用
Form::buffer(file_name, bytes)并设置content_type(...),让 libcurl 生成真正的name="image"; filename="..."文件 part。 - 验证:release 上先看
journalctl -u genarrative-api.service中VectorEngine 图片请求发送失败,准备重试与最终HTTP 返回;若仍失败,再用同一图片分别跑 curl 与最小 reqwest 探针对照。 - 关联:
server-rs/crates/platform-image/src/vector_engine/client.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
个人中心不再保留直达“存档”按钮入口
- 现象:2026-05-25 起,移动端“我的”页顶部改为品牌行 + 扫码 / 设置按钮,设置区和次级入口不再提供独立的
存档按钮;用户仍可在“玩过”弹窗里查看可继续存档。 - 原因:产品布局收口后,个人中心只保留设置、扫码、常用功能和条件性次级入口,存档恢复继续以后端
/api/profile/save-archives真相为准,但不再作为页面直达入口。 - 处理:后续如果需要重新暴露存档入口,优先评估是否应回到“玩过”或别的独立弹窗流程,不要默认把存档再塞回常用功能宫格或设置列表。
- 验证:
npm test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile profile page matches the reference layout sections|profile scan action opens camera scanner instead of recharge panel"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、docs/【项目基线】当前产品与工程约束-2026-05-15.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
旧创作入口先确认是不是旧 worktree 在响应
- 现象:浏览器里明明还看到跳一跳旧入口,比如
俯视角跳跃闯关和puzzle.webp,但当前 worktree 里已经改成了主题驱动平台跳跃和jump-hop.webp。 - 原因:本机常同时存在两个开发栈,旧 worktree 可能还在占用
3000/8082/3101/3102,而当前 worktree 可能跑在另一组端口。只看页面文案就下结论,容易把旧进程误认成当前改动没生效。 - 处理:先用
Get-NetTCPConnection/Get-CimInstance Win32_Process确认端口对应的可执行文件和命令行,再分别请求/api/creation-entry/config比对旧端口与当前 worktree 端口。必要时以当前 worktree 的实际端口为准重新打开页面。 - 验证:旧端口返回旧跳一跳入口,当前 worktree 端口返回新跳一跳入口;两边的
api-server/vite-cli命令行应指向不同仓库路径。 - 关联:
scripts/dev.mjs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
3001 无法访问先查旧 worktree 占端口和 SpacetimeDB 版本
- 现象:
http://127.0.0.1:3001/打不开,但3000 / 3101 / 8082仍有进程;npm run dev直接退出,没有把新栈拉起来。 - 原因:旧 worktree 的
api-server、spacetime-standalone和 Vite 还活着,或者当前 worktree 的本机 SpacetimeDB CLI 默认版本低于仓库锁定版本,scripts/dev.mjs会先校验版本再启动并直接报错退出。 - 处理:先停掉占用端口的旧进程,再执行
spacetime version list,确认本机 CLI/standalone 与server-rs/Cargo.toml锁定版本一致;不一致时先直接升级 / 切换到锁定版本,再重新启动npm run dev -- --no-interactive --web-port 3001 --api-port 8083 --spacetime-port 3103 --admin-web-port 3104。 - 验证:
http://127.0.0.1:3001/、http://127.0.0.1:8083/healthz、http://127.0.0.1:3103/v1/ping都返回 200,且进程命令行指向当前 worktree 路径而不是别的仓库。 - 关联:
scripts/dev.mjs、docs/project-memory/shared-memory/pitfalls.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
微信历史孤儿作品不要让新注册账号顶替
- 现象:清空用户数据或迁移历史数据后,旧作品的
owner_user_id为空或失效,新注册用户会因为顺序号复用或旧 ID 残留顶替作品归属,导致刚注册就看到别人的草稿或已发布作品。 - 原因:作品作者解析曾经把缺失作者简单回退到普通登录用户,且微信新用户用户名 / 内部 ID 都太容易被误认或复用。
- 处理:作品作者找不到真实账号时统一回退到占位作者
wx-openid-placeholder,展示名固定为失效作者;微信新用户用户名改为名字_openid,内部user_id改成不可复用的 UUID 风格;离线回填时先识别真实有效用户,再把孤儿作品表写回占位账号。 - 验证:
cargo test -p module-auth --manifest-path server-rs/Cargo.toml、cargo test -p api-server --manifest-path server-rs/Cargo.toml work_author、npm run test -- scripts/rebind-orphan-work-owners.test.ts。 - 关联:
server-rs/crates/api-server/src/work_author.rs、server-rs/crates/module-auth/src/domain.rs、scripts/rebind-orphan-work-owners.mjs。
访客推荐页上下滑不要绑定登录态
- 现象:访客模式进入移动端推荐页后,推荐内容可展示和点击底部“下一个”,但在作品信息区域上下滑不会切换推荐作品,表现为推荐页不能上下滑动。
- 原因:推荐页滑动切换逻辑
beginRecommendDrag(...)误把isAuthenticated作为启用条件;访客态虽然允许浏览和通过底部按钮切换,却无法触发同一套拖拽切换。 - 处理:推荐页拖拽只校验当前是否有作品、多作品可切换以及是否正在提交动画,不再要求登录;登录态相关操作仍由点赞、改造等按钮自身权限控制。
- 验证:
npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx覆盖访客态纵向滑动不弹登录且触发下一条推荐。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx。
Windows junction worktree 下 Vitest 定向路径失败先切真实路径
- 现象:在
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.tsx、src/components/puzzle-clear-result/PuzzleClearResultView.test.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/routing/appPageRoutes.test.ts。
拼消消草稿试玩要和正式 runtime 分流
- 现象:拼消消结果页点击“试玩”后如果仍然调用
/api/runtime/puzzle-clear/runs,草稿试玩会被正式 run 规则和统计约束卡住,公开作品又可能和草稿恢复串台。 - 原因:拼消消既有草稿生成 / 结果页 / 发布闭环,也有正式公开 runtime;如果把结果页试玩和公开运行态复用同一个后端 startRun 入口,
work detail读取路径和统计口径都会混在一起。 - 处理:结果页试玩改走前端本地
runtimeMode=draftsnapshot,只用于草稿试玩和关卡切换,不写正式 run;公开详情和推荐流进入正式 runtime 时才走后端/api/runtime/puzzle-clear/*。客户端读取作品详情时也要区分创作详情/api/creation/puzzle-clear/works/{profileId}与公开运行态详情/api/runtime/puzzle-clear/works/{profileId}。 - 验证:点击拼消消结果页的试玩按钮,不应再请求
/api/runtime/puzzle-clear/runs;公开详情入口仍应能读取后端运行态详情。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/services/puzzle-clear/puzzleClearClient.ts、src/services/puzzle-clear/puzzleClearLocalRuntime.ts、docs/prd/【玩法创作】拼消消玩法模板PRD-2026-05-30.md。
拼消消 runtime 必须继承拼图模板的原生交互基线
- 现象:拼消消卡片在浏览器里会出现原生图片拖拽 / 下载手柄,或窗口拉伸后棋盘和卡片被拉成矩形。
- 原因:拼消消 runtime 早期只继承了“交换 / 消除”的业务逻辑,没有完整继承拼图模板在基础交互上的防护:
touch-none、select-none、aspect-square、draggable={false}、onDragStart(event.preventDefault())、-webkit-user-drag: none。 - 处理:棋盘容器必须保持正方形约束,卡片按钮和内层
<img>都要显式禁用浏览器原生拖拽,样式层也要补user-select: none与-webkit-user-drag: none,不能只靠业务指针逻辑。 - 验证:浏览器中检查棋盘
getBoundingClientRect().width === height,卡片图片draggable="false"且-webkit-user-drag为none;真实拖拽只应进入交换逻辑,不应触发原生图片拖拽。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/index.css、src/components/puzzle-runtime/PuzzleRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消拖拽浮层要挂到页面级 portal
- 现象:拼消消拖拽时图片看起来没有贴在鼠标或手指上,尤其是平台壳层本身带有 transform 时更明显。
- 原因:拖拽 ghost 用了
position: fixed,但如果还挂在会被 transform 的局部容器里,浏览器会把 fixed 当成相对该祖先定位;clientX/clientY读到的是视口坐标,两个坐标系一混就会出现肉眼可见的偏移。 - 处理:拖拽浮层必须通过 portal 挂到
document.body这一层,再继续使用clientX/clientY - pointerOffset计算 left/top;不要把 ghost 留在平台壳或任何会参与 transform 的容器里。 - 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx应断言拖拽浮层父节点是document.body,且 left/top 与按下点偏移一致。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消要继承拼图模板的动作语言,不只是规则
- 现象:拼消消如果只实现“交换后裁决”,但没有开局翻牌、按下留空位、被替换卡快速飞回、以及局部拼接块整体拖动,玩家会直觉上觉得比原拼图更笨重。
- 原因:早期实现容易把“规则独立”误读成“动作语言也要重写”,结果只保留了交换逻辑,没有沿用拼图模板里已经验证过的拖拽反馈、空位让位和合并块连续感。
- 处理:拼消消运行态要继承拼图模板的基础手感:只在开局保留入场翻牌,拖起时源位立即呈空,放下时被替换卡要有明确飞向空位的位移感,连通块要作为整体拖动和整体呈现。
- 验证:浏览器拖拽时能看到跟手 ghost、源位空槽、落点飞入和整组拼接层;
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx应覆盖这些行为。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/index.css。
拼消消空格位必须允许落位,不能当成不可交互死格
- 现象:运行到某一关后,棋盘里出现空格位,用户能看见空洞但拖不进去,也点不动。
- 原因:空格位被前端交互或后端裁决误当成“无效目标”,只保留了交换逻辑,没有把“源卡落入空位、源位清空”当成合法移动。
- 处理:空格位必须保留 button 交互态和落点命中逻辑;前端拖拽 / 点击落到空格时直接提交移动,后端和本地 runtime 都要把源卡移动到目标格并清空源格,不再走失败交换。
- 验证:
npm run test -- src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml player_move_can_drop_card_into_empty_target_cell -- --nocapture。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
拼消消空位落卡后必须立即补位,不能把空洞留成真空格
- 现象:卡牌成功落进空格后,源位仍然留空,玩家会误以为那个格子坏掉了。
- 原因:移动逻辑只处理了“落到空位”,没有在未消除时同步走一遍重力补位,所以源列会短暂或永久留下空洞。
- 处理:只要移动后棋盘存在空位,就立即走补位和可解性修复;这样源位会从顶部准备区补卡,不会留下不可交互空洞。
- 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts、cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml player_move_can_drop_card_into_empty_target_cell -- --nocapture。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
拼消消素材错位先查 sheet 质量门禁
- 现象:一张卡牌切片里同时出现两个或多个错位图案,或空白格、相邻编号区域里混入其他图案碎片。
- 原因:provider 生成的
1024x1536 / 4x6工作表可能违反视觉契约;旧流程只校验布局元数据和切片数量,无法发现图像内容已经主体缺失或污染空白格。边界贴边检测容易把正常铺满主体误判成跨格污染,不能作为高可靠硬门禁。 - 处理:先强化 atlas prompt,要求每个
256x256单元独立查看时只能包含一个主体或同一主体单一局部;服务端在 sheet 切片前做像素级质量门禁,硬拦截非空格前景占比过低和空白格污染,严重多边非同组边界贴边只记录 warning 供排查,不直接让创作失败。硬门禁失败的 sheet 最多尝试 4 次,仍失败则拒绝持久化脏 atlas。 - 追加处理:照片式微场景素材必须把每个
256x256单元收束为一张完整的单场景照片裁片;同编号连续格表示同一视觉家族,不是随机独立小图,要求共享同一场景锚点、主色和道具语言。禁止单格内部出现两张照片、两个不同场景、拼接线、内部竖切、内部横切或左右 / 上下两块不同背景;质量门禁只在单格内部强色差直线贯穿大部分高度或宽度,且两侧都像低纹理人工平铺色块时,按“单格内部疑似拼接线”硬失败并重试 sheet,避免把窗框、桌沿、地平线等自然场景强边缘误杀。 - 追加处理:sheet 生成时如果 VectorEngine 返回
retryable=true的502、504、429或请求超时,例如 nginx HTML502 Bad Gateway,不要立刻把草稿置为 failed,应消耗同一 sheet 的下一次 attempt;仍失败再回写失败状态。 - 追加处理:
sheet-03原本唯一空白格容易被模型画入主题主体,导致第 6 行第 4 列反复报“空白格有主体”并消耗多次 image2 请求。该格改为FILL补位格,允许生成主题小图但服务端切片、atlas 合成和运行态全部丢弃;前端拼消消 action 等待窗口同步提高到 40 分钟,避免上游单图慢返回时用户侧 20 分钟超时。 - 验证:
cargo test -p api-server puzzle_clear --manifest-path server-rs/Cargo.toml -- --nocapture、cargo check -p api-server --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/api-server/src/puzzle_clear.rs、docs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md。
拼消消锁定组覆盖层必须锚定在棋盘本身
- 现象:消除或补牌过程中,局部完成的组图偶尔会看起来从格子里“飘出去”,并且大小会随着窗口和外层面板变化而异常拉伸。
- 原因:锁定组视觉层用了
absolute inset-0,但棋盘容器本身不是position: relative,于是覆盖层实际锚到了更外层的运行态面板,gridColumn/gridRow只能在错误坐标系里排版。 - 处理:棋盘容器必须显式
relative,让锁定组覆盖层、拖拽鬼影和格子坐标都在同一正方形棋盘坐标系内排版;不要把这类覆盖层锚到外层section或整页容器。 - 验证:浏览器里棋盘
getBoundingClientRect()和锁定组覆盖层应共享同一块正方形区域,窗口缩放后组图不应再出现越界或被拉伸的现象;PuzzleClearRuntimeShell.test.tsx需要断言棋盘 class 包含relative。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx。
拼消消中央场地底图必须挂在棋盘内部
- 现象:创作阶段选择了中央场地底图,但运行态消除卡片后只看到浅色格子或空点,看不到底图。
- 原因:底图被渲染成整页氛围背景,并被页面渐变、棋盘面板和格子
bg-white/78遮住;棋盘内部没有静态底图层,空格仍保留不透明卡片底色。 - 处理:
boardBackgroundAsset.imageSrc必须作为puzzle-clear-board内部的absolute inset-0静态底图渲染;空格、消除空位和拖拽源位必须透明或近透明,不能继续使用实体卡片白底。 - 验证:
PuzzleClearRuntimeShell.test.tsx断言puzzle-clear-board-background在棋盘内,/board-bg.png只出现一次,空格 class 包含bg-transparent且不包含bg-white/78。 - 关联:
src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
创作入口突然消失先查前后端是否串到不同 worktree
- 现象:
http://127.0.0.1:3000/可访问,但创作 Tab 里新增玩法入口消失;例如puzzle-clear已在代码默认种子中存在,浏览器仍看不到“拼消消”。 - 原因:Vite 可能来自当前 worktree,但代理目标的
api-server仍是另一个 worktree 的旧进程,或者api-server连到旧 SpacetimeDB 模块;此时/api/creation-entry/config会返回旧入口配置。 - 处理:先用
Get-NetTCPConnection -State Listen -LocalPort 3000,8083,3103结合Get-CimInstance Win32_Process确认端口进程路径;停止串线的旧api-server,再用当前 worktree 的npm run dev:spacetime -- --spacetime-port <port> --database <database>和npm run dev:api-server -- --api-port <port> --spacetime-port <port> --database <database>拉起同一套服务。 - 验证:
GET /api/creation-entry/config应包含目标入口,且监听端口的命令行都指向同一个 worktree;浏览器创作 Tab 对应分类应显示入口卡。 - 关联:
scripts/dev.mjs、.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md、docs/【玩法创作】平台入口与玩法链路-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.mjs、scripts/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.tsx、src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx、src/routing/appPageRoutes.test.ts。 - 现象:新增或扩展
*-generating页面后,生成卡只渲染首帧,已耗时/预计等待停在进入页那一刻不动。 - 原因:平台壳层的共享
miniGameGenerationProgressNowMs时钟没有把新生成阶段纳入 tick 条件,或者该阶段的buildMiniGameDraftGenerationProgress(..., nowMs)没有接入同一时钟。 - 处理:任何共享生成页都要通过平台壳层统一的时钟判断和
nowMs传递刷新,新增生成阶段时要同时补selectionStage判定、useEffect依赖和进度调用点。 - 验证:浏览器里进入对应生成页后,
已耗时/预计等待应持续变化,不应停在首帧。
拼消消要用真实可消除判断,不要把“已相邻”当成可解
- 现象:拼消消开局或补牌后会直接出现已完成的图案组,或者
1x2被当成半锁定局部留在场上。 - 原因:早期把可解性写成“场上已经有同组相邻卡”或“只要有一对相邻同组卡就算可解”,这会把已完成盘面误当成合法盘面;同时半锁定规则没有排除
1x2。 - 处理:开局和补牌后的重排必须先排除现成消除,再用真实交换 / 落位模拟判断是否会产生新消除;
1x2永远不进入半锁定组,半锁定只允许1x3、2x2、2x3。 - 验证:
npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx与cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml -- --nocapture通过后,开局盘面不应直接出现 completed group。 - 关联:
src/services/puzzle-clear/puzzleClearLocalRuntime.ts、server-rs/crates/module-puzzle-clear/src/application.rs。
推荐页作品 key 漏玩法会导致运行内容和标题作者错位
- 现象:移动端推荐页进入跳一跳或敲木鱼等作品时,游戏运行内容已经切到当前作品,但下方标题、作者和头像仍显示第一条拼图或其它推荐作品。
- 原因:平台壳层用
getPlatformPublicGalleryEntryKey(...)写入activeRecommendEntryKey,而RpgEntryHomeView内部的buildPublicGalleryCardKey(...)漏掉新玩法sourceType分支,导致当前 key 查不到条目后回退到推荐列表第一条。 - 处理:推荐页和平台壳层的公开作品 key 规则必须复用
buildPlatformPublicGalleryCardKey(...),覆盖同一批sourceType,至少包括big-fish、puzzle、jump-hop、wooden-fish、match3d、square-hole、visual-novel、bark-battle和edutainment:<templateId>;新增玩法公开推荐流时先补这个共享 helper。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile recommend meta matches active"应覆盖跳一跳和敲木鱼的当前运行内容、标题和作者一致。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
跳一跳飞行动画不要直接用最新 run 重绘地块窗口
- 现象:跳一跳松手后如果后端很快返回下一帧 run,地块窗口会立刻前移,角色翻腾动画看起来像没播放;若同时刷新图片资产,还可能被误认为地块频闪。
- 原因:后端 run 是规则真相,前端 runtime 又需要低延迟表现。如果 DOM 平台层直接用最新
run.currentPlatformIndex渲染,后端回包会抢在动画前完成视觉切换。 - 处理:前端保留独立
displayRun,松手后先进入isJumpAnimating=true,角色在当前显示窗口内飞向前端预测真实落点;视觉预测必须用当前显示窗口的 current/next 地块作为方向来源,不能拿已经提前返回的后端新 run 目标配旧窗口角色,否则下一跳会朝实际目标反方向飞。飞行动画完成后再把displayRun切到最新后端 run,并进入约1440ms的platformAdvancing表现态。成功后的角色显示必须使用lastJump.landedX/landedY映射出的真实偏移,不要吸附到目标地块中心。推进期间地块层和角色层必须统一包在同一个 camera layer 下移动,旧当前地块先跟随相机偏移离开主视野,之后只保留在屏幕后方;不要给旧地块加独立向上 / 向下飞走 keyframes,也不要因为旧地块还在保留列表里阻塞下一跳。玩家继续向前跳时,已完成旧地块继续被新的相机推进自然带离屏幕,超过离屏阈值后销毁。相机层必须同时设置--jump-hop-camera-shift-x与--jump-hop-camera-shift-y,并以旧窗口真实落点和新窗口真实落点为锚点,避免先横向瞬切居中再纵向推进;运行态相机层当前为约1.3x近距缩放。地块保留当前 / 目标 / 预览的深度尺寸差异,但深度差异必须用固定宽高 + CSS transform scale 缓动实现,不能直接改宽高瞬切;当前态不要额外叠 CSS scale。Three.js Sprite 角色与平台共用同一套屏幕坐标投影,DOM 角色只作为 WebGL 或贴图加载失败 fallback;DOM fallback 在相机推进期间自身不能保留left/toptransition,否则displayRun切换造成的角色局部坐标变更会和父级 camera layer 位移叠加,视觉上像落地后又从屏幕外飞回。正式胜负、成功跳跃次数、时长和排行榜仍以后端 run 为准,前端只延迟显示态。 - 验证:
npm test -- src/services/jump-hop/jumpHopRuntimeModel.test.ts src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖动画期间平台仍停在旧窗口,成功落地保留真实落点偏移,动画结束后进入data-platform-advancing=true,角色 Three 帧沿真实预测落点插值并保留飞行弧线,DOM fallback 角色与地块层同在jump-hop-camera-layer内,通过--jump-hop-camera-shift-x和--jump-hop-camera-shift-y完成相机斜向推进,并校验可见地块按深度保留不同视觉尺寸、运行态平台宽高使用固定基准值、推进态 transform transition 为1440ms、推进态 DOM fallback 角色 transition 不包含left/top、旧地块没有独立jump-hop-platform-exit-driftkeyframes 且下一跳不会被旧地块保留态阻塞。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/services/jump-hop/jumpHopRuntimeModel.ts、server-rs/crates/module-jump-hop/src/application.rs。
跳一跳相机推进不要让地块图片回退到原型方块
- 现象:角色落到下一块后,相机推进时旧地块图片突然消失,或新预览地块先露出浅色原型方块,随后真实 image2 切片才出现。
- 原因:旧地块进入 exiting 状态时如果 React key 从
platformId变成platformId-exiting,图片组件会重新挂载并丢失已加载状态;同时JumpHopTileImage曾在真实图片 URL 已存在但onLoad尚未触发时显示 fallback 原型地块。Three.js 平台层接入后,如果隐藏预加载只让浏览器缓存<img>,但没有把未来platformId的纹理 URL 写入platformTextureUrlsByRenderKey,相机推进时新预览地块会短暂缺 Three 贴图;若旧 blob 贴图在空 URL 回调时先被 revoke,再继续保留在 state 中,也会留下一个看似 ready、实际已失效的贴图地址。 - 处理:exiting 地块继续使用稳定
platformIdkey,让旧图片组件在推进期复用;有真实resolvedUrl且未错误时直接保留真实<img>,只在无 URL 或加载失败时显示 fallback;当前 3 块之外的后续地块通过隐藏预加载图片提前解析签名 URL 和浏览器缓存,并同步按未来platformId发布 Three 纹理 URL。Three 平台层在当前 render items 全部有贴图 URL 后继续承接包含 exiting 地块在内的 3D 渲染;退出地块只随相机推进自然离屏,不播放独立飞走动画,避免退出期露出被放大的平面贴图或重复飞多次;贴图 URL 替换必须等新 URL 到达后再释放旧 parent-owned blob,空 URL 回调不得清空或 revoke 仍在活跃 / 预加载 key 上的旧贴图。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/services/jump-hop/jumpHopRuntimeModel.test.ts应覆盖真实 tile URL 不露出.jump-hop-runtime__fallback-tile,并存在jump-hop-tile-preload-image。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx。
跳一跳 Three.js 平台层不能左右镜像 DOM 坐标
- 现象:视觉上下一块地块在角色右侧,但蓄力引导和角色飞行动画朝左侧;后端回包后地块窗口又闪现摆回正确位置,像是先按反方向飞、再由快照刷新纠正。
- 原因:Three.js 平台层如果把相机
up设置成反向,或在 Three 容器上做左右镜像,会让 WebGL 地块的屏幕 X 轴和角色 / 落点预测的屏幕 X 轴相反。规则层仍沿当前地块中心到下一块中心裁决,所以后端快照会把状态纠正回来,表现为跳后刷新。 - 处理:Three 相机保持
up=(0, 1, 0),再用内部投影公式抵消 45° 下压导致的 Y 轴压缩;不要通过反向camera.up解决上下方向。Three.js Sprite 角色、DOM fallback 角色、蓄力引导、落点预测和 Three 平台层必须共用同向屏幕坐标。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/services/jump-hop/jumpHopRuntimeModel.test.ts应覆盖JUMP_HOP_THREE_CAMERA_UP_Y=1,并断言 Three 投影与 DOM 屏幕坐标同向。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx。
跳一跳 Three.js 角色不要被地块透明排序压住
- 现象:角色已经进 Three.js 场景后,看起来像落在地块内部或只露出头,角色没有站在方块顶面上。
- 原因:地块材质如果设置
transparent=true会进入 Three.js 透明物体排序队列,可能在 Sprite 角色之后绘制;同时角色脚点如果仍用固定 Z 高度,遇到标准1x1x1方块放大后的当前块时会落到顶面后方或方块体内。 - 处理:地块贴图材质只使用
alphaTest裁掉透明边,不放入透明材质队列;角色 Sprite 的renderOrder必须高于平台 mesh,脚点 Z 高度按最近方块半高加顶面偏移计算,确保角色站在当前方块顶面上方。 - 验证:
npm run test -- src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx应覆盖平台材质不透明队列、角色 renderOrder 高于地块、角色脚点高度高于方块顶面。 - 关联:
src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md。
跳一跳立方体贴图不要走透明主体切片
- 现象:水果等主题生成成功后,运行态地块看起来像薄的纯水果 PNG、果切贴纸、透明 cutout;或者反过来六个面都是同一张平铺果皮 / 果肉材质,无法组合成方块苹果 / 方块香蕉这类完整主题对象表达。
- 原因:跳一跳地板已经改为 Three.js 标准
1x1x1等比极小倒角立方体复用几何体,运行态视角固定为近距相机和 45° 下压视角;image2 应生成1024x1536的 18 个 cube object UV unwrap,每个大单元内的 top/front/right/back/left/bottom 六面要共同包装同一个主题物体。只强调 full-bleed 容易让水果主题退化成果皮、果肉、叶脉等表面纹理;如果仍把一张图贴给六个面,模型也不需要理解正反和跨面连续特征。旧切图链路若把洋红 key 转 alpha、裁边、只保留最大 alpha 连通主体并补透明安全边,会把整格贴图重新抠成苹果 / 香蕉 / 果切等居中主体,贴到立方体上后四角和侧面都变透明。 - 处理:跳一跳地板图集 prompt 固定要求
cube object UV unwrap atlas / 立方体主题物体六面展开图集,一张图只生成 18 个大单元,每个大单元固定4列*3行UV 网:第 1 行第 2 列 top,第 2 行 left/front/right/back,第 3 行第 2 列 bottom;水果主题要明确生成能一眼说出名称的方块苹果、方块香蕉、方块橙子、方块西瓜等可识别对象,并要求果柄叶片、剥皮条带、放射切面、红瓤黑籽等身份特征跨面连续。禁止自然圆形水果、自然长条香蕉、非方块化完整水果、果切小贴纸、居中小物体、透明背景和留白,同时也禁止“单纯平铺材质 / 抽象纹理 / 只铺主题颜色 / 纯果皮材质 / 纯果肉纹理 / 纯叶脉纹理”。后端先对图集做洋红去背,再以jump_hop_atlas_slicing.rs的自适应 blob+gradient 算法检测 3x6 大单元和单元内六面区域,输出 108 张256x256不透明面贴图;固定 3x6 / 4x3 切片只作为测试对照和必要 fallback 参考,不作为优先生图切图路径。洋红#FF00FF只作为图集安全缝 / UV 空位 / 外圈 key 色;绿色、白色、雪地、云朵、草地、花朵、果肉粉色和浅黄色等主题颜色必须完整保留。 - 验证:
cargo test -p api-server jump_hop --manifest-path server-rs/Cargo.toml -- --nocapture覆盖跳一跳 UV unwrap prompt、18 个大单元、108 张不透明面贴图、绿色 / 白色材质不被透明化、洋红 key 残留不作为透明洞;前端JumpHopRuntimeShell测试覆盖新 UV 资产会解析六张面贴图,旧单贴图资产仍可 fallback。 - 关联:
server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs、server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs、server-rs/crates/api-server/src/jump_hop.rs。
跳一跳 UV 图集切片要防贴边矩形 u32 中间溢出
- 现象:跳一跳草稿在背景、返回按钮和地板图集 image2 都生成成功后,前端报“执行跳一跳共创操作失败”,Vite 代理日志出现
socket hang up,后端日志出现jump_hop_atlas_slicing.rs内attempt to subtract with overflow。 - 原因:blob gradient 切片的 histogram 最大不透明矩形在计算顶部坐标时写成
by0 + ly - sh + 1。当模型输出的 UV 面内容刚好贴到 cell 顶边,数学结果本应是 0,但u32会先执行中间步骤0 - 1并在 debug 运行时 panic。 - 处理:顶部坐标先在局部坐标内用
ly.saturating_add(1).saturating_sub(sh)计算,再加 block 偏移;不要恢复成连写减法。补充贴顶两行不透明矩形回归测试,保证贴边 UV 面不会打崩共创接口。 - 验证:
RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop_atlas_slicing::tests::max_opaque_rect_handles_content_touching_top_edge;整组再跑RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop。 - 关联:
server-rs/crates/api-server/src/jump_hop_atlas_slicing.rs、server-rs/crates/api-server/src/jump_hop.rs。
跳一跳生图切图主路径不要绕过自适应图集切片
- 现象:拉取
fix/jump-hop-image-gen后,如果又把生成链路切回旧固定坐标裁切,容易和该分支解决的 AI 图集偏移、间距不均、UV 面位置漂移问题互相抵消,导致新生图链路的实际收益无法验证。 - 原因:当前跳一跳 image2 prompt 仍要求 3x6 大单元和 4x3 UV 子网格,这是给模型和算法的结构约束;真实生产切图由自适应
SeedRefinement + blob + gradient + max opaque rectangle链路消化 AI 输出偏差。固定网格切片只能验证理想图集,不适合覆盖新分支的主修复。 - 处理:生产生成链路优先调用
slice_tile_atlas_adaptive(...);旧固定slice_jump_hop_tile_atlas(...)只保留为对照测试、实验和必要 fallback 参考。若自适应切图出现具体误切,应优先修正自适应模块的边界检测、主 blob、透明/安全色处理和回归测试,而不是直接全局切回固定坐标。 - 验证:新生成作品下载
tile-01-top/front/right等面贴图时,单图应基本充满对应主题面内容,不应出现大块空背景、相邻面混入或纯色原型 cube;同时执行RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml jump_hop_atlas_slicing -- --nocapture。 - 关联:
server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/api-server/src/jump_hop_atlas_slicing.rs、docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md。
含中文 image2 live 验证不要用 PowerShell 管道喂 Node 源码
- 现象:本地用
@'...'@ | node -跑 VectorEngine / gpt-image-2 live 验证时,request.json里的中文 prompt 可能全部变成????,生成图会变成完全不相关的 UI、建筑海报或其它随机内容,容易误判为模型不服从提示词。 - 原因:Windows PowerShell 管道到 Node stdin 时可能按本机非 UTF-8 编码传输脚本文本,JS 源码里的中文字符串在进入 Node 前已经损坏;Rust 后端真实请求不会走这条编码路径。
- 处理:含中文提示词的 live 验证优先写成 UTF-8
.mjs文件再执行,或使用能确认 UTF-8 的运行入口;执行后先检查本次request.json是否保留真实中文,再判断生图质量。不要基于????prompt 生成的图片调整项目提示词。 - 验证:生成前后检查
request.json,其中prompt字段应显示中文而不是问号;同一提示词在 UTF-8 文件脚本下应能得到符合主题的图。 - 关联:
.codex/skills/gpt-image-2-apimart/SKILL.md、server-rs/crates/api-server/src/jump_hop.rs。
Tauri devUrl 不会自动跟随 dev:web 端口漂移
- 现象:运行
npm run desktop-shell:dev时终端显示主站 Vite 实际启动在10000+端口,但 Tauri 窗口仍加载http://127.0.0.1:3000/,桌面壳表现为白屏、连接失败或加载到旧页面。 - 原因:Linux dev 端口段只把 CLI
--web-port视为显式端口;桌面壳 package script 里的WEB_PORT=3000会被端口段映射覆盖。TauridevUrl是静态配置,不会读取scripts/dev.mjs最终解析出的漂移端口。 - 处理:桌面壳
beforeDevCommand必须使用npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port,让 Vite 实际监听端口和 TauridevUrl一致,并在 3000 被占用时直接失败。若 3000 被占用,先释放占用进程再启动桌面壳,不要依赖 Vite 漂移。 - 验证:
npm run test -- scripts/dev.test.ts -t "Linux 桌面壳显式指定 web-port"、npm run desktop-shell:typecheck、实际启动时终端应显示[dev] web: http://127.0.0.1:3000。 - 关联:
apps/desktop-shell/src-tauri/tauri.conf.json、apps/desktop-shell/scripts/check-config.mjs、scripts/dev.mjs。
Tauri 手动创建主窗口时 devUrl 不会自动套到 index.html
- 现象:
npm run desktop-shell:dev启动后窗口地址显示http://tauri.localhost/index.html或tauri://localhost/index.html,即使 Vite 已经在http://127.0.0.1:3000/正常监听。 - 原因:桌面壳为了注册导航、下载、生命周期和托盘行为,把 Tauri 配置里的主窗口设为
create=false,再在 Rustapp.rs中用WebviewWindowBuilder::from_config(...)手动创建窗口。此时如果只读取app.windows[].url = index.html并补 HostBridge query,手动窗口会沿 release 入口走打包资源协议;Tauri CLI 的build.devUrl不会自动替换这份手动克隆后的窗口 URL。 - 处理:
app.rs在 dev build 下必须先把主窗口 URL 替换为config.build.dev_url,再调用desktop_window_config_with_runtime_platform(...)补写宿主上下文;shell/navigation.rs也必须允许 dev build 下的http://127.0.0.1:3000留在 WebView 内,不要把自己的 Vite 首页当外链交给系统浏览器。release build 保持index.html打包入口。 - 验证:
cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml desktop_main_window_config_uses_dev_url_in_dev_builds desktop_webview_navigation_stays_on_packaged_or_same_origin_pages,实际启动时窗口应加载http://127.0.0.1:3000/...而不是tauri.localhost/index.html。 - 关联:
apps/desktop-shell/src-tauri/src/app.rs、apps/desktop-shell/src-tauri/src/shell/url.rs、apps/desktop-shell/src-tauri/src/shell/navigation.rs、apps/desktop-shell/src-tauri/tauri.conf.json。
Tauri release 的 tauri.localhost 不要交给系统浏览器
- 现象:Windows / release 包启动桌面壳时,系统默认浏览器被打开到
http://tauri.localhost/index.html。 - 原因:release 打包资源在 WebView 内可能表现为
tauri://localhost/index.html、https://tauri.localhost/index.html或http://tauri.localhost/index.html;如果导航白名单只允许tauri:和https://*.localhost,http://tauri.localhost会被误判成普通外链并交给opener.open_url。 - 处理:桌面壳导航策略必须把
http/https的*.localhost都视为 Tauri 内部打包资源,只允许真正外部http/https、mailto、tel走系统浏览器。Windows release 入口还必须使用windows_subsystem = "windows",避免正式包额外弹出控制台窗口;dev build 保留控制台日志。 - 验证:
cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml desktop_webview_navigation_stays_on_packaged_or_same_origin_pages、npm run desktop-shell:typecheck、Windows release 启动时不应打开系统浏览器或控制台窗口。 - 关联:
apps/desktop-shell/src-tauri/src/main.rs、apps/desktop-shell/src-tauri/src/shell/navigation.rs、apps/desktop-shell/scripts/check-config.mjs。
自动试玩退出不要回到生成页
- 现象:拼图草稿生成完成后自动进入试玩,用户从试玩退出或使用系统返回时落回生成进度页,页面还暴露“重新生成”按钮。
- 原因:自动试玩前如果没有先把
/creation/puzzle/result写成/runtime/puzzle的浏览器历史前一站,系统返回会命中旧的生成页历史项;仅靠运行态内部returnStage='puzzle-result'只能覆盖运行态按钮返回,不能覆盖浏览器 / WebView 系统返回。 - 处理:所有“生成完成后自动进入草稿试玩”的分支在
openPuzzleRuntimeStage(...)前都必须调用结果页历史写入 helper,把/creation/puzzle/result与当前sessionId/profileId/workId写入历史;运行态按钮返回到puzzle-result时也同步写回创作恢复 query。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial and runtime back opens draft result"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
推荐页 ready 不能只等主图或首次 DOM 图片
- 现象:移动端推荐页卡面遮罩在作品主图加载后就渐隐,但游戏内 UI 图集、背景、道具图或换签中的 generated 图片还没有准备好,用户会看到运行态半成品或资源闪入。
- 原因:推荐页 ready probe 如果只扫描首次挂载时已有的
<img>,就会漏掉 React effect、/api/assets/read-url换签、spritesheet 解析或后续 state 更新才新增的资源。 - 处理:推荐页 runtime 遮罩必须持续观察运行态 DOM 内新增图片、内联
background-image和data-runtime-resource-pending隐藏标记;各玩法对换签中、解析中的资源源头要暴露 pending 标记,失败后释放标记并交给玩法兜底,避免遮罩永久卡住。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "mobile recommend cover waits for async runtime resources beyond the main image|mobile recommend cover waits until runtime images are ready"。 - 关联:
src/components/rpg-entry/RpgEntryHomeView.tsx、src/components/common/RuntimeResourcePendingMarker.tsx、src/components/ResolvedAssetImage.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
拼图文字直创的 compile 回包不等于生成完成
- 现象:只输入文字点击生成拼图时,页面刚进入生成页就弹出“生成任务已完成,可以继续查看草稿。”,随后又提示“请先选择一张正式拼图图片。”,结果页关卡里也没有图。
- 原因:统一创作表单路径把
compile_puzzle_draft的同步回包无条件当成 ready;但后端在 AI 重绘路径会先返回stage=image_refining、progressPercent=88的会话,只表示首关草稿已编译且后台首图 / UI 资产任务已启动,还没有正式封面或候选图。 - 处理:前端必须继续用
isPuzzleCompileActionReady(...)判断回包 session;没有draft.coverImageSrc、首关coverImageSrc或候选图时保持生成中,不弹完成、不把作品架 pending 标 ready、不自动试玩。生成页轮询合并 session 进度时,未进入编译态或进度无变化就返回原 state,避免轮询制造重复 render。 - 验证:
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle text-only form stays generating|puzzle draft generation auto starts trial|running puzzle draft opens generation progress"。 - 关联:
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
CreativeImageInputPanel 主图点击默认预览
- 现象:复用
CreativeImageInputPanel的结果页 / 编辑页已有主图时,用户点击图片却触发上传,无法直接查看大图;不同玩法若各自手写上传按钮会让主图、历史图、AI 重绘和参考图行为再次分叉。 - 原因:旧主图卡整卡是上传 label,缺少主图预览模式和上传 / 历史入口的显式控制参数。
- 处理:通用面板已有主图时默认点击主图打开全屏预览,上传 / 更换收口到右下角
ImagePlus图标按钮;无图时仍允许点击空图卡上传。调用方用canUploadMainImage和canUseImageHistory分别控制上传与历史按钮,不要复制面板或用样式遮挡按钮。 - 验证:
npm run test -- src/components/common/CreativeImageInputPanel.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx。 - 关联:
src/components/common/CreativeImageInputPanel.tsx、src/components/puzzle-result/PuzzleResultView.tsx、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
项目画布跳转不要先写无参画布路由
- 现象:从
/creation最近项目或/project项目卡进入画布时,浏览器先进入/editor/canvas,随后再进入/editor/canvas?projectid=xxx,导致返回来源页需要点两次。 - 原因:
App传给平台壳的setSelectionStage会按 stage 自动pushAppHistoryPath(resolvePathForSelectionStage(stage));如果项目入口先setSelectionStage('image-editor')再写项目 URL,就会把无参数画布路由塞入 history。 - 处理:项目入口必须先写入最终
/editor/canvas?projectid=xxx,再切image-editor阶段;App的 stage setter 在当前位置已经解析为image-editor时不要再补写基础画布路由。 - 验证:
npm run test -- src/App.test.tsx;浏览器中从最近项目或项目页打开项目后,后退一次应直接回到/creation或/project。 - 关联:
src/App.tsx、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md。
统一创作页短表单软键盘打开不要露出黑底
- 现象:小程序 / H5 移动端点击拼图或敲木鱼创作输入框后,输入框和键盘之间出现一大片黑色区域;H5 还会明显弹一下。跳一跳因为按钮区用
mt-auto撑开页面,看起来没有同样问题。 - 原因:旧移动键盘处理会用
--platform-keyboard-focus-offset把.platform-viewport-shell整体上移;但 H5 浏览器和小程序web-view已会自行处理输入框可见性,二次整体上移会造成页面弹跳并露出body或原生page的黑色宿主底色。统一创作短表单若内容区按短内容收缩,也会放大这个黑底暴露。 - 处理:
UnifiedCreationPage根容器必须保留bg-[image:var(--platform-body-fill)]和overscroll-contain,内容区必须用flex-1 min-h-0占满统一页剩余高度;移动端键盘打开时只记录data-mobile-keyboard-open、隐藏底部 dock、设置键盘 inset 和浅色--platform-keyboard-exposed-fill,不要再对.platform-viewport-shell做全局transform;小程序pages/web-view的page和 web-view class 也要用浅色背景。不要只给某个玩法工作台单独加高度补丁。 - 验证:
npm run test -- src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedCreationWorkspace.test.tsx src/mobileViewportKeyboardFocus.test.ts src/index.test.ts miniprogram/pages/web-view/index.style.test.js;移动端点击拼图、敲木鱼、跳一跳输入框时,页面不应整体弹起,键盘上方应持续显示平台浅色背景。 - 关联:
src/components/unified-creation/UnifiedCreationPage.tsx、src/mobileViewportKeyboardFocus.ts、src/index.css、miniprogram/pages/web-view/index.wxml、miniprogram/pages/web-view/index.wxss、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
小程序订阅消息授权不要依赖 web-view bindmessage
- 现象:拼图点击生成后,H5 以为已经请求了生成结果订阅授权,但小程序没有弹出
wx.requestSubscribeMessage授权框。 - 原因:
web-view bindmessage/wx.miniProgram.postMessage不适合承接“当前用户点击后立刻请求授权”的时序,消息可能等到 web-view 后退、分享或销毁时才派发,导致授权请求没有发生在compile_puzzle_draft前。 - 处理:不要在原生页
onLoad自动触发wx.requestSubscribeMessage,真机会闪页返回且不弹授权框。H5 在compile_puzzle_draft前应先进入生成进度态并立即发起生成 action,再通过微信 JS SDKminiProgram.navigateTo非阻塞跳转到小程序原生订阅页尝试请求授权;用户接受、拒绝或返回都不能阻塞生成。原生页不要改写上一页webViewUrl,否则 web-view 可能重新加载首页并丢失进度页状态。后端发送订阅消息仍只允许在拼图资产成功或失败终态后执行。 - 验证:
npm run test -- src/services/wechatMiniProgramSubscribe.test.ts miniprogram/pages/subscribe-message/index.test.js。 - 关联:
src/services/wechatMiniProgramSubscribe.ts、src/components/platform-entry/PlatformEntryFlowShellImpl.tsx、miniprogram/pages/subscribe-message/index.shared.js、miniprogram/pages/web-view/index.js。
微信订阅消息 time 字段不能用内部时间戳
- 现象:dev 服务器拼图资产生成终态后已经调用订阅消息发送,但日志出现
微信订阅消息发送失败:argument invalid! data.time4.value invalid,用户收不到生成结果通知。 - 原因:微信模板
time字段不接受内部微秒时间戳、秒级时间戳或带Z/ 时区后缀的字符串;发送1713686401.234567Z或类似2026-06-08 08:09:18Z会被微信拒绝。 - 处理:
api-server构造生成结果订阅消息时,time4固定格式化为北京时间YYYY-MM-DD HH:mm;不要复用shared_kernel::format_timestamp_micros。 - 验证:
cargo test --manifest-path server-rs\Cargo.toml -p api-server generation_result_template -- --nocapture;dev 日志中不应再出现data.time4.value invalid。 - 关联:
server-rs/crates/api-server/src/wechat_subscribe_message.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
待解决:跳一跳生成超时后可能后台继续成功
- 风险程度:高。
- 现象:跳一跳生成页可能在
98% 写入正式草稿后报“请求超时,请稍后重试”,但后端仍在继续生成,稍后才把同一 session 写成DraftCompiled=100。2026-06-08 排查jump-hop-session-6db8fa7af57c4fa2a71e6430cc808412时,背景底图 image2 成功但耗时约18分25秒,返回按钮约2分44秒,地板图集约1分46秒,总耗时超过前端 20 分钟等待窗口,最终在前端超时后约 3 分钟写草稿成功。 - 原因:跳一跳创作链路仍把背景、返回按钮、地板图集、切片和 OSS 写入串在一次 HTTP 请求里;VectorEngine image2 单步 timeout/connect 失败会在后端重试,单步耗时可能超过前端总等待窗口。中间资产和真实阶段没有落库,session 在完成前仍显示
Collecting、progress_percent=0,前端只能按时间显示假进度;超时后重试同一 session 时,后端还可能因为 session 没有中间素材而重新从背景开始生成。 - 待处理:将跳一跳生成改为后端任务化 / 可轮询真实阶段进度,按背景、返回按钮、图集、切片、持久化、写草稿分阶段落库;统一后端全局生成 deadline、VectorEngine 重试预算、前端等待窗口和失败态回写。超时后再次进入同一 session 应优先恢复正在运行或已完成的任务,不应重复生图。
- 验证:模拟首张 image2 超长耗时或超时重试时,生成页应显示真实阶段和可恢复状态;前端请求超时不应把最终成功草稿标记为失败;刷新
/creation/jump-hop/generating?sessionId=<id>后应能恢复到后端真实状态;同一 session 重试不得重复生成已完成阶段。 - 关联:
src/services/jump-hop/jumpHopClient.ts、src/services/miniGameDraftGenerationProgress.ts、server-rs/crates/api-server/src/jump_hop.rs、server-rs/crates/platform-image/src/vector_engine/client.rs、docs/【玩法创作】平台入口与玩法链路-2026-05-15.md。
画布生成完成态不能被旧 autosave 覆盖
- 现象:release 外部生成 worker 补跑完成后,生成图已进入素材库或项目资源,但画布生成器仍显示
generating;刷新后可能仍看到历史生成框卡住。 - 原因:画布前端在提交生成后会把
generatinglayout 放入 450ms 自动保存队列;worker 完成后后端会写入idle + generatedLayerId + 生成层,但旧的 pending / in-flight layout save 可能晚到并覆盖完成态。另有历史 inline 请求在 api-server 重启时只留下前端已保存的generating框,没有终态任务或生成资源。 - 处理:前端
applyProjectSnapshot必须取消 pending layout save,并跳过一次由后端快照恢复触发的 autosave;后端save_editor_project_layout要保护已完成的 generation dialog,如果传入旧generating且无generatedLayerId,而当前 layout 已有同一 dialog 的完成态,则保留完成态和生成层。线上脏数据只在确认无任务 / 无资源时标成failed并保留原 prompt 供用户重试。 - 验证:
npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx;cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml editor_project_storage --lib;release 排障用list_editor_projects_and_return/get_editor_project_and_return查generation-dialog状态,不要只看素材库。 - 关联:
src/components/image-editor/useImageCanvasProjectPersistence.ts、server-rs/crates/spacetime-module/src/editor_project_storage.rs、server-rs/crates/api-server/src/external_generation_worker.rs。
Pingora 静态缓存不能只写 Cache-Control
- 现象:直连 Pingora 后,HTML 入口虽然是
Cache-Control: no-cache,但浏览器每次都重新下载完整入口页或普通静态文件;或者 Vite 指纹资源长期缓存正常,但旧标签页刷新时协商缓存行为和 Nginx 直连不同。 - 原因:
Cache-Control只决定缓存策略,不等于条件请求能力。Nginx 静态文件默认会按文件 metadata 提供ETag/Last-Modified,浏览器随后可用If-None-Match/If-Modified-Since得到304;Pingora 自实现静态读取时如果只写 body 和Cache-Control,就会丢掉这层协商缓存。 - 处理:Pingora 静态响应读取文件 metadata,写入弱
ETag和Last-Modified;GET/HEAD命中If-None-Match或If-Modified-Since时直接返回304,不读取或发送 body。HEAD静态响应只读 metadata,仍写正确Content-Length。 - 验证:
npm run check:pingora-gateway-smoke必须覆盖静态HEAD、If-None-Match304、If-Modified-Since304,并用 access log method/path/status 对账证明本地静态边界进入日志证据链;cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml必须覆盖 ETag 构造和匹配 helper。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 静态路径必须按 URL segment 解码
- 现象:dev 页面里部分像素图标加载失败,浏览器直接打开
/Icons/Admurin%27s%20Pixel%20Items/.../499_Iron_Gear.png返回200 text/html,响应体是主站index.html,但服务器磁盘上真实 PNG 文件存在。 - 原因:浏览器请求中的空格和英文撇号会变成
%20/%27;Pingora 静态文件解析如果直接把编码后的 path 当磁盘路径查找,就会错过真实文件,并继续落到 SPA fallback,最终让图片解码看到 HTML。 - 处理:静态路径按
/拆分 URL segment 后逐段 percent-decode;解码后拒绝/、\、NUL、..和非法%编码,既能读取带空格 / 撇号的真实文件,又不重新打开目录穿越边界。 - 验证:
cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml必须覆盖编码空格 / 撇号、%2e%2e、%2f和非法%GG;npm run check:pingora-gateway-smoke必须覆盖编码图标路径返回image/png,并确认危险编码路径仍返回404。dev 切换后用浏览器或 curl 直接验证对应图标 URL 的Content-Type和 PNG magic bytes。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、scripts/check-pingora-gateway-smoke.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
dev health patrol 不能缺少公网 HTTPS 入口配置
- 现象:dev 上
genarrative-health-patrol.timer正常 active,但genarrative-health-patrol.service最近一次运行失败;Pingora 直连彩排状态脚本只因/etc/genarrative/health-patrol.env缺失或 public probe 命中http://127.0.0.1后被 Nginx 301 而报CRITICAL。 - 原因:health patrol systemd unit 的
EnvironmentFile=-/etc/genarrative/health-patrol.env允许文件缺失,脚本会退回默认 public base URLhttp://127.0.0.1;dev / release 的 Nginx 公开入口会把 HTTP 跳到 HTTPS,巡检按非 2xx 判失败。 - 处理:目标机应创建
/etc/genarrative/health-patrol.env,保持GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx,把GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL指向真实 HTTPS 域名,例如https://dev.genarrative.world;Pingora shadow 巡检同时配置GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL=http://127.0.0.1:18081和与/etc/genarrative/pingora-gateway.env一致的 probe token。不要为了让彩排状态变绿把缺 env 降级成 warning。 - 验证:先运行随包
node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs --env-file /etc/genarrative/health-patrol.env --expected-gateway-mode nginx --expected-public-base-url https://dev.genarrative.world --require-empty-public-host,再systemctl start genarrative-health-patrol.service;最后运行node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs --release-root /opt/genarrative/current --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical。 - 关联:
deploy/env/health-patrol.env.example、scripts/ops/production-health-patrol.mjs、scripts/ops/pingora-direct-rehearsal-status.mjs、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 高端口直连演练不要 source env 文件
- 现象:在 dev 上用临时 env 启动高端口 Pingora direct 演练时,shell 报
/tmp/pingora-direct-highport-*.env: line ...: max-age=31536000,: command not found,或者临时演练进程启动后没有按预期监听18443/18080。 - 原因:
pingora-gateway.env是 systemd EnvironmentFile 口径,允许GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutable这类带空格的值;它不是可安全source的 shell 脚本。用 shellsource会把空格后的内容拆成命令或参数。另一个容易误判的点是 Pingora 默认优雅退出窗口较长,停止临时 systemd unit 后可能短暂停在stop-sigterm,即使监听端口已经释放。 - 处理:高端口真实演练优先用临时 systemd unit 启动 current release 的
/opt/genarrative/current/pingora-gateway,通过systemd-run --property=EnvironmentFile=/tmp/<run>.env --property=User=genarrative --property=WorkingDirectory=/opt/genarrative/current ...让 systemd 解析 env;或使用显式安全 env 解析器,禁止直接source。临时 env 要把正式 shadow 端口改到独立 loopback 端口,例如127.0.0.1:18084,HTTPS / HTTP redirect 用127.0.0.1:18443/127.0.0.1:18080,access log 写独立文件。演练结束先systemctl stop <临时unit>,再用ss -ltnp确认高端口已释放;若临时 unit 仍停在deactivating/stop-sigterm且只剩演练进程,可对该临时 unit 执行systemctl kill -s SIGKILL <临时unit>收尾,不要碰正式genarrative-pingora-gateway.service。 - 处理补充:正式
plan:pingora-direct-cutover/check-pingora-release-readiness.mjs --dry-run-cutover --require-direct生成的 runbook 默认读取 active/etc/genarrative/pingora-gateway.env,不会自动使用/tmp候选 env。若只生成了候选 direct env,必须先在维护窗口内把候选 env 提升为 active env,并确认 Nginx 已释放80/443,再执行 runbook 的 direct preflight、enable dry-run 和 enable apply;否则 runbook 第 5 步仍会按 shadow env 报缺TLS_LISTEN、HTTP_REDIRECT_LISTEN、cert/key、FORWARDED_PROTO=https以及 direct-entry capability。不要把候选 env 的 loopback / 高端口预检通过误解为 active env 已满足正式直连门禁。 - 验证:先跑
check-pingora-direct-preflight.mjs --env-file <临时env> --require-live-env --check-cert-readable --check-service-user-cert-readable --check-ports-free --allow-loopback-only;启动临时 unit 后跑check-pingora-direct-live.mjs --https-base-url https://127.0.0.1:18443 --http-base-url http://127.0.0.1:18080 --host <域名> --redirect-host <域名> --redirect-base-url https://<域名> --require-wss-upgrade --pingora-access-log <临时log> --insecure-tls --json,要求OK且direct-access-log matchedCount == checked。收尾后复核80/443仍由 Nginx 监听,正式 Pingora shadow 仍为127.0.0.1:18081。 - 关联:
scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
Pingora 直连接管同 IP 多域名前先确认 Host 和证书覆盖
- 现象:dev 上准备让 Pingora 直接绑定
0.0.0.0:80/443时,只按dev.genarrative.world配置证书和路由会让同 IP 的git.genarrative.world也进入主站 Pingora 路由,Gitea 可能不可访问;即使补了 Gitea Host 路由,如果仍使用只覆盖dev.genarrative.world的单域名证书,浏览器和 Git 客户端访问git.genarrative.world也会遇到证书域名不匹配。 - 原因:Nginx 原来通过多个
server_namevhost 承载主站和 Gitea;当前 Pingora direct listener 默认只有一组 TLS cert/key,且路径路由本身无法区分同一 IP 上的多个域名。 - 处理:direct env 必须配置
GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.world与GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000;TLS 证书必须同时覆盖dev.genarrative.world和git.genarrative.world,并通过pingora-tls-cert-sync.mjs同步到 Pingora 私有目录后再指向 env。不要通过“临时释放 Gitea vhost”把 Gitea 从切换窗口里牺牲掉。 - 验证:本地
npm run check:pingora-gateway-smoke必须覆盖 Gitea Host 整站转发、维护模式不拦截 Gitea Host 和 access logproxy_target=Gitea;dev 切换后除https://dev.genarrative.world/外,还必须验证https://git.genarrative.world/返回 Gitea,HTTP 到 HTTPS redirect 保留正确 Host,Pingora access log 中有host=git.genarrative.world/proxy_target=Gitea。 - 关联:
server-rs/crates/pingora-gateway/src/main.rs、deploy/pingora/pingora-gateway.env.example、docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md。
SpacetimeDB 连接池租约必须有 Drop 兜底,acquire 不允许无界自旋
- 现象:release 上 api-server 周期性出现全量
spacetime_stage="pool_acquire" elapsed_ms=45000业务超时,/readyz503(reason=spacetime_unhealthy, stage=pool_acquire),/healthz仍 200,只有重启能恢复,过若干小时复发。 - 原因:旧
PooledConnectionLease只能显式release_connection归还;HTTP 请求方在等待 StDB 回包期间断开时 handler future 被取消,permit 自动归还但槽位in_use永不复位。后续 acquire 在拿到 permit 后进入无界loop + yield_now扫描空闲槽位,泄漏积累到 pool_size 后整池挂死。 - 处理:租约持有
Arc<SpacetimeConnectionPool>并实现Drop统一复位槽位/归还连接;槽位改AtomicBoolCAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。 - 验证:
cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib(dropped_lease_releases_slot_and_permit、acquire_times_out_at_pool_acquire_when_pool_is_busy)。 - 关联:
server-rs/crates/spacetime-client/src/lib.rs、docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md。
后台灰度配置不能从 SpacetimeDB 本地表缓存读取
- 现象:后台灰度页保存
image-editor:agent-sidebar后当前响应能看到 gate,但刷新后台页列表变空;前台画布 Agent 入口仍显示,0% 灰度没有生效。 - 原因:
feature_gate_config是后台私有事实表,spacetime-client如果优先读 SDK 本地订阅表缓存,可能得到空表并覆盖 procedure 返回后的正确缓存。灰度语义里“未配置 gate”表示不限制访问,所以空列表会让功能继续开放。 - 处理:灰度配置读取必须走
get_feature_gate_configprocedure 的事务快照,成功后再更新进程缓存;缓存只作为 procedure 暂时失败后的兜底。不要订阅或读取feature_gate_config本地表来判断后台配置。 - 验证:
RUSTC_WRAPPER= cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml;RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml frontend_runtime_config_denies_anonymous_agent_sidebar_when_gate_enabled;RUSTC_WRAPPER= cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent_api_returns_service_unavailable_when_sidebar_gate_denies_user。 - 关联:
server-rs/crates/spacetime-client/src/runtime.rs、server-rs/crates/spacetime-client/src/lib.rs、server-rs/crates/api-server/src/frontend_runtime_config.rs、server-rs/crates/api-server/src/editor_agent.rs。
后台灰度新 target 不能继承旧规则
- 现象:管理员先点开一条已有 gate,再从两段式下拉框选择一个尚不存在的新 target,保存后新 gate 可能带着上一条 gate 的启用状态、灰度比例和黑白名单。
- 原因:新 target 分支如果只更新 gate key,会复用当前 React 表单状态;这些字段对运营不可见地跨 target 泄漏。
- 处理:
applyGateTarget进入不存在的新 target 时必须重置为新建态:enabled=false、rolloutPercent=0、allow / deny 列表为空,并使用 target 默认描述。只有显式点已有 gate 才fillForm复制服务端规则。 - 验证:
npm run test -- apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx。 - 关联:
apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx、apps/admin-web/src/pages/AdminGrayReleaseConfigPage.test.tsx。
背景色决策喂 gpt-5-mini 的图不必按阿里云抠图那样归一化
- 现象:担心带图背景色决策把源角色图原样 base64 塞给 gpt-5-mini(
resolve_media_source_as_data_url不做 resize / 字节上限),会像阿里云通用抠图那样因超尺寸 / 超体积被上游拒绝,于是想给决策链路也补一套图片归一化。 - 原因:两条链路的上游限制完全不同。阿里云 SegmentCommonImage 有硬限制(≤3MB、分辨率 <2000×2000、最长边 ≤1999),必须归一化;而 gpt-5-mini(经 VectorEngine
/v1/responses,Responses 协议 +input_image)对图片输入宽松得多,实测远超 App 真实源图范围仍全部 HTTP 200:纯色图到 5000×5000(隔离像素维度)正常识别;噪声图到 base64 请求体 34MB(隔离字节维度,PNG 25.8MB)仍成功返回。App 真实源角色图一般 ≤2048px、几 MB,稳落在安全区。 - 处理:不要给
resolve_editor_screen_background_color的带图路径加图片归一化——那是阿里云抠图链路(platform-matting)专属需求,两者别混。真要加保护也应放在字节 / 像素远高于当前实测通过档(如 base64 >40MB 或长边 >6000px)才截断,避免无谓重编码开销与画质损失。 - 验证:探针脚本
Myscripts/probe_gpt5mini_image_limits.py(本地不入库,逐级放大纯色 / 噪声图打/v1/responses,记录 HTTP 状态与响应)。2026-07-10 实测:solid 512²~5000² 全 200;noise 900²(4.1MB)~2600²(34.4MB) 全 200,无拒绝阈值出现在实用范围内。 - 关联:
server-rs/crates/api-server/src/character_animation_assets.rs(resolve_media_source_as_data_url)、server-rs/crates/api-server/src/editor_screen_background_decision.rs、server-rs/crates/platform-matting/src/lib.rs(对照:阿里云输入归一化)。
不要把 BgFilter segModel 暴露为外部可选参数
- 现象:看到
EditorImageGenerationRequest、EditorIconSpritesheetGenerationRequest和EditorUiDesignAssetExtractionRequest能反序列化segModel,容易认为外部 OpenAPI 也应公开该字段,或让用户在birefnet与anime-seg间自行选择。 - 原因:
segModel是 BgFilter 内部调用链的有效兼容字段,不等于稳定的外部产品契约。当前 BgFilter 服务受进程内存和并发容量约束,不同分割模型的资源消耗不能交给外部调用方控制;任意开放模型切换会让容量规划、超时和故障隔离失去确定性。 - 处理:产品 UI 不提供模型选择,应用内调用固定
birefnet;外部编辑器 OpenAPI 不声明segModel,并保持相关请求 schema 的additionalProperties: false,使外部请求携带该字段时被契约拒绝。只有维护 BgFilter 模型与容量的后端代码可使用该内部字段;若未来需要开放,先完成各模型的内存、并发和超时压测,再明确版本化外部契约。 - 验证:检查
docs/openapi/genarrative-external-v1.openapi.json的图片生成、图标 spritesheet 和 UI 素材提取请求 schema 均未包含segModel,且均保持additionalProperties: false。 - 关联:
server-rs/crates/api-server/src/editor_project.rs、src/services/image-editor/editorProjectClient.ts、docs/project-memory/shared-memory/decision-log.md。
SPA 路由白名单不能只按一级目录放行
- 现象:
/not-exist已返回 404,但/creation/not-exist、/runtime/not-exist或/puzzle/not-exist仍返回 200 首页,搜索引擎继续判定为 soft 404。 - 原因:Nginx 或 Pingora 使用
/creation/*、/runtime/*等宽前缀作为 SPA fallback,前端对未知路径又回到平台首页;只验收根级未知 URL 无法发现该问题。 - 处理:SPA fallback 必须精确匹配当前真实完整路径,同时允许前端已有的大小写归一和尾部斜杠;最终 catch-all 只提供真实静态文件。浏览器 HTML 导航失败时返回品牌
404.html,但状态码仍为 404;API、探针和非 HTML 请求保持原有 404 响应。路由增删同步三套 Nginx、Pingora、route parity matrix 和路由门禁。 - 验证:除全部真实 SPA 路径外,至少检查
/not-exist、/creation/not-exist、/runtime/not-exist和/puzzle/not-exist均返回 404;带Accept: text/html的未知 Web 路径正文命中品牌页,不带 HTML Accept 的请求不得命中品牌页;维护模式仍保持页面 503 优先语义。 - 关联:
src/routing/appRoutes.tsx、src/routing/appPageRoutes.ts、deploy/nginx/、deploy/container/nginx.conf、server-rs/crates/pingora-gateway/src/main.rs。
Jenkins Job UI 参数会被 SCM Jenkinsfile 覆盖
- 现象:在 Jenkins Job 页面给
MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID配了默认值,下一次加载 Declarative Pipeline 后又变空或恢复旧描述;04:00 Full Job 还可能因默认选择pause-after-stdb且 approvers 为空而失败。 - 原因:这些 Job 使用 Pipeline script from SCM,
parameters {}和triggers {}会作为 Job property 回写现场配置;只改 UI 不是持久修复。构建编排如果不显式关闭下游PUBLISH_AFTER_BUILD,还会受下游默认值漂移影响。 - 处理:credential ID 和参数默认值写回三个 Jenkinsfile;仅供开发使用的 dev 定时 Full Job 默认
STDB_API_ROLLOUT_MODE=normal,三路 Build 调用显式传PUBLISH_AFTER_BUILD=false,再由 Full Job 统一按 Stdb → API → Web 发布。Secret 原文只放 Jenkins Secret File,旧 Secret Text 保留给 Import / Export。 - 验证:推送后让 Full / Stdb Build 用不存在的源码分支在 checkout 阶段 fail-closed,让 Stdb Publish 用空构建版本在 Prepare 阶段 fail-closed,以安全刷新参数 schema;随后只读检查三个 live
config.xml的参数描述和默认值,确认 Full timer 仍为0 4 * * *、rollout 默认值为normal,并确认刷新运行未进入 publish / deploy stage。 - 关联:
jenkins/Jenkinsfile.production-full-build-and-deploy、jenkins/Jenkinsfile.production-stdb-module-build、jenkins/Jenkinsfile.production-stdb-module-publish、scripts/check-production-ops-guardrails.mjs。
维护模式内网全站放行不能信任 X-Forwarded-For
- 现象:维护期间希望让内网继续访问整站,如果直接按
X-Forwarded-For: 192.168.x.x放行,公网请求可伪造该头绕过维护闸;如果仍按路径只放行后台,又会让内网主站和普通 API 继续返回 503。 - 原因:XFF 是客户端可提交的普通请求头,当前 Nginx 的
$proxy_add_x_forwarded_for还会保留已有前缀;维护放行属于授权判断,必须建立在不可伪造的网络来源边界上,并在路由分类前按来源统一决定是否绕过维护闸。 - 处理:Nginx 按 TCP
$remote_addr判断内网;Pingora 按 TCP peer 判断,只有 peer 为 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的X-Real-IP。可信内网来源绕过整站维护响应,公网应用主站、普通 API、后台和 SpacetimeDB 路由仍保持维护响应;绝不能用X-Forwarded-For做放行判断。 - 验证:Pingora smoke 同时覆盖公网主站、普通 API、后台为 503,以及内网对应路由为 200;Rust 单测覆盖 IPv4 / IPv6 内网、公网和空来源;Nginx 静态门禁反查两份模板的内网来源定义与全局维护变量清零逻辑。
- 限制:如果发布门禁已经停止 api-server,网关放行后普通 API 和后台 API 仍会失败;需要调用后端时应确保对应服务仍运行,不能把维护页绕过误当作服务可用性保证。
Full 结束后保持维护不能只加一个 UI 参数
- 现象:Full Job 参数页没有“完整发布成功后是否退出维护”选项,或者补了选项后 API readiness 一通过仍自动撤掉维护。
- 原因:维护退出发生在随 API artifact 发布的
production-api-deploy.sh内;Full、API Deploy Job 和脚本任一层没有透传,最终都会回到固定执行maintenance-off.sh。Declarative Pipeline 参数还要等 live Job 加载新版 Jenkinsfile 后才会刷新。 - 处理:Full 使用
EXIT_MAINTENANCE_MODE_AFTER_COMPLETION表达产品选择,Stdb Publish 和 API Deploy 全程固定保持维护,Web Deploy 成功后才进入独立最终退出阶段;API Deploy 的独立KEEP_MAINTENANCE_MODE再转换为脚本--keep-maintenance-mode。API deploy 还必须把production-api-deploy.sh、maintenance-on.sh和maintenance-off.sh从同一 build artifact 复制进 current release,否则 Full 最终阶段即使有选项也找不到随包退出脚本。默认值仍在 Full 结束时退出维护,避免定时 dev 发布行为变化。 - 验证:API deploy fixture 必须覆盖成功发布并保留 marker,还要断言 current release 中三个部署 / 维护脚本存在;生产运维静态门禁同时反查 Full 参数、下游透传、API Deploy 参数和脚本 flag。推送后用 fail-closed 首阶段运行刷新 live Job 参数,再核对
config.xml,不能只看仓库文件。
临时维护公告不能提交进版本化默认页
- 现象:现场已恢复通用维护页,但后续 Web Deploy 或下一次进入维护后,又显示昨天的“今天晚上 HH:MM~HH:MM”公告。
- 原因:
public/maintenance.html会被 Vite 复制进web.tar.gz,Web Deploy 解包后把/srv/genarrative/web指向新制品;临时公告一旦进入该源码,就会成为每次发布都恢复的长期内容。旧维护 on / off 只控制 marker,浏览器缓存不是根因。 - 处理:版本化默认页只保留无日期通用文案;临时公告用
maintenance-on.sh --page-file <公告HTML>安装到/var/lib/genarrative/maintenance/page.html。Nginx / Pingora 优先读取运行态公告,退出维护时同步清理;不要再原地编辑/srv/genarrative/web/maintenance.html或提交临时公告到public/。 - 验证:
npm run check:maintenance-page必须拒绝相对日期、具体日期和具体时间,并覆盖公告安装、同窗口保留、退出清理与新窗口清残留;Pingora smoke 必须证明运行态公告优先且删除后回退默认页。 - 关联:
public/maintenance.html、scripts/deploy/maintenance-on.sh、scripts/deploy/maintenance-off.sh、deploy/nginx/snippets/genarrative-maintenance.conf、server-rs/crates/pingora-gateway/src/main.rs。
遮罩点击关闭必须校验完整指针序列
- 现象:在弹窗内容内按下鼠标,拖到弹窗外的遮罩上松开时,弹窗被误关闭。
- 原因:只在
click阶段判断event.target === event.currentTarget不足以确认用户点击了遮罩;跨弹窗边界松开时,浏览器可能把合成点击的目标归到弹窗和遮罩的共同祖先。 - 处理:共享弹窗统一记录
pointerdown与pointerup的目标,只有按下和松开都发生在遮罩自身时才允许关闭。新增弹窗优先复用UnifiedModal,不要继续复制只判断最终click目标的手写遮罩逻辑。 - 验证:回归测试同时覆盖“弹窗内按下、遮罩松开不关闭”和“遮罩按下、遮罩松开正常关闭”。
- 关联:
src/components/common/UnifiedModal.tsx、src/components/common/UnifiedModal.test.tsx、src/components/auth/PlatformAuthModalShell.test.tsx。
公开作品资产不能用 generated 前缀或 PublicRead 批量放行
- 现象:资产 ACL 收紧后,公开页面读取其他作者作品资产集中返回
404;对象在 OSS 中真实存在,但已登记asset_object.access_policy = private。 - 原因:“作品公开”不等于“作者账号下所有 generated 对象永久公开”。只按 profile / session 关联也会误公开同会话的未选候选图、参考图或生成输入;批量改
PublicRead则无法随作品隐藏、删除或取消发布自动撤销。 - 处理:已登记对象继续保持
private,通过public_work_asset_read_grant只派生Published + visible(custom-world还必须未删除)正式发布快照实际使用资产的匿名读授权。API 必须同时校验 grant owner 与资产 owner 一致,以及asset_object_id或精确object_key命中;明确排除参考图、未选候选图和generationInputs。Custom World 只能扫描角色、地标、营地、章节和 opening CG 等正式根,不能遍历 legacy payload 的未知根。历史作品交给 view 现算补齐,不做永久 ACL 数据补丁。 - 权威查询边界:不能从
asset_object或public_work_asset_read_grant的连接级订阅 cache 推断当前 ACL;池连接水位不一致会让刚撤销的 grant 继续签发 URL,也会让刚公开的作品短暂 404。资产定位和公开授权必须通过受 runtime service identity 限制的 procedure 在同一事务快照中计算,失败时拒绝读取;procedure 先按 asset owner 使用各玩法 owner 索引缩小到该作者作品,再匹配候选asset_object_id/ 精确 key,不能每张图都执行全站公开 view,也不要在每个池连接订阅复制全量 private 资产表。公开派生授权、PublicRead和 legacy 兼容读取的签名 URL 最长 600 秒,owner / admin 不受该公开上限影响。 - Remix 边界:拼图、Custom World 和大鱼现有 Remix 会把源资产引用复制到新 owner,但没有持久化不可伪造的资产来源。不得因此放宽跨 owner grant;源作品隐藏后仍公开的 Remix 资产,需要后续通过 Remix 时复制资产或持久化 provenance 解决。
- 验证:资产 owner 本人仍可读;公开可见作品的正式资产可匿名读;跨 owner、只命中前缀、参考图、未选候选图和
generationInputs仍返回不存在;作品隐藏、删除或取消发布后 grant 消失。 - 关联:
server-rs/crates/spacetime-module/src/public_asset_access.rs、server-rs/crates/spacetime-client/src/assets.rs、server-rs/crates/api-server/src/assets.rs、docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。
iOS 退款问询的 result_code 不是 debug 状态
- 现象:为了先观察真实 iOS 退款通知,回调返回
ErrCode=0 + IosRefundQueryResponse.result_code=1,并把 evidence 写成“调试阶段不执行自动退款决策”,看起来像安全 ACK,实际已经向微信建议拒绝退款。 - 原因:
xpay_subscribe_ios_refund_query_notify只有result_code=0(建议退款)和1(建议拒绝)两种正式决策;evidence必须是可审计的履约或消耗事实,不存在中立调试值。与此同时,解密后的完整 payload 含 OpenID、Apple 交易号、退款原因和票据,不能为了排障直接落日志。 - 处理:未接入真实履约决策时返回非零
ErrCode让微信重试,不携带IosRefundQueryResponse;所有事件只写脱敏结构化摘要,payload、未知事件/字段、标识符和字符串值使用消息 Token 加用途域派生的稳定 HMAC 引用,自由文本只写长度和 HMAC 引用。Android 订阅成功和普通 goods 通知可能同形,payload marker 只能快速分流;只有存在同号wechat_mp_virtual本地充值订单,并在 2.5 秒内通过/xpay/query_order校验订单号、金额、order_type=0/7、支付状态和权威paid_time,才允许入账。Apple 通知缺少WeChatPayInfo.PaidTime时走查单,绝不能用本机时间补齐。 - 验证:
cargo test -p platform-wechat virtual_payment_debug_summary --manifest-path server-rs/Cargo.toml、cargo test -p api-server virtual_payment_debug_routing --manifest-path server-rs/Cargo.toml、cargo test -p api-server virtual_payment_ios_refund_query_has_no_fake_decision_response --manifest-path server-rs/Cargo.toml。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、docs/【技术方案】微信虚拟支付接入-2026-05-26.md。
微信支付 V3 的支付 notify_url 不会自动接收退款结果或发现全部手工退款
- 现象:普通微信支付成功回调已经配置并可达,但在商户平台或代码里发起退款后,
/api/profile/recharge/wechat/notify收不到退款单状态变化;商户平台手工退款也可能没有请求本系统的退款回调入口。 - 原因:V3 支付成功通知与退款结果通知是不同契约;代码发起退款时,退款通知地址来自每次
POST /v3/refund/domestic/refunds请求里的notify_url,支付下单使用的WECHAT_PAY_NOTIFY_URL不会自动复用。商户平台手工退款不能假设会携带本系统按 API 请求传入的回调地址;退款接口返回成功也只表示受理,不能当成退款终态。 - 处理:代码退款显式传入公网
https://<API 域名>/api/profile/recharge/wechat/refund-notify,并用稳定out_refund_no串联申请、重复通知和主动查单。回调先用原始 body 验签、检查正负 5 分钟时间窗,再用 APIv3 密钥解密;校验事件、资源类型、商户号和退款状态后,将 callback observation 写入统一 SpacetimeDB 事务,持久化成功才返回204。正式链路不再是“debug 只记日志”:部分 / 全额退款、泥点回收、欠款冻结和会员人工复核均由事务收口;未知事件、校验或持久化失败返回微信FAIL响应。另开启WECHAT_PAY_REFUND_RECONCILIATION_ENABLED=true,对order_missing / order_not_paid继续等待晚到支付通知,候选退款按分钟轮转分页且错误日志不回显 provider URL;次日 10 点后按分片补扫微信 API 可查询的近 90 天bill_type=REFUND交易账单,并在落账前再主动查单。单行失败不能阻塞其他行或日期,也不能提前写完成 checkpoint;昨日NO_STATEMENT_EXIST至少延迟到次日 10 点后再确认;不要为联调开放未鉴权公网退款或补录接口。 - 验证:
cargo test -p platform-wechat v3_refund_notify --manifest-path server-rs/Cargo.toml、cargo test -p platform-wechat v3_transaction_notify --manifest-path server-rs/Cargo.toml、cargo test -p api-server v3_refund_notify_failure --manifest-path server-rs/Cargo.toml;真实联调后只读核对profile_recharge_refund、profile_recharge_refund_observation、profile_recharge_order_refund_settlement和profile_recharge_refund_bill_checkpoint。 - 关联:
server-rs/crates/platform-wechat/src/pay.rs、server-rs/crates/api-server/src/wechat/pay.rs、server-rs/crates/api-server/src/app.rs、docs/【技术方案】微信虚拟支付接入-2026-05-26.md。
已 ACK 的历史退款通知不会因正式落账上线而自动重放
- 现象:微信侧退款已经是
SUCCESS,旧 debug 回调也曾返回204,但部署正式退款表和权益回收事务后,本地充值订单仍为paid,退款表没有记录。 - 原因:微信收到成功应答后会把该次通知视为已送达;服务升级不会让已经 ACK 的历史通知自动重放。主动 reconciliation 只能继续查询本地已经知道
out_refund_no的非终态退款,不能凭空枚举所有历史退款。 - 处理:已知
out_refund_no时,由持有真实商户凭据的受控服务端先调用单笔退款查询,验微信响应签名后写入统一 observation 事务;未知的商户平台退款等待 T+1REFUND交易账单发现,再查单落账。自动账单按分片补扫微信 API 可查询的近 90 天,超过窗口的数据需从商户平台导出候选后逐笔受控查单。禁止用 SQL 直接把订单改为refunded,也禁止直接插入退款表或按账单 CSV 状态扣泥点,这些做法会绕过不可变字段冲突校验、累计部分退款和权益结算。 - 验证:核对退款 observation 的
source、resolution_code与金额,再核对订单级 settlement 的累计退款、recovery_status、unrecovered_points和wallet_frozen;全额退款应保留原订单paid_at,防止错误恢复首充资格。 - 关联:
server-rs/crates/api-server/src/profile_recharge_refund_reconciliation.rs、server-rs/crates/spacetime-module/src/runtime/profile.rs、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md。
退款请求结果未知时不能立即释放钱包占用
- 现象:后台调用微信退款超时或连接中断,页面提示状态未知;如果服务端立即释放泥点占用,用户可以继续消费,而微信稍后仍可能完成退款,最终形成可避免的退款欠账。反过来,永久保留占用又会让一次明确未创建的退款长期冻结余额。
- 原因:HTTP 错误只能说明客户端没有拿到确定响应,不能证明微信没有受理;单次退款查单
RESOURCE_NOT_EXISTS也可能处于短暂传播窗口。只有使用原out_refund_no主动查单才能继续判定。 - 处理:网络结果未知时保留活动 hold,页面把原
requestId、订单、金额、原因和已知out_refund_no保存到当前后台标签页、当前管理员会话隔离的sessionStorage,有效期 2 小时;刷新后必须与后端 active hold 的金额、原因和退款号对账一致才允许直接复用原请求,服务端明确拒绝时清理上下文。没有原请求上下文、上下文过期或管理员会话已切换时只开放预填out_refund_no的安全查单登记,不生成新 ID 硬撞活动 hold。worker 在占用创建至少 10 分钟后查同一退款号,查到退款就将验签事实写入统一 observation,只有连续 3 次查单收到官方RESOURCE_NOT_EXISTS才释放。进程重启清空连续次数并重新观察;超时、签名、配置、解析等错误一律重置次数并继续占用。 - 补充:退款查单适配器必须保留微信
RESOURCE_NOT_EXISTS业务码,并兼容同类ORDER_NOT_EXIST,不能把所有非 2xx 都抹平成通用上游错误;签名有效的退款申请/查询响应仍须与本次out_refund_no、订单号、交易号和金额做关联校验。已释放 hold 复用旧requestId时必须在调用微信前拒绝,并要求重新预检生成新的请求 ID。 - 关联:
server-rs/crates/api-server/src/admin_recharge.rs、server-rs/crates/api-server/src/profile_recharge_refund_reconciliation.rs、profile_recharge_refund_hold。
时段 UV 不能伪装成单日趋势
- 现象:访问人数卡显示整段时间有数百人,但趋势图只有终止日一根满柱,其余日期是同样高度的小短柱;横向滚动后还容易误以为后半月突然出现访问。
- 原因:把时段跨日去重 UV 通过单值 series 塞进
anchor_date_key,再对 0 值强制设置最小可见高度。时段 UV、每日 UV 和每日 UV 之和是三个不同指标,不能互相替代。 - 处理:趋势 bucket 按
day_key + user_id每日去重,图头单独使用整个筛选范围跨日去重人数;0 值高度必须为 0。多张同轴图使用同一日期范围并同步横向滚动,快捷范围不生成未来日 bucket。 - 验证:构造同一用户跨两日访问与某日零访问的 fixture,断言每日 bucket、时段去重总数和零值柱分别正确;浏览器核对四图首尾日期窗口一致。
运营聚合不能用固定 LIMIT 的原始事实冒充精确结果
- 现象:Dashboard 出现“达到单次读取上限 50000 行”告警,但模块分布和累计值仍以无“不完整”标识的精确数字展示;数据增长后结果会随任意截断样本漂移。
- 原因:api-server 拉取
SELECT ... LIMIT 50000原始事实再聚合,没有完整分页、稳定排序或数据库侧聚合。提高上限只会推迟错误,并增加响应体与内存压力。 - 处理:精确运营指标通过受 runtime service identity 限制的 SpacetimeDB procedure 在事务内聚合,只返回紧凑统计投影,再由
spacetime-clientfacade 交给 BFF。权威聚合失败时请求必须失败,不能用默认 0 代替未知值;现有索引无法覆盖跨 scope、跨日期统计时先监控事务扫描耗时,数据增长后补日期前缀索引或持久化日聚合事实,不能退回固定LIMIT。若某查询只能采样,契约和 UI 必须明确标为采样,不能展示成精确值。 - 验证:聚合结果不随 HTTP SQL 行上限变化;超过 50,000 条事实时仍无截断告警,并用数据库事实抽样对账每日、时段、累计与分布结果。
后台详情列表的 grid 规则不要命中嵌套身份组件
- 现象:素材查询或精选素材详情弹窗中的作者陶泥号被逐字符竖排,用户详情按钮也被挤到编号旁边;窄屏下图片与详情列继续互相挤压。
- 原因:
.admin-info-list div会命中列表内所有后代div,把字段值内部的.admin-inline-identity和昵称容器也覆盖成双列 grid;陶泥号又允许任意位置换行,最终只剩单字符宽度。素材详情布局若始终固定为220px + 信息列,移动端也没有足够空间。 - 处理:信息列表的行布局只使用直接子选择器
.admin-info-list > div;作者昵称与陶泥号在身份组件内分行,陶泥号保持单行并在真正不足时省略。560px以下的素材详情改为单列,缩略图居中;素材查询与精选审核共用该规则。 - 验证:在桌面、560px、390px 和 320px 浏览器宽度打开素材详情,确认
.admin-inline-identity的 computeddisplay为flex、陶泥号横向显示、详情字段不溢出页面。