diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 54f6f6e6..f0f26e0c 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -186,6 +186,14 @@ - 验证方式:`cargo check -p api-server`、`cargo test -p module-puzzle creative`、`cargo test -p api-server creative_agent`、`npm run api-server` 后检查 `/healthz`、`POST /api/runtime/creative-agent/sessions`、`POST /api/runtime/creative-agent/sessions/{sessionId}/messages/stream`。 - 关联文档:`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。 +## 2026-05-10 视觉小说入口收敛为单句创作 + 画风选择 + +- 背景:视觉小说入口页要对齐抓大鹅式的线性创作入口,只保留最小可用输入,避免再暴露文档 / 空白 / 对话式工作台。 +- 决策:入口页只展示一句话创作输入框和横向视觉画风卡片;画风通过 `seedText` 追加 `视觉画风` 和 `画风要求` 两行透传给既有创作链路;点击生成后先进入 `visual-novel-generating` 过程页,再自动进入 `visual-novel-result`。 +- 影响范围:`VisualNovelAgentWorkspace`、`PlatformEntryFlowShellImpl`、`platformEntryTypes`、视觉小说 PRD;不新增后端字段或数据库结构。 +- 验证方式:视觉小说工作台单测通过,`npm run check:encoding` 通过;`npm run typecheck` 仍受仓库里 `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx` 的既有类型错误影响。 +- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。 + ## 2026-05-07 移动端整页缩放由入口统一锁定 - 背景:移动端游戏式页面如果允许浏览器整页缩放,容易把固定画布、HUD 和底部操作区一起放大或缩小,破坏操作节奏。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index c2e3f02e..fe718996 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -67,6 +67,14 @@ - 验证:后端单测覆盖 `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` 时直接走 edits,prompt 仍保留参考图强约束;入口页关闭 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 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。 @@ -147,6 +155,14 @@ - 验证:不打印密钥内容,只检查 `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 签名时间格式化失败`。 @@ -325,6 +341,14 @@ - 验证:查看 `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: caused by: Failed to send data to or receive data from server / Failed to read response header / failed to fill whole buffer`;真实 `rustc -Vv` 可以执行,但构建在调用包装器时失败。 @@ -364,3 +388,43 @@ - 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 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 模型下载 + +- 现象:抓大鹅草稿生成时 Hyper3D 状态已完成,但下载列表为空或没有可用模型文件,`/api/creation/match3d/sessions/{sessionId}/actions` 返回 `502 Bad Gateway`,前端提示 `Hyper3D 已完成但未返回可下载模型文件`。 +- 原因:草稿生成链路曾在切割图片后立即并行调用 Rodin 图生模型,并把模型下载成功作为草稿完成前置条件;上游完成态和可下载文件列表不是强一致,容易把本来可用的图片草稿卡死。 +- 处理:草稿阶段只生成物品名、素材图、切割独立图片并上传 OSS,返回 `status = image_ready`;Rodin 3D 模型生成留到结果页 `3D素材` Tab 手动触发。 +- 验证:草稿响应中的 `generatedItemAssets[].imageSrc` 有值、`modelSrc` 为空、状态为 `image_ready`;结果页显示 `图片已就绪` 和 `0 文件`,不会自动请求 Hyper3D 下载。 +- 关联:`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 中多个素材名称不同,但预览图片完全一样;点击图生模型生成时还可能提示 `参考图必须是 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`。 diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 7206e1d4..b39ca305 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -154,6 +154,7 @@ export interface AdminUpsertProfileRedeemCodeRequest { export interface AdminUpsertProfileInviteCodeRequest { inviteCode: string; metadata?: Record; + grantedUserTags: string[]; startsAt?: string | null; expiresAt?: string | null; } @@ -200,6 +201,7 @@ export interface ProfileInviteCodeAdminResponse { userId: string; inviteCode: string; metadata: Record; + grantedUserTags: string[]; startsAt?: string | null; expiresAt?: string | null; status: 'pending' | 'active' | 'expired'; diff --git a/apps/admin-web/src/pages/AdminInviteCodePage.tsx b/apps/admin-web/src/pages/AdminInviteCodePage.tsx index dfbf119a..48f7c3c4 100644 --- a/apps/admin-web/src/pages/AdminInviteCodePage.tsx +++ b/apps/admin-web/src/pages/AdminInviteCodePage.tsx @@ -28,6 +28,7 @@ export function AdminInviteCodePage({ const [inviteCode, setInviteCode] = useState(''); const [startsAt, setStartsAt] = useState(''); const [expiresAt, setExpiresAt] = useState(''); + const [grantedTagsText, setGrantedTagsText] = useState(''); const [metadataText, setMetadataText] = useState('{}'); const [errorMessage, setErrorMessage] = useState(''); const [listErrorMessage, setListErrorMessage] = useState(''); @@ -80,6 +81,7 @@ export function AdminInviteCodePage({ const payload: AdminUpsertProfileInviteCodeRequest = { inviteCode: inviteCode.trim(), metadata: parseMetadata(metadataText), + grantedUserTags: parseUserTags(grantedTagsText), startsAt: startsAt ? toIsoDateTime(startsAt) : null, expiresAt: expiresAt ? toIsoDateTime(expiresAt) : null, }; @@ -115,6 +117,7 @@ export function AdminInviteCodePage({ setInviteCode(entry.inviteCode); setStartsAt(toDateTimeLocalValue(entry.startsAt)); setExpiresAt(toDateTimeLocalValue(entry.expiresAt)); + setGrantedTagsText(entry.grantedUserTags.join('、')); setMetadataText(JSON.stringify(entry.metadata, null, 2)); } @@ -174,6 +177,15 @@ export function AdminInviteCodePage({ + +