This commit is contained in:
2026-05-13 03:10:55 +08:00
154 changed files with 16812 additions and 708 deletions

36
docs/agents/domain.md Normal file
View File

@@ -0,0 +1,36 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Layout
This repo uses a **single-context** layout for Matt Pocock engineering skills:
- `CONTEXT.md` at the repo root, when present, is the primary domain glossary/context file.
- `docs/adr/`, when present, contains architecture decision records.
- If either path does not exist, proceed silently; do not block the task just to create it.
## Before exploring, read these
1. Root `CONTEXT.md`, if present.
2. Relevant ADRs under `docs/adr/`, if present.
3. Existing project context that predates this setup:
- `.hermes/README.md`
- `.hermes/shared-memory/project-overview.md`
- `.hermes/shared-memory/team-conventions.md`
- `.hermes/shared-memory/development-workflow.md`
- `.hermes/shared-memory/decision-log.md`
- `.hermes/shared-memory/pitfalls.md`
- Relevant files under `docs/technical/`, `docs/prd/`, `docs/design/`, and `docs/experience/`
Follow `AGENTS.md` when it is more specific than this file. If older docs conflict with current code or newer technical docs, treat current code and newer docs as authoritative and update stale docs when the task requires it.
## Use the glossary's vocabulary
When your output names a domain concept in an issue title, refactor proposal, diagnosis, test name, or implementation plan, use the term as defined in `CONTEXT.md` when available. Do not drift to synonyms the glossary explicitly avoids.
If the concept you need is not in the glossary yet, either use the established vocabulary from `.hermes/shared-memory/` and `docs/`, or note the gap for a future documentation pass.
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding it.

View File

@@ -0,0 +1,35 @@
# Issue tracker: Gitea
Issues and PRDs for this repo live as issues in the self-hosted Gitea remote:
- Remote: `http://82.157.175.59:3000/GenarrativeAI/Genarrative.git`
- Tracker type: Gitea Issues
## Conventions
- Prefer the Gitea `tea` CLI when it is installed and configured for this host.
- Do not use GitHub `gh` or GitLab `glab` for this repo unless the repository is explicitly migrated to those platforms.
- If `tea` is unavailable, use the Gitea Web UI or Gitea REST API for the same operations.
## Common operations with `tea`
Exact flags can vary by `tea` version. Run `tea issues --help` or `tea issue --help` before using a command in a new environment.
- Create an issue: `tea issues create --title "..." --body "..."`
- Read an issue: `tea issues view <number>`
- List issues: `tea issues list`
- Comment on an issue: use the installed `tea` issue comment command shown by `tea issues --help`; if unavailable, use the Gitea Web UI or REST API.
- Apply labels: use the installed `tea` issue update/edit command shown by `tea issues --help`; if unavailable, use the Gitea Web UI or REST API.
- Close an issue: use the installed `tea` issue close/update command shown by `tea issues --help`; if unavailable, use the Gitea Web UI or REST API.
## When a skill says "publish to the issue tracker"
Create a Gitea issue in `GenarrativeAI/Genarrative` with the requested title, body, labels, and links back to any relevant docs or branch.
## When a skill says "fetch the relevant ticket"
Read the Gitea issue body and comments/notes for the referenced issue number. Include labels and current open/closed state in the working context.
## Authentication
Use the locally configured Gitea credentials for the current developer. Do not commit tokens, cookies, `.env`, or local credential files.

View File

@@ -0,0 +1,15 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's Gitea issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role, use the corresponding Gitea label string from this table.
If the Gitea repository later adopts Chinese labels or a different naming scheme, edit the right-hand column here rather than letting skills create duplicate labels.

View 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-1Vite 依赖存在高危 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 dependencyseverity 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-1Rust 依赖漏洞未完成扫描
**原因:**
本机没有 `cargo-audit`,本次没有擅自安装用户级 Cargo 工具。
**建议:**
如确认允许安装:
```bash
cargo install cargo-audit --locked
cargo audit --manifest-path server-rs/Cargo.toml
```
或在 CI/具备工具的环境执行并回填结果。
## Informational / 源码热点
### I-1JS/TS XSS / 动态执行热点
扫描命中 1 行:
- `src/routing/RouteImageReadyGate.test.ts` 中测试代码使用 `root.innerHTML`。
初步判断为测试环境构造 DOM不是生产漏洞。若后续发现生产代码使用 `dangerouslySetInnerHTML` 或直接 `innerHTML = userInput`,应升级为 High。
### I-2Node 脚本命令执行热点
扫描命中 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-3Rust 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。

View File

@@ -108,3 +108,42 @@ output/imagegen/baimeng-expo-rollup/baimeng-rollup-final-cn-preview.png
2. 若需要放二维码,应放在底部独立留白区,不遮挡产品心智和关键技术段。
3. 若展会现场观众偏投资人或B端合作方可以把“产品心智”段压缩放大“关键技术”与平台愿景。
4. 若观众偏玩家或普通创作者可以把“关键技术”段压缩放大“10分钟创作、玩过就改、发布分享”的闭环。
## 6. 公司招聘版 2026-05-11
2026-05-11 根据线下招聘场景,将海报方向从“纯产品宣传”调整为“公司 + 产品 + 岗位”的整体宣传。
新版定位:
```text
北京亓盒网络科技有限公司
岗位名称AI 原生游戏产品/内容实习生
行业方向AI 原生游戏 × UGC 内容创作 × 互动叙事
产品:百梦 AI互动内容创作平台
```
新版保留百梦气泡色彩、轻盈白底和创作流动感但新增校园实验室、AI 游戏创作、作品卡、产品测试与内容设计氛围。版面结构调整为:
1. 顶部:公司名、岗位名、行业方向与招聘主标题。
2. 中上:百梦产品主张与三枚产品能力标签。
3. 中部:按 `游玩 -> 改造 -> 创作` 顺序展示产品体验闭环。
4. 中下:介绍“我们正在做的事「百梦」”。
5. 下部:实习生参与内容、加分项、团队背景和联系方式。
6. 底部:预留两个方形二维码占位,收尾文案为 `百梦 | 让每个人都能做自己的游戏`
新版使用当前仓库 `VectorEngine gpt-image-2-all` 路径生成底图:
```text
model: gpt-image-2-all
size: 1536x3840
reference image 1: 用户提供的上一版海报截图
reference image 2: 百梦气泡共创logo方向图
output: output/imagegen/baimeng-recruitment-rollup/baimeng-recruitment-rollup-background-gpt-image-2-all.png
```
最终输出:
```text
output/imagegen/baimeng-recruitment-rollup/baimeng-recruitment-rollup-final-cn.png
output/imagegen/baimeng-recruitment-rollup/baimeng-recruitment-rollup-final-cn-preview.png
```

View File

@@ -48,7 +48,7 @@
1. 发现页隐藏“寓教于乐”标签;
2. 隐藏“寓教于乐”标签下内容;
3. 该内容线内容不进入推荐、今日、分类、排行和搜索结果;
4. 该内容线内容完全不可见,公开作品搜索、作品号搜索直达、公开详情深链、浏览历史入口等平台公开入口都不能打开该内容。
4. 该内容线内容完全不可见,公开作品搜索、作品号搜索直达、公开详情深链、浏览历史入口、创作入口和创作页作品架等平台入口都不能打开或展示该内容。
## 4. 内容识别规则
@@ -114,4 +114,23 @@ no
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 直达路由拦截和精确标签识别。
7. `宝贝识物` 创作入口和创作页作品架也复用同一开关;开关关闭时不展示模板入口,也不展示本地宝贝识物草稿或已发布卡片。
8. 定向回归覆盖在 `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/components/platform-entry/platformEntryCreationTypes.test.ts``src/routing/appRoutes.test.ts`包含频道顺序、开关关闭、普通列表过滤、搜索过滤、作品号直达拦截、Demo 直达路由拦截、创作入口隐藏和精确标签识别。
## 9. 第 4 项作品架 / 广场接入边界
`宝贝识物` 首关的公开作品展示接入按以下口径收口:
1. 平台公共作品模型新增 `sourceType = edutainment`,当前只承接 `templateId = baby-object-match``templateName = 宝贝识物`
2. `宝贝识物` 作品仍必须携带精确等于“寓教于乐”的公开标签,才会进入“发现 / 寓教于乐”频道。
3. `宝贝识物` 不因为模板名自动归入寓教于乐,也不因为近似标签归入寓教于乐。
4. 第 4 项只负责公开作品卡片、发现页专属频道、公开详情、分享作品号和开关隐藏保护。
5. 创作模板、image-2 资产生成、发布接口、运行时开始游戏和关卡状态由对应线程接入;当前公共作品卡直接透传后续数据源提供的 `publicWorkCode`,不在前端新增最终作品号前缀规则。
6. 在创作和运行时链路真正接入前,公开详情内的启动、改造、编辑和点赞只做保护性占位,不新增玩法规则。
当前工程落点:
1. `src/components/rpg-entry/rpgEntryWorldPresentation.ts` 定义 `PlatformEdutainmentGalleryCard``isEdutainmentGalleryEntry`
2. `src/components/rpg-entry/RpgEntryHomeView.tsx``宝贝识物` 卡片识别为寓教于乐公开作品,并继续从推荐、今日、分类、排行和搜索结果中过滤。
3. `src/components/platform-entry/PlatformWorkDetailView.tsx` 在公开详情中显示 `宝贝识物` 类型标签,并继续复用作品号复制和分享链路。
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` 已识别 `edutainment` 公共作品,避免落入 RPG 默认详情、推荐运行态或错误的改造链路。

View File

@@ -0,0 +1,99 @@
# 宝贝识物寓教于乐模板 PRD 2026-05-11
## 1. 目标
新增寓教于乐内容线的创作模板:
```text
宝贝识物
```
创作者必须通过该模板创作并发布作品后,用户才能在寓教于乐板块体验对应关卡。
本模板只服务儿童动作 Demo 内容线,不把普通教育题材作品自动归入寓教于乐。
## 2. 创作输入
创作者必须填写两个物品名称:
1. 物品 A 名称;
2. 物品 B 名称。
两个名称都必须去除首尾空白后非空。当前阶段不新增题材、难度、计时、失败次数、分数、体力或递增规则。
## 3. 生成规则
提交后生成一份宝贝识物草稿,草稿包含:
1. 模板 ID`baby-object-match`
2. 模板名称:`宝贝识物`
3. 两个物品;
4. 两个物品图;
5. 作品标签。
物品图使用 VectorEngine `gpt-image-2-all` / image-2 生成。图片生成只能走后端或后续后端预留接口,前端不得泄露 `VECTOR_ENGINE_API_KEY`
本地 Demo 阶段若真实生图接口未接入完成,允许前端 service 返回明确标记为 `placeholder` 的占位图形,用于打通创作到结果页的交互链路;该占位结果不得伪装成正式 image-2 资产。
## 4. 标签规则
发布作品必须携带精确标签:
```text
寓教于乐
```
标签识别只接受精确等于 `寓教于乐`。不接受 `儿童教育``动作教育``寓教于乐 ` 等近似标签。
宝贝识物草稿与发布 payload 中都必须保留该标签。发布后的公开展示、搜索、深链和入口开关继续遵循 `CHILD_MOTION_EDUTAINMENT_DISCOVER_ENTRY_2026-05-09.md`
## 5. 结果页能力
结果页展示:
1. 作品名称;
2. 两个物品名称;
3. 两个物品图;
4. 标签;
5. 保存草稿;
6. 发布;
7. 试玩。
结果页不展示长规则说明文案。试玩按钮直接进入宝贝识物首关本地运行态。
试玩按钮进入宝贝识物首关运行态,运行态消费当前草稿中的两个物品名称和两张物品图,不重新生成或改写物品内容。
## 6. 发布后体验
发布完成后作品应进入寓教于乐内容线,并在寓教于乐入口开启时可被板块消费。
入口关闭时,发布作品完全不可见,不能通过推荐、发现普通频道、搜索、作品号、公开详情深链或浏览历史访问。
## 7. 与运行时线程的边界
本 PRD 同步约束首关运行态,已确认规则包括:
1. 礼物盒打开在本地调试绑定 `F` 键;
2. 每轮仅中间礼物盒跳出的物品随机;左右两侧篮子固定为当前草稿两个物品的顺序;
3. 下一关按钮当前占位;
4. 不新增用户未确认的计时、失败次数、分数、体力或难度递增。
5. 屏幕中上方字幕固定为“将物品放入对应的篮子里”。
6. 礼物盒位于屏幕中下方,任意手抬起后打开并跳出下一个随机物品。
7. 屏幕下方左侧和右侧分别展示两个固定篮子,左侧固定使用草稿第一个物品图,右侧固定使用草稿第二个物品图。
8. 明确左手连续横向移动达到阈值时将当前物品送入左侧篮子,明确右手连续横向移动达到阈值时将当前物品送入右侧篮子;选篮不使用动作名判定,侧别未知的手部轨迹不参与选篮。
9. 正确时展示“真棒”字幕和正确特效;错误时展示“再想一想吧”字幕和错误特效,物品回到中央。
10. 成功 20 次后展示“恭喜你!小朋友!”字幕和特效,并展示“再来一次”和“下一关”按钮。
11. 当前本地 Demo 阶段音效与语音播报接口只预留调用点,不在前端写死外部硬件或服务接口。
## 8. 验收
1. 创作入口显示 `宝贝识物` 并可进入模板表单。
2. 未填写任一物品名称时不能生成草稿。
3. 生成草稿后进入结果页,展示两个物品名称和物品图。
4. 草稿标签中始终包含精确 `寓教于乐`
5. 发布 payload 始终包含精确 `寓教于乐`
6. 发布完成后出现分享弹窗或发布完成状态。
7. 前端不读取或暴露 VectorEngine 密钥。
8. 结果页试玩进入宝贝识物运行态,不再显示“试玩关卡正在接入中”。
9. 运行态可通过 `F` 打开礼物盒,通过鼠标左键拖动映射左手横向移动,通过鼠标右键拖动映射右手横向移动。
10. 成功 20 次后出现“再来一次”和“下一关”按钮。

View File

@@ -0,0 +1,412 @@
# 汪汪声浪大作战 / bark-battle BDD 验收场景
## 背景
- 需求来源:用户提供的视频 `C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`,并已在 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md` 中完成抽帧分析和玩法方案整理。
- 玩法定位:浏览器 2D 声控狗叫对战小游戏,暂定中文名 `汪汪声浪大作战`,英文代号与 play type ID 建议为 `bark-battle`
- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效叫声次数和叫声节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。
- 文档目的:为产品、测试、前端、后端在编码前统一可验证验收口径;本文只定义 PRD/BDD 级行为与测试映射,不实现工程代码。
## 角色与目标
### 主要角色
- 浏览器玩家:进入 `bark-battle` 玩法,用麦克风发声参与对战。
- 移动端玩家:在手机浏览器中进入玩法,需要看到核心游戏信息并完成授权、对战、结算。
- 无麦克风或无 API 支持玩家:设备或浏览器不支持声控输入时,需要得到明确降级反馈并可返回。
- 测试人员:依据本文场景验证权限、校准、叫声识别、能量条、胜负结算和布局兼容。
- 前端实现人员:依据本文拆分 Web Audio 输入、领域规则、Phaser/DOM HUD 表现和自动化测试。
### 用户目标
- 玩家可以在开局前完成麦克风授权和环境噪音校准。
- 玩家发出有效狗叫时,能看到叫声计数、狗狗动画、拟声词/冲击波以及能量条变化。
- 低于阈值的背景噪音不会被误计为有效叫声。
- 单局在 30 秒后给出明确胜负、平局和关键数据。
- 移动端和不支持麦克风的环境不会进入不可操作状态。
### 非目标
- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值间隔、持续时间和校准结果为准。
- MVP 不要求实时联机对战;可先按“玩家 vs AI 对手”完成单机浏览器 runtime。
- MVP 不要求成绩持久化、作品发布、作品架、广场和排行榜;若后续接入 Genarrative 作品闭环,需要另补玩法类型集成 PRD/技术文档。
- MVP 不要求在 UI 中长期展示大段规则说明;游戏界面应保持倒计时、能量条、狗狗、麦克风状态和结算信息为主。
- MVP 不处理竞技级反作弊;播放录音、拍桌、喊叫等非狗叫输入只作为后续公平性风险记录。
## 业务规则口径
- 单局时长:默认 30 秒,从正式进入 `playing` 阶段开始计时。
- 能量条:使用 `-100``100` 的连续值表示,负数偏对手侧,正数偏玩家侧,`0` 为中线。
- 平局阈值:倒计时结束时,若能量条绝对值小于或等于 `drawThreshold`,判定平局;具体数值由实现配置,但测试需可注入固定阈值。
- 有效叫声:一次有效叫声至少满足:音量超过校准后的有效阈值、与上一次有效峰值间隔不小于 `minBarkGapMs`、持续时长在 `minBarkDurationMs``maxBarkDurationMs` 之间。
- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加叫声次数,也不得让能量条出现可见推进。
- 推动力:玩家推动力由音量分数、有效叫声频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100``100`
- UI 反馈:有效叫声应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。
## 中文 Gherkin 场景
### 功能: 麦克风授权与开局准备
```gherkin
功能: 狗叫对战麦克风准备
背景:
假如 bark-battle
而且 navigator.mediaDevices.getUserMedia
场景: 玩家允许麦克风权限后进入环境噪音校准
那么
而且 playing
而且
场景: 校准完成后进入开局倒计时
假如
而且
那么
而且 30
而且线
场景: 玩家拒绝麦克风权限后不能开始声控对战
那么
而且
而且 playing
```
### 功能: 环境噪音校准
```gherkin
功能: 环境噪音校准
场景: 安静环境生成低但非零的有效阈值
假如 RMS 线
那么
而且
场景: 嘈杂环境生成更高的有效阈值
假如 RMS 线
那么
而且
场景: 校准期间无法获得有效音频样本
假如
那么
而且
而且
```
### 功能: 有效叫声计数
```gherkin
功能: 有效叫声计数
背景:
假如 30 playing
而且
场景: 单次超过阈值且间隔足够的叫声计数加一
假如 0
而且 minBarkGapMs
那么 1
而且
而且
场景: 持续噪音不会被无限计数
假如 1
那么 tick
而且
场景: 间隔过短的连续峰值不重复计数
假如
minBarkGapMs
那么
而且
```
### 功能: 声音大小和连续叫声推动能量条
```gherkin
功能: 声浪推动能量条
背景:
假如 30 playing
而且线
场景: 玩家推动力高于对手时能量条向玩家侧移动
假如
而且
simulation tick
那么
而且
场景: 连续大声叫声触发更强反馈
假如
那么
而且
但是
场景: 能量条到达边界后不会越界
假如
而且
simulation tick
那么 100
而且
场景: 对手推动力高于玩家时能量条向对手侧移动
假如
simulation tick
那么
而且
```
### 功能: 低噪音和无效输入过滤
```gherkin
功能: 背景噪音过滤
背景:
假如 30 playing
而且
场景: 低于阈值的背景噪音不计数
那么
而且
而且
场景: 过短脉冲不计为有效叫声
假如
但是 minBarkDurationMs
那么
而且
场景: 过长持续声被削弱为单段输入
假如
但是 maxBarkDurationMs
那么
而且
```
### 功能: 倒计时与胜负结算
```gherkin
功能: 狗叫对战胜负结算
30
背景:
假如 30 playing
场景: 倒计时每秒递减并在归零时停止对战输入
30 0
那么
而且 finished
而且
场景: 玩家侧占优时判定玩家胜利
假如 drawThreshold
那么
而且
而且
场景: 对手侧占优时判定玩家失败
假如 -drawThreshold
那么
而且
而且
场景: 能量条接近平衡时判定平局
假如 -drawThreshold drawThreshold
那么
而且
而且
```
### 功能: 再来一局与状态重置
```gherkin
功能: 对战重开
场景: 结算后点击再来一局重置本局状态
假如 finished
而且
那么 30
而且线
而且
而且
场景: 结算后返回玩法入口
假如 finished
那么
而且
```
### 功能: 移动端布局
```gherkin
功能: 移动端 bark-battle 布局
场景: 移动端进入对战页面时核心元素可见
假如使 430px
bark-battle
那么
而且
而且
而且
场景: 移动端授权和开始必须由用户手势触发
假如使
那么
而且 AudioContext
场景: 移动端结算面板不遮挡主要操作
假如
那么
而且
```
### 功能: 无 getUserMedia 或不可用环境降级
```gherkin
功能: 无麦克风 API 降级
API
退
场景: 当前浏览器不支持 getUserMedia
假如 navigator.mediaDevices.getUserMedia
bark-battle
那么
而且
而且
场景: getUserMedia 调用失败但浏览器 API 存在
假如 getUserMedia
但是 NotFoundError NotReadableError
那么
而且
而且 playing
场景: 非安全上下文导致麦克风不可用
假如
bark-battle
那么使
而且使
而且
```
### 功能: 刷新与退出时释放麦克风资源
```gherkin
功能: 麦克风资源释放
场景: 对战中离开页面停止采集
假如 playing
bark-battle
那么 MediaStream
而且 simulation tick
场景: 刷新页面后不沿用旧局临时状态
假如 playing
bark-battle
那么
而且沿
```
## 测试映射
| 场景 | 测试层级 | 建议目标文件 | 自动化状态 |
| --- | --- | --- | --- |
| 玩家允许麦克风权限后进入环境噪音校准 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
| 校准完成后进入开局倒计时 | application/unit | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/domain/BarkBattleSession.test.ts` | planned |
| 玩家拒绝麦克风权限后不能开始声控对战 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
| 安静环境生成低但非零的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned |
| 嘈杂环境生成更高的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned |
| 校准期间无法获得有效音频样本 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
| 单次超过阈值且间隔足够的叫声计数加一 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 持续噪音不会被无限计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 间隔过短的连续峰值不重复计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 玩家推动力高于对手时能量条向玩家侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
| 连续大声叫声触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned |
| 能量条到达边界后不会越界 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
| 对手推动力高于玩家时能量条向对手侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
| 低于阈值的背景噪音不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 过短脉冲不计为有效叫声 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 过长持续声被削弱为单段输入 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
| 倒计时每秒递减并在归零时停止对战输入 | unit/application | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/application/BarkBattleController.test.ts` | planned |
| 玩家侧占优时判定玩家胜利 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
| 对手侧占优时判定玩家失败 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
| 能量条接近平衡时判定平局 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
| 结算后点击再来一局重置本局状态 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
| 结算后返回玩法入口 | integration/smoke | `src/games/bark-battle/application/BarkBattleController.test.ts`, Playwright 或人工 smoke 清单 | planned |
| 移动端进入对战页面时核心元素可见 | component/visual/smoke | `src/games/bark-battle/ui/BarkBattleHud.test.tsx`, Playwright 移动端视口 smoke | planned |
| 移动端授权和开始必须由用户手势触发 | infrastructure/application/e2e-smoke | `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts`, Playwright 移动端 smoke | planned |
| 移动端结算面板不遮挡主要操作 | component/visual/smoke | `src/games/bark-battle/ui/__tests__/BarkBattleResultPanel.test.tsx`, Playwright 移动端视口 smoke | planned |
| 当前浏览器不支持 getUserMedia | infrastructure/component | `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/ui/__tests__/BarkBattlePermissionPanel.test.tsx` | planned |
| getUserMedia 调用失败但浏览器 API 存在 | infrastructure/application/component | `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts`, `src/games/bark-battle/ui/__tests__/BarkBattlePermissionPanel.test.tsx` | planned |
| 非安全上下文导致麦克风不可用 | infrastructure/application/component | `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts`, `src/games/bark-battle/ui/__tests__/BarkBattlePermissionPanel.test.tsx` | planned |
| 对战中离开页面停止采集 | infrastructure/application/integration | `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts` | planned |
| 刷新页面后不沿用旧局临时状态 | integration/smoke | Playwright 或人工 smoke 清单 | planned |
## 验收清单
- [ ] 权限允许、拒绝、非安全上下文、API 不支持、麦克风未找到/不可读、AudioContext 被拦截、校准超时或样本不可读均有明确状态,且不会误进入 playing。
- [ ] 校准阶段会影响有效叫声阈值,低噪音不会增加叫声计数。
- [ ] 有效叫声计数具备阈值、峰值间隔、持续时长约束。
- [ ] 能量条根据双方推动力差值双向移动,并限制在 `-100``100`
- [ ] 30 秒归零后停止本局输入影响,并按玩家胜利、对手胜利、平局三类结果结算。
- [ ] 移动端核心元素可见,非关键设置收起,不在主画面堆叠长规则说明。
- [ ] 离开页面或返回入口时停止麦克风采集。
- [ ] 每个编码依据场景已在测试映射中标注测试层级和建议文件。
## 开放问题
1. MVP 是否确认只做“玩家 vs AI”还是第一版需要双人同屏或联机对战
2. `drawThreshold``minBarkGapMs``minBarkDurationMs``maxBarkDurationMs` 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值?
3. 是否允许无麦克风设备提供键盘/点击备用输入?若允许,需要另补非声控模式场景;若不允许,当前降级只提供返回入口。
4. 是否需要在结算中记录或上报成绩、最高音量、叫声次数和声浪评分?若需要,需补埋点/后端持久化场景。
5. bark-battle 是否作为 Genarrative 正式 play type 接入创作入口、作品发布和广场,还是先作为独立 runtime 原型验证?
6. 狗狗、背景、拟声词和冲击波素材来源是临时占位、AI 生成,还是复用项目现有素材管线?

View File

@@ -4,6 +4,7 @@
## 重点入口
- [宝贝识物寓教于乐模板 PRD](./BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md):定义寓教于乐内容线的 `宝贝识物` 创作模板覆盖两个物品名称输入、image-2 物品图生成、精确 `寓教于乐` 标签、结果页和发布边界。
- [AI 原生幕间文字游戏模板 PRD参考 MOKU 的剧本模拟器闭环](./AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md):参考 MOKU / 幕间类 AI 文游的剧本游乐场、自由行动、AI GM、记忆和模拟器强反馈经验但只落为百梦 `text-game` 模板,复用平台接口,不迁入外部社区、支付、私有存档或回放。
- [AI 原生视觉小说模板 PRDTXT 玩法平台化接入](./AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md):参考 `Interactive-fiction-backend` / `Interactive-fiction-frontend` 的 TXT 玩法经验,但只保留视觉小说模板创作与运行闭环,完全使用 Genarrative 平台接口,并明确删除回放和外部平台功能。
- [AI 原生幸存者类游戏模板 PRD](./AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md):定义 `survivor` 幸存者挑战模板,从 Agent 创作、结果页、资产、试玩、发布到后端权威配置与前端高频运行表现的完整闭环。

View File

@@ -0,0 +1,164 @@
# 宝贝识物创作发布实现方案 2026-05-11
## 1. 范围
本方案对应第 2 线程:创作发布线程。
本线程落地:
1. 创作入口配置;
2. 模板表单;
3. 本地草稿生成 service
4. 结果页;
5. 发布 payload 约束;
6. 本地 Demo 运行态;
7. 后端 image-2 / 作品持久化 / 运行态接口预留形状。
本阶段运行态先做浏览器本地 Demo并消费现有本地 mocap 动作数据源;正式硬件接口和摄像头调教在后续接口稳定后继续接入。
## 2. 前端接入点
新增玩法 ID
```text
baby-object-match
```
用户展示名:
```text
宝贝识物
```
入口文件:
1. `src/config/newWorkEntryConfig.ts`
2. `src/components/platform-entry/platformEntryCreationTypes.ts`
3. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
`baby-object-match` 必须复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时,创作类型弹层不展示 `宝贝识物`,创作页作品架不展示本地宝贝识物草稿或已发布作品卡,公开发现、搜索、详情、作品号和浏览历史也继续完全不可见。
新增阶段:
```text
baby-object-match-workspace
baby-object-match-generating
baby-object-match-result
baby-object-match-runtime
```
## 3. 契约
前端共享契约放在:
```text
packages/shared/src/contracts/edutainmentBabyObject.ts
```
核心字段:
1. `BabyObjectMatchDraft.templateId = "baby-object-match"`
2. `BabyObjectMatchDraft.templateName = "宝贝识物"`
3. `BabyObjectMatchDraft.themeTags` 必须包含精确 `寓教于乐`
4. `BabyObjectMatchItemAsset.generationProvider` 首版允许为 `vector-engine-gpt-image-2``placeholder`
5. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`
## 4. Service 边界
前端 service 放在:
```text
src/services/edutainment-baby-object/babyObjectMatchClient.ts
```
首版提供:
1. `createBabyObjectMatchDraft(payload)`
2. `saveBabyObjectMatchDraft(draft)`
3. `publishBabyObjectMatchWork(payload)`
当前后端正式接口未在本线程扩表落地,因此 service 先走本地 Demo 存储,并把 asset 结果标记为 `placeholder`。后续后端接入时,应替换为:
```text
POST /api/creation/edutainment/baby-object-match/drafts
PUT /api/creation/edutainment/baby-object-match/drafts/{draftId}
POST /api/creation/edutainment/baby-object-match/drafts/{draftId}/publish
```
图片生成必须在后端调用 VectorEngine `gpt-image-2-all`,不得从前端直接调用外部图片接口。
## 5. UI 边界
工作台只展示两个必填输入和生成按钮。
结果页只展示草稿核心信息、两个物品、保存草稿、发布、试玩。不在 UI 内写玩法说明长文案。
移动端优先:表单和结果页使用单列布局,桌面端自然扩展为双列。
## 6. 运行态边界
前端运行态放在:
```text
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
```
运行态直接消费 `BabyObjectMatchDraft`,必须使用草稿中的两个物品名称和物品图。
每轮只随机当前从礼物盒跳出的物品;左右篮子不随机交换,左侧固定为草稿 `itemAssets[0]`,右侧固定为草稿 `itemAssets[1]`
首关状态机:
1. `waiting`:礼物盒关闭,等待任意手抬起;
2. `active`:当前物品停留在屏幕中央;
3. `correct`:展示“真棒”反馈,成功次数加 1
4. `wrong`:展示“再想一想吧”反馈,当前物品回到中央;
5. `complete`:成功次数达到 20展示“恭喜你小朋友”和按钮。
动作输入:
1. 任意手完成一次 `open_palm -> grab` 抓握序列:打开礼物盒并生成当前物品;
2. 左手连续横向移动达到阈值:将当前物品送入左侧篮子;
3. 右手连续横向移动达到阈值:将当前物品送入右侧篮子。
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。选篮只使用明确 `leftHand``rightHand` 的连续横向轨迹阈值,不再通过 `wave_left_hand``wave_right_hand``wave` 等动作名触发;侧别为 `unknown` 的手部轨迹也不参与选篮,以避免多套判定误命中和连续误触发。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:`rightHand` 轨迹映射玩家左手并进入左侧篮子,`leftHand` 轨迹映射玩家右手并进入右侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
开发者调试输入:
1. `F`:映射任意手抬起,打开礼物盒并生成当前物品;
2. 鼠标左键按下并拖动:映射左手轨迹,抬起后将当前物品送入左侧篮子;
3. 鼠标右键按下并拖动:映射右手轨迹,抬起后将当前物品送入右侧篮子。
运行态不得新增计时、失败次数、分数、体力或难度递增规则。
音效和语音播报当前只保留接口预留边界,正式语音接口后续接入。
## 7. 发布约束
发布前必须执行:
1. 两个物品名非空;
2. 两个物品名对应的 asset 存在;
3. 标签补齐精确 `寓教于乐`
4. `publicationStatus``draft` 变为 `published`
发布后首版本地响应返回 `publicWorkCode`,用于分享弹窗;正式后端接入时 public code 生成规则需要纳入统一作品号服务。
## 8. 热身关衔接
`/child-motion-demo` 热身完成后的“开始游戏”按钮进入同一个 `BabyObjectMatchRuntimeShell`
热身关独立 Demo 没有创作者草稿上下文,因此使用固定本地 Demo 草稿承载两个物品,仅用于热身关后验证首关体验;正式平台体验仍必须从 `宝贝识物` 模板创作发布后进入寓教于乐板块。
## 9. 验收命令
```bash
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/services/edutainment-baby-object/babyObjectMatchClient.test.ts
npx vitest run src/components/platform-entry/platformEdutainmentVisibility.test.ts src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/custom-world-home/creationWorkShelf.test.ts src/services/useMocapInput.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts
npx eslint src/components/platform-entry/platformEntryCreationTypes.ts src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --ext .ts,.tsx --max-warnings 0
npm run check:encoding
npm run typecheck
npm run build:raw
```
若后续接入真实 Rust API 和 SpacetimeDB 表,再补充 `npm run api-server``/healthz`、Rust contract / api-server / spacetime-client 定向测试和 migration 表目录更新。

View File

@@ -0,0 +1,729 @@
# bark-battle 2D Runtime 前端技术方案2026-05-11
## 1. 背景与目标
本方案基于 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md`,为“汪汪声浪大作战 / bark-battle”细化前端与浏览器游戏 runtime 的技术实现路线。
本任务只产出技术方案,不直接实现代码。后续编码应以本文作为 runtime 层设计约束,再按 TDD 小步落地。
### 1.1 玩法定位
`bark-battle` 是一个声控拔河式 2D 浏览器小游戏:玩家对麦克风发出狗叫声,系统根据音量峰值、有效叫声次数和节奏计算本方声浪推动力,在限时 30 秒内推动顶部红蓝能量条,时间结束后按能量条偏向判定胜负。
### 1.2 本文范围
本文覆盖:
- Phaser + TypeScript + Vite 栈选择
- simulation / render / HUD 边界
- 建议目录结构
- 核心 domain 类型
- Web Audio 输入适配
- Phaser Scene 切分
- DOM HUD 设计
- 移动端与权限降级
- 测试与验证命令
本文不覆盖:
- 后端表结构、持久化成绩、作品发布、广场接入
- 实时多人对战协议
- 复杂 AI 狗叫识别模型
- 美术素材正式生产流程
## 2. 技术栈选择
### 2.1 推荐栈
```text
Runtime Renderer: Phaser 3
Language: TypeScript
Build: Vite
Host UI: React / DOM overlay
Audio Input: Web Audio API + MediaDevices.getUserMedia
Test: Vitest + Testing Library + 浏览器 smoke / Playwright 可选
```
### 2.2 选择理由
1. 玩法是横版 2D 舞台,核心表现是狗狗 sprite、声浪、拟声词、粒子、屏幕震动与能量条反馈Phaser 对 2D 渲染、时间循环、sprite animation、camera、粒子和 Scene 生命周期支持成熟。
2. 当前项目主前端已使用 TypeScript + Vite继续复用现有构建和测试体系避免引入独立构建链。
3. 文字密集、权限提示、结算、设置和移动端响应式布局适合 DOM HUDCanvas 保持负责 playfield 和动态特效。
4. Web Audio API 可以在浏览器端完成 MVP 所需音量采样、RMS/peak 计算、环境噪音校准和输入归一化,不需要首版接入后端音频处理。
### 2.3 不选择其它路线的原因
- 不使用 Three.js / 3D当前玩法画面是 2D 横版舞台,不需要 3D 相机、模型和材质管线。
- 不把 HUD 全部塞入 Phaser Canvas权限说明、重试、结算、移动端布局和可访问性更适合 DOM。
- 不在前端实现正式业务真相:浏览器 runtime 可承载单局即时 simulation但若后续涉及成绩、作品、排行榜、发布和奖励必须交给后端投影/API 裁决。
## 3. 总体架构
### 3.1 分层总览
```text
React Runtime Shell
├─ DOM HUD / Panels
│ ├─ PermissionPanel
│ ├─ TopEnergyBar
│ ├─ TimerChip
│ └─ ResultPanel
├─ Application Controller
│ ├─ permission / calibration orchestration
│ ├─ simulation tick
│ ├─ audio sample submission
│ └─ snapshot publish
├─ Pure Domain / Simulation
│ ├─ BarkBattleSession
│ ├─ BarkDetector
│ ├─ EnergyTugOfWar
│ ├─ BarkBattleScoring
│ └─ OpponentStrategy
├─ Infrastructure Adapters
│ ├─ BrowserMicrophoneInput
│ ├─ AudioAnalyserSampler
│ └─ PhaserGameHost
└─ Phaser Renderer
├─ BootScene
├─ PreloadScene
├─ BattleScene
├─ FxScene / DebugScene可选
└─ Asset manifest
```
### 3.2 强制边界
1. `domain/` 不依赖 Phaser、Web Audio、DOM、React、浏览器全局对象或后端 API。
2. `domain/` 只接收 plain data例如时间增量、归一化音量样本、对手 power、配置参数。
3. `application/` 负责编排权限、校准、音频输入、AI 对手、tick 和 snapshot 分发。
4. Phaser Scene 只消费 `BarkBattleSnapshot`,把 snapshot 映射成 sprite、动画、粒子、camera 和 sound effect不得持有核心胜负、计数和能量条规则。
5. DOM HUD 只消费 snapshot 和少量 runtime UI 状态,负责展示、按钮和弹层;不得重复实现核心胜负规则。
6. 若后续接入平台作品/成绩/排行榜,前端只调用后端 API 和展示投影,不在本地绕过后端生成正式结论。
## 4. 建议目录结构
首版建议以独立 runtime 原型落在 `src/games/bark-battle/`,避免提前侵入平台创作链路。
```text
src/games/bark-battle/
domain/
BarkBattleTypes.ts
BarkBattleSession.ts
BarkDetector.ts
EnergyTugOfWar.ts
BarkBattleScoring.ts
OpponentStrategy.ts
__tests__/
BarkDetector.test.ts
EnergyTugOfWar.test.ts
BarkBattleSession.test.ts
BarkBattleScoring.test.ts
application/
BarkBattleController.ts
BarkBattleConfig.ts
BarkBattleSnapshotStore.ts
__tests__/
BarkBattleController.test.ts
infrastructure/
BrowserMicrophoneInput.ts
AudioAnalyserSampler.ts
MicrophonePermission.ts
__tests__/
BrowserMicrophoneInput.test.ts
AudioAnalyserSampler.test.ts
phaser/
BarkBattleGameHost.ts
scenes/
BarkBattleBootScene.ts
BarkBattlePreloadScene.ts
BarkBattleScene.ts
BarkBattleFxScene.ts
assets/
barkBattleAssetManifest.ts
ui/
BarkBattleRuntimeShell.tsx
BarkBattleHud.tsx
BarkBattlePermissionPanel.tsx
BarkBattleResultPanel.tsx
BarkBattleMobileControls.tsx
BarkBattleHud.css
__tests__/
BarkBattleHud.test.tsx
BarkBattlePermissionPanel.test.tsx
BarkBattleResultPanel.test.tsx
```
若后续进入 Genarrative 正式玩法类型闭环,再按 `genarrative-play-type-integration` 扩展到:
```text
src/components/bark-battle-runtime/BarkBattleRuntimeShell.tsx
src/components/bark-battle-result/BarkBattleResultView.tsx
src/services/barkBattleRuntimeClient.ts
packages/shared/src/contracts/barkBattle.ts
server-rs/crates/shared-contracts/src/bark_battle.rs
```
首版不建议直接新增后端表或正式作品链路,除非产品明确要求成绩、发布和广场能力。
## 5. 核心 Domain 类型
### 5.1 基础枚举与数值约定
```ts
export type BarkBattlePhase =
| 'permission'
| 'calibration'
| 'countdown'
| 'playing'
| 'finished'
| 'unavailable'
export type BarkBattleSide = 'player' | 'opponent'
export type BarkBattleWinner = BarkBattleSide | 'draw' | null
export type BarkBattleDifficulty = 'easy' | 'normal' | 'hard'
export type BarkBattleUiState =
| 'idle'
| 'permission-ready'
| 'microphone-authorized'
| 'calibrating'
| 'ready-countdown'
| 'playing'
| 'finished'
| 'microphone-unavailable'
export type MicrophoneFailureReason =
| 'unsupported'
| 'permission-denied'
| 'non-secure-context'
| 'not-found'
| 'not-readable'
| 'audio-context-blocked'
| 'calibration-timeout'
| 'calibration-sample-unreadable'
| 'unknown'
```
关键数值:
- `energy`: `-100..100`,正数偏玩家侧,负数偏对手侧。
- `currentVolume`: `0..1`,音频采样归一化后的瞬时音量。
- `recentPeak`: `0..1`,短窗口内峰值。
- `power`: `0..1``0..100` 二选一,建议 domain 内统一 `0..1`HUD 显示再转百分比。
- `remainingMs`: 单局剩余毫秒。
### 5.2 Snapshot
```ts
export type BarkBattleSnapshot = {
phase: BarkBattlePhase
uiState: BarkBattleUiState
errorReason: MicrophoneFailureReason | null
statusMessageKey: BarkBattleStatusMessageKey | null
elapsedMs: number
remainingMs: number
countdownMs: number
energy: number
player: BarkSideState
opponent: BarkSideState
winner: BarkBattleWinner
result: BarkBattleResult | null
lastEvents: BarkBattleVisualEvent[]
}
export type BarkSideState = {
side: BarkBattleSide
barkCount: number
currentVolume: number
recentPeak: number
combo: number
power: number
isBarking: boolean
lastBarkAtMs: number | null
maxVolume: number
}
export type BarkBattleResult = {
winner: BarkBattleWinner
finalEnergy: number
playerBarkCount: number
playerMaxVolume: number
playerAveragePower: number
score: number
}
export type BarkBattleStatusMessageKey =
| 'microphone-unsupported'
| 'microphone-permission-denied'
| 'microphone-non-secure-context'
| 'microphone-not-found'
| 'microphone-not-readable'
| 'microphone-audio-context-blocked'
| 'microphone-calibration-timeout'
| 'microphone-calibration-sample-unreadable'
| 'microphone-unknown-error'
```
### 5.3 输入样本与叫声事件
```ts
export type BarkAudioSample = {
atMs: number
volume: number
peak: number
rms: number
}
export type BarkDetectedEvent = {
id: string
atMs: number
side: BarkBattleSide
volume: number
strength: number
combo: number
}
```
### 5.4 视觉事件
视觉事件由 domain 或 application 生成,但只包含 plain data不包含 Phaser 对象:
```ts
export type BarkBattleVisualEvent =
| {
type: 'bark-word'
id: string
side: BarkBattleSide
atMs: number
strength: number
text: 'BARK' | 'WOOF' | 'WAN' | 'WANGOOF'
}
| {
type: 'shockwave'
id: string
side: BarkBattleSide
atMs: number
strength: number
}
| {
type: 'combo-burst'
id: string
side: BarkBattleSide
atMs: number
combo: number
}
```
Phaser 只根据这些事件播放一次性特效,并维护已消费事件 ID避免重复播放。
## 6. Domain 模块职责
### 6.1 BarkDetector
职责:把连续音频样本转换为有效叫声事件。
输入:
- `BarkAudioSample`
- 校准后的 `ambientNoiseFloor`
- `barkThreshold`
- `minBarkGapMs`
- `minBarkDurationMs`
- `maxBarkDurationMs`
规则建议:
1. 音量超过动态阈值进入 candidate 状态。
2. 峰值回落到阈值以下或持续时长达到上限时结束 candidate。
3. candidate 持续时间在 `80ms..1200ms` 且与上一次有效叫声间隔足够时,记为一次叫声。
4. 长时间持续噪音不应无限计数,只能按冷却和峰值回落形成新事件。
5. MVP 不要求识别“是否真狗叫”,先基于音量峰值、时长和间隔判断。
### 6.2 EnergyTugOfWar
职责:更新红蓝拉锯条。
建议公式:
```text
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
opponentPower = opponentStrategy.tick(...)
energyDelta = (playerPower - opponentPower) * deltaSeconds * balanceFactor
energy = clamp(energy + energyDelta, -100, 100)
```
约束:
- `EnergyTugOfWar` 不知道玩家来自麦克风还是 mock input。
- `EnergyTugOfWar` 不知道 Phaser 能量条宽度。
- 平衡参数集中在 `BarkBattleConfig`,不要散落在 Scene 或 HUD 中。
### 6.3 BarkBattleSession
职责:管理局内 phase、计时、胜负和 snapshot。
状态机建议:
```text
permission → calibration → countdown → playing → finished
↘ unavailable
```
`phase` 只表达 runtime 是否可继续参与局内流程;所有麦克风不可用、权限失败、非安全上下文和校准失败都统一收敛到 `phase: 'unavailable'`,再通过 `uiState: 'microphone-unavailable'``errorReason` 区分 HUD 展示和重试策略,避免把基础设施错误枚举直接扩散成 domain 阶段。
关键规则:
- `countdown` 结束才进入 `playing`
- `playing``remainingMs` 随 tick 递减。
- `remainingMs <= 0` 后进入 `finished`
- `energy > drawThreshold` 判定玩家胜利。
- `energy < -drawThreshold` 判定对手胜利。
- `abs(energy) <= drawThreshold` 判定平局。
### 6.4 OpponentStrategy
职责:为单机 MVP 提供对手推动力。
```ts
export interface OpponentStrategy {
tick(input: OpponentTickInput): OpponentTickOutput
}
```
普通难度建议:
- 周期性小叫声提供基础压力。
- 每 3~6 秒一次短爆发。
- 玩家大幅领先时可轻微增强,但不能追到不可赢。
## 7. Web Audio 输入适配
### 7.1 BrowserMicrophoneInput
职责:封装浏览器麦克风权限与音频流生命周期。
建议 API
```ts
export interface MicrophoneInputPort {
isSupported(): boolean
requestPermission(): Promise<MicrophoneSession>
stop(): void
}
export interface MicrophoneSession {
sample(atMs: number): BarkAudioSample
stop(): void
}
```
实现要点:
1. 使用 `navigator.mediaDevices?.getUserMedia({ audio: true })`
2. 在用户点击“开始”后创建或 resume `AudioContext`,避免移动端自动播放策略拦截。
3. 使用 `AnalyserNode` 读取时域数据,计算 RMS 与 peak。
4. 输出归一化样本,不把 `MediaStream``AudioContext``AnalyserNode` 泄漏到 domain。
5. 退出、重开、页面卸载时停止 track避免麦克风占用残留。
### 7.2 校准流程
`calibration` 阶段建议持续 `800ms..1500ms`
1. 收集静默环境样本。
2. 计算 `ambientNoiseFloor`,例如 `p75` 或均值 + 标准差。
3. 设置动态阈值:
```text
barkThreshold = clamp(ambientNoiseFloor + 0.12, 0.18, 0.55)
```
4. 若环境噪音过高HUD 给出简短提示和“继续 / 重新校准”入口,但不要把长说明常驻在画面上。
### 7.3 权限与错误分类
```ts
export type MicrophoneFailureReason =
| 'unsupported'
| 'permission-denied'
| 'non-secure-context'
| 'not-found'
| 'not-readable'
| 'audio-context-blocked'
| 'calibration-timeout'
| 'calibration-sample-unreadable'
| 'unknown'
```
错误来源与分层归属:
| 失败原因 | 主要检测位置 | controller snapshot 表达 | HUD 可区分状态 |
| --- | --- | --- | --- |
| 浏览器无 `mediaDevices.getUserMedia` | `BrowserMicrophoneInput.isSupported()` | `phase: 'unavailable'`, `uiState: 'microphone-unavailable'`, `errorReason: 'unsupported'` | 设备或浏览器不支持麦克风输入,只提供返回入口,不展示可开始声控按钮 |
| 非安全上下文 | `BrowserMicrophoneInput.isSupported()``MicrophonePermission` 预检 `window.isSecureContext` | `phase: 'unavailable'`, `errorReason: 'non-secure-context'` | 当前环境无法使用麦克风,提示使用受支持的安全环境或返回 |
| 用户拒绝授权 | `BrowserMicrophoneInput.requestPermission()` 捕获 `NotAllowedError` / `SecurityError` | `phase: 'unavailable'`, `errorReason: 'permission-denied'` | 提供重新授权或返回入口,不进入 calibration/countdown/playing |
| 未检测到设备 | `getUserMedia` 捕获 `NotFoundError` / `DevicesNotFoundError` | `phase: 'unavailable'`, `errorReason: 'not-found'` | 展示麦克风不可用,可重试授权或返回 |
| 设备被占用或不可读 | `getUserMedia` 捕获 `NotReadableError` / `TrackStartError` | `phase: 'unavailable'`, `errorReason: 'not-readable'` | 展示麦克风不可用,可重试授权或返回 |
| AudioContext 被移动端策略拦截 | 用户手势后创建 / resume `AudioContext` 失败 | `phase: 'unavailable'`, `errorReason: 'audio-context-blocked'` | 提示点击重试,不自动循环请求 |
| 校准超时 | `BarkBattleController` 在 calibration 阶段等待样本超出 `calibrationMaxWaitMs` | `phase: 'unavailable'`, `errorReason: 'calibration-timeout'` | 展示麦克风输入不可用,提供重试校准入口 |
| 校准样本不可读 | `AudioAnalyserSampler.sample()` 持续返回空样本、NaN 或无法读取 buffer | `phase: 'unavailable'`, `errorReason: 'calibration-sample-unreadable'` | 展示麦克风输入不可用,提供重试校准入口 |
前端只根据错误分类展示可操作状态:重试授权、重试校准、返回、或使用调试备用输入。不要把浏览器原始错误堆栈展示给玩家。
## 8. Phaser Scene 切分
### 8.1 BarkBattleBootScene
职责:
- 初始化 Phaser 全局配置。
- 注册 scale、background color、全局事件桥。
- 不加载重资源,不处理玩法规则。
### 8.2 BarkBattlePreloadScene
职责:
- 根据 `barkBattleAssetManifest` 加载背景、狗狗 sprite、声浪 FX、拟声词 bitmap / atlas、轻量音效。
- 使用稳定 manifest key不在 gameplay 代码中散写文件路径。
- 加载完成后进入 `BarkBattleScene`
### 8.3 BarkBattleScene
职责:
- 创建横版舞台、左右狗狗、背景层、声浪层、拟声词层。
- 每帧读取最新 `BarkBattleSnapshot`
- 根据 snapshot 更新:
- 狗狗 idle / bark / win / lose 动画
- 声浪强度
- camera shake
- transient bark words
- shockwave
- 把可选的调试输入 action 传给 controller但不处理麦克风和规则。
不得在 Scene 中实现:
- 叫声计数
- 胜负判定
- 能量条规则
- 权限流程
- 结算数据计算
### 8.4 BarkBattleFxScene可选
如果特效复杂,可拆出叠加 Scene
- 专门处理拟声词、粒子、冲击波和 camera shake。
- 通过视觉事件 ID 去重。
-`prefers-reduced-motion` 或低端设备降级。
首版也可以先把 FX 保持在 `BarkBattleScene` 内,但必须仍然只消费 snapshot / visual events。
## 9. DOM HUD 设计
### 9.1 HUD 层级
DOM HUD 建议覆盖在 Phaser Canvas 上方:
```text
BarkBattleRuntimeShell
├─ <div className="bark-battle-canvas-host" />
└─ <BarkBattleHud snapshot={snapshot} uiState={uiState} />
```
HUD 分区:
- 顶部:红蓝声浪能量条 + 小型剩余时间。
- 中央:仅在倒计时、关键提示或结算时短暂展示大号状态。
- 左右边缘:双方简洁状态,例如叫声次数 / combo chip。
- 底部角落:麦克风状态、重试、小菜单。
- 结算:独立居中面板,显示胜负、叫声次数、最大音量、评分、再来一局、返回。
### 9.2 Playfield 保护
遵循 game UI 约束:
1. 正常 playing 阶段保持中心和下中部 playfield 清爽,不常驻长文案。
2. 不把规则说明、长控制说明、多段提示默认铺在画面上。
3. 权限、设置、结算使用独立面板或弹层,不在当前面板下面展开一大块内容。
4. 移动端优先保证顶部能量条、倒计时、狗狗和重试入口可见可点。
5. 大动效不能遮挡顶部能量条和倒计时。
### 9.3 CSS 设计建议
- 使用局部 CSS class 或 CSS module避免污染全站。
- 使用 CSS 变量定义主题:
- `--bark-player-color`
- `--bark-opponent-color`
- `--bark-panel-bg`
- `--bark-safe-bottom`
- 使用 `dvh` / `svh` 和 safe-area inset 处理移动端地址栏与刘海。
- `pointer-events` 分层HUD 容器默认 `pointer-events: none`,按钮和面板恢复 `pointer-events: auto`
## 10. 移动端与权限降级
### 10.1 移动端输入约束
移动端浏览器通常要求用户手势才能启动 AudioContext。开局流程必须是
```text
玩家点击“开始” → requestPermission → 创建/恢复 AudioContext → calibration → countdown → playing
```
不要在页面加载时自动请求或自动启动 AudioContext。
### 10.2 响应式布局
移动端建议:
- 横屏优先呈现完整舞台;竖屏可保持舞台居中并缩小 HUD。
- 顶部能量条高度保持可读,但不要占满大面积。
- 结算面板宽度使用 `min(92vw, 420px)`
- 底部按钮避开 `env(safe-area-inset-bottom)`
- 非关键设置折叠进小菜单。
### 10.3 权限失败降级
权限失败时:
- `unsupported`:展示“当前浏览器不支持麦克风输入”,提供返回入口,不展示开始声控按钮。
- `non-secure-context`:展示“当前环境无法使用麦克风”,提示切换到受支持的安全环境或返回。
- `permission-denied`:展示简短说明和“重新授权”入口。
- `not-found`:提示未检测到麦克风,提供重试授权或返回入口。
- `not-readable`:提示麦克风被占用或暂时不可读,提供重试授权或返回入口。
- `audio-context-blocked`:提示点击重试。
- `calibration-timeout` / `calibration-sample-unreadable`:提示麦克风输入不可用,提供“重新校准”和返回入口。
可选开发调试降级:
- 本地 dev 可启用键盘 mock input例如按住空格模拟音量峰值。
- mock input 必须标记为开发/调试能力,不作为正式竞技能力。
## 11. 测试策略
### 11.1 Domain 单元测试(优先)
目标:不接 Phaser、不接 DOM、不接 Web Audio。
建议测试:
- `BarkDetector`:超过阈值且间隔足够时计为一次有效叫声。
- `BarkDetector`:持续噪音不会无限计数。
- `BarkDetector`:低于环境噪音阈值不计入叫声。
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
- `EnergyTugOfWar`energy clamp 在 `-100..100`
- `BarkBattleSession`:倒计时结束进入 playing。
- `BarkBattleSession`:剩余时间归零进入 finished。
- `BarkBattleSession`:按 energy 和 drawThreshold 判定胜 / 负 / 平。
### 11.2 Application 测试
目标验证输入样本、AI、tick 和 snapshot 编排。
建议测试:
- 权限允许后进入校准,再进入倒计时。
- 权限拒绝后 `phase``unavailable``errorReason``permission-denied`,不进入 playing。
- 非安全上下文、设备未找到、设备不可读、AudioContext 被拦截时controller snapshot 都进入 `phase: 'unavailable'`,并保留可供 HUD 区分的 `errorReason`
- 校准超时或样本持续不可读时controller snapshot 使用 `errorReason: 'calibration-timeout'``calibration-sample-unreadable`,并提供重试校准动作。
- 提交 mock audio sample 后 snapshot 中玩家状态更新。
- AI 对手 power 参与能量条拉锯。
- `lastEvents` 只发布新增视觉事件。
### 11.3 HUD 组件测试
目标:验证 snapshot 到 DOM 的展示映射。
建议测试:
- playing 阶段展示倒计时和能量条。
- energy 正负值映射到玩家 / 对手侧比例。
- `errorReason: 'permission-denied'` 展示重试授权入口。
- `errorReason: 'unsupported'` 展示返回入口且不展示开始声控按钮。
- `not-found``not-readable``non-secure-context``audio-context-blocked``calibration-timeout``calibration-sample-unreadable` 分别映射到可区分的简短状态文案和对应操作。
- finished 展示胜负、叫声次数和再来一局。
- 移动端 class / 结构不依赖 Phaser Canvas 才能渲染。
### 11.4 Phaser 集成与 smoke
自动化层面不强行在 Vitest 中完整启动 Phaser。建议
- 用 adapter mock 测试 Scene 消费 snapshot 的纯映射函数。
- 浏览器 smoke 验证真实 Canvas、Web Audio 和动画。
- 若后续引入 Playwright再做最小视觉和交互 smoke。
## 12. 验证命令建议
文档阶段只需要编码检查和 diff 检查:
```bash
npm run check:encoding
git diff -- docs/prd/BARK_BATTLE_BDD_2026-05-11.md docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
```
后续实现 domain 后建议:
```bash
npm run test -- --run src/games/bark-battle/domain/**/*.test.ts
npm run typecheck
npm run check:encoding
```
后续实现 infrastructure/application 错误状态后建议:
```bash
npm run test -- --run src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts
npm run typecheck
npm run check:encoding
```
后续实现 HUD 后建议:
```bash
npm run test -- --run src/games/bark-battle/ui/**/*.test.tsx
npm run lint:eslint
npm run typecheck
npm run check:encoding
```
后续接入浏览器 runtime 后建议:
```bash
npm run dev:web
# 人工 smoke授权麦克风 → 校准 → 发声 → 能量条变化 → 结算 → 再来一局
```
若未来接入 Genarrative 正式玩法类型、后端持久化或发布链路再追加对应契约、api-server 和 SpacetimeDB 验证;首版 runtime 原型不应提前新增这些命令作为门槛。
## 13. 后续落地顺序
建议后续实现按以下顺序推进:
1. 先建 `domain/` 和纯单元测试。
2. 实现 `BarkDetector``EnergyTugOfWar``BarkBattleSession` 的最小规则。
3.`application/` controller用 mock audio sample 跑通 snapshot。
4. 实现 DOM HUD 的 permission / energy / timer / result 展示。
5. 接入 `BrowserMicrophoneInput` 和校准流程。
6. 接入 Phaser host、Scene 和 asset manifest占位素材先跑通视觉反馈。
7. 做移动端视口和权限失败 smoke。
8. 产品确认后再决定是否进入正式玩法类型、作品发布和后端真相链。
## 14. 关键技术决策
1. 默认采用 Phaser 3 + TypeScript + Vite符合 2D 浏览器游戏默认路线。
2. 核心 simulation 放在纯 TypeScript domain严格不依赖 Phaser / Web Audio / DOM。
3. Web Audio 只作为输入 adapter输出归一化 `BarkAudioSample`
4. Phaser Scene 是 renderer只消费 snapshot 和 visual events不承载规则真相。
5. HUD 使用 DOM overlay承载权限、能量条、倒计时、结算和移动端响应式布局。
6. MVP 不做复杂狗叫语义识别,先用音量峰值、持续时长、冷却和环境噪音校准。
7. MVP 建议玩家 vs AI 单机 runtime正式成绩、排行榜、发布和奖励后续再交给后端链路。

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@
用户完成热身关所有步骤后,进入关卡选择。
当前后续游戏仍在设计中。热身结束后可先展示“开始游戏”按钮作为关卡选择占位,用户点击后进入下一关占位界面
热身结束后展示“开始游戏”按钮,用户点击后进入宝贝识物首关本地 Demo。该入口只用于热身关后的本地体验验证正式平台体验仍必须通过“宝贝识物”创作模板发布后在寓教于乐板块进入
### 3.3 固定流程顺序
@@ -642,7 +642,7 @@
1. `src/ChildMotionDemoApp.tsx` 挂载独立 Demo 应用壳。
2. `src/components/child-motion-demo/childMotionWarmupModel.ts` 维护热身步骤、圆环目标、2 秒保持判定、热身校准记录和当前运行时会话完成标记。
3. `src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 实现横屏舞台、背景虚化占位层、角色剪影、绿色圆环、手势引导、热身记录面板、热身完成后的“开始游戏”按钮和下一关占位界面
3. `src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 实现横屏舞台、背景虚化占位层、角色剪影、绿色圆环、手势引导、热身记录面板、热身完成后的“开始游戏”按钮,并复用宝贝识物运行态进入首关本地 Demo
4. `src/services/child-motion-demo/childMotionDebugInput.ts` 保留开发者调试输入适配层,后续可被正式动作识别 SDK 适配层替换或并行接入。
5. `src/routing/appRoutes.tsx` 新增 `/child-motion-demo` 独立路由,并复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时不允许通过该直达路径进入 Demo。
@@ -669,19 +669,27 @@
当前未接入但已保留边界:
1. 正式语音播报接口暂不接入,当前先展示热身文案。
2. 正式 gpt-image-2 视觉资源暂不接入,当前使用 CSS 占位表达相同位置和状态
3. 后续关卡安全边界暂停逻辑暂未落地,当前只完成热身记录和下一关按钮占位。
2. 后续关卡安全边界暂停逻辑暂未落地,当前只完成热身记录和宝贝识物首关本地 Demo 衔接
## 16. 当前视觉资产与生图口径补充
儿童动作 Demo 的视觉口径已经统一收敛到绘本风格草地舞台:
1. 舞台主环境采用卡通绘本风格、明亮草地、天空、小山坡和树木的组合,默认背景环境需要保证中心与下方前景留空,便于角色轮廓和地面指示环叠加。
2. `src/index.css` 中的热身舞台、摄像头背景层、地面、角色轮廓、地面圆环、开始按钮和横屏提示均按绘本草地风格重做,未生成真实背景图时由 CSS 兜底
3. 真实背景图的默认输出路径固定为 `public/child-motion-demo/picture-book-grass-stage.webp`
4. 生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 触发;脚本使用 `gpt-image-2-all` 调用 VectorEngine `POST /v1/images/generations`
5. 当前本机工作区未检测到 `VECTOR_ENGINE_BASE_URL``VECTOR_ENGINE_API_KEY`,因此暂时只能完成 dry-run 或代码层接入,不能直接产出真实 image-2 资产。
6. 若后续补齐 VectorEngine 私密配置,再运行 live 生成即可把真实绘本背景写入上述固定路径,页面会自动读取
2. 该卡通绘本草地风格是儿童动作 Demo 后续场景、物品、UI 资源的全局风格要求;新增资源不得切回暗色科技风、真实照片风或后台面板风
3. `src/index.css` 中的热身舞台、摄像头背景层、地面、角色轮廓、地面圆环、开始按钮和横屏提示均按绘本草地风格接入真实资源;资源加载失败时保留 CSS 兜底
4. 生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 触发;脚本使用 `gpt-image-2-all` 调用 VectorEngine `POST /v1/images/generations`,透明资源先生成品红底源图,再在本地移除色键,源图写入 `tmp/child-motion-demo-assets/`
5. 当前已生成并接入以下正式 Demo 资源:
- `public/child-motion-demo/picture-book-grass-stage.png`:默认草地舞台背景
- `public/child-motion-demo/picture-book-foreground-grass-v2.png`:底部前景草坪条,只覆盖舞台下沿,不作为整块地板拉伸。
- `public/child-motion-demo/picture-book-ground-ring-v2.png`已按透视绘制的地面椭圆指示环CSS 只等比缩放。
- `public/child-motion-demo/picture-book-character-outline-v2.png`:半透明用户角色轮廓,使用独立去背后处理避免内部填充被误删。
- `public/child-motion-demo/picture-book-hud-strip-v2.png`:顶部 HUD 细长软纸条。
- `public/child-motion-demo/picture-book-calibration-strip-v2.png`:右下角五格热身状态条。
- `public/child-motion-demo/picture-book-start-panel-v2.png`:开始按钮背后的轻盈托盘。
- `public/child-motion-demo/picture-book-ui-button-v2.png`:开始按钮绘本风按钮底图。
6. v2 资源按最终用途拆分CSS 必须按资源原始比例、`aspect-ratio``background-size: contain / auto` 等方式等比使用;禁止把方形面板强行拉伸为 HUD、状态条或地板也禁止把底部草坪扩展成覆盖角色脚下的大色块。
7. 若后续补充或重绘资源,应先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt 和输出路径,再使用 `--live --only <asset-id>` 小批量生成;仅调整透明去背、裁切、画布归一或品红边缘时,可用 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用 `tmp/child-motion-demo-assets/` 中的源图,不额外请求 image-2不得把 `VECTOR_ENGINE_API_KEY`、源图或中间预览图提交到仓库。
已执行的定向验证命令:

View File

@@ -76,3 +76,16 @@
3. 只有用户显式修改或重置密码后,才允许密码登录。
后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力,验证码登录 reducer 承担“无账号则自动注册”的唯一注册入口。
## 5. 2026-05-12 快照同步修复
重置密码和修改密码都会改变认证真相:`password_hash``password_login_enabled``token_version`,重置密码还会立即创建新的 refresh session。因此 API 层在 `POST /api/auth/password/change``POST /api/auth/password/reset` 成功后,必须和密码登录、手机号登录、刷新、退出一样调用 `sync_auth_store_snapshot_to_spacetime()`
若只更新本地 `InMemoryAuthStore` 而不同步 SpacetimeDB 认证快照,`api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态,表现为用户已通过忘记密码重设成功,但再次密码登录仍返回“手机号或密码错误”。启动恢复时应从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照;当本地文件更新且远端表无更新时间戳时,优先使用本地文件并尝试回写 SpacetimeDB避免旧远端状态覆盖刚重设的密码。
验证命令:
```bash
cargo test -p module-auth password --manifest-path server-rs/Cargo.toml
cargo test -p api-server password --manifest-path server-rs/Cargo.toml
```

View File

@@ -4,8 +4,12 @@
## 文档列表
- [WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md](./WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md):记录微信小程序 `web-view` 壳的最小接入范围、需要填写的 H5 业务域名、微信后台配置、`npm run check:wechat-miniprogram-auth` 可重复登录链路 smoke 和后续原生化边界。
- [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”后端 DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界、shared contracts、作品配置、runtime run、派生成绩、排行榜、`work_play_start` 埋点、migration/绑定生成策略,以及不保存原始麦克风音频的隐私与反作弊约束。
- [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”2D 浏览器 runtime 技术方案,明确 Phaser + TypeScript + Vite 选型、纯 TS simulation 与 Phaser renderer/DOM HUD 边界、Web Audio 输入适配、移动端权限降级和后续测试验证命令。
- [PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md](./PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md):记录直接访问公开作品详情深链时作品不存在或已下架的回首页修复,避免关闭提示后停在 `work-detail` 空状态白屏。
- [PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md](./PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md):冻结移动端推荐页隐藏顶部品牌栏、扩大推荐卡片可用高度,以及只在底部作品信息区承接切换作品手势的布局口径。
- [BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md](./BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md):冻结寓教于乐 `宝贝识物` 模板创作发布线程的前端入口、契约、service、结果页、发布标签和后端 image-2 接口预留边界。
- [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开发落地规格,覆盖横屏展示、摄像头背景虚化、角色剪影、绿色圆环 2 秒保持、动作教学、当前会话内空间边界记录和后续关卡安全暂停规则。
- [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md)记录运行态输入设备抽象层明确鼠标、触控、mocap 等设备统一归一为通用拖拽语义,玩法组件只负责解释目标和落点。
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异;同时冻结 `shared-contracts` 不得反向依赖 `platform-*`,避免 SpacetimeDB 模块发布时拉入 `wasm-bindgen`

View File

@@ -92,6 +92,25 @@
2. 若是并入已有手机号正式账号,则返回目标正式账号快照,当前实现会保持其账号主登录方式,例如 `loginMethod = phone`
3. 但 access token 中的 `provider` 仍按**本次登录来源**签发,不依赖账号主登录方式推断,因此微信绑定后的当前会话仍会签发 `provider = wechat`
### 3.4 `POST /api/auth/wechat/miniprogram-login`
职责固定为:
1. 接收微信小程序原生壳通过 `wx.login` 拿到的 `code`
2.`Axum` 内调用微信 `jscode2session`,兑换 `openid/unionid`
3. 复用 `resolve_login` 处理 `unionid/openid -> user_id` 的查找、补写和待绑定账号创建。
4. 签发本系统 access token并创建 refresh session。
5. 返回:
- `token`
- `bindingStatus`
- `user`
关键约束:
1. 小程序壳不能把裸 `openid` 直接拼给 H5 做登录。
2. H5 仍只消费本系统 `auth_token`,小程序壳只是把这枚 token 放入既有 hash 回调格式。
3. 小程序请求必须补传 `x-client-type=mini_program``x-client-runtime=wechat_mini_program`,用于 refresh session 记录来源。
## 4. 当前最小实现策略
当前阶段为了先打通 Rust 后端闭环,采用以下最小实现:
@@ -125,6 +144,7 @@
4. `GET /api/auth/wechat/start`
5. `GET /api/auth/wechat/callback`
6. `POST /api/auth/wechat/bind-phone`
7. `POST /api/auth/wechat/miniprogram-login`
## 6. 环境变量
@@ -139,11 +159,14 @@
7. `WECHAT_AUTHORIZE_ENDPOINT`
8. `WECHAT_ACCESS_TOKEN_ENDPOINT`
9. `WECHAT_USER_INFO_ENDPOINT`
10. `WECHAT_STATE_TTL_MINUTES`
11. `WECHAT_MOCK_USER_ID`
12. `WECHAT_MOCK_UNION_ID`
13. `WECHAT_MOCK_DISPLAY_NAME`
14. `WECHAT_MOCK_AVATAR_URL`
10. `WECHAT_JS_CODE_SESSION_ENDPOINT`
11. `WECHAT_MINI_PROGRAM_APP_ID`
12. `WECHAT_MINI_PROGRAM_APP_SECRET`
13. `WECHAT_STATE_TTL_MINUTES`
14. `WECHAT_MOCK_USER_ID`
15. `WECHAT_MOCK_UNION_ID`
16. `WECHAT_MOCK_DISPLAY_NAME`
17. `WECHAT_MOCK_AVATAR_URL`
## 7. 与后续 SpacetimeDB 的衔接要求

View File

@@ -100,6 +100,9 @@ real 模式行为固定为:
| `WECHAT_AUTHORIZE_ENDPOINT` | 否 | 默认桌面二维码授权地址 |
| `WECHAT_ACCESS_TOKEN_ENDPOINT` | 否 | 默认 access_token 接口 |
| `WECHAT_USER_INFO_ENDPOINT` | 否 | 默认用户信息接口 |
| `WECHAT_JS_CODE_SESSION_ENDPOINT` | 否 | 默认小程序 `jscode2session` 接口 |
| `WECHAT_MINI_PROGRAM_APP_ID` | 小程序 `real` 模式必填 | 微信小程序 AppID不填时回退 `WECHAT_APP_ID` |
| `WECHAT_MINI_PROGRAM_APP_SECRET` | 小程序 `real` 模式必填 | 微信小程序 AppSecret不填时回退 `WECHAT_APP_SECRET` |
| `WECHAT_STATE_TTL_MINUTES` | 否 | state 有效期,默认 `15` 分钟 |
补充说明:
@@ -225,7 +228,46 @@ https://game.example.com
- `wechatBound = true`
- `bindingStatus` 已更新为目标状态
## 8. 账号命中规则
## 8. 小程序 web-view 登录联调步骤
小程序壳走原生 `wx.login`,不走网页 OAuth callback。联调前需要额外确认
```bash
WECHAT_AUTH_ENABLED=true
WECHAT_AUTH_PROVIDER=real
WECHAT_MINI_PROGRAM_APP_ID="你的微信小程序 AppID"
WECHAT_MINI_PROGRAM_APP_SECRET="你的微信小程序 AppSecret"
```
`miniprogram/config.js` 中确认:
```js
const WEB_VIEW_ENTRY_URL = 'https://你的H5业务域名/';
const API_BASE_URL = 'https://你的服务器域名/';
const MINI_PROGRAM_APP_ID = '你的微信小程序 AppID';
```
联调流程:
1. 微信开发者工具打开项目根目录。
2. 小程序启动后调用 `wx.login`
3. 小程序壳请求:
```http
POST /api/auth/wechat/miniprogram-login
```
4. 后端通过 `jscode2session` 兑换 `openid/unionid`
5. 后端返回系统 `token``bindingStatus``user`
6. 小程序壳打开 H5并在 hash 中附加:
- `auth_provider=wechat`
- `auth_token=...`
- `auth_binding_status=active|pending_bind_phone`
7. H5 消费 hash 后通过 `/api/auth/me` 恢复登录态。
这里不能把裸 `openid` 作为 web-view query 登录凭证;`openid` 只能留在后端身份绑定层H5 只消费本系统 JWT。
## 9. 账号命中规则
当前实现固定按以下顺序命中已有账号:
@@ -238,7 +280,7 @@ https://game.example.com
1. 若按 `unionid` 命中了已有微信身份,但本次微信回调带来了新的 `openid`,后端会把新的 `openid -> user_id` 映射补齐
2. 若后续绑定手机号时发现该手机号已经属于正式账号,则会把微信身份并入这个正式账号
## 9. 前端验收点
## 10. 前端验收点
前端联调时至少检查以下行为:

View File

@@ -0,0 +1,187 @@
# 微信小程序 web-view 壳接入记录
日期:`2026-05-03`
## 1. 目标
本次先用微信小程序 `web-view` 承载现有 H5不重写 React/Vite 主前端,也不把 SpacetimeDB SDK 或业务规则搬进小程序端。
当前小程序壳只承担五件事:
1. 提供微信开发者工具可识别的 `miniprogram/` 工程根目录。
2. 在原生小程序壳中调用 `wx.login` 获取小程序 `code`
3. 调用服务器域名下的 `/api/auth/wechat/miniprogram-login`,由 Rust `api-server` 兑换微信身份并签发系统登录态。
4. 若后端返回 `pending_bind_phone`,先在小程序原生层通过 `button open-type="getPhoneNumber"` 取得用户同意后的手机号动态令牌,再调用 `/api/auth/wechat/bind-phone` 完成绑定。
5. 用一个全屏 `web-view` 打开现有 H5 入口,并把系统 `auth_token` 放入 H5 现有登录回调 hash。
重要边界:
1. `openid` 只作为后端微信身份绑定依据,不直接暴露给 H5 当登录凭证。
2. H5 继续消费本系统 JWT也就是 `#auth_provider=wechat&auth_token=...&auth_binding_status=...`
3. 这与 [`WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md`](./WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md) 中“微信只提供三方身份Axum 签发系统 JWT”的边界一致。
## 2. 文件入口
| 文件 | 说明 |
| --- | --- |
| `project.config.json` | 指定 `miniprogramRoot: "miniprogram/"`。 |
| `miniprogram/app.json` | 小程序全局配置,注册 `pages/web-view/index`。 |
| `miniprogram/config.js` | 业务域名入口配置,需要部署时填写。 |
| `miniprogram/pages/web-view/index.*` | 最小 web-view 页面。 |
| `server-rs/crates/api-server/src/wechat_auth.rs` | 新增小程序登录接口 `/api/auth/wechat/miniprogram-login`。 |
| `server-rs/crates/platform-auth/src/lib.rs` | 新增 `jscode2session` 兑换能力。 |
## 3. 需要手工填写的配置
`miniprogram/config.js` 中填写:
```js
const WEB_VIEW_ENTRY_URL = 'https://你的H5业务域名/';
const API_BASE_URL = 'https://你的服务器域名/';
const MINI_PROGRAM_APP_ID = '你的微信小程序 AppID';
const MINI_PROGRAM_ENV = 'develop';
```
约束:
1. 必须是 `https`
2. 不能是 `localhost` 或 IP。
3. `WEB_VIEW_ENTRY_URL` 域名需要在微信小程序后台配置为业务域名。
4. `API_BASE_URL` 域名需要在微信小程序后台配置为 request 合法域名。
5. H5 页面里的 API、图片、音视频、iframe 等外链也要满足微信侧域名与证书要求。
`api-server` 环境变量中填写:
```bash
WECHAT_AUTH_ENABLED=true
WECHAT_AUTH_PROVIDER=real
WECHAT_MINI_PROGRAM_APP_ID="你的微信小程序 AppID"
WECHAT_MINI_PROGRAM_APP_SECRET="你的微信小程序 AppSecret"
WECHAT_JS_CODE_SESSION_ENDPOINT="https://api.weixin.qq.com/sns/jscode2session"
WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/cgi-bin/stable_token"
WECHAT_PHONE_NUMBER_ENDPOINT="https://api.weixin.qq.com/wxa/business/getuserphonenumber"
```
如果开放平台网页 OAuth 与小程序使用同一个 AppID/Secret也可以继续使用已有
```bash
WECHAT_APP_ID="你的微信 AppID"
WECHAT_APP_SECRET="你的微信 AppSecret"
```
但正式部署建议把小程序配置写到 `WECHAT_MINI_PROGRAM_APP_ID``WECHAT_MINI_PROGRAM_APP_SECRET`,避免和网页 OAuth 配置混淆。
`WEB_VIEW_SOURCE_QUERY` 默认附加:
```text
clientType=mini_program
clientRuntime=wechat_mini_program
```
小程序壳调用登录接口时会补传:
```text
x-client-type=mini_program
x-client-runtime=wechat_mini_program
x-client-platform=ios|android|unknown
x-client-instance-id=<小程序本地持久化随机值>
x-mini-program-app-id=<MINI_PROGRAM_APP_ID>
x-mini-program-env=<MINI_PROGRAM_ENV>
```
这些字段会进入 refresh session 的客户端身份快照URL query 只作为 H5 识别宿主来源的轻量标记,不作为鉴权依据。
## 4. 登录链路
当前登录链路固定为:
1. 小程序页面启动。
2. 调用 `wx.login` 获取一次性 `code`
3. 小程序壳请求:
```http
POST /api/auth/wechat/miniprogram-login
Content-Type: application/json
{
"code": "wx.login code"
}
```
4. `api-server` 调用微信 `jscode2session` 兑换 `openid/unionid`
5. `api-server` 复用现有微信身份逻辑:
- 先按 `unionid` 命中已有身份
- 再按 `openid` 命中已有身份
- 都没有命中时创建 `pending_bind_phone` 的微信壳账号
6. `api-server` 签发系统 access token并写入 refresh session。
7. 如果返回 `bindingStatus=active`,小程序壳打开:
```text
https://你的H5业务域名/#auth_provider=wechat&auth_token=<系统JWT>&auth_binding_status=active
```
8. 如果返回 `bindingStatus=pending_bind_phone`,小程序壳暂不打开 H5而是展示原生 `getPhoneNumber` 按钮。用户点击并同意后,小程序把 `bindgetphonenumber` 事件里的 `detail.code` 作为 `wechatPhoneCode` 传给:
```http
POST /api/auth/wechat/bind-phone
Authorization: Bearer <JWT>
Content-Type: application/json
{
"wechatPhoneCode": "getPhoneNumber code"
}
```
9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。成功后重新签发 `active` 系统 token。
10. H5 复用 `consumeAuthCallbackResult()` 消费 `auth_token` 并进入现有登录态恢复流程。
补充H5 里的旧短信验证码绑定页继续保留为非小程序环境兜底;小程序原生手机号授权只替代“手动输入手机号 + 短信验证码”这一步,不代表后台静默读取本机号码。
## 5. 微信后台配置
至少需要在小程序后台配置:
1. `业务域名`:承载 H5 的域名。
2. `request 合法域名``API_BASE_URL` 对应的服务器域名。
3. `socket 合法域名`:若后续小程序原生层直连 WebSocket 才需要;当前不启用。
当前仓库的 H5 仍建议通过同域 `/api/*` 访问 Rust `api-server`,避免在小程序和 H5 中分别维护跨域白名单。
## 6. 当前不做的事
本次不做原生小程序页面迁移,原因是当前主前端依赖:
1. React DOM 挂载、浏览器 history 和 `window.location`
2. `localStorage` / `sessionStorage`
3. 浏览器 `fetch``ReadableStream` SSE。
4. DOM、Canvas、Three.js 等浏览器渲染能力。
这些能力不能稳定原样运行在原生小程序宿主中。后续如要原生化,应新建小程序端宿主,复用 `packages/shared` 契约和 `api-server` BFF而不是把 `src/` 整体搬过去。
本次也不做 `openid` query 直登。原因是 `openid` 不是本系统签发的登录凭证,不能表达 token 版本、会话 ID、绑定状态、角色与过期时间也不能被 H5 直接信任。
## 7. 验收口径
可重复自动化 smoke
```bash
npm run check:wechat-miniprogram-auth
```
该命令固定覆盖三段链路:
1. 静态确认 `miniprogram/pages/web-view/index.js` 会请求 `/api/auth/wechat/miniprogram-login`,携带 `mini_program / wechat_mini_program` 客户端来源头,并把 `auth_provider/auth_token/auth_binding_status` 拼入 H5 hash。
2. 运行 `api-server` 定向测试 `wechat_miniprogram_login_returns_system_token_and_marks_session_source`,断言小程序登录返回 `token/bindingStatus/user`、写入 refresh cookie并且 `/api/auth/sessions` 能看到 `clientType=mini_program``clientRuntime=wechat_mini_program``miniProgramAppId`
3. 静态确认小程序壳在 `pending_bind_phone` 时使用 `getPhoneNumber``wechatPhoneCode` 调用 `/api/auth/wechat/bind-phone`,而不是打开 H5 后再要求手输手机号。
4. 运行前端 `authService` 定向测试,断言 `consumeAuthCallbackResult()` 会消费 `#auth_provider=wechat&auth_token=...&auth_binding_status=...`、保存 access token并清理地址栏 hash。
手工联调仍按以下口径确认真实微信与域名配置:
1. 微信开发者工具打开项目根目录后,识别 `miniprogram/` 为小程序源码目录。
2. 未填写 `WEB_VIEW_ENTRY_URL``API_BASE_URL` 时,页面显示配置提示,不出现空白页。
3. 填写已配置业务域名后,小程序先请求 `/api/auth/wechat/miniprogram-login`
4. 后端返回 `token/bindingStatus/user`,并写入 refresh cookie。
5. 若返回 `pending_bind_phone`,先看到小程序原生授权手机号按钮;用户同意后,小程序请求 `/api/auth/wechat/bind-phone` 且请求体包含 `wechatPhoneCode`
6. 绑定成功后首页全屏打开 H5URL hash 中包含 `auth_provider=wechat``auth_token``auth_binding_status=active`
7. H5 内 `consumeAuthCallbackResult()` 消费 hash 后,`/api/auth/me` 能返回当前用户。
8. `/api/auth/sessions` 能看到来源为 `mini_program / wechat_mini_program` 的会话记录。