Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
This commit is contained in:
@@ -0,0 +1,561 @@
|
||||
# 声控狗叫对战 2D 浏览器游戏设计与实现计划
|
||||
|
||||
## 目标
|
||||
|
||||
基于用户提供的视频:
|
||||
|
||||
`C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`
|
||||
|
||||
提取其中“双方比狗叫”的核心玩法,并按照 BDD / TDD / DDD 的方法,为 Genarrative 中可运行于浏览器的 2D 游戏方案生成一份可落地设计与实现思路。实现方向遵循仓库内 `game-studio` 插件工作流,默认采用 2D Phaser + TypeScript + Vite + DOM HUD 的浏览器游戏架构。
|
||||
|
||||
本计划仅做方案设计,不直接编码。
|
||||
|
||||
## 当前上下文与输入分析
|
||||
|
||||
### 已识别视频核心画面
|
||||
|
||||
通过抽帧观察,视频中的游戏呈现出以下稳定特征:
|
||||
|
||||
- 画面是横版 2D 手绘舞台,场景包括公园、海边等固定关卡背景。
|
||||
- 双方各有一只狗作为对战角色,站在左右两侧。
|
||||
- 中央有明显倒计时,例如 `30`、`28`。
|
||||
- 顶部有红蓝双方拉锯式能量条 / 进度条。
|
||||
- 中央提示出现:`对着麦克风汪一声`、`用声音大小 + 叫声次数推动能量条!`
|
||||
- 玩家输入不是传统键鼠,而是麦克风声音。
|
||||
- 玩家需要模仿狗叫,系统根据声音大小与叫声次数推动能量条。
|
||||
- 屏幕会根据叫声出现 `BARK`、`WOOF`、`WAN`、`WANGOOF` 等拟声词与冲击波视觉反馈。
|
||||
- 回合结束时,根据能量条偏向或推进结果判定胜负。
|
||||
|
||||
### 提炼出的核心玩法
|
||||
|
||||
这是一个“声控拔河式狗叫对战”小游戏:
|
||||
|
||||
- 两名玩家 / 一名玩家对 AI 分别代表左右两只狗。
|
||||
- 每局限时 30 秒。
|
||||
- 玩家通过麦克风持续发出狗叫声。
|
||||
- 游戏实时分析音量峰值、叫声次数、叫声节奏。
|
||||
- 声音越大、叫声越密集,己方推动力越强。
|
||||
- 顶部能量条在双方推动力差值下左右移动。
|
||||
- 时间结束后,能量条偏向哪一方,哪一方获胜。
|
||||
|
||||
### 需要合理抽象的地方
|
||||
|
||||
视频中存在直播弹幕、贴图、表情包、遮挡层,这些不是游戏本体机制。本方案只吸收游戏本体核心:
|
||||
|
||||
- 双方狗狗对叫
|
||||
- 麦克风输入
|
||||
- 声音强度 + 次数判定
|
||||
- 红蓝拉锯能量条
|
||||
- 限时回合
|
||||
- 夸张拟声词与冲击波反馈
|
||||
|
||||
## game-studio 插件路线
|
||||
|
||||
根据仓库内 `.hermes/plugins/game-studio` 技能:
|
||||
|
||||
- 早期游戏工作先走 `game-studio` 总入口。
|
||||
- 2D 浏览器游戏默认选择 Phaser。
|
||||
- 架构上需要分离 simulation 与 renderer。
|
||||
- HUD / 菜单 / 设置优先使用 DOM overlay,不把密集文字塞进 canvas。
|
||||
- 玩法状态不应由 Phaser Scene 直接持有,Scene 只负责渲染、动画、相机、输入适配。
|
||||
|
||||
因此本方案采用:
|
||||
|
||||
- Runtime:Phaser 3
|
||||
- Language:TypeScript
|
||||
- Build:Vite
|
||||
- UI:React/DOM HUD overlay 或项目现有 DOM UI 层
|
||||
- Audio input:Web Audio API + MediaDevices.getUserMedia
|
||||
- Simulation:纯 TS domain/service 层
|
||||
- Renderer:Phaser Scene 读取 simulation snapshot 并播放动画/特效
|
||||
|
||||
## 游戏概念设计
|
||||
|
||||
### 游戏名建议
|
||||
|
||||
- 中文:`汪汪声浪大作战`
|
||||
- 英文代号:`bark-battle`
|
||||
- Play type ID 建议:`bark-battle`
|
||||
|
||||
### 玩家幻想
|
||||
|
||||
玩家不是通过按键战斗,而是真的对着麦克风“汪汪叫”,把自己的狗狗声浪推向对手。游戏目标是在倒计时结束前用更响、更密集、更有节奏的叫声赢得声浪拔河。
|
||||
|
||||
### 核心动词
|
||||
|
||||
- 叫:对麦克风发出狗叫声。
|
||||
- 推:通过叫声推动能量条。
|
||||
- 压制:让能量条持续向对手方向倾斜。
|
||||
- 爆发:短时间内连续高质量叫声触发冲击波。
|
||||
- 防守:对手强势时通过持续叫声把能量条拉回。
|
||||
|
||||
### 单局流程
|
||||
|
||||
1. 准备阶段
|
||||
- 展示双方狗狗、地图、麦克风权限提示。
|
||||
- 用户授权麦克风。
|
||||
- 系统检测环境噪音并校准阈值。
|
||||
|
||||
2. 倒计时阶段
|
||||
- 3、2、1 或中央 `30` 倒计时开始。
|
||||
- 玩家看到提示:`对着麦克风汪一声`。
|
||||
|
||||
3. 对战阶段
|
||||
- 每帧或固定 tick 采集麦克风音量。
|
||||
- 根据音量峰值与短促叫声次数计算本方 barkPower。
|
||||
- AI 或远端对手产生 opponentPower。
|
||||
- 能量条根据 `playerPower - opponentPower` 拉锯。
|
||||
- 狗狗张嘴动画、拟声词、冲击波按声音强度生成。
|
||||
|
||||
4. 结算阶段
|
||||
- 30 秒结束。
|
||||
- 能量条偏玩家侧则胜利,偏对手侧则失败,接近中线则平局。
|
||||
- 展示叫声次数、最大音量、平均节奏、声浪评分。
|
||||
|
||||
5. 重开 / 返回
|
||||
- 支持再来一局。
|
||||
- 支持返回玩法入口或结果页。
|
||||
|
||||
## 规则设计
|
||||
|
||||
### 关键状态
|
||||
|
||||
```ts
|
||||
type BarkBattlePhase = 'permission' | 'calibration' | 'countdown' | 'playing' | 'finished'
|
||||
|
||||
type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
remainingMs: number
|
||||
energy: number // -100 到 100,负数偏对手,正数偏玩家
|
||||
player: BarkSideState
|
||||
opponent: BarkSideState
|
||||
winner: 'player' | 'opponent' | 'draw' | null
|
||||
}
|
||||
|
||||
type BarkSideState = {
|
||||
barkCount: number
|
||||
currentVolume: number
|
||||
recentPeak: number
|
||||
combo: number
|
||||
power: number
|
||||
isBarking: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 输入判定
|
||||
|
||||
#### 音量采样
|
||||
|
||||
- 使用 Web Audio API 创建 `AnalyserNode`。
|
||||
- 每个 simulation tick 读取频域或时域数据。
|
||||
- 计算 RMS 或 peak volume。
|
||||
- 根据校准后的环境噪音设置动态阈值。
|
||||
|
||||
#### 一次“叫声”的判定
|
||||
|
||||
一次有效叫声建议满足:
|
||||
|
||||
- 音量超过 `barkThreshold`。
|
||||
- 与上一次叫声峰值至少间隔 `minBarkGapMs`,避免持续噪音被无限计数。
|
||||
- 持续时长在合理范围,例如 80ms 到 1200ms。
|
||||
- 可选:频谱能量集中在中高频,不强制做复杂语音识别,MVP 先用音量 + 峰值节奏。
|
||||
|
||||
#### 推动力计算
|
||||
|
||||
```text
|
||||
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
|
||||
opponentPower = aiPower 或远端玩家 power
|
||||
energyDelta = (playerPower - opponentPower) * deltaTime * balanceFactor
|
||||
energy = clamp(energy + energyDelta, -100, 100)
|
||||
```
|
||||
|
||||
### AI 对手 MVP
|
||||
|
||||
若先做单机浏览器版,右侧对手可由 AI 模拟:
|
||||
|
||||
- 简单难度:周期性小叫,power 低。
|
||||
- 普通难度:有节奏地爆发,power 中等。
|
||||
- 困难难度:根据玩家领先程度自适应追赶,但不得作弊到不可赢。
|
||||
|
||||
后续可扩展为多人实时对战。
|
||||
|
||||
## BDD 行为场景
|
||||
|
||||
### 功能: 麦克风授权与准备
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战麦克风准备
|
||||
为了让玩家能用声音参与对战
|
||||
作为浏览器玩家
|
||||
我希望游戏在开局前明确请求麦克风权限并完成环境校准
|
||||
|
||||
场景: 玩家允许麦克风权限后进入准备倒计时
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家同意浏览器麦克风授权
|
||||
那么系统应进入环境噪音校准阶段
|
||||
而且校准完成后应显示开局倒计时
|
||||
|
||||
场景: 玩家拒绝麦克风权限
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家拒绝浏览器麦克风授权
|
||||
那么系统应显示无法声控游玩的提示
|
||||
而且应提供重试授权入口
|
||||
而且不应直接开始对战
|
||||
```
|
||||
|
||||
### 功能: 声音推动能量条
|
||||
|
||||
```gherkin
|
||||
功能: 声音大小和叫声次数推动能量条
|
||||
为了复刻双方比狗叫的核心体验
|
||||
作为玩家
|
||||
我希望自己的叫声能实时推动顶部能量条
|
||||
|
||||
场景: 玩家发出一次有效狗叫
|
||||
假如游戏处于 playing 阶段
|
||||
而且麦克风输入音量超过有效叫声阈值
|
||||
当系统检测到一次新的叫声峰值
|
||||
那么玩家叫声次数应增加 1
|
||||
而且玩家狗狗应播放张嘴吠叫动画
|
||||
而且画面应出现拟声词反馈
|
||||
|
||||
场景: 玩家连续大声狗叫压制对手
|
||||
假如游戏处于 playing 阶段
|
||||
而且玩家在短时间内产生多次有效叫声
|
||||
当玩家推动力高于对手推动力
|
||||
那么顶部能量条应向玩家侧移动
|
||||
而且玩家侧声浪特效应增强
|
||||
|
||||
场景: 环境噪音低于阈值不计入叫声
|
||||
假如游戏处于 playing 阶段
|
||||
当麦克风只有低于阈值的背景噪音
|
||||
那么玩家叫声次数不应增加
|
||||
而且能量条不应因为背景噪音明显移动
|
||||
```
|
||||
|
||||
### 功能: 限时胜负结算
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战胜负结算
|
||||
为了让单局对抗有明确目标
|
||||
作为玩家
|
||||
我希望倒计时结束后根据能量条位置判定胜负
|
||||
|
||||
场景: 倒计时结束时玩家侧占优
|
||||
假如游戏剩余时间归零
|
||||
而且能量条位于玩家侧
|
||||
当系统进入结算阶段
|
||||
那么系统应判定玩家胜利
|
||||
而且展示玩家叫声次数、最大音量和声浪评分
|
||||
|
||||
场景: 倒计时结束时双方接近平衡
|
||||
假如游戏剩余时间归零
|
||||
而且能量条处于平局阈值范围内
|
||||
当系统进入结算阶段
|
||||
那么系统应判定为平局
|
||||
而且展示再来一局入口
|
||||
```
|
||||
|
||||
### 功能: 移动端与无麦克风降级
|
||||
|
||||
```gherkin
|
||||
功能: 声控游戏移动端与无麦克风降级
|
||||
为了让不同设备玩家都能理解当前状态
|
||||
作为移动端或无麦克风环境玩家
|
||||
我希望系统给出清晰、可操作的降级路径
|
||||
|
||||
场景: 当前浏览器不支持麦克风 API
|
||||
假如玩家设备不支持 getUserMedia
|
||||
当玩家进入狗叫对战页面
|
||||
那么系统应显示设备不支持麦克风输入
|
||||
而且提供返回入口
|
||||
|
||||
场景: 移动端进入对战页面
|
||||
假如玩家使用移动端浏览器
|
||||
当玩家进入狗叫对战页面
|
||||
那么主要能量条、倒计时和狗狗角色应保持可见
|
||||
而且非关键设置应收起到菜单中
|
||||
```
|
||||
|
||||
## DDD 领域划分
|
||||
|
||||
### 领域层:bark-battle domain
|
||||
|
||||
职责:只处理玩法规则,不依赖 Phaser、DOM、Web Audio、后端。
|
||||
|
||||
建议模块:
|
||||
|
||||
- `BarkBattleSession`
|
||||
- 管理 phase、remainingMs、energy、winner。
|
||||
- `BarkDetector`
|
||||
- 根据音量样本判断是否形成一次有效叫声。
|
||||
- `EnergyTugOfWar`
|
||||
- 根据双方 power 更新能量条。
|
||||
- `BarkBattleScoring`
|
||||
- 计算最大音量、叫声次数、combo、评分。
|
||||
- `OpponentStrategy`
|
||||
- 单机 AI 对手策略接口。
|
||||
|
||||
领域规则必须可用纯单元测试验证。
|
||||
|
||||
### 应用层:use case / controller
|
||||
|
||||
职责:编排麦克风输入、simulation tick、AI 对手、结果输出。
|
||||
|
||||
建议用例:
|
||||
|
||||
- `requestMicrophonePermission()`
|
||||
- `calibrateAmbientNoise()`
|
||||
- `startBarkBattleSession()`
|
||||
- `submitAudioSample(sample)`
|
||||
- `tickBarkBattle(deltaMs)`
|
||||
- `finishBarkBattle()`
|
||||
|
||||
### 基础设施层
|
||||
|
||||
职责:浏览器 API 与引擎适配。
|
||||
|
||||
- `BrowserMicrophoneInput`
|
||||
- 封装 `navigator.mediaDevices.getUserMedia`。
|
||||
- 输出 normalized volume samples。
|
||||
- `PhaserBarkBattleScene`
|
||||
- 渲染狗狗、背景、拟声词、冲击波。
|
||||
- 不持有核心玩法规则。
|
||||
- `DomBarkBattleHud`
|
||||
- 展示倒计时、能量条、权限提示、结算面板。
|
||||
|
||||
### 表现层
|
||||
|
||||
- Phaser Canvas:地图、狗狗、声浪、粒子、拟声词。
|
||||
- DOM HUD:顶部能量条、倒计时、权限/结算/设置面板。
|
||||
|
||||
## TDD 落地顺序
|
||||
|
||||
### 第一轮:领域规则 RED-GREEN-REFACTOR
|
||||
|
||||
先写纯 TS 单元测试,不接 Phaser,不接麦克风。
|
||||
|
||||
目标测试:
|
||||
|
||||
- `BarkDetector`:超过阈值且间隔足够时计为一次叫声。
|
||||
- `BarkDetector`:持续噪音不会无限增加叫声次数。
|
||||
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
|
||||
- `EnergyTugOfWar`:energy 被 clamp 在 -100 到 100。
|
||||
- `BarkBattleSession`:倒计时归零后进入 finished。
|
||||
- `BarkBattleSession`:根据 energy 判定 player/opponent/draw。
|
||||
|
||||
### 第二轮:应用层测试
|
||||
|
||||
- 模拟音频 sample 输入,验证 session snapshot 更新。
|
||||
- 模拟 AI 对手 power,验证能量条拉锯。
|
||||
- 模拟权限失败,验证 phase 不进入 playing。
|
||||
|
||||
### 第三轮:组件 / 集成测试
|
||||
|
||||
- HUD 根据 snapshot 显示倒计时。
|
||||
- HUD 根据 energy 渲染红蓝能量条比例。
|
||||
- 权限拒绝时显示重试入口。
|
||||
- 结算阶段显示胜负与再来一局。
|
||||
|
||||
### 第四轮:浏览器 smoke / playtest
|
||||
|
||||
- 本地启动页面。
|
||||
- 授权麦克风。
|
||||
- 对麦克风发声后看到拟声词与能量条变化。
|
||||
- 移动端宽度下主游戏画面不被 HUD 遮挡。
|
||||
|
||||
## 建议文件结构
|
||||
|
||||
如果作为独立前端玩法原型,可采用:
|
||||
|
||||
```text
|
||||
src/games/bark-battle/
|
||||
domain/
|
||||
BarkBattleSession.ts
|
||||
BarkDetector.ts
|
||||
EnergyTugOfWar.ts
|
||||
BarkBattleScoring.ts
|
||||
OpponentStrategy.ts
|
||||
application/
|
||||
BarkBattleController.ts
|
||||
BrowserMicrophoneInput.ts
|
||||
phaser/
|
||||
BarkBattleScene.ts
|
||||
BarkBattlePreloadScene.ts
|
||||
barkBattleAssets.ts
|
||||
ui/
|
||||
BarkBattleHud.tsx
|
||||
BarkBattleResultPanel.tsx
|
||||
BarkBattlePermissionPanel.tsx
|
||||
tests/
|
||||
BarkDetector.test.ts
|
||||
EnergyTugOfWar.test.ts
|
||||
BarkBattleSession.test.ts
|
||||
```
|
||||
|
||||
如果接入 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
|
||||
```
|
||||
|
||||
MVP 阶段建议先做浏览器单机 runtime 原型,再决定是否进入创作入口、作品发布、广场和后端持久化。
|
||||
|
||||
## UI / 视觉方向
|
||||
|
||||
### 画面
|
||||
|
||||
- 横版固定舞台。
|
||||
- 左右两只狗对峙。
|
||||
- 背景可先做公园一张图,后续扩展海边、街区等地图。
|
||||
- 狗狗用 2D sprite 或简单骨架帧动画。
|
||||
|
||||
### HUD
|
||||
|
||||
- 顶部:红蓝声浪能量条。
|
||||
- 中央:大号倒计时,只在开局和关键时间突出显示。
|
||||
- 左右:双方狗狗状态,不堆叠复杂面板。
|
||||
- 底部或角落:麦克风状态、小型重试按钮。
|
||||
- 结算:居中弹出简洁面板,显示胜负和关键数据。
|
||||
|
||||
### 动效
|
||||
|
||||
- 叫声触发狗狗张嘴。
|
||||
- 声音越大,拟声词越大,冲击波越宽。
|
||||
- combo 时触发短暂屏幕震动,但不能遮挡能量条。
|
||||
- 尊重 reduced motion,非必要动画可降级。
|
||||
|
||||
## 测试映射
|
||||
|
||||
| BDD 场景 | 测试层级 | 目标文件 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 玩家允许麦克风权限后进入准备倒计时 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家拒绝麦克风权限 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家发出一次有效狗叫 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 玩家连续大声狗叫压制对手 | unit/integration | `EnergyTugOfWar.test.ts`, `BarkBattleController.test.ts` | planned |
|
||||
| 环境噪音低于阈值不计入叫声 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 倒计时结束时玩家侧占优 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 倒计时结束时双方接近平衡 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 当前浏览器不支持麦克风 API | component | `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 移动端进入对战页面 | visual/smoke | Playwright 或人工 playtest 清单 | planned |
|
||||
|
||||
## 验证命令建议
|
||||
|
||||
具体命令以后续实际落地位置为准,建议包括:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.ts
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
若接入 Genarrative 后端或玩法配置,还需要追加:
|
||||
|
||||
```bash
|
||||
cd server-rs && cargo check -p api-server -p shared-contracts --no-default-features
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
|
||||
```
|
||||
|
||||
## 实施阶段拆分
|
||||
|
||||
### Phase 0:产品与技术定稿
|
||||
|
||||
- 确认玩法 ID:`bark-battle`。
|
||||
- 确认 MVP 只做单机玩家 vs AI,不做实时多人。
|
||||
- 确认是否只做 runtime 原型,还是接入 Genarrative 创作入口。
|
||||
- 确认是否允许浏览器麦克风权限作为核心输入。
|
||||
|
||||
### Phase 1:纯领域模型
|
||||
|
||||
- 建立 bark-battle domain。
|
||||
- 按 TDD 写 `BarkDetector`、`EnergyTugOfWar`、`BarkBattleSession` 测试。
|
||||
- 实现最小规则让测试通过。
|
||||
|
||||
### Phase 2:麦克风输入适配
|
||||
|
||||
- 封装 Web Audio API。
|
||||
- 支持权限请求、权限失败、环境噪音校准。
|
||||
- 使用 mock input 完成自动化测试,真实麦克风做 smoke。
|
||||
|
||||
### Phase 3:Phaser 2D runtime
|
||||
|
||||
- 新建 Phaser Scene。
|
||||
- 绘制或占位加载公园背景、左右狗狗、声浪特效。
|
||||
- Scene 只消费 snapshot,不写规则。
|
||||
- 接入 DOM HUD。
|
||||
|
||||
### Phase 4:反馈与结算
|
||||
|
||||
- 加入拟声词、冲击波、狗狗张嘴动画。
|
||||
- 加入结算面板。
|
||||
- 加入再来一局与返回入口。
|
||||
|
||||
### Phase 5:Genarrative 集成可选项
|
||||
|
||||
若要正式接入玩法类型:
|
||||
|
||||
- 补 `shared-contracts` 中 bark-battle runtime/result DTO。
|
||||
- 补前端 service 与 runtime shell。
|
||||
- 补入口配置数据库 seed。
|
||||
- 补作品架 / 发布 / 广场链路,若需要持久化成绩或作品。
|
||||
- 按 `genarrative-play-type-integration` 执行完整闭环验证。
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
### 麦克风权限风险
|
||||
|
||||
浏览器麦克风权限受 HTTPS、浏览器策略、用户设置影响。MVP 需要明确:
|
||||
|
||||
- 本地开发可在 localhost 使用。
|
||||
- 线上必须 HTTPS。
|
||||
- 权限拒绝需要可恢复。
|
||||
|
||||
### 声音识别准确性风险
|
||||
|
||||
MVP 不建议做复杂“是否真的是狗叫”的 AI 识别,否则实现成本高、误判多。建议先用:
|
||||
|
||||
- 音量阈值
|
||||
- 峰值次数
|
||||
- 节奏间隔
|
||||
- 环境噪音校准
|
||||
|
||||
后续再考虑加入频谱特征或 ML 分类。
|
||||
|
||||
### 噪音作弊风险
|
||||
|
||||
玩家可以喊叫、拍桌子或播放音频。若是娱乐派对玩法可以接受;若要竞技公平,需要后续加入:
|
||||
|
||||
- 频谱特征
|
||||
- 输入冷却
|
||||
- 异常持续噪音削弱
|
||||
- 本地/服务端反作弊策略
|
||||
|
||||
### 移动端兼容风险
|
||||
|
||||
移动端 Web Audio 可能需要用户手势激活 AudioContext。计划中需把“开始”按钮作为显式用户手势,避免自动启动失败。
|
||||
|
||||
### UI 遮挡风险
|
||||
|
||||
视频原型中的核心可读信息非常少:倒计时、能量条、狗狗、拟声词。实现时应避免把说明文案、复杂面板长期铺在画面上。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. MVP 是“玩家 vs AI”,还是需要从第一版开始支持双人同屏 / 联机?
|
||||
2. 是否要作为 Genarrative 新玩法入口完整接入,还是先做独立 runtime 原型?
|
||||
3. 是否需要记录成绩、发布作品、进入作品架和广场?
|
||||
4. 狗狗与背景素材是使用临时占位、AI 生成,还是需要复用项目既有素材系统?
|
||||
5. 是否允许游戏强依赖麦克风权限,还是必须提供键盘备用输入?
|
||||
|
||||
## 推荐下一步
|
||||
|
||||
建议下一步先执行 Phase 0 + Phase 1:
|
||||
|
||||
1. 明确 MVP 边界:单机玩家 vs AI。
|
||||
2. 写 `BarkDetector` / `EnergyTugOfWar` / `BarkBattleSession` 的 BDD 对应单元测试。
|
||||
3. 不接 Phaser、不接麦克风,先把核心规则用 TDD 跑通。
|
||||
4. 规则稳定后再接 Web Audio 与 Phaser runtime。
|
||||
@@ -0,0 +1,709 @@
|
||||
# bark-battle 三阶段实施计划:浏览器原型 → AI 创作入口 → 数据库落地
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 按“三阶段”推进 `bark-battle / 汪汪声浪大作战`:第一阶段先做纯浏览器可运行游戏原型并验证玩法跑通;第二阶段接入 Genarrative 创作入口,用 AI 生成可试玩内容;第三阶段再打通后端数据库、发布、成绩和作品闭环。
|
||||
|
||||
**Architecture:** 第一阶段只在前端 runtime 内闭环,优先落 `src/games/bark-battle/` 与直达路由,不依赖后端和 SpacetimeDB。第二阶段在已有创作入口、Agent flow controller、结果页和 runtime shell 上接入 `bark-battle`,AI 只生成配置化草稿,不承接正式业务真相。第三阶段按 `server-rs + Axum + SpacetimeDB` DDD 分层落库,前端只展示后端投影和调用后端 API。
|
||||
|
||||
**Tech Stack:** React 19、TypeScript、Vite、Vitest、Testing Library;第一阶段优先 DOM/Canvas 原型,可在验证玩法后再引入 Phaser 3;后端阶段使用 `server-rs`、Axum、SpacetimeDB、shared-contracts。
|
||||
|
||||
---
|
||||
|
||||
## 0. 当前上下文 / 假设
|
||||
|
||||
- 现有需求与技术文档:
|
||||
- `docs/prd/BARK_BATTLE_BDD_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- 用户明确要求阶段顺序:
|
||||
1. 第一阶段:先制作纯浏览器运行的游戏原型,需要测试游戏功能是否能跑通。
|
||||
2. 第二阶段:打通创作入口,使用 AI 赋能游戏内容创作。
|
||||
3. 第三阶段:最后打通数据库落地。
|
||||
- 因此本计划调整原技术方案中的落地优先级:
|
||||
- 第一阶段不新增后端表、不接发布、不接作品架。
|
||||
- 第一阶段可以用 mock / local draft 配置与直达路由 `/bark-battle` 完成 playable prototype。
|
||||
- 第一阶段若 Phaser 依赖尚未安装,优先用 React DOM + Canvas/CSS 2D 原型跑通功能;待核心规则验证后再决定是否引入 Phaser,避免第一阶段被依赖安装和素材管线阻塞。
|
||||
- 当前仓库 `package.json` 还没有 `phaser` 依赖;如实现者选择 Phaser,需要单独评估依赖引入、包体和测试影响。
|
||||
- 本计划只写计划,不直接实现代码。
|
||||
|
||||
## 1. 总体分阶段验收口径
|
||||
|
||||
### Phase 1:纯浏览器游戏原型
|
||||
|
||||
目标:打开本地前端路由即可玩到一局 `bark-battle`,并通过自动测试确认核心规则跑通。
|
||||
|
||||
必须满足:
|
||||
- 可从 `/bark-battle` 进入独立原型页面。
|
||||
- 不登录、不请求后端、不依赖数据库。
|
||||
- 支持开发 mock input:点击/按键/按钮可模拟音量峰值;有真实麦克风时可走 Web Audio。
|
||||
- 能完成:权限/开始 → 校准或 mock 准备 → 倒计时 → 30 秒 playing → 结算 → 再来一局。
|
||||
- 低于阈值输入不计数;有效叫声计数;能量条向玩家或对手移动;结算胜/负/平。
|
||||
- 移动端至少能看到能量条、倒计时、双方狗狗、主要按钮和结算。
|
||||
|
||||
### Phase 2:AI 创作入口
|
||||
|
||||
目标:创作者能从创作中心选择 `bark-battle`,用 AI 生成玩法配置草稿,并进入结果页试玩。
|
||||
|
||||
必须满足:
|
||||
- 后端入口配置中出现 `bark-battle`,按开关展示/可点击。
|
||||
- 前端类型分流、SelectionStage、工作台、结果页、runtime 入口齐全。
|
||||
- AI 生成内容仅限配置化草稿:标题、主题、狗狗外观描述、背景风格、难度、局长、AI 对手参数、提示文案 key 等。
|
||||
- 生成结果可在本地 runtime 中试玩。
|
||||
- 未落库前可先用 session/local state 保存草稿,但要清楚标识为“未发布草稿”。
|
||||
|
||||
### Phase 3:数据库落地与正式作品闭环
|
||||
|
||||
目标:`bark-battle` 草稿、发布态配置、runtime start/finish、成绩和作品级游玩埋点都进入后端 DDD / SpacetimeDB 链路。
|
||||
|
||||
必须满足:
|
||||
- `shared-contracts`、`module-bark-battle`、`spacetime-module`、`spacetime-client`、`api-server` 分层清晰。
|
||||
- 发布为稳定作品 ID,runtime 从后端读取发布态 config。
|
||||
- start 成功写 `work_play_start`:`scope_kind=work`、`scope_id=稳定作品 ID`、metadata 包含 `playType/workId/sourceRoute/userId`。
|
||||
- finish 只上传派生指标,不保存原始麦克风音频、波形或可还原语音内容。
|
||||
- 作品架/广场/分享/排行榜如启用,均来自后端投影。
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 1:纯浏览器运行游戏原型
|
||||
|
||||
### Task 1.1:补齐阶段边界文档
|
||||
|
||||
**Objective:** 在现有技术方案中明确“先浏览器原型,后 AI 创作,最后数据库”的落地顺序,避免实现时过早接后端。
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- Modify: `docs/prd/BARK_BATTLE_BDD_2026-05-11.md`
|
||||
|
||||
**Steps:**
|
||||
1. 在 runtime 技术方案中新增“三阶段落地顺序”小节。
|
||||
2. 明确 Phase 1 不接后端、不接数据库、不接创作入口事实源。
|
||||
3. 在 BDD 中补充“浏览器原型 smoke”验收场景。
|
||||
4. 运行:
|
||||
```bash
|
||||
npm run check:encoding -- docs/prd/BARK_BATTLE_BDD_2026-05-11.md docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
git diff --check
|
||||
```
|
||||
|
||||
**Expected:** 编码检查和 diff 空白检查通过。
|
||||
|
||||
### Task 1.2:建立 Phase 1 目录骨架和类型
|
||||
|
||||
**Objective:** 建立不依赖 React/DOM/Web Audio 的核心类型,后续所有测试和 UI 都围绕这些类型。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleTypes.ts`
|
||||
- Create: `src/games/bark-battle/application/BarkBattleConfig.ts`
|
||||
|
||||
**Key design:**
|
||||
- `BarkBattlePhase = 'permission' | 'calibration' | 'countdown' | 'playing' | 'finished' | 'unavailable'`
|
||||
- `MicrophoneFailureReason` 覆盖已有文档中的 9 类失败原因。
|
||||
- `BarkBattleSnapshot` 包含 `phase/uiState/errorReason/statusMessageKey/remainingMs/energy/player/opponent/winner/result/lastEvents`。
|
||||
- `BarkBattleConfig` 包含 `roundDurationMs/drawThreshold/minBarkGapMs/minBarkDurationMs/maxBarkDurationMs/balanceFactor/calibrationMaxWaitMs`。
|
||||
|
||||
**Tests:**
|
||||
- 本任务可先不写运行时逻辑,但需要让 typecheck 能引用这些类型。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run check:encoding -- src/games/bark-battle/domain/BarkBattleTypes.ts src/games/bark-battle/application/BarkBattleConfig.ts
|
||||
```
|
||||
|
||||
### Task 1.3:TDD 实现叫声检测 BarkDetector
|
||||
|
||||
**Objective:** 用纯函数/纯类把音频样本转换为有效叫声事件。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkDetector.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkDetector.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 超过阈值、持续时长合规、间隔足够时计为一次有效叫声。
|
||||
2. 持续噪音不在每个 tick 无限计数。
|
||||
3. 低于阈值的背景噪音不计数。
|
||||
4. `minBarkGapMs` 内连续峰值不重复计数。
|
||||
5. 过短脉冲不计数;过长持续声削弱为单段输入。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.4:TDD 实现能量条 EnergyTugOfWar
|
||||
|
||||
**Objective:** 验证玩家/对手推动力能稳定改变 `energy`,并 clamp 到 `-100..100`。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/EnergyTugOfWar.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/EnergyTugOfWar.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 玩家 power 高于对手时 `energy` 增加。
|
||||
2. 对手 power 高于玩家时 `energy` 减少。
|
||||
3. energy 不超过 `100`。
|
||||
4. energy 不低于 `-100`。
|
||||
5. power 相等时变化不超过浮点误差。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/EnergyTugOfWar.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.5:TDD 实现单局状态机 BarkBattleSession
|
||||
|
||||
**Objective:** 跑通 permission/calibration/countdown/playing/finished/unavailable 状态流转和结算。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleSession.ts`
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleScoring.ts`
|
||||
- Create: `src/games/bark-battle/domain/OpponentStrategy.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkBattleSession.test.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkBattleScoring.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 校准完成后进入 countdown。
|
||||
2. countdown 结束后进入 playing。
|
||||
3. playing 中 `remainingMs` 随 tick 递减。
|
||||
4. `remainingMs <= 0` 后进入 finished。
|
||||
5. `energy > drawThreshold` 判定玩家胜。
|
||||
6. `energy < -drawThreshold` 判定对手胜。
|
||||
7. `abs(energy) <= drawThreshold` 判定平局。
|
||||
8. finished 后新输入不再改变本局计数和能量。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/BarkBattleSession.test.ts src/games/bark-battle/domain/__tests__/BarkBattleScoring.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.6:实现 mock-first Application Controller
|
||||
|
||||
**Objective:** 不依赖真实麦克风,先用 mock audio sample 驱动完整 snapshot。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/application/BarkBattleController.ts`
|
||||
- Create: `src/games/bark-battle/application/BarkBattleSnapshotStore.ts`
|
||||
- Create: `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- `startWithMockInput()` 进入校准完成或直接 countdown。
|
||||
- `submitMockSample(sample)` 更新玩家输入。
|
||||
- `tick(deltaMs)` 推进对手、能量条、倒计时。
|
||||
- `restart()` 重置状态。
|
||||
- `failMicrophone(reason)` 进入 `phase: 'unavailable'`,并设置 `errorReason/statusMessageKey`。
|
||||
|
||||
**Test cases:**
|
||||
1. mock start 后能进入 countdown/playing。
|
||||
2. 提交 mock 峰值后 bark count 增加。
|
||||
3. tick 后 energy 可变化。
|
||||
4. finish 后生成 result。
|
||||
5. `failMicrophone('permission-denied')` 不进入 playing。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/application/__tests__/BarkBattleController.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.7:实现浏览器原型 UI Shell(不接平台)
|
||||
|
||||
**Objective:** 提供 `/bark-battle` 可访问的 playable prototype。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/BarkBattlePlaygroundApp.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleHud.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleResultPanel.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleHud.css`
|
||||
- Modify: `src/routing/appRoutes.tsx`
|
||||
|
||||
**Behavior:**
|
||||
- 新增路由匹配:`/bark-battle`。
|
||||
- 首屏只有清爽开始面板,不常驻大段规则。
|
||||
- 提供开发原型按钮:开始、模拟叫声、模拟对手增强、再来一局。
|
||||
- playing 画面展示:顶部能量条、倒计时、玩家/对手狗狗、叫声次数、麦克风/mock 状态。
|
||||
- 结算面板独立居中,不追加在当前面板下方。
|
||||
|
||||
**UI constraints:**
|
||||
- 移动端优先。
|
||||
- 正常 playing 阶段不在 playfield 常驻规则说明。
|
||||
- 大动效不遮挡顶部能量条和倒计时。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 访问 /bark-battle → 开始 → 模拟叫声 → energy 变化 → 结算 → 再来一局
|
||||
```
|
||||
|
||||
### Task 1.8:实现 HUD 组件测试
|
||||
|
||||
**Objective:** 自动验证核心 UI 状态,不只依赖人工试玩。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/ui/__tests__/BarkBattleHud.test.tsx`
|
||||
- Create: `src/games/bark-battle/ui/__tests__/BarkBattleResultPanel.test.tsx`
|
||||
|
||||
**Test cases:**
|
||||
1. playing 阶段展示倒计时和能量条。
|
||||
2. energy 正值时玩家侧占比更大。
|
||||
3. energy 负值时对手侧占比更大。
|
||||
4. permission-denied 展示重试授权入口。
|
||||
5. unsupported 不展示开始声控按钮。
|
||||
6. finished 展示胜负、叫声次数、再来一局。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/__tests__/BarkBattleHud.test.tsx src/games/bark-battle/ui/__tests__/BarkBattleResultPanel.test.tsx
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.9:接入真实 Web Audio(可晚于 mock 原型)
|
||||
|
||||
**Objective:** 在支持麦克风的浏览器中真实采样并驱动 controller,同时保留 mock fallback 便于测试。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/infrastructure/BrowserMicrophoneInput.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/AudioAnalyserSampler.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/MicrophonePermission.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/__tests__/AudioAnalyserSampler.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 用户点击开始后才请求麦克风。
|
||||
- 用户手势后创建/resume `AudioContext`。
|
||||
- 输出归一化 `BarkAudioSample`。
|
||||
- 捕获并映射:unsupported、permission-denied、non-secure-context、not-found、not-readable、audio-context-blocked、unknown。
|
||||
- stop/restart/page unload 时停止 tracks。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts src/games/bark-battle/infrastructure/__tests__/AudioAnalyserSampler.test.ts
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 真实麦克风授权 → 校准 → 发声 → 结算
|
||||
```
|
||||
|
||||
### Task 1.10:Phase 1 收口验证
|
||||
|
||||
**Objective:** 确认“纯浏览器原型”已经可以交给产品/测试试玩。
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/**/*.test.ts src/games/bark-battle/application/**/*.test.ts src/games/bark-battle/infrastructure/**/*.test.ts src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
npm run dev:web
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] `/bark-battle` 能打开。
|
||||
- [ ] mock 模式可完整完成一局。
|
||||
- [ ] 真实麦克风模式可授权、校准、发声、结算。
|
||||
- [ ] 拒绝权限后不会进入 playing。
|
||||
- [ ] 移动端窄屏能看到核心信息并能点击主要按钮。
|
||||
- [ ] 再来一局不会继承上一局 energy/barkCount/result。
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2:打通创作入口,用 AI 赋能内容创作
|
||||
|
||||
### Task 2.1:定义 `bark-battle` 草稿契约(前端本地版)
|
||||
|
||||
**Objective:** 在接后端前,先定义 AI 可生成的 runtime draft shape。
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/shared/src/contracts/barkBattle.ts`(临时前端共享类型,后端阶段再对齐 Rust shared-contracts)
|
||||
- Create: `src/services/bark-battle-creation/barkBattleDraftDefaults.ts`
|
||||
- Create: `src/services/bark-battle-creation/barkBattleDraftValidation.ts`
|
||||
|
||||
**Draft fields:**
|
||||
- `title`
|
||||
- `description`
|
||||
- `themePrompt`
|
||||
- `playerDogName`
|
||||
- `opponentDogName`
|
||||
- `backgroundStyle`
|
||||
- `difficulty`
|
||||
- `roundDurationMs`
|
||||
- `drawThreshold`
|
||||
- `opponentConfig`
|
||||
- `audioSensitivityPreset`
|
||||
- `visualStyle`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.2:新增创作入口配置
|
||||
|
||||
**Objective:** 让 `bark-battle` 出现在创作中心入口中,但可通过后端入口配置开关控制。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
|
||||
- `server-rs/crates/module-runtime/src/domain.rs`
|
||||
- `server-rs/crates/module-runtime/src/application.rs`
|
||||
- `server-rs/crates/shared-contracts/src/creation_entry_config.rs`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.test.ts`
|
||||
|
||||
**Plan:**
|
||||
1. 在入口 seed 中新增 `bark-battle`,首轮可设:`visible: true`、`open: true`(若需要灰度则 `open: false`)。
|
||||
2. 前端展示派生只消费 API 返回,不恢复旧静态入口事实源。
|
||||
3. 更新排序和锁定态测试。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
|
||||
npm run typecheck
|
||||
cd server-rs && cargo check -p api-server -p spacetime-module --no-default-features
|
||||
```
|
||||
|
||||
### Task 2.3:扩展 SelectionStage 与流程分流
|
||||
|
||||
**Objective:** 点击 `bark-battle` 入口后进入对应创作工作台。
|
||||
|
||||
**Files likely to change:**
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`(如复用通用 agent flow)
|
||||
|
||||
**Stages:**
|
||||
- `bark-battle-agent-workspace`
|
||||
- `bark-battle-generating`(可选)
|
||||
- `bark-battle-result`
|
||||
- `bark-battle-runtime`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/**/*.test.tsx src/components/platform-entry/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.4:实现 AI 创作工作台
|
||||
|
||||
**Objective:** 用对话式或表单式输入生成 `BarkBattleDraft`。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-creation/BarkBattleAgentWorkspace.tsx`
|
||||
- Create: `src/services/bark-battle-creation/barkBattleCreationClient.ts`
|
||||
- Create: `src/services/bark-battle-creation/barkBattlePromptBuilder.ts`
|
||||
- Create: `src/services/bark-battle-creation/__tests__/barkBattlePromptBuilder.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 用户输入一句主题,例如“柴犬在赛博公园比谁叫得响”。
|
||||
- AI 返回结构化草稿。
|
||||
- 前端校验并填默认值,不让非法 roundDuration/difficulty 进入 runtime。
|
||||
- 错误时保留用户输入和已生成草稿。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.5:实现结果页与试玩入口
|
||||
|
||||
**Objective:** AI 草稿生成后可查看、返回编辑、进入 Phase 1 runtime 试玩。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- Create: `src/components/bark-battle-result/BarkBattleResultView.test.tsx`
|
||||
- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`(允许传入 draft config)
|
||||
|
||||
**Behavior:**
|
||||
- 展示标题、主题、狗狗名、背景风格、难度、局长。
|
||||
- 提供“返回编辑”“试玩”按钮。
|
||||
- 暂不展示发布按钮,或发布按钮显示为后端阶段能力。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/components/bark-battle-result/BarkBattleResultView.test.tsx
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 创作入口 → AI 草稿 → 结果页 → 试玩 → 返回编辑
|
||||
```
|
||||
|
||||
### Task 2.6:Phase 2 收口验证
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/bark-battle-result/BarkBattleResultView.test.tsx src/services/bark-battle-creation/**/*.test.ts src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] 创作中心展示 `bark-battle`。
|
||||
- [ ] 点击入口进入工作台。
|
||||
- [ ] AI 可生成草稿。
|
||||
- [ ] 草稿结果页可展示并返回编辑。
|
||||
- [ ] 试玩使用草稿配置影响 runtime 表现。
|
||||
- [ ] 未接数据库前不会假装发布成功。
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 3:数据库落地与正式作品闭环
|
||||
|
||||
### Task 3.1:补齐 shared contracts
|
||||
|
||||
**Objective:** 前后端共享 bark-battle DTO,避免前端手写正式契约漂移。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
- `server-rs/crates/shared-contracts/src/lib.rs`
|
||||
- `packages/shared/src/contracts/barkBattle.ts`
|
||||
|
||||
**DTO:**
|
||||
- `BarkBattleDraft`
|
||||
- `BarkBattlePublishedConfig`
|
||||
- `CreateBarkBattleSessionRequest/Response`
|
||||
- `BarkBattleRuntimeStartRequest/Response`
|
||||
- `BarkBattleRuntimeFinishRequest/Response`
|
||||
- `BarkBattleRunResult`
|
||||
- `BarkBattleScoreSummary`
|
||||
- `BarkBattleLeaderboardEntry`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run typecheck
|
||||
cd server-rs && cargo check -p shared-contracts --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.2:新增 `module-bark-battle` 纯领域模块
|
||||
|
||||
**Objective:** 后端正式分数、配置校验、提交合法性不写在 api-server handler 里。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/module-bark-battle/`
|
||||
- Modify: `server-rs/Cargo.toml`
|
||||
|
||||
**Responsibilities:**
|
||||
- 配置合法性校验。
|
||||
- run start/finish 状态约束。
|
||||
- 派生指标范围校验。
|
||||
- 分数与排行榜排序分计算。
|
||||
- 不接 Axum、不接 SpacetimeDB、不接 HTTP。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
cd server-rs && cargo test -p module-bark-battle --no-default-features
|
||||
cd server-rs && cargo check -p module-bark-battle --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.3:SpacetimeDB 表、reducer、migration
|
||||
|
||||
**Objective:** 保存草稿、发布态配置、run、result、leaderboard 投影。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-module/src/runtime/bark_battle.rs`(或按现有模块目录命名)
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- `server-rs/crates/spacetime-module/src/lib.rs`
|
||||
- 生成绑定目录(通过命令生成,不手改生成物)
|
||||
|
||||
**Tables draft:**
|
||||
- `bark_battle_draft`
|
||||
- `bark_battle_published_config`
|
||||
- `bark_battle_run`
|
||||
- `bark_battle_run_result`
|
||||
- `bark_battle_leaderboard_entry`
|
||||
|
||||
**Reducers/procedures:**
|
||||
- `create_bark_battle_draft`
|
||||
- `publish_bark_battle_config`
|
||||
- `start_bark_battle_run`
|
||||
- `finish_bark_battle_run`
|
||||
- `list_bark_battle_leaderboard`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
cd server-rs && cargo check -p spacetime-module --no-default-features
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
### Task 3.4:spacetime-client facade
|
||||
|
||||
**Objective:** api-server 通过 facade 调用 SpacetimeDB,不直接散落 reducer 细节。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- `server-rs/crates/spacetime-client/src/lib.rs`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
cd server-rs && cargo check -p spacetime-client --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.5:api-server BFF 路由
|
||||
|
||||
**Objective:** 提供创作、发布态 runtime start/finish、leaderboard API。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/api-server/src/bark_battle.rs`
|
||||
- `server-rs/crates/api-server/src/main.rs` 或路由注册文件
|
||||
|
||||
**Routes draft:**
|
||||
- `POST /api/bark-battle/sessions`
|
||||
- `GET /api/bark-battle/sessions/:sessionId`
|
||||
- `POST /api/bark-battle/runtime/start`
|
||||
- `POST /api/bark-battle/runtime/finish`
|
||||
- `GET /api/bark-battle/works/:workId/runtime-config`
|
||||
- `GET /api/bark-battle/works/:workId/leaderboard`
|
||||
|
||||
**Tracking:**
|
||||
- runtime start 成功后主动写 `work_play_start`。
|
||||
- `scope_kind=work`。
|
||||
- `scope_id=稳定作品 ID`。
|
||||
- metadata 包含 `playType=bark-battle`、`workId`、`sourceRoute`、`userId`。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run api-server
|
||||
# 另一个终端检查 /healthz,并执行对应 API smoke
|
||||
cd server-rs && cargo check -p api-server --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.6:前端正式 client 与 runtime 切换
|
||||
|
||||
**Objective:** runtime 从本地草稿模式升级为可读取后端发布态 config,并提交正式派生结果。
|
||||
|
||||
**Files likely to change:**
|
||||
- Create: `src/services/bark-battle-runtime/barkBattleRuntimeClient.ts`
|
||||
- Create: `src/services/bark-battle-works/barkBattleWorksClient.ts`
|
||||
- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- Modify: `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- Modify: `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- Modify: `src/services/publicWorkCode.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 本地 preview 仍可使用 draft config。
|
||||
- 正式作品 runtime 必须先调用 start API,拿 run token/session。
|
||||
- finish 只提交派生 metrics。
|
||||
- 发布后刷新作品架/广场。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/services/bark-battle-runtime/**/*.test.ts src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
### Task 3.7:Phase 3 收口验证
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx src/services/bark-battle-runtime/**/*.test.ts src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
npm run check:server-rs-ddd
|
||||
cd server-rs && cargo test -p module-bark-battle --no-default-features
|
||||
cd server-rs && cargo check -p api-server -p spacetime-module -p spacetime-client -p shared-contracts --no-default-features
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] 创作者生成并发布 bark-battle 作品。
|
||||
- [ ] 玩家从作品页进入 runtime。
|
||||
- [ ] start API 成功并写 `work_play_start`。
|
||||
- [ ] 浏览器本地完成一局。
|
||||
- [ ] finish API 只上传派生指标。
|
||||
- [ ] 成绩/排行榜/作品架刷新来自后端投影。
|
||||
- [ ] 拒绝麦克风权限时不会创建非法 finished result。
|
||||
|
||||
---
|
||||
|
||||
## 5. 文件清单总览
|
||||
|
||||
### Phase 1 likely files
|
||||
|
||||
- `src/routing/appRoutes.tsx`
|
||||
- `src/BarkBattlePlaygroundApp.tsx`
|
||||
- `src/games/bark-battle/domain/BarkBattleTypes.ts`
|
||||
- `src/games/bark-battle/domain/BarkDetector.ts`
|
||||
- `src/games/bark-battle/domain/EnergyTugOfWar.ts`
|
||||
- `src/games/bark-battle/domain/BarkBattleSession.ts`
|
||||
- `src/games/bark-battle/domain/BarkBattleScoring.ts`
|
||||
- `src/games/bark-battle/domain/OpponentStrategy.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleConfig.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleController.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleSnapshotStore.ts`
|
||||
- `src/games/bark-battle/infrastructure/BrowserMicrophoneInput.ts`
|
||||
- `src/games/bark-battle/infrastructure/AudioAnalyserSampler.ts`
|
||||
- `src/games/bark-battle/infrastructure/MicrophonePermission.ts`
|
||||
- `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleHud.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleResultPanel.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleHud.css`
|
||||
|
||||
### Phase 2 likely files
|
||||
|
||||
- `packages/shared/src/contracts/barkBattle.ts`
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
- `src/components/bark-battle-creation/BarkBattleAgentWorkspace.tsx`
|
||||
- `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- `src/services/bark-battle-creation/*`
|
||||
|
||||
### Phase 3 likely files
|
||||
|
||||
- `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
- `server-rs/crates/module-bark-battle/*`
|
||||
- `server-rs/crates/spacetime-module/src/runtime/bark_battle.rs`
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- `server-rs/crates/api-server/src/bark_battle.rs`
|
||||
- `src/services/bark-battle-runtime/*`
|
||||
- `src/services/bark-battle-works/*`
|
||||
- `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- `src/services/publicWorkCode.ts`
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险、取舍与开放问题
|
||||
|
||||
### 风险
|
||||
|
||||
1. **麦克风权限和移动端 AudioContext 差异大。** 需要 mock input 保底,否则自动化和本地开发会被真实设备阻塞。
|
||||
2. **第一阶段过早引入 Phaser 可能拖慢验证。** 当前仓库没有 `phaser` 依赖;建议先用 DOM/Canvas 跑通玩法,再决定是否引入 Phaser。
|
||||
3. **AI 草稿和正式发布配置容易漂移。** Phase 2 临时 TS 类型必须在 Phase 3 与 Rust shared-contracts 对齐。
|
||||
4. **不能保存原始音频。** 后端阶段只能保存派生指标,任何音频片段、波形、频谱明细都不应落库。
|
||||
5. **入口配置事实源在后端/SpacetimeDB。** Phase 2 接入口时不要恢复旧前端静态入口配置。
|
||||
|
||||
### 取舍
|
||||
|
||||
- Phase 1 先把“游戏是否好玩、功能是否跑通”作为第一目标,不追求正式作品闭环。
|
||||
- Phase 2 让 AI 生成内容配置,而不是让 AI 直接生成任意代码或不受控规则。
|
||||
- Phase 3 再把正式业务真相交给后端,避免前端 runtime 先背上发布、成绩、排行榜的复杂度。
|
||||
|
||||
### 开放问题
|
||||
|
||||
1. Phase 1 是否必须使用 Phaser?如果只是验证玩法,可先使用 DOM/CSS/Canvas 原型,后续再替换 renderer。
|
||||
2. `bark-battle` 的正式中文名是否固定为“汪汪声浪大作战”?如果名称要改,需先统一文档、入口配置和分享标题。
|
||||
3. AI 创作阶段是否需要生成图片/狗狗视觉资产,还是只生成风格描述和使用占位素材?
|
||||
4. 是否需要排行榜作为 Phase 3 必选,还是作为数据库落地后的增强项?
|
||||
5. 真实麦克风 smoke 需要哪些目标设备:Chrome 桌面、Android Chrome、iOS Safari 是否都纳入首批验收?
|
||||
|
||||
---
|
||||
|
||||
## 7. 建议执行方式
|
||||
|
||||
1. 先按 Phase 1 执行,且每个 domain/application task 坚持 TDD:先失败测试,再实现。
|
||||
2. Phase 1 合并前不要接数据库,不要新增后端表,不要把入口配置切到 open。
|
||||
3. Phase 1 验证通过后,让产品/团队试玩 `/bark-battle`,确认玩法数值和 UI 方向。
|
||||
4. 再进入 Phase 2,把 AI 创作工作台接到同一个 runtime draft config。
|
||||
5. 最后进入 Phase 3,按后端 DDD 文档做数据库、发布、成绩和追踪闭环。
|
||||
|
||||
310
.hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md
Normal file
310
.hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# K6 作品列表压测计划(使用 spacetime-migration-7.json 作为数据源)
|
||||
|
||||
## 目标
|
||||
|
||||
使用 K6 对 Genarrative 的“作品列表”相关接口进行压测,并将用户提供的 `spacetime-migration-7.json` 作为压测数据源;数据处理时**只导入作品列表相关数据**,不导入用户、会话、钱包、埋点、运行存档等非作品表,避免把敏感或无关数据带入压测环境。
|
||||
|
||||
## 当前上下文
|
||||
|
||||
- 工作区:`/c/proj/Genarrative`
|
||||
- 原始迁移文件:`C:\Users\DSK\AppData\Local\hermes\cache\documents\doc_150e84029b2d_spacetime-migration-7.json`
|
||||
- 已确认原始迁移文件结构:
|
||||
- `schema_version = 1`
|
||||
- `tables = 53`
|
||||
- 作品相关表中当前有数据的重点表:
|
||||
- `puzzle_work_profile`:80 行
|
||||
- `custom_world_profile`:1 行
|
||||
- `match3d_work_profile`:0 行
|
||||
- `big_fish_*`:当前样本中相关表为 0 行
|
||||
- 原始文件还包含 `user_account`、`auth_identity`、`refresh_session`、`profile_wallet_ledger`、`asset_object`、运行记录等数据,压测导入时必须过滤。
|
||||
- 当前仓库未发现现成 K6 脚本或 `k6` 相关文件,需要新增压测脚本与数据提取脚本。
|
||||
- `package.json` 当前有 `dev/dev:rust/test/check` 等脚本,未发现 K6 npm script。
|
||||
|
||||
## 范围约束
|
||||
|
||||
### 本次只导入/使用
|
||||
|
||||
1. 作品列表表:
|
||||
- `puzzle_work_profile`
|
||||
- `custom_world_profile`
|
||||
- 后续若接口覆盖其他玩法,可扩展:
|
||||
- `match3d_work_profile`
|
||||
- `square_hole_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
- `big_fish_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
- `visual_novel_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
2. 为作品列表卡片展示所需的最小字段:
|
||||
- 稳定 ID:`profile_id`、`work_id` 或 `public_work_code`
|
||||
- 标题:`work_title` / `level_name` / `world_name`
|
||||
- 描述:`work_description` / `summary` / `summary_text` / `subtitle`
|
||||
- 作者:`owner_user_id`、`author_display_name`、`author_public_user_code`
|
||||
- 封面:`cover_image_src`、`cover_asset_id`(如果接口只返回 asset id,则压测阶段不额外导入二进制 asset)
|
||||
- 状态与计数:`publication_status`、`published_at`、`play_count`、`like_count`、`remix_count`
|
||||
- 作品内容摘要:`levels_json`、`profile_payload_json`、`theme_tags_json` 等列表渲染或进入作品详情可能需要的 JSON 字段
|
||||
|
||||
### 本次不导入/不使用
|
||||
|
||||
- 认证与账号:`user_account`、`auth_identity`、`refresh_session`、`auth_store_snapshot`
|
||||
- 用户资产与钱包:`profile_wallet_ledger`、`profile_dashboard_state`、`profile_redeem_*`、`profile_invite_*`
|
||||
- 游玩历史/存档/运行态:`profile_played_world`、`public_work_play_daily_stat`、`puzzle_runtime_run`、`profile_save_archive`、`runtime_snapshot` 等
|
||||
- AI 任务过程:`ai_task`、`ai_task_stage`、`ai_text_chunk`
|
||||
- asset 二进制与绑定:`asset_object`、`asset_entity_binding`,除非后续确认作品列表接口强依赖它们;即便需要,也只导入作品列表封面所需的最小 metadata,不导入原始大对象。
|
||||
|
||||
## 推荐目录与文件
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
.hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md # 本计划
|
||||
scripts/loadtest/extract-works-list-data.mjs # 从迁移文件提取作品列表数据
|
||||
scripts/loadtest/k6-works-list.js # K6 压测脚本
|
||||
scripts/loadtest/data/works-list.sample.json # 过滤后的样例数据(不要提交敏感原始迁移全量)
|
||||
scripts/loadtest/README.md # 执行说明与指标阈值
|
||||
```
|
||||
|
||||
可选新增 npm scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"loadtest:extract-works": "node scripts/loadtest/extract-works-list-data.mjs",
|
||||
"loadtest:k6:works": "k6 run scripts/loadtest/k6-works-list.js"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据提取方案
|
||||
|
||||
### 输入
|
||||
|
||||
默认读取:
|
||||
|
||||
```bash
|
||||
node scripts/loadtest/extract-works-list-data.mjs \
|
||||
--input "C:/Users/DSK/AppData/Local/hermes/cache/documents/doc_150e84029b2d_spacetime-migration-7.json" \
|
||||
--output scripts/loadtest/data/works-list.local.json
|
||||
```
|
||||
|
||||
### 输出结构
|
||||
|
||||
建议输出为 K6 直接可读的 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "spacetime-migration-7.json",
|
||||
"generatedAt": "<iso datetime>",
|
||||
"tables": {
|
||||
"puzzle_work_profile": [
|
||||
{
|
||||
"profile_id": "...",
|
||||
"work_id": "...",
|
||||
"owner_user_id": "...",
|
||||
"work_title": "...",
|
||||
"work_description": "...",
|
||||
"publication_status": "Published",
|
||||
"published_at": { "__timestamp_micros_since_unix_epoch__": 0 },
|
||||
"play_count": 0,
|
||||
"like_count": 0,
|
||||
"levels_json": "..."
|
||||
}
|
||||
],
|
||||
"custom_world_profile": []
|
||||
},
|
||||
"workIds": {
|
||||
"puzzle": ["<profile_id>"],
|
||||
"customWorld": ["<profile_id>"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 过滤原则
|
||||
|
||||
1. 按 `tables[].name` 白名单过滤,只保留作品 profile 表。
|
||||
2. 对每个 row 再按字段白名单过滤,避免误带账号、手机号、token、钱包流水等字段。
|
||||
3. 对特别大的字段进行处理:
|
||||
- `cover_image_src` 如果是 `data:image/...base64`,默认替换为占位符或截断,避免压测数据文件过大。
|
||||
- `levels_json`、`profile_payload_json` 保留原文,但可以记录大小;如果过大,再提供 `--compact` 选项只保留摘要。
|
||||
4. 输出 `.local.json` 默认加入 `.gitignore`;如果要提交样例数据,只提交脱敏/裁剪后的 `works-list.sample.json`。
|
||||
|
||||
## K6 压测接口矩阵
|
||||
|
||||
需要先确认本地 api-server 实际端口。默认以 `http://127.0.0.1:8787` 为例,实际运行时通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:<actual-api-port> k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
初版建议覆盖以下“作品列表”读接口,具体路径以仓库服务端路由为准,实施时需要通过搜索 api-server 路由确认:
|
||||
|
||||
| 场景 | 目的 | 候选路径 |
|
||||
| --- | --- | --- |
|
||||
| 拼图作品列表 | 作品列表主场景之一,当前数据量最多 | `/api/creation/puzzle/works` 或实际 puzzle works list route |
|
||||
| RPG/自定义世界作品列表 | 使用 `custom_world_profile` 数据 | `/api/creation/custom-world/works` 或实际 custom world works route |
|
||||
| 作品详情/启动前读取 | 模拟用户从列表点进作品 | `/api/creation/*/works/:profileId` 或 `/api/runtime/*/works/:profileId` |
|
||||
| 公开作品库 | 如果首页/发现页依赖 | `/api/runtime/*/works` 或 gallery/list route |
|
||||
|
||||
> 注意:不要凭空固定 endpoint。实施阶段先用 `search_files` / 路由源码确认真实路径,再写入 K6 脚本。
|
||||
|
||||
## K6 场景设计
|
||||
|
||||
### 阶段 1:基线 smoke
|
||||
|
||||
目的:确认脚本、数据和目标服务可用。
|
||||
|
||||
```js
|
||||
export const options = {
|
||||
scenarios: {
|
||||
smoke: {
|
||||
executor: 'constant-vus',
|
||||
vus: 1,
|
||||
duration: '30s'
|
||||
}
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ['rate<0.01'],
|
||||
http_req_duration: ['p(95)<800']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 阶段 2:常规读压
|
||||
|
||||
目的:模拟日常列表浏览。
|
||||
|
||||
- `constant-vus`: 10/25/50 三档
|
||||
- 每个 VU 随机选择作品类型和列表分页参数
|
||||
- `sleep(0.5~2s)` 模拟用户停留
|
||||
- 阈值建议:
|
||||
- `http_req_failed < 1%`
|
||||
- `p95 < 800ms`
|
||||
- `p99 < 1500ms`
|
||||
|
||||
### 阶段 3:峰值/突刺
|
||||
|
||||
目的:模拟首页入口或活动导致的作品列表突增。
|
||||
|
||||
- `ramping-arrival-rate`
|
||||
- 从 5 RPS 增长到 100 RPS,维持 2~5 分钟,再降回
|
||||
- 单独输出 `checks`:列表接口状态码、响应 JSON shape、items 数量
|
||||
|
||||
### 阶段 4:容量探索
|
||||
|
||||
目的:找瓶颈,不作为每次回归必跑。
|
||||
|
||||
- 每轮提升 RPS 或 VU
|
||||
- 观察:api-server CPU/内存、SpacetimeDB 日志、错误率、p95/p99
|
||||
- 一旦 `http_req_failed >= 5%` 或 p95 持续超过 2s,停止继续加压并记录容量点。
|
||||
|
||||
## K6 脚本设计要点
|
||||
|
||||
1. 使用 `SharedArray` 加载 `works-list.local.json`,避免每个 VU 重复解析大 JSON。
|
||||
2. 基于数据源里的 `profile_id` / `work_id` 随机抽样,保证请求覆盖真实作品 ID。
|
||||
3. 对列表接口添加分页/排序 query,例如:
|
||||
- `?limit=20&offset=0`
|
||||
- `?pageSize=20&cursor=...`(以真实 API 为准)
|
||||
4. 使用 `check()` 验证:
|
||||
- HTTP 200
|
||||
- 响应体是 JSON
|
||||
- `items` 或 `works` 是数组
|
||||
- 列表项包含 `profileId/profile_id`、标题字段、状态字段
|
||||
5. 使用 `Trend` / `Rate` 细分指标:
|
||||
- `works_list_duration`
|
||||
- `works_detail_duration`
|
||||
- `works_list_shape_error_rate`
|
||||
6. 支持环境变量:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:8787 \
|
||||
WORKS_DATA=scripts/loadtest/data/works-list.local.json \
|
||||
SCENARIO=baseline \
|
||||
k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. **确认路由**
|
||||
- 搜索 api-server / BFF 的作品列表路由。
|
||||
- 明确各玩法对应 endpoint、鉴权要求、分页参数、返回字段。
|
||||
2. **实现数据提取脚本**
|
||||
- 新增 `scripts/loadtest/extract-works-list-data.mjs`。
|
||||
- 只按表白名单读取作品列表 profile 表。
|
||||
- 对字段做白名单与脱敏/截断。
|
||||
- 输出 `works-list.local.json`。
|
||||
3. **生成本地压测数据**
|
||||
- 用用户提供的迁移文件生成 `scripts/loadtest/data/works-list.local.json`。
|
||||
- 验证输出只包含作品表和作品字段。
|
||||
4. **实现 K6 脚本**
|
||||
- 新增 `scripts/loadtest/k6-works-list.js`。
|
||||
- 支持 `BASE_URL`、`WORKS_DATA`、`SCENARIO`。
|
||||
- 覆盖列表接口,必要时增加详情/启动前读取接口。
|
||||
5. **新增执行说明**
|
||||
- 在 `scripts/loadtest/README.md` 写明:安装 K6、启动本地 dev 栈、提取数据、运行 smoke/baseline/spike、查看结果。
|
||||
6. **本地验证**
|
||||
- 启动 Genarrative dev 栈;注意端口可能漂移,使用实际 api-server 端口。
|
||||
- 跑 smoke:`SCENARIO=smoke`。
|
||||
- 确认失败率、p95、响应 shape。
|
||||
7. **可选集成 npm scripts**
|
||||
- 如果团队希望标准化入口,再加入 `package.json` scripts。
|
||||
8. **记录结果**
|
||||
- 将 smoke/baseline/spike 的结果摘要追加到 `scripts/loadtest/README.md` 或单独保存到 `.hermes/plans/` 的结果文档中。
|
||||
|
||||
## 启动与运行建议
|
||||
|
||||
本地服务启动按当前 Genarrative dev 栈约定:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
如果 SpacetimeDB/API/Vite 端口被占用,项目脚本会寻找可用端口;压测时必须从启动日志中读取实际 api-server 地址,并传给 K6:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:<actual-api-port> \
|
||||
WORKS_DATA=scripts/loadtest/data/works-list.local.json \
|
||||
SCENARIO=smoke \
|
||||
k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
## 验证标准
|
||||
|
||||
### 数据源验证
|
||||
|
||||
- `works-list.local.json` 中只出现作品 profile 表。
|
||||
- 不出现以下字段或内容:
|
||||
- `password_hash`
|
||||
- `refresh_token_hash`
|
||||
- `phone_number_e164`
|
||||
- `phone_number_masked`
|
||||
- `wallet_ledger_id`
|
||||
- `auth_identity`
|
||||
- `user_account`
|
||||
- `puzzle_work_profile` 行数应接近原始文件中的 80 行。
|
||||
- `custom_world_profile` 行数应接近原始文件中的 1 行。
|
||||
|
||||
### K6 smoke 验证
|
||||
|
||||
- 所有目标接口返回 2xx。
|
||||
- `http_req_failed < 1%`。
|
||||
- 响应 JSON shape 与 shared contracts 对齐:`items` 或 `works` 数组。
|
||||
- K6 输出中能区分不同 endpoint 的耗时。
|
||||
|
||||
### 性能阈值初稿
|
||||
|
||||
- Smoke:`p95 < 800ms`,失败率 `< 1%`
|
||||
- Baseline:`p95 < 1000ms`,`p99 < 2000ms`,失败率 `< 1%`
|
||||
- Spike:允许短暂 p95 抖动,但 1 分钟内应恢复;失败率 `< 5%`
|
||||
|
||||
阈值后续需要结合本地机器性能、SpacetimeDB 本地模式和正式部署规格调整。
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **原始迁移文件包含敏感数据。** 必须只提取作品列表白名单字段,禁止把原始 JSON 全量提交到仓库。
|
||||
2. **base64 封面可能导致压测数据膨胀。** 默认截断或替换为占位符,除非本次明确要测封面 payload 对响应体积的影响。
|
||||
3. **本地 SpacetimeDB 与 api-server 端口会漂移。** 不要硬编码端口,运行时通过 `BASE_URL` 注入。
|
||||
4. **列表接口可能需要鉴权。** 若实际接口要求登录,不要导入真实 refresh session;应使用本地测试账号或专门的压测 token 生成流程。
|
||||
5. **作品表名/接口路径可能与候选名称不完全一致。** 实施前必须以源码路由为准。
|
||||
6. **本计划仅保存压测方案,不执行实际压测。** 后续执行时再创建/修改脚本、导出过滤数据、跑 K6 并记录结果。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. 压测目标是本地 dev 栈、测试环境,还是预发/生产只读接口?不同环境阈值和安全边界不同。
|
||||
2. “作品列表”是否只包含拼图和自定义世界,还是要覆盖 match3d、square-hole、big-fish、visual-novel 的统一列表入口?
|
||||
3. 是否允许使用专门压测账号/token?如果接口无鉴权则无需处理。
|
||||
4. 是否需要测封面/asset 加载,还是只测作品列表 JSON API?
|
||||
447
.hermes/plans/2026-05-11_205645-genarrative-disaster-recovery.md
Normal file
447
.hermes/plans/2026-05-11_205645-genarrative-disaster-recovery.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Genarrative 容灾方案设计计划
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 基于当前 Genarrative 单机生产部署、Jenkins 流水线、SpacetimeDB 与 Rust `api-server` 架构,补齐一套可落地、可演练、可审计的容灾方案。
|
||||
|
||||
**Architecture:** 首版容灾不引入复杂多活系统,优先围绕现有 `systemd + Nginx + SpacetimeDB + api-server + Jenkins` 单机生产推荐方案做“备份可恢复、版本可回滚、故障可切换、演练可复盘”。方案采用分层容灾:入口层、静态资源层、API 服务层、SpacetimeDB 数据层、外部服务与密钥层、Jenkins/发布链路层。
|
||||
|
||||
**Tech Stack:** Nginx、systemd、SpacetimeDB self-hosting、Rust `api-server` / Axum、Jenkins Pipeline、Shell/Node.js 运维脚本、仓库 `deploy/` 与 `docs/technical/` 文档体系。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前上下文与已确认事实
|
||||
|
||||
### 1.1 当前生产部署口径
|
||||
|
||||
来自 `docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 的现状:
|
||||
|
||||
- 生产为单机推荐方案,不使用 Docker。
|
||||
- 公网入口为 Nginx,负责 HTTPS、静态站点、后台静态页面、维护页、`/admin/api/` 与临时 `/api/*` 反向代理。
|
||||
- SpacetimeDB 作为 systemd 服务运行:
|
||||
- `spacetimedb.service`
|
||||
- 监听:`127.0.0.1:3101`
|
||||
- 数据根目录:`/stdb`
|
||||
- Rust `api-server` 作为 systemd 服务运行:
|
||||
- `genarrative-api.service`
|
||||
- 监听:`127.0.0.1:8082`
|
||||
- 环境文件:`/etc/genarrative/api-server.env`
|
||||
- 静态站点发布到 release/current 目录:
|
||||
- `/opt/genarrative/releases/<version>/`
|
||||
- `/opt/genarrative/current`
|
||||
- `/srv/genarrative/web`
|
||||
- 已有维护模式:
|
||||
- 开关文件:`/var/lib/genarrative/maintenance/enabled`
|
||||
- API 发布、SpacetimeDB 模块发布、数据库导入、服务器配置变更必须进入维护模式。
|
||||
- 已有数据库导入导出 Jenkins Job:
|
||||
- `Genarrative-Database-Export`
|
||||
- `Genarrative-Database-Import`
|
||||
- 对应文件:`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`
|
||||
- 已有回滚基本口径:
|
||||
- Web 回滚:切 `/srv/genarrative/web` 或 `/opt/genarrative/current` 到上一版本并 reload Nginx。
|
||||
- API 回滚:切 `/opt/genarrative/current` 到上一版本并重启 `genarrative-api.service`。
|
||||
- SpacetimeDB 模块回滚:发布上一版本 `spacetime_module.wasm`。
|
||||
- 数据回滚:使用导入流水线恢复指定备份,必须进入维护模式。
|
||||
|
||||
### 1.2 关键风险
|
||||
|
||||
- 当前是单机生产拓扑,单机磁盘、系统盘、`/stdb`、Nginx 或公网 IP 故障会造成整体不可用。
|
||||
- SpacetimeDB 是核心业务真相,容灾重点必须围绕 `/stdb`、数据库导出产物、schema 迁移与导入验证。
|
||||
- `/etc/genarrative/api-server.env` 持有生产密钥,不能进入 Git,也不能写进普通备份明文归档。
|
||||
- Jenkins controller/agent 同时承担构建、发布、备份、导入导出编排;Jenkins 不可用时仍需要有最小人工恢复路径。
|
||||
- 外部 LLM、图片、语音、3D 网关不是本仓库可控系统,容灾只能做到配置降级、超时隔离、能力熔断与可观测告警。
|
||||
|
||||
---
|
||||
|
||||
## 2. 容灾目标
|
||||
|
||||
### 2.1 恢复目标建议
|
||||
|
||||
| 灾难类型 | 目标 RTO | 目标 RPO | 首版策略 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Web 静态资源发布失败 | 5 分钟 | 0 | release/current 原子切换回滚 |
|
||||
| API 发布失败 | 10 分钟 | 0 | 维护模式 + 上一版二进制回滚 |
|
||||
| SpacetimeDB wasm 发布失败 | 15 分钟 | 0 或按迁移前备份 | 发布前导出 + 上一版 wasm 回滚 |
|
||||
| 数据误写 / 迁移失败 | 30-60 分钟 | 最近一次导出点 | 导入流水线从备份恢复 |
|
||||
| 生产机磁盘损坏 | 2-4 小时 | 最近一次异地备份 | 新机器 provision + 拉取 release 包 + 恢复数据库 |
|
||||
| Jenkins controller 不可用 | 1-2 小时 | 不影响线上数据 | 手工脚本恢复 + Jenkins 备份恢复 |
|
||||
| 第三方模型网关不可用 | 5-15 分钟内降级 | 不丢核心数据 | 配置切换 / 功能熔断 / 队列失败可重试 |
|
||||
|
||||
### 2.2 首版不做
|
||||
|
||||
- 不做跨地域双活写入。
|
||||
- 不做 SpacetimeDB 在线主从复制,除非后续官方能力与项目压测验证支持。
|
||||
- 不让前端绕过 `api-server` 直接承担正式业务真相。
|
||||
- 不把生产密钥、Token、数据库 dump、Jenkins secret 写入 Git。
|
||||
- 不恢复旧 `server-node`、Express、PostgreSQL 或 Docker 一体化部署方案。
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体容灾设计
|
||||
|
||||
### 3.1 分层策略
|
||||
|
||||
1. **入口层:Nginx / DNS / HTTPS**
|
||||
- 保留 Nginx 配置模板在 Git:`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`。
|
||||
- 为 release 环境建立 Nginx 配置备份与证书恢复流程。
|
||||
- 明确 DNS 切换预案:生产机不可恢复时,将域名指向灾备机公网 IP。
|
||||
|
||||
2. **静态资源层:Web / Admin Web**
|
||||
- 依赖 `web.tar.gz`、`web.tar.gz.sha256`、`release-manifest.json`。
|
||||
- 保留最近 N 个 release 目录与构建产物指针。
|
||||
- 回滚只切软链,不重新构建。
|
||||
|
||||
3. **API 服务层:Rust `api-server`**
|
||||
- 依赖归档的 `api-server` 二进制、checksum、`release-manifest.json`。
|
||||
- `/etc/genarrative/api-server.env` 通过加密备份或密钥管理恢复,不进入 release 包。
|
||||
- systemd unit 由 `deploy/systemd/genarrative-api.service` 重新安装。
|
||||
|
||||
4. **数据层:SpacetimeDB**
|
||||
- 每次高风险发布前强制导出数据库。
|
||||
- 定时导出:建议每天至少 1 次;高活跃期可每 4 小时 1 次。
|
||||
- 导出产物同时保存在:Jenkins 归档 + 生产机 `SERVER_BACKUP_DIRECTORY` + 异地对象存储/备份机。
|
||||
- 导入前自动生成安全备份,保留当前实现口径。
|
||||
|
||||
5. **发布编排层:Jenkins**
|
||||
- Jenkins Job、Jenkinsfile 在 Git 中可恢复。
|
||||
- Jenkins controller 配置、凭据、插件清单需要额外备份。
|
||||
- 发布 agent 使用 inbound + systemd 自恢复,agent secret 仅存在目标机或 Jenkins 凭据。
|
||||
|
||||
6. **密钥与外部服务层**
|
||||
- `/etc/genarrative/api-server.env`、Jenkins Secret Text、SSH PEM、agent secret 不进 Git。
|
||||
- 制定密钥清单和恢复责任人,但不在仓库记录明文。
|
||||
- 外部服务配置按 `docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md` 维护必配项。
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议新增/更新的文档
|
||||
|
||||
### Task 1: 新增生产容灾技术方案文档
|
||||
|
||||
**Objective:** 形成团队可共享、可执行的容灾总纲。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Modify: `docs/technical/README.md`(若已有技术索引,应加入该文档入口)
|
||||
- Optional Modify: `.hermes/shared-memory/project-overview.md`(只加稳定索引,不写敏感信息)
|
||||
|
||||
**文档必须覆盖:**
|
||||
|
||||
1. 容灾目标:RTO/RPO 表。
|
||||
2. 生产资产清单:Nginx、systemd、release/current、`/stdb`、`/etc/genarrative/api-server.env`、Jenkins、构建产物。
|
||||
3. 备份策略:
|
||||
- 数据库导出。
|
||||
- release 产物保留。
|
||||
- Nginx/systemd/env 配置备份。
|
||||
- Jenkins 配置备份。
|
||||
4. 恢复流程:
|
||||
- Web 回滚。
|
||||
- API 回滚。
|
||||
- Stdb module 回滚。
|
||||
- 数据恢复。
|
||||
- 整机重建。
|
||||
5. 演练计划:每月一次数据库恢复演练,每季度一次整机重建演练。
|
||||
6. 安全边界:密钥不进 Git,备份加密,最小权限。
|
||||
7. 验收命令与人工检查清单。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS,无中文乱码、无 BOM/CRLF 问题。
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议新增/更新的脚本与流水线
|
||||
|
||||
### Task 2: 增强数据库定时备份流水线
|
||||
|
||||
**Objective:** 把现有人工导出扩展为可定时执行、可异地保存、可审计的备份流程。
|
||||
|
||||
**Files:**
|
||||
- Modify: `jenkins/Jenkinsfile.production-database-export`
|
||||
- Modify: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Optional Create: `scripts/deploy/production-backup-sync.sh`
|
||||
|
||||
**Implementation notes:**
|
||||
|
||||
- 在 Jenkins Job 中保留人工触发能力,同时建议配置 cron:
|
||||
- development:每天凌晨。
|
||||
- release:每天凌晨或业务低峰。
|
||||
- 增加备份命名规范:
|
||||
- `spacetime-migration-<database>-<yyyyMMdd-HHmmss>-<source_commit>.json`
|
||||
- 增加 `SERVER_BACKUP_DIRECTORY` 默认建议:
|
||||
- `/var/backups/genarrative/spacetimedb/<database>/`
|
||||
- 增加备份保留策略:
|
||||
- 本机保留 7-14 天。
|
||||
- 异地保留 30-90 天。
|
||||
- 如实现 `production-backup-sync.sh`,只做同步框架,不硬编码真实 bucket、账号、endpoint 或密钥。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/production-backup-sync.sh
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: shell 语法通过;文档编码检查通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 增加灾备恢复 Runbook
|
||||
|
||||
**Objective:** 在真正故障时不依赖临场推理,按清单执行恢复。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/operations/PRODUCTION_DR_RUNBOOK_2026-05-11.md`
|
||||
- Modify: `docs/operations/README.md`(如果存在)
|
||||
|
||||
**Runbook sections:**
|
||||
|
||||
1. 故障分级:P0/P1/P2。
|
||||
2. 第一响应:
|
||||
- 判断 Nginx 是否在线。
|
||||
- 判断 `genarrative-api.service` 是否在线。
|
||||
- 判断 `spacetimedb.service` 是否在线。
|
||||
- 判断磁盘是否满。
|
||||
- 判断 Jenkins agent 是否在线。
|
||||
3. 快速止血:
|
||||
- 开维护模式。
|
||||
- 禁止继续发布。
|
||||
- 保留现场日志。
|
||||
4. 回滚流程:
|
||||
- Web 回滚命令。
|
||||
- API 回滚命令。
|
||||
- Stdb wasm 回滚命令。
|
||||
5. 数据恢复流程:
|
||||
- 选择备份。
|
||||
- dry-run 导入。
|
||||
- 确认导入。
|
||||
- smoke test。
|
||||
6. 整机重建流程:
|
||||
- 新机器 provision。
|
||||
- 恢复 `/etc/genarrative/api-server.env`。
|
||||
- 恢复 SpacetimeDB 数据。
|
||||
- 发布最近稳定 release。
|
||||
- DNS 切换。
|
||||
7. 复盘模板。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 增加备份健康检查与恢复演练记录模板
|
||||
|
||||
**Objective:** 防止“有备份但不可恢复”。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/operations/DR_DRILL_REPORT_TEMPLATE.md`
|
||||
- Optional Create: `scripts/deploy/verify-database-backup.sh`
|
||||
- Modify: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
|
||||
**建议检查项:**
|
||||
|
||||
- 备份文件存在且大小非 0。
|
||||
- 备份文件 checksum 可验证。
|
||||
- 备份文件可被 `Genarrative-Database-Import` dry-run 解析。
|
||||
- 最近一次备份时间未超过 RPO 阈值。
|
||||
- 导入后 `/healthz` 可用。
|
||||
- 首页、后台登录页、关键 API smoke 可用。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/verify-database-backup.sh
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
## 6. 具体恢复流程草案
|
||||
|
||||
### 6.1 Web 静态资源回滚
|
||||
|
||||
1. 进入目标机。
|
||||
2. 查看 release 目录:`/opt/genarrative/releases/`。
|
||||
3. 选择上一个稳定版本。
|
||||
4. 切换 `/srv/genarrative/web` 或 `/opt/genarrative/current` 软链。
|
||||
5. 执行 Nginx 配置检查与 reload。
|
||||
6. 访问首页与后台静态入口。
|
||||
|
||||
验收:
|
||||
|
||||
- `/` 返回最新稳定页面。
|
||||
- `/admin/` 返回后台页面。
|
||||
- 静态资源无 404。
|
||||
|
||||
### 6.2 API 回滚
|
||||
|
||||
1. 开维护模式。
|
||||
2. 切 `/opt/genarrative/current` 到上一版包含稳定 `api-server` 的 release。
|
||||
3. 重启 `genarrative-api.service`。
|
||||
4. 本机检查 `http://127.0.0.1:8082/healthz`。
|
||||
5. 检查 Nginx 反代路径。
|
||||
6. 解除维护模式。
|
||||
|
||||
验收:
|
||||
|
||||
- `systemctl status genarrative-api.service` 正常。
|
||||
- `/healthz` 正常。
|
||||
- 后台 `/admin/api/*` 基础接口正常。
|
||||
|
||||
### 6.3 SpacetimeDB 模块回滚
|
||||
|
||||
1. 开维护模式。
|
||||
2. 确认目标数据库名与当前 API 环境一致:`GENARRATIVE_SPACETIME_DATABASE`。
|
||||
3. 选择上一版 `spacetime_module.wasm`。
|
||||
4. 使用 `spacetimedb` 服务用户发布上一版 wasm。
|
||||
5. 重启或检查 `spacetimedb.service`。
|
||||
6. 检查 `api-server` 对目标数据库访问。
|
||||
7. 解除维护模式。
|
||||
|
||||
注意:如果 schema 已迁移且旧 wasm 不兼容当前数据,需要走数据恢复,不应直接盲目发布旧 wasm。
|
||||
|
||||
### 6.4 数据恢复
|
||||
|
||||
1. 开维护模式。
|
||||
2. 从 Jenkins 归档或 `SERVER_BACKUP_DIRECTORY` 选择备份。
|
||||
3. 先执行导入 dry-run。
|
||||
4. 真正导入前生成当前数据库安全备份。
|
||||
5. 执行导入。
|
||||
6. 执行 smoke test。
|
||||
7. 解除维护模式。
|
||||
|
||||
必须记录:
|
||||
|
||||
- 备份文件名。
|
||||
- 来源 Job/build number。
|
||||
- 恢复目标 database。
|
||||
- 恢复开始/结束时间。
|
||||
- 恢复后验证结果。
|
||||
|
||||
### 6.5 整机重建
|
||||
|
||||
1. 准备新 Linux 机器。
|
||||
2. 接入 Jenkins release deploy agent,或准备人工 SSH 运维路径。
|
||||
3. 运行 `Genarrative-Server-Provision`:
|
||||
- 创建用户和目录。
|
||||
- 安装 SpacetimeDB。
|
||||
- 安装 systemd unit。
|
||||
- 安装 Nginx 配置。
|
||||
4. 恢复 `/etc/genarrative/api-server.env`。
|
||||
5. 发布最近稳定 Web/API/Stdb 产物。
|
||||
6. 导入最近一次有效数据库备份。
|
||||
7. smoke test。
|
||||
8. 切 DNS。
|
||||
9. 观察 30-60 分钟。
|
||||
|
||||
---
|
||||
|
||||
## 7. 文件可能变更清单
|
||||
|
||||
首版落地建议按以下文件收口:
|
||||
|
||||
- Create: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Create: `docs/operations/PRODUCTION_DR_RUNBOOK_2026-05-11.md`
|
||||
- Create: `docs/operations/DR_DRILL_REPORT_TEMPLATE.md`
|
||||
- Modify: `docs/technical/README.md`
|
||||
- Modify: `docs/operations/README.md`(若存在)
|
||||
- Modify: `.hermes/shared-memory/project-overview.md`(仅增加文档索引)
|
||||
- Optional Modify: `jenkins/Jenkinsfile.production-database-export`
|
||||
- Optional Modify: `jenkins/Jenkinsfile.production-database-import`
|
||||
- Optional Create: `scripts/deploy/production-backup-sync.sh`
|
||||
- Optional Create: `scripts/deploy/verify-database-backup.sh`
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试与验收
|
||||
|
||||
### 8.1 文档与编码
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### 8.2 Shell 脚本语法
|
||||
|
||||
如新增 shell 脚本:
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/production-backup-sync.sh
|
||||
bash -n scripts/deploy/verify-database-backup.sh
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### 8.3 Jenkinsfile 静态检查
|
||||
|
||||
建议在 Jenkins UI 或本地 Jenkins Pipeline Linter 中检查:
|
||||
|
||||
- `jenkins/Jenkinsfile.production-database-export`
|
||||
- `jenkins/Jenkinsfile.production-database-import`
|
||||
|
||||
Expected: Pipeline syntax valid。
|
||||
|
||||
### 8.4 演练验收
|
||||
|
||||
至少完成一次 development 目标演练:
|
||||
|
||||
1. 触发 `Genarrative-Database-Export`。
|
||||
2. 确认备份产物存在并归档。
|
||||
3. 使用 `Genarrative-Database-Import` dry-run 验证备份可解析。
|
||||
4. 不覆盖生产数据的前提下,记录演练报告。
|
||||
|
||||
release 目标演练应在业务低峰进行,并先确认通知渠道可用。
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险、取舍与开放问题
|
||||
|
||||
### 9.1 风险
|
||||
|
||||
- 单机生产仍存在物理机级单点故障,首版只能通过“快速重建 + 异地备份”降低恢复时间。
|
||||
- SpacetimeDB schema 回滚不一定可逆,必须把发布前备份作为强约束。
|
||||
- Jenkins controller 若在本地 Windows,controller 自身备份和恢复需要单独制定,不应只依赖 agent 自恢复。
|
||||
- 外部模型网关失败可能影响创作能力,但不应影响已发布作品浏览和后台基础能力。
|
||||
|
||||
### 9.2 取舍
|
||||
|
||||
- 选择先做可执行 runbook 和备份恢复演练,而不是直接引入复杂多活。
|
||||
- 选择继续复用现有 Jenkins 导入导出流水线,降低工程改造风险。
|
||||
- 选择不把密钥恢复细节写死到 Git 文档,避免泄露。
|
||||
|
||||
### 9.3 开放问题
|
||||
|
||||
1. release 环境是否已经有独立备份机或对象存储?如果有,需要补充备份同步目标,但不能提交密钥。
|
||||
2. Jenkins controller 的 `JENKINS_HOME` 当前实际部署在哪里?是否已有周期备份?
|
||||
3. 生产域名 DNS TTL 当前是多少?是否可降低到适合故障切换的值?
|
||||
4. `/stdb` 所在磁盘是否独立于系统盘?是否已有磁盘水位告警?
|
||||
5. release 环境的通知渠道除邮件外是否需要接入企业微信/飞书/Telegram?
|
||||
|
||||
---
|
||||
|
||||
## 10. 推荐实施顺序
|
||||
|
||||
1. 先只落文档:技术方案 + runbook + 演练模板。
|
||||
2. 在 development 目标做一次数据库导出 + dry-run 导入演练。
|
||||
3. 根据演练结果补脚本:备份同步、备份健康检查。
|
||||
4. 再把 release 备份设置为定时任务。
|
||||
5. 最后规划整机重建演练与 DNS 切换演练。
|
||||
|
||||
首版完成标准:
|
||||
|
||||
- 团队任一成员打开 runbook,即可在 30 分钟内完成 Web/API 回滚或数据库备份 dry-run 恢复。
|
||||
- 最近一次数据库备份时间、备份位置、checksum、恢复演练结果可追溯。
|
||||
- 生产密钥仍只存在于服务器/Jenkins 凭据/加密备份中,不进入 Git。
|
||||
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 凭据和生产运行时配置,需要另行确认访问范围和凭据边界。
|
||||
@@ -0,0 +1,206 @@
|
||||
# 远端作品列表压测排查报告
|
||||
|
||||
时间:2026-05-12 06:16 CST
|
||||
目标:`http://82.157.175.59`
|
||||
SSH:远端生产机 root 账号(具体私钥路径仅保留在本机环境,不写入仓库)
|
||||
|
||||
## 背景
|
||||
|
||||
远端 `k6-works-list.js` 压测中:
|
||||
|
||||
- smoke 通过。
|
||||
- baseline 10 VU:无 HTTP 错误,但 p95/p99 超阈值。
|
||||
- 50 RPS spike:`http_req_failed` / `works_list_shape_error_rate` 约 21.99%。
|
||||
- 100 RPS spike:`http_req_failed` / `works_list_shape_error_rate` 约 25.47%。
|
||||
- 从 k6 check 看,失败主要集中在 `puzzle_gallery_list`,`custom_world_gallery_list` 基本正常。
|
||||
|
||||
## 已完成排查
|
||||
|
||||
### 1. 服务器进程与资源
|
||||
|
||||
远端服务监听:
|
||||
|
||||
- Rust api-server:`127.0.0.1:8082`,systemd 服务 `genarrative-api.service`。
|
||||
- SpacetimeDB:`127.0.0.1:3101`,systemd 服务 `spacetimedb.service`。
|
||||
- Nginx:公网 80 反代 `/api/*` 到 `127.0.0.1:8082`。
|
||||
|
||||
服务器规格/状态:
|
||||
|
||||
- 2 vCPU。
|
||||
- 内存约 1.9GiB。
|
||||
- Swap 约 1.9GiB,已有约 600MiB 使用。
|
||||
- `/` 磁盘约 69%。
|
||||
- Rust api-server 当前 CPU 不高。
|
||||
- SpacetimeDB 当前 CPU 不高。
|
||||
|
||||
发现一个独立异常:
|
||||
|
||||
- PM2 下旧 `server-node` 进程 `genarrative` 正在重启风暴。
|
||||
- cwd:`/work/Genarrative/server-node`
|
||||
- 错误:连接 `127.0.0.1:5432` PostgreSQL 被拒绝。
|
||||
- PM2 restart 次数已超过 33 万。
|
||||
- 该进程不是当前公网 `/api/*` 使用的 Rust api-server,但会制造额外 CPU/内存/日志抖动。
|
||||
|
||||
### 2. 压测窗口服务端日志
|
||||
|
||||
子任务聚合了 2026-05-12 04:50-05:05 的 nginx 与 api-server 日志。
|
||||
|
||||
nginx access:
|
||||
|
||||
- `/api/runtime/puzzle/gallery`:4661 次,全部 200。
|
||||
- `/api/runtime/custom-world-gallery`:4659 次,全部 200。
|
||||
|
||||
api-server journal:
|
||||
|
||||
`/api/runtime/puzzle/gallery`:
|
||||
|
||||
- completed:4661
|
||||
- status:200 全部
|
||||
- slow_request:0
|
||||
- latency_ms:min 13 / p50 30 / p90 43 / p95 50 / p99 62 / max 88
|
||||
|
||||
`/api/runtime/custom-world-gallery`:
|
||||
|
||||
- completed:4659
|
||||
- status:200 全部
|
||||
- slow_request:0
|
||||
- latency_ms:min 0 / p50 1 / p90 5 / p95 7 / p99 13 / max 49
|
||||
|
||||
结论:
|
||||
|
||||
- 在服务端视角,两个接口在该窗口都没有 5xx,也没有慢请求。
|
||||
- 这与 k6 客户端侧 30s timeout / failed check 存在明显不一致。
|
||||
- 需要进一步区分:客户端侧网络/连接耗尽/本机 k6 执行环境问题,还是 k6 统计混合/响应解析问题。
|
||||
|
||||
### 3. k6 脚本行为
|
||||
|
||||
文件:`scripts/loadtest/k6-works-list.js`
|
||||
|
||||
无 `AUTH_TOKEN` 时,每轮 iteration 顺序请求两个接口:
|
||||
|
||||
1. `GET /api/runtime/puzzle/gallery`
|
||||
2. `GET /api/runtime/custom-world-gallery`
|
||||
|
||||
`DETAIL_RATIO=0` 时不会请求详情。
|
||||
|
||||
`works_list_shape_error_rate` 不只代表字段结构错误,只要下面任意 check 失败都会计入:
|
||||
|
||||
- status is 200
|
||||
- returns json object
|
||||
- has collection
|
||||
- list item shape
|
||||
|
||||
因此 timeout、非 JSON、非 200、响应结构不符合都会表现为 shape error。
|
||||
|
||||
数据文件实际路径:
|
||||
|
||||
- `scripts/loadtest/data/works-list.local.json`
|
||||
|
||||
脚本里 `data/works-list.local.json` 是相对 k6 脚本文件解析的,因此本身合理。
|
||||
|
||||
### 4. 代码层疑似瓶颈
|
||||
|
||||
虽然这次远端服务端日志没有复现慢请求,但代码层仍发现一个真实性能隐患。
|
||||
|
||||
`/api/runtime/puzzle/gallery` 调用链:
|
||||
|
||||
- `server-rs/crates/api-server/src/app.rs:1192`
|
||||
- `server-rs/crates/api-server/src/puzzle.rs:1385-1409`
|
||||
- `server-rs/crates/spacetime-client/src/puzzle.rs:367-381`
|
||||
- `server-rs/crates/spacetime-module/src/puzzle.rs:430-443`
|
||||
- `server-rs/crates/spacetime-module/src/puzzle.rs:1393-1404`
|
||||
|
||||
关键实现:
|
||||
|
||||
- `list_puzzle_gallery_tx` 对 `puzzle_work_profile().iter()` 全表扫描。
|
||||
- 再过滤 `publication_status == Published`。
|
||||
- 对每个公开作品调用 `build_puzzle_work_profile_from_row_with_recent_count`。
|
||||
- 该函数调用 `count_recent_public_work_plays(ctx, "puzzle", &row.profile_id, now_micros)`。
|
||||
|
||||
`count_recent_public_work_plays`:
|
||||
|
||||
- 文件:`server-rs/crates/spacetime-module/src/runtime/profile.rs:1296-1321`
|
||||
- 当前实现对 `public_work_play_daily_stat().iter()` 全表扫描过滤。
|
||||
- 但表定义已有复合索引:
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs:242-248`
|
||||
- `by_public_work_play_daily_stat_work_day(source_type, profile_id, played_day)`
|
||||
- 当前统计函数未使用该索引。
|
||||
|
||||
复杂度风险:
|
||||
|
||||
```text
|
||||
puzzle gallery ~= O(puzzle_work_profile 全表扫描 + Published作品数 * public_work_play_daily_stat 全表扫描)
|
||||
```
|
||||
|
||||
`custom-world-gallery` 与 puzzle 的差异:
|
||||
|
||||
- custom-world 使用 `CustomWorldGalleryEntry` 公开读模型表。
|
||||
- puzzle 直接从 `puzzle_work_profile` 即席拼装。
|
||||
- 两者都调用 recent count,但 puzzle 更容易受作品表规模和统计表规模影响。
|
||||
|
||||
## 当前判断
|
||||
|
||||
本次排查有两个层面的结论:
|
||||
|
||||
1. 生产服务端日志没有证明 `puzzle/gallery` 在 04:50-05:05 窗口真的 30s 慢或 5xx。
|
||||
- api-server 记录的 p95 只有 50ms。
|
||||
- nginx 看到两个接口都是 200。
|
||||
- 所以 k6 侧的 30s timeout 需要进一步从客户端网络、连接池、Windows/k6 执行环境、summary 混合统计角度验证。
|
||||
|
||||
2. 代码层确实存在可修的性能隐患。
|
||||
- `count_recent_public_work_plays` 未使用已有索引。
|
||||
- puzzle gallery 对每个作品重复做 recent count。
|
||||
- puzzle gallery 未使用 `publication_status` 索引或读模型。
|
||||
|
||||
## 建议下一步
|
||||
|
||||
### A. 先处理服务器 PM2 重启风暴
|
||||
|
||||
建议确认旧 Node 服务是否仍需要。
|
||||
|
||||
如果不需要,应停止并禁用 PM2 中的旧 `server-node`:
|
||||
|
||||
```bash
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 stop genarrative
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 delete genarrative
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 save
|
||||
```
|
||||
|
||||
这是生产侧操作,执行前需要确认。
|
||||
|
||||
### B. 单接口短压验证客户端/服务端不一致
|
||||
|
||||
不要继续用混合脚本大压。
|
||||
|
||||
建议新增或临时使用单接口 k6 脚本,分别只测:
|
||||
|
||||
- `/api/runtime/puzzle/gallery`
|
||||
- `/api/runtime/custom-world-gallery`
|
||||
|
||||
并在同一时间窗口并行采集:
|
||||
|
||||
- k6 客户端 summary
|
||||
- nginx access 请求数/状态码
|
||||
- api-server journal latency
|
||||
- 本机到服务器网络错误/timeout
|
||||
|
||||
目标是确认 timeout 是不是发生在客户端侧连接/网络,而不是服务端处理慢。
|
||||
|
||||
### C. 修复代码性能隐患
|
||||
|
||||
优先级建议:
|
||||
|
||||
1. `count_recent_public_work_plays` 改为使用 `by_public_work_play_daily_stat_work_day` 复合索引,或至少改成批量统计,避免 N 次全表扫描。
|
||||
2. `list_puzzle_gallery_tx` 使用 `by_puzzle_work_publication_status` 索引查询 Published,或参考 custom-world 建立 `puzzle_gallery_entry` 公开读模型。
|
||||
3. gallery 列表页不要实时逐条扫描统计表,可维护读模型或批量聚合 `recent_play_count_7d`。
|
||||
|
||||
### D. 调整 k6 脚本输出
|
||||
|
||||
建议 k6 summary 按 endpoint tag 输出或新增单接口模式,否则 overall 指标会把 puzzle/custom-world 混在一起。
|
||||
|
||||
建议增加:
|
||||
|
||||
- `ENDPOINT=puzzle_gallery_list`
|
||||
- `ENDPOINT=custom_world_gallery_list`
|
||||
|
||||
让脚本只跑一个 endpoint,避免诊断时混淆。
|
||||
BIN
.hermes/plans/frame_003.jpg
Normal file
BIN
.hermes/plans/frame_003.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
.hermes/plans/frame_010.jpg
Normal file
BIN
.hermes/plans/frame_010.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
.hermes/plans/frame_020.jpg
Normal file
BIN
.hermes/plans/frame_020.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
.hermes/plans/frame_035.jpg
Normal file
BIN
.hermes/plans/frame_035.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -35,7 +35,7 @@
|
||||
## 2026-05-12 拼图 UI 背景图复用 levels_json 持久化
|
||||
|
||||
- 背景:拼图草稿结果页需要像抓大鹅一样支持 UI 背景生成,但首版只需要作品级/首关背景,不应为图片生成结果新增 SpacetimeDB 表结构。
|
||||
- 决策:拼图 UI 背景字段存入首关 `levels_json`,字段为 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`;结果页新增 `UI` Tab,可编辑提示词并触发 `generate_puzzle_ui_background`。`api-server` 读取 `public/ui-previews/puzzle-image-compact-ui-2026-05-08.png` 作为非拼图 UI 参考图,调用 VectorEngine `gpt-image-2-all` 生成 9:16 背景并要求中央正方形拼图区与外部 UI 背景边界清晰。SpacetimeDB 只保存结果,不做外部 I/O。
|
||||
- 决策:拼图 UI 背景字段存入首关 `levels_json`,字段为 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`;`compile_puzzle_draft` 草稿编译阶段在首图和背景音乐后自动生成首关 UI 背景,失败只记录 warning 并允许结果页重试;结果页新增 `UI` Tab,可编辑提示词并触发 `generate_puzzle_ui_background`。`api-server` 读取 `public/ui-previews/puzzle-image-compact-ui-2026-05-08.png` 作为非拼图 UI 参考图,调用 VectorEngine `gpt-image-2-all` 生成 9:16 背景并要求中央正方形拼图区与外部 UI 背景边界清晰。SpacetimeDB 只保存结果,不做外部 I/O。
|
||||
- 影响范围:拼图结果页、拼图运行态背景渲染、拼图 agent action、`module-puzzle` / `spacetime-module` / `spacetime-client` 的拼图关卡 JSON 映射、拼图流程技术文档。
|
||||
- 验证方式:执行 `npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_ui_background --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。
|
||||
@@ -56,6 +56,14 @@
|
||||
- 验证方式:执行 `npm run test -- src/components/auth/AuthGate.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、触碰文件 ESLint、`npm run check:encoding`。
|
||||
- 关联文档:`docs/prd/PROFILE_LEGAL_INFO_AND_AUTH_AGREEMENT_PRD_2026-05-12.md`。
|
||||
|
||||
## 2026-05-12 微信小程序待绑定手机号优先走原生手机号授权
|
||||
|
||||
- 背景:微信小程序 `web-view` 壳登录后若返回 `pending_bind_phone`,H5 仍会展示手输手机号和短信验证码绑定页,体验上多了一步。
|
||||
- 决策:小程序壳在 `pending_bind_phone` 时暂不打开 H5,先展示原生 `button open-type="getPhoneNumber"`;用户同意后把 `bindgetphonenumber` 返回的 `code` 作为 `wechatPhoneCode` 调用 `/api/auth/wechat/bind-phone`。后端通过微信 `stable_token` 与 `getuserphonenumber` 换取平台验证后的手机号,再复用现有微信待绑定账号合并逻辑并重新签发 active 系统 token。H5 旧短信验证码绑定流程继续作为非小程序环境兜底。
|
||||
- 影响范围:`miniprogram/pages/web-view/index.*`、`server-rs/crates/platform-auth`、`server-rs/crates/api-server/src/wechat_auth.rs`、认证共享契约、微信小程序 web-view 壳技术文档。
|
||||
- 验证方式:执行 `npm run check:encoding`、`node scripts/check-wechat-miniprogram-auth-smoke.mjs`、`cargo test -p shared-contracts wechat_bind_phone_request_accepts_mini_program_phone_code --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat_miniprogram_bind_phone_code_activates_pending_user --manifest-path server-rs/Cargo.toml -- --nocapture`。
|
||||
- 关联文档:`docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md`。
|
||||
|
||||
## 2026-05-11 拼图与抓大鹅结果页音频资产复用通用创作音频链路
|
||||
|
||||
- 背景:拼图和抓大鹅结果页需要接入 Suno 背景音乐,抓大鹅还需要物体点击音效,但当前两类作品没有独立的作品级音频表或 metadata 字段。
|
||||
@@ -65,12 +73,20 @@
|
||||
- 验证方式:执行拼图/抓大鹅结果页定向测试、`npm run typecheck`、`cargo test -p api-server vector_engine_audio_generation`、`cargo test -p shared-contracts creation_audio`、`cargo check -p api-server`,真实生成需配置 VectorEngine 与 OSS 私密环境。
|
||||
- 关联文档:`docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md`。
|
||||
|
||||
## 2026-05-11 寓教于乐公开作品使用独立 `edutainment` 来源接入
|
||||
|
||||
- 背景:`宝贝识物` 首关需要通过创作模板发布后进入寓教于乐板块,同时关闭入口时必须从发现页、搜索、详情深链、作品号和历史入口完全不可见;若继续落入 RPG 默认公共作品链路,容易出现误启动、误改造或近似标签误归类。
|
||||
- 决策:寓教于乐公开作品在前端公共作品模型中使用 `sourceType = edutainment`,当前只承接 `templateId = baby-object-match`、`templateName = 宝贝识物`;进入“发现 / 寓教于乐”频道仍必须携带精确等于 `寓教于乐` 的公开标签,不因模板名或近似标签自动归类。公开详情、推荐运行态、改造、编辑、点赞和分享链路都必须显式识别 `edutainment`,不得回落到 RPG 默认处理。
|
||||
- 影响范围:公开作品卡、发现页频道、作品号搜索、公开详情深链、分享、作品架聚合、后续儿童动作 Demo 模板的发布结果展示。
|
||||
- 验证方式:执行第4线程定向单测、前端类型检查、ESLint 与编码检查;关闭 `VITE_ENABLE_EDUTAINMENT_ENTRY` 时确认精确 `寓教于乐` 作品不可通过任何公开入口访问。
|
||||
- 关联文档:`docs/design/CHILD_MOTION_EDUTAINMENT_DISCOVER_ENTRY_2026-05-09.md`、`docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md`、`docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md`。
|
||||
|
||||
## 2026-05-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 生图结果。
|
||||
- 背景:儿童动作 Demo 需要从暗色科技风切换到更适合儿童互动的卡通绘本草地风格,并且要让背景、地面、UI、地面指示环和用户轮廓使用同一套 image-2 资源口径。
|
||||
- 决策:热身舞台及后续儿童动作 Demo 场景、物品、UI 资源统一采用明亮卡通绘本草地视觉语言。真实资源默认输出到 `public/child-motion-demo/`。背景沿用 `picture-book-grass-stage.png`;地面、指示环、角色轮廓和 UI 已拆分为 v2 用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v2.png`、`picture-book-character-outline-v2.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png` 和 `picture-book-ui-button-v2.png`。生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 调用 VectorEngine `gpt-image-2-all`;透明资源使用品红底生成后本地去背,中间源图仅保存在 `tmp/child-motion-demo-assets/`。在缺少 `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` 应能写出默认背景文件。
|
||||
- 验证方式:检查 `/child-motion-demo` 舞台是否在未生成资产时仍有可用草地绘本兜底;补齐 VectorEngine 私密配置后运行 `npm run assets:child-motion-demo -- --live` 或 `--live --only <asset-id>` 应能写出对应 PNG,并确认页面静态资源返回 `image/png`。若只调整透明去背、裁切或品红边缘,可运行 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用源图后处理。页面接入时必须按资源原始比例等比使用,不得把方形软纸面板拉伸成 HUD、状态条或底部草坪。
|
||||
- 关联文档:`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 方洞挑战从创作页入口和作品架隐藏
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
|
||||
- 关联:`AGENTS.md`、`npm run check:encoding`。
|
||||
|
||||
## 忘记密码后仍提示手机号或密码错误先查认证快照同步
|
||||
|
||||
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 `Bearer JWT 版本已失效`,日志里的 token version 与本地快照不一致。
|
||||
- 原因:重置/修改密码会更新 `password_hash`、`password_login_enabled` 和 `token_version`,如果 API 层只更新本地 `InMemoryAuthStore`,没有调用 `sync_auth_store_snapshot_to_spacetime()`,`api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态。
|
||||
- 处理:`POST /api/auth/password/change` 与 `POST /api/auth/password/reset` 成功后必须同步认证快照;启动恢复时从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照,本地文件更新时尝试回写 SpacetimeDB。
|
||||
- 验证:执行 `cargo test -p module-auth password --manifest-path server-rs/Cargo.toml` 与 `cargo test -p api-server password --manifest-path server-rs/Cargo.toml`;手测时重设密码后旧密码应失败,新密码应成功,重启后仍应保持。
|
||||
- 关联:`server-rs/crates/api-server/src/password_management.rs`、`server-rs/crates/api-server/src/state.rs`、`docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md`。
|
||||
|
||||
## `.hermes` 只放共享内容,不放个人 Hermes 配置
|
||||
|
||||
- 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
|
||||
@@ -59,14 +67,31 @@
|
||||
- 验证:运行 `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` 仍通过。
|
||||
- 现象:`宝贝识物` 运行态打开礼物盒或反馈结束后,当前物品被连续送入左侧或右侧篮子,或硬件动作名偶发命中导致未做明确横移动作也触发选篮。
|
||||
- 原因:选篮如果同时消费 `wave_left_hand` / `wave_right_hand` / `wave` 动作名和手部轨迹,或在 `correct` / `wrong` 反馈阶段继续累计手部路径,会把抓握、反馈期间残留移动或未知侧别手部误算成下一次选篮。
|
||||
- 处理:宝贝识物选篮只使用明确 `leftHand` / `rightHand` 的连续横向轨迹阈值;侧别为 `unknown` 的手部轨迹不参与选篮;礼物盒打开和反馈阶段清空轨迹,不在非 `active` 阶段累计路径。礼物盒激活仍使用 `open_palm -> grab` 抓握序列。
|
||||
- 补充:当前本地 mocap 的 handedness 是摄像头视角,宝贝识物选篮前需要换算为用户身体视角;`rightHand` 轨迹代表玩家左手并进入左篮,`leftHand` 轨迹代表玩家右手并进入右篮。键鼠调试不走该换算,仍保持鼠标左键=左篮、右键=右篮。
|
||||
- 验证:运行 `npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/services/useMocapInput.test.ts`,确认动作名负向测试、未知侧别负向测试和左右手横向轨迹测试通过。
|
||||
- 关联:`src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx`、`docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md`。
|
||||
|
||||
## 儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置
|
||||
|
||||
- 现象:`/child-motion-demo` 已经呈现绘本草地风格,但 `public/child-motion-demo/picture-book-grass-stage.png`、`picture-book-grass-floor.png`、`picture-book-ground-ring.png`、`picture-book-character-outline.png`、`picture-book-ui-panel.png` 或 `picture-book-ui-button.png` 不存在,Network 里对应图片返回 404,或运行 `npm run assets:child-motion-demo -- --live` 返回缺少 VectorEngine 配置。
|
||||
- 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 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` 或 `npm run assets:child-motion-demo -- --live --only ui-panel` 等小批量命令生成资源。透明资源的品红底源图写入 `tmp/child-motion-demo-assets/`,不要把源图或预览图放入 `public/child-motion-demo/` 作为正式资产。
|
||||
- 验证:生成后确认 `public/child-motion-demo/` 只保留页面引用的最终 PNG,重新打开 `/child-motion-demo` 可看到真实绘本草地背景、地面、圆环、角色轮廓和 UI 资源;`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`。
|
||||
|
||||
## 儿童动作 Demo 绘本资源变形先查用途拆分和透明后处理
|
||||
|
||||
- 现象:`/child-motion-demo` 背景风格正确,但底部草坪被拉成厚色块、顶部 HUD 或右下状态条像方形面板被横向拉伸,或旧 `picture-book-ui-panel.png` 与新资源叠在一起。
|
||||
- 原因:早期资源中 `picture-book-ui-panel.png` 是接近方形画布,`picture-book-grass-floor.png` 也含大量透明边界;若 CSS 用 `background-size: 100% 100%` 把同一资源强行铺成 HUD、状态条、开始面板或底部地板,就会出现变形和层叠观感。
|
||||
- 处理:使用 v2 用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v2.png`、`picture-book-character-outline-v2.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png`、`picture-book-ui-button-v2.png`;CSS 按资源比例等比缩放,底部草坪只覆盖下沿,HUD / 状态条 / 开始托盘分别引用各自资源。若只需修透明裁切或品红边,运行 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>`,不重新请求 image-2。
|
||||
- 验证:用横屏截图检查没有新旧资源叠加、没有方形面板拉成长条、角色和地面指示环不被前景草坪埋住;同时运行 `npm run check:encoding`。
|
||||
- 关联:`scripts/generate-child-motion-demo-assets.mjs`、`src/index.css`、`public/child-motion-demo/`、`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`。
|
||||
|
||||
Reference in New Issue
Block a user