Prune stale docs and update .hermes content
Delete a large set of outdated documentation (many files under docs/ and .hermes/plans/, including audits, design, prd, technical, planning, assets, and todos). Update and consolidate .hermes content: refresh shared-memory pages (decision-log, development-workflow, document-map, pitfalls, project-overview, team-conventions) and several skills/references under .hermes/skills. Also modify AGENTS.md, README.md, UI_CODING_STANDARD.md, docs/README.md and .encoding-check-ignore. Purpose: clean up stale planning/audit material and keep current hermes documentation and related top-level docs in sync.
This commit is contained in:
@@ -1,724 +0,0 @@
|
||||
# 埋点系统新增周、月、季、年维度映射表计划
|
||||
|
||||
## 目标
|
||||
|
||||
在 Genarrative 的埋点/统计系统中新增“周、月、季、年”维度映射表,让后续统计查询可以按不同时间粒度稳定聚合,而不是只依赖运行时临时计算日期范围。
|
||||
|
||||
本计划只做设计与落地步骤,不直接修改业务代码。
|
||||
|
||||
## 当前上下文与初步发现
|
||||
|
||||
1. 当前仓库根目录为 `/home/dsk/workspace/Genarrative`。
|
||||
2. 远端更新后已定位到当前真实埋点/任务系统:
|
||||
- 原始埋点表:`tracking_event`
|
||||
- 日聚合投影表:`tracking_daily_stat`
|
||||
- 任务配置表:`profile_task_config`
|
||||
- 任务进度表:`profile_task_progress`
|
||||
- 领奖记录表:`profile_task_reward_claim`
|
||||
3. 相关文件:
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- `server-rs/crates/module-runtime/src/domain.rs`
|
||||
- `server-rs/crates/module-runtime/src/application.rs`
|
||||
- `server-rs/crates/api-server/src/runtime_profile.rs`
|
||||
- `apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
|
||||
- `apps/admin-web/src/api/adminApiTypes.ts`
|
||||
- `apps/admin-web/src/config/trackingEventDefinitions.ts`
|
||||
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
|
||||
- `docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md`
|
||||
4. 当前 `tracking_event` 是明细表,`tracking_daily_stat` 是统一日汇总表,不按范围拆表;它通过 `event_key + scope_kind + scope_id + day_key` 区分不同聚合桶。
|
||||
5. 当前任务系统是“个人任务系统”,首版任务配置均面向用户维度;后台暴露“埋点范围”选择会导致运营误配。
|
||||
6. 已决定采用任务配置方案 B:
|
||||
- 后台任务配置不再让运营手动选择埋点范围。
|
||||
- 后端个人任务配置统一限制为 `RuntimeTrackingScopeKind::User`。
|
||||
- 若 API 收到非 `user` 的 `scopeKind`,应拒绝或兼容忽略但最终落库必须为 `User`;推荐直接拒绝并返回清晰错误。
|
||||
7. 已发现一个需要纳入本计划修复的语义问题:`profile_task_tracking_scope_id` 里 `RuntimeTrackingScopeKind::Work` 当前返回 `user_id`,这不符合 work 维度语义。即使个人任务暂不支持 work,也应避免错误映射继续存在。
|
||||
8. 当前日期桶使用北京时间自然日:`day_key = floor((occurred_at_micros + 8h) / 1d)`;周/月/季/年映射表应优先沿用这一业务日口径,除非产品另行确认。
|
||||
9. 如果新增正式后端表,需要同步:
|
||||
- 表定义
|
||||
- reducer/procedure
|
||||
- migration.rs
|
||||
- 生成绑定
|
||||
- spacetime-client facade
|
||||
- shared-contracts / API DTO,如有接口暴露
|
||||
|
||||
## 关键假设
|
||||
|
||||
在未定位现有埋点模块前,先按以下假设规划:
|
||||
|
||||
1. 当前已有某种事件明细表或统计事实表,例如:
|
||||
- `telemetry_event`
|
||||
- `analytics_event`
|
||||
- `metric_event`
|
||||
- `narrative_telemetry`
|
||||
- 或类似命名
|
||||
2. 新增的“映射表”用于把具体日期或事件时间映射到时间维度 bucket。
|
||||
3. 映射维度包括:
|
||||
- 周:week
|
||||
- 月:month
|
||||
- 季:quarter
|
||||
- 年:year
|
||||
4. 已明确选择“一张通用日期维度映射表”方案:`analytics_date_dimension`。
|
||||
5. 统计口径需要明确:
|
||||
- 周从周一还是周日开始
|
||||
- 是否使用 ISO week
|
||||
- 季度是自然季度还是财务季度
|
||||
- 时区使用 UTC 还是业务本地时区
|
||||
|
||||
## 推荐设计方向
|
||||
|
||||
### 方案 A:单一时间维度映射表,推荐
|
||||
|
||||
新增一张日历维度表,每一行对应一个自然日,并包含它归属的周、月、季、年。
|
||||
|
||||
表概念:
|
||||
|
||||
```text
|
||||
analytics_date_dimension
|
||||
```
|
||||
|
||||
建议字段:
|
||||
|
||||
```text
|
||||
date_key string 例如 2026-05-04
|
||||
calendar_date string 真实日期,按 YYYY-MM-DD 存储
|
||||
weekday u8 1-7 或 0-6,需要统一约定
|
||||
iso_week_key string 例如 2026-W19
|
||||
week_start_date_key string 例如 2026-05-04
|
||||
week_end_date_key string 例如 2026-05-10
|
||||
month_key string 例如 2026-05
|
||||
month_start_date_key string
|
||||
month_end_date_key string
|
||||
quarter_key string 例如 2026-Q2
|
||||
year_key string 例如 2026
|
||||
created_at timestamp/string
|
||||
updated_at timestamp/string
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 一张表即可支持日、周、月、季、年映射。
|
||||
- 便于后续新增半月、财年、节假日、自然周等维度。
|
||||
- 查询逻辑简单:事件日期 join/date_key 映射到目标粒度。
|
||||
- 数据量很小,按 20 年也只有约 7300 行。
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要在事件时间写入或统计查询时把 timestamp 归一为 date_key。
|
||||
- 如果要支持多时区,可能需要增加 timezone 字段或多套 calendar。
|
||||
|
||||
### 方案 B:四张独立映射表
|
||||
|
||||
分别新增:
|
||||
|
||||
```text
|
||||
analytics_week_dimension
|
||||
analytics_month_dimension
|
||||
analytics_quarter_dimension
|
||||
analytics_year_dimension
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 每个粒度表结构更纯粹。
|
||||
- 查询时可以直接针对目标粒度表。
|
||||
|
||||
缺点:
|
||||
|
||||
- 表更多,维护复杂。
|
||||
- 日期归属关系仍然需要额外处理。
|
||||
- 容易出现周/月/季/年口径漂移。
|
||||
|
||||
### 最终选择
|
||||
|
||||
本计划采用方案 A:单一 `analytics_date_dimension` 日期维表,而不是四张独立映射表。
|
||||
|
||||
如业务未来明确要求“周、月、季、年各自有独立映射表”,也应优先在日期维表基础上派生视图或物化派生表,而不是一开始拆成四张重复表。
|
||||
|
||||
## 后端设计建议
|
||||
|
||||
### 1. 明确埋点领域归属
|
||||
|
||||
先定位现有埋点模块。如果没有独立模块,建议新增或归入:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-analytics/
|
||||
```
|
||||
|
||||
或如果当前项目已有 telemetry 命名,则保持已有命名,例如:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-telemetry/
|
||||
```
|
||||
|
||||
领域层职责:
|
||||
|
||||
- 时间粒度定义
|
||||
- date_key/week_key/month_key/quarter_key/year_key 生成规则
|
||||
- 时间维度校验
|
||||
- 事件聚合查询输入的纯规则
|
||||
|
||||
不应包含:
|
||||
|
||||
- SpacetimeDB 表读写
|
||||
- Axum handler
|
||||
- HTTP response
|
||||
|
||||
### 2. SpacetimeDB 表设计
|
||||
|
||||
在 `spacetime-module` 中新增时间维度表。
|
||||
|
||||
建议表名:
|
||||
|
||||
```text
|
||||
analytics_date_dimension
|
||||
```
|
||||
|
||||
建议主键:
|
||||
|
||||
```text
|
||||
date_key
|
||||
```
|
||||
|
||||
建议索引:
|
||||
|
||||
```text
|
||||
iso_week_key
|
||||
month_key
|
||||
quarter_key
|
||||
year_key
|
||||
```
|
||||
|
||||
如果 SpacetimeDB 表定义已有统一命名规范,应按现有规范命名。
|
||||
|
||||
### 3. 初始化/补全 reducer
|
||||
|
||||
新增 reducer 或内部 procedure,用于生成指定日期范围内的维度数据。
|
||||
|
||||
建议能力:
|
||||
|
||||
```text
|
||||
seed_analytics_date_dimensions(start_date, end_date)
|
||||
ensure_analytics_date_dimension_for_date(date_key)
|
||||
ensure_analytics_date_dimensions_for_range(start_date, end_date)
|
||||
```
|
||||
|
||||
设计原则:
|
||||
|
||||
- 可幂等执行。
|
||||
- 已存在 date_key 时不重复插入。
|
||||
- 支持一次补一段日期。
|
||||
- 避免一次补太大范围导致事务过重。
|
||||
- 生产环境建议按年份或月份分批。
|
||||
|
||||
### 4. 事件表和映射表关系
|
||||
|
||||
如果事件表目前只有 timestamp,建议新增或计算出:
|
||||
|
||||
```text
|
||||
event_date_key
|
||||
```
|
||||
|
||||
可选策略:
|
||||
|
||||
1. 写入事件时同步写 `event_date_key`。
|
||||
2. 查询统计时从 timestamp 临时计算 date_key。
|
||||
3. 后台迁移为历史事件补 `event_date_key`。
|
||||
|
||||
推荐:
|
||||
|
||||
- 新事件写入时保存 `event_date_key`。
|
||||
- 历史事件通过批量迁移 reducer 分批补齐。
|
||||
|
||||
### 5. 聚合查询设计
|
||||
|
||||
支持按粒度查询时,API 或 facade 可以接收:
|
||||
|
||||
```text
|
||||
granularity = day | week | month | quarter | year
|
||||
start_date
|
||||
end_date
|
||||
metric/event_name
|
||||
filters
|
||||
```
|
||||
|
||||
内部根据粒度选择 bucket key:
|
||||
|
||||
```text
|
||||
day -> date_key
|
||||
week -> iso_week_key 或 week_key
|
||||
month -> month_key
|
||||
quarter -> quarter_key
|
||||
year -> year_key
|
||||
```
|
||||
|
||||
返回结构建议统一:
|
||||
|
||||
```text
|
||||
bucket_key
|
||||
bucket_label
|
||||
bucket_start_date
|
||||
bucket_end_date
|
||||
value
|
||||
```
|
||||
|
||||
## 可能涉及的文件
|
||||
|
||||
由于当前尚未定位明确埋点模块,以下是预计文件范围。
|
||||
|
||||
### 必查文件/目录
|
||||
|
||||
```text
|
||||
./Genarrative/server-rs/crates/
|
||||
./Genarrative/server-rs/crates/spacetime-module/src/
|
||||
./Genarrative/server-rs/crates/spacetime-client/src/
|
||||
./Genarrative/server-rs/crates/shared-contracts/src/
|
||||
./Genarrative/server-rs/crates/api-server/src/
|
||||
./Genarrative/packages/shared/src/contracts/
|
||||
./Genarrative/src/services/
|
||||
```
|
||||
|
||||
### 可能新增文件
|
||||
|
||||
如果采用 analytics 命名:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-analytics/src/domain.rs
|
||||
server-rs/crates/module-analytics/src/commands.rs
|
||||
server-rs/crates/module-analytics/src/application.rs
|
||||
server-rs/crates/module-analytics/src/errors.rs
|
||||
server-rs/crates/module-analytics/src/events.rs
|
||||
server-rs/crates/shared-contracts/src/analytics.rs
|
||||
server-rs/crates/spacetime-client/src/analytics.rs
|
||||
server-rs/crates/api-server/src/analytics.rs
|
||||
packages/shared/src/contracts/analytics.ts
|
||||
```
|
||||
|
||||
如果只是新增 SpacetimeDB 映射表且暂不暴露 API,则可能只需:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-module/src/**
|
||||
server-rs/crates/spacetime-module/src/migration.rs
|
||||
server-rs/crates/spacetime-client/src/** # 如果查询会被 api-server 使用
|
||||
```
|
||||
|
||||
## 详细实施步骤
|
||||
|
||||
### Step 1:复核现有埋点系统与任务配置链路
|
||||
|
||||
当前已定位真实链路,实施前再做一次只读复核,确认远端最新代码没有继续变化。
|
||||
|
||||
已知核心表:
|
||||
|
||||
```text
|
||||
tracking_event # 原始埋点明细
|
||||
tracking_daily_stat # 日聚合投影
|
||||
profile_task_config # 个人任务配置
|
||||
profile_task_progress # 个人任务进度
|
||||
profile_task_reward_claim # 领奖记录
|
||||
```
|
||||
|
||||
已知核心文件:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-module/src/runtime/profile.rs
|
||||
server-rs/crates/module-runtime/src/domain.rs
|
||||
server-rs/crates/module-runtime/src/application.rs
|
||||
server-rs/crates/api-server/src/runtime_profile.rs
|
||||
apps/admin-web/src/pages/AdminTaskConfigPage.tsx
|
||||
apps/admin-web/src/api/adminApiTypes.ts
|
||||
apps/admin-web/src/config/trackingEventDefinitions.ts
|
||||
docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
|
||||
docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md
|
||||
```
|
||||
|
||||
重点确认:
|
||||
|
||||
1. `tracking_event` 是否仍包含 `event_key/scope_kind/scope_id/day_key/user_id/occurred_at`。
|
||||
2. `tracking_daily_stat` 是否仍按 `event_key + scope_kind + scope_id + day_key` 生成 `stat_id`。
|
||||
3. `profile_task_config` 是否仍包含 `scope_kind` 和 `sort_order`。
|
||||
4. 后台 `AdminTaskConfigPage` 是否仍暴露“埋点范围”下拉。
|
||||
5. `profile_task_tracking_scope_id` 中 `Work => user_id` 的错误映射是否仍存在。
|
||||
|
||||
### Step 1.5:先收紧个人任务配置的埋点范围,采用方案 B
|
||||
|
||||
在做周/月/季/年维度映射前,先修正个人任务配置边界,避免后续在错误配置模型上继续扩展。
|
||||
|
||||
目标行为:
|
||||
|
||||
```text
|
||||
个人任务配置只支持用户维度埋点。
|
||||
后台页面不再展示“埋点范围”。
|
||||
后端不允许 profile_task_config 被写入 site/work/module 维度。
|
||||
```
|
||||
|
||||
建议实现:
|
||||
|
||||
1. 前端隐藏 `AdminTaskConfigPage` 的“埋点范围”选择。
|
||||
- 文件:`apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
|
||||
- 移除或隐藏:`scopeKinds` 下拉 UI。
|
||||
- 保存请求仍可兼容传 `scopeKind: 'user'`,避免一次性改动 API contract。
|
||||
2. 后端 upsert 校验 `scopeKind` 必须为 `RuntimeTrackingScopeKind::User`。
|
||||
- 文件:`server-rs/crates/api-server/src/runtime_profile.rs`
|
||||
- 或更底层:`server-rs/crates/module-runtime/src/domain.rs` / `server-rs/crates/spacetime-module/src/runtime/profile.rs` 的输入构造函数。
|
||||
- 推荐在领域输入构造处兜底校验,API 层返回清晰错误。
|
||||
3. 若暂不改 API DTO,则保持字段存在但限定值只能是 `user`。
|
||||
- 文件:`apps/admin-web/src/api/adminApiTypes.ts`
|
||||
- `AdminUpsertProfileTaskConfigRequest.scopeKind` 可保留,前端固定传 `user`。
|
||||
4. 更新后台埋点定义注册表的语义:
|
||||
- 文件:`apps/admin-web/src/config/trackingEventDefinitions.ts`
|
||||
- 当前每个 event definition 包含 `scopeKind`,如果个人任务统一 `user`,可以保留为只读内部默认值;但不要让运营在页面改。
|
||||
5. 更新技术文档:
|
||||
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
|
||||
- 明确“个人任务首版只支持用户维度埋点,后台不开放 scope_kind 配置”。
|
||||
|
||||
验收:
|
||||
|
||||
```text
|
||||
后台任务配置页不再出现“埋点范围”选择。
|
||||
保存 daily_login 后落库 scope_kind 仍为 User。
|
||||
直接调用后台 upsert 接口传 site/work/module 时被拒绝,或最终不会落库为非 User;推荐拒绝。
|
||||
```
|
||||
|
||||
### Step 1.6:修复 Work 范围错误返回 user_id 的语义问题
|
||||
|
||||
当前函数:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-module/src/runtime/profile.rs
|
||||
profile_task_tracking_scope_id(user_id, config)
|
||||
```
|
||||
|
||||
当前问题:
|
||||
|
||||
```rust
|
||||
RuntimeTrackingScopeKind::Work => user_id.to_string()
|
||||
```
|
||||
|
||||
这会把 work 维度错误映射为用户 ID。虽然个人任务将限制为 User,但保留这个分支会误导后续扩展。
|
||||
|
||||
推荐修复策略:
|
||||
|
||||
1. 对个人任务进度计算来说,`Work` 不应进入该函数。
|
||||
2. 将 `profile_task_tracking_scope_id` 改为返回 `Result<String, String>` 或 `Option<String>`。
|
||||
3. 对不支持的范围返回错误,而不是伪造 scope_id:
|
||||
|
||||
```text
|
||||
Site -> "site",如果个人任务仍不允许 site,则上游先拒绝
|
||||
Module -> "profile",如果个人任务仍不允许 module,则上游先拒绝
|
||||
User -> user_id
|
||||
Work -> error: personal task progress does not support work scope without work_id
|
||||
```
|
||||
|
||||
更严格的推荐:
|
||||
|
||||
```text
|
||||
个人任务链路只接受 User。
|
||||
Work/Site/Module 在 profile_task_progress_count 前就被拒绝。
|
||||
profile_task_tracking_scope_id 只保留 User 分支,或者非 User 返回错误。
|
||||
```
|
||||
|
||||
需要同步调整调用点:
|
||||
|
||||
```text
|
||||
profile_task_progress_count
|
||||
refresh_profile_task_progress
|
||||
build_profile_task_center_snapshot
|
||||
claim_profile_task_reward
|
||||
```
|
||||
|
||||
避免因为函数返回 `Result` 后调用链未处理错误。
|
||||
|
||||
验收:
|
||||
|
||||
```text
|
||||
不存在 Work => user_id 的映射。
|
||||
个人任务配置非 User 时不会静默算出错误进度。
|
||||
相关测试覆盖:User 正常;Work/Site/Module 被拒绝。
|
||||
|
||||
```
|
||||
|
||||
### Step 2:确定时间口径
|
||||
|
||||
必须先确认:
|
||||
|
||||
1. 周维度是否使用 ISO week。
|
||||
2. 周开始日是周一还是周日。
|
||||
3. 月/季/年是否按自然日历。
|
||||
4. 统计时区是 UTC、服务器时区,还是用户本地时区。
|
||||
5. 跨年周如何命名,例如 `2025-W01` 可能开始于 2024 年末。
|
||||
|
||||
推荐默认:
|
||||
|
||||
```text
|
||||
时区:UTC,除非产品明确要求中国时区
|
||||
周:ISO week,周一开始
|
||||
月:自然月
|
||||
季:自然季度
|
||||
年:自然年
|
||||
```
|
||||
|
||||
如果业务面向国内用户,建议考虑:
|
||||
|
||||
```text
|
||||
时区:Asia/Shanghai
|
||||
周:周一开始
|
||||
```
|
||||
|
||||
### Step 3:设计 date dimension 表
|
||||
|
||||
设计字段和 key 格式,写入技术文档。
|
||||
|
||||
建议 key 格式:
|
||||
|
||||
```text
|
||||
date_key: 2026-05-04
|
||||
week_key: 2026-W19
|
||||
month_key: 2026-05
|
||||
quarter_key: 2026-Q2
|
||||
year_key: 2026
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- `week_key` 建议使用 ISO week-year,不一定等于 calendar year。
|
||||
- `quarter_key` 使用 calendar year。
|
||||
|
||||
### Step 4:新增领域纯函数
|
||||
|
||||
在领域层或 shared-kernel 中实现纯函数:
|
||||
|
||||
```text
|
||||
resolve_date_dimension(date, timezone) -> AnalyticsDateDimension
|
||||
resolve_bucket_key(date_dimension, granularity) -> String
|
||||
resolve_bucket_range(bucket_key, granularity) -> start/end date
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 有单元测试。
|
||||
- 覆盖跨年周。
|
||||
- 覆盖闰年 2 月。
|
||||
- 覆盖季度边界。
|
||||
|
||||
### Step 5:新增 SpacetimeDB 表
|
||||
|
||||
在 `spacetime-module` 中新增表。
|
||||
|
||||
遵守约束:
|
||||
|
||||
- 新增表通常安全。
|
||||
- 不修改已有表字段。
|
||||
- 如果必须给已有事件表加 `event_date_key`,必须加在表定义末尾并提供 default。
|
||||
- 若要补历史数据,使用 reducer 分批迁移。
|
||||
|
||||
### Step 6:新增 seed/ensure reducer
|
||||
|
||||
新增幂等 reducer:
|
||||
|
||||
```text
|
||||
seed_analytics_date_dimensions(start_date, end_date)
|
||||
ensure_analytics_date_dimension_for_date(date_key)
|
||||
```
|
||||
|
||||
验证点:
|
||||
|
||||
- 重复执行不会重复插入。
|
||||
- 日期范围非法时返回稳定错误。
|
||||
- 单次范围过大时拒绝或分页。
|
||||
|
||||
### Step 7:接入事件写入链路
|
||||
|
||||
如果现有事件写入链路存在,新增:
|
||||
|
||||
```text
|
||||
event_date_key
|
||||
```
|
||||
|
||||
策略:
|
||||
|
||||
- 新事件写入时同步计算并保存。
|
||||
- 写入前确保对应 date dimension 存在。
|
||||
- 历史事件通过迁移 reducer 补齐。
|
||||
|
||||
如果暂不改事件表,也可以在查询阶段临时映射,但性能和一致性较差。
|
||||
|
||||
### Step 8:接入聚合查询
|
||||
|
||||
如已有统计接口,扩展请求参数:
|
||||
|
||||
```text
|
||||
granularity: day | week | month | quarter | year
|
||||
```
|
||||
|
||||
查询逻辑改为:
|
||||
|
||||
```text
|
||||
事件/事实表
|
||||
→ event_date_key
|
||||
→ analytics_date_dimension
|
||||
→ 取对应 bucket key
|
||||
→ group by bucket key
|
||||
```
|
||||
|
||||
返回 bucket 时包含:
|
||||
|
||||
```text
|
||||
bucket_key
|
||||
bucket_start_date
|
||||
bucket_end_date
|
||||
value
|
||||
```
|
||||
|
||||
### Step 9:补 shared contracts 和前端 contracts
|
||||
|
||||
如果有 API 暴露,需要补:
|
||||
|
||||
```text
|
||||
server-rs/crates/shared-contracts/src/analytics.rs
|
||||
packages/shared/src/contracts/analytics.ts
|
||||
```
|
||||
|
||||
建议 DTO:
|
||||
|
||||
```text
|
||||
AnalyticsGranularity = day | week | month | quarter | year
|
||||
AnalyticsBucketMetric
|
||||
AnalyticsMetricQueryRequest
|
||||
AnalyticsMetricQueryResponse
|
||||
```
|
||||
|
||||
### Step 10:补测试
|
||||
|
||||
测试范围:
|
||||
|
||||
1. 领域日期映射测试
|
||||
2. SpacetimeDB reducer 幂等测试
|
||||
3. API 查询维度测试
|
||||
4. 历史事件迁移测试,如涉及
|
||||
5. 跨边界日期测试
|
||||
6. 个人任务配置 scope 限制测试
|
||||
7. `Work => user_id` 错误映射回归测试
|
||||
|
||||
重点用例:
|
||||
|
||||
```text
|
||||
2024-02-29 闰年
|
||||
2025-12-29 ISO week 可能属于 2026-W01
|
||||
2026-01-01 跨年周
|
||||
2026-03-31 Q1 结束
|
||||
2026-04-01 Q2 开始
|
||||
2026-12-31 年末
|
||||
```
|
||||
|
||||
任务配置重点用例:
|
||||
|
||||
```text
|
||||
admin upsert daily_login + scopeKind=user -> 成功
|
||||
admin upsert daily_login + scopeKind=site -> 失败,错误信息说明个人任务仅支持 user
|
||||
admin upsert daily_login + scopeKind=module -> 失败
|
||||
admin upsert daily_login + scopeKind=work -> 失败
|
||||
任务中心读取 daily_login -> 按 User + 当前 user_id 查询进度
|
||||
代码中不存在 Work => user_id 的静默映射
|
||||
```
|
||||
|
||||
## 测试与验证命令
|
||||
|
||||
具体命令需在定位模块后确认。初步建议:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
```
|
||||
|
||||
后端如涉及 Rust:
|
||||
|
||||
```text
|
||||
cargo test -p module-analytics
|
||||
cargo test -p spacetime-module
|
||||
cargo test -p api-server
|
||||
```
|
||||
|
||||
涉及 API smoke:
|
||||
|
||||
```text
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
然后验证:
|
||||
|
||||
```text
|
||||
GET /healthz
|
||||
```
|
||||
|
||||
涉及 SpacetimeDB schema:
|
||||
|
||||
- 需要生成绑定。
|
||||
- 需要确认 migration.rs 对齐。
|
||||
- 需要确认 publish 不触发不安全 schema 变更。
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
### 风险 1:个人任务 scope_kind 被误配置导致进度异常
|
||||
|
||||
当前个人任务系统本质上按用户维度计算进度。如果允许运营配置 `site/work/module`,可能导致任务进度查错 `tracking_daily_stat` 聚合桶,出现任务永远不可领取或错误可领取。
|
||||
|
||||
缓解:
|
||||
|
||||
```text
|
||||
采用方案 B:后台隐藏埋点范围,后端限制个人任务配置只能写入 User。
|
||||
```
|
||||
|
||||
### 风险 2:Work 维度缺少 work_id,上游却静默用 user_id 代替
|
||||
|
||||
当前 `profile_task_tracking_scope_id` 中 `Work => user_id` 是错误语义。若后续扩展作品任务,会把作品维度统计错误映射到用户维度。
|
||||
|
||||
缓解:
|
||||
|
||||
```text
|
||||
移除 Work => user_id 映射;非 User 的个人任务配置应被拒绝。未来做作品任务时新增明确 work_id 来源和任务类型。
|
||||
```
|
||||
|
||||
### 风险 3:时区口径影响统计结果
|
||||
|
||||
周/月/季/年映射对时区敏感。当前日桶使用北京时间自然日:`floor((occurred_at_micros + 8h) / 1d)`。新增映射表应明确沿用北京时间业务日,还是切换为 UTC/用户本地时区。
|
||||
|
||||
### 风险 4:ISO week 跨年
|
||||
|
||||
ISO week-year 与自然年不同。若前端展示按自然年理解,可能产生认知差异。
|
||||
|
||||
### 风险 5:修改已有事件表可能触发 SpacetimeDB 迁移限制
|
||||
|
||||
如果已有事件表需要新增字段:
|
||||
|
||||
- 字段必须加末尾。
|
||||
- 必须提供 default。
|
||||
- 历史数据要分批迁移。
|
||||
|
||||
### 风险 5:表设计过早绑定单一业务
|
||||
|
||||
建议用通用 date dimension,而不是为某个单一埋点写死周/月/季/年表,避免后续复用困难。
|
||||
|
||||
## 待确认问题
|
||||
|
||||
1. 周维度使用 ISO week 还是自然周?周一开始还是周日开始?
|
||||
2. 周/月/季/年映射是否沿用当前北京时间业务日口径?
|
||||
3. 这个映射表服务的是所有埋点,还是只服务个人任务/运营后台统计?
|
||||
4. 是否需要 API 暴露这些映射关系,还是只用于后端聚合?
|
||||
5. 是否需要回填历史事件?历史数据规模多大?
|
||||
6. 未来是否会存在非个人任务,例如整站任务、模块任务、作品任务?如果会,应另行设计任务类型和 `scope_id` 来源,不应复用当前个人任务配置页直接开放 scope。
|
||||
|
||||
## 建议结论
|
||||
|
||||
优先采用“一张通用日期维度映射表”的设计:
|
||||
|
||||
```text
|
||||
analytics_date_dimension
|
||||
```
|
||||
|
||||
通过字段同时提供:
|
||||
|
||||
```text
|
||||
day / week / month / quarter / year
|
||||
```
|
||||
|
||||
后续统计按 `granularity` 选择 bucket key 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。
|
||||
@@ -1,271 +0,0 @@
|
||||
# 邀请码有效期与后台二次确认实施计划
|
||||
|
||||
> **For Hermes:** 按 plan 模式,仅输出并保存实施计划,不直接改业务代码。
|
||||
|
||||
**Goal:** 为邀请码新增开始日期与截止日期,并让后台所有会修改数据的操作在提交前增加二次确认,降低误操作风险。
|
||||
|
||||
**Architecture:**
|
||||
邀请码仍作为“用户稳定邀请身份码”保留,不做停用删除;在数据层增加 `starts_at` / `expires_at`,前台填写邀请码时按时间窗校验,后台列表与编辑页展示状态。后台所有写操作统一先弹二次确认,再真正调用 API,避免对兑换码、邀请码、任务配置等管理动作误触发。
|
||||
|
||||
**Tech Stack:**
|
||||
Rust / SpacetimeDB / Axum / shared-contracts / TS + React 的 admin-web。
|
||||
|
||||
---
|
||||
|
||||
## 当前上下文
|
||||
|
||||
- 邀请码当前只有 `user_id`、`invite_code`、`metadata_json`、`created_at`、`updated_at`,没有状态字段。
|
||||
- 目前后台存在邀请码管理入口,但没有停用能力,也没有有效期概念。
|
||||
- 邀请码用于 `redeem_profile_referral_invite_code` 时的实时校验,适合增加“时间窗”而不是“禁用删除”。
|
||||
- 后台已存在兑换码、任务配置等可写操作;本次要求把所有后台操作统一加二次确认,包括新增、编辑、禁用、删除等写入口。
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **邀请码不做软删除**:保留历史记录和邀请链路。
|
||||
2. **有效期由时间窗推导**:
|
||||
- `starts_at` 为空表示立即生效。
|
||||
- `expires_at` 为空表示长期有效。
|
||||
3. **前台只拒绝新绑定**:已绑定关系不回溯修改。
|
||||
4. **后台写操作统一确认**:所有会触发 POST / PATCH / DELETE 的管理动作,在真正提交前必须弹出二次确认。
|
||||
5. **尽量少改接口语义**:优先在现有 admin upsert/list 体系内扩展字段,而不是新增一套并行 API。
|
||||
|
||||
---
|
||||
|
||||
## 方案概要
|
||||
|
||||
### 邀请码时间窗
|
||||
|
||||
新增字段:
|
||||
- `starts_at: Option<Timestamp>`
|
||||
- `expires_at: Option<Timestamp>`
|
||||
|
||||
校验规则:
|
||||
- 当前时间 `< starts_at`:返回“邀请码未生效”
|
||||
- 当前时间 `>= expires_at`:返回“邀请码已过期”
|
||||
- 其他情况允许填写
|
||||
|
||||
建议把状态展示为:
|
||||
- 未生效
|
||||
- 有效
|
||||
- 已过期
|
||||
- 长期有效(两个字段都为空或仅无截止)
|
||||
|
||||
### 后台二次确认
|
||||
|
||||
对 admin-web 所有管理动作统一加确认弹窗/对话框,至少覆盖:
|
||||
- 兑换码新增/更新
|
||||
- 兑换码停用
|
||||
- 邀请码新增/更新
|
||||
- 任务配置新增/更新
|
||||
- 任务配置停用
|
||||
- 其他后续新增的后台写操作
|
||||
|
||||
确认文案要求:
|
||||
- 显示对象标识(如 code / inviteCode / taskId)
|
||||
- 显示操作类型(新增 / 更新 / 停用)
|
||||
- 明确提醒“该操作会立即影响线上数据”
|
||||
- 允许取消返回,不调用 API
|
||||
|
||||
---
|
||||
|
||||
## 预期修改文件
|
||||
|
||||
### 1. 服务端领域与契约
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- `ProfileInviteCode` 表结构新增开始/截止字段
|
||||
- 邀请码 upsert 逻辑写入时间窗
|
||||
- 邀请码 redeem 逻辑增加时间窗校验
|
||||
- 邀请中心快照补充时间窗/状态
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- 兼容旧表数据,给旧邀请码补默认空值
|
||||
- `server-rs/crates/shared-contracts/src/runtime*.rs` 或对应生成/手写契约文件
|
||||
- `AdminUpsertProfileInviteCodeRequest` 扩展字段
|
||||
- `ProfileInviteCodeAdminResponse` 扩展字段
|
||||
- 如需要,增加时间窗相关状态枚举或派生字段
|
||||
- `server-rs/crates/spacetime-client/src/module_bindings/*`
|
||||
- 重新生成 bindings
|
||||
- mapper 补齐新字段
|
||||
|
||||
### 2. API Server
|
||||
- `server-rs/crates/api-server/src/runtime_profile.rs`
|
||||
- 接收/转发邀请码时间窗参数
|
||||
- 返回新增字段给后台
|
||||
- 如需要,调整校验错误文案
|
||||
- `server-rs/crates/api-server/src/app.rs`
|
||||
- 若有新路由或错误码需挂接,在此统一登记
|
||||
|
||||
### 3. Admin Web
|
||||
- `apps/admin-web/src/api/adminApiTypes.ts`
|
||||
- 增加邀请码时间窗字段
|
||||
- 如有需要,增加后台操作请求结构字段
|
||||
- `apps/admin-web/src/api/adminApiClient.ts`
|
||||
- 透传新的请求/响应字段
|
||||
- `apps/admin-web/src/app/adminRoutes.ts`
|
||||
- 不一定需要改,但如果新增独立页面/子面板,需要在此登记
|
||||
- `apps/admin-web/src/styles/admin.css`
|
||||
- 确认弹窗与时间窗展示样式
|
||||
- `apps/admin-web/src/**` 实际管理页面组件
|
||||
- 邀请码编辑表单
|
||||
- 邀请码列表状态展示
|
||||
- 所有写操作前的二次确认弹窗
|
||||
|
||||
### 4. 文档
|
||||
- `docs/technical/` 或 `docs/design/`
|
||||
- 补一份邀请码时间窗与后台确认交互说明
|
||||
- 如现有文档已经覆盖后台管理规范,则优先补充现有文档,不重复造新说明页
|
||||
|
||||
---
|
||||
|
||||
## 分步实施计划
|
||||
|
||||
### Task 1: 明确数据模型与契约扩展
|
||||
**Objective:** 定义邀请码开始/截止日期字段及其在响应中的展示方式。
|
||||
|
||||
**要点:**
|
||||
- 确认字段名采用 `starts_at` / `expires_at`,避免与现有字段语义冲突。
|
||||
- 确认时间类型统一用 `Timestamp` / 毫秒微秒整数转换策略。
|
||||
- 明确返回给后台的字段是否需要附带派生状态(如 `status`)。
|
||||
|
||||
**产出:**
|
||||
- 契约字段定义
|
||||
- 状态枚举/派生规则
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 更新 SpacetimeDB 表与迁移
|
||||
**Objective:** 让邀请码表可保存有效期,并兼容旧数据。
|
||||
|
||||
**要点:**
|
||||
- 修改 `ProfileInviteCode` 表结构。
|
||||
- 更新迁移逻辑,旧记录默认无开始/截止。
|
||||
- 检查是否需要补充索引或查询辅助字段。
|
||||
|
||||
**验证:**
|
||||
- 旧数据能正常读取。
|
||||
- 新数据能写入开始/截止。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 实现邀请填写时的时间窗校验
|
||||
**Objective:** 在邀请码被填写时正确拒绝未生效或已过期的邀请码。
|
||||
|
||||
**要点:**
|
||||
- 在 `redeem_profile_referral_invite_code_record` 内增加开始/截止校验。
|
||||
- 保持“自己的邀请码不能填”“邀请码不存在”等原有错误优先级清晰。
|
||||
- 保留历史绑定关系不受影响。
|
||||
|
||||
**验证:**
|
||||
- 未到开始时间时返回明确错误。
|
||||
- 超过截止时间时返回明确错误。
|
||||
- 正常区间可绑定成功。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 扩展后台邀请码管理接口
|
||||
**Objective:** 让后台可以创建/修改邀请码时间窗,并在列表中查看状态。
|
||||
|
||||
**要点:**
|
||||
- 扩展 `AdminUpsertProfileInviteCodeRequest`。
|
||||
- 扩展 `ProfileInviteCodeAdminResponse`。
|
||||
- `api-server` 接口负责接收新字段并转发。
|
||||
- 列表接口返回可读时间字段与状态。
|
||||
|
||||
**验证:**
|
||||
- 后台表单提交后,返回结果包含时间窗信息。
|
||||
- 列表页能看到状态与时间。
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 给后台所有写操作加二次确认
|
||||
**Objective:** 统一拦截所有后台写动作,避免误点直接生效。
|
||||
|
||||
**覆盖范围建议:**
|
||||
- 邀请码新增/更新
|
||||
- 兑换码新增/更新/停用
|
||||
- 任务配置新增/更新/停用
|
||||
- 后续新增的管理写操作
|
||||
|
||||
**实现要求:**
|
||||
- 在真正调用 API 之前弹出确认框。
|
||||
- 确认框需要展示对象名、操作类型、影响范围。
|
||||
- 取消后不发送请求。
|
||||
- 尽量抽象出通用确认组件/通用 action 包装函数,避免每个页面重复写。
|
||||
|
||||
**验证:**
|
||||
- 点击“保存”不会直接提交,需先确认。
|
||||
- 点击“取消”不会发请求。
|
||||
- 所有后台写入口行为一致。
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 补充文档与交互说明
|
||||
**Objective:** 把新规则写进项目文档,避免后续实现偏差。
|
||||
|
||||
**要点:**
|
||||
- 记录邀请码时间窗语义。
|
||||
- 记录后台二次确认规范。
|
||||
- 说明哪些动作属于“必须确认”的写操作。
|
||||
|
||||
---
|
||||
|
||||
## 测试与验证
|
||||
|
||||
### 服务端
|
||||
- 邀请码时间窗单测 / 集成测试
|
||||
- 邀请码 redeem 流程回归测试
|
||||
- 旧数据兼容测试
|
||||
|
||||
### API / 前端
|
||||
- 管理后台列表展示正确
|
||||
- 表单提交能回传新字段
|
||||
- 二次确认取消后不请求接口
|
||||
- 二次确认确认后正常提交
|
||||
|
||||
### 推荐验证命令
|
||||
- 视项目现有脚本执行对应后端测试
|
||||
- 前端按 admin-web 构建/测试脚本验证
|
||||
- 如涉及生成绑定,先确认生成产物无漏字段
|
||||
|
||||
---
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
1. **时间字段格式不统一**
|
||||
- 风险:前后端对时间单位理解不一致。
|
||||
- 处理:在契约层明确是 ISO 字符串还是微秒整数,并全链路统一。
|
||||
|
||||
2. **后台“所有操作”范围过大**
|
||||
- 风险:遗漏某些写入口。
|
||||
- 处理:先枚举现有写 API,再做统一确认封装。
|
||||
|
||||
3. **邀请码过期后历史链接解释成本**
|
||||
- 风险:用户误以为历史邀请码失效影响已绑定关系。
|
||||
- 处理:文案明确“仅影响新填写,不影响已绑定记录”。
|
||||
|
||||
4. **契约与生成绑定联动较多**
|
||||
- 风险:字段变更后生成文件数量较多。
|
||||
- 处理:先改源契约与服务端,再统一重生成 bindings。
|
||||
|
||||
---
|
||||
|
||||
## 待确认问题
|
||||
|
||||
1. `starts_at` / `expires_at` 在接口里要返回 **ISO 字符串** 还是 **微秒整数**?
|
||||
2. 后台二次确认是否统一用一个全局弹窗组件,还是页面级本地实现?
|
||||
3. 邀请码列表是否需要直接展示“状态标签”还是只展示时间字段由前端推导?
|
||||
4. 现有后台所有写操作里,是否还要覆盖调试类接口,还是仅覆盖业务管理接口?
|
||||
|
||||
---
|
||||
|
||||
## 建议执行顺序
|
||||
|
||||
1. 先确认时间字段格式与确认弹窗范围。
|
||||
2. 再改服务端契约与迁移。
|
||||
3. 再改 redeem 校验与后台接口。
|
||||
4. 最后统一改 admin-web 的二次确认与表单展示。
|
||||
|
||||
---
|
||||
|
||||
**结论:** 这是一个适合分阶段落地的改动,建议先做“邀请码时间窗 + 后台统一二次确认”的基础能力,再补交互细节。
|
||||
@@ -1,584 +0,0 @@
|
||||
# 我的页签反馈入口与反馈页 Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在平台“我的”页签中新增“反馈”入口,点击后进入独立反馈路由,并按用户提供的参考图落地反馈页面 UI。
|
||||
|
||||
**Architecture:** 复用现有前端单页路由体系:`SelectionStage` 负责页面阶段,`appPageRoutes.ts` 负责 URL 映射,`PlatformEntryFlowShellImpl` 负责按阶段渲染视图。“我的”页签只增加一个入口回调,不在当前面板下方展开内容;反馈页作为独立页面组件挂到新阶段。首版先做前端静态表单与本地提交成功态,不新增后端表结构或 SpacetimeDB 写入,除非产品补充明确要求持久化反馈。
|
||||
|
||||
**Tech Stack:** React 19、TypeScript、Tailwind utility class、lucide-react、现有 Genarrative 平台入口组件体系。
|
||||
|
||||
---
|
||||
|
||||
## Current context / assumptions
|
||||
|
||||
## Reference image
|
||||
|
||||

|
||||
|
||||
参考图是一张移动端“帮助与反馈”页面,视觉和信息结构如下:
|
||||
|
||||
- 页面整体:浅灰背景,白色圆角卡片,黑/深灰标题文字,浅灰 placeholder,蓝色主按钮与蓝色文本链接。
|
||||
- 顶部栏:白色导航/header,左侧为小 home 图标,中间标题为“帮助与反馈”,右侧为胶囊形更多/控制区。项目实现时可按现有平台导航规范简化为返回按钮 + 居中标题;若需要完全贴近图片,可使用 home 图标作为返回到“我的”页签的按钮。
|
||||
- 内容区 section label:左上灰色文字“反馈问题”。
|
||||
- 第一张表单卡:标题“问题描述”,大文本输入区域,placeholder 为“请填写10个字以上的问题描述以便我们提供更好的帮助,温馨提醒您请勿填写身份证号等个人隐私信息。”,右下角字数统计“0/200”。
|
||||
- 第二张表单卡:标题“上传凭证(提供问题截图)”,左侧虚线边框上传方块,内含图片/上传 + 加号图标,文字“上传凭证”“(最多四张)”。
|
||||
- 第三张表单卡:标题“联系电话”,placeholder 为“选填,如您填写则将会同步开发者与您联系”。
|
||||
- 底部操作:大号蓝色圆角按钮“提交”,下方居中蓝色链接“查看反馈与投诉记录”。
|
||||
|
||||
实现约束:
|
||||
|
||||
- 反馈页面应命名为“帮助与反馈”,但“我的”页签入口可显示为“反馈”或“帮助与反馈”,优先以清爽短入口为准。
|
||||
- 问题描述最少 10 个字、最多 200 个字,并实时显示 `当前字数/200`。
|
||||
- 上传凭证首版如不接后端,可先支持前端选择/预览最多 4 张图片,提交时仅进入成功态;如无法快速安全实现预览,可先保留上传占位并在文档中标注待接入。
|
||||
- 联系电话为选填。
|
||||
- “查看反馈与投诉记录”首版无后端记录时可以先禁用、隐藏,或点击后给出轻量提示;若保留可见,应在计划/PRD 标明记录页不在首版范围。
|
||||
|
||||
1. 当前工作区是 `/home/dsk/workspace/Genarrative/.worktrees/hermes-19e77eb0`,不要额外拼接 `Genarrative/`。
|
||||
2. 平台首页复用 `src/components/rpg-entry/RpgEntryHomeView.tsx`;`src/components/platform-entry/PlatformEntryHomeView.tsx` 只是 re-export。
|
||||
3. “我的”页签的常用功能区域位于 `src/components/rpg-entry/RpgEntryHomeView.tsx:3958-4000`,现有入口包括“每日任务 / 邀请好友 / 填邀请码 / 玩家社区”。
|
||||
4. 当前页面阶段类型位于 `src/components/platform-entry/platformEntryTypes.ts:16-38`;路由映射位于 `src/routing/appPageRoutes.ts:7-27`。
|
||||
5. `src/App.tsx:60-63` 调用 `pushAppHistoryPath(resolvePathForSelectionStage(stage))`,所以新增阶段必须同步 `APP_STAGE_ROUTES`。
|
||||
6. `PlatformEntryFlowShellImpl` 在 `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx:5081+` 根据 `selectionStage` 渲染不同页面,平台首页在 `selectionStage === 'platform'` 分支。
|
||||
7. 参考图片已保存到 `.hermes/plans/assets/profile-feedback-reference-2026-05-08.png`,计划与实现均以该图片内容为主要 UI 依据。
|
||||
8. 按项目约束,工程修改需同步文档;若没有更具体 PRD,需要先补一份简洁落地文档到 `docs/`。
|
||||
|
||||
## Proposed approach
|
||||
|
||||
新增一个轻量前端反馈页面阶段:
|
||||
|
||||
- 路由:`/profile/feedback`
|
||||
- 阶段:`profile-feedback`
|
||||
- 组件:`src/components/platform-entry/PlatformFeedbackView.tsx`
|
||||
- “我的”页签入口:在常用功能区增加“反馈”按钮,点击调用新 prop `onOpenFeedback`。
|
||||
- 页面行为:
|
||||
- 顶部返回按钮返回 `platform` 阶段,并切回 `profile` 页签。
|
||||
- 未登录用户点击入口时,优先弹登录;如果产品允许匿名反馈,可改为允许进入。
|
||||
- 表单字段首版只在前端维护:问题描述、上传凭证图片、联系电话。
|
||||
- 提交后显示成功态,不做 API 请求;后续如要持久化,再补 `shared-contracts + api-server + SpacetimeDB` 方案。
|
||||
|
||||
## Step-by-step plan
|
||||
|
||||
### Task 1: 补充反馈页落地文档
|
||||
|
||||
**Objective:** 先把反馈入口和页面边界写清楚,避免编码时需求漂移。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`
|
||||
|
||||
**Step 1: 新建 PRD 文档**
|
||||
|
||||
写入内容建议包含:
|
||||
|
||||
```markdown
|
||||
# 我的页签反馈入口 PRD
|
||||
|
||||
## 目标
|
||||
- 在“我的”页签提供反馈入口。
|
||||
- 点击入口进入独立反馈路由 `/profile/feedback`。
|
||||
- 反馈页移动端优先,桌面端居中卡片展示。
|
||||
|
||||
## 首版范围
|
||||
- 前端表单:问题描述、上传凭证占位/前端图片预览、联系电话。
|
||||
- 问题描述 10-200 字,显示实时字数统计。
|
||||
- 提交后显示成功态。
|
||||
- 不新增后端存储,不修改 SpacetimeDB 表结构。
|
||||
|
||||
## 交互
|
||||
- 已登录用户:点击“反馈”进入反馈页。
|
||||
- 未登录用户:点击入口触发登录弹窗。
|
||||
- 返回:回到平台首页并定位“我的”页签。
|
||||
|
||||
## UI
|
||||
- 以 `.hermes/plans/assets/profile-feedback-reference-2026-05-08.png` 为准,落地“帮助与反馈”移动端表单。
|
||||
- 不在 UI 中堆叠说明性长文案。
|
||||
- 入口是独立页面导航,不在“我的”面板下方展开。
|
||||
|
||||
## 验收
|
||||
- `/profile/feedback` 可被浏览器前进/后退访问。
|
||||
- “我的”页签反馈入口可进入该路由。
|
||||
- 移动端和桌面端均不溢出。
|
||||
- `npm run check:encoding`、`npm run typecheck` 通过。
|
||||
```
|
||||
|
||||
**Step 2: 验证文档编码**
|
||||
|
||||
Run: `npm run check:encoding`
|
||||
|
||||
Expected: PASS,无中文编码错误。
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md
|
||||
git commit -m "docs: add profile feedback entry prd"
|
||||
```
|
||||
|
||||
### Task 2: 扩展页面阶段与路由映射
|
||||
|
||||
**Objective:** 让 `/profile/feedback` 成为主应用可识别的独立路由。
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- Modify: `src/routing/appPageRoutes.ts`
|
||||
|
||||
**Step 1: 修改 SelectionStage 类型**
|
||||
|
||||
在 `SelectionStage` union 中追加:
|
||||
|
||||
```ts
|
||||
| 'profile-feedback'
|
||||
```
|
||||
|
||||
推荐放在 `'platform'` 附近或末尾,保持字面量清晰。
|
||||
|
||||
**Step 2: 修改 STAGE_ROUTE_ENTRIES**
|
||||
|
||||
在 `src/routing/appPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` 中追加:
|
||||
|
||||
```ts
|
||||
['profile-feedback', '/profile/feedback'],
|
||||
```
|
||||
|
||||
建议放在 `['platform', '/']` 后面,表示平台个人页子路由。
|
||||
|
||||
**Step 3: 验证类型推导**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
|
||||
Expected: 若还未创建渲染组件,可能只通过路由类型;若出现 exhaustive 相关错误,留到后续任务处理。
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/platform-entry/platformEntryTypes.ts src/routing/appPageRoutes.ts
|
||||
git commit -m "feat: add profile feedback route stage"
|
||||
```
|
||||
|
||||
### Task 3: 新建反馈页面组件
|
||||
|
||||
**Objective:** 创建移动端优先的独立反馈页面。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/platform-entry/PlatformFeedbackView.tsx`
|
||||
|
||||
**Step 1: 创建组件 props**
|
||||
|
||||
组件接口建议:
|
||||
|
||||
```ts
|
||||
export type PlatformFeedbackViewProps = {
|
||||
onBack: () => void;
|
||||
onSubmit?: (payload: PlatformFeedbackPayload) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type PlatformFeedbackPayload = {
|
||||
description: string;
|
||||
contactPhone: string;
|
||||
evidenceFiles: File[];
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: 实现 UI 状态**
|
||||
|
||||
使用 `useState` 管理:
|
||||
|
||||
- `description`
|
||||
- `contactPhone`
|
||||
- `evidenceFiles`
|
||||
- `evidencePreviewUrls`
|
||||
- `error`
|
||||
- `isSubmitting`
|
||||
- `submitted`
|
||||
|
||||
**Step 3: 实现页面结构**
|
||||
|
||||
建议结构:
|
||||
|
||||
```tsx
|
||||
import { ArrowLeft, CheckCircle2, Home, ImagePlus, Send } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const MAX_FEEDBACK_DESCRIPTION_LENGTH = 200;
|
||||
const MIN_FEEDBACK_DESCRIPTION_LENGTH = 10;
|
||||
const MAX_FEEDBACK_EVIDENCE_COUNT = 4;
|
||||
```
|
||||
|
||||
页面外壳建议复用现有视觉变量:
|
||||
|
||||
```tsx
|
||||
<div className="platform-page-stage platform-remap-surface min-h-0 min-w-0 overflow-y-auto px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col gap-4">
|
||||
<header className="platform-surface platform-surface--soft rounded-[1.6rem] px-4 py-4">
|
||||
<button type="button" onClick={onBack} ...>
|
||||
<ArrowLeft ... /> 返回
|
||||
</button>
|
||||
<h1>反馈</h1>
|
||||
</header>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
注意:不要写大段“功能说明类文案”;字段 label 简短即可。
|
||||
|
||||
**Step 4: 表单校验**
|
||||
|
||||
提交时:
|
||||
|
||||
- `description.trim().length < 10`:提示“请填写10个字以上的问题描述”
|
||||
- `description.trim().length > 200`:提示“问题描述不能超过 200 字”
|
||||
- `contactPhone.trim().length > 40`:提示“联系电话不能超过 40 字”
|
||||
- 上传凭证最多 4 张;超出时提示“最多上传四张凭证”
|
||||
|
||||
**Step 5: 提交行为**
|
||||
|
||||
首版无后端时:
|
||||
|
||||
```ts
|
||||
await onSubmit?.({
|
||||
description: description.trim(),
|
||||
contactPhone: contactPhone.trim(),
|
||||
evidenceFiles,
|
||||
});
|
||||
setSubmitted(true);
|
||||
```
|
||||
|
||||
如果没有传 `onSubmit`,也显示成功态。代码注释说明:
|
||||
|
||||
```ts
|
||||
// 中文注释:首版反馈页只完成前端收集与成功态;接入后端时在 onSubmit 中替换为 API 调用。
|
||||
```
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/platform-entry/PlatformFeedbackView.tsx
|
||||
git commit -m "feat: add platform feedback view"
|
||||
```
|
||||
|
||||
### Task 4: 在“我的”页签增加反馈入口 prop
|
||||
|
||||
**Objective:** 让 Profile 页面能触发反馈路由,同时保持组件职责清晰。
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
- Modify: `src/components/platform-entry/PlatformEntryHomeView.tsx`(通常无需改,re-export 类型会自动带出)
|
||||
|
||||
**Step 1: 扩展 Props**
|
||||
|
||||
在 `RpgEntryHomeViewProps` 中新增:
|
||||
|
||||
```ts
|
||||
onOpenFeedback?: () => void;
|
||||
```
|
||||
|
||||
**Step 2: 从 props 解构**
|
||||
|
||||
在 `RpgEntryHomeView` 函数参数解构区新增:
|
||||
|
||||
```ts
|
||||
onOpenFeedback,
|
||||
```
|
||||
|
||||
**Step 3: 增加入口按钮**
|
||||
|
||||
在 `profileContent` 的常用功能 grid 中,建议在“玩家社区”后追加:
|
||||
|
||||
```tsx
|
||||
<ProfileShortcutButton
|
||||
label="反馈"
|
||||
subLabel="问题与建议"
|
||||
icon={MessageCircle}
|
||||
onClick={onOpenFeedback}
|
||||
/>
|
||||
```
|
||||
|
||||
如果参考图中入口位置不同,按参考图调整;但仍必须进入独立路由。
|
||||
|
||||
**Step 4: 未提供回调时行为**
|
||||
|
||||
`ProfileShortcutButton` 已允许 `onClick` 为空;此处传 `onOpenFeedback` 即可。若希望按钮始终可点,应在父组件必传。
|
||||
|
||||
**Step 5: 验证类型**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
|
||||
Expected: PASS 或只剩父组件未传 prop 的问题。
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/rpg-entry/RpgEntryHomeView.tsx src/components/platform-entry/PlatformEntryHomeView.tsx
|
||||
git commit -m "feat: add feedback shortcut to profile tab"
|
||||
```
|
||||
|
||||
### Task 5: 接入 PlatformEntryFlowShellImpl 渲染与导航
|
||||
|
||||
**Objective:** 点击“反馈”进入 `/profile/feedback`,返回后回到“我的”页签。
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
**Step 1: 导入组件**
|
||||
|
||||
在 imports 中新增:
|
||||
|
||||
```ts
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
```
|
||||
|
||||
**Step 2: 创建打开反馈页函数**
|
||||
|
||||
在 `const { setPlatformTab } = platformBootstrap;` 附近新增:
|
||||
|
||||
```ts
|
||||
const openProfileFeedback = useCallback(() => {
|
||||
if (!authUi?.user) {
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
setPlatformTab('profile');
|
||||
setSelectionStage('profile-feedback');
|
||||
}, [authUi, setPlatformTab, setSelectionStage]);
|
||||
```
|
||||
|
||||
如产品允许匿名反馈,则移除登录判断。
|
||||
|
||||
**Step 3: 给首页传入入口回调**
|
||||
|
||||
在 `PlatformEntryHomeView` props 中加入:
|
||||
|
||||
```tsx
|
||||
onOpenFeedback={openProfileFeedback}
|
||||
```
|
||||
|
||||
**Step 4: 增加渲染分支**
|
||||
|
||||
在 `selectionStage === 'platform'` 分支后、详情页分支前新增:
|
||||
|
||||
```tsx
|
||||
{selectionStage === 'profile-feedback' && (
|
||||
<motion.div
|
||||
key="platform-profile-feedback"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<PlatformFeedbackView
|
||||
onBack={() => {
|
||||
setPlatformTab('profile');
|
||||
setSelectionStage('platform');
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
```
|
||||
|
||||
**Step 5: 直接访问路由的 tab 同步**
|
||||
|
||||
为处理用户直接访问 `/profile/feedback` 后返回,返回逻辑已 `setPlatformTab('profile')`。如需要进入反馈页时也设置 tab,可加 effect:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (selectionStage === 'profile-feedback') {
|
||||
setPlatformTab('profile');
|
||||
}
|
||||
}, [selectionStage, setPlatformTab]);
|
||||
```
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
|
||||
git commit -m "feat: wire profile feedback navigation"
|
||||
```
|
||||
|
||||
### Task 6: 增加路由与反馈页基础测试
|
||||
|
||||
**Objective:** 用自动化测试覆盖新路由映射和反馈页核心交互。
|
||||
|
||||
**Files:**
|
||||
- Create or Modify: `src/routing/appPageRoutes.test.ts`
|
||||
- Create: `src/components/platform-entry/PlatformFeedbackView.test.tsx`
|
||||
|
||||
**Step 1: 路由测试**
|
||||
|
||||
如果已有 `appPageRoutes.test.ts`,追加;否则创建:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
resolvePathForSelectionStage,
|
||||
resolveSelectionStageFromPath,
|
||||
} from './appPageRoutes';
|
||||
|
||||
describe('appPageRoutes', () => {
|
||||
it('resolves profile feedback route', () => {
|
||||
expect(resolveSelectionStageFromPath('/profile/feedback')).toBe('profile-feedback');
|
||||
expect(resolvePathForSelectionStage('profile-feedback')).toBe('/profile/feedback');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: 反馈页测试**
|
||||
|
||||
测试重点:
|
||||
|
||||
- 渲染“帮助与反馈”标题。
|
||||
- 问题描述过短时提交显示错误。
|
||||
- 输入有效问题描述后提交显示成功态。
|
||||
- 字数统计随输入更新。
|
||||
- 上传凭证入口最多接受 4 张图片。
|
||||
- 点击返回调用 `onBack`。
|
||||
|
||||
示例:
|
||||
|
||||
```tsx
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
|
||||
describe('PlatformFeedbackView', () => {
|
||||
it('validates content before submit', () => {
|
||||
render(<PlatformFeedbackView onBack={vi.fn()} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交' }));
|
||||
expect(screen.getByText('请填写10个字以上的问题描述')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
注意检查项目当前 test setup 是否已引入 jest-dom matcher;若没有,使用 truthy DOM 节点断言:
|
||||
|
||||
```ts
|
||||
expect(screen.getByText('请补充反馈内容')).toBeTruthy();
|
||||
```
|
||||
|
||||
**Step 3: 运行定向测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run test -- src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
|
||||
git commit -m "test: cover profile feedback route and form"
|
||||
```
|
||||
|
||||
### Task 7: 全量前端验证与移动端 smoke
|
||||
|
||||
**Objective:** 确认新增页面不破坏编码、类型和基础交互。
|
||||
|
||||
**Files:**
|
||||
- No code changes unless validation finds issues.
|
||||
|
||||
**Step 1: 编码检查**
|
||||
|
||||
Run: `npm run check:encoding`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 2: ESLint**
|
||||
|
||||
Run: `npm run lint:eslint`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 3: TypeScript**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 4: 测试**
|
||||
|
||||
Run: `npm run test -- src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
**Step 5: 本地页面 smoke**
|
||||
|
||||
Run: `npm run dev:web`
|
||||
|
||||
手动验证:
|
||||
|
||||
1. 打开 `http://127.0.0.1:3000/`。
|
||||
2. 登录后进入“我的”页签。
|
||||
3. 点击“反馈”。
|
||||
4. 地址变为 `/profile/feedback`。
|
||||
5. 页面显示反馈表单。
|
||||
6. 提交空内容出现错误。
|
||||
7. 输入有效内容后显示成功态。
|
||||
8. 点击返回后回到首页“我的”页签。
|
||||
9. 直接打开 `http://127.0.0.1:3000/profile/feedback` 能显示反馈页。
|
||||
10. 使用移动端视口(如 390×844)确认按钮和表单不溢出。
|
||||
|
||||
**Step 6: Commit validation fixes if any**
|
||||
|
||||
```bash
|
||||
git add <fixed-files>
|
||||
git commit -m "fix: polish profile feedback validation"
|
||||
```
|
||||
|
||||
## Files likely to change
|
||||
|
||||
- `docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`:新增反馈入口落地文档。
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`:新增 `profile-feedback` 阶段。
|
||||
- `src/routing/appPageRoutes.ts`:新增 `/profile/feedback` 路由映射。
|
||||
- `.hermes/plans/assets/profile-feedback-reference-2026-05-08.png`:反馈页参考图。
|
||||
- `src/components/platform-entry/PlatformFeedbackView.tsx`:新增反馈页面。
|
||||
- `src/components/rpg-entry/RpgEntryHomeView.tsx`:新增“我的”页签反馈入口和 `onOpenFeedback` prop。
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`:接入反馈页打开与返回导航。
|
||||
- `src/routing/appPageRoutes.test.ts`:新增路由映射测试。
|
||||
- `src/components/platform-entry/PlatformFeedbackView.test.tsx`:新增反馈页交互测试。
|
||||
|
||||
## Tests / validation
|
||||
|
||||
Minimum required:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run test -- src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
|
||||
```
|
||||
|
||||
Recommended before merge:
|
||||
|
||||
```bash
|
||||
npm run lint:eslint
|
||||
npm run test
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual smoke:
|
||||
|
||||
- 登录后“我的”页签显示“反馈”入口。
|
||||
- 点击入口进入 `/profile/feedback`。
|
||||
- 浏览器后退和页面返回按钮行为符合预期。
|
||||
- 移动端视口无横向溢出。
|
||||
- 页面没有把反馈表单展开在“我的”页签下方。
|
||||
|
||||
## Risks, tradeoffs, and open questions
|
||||
|
||||
1. **参考图落地风险:** 参考图是浅色移动端表单,而项目现有平台 UI 可能偏游戏化/深色变量;实现时需要优先复刻信息结构与交互,不要为了完全一致而破坏现有主题适配。
|
||||
2. **反馈是否需要后端存储:** 本计划首版不新增后端,只做前端收集和成功态。若产品要求真实提交,需要新增后端方案:`shared-contracts` DTO、`api-server` 路由、SpacetimeDB 表/迁移、后台查看入口,并按 SpacetimeDB skills 执行。
|
||||
3. **登录要求:** 计划默认未登录用户点击入口弹登录。若希望匿名反馈,应取消该限制,并在 payload 中允许无用户身份。
|
||||
4. **入口位置:** 当前建议放在“我的”页签常用功能 grid 中。若参考图明确是列表项或设置区入口,应按图调整,但仍进入独立路由。
|
||||
5. **图标复用:** 可先用 `MessageCircle` 或 `MessageSquareText`,避免引入新依赖。
|
||||
6. **现有大文件风险:** `RpgEntryHomeView.tsx` 很大,实施时必须局部补丁,避免整文件重写导致中文编码或格式大范围变化。
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- 所有中文注释和文案保持 UTF-8。
|
||||
- 不要新增 `.env.local` 到 `.gitignore`。
|
||||
- 不要把反馈页做成“我的”页签内部展开面板。
|
||||
- 不要新增后端或数据库,除非用户确认反馈必须持久化。
|
||||
- 若后续接入后端,必须先补技术文档,再按 DDD 与 SpacetimeDB 约束落地。
|
||||
@@ -1,561 +0,0 @@
|
||||
# 声控狗叫对战 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。
|
||||
@@ -1,709 +0,0 @@
|
||||
# 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 文档做数据库、发布、成绩和追踪闭环。
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# 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?
|
||||
@@ -1,447 +0,0 @@
|
||||
# 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。
|
||||
@@ -1,403 +0,0 @@
|
||||
# 当前项目安全漏洞检查计划
|
||||
|
||||
> **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 凭据和生产运行时配置,需要另行确认访问范围和凭据边界。
|
||||
@@ -1,206 +0,0 @@
|
||||
# 远端作品列表压测排查报告
|
||||
|
||||
时间: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,避免诊断时混淆。
|
||||
@@ -1,343 +0,0 @@
|
||||
# Genarrative 视觉小说“一句话生成”最小闭环落地计划
|
||||
|
||||
生成时间:2026-05-13 11:22
|
||||
工作区:`C:/proj/Genarrative/.worktrees/hermes-visual-novel`
|
||||
参考文档:`C:/Users/DSK/Documents/Interactive-fiction/一句话生成视觉小说整体流程总结.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 Interactive-fiction 总结文档中的“一句话生成视觉小说”流程,映射并落地到 Genarrative 现有视觉小说能力中,优先做成一个可端到端验证的最小闭环:
|
||||
|
||||
1. 用户在视觉小说入口输入一句话并选择画风。
|
||||
2. 前端进入生成过程页,展示分阶段进度。
|
||||
3. 后端创建视觉小说创作会话,并基于 seedText 生成 `VisualNovelResultDraft`。
|
||||
4. 生成完成后进入草稿结果页,可看到世界观、角色、场景、剧情阶段、开场选择。
|
||||
5. 草稿可编译/保存为作品 profile,并进入视觉小说运行态测试/正式游玩。
|
||||
|
||||
本计划只覆盖 Genarrative 内部最小闭环,不引入 Interactive-fiction 原项目的独立 TXT 播放记录、分享播放包、外部活动运营、独立账号/交易/资产系统。
|
||||
|
||||
## 2. 当前上下文与已发现实现
|
||||
|
||||
### 2.1 Interactive-fiction 总结文档提炼
|
||||
|
||||
参考文档将整体流程分为:
|
||||
|
||||
- 输入侧:一句话创意、主题/风格、可选文档或素材。
|
||||
- 生成侧:理解意图、扩展世界观、角色、场景、剧情阶段、开场与选择。
|
||||
- 编辑侧:草稿页可查看和调整生成结果。
|
||||
- 运行侧:从草稿进入视觉小说游玩,支持剧情推进、玩家选择、历史与状态。
|
||||
- 资产侧:角色立绘、背景、音乐/音效可作为后续增强,最小闭环可先使用文字描述与空资产占位。
|
||||
|
||||
### 2.2 Genarrative 已有实现基础
|
||||
|
||||
已确认项目中视觉小说相关能力并非从零开始:
|
||||
|
||||
- 前端入口表单:
|
||||
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
|
||||
- 已有“一句话创作” textarea、6 个视觉画风选项、提交按钮“生成视觉小说草稿”。
|
||||
- 前端入口 payload/progress:
|
||||
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
|
||||
- 已有 `VisualNovelEntryFormPayload`、锚点展示、一句话/画风生成进度步骤。
|
||||
- 前端平台主流程:
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- 已接入 `createVisualNovelDraftFromForm`,会创建 session、stream message、进入 `visual-novel-generating`,完成后进入 `visual-novel-result`。
|
||||
- 前端 API client:
|
||||
- `src/services/visual-novel-creation/visualNovelCreationClient.ts`
|
||||
- 已封装 session/message/action/compile 接口。
|
||||
- 共享契约:
|
||||
- `packages/shared/src/contracts/visualNovel.ts`
|
||||
- 已定义 `VisualNovelResultDraft`、world/characters/scenes/storyPhases/opening/runtimeConfig/work/run/history 等结构。
|
||||
- 后端 API:
|
||||
- `server-rs/crates/api-server/src/visual_novel.rs`
|
||||
- 已有创建 session、发消息、流式消息、执行 action、compile、work、runtime run 等接口。
|
||||
- 后端 prompt:
|
||||
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
|
||||
- 已有 `VISUAL_NOVEL_CREATION_SYSTEM_PROMPT`、结构化输出契约、runtime GM prompt、repair prompt。
|
||||
- SpacetimeDB 模块:
|
||||
- `server-rs/crates/spacetime-module/src/visual_novel.rs`
|
||||
- 已有 session/message/work/run/history/event 表与 procedure。
|
||||
- 文档参考:
|
||||
- `docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`
|
||||
- `docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`
|
||||
- `docs/technical/VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`
|
||||
|
||||
### 2.3 关键实现判断
|
||||
|
||||
当前项目已经实现了视觉小说的主要骨架,本次不应大规模重写。更合理的落地方式是补齐“一句话生成”闭环中最容易断裂的点:
|
||||
|
||||
- 入口输入与画风信息是否被稳定传给后端 prompt。
|
||||
- 后端生成 draft 后是否自动保存/关联可编辑 work profile。
|
||||
- 生成过程页是否能清晰展示 Interactive-fiction 文档中提到的阶段。
|
||||
- 结果页是否有足够的字段展示与继续游玩入口。
|
||||
- 运行态是否能基于 opening/choices 正常启动,而不依赖尚未生成的图片/音乐资产。
|
||||
|
||||
## 3. 拟采用方案
|
||||
|
||||
### 3.1 最小闭环范围
|
||||
|
||||
本次优先实现:
|
||||
|
||||
1. “一句话 + 视觉画风”作为 `sourceMode: 'idea'` 的 seedText。
|
||||
2. 后端生成完整 `VisualNovelResultDraft`,包括:
|
||||
- world
|
||||
- 3-6 个角色
|
||||
- 3-8 个场景
|
||||
- 3-6 个剧情阶段
|
||||
- opening narration/firstDialogue/2-4 个 choices
|
||||
- runtimeConfig
|
||||
3. 若 LLM 输出失败,使用 repair 或确定性 fallback,保证可回到草稿页并显示错误/警告。
|
||||
4. 结果页支持保存/编译为 work profile。
|
||||
5. work profile 支持启动 runtime run,opening 能展示初始场景、旁白、对话和选择。
|
||||
|
||||
暂不做或仅预留:
|
||||
|
||||
- 真实图片/音乐生成队列。
|
||||
- 多文档解析导入的完整链路。
|
||||
- 复杂分镜/节点图编辑器。
|
||||
- 外部 Interactive-fiction 项目的播放器、TXT 记录包、分享活动、独立账号系统。
|
||||
|
||||
### 3.2 与 Genarrative 架构的映射
|
||||
|
||||
| Interactive-fiction 概念 | Genarrative 落点 |
|
||||
| --- | --- |
|
||||
| 一句话创意 | `VisualNovelEntryFormPayload.ideaText` / `seedText` |
|
||||
| 画风/主题 | `seedText` 中的“视觉画风/画风要求”,后续可结构化为 metadata |
|
||||
| 世界观设定 | `VisualNovelResultDraft.world` |
|
||||
| 角色设定 | `VisualNovelResultDraft.characters` |
|
||||
| 场景设定 | `VisualNovelResultDraft.scenes` |
|
||||
| 剧情阶段/章节 | `VisualNovelResultDraft.storyPhases` |
|
||||
| 开场文本与选项 | `VisualNovelResultDraft.opening` |
|
||||
| 运行时剧情推进 | `VisualNovelRuntimeStep[]` + run snapshot/history |
|
||||
| 发布/作品库 | `VisualNovelWorkProfileRecord` / works API |
|
||||
|
||||
## 4. 分步计划
|
||||
|
||||
### Step 1:补齐入口 payload 与生成过程语义
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
|
||||
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
任务:
|
||||
|
||||
1. 保持现有 6 个画风选项,但确认每个 option 的 prompt 会进入 `seedText`。
|
||||
2. 将生成过程阶段从当前 3 步细化为更贴合参考文档的 4-5 步,例如:
|
||||
- 理解一句话创意
|
||||
- 扩展世界观与玩家身份
|
||||
- 设计角色/场景/剧情阶段
|
||||
- 生成开场与选择
|
||||
- 准备可编辑草稿
|
||||
3. 生成过程页的 anchor 保留“一句话”和“视觉画风”,必要时增加“生成目标:视觉小说草稿”。
|
||||
4. 确认 `createVisualNovelDraftFromForm` 对失败状态会保留返回入口/重试能力。
|
||||
|
||||
验收点:提交一句话后能进入 `visual-novel-generating`,看到阶段进度;完成后进入 `visual-novel-result`。
|
||||
|
||||
### Step 2:增强后端 creation prompt 与 fallback 约束
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
|
||||
- `server-rs/crates/api-server/src/visual_novel.rs`
|
||||
- 如已有 domain crate:`server-rs/crates/module-visual-novel/**` 或相关 normalize/validate 文件
|
||||
|
||||
任务:
|
||||
|
||||
1. 在 creation prompt 中显式吸收 Interactive-fiction 的“一句话生成”目标:
|
||||
- 从 seedText 提取核心创意、视觉风格、故事类型。
|
||||
- 生成可直接运行的 opening 和 choices。
|
||||
- 图片/音乐资产先置 null,但必须有可生成图像的描述。
|
||||
2. 强化输出约束:
|
||||
- `opening.sceneId` 必须指向存在且 availability 为 `opening` 的 scene。
|
||||
- `opening.initialChoices` 必须 2-4 个。
|
||||
- `storyPhases[0]` 必须包含 opening scene 和主要角色。
|
||||
- `publishReady` 的判定与 validationIssues 一致。
|
||||
3. 检查 `submit_visual_novel_message_turn` / `resolve_action_draft` / compile 相关代码:
|
||||
- 如果 LLM 失败,是否已有 fallback;没有则补确定性 fallback draft。
|
||||
- 如果 draft 不完整,是否会 normalize/repair 并写入 session。
|
||||
4. 保留现有“不要输出旧 TXT 播放记录、分享播放包、外部商业字段”的约束,避免把参考项目的外部概念误并入 Genarrative。
|
||||
|
||||
验收点:后端给定 seedText 时,返回 session.draft 不为空且满足共享契约。
|
||||
|
||||
### Step 3:确认草稿结果页、保存/编译与作品库链路
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/components/visual-novel-creation/**`
|
||||
- `src/services/visual-novel-works*` 或相关 visual novel works client
|
||||
- `server-rs/crates/api-server/src/visual_novel.rs`
|
||||
- `packages/shared/src/contracts/visualNovel.ts`
|
||||
|
||||
任务:
|
||||
|
||||
1. 查找并确认 `visual-novel-result` 页面组件:
|
||||
- 是否显示 workTitle/workDescription/world/characters/scenes/storyPhases/opening。
|
||||
- 是否有保存/发布/开始试玩按钮。
|
||||
2. 确认 `compileVisualNovelWorkProfile` 或 `executeVisualNovelAction({kind:'compile_work_profile'})` 会生成/更新 work profile。
|
||||
3. 确认作品架上使用 `profileId` 而不是 sessionId 作为稳定作品 ID。
|
||||
4. 如果结果页缺少“一句话来源/画风”的可视化提示,可在结果页或 summary 中补轻量展示,避免用户以为画风丢失。
|
||||
|
||||
验收点:生成完成后能保存为作品;作品出现在“我的作品/创作架”;再次打开能读取同一 draft。
|
||||
|
||||
### Step 4:确认运行态 opening 闭环
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `src/components/visual-novel-runtime/**`
|
||||
- `src/services/visual-novel-runtime*`
|
||||
- `server-rs/crates/api-server/src/visual_novel.rs`
|
||||
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
|
||||
- `packages/shared/src/contracts/visualNovel.ts`
|
||||
|
||||
任务:
|
||||
|
||||
1. 启动 visual novel work run 时,优先使用 `draft.opening` 生成第一轮 runtime snapshot/history。
|
||||
2. 如果没有图片/音乐,前端 runtime shell 必须可用文字 fallback,不应白屏或阻断游玩。
|
||||
3. 玩家选择 `choice` 后,后端 runtime GM prompt 生成下一轮 `VisualNovelRuntimeStep[]`。
|
||||
4. 确认正式游玩入口调用 `work_play_start`,并满足已有埋点约定:
|
||||
- `scope_kind=work`
|
||||
- `scope_id=稳定作品 ID`
|
||||
- metadata 包含 `playType/workId/sourceRoute/userId` 等。
|
||||
|
||||
验收点:从生成出的作品进入运行态,能看到 opening 并点击至少一个选择推进一轮。
|
||||
|
||||
### Step 5:补测试与文档
|
||||
|
||||
涉及文件:
|
||||
|
||||
- 前端测试:按仓库现有测试布局查找 `*.test.ts` / `*.test.tsx`
|
||||
- Rust 测试:`server-rs/crates/api-server/src/**` 或 domain crate tests
|
||||
- 文档:可追加到 `docs/technical/` 或 `.hermes/shared-memory/decision-log.md`(如团队约定需要)
|
||||
|
||||
建议测试:
|
||||
|
||||
1. TypeScript 单元测试:
|
||||
- `buildVisualNovelEntryGenerationProgress` 阶段输出。
|
||||
- `buildVisualNovelEntryGenerationAnchorEntries` 能展示一句话和画风。
|
||||
2. Rust 单元测试:
|
||||
- creation prompt 包含 seedText、sourceMode、输出契约。
|
||||
- draft normalize/fallback 能生成合法 opening/choices。
|
||||
- runtime opening 或 first-step 构造不依赖图片/音乐。
|
||||
3. 集成/手工测试文档:
|
||||
- 访问平台视觉小说入口。
|
||||
- 输入一句话。
|
||||
- 选择画风。
|
||||
- 点击生成。
|
||||
- 查看结果页。
|
||||
- 保存作品。
|
||||
- 启动试玩并点击选择。
|
||||
|
||||
## 5. 可能改动文件清单
|
||||
|
||||
高概率改动:
|
||||
|
||||
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
|
||||
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
|
||||
- `server-rs/crates/api-server/src/visual_novel.rs`
|
||||
- `packages/shared/src/contracts/visualNovel.ts`
|
||||
|
||||
中概率改动:
|
||||
|
||||
- `src/components/visual-novel-runtime/**`
|
||||
- `src/services/visual-novel-creation/**`
|
||||
- `src/services/visual-novel-runtime/**`
|
||||
- `src/services/visual-novel-works/**`
|
||||
- `server-rs/crates/spacetime-module/src/visual_novel.rs`
|
||||
- `server-rs/crates/spacetime-client/**` 生成/绑定文件,若 SpacetimeDB contract 需要更新
|
||||
|
||||
低概率/仅文档:
|
||||
|
||||
- `docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`
|
||||
- `docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`
|
||||
- `.hermes/shared-memory/decision-log.md`
|
||||
|
||||
## 6. 验证计划
|
||||
|
||||
### 6.1 静态检查
|
||||
|
||||
在 worktree 根目录执行:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
如仓库无统一 typecheck,则按 package scripts 选择最接近的前端类型检查命令。
|
||||
|
||||
### 6.2 前端定向测试
|
||||
|
||||
优先运行与 visual novel / platform entry 相关测试,如存在:
|
||||
|
||||
```bash
|
||||
npm test -- visual-novel
|
||||
npm test -- platform-entry
|
||||
```
|
||||
|
||||
若仓库使用 vitest:
|
||||
|
||||
```bash
|
||||
npm run test -- visual-novel
|
||||
```
|
||||
|
||||
### 6.3 Rust 定向测试
|
||||
|
||||
在 `server-rs` 下运行 visual novel 相关测试:
|
||||
|
||||
```bash
|
||||
cargo test -p api-server visual_novel
|
||||
cargo test -p shared-contracts visual_novel
|
||||
```
|
||||
|
||||
如改动 SpacetimeDB module:
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-module visual_novel
|
||||
```
|
||||
|
||||
### 6.4 人工验收步骤
|
||||
|
||||
1. 启动本地 dev 栈。
|
||||
2. 访问 Genarrative 主站。
|
||||
3. 进入创作/视觉小说入口。
|
||||
4. 输入:`一个雨夜,失忆的高中生在旧图书馆发现一本会回应她心声的日记。`
|
||||
5. 选择任一画风,例如“映画动画”。
|
||||
6. 点击“生成视觉小说草稿”。
|
||||
7. 预期:进入生成过程页,能看到分阶段进度。
|
||||
8. 预期:完成后进入草稿结果页,包含标题、简介、世界观、角色、场景、剧情阶段和 opening choices。
|
||||
9. 点击保存/编译作品。
|
||||
10. 从作品入口进入试玩。
|
||||
11. 预期:opening 文本出现,至少 2 个选择可点击;点击后剧情继续推进一轮。
|
||||
|
||||
## 7. 风险、权衡与开放问题
|
||||
|
||||
### 7.1 风险
|
||||
|
||||
- 现有视觉小说代码已较完整,贸然新增一套 parallel pipeline 会制造重复逻辑;应复用当前 `VisualNovelResultDraft` 与 creation agent flow。
|
||||
- LLM 输出不稳定可能导致草稿结构不完整;需要 normalize/repair/fallback 确保最小闭环。
|
||||
- 视觉/音乐资产生成未接入时,UI 必须接受 null asset,否则运行态可能白屏。
|
||||
- `PlatformEntryFlowShellImpl.tsx` 文件很大,改动需局部、谨慎,避免影响其他玩法入口。
|
||||
- 若改动 SpacetimeDB 表结构,可能牵涉 publish、client binding、清库/迁移;最小闭环阶段应尽量避免 schema 变更。
|
||||
|
||||
### 7.2 权衡
|
||||
|
||||
- 先让文字版视觉小说完整跑通,再补角色立绘/背景图生成。
|
||||
- 先用 `seedText` 承载画风,再考虑把 `visualStyleId/Label/Prompt` 结构化进 draft metadata。
|
||||
- 先用现有 result/work/runtime 页面闭环,不引入新编辑器。
|
||||
|
||||
### 7.3 开放问题
|
||||
|
||||
1. 用户是否要求把 Interactive-fiction 原项目中的具体 UI 样式/页面布局迁移到 Genarrative?当前计划只迁移流程语义,不迁移独立 UI。
|
||||
2. 画风是否需要成为作品可编辑字段?当前以 seedText/prompt 影响生成内容,后续可在 draft 中增加 metadata。
|
||||
3. 文档导入模式是否本期要做?当前计划聚焦一句话模式,document 模式只保留契约能力。
|
||||
4. 是否需要真实图片/音乐生成?当前计划作为后续增强,不纳入最小闭环。
|
||||
|
||||
## 8. 建议实施顺序
|
||||
|
||||
1. 先做只改 prompt/progress/少量前端展示的轻量闭环修补。
|
||||
2. 运行前后端定向测试,确认现有能力是否已足够。
|
||||
3. 如果后端没有 fallback 或 normalize,再补 Rust 层确定性兜底。
|
||||
4. 手工跑通“一句话 -> 生成 -> 结果页 -> 保存 -> 试玩”。
|
||||
5. 最后再考虑是否需要资产生成、文档导入、结构化画风 metadata。
|
||||
@@ -1,549 +0,0 @@
|
||||
# Bark Battle Phase 2 Platform Work Loop Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 将 `bark-battle` 从内部试玩 demo 升级为 Genarrative 正式 play type,打通轻创作配置、发布态作品、正式 runtime、run start / finish、后端裁决、个人历史、作品统计和最小排行榜闭环。
|
||||
|
||||
**Architecture:** 先冻结 shared contracts 与 `module-bark-battle` 纯领域规则,再落 SpacetimeDB 表/reducer、`spacetime-client` facade 和 `api-server` BFF,随后接前端最小纵切,最后补排行榜/个人历史/作品统计投影体验。前端只承接表现、交互和临时 UI 状态,正式业务真相由后端裁决。
|
||||
|
||||
**Tech Stack:** React + TypeScript + Vite, server-rs + Axum, SpacetimeDB Rust module, shared-contracts, Vitest, Cargo tests, npm scripts.
|
||||
|
||||
---
|
||||
|
||||
## 0. 已确认决策
|
||||
|
||||
1. “有效叫声”统一为 **有效声浪触发**:当前采样响度达到有效阈值且满足 `minBarkGapMs` 冷却即触发;不再要求 `minBarkDurationMs` / `maxBarkDurationMs`,也不等待响度回落。
|
||||
2. Phase 2 范围是 **Bark Battle 平台作品闭环**,不是单纯玩法表现深化。
|
||||
3. 作品形态是 **轻创作配置作品**:标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。
|
||||
4. 难度预设只影响 AI 对手行为;不影响有效阈值、冷却、时长、分数公式或反作弊阈值。
|
||||
5. 排行榜按 `workId + difficultyPreset + rulesetVersion` 分榜。
|
||||
6. 后端裁决正式单局结果;前端只提交派生指标,`clientResult` 只用于 debug/对账。
|
||||
7. 排行榜只收录 `serverResult = player_win` 且未被反作弊拒绝的单局结果,排序以 `finalEnergy` 优先。
|
||||
8. 作品统计使用最小后端投影:start、finish、win/draw/loss、flagged、leaderboard、best/avg energy。
|
||||
9. 个人历史成绩 = 最近记录列表 + 个人最佳摘要;仅本人可见。
|
||||
10. 正式入口闭环覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品、稳定作品 ID runtime 路由和 `work_play_start`。
|
||||
11. 创作编辑形态是单页轻配置表单 + 预览卡片。
|
||||
12. 实施顺序固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。
|
||||
|
||||
---
|
||||
|
||||
## 1. 必读文档与约束
|
||||
|
||||
实施前先读:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `CONTEXT.md`
|
||||
- `docs/prd/BARK_BATTLE_BDD_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||
- `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
- `.codex/skills/spacetimedb-cli/SKILL.md`
|
||||
- `.codex/skills/spacetimedb-rust/SKILL.md`
|
||||
- `.codex/skills/spacetimedb-concepts/SKILL.md`
|
||||
- `.codex/skills/spacetimedb-typescript/SKILL.md`
|
||||
|
||||
关键约束:
|
||||
|
||||
- 后端路线固定 `server-rs + Axum + SpacetimeDB`。
|
||||
- 领域规则进 `module-bark-battle`,SpacetimeDB 表和事务编排进 `spacetime-module`。
|
||||
- HTTP/SSE/BFF 留在 `api-server`。
|
||||
- 前后端 DTO 留在 `shared-contracts`。
|
||||
- 数据库表结构更改必须同步 `migration.rs` 和生成绑定。
|
||||
- 人工命令/文档示例禁止继续使用 `spacetime --root-dir`。
|
||||
- 修改中文文件后必须跑 `npm run check:encoding`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 阶段一:契约与领域规则
|
||||
|
||||
### Task 1.1: 新增 Rust shared-contracts 模块
|
||||
|
||||
**Objective:** 定义 Bark Battle Phase 2 的 Rust DTO 边界。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
- Modify: `server-rs/crates/shared-contracts/src/lib.rs`
|
||||
- Test: `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
|
||||
**Steps:**
|
||||
1. 新增枚举:`BarkBattleDifficultyPreset { Easy, Normal, Hard }`、`BarkBattleServerResult { PlayerWin, OpponentWin, Draw }`、`BarkBattleFinishStatus { Accepted, AcceptedWithFlags, Rejected }`。
|
||||
2. 新增配置 DTO:`BarkBattleDraftConfig`、`BarkBattlePublishedConfig`、`BarkBattleRuntimeConfig`。
|
||||
3. 新增 run DTO:`BarkBattleRunStartRequest/Response`、`BarkBattleRunFinishRequest/Response`。
|
||||
4. 新增派生指标 DTO:`BarkBattleDerivedMetrics`,字段包含 `trigger_count`、`max_volume`、`average_volume`、`final_energy`、`combo_max`。
|
||||
5. 新增排行榜/历史/统计 DTO:`BarkBattleLeaderboardEntry`、`BarkBattlePersonalHistoryItem`、`BarkBattlePersonalBestSummary`、`BarkBattleWorkStats`。
|
||||
6. 在 `lib.rs` 导出 `pub mod bark_battle;`。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p shared-contracts bark_battle
|
||||
```
|
||||
|
||||
Expected: contracts tests pass.
|
||||
|
||||
### Task 1.2: 新增 TypeScript shared contracts mirror
|
||||
|
||||
**Objective:** 让前端获得与 Rust DTO 对齐的类型。
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/shared/src/contracts/barkBattle.ts`
|
||||
- Modify: `packages/shared/src/contracts/index.ts`
|
||||
- Test: `packages/shared/src/contracts/barkBattle.test.ts`
|
||||
|
||||
**Steps:**
|
||||
1. 定义 `BarkBattleDifficultyPreset = 'easy' | 'normal' | 'hard'`。
|
||||
2. 定义 `BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw'`。
|
||||
3. 定义 draft / published / runtime config 类型。
|
||||
4. 定义 start / finish request response 类型。
|
||||
5. 定义 leaderboard / personal history / work stats 类型。
|
||||
6. 写最小序列化/fixture 测试,确保字段命名采用前端约定 camelCase,并在 API client 层做必要映射。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm test -- --run packages/shared/src/contracts/barkBattle.test.ts
|
||||
npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false
|
||||
```
|
||||
|
||||
### Task 1.3: 新建 module-bark-battle crate
|
||||
|
||||
**Objective:** 将正式裁决规则放入纯领域 crate。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/module-bark-battle/Cargo.toml`
|
||||
- Create: `server-rs/crates/module-bark-battle/src/lib.rs`
|
||||
- Create: `server-rs/crates/module-bark-battle/src/domain.rs`
|
||||
- Create: `server-rs/crates/module-bark-battle/src/scoring.rs`
|
||||
- Modify: `server-rs/Cargo.toml`
|
||||
|
||||
**Steps:**
|
||||
1. 在 workspace 中注册 `module-bark-battle`。
|
||||
2. 定义 `RulesetVersion`,首版固定如 `bark-battle-ruleset-v1`。
|
||||
3. 定义 `BarkBattleRuleset`,包含标准局时长 30s、`min_bark_gap_ms`、合法音量/能量/连击范围、duration tolerance。
|
||||
4. 实现 `validate_finish_metrics()`。
|
||||
5. 实现 `adjudicate_result()`:以后端 `final_energy` 和 draw threshold 生成 `serverResult`。
|
||||
6. 实现 `compute_leaderboard_score()`:只允许胜利局入榜,排序因子为 `finalEnergy`、`triggerCount`、`maxVolume`、duration 接近度、`finishedAt`。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p module-bark-battle
|
||||
```
|
||||
|
||||
### Task 1.4: 领域规则单测覆盖作弊边界
|
||||
|
||||
**Objective:** 防止前端伪造 finish 直接刷榜。
|
||||
|
||||
**Files:**
|
||||
- Modify: `server-rs/crates/module-bark-battle/src/scoring.rs`
|
||||
|
||||
**Test cases:**
|
||||
- 28s-35s 合法窗口内可接受。
|
||||
- 1s / 300s 应 rejected 或 flagged。
|
||||
- `triggerCount > durationMs / minBarkGapMs + tolerance` 应 flagged。
|
||||
- `finalEnergy` 越界应 rejected。
|
||||
- 平/负不生成 leaderboard entry。
|
||||
- easy/normal/hard 不改变阈值、冷却、分数公式,只改变 AI preset key。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p module-bark-battle -- --nocapture
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段二:SpacetimeDB 表/reducer 与 api-server BFF
|
||||
|
||||
### Task 2.1: 设计 SpacetimeDB 表目录
|
||||
|
||||
**Objective:** 新增 Bark Battle 表并与 migration 对齐。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/spacetime-module/src/bark_battle/mod.rs`
|
||||
- Create: `server-rs/crates/spacetime-module/src/bark_battle/types.rs`
|
||||
- Create: `server-rs/crates/spacetime-module/src/bark_battle/tables.rs`
|
||||
- Modify: `server-rs/crates/spacetime-module/src/lib.rs`
|
||||
- Modify: `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
|
||||
**Tables:**
|
||||
- `bark_battle_draft_config`
|
||||
- `bark_battle_published_config`
|
||||
- `bark_battle_runtime_run`
|
||||
- `bark_battle_score_record`
|
||||
- `bark_battle_leaderboard_entry`
|
||||
- `bark_battle_work_stats_projection`
|
||||
- `bark_battle_personal_best_projection`
|
||||
|
||||
**Pitfalls:**
|
||||
- 表结构不要 derive `SpacetimeType`。
|
||||
- reducer 使用 `&ReducerContext`。
|
||||
- 授权身份来自 `ctx.sender()`。
|
||||
- 需要公开订阅的表才加 `public`。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-module
|
||||
```
|
||||
|
||||
### Task 2.2: 实现草稿/发布 reducer
|
||||
|
||||
**Objective:** 支持轻配置草稿保存和发布态 config 固化。
|
||||
|
||||
**Reducers:**
|
||||
- `create_bark_battle_draft`
|
||||
- `update_bark_battle_draft_config`
|
||||
- `publish_bark_battle_work`
|
||||
- `get_bark_battle_runtime_config` 如仓库约定使用 reducer/procedure 查询则按现有 pattern 实现。
|
||||
|
||||
**Rules:**
|
||||
- 草稿配置只允许标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。
|
||||
- 发布生成稳定作品 ID / config version。
|
||||
- 发布态 config 包含 `rulesetVersion`。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-module bark_battle
|
||||
```
|
||||
|
||||
### Task 2.3: 实现 run start / finish reducer
|
||||
|
||||
**Objective:** 打通正式运行态后端事务。
|
||||
|
||||
**Reducers:**
|
||||
- `start_bark_battle_run`
|
||||
- `finish_bark_battle_run`
|
||||
- `get_bark_battle_run`
|
||||
|
||||
**Rules:**
|
||||
- start 创建 `run_id` 和一次性 `run_token`。
|
||||
- start 记录 work/config/ruleset/difficulty 快照。
|
||||
- finish 必须校验 run token、未 finish、work/config/ruleset/difficulty 一致。
|
||||
- finish 调用 `module-bark-battle` 裁决结果。
|
||||
- accepted 写 score record。
|
||||
- `serverResult = player_win` 且排行榜开启且未 rejected 时写 leaderboard entry。
|
||||
- accepted / accepted_with_flags 更新 work stats 和 personal best projection。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-module bark_battle_run
|
||||
```
|
||||
|
||||
### Task 2.4: 更新 migration 与生成绑定
|
||||
|
||||
**Objective:** 让 SpacetimeDB 表结构变更可发布。
|
||||
|
||||
**Files:**
|
||||
- Modify: `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- Generated: `server-rs/crates/spacetime-client/src/module_bindings/*bark*`
|
||||
|
||||
**Commands:**
|
||||
按仓库现有脚本优先;不要手改 generated bindings。
|
||||
|
||||
```bash
|
||||
npm run spacetime:build
|
||||
npm run spacetime:generate
|
||||
```
|
||||
|
||||
若脚本名不同,先查 `package.json` 和 `server-rs` README。
|
||||
|
||||
### Task 2.5: 实现 spacetime-client facade
|
||||
|
||||
**Objective:** api-server 不直接操作 generated bindings。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/spacetime-client/src/bark_battle.rs`
|
||||
- Modify: `server-rs/crates/spacetime-client/src/lib.rs`
|
||||
|
||||
**Methods:**
|
||||
- `create_bark_battle_draft`
|
||||
- `save_bark_battle_draft_config`
|
||||
- `publish_bark_battle_work`
|
||||
- `get_bark_battle_runtime_config`
|
||||
- `start_bark_battle_run`
|
||||
- `finish_bark_battle_run`
|
||||
- `list_bark_battle_leaderboard`
|
||||
- `list_my_bark_battle_history`
|
||||
- `get_my_bark_battle_best_summary`
|
||||
- `get_bark_battle_work_stats`
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-client bark_battle
|
||||
```
|
||||
|
||||
### Task 2.6: 实现 api-server BFF 路由
|
||||
|
||||
**Objective:** 暴露前端需要的 HTTP API。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/api-server/src/bark_battle.rs`
|
||||
- Modify: `server-rs/crates/api-server/src/app.rs`
|
||||
|
||||
**Routes:**
|
||||
- `POST /api/bark-battle/drafts`
|
||||
- `PATCH /api/bark-battle/drafts/:draftId`
|
||||
- `POST /api/bark-battle/drafts/:draftId/publish`
|
||||
- `GET /api/bark-battle/works/:workId/runtime-config`
|
||||
- `POST /api/bark-battle/runs/start`
|
||||
- `POST /api/bark-battle/runs/:runId/finish`
|
||||
- `GET /api/bark-battle/works/:workId/leaderboard`
|
||||
- `GET /api/bark-battle/me/history`
|
||||
- `GET /api/bark-battle/me/best-summary`
|
||||
- `GET /api/bark-battle/works/:workId/stats`
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
cargo test -p api-server bark_battle
|
||||
npm run api-server
|
||||
curl -f http://127.0.0.1:<api-port>/healthz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 阶段三:最小前端纵切
|
||||
|
||||
### Task 3.1: 新增前端 service client
|
||||
|
||||
**Files:**
|
||||
- Create: `src/services/bark-battle/barkBattleClient.ts`
|
||||
- Test: `src/services/bark-battle/barkBattleClient.test.ts`
|
||||
|
||||
**Methods:** 与 BFF routes 一一对应。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm test -- --run src/services/bark-battle/barkBattleClient.test.ts
|
||||
```
|
||||
|
||||
### Task 3.2: 接入创作入口与 SelectionStage
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/config/newWorkEntryConfig.ts`
|
||||
- Modify: `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
- Modify: `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
**Rules:**
|
||||
- 新增 `bark-battle` play type。
|
||||
- 入口打开单页轻配置表单,不走复杂 agent workspace。
|
||||
- 移动端入口布局不能溢出。
|
||||
|
||||
### Task 3.3: 实现单页轻配置表单 + 预览卡片
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`
|
||||
- Create: `src/components/bark-battle-creation/BarkBattlePreviewCard.tsx`
|
||||
- Test: `src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx`
|
||||
|
||||
**UI fields:**
|
||||
- 标题必填
|
||||
- 简介选填
|
||||
- 主题/背景预设
|
||||
- 狗狗皮肤预设
|
||||
- 难度预设,默认 `normal`
|
||||
- 排行榜开关,默认开启
|
||||
|
||||
**UI constraints:**
|
||||
- 不堆大段玩法说明。
|
||||
- 按现有游戏 UI 风格设计。
|
||||
- 移动端优先。
|
||||
|
||||
### Task 3.4: 发布后进入作品详情
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx`
|
||||
- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- Modify: `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
|
||||
**Rules:**
|
||||
- 发布成功刷新 works/gallery/shelf。
|
||||
- 跳作品详情。
|
||||
- 详情 CTA 可以进入正式 runtime。
|
||||
|
||||
### Task 3.5: runtime 拉发布态 config 并 start / finish
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/games/bark-battle/*`
|
||||
- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- Create/Modify: `src/components/bark-battle-runtime/BarkBattleRuntimeRoute.tsx` 如需要
|
||||
|
||||
**Rules:**
|
||||
- runtime 通过稳定 `workId` 拉 `BarkBattleRuntimeConfig`。
|
||||
- 开始正式局时调用 start run。
|
||||
- 结束时提交 finish 派生指标。
|
||||
- 结算展示 `serverResult`、`scoreSummary`、`antiCheatFlags`、leaderboard entry。
|
||||
- 麦克风原始音频不上传。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 阶段四:投影与列表体验
|
||||
|
||||
### Task 4.1: 排行榜 UI
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.tsx`
|
||||
- Test: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.test.tsx`
|
||||
- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx`
|
||||
|
||||
**Rules:**
|
||||
- 查询维度 `workId + difficultyPreset + rulesetVersion`。
|
||||
- 只展示胜利入榜成绩。
|
||||
- 不展示平/负/flagged 历史。
|
||||
|
||||
### Task 4.2: 个人历史最近记录 + 最佳摘要 UI
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.tsx`
|
||||
- Test: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.test.tsx`
|
||||
|
||||
**Rules:**
|
||||
- 默认最近 20 条。
|
||||
- 仅本人可见。
|
||||
- 可按 workId / difficultyPreset 过滤。
|
||||
- flagged 只做轻提示,不展示详细反作弊原因。
|
||||
|
||||
### Task 4.3: 作品统计展示
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.tsx`
|
||||
- Test: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.test.tsx`
|
||||
|
||||
**Fields:**
|
||||
- `playStartCount`
|
||||
- `finishCount`
|
||||
- `winCount`
|
||||
- `drawCount`
|
||||
- `lossCount`
|
||||
- `flaggedCount`
|
||||
- `leaderboardEntryCount`
|
||||
- `bestLeaderboardScore`
|
||||
- `bestFinalEnergy`
|
||||
- `averageFinalEnergy`
|
||||
- `updatedAt`
|
||||
|
||||
### Task 4.4: 广场卡片/我的作品适配
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- Modify: `src/components/rpg-entry/rpgEntryWorldPresentation.ts`
|
||||
- Modify: `src/services/publicWorkCode.ts` 如分享码需要支持
|
||||
|
||||
**Rules:**
|
||||
- Bark Battle 作品能展示、打开详情、开始游玩。
|
||||
- 不新增独立 Bark Battle 专区。
|
||||
|
||||
---
|
||||
|
||||
## 6. 阶段五:收口验证
|
||||
|
||||
### Task 5.1: 自动测试清单
|
||||
|
||||
```bash
|
||||
cargo test -p shared-contracts bark_battle
|
||||
cargo test -p module-bark-battle
|
||||
cargo test -p spacetime-module bark_battle
|
||||
cargo test -p spacetime-client bark_battle
|
||||
cargo test -p api-server bark_battle
|
||||
npm test -- --run packages/shared/src/contracts/barkBattle.test.ts
|
||||
npm test -- --run src/services/bark-battle/barkBattleClient.test.ts
|
||||
npm test -- --run src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx
|
||||
npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx
|
||||
npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
### Task 5.2: 后端 smoke
|
||||
|
||||
1. 按项目脚本启动 SpacetimeDB + api-server,优先使用 `npm run api-server`,不要使用旧命令。
|
||||
2. 确认 `/healthz`。
|
||||
3. smoke 流程:创建草稿 → 保存配置 → 发布 → 拉 runtime config → start run → finish run → 查询 leaderboard/history/stats。
|
||||
|
||||
### Task 5.3: 人工验收路径
|
||||
|
||||
1. 进入创作入口/玩法选择,选择 Bark Battle。
|
||||
2. 在单页轻配置表单中填写标题,选择主题、狗狗皮肤、难度,保持排行榜开启。
|
||||
3. 保存草稿。
|
||||
4. 发布作品。
|
||||
5. 发布后自动进入作品详情。
|
||||
6. 点击开始游玩进入正式 runtime。
|
||||
7. 授权麦克风,完成 30 秒单局。
|
||||
8. 结算页显示后端 `serverResult` 和 score summary。
|
||||
9. 若胜利,排行榜出现本局成绩。
|
||||
10. 我的记录显示最近记录和个人最佳摘要。
|
||||
11. 作品详情/作者视角能看到作品统计。
|
||||
12. 广场/作品卡片和我的作品入口都能再次进入详情和 runtime。
|
||||
|
||||
---
|
||||
|
||||
## 7. 不做范围
|
||||
|
||||
- 不做实时多人。
|
||||
- 不做 ghost replay。
|
||||
- 不做 AI 狗叫识别。
|
||||
- 不保存原始音频、PCM、waveform 或可还原语音内容。
|
||||
- 不做独立 Bark Battle 专区/活动页。
|
||||
- 不做挑战分享、好友邀请、多人数房间。
|
||||
- 不做复杂编辑器、多步骤向导、规则参数编辑、AI 生成配置。
|
||||
- 不做 DAU/留存、按小时统计曲线、好友对比。
|
||||
|
||||
---
|
||||
|
||||
## 8. 三人并行建议
|
||||
|
||||
### 开发者 A:后端契约与领域规则
|
||||
|
||||
负责 Task 1.1、1.3、1.4。先提交 contracts 与 `module-bark-battle`,为后续后端/前端提供稳定类型和裁决规则。
|
||||
|
||||
### 开发者 B:SpacetimeDB + api-server
|
||||
|
||||
负责 Task 2.1 到 2.6。必须等开发者 A 的 DTO/领域规则基本稳定后开始,或先基于计划字段开分支实现表结构。
|
||||
|
||||
### 开发者 C:前端纵切与 UI
|
||||
|
||||
负责 Task 3.x 与 4.x。开始时可先做组件空态和 service client 类型,真正联调等 B 的 BFF ready。
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐提交节奏
|
||||
|
||||
1. `feat: add bark battle contracts and domain rules`
|
||||
2. `feat: add bark battle spacetime tables and reducers`
|
||||
3. `feat: add bark battle api server routes`
|
||||
4. `feat: add bark battle creation editor`
|
||||
5. `feat: connect bark battle runtime to server results`
|
||||
6. `feat: add bark battle leaderboard history stats`
|
||||
7. `docs: finalize bark battle phase2 verification guide`
|
||||
|
||||
---
|
||||
|
||||
## 10. 完成定义
|
||||
|
||||
Phase 2 完成必须同时满足:
|
||||
|
||||
- Bark Battle 可以从正式创作入口创建轻配置作品。
|
||||
- 作品可以发布为稳定 workId。
|
||||
- 作品详情/广场/我的作品可以发现并进入正式 runtime。
|
||||
- runtime 从后端发布态 config 拉配置。
|
||||
- start run 写 `work_play_start`。
|
||||
- finish 只上传派生指标。
|
||||
- 后端裁决 `serverResult` / `scoreSummary` / `leaderboardScore` / `antiCheatFlags`。
|
||||
- 胜利局进入按 `workId + difficultyPreset + rulesetVersion` 分榜的排行榜。
|
||||
- 个人历史和作品统计可查询。
|
||||
- 自动测试、encoding、typecheck、diff check 和人工验收路径通过。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user