Files
Genarrative/.hermes/shared-memory/pitfalls.md
五香丸子 d41f260a2a
Some checks failed
CI / verify (push) Has been cancelled
feat: add baby object match edutainment flow
2026-05-12 16:08:59 +08:00

536 lines
72 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 踩坑与排障记录
> 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
## 记录格式
```md
## 问题标题
- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue
```
## 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`
## 中文乱码与编码风险
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
- 原因Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
- 处理:
- 不要直接沿用乱码文本。
- 不要用英文替换中文,除非用户明确要求翻译。
- 在 PowerShell 5.1 中显式使用 UTF-8。
- 优先用 Python/Node 或 `Get-Content -Encoding UTF8` 核对原文。
- 修改中文文件时优先局部补丁,避免无关内容重写。
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:`AGENTS.md``npm run check:encoding`
## 忘记密码后仍提示手机号或密码错误先查认证快照同步
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 `Bearer JWT 版本已失效`,日志里的 token version 与本地快照不一致。
- 原因:重置/修改密码会更新 `password_hash``password_login_enabled``token_version`,如果 API 层只更新本地 `InMemoryAuthStore`,没有调用 `sync_auth_store_snapshot_to_spacetime()``api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态。
- 处理:`POST /api/auth/password/change``POST /api/auth/password/reset` 成功后必须同步认证快照;启动恢复时从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照,本地文件更新时尝试回写 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`
## `.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`
## 宝贝识物选篮误触发先查多套判定和残余轨迹
- 现象:`宝贝识物` 运行态打开礼物盒或反馈结束后,当前物品被连续送入左侧或右侧篮子,或硬件动作名偶发命中导致未做明确横移动作也触发选篮。
- 原因:选篮如果同时消费 `wave_left_hand` / `wave_right_hand` / `wave` 动作名和手部轨迹,或在 `correct` / `wrong` 反馈阶段继续累计手部路径,会把抓握、反馈期间残留移动或未知侧别手部误算成下一次选篮。
- 处理:宝贝识物选篮只使用明确 `leftHand` / `rightHand` 的连续横向轨迹阈值;侧别为 `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`
## 儿童动作 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-all` 生成,脚本只读取 `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、状态条、开始面板或底部地板就会出现变形和层叠观感。
- 处理:使用 v2 用途专属资源:`picture-book-foreground-grass-v2.png``picture-book-ground-ring-v2.png``picture-book-character-outline-v2.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 / 状态条 / 开始托盘分别引用各自资源。若只需修透明裁切或品红边,运行 `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`
## GPT-image-2 不再读 APIMart 图片配置
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY`RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`
- 原因2026-05-09 后 GPT-image-2 图片生成已切到 VectorEngine `gpt-image-2-all`APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认路径为 `/v1/images/generations`、模型为 `gpt-image-2-all`、参考图字段为 `image`
- 验证:运行 `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/puzzle-agent/PuzzleAgentWorkspace.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
- 现象Network payload 已带 `referenceImageSrc`,但 VectorEngine 生成结果仍明显不像上传图。
- 原因:`gpt-image-2-all``/v1/images/generations` 更适合纯文生图;有参考图且需要重绘时应切到 `/v1/images/edits` 的 multipart 图生图接口。
- 处理:`referenceImageSrc` 存在且 `aiRedraw = true` 时直接走 editsprompt 仍保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。
- 验证:后端单测应覆盖 `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`
## 旧后端路线文档造成判断漂移
- 现象:开发时参考到 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`;涉及表变化时同步 `migration.rs``SPACETIMEDB_TABLE_CATALOG.md` 和 bindings必要时走 JSON 导入导出与分片导入迁移流程。
- 验证:发布前完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md``docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
- 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`
- 原因SpacetimeDB module 的 wasm32 构建树被间接带入原生/网页依赖;已验证链路是 `reqwest -> platform-oss -> shared-contracts -> module-runtime -> spacetime-module`,由共享契约默认启用资产 OSS 契约触发。
- 处理:让 `shared-contracts` 的 OSS 资产契约走 `oss-contracts` featureworkspace 根依赖保持 `default-features = 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:rust` 或从 `server-rs` 目录执行显式 `--server``spacetime publish`。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。
- 验证:`spacetime list --server http://127.0.0.1:3101` 能看到目标库;重新发布不再使用无权限 identity。
- 关联:`scripts/dev-rust-stack.sh``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 或旧本地库。
- 原因:本机 `spacetime` CLI 保存的旧 token、默认 server、正在运行的 standalone 进程或默认 local 数据库与当前发布身份不一致。
- 处理:确认只是本地测试库且数据可丢弃后,先查看并停止本地 `spacetimedb-standalone`,执行 `spacetime logout`,确认并设置 `spacetime server set-default local`,停 server 后用 `spacetime server clear -y` 清空默认本地库,再 `spacetime start`,另开终端执行 `spacetime login --server-issued-login local`,最后用 `spacetime publish --server local A` 或项目脚本重新发布。
- 验证:`spacetime server list` 默认目标为 local重新登录后发布不再返回 `401` / `403``npm run dev` 可以完成 SpacetimeDB publish 并继续启动 `api-server`
- 关联:`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md``scripts/dev-rust-stack.sh`
## 本地 SpacetimeDB 联调可按阶段跳过宿主或发布
- 现象:本地 `npm run dev``3101` 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 `spacetime-module` 语法而被完整联调链路拖慢。
- 原因:`npm run dev` 默认同时启动 SpacetimeDB standalone、发布 `server-rs/crates/spacetime-module`、启动 Rust `api-server`、主站 Vite 与后台 Vite并非每个阶段都需要完整重启和重新发布。
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 `3101`,被占用则输出占用进程并选择最近可用端口,保证 publish 与 `api-server` 都连接同一个实际 SpacetimeDB URL。`api-server` 启动前也会检测 `8082` 并选择最近可用端口。Windows / Git Bash 下不要用 `tr/head/xargs` 管道读取 `spacetime.pid` 或 URL 记录,脚本应使用 Node 读取并短重试,避免 `tr: read error: Device or resource busy`;未修改 `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``3101``8082` 被其他进程占用时,脚本输出占用进程并使用最近可用端口;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。端口漂移时控制台会打印 `[dev:ports] ... 不可用,改用 ...`,后续 `[dev:rust] web/admin web/rust api/spacetime` 地址应与实际端口一致。
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md``scripts/dev-rust-stack.sh`
## 本地 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`
## 反馈页清空 file input 前必须先拷贝 FileList
- 现象:点击上传凭证会打开文件选择框,但选择图片后页面没有展示预览,提交时也没有携带图片凭证。
- 原因:浏览器传入的 `FileList` 可能跟 `<input type="file">` 保持 live 绑定;如果先执行 `input.value = ''`,再从参数里的 `FileList` 读取文件,列表可能已经为空。
- 处理:在清空 file input 前先执行 `const selectedFiles = files ? Array.from(files) : []`后续图片类型、大小、Data URL 读取和预览都基于这个普通数组。
- 验证:`PlatformFeedbackView.test.tsx` 用 mock `FileReader` 断言选择图片后出现 `反馈凭证预览`,且提交 payload 带 `evidenceItems[].dataUrl`
- 关联:`src/components/platform-entry/PlatformFeedbackView.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 api-server` 读取 env 的顺序必须让 `.env.secrets.local` 最后覆盖
- 现象:`POST /api/assets/hyper3d/text-to-model` 在本地返回 503详情里提示 `HYPER3D_API_KEY 未配置`,但开发者明明已经在本地私密文件里写了 key。
- 原因:`scripts/api-server-dev.mjs` 之前按 `.env.secrets.local → .env.local → .env` 合并,结果仓库里的 `.env` 空示例值会把前面已经设置好的私密 key 覆盖掉。
- 处理:`npm run api-server` / `npm run dev:rust` / `npm run dev` 统一按“外层 shell 变量优先,其后 `.env``.env.local``.env.secrets.local` 逐层覆盖”的顺序加载;真实密钥优先放 `.env.secrets.local`
- 验证:本地加入临时测试后,`HYPER3D_API_KEY` 应能被 `.env.secrets.local` 覆盖,且 shell 变量仍然最高优先级。
- 关联:`scripts/api-server-dev.mjs``server-rs/crates/api-server/src/hyper3d_generation.rs``docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`
## 拼图图片生成 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 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`,它只能用于重新请求 `/api/assets/read-url`,不能给 OSS V4 签名 URL 追加 `_v`OSS 会把 query 纳入签名,额外参数会让签名失效,历史素材常因未传 `refreshKey` 而表现正常。
- 处理:拼图结果页、发布预览、运行态和历史素材预览都走 `ResolvedAssetImage``useResolvedAssetReadUrl``isGeneratedLegacyPath(...)` 必须同时识别 `/generated-*``generated-*``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`
## 本地短信登录页签突然消失
- 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但 `LoginScreen` 中手机号验证码表单仍存在。
- 原因:前端根据 `GET /api/auth/login-options` 返回的 `availableLoginMethods` 渲染页签;常见根因有两类:
- 本地启动脚本没有让 `.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 页面,旧页面继续代理到已经下线的端口。
- 单独 `npm run dev:web` 启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
- 处理:优先用 `npm run api-server``npm run dev:rust``npm run dev` 启动,这些入口应保持 shell 环境变量最高优先级,并允许 `.env.local` 覆盖 `.env`;完整栈启动时还要确保脚本计算出的 `RUST_SERVER_TARGET` 不被 `.env.local` 里的旧值覆盖。排查时先请求 3000 域名下的 `/api/auth/login-options`,再直连 Rust API 目标,并核对 `.env.local``SMS_AUTH_ENABLED` 与代理端口;若 3001/3002 才返回正确结果,说明当前 3000 是旧前端进程,应清理旧进程后重启。
- 验证:`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。
- 关联:`scripts/api-server-dev.mjs``scripts/api-server-maincloud.mjs``scripts/dev-rust-stack.sh``scripts/dev-web-rust.mjs``docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`
## 本地短信收不到验证码先查 provider
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
- 原因:本地 `.env.local` 里如果是 `SMS_AUTH_PROVIDER="mock"`,后端不会发真实短信,只会返回固定 mock 验证码;另外 `npm run api-server` 过去曾让 `.env` 覆盖 `.env.local`,导致本地真实短信配置被错误压回默认值。
- 处理:真实短信联调时把 `.env.local``SMS_AUTH_PROVIDER` 显式设为 `aliyun`,然后重启 `api-server`;如果只想验证 UI 和账号链路,则保留 `mock` 并使用 `SMS_AUTH_MOCK_VERIFY_CODE`
- 验证:`GET /api/auth/login-options` 返回 `["phone","password"]``api-server` 日志里 `provider=aliyun` 才说明真实短信链路已生效。
- 关联:`scripts/api-server-dev.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 失败。
- 处理:保留 provider 错误语义,配置错误映射 `503 Service Unavailable`,上游短信失败映射 `502 Bad Gateway`;本地只验证 UI/账号链路时可用 shell 临时覆盖 `SMS_AUTH_PROVIDER=mock` 后启动 `npm run 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`
## 手机验证码登录成功后又瞬间回到未登录
- 现象:手机号验证码登录先成功,随后 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却回到未登录状态。
- 原因:`AuthGate` hydrate 曾先强制调用 `refreshStoredAccessToken()`;当 refresh cookie 临时失效、代理错配或后端返回 `401` 时,该方法会先清空本地 access token随后 `/api/auth/me` 只能恢复成未登录。
- 处理:`refreshStoredAccessToken()` 增加 `clearOnFailure` 选项;`AuthGate` 在已有本地 access token 时先用 `/api/auth/me` 确认用户,确认成功后再后台 refresh 续期与写每日登录埋点,后台 refresh 失败不清 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 后再启动;用户主动点击的账号动作仍保留默认全局鉴权失败处理。
- 追加处理generated 私有图片换签 `/api/assets/read-url` 也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。
- 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,`startPuzzleRun`、通关自动 `submitPuzzleLeaderboard`、下一关 `advancePuzzleNextLevel` 和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。
- 追加处理:通关后 `refreshSaveArchives()`、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。
- 验证:`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、推荐封面、推荐运行态错误重试和桌面推荐模块统一走登录门禁。未登录推荐页只显示封面点击封面只弹登录窗不携带登录后自动打开详情的回调。
- 验证:`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`
- 原因:脚本把 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 api-server` 在 Windows debug 启动时 `thread 'main' has overflowed its stack`
- 原因:`api-server` Axum 路由树已经很深debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。
- 处理:入口 `main` 用显式 16MB 栈线程启动 Tokio runtime并把实际服务逻辑放入 `run_server()`;新增路由时优先用小 router `.merge()`,避免继续拉长主链。
- 验证:`npm run 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 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 api-server` 链路仍在运行,进程树为 `npm run api-server -> node scripts/api-server-dev.mjs -> cargo run -> api-server.exe``0xffffffff` 常见于排障时用 `Stop-Process -Force` 强制结束旧 `api-server.exe` 后由 Cargo 回显,不一定代表新启动失败。
- 处理:先按目标路径确认并停止本仓库的旧 `api-server.exe` 及其父级 `cargo/node/cmd` 启动链路,再重新启动;不要同时开多个 `npm run api-server`
- 验证:确认没有匹配 `C:\Genarrative\server-rs\target\debug\api-server.exe` 的进程后,`Remove-Item` 能删除旧 exe随后 `npm run api-server` 启动并访问 `/healthz` 返回 200。
- 关联:`scripts/api-server-dev.mjs``server-rs/crates/api-server/src/main.rs`
## dev-rust-stack 端口被旧进程占用时会误判健康检查
- 现象:`node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh --skip-spacetime` 输出 `Port 3000 is in use, trying another one...`,随后 `api-server.exe``AddrInUse` / `code: 10048`
- 原因:旧 `api-server` 仍监听默认 `8082` 时,脚本的 `/healthz` 探测会命中旧进程并误判新服务已就绪;旧 Vite 占住 `3000`Vite 默认漂移到新端口,浏览器仍可能打开旧页面。
- 处理:`scripts/dev-rust-stack.sh` 已在 publish / 编译前预检 `api-server`、主站 Vite、后台 Vite 端口,并让 Vite 使用 `--strictPort`;遇到端口占用时按脚本打印的 PID 停止旧进程,或显式传入 `--api-port` / `--web-port` / `--admin-web-port`
- 验证:默认端口被占用时,完整栈应在发布模块前直接失败并打印监听进程;清理端口后重新启动不再漂移端口或命中旧 `/healthz`
- 关联:`scripts/dev-rust-stack.sh``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:rust` 回收 SpacetimeDB、Vite 和后台 Vite。
- 原因:单个 `async_stream::stream!` 中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。
- 处理:长 SSE 路由优先使用 `tokio::spawn` 跑业务流程,通过 `mpsc` + `UnboundedReceiverStream` 向 Axum 返回轻量 stream失败时更新会话为 `failed` 并发送 SSE `error`,不要把大段执行逻辑内联到路由返回的 stream future 中。
- 验证:补充实际 `collect()` SSE body 的路由测试,确认首轮包含 `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 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.toml` default-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` 启用了 `sccache` wrapper但当前 agent 没有可执行的 `sccache`、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。Windows 本机若配置了 `SCCACHE_OSS_*`sccache daemon 冷启动会先经 OSS/本机代理完成缓存读写检查,再监听 `127.0.0.1:4226`;代理或 OSS 链路慢时Cargo 的 `sccache rustc -vV` 可能先超时。
- 处理:保留 `server-rs/.cargo/config.toml``rustc-wrapper = "sccache"`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` 能输出版本;冷启动后原始 `cargo check -p api-server``cargo check -p spacetime-module` 能通过;`sccache --show-stats` 显示 `Cache location oss, name: genarrative-sccache`,证明仍在使用 sccache/OSS 缓存Jenkins 日志出现“未找到可用 sccache改用 rustc 直接构建”后仍继续真实构建。
- 关联:`scripts/dev-rust-stack.sh``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`
## 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`
## 个人任务 scope 不得扩成 work/site/module
- 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。
- 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
- 处理Admin 任务配置页不展示范围选择,保存时固定 `scopeKind: 'user'`API 和领域构造层拒绝非 `User`
- 验证:非 `user` scope 返回错误;相关测试覆盖 `Site` / `Module` / `Work` 被拒绝。
- 关联:`docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.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`
## WebGL 画布在高 DPR 移动端放大溢出
- 现象抓大鹅试玩入口进入后3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
- 原因:`WebGLRenderer.setPixelRatio(...)` 会把绘图缓冲区乘上设备 DPR如果没有给 `renderer.domElement` 单独设置 CSS `width/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 转存完成前超时;或 Hyper3D 控制台显示 3 个任务已完成,但草稿进度页仍停留在 `生成3D模型`;或结果页把 `/generated-match3d-assets/.../model.glb` 直接当浏览器 URL 加载导致私有 OSS/CORS 读取失败。
- 原因:`match3d_compile_draft` 会完成作品元信息、素材图、切图上传、3 件 Rodin 图生模型、GLB 下载和 OSS 转存,耗时远长于普通 Agent action如果 3 件 Rodin 模型逐个提交和轮询,等待时间会线性叠加;同时 generated 私有资产不能被 Three.js 直接 `fetch`
- 处理:切图和图片入库后,所有 Rodin 图生模型任务必须并行提交、并行轮询、并行下载转存Match3D creation client 的 `executeAction` 必须给长超时,当前为 20 分钟;生成进度页要包含 `生成3D模型` 阶段,但它不是 Hyper3D task 订阅页,而是在长 action 执行期间旁路轮询 session / work detail并用 profile 的 `generatedItemAssets` 更新完成数量;控制台看到 Rodin `Done` 后仍需等待下载列表、GLB 下载、OSS 转存和草稿 JSON 写回。结果页模型预览、场内运行态和备选栏预览都必须通过 `/api/assets/read-bytes` 读取 GLB 字节后交给 Three.js GLTFLoader不要直接请求裸 generated 路径。排查时按同一个 session/profile 查看 api-server 日志:`抓大鹅 Rodin 状态轮询返回``抓大鹅 Rodin 下载列表轮询返回``抓大鹅 Rodin GLB 下载完成``抓大鹅 Rodin GLB 转存 OSS 完成`;同时检查前端 work detail 响应里的 `generatedItemAssets[].status/modelObjectKey/error`
- 验证:`npm run test -- src\services\miniGameDraftGenerationProgress.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx``npm run typecheck`;真实联调需配置 `VECTOR_ENGINE_API_KEY``HYPER3D_API_KEY` 和 OSS 变量。
- 关联:`src/services/match3d-creation/match3dCreationClient.ts``src/services/creation-agent/creationAgentClientFactory.ts``src/components/match3d-result/Match3DModelPreview.tsx``src/components/match3d-runtime/Match3DPhysicsBoard.tsx``server-rs/crates/api-server/src/match3d.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
## Rodin 完成态后下载列表可能延迟或字段漂移
- 现象:抓大鹅草稿生成时 Rodin 状态已完成,但 `/api/creation/match3d/sessions/{sessionId}/actions` 返回 502提示 `{物品名} 3D 模型已完成但未返回可下载模型文件:{taskUuid}`
- 原因Hyper3D Rodin 官方 `check-status_reset_v` 示例要求看 `jobs` 列表,只有所有 job 都 `Done` 才能进入下载;`download-results_reset_v` 还明确要求用生成响应顶层 `uuid` 作为 `task_uuid`,不要用 `jobs.uuids` 子任务 uuid。旧聚合若只看 root status 或第一个 job可能在 preview job 完成但模型 job 仍在生成时提前下载。另外任务完成和下载列表文件发布不是强同步的;上游下载结果还可能使用 `fileUrl``signedUrl``presignedUrl``fileName` 等字段别名,旧解析器只识别 `url/downloadUrl/name/file_name/filename` 时会得到空列表。
- 处理:`query_task_status` 聚合状态必须以 `jobs` 为准:任一 failed 即 failed全部 done 才 done`match3d_compile_draft` 在状态完成后对 `query_downloads` 继续轮询;下载解析兼容常见 URL 和文件名字段别名;模型选择优先 `.glb`,可兜底到非图片下载文件,但只有 preview/png/jpg/webp 这类预览图时必须继续失败,不能伪装成 GLB。
- 验证:`cargo test -p api-server match3d_model_download --manifest-path server-rs/Cargo.toml``cargo test -p api-server extracts_download_files --manifest-path server-rs/Cargo.toml``cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml``cargo test -p api-server hyper3d --manifest-path server-rs/Cargo.toml`
- 关联:`server-rs/crates/api-server/src/match3d.rs``server-rs/crates/api-server/src/hyper3d_generation.rs``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`
## 抓大鹅切图路径不能只用中文物品名
- 现象:草稿页 `3D素材` Tab 中多个素材名称不同,但预览图片完全一样;点击图生模型生成时还可能提示 `参考图必须是 data URL`
- 原因:中文物品名经过 OSS 路径段清洗后都可能退化成 `item`,多张切割图片写到同一个 `items/item/image.png` object key后写入覆盖先写入结果页手动 Rodin 图生模型还曾把 `/generated-match3d-assets/...` 私有路径直接作为 `imageDataUrls` 提交。
- 处理:切割图上传路径必须带稳定唯一 `itemId` 前缀,例如 `items/match3d-item-1-item/image.png`结果页提交图生模型前generated 私有路径先经同源 `/api/assets/read-bytes` 由后端换签并读取字节,前端再转成 `data:image/...;base64,...`,不要在浏览器里直接 `fetch` OSS 签名 URL否则会被 bucket CORS 拦截。
- 验证:后端单测覆盖中文名路径唯一;前端单测覆盖 generated 参考图会换签、fetch 并以 Data URL 调用 `submitHyper3dImageToModel`
- 关联:`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
- 现象:抓大鹅草稿生成完成后停留在结果页能看到切割好的 `3D素材` 图片;退出后从草稿 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`
## 抓大鹅试玩和正式运行态不要只读草稿页本地模型预览
- 现象:历史草稿页 `3D素材` Tab 能看到水果模型,但点击试玩或从推荐 / 公开作品进入正式抓大鹅时,局内仍显示默认积木素材。
- 原因:结果页手动 `重新生成` 曾只更新本地 `assetDrafts.downloads`,没有把新的 GLB 写回 `generatedItemAssets`;历史数据还可能只有 `modelObjectKey` 而没有 `modelSrc`;推荐流内嵌运行态若只读卡片摘要,卡片缺素材时会把已持久化 profile 模型丢掉;本次生成 response 的 draft 也可能比 profile 旧,只带图片而不带模型。
- 处理:结果页模型预览和运行态都按 `modelSrc || modelObjectKey` 读取;手动重新生成成功后把素材草稿重新序列化并写回作品 profile`Match3DResultView` 合并同 `itemId` 的 draft/profile 素材,用 profile 已有模型补齐旧 draft推荐流内嵌运行态启动前若卡片摘要没有生成素材补读 `getMatch3DWorkDetail(profileId)` 并把详情资产传给 `Match3DRuntimeShell`
- 验证:执行 `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[].modelSrc/modelObjectKey`
- 关联:`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`
## 抓大鹅标签清洗不要把 `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`
## 用户标签不要直接外显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`