diff --git a/.codex/logs/run-dev-web-final.ps1 b/.codex/logs/run-dev-web-final.ps1 new file mode 100644 index 00000000..196bc9f0 --- /dev/null +++ b/.codex/logs/run-dev-web-final.ps1 @@ -0,0 +1,4 @@ +Set-Location 'C:\Genarrative' +$env:RUST_SERVER_TARGET = 'http://127.0.0.1:8082' +$env:GENARRATIVE_RUNTIME_SERVER_TARGET = 'http://127.0.0.1:8082' +npm.cmd run dev:web *> 'C:\Genarrative\.codex\logs\dev-web-final.out.log' diff --git a/.codex/skills/gpt-image-2-apimart/SKILL.md b/.codex/skills/gpt-image-2-apimart/SKILL.md index d9e8aae6..2cc40454 100644 --- a/.codex/skills/gpt-image-2-apimart/SKILL.md +++ b/.codex/skills/gpt-image-2-apimart/SKILL.md @@ -47,7 +47,7 @@ Default body: } ``` -For a reference image, add: +For weak visual references in text-to-image generation, add: ```json { @@ -55,6 +55,26 @@ For a reference image, add: } ``` +For image-to-image work that must follow a reference image closely, use the VectorEngine edits endpoint instead of the generations `image` array: + +```text +POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits +Authorization: Bearer {VECTOR_ENGINE_API_KEY} +Content-Type: multipart/form-data +``` + +Multipart fields: + +```text +model=gpt-image-2 +prompt= +n=1 +size=1024x1024 +image=@reference.png +``` + +Prefer edits for workflows where the reference image controls composition, pose, container shape, or layout. In this repository, Match3D container UI generation uses edits with `public/match3d-background-references/pot-fused-reference.png` as the `image` part. + Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints. ## Environment diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs new file mode 100644 index 00000000..1ba9eb69 --- /dev/null +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -0,0 +1,351 @@ +import { Buffer } from 'node:buffer'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const skillRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(skillRoot, '..', '..', '..'); +const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations'); +const defaultTimeoutMs = 1000000; + +const prompts = [ + { + id: 'cat-barista', + title: '咖啡师猫咪', + subject: + '一只奶油色猫咪像人一样双足站立,穿深绿色围裙,在温暖咖啡馆吧台前专注拉花,爪子扶着咖啡杯,蓬松尾巴自然弯起,童书级精致插画,柔和自然光,主体清晰。', + }, + { + id: 'cat-detective', + title: '侦探猫咪', + subject: + '一只黑白猫咪像侦探一样双足站在雨后街角,穿短风衣和小帽子,单爪拿放大镜,另一只爪插兜,路灯和湿润石板路反光,电影感但可爱,插画风格。', + }, + { + id: 'cat-dancer', + title: '舞者猫咪', + subject: + '一只橘猫以拟人舞者姿态单脚旋转,穿轻盈舞台披肩,前爪展开,尾巴形成优雅弧线,背景是暖色小剧场灯光,动作灵动,精致插画。', + }, + { + id: 'cat-knight', + title: '骑士猫咪', + subject: + '一只银灰猫咪像小骑士一样站在苔藓石台上,披短斗篷,双爪握着细剑指向地面,姿态勇敢但可亲,远处森林微光,奇幻插画风格。', + }, + { + id: 'cat-painter', + title: '画家猫咪', + subject: + '一只三花猫咪双足站在画架前,穿宽松蓝色工作衫,一爪拿画笔一爪托调色盘,鼻尖有颜料点,窗边画室阳光明亮,温柔手绘插画。', + }, + { + id: 'cat-astronaut', + title: '宇航员猫咪', + subject: + '一只白猫咪以拟人宇航员姿态站在月面,透明头盔内露出猫脸,尾巴在宇航服后轻轻翘起,爪子向远处蓝色星球敬礼,梦幻插画风格。', + }, +]; + +const args = new Map(); +for (let index = 2; index < process.argv.length; index += 1) { + const raw = process.argv[index]; + if (raw.startsWith('--')) { + const next = process.argv[index + 1]; + if (next && !next.startsWith('--')) { + args.set(raw, next); + index += 1; + } else { + args.set(raw, true); + } + } +} + +function readDotenv(fileName) { + const filePath = path.join(repoRoot, fileName); + if (!existsSync(filePath)) { + return {}; + } + + const values = {}; + for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed); + if (!match) { + continue; + } + let value = match[2].trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + values[match[1]] = value; + } + return values; +} + +function resolveEnv() { + const loaded = { + ...readDotenv('.env.example'), + ...readDotenv('.env.local'), + ...readDotenv('.env.secrets.local'), + ...process.env, + }; + return { + baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '') + .trim() + .replace(/\/+$/u, ''), + apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), + timeoutMs: Number.parseInt( + String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs), + 10, + ), + }; +} + +function buildVectorEngineImagesGenerationUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/generations` + : `${baseUrl}/v1/images/generations`; +} + +function buildPrompt(entry) { + return [ + '请生成一张高清 1:1 方形插画。', + `画面主体:${entry.subject}`, + '要求:猫咪保留清晰猫脸、猫耳、猫尾和毛发质感,但身体姿态像人一样自然;构图完整,角色占画面主体,适合作为项目插画素材。', + '避免:文字、水印、边框、按钮、UI 元素、低清晰度、过度写实恐怖感、畸形肢体、多余手指。', + ].join(''); +} + +function collectStringsByKey(value, targetKey, output) { + if (Array.isArray(value)) { + value.forEach((entry) => collectStringsByKey(entry, targetKey, output)); + return; + } + if (!value || typeof value !== 'object') { + return; + } + + for (const [key, nested] of Object.entries(value)) { + if (key === targetKey) { + if (typeof nested === 'string' && nested.trim()) { + output.push(nested.trim()); + } + if (Array.isArray(nested)) { + nested.forEach((entry) => { + if (typeof entry === 'string' && entry.trim()) { + output.push(entry.trim()); + } + }); + } + } + collectStringsByKey(nested, targetKey, output); + } +} + +function extractImageUrls(payload) { + const urls = []; + collectStringsByKey(payload, 'url', urls); + collectStringsByKey(payload, 'image', urls); + collectStringsByKey(payload, 'image_url', urls); + return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url)); +} + +function extractBase64Images(payload) { + const values = []; + collectStringsByKey(payload, 'b64_json', values); + return values; +} + +function inferExtensionFromContentType(contentType) { + const normalized = contentType.split(';')[0]?.trim().toLowerCase(); + if (normalized === 'image/png') { + return 'png'; + } + if (normalized === 'image/webp') { + return 'webp'; + } + if (normalized === 'image/gif') { + return 'gif'; + } + return 'jpg'; +} + +function inferExtensionFromBytes(bytes) { + if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) { + return 'png'; + } + if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) { + return 'jpg'; + } + if ( + bytes.subarray(0, 4).toString('ascii') === 'RIFF' && + bytes.subarray(8, 12).toString('ascii') === 'WEBP' + ) { + return 'webp'; + } + return 'png'; +} + +async function fetchJson(url, options, timeoutMs) { + const abortController = new AbortController(); + const timer = setTimeout(() => abortController.abort(), timeoutMs); + try { + const response = await fetch(url, { + ...options, + signal: abortController.signal, + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`); + } + return JSON.parse(text); + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function downloadUrl(url, timeoutMs) { + const abortController = new AbortController(); + const timer = setTimeout(() => abortController.abort(), timeoutMs); + try { + const response = await fetch(url, { signal: abortController.signal }); + if (!response.ok) { + throw new Error(`download ${response.status}`); + } + const bytes = Buffer.from(await response.arrayBuffer()); + return { + bytes, + extension: inferExtensionFromContentType( + response.headers.get('content-type') || 'image/jpeg', + ), + }; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`Generated image download timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function generateOne(env, entry, outDir) { + const requestBody = { + model: 'gpt-image-2-all', + prompt: buildPrompt(entry), + n: 1, + size: '1024x1024', + }; + const payload = await fetchJson( + buildVectorEngineImagesGenerationUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }, + env.timeoutMs, + ); + + const urls = extractImageUrls(payload); + const b64Images = extractBase64Images(payload); + + let image; + if (urls[0]) { + image = await downloadUrl(urls[0], env.timeoutMs); + } else if (b64Images[0]) { + const bytes = Buffer.from(b64Images[0], 'base64'); + image = { + bytes, + extension: inferExtensionFromBytes(bytes), + }; + } else { + throw new Error(`VectorEngine returned no image for ${entry.id}`); + } + + mkdirSync(outDir, { recursive: true }); + const outputPath = path.join(outDir, `${entry.id}.${image.extension}`); + writeFileSync(outputPath, image.bytes); + return outputPath; +} + +const dryRun = args.has('--dry-run') || !args.has('--live'); +const outDir = path.resolve(String(args.get('--out-dir') || defaultOutDir)); +const limit = Number.parseInt(String(args.get('--limit') || '0'), 10); +const selectedPrompts = limit > 0 ? prompts.slice(0, limit) : prompts; + +if (dryRun) { + const env = resolveEnv(); + console.log( + JSON.stringify( + { + mode: 'dry-run', + outDir, + count: selectedPrompts.length, + hasBaseUrl: Boolean(env.baseUrl), + hasApiKey: Boolean(env.apiKey), + requests: selectedPrompts.map((entry) => ({ + id: entry.id, + title: entry.title, + body: { + model: 'gpt-image-2-all', + prompt: buildPrompt(entry), + n: 1, + size: '1024x1024', + }, + })), + }, + null, + 2, + ), + ); + process.exit(0); +} + +const env = resolveEnv(); +if (!env.baseUrl || !env.apiKey) { + console.error( + JSON.stringify({ + ok: false, + error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY', + hasBaseUrl: Boolean(env.baseUrl), + hasApiKey: Boolean(env.apiKey), + }), + ); + process.exit(1); +} + +const generated = []; +for (const entry of selectedPrompts) { + console.log(`Generating ${entry.id}...`); + generated.push(await generateOne(env, entry, outDir)); +} + +console.log( + JSON.stringify( + { + ok: true, + count: generated.length, + files: generated, + }, + null, + 2, + ), +); diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index f3a69aed..72e05646 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -13,7 +13,7 @@ const promptsPath = path.join( 'puzzle-template-prompts.json', ); const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates'); -const defaultTimeoutMs = 180000; +const defaultTimeoutMs = 1000000; const args = new Map(); for (let index = 2; index < process.argv.length; index += 1) { diff --git a/.env.example b/.env.example index 482669b7..03d8c2c1 100644 --- a/.env.example +++ b/.env.example @@ -122,11 +122,16 @@ RPG_LLM_WEB_SEARCH_ENABLED="true" DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1" DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY" -# Server-side APIMart image generation config for optional puzzle image models. +# APIMart Responses config for creative-agent text/multimodal understanding. APIMART_BASE_URL="https://api.apimart.ai/v1" APIMART_API_KEY="YOUR_APIMART_API_KEY" APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000" +# VectorEngine GPT-image-2 / Gemini image generation config. +VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai" +VECTOR_ENGINE_API_KEY="" +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000" + # 阿里云 OSS 配置。 # Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量, # 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。 diff --git a/.gitignore b/.gitignore index 8abc3e04..6f27c449 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ temp*build*/ /logs .worktrees/ .env.secrets.local +spacetime.local.json # Local load-test data extracted from private migration files scripts/loadtest/data/*.local.json diff --git a/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md b/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md new file mode 100644 index 00000000..ded40159 --- /dev/null +++ b/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md @@ -0,0 +1,549 @@ +# Bark Battle Phase 2 Platform Work Loop Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** 将 `bark-battle` 从内部试玩 demo 升级为 Genarrative 正式 play type,打通轻创作配置、发布态作品、正式 runtime、run start / finish、后端裁决、个人历史、作品统计和最小排行榜闭环。 + +**Architecture:** 先冻结 shared contracts 与 `module-bark-battle` 纯领域规则,再落 SpacetimeDB 表/reducer、`spacetime-client` facade 和 `api-server` BFF,随后接前端最小纵切,最后补排行榜/个人历史/作品统计投影体验。前端只承接表现、交互和临时 UI 状态,正式业务真相由后端裁决。 + +**Tech Stack:** React + TypeScript + Vite, server-rs + Axum, SpacetimeDB Rust module, shared-contracts, Vitest, Cargo tests, npm scripts. + +--- + +## 0. 已确认决策 + +1. “有效叫声”统一为 **有效声浪触发**:当前采样响度达到有效阈值且满足 `minBarkGapMs` 冷却即触发;不再要求 `minBarkDurationMs` / `maxBarkDurationMs`,也不等待响度回落。 +2. Phase 2 范围是 **Bark Battle 平台作品闭环**,不是单纯玩法表现深化。 +3. 作品形态是 **轻创作配置作品**:标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。 +4. 难度预设只影响 AI 对手行为;不影响有效阈值、冷却、时长、分数公式或反作弊阈值。 +5. 排行榜按 `workId + difficultyPreset + rulesetVersion` 分榜。 +6. 后端裁决正式单局结果;前端只提交派生指标,`clientResult` 只用于 debug/对账。 +7. 排行榜只收录 `serverResult = player_win` 且未被反作弊拒绝的单局结果,排序以 `finalEnergy` 优先。 +8. 作品统计使用最小后端投影:start、finish、win/draw/loss、flagged、leaderboard、best/avg energy。 +9. 个人历史成绩 = 最近记录列表 + 个人最佳摘要;仅本人可见。 +10. 正式入口闭环覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品、稳定作品 ID runtime 路由和 `work_play_start`。 +11. 创作编辑形态是单页轻配置表单 + 预览卡片。 +12. 实施顺序固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。 + +--- + +## 1. 必读文档与约束 + +实施前先读: + +- `AGENTS.md` +- `CONTEXT.md` +- `docs/prd/BARK_BATTLE_BDD_2026-05-11.md` +- `docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md` +- `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md` +- `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` +- `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md` +- `.codex/skills/spacetimedb-cli/SKILL.md` +- `.codex/skills/spacetimedb-rust/SKILL.md` +- `.codex/skills/spacetimedb-concepts/SKILL.md` +- `.codex/skills/spacetimedb-typescript/SKILL.md` + +关键约束: + +- 后端路线固定 `server-rs + Axum + SpacetimeDB`。 +- 领域规则进 `module-bark-battle`,SpacetimeDB 表和事务编排进 `spacetime-module`。 +- HTTP/SSE/BFF 留在 `api-server`。 +- 前后端 DTO 留在 `shared-contracts`。 +- 数据库表结构更改必须同步 `migration.rs` 和生成绑定。 +- 人工命令/文档示例禁止继续使用 `spacetime --root-dir`。 +- 修改中文文件后必须跑 `npm run check:encoding`。 + +--- + +## 2. 阶段一:契约与领域规则 + +### Task 1.1: 新增 Rust shared-contracts 模块 + +**Objective:** 定义 Bark Battle Phase 2 的 Rust DTO 边界。 + +**Files:** +- Create: `server-rs/crates/shared-contracts/src/bark_battle.rs` +- Modify: `server-rs/crates/shared-contracts/src/lib.rs` +- Test: `server-rs/crates/shared-contracts/src/bark_battle.rs` + +**Steps:** +1. 新增枚举:`BarkBattleDifficultyPreset { Easy, Normal, Hard }`、`BarkBattleServerResult { PlayerWin, OpponentWin, Draw }`、`BarkBattleFinishStatus { Accepted, AcceptedWithFlags, Rejected }`。 +2. 新增配置 DTO:`BarkBattleDraftConfig`、`BarkBattlePublishedConfig`、`BarkBattleRuntimeConfig`。 +3. 新增 run DTO:`BarkBattleRunStartRequest/Response`、`BarkBattleRunFinishRequest/Response`。 +4. 新增派生指标 DTO:`BarkBattleDerivedMetrics`,字段包含 `trigger_count`、`max_volume`、`average_volume`、`final_energy`、`combo_max`。 +5. 新增排行榜/历史/统计 DTO:`BarkBattleLeaderboardEntry`、`BarkBattlePersonalHistoryItem`、`BarkBattlePersonalBestSummary`、`BarkBattleWorkStats`。 +6. 在 `lib.rs` 导出 `pub mod bark_battle;`。 + +**Verification:** + +```bash +cargo test -p shared-contracts bark_battle +``` + +Expected: contracts tests pass. + +### Task 1.2: 新增 TypeScript shared contracts mirror + +**Objective:** 让前端获得与 Rust DTO 对齐的类型。 + +**Files:** +- Create: `packages/shared/src/contracts/barkBattle.ts` +- Modify: `packages/shared/src/contracts/index.ts` +- Test: `packages/shared/src/contracts/barkBattle.test.ts` + +**Steps:** +1. 定义 `BarkBattleDifficultyPreset = 'easy' | 'normal' | 'hard'`。 +2. 定义 `BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw'`。 +3. 定义 draft / published / runtime config 类型。 +4. 定义 start / finish request response 类型。 +5. 定义 leaderboard / personal history / work stats 类型。 +6. 写最小序列化/fixture 测试,确保字段命名采用前端约定 camelCase,并在 API client 层做必要映射。 + +**Verification:** + +```bash +npm test -- --run packages/shared/src/contracts/barkBattle.test.ts +npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false +``` + +### Task 1.3: 新建 module-bark-battle crate + +**Objective:** 将正式裁决规则放入纯领域 crate。 + +**Files:** +- Create: `server-rs/crates/module-bark-battle/Cargo.toml` +- Create: `server-rs/crates/module-bark-battle/src/lib.rs` +- Create: `server-rs/crates/module-bark-battle/src/domain.rs` +- Create: `server-rs/crates/module-bark-battle/src/scoring.rs` +- Modify: `server-rs/Cargo.toml` + +**Steps:** +1. 在 workspace 中注册 `module-bark-battle`。 +2. 定义 `RulesetVersion`,首版固定如 `bark-battle-ruleset-v1`。 +3. 定义 `BarkBattleRuleset`,包含标准局时长 30s、`min_bark_gap_ms`、合法音量/能量/连击范围、duration tolerance。 +4. 实现 `validate_finish_metrics()`。 +5. 实现 `adjudicate_result()`:以后端 `final_energy` 和 draw threshold 生成 `serverResult`。 +6. 实现 `compute_leaderboard_score()`:只允许胜利局入榜,排序因子为 `finalEnergy`、`triggerCount`、`maxVolume`、duration 接近度、`finishedAt`。 + +**Verification:** + +```bash +cargo test -p module-bark-battle +``` + +### Task 1.4: 领域规则单测覆盖作弊边界 + +**Objective:** 防止前端伪造 finish 直接刷榜。 + +**Files:** +- Modify: `server-rs/crates/module-bark-battle/src/scoring.rs` + +**Test cases:** +- 28s-35s 合法窗口内可接受。 +- 1s / 300s 应 rejected 或 flagged。 +- `triggerCount > durationMs / minBarkGapMs + tolerance` 应 flagged。 +- `finalEnergy` 越界应 rejected。 +- 平/负不生成 leaderboard entry。 +- easy/normal/hard 不改变阈值、冷却、分数公式,只改变 AI preset key。 + +**Verification:** + +```bash +cargo test -p module-bark-battle -- --nocapture +``` + +--- + +## 3. 阶段二:SpacetimeDB 表/reducer 与 api-server BFF + +### Task 2.1: 设计 SpacetimeDB 表目录 + +**Objective:** 新增 Bark Battle 表并与 migration 对齐。 + +**Files:** +- Create: `server-rs/crates/spacetime-module/src/bark_battle/mod.rs` +- Create: `server-rs/crates/spacetime-module/src/bark_battle/types.rs` +- Create: `server-rs/crates/spacetime-module/src/bark_battle/tables.rs` +- Modify: `server-rs/crates/spacetime-module/src/lib.rs` +- Modify: `server-rs/crates/spacetime-module/src/migration.rs` + +**Tables:** +- `bark_battle_draft_config` +- `bark_battle_published_config` +- `bark_battle_runtime_run` +- `bark_battle_score_record` +- `bark_battle_leaderboard_entry` +- `bark_battle_work_stats_projection` +- `bark_battle_personal_best_projection` + +**Pitfalls:** +- 表结构不要 derive `SpacetimeType`。 +- reducer 使用 `&ReducerContext`。 +- 授权身份来自 `ctx.sender()`。 +- 需要公开订阅的表才加 `public`。 + +**Verification:** + +```bash +cargo test -p spacetime-module +``` + +### Task 2.2: 实现草稿/发布 reducer + +**Objective:** 支持轻配置草稿保存和发布态 config 固化。 + +**Reducers:** +- `create_bark_battle_draft` +- `update_bark_battle_draft_config` +- `publish_bark_battle_work` +- `get_bark_battle_runtime_config` 如仓库约定使用 reducer/procedure 查询则按现有 pattern 实现。 + +**Rules:** +- 草稿配置只允许标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。 +- 发布生成稳定作品 ID / config version。 +- 发布态 config 包含 `rulesetVersion`。 + +**Verification:** + +```bash +cargo test -p spacetime-module bark_battle +``` + +### Task 2.3: 实现 run start / finish reducer + +**Objective:** 打通正式运行态后端事务。 + +**Reducers:** +- `start_bark_battle_run` +- `finish_bark_battle_run` +- `get_bark_battle_run` + +**Rules:** +- start 创建 `run_id` 和一次性 `run_token`。 +- start 记录 work/config/ruleset/difficulty 快照。 +- finish 必须校验 run token、未 finish、work/config/ruleset/difficulty 一致。 +- finish 调用 `module-bark-battle` 裁决结果。 +- accepted 写 score record。 +- `serverResult = player_win` 且排行榜开启且未 rejected 时写 leaderboard entry。 +- accepted / accepted_with_flags 更新 work stats 和 personal best projection。 + +**Verification:** + +```bash +cargo test -p spacetime-module bark_battle_run +``` + +### Task 2.4: 更新 migration 与生成绑定 + +**Objective:** 让 SpacetimeDB 表结构变更可发布。 + +**Files:** +- Modify: `server-rs/crates/spacetime-module/src/migration.rs` +- Generated: `server-rs/crates/spacetime-client/src/module_bindings/*bark*` + +**Commands:** +按仓库现有脚本优先;不要手改 generated bindings。 + +```bash +npm run spacetime:build +npm run spacetime:generate +``` + +若脚本名不同,先查 `package.json` 和 `server-rs` README。 + +### Task 2.5: 实现 spacetime-client facade + +**Objective:** api-server 不直接操作 generated bindings。 + +**Files:** +- Create: `server-rs/crates/spacetime-client/src/bark_battle.rs` +- Modify: `server-rs/crates/spacetime-client/src/lib.rs` + +**Methods:** +- `create_bark_battle_draft` +- `save_bark_battle_draft_config` +- `publish_bark_battle_work` +- `get_bark_battle_runtime_config` +- `start_bark_battle_run` +- `finish_bark_battle_run` +- `list_bark_battle_leaderboard` +- `list_my_bark_battle_history` +- `get_my_bark_battle_best_summary` +- `get_bark_battle_work_stats` + +**Verification:** + +```bash +cargo test -p spacetime-client bark_battle +``` + +### Task 2.6: 实现 api-server BFF 路由 + +**Objective:** 暴露前端需要的 HTTP API。 + +**Files:** +- Create: `server-rs/crates/api-server/src/bark_battle.rs` +- Modify: `server-rs/crates/api-server/src/app.rs` + +**Routes:** +- `POST /api/bark-battle/drafts` +- `PATCH /api/bark-battle/drafts/:draftId` +- `POST /api/bark-battle/drafts/:draftId/publish` +- `GET /api/bark-battle/works/:workId/runtime-config` +- `POST /api/bark-battle/runs/start` +- `POST /api/bark-battle/runs/:runId/finish` +- `GET /api/bark-battle/works/:workId/leaderboard` +- `GET /api/bark-battle/me/history` +- `GET /api/bark-battle/me/best-summary` +- `GET /api/bark-battle/works/:workId/stats` + +**Verification:** + +```bash +cargo test -p api-server bark_battle +npm run api-server +curl -f http://127.0.0.1:/healthz +``` + +--- + +## 4. 阶段三:最小前端纵切 + +### Task 3.1: 新增前端 service client + +**Files:** +- Create: `src/services/bark-battle/barkBattleClient.ts` +- Test: `src/services/bark-battle/barkBattleClient.test.ts` + +**Methods:** 与 BFF routes 一一对应。 + +**Verification:** + +```bash +npm test -- --run src/services/bark-battle/barkBattleClient.test.ts +``` + +### Task 3.2: 接入创作入口与 SelectionStage + +**Files:** +- Modify: `src/config/newWorkEntryConfig.ts` +- Modify: `src/components/platform-entry/platformEntryCreationTypes.ts` +- Modify: `src/components/platform-entry/platformEntryTypes.ts` +- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + +**Rules:** +- 新增 `bark-battle` play type。 +- 入口打开单页轻配置表单,不走复杂 agent workspace。 +- 移动端入口布局不能溢出。 + +### Task 3.3: 实现单页轻配置表单 + 预览卡片 + +**Files:** +- Create: `src/components/bark-battle-creation/BarkBattleConfigEditor.tsx` +- Create: `src/components/bark-battle-creation/BarkBattlePreviewCard.tsx` +- Test: `src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx` + +**UI fields:** +- 标题必填 +- 简介选填 +- 主题/背景预设 +- 狗狗皮肤预设 +- 难度预设,默认 `normal` +- 排行榜开关,默认开启 + +**UI constraints:** +- 不堆大段玩法说明。 +- 按现有游戏 UI 风格设计。 +- 移动端优先。 + +### Task 3.4: 发布后进入作品详情 + +**Files:** +- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` +- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx` +- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx` +- Modify: `src/components/custom-world-home/creationWorkShelf.ts` + +**Rules:** +- 发布成功刷新 works/gallery/shelf。 +- 跳作品详情。 +- 详情 CTA 可以进入正式 runtime。 + +### Task 3.5: runtime 拉发布态 config 并 start / finish + +**Files:** +- Modify: `src/games/bark-battle/*` +- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx` +- Create/Modify: `src/components/bark-battle-runtime/BarkBattleRuntimeRoute.tsx` 如需要 + +**Rules:** +- runtime 通过稳定 `workId` 拉 `BarkBattleRuntimeConfig`。 +- 开始正式局时调用 start run。 +- 结束时提交 finish 派生指标。 +- 结算展示 `serverResult`、`scoreSummary`、`antiCheatFlags`、leaderboard entry。 +- 麦克风原始音频不上传。 + +**Verification:** + +```bash +npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx +``` + +--- + +## 5. 阶段四:投影与列表体验 + +### Task 4.1: 排行榜 UI + +**Files:** +- Create: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.tsx` +- Test: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.test.tsx` +- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx` + +**Rules:** +- 查询维度 `workId + difficultyPreset + rulesetVersion`。 +- 只展示胜利入榜成绩。 +- 不展示平/负/flagged 历史。 + +### Task 4.2: 个人历史最近记录 + 最佳摘要 UI + +**Files:** +- Create: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.tsx` +- Test: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.test.tsx` + +**Rules:** +- 默认最近 20 条。 +- 仅本人可见。 +- 可按 workId / difficultyPreset 过滤。 +- flagged 只做轻提示,不展示详细反作弊原因。 + +### Task 4.3: 作品统计展示 + +**Files:** +- Create: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.tsx` +- Test: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.test.tsx` + +**Fields:** +- `playStartCount` +- `finishCount` +- `winCount` +- `drawCount` +- `lossCount` +- `flaggedCount` +- `leaderboardEntryCount` +- `bestLeaderboardScore` +- `bestFinalEnergy` +- `averageFinalEnergy` +- `updatedAt` + +### Task 4.4: 广场卡片/我的作品适配 + +**Files:** +- Modify: `src/components/custom-world-home/creationWorkShelf.ts` +- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx` +- Modify: `src/components/rpg-entry/rpgEntryWorldPresentation.ts` +- Modify: `src/services/publicWorkCode.ts` 如分享码需要支持 + +**Rules:** +- Bark Battle 作品能展示、打开详情、开始游玩。 +- 不新增独立 Bark Battle 专区。 + +--- + +## 6. 阶段五:收口验证 + +### Task 5.1: 自动测试清单 + +```bash +cargo test -p shared-contracts bark_battle +cargo test -p module-bark-battle +cargo test -p spacetime-module bark_battle +cargo test -p spacetime-client bark_battle +cargo test -p api-server bark_battle +npm test -- --run packages/shared/src/contracts/barkBattle.test.ts +npm test -- --run src/services/bark-battle/barkBattleClient.test.ts +npm test -- --run src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx +npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx +npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false +npm run check:encoding +git diff --check +``` + +### Task 5.2: 后端 smoke + +1. 按项目脚本启动 SpacetimeDB + api-server,优先使用 `npm run api-server`,不要使用旧命令。 +2. 确认 `/healthz`。 +3. smoke 流程:创建草稿 → 保存配置 → 发布 → 拉 runtime config → start run → finish run → 查询 leaderboard/history/stats。 + +### Task 5.3: 人工验收路径 + +1. 进入创作入口/玩法选择,选择 Bark Battle。 +2. 在单页轻配置表单中填写标题,选择主题、狗狗皮肤、难度,保持排行榜开启。 +3. 保存草稿。 +4. 发布作品。 +5. 发布后自动进入作品详情。 +6. 点击开始游玩进入正式 runtime。 +7. 授权麦克风,完成 30 秒单局。 +8. 结算页显示后端 `serverResult` 和 score summary。 +9. 若胜利,排行榜出现本局成绩。 +10. 我的记录显示最近记录和个人最佳摘要。 +11. 作品详情/作者视角能看到作品统计。 +12. 广场/作品卡片和我的作品入口都能再次进入详情和 runtime。 + +--- + +## 7. 不做范围 + +- 不做实时多人。 +- 不做 ghost replay。 +- 不做 AI 狗叫识别。 +- 不保存原始音频、PCM、waveform 或可还原语音内容。 +- 不做独立 Bark Battle 专区/活动页。 +- 不做挑战分享、好友邀请、多人数房间。 +- 不做复杂编辑器、多步骤向导、规则参数编辑、AI 生成配置。 +- 不做 DAU/留存、按小时统计曲线、好友对比。 + +--- + +## 8. 三人并行建议 + +### 开发者 A:后端契约与领域规则 + +负责 Task 1.1、1.3、1.4。先提交 contracts 与 `module-bark-battle`,为后续后端/前端提供稳定类型和裁决规则。 + +### 开发者 B:SpacetimeDB + api-server + +负责 Task 2.1 到 2.6。必须等开发者 A 的 DTO/领域规则基本稳定后开始,或先基于计划字段开分支实现表结构。 + +### 开发者 C:前端纵切与 UI + +负责 Task 3.x 与 4.x。开始时可先做组件空态和 service client 类型,真正联调等 B 的 BFF ready。 + +--- + +## 9. 推荐提交节奏 + +1. `feat: add bark battle contracts and domain rules` +2. `feat: add bark battle spacetime tables and reducers` +3. `feat: add bark battle api server routes` +4. `feat: add bark battle creation editor` +5. `feat: connect bark battle runtime to server results` +6. `feat: add bark battle leaderboard history stats` +7. `docs: finalize bark battle phase2 verification guide` + +--- + +## 10. 完成定义 + +Phase 2 完成必须同时满足: + +- Bark Battle 可以从正式创作入口创建轻配置作品。 +- 作品可以发布为稳定 workId。 +- 作品详情/广场/我的作品可以发现并进入正式 runtime。 +- runtime 从后端发布态 config 拉配置。 +- start run 写 `work_play_start`。 +- finish 只上传派生指标。 +- 后端裁决 `serverResult` / `scoreSummary` / `leaderboardScore` / `antiCheatFlags`。 +- 胜利局进入按 `workId + difficultyPreset + rulesetVersion` 分榜的排行榜。 +- 个人历史和作品统计可查询。 +- 自动测试、encoding、typecheck、diff check 和人工验收路径通过。 diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 47a5110c..dde079e5 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,14 +16,72 @@ --- -## 2026-05-14 抓大鹅物品素材 sheet 改用 APIMart nanobanana +## 2026-05-14 创作页图像输入统一封装为图像组件 -- 背景:抓大鹅 2D 五视角物品素材仍沿用 5x5 sheet、绿幕去背、切图、OSS 转存和 `generatedItemAssets` 持久化,但用户要求物品素材图片生成步骤改用 APIMart 已接好的 nanobanana / Gemini 图片模型。 -- 决策:抓大鹅物品素材 sheet 生图固定走 APIMart `POST {APIMART_BASE_URL}/images/generations`,模型为 `gemini-3.1-flash-image-preview`,`size = 1:1`,`resolution = 1K`,`official_fallback = true`;响应优先读图片 URL 或 base64,缺图片时按 `task_id` 轮询 `/tasks/{task_id}`。封面、9:16 纯背景图、1:1 容器 UI 图、音频、切图、OSS、扣费和运行态消费链路保持不变。 -- 影响范围:`server-rs/crates/api-server/src/match3d.rs`、`server-rs/crates/api-server/src/config.rs`、`deploy/env/api-server.env.example`、抓大鹅素材生成技术文档。 -- 验证方式:执行 `cargo test -p api-server match3d_material_sheet --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server from_env_reads_non_public_models_and_urls --manifest-path server-rs\Cargo.toml`、`cargo check -p api-server --manifest-path server-rs\Cargo.toml`、`npm run check:encoding`。 +- 背景:拼图创作页已经具备“画面描述生图 / 多参考图生图 / 上传主图后 AI 重绘 / 上传主图后不重绘”四条路径,抓大鹅封面和后续创作页也会复用同一套交互;继续在页面内复制会导致参考图、预览、删除确认和重绘开关漂移。 +- 决策:通用图像输入 UI 统一使用 `src/components/common/CreativeImageInputPanel.tsx`。组件采用受控模式,只负责主图上传卡、画面描述输入、参考图缩略图与预览、AI 重绘开关、错误展示和提交按钮;外层页面负责文件读取/裁剪、历史素材弹层、计费确认、自动保存和具体后端请求。 +- 影响范围:拼图创作入口、后续抓大鹅封面生成入口、其它需要复用图像输入链路的创作页。 +- 验证方式:拼图入口交互测试继续覆盖四种路径;后续页面接入时只传入业务回调与文案,不复制上传卡和参考图缩略图实现。 +- 关联文档:`docs/technical/【前端体验】图像组件统一封装与复用边界-2026-05-14.md`。 + +## 2026-05-14 汪汪声浪创作入口改为创作 Tab 内嵌轻配置 + +- 背景:汪汪声浪入口最初走独立配置阶段,和拼图、抓大鹅的创作页内嵌结构不一致,用户在入口切换时会感觉像跳到了另一张页面。 +- 决策:`bark-battle` 的创作入口只在创作 Tab 内嵌渲染轻配置表单,入口点击只切到创作页并选中该模板,不再使用 `bark-battle-config` 独立阶段;runtime 退出时回到创作页并恢复汪汪声浪模板选中态。 +- 影响范围:`PlatformEntryFlowShellImpl`、`BarkBattleConfigEditor`、`BarkBattleRuntimeShell`、入口配置说明和相关交互测试。 +- 验证方式:创作 Tab 中点击汪汪声浪后直接看到内嵌表单,不应再出现单独配置页;发布进入 runtime 后退出应回到创作页的汪汪声浪模板。 +- 关联文档:`docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md`。 + +## 2026-05-14 拼图与抓大鹅生成页移动端收口为等待与计时双栏 + +- 背景:拼图与抓大鹅的草稿生成页在移动端同时展示“当前批次”“预计等待”“计时”时,模型执行视角过重,信息也显得散。 +- 决策:这两类轻量玩法的生成页隐藏“当前批次”模块,只保留“预计等待”和“计时”并排展示;生成步骤进入页面时按顺序从左侧滑入,强化推进感。 +- 影响范围:`CustomWorldGenerationView`、拼图与抓大鹅创作入口调用处、移动端生成页体验文档。 +- 验证方式:拼图与抓大鹅生成页在手机竖屏下只显示等待与计时双栏,步骤卡按顺序滑入;其它未传入隐藏参数的生成页继续保留原批次模块。 +- 关联文档:`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 + +## 2026-05-14 移动端输入法弹出时平台画布不压缩 + +- 背景:平台根壳使用 `100dvh` 后,手机浏览器输入法弹出会让可见视口变小,导致创作首页、推荐页等固定游戏式画布被重新压缩。 +- 决策:主站入口统一注册移动端输入法聚焦适配;输入法未打开时记录稳定布局高度,输入法打开期间 `.platform-viewport-shell` 不跟随 `visualViewport.height` 缩小,只通过 `--platform-keyboard-focus-offset` 上移画面聚焦当前输入框,并临时隐藏移动端底部 dock。 +- 影响范围:主站平台壳、移动端创作首页底部输入框、后续所有复用 `.platform-viewport-shell` 的输入表单;业务组件不重复注册键盘适配。 +- 验证方式:手机竖屏点击输入框,画布不压缩,输入框移动到输入法上方;输入法关闭后画布回位,底部 dock 恢复。 +- 关联文档:`docs/technical/【前端体验】移动端输入法不压缩画布聚焦方案-2026-05-14.md`、`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 + +## 2026-05-14 抓大鹅物品素材批量重新生成复用 item-assets 替换模式 + +- 背景:抓大鹅结果页 `素材配置 > 物品` 需要在不改变玩法物品映射的前提下,批量重新生成已存在物品的 2D 五视角图片。 +- 决策:继续复用 `POST /api/creation/match3d/works/{profileId}/item-assets`,请求体通过 `mode = "replace"` 表达替换模式;前端面板预填当前素材名称,只提交仍能匹配到已有素材的名称。后端只替换匹配素材的 `imageSrc/imageObjectKey/imageViews/status/error`,保留原 `itemId`、列表顺序、模型兼容字段、UI 背景、历史背景音乐和点击音效字段;未匹配名称不计费、不新增、不持久化。 +- 影响范围:Match3D 结果页素材配置、前端/后端 shared contracts、`api-server` Match3D item-assets 编排、运行态物品类型映射和素材生成技术文档。 +- 验证方式:执行 `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`、`npm run check:encoding`。 - 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 +## 2026-05-14 拼图与抓大鹅音频生成入口临时关闭 + +- 背景:当前需要暂时关闭抓大鹅、拼图中生成背景音乐和音效的能力,并隐藏草稿中的相关入口。 +- 决策:拼图 `compile_puzzle_draft` 不再自动生成背景音乐,结果页素材配置只保留 `UI`;抓大鹅 `match3d_compile_draft` 和批量新增只生成 2D 图片、背景和容器 UI,不再调用 Suno/Vidu,结果页隐藏 `背景音乐` 子 Tab 与点击音效生成控件;通用 `/api/creation/audio/*` 当前整体返回 `410 Gone`。历史已写入的 `backgroundMusic` / `clickSound` 字段保留,运行态继续兼容播放旧音频。 +- 影响范围:`api-server` 拼图/抓大鹅草稿编排、通用创作音频路由、拼图/抓大鹅结果页、生成进度模型、相关技术文档。 +- 验证方式:执行拼图/抓大鹅结果页定向测试、生成进度单测、`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 和 `npm run check:encoding`。 +- 关联文档:`docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + +## 2026-05-14 抓大鹅物品素材 sheet 改用 VectorEngine Gemini + +- 背景:抓大鹅 2D 五视角物品素材仍沿用 5x5 sheet、绿幕去背、切图、OSS 转存和 `generatedItemAssets` 持久化,但用户要求物品素材图片生成步骤改用 VectorEngine Apifox `api-381740608` 对应的 Gemini 原生图片接口。 +- 决策:抓大鹅物品素材 sheet 生图固定走 VectorEngine `POST {VECTOR_ENGINE_BASE_URL}/v1beta/models/gemini-3-pro-image-preview:generateContent?key={VECTOR_ENGINE_API_KEY}`,请求体使用 `contents[].parts[].text` 与 `generationConfig.responseModalities = ["TEXT", "IMAGE"]`、`imageConfig.aspectRatio = "1:1"`;响应从 `candidates[].content.parts[].inlineData.data` / `inline_data.data` 读取 base64 图片。封面、9:16 纯背景图、1:1 容器 UI 图、切图、OSS、扣费和运行态消费链路保持不变;音频以后续“拼图与抓大鹅音频生成入口临时关闭”决策为准。 +- 影响范围:`server-rs/crates/api-server/src/match3d.rs`、`server-rs/crates/api-server/src/config.rs`、`deploy/env/api-server.env.example`、抓大鹅素材生成技术文档。 +- 验证方式:执行 `cargo test -p api-server match3d_material_sheet --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server match3d_vector_engine_gemini --manifest-path server-rs\Cargo.toml`、`cargo check -p api-server --manifest-path server-rs\Cargo.toml`、`npm run check:encoding`。 +- 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + +## 2026-05-14 草稿页作品卡对齐分类页列表 + +- 背景:草稿页作品架原本偏封面大卡片,和发现页分类列表的横向卡片样式不一致;生成中状态也缺少整卡级的统一遮罩。 +- 决策:草稿页作品卡统一收口为与分类页一致的横向列表卡结构,左侧承载标题/状态/类型/摘要与必要数据,右侧显示带透明度的封面图;移动端保持单列列表,网页端使用两到三列卡片式网格,避免宽屏长条列表。不再常驻“继续创作”“查看详情”“查看进度”等右侧动作按钮。原有删除、分享、积分激励、公开统计、未读红点全部保留,其中删除与分享进入左滑操作层,常态不显示删除按钮,也不得透出删除底层。生成中的作品在整卡上加半透明蒙版、旋转等待符号和“生成中...”标识,但不移除任何原有信息。 +- 影响范围:`src/components/custom-world-home/CustomWorldCreationHub.tsx`、`src/components/custom-world-home/CustomWorldWorkCard.tsx`、相关样式与测试、草稿页 UI 文档。 +- 验证方式:草稿页作品卡与分类页列表视觉口径保持一致;`npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md`、`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 + +2026-05-14 补充:草稿页作品卡不再用“草稿 / 已发布”文字标识状态,改为图标化 UI 状态点;作品封面直接铺到卡片右半区并从右向左渐隐;已发布作品右上角常驻分享图标;草稿长按弹出删除面板,已发布长按弹出分享和删除面板。 + ## 2026-05-13 认证运行期同步直接导入正式认证表 - 背景:`auth_store_snapshot` 是 Stage 1 整包快照过渡表,主键固定 `default`,会让所有用户状态集中在一条 `snapshot_json` 中;Stage 2/3 已有 `user_account/auth_identity/refresh_session` 正式认证表,继续刷新 `default` 容易让运行时真相和表拆分目标混在一起。 @@ -82,8 +140,8 @@ ## 2026-05-12 抓大鹅结果页素材编辑统一走作品级资产面板 -- 背景:抓大鹅结果页需要支持碰面图上传 / AI 重绘、物品素材独立预览、单项删除和批量新增,且不能把素材编辑继续做成列表内联展开或前端临时状态。 -- 决策:结果页 `作品信息` 的碰面图点击打开独立面板,参考图可来自本地上传、物品素材和 UI 素材;AI 重绘统一调用 `POST /api/creation/match3d/works/{profileId}/cover-image` 并转存到 `generated-match3d-assets`。`素材配置 > 物品` 列表项点击打开独立预览面板,不再提供单项重新生成按钮;单项删除和批量新增都写回同一份 `generated_item_assets_json`。批量新增调用 `POST /api/creation/match3d/works/{profileId}/item-assets`,复用草稿生成的 2D 素材图、5x5 切图、OSS 上传和可选点击音效链路,仅作用于新增物品,不新增 SpacetimeDB 表。 +- 背景:抓大鹅结果页需要支持封面图上传 / AI 重绘、物品素材独立预览、单项删除和批量新增,且不能把素材编辑继续做成列表内联展开或前端临时状态。 +- 决策:结果页 `作品信息` 的封面图点击打开独立面板,封面图面板对齐拼图入口上传卡。已有上传主图时,请求体传 `uploadedImageSrc`,AI 重绘走 VectorEngine `/v1/images/edits`;关闭 AI 重绘时只写回上传图,不调用生图。没有上传主图时,请求体传 `referenceImageSrcs`,可混合本地上传、物品素材和 UI 素材,多参考图作为 `gpt-image-2-all` generations 的 `image` 数组传入。生成结果统一调用 `POST /api/creation/match3d/works/{profileId}/cover-image` 并转存到 `generated-match3d-assets`。`素材配置 > 物品` 列表项点击打开独立预览面板,不再提供单项重新生成按钮;单项删除和批量新增都写回同一份 `generated_item_assets_json`。批量新增调用 `POST /api/creation/match3d/works/{profileId}/item-assets`,复用草稿生成的 2D 素材图、5x5 切图、OSS 上传和可选点击音效链路,仅作用于新增物品,不新增 SpacetimeDB 表。 - 影响范围:Match3D 结果页、Match3D works shared contracts、`api-server` Match3D 作品路由、生成资产历史类型和草稿恢复路径。 - 验证方式:执行 `npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`npm run typecheck`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`。 - 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -103,6 +161,22 @@ - 验证方式:执行 `npm run check:encoding`、`node scripts/check-wechat-miniprogram-auth-smoke.mjs`、`cargo test -p shared-contracts wechat_bind_phone_request_accepts_mini_program_phone_code --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat_miniprogram_bind_phone_code_activates_pending_user --manifest-path server-rs/Cargo.toml -- --nocapture`。 - 关联文档:`docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md`。 +## 2026-05-13 宝贝爱画先作为寓教于乐独立本地 Demo 落地 + +- 背景:第三关 `宝贝爱画` 需要默认出现在“发现 / 寓教于乐”板块下方,但本阶段只验证画板、手部绘制、绘画魔法和本地保存闭环,不进入创作模板、公开作品或正式持久化。 +- 决策:`baby-love-drawing / 宝贝爱画` 先作为独立运行态接入,入口由发现页寓教于乐默认卡片打开,并支持 `/runtime/baby-love-drawing` 直达;关闭 `VITE_ENABLE_EDUTAINMENT_ENTRY` 时前端不展示频道/卡片且直达路由回落主应用。绘画魔法统一走 `POST /api/creation/edutainment/baby-love-drawing/magic` 后端安全代理,使用 VectorEngine `gpt-image-2-all` 与原始画布 Data URL 参考图生成绘本风图片;保存只写 localStorage,正式持久化后续再设计。 +- 影响范围:`packages/shared/src/contracts/edutainmentBabyDrawing.ts`、`src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx`、`src/services/edutainment-baby-drawing/`、`src/routing/appRoutes.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`server-rs/crates/api-server/src/edutainment_baby_drawing.rs`、`src/index.css`、宝贝爱画 PRD 与技术方案。 +- 验证方式:执行宝贝爱画 model/runtime/service/route 定向测试、`npm run typecheck`、定向 ESLint、`cargo test -p api-server edutainment_baby_drawing --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server resolves_runtime_paths_to_creation_type_ids --manifest-path server-rs/Cargo.toml` 和编码检查;真实魔法生成需配置 `VECTOR_ENGINE_BASE_URL` 与 `VECTOR_ENGINE_API_KEY`。 +- 关联文档:`docs/prd/BABY_LOVE_DRAWING_EDUTAINMENT_LEVEL_PRD_2026-05-13.md`、`docs/technical/BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md`。 + +## 2026-05-12 宝贝识物创作同时生成玩法视觉主题包 + +- 背景:`宝贝识物` 创作原本只根据两个关键词生成物品透明图,运行态背景、UI、礼物盒和篮子仍使用固定 CSS 绘本风,无法根据“小猪佩琪 / 奥特曼”或“苹果 / 橘子”等创作者提示词做主题化包装。 +- 决策:`POST /api/creation/edutainment/baby-object-match/assets` 同一次 image-2 / VectorEngine 调用链返回两个物品图和 `visualPackage`。视觉包包含 `background`、`ui-frame`、`gift-box`、`basket`、`smoke-puff` 五类资源;总风格保持寓教于乐明亮卡通绘本插画风,主题按两个物品关键词匹配,水果偏果园自然,动漫角色 / 玩具偏动漫玩具。物品图和礼物盒 / 篮子 / UI / 烟雾特效资源走透明 PNG 后处理,背景为清爽不遮挡玩法区的环境图;运行态中礼物盒按约 2 倍视觉尺寸展示、篮子按约 1.5 倍展示,礼物盒打开时使用 `smoke-puff` 弹出中央物品并移除礼盒。前端草稿保存该包,运行态消费该包;旧草稿以 `visualPackage = null` 继续使用 CSS 兜底。 +- 影响范围:`packages/shared/src/contracts/edutainmentBabyObject.ts`、`server-rs/crates/api-server/src/edutainment_baby_object.rs`、`src/services/edutainment-baby-object/babyObjectMatchClient.ts`、`src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx`、`src/index.css`、宝贝识物 PRD 与技术方案。 +- 验证方式:执行宝贝识物 service / runtime 定向测试、`cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml`、相关 ESLint 与编码检查;真实生图需配置 `VECTOR_ENGINE_BASE_URL` 与 `VECTOR_ENGINE_API_KEY`。 +- 关联文档:`docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md`、`docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md`。 + ## 2026-05-11 拼图与抓大鹅结果页音频资产复用通用创作音频链路 - 背景:拼图和抓大鹅结果页需要接入 Suno 背景音乐,抓大鹅还需要物体点击音效,但当前两类作品没有独立的作品级音频表或 metadata 字段。 @@ -136,6 +210,14 @@ - 验证方式:执行入口配置、创作 Hub 和平台入口交互定向测试,确认看不到“方洞挑战” Tab、按钮和作品架条目。 - 关联文档:`docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md`、`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。 +## 2026-05-14 视觉小说从创作页入口隐藏 + +- 背景:当前创作页需要关闭视觉小说模板入口,不能继续在模板 Tab、旧选择弹层或创作 Hub 卡片中展示。 +- 决策:SpacetimeDB `creation_entry_type_config` 默认种子中 `visual-novel.visible=false` 且 `open=false`;旧默认可见配置会被迁移为隐藏和关闭。前端继续只消费 `GET /api/creation-entry/config`,不得用硬编码恢复视觉小说模板入口。 +- 影响范围:SpacetimeDB 入口配置默认种子、api-server 测试配置、创作页模板 Tab、创作 Hub 测试和创作入口文档。 +- 验证方式:执行入口配置、创作 Hub、平台入口交互和 api-server 路由熔断定向测试,确认“视觉小说”不出现在创作页且 `/api/creation/visual-novel/*` 默认被熔断。 +- 关联文档:`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`、`docs/technical/ADMIN_CREATION_ENTRY_SWITCH_CONFIG_2026-05-11.md`。 + ## 2026-05-10 运行态输入设备抽象层全项目通用化 - 背景:拼图运行态接入 mocap 后,鼠标/触控和 mocap 各自维护输入逻辑会导致合并大块、拖拽语义和取消会话行为不一致;后续其他玩法也需要复用体感、摇杆、键盘等设备输入。 @@ -147,7 +229,7 @@ ## 2026-05-11 前端调试模式统一判断 - 背景:拼图 mocap 调试面板此前在运行态常驻展示,生产构建和正式体验里容易遮挡棋盘内容;后续其它局部诊断 UI 也需要统一的调试模式入口。 -- 决策:前端新增 `src/config/debugMode.ts` 作为全局调试模式判断,默认跟随 Vite 开发态,允许 `VITE_DEBUG_MODE=true/false` 显式覆盖。拼图运行态 mocap 调试面板只在调试模式下渲染,并默认折叠,只保留连接状态行。 +- 决策:前端新增 `src/config/debugMode.ts` 作为全局调试模式判断,默认跟随 Vite 开发态,允许 `VITE_DEBUG_MODE=true/false` 显式覆盖。2026-05-14 起,拼图运行态已临时移除 mocap 调用、体感光标和 mocap 调试面板;调试模式仍供其它局部诊断 UI 使用。 - 影响范围:前端局部调试 UI、拼图运行态 mocap 诊断面板、`.env.example` 和运行态输入技术文档。 - 验证方式:执行 `npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。 - 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`。 @@ -298,6 +380,14 @@ - 验证方式:检查移动端底部导航文案和顺序,确认登录态为“推荐/发现/创作/草稿/我的”,未登录态为“推荐/创作/发现”且创作居中;“推荐”无搜索/频道栏直出作品流,“发现”包含搜索/推荐/今日/分类/排行,“创作”只显示新建入口,“草稿”显示作品架,“我的-玩过”可恢复存档。 - 关联文档:`docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md`。 +## 2026-05-14 推荐页卡片主视觉优先于底部作者热区 + +- 背景:移动端推荐页的卡片底部作者与操作区如果过高,会压缩作品运行态可视高度,影响首屏沉浸感。 +- 决策:推荐页卡片底部信息区保持紧凑固定高度,切换手势仍只绑定在该区域;视觉主体高度优先扩展,不再让作者信息区占用过多首屏空间。 +- 影响范围:`src/components/rpg-entry/RpgEntryHomeView.tsx` 的推荐页卡片布局,以及 `src/index.css` 中的推荐页卡片热区样式。 +- 验证方式:移动端推荐页首屏应明显看到更大的作品内容区,底部作者信息区只保留紧凑一条,不再明显挤压运行态。 +- 关联文档:`docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md`。 + ## 2026-05-05 创作 Tab 固定为智能创作首页,草稿 Tab 承接旧作品架 - 背景:创作首页需要变成面向对话式生成的智能创作页,旧模板卡和作品架继续保留但不应再占据创作首屏。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 5f73294a..ab9c218a 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -69,6 +69,8 @@ npm run dev:web npm run api-server ``` +该命令会保留终端实时输出,并把同一份输出持久化到 `logs/api-server/api-server-.log`。完整联调入口 `npm run dev` / `npm run dev:rust` 启动的 Rust `api-server` 也会写入 `logs/api-server/api-server-dev-rust-.log`。如需改写路径,可设置 `GENARRATIVE_API_SERVER_LOG_FILE`;如只改目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR`。 + 查看本地 Rust/SpacetimeDB 日志: ```bash @@ -159,6 +161,8 @@ npm run check:server-rs-ddd - 检查 `/healthz`。 - 执行对应自动测试。 - 涉及 SpacetimeDB 表、reducer、procedure、row shape 或绑定变化时,同步更新 `migration.rs`、表目录和生成绑定。 +- SpacetimeDB 已有表新增字段必须放在 Rust 表结构体最后,并设置明确默认值;需要修改字段名时,先询问用户并确认迁移计划,再同步更新 `server-rs/crates/spacetime-module/src/migration.rs`、表目录和生成绑定。 +- 修改 SpacetimeDB schema 后运行 `npm run check:spacetime-schema`,用自动检查拦截缺 default、插入中间、字段删除/改名/重排/改类型,以及漏改迁移、表目录或绑定。 关键文档: diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 5be6e6a2..44c7fc30 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -14,6 +14,38 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 图像输入组件不要把业务状态藏在页面内联实现里 + +- 现象:拼图页把参考图上传、缩略图、主图删除确认和 AI 重绘开关内联实现后,后续想复用到其它创作页时,页面级状态和通用 UI 状态混在一起,容易出现多套上传卡和参考图展示口径。 +- 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。 +- 处理:抽 `CreativeImageInputPanel` 时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。 +- 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。 +- 关联:`src/components/common/CreativeImageInputPanel.tsx`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`。 + +## 汪汪声浪入口不要再回到独立配置阶段 + +- 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。 +- 原因:旧实现把 `bark-battle` 单独挂到 `bark-battle-config` selectionStage,而不是复用创作 Tab 里的模板区。 +- 处理:入口点击只设置 `activeCreationFormType = 'bark-battle'` 并回到创作 Tab;`BarkBattleConfigEditor` 作为内嵌表单使用,默认隐藏返回按钮和页面标题;runtime `onExit` 重新回到创作 Tab 的汪汪声浪模板。 +- 验证:点击汪汪声浪后直接看到创作页内嵌表单,不再出现独立配置页;测试应覆盖内嵌表单与 runtime 返回路径。 +- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`。 + +## 抓大鹅批量重新生成物品不要新增 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/mod.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`。 @@ -63,11 +95,19 @@ - 现象:点击生成抓大鹅草稿后,页面只提示“服务暂不可用”,或者本地 `npm run api-server` 看似启动但生成接口不可用。 - 原因:配置缺失类错误通常在后端 `error.details.reason` 中给出具体缺项,前端如果只读 `details.message` 会吞掉原因;本地只配置 `ALIYUN_OSS_BUCKET` / `ALIYUN_OSS_ENDPOINT` 时,旧逻辑还会在启动期构造空 AccessKey 的 OSS 客户端并失败。抓大鹅新链路仍是 2D 生图切割,不需要也不应回退 Rodin/GLB。 -- 处理:前端 API 错误展示读取 `details.message` 后继续读取 `details.reason`;`api-server` 只有在 OSS 四件套齐全时初始化 OSS 客户端,部分缺失只记 warning 并让具体 generated 上传/换签接口返回 `OSS 未完成环境变量配置`。抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS,并通过 `details.missingEnv` 列出缺项;真实生成需补齐 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和完整 `ALIYUN_OSS_*` 四件套。抓大鹅 `5*5` 素材图提示词还必须要求相邻物体主体至少保留 `1/4` 单格宽度空白间距,避免切割后相邻格内容污染。 +- 处理:前端 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_*` 四件套。抓大鹅 `5*5` 素材图提示词还必须要求相邻物体主体至少保留 `1/4` 单格宽度空白间距,避免切割后相邻格内容污染。 - 验证:`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 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-14 补充:抓大鹅“物品素材 sheet”已改用 APIMart `gemini-3.1-flash-image-preview`,真实生成还需要 `APIMART_BASE_URL`、`APIMART_API_KEY` 和 `APIMART_IMAGE_REQUEST_TIMEOUT_MS`;封面、背景图和容器 UI 仍继续使用 VectorEngine。排查时先按失败阶段区分缺 APIMart、VectorEngine 还是 OSS,不能把物品素材缺 APIMart 误判成 VectorEngine 缺配置。 +2026-05-14 补充:抓大鹅“物品素材 sheet”已改用 VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`,真实生成读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;封面和 `9:16` 背景图走 VectorEngine `/v1/images/generations`,`1:1` 容器 UI 走 VectorEngine `/v1/images/edits` multipart 参考图链路。排查素材 sheet 时看请求路径是否为 `/v1beta/models/gemini-3-pro-image-preview:generateContent?key=...`,响应图片在 `candidates[].content.parts[].inlineData.data` / `inline_data.data`,不要再按 APIMart `/images/generations` 或 `/tasks/{task_id}` 排查。 + +## 抓大鹅发布按钮要先开发布面板,封面编辑收口到发布面板内 + +- 现象:抓大鹅结果页发布按钮看起来点不了,或者封面编辑仍然分散在作品信息 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 配置 @@ -85,15 +125,55 @@ - 验证:运行 `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` 反馈阶段继续累计手部路径,会把抓握、反馈期间残留移动或未知侧别手部误算成下一次选篮。 -- 处理:宝贝识物选篮只使用明确 `leftHand` / `rightHand` 的连续横向轨迹阈值;侧别为 `unknown` 的手部轨迹不参与选篮;礼物盒打开和反馈阶段清空轨迹,不在非 `active` 阶段累计路径。礼物盒激活仍使用 `open_palm -> grab` 抓握序列。 +- 处理:宝贝识物选篮只使用明确 `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`。 +## 宝贝爱画左右手反了先查 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。 +- 原因:宝贝识物一次创作会生成 2 张物品图和 `background`、`ui-frame`、`gift-box`、`basket`、`smoke-puff` 5 张视觉包装图。旧前端只等待 180 秒并对长耗时 POST 自动重试,容易在 VectorEngine 仍在生成时先 abort,再重复发起第二次生成;上游某张图超过后端 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 或返回 5xx 时会表现为 502。 +- 处理:`babyObjectMatchClient` 对 `/api/creation/edutainment/baby-object-match/assets` 使用 10 分钟超时并取消自动重试;后端并发启动物品图和视觉主题包生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,按资源类别输出开始、完成和耗时日志。 +- 验证:运行 `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 资源生成完成` 耗时是否小于前端超时,若仍 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`。 + +## 寓教于乐作品和宝贝识物模板同时消失先查入口种子 + +- 现象:发现页“寓教于乐”分类下已发布的宝贝识物作品突然消失,同时创作界面模板选项中也看不到或无法正常展示 `宝贝识物`。 +- 原因:创作入口配置事实源已迁到 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 配置。 @@ -147,16 +227,16 @@ - 现象:拼图有参考图时返回 `拼图图片生成失败:创建拼图 VectorEngine 图片编辑任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/edits)`,后端没有 `拼图 VectorEngine 图片编辑 HTTP 返回` 日志。 - 原因:这是 `reqwest` 在 `send()` 阶段失败,尚未收到 VectorEngine HTTP 响应;常见原因是服务器网络 / DNS / 防火墙 / 代理问题,或上游网关中断 multipart 连接。 - 处理:查看错误响应 `details.reason/source/connect/body/timeout/endpoint` 和 `拼图 VectorEngine 请求发送失败` 日志。拼图图片客户端已强制 HTTP/1.1,降低 multipart HTTP/2 兼容风险;若 `connect=true` 先查网络出口,若 `body=true` 先查参考图大小和 multipart 发送。 -- 验证:`curl -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2-all" -F "prompt=test" -F "n=1" -F "size=1024x1024"` 至少应返回 HTTP `401`,说明域名、TLS 和路径可达;执行 `cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`。 +- 验证:`curl --http1.1 -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2" -F "prompt=test" -F "n=1" -F "size=1024x1024" -F "image=@public/match3d-background-references/pot-fused-reference.png;type=image/png"` 至少应返回 HTTP `401`,说明域名、TLS、路径和 multipart 上传可达;执行 `cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`。 - 关联:`server-rs/crates/api-server/src/puzzle.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 背景缺失先区分生成失败和消费链路丢字段 -- 现象:拼图草稿生成完成后,草稿数据里已有首关 `uiBackgroundImageSrc`,结果页素材配置也能看到背景图,但自动试玩或结果页“试玩”进入局内仍只显示封面模糊背景,甚至看不到 UI 背景。 -- 原因:生成完成后的自动试玩走前端 `startLocalPuzzleRun(...)` 本地运行态兜底,不经过后端 `start_puzzle_run`;如果本地 run 只把 `coverImageSrc` 带入 `currentLevel`,就会丢掉 `levels[].uiBackgroundImageSrc` 和 `levels[].backgroundMusic`。 -- 处理:`startLocalPuzzleRun` 与本地下一关 handoff 都要从关卡 `levels[]` 复制 `uiBackgroundImageSrc`、`backgroundMusic` 到 `currentLevel`;`PuzzleRuntimeShell` 继续读取 `currentLevel.uiBackgroundImageSrc` 渲染全屏背景。 -- 验证:`npm run test -- src/services/puzzle-runtime/puzzleLocalRuntime.test.ts src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`。 -- 关联:`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 背景,或结果页能看到背景但自动试玩 / 结果页“试玩”进入局内仍只显示封面模糊背景。 +- 原因:`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 又变空先查结果页回包合并 @@ -177,11 +257,19 @@ ## 拼图草稿生成 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-all`,默认 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000`;若上游在该窗口内未返回,后端退款并返回超时错误。旧前端 action 写请求会对 502/503/504 自动重试一次,导致同一次点击重复触发生图与扣退费。 +- 原因:首图生成走 VectorEngine `gpt-image-2-all`,默认 `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 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`。 +## 本地脚本调 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-all` 请求体、同一环境变量下,原生 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 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。 @@ -194,8 +282,8 @@ - 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。 - 原因:SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。 -- 处理:变更前阅读 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`;涉及表变化时同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 bindings;必要时走 JSON 导入导出与分片导入迁移流程。 -- 验证:发布前完成 schema 检查、bindings 生成、表目录更新和相关 smoke。 +- 处理:变更前阅读 `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 publish 报 wasm-bindgen 时先查 shared-contracts feature @@ -234,8 +322,8 @@ - 现象:本地 `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` 地址应与实际端口一致。 +- 处理:`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。显式传 `--skip-spacetime` 时表示复用既有宿主,脚本不再对 SpacetimeDB 端口做可用性漂移;`--spacetime-port 3101` 就是后端要连接的实际端口,避免被误改到空闲但未启动的 `3102`。`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`;日志中的 `[dev:rust] 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: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 可清本地库重发 @@ -316,6 +404,7 @@ - 本地启动脚本没有让 `.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 继续代理到空端口。 - 处理:优先用 `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"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。 @@ -513,11 +602,11 @@ ## Jenkins 生产流水线拉 Git 先本机再域名备用 -- 现象:生产发布、数据库导入导出或服务器配置流水线在目标 Linux agent 上执行 `GitSCM checkout` 时,`http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` 不可达,导致脚本还没拉下来就失败;若 fallback 到公网 Git 时没有限制 refspec、浅克隆和 tags,还可能在约 10 分钟后出现 `git-remote-https died of signal 15`、`early EOF`、`invalid index-pack output`。 -- 原因:`127.0.0.1` 只代表当前执行阶段的 agent 自身;当 release agent 与 Git 服务不在同一台机器,或本机 Git/Web 服务临时不可用时,固定写死 localhost 会阻断 Jenkinsfile 内部源码/脚本 checkout。 -- 处理:Jenkins Job 的 `Pipeline script from SCM` 由 Windows controller 执行,SCM URL 使用公网域名 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`。运行于 Linux agent 的 Jenkinsfile 首次 `checkout([$class: 'GitSCM', ...])` 层先尝试 `GIT_REMOTE_URL=http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后直接尝试 `GIT_REMOTE_FALLBACK_URL=https://git.genarrative.world/GenarrativeAI/Genarrative.git`,不再配置内网 IP fallback;首次 checkout 必须使用目标分支 refspec、`CloneOption shallow=true depth=1 noTags=true honorRefspec=true`。后续统一走 `scripts/jenkins-checkout-source.sh`,该脚本也按主地址、域名备用地址顺序重新 fetch 并把 `origin` 切到实际可用地址;`COMMIT_HASH` 为空时继续 `--depth=1 --no-tags`,只有指定 commit 时才允许加深历史做分支归属校验。 -- 验证:扫描本地 Jenkins live job `config.xml`,确认 SCM `` 都是 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;扫描所有以 `127.0.0.1:3000` 拉 Git 且运行在 Linux agent 的生产 Jenkinsfile,确认存在 `GIT_REMOTE_FALLBACK_URL`、`EFFECTIVE_GIT_REMOTE_URL` 和脚本层 `GIT_REMOTE_FALLBACK_URL` 透传;运行 `bash -n scripts/jenkins-checkout-source.sh`。 -- 关联:`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`。 +- 现象:生产发布、数据库导入导出、服务器配置、构建或 `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` 失败;发布类流水线还可能先遇到 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` 不可达。 +- 原因:`127.0.0.1` 只代表当前执行阶段的 agent 自身;当 release agent 与 Git 服务不在同一台机器,或本机 Git/Web 服务临时不可用时,固定写死 localhost 会阻断 Jenkinsfile 内部源码/脚本 checkout。即使只使用域名 Git,如果 `GitSCM` 没有显式 refspec 并开启 `CloneOption honorRefspec=true`,Jenkins Git 插件也会拉取所有分支。 +- 处理:Jenkins Job 的 `Pipeline script from SCM` 由 Windows controller 执行,SCM URL 使用公网域名 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`。运行于 Linux agent 的 Jenkinsfile 首次 `checkout([$class: 'GitSCM', ...])` 层先尝试 `GIT_REMOTE_URL=http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后直接尝试 `GIT_REMOTE_FALLBACK_URL=https://git.genarrative.world/GenarrativeAI/Genarrative.git`,不再配置内网 IP fallback;所有生产 Jenkinsfile 的首次 checkout 都必须使用目标分支 refspec、`CloneOption shallow=true depth=1 noTags=true honorRefspec=true`。后续统一走 `scripts/jenkins-checkout-source.sh`,该脚本也按主地址、域名备用地址顺序重新 fetch 并把 `origin` 切到实际可用地址;`COMMIT_HASH` 为空时继续 `--depth=1 --no-tags`,只有指定 commit 时才允许加深历史做分支归属校验。 +- 验证:扫描本地 Jenkins live job `config.xml`,确认 SCM `` 都是 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;扫描所有生产 Jenkinsfile 的首次 `GitSCM checkout`,确认 `userRemoteConfigs` 带 `+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}`,`CloneOption` 带 `honorRefspec: true`;运行 `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 下不能裸读 @@ -591,6 +680,22 @@ - 验证:执行 `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 并接管棋盘外观 + +- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 `pot-fused-reference.png`,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。 +- 原因:`/v1/images/generations` 的 `image` 数组更适合弱参考文生图,难以稳定锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。 +- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传;共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`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`,但 `素材配置 > 背景音乐` 或物品详情音效 `