Compare commits
55 Commits
b2ac92e0fc
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
| ea0b67a951 | |||
| 54968701f0 | |||
| 7cea41c911 | |||
| 928acb4302 | |||
| fda996031f | |||
| 10ed4fa051 | |||
| ac2cf78ffa | |||
| 5edfb756c7 | |||
| 0c9254502c | |||
| 0461c0ee41 | |||
| 81f57ea5ce | |||
| d23cf3807d | |||
| 6c1579a786 | |||
| 793d82cccd | |||
| 5cc8293380 | |||
| 85ed8ca90c | |||
| d0a9348e72 | |||
| 192accd796 | |||
| 54c2d6de47 | |||
| 7f2461313e | |||
| f74717c415 | |||
| 75bca28191 | |||
| 46a254f142 | |||
| 86fc382413 | |||
| 643161a168 | |||
| d6219f1a0c | |||
| 35d63f5b2e | |||
| 1c16152708 | |||
| f6084d0910 | |||
| 6ed6859855 | |||
|
|
9b39a52049 | ||
|
|
fc54bff62f | ||
| 1767bed609 | |||
| dada5a4797 | |||
| 7e608d4230 | |||
| 3ad1075227 | |||
| 32a1530ab1 | |||
| 7c8aa1e124 | |||
| 641d91cf11 | |||
| 052dbc248b | |||
| bc704d0c22 | |||
| a0ed128bde | |||
| 80a4183b45 | |||
| 8669a996ca | |||
| 9ca66715a4 | |||
| e390b72a0c | |||
| cf9fb5ac40 | |||
| a1e5c2150c | |||
| 23ba2703b4 | |||
| 96df12cd15 | |||
| 65c2b8cd79 | |||
| 199b44c18c | |||
| e410f7974e | |||
| 94975e4735 | |||
| abf1f1ebea |
1
.codex/skills/behavior-driven-development
Symbolic link
1
.codex/skills/behavior-driven-development
Symbolic link
@@ -0,0 +1 @@
|
||||
C:/proj/Genarrative/.hermes/skills/behavior-driven-development
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: gpt-image-2-apimart
|
||||
description: Generate or inspect project image assets through this repository's APIMart OpenAI-compatible gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug APIMART_BASE_URL / APIMART_API_KEY image-generation configuration without exposing secrets.
|
||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
---
|
||||
|
||||
# gpt-image-2 APIMart
|
||||
# gpt-image-2 VectorEngine
|
||||
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` APIMart `gpt-image-2` path.
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -24,15 +24,15 @@ Use this skill for project-local image asset generation that must match the repo
|
||||
```
|
||||
|
||||
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
||||
6. Never print `APIMART_API_KEY`. Report only whether configuration exists.
|
||||
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
|
||||
|
||||
## Request Contract
|
||||
|
||||
The repository image path uses:
|
||||
|
||||
```text
|
||||
POST {APIMART_BASE_URL}/images/generations
|
||||
Authorization: Bearer {APIMART_API_KEY}
|
||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
@@ -40,10 +40,10 @@ Default body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-image-2",
|
||||
"model": "gpt-image-2-all",
|
||||
"prompt": "<prompt>",
|
||||
"n": 1,
|
||||
"size": "1:1"
|
||||
"size": "1024x1024"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,17 +51,11 @@ For a reference image, add:
|
||||
|
||||
```json
|
||||
{
|
||||
"image_urls": ["data:image/png;base64,..."]
|
||||
"image": ["data:image/png;base64,..."]
|
||||
}
|
||||
```
|
||||
|
||||
Poll async responses with:
|
||||
|
||||
```text
|
||||
GET {APIMART_BASE_URL}/tasks/{task_id}
|
||||
```
|
||||
|
||||
Accept image output from `data[].url`, `data[].b64_json`, direct nested `url` fields, or async task results.
|
||||
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
|
||||
|
||||
@@ -69,12 +63,12 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
|
||||
|
||||
Required for live generation:
|
||||
|
||||
- `APIMART_BASE_URL`
|
||||
- `APIMART_API_KEY`
|
||||
- `VECTOR_ENGINE_BASE_URL`
|
||||
- `VECTOR_ENGINE_API_KEY`
|
||||
|
||||
Optional:
|
||||
|
||||
- `APIMART_IMAGE_REQUEST_TIMEOUT_MS`
|
||||
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
|
||||
|
||||
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
interface:
|
||||
display_name: "GPT Image 2 APIMart"
|
||||
short_description: "Generate project thumbnails through APIMart"
|
||||
display_name: "GPT Image 2 VectorEngine"
|
||||
short_description: "Generate project thumbnails through VectorEngine"
|
||||
brand_color: "#10B981"
|
||||
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through APIMart."
|
||||
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -14,7 +14,6 @@ const promptsPath = path.join(
|
||||
);
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
||||
const defaultTimeoutMs = 180000;
|
||||
const pollDelayMs = 3000;
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 1) {
|
||||
@@ -66,15 +65,23 @@ function resolveEnv() {
|
||||
...process.env,
|
||||
};
|
||||
return {
|
||||
baseUrl: String(loaded.APIMART_BASE_URL || '').trim().replace(/\/+$/u, ''),
|
||||
apiKey: String(loaded.APIMART_API_KEY || '').trim(),
|
||||
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
|
||||
.trim()
|
||||
.replace(/\/+$/u, ''),
|
||||
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
|
||||
timeoutMs: Number.parseInt(
|
||||
String(loaded.APIMART_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
||||
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(template) {
|
||||
return [
|
||||
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
|
||||
@@ -124,14 +131,6 @@ function extractBase64Images(payload) {
|
||||
return values;
|
||||
}
|
||||
|
||||
function extractTaskId(payload) {
|
||||
const ids = [];
|
||||
collectStringsByKey(payload, 'task_id', ids);
|
||||
collectStringsByKey(payload, 'taskId', ids);
|
||||
collectStringsByKey(payload, 'id', ids);
|
||||
return ids[0] || null;
|
||||
}
|
||||
|
||||
function inferExtensionFromContentType(contentType) {
|
||||
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
||||
if (normalized === 'image/png') {
|
||||
@@ -172,9 +171,14 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`APIMart ${response.status}: ${text.slice(0, 600)}`);
|
||||
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);
|
||||
}
|
||||
@@ -195,56 +199,30 @@ async function downloadUrl(url, timeoutMs) {
|
||||
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 waitForTask(env, taskId) {
|
||||
const deadline = Date.now() + env.timeoutMs;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const payload = await fetchJson(
|
||||
`${env.baseUrl}/tasks/${encodeURIComponent(taskId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
},
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
const statuses = [];
|
||||
collectStringsByKey(payload, 'status', statuses);
|
||||
collectStringsByKey(payload, 'task_status', statuses);
|
||||
const status = String(statuses[0] || '').trim().toLowerCase();
|
||||
|
||||
if (['completed', 'succeeded', 'success'].includes(status)) {
|
||||
return payload;
|
||||
}
|
||||
if (['failed', 'error', 'canceled', 'cancelled', 'unknown'].includes(status)) {
|
||||
throw new Error(`APIMart task ${taskId} failed: ${JSON.stringify(payload).slice(0, 600)}`);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
|
||||
}
|
||||
|
||||
throw new Error(`APIMart task ${taskId} timed out`);
|
||||
}
|
||||
|
||||
async function generateOne(env, template, outDir) {
|
||||
const requestBody = {
|
||||
model: 'gpt-image-2',
|
||||
model: 'gpt-image-2-all',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1:1',
|
||||
size: '1024x1024',
|
||||
};
|
||||
const payload = await fetchJson(
|
||||
`${env.baseUrl}/images/generations`,
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
@@ -252,12 +230,8 @@ async function generateOne(env, template, outDir) {
|
||||
env.timeoutMs,
|
||||
);
|
||||
|
||||
const resolvedPayload =
|
||||
extractImageUrls(payload).length || extractBase64Images(payload).length
|
||||
? payload
|
||||
: await waitForTask(env, extractTaskId(payload));
|
||||
const urls = extractImageUrls(resolvedPayload);
|
||||
const b64Images = extractBase64Images(resolvedPayload);
|
||||
const urls = extractImageUrls(payload);
|
||||
const b64Images = extractBase64Images(payload);
|
||||
|
||||
let image;
|
||||
if (urls[0]) {
|
||||
@@ -269,7 +243,7 @@ async function generateOne(env, template, outDir) {
|
||||
extension: inferExtensionFromBytes(bytes),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`APIMart returned no image for ${template.id}`);
|
||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
}
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
@@ -301,10 +275,10 @@ if (dryRun) {
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
body: {
|
||||
model: 'gpt-image-2',
|
||||
model: 'gpt-image-2-all',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1:1',
|
||||
size: '1024x1024',
|
||||
},
|
||||
})),
|
||||
},
|
||||
@@ -320,7 +294,7 @@ if (!env.baseUrl || !env.apiKey) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: 'Missing APIMART_BASE_URL or APIMART_API_KEY',
|
||||
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
|
||||
hasBaseUrl: Boolean(env.baseUrl),
|
||||
hasApiKey: Boolean(env.apiKey),
|
||||
}),
|
||||
|
||||
@@ -173,6 +173,10 @@ VITE_SCENE_IMAGE_REQUEST_TIMEOUT_MS="150000"
|
||||
# Keep this off by default for cleaner logs.
|
||||
VITE_LLM_DEBUG_LOG="false"
|
||||
|
||||
# Optional: global frontend debug mode. When empty, it follows Vite dev mode.
|
||||
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
|
||||
VITE_DEBUG_MODE=""
|
||||
|
||||
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||
# the fixed `bge-large-zh` embedding model.
|
||||
|
||||
@@ -60,8 +60,9 @@ ALIYUN_OSS_ACCESS_KEY_ID="LTAI5t7aiyw6uDFW4miJvU8f"
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS"
|
||||
|
||||
# Local Rust backend target for Vite dev proxy.
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:3100"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:3100"
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:8082"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:8082"
|
||||
GENARRATIVE_API_PORT="8082"
|
||||
|
||||
GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3101"
|
||||
GENARRATIVE_SPACETIME_DATABASE="xushi-p4wfr"
|
||||
@@ -70,4 +71,4 @@ GENARRATIVE_SPACETIME_TOKEN=""
|
||||
# admin
|
||||
GENARRATIVE_ADMIN_USERNAME=admin
|
||||
GENARRATIVE_ADMIN_PASSWORD=123456
|
||||
ADMIN_API_TARGET=http://127.0.0.1:8082
|
||||
ADMIN_API_TARGET=http://127.0.0.1:3100
|
||||
|
||||
@@ -24,9 +24,47 @@
|
||||
│ ├─ pitfalls.md # 踩坑与排障记录
|
||||
│ └─ handoff-template.md # 任务交接模板
|
||||
├─ plans/ # 阶段性计划与实施方案
|
||||
└─ skills/ # 未来可沉淀的仓库级 Hermes skills
|
||||
├─ skills/ # 仓库级 Hermes skills
|
||||
└─ plugins/ # 仓库级 Hermes plugins(需显式启用项目 plugin)
|
||||
```
|
||||
|
||||
## 仓库级 Plugins
|
||||
|
||||
本仓库可共享的 Hermes plugin 放在 `.hermes/plugins/<plugin-name>/`。当前已包含:
|
||||
|
||||
- `.hermes/plugins/game-studio/`:浏览器游戏设计、原型、2D/3D 技术栈、素材管线与 playtest 相关工作流。
|
||||
|
||||
Hermes 的项目级 plugin 默认不会自动加载。团队成员拉取仓库后,如需使用本仓库内 plugin,请在仓库根目录启动 Hermes 前设置:
|
||||
|
||||
```bash
|
||||
export HERMES_ENABLE_PROJECT_PLUGINS=1
|
||||
```
|
||||
|
||||
然后确认当前 Hermes 配置的 `plugins.enabled` 中包含 `game-studio`。如果成员本机尚未启用过该 plugin,当前 Hermes 的 `hermes plugins enable` 只识别用户级或内置 plugin,可能不会识别项目级 plugin;可用以下命令写入个人配置:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
from hermes_cli.config import load_config, save_config
|
||||
config = load_config()
|
||||
plugins = config.setdefault('plugins', {})
|
||||
enabled = set(plugins.get('enabled') or [])
|
||||
disabled = set(plugins.get('disabled') or [])
|
||||
enabled.add('game-studio')
|
||||
disabled.discard('game-studio')
|
||||
plugins['enabled'] = sorted(enabled)
|
||||
plugins['disabled'] = sorted(disabled)
|
||||
save_config(config)
|
||||
PY
|
||||
```
|
||||
|
||||
启用后重新进入一个新 Hermes 会话。`hermes plugins list` 当前主要展示内置和用户级 plugin,未必列出项目级 plugin;如需验证项目级扫描,可在仓库根目录运行:
|
||||
|
||||
```bash
|
||||
HERMES_ENABLE_PROJECT_PLUGINS=1 HERMES_PLUGINS_DEBUG=1 hermes chat -q "请读取 game-studio:game-studio skill 并概括它的用途"
|
||||
```
|
||||
|
||||
该 plugin 注册的是带命名空间的 plugin skills,可用类似 `game-studio:phaser-2d-game` 的名称显式加载。
|
||||
|
||||
## 推荐给 Hermes 的启动提示
|
||||
|
||||
在本仓库中开始复杂任务时,可以先对 Hermes 说:
|
||||
@@ -51,3 +89,4 @@
|
||||
- 大段临时聊天记录
|
||||
- 尚未确认的一次性猜测
|
||||
- 构建产物、日志、缓存、数据库 dump
|
||||
|
||||
|
||||
403
.hermes/plans/2026-05-11_205658-security-vulnerability-scan.md
Normal file
403
.hermes/plans/2026-05-11_205658-security-vulnerability-scan.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# 当前项目安全漏洞检查计划
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill only if the user later asks to execute this plan. 本计划当前仅用于规划,不实施代码修改。
|
||||
|
||||
**Goal:** 对 Genarrative 当前工作区做一次可复现的安全漏洞基线检查,覆盖依赖漏洞、密钥泄露、常见高风险代码模式、后端 Rust crate 风险和前端/Node 供应链风险,并输出可落地的整改清单。
|
||||
|
||||
**Architecture:** 采用“只读扫描 → 结果归档 → 人工分级 → 最小修复建议”的方式推进。先不直接升级依赖或改代码,避免安全扫描引入不可控 breaking change;执行阶段只在用户确认后运行扫描命令,并把报告保存到 `docs/audits/` 或 `.hermes/plans/` 附件中。
|
||||
|
||||
**Tech Stack:** Node/Vite/React/TypeScript、Rust workspace/Axum/SpacetimeDB、npm lockfile、Cargo.lock、Git worktree。
|
||||
|
||||
---
|
||||
|
||||
## 当前上下文 / 假设
|
||||
|
||||
- 当前有效工作区:`C:/proj/Genarrative/.worktrees/hermes-3337436a`。
|
||||
- 本次用户以 `/plan` 模式要求“检查一下当前项目的安全漏洞”,因此本轮只制定计划,不执行会产生报告、安装工具、修改依赖、提交或推送的操作。
|
||||
- 已确认项目包含:
|
||||
- 根 `package.json`,脚本包括 `npm run lint`、`npm run test`、`npm run build`、`npm run check:encoding`。
|
||||
- 根 `package-lock.json`。
|
||||
- `server-rs/Cargo.toml` 和 `server-rs/Cargo.lock`。
|
||||
- `apps/admin-web/package.json`、`packages/shared/package.json`。
|
||||
- `.hermes/shared-memory/development-workflow.md` 要求开发前读取共享记忆,并以当前代码、`docs/`、`AGENTS.md` 为准。
|
||||
- 安全扫描不应把真实密钥写入仓库;发现疑似密钥时只记录文件位置、变量名、脱敏片段和处置建议。
|
||||
|
||||
## 总体策略
|
||||
|
||||
1. 先做仓库状态和范围确认,避免扫描其他 worktree 或错误路径。
|
||||
2. 优先运行不会修改文件的安全检查:`npm audit --json`、`cargo audit`、密钥扫描、危险代码模式扫描。
|
||||
3. 分前端供应链、后端供应链、源码安全、配置/脚本安全四类归档。
|
||||
4. 对结果按严重级别分层:Critical / High / Medium / Low / Informational。
|
||||
5. 对每个真实问题给出:影响范围、证据、可行修复、验证命令、是否需要业务回归。
|
||||
6. 只有在用户确认进入执行/修复阶段后,才做依赖升级、代码修复、文档更新、测试和提交。
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step Plan
|
||||
|
||||
### Task 1: 确认扫描工作区和基线状态
|
||||
|
||||
**Objective:** 确保后续扫描针对当前 worktree,且不会误把既有未提交变更当成安全修复结果。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `AGENTS.md`
|
||||
- Read-only: `.hermes/README.md`
|
||||
- Read-only: `.hermes/shared-memory/development-workflow.md`
|
||||
- Read-only: `package.json`
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git status --short
|
||||
git branch --show-current
|
||||
git rev-parse --show-toplevel
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- `pwd` / `git rev-parse --show-toplevel` 指向 `C:/proj/Genarrative/.worktrees/hermes-3337436a` 对应路径。
|
||||
- 分支为当前隔离 worktree 分支。
|
||||
- 记录是否已有未提交变更;如存在,扫描报告需标注“基于含未提交变更的工作区”。
|
||||
|
||||
**Validation:**
|
||||
- 不修改任何项目文件。
|
||||
- 如发现路径不是当前 worktree,停止并重新确认路径。
|
||||
|
||||
### Task 2: 生成依赖清单和锁文件基线
|
||||
|
||||
**Objective:** 明确 Node 与 Rust 依赖入口,避免漏扫子包或 admin web。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `package.json`
|
||||
- Read-only: `package-lock.json`
|
||||
- Read-only: `apps/admin-web/package.json`
|
||||
- Read-only: `packages/shared/package.json`
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
- Read-only: `server-rs/Cargo.lock`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
npm --version
|
||||
node --version
|
||||
cargo --version
|
||||
rustc --version
|
||||
```
|
||||
|
||||
可选只读清单:
|
||||
|
||||
```bash
|
||||
npm ls --all --json > /tmp/genarrative-npm-ls.json
|
||||
cargo metadata --manifest-path server-rs/Cargo.toml --format-version 1 > /tmp/genarrative-cargo-metadata.json
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- 明确 npm / Node / Rust / Cargo 版本。
|
||||
- 若 `npm ls` 因 peer dependency 或历史依赖问题非 0,保留输出并继续 audit。
|
||||
|
||||
**Validation:**
|
||||
- `/tmp` 输出不进入 Git。
|
||||
- 不运行 `npm install`、`npm update`、`cargo update`。
|
||||
|
||||
### Task 3: Node 供应链漏洞扫描
|
||||
|
||||
**Objective:** 检查根 lockfile 覆盖的前端、脚本和 admin web 依赖漏洞。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `package-lock.json`
|
||||
- Read-only: `package.json`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
npm audit --json > /tmp/genarrative-npm-audit.json
|
||||
npm audit --audit-level=moderate
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- `npm audit --json` 生成机器可读结果。
|
||||
- 第二条命令给出人类可读摘要;如返回非 0,按漏洞严重度记录,不直接执行 `npm audit fix`。
|
||||
|
||||
**Result fields to extract:**
|
||||
- package name
|
||||
- vulnerable versions
|
||||
- installed version
|
||||
- severity
|
||||
- CVE / GHSA
|
||||
- via chain
|
||||
- fixAvailable 是否为 major/breaking
|
||||
- affected direct dependency or transitive dependency
|
||||
|
||||
**Validation:**
|
||||
- 不执行 `npm audit fix`。
|
||||
- 如 npm registry 网络不可用,记录阻塞原因和可重试命令。
|
||||
|
||||
### Task 4: Rust 供应链漏洞扫描
|
||||
|
||||
**Objective:** 检查 `server-rs` workspace 的 Cargo 依赖漏洞、弃用 crate 和 yanked crate。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
- Read-only: `server-rs/Cargo.lock`
|
||||
|
||||
**Commands:**
|
||||
|
||||
优先:
|
||||
|
||||
```bash
|
||||
cargo audit --json --manifest-path server-rs/Cargo.toml > /tmp/genarrative-cargo-audit.json
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
如果本机没有 `cargo audit`:
|
||||
|
||||
```bash
|
||||
cargo install cargo-audit --locked
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
**Execution note:**
|
||||
- 安装 `cargo-audit` 会改变用户 Cargo 工具目录,不属于纯只读扫描;执行前需用户确认。
|
||||
- 如果用户不希望安装工具,则记录“Rust 漏洞扫描未完成”,并给出本地安装或 CI 执行建议。
|
||||
|
||||
**Result fields to extract:**
|
||||
- advisory id
|
||||
- package
|
||||
- version
|
||||
- patched versions
|
||||
- unaffected versions
|
||||
- severity / CVSS if available
|
||||
- dependency path
|
||||
- whether it is runtime reachable in `api-server` / `spacetime-module`
|
||||
|
||||
**Validation:**
|
||||
- 不运行 `cargo update`。
|
||||
- 不改 `Cargo.lock`。
|
||||
|
||||
### Task 5: 密钥和敏感配置泄露扫描
|
||||
|
||||
**Objective:** 检查仓库中是否误提交 API key、token、私钥、cookie、`.env` 类文件或个人 Hermes 配置。
|
||||
|
||||
**Files / paths to scan:**
|
||||
- Full repo excluding `.git/`, `node_modules/`, `target/`, `dist/`, build artifacts。
|
||||
- 特别关注:`.hermes/`、`scripts/`、`server-rs/`、`apps/admin-web/`、`src/`、`docs/`。
|
||||
|
||||
**Preferred commands:**
|
||||
|
||||
如果有 gitleaks:
|
||||
|
||||
```bash
|
||||
gitleaks detect --source . --no-git --redact --report-format json --report-path /tmp/genarrative-gitleaks.json
|
||||
```
|
||||
|
||||
如果没有 gitleaks,先用只读 grep/ripgrep 兜底:
|
||||
|
||||
```bash
|
||||
git ls-files -z | xargs -0 grep -nIE "(api[_-]?key|secret|password|passwd|token|private[_-]?key|BEGIN (RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|AKIA[0-9A-Z]{16}|xox[baprs]-|sk-[A-Za-z0-9_-]{20,})" > /tmp/genarrative-secret-grep.txt || true
|
||||
```
|
||||
|
||||
**Execution note:**
|
||||
- 安装 gitleaks 需要用户确认。
|
||||
- grep 结果包含 false positive,必须人工分级,不得直接当作泄露结论。
|
||||
|
||||
**Validation:**
|
||||
- 报告中对值做脱敏,只保留前后 3-4 位或完全不记录值。
|
||||
- 如果发现 `.env.local` 或真实 token 被跟踪,立即标为 Critical。
|
||||
|
||||
### Task 6: 常见源码安全模式扫描
|
||||
|
||||
**Objective:** 快速发现高风险代码模式:命令注入、动态执行、路径穿越、危险反序列化、XSS、日志泄密、宽松 CORS 等。
|
||||
|
||||
**Files / paths:**
|
||||
- `src/**/*.{ts,tsx,js,mjs,cjs}`
|
||||
- `apps/admin-web/**/*.{ts,tsx,js,mjs,cjs}`
|
||||
- `scripts/**/*.{js,mjs,cjs,ts}`
|
||||
- `server-rs/crates/**/*.rs`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# JS/TS 动态执行与 HTML 注入
|
||||
rg -n "\beval\(|new Function\(|dangerouslySetInnerHTML|innerHTML\s*=|document\.write\(" src apps scripts packages
|
||||
|
||||
# Node 命令执行风险
|
||||
rg -n "exec\(|execSync\(|spawn\(|spawnSync\(|shell:\s*true|child_process" scripts src apps packages
|
||||
|
||||
# Rust 命令、文件路径、unwrap 风险热点
|
||||
rg -n "Command::new|std::process|\.unwrap\(|\.expect\(|fs::|File::open|PathBuf|set_header|cors|CorsLayer" server-rs/crates
|
||||
|
||||
# 宽松 CORS / Cookie / Auth 相关热点
|
||||
rg -n "allow_origin|Any|cookie|Authorization|Bearer|refresh|access_token|set_cookie|SameSite|Secure|HttpOnly" server-rs/crates src apps scripts
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- 输出作为“热点清单”,不等同于漏洞。
|
||||
- 对 auth/session、文件上传、OSS 签名、外部 LLM/图片服务请求、SpacetimeDB 访问 facade 做人工复核。
|
||||
|
||||
**Validation:**
|
||||
- 每个疑似问题必须能说明可利用条件,无法说明则降级为 Informational。
|
||||
|
||||
### Task 7: Web/API 安全配置人工复核
|
||||
|
||||
**Objective:** 对项目特有的安全边界做代码审阅,补足工具扫描无法覆盖的业务风险。
|
||||
|
||||
**Likely files to review:**
|
||||
- `server-rs/crates/api-server/src/**`
|
||||
- `server-rs/crates/platform-auth/src/**`
|
||||
- `server-rs/crates/platform-oss/src/**`
|
||||
- `server-rs/crates/platform-llm/src/**`
|
||||
- `server-rs/crates/spacetime-client/src/**`
|
||||
- `src/services/**`
|
||||
- `apps/admin-web/src/**`
|
||||
- `scripts/*deploy*`
|
||||
- `scripts/*api-server*`
|
||||
- `.github/workflows/**` if present
|
||||
|
||||
**Checklist:**
|
||||
- Auth / session:access token 与 refresh cookie 的生命周期、SameSite/Secure/HttpOnly、错误日志是否泄露 token。
|
||||
- CORS:开发环境与生产环境是否区分,是否存在生产 `Any`。
|
||||
- SSRF / outbound:LLM、图片生成、OSS、任意 URL 下载是否校验协议和大小。
|
||||
- Upload / Data URL:大小限制、MIME 校验、base64 解析错误处理。
|
||||
- Path traversal:脚本和后端是否拼接用户输入路径。
|
||||
- Admin:后台接口是否有权限校验,是否复用普通用户 token。
|
||||
- SpacetimeDB:private table / reducer 是否绕过 api-server facade 暴露敏感数据。
|
||||
- Logging:日志是否打印 API key、token、cookie、用户私密内容。
|
||||
|
||||
**Validation:**
|
||||
- 对每个命中的真实风险,记录具体文件路径和函数名。
|
||||
- 对“需要运行环境才能验证”的风险,列出 smoke 或单测建议。
|
||||
|
||||
### Task 8: 汇总漏洞分级与整改建议
|
||||
|
||||
**Objective:** 把扫描结果转成团队可执行的安全整改报告。
|
||||
|
||||
**Deliverable candidates:**
|
||||
- `docs/audits/SECURITY_VULNERABILITY_SCAN_YYYY-MM-DD.md`
|
||||
- 或如果用户只要临时报告:`.hermes/plans/assets/security-scan-YYYY-MM-DD.md`
|
||||
|
||||
**Report structure:**
|
||||
|
||||
```markdown
|
||||
# 安全漏洞扫描报告 YYYY-MM-DD
|
||||
|
||||
## 扫描范围
|
||||
## 扫描命令与环境
|
||||
## 摘要
|
||||
## Critical
|
||||
## High
|
||||
## Medium
|
||||
## Low
|
||||
## Informational / False Positive
|
||||
## 依赖升级建议
|
||||
## 代码修复建议
|
||||
## 需要人工确认的问题
|
||||
## 验证命令
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- 报告不包含真实密钥。
|
||||
- 每条问题都有“证据、影响、建议、验证”。
|
||||
- 明确哪些是工具扫描结果,哪些是人工判断。
|
||||
|
||||
### Task 9: 如用户要求修复,再分批执行最小修复
|
||||
|
||||
**Objective:** 避免一次性大规模升级导致回归,把修复拆为可验证的小批次。
|
||||
|
||||
**Suggested order:**
|
||||
1. Critical secrets:立即移除、轮换密钥、补 `.gitignore`/文档约束(注意项目约束:不要在 `.gitignore` 中添加 `.env.local`)。
|
||||
2. Critical/High direct dependencies:优先升级 direct dependency,运行最小测试。
|
||||
3. Critical/High transitive dependencies:评估是否由 direct dependency patch/minor 升级带出。
|
||||
4. 源码漏洞:按入口编写回归测试,再修复。
|
||||
5. Medium/Low:按风险和 breaking change 代价排期。
|
||||
|
||||
**Required verification after fixes:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
cd server-rs && cargo test --workspace
|
||||
```
|
||||
|
||||
后端 API 或 auth 修复涉及运行态时,还需要:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
# 另一个终端检查 /healthz 并执行对应 smoke
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- 修复后重新跑对应 audit / secret scan。
|
||||
- 走 `requesting-code-review` 的独立安全复核流程。
|
||||
|
||||
---
|
||||
|
||||
## Files likely to change(仅修复阶段)
|
||||
|
||||
本计划阶段不修改以下文件;只有用户确认执行修复时才可能变化:
|
||||
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `apps/admin-web/package.json`
|
||||
- `server-rs/Cargo.toml`
|
||||
- `server-rs/Cargo.lock`
|
||||
- `server-rs/crates/api-server/src/**`
|
||||
- `server-rs/crates/platform-auth/src/**`
|
||||
- `server-rs/crates/platform-oss/src/**`
|
||||
- `server-rs/crates/platform-llm/src/**`
|
||||
- `src/services/**`
|
||||
- `apps/admin-web/src/**`
|
||||
- `scripts/**`
|
||||
- `docs/audits/SECURITY_VULNERABILITY_SCAN_YYYY-MM-DD.md`
|
||||
- `.hermes/shared-memory/pitfalls.md`(仅当发现长期有效、会反复踩的安全排障经验时更新)
|
||||
|
||||
## Tests / Validation
|
||||
|
||||
安全扫描执行阶段:
|
||||
|
||||
```bash
|
||||
npm audit --json > /tmp/genarrative-npm-audit.json
|
||||
npm audit --audit-level=moderate
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
rg -n "\beval\(|new Function\(|dangerouslySetInnerHTML|innerHTML\s*=|document\.write\(" src apps scripts packages
|
||||
rg -n "exec\(|execSync\(|spawn\(|spawnSync\(|shell:\s*true|child_process" scripts src apps packages
|
||||
rg -n "Command::new|std::process|\.unwrap\(|\.expect\(|fs::|File::open|PathBuf|set_header|cors|CorsLayer" server-rs/crates
|
||||
```
|
||||
|
||||
修复执行阶段:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
cd server-rs && cargo test --workspace
|
||||
```
|
||||
|
||||
如变更后端运行态、安全中间件、auth/session:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
# 检查 /healthz
|
||||
# 执行相关 auth / API smoke
|
||||
```
|
||||
|
||||
## Risks, tradeoffs, and open questions
|
||||
|
||||
- `npm audit fix` 可能升级 major version,破坏 Vite/React/ESLint/Vitest 兼容性;必须先人工审查 `fixAvailable`。
|
||||
- `cargo audit` 可能需要安装 `cargo-audit`;安装工具属于用户环境变更,应先确认。
|
||||
- 密钥扫描极易产生 false positive;必须人工复核,报告中禁止输出真实密钥。
|
||||
- Rust `unwrap/expect` 不是天然漏洞;只有对外部输入、网络、文件、数据库响应等不可信数据造成 panic/DoS 时才升级为真实风险。
|
||||
- Web 安全检查需要区分开发环境和生产环境;开发 CORS 放宽不等于生产漏洞,但生产配置必须有明确边界。
|
||||
- 如果扫描发现历史提交中曾泄露密钥,删除当前文件不够,必须轮换密钥并考虑历史清理策略。
|
||||
- 当前计划未直接访问 CI/Jenkins/生产配置;若用户希望覆盖 CI/CD、镜像、部署主机和运行时端口,需要补充 Jenkins console、部署脚本和生产环境配置的只读访问方式。
|
||||
|
||||
## Missing artifacts / follow-up checkpoints
|
||||
|
||||
- 尚未获得用户确认是否允许安装 `cargo-audit` / `gitleaks` 等工具。
|
||||
- 尚未执行真实扫描,因此当前没有漏洞结论;执行后需要生成正式报告。
|
||||
- 如果用户希望“检查当前项目”包含远端仓库历史 secrets、Docker 镜像、Jenkins 凭据和生产运行时配置,需要另行确认访问范围和凭据边界。
|
||||
46
.hermes/plugins/game-studio/.codex-plugin/plugin.json
Normal file
46
.hermes/plugins/game-studio/.codex-plugin/plugin.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "game-studio",
|
||||
"version": "0.1.0",
|
||||
"description": "Design, prototype, and ship browser games with guided 2D and 3D workflows, asset pipelines, and playtesting support.",
|
||||
"author": {
|
||||
"name": "OpenAI",
|
||||
"email": "support@openai.com",
|
||||
"url": "https://openai.com/"
|
||||
},
|
||||
"homepage": "https://openai.com/",
|
||||
"repository": "https://github.com/openai/plugins",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"games",
|
||||
"phaser",
|
||||
"threejs",
|
||||
"react-three-fiber",
|
||||
"gltf",
|
||||
"rapier",
|
||||
"webgl",
|
||||
"sprites",
|
||||
"playtest"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "Game Studio",
|
||||
"shortDescription": "Design, prototype, and ship browser games",
|
||||
"longDescription": "Plan, prototype, and build browser games with guided workflows for gameplay systems, UI, asset pipelines, and playtesting across 2D and 3D projects.",
|
||||
"developerName": "OpenAI",
|
||||
"category": "Coding",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://openai.com/",
|
||||
"privacyPolicyURL": "https://openai.com/policies/privacy-policy/",
|
||||
"termsOfServiceURL": "https://openai.com/policies/terms-of-use/",
|
||||
"defaultPrompt": [
|
||||
"Design a browser game and plan the core loop"
|
||||
],
|
||||
"brandColor": "#0F766E",
|
||||
"composerIcon": "./assets/game-studio.svg",
|
||||
"logo": "./assets/app-icon.png",
|
||||
"screenshots": []
|
||||
}
|
||||
}
|
||||
38
.hermes/plugins/game-studio/__init__.py
Normal file
38
.hermes/plugins/game-studio/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Hermes wrapper for the OpenAI Codex Game Studio plugin.
|
||||
|
||||
This plugin was imported from a Codex curated plugin cache. It exposes the
|
||||
plugin's bundled SKILL.md files as Hermes plugin skills using qualified names
|
||||
like `game-studio:phaser-2d-game`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _read_description(skill_md: Path) -> str:
|
||||
try:
|
||||
text = skill_md.read_text(encoding="utf-8")[:4000]
|
||||
except Exception:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
frontmatter = text[3:end]
|
||||
for line in frontmatter.splitlines():
|
||||
if line.strip().startswith("description:"):
|
||||
return line.split(":", 1)[1].strip().strip("\"'")
|
||||
return ""
|
||||
|
||||
|
||||
def register(ctx) -> None:
|
||||
root = Path(__file__).resolve().parent
|
||||
skills_root = root / "skills"
|
||||
if not skills_root.exists():
|
||||
return
|
||||
for skill_md in sorted(skills_root.glob("*/SKILL.md")):
|
||||
ctx.register_skill(
|
||||
name=skill_md.parent.name,
|
||||
path=skill_md,
|
||||
description=_read_description(skill_md),
|
||||
)
|
||||
BIN
.hermes/plugins/game-studio/assets/app-icon.png
Normal file
BIN
.hermes/plugins/game-studio/assets/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
25
.hermes/plugins/game-studio/assets/game-studio.svg
Normal file
25
.hermes/plugins/game-studio/assets/game-studio.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Game Studio</title>
|
||||
<desc id="desc">A stylized browser game plugin icon with a viewport frame, a d-pad, and layered tiles.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#134E4A"/>
|
||||
<stop offset="100%" stop-color="#0F766E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screen" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CCFBF1"/>
|
||||
<stop offset="100%" stop-color="#5EEAD4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="52" fill="url(#bg)"/>
|
||||
<rect x="40" y="46" width="176" height="124" rx="20" fill="#062F2D" stroke="#7DD3C7" stroke-width="8"/>
|
||||
<rect x="58" y="62" width="140" height="92" rx="12" fill="url(#screen)"/>
|
||||
<path d="M76 132h28l14-18 18 20 24-30 18 18v18H76z" fill="#0F766E" opacity="0.9"/>
|
||||
<rect x="62" y="178" width="50" height="50" rx="18" fill="#0B2F2D" stroke="#7DD3C7" stroke-width="6"/>
|
||||
<rect x="144" y="178" width="50" height="50" rx="18" fill="#0B2F2D" stroke="#7DD3C7" stroke-width="6"/>
|
||||
<rect x="81" y="189" width="12" height="28" rx="4" fill="#CCFBF1"/>
|
||||
<rect x="73" y="197" width="28" height="12" rx="4" fill="#CCFBF1"/>
|
||||
<circle cx="160" cy="203" r="8" fill="#FDE68A"/>
|
||||
<circle cx="178" cy="203" r="8" fill="#FCA5A5"/>
|
||||
<circle cx="169" cy="192" r="8" fill="#BFDBFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
6
.hermes/plugins/game-studio/plugin.yaml
Normal file
6
.hermes/plugins/game-studio/plugin.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: game-studio
|
||||
version: 0.1.0
|
||||
description: Design, prototype, and ship browser games with guided 2D and 3D workflows,
|
||||
asset pipelines, and playtesting support.
|
||||
author: OpenAI
|
||||
kind: standalone
|
||||
@@ -0,0 +1,50 @@
|
||||
# Alternative 3D Engines
|
||||
|
||||
This plugin defaults to Three.js and React Three Fiber for code generation. Babylon.js and PlayCanvas still matter, but they are reference-only alternatives in the current plugin shape.
|
||||
|
||||
## Babylon.js
|
||||
|
||||
Useful sources:
|
||||
|
||||
- [Babylon.js home](https://babylonjs.com/)
|
||||
- [Engine specifications](https://www.babylonjs.com/specifications/)
|
||||
- [Babylon.js Editor](https://editor.babylonjs.com/)
|
||||
|
||||
Choose Babylon.js when:
|
||||
|
||||
- the user explicitly wants Babylon.js
|
||||
- the team wants a more engine-heavy stack with scene, material, viewer, and editor tooling built around one ecosystem
|
||||
- WebGPU, Havok, node-based rendering or material tooling, or Babylon-specific runtime features are part of the reason for the choice
|
||||
|
||||
What Babylon.js is good at:
|
||||
|
||||
- full-engine 3D workflows
|
||||
- strong built-in tooling and editor surfaces
|
||||
- WebGL and WebGPU support inside one ecosystem
|
||||
- integrated viewer and inspection-oriented workflows
|
||||
|
||||
## PlayCanvas
|
||||
|
||||
Useful sources:
|
||||
|
||||
- [PlayCanvas graphics overview](https://developer.playcanvas.com/user-manual/graphics/)
|
||||
- [Supported formats](https://developer.playcanvas.com/user-manual/assets/supported-formats/)
|
||||
- [PlayCanvas React GLTF API](https://developer.playcanvas.com/user-manual/react/api/gltf/)
|
||||
- [PlayCanvas Web Components](https://developer.playcanvas.com/user-manual/web-components/)
|
||||
|
||||
Choose PlayCanvas when:
|
||||
|
||||
- the user explicitly wants PlayCanvas
|
||||
- the team prefers an editor-centric browser engine workflow
|
||||
- GLB import, runtime tooling, React bindings, or web-component-based embedding are central to the project
|
||||
|
||||
What PlayCanvas is good at:
|
||||
|
||||
- editor and engine working together
|
||||
- GLB-centric browser asset workflows
|
||||
- strong web embedding patterns
|
||||
- WebGL and WebGPU support with browser-focused runtime tooling
|
||||
|
||||
## Default recommendation
|
||||
|
||||
If the user has not already chosen Babylon.js or PlayCanvas, prefer Three.js or React Three Fiber in this plugin because they give the best balance of portability, ecosystem depth, and predictable code generation across normal browser-game repos.
|
||||
53
.hermes/plugins/game-studio/references/engine-selection.md
Normal file
53
.hermes/plugins/game-studio/references/engine-selection.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Engine Selection
|
||||
|
||||
Use this table to choose the default implementation path for browser games in this plugin.
|
||||
|
||||
## Default choices
|
||||
|
||||
- Choose Phaser for 2D games unless the user explicitly asks for another stack.
|
||||
- Choose vanilla Three.js for explicit 3D or WebGL-first experiences in plain TypeScript or Vite apps.
|
||||
- Choose React Three Fiber when the 3D scene is part of a React application and shared app state or declarative composition matters.
|
||||
- Choose raw WebGL only when engine abstractions are the problem, not because WebGL sounds more advanced.
|
||||
- Treat Babylon.js and PlayCanvas as alternative ecosystems, not the default path in this plugin.
|
||||
|
||||
## Phaser is the best fit when
|
||||
|
||||
- the game is sprite- or tile-based
|
||||
- the game is top-down, side-view, or grid tactics
|
||||
- you need camera, sprite, and scene primitives quickly
|
||||
- the UI will mostly live in DOM overlays
|
||||
- the game loop is gameplay-first rather than renderer-first
|
||||
|
||||
## Three.js is the best fit when
|
||||
|
||||
- the game is genuinely 3D
|
||||
- camera movement and depth are central to play
|
||||
- materials, lighting, or scene composition matter more than sprite tooling
|
||||
- the user explicitly asks for Three.js or WebGL-based 3D work
|
||||
- the team wants direct control over scene setup, loaders, physics integration, and the game loop
|
||||
|
||||
## React Three Fiber is the best fit when
|
||||
|
||||
- the project already lives in React
|
||||
- the 3D scene needs to share app state with the rest of the product
|
||||
- declarative scene composition is more valuable than a fully imperative loop
|
||||
- the team benefits from pmndrs tooling such as Drei, React Postprocessing, or `@react-three/rapier`
|
||||
|
||||
## Babylon.js or PlayCanvas are the best fit when
|
||||
|
||||
- the user explicitly asks for those engines
|
||||
- the team already has engine-specific tooling or editor workflows in those ecosystems
|
||||
- the project wants engine-heavy runtime features, editor-first workflows, or platform-specific tooling that Three.js does not provide by default
|
||||
|
||||
## Raw WebGL is the best fit when
|
||||
|
||||
- the project is shader-heavy
|
||||
- you need a custom renderer or post-processing pipeline
|
||||
- the user explicitly wants low-level rendering control
|
||||
|
||||
## Avoid these mismatches
|
||||
|
||||
- Do not choose a 3D stack for a normal 2D tactics or platformer game.
|
||||
- Do not choose raw WebGL for a game that mostly needs engine conveniences.
|
||||
- Do not force HUD and menus into canvas or WebGL when DOM would be clearer.
|
||||
- Do not default to Babylon.js or PlayCanvas when the user mainly wants portable TypeScript code generation across browser-game repos.
|
||||
97
.hermes/plugins/game-studio/references/frontend-prompts.md
Normal file
97
.hermes/plugins/game-studio/references/frontend-prompts.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Frontend Prompts
|
||||
|
||||
Use these prompt shapes to keep browser-game UI intentional instead of generic.
|
||||
|
||||
## Prompt ingredients
|
||||
|
||||
- game genre and fantasy
|
||||
- camera or viewpoint
|
||||
- player verbs
|
||||
- HUD zones
|
||||
- menu surfaces
|
||||
- motion tone
|
||||
- desktop and mobile expectations
|
||||
- playfield protection and disclosure strategy
|
||||
- anti-patterns to avoid
|
||||
|
||||
## HUD implementation prompt
|
||||
|
||||
```text
|
||||
Design and implement the HUD for a browser game.
|
||||
|
||||
Game fantasy: <genre and world>
|
||||
Viewpoint: <top-down, side-view, tactical grid, third-person, first-person>
|
||||
Primary verbs: <attack, move, cast, build, dodge, inspect>
|
||||
HUD zones: <top status, bottom command bar, side objectives, modal panels>
|
||||
Tone: <ornate, rugged, clean sci-fi, painterly, arcade>
|
||||
Motion: <restrained, snappy, dramatic only on important state changes>
|
||||
Platforms: desktop and mobile
|
||||
Constraints: readable over active gameplay, DOM-based overlays, CSS variables, no generic dashboard look
|
||||
Playfield protection: keep the central play area clear during normal play, prefer one primary persistent HUD cluster, and move long-form notes or controls behind menus
|
||||
Avoid: flat admin UI, default font stack, cluttered overlays, constant micro-animation, equal-weight cards around every edge, broad always-on panels that cover the world
|
||||
```
|
||||
|
||||
## Menu implementation prompt
|
||||
|
||||
```text
|
||||
Build the shell UI for a browser game with the following surfaces:
|
||||
- title screen
|
||||
- pause menu
|
||||
- settings panel
|
||||
- game-over or victory screen
|
||||
|
||||
Keep the menus visually tied to the game world, not to a SaaS app aesthetic. Use strong hierarchy, intentional typography, meaningful motion, and responsive layout.
|
||||
```
|
||||
|
||||
## Low-chrome 3D starter prompt
|
||||
|
||||
```text
|
||||
Design the initial playable HUD for a browser 3D game.
|
||||
|
||||
Goal: the first screen should feel playable in under 3 seconds, not like a dashboard.
|
||||
Camera mode: <third-person, first-person, orbit, rail>
|
||||
Primary verbs: <move, inspect, interact, attack, build>
|
||||
Persistent UI budget:
|
||||
- one compact objective chip or status cluster
|
||||
- one optional small secondary surface
|
||||
- one transient controls or interaction hint
|
||||
|
||||
Interaction rules:
|
||||
- keep the center of the playfield clear
|
||||
- keep the lower-middle playfield mostly clear during normal play
|
||||
- lore, notes, quest details, and long control lists live behind a drawer, pause menu, or toggle
|
||||
- modal and pause states must gate camera input correctly
|
||||
|
||||
Avoid:
|
||||
- giant title cards over live gameplay
|
||||
- field notes, controls, and objectives all open at once
|
||||
- equally weighted glass panels in every corner
|
||||
- full-screen overlay chrome during normal movement
|
||||
```
|
||||
|
||||
## 3D overlay prompt
|
||||
|
||||
```text
|
||||
Design and implement the HUD and menu overlays for a browser 3D game.
|
||||
|
||||
Engine context: <vanilla Three.js or React Three Fiber>
|
||||
Camera mode: <third-person, first-person, orbit, rail>
|
||||
Primary verbs: <move, inspect, interact, attack, build>
|
||||
Overlay surfaces: <reticle, quest log, inventory, pause menu, settings>
|
||||
Interaction constraints:
|
||||
- DOM overlays, not in-scene UI by default
|
||||
- modal and menu states must suspend or gate camera input correctly
|
||||
- keyboard and pointer states must be explicit
|
||||
- reduced-motion support for non-essential transitions
|
||||
- keep the center of the screen clear during normal play
|
||||
- keep the lower-middle playfield mostly clear during normal play
|
||||
- start with one compact objective surface and transient hints rather than multiple permanent cards
|
||||
- secondary content such as notes, lore, and full control references should be collapsed by default
|
||||
Avoid:
|
||||
- dashboard UI
|
||||
- cluttered full-screen overlays
|
||||
- boxed panels around every edge of the viewport
|
||||
- full-width top-and-bottom panel stacks
|
||||
- permanent text-heavy cards competing with the scene
|
||||
- camera movement continuing under active menus
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# GLB Loading Starter
|
||||
|
||||
Use this as the canonical minimal pattern for loading shipped 3D content.
|
||||
|
||||
## Vanilla Three.js
|
||||
|
||||
```ts
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
||||
|
||||
const draco = new DRACOLoader();
|
||||
draco.setDecoderPath("/draco/");
|
||||
|
||||
const gltfLoader = new GLTFLoader();
|
||||
gltfLoader.setDRACOLoader(draco);
|
||||
|
||||
gltfLoader.load("/assets/hero.glb", (gltf) => {
|
||||
const root = gltf.scene;
|
||||
root.traverse((node) => {
|
||||
if ("castShadow" in node) {
|
||||
node.castShadow = true;
|
||||
node.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
scene.add(root);
|
||||
});
|
||||
```
|
||||
|
||||
## React Three Fiber
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
|
||||
function HeroModel() {
|
||||
const gltf = useGLTF("/assets/hero.glb");
|
||||
return <primitive object={gltf.scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Default shipping format is GLB or glTF 2.0.
|
||||
- Keep optimization upstream in the asset pipeline; loader code should stay boring.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Phaser Architecture
|
||||
|
||||
This is the default 2D structure for the plugin.
|
||||
|
||||
## Recommended module split
|
||||
|
||||
```text
|
||||
src/
|
||||
game/
|
||||
simulation/
|
||||
state.ts
|
||||
systems/
|
||||
rules/
|
||||
content/
|
||||
encounters/
|
||||
items/
|
||||
maps/
|
||||
input/
|
||||
actions.ts
|
||||
bindings.ts
|
||||
assets/
|
||||
manifest.ts
|
||||
phaser/
|
||||
boot/
|
||||
scenes/
|
||||
BootScene.ts
|
||||
MenuScene.ts
|
||||
BattleScene.ts
|
||||
view/
|
||||
sprites/
|
||||
fx/
|
||||
camera/
|
||||
adapters/
|
||||
sceneBridge.ts
|
||||
ui/
|
||||
hud/
|
||||
menus/
|
||||
overlays/
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- `simulation/`: source of truth for rules and saveable state
|
||||
- `content/`: authored data and encounter configuration
|
||||
- `input/`: action map and physical control bindings
|
||||
- `assets/`: stable manifest keys and asset metadata
|
||||
- `phaser/scenes/`: scene orchestration, not game rules
|
||||
- `phaser/view/`: render and effect helpers
|
||||
- `ui/`: DOM HUD, menus, and narrative panels
|
||||
|
||||
## Rules
|
||||
|
||||
- Phaser scenes read from and write to the simulation through a defined bridge.
|
||||
- Game state changes should not depend on sprite or tween lifetime.
|
||||
- Camera behavior should be isolated from combat or movement rules.
|
||||
- Use DOM for dense text and settings surfaces.
|
||||
51
.hermes/plugins/game-studio/references/playtest-checklist.md
Normal file
51
.hermes/plugins/game-studio/references/playtest-checklist.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Playtest Checklist
|
||||
|
||||
Use this checklist for browser-game QA.
|
||||
|
||||
## Universal checks
|
||||
|
||||
- Does the game boot into a useful first state?
|
||||
- Are the main verbs obvious and responsive?
|
||||
- Does the HUD remain readable over gameplay?
|
||||
- Does the first playable screen prioritize play over dashboard chrome?
|
||||
- Does the central playfield stay mostly clear during normal play?
|
||||
- Do pause, failure, and recovery states work?
|
||||
- Does the game survive viewport changes?
|
||||
|
||||
## 2D checks
|
||||
|
||||
- sprite baseline consistency
|
||||
- hit, hurt, and attack timing
|
||||
- command menu focus and input state
|
||||
- tile or platform readability
|
||||
- particle or camera effects obscuring gameplay
|
||||
|
||||
## 3D checks
|
||||
|
||||
- camera control stability
|
||||
- camera and menu-state handoff
|
||||
- depth readability
|
||||
- persistent overlay weight versus scene readability
|
||||
- secondary notes, controls, and quest details collapsed by default
|
||||
- resize and aspect-ratio handling
|
||||
- renderer fallback or context-loss handling
|
||||
- material and lighting stability across states
|
||||
- GLB asset and texture streaming behavior
|
||||
- collision proxy alignment
|
||||
- GPU bottlenecks isolated with capture tools when needed
|
||||
|
||||
## Browser checks
|
||||
|
||||
- desktop and mobile viewports
|
||||
- input modality differences
|
||||
- reduced-motion behavior
|
||||
- pause behavior when focus changes
|
||||
- pointer-lock and camera-input release when overlays open
|
||||
- transient onboarding hints dismiss or fade once the player is moving
|
||||
|
||||
## Reporting
|
||||
|
||||
- Capture screenshots for visual findings.
|
||||
- Put findings in severity order.
|
||||
- Include reproduction steps.
|
||||
- Call out whether the likely owner is simulation, renderer, frontend, or asset pipeline.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Rapier Integration Starter
|
||||
|
||||
Use this as the smallest canonical pattern for adding physics without letting it take over the whole runtime.
|
||||
|
||||
## Vanilla Three.js
|
||||
|
||||
```ts
|
||||
import RAPIER from "@dimforge/rapier3d-compat";
|
||||
|
||||
await RAPIER.init();
|
||||
|
||||
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
|
||||
const body = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 2, 0));
|
||||
world.createCollider(RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5), body);
|
||||
|
||||
renderer.setAnimationLoop(() => {
|
||||
world.step();
|
||||
const p = body.translation();
|
||||
mesh.position.set(p.x, p.y, p.z);
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
```
|
||||
|
||||
## React Three Fiber
|
||||
|
||||
```tsx
|
||||
import { Physics, RigidBody } from "@react-three/rapier";
|
||||
|
||||
<Physics gravity={[0, -9.81, 0]}>
|
||||
<RigidBody colliders="cuboid">
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#3dd9b8" />
|
||||
</mesh>
|
||||
</RigidBody>
|
||||
</Physics>;
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep physics state synchronized through an explicit bridge.
|
||||
- Do not bury gameplay rules inside render or physics callbacks.
|
||||
@@ -0,0 +1,42 @@
|
||||
# React Three Fiber Stack
|
||||
|
||||
This is the default React-native 3D stack for the plugin.
|
||||
|
||||
## Primary components
|
||||
|
||||
- [React Three Fiber](https://r3f.docs.pmnd.rs/getting-started/introduction) for declarative Three.js rendering in React.
|
||||
- [Drei](https://drei.docs.pmnd.rs/controls/introduction) for controls, loaders, helpers, environments, and common scene utilities.
|
||||
- [React Postprocessing](https://react-postprocessing.docs.pmnd.rs/introduction) for effect composition in React-hosted scenes.
|
||||
- [React Three Rapier](https://pmndrs.github.io/react-three-rapier/) for physics integration.
|
||||
- [React Three A11y](https://a11y.docs.pmnd.rs/introduction) when scene interaction benefits from accessibility-aware patterns.
|
||||
- [glTF Transform](https://gltf-transform.dev/) and the [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) for shipped assets.
|
||||
|
||||
## Default stack choices
|
||||
|
||||
- Runtime: `@react-three/fiber` + `three`
|
||||
- Helper ecosystem: `@react-three/drei`
|
||||
- Physics: `@react-three/rapier`
|
||||
- Effects: `@react-three/postprocessing`
|
||||
- Accessibility: `@react-three/a11y` when appropriate
|
||||
- Assets: GLB or glTF 2.0
|
||||
|
||||
## Choose this stack when
|
||||
|
||||
- the 3D scene lives inside a React app
|
||||
- the UI shell, settings, or product flow already uses React
|
||||
- the team benefits from declarative scene composition
|
||||
- the scene must share app state with non-canvas UI
|
||||
|
||||
## Avoid this stack when
|
||||
|
||||
- the project wants a cleaner imperative loop with minimal React coordination
|
||||
- the whole game runtime would be easier to reason about in plain TypeScript
|
||||
|
||||
## Companion references
|
||||
|
||||
- `threejs-stack.md`
|
||||
- `react-three-fiber-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
- `web-3d-asset-pipeline.md`
|
||||
- `webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,51 @@
|
||||
# React Three Fiber Starter
|
||||
|
||||
Use this as the smallest canonical starting point for a React-hosted 3D scene.
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
src/
|
||||
App.tsx
|
||||
```
|
||||
|
||||
## `src/App.tsx`
|
||||
|
||||
```tsx
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<mesh rotation={[0.4, 0.6, 0]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#3dd9b8" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Canvas camera={{ position: [0, 1.5, 4], fov: 60 }}>
|
||||
<color attach="background" args={["#101418"]} />
|
||||
<ambientLight intensity={0.7} />
|
||||
<directionalLight position={[4, 6, 3]} intensity={1.2} />
|
||||
<Spinner />
|
||||
</Canvas>
|
||||
<div className="hud">
|
||||
<div className="objective-chip">Reach the lantern bridge.</div>
|
||||
<div className="hint-pill">WASD to move. Hold mouse to look.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Start here when the 3D scene lives inside an existing React app.
|
||||
- Keep the initial HUD sparse. One compact objective surface and one transient hint is usually enough for a first playable scaffold.
|
||||
- Put lore, notes, map, and settings behind drawers or modals instead of opening them all by default.
|
||||
- Add GLB loading with `gltf-loading-starter.md`.
|
||||
- Add physics with `rapier-integration-starter.md`.
|
||||
- Use `three-hud-layout-patterns.md` for low-chrome 3D overlay defaults.
|
||||
57
.hermes/plugins/game-studio/references/sprite-pipeline.md
Normal file
57
.hermes/plugins/game-studio/references/sprite-pipeline.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Sprite Pipeline
|
||||
|
||||
This is the default 2D animation workflow for the plugin.
|
||||
|
||||
## Principles
|
||||
|
||||
- Start from one approved in-game frame.
|
||||
- Generate the animation as one strip, not isolated frames.
|
||||
- Normalize the whole strip with one shared scale.
|
||||
- Use one shared anchor, typically bottom-center.
|
||||
- Preview before approving the asset.
|
||||
|
||||
## Why this works
|
||||
|
||||
- The approved seed frame preserves identity.
|
||||
- Strip-first generation reduces frame-to-frame drift.
|
||||
- Shared-scale normalization prevents one tall pose from making the character feel smaller.
|
||||
- Locking frame 01 back to the shipped sprite preserves continuity for idle-to-action transitions.
|
||||
|
||||
## Prompt template
|
||||
|
||||
```text
|
||||
Intended use: candidate production spritesheet for a 2D browser game animation review.
|
||||
Edit the provided transparent reference canvas into a single horizontal <N>-frame spritesheet.
|
||||
|
||||
The existing sprite in the leftmost slot is the anchor frame and must remain the same character:
|
||||
- same facing direction
|
||||
- same silhouette family
|
||||
- same palette family
|
||||
- same proportions
|
||||
- same readable face or key features
|
||||
- same outfit details
|
||||
|
||||
Composition:
|
||||
- transparent canvas
|
||||
- exactly one row of <N> equal frame slots
|
||||
- no extra characters
|
||||
- no labels
|
||||
- no scenery
|
||||
- no poster layout
|
||||
|
||||
Action:
|
||||
- describe the specific animation beat from frame 1 through frame N
|
||||
|
||||
Style:
|
||||
- authentic pixel-art production asset
|
||||
- crisp pixel clusters
|
||||
- restrained palette
|
||||
- not concept art
|
||||
```
|
||||
|
||||
## Normalization notes
|
||||
|
||||
- Use the union of detected sprite bounds per slot.
|
||||
- Compute one scale from the largest detected frame and anchor.
|
||||
- Bottom-align frames into the target canvas.
|
||||
- Reuse the exact shipped frame for frame 01 when `--lock-frame1` is appropriate.
|
||||
@@ -0,0 +1,61 @@
|
||||
# 3D HUD Layout Patterns
|
||||
|
||||
Use these defaults for initial 3D browser-game scaffolds. The first screen should be playable before it is informational.
|
||||
|
||||
## Layout Budget
|
||||
|
||||
- Keep the center of the screen clear during normal play.
|
||||
- On desktop, prefer one primary persistent cluster and one small secondary cluster.
|
||||
- On mobile, prefer one compact persistent cluster and transient prompts.
|
||||
- Secondary information belongs in drawers, toggles, pause menus, or contextual popovers.
|
||||
|
||||
## Good Default Patterns
|
||||
|
||||
### Objective chip
|
||||
|
||||
- One short objective in a compact top-corner chip.
|
||||
- One optional sublabel for location or mode.
|
||||
- No giant hero banner over the live scene.
|
||||
|
||||
### Contextual interaction prompt
|
||||
|
||||
- Bottom-center or lower-corner pill.
|
||||
- Appears only near interactables or during onboarding.
|
||||
- Dismisses after first use or fades once the player is moving confidently.
|
||||
|
||||
### Small status strip
|
||||
|
||||
- Health, energy, party count, or beacon progress in a narrow edge-aligned strip.
|
||||
- Use icons, short labels, and compact meters instead of stacked cards.
|
||||
|
||||
### Collapsible journal or quest log
|
||||
|
||||
- Closed by default.
|
||||
- Opened by a hotkey, button, or pause state.
|
||||
- Holds longer prose, lore, map notes, and multi-step objective details.
|
||||
|
||||
### Pause and settings modal
|
||||
|
||||
- Explicit modal state.
|
||||
- Suspends pointer-lock, drag-look, or camera input while active.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- four to six glass cards permanently framing the viewport
|
||||
- large lore or field-notes panels open during normal movement
|
||||
- controls lists permanently pinned to the screen
|
||||
- symmetric dashboard composition that competes with the scene
|
||||
- oversized title panels staying visible after the first second of play
|
||||
|
||||
## Example UI Budget
|
||||
|
||||
- top-left: objective chip
|
||||
- top-right: compact status strip
|
||||
- bottom-center: transient interaction or controls hint
|
||||
- pause menu or drawer: map, notes, inventory, settings
|
||||
|
||||
## Prompt Add-On
|
||||
|
||||
```text
|
||||
Default to a low-chrome playable HUD. Keep the central playfield clear. Use one compact objective chip, one small status surface, and transient prompts. Put lore, field notes, full controls, and long checklists behind a drawer or pause menu. Avoid equal-weight boxed panels in every corner.
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Three WebGL Architecture
|
||||
|
||||
This is the default 3D structure for the plugin.
|
||||
|
||||
## Recommended module split
|
||||
|
||||
```text
|
||||
src/
|
||||
game/
|
||||
simulation/
|
||||
content/
|
||||
input/
|
||||
save/
|
||||
render/
|
||||
app/
|
||||
createRenderer.ts
|
||||
createScene.ts
|
||||
createCamera.ts
|
||||
createLoop.ts
|
||||
loaders/
|
||||
loadGltf.ts
|
||||
loadEnvironment.ts
|
||||
loadTextures.ts
|
||||
objects/
|
||||
materials/
|
||||
lights/
|
||||
post/
|
||||
adapters/
|
||||
renderBridge.ts
|
||||
physics/
|
||||
world.ts
|
||||
colliders.ts
|
||||
sync.ts
|
||||
diagnostics/
|
||||
debugFlags.ts
|
||||
perf.ts
|
||||
ui/
|
||||
hud/
|
||||
menus/
|
||||
overlays/
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- `simulation/`: rules, state, AI, progression, save data
|
||||
- `render/app/`: renderer, scene, camera, resize, context lifecycle
|
||||
- `render/loaders/`: GLTF, compression, texture, and environment loading
|
||||
- `render/objects/`: scene graph construction and disposal
|
||||
- `render/materials/`: material setup and shader boundaries
|
||||
- `physics/`: Rapier world and simulation bridge
|
||||
- `diagnostics/`: performance probes and GPU debugging hooks
|
||||
- `ui/`: DOM HUD and menus
|
||||
|
||||
## Rules
|
||||
|
||||
- Scene graph objects are not the source of truth for game rules.
|
||||
- Keep camera logic explicit and testable.
|
||||
- Handle resize and context-loss as real browser concerns.
|
||||
- Keep high-density UI in DOM even when the world is fully 3D.
|
||||
- Treat GLB or glTF 2.0 as the default content format.
|
||||
- Add physics and diagnostics as real subsystems, not temporary one-off utilities.
|
||||
41
.hermes/plugins/game-studio/references/threejs-stack.md
Normal file
41
.hermes/plugins/game-studio/references/threejs-stack.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Three.js Stack
|
||||
|
||||
This is the default non-React 3D runtime stack for the plugin.
|
||||
|
||||
## Primary components
|
||||
|
||||
- [Three.js documentation](https://threejs.org/docs/) for the core renderer, scene graph, materials, cameras, loaders, and examples.
|
||||
- [Rapier JavaScript guide](https://rapier.rs/docs/user_guides/javascript/getting_started_js/) for physics integration.
|
||||
- [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) for the default shipping asset format.
|
||||
- [glTF Transform](https://gltf-transform.dev/) for optimization, packaging, and compression workflows.
|
||||
- [SpectorJS](https://spector.babylonjs.com/) for WebGL frame capture and GPU debugging.
|
||||
|
||||
## Default stack choices
|
||||
|
||||
- Runtime: `three`
|
||||
- Tooling: TypeScript + Vite
|
||||
- Assets: GLB or glTF 2.0
|
||||
- Loaders: `GLTFLoader`, `DRACOLoader`, `KTX2Loader` when the asset pipeline requires them
|
||||
- Physics: Rapier JS
|
||||
- UI: DOM overlays, not in-scene UI by default
|
||||
|
||||
## Choose this stack when
|
||||
|
||||
- the project is not React-first
|
||||
- the team wants direct control over the render loop
|
||||
- scene composition, loader setup, or custom render behavior needs imperative structure
|
||||
- the game code should feel engine-like without a React abstraction layer
|
||||
|
||||
## Avoid this stack when
|
||||
|
||||
- the surrounding app is already React-heavy and wants shared declarative state
|
||||
- the project needs an editor-first engine workflow more than a portable TypeScript runtime
|
||||
|
||||
## Companion references
|
||||
|
||||
- `three-webgl-architecture.md`
|
||||
- `threejs-vanilla-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
- `web-3d-asset-pipeline.md`
|
||||
- `webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,58 @@
|
||||
# Three.js Vanilla Starter
|
||||
|
||||
Use this as the smallest canonical starting point for a plain TypeScript or Vite Three.js app.
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
src/
|
||||
main.ts
|
||||
```
|
||||
|
||||
## `src/main.ts`
|
||||
|
||||
```ts
|
||||
import * as THREE from "three";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color("#101418");
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
|
||||
camera.position.set(0, 1.5, 4);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
|
||||
const light = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
light.position.set(4, 6, 3);
|
||||
scene.add(light);
|
||||
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshStandardMaterial({ color: "#3dd9b8" }),
|
||||
);
|
||||
scene.add(mesh);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
renderer.setAnimationLoop(() => {
|
||||
mesh.rotation.y += 0.01;
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Start here for direct loop and renderer control.
|
||||
- If the scaffold needs UI, start with one compact objective chip and one transient controls hint rather than multiple permanent cards.
|
||||
- Keep notes, codex, maps, and settings behind on-demand surfaces. The starter scene should stay readable while moving the camera.
|
||||
- Add GLB loading with `gltf-loading-starter.md`.
|
||||
- Add physics sync with `rapier-integration-starter.md`.
|
||||
- Use `three-hud-layout-patterns.md` for low-chrome 3D overlay defaults.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Web 3D Asset Pipeline
|
||||
|
||||
This is the default 3D asset shipping guidance for the plugin.
|
||||
|
||||
## Primary sources
|
||||
|
||||
- [Blender glTF exporter manual](https://docs.blender.org/manual/en/latest/addons/import_export/scene_gltf2.html)
|
||||
- [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html)
|
||||
- [glTF Transform](https://gltf-transform.dev/)
|
||||
- [PlayCanvas supported formats](https://developer.playcanvas.com/user-manual/assets/supported-formats/) for a good reference on why GLB is the recommended runtime format in browser engines
|
||||
|
||||
## Default output
|
||||
|
||||
- Ship GLB when possible.
|
||||
- Use `.gltf` with external files only when the asset pipeline or delivery strategy genuinely needs that shape.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Clean the source asset in the DCC tool.
|
||||
2. Export to GLB or glTF 2.0.
|
||||
3. Run glTF Transform for validation, pruning, deduplication, and size reduction.
|
||||
4. Apply the chosen geometry and texture compression strategy.
|
||||
5. Verify pivots, scale, collision assumptions, and hierarchy naming.
|
||||
6. Test the asset in the runtime before treating it as final.
|
||||
|
||||
## Compression and optimization
|
||||
|
||||
- Use Draco or Meshopt deliberately, not both by default.
|
||||
- Use KTX2 or BasisU when the runtime stack supports GPU-friendly compressed textures.
|
||||
- Keep texture resolution aligned with actual on-screen use.
|
||||
- Reuse materials and avoid unnecessary texture uniqueness.
|
||||
|
||||
## Runtime checks
|
||||
|
||||
- scale is consistent across assets
|
||||
- pivots match gameplay expectations
|
||||
- node names are stable
|
||||
- collision proxy needs are handled
|
||||
- animation clips and variants load correctly
|
||||
- memory and load time are reasonable for the scene
|
||||
|
||||
## Starter patterns
|
||||
|
||||
- `threejs-vanilla-starter.md`
|
||||
- `react-three-fiber-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
@@ -0,0 +1,36 @@
|
||||
# WebGL Debugging and Performance
|
||||
|
||||
Use this reference when a browser 3D scene is visually wrong, unstable, or slower than expected.
|
||||
|
||||
## Primary tools
|
||||
|
||||
- [SpectorJS](https://spector.babylonjs.com/) for frame capture, pipeline inspection, draw-call review, and shader debugging.
|
||||
- Browser performance tooling for main-thread work, asset decode stalls, and memory pressure.
|
||||
- Engine-native debug views and stats surfaces where available.
|
||||
|
||||
## What to inspect first
|
||||
|
||||
- draw-call count
|
||||
- shader compilation churn
|
||||
- texture memory pressure
|
||||
- geometry count and material count
|
||||
- post-processing cost
|
||||
- asset decode and streaming stalls
|
||||
- WebGL context loss or fallback behavior
|
||||
|
||||
## Common causes of poor performance
|
||||
|
||||
- too many unique materials
|
||||
- oversized textures
|
||||
- heavy GLB assets loaded without optimization
|
||||
- complex post-processing on top of an already expensive scene
|
||||
- physics and render state fighting for ownership
|
||||
- React and scene state updating each other too frequently in React-hosted 3D apps
|
||||
|
||||
## Debugging rules
|
||||
|
||||
- Capture first, then guess.
|
||||
- Reduce the scene until the perf cliff becomes obvious.
|
||||
- Disable post-processing before rewriting core scene code.
|
||||
- Verify the asset pipeline before blaming the renderer.
|
||||
- Treat context-loss handling as a browser requirement, not an edge case.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a transparent edit canvas around a shipped seed sprite frame."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Upscale a seed sprite with nearest-neighbor sampling and place it into "
|
||||
"the leftmost slot of a larger transparent edit canvas."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--seed", required=True, help="Path to the approved seed frame.")
|
||||
parser.add_argument("--out", required=True, help="Path to the output PNG.")
|
||||
parser.add_argument(
|
||||
"--frames",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of horizontal frame slots to reserve. Default: 4.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slot-size",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Size of each square frame slot in pixels. Default: 256.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--canvas-size",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Size of the square transparent canvas in pixels. Default: 1024.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resize_seed(seed: Image.Image, slot_size: int) -> Image.Image:
|
||||
max_dim = max(seed.size)
|
||||
scale = slot_size / max_dim
|
||||
if scale >= 1:
|
||||
scale = max(1, int(scale))
|
||||
width = max(1, int(round(seed.width * scale)))
|
||||
height = max(1, int(round(seed.height * scale)))
|
||||
return seed.resize((width, height), Image.Resampling.NEAREST)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.frames < 1:
|
||||
raise SystemExit("--frames must be at least 1.")
|
||||
if args.slot_size < 1 or args.canvas_size < 1:
|
||||
raise SystemExit("--slot-size and --canvas-size must be positive.")
|
||||
|
||||
strip_width = args.frames * args.slot_size
|
||||
if strip_width > args.canvas_size or args.slot_size > args.canvas_size:
|
||||
raise SystemExit("Frame slots do not fit inside the requested canvas size.")
|
||||
|
||||
seed = Image.open(args.seed).convert("RGBA")
|
||||
seed = resize_seed(seed, args.slot_size)
|
||||
|
||||
canvas = Image.new("RGBA", (args.canvas_size, args.canvas_size), (0, 0, 0, 0))
|
||||
strip_left = (args.canvas_size - strip_width) // 2
|
||||
strip_top = (args.canvas_size - args.slot_size) // 2
|
||||
slot_left = strip_left
|
||||
slot_top = strip_top
|
||||
paste_x = slot_left + (args.slot_size - seed.width) // 2
|
||||
paste_y = slot_top + (args.slot_size - seed.height) // 2
|
||||
canvas.alpha_composite(seed, (paste_x, paste_y))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
152
.hermes/plugins/game-studio/scripts/normalize_sprite_strip.py
Normal file
152
.hermes/plugins/game-studio/scripts/normalize_sprite_strip.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Normalize a raw animation strip into fixed-size transparent frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Extract one horizontal strip into fixed-size frames using a shared "
|
||||
"global scale and bottom-center alignment."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--input", required=True, help="Path to the raw strip image.")
|
||||
parser.add_argument("--out-dir", required=True, help="Output directory for frames.")
|
||||
parser.add_argument(
|
||||
"--frames",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Number of horizontal frames in the strip.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frame-size",
|
||||
type=int,
|
||||
default=64,
|
||||
help="Output square frame size in pixels. Default: 64.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--anchor",
|
||||
help="Optional anchor frame used to stabilize global scale and frame 01 output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lock-frame1",
|
||||
action="store_true",
|
||||
help="Replace frame 01 with the provided anchor frame after normalization.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alpha-threshold",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Pixels with alpha above this threshold count as sprite content. Default: 8.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def threshold_bbox(image: Image.Image, alpha_threshold: int) -> tuple[int, int, int, int] | None:
|
||||
alpha = image.getchannel("A").point(lambda value: 255 if value > alpha_threshold else 0)
|
||||
return alpha.getbbox()
|
||||
|
||||
|
||||
def crop_to_content(image: Image.Image, alpha_threshold: int) -> Image.Image | None:
|
||||
bbox = threshold_bbox(image, alpha_threshold)
|
||||
if bbox is None:
|
||||
return None
|
||||
return image.crop(bbox)
|
||||
|
||||
|
||||
def split_strip(strip: Image.Image, frames: int) -> list[Image.Image]:
|
||||
if frames < 1:
|
||||
raise ValueError("frames must be at least 1")
|
||||
step = strip.width / frames
|
||||
slots: list[Image.Image] = []
|
||||
for index in range(frames):
|
||||
left = int(round(index * step))
|
||||
right = int(round((index + 1) * step))
|
||||
slots.append(strip.crop((left, 0, right, strip.height)))
|
||||
return slots
|
||||
|
||||
|
||||
def max_content_size(images: Iterable[Image.Image | None]) -> tuple[int, int]:
|
||||
widths: list[int] = []
|
||||
heights: list[int] = []
|
||||
for image in images:
|
||||
if image is None:
|
||||
continue
|
||||
widths.append(image.width)
|
||||
heights.append(image.height)
|
||||
if not widths or not heights:
|
||||
raise SystemExit("No sprite content was detected in the provided strip.")
|
||||
return max(widths), max(heights)
|
||||
|
||||
|
||||
def compose_frame(
|
||||
image: Image.Image | None,
|
||||
frame_size: int,
|
||||
scale: float,
|
||||
) -> Image.Image:
|
||||
canvas = Image.new("RGBA", (frame_size, frame_size), (0, 0, 0, 0))
|
||||
if image is None:
|
||||
return canvas
|
||||
|
||||
width = max(1, int(round(image.width * scale)))
|
||||
height = max(1, int(round(image.height * scale)))
|
||||
resized = image.resize((width, height), Image.Resampling.NEAREST)
|
||||
offset_x = (frame_size - width) // 2
|
||||
offset_y = frame_size - height
|
||||
canvas.alpha_composite(resized, (offset_x, offset_y))
|
||||
return canvas
|
||||
|
||||
|
||||
def load_anchor(path: str | None, alpha_threshold: int) -> tuple[Image.Image | None, Image.Image | None]:
|
||||
if path is None:
|
||||
return None, None
|
||||
anchor = Image.open(path).convert("RGBA")
|
||||
cropped = crop_to_content(anchor, alpha_threshold)
|
||||
return anchor, cropped
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.frames < 1:
|
||||
raise SystemExit("--frames must be at least 1.")
|
||||
if args.frame_size < 1:
|
||||
raise SystemExit("--frame-size must be positive.")
|
||||
if args.lock_frame1 and not args.anchor:
|
||||
raise SystemExit("--lock-frame1 requires --anchor.")
|
||||
|
||||
strip = Image.open(args.input).convert("RGBA")
|
||||
slots = split_strip(strip, args.frames)
|
||||
contents = [crop_to_content(slot, args.alpha_threshold) for slot in slots]
|
||||
anchor_image, anchor_content = load_anchor(args.anchor, args.alpha_threshold)
|
||||
max_width, max_height = max_content_size([*contents, anchor_content])
|
||||
scale = min(args.frame_size / max_width, args.frame_size / max_height)
|
||||
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for index, content in enumerate(contents, start=1):
|
||||
if index == 1 and args.lock_frame1:
|
||||
assert anchor_image is not None
|
||||
if anchor_image.width == args.frame_size and anchor_image.height == args.frame_size:
|
||||
frame = anchor_image
|
||||
else:
|
||||
frame = compose_frame(anchor_content, args.frame_size, scale)
|
||||
else:
|
||||
frame = compose_frame(content, args.frame_size, scale)
|
||||
frame.save(out_dir / f"{index:02d}.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a simple contact sheet from a directory of normalized sprite frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
NUMBER_RE = re.compile(r"(\d+)")
|
||||
|
||||
|
||||
def natural_key(path: Path) -> list[int | str]:
|
||||
parts: list[int | str] = []
|
||||
for chunk in NUMBER_RE.split(path.stem):
|
||||
if not chunk:
|
||||
continue
|
||||
if chunk.isdigit():
|
||||
parts.append(int(chunk))
|
||||
else:
|
||||
parts.append(chunk)
|
||||
parts.append(path.suffix)
|
||||
return parts
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a preview contact sheet from a directory of sprite frames."
|
||||
)
|
||||
parser.add_argument("--frames-dir", required=True, help="Directory containing PNG frames.")
|
||||
parser.add_argument("--out", required=True, help="Output PNG path.")
|
||||
parser.add_argument(
|
||||
"--columns",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of columns in the preview sheet. Default: 4.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gap",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Gap between frames in pixels. Default: 8.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def paint_checkerboard(image: Image.Image, tile: int = 16) -> None:
|
||||
draw = ImageDraw.Draw(image)
|
||||
colors = ((240, 243, 246, 255), (225, 230, 235, 255))
|
||||
for top in range(0, image.height, tile):
|
||||
for left in range(0, image.width, tile):
|
||||
color = colors[((left // tile) + (top // tile)) % 2]
|
||||
draw.rectangle((left, top, left + tile, top + tile), fill=color)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.columns < 1:
|
||||
raise SystemExit("--columns must be at least 1.")
|
||||
if args.gap < 0:
|
||||
raise SystemExit("--gap cannot be negative.")
|
||||
|
||||
frame_dir = Path(args.frames_dir)
|
||||
frames = sorted(frame_dir.glob("*.png"), key=natural_key)
|
||||
if not frames:
|
||||
raise SystemExit("No PNG frames were found in --frames-dir.")
|
||||
|
||||
images = [Image.open(path).convert("RGBA") for path in frames]
|
||||
frame_width = max(image.width for image in images)
|
||||
frame_height = max(image.height for image in images)
|
||||
rows = math.ceil(len(images) / args.columns)
|
||||
sheet_width = args.columns * frame_width + max(0, args.columns - 1) * args.gap
|
||||
sheet_height = rows * frame_height + max(0, rows - 1) * args.gap
|
||||
sheet = Image.new("RGBA", (sheet_width, sheet_height), (255, 255, 255, 255))
|
||||
paint_checkerboard(sheet)
|
||||
|
||||
for index, image in enumerate(images):
|
||||
row = index // args.columns
|
||||
column = index % args.columns
|
||||
left = column * (frame_width + args.gap) + (frame_width - image.width) // 2
|
||||
top = row * (frame_height + args.gap) + (frame_height - image.height) // 2
|
||||
sheet.alpha_composite(image, (left, top))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
76
.hermes/plugins/game-studio/skills/game-playtest/SKILL.md
Normal file
76
.hermes/plugins/game-studio/skills/game-playtest/SKILL.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: game-playtest
|
||||
description: Run browser-game playtests and frontend QA. Use when the user asks for smoke tests, screenshot-based verification, browser automation, HUD or overlay review, or structured issue-finding in a browser game.
|
||||
---
|
||||
|
||||
# Game Playtest
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to test browser games the way players experience them: through boot, input, scene transitions, HUD readability, and visual state changes. Prefer browser automation and screenshot review when the project supports it.
|
||||
|
||||
## Preferred Workflow
|
||||
|
||||
1. Boot the game and confirm the first actionable screen.
|
||||
2. Exercise the main verbs.
|
||||
3. Capture screenshots from representative states.
|
||||
4. Check the UI layer independently from the render layer.
|
||||
5. Report findings in severity order with reproduction steps.
|
||||
|
||||
## Tooling Guidance
|
||||
|
||||
- Prefer Playwright or equivalent browser automation already available in the repo.
|
||||
- When the game is canvas or WebGL heavy, screenshots are mandatory because DOM assertions alone miss visual regressions.
|
||||
- Use screenshots to judge playfield obstruction and HUD weight, not just correctness of text or layout.
|
||||
- When deterministic automation is not practical, do a structured manual pass and capture evidence.
|
||||
- For 3D rendering bugs or unexplained frame cost, use SpectorJS and browser performance tooling rather than guessing from code alone.
|
||||
|
||||
## Common Checks
|
||||
|
||||
### 2D checks
|
||||
|
||||
- sprite alignment and baseline consistency
|
||||
- hit or hurt animation readability
|
||||
- HUD overlap with the playfield
|
||||
- command menu state changes
|
||||
- tile or platform readability
|
||||
- input-state feedback and turn-state clarity
|
||||
|
||||
### 3D checks
|
||||
|
||||
- first-load playability versus dashboard-like chrome
|
||||
- persistent overlay weight versus playfield visibility
|
||||
- camera control and camera reset behavior
|
||||
- pointer-lock or drag-look transitions when menus and overlays open
|
||||
- depth readability and silhouette clarity
|
||||
- secondary panels collapsed or dismissible during normal play
|
||||
- resize behavior
|
||||
- WebGL context loss or renderer fallback behavior
|
||||
- material or lighting regressions
|
||||
- GLB or texture streaming stalls
|
||||
- collision proxy or physics mismatch
|
||||
- performance cliffs tied to post-processing or asset load
|
||||
|
||||
## Responsive and Browser Checks
|
||||
|
||||
- desktop and mobile viewport sanity
|
||||
- safe-area and notch issues where relevant
|
||||
- reduced-motion behavior for UI transitions
|
||||
- keyboard, pointer, and pause-state handling
|
||||
- React state and scene state synchronization when the project uses React Three Fiber
|
||||
|
||||
## Reporting Standard
|
||||
|
||||
Lead with findings. Keep each finding concrete:
|
||||
|
||||
- what the user sees
|
||||
- how to reproduce it
|
||||
- why it matters
|
||||
- what likely subsystem owns it
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend review cues: `../game-ui-frontend/SKILL.md`
|
||||
- 3D debugging notes: `../../references/webgl-debugging-and-performance.md`
|
||||
- Full checklist: `../../references/playtest-checklist.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game Playtest"
|
||||
short_description: "Run browser-game playtests and QA"
|
||||
default_prompt: "Playtest the browser game, check core interactions and visual state changes, and report concrete issues."
|
||||
94
.hermes/plugins/game-studio/skills/game-studio/SKILL.md
Normal file
94
.hermes/plugins/game-studio/skills/game-studio/SKILL.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: game-studio
|
||||
description: Route early browser-game work. Use when the user needs stack selection and workflow planning across design, implementation, assets, and playtesting before moving to a specialist skill.
|
||||
---
|
||||
|
||||
# Game Studio
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill as the umbrella entrypoint for browser-game work. Default to a 2D Phaser path unless the user explicitly asks for 3D, Three.js, React Three Fiber, shader-heavy rendering, or another WebGL-first direction.
|
||||
|
||||
This plugin is intentionally asymmetric:
|
||||
|
||||
- 2D is the strongest execution path in v1.
|
||||
- 3D has one opinionated default ecosystem: vanilla Three.js for plain TypeScript or Vite apps, React Three Fiber for React-hosted 3D apps, and GLB or glTF 2.0 as the default shipping asset format.
|
||||
- Shared architecture, UI, and playtest practices apply to both.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the user is still choosing a stack
|
||||
- the request spans multiple domains such as runtime, UI, asset pipeline, and QA
|
||||
- the user says "help me build a game" without naming the implementation path
|
||||
|
||||
## Do Not Stay Here When
|
||||
|
||||
- the runtime is clearly plain Three.js
|
||||
- the runtime is clearly React Three Fiber
|
||||
- the task is clearly a shipped-asset problem
|
||||
- the task is clearly frontend-only or QA-only
|
||||
|
||||
Once the intent is clear, route to the most specific specialist skill and continue from there.
|
||||
|
||||
## Routing Rules
|
||||
|
||||
1. Classify the request before designing or coding:
|
||||
- `2D default`: Phaser, sprites, tilemaps, top-down, side-view, grid tactics, action platformers.
|
||||
- `3D + plain TS/Vite`: imperative scene control, engine-like loops, non-React apps, direct Three.js work.
|
||||
- `3D + React`: React-hosted product surfaces, declarative scene composition, shared React state, UI-heavy 3D apps.
|
||||
- `3D asset pipeline`: GLB, glTF, texture packaging, compression, LOD, runtime asset size.
|
||||
- `Alternative engine`: Babylon.js or PlayCanvas requests, usually as comparison or ecosystem fit questions.
|
||||
- `Shared`: core loop design, frontend direction, save/debug/perf boundaries, browser QA.
|
||||
2. Route to the specialist skills immediately after classification:
|
||||
- Shared architecture and engine choice: `../web-game-foundations/SKILL.md`
|
||||
- Deep 2D implementation: `../phaser-2d-game/SKILL.md`
|
||||
- Vanilla Three.js implementation: `../three-webgl-game/SKILL.md`
|
||||
- React-hosted 3D implementation: `../react-three-fiber-game/SKILL.md`
|
||||
- 3D asset shipping and optimization: `../web-3d-asset-pipeline/SKILL.md`
|
||||
- HUD and menu direction: `../game-ui-frontend/SKILL.md`
|
||||
- 2D sprite generation and normalization: `../sprite-pipeline/SKILL.md`
|
||||
- Browser QA and visual review: `../game-playtest/SKILL.md`
|
||||
3. Keep one coherent plan across the routed skills. Do not let engine, UI, asset, and QA decisions drift apart.
|
||||
|
||||
## Default Workflow
|
||||
|
||||
1. Lock the game fantasy and player verbs.
|
||||
2. Define the core loop, failure states, progression, and target play session length.
|
||||
3. Choose the implementation track:
|
||||
- Default to Phaser for 2D browser games.
|
||||
- Choose vanilla Three.js when the project is explicitly 3D and wants direct render-loop control in a plain TypeScript or Vite app.
|
||||
- Choose React Three Fiber when the project already lives in React or wants declarative scene composition with shared React state.
|
||||
- Choose raw WebGL only when the user explicitly wants a custom renderer or shader-first surface.
|
||||
4. Define the UI surface early. Browser games usually need a DOM HUD and menu layer even when the playfield is canvas or WebGL.
|
||||
- For 3D starter scaffolds, default to a low-chrome HUD that preserves the playfield and keeps secondary panels collapsed.
|
||||
5. Decide the asset workflow:
|
||||
- 2D characters and effects: use `sprite-pipeline`.
|
||||
- 3D models, textures, and shipping format: use `web-3d-asset-pipeline`.
|
||||
6. Close with a playtest loop before calling the work production-ready.
|
||||
|
||||
## Output Expectations
|
||||
|
||||
- For planning requests, return a game-specific plan with stack choice, gameplay loop, UI surface, asset workflow, and test approach.
|
||||
- For implementation requests, keep the chosen stack obvious in the file structure and code boundaries.
|
||||
- For mixed requests, preserve the plugin default: 2D Phaser first unless the user asks for something else.
|
||||
- When the user asks about Babylon.js or PlayCanvas, compare them honestly but keep Three.js and R3F as the primary code-generation defaults unless the user explicitly chooses another engine.
|
||||
|
||||
## References
|
||||
|
||||
- Engine selection: `../../references/engine-selection.md`
|
||||
- Three.js stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- Vanilla Three.js starter: `../../references/threejs-vanilla-starter.md`
|
||||
- React Three Fiber starter: `../../references/react-three-fiber-starter.md`
|
||||
- Frontend prompting patterns: `../../references/frontend-prompts.md`
|
||||
- Playtest checklist: `../../references/playtest-checklist.md`
|
||||
|
||||
## Examples
|
||||
|
||||
- "Help me prototype a browser tactics game."
|
||||
- "I need a Phaser-based action game loop with a HUD and menus."
|
||||
- "I want a Three.js exploration demo with WebGL lighting and browser-safe UI."
|
||||
- "I want a React-based 3D configurator with React Three Fiber."
|
||||
- "Optimize my GLB assets for the web and keep the file sizes under control."
|
||||
- "Set up the asset workflow for consistent 2D sprite animations."
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game Studio"
|
||||
short_description: "Route browser-game work to the right path"
|
||||
default_prompt: "Help me choose the right browser-game stack and workflow before implementation starts."
|
||||
112
.hermes/plugins/game-studio/skills/game-ui-frontend/SKILL.md
Normal file
112
.hermes/plugins/game-studio/skills/game-ui-frontend/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: game-ui-frontend
|
||||
description: Design UI surfaces for browser games. Use when the user asks for HUDs, menus, overlays, responsive layouts, or visual direction that must protect the playfield.
|
||||
---
|
||||
|
||||
# Game UI Frontend
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill whenever the game needs a visible interface layer. The job is not to produce generic dashboard UI. The job is to produce a readable, thematic browser-game interface that supports the play experience.
|
||||
|
||||
Default assumption: build the game world in canvas or WebGL, and build text-heavy UI in DOM.
|
||||
|
||||
## Frontend Standards
|
||||
|
||||
1. Establish visual direction before coding.
|
||||
- Genre and fantasy
|
||||
- Material language
|
||||
- Typography
|
||||
- Palette
|
||||
- Motion tone
|
||||
2. Use CSS variables for the UI theme.
|
||||
3. Build clear hierarchy.
|
||||
- Critical combat or survival information first
|
||||
- Secondary tools second
|
||||
- Rarely used settings behind menus or drawers
|
||||
4. Protect the playfield first, especially in 3D.
|
||||
- The initial screen should feel playable within a few seconds.
|
||||
- Default to one primary persistent HUD cluster and at most one small secondary cluster.
|
||||
- Keep the center of the playfield clear during normal play.
|
||||
- Keep the lower-middle playfield mostly clear during normal play.
|
||||
- Put lore, field notes, quest details, and long control lists behind drawers, toggles, or pause surfaces.
|
||||
- Prefer contextual prompts and transient hints over permanent boxed panels.
|
||||
5. Keep overlays readable over motion.
|
||||
- Use backing panels, edge treatment, contrast, and restrained blur where needed.
|
||||
6. Design for both desktop and mobile from the start.
|
||||
7. Design 3D UI around camera and input control boundaries.
|
||||
- Pause or gate camera-control input when menus, dialogs, or pointer-driven UI are active.
|
||||
- Keep pointer-lock, drag-to-look, and menu interaction states explicit.
|
||||
|
||||
## 3D Starter Defaults
|
||||
|
||||
For exploration, traversal, or third-person starter scaffolds, prefer this UI budget:
|
||||
|
||||
- one compact objective chip or status strip at the edge
|
||||
- one transient controls hint or interaction prompt
|
||||
- one optional collapsible secondary surface such as a journal, map, or quest log
|
||||
|
||||
Do not open every informational surface on first load. The scene should be readable before the user opens any deeper UI.
|
||||
|
||||
As a default implementation constraint for 3D browser games:
|
||||
|
||||
- no always-on full-width header plus multi-card body plus full-width footer layout
|
||||
- no large center-screen or lower-middle overlays during normal movement
|
||||
- no more than roughly 20-25% of the viewport covered by persistent HUD on desktop unless the user explicitly requests a denser layout
|
||||
- on mobile, collapse to a narrow stack or contextual chips before covering the playfield with larger panels
|
||||
|
||||
## Prompting Rules
|
||||
|
||||
When asking the model to design or implement game UI, include:
|
||||
|
||||
- the game fantasy
|
||||
- the camera or viewpoint
|
||||
- the player verbs
|
||||
- the HUD layers
|
||||
- the camera or control mode when the game is 3D
|
||||
- the tone of motion
|
||||
- desktop and mobile expectations
|
||||
- playfield protection and disclosure strategy
|
||||
- explicit anti-patterns to avoid
|
||||
|
||||
Use `../../references/frontend-prompts.md` for concrete prompt shapes.
|
||||
|
||||
## Motion Rules
|
||||
|
||||
- Prefer a few meaningful transitions over constant micro-animation.
|
||||
- Reserve strong motion for state change, reward, danger, and onboarding.
|
||||
- Respect reduced-motion settings for non-essential animation.
|
||||
- Keep 3D HUD motion from competing with camera motion.
|
||||
|
||||
## What Good Looks Like
|
||||
|
||||
- HUD elements are legible without flattening the scene.
|
||||
- Menus feel native to the game world, not like a SaaS admin panel.
|
||||
- Layout adapts cleanly across breakpoints.
|
||||
- Pointer, keyboard, and game-state feedback are obvious.
|
||||
- In 3D games, menu and HUD states do not fight camera control or pointer-lock.
|
||||
- In 3D games, the first playable view keeps most of the viewport available for movement, aiming, and spatial reading.
|
||||
- Persistent information density is low enough that screenshots still read as game scenes, not UI comps.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Generic app dashboard layouts
|
||||
- Flat placeholder styling with no theme
|
||||
- Default font stacks without intent
|
||||
- Dense overlays that obscure the playfield
|
||||
- Large title cards or multi-paragraph notes sitting over a live playable scene
|
||||
- Equal-weight boxed panels distributed around every edge of the viewport
|
||||
- Controls, objectives, notes, and lore all expanded at once on first load
|
||||
- Full-width top-and-bottom chrome with large always-on center or body panels in 3D play
|
||||
- Excessive motion on every element
|
||||
- Canvas-only UI when DOM would be clearer and cheaper
|
||||
- Forcing HUD controls into the 3D scene when standard DOM would be clearer
|
||||
- Letting camera input remain active under modals or inventory panels
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Prompt recipes: `../../references/frontend-prompts.md`
|
||||
- Low-chrome 3D layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- React-hosted 3D UI context: `../react-three-fiber-game/SKILL.md`
|
||||
- Playtest review: `../../references/playtest-checklist.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game UI Frontend"
|
||||
short_description: "Design browser-game HUDs, menus, and overlays"
|
||||
default_prompt: "Design a browser-game UI layer that supports the play experience without crowding the playfield."
|
||||
88
.hermes/plugins/game-studio/skills/phaser-2d-game/SKILL.md
Normal file
88
.hermes/plugins/game-studio/skills/phaser-2d-game/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: phaser-2d-game
|
||||
description: Implement 2D browser games with Phaser. Use when the user wants a Phaser, TypeScript, and Vite stack for scenes, gameplay systems, cameras, sprite animation, and DOM-overlay HUD patterns.
|
||||
---
|
||||
|
||||
# Phaser 2D Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for the main execution path in this plugin. Phaser is the default stack for 2D browser games here because it handles rendering, timing, sprites, cameras, and scene orchestration well without forcing gameplay rules into the framework.
|
||||
|
||||
Preferred stack:
|
||||
|
||||
- Phaser
|
||||
- TypeScript
|
||||
- Vite
|
||||
- DOM-based HUD or menus layered over the game canvas
|
||||
|
||||
## Architecture
|
||||
|
||||
1. Keep gameplay state outside Phaser scenes.
|
||||
- Systems own rules, turn order, movement, combat, inventory, objectives, and progression.
|
||||
- Phaser scenes adapt system state into sprites, camera motion, animation playback, and effects.
|
||||
2. Make scenes thin.
|
||||
- Boot and asset preload
|
||||
- Menu or shell scene
|
||||
- Gameplay scene
|
||||
- Optional overlay or debug scene
|
||||
3. Keep renderer-facing objects disposable.
|
||||
- Sprite containers, emitters, tweens, and camera rigs are view state, not source of truth.
|
||||
4. Favor stable asset manifest keys over direct file-path references throughout gameplay code.
|
||||
|
||||
## Implementation Guidance
|
||||
|
||||
- Use one integration boundary where the scene reads simulation state and emits input actions back.
|
||||
- Prefer deterministic system updates over scene-local mutation.
|
||||
- Treat HUD and menus as DOM when text, status density, or responsiveness matter.
|
||||
- Keep animation state derived from gameplay state rather than ad hoc sprite flags.
|
||||
|
||||
## 2D Modes Covered Well
|
||||
|
||||
- Turn-based grids and tactics
|
||||
- Top-down exploration
|
||||
- Side-view action platformers
|
||||
- Character-action combat with sprite animation
|
||||
- Lightweight management or deck-driven battle scenes
|
||||
|
||||
## Camera and Presentation
|
||||
|
||||
- Choose the camera model early: locked, follow, room-based, or tactical-pan.
|
||||
- Keep camera logic separate from game rules.
|
||||
- Use restrained screen shake, hit-stop, and parallax. Effects should improve readability, not obscure it.
|
||||
|
||||
## UI Integration
|
||||
|
||||
- Use DOM overlays for HUD, command menus, settings, and narrative panels.
|
||||
- Keep the canvas responsible for the world, combat readability, and motion.
|
||||
- Avoid shoving dense text or complex settings UIs into Phaser unless the project explicitly needs an in-canvas presentation.
|
||||
|
||||
## Asset Organization
|
||||
|
||||
- `characters/`
|
||||
- `environment/`
|
||||
- `ui/`
|
||||
- `fx/`
|
||||
- `audio/`
|
||||
- `data/`
|
||||
|
||||
Keep manifest keys human-readable and stable.
|
||||
|
||||
## Default Directory Shape
|
||||
|
||||
See `../../references/phaser-architecture.md` for a concrete module split.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Game rules inside `update()` loops without a system boundary
|
||||
- Scene-to-scene state passed through mutable global objects
|
||||
- HUD text rendered in the game canvas just because it is convenient
|
||||
- Asset paths embedded everywhere instead of a manifest layer
|
||||
- Overusing generic React dashboard patterns for game UI
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- Sprite workflow: `../sprite-pipeline/SKILL.md`
|
||||
- Phaser structure: `../../references/phaser-architecture.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Phaser 2D Game"
|
||||
short_description: "Build 2D browser games with Phaser"
|
||||
default_prompt: "Implement this 2D browser game with Phaser, TypeScript, and a clear gameplay architecture."
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: react-three-fiber-game
|
||||
description: Build React-hosted 3D browser games with React Three Fiber. Use when the user wants pmndrs-based scene composition, shared React state, and 3D HUD integration inside a React app.
|
||||
---
|
||||
|
||||
# React Three Fiber Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when the 3D runtime lives inside a React application. This is the default React-native 3D path in the plugin and should be preferred over vanilla Three.js when the app shell, settings, storefront, editor surface, or surrounding product already uses React.
|
||||
|
||||
Recommended stack:
|
||||
|
||||
- `@react-three/fiber`
|
||||
- `three`
|
||||
- `@react-three/drei`
|
||||
- `@react-three/rapier`
|
||||
- `@react-three/postprocessing`
|
||||
- `@react-three/a11y` when accessibility-sensitive interaction matters
|
||||
- DOM overlays in the normal React tree
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the project already uses React
|
||||
- the 3D scene must share state with the rest of the app
|
||||
- declarative scene composition is a net gain
|
||||
- the team wants pmndrs helpers instead of building every helper layer by hand
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the app is not React-based
|
||||
- the project wants a cleaner imperative runtime with minimal React coordination
|
||||
- the problem is asset packaging rather than runtime composition
|
||||
|
||||
## Best Fit Scenarios
|
||||
|
||||
- 3D configurators and tool-rich browser products
|
||||
- React apps with embedded game or scene surfaces
|
||||
- 3D menus, editors, or world maps in an existing React app
|
||||
- 3D game UIs that depend on shared app state and non-canvas shells
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. Keep simulation state outside render components.
|
||||
- React components should describe scene composition, not become the source of truth for gameplay rules.
|
||||
2. Use React state and scene state deliberately.
|
||||
- Shared UI state can live in app state.
|
||||
- High-frequency simulation should not force the whole app through unnecessary React churn.
|
||||
3. Use pmndrs helpers intentionally.
|
||||
- Drei for controls, loaders, helpers, environments, and common scene primitives.
|
||||
- `@react-three/rapier` for physics integration.
|
||||
- `@react-three/postprocessing` for optional effects.
|
||||
- `@react-three/a11y` when the interaction model benefits from accessible scene semantics.
|
||||
4. Keep HUD, settings, and menus in DOM by default.
|
||||
5. Keep starter scaffolds visually restrained.
|
||||
- Start with one compact objective or status surface and transient prompts.
|
||||
- Keep notes, maps, and multi-step checklists collapsed until opened.
|
||||
- Do not surround the `Canvas` with equally weighted glass cards.
|
||||
|
||||
## Architectural Guidance
|
||||
|
||||
- Use a dedicated scene root component that owns the `Canvas`.
|
||||
- Keep camera rigs and control components isolated from gameplay systems.
|
||||
- Keep loader and asset wrappers predictable.
|
||||
- Keep DOM overlays and the 3D scene coordinated through explicit state boundaries.
|
||||
- If a system needs tight imperative control, isolate it rather than forcing everything into declarative patterns.
|
||||
- If the scene is immediately playable, keep the initial overlay budget low and let the world do more of the onboarding.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Treating React components as the gameplay state store
|
||||
- Pushing heavy per-frame mutation through broad app state
|
||||
- Using R3F only because React is available, even when the project needs a cleaner imperative runtime
|
||||
- Building HUD or inventory UI inside the 3D scene by default
|
||||
- Shipping an initial scaffold with large cards occupying every side of the viewport
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- 3D HUD layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- React starter: `../../references/react-three-fiber-starter.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- WebGL debugging and perf: `../../references/webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "React Three Fiber Game"
|
||||
short_description: "Build React-hosted 3D browser games"
|
||||
default_prompt: "Build this 3D browser game with React Three Fiber and keep the 3D runtime aligned with the React app shell."
|
||||
102
.hermes/plugins/game-studio/skills/sprite-pipeline/SKILL.md
Normal file
102
.hermes/plugins/game-studio/skills/sprite-pipeline/SKILL.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: sprite-pipeline
|
||||
description: Generate and normalize 2D sprite animations. Use when the user asks for full-strip generation from approved source frames, consistent anchor and scale normalization, or preview assets for browser-game animation.
|
||||
---
|
||||
|
||||
# Sprite Pipeline
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for 2D sprite generation and normalization. This workflow is intentionally anchored around one approved frame and a whole-strip generation pass because frame-by-frame generation drifts too easily.
|
||||
|
||||
This skill is 2D-specific. If the request is for 3D characters, meshes, or materials, route back through `../game-studio/SKILL.md`.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. Start from an approved in-game seed frame.
|
||||
- The seed frame should already reflect the right silhouette, palette, costume, and proportions.
|
||||
2. Build a larger transparent reference canvas around that frame.
|
||||
- Use `../../scripts/build_sprite_edit_canvas.py`.
|
||||
3. Ask for the full animation strip in one edit request.
|
||||
- Do not generate each frame independently unless the user explicitly accepts lower consistency.
|
||||
4. Normalize the result into fixed-size game frames.
|
||||
- Use `../../scripts/normalize_sprite_strip.py`.
|
||||
- Use one shared scale across the whole strip.
|
||||
- Align frames with one shared anchor, typically bottom-center.
|
||||
5. Optionally lock frame 01 back to the shipped seed frame.
|
||||
- Do this when the animation should begin from the exact idle or base pose already in game.
|
||||
6. Render a preview sheet and inspect the animation in-engine before approving it.
|
||||
- Use `../../scripts/render_sprite_preview_sheet.py`.
|
||||
|
||||
## Prompting Rules
|
||||
|
||||
Always preserve these invariants in the prompt:
|
||||
|
||||
- same character
|
||||
- same facing direction
|
||||
- same palette family
|
||||
- same silhouette family
|
||||
- same readable face or key features
|
||||
- same outfit proportions
|
||||
- transparent background
|
||||
- exact frame count and slot layout
|
||||
|
||||
Always ask for:
|
||||
|
||||
- one strip at once
|
||||
- a transparent canvas
|
||||
- no scenery, labels, or poster composition
|
||||
- crisp pixel-art clusters for pixel work
|
||||
- production asset tone, not concept art
|
||||
|
||||
## Using Image Generation
|
||||
|
||||
For live asset generation or edits, use the installed `imagegen` skill in this workspace. This skill defines the game-specific process; `imagegen` handles the API-backed generation or edit execution.
|
||||
|
||||
## Script Recipes
|
||||
|
||||
Create a reference canvas:
|
||||
|
||||
```bash
|
||||
python3 scripts/build_sprite_edit_canvas.py \
|
||||
--seed output/sprites/idle-01.png \
|
||||
--out output/sprites/hurt-edit-canvas.png \
|
||||
--frames 4 \
|
||||
--slot-size 256 \
|
||||
--canvas-size 1024
|
||||
```
|
||||
|
||||
Normalize a raw strip:
|
||||
|
||||
```bash
|
||||
python3 scripts/normalize_sprite_strip.py \
|
||||
--input output/sprites/hurt-raw.png \
|
||||
--out-dir output/sprites/hurt \
|
||||
--frames 4 \
|
||||
--frame-size 64 \
|
||||
--anchor output/sprites/idle-01.png \
|
||||
--lock-frame1
|
||||
```
|
||||
|
||||
Render a preview sheet:
|
||||
|
||||
```bash
|
||||
python3 scripts/render_sprite_preview_sheet.py \
|
||||
--frames-dir output/sprites/hurt \
|
||||
--out output/sprites/hurt-preview.png \
|
||||
--columns 4
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- proportions stay stable across frames
|
||||
- frame-to-frame size does not drift
|
||||
- action reads clearly at game scale
|
||||
- transparency is preserved
|
||||
- frame 01 matches the shipped sprite when lockback is enabled
|
||||
- preview looks correct before any in-engine asset index update
|
||||
|
||||
## References
|
||||
|
||||
- Detailed workflow: `../../references/sprite-pipeline.md`
|
||||
- Shared frontend context: `../game-ui-frontend/SKILL.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Sprite Pipeline"
|
||||
short_description: "Generate and normalize 2D sprite animations"
|
||||
default_prompt: "Create and normalize 2D sprite animation assets for a browser game with consistent scale and anchors."
|
||||
124
.hermes/plugins/game-studio/skills/three-webgl-game/SKILL.md
Normal file
124
.hermes/plugins/game-studio/skills/three-webgl-game/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: three-webgl-game
|
||||
description: Implement browser-game runtimes with plain Three.js. Use when the user wants imperative scene control in TypeScript or Vite with GLB assets, loaders, physics, and low-level WebGL debugging.
|
||||
---
|
||||
|
||||
# Three WebGL Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for the default non-React 3D path in the plugin. This is not generic WebGL advice. It is an opinionated stack for browser 3D work:
|
||||
|
||||
- `three`
|
||||
- TypeScript
|
||||
- Vite
|
||||
- GLB or glTF 2.0 assets
|
||||
- Three.js loaders such as `GLTFLoader`, `DRACOLoader`, and `KTX2Loader`
|
||||
- Rapier JS for physics
|
||||
- SpectorJS for GPU and frame debugging
|
||||
- DOM overlays for HUD, menus, and settings
|
||||
|
||||
Use this skill when the project wants direct scene, camera, renderer, and game-loop control. If the app already lives in React, route to `../react-three-fiber-game/SKILL.md` instead.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the app is plain TypeScript or Vite rather than React-first
|
||||
- the project wants direct imperative control over the render loop
|
||||
- the user asks for Three.js specifically
|
||||
- the runtime needs engine-like control over scene, camera, loaders, and physics
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the 3D scene lives inside an existing React app
|
||||
- the main problem is shipped-asset optimization rather than runtime code
|
||||
- the user explicitly chose Babylon.js or PlayCanvas
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. Keep simulation state outside Three.js objects.
|
||||
- Game rules, AI, quest state, timers, and progression should not live inside meshes or materials.
|
||||
2. Treat the render graph as an adapter.
|
||||
- Scene graph, cameras, materials, loaders, and post-processing are view concerns layered over simulation state.
|
||||
3. Keep camera behavior explicit.
|
||||
- Orbit, follow, chase, rail, and first-person styles each need their own control boundary.
|
||||
4. Keep UI out of WebGL unless the presentation absolutely depends on it.
|
||||
- Menus, HUD, inventories, and settings should default to DOM.
|
||||
5. Use GLB or glTF 2.0 as the default shipping model format.
|
||||
- Do not build the runtime around DCC-native formats.
|
||||
6. Use Rapier instead of ad hoc collision code when the game has meaningful 3D physics or collision response.
|
||||
7. Keep the first playable view low-chrome.
|
||||
- Default to one compact objective or status cluster plus transient prompts.
|
||||
- Long notes, lore, and controls references should be collapsed until asked for.
|
||||
- Do not frame the scene with multiple equal-weight cards during normal play.
|
||||
|
||||
## Initial Scaffold UX
|
||||
|
||||
For exploration, traversal, and character-control prototypes, start with a sparse shell:
|
||||
|
||||
- one edge-aligned objective chip
|
||||
- one transient controls hint
|
||||
- one optional compact status strip
|
||||
|
||||
Only add larger UI surfaces when the game loop truly requires them. Journal, quest log, codex, map, and settings surfaces should open on demand, not occupy the viewport by default.
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
Use the module shape in `../../references/three-webgl-architecture.md`, then keep these boundaries clean:
|
||||
|
||||
- `simulation/`: rules, progression, state, and AI
|
||||
- `render/app/`: renderer, scene, camera, resize, context lifecycle
|
||||
- `render/loaders/`: GLTF, Draco, KTX2, texture, and environment loading
|
||||
- `render/objects/`: mesh instantiation and disposal
|
||||
- `render/materials/`: material setup and shader boundaries
|
||||
- `physics/`: Rapier world, bodies, colliders, and simulation bridge
|
||||
- `ui/`: DOM overlays and menus
|
||||
- `diagnostics/`: debug toggles, perf probes, and capture hooks
|
||||
|
||||
## Good Fit Scenarios
|
||||
|
||||
- Exploration demos
|
||||
- Lightweight 3D combat prototypes
|
||||
- Vehicle or traversal prototypes
|
||||
- Scene-driven product or world showcases with gameplay
|
||||
- Material, lighting, or post-process-led experiences
|
||||
- 3D games where camera movement and depth readability are central
|
||||
|
||||
## Loaders, Assets, and Post-Processing
|
||||
|
||||
- Start with `GLTFLoader` for shipped 3D content.
|
||||
- Add `DRACOLoader` or Meshopt-compatible optimization as part of the asset pipeline, not as a random runtime patch.
|
||||
- Use `KTX2Loader` for compressed textures when the asset pipeline provides them.
|
||||
- Prefer built-in Three.js render and post-processing utilities first. Add heavier abstraction only when the project actually needs it.
|
||||
- Keep post-processing optional and measurable. Bloom and color effects should not hide gameplay readability.
|
||||
|
||||
## Shader and Material Guidance
|
||||
|
||||
- Start with standard Three.js materials and correct lighting before reaching for custom shaders.
|
||||
- Use custom shaders only when the visual target genuinely needs them.
|
||||
- Keep shader parameters driven by game state, not by incidental scene mutations.
|
||||
- If a material stack gets complex, isolate it behind material factories instead of scattering shader setup across scene code.
|
||||
|
||||
## Browser Safety
|
||||
|
||||
- Handle resize explicitly.
|
||||
- Expect WebGL context loss and recovery.
|
||||
- Keep a fallback or degraded mode in mind for fragile GPU paths.
|
||||
- Watch texture size, geometry count, draw-call growth, and post-processing cost.
|
||||
- Use SpectorJS when the scene behaves incorrectly or frame cost is unclear.
|
||||
|
||||
## Scope Warning
|
||||
|
||||
Do not claim that this plugin offers equal 3D depth to the Phaser track. It supports serious 3D implementation, but the plugin is still 2D-first overall.
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- 3D HUD layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- Three.js ecosystem: `../../references/threejs-stack.md`
|
||||
- Three.js structure: `../../references/three-webgl-architecture.md`
|
||||
- Vanilla starter: `../../references/threejs-vanilla-starter.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- WebGL debugging and perf: `../../references/webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Three WebGL Game"
|
||||
short_description: "Build browser-game runtimes with Three.js"
|
||||
default_prompt: "Implement this browser-game runtime with plain Three.js and keep the scene architecture easy to debug."
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: web-3d-asset-pipeline
|
||||
description: Prepare and optimize browser-game 3D assets. Use when the user asks for GLB or glTF shipping work, including Blender cleanup and export, collision or LOD setup, compression, texture packaging, and runtime validation.
|
||||
---
|
||||
|
||||
# Web 3D Asset Pipeline
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for shipped 3D assets, not runtime scene code. The default output format for browser 3D work in this plugin is GLB or glTF 2.0. The goal is predictable runtime assets, not whatever the DCC tool happened to export first.
|
||||
|
||||
This guidance is engine-agnostic and can serve Three.js, React Three Fiber, Babylon.js, or PlayCanvas.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the task is about GLB or glTF shipping format
|
||||
- the task is about model cleanup, texture packaging, compression, LOD, or collision proxies
|
||||
- the runtime stack is already chosen and the remaining problem is asset quality or size
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the task is about scene, camera, renderer, or game-loop structure
|
||||
- the task is purely about React versus vanilla Three.js routing
|
||||
- the user is still deciding between runtime engines
|
||||
|
||||
## Default Pipeline
|
||||
|
||||
1. Author and clean the source asset in a DCC tool such as Blender.
|
||||
2. Export to GLB or glTF 2.0.
|
||||
3. Optimize with glTF Transform.
|
||||
4. Validate naming, pivots, transforms, material reuse, and texture budgets.
|
||||
5. Add collision proxies, LOD strategy, and baked-lighting assumptions as needed.
|
||||
6. Ship the optimized asset and load it with engine-native GLTF support.
|
||||
|
||||
## Format Rules
|
||||
|
||||
- Default shipping format: GLB or glTF 2.0.
|
||||
- Do not treat FBX, OBJ, or DCC-native formats as the long-term runtime contract.
|
||||
- Apply or normalize transforms before shipping.
|
||||
- Keep units, pivots, and orientation conventions consistent across the whole asset set.
|
||||
|
||||
## Optimization Rules
|
||||
|
||||
- Use glTF Transform for pruning, deduplication, simplification, and packaging.
|
||||
- Use geometry compression intentionally.
|
||||
- Draco is a valid option when decode cost and compatibility fit the runtime.
|
||||
- Meshopt is often a strong default for web delivery.
|
||||
- Compress textures deliberately.
|
||||
- Use KTX2 or BasisU when the runtime stack supports it.
|
||||
- Use WebP or AVIF where they make sense in the broader asset pipeline.
|
||||
- Reuse materials and textures where possible to cut memory and draw-call cost.
|
||||
|
||||
## Runtime-Ready Asset Rules
|
||||
|
||||
- Keep model hierarchy names stable and meaningful.
|
||||
- Set pivots and origins for gameplay interaction, not just for DCC convenience.
|
||||
- Author explicit collision proxies for physics-heavy scenes.
|
||||
- Decide whether lighting is dynamic, baked, or hybrid before final export.
|
||||
- Plan LODs for large environments or repeated props.
|
||||
- Keep texture resolution proportional to on-screen use, not source-art ambition.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
- Shipping raw DCC exports without cleanup
|
||||
- Too many unique materials
|
||||
- Texture sizes far above visible need
|
||||
- Missing collision proxies
|
||||
- Scale or pivot mismatches between assets
|
||||
- Runtime code compensating for asset mistakes that should be fixed upstream
|
||||
|
||||
## References
|
||||
|
||||
- Three.js stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline reference: `../../references/web-3d-asset-pipeline.md`
|
||||
- Alternative engines: `../../references/alternative-3d-engines.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Web 3D Asset Pipeline"
|
||||
short_description: "Prepare and optimize browser-game 3D assets"
|
||||
default_prompt: "Prepare these browser-game 3D assets for shipping as predictable runtime-ready GLB or glTF files."
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: web-game-foundations
|
||||
description: Set browser-game architecture before implementation. Use when the user needs engine choice, simulation and render boundaries, input model, asset organization, or save/debug/performance strategy.
|
||||
---
|
||||
|
||||
# Web Game Foundations
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to establish the non-negotiable architecture before implementation starts. Browser games degrade quickly when simulation, rendering, UI, asset loading, and input handling are mixed together.
|
||||
|
||||
Default rule: simulation state is owned outside the renderer, browser UI is not forced into the canvas unless there is a clear reason, and shipped 3D assets default to GLB or glTF 2.0 rather than ad hoc model formats.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the user has not settled the engine or renderer choice
|
||||
- the task is about boundaries, module shape, state ownership, or asset policy
|
||||
- multiple specialist skills need one shared architectural frame
|
||||
|
||||
## Do Not Stay Here When
|
||||
|
||||
- the runtime track is clearly Phaser
|
||||
- the runtime track is clearly vanilla Three.js
|
||||
- the runtime track is clearly React Three Fiber
|
||||
- the task is purely about shipped 3D assets
|
||||
|
||||
Once the stack is clear, hand off to the runtime or asset specialist skill.
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
1. Separate simulation from rendering.
|
||||
- Simulation owns entities, turns, timers, collisions, progression, and saveable state.
|
||||
- The renderer owns scene composition, animation playback, camera, particles, and input plumbing.
|
||||
2. Keep input mapping explicit.
|
||||
- Define actions such as `move`, `confirm`, `cancel`, `ability-1`, and `pause`.
|
||||
- Map physical inputs to actions in one place.
|
||||
3. Treat asset loading as a first-class system.
|
||||
- Use stable manifest keys.
|
||||
- Group by domain: characters, environment, UI, audio, FX.
|
||||
- For 3D content, standardize on GLB or glTF 2.0 unless the chosen engine ecosystem requires another format upstream.
|
||||
4. Define save/debug/perf boundaries up front.
|
||||
- Save serializable simulation state, not renderer objects.
|
||||
- Keep debug overlays and perf probes easy to toggle.
|
||||
5. Use DOM overlays for menus and HUD by default.
|
||||
- Canvas or WebGL should handle the playfield.
|
||||
- DOM should handle text-heavy HUD, menus, settings, and accessibility-sensitive controls.
|
||||
- In 3D, keep the persistent UI budget small so the scene stays readable and interactive.
|
||||
6. Lock 3D runtime conventions early.
|
||||
- Choose consistent units, origins, pivots, and naming conventions.
|
||||
- Decide how collision proxies, LODs, and baked lighting data are authored before runtime integration starts.
|
||||
|
||||
## Engine Selection
|
||||
|
||||
- Default to Phaser for 2D games with sprites, tilemaps, top-down or side-view action, turn-based grids, and classic browser arcade flows.
|
||||
- Default to vanilla Three.js for explicit 3D scenes that want direct scene, camera, renderer, and loop control in plain TypeScript or Vite.
|
||||
- Default to React Three Fiber when the 3D scene lives inside a React application and needs declarative composition, shared app state, or React-first UI coordination.
|
||||
- Use raw WebGL only for shader-heavy or renderer-first projects where engine abstractions would get in the way.
|
||||
- Keep Babylon.js and PlayCanvas as alternative-engine paths rather than the default code-generation target.
|
||||
|
||||
See `../../references/engine-selection.md` for the default decision table.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
Define these before writing core code:
|
||||
|
||||
- Player fantasy and primary verbs
|
||||
- Core loop and loss or reset states
|
||||
- Camera model
|
||||
- Input action map
|
||||
- Simulation modules
|
||||
- Renderer modules
|
||||
- Asset manifest layout
|
||||
- 3D asset format and optimization rules
|
||||
- HUD and menu surfaces
|
||||
- Save data boundary
|
||||
- Debug and perf surfaces
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Mixing gameplay rules directly into scene callbacks
|
||||
- Treating the renderer as the source of truth for game state
|
||||
- Putting all HUD and menu UI into the canvas by default
|
||||
- Letting asset filenames become the public API instead of manifest keys
|
||||
- Shipping unoptimized 3D assets straight from the DCC tool into the browser
|
||||
- Mixing camera-control state and menu or modal state without an explicit input boundary
|
||||
- Rebuilding architecture every time the game changes genre
|
||||
|
||||
## References
|
||||
|
||||
- Engine selection: `../../references/engine-selection.md`
|
||||
- Phaser structure: `../../references/phaser-architecture.md`
|
||||
- Three.js structure: `../../references/three-webgl-architecture.md`
|
||||
- Three.js ecosystem stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- 3D asset shipping: `../../references/web-3d-asset-pipeline.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Web Game Foundations"
|
||||
short_description: "Set browser-game architecture before implementation"
|
||||
default_prompt: "Establish the core architecture for this browser game before implementation starts."
|
||||
@@ -16,6 +16,64 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-10 儿童动作 Demo 视觉资产统一为绘本草地舞台
|
||||
|
||||
- 背景:儿童动作 Demo 需要从暗色科技风切换到更适合儿童互动的卡通绘本草地风格,并且要预留 image-2 真实背景图的固定接入位。
|
||||
- 决策:热身舞台统一采用绘本草地视觉语言,真实背景图默认输出到 `public/child-motion-demo/picture-book-grass-stage.webp`,生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 调用 VectorEngine `gpt-image-2-all`。在缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时,只允许 dry-run 和 CSS 兜底,不伪造 live 生图结果。
|
||||
- 影响范围:`src/index.css`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 的舞台视觉层、儿童动作 Demo 技术文档、后续 image-2 资产生成流程。
|
||||
- 验证方式:检查 `/child-motion-demo` 舞台是否在未生成资产时仍有可用草地绘本兜底;补齐 VectorEngine 私密配置后运行 `npm run assets:child-motion-demo -- --live` 应能写出默认背景文件。
|
||||
- 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。
|
||||
|
||||
## 2026-05-10 运行态输入设备抽象层全项目通用化
|
||||
|
||||
- 背景:拼图运行态接入 mocap 后,鼠标/触控和 mocap 各自维护输入逻辑会导致合并大块、拖拽语义和取消会话行为不一致;后续其他玩法也需要复用体感、摇杆、键盘等设备输入。
|
||||
- 决策:前端运行态输入统一通过 `src/services/input-devices/` 承接,设备适配层只输出 `press / move / release / tap / drop` 等通用语义和通用坐标;玩法组件自己解释目标对象、落点和业务动作,输入层不得写拼图等玩法专用规则。
|
||||
- 影响范围:拼图运行态鼠标/触控/mocap 输入、后续运行态设备接入、运行态输入技术文档与相关前端回归测试。
|
||||
- 验证方式:执行 `npm run test -- src\services\input-devices\runtimeDragInputController.test.ts`、`npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。
|
||||
- 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`、`docs/technical/PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md`。
|
||||
|
||||
## 2026-05-11 前端调试模式统一判断
|
||||
|
||||
- 背景:拼图 mocap 调试面板此前在运行态常驻展示,生产构建和正式体验里容易遮挡棋盘内容;后续其它局部诊断 UI 也需要统一的调试模式入口。
|
||||
- 决策:前端新增 `src/config/debugMode.ts` 作为全局调试模式判断,默认跟随 Vite 开发态,允许 `VITE_DEBUG_MODE=true/false` 显式覆盖。拼图运行态 mocap 调试面板只在调试模式下渲染,并默认折叠,只保留连接状态行。
|
||||
- 影响范围:前端局部调试 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`。
|
||||
|
||||
## 2026-05-10 儿童动作热身关直接消费 mocap 数据源
|
||||
|
||||
- 背景:儿童动作 Demo 不能只依赖浏览器摄像头状态和键鼠调试输入,否则真实硬件接入后会出现“mocap 在线但页面提示摄像头不可用”或“能看到画面但动作不推进”的卡点。
|
||||
- 决策:热身关全流程直接接入 `useMocapInput`,通过本地 mocap WebSocket `/stream` 消费 `general.body.center_norm` 身体中心、`actions/action/gesture/gestures/event/name/type` 动作名,以及 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 手部坐标;位置步骤由身体中心推进,`wave_greeting`、`wave_left_hand`、`wave_right_hand` 和 `jump_once` 由 mocap 手势/轨迹推进。浏览器摄像头只作为背景层,动作数据源状态优先展示,键鼠仍作为本地调试兜底。
|
||||
- 影响范围:`src/services/useMocapInput.ts`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx`、对应单测与热身关技术文档。
|
||||
- 验证方式:执行 `npx vitest run src/services/useMocapInput.test.ts src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/components/child-motion-demo/childMotionWarmupModel.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts`、`npx eslint ...`、`npm run typecheck`、`npm run check:encoding`,并确认 `http://127.0.0.1:8876/stream` WebSocket 可握手、`http://127.0.0.1:3000/child-motion-demo` 可访问。
|
||||
- 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## 2026-05-09 GPT-image-2 图片生成统一迁移到 VectorEngine
|
||||
|
||||
- 背景:仓库内 RPG、拼图、方洞和本地模板脚本的 GPT-image-2 生图此前依赖 APIMart 图片网关;团队要求参考 VectorEngine Apifox `api-448710071`,后续不再使用 APIMart 执行 GPT-image-2 图片生成。
|
||||
- 决策:所有 GPT-image-2 生图请求统一走 VectorEngine `POST /v1/images/generations`,基础配置读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,上游模型使用 `gpt-image-2-all`,请求体不再携带 `official_fallback`,参考图字段改为 `image`。APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 影响范围:`api-server` 共享图片 helper、拼图图片生成、角色主图、RPG 场景图、开局 CG 故事板、方洞视觉资产、生产环境示例、gpt-image-2 本地 skill 和相关技术文档。
|
||||
- 验证方式:执行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`,并用 `npm run api-server` + `/healthz` 做后端 smoke。
|
||||
- 关联文档:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 Hyper3D Rodin Gen-2 只通过后端安全代理接入
|
||||
|
||||
- 背景:需要接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型,但供应商 API Key 不能进入前端、文档或 Git;本次只是外部副作用代理,不需要新增平台真相表。
|
||||
- 决策:Hyper3D 统一走 `api-server` 的 `/api/assets/hyper3d/*` 鉴权路由,配置只读取 `HYPER3D_BASE_URL` / `HYPER3D_API_KEY` / `HYPER3D_MODEL_REQUEST_TIMEOUT_MS` 及兼容 `RODIN_*` 变量;生成提交、状态查询和下载列表都由后端代理。首版不写 SpacetimeDB、不确认 `asset_object`,下载链接后续由调用方决定是否进入 OSS 资产链。
|
||||
- 影响范围:`api-server` 外部服务配置、Hyper3D route、`shared-contracts` / TS contract、前端 service、生产环境示例和外部服务环境变量文档。
|
||||
- 验证方式:执行 `cargo test -p api-server hyper3d --manifest-path server-rs/Cargo.toml`、`cargo test -p shared-contracts hyper3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck` 和编码检查;真实 API smoke 只在本地私密环境配置 key 后手动执行。
|
||||
- 关联文档:`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 APIMart 接口统一携带 `official_fallback`
|
||||
|
||||
> 2026-05-09 追认:本决策中的图片生成部分已被“GPT-image-2 图片生成统一迁移到 VectorEngine”覆盖;当前只保留 APIMart `gpt-5` Responses 文本/多模态链路继续显式携带 `official_fallback`。
|
||||
|
||||
- 背景:APIMart 的图片生成和 Responses 接口在仓库内分散于 `api-server`、`platform-llm` 和本地 skill 脚本,若只修单点,容易出现不同入口的上游请求体不一致。
|
||||
- 决策:凡是仓库内调用 APIMart 的 OpenAI 兼容接口,请求体统一携带 `official_fallback: true`;其中图片生成请求直接固定写入,`platform-llm` 的 APIMart GPT-5 client 通过显式开关开启,不默认扩散到 Ark 等其它 provider。
|
||||
- 影响范围:`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/puzzle.rs`、`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/platform-llm/src/lib.rs`、`.codex/skills/gpt-image-2-apimart/` 和相关技术文档。
|
||||
- 验证方式:图片生成与 creative-agent APIMart 路径的单测都应断言 `official_fallback` 已写入请求 JSON;编码检查和相关 Rust 测试应持续通过。
|
||||
- 关联文档:`docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md`、`docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md`、`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`。
|
||||
|
||||
## 2026-05-07 server-rs Cargo 依赖集中到 workspace
|
||||
|
||||
- 背景:`server-rs` 多 crate 已稳定成 DDD workspace,成员 `Cargo.toml` 中重复散写第三方版本和本地 path 依赖,升级 SpacetimeDB SDK、`serde`、`reqwest`、`tokio` 等依赖时容易漂移。
|
||||
@@ -24,6 +82,14 @@
|
||||
- 验证方式:修改 Cargo 配置后先执行 `cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps`,再按影响范围执行 `cargo check`、DDD 边界检查和编码检查。
|
||||
- 关联文档:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 资料页反馈提交必须走 Rust 后端与 SpacetimeDB
|
||||
|
||||
- 背景:`/profile/feedback` 首版页面曾只做前端成功态,无法沉淀到用户账号和数据库,也容易与主站平台主题脱节。
|
||||
- 决策:反馈提交统一走鉴权 HTTP 路由 `POST /api/profile/feedback`,由 `api-server` 取当前 access token 用户,调用 `spacetime-client` facade,再通过 `spacetime-module` procedure 写入私有表 `profile_feedback_submission`;前端只负责输入采集、Data URL 预览和提交元数据,不再保存 `File[]` 作为外部契约。
|
||||
- 影响范围:`src/components/platform-entry/PlatformFeedbackView.tsx`、`src/services/rpg-entry/rpgProfileClient.ts`、`packages/shared/src/contracts/runtime.ts`、`server-rs/crates/shared-contracts`、`api-server`、`module-runtime`、`spacetime-client`、`spacetime-module`、表目录与 bindings。
|
||||
- 验证方式:前端定向测试应覆盖 Data URL 预览与 `/api/profile/feedback` 请求体;后端变更需同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和生成绑定;API smoke 使用 `npm run api-server` 和 `/healthz`。
|
||||
- 关联文档:`docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`、`docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md`。
|
||||
|
||||
## 2026-05-06 Maincloud 历史残留引用禁止再使用
|
||||
|
||||
- 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
||||
@@ -40,6 +106,142 @@
|
||||
- 验证方式:未登录首次访问应展示新手引导;生成后只进入 1 关本地拼图;通关后登录保存应在当前用户拼图作品架出现草稿作品;不应产生 SpacetimeDB schema 变更。
|
||||
- 关联文档:`docs/prd/FIRST_LAUNCH_PUZZLE_ONBOARDING_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 text-game 作为百梦幕间文字游戏模板接入
|
||||
|
||||
- 背景:团队希望参考 MOKU / 幕间类 AI 文游,设计可在百梦内落地的 AI 文字游戏模板,但不能把外部平台社区、支付、榜单、论坛、账号或私有存档迁入 Genarrative。
|
||||
- 决策:新增 `text-game` 作为百梦 AI 原生文字游戏模板口径,展示名可用“幕间”或“幕间文字”;它与 `visual-novel` 分离,重点是 AI GM、自由行动、状态后果、长期记忆、章节目标和轻量剧本模拟器;入口、作品、发布、资产、钱包、埋点、存档和广场全部复用百梦平台接口;禁止新增 replay、外部社区、外部支付、外部榜单和私有存档系统。
|
||||
- 影响范围:后续 `text-game` shared contracts、`module-text-game`、SpacetimeDB 表、`api-server` 路由、前端入口 / workspace / result / runtime、平台作品架和发现聚合。
|
||||
- 验证方式:后续落地时确认路由使用 `/api/creation/text-game/*` 与 `/api/runtime/text-game/*`;确认正式业务真相在 Rust / SpacetimeDB 后端;确认没有 `replay` 能力和外部平台功能误入;确认 `text-game` 不复用 `visual-novel` step 契约作为运行态真相。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 2048 玩法模板采用 `twenty-forty-eight` 工程域
|
||||
|
||||
- 背景:平台计划新增 2048 游戏玩法模板,需要同时适配前端 stage、HTTP 路由、Rust 模块、SpacetimeDB 表和公开作品号;裸 `2048` 不适合作为模块或文件命名前缀。
|
||||
- 决策:面向用户展示名保持 `2048`,工程玩法 ID 固定为 `twenty-forty-eight`,Rust 模块与表前缀使用 `twenty_forty_eight`,公开作品号前缀使用 `TF-`;玩法按完整闭环设计,包含 Agent 创作、结果页、试玩、发布、公开运行、后端棋盘裁决、排行榜和作品架 / 广场接入。
|
||||
- 影响范围:后续 `src/config/newWorkEntryConfig.ts`、平台 `SelectionStage`、前端 `twenty-forty-eight-*` 组件与 service、`module-twenty-forty-eight`、`shared-contracts`、`spacetime-module` 表、`spacetime-client` facade、`api-server` 路由、作品号和 PRD 索引。
|
||||
- 验证方式:后续落地时确认用户可见标题为 `2048`,代码、路由和表统一使用 `twenty-forty-eight` / `twenty_forty_eight`;移动、合并、生成新方块、目标达成、失败和榜单成绩由后端正式裁决,前端不伪造分数或目标达成。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_2048_GAMEPLAY_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 幸存者类玩法作为平台模板接入
|
||||
|
||||
- 背景:平台继续扩展新玩法模板,需要把幸存者 / 割草 / 轻度 Roguelite 类玩法纳入统一创作中心、作品架、广场和运行态体系,避免再起一套独立小游戏工程。
|
||||
- 决策:新增 `survivor` 作为 Genarrative 平台玩法模板,统一使用 `server-rs + Axum + SpacetimeDB`,创作端、结果页、试玩、发布和运行态都复用平台接口;前端只负责表现和高频模拟,不承接正式规则真相。
|
||||
- 影响范围:`docs/prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md`、后续 `survivor` shared contracts、前端入口 / result / runtime、`server-rs` DDD 分层、SpacetimeDB 表设计和平台作品闭环。
|
||||
- 验证方式:后续落地时检查 `survivor` 入口、session、work profile、runtime run、checkpoint、升级候选和结算接口是否都落在平台统一链路内,并确认没有新增独立小游戏壳层。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 TXT 玩法只作为平台模板接入且删除回放
|
||||
|
||||
- 背景:`Interactive-fiction-backend` 与 `Interactive-fiction-frontend` 是完整平台类工程,其中 TXT / Galgame 玩法可借鉴,但账号、商城、后台、公开市场、回放等平台能力不能迁入 Genarrative。
|
||||
- 决策:`visual-novel` 只作为 Genarrative 视觉小说模板接入,保留想法 / 文档 / 空白创建、世界观 / 角色 / 场景 / 剧情阶段编辑、视觉小说 step 运行时、历史和重生成等模板能力;入口、作品、发布、资产、钱包、存档和广场全部使用 Genarrative 平台接口;彻底删除回放、分享回放、回放编译、回放路由、回放表和回放 UI。
|
||||
- 影响范围:视觉小说 PRD、旧 TXT 文档口径、后续 `visual-novel` shared contracts、前端入口 / result / runtime、`server-rs` DDD 分层、SpacetimeDB 表设计和平台存档接入。
|
||||
- 验证方式:后续落地时扫描前端、后端、契约、表和文档,确认不存在 `replay` 能力;确认视觉小说没有迁入外部平台账号、订单、会员、促销、后台、公开市场或私有存档系统;确认后端落在 `server-rs + Axum + SpacetimeDB`。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/prd/TXT_MODE_CORE_GAMEPLAY_PRD_2026-04-20.md`、`docs/technical/TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 VN-02 表与 spacetime-client facade 收口
|
||||
|
||||
- 背景:`visual-novel` 后续 API、创作工作台和运行时需要稳定的 SpacetimeDB schema 与 Rust facade,且必须延续“无回放、无私有存档”的产品边界。
|
||||
- 决策:视觉小说首批数据库只落六张表:`visual_novel_agent_session`、`visual_novel_agent_message`、`visual_novel_work_profile`、`visual_novel_runtime_run`、`visual_novel_runtime_history_entry`、`visual_novel_runtime_event`;`visual_novel_runtime_event` 是 `public event` 审计事件表,不是 replay 数据源;运行历史只保存继续体验与历史重生成需要的 typed step 和快照哈希。`api-server` 后续接入必须经 `spacetime-client/src/visual_novel.rs` typed facade,不直接依赖生成 bindings。
|
||||
- 影响范围:`server-rs/crates/spacetime-module/src/visual_novel.rs`、`migration.rs`、`server-rs/crates/spacetime-client/src/visual_novel.rs`、`module_bindings/`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`、VN-05 API 联调。
|
||||
- 验证方式:执行 `npm run spacetime:generate -- --rust-only`、`cargo check -p spacetime-module`、`cargo check -p spacetime-client`、`npm run check:encoding`;扫描视觉小说 schema / facade / 表目录确认没有 `replay` 表、路由或私有 save 表。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 VN-07 前端创作闭环按阶段边界落地
|
||||
|
||||
- 背景:`visual-novel` 模板需要先完成创作工作台与结果页,真实生成和正式玩家 runtime 仍依赖 VN-05 后端路由。
|
||||
- 决策:VN-07 前端只接入口、Agent 工作台、可编辑 `VisualNovelResultDraft` 结果页和测试 run;`blank` 起点直接生成本地空白草稿进入结果页,`idea` / `document` 继续调用 `/api/creation/visual-novel/sessions`;结果页保存先更新当前 session 草稿,显式“编译草稿”才调用 `/compile`,测试 run 在真实 runtime 不可用时降级为本地 test run。
|
||||
- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/visual-novel-creation/`、`src/components/visual-novel-result/`、`packages/shared/src/contracts/visualNovel.ts`、视觉小说 PRD。
|
||||
- 验证方式:执行前端 typecheck、视觉小说工作台 / 结果页定向测试和编码检查;确认未新增 replay、作品聚合或正式 runtime 能力。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-11 负向扫描门禁
|
||||
|
||||
- 背景:视觉小说 TXT 模板进入收口后,需要一个可重复执行的守门方式,避免工程代码误入回放能力或外部平台功能。
|
||||
- 决策:新增 `npm run check:visual-novel-vn11`,由 `scripts/check-visual-novel-vn11-negative-scan.mjs` 扫描 `src/`、`packages/shared/src/`、`server-rs/crates/`、`docs/` 与 `.hermes/shared-memory/`;工程代码中不允许出现 replay / 回放 / 录制 / 复盘类直出命中;外部平台能力误入只在视觉小说实现路径内检查,避免把平台已有账号、会员、后台等能力误判为视觉小说迁入。
|
||||
- 影响范围:视觉小说 VN-11 验收、后续 `visual-novel` 增量改动、同类新玩法负向扫描脚本。
|
||||
- 验证方式:执行 `npm run check:visual-novel-vn11`,报告写入 `docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`;当前扫描结论为工程代码无回放类直出命中,视觉小说实现路径无外部平台能力误入。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-12 采用单独验收门禁脚本
|
||||
|
||||
- 背景:VN-12 是视觉小说模板的全链路联调与自动化验收收口任务,需要把关键路径、API smoke、前端测试和报告输出固化成可复跑门禁,避免后续改动只靠手工口述结论。
|
||||
- 决策:新增 `npm run check:visual-novel-vn12`,由 `scripts/check-visual-novel-vn12-acceptance.mjs` 校验 PRD、VN-11 报告、关键前端测试、视觉小说 service client、`api-server` / `module-visual-novel` / `shared-contracts` 相关文件和路由命中,并生成 `docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md`。
|
||||
- 影响范围:VN-12 验收、视觉小说后续回归、同类玩法的收口门禁模式。
|
||||
- 验证方式:执行 `npm run check:visual-novel-vn12 -- --write-report`,报告应覆盖自动化验收清单、API smoke、前端关键路径、桌面/移动端检查说明和已执行命令;若脚本失败,直接回流到对应 owner 修复。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-13 文档与交接收口
|
||||
|
||||
- 背景:视觉小说模板主链已经落地完成,需要把 PRD、表目录、prompt 工具说明、负向扫描报告和维护经验收成新开发者可直接接手的一组文档,避免后续仍回头查旧 TXT 迁移方案。
|
||||
- 决策:视觉小说后续维护的正式入口固定为 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`SPACETIMEDB_TABLE_CATALOG.md`、`VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`、`VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md` 和 `VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`;旧 TXT 迁移文档仅保留历史参考地位。
|
||||
- 影响范围:视觉小说 PRD 收口、技术文档索引、经验文档索引、Hermes 共享记忆和后续维护阅读顺序。
|
||||
- 验证方式:打开上述文档即可获得当前实现边界、表目录、Prompt 口径、负向扫描和维护经验;后续维护不需要把旧 TXT 平台工程文档重新当作实现目标。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`docs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md`。
|
||||
|
||||
## 2026-05-05 平台移动端一级 Tab 改为推荐/发现/创作/草稿/我的
|
||||
|
||||
- 背景:移动端平台入口需要从旧“首页/排行/创作/存档/我的”调整为更直接的推荐流和应用式底部导航。
|
||||
- 决策:前端内部继续复用 `PlatformHomeTab` 的 `home/category/create/saves/profile` 状态值,但用户看到的一级 Tab 分别为“推荐/发现/创作/草稿/我的”;`home` 直接展示公开推荐流,`category` 承载发现页及排行子 Tab,`saves` 承载草稿作品架,原存档结构并入“我的-玩过”弹层。
|
||||
- 影响范围:平台入口导航、移动端推荐页、发现页子 Tab、创作中心作品架、个人页玩过弹层、相关设计文档。
|
||||
- 验证方式:检查移动端底部导航文案和顺序,确认登录态为“推荐/发现/创作/草稿/我的”,未登录态为“推荐/创作/发现”且创作居中;“推荐”无搜索/频道栏直出作品流,“发现”包含搜索/推荐/今日/分类/排行,“创作”只显示新建入口,“草稿”显示作品架,“我的-玩过”可恢复存档。
|
||||
- 关联文档:`docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 创作 Tab 固定为智能创作首页,草稿 Tab 承接旧作品架
|
||||
|
||||
- 背景:创作首页需要变成面向对话式生成的智能创作页,旧模板卡和作品架继续保留但不应再占据创作首屏。
|
||||
- 决策:`create` 只承载 `CreativeAgentHome` 智能创作首页与会话流,顶部品牌栏、问候、快捷胶囊、底部输入框和左侧抽屉是主结构;旧的新建作品类型卡不再在 `create` 里展示。原本的 RPG / 拼图 / 大鱼 / Match3D / 方洞 / 视觉小说作品架统一归到 `saves` 草稿 Tab。
|
||||
- 影响范围:平台创作页布局、创作首页抽屉、草稿页作品架、相关交互测试、旧创作入口 helper。
|
||||
- 验证方式:移动端点击“创作”直接看到智能创作首页;点击“草稿”看到旧作品架;旧模板入口不再从创作页出现。
|
||||
- 关联文档:`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 创意互动内容生成 Agent 采用 LangChain-Rust 六模块闭环
|
||||
|
||||
- 背景:需要支持用户输入文字、图片或文档后,先理解创作意图,再从多个模板候选中选择一个,并把内容填入拼图等目标玩法草稿契约中。
|
||||
- 决策:新增方案改为基于 LangChain-Rust 的六模块 Agent 架构,核心模块是感知、思考、记忆、行动、反思、协作;首版只支持拼图玩法,但必须先展示多个拼图子模板候选,用户选择某个模板后,再确认该模板下的关卡模式、关卡数和预计积分范围,确认后才生成草稿;Agent 理解、规划和修订统一使用 APIMart Responses `gpt-5` 并支持文本/图像多模态输入;Agent 创作方式就是填充和修订模板草稿字段,表单化创作页与 Agent 自然语言修订都操作同一份 `PuzzleResultDraft`,且草稿可编辑字段只收敛为 `workTitle`、`workDescription`、`workTags`、`levels[].levelName`、`levels[].pictureDescription`、`levels[].pictureReference`;其中 `pictureReference` 已采用 `PuzzleDraftLevel.pictureReference` / Rust `picture_reference` 正式字段方案,不再走 metadata 过渡;单关卡/多关卡图片生成通过拼图模块 Tool 与模板协议实现;生成好的内容必须可立即试玩。
|
||||
- 影响范围:创作中心入口、`platform-agent`、`module-creative-agent`、`module-puzzle` 拼图模板协议和工具、`shared-contracts`、`api-server` creative facade、SpacetimeDB creative agent 表、拼图玩法工具。
|
||||
- 验证方式:后续落地时以创意互动内容生成 Agent 技术方案和 Phase 1 PRD 为编码依据,优先完成拼图 Phase 1,并执行 shared contracts、module、platform-agent、api-server、前端 typecheck 与编码检查。
|
||||
- 关联文档:`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`、`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 creative-agent Task C 首版平台 PoC 已落地
|
||||
|
||||
- 背景:Phase 1 的平台侧需要先把 LangChain-Rust 适配层、APIMart `gpt-5` 多模态 Responses 请求和工具注册边界立起来,才能继续接 API facade。
|
||||
- 决策:新增 `server-rs/crates/platform-agent` 作为独立 workspace crate,保留项目自有 `CreativeAgentExecutor`、工具注册表、回调事件和 mock executor;`platform-llm` 的 Responses 请求体扩展为可序列化 `input_text` / `input_image` content part。
|
||||
- 影响范围:`server-rs/Cargo.toml`、`server-rs/crates/platform-agent`、`server-rs/crates/platform-llm`、任务 C 的后续 API / SSE 接入。
|
||||
- 验证方式:`cargo check -p platform-agent`、`cargo test -p platform-agent`、`cargo test -p platform-llm responses_multimodal` 已通过。
|
||||
- 关联文档:`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 creative-agent Task E API / SSE facade 已落地
|
||||
|
||||
- 背景:Phase 1 需要先把创意 Agent 的 HTTP/SSE 门面接入 Rust `api-server`,用于前端工作区调用和拼图模板确认闭环。
|
||||
- 决策:`api-server` 挂载 `/api/runtime/creative-agent/*` 六个鉴权路由;creative session 在 Task D 表未收口前暂存在 `api-server` 运行态并按 authenticated user 校验 owner;未确认模板前不创建拼图 session,`confirm-template` 后才通过既有 `spacetime-client` 创建/编译 `puzzle_agent_session`;`gpt-5` 请求只从 `APIMART_BASE_URL` / `APIMART_API_KEY` 构造专用 Responses client,不复用通用 `GENARRATIVE_LLM_API_KEY`。
|
||||
- 影响范围:`server-rs/crates/api-server/src/creative_agent.rs`、`creative_agent_sse.rs`、`app.rs`、`state.rs`、`module-puzzle` creative template/tool、Phase 1 PRD。
|
||||
- 验证方式:`cargo check -p api-server`、`cargo test -p module-puzzle creative`、`cargo test -p api-server creative_agent`、`npm run api-server` 后检查 `/healthz`、`POST /api/runtime/creative-agent/sessions`、`POST /api/runtime/creative-agent/sessions/{sessionId}/messages/stream`。
|
||||
- 关联文档:`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-10 视觉小说入口收敛为单句创作 + 画风选择
|
||||
|
||||
- 背景:视觉小说入口页要对齐抓大鹅式的线性创作入口,只保留最小可用输入,避免再暴露文档 / 空白 / 对话式工作台。
|
||||
- 决策:入口页只展示一句话创作输入框和横向视觉画风卡片;画风通过 `seedText` 追加 `视觉画风` 和 `画风要求` 两行透传给既有创作链路;点击生成后先进入 `visual-novel-generating` 过程页,再自动进入 `visual-novel-result`。
|
||||
- 影响范围:`VisualNovelAgentWorkspace`、`PlatformEntryFlowShellImpl`、`platformEntryTypes`、视觉小说 PRD;不新增后端字段或数据库结构。
|
||||
- 验证方式:视觉小说工作台单测通过,`npm run check:encoding` 通过;`npm run typecheck` 仍受仓库里 `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx` 的既有类型错误影响。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-07 移动端整页缩放由入口统一锁定
|
||||
|
||||
- 背景:移动端游戏式页面如果允许浏览器整页缩放,容易把固定画布、HUD 和底部操作区一起放大或缩小,破坏操作节奏。
|
||||
- 决策:主站入口统一使用 `viewport` 锁定 `minimum-scale=1.0`、`maximum-scale=1.0`、`user-scalable=no` 和 `viewport-fit=cover`,并在应用启动时调用 `lockMobileViewportZoom()` 拦截 iOS `gesture*` 与多指 `touchmove` 触发的页面级缩放。
|
||||
- 影响范围:主站 `index.html`、`src/main.tsx`、后续所有依赖主入口的移动端游戏/画布页面;不要求每个画布组件重复实现缩放锁定。
|
||||
- 验证方式:移动端打开主站后,双指捏合和快速双击不应再缩放整页;单指滚动、点击和组件内交互保持正常。
|
||||
- 关联文档:`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-10 资产引用统一走平台资产对象
|
||||
|
||||
- 背景:视觉小说文档输入、封面、场景背景、角色立绘和音乐需要接入平台资产链路,不能在前端状态或 SpacetimeDB 中保存大 Data URL、二进制对象或外部 R2 路径。
|
||||
- 决策:VN 上传统一复用 `/api/assets/direct-upload-tickets`、OSS 直传、`/api/assets/objects/confirm`、`/api/assets/read-url`。文档上传后只把 `assetObjectId` 放入 `sourceAssetIds`,`seedText` 仅放截断摘要;封面、场景、角色、音乐只写 `/generated-*` 引用和平台 asset id。角色立绘写入 `imageAssets[].source = platform_asset`。运行时图片渲染统一使用 `ResolvedAssetImage` 换签。
|
||||
- 影响范围:`src/services/visual-novel-creation/visualNovelAssetClient.ts`、`VisualNovelAgentWorkspace`、`VisualNovelResultView`、`VisualNovelRuntimeShell`、`server-rs/crates/api-server/src/visual_novel.rs`。
|
||||
- 验证方式:VN 定向前端测试、`npm run typecheck`、`npm run check:encoding`、`cargo test -p api-server visual_novel`、`cargo test -p api-server creation_agent_document_input`。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-04 在仓库 `.hermes/` 中建立团队共享记忆
|
||||
|
||||
- 背景:团队有 3 名开发人员,均在各自本地安装 Hermes,并需要独立拉取仓库、修改代码、本地测试;团队希望形成共享的长期项目记忆。
|
||||
|
||||
@@ -91,6 +91,8 @@ npm run spacetime:generate
|
||||
|
||||
## 常用检查命令
|
||||
|
||||
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。
|
||||
|
||||
编码检查:
|
||||
|
||||
```bash
|
||||
@@ -183,6 +185,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- 移动端优先,再兼容网页端。
|
||||
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
|
||||
- 创作中心入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;前端只在 `platformEntryCreationTypes.ts` 做展示派生,api-server 路由熔断也使用同一份配置,禁止恢复前端硬编码入口配置文件。
|
||||
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
|
||||
- 不在 UI 中默认写功能说明类文本。
|
||||
- 弹出独立面板的交互不要实现成在当前面板下方追加内容。
|
||||
|
||||
@@ -43,6 +43,54 @@
|
||||
- 验证:提交前检查 `git diff -- .hermes`,确认没有密钥、会话记录或个人路径敏感信息。
|
||||
- 关联:`.hermes/README.md`。
|
||||
|
||||
## 儿童动作 Demo 卡在摄像头不可用或挥手不推进先查 mocap 消费链路
|
||||
|
||||
- 现象:`/child-motion-demo` 打开后即使 `http://127.0.0.1:8876/` 已启动,页面仍提示“摄像头暂不可用”,或到“打个招呼”、左右手挥动、站位步骤时真实硬件动作无法检测通过,只能用鼠标拖拽或键盘调试继续。
|
||||
- 原因:浏览器摄像头视频流只是舞台背景;如果热身关把 `getUserMedia` 状态当成主动作数据源,或只在 gesture 阶段消费 `useMocapInput`,就会错过 mocap 的身体中心、动作名和手部坐标。
|
||||
- 处理:确认 `src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 全热身流程启用 `useMocapInput`,页面主提示展示 mocap 动作数据源状态而不是浏览器摄像头状态;确认 `src/services/useMocapInput.ts` 能解析 `/stream` 包里的 `general.body.center_norm`、`actions/action/gesture/gestures/event/name/type`、`hands[]`、`leftHand/rightHand`、`left_hand/right_hand`、左右手标记和 `open_palm/grab` 状态。`/stream` 是 WebSocket,普通 HTTP 访问返回 404 不能当成服务不可用。
|
||||
- 验证:运行 `npx vitest run src\services\useMocapInput.test.ts src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx`,并在本地硬件服务启动后进入 `/child-motion-demo` 实测站位、招手、左右手挥动和跳跃阶段。
|
||||
- 关联:`src/services/useMocapInput.ts`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## 儿童动作 Demo 真实绘本背景图未生成先查 VectorEngine 配置
|
||||
|
||||
- 现象:`/child-motion-demo` 已经呈现绘本草地风格,但 `public/child-motion-demo/picture-book-grass-stage.webp` 不存在,Network 里该图返回 404,或运行 `npm run assets:child-motion-demo -- --live` 返回缺少 VectorEngine 配置。
|
||||
- 原因:儿童动作 Demo 的真实背景图使用 VectorEngine `gpt-image-2-all` 生成,脚本只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和可选 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。
|
||||
- 处理:在本地私密环境补齐 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai` 与 `VECTOR_ENGINE_API_KEY`,不要把 key 写入 Git;先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt,再运行 `npm run assets:child-motion-demo -- --live` 生成默认背景图。
|
||||
- 验证:生成后确认 `public/child-motion-demo/picture-book-grass-stage.webp` 存在,重新打开 `/child-motion-demo` 可看到真实绘本草地背景;`npm run check:encoding` 仍通过。
|
||||
- 关联:`scripts/generate-child-motion-demo-assets.mjs`、`src/index.css`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## GPT-image-2 不再读 APIMart 图片配置
|
||||
|
||||
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。
|
||||
- 原因:2026-05-09 后 GPT-image-2 图片生成已切到 VectorEngine `gpt-image-2-all`,APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认路径为 `/v1/images/generations`、模型为 `gpt-image-2-all`、参考图字段为 `image`。
|
||||
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
|
||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。
|
||||
|
||||
## 拼图参考图没有影响生成时先查 action payload 和阶段日志
|
||||
|
||||
- 现象:拼图上传参考图后生成出的画面明显不像参考图,或结果页重新生成没有按保存的参考图走图生图。
|
||||
- 原因:首图生成只通过 `compile_puzzle_draft.referenceImageSrc` 临时传 Data URL,不持久化到 SpacetimeDB;结果页重新生成则要把当前上传图或关卡 `pictureReference` 作为 `generate_puzzle_images.referenceImageSrc` 继续传给后端。
|
||||
- 处理:浏览器 Network 里确认 action payload 带 `referenceImageSrc`;api-server 日志按同一 `session_id` 查看 `拼图参考图解析完成`、`拼图 VectorEngine 图片生成 HTTP 返回`、`拼图 VectorEngine 图片下载完成`、`拼图生成图片已写入 OSS 与资产索引`,可定位慢在参考图读取、VectorEngine、下载或 OSS。
|
||||
- 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的 `pictureReference` 重新生成;后端单测覆盖 VectorEngine 请求体 `image` 字段。
|
||||
- 关联:`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle.rs`。
|
||||
|
||||
## 拼图首图生成后要把入口参考图写回 `pictureReference`
|
||||
|
||||
- 现象:入口页上传图后,首图看着像没吃到参考图;结果页重新生成时默认只沿用关卡旧图,没有继续带入口上传图。
|
||||
- 原因:首图生成请求虽然已经把 `referenceImageSrc` 传给 VectorEngine,但如果后端只更新 `cover_image_src` / `selected_candidate_id` 而不回写首关 `pictureReference`,结果页后续重绘就会丢失参考图。
|
||||
- 处理:在 `compile_puzzle_draft` 和 `generate_puzzle_images` 的成功与 SpacetimeDB 降级快照路径里,都把本次入口参考图写入首关 `pictureReference`。
|
||||
- 验证:后端单测覆盖 `build_puzzle_levels_with_primary_update` 和 `apply_generated_puzzle_candidates_to_session_snapshot`;结果页重新生成应在未重新上传时继续带入 `level.pictureReference`。
|
||||
- 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/components/puzzle-result/PuzzleResultView.tsx`。
|
||||
|
||||
## 拼图图生图仍不像参考图时先看是否走了 edits
|
||||
|
||||
- 现象:Network payload 已带 `referenceImageSrc`,但 VectorEngine 生成结果仍明显不像上传图。
|
||||
- 原因:`gpt-image-2-all` 的 `/v1/images/generations` 更适合纯文生图;有参考图且需要重绘时应切到 `/v1/images/edits` 的 multipart 图生图接口。
|
||||
- 处理:`referenceImageSrc` 存在且 `aiRedraw = true` 时直接走 edits,prompt 仍保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。
|
||||
- 验证:后端单测应覆盖 `images/edits` 路由、`b64_json` 响应解码和参考图强提示;真实联调先看日志里是否命中 `拼图 VectorEngine 图片编辑 HTTP 返回`。
|
||||
- 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/services/puzzleReferenceImage.ts`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。
|
||||
|
||||
## 旧后端路线文档造成判断漂移
|
||||
|
||||
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
|
||||
@@ -59,14 +107,54 @@
|
||||
- 验证:发布前完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
|
||||
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
|
||||
|
||||
- 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`。
|
||||
- 原因:SpacetimeDB module 的 wasm32 构建树被间接带入原生/网页依赖;已验证链路是 `reqwest -> platform-oss -> shared-contracts -> module-runtime -> spacetime-module`,由共享契约默认启用资产 OSS 契约触发。
|
||||
- 处理:让 `shared-contracts` 的 OSS 资产契约走 `oss-contracts` feature,workspace 根依赖保持 `default-features = false`;`api-server` 这类原生后端需要资产 DTO 时在自身 `Cargo.toml` 显式启用 `features = ["oss-contracts"]`。
|
||||
- 验证:执行 `cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown` 应显示 nothing to print;再执行 `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown`。
|
||||
- 关联:`server-rs/crates/shared-contracts/Cargo.toml`、`server-rs/crates/api-server/Cargo.toml`、`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`。
|
||||
|
||||
## 本地 SpacetimeDB replica identity 不匹配
|
||||
|
||||
- 现象:本地 standalone 启动时报 `mismatched database identity`。
|
||||
- 原因:root-dir / replica 数据残留与当前数据库身份不一致。
|
||||
- 原因:本地 SpacetimeDB 数据目录中的 replica 数据残留与当前数据库身份不一致。
|
||||
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
|
||||
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
|
||||
- 关联:`docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md`。
|
||||
|
||||
## 本地 SpacetimeDB publish 403 优先查 CLI 身份和目标库
|
||||
|
||||
- 现象:`spacetime publish` 在 `Pre-publish check` 阶段返回 `403 Forbidden`,提示当前 identity 无权对目标 database identity 执行 `update database`。
|
||||
- 原因:当前 CLI 登录态不是目标数据库的创建者或授权身份,或 `.env.local` / publish 命令指向了另一个数据库或 SpacetimeDB 服务。
|
||||
- 处理:除 CI/CD 脚本内部受控用法外,不再使用 `spacetime --root-dir` 排障或发布。先执行 `spacetime login show`、`spacetime server list`,再用 `spacetime list --server http://127.0.0.1:3101` 或实际 `--server-url` 确认当前身份是否能看到目标库;本地开发发布优先使用 `npm run dev:rust` 或从 `server-rs` 目录执行显式 `--server` 的 `spacetime publish`。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。
|
||||
- 验证:`spacetime list --server http://127.0.0.1:3101` 能看到目标库;重新发布不再使用无权限 identity。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`。
|
||||
|
||||
## `npm run dev` 本地 SpacetimeDB 401 / 403 可重置默认 local 身份
|
||||
|
||||
- 现象:`npm run dev` 启动本地开发栈时,SpacetimeDB 在登录、发布或预检查阶段返回 `401` / `403`,清理后仍像在使用旧 token 或旧本地库。
|
||||
- 原因:本机 `spacetime` CLI 保存的旧 token、默认 server、正在运行的 standalone 进程或默认 local 数据库与当前发布身份不一致。
|
||||
- 处理:确认只是本地测试库且数据可丢弃后,先查看并停止本地 `spacetimedb-standalone`,执行 `spacetime logout`,确认并设置 `spacetime server set-default local`,停 server 后用 `spacetime server clear -y` 清空默认本地库,再 `spacetime start`,另开终端执行 `spacetime login --server-issued-login local`,最后用 `spacetime publish --server local A` 或项目脚本重新发布。
|
||||
- 验证:`spacetime server list` 默认目标为 local;重新登录后发布不再返回 `401` / `403`;`npm run dev` 可以完成 SpacetimeDB publish 并继续启动 `api-server`。
|
||||
- 关联:`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`、`scripts/dev-rust-stack.sh`。
|
||||
|
||||
## 本地 SpacetimeDB 联调可按阶段跳过宿主或发布
|
||||
|
||||
- 现象:本地 `npm run dev` 因 `3101` 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 `spacetime-module` 语法而被完整联调链路拖慢。
|
||||
- 原因:`npm run dev` 默认同时启动 SpacetimeDB standalone、发布 `server-rs/crates/spacetime-module`、启动 Rust `api-server`、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。
|
||||
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 `3101`,被占用则输出占用进程并选择最近可用端口,保证 publish 与 `api-server` 都连接同一个实际 SpacetimeDB URL。`api-server` 启动前也会检测 `8082` 并选择最近可用端口。Windows / Git Bash 下不要用 `tr/head/xargs` 管道读取 `spacetime.pid` 或 URL 记录,脚本应使用 Node 读取并短重试,避免 `tr: read error: Device or resource busy`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。`npm run dev` 会在启动前检查 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,不可用时自动寻找后续可用端口,并把实际端口传给 publish、后端环境变量和前端代理目标。
|
||||
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101`;`3101` 或 `8082` 被其他进程占用时,脚本输出占用进程并使用最近可用端口;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。端口漂移时控制台会打印 `[dev:ports] ... 不可用,改用 ...`,后续 `[dev:rust] web/admin web/rust api/spacetime` 地址应与实际端口一致。
|
||||
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`、`scripts/dev-rust-stack.sh`。
|
||||
|
||||
## 本地 SpacetimeDB publish 401 可清本地库重发
|
||||
|
||||
- 现象:本地 `spacetime publish` 显示 `401` 无权限,或重新发布仍像是在更新旧库。
|
||||
- 原因:本地开发数据目录中保留的数据库、控制库身份或发布身份与当前目标不一致。
|
||||
- 处理:确认本地开发数据可以丢弃后,停止本地 SpacetimeDB,备份或删除 `server-rs/.spacetimedb/local/data`,再重新运行 `npm run dev` 或本地 publish;不要用 `--root-dir` 手工清库。
|
||||
- 验证:重新发布日志应显示创建新的数据库,而不是更新旧数据库;若仍显示更新或继续 `401`,继续检查数据目录、库名和 CLI 身份。
|
||||
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`、`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`。
|
||||
|
||||
## Vite SPA fallback 吞掉 API 请求
|
||||
|
||||
- 现象:本地请求 `/api/profile/*` 等接口时返回 HTML,被前端当 JSON 解析报错。
|
||||
@@ -75,13 +163,116 @@
|
||||
- 验证:请求返回 JSON,相关页面不再出现 HTML parse 错误。
|
||||
- 关联:`docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md`。
|
||||
|
||||
## 拼图 APIMart 图片生成密钥不能复用 DashScope / ARK key
|
||||
## 反馈页清空 file input 前必须先拷贝 FileList
|
||||
|
||||
- 现象:拼图新手引导或拼图创作点击生成后返回 `APIMart 图片生成密钥未配置`。
|
||||
- 原因:拼图 `gpt-image-2` / `nanobanana2` 图片生成已按技术方案统一走 APIMart;后端只读取 `APIMART_BASE_URL`、`APIMART_API_KEY`、`APIMART_IMAGE_REQUEST_TIMEOUT_MS`,不会用 `DASHSCOPE_API_KEY`、`LLM_API_KEY` 或 `ARK_API_KEY` 兜底。
|
||||
- 处理:在本机私密配置 `.env.secrets.local` 或进程环境中配置真实 `APIMART_API_KEY`,不要提交到 Git;填入后必须重启 `api-server` / `npm run dev`,运行中的进程不会自动加载新 env。
|
||||
- 验证:不打印密钥内容,只检查 `APIMART_API_KEY` 非空;重启后触发拼图生成不再返回本地配置缺失的 503。
|
||||
- 关联:`docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md`、`.codex/skills/gpt-image-2-apimart/SKILL.md`。
|
||||
- 现象:点击上传凭证会打开文件选择框,但选择图片后页面没有展示预览,提交时也没有携带图片凭证。
|
||||
- 原因:浏览器传入的 `FileList` 可能跟 `<input type="file">` 保持 live 绑定;如果先执行 `input.value = ''`,再从参数里的 `FileList` 读取文件,列表可能已经为空。
|
||||
- 处理:在清空 file input 前先执行 `const selectedFiles = files ? Array.from(files) : []`,后续图片类型、大小、Data URL 读取和预览都基于这个普通数组。
|
||||
- 验证:`PlatformFeedbackView.test.tsx` 用 mock `FileReader` 断言选择图片后出现 `反馈凭证预览`,且提交 payload 带 `evidenceItems[].dataUrl`。
|
||||
- 关联:`src/components/platform-entry/PlatformFeedbackView.tsx`、`docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md`。
|
||||
|
||||
## 拼图 VectorEngine 图片生成密钥不能复用 DashScope / ARK key
|
||||
|
||||
- 现象:拼图新手引导或拼图创作点击生成后返回 `VectorEngine 图片生成密钥未配置`。
|
||||
- 原因:拼图 `gpt-image-2` / 历史 `nanobanana2` 图片生成已统一走 VectorEngine;后端只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,不会用 `DASHSCOPE_API_KEY`、`LLM_API_KEY`、`ARK_API_KEY` 或 `APIMART_API_KEY` 兜底。
|
||||
- 处理:在本机私密配置 `.env.secrets.local` 或进程环境中配置真实 `VECTOR_ENGINE_API_KEY`,不要提交到 Git;填入后必须重启 `api-server` / `npm run dev`,运行中的进程不会自动加载新 env。
|
||||
- 验证:不打印密钥内容,只检查 `VECTOR_ENGINE_API_KEY` 非空;重启后触发拼图生成不再返回本地配置缺失的 503。
|
||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`.codex/skills/gpt-image-2-apimart/SKILL.md`。
|
||||
|
||||
## `npm run api-server` 读取 env 的顺序必须让 `.env.secrets.local` 最后覆盖
|
||||
|
||||
- 现象:`POST /api/assets/hyper3d/text-to-model` 在本地返回 503,详情里提示 `HYPER3D_API_KEY 未配置`,但开发者明明已经在本地私密文件里写了 key。
|
||||
- 原因:`scripts/api-server-dev.mjs` 之前按 `.env.secrets.local → .env.local → .env` 合并,结果仓库里的 `.env` 空示例值会把前面已经设置好的私密 key 覆盖掉。
|
||||
- 处理:`npm run api-server` / `npm run dev:rust` / `npm run dev` 统一按“外层 shell 变量优先,其后 `.env`、`.env.local`、`.env.secrets.local` 逐层覆盖”的顺序加载;真实密钥优先放 `.env.secrets.local`。
|
||||
- 验证:本地加入临时测试后,`HYPER3D_API_KEY` 应能被 `.env.secrets.local` 覆盖,且 shell 变量仍然最高优先级。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/hyper3d_generation.rs`、`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`。
|
||||
|
||||
## 拼图图片生成 98% 后报 OSS V4 签名时间格式化失败
|
||||
|
||||
- 现象:拼图创作表单生成进度卡在 98%,`POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions` 返回 `502 Bad Gateway`,前端提示 `拼图图片生成失败:OSS V4 签名时间格式化失败`。
|
||||
- 原因:`platform-oss` 曾用 `OffsetDateTime::time().to_string()` 拼接 `x-oss-date`,UTC 小时、分钟或秒为个位数时可能缺少前导零,导致 V4 签名时间不是固定 `YYYYMMDDTHHMMSSZ`。
|
||||
- 处理:OSS V4 签名日期统一显式补零格式化;签名 scope 用 `YYYYMMDD`,完整签名时间用 `YYYYMMDDTHHMMSSZ`,不要再依赖 `time().to_string()`。
|
||||
- 验证:运行 `cargo test -p platform-oss` 和 `cargo check -p api-server`;重启 `npm run api-server` 后检查 `/healthz`,再重新触发拼图生成。
|
||||
- 关联:`server-rs/crates/platform-oss/src/lib.rs`、`server-rs/crates/api-server/src/assets.rs`、`docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md`。
|
||||
|
||||
## 拼图生成完成后图片只显示破图或 alt 文案
|
||||
|
||||
- 现象:拼图结果页生成完成后,“画面图”区域出现破图图标和作品名,图片无法正常预览;但打开历史拼图素材时同一张图可能可以正常预览。
|
||||
- 原因:拼图正式图保存为 `/generated-puzzle-assets/*` 兼容标识,旧 `/generated-*` 直读代理已删除;如果前端没有通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签,或收到无前导斜杠的 `generated-puzzle-assets/*` object key 后未识别为 generated 私有资源,浏览器会直接请求裸路径并失败。生成完成后的结果图还会传入 `refreshKey`,它只能用于重新请求 `/api/assets/read-url`,不能给 OSS V4 签名 URL 追加 `_v`;OSS 会把 query 纳入签名,额外参数会让签名失效,历史素材常因未传 `refreshKey` 而表现正常。
|
||||
- 处理:拼图结果页、发布预览、运行态和历史素材预览都走 `ResolvedAssetImage` 或 `useResolvedAssetReadUrl`;`isGeneratedLegacyPath(...)` 必须同时识别 `/generated-*` 和 `generated-*`;`refreshKey` 只绕过前端签名缓存并重新换签,不修改已返回的 OSS 签名 URL;禁止恢复 `/generated-puzzle-assets` 直读代理。
|
||||
- 验证:运行 `npm run test -- src\services\assetReadUrlService.test.ts src\hooks\useResolvedAssetReadUrl.test.tsx src\components\puzzle-result\PuzzleResultView.test.tsx`,再触发一次真实生成确认 Network 中先请求 `/api/assets/read-url`,图片 `src` 为未追加 `_v` 的签名 URL。
|
||||
- 关联:`src/services/assetReadUrlService.ts`、`src/components/ResolvedAssetImage.tsx`、`docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md`。
|
||||
|
||||
## 本地短信登录页签突然消失
|
||||
|
||||
- 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但 `LoginScreen` 中手机号验证码表单仍存在。
|
||||
- 原因:前端根据 `GET /api/auth/login-options` 返回的 `availableLoginMethods` 渲染页签;常见根因有两类:
|
||||
- 本地启动脚本没有让 `.env.local` 覆盖 `.env`,`SMS_AUTH_ENABLED=true` 不生效,后端只返回 `["password"]`。
|
||||
- Rust API 直连已返回 `["phone","password"]`,但 Vite 代理目标指向未监听端口,导致 3000 域名下的 `login-options` 返回 `500`,`AuthGate` 降级成 `["password"]`。
|
||||
- 3000 端口被旧 `dev:web` 占用后,新的完整栈 Vite 自动漂移到 3001/3002;浏览器仍打开旧 3000 页面,旧页面继续代理到已经下线的端口。
|
||||
- 单独 `npm run dev:web` 启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
|
||||
- 处理:优先用 `npm run api-server`、`npm run dev:rust` 或 `npm run dev` 启动,这些入口应保持 shell 环境变量最高优先级,并允许 `.env.local` 覆盖 `.env`;完整栈启动时还要确保脚本计算出的 `RUST_SERVER_TARGET` 不被 `.env.local` 里的旧值覆盖。排查时先请求 3000 域名下的 `/api/auth/login-options`,再直连 Rust API 目标,并核对 `.env.local` 的 `SMS_AUTH_ENABLED` 与代理端口;若 3001/3002 才返回正确结果,说明当前 3000 是旧前端进程,应清理旧进程后重启。
|
||||
- 验证:`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`scripts/api-server-maincloud.mjs`、`scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`。
|
||||
|
||||
## 本地短信收不到验证码先查 provider
|
||||
|
||||
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
|
||||
- 原因:本地 `.env.local` 里如果是 `SMS_AUTH_PROVIDER="mock"`,后端不会发真实短信,只会返回固定 mock 验证码;另外 `npm run api-server` 过去曾让 `.env` 覆盖 `.env.local`,导致本地真实短信配置被错误压回默认值。
|
||||
- 处理:真实短信联调时把 `.env.local` 的 `SMS_AUTH_PROVIDER` 显式设为 `aliyun`,然后重启 `api-server`;如果只想验证 UI 和账号链路,则保留 `mock` 并使用 `SMS_AUTH_MOCK_VERIFY_CODE`。
|
||||
- 验证:`GET /api/auth/login-options` 返回 `["phone","password"]`,`api-server` 日志里 `provider=aliyun` 才说明真实短信链路已生效。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`、`docs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md`。
|
||||
|
||||
## 手机验证码登录 500 先查短信 provider 语义
|
||||
|
||||
- 现象:登录弹窗手机号验证码登录失败,浏览器看到 `POST /api/auth/phone/login 500`,后端日志里同时出现阿里云短信 `UNKNOWN`、`biz.FREQUENCY` 或 `check frequency failed`。
|
||||
- 原因:真实短信 provider 的配置错误或上游失败曾被 `module-auth` 折叠成 `PhoneAuthError::Store`,HTTP 层只能按内部错误返回 `500`,掩盖了 provider 失败。
|
||||
- 处理:保留 provider 错误语义,配置错误映射 `503 Service Unavailable`,上游短信失败映射 `502 Bad Gateway`;本地只验证 UI/账号链路时可用 shell 临时覆盖 `SMS_AUTH_PROVIDER=mock` 后启动 `npm run api-server`。
|
||||
- 验证:`cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml`,真实 provider 频控时接口不再返回 `500`。
|
||||
- 关联:`server-rs/crates/module-auth/src/errors.rs`、`server-rs/crates/api-server/src/phone_auth.rs`、`docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md`。
|
||||
|
||||
## 手机验证码登录成功后又瞬间回到未登录
|
||||
|
||||
- 现象:手机号验证码登录先成功,随后 UI 又闪回“未登录”,登录弹窗可能重新出现。
|
||||
- 原因:`AuthGate` 首次 hydrate 会异步轮换 refresh cookie 并请求 `/api/auth/me`。如果用户在 hydrate 完成前已经登录,晚到的旧 hydrate 仍可能把刚写入的 `user` 覆盖成 `null`。
|
||||
- 处理:给 `AuthGate` 的 hydrate 增加版本号保护;登录成功、退出登录和全局 auth 事件都会推进版本号,旧 hydrate 结果到达后直接丢弃。
|
||||
- 验证:`npm run test -- src/components/auth/AuthGate.test.tsx`,新增用例应覆盖“旧 guest hydrate 不覆盖新登录态”。
|
||||
- 关联:`src/components/auth/AuthGate.tsx`、`src/components/auth/AuthGate.test.tsx`、`docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md`。
|
||||
|
||||
## 刷新网页后登录态失效
|
||||
|
||||
- 现象:刷新网页后,用户明明有本地 access token,却回到未登录状态。
|
||||
- 原因:`AuthGate` hydrate 曾先强制调用 `refreshStoredAccessToken()`;当 refresh cookie 临时失效、代理错配或后端返回 `401` 时,该方法会先清空本地 access token,随后 `/api/auth/me` 只能恢复成未登录。
|
||||
- 处理:`refreshStoredAccessToken()` 增加 `clearOnFailure` 选项;`AuthGate` 在已有本地 access token 时先用 `/api/auth/me` 确认用户,确认成功后再后台 refresh 续期与写每日登录埋点,后台 refresh 失败不清 token。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx -t "explicit refresh opts out|auth gate keeps a valid local token login"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/components/auth/AuthGate.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## 登录后推荐页加载出作品又回到未登录
|
||||
|
||||
- 现象:前端登录成功后进入推荐页,推荐页自动加载出一个作品,随后瞬间回到未登录;停留在其他页面或推荐页没加载出作品时不复现。
|
||||
- 原因:推荐页 embedded 运行态会自动发起受保护写请求。若这些卡片级后台请求遇到 `401` 或 refresh 失败,默认请求层曾清空 access token 并广播全局 auth 事件,导致 `AuthGate` 重新 hydrate 成未登录态。更隐蔽的是,`refreshAccessToken()` 自身曾在 refresh 失败时静默清 token,即便调用方关闭了 `clearAuthOnUnauthorized`,也可能让后续 hydrate 变成未登录。
|
||||
- 处理:请求层统一使用 `authImpact: 'global' | 'local'` 区分账号权威请求与局部后台请求;推荐页自动运行态、图片换签、公开拼图运行态和平台 bootstrap 私有投影刷新统一使用 `BACKGROUND_AUTH_REQUEST_OPTIONS` / `RUNTIME_BACKGROUND_AUTH_OPTIONS`,并等 `canReadProtectedData` 为 true 后再启动;用户主动点击的账号动作仍保留默认全局鉴权失败处理。
|
||||
- 追加处理:generated 私有图片换签 `/api/assets/read-url` 也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。
|
||||
- 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,`startPuzzleRun`、通关自动 `submitPuzzleLeaderboard`、下一关 `advancePuzzleNextLevel` 和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。
|
||||
- 追加处理:通关后 `refreshSaveArchives()`、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/services/assetReadUrlService.ts`、`src/services/puzzle-runtime/puzzleRuntimeClient.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md`。
|
||||
|
||||
## 推荐页作品卡一直显示加载中
|
||||
|
||||
- 现象:推荐页有公开作品,但主视口一直停在“加载中...”,没有进入作品,也没有显示可操作错误。
|
||||
- 原因:推荐页自动启动嵌入运行态时先设置 `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,但失败或并发切换时外层缺少稳定错误态和请求版本保护,旧启动请求可能晚到覆盖新状态。
|
||||
- 处理:`selectRecommendRuntimeEntry` 使用启动请求版本号丢弃旧请求;启动失败统一设置 `activeRecommendRuntimeError = "作品暂时无法进入,请稍后再试。"` 并关闭 `isStartingRecommendEntry`。
|
||||
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation surfaces start failure"`。
|
||||
- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## 推荐页未登录入口误打开公开详情
|
||||
|
||||
- 现象:新用户默认在发现页,但点击推荐页或推荐封面后,如果复用公开作品详情入口,可能绕过推荐页“登录后游玩”的产品门禁。
|
||||
- 原因:`RpgEntryHomeView` 曾只有 `onOpenGalleryDetail` 一个回调,同时服务发现页公开详情和推荐页作品入口;一旦为发现页保留公开浏览能力,推荐页也会跟着打开详情。
|
||||
- 处理:公开详情与推荐页入口分离为 `onOpenGalleryDetail` 和 `onOpenRecommendGalleryDetail`。发现页、搜索和排行榜保留公开详情;推荐 Tab、推荐封面、推荐运行态错误重试和桌面推荐模块统一走登录门禁。未登录推荐页只显示封面,点击封面只弹登录窗,不携带登录后自动打开详情的回调。
|
||||
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend"`。
|
||||
- 关联:`src/components/rpg-entry/RpgEntryHomeView.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## Rust 冷编译导致 api-server 健康检查误超时
|
||||
|
||||
@@ -91,14 +282,113 @@
|
||||
- 验证:冷启动时不再误杀仍在编译的 api-server。
|
||||
- 关联:`docs/technical/API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md`。
|
||||
|
||||
## Windows debug api-server 主线程栈溢出
|
||||
|
||||
- 现象:`cargo check -p api-server` 和 `build_router` 测试通过,但 `npm run api-server` 在 Windows debug 启动时 `thread 'main' has overflowed its stack`。
|
||||
- 原因:`api-server` Axum 路由树已经很深,debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。
|
||||
- 处理:入口 `main` 用显式 16MB 栈线程启动 Tokio runtime,并把实际服务逻辑放入 `run_server()`;新增路由时优先用小 router `.merge()`,避免继续拉长主链。
|
||||
- 验证:`npm run api-server` 后 `/healthz` 返回 200,相关路由冒烟通过。
|
||||
- 关联:`server-rs/crates/api-server/src/main.rs`、`server-rs/crates/api-server/src/app.rs`。
|
||||
|
||||
## Windows debug api-server.exe 锁文件与强杀退出码容易混淆
|
||||
|
||||
- 现象:`cargo run -p api-server` 或 `npm run api-server` 报 `failed to remove file ... target\debug\api-server.exe`;清理旧进程后,旧终端可能继续打印 `process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)`。
|
||||
- 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条 `npm run api-server` 链路仍在运行,进程树为 `npm run api-server -> node scripts/api-server-dev.mjs -> cargo run -> api-server.exe`。`0xffffffff` 常见于排障时用 `Stop-Process -Force` 强制结束旧 `api-server.exe` 后由 Cargo 回显,不一定代表新启动失败。
|
||||
- 处理:先按目标路径确认并停止本仓库的旧 `api-server.exe` 及其父级 `cargo/node/cmd` 启动链路,再重新启动;不要同时开多个 `npm run api-server`。
|
||||
- 验证:确认没有匹配 `C:\Genarrative\server-rs\target\debug\api-server.exe` 的进程后,`Remove-Item` 能删除旧 exe;随后 `npm run api-server` 启动并访问 `/healthz` 返回 200。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/main.rs`。
|
||||
|
||||
## dev-rust-stack 端口被旧进程占用时会误判健康检查
|
||||
|
||||
- 现象:`node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh --skip-spacetime` 输出 `Port 3000 is in use, trying another one...`,随后 `api-server.exe` 报 `AddrInUse` / `code: 10048`。
|
||||
- 原因:旧 `api-server` 仍监听默认 `8082` 时,脚本的 `/healthz` 探测会命中旧进程并误判新服务已就绪;旧 Vite 占住 `3000` 时,Vite 默认漂移到新端口,浏览器仍可能打开旧页面。
|
||||
- 处理:`scripts/dev-rust-stack.sh` 已在 publish / 编译前预检 `api-server`、主站 Vite、后台 Vite 端口,并让 Vite 使用 `--strictPort`;遇到端口占用时按脚本打印的 PID 停止旧进程,或显式传入 `--api-port` / `--web-port` / `--admin-web-port`。
|
||||
- 验证:默认端口被占用时,完整栈应在发布模块前直接失败并打印监听进程;清理端口后重新启动不再漂移端口或命中旧 `/healthz`。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`docs/technical/DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md`。
|
||||
|
||||
## Windows debug 长 SSE Future 触发 api-server 断连
|
||||
|
||||
- 现象:前端 Vite 代理请求 `/api/runtime/creative-agent/sessions/{sessionId}/messages/stream` 报 `read ECONNRESET`,随后 `api-server.exe` 以 `0xffffffff` 退出,`dev:rust` 回收 SpacetimeDB、Vite 和后台 Vite。
|
||||
- 原因:单个 `async_stream::stream!` 中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future;真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。
|
||||
- 处理:长 SSE 路由优先使用 `tokio::spawn` 跑业务流程,通过 `mpsc` + `UnboundedReceiverStream` 向 Axum 返回轻量 stream;失败时更新会话为 `failed` 并发送 SSE `error`,不要把大段执行逻辑内联到路由返回的 stream future 中。
|
||||
- 验证:补充实际 `collect()` SSE body 的路由测试,确认首轮包含 `stage`、`puzzle_template_catalog` 和 `done`,且不会提前发送 `puzzle_template_selection` / `puzzle_cost_range`;再执行 `cargo check -p api-server`、`cargo test -p api-server creative_agent`,联调时用 `npm run api-server` 检查 `/healthz`。
|
||||
- 关联:`server-rs/crates/api-server/src/creative_agent.rs`、`server-rs/crates/api-server/src/app.rs`。
|
||||
|
||||
## creative-agent 过程项不要把历史事件渲染成运行中
|
||||
|
||||
- 现象:智能创作页过程中多个阶段从一开始同时转圈,生成结束或进入模板确认后仍有过程项保持转圈。
|
||||
- 原因:前端把历史 `stage`、`tool_started` 和 `thought_summary_delta` 都按 active 渲染;后端工具开始/完成事件如果 `toolCallId` 不一致,也会导致开始事件无法收口。
|
||||
- 处理:
|
||||
- 只有最新且仍在执行的 stage 可为 active;等待确认、等待用户、target ready 和 failed 都是静态状态。
|
||||
- 工具开始事件必须等同一 `toolCallId` 的 `tool_completed` 收口;兼容旧流时可按后续同名完成事件兜底。
|
||||
- 思考摘要只展示用户可见摘要,且流结束或会话进入等待/完成/失败态后必须改成 done。
|
||||
- 验证:前端测试断言完成后 `CreativeAgentProcessItem` 不再存在 `tone === 'active'`;后端测试确认工具开始/完成事件使用相同 `toolCallId`。
|
||||
- 关联:`src/components/creative-agent/creativeAgentViewModel.ts`、`server-rs/crates/api-server/src/creative_agent.rs`、`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## creative-agent 会话切换要清理本地待确认模板
|
||||
|
||||
- 现象:用户在一个智能创作会话中点开模板确认面板后,立即切到另一条创作会话,可能看到上一会话的确认面板残留。
|
||||
- 原因:模板确认面板的 `pendingSelection` 是 `CreativeAgentWorkspace` 本地 UI 状态,不属于后端 session 快照;组件复用时如果不监听 `sessionId` 清理,会跨会话泄漏。
|
||||
- 处理:工作区以 `session?.sessionId` 为边界清空 `pendingSelection`;服务端仍以 `puzzleTemplateSelection` / `targetBinding` 作为正式业务状态。
|
||||
- 验证:前端测试先点开模板确认面板,再 rerender 到另一 session,断言确认面板消失。
|
||||
- 关联:`src/components/creative-agent/CreativeAgentWorkspace.tsx`、`src/components/creative-agent/CreativeAgentWorkspace.test.tsx`。
|
||||
|
||||
## 视觉小说 VN-10 不要绕过平台资产引用
|
||||
|
||||
- 现象:文档、封面、场景背景、角色立绘或音乐为了预览方便被写成 Data URL、裸对象路径、外部 URL 或本地临时文件路径。
|
||||
- 原因:前端上传与预览容易混在一起,若不走平台资产对象,SpacetimeDB 和长期草稿会被大文本或大二进制污染。
|
||||
- 处理:VN 资产统一用 `/api/assets/direct-upload-tickets`、OSS 直传、`/api/assets/objects/confirm`,长期状态只保存 `assetObjectId` 和 `/generated-*` 引用;运行时图片用 `ResolvedAssetImage` 换签。
|
||||
- 验证:文档模式 `sourceAssetIds` 为平台资产 id;草稿中不出现 `data:`;图片和音乐字段为平台 generated 引用或 null。
|
||||
- 关联:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`src/services/visual-novel-creation/visualNovelAssetClient.ts`。
|
||||
|
||||
## 视觉小说 VN-13 交接时不要再回头找旧迁移方案
|
||||
|
||||
- 现象:接手视觉小说的人容易重新打开旧 TXT 迁移文档,把“外部平台工程迁入”误当成当前实现目标。
|
||||
- 原因:视觉小说历史资料里保留了很多迁移阶段的讨论,而当前真正的实现口径已经收口到 PRD、表目录、Prompt 工具说明、实现收口文档和负向扫描报告。
|
||||
- 处理:维护视觉小说时优先看 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`SPACETIMEDB_TABLE_CATALOG.md`、`VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`、`VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md` 和 `VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`。
|
||||
- 验证:新开发者只读这组文档即可继续维护,不需要把旧 TXT 迁移方案重新当作编码依据。
|
||||
- 关联:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`docs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md`。
|
||||
|
||||
## 视觉小说公开广场不要触发登录刷新
|
||||
|
||||
- 现象:未登录用户进入平台公开广场或从推荐流读取视觉小说公开作品时,前端可能先尝试 `/api/auth/refresh`,失败后再读取公开列表,导致无意义的鉴权噪声或 401 状态刷新。
|
||||
- 原因:公开只读接口如果复用默认 `requestJson` 选项,缺少 access token 时会先走静默 refresh。
|
||||
- 处理:视觉小说公开广场列表使用 `skipAuth: true` 与 `skipRefresh: true`;鉴权 mutation 仍保持默认鉴权链路。
|
||||
- 验证:执行 `src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts`,确认 `/api/runtime/visual-novel/gallery` 请求携带 `skipAuth` / `skipRefresh`,而 run、重生成和存档 mutation 仍走受保护路由。
|
||||
- 关联:`src/services/visual-novel-runtime/visualNovelRuntimeClient.ts`、`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 创作 Tab 语义迁移后,旧“新建作品”测试要改看智能创作首页
|
||||
|
||||
- 现象:把 `create` 从旧创作中心切到 `CreativeAgentHome` 后,旧测试仍尝试在创作页找“新建作品”类型卡,导致用例失败或定位不到元素。
|
||||
- 原因:产品语义已经变成“创作 = 智能创作首页,草稿 = 旧作品架”,但测试夹具和 helper 还沿用旧入口。
|
||||
- 处理:把这类测试改成验证智能创作首页、快捷胶囊、抽屉与草稿 Tab;同时给 `useRpgEntryLibraryDetail` 这类恢复路径补上 `setPlatformTabToDraft`。
|
||||
- 验证:定向 `vitest`、`eslint`、`typecheck`、`check:encoding` 都通过。
|
||||
- 关联:`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx`、`src/components/rpg-entry/useRpgEntryLibraryDetail.ts`。
|
||||
|
||||
## server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
|
||||
|
||||
- 现象:在 `server-rs` 下无参数 `cargo build` 期望同时构建 `spacetime-module`,导致链接或构建范围误判。
|
||||
- 原因:workspace default-members 当前只包含 `crates/api-server`;SpacetimeDB module 有独立构建/发布方式。
|
||||
- 处理:默认 Rust 构建只覆盖原生 `api-server`;模块产物继续走 `spacetime build` / publish / bindings 生成流程。
|
||||
- 处理:默认 Rust 构建只覆盖原生 `api-server`;本地模块发布继续走 `spacetime publish --module-path ... --build-options="--debug"` / bindings 生成流程。
|
||||
- 验证:查看 `server-rs/Cargo.toml` default-members,并按相关 SpacetimeDB 文档执行模块构建。
|
||||
- 关联:`server-rs/Cargo.toml`、`docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md`。
|
||||
|
||||
## Windows 原生 `spacetime-module` 单测会链接缺失 SpacetimeDB 宿主符号
|
||||
|
||||
- 现象:在 Windows 上执行 `cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml` 可能编译到链接阶段后失败,出现 `LNK2019` / `LNK1120`,缺失 `datastore_insert_bsatn`、`procedure_start_mut_tx`、`console_log` 等 SpacetimeDB 宿主符号。
|
||||
- 原因:`spacetime-module` 依赖的 SpacetimeDB runtime API 面向 wasm 宿主环境,原生 test exe 链接不到这些宿主导出。
|
||||
- 处理:日常语法和类型验证使用 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`;需要验证模块行为时走 SpacetimeDB publish/dev 或模块域纯 Rust crate 的单测,不把该原生链接错误当作业务测试失败。
|
||||
- 验证:`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能通过;原生 `cargo test` 若仍报上述宿主符号缺失,按当前限制记录为未执行。
|
||||
- 关联:`server-rs/crates/spacetime-module`、`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`。
|
||||
|
||||
## Rust 构建不要让不可用的 sccache 阻断 rustc
|
||||
|
||||
- 现象:Cargo 报 `could not execute process sccache ... rustc.exe -vV (never executed)`、`sccache: error: Timed out waiting for server startup`,或 `sccache: caused by: Failed to send data to or receive data from server / Failed to read response header / failed to fill whole buffer`;真实 `rustc -Vv` 可以执行,但构建在调用包装器时失败。
|
||||
- 原因:环境、Jenkinsfile 或 `server-rs/.cargo/config.toml` 启用了 `sccache` wrapper,但当前 agent 没有可执行的 `sccache`、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。Windows 本机若配置了 `SCCACHE_OSS_*`,sccache daemon 冷启动会先经 OSS/本机代理完成缓存读写检查,再监听 `127.0.0.1:4226`;代理或 OSS 链路慢时,Cargo 的 `sccache rustc -vV` 可能先超时。
|
||||
- 处理:保留 `server-rs/.cargo/config.toml` 的 `rustc-wrapper = "sccache"`;Windows 本机优先在 `%APPDATA%\Mozilla\sccache\config\config` 写入 `server_startup_timeout_ms = 60000`,拉长 client 等待 daemon 完成 OSS 初始化的时间,然后删除 `server-rs/target/.rustc_info.json` 里缓存的失败探测结果并重跑原始 Cargo 命令。冷启动验证优先用 `sccache --stop-server`,不要在另一个 `cargo` / `rustc` 仍在编译时 `taskkill /F /IM sccache.exe /T`,否则 proc-macro crate 可能被打断并表现为 `serde_derive` / `spacetimedb-bindings-macro` 的 `sccache ... exit code: 1`。若只做临时排障,可在 Git Bash 中执行 `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build ...`,或在 PowerShell 用 `cargo check -p api-server --config "build.rustc-wrapper=''"` 一次性绕过 wrapper;生产流水线必须先实际执行 `sccache --version`,失败时移除 `RUSTC_WRAPPER` 并回退到直接 `rustc`。
|
||||
- 验证:`rustc -Vv` 能输出版本;冷启动后原始 `cargo check -p api-server` 和 `cargo check -p spacetime-module` 能通过;`sccache --show-stats` 显示 `Cache location oss, name: genarrative-sccache`,证明仍在使用 sccache/OSS 缓存;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## 生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
|
||||
|
||||
- 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
|
||||
@@ -107,6 +397,14 @@
|
||||
- 验证:发布链路使用当前 `deploy/systemd`、`deploy/nginx`、`scripts/deploy` 和 `jenkins/Jenkinsfile.production-*`。
|
||||
- 关联:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## Jenkins 可选参数在 set -u 下不能裸读
|
||||
|
||||
- 现象:数据库导入或导出流水线报 `INCLUDE_TABLES: unbound variable`,或其它可选参数在 Bash 中未定义即退出。
|
||||
- 原因:Jenkins string/boolean 参数留空时不一定会导出同名环境变量,而生产数据库导入导出脚本块启用了 `set -u`。
|
||||
- 处理:进入 Bash 执行块后先使用 `${VAR:-}` 或 `${VAR:-默认值}` 收敛成本地变量;必填项使用 `${VAR:?中文错误}` 明确失败原因。
|
||||
- 验证:扫描 `jenkins/Jenkinsfile.production-database-export` 与 `jenkins/Jenkinsfile.production-database-import`,确认 `INCLUDE_TABLES`、`CHUNK_SIZE`、`SERVER_BACKUP_DIRECTORY`、`SMOKE_HEALTH_URL` 等可选参数不再裸读。
|
||||
- 关联:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`、`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`。
|
||||
|
||||
## 个人任务 scope 不得扩成 work/site/module
|
||||
|
||||
- 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。
|
||||
@@ -114,3 +412,59 @@
|
||||
- 处理:Admin 任务配置页不展示范围选择,保存时固定 `scopeKind: 'user'`;API 和领域构造层拒绝非 `User`。
|
||||
- 验证:非 `user` scope 返回错误;相关测试覆盖 `Site` / `Module` / `Work` 被拒绝。
|
||||
- 关联:`docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。
|
||||
|
||||
## 拼图发布 409 不一定是接口故障
|
||||
|
||||
- 现象:拼图结果页点击发布后,控制台出现 `POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions 409 (Conflict)`,用户只看到发布失败。
|
||||
- 原因:`publish_puzzle_work` 是资产操作发布入口,发布前会预扣 `1` 枚光点;余额不足时后端按业务冲突返回 `409 CONFLICT`,`details.message` 为 `光点余额不足`。
|
||||
- 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
|
||||
- 验证:`PuzzleResultView` 单测覆盖发布弹窗内展示 `光点余额不足`。
|
||||
- 关联:`src/components/puzzle-result/PuzzleResultView.tsx`、`docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md`、`docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md`。
|
||||
|
||||
## WebGL 画布在高 DPR 移动端放大溢出
|
||||
|
||||
- 现象:抓大鹅试玩入口进入后,3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
|
||||
- 原因:`WebGLRenderer.setPixelRatio(...)` 会把绘图缓冲区乘上设备 DPR;如果没有给 `renderer.domElement` 单独设置 CSS `width/height: 100%` 和绝对铺满,浏览器可能把高 DPR 缓冲区尺寸当成页面显示尺寸。
|
||||
- 处理:中心棋盘和托盘预览的 WebGL canvas 统一套用 `position:absolute; inset:0; width:100%; height:100%; display:block`,`renderer.setSize(..., false)` 只负责同步绘图缓冲区。
|
||||
- 验证:强制移动端 `390x844`、DPR 2 截图,确认棋盘左右边界在视口内,canvas CSS 尺寸等于容器尺寸,内部 `width/height` 属性可大于 CSS 尺寸。
|
||||
- 关联:`src/components/match3d-runtime/Match3DPhysicsBoard.tsx`、`docs/technical/MATCH3D_RUNTIME_3D_GEOMETRY_EXPERIMENT_2026-05-02.md`。
|
||||
|
||||
## Hyper3D subscriptionKey 不要按固定短文本限长
|
||||
|
||||
- 现象:抓大鹅生成草稿时,内联 Rodin 图生 3D 模型提交成功后,状态轮询报 `subscriptionKey 超过 256 字符`,导致 `/api/creation/match3d/sessions/{sessionId}/actions` 返回 400。
|
||||
- 原因:`subscriptionKey` 是 Hyper3D 返回的 opaque token,长度由上游决定;后端状态查询曾复用普通文本校验,把它限制在 256 字符。
|
||||
- 处理:`query_task_status` 对 `subscriptionKey` 只做 trim 和非空校验,不做固定长度限制;前端临时任务和 Match3D 草稿响应可继续展示该 token,但不要把它当作可编辑短文本。
|
||||
- 验证:`cargo test -p api-server accepts_opaque_subscription_key_without_length_cap --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`server-rs/crates/api-server/src/hyper3d_generation.rs`、`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`。
|
||||
|
||||
## 抓大鹅草稿生成不要阻塞在 Rodin 模型下载
|
||||
|
||||
- 现象:抓大鹅草稿生成时 Hyper3D 状态已完成,但下载列表为空或没有可用模型文件,`/api/creation/match3d/sessions/{sessionId}/actions` 返回 `502 Bad Gateway`,前端提示 `Hyper3D 已完成但未返回可下载模型文件`。
|
||||
- 原因:草稿生成链路曾在切割图片后立即并行调用 Rodin 图生模型,并把模型下载成功作为草稿完成前置条件;上游完成态和可下载文件列表不是强一致,容易把本来可用的图片草稿卡死。
|
||||
- 处理:草稿阶段只生成物品名、素材图、切割独立图片并上传 OSS,返回 `status = image_ready`;Rodin 3D 模型生成留到结果页 `3D素材` Tab 手动触发。
|
||||
- 验证:草稿响应中的 `generatedItemAssets[].imageSrc` 有值、`modelSrc` 为空、状态为 `image_ready`;结果页显示 `图片已就绪` 和 `0 文件`,不会自动请求 Hyper3D 下载。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅切图路径不能只用中文物品名
|
||||
|
||||
- 现象:草稿页 `3D素材` Tab 中多个素材名称不同,但预览图片完全一样;点击图生模型生成时还可能提示 `参考图必须是 data URL`。
|
||||
- 原因:中文物品名经过 OSS 路径段清洗后都可能退化成 `item`,多张切割图片写到同一个 `items/item/image.png` object key,后写入覆盖先写入;结果页手动 Rodin 图生模型还曾把 `/generated-match3d-assets/...` 私有路径直接作为 `imageDataUrls` 提交。
|
||||
- 处理:切割图上传路径必须带稳定唯一 `itemId` 前缀,例如 `items/match3d-item-1-item/image.png`;结果页提交图生模型前,generated 私有路径先经同源 `/api/assets/read-bytes` 由后端换签并读取字节,前端再转成 `data:image/...;base64,...`,不要在浏览器里直接 `fetch` OSS 签名 URL,否则会被 bucket CORS 拦截。
|
||||
- 验证:后端单测覆盖中文名路径唯一;前端单测覆盖 generated 参考图会换签、fetch 并以 Data URL 调用 `submitHyper3dImageToModel`。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅生成素材不能只挂在 compile response
|
||||
|
||||
- 现象:抓大鹅草稿生成完成后停留在结果页能看到切割好的 `3D素材` 图片;退出后从草稿 Tab 重新进入同一草稿,素材列表变回默认占位或为空,已生成的物品名称和图片丢失。
|
||||
- 原因:`generatedItemAssets` 如果只附加在 `match3d_compile_draft` 的 HTTP response draft 上,刷新或重进时 `getMatch3DWorkDetail` 只能读取 SpacetimeDB 中的 `match3d_work_profile`;旧 mapper 返回空数组,自然无法恢复素材。拼图链路已经通过 `save_puzzle_generated_images` 把候选图和 levels 写回 work profile,抓大鹅也必须同样写持久字段。
|
||||
- 处理:compile 成功时把独立物品图片列表序列化写入 `match3d_work_profile.generated_item_assets_json`;`update_match3d_work` / `publish_match3d_work` 保留该字段;API work summary/detail 映射反序列化为 `generatedItemAssets`。前端保持“本次 draft 优先,重进 profile 兜底”的读取顺序。
|
||||
- 验证:`cargo test -p spacetime-client match3d --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/match3d/*`、`server-rs/crates/spacetime-client/src/mapper.rs`、`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 公开作品详情深链找不到作品不能停在空详情页
|
||||
|
||||
- 现象:直接访问 `/works/detail?work=PZ-...`,作品不存在或已下架时会弹出“作品不存在或已下架,将返回首页。”;关闭提示后仍可能停在大白屏。
|
||||
- 原因:旧恢复逻辑只覆盖 `/runtime/...`,没有覆盖 `/works/detail`。同时 `selectionStage === 'work-detail'` 且 `selectedPublicWorkDetail === null` 时没有兜底渲染,详情数据为空就只剩空页面。
|
||||
- 处理:公开详情失效统一走 `resolveWorkNotFoundRecoveryAction(...)`,覆盖 `/works/detail`、`/gallery/puzzle/detail` 和 `/gallery/visual-novel/detail`;搜索失败和拼图详情 404 分支清理详情/运行态临时状态并回首页;`work-detail` 空数据阶段显示轻量读取态,避免异步间隙白屏。
|
||||
- 验证:`npm run test -- src/routing/runtimeNotFoundRecovery.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"`。
|
||||
- 关联:`docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md`、`src/routing/runtimeNotFoundRecovery.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`。
|
||||
|
||||
@@ -79,7 +79,7 @@ DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
- SpacetimeDB 接入:`spacetime-client`、`spacetime-module`
|
||||
- HTTP 服务与测试:`api-server`、`tests-support`
|
||||
|
||||
注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`,SpacetimeDB 模块产物继续走 `spacetime build` / 发布链路。
|
||||
注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`,本地 SpacetimeDB 模块发布继续走 `spacetime publish --module-path ... --build-options="--debug"`。
|
||||
|
||||
Cargo 依赖口径:第三方依赖版本和 workspace 内部 crate path 统一维护在 `server-rs/Cargo.toml` 的 `[workspace.dependencies]`,成员 crate 默认继承 workspace 依赖,只保留自身 `features`、`optional` 或 target-specific 差异。
|
||||
|
||||
|
||||
392
.hermes/skills/behavior-driven-development/SKILL.md
Normal file
392
.hermes/skills/behavior-driven-development/SKILL.md
Normal file
@@ -0,0 +1,392 @@
|
||||
---
|
||||
name: behavior-driven-development
|
||||
description: 在 Genarrative 中需要用 BDD/行为驱动方式把 PRD、用户故事、验收标准转成可执行场景、Gherkin 用例、测试计划或 TDD 落地顺序时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [BDD, Gherkin, 验收标准, 用户故事, 测试, Genarrative]
|
||||
related_skills: [writing-plans, test-driven-development, systematic-debugging, requesting-code-review]
|
||||
---
|
||||
|
||||
# BDD 行为驱动开发流程
|
||||
|
||||
用于在 Genarrative 项目中,把产品需求、用户故事、业务规则和验收标准沉淀成清晰、可讨论、可验证的行为场景,并进一步映射到前端测试、API 测试、领域服务测试或 E2E 测试。
|
||||
|
||||
BDD 的重点不是“先写一堆 UI 自动化脚本”,而是让团队先对“用户在什么上下文下做什么,系统应该给出什么可观察结果”达成一致。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 从 PRD、设计文档、用户故事中提炼验收标准。
|
||||
- 功能需求容易产生理解偏差,需要先把行为边界说清楚。
|
||||
- 涉及前端、后端、运行态、异步任务、权限、埋点或状态流转的跨层功能。
|
||||
- 需要把“完成标准”写成 Given / When / Then 场景。
|
||||
- 需要在编码前规划测试覆盖:单元测试、组件测试、API 测试、E2E 测试。
|
||||
- 希望和 TDD 配合:先写行为场景,再把场景拆到 RED-GREEN-REFACTOR。
|
||||
- 需要给产品、测试、前后端开发共同评审的一份中文验收说明。
|
||||
|
||||
不适用:
|
||||
|
||||
- 纯重构且对外行为不变,只需要 characterization tests 或回归测试。
|
||||
- 一次性小改动,验收规则非常明确且无跨层状态。
|
||||
- 只想记录实现细节、代码结构或技术方案;这类内容更适合技术设计文档。
|
||||
|
||||
## 必读约束
|
||||
|
||||
1. 先描述用户可观察行为,再讨论实现细节。
|
||||
2. 场景必须可验证,避免“体验更好”“更智能”“合理展示”等不可判定表述。
|
||||
3. 不要把 Gherkin 写成低层 UI 点击脚本;UI 细节只在确实属于业务行为时出现。
|
||||
4. 中文 PRD、中文 UI 文案、中文剧情和注释不要擅自改成英文。
|
||||
5. 如果项目文档不足以支持准确落地,应先补齐 `docs/` 下的 PRD/设计/技术文档,再进入编码。
|
||||
6. BDD 场景要覆盖主要成功路径、关键失败路径、权限/登录态、边界条件和回归风险。
|
||||
|
||||
## 核心格式
|
||||
|
||||
推荐使用中文 Gherkin:
|
||||
|
||||
```gherkin
|
||||
功能: <业务能力名称>
|
||||
为了 <用户/业务价值>
|
||||
作为 <角色>
|
||||
我希望 <能力>
|
||||
|
||||
背景:
|
||||
假如 <所有场景共享的前置条件>
|
||||
|
||||
场景: <具体行为名称>
|
||||
假如 <上下文/已有状态>
|
||||
当 <用户动作或系统事件>
|
||||
那么 <可观察结果>
|
||||
而且 <额外可观察结果>
|
||||
|
||||
场景大纲: <带参数的行为名称>
|
||||
假如 <上下文中包含 <变量>>
|
||||
当 <动作>
|
||||
那么 <结果>
|
||||
|
||||
例子:
|
||||
| 变量 | 期望 |
|
||||
| A | X |
|
||||
| B | Y |
|
||||
```
|
||||
|
||||
英文关键字也可以使用:
|
||||
|
||||
```gherkin
|
||||
Feature: Work publish permission
|
||||
Scenario: Anonymous user attempts to publish a draft
|
||||
Given an anonymous user has a generated draft
|
||||
When the user clicks publish
|
||||
Then the login modal should be shown
|
||||
And the draft should remain unchanged
|
||||
```
|
||||
|
||||
在 Genarrative 项目内,若参与评审的人主要使用中文,优先中文场景;测试框架要求英文命名时,可以保留中文场景标题并在测试文件中使用英文 describe/it。
|
||||
|
||||
## 从需求提炼 BDD 场景
|
||||
|
||||
### Step 1: 识别角色和业务目标
|
||||
|
||||
先回答:
|
||||
|
||||
- 谁在使用?游客、已登录用户、创作者、管理员、审核人员、系统任务?
|
||||
- 用户想完成什么?创建、生成、保存、发布、试玩、查看、兑换、导出?
|
||||
- 业务价值是什么?降低创作门槛、保护权限、保证数据一致性、提升运营可见性?
|
||||
|
||||
### Step 2: 抽取领域词汇
|
||||
|
||||
建立统一术语,避免同一概念多种叫法:
|
||||
|
||||
- work / 作品
|
||||
- draft / 草稿
|
||||
- session / 创作会话
|
||||
- runtime / 运行态
|
||||
- publish / 发布
|
||||
- profile / 我的页签
|
||||
- invite code / 邀请码
|
||||
- analytics event / 埋点事件
|
||||
|
||||
场景中优先使用业务词,不要直接写组件名、函数名、数据库表名,除非这些就是用户可见对象。
|
||||
|
||||
### Step 3: 列出行为切片
|
||||
|
||||
按用户旅程切分:
|
||||
|
||||
1. 入口是否出现、是否可点击。
|
||||
2. 进入页面或工作台后的初始状态。
|
||||
3. 用户提交输入后的成功路径。
|
||||
4. 失败路径:未登录、参数无效、权限不足、网络/API 失败、异步任务失败。
|
||||
5. 状态持久化:刷新、返回、重新进入、跨设备或重新登录。
|
||||
6. 对外副作用:保存、发布、埋点、通知、导出、生成资产。
|
||||
7. 回归风险:旧入口、旧数据、移动端布局、中文编码。
|
||||
|
||||
### Step 4: 把每个切片写成 Given / When / Then
|
||||
|
||||
检查每个场景:
|
||||
|
||||
- Given 只描述前置状态,不写动作过程。
|
||||
- When 只描述一个主要触发动作或事件。
|
||||
- Then 描述可观察结果,可以被测试或人工验收。
|
||||
- 一个场景只验证一个核心行为;不要把完整长流程塞进一个巨型场景。
|
||||
|
||||
## Genarrative 场景模板
|
||||
|
||||
### 前端入口 / 页面行为
|
||||
|
||||
```gherkin
|
||||
功能: 我的页签反馈入口
|
||||
为了让用户能从个人中心提交问题
|
||||
作为已登录用户
|
||||
我希望在我的页签打开独立的帮助与反馈页面
|
||||
|
||||
场景: 已登录用户从我的页签进入反馈页面
|
||||
假如用户已登录并停留在我的页签
|
||||
当用户点击“帮助与反馈”入口
|
||||
那么系统应进入独立的反馈页面
|
||||
而且底部 tab 应保持选中“我的”
|
||||
而且页面不应展开在我的页签当前面板下方
|
||||
|
||||
场景: 用户从反馈页面返回我的页签
|
||||
假如用户正在反馈页面
|
||||
当用户点击返回按钮
|
||||
那么系统应回到平台主页面
|
||||
而且当前 tab 应为“我的”
|
||||
```
|
||||
|
||||
### 登录态 / 权限行为
|
||||
|
||||
```gherkin
|
||||
功能: 需要登录的发布能力
|
||||
为了保护作品归属和发布链路
|
||||
作为游客
|
||||
我不能在未登录时发布作品
|
||||
|
||||
场景: 游客尝试发布生成草稿
|
||||
假如游客已经生成一个草稿
|
||||
当游客点击发布按钮
|
||||
那么系统应打开登录弹窗
|
||||
而且不应创建正式作品
|
||||
而且草稿内容应保留在当前会话中
|
||||
```
|
||||
|
||||
### 后端 API / 领域规则
|
||||
|
||||
```gherkin
|
||||
功能: 作品正式游玩开始埋点
|
||||
为了统计不同玩法的正式游玩行为
|
||||
作为数据分析人员
|
||||
我希望每次用户进入正式作品运行态时记录统一事件
|
||||
|
||||
场景大纲: 支持的玩法进入正式游玩
|
||||
假如存在一个已发布的 <玩法> 作品
|
||||
当用户从作品详情进入正式游玩
|
||||
那么后端应记录 work_play_start 事件
|
||||
而且 scope_kind 应为 work
|
||||
而且 metadata 应包含 playType、workId、sourceRoute 和 userId
|
||||
|
||||
例子:
|
||||
| 玩法 |
|
||||
| puzzle |
|
||||
| match3d |
|
||||
| square-hole |
|
||||
| custom-world |
|
||||
| big-fish |
|
||||
| visual-novel |
|
||||
```
|
||||
|
||||
### 异步生成 / SSE 行为
|
||||
|
||||
```gherkin
|
||||
功能: AI 创作会话流式回复
|
||||
为了让用户看到生成进度
|
||||
作为创作者
|
||||
我希望提交创作指令后能收到流式反馈并最终得到可编辑草稿
|
||||
|
||||
场景: 成功生成草稿
|
||||
假如用户已登录并创建了创作会话
|
||||
当用户提交有效的创作指令
|
||||
那么系统应开始展示流式回复
|
||||
而且生成结束后应展示草稿结果
|
||||
而且会话快照应包含最新用户输入和 AI 回复
|
||||
|
||||
场景: 生成失败
|
||||
假如用户已登录并创建了创作会话
|
||||
当用户提交创作指令但后端生成失败
|
||||
那么系统应展示可理解的失败状态
|
||||
而且用户应能够重试
|
||||
而且不应覆盖上一次成功生成的草稿
|
||||
```
|
||||
|
||||
## 映射到测试类型
|
||||
|
||||
| BDD 场景关注点 | 推荐测试层级 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| 纯领域规则、状态机、校验 | Rust/TS 单元测试 | reducer、module-*、schema validator |
|
||||
| DTO 契约、API 请求响应 | API/contract 测试 | Axum handler、shared-contracts serde |
|
||||
| 页面渲染、按钮状态、表单校验 | 组件测试 | Vitest + Testing Library |
|
||||
| 路由、tab、页面阶段切换 | 前端集成测试 | appPageRoutes、FlowShell 行为 |
|
||||
| 登录态、发布、运行态完整链路 | E2E/smoke | Playwright 或项目 smoke 脚本 |
|
||||
| 埋点、副作用、后台导出 | 后端集成/API 测试 | tracking event、admin export |
|
||||
|
||||
原则:
|
||||
|
||||
- 不是每个 BDD 场景都必须落成 E2E。
|
||||
- 能在低层稳定验证的规则,不要强行放到脆弱的浏览器自动化里。
|
||||
- E2E 只覆盖最关键的用户旅程和跨层集成风险。
|
||||
|
||||
## 与 TDD 的配合方式
|
||||
|
||||
BDD 先回答“行为是什么”,TDD 再推动“代码怎么长出来”。
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 写 BDD 场景,确认业务行为和验收标准。
|
||||
2. 给每个场景标注测试层级:unit / component / API / E2E。
|
||||
3. 选择一个最小场景进入 TDD。
|
||||
4. RED:先写失败测试,测试名称对应场景标题。
|
||||
5. GREEN:实现最小代码让测试通过。
|
||||
6. REFACTOR:清理重复、命名、边界和文档。
|
||||
7. 回到下一个场景,直到主要路径和关键失败路径覆盖。
|
||||
|
||||
测试命名建议:
|
||||
|
||||
```ts
|
||||
describe('帮助与反馈入口', () => {
|
||||
it('已登录用户从我的页签进入独立反馈页面', () => {
|
||||
// Given ...
|
||||
// When ...
|
||||
// Then ...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Rust 测试命名建议:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn anonymous_user_cannot_publish_generated_draft() {
|
||||
// Given
|
||||
// When
|
||||
// Then
|
||||
}
|
||||
```
|
||||
|
||||
## 推荐产物
|
||||
|
||||
根据任务复杂度选择产物位置。使用本 skill 产出 Gherkin/BDD 场景时,必须先决定落点,不要把正式验收场景随手写在聊天记录里。
|
||||
|
||||
### Gherkin/BDD 场景默认落点
|
||||
|
||||
| 产物类型 | 推荐路径 | 适用场景 |
|
||||
| --- | --- | --- |
|
||||
| 实施前分析 / 临时计划 | `.hermes/plans/<task-name>-bdd-scenarios.md` | 某次 Hermes 开发任务前,用于澄清行为、拆测试、辅助实现;不一定作为长期产品依据。 |
|
||||
| 正式产品验收 / PRD 场景 | `docs/prd/<FEATURE>_BDD_YYYY-MM-DD.md` | 产品、测试、开发都需要长期参考的验收标准、用户故事、功能边界。 |
|
||||
| 技术/API/领域行为场景 | `docs/technical/<FEATURE>_BDD_YYYY-MM-DD.md` | 后端 API、领域规则、状态机、SpacetimeDB reducer/table、SSE/异步任务、埋点副作用。 |
|
||||
| 自动化 Gherkin feature 文件 | `tests/features/*.feature` 或 `e2e/features/*.feature` | 项目已接入 Cucumber/Playwright BDD 等 Gherkin runner 时。未接入前不要随意新建测试 runner 目录。 |
|
||||
| 稳定流程或团队经验 | `.hermes/shared-memory/` 或 `.hermes/skills/` | 不是某个功能验收,而是长期可复用的团队流程、坑点、执行规范。 |
|
||||
|
||||
默认规则:
|
||||
|
||||
1. 用户只说“先用 BDD 梳理一下/写场景/写 Gherkin”,默认写到 `.hermes/plans/<task-name>-bdd-scenarios.md`。
|
||||
2. 用户说“正式验收标准/PRD/产品文档/给测试验收”,写到 `docs/prd/<FEATURE>_BDD_YYYY-MM-DD.md`。
|
||||
3. 用户说“API 行为/后端规则/状态机/埋点/异步任务/SpacetimeDB”,写到 `docs/technical/<FEATURE>_BDD_YYYY-MM-DD.md`。
|
||||
4. 用户明确要求“可执行 feature 文件”且项目已有 runner,再写 `.feature` 文件;否则先写 Markdown BDD 文档,并在测试映射中标注未来自动化落点。
|
||||
5. 如果 BDD 场景会作为编码依据,文档中必须包含“测试映射”表,标注场景要落到哪些测试文件。
|
||||
|
||||
命名建议:
|
||||
|
||||
```text
|
||||
.hermes/plans/profile-feedback-bdd-scenarios.md
|
||||
docs/prd/PROFILE_FEEDBACK_BDD_2026-05-11.md
|
||||
docs/technical/WORK_PLAY_TRACKING_BDD_2026-05-11.md
|
||||
tests/features/profile-feedback.feature
|
||||
e2e/features/invite-code.feature
|
||||
```
|
||||
|
||||
### 其他配套产物
|
||||
|
||||
除 BDD/Gherkin 场景外,相关配套内容可放在:
|
||||
|
||||
- 实施计划:`.hermes/plans/<task-name>.md`
|
||||
- 产品/验收文档:`docs/prd/<FEATURE>_PRD_YYYY-MM-DD.md`
|
||||
- 技术设计:`docs/technical/<FEATURE>_TECHNICAL_YYYY-MM-DD.md`
|
||||
- 共享经验或稳定流程:`.hermes/shared-memory/` 或 `.hermes/skills/`
|
||||
|
||||
BDD 文档建议包含:
|
||||
|
||||
```markdown
|
||||
# <功能名> BDD 验收场景
|
||||
|
||||
## 背景
|
||||
- 需求来源:
|
||||
- 相关文档:
|
||||
- 相关入口/接口:
|
||||
|
||||
## 角色与目标
|
||||
- 角色:
|
||||
- 目标:
|
||||
- 非目标:
|
||||
|
||||
## 场景清单
|
||||
|
||||
### 功能: <能力>
|
||||
|
||||
```gherkin
|
||||
场景: <场景名>
|
||||
假如 ...
|
||||
当 ...
|
||||
那么 ...
|
||||
```
|
||||
|
||||
## 测试映射
|
||||
|
||||
| 场景 | 测试层级 | 目标文件 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| ... | component | ... | planned |
|
||||
|
||||
## 开放问题
|
||||
- ...
|
||||
```
|
||||
|
||||
注意:上面的 Markdown 模板中如果嵌套代码块,需要在真实文档里调整围栏长度,避免代码块提前闭合。
|
||||
|
||||
## 评审检查清单
|
||||
|
||||
- [ ] 每个场景都有清晰角色或业务上下文。
|
||||
- [ ] Given / When / Then 没有混入过多实现细节。
|
||||
- [ ] Then 都是可观察、可测试、可人工验收的结果。
|
||||
- [ ] 覆盖成功路径、失败路径、权限/登录态、边界条件。
|
||||
- [ ] 明确哪些场景需要自动化,哪些只需人工验收。
|
||||
- [ ] 自动化测试层级合理,没有把所有行为都塞进 E2E。
|
||||
- [ ] 中文文案、剧情、注释、文档没有被无意翻译或改写成英文。
|
||||
- [ ] 涉及中文文件修改时计划运行编码检查。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. **把 BDD 写成 UI 操作流水账。** 例如“点击第一个按钮,再点第二个按钮”。应改为用户意图和业务结果。
|
||||
2. **Then 不可验证。** “体验更顺滑”不是验收标准;要写成加载状态、错误提示、数据状态、页面阶段等可观察结果。
|
||||
3. **一个场景塞太多断言。** 长流程应拆成多个小场景,避免失败时不知道真正坏在哪里。
|
||||
4. **只写 happy path。** Genarrative 常见风险在登录态、刷新恢复、异步失败、端口/后端不可用、旧数据兼容和移动端布局。
|
||||
5. **把实现方案当成业务规则。** “调用某函数”通常不是用户行为;除非是 API/技术验收,否则放到技术设计或测试实现里。
|
||||
6. **BDD 和 TDD 脱节。** 写完场景后要映射测试层级和目标文件,否则场景容易停留在文档层。
|
||||
7. **场景词汇不统一。** work、draft、session、runtime、publish 等概念要和项目现有文档/代码保持一致。
|
||||
8. **忽略文档先行约束。** 若 PRD 不足以编码落地,先补文档,再开始工程修改。
|
||||
|
||||
## 验证与收口
|
||||
|
||||
执行 BDD 相关任务后,至少确认:
|
||||
|
||||
- [ ] 已产出或更新 BDD 场景文档/计划。
|
||||
- [ ] 场景已映射到具体测试层级和目标文件。
|
||||
- [ ] 若进入编码,已按 TDD 或等价方式先补测试。
|
||||
- [ ] 已运行相关验证命令,例如:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run test -- --run <相关测试文件>
|
||||
```
|
||||
|
||||
- [ ] 若涉及后端 Rust/API,按相关 DDD/SpacetimeDB 文档运行对应 cargo/npm/API smoke 验证。
|
||||
- [ ] 若产生长期有效经验,已同步到 `.hermes/shared-memory/` 或合适的仓库级 skill。
|
||||
@@ -165,15 +165,7 @@ npm run dev
|
||||
- `admin-web` 的 `/` 可能返回 302 跳转到 `/admin/`;验证前端时直接请求 `/admin/`。
|
||||
- api-server 启动日志中 SpacetimeDB `127.0.0.1:3101` 连接被拒绝,不一定代表 api-server 没起来;只表示依赖的本地 SpacetimeDB 不可用。后台中需要读 SpacetimeDB 的页面(如埋点明细、表查询)要等 SpacetimeDB 可用后才能返回真实数据。
|
||||
- `npm run dev` 依赖 `spacetime` CLI;先用 `command -v spacetime && spacetime --version` 确认可用。
|
||||
- WSL/Linux 下 SpacetimeDB CLI 2.2.0 使用项目内 `--root-dir` 时,standalone 可能会回调 `${root_dir}/bin/current/spacetimedb-cli`。如果报 `It seems like the spacetime version set as current may not exist` 或 `exec failed for .../.spacetimedb/local/bin/current/spacetimedb-cli`,把用户级安装同步到项目 root-dir:
|
||||
|
||||
```bash
|
||||
rm -rf server-rs/.spacetimedb/local/bin
|
||||
mkdir -p server-rs/.spacetimedb/local/bin
|
||||
cp -a ~/.local/share/spacetime/bin/2.2.0 server-rs/.spacetimedb/local/bin/2.2.0
|
||||
ln -sfn 2.2.0 server-rs/.spacetimedb/local/bin/current
|
||||
server-rs/.spacetimedb/local/bin/current/spacetimedb-cli --version
|
||||
```
|
||||
- 本地和人工排障不再使用 `spacetime --root-dir`。如果看到 `bin/current/spacetimedb-cli` 缺失类错误,优先确认是否仍在运行旧脚本或旧发布包;本地开发应使用 `npm run dev` / `npm run dev:rust`,通过项目脚本和 `--data-dir` 隔离 SpacetimeDB 数据目录,不再把用户级 SpacetimeDB 安装同步到项目目录。
|
||||
|
||||
- `scripts/dev-rust-stack.sh` 默认 `api timeout: 300s`. 合并 master 后首次 Rust 依赖/工作区重编译可能超过 300s,导致完整 `npm run dev` 在 api-server 就绪前超时并回收 SpacetimeDB。先让 Rust 编译完成,或临时用 `bash scripts/dev-rust-stack.sh --skip-spacetime --skip-publish --api-timeout-seconds 900` 预热 api-server 编译;之后再重新跑完整 `npm run dev`。
|
||||
- 用户贴出的 Hermes background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。
|
||||
@@ -219,7 +211,7 @@ npm install
|
||||
5. `adminRoutes` 新增 route id 后,`AdminShell.routeIcons` 必须同步,否则 TypeScript 会因 `satisfies Record<AdminRouteId, ...>` 报错。
|
||||
- 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`。
|
||||
- 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。
|
||||
- 若用户追问“之前不是说要把 npm run dev 修好吗”这类已承诺的 dev 启动问题,不要只解释;先复现 `npm run dev`,再按启动日志修脚本并验证到服务就绪。WSL/Linux 下 `spacetime start --root-dir=server-rs/.spacetimedb/local` 可能需要把用户级 SpacetimeDB 版本目录同步到项目 root-dir 的 `bin/<version>` 并建立 `bin/current`,详见 `references/dev-rust-stack-startup-2026-05-08.md`。
|
||||
- 若用户追问“之前不是说要把 npm run dev 修好吗”这类已承诺的 dev 启动问题,不要只解释;先复现 `npm run dev`,再按启动日志修脚本并验证到服务就绪。WSL/Linux 下本地开发应走 `spacetime start --data-dir=server-rs/.spacetimedb/local/data` 这一类数据目录隔离,不再用项目级 `--root-dir`,详见 `references/dev-rust-stack-startup-2026-05-08.md`。
|
||||
- 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`。
|
||||
|
||||
## 参考资料
|
||||
@@ -230,4 +222,4 @@ npm install
|
||||
- `references/spacetimedb-http-sql-sats-display.md`:通过 HTTP SQL 读取 private table 时,enum / Option / Timestamp 的 SATS 原始 rows 如何转换为后台列表、详情和 Excel 可读值。
|
||||
- `references/daily-login-tracking-trigger-points.md`:排查后台 `daily_login` 埋点为何不是登录接口写入,而是任务中心读取/领奖兜底写入的触发点记录。
|
||||
- `references/daily-login-auth-closure.md`:将方案A拆出的每日登录埋点入口接入真实认证成功链路时的推荐接入点、非阻断语义、测试和提交注意事项。
|
||||
- `references/dev-rust-stack-startup-2026-05-08.md`:`npm run dev` / `scripts/dev-rust-stack.sh` 在 WSL/Linux 下同步 SpacetimeDB root-dir 安装、避免 `bin/current/spacetimedb-cli` 缺失和冷编译超时的修复记录。
|
||||
- `references/dev-rust-stack-startup-2026-05-08.md`:`npm run dev` / `scripts/dev-rust-stack.sh` 在 WSL/Linux 下改用用户级 SpacetimeDB CLI、项目数据目录和显式 publish server,避免项目级 `--root-dir` 与冷编译超时的修复记录。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# `npm run dev` / `scripts/dev-rust-stack.sh` 启动修复记录
|
||||
|
||||
## 症状
|
||||
- 多个 worktree 同时本地开发时,SpacetimeDB 数据库名可能相同,早期用项目级 `--root-dir` 隔离 CLI 状态来规避冲突。
|
||||
- 实测后确认:真正需要隔离的是 standalone 的 `data-dir`,不需要把 publish 也绑到项目级 root-dir。
|
||||
- 多个 worktree 同时本地开发时,SpacetimeDB 数据库名可能相同,早期曾用项目级 CLI root 隔离 CLI 状态来规避冲突。
|
||||
- 实测后确认:真正需要隔离的是 standalone 的 `data-dir`,不需要把 publish 也绑到项目级 CLI root。
|
||||
- 早期脚本曾通过把用户级 SpacetimeDB 可执行文件目录同步到 `server-rs/.spacetimedb/local/bin/current` 来满足 standalone 回调需求,但这会把整套可执行文件复制进项目本地目录,维护成本高,也容易和用户级 CLI 版本漂移。
|
||||
- 多个 worktree 同时启动时,SpacetimeDB 端口可能冲突;CLI 会询问是否使用最近的可用端口。
|
||||
- 若 `npm run dev` 从仓库根目录直接执行 `spacetime publish --module-path server-rs/crates/spacetime-module`,内部 Cargo 不一定读取 `server-rs/.cargo/config.toml`,可能绕过 sccache/linker/target 缓存策略,表现为 spacetime-module 每次像全量重编译。
|
||||
@@ -11,7 +11,7 @@
|
||||
## 当前方案
|
||||
1. SpacetimeDB 可执行文件继续使用用户环境里的 `spacetime` 命令
|
||||
- 启动 standalone 时不再复制 `spacetimedb-cli`、版本目录或 `bin/current`。
|
||||
- `spacetime start` 不再通过工程内 `--root-dir` 寻找可执行文件。
|
||||
- `spacetime start` 不再通过工程内 CLI root 寻找可执行文件。
|
||||
2. 数据目录显式指定到项目本地
|
||||
- 默认 `SPACETIME_DATA_DIR=${SERVER_RS_DIR}/.spacetimedb/local/data`。
|
||||
- 启动命令使用 `spacetime start --data-dir "${SPACETIME_DATA_DIR}" --listen-addr ...`。
|
||||
@@ -21,9 +21,9 @@
|
||||
- 脚本向 `spacetime start` 发送回车,接受“最近可用端口”的默认建议。
|
||||
- 随后从启动日志中的 `Starting SpacetimeDB listening on ...` 解析实际端口。
|
||||
- 解析出的实际端口会覆盖 `SPACETIME_SERVER`,后续 publish、api-server、前端代理统一使用这个端口。
|
||||
4. publish 不再使用项目级 root-dir,但要从 `server-rs` 目录执行
|
||||
4. publish 不再使用项目级 CLI root,但要从 `server-rs` 目录执行
|
||||
- 发布模块改为在 `server-rs` 下执行 `spacetime publish ... --server "${SPACETIME_SERVER}" ...`。
|
||||
- 这样 publish 使用用户级 CLI 默认身份/配置,不再依赖 worktree 内 root-dir。
|
||||
- 这样 publish 使用用户级 CLI 默认身份/配置,不再依赖 worktree 内 CLI root。
|
||||
- 同时确保内部 Cargo 能读取 `server-rs/.cargo/config.toml`,复用项目级 sccache/linker/target 缓存策略,避免 `npm run dev` 比手动 publish 更容易触发慢速重编译。
|
||||
5. 提高 api-server 就绪等待时间
|
||||
- `API_SERVER_TIMEOUT_SECONDS` 保持 600,降低首次冷编译误判失败概率。
|
||||
@@ -33,7 +33,7 @@
|
||||
- 运行帮助检查:`bash scripts/dev-rust-stack.sh --help`,确认有 `--spacetime-data-dir`。
|
||||
- 运行 `npm run dev` 后观察日志:
|
||||
- 输出 `spacetime data: .../server-rs/.spacetimedb/local/data`。
|
||||
- 不再出现同步/复制本机 SpacetimeDB 安装到项目 root-dir 的日志。
|
||||
- 不再出现同步/复制本机 SpacetimeDB 安装到项目 CLI root 的日志。
|
||||
- SpacetimeDB 能正常监听,并输出 `spacetime actual: http://127.0.0.1:<实际端口>`。
|
||||
- 若默认端口被占用,脚本应自动接受 SpacetimeDB 建议端口,并用实际端口发布模块、启动 api-server 和前端代理。
|
||||
- 模块发布成功。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
操作步骤:
|
||||
|
||||
1. 清空本地 SpacetimeDB 数据目录
|
||||
- 使用:`spacetime --root-dir=<root> server clear --yes`
|
||||
- 使用项目脚本停止本地实例后,备份或删除 `server-rs/.spacetimedb/local/data`。
|
||||
- 只清本地开发环境,不要误伤远端或其他 worktree。
|
||||
|
||||
2. 启动本地 standalone
|
||||
@@ -19,7 +19,7 @@
|
||||
- 只记录 identity,不要在日志中打印 token 明文。
|
||||
|
||||
4. 用新 token 登录 CLI
|
||||
- 运行:`spacetime --root-dir=<root> login --token <token>`
|
||||
- 运行:`spacetime login --token <token>`
|
||||
- 这会把 token 写到本地 CLI 配置,后续 HTTP SQL 可读 private table。
|
||||
|
||||
5. 重新验证 SQL
|
||||
|
||||
127
.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md
Normal file
127
.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: genarrative-dev-stack-port-routing
|
||||
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
|
||||
description: 在 Genarrative 中修改 npm run dev / npm run dev:rust / npm run dev:web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, dev-stack, 端口探测, Vite, api-server, SpacetimeDB, npm-run-dev]
|
||||
related_skills: [genarrative-admin-backoffice]
|
||||
---
|
||||
|
||||
# Genarrative 本地 dev 启动端口与代理目标串联流程
|
||||
|
||||
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` / `npm run dev:rust` / `npm run dev:web` 的端口检查、端口漂移和后续流程目标传递。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 修改 `scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`scripts/dev-stack-port-utils.mjs`。
|
||||
- 处理 `3000`、`3101`、`3102`、`8082` 等端口被占用导致本地开发栈启动失败。
|
||||
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
|
||||
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
|
||||
- 修改本地联调文档或 `.hermes/shared-memory/pitfalls.md` 中的 dev 启动口径。
|
||||
|
||||
## 当前端口职责
|
||||
|
||||
默认优先端口:
|
||||
|
||||
1. 主站 Vite:`3000`,对浏览器通常展示为 `http://127.0.0.1:<web-port>/`。
|
||||
2. Rust `api-server`:`8082`,健康检查为 `http://127.0.0.1:<api-port>/healthz`。
|
||||
3. SpacetimeDB standalone:`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`。
|
||||
4. 后台 Vite:`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`。
|
||||
|
||||
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
|
||||
|
||||
## 实现入口
|
||||
|
||||
- `package.json`
|
||||
- `dev` 和 `dev:rust`:执行 `node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh`。
|
||||
- `dev:web`:执行 `node scripts/dev-web-rust.mjs`。
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `isPortAvailable(...)`:探测端口是否可监听。
|
||||
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
|
||||
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
|
||||
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102`。
|
||||
- `scripts/dev-rust-stack.sh`
|
||||
- 解析 CLI 参数后,先计算 `API_TARGET_HOST` 与 `ADMIN_WEB_TARGET_HOST`。
|
||||
- 调用 `resolve_dev_stack_ports` 覆盖 `SPACETIME_PORT`、`API_PORT`、`WEB_PORT`、`ADMIN_WEB_PORT`。
|
||||
- 再构造 `SPACETIME_SERVER` 和 `RUST_SERVER_TARGET`。
|
||||
- `scripts/dev-web-rust.mjs`
|
||||
- 单独启动主站前端时,也先用 `findAvailablePort` 检查 `WEB_PORT` / 默认 `3000`。
|
||||
|
||||
## 必须保持的传递链路
|
||||
|
||||
`npm run dev` / `npm run dev:rust` 中端口解析后,必须同步到以下位置:
|
||||
|
||||
1. SpacetimeDB 启动:`spacetime start --listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"`。
|
||||
2. SpacetimeDB 发布:`spacetime publish ... --server "${SPACETIME_SERVER}"`。
|
||||
3. Rust api-server:`GENARRATIVE_API_HOST`、`GENARRATIVE_API_PORT`、`GENARRATIVE_SPACETIME_SERVER_URL`、`GENARRATIVE_SPACETIME_DATABASE`。
|
||||
4. api-server 健康检查:`wait_for_api_server "${RUST_SERVER_TARGET}/healthz" ...`。
|
||||
5. 主站 Vite:`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET`、`ADMIN_WEB_TARGET`、`ADMIN_WEB_PORT`、`--port=${WEB_PORT}`、`--host=${WEB_HOST}`。
|
||||
6. 后台 Vite:`ADMIN_API_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_API_PORT`、`--port=${ADMIN_WEB_PORT}`。
|
||||
7. 控制台日志:`[dev:ports]` 和 `[dev:rust] web/admin web/rust api/spacetime` 必须显示最终实际地址。
|
||||
|
||||
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
|
||||
|
||||
## 修改流程
|
||||
|
||||
1. 先读当前脚本和文档:
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `scripts/dev-rust-stack.sh`
|
||||
- `scripts/dev-web-rust.mjs`
|
||||
- `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
|
||||
- `.hermes/shared-memory/pitfalls.md`
|
||||
2. 优先改公共端口工具,不要把端口探测逻辑复制到多个脚本。
|
||||
3. 对 Bash 脚本只做局部补丁,避免整文件重写导致中文注释或换行大面积变化。
|
||||
4. 修改 `dev-rust-stack.sh` 时确认变量顺序:
|
||||
- 先有 `REPO_ROOT`。
|
||||
- 再计算 `API_TARGET_HOST` / `ADMIN_WEB_TARGET_HOST`。
|
||||
- 再调用端口解析工具。
|
||||
- 再构造 `SPACETIME_SERVER` / `RUST_SERVER_TARGET`。
|
||||
5. 修改 `dev:web` 时不要自动改后端目标策略;`dev:web` 只负责主站 Vite 端口可用性与已有后端目标选择。
|
||||
6. 同步更新技术文档和团队共享记忆。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
最小验证:
|
||||
|
||||
```bash
|
||||
bash -n scripts/dev-rust-stack.sh
|
||||
npm run test -- scripts/dev-stack-port-utils.test.ts
|
||||
npm run check:encoding
|
||||
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0
|
||||
```
|
||||
|
||||
端口冲突回归测试建议:
|
||||
|
||||
1. 用测试或临时 Node server 占用某个优先端口。
|
||||
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
|
||||
3. 调用 `resolveDevStackPorts`,断言四个结果互不相同。
|
||||
4. 如果实际启动完整栈,观察控制台:
|
||||
- `[dev:ports] ... 不可用,改用 ...`
|
||||
- `[dev:rust] rust api: http://...:<actual-api-port>`
|
||||
- `[dev:rust] spacetime: http://...:<actual-spacetime-port>`
|
||||
- 主站和后台 Vite 启动端口与日志一致。
|
||||
|
||||
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 `/healthz`、`/v1/ping` 和页面端口;不要等待 `npm run dev` 自然退出。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. **只让 Vite 自己漂移端口。** 这样终端可能出现可访问前端,但脚本和文档仍认为是 `3000`,后台目标或日志会错。
|
||||
2. **只改 SpacetimeDB start,不改 publish。** standalone 可能监听新端口,但 publish 仍连旧 `3101`。
|
||||
3. **只改 `GENARRATIVE_API_PORT`,不改 `RUST_SERVER_TARGET`。** api-server 已在新端口监听,但 Vite 代理仍打旧端口。
|
||||
4. **使用 `0.0.0.0` 作为浏览器访问地址。** 监听可以是 `0.0.0.0`,展示给用户和健康检查通常用 `127.0.0.1`。
|
||||
5. **端口探测和实际启动之间存在竞态。** 已经探测可用的端口仍可能被外部进程抢占;SpacetimeDB 启动后仍要解析实际监听地址,api-server 和 Vite 失败时要打印清晰日志。
|
||||
6. **运行全仓库 lint 误判。** 当前仓库可能有既有 lint 问题。验证本功能时优先运行定向测试、Bash 语法检查、编码检查,并在最终说明中区分既有 lint 失败与本次改动。
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] 端口工具有测试覆盖端口被占用和多端口互斥解析。
|
||||
- [ ] `dev-rust-stack.sh` 通过 `bash -n`。
|
||||
- [ ] `npm run dev` / `npm run dev:rust` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
|
||||
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
|
||||
- [ ] 文档同步更新 `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`。
|
||||
- [ ] 长期踩坑同步更新 `.hermes/shared-memory/pitfalls.md`。
|
||||
- [ ] 修改中文文件后运行 `npm run check:encoding`。
|
||||
@@ -40,12 +40,14 @@
|
||||
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影或后端 API 直接实现业务规则。
|
||||
- 修改后端代码后,按对应 DDD 文档中的验收命令执行测试;涉及 API smoke 时使用 `npm run api-server` 重新拉起后端并执行相应自动测试,同时确认 `/healthz`。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关脚本、环境变量、测试、文档要求和命名全部视为历史残留,禁止新增、运行或引用;若旧文档仍要求 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`,以 [`docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`](docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md) 和本文件为准,并先修正文档口径。
|
||||
- 除 CI/CD 脚本内部受控用法外,人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`。本地数据隔离使用项目脚本或 `--data-dir`,发布目标必须显式传 `--server` / `--server-url`,身份问题通过同一 CLI 登录态、专用运行用户或显式 token 处理;若旧文档仍推荐 `--root-dir`,先修正文档口径再执行。
|
||||
- 凡是涉及 SpacetimeDB 的设计、实现、脚本、调试、前端绑定接入,统一显式使用以下 skill 作为执行依据:
|
||||
- [$spacetimedb-cli](.codex\\skills\\spacetimedb-cli\\SKILL.md)
|
||||
- [$spacetimedb-rust](.codex\\skills\\spacetimedb-rust\\SKILL.md)
|
||||
- [$spacetimedb-concepts](.codex\\skills\\spacetimedb-concepts\\SKILL.md)
|
||||
- [$spacetimedb-typescript](.codex\\skills\\spacetimedb-typescript\\SKILL.md)
|
||||
- 涉及 `spacetime` CLI、发布、绑定生成、本地联调时,按 `spacetimedb-cli` 执行。
|
||||
- 涉及 `npm run dev` / `npm run dev:rust` / `npm run dev:web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
|
||||
- 涉及 `crates/spacetime-module` 的表、reducer、view、Rust API 使用时,按 `spacetimedb-rust` 与 `spacetimedb-concepts` 执行。
|
||||
- 涉及前端或 Node 侧的 SpacetimeDB TypeScript SDK、订阅、绑定使用时,按 `spacetimedb-typescript` 与 `spacetimedb-concepts` 执行。
|
||||
- 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type {
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
AdminCreationEntryConfigResponse,
|
||||
AdminDebugHttpRequest,
|
||||
AdminDebugHttpResponse,
|
||||
AdminDisableProfileRedeemCodeRequest,
|
||||
@@ -167,6 +169,28 @@ export function listAdminTrackingEvents(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function getAdminCreationEntryConfig(token: string) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config',
|
||||
{token},
|
||||
);
|
||||
}
|
||||
|
||||
export function upsertAdminCreationEntryConfig(
|
||||
token: string,
|
||||
payload: AdminUpsertCreationEntryTypeConfigRequest,
|
||||
) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function listProfileRedeemCodes(token: string) {
|
||||
return request<ProfileRedeemCodeAdminListResponse>(
|
||||
'/admin/api/profile/redeem-codes',
|
||||
|
||||
@@ -141,6 +141,34 @@ export interface AdminTrackingEventListQuery {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface AdminCreationEntryConfigResponse {
|
||||
entries: AdminCreationEntryTypeConfigPayload[];
|
||||
}
|
||||
|
||||
export interface AdminCreationEntryTypeConfigPayload {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
imageSrc: string;
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
sortOrder: number;
|
||||
updatedAtMicros: number;
|
||||
}
|
||||
|
||||
export interface AdminUpsertCreationEntryTypeConfigRequest {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
imageSrc: string;
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface AdminUpsertProfileRedeemCodeRequest {
|
||||
code: string;
|
||||
mode: ProfileRedeemCodeMode;
|
||||
@@ -154,6 +182,7 @@ export interface AdminUpsertProfileRedeemCodeRequest {
|
||||
export interface AdminUpsertProfileInviteCodeRequest {
|
||||
inviteCode: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
grantedUserTags: string[];
|
||||
startsAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
}
|
||||
@@ -200,6 +229,7 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
userId: string;
|
||||
inviteCode: string;
|
||||
metadata: Record<string, unknown>;
|
||||
grantedUserTags: string[];
|
||||
startsAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
status: 'pending' | 'active' | 'expired';
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getStoredAdminToken,
|
||||
setStoredAdminToken,
|
||||
} from '../auth/adminAuthStore';
|
||||
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
|
||||
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
|
||||
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
|
||||
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
||||
@@ -192,6 +193,12 @@ export function AdminApp() {
|
||||
onResultChange={setInviteResult}
|
||||
/>
|
||||
) : null}
|
||||
{routeId === 'creation-entry' ? (
|
||||
<AdminCreationEntrySwitchPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{routeId === 'tasks' ? (
|
||||
<AdminTaskConfigPage
|
||||
result={taskConfigResult}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
LogOut,
|
||||
ShieldCheck,
|
||||
ListChecks,
|
||||
SlidersHorizontal,
|
||||
Database,
|
||||
Table2,
|
||||
TicketCheck,
|
||||
@@ -31,6 +32,7 @@ const routeIcons = {
|
||||
redeem: TicketPercent,
|
||||
invite: TicketCheck,
|
||||
tasks: ListChecks,
|
||||
'creation-entry': SlidersHorizontal,
|
||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||
|
||||
export function AdminShell({
|
||||
|
||||
@@ -5,7 +5,8 @@ export type AdminRouteId =
|
||||
| 'tracking'
|
||||
| 'redeem'
|
||||
| 'invite'
|
||||
| 'tasks';
|
||||
| 'tasks'
|
||||
| 'creation-entry';
|
||||
|
||||
export interface AdminRouteDefinition {
|
||||
id: AdminRouteId;
|
||||
@@ -21,6 +22,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
||||
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
||||
{id: 'invite', label: '邀请码', hash: '#invite'},
|
||||
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
||||
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
||||
];
|
||||
|
||||
export function resolveAdminRoute(hash: string): AdminRouteId {
|
||||
|
||||
42
apps/admin-web/src/config/trackingEventDefinitions.test.ts
Normal file
42
apps/admin-web/src/config/trackingEventDefinitions.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
import {
|
||||
adminProfileTaskTrackingEventDefinitions,
|
||||
adminTrackingEventDefinitions,
|
||||
filterAdminProfileTaskTrackingEventDefinitions,
|
||||
filterAdminTrackingEventDefinitions,
|
||||
findAdminTrackingEventDefinition,
|
||||
} from './trackingEventDefinitions';
|
||||
|
||||
describe('admin tracking event definitions', () => {
|
||||
test('后台埋点筛选候选包含后端通用埋点清单', () => {
|
||||
const keys = adminTrackingEventDefinitions.map((definition) => definition.key);
|
||||
|
||||
expect(keys.length).toBeGreaterThan(40);
|
||||
expect(keys).toContain('daily_login');
|
||||
expect(keys).toContain('auth_login_options_view');
|
||||
expect(keys).toContain('task_center_view');
|
||||
expect(keys).toContain('asset_upload_ticket_create');
|
||||
expect(keys).toContain('creative_agent_route_success');
|
||||
expect(keys).toContain('work_play_start');
|
||||
});
|
||||
|
||||
test('任务配置候选只开放适合个人任务的事件', () => {
|
||||
expect(adminProfileTaskTrackingEventDefinitions.map(({key}) => key)).toEqual([
|
||||
'daily_login',
|
||||
]);
|
||||
expect(filterAdminProfileTaskTrackingEventDefinitions('').map(({key}) => key)).toEqual([
|
||||
'daily_login',
|
||||
]);
|
||||
});
|
||||
|
||||
test('后台埋点筛选支持按中文名称和 key 搜索', () => {
|
||||
expect(filterAdminTrackingEventDefinitions('上传票据').map(({key}) => key)).toEqual([
|
||||
'asset_upload_ticket_create',
|
||||
]);
|
||||
expect(filterAdminTrackingEventDefinitions('work_play').map(({key}) => key)).toEqual([
|
||||
'work_play_start',
|
||||
]);
|
||||
expect(findAdminTrackingEventDefinition(' daily_login ')?.title).toBe('每日登录');
|
||||
});
|
||||
});
|
||||
@@ -5,17 +5,400 @@ export interface AdminTrackingEventDefinition {
|
||||
title: string;
|
||||
scopeKind: TrackingScopeKind;
|
||||
remark: string;
|
||||
taskConfigEligible?: boolean;
|
||||
}
|
||||
|
||||
export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
{
|
||||
key: 'auth_login_options_view',
|
||||
title: '登录方式查看',
|
||||
scopeKind: 'site',
|
||||
remark: '读取当前可用登录方式时记录,用于观察登录入口曝光。',
|
||||
},
|
||||
{
|
||||
key: 'auth_phone_code_send',
|
||||
title: '发送手机验证码',
|
||||
scopeKind: 'site',
|
||||
remark: '提交手机验证码发送请求成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'daily_login',
|
||||
title: '每日登录',
|
||||
scopeKind: 'user',
|
||||
remark: '用户打开任务中心时由后端幂等记录,用于每日登录任务进度校验。',
|
||||
remark: '认证成功或 refresh 续期后由后端幂等记录,用于每日登录任务进度校验。',
|
||||
taskConfigEligible: true,
|
||||
},
|
||||
{
|
||||
key: 'auth_phone_login_success',
|
||||
title: '手机号登录成功',
|
||||
scopeKind: 'user',
|
||||
remark: '手机号验证码登录成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_me_view',
|
||||
title: '当前账号查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取当前登录账号信息成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_sessions_view',
|
||||
title: '登录会话查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取当前账号登录会话列表成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_refresh_success',
|
||||
title: '登录续期成功',
|
||||
scopeKind: 'site',
|
||||
remark: 'refresh cookie 续期成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_logout',
|
||||
title: '退出登录',
|
||||
scopeKind: 'user',
|
||||
remark: '退出当前登录会话成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_logout_all',
|
||||
title: '退出全部会话',
|
||||
scopeKind: 'user',
|
||||
remark: '退出全部登录会话成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'auth_wechat_bind_phone_success',
|
||||
title: '微信绑定手机成功',
|
||||
scopeKind: 'user',
|
||||
remark: '微信账号绑定手机号成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'profile_identity_update',
|
||||
title: '资料更新',
|
||||
scopeKind: 'user',
|
||||
remark: '用户资料更新成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'profile_dashboard_view',
|
||||
title: '个人看板查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人看板成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'wallet_ledger_view',
|
||||
title: '钱包流水查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取光点钱包流水成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'recharge_center_view',
|
||||
title: '充值中心查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取充值中心信息成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'recharge_order_create',
|
||||
title: '充值订单创建',
|
||||
scopeKind: 'user',
|
||||
remark: '创建充值订单成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'feedback_submit',
|
||||
title: '反馈提交',
|
||||
scopeKind: 'user',
|
||||
remark: '资料页反馈提交成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'invite_center_view',
|
||||
title: '邀请中心查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取邀请中心成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'referral_invite_code_redeem',
|
||||
title: '邀请码绑定',
|
||||
scopeKind: 'user',
|
||||
remark: '绑定推荐邀请码成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'redeem_code_submit',
|
||||
title: '兑换码提交',
|
||||
scopeKind: 'user',
|
||||
remark: '提交运营兑换码成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'task_center_view',
|
||||
title: '任务中心查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人任务中心成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'task_reward_claim',
|
||||
title: '任务奖励领取',
|
||||
scopeKind: 'user',
|
||||
remark: '领取个人任务奖励成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'save_archive_list_view',
|
||||
title: '存档列表查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人存档列表成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'save_archive_detail_view',
|
||||
title: '存档详情查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人存档详情成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_view',
|
||||
title: '浏览历史查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_record',
|
||||
title: '浏览历史写入',
|
||||
scopeKind: 'user',
|
||||
remark: '记录浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_clear',
|
||||
title: '浏览历史清空',
|
||||
scopeKind: 'user',
|
||||
remark: '清空浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'play_stats_view',
|
||||
title: '游玩统计查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人游玩统计成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'profile_analytics_metric_view',
|
||||
title: '个人指标查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人埋点指标成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_create',
|
||||
title: 'AI 任务创建',
|
||||
scopeKind: 'user',
|
||||
remark: '创建 AI 任务成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_start',
|
||||
title: 'AI 任务启动',
|
||||
scopeKind: 'user',
|
||||
remark: '启动 AI 任务成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_stage_start',
|
||||
title: 'AI 阶段启动',
|
||||
scopeKind: 'user',
|
||||
remark: '启动 AI 任务阶段成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_chunk_append',
|
||||
title: 'AI 分片追加',
|
||||
scopeKind: 'user',
|
||||
remark: '追加 AI 文本分片成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_stage_complete',
|
||||
title: 'AI 阶段完成',
|
||||
scopeKind: 'user',
|
||||
remark: '完成 AI 任务阶段成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_reference_attach',
|
||||
title: 'AI 结果引用绑定',
|
||||
scopeKind: 'user',
|
||||
remark: '绑定 AI 结果引用成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_complete',
|
||||
title: 'AI 任务完成',
|
||||
scopeKind: 'user',
|
||||
remark: '完成 AI 任务成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_fail',
|
||||
title: 'AI 任务失败标记',
|
||||
scopeKind: 'user',
|
||||
remark: '标记 AI 任务失败成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'ai_task_cancel',
|
||||
title: 'AI 任务取消',
|
||||
scopeKind: 'user',
|
||||
remark: '取消 AI 任务成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_upload_ticket_create',
|
||||
title: '资产上传票据创建',
|
||||
scopeKind: 'user',
|
||||
remark: '创建直传票据成功后记录,metadata 包含低敏资产定位字段。',
|
||||
},
|
||||
{
|
||||
key: 'asset_sts_credentials_create',
|
||||
title: '资产 STS 凭证创建',
|
||||
scopeKind: 'user',
|
||||
remark: '创建临时上传凭证成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_upload_confirm',
|
||||
title: '资产上传确认',
|
||||
scopeKind: 'user',
|
||||
remark: '确认 OSS 对象为平台资产成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_bind',
|
||||
title: '资产绑定',
|
||||
scopeKind: 'user',
|
||||
remark: '把平台资产绑定到业务实体成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_visual_generate',
|
||||
title: '角色形象生成',
|
||||
scopeKind: 'user',
|
||||
remark: '角色主图生成请求成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_visual_publish',
|
||||
title: '角色形象发布',
|
||||
scopeKind: 'user',
|
||||
remark: '角色主图发布成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_animation_generate',
|
||||
title: '角色动画生成',
|
||||
scopeKind: 'user',
|
||||
remark: '角色动画生成请求成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_animation_publish',
|
||||
title: '角色动画发布',
|
||||
scopeKind: 'user',
|
||||
remark: '角色动画发布成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_animation_import',
|
||||
title: '角色动画视频导入',
|
||||
scopeKind: 'user',
|
||||
remark: '角色动画导入视频成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_character_workflow_cache_save',
|
||||
title: '角色工作流缓存保存',
|
||||
scopeKind: 'user',
|
||||
remark: '保存角色工作流缓存成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asset_history_view',
|
||||
title: '资产历史查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取资产历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'llm_request',
|
||||
title: 'LLM 请求',
|
||||
scopeKind: 'user',
|
||||
remark: '调用后端 LLM 门面成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'speech_config_view',
|
||||
title: '语音配置查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取语音配置成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'asr_stream_start',
|
||||
title: 'ASR 流启动',
|
||||
scopeKind: 'user',
|
||||
remark: '启动语音识别流成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'tts_bidirection_start',
|
||||
title: 'TTS 双向流启动',
|
||||
scopeKind: 'user',
|
||||
remark: '启动双向语音合成流成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'tts_sse_start',
|
||||
title: 'TTS SSE 启动',
|
||||
scopeKind: 'user',
|
||||
remark: '启动 SSE 语音合成成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_settings_view',
|
||||
title: '运行设置查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取运行设置成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_settings_update',
|
||||
title: '运行设置更新',
|
||||
scopeKind: 'user',
|
||||
remark: '更新运行设置成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_view',
|
||||
title: '运行快照查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_save',
|
||||
title: '运行快照保存',
|
||||
scopeKind: 'user',
|
||||
remark: '保存运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_delete',
|
||||
title: '运行快照删除',
|
||||
scopeKind: 'user',
|
||||
remark: '删除运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'puzzle_route_success',
|
||||
title: '拼图路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '拼图运行或创作接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'match3d_route_success',
|
||||
title: '抓大鹅路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '抓大鹅创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'square_hole_route_success',
|
||||
title: '方洞路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '方洞创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'custom_world_route_success',
|
||||
title: '自定义世界路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '自定义世界运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'creative_agent_route_success',
|
||||
title: '创意 Agent 路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '创意 Agent 接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'work_play_start',
|
||||
title: '作品开始游玩',
|
||||
scopeKind: 'work',
|
||||
remark: '拼图、抓大鹅、方洞、自定义世界、大鱼吃小鱼、Visual Novel 正式开始游玩时记录。',
|
||||
},
|
||||
];
|
||||
|
||||
export const adminProfileTaskTrackingEventDefinitions =
|
||||
adminTrackingEventDefinitions.filter((definition) => definition.taskConfigEligible);
|
||||
|
||||
export function findAdminTrackingEventDefinition(eventKey: string) {
|
||||
const normalizedEventKey = eventKey.trim();
|
||||
return (
|
||||
@@ -26,12 +409,26 @@ export function findAdminTrackingEventDefinition(eventKey: string) {
|
||||
}
|
||||
|
||||
export function filterAdminTrackingEventDefinitions(query: string) {
|
||||
return filterTrackingEventDefinitions(adminTrackingEventDefinitions, query);
|
||||
}
|
||||
|
||||
export function filterAdminProfileTaskTrackingEventDefinitions(query: string) {
|
||||
return filterTrackingEventDefinitions(
|
||||
adminProfileTaskTrackingEventDefinitions,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
function filterTrackingEventDefinitions(
|
||||
definitions: AdminTrackingEventDefinition[],
|
||||
query: string,
|
||||
) {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return adminTrackingEventDefinitions;
|
||||
return definitions;
|
||||
}
|
||||
|
||||
return adminTrackingEventDefinitions.filter((definition) => {
|
||||
return definitions.filter((definition) => {
|
||||
const haystack = [
|
||||
definition.key,
|
||||
definition.title,
|
||||
|
||||
260
apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx
Normal file
260
apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import {RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
|
||||
import {
|
||||
getAdminCreationEntryConfig,
|
||||
upsertAdminCreationEntryConfig,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {handlePageError} from './pageUtils';
|
||||
|
||||
interface AdminCreationEntrySwitchPageProps {
|
||||
token: string;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
}
|
||||
|
||||
export function AdminCreationEntrySwitchPage({
|
||||
token,
|
||||
onUnauthorized,
|
||||
}: AdminCreationEntrySwitchPageProps) {
|
||||
const [entries, setEntries] = useState<AdminCreationEntryTypeConfigPayload[]>([]);
|
||||
const [selectedId, setSelectedId] = useState('puzzle');
|
||||
const [title, setTitle] = useState('');
|
||||
const [subtitle, setSubtitle] = useState('');
|
||||
const [badge, setBadge] = useState('');
|
||||
const [imageSrc, setImageSrc] = useState('');
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [sortOrder, setSortOrder] = useState('30');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshEntries();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
async function refreshEntries() {
|
||||
setIsLoading(true);
|
||||
setListErrorMessage('');
|
||||
try {
|
||||
const response = await getAdminCreationEntryConfig(token);
|
||||
const nextEntries = sortEntries(response.entries);
|
||||
setEntries(nextEntries);
|
||||
fillForm(
|
||||
nextEntries.find((entry) => entry.id === selectedId) ?? nextEntries[0] ?? null,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setListErrorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = selectedId.trim();
|
||||
setErrorMessage('');
|
||||
const confirmed = await confirmWrite({
|
||||
action: '保存创作入口开关',
|
||||
target: targetId,
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await upsertAdminCreationEntryConfig(token, {
|
||||
id: targetId,
|
||||
title: title.trim(),
|
||||
subtitle: subtitle.trim(),
|
||||
badge: badge.trim(),
|
||||
imageSrc: imageSrc.trim(),
|
||||
visible,
|
||||
open,
|
||||
sortOrder: parseInteger(sortOrder),
|
||||
});
|
||||
const nextEntries = sortEntries(response.entries);
|
||||
setEntries(nextEntries);
|
||||
fillForm(nextEntries.find((entry) => entry.id === targetId) ?? null);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function fillForm(entry: AdminCreationEntryTypeConfigPayload | null) {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
setSelectedId(entry.id);
|
||||
setTitle(entry.title);
|
||||
setSubtitle(entry.subtitle);
|
||||
setBadge(entry.badge);
|
||||
setImageSrc(entry.imageSrc);
|
||||
setVisible(entry.visible);
|
||||
setOpen(entry.open);
|
||||
setSortOrder(String(entry.sortOrder));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-heading">
|
||||
<div>
|
||||
<h2>创作入口开关</h2>
|
||||
<p>控制创作中心入口展示与运行态路由可用性</p>
|
||||
</div>
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
onClick={refreshEntries}
|
||||
>
|
||||
<RefreshCcw size={17} aria-hidden="true" />
|
||||
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{listErrorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{listErrorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-two-column admin-two-column-wide">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field admin-field-fill">
|
||||
<span>入口 ID</span>
|
||||
<input
|
||||
value={selectedId}
|
||||
onChange={(event) => setSelectedId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={visible}
|
||||
type="checkbox"
|
||||
onChange={(event) => setVisible(event.target.checked)}
|
||||
/>
|
||||
<span>展示</span>
|
||||
</label>
|
||||
<label className="admin-switch-field">
|
||||
<input
|
||||
checked={open}
|
||||
type="checkbox"
|
||||
onChange={(event) => setOpen(event.target.checked)}
|
||||
/>
|
||||
<span>开放</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field">
|
||||
<span>标题</span>
|
||||
<input value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>角标</span>
|
||||
<input value={badge} onChange={(event) => setBadge(event.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>副标题</span>
|
||||
<input value={subtitle} onChange={(event) => setSubtitle(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>图片路径</span>
|
||||
<input value={imageSrc} onChange={(event) => setImageSrc(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>排序</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={sortOrder}
|
||||
onChange={(event) => setSortOrder(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button className="admin-primary-button" disabled={isSaving} type="submit">
|
||||
<Save size={17} aria-hidden="true" />
|
||||
<span>{isSaving ? '保存中' : '保存入库'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section className="admin-panel">
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table admin-table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>入口</th>
|
||||
<th>展示</th>
|
||||
<th>开放</th>
|
||||
<th>排序</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>
|
||||
<button
|
||||
className="admin-link-button"
|
||||
type="button"
|
||||
onClick={() => fillForm(entry)}
|
||||
>
|
||||
{entry.title || entry.id}
|
||||
</button>
|
||||
</td>
|
||||
<td>{entry.visible ? '是' : '否'}</td>
|
||||
<td>{entry.open ? '是' : '否'}</td>
|
||||
<td>{entry.sortOrder}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{confirmDialog}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function sortEntries(entries: AdminCreationEntryTypeConfigPayload[]) {
|
||||
return [...entries].sort((left, right) => {
|
||||
if (left.sortOrder !== right.sortOrder) {
|
||||
return left.sortOrder - right.sortOrder;
|
||||
}
|
||||
return left.id.localeCompare(right.id);
|
||||
});
|
||||
}
|
||||
|
||||
function parseInteger(value: string) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
107
apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx
Normal file
107
apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {render, screen, waitFor} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminDatabaseTableRows,
|
||||
getAdminDatabaseTables,
|
||||
} from '../api/adminApiClient';
|
||||
import {AdminDatabaseTablesPage} from './AdminDatabaseTablesPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
formatAdminApiError: vi.fn((error: unknown) =>
|
||||
error instanceof Error ? error.message : '请求失败',
|
||||
),
|
||||
getAdminDatabaseTableRows: vi.fn(),
|
||||
getAdminDatabaseTables: vi.fn(),
|
||||
isAdminApiError: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
window.location.hash = '#tables?table=profile_referral_relation';
|
||||
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
|
||||
fetchErrors: [],
|
||||
tables: ['profile_referral_relation'],
|
||||
});
|
||||
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
|
||||
columns: ['invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at'],
|
||||
limit: 100,
|
||||
rows: [
|
||||
{
|
||||
cells: {
|
||||
bound_at: '2026-05-02T00:00:00Z',
|
||||
invitee_user_id: 'u-b',
|
||||
invite_code: 'INV-1001',
|
||||
inviter_user_id: 'u-a',
|
||||
},
|
||||
raw: [
|
||||
'u-b',
|
||||
'u-a',
|
||||
'INV-1001',
|
||||
'2026-05-02T00:00:00Z',
|
||||
],
|
||||
},
|
||||
{
|
||||
cells: {
|
||||
bound_at: '2026-05-01T00:00:00Z',
|
||||
invitee_user_id: 'u-a',
|
||||
invite_code: 'INV-1002',
|
||||
inviter_user_id: 'u-c',
|
||||
},
|
||||
raw: ['u-a', 'u-c', 'INV-1002', '2026-05-01T00:00:00Z'],
|
||||
},
|
||||
{
|
||||
cells: {
|
||||
bound_at: '2026-05-03T00:00:00Z',
|
||||
invitee_user_id: 'u-c',
|
||||
invite_code: 'INV-1003',
|
||||
inviter_user_id: 'u-a',
|
||||
},
|
||||
raw: ['u-c', 'u-a', 'INV-1003', '2026-05-03T00:00:00Z'],
|
||||
},
|
||||
],
|
||||
tableName: 'profile_referral_relation',
|
||||
totalReturned: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('后台表查询页支持宽表滚动容器和表头排序', async () => {
|
||||
const user = userEvent.setup();
|
||||
const {container} = render(
|
||||
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
await screen.findByText('u-b');
|
||||
|
||||
const tableWrap = container.querySelector('.admin-table-wrap');
|
||||
expect(tableWrap?.querySelector('.admin-database-table')).not.toBeNull();
|
||||
expect(screen.getByRole('option', {name: '邀请关系(profile_referral_relation)'}).getAttribute('title')).toBe(
|
||||
'原始表名:profile_referral_relation。邀请关系记录表。',
|
||||
);
|
||||
expect(screen.getByText('已选表:邀请关系(profile_referral_relation)')).toBeTruthy();
|
||||
expect(screen.getByRole('heading', {name: '邀请关系'}).getAttribute('title')).toBe(
|
||||
'原始表名:profile_referral_relation。邀请关系记录表。',
|
||||
);
|
||||
expect(screen.getByRole('button', {name: '被邀请人ID'}).getAttribute('title')).toBe(
|
||||
'原始字段名:invitee_user_id。被邀请人的用户标识。点击可按此列排序。',
|
||||
);
|
||||
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-a', 'u-c']);
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '邀请人ID'}));
|
||||
await waitFor(() => {
|
||||
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-c', 'u-a']);
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', {name: '邀请人ID'}));
|
||||
await waitFor(() => {
|
||||
expect(readFirstColumnValues(container)).toEqual(['u-a', 'u-b', 'u-c']);
|
||||
});
|
||||
});
|
||||
|
||||
function readFirstColumnValues(container: HTMLElement) {
|
||||
return Array.from(container.querySelectorAll('tbody tr')).map(
|
||||
(row) => row.querySelector('td')?.textContent?.trim() ?? '',
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ export function AdminInviteCodePage({
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [startsAt, setStartsAt] = useState('');
|
||||
const [expiresAt, setExpiresAt] = useState('');
|
||||
const [grantedTagsText, setGrantedTagsText] = useState('');
|
||||
const [metadataText, setMetadataText] = useState('{}');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
@@ -80,6 +81,7 @@ export function AdminInviteCodePage({
|
||||
const payload: AdminUpsertProfileInviteCodeRequest = {
|
||||
inviteCode: inviteCode.trim(),
|
||||
metadata: parseMetadata(metadataText),
|
||||
grantedUserTags: parseUserTags(grantedTagsText),
|
||||
startsAt: startsAt ? toIsoDateTime(startsAt) : null,
|
||||
expiresAt: expiresAt ? toIsoDateTime(expiresAt) : null,
|
||||
};
|
||||
@@ -115,6 +117,7 @@ export function AdminInviteCodePage({
|
||||
setInviteCode(entry.inviteCode);
|
||||
setStartsAt(toDateTimeLocalValue(entry.startsAt));
|
||||
setExpiresAt(toDateTimeLocalValue(entry.expiresAt));
|
||||
setGrantedTagsText(entry.grantedUserTags.join('、'));
|
||||
setMetadataText(JSON.stringify(entry.metadata, null, 2));
|
||||
}
|
||||
|
||||
@@ -174,6 +177,15 @@ export function AdminInviteCodePage({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>用户标签</span>
|
||||
<input
|
||||
autoComplete="off"
|
||||
value={grantedTagsText}
|
||||
onChange={(event) => setGrantedTagsText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>Metadata JSON</span>
|
||||
<textarea
|
||||
@@ -222,6 +234,7 @@ export function AdminInviteCodePage({
|
||||
<thead>
|
||||
<tr>
|
||||
<th>邀请码</th>
|
||||
<th>标签</th>
|
||||
<th>有效期</th>
|
||||
<th>创建</th>
|
||||
</tr>
|
||||
@@ -238,6 +251,9 @@ export function AdminInviteCodePage({
|
||||
{entry.inviteCode}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<TagList tags={entry.grantedUserTags} />
|
||||
</td>
|
||||
<td>
|
||||
<span className={`admin-status ${inviteValidityClass(entry)}`}>
|
||||
{inviteValidityLabel(entry)}
|
||||
@@ -272,6 +288,12 @@ export function AdminInviteCodePage({
|
||||
<dt>有效期</dt>
|
||||
<dd>{formatValidityWindow(result)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>标签</dt>
|
||||
<dd>
|
||||
<TagList tags={result.grantedUserTags} />
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>创建</dt>
|
||||
<dd>{result.createdAt}</dd>
|
||||
@@ -300,6 +322,33 @@ export function AdminInviteCodePage({
|
||||
);
|
||||
}
|
||||
|
||||
function TagList({tags}: {tags: string[]}) {
|
||||
if (!tags.length) {
|
||||
return <span className="admin-muted-text">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="admin-tag-list">
|
||||
{tags.map((tag) => (
|
||||
<span className="admin-tag" key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function parseUserTags(value: string) {
|
||||
const tags: string[] = [];
|
||||
for (const raw of value.split(/[\n,,;;、]+/)) {
|
||||
const tag = raw.trim();
|
||||
if (tag && !tags.includes(tag)) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function parseMetadata(value: string): Record<string, unknown> {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {
|
||||
filterAdminTrackingEventDefinitions,
|
||||
filterAdminProfileTaskTrackingEventDefinitions,
|
||||
findAdminTrackingEventDefinition,
|
||||
} from '../config/trackingEventDefinitions';
|
||||
import {handlePageError} from './pageUtils';
|
||||
@@ -68,7 +68,7 @@ export function AdminTaskConfigPage({
|
||||
[eventKey],
|
||||
);
|
||||
const filteredEventDefinitions = useMemo(
|
||||
() => filterAdminTrackingEventDefinitions(eventKeySearch),
|
||||
() => filterAdminProfileTaskTrackingEventDefinitions(eventKeySearch),
|
||||
[eventKeySearch],
|
||||
);
|
||||
|
||||
|
||||
@@ -350,11 +350,6 @@ button:disabled {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.admin-query-reset-button {
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
@@ -603,6 +598,13 @@ button:disabled {
|
||||
background: #eef3f6;
|
||||
}
|
||||
|
||||
.admin-ghost-button.admin-query-reset-button {
|
||||
width: auto;
|
||||
min-width: 76px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-text-button {
|
||||
display: inline;
|
||||
border: 0;
|
||||
@@ -650,7 +652,10 @@ button:disabled {
|
||||
}
|
||||
|
||||
.admin-table-wrap {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
@@ -679,6 +684,32 @@ button:disabled {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-muted-text {
|
||||
color: #86939c;
|
||||
}
|
||||
|
||||
.admin-tag-list {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-tag {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
border: 1px solid #cbdfe6;
|
||||
border-radius: 999px;
|
||||
background: #eef7f8;
|
||||
color: #0f5666;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-table-compact {
|
||||
min-width: 360px;
|
||||
}
|
||||
@@ -687,6 +718,65 @@ button:disabled {
|
||||
min-width: 1180px;
|
||||
}
|
||||
|
||||
.admin-database-table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.admin-database-table th,
|
||||
.admin-database-table td {
|
||||
width: 220px;
|
||||
min-width: 220px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.admin-database-table th:last-child,
|
||||
.admin-database-table td:last-child {
|
||||
width: 112px;
|
||||
min-width: 112px;
|
||||
max-width: 112px;
|
||||
}
|
||||
|
||||
.admin-table-sort-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
color: #667682;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.admin-table-sort-button span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.admin-table-sort-button svg {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.admin-table-cell-ellipsis {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-table-sort-button:hover,
|
||||
.admin-table-sort-button:focus-visible,
|
||||
.admin-table-sort-button[data-active="true"] {
|
||||
color: #0f5666;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.admin-json-preview {
|
||||
max-width: 360px;
|
||||
max-height: 160px;
|
||||
|
||||
20
deploy/env/api-server.env.example
vendored
20
deploy/env/api-server.env.example
vendored
@@ -37,7 +37,25 @@ GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
|
||||
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
VOLCENGINE_SPEECH_API_KEY=
|
||||
VOLCENGINE_SPEECH_APP_ID=
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY=
|
||||
VOLCENGINE_SPEECH_ASR_RESOURCE_ID=volc.seedasr.sauc.concurrent
|
||||
VOLCENGINE_SPEECH_TTS_RESOURCE_ID=seed-tts-2.0
|
||||
VOLCENGINE_SPEECH_REQUEST_TIMEOUT_MS=180000
|
||||
VOLCENGINE_SPEECH_ASR_WS_URL=wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
|
||||
VOLCENGINE_SPEECH_TTS_BIDIRECTION_WS_URL=wss://openspeech.bytedance.com/api/v3/tts/bidirection
|
||||
VOLCENGINE_SPEECH_TTS_SSE_URL=https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse
|
||||
|
||||
ARK_CHARACTER_VIDEO_BASE_URL=
|
||||
ARK_CHARACTER_VIDEO_API_KEY=
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
|
||||
- [埋点查询](./tracking/README.md):埋点原始事件与聚合投影的本地 SQL 查询。
|
||||
- [运营查询](./operations/README.md):任务、领奖、钱包对账等后台核查查询。
|
||||
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md)。
|
||||
- [PRD](./prd/README.md):产品需求与阶段计划;参考 MOKU / 幕间类 AI 文游的百梦 `text-game` 模板口径见 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md](./prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md),视觉小说模板 TXT 玩法平台化接入口径见 [AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md](./prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md),创意互动内容 Agent Phase 1 的 LangChain-Rust PoC、拼图闭环和并行任务拆分见 [CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md](./prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md),幸存者类模板闭环见 [AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md](./prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md),后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md)。
|
||||
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
|
||||
@@ -23,6 +23,14 @@ SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段
|
||||
|
||||
`maincloud` 相关脚本、环境变量、测试名和文档要求已统一判定为历史残留,后续禁止新增、运行或引用;当前后端 smoke 使用 `npm run api-server` 与 `/healthz`,详细规则见 [MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md](./technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md)。
|
||||
|
||||
基于 LangChain-Rust,以“感知—思考—记忆—行动—反思—协作”闭环完成图文创意理解、拼图模板选择、积分范围确认、拼图草稿字段填充、立即试玩和自然语言修订草稿字段的 Agent 方案见 [CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md](./technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md)。
|
||||
|
||||
创意互动内容 Agent Phase 1 的产品边界、实现细节、SpacetimeDB 落点、前端接入和可并行任务拆分见 [CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md](./prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md)。
|
||||
|
||||
视觉小说模板接入以 [AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md](./prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md) 为最新口径:只吸收外部 TXT 玩法的模板创作与运行经验,禁止迁入外部平台功能,并删除回放。
|
||||
|
||||
AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md](./prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md) 为最新口径:只吸收 MOKU / 幕间类 AI 文游的剧本游乐场、自由行动、AI GM、记忆和模拟器强反馈经验,禁止迁入外部社区、支付、榜单、私有存档或回放。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
- [CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md](./CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md):角色资产默认描述文本、正式图像/动作 prompt、共享模板与保留接口的分层与冗余审计。
|
||||
- [RPG_RUNTIME_DIRECT_DRAFT_PROFILE_AUDIT_2026-04-25.md](./RPG_RUNTIME_DIRECT_DRAFT_PROFILE_AUDIT_2026-04-25.md):RPG 运行时进入世界时改为直读 Agent session 草稿 profile 的链路检查。
|
||||
- [RPG_WORLD_DRAFT_EDIT_AUTOSAVE_OVERRIDE_AUDIT_2026-04-28.md](./RPG_WORLD_DRAFT_EDIT_AUTOSAVE_OVERRIDE_AUDIT_2026-04-28.md):RPG 世界草稿结果页编辑后被旧设定覆盖的前端本地态、session 真相源与自动保存链路审计。
|
||||
- [VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md](./VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md):视觉小说 VN-11 回放删除与外部平台功能误入负向扫描报告。
|
||||
- [VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md](./VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md):视觉小说 VN-12 全链路联调与自动化验收报告。
|
||||
- [engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md](./engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md):RPG 前端脚本中仍应迁到 `server-rs` / SpacetimeDB 的开局、快照、story engine、战斗、NPC/背包规则与创作残留后门审计。
|
||||
- [engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md](./engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md):RPG 前端脚本后端迁移完成度复核,标明开局、快照、story engine / prompt context、`camp_travel_home_scene`、战斗、NPC、背包/锻造、结果页保存 normalize 与角色资产 prompt 主链均已收口。
|
||||
- [engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md):对 `2026-04-19` 工程清理审计的当前仓库复核,区分已完成项、仍存边界问题和新的热点迁移。
|
||||
|
||||
377
docs/audits/SECURITY_VULNERABILITY_SCAN_2026-05-11.md
Normal file
377
docs/audits/SECURITY_VULNERABILITY_SCAN_2026-05-11.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 安全漏洞扫描报告 2026-05-11
|
||||
|
||||
## 扫描范围
|
||||
|
||||
- 工作区:`C:/proj/Genarrative/.worktrees/hermes-3337436a`
|
||||
- 分支:`hermes/hermes-3337436a`
|
||||
- Git 基线:扫描时存在一个未跟踪计划文件 `.hermes/plans/2026-05-11_205658-security-vulnerability-scan.md`。
|
||||
- 扫描对象:根 Node/Vite/React 依赖、`server-rs` Rust workspace 依赖入口、仓库已跟踪文件中的敏感配置、JS/TS/Rust 源码安全热点。
|
||||
|
||||
## 扫描命令与工具状态
|
||||
|
||||
已执行:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git branch --show-current
|
||||
git rev-parse --show-toplevel
|
||||
git status --short
|
||||
node --version
|
||||
npm --version
|
||||
cargo --version
|
||||
rustc --version
|
||||
rg --version
|
||||
npm audit --json
|
||||
npm audit --audit-level=moderate
|
||||
git ls-files -z | xargs -0 grep -nIE "(api[_-]?key|secret|password|passwd|token|private[_-]?key|BEGIN (RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|AKIA[0-9A-Z]{16}|xox[baprs]-|sk-[A-Za-z0-9_-]{20,})"
|
||||
rg -n "\beval\(|new Function\(|dangerouslySetInnerHTML|innerHTML\s*=|document\.write\(" src apps scripts packages
|
||||
rg -n "exec\(|execSync\(|spawn\(|spawnSync\(|shell:\s*true|child_process" scripts src apps packages
|
||||
rg -n "Command::new|std::process|\.unwrap\(|\.expect\(|fs::|File::open|PathBuf|set_header|cors|CorsLayer" server-rs/crates
|
||||
rg -n "allow_origin|Any|cookie|Authorization|Bearer|refresh|access_token|set_cookie|SameSite|Secure|HttpOnly" server-rs/crates src apps scripts
|
||||
```
|
||||
|
||||
工具版本:
|
||||
|
||||
- Node:`v22.22.2`
|
||||
- npm:`10.9.7`
|
||||
- Cargo:`cargo 1.95.0 (f2d3ce0bd 2026-03-21)`
|
||||
- Rustc:`rustc 1.95.0 (59807616e 2026-04-14)`
|
||||
- ripgrep:`15.1.0`
|
||||
- `gitleaks`:未安装,本次未执行。
|
||||
- `cargo-audit`:未安装,本次未执行;未擅自安装到用户环境。
|
||||
|
||||
原始扫描输出保存于:
|
||||
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/npm-audit.json`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/npm-audit.txt`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/secret-grep.txt`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/js-xss-dynamic.txt`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/node-command-exec.txt`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/rust-hotspots.txt`
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/auth-cors-hotspots.txt`
|
||||
|
||||
注意:`secret-grep.txt` 可能包含敏感片段,提交前应删除或改为脱敏摘要,不建议直接进入 Git。
|
||||
|
||||
## 摘要
|
||||
|
||||
| 等级 | 数量 | 说明 |
|
||||
| --- | ---: | --- |
|
||||
| Critical | 1 | `.env.local` 被 Git 跟踪且含多项非空真实密钥/凭据形态配置。 |
|
||||
| High | 2 | npm 依赖存在 8 个 high advisory 聚合项;Vite dev server 任意文件读取类漏洞需要优先升级。另有 TypeScript ESLint 链路 ReDoS 风险。 |
|
||||
| Medium | 2 | esbuild dev server 请求读取、PostCSS CSS stringify XSS。 |
|
||||
| Low | 3 | jsdom/http-proxy-agent/@tootallnate/once 低危链路。 |
|
||||
| Unknown | 1 | Rust 依赖漏洞未完成扫描,因为本机未安装 `cargo-audit`。 |
|
||||
| Informational | 多项 | 源码热点扫描命中大量测试/脚本/unwrap/expect,需要按入口人工复核。 |
|
||||
|
||||
## Critical
|
||||
|
||||
### C-1:仓库跟踪了 `.env.local`,且包含多项非空真实密钥形态配置
|
||||
|
||||
**证据:**
|
||||
|
||||
`git ls-files --error-unmatch .env.local` 显示 `.env.local` 已被 Git 跟踪。扫描确认该文件包含多项非空密钥/凭据形态变量,包括但不限于:
|
||||
|
||||
- `LLM_API_KEY`
|
||||
- `ARK_API_KEY`
|
||||
- `ARK_CHARACTER_VIDEO_API_KEY`
|
||||
- `DASHSCOPE_API_KEY`
|
||||
- `VOLCENGINE_ACCESS_KEY_ID`
|
||||
- `VOLCENGINE_SECRET_ACCESS_KEY`
|
||||
- `ALIYUN_SMS_ACCESS_KEY_ID`
|
||||
- `ALIYUN_SMS_ACCESS_KEY_SECRET`
|
||||
- `GENARRATIVE_LLM_API_KEY`
|
||||
- `ALIYUN_OSS_ACCESS_KEY_ID`
|
||||
- `ALIYUN_OSS_ACCESS_KEY_SECRET`
|
||||
- `GENARRATIVE_ADMIN_PASSWORD`
|
||||
|
||||
报告中不记录具体值。
|
||||
|
||||
**影响:**
|
||||
|
||||
如果该文件已进入远端仓库或被团队成员拉取,相关外部服务密钥、OSS/SMS/LLM/后台密码均应视为已泄露。即使后续从当前工作树删除,也不能撤销历史泄露风险。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
1. 立即轮换 `.env.local` 中出现过的所有真实密钥、访问密钥、后台密码和 token。
|
||||
2. 从 Git 跟踪中移除 `.env.local`,但不要删除本地私有文件:
|
||||
|
||||
```bash
|
||||
git rm --cached .env.local
|
||||
```
|
||||
|
||||
3. 按项目约束,不要在 `.gitignore` 中新增 `.env.local`;如果仓库已有其他机制管理本地私密 env,应遵循既有约定。若没有,应先补一份安全说明文档,而不是提交真实 `.env.local`。
|
||||
4. 将必要的占位示例保留在 `.env.example` 或 `deploy/env/api-server.env.example`,确保示例值不是可用密钥。
|
||||
5. 如该文件已推送到远端历史,评估是否需要历史清理;无论是否清历史,密钥轮换都是必须步骤。
|
||||
|
||||
**验证:**
|
||||
|
||||
```bash
|
||||
git ls-files --error-unmatch .env.local
|
||||
# 预期:返回非 0,表示不再跟踪
|
||||
|
||||
git diff --cached -- .env.local
|
||||
# 预期:只显示从索引移除,不输出真实值到公开报告
|
||||
```
|
||||
|
||||
## High
|
||||
|
||||
### H-1:Vite 依赖存在高危 dev server 任意文件读取/路径遍历类 advisory
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- package:`vite`
|
||||
- direct dependency:是
|
||||
- installed vulnerable range:`<=6.4.1`
|
||||
- severity:`high`
|
||||
- 相关 advisory 包括:
|
||||
- `GHSA-p9ff-h696-f583`:Vite dev server WebSocket 任意文件读取,高危。
|
||||
- `GHSA-4w7w-66w2-5vf9`:optimized deps `.map` 处理路径遍历,中危。
|
||||
- 多个 `server.fs` / public directory 相关低中危问题。
|
||||
- `fixAvailable=true`。
|
||||
|
||||
**影响:**
|
||||
|
||||
主要影响开发服务器和预览环境。如果开发机、测试机或内网联调环境将 Vite dev server 暴露给不可信网络,攻击者可能读取工作区文件或旁路 `server.fs` 限制。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
1. 优先将 `vite` 升级到 npm audit 推荐的安全版本范围。
|
||||
2. 升级后执行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. 检查 `scripts/vite-cli.mjs`、`scripts/dev-web-rust.mjs`、Vite 配置中的 dev server host 暴露范围,开发环境避免绑定 `0.0.0.0` 或暴露到公网。
|
||||
|
||||
### H-2:`@typescript-eslint/*` 链路经 `minimatch` 存在 ReDoS 高危 advisory
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- direct packages:
|
||||
- `@typescript-eslint/eslint-plugin`,当前范围 `6.16.0 - 7.5.0`,high。
|
||||
- `@typescript-eslint/parser`,当前范围 `6.16.0 - 7.5.0`,high。
|
||||
- transitive packages:
|
||||
- `@typescript-eslint/type-utils`
|
||||
- `@typescript-eslint/typescript-estree`
|
||||
- `@typescript-eslint/utils`
|
||||
- `minimatch`
|
||||
- `minimatch` advisory:
|
||||
- `GHSA-3ppc-4f35-3m26`
|
||||
- `GHSA-7r86-cg39-jmmj`
|
||||
- `GHSA-23c5-xmqv-rm74`
|
||||
- npm 建议升级到 `@typescript-eslint/* 8.59.3`,属于 SemVer major。
|
||||
|
||||
**影响:**
|
||||
|
||||
主要影响 lint/构建工具链。如果 CI 或开发命令处理不可信 glob pattern,可能造成 ReDoS。生产运行时直接影响较低,但 CI 可用性和供应链安全仍应修复。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
1. 单独开依赖升级分支,将 `@typescript-eslint/eslint-plugin` 与 `@typescript-eslint/parser` 升级到兼容 ESLint 8/TypeScript 5.8 的安全版本。
|
||||
2. 因为是 major 升级,先阅读迁移说明并运行 ESLint 全量检查。
|
||||
3. 验证:
|
||||
|
||||
```bash
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
```
|
||||
|
||||
### H-3:`picomatch` ReDoS / glob matching 高危 advisory
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- package:`picomatch`
|
||||
- severity:`high`
|
||||
- vulnerable range:`4.0.0 - 4.0.3`
|
||||
- advisory:
|
||||
- `GHSA-c2c7-rcm5-vvqj`:extglob quantifiers ReDoS,高危。
|
||||
- `GHSA-3v7f-55p6-f55p`:POSIX character classes method injection,中危。
|
||||
- `fixAvailable=true`。
|
||||
|
||||
**影响:**
|
||||
|
||||
主要影响依赖 picomatch 的构建、测试、文件匹配工具链。生产直接影响取决于是否在服务端运行时用它处理用户输入 glob;当前未在扫描摘要中发现明显业务入口直接使用。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
通过升级引入它的 direct dependency 来消除,不建议手工改 lockfile。
|
||||
|
||||
## Medium
|
||||
|
||||
### M-1:`esbuild <=0.24.2` dev server 允许任意网站请求并读取响应
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- package:`esbuild`
|
||||
- severity:`moderate`
|
||||
- advisory:`GHSA-67mh-4wv8-2f99`
|
||||
- vulnerable range:`<=0.24.2`
|
||||
- `fixAvailable=true`。
|
||||
|
||||
**影响:**
|
||||
|
||||
主要影响开发服务器场景。若本地开发服务暴露到不可信网络,风险上升。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
随 Vite / 构建链路升级一并修复,升级后跑前端检查与构建。
|
||||
|
||||
### M-2:`postcss <8.5.10` CSS stringify XSS advisory
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- package:`postcss`
|
||||
- severity:`moderate`
|
||||
- advisory:`GHSA-qx2v-qp2m-jg93`
|
||||
- vulnerable range:`<8.5.10`
|
||||
- `fixAvailable=true`。
|
||||
|
||||
**影响:**
|
||||
|
||||
如果系统把不可信 CSS 内容 stringify 后注入页面,可能触发 XSS。当前项目是否存在这类业务入口需人工复核;从依赖角度建议升级。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
升级 Tailwind/Vite/PostCSS 链路带出的安全版本,并执行前端构建验证。
|
||||
|
||||
## Low
|
||||
|
||||
### L-1:`jsdom` 链路低危 advisory
|
||||
|
||||
**证据:**
|
||||
|
||||
`npm audit` 显示:
|
||||
|
||||
- `jsdom` direct dependency,severity low。
|
||||
- transitive:`http-proxy-agent`、`@tootallnate/once`。
|
||||
- npm 建议升级到 `jsdom 29.1.1`,SemVer major。
|
||||
|
||||
**影响:**
|
||||
|
||||
通常影响测试环境。若测试工具处理不可信 URL/代理输入,风险上升。
|
||||
|
||||
**建议修复:**
|
||||
|
||||
不要和 Vite/TypeScript ESLint 大升级混在一个提交里。单独升级 jsdom 后运行:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Unknown / 未完成项
|
||||
|
||||
### U-1:Rust 依赖漏洞未完成扫描
|
||||
|
||||
**原因:**
|
||||
|
||||
本机没有 `cargo-audit`,本次没有擅自安装用户级 Cargo 工具。
|
||||
|
||||
**建议:**
|
||||
|
||||
如确认允许安装:
|
||||
|
||||
```bash
|
||||
cargo install cargo-audit --locked
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
或在 CI/具备工具的环境执行并回填结果。
|
||||
|
||||
## Informational / 源码热点
|
||||
|
||||
### I-1:JS/TS XSS / 动态执行热点
|
||||
|
||||
扫描命中 1 行:
|
||||
|
||||
- `src/routing/RouteImageReadyGate.test.ts` 中测试代码使用 `root.innerHTML`。
|
||||
|
||||
初步判断为测试环境构造 DOM,不是生产漏洞。若后续发现生产代码使用 `dangerouslySetInnerHTML` 或直接 `innerHTML = userInput`,应升级为 High。
|
||||
|
||||
### I-2:Node 脚本命令执行热点
|
||||
|
||||
扫描命中 21 行,主要集中在:
|
||||
|
||||
- `scripts/spacetime-migration-common.mjs`
|
||||
- `scripts/run-bash-script.mjs`
|
||||
- `scripts/generate-spacetime-bindings.mjs`
|
||||
- `scripts/dev-web-rust.mjs`
|
||||
|
||||
初步判断为项目脚本启动 `spacetime`、bash、Vite 等工具的正常行为。后续人工复核重点:
|
||||
|
||||
- 是否使用固定命令和参数数组,而不是拼接 shell 字符串。
|
||||
- 是否把用户输入直接作为命令或 shell 参数。
|
||||
- 是否设置 `shell: true`。
|
||||
|
||||
### I-3:Rust unwrap/expect、文件路径、CORS/Auth 热点较多
|
||||
|
||||
扫描命中:
|
||||
|
||||
- `rust-hotspots.txt`:1348 行。
|
||||
- `auth-cors-hotspots.txt`:1157 行。
|
||||
|
||||
这些是热点,不等于漏洞。建议后续按模块分批人工复核:
|
||||
|
||||
1. `server-rs/crates/api-server/src/admin.rs`
|
||||
2. `server-rs/crates/api-server/src/app.rs`
|
||||
3. `server-rs/crates/platform-auth/src/**`
|
||||
4. `server-rs/crates/platform-oss/src/**`
|
||||
5. `server-rs/crates/platform-llm/src/**`
|
||||
6. `server-rs/crates/spacetime-client/src/**`
|
||||
|
||||
重点看:生产 CORS、Cookie 安全属性、token 日志、路径拼接、外部 URL 下载、Data URL 大小限制、OSS 签名边界。
|
||||
|
||||
## 推荐修复顺序
|
||||
|
||||
1. 立即处理 C-1:轮换 `.env.local` 里所有真实密钥,并从 Git 索引移除 `.env.local`。
|
||||
2. 升级 `vite` 相关依赖,优先消除 dev server 任意文件读取/路径遍历 advisory。
|
||||
3. 升级 `@typescript-eslint/*`,消除 minimatch 链路 ReDoS;因 major 升级,单独提交。
|
||||
4. 升级 `postcss` / `esbuild` / `picomatch` 的来源依赖。
|
||||
5. 单独评估 `jsdom` major 升级。
|
||||
6. 用户确认后安装或使用 CI 执行 `cargo audit`,补齐 Rust 依赖漏洞结论。
|
||||
7. 对 `auth-cors-hotspots.txt` 和 `rust-hotspots.txt` 做模块级人工审计。
|
||||
|
||||
## 修复后的验证命令
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
```
|
||||
|
||||
如修改后端安全、Auth、Cookie、CORS 或 API:
|
||||
|
||||
```bash
|
||||
cd server-rs && cargo test --workspace
|
||||
npm run api-server
|
||||
# 检查 /healthz,并执行相关 API/auth smoke
|
||||
```
|
||||
|
||||
如补齐 Rust audit:
|
||||
|
||||
```bash
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
## 备注
|
||||
|
||||
- 本报告没有输出任何真实密钥值。
|
||||
- `.hermes/plans/assets/security-scan-2026-05-11/secret-grep.txt` 可能包含敏感内容,仅用于本地排查;提交前应删除或替换为脱敏报告。
|
||||
- 由于 `gitleaks` 未安装,本次密钥扫描只是 grep 兜底,不等价于完整 secrets audit。
|
||||
32
docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md
Normal file
32
docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# VN-11 负向扫描报告
|
||||
|
||||
生成日期:2026-05-07
|
||||
|
||||
## 扫描范围
|
||||
|
||||
- 工程代码:`src/`、`packages/shared/src/`、`server-rs/crates/`
|
||||
- 文档与共享记忆:`docs/`、`.hermes/shared-memory/`
|
||||
- 外部平台误入复核:视觉小说前端、service、shared contracts、Rust contracts、module、api-server、SpacetimeDB schema 与 facade 路径
|
||||
|
||||
## 扫描结论
|
||||
|
||||
- 工程代码回放类直出命中:0
|
||||
- 文档 / 共享记忆回放类命中:222
|
||||
- 视觉小说实现路径外部平台能力疑似误入命中:0
|
||||
|
||||
## 处理记录
|
||||
|
||||
- 已将 `storyEngine` 回归工具的命名从 replay 语义收口为 rerun / 复测语义。
|
||||
- 已将技能效果预览按钮的内部状态与文案从重播语义收口为重新预览语义。
|
||||
- 已确认视觉小说工程路径未新增回放路由、DTO、表、按钮、文案、外部平台账号 / 订单 / 会员 / 促销 / 后台 / 公开市场或私有存档能力。
|
||||
|
||||
## 文档命中说明
|
||||
|
||||
- 文档命中来自历史旧文档、设计复盘、禁止语境、负向验收或本报告记录。VN-11 工程门禁只阻断代码路径新增能力。
|
||||
|
||||
## 门禁命令
|
||||
|
||||
```bash
|
||||
npm run check:visual-novel-vn11
|
||||
```
|
||||
|
||||
92
docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md
Normal file
92
docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# VN-12 全链路联调与自动化验收报告
|
||||
|
||||
生成日期:2026-05-07
|
||||
|
||||
## 结论
|
||||
|
||||
- 状态:通过
|
||||
- 失败项:0
|
||||
- 收口说明:VN-12 本次只补验收门禁、关键路径测试和报告记录,未扩展新玩法功能。
|
||||
|
||||
## 自动化验收清单
|
||||
|
||||
- docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md
|
||||
- docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md
|
||||
- src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx
|
||||
- src/components/visual-novel-result/VisualNovelResultView.test.tsx
|
||||
- src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx
|
||||
- src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts
|
||||
- src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts
|
||||
- server-rs/crates/api-server/src/visual_novel.rs
|
||||
- server-rs/crates/module-visual-novel/src/application.rs
|
||||
- server-rs/crates/shared-contracts/src/visual_novel.rs
|
||||
- package.json
|
||||
- server-rs/crates/api-server/src/app.rs
|
||||
- src/services/visual-novel-runtime/visualNovelRuntimeClient.ts
|
||||
- src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts
|
||||
- src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts
|
||||
- src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx
|
||||
- src/components/visual-novel-result/VisualNovelResultView.test.tsx
|
||||
- src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx
|
||||
|
||||
## API smoke
|
||||
|
||||
- `/api/creation/visual-novel/sessions`
|
||||
- `/api/creation/visual-novel/works`
|
||||
- `/api/runtime/visual-novel/gallery`
|
||||
- `/api/runtime/visual-novel/works/{profile_id}/runs`
|
||||
- `/api/runtime/visual-novel/runs/{run_id}/actions/stream`
|
||||
- `/api/runtime/visual-novel/runs/{run_id}/history`
|
||||
- `/api/runtime/visual-novel/runs/{run_id}/regenerate`
|
||||
- `/api/profile/save-archives`
|
||||
- `/api/profile/save-archives/{world_key}`
|
||||
- `/api/runtime/save/snapshot`
|
||||
|
||||
本次实测:
|
||||
|
||||
- `npm run api-server` 可启动 Rust `api-server`。
|
||||
- `GET http://127.0.0.1:3100/healthz` 返回 `200`,响应为 `{"ok":true,"service":"genarrative-api-server"}`。
|
||||
- `GET /api/runtime/visual-novel/gallery` 在当前本地环境返回超时 / `502`,日志显示 `api-server` 连接 `127.0.0.1:3101` SpacetimeDB 数据库 `xushi-p4wfr` 被拒绝;该项按本地 SpacetimeDB 未完整就绪记录为环境阻塞,不新增工程实现。
|
||||
|
||||
## 前端关键路径
|
||||
|
||||
- 创作工作台:`VisualNovelAgentWorkspace`
|
||||
- 结果页:`VisualNovelResultView`
|
||||
- 运行时:`VisualNovelRuntimeShell`
|
||||
- 运行时 SSE:`visualNovelRuntimeSse` / `visualNovelRuntimeClient`
|
||||
|
||||
## 桌面 / 移动端检查
|
||||
|
||||
- 桌面端:已用 Edge headless 截取 `/creation/visual-novel/agent`,文件为 `docs/audits/VN12_VISUAL_NOVEL_DESKTOP_2026-05-07.png`。
|
||||
- 移动端:已用 Edge headless 截取 `/creation/visual-novel/agent`,文件为 `docs/audits/VN12_VISUAL_NOVEL_MOBILE_2026-05-07.png`。
|
||||
- in-app browser 插件本次未发现可用 IAB backend,截图使用本机 Edge headless 兜底完成。
|
||||
|
||||
## 校验摘要
|
||||
|
||||
- package.json scripts: 通过
|
||||
- api-server visual novel routes: 通过
|
||||
- visual novel runtime client routes: 通过
|
||||
- visual novel runtime client tests: 通过
|
||||
- visual novel SSE tests: 通过
|
||||
- visual novel creation tests: 通过
|
||||
- visual novel result tests: 通过
|
||||
- visual novel runtime tests: 通过
|
||||
|
||||
## 执行命令
|
||||
|
||||
```bash
|
||||
npm run check:visual-novel-vn12 -- --write-report
|
||||
npm run test -- src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts
|
||||
cargo test -p module-visual-novel
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
## 未覆盖风险
|
||||
|
||||
- 当前本地 SpacetimeDB 连接未完整就绪,公开 gallery API 的真实数据返回未在本次环境完成;`/healthz` 与编译 / 单测已通过。
|
||||
- 若接口路由或测试名称后续调整,需要同步更新本门禁脚本与报告模板。
|
||||
|
||||
BIN
docs/audits/VN12_VISUAL_NOVEL_DESKTOP_2026-05-07.png
Normal file
BIN
docs/audits/VN12_VISUAL_NOVEL_DESKTOP_2026-05-07.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/audits/VN12_VISUAL_NOVEL_MOBILE_2026-05-07.png
Normal file
BIN
docs/audits/VN12_VISUAL_NOVEL_MOBILE_2026-05-07.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
1. 产品名称:百梦。
|
||||
2. 产品愿景:百梦AI团队致力于打造AI互动内容UGC平台。
|
||||
3. 产品slogan:每个人都可以在10分钟内轻松创作出一款精品互动作品。
|
||||
3. 产品slogan:不用代码,不用美术,10分钟把脑洞变成有趣的体验。
|
||||
4. 产品特点:低门槛创作、高完成度作品、玩过后可改造并发布。
|
||||
5. 关键技术:Harness Engineering、多Agent调度、AI创作工具、AI原生游戏框架。
|
||||
6. 产品心智:想玩但找不到、玩到不满意、平台外体验不满意时,都可以来百梦做成自己满意的。
|
||||
@@ -28,16 +28,14 @@
|
||||
|
||||
```text
|
||||
百梦
|
||||
AI互动内容UGC平台
|
||||
把想玩的世界,亲手做出来
|
||||
10分钟做自己的互动内容
|
||||
```
|
||||
|
||||
第二层:远读slogan
|
||||
|
||||
```text
|
||||
每个人都可以在10分钟内
|
||||
轻松创作出一款
|
||||
精品互动作品
|
||||
不用代码,不用美术,
|
||||
10分钟把脑洞变成有趣的体验
|
||||
```
|
||||
|
||||
第三层:产品特点
|
||||
@@ -79,7 +77,7 @@ AI原生游戏框架:
|
||||
|
||||
## 4. 生成方式
|
||||
|
||||
主视觉底图使用仓库内 APIMart OpenAI 兼容 `gpt-image-2` 工作流生成:
|
||||
主视觉底图使用仓库内 `gpt-image-2` 工作流生成;2026-05-09 起同类工作流走 VectorEngine:
|
||||
|
||||
```text
|
||||
model: gpt-image-2
|
||||
@@ -88,6 +86,8 @@ reference image: 百梦气泡共创logo方向图
|
||||
output: output/imagegen/baimeng-expo-rollup/baimeng-rollup-background-gpt-image-2.png
|
||||
```
|
||||
|
||||
2026-05-08 根据新文案重新调用 `gpt-image-2` 生成新版底图。新版底图在中上部保留更干净的两行 slogan 留白,并在下半部增加轻量内容卡、创作路径和 AI 辅助创作氛围,最终再叠加精确中文排版。
|
||||
|
||||
因为图片模型直接生成中文长文案存在错字风险,最终稿采用“gpt-image-2 底图 + 本地精确中文排版”的方式生成:
|
||||
|
||||
```text
|
||||
|
||||
@@ -247,11 +247,11 @@ python "C:\Users\wuxiangwanzi\.codex\skills\.system\imagegen\scripts\image_gen.p
|
||||
--size 1024x1024
|
||||
```
|
||||
|
||||
若继续使用仓库现有 APIMart 路由,则需要配置:
|
||||
若继续使用仓库现有 VectorEngine GPT-image-2 路由,则需要配置:
|
||||
|
||||
```text
|
||||
APIMART_BASE_URL=https://api.apimart.ai/v1
|
||||
APIMART_API_KEY=...
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=...
|
||||
```
|
||||
|
||||
## 13. 04/07 气泡方向单独优化补充
|
||||
|
||||
529
docs/design/CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md
Normal file
529
docs/design/CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md
Normal file
@@ -0,0 +1,529 @@
|
||||
# 儿童动作识别互动玩法 Demo 热身关开发文档
|
||||
|
||||
> 日期:2026-05-09
|
||||
> 适用范围:儿童动作识别互动玩法 Demo 的固定启动热身关
|
||||
> 文档性质:玩法 Demo 开发设计文档
|
||||
> 说明:本文整理当前已确认的热身关内容、体验、流程和热身数据记录要求。
|
||||
|
||||
## 1. 热身关定位
|
||||
|
||||
热身关是 Demo 启动后的固定流程,用于在正式进入后续趣味学习关前完成以下事项:
|
||||
|
||||
- 调用摄像头;
|
||||
- 识别用户和环境;
|
||||
- 引导用户来到建议互动位置;
|
||||
- 教学基础交互方式;
|
||||
- 确认用户可在互动空间内完成左右移动、挥手和跳跃;
|
||||
- 记录用户左右移动距离、挥动手臂空间和跳跃空间,作为后续关卡的空间边界与行为坐标;
|
||||
- 完成后进入关卡选择。
|
||||
|
||||
热身关不接入创作模块,不作为可配置玩法模板提供给创作者。
|
||||
|
||||
## 2. 屏幕与设备适配
|
||||
|
||||
本产品适用于电视屏幕、电脑屏幕等环境。
|
||||
|
||||
热身关制作表达使用横屏比例。
|
||||
|
||||
## 3. 画面基础表现
|
||||
|
||||
用户进入热身关后,摄像头被调用,并开始识别用户和环境。
|
||||
|
||||
画面基础表现如下:
|
||||
|
||||
1. 在屏幕中央位置的地面生成预设的绿色圆环,作为建议位置的指引。
|
||||
2. 将用户的实际位置生成角色剪影,作为用户在画面中的标识。
|
||||
3. 只对摄像头背景做虚化处理,用于表达对用户隐私的保护、屏蔽周围环境干扰,并营造空间感。
|
||||
|
||||
## 4. 通用检测与引导规则
|
||||
|
||||
### 4.1 不允许跳过
|
||||
|
||||
热身关每个步骤都必须由用户完成,不允许跳过,也不允许系统自动进入下一步。
|
||||
|
||||
### 4.2 引导动画播放规则
|
||||
|
||||
每个动作等待 3 秒后可以播放引导动画。
|
||||
|
||||
当前不设置最长等待时间。
|
||||
|
||||
### 4.3 绿色圆环完成规则
|
||||
|
||||
用户到达绿色圆环后,绿色圆环进入 2 秒选中状态。
|
||||
|
||||
用户需要在绿色圆环内保持停留 2 秒,才算完成该圆环位置检测。
|
||||
|
||||
### 4.4 左右距离映射规则
|
||||
|
||||
“约半米”的左右移动距离,技术上以角色剪影移动距离为准。
|
||||
|
||||
该距离后续会根据实际体验继续调校。
|
||||
|
||||
### 4.5 手势区分规则
|
||||
|
||||
招手 / 摆手、挥动左手、挥动右手三类动作需要有动作区分。
|
||||
|
||||
手势检测仅对肢体进行区分,不对手部细节进行区分。
|
||||
|
||||
### 4.6 手势引导规则
|
||||
|
||||
挥动哪只手,就使用对应手的引导。
|
||||
|
||||
## 5. 热身关完整流程
|
||||
|
||||
### 5.1 进入热身关
|
||||
|
||||
#### 画面表现
|
||||
|
||||
- 摄像头被调用。
|
||||
- 系统识别用户和环境。
|
||||
- 屏幕中央位置的地面出现预设绿色圆环。
|
||||
- 用户实际位置以角色剪影形式显示。
|
||||
- 只对摄像头背景做虚化处理,保留空间感。
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
屏幕中上方浮现文字,同时语音播报:
|
||||
|
||||
```text
|
||||
欢迎你,小朋友,见到你真开心
|
||||
```
|
||||
|
||||
随后继续播报:
|
||||
|
||||
```text
|
||||
请你来到圆圈这里和我打个招呼吧
|
||||
```
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否到达屏幕中央绿色圆环位置。
|
||||
|
||||
用户到达圆环后,绿色圆环进入 2 秒选中状态。用户保持停留 2 秒后,该步骤完成。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成中央圆环位置检测后:
|
||||
|
||||
- 播放圆圈消失特效;
|
||||
- 进入招手手势教学步骤。
|
||||
|
||||
---
|
||||
|
||||
### 5.2 招手教学
|
||||
|
||||
#### 画面表现
|
||||
|
||||
播放招手的手势引导。
|
||||
|
||||
若用户进入该步骤后 3 秒仍未完成动作,可以播放引导动画。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否完成招手 / 摆手手势。
|
||||
|
||||
该动作与后续挥动左手、挥动右手需要有动作区分,但仅对肢体进行区分,不对手部细节进行区分。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成招手 / 摆手手势后,进入下一步。
|
||||
|
||||
---
|
||||
|
||||
### 5.3 热身说明
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧
|
||||
```
|
||||
|
||||
播放完成后进入左右移动热身步骤。
|
||||
|
||||
---
|
||||
|
||||
### 5.4 向左一步
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
向左一步
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
屏幕中心向左一个身位,约半米的地面位置,出现新的绿色圆圈。
|
||||
|
||||
“约半米”技术上以角色剪影移动距离为准,后续根据体验调校。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否到达该绿色圆圈位置。
|
||||
|
||||
用户到达圆环后,绿色圆环进入 2 秒选中状态。用户保持停留 2 秒后,该步骤完成。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
同时记录本次向左移动距离,作为后续关卡中的左侧空间边界参考。
|
||||
|
||||
完成后进入“回到中间来”。
|
||||
|
||||
---
|
||||
|
||||
### 5.5 回到中间来(一)
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
回到中间来
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
场地中心位置出现绿色圆圈。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否到达场地中心绿色圆圈位置。
|
||||
|
||||
用户到达圆环后,绿色圆环进入 2 秒选中状态。用户保持停留 2 秒后,该步骤完成。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
完成后进入“向右一步”。
|
||||
|
||||
---
|
||||
|
||||
### 5.6 向右一步
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
向右一步
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
屏幕中心向右一个身位,约半米的地面位置,出现新的绿色圆圈。
|
||||
|
||||
“约半米”技术上以角色剪影移动距离为准,后续根据体验调校。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否到达该绿色圆圈位置。
|
||||
|
||||
用户到达圆环后,绿色圆环进入 2 秒选中状态。用户保持停留 2 秒后,该步骤完成。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
同时记录本次向右移动距离,作为后续关卡中的右侧空间边界参考。
|
||||
|
||||
完成后进入“回到中间来”。
|
||||
|
||||
---
|
||||
|
||||
### 5.7 回到中间来(二)
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
回到中间来
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
场地中心位置出现绿色圆圈。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否到达场地中心绿色圆圈位置。
|
||||
|
||||
用户到达圆环后,绿色圆环进入 2 秒选中状态。用户保持停留 2 秒后,该步骤完成。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
完成后进入左手挥动教学。
|
||||
|
||||
---
|
||||
|
||||
### 5.8 挥动左手
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
挥动左手
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
播放伸展手臂挥动左手的手势引导。
|
||||
|
||||
若用户进入该步骤后 3 秒仍未完成动作,可以播放引导动画。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否完成挥动左手手势。
|
||||
|
||||
该手势检测仅对肢体进行区分,不对手部细节进行区分。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
同时记录用户挥动左手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
完成后进入右手挥动教学。
|
||||
|
||||
---
|
||||
|
||||
### 5.9 挥动右手
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
挥动右手
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
播放伸展手臂挥动右手的手势引导。
|
||||
|
||||
若用户进入该步骤后 3 秒仍未完成动作,可以播放引导动画。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否完成挥动右手手势。
|
||||
|
||||
该手势检测仅对肢体进行区分,不对手部细节进行区分。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后播放鼓励语:
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
同时记录用户挥动右手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
完成后进入跳跃教学。
|
||||
|
||||
---
|
||||
|
||||
### 5.10 原地跳一下
|
||||
|
||||
#### 屏幕文字与语音
|
||||
|
||||
```text
|
||||
原地跳一下
|
||||
```
|
||||
|
||||
#### 画面表现
|
||||
|
||||
播放跳跃姿势引导。
|
||||
|
||||
若用户进入该步骤后 3 秒仍未完成动作,可以播放引导动画。
|
||||
|
||||
#### 检测逻辑
|
||||
|
||||
系统检测用户是否完成跳跃姿势。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
用户完成后:
|
||||
|
||||
- 记录用户跳跃空间,保存为该用户对应的行为坐标;
|
||||
- 播放热身结束特效、上浮字幕和语音:
|
||||
|
||||
```text
|
||||
真厉害,你是我见过最聪明的小朋友
|
||||
```
|
||||
|
||||
随后继续播放:
|
||||
|
||||
```text
|
||||
别走开,现在开始我们的游戏吧
|
||||
```
|
||||
|
||||
热身关结束,进入关卡选择。
|
||||
|
||||
## 6. 流程状态表
|
||||
|
||||
| 顺序 | 步骤 | 屏幕文字 / 语音 | 画面表现 | 检测目标 | 完成后反馈 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 进入热身关 | 欢迎你,小朋友,见到你真开心;请你来到圆圈这里和我打个招呼吧 | 中央地面绿色圆环;用户角色剪影;摄像头背景虚化 | 用户到达中央圆环并保持 2 秒 | 圆圈消失特效 |
|
||||
| 2 | 招手教学 | 同上流程延续 | 招手手势引导;等待 3 秒可播放引导动画 | 招手 / 摆手 | 进入下一步 |
|
||||
| 3 | 热身说明 | 你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧 | 保持热身引导状态 | 无新增动作检测 | 进入移动热身 |
|
||||
| 4 | 向左一步 | 向左一步 | 左侧约半米处绿色圆圈 | 用户到达左侧圆环并保持 2 秒 | 真棒;记录左侧空间边界 |
|
||||
| 5 | 回到中间来 | 回到中间来 | 中心位置绿色圆圈 | 用户到达中心圆环并保持 2 秒 | 真棒 |
|
||||
| 6 | 向右一步 | 向右一步 | 右侧约半米处绿色圆圈 | 用户到达右侧圆环并保持 2 秒 | 真棒;记录右侧空间边界 |
|
||||
| 7 | 回到中间来 | 回到中间来 | 中心位置绿色圆圈 | 用户到达中心圆环并保持 2 秒 | 真棒 |
|
||||
| 8 | 挥动左手 | 挥动左手 | 伸展手臂挥动左手手势引导;等待 3 秒可播放引导动画 | 挥动左手 | 真棒;记录左手挥动空间 |
|
||||
| 9 | 挥动右手 | 挥动右手 | 伸展手臂挥动右手手势引导;等待 3 秒可播放引导动画 | 挥动右手 | 真棒;记录右手挥动空间 |
|
||||
| 10 | 原地跳一下 | 原地跳一下 | 跳跃姿势引导;等待 3 秒可播放引导动画 | 跳跃姿势 | 记录跳跃空间;真厉害,你是我见过最聪明的小朋友;别走开,现在开始我们的游戏吧;进入关卡选择 |
|
||||
|
||||
## 7. 固定文案与语音清单
|
||||
|
||||
以下文案需要作为屏幕中上方浮现文字,并同步语音播报。
|
||||
|
||||
```text
|
||||
欢迎你,小朋友,见到你真开心
|
||||
请你来到圆圈这里和我打个招呼吧
|
||||
你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧
|
||||
向左一步
|
||||
真棒
|
||||
回到中间来
|
||||
真棒
|
||||
向右一步
|
||||
真棒
|
||||
回到中间来
|
||||
真棒
|
||||
挥动左手
|
||||
真棒
|
||||
挥动右手
|
||||
真棒
|
||||
原地跳一下
|
||||
真厉害,你是我见过最聪明的小朋友
|
||||
别走开,现在开始我们的游戏吧
|
||||
```
|
||||
|
||||
## 8. 需要开发支持的识别能力
|
||||
|
||||
热身关当前流程需要支持以下识别能力:
|
||||
|
||||
1. 摄像头调用;
|
||||
2. 用户识别;
|
||||
3. 环境识别;
|
||||
4. 用户实际位置识别;
|
||||
5. 用户是否到达中央绿色圆环位置;
|
||||
6. 用户是否在绿色圆环内持续保持 2 秒;
|
||||
7. 用户是否到达左侧约半米绿色圆环位置;
|
||||
8. 用户是否到达右侧约半米绿色圆环位置;
|
||||
9. 招手 / 摆手手势识别;
|
||||
10. 挥动左手识别;
|
||||
11. 挥动右手识别;
|
||||
12. 原地跳跃姿势识别;
|
||||
13. 用户左右移动距离记录;
|
||||
14. 用户挥动手臂空间记录;
|
||||
15. 用户跳跃空间记录。
|
||||
|
||||
## 9. 需要开发支持的表现能力
|
||||
|
||||
热身关当前流程需要支持以下表现能力:
|
||||
|
||||
1. 横屏比例显示;
|
||||
2. 摄像头背景虚化;
|
||||
3. 用户位置生成角色剪影;
|
||||
4. 屏幕中央地面绿色圆环;
|
||||
5. 左侧约半米地面绿色圆环;
|
||||
6. 右侧约半米地面绿色圆环;
|
||||
7. 绿色圆环 2 秒选中状态;
|
||||
8. 圆圈消失特效;
|
||||
9. 招手手势引导;
|
||||
10. 伸展手臂挥动左手手势引导;
|
||||
11. 伸展手臂挥动右手手势引导;
|
||||
12. 跳跃姿势引导;
|
||||
13. 热身结束特效;
|
||||
14. 上浮字幕;
|
||||
15. 语音播报。
|
||||
|
||||
## 10. 热身数据记录要求
|
||||
|
||||
热身关需要记录以下数据,用于后续关卡的空间边界和行为坐标判断。
|
||||
|
||||
### 10.1 左右空间边界
|
||||
|
||||
用户完成向左一步后,记录该移动距离,作为后续关卡中的左侧空间边界。
|
||||
|
||||
用户完成向右一步后,记录该移动距离,作为后续关卡中的右侧空间边界。
|
||||
|
||||
后续关卡中,当用户身体主体覆盖安全边界线时,对应侧屏幕边缘出现虚影提醒。
|
||||
|
||||
后续关卡中,当用户身体主体超出安全边界线时:
|
||||
|
||||
1. 关卡内容暂停;
|
||||
2. 屏幕虚化;
|
||||
3. 屏幕中央地面出现绿色圆圈;
|
||||
4. 屏幕提示文案:
|
||||
|
||||
```text
|
||||
小朋友,要注意安全哦
|
||||
```
|
||||
|
||||
5. 用户需要回到中心绿色圆圈并保持 2 秒后,才能继续游戏内容。
|
||||
|
||||
### 10.2 手臂挥动空间
|
||||
|
||||
用户完成挥动左手后,记录用户挥动左手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
用户完成挥动右手后,记录用户挥动右手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
### 10.3 跳跃空间
|
||||
|
||||
用户完成原地跳一下后,记录用户跳跃空间,保存为该用户对应的行为坐标。
|
||||
|
||||
## 11. 热身关完成条件
|
||||
|
||||
热身关完成条件为用户按顺序完成以下流程:
|
||||
|
||||
1. 到达中央圆环位置并保持 2 秒;
|
||||
2. 完成招手 / 摆手手势;
|
||||
3. 到达左侧约半米圆环位置并保持 2 秒;
|
||||
4. 记录左侧空间边界;
|
||||
5. 回到中央圆环位置并保持 2 秒;
|
||||
6. 到达右侧约半米圆环位置并保持 2 秒;
|
||||
7. 记录右侧空间边界;
|
||||
8. 回到中央圆环位置并保持 2 秒;
|
||||
9. 完成挥动左手;
|
||||
10. 记录左手挥动空间;
|
||||
11. 完成挥动右手;
|
||||
12. 记录右手挥动空间;
|
||||
13. 完成原地跳一下;
|
||||
14. 记录跳跃空间;
|
||||
15. 播放热身结束特效和结束语音;
|
||||
16. 进入关卡选择。
|
||||
|
||||
## 12. 数据保存方式
|
||||
|
||||
左右空间边界、手臂挥动空间、跳跃空间仅在当前 Demo 体验会话内保存。
|
||||
|
||||
这里的“当前 Demo 体验会话”指用户本次打开并体验 Demo 的过程。当用户关闭 Demo、刷新页面、退出当前体验流程、重新进入 Demo,或更换设备后,系统不再沿用上一次热身记录的数据,需要重新完成热身关并重新记录。
|
||||
|
||||
采用仅当前 Demo 体验会话内保存的原因:
|
||||
|
||||
1. 每名用户的身高、体型、动作幅度不同,安全边界和行为坐标会发生变化。
|
||||
2. 当前 Demo 不做特定用户识别,无法确认下一次体验的是否仍是同一名用户。
|
||||
3. 用户所处的体验环境可能变化,包括房间大小、摄像头位置、屏幕位置和站立距离。
|
||||
4. 为保证安全,每次体验都需要重新对环境和距离进行安全检查。
|
||||
|
||||
## 13. 后续待确认事项
|
||||
|
||||
当前暂无待确认事项。
|
||||
@@ -0,0 +1,117 @@
|
||||
# 寓教于乐发现页临时入口设计
|
||||
|
||||
> 日期:2026-05-09
|
||||
> 适用范围:平台入口发现页、儿童动作识别娱乐教育内容线临时入口
|
||||
> 文档性质:产品与前端落地边界
|
||||
|
||||
## 1. 目标
|
||||
|
||||
为儿童动作识别娱乐教育内容线提供一个临时入口。
|
||||
|
||||
入口放置在平台“发现”页面内,作为独立标签展示,标签名称为:
|
||||
|
||||
```text
|
||||
寓教于乐
|
||||
```
|
||||
|
||||
后续生产的该内容线模板和游戏关卡,都放置在“寓教于乐”独立标签下。
|
||||
|
||||
该内容线当前只覆盖儿童动作识别 Demo 内容。后续创作环节需要继续对该板块内容做区分和独立管理,不把普通公开作品仅凭近似教育题材自动归入本板块。
|
||||
|
||||
## 2. 展示边界
|
||||
|
||||
寓教于乐内容不直接展示在以下位置:
|
||||
|
||||
1. 推荐页;
|
||||
2. 发现页的推荐标签;
|
||||
3. 发现页的今日标签;
|
||||
4. 发现页的分类标签;
|
||||
5. 发现页的排行标签;
|
||||
6. 发现页搜索结果。
|
||||
|
||||
寓教于乐内容只在“发现 / 寓教于乐”标签下展示。
|
||||
|
||||
“寓教于乐”标签在发现页频道列表中放在最后,桌面端和移动端都显示。移动端访问该内容线的动作识别 Demo 时,需要提示横屏体验。
|
||||
|
||||
## 3. 开关规则
|
||||
|
||||
该入口需要支持灵活开关。
|
||||
|
||||
开关打开时:
|
||||
|
||||
1. 发现页显示“寓教于乐”标签;
|
||||
2. “寓教于乐”标签下展示该内容线内容;
|
||||
3. 该内容线内容仍不进入推荐、今日、分类、排行和搜索结果。
|
||||
|
||||
开关关闭时:
|
||||
|
||||
1. 发现页隐藏“寓教于乐”标签;
|
||||
2. 隐藏“寓教于乐”标签下内容;
|
||||
3. 该内容线内容不进入推荐、今日、分类、排行和搜索结果;
|
||||
4. 该内容线内容完全不可见,公开作品搜索、作品号搜索直达、公开详情深链、浏览历史入口等平台公开入口都不能打开该内容。
|
||||
|
||||
## 4. 内容识别规则
|
||||
|
||||
临时阶段使用作品标签识别寓教于乐内容。
|
||||
|
||||
当公开作品标签中存在一个精确等于以下文本的标签:
|
||||
|
||||
```text
|
||||
寓教于乐
|
||||
```
|
||||
|
||||
则该作品归入寓教于乐内容线。
|
||||
|
||||
识别规则为精确匹配,不做包含匹配,不兼容空格、大小写变体或同义标签,例如“教育”“儿童教育”“动作教育”都不视为寓教于乐内容。
|
||||
|
||||
关闭开关时,即使作品具备精确的“寓教于乐”标签,也不允许通过任何平台公开展示入口或搜索入口访问。
|
||||
|
||||
## 5. 技术落地边界
|
||||
|
||||
本次只做前端入口和前端展示过滤,不新增后端接口。
|
||||
|
||||
前端通过功能开关控制入口显隐。
|
||||
|
||||
开关环境变量:
|
||||
|
||||
```text
|
||||
VITE_ENABLE_EDUTAINMENT_ENTRY
|
||||
```
|
||||
|
||||
默认开启。
|
||||
|
||||
当该变量显式配置为以下值时,入口关闭:
|
||||
|
||||
```text
|
||||
false
|
||||
0
|
||||
off
|
||||
no
|
||||
```
|
||||
|
||||
## 6. 验收点
|
||||
|
||||
1. 开关打开时,发现页显示“寓教于乐”标签。
|
||||
2. 开关关闭时,发现页不显示“寓教于乐”标签。
|
||||
3. 带有“寓教于乐”标签的公开作品不进入推荐页。
|
||||
4. 带有“寓教于乐”标签的公开作品不进入发现页推荐、今日、分类、排行和搜索结果。
|
||||
5. 带有“寓教于乐”标签的公开作品只在“发现 / 寓教于乐”标签下展示。
|
||||
6. “寓教于乐”标签位于发现页频道列表最后,桌面端和移动端均可见。
|
||||
7. 开关关闭后,带有“寓教于乐”标签的公开作品不可通过作品号搜索、公开详情深链或浏览历史入口打开。
|
||||
8. 标签识别只接受精确等于“寓教于乐”的作品标签,近似标签不归入该内容线。
|
||||
|
||||
## 7. 待补充事项
|
||||
|
||||
“寓教于乐”标签下暂无内容时的空状态文案待定。落地时可先复用平台现有空状态组件,但不新增功能说明类长文案。
|
||||
|
||||
## 8. 第 1-2 项工程落地状态
|
||||
|
||||
第 1 项“发现页入口与过滤”和第 2 项“搜索 / 深链 / 历史入口拦截”已进入前端落地阶段,当前实现口径如下:
|
||||
|
||||
1. 入口开关由 `VITE_ENABLE_EDUTAINMENT_ENTRY` 控制,默认开启,显式配置 `false`、`0`、`off`、`no` 时关闭。
|
||||
2. 内容识别集中在 `src/components/platform-entry/platformEdutainmentVisibility.ts`,只读取公开作品原始 `themeTags`,且只接受精确等于“寓教于乐”的标签。
|
||||
3. `src/components/rpg-entry/RpgEntryHomeView.tsx` 已在发现页频道末尾追加“寓教于乐”频道,并将该类作品从推荐、今日、分类、排行、搜索、本地搜索兜底和桌面推荐模块中过滤。
|
||||
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` 已复用同一过滤 helper,避免推荐运行态自动启动寓教于乐作品,并在公开详情、作品号直达和公开详情深链等公开入口保留不可见保护。
|
||||
5. 浏览历史入口会优先按当前公开作品集合匹配作品标签;匹配到“寓教于乐”作品且开关关闭时不再展示历史入口。
|
||||
6. `/child-motion-demo` 本地动作 Demo 直达路由也复用同一开关;开关关闭时不匹配独立 Demo 应用,回落到主站入口。
|
||||
7. 定向回归覆盖在 `src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`src/components/platform-entry/platformEdutainmentVisibility.test.ts` 和 `src/routing/appRoutes.test.ts`,包含频道顺序、开关关闭、普通列表过滤、搜索过滤、作品号直达拦截、Demo 直达路由拦截和精确标签识别。
|
||||
@@ -28,3 +28,13 @@
|
||||
- 锁定的模板卡统一以“敬请期待”作为状态标注,不再显示“锁定”。
|
||||
- RPG 入口展示为“角色扮演 / 剧情演绎,冒险成长”,拼图入口展示为“拼图 / 创意礼物,生活分享”。
|
||||
- 忙碌状态仅保留在模块标题行的轻量状态中,避免占用每张可用卡片的首要视觉层级。
|
||||
|
||||
## 2026-05-07 玩法参考图
|
||||
|
||||
1. 每个玩法入口都必须配置一张 `public/creation-type-references/` 下的参考图。
|
||||
2. 当前创作 Tab 顶部玩法卡带、旧创作中心卡带和玩法类型弹层都消费同一份 `PLATFORM_CREATION_TYPES.imageSrc`,避免多入口视觉漂移。
|
||||
3. 图片只承担玩法识别和氛围锚定,不在卡片上叠加规则说明文案。
|
||||
4. 移动端卡片仍以紧凑横滑为主,参考图使用暗色遮罩承接标题,文本不得溢出卡片。
|
||||
5. 当前创作 Tab 的可见玩法卡必须真实渲染 `img`,不能只在隐藏弹窗或旧入口中配置图片。
|
||||
6. 参考图卡片上的标题和副标题必须显式使用白色文字,并配合底部加深渐变与文字阴影;禁止依赖 `text-inherit`,避免黑字叠在暗蒙版上。
|
||||
7. 当前创作 Tab 顶部不再保留“10分钟创作一个精品互动玩法”标题,玩法参考图卡带直接作为首屏入口;移动端卡带必须支持横向拖动滑动。
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
- 新增“分类” Tab,用作品标签聚合所有公开发布作品。
|
||||
- 强化“创作” Tab 的导航视觉权重,让它在底部导航中居中并更醒目。
|
||||
- 登录态底部导航顺序为:首页、分类、创作、存档、我的。
|
||||
- 未登录态底部导航只保留:首页、创作、分类,其中创作保持居中。
|
||||
- 登录态底部导航顺序为:推荐、发现、创作、草稿、我的。
|
||||
- 未登录态底部导航只保留:推荐、创作、发现,其中创作保持居中。
|
||||
|
||||
## 2. 数据边界
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
|
||||
底部导航展示 5 个入口:
|
||||
|
||||
1. 首页
|
||||
2. 分类
|
||||
1. 推荐
|
||||
2. 发现
|
||||
3. 创作
|
||||
4. 存档
|
||||
4. 草稿
|
||||
5. 我的
|
||||
|
||||
创作入口位于第三位,视觉上使用更大的图标壳、轻微上浮、渐变高亮和阴影,保证它是主行动入口。
|
||||
@@ -36,11 +36,11 @@
|
||||
|
||||
底部导航展示 3 个入口:
|
||||
|
||||
1. 首页
|
||||
1. 推荐
|
||||
2. 创作
|
||||
3. 分类
|
||||
3. 发现
|
||||
|
||||
不展示“存档”和“我的”,避免未登录用户在底部导航看到必须登录后才有价值的入口。创作入口位于第二位,保持几何居中。
|
||||
不展示“草稿”和“我的”,避免未登录用户在底部导航看到必须登录后才有价值的入口。创作入口位于第二位,保持几何居中,并沿用原推荐 Tab 的星光图标;推荐 Tab 改用游戏手柄图标。
|
||||
|
||||
### 3.3 桌面端
|
||||
|
||||
@@ -59,6 +59,6 @@
|
||||
|
||||
- 登录态移动端底部导航顺序准确,创作在 5 个 Tab 中居中。
|
||||
- 未登录态移动端底部导航只显示 3 个 Tab,创作在中间。
|
||||
- 分类 Tab 能按标签切换并展示公开作品。
|
||||
- 发现 Tab 能按标签切换并展示公开作品。
|
||||
- 创作 Tab 在移动端和桌面端都比普通 Tab 更醒目。
|
||||
- 不修改 server-node,不新增后端逻辑。
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 平台创作 Tab 模板入口设计
|
||||
|
||||
更新时间:`2026-05-07`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
创作 Tab 恢复为模板选择入口,但不回到旧的大卡片选择面板:
|
||||
|
||||
1. 首屏保留现有创作页布局骨架,顶部标题固定为“10分钟创作一个精品互动玩法”。
|
||||
2. 选择模板入口改为横向 Tab,数据来自 `src/config/newWorkEntryConfig.ts` 的可见玩法配置。
|
||||
3. 默认选中“拼图”模板,并在创作 Tab 内直接展示拼图创作表单。
|
||||
4. 智能创作入口从可见模板中隐藏,保留既有 `creative-agent` 运行链路用于后续内部恢复或草稿目标打开。
|
||||
5. 草稿、发现、我的等一级 Tab 职责不变,作品管理仍在草稿 Tab。
|
||||
|
||||
## 2. 页面结构
|
||||
|
||||
移动端和桌面端共用同一信息结构:
|
||||
|
||||
```text
|
||||
标题:10分钟创作一个精品互动玩法
|
||||
模板 Tab:拼图 / 方洞挑战 / 视觉小说 / AIRP
|
||||
默认内容:拼图创作表单
|
||||
```
|
||||
|
||||
拼图表单嵌入创作 Tab 时:
|
||||
|
||||
- 不展示工作台返回按钮。
|
||||
- 不重复展示“创建拼图”标题。
|
||||
- 保留表单内的拼图模板、参考图上传、画面描述和生图模型选择。
|
||||
- 生成草稿仍走登录保护,未登录时先触发登录流程。
|
||||
|
||||
## 3. 交互
|
||||
|
||||
1. 打开“创作”一级 Tab 时默认停留在拼图 Tab,不主动创建拼图 session。
|
||||
2. 点击拼图表单“生成草稿”后,才创建拼图 session 并执行 `compile_puzzle_draft`。
|
||||
3. 拼图表单内的模板按钮使用 `tablist / tab` 语义,点击后只填充画面描述。
|
||||
4. 点击非拼图且已开放的模板 Tab 时,进入该玩法既有工作台;未开放模板保持禁用。
|
||||
5. `creative-agent` 不出现在模板 Tab 和选择弹层中,不再作为创作 Tab 首屏入口。
|
||||
|
||||
## 4. 验收
|
||||
|
||||
1. 点击“创作”后首屏出现“10分钟创作一个精品互动玩法”。
|
||||
2. 顶部选择模板入口为 Tab,拼图 Tab 默认 `aria-selected=true`。
|
||||
3. 创作 Tab 默认显示拼图创作表单内容,且不显示旧“Hi, 朋友”、输入框或智能创作快捷按钮。
|
||||
4. 隐藏的智能创作类型不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中。
|
||||
5. 草稿页返回创作页后仍回到同一模板入口,并可保留拼图表单草稿内容。
|
||||
|
||||
## 5. 嵌入式表单 UI 细节
|
||||
|
||||
2026-05-10 补充:抓大鹅与视觉小说作为创作 Tab 内嵌表单时,风格类横滑选择器应统一使用浅底卡片、柔和玫瑰色选中态和小圆点确认标记。不要使用大面积黑色渐变、黑底胶囊标签或高饱和红色外框,以免在输入框下方误读为错误提示。
|
||||
|
||||
嵌入式表单控件保持以下口径:
|
||||
|
||||
1. 大文本输入框使用白底、低饱和边框和轻量 focus ring。
|
||||
2. 风格选择区作为独立浅色分组承载横滑卡片,移动端只横向滚动,不挤压生成按钮。
|
||||
3. 风格卡标签使用浅底胶囊,保证图片仍是主体。
|
||||
4. 难度等分段选项可以使用主品牌色,但选中态需要降低阴影和饱和度。
|
||||
5. UI 中不补充玩法规则说明文案,保持创作入口清爽。
|
||||
@@ -0,0 +1,116 @@
|
||||
# 平台移动端推荐、发现与草稿 Tab 改版设计
|
||||
|
||||
更新时间:`2026-05-05`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本次只调整平台入口的信息架构与移动端视觉,不新增后端接口:
|
||||
|
||||
1. 原“首页”一级 Tab 对用户改名为“推荐”,进入后直接展示原首页推荐榜单启动后的公开游戏内容流。
|
||||
2. 原“首页”中的搜索、今日游戏、游戏分类等探索内容移动到第二个一级 Tab;第二个 Tab 对用户命名为“发现”。
|
||||
3. 原“排行”页内容并入“发现”页的子 Tab 中,不再作为一级主 Tab 独立展示。
|
||||
4. 创作页只保留新建创作入口;原创作页作品列表拆到一级“草稿” Tab,替换原“存档” Tab。
|
||||
5. 原“存档”列表结构并入“我的”页面的“玩过”列表弹层,作为每个已玩作品的可继续存档入口。
|
||||
6. 移动端推荐页与底部 Tab 栏参考用户给定样式,使用大画面推荐流、顶部品牌/通知、悬浮胶囊底部导航;保留当前平台已有明暗两套主题色 token。
|
||||
|
||||
## 2. 状态映射
|
||||
|
||||
为降低迁移风险,前端内部 `PlatformHomeTab` 仍复用既有状态值:
|
||||
|
||||
- `home`:用户看到“推荐”。
|
||||
- `category`:用户看到“发现”,内容包含搜索、今日游戏、分类和排行子 Tab。
|
||||
- `create`:用户看到“创作”,只承担新建入口。
|
||||
- `saves`:用户看到“草稿”,承载原创作页作品列表。
|
||||
- `profile`:用户看到“我的”,其中“玩过”弹层合并存档入口。
|
||||
|
||||
旧 `category` 状态此前承载“排行”,本次不改状态名,只改用户文案和页面内容,避免详情页返回目标、测试辅助和历史路由状态大范围迁移。
|
||||
|
||||
## 3. 推荐页
|
||||
|
||||
移动端推荐页默认不展示搜索和频道横滑条,进入一级“推荐”后直接渲染公开作品启动后的内容:
|
||||
|
||||
- 数据来源沿用 `featuredEntries + latestEntries` 去重后的公开作品列表。
|
||||
- 首个可运行作品自动进入推荐页内嵌运行态,主视口不再展示作品封面卡。
|
||||
- 主视口占据顶部栏与作品信息区之间的主要空间,使用平台主题 token 控制运行容器背景、边框、阴影、加载态文字和错误态按钮;亮色主题不得残留纯黑底白字加载块。
|
||||
- 主视口下方展示当前作品操作区与基础信息:作者头像、作者名、作品名、点赞按钮、点赞数、分享按钮、改造按钮;不展示游玩次数、评论入口或额外心形收藏入口,不写规则说明类文案。
|
||||
- 作品信息区不再提供详情箭头或点击详情入口;点击该区域无效,上滑切换下一个推荐作品,下滑切换上一个推荐作品。
|
||||
- 推荐页切换参考短视频上下滑交互:当前运行画布位于中间,上一个/下一个作品的封面预览提前挂载在画布上下屏幕外;拖动作品信息区时三屏轨道跟随位移,松手后完成切换或回弹。
|
||||
- 用户停留在推荐页时,底部当前 Tab 从“推荐”切换为“下一个”,图标使用向下的倒三角 / 双下箭头语义,点击后切换下一个推荐作品。
|
||||
- 推荐页不再展示额外的底部作品切换块;当前作品的点赞、分享与改造在推荐页底部操作区直接完成,其它作品深层操作继续由详情页和作品自身运行态承接。
|
||||
- 推荐页嵌入运行只调整平台外壳容器、主题注入和玩法壳层配色,不改写作品数据、关卡设定、道具设定或图片资产。
|
||||
- 屏幕外预览只允许使用公开封面、作品名和类型,不提前启动其它作品 run,不触发道具、计时、存档或作品数据变更。
|
||||
- 推荐页嵌入拼图玩法时隐藏拼图左上返回按钮,并在设置弹层中隐藏退出入口;作品切换前对当前拼图 run 执行既有“保存并退出”收口,正式 run 的交互状态以已写回后端的快照为准。
|
||||
- 底部操作区移除游玩次数、评论功能和额外心形收藏入口;点赞按钮可直接点击并刷新公开读模型返回的点赞数,分享按钮复制作品号与公开作品链接,改造按钮只保留改造 icon 并复用既有改造入口。操作按钮只响应自身点击,按钮外的信息区点击无效,拖动时仍跟随整张推荐卡上下切换作品。
|
||||
- 无数据、加载中、启动失败和暂不支持内嵌运行的作品沿用短状态文案。
|
||||
|
||||
桌面端仍保持现有首页布局,只把一级导航文案从“首页”改为“推荐”。
|
||||
|
||||
## 4. 发现页
|
||||
|
||||
发现页承接原首页探索能力和原排行能力,子 Tab 为:
|
||||
|
||||
1. 推荐:原首页推荐内容流,可作为发现页内的快速回看。
|
||||
2. 今日:原首页“今日游戏”。
|
||||
3. 分类:原首页“游戏分类”。
|
||||
4. 排行:原一级“排行”页四榜切换。
|
||||
|
||||
移动端发现页顶部保留搜索框和子 Tab;分类内容继续使用纵向应用商店式列表;排行内容继续使用榜单行。
|
||||
|
||||
## 5. 创作与草稿
|
||||
|
||||
创作页只保留新建创作入口:
|
||||
|
||||
- 继续复用 `CustomWorldCreationStartCard` 和现有创作类型弹窗。
|
||||
- 不在创作页下方展示作品列表,避免“创作入口”和“作品管理”挤在同一首屏。
|
||||
|
||||
草稿页复用创作中心作品架:
|
||||
|
||||
- 默认显示全部作品列表,保留草稿/已发布筛选。
|
||||
- 入口、删除、分享、领取拼图激励等行为全部复用现有 `CustomWorldCreationHub` 的作品卡逻辑。
|
||||
- 一级底部 Tab 文案为“草稿”,内部仍可按草稿与已发布筛选。
|
||||
|
||||
## 6. 我的页玩过列表
|
||||
|
||||
“我的”页面的“玩过”列表弹层合并存档结构:
|
||||
|
||||
- 顶部仍展示总游戏时长。
|
||||
- 列表先展示可恢复存档,使用原 `SaveArchiveCard` 的字段结构和恢复行为。
|
||||
- 再展示已玩作品统计列表,保持作品号、最近时间和时长。
|
||||
- 若某个存档被点击,必须继续走既有后端恢复接口,不在前端拼接运行态。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 移动端底部导航非推荐页显示“推荐 / 发现 / 创作 / 草稿 / 我的”,未登录时显示“推荐 / 创作 / 发现”;停留在推荐页时当前 Tab 显示“下一个”。
|
||||
2. 点击“推荐”直接看到公开作品启动后的内容,不再先看到搜索框、频道 Tab 或封面卡流。
|
||||
3. 点击“发现”可看到搜索、推荐、今日、分类、排行子 Tab。
|
||||
4. 点击“草稿”看到原创作页作品列表。
|
||||
5. 点击“创作”只看到新建创作入口。
|
||||
6. “我的”里的“玩过”弹层包含原存档列表入口,点击存档能继续恢复。
|
||||
7. 移动端底部导航为悬浮胶囊样式,保留当前明暗主题色变量,不新增第三套主题。
|
||||
8. 推荐页不出现额外底部作品卡或横滑切换块,运行视口、加载态和错误态跟随当前明暗主题。
|
||||
9. 拼图玩法背景、HUD、按钮、弹窗、排行榜和相似作品卡跟随平台主题色;暗色主题仍保留深色游戏感,亮色主题不得出现大面积固定黑底。
|
||||
10. 推荐页作品信息区点击无效,上滑切下一个、下滑切上一个;点击底部“下一个”也切下一个作品。
|
||||
11. 仅推荐页嵌入拼图态隐藏返回与设置内退出入口;详情页、新手引导和普通拼图运行态继续保留原有退出能力。
|
||||
12. 推荐页切换作品前,如果当前作品是拼图,必须先执行当前拼图 run 的退出收口,再启动下一作品。
|
||||
13. 已登录推荐页的上/下滑切换必须展示相邻作品的屏幕外预览,并在拖动不足阈值时回弹;相邻预览不得提前启动玩法运行态。
|
||||
14. 推荐页底部区域不展示游玩次数、评论入口和额外心形收藏入口;点赞、分享和改造都使用 icon 按钮,点赞数紧邻点赞按钮展示。
|
||||
|
||||
## 8. 2026-05-07 未登录三栏补充
|
||||
|
||||
未登录状态下底部导航只显示 3 个入口,顺序调整为:
|
||||
|
||||
1. 推荐
|
||||
2. 创作
|
||||
3. 发现
|
||||
|
||||
创作 Tab 必须位于中间,并使用原推荐 Tab 的星光图标,保持几何和视觉上的主行动入口。推荐 Tab 改用游戏手柄图标,避免与创作图标重复。
|
||||
|
||||
## 9. 2026-05-08 新用户默认发现与推荐门禁补充
|
||||
|
||||
未登录新用户首次进入平台时默认落在“发现”Tab,不再直接进入“推荐”Tab 的内嵌运行态。
|
||||
|
||||
- 未登录用户点击底部或侧边栏“推荐”Tab 时,页面可切到推荐封面预览态,同时打开登录弹窗。
|
||||
- 未登录状态下推荐页只展示当前推荐作品封面,不启动作品运行态,不展示推荐作品信息区。
|
||||
- 未登录用户点击推荐页封面时,再次打开同一个登录弹窗;登录成功后由既有受保护动作继续进入作品详情或玩法入口。
|
||||
- 未登录状态下点击“下一个”只切换下一张推荐封面,不触发登录弹窗,也不启动玩法。
|
||||
- 已登录用户继续沿用推荐页内嵌运行态、上下滑切换和底部“下一个”行为。
|
||||
@@ -17,9 +17,8 @@
|
||||
|
||||
1. 左侧保留返回按钮。
|
||||
2. 中间居中展示关卡主信息:
|
||||
- 第一行:拼图关卡名
|
||||
- 第二行:作者昵称
|
||||
- 第三行:`第 N 关`
|
||||
- 第一行:`第 N 关` 与拼图关卡名,二者保持同一行。
|
||||
- 第二行:紧凑倒计时。
|
||||
3. 右侧新增设置按钮。
|
||||
|
||||
同时移除以下冗余标识:
|
||||
@@ -31,12 +30,33 @@
|
||||
|
||||
### 1.1 2026-04-30 顶栏与底部工具补充
|
||||
|
||||
1. 顶栏作者信息不再只显示一行作者名,必须展示为作者头像与昵称组合;当前运行态只提供昵称时,用昵称首字生成圆形占位头像。
|
||||
2. 倒计时组件提升为顶栏中的强信息,字号、内边距和图标尺寸都需要明显大于作者昵称与关卡序号。
|
||||
1. 历史口径曾要求顶栏展示作者头像与昵称;该要求已被 2026-05-08 精简口径替代,当前拼图棋盘 HUD 不再展示作者信息。
|
||||
2. 倒计时组件保持为顶栏中的强信息,但需要采用紧凑尺寸,不得遮挡棋盘内容。
|
||||
3. 底部只保留 `提示 / 原图 / 冻结` 三个功能按钮,并整体居中展示;三个按钮触控面积和图标字号都需要放大。
|
||||
4. 底部不再展示“等待下一关候选”这类状态占位。通关后在三个道具按钮上方固定展示“下一关”按钮,展示条件只依赖当前关卡已通关,不依赖 `recommendedNextProfileId` 是否已有值。
|
||||
5. 点击底部“下一关”按钮继续调用运行时壳层已有 `onAdvanceNextLevel` 事件;正式 run 由后端 `next-level` 选择候选,本地 run 由 `local-next-level` 生成或接续下一关,前端不在按钮层自行决定下一关来源。
|
||||
|
||||
### 1.2 2026-05-08 推荐页嵌入态 HUD 精简
|
||||
|
||||
1. 拼图运行时顶栏不再展示作者头像、作者昵称或作者首字占位,作者信息只在推荐页作品信息、详情页和排行榜等非棋盘 HUD 区域展示。
|
||||
2. 顶部主信息压缩为两行:第一行 `第 N 关 + 关卡名`,第二行小号倒计时;倒计时不能使用此前的大号胶囊尺寸。
|
||||
3. 返回按钮和设置按钮上移并缩小移动端基础尺寸,减少对棋盘顶部空间的占用。
|
||||
4. 棋盘容器必须保留固定顶部安全区,确保关卡名和倒计时不会遮挡拼图内容。
|
||||
5. 推荐页嵌入时只调整外壳间距和 HUD 布局,不改写作品关卡、作者、道具、时间限制或图片资产等作品设定。
|
||||
|
||||
### 1.3 2026-05-08 主题色联动
|
||||
|
||||
1. 拼图运行态根容器、背景、棋盘底色、顶部 HUD、底部工具栏、加载态、失败弹窗、通关弹窗、道具确认弹窗、设置弹窗和相似作品卡必须通过 `src/index.css` 中的 `--puzzle-runtime-*` 主题变量控制。
|
||||
2. `platform-theme--light` 下拼图玩法背景应使用浅色平台底色与粉橙主色点缀,文字使用平台正文 token;不得继续使用固定 `bg-slate-950 text-white` 作为大面积底色。
|
||||
3. `platform-theme--dark` 下可保留深色棋盘氛围,但按钮、选中态、倒计时、弹窗边框和推荐页加载态仍要从主题 token 取色,避免局部色板漂移。
|
||||
4. 推荐页内嵌拼图时,父级必须保持 `platform-theme` 类可传递到 `PuzzleRuntimeShell`,不能让 runtime 脱离平台主题变量。
|
||||
|
||||
### 1.4 2026-05-08 推荐页嵌入态退出控制
|
||||
|
||||
1. 推荐页嵌入拼图时需要单独隐藏左上返回按钮和设置面板里的退出入口,避免把推荐流里的作品切换与普通退出混在一起。
|
||||
2. 推荐页切换到下一作品前,平台外壳会先沿用现有“保存并退出”语义收口当前拼图 run,再进入新作品。
|
||||
3. 该隐藏规则只作用于推荐页嵌入态,不影响详情页、新手引导或普通拼图入口。
|
||||
|
||||
### 2. 拼图块显示规则
|
||||
|
||||
运行时单块右下角编号全部移除。
|
||||
@@ -45,6 +65,7 @@
|
||||
|
||||
1. 玩家需要优先依赖画面主体、构图和色块识别位置。
|
||||
2. 编号覆盖会削弱“完整图片被逐步复原”的视觉奖励。
|
||||
3. 单块和合成后的拼图块只保留原图切片、外轮廓描边和必要的拖拽层级,不叠加额外的色块、暗层或蒙版,避免破坏原图识别。
|
||||
|
||||
### 3. 设置能力
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md):4-8 岁儿童动作识别互动玩法 Demo 固定热身关的横屏体验流程、识别目标、表现需求与待确认事项。
|
||||
- [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 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。
|
||||
@@ -12,6 +13,8 @@
|
||||
- [MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md](./MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md):移动端创作页新建作品模块最多占用首屏约 1/3 高度的紧凑布局设计。
|
||||
- [MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md](./MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md):移动端创作页作品列表至少 2 列的紧凑布局设计。
|
||||
- [PLATFORM_HOME_MOBILE_FEED_CARD_REDESIGN_2026-04-28.md](./PLATFORM_HOME_MOBILE_FEED_CARD_REDESIGN_2026-04-28.md):平台首页移动端参考图式信息流、双端公开作品卡 16:9 封面结构与点赞数读模型设计。
|
||||
- [PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md](./PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md):平台移动端推荐、发现、创作、草稿、我的 Tab 重新分工与推荐页/底部导航改版设计。
|
||||
- [PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md](./PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md):平台创作 Tab 恢复模板 Tab 入口、默认选中拼图并内嵌拼图创作表单的布局设计。
|
||||
- [PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md](./PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md):平台入口新增分类 Tab、登录态导航裁剪与创作 Tab 视觉强化设计。
|
||||
- [PLATFORM_BIG_FISH_ENTRY_HIDE_2026-04-28.md](./PLATFORM_BIG_FISH_ENTRY_HIDE_2026-04-28.md):平台入口暂时隐藏大鱼吃小鱼创作卡片,但保留现有玩法链路。
|
||||
- [UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md](./UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md):统一平台风与 RPG 像素风模态窗口外壳、交互边界和迁移顺序。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 943 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 999 KiB |
@@ -118,6 +118,11 @@
|
||||
- 早期方案曾在 `AuthGate` 层提供右上角全局账号信息条,并在 `GameShellRuntime` 中临时隐藏。
|
||||
- 2026-04-20 起,这个全局悬浮入口已整体下线,不再区分“平台显示 / 冒险隐藏”。
|
||||
- 原因是右上角高频观察区不适合承载账号入口,且平台内已经有更明确的页面内入口。
|
||||
|
||||
## 9. 2026-05-08 创作首页通知入口下线
|
||||
|
||||
- `CreativeAgentHome` 顶栏右上角不再展示“通知与账户”按钮,避免创作首页把通知入口放在首屏高频区域。
|
||||
- 账号入口仍保留在侧边栏底部,创作首页顶栏维持左侧菜单、居中品牌的轻量结构。
|
||||
- 当前账号相关入口统一保留在平台首页受保护动作、个人页、存档页与账号弹窗,不再占用全局悬浮层。
|
||||
|
||||
---
|
||||
|
||||
@@ -221,3 +221,8 @@
|
||||
- 后续新增怪物资源时,先检查红圈标注的实际落点,再调整锚点分档或单怪物偏移,避免出现“悬在地面上方”的状态。
|
||||
- 自定义世界里敌对角色已经先作为场景 NPC 存在,即使它同时携带 `characterId` 和 `monsterPresetId`,画布也不能直接沿用模板角色的 `groundOffsetY`;只要 encounter 自身有 `imageSrc` 或 `visual`,就按场景 NPC 自定义形象锚点处理。
|
||||
- 幕预览运行时还会构造“无 `characterId`、但有 `visual` 的场景 NPC”,这类和平相遇分支同样必须套用场景 NPC 自定义形象锚点,否则会停在画面中上部。
|
||||
|
||||
### 10.3 移动端固定整页画布缩放
|
||||
- 主站移动端以固定游戏画布体验为准,入口 `viewport` 需要锁定 `minimum-scale=1.0`、`maximum-scale=1.0` 和 `user-scalable=no`,同时保留 `viewport-fit=cover` 适配安全区。
|
||||
- 浏览器仍可能通过 iOS `gesture*` 或多指 `touchmove` 触发整页缩放,因此主站启动入口应统一调用 `lockMobileViewportZoom()` 拦截页面级捏合与快速双击缩放。
|
||||
- 不要在每个画布组件里重复注册缩放拦截;单指滚动、点击、拖拽应继续留给具体页面和玩法处理。
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。
|
||||
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
||||
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
||||
- [VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md](./VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md):记录视觉小说模板交接时的阅读顺序、常见坑和维护检查口径。
|
||||
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
||||
- [BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md](./BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md):记录大鱼吃小鱼发布成功后结果页反馈与作品列表刷新的修复口径。
|
||||
- [BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md](./BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md):记录大鱼作品列表 `items_json` 字段升级后的向后兼容修复口径,避免旧 JSON 直接打崩 works 接口。
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 视觉小说模板交接与维护经验 2026-05-07
|
||||
|
||||
## 1. 先读什么
|
||||
|
||||
新开发者接手视觉小说时,建议按这个顺序看:
|
||||
|
||||
1. [AI 原生视觉小说模板 PRD](../prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md)
|
||||
2. [视觉小说模板实现收口与交接说明](../technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md)
|
||||
3. [SpacetimeDB 表说明与查询目录](../technical/SPACETIMEDB_TABLE_CATALOG.md)
|
||||
4. [视觉小说 VN-03 Prompt 与 LLM 工具实现说明](../technical/VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md)
|
||||
5. [视觉小说 VN-11 负向扫描报告](../audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md)
|
||||
|
||||
## 2. 最容易踩的坑
|
||||
|
||||
1. 不要把 `visual_novel_runtime_history_entry` 当回放表来扩。
|
||||
2. 不要把 `visual_novel_runtime_event` 当业务回放数据源来用。
|
||||
3. 不要绕过平台资产对象去保存图片、音乐或文档。
|
||||
4. 不要让旧 TXT 迁移文档重新变成实现口径。
|
||||
5. 不要忘记发布后刷新作品架和公开聚合。
|
||||
6. 不要忘记退出登录时清理视觉小说私有状态。
|
||||
|
||||
## 3. 维护时的判断顺序
|
||||
|
||||
1. 先看是不是共享契约变化。
|
||||
2. 再看是不是 SpacetimeDB 表或 facade 变化。
|
||||
3. 然后看是不是作品架、广场或 runtime 的前端分流变化。
|
||||
4. 最后才看文档措辞和历史说明。
|
||||
|
||||
## 4. 常用检查
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run check:visual-novel-vn11
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
如果改了后端,再补:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts
|
||||
cargo test -p module-visual-novel
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
## 5. 一句话结论
|
||||
|
||||
视觉小说模板已经是平台内的正式模板玩法,不是外部平台迁移;后续维护只需要沿着 PRD、表目录、prompt 文档和这份交接说明往下走。
|
||||
863
docs/prd/AI_NATIVE_2048_GAMEPLAY_TEMPLATE_PRD_2026-05-05.md
Normal file
863
docs/prd/AI_NATIVE_2048_GAMEPLAY_TEMPLATE_PRD_2026-05-05.md
Normal file
@@ -0,0 +1,863 @@
|
||||
# AI 原生 2048 游戏玩法模板 PRD
|
||||
|
||||
更新时间:`2026-05-05`
|
||||
|
||||
## 0. 文档目的
|
||||
|
||||
这份 PRD 用于在当前平台内新增一条 `2048` 游戏玩法模板,并冻结它从创作入口、Agent 生成、结果页编辑、试玩、发布、公开运行到作品架展示的完整产品边界。
|
||||
|
||||
本次不是只做一个浏览器本地 2048 小游戏,也不是把经典 2048 规则随手写进某个前端组件。正式落地时,`2048` 必须作为平台内独立玩法类型接入现有创作中心、作品、广场、运行态和后端 DDD 分层。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定义
|
||||
|
||||
`2048` 是一个主题化数字合成玩法模板:百梦主通过 Agent 设定棋盘主题、合成链、视觉皮肤、目标格和难度参数,系统生成可试玩、可发布的 2048 作品;玩家通过滑动方向移动棋盘格,相同等级方块合并升级,直到达成目标格或棋盘无可行动作。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前接入级别
|
||||
|
||||
根据 `genarrative-play-type-integration` 的接入分级,本玩法按 **完整玩法闭环** 设计:
|
||||
|
||||
1. 新增玩法 ID:`twenty-forty-eight`。
|
||||
2. 展示名称:`2048`。
|
||||
3. 子标题:`主题合成棋盘`。
|
||||
4. 新建作品入口可展示,开放节奏由 `src/config/newWorkEntryConfig.ts` 控制。
|
||||
5. 支持 Agent 创作、草稿生成、结果页编辑、试玩、发布、公开运行、作品架和广场。
|
||||
6. 后端以 `server-rs + Axum + SpacetimeDB` 为服务侧真相源。
|
||||
7. 前端负责棋盘渲染、输入采集和动画表现,不绕过后端保存正式运行结果、榜单、扣费和发布状态。
|
||||
|
||||
工程命名采用 `twenty-forty-eight` 而不是裸 `2048`,避免 TypeScript、Rust 模块、文件名和路由中出现数字开头命名;面向用户的标题始终显示 `2048`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品定位
|
||||
|
||||
## 3.1 模板名称
|
||||
|
||||
1. 对外模板名称:`2048`。
|
||||
2. 对外子标题:`主题合成棋盘`。
|
||||
3. 开发代号:`TwentyFortyEight`。
|
||||
4. 工程玩法域:`twenty-forty-eight`。
|
||||
5. 后端模块命名:`twenty_forty_eight`。
|
||||
6. 公开作品号前缀:`TF-`。
|
||||
|
||||
## 3.2 核心乐趣
|
||||
|
||||
1. 玩家通过一次滑动改变整盘局势。
|
||||
2. 相同方块合并升级,形成清晰的成长反馈。
|
||||
3. 主题化合成链让数字升级变成可感知的叙事或收藏序列。
|
||||
4. 棋盘越来越拥挤,玩家在空间管理和短期收益之间做取舍。
|
||||
5. 达成目标格后可以继续冲击更高分,也可以结算分享成绩。
|
||||
|
||||
## 3.3 与现有玩法的区别
|
||||
|
||||
1. 不等同于拼图:不切图、不交换、不合并图片碎片。
|
||||
2. 不等同于抓大鹅:不做三消备选栏,不做物体堆叠点击。
|
||||
3. 不等同于方洞挑战:不做单次投放裁决,不靠反直觉规则制造误导。
|
||||
4. 不等同于大鱼吃小鱼:不做实时移动、吞噬和碰撞成长。
|
||||
5. 不复用 RPG 的世界、角色、章节或剧情推进结构。
|
||||
|
||||
---
|
||||
|
||||
## 4. 完整闭环目标
|
||||
|
||||
本玩法完整闭环必须补齐:
|
||||
|
||||
1. 平台创作中心选择 `2048`。
|
||||
2. Agent 对话收集主题、合成链、视觉风格、目标格和难度。
|
||||
3. 生成 2048 草稿。
|
||||
4. 进入结果页编辑作品名、简介、标签、封面、棋盘配置和方块皮肤。
|
||||
5. 支持发布前试玩。
|
||||
6. 发布作品。
|
||||
7. 玩家从作品详情、广场或作品架进入运行态。
|
||||
8. 后端初始化 run,保存种子、当前棋盘、分数、目标、状态和动作序列摘要。
|
||||
9. 玩家滑动方向后,前端提交动作,后端裁决新棋盘;前端按返回快照播放移动与合并动画。
|
||||
10. 达成目标、继续挑战、失败结算和排行榜写入都由后端正式裁决。
|
||||
11. 前端不得自行提交伪造分数、目标达成或榜单记录。
|
||||
|
||||
---
|
||||
|
||||
## 5. 明确不做
|
||||
|
||||
首版不做:
|
||||
|
||||
1. 不做多人实时对战。
|
||||
2. 不做复杂技能树或 RPG 数值养成。
|
||||
3. 不做可破坏障碍、棋盘机关和随机事件。
|
||||
4. 不支持玩家自定义任意 JavaScript 规则。
|
||||
5. 不在 UI 中默认展示长篇玩法说明、规则描述或内部字段解释。
|
||||
6. 不新增独立于平台之外的 2048 站点。
|
||||
7. 不复用 `customWorld`、`rpgWorld` 或旧 `server-node` 命名承载 2048 业务。
|
||||
8. 不把 LLM、生图、OSS 上传放进 SpacetimeDB reducer。
|
||||
|
||||
---
|
||||
|
||||
## 6. 创作锚点设计
|
||||
|
||||
Agent 型创作至少收集下面 5 个锚点:
|
||||
|
||||
| 锚点 | 字段建议 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 合成主题 | `themePrompt` | 决定棋盘整体题材,例如修仙境界、猫咪成长、城市建设、料理升级。 |
|
||||
| 合成链 | `tileLadder` | 决定从 `2` 到目标格的每一级显示名、图标提示和视觉差异。 |
|
||||
| 视觉皮肤 | `visualStyle` | 决定方块材质、色彩、背景、动效气质和封面方向。 |
|
||||
| 难度参数 | `difficultyConfig` | 决定棋盘尺寸、目标格、初始方块、生成概率和是否允许撤销。 |
|
||||
| 反馈节奏 | `feedbackRhythm` | 决定移动、合并、升级、目标达成、失败结算的反馈强度。 |
|
||||
|
||||
Agent 行为要求:
|
||||
|
||||
1. 优先接住百梦主的一句话灵感,不把创作变成问卷。
|
||||
2. 每轮最多追问 1 个最影响成品质量的问题。
|
||||
3. 当主题和合成链已经足够明确时,优先生成草稿。
|
||||
4. 合成链必须围绕同一主题递进,不允许出现互不相关的方块名。
|
||||
5. 进入结果页前至少生成 `2` 到目标格之间的完整等级定义。
|
||||
|
||||
---
|
||||
|
||||
## 7. 玩法规则
|
||||
|
||||
## 7.1 棋盘
|
||||
|
||||
首版默认支持:
|
||||
|
||||
1. `4x4` 标准棋盘。
|
||||
2. 可选 `5x5` 放松棋盘。
|
||||
3. 可选 `3x3` 高压棋盘。
|
||||
|
||||
发布默认值:
|
||||
|
||||
1. `boardSize = 4`。
|
||||
2. `targetTileValue = 2048`。
|
||||
3. 初始方块数量为 `2`。
|
||||
4. 新方块生成值为 `2` 或 `4`。
|
||||
5. `2` 的生成概率为 `90%`,`4` 的生成概率为 `10%`。
|
||||
|
||||
## 7.2 输入
|
||||
|
||||
玩家每次只能提交一个方向:
|
||||
|
||||
```ts
|
||||
type TwentyFortyEightMoveDirection = 'up' | 'down' | 'left' | 'right';
|
||||
```
|
||||
|
||||
交互支持:
|
||||
|
||||
1. 移动端滑动。
|
||||
2. 桌面端方向键。
|
||||
3. 桌面端按钮或触控手势兜底。
|
||||
|
||||
无效输入规则:
|
||||
|
||||
1. 如果某方向不会改变棋盘,该动作不消耗步数。
|
||||
2. 无效动作返回当前快照和 `moveAccepted = false`。
|
||||
3. 前端只展示轻量反馈,不弹长说明。
|
||||
|
||||
## 7.3 合并
|
||||
|
||||
合并规则遵循经典 2048:
|
||||
|
||||
1. 同一行或列按移动方向压缩。
|
||||
2. 相邻且同值的两个方块合并成一个更高值方块。
|
||||
3. 每个方块在一次移动中最多合并一次。
|
||||
4. 合并后的方块值为原值的 `2` 倍。
|
||||
5. 本次得分增加合并后方块值。
|
||||
6. 移动完成后,如果棋盘有变化,随机空格生成一个新方块。
|
||||
|
||||
规则核心应封装为可测试的领域引擎。实现时优先评估成熟 2048 规则库;如 Rust / TypeScript 生态无合适库,必须把自研规则收敛在 `module-twenty-forty-eight` 的纯函数内,并用黄金用例、属性测试和前后端 fixture 保证一致。
|
||||
|
||||
## 7.4 胜负
|
||||
|
||||
状态取值:
|
||||
|
||||
```ts
|
||||
type TwentyFortyEightRunStatus =
|
||||
| 'playing'
|
||||
| 'target_reached'
|
||||
| 'continued_after_target'
|
||||
| 'game_over'
|
||||
| 'abandoned';
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. 首次出现 `targetTileValue` 时进入 `target_reached`。
|
||||
2. 玩家可选择结算,或继续挑战。
|
||||
3. 继续挑战后状态变为 `continued_after_target`。
|
||||
4. 棋盘无空格且四个方向都无法移动时进入 `game_over`。
|
||||
5. 玩家主动退出未结算时为 `abandoned`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 草稿与结果页
|
||||
|
||||
## 8.1 结果页定位
|
||||
|
||||
2048 结果页是发布前的最小工作台,承担:
|
||||
|
||||
1. 编辑作品基本信息。
|
||||
2. 编辑棋盘参数。
|
||||
3. 编辑合成链显示名和视觉提示。
|
||||
4. 生成或上传封面。
|
||||
5. 进入试玩。
|
||||
6. 发布作品。
|
||||
|
||||
## 8.2 必备字段
|
||||
|
||||
```ts
|
||||
interface TwentyFortyEightResultDraft {
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
workTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
coverAssetId: string | null;
|
||||
themePrompt: string;
|
||||
visualStyle: string;
|
||||
boardConfig: TwentyFortyEightBoardConfig;
|
||||
tileLadder: TwentyFortyEightTileDefinition[];
|
||||
scoringConfig: TwentyFortyEightScoringConfig;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightBoardConfig {
|
||||
boardSize: 3 | 4 | 5;
|
||||
targetTileValue: 512 | 1024 | 2048 | 4096 | 8192;
|
||||
initialTileCount: 2 | 3 | 4;
|
||||
spawnValueWeights: Array<{ value: 2 | 4; weight: number }>;
|
||||
allowUndo: boolean;
|
||||
maxUndoCount: number;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightTileDefinition {
|
||||
value: number;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
colorToken: string;
|
||||
iconPrompt: string;
|
||||
imageSrc: string | null;
|
||||
assetId: string | null;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightScoringConfig {
|
||||
scoreMode: 'classic';
|
||||
leaderboardMetric: 'score_then_steps' | 'score_then_time';
|
||||
}
|
||||
```
|
||||
|
||||
## 8.3 字段约束
|
||||
|
||||
1. `workTitle` 必填,建议 `4~16` 个中文字符。
|
||||
2. `workDescription` 必填,建议 `12~80` 个中文字符。
|
||||
3. `workTags` 必须为 `3~6` 个中文短标签。
|
||||
4. `boardSize` 首版只能是 `3`、`4`、`5`。
|
||||
5. `targetTileValue` 必须存在于 `tileLadder`。
|
||||
6. `tileLadder` 必须从 `2` 开始按倍增连续覆盖到目标格。
|
||||
7. `shortLabel` 移动端格子内最多建议 `4` 个中文字符。
|
||||
8. `spawnValueWeights` 权重和必须大于 `0`,归一化后用于后端生成。
|
||||
9. `allowUndo = true` 时 `maxUndoCount` 必须在 `1~3`。
|
||||
|
||||
## 8.4 结果页 UI
|
||||
|
||||
结果页采用移动端优先的页签结构:
|
||||
|
||||
1. `基本信息`:标题、简介、标签。
|
||||
2. `棋盘`:棋盘尺寸、目标格、撤销次数、生成概率。
|
||||
3. `合成链`:每级方块的显示名、短标签、颜色和图标预览。
|
||||
4. `封面`:封面预览、生成、上传或历史素材选择。
|
||||
|
||||
交互要求:
|
||||
|
||||
1. 发布按钮放在结果页右下操作区,移动端固定在底部安全区。
|
||||
2. 试玩按钮与发布按钮并列,但试玩不触发发布阻断。
|
||||
3. 发布校验只在点击发布后进入独立发布面板展示。
|
||||
4. 合成链编辑使用独立面板或抽屉,不在当前列表下方展开大表单。
|
||||
5. 页面不默认展示玩法规则说明。
|
||||
|
||||
---
|
||||
|
||||
## 9. 作品发布与广场
|
||||
|
||||
## 9.1 发布阻断
|
||||
|
||||
发布前必须校验:
|
||||
|
||||
1. 作品标题非空。
|
||||
2. 简介非空。
|
||||
3. 标签数量为 `3~6`。
|
||||
4. 棋盘配置合法。
|
||||
5. 合成链完整覆盖到目标格。
|
||||
6. 目标格存在。
|
||||
7. 封面存在。
|
||||
8. 作者信息可读。
|
||||
|
||||
## 9.2 作品摘要
|
||||
|
||||
```ts
|
||||
interface TwentyFortyEightWorkSummary {
|
||||
workId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
gameName: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
coverImageSrc: string;
|
||||
boardSize: 3 | 4 | 5;
|
||||
targetTileValue: number;
|
||||
bestScore: number | null;
|
||||
playCount: number;
|
||||
likeCount: number;
|
||||
sourceSessionId: string;
|
||||
publicationStatus: 'draft' | 'published';
|
||||
updatedAt: string;
|
||||
publishedAt: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
## 9.3 广场卡片
|
||||
|
||||
广场卡片至少展示:
|
||||
|
||||
1. 封面。
|
||||
2. 作品名。
|
||||
3. 作者名。
|
||||
4. 标签。
|
||||
5. 棋盘规格和目标格。
|
||||
6. 进入游戏按钮。
|
||||
|
||||
不在卡片内展示完整规则说明。
|
||||
|
||||
---
|
||||
|
||||
## 10. 运行态设计
|
||||
|
||||
## 10.1 首屏
|
||||
|
||||
运行态首屏必须直接展示棋盘:
|
||||
|
||||
1. 顶部 HUD:分数、最高格、目标格、步数。
|
||||
2. 中部正方形棋盘。
|
||||
3. 底部轻量操作区:撤销、重新开始、退出。
|
||||
4. 移动端棋盘尽量贴近屏幕两侧安全边界。
|
||||
5. 桌面端棋盘居中,不使用营销式大卡片布局。
|
||||
|
||||
## 10.2 运行快照
|
||||
|
||||
```ts
|
||||
interface TwentyFortyEightRunSnapshot {
|
||||
runId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
status: TwentyFortyEightRunStatus;
|
||||
seed: string;
|
||||
board: TwentyFortyEightBoardSnapshot;
|
||||
score: number;
|
||||
bestTileValue: number;
|
||||
moveCount: number;
|
||||
undoRemaining: number;
|
||||
targetTileValue: number;
|
||||
reachedTargetAtMove: number | null;
|
||||
startedAtMs: number;
|
||||
updatedAtMs: number;
|
||||
endedAtMs: number | null;
|
||||
lastMove: TwentyFortyEightMoveResult | null;
|
||||
work: TwentyFortyEightWorkSummary;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightBoardSnapshot {
|
||||
size: 3 | 4 | 5;
|
||||
cells: TwentyFortyEightCellSnapshot[];
|
||||
}
|
||||
|
||||
interface TwentyFortyEightCellSnapshot {
|
||||
row: number;
|
||||
col: number;
|
||||
tile: TwentyFortyEightTileSnapshot | null;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightTileSnapshot {
|
||||
tileId: string;
|
||||
value: number;
|
||||
mergedFromTileIds: string[];
|
||||
spawnedAtMove: number;
|
||||
}
|
||||
|
||||
interface TwentyFortyEightMoveResult {
|
||||
direction: TwentyFortyEightMoveDirection;
|
||||
moveAccepted: boolean;
|
||||
scoreDelta: number;
|
||||
spawnedTile: TwentyFortyEightTileSnapshot | null;
|
||||
mergedTileIds: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## 10.3 前后端职责
|
||||
|
||||
前端负责:
|
||||
|
||||
1. 渲染棋盘、HUD、结算面板。
|
||||
2. 采集滑动、键盘和按钮输入。
|
||||
3. 根据后端返回的 `lastMove` 播放移动、合并和生成动画。
|
||||
4. 做乐观动画可以,但必须以服务端快照回正。
|
||||
|
||||
前端不负责:
|
||||
|
||||
1. 保存正式分数。
|
||||
2. 写入榜单。
|
||||
3. 伪造目标达成。
|
||||
4. 绕过后端生成新方块。
|
||||
5. 自行发布作品状态。
|
||||
|
||||
后端负责:
|
||||
|
||||
1. 创建 run。
|
||||
2. 按 seed 初始化棋盘。
|
||||
3. 裁决每次移动。
|
||||
4. 生成新方块。
|
||||
5. 保存分数、步数、最高格和状态。
|
||||
6. 裁决目标达成、继续挑战、失败和放弃。
|
||||
7. 写入排行榜和埋点事件。
|
||||
|
||||
---
|
||||
|
||||
## 11. 后端分层边界
|
||||
|
||||
正式实现必须遵循当前 `server-rs + Axum + SpacetimeDB` 路线:
|
||||
|
||||
1. `server-rs/crates/module-twenty-forty-eight`
|
||||
- 纯领域规则、棋盘移动、合并裁决、随机种子输入、分数计算、发布校验。
|
||||
- 不依赖 Axum、SpacetimeDB、OSS 或 LLM。
|
||||
2. `server-rs/crates/shared-contracts`
|
||||
- 暴露 Agent、作品、运行态、广场 DTO。
|
||||
3. `server-rs/crates/spacetime-module`
|
||||
- 存储 session、message、work profile、runtime run、leaderboard、event。
|
||||
- 表结构变化必须同步 `migration.rs` 与表目录。
|
||||
4. `server-rs/crates/spacetime-client`
|
||||
- 提供 api-server 调用 SpacetimeDB 的 typed facade。
|
||||
5. `server-rs/crates/api-server`
|
||||
- 暴露 `/api/creation/twenty-forty-eight/*` 与 `/api/runtime/twenty-forty-eight/*`。
|
||||
- 处理鉴权、错误 envelope、LLM turn、生图编排、OSS 资产和 HTTP facade。
|
||||
6. `platform-llm` / `platform-oss`
|
||||
- 分别承载外部模型和资产副作用。
|
||||
|
||||
涉及 SpacetimeDB 的表、reducer、procedure、绑定生成、前端 SDK 接入时,必须按 `spacetimedb-cli`、`spacetimedb-rust`、`spacetimedb-concepts`、`spacetimedb-typescript` 约束执行。
|
||||
|
||||
---
|
||||
|
||||
## 12. SpacetimeDB 表建议
|
||||
|
||||
首版建议新增:
|
||||
|
||||
1. `twenty_forty_eight_agent_session`
|
||||
2. `twenty_forty_eight_agent_message`
|
||||
3. `twenty_forty_eight_work_profile`
|
||||
4. `twenty_forty_eight_runtime_run`
|
||||
5. `twenty_forty_eight_leaderboard_entry`
|
||||
6. `twenty_forty_eight_event`
|
||||
|
||||
表职责:
|
||||
|
||||
| 表 | 职责 |
|
||||
| --- | --- |
|
||||
| `twenty_forty_eight_agent_session` | 创作会话、阶段、草稿、已发布 profile 绑定。 |
|
||||
| `twenty_forty_eight_agent_message` | Agent 对话消息和流式 turn 结果。 |
|
||||
| `twenty_forty_eight_work_profile` | 作品草稿、发布状态、封面、棋盘配置、合成链和统计投影。 |
|
||||
| `twenty_forty_eight_runtime_run` | 单次运行快照、动作摘要、分数、状态和结算时间。 |
|
||||
| `twenty_forty_eight_leaderboard_entry` | 按作品、用户、棋盘规格和目标格记录最好成绩。 |
|
||||
| `twenty_forty_eight_event` | 发布、试玩、开始、移动、达成目标、失败、结算等审计事件。 |
|
||||
|
||||
reducer / procedure 不允许调用 LLM、OSS、生图、HTTP 或非确定性外部服务。
|
||||
|
||||
---
|
||||
|
||||
## 13. API 设计
|
||||
|
||||
## 13.1 创作接口
|
||||
|
||||
统一前缀:
|
||||
|
||||
```text
|
||||
/api/creation/twenty-forty-eight
|
||||
```
|
||||
|
||||
建议接口:
|
||||
|
||||
1. `POST /api/creation/twenty-forty-eight/sessions`
|
||||
2. `GET /api/creation/twenty-forty-eight/sessions/{sessionId}`
|
||||
3. `POST /api/creation/twenty-forty-eight/sessions/{sessionId}/messages`
|
||||
4. `POST /api/creation/twenty-forty-eight/sessions/{sessionId}/messages/stream`
|
||||
5. `POST /api/creation/twenty-forty-eight/sessions/{sessionId}/actions`
|
||||
6. `POST /api/creation/twenty-forty-eight/sessions/{sessionId}/compile`
|
||||
7. `GET /api/creation/twenty-forty-eight/works`
|
||||
8. `GET /api/creation/twenty-forty-eight/works/{profileId}`
|
||||
9. `PUT /api/creation/twenty-forty-eight/works/{profileId}`
|
||||
10. `POST /api/creation/twenty-forty-eight/works/{profileId}/publish`
|
||||
11. `DELETE /api/creation/twenty-forty-eight/works/{profileId}`
|
||||
|
||||
## 13.2 运行接口
|
||||
|
||||
统一前缀:
|
||||
|
||||
```text
|
||||
/api/runtime/twenty-forty-eight
|
||||
```
|
||||
|
||||
建议接口:
|
||||
|
||||
1. `GET /api/runtime/twenty-forty-eight/gallery`
|
||||
2. `GET /api/runtime/twenty-forty-eight/gallery/{profileId}`
|
||||
3. `POST /api/runtime/twenty-forty-eight/works/{profileId}/runs`
|
||||
4. `GET /api/runtime/twenty-forty-eight/runs/{runId}`
|
||||
5. `POST /api/runtime/twenty-forty-eight/runs/{runId}/moves`
|
||||
6. `POST /api/runtime/twenty-forty-eight/runs/{runId}/undo`
|
||||
7. `POST /api/runtime/twenty-forty-eight/runs/{runId}/continue`
|
||||
8. `POST /api/runtime/twenty-forty-eight/runs/{runId}/restart`
|
||||
9. `POST /api/runtime/twenty-forty-eight/runs/{runId}/abandon`
|
||||
10. `POST /api/runtime/twenty-forty-eight/runs/{runId}/leaderboard`
|
||||
|
||||
移动请求:
|
||||
|
||||
```ts
|
||||
interface TwentyFortyEightMoveRequest {
|
||||
clientActionId: string;
|
||||
direction: TwentyFortyEightMoveDirection;
|
||||
baseSnapshotVersion: number;
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```ts
|
||||
interface TwentyFortyEightMoveResponse {
|
||||
snapshot: TwentyFortyEightRunSnapshot;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 前端落点
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
src/components/twenty-forty-eight-creation/TwentyFortyEightAgentWorkspace.tsx
|
||||
src/components/twenty-forty-eight-result/TwentyFortyEightResultView.tsx
|
||||
src/components/twenty-forty-eight-runtime/TwentyFortyEightRuntimeShell.tsx
|
||||
src/components/twenty-forty-eight-runtime/TwentyFortyEightBoard.tsx
|
||||
src/components/twenty-forty-eight-runtime/TwentyFortyEightHud.tsx
|
||||
src/services/twenty-forty-eight-creation/twentyFortyEightCreationClient.ts
|
||||
src/services/twenty-forty-eight-works/twentyFortyEightWorksClient.ts
|
||||
src/services/twenty-forty-eight-runtime/twentyFortyEightRuntimeClient.ts
|
||||
src/services/twenty-forty-eight-gallery/twentyFortyEightGalleryClient.ts
|
||||
```
|
||||
|
||||
平台入口接入时需要扩展:
|
||||
|
||||
1. `src/config/newWorkEntryConfig.ts`
|
||||
2. `src/components/platform-entry/platformEntryTypes.ts`
|
||||
3. `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
5. `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
6. `src/services/publicWorkCode.ts`
|
||||
|
||||
新增 selection stage:
|
||||
|
||||
```ts
|
||||
| 'twenty-forty-eight-agent-workspace'
|
||||
| 'twenty-forty-eight-result'
|
||||
| 'twenty-forty-eight-runtime'
|
||||
| 'twenty-forty-eight-gallery-detail'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. UI 要求
|
||||
|
||||
## 15.1 创作入口
|
||||
|
||||
入口卡片只表达:
|
||||
|
||||
1. `2048`
|
||||
2. `主题合成棋盘`
|
||||
3. 开放状态
|
||||
|
||||
不在入口卡片里堆规则说明。
|
||||
|
||||
## 15.2 Agent 工作台
|
||||
|
||||
工作台结构:
|
||||
|
||||
1. 对话流。
|
||||
2. 当前锚点摘要。
|
||||
3. 生成草稿动作。
|
||||
4. 进入结果页动作。
|
||||
|
||||
不展示世界观、角色、地点等 RPG 重结构。
|
||||
|
||||
## 15.3 运行态
|
||||
|
||||
运行态设计原则:
|
||||
|
||||
1. 棋盘是绝对主角。
|
||||
2. 移动端优先,单手可滑动。
|
||||
3. HUD 信息克制,只显示分数、目标、步数和最高格。
|
||||
4. 撤销、重新开始、退出使用 icon button 或短按钮。
|
||||
5. 目标达成、失败和排行榜使用独立弹窗或底部面板。
|
||||
6. 不把弹出面板实现成当前面板下方追加内容。
|
||||
|
||||
---
|
||||
|
||||
## 16. 测试与验收
|
||||
|
||||
## 16.1 领域测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
1. 向左移动压缩。
|
||||
2. 向右移动压缩。
|
||||
3. 单次移动中每个方块最多合并一次。
|
||||
4. 合并得分计算。
|
||||
5. 无效移动不生成新方块。
|
||||
6. 有效移动按 seed 生成新方块。
|
||||
7. 目标格达成。
|
||||
8. 无可行动作进入失败。
|
||||
9. 撤销次数消耗和快照恢复。
|
||||
|
||||
## 16.2 API 测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
1. 未登录不能创建作品和 run。
|
||||
2. 创建 session。
|
||||
3. 编译草稿。
|
||||
4. 发布校验阻断。
|
||||
5. 创建 run。
|
||||
6. 提交 move 后返回新快照。
|
||||
7. 无效 move 不增加步数。
|
||||
8. 达成目标后可继续挑战。
|
||||
9. 失败后不能继续提交 move。
|
||||
10. 排行榜只接受后端已结算 run。
|
||||
|
||||
## 16.3 前端测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
1. 入口展示与点击分流。
|
||||
2. Agent 工作台打开。
|
||||
3. 结果页编辑棋盘参数和合成链。
|
||||
4. 试玩进入运行态。
|
||||
5. 移动端滑动提交方向。
|
||||
6. 键盘方向键提交方向。
|
||||
7. 无效移动反馈。
|
||||
8. 目标达成弹窗。
|
||||
9. 失败结算弹窗。
|
||||
|
||||
## 16.4 建议验证命令
|
||||
|
||||
按改动范围执行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run test
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts twenty_forty_eight
|
||||
cargo test -p module-twenty-forty-eight
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
涉及 SpacetimeDB 表变化后:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate -- --rust-only
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
涉及 API smoke 时:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
启动后确认:
|
||||
|
||||
```text
|
||||
GET /healthz
|
||||
POST /api/creation/twenty-forty-eight/sessions
|
||||
POST /api/runtime/twenty-forty-eight/works/{profileId}/runs
|
||||
POST /api/runtime/twenty-forty-eight/runs/{runId}/moves
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. 并行任务拆分
|
||||
|
||||
## 任务 A:契约与共享类型
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/twentyFortyEightAgent.ts
|
||||
packages/shared/src/contracts/twentyFortyEightWorks.ts
|
||||
packages/shared/src/contracts/twentyFortyEightRuntime.ts
|
||||
packages/shared/src/contracts/twentyFortyEightGallery.ts
|
||||
server-rs/crates/shared-contracts/src/twenty_forty_eight_agent.rs
|
||||
server-rs/crates/shared-contracts/src/twenty_forty_eight_works.rs
|
||||
server-rs/crates/shared-contracts/src/twenty_forty_eight_runtime.rs
|
||||
server-rs/crates/shared-contracts/src/twenty_forty_eight_gallery.rs
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts twenty_forty_eight
|
||||
```
|
||||
|
||||
## 任务 B:领域规则模块
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-twenty-forty-eight/src/domain.rs
|
||||
server-rs/crates/module-twenty-forty-eight/src/application.rs
|
||||
server-rs/crates/module-twenty-forty-eight/src/rule_engine.rs
|
||||
server-rs/crates/module-twenty-forty-eight/src/random.rs
|
||||
server-rs/crates/module-twenty-forty-eight/src/lib.rs
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p module-twenty-forty-eight
|
||||
```
|
||||
|
||||
## 任务 C:SpacetimeDB 表与 facade
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-module/src/twenty_forty_eight.rs
|
||||
server-rs/crates/spacetime-module/src/lib.rs
|
||||
server-rs/crates/spacetime-module/src/migration.rs
|
||||
server-rs/crates/spacetime-client/src/twenty_forty_eight.rs
|
||||
docs/technical/SPACETIMEDB_TABLE_CATALOG.md
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate -- --rust-only
|
||||
npm run check:server-rs-ddd
|
||||
cd server-rs
|
||||
cargo check -p spacetime-module
|
||||
cargo check -p spacetime-client
|
||||
```
|
||||
|
||||
## 任务 D:API / SSE facade
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
server-rs/crates/api-server/src/twenty_forty_eight.rs
|
||||
server-rs/crates/api-server/src/twenty_forty_eight_sse.rs
|
||||
server-rs/crates/api-server/src/app.rs
|
||||
server-rs/crates/api-server/src/state.rs
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo check -p api-server
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
## 任务 E:前端创作、结果页与运行态
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
src/config/newWorkEntryConfig.ts
|
||||
src/components/platform-entry/*
|
||||
src/components/twenty-forty-eight-creation/*
|
||||
src/components/twenty-forty-eight-result/*
|
||||
src/components/twenty-forty-eight-runtime/*
|
||||
src/services/twenty-forty-eight-*
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run test -- twenty
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
## 任务 F:作品架、广场、分享与回归
|
||||
|
||||
写入范围:
|
||||
|
||||
```text
|
||||
src/components/custom-world-home/creationWorkShelf.ts
|
||||
src/services/publicWorkCode.ts
|
||||
src/components/common/PublishShareModal.tsx
|
||||
src/components/twenty-forty-eight-gallery/*
|
||||
docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md
|
||||
docs/technical/SPACETIMEDB_TABLE_CATALOG.md
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. 最小上线清单
|
||||
|
||||
1. `twenty-forty-eight` 入口可展示并进入工作台。
|
||||
2. Agent 可生成主题化 2048 草稿。
|
||||
3. 结果页可编辑基本信息、棋盘配置和合成链。
|
||||
4. 结果页可试玩。
|
||||
5. 运行态可完成标准 2048 移动、合并、生成新方块、目标达成和失败。
|
||||
6. 发布后作品进入作品架和广场。
|
||||
7. 玩家可从广场进入公开 run。
|
||||
8. 榜单只记录后端裁决的正式成绩。
|
||||
9. 前后端契约字段 camelCase / snake_case 映射明确。
|
||||
10. SpacetimeDB 表、migration、表目录和 bindings 同步。
|
||||
|
||||
---
|
||||
|
||||
## 19. 验收标准
|
||||
|
||||
当下面结果都成立时,视为 `2048` 玩法模板落地完成:
|
||||
|
||||
1. 平台有独立 `2048` 创作入口。
|
||||
2. 玩法 ID 使用 `twenty-forty-eight`。
|
||||
3. 能进入 2048 Agent 工作台。
|
||||
4. 能通过 Agent 生成草稿。
|
||||
5. 结果页可编辑作品名、简介、标签、棋盘参数、合成链和封面。
|
||||
6. 发布校验能阻断非法草稿。
|
||||
7. 试玩能进入 2048 运行态。
|
||||
8. 运行态支持移动端滑动和桌面方向键。
|
||||
9. 后端能裁决移动、合并、得分、新方块生成和胜负。
|
||||
10. 无效移动不增加步数,不生成新方块。
|
||||
11. 达成目标后可结算或继续挑战。
|
||||
12. 失败后不能继续提交移动。
|
||||
13. 发布作品能进入作品架、广场和分享链路。
|
||||
14. 排行榜只接受正式 run 的后端结算成绩。
|
||||
15. 新增表结构同步 `migration.rs`、表目录和 bindings。
|
||||
16. UI 不默认展示长篇规则说明,不把独立弹窗做成面板下方展开。
|
||||
17. 移动端和桌面端都能正常显示和操作。
|
||||
|
||||
---
|
||||
|
||||
## 20. 一句话结论
|
||||
|
||||
`2048` 在 Genarrative 中应被做成一个可创作、可换皮、可发布、可排行的主题合成棋盘模板:创作端让百梦主定义合成链和视觉承诺,运行端保持经典 2048 的滑动合并手感,服务端负责正式棋盘裁决、作品状态和成绩真相。
|
||||
@@ -468,8 +468,9 @@ interface CustomWorldCoverProfile {
|
||||
|
||||
1. 上传后先进入独立裁剪面板
|
||||
2. 裁剪框比例固定为 `16:9`
|
||||
3. 作者只能平移和缩放,不允许自由改比例
|
||||
4. 裁剪完成后,再提交给后端保存
|
||||
3. 作者直接在图片上拖拽裁剪框内部移动区域,拖拽四边或四角调整裁剪范围,不再通过参数滑杆调整
|
||||
4. 裁剪框调整过程中必须持续锁定 `16:9`,不允许自由改比例
|
||||
5. 裁剪完成后,再提交给后端保存
|
||||
|
||||
### 上传大小与格式限制
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 原生抓大鹅 Match3D 玩法创作工具与玩法系统 PRD
|
||||
|
||||
更新时间:`2026-04-30`
|
||||
更新时间:`2026-05-10`
|
||||
|
||||
## 0. 文档目的
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
## 1. 一句话定义
|
||||
|
||||
让百梦主通过 Agent 对话确认题材、需要消除次数和难度,系统编译出一个可试玩、可发布的单局抓大鹅玩法作品;玩家在 `10` 分钟倒计时内点击圆形空间中可见物品,把物品放入下方 `7` 格备选栏,每凑齐 `3` 个同物品 id 自动消除,最终清空圆形空间内全部物品即胜利。
|
||||
让百梦主在创作页通过表单确认题材主题和难度选项,系统根据难度选项派生需要消除次数与难度数值,并编译出一个可试玩、可发布的单局抓大鹅玩法作品;玩家在 `10` 分钟倒计时内点击圆形空间中可见物品,把物品放入下方 `7` 格备选栏,每凑齐 `3` 个同物品 id 自动消除,最终清空圆形空间内全部物品即胜利。
|
||||
|
||||
---
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
```text
|
||||
平台创作入口
|
||||
-> 选择“抓大鹅”
|
||||
-> Agent 对话确认题材、需要消除次数、难度
|
||||
-> 入口表单确认题材主题、难度选项
|
||||
-> 生成待发布结果页
|
||||
-> 编辑作品基础信息
|
||||
-> 发布前试玩
|
||||
@@ -62,7 +62,7 @@
|
||||
## 3.1 复用什么
|
||||
|
||||
1. 复用平台创作中心入口。
|
||||
2. 复用 Agent-first 创作体验。
|
||||
2. 复用拼图式创作页入口表单体验。
|
||||
3. 复用“创作会话 -> 结果页 -> 发布 -> 运行态”的平台主链。
|
||||
4. 复用作品基础信息、标签、封面、发布接口和作品管理体验。
|
||||
5. 复用现有 Rust / SpacetimeDB 后端基线。
|
||||
@@ -93,13 +93,13 @@ Match3D 必须形成独立玩法域,后续技术方案至少需要覆盖:
|
||||
首版 demo 必须满足:
|
||||
|
||||
1. 平台新增“抓大鹅”玩法创作入口。
|
||||
2. 创建流程采用 Agent 对话收集关键配置。
|
||||
3. Agent 必须在进入结果页前确认:
|
||||
2. 创建流程采用入口表单收集关键配置。
|
||||
3. 表单必须在进入结果页前确认:
|
||||
- 题材主题
|
||||
- 需要消除次数
|
||||
- 难度 `1~10`
|
||||
4. 支持系统自动补全配置,用户确认后开始创造。
|
||||
5. 题材阶段允许上传参考图片。
|
||||
- 3D 素材风格
|
||||
- 难度选项
|
||||
4. `需要消除次数` 与难度 `1~10` 数值不再作为独立输入框展示,由难度选项派生。
|
||||
5. 生成抓大鹅草稿消耗 `20` 光点,生成按钮必须显式展示。
|
||||
6. 结果页支持编辑游戏名称、标签、封面图等基础发布信息。
|
||||
7. 发布前支持试玩,并允许随时停止和修改配置。
|
||||
8. 发布不要求试玩通关。
|
||||
@@ -110,7 +110,8 @@ Match3D 必须形成独立玩法域,后续技术方案至少需要覆盖:
|
||||
13. 清空圆形空间中全部物品即胜利。
|
||||
14. 倒计时结束或备选栏满即失败。
|
||||
15. 胜利 / 失败后展示结算界面。
|
||||
16. 点击、入槽、消除、失败、胜利的即时反馈效果由前端先行呈现,后端负责权威确认、状态落库和成绩可信性。
|
||||
16. 入口页的 3D 素材风格选择会进入素材图提示词,并作为结果页手动 Rodin 3D 模型生成的默认提示词依据。
|
||||
17. 点击、入槽、消除、失败、胜利的即时反馈效果由前端先行呈现,后端负责权威确认、状态落库和成绩可信性。
|
||||
|
||||
---
|
||||
|
||||
@@ -137,13 +138,22 @@ Match3D 必须形成独立玩法域,后续技术方案至少需要覆盖:
|
||||
|
||||
## 6.1 创作方式
|
||||
|
||||
Match3D 首版参考拼图早期的 Agent 对话收集锚点,而不是拼图后期的纯表单入口。
|
||||
Match3D 首版参考拼图后期的入口表单收集方式,而不是早期的 Agent 对话锚点收集。
|
||||
|
||||
Agent 的职责是帮助用户确认可以直接编译 demo 的最小配置:
|
||||
表单的职责是帮助用户确认可以直接编译 demo 的最小配置:
|
||||
|
||||
1. 题材主题。
|
||||
2. 需要消除次数。
|
||||
3. 游戏难度。
|
||||
2. 3D 素材风格。
|
||||
3. 游戏难度选项。
|
||||
|
||||
`需要消除次数` 与游戏难度数值仍属于后端会话配置,但不再要求用户手填。当前入口页固定采用以下映射:
|
||||
|
||||
```text
|
||||
轻松 -> 需要消除 8 次,难度 2
|
||||
标准 -> 需要消除 12 次,难度 4
|
||||
进阶 -> 需要消除 16 次,难度 6
|
||||
硬核 -> 需要消除 20 次,难度 8
|
||||
```
|
||||
|
||||
## 6.2 必填配置
|
||||
|
||||
@@ -151,13 +161,13 @@ Agent 的职责是帮助用户确认可以直接编译 demo 的最小配置:
|
||||
|
||||
题材决定后续生成或选择物品素材的方向。用户可以自定义主题,例如水果、玩具、食物、符号等。
|
||||
|
||||
首版 demo 不接入真实图片生成。当前运行态可消除物统一使用参考图方向的 25 个积木件类型表现,不使用透明气泡,也不在图案上放文字标识。前端首版用差异化颜色、积木造型和 3D 程序化模型表现可消除物,避免玩家在堆叠状态下难以辨认。
|
||||
首版 demo 不接入真实图片生成。当前运行态可消除物统一使用题材方向的 25 个积木件类型表现,不使用透明气泡,也不在图案上放文字标识。前端首版用差异化颜色、积木造型和 3D 程序化模型表现可消除物,避免玩家在堆叠状态下难以辨认。
|
||||
|
||||
可消除物尺寸使用五档相对体积规则:XL 型相对体积为 `1.60~2.30`,L 型为 `1.25~1.60`,M 型为 `1.00`,XS 型为 `0.65~0.85`,S 型为 `0.35~0.50`。单局中 XL / L / M / XS / S 按本局使用的消除物类型数的 `20% / 30% / 30% / 15% / 5%` 分配;非整数配额按最大余数补齐,确保总数等于本局使用类型数量。同一关卡内同一个颜色和造型的物品只能对应一个尺寸档位;可存在同尺寸但不同颜色和造型的物品。后端运行态通过 `radius` 下发权威尺寸,前端只按快照表现。
|
||||
|
||||
### 需要消除次数
|
||||
|
||||
用户输入任意正整数。
|
||||
该字段由难度选项派生,不作为入口页独立输入框展示。
|
||||
|
||||
该字段不是胜利条件,而是本局总物品规模配置:
|
||||
|
||||
@@ -169,21 +179,23 @@ Agent 的职责是帮助用户确认可以直接编译 demo 的最小配置:
|
||||
|
||||
### 难度
|
||||
|
||||
用户输入 `1~10` 的数字,代表从低到高的难度感受。
|
||||
用户选择 `轻松 / 标准 / 进阶 / 硬核` 之一,系统派生 `1~10` 范围内的难度数值。
|
||||
|
||||
首版 demo 中,用户只需凭感觉选择难度;具体难度规则由系统内部解释。后续优化阶段再细化难度曲线、生成算法和遮挡策略。
|
||||
|
||||
## 6.3 自动配置
|
||||
### 3D 素材风格
|
||||
|
||||
如果用户不想逐项填写,系统可以自动补全题材、需要消除次数和难度。
|
||||
入口页在题材主题与难度之间展示 `3D素材风格` 横向滑动选择。首批固定选项为:
|
||||
|
||||
自动补全后的配置必须展示给用户确认,用户确认后才能开始创造。
|
||||
```text
|
||||
黏土手作 / 低多边形 / 玩具塑料 / 木质雕刻 / 体素积木 / 金属机甲 / 自定义
|
||||
```
|
||||
|
||||
## 6.4 参考图片
|
||||
每个内置选项使用 VectorEngine `gpt-image-2-all` 生成的画风参考图展示;参考图保存在 `public/match3d-style-references/`,只作为入口选择的视觉提示,不作为用户上传参考图。选择内置风格时,前端提交 `assetStyleId`、`assetStyleLabel` 与对应 `assetStylePrompt`。选择 `自定义` 时必须弹出独立面板,用户填写描述后才允许应用;自定义描述作为 `assetStylePrompt` 进入后端生成链路。
|
||||
|
||||
题材阶段允许用户上传参考图片。
|
||||
## 6.3 参考图片
|
||||
|
||||
首版只支持图片参考,不支持视频参考。参考图片用于影响题材表现,不作为运行时规则裁决依据。
|
||||
抓大鹅入口页不展示参考图片上传。题材表现先由题材文本和草稿切割图片链路承接;后续需要 3D 模型时,在结果页 `3D素材` Tab 以切割图片作为图生模型参考图手动触发。
|
||||
|
||||
---
|
||||
|
||||
@@ -210,9 +222,9 @@ Agent 的职责是帮助用户确认可以直接编译 demo 的最小配置:
|
||||
|
||||
## 7.3 素材生成边界
|
||||
|
||||
首版结果页暂时不生成额外素材。
|
||||
抓大鹅草稿生成链路会生成首批 `3` 个题材物品素材:文本模型生成物品名,VectorEngine 生成 `2*2` 素材图并切割独立图片。入口页选择的 `assetStylePrompt` 必须写入素材图提示词;结果页手动 Rodin 图生模型时,继续以该物品图片和默认提示词作为起点。
|
||||
|
||||
后续如果需要生成题材物品、伪 3D 物品、场景背景或封面图,需要先补充本文档或新增技术方案,再进入编码。
|
||||
生成出的独立图片先作为草稿页 `3D素材` Tab 的预览资产返回,状态为 `image_ready`,模型文件为空。正式平台资产绑定、Rodin 生成模型转存和二次编辑流程以后续技术方案为准。
|
||||
|
||||
## 7.4 发布前试玩
|
||||
|
||||
@@ -415,15 +427,14 @@ itemTypeCount = clearCount <= 25 ? clearCount : 25
|
||||
|
||||
前端负责所有游戏过程中需要即时呈现的反馈效果:
|
||||
|
||||
1. 展示 Agent 创作界面。
|
||||
1. 展示表单式创作界面。
|
||||
2. 展示结果页和基础编辑表单。
|
||||
3. 上传参考图片。
|
||||
4. 展示运行态场景、物品、倒计时和备选栏。
|
||||
5. 基于最新后端快照执行 2D 命中检测、悬停、按压和选中反馈。
|
||||
6. 发送玩家点击意图。
|
||||
7. 在等待后端确认期间,先行播放飞入、入槽、三消、腾格、胜利和失败过渡效果。
|
||||
8. 收到后端确认后,把本地表现校正到权威快照。
|
||||
9. 展示结算界面。
|
||||
3. 展示运行态场景、物品、倒计时和备选栏。
|
||||
4. 基于最新后端快照执行 2D 命中检测、悬停、按压和选中反馈。
|
||||
5. 发送玩家点击意图。
|
||||
6. 在等待后端确认期间,先行播放飞入、入槽、三消、腾格、胜利和失败过渡效果。
|
||||
7. 收到后端确认后,把本地表现校正到权威快照。
|
||||
8. 展示结算界面。
|
||||
|
||||
前端可以做即时表现预判,但不得把预判结果作为最终规则真相或成绩来源。
|
||||
|
||||
@@ -444,7 +455,7 @@ itemTypeCount = clearCount <= 25 ? clearCount : 25
|
||||
```ts
|
||||
interface Match3DCreatorConfig {
|
||||
themeText: string;
|
||||
referenceImageSrc?: string;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
}
|
||||
@@ -453,9 +464,9 @@ interface Match3DCreatorConfig {
|
||||
字段说明:
|
||||
|
||||
1. `themeText`:题材主题。
|
||||
2. `referenceImageSrc`:可选参考图片。
|
||||
3. `clearCount`:需要消除次数,必须为正整数。
|
||||
4. `difficulty`:难度,范围为 `1~10`。
|
||||
2. `referenceImageSrc`:历史兼容字段,入口页固定提交为 `null`。
|
||||
3. `clearCount`:由难度选项派生的需要消除次数,必须为正整数。
|
||||
4. `difficulty`:由难度选项派生的难度数值,范围为 `1~10`。
|
||||
|
||||
## 11.2 作品结构
|
||||
|
||||
@@ -656,15 +667,17 @@ GET /api/runtime/match3d/runs/:runId
|
||||
|
||||
创作入口只展示必要信息,不默认堆叠玩法规则说明。
|
||||
|
||||
## 14.2 Agent 工作区
|
||||
## 14.2 入口表单
|
||||
|
||||
Agent 每轮优先追问最影响 demo 生成的一个问题。
|
||||
入口表单只展示三个输入块:
|
||||
|
||||
已确认的信息应清晰展示给用户确认:
|
||||
1. `想做一个什么题材的抓大鹅?` 大文本输入框。
|
||||
2. `3D素材风格` 横向滑动风格卡,最后一个为 `自定义`。
|
||||
3. `难度` 选项按钮。
|
||||
|
||||
1. 题材主题。
|
||||
2. 需要消除次数。
|
||||
3. 难度。
|
||||
入口页不展示参考图、`需要消除次数` 数值输入、`难度数值` 滑杆,也不展示 `题材 / 物品 / 难度` 三个摘要框。`需要消除次数` 和 `difficulty` 由难度选项派生后提交给后端。
|
||||
|
||||
抓大鹅入口页必须适配移动端创作页一屏内展示,页面自身不产生纵向滚动。题材输入框、风格横滑区、难度按钮和生成按钮根据可视高度自适应压缩,风格区只允许横向滑动。
|
||||
|
||||
## 14.3 结果页
|
||||
|
||||
@@ -689,22 +702,24 @@ Agent 每轮优先追问最影响 demo 生成的一个问题。
|
||||
首版 PRD 对应 demo 验收标准:
|
||||
|
||||
1. 用户可从平台创作入口进入“抓大鹅”模板。
|
||||
2. Agent 能确认题材、需要消除次数和难度。
|
||||
3. 用户可上传参考图片。
|
||||
4. 系统可生成待发布结果页。
|
||||
5. 用户可编辑游戏名称、标签、封面图等基础信息。
|
||||
6. 用户可发布前试玩,且试玩失败不阻断发布。
|
||||
7. 运行态能展示圆形空间、倒计时、物品和 `7` 格备选栏。
|
||||
8. 物品可重叠、遮挡、堆叠。
|
||||
9. 被完全遮挡物品不可点击,露出可点击区域的物品可点击。
|
||||
10. 点击通过后物品飞入备选栏。
|
||||
11. 备选栏中 `3` 个相同物品 id 自动消除。
|
||||
12. 清空空间中全部物品后胜利。
|
||||
13. 倒计时结束或备选栏满后失败。
|
||||
14. 胜利结算展示使用时间。
|
||||
15. 失败结算展示完成进度和重新开始按钮。
|
||||
16. 局内即时反馈由前端先行呈现,关键状态以后端确认快照校正。
|
||||
17. 相关中文文档通过编码检查。
|
||||
2. 入口表单能确认题材主题、3D 素材风格和难度选项,并提交派生后的消除次数与难度数值。
|
||||
3. 入口页不展示参考图上传。
|
||||
4. 内置风格显示画风参考图,自定义风格通过独立面板填写并进入提交 payload。
|
||||
5. 移动端入口页所有内容一屏展示,不产生纵向滚动。
|
||||
6. 系统可生成待发布结果页,并在草稿中返回首批切割图片素材预览。
|
||||
7. 用户可编辑游戏名称、标签、封面图等基础信息。
|
||||
8. 用户可发布前试玩,且试玩失败不阻断发布。
|
||||
9. 运行态能展示圆形空间、倒计时、物品和 `7` 格备选栏。
|
||||
10. 物品可重叠、遮挡、堆叠。
|
||||
11. 被完全遮挡物品不可点击,露出可点击区域的物品可点击。
|
||||
12. 点击通过后物品飞入备选栏。
|
||||
13. 备选栏中 `3` 个相同物品 id 自动消除。
|
||||
14. 清空空间中全部物品后胜利。
|
||||
15. 倒计时结束或备选栏满后失败。
|
||||
16. 胜利结算展示使用时间。
|
||||
17. 失败结算展示完成进度和重新开始按钮。
|
||||
18. 局内即时反馈由前端先行呈现,关键状态以后端确认快照校正。
|
||||
19. 相关中文文档通过编码检查。
|
||||
|
||||
---
|
||||
|
||||
@@ -719,7 +734,7 @@ Agent 每轮优先追问最影响 demo 生成的一个问题。
|
||||
## 阶段 B:创作与结果页
|
||||
|
||||
1. 新增平台创作入口。
|
||||
2. 接入 Agent 会话。
|
||||
2. 接入创作会话。
|
||||
3. 编译草稿并进入结果页。
|
||||
4. 复用基础信息编辑和发布链。
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user