Compare commits
20 Commits
hermes/her
...
hermes/wec
| Author | SHA1 | Date | |
|---|---|---|---|
| 73424f958a | |||
| ed8c93fb5d | |||
| 8ade75390c | |||
| 2801b55d2f | |||
| b24af5a279 | |||
| 4642855fd0 | |||
| cf3dcc6195 | |||
| bca439726d | |||
| 548db78ca7 | |||
| 5c5a8d4a40 | |||
| 514365fdec | |||
| de25324991 | |||
| 1c35662ed5 | |||
| 379ce60839 | |||
| 4f36235f60 | |||
| e55c12b68b | |||
| 502811a103 | |||
| 1d7ef7e4b6 | |||
| cb794601be | |||
| e444266e1e |
@@ -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=<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
|
||||
|
||||
@@ -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 = 180000;
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
@@ -122,7 +122,7 @@ 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"
|
||||
|
||||
1
.gitignore
vendored
@@ -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
|
||||
|
||||
549
.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md
Normal file
@@ -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:<api-port>/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 和人工验收路径通过。
|
||||
@@ -16,14 +16,38 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-14 抓大鹅物品素材 sheet 改用 APIMart nanobanana
|
||||
## 2026-05-14 抓大鹅物品素材批量重新生成复用 item-assets 替换模式
|
||||
|
||||
- 背景:抓大鹅 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`。
|
||||
- 背景:抓大鹅结果页 `素材配置 > 物品` 需要在不改变玩法物品映射的前提下,批量重新生成已存在物品的 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-13 认证运行期同步直接导入正式认证表
|
||||
|
||||
- 背景:`auth_store_snapshot` 是 Stage 1 整包快照过渡表,主键固定 `default`,会让所有用户状态集中在一条 `snapshot_json` 中;Stage 2/3 已有 `user_account/auth_identity/refresh_session` 正式认证表,继续刷新 `default` 容易让运行时真相和表拆分目标混在一起。
|
||||
@@ -82,8 +106,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 +127,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 +176,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 +195,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`。
|
||||
|
||||
@@ -69,6 +69,8 @@ npm run dev:web
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
该命令会保留终端实时输出,并把同一份输出持久化到 `logs/api-server/api-server-<timestamp>.log`。完整联调入口 `npm run dev` / `npm run dev:rust` 启动的 Rust `api-server` 也会写入 `logs/api-server/api-server-dev-rust-<timestamp>.log`。如需改写路径,可设置 `GENARRATIVE_API_SERVER_LOG_FILE`;如只改目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR`。
|
||||
|
||||
查看本地 Rust/SpacetimeDB 日志:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 抓大鹅批量重新生成物品不要新增 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`。
|
||||
|
||||
## OSS V4 签名时间和 bucket/object_key 兼容
|
||||
|
||||
- 现象:OSS V4 私有读签名在部分时间点失败,可能出现 `OSS V4 签名时间格式化失败` 或服务端判定签名格式错误;排查用例中 bucket 为 `xushi-dev`,object_key 为 `generated-square-hole-assets/.../image.png`。
|
||||
@@ -67,7 +75,7 @@
|
||||
- 验证:`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}` 排查。
|
||||
|
||||
## `.hermes` 只放共享内容,不放个人 Hermes 配置
|
||||
|
||||
@@ -85,15 +93,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 配置。
|
||||
@@ -150,13 +198,13 @@
|
||||
- 验证:`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`。
|
||||
- 关联:`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 又变空先查结果页回包合并
|
||||
|
||||
@@ -182,6 +230,14 @@
|
||||
- 验证:运行 `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 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
|
||||
@@ -234,8 +290,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 可清本地库重发
|
||||
@@ -591,6 +647,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`,但 `素材配置 > 背景音乐` 或物品详情音效 `<audio>` 不能播放,Network 可能请求裸 `/generated-match3d-assets/...mp3` 并返回 403。
|
||||
@@ -619,9 +691,26 @@
|
||||
- 现象:微信小程序支付下单能返回 `prepay_id`,但真实支付通知验签失败,或者本地实现误把商户 API 私钥当作回调验签 key。
|
||||
- 原因:商户私钥只用于商户请求微信支付和生成小程序 `paySign`;微信支付通知的 `Wechatpay-Signature` 需要使用微信支付平台公钥或平台证书公钥验签,并按通知头里的平台序列号匹配。
|
||||
- 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:`WECHAT_PAY_PRIVATE_KEY_*` 用于签名,`WECHAT_PAY_PLATFORM_PUBLIC_KEY_*` 与 `WECHAT_PAY_PLATFORM_SERIAL_NO` 用于通知验签,`WECHAT_PAY_API_V3_KEY` 只用于解密通知 resource。支付成功后只通过通知里的 `out_trade_no` 确认本地 pending 订单,并保存 `transaction_id` 到 `profile_recharge_order.provider_transaction_id`。
|
||||
- APIv3 通知成功应答使用 HTTP `204 No Content`,不要沿用 V2 XML 成功报文;失败仍返回 4XX/5XX 让微信重试。
|
||||
- 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
||||
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
||||
|
||||
## 微信支付 JSAPI 下单必须显式带 User-Agent
|
||||
|
||||
- 现象:调用 `/v3/pay/transactions/jsapi` 失败,微信返回“Http头缺少Accept或User-Agent”。
|
||||
- 原因:`reqwest` 请求即使已设置 `Accept: application/json`,也不会默认附带业务侧 `User-Agent`;微信支付网关会校验这两个头。
|
||||
- 处理:`api-server` 的 JSAPI 下单请求统一通过 `with_wechat_pay_jsapi_headers(...)` 设置 `Accept: application/json`、`Content-Type: application/json` 和 `User-Agent: Genarrative-WechatPay/1.0`。
|
||||
- 验证:执行 `cargo test -p api-server jsapi_order_request_sets_wechat_required_http_headers --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
||||
|
||||
## 后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码
|
||||
|
||||
- 现象:后台“表查询”查看 `profile_recharge_order` 时,`kind` 和 `status` 显示为空数组 `[]`,例如充值订单原始行里 `points_60` 的类型和状态都不可读。
|
||||
- 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态 `[variant_index, []]`;后台通用 normalizer 曾把任何 `[0, value]` 都当作 `Option::Some(value)` 展开,导致 `[0, []]` 最终只剩 `[]`。
|
||||
- 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如 `profile_recharge_order.kind` 映射为 `points` / `membership`,`profile_recharge_order.status` 映射为 `pending` / `paid` / `failed` / `closed` / `refunded`。
|
||||
- 验证:执行 `cargo test -p api-server admin_database -- --nocapture`,并确认后台详情弹层的 `raw` 与表格 `cells` 都显示业务字符串。
|
||||
- 关联:`server-rs/crates/api-server/src/admin.rs`、`docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md`。
|
||||
|
||||
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
||||
|
||||
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
||||
@@ -646,6 +735,22 @@
|
||||
- 验证:`cargo test -p api-server match3d_tag_normalization --manifest-path server-rs/Cargo.toml`,并保留 `normalize_match3d_tag("3D素材") == "3D素材"` 的单测。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`。
|
||||
|
||||
## 抓大鹅物品切图白边或绿幕残留先查后端透明化
|
||||
|
||||
- 现象:抓大鹅生成的物品视角图裁剪后仍带白边,或者整块纯绿色绿幕背景没有被透明化,运行态看到绿色方块。
|
||||
- 原因:素材 sheet 可能是“每格内部绿幕、整张图外圈近白底”,内部绿幕不一定连通到 sheet 外边缘;旧 flood fill 只从外边缘找背景会漏掉这种绿幕块。白底抗锯齿如果不纳入抠像和边缘去污染,也会随裁剪输出成一圈白边。
|
||||
- 处理:`api-server` 的 `slice_match3d_material_sheet` 必须先在整张 sheet 上做透明背景后处理:外边缘连通绿幕/近白底清 alpha,非连通但高置信纯绿块也清 alpha,边缘近白和绿幕抗锯齿做透明或去污染;同时保护不够纯的绿色主体像素。
|
||||
- 验证:`cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml` 覆盖非连通绿幕、白边、贴边主体保留和固定 5x5 切图。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 草稿页卡片有真实素材但仍显示黑卡先查摘要字段
|
||||
|
||||
- 现象:草稿页拼图卡片没有关卡图背景,抓大鹅卡片没有背景图或物品图背景,甚至兜底视觉也退回黑色面板。
|
||||
- 原因:拼图列表摘要若不下发 `levels`,前端拿不到关卡 `coverImageSrc` / 候选图;抓大鹅列表摘要若只提供公开 URL、不保留 `generatedBackgroundAsset` 或 `generatedItemAssets` 中的 object key,前端无法换签读取私有生成图。卡片封面组件如果自带暗色默认背景,也会让兜底失败时看起来仍是黑卡。
|
||||
- 处理:拼图 `map_puzzle_work_summary_response` 必须保留 `levels`;草稿页优先用关卡 `coverImageSrc`,再用候选图。抓大鹅货架封面解析必须读取 `backgroundImageObjectKey`、`generatedBackgroundAsset.imageObjectKey/containerImageObjectKey`、`generatedItemAssets[].imageObjectKey` 和 `imageViews[].imageObjectKey`。图片渲染统一交给 `ResolvedAssetImage` 换签,并给卡片传入玩法参考图与暖色底兜底。
|
||||
- 验证:执行 `npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/hooks/useResolvedAssetReadUrl.test.tsx`、`cargo test -p api-server puzzle_work_summary_response_keeps_levels_for_shelf_cover --manifest-path server-rs\Cargo.toml`、`npm run typecheck`。
|
||||
- 关联:`src/components/custom-world-home/creationWorkShelf.ts`、`src/components/CustomWorldCoverArtwork.tsx`、`server-rs/crates/api-server/src/puzzle.rs`、`docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md`。
|
||||
|
||||
## 用户标签不要直接外显,SpacetimeDB Vec 字段不要写 default 宏
|
||||
|
||||
- 现象:给 `user_account.user_tags` 或邀请码独立标签列写 `#[default(Vec::<String>::new())]` 时,SpacetimeDB WASM 构建报 `destructor of Vec<String> cannot be evaluated at compile-time`。
|
||||
@@ -661,3 +766,27 @@
|
||||
- 处理:公开详情失效统一走 `resolveWorkNotFoundRecoveryAction(...)`,覆盖 `/works/detail`、`/gallery/puzzle/detail` 和 `/gallery/visual-novel/detail`;搜索失败和拼图详情 404 分支清理详情/运行态临时状态并回首页;`work-detail` 空数据阶段显示轻量读取态,避免异步间隙白屏。
|
||||
- 验证:`npm run test -- src/routing/runtimeNotFoundRecovery.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"`。
|
||||
- 关联:`docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md`、`src/routing/runtimeNotFoundRecovery.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`。
|
||||
|
||||
## 拼图 UI 背景只有 objectKey 时不要回退默认 UI
|
||||
|
||||
- 现象:拼图草稿页、试玩和正式运行态都显示默认 UI,或者只在结果页看到生成图,进入试玩后又回到默认背景。
|
||||
- 原因:`uiBackgroundImageSrc` 可能为空而真实生成结果只写了 `uiBackgroundImageObjectKey`;如果前端和运行态只读 `src`,或者本地试玩 / 正式 run 没把 `objectKey` 一起传递,就会丢掉已有背景。
|
||||
- 处理:统一通过一个解析入口把 `uiBackgroundImageSrc || uiBackgroundImageObjectKey` 归一到可展示路径;本地试玩和正式运行态都要保留 `uiBackgroundImageObjectKey`,并在 `uiBackgroundImageSrc` 为空时换签读取。
|
||||
- 验证:结果页 UI Tab、`startLocalPuzzleRun` 和 `PuzzleRuntimeShell` 都应在仅有 `objectKey` 时显示生成背景,不再回落默认 UI。
|
||||
- 关联:`src/services/puzzle-runtime/puzzleUiBackgroundSource.ts`、`src/components/puzzle-result/PuzzleResultView.tsx`、`src/services/puzzle-runtime/puzzleLocalRuntime.ts`、`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`、`server-rs/crates/module-puzzle/src/application.rs`。
|
||||
|
||||
## 拼图 UI 背景提示词不像 AI 生成先查首关命名契约
|
||||
|
||||
- 现象:拼图草稿生成完成后,`素材配置 > UI` 里显示的 `UI背景提示词` 像前端或后端模板拼接,而不是 AI 生成的视觉提示词。
|
||||
- 原因:首关命名 LLM 旧契约只返回 `levelName`,自动 UI 背景阶段只能用作品名、作品描述、关卡描述和标签拼接确定性兜底提示词;前端旧实现又会在 `uiBackgroundPrompt` 为空时把本地默认模板直接填进文本框,造成“看起来已有 AI 提示词”的假象。
|
||||
- 处理:首关命名 LLM 契约必须同时返回 `{"levelName":"...","uiBackgroundPrompt":"..."}`;草稿自动 UI 背景生成优先使用该 AI 提示词,视觉精修请求若返回新提示词则覆盖文本请求提示词,否则保留文本请求提示词。前端文本框只展示已保存的 `uiBackgroundPrompt` 或用户编辑值,字段为空时不展示本地兜底模板。
|
||||
- 验证:执行 `cargo test -p api-server puzzle_level_naming --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server puzzle_initial_ui_background_prompt --manifest-path server-rs\Cargo.toml`、`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`。
|
||||
- 关联:`server-rs/crates/api-server/src/prompt/puzzle/level_name.rs`、`server-rs/crates/api-server/src/puzzle.rs`、`src/components/puzzle-result/PuzzleResultView.tsx`、`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。
|
||||
|
||||
## 拼图 / 抓大鹅 UI 背景重生成报 No such procedure 先查 SpacetimeDB 版本漂移
|
||||
|
||||
- 现象:拼图或抓大鹅结果页点击 `重新生成` UI 背景时报 `No such procedure`,常见位置是泥点预扣、`save_puzzle_ui_background` 或 Match3D 草稿写回。
|
||||
- 原因:`api-server` 和 `spacetime-client` 已按新 bindings 调用 procedure,但目标 SpacetimeDB 数据库仍运行旧 wasm,尚未导出钱包扣退费、拼图 UI 背景保存或 Match3D 写回相关 procedure。
|
||||
- 处理:临时容错是把这类 `No such procedure` 当作后端版本漂移:泥点预扣阶段跳过扣费,图片已经生成但保存失败时返回本次内存快照 / 内存 profile,避免草稿页直接报错。长期修复仍是发布最新 `spacetime-module`、重新生成 bindings,并用 `spacetime describe` 或定向 smoke 确认 procedure 已导出。
|
||||
- 验证:`cargo test -p api-server asset_operation_billing_skips_spacetime_connectivity_errors --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server match3d_fallback_work_profile_keeps_generated_background_asset --manifest-path server-rs\Cargo.toml`、`npm run api-server` 后检查 `/healthz`。
|
||||
- 关联:`server-rs/crates/api-server/src/asset_billing.rs`、`server-rs/crates/api-server/src/match3d.rs`、`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
110
CONTEXT.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Genarrative Domain Context
|
||||
|
||||
Genarrative 是一个 AI 原生互动内容与小游戏平台,当前上下文记录团队在玩法、作品、运行态和平台闭环中使用的领域语言。
|
||||
|
||||
## Language
|
||||
|
||||
### Bark Battle
|
||||
|
||||
**汪汪声浪大作战**:
|
||||
浏览器 2D 声控对战玩法,玩家通过麦克风响度触发声浪并推动能量条。
|
||||
_Avoid_: 狗叫识别游戏、声纹识别玩法
|
||||
|
||||
**有效声浪触发**:
|
||||
玩家麦克风采样点的归一化响度在冷却结束后达到或超过有效阈值时产生的一次计分输入。
|
||||
_Avoid_: 有效叫声持续时长、狗叫识别结果、等待回落后的叫声
|
||||
|
||||
**有效阈值**:
|
||||
用于判定麦克风采样是否产生有效声浪触发的响度门槛。
|
||||
_Avoid_: 狗叫识别阈值、语义识别阈值
|
||||
|
||||
**声浪冷却**:
|
||||
两次有效声浪触发之间必须满足的最小时间间隔。
|
||||
_Avoid_: 叫声持续时长、回落等待时间
|
||||
|
||||
**能量条**:
|
||||
表示玩家与对手当前声浪优势的连续对抗刻度。
|
||||
_Avoid_: 血条、分数条
|
||||
|
||||
**单局结果**:
|
||||
一局 Bark Battle 结束后形成的胜负、平局和派生统计摘要。
|
||||
_Avoid_: 原始音频记录、语音内容
|
||||
|
||||
**Bark Battle 平台作品闭环**:
|
||||
Bark Battle 从创作配置、发布作品、正式运行态到单局结果记录和作品统计的完整平台流程。
|
||||
_Avoid_: 孤立 demo、只做表现深化
|
||||
|
||||
**轻创作配置作品**:
|
||||
创作者只配置展示与难度预设字段、但不能直接配置公平性敏感规则的 Bark Battle 作品。
|
||||
_Avoid_: 完整规则编辑器、固定官方关卡
|
||||
|
||||
**难度预设**:
|
||||
Bark Battle 作品中用于选择 AI 对手行为强度的发布态配置值。
|
||||
_Avoid_: 阈值配置、分数公式配置
|
||||
|
||||
**排行榜分榜**:
|
||||
排行榜按作品、难度预设和规则集版本拆分后的独立排名空间,只收录后端裁决为玩家胜利的单局结果。
|
||||
_Avoid_: 全难度混排、跨规则版本混排、失败刷分榜
|
||||
|
||||
**后端裁决结果**:
|
||||
后端根据 start run 记录和 finish 派生指标校验后生成的正式单局结果。
|
||||
_Avoid_: 前端最终分数、客户端胜负裁决
|
||||
|
||||
**派生指标**:
|
||||
前端从本地 runtime 汇总出的不可还原原始音频的单局统计值。
|
||||
_Avoid_: 原始音频、可还原语音内容
|
||||
|
||||
**作品统计投影**:
|
||||
按作品聚合的 Bark Battle 游玩开始、完成结果、反作弊标记和最佳/平均表现摘要。
|
||||
_Avoid_: 只从排行榜反推、原始音频分析、留存分析
|
||||
|
||||
**个人历史成绩**:
|
||||
玩家本人可查看的 Bark Battle 最近完成记录和个人最佳摘要。
|
||||
_Avoid_: 公开失败记录、完整无限历史、好友对比
|
||||
|
||||
**正式作品入口闭环**:
|
||||
Bark Battle 作品从创作入口、作品详情、广场/作品卡片、我的作品到正式 runtime 路由的可发现、可进入流程。
|
||||
_Avoid_: 内部试玩入口、独立活动专区
|
||||
|
||||
**轻配置编辑流程**:
|
||||
Bark Battle 创作者用单页轻配置表单和预览卡片完成草稿保存与发布的创作流程。
|
||||
_Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
|
||||
|
||||
**Phase 2 实施顺序**:
|
||||
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
|
||||
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
||||
|
||||
## Relationships
|
||||
|
||||
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。
|
||||
- 每个 **有效声浪触发** 必须达到 **有效阈值** 并满足 **声浪冷却**。
|
||||
- **有效声浪触发** 推动 **能量条**。
|
||||
- **能量条** 在倒计时结束时产生一个 **单局结果**。
|
||||
- **单局结果** 可以被后端记录为派生摘要,但不包含原始麦克风音频。
|
||||
- **Bark Battle 平台作品闭环** 包含发布态作品配置、**正式作品入口闭环**、run start / finish、个人历史成绩、**作品统计投影** 和最小排行榜。
|
||||
- Phase 2 的 Bark Battle 作品是 **轻创作配置作品**,通过 **轻配置编辑流程** 创建;配置范围限制为标题、描述、主题/背景预设、狗狗皮肤预设和排行榜开关,其中 **难度预设** 只影响 AI 对手行为。
|
||||
- **排行榜分榜** 由 `workId + difficultyPreset + rulesetVersion` 唯一确定,只收录 `serverResult = player_win` 的单局结果。
|
||||
- **单局结果** 的正式胜负、分数与排行榜成绩来自 **后端裁决结果**;前端只提交 **派生指标**,客户端结果仅用于 debug/对账。
|
||||
- **作品统计投影** 计入成功 start run 的 playStartCount、后端接受 finish 的 finishCount、胜/平/负、flagged、leaderboard 以及最佳/平均能量表现。
|
||||
- **个人历史成绩** 由最近记录列表和个人最佳摘要组成,只允许本人查看;排行榜只公开入榜胜利成绩。
|
||||
- **正式作品入口闭环** 必须覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品/个人作品架、稳定作品 ID runtime 路由和 `work_play_start` 埋点。
|
||||
- **Phase 2 实施顺序** 固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。
|
||||
|
||||
## Example dialogue
|
||||
|
||||
> **Dev:** “第二阶段排行榜要按玩家狗叫持续时间排序吗?”
|
||||
> **Domain expert:** “不按持续时间;Bark Battle 的计分输入是有效声浪触发,排行榜只能基于触发次数、峰值、能量条结果等派生摘要。”
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- “有效叫声”曾同时指代持续时长合规的声音片段和瞬时响度触发;已解析为 **有效声浪触发**,不再要求 `minBarkDurationMs` / `maxBarkDurationMs`,也不等待响度回落。
|
||||
- “第二阶段”曾可能指玩法表现深化或平台接入;已解析为 **Bark Battle 平台作品闭环**,优先补正式 play type、作品配置、发布、正式 runtime、结果持久化、历史成绩、作品统计和最小排行榜。
|
||||
- “创作者可配置作品”曾可能指完整规则编辑器;已解析为 **轻创作配置作品**,Phase 2 不允许创作者直接配置单局时长、有效阈值、声浪冷却、AI 细粒度参数、分数公式或反作弊阈值。
|
||||
- “难度预设”曾可能影响阈值、冷却或计分;已解析为只影响 AI 对手行为,排行榜按 `workId + difficultyPreset + rulesetVersion` 分榜。
|
||||
- “单局结果”曾可能由前端直接决定;已解析为必须由 **后端裁决结果** 决定,前端只提交触发次数、音量、能量、连击、时长等 **派生指标**。
|
||||
- “排行榜成绩”曾可能收录胜/平/负或按触发次数排序;已解析为只收录玩家胜利局,并以 `finalEnergy` 优先、`triggerCount` / `maxVolume` / 标准局时长接近度 / `finishedAt` 作为后续排序因子。
|
||||
- “作品统计”曾可能只从排行榜反推;已解析为独立 **作品统计投影**,失败、平局和 flagged finish 都可进入统计,但 rejected finish 不进入完成统计。
|
||||
- “个人历史成绩”曾可能指完整无限历史或公开记录;已解析为仅本人可见的最近记录列表 + 个人最佳摘要,不公开失败、平局或 flagged 历史。
|
||||
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
|
||||
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
|
||||
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
|
||||
2
deploy/env/api-server.env.example
vendored
@@ -39,7 +39,7 @@ APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
# 移动端创作页作品列表统一卡片设计 2026-04-29
|
||||
# 移动端草稿页作品列表统一卡片设计 2026-04-29
|
||||
|
||||
## 背景
|
||||
|
||||
创作页的作品模块需要同时承载 RPG、拼图和大鱼吃小鱼等玩法。不同玩法卡片不能各自展示阶段、素材、主题等细节标签,否则作品列表会在移动端显得拥挤,并且草稿作品会暴露过多编辑态信息。
|
||||
草稿页的作品模块需要同时承载 RPG、拼图和大鱼吃小鱼等玩法。不同玩法卡片不能各自展示阶段、素材、主题等细节标签,否则作品列表会在移动端显得拥挤,并且草稿作品会暴露过多编辑态信息。
|
||||
|
||||
本次将作品列表卡片收口成统一信息结构:草稿只用于快速识别和继续创作,已发布作品才展示公开数据与分享入口。
|
||||
本次将作品列表卡片收口成统一信息结构:草稿只用于快速识别和继续创作,已发布作品才展示公开数据;删除与分享等低频操作收进左滑操作层,避免列表常态被按钮挤占。
|
||||
|
||||
## 落地范围
|
||||
|
||||
- 列表容器:`src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- 作品卡片:`src/components/custom-world-home/CustomWorldWorkCard.tsx`
|
||||
- 不改动作品数据聚合、筛选、打开和体验逻辑。
|
||||
- 已发布作品右上角动作从删除改为分享;草稿仍保留删除入口。
|
||||
- 已发布作品保留分享能力;可删除作品保留删除能力,但常态不显示为右侧按钮。
|
||||
|
||||
## 卡片结构规则
|
||||
|
||||
1. 标题上方只显示两个标签:作品状态与游戏类型。
|
||||
1. 卡片整体对齐发现 / 分类页的横向作品列表结构:左侧为标题、状态、类型、摘要与必要数据,右侧为带透明度的封面图。
|
||||
2. 不再显示阶段、主题、素材完成度、作者、作品号等额外标签。
|
||||
3. 标签下方依次显示作品名称与作品描述。
|
||||
4. 草稿卡片到作品描述为止,不显示其他统计、作品号或体验按钮。
|
||||
3. 标题区域保留作品状态与游戏类型;生成中的草稿状态显示为“生成中”。
|
||||
4. 草稿卡片到作品描述为止,不显示右侧“继续创作”等固定动作、统计、作品号或公开指标。
|
||||
5. 已发布卡片在描述下方显示三项公开指标:游玩数、改造数、点赞数。
|
||||
6. 已发布卡片右上角显示分享 icon,点击后复制作品分享文案,不触发卡片打开。
|
||||
7. 草稿卡片右上角继续显示删除 icon,点击删除不触发卡片打开。
|
||||
6. 已发布卡片的分享入口收进左滑操作层,点击后复制作品分享文案,不触发卡片打开。
|
||||
7. 可删除卡片的删除入口收进左滑操作层,常态不显示删除按钮;左滑露出后点击删除不触发卡片打开。
|
||||
8. 卡片不显示最后修改时间;`updatedAt` 只用于作品列表排序。
|
||||
9. 生成中的卡片在整卡上叠加半透明蒙版、旋转等待符号和“生成中...”标识;蒙版不能移除标题、状态、类型、摘要、右侧封面等原有信息。
|
||||
|
||||
## 公开指标重点展示补充
|
||||
|
||||
@@ -31,16 +32,24 @@
|
||||
3. 用户每次进入创作页时,前端读取上一次进入该页面缓存的公开指标快照;当已发布作品卡片滑动进入视口后,数字从缓存值增长到本次接口返回的最新值。
|
||||
4. 若最新值高于缓存值,动画完成后在对应指标右下角展示红色向上箭头和本次上涨的具体数值,字号低于主数字,避免抢占主信息层级。
|
||||
5. 若没有缓存值、缓存值不低于最新值或作品仍是草稿,则直接显示最新值,不展示上涨标记。
|
||||
6. 每张作品卡片继续使用作品封面作为整卡背景,封面保持主视觉可见;遮罩只用于保证标题、描述和指标可读,不能把封面洗成普通面板底色。
|
||||
6. 每张作品卡片继续使用作品封面作为右侧方形半透明主视觉;封面不能因为列表收缩被拉伸变形。
|
||||
7. 作品列表按 `updatedAt` 倒序排列;前端排序需要兼容 ISO 时间和 Rust 后端常用的 `seconds.microsZ` 时间文本。
|
||||
8. 若玩法摘要缺少 `coverImageSrc`,允许从同一作品的正式关卡图、背景图或素材图里取第一张可用图片作为卡片背景兜底。
|
||||
9. 若作品真实图片为空、私有资源换签失败或浏览器图片加载失败,卡片必须切到对应玩法的 `public/creation-type-references/` 参考图;最终兜底底色使用平台浅粉暖白色系,不允许退回黑色普通面板。
|
||||
|
||||
## 移动端布局规则
|
||||
|
||||
1. 作品列表默认仍使用 2 列网格,保证草稿可以快速扫视。
|
||||
2. 已发布作品卡片在移动端固定 `col-span-2`,即占据一整行,避免公开指标和分享入口互相挤压。
|
||||
3. `sm` 及以上视口恢复普通网格跨度,由卡片自然进入多列布局。
|
||||
1. 作品列表默认使用单列纵向列表,视觉上与发现页分类列表保持一致。
|
||||
2. 移动端每张卡片使用固定右列方形半透明封面,右列建议 `5.1rem` 左右;草稿卡即使复用分类页基础类名,也必须用自身选择器覆盖分类页的 `4.3rem + 正文 + action` 三列规则,避免正文被压进窄列。
|
||||
3. 已发布作品的公开指标在卡片正文内保留,但需要压缩字号和间距,不能让右侧封面列错位。
|
||||
4. 小屏卡片降低高度、内边距、标题字号和徽标尺寸,避免长标题或中文描述撑破容器。
|
||||
5. 右侧封面容器本身必须带玩法参考图 CSS 背景兜底;`img` 的真实封面或 `ResolvedAssetImage` fallback 加载失败时,也不能出现空白或黑卡。
|
||||
|
||||
## 网页端布局规则
|
||||
|
||||
1. 网页端作品架不能继续拉成整行超宽列表;从 `768px` 起使用多列卡片式网格,默认两列,宽屏提升到三列。
|
||||
2. 网页端卡片保留移动端同一信息结构,但卡片高度增加,正文区可显示更多摘要与公开指标,右侧封面改为更高的半透明视觉区。
|
||||
3. 删除与分享仍然只在左滑或键盘揭示态显示;默认态不得透出红色删除底层或分享底层。
|
||||
|
||||
## 文案约束
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 平台创作 Tab 模板入口设计
|
||||
|
||||
更新时间:`2026-05-07`
|
||||
更新时间:`2026-05-14`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
```text
|
||||
标题:10分钟创作一个精品互动玩法
|
||||
模板 Tab:拼图 / 抓大鹅 / 视觉小说(敬请期待)/ AIRP
|
||||
模板 Tab:拼图 / 抓大鹅 / AIRP
|
||||
默认内容:拼图创作表单
|
||||
```
|
||||
|
||||
@@ -34,16 +34,17 @@
|
||||
1. 打开“创作”一级 Tab 时默认停留在拼图 Tab,不主动创建拼图 session。
|
||||
2. 点击拼图表单“生成草稿”后,才创建拼图 session 并执行 `compile_puzzle_draft`。
|
||||
3. 拼图表单内的模板按钮使用 `tablist / tab` 语义,点击后只填充画面描述。
|
||||
4. 点击非拼图且已开放的模板 Tab 时,进入该玩法既有工作台;视觉小说与 AIRP 当前保持敬请期待禁用态。
|
||||
5. `creative-agent` 不出现在模板 Tab 和选择弹层中,不再作为创作 Tab 首屏入口。
|
||||
6. 方洞挑战暂时从创作页完全隐藏,不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中;既有作品链路继续保留。
|
||||
4. 点击非拼图且已开放的模板 Tab 时,进入该玩法既有工作台;AIRP 当前保持敬请期待禁用态。
|
||||
5. `visual-novel` 暂时从创作页完全隐藏,不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中;既有作品、详情、运行链路继续保留。
|
||||
6. `creative-agent` 不出现在模板 Tab 和选择弹层中,不再作为创作 Tab 首屏入口。
|
||||
7. 方洞挑战暂时从创作页完全隐藏,不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中;既有作品链路继续保留。
|
||||
|
||||
## 4. 验收
|
||||
|
||||
1. 点击“创作”后首屏出现“10分钟创作一个精品互动玩法”。
|
||||
2. 顶部选择模板入口为 Tab,拼图 Tab 默认 `aria-selected=true`。
|
||||
3. 创作 Tab 默认显示拼图创作表单内容,且不显示旧“Hi, 朋友”、输入框或智能创作快捷按钮。
|
||||
4. 隐藏的智能创作类型与方洞挑战不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中。
|
||||
4. 隐藏的智能创作类型、视觉小说与方洞挑战不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中。
|
||||
5. 草稿页返回创作页后仍回到同一模板入口,并可保留拼图表单草稿内容。
|
||||
|
||||
## 5. 嵌入式表单 UI 细节
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
- 入口、删除、分享、领取拼图激励等行为全部复用现有 `CustomWorldCreationHub` 的作品卡逻辑。
|
||||
- 一级底部 Tab 文案为“草稿”,内部仍可按草稿与已发布筛选。
|
||||
- 作品列表必须按最后修改时间倒序排列;排序使用后端 `updatedAt`,前端需要兼容 ISO 字符串和 `seconds.microsZ` 两种时间文本。
|
||||
- 每张作品卡以作品封面图铺满整卡背景,并叠加遮罩保证标题和描述可读;遮罩不能把封面洗成普通面板底色。缺 `coverImageSrc` 但同一作品已有正式关卡图、背景图或素材图时,优先用这些真实作品图兜底,再使用现有兜底底图。
|
||||
- 每张作品卡以作品封面图铺满整卡背景,并叠加遮罩保证标题和描述可读;遮罩不能把封面洗成普通面板底色。缺 `coverImageSrc` 但同一作品已有正式关卡图、背景图或素材图时,优先用这些真实作品图兜底;若封面签名失败或作品还没有可用图片,再按玩法使用 `public/creation-type-references/` 的参考图兜底,最终无图底色也必须保持百梦浅粉暖色调,不能回退为黑色卡片。
|
||||
- 草稿页卡片不展示“最后修改时间”“更新于”或原始 `updatedAt` 文本,时间只参与排序。
|
||||
|
||||
## 6. 我的页玩过列表
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
- 平台层正文、按钮、说明、功能标签统一使用非像素字体
|
||||
- 左上角 `陶泥儿 / GENARRATIVE` 品牌字标允许单独做成像素化 logo
|
||||
- `GENARRATIVE` 与 `陶泥儿` 都优先直接使用游戏内同款 `Fusion Pixel`
|
||||
- 品牌中文主标固定显示为 `陶泥` + 半字号 `儿`,`儿` 必须继承主标同一字体和字号后再视觉缩放到 50%,并与 `陶泥` 底部对齐,随移动端和场景覆盖等比例缩放
|
||||
- 品牌字标默认保持正常像素字观感,禁止再叠双层粗阴影或手动加粗到影响识别
|
||||
- 品牌字标直接使用字体文件内原字形,不额外做运行时描字、轮廓拼字或伪粗体处理
|
||||
- 主标题保留明显层级,但不再做像素描边效果
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 文档列表
|
||||
|
||||
- [CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md):4-8 岁儿童动作识别互动玩法 Demo 固定热身关的横屏体验流程、识别目标、表现需求与待确认事项。
|
||||
- [TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md](./TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md):候选产品名“陶泥儿”的品牌定位归纳、gpt-image-2 Logo 概念图和主标选择建议。
|
||||
- [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里陶泥儿主输入与 AI 分工边界设计。
|
||||
- [CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md):自定义世界创作里“手填锚点 / AI 可改初稿 / 系统托管层”的平衡设计。
|
||||
- [CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md):纯 Agent 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。
|
||||
|
||||
423
docs/design/TAONIER_BRAND_LOGO_CONCEPTS_2026-05-13.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# 陶泥儿品牌 Logo 概念稿
|
||||
|
||||
> 本稿是围绕候选产品名“陶泥儿”的品牌视觉探索,不替代当前已冻结的“百梦”正式命名口径。若后续确认更名,需要另起产品命名、前后端文案和商标检索落地方案。
|
||||
|
||||
## 1. 品牌定位归纳
|
||||
|
||||
“陶泥儿”适合承接的不是传统陶艺或儿童黏土,而是“把灵感塑形成可玩内容”的 AI 创作平台隐喻。
|
||||
|
||||
核心关键词:
|
||||
|
||||
- 精品:作品不是随手糊出来,而是经过 AI 辅助打磨、可被消费和传播的轻精品内容。
|
||||
- UGC:用户是主要造物者,平台降低创作门槛。
|
||||
- 创作:从一句脑洞、一个梗、一张图,生成小游戏、互动作品或可分享内容。
|
||||
- 裂变与梗:名字要支持“开捏”“捏个梗”“捏个小游戏”这类用户语言。
|
||||
- 轻度休闲:体验应松弛、即时、好玩,不走硬核生产工具气质。
|
||||
- AI:AI 是塑形能力,不是冷冰冰的技术标签。
|
||||
|
||||
推荐品牌主张:
|
||||
|
||||
```text
|
||||
把脑洞捏成小游戏
|
||||
```
|
||||
|
||||
备选表达:
|
||||
|
||||
```text
|
||||
捏个脑洞,马上开玩
|
||||
AI 开捏,人人会创作
|
||||
随手造梗,随心开玩
|
||||
```
|
||||
|
||||
## 2. 生成原则
|
||||
|
||||
本轮使用仓库 GPT-image-2 / VectorEngine 工作流生成 Logo 概念图,生成时刻意要求“无文字 Logo 图标”。
|
||||
|
||||
原因:
|
||||
|
||||
- AI 生图直接生成中文品牌字容易出现笔画错误,不适合作为正式字标。
|
||||
- 当前阶段更适合先确定图形符号方向,再由设计师或前端继续做矢量化、字标搭配和多尺寸适配。
|
||||
- 图标需要优先服务 App icon、平台左上角品牌、分享卡片和加载页,而不是一次性海报图。
|
||||
|
||||
生成文件:
|
||||
|
||||
```text
|
||||
public/branding/taonier-logo-v3-concepts/
|
||||
├─ taonier-logo-v3-contact-sheet.png
|
||||
├─ taonier-v3-finger-spark.png
|
||||
├─ taonier-v3-seed-pop.png
|
||||
├─ taonier-v3-magic-dot.png
|
||||
├─ taonier-v3-work-gem.png
|
||||
└─ taonier-v3-soft-t.png
|
||||
|
||||
public/branding/taonier-logo-magic-dot-concepts/
|
||||
├─ taonier-logo-magic-dot-contact-sheet.png
|
||||
├─ taonier-magic-dot-orbit.png
|
||||
├─ taonier-magic-dot-seal.png
|
||||
├─ taonier-magic-dot-squish.png
|
||||
├─ taonier-magic-dot-mold.png
|
||||
└─ taonier-magic-dot-bloom.png
|
||||
|
||||
public/branding/taonier-logo-flat-concepts/
|
||||
├─ taonier-logo-flat-contact-sheet.png
|
||||
├─ taonier-flat-play-clay.png
|
||||
├─ taonier-flat-spark-clay.png
|
||||
├─ taonier-flat-meme-smile.png
|
||||
├─ taonier-flat-loop-mold.png
|
||||
└─ taonier-flat-seal-blocks.png
|
||||
|
||||
public/branding/taonier-logo-concepts/
|
||||
├─ taonier-logo-contact-sheet.png
|
||||
├─ taonier-clay-spark.png
|
||||
├─ taonier-play-mold.png
|
||||
├─ taonier-meme-bubble.png
|
||||
├─ taonier-creation-loop.png
|
||||
└─ taonier-premium-seal.png
|
||||
```
|
||||
|
||||
生成脚本:
|
||||
|
||||
```text
|
||||
scripts/generate-taonier-logo-concepts.mjs
|
||||
```
|
||||
|
||||
## 3. V3-03 一捏成型延展
|
||||
|
||||
这一组专门沿 V3 “一捏成型”继续打磨。目标是保留“两个软形触点 + 中央作品核”的成型瞬间,同时降低括号感、碰撞特效感和功能按钮感。
|
||||
|
||||

|
||||
|
||||
### 3.1 捏合星核
|
||||
|
||||

|
||||
|
||||
定位:一捏成型方向的主标首选。
|
||||
|
||||
这个方向最稳地保留了“左右合拢、中央成型”的核心动作,中心青绿色星核形成了明确焦点,整体比原 V3-03 更完整,也没有明显播放器、聊天或表情联想。
|
||||
|
||||
优点:
|
||||
|
||||
- 结构清楚,第一眼能看出“合拢生成”。
|
||||
- 元素少,小尺寸适配潜力好。
|
||||
- 中央星核可以做加载、生成成功、发布完成等动效延展。
|
||||
|
||||
风险:
|
||||
|
||||
- 左右软形仍有一点括号感,后续矢量化可把外轮廓做得更不对称、更像被捏塑的软泥。
|
||||
|
||||
建议用途:主 Logo 备选首选、AI 生成按钮、启动动效核心符号。
|
||||
|
||||
### 3.2 成型印记
|
||||
|
||||

|
||||
|
||||
定位:完整主标感最强的延展方向。
|
||||
|
||||
这个方向把左右触点收成一个更完整的软形图腾,减少了“两个括号”的割裂感。视觉上更像独立品牌符号,但也因此少了一点“捏合动作”的即时感。
|
||||
|
||||
建议用途:主 Logo 强备选;若选择它,后续应去掉背景底色并强化中心负形星点。
|
||||
|
||||
### 3.3 软泥合拍
|
||||
|
||||

|
||||
|
||||
定位:轻松、年轻、动效友好。
|
||||
|
||||
这个方向的上下软形更活泼,适合表达“啪嗒一下成型”。但静态 Logo 中的黄色星点和短线略像特效贴纸,主标使用前需要继续简化。
|
||||
|
||||
建议用途:生成中动效、运营图、互动反馈,不建议直接定为主 Logo。
|
||||
|
||||
### 3.4 灵感模口
|
||||
|
||||

|
||||
|
||||
定位:最有“模口 / 造物容器”意味。
|
||||
|
||||
这个方向图形独特,和“从软泥模口里生成作品”的隐喻贴合。但外形复杂度比 01、02 更高,边缘细节在小尺寸下可能损失。
|
||||
|
||||
建议用途:主 Logo 备选探索,适合继续做专业矢量简化。
|
||||
|
||||
### 3.5 捏开灵感
|
||||
|
||||

|
||||
|
||||
定位:温和、包裹、生成容器。
|
||||
|
||||
这个方向亲和、平衡,但整体像眼睛 / 容器 / 开合结构,陶泥儿的“捏”动作弱一些。
|
||||
|
||||
建议用途:AI 生成入口、等待态、创作容器辅助图形。
|
||||
|
||||
## 4. V3 抽象主标候选
|
||||
|
||||
V3 根据评审反馈重新避开了五个问题:播放三角、褐色陶土主色、聊天气泡 / 表情包、循环符号,以及过多碎元素。方向转为更抽象、更亮眼、更像长期主 Logo 的符号。
|
||||
|
||||

|
||||
|
||||
### 4.1 灵感捏痕
|
||||
|
||||

|
||||
|
||||
定位:主 Logo 首选。
|
||||
|
||||
这个方向用醒目的珊瑚红软形、指纹捏痕和星点负形建立记忆点。它不再依赖“陶泥的褐色”,而是用“被捏过的痕迹”表达陶泥儿的核心动作:用户把脑洞捏成作品。
|
||||
|
||||
优点:
|
||||
|
||||
- 第一眼足够醒目,远离旧版褐色和播放器感。
|
||||
- 指纹捏痕有独特性,能承接“人人创作”和“亲手塑形”。
|
||||
- 元素少,适合继续矢量化和小尺寸适配。
|
||||
|
||||
风险:
|
||||
|
||||
- 指纹弧线后续需要进一步简化,避免在 24px 以下变糊。
|
||||
- 星点比例要克制,避免变成普通灵感图标。
|
||||
|
||||
建议用途:主 Logo、App icon、平台顶栏、启动页、生成按钮。
|
||||
|
||||
### 4.2 脑洞种子
|
||||
|
||||

|
||||
|
||||
定位:创意生长与新手友好。
|
||||
|
||||
这个方向从“灵感发芽”切入,比陶泥更偏创造生命力。它亲和、可爱,但容易让用户联想到教育、植物、儿童启蒙或种植类产品。
|
||||
|
||||
建议用途:新手引导、创作孵化、儿童 / 寓教于乐支线,不建议作为主 Logo。
|
||||
|
||||
### 4.3 一捏成型
|
||||
|
||||

|
||||
|
||||
定位:AI 把灵感合成为作品的瞬间。
|
||||
|
||||
这个方向很简洁,用左右两个软形触点和中心星点表达“捏合”。它避开了播放器和聊天气泡,也能做动效,但静态图形目前稍像碰撞特效或括号,需要继续重绘增强独特轮廓。
|
||||
|
||||
建议用途:生成按钮、AI 施法动效、主 Logo 备选微调方向。
|
||||
|
||||
### 4.4 作品胶囊
|
||||
|
||||

|
||||
|
||||
定位:精品内容和作品沉淀。
|
||||
|
||||
这个方向更稳、更精品,青绿色也比褐色更吸睛。但整体像水滴、宝石或通用内容图标,和“捏”这个动作的关系弱。
|
||||
|
||||
建议用途:精选作品、作品库、创作者中心,不建议优先做主 Logo。
|
||||
|
||||
### 4.5 软体 T 形
|
||||
|
||||

|
||||
|
||||
定位:英文辅助名 / Taonier 的抽象首字母。
|
||||
|
||||
这个方向试图做更品牌化的抽象符号,但当前形体还不够自然,也未形成足够强的“陶泥儿”心智。若未来英文名确定为 `Taonier` 或类似形式,可以继续沿这个方向做专业字母标重绘。
|
||||
|
||||
建议用途:英文标识探索,不作为当前主 Logo 首选。
|
||||
|
||||
## 5. V2 扁平矢量候选
|
||||
|
||||
第一批图形偏 3D 和拟物,更适合作为吉祥物、运营图或启动页气氛图,不适合作为长期主 Logo。V2 已把约束收紧为扁平、矢量、少元素、强轮廓和小尺寸可识别。
|
||||
|
||||

|
||||
|
||||
### 5.1 扁平开捏
|
||||
|
||||

|
||||
|
||||
定位:最直接的主 Logo 候选。
|
||||
|
||||
这个方向用一团柔软陶泥承载播放符号,用户一眼能理解“点开玩 / 马上玩”,同时外形保留“捏出来”的不规则软泥感。
|
||||
|
||||
优点:
|
||||
|
||||
- 识别速度最快,移动端小尺寸也成立。
|
||||
- 符合主流 App Logo 语言,亲和、不重、不技术冷。
|
||||
- 和“把脑洞捏成小游戏”的主张绑定最强。
|
||||
|
||||
风险:
|
||||
|
||||
- 播放符号是常见母题,后续矢量化时要通过不规则软泥外轮廓、颜色和字标形成独特资产。
|
||||
|
||||
建议用途:主 Logo 首选、App icon、平台顶栏、分享卡片角标。
|
||||
|
||||
### 5.2 灵感泥星
|
||||
|
||||

|
||||
|
||||
定位:AI 创作与灵感生成。
|
||||
|
||||
这个方向比“扁平开捏”更品牌化,中心负形星点表达灵感、AI 生成和创意爆发。它没有播放符号那么直白,但更容易和“陶泥儿”的创作平台气质绑定。
|
||||
|
||||
优点:
|
||||
|
||||
- 图形更简洁,品牌记忆点强。
|
||||
- 陶泥心智、AI 灵感和精品感比较平衡。
|
||||
- 适合未来扩成字标、启动页和生成态动效。
|
||||
|
||||
风险:
|
||||
|
||||
- 对“小游戏/马上玩”的表达弱于播放符号。
|
||||
|
||||
建议用途:主 Logo 强备选、创作首页、AI 生成按钮和品牌主视觉。
|
||||
|
||||
### 5.3 造梗笑泥
|
||||
|
||||

|
||||
|
||||
定位:社交传播和玩梗亲和力。
|
||||
|
||||
这个方向的气泡与笑脸非常亲和,适合表达“分享快乐”和“造梗”。但它和聊天、社区类产品的通用图形过近,作为主 Logo 可能会让用户误判产品品类。
|
||||
|
||||
建议用途:社区、评论、分享、活动贴纸,不建议做主 Logo。
|
||||
|
||||
### 5.4 共创泥环
|
||||
|
||||

|
||||
|
||||
定位:AI 与用户共创闭环。
|
||||
|
||||
这个方向表达共创与循环,但生成结果带有偏柔和彩虹渐变的视觉倾向,与“陶泥儿”的软泥名称关联不够直观,也不如 01/02 容易记住。
|
||||
|
||||
建议用途:创作流程、共创能力、生成进度辅助图形。
|
||||
|
||||
### 5.5 精品泥印
|
||||
|
||||

|
||||
|
||||
定位:精品作品和内容集合。
|
||||
|
||||
这个方向像内容平台或作品库入口,能表达图片、用户、游戏等多形态内容。但图形元素较多,主标识别不如 01/02 凝练。
|
||||
|
||||
建议用途:精选作品、作品集、创作者中心、内容品质标识。
|
||||
|
||||
## 6. V1 立体探索
|
||||
|
||||
### 6.1 灵感陶团
|
||||
|
||||

|
||||
|
||||
定位:AI 共创与灵感造物。
|
||||
|
||||
这个方向把“陶泥”作为主视觉,内部用发光火花和节点表达 AI 赋能。它最贴“陶泥儿”名字本身,也能说明平台不是普通小游戏集合,而是从灵感生成作品的创作容器。
|
||||
|
||||
优点:
|
||||
|
||||
- 与“陶泥儿”的名称绑定最强。
|
||||
- 有 AI、创作、造物的综合含义。
|
||||
- 适合启动页、品牌介绍、创作首页空状态。
|
||||
|
||||
风险:
|
||||
|
||||
- 小尺寸下细节偏多,需要后续矢量化时压缩节点和纹理。
|
||||
- 如果色彩处理不当,会回到手工陶艺联想。
|
||||
|
||||
建议用途:品牌主视觉备选、官网/启动页、创作入口图形。
|
||||
|
||||
### 6.2 开玩模具
|
||||
|
||||

|
||||
|
||||
定位:把脑洞捏成小游戏。
|
||||
|
||||
这个方向用软陶捏出播放符号,最直接地连接“创作”和“马上玩”。它比单纯陶泥团更有产品动作,也更适合轻休闲、小游戏、短内容传播。
|
||||
|
||||
优点:
|
||||
|
||||
- 识别强,小尺寸也清楚。
|
||||
- 与轻度休闲小游戏的关系最直接。
|
||||
- 适合作为 App icon 和主 Logo 图形。
|
||||
|
||||
风险:
|
||||
|
||||
- 播放符号相对常见,需要后续在外轮廓、捏痕和色彩上做独特性。
|
||||
- 如果三角形过硬,会削弱“陶泥儿”的柔软感。
|
||||
|
||||
建议用途:主 Logo 首选、App icon、分享卡片角标、加载态图形。
|
||||
|
||||
### 6.3 造梗气泡
|
||||
|
||||

|
||||
|
||||
定位:社交传播、玩梗、裂变。
|
||||
|
||||
这个方向把陶泥变形成聊天气泡和表情,强调“梗”和“传播”。它最有社交平台感,也适合表情包、活动贴纸和运营视觉。
|
||||
|
||||
优点:
|
||||
|
||||
- 传播感强,年轻、轻松、容易做 IP 化。
|
||||
- 能承接社区、评论、分享和玩梗场景。
|
||||
- 比较容易延展成贴纸和表情包。
|
||||
|
||||
风险:
|
||||
|
||||
- 偏软萌,可能削弱“精品 AI 创作平台”的质感。
|
||||
- 作为主 Logo 容易显得像聊天或表情产品。
|
||||
|
||||
建议用途:社区模块、活动运营、IP 辅助形象,不建议作为唯一主 Logo。
|
||||
|
||||
### 6.4 共创回路
|
||||
|
||||

|
||||
|
||||
定位:AI 与用户共同迭代生成。
|
||||
|
||||
这个方向用软陶带形成循环和造物轨迹,表达“灵感 -> AI 塑形 -> 用户修改 -> 作品传播”的闭环。它比其他方向更抽象,也更有平台级和工具级气质。
|
||||
|
||||
优点:
|
||||
|
||||
- 高级、简洁,避免儿童化。
|
||||
- 适合表达 AI 共创、迭代和作品循环。
|
||||
- 可用于创作者工作台或生成进度标识。
|
||||
|
||||
风险:
|
||||
|
||||
- 与“陶泥儿”名称的直观关联较弱。
|
||||
- 缺少小游戏和玩梗的即时识别。
|
||||
|
||||
建议用途:创作流程标识、AI 共创能力图标、品牌辅助图形。
|
||||
|
||||
### 6.5 精品泥印
|
||||
|
||||

|
||||
|
||||
定位:精品内容、作品认证、创作者成果。
|
||||
|
||||
这个方向像一个被压印的软陶徽章,中间有方块和火花,比较适合表达“作品被打磨成型”。它的内容平台感强于游戏入口感。
|
||||
|
||||
优点:
|
||||
|
||||
- 精品感和作品库气质较强。
|
||||
- 适合作品认证、精选、创作者徽章。
|
||||
- 与“陶泥压印”隐喻相对自然。
|
||||
|
||||
风险:
|
||||
|
||||
- 细节较多,主 Logo 小尺寸可读性不如“开玩模具”。
|
||||
- 徽章感偏静态,轻休闲的即时性稍弱。
|
||||
|
||||
建议用途:精选作品标识、创作者荣誉、内容品质标签。
|
||||
|
||||
## 7. 推荐结论
|
||||
|
||||
优先级建议:
|
||||
|
||||
```text
|
||||
主 Logo 首选:V3 01 灵感捏痕
|
||||
一捏成型首选:V3-03 延展 01 捏合星核
|
||||
完整主标备选:V3-03 延展 02 成型印记
|
||||
英文标识探索:V3 05 软体 T 形
|
||||
精品内容辅助:V3 04 作品胶囊
|
||||
新手 / 寓教于乐辅助:V3 02 脑洞种子
|
||||
```
|
||||
|
||||
若要兼顾主流、亲和、醒目和“陶泥儿”的动作隐喻,优先继续打磨 V3 “灵感捏痕”。
|
||||
若想把 Logo 做得更抽象、更像 AI 生成瞬间,可以继续打磨 V3-03 延展中的“捏合星核”或“成型印记”。
|
||||
V1 的 3D 图标不建议直接作为主 Logo,只适合做运营图、吉祥物探索或风格参考;V2 的播放、气泡、碎元素方向本轮已降级为历史探索。
|
||||
|
||||
## 8. 后续落地建议
|
||||
|
||||
1. 先围绕 V3 “灵感捏痕”做 3 到 5 个专业矢量微调版:减少指纹线条、强化软形轮廓、测试深色 / 浅色底。
|
||||
2. 同步对 V3-03 “捏合星核”做一版更独特的轮廓重绘,弱化括号感,保留中央成型星核。
|
||||
3. 字标不要直接使用生图结果,应单独设计“陶泥儿”中文字标,并准备英文辅助名。
|
||||
4. 正式应用前做商标近似检索,重点覆盖第 9、35、38、41、42 类。
|
||||
5. 若确认替换“百梦”,再更新现有命名规范文档、前端品牌组件、HTML metadata、后台和后端默认文案。
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,5 @@
|
||||
生成一张适合商业路演 PPT 使用的横版高清主视觉配图,主题是“陶泥:将 Harness Engineering 理论、专家知识、多 Agent 调度融入 AI 创作工具与 AI 原生游戏框架”。
|
||||
画面构图:深色但清爽的现代科技工作室/游戏创作中枢,中心是一个温润陶泥质感的抽象核心装置,核心装置连接两条清晰的知识管线。左侧表现 AI 创作工具:多 Agent 协作节点、策划 SOP、美术 SOP、模板框架、剧本/数值/系统/角色/场景/CG 等垂类任务以图标化模块呈现。右侧表现 AI 原生游戏框架:实时剧情生成、数值与剧情对齐、画面与剧情对齐、实时任务与物品生成,以游戏世界投影、角色剪影、场景画面和规则约束网格呈现。
|
||||
底部远景加入克制的学术评审氛围:讲台、投影、评审席、优秀课程设计的荣誉感,但不要出现真实学校校徽、真实教授肖像或任何可识别真实人物。
|
||||
视觉风格:高端产品发布会 Key Visual,精致 3D 插画结合轻量界面光效,专业、可信、面向 AI 游戏与创作者工具,色彩使用陶土暖色、青绿色、冷白光和少量金色点缀,信息结构清楚,主体居中偏左,右上和左上保留干净留白用于 PPT 叠加标题。
|
||||
要求:无文字、无字母、无水印、无 UI 按钮、无品牌 logo、无真实校徽、无真实人物肖像、避免杂乱、避免过暗、避免卡通幼稚、避免纯抽象背景。
|
||||
@@ -142,6 +142,15 @@
|
||||
4. 说明
|
||||
5. 标签
|
||||
|
||||
## 6.3 草稿页作品卡对齐分类列表
|
||||
- 草稿 Tab 的作品架要优先对齐发现页分类列表的横向卡片:左侧承载标题/状态/类型/摘要,右侧显示带透明度的方形封面图。
|
||||
- 草稿卡不能为了视觉对齐丢掉原有信息:删除、分享、已发布统计、拼图积分激励、未读红点都要保留;其中删除和分享属于低频动作,常态不显示按钮,向左划动列表项后露出操作。
|
||||
- 卡片右侧不再常驻“继续创作”“查看详情”“查看进度”等动作按钮,打开作品由整张卡片承担。
|
||||
- 移动端保持单列列表;网页端使用多列卡片式网格,避免在宽屏上把作品卡拉成一整行长条。
|
||||
- 生成中的状态使用整卡蒙版、旋转等待符号和“生成中...”标识;蒙版只能作为状态层,不能替换或移除卡片本身的信息。
|
||||
- 草稿卡复用分类页基础类名时,要用 `.creation-work-card.platform-category-game-item` 覆盖分类页移动端三列规则;否则正文会被当作封面列压缩,中文标题会断成一两个字一行。
|
||||
- 右侧封面不要只依赖 `img` fallback,容器层也要有玩法参考图 CSS 背景兜底,私有资源换签失败或图片 onerror 时仍能看到封面视觉。
|
||||
|
||||
## 7. 样式与动画经验
|
||||
|
||||
### 7.1 轮播动画要连续,不要离散
|
||||
|
||||
@@ -198,13 +198,13 @@ Match3D 首版参考拼图后期的入口表单收集方式,而不是早期的
|
||||
|
||||
## 6.3 参考图片
|
||||
|
||||
抓大鹅入口页不展示参考图片上传。题材表现由题材文本和草稿切割图片链路承接;草稿生成阶段会生成多视角 2D 物品素材并写入作品 profile。结果页 `素材配置 > 物品` 继续承接物品素材预览、删除、批量新增和音效配置。
|
||||
抓大鹅入口页不展示参考图片上传。题材表现由题材文本和草稿切割图片链路承接;草稿生成阶段会生成多视角 2D 物品素材并写入作品 profile。结果页 `素材配置 > 物品` 继续承接物品素材预览、删除和批量新增;点击音效配置当前临时隐藏。
|
||||
|
||||
## 6.4 生成音效入口
|
||||
|
||||
抓大鹅入口页不展示 `生成音效` Toggle。草稿生成阶段只保存 `generatedItemAssets[].soundPrompt`,不调用 Vidu 生成点击音效,也不产生点击音效相关扣费。
|
||||
|
||||
结果页仍保留单个物品音效的手动补生成入口。用户在 `素材配置 > 物品` 详情面板中生成点击音效时,后端复用通用创作音频接口和资产落点,把结果写入对应 `generatedItemAssets[].clickSound`。
|
||||
结果页当前不保留单个物品音效的手动补生成入口。历史 `generatedItemAssets[].clickSound` 字段继续兼容传递给运行态;后续恢复音效能力时再复用通用创作音频接口和资产落点。
|
||||
|
||||
---
|
||||
|
||||
@@ -239,7 +239,7 @@ Match3D 首版参考拼图后期的入口表单收集方式,而不是早期的
|
||||
|
||||
生成出的独立图片必须作为结果页 `素材配置 > 物品` 的预览资产返回。图片素材生成成功时 `generatedItemAssets[].status = image_ready`,并携带 `imageViews[]`,兼容字段 `imageSrc` / `imageObjectKey` 指向首张视角图;正式平台资产绑定和更完整的二次编辑流程以后续技术方案为准。
|
||||
|
||||
局内 UI 生成分为两类图片:`9:16` 纯背景图和 `1:1` 中心容器 UI 图。纯背景图只表现题材氛围和环境,不得生成锅、圆盘、托盘、拼图槽、物品槽、HUD、文字、按钮、倒计时、分数或物品;中心容器 UI 图单独生成,贴合题材设定,用于覆盖运行态默认圆形竞技容器。容器图必须参考 `public/match3d-background-references/pot-fused-reference.png` 的大尺寸轻俯视构图:容器外轮廓接近画布四边,宽度约占画布 `86%-92%`、高度约占 `82%-90%`,内口为横向椭圆,不能生成小圆盘、正俯视扁盘、侧视碗或小托盘。底部备选栏、顶部控件和拼图/物品槽位继续使用运行态默认 UI,不烘进生成背景。
|
||||
局内 UI 生成分为两类图片:`9:16` 纯背景图和 `1:1` 中心容器 UI 图。纯背景图只表现题材氛围和环境,不得生成锅、圆盘、托盘、拼图槽、物品槽、HUD、文字、按钮、倒计时、分数或物品;中心容器 UI 图单独生成,贴合题材设定,用于覆盖运行态默认圆形竞技容器。容器图必须通过 VectorEngine `/v1/images/edits` multipart 图生图生成,并参考 `public/match3d-background-references/pot-fused-reference.png` 的大尺寸轻俯视构图:容器外轮廓接近画布四边,宽度约占画布 `86%-92%`、高度约占 `82%-90%`,内口为横向椭圆,不能生成小圆盘、正俯视扁盘、侧视碗或小托盘。底部备选栏、顶部控件和拼图/物品槽位继续使用运行态默认 UI,不烘进生成背景;容器图成功加载后,运行态默认圆形锅壳不得再覆盖或裁切生成容器。
|
||||
|
||||
## 7.4 发布前试玩
|
||||
|
||||
@@ -728,7 +728,7 @@ GET /api/runtime/match3d/runs/:runId
|
||||
3. 入口页不展示参考图上传。
|
||||
4. 内置风格显示画风参考图,自定义风格通过独立面板填写并进入提交 payload。
|
||||
5. 移动端入口页所有内容一屏展示,不产生纵向滚动。
|
||||
6. 入口页不展示 `生成音效` Toggle;草稿生成不产生 `clickSound`,结果页物品详情仍可手动生成点击音效。
|
||||
6. 入口页不展示 `生成音效` Toggle;草稿生成不产生 `clickSound`,结果页物品详情当前也不展示点击音效生成入口。
|
||||
7. 系统可生成待发布结果页,并在草稿中返回首批多视角 2D 切割图片素材预览。
|
||||
8. 用户可编辑游戏名称、标签、封面图等基础信息。
|
||||
9. 用户可发布前试玩,且试玩失败不阻断发布。
|
||||
|
||||
101
docs/prd/BABY_LOVE_DRAWING_EDUTAINMENT_LEVEL_PRD_2026-05-13.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 宝贝爱画寓教于乐独立关卡 PRD 2026-05-13
|
||||
|
||||
## 1. 目标
|
||||
|
||||
新增寓教于乐内容线独立关卡:
|
||||
|
||||
```text
|
||||
宝贝爱画
|
||||
```
|
||||
|
||||
工程 ID 固定为:
|
||||
|
||||
```text
|
||||
baby-love-drawing
|
||||
```
|
||||
|
||||
该关卡默认出现在“发现 / 寓教于乐”板块下方。当前阶段只做本地 Demo 保存,验证完成后再补正式持久化。
|
||||
|
||||
## 2. 关卡结构
|
||||
|
||||
启动宝贝爱画后进入绘画运行态:
|
||||
|
||||
1. 屏幕中央是带边框的空白画板;
|
||||
2. 画板边框内是可绘画区域;
|
||||
3. 画板外左侧展示彩虹 7 色;
|
||||
4. 画板外右侧中下方展示画笔和橡皮;
|
||||
5. 用户进入内容后默认手持画笔;
|
||||
6. 手持画笔或橡皮时,屏幕上实时显示跟随右手位置的工具图案标识。
|
||||
|
||||
## 3. 输入规则
|
||||
|
||||
颜色选择:
|
||||
|
||||
1. 仅检测左手;
|
||||
2. 左手悬停在某个颜色区域 1.5 秒后,选中该颜色;
|
||||
3. 颜色固定为彩虹 7 色。
|
||||
|
||||
工具切换:
|
||||
|
||||
1. 右手移动到画笔或橡皮工具区域;
|
||||
2. 右手握拳后,将手里的工具切换为对应工具。
|
||||
|
||||
绘画与擦除:
|
||||
|
||||
1. 右手在画布区域握拳时,当前工具生效;
|
||||
2. 当前工具为画笔时留下轨迹;
|
||||
3. 当前工具为橡皮时擦除轨迹;
|
||||
4. 右手张开时,画笔或橡皮抬起,不在画布上生效。
|
||||
|
||||
按钮选择:
|
||||
|
||||
1. 完成;
|
||||
2. 使用绘画魔法;
|
||||
3. 保存;
|
||||
4. 再画一张;
|
||||
5. 返回。
|
||||
|
||||
以上按钮都使用任一手悬停 2 秒完成选中。
|
||||
|
||||
## 4. 绘画魔法
|
||||
|
||||
用户完成绘画后,可使用“绘画魔法”。
|
||||
|
||||
绘画魔法使用 image-2,以用户绘画内容和笔触轨迹生成对应绘本风格图片内容。
|
||||
|
||||
前端不得直接读取、拼接或暴露图片生成密钥。image-2 调用必须通过后端代理。
|
||||
|
||||
## 5. 保存规则
|
||||
|
||||
当前只做本地 Demo 保存。
|
||||
|
||||
保存规则:
|
||||
|
||||
1. 魔法生成前保存,只保存一张原图;
|
||||
2. 若用户未保存原图,直接点击魔法生成,则魔法生成后保存时同时保存原图和魔法图;
|
||||
3. 保存完毕后,可继续“再画一张”或“返回”。
|
||||
|
||||
## 6. 展示与开关
|
||||
|
||||
宝贝爱画只属于寓教于乐内容线。
|
||||
|
||||
`VITE_ENABLE_EDUTAINMENT_ENTRY` 关闭时:
|
||||
|
||||
1. 不展示“发现 / 寓教于乐”频道;
|
||||
2. 不展示宝贝爱画默认卡片;
|
||||
3. 不允许通过 `/runtime/baby-love-drawing` 直达运行态。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 寓教于乐开启时,“发现 / 寓教于乐”下方展示“宝贝爱画”默认关卡卡片;
|
||||
2. 寓教于乐关闭时,不展示宝贝爱画,也不能直达运行态;
|
||||
3. 进入后展示空白画板、彩虹 7 色、画笔和橡皮;
|
||||
4. 默认工具为画笔;
|
||||
5. 左手悬停颜色 1.5 秒后选中颜色;
|
||||
6. 右手移动到工具区并握拳后切换画笔或橡皮;
|
||||
7. 右手握拳在画布内绘制或擦除,张开时不生效;
|
||||
8. 任一手悬停按钮 2 秒后触发按钮;
|
||||
9. 完成后可保存原图;
|
||||
10. 完成后可使用绘画魔法生成绘本风格图片;
|
||||
11. 未保存原图直接使用绘画魔法后,保存会同时保存原图和魔法图;
|
||||
12. 保存后可再画一张或返回。
|
||||
@@ -29,11 +29,24 @@
|
||||
2. 模板名称:`宝贝识物`;
|
||||
3. 两个物品;
|
||||
4. 两个物品图;
|
||||
5. 作品标签。
|
||||
5. 游戏视觉主题包;
|
||||
6. 作品标签。
|
||||
|
||||
物品图使用 VectorEngine `gpt-image-2-all` / image-2 生成。图片生成只能走后端或后续后端预留接口,前端不得泄露 `VECTOR_ENGINE_API_KEY`。
|
||||
物品图使用 VectorEngine `gpt-image-2-all` / image-2 生成。图片生成只能走后端接口,前端不得读取、拼接或暴露 `VECTOR_ENGINE_API_KEY`。
|
||||
|
||||
本地 Demo 阶段若真实生图接口未接入完成,允许前端 service 返回明确标记为 `placeholder` 的占位图形,用于打通创作到结果页的交互链路;该占位结果不得伪装成正式 image-2 资产。
|
||||
每个关键词只生成一张围绕该关键词的单一物品形象。生成 prompt 必须锁定寓教于乐板块统一的卡通绘本草地舞台插画风,但最终画面不生成背景、场景、氛围渲染、人物、手、篮子、礼物盒、文字、水印或 UI。服务端必须把生成结果转成透明 PNG,并执行透明抠图后处理;只有透明抠图后的素材才允许写入草稿 `itemAssets` 并进入游戏运行态。
|
||||
|
||||
同一次创作还必须使用 image-2 生成游戏视觉主题包,包含背景环境、UI 装饰框、礼物盒、篮子和烟雾弹出特效资源。主题包必须继续保持寓教于乐插画风,并根据用户填写的两个物品关键词匹配主题:例如关键词偏动漫角色或玩具时,背景环境和元素可使用动漫、玩具主题;关键词偏水果时,背景环境和元素可匹配果园、自然主题;其它关键词按其语义匹配合适主题。主题包不得改变关卡玩法规则,不新增文字说明、额外按钮或额外判定规则。
|
||||
|
||||
视觉主题包的资源边界:
|
||||
|
||||
1. 背景环境图不做透明抠图,但必须保证屏幕中间、中下方和底部左右篮子区域清爽,不遮挡放大后的物品、礼物盒和篮子;
|
||||
2. UI 装饰框用于字幕条和计数器风格化包装,只生成装饰边框和主题点缀,不生成文字、数字或按钮;
|
||||
3. 礼物盒资源输出为透明 PNG,运行态按当前礼盒视觉的 2 倍尺寸展示,素材主体必须饱满清晰;
|
||||
4. 篮子资源输出为透明 PNG,运行态按当前篮子视觉的 1.5 倍尺寸展示,左右篮子仍固定为两个物品对应选项,篮子造型资源可以复用同一张主题篮子图;
|
||||
5. 烟雾弹出特效资源输出为透明 PNG,用于礼物盒打开瞬间覆盖开盒区域并承接中央物品弹出,不生成物品、篮子、礼物盒或文字。
|
||||
|
||||
当前本地 Demo 阶段已接入真实 image-2 资源链路。创作提交必须成功获得 `generationProvider = "vector-engine-gpt-image-2"` 的两个物品透明 PNG 和完整视觉主题包后,才能进入结果页、试玩或发布;若后端接口、登录态、VectorEngine 配置或上游生成失败,前端必须停留在生成失败状态并展示错误,不得静默回退为占位图。历史草稿中若仍存在 `generationProvider = "placeholder"` 的占位资源,结果页必须提示重新生成,试玩和发布前必须先补齐 image-2 资源。
|
||||
|
||||
## 4. 标签规则
|
||||
|
||||
@@ -63,6 +76,8 @@
|
||||
|
||||
试玩按钮进入宝贝识物首关运行态,运行态消费当前草稿中的两个物品名称和两张物品图,不重新生成或改写物品内容。
|
||||
|
||||
若草稿包含视觉主题包,运行态还必须消费该主题包中的背景环境、UI 装饰、礼物盒、篮子和烟雾弹出特效资源;旧草稿或接口失败时允许回退到当前 CSS 绘本风兜底。
|
||||
|
||||
## 6. 发布后体验
|
||||
|
||||
发布完成后作品应进入寓教于乐内容线,并在寓教于乐入口开启时可被板块消费。
|
||||
@@ -73,27 +88,30 @@
|
||||
|
||||
本 PRD 同步约束首关运行态,已确认规则包括:
|
||||
|
||||
1. 礼物盒打开在本地调试绑定 `F` 键;
|
||||
1. 进入关卡后礼物盒自动打开并弹出首个随机物品;
|
||||
2. 每轮仅中间礼物盒跳出的物品随机;左右两侧篮子固定为当前草稿两个物品的顺序;
|
||||
3. 下一关按钮当前占位;
|
||||
4. 不新增用户未确认的计时、失败次数、分数、体力或难度递增。
|
||||
5. 屏幕中上方字幕固定为“将物品放入对应的篮子里”。
|
||||
6. 礼物盒位于屏幕中下方,任意手抬起后打开并跳出下一个随机物品。
|
||||
6. 礼物盒位于屏幕中下方并按当前视觉放大一倍,首次进入关卡和每次正确反馈结束后的新轮次都从上方落下后自动打开。
|
||||
7. 屏幕下方左侧和右侧分别展示两个固定篮子,左侧固定使用草稿第一个物品图,右侧固定使用草稿第二个物品图。
|
||||
8. 明确左手连续横向移动达到阈值时将当前物品送入左侧篮子,明确右手连续横向移动达到阈值时将当前物品送入右侧篮子;选篮不使用动作名判定,侧别未知的手部轨迹不参与选篮。
|
||||
9. 正确时展示“真棒”字幕和正确特效;错误时展示“再想一想吧”字幕和错误特效,物品回到中央。
|
||||
10. 成功 20 次后展示“恭喜你!小朋友!”字幕和特效,并展示“再来一次”和“下一关”按钮。
|
||||
11. 当前本地 Demo 阶段音效与语音播报接口只预留调用点,不在前端写死外部硬件或服务接口。
|
||||
8. 左右篮子按当前视觉放大 50%。
|
||||
9. 礼物盒打开时播放烟雾特效,中央物品从烟雾特效中弹出;物品弹出后礼物盒从舞台移除。
|
||||
10. 明确左手连续横向移动达到阈值时将当前物品送入左侧篮子,明确右手连续横向移动达到阈值时将当前物品送入右侧篮子;选篮不使用动作名判定,侧别未知的手部轨迹不参与选篮。
|
||||
11. 正确时展示“真棒”字幕和正确特效;错误时展示“再想一想吧”字幕和错误特效,物品回到中央。
|
||||
12. 成功 20 次后展示“恭喜你!小朋友!”字幕和特效,并展示“再来一次”和“下一关”按钮。
|
||||
13. 当前本地 Demo 阶段音效与语音播报接口只预留调用点,不在前端写死外部硬件或服务接口。
|
||||
|
||||
## 8. 验收
|
||||
|
||||
1. 创作入口显示 `宝贝识物` 并可进入模板表单。
|
||||
2. 未填写任一物品名称时不能生成草稿。
|
||||
3. 生成草稿后进入结果页,展示两个物品名称和物品图。
|
||||
4. 草稿标签中始终包含精确 `寓教于乐`。
|
||||
5. 发布 payload 始终包含精确 `寓教于乐`。
|
||||
6. 发布完成后出现分享弹窗或发布完成状态。
|
||||
7. 前端不读取或暴露 VectorEngine 密钥。
|
||||
8. 结果页试玩进入宝贝识物运行态,不再显示“试玩关卡正在接入中”。
|
||||
9. 运行态可通过 `F` 打开礼物盒,通过鼠标左键拖动映射左手横向移动,通过鼠标右键拖动映射右手横向移动。
|
||||
10. 成功 20 次后出现“再来一次”和“下一关”按钮。
|
||||
4. 生成草稿后包含视觉主题包,主题包含背景环境、UI 装饰框、礼物盒、篮子和烟雾弹出特效资源。
|
||||
5. 草稿标签中始终包含精确 `寓教于乐`。
|
||||
6. 发布 payload 始终包含精确 `寓教于乐`。
|
||||
7. 发布完成后出现分享弹窗或发布完成状态。
|
||||
8. 前端不读取或暴露 VectorEngine 密钥。
|
||||
9. 结果页试玩进入宝贝识物运行态,不再显示“试玩关卡正在接入中”。
|
||||
10. 运行态通过鼠标左键拖动映射左手横向移动,通过鼠标右键拖动映射右手横向移动。
|
||||
11. 成功 20 次后出现“再来一次”和“下一关”按钮。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- 需求来源:用户提供的视频 `C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`,并已在 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md` 中完成抽帧分析和玩法方案整理。
|
||||
- 玩法定位:浏览器 2D 声控狗叫对战小游戏,暂定中文名 `汪汪声浪大作战`,英文代号与 play type ID 建议为 `bark-battle`。
|
||||
- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效叫声次数和叫声节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。
|
||||
- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效声浪触发次数和声浪节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。
|
||||
- 文档目的:为产品、测试、前端、后端在编码前统一可验证验收口径;本文只定义 PRD/BDD 级行为与测试映射,不实现工程代码。
|
||||
|
||||
## 角色与目标
|
||||
@@ -20,14 +20,14 @@
|
||||
### 用户目标
|
||||
|
||||
- 玩家可以在开局前完成麦克风授权和环境噪音校准。
|
||||
- 玩家发出有效狗叫时,能看到叫声计数、狗狗动画、拟声词/冲击波以及能量条变化。
|
||||
- 低于阈值的背景噪音不会被误计为有效叫声。
|
||||
- 玩家产生有效声浪触发时,能看到声浪计数、狗狗动画、拟声词/冲击波以及能量条变化。
|
||||
- 低于阈值的背景噪音不会被误计为有效声浪触发。
|
||||
- 单局在 30 秒后给出明确胜负、平局和关键数据。
|
||||
- 移动端和不支持麦克风的环境不会进入不可操作状态。
|
||||
|
||||
### 非目标
|
||||
|
||||
- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值间隔、持续时间和校准结果为准。
|
||||
- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值冷却间隔和校准结果为准。
|
||||
- MVP 不要求实时联机对战;可先按“玩家 vs AI 对手”完成单机浏览器 runtime。
|
||||
- MVP 不要求成绩持久化、作品发布、作品架、广场和排行榜;若后续接入 Genarrative 作品闭环,需要另补玩法类型集成 PRD/技术文档。
|
||||
- MVP 不要求在 UI 中长期展示大段规则说明;游戏界面应保持倒计时、能量条、狗狗、麦克风状态和结算信息为主。
|
||||
@@ -38,10 +38,10 @@
|
||||
- 单局时长:默认 30 秒,从正式进入 `playing` 阶段开始计时。
|
||||
- 能量条:使用 `-100` 到 `100` 的连续值表示,负数偏对手侧,正数偏玩家侧,`0` 为中线。
|
||||
- 平局阈值:倒计时结束时,若能量条绝对值小于或等于 `drawThreshold`,判定平局;具体数值由实现配置,但测试需可注入固定阈值。
|
||||
- 有效叫声:一次有效叫声至少满足:音量超过校准后的有效阈值、与上一次有效峰值间隔不小于 `minBarkGapMs`、持续时长在 `minBarkDurationMs` 到 `maxBarkDurationMs` 之间。
|
||||
- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加叫声次数,也不得让能量条出现可见推进。
|
||||
- 推动力:玩家推动力由音量分数、有效叫声频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100` 到 `100`。
|
||||
- UI 反馈:有效叫声应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。
|
||||
- 有效声浪触发:一次有效声浪触发满足:当前麦克风采样点的归一化响度达到或超过校准后的有效阈值,且与上一次有效声浪触发间隔不小于 `minBarkGapMs`;不再要求持续高响度时长达标,也不等待响度回落。
|
||||
- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加声浪触发次数,也不得让能量条出现可见推进。
|
||||
- 推动力:玩家推动力由音量分数、有效声浪触发频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100` 到 `100`。
|
||||
- UI 反馈:有效声浪触发应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。
|
||||
|
||||
## 中文 Gherkin 场景
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
场景: 校准完成后进入开局倒计时
|
||||
假如玩家已允许麦克风权限
|
||||
而且系统已采集足够的环境噪音样本
|
||||
当校准计算出有效叫声阈值
|
||||
当校准计算出有效声浪阈值
|
||||
那么系统应进入开局倒计时阶段
|
||||
而且倒计时结束后应进入 30 秒对战阶段
|
||||
而且初始能量条应位于中线
|
||||
@@ -84,19 +84,19 @@
|
||||
功能: 环境噪音校准
|
||||
为了减少背景噪音误触发
|
||||
作为浏览器玩家
|
||||
我希望游戏在开局前根据当前环境设置有效叫声阈值
|
||||
我希望游戏在开局前根据当前环境设置有效声浪阈值
|
||||
|
||||
场景: 安静环境生成低但非零的有效阈值
|
||||
假如校准阶段采集到的环境噪音 RMS 稳定低于默认噪音基线
|
||||
当系统完成校准
|
||||
那么有效叫声阈值应高于环境噪音平均值
|
||||
那么有效声浪阈值应高于环境噪音平均值
|
||||
而且阈值不应低于系统配置的最小阈值
|
||||
|
||||
场景: 嘈杂环境生成更高的有效阈值
|
||||
假如校准阶段采集到的环境噪音 RMS 高于默认噪音基线
|
||||
当系统完成校准
|
||||
那么有效叫声阈值应随环境噪音上调
|
||||
而且低于该阈值的后续输入不应计为有效叫声
|
||||
那么有效声浪阈值应随环境噪音上调
|
||||
而且低于该阈值的后续输入不应计为有效声浪触发
|
||||
|
||||
场景: 校准期间无法获得有效音频样本
|
||||
假如麦克风授权成功但音频样本持续为空或不可读
|
||||
@@ -106,36 +106,36 @@
|
||||
而且不应直接开始对战
|
||||
```
|
||||
|
||||
### 功能: 有效叫声计数
|
||||
### 功能: 有效声浪触发计数
|
||||
|
||||
```gherkin
|
||||
功能: 有效叫声计数
|
||||
为了把玩家的狗叫行为转换为可计分输入
|
||||
功能: 有效声浪触发计数
|
||||
为了把玩家的声控行为转换为可计分输入
|
||||
作为玩家
|
||||
我希望每次符合规则的短促叫声只被计数一次
|
||||
我希望每次超过阈值且满足冷却的声浪触发只被计数一次
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且系统已完成环境噪音校准
|
||||
|
||||
场景: 单次超过阈值且间隔足够的叫声计数加一
|
||||
假如玩家当前叫声次数为 0
|
||||
而且上一次有效叫声时间早于 minBarkGapMs
|
||||
当麦克风输入出现一次超过有效阈值且持续时长合规的峰值
|
||||
那么玩家叫声次数应变为 1
|
||||
场景: 单次超过阈值且间隔足够的声浪触发计数加一
|
||||
假如玩家当前声浪触发次数为 0
|
||||
而且上一次有效声浪触发时间早于 minBarkGapMs
|
||||
当某个麦克风采样点达到或超过有效阈值且满足声浪冷却
|
||||
那么玩家声浪触发次数应变为 1
|
||||
而且玩家侧应出现一次吠叫动画反馈
|
||||
而且画面应出现一次拟声词或冲击波反馈
|
||||
|
||||
场景: 持续噪音不会被无限计数
|
||||
假如玩家当前叫声次数为 1
|
||||
当麦克风输入持续超过阈值但没有新的峰值间隔
|
||||
那么玩家叫声次数不应在每个 tick 中持续增加
|
||||
而且系统最多只应记录当前连续声音段内的一次有效叫声
|
||||
场景: 持续高响度输入只按冷却节奏计数
|
||||
假如玩家当前声浪触发次数为 1
|
||||
当麦克风输入持续超过阈值但仍处于声浪冷却内
|
||||
那么玩家声浪触发次数不应在每个 tick 中持续增加
|
||||
而且系统只应在冷却结束后的采样点再次达阈值时记录下一次有效声浪触发
|
||||
|
||||
场景: 间隔过短的连续峰值不重复计数
|
||||
假如玩家刚刚产生一次有效叫声
|
||||
当麦克风输入在 minBarkGapMs 内再次出现峰值
|
||||
那么玩家叫声次数不应增加
|
||||
假如玩家刚刚产生一次有效声浪触发
|
||||
当麦克风输入在 minBarkGapMs 内再次达到有效阈值
|
||||
那么玩家声浪触发次数不应增加
|
||||
而且连击或推动力不应因该峰值重复加成
|
||||
```
|
||||
|
||||
@@ -145,23 +145,23 @@
|
||||
功能: 声浪推动能量条
|
||||
为了复刻双方比狗叫的核心体验
|
||||
作为玩家
|
||||
我希望更响、更连续的有效叫声能把顶部能量条推向自己一侧
|
||||
我希望更响、更高频的有效声浪触发能把顶部能量条推向自己一侧
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且能量条当前位于中线
|
||||
|
||||
场景: 玩家推动力高于对手时能量条向玩家侧移动
|
||||
假如玩家在短时间窗口内产生多次有效叫声
|
||||
假如玩家在短时间窗口内产生多次有效声浪触发
|
||||
而且玩家推动力高于对手推动力
|
||||
当系统推进一个 simulation tick
|
||||
那么能量条数值应向玩家侧增加
|
||||
而且顶部红蓝能量条的玩家侧占比应变大
|
||||
|
||||
场景: 连续大声叫声触发更强反馈
|
||||
假如玩家连续产生多次高于强叫声阈值的有效叫声
|
||||
场景: 连续强声浪触发触发更强反馈
|
||||
假如玩家连续产生多次高于强声浪阈值的有效声浪触发
|
||||
当系统计算玩家连击加成
|
||||
那么玩家侧推动力应高于单次普通叫声推动力
|
||||
那么玩家侧推动力应高于单次普通声浪触发推动力
|
||||
而且玩家侧声浪或冲击波反馈应比普通叫声更明显
|
||||
但是反馈不应遮挡倒计时和能量条
|
||||
|
||||
@@ -185,31 +185,30 @@
|
||||
功能: 背景噪音过滤
|
||||
为了避免环境声替玩家自动得分
|
||||
作为玩家
|
||||
我希望低于阈值或不合规的声音不会被当作有效狗叫
|
||||
我希望低于阈值或处于冷却内的声音不会被当作有效声浪触发
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且系统已完成环境噪音校准
|
||||
|
||||
场景: 低于阈值的背景噪音不计数
|
||||
当麦克风只接收到低于有效叫声阈值的背景噪音
|
||||
那么玩家叫声次数不应增加
|
||||
当麦克风只接收到低于有效声浪阈值的背景噪音
|
||||
那么玩家声浪触发次数不应增加
|
||||
而且玩家侧不应播放吠叫动画
|
||||
而且能量条不应因为该背景噪音出现可见推进
|
||||
|
||||
场景: 过短脉冲不计为有效叫声
|
||||
假如麦克风输入峰值超过有效阈值
|
||||
但是持续时长短于 minBarkDurationMs
|
||||
当系统完成该声音段判定
|
||||
那么玩家叫声次数不应增加
|
||||
场景: 冷却内重复达阈值不计数
|
||||
假如玩家刚刚产生一次有效声浪触发
|
||||
当麦克风输入在 minBarkGapMs 内再次达到有效声浪阈值
|
||||
那么玩家声浪触发次数不应增加
|
||||
而且不应触发连击加成
|
||||
|
||||
场景: 过长持续声被削弱为单段输入
|
||||
假如麦克风输入持续超过有效阈值
|
||||
但是持续时长长于 maxBarkDurationMs
|
||||
当系统完成该声音段判定
|
||||
那么系统不应按多个叫声重复计数
|
||||
而且该声音段的推动力应按持续噪音削弱规则处理
|
||||
场景: 持续高响度输入只按冷却节奏产生触发
|
||||
假如麦克风输入持续达到或超过有效声浪阈值
|
||||
当声浪冷却尚未结束
|
||||
那么系统不应在每个 tick 中重复计数
|
||||
当声浪冷却结束且当前采样仍达到有效声浪阈值
|
||||
那么系统可以记录下一次有效声浪触发
|
||||
```
|
||||
|
||||
### 功能: 倒计时与胜负结算
|
||||
@@ -227,20 +226,20 @@
|
||||
当系统时间从 30 秒推进到 0 秒
|
||||
那么界面应显示倒计时归零
|
||||
而且系统应进入 finished 结算阶段
|
||||
而且归零后的麦克风输入不应再改变本局能量条和叫声次数
|
||||
而且归零后的麦克风输入不应再改变本局能量条和声浪触发次数
|
||||
|
||||
场景: 玩家侧占优时判定玩家胜利
|
||||
假如倒计时归零时能量条数值大于 drawThreshold
|
||||
当系统进入结算阶段
|
||||
那么系统应判定玩家胜利
|
||||
而且结算面板应展示玩家叫声次数、最大音量和声浪评分
|
||||
而且结算面板应展示玩家声浪触发次数、最大音量和声浪评分
|
||||
而且应提供再来一局入口
|
||||
|
||||
场景: 对手侧占优时判定玩家失败
|
||||
假如倒计时归零时能量条数值小于 -drawThreshold
|
||||
当系统进入结算阶段
|
||||
那么系统应判定对手胜利
|
||||
而且结算面板应展示玩家叫声次数、最大音量和声浪评分
|
||||
而且结算面板应展示玩家声浪触发次数、最大音量和声浪评分
|
||||
而且应提供再来一局入口
|
||||
|
||||
场景: 能量条接近平衡时判定平局
|
||||
@@ -265,7 +264,7 @@
|
||||
当玩家选择再来一局
|
||||
那么系统应重置剩余时间为 30 秒
|
||||
而且能量条应回到中线
|
||||
而且玩家叫声次数、最大音量、连击和胜负结果应清零
|
||||
而且玩家声浪触发次数、最大音量、连击和胜负结果应清零
|
||||
而且系统应重新进入校准或开局倒计时流程
|
||||
|
||||
场景: 结算后返回玩法入口
|
||||
@@ -353,7 +352,7 @@
|
||||
假如玩家在 playing 阶段刷新页面
|
||||
当页面重新加载 bark-battle
|
||||
那么系统应重新进入权限检查或授权准备状态
|
||||
而且不应沿用刷新前的剩余时间、能量条和叫声次数作为新局结果
|
||||
而且不应沿用刷新前的剩余时间、能量条和声浪触发次数作为新局结果
|
||||
```
|
||||
|
||||
## 测试映射
|
||||
@@ -367,15 +366,15 @@
|
||||
| 嘈杂环境生成更高的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned |
|
||||
| 校准期间无法获得有效音频样本 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 单次超过阈值且间隔足够的叫声计数加一 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 持续噪音不会被无限计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 持续高响度输入只按冷却节奏计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 间隔过短的连续峰值不重复计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 玩家推动力高于对手时能量条向玩家侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 连续大声叫声触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned |
|
||||
| 连续强声浪触发触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned |
|
||||
| 能量条到达边界后不会越界 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 对手推动力高于玩家时能量条向对手侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 低于阈值的背景噪音不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 过短脉冲不计为有效叫声 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 过长持续声被削弱为单段输入 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 冷却内重复达阈值不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 持续高响度输入只按冷却节奏产生触发 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 倒计时每秒递减并在归零时停止对战输入 | unit/application | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/application/BarkBattleController.test.ts` | planned |
|
||||
| 玩家侧占优时判定玩家胜利 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
| 对手侧占优时判定玩家失败 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
@@ -394,8 +393,8 @@
|
||||
## 验收清单
|
||||
|
||||
- [ ] 权限允许、拒绝、非安全上下文、API 不支持、麦克风未找到/不可读、AudioContext 被拦截、校准超时或样本不可读均有明确状态,且不会误进入 playing。
|
||||
- [ ] 校准阶段会影响有效叫声阈值,低噪音不会增加叫声计数。
|
||||
- [ ] 有效叫声计数具备阈值、峰值间隔、持续时长约束。
|
||||
- [ ] 校准阶段会影响有效声浪阈值,低噪音不会增加叫声计数。
|
||||
- [ ] 有效声浪触发计数具备阈值与声浪冷却约束。
|
||||
- [ ] 能量条根据双方推动力差值双向移动,并限制在 `-100` 到 `100`。
|
||||
- [ ] 30 秒归零后停止本局输入影响,并按玩家胜利、对手胜利、平局三类结果结算。
|
||||
- [ ] 移动端核心元素可见,非关键设置收起,不在主画面堆叠长规则说明。
|
||||
@@ -405,8 +404,8 @@
|
||||
## 开放问题
|
||||
|
||||
1. MVP 是否确认只做“玩家 vs AI”,还是第一版需要双人同屏或联机对战?
|
||||
2. `drawThreshold`、`minBarkGapMs`、`minBarkDurationMs`、`maxBarkDurationMs` 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值?
|
||||
2. `drawThreshold`、`minBarkGapMs`、有效声浪阈值 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值?
|
||||
3. 是否允许无麦克风设备提供键盘/点击备用输入?若允许,需要另补非声控模式场景;若不允许,当前降级只提供返回入口。
|
||||
4. 是否需要在结算中记录或上报成绩、最高音量、叫声次数和声浪评分?若需要,需补埋点/后端持久化场景。
|
||||
4. 是否需要在结算中记录或上报成绩、最高音量、声浪触发次数和声浪评分?若需要,需补埋点/后端持久化场景。
|
||||
5. bark-battle 是否作为 Genarrative 正式 play type 接入创作入口、作品发布和广场,还是先作为独立 runtime 原型验证?
|
||||
6. 狗狗、背景、拟声词和冲击波素材来源是临时占位、AI 生成,还是复用项目现有素材管线?
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 重点入口
|
||||
|
||||
- [宝贝爱画寓教于乐独立关卡 PRD](./BABY_LOVE_DRAWING_EDUTAINMENT_LEVEL_PRD_2026-05-13.md):定义寓教于乐内容线的 `宝贝爱画` 独立本地 Demo 关卡,覆盖画板、七色选择、画笔/橡皮、手部绘画、完成、image-2 绘画魔法、本地保存和关闭入口隐藏边界。
|
||||
- [宝贝识物寓教于乐模板 PRD](./BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md):定义寓教于乐内容线的 `宝贝识物` 创作模板,覆盖两个物品名称输入、image-2 物品图生成、精确 `寓教于乐` 标签、结果页和发布边界。
|
||||
- [AI 原生幕间文字游戏模板 PRD:参考 MOKU 的剧本模拟器闭环](./AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md):参考 MOKU / 幕间类 AI 文游的剧本游乐场、自由行动、AI GM、记忆和模拟器强反馈经验,但只落为陶泥儿 `text-game` 模板,复用平台接口,不迁入外部社区、支付、私有存档或回放。
|
||||
- [AI 原生视觉小说模板 PRD:TXT 玩法平台化接入](./AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md):参考 `Interactive-fiction-backend` / `Interactive-fiction-frontend` 的 TXT 玩法经验,但只保留视觉小说模板创作与运行闭环,完全使用 Genarrative 平台接口,并明确删除回放和外部平台功能。
|
||||
|
||||
@@ -67,6 +67,8 @@ Admin Web
|
||||
|
||||
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态,并让 api-server 熔断对应玩法创作 / 运行态 API。隐藏入口但仍保留既有作品号、广场详情或试玩链路时,应只关闭 `visible`,不要关闭 `open`。
|
||||
|
||||
当前默认配置中,`visual-novel` 暂时从创作页隐藏并关闭入口,默认种子为 `visible=false`、`open=false`。如果后续只想恢复已发布作品试玩而不恢复创作入口,需要先明确 API 熔断范围,再通过后台入口开关调整,不能在前端硬编码恢复模板 Tab。
|
||||
|
||||
## 注意
|
||||
|
||||
- 前端后台页面只做管理表单,不成为配置事实源。
|
||||
|
||||
@@ -70,7 +70,7 @@ Query:
|
||||
- SQL 固定为 `SELECT * FROM {tableName} LIMIT {limit}`;SpacetimeDB 2.2 HTTP SQL 不拼 `ORDER BY`。
|
||||
- 用户输入不直接拼入 SQL;关键词和条件在 API Server 内存中过滤。
|
||||
- private 表或 token 不可见时返回后台可读错误信息。
|
||||
- SpacetimeDB SQL 行和 SATS 值统一转成人可读 JSON:Option None 为 null,Some 展开为内部值,Timestamp 单元素数组展开为内部值,enum 可保留 tag/name 或原始数组文本。
|
||||
- SpacetimeDB SQL 行和 SATS 值统一转成人可读 JSON:Option None 为 null,Some 展开为内部值,Timestamp 单元素数组展开为内部值;已知业务枚举列应在 API Server 按表名和列名转换为业务字符串,例如 `profile_recharge_order.kind` 转为 `points` / `membership`,`profile_recharge_order.status` 转为 `pending` / `paid` / `failed` / `closed` / `refunded`。
|
||||
|
||||
## 前端页面
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ GENARRATIVE_LLM_BASE_URL=
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=
|
||||
|
||||
# APIMart / OpenAI 兼容 Responses 文本网关与抓大鹅 nanobanana 物品素材图
|
||||
# APIMart / OpenAI 兼容 Responses 文本网关
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# VectorEngine / GPT-image-2 / Suno / Vidu 生成网关
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
# VectorEngine / Gemini 原生图片 / GPT-image-2 / Suno / Vidu 生成网关
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
@@ -102,13 +102,13 @@ HYPER3D_MODEL_REQUEST_TIMEOUT_MS / RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
3. 文本 LLM provider 为 `ark` 且未配置 `GENARRATIVE_LLM_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
4. 角色视频 provider 复用 Ark 且未配置 `ARK_CHARACTER_VIDEO_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
5. 具体模型名缺失时不在配置层伪造默认模型,调用到对应能力时由下游配置校验返回缺配置错误。
|
||||
6. VectorEngine 图片与音频生成只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 图片生成额外读取 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;不复用 `APIMART_*`、`GENARRATIVE_LLM_*` 或前端变量。拼图 Agent 的生成 action 不做前端自动重试,避免一次点击在上游超时后重复触发外部生图与钱包扣退费;若 VectorEngine 请求达到该超时窗口,api-server 返回 `504 Gateway Timeout`,`error.details.provider` 为 `vector-engine`,并保留具体超时 message。
|
||||
6. VectorEngine 图片与音频生成只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 与抓大鹅 Gemini 素材 sheet 图片生成额外读取 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;不复用 `APIMART_*`、`GENARRATIVE_LLM_*` 或前端变量。拼图 Agent 的生成 action 不做前端自动重试,避免一次点击在上游超时后重复触发外部生图与钱包扣退费;若 VectorEngine 请求达到该超时窗口,api-server 返回 `504 Gateway Timeout`,`error.details.provider` 为 `vector-engine`,并保留具体超时 message。
|
||||
7. 火山引擎语音能力由 `platform-speech` 收口协议帧与上游鉴权,`api-server` 只暴露平台鉴权后的代理路由,不向前端返回任何密钥字段。
|
||||
8. Hyper3D Rodin Gen-2 使用公开默认 `https://api.hyper3d.com/api/v2`,API Key 只读取 `HYPER3D_API_KEY` / `RODIN_API_KEY`,不复用文本 LLM、图片或音频网关密钥。
|
||||
9. APIMart 当前保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路,并用于抓大鹅物品素材 sheet 的 `nanobanana2` / Gemini 图片模型;GPT-image-2 图片生成不得读取 APIMart 配置。
|
||||
9. APIMart 当前只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路;抓大鹅物品素材 sheet、GPT-image-2 图片生成和音频生成都不得读取 APIMart 配置。
|
||||
10. 本地 `npm run api-server`、`npm run dev:rust`、`npm run dev` 与 `npm run dev:web` 的环境文件优先级固定为非空外层 shell 变量最高,其后 `.env`、`.env.local`、`.env.secrets.local` 逐层覆盖;真实密钥建议放在 `.env.secrets.local`,防止 `.env` 中的空示例值覆盖私密配置。外层 shell 变量如果是空字符串或全空白,不再遮蔽本地 env 文件中的真实值。
|
||||
11. OSS 客户端只在 `ALIYUN_OSS_BUCKET`、`ALIYUN_OSS_ENDPOINT`、`ALIYUN_OSS_ACCESS_KEY_ID`、`ALIYUN_OSS_ACCESS_KEY_SECRET` 四项齐全时初始化。四项全部缺失表示未启用 OSS;部分缺失时 `api-server` 记录 warning 并继续启动,具体上传、换签或读取 generated 私有资产的接口返回 `OSS 未完成环境变量配置`,并在 `error.details.missingEnv` 中列出缺失变量。
|
||||
12. 抓大鹅 2D 草稿素材生成需要同时具备 APIMart、VectorEngine 与 OSS 配置:APIMart `gemini-3.1-flash-image-preview` 负责生成 5x5 物品素材 sheet,VectorEngine `gpt-image-2-all` 负责封面、9:16 背景图和 1:1 容器 UI 图,OSS 负责保存切割后的五视角图片及其它生成图。缺少 APIMart、VectorEngine 或 OSS 时应通过 `error.details.reason` 向前端暴露具体缺项,不能只显示泛化“服务暂不可用”。素材图、封面图和背景图生成在调用外部生图前必须先预检 OSS,避免已消耗外部生图后才发现无法落库。
|
||||
12. 抓大鹅 2D 草稿素材生成需要同时具备 VectorEngine 与 OSS 配置:VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent` 负责生成 5x5 物品素材 sheet;封面和 `9:16` 背景图走 VectorEngine `/v1/images/generations` 的 `gpt-image-2-all` JSON 链路;`1:1` 容器 UI 图走 VectorEngine `/v1/images/edits` multipart 链路,并把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,不能再用 generations `image` 数组弱参考。OSS 负责保存切割后的五视角图片及其它生成图。缺少 VectorEngine 或 OSS 时应通过 `error.details.reason` 向前端暴露具体缺项,不能只显示泛化“服务暂不可用”。素材图、封面图和背景图生成在调用外部生图前必须先预检 OSS,避免已消耗外部生图后才发现无法落库。
|
||||
13. 拼图有参考图且开启 AI 重绘时使用 VectorEngine `POST /v1/images/edits` multipart 接口。若返回 `error sending request for url`,代表后端未收到 HTTP 响应;响应 `details` 会带 `reason`、`source`、`connect`、`body`、`timeout` 和 `endpoint`,排查时优先检查服务器网络、DNS、防火墙、代理和参考图大小。拼图图片客户端强制 HTTP/1.1,以降低上游 multipart HTTP/2 连接中断风险。
|
||||
14. 本地排查 `OSS 未完成环境变量配置` 时必须核对键名是否精确为 `ALIYUN_OSS_ACCESS_KEY_SECRET`。常见误写是把 `OSS` 的首字母 `O` 写成数字 `0`,例如 `ALIYUN_0SS_ACCESS_KEY_SECRET`;该键不会被 `api-server` 读取。
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- Puzzle Agent 图片生成动作 `compile_puzzle_draft`、`generate_puzzle_images`
|
||||
- Puzzle Agent 动作 `publish_puzzle_work`
|
||||
- Match3D / 抓大鹅草稿生成动作 `match3d_compile_draft`
|
||||
- 拼图 / 抓大鹅结果页手动生成背景音乐、UI 背景与抓大鹅批量新增物品素材
|
||||
- 拼图 / 抓大鹅结果页 UI 背景与抓大鹅批量新增物品素材;背景音乐和点击音效生成入口当前临时关闭,不进入计费范围
|
||||
|
||||
暂不接入以下入口:
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
- 每次可计费资产操作消耗 `1` 枚泥点。
|
||||
- 例外:Match3D / 抓大鹅草稿生成是一次完整草稿外部生成动作,固定消耗 `10` 枚泥点;流水仍复用 `asset_operation_consume` / `asset_operation_refund`,`asset_kind = match3d_draft_generation`。
|
||||
- 例外:拼图 / 抓大鹅背景音乐生成固定消耗 `5` 枚泥点;物品点击音效仍按单个音效任务消耗 `10` 枚泥点。
|
||||
- 例外:拼图 / 抓大鹅 UI 背景重新生成固定消耗 `2` 枚泥点。
|
||||
- 例外:抓大鹅结果页批量新增物品素材按实际可新增物品名计费,每 `5` 个消耗 `2` 枚泥点,不足 `5` 个向上按 `5` 个计。重复名称、作品中已有名称和超过容量上限的名称不进入计费数量。
|
||||
- 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。
|
||||
@@ -42,6 +41,20 @@
|
||||
- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。
|
||||
- 如果退款失败,原始错误仍返回给调用方,同时服务端日志记录退款失败,便于后续人工核对。
|
||||
|
||||
## 前端确认交互
|
||||
|
||||
所有前端可见且会消耗泥点的按钮,点击后必须先弹出独立确认面板,面板标题使用 `确认消耗泥点`,正文只展示本次消耗数量,例如 `消耗 2 泥点`。用户点击 `确定` 后才允许调用后端扣费动作;点击 `取消` 或关闭面板不得触发接口。
|
||||
|
||||
2026-05-14 当前已覆盖的草稿页入口包括:
|
||||
|
||||
- 拼图入口 `AI重绘=true` 的 `生成拼图游戏草稿`:`2` 泥点;`AI重绘=false` 直接使用上传图,不显示泥点确认。
|
||||
- 拼图结果页关卡 `生成画面` / `重新生成画面`:`2` 泥点。
|
||||
- 拼图结果页 `素材配置 > UI` 的 `生成UI背景` / `重新生成`:`2` 泥点。
|
||||
- 拼图结果页发布按钮:`1` 泥点,发布确认面板必须显示本次消耗数量。
|
||||
- 抓大鹅入口 `生成抓大鹅草稿`:`10` 泥点。
|
||||
- 抓大鹅结果页 `素材配置 > 物品` 的批量新增与批量重新生成:按实际计费数量展示动态泥点数。
|
||||
- 抓大鹅结果页 `素材配置 > UI` 与 `素材配置 > 容器形象` 的 `重新生成`:各 `2` 泥点。
|
||||
|
||||
## 钱包流水
|
||||
|
||||
公开两个流水来源类型,统一覆盖“资产生成”和“资产发布”这两类资产操作。流水金额由具体资产操作成本决定,不再假定所有资产操作都是 `1` 枚泥点:
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# 宝贝爱画本地 Demo 运行态实现方案 2026-05-13
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案落地寓教于乐独立关卡:
|
||||
|
||||
```text
|
||||
baby-love-drawing / 宝贝爱画
|
||||
```
|
||||
|
||||
当前范围只做本地 Demo 闭环:
|
||||
|
||||
1. 寓教于乐频道默认关卡卡片;
|
||||
2. 独立运行态;
|
||||
3. mocap 与开发者调试输入;
|
||||
4. Canvas 绘制和擦除;
|
||||
5. image-2 绘画魔法后端代理;
|
||||
6. localStorage 本地保存;
|
||||
7. 直达路由开关保护。
|
||||
|
||||
本阶段不接正式持久化表,不新增作品发布、作品号、公开详情或搜索入口。
|
||||
|
||||
## 2. 前端接入点
|
||||
|
||||
已新增页面阶段:
|
||||
|
||||
```text
|
||||
baby-love-drawing-runtime
|
||||
```
|
||||
|
||||
已新增路由:
|
||||
|
||||
```text
|
||||
/runtime/baby-love-drawing
|
||||
```
|
||||
|
||||
已新增文件:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/edutainmentBabyDrawing.ts
|
||||
src/services/edutainment-baby-drawing/babyDrawingClient.ts
|
||||
src/components/edutainment-runtime/babyLoveDrawingModel.ts
|
||||
src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx
|
||||
server-rs/crates/api-server/src/edutainment_baby_drawing.rs
|
||||
```
|
||||
|
||||
已接入:
|
||||
|
||||
1. `src/components/rpg-entry/RpgEntryHomeView.tsx`:寓教于乐频道默认展示宝贝爱画卡片;
|
||||
2. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`:启动宝贝爱画运行态;
|
||||
3. `src/components/platform-entry/platformEntryTypes.ts`:扩展 `SelectionStage`;
|
||||
4. `src/routing/appPageRoutes.ts`:扩展路由;
|
||||
5. `src/routing/appRoutes.tsx`:直达路由开关保护;
|
||||
6. `src/index.css`:补齐寓教于乐默认关卡卡片和宝贝爱画运行态样式;
|
||||
7. `server-rs/crates/api-server/src/app.rs`:挂载绘画魔法后端路由。
|
||||
|
||||
## 3. 契约
|
||||
|
||||
契约放在:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/edutainmentBabyDrawing.ts
|
||||
```
|
||||
|
||||
核心字段:
|
||||
|
||||
1. `templateId = "baby-love-drawing"`;
|
||||
2. `templateName = "宝贝爱画"`;
|
||||
3. `originalImageSrc` 保存原始画布图;
|
||||
4. `magicImageSrc` 保存 image-2 魔法图,可为 `null`;
|
||||
5. `strokeTrace` 保存画笔和橡皮轨迹;
|
||||
6. `saveMode = "original-only" | "original-and-magic"` 记录保存结果。
|
||||
|
||||
## 4. 运行态模型
|
||||
|
||||
运行态状态:
|
||||
|
||||
```text
|
||||
drawing
|
||||
finished
|
||||
magicPending
|
||||
magicReady
|
||||
saved
|
||||
```
|
||||
|
||||
工具:
|
||||
|
||||
```text
|
||||
brush
|
||||
eraser
|
||||
```
|
||||
|
||||
颜色:
|
||||
|
||||
```text
|
||||
红、橙、黄、绿、青、蓝、紫
|
||||
```
|
||||
|
||||
按钮悬停:
|
||||
|
||||
1. 颜色选择只接受左手悬停,阈值 1500ms;
|
||||
2. 按钮选择接受任一手悬停,阈值 2000ms;
|
||||
3. 工具切换只接受右手在工具区域握拳。
|
||||
4. 画笔 / 橡皮光标位置只接受右手坐标;左手缺帧或左手移动不得重置、替换或驱动画笔位置。
|
||||
5. 左手需要显示独立位置指示器,帮助用户确认当前是否悬停在目标颜色上;该指示器只表达左手位置,不参与画笔 / 橡皮操作。
|
||||
6. 本地 mocap handedness 当前按摄像头视角输出,宝贝爱画运行态消费前需要换算为用户身体视角:`rightHand` 作为用户左手,`leftHand` 作为用户右手。键鼠调试输入不做该换算。
|
||||
7. 真实硬件短暂缺失某只手时,显示层保留上一帧位置约 320ms 并做轻微坐标平滑;绘制层仍只在当前帧确认用户右手存在时生效。
|
||||
8. 为避免左手抢画笔,本关不做动态 handedness 换手纠正;`rightHand` 永远只进入用户左手选色通道,`leftHand` 永远只进入用户右手画笔通道。若硬件侧 handedness 继续抖动,宁可右手画笔短暂停住,也不允许左手驱动画笔。
|
||||
9. 右手画笔通道增加单帧最大位移门禁;若 camera-left 候选点相对上一帧右手位置出现不合理大跳,判定为不可信帧,只保留上一帧光标并停止绘制。
|
||||
|
||||
## 5. Canvas 绘制
|
||||
|
||||
画板使用 DOM Canvas。
|
||||
|
||||
绘制规则:
|
||||
|
||||
1. 右手在画板内且状态为 `grab` 时生效;
|
||||
2. 工具为 `brush` 时,以当前颜色绘制连续线段;
|
||||
3. 工具为 `eraser` 时,以 `destination-out` 擦除;
|
||||
4. 右手状态为 `open_palm` 或离开画板时结束当前笔画;
|
||||
5. 当前帧没有右手坐标时只结束当前笔画,不把左手坐标用于绘制、擦除或光标定位;
|
||||
6. 每条笔画记录工具、颜色、点位和时间。
|
||||
|
||||
## 6. 绘画魔法
|
||||
|
||||
前端 service:
|
||||
|
||||
```text
|
||||
createBabyDrawingMagicImage(payload)
|
||||
```
|
||||
|
||||
后端接口:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-love-drawing/magic
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"originalImageSrc": "data:image/png;base64,...",
|
||||
"strokeTrace": []
|
||||
}
|
||||
```
|
||||
|
||||
响应体:
|
||||
|
||||
```json
|
||||
{
|
||||
"magicImageSrc": "data:image/png;base64,...",
|
||||
"generationProvider": "vector-engine-gpt-image-2",
|
||||
"prompt": "..."
|
||||
}
|
||||
```
|
||||
|
||||
后端使用 VectorEngine `gpt-image-2-all`,把原始画布图作为参考图,生成绘本风格图片。
|
||||
|
||||
本地未配置 VectorEngine 或接口失败时,前端允许提示错误并保留原图保存能力;不得把失败伪装成正式魔法图。
|
||||
|
||||
后端接入约束:
|
||||
|
||||
1. 接口需要 Bearer 鉴权;
|
||||
2. 请求体限制为 8MB;
|
||||
3. `originalImageSrc` 只接受图片 Data URL;
|
||||
4. 笔触数量上限为 600 条;
|
||||
5. 上游参考图字段使用 VectorEngine 统一契约 `image`;
|
||||
6. 关闭入口时,`creation_entry_config` 路由熔断可识别 `baby-love-drawing`。
|
||||
|
||||
## 7. 本地保存
|
||||
|
||||
本地保存使用:
|
||||
|
||||
```text
|
||||
localStorage key = genarrative.edutainmentBabyDrawing.localDrawings.v1
|
||||
```
|
||||
|
||||
保存策略:
|
||||
|
||||
1. 魔法生成前保存:`saveMode = "original-only"`,只保存 `originalImageSrc`;
|
||||
2. 未保存原图直接生成魔法后保存:`saveMode = "original-and-magic"`,保存 `originalImageSrc` 和 `magicImageSrc`;
|
||||
3. 保存后展示“再画一张”和“返回”。
|
||||
|
||||
## 8. 验收命令
|
||||
|
||||
```bash
|
||||
npm run test -- src/components/edutainment-runtime/babyLoveDrawingModel.test.ts src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx src/services/edutainment-baby-drawing/babyDrawingClient.test.ts src/routing/appRoutes.test.ts
|
||||
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
|
||||
npx eslint src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx src/components/edutainment-runtime/babyLoveDrawingModel.ts src/services/edutainment-baby-drawing/babyDrawingClient.ts src/routing/appRoutes.tsx --ext .ts,.tsx --max-warnings 0
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
## 9. 已覆盖测试
|
||||
|
||||
1. `src/components/edutainment-runtime/babyLoveDrawingModel.test.ts`:颜色 / 按钮悬停阈值、坐标归一化、笔触追加;
|
||||
2. `src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx`:画板、七色、画笔 / 橡皮、完成保存、返回按钮、左手位置指示器、mocap 摄像头视角到用户身体视角换算、左手输入不替换画笔光标位置、左手短暂缺帧不闪烁、用户左手不能抢占右手画笔、camera-left 大跳不接入画笔;
|
||||
3. `src/services/edutainment-baby-drawing/babyDrawingClient.test.ts`:原图保存、原图加魔法图保存、后端魔法接口请求;
|
||||
4. `src/routing/appRoutes.test.ts`:`/runtime/baby-love-drawing` 开启可达、关闭回落主应用;
|
||||
5. `server-rs/crates/api-server/src/edutainment_baby_drawing.rs` 内部单测:prompt、Data URL 校验、PNG 输出和轨迹范围摘要;
|
||||
6. `server-rs/crates/api-server/src/creation_entry_config.rs` 路由映射单测:确认后端熔断可识别 `baby-love-drawing`。
|
||||
@@ -30,13 +30,15 @@ baby-object-match
|
||||
宝贝识物
|
||||
```
|
||||
|
||||
入口文件:
|
||||
工程接入文件:
|
||||
|
||||
1. `src/config/newWorkEntryConfig.ts`
|
||||
1. `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
|
||||
2. `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
3. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
|
||||
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
`src/config/newWorkEntryConfig.ts` 已迁移删除,不再作为入口事实源。`baby-object-match` 必须存在于 SpacetimeDB `creation_entry_type_config` 默认种子中,默认展示名为 `宝贝识物`、`visible=true`、`open=true`、`sortOrder=90`;前端只通过 `GET /api/creation-entry/config` 读取后端配置并在 `platformEntryCreationTypes.ts` 做展示派生。
|
||||
|
||||
`baby-object-match` 必须复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时,创作类型弹层不展示 `宝贝识物`,创作页作品架不展示本地宝贝识物草稿或已发布作品卡,公开发现、搜索、详情、作品号和浏览历史也继续完全不可见。
|
||||
|
||||
新增阶段:
|
||||
@@ -62,7 +64,8 @@ packages/shared/src/contracts/edutainmentBabyObject.ts
|
||||
2. `BabyObjectMatchDraft.templateName = "宝贝识物"`;
|
||||
3. `BabyObjectMatchDraft.themeTags` 必须包含精确 `寓教于乐`;
|
||||
4. `BabyObjectMatchItemAsset.generationProvider` 首版允许为 `vector-engine-gpt-image-2` 或 `placeholder`;
|
||||
5. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`。
|
||||
5. `BabyObjectMatchDraft.visualPackage` 可选承载背景环境、UI 装饰框、礼物盒、篮子和烟雾弹出特效五类视觉资源;
|
||||
6. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`。
|
||||
|
||||
## 4. Service 边界
|
||||
|
||||
@@ -76,9 +79,64 @@ src/services/edutainment-baby-object/babyObjectMatchClient.ts
|
||||
|
||||
1. `createBabyObjectMatchDraft(payload)`;
|
||||
2. `saveBabyObjectMatchDraft(draft)`;
|
||||
3. `publishBabyObjectMatchWork(payload)`。
|
||||
3. `publishBabyObjectMatchWork(payload)`;
|
||||
4. `deleteLocalBabyObjectMatchDraft(profileId)`;
|
||||
5. `regenerateBabyObjectMatchDraftAssets(draft)`;
|
||||
6. `hasBabyObjectMatchPlaceholderAssets(draft)`。
|
||||
|
||||
当前后端正式接口未在本线程扩表落地,因此 service 先走本地 Demo 存储,并把 asset 结果标记为 `placeholder`。后续后端接入时,应替换为:
|
||||
当前后端正式作品持久化接口未在本线程扩表落地,因此 service 仍使用本地 Demo 存储草稿和发布状态。由于 image-2 会返回多张 base64 PNG 大图,本地 Demo 草稿必须优先写入 IndexedDB `genarrative-edutainment-baby-object-drafts/drafts`,不得把完整草稿 JSON 写入 `localStorage`;`localStorage` 仅作为旧版小草稿迁移读取来源,读取后迁移到 IndexedDB 并清理旧 key,避免触发浏览器 `Storage` 配额错误。
|
||||
|
||||
物品图片生成已接入后端 image-2 接口:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-object-match/assets
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"itemNames": ["苹果", "香蕉"]
|
||||
}
|
||||
```
|
||||
|
||||
响应体:
|
||||
|
||||
```json
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"itemId": "baby-object-item-1",
|
||||
"itemName": "苹果",
|
||||
"imageSrc": "data:image/png;base64,...",
|
||||
"assetObjectId": null,
|
||||
"generationProvider": "vector-engine-gpt-image-2",
|
||||
"prompt": "..."
|
||||
}
|
||||
],
|
||||
"visualPackage": {
|
||||
"themePrompt": "...",
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "baby-object-visual-background",
|
||||
"assetKind": "background",
|
||||
"imageSrc": "data:image/png;base64,...",
|
||||
"assetObjectId": null,
|
||||
"generationProvider": "vector-engine-gpt-image-2",
|
||||
"prompt": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该接口返回物品透明 PNG data URL,以及同一次创作生成的视觉主题包。本地 Demo 阶段暂不写入 OSS 或 SpacetimeDB `asset_object`。当前创作链路必须真实拿到 `generationProvider = "vector-engine-gpt-image-2"` 的物品图和视觉主题包后才允许进入结果页;若本地未配置 VectorEngine、登录态失效、接口返回 401/5xx、上游生成失败或响应缺少任一资源,前端 service 必须抛出错误并停留在生成失败状态,不得静默回退到占位图。
|
||||
|
||||
由于一次创作会生成 2 张物品图和 `background`、`ui-frame`、`gift-box`、`basket`、`smoke-puff` 5 张视觉包装图,该请求属于长耗时 image-2 链路。前端 `babyObjectMatchClient` 对该 POST 使用 10 分钟请求超时,且不做自动重试,避免第一次生成仍在后端执行时又发起第二次重复生成。后端同时启动物品图与视觉主题包生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,避免某张图 3 分钟附近仍在生成时被后端提前断开。后端日志记录每类资源的开始、完成和耗时,排查时优先按同一次 HTTP 请求查看 `宝贝识物 image-2 物品资源生成完成`、`宝贝识物 image-2 视觉资源生成完成` 与 `VectorEngine 图片生成上游错误`。
|
||||
|
||||
历史本地草稿中若已保存 `generationProvider = "placeholder"` 的旧占位资源,结果页必须提示“重新生成 image-2 资源”,并禁用试玩和发布。用户点击重新生成、发布或试玩前,前端统一调用 `regenerateBabyObjectMatchDraftAssets(draft)` 补齐资源;补齐失败时保留在结果页并展示错误。
|
||||
|
||||
后续正式作品持久化接入时,应补齐:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-object-match/drafts
|
||||
@@ -88,6 +146,25 @@ POST /api/creation/edutainment/baby-object-match/drafts/{draftId}/publish
|
||||
|
||||
图片生成必须在后端调用 VectorEngine `gpt-image-2-all`,不得从前端直接调用外部图片接口。
|
||||
|
||||
后端 image-2 prompt 约束:
|
||||
|
||||
1. 锁定寓教于乐板块统一的卡通绘本草地舞台插画风;
|
||||
2. 每张图只能围绕对应关键词生成一个单一物品;
|
||||
3. 不生成背景、场景、氛围渲染、人物、手、篮子、礼物盒、文字、水印或 UI;
|
||||
4. 优先要求纯白或透明抠图友好的干净背景,服务端再统一转透明 PNG 并执行背景 alpha 清理;
|
||||
5. 返回 `generationProvider = "vector-engine-gpt-image-2"` 的素材必须已经完成透明抠图。
|
||||
|
||||
后端视觉主题包 prompt 约束:
|
||||
|
||||
1. 同一次请求根据两个物品关键词生成 `background`、`ui-frame`、`gift-box`、`basket`、`smoke-puff` 五类资源;
|
||||
2. 总风格继续锁定寓教于乐明亮卡通绘本插画风;
|
||||
3. 若关键词偏动漫角色、玩具或公仔,背景环境和 UI 元素匹配动漫、玩具主题;若关键词偏水果,匹配果园、自然主题;其它关键词按语义匹配合适主题;
|
||||
4. 背景环境图使用非透明 16:9 图,但必须保证中间、中下方和底部左右篮子区域清爽,给放大后的礼物盒、中央物品和左右篮子预留空间,不画入礼物盒、篮子、物品、人物、文字或操作 UI;
|
||||
5. UI 装饰框、礼物盒、篮子和烟雾弹出特效使用透明 PNG 后处理,不生成文字、数字、按钮、人物或待分类物品;
|
||||
6. `gift-box` 提示词必须面向运行态约 2 倍视觉尺寸生成主体饱满的大号礼物盒,`basket` 提示词必须面向运行态约 1.5 倍视觉尺寸生成可读性高的大号篮子;
|
||||
7. `smoke-puff` 只生成礼物盒打开瞬间使用的柔和烟雾云朵特效,不生成礼物盒、篮子、物品或文字;
|
||||
8. 左右篮子的固定选项规则不受主题包影响,运行态只把 `basket` 作为篮子造型包装复用。
|
||||
|
||||
## 5. UI 边界
|
||||
|
||||
工作台只展示两个必填输入和生成按钮。
|
||||
@@ -107,27 +184,37 @@ src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
|
||||
运行态直接消费 `BabyObjectMatchDraft`,必须使用草稿中的两个物品名称和物品图。
|
||||
每轮只随机当前从礼物盒跳出的物品;左右篮子不随机交换,左侧固定为草稿 `itemAssets[0]`,右侧固定为草稿 `itemAssets[1]`。
|
||||
|
||||
若草稿包含 `visualPackage`,运行态通过背景图片层、CSS 变量和图片节点消费:
|
||||
|
||||
1. `background`:作为舞台最底层 `ResolvedAssetImage` 背景图;存在该资源时必须关闭默认草地兜底层,避免生成场景被 CSS 草地遮住或弱化;
|
||||
2. `ui-frame`:作为字幕条和计数器装饰背景;
|
||||
3. `gift-box`:替换 CSS 礼物盒主体,按旧视觉约 2 倍尺寸展示,只在礼盒入场和打开阶段存在;
|
||||
4. `basket`:替换篮子主体造型,按旧视觉约 1.5 倍尺寸展示,左右两侧复用同一张主题篮子图;
|
||||
5. `smoke-puff`:作为礼物盒打开和中央物品弹出期间的透明烟雾特效资源。
|
||||
|
||||
旧草稿或接口失败时 `visualPackage = null`,运行态继续使用现有 CSS 绘本风兜底。
|
||||
|
||||
首关状态机:
|
||||
|
||||
1. `waiting`:礼物盒关闭,等待任意手抬起;
|
||||
2. `active`:当前物品停留在屏幕中央;
|
||||
3. `correct`:展示“真棒”反馈,成功次数加 1;
|
||||
4. `wrong`:展示“再想一想吧”反馈,当前物品回到中央;
|
||||
5. `complete`:成功次数达到 20,展示“恭喜你!小朋友!”和按钮。
|
||||
1. `gift-entering`:礼物盒从上方落下入场动画阶段,不接受动作判定;
|
||||
2. `gift-opening`:礼物盒打开并播放烟雾特效阶段,不接受动作判定;
|
||||
3. `item-appearing`:礼物盒从舞台移除,当前物品从烟雾中出现并停稳,不接受动作判定;
|
||||
4. `active`:物品彻底出现后才开放选篮判定;
|
||||
5. `correct`:展示“真棒”反馈,对应篮筐播放正确特效并停顿,成功次数加 1;特效完全结束后重新进入 `gift-entering`,下一轮礼物盒从上方落下;
|
||||
6. `wrong`:展示“再想一想吧”反馈,物品弹回中央;反馈结束后回到 `active`,不重新随机物品;
|
||||
7. `complete`:成功次数达到 20,展示“恭喜你!小朋友!”和按钮。
|
||||
|
||||
动作输入:
|
||||
|
||||
1. 任意手完成一次 `open_palm -> grab` 抓握序列:打开礼物盒并生成当前物品;
|
||||
2. 左手连续横向移动达到阈值:将当前物品送入左侧篮子;
|
||||
3. 右手连续横向移动达到阈值:将当前物品送入右侧篮子。
|
||||
1. 左手连续横向移动达到阈值:将当前物品送入左侧篮子;
|
||||
2. 右手连续横向移动达到阈值:将当前物品送入右侧篮子。
|
||||
|
||||
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。选篮只使用明确 `leftHand` 或 `rightHand` 的连续横向轨迹阈值,不再通过 `wave_left_hand`、`wave_right_hand`、`wave` 等动作名触发;侧别为 `unknown` 的手部轨迹也不参与选篮,以避免多套判定误命中和连续误触发。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:`rightHand` 轨迹映射玩家左手并进入左侧篮子,`leftHand` 轨迹映射玩家右手并进入右侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
|
||||
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。选篮只使用明确 `leftHand` 或 `rightHand` 的连续横向轨迹阈值,不再通过 `wave_left_hand`、`wave_right_hand`、`wave` 等动作名触发;侧别为 `unknown` 的手部轨迹也不参与选篮,以避免多套判定误命中和连续误触发。动作判定只在 `active` 阶段开放,礼盒入场、礼盒打开、物品出现、正确反馈和错误反馈阶段收到的动作包必须清空轨迹并忽略,不允许跨阶段补判定。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:`rightHand` 轨迹映射玩家左手并进入左侧篮子,`leftHand` 轨迹映射玩家右手并进入右侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
|
||||
|
||||
开发者调试输入:
|
||||
|
||||
1. `F`:映射任意手抬起,打开礼物盒并生成当前物品;
|
||||
2. 鼠标左键按下并拖动:映射左手轨迹,抬起后将当前物品送入左侧篮子;
|
||||
3. 鼠标右键按下并拖动:映射右手轨迹,抬起后将当前物品送入右侧篮子。
|
||||
1. 鼠标左键按下并拖动:映射左手轨迹,抬起后将当前物品送入左侧篮子;
|
||||
2. 鼠标右键按下并拖动:映射右手轨迹,抬起后将当前物品送入右侧篮子。
|
||||
|
||||
运行态不得新增计时、失败次数、分数、体力或难度递增规则。
|
||||
|
||||
@@ -154,6 +241,7 @@ src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
|
||||
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/services/edutainment-baby-object/babyObjectMatchClient.test.ts
|
||||
cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml
|
||||
npx vitest run src/components/platform-entry/platformEdutainmentVisibility.test.ts src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/custom-world-home/creationWorkShelf.test.ts src/services/useMocapInput.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts
|
||||
npx eslint src/components/platform-entry/platformEntryCreationTypes.ts src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --ext .ts,.tsx --max-warnings 0
|
||||
npm run check:encoding
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
`bark-battle` / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效叫声次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。
|
||||
`bark-battle` / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效声浪触发次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。
|
||||
|
||||
现有前端方案 `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md` 已覆盖 Phaser / TypeScript / Vite / Web Audio / DOM HUD 的 runtime 落地方式,并明确不覆盖后端表结构、成绩持久化、作品发布、广场接入与实时多人协议。因此需要单独补充后端 DDD 技术方案,避免前端 runtime 在接入平台作品、正式游玩埋点、成绩、排行榜和发布闭环时承接不属于表现层的业务真相。
|
||||
|
||||
@@ -44,26 +44,80 @@ MVP 明确不做:
|
||||
|
||||
## 2. 玩法接入级别建议
|
||||
|
||||
### 2.1 推荐首版闭环
|
||||
### 2.1 第二阶段范围:平台作品闭环
|
||||
|
||||
建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 / 可选排行榜”的闭环:
|
||||
第二阶段已明确为“Bark Battle 平台作品闭环”,不是单纯玩法表现深化。目标是让 bark-battle 成为 Genarrative 的正式 play type,并完成从轻创作配置、发布、正式 runtime、run start / finish、单局结果持久化、个人历史成绩、作品统计到最小排行榜的闭环。
|
||||
|
||||
1. 创作者创建 bark-battle 草稿,配置标题、描述、狗狗主题、背景、难度、单局时长、音量阈值、AI 对手参数和排行榜开关。
|
||||
Phase 2 的作品配置边界是“轻创作配置作品”:创作者可以配置标题、描述、主题/背景预设、狗狗皮肤预设、难度预设和排行榜开关;不得直接配置单局时长、有效声浪阈值、`minBarkGapMs`、AI 对手细粒度参数、分数公式或反作弊阈值。难度预设只影响 AI 对手行为强度,不影响有效阈值、声浪冷却、单局时长或分数公式;排行榜必须按 `workId + difficultyPreset + rulesetVersion` 分榜,避免不同难度和不同规则版本混排。
|
||||
|
||||
建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 + 个人历史成绩 / 作品统计 / 最小排行榜”的闭环:
|
||||
|
||||
1. 创作者从玩法选择进入 bark-battle 后创建草稿,通过单页轻配置表单 + 预览卡片配置标题、描述、主题/背景预设、狗狗皮肤预设、难度预设和排行榜开关。
|
||||
2. 发布为稳定作品 ID,`playTypeId = "bark-battle"`。
|
||||
3. 玩家从作品页或广场进入 runtime,前端获取发布态 runtime config。
|
||||
3. 玩家可从作品详情页 CTA、广场/作品卡片、我的作品/个人作品架进入正式 runtime,前端使用稳定作品 ID 获取发布态 runtime config。
|
||||
4. 玩家授权麦克风后在本地完成 30 秒声控对战。
|
||||
5. 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效叫声次数、节奏命中、最终能量、客户端结果摘要等。
|
||||
6. 后端校验 work、config version、run token、时长、分数范围和权限后,生成服务端认可的 run result / score summary。
|
||||
7. 若作品开启排行榜,则写入可投影的 leaderboard 记录。
|
||||
8. 正式作品级游玩埋点统一写 `work_play_start`,其中 `scope_kind=work`,`scope_id=稳定作品 ID`,metadata 包含 `playType`、`workId`、`sourceRoute`、`userId`。
|
||||
5. 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效声浪触发次数、节奏命中、最终能量、客户端结果摘要等。
|
||||
6. 后端校验 work、config version、ruleset version、difficulty preset、run token、时长、派生指标范围和权限后,生成服务端裁决的 run result / score summary。
|
||||
7. 写入个人历史成绩与最小作品统计投影。
|
||||
8. 若作品开启排行榜且后端裁决 `serverResult = player_win`,则写入可投影的 leaderboard 记录;排行榜首版只做最小排序与展示,不引入赛季、段位或复杂反作弊,并按 `workId + difficultyPreset + rulesetVersion` 分榜。
|
||||
9. 正式作品级游玩埋点统一写 `work_play_start`,其中 `scope_kind=work`,`scope_id=稳定作品 ID`,metadata 包含 `playType`、`workId`、`sourceRoute`、`userId`。
|
||||
|
||||
|
||||
### 2.2.1 难度预设与排行榜分榜
|
||||
|
||||
Phase 2 只允许三个难度预设:`easy`、`normal`、`hard`。难度预设只能影响 AI 对手推动力曲线和 AI 声浪节奏;不得影响单局时长、有效声浪阈值、`minBarkGapMs`、分数公式或反作弊阈值。排行榜记录和查询必须带上 `difficultyPreset` 与 `rulesetVersion`,以 `workId + difficultyPreset + rulesetVersion` 作为分榜维度。
|
||||
|
||||
### 2.2.2 单局结果后端裁决
|
||||
|
||||
Phase 2 不信任前端提交的最终胜负和正式分数。前端 `finish` 只提交不可还原原始音频的派生指标:`runId`、`workId`、`configVersion`、`rulesetVersion`、`difficultyPreset`、`clientStartedAt`、`clientFinishedAt`、`durationMs`、`triggerCount`、`maxVolume`、`averageVolume`、`finalEnergy`、`comboMax`、`clientResult`,以及可选的 `sampleDigest`。其中 `clientResult` 只用于 debug/对账,不进入正式结果或排行榜。
|
||||
|
||||
后端必须校验 run 由 start 创建且未 finish、run token 匹配、work/config/ruleset/difficulty 与 start 时一致、duration 处于合理窗口、triggerCount 不超过 `durationMs / minBarkGapMs + tolerance`、音量/能量/连击字段在合法范围内。后端生成 `serverResult`、`scoreSummary`、`leaderboardScore` 和 `antiCheatFlags`,排行榜只使用后端裁决后的胜利局成绩。
|
||||
|
||||
### 2.2.3 排行榜排序口径
|
||||
|
||||
Phase 2 排行榜只收录 `serverResult = player_win` 且未被反作弊规则拒绝的单局结果;平局和失败仍进入个人历史成绩与作品统计,但不进入排行榜。`leaderboardScore` 由后端规则集生成,排序优先级为:`finalEnergy` 降序、`triggerCount` 降序、`maxVolume` 降序、`durationMs` 越接近标准局时长越优、`finishedAt` 越早越优。
|
||||
|
||||
### 2.2.4 作品统计投影口径
|
||||
|
||||
Phase 2 的作品统计是最小后端投影,不从排行榜反推。`playStartCount` 在 start run 成功时计入一次,并对齐 `work_play_start` 埋点;`finishCount` 在 finish 被后端接受时计入一次,包含胜利、平局和失败。`accepted_with_flags` 可以计入 `finishCount`,但必须同时计入 `flaggedCount`;未 start 成功、run token 不合法、重复 finish、被后端 rejected 的结果不计入 `finishCount`。
|
||||
|
||||
作品统计字段首版包含:`playStartCount`、`finishCount`、`winCount`、`drawCount`、`lossCount`、`flaggedCount`、`leaderboardEntryCount`、`bestLeaderboardScore`、`bestFinalEnergy`、`averageFinalEnergy`、`updatedAt`。Phase 2 不做 DAU/留存、按小时曲线、原始音频分析或每玩家每天聚合统计。
|
||||
|
||||
### 2.2.5 个人历史成绩口径
|
||||
|
||||
Phase 2 的个人历史成绩由“最近记录列表 + 个人最佳摘要”组成,并且只允许本人查询。后端可以保存每次被接受的 finish 记录,但首版查询接口只暴露默认最近 20 条记录,可按 `workId` 和 `difficultyPreset` 过滤;最近记录包含胜利、平局、失败和是否 flagged,但不展示详细反作弊原因。
|
||||
|
||||
个人最佳摘要按 `userId + workId + difficultyPreset + rulesetVersion` 聚合,字段包含 `bestLeaderboardScore`、`bestFinalEnergy`、`bestTriggerCount`、`bestMaxVolume`、`winCount`、`finishCount`、`lastPlayedAt`。失败、平局和 flagged 历史不对其他玩家公开;排行榜只展示公开入榜的胜利成绩。Phase 2 不做无限滚动完整历史、每日/每周曲线、好友对比或普通玩家可见的详细反作弊说明。
|
||||
|
||||
### 2.2.6 正式作品入口闭环
|
||||
|
||||
Phase 2 必须接入 Bark Battle 正式作品入口闭环,但不新增独立专区、活动页、挑战分享页、好友邀请或多人房间入口。入口范围包括:创作入口/玩法选择中出现 `bark-battle`,进入单页轻配置表单 + 预览卡片;作品详情页 CTA 点击“开始游玩”进入正式 runtime;广场/作品卡片可以展示、打开详情并开始游玩;我的作品/个人作品架能看到作者发布的 Bark Battle 作品;runtime 路由使用稳定作品 ID 并从后端发布态 config 拉取配置。
|
||||
|
||||
正式 run start 成功后必须写 `work_play_start`,其中 `scope_kind=work`、`scope_id=稳定作品 ID`,metadata 至少包含 `playType=bark-battle`、`workId`、`sourceRoute`、`userId`。内部试玩入口可以作为开发调试保留,但不得作为 Phase 2 正式入口。
|
||||
|
||||
### 2.2.7 轻配置编辑流程
|
||||
|
||||
Phase 2 的创作编辑形态是“单页轻配置表单 + 预览卡片”,不是多步骤向导、拖拽编辑器或完整规则编辑器。表单字段包含:标题(必填)、简介(选填)、主题/背景预设(必填枚举)、狗狗皮肤预设(必填枚举)、难度预设(必填,默认 `normal`)、排行榜开关(默认开启)。
|
||||
|
||||
交互流程:创作者从玩法选择进入后生成草稿;在同一页编辑轻配置并查看预览卡片;支持保存草稿和发布;发布成功后跳转作品详情;可从我的作品再次编辑草稿或基于已发布作品创建新版本。Phase 2 不做 AI 生成配置、多步骤 wizard、规则参数编辑、复杂封面编辑、runtime 内嵌预览或大段玩法说明文案。
|
||||
|
||||
### 2.2 后续增强路径
|
||||
|
||||
后续再考虑多人实时:
|
||||
第二阶段之后再考虑:
|
||||
|
||||
- Phase 2:排行榜、挑战分享、个人历史成绩、作品统计面板。
|
||||
- Phase 2.1:挑战分享、作品统计面板细化、排行榜体验优化。
|
||||
- Phase 3:异步影子对手 / ghost replay,但仍不保存原始音频,只保存低维派生曲线或聚合指标。
|
||||
- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入 MVP。
|
||||
- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入第二阶段平台作品闭环。
|
||||
|
||||
## 2.3 Phase 2 技术实施顺序
|
||||
|
||||
Phase 2 按“契约和领域规则先行,然后最小纵切,再扩展投影”的顺序实施,避免前端 mock 堆积、后端孤岛或排行榜 UI 先行。
|
||||
|
||||
1. 契约与领域规则:补 `shared-contracts` DTO、`module-bark-battle` 纯领域规则、`rulesetVersion` / `difficultyPreset` / score adjudication,并先写单测。
|
||||
2. SpacetimeDB 表与 reducer + api-server BFF:落草稿/config/发布态 config、runtime run start / finish、score record、leaderboard entry、work stats projection、personal summary projection、`migration.rs` 与绑定生成。
|
||||
3. 最小前端纵切:接创作入口、单页轻配置表单、发布到稳定 workId、作品详情 CTA、runtime 拉 config、start / finish 串通、结算展示 `serverResult`。
|
||||
4. 投影与列表体验:接排行榜、个人历史最近记录 + 最佳摘要、作品统计、我的作品/广场卡片适配。
|
||||
5. 收口验证:把 BDD 场景落到测试,执行编码检查、后端 `/healthz` + API smoke、前端人工验收路径,并更新 README/文档。
|
||||
|
||||
## 3. DDD 分层设计
|
||||
|
||||
@@ -90,7 +144,7 @@ frontend/runtime
|
||||
- 定义配置版本兼容规则。
|
||||
- 计算提交结果的派生分数区间与胜负判定是否自洽。
|
||||
- 计算 `ScoreSummary`、排行榜排序分数、统计指标。
|
||||
- 定义反作弊基础规则:时长范围、有效叫声次数上限、峰值范围、能量范围、提交窗口、run 状态机。
|
||||
- 定义反作弊基础规则:时长范围、有效声浪触发次数上限、峰值范围、能量范围、提交窗口、run 状态机。
|
||||
|
||||
不职责:
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@
|
||||
3. 位置类状态必须满足“到达绿色圆环并保持 2 秒”。
|
||||
4. 动作类状态没有最长等待时间。
|
||||
5. 动作类状态等待 3 秒后可以播放对应引导动画。
|
||||
6. 每个步骤进入时需要先展示本步骤文字字幕和语音播报入口,约 1 秒后再进入可交互阶段并展示绿色圆环、手势引导等检测提示。
|
||||
7. 步骤完成后需要先进入完成停顿阶段,当前停顿约 0.8 秒;停顿期间保留完成反馈位置,后续可在该阶段补充完成特效或音效,再切换到下一步骤。
|
||||
8. 入场等待和完成停顿阶段不消费动作完成判定,避免用户上一步残留动作直接触发下一步。
|
||||
|
||||
### 6.3 开发者调试输入
|
||||
|
||||
@@ -368,6 +371,17 @@
|
||||
|
||||
用户完成挥动左手。
|
||||
|
||||
当前本地 mocap 的 handedness 按摄像头视角输出,热身关内需要先换算成用户身体视角再判断:摄像头右侧手对应用户左手。挥动左手不是普通横向轨迹检测,而是用于确认现实环境中用户左侧手臂打开空间足够和安全。
|
||||
|
||||
完成条件必须同时满足:
|
||||
|
||||
1. 使用用户身体左手轨迹。
|
||||
2. 手腕在左肩外侧达到最小外展距离。
|
||||
3. 手腕不能处于自然下垂低位。
|
||||
4. 最近连续有效帧中,手臂存在足够上下摆动幅度。
|
||||
5. 最近连续有效帧中,肩膀到手腕向量的角度变化达到阈值。
|
||||
6. 至少出现一次上下摆动方向变化。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
@@ -376,7 +390,7 @@
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录用户挥动左手的空间,保存为该用户对应的行为坐标。
|
||||
记录用户挥动左手的轨迹、空间包络、角度范围和最大外展距离,保存为该用户对应的行为坐标。
|
||||
|
||||
---
|
||||
|
||||
@@ -398,6 +412,17 @@
|
||||
|
||||
用户完成挥动右手。
|
||||
|
||||
当前本地 mocap 的 handedness 按摄像头视角输出,热身关内需要先换算成用户身体视角再判断:摄像头左侧手对应用户右手。挥动右手不是普通横向轨迹检测,而是用于确认现实环境中用户右侧手臂打开空间足够和安全。
|
||||
|
||||
完成条件必须同时满足:
|
||||
|
||||
1. 使用用户身体右手轨迹。
|
||||
2. 手腕在右肩外侧达到最小外展距离。
|
||||
3. 手腕不能处于自然下垂低位。
|
||||
4. 最近连续有效帧中,手臂存在足够上下摆动幅度。
|
||||
5. 最近连续有效帧中,肩膀到手腕向量的角度变化达到阈值。
|
||||
6. 至少出现一次上下摆动方向变化。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
@@ -406,7 +431,7 @@
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录用户挥动右手的空间,保存为该用户对应的行为坐标。
|
||||
记录用户挥动右手的轨迹、空间包络、角度范围和最大外展距离,保存为该用户对应的行为坐标。
|
||||
|
||||
---
|
||||
|
||||
@@ -653,18 +678,21 @@
|
||||
3. 鼠标左键按下并拖动映射左手轨迹。
|
||||
4. 鼠标右键按下并拖动映射右手轨迹。
|
||||
5. 空格键映射原地跳跃。
|
||||
6. 调试输入只在步骤可交互阶段触发步骤完成;步骤入场字幕阶段和完成停顿阶段会忽略完成判定,便于观察节奏和后续补充特效。
|
||||
|
||||
当前硬件和动作检测接口接入:
|
||||
|
||||
1. 浏览器摄像头视频流已接入舞台背景。
|
||||
2. 热身关全流程已通过 `src/services/useMocapInput.ts` 接入本地 mocap WebSocket `/stream`;动作数据源状态优先于浏览器背景摄像头状态展示。
|
||||
3. mocap 包支持从 `general.body.center_norm` 读取身体中心,位置类步骤使用该身体中心更新角色剪影横向位置并完成圆环保持检测。
|
||||
4. mocap 包支持从 `actions/action/gesture/gestures/event/name/type` 读取动作名,并支持 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 读取左右手坐标。
|
||||
5. `hands[].landmarks` 存在时优先用手腕和 MCP 点计算掌心中心;掌心点不足时退回 wrist landmark,再退回 hand 直出坐标。
|
||||
6. `wave_greeting` 可由 `wave/wave_greeting/hand_wave/open_palm` 等动作或 open palm 手势完成。
|
||||
7. `wave_left_hand` 和 `wave_right_hand` 优先消费对应左右手动作名;当硬件只持续输出手部坐标时,也可以根据连续手部横向轨迹完成挥手检测。
|
||||
8. `jump_once` 消费 `jump/jump_once/hop` 等跳跃动作事件完成。
|
||||
9. 键盘 `A/D/Space` 与鼠标左右键拖拽仍保留为本地 Demo 调试兜底,不代表正式硬件口径。
|
||||
4. 身体中心横向坐标进入角色剪影前必须做输入稳定化处理:先 clamp 到 `0..1`,再使用小幅死区、低通阻尼和单包最大步长限制,避免硬件噪声造成角色左右误判、画面抽搐或视觉上的忽大忽小。当前实现参数为死区 `0.012`、阻尼系数 `0.28`、单包最大步长 `0.035`;位置保持检测使用稳定化后的角色坐标。
|
||||
5. 角色剪影渲染需要把水平位移和跳跃表现拆开:外层只负责横向定位,内层资源只负责轮廓图和跳跃位移,避免 `left` 与 `transform` 同时抢占导致半透明资源重采样抖动。
|
||||
6. mocap 包支持从 `actions/action/gesture/gestures/event/name/type` 读取动作名,并支持 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 读取左右手坐标。
|
||||
7. `hands[].landmarks` 存在时优先用手腕和 MCP 点计算掌心中心;掌心点不足时退回 wrist landmark,再退回 hand 直出坐标。
|
||||
8. `wave_greeting` 只消费左手、右手或未知单手的连续横向挥手轨迹,不再使用 `wave`、`hand_wave`、`open_palm`、张手状态或动作名直接完成判定;进入轨迹判定前必须先满足抬手有效区:优先使用 `hands[].landmarks.wrist` 与 `general.limb_nodes` 的同侧 `*_elbow` / `*_shoulder` 判断,当前阈值为 `wrist.y <= elbow.y + 0.04`,缺少肘部时使用 `wrist.y <= shoulder.y + 0.08`;缺少同侧肘部和肩膀参考时不允许招呼通过,不再使用身体中心兜底判断抬手。轨迹阈值为至少 5 个连续抬手点,横向 `x` 范围差值不小于 `0.075`,且至少出现 1 次横向方向变化,避免“手刚露出画面”或“手自然下垂抖动”被误判为招手。
|
||||
9. `wave_left_hand` 和 `wave_right_hand` 只消费用户身体侧对应手的连续坐标轨迹,不再使用动作名、张手状态或 primary hand 兜底完成判定;本地 mocap handedness 当前按摄像头视角输出,因此用户左手使用 camera-right,用户右手使用 camera-left。完成判定必须同时满足对应肩肘腕外展、手腕非自然下垂、连续有效帧、横向范围、上下摆动范围、肩腕角度范围和上下方向变化,当前阈值为连续外展点不少于 5 个、横向 `x` 范围不小于 `0.055`、垂直 `y` 范围不小于 `0.08`、肩腕角度范围不小于 `28°`、外展距离不小于 `0.12`、手腕相对肩膀外侧距离不小于 `0.1`;后续以真实体验结果继续调参。
|
||||
10. `jump_once` 消费 `jump/jump_once/hop` 等跳跃动作事件完成。
|
||||
11. 键盘 `A/D/Space` 与鼠标左右键拖拽仍保留为本地 Demo 调试兜底,不代表正式硬件口径。
|
||||
|
||||
当前未接入但已保留边界:
|
||||
|
||||
@@ -682,14 +710,17 @@
|
||||
5. 当前已生成并接入以下正式 Demo 资源:
|
||||
- `public/child-motion-demo/picture-book-grass-stage.png`:默认草地舞台背景。
|
||||
- `public/child-motion-demo/picture-book-foreground-grass-v2.png`:底部前景草坪条,只覆盖舞台下沿,不作为整块地板拉伸。
|
||||
- `public/child-motion-demo/picture-book-ground-ring-v2.png`:已按透视绘制的地面椭圆指示环,CSS 只等比缩放。
|
||||
- `public/child-motion-demo/picture-book-ground-ring-v3.png`:已按透视绘制的浅蓝与暖黄色地面椭圆指示环,和草地材质做明显区分,CSS 只等比缩放。
|
||||
- `public/child-motion-demo/picture-book-character-outline-v2.png`:半透明用户角色轮廓,使用独立去背后处理避免内部填充被误删。
|
||||
- `public/child-motion-demo/picture-book-hud-strip-v2.png`:顶部 HUD 细长软纸条。
|
||||
- `public/child-motion-demo/picture-book-calibration-strip-v2.png`:右下角五格热身状态条。
|
||||
- `public/child-motion-demo/picture-book-start-panel-v2.png`:开始按钮背后的轻盈托盘。
|
||||
- `public/child-motion-demo/picture-book-ui-button-v2.png`:开始按钮绘本风按钮底图。
|
||||
- `public/child-motion-demo/picture-book-wave-cat-body-guide-v6.png`:招手阶段中央猫咪身体底座资源,按可动纸偶结构只包含猫头、短身体和肩部连接点,不再和旧猫头、胸口或猫爪资源叠加。
|
||||
- `public/child-motion-demo/picture-book-wave-cat-arm-guide-v6.png`:招手阶段左右独立手臂资源,也用于左右手阶段单手提示;网页用同一拆件镜像复用,并围绕肩部挂点做挥手摆动动画。
|
||||
6. v2 资源按最终用途拆分,CSS 必须按资源原始比例、`aspect-ratio` 或 `background-size: contain / auto` 等方式等比使用;禁止把方形面板强行拉伸为 HUD、状态条或地板,也禁止把底部草坪扩展成覆盖角色脚下的大色块。
|
||||
7. 若后续补充或重绘资源,应先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt 和输出路径,再使用 `--live --only <asset-id>` 小批量生成;仅调整透明去背、裁切、画布归一或品红边缘时,可用 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用 `tmp/child-motion-demo-assets/` 中的源图,不额外请求 image-2;不得把 `VECTOR_ENGINE_API_KEY`、源图或中间预览图提交到仓库。
|
||||
7. 猫咪招手引导资源使用 `cat-guide` 透明后处理:先由 image-2 生成品红底源图,再通过边缘背景连通区域去背,避免把浅粉、淡橘和暖棕主体误删。源图只保存在 `tmp/child-motion-demo-assets/`,正式页面只引用 `public/child-motion-demo/` 下的最终 PNG。
|
||||
8. 若后续补充或重绘资源,应先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt 和输出路径,再使用 `--live --only <asset-id>` 小批量生成;仅调整透明去背、裁切、画布归一或品红边缘时,可用 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用 `tmp/child-motion-demo-assets/` 中的源图,不额外请求 image-2;不得把 `VECTOR_ENGINE_API_KEY`、源图或中间预览图提交到仓库。
|
||||
|
||||
已执行的定向验证命令:
|
||||
|
||||
|
||||
@@ -47,3 +47,11 @@
|
||||
5. 作品卡片以 `coverImageSrc` 作为整卡背景;若 `coverImageSrc` 为空,允许从同一作品已有的关卡图、背景图或素材图兜底,避免草稿页退回普通面板视觉。
|
||||
6. 卡片不展示最后修改时间,`updatedAt` 只参与排序。
|
||||
7. 现有创作中心交互测试通过。
|
||||
|
||||
## 2026-05-14 封面兜底补充
|
||||
|
||||
1. 货架视图模型仍只保存作品真实 `coverImageSrc` 或同作品真实素材兜底,不把玩法参考图写进数据模型,避免把 UI 兜底误认为作品资产。
|
||||
2. `CustomWorldWorkCard` 按 `CreationWorkShelfKind` 为 `CustomWorldCoverArtwork` 传入本地玩法参考图;`ResolvedAssetImage` 在私有资源换签失败、普通图片 404 或真实封面缺失时使用该参考图作为卡片背景。
|
||||
3. 兜底背景底色跟随百梦浅粉、暖白和珊瑚色调,不能继续使用深黑或暗蓝渐变作为草稿卡默认视觉。
|
||||
4. 拼图作品列表摘要必须下发 `levels`,草稿页优先用关卡 `coverImageSrc`,再用选中候选图或最后一张候选图作为真实作品封面兜底。
|
||||
5. 抓大鹅作品列表摘要必须保留 `generatedBackgroundAsset` 与 `generatedItemAssets` 中的 `imageObjectKey`、`containerImageObjectKey` 和 `imageViews[].imageObjectKey`;前端拿到 object key 后统一交给 `ResolvedAssetImage` 换签,不能因为缺少公开 URL 而退回黑卡。
|
||||
|
||||
@@ -32,6 +32,8 @@ process didn't exit successfully: `server-rs\target\debug\api-server.exe`
|
||||
|
||||
主站和后台 Vite 也追加 `--strictPort`,避免默认漂移到 `3001`、`3103` 等端口后让浏览器继续访问旧页面。
|
||||
|
||||
`--skip-spacetime` 是复用既有 SpacetimeDB 宿主的模式。该模式下脚本不会再把 SpacetimeDB 端口纳入可用性漂移;如果传入 `--spacetime-port 3101`,后端就会连接 `http://127.0.0.1:3101`。这可以避免 `3101` 已有 SpacetimeDB 在线时,端口工具误把它当作冲突并改到空闲的 `3102`,导致 api-server 连接空端口后 `/api/creation-entry/config`、作品架和公开图库接口同时返回 `502`。
|
||||
|
||||
## 排障步骤
|
||||
|
||||
PowerShell 查看默认端口占用:
|
||||
@@ -72,3 +74,4 @@ node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh \
|
||||
1. `bash -n scripts/dev-rust-stack.sh` 通过。
|
||||
2. 默认端口被占用时重新运行完整栈,脚本应在 publish 前失败并打印占用进程。
|
||||
3. 清理占用进程或换端口后,重新启动时不再出现 Vite 端口漂移或 `api-server` `AddrInUse`。
|
||||
4. 复用 SpacetimeDB 时执行 `npm run dev -- --skip-spacetime --skip-publish --spacetime-port 3101`,启动日志里的 `[dev:rust] spacetime:` 应保持为 `http://127.0.0.1:3101`,并且 `GET /api/creation-entry/config` 不应因连接 `3102` 这类空端口而失败。
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 抓大鹅草稿素材生成流水线 2026-05-10
|
||||
|
||||
## 0. 2026-05-14 临时关闭音频生成
|
||||
|
||||
抓大鹅音频生成能力暂时关闭:
|
||||
|
||||
1. `match3d_compile_draft` 不再调用 Suno 生成背景音乐,也不再生成点击音效。
|
||||
2. 结果页 `素材配置` 不再展示 `背景音乐` 子 Tab;物品详情面板不再展示点击音效提示词和生成按钮。
|
||||
3. 批量新增物品只生成 2D 五视角图片,不生成点击音效。
|
||||
4. 通用 `/api/creation/audio/*` 路由对 `match3d_work` / `match3d_item` 暂时返回 `410 Gone`;视觉小说专用音频路由保持可用。
|
||||
5. 历史 `generatedItemAssets[].backgroundMusic` 与 `clickSound` 字段保留,运行态仍可消费旧音频。
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于改造 `生成抓大鹅草稿` 的首版生成链路:点击按钮后先进入独立生成过程页,生成结束后自动进入抓大鹅结果页,并在结果页 `素材配置 > 物品` 预览本次生成的 2D 多视角物品素材。
|
||||
@@ -8,7 +18,7 @@
|
||||
|
||||
## 2. 前端流程
|
||||
|
||||
入口仍复用 `Match3DAgentWorkspace` 表单。点击 `生成抓大鹅草稿` 后:
|
||||
入口仍复用 `Match3DAgentWorkspace` 表单。点击 `生成抓大鹅草稿` 后必须先弹出 `确认消耗泥点` 面板,展示 `消耗 10 泥点`;用户确认后才进入后端生成流程:
|
||||
|
||||
1. 创建 Match3D session。
|
||||
2. 后端先用当前题材和本地兜底元信息创建同一个 Match3D 草稿 profile,草稿 Tab 必须立即能看到这份存档。
|
||||
@@ -20,7 +30,7 @@
|
||||
生成页步骤固定为:
|
||||
|
||||
```text
|
||||
建立草稿存档 -> 生成作品计划 -> 生成背景提示词 -> 分批生成素材图 -> 切割独立图片 -> 上传图片资产 -> 校验素材结构 -> 生成背景音乐 -> 生成UI背景与容器 -> 写入草稿页
|
||||
建立草稿存档 -> 生成作品计划 -> 生成背景提示词 -> 分批生成素材图 -> 切割独立图片 -> 上传图片资产 -> 校验素材结构 -> 生成UI背景与容器 -> 写入草稿页
|
||||
```
|
||||
|
||||
生成页只展示题材和物品数量,不展示玩法规则说明。
|
||||
@@ -29,29 +39,27 @@
|
||||
|
||||
## 3. 后端编排边界
|
||||
|
||||
外部生图、音频生成和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
|
||||
外部生图和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。音频生成当前临时关闭。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
|
||||
|
||||
`match3d_compile_draft` action 的后端顺序为:
|
||||
|
||||
1. 读取 session config。
|
||||
2. 对本次 `match3d_compile_draft` 生成动作按 `sessionId + profileId + action 时间戳` 构造幂等流水并预扣 `10` 泥点。余额不足时不继续创建草稿;后续任一步失败时自动按同额退款。
|
||||
3. 草稿编译先创建可恢复 profile;素材生成数量由入口页难度派生的物品种类决定:轻松 `3` 种、标准 `9` 种、进阶 `15` 种、硬核 `21` 种。
|
||||
4. 先调用 SpacetimeDB compile procedure 写入草稿。首次执行使用新 `profileId`;重试时复用 session draft / work profile 中已有 `profileId`。这一步不能等待 LLM、图片、音频或 OSS 成功后才执行。
|
||||
5. 基于入口页题材设定文本调用文本模型生成作品生成计划。模型固定请求 `gpt-4o`,只返回 JSON,其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`summary` 为 18 到 48 个中文字符的作品描述。生成计划还必须包含 `tags`、`backgroundMusic.title`、`backgroundMusic.style`、`backgroundMusic.prompt`、`backgroundPrompt`,以及 `items[]` 中每个物品的 `name` 与 `soundPrompt`。`backgroundMusic.title` 是背景音乐名称,`backgroundMusic.prompt` 固定为空字符串,用于后续 Suno 纯音乐生成;`backgroundPrompt` 用于生成局内竖屏纯背景图,只描述题材氛围、色彩和环境,不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品。文本模型不可用时保留第 4 步的本地兜底,不阻断草稿。
|
||||
4. 先调用 SpacetimeDB compile procedure 写入草稿。首次执行使用新 `profileId`;重试时复用 session draft / work profile 中已有 `profileId`。这一步不能等待 LLM、图片或 OSS 成功后才执行。
|
||||
5. 基于入口页题材设定文本调用文本模型生成作品生成计划。模型固定请求 `gpt-4o`,只返回 JSON,其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`summary` 为 18 到 48 个中文字符的作品描述。生成计划还必须包含 `tags`、`backgroundPrompt`,以及 `items[]` 中每个物品的 `name` 与 `soundPrompt`。`soundPrompt` 只作为历史字段保留,当前不触发音效生成;`backgroundPrompt` 用于生成局内竖屏纯背景图,只描述题材氛围、色彩和环境,不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品。文本模型不可用时保留第 4 步的本地兜底,不阻断草稿。
|
||||
6. 后端把生成计划中的 `gameName` 和 `summary` 写入 `match3d_work_profile` 作品信息后,自动调用作品标签生成器。标签生成器使用题材、作品名称和作品描述生成 3 到 6 个中文短标签;若调用失败或返回不足,则使用生成计划 tags 和本地兜底标签补齐。结果页手动 `AI生成作品标签` 也使用同一接口,并传入当前作品描述。
|
||||
7. 后端从同一份作品生成计划读取当前难度所需数量的短物品名称和音效提示词;不得再只生成物品名称而丢失后续音效生成上下文。
|
||||
8. 调用 APIMart `nanobanana2` / Gemini 图片模型生成 `1:1`、`1K` 素材图,请求模型固定为 `gemini-3.1-flash-image-preview`,走 `POST {APIMART_BASE_URL}/images/generations`,并携带 `official_fallback = true`。提示词必须合入入口页选择的 `assetStylePrompt`,并强制每格使用统一纯绿色绿幕背景,避免白底或纹理背景进入运行态素材。该调整只作用于抓大鹅物品素材 sheet;封面、9:16 纯背景图和 1:1 容器 UI 图仍继续使用项目现有 VectorEngine `gpt-image-2-all` 链路。
|
||||
7. 后端从同一份作品生成计划读取当前难度所需数量的短物品名称,并兼容保存历史 `soundPrompt` 字段;当前不生成点击音效。
|
||||
8. 调用 VectorEngine Gemini 原生图片接口生成 `1:1` 素材图,请求模型固定为 `gemini-3-pro-image-preview`,走 `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"]`、`generationConfig.imageConfig.aspectRatio = "1:1"`,响应从 `candidates[].content.parts[].inlineData.data` / `inline_data.data` 读取 base64 图片。提示词必须合入入口页选择的 `assetStylePrompt`,并强制每格使用统一纯绿色绿幕背景,避免白底或纹理背景进入运行态素材。该调整只作用于抓大鹅物品素材 sheet;封面和 `9:16` 纯背景图继续使用 VectorEngine `/v1/images/generations` 的 `gpt-image-2-all` JSON 链路,`1:1` 容器 UI 图必须使用 VectorEngine `/v1/images/edits` multipart 图生图链路,不能再把参考图作为 generations 的 `image` 数组弱参考。
|
||||
9. 每个物品固定需要 `5` 个不同视角。单张素材图固定为 `5*5 = 25` 格,因此单张图承载 `5` 个物品。若用户要求或难度派生的物品种类不是 `5` 的倍数,后端必须向上补齐物品名称和对应图片到最近的 `5` 的倍数;例如标准难度需要 `9` 种玩法物品,实际生成 `10` 个物品名称和对应五视角图片。若草稿物品数超过 `5`,后端按每批 `5` 个物品自动分批,多张素材图并行生成。
|
||||
10. 将每张素材图按固定 `5 行 * 5 列` 切割成独立图片,并按物品顺序连续分配 `5` 张视角图。素材图提示词必须要求 `5*5` 严格均匀排布、每格主体完整居中、统一纯绿色绿幕背景、相邻物体主体至少保留 `1/4` 单格宽度空白间距、不得跨格、贴边或越界,避免裁剪后相邻格内容污染。切割前必须先在整张素材图上把绿幕背景处理为透明 alpha,再在每个理论格子内按透明背景/前景像素做内容边界校准,并带少量安全留白导出;不能做固定内缩裁剪,避免贴近格线但未跨格的樱桃、叶片、把手等主体边缘被切掉。每个物品 JSON 写入 `imageViews[]`,同时把第一个视角兼容写入 `imageSrc/imageObjectKey`。
|
||||
11. 将素材图和每张独立视角图片上传到 OSS。每次获得可恢复的图片资产后,都要回写 `match3d_work_profile.generated_item_assets_json`。成功素材状态为 `image_ready`;失败素材保留已成功图片引用并记录 `error`。每个素材 JSON 同步保存 `soundPrompt`,首个素材 JSON 同步保存 `backgroundMusicTitle` 与 `backgroundMusicStyle`,`backgroundMusicPrompt` 保存为空字符串作为兼容字段。
|
||||
12. 后端在图片素材生成后使用 `backgroundMusic.title` 提交 Suno 背景音乐任务,`prompt` 为空,`tags` 来自 `backgroundMusic.style`,并固定走纯音乐生成。轮询完成后通过通用创作音频资产链路转存 OSS、确认 `asset_object`、绑定到 `match3d_work/background_music`,再写回首个素材的 `backgroundMusic`。自动草稿阶段必须拿到非空 `backgroundMusic.audioSrc` 才能返回成功;曲名为空、Suno 提交/轮询失败、音频下载失败、OSS 转存失败或资产绑定失败时,本次 `match3d_compile_draft` 返回失败并停留在生成页,不能进入结果页后显示“暂无音乐”。
|
||||
13. 草稿生成阶段不生成点击音效,只保存 `generatedItemAssets[].soundPrompt`;点击音效由结果页 `素材配置 > 物品` 详情面板手动生成并写回对应素材。
|
||||
14. UI 背景生成由 `api-server` 调用 VectorEngine `gpt-image-2-all` 分成两张资产:第一张是 `9:16` 纯背景图,不传锅参考图,且必须禁止锅、圆盘、托盘、拼图槽、物品槽、HUD、文字、按钮、倒计时、分数和物品;第二张是 `1:1` 题材容器 UI 图,固定传入 `public/match3d-background-references/pot-fused-reference.png` 作为参考图,只生成一个贴合题材设定的圆形或浅盘状竞技容器,不生成整页背景、文字、按钮或物品。容器图必须沿用参考图的大尺寸轻俯视构图:外轮廓接近画布四边,宽度约占 `86%-92%`、高度约占 `82%-90%`,内口为横向椭圆,禁止生成小容器、正俯视圆盘、侧视碗、餐盘或小托盘。纯背景上传到 `generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png`,容器 UI 图上传到 `generated-match3d-assets/{sessionId}/{profileId}/ui-container/{taskId}/container.png`,两者都作为 `backgroundAsset` 挂在首个 `generatedItemAssets[]` JSON 上;HTTP DTO 同时顶层输出兼容的 `backgroundPrompt`、`backgroundImageSrc`、`backgroundImageObjectKey` 与 `generatedBackgroundAsset`,容器图通过 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey` 返回。若作品尚无用户自定义封面,草稿生成完成后默认把容器 UI 图写入 `coverImageSrc`,作为草稿架和作品信息的默认封面。
|
||||
15. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息、背景音乐资产信息、背景资产信息和默认封面;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 与 `coverImageSrc` 恢复同一批素材、音乐、UI 与封面。
|
||||
10. 将每张素材图按固定 `5 行 * 5 列` 切割成独立图片,并按物品顺序连续分配 `5` 张视角图。素材图提示词必须要求 `5*5` 严格均匀排布、每格主体完整居中、统一纯绿色绿幕背景、相邻物体主体至少保留 `1/4` 单格宽度空白间距、不得跨格、贴边或越界,避免裁剪后相邻格内容污染。切割前必须先在整张素材图上做透明背景后处理:连通到 sheet 外边缘的绿幕/近白底要清成 alpha;每格内部未连到外边缘但高置信的纯绿绿幕块也必须清成 alpha;物品边缘的绿幕抗锯齿和近白白边要做透明或去污染处理;不够纯的绿色主体像素不得被当作绿幕误删。随后再在每个理论格子内按透明背景/前景像素做内容边界校准,并带少量安全留白导出;不能做固定内缩裁剪,避免贴近格线但未跨格的樱桃、叶片、把手等主体边缘被切掉。每个物品 JSON 写入 `imageViews[]`,同时把第一个视角兼容写入 `imageSrc/imageObjectKey`。
|
||||
11. 将素材图和每张独立视角图片上传到 OSS。每次获得可恢复的图片资产后,都要回写 `match3d_work_profile.generated_item_assets_json`。成功素材状态为 `image_ready`;失败素材保留已成功图片引用并记录 `error`。每个素材 JSON 可继续保存历史 `soundPrompt`;不再写入新的 `backgroundMusicTitle/backgroundMusicStyle/backgroundMusicPrompt`。
|
||||
12. UI 背景生成由 `api-server` 分成两张资产:第一张是 `9:16` 纯背景图,走 VectorEngine `/v1/images/generations` 的 `gpt-image-2-all` JSON 请求,不传锅参考图,且必须禁止锅、圆盘、托盘、拼图槽、物品槽、HUD、文字、按钮、倒计时、分数和物品;第二张是 `1:1` 题材容器 UI 图,走 VectorEngine `/v1/images/edits` multipart 请求,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,只生成一个贴合题材设定的圆形或浅盘状竞技容器,不生成整页背景、文字、按钮或物品。容器图必须沿用参考图的大尺寸轻俯视构图:外轮廓接近画布四边,宽度约占 `86%-92%`、高度约占 `82%-90%`,内口为横向椭圆,禁止生成小容器、正俯视圆盘、侧视碗、餐盘或小托盘。纯背景上传到 `generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png`,容器 UI 图上传到 `generated-match3d-assets/{sessionId}/{profileId}/ui-container/{taskId}/container.png`,两者都作为 `backgroundAsset` 挂在首个 `generatedItemAssets[]` JSON 上;HTTP DTO 同时顶层输出兼容的 `backgroundPrompt`、`backgroundImageSrc`、`backgroundImageObjectKey` 与 `generatedBackgroundAsset`,容器图通过 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey` 返回。若作品尚无用户自定义封面,草稿生成完成后默认把容器 UI 图写入 `coverImageSrc`,作为草稿架和作品信息的默认封面。
|
||||
13. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息、背景资产信息和默认封面;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 与 `coverImageSrc` 恢复同一批素材、UI 与封面。历史音频字段只做兼容传递。
|
||||
|
||||
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅`、本地作品描述与本地标签兜底,不阻断素材生成;标签仍通过作品标签生成器优先生成,失败后再用兜底标签补齐。
|
||||
|
||||
草稿生成阶段不再调用 Hyper3D Rodin,不生成 GLB,也不等待任何模型轮询。前端 `match3d_compile_draft` action 的长耗时主要来自文本生成、分批 1K 生图、切图、OSS 上传、纯背景图、容器 UI 图和可选音频生成。批量新增物品由 `POST /api/creation/match3d/works/{profileId}/item-assets` 复用同一套 2D 素材图生成、固定 `5*5` 切图、OSS 上传和可选点击音效链路;若本次新增数量不是 `5` 的倍数,同样向上补齐名称和图片到最近的 `5` 的倍数。整图生成完成后立即丢弃补齐用临时物品,只对用户实际新增项执行绿幕抠背景、切割和上传,并只把这些真实新增项的 `imageViews[]` 写回 `generatedItemAssets`。
|
||||
草稿生成阶段不再调用 Hyper3D Rodin,不生成 GLB,也不等待任何模型轮询。前端 `match3d_compile_draft` action 的长耗时主要来自文本生成、分批 1K 生图、切图、OSS 上传、纯背景图和容器 UI 图。批量新增物品由 `POST /api/creation/match3d/works/{profileId}/item-assets` 复用同一套 2D 素材图生成、固定 `5*5` 切图和 OSS 上传链路;若本次新增数量不是 `5` 的倍数,同样向上补齐名称和图片到最近的 `5` 的倍数。整图生成完成后立即丢弃补齐用临时物品,只对用户实际新增项执行绿幕抠背景、切割和上传,并只把这些真实新增项的 `imageViews[]` 写回 `generatedItemAssets`。批量重新生成同样调用该接口,但请求体增加 `mode = "replace"`,前端从现有素材列表收集用户确认的物品名称;后端只匹配已存在的同名素材,保留原 `itemId`、列表顺序、UI 背景、历史背景音乐和点击音效字段,只替换该素材的 `imageSrc/imageObjectKey/imageViews/status/error`,避免试玩和正式运行态的物品类型映射漂移。
|
||||
|
||||
## 4. 图片提示词
|
||||
|
||||
@@ -92,7 +100,7 @@ public/match3d-style-references/painterly-icon.png
|
||||
public/match3d-background-references/pot-fused-reference.png
|
||||
```
|
||||
|
||||
这张图只作为容器 UI 图的 VectorEngine `image` 参考输入,用来锁定“大尺寸轻俯视浅盘容器”的构图。参考图本身是 `1:1` 透明底容器素材,外轮廓接近画布四边,内口为横向椭圆;结果页没有真实生成容器时也只把它作为容器预览兜底,不能再作为 `9:16` 背景预览。每次草稿生成仍会根据 `backgroundPrompt` 生成新的题材化纯背景图;纯背景图不再传入该参考图,也不得生成锅或 UI 元素。
|
||||
这张图只作为容器 UI 图的 VectorEngine `/v1/images/edits` multipart `image` part,用来锁定“大尺寸轻俯视浅盘容器”的构图。参考图本身是 `1:1` 透明底容器素材,外轮廓接近画布四边,内口为横向椭圆;结果页没有真实生成容器时也只把它作为容器预览兜底,不能再作为 `9:16` 背景预览。每次草稿生成仍会根据 `backgroundPrompt` 生成新的题材化纯背景图;纯背景图不再传入该参考图,也不得生成锅或 UI 元素。
|
||||
|
||||
## 5. OSS 路径
|
||||
|
||||
@@ -116,7 +124,9 @@ generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.
|
||||
|
||||
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key,导致草稿页预览图全部一致。
|
||||
|
||||
HTTP DTO 同时返回兼容字段 `imageSrc`、`imageObjectKey`,以及正式 2D 字段 `imageViews[]`、`backgroundAsset` 和 `status`。图片素材生成成功后 `status = image_ready`;纯背景和容器 UI 图都生成成功后首个素材的 `backgroundAsset.status = image_ready`,并携带 `containerImageSrc/containerImageObjectKey`。前端通过 `/api/assets/read-url` 将 generated legacy path 换签后加载私有图片,不直接请求裸 `/generated-match3d-assets/...` 路径。运行态背景图和容器 UI 图同样通过 `/api/assets/read-url` 换签后加载:背景作为全屏 `object-cover`,容器作为中心棋盘覆盖层。
|
||||
HTTP DTO 同时返回兼容字段 `imageSrc`、`imageObjectKey`,以及正式 2D 字段 `imageViews[]`、`backgroundAsset` 和 `status`。图片素材生成成功后 `status = image_ready`;纯背景和容器 UI 图都生成成功后首个素材的 `backgroundAsset.status = image_ready`,并携带 `containerImageSrc/containerImageObjectKey`。前端通过 `/api/assets/read-url` 将 generated legacy path 换签后加载私有图片,不直接请求裸 `/generated-match3d-assets/...` 路径。运行态背景图和容器 UI 图同样通过 `/api/assets/read-url` 换签后加载:背景作为全屏 `object-cover`,容器作为中心棋盘视觉层;当容器图成功解析为可渲染图片时,`Match3DRuntimeShell` 必须移除默认圆形锅壳、边框和径向底色,让生成容器接管棋盘外观。
|
||||
|
||||
结果页手动点击 `素材配置 > UI > 重新生成` 时,目标库可能仍运行旧 SpacetimeDB wasm,缺少钱包扣退费或 Match3D 写回相关 procedure。`api-server` 对这类 `No such procedure` 只做临时容错:泥点预扣阶段跳过扣费,背景图和容器 UI 已经生成但草稿写回失败时,HTTP 响应仍返回本次生成的 `generatedBackgroundAsset` 和带该资产的内存 profile,避免草稿页直接报错。该容错不等于持久化成功;刷新、换设备或公开详情稳定恢复仍以发布最新 SpacetimeDB module、确认 procedure 导出和重新生成 bindings 为准。
|
||||
|
||||
## 5.1 运行态 2D 素材消费
|
||||
|
||||
@@ -124,25 +134,25 @@ HTTP DTO 同时返回兼容字段 `imageSrc`、`imageObjectKey`,以及正式 2
|
||||
|
||||
```text
|
||||
Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
-> Match3DRuntimeShell(generatedItemAssets, backgroundImageSrc)
|
||||
-> Match3DRuntimeShell(generatedItemAssets, generatedBackgroundAsset, backgroundImageSrc)
|
||||
-> Match3DPhysicsBoard / Match3DTrayPreviewBoard
|
||||
```
|
||||
|
||||
运行态按运行快照中的 `itemTypeId` 稳定排序后,把 `generatedItemAssets` 顺序映射到对应类型。加载某个物品实例时,从该类型素材的 `imageViews[]` 中按实例 id 稳定随机选择一个视角;若历史数据没有 `imageViews[]`,则回退到 `imageSrc/imageObjectKey`。没有生成图片或图片加载失败时,继续使用默认积木图标兜底。
|
||||
|
||||
运行态背景优先读取 `backgroundImageSrc` / `generatedBackgroundAsset.imageSrc`,为空时从 `generatedItemAssets[].backgroundAsset.imageSrc/imageObjectKey` 兜底。中心容器优先读取 `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`;为空时继续使用默认圆形容器样式。运行态入口判断是否需要补读作品详情时,只能把 `imageViews[]` 或 `imageSrc/imageObjectKey` 视为“已有物品图片素材”;`backgroundMusic.audioSrc`、`clickSound.audioSrc`、`backgroundAsset.image*` 和 `backgroundAsset.containerImage*` 是随物品素材一起传入的附属运行态资产,不能单独证明物品素材已完整。也不能继续只用历史 `modelSrc/modelObjectKey` 判断,否则新 2D 草稿会在试玩或推荐流中被当成“无素材”并回退默认积木。`Match3DRuntimeShell` 只保留顶部返回、倒计时、重开三个控件;这些顶部控件和底部备选栏统一使用题材无关的半透明玻璃组件样式,不能随背景题材改成木质、金属、果园、科幻等主题皮肤,也不能重新烘进 AI 背景图。进度、组数、版本等状态信息不得再作为顶部常驻 UI 出现,避免遮挡生成背景和中心容器。
|
||||
运行态背景优先读取 `backgroundImageSrc` / 顶层 `generatedBackgroundAsset.imageSrc/imageObjectKey`,为空时从 `generatedItemAssets[].backgroundAsset.imageSrc/imageObjectKey` 兜底。中心容器优先读取顶层 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey`,再读取 `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`;为空或换签/图片加载失败时继续使用默认圆形容器样式。容器图成功加载后,`Match3DRuntimeShell` 的棋盘容器必须切换为透明、可溢出承载,不再叠加默认 `rounded-full` 圆形锅壳、金色边框和默认径向背景,避免 AI 生成的大尺寸轻俯视容器被裁切或被默认锅视觉覆盖。运行态入口判断是否需要补读作品详情时,只能把 `imageViews[]` 或 `imageSrc/imageObjectKey` 视为“已有物品图片素材”;`backgroundMusic.audioSrc`、`clickSound.audioSrc`、`generatedBackgroundAsset`、`backgroundAsset.image*` 和 `backgroundAsset.containerImage*` 是随物品素材一起传入的附属运行态资产,不能单独证明物品素材已完整。也不能继续只用历史 `modelSrc/modelObjectKey` 判断,否则新 2D 草稿会在试玩或推荐流中被当成“无素材”并回退默认积木。`Match3DRuntimeShell` 只保留顶部返回、倒计时、重开三个控件;这些顶部控件和底部备选栏统一使用题材无关的半透明玻璃组件样式,不能随背景题材改成木质、金属、果园、科幻等主题皮肤,也不能重新烘进 AI 背景图。进度、组数、版本等状态信息不得再作为顶部常驻 UI 出现,避免遮挡生成背景和中心容器。
|
||||
|
||||
前端加载规则:
|
||||
|
||||
1. 优先读取 `imageViews[]` 中的 `imageSrc/imageObjectKey`,为空时使用兼容字段 `imageSrc/imageObjectKey`。
|
||||
2. 对 generated legacy path 通过同源 `/api/assets/read-url` 换签后交给浏览器图片加载;结果页 `素材配置 > 背景音乐` 与 `素材配置 > 物品` 的音频试听控件也必须先换签,不能把裸 `/generated-match3d-assets/...` 音频路径直接交给 `<audio>`。
|
||||
2. 对 generated legacy path 通过同源 `/api/assets/read-url` 换签后交给浏览器图片加载;结果页音频试听入口当前隐藏,后续恢复时也必须先换签,不能把裸 `/generated-match3d-assets/...` 音频路径直接交给 `<audio>`。
|
||||
3. 场内物品、点击命中和备选栏继续使用后端快照中的 `itemInstanceId/itemTypeId/x/y/radius/layer`;生成 2D 图片只替换视觉表现,不承接规则真相。
|
||||
4. 同一物品类型的多个实例可以展示不同视角,但同一实例在本局中应稳定使用同一个视角,避免移动或入槽时闪图。
|
||||
5. 图片缺失、读取失败或解码失败时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算。
|
||||
|
||||
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `generatedItemAssets` 带入运行态启动入参。`PUT /api/runtime/match3d/works/{profileId}` 若因为并发或旧快照返回了缺少素材的 profile,`Match3DResultView` 需要把当前 draft / profile 的素材重新合并到运行态 profile,并在启动试玩前调用生成素材保存接口把当前可见的 `generatedItemAssets` 写回作品 profile;不能只在内存里把素材补到 `onStartTestRun(profile)`。发布同理必须先落库当前素材,再调用 `publish_match3d_work`,否则公开推荐流和正式运行态只能读到旧 profile 快照。结果页顶部返回按钮固定回到平台创作页,不再回到抓大鹅专属内嵌入口表单;需要修改题材时由用户在创作页重新选择或从草稿继续进入。若历史草稿同时存在旧 `draft.generatedItemAssets` 和较新的 `profile.generatedItemAssets`,同 `itemId` 下以 profile 中已有的 `imageViews[]`、`imageSrc`、`imageObjectKey`、`backgroundMusic` 或 `backgroundAsset` 补齐 draft,不能让旧 draft 把素材覆盖成空列表。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dRuntimeProfile / match3dProfile.generatedItemAssets`,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets`;即使当前 profile 暂时没有物品图片,也不能把同 profile 的已有 `generatedItemAssets` 覆盖为空数组。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少物品图片素材时,启动前补读 `getMatch3DWorkDetail(profileId)`,把详情里的生成图片、背景音乐和 UI 素材写入 `match3dRuntimeProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 2D 素材、音乐或 UI 覆盖成空列表。
|
||||
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `generatedItemAssets` 带入运行态启动入参。`PUT /api/runtime/match3d/works/{profileId}` 若因为并发或旧快照返回了缺少素材的 profile,`Match3DResultView` 需要把当前 draft / profile 的素材重新合并到运行态 profile,并在启动试玩前调用生成素材保存接口把当前可见的 `generatedItemAssets` 写回作品 profile;不能只在内存里把素材补到 `onStartTestRun(profile)`。发布同理必须先落库当前素材,再调用 `publish_match3d_work`,否则公开推荐流和正式运行态只能读到旧 profile 快照。结果页顶部返回按钮固定回到平台创作页,不再回到抓大鹅专属内嵌入口表单;需要修改题材时由用户在创作页重新选择或从草稿继续进入。若历史草稿同时存在旧 `draft.generatedItemAssets` 和较新的 `profile.generatedItemAssets`,同 `itemId` 下以 profile 中已有的 `imageViews[]`、`imageSrc`、`imageObjectKey`、`backgroundMusic` 或 `backgroundAsset` 补齐 draft,不能让旧 draft 把素材覆盖成空列表。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dRuntimeProfile / match3dProfile.generatedItemAssets`,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets`;即使当前 profile 暂时没有物品图片,也不能把同 profile 的已有 `generatedItemAssets` 覆盖为空数组。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少物品图片素材时,启动前补读 `getMatch3DWorkDetail(profileId)`,把详情里的生成图片、历史背景音乐和 UI 素材写入 `match3dRuntimeProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 2D 素材、历史音乐或 UI 覆盖成空列表。
|
||||
|
||||
2026-05-14 补充:`backgroundMusic` 虽然暂存在 `generatedItemAssets[]` 中,但语义上是作品级音乐。前端读取、保存、试玩、推荐流和运行态入口都必须先通过统一归一化逻辑把任意素材上的 `backgroundMusic/backgroundMusicTitle/backgroundMusicStyle/backgroundMusicPrompt` 迁移到首个素材,并清空其它素材上的作品级音乐字段,避免 `素材配置 > 背景音乐` 只读首项时显示“暂无音乐”,也避免 action response 中缺音乐的 draft assets 覆盖 work detail 中已经持久化的音乐。`match3d_compile_draft` action 完成后,如果同时拿到 `response.session.draft.generatedItemAssets` 和 `getMatch3DWorkDetail(profileId).item.generatedItemAssets`,必须以同 `itemId` 合并,保留详情里的背景音乐、UI 背景和点击音效,再进入结果页或试玩。
|
||||
2026-05-14 补充:`backgroundMusic` 虽然暂存在 `generatedItemAssets[]` 中,但语义上是作品级音乐。前端读取、保存、试玩、推荐流和运行态入口都必须先通过统一归一化逻辑把任意素材上的 `backgroundMusic/backgroundMusicTitle/backgroundMusicStyle/backgroundMusicPrompt` 迁移到首个素材,并清空其它素材上的作品级音乐字段,避免 action response 中缺音乐的 draft assets 覆盖 work detail 中已经持久化的音乐,也为未来恢复音乐入口保留稳定读取口径。`match3d_compile_draft` action 完成后,如果同时拿到 `response.session.draft.generatedItemAssets` 和 `getMatch3DWorkDetail(profileId).item.generatedItemAssets`,必须以同 `itemId` 合并,保留详情里的历史背景音乐、UI 背景和点击音效,再进入结果页或试玩。
|
||||
|
||||
历史草稿若仍保存 `status = model_ready`、`modelSrc` 或 `modelObjectKey`,仅作为旧版本兼容读取,不再参与新素材生产。历史外部模型链接转存接口只用于清理旧数据,不能被新草稿生成、批量新增或结果页普通编辑入口调用。
|
||||
|
||||
@@ -152,9 +162,9 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
|
||||
点击 `生成抓大鹅草稿` 后,草稿存档创建与素材生成解耦:
|
||||
|
||||
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行,即使后续卡在文本模型、图片生成、音频生成或 OSS 上传任意阶段。
|
||||
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行,即使后续卡在文本模型、图片生成或 OSS 上传任意阶段。
|
||||
2. 失败态前端要重新读取 session / work detail,并刷新草稿作品架,保证用户离开生成页后仍能在草稿 Tab 找到这份作品。
|
||||
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId`,不得重新创建 session;后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片或缺失音频的阶段。
|
||||
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId`,不得重新创建 session;后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片、UI 背景或容器资源。
|
||||
4. 已有 `status = image_ready` 且带 `imageViews[]` 或 `imageSrc/imageObjectKey` 的素材视为完成,不再重复生成图片。
|
||||
|
||||
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json`。结果页 `素材配置 > 物品` 只在独立面板中预览和编辑当前素材,不再提供单项重新生成入口;删除单项或批量新增成功后,都必须把当前素材列表重新序列化成 `generatedItemAssets` 并写回作品 profile,否则试玩、发布和重进草稿会读取旧素材快照。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
|
||||
@@ -174,7 +184,7 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
1. `作品名称` 对应 Match3D `gameName`。
|
||||
2. `作品描述` 对应 Match3D `summary`,草稿生成阶段由同一次作品生成计划自动填入。
|
||||
3. `作品标签` 对应 Match3D `tags`,草稿生成阶段在写入名称和描述后自动调用标签生成器填入;结果页仍允许用户继续编辑或再次 AI 生成。
|
||||
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选入口,避免和作品基础信息割裂。草稿生成默认使用生成出的中心容器 UI 图作为 `coverImageSrc`。点击封面图必须弹出独立编辑面板,不允许在当前作品信息面板下方展开。封面面板布局参考拼图创作页上传卡:移动端优先、左侧/上方为方形预览,右侧/下方为提示词与操作区。面板支持三类输入:本地上传图片、上传后开启 AI 重绘、直接引用 `物品素材` 或 `UI素材` 中已有图片作为封面或 AI 重绘参考图。AI 重绘通过 `api-server` 的 Match3D 作品封面生成接口调用 VectorEngine `gpt-image-2-all`,生成结果转存到 `generated-match3d-assets/{sessionId}/{profileId}/cover/{taskId}/cover.png` 后再写回 `coverImageSrc`;关闭 AI 重绘时只把选中的 Data URL 或 generated legacy path 写入封面字段。
|
||||
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选入口,避免和作品基础信息割裂。旧称“碰面图”统一改为“封面图”。草稿生成默认使用生成出的中心容器 UI 图作为 `coverImageSrc`。点击封面图必须弹出独立编辑面板,不允许在当前作品信息面板下方展开。封面面板布局对齐拼图创作页上传卡:移动端优先,左侧/上方为方形预览卡,预览卡本身就是上传热区;上传图片后,预览卡内出现和拼图入口一致的 `AI重绘` 开关与删除按钮,面板底部不再额外展示旧 `AI重绘` 选项。已有上传图时,右侧/下方输入框标题为 `AI重绘要求`;关闭 AI 重绘时只把上传图 Data URL 写入封面字段,不调用生图模型。没有上传图时,输入框标题为 `封面描述`,可选择多张参考图后调用 VectorEngine `gpt-image-2-all` 文生图链路,参考图通过请求体 `image` 数组传入;参考图来源支持直接引用 `物品素材` / `UI素材` 中已有图片,也支持自定义上传。上传图 AI 重绘与无上传图多参考图生成都通过 `api-server` 的 Match3D 作品封面生成接口完成,生成结果转存到 `generated-match3d-assets/{sessionId}/{profileId}/cover/{taskId}/cover.png` 后再写回 `coverImageSrc`。
|
||||
|
||||
结果页 `难度配置` Tab 取代旧 `玩法配置`,不再展示旧的分散输入项。该 Tab 顶部使用横向离散拖动条调整难度,四个刻度分别为 `轻松 / 标准 / 进阶 / 硬核`;拖动条只能落在这四个点上,刻度标签可点击切换。该 Tab 必须与创作入口页使用同一组难度选项,并统一把原“类型素材图片 / 局内类型”等口径归一为 `物品种类`:
|
||||
|
||||
@@ -187,36 +197,36 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
|
||||
预览区展示 `需要消除`、`总物品数`、`物品种类` 和 `已生成物品种类`。历史草稿如果保存的是旧 `clearCount/difficulty`,前端按 `clearCount` 精确命中优先、否则按 `difficulty` 就近归一到上述选项,并把归一后的数值保存回 profile。发布校验以 `generatedItemAssets[]` 中 `image_ready` 且至少有 `5` 张有效 `imageViews[]` 的素材数量为准;试玩启动时用同一数量计算 `itemTypeCountOverride`,不足时自动降低,不修改草稿难度配置本身。历史单图 `imageSrc/imageObjectKey` 只作为运行态和预览兜底,不计入新发布素材完成数。
|
||||
|
||||
结果页 `素材配置` Tab 取代旧一级素材入口,并包含三个子 Tab:
|
||||
结果页 `素材配置` Tab 取代旧一级素材入口,当前包含三个子 Tab:
|
||||
|
||||
1. `物品`:显示 2D 物品素材列表、五视角预览、素材名称、点击音效提示词和点击音效生成入口。
|
||||
2. `UI`:预览生成的竖屏游戏纯背景图和中心容器 UI 图。背景读取顺序为 draft 顶层背景、draft `generatedBackgroundAsset`、profile 顶层背景、profile `generatedBackgroundAsset`、`generatedItemAssets[].backgroundAsset`、本地兜底图;容器读取 `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`,缺失时使用默认圆形容器。该页必须展示默认画面描述提示词,默认值来自草稿生成计划的 `backgroundPrompt` 或持久化 `backgroundAsset.prompt`;用户修改后点击重新生成,后端同时生成纯背景图和容器 UI 图,并把新的 `backgroundAsset` 写回同一份 `generated_item_assets_json`。UI 子 Tab 还必须提供独立的运行态 UI 预览面板,直接用当前纯背景图、容器 UI 图、顶部返回/倒计时/重开控件和底部默认托盘模拟竖屏页面,不在 Tab 下方内联展开。
|
||||
3. `背景音乐`:承载原一级音乐 Tab 的背景音乐曲名、风格、生成进度和试听控件;背景音乐始终按纯音乐生成,前端不提供提示词输入。
|
||||
1. `物品`:显示 2D 物品素材列表、五视角预览和素材名称;点击音效提示词与生成入口已临时隐藏。
|
||||
2. `UI`:预览生成的竖屏游戏纯背景图,并在独立运行态 UI 预览面板中叠加当前中心容器形象。背景读取顺序为 draft 顶层背景、draft `generatedBackgroundAsset`、profile 顶层背景、profile `generatedBackgroundAsset`、`generatedItemAssets[].backgroundAsset`、本地兜底图;容器读取 `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`,缺失时使用默认圆形容器。该页必须展示默认画面描述提示词,默认值来自草稿生成计划的 `backgroundPrompt` 或持久化 `backgroundAsset.prompt`;用户修改后点击重新生成,先弹出 `确认消耗泥点` 面板并展示 `消耗 2 泥点`,确认后调用 `POST /api/creation/match3d/works/{profileId}/background-image`,现阶段后端仍会同时生成纯背景图和容器 UI 图,并把新的 `backgroundAsset` 写回同一份 `generated_item_assets_json`。确认后按钮区域必须显示生成进度条,按 `90` 秒估算倒计时,后端真实返回后立即收口。UI 子 Tab 的预览面板直接用当前纯背景图、容器 UI 图、顶部返回/倒计时/重开控件和底部默认托盘模拟竖屏页面,不在 Tab 下方内联展开。
|
||||
3. `容器形象`:预览和重新生成 `1:1` 中心容器 UI 图,页面能力与 `UI` 子 Tab 对齐,包含方形容器预览、容器提示词输入、独立运行态 UI 预览面板、泥点确认和生成进度。默认提示词来自持久化 `backgroundAsset.containerPrompt`,缺失时从当前题材、作品描述和容器参考约束生成兜底提示词;用户修改后点击重新生成,先弹出 `确认消耗泥点` 面板并展示 `消耗 2 泥点`,确认后调用 `POST /api/creation/match3d/works/{profileId}/container-image`,按钮区域同样按 `90` 秒估算显示倒计时进度。该接口只重新生成并写回 `backgroundAsset.containerPrompt/containerImageSrc/containerImageObjectKey`,必须保留已有纯背景 `prompt/imageSrc/imageObjectKey`、物品素材、历史背景音乐和点击音效字段,避免用户只换容器时刷新掉背景图或物品素材。
|
||||
|
||||
旧一级 `音乐` Tab 删除;抓大鹅背景音乐入口只保留在 `素材配置 > 背景音乐`。
|
||||
旧一级 `音乐` Tab 删除;`素材配置 > 背景音乐` 当前也不展示。
|
||||
|
||||
`素材配置 > 物品` 详情页只保留:
|
||||
|
||||
1. 五视角预览区:优先展示 `imageViews[]`,缺失时展示兼容字段 `imageSrc/imageObjectKey`。
|
||||
2. 素材名称输入。
|
||||
3. 可编辑的点击音效提示词输入。
|
||||
4. 点击音效生成入口。
|
||||
3. 不展示点击音效提示词输入或点击音效生成入口。
|
||||
|
||||
五视角预览区采用“上方大预览 + 底部缩略图栏”的布局:上方是方形焦点预览区,中间横向排列当前物品的各视角图片,并用内框标出当前焦点;底部缩略图栏固定露出 `4` 个方形槽位,多出的第 `5` 个视角通过横向滚动访问。点击缩略图只切换焦点视角,不在面板内新增说明文案或额外规则区。
|
||||
|
||||
详情页不再展示参考图、用途、模型提示词、文生/图生切换、状态查询、下载列表、taskUuid 或 subscriptionKey。
|
||||
|
||||
`物品素材` 列表项点击必须弹出独立预览面板,不允许在列表右侧或列表下方内联展示。列表本身使用移动端至少两列的多列卡片布局;每个列表项只展示图片预览、物品名称和垃圾箱删除图标,不展示用途、状态胶囊、视角数量或 `2D素材` 标记。预览面板只承担查看五视角图片、编辑素材名称、编辑点击音效提示词和生成点击音效;不再展示 `重新生成` 按钮。列表项自身支持单项删除,删除后立即把剩余 `generatedItemAssets` 写回作品 profile。批量新增通过列表顶部按钮打开独立面板,面板内每个输入框只输入一个物品名称,`新增物品名称` 按钮追加一个输入框;提交后按输入框顺序清洗、去重并调用 Match3D 作品批量生图接口。生成进度同时显示在批量新增面板和 `素材配置 > 物品` 列表顶部,面板可关闭,后台生成继续推进,不阻塞封面、音频等其他生成操作。后端复用草稿生成的素材图、切图、OSS 上传和可选点击音效流程,但仅按实际可新增名称持久化,不重新生成已有物品,不新增 SpacetimeDB 表,最终仍写回同一份 `generated_item_assets_json`。批量新增先补齐到 `5` 个参与整图生成,随后丢弃补齐用临时物品,只对真实新增物品抠背景、切割和上传。批量新增计费按实际可新增名称每 `5` 个消耗 `2` 泥点,不足 `5` 个向上取整;重复名称、已有名称和超过容量上限的名称不计费。
|
||||
`物品素材` 列表项点击必须弹出独立预览面板,不允许在列表右侧或列表下方内联展示。列表本身使用移动端至少两列的多列卡片布局;每个列表项只展示图片预览、物品名称和垃圾箱删除图标,不展示用途、状态胶囊、视角数量或 `2D素材` 标记。预览面板只承担查看五视角图片和编辑素材名称;不再展示单项 `重新生成` 按钮。列表项自身支持单项删除,删除后立即把剩余 `generatedItemAssets` 写回作品 profile。批量新增通过列表顶部按钮打开独立面板,面板内每个输入框只输入一个物品名称,`新增物品名称` 按钮追加一个输入框;提交按钮点击后必须先弹出 `确认消耗泥点` 面板并展示按当前有效名称计算出的泥点数,用户确认后才按输入框顺序清洗、去重并调用 Match3D 作品批量生图接口。批量重新生成通过列表顶部另一个按钮打开独立面板,面板预填当前全部素材名称,用户可清空不需要重新生成的输入框;提交按钮点击后同样先弹出泥点确认面板,确认后按输入框顺序清洗、去重并以替换模式调用同一接口。生成进度同时显示在批量面板和 `素材配置 > 物品` 列表顶部,面板可关闭,后台生成继续推进,不阻塞封面或 UI 生成操作。后端复用草稿生成的素材图、切图和 OSS 上传流程,但新增模式仅按实际可新增名称持久化,不重新生成已有物品;替换模式仅按实际匹配到的已有名称持久化,不新增物品、不改变 `itemId`。两者都不新增 SpacetimeDB 表,最终仍写回同一份 `generated_item_assets_json`。批量新增和批量重新生成都先补齐到 `5` 个参与整图生成,随后丢弃补齐用临时物品,只对真实新增或真实替换物品抠背景、切割和上传。计费按实际新增或替换名称每 `5` 个消耗 `2` 泥点,不足 `5` 个向上取整;重复名称、已有名称、未匹配名称和超过容量上限的名称不计费。
|
||||
|
||||
## 6.1 音频生成与扣费
|
||||
|
||||
抓大鹅结果页音频生成复用通用创作音频路由:
|
||||
抓大鹅结果页音频生成当前临时关闭:
|
||||
|
||||
1. `素材配置 > 背景音乐` 默认读取首个 `generatedItemAssets[0].backgroundMusicTitle/backgroundMusicStyle`,用户可继续编辑曲名和风格;`backgroundMusicPrompt` 保留为空字符串兼容旧 JSON,生成请求固定传空 `prompt`。
|
||||
2. 物品点击音效默认读取对应 `generatedItemAssets[].soundPrompt`,用户可在 `素材配置 > 物品` 详情面板内编辑。
|
||||
3. 背景音乐与物品音效生成过程必须显示进度条;提交任务、等待生成、转存资产和完成分别推进到不同进度,不再只展示旋转图标。
|
||||
4. 音频生成完成后立即展示浏览器原生 audio 控件,支持试听。
|
||||
5. `POST /api/creation/audio/background-music/{task_id}/asset` 和 `POST /api/creation/audio/sound-effect/{task_id}/asset` 在真正拿到音频并转存资产前,由后端按 `taskId + 资产槽位` 幂等预扣;背景音乐扣 `5` 泥点,物品点击音效扣 `10` 泥点。任务仍在处理中时不扣费。资产下载、OSS 转存或资产绑定失败时后端自动退款。前端只展示生成按钮和进度,不自行计算或写入钱包。
|
||||
1. `素材配置 > 背景音乐` 不展示。
|
||||
2. `素材配置 > 物品` 详情面板不展示点击音效提示词和生成入口。
|
||||
3. 通用 `/api/creation/audio/*` 路由对 `match3d_work` / `match3d_item` 暂时返回 `410 Gone`,防止旧前端或脚本继续触发抓大鹅扣费音频任务。
|
||||
4. 历史 `generatedItemAssets[].backgroundMusic` 与 `clickSound` 字段保留,运行态仍可消费旧音频;结果页不提供重新生成入口。
|
||||
|
||||
创作入口不展示 `生成音效` Toggle。草稿生成阶段不产生物品点击音效任务,也不产生点击音效相关扣费;入口只产生一次固定 `10` 泥点的草稿生成扣费。结果页 `素材配置 > UI` 重新生成背景固定扣 `2` 泥点。物品点击音效由结果页 `素材配置 > 物品` 详情面板手动触发,每个音效按单独任务和单独 `match3d_click_sound` 资产槽位扣费。音效生成失败不影响草稿,失败素材保留 `soundPrompt`,用户可在结果页物品详情面板手动重试。
|
||||
创作入口不展示 `生成音效` Toggle。草稿生成阶段不产生背景音乐或物品点击音效任务,也不产生音频相关扣费;入口只产生一次固定 `10` 泥点的草稿生成扣费。结果页 `素材配置 > UI` 重新生成背景固定扣 `2` 泥点;`素材配置 > 容器形象` 单独重新生成容器同样固定扣 `2` 泥点。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
@@ -238,4 +248,4 @@ cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
|
||||
```
|
||||
|
||||
真实草稿生成需要本地私密环境配置 `APIMART_BASE_URL` / `APIMART_API_KEY` / `APIMART_IMAGE_REQUEST_TIMEOUT_MS` 用于物品素材 sheet,配置 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 用于封面、背景和容器 UI,并补齐完整 `ALIYUN_OSS_BUCKET`、`ALIYUN_OSS_ENDPOINT`、`ALIYUN_OSS_ACCESS_KEY_ID`、`ALIYUN_OSS_ACCESS_KEY_SECRET`。如果只配置 bucket 和 endpoint,抓大鹅素材、封面或背景生成会在调用外部生图前返回 `OSS 未完成环境变量配置`,`details.missingEnv` 会列出缺少的 AccessKey 项;不要回退到 Rodin/GLB 或伪造本地上传成功。开启音频生成还需要对应音频上游配置。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;其中物品素材 sheet 走 VectorEngine Gemini 原生 `generateContent`,封面和 `9:16` 纯背景走 VectorEngine `/v1/images/generations` 的 `gpt-image-2-all` JSON 链路,`1:1` 容器 UI 走 VectorEngine `/v1/images/edits` multipart 参考图链路。同时必须补齐完整 `ALIYUN_OSS_BUCKET`、`ALIYUN_OSS_ENDPOINT`、`ALIYUN_OSS_ACCESS_KEY_ID`、`ALIYUN_OSS_ACCESS_KEY_SECRET`。如果只配置 bucket 和 endpoint,抓大鹅素材、封面或背景生成会在调用外部生图前返回 `OSS 未完成环境变量配置`,`details.missingEnv` 会列出缺少的 AccessKey 项;不要回退到 Rodin/GLB 或伪造本地上传成功。临时关闭期间不需要音频上游配置。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
|
||||
1. 校验 `productId`
|
||||
2. `paymentChannel = "mock"` 时后端创建已支付订单
|
||||
3. `paymentChannel = "wechat_mp"` 时后端创建待支付订单,并调用微信支付 JSAPI 下单生成小程序支付参数
|
||||
3. `paymentChannel = "wechat_mp"` 时后端创建待支付订单,并调用微信支付 JSAPI 下单生成小程序支付参数;本地 `orderId` 会作为微信 `out_trade_no` 传递,格式固定为 `rcg` 前缀 + 小写字母数字,长度在 6-32 字符内,满足微信支付 JSAPI 下单文档对商户订单号的限制。商品描述限制为 127 字符内,回调地址限制为 HTTPS、255 字符内且不携带 query/fragment。
|
||||
- JSAPI 下单请求必须显式携带 `Accept: application/json`、`Content-Type: application/json` 和 `User-Agent: Genarrative-WechatPay/1.0`;微信侧会把缺少 `User-Agent` 的请求返回为“Http头缺少Accept或User-Agent”。
|
||||
4. mock 泥点套餐立即写入钱包余额与流水,mock 会员套餐立即写入会员状态
|
||||
5. wechat_mp 订单不提前发泥点或会员,只返回待支付订单、账户中心快照与 `wechatMiniProgramPayParams`
|
||||
|
||||
@@ -84,16 +85,42 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 `POST /api/profile/recharge/wechat/notify`
|
||||
### 3.3 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm`
|
||||
|
||||
需要 Bearer JWT。该接口用于小程序支付页返回 web-view 后的主动查单确认,不替代微信支付通知:
|
||||
|
||||
1. 后端读取本地 `profile_recharge_order` 并校验订单归属、支付渠道和当前状态。
|
||||
2. 若订单已是 `paid`,直接返回订单与账户中心快照。
|
||||
3. 若订单仍是 `pending`,后端调用微信支付按商户订单号查单接口。
|
||||
4. 只有微信查单返回 `trade_state = "SUCCESS"` 时,才调用统一入账 procedure 把订单改为 `paid` 并写入钱包流水或会员状态。
|
||||
5. 如果微信查单仍不是 `SUCCESS`,接口返回当前 pending 订单与账户中心快照;前端只在全局支付结果模态显示“支付已提交”,不提前发放泥点或会员。
|
||||
|
||||
响应结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"order": {
|
||||
"orderId": "rcg...",
|
||||
"status": "paid"
|
||||
},
|
||||
"center": {
|
||||
"walletBalance": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 `POST /api/profile/recharge/wechat/notify`
|
||||
|
||||
微信支付通知地址,无需 Bearer JWT。行为:
|
||||
|
||||
1. 真实渠道使用微信支付平台公钥和 `Wechatpay-*` 请求头验签。
|
||||
1. 真实渠道使用微信支付平台公钥和 `Wechatpay-*` 请求头验签;验签必须使用原始 HTTP body bytes 构造 `timestamp\nnonce\nbody\n`,不能先把 body 转成字符串再重建。
|
||||
2. 使用 `WECHAT_PAY_API_V3_KEY` 解密通知 `resource`。
|
||||
3. 仅当 `trade_state = "SUCCESS"` 时确认订单支付。
|
||||
4. 使用微信通知里的 `out_trade_no` 查本地 `profile_recharge_order.order_id`,把订单从 `pending` 改为 `paid`。
|
||||
5. 将微信平台订单号写入 `provider_transaction_id`,用于对账、查单、退款和客服排障。
|
||||
6. 在同一 SpacetimeDB procedure 内写入钱包流水或会员到期时间,确保重复通知幂等。
|
||||
7. 验签、解密和业务确认通过后返回 HTTP `204 No Content`;不要返回 V2 XML。
|
||||
8. 微信支付公钥模式下,真实请求会携带 `Wechatpay-Serial: PUB_KEY_ID_...`,通知验签必须要求回调头 `Wechatpay-Serial` 与 `WECHAT_PAY_PLATFORM_SERIAL_NO` 对应;若不匹配应返回 `401` 并在日志里记录 reason。
|
||||
|
||||
关键环境变量:
|
||||
|
||||
@@ -115,8 +142,14 @@
|
||||
2. 弹窗顶部标题为 `账户充值`,右上角关闭。
|
||||
3. 默认打开 `泥点充值`,可切换到 `会员卡充值`。
|
||||
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`。
|
||||
5. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和状态反馈。
|
||||
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
|
||||
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
|
||||
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。
|
||||
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
|
||||
- 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。
|
||||
- `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
|
||||
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
|
||||
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
|
||||
7. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
1. 新建作品入口配置统一存放在 SpacetimeDB 的 `creation_entry_config` / `creation_entry_type_config` 表;默认种子位于 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`。
|
||||
2. `visible` 控制玩法是否展示在创作 Tab 模板入口、新建作品入口和创作类型弹层中。
|
||||
3. `open` 控制玩法是否允许点击创建以及对应创作 / runtime API 路由是否放行;`open: false` 时入口保持展示但禁用,并由 `api-server` 熔断对应玩法 API。
|
||||
- 前端作品架、发现页聚合和预加载只应请求 `open: true` 的玩法接口;`open: false` 的未开放玩法可以展示为敬请期待入口,但不得把对应 API 熔断错误透传到草稿页或发现页。
|
||||
4. `title`、`subtitle`、`badge` 控制玩法卡片文案。
|
||||
5. `startCard` 控制旧创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案;当前创作 Tab 首屏标题固定在 `PlatformEntryFlowShellImpl.tsx`,不再由 `startCard` 控制。
|
||||
6. `typeModal` 控制平台创作类型弹层标题和描述。
|
||||
@@ -28,6 +29,22 @@
|
||||
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待,暂不允许创建视觉小说草稿 |
|
||||
| 智能创作 | 否 | 是 | 入口隐藏,既有 `creative-agent` 链路保留 |
|
||||
| 汪汪声浪 | 是 | 是 | `bark-battle` 正式轻创作入口,进入单页配置表单并发布后启动声控对战 runtime |
|
||||
| 宝贝识物 | 是 | 是 | 寓教于乐首关模板,必须由 `creation_entry_type_config` 默认种子或后台入口开关保持存在 |
|
||||
|
||||
## 排障约束
|
||||
|
||||
`baby-object-match` 是寓教于乐创作模板和发现页宝贝识物作品合流的共同开关。若 `creation_entry_type_config` 缺少该行,前端会同时出现“创作界面没有宝贝识物模板”和“寓教于乐分类下已发布作品消失”的现象。
|
||||
|
||||
默认种子必须包含:
|
||||
|
||||
- `id = baby-object-match`
|
||||
- `title = 宝贝识物`
|
||||
- `visible = true`
|
||||
- `open = true`
|
||||
- `sort_order = 90`
|
||||
|
||||
入口图首版复用 `/child-motion-demo/picture-book-grass-stage.png`,后续如生成专属 image-2 入口图,可通过后台入口开关更新 `imageSrc`。
|
||||
|
||||
## 验收
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
1. 入口表单只展示 `画面描述`、参考图和图片模型选择;`画面描述` 是唯一必填字段。
|
||||
2. 表单自动保存只保存 `pictureDescription`,不再保存入口作品名称、作品描述或推断标签。
|
||||
3. 点击“生成草稿”后进入生成进度页,步骤固定对齐后端当前编排:“编译首关草稿 -> 生成关卡名称 / 生成首关画面 -> 生成背景音乐 / 生成UI背景 -> 写入正式草稿”。其中关卡名称文本生成与首关画面生成可并行;首关最终名称确定后,背景音乐与 UI 背景必须并行启动。
|
||||
3. 点击“生成草稿”后进入生成进度页,步骤固定对齐后端当前编排:“编译首关草稿 -> 生成关卡名称与 UI 背景提示词 / 生成首关画面 -> 生成UI背景 -> 写入正式草稿”。其中关卡名称文本生成、UI 背景提示词生成与首关画面生成可并行;首关最终名称确定后生成 UI 背景。背景音乐生成已于 2026-05-14 临时关闭。
|
||||
4. 生成进度页“当前拼图信息”只展示画面描述;不得展示空作品名称、空作品描述或旧五锚点结构。
|
||||
5. 结果页打开后,作品名称默认使用首关名称,作品描述与作品标签保持为空,等待用户在作品信息 Tab 补全或触发 AI 标签生成。
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
12. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时,api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
|
||||
13. `compile_puzzle_draft` 前置泥点预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`,SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
|
||||
14. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs`;`puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化,不再直接拼草稿 prompt 文本。
|
||||
15. `compile_puzzle_draft_with_initial_cover` 中,首关文本名称生成与首关图片生成互不等待:首图 prompt 只读取画面描述,OSS 临时路径使用已有名或确定性兜底名;首图返回后再用图片语义尝试精修最终关卡名。最终关卡名确定后,必须继续生成首关 UI 背景图与背景音乐,并在写入正式草稿前校验 `levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey` 与 `levels[0].backgroundMusic.audioSrc` 都不为空;任一资产失败时 `compile_puzzle_draft` 返回上游错误,生成页停留失败态,不能返回一个显示“暂无音乐”或默认 UI 的成功草稿。
|
||||
16. `compile_puzzle_draft_with_uploaded_cover` 中,上传图解析后,文本名称生成、图片语义名称生成和上传图转存 OSS 可并行;上传图转存失败必须立即返回,不得继续触发背景音乐或 UI 背景生成。上传图转存成功且最终关卡名确定后,同样必须生成并校验首关 UI 背景图与背景音乐。自动草稿阶段先生成 UI 背景,再生成会单独扣费的音乐资产,避免 UI 失败后留下未写入草稿的已扣费音频。
|
||||
15. `compile_puzzle_draft_with_initial_cover` 中,首关文本名称生成与首关图片生成互不等待:首图 prompt 只读取画面描述,OSS 临时路径使用已有名或确定性兜底名;同一次首关命名 LLM 请求必须返回 `levelName` 与 `uiBackgroundPrompt`,首图返回后再用图片语义尝试精修最终关卡名与 UI 背景提示词。最终关卡名确定后,必须继续用 AI 返回的 `uiBackgroundPrompt` 生成首关 UI 背景图;若命名模型未返回可用提示词,才按作品、关卡和标签拼接确定性兜底提示词。写入正式草稿前校验 `levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey` 不为空;UI 背景失败时 `compile_puzzle_draft` 返回上游错误,生成页停留失败态。背景音乐生成临时关闭,不再作为草稿完成门槛。
|
||||
16. `compile_puzzle_draft_with_uploaded_cover` 中,上传图解析后,文本名称生成、图片语义名称生成和上传图转存 OSS 可并行;上传图转存失败必须立即返回,不得继续触发 UI 背景生成。上传图转存成功且最终关卡名确定后,同样必须生成并校验首关 UI 背景图。自动草稿阶段不再触发音乐资产生成。
|
||||
|
||||
## 结果页
|
||||
|
||||
@@ -99,22 +99,24 @@
|
||||
|
||||
1. 拼图关卡列表:默认展示草稿生成出的第一关。列表项参考 RPG 草稿卡片样式,显示画面图、关卡名称和轻量状态。支持新增关卡、删除关卡。点击列表项进入独立关卡详情页,不在列表项下方展开。关卡详情页可编辑关卡名称、画面描述、生成或重新生成画面,并在已有正式图后支持关卡测试。
|
||||
2. 作品信息:展示并编辑作品名称、作品描述、作品标签。
|
||||
3. 素材配置:对齐抓大鹅草稿页结构,内部包含 `UI` 与 `背景音乐` 子 Tab。
|
||||
3. 素材配置:对齐抓大鹅草稿页结构,当前只包含 `UI` 子 Tab;`背景音乐` 子 Tab 已临时隐藏。
|
||||
|
||||
`素材配置 > UI` 展示并编辑拼图运行态 UI 背景提示词。`compile_puzzle_draft` 草稿编译完成首图和背景音乐后,`api-server` 会基于作品名称、作品描述、标签和首关信息自动生成首关 9:16 纯背景图;结果页继续支持用户修改提示词并通过 `generate_puzzle_ui_background` 重新生成。图片生成调用 VectorEngine `gpt-image-2-all` 的 `9:16` 图片生成链路。生成结果写入首关 `levels_json` 的 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`,不新增 SpacetimeDB 表字段。
|
||||
`素材配置 > UI` 展示并编辑拼图运行态 UI 背景提示词。`compile_puzzle_draft` 草稿编译完成首图后,`api-server` 会优先使用首关命名 LLM 同次返回的 `uiBackgroundPrompt` 自动生成首关 9:16 纯背景图;只有模型未返回可用提示词时,才基于作品名称、作品描述、标签和首关信息拼接兜底提示词。结果页继续支持用户修改提示词并通过 `generate_puzzle_ui_background` 重新生成。图片生成调用 VectorEngine `gpt-image-2-all` 的 `9:16` 图片生成链路。生成结果写入首关 `levels_json` 的 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`,不新增 SpacetimeDB 表字段。
|
||||
|
||||
`素材配置 > 背景音乐` 编辑并生成背景音乐,音乐资产暂存到首关 `levels_json[0].backgroundMusic`。拼图结果页不再保留一级 `UI` 或一级 `音乐` Tab。
|
||||
历史 `levels_json[0].backgroundMusic` 字段继续兼容读取和运行态播放,但结果页暂不提供编辑或生成入口。拼图结果页不再保留一级 `UI` 或一级 `音乐` Tab。
|
||||
|
||||
### 2026-05-12 UI 背景生成补充
|
||||
|
||||
1. UI 背景图只生成拼图运行态的题材氛围纯背景,不得把拼图槽、棋盘外框、按钮、HUD 或其它 UI 元素烘进图片。拼图槽位、棋盘边框和空格继续使用运行态默认样式绘制。
|
||||
2. UI 背景图不得生成文字、水印、按钮文字、数字、拼图碎片、完整拼图图像、教程浮层、拼图槽或物品槽,避免与真实拼图图块和运行态 HUD 混淆。
|
||||
3. 结果页 UI Tab 支持直接修改提示词并重新生成;点击生成前会把本地首关 `uiBackgroundPrompt` 同步进 `levelsJson`,使自动保存尚未完成时后端仍能拿到最新提示词。
|
||||
3. 结果页 UI Tab 支持直接修改提示词并重新生成;点击生成前必须先弹出 `确认消耗泥点` 面板并展示 `消耗 2 泥点`,确认后会把本地首关 `uiBackgroundPrompt` 同步进 `levelsJson`,使自动保存尚未完成时后端仍能拿到最新提示词。
|
||||
4. 草稿编译阶段自动生成 UI 背景失败时必须让 `compile_puzzle_draft` 失败并停留在生成页;不能保留一个成功状态但缺少 UI 背景的草稿。只有结果页内用户主动点击 `生成UI背景` 或 `重新生成` 时,失败才作为当前面板错误展示,由用户手动重试。
|
||||
5. `api-server` 负责拼接生成 prompt、调用 VectorEngine、下载并转存 OSS;SpacetimeDB 只通过 `save_puzzle_ui_background` procedure 保存结果,不做外部 I/O。
|
||||
6. 拼图运行态读取 `currentLevel.uiBackgroundImageSrc` 渲染为全屏背景;无 UI 背景图时继续使用原封面模糊背景兜底。棋盘本身仍由正式拼图图生成,不能把 UI 背景当作拼图切块来源。
|
||||
7. 生成完成后的自动试玩和结果页“试玩”走前端本地运行态兜底时,`startLocalPuzzleRun` 也必须从 `PuzzleWorkSummary.levels[]` 复制 `uiBackgroundImageSrc` 与 `backgroundMusic` 到 `currentLevel`;不得只带 `coverImageSrc`,否则草稿结果页有背景但试玩局内空白。
|
||||
6. 拼图运行态读取 `currentLevel.uiBackgroundImageSrc` 渲染为全屏背景;若 `uiBackgroundImageSrc` 为空,则必须回退读取 `uiBackgroundImageObjectKey` 并交给 `/api/assets/read-url` 换签;无 UI 背景图时继续使用原封面模糊背景兜底。棋盘本身仍由正式拼图图生成,不能把 UI 背景当作拼图切块来源。
|
||||
7. 生成完成后的自动试玩和结果页“试玩”走前端本地运行态兜底时,`startLocalPuzzleRun` 也必须从 `PuzzleWorkSummary.levels[]` 复制 `uiBackgroundImageSrc` / `uiBackgroundImageObjectKey` 与 `backgroundMusic` 到 `currentLevel`;不得只带 `coverImageSrc`,否则草稿结果页有背景但试玩局内空白。
|
||||
8. 结果页在本地关卡处于 `generationStatus = generating` 时合并后端生成完成回包,必须同时合并 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey` 与 `backgroundMusic`。若只合并图片字段而漏掉音乐字段,随后自动保存会把空 `backgroundMusic` 写回 `puzzle_work_profile.levels_json`,导致素材配置显示“暂无音乐”并让自动试玩/试玩局内无音乐。
|
||||
9. 结果页手动点击 `生成UI背景` / `重新生成` 时,如果目标 SpacetimeDB wasm 尚未发布到包含 `consume_profile_wallet_points_and_return`、`refund_profile_wallet_points_and_return` 或 `save_puzzle_ui_background` 的版本,`api-server` 必须把 `No such procedure` 视为后端版本漂移:泥点预扣阶段降级跳过扣费,UI 背景已经生成但保存失败时返回本次内存合成草稿快照,不能把原始 `No such procedure` 暴露到草稿页。该容错只保证当前页面可见本次图片;刷新后要稳定恢复仍必须重新发布最新 SpacetimeDB module 并重新生成 bindings。
|
||||
10. 结果页 `UI背景提示词` 文本框只能展示后端已持久化的 `levels[0].uiBackgroundPrompt` 或用户正在编辑的本地值;后端字段为空时不得把前端兜底提示词直接填入文本框,避免把兜底模板误展示成 AI 已生成提示词。点击生成时仍可用同一兜底提示词作为空输入兜底。
|
||||
|
||||
### 2026-05-12 草稿生成完成自动试玩补充
|
||||
|
||||
@@ -145,7 +147,7 @@
|
||||
1. 关卡详情页的 `画面图` 与 `画面描述` 模块对齐入口页拼图表单:画面图使用稳定正方形图卡,画面描述使用固定高度输入区并保留图片模型选择。
|
||||
2. 新建关卡或无正式图关卡也展示 `画面图` 图卡;空图态只保留图标化占位和生成中状态,不追加规则说明文案。
|
||||
3. 关卡详情页删除手填 `参考图链接或资产ID` 输入框。参考图只能通过本地上传或历史拼图素材选择进入本次生成请求;字段 `levels[].pictureReference` 继续作为后端生成后的复用字段透传,不作为用户可手填表单项。
|
||||
4. 单关生成等待估算从 `30` 秒调整为 `90` 秒;生成按钮内展示小字 `等待时间可以制作更多关卡哦~`,不得另起说明面板。
|
||||
4. 单关生成等待估算从 `30` 秒调整为 `90` 秒;生成按钮内展示小字 `等待时间可以制作更多关卡哦~`,不得另起说明面板。点击 `生成画面` / `重新生成画面` 前必须先弹出 `确认消耗泥点` 面板并展示 `消耗 2 泥点`。
|
||||
5. 触发某一关生成时,前端必须立即把该关 `generationStatus` 标为 `generating` 并随当前 `levelsJson` 写入草稿自动保存链路;后端生成完成后再写回 `ready`。
|
||||
6. `generationStatus = generating` 的关卡在详情弹窗关闭后仍保留进度展示,再次打开同一关详情能继续看到生成进度;关卡列表卡片也必须展示生成中的轻量状态。
|
||||
7. 单关图片生成必须作为后台 action 执行,不占用拼图结果页全局 busy 状态;生成期间仍允许编辑作品信息、编辑关卡、新增关卡、删除其他关卡、关卡测试和继续触发其他可并行动作。
|
||||
@@ -157,9 +159,9 @@
|
||||
## 验收
|
||||
|
||||
1. 从拼图创作入口只能看到作品名称、作品描述、画面描述和参考图上传,不出现 Agent 聊天输入、补齐设定、锚点问答。
|
||||
2. 点击确认后进入拼图草稿生成进度页,并自动完成草稿编译、首图生成、正式图选择、背景音乐生成和首关 UI 背景图生成。
|
||||
2. 点击确认后进入拼图草稿生成进度页,并自动完成草稿编译、首图生成、正式图选择和首关 UI 背景图生成。
|
||||
3. 首图生成请求使用玩家画面描述作为 prompt;上传参考图时走图生图;作品详情页展示玩家作品描述。
|
||||
4. 结果页包含“拼图关卡”“作品信息”“素材配置”三个一级 Tab;`素材配置` 内包含 `UI` 和 `背景音乐` 子 Tab。关卡列表默认至少一关,支持新增、删除和进入关卡详情。
|
||||
4. 结果页包含“拼图关卡”“作品信息”“素材配置”三个一级 Tab;`素材配置` 内当前只包含 `UI` 子 Tab,不展示背景音乐生成入口。关卡列表默认至少一关,支持新增、删除和进入关卡详情。
|
||||
5. 关卡详情页支持生成或重新生成画面;已有正式图后显示吸底“关卡测试”入口。
|
||||
6. 发布、作品测试、自动保存作品名称、作品描述、作品标签和关卡列表仍可用。
|
||||
7. 草稿初次生成后首关默认带 `uiBackgroundImageSrc`;UI Tab 可修改提示词并重新生成背景图;生成后运行态应显示 `uiBackgroundImageSrc`,拼图槽位和棋盘边界仍由默认运行态样式绘制。
|
||||
7. 草稿初次生成后首关默认带 `uiBackgroundImageSrc`;若后端只返回 `uiBackgroundImageObjectKey` 也必须能在结果页、试玩和运行态正常预览;UI Tab 可修改提示词并重新生成背景图;生成后运行态应显示 `uiBackgroundImageSrc` 或换签后的 `uiBackgroundImageObjectKey`,拼图槽位和棋盘边界仍由默认运行态样式绘制。
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
# 拼图与抓大鹅结果页音乐入口 2026-05-11
|
||||
|
||||
> 2026-05-14 临时关闭:拼图与抓大鹅草稿生成阶段不再自动生成背景音乐;拼图结果页 `素材配置` 只保留 `UI` 子 Tab;抓大鹅结果页 `素材配置` 只保留 `物品` 与 `UI` 子 Tab,物品详情不展示点击音效生成入口。通用 `/api/creation/audio/*` 路由对拼图与抓大鹅目标暂时返回 `410 Gone`;视觉小说专用音频路由不受本次关闭影响。
|
||||
|
||||
## 0. 2026-05-14 临时关闭口径
|
||||
|
||||
拼图与抓大鹅的创作音频生成入口暂时关闭:
|
||||
|
||||
1. 拼图草稿编译阶段不再自动生成背景音乐,只生成首图与 UI 背景。
|
||||
2. 抓大鹅草稿编译阶段不再自动生成背景音乐,也不再生成物品点击音效。
|
||||
3. 拼图结果页 `素材配置` 只保留 `UI` 子 Tab,隐藏 `背景音乐` 子 Tab 与重新生成入口。
|
||||
4. 抓大鹅结果页 `素材配置` 只保留 `物品` 与 `UI` 子 Tab,物品详情隐藏点击音效生成入口。
|
||||
5. 通用 `/api/creation/audio/*` 路由对拼图与抓大鹅目标暂时返回 `410 Gone`;视觉小说专用音频路由保持可用。
|
||||
6. 既有草稿或作品中的 `backgroundMusic` / `clickSound` 字段不清空,运行态仍按历史兼容逻辑播放已存在的音频。
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案把 VectorEngine 音频生成能力从视觉小说结果页扩展到拼图与抓大鹅结果页:
|
||||
本方案记录 VectorEngine 音频生成能力曾从视觉小说结果页扩展到拼图与抓大鹅结果页;当前拼图与抓大鹅入口已临时关闭:
|
||||
|
||||
1. 拼图结果页在 `素材配置 > 背景音乐` 中支持通过 Suno 生成作品背景音乐;旧一级 `音乐` Tab 已删除。
|
||||
2. 抓大鹅结果页在 `素材配置 > 背景音乐` 中支持通过 Suno 生成作品背景音乐;旧一级 `音乐` Tab 已删除。
|
||||
3. 抓大鹅 `素材配置 > 物品` 支持为每个生成物体通过 Vidu 手动生成点击音效;创作入口不展示点击音效生成开关。
|
||||
1. 拼图结果页不展示 `素材配置 > 背景音乐`,旧一级 `音乐` Tab 继续删除。
|
||||
2. 抓大鹅结果页不展示 `素材配置 > 背景音乐`,旧一级 `音乐` Tab 继续删除。
|
||||
3. 抓大鹅 `素材配置 > 物品` 详情面板不展示点击音效生成入口。
|
||||
4. 拼图运行态与抓大鹅运行态内置默认关卡音频配置:通用点击音效 `/audio/ui-click-soft.wav`、过关音效 `/audio/ui-level-clear.wav`、倒计时临界音效 `/audio/ui-countdown-warning.wav`。
|
||||
5. 拼图和抓大鹅草稿生成阶段会自动生成背景音乐并转存 OSS,结果页继续支持试听和重新生成。
|
||||
5. 拼图和抓大鹅草稿生成阶段不生成背景音乐,也不因缺少背景音乐阻塞草稿完成。
|
||||
|
||||
本轮不新增 SpacetimeDB 表,不修改表字段,不把供应商密钥下发到前端。
|
||||
|
||||
## 2. 通用音频接口
|
||||
|
||||
后端在既有视觉小说音频路由外新增通用创作音频路由:
|
||||
后端在既有视觉小说音频路由外曾新增通用创作音频路由。临时关闭期间,以下通用路由保留;对拼图或抓大鹅目标直接返回 `410 Gone`,不得被拼图或抓大鹅入口继续调用:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
@@ -32,7 +45,7 @@
|
||||
5. 确认 `asset_object` 并绑定 `asset_entity_binding`。
|
||||
6. 音频真正可下载并准备转存时,按 `taskId + assetKind + entityId + slot` 幂等扣费;背景音乐固定扣除 `5` 泥点,物品点击音效固定扣除 `10` 泥点。任务仍在处理中不扣费,转存或资产绑定失败自动退款。
|
||||
|
||||
通用背景音乐提交允许 `prompt = ""`。拼图和抓大鹅草稿生成都按纯音乐处理:后端提交 Suno 时固定带 `make_instrumental = true`,只用 `title` 和 `tags` 约束作品气质,不把歌词或规则描述写入 prompt。视觉小说原路由保持兼容,内部继续复用同一套提交、轮询、转存逻辑。
|
||||
通用背景音乐提交历史上允许 `prompt = ""`。当前通用创作音频入口被后端显式熔断,不能通过该路由继续提交或转存音频。视觉小说专用路由保持兼容,内部继续复用同一套提交、轮询、转存逻辑。
|
||||
|
||||
## 3. 数据落点
|
||||
|
||||
@@ -53,7 +66,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
草稿生成阶段在生成首关作品题目后,使用作品题目作为 Suno `title`,`prompt` 为空,`tags` 使用轻快、拼图、循环、instrumental。自动草稿编译必须拿到可播放的 `backgroundMusic.audioSrc` 后才返回成功;生成失败应返回上游错误并停留在生成页,不能进入结果页后显示“暂无音乐”。运行态从 `PuzzleRuntimeLevelSnapshot.backgroundMusic.audioSrc` 读取该字段作为背景音乐源,游戏开始后自动循环播放;历史草稿或手动删除音乐时字段为空,则保持静默背景音乐兜底。
|
||||
当前草稿生成阶段跳过背景音乐生成;自动草稿编译只校验首图与 UI 背景。运行态仍兼容历史 `PuzzleRuntimeLevelSnapshot.backgroundMusic.audioSrc`,旧草稿若已有音乐可继续播放;新草稿默认保持静默背景音乐兜底。
|
||||
|
||||
### 3.2 抓大鹅
|
||||
|
||||
@@ -62,18 +75,16 @@
|
||||
1. 作品背景音乐暂存到第一个 `Match3DGeneratedItemAsset.backgroundMusic`,表示当前 work profile 的作品级背景音乐。
|
||||
2. 单个物体点击音效保存到对应 `Match3DGeneratedItemAsset.clickSound`。
|
||||
|
||||
这是一个兼容性折中:当前 Match3D work profile 没有 work-level metadata 字段,而 `generated_item_assets_json` 已经随作品详情、草稿架、运行态入口稳定传递。草稿生成阶段的文本计划在生成物品名称时同步生成 `backgroundMusic.title` 作为背景音乐名称,`backgroundMusic.prompt` 固定为空字符串,后端用该名称作为 Suno `title` 并生成纯音乐。自动草稿编译必须把生成后的 `backgroundMusic.audioSrc` 写回首个素材后才返回成功;若 Suno 提交、轮询、下载、OSS 转存或资产绑定失败,本次草稿生成返回失败并允许用户重试同一 session/profile。后续若新增正式作品 metadata 表达,应迁移 `backgroundMusic` 到作品级字段。
|
||||
这是一个兼容性折中:当前 Match3D work profile 没有 work-level metadata 字段,而 `generated_item_assets_json` 已经随作品详情、草稿架、运行态入口稳定传递。当前草稿生成阶段不再生成 `backgroundMusic` 或 `clickSound`;历史草稿中已有字段仍按旧兼容逻辑传递到运行态。后续若重新打开音频能力或新增正式作品 metadata 表达,应迁移 `backgroundMusic` 到作品级字段。
|
||||
|
||||
## 4. 前端交互
|
||||
|
||||
结果页 UI 保持轻量:
|
||||
|
||||
1. 拼图 `素材配置 > 背景音乐` 与抓大鹅 `素材配置 > 背景音乐` 只展示必要输入、生成按钮、状态与音频预览,不展示供应商规则说明。
|
||||
1. 拼图与抓大鹅结果页暂不展示背景音乐生成输入、生成按钮、状态和音频预览。
|
||||
2. 生成完成后立即写回本地草稿状态,并触发既有保存链路或专用保存接口。
|
||||
3. 抓大鹅每个物体音效生成入口放在对应素材详情面板内,不在列表下方展开大段配置。
|
||||
4. 抓大鹅物体音效提示词允许在素材详情面板内编辑;背景音乐只允许在 `素材配置 > 背景音乐` 编辑曲名和风格,生成请求固定使用空 `prompt`。
|
||||
5. 背景音乐和物体音效生成期间都显示进度条,生成完成后展示 audio 控件试听。
|
||||
6. 背景音乐重新生成只要求曲名非空;重新生成继续按纯音乐提交,`prompt = ""`,按钮展示 `5` 泥点成本。
|
||||
3. 抓大鹅每个物体详情面板只保留素材查看和名称编辑,不展示音效提示词或生成入口。
|
||||
4. 若历史素材已有 `backgroundMusic` 或 `clickSound`,结果页不提供重新生成入口;试玩和运行态仍可消费已存在字段。
|
||||
|
||||
### 4.1 运行态默认点击音效
|
||||
|
||||
@@ -89,11 +100,11 @@
|
||||
|
||||
2026-05-13 修正:
|
||||
|
||||
1. 拼图 `素材配置 > 背景音乐` 的试听控件也必须通过 `useResolvedAssetReadUrl` 对 generated legacy path 换签后再设置 `<audio src>`;签名未就绪或失败时只显示“音频已绑定”,不得把裸 `/generated-puzzle-assets/...` 路径交给浏览器请求。
|
||||
2. 抓大鹅结果页已使用同一换签口径,后续新增音频试听入口必须复用该模式。
|
||||
1. 拼图 `素材配置 > 背景音乐` 的试听控件当前隐藏;后续恢复时必须通过 `useResolvedAssetReadUrl` 对 generated legacy path 换签后再设置 `<audio src>`。
|
||||
2. 抓大鹅结果页后续恢复音频试听入口时必须复用同一换签口径,不能把裸 `/generated-match3d-assets/...` 音频路径直接交给 `<audio>`。
|
||||
3. 拼图和抓大鹅运行态在开局时会尝试自动播放背景音乐;若浏览器因自动播放策略拒绝,玩家首次按下拼图块或点击抓大鹅物品时必须再次调用同一个背景音乐播放函数,避免草稿音乐已经传入运行态但局内始终无声。
|
||||
4. 播放失败仍只做静默兜底,不弹出规则说明或阻断局内交互。
|
||||
5. 拼图结果页合并后端生成完成回包时,若本地首关仍处于 `generationStatus = generating`,必须把 `backgroundMusic` 与候选图、正式图、UI 背景一起合并进编辑态;否则音乐面板会继续显示“暂无音乐”,后续自动保存还会把空音乐写回 profile。
|
||||
5. 拼图结果页合并后端生成完成回包时,若本地首关仍处于 `generationStatus = generating`,必须保留回包中的历史 `backgroundMusic` 字段,避免自动保存把旧音频覆盖为空;该字段不再驱动可见音乐面板。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
拼图创作入口继续保留填表式体验,但入口表单不再要求陶泥儿主提前填写作品名称和作品描述。入口只收集“拼图画面描述”,后端用该描述完成首图生成和第一关关卡名生成;进入结果页后再补作品信息。
|
||||
|
||||
2026-05-14 起,拼图草稿阶段的背景音乐生成临时关闭,结果页也不再展示背景音乐入口。
|
||||
|
||||
## 入口表单
|
||||
|
||||
1. 点击“开始创作”后的拼图表单只展示 `画面描述`、参考图和图片模型选择。
|
||||
@@ -17,15 +19,15 @@
|
||||
1. `compile` 展示为“编译首关草稿”:建立结果页草稿,不在本步骤生成作品标签。
|
||||
2. `puzzle-level-name` 展示为“生成关卡名称”:按画面描述生成文本名,首图返回后可再基于图像语义精修最终关卡名。
|
||||
3. `puzzle-images` 展示为“生成首关画面”:按画面描述、参考图和当前图片模型生成第一张拼图图;后端允许该步骤与 `puzzle-level-name` 文本名生成并行。
|
||||
4. `puzzle-background-music` 展示为“生成背景音乐”:最终关卡名确定后生成纯音乐并转存音频资产。
|
||||
5. `puzzle-ui-background` 展示为“生成UI背景”:最终关卡名确定后生成 9:16 纯背景图;后端必须与背景音乐并行启动。
|
||||
6. `puzzle-select-image` 展示为“写入正式草稿”:把首图、最终关卡名、可用音乐和可用 UI 背景同步到结果页草稿。
|
||||
4. 背景音乐步骤当前临时隐藏,不再执行。
|
||||
5. `puzzle-ui-background` 展示为“生成UI背景”:最终关卡名确定后生成 9:16 纯背景图。
|
||||
6. `puzzle-select-image` 展示为“写入正式草稿”:把首图、最终关卡名和可用 UI 背景同步到结果页草稿。
|
||||
7. `ready` 文案提示进入结果页补作品信息;不得暗示作品名称、作品描述或作品标签已经完整生成。
|
||||
|
||||
### 2026-05-08 进度页预计等待与步骤动效补充
|
||||
|
||||
1. 拼图草稿生成进度页预计等待时间固定按 `60` 秒展示和倒计时,后端真实完成后立即进入结果页,不强制等满 60 秒。
|
||||
2. 前端进度按本地时间展示为多段估算;后端真实编排中,首关名称文本生成与首关画面生成可并行,背景音乐与 UI 背景在最终关卡名确定后并行生成。进度条只是等待接口返回时的体验估算,不作为后端任务调度依据。
|
||||
2. 前端进度按本地时间展示为多段估算;后端真实编排中,首关名称文本生成与首关画面生成可并行,UI 背景在最终关卡名确定后生成。进度条只是等待接口返回时的体验估算,不作为后端任务调度依据。
|
||||
3. 生成中即使后端 `compile_puzzle_draft` 仍是一次同步 action,前端也必须按本地计时推进总进度和当前步骤进度,避免页面停在静态等待态。
|
||||
4. 每个步骤卡片都展示独立进度条;已完成步骤显示 100%,当前步骤按该段预计时长推进,后续步骤保留 0% 待处理状态。
|
||||
5. 后端未返回前总进度最多推进到 98%,防止 UI 提前宣称生成完成;只有 action 成功并写回 `ready` 后才显示 100%。
|
||||
|
||||
@@ -98,7 +98,7 @@ size = 1024x1024
|
||||
- 已上传图片时,输入框标题为 `画面AI重绘要求(提示词)`。
|
||||
- 展示图片模型切换。
|
||||
- `compile_puzzle_draft` 携带 `aiRedraw: true`,继续走 VectorEngine 生图与 `PUZZLE_IMAGE_GENERATION_POINTS_COST = 2` 扣费链路。
|
||||
- 生成按钮展示 `消耗2泥点`。
|
||||
- 生成按钮展示 `消耗2泥点`,点击后必须先弹出 `确认消耗泥点` 面板并展示 `消耗 2 泥点`,用户确认后才调用生成动作。
|
||||
2. `AI重绘=false`
|
||||
- 隐藏画面描述输入框和模型切换。
|
||||
- 必须上传拼图图片,按钮不展示 `消耗2泥点`。
|
||||
@@ -126,7 +126,7 @@ Rust 共享契约使用 `ai_redraw: Option<bool>` 并按 camelCase 序列化为
|
||||
3. 图片模型切换仍可打开并选择 `gpt-image-2` / `nanobanana2`。
|
||||
4. 历史模板样例图文件可保留,但不出现在拼图入口表单。
|
||||
5. 当前创作 Tab 顶部的拼图、方洞挑战、视觉小说和 AIRP 卡片能看到对应 `creation-type-references` 图片。
|
||||
6. 默认 `AI重绘` 打开时,无图状态展示 `画面描述` 与 `消耗2泥点`;上传图片后输入框标题改为 `画面AI重绘要求(提示词)`。
|
||||
6. 默认 `AI重绘` 打开时,无图状态展示 `画面描述` 与 `消耗2泥点`;上传图片后输入框标题改为 `画面AI重绘要求(提示词)`;点击生成前必须确认本次 `2` 泥点消耗。
|
||||
7. 关闭 `AI重绘` 后隐藏画面描述输入框,生成按钮不展示 `消耗2泥点`,后端直接应用上传图片为第一关图片。
|
||||
8. 上传非 1:1 图片时必须先通过拖拽裁剪框完成正方形裁剪。
|
||||
9. gpt-image-2 Skill 校验通过,且脚本 dry-run 能输出计划请求而不泄露密钥。
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md](./【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md):冻结 api-server 能力模块化、生成资产 Adapter、复杂媒体链路和大 Handler 瘦身的总边界,明确多 agent 并行 owner、禁止改动范围、阶段退出条件与验证命令。
|
||||
- [【后端架构】api-server路由能力模块化执行计划-2026-05-14.md](./【后端架构】api-server路由能力模块化执行计划-2026-05-14.md):记录 app.rs 路由按 admin/auth/assets/platform/creation/runtime/profile/story 等能力迁入 modules router 的执行计划,要求 route path、method、middleware 和 handler contract 不变。
|
||||
- [【后端架构】生成图片资产Adapter收口执行计划-2026-05-14.md](./【后端架构】生成图片资产Adapter收口执行计划-2026-05-14.md):记录 Big Fish、Square Hole、Custom World 生成图片的 provider 归一、下载/base64 解码、OSS、asset_object confirm 和 entity binding 收口计划。
|
||||
- [【后端架构】复杂媒体资产链路Adapter扩展计划-2026-05-14.md](./【后端架构】复杂媒体资产链路Adapter扩展计划-2026-05-14.md):记录音频、视频、角色工作流、Hyper3D/GLB 等复杂媒体只复用媒体持久化底座、不污染图片 Adapter 的扩展计划。
|
||||
- [【后端架构】api-server大Handler瘦身执行计划-2026-05-14.md](./【后端架构】api-server大Handler瘦身执行计划-2026-05-14.md):记录 api-server 大 handler 拆成 router、handlers、application、assets、mapper、errors 的执行计划,明确不改 contract、schema、计费和领域规则。
|
||||
- [WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md](./WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md):记录微信小程序 `web-view` 壳的最小接入范围、需要填写的 H5 业务域名、微信后台配置、`npm run check:wechat-miniprogram-auth` 可重复登录链路 smoke 和后续原生化边界。
|
||||
- [BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md](./BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md):冻结寓教于乐 `宝贝爱画` 独立本地 Demo 运行态实现方案,明确发现页默认卡片、`/runtime/baby-love-drawing` 路由、画板交互、mocap/键鼠调试映射、本地保存和 VectorEngine image-2 绘画魔法后端代理。
|
||||
- [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”后端 DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界、shared contracts、作品配置、runtime run、派生成绩、排行榜、`work_play_start` 埋点、migration/绑定生成策略,以及不保存原始麦克风音频的隐私与反作弊约束。
|
||||
- [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”2D 浏览器 runtime 技术方案,明确 Phaser + TypeScript + Vite 选型、纯 TS simulation 与 Phaser renderer/DOM HUD 边界、Web Audio 输入适配、移动端权限降级和后续测试验证命令。
|
||||
- [PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md](./PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md):记录直接访问公开作品详情深链时作品不存在或已下架的回首页修复,避免关闭提示后停在 `work-detail` 空状态白屏。
|
||||
@@ -27,7 +23,7 @@
|
||||
- [AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md](./AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md):记录 `AuthGate` 登录成功后又被旧 hydrate 覆盖回未登录态的竞态根因、版本号保护修复与回归测试。
|
||||
- [HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md](./HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md):记录 Hyper3D Rodin Gen-2 文生 3D 模型、图生 3D 模型、状态查询和下载列表的后端代理、环境变量、请求约束与验收边界。
|
||||
- [MATCH3D_RODIN_ASSET_TAB_2026-05-10.md](./MATCH3D_RODIN_ASSET_TAB_2026-05-10.md):历史记录抓大鹅 Rodin 3D 素材列表/详情页的早期接入边界;当前新草稿不再调用 Rodin 或生成 GLB。
|
||||
- [MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md](./MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md):冻结抓大鹅草稿生成过程页、按题材生成 UI 背景提示词、VectorEngine 2D 五视角素材、UI 背景图、背景音乐和 OSS 回填草稿页的端到端边界。
|
||||
- [MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md](./MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md):冻结抓大鹅草稿生成过程页、按题材生成 UI 背景提示词、VectorEngine 2D 五视角素材、UI 背景图和 OSS 回填草稿页的端到端边界;背景音乐和点击音效生成当前临时关闭。
|
||||
- [VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md](./VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md):记录火山引擎大模型 ASR 双向流式、TTS WebSocket 双向流式和 TTS HTTP SSE 单向流式的后端代理、环境变量、协议帧和验收边界。
|
||||
- [VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md](./VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md):记录视觉小说结果页接入 VectorEngine Suno 文生背景音乐与 Vidu 文生音效的接口、环境变量、后端路由、OSS 资产回写和前端弹层交互边界。
|
||||
- [PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md](./PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md):冻结“我的”页签帮助与反馈入口的后端接入方案,覆盖 `POST /api/profile/feedback`、`profile_feedback_submission`、凭证图片 Data URL 校验和前端预览/提交边界。
|
||||
|
||||
@@ -19,18 +19,18 @@
|
||||
|
||||
`useMocapInput` 解析 mocap `hands[].landmarks` 时应优先用 MediaPipe 21 点里的 `wrist / index_mcp / middle_mcp / ring_mcp / pinky_mcp` 加权计算掌心派生点;少于 3 个掌心关键点时才回退到 `wrist` 或直出 `hand.x/y`。这样运行态光标不会直接贴在腕部或指尖。
|
||||
|
||||
拼图运行态已接入该层:
|
||||
2026-05-14 临时关闭:拼图运行态不再调用 `useMocapInput`,也不渲染 mocap 光标或调试面板。`src/services/input-devices/` 仍保留为鼠标 / 触控等输入的统一拖拽状态机;mocap 解析能力继续供儿童动作 Demo 和宝贝识物等明确需要体感输入的玩法使用。
|
||||
|
||||
拼图运行态当前接入该层:
|
||||
|
||||
- 鼠标/触控 `pointerdown / pointermove / pointerup` 进入同一个 drag controller。
|
||||
- mocap `grab` 进入同一个 drag controller,并强制使用持续拖拽语义。
|
||||
- mocap 光标按 60Hz 插值更新 UI 位置,并在拖拽中用插值后的当前点持续驱动输入层,避免输入包帧率低或抖动时出现明显跳变。
|
||||
- 合并大块由拼图运行态把手部坐标命中到任一成员拼块;本地拼图运行时再按 `mergedGroupId` 执行整组平移。
|
||||
- 合并大块由拼图运行态把指针坐标命中到任一成员拼块;本地拼图运行时再按 `mergedGroupId` 执行整组平移。
|
||||
|
||||
## 调试模式
|
||||
|
||||
前端全局调试模式统一通过 `src/config/debugMode.ts` 判断。默认跟随 Vite 开发态:`import.meta.env.DEV` 为真时开启,生产构建默认关闭;如需显式覆盖,可设置 `VITE_DEBUG_MODE=true` 或 `VITE_DEBUG_MODE=false`。
|
||||
|
||||
拼图运行态的 mocap 调试面板只在全局调试模式下渲染。面板默认折叠,只保留一行连接状态,展开后才显示动作、手势、解析告警和原始包预览,避免开发诊断信息遮挡拼图棋盘和底部操作。
|
||||
拼图运行态不再渲染 mocap 调试面板。需要排查 mocap 数据源时,应在仍消费 `useMocapInput` 的儿童动作 Demo 或宝贝识物运行态中验证数据链路。
|
||||
|
||||
## 接入规则
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
2. 设备适配层只负责把原始输入转换成通用输入事件。
|
||||
3. 玩法壳层负责从通用输入点解析本玩法目标,例如拼块、洞口、角色或实体。
|
||||
4. 玩法壳层负责决定 drop 后调用哪个本地运行态函数或后端接口。
|
||||
5. 需要取消输入时优先按 `inputId` 取消,避免 mocap 丢帧误伤正在进行的鼠标/触控会话。
|
||||
5. 需要取消输入时优先按 `inputId` 取消,避免某一输入设备误伤正在进行的鼠标/触控会话。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ npm run dev:rust
|
||||
|
||||
1. `npm run dev` / `npm run dev:rust` 会先检查 SpacetimeDB、Rust `api-server`、主站 Vite、后台 Vite 需要使用的端口。
|
||||
2. 如果优先端口不可用,脚本会从该端口开始向后寻找可用端口,并将解析后的端口覆盖到后续 `spacetime start`、`spacetime publish --server`、`GENARRATIVE_API_PORT`、`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET`、`ADMIN_API_TARGET` 与 Vite 启动参数。
|
||||
3. 控制台会打印 `[dev:ports] ... 可用` 或 `[dev:ports] ... 不可用,改用 ...`,排查代理错配时以该日志和后续 `[dev:rust] web/admin web/rust api/spacetime` 实际地址为准。
|
||||
4. 单独 `npm run dev:web` 也会检查主站 Vite 端口;`WEB_PORT` 或默认 `3000` 不可用时,会自动切到后续可用端口并继续严格端口启动。
|
||||
3. 显式传入 `--skip-spacetime` 时,脚本不会对 SpacetimeDB 端口做可用性漂移;此时 `--spacetime-port` 表示要复用的既有 SpacetimeDB 地址。后端会直接使用该地址,避免 `3101` 已有可用宿主时被误改到空闲但未启动的 `3102`。
|
||||
4. 控制台会打印 `[dev:ports] ... 可用` 或 `[dev:ports] ... 不可用,改用 ...`,排查代理错配时以该日志和后续 `[dev:rust] web/admin web/rust api/spacetime` 实际地址为准。
|
||||
5. 单独 `npm run dev:web` 也会检查主站 Vite 端口;`WEB_PORT` 或默认 `3000` 不可用时,会自动切到后续可用端口并继续严格端口启动。
|
||||
|
||||
默认流程:
|
||||
|
||||
@@ -48,7 +49,7 @@ npm run dev:rust
|
||||
5. 如果确认需要新启动 SpacetimeDB,脚本会先检测 `127.0.0.1:3101` 是否可监听;若已占用,输出占用进程并选择从 `3101` 起向后的最近可用端口,再执行 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr <实际地址>`。启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`,后续 publish 与 `api-server` 都使用同一个实际 URL。
|
||||
6. 等待 SpacetimeDB 就绪:优先接受 `spacetime server ping http://127.0.0.1:<spacetime-port>` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:<spacetime-port>/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`。
|
||||
7. 执行 `spacetime publish <本地数据库名> --server <实际 SpacetimeDB URL> --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
8. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
8. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。本地启动器会保留终端实时输出,并把同一份 `cargo` / `api-server` 输出持久化到 `logs/api-server/`。
|
||||
9. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`。
|
||||
10. 注入 `RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
11. 任一子进程退出时,脚本回收其余子进程。
|
||||
@@ -70,7 +71,7 @@ Vite 代理覆盖范围:
|
||||
|
||||
本地联调跳过策略:
|
||||
|
||||
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,脚本会优先按 `spacetime.pid` 与 `dev-rust-spacetime-url` 复用该宿主;如果确认不是可复用宿主,则会先输出占用进程并选择最近可用端口。也可显式使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,或用 `--spacetime-port` 指定起始探测端口。
|
||||
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,脚本会优先按 `spacetime.pid` 与 `dev-rust-spacetime-url` 复用该宿主;如果确认不是可复用宿主,则会先输出占用进程并选择最近可用端口。也可显式使用 `npm run dev -- --skip-spacetime --spacetime-port 3101` 跳过 SpacetimeDB 宿主启动并复用 `http://127.0.0.1:3101`。在 `--skip-spacetime` 模式下,`--spacetime-port` 不是起始探测端口,而是必须已经在线的目标端口。
|
||||
2. 如果当前没有修改 `server-rs/crates/spacetime-module`,可使用 `npm run dev -- --skip-publish` 跳过数据库发布,降低本地启动时的 SpacetimeDB wasm 编译耗时。
|
||||
3. 如果当前阶段只需要检查 `spacetime-module` 语法,不需要重新发布本地数据库,可执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。该命令只做 Rust 编译检查,不生成新数据库,也不刷新 bindings。
|
||||
|
||||
@@ -119,6 +120,12 @@ npm run dev:rust:logs -- --follow
|
||||
3. 默认输出到 `logs/spacetime/<database>-<timestamp>.log`,并通过 `tee` 同步显示在终端。
|
||||
4. `--follow` 仅用于本地追踪,会持续追加到同一个输出文件;停止时用 `Ctrl+C`。
|
||||
|
||||
api-server 本地持久化日志:
|
||||
|
||||
1. `npm run api-server` 默认写入 `logs/api-server/api-server-<timestamp>.log`,同时继续把同一份输出显示在当前终端。
|
||||
2. `npm run dev` / `npm run dev:rust` 中由脚本启动的 Rust `api-server` 默认写入 `logs/api-server/api-server-dev-rust-<timestamp>.log`;等待 `/healthz` 失败时,脚本会自动输出该日志最后 80 行。
|
||||
3. 如需固定日志文件,可设置 `GENARRATIVE_API_SERVER_LOG_FILE=logs/api-server/local.log`;如只需更换目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR=logs/api-server`。相对路径都按仓库根目录解析。
|
||||
|
||||
联调排错补充:
|
||||
|
||||
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# api-server 大 Handler 瘦身执行计划
|
||||
|
||||
状态:D2 首批已落地;Big Fish、Square Hole、Custom World AI、Match3D、Puzzle、Custom World 已完成低风险 mapper/tag/asset glue 拆分,后续继续按 owner 深化 application/errors 拆分
|
||||
日期:2026-05-14
|
||||
范围:只拆分 `server-rs/crates/api-server` 内大 handler 文件的内部职责;不改 HTTP contract、DTO、SpacetimeDB schema、module-* 领域规则、前端行为、计费语义和 provider 策略。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在 `app.rs` 路由装配迁入 `modules/*/router.rs` 后,继续把超大 handler 文件拆成清晰层次,避免 HTTP 解析、应用编排、资产持久化、DTO mapper、错误映射和玩法策略继续堆在单文件。
|
||||
|
||||
目标分层:
|
||||
|
||||
```text
|
||||
router.rs 只挂 route 与 middleware
|
||||
handlers.rs 只做 Axum extractor、鉴权上下文、Json/Sse response envelope
|
||||
application.rs 编排 spacetime-client facade、platform provider、asset adapter、计费 wrapper
|
||||
assets.rs 当前能力私有的 asset request 构造与 adapter 调用 glue
|
||||
mapper.rs HTTP DTO <-> application input/output 映射
|
||||
errors.rs 当前能力到 AppError/envelope 的映射
|
||||
```
|
||||
|
||||
本阶段不是 DDD 领域重构。凡是玩法裁决、实体规则、钱包语义、表结构语义已经属于 `module-*`、`spacetime-module` 或 `spacetime-client` 的,不得搬回 `api-server`。
|
||||
|
||||
## 2. Owner 与禁止改动范围
|
||||
|
||||
Owner:D2 大 Handler 瘦身 agent。建议一次只领取一个能力 owner,避免和 B/C/D1 冲突。
|
||||
|
||||
首批候选 owner:
|
||||
|
||||
- `big_fish.rs`
|
||||
- `square_hole.rs`
|
||||
- `custom_world_ai.rs` / `custom_world.rs`
|
||||
- `puzzle.rs`
|
||||
- `match3d.rs`
|
||||
- `visual_novel.rs`
|
||||
- `character_visual_assets.rs`
|
||||
- `character_animation_assets.rs`
|
||||
- `vector_engine_audio_generation.rs`
|
||||
|
||||
禁止本阶段修改:
|
||||
|
||||
- route path、HTTP method、handler 函数对外 contract、DTO 字段和 error envelope。
|
||||
- `shared-contracts` 公开类型,除非另开 contract owner 文档。
|
||||
- `spacetime-module` schema/procedure 和 Rust bindings。
|
||||
- `module-*` 领域规则和命令语义。
|
||||
- `asset_billing.rs` 的扣费/退款策略。
|
||||
- 图片 Adapter、复杂媒体 Adapter 的公共接口;只允许调用,不允许在瘦身切片里顺手改接口。
|
||||
- 前端调用路径、页面行为和测试快照。
|
||||
|
||||
## 3. 拆分原则
|
||||
|
||||
### 3.1 Handler 只做 HTTP 边界
|
||||
|
||||
允许保留:
|
||||
|
||||
- Axum extractor:`State`、`Extension`、`Path`、`Query`、`Json`。
|
||||
- 请求体基础解析和鉴权上下文读取。
|
||||
- 调用 application service。
|
||||
- 统一包装 `Json(ApiResponse<T>)`、SSE 或 `AppError`。
|
||||
|
||||
禁止保留:
|
||||
|
||||
- provider task create/poll/download 细节。
|
||||
- OSS object key 拼接、MIME 推断、asset_object confirm、entity binding。
|
||||
- 大段 SpacetimeDB row JSON mapper。
|
||||
- 玩法规则判断、发布门槛、运行态裁决。
|
||||
|
||||
### 3.2 Application 只做编排
|
||||
|
||||
Application 层可以:
|
||||
|
||||
- 调 `spacetime-client` facade。
|
||||
- 调 `platform-*` provider client 或既有 provider helper。
|
||||
- 调图片/复杂媒体 Adapter。
|
||||
- 维持既有计费 wrapper 的调用位置。
|
||||
- 组装 application output。
|
||||
|
||||
Application 层不允许新增领域真相;需要新增领域规则时必须暂停并拆给对应 `module-*` owner。
|
||||
|
||||
### 3.3 Mapper 可独立测试
|
||||
|
||||
Mapper 拆出后应优先覆盖:
|
||||
|
||||
- row snapshot JSON 到 HTTP response 的兼容字段。
|
||||
- legacy ID/path/kind/slot 字符串映射。
|
||||
- null/缺字段 fallback 的既有行为。
|
||||
|
||||
## 4. 能力级执行顺序
|
||||
|
||||
### D2-0:基线扫描
|
||||
|
||||
每领取一个文件先记录:
|
||||
|
||||
- 当前公开 route 与 handler 名称。
|
||||
- 当前 provider/asset/计费/spacetime-client 调用点。
|
||||
- 当前已有测试命令和缺口。
|
||||
- 与 B/C/D1 正在修改的文件是否冲突。
|
||||
|
||||
退出条件:只写本地执行 notes 或更新本文件状态,不改 Rust 行为。
|
||||
|
||||
### D2-1:低风险纯移动
|
||||
|
||||
- 先拆 `types.rs`/`mapper.rs`/`errors.rs` 中纯类型和纯函数。
|
||||
- 不改函数签名,不改错误文案。
|
||||
- 每次移动后跑 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||
|
||||
退出条件:git diff 显示主要是 move/extract;无业务逻辑重写。
|
||||
|
||||
### D2-2:资产 glue 下沉
|
||||
|
||||
- 对已接入图片 Adapter 或复杂媒体 Adapter 的能力,把 request 构造放到能力私有 `assets.rs`。
|
||||
- 删除 handler 内重复 OSS/confirm/binding 代码。
|
||||
- 保留计费外层在原 application 编排位置。
|
||||
|
||||
退出条件:调用方仍显式传入 asset kind/entity kind/slot;Adapter 不反向知道玩法规则。
|
||||
|
||||
### D2-3:Application service 固化
|
||||
|
||||
- 每个能力暴露少量 `pub(crate)` application 函数。
|
||||
- handler 不直接调多个 facade/provider/adapter。
|
||||
- 对 SSE handler 保持流式语义,不包成非流式完整字符串。
|
||||
|
||||
退出条件:handler 文件行数显著下降;provider/asset/spacetime 调用集中在 application/assets 层。
|
||||
|
||||
### D2-4:清理与索引更新
|
||||
|
||||
- 删除无用私有 helper。
|
||||
- 更新对应技术文档状态。
|
||||
- 如果新建能力目录,确保 `mod.rs` 导出最小化。
|
||||
|
||||
退出条件:无未使用代码、无重复 helper、README/TODO 状态同步。
|
||||
|
||||
## 5. 文件级 owner 建议
|
||||
|
||||
| 能力 | 建议目标目录 | 可并行边界 | 退出条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| Big Fish | `modules/runtime/big_fish/` | 不碰 Square Hole/Puzzle;图片 Adapter 接口由 C 线 owner 控制 | 正式图 asset glue 不在 handler;SSE/works/gallery route contract 不变 |
|
||||
| Square Hole | `modules/creation/square_hole/` 与 `modules/runtime/square_hole/` | 不改 Puzzle 图片规则 | 图片重生成 fallback Data URL 行为不变 |
|
||||
| Custom World | `modules/runtime/custom_world/` | opening CG 视频等复杂媒体与 D1 协调 | profile/entity/scene/cover/opening route contract 不变 |
|
||||
| Puzzle | `modules/runtime/puzzle/` | 不改前端即时运行态规则 | 运行态后端真相接口和排行榜语义不变 |
|
||||
| Match3D | `modules/creation/match3d/` 与 `modules/runtime/match3d/` | 不恢复 Rodin/GLB 新草稿 | 创作草稿、发布、运行态接口不变 |
|
||||
| Visual Novel/audio | `modules/creation/visual_novel/` | 音频持久化与 D1 协调 | SSE、compile、音频 asset route 不变 |
|
||||
| Character assets | `modules/assets/character_visual/`、`character_animation/` | 不改 workflow cache contract | 角色视觉/动作发布、导入、模板接口不变 |
|
||||
|
||||
## 6. 验收命令
|
||||
|
||||
每个能力切片至少运行:
|
||||
|
||||
```bash
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server --manifest-path server-rs/Cargo.toml
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
按能力追加定向测试:
|
||||
|
||||
```bash
|
||||
cargo test -p api-server big_fish --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server square_hole --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server custom_world --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server visual_novel --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
如果 route 装配已迁移,还要跑:
|
||||
|
||||
```bash
|
||||
cargo test -p api-server app --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
## 7. 当前落地状态(2026-05-14)
|
||||
|
||||
本轮已按“不改 route/HTTP contract/DTO/SpacetimeDB schema”的低风险策略完成首批机械拆分,并通过 `cargo check -p api-server`:
|
||||
|
||||
- `big_fish.rs`:正式图生成、下载与持久化拆入 `big_fish/formal_assets.rs`;会话/作品/运行态 response 映射与欢迎语拆入 `big_fish/mappers.rs`。
|
||||
- `square_hole.rs`:视觉资源生成、Adapter 持久化与图片 prompt glue 拆入 `square_hole/visual_assets.rs`;response mapper 拆入 `square_hole/mappers.rs`。
|
||||
- `custom_world_ai.rs`:通用图片资产持久化拆入 `custom_world_ai/assets.rs`;opening CG storyboard/video 生成与持久化拆入 `custom_world_ai/opening_cg.rs`。
|
||||
- `match3d.rs`:response mapper 拆入 `match3d/mappers.rs`;标签生成/归一化拆入 `match3d/tags.rs`。
|
||||
- `puzzle.rs`:response mapper 拆入 `puzzle/mappers.rs`;标签生成/保存/发布就绪判断拆入 `puzzle/tags.rs`。
|
||||
- `custom_world.rs`:library/gallery/work response mapper 拆入 `custom_world/mappers.rs`。
|
||||
|
||||
拆分后当前主文件规模约为:
|
||||
|
||||
| 文件 | 当前行数 | 已拆出模块 |
|
||||
| --- | ---: | --- |
|
||||
| `big_fish.rs` | 1005 | `formal_assets.rs`、`mappers.rs` |
|
||||
| `square_hole.rs` | 1674 | `visual_assets.rs`、`mappers.rs` |
|
||||
| `custom_world_ai.rs` | 3113 | `assets.rs`、`opening_cg.rs` |
|
||||
| `custom_world.rs` | 3519 | `mappers.rs` |
|
||||
| `puzzle.rs` | 5609 | `mappers.rs`、`tags.rs` |
|
||||
| `match3d.rs` | 6541 | `mappers.rs`、`tags.rs` |
|
||||
|
||||
后续建议继续拆分:
|
||||
|
||||
- `match3d`: `draft.rs`、`background_and_cover.rs`、`material_sheet.rs`、`apimart_image.rs`。
|
||||
- `puzzle`: `session_form.rs`、`draft_compile.rs`、`image_provider.rs`、`errors.rs`。
|
||||
- `custom_world`: `publish_gate.rs`、`foundation_job.rs`、`foundation_assets.rs`、`errors.rs`。
|
||||
- `square_hole`: `config.rs`、`errors.rs`。
|
||||
- `big_fish`: `errors.rs`,以及按需将 application 编排从 handler 中继续拆出。
|
||||
|
||||
## 8. 完成定义
|
||||
|
||||
D2 完成必须同时满足:
|
||||
|
||||
- 至少 3 个大 handler 完成 route/handler/application/assets/mapper/errors 的职责拆分。
|
||||
- handler 不再直接包含大段 provider 下载、OSS 上传、asset_object confirm、entity binding 逻辑。
|
||||
- 所有拆分前 route、method、DTO、error envelope、计费外层和 SpacetimeDB schema 不变。
|
||||
- 不新增领域规则到 `api-server`。
|
||||
- 验收命令通过,并同步更新 README 与本系列总纲/TODO 状态。
|
||||
@@ -1,296 +0,0 @@
|
||||
# api-server 能力模块化与生成资产 Adapter 总纲
|
||||
|
||||
状态:A 线文档基线已补齐;B1 路由模块化已由 controller 接手并以 cargo check 通过为当前编码基线;后续 C/D 线按本文 owner 拆分执行
|
||||
日期:2026-05-14
|
||||
范围:只约束 `server-rs/crates/api-server` 内部结构,不改 HTTP contract、DTO、SpacetimeDB schema、前端行为和计费语义。
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前 `api-server` 仍以超大 `app.rs` 直接装配全部 Axum route,并由 `big_fish.rs`、`square_hole.rs`、`custom_world_ai.rs`、`puzzle.rs`、`match3d.rs`、`character_visual_assets.rs`、`character_animation_assets.rs`、`vector_engine_audio_generation.rs` 等大 handler 文件承载 HTTP 解析、平台编排、生成资产下载/解码、OSS 上传、asset_object confirm、entity binding、计费包裹和部分玩法策略。
|
||||
|
||||
本轮总目标是分阶段把能力模块化和生成资产 Adapter 收口落地到可维护结构:
|
||||
|
||||
- `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、模块 router merge/nest 和少量 glue。
|
||||
- 每个能力以 `modules/<capability>/router.rs` 暴露 `router(state) -> Router<AppState>`,迁移时保持原 route 字符串和 handler 函数不变。
|
||||
- 图片生成资产先收口到 `generated_image_assets` 内部 Adapter,复用 provider 生成、下载/base64 解码、MIME/extension 归一、OSS private upload、HEAD/确认、asset_object confirm、entity binding。
|
||||
- 音频、视频、GLB/Hyper3D/character assets 只复用底层“媒体持久化 + asset_object + binding”能力,不强行塞进图片 Adapter。
|
||||
|
||||
## 2. 不变边界
|
||||
|
||||
必须遵守既有 DDD 总纲和 G1 契约矩阵:
|
||||
|
||||
- 后端主线为 `server-rs + Axum + SpacetimeDB`。
|
||||
- `api-server` 只做 HTTP/SSE/BFF、鉴权、DTO 映射、平台服务编排、错误 envelope 映射、读写 facade 调用。
|
||||
- 领域规则不沉回 handler;新增模块不得把玩法裁决、实体规则、钱包语义、表结构语义写成 HTTP 层私有规则。
|
||||
- `module-*` 保持领域规则 owner;`spacetime-module` 保持真相源 schema/procedure owner;`spacetime-client` 保持 facade/adapter owner。
|
||||
- 本轮默认不改 HTTP route、DTO 字段、error envelope、SpacetimeDB schema、前端行为、计费语义。
|
||||
- `asset_billing.rs` 仍由调用方显式包裹;生成资产 Adapter 不扣费、不退款、不读钱包。
|
||||
- 生成资产读取继续走 `/api/assets/read-url` 或 `/api/assets/read-bytes` 换签/代理链路,不恢复 `/generated-*` 直读代理。
|
||||
- 禁止新增或复活 Maincloud 口径;smoke 以 `/healthz` 为准。
|
||||
|
||||
## 3. 当前源码入口
|
||||
|
||||
- 路由装配:`server-rs/crates/api-server/src/app.rs`
|
||||
- 资产基础 BFF:`assets.rs`
|
||||
- 图片 provider 公共 helper:`openai_image_generation.rs`
|
||||
- 玩法/创作大 handler:`big_fish.rs`、`square_hole.rs`、`custom_world_ai.rs`、`custom_world.rs`、`puzzle.rs`、`match3d.rs`
|
||||
- 复杂资产:`vector_engine_audio_generation.rs`、`character_visual_assets.rs`、`character_animation_assets.rs`
|
||||
- Prompt 辅助同名文件:`prompt/big_fish.rs`、`prompt/square_hole.rs`,不是 HTTP handler owner。
|
||||
|
||||
## 4. Route inventory 总览
|
||||
|
||||
完整执行细节见《api-server路由能力模块化执行计划》。当前 `app.rs` route 按能力归类如下。
|
||||
|
||||
### admin
|
||||
|
||||
Handler 主要在 `admin.rs`、`admin_creation_entry.rs`、`admin_profile.rs`:
|
||||
|
||||
- `/admin/api/login`
|
||||
- `/admin/api/me`
|
||||
- `/admin/api/overview`
|
||||
- `/admin/api/debug/http`
|
||||
- `/admin/api/tracking/events`
|
||||
- `/admin/api/database/tables`
|
||||
- `/admin/api/database/tables/{table_name}/rows`
|
||||
- `/admin/api/creation-entry/config`
|
||||
- `/admin/api/profile/redeem-codes`
|
||||
- `/admin/api/profile/redeem-codes/disable`
|
||||
- `/admin/api/profile/invite-codes`
|
||||
- `/admin/api/profile/tasks`
|
||||
- `/admin/api/profile/tasks/disable`
|
||||
|
||||
### health/internal
|
||||
|
||||
Handler 主要在 `app.rs` glue、`auth.rs`:
|
||||
|
||||
- `/healthz`
|
||||
- `/_internal/auth/claims`
|
||||
- `/_internal/auth/refresh-cookie`
|
||||
|
||||
### auth
|
||||
|
||||
Handler 主要在 `auth.rs`、`wechat_auth.rs`:
|
||||
|
||||
- `/api/auth/login-options`
|
||||
- `/api/auth/public-users/by-code/{code}`
|
||||
- `/api/auth/public-users/by-id/{user_id}`
|
||||
- `/api/auth/me`
|
||||
- `/api/auth/sessions`
|
||||
- `/api/auth/sessions/{session_id}/revoke`
|
||||
- `/api/auth/refresh`
|
||||
- `/api/auth/phone/send-code`
|
||||
- `/api/auth/phone/login`
|
||||
- `/api/auth/wechat/start`
|
||||
- `/api/auth/wechat/callback`
|
||||
- `/api/auth/wechat/miniprogram-login`
|
||||
- `/api/auth/wechat/bind-phone`
|
||||
- `/api/auth/logout`
|
||||
- `/api/auth/logout-all`
|
||||
- `/api/auth/entry`
|
||||
- `/api/auth/password/change`
|
||||
- `/api/auth/password/reset`
|
||||
|
||||
### assets
|
||||
|
||||
Handler 主要在 `assets.rs`、`character_visual_assets.rs`、`character_animation_assets.rs`、`hyper3d.rs`:
|
||||
|
||||
- `/api/assets/direct-upload-tickets`
|
||||
- `/api/assets/sts-upload-credentials`
|
||||
- `/api/assets/objects/confirm`
|
||||
- `/api/assets/objects/bind`
|
||||
- `/api/assets/read-url`
|
||||
- `/api/assets/read-bytes`
|
||||
- `/api/assets/history`
|
||||
- `/api/assets/character-visual/generate`
|
||||
- `/api/assets/character-visual/jobs/{task_id}`
|
||||
- `/api/assets/character-visual/publish`
|
||||
- `/api/assets/character-animation/generate`
|
||||
- `/api/assets/character-animation/jobs/{task_id}`
|
||||
- `/api/assets/character-animation/publish`
|
||||
- `/api/assets/character-animation/import-video`
|
||||
- `/api/assets/character-animation/templates`
|
||||
- `/api/assets/character-workflow-cache`
|
||||
- `/api/assets/character-workflow-cache/{character_id}`
|
||||
- `/api/runtime/custom-world/asset-studio/role/{character_id}/workflow`
|
||||
- `/api/assets/hyper3d/text-to-model`
|
||||
- `/api/assets/hyper3d/image-to-model`
|
||||
- `/api/assets/hyper3d/status`
|
||||
- `/api/assets/hyper3d/download`
|
||||
|
||||
### platform/BFF
|
||||
|
||||
Handler 主要在 `llm.rs`、`speech.rs`、`ai_tasks.rs`、`creation_entry.rs`、`runtime_chat.rs`:
|
||||
|
||||
- `/api/llm/chat/completions`
|
||||
- `/api/speech/volcengine/config`
|
||||
- `/api/speech/volcengine/asr/stream`
|
||||
- `/api/speech/volcengine/tts/bidirection`
|
||||
- `/api/speech/volcengine/tts/sse`
|
||||
- `/api/ai/tasks` 及 `{task_id}` start/chunks/complete/fail/cancel/stages/references 子路由
|
||||
- `/api/creation-entry/config`
|
||||
- `/api/runtime/chat/character/suggestions`
|
||||
- `/api/runtime/chat/character/summary`
|
||||
- `/api/runtime/chat/character/reply/stream`
|
||||
- `/api/runtime/chat/npc/dialogue/stream`
|
||||
- `/api/runtime/chat/npc/turn/stream`
|
||||
- `/api/runtime/chat/npc/recruit/stream`
|
||||
- `/api/runtime/creation-agent/document-inputs/parse`
|
||||
|
||||
### creation
|
||||
|
||||
Handler 主要在 `match3d.rs`、`square_hole.rs`、`visual_novel.rs`、`vector_engine_audio_generation.rs`:
|
||||
|
||||
- `/api/creation/match3d/*`
|
||||
- `/api/creation/square-hole/*`
|
||||
- `/api/creation/visual-novel/*`
|
||||
- `/api/creation/visual-novel/audio/*`
|
||||
- `/api/creation/audio/background-music`
|
||||
- `/api/creation/audio/background-music/{task_id}/asset`
|
||||
- `/api/creation/audio/sound-effect`
|
||||
- `/api/creation/audio/sound-effect/{task_id}/asset`
|
||||
|
||||
### runtime/gameplay
|
||||
|
||||
Handler 主要在 `custom_world.rs`、`custom_world_ai.rs`、`big_fish.rs`、`puzzle.rs`、`match3d.rs`、`square_hole.rs`、`visual_novel.rs`:
|
||||
|
||||
- `/api/runtime/settings`
|
||||
- `/api/runtime/save/snapshot`
|
||||
- `/api/runtime/custom-world-library*`
|
||||
- `/api/runtime/custom-world-gallery*`
|
||||
- `/api/runtime/custom-world/agent/*`
|
||||
- `/api/runtime/custom-world/works`
|
||||
- `/api/runtime/custom-world/profile|entity|scene-npc|scene-image|cover-image|cover-upload|opening-cg`
|
||||
- `/api/runtime/big-fish/*`
|
||||
- `/api/runtime/puzzle/*`
|
||||
- `/api/runtime/match3d/*`
|
||||
- `/api/runtime/square-hole/*`
|
||||
- `/api/runtime/visual-novel/*`
|
||||
- `/api/runtime/creative-agent/*`
|
||||
- `/api/runtime/sessions/{runtime_session_id}/inventory`
|
||||
|
||||
### profile
|
||||
|
||||
Handler 主要在 `profile.rs`、`runtime_profile.rs`、`tracking.rs`:
|
||||
|
||||
- `/api/profile/me`
|
||||
- `/api/profile/browse-history`
|
||||
- `/api/profile/dashboard`
|
||||
- `/api/profile/wallet-ledger`
|
||||
- `/api/profile/recharge-center`
|
||||
- `/api/profile/recharge/orders`
|
||||
- `/api/profile/recharge/wechat/notify`
|
||||
- `/api/profile/feedback`
|
||||
- `/api/profile/referrals/invite-center`
|
||||
- `/api/profile/referrals/redeem-code`
|
||||
- `/api/profile/redeem-codes/redeem`
|
||||
- `/api/profile/analytics/metric`
|
||||
- `/api/profile/tasks`
|
||||
- `/api/profile/tasks/{task_id}/claim`
|
||||
- `/api/profile/save-archives`
|
||||
- `/api/profile/save-archives/{world_key}`
|
||||
- `/api/profile/play-stats`
|
||||
|
||||
### story
|
||||
|
||||
Handler 主要在 `story.rs`、`combat.rs`、`runtime_inventory.rs`:
|
||||
|
||||
- `/api/story/sessions`
|
||||
- `/api/story/sessions/runtime`
|
||||
- `/api/story/sessions/{story_session_id}/state`
|
||||
- `/api/story/sessions/{story_session_id}/runtime-projection`
|
||||
- `/api/story/sessions/{story_session_id}/actions/resolve`
|
||||
- `/api/story/sessions/continue`
|
||||
- `/api/story/battles`
|
||||
- `/api/story/battles/{battle_state_id}`
|
||||
- `/api/story/npc/battle`
|
||||
- `/api/story/battles/resolve`
|
||||
|
||||
## 5. 生成资产链路 inventory 总览
|
||||
|
||||
详细迁移计划见图片 Adapter、复杂媒体 Adapter 文档。图片链路由 C 线收口;音频、视频、GLB/Hyper3D、角色工作流等复杂媒体由 D1 线只复用媒体持久化底座,不反向扩大图片 Adapter interface。
|
||||
|
||||
| 链路 | provider | 下载/解码 | OSS prefix | asset kind | entity binding | 计费位置 | 降级行为 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Big Fish 正式图 | DashScope `wan2.2-t2i-flash` | 轮询 task 后 HTTP GET 图片 URL | `LegacyAssetPrefix::BigFishAssets` | 由 assetKind 映射主图/动作图/舞台背景等 | `big_fish_session` + session/entity id + slot | `big_fish.rs` 调用方 `execute_billable_asset_operation` | 配置缺失/上游失败直接错误;gallery 对部分 Spacetime 运行错误软降级 |
|
||||
| Square Hole 图片重生成 | OpenAI/VectorEngine GPT image helper | URL 下载或 base64/data URL 解码 | `LegacyAssetPrefix::SquareHoleAssets` | 方洞作品图片槽位相关 kind | profile/work + image slot | 调用方包裹 | 生成成功但入库失败保留 Data URL 回包 |
|
||||
| Custom World 场景/封面 | VectorEngine GPT image 2 / OpenAI helper | URL 下载或 base64 解码 | `LegacyAssetPrefix::CustomWorldScenes` 等 | scene/cover/opening storyboard | `custom_world_profile` 或 profile/landmark/scene slot | `custom_world_ai.rs` 调用方包裹 | entity/scene 生成存在 LLM fallback;资产持久化失败按当前错误口径返回 |
|
||||
| Puzzle 图片 | GPT image 2 generations/edits | multipart/base64/URL 结果归一 | `LegacyAssetPrefix::PuzzleAssets` | puzzle level/background/generated image,另有 `puzzle_background_music` | puzzle profile/run/level slot | `puzzle.rs` 调用方包裹 | connectivity 可按既有规则跳过部分计费;运行态 fallback 保持原逻辑 |
|
||||
| Match3D 图片 | APIMart/VectorEngine/OpenAI image helper | 下载、切图、透明化、校准后入库 | `LegacyAssetPrefix::Match3DAssets` | cover/background/item material sheet,音频 kind 另列 | match3d profile/session slot | `match3d.rs` 调用方包裹 | 新草稿不回退 Rodin/GLB;部分连接错误按现有计费跳过规则处理 |
|
||||
| Visual Novel 音频 | VectorEngine Suno/Vidu | 任务提交后按 task publish 下载音频 | 视觉小说/creation audio scope | `visual_novel_music`、`visual_novel_ambient_sound` | `visual_novel_scene` + scene id + `music`/`ambient_sound` | `vector_engine_audio_generation.rs` 调用方包裹 | 上游/下载失败显式错误,不混入图片 Adapter |
|
||||
| 通用音频 | VectorEngine Suno/Vidu | 同上 | creation audio scope | background_music/sound_effect 由调用方目标指定 | creation target entity/slot | 调用方包裹 | 不与 VN 场景语义混用 |
|
||||
| 视频 Opening CG | Ark/火山视频 + storyboard | 先生 storyboard,再图生视频,下载 remote video | Custom World 相关 prefix | `custom_world_opening_cg_storyboard`、`custom_world_opening_cg_video` | `custom_world_profile` + opening cg slots | `execute_billable_asset_operation_with_cost` 固定点数 | 配置缺失/超时显式错误,不应静默降级 |
|
||||
| Character visual assets | GPT image helper / role asset workflow | base64/data URL/下载后入库 | 角色视觉资产相关 prefix | character_visual / reference / workflow cache | `character` + visual slots | 调用方包裹或当前无扣费处保持不变 | workflow cache 可无缓存继续生成 |
|
||||
| Character animation assets | Ark video 或阶段占位 | data:video base64 导入、remote video 下载、预览视频保存 | 角色动画资产相关 prefix | `character_animation`、`character_animation_reference_video`、`character_workflow_cache` | `character` + animation slots | 当前调用方语义保持 | stage1 image sequence/video placeholder 继续保留 |
|
||||
| Hyper3D/GLB | Hyper3D Rodin 历史代理 | status/download 列表代理,历史转存可复用 OSS | Hyper3D/model prefix | model/glb 相关历史 kind | 作品/profile 绑定视历史链路 | 不新增计费语义 | 当前 Match3D 新草稿不再回退 Rodin/GLB |
|
||||
|
||||
## 6. 并行执行规则
|
||||
|
||||
### 单 owner 文件/模块
|
||||
|
||||
同一时间只允许一个 agent 修改:
|
||||
|
||||
- `server-rs/crates/api-server/src/app.rs`
|
||||
- 未来 `server-rs/crates/api-server/src/modules/mod.rs`
|
||||
- 未来 `server-rs/crates/api-server/src/modules/assets/generated_image_assets/*`
|
||||
- `asset_billing.rs`
|
||||
- `openai_image_generation.rs`
|
||||
- `assets.rs`
|
||||
- `docs/technical/README.md`
|
||||
- 原 TODO 文档
|
||||
|
||||
### 可并行能力 owner
|
||||
|
||||
在 route inventory 和 Adapter interface 冻结后,可按能力分配:
|
||||
|
||||
- admin/auth/internal/health route module
|
||||
- assets/character assets/hyper3d route module
|
||||
- profile/runtime settings/save route module
|
||||
- Big Fish
|
||||
- Square Hole
|
||||
- Custom World
|
||||
- Puzzle
|
||||
- Match3D
|
||||
- Visual Novel/audio
|
||||
- story/combat/inventory
|
||||
|
||||
并行前提:只改自己能力目录和对应 handler;跨能力公共 helper 必须先锁单 owner 并在文档中声明。
|
||||
|
||||
## 7. 阶段与退出条件
|
||||
|
||||
- A0 文档基线(owner:A 线文档 agent):5 篇执行文档和 README/TODO 索引更新;`npm run check:encoding`、`git diff --check` 通过;不改 Rust 代码。
|
||||
- B1 route 模块化(owner:B1 route agent/controller):route inventory 全部仍可在新 modules router 中找到;旧明确下线 route 仍 404;`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 与 `cargo test -p api-server app --manifest-path server-rs/Cargo.toml` 或等价 route 测试通过。
|
||||
- C1-C4 图片 Adapter(owner:C 线图片 Adapter agent):Big Fish、Square Hole、Custom World 至少 3 个真实调用方接入同一 Adapter;旧重复 persist helper 可删除且行为不变;禁止修改计费语义。
|
||||
- D1 复杂媒体 Adapter(owner:D1 复杂媒体 agent):Puzzle/Match3D/音频/视频/角色工作流/Hyper3D 只复用合适的底层持久化能力,不污染图片 interface;`/api/assets/read-url` 和 `/api/assets/read-bytes` 读取链路仍可用。
|
||||
- D2 大 handler 瘦身(owner:D2 handler 瘦身 agent):route、handler、application、assets、mapper、errors 分层清晰;不扩大到领域 crate 重构;每次只领取一个能力 owner。
|
||||
|
||||
## 8. TODO / 多 agent 拆分状态
|
||||
|
||||
| 线别 | Owner | 当前状态 | 只允许改动 | 禁止改动 | 退出条件 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| A0 文档基线 | A 线文档 agent | 本次补齐缺失复杂媒体与大 Handler 文档,更新 README 和本 TODO 视角 | `docs/technical/【后端架构】*2026-05-14.md`、`docs/technical/README.md` | Rust 代码、Cargo 配置、前端、schema | `npm run check:encoding`、`git diff --check` 通过 |
|
||||
| B1 路由模块化 | B1 route agent/controller | controller 已接手,当前 cargo check 通过;后续以 B1 diff 为准 | `app.rs`、`modules/*/router.rs`、必要 `mod.rs` | handler 行为、DTO、middleware 顺序、计费 | route inventory 完整、cargo check/route tests 通过 |
|
||||
| C 图片 Adapter | C 线图片 Adapter agent | 待执行 | `modules/assets/generated_image_assets/*`,以及 Big Fish/Square Hole/Custom World 接线文件 | `asset_billing.rs`、schema、公开 contract、复杂媒体接口 | 三类真实图片调用方共用 Adapter |
|
||||
| D1 复杂媒体 Adapter | D1 复杂媒体 agent | 待执行 | `modules/assets/media_assets/*` 与音频/视频/角色资产接线文件 | 图片 Adapter interface、provider 策略、schema、计费 | 至少音频和视频两类复用 media persist |
|
||||
| D2 大 Handler 瘦身 | D2 handler 瘦身 agent | 待 B/C/D1 稳定后执行 | 单个能力 handler 及其能力目录内 `handlers/application/assets/mapper/errors` | 领域 crate、contract、schema、前端、计费 | 至少 3 个大 handler 职责拆分完成 |
|
||||
|
||||
并行要求:同一文件同一时间只允许一个 owner;跨线公共 helper 必须先在对应执行文档中声明 owner 和退出条件。若编码 diff 与本 TODO 冲突,以最新通过验收命令的代码事实为准,并同步修正文档。
|
||||
|
||||
## 9. 验证命令
|
||||
|
||||
文档阶段:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
后续编码阶段按变更范围追加:
|
||||
|
||||
```bash
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server app --manifest-path server-rs/Cargo.toml
|
||||
npm run check:server-rs-ddd
|
||||
npm run api-server
|
||||
# 另开终端 curl /healthz
|
||||
```
|
||||
@@ -1,393 +0,0 @@
|
||||
# api-server 路由能力模块化执行计划
|
||||
|
||||
状态:A 线文档基线已补齐;B1 低风险路由模块已落地并通过 route/app smoke、cargo check、编码检查;后续 B2/B3 继续按本文 owner 拆分执行
|
||||
日期:2026-05-14
|
||||
范围:只移动/重组 `api-server` 路由装配;不改 route path、HTTP method、handler 函数签名、DTO、鉴权策略、middleware 顺序和前端行为。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把当前 `app.rs` 中所有 `.route(...)` 按能力迁入 `server-rs/crates/api-server/src/modules/`,每个能力暴露:
|
||||
|
||||
```rust
|
||||
pub(crate) fn router(state: AppState) -> Router<AppState>
|
||||
```
|
||||
|
||||
第一阶段 handler 实现仍可留在原文件;本阶段只改变路由装配位置。`app.rs` 最终只负责:
|
||||
|
||||
- 构建 shared state。
|
||||
- 注入全局 CORS、TraceLayer、request context、tracking/auth middleware。
|
||||
- `.merge(modules::<capability>::router(state.clone()))`。
|
||||
- 保留 `/healthz` 等极少量 glue,或迁到 `modules/health` 后统一 merge。
|
||||
|
||||
## 2. 建议目录
|
||||
|
||||
```text
|
||||
server-rs/crates/api-server/src/modules/
|
||||
mod.rs
|
||||
admin/router.rs
|
||||
auth/router.rs
|
||||
assets/router.rs
|
||||
profile/router.rs
|
||||
platform/router.rs
|
||||
creation/router.rs
|
||||
runtime/router.rs
|
||||
story/router.rs
|
||||
internal/router.rs
|
||||
health/router.rs
|
||||
```
|
||||
|
||||
可进一步拆分:
|
||||
|
||||
- `creation/{match3d,square_hole,visual_novel,audio}/router.rs`
|
||||
- `runtime/{custom_world,big_fish,puzzle,match3d,square_hole,visual_novel,creative_agent,settings,save}/router.rs`
|
||||
- `assets/{base,character_visual,character_animation,hyper3d}/router.rs`
|
||||
|
||||
## 3. Route inventory 与 owner
|
||||
|
||||
### 3.1 admin
|
||||
|
||||
Owner:`modules/admin/router.rs`。主要 handler 文件:`admin.rs`、`admin_creation_entry.rs`、`admin_profile.rs`。
|
||||
|
||||
- `/admin/api/login`
|
||||
- `/admin/api/me`
|
||||
- `/admin/api/overview`
|
||||
- `/admin/api/debug/http`
|
||||
- `/admin/api/tracking/events`
|
||||
- `/admin/api/database/tables`
|
||||
- `/admin/api/database/tables/{table_name}/rows`
|
||||
- `/admin/api/creation-entry/config`
|
||||
- `/admin/api/profile/redeem-codes`
|
||||
- `/admin/api/profile/redeem-codes/disable`
|
||||
- `/admin/api/profile/invite-codes`
|
||||
- `/admin/api/profile/tasks`
|
||||
- `/admin/api/profile/tasks/disable`
|
||||
|
||||
退出条件:后台接口路径、鉴权 middleware、错误 envelope 不变。
|
||||
|
||||
### 3.2 health/internal
|
||||
|
||||
Owner:`modules/health/router.rs`、`modules/internal/router.rs`。主要 handler:`app.rs` glue、`auth.rs`。
|
||||
|
||||
- `/healthz`
|
||||
- `/_internal/auth/claims`
|
||||
- `/_internal/auth/refresh-cookie`
|
||||
|
||||
退出条件:`/healthz` 可作为本地 smoke;internal route 不暴露新增契约。
|
||||
|
||||
### 3.3 auth
|
||||
|
||||
Owner:`modules/auth/router.rs`。主要 handler:`auth.rs`、`wechat_auth.rs`。
|
||||
|
||||
- `/api/auth/login-options`
|
||||
- `/api/auth/public-users/by-code/{code}`
|
||||
- `/api/auth/public-users/by-id/{user_id}`
|
||||
- `/api/auth/me`
|
||||
- `/api/auth/sessions`
|
||||
- `/api/auth/sessions/{session_id}/revoke`
|
||||
- `/api/auth/refresh`
|
||||
- `/api/auth/phone/send-code`
|
||||
- `/api/auth/phone/login`
|
||||
- `/api/auth/wechat/start`
|
||||
- `/api/auth/wechat/callback`
|
||||
- `/api/auth/wechat/miniprogram-login`
|
||||
- `/api/auth/wechat/bind-phone`
|
||||
- `/api/auth/logout`
|
||||
- `/api/auth/logout-all`
|
||||
- `/api/auth/entry`
|
||||
- `/api/auth/password/change`
|
||||
- `/api/auth/password/reset`
|
||||
|
||||
退出条件:cookie/session/refresh 行为不变;手机号/微信配置门控不变。
|
||||
|
||||
### 3.4 assets
|
||||
|
||||
Owner:`modules/assets/router.rs`。主要 handler:`assets.rs`、`character_visual_assets.rs`、`character_animation_assets.rs`、`hyper3d.rs`。
|
||||
|
||||
- `/api/assets/direct-upload-tickets`
|
||||
- `/api/assets/sts-upload-credentials`
|
||||
- `/api/assets/objects/confirm`
|
||||
- `/api/assets/objects/bind`
|
||||
- `/api/assets/read-url`
|
||||
- `/api/assets/read-bytes`
|
||||
- `/api/assets/history`
|
||||
- `/api/assets/character-visual/generate`
|
||||
- `/api/assets/character-visual/jobs/{task_id}`
|
||||
- `/api/assets/character-visual/publish`
|
||||
- `/api/assets/character-animation/generate`
|
||||
- `/api/assets/character-animation/jobs/{task_id}`
|
||||
- `/api/assets/character-animation/publish`
|
||||
- `/api/assets/character-animation/import-video`
|
||||
- `/api/assets/character-animation/templates`
|
||||
- `/api/assets/character-workflow-cache`
|
||||
- `/api/assets/character-workflow-cache/{character_id}`
|
||||
- `/api/runtime/custom-world/asset-studio/role/{character_id}/workflow`(可暂挂 assets module,但文档注明 runtime 前缀历史兼容)
|
||||
- `/api/assets/hyper3d/text-to-model`
|
||||
- `/api/assets/hyper3d/image-to-model`
|
||||
- `/api/assets/hyper3d/status`
|
||||
- `/api/assets/hyper3d/download`
|
||||
|
||||
退出条件:私有资产读取仍走 read-url/read-bytes;Hyper3D 不新增 Match3D 新草稿回退。
|
||||
|
||||
### 3.5 platform/BFF
|
||||
|
||||
Owner:`modules/platform/router.rs`。主要 handler:`llm.rs`、`speech.rs`、`ai_tasks.rs`、`creation_entry.rs`、`runtime_chat.rs`。
|
||||
|
||||
- `/api/llm/chat/completions`
|
||||
- `/api/speech/volcengine/config`
|
||||
- `/api/speech/volcengine/asr/stream`
|
||||
- `/api/speech/volcengine/tts/bidirection`
|
||||
- `/api/speech/volcengine/tts/sse`
|
||||
- `/api/runtime/chat/character/suggestions`
|
||||
- `/api/runtime/chat/character/summary`
|
||||
- `/api/runtime/chat/character/reply/stream`
|
||||
- `/api/runtime/chat/npc/dialogue/stream`
|
||||
- `/api/runtime/chat/npc/turn/stream`
|
||||
- `/api/runtime/chat/npc/recruit/stream`
|
||||
- `/api/runtime/creation-agent/document-inputs/parse`
|
||||
- `/api/ai/tasks`
|
||||
- `/api/ai/tasks/{task_id}/start`
|
||||
- `/api/ai/tasks/{task_id}/stages/{stage_kind}/start`
|
||||
- `/api/ai/tasks/{task_id}/chunks`
|
||||
- `/api/ai/tasks/{task_id}/stages/{stage_kind}/complete`
|
||||
- `/api/ai/tasks/{task_id}/references`
|
||||
- `/api/ai/tasks/{task_id}/complete`
|
||||
- `/api/ai/tasks/{task_id}/fail`
|
||||
- `/api/ai/tasks/{task_id}/cancel`
|
||||
- `/api/creation-entry/config`
|
||||
|
||||
退出条件:SSE 流式 route 不被非流式 wrapper 改写;外部服务错误分类不变。
|
||||
|
||||
### 3.6 creation
|
||||
|
||||
Owner:`modules/creation/router.rs`,可由子模块并行。主要 handler:`match3d.rs`、`square_hole.rs`、`visual_novel.rs`、`vector_engine_audio_generation.rs`。
|
||||
|
||||
Match3D:
|
||||
|
||||
- `/api/creation/match3d/sessions`
|
||||
- `/api/creation/match3d/sessions/{session_id}`
|
||||
- `/api/creation/match3d/sessions/{session_id}/messages`
|
||||
- `/api/creation/match3d/sessions/{session_id}/messages/stream`
|
||||
- `/api/creation/match3d/sessions/{session_id}/actions`
|
||||
- `/api/creation/match3d/sessions/{session_id}/compile`
|
||||
- `/api/creation/match3d/works`
|
||||
- `/api/creation/match3d/works/tags`
|
||||
- `/api/creation/match3d/works/{profile_id}`
|
||||
- `/api/creation/match3d/works/{profile_id}/audio-assets`
|
||||
- `/api/creation/match3d/works/{profile_id}/cover-image`
|
||||
- `/api/creation/match3d/works/{profile_id}/background-image`
|
||||
- `/api/creation/match3d/works/{profile_id}/item-assets`
|
||||
- `/api/creation/match3d/works/{profile_id}/generated-models`
|
||||
- `/api/creation/match3d/works/{profile_id}/publish`
|
||||
|
||||
Square Hole:
|
||||
|
||||
- `/api/creation/square-hole/sessions`
|
||||
- `/api/creation/square-hole/sessions/{session_id}`
|
||||
- `/api/creation/square-hole/sessions/{session_id}/messages`
|
||||
- `/api/creation/square-hole/sessions/{session_id}/messages/stream`
|
||||
- `/api/creation/square-hole/sessions/{session_id}/actions`
|
||||
- `/api/creation/square-hole/sessions/{session_id}/compile`
|
||||
- `/api/creation/square-hole/works`
|
||||
- `/api/creation/square-hole/works/{profile_id}`
|
||||
- `/api/creation/square-hole/works/{profile_id}/publish`
|
||||
- `/api/creation/square-hole/works/{profile_id}/images/regenerate`
|
||||
|
||||
Visual Novel 与音频:
|
||||
|
||||
- `/api/creation/visual-novel/sessions`
|
||||
- `/api/creation/visual-novel/sessions/{session_id}`
|
||||
- `/api/creation/visual-novel/sessions/{session_id}/messages`
|
||||
- `/api/creation/visual-novel/sessions/{session_id}/messages/stream`
|
||||
- `/api/creation/visual-novel/sessions/{session_id}/actions`
|
||||
- `/api/creation/visual-novel/sessions/{session_id}/compile`
|
||||
- `/api/creation/visual-novel/works`
|
||||
- `/api/creation/visual-novel/works/{profile_id}`
|
||||
- `/api/creation/visual-novel/works/{profile_id}/publish`
|
||||
- `/api/creation/visual-novel/audio/background-music`
|
||||
- `/api/creation/visual-novel/audio/background-music/{task_id}/asset`
|
||||
- `/api/creation/visual-novel/audio/sound-effect`
|
||||
- `/api/creation/visual-novel/audio/sound-effect/{task_id}/asset`
|
||||
- `/api/creation/audio/background-music`
|
||||
- `/api/creation/audio/background-music/{task_id}/asset`
|
||||
- `/api/creation/audio/sound-effect`
|
||||
- `/api/creation/audio/sound-effect/{task_id}/asset`
|
||||
|
||||
退出条件:所有 creation route method 和 auth extension 不变。
|
||||
|
||||
### 3.7 runtime
|
||||
|
||||
Owner:`modules/runtime/router.rs`,建议按玩法子模块并行。
|
||||
|
||||
Custom World:
|
||||
|
||||
- `/api/runtime/settings`
|
||||
- `/api/runtime/save/snapshot`
|
||||
- `/api/runtime/custom-world-library`
|
||||
- `/api/runtime/custom-world-library/{profile_id}`
|
||||
- `/api/runtime/custom-world-library/{profile_id}/publish`
|
||||
- `/api/runtime/custom-world-library/{profile_id}/unpublish`
|
||||
- `/api/runtime/custom-world-gallery`
|
||||
- `/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}`
|
||||
- `/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/remix`
|
||||
- `/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/play`
|
||||
- `/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/like`
|
||||
- `/api/runtime/custom-world-gallery/by-code/{code}`
|
||||
- `/api/runtime/custom-world/agent/sessions`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/result-view`
|
||||
- `/api/runtime/custom-world/works`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/cards/{card_id}`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/messages`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/messages/stream`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/actions`
|
||||
- `/api/runtime/custom-world/agent/sessions/{session_id}/operations/{operation_id}`
|
||||
- `/api/runtime/custom-world/profile`
|
||||
- `/api/runtime/custom-world/entity`
|
||||
- `/api/runtime/custom-world/scene-npc`
|
||||
- `/api/runtime/custom-world/scene-image`
|
||||
- `/api/runtime/custom-world/cover-image`
|
||||
- `/api/runtime/custom-world/cover-upload`
|
||||
- `/api/runtime/custom-world/opening-cg`
|
||||
|
||||
Big Fish:
|
||||
|
||||
- `/api/runtime/big-fish/agent/sessions`
|
||||
- `/api/runtime/big-fish/agent/sessions/{session_id}`
|
||||
- `/api/runtime/big-fish/agent/sessions/{session_id}/messages`
|
||||
- `/api/runtime/big-fish/agent/sessions/{session_id}/messages/stream`
|
||||
- `/api/runtime/big-fish/agent/sessions/{session_id}/actions`
|
||||
- `/api/runtime/big-fish/works`
|
||||
- `/api/runtime/big-fish/gallery`
|
||||
- `/api/runtime/big-fish/gallery/{session_id}/remix`
|
||||
- `/api/runtime/big-fish/gallery/{session_id}/like`
|
||||
- `/api/runtime/big-fish/works/{session_id}`
|
||||
- `/api/runtime/big-fish/sessions/{session_id}/play`
|
||||
- `/api/runtime/big-fish/works/{session_id}/play`
|
||||
- `/api/runtime/big-fish/sessions/{session_id}/runs`
|
||||
- `/api/runtime/big-fish/runs/{run_id}`
|
||||
- `/api/runtime/big-fish/runs/{run_id}/input`
|
||||
|
||||
Puzzle:
|
||||
|
||||
- `/api/runtime/puzzle/agent/sessions`
|
||||
- `/api/runtime/puzzle/agent/sessions/{session_id}`
|
||||
- `/api/runtime/puzzle/agent/sessions/{session_id}/messages`
|
||||
- `/api/runtime/puzzle/agent/sessions/{session_id}/messages/stream`
|
||||
- `/api/runtime/puzzle/agent/sessions/{session_id}/actions`
|
||||
- `/api/runtime/puzzle/onboarding/generate`
|
||||
- `/api/runtime/puzzle/onboarding/save`
|
||||
- `/api/runtime/puzzle/works`
|
||||
- `/api/runtime/puzzle/works/{profile_id}`
|
||||
- `/api/runtime/puzzle/works/{profile_id}/point-incentive/claim`
|
||||
- `/api/runtime/puzzle/gallery`
|
||||
- `/api/runtime/puzzle/gallery/{profile_id}`
|
||||
- `/api/runtime/puzzle/gallery/{profile_id}/remix`
|
||||
- `/api/runtime/puzzle/gallery/{profile_id}/like`
|
||||
- `/api/runtime/puzzle/runs`
|
||||
- `/api/runtime/puzzle/runs/{run_id}`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/swap`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/drag`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/next-level`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/pause`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/props`
|
||||
- `/api/runtime/puzzle/runs/{run_id}/leaderboard`
|
||||
|
||||
Match3D/Square Hole/Visual Novel/Creative Agent:
|
||||
|
||||
- `/api/runtime/match3d/gallery`
|
||||
- `/api/runtime/match3d/works/{profile_id}/runs`
|
||||
- `/api/runtime/match3d/runs/{run_id}`
|
||||
- `/api/runtime/match3d/runs/{run_id}/click|stop|restart|time-up`
|
||||
- `/api/runtime/square-hole/gallery`
|
||||
- `/api/runtime/square-hole/works/{profile_id}/runs`
|
||||
- `/api/runtime/square-hole/runs/{run_id}`
|
||||
- `/api/runtime/square-hole/runs/{run_id}/drop|stop|restart|time-up`
|
||||
- `/api/runtime/visual-novel/gallery`
|
||||
- `/api/runtime/visual-novel/works/{profile_id}/runs`
|
||||
- `/api/runtime/visual-novel/runs/{run_id}`
|
||||
- `/api/runtime/visual-novel/runs/{run_id}/actions/stream|history|regenerate`
|
||||
- `/api/runtime/creative-agent/sessions`
|
||||
- `/api/runtime/creative-agent/sessions/{session_id}`
|
||||
- `/api/runtime/creative-agent/sessions/{session_id}/messages/stream`
|
||||
- `/api/runtime/creative-agent/sessions/{session_id}/confirm-template`
|
||||
- `/api/runtime/creative-agent/sessions/{session_id}/draft-edits/stream`
|
||||
- `/api/runtime/creative-agent/sessions/{session_id}/cancel`
|
||||
- `/api/runtime/sessions/{runtime_session_id}/inventory`
|
||||
|
||||
退出条件:运行态玩法 route 后端真相源保持现状;不恢复旧 `/api/custom-world/*` 非 runtime 前缀。
|
||||
|
||||
### 3.8 profile
|
||||
|
||||
Owner:`modules/profile/router.rs`。主要 handler:`profile.rs`、`runtime_profile.rs`、`tracking.rs`。
|
||||
|
||||
- `/api/profile/me`
|
||||
- `/api/profile/browse-history`
|
||||
- `/api/profile/dashboard`
|
||||
- `/api/profile/wallet-ledger`
|
||||
- `/api/profile/recharge-center`
|
||||
- `/api/profile/recharge/orders`
|
||||
- `/api/profile/recharge/wechat/notify`
|
||||
- `/api/profile/feedback`
|
||||
- `/api/profile/referrals/invite-center`
|
||||
- `/api/profile/referrals/redeem-code`
|
||||
- `/api/profile/redeem-codes/redeem`
|
||||
- `/api/profile/analytics/metric`
|
||||
- `/api/profile/tasks`
|
||||
- `/api/profile/tasks/{task_id}/claim`
|
||||
- `/api/profile/save-archives`
|
||||
- `/api/profile/save-archives/{world_key}`
|
||||
- `/api/profile/play-stats`
|
||||
|
||||
退出条件:钱包、任务、邀请码、充值语义不变。
|
||||
|
||||
### 3.9 story
|
||||
|
||||
Owner:`modules/story/router.rs`。主要 handler:`story.rs`、`combat.rs`、`runtime_inventory.rs`。
|
||||
|
||||
- `/api/story/sessions`
|
||||
- `/api/story/sessions/runtime`
|
||||
- `/api/story/sessions/{story_session_id}/state`
|
||||
- `/api/story/sessions/{story_session_id}/runtime-projection`
|
||||
- `/api/story/sessions/{story_session_id}/actions/resolve`
|
||||
- `/api/story/sessions/continue`
|
||||
- `/api/story/battles`
|
||||
- `/api/story/battles/{battle_state_id}`
|
||||
- `/api/story/npc/battle`
|
||||
- `/api/story/battles/resolve`
|
||||
|
||||
退出条件:继续使用 story session scoped route;旧 `/api/runtime/story/*` 不重新挂载。
|
||||
|
||||
## 4. 执行顺序
|
||||
|
||||
1. 新建 `modules/mod.rs` 和低风险子模块骨架。
|
||||
2. 迁移 health/internal/admin/auth/assets/profile route;每迁一组跑 route 编译测试。
|
||||
3. 迁移 platform route,特别保护 SSE handler 类型。
|
||||
4. 迁移 creation/runtime/story route。
|
||||
5. 清理 `app.rs` 中重复 route 装配,只保留 merge。
|
||||
6. 用脚本/测试确认 route 字符串未丢失。
|
||||
|
||||
## 5. 单 owner 与并行规则
|
||||
|
||||
- `app.rs`、`modules/mod.rs` 必须单 owner。
|
||||
- 同一能力的 `router.rs` 单 owner。
|
||||
- 不同能力 router 可并行,但不能同时改公共 middleware、state、auth extractor。
|
||||
- Handler 文件拆分不属于本阶段;若必须触碰 handler,只允许 import 路径修正。
|
||||
|
||||
## 6. 验证命令
|
||||
|
||||
```bash
|
||||
cargo test -p api-server app --manifest-path server-rs/Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
npm run check:server-rs-ddd
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
可选人工 smoke:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
curl -fsS http://127.0.0.1:<API_PORT>/healthz
|
||||
```
|
||||
|
||||
禁止用 `api-server:maincloud` 作为 smoke。
|
||||
@@ -1,151 +0,0 @@
|
||||
# 复杂媒体资产链路 Adapter 扩展计划
|
||||
|
||||
状态:待 D1 线执行;依赖 C 线图片 Adapter 的持久化底座稳定后开始
|
||||
日期:2026-05-14
|
||||
范围:只约束 `server-rs/crates/api-server` 内复杂媒体资产的持久化与绑定复用方式;不把音频、视频、GLB、角色工作流强行塞入图片生成 Adapter;不改 HTTP contract、DTO、SpacetimeDB schema、OSS 访问策略、前端行为和计费语义。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在图片 Adapter 收口之后,抽出可复用的“媒体持久化 + asset_object confirm + entity binding”底座,让复杂媒体链路减少重复代码,但保留各自 provider、任务轮询、业务语义和回包契约。
|
||||
|
||||
目标链路:
|
||||
|
||||
```text
|
||||
provider 或外部任务产物
|
||||
-> media source 归一(remote URL / data URL / base64 / bytes / object key)
|
||||
-> MIME/extension/文件名归一
|
||||
-> OSS private upload 或确认已有 object key
|
||||
-> 可选 HEAD/大小/类型检查
|
||||
-> module-assets asset_object confirm
|
||||
-> asset entity binding
|
||||
-> 返回调用方需要的 legacy path/object key/asset id/metadata
|
||||
```
|
||||
|
||||
本计划不定义新的公开 API,只定义 `api-server` 内部复用能力。首批完成必须至少覆盖:
|
||||
|
||||
- Visual Novel / 通用创作音频:背景音乐、音效任务发布后的音频下载入库。
|
||||
- Custom World opening CG:storyboard 图片可走图片 Adapter,最终视频走复杂媒体底座。
|
||||
- Character visual / animation:角色视觉、参考视频、导入视频和 workflow cache 的持久化复用。
|
||||
- Match3D / Puzzle 资产链路中非图片资产:背景音乐、历史 GLB/Hyper3D 代理边界只做复用,不恢复新草稿 GLB 回退。
|
||||
|
||||
## 2. Owner 与禁止改动范围
|
||||
|
||||
Owner:D1 复杂媒体 Adapter agent。
|
||||
|
||||
单 owner 文件/目录:
|
||||
|
||||
- `server-rs/crates/api-server/src/modules/assets/media_assets/*`(建议新增)
|
||||
- `server-rs/crates/api-server/src/vector_engine_audio_generation.rs` 中音频持久化接线
|
||||
- `server-rs/crates/api-server/src/character_visual_assets.rs` 中角色视觉持久化接线
|
||||
- `server-rs/crates/api-server/src/character_animation_assets.rs` 中角色动作/视频持久化接线
|
||||
- `server-rs/crates/api-server/src/hyper3d.rs` 中历史模型代理持久化接线
|
||||
|
||||
禁止本阶段修改:
|
||||
|
||||
- `asset_billing.rs`:复杂媒体 Adapter 不扣费、不退款、不判断钱包。
|
||||
- `shared-contracts`:不新增或改动公开 DTO。
|
||||
- `spacetime-module` schema/procedure:不新增表、不改 reducer 签名。
|
||||
- 前端页面、service、路由路径。
|
||||
- provider 策略:不切换 Suno/Vidu/Ark/Hyper3D/DashScope/OpenAI 模型,不改任务轮询超时语义。
|
||||
- `/generated-*` 直读代理:禁止恢复;读取仍走 `/api/assets/read-url` 或 `/api/assets/read-bytes`。
|
||||
|
||||
## 3. 建议模块边界
|
||||
|
||||
建议目录:
|
||||
|
||||
```text
|
||||
server-rs/crates/api-server/src/modules/assets/media_assets/
|
||||
mod.rs
|
||||
persist.rs
|
||||
source.rs
|
||||
types.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
建议只暴露 crate 内 API:
|
||||
|
||||
```rust
|
||||
pub(crate) async fn persist_generated_media_asset(
|
||||
state: &AppState,
|
||||
request: GeneratedMediaAssetPersistRequest,
|
||||
) -> Result<GeneratedMediaAssetPersistOutput, AppError>
|
||||
```
|
||||
|
||||
Adapter 只负责媒体持久化和资产绑定,不负责:
|
||||
|
||||
- 生成 prompt、提交 provider 任务、轮询 provider 状态。
|
||||
- 玩法 draft/profile JSON 写回。
|
||||
- 运行态裁决、发布校验、作品可见性。
|
||||
- 计费、退款、钱包流水。
|
||||
- 复杂媒体失败后的业务 fallback 决策。
|
||||
|
||||
## 4. 链路 inventory 与迁移策略
|
||||
|
||||
| 链路 | 当前 owner | 媒体类型 | 来源 | Adapter 复用点 | 禁止改变 | 退出条件 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Visual Novel 背景音乐 | `vector_engine_audio_generation.rs` | audio | VectorEngine Suno/Vidu task publish URL | 下载、MIME/extension、OSS、confirm、binding | VN 场景字段、task id、错误 envelope、计费外层 | VN 音频生成成功后仍能通过 read-url/read-bytes 读取 |
|
||||
| 通用创作背景音乐/音效 | `vector_engine_audio_generation.rs` | audio | 同上 | 同上 | 不混用 VN 场景语义 | creation target entity/slot 不变 |
|
||||
| Custom World opening CG video | `custom_world_ai.rs` 或后续分层文件 | video | Ark/火山视频 task 结果 URL | 视频下载、OSS、confirm、binding | storyboard->video 顺序、固定点数计费、超时错误 | storyboard 图片仍走图片 Adapter,最终视频走 media persist |
|
||||
| Character visual reference/workflow | `character_visual_assets.rs` | image/cache metadata | GPT image helper、workflow cache | 可复用 media persist 的 source/OSS/confirm/binding;图片生成 provider 不迁入复杂媒体 | 角色 workflow cache 可空继续生成 | 角色视觉发布链路回包字段不变 |
|
||||
| Character animation publish/import | `character_animation_assets.rs` | video / image sequence | data:video base64、remote video、阶段占位 | data URL/base64 解码、视频 OSS、confirm、binding | stage1 placeholder 语义、import-video contract | 导入视频和发布视频都不再复制 OSS/confirm 代码 |
|
||||
| Match3D 背景音乐 | `match3d.rs` | audio | 现有生成/上传链路 | 仅复用音频持久化 | 不恢复 Rodin/GLB 新草稿回退 | 图片素材仍按图片 Adapter 计划处理 |
|
||||
| Puzzle 背景音乐 | `puzzle.rs` | audio | 现有生成/上传链路 | 仅复用音频持久化 | puzzle 运行态和排行榜语义不变 | `puzzle_background_music` kind/binding 不变 |
|
||||
| Hyper3D/GLB 历史代理 | `hyper3d.rs` | model/glb | Hyper3D Rodin status/download | 如存在转存需求,仅复用 media persist | Match3D 新草稿禁止回退 Rodin/GLB | 历史代理 route contract 不变 |
|
||||
|
||||
## 5. 分阶段执行
|
||||
|
||||
### D1-0:只抽类型与 source 归一
|
||||
|
||||
- 新增 media source 类型:`RemoteUrl`、`DataUrl`、`Base64Bytes`、`Bytes`、`ExistingObjectKey`。
|
||||
- 新增 `MediaAssetKind`/`GeneratedMediaAssetPersistRequest`/`GeneratedMediaAssetPersistOutput`。
|
||||
- 不接任何调用方。
|
||||
|
||||
退出条件:`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 通过;无公开 contract 变化。
|
||||
|
||||
### D1-1:接音频持久化
|
||||
|
||||
- 先接 Visual Novel 背景音乐/音效。
|
||||
- 再接通用 creation audio。
|
||||
- 保留 provider submit/poll/publish 在 `vector_engine_audio_generation.rs` 或后续 application 层。
|
||||
|
||||
退出条件:音频 asset kind、entity kind、slot、task id 和错误语义不变;重复下载/OSS/confirm 逻辑减少。
|
||||
|
||||
### D1-2:接视频持久化
|
||||
|
||||
- 接 opening CG 最终视频。
|
||||
- 接 character animation import/publish 视频。
|
||||
- data URL/base64 视频解析必须复用 source 层,禁止各 handler 再各写一份。
|
||||
|
||||
退出条件:视频入库后仍走私有资产读取链路;opening CG 固定点数计费外层不变。
|
||||
|
||||
### D1-3:接角色工作流与历史模型边界
|
||||
|
||||
- 角色视觉/动作 workflow cache 只复用持久化,不改缓存命中策略。
|
||||
- Hyper3D 只做历史代理可选转存复用,不扩大到 Match3D 新草稿。
|
||||
|
||||
退出条件:角色资产发布和历史 Hyper3D route contract 不变;文档状态更新。
|
||||
|
||||
## 6. 验收命令
|
||||
|
||||
文档或小步接线后必须运行:
|
||||
|
||||
```bash
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server vector_engine_audio_generation --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server character_animation --manifest-path server-rs/Cargo.toml
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
如果执行外部 provider smoke,只允许读取本地显式环境变量;日志、文档和测试快照中禁止写出 API key/token。
|
||||
|
||||
## 7. 完成定义
|
||||
|
||||
D1 完成必须同时满足:
|
||||
|
||||
- 至少音频和视频两类复杂媒体通过同一个 media persist 底座。
|
||||
- 图片生成仍走图片 Adapter,不被复杂媒体接口反向污染。
|
||||
- 计费外层、HTTP route、DTO、SpacetimeDB schema、OSS 私有读取链路全部不变。
|
||||
- 大 handler 中重复的下载/base64 解码/OSS 上传/asset_object confirm/entity binding 代码有明确删除或替换记录。
|
||||
- README 与本系列总纲状态同步更新。
|
||||
@@ -1,156 +0,0 @@
|
||||
# 生成图片资产 Adapter 收口执行计划
|
||||
|
||||
状态:待 C 线执行
|
||||
日期:2026-05-14
|
||||
范围:只新增/使用 `api-server` 内部生成图片资产 Adapter;不改 HTTP contract、DTO、SpacetimeDB schema、OSS 访问策略、前端行为、计费语义。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 Big Fish、Square Hole、Custom World 中重复的图片生成持久化链路收口为一个内部能力:
|
||||
|
||||
```text
|
||||
provider 生成
|
||||
-> 下载 URL 或 base64/data URL 解码
|
||||
-> MIME/extension 归一
|
||||
-> OSS private upload
|
||||
-> 可选 HEAD/存在性确认
|
||||
-> module-assets asset_object confirm
|
||||
-> asset entity binding
|
||||
-> 返回 legacy_public_path/object_key/asset_object_id/mime/extension/task_id/actual_prompt
|
||||
```
|
||||
|
||||
首批必须接入 3 个真实调用方才算完成:
|
||||
|
||||
- Big Fish 正式图片:主图、动作图、舞台背景等。
|
||||
- Square Hole 图片:作品图片槽位重生成。
|
||||
- Custom World 场景图/封面图/opening storyboard 中的稳定单图链路。
|
||||
|
||||
## 2. 建议模块边界
|
||||
|
||||
建议放在:
|
||||
|
||||
```text
|
||||
server-rs/crates/api-server/src/modules/assets/generated_image_assets/
|
||||
mod.rs
|
||||
adapter.rs
|
||||
provider.rs
|
||||
persist.rs
|
||||
types.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
对外只暴露 crate 内部 API,不进入 `shared-contracts`:
|
||||
|
||||
```rust
|
||||
pub(crate) async fn generate_and_persist_image(
|
||||
state: &AppState,
|
||||
request: GeneratedImageAssetRequest,
|
||||
) -> Result<GeneratedImageAssetOutput, AppError>
|
||||
```
|
||||
|
||||
Adapter 不做:
|
||||
|
||||
- 不扣费、不退款、不判断钱包。
|
||||
- 不生成玩法 prompt。
|
||||
- 不修改玩法 draft/profile JSON。
|
||||
- 不决定 retry/fallback 策略,除通用下载/入库错误映射外。
|
||||
- 不写 SpacetimeDB schema/procedure。
|
||||
|
||||
## 3. Interface 草案
|
||||
|
||||
### 3.1 输入字段
|
||||
|
||||
- provider:`OpenAiImage` / `VectorEngineGptImage2` / `DashScopeTextToImage` / `PreGeneratedImage`。
|
||||
- prompt / negative_prompt / size / count。
|
||||
- reference_images:data URL、object key、remote URL,由调用方先裁定安全来源。
|
||||
- output:OSS prefix、path segments、file name stem、access policy。
|
||||
- asset:asset_kind、entity_kind、entity_id、slot、owner_user_id、profile_id、source_job_id、metadata。
|
||||
- post_process:可选透明背景、裁剪、MIME 强制转换;首版只接入已有透明背景后处理,不新增玩法规则。
|
||||
- fallback_policy:调用方指定 `ReturnDataUrlOnPersistFailure` / `FailFast` 等,默认 fail fast。
|
||||
|
||||
### 3.2 输出字段
|
||||
|
||||
- `legacy_public_path`
|
||||
- `object_key`
|
||||
- `asset_object_id`
|
||||
- `mime_type`
|
||||
- `extension`
|
||||
- `task_id`
|
||||
- `actual_prompt`
|
||||
- `width/height`(若当前链路已有则保留,没有不强行新增 contract)
|
||||
- `metadata`
|
||||
|
||||
## 4. 图片资产 inventory
|
||||
|
||||
| 调用方 | provider | 下载/解码 | OSS prefix | asset kind | entity binding | 计费位置 | 降级行为 | Adapter 迁移策略 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Big Fish 正式图 | DashScope `wan2.2-t2i-flash`,`DASHSCOPE_BASE_URL`/API key 配置 | 创建 task、轮询状态、HTTP GET 图片 URL;可选透明背景后处理 | `LegacyAssetPrefix::BigFishAssets` | 由请求 assetKind 映射主鱼主图、动作图、舞台背景、等级资产等 | `BIG_FISH_ENTITY_KIND = big_fish_session`,绑定 session/entity id + slot | `big_fish.rs` 在调用正式图片动作处 `execute_billable_asset_operation` | 上游失败显式错误;gallery 另有 Spacetime runtime/connect dropped 软降级,不属于 Adapter | 先抽 DashScope provider + persist,保留 prompt/assetKind 校验在 Big Fish application 层 |
|
||||
| Square Hole 图片重生成 | `openai_image_generation.rs` OpenAI/VectorEngine GPT image helper | `create_openai_image_generation` 返回 URL/base64/data URL 后下载/解码 | `LegacyAssetPrefix::SquareHoleAssets` | 方洞作品图片槽位相关 kind | profile/work entity + image slot | 调用方现有计费包裹保持 | 生成成功但入库失败返回 Data URL 的兼容行为必须保留 | 使用 `fallback_policy = ReturnDataUrlOnPersistFailure`;slot/作品 JSON 更新仍在 Square Hole 层 |
|
||||
| Custom World 场景图 | VectorEngine GPT image 2 / OpenAI helper | URL 下载或 base64 解码 | `LegacyAssetPrefix::CustomWorldScenes` | scene image kind | profile/landmark/scene entity slot | `custom_world_ai.rs` 场景图调用处包裹 | entity/scene 文本生成存在 fallback;图片入库失败按当前错误口径 | 抽 `persist_custom_world_asset` 中图片分支;scene/npc/profile 更新留在 Custom World 层 |
|
||||
| Custom World 封面图 | VectorEngine GPT image 2 / OpenAI helper | 同上 | Custom World cover prefix/segments | cover image kind | `custom_world_profile`/profile id + cover slot | `execute_billable_asset_operation` | 失败显式错误 | 接入同一 Adapter,保留 cover upload 手动上传链路不混入生成图 provider |
|
||||
| Opening CG storyboard | GPT image 2 | 生成 storyboard 图片并下载 | Custom World opening prefix/segments | `custom_world_opening_cg_storyboard` | `custom_world_profile` + `opening_cg_storyboard` | opening CG 固定点数计费外层 | storyboard 成功后才进入视频;失败显式错误 | 可复用图片 Adapter,但 opening video 仍归复杂媒体计划 |
|
||||
|
||||
## 5. 迁移步骤
|
||||
|
||||
### 阶段 C1:抽只读类型和 persist 能力
|
||||
|
||||
- 新增 `GeneratedImageAssetRequest/Output`。
|
||||
- 抽 OSS put、asset_object confirm、entity binding 公共 persist。
|
||||
- 先不接 provider,允许调用方传入已下载图片 bytes。
|
||||
- 迁移目标:Square Hole 或 Custom World 中最小一条 persist helper。
|
||||
|
||||
退出条件:一条链路编译通过;原 HTTP 回包不变。
|
||||
|
||||
### 阶段 C2:接 OpenAI/VectorEngine provider
|
||||
|
||||
- 封装 `openai_image_generation.rs` 的 settings/client/create/download 结果归一。
|
||||
- Square Hole 接入 provider + persist。
|
||||
- Custom World 场景图/封面图接入。
|
||||
|
||||
退出条件:Square Hole 生成成功但入库失败回退 Data URL 的行为仍被测试或代码路径覆盖。
|
||||
|
||||
### 阶段 C3:接 DashScope provider
|
||||
|
||||
- 抽 Big Fish DashScope settings/client/task create/poll/download。
|
||||
- 保留 Big Fish assetKind、level、motionKey、prompt 构造、透明背景策略在 Big Fish 层。
|
||||
- 正式图入库走同一 persist。
|
||||
|
||||
退出条件:Big Fish 不再有独立 OSS + asset_object + binding 重复实现;计费外层不变。
|
||||
|
||||
### 阶段 C4:删除重复 helper
|
||||
|
||||
- 删除已迁移的私有 persist/download helper。
|
||||
- 保留 provider 特定错误 message 与现有 envelope 尽量一致。
|
||||
- 更新本系列文档状态。
|
||||
|
||||
退出条件:三类调用方都经过 Adapter;旧 helper 无未使用残留。
|
||||
|
||||
## 6. 单 owner 与并行规则
|
||||
|
||||
单 owner:
|
||||
|
||||
- `modules/assets/generated_image_assets/*`
|
||||
- `openai_image_generation.rs` 如果需要改 public helper
|
||||
- `asset_billing.rs` 禁止本阶段修改,除非单独批准
|
||||
|
||||
可并行:
|
||||
|
||||
- Big Fish 接入 owner 只改 `big_fish.rs` 和对应 tests。
|
||||
- Square Hole 接入 owner 只改 `square_hole.rs`。
|
||||
- Custom World 接入 owner 只改 `custom_world_ai.rs`。
|
||||
|
||||
合并顺序必须是:Adapter skeleton -> Square Hole/Custom World -> Big Fish -> 清理。
|
||||
|
||||
## 7. 验证命令
|
||||
|
||||
```bash
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server big_fish --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server square_hole --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server custom_world --manifest-path server-rs/Cargo.toml
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
如执行外部 provider smoke,只允许使用显式本地环境变量;不要在日志或文档中写出 API key/token。
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
const MINI_PROGRAM_CLIENT_TYPE = 'mini_program';
|
||||
const MINI_PROGRAM_CLIENT_RUNTIME = 'wechat_mini_program';
|
||||
const CLIENT_INSTANCE_STORAGE_KEY = 'genarrative:mini-program-client-instance-id';
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
|
||||
function isConfiguredEntryUrl(value) {
|
||||
const trimmed = String(value || '').trim();
|
||||
@@ -273,6 +274,20 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const result = wx.getStorageSync(PAY_RESULT_STORAGE_KEY);
|
||||
if (!result || !this.data.webViewUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
wx.removeStorageSync(PAY_RESULT_STORAGE_KEY);
|
||||
this.setData({
|
||||
webViewUrl: appendHashParams(this.data.webViewUrl, {
|
||||
wx_pay_result: result,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async handleGetPhoneNumber(event) {
|
||||
if (!this.data.authResult || !this.data.authResult.token) {
|
||||
this.handleRetryLogin();
|
||||
|
||||
@@ -30,18 +30,25 @@ function requestPayment(payParams) {
|
||||
});
|
||||
}
|
||||
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
|
||||
function appendPayResult(url, requestId, status) {
|
||||
const value = `${requestId}:${status}`;
|
||||
const hashIndex = String(url || '').indexOf('#');
|
||||
const baseUrl =
|
||||
hashIndex >= 0 ? String(url).slice(0, hashIndex) : String(url || '');
|
||||
const rawHash = hashIndex >= 0 ? String(url).slice(hashIndex + 1) : '';
|
||||
const params = new URLSearchParams(rawHash);
|
||||
params.set('wx_pay_result', value);
|
||||
return `${baseUrl}#${params.toString()}`;
|
||||
const nextHash = rawHash
|
||||
.split('&')
|
||||
.filter((part) => part && !part.startsWith('wx_pay_result='))
|
||||
.concat(`wx_pay_result=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
return `${baseUrl}#${nextHash}`;
|
||||
}
|
||||
|
||||
function notifyPreviousWebView(requestId, status) {
|
||||
const result = `${requestId}:${status}`;
|
||||
wx.setStorageSync(PAY_RESULT_STORAGE_KEY, result);
|
||||
const pages = getCurrentPages();
|
||||
const previousPage = pages.length >= 2 ? pages[pages.length - 2] : null;
|
||||
if (previousPage && typeof previousPage.setData === 'function') {
|
||||
|
||||
130
packages/shared/src/contracts/barkBattle.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
BARK_BATTLE_DIFFICULTY_PRESETS,
|
||||
type BarkBattleDraftConfig,
|
||||
type BarkBattleFinishResponse,
|
||||
type BarkBattlePersonalBestSummary,
|
||||
type BarkBattleWorkStats,
|
||||
} from './barkBattle';
|
||||
|
||||
describe('Bark Battle shared contracts', () => {
|
||||
test('default draft config fixture uses normal difficulty and camelCase fields', () => {
|
||||
const draft: BarkBattleDraftConfig = {
|
||||
draftId: 'draft-bark-1',
|
||||
title: '汪汪声浪挑战',
|
||||
description: '轻配置草稿',
|
||||
themePreset: 'city-park',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
updatedAt: '2026-05-13T03:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(BARK_BATTLE_DIFFICULTY_PRESETS).toEqual(['easy', 'normal', 'hard']);
|
||||
expect(draft.difficultyPreset).toBe('normal');
|
||||
expect(Object.keys(draft)).toEqual([
|
||||
'draftId',
|
||||
'title',
|
||||
'description',
|
||||
'themePreset',
|
||||
'playerDogSkinPreset',
|
||||
'opponentDogSkinPreset',
|
||||
'difficultyPreset',
|
||||
'leaderboardEnabled',
|
||||
'updatedAt',
|
||||
]);
|
||||
});
|
||||
|
||||
test('finish accepted player_win fixture exposes backend adjudication result', () => {
|
||||
const response: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-1',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'hard',
|
||||
serverResult: 'player_win',
|
||||
scoreSummary: {
|
||||
finalEnergy: 87,
|
||||
triggerCount: 42,
|
||||
maxVolume: 0.96,
|
||||
averageVolume: 0.61,
|
||||
comboMax: 9,
|
||||
durationMs: 30000,
|
||||
},
|
||||
leaderboardScore: 870429630,
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
|
||||
expect(response.status).toBe('accepted');
|
||||
expect(response.serverResult).toBe('player_win');
|
||||
expect(response.scoreSummary.finalEnergy).toBe(87);
|
||||
expect(response.antiCheatFlags).toEqual([]);
|
||||
});
|
||||
|
||||
test('work stats fixture tracks starts, finishes, result counts, flags and energy summary', () => {
|
||||
const stats: BarkBattleWorkStats = {
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
playStartCount: 18,
|
||||
finishCount: 15,
|
||||
winCount: 8,
|
||||
drawCount: 2,
|
||||
lossCount: 5,
|
||||
flaggedCount: 1,
|
||||
leaderboardEntryCount: 7,
|
||||
bestLeaderboardScore: 930389410,
|
||||
bestFinalEnergy: 93,
|
||||
averageFinalEnergy: 41.25,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(stats.playStartCount).toBe(18);
|
||||
expect(stats.finishCount).toBe(15);
|
||||
expect(stats.winCount + stats.drawCount + stats.lossCount).toBe(15);
|
||||
expect(stats.flaggedCount).toBe(1);
|
||||
expect(stats.bestFinalEnergy).toBeGreaterThan(stats.averageFinalEnergy);
|
||||
});
|
||||
|
||||
test('optional score fields may be omitted instead of serialized as null', () => {
|
||||
const finishWithoutLeaderboard: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-no-rank',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
serverResult: 'draw',
|
||||
scoreSummary: {
|
||||
finalEnergy: 50,
|
||||
triggerCount: 12,
|
||||
maxVolume: 0.7,
|
||||
averageVolume: 0.5,
|
||||
comboMax: 3,
|
||||
durationMs: 30000,
|
||||
},
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
const personalBestWithoutWin: BarkBattlePersonalBestSummary = {
|
||||
workId: 'work-bark-1',
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
winCount: 0,
|
||||
drawCount: 1,
|
||||
lossCount: 2,
|
||||
finishCount: 3,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect('leaderboardScore' in finishWithoutLeaderboard).toBe(false);
|
||||
expect('bestLeaderboardScore' in personalBestWithoutWin).toBe(false);
|
||||
expect('bestFinalEnergy' in personalBestWithoutWin).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
218
packages/shared/src/contracts/barkBattle.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
export const BARK_BATTLE_DIFFICULTY_PRESETS = [
|
||||
'easy',
|
||||
'normal',
|
||||
'hard',
|
||||
] as const;
|
||||
|
||||
export type BarkBattleDifficultyPreset =
|
||||
(typeof BARK_BATTLE_DIFFICULTY_PRESETS)[number];
|
||||
|
||||
export type BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw';
|
||||
|
||||
export type BarkBattleFinishStatus =
|
||||
| 'accepted'
|
||||
| 'accepted_with_flags'
|
||||
| 'rejected';
|
||||
|
||||
export type BarkBattlePlayTypeId = 'bark-battle';
|
||||
|
||||
export interface BarkBattleConfigEditorPayload {
|
||||
title: string;
|
||||
description?: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
leaderboardEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftCreateRequest extends BarkBattleConfigEditorPayload {}
|
||||
|
||||
export interface BarkBattleWorkPublishRequest {
|
||||
draftId: string;
|
||||
workId?: string;
|
||||
publishedSnapshot?: BarkBattleConfigEditorPayload;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftConfig extends BarkBattleConfigEditorPayload {
|
||||
draftId: string;
|
||||
workId?: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePublishedConfig {
|
||||
workId: string;
|
||||
draftId?: string | null;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
title: string;
|
||||
description?: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
leaderboardEnabled: boolean;
|
||||
updatedAt: string;
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRuntimeConfig {
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
durationMs: number;
|
||||
energyMin: number;
|
||||
energyMax: number;
|
||||
drawThreshold: number;
|
||||
minBarkGapMs: number;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
leaderboardEnabled: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartRequest {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
sourceRoute?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartResponse {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
runtimeConfig: BarkBattleRuntimeConfig;
|
||||
serverStartedAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleDerivedMetrics {
|
||||
triggerCount: number;
|
||||
maxVolume: number;
|
||||
averageVolume: number;
|
||||
finalEnergy: number;
|
||||
comboMax: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunFinishRequest {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
clientStartedAt: string;
|
||||
clientFinishedAt: string;
|
||||
durationMs: number;
|
||||
derivedMetrics: BarkBattleDerivedMetrics;
|
||||
clientResult?: BarkBattleServerResult;
|
||||
sampleDigest?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleScoreSummary extends BarkBattleDerivedMetrics {
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleFinishResponse {
|
||||
status: BarkBattleFinishStatus;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardEntry {
|
||||
rank: number;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
displayName: string;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardResponse {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
entries: BarkBattleLeaderboardEntry[];
|
||||
viewerBest?: BarkBattleLeaderboardEntry | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryItem {
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalBestSummary {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
bestTriggerCount?: number;
|
||||
bestMaxVolume?: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
finishCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryResponse {
|
||||
workId?: string;
|
||||
difficultyPreset?: BarkBattleDifficultyPreset;
|
||||
items: BarkBattlePersonalHistoryItem[];
|
||||
bestSummary?: BarkBattlePersonalBestSummary | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleWorkStats {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
playStartCount: number;
|
||||
finishCount: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
flaggedCount: number;
|
||||
leaderboardEntryCount: number;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
averageFinalEnergy?: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
85
packages/shared/src/contracts/edutainmentBabyDrawing.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_ID = 'baby-love-drawing';
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_NAME = '宝贝爱画';
|
||||
export const BABY_LOVE_DRAWING_EDUTAINMENT_TAG = '寓教于乐';
|
||||
|
||||
export type BabyLoveDrawingTemplateId =
|
||||
typeof BABY_LOVE_DRAWING_TEMPLATE_ID;
|
||||
|
||||
export type BabyLoveDrawingTool = 'brush' | 'eraser';
|
||||
|
||||
export type BabyLoveDrawingSaveMode =
|
||||
| 'original-only'
|
||||
| 'original-and-magic';
|
||||
|
||||
export type BabyLoveDrawingGenerationProvider =
|
||||
| 'vector-engine-gpt-image-2'
|
||||
| 'local-demo';
|
||||
|
||||
export type BabyLoveDrawingPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
t: number;
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingStroke = {
|
||||
strokeId: string;
|
||||
tool: BabyLoveDrawingTool;
|
||||
color: string;
|
||||
points: BabyLoveDrawingPoint[];
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingRecord = {
|
||||
drawingId: string;
|
||||
templateId: BabyLoveDrawingTemplateId;
|
||||
templateName: typeof BABY_LOVE_DRAWING_TEMPLATE_NAME;
|
||||
originalImageSrc: string;
|
||||
magicImageSrc: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
saveMode: BabyLoveDrawingSaveMode;
|
||||
themeTags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicRequest = {
|
||||
originalImageSrc: string;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicResponse = {
|
||||
magicImageSrc: string;
|
||||
generationProvider: BabyLoveDrawingGenerationProvider;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingRequest = {
|
||||
originalImageSrc: string;
|
||||
magicImageSrc?: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingResponse = {
|
||||
record: BabyLoveDrawingRecord;
|
||||
};
|
||||
|
||||
export const BABY_LOVE_DRAWING_RAINBOW_COLORS = [
|
||||
{ id: 'red', label: '红', value: '#ef4444' },
|
||||
{ id: 'orange', label: '橙', value: '#f97316' },
|
||||
{ id: 'yellow', label: '黄', value: '#facc15' },
|
||||
{ id: 'green', label: '绿', value: '#22c55e' },
|
||||
{ id: 'cyan', label: '青', value: '#06b6d4' },
|
||||
{ id: 'blue', label: '蓝', value: '#3b82f6' },
|
||||
{ id: 'purple', label: '紫', value: '#a855f7' },
|
||||
] as const;
|
||||
|
||||
export type BabyLoveDrawingRainbowColorId =
|
||||
(typeof BABY_LOVE_DRAWING_RAINBOW_COLORS)[number]['id'];
|
||||
|
||||
export function normalizeBabyLoveDrawingTags(tags: string[]) {
|
||||
return [
|
||||
...new Set([
|
||||
BABY_LOVE_DRAWING_EDUTAINMENT_TAG,
|
||||
...tags.map((tag) => tag.trim()).filter(Boolean),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -2,8 +2,7 @@ export const BABY_OBJECT_MATCH_TEMPLATE_ID = 'baby-object-match';
|
||||
export const BABY_OBJECT_MATCH_TEMPLATE_NAME = '宝贝识物';
|
||||
export const BABY_OBJECT_MATCH_EDUTAINMENT_TAG = '寓教于乐';
|
||||
|
||||
export type BabyObjectMatchTemplateId =
|
||||
typeof BABY_OBJECT_MATCH_TEMPLATE_ID;
|
||||
export type BabyObjectMatchTemplateId = typeof BABY_OBJECT_MATCH_TEMPLATE_ID;
|
||||
|
||||
export type BabyObjectMatchAssetProvider =
|
||||
| 'vector-engine-gpt-image-2'
|
||||
@@ -20,6 +19,27 @@ export type BabyObjectMatchItemAsset = {
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchVisualAssetKind =
|
||||
| 'background'
|
||||
| 'ui-frame'
|
||||
| 'gift-box'
|
||||
| 'basket'
|
||||
| 'smoke-puff';
|
||||
|
||||
export type BabyObjectMatchVisualAsset = {
|
||||
assetId: string;
|
||||
assetKind: BabyObjectMatchVisualAssetKind;
|
||||
imageSrc: string;
|
||||
assetObjectId: string | null;
|
||||
generationProvider: BabyObjectMatchAssetProvider;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchVisualPackage = {
|
||||
themePrompt: string;
|
||||
assets: BabyObjectMatchVisualAsset[];
|
||||
};
|
||||
|
||||
export type BabyObjectMatchDraft = {
|
||||
draftId: string;
|
||||
profileId: string;
|
||||
@@ -29,6 +49,7 @@ export type BabyObjectMatchDraft = {
|
||||
workDescription: string;
|
||||
itemNames: [string, string];
|
||||
itemAssets: [BabyObjectMatchItemAsset, BabyObjectMatchItemAsset];
|
||||
visualPackage?: BabyObjectMatchVisualPackage | null;
|
||||
themeTags: string[];
|
||||
publicationStatus: BabyObjectMatchPublicationStatus;
|
||||
createdAt: string;
|
||||
@@ -41,6 +62,15 @@ export type CreateBabyObjectMatchDraftRequest = {
|
||||
itemBName: string;
|
||||
};
|
||||
|
||||
export type GenerateBabyObjectMatchAssetsRequest = {
|
||||
itemNames: [string, string];
|
||||
};
|
||||
|
||||
export type GenerateBabyObjectMatchAssetsResponse = {
|
||||
assets: [BabyObjectMatchItemAsset, BabyObjectMatchItemAsset];
|
||||
visualPackage?: BabyObjectMatchVisualPackage | null;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchDraftResponse = {
|
||||
draft: BabyObjectMatchDraft;
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@ export type * from './creationAudio';
|
||||
export type * from './hyper3d';
|
||||
export type * from './puzzleCreativeTemplate';
|
||||
export type * from './visualNovel';
|
||||
export type * from './barkBattle';
|
||||
|
||||
@@ -73,7 +73,9 @@ export interface PersistMatch3DGeneratedModelResponse {
|
||||
|
||||
export interface GenerateMatch3DCoverImageRequest {
|
||||
prompt: string;
|
||||
uploadedImageSrc?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
referenceImageSrcs?: string[];
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DCoverImageResponse {
|
||||
@@ -95,8 +97,23 @@ export interface GenerateMatch3DBackgroundImageResponse {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DContainerImageRequest {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DContainerImageResponse {
|
||||
item: Match3DWorkProfile;
|
||||
containerImageSrc: string;
|
||||
containerImageObjectKey: string;
|
||||
generatedBackgroundAsset: Match3DGeneratedBackgroundAsset;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export type GenerateMatch3DItemAssetsMode = 'append' | 'replace';
|
||||
|
||||
export interface GenerateMatch3DItemAssetsRequest {
|
||||
itemNames: string[];
|
||||
mode?: GenerateMatch3DItemAssetsMode;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DItemAssetsResponse {
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface PuzzleRuntimeLevelSnapshot {
|
||||
themeTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
uiBackgroundImageSrc?: string | null;
|
||||
uiBackgroundImageObjectKey?: string | null;
|
||||
backgroundMusic?: CreationAudioAsset | null;
|
||||
board: PuzzleBoardSnapshot;
|
||||
status: PuzzleRuntimeLevelStatus;
|
||||
|
||||
@@ -158,6 +158,11 @@ export type CreateProfileRechargeOrderResponse = {
|
||||
wechatMiniProgramPayParams?: WechatMiniProgramPayParams | null;
|
||||
};
|
||||
|
||||
export type ConfirmWechatProfileRechargeOrderResponse = {
|
||||
order: ProfileRechargeOrder;
|
||||
center: ProfileRechargeCenterResponse;
|
||||
};
|
||||
|
||||
export type ProfileFeedbackStatus = 'open';
|
||||
|
||||
export type ProfileFeedbackEvidenceItemInput = {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type * from './contracts/creationAgentDocumentInput';
|
||||
export type * from './contracts/creationAudio';
|
||||
export type * from './contracts/creativeAgent';
|
||||
export type * from './contracts/customWorldAgent';
|
||||
export * from './contracts/edutainmentBabyDrawing';
|
||||
export * from './contracts/edutainmentBabyObject';
|
||||
export type * from './contracts/hyper3d';
|
||||
export * from './contracts/match3dAgent';
|
||||
|
||||
@@ -12,7 +12,16 @@
|
||||
"outputPath": ""
|
||||
},
|
||||
"useCompilerPlugins": false,
|
||||
"minifyWXML": true
|
||||
"minifyWXML": true,
|
||||
"compileWorklet": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"packNpmManually": false,
|
||||
"minifyWXSS": true,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"miniprogramRoot": "miniprogram/",
|
||||
@@ -22,5 +31,6 @@
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx3da23ea14ca66b65",
|
||||
"editorSetting": {}
|
||||
}
|
||||
"editorSetting": {},
|
||||
"libVersion": "3.15.2"
|
||||
}
|
||||
@@ -2,13 +2,20 @@
|
||||
"libVersion": "3.15.2",
|
||||
"projectname": "Genarrative",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"urlCheck": false,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true
|
||||
"compileHotReLoad": true,
|
||||
"useApiHook": true,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false
|
||||
}
|
||||
}
|
||||
BIN
public/anthro-cat-illustrations/cat-astronaut.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/anthro-cat-illustrations/cat-barista.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/anthro-cat-illustrations/cat-dancer.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/anthro-cat-illustrations/cat-detective.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
public/anthro-cat-illustrations/cat-knight.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/anthro-cat-illustrations/cat-painter.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/branding/taonier-logo-concepts/taonier-clay-spark.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/branding/taonier-logo-concepts/taonier-creation-loop.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 699 KiB |
BIN
public/branding/taonier-logo-concepts/taonier-meme-bubble.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/branding/taonier-logo-concepts/taonier-play-mold.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/branding/taonier-logo-concepts/taonier-premium-seal.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 846 KiB |
|
After Width: | Height: | Size: 927 KiB |
|
After Width: | Height: | Size: 840 KiB |
|
After Width: | Height: | Size: 509 KiB |
|
After Width: | Height: | Size: 584 KiB |
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 912 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 886 KiB |
|
After Width: | Height: | Size: 699 KiB |
|
After Width: | Height: | Size: 637 KiB |
|
After Width: | Height: | Size: 901 KiB |
|
After Width: | Height: | Size: 715 KiB |
BIN
public/branding/taonier-logo-v3-concepts/taonier-v3-seed-pop.png
Normal file
|
After Width: | Height: | Size: 1016 KiB |
BIN
public/branding/taonier-logo-v3-concepts/taonier-v3-soft-t.png
Normal file
|
After Width: | Height: | Size: 848 KiB |
BIN
public/branding/taonier-logo-v3-concepts/taonier-v3-work-gem.png
Normal file
|
After Width: | Height: | Size: 900 KiB |
BIN
public/child-motion-demo/picture-book-ground-ring-v3.png
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-arm-guide-v4.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-arm-guide-v5.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-arm-guide-v6.png
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-body-guide-v4.png
Normal file
|
After Width: | Height: | Size: 606 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-body-guide-v5.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
public/child-motion-demo/picture-book-wave-cat-body-guide-v6.png
Normal file
|
After Width: | Height: | Size: 784 KiB |