Compare commits
247 Commits
8d46c05129
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
| e8d832c1ea | |||
| e8648e45fc | |||
| b08f878841 | |||
| ae58a443a3 | |||
| bf4423e53b | |||
| 57de9a8df6 | |||
| c1131e6f55 | |||
| 2a75a19ece | |||
| 5b96265c50 | |||
| ad1b91498d | |||
| 2277b37888 | |||
| be53a90f77 | |||
| 71c7dd2558 | |||
| bcd7617fb7 | |||
| a6de4d8a32 | |||
| 49468441bc | |||
| 8ddaf72eb7 | |||
| a92dc2b7b0 | |||
| 4fecf9c975 | |||
| ad970797e9 | |||
| c3fbf7a30b | |||
| b13870f71b | |||
| e4a8bd42bb | |||
| 01c5ab985a | |||
| ac12f1ed5e | |||
| e36a562098 | |||
| 36e134e323 | |||
| 26139f80d3 | |||
| 9b72dbb3ea | |||
| 188c6704db | |||
| d641840098 | |||
| aec9142481 | |||
| d41f260a2a | |||
| cf074837a4 | |||
| ed7a6f48d0 | |||
| 8c6ec9e6e4 | |||
| 33c9079d3b | |||
| 7b4ba61b4d | |||
| ea0b67a951 | |||
| 4dfa8452db | |||
| 22810245f5 | |||
| eb76bfc031 | |||
| 183e78d475 | |||
| 612d105a23 | |||
| b994acf635 | |||
| ef4f91a75e | |||
| 481a27fc53 | |||
| 54968701f0 | |||
| 5cb5329f4e | |||
| 2b046656dc | |||
| 7cea41c911 | |||
| 928acb4302 | |||
| bf72c2e48d | |||
| e30b733b17 | |||
| fa61eeb0b0 | |||
| 2ca096f821 | |||
| 2b6087de4c | |||
| fda996031f | |||
| 10ed4fa051 | |||
| ac2cf78ffa | |||
| 5edfb756c7 | |||
| 0c9254502c | |||
| 0461c0ee41 | |||
| 81f57ea5ce | |||
| d23cf3807d | |||
| 6c1579a786 | |||
| 793d82cccd | |||
| 5cc8293380 | |||
| 85ed8ca90c | |||
| d0a9348e72 | |||
| 192accd796 | |||
| 54c2d6de47 | |||
| 7f2461313e | |||
| f74717c415 | |||
| 75bca28191 | |||
| 46a254f142 | |||
| 86fc382413 | |||
| 643161a168 | |||
| d6219f1a0c | |||
| 35d63f5b2e | |||
| 1c16152708 | |||
| f6084d0910 | |||
| 6ed6859855 | |||
|
|
9b39a52049 | ||
|
|
fc54bff62f | ||
| 1767bed609 | |||
| dada5a4797 | |||
| 7e608d4230 | |||
| 3ad1075227 | |||
| 32a1530ab1 | |||
| 7c8aa1e124 | |||
| 641d91cf11 | |||
| 052dbc248b | |||
| bc704d0c22 | |||
| a0ed128bde | |||
| 80a4183b45 | |||
| 8669a996ca | |||
| 9ca66715a4 | |||
| e390b72a0c | |||
| cf9fb5ac40 | |||
| a1e5c2150c | |||
| 23ba2703b4 | |||
| 96df12cd15 | |||
| 65c2b8cd79 | |||
| 199b44c18c | |||
| e410f7974e | |||
| 94975e4735 | |||
| b2ac92e0fc | |||
| edcdc01e43 | |||
| 621bf6506c | |||
| e226c39b2c | |||
| 26a3c89d1d | |||
| 7e35231dfe | |||
| 3efc646868 | |||
| 747ef790ac | |||
| b995809f75 | |||
| 72fce47187 | |||
| 63aaf1ecaf | |||
| 1f6ac7dddd | |||
| c55745f70b | |||
| 5e419fa2f7 | |||
| 539ddbde24 | |||
| e694c6605a | |||
| 326cc6b062 | |||
| 7e8cc22859 | |||
| 20d5121c6c | |||
| 91d993dc6b | |||
| f343555a19 | |||
| 4aa127cd18 | |||
| 78ce8527fc | |||
| 424e75a922 | |||
| 7eb531ccca | |||
| 3b0dd2ebeb | |||
| b5c8ec304f | |||
| 104e19d257 | |||
| 0b71fa8eb0 | |||
| 98be6eb0e4 | |||
| abf1f1ebea | |||
| bdded3d708 | |||
| b08127031c | |||
| 0235200d32 | |||
| e8fee0172a | |||
| a71df45437 | |||
| df80876f60 | |||
| 9146e5b8ec | |||
| fd16485827 | |||
| 89be59d701 | |||
| 59ef2ab472 | |||
| 13547091ca | |||
| 6a830b349b | |||
| d5d3fa1641 | |||
| 0816d2e326 | |||
| dcd5201bb3 | |||
| cc9d289310 | |||
| 3111e22288 | |||
| 014e88afaa | |||
| cf27686e17 | |||
| ca394766d1 | |||
| e9dfcda418 | |||
| b27424105d | |||
| 7364d0f2d3 | |||
| b25b14a329 | |||
| 950a785213 | |||
| 95a2adbdc0 | |||
| ceea868478 | |||
| bc0ebe3c25 | |||
| 3ccbe6fe77 | |||
| d31a28178e | |||
| 96e1d7c4fb | |||
|
|
0fae3319a9 | ||
|
|
d06107f2c6 | ||
|
|
60b667a9d1 | ||
| 2252afb080 | |||
| 06b8b46530 | |||
| 07e777fef8 | |||
|
|
995661e7cc | ||
| 100fee7e7a | |||
| 1d9d8c2e41 | |||
| 44d9bd55de | |||
| 8692dbad85 | |||
| 5c7c039e52 | |||
| 9f3e34e81a | |||
| 1142e90a35 | |||
|
|
e847fcea6f | ||
|
|
46d240e37d | ||
| 45719e7650 | |||
| 34aecdddf1 | |||
| f1e86a88da | |||
| ce98a29c4d | |||
| 9baa515a75 | |||
| 49aad7311c | |||
| 01b302d7eb | |||
| 8e6d1971ea | |||
| fecac5344a | |||
| 27342a8cca | |||
| cc38057c3c | |||
| 62afaf620a | |||
| 562b5eb720 | |||
| e0d0531c9c | |||
| 96f13bdfed | |||
| d39ac86c27 | |||
| 801d1d534a | |||
| 019c8a2b03 | |||
| 4358f38259 | |||
| 98c83478ad | |||
| 36c81c30be | |||
| a18f4db4bb | |||
| 813dbf1fdd | |||
| 39b1141287 | |||
| ace13c7047 | |||
| 73d5ef40ed | |||
| 6c519970b4 | |||
| e61b1a1586 | |||
| 5831703156 | |||
| 543ccf2509 | |||
| d48916157b | |||
| 7160e90909 | |||
| 64fda2a677 | |||
| 0c9e58b75a | |||
| 06d6f7716e | |||
| a53167c872 | |||
| 23cb37c18a | |||
| ae33f10f17 | |||
| bdc3257003 | |||
| 5b1fa72ad7 | |||
| acc55d0e13 | |||
| a2c71fcb3a | |||
| 2311edb2e6 | |||
| 4716d9b582 | |||
|
|
9b5aa25fe9 | ||
|
|
8f4ca9abfa | ||
|
|
879a53bf8d | ||
|
|
9d9913095d | ||
|
|
fb6fb6e9f5 | ||
|
|
d796e7d491 | ||
|
|
1abec5754e | ||
| 33dd105630 | |||
| c3b07550f4 | |||
| 9a3db67e13 | |||
| fd08262bf0 | |||
| 7ab0933f6d | |||
| 62934b0809 | |||
|
|
f82775b852 | ||
|
|
9d3fcfae77 | ||
|
|
aa2e9b36d7 | ||
|
|
39200ea9cc | ||
| 90a19aeb0d |
1
.codex/skills/behavior-driven-development
Normal file
@@ -0,0 +1 @@
|
||||
C:/proj/Genarrative/.hermes/skills/behavior-driven-development
|
||||
389
.codex/skills/genarrative-play-type-integration/SKILL.md
Normal file
@@ -0,0 +1,389 @@
|
||||
---
|
||||
name: genarrative-play-type-integration
|
||||
description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Hermes Agent
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Genarrative 新增玩法类型接入流程
|
||||
|
||||
用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增一个游戏玩法入口
|
||||
- 让某个玩法从“敬请期待”变为可创建
|
||||
- 为新玩法补齐创作工作台、结果页、发布与试玩链路
|
||||
- 将新玩法接入创作中心作品架与广场
|
||||
|
||||
## 先判断接入级别
|
||||
|
||||
### 1. 只做入口占位
|
||||
|
||||
只需要新增入口配置,不接 session/workspace/result/runtime。
|
||||
|
||||
适合:
|
||||
|
||||
- 敬请期待
|
||||
- 灰度占位
|
||||
|
||||
### 2. 可进入创作工作台
|
||||
|
||||
需要补齐前端分流、session、工作台、结果页,至少能生成草稿。
|
||||
|
||||
### 3. 完整玩法闭环
|
||||
|
||||
需要补齐:
|
||||
|
||||
- 创作入口
|
||||
- 工作台
|
||||
- 草稿生成
|
||||
- 结果页
|
||||
- 发布
|
||||
- 试玩 runtime
|
||||
- 作品架 / 广场 / 分享
|
||||
|
||||
## 推荐接入顺序
|
||||
|
||||
### Step 1: 先定玩法 ID 和能力边界
|
||||
|
||||
先明确:
|
||||
|
||||
- `id` 是什么
|
||||
- 入口是否可见
|
||||
- 是否可点击创建
|
||||
- 是否需要对话式创作
|
||||
- 是否需要生成中页面
|
||||
- 是否需要 result/runtime/gallery/share
|
||||
|
||||
不要先随便起临时 ID 再改名。
|
||||
|
||||
### Step 2: 新增入口配置
|
||||
|
||||
文件:
|
||||
|
||||
- `src/config/newWorkEntryConfig.ts`
|
||||
|
||||
在 `creationTypes` 中新增:
|
||||
|
||||
- `id`
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `badge`
|
||||
- `visible`
|
||||
- `open`
|
||||
|
||||
如果只是占位:
|
||||
|
||||
- `visible: true`
|
||||
- `open: false`
|
||||
|
||||
### Step 3: 确认类型过滤逻辑
|
||||
|
||||
文件:
|
||||
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
|
||||
检查:
|
||||
|
||||
- `getVisiblePlatformCreationTypes()` 是否能展示新类型
|
||||
- `isPlatformCreationTypeVisible()` 是否能识别新类型
|
||||
- `locked` / `hidden` 是否正确映射
|
||||
|
||||
### Step 4: 扩展页面阶段
|
||||
|
||||
文件:
|
||||
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
|
||||
为新玩法补充 `SelectionStage`:
|
||||
|
||||
- `*-agent-workspace`
|
||||
- `*-generating`(可选)
|
||||
- `*-result`
|
||||
- `*-runtime`(可选)
|
||||
- `*-gallery-detail`(可选)
|
||||
|
||||
### Step 5: 在总流程中加类型分流
|
||||
|
||||
文件:
|
||||
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
在 `handleCreationHubCreateType(type)` 中新增分支,确保:
|
||||
|
||||
- 能进入对应工作台
|
||||
- 能设置对应 `selectionStage`
|
||||
- 能关闭类型弹层
|
||||
|
||||
同时按玩法补齐:
|
||||
|
||||
- `open<Play>AgentWorkspace()`
|
||||
- `leave<Play>Flow()`
|
||||
- `submit<Play>Message()`(对话式玩法)
|
||||
- `execute<Play>Action()`
|
||||
|
||||
### Step 6: 接入通用 Agent flow controller
|
||||
|
||||
文件:
|
||||
|
||||
- `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
|
||||
|
||||
如果是 Agent 型玩法,复用通用控制器:
|
||||
|
||||
- `createSession`
|
||||
- `getSession`
|
||||
- `streamMessage`
|
||||
- `executeAction`
|
||||
- `isBusy`
|
||||
- `error`
|
||||
- `streamingReplyText`
|
||||
- `selectionStage` 切换
|
||||
|
||||
### Step 7: 定义 shared contracts
|
||||
|
||||
前端:
|
||||
|
||||
- `packages/shared/src/contracts/`
|
||||
|
||||
后端:
|
||||
|
||||
- `server-rs/crates/shared-contracts/src/`
|
||||
|
||||
至少补齐:
|
||||
|
||||
- session snapshot
|
||||
- create session request/response
|
||||
- message request/response
|
||||
- action request/response
|
||||
- draft/result 结构
|
||||
- work summary / gallery 结构(如果需要)
|
||||
- runtime 结构(如果需要)
|
||||
|
||||
### Step 8: 实现前端 service client
|
||||
|
||||
目录参考:
|
||||
|
||||
- `src/services/`
|
||||
|
||||
按玩法补:
|
||||
|
||||
- creation client
|
||||
- runtime client(可选)
|
||||
- works client(可选)
|
||||
- gallery client(可选)
|
||||
|
||||
建议保持和现有玩法一致的 API base 与命名风格。
|
||||
|
||||
### Step 9: 接后端 API
|
||||
|
||||
文件参考:
|
||||
|
||||
- `server-rs/crates/api-server/src/puzzle.rs`
|
||||
- `server-rs/crates/api-server/src/puzzle_agent_turn.rs`
|
||||
- `server-rs/crates/api-server/src/match3d.rs`
|
||||
|
||||
通常需要:
|
||||
|
||||
- create session
|
||||
- get session
|
||||
- send message
|
||||
- stream message
|
||||
- execute action
|
||||
- publish / save / delete
|
||||
- runtime start / action(可选)
|
||||
- gallery / detail(可选)
|
||||
|
||||
后端设计优先按 Genarrative 的 DDD 分层拆开,不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里:
|
||||
|
||||
- `module-<play>`:纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。
|
||||
- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。
|
||||
- `spacetime-module`:表定义、reducer/procedure、事务编排、migration;表结构变化要同步生成绑定。
|
||||
- `spacetime-client`:api-server 到 SpacetimeDB 的 facade,隐藏 reducer 调用细节。
|
||||
- `api-server`:Axum 路由、鉴权、SSE/stream、应用层编排。
|
||||
- `platform-*`:LLM、资产上传、鉴权、第三方服务等副作用。
|
||||
|
||||
建议按四条线设计后端能力:
|
||||
|
||||
- Agent 创作线:session、turn、stream、compile action。
|
||||
- Works 作品线:保存、发布、删除、草稿恢复。
|
||||
- Gallery 广场线:公开列表、详情、like/remix/share。
|
||||
- Runtime 运行态线:开始试玩、提交动作、读取状态。
|
||||
|
||||
### Step 10: 新增工作台组件
|
||||
|
||||
目录建议:
|
||||
|
||||
- `src/components/<play>-creation/<Play>AgentWorkspace.tsx`
|
||||
|
||||
两种形态:
|
||||
|
||||
#### 对话式
|
||||
|
||||
适合设定逐轮补齐。
|
||||
|
||||
参考:
|
||||
|
||||
- `BigFishAgentWorkspace.tsx`
|
||||
- `Match3DAgentWorkspace.tsx`
|
||||
|
||||
#### 表单式
|
||||
|
||||
适合输入结构明确的玩法。
|
||||
|
||||
参考:
|
||||
|
||||
- `PuzzleAgentWorkspace.tsx`
|
||||
|
||||
### Step 11: 在渲染树中挂载新页面
|
||||
|
||||
文件:
|
||||
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
补齐:
|
||||
|
||||
- workspace 分支
|
||||
- generating 分支(如需要)
|
||||
- result 分支
|
||||
- runtime 分支(如需要)
|
||||
|
||||
### Step 12: 新增结果页
|
||||
|
||||
目录建议:
|
||||
|
||||
- `src/components/<play>-result/<Play>ResultView.tsx`
|
||||
|
||||
结果页至少支持:
|
||||
|
||||
- 展示 draft
|
||||
- 返回编辑
|
||||
- 发布
|
||||
- 试玩
|
||||
- 错误展示
|
||||
|
||||
### Step 13: 需要试玩就补 runtime
|
||||
|
||||
目录建议:
|
||||
|
||||
- `src/components/<play>-runtime/<Play>RuntimeShell.tsx`
|
||||
|
||||
如果玩法是游戏类,建议补完整 runtime 闭环。
|
||||
|
||||
### Step 14: 接入作品架 / 广场 / 分享
|
||||
|
||||
需要改:
|
||||
|
||||
- `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- `src/services/publicWorkCode.ts`
|
||||
|
||||
如果玩法支持发布,还要补:
|
||||
|
||||
- public work code
|
||||
- public detail
|
||||
- publish share modal
|
||||
- like/remix(可选)
|
||||
|
||||
### Step 15: 处理登录态与草稿恢复
|
||||
|
||||
要考虑:
|
||||
|
||||
- 刷新恢复草稿
|
||||
- 退出登录清空私有状态
|
||||
- result/draft 缺失时回退
|
||||
- busy / generating / runtime 中断恢复
|
||||
|
||||
### Step 16: 补测试
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- 入口展示
|
||||
- 类型分流
|
||||
- 工作台打开
|
||||
- session 创建
|
||||
- compile action
|
||||
- result 页切换
|
||||
- 发布后刷新作品架
|
||||
- runtime 进入与退出
|
||||
|
||||
## 最小改动清单
|
||||
|
||||
### 只做占位
|
||||
|
||||
只改:
|
||||
|
||||
- `src/config/newWorkEntryConfig.ts`
|
||||
|
||||
### 做到可进入工作台
|
||||
|
||||
至少改:
|
||||
|
||||
- `newWorkEntryConfig.ts`
|
||||
- `platformEntryTypes.ts`
|
||||
- `PlatformEntryFlowShellImpl.tsx`
|
||||
- 新玩法 service client
|
||||
- 新玩法工作台组件
|
||||
- shared contracts
|
||||
- 后端 API
|
||||
|
||||
### 做到完整闭环
|
||||
|
||||
还要补:
|
||||
|
||||
- result 页
|
||||
- runtime
|
||||
- works / gallery
|
||||
- public code
|
||||
- share
|
||||
- 作品架聚合
|
||||
- 测试
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 只加入口配置不够,类型分流和页面阶段也要补。
|
||||
2. `SelectionStage` 不扩展,前端无法安全切页。
|
||||
3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。
|
||||
4. 发布后不刷新 works/gallery,用户会看不到新作品。
|
||||
5. 如果走 SpacetimeDB,表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。
|
||||
6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO;先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤),RED 后再补领域类型与聚合函数。
|
||||
7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucket;bucket 输出要有稳定排序,并显式携带 `bucketKey`、`bucketStartDateKey`、`bucketEndDateKey`、`value`。
|
||||
8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type union;admin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。
|
||||
9. 退出登录时要清空新玩法私有状态,避免串用户。
|
||||
10. 移动端入口卡片增多后要检查布局和滚动体验。
|
||||
|
||||
## 验证标准
|
||||
|
||||
一个玩法算真正接入成功,至少要满足:
|
||||
|
||||
- 入口能展示
|
||||
- 能进入对应工作台
|
||||
- 能创建 session
|
||||
- 能生成草稿
|
||||
- 能进入结果页
|
||||
- 能返回编辑
|
||||
- 如果需要,可试玩
|
||||
- 如果需要,可发布
|
||||
- 发布后能回到作品架 / 广场 / 分享链路
|
||||
|
||||
## 建议验证命令
|
||||
|
||||
按改动范围选择:
|
||||
|
||||
```bash
|
||||
# 后端 contracts / module-runtime / api-server
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts
|
||||
cargo test -p module-runtime
|
||||
cargo check -p api-server
|
||||
|
||||
# SpacetimeDB schema/reducer/procedure 改动后,优先在有 CLI 的机器重新生成 bindings
|
||||
npm run spacetime:generate -- --rust-only
|
||||
|
||||
# 前端类型
|
||||
npm run admin-web:typecheck
|
||||
```
|
||||
|
||||
如果新增完整前端玩法闭环,还要按项目实际脚本补充 web typecheck、lint 或 Playwright/单元测试。
|
||||
85
.codex/skills/gpt-image-2-apimart/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: gpt-image-2-apimart
|
||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
---
|
||||
|
||||
# gpt-image-2 VectorEngine
|
||||
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the local task and decide whether the image is project-bound.
|
||||
2. Prefer `scripts/generate-template-samples.mjs` for puzzle template thumbnails or small batches.
|
||||
3. Run dry-run first:
|
||||
|
||||
```powershell
|
||||
node .codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs --dry-run
|
||||
```
|
||||
|
||||
4. If dry-run looks correct and the user asked for real assets, run live generation with a small limit:
|
||||
|
||||
```powershell
|
||||
node .codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs --live --limit 6
|
||||
```
|
||||
|
||||
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
||||
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
|
||||
|
||||
## Request Contract
|
||||
|
||||
The repository image path uses:
|
||||
|
||||
```text
|
||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Default body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-image-2-all",
|
||||
"prompt": "<prompt>",
|
||||
"n": 1,
|
||||
"size": "1024x1024"
|
||||
}
|
||||
```
|
||||
|
||||
For a reference image, add:
|
||||
|
||||
```json
|
||||
{
|
||||
"image": ["data:image/png;base64,..."]
|
||||
}
|
||||
```
|
||||
|
||||
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints.
|
||||
|
||||
## Environment
|
||||
|
||||
Load environment values from process env first, then `.env.secrets.local`, `.env.local`, and `.env.example`.
|
||||
|
||||
Required for live generation:
|
||||
|
||||
- `VECTOR_ENGINE_BASE_URL`
|
||||
- `VECTOR_ENGINE_API_KEY`
|
||||
|
||||
Optional:
|
||||
|
||||
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
|
||||
|
||||
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
|
||||
|
||||
## Prompt Rules
|
||||
|
||||
- Use Chinese prompts when generating project puzzle templates.
|
||||
- Keep template samples square, clear, image-only, and suitable for puzzle thumbnails.
|
||||
- Avoid text, watermark, UI chrome, buttons, borders, and tutorial overlays.
|
||||
- Include local negative constraints in the prompt instead of relying on provider-specific negative prompt fields.
|
||||
|
||||
## Resources
|
||||
|
||||
- `scripts/generate-template-samples.mjs`: dry-run or live-generate puzzle template sample thumbnails.
|
||||
- `assets/puzzle-template-prompts.json`: default prompt list consumed by the script.
|
||||
7
.codex/skills/gpt-image-2-apimart/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "GPT Image 2 VectorEngine"
|
||||
short_description: "Generate project thumbnails through VectorEngine"
|
||||
brand_color: "#10B981"
|
||||
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"id": "couple-memory",
|
||||
"title": "情侣合照拼图",
|
||||
"prompt": "温暖自然光下的一对情侣纪念合照,城市咖啡馆窗边,桌面有花束和两杯热饮,人物神情自然,画面主体清晰,前中后景层次明确,适合切成拼图。"
|
||||
},
|
||||
{
|
||||
"id": "family-keepsake",
|
||||
"title": "家庭纪念拼图",
|
||||
"prompt": "三代家人在客厅沙发前的家庭纪念合照,柔和午后阳光,孩子抱着生日蛋糕,长辈微笑,画面温暖完整,细节丰富但不杂乱。"
|
||||
},
|
||||
{
|
||||
"id": "friends-party",
|
||||
"title": "朋友聚会拼图",
|
||||
"prompt": "朋友们在露台夜晚聚会,彩灯、桌上零食和举杯瞬间,人物分布有层次,中央焦点清楚,氛围轻松热闹,适合社交分享拼图。"
|
||||
},
|
||||
{
|
||||
"id": "festival-card",
|
||||
"title": "节日贺卡拼图",
|
||||
"prompt": "节日餐桌与礼物布置,暖色灯光、彩带、蜡烛和窗外烟花,画面像无字贺卡,主体集中,边角细节可辨,适合节日拼图。"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-summary",
|
||||
"title": "知识总结拼图",
|
||||
"prompt": "一张无文字的知识学习主题插画,书桌上有打开的笔记本、便签、咖啡、台灯和思维导图式图形元素,构图整洁,重点明确,适合学习打卡拼图。"
|
||||
},
|
||||
{
|
||||
"id": "product-detail",
|
||||
"title": "商品细节拼图",
|
||||
"prompt": "精致商品静物展示,一只高质感香水瓶放在丝绸与花瓣之间,玻璃反光清晰,包装和材质细节丰富,背景干净,适合作为电商细节拼图。"
|
||||
},
|
||||
{
|
||||
"id": "healing-landscape",
|
||||
"title": "治愈风景拼图",
|
||||
"prompt": "治愈风景插画,清晨湖边、薄雾、远山、木栈道和一盏小灯,色彩柔和,层次清楚,局部元素可辨,适合长时间拼图。"
|
||||
},
|
||||
{
|
||||
"id": "cute-pet",
|
||||
"title": "宠物可爱拼图",
|
||||
"prompt": "一只可爱的橘猫趴在阳光窗台上,旁边有绿植、毛线球和小毯子,猫的表情清楚,画面温柔干净,适合萌宠拼图分享。"
|
||||
},
|
||||
{
|
||||
"id": "hot-topic-poster",
|
||||
"title": "热点海报拼图",
|
||||
"prompt": "电影感热点海报风插画,雨夜街头、霓虹反光、奔跑的人影和远处光束,强烈视觉焦点,画面无文字,适合热点话题拼图。"
|
||||
},
|
||||
{
|
||||
"id": "event-invitation",
|
||||
"title": "活动邀请拼图",
|
||||
"prompt": "活动邀请主题插画,展厅入口、花艺装置、签到台和柔和灯带,人群剪影自然分布,画面高级干净,无文字,适合活动预热拼图。"
|
||||
},
|
||||
{
|
||||
"id": "daily-challenge",
|
||||
"title": "每日挑战拼图",
|
||||
"prompt": "每日挑战主题插画,清爽桌面上摆放相机、明信片、计时器和小奖章,色彩明亮,构图有趣,细节可拆解,适合平台每日拼图。"
|
||||
},
|
||||
{
|
||||
"id": "children-learning",
|
||||
"title": "儿童认知拼图",
|
||||
"prompt": "儿童认知学习插画,木质桌面上有积木、彩色形状、动物玩偶和小书本,色彩明快,元素边界清晰,无文字,适合儿童教育拼图。"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,321 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const skillRoot = path.resolve(__dirname, '..');
|
||||
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
|
||||
const promptsPath = path.join(
|
||||
skillRoot,
|
||||
'assets',
|
||||
'puzzle-template-prompts.json',
|
||||
);
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
||||
const defaultTimeoutMs = 180000;
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 1) {
|
||||
const raw = process.argv[index];
|
||||
if (raw.startsWith('--')) {
|
||||
const next = process.argv[index + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
args.set(raw, next);
|
||||
index += 1;
|
||||
} else {
|
||||
args.set(raw, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readDotenv(fileName) {
|
||||
const filePath = path.join(repoRoot, fileName);
|
||||
if (!existsSync(filePath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const values = {};
|
||||
for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
let value = match[2].trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
values[match[1]] = value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function resolveEnv() {
|
||||
const loaded = {
|
||||
...readDotenv('.env.example'),
|
||||
...readDotenv('.env.local'),
|
||||
...readDotenv('.env.secrets.local'),
|
||||
...process.env,
|
||||
};
|
||||
return {
|
||||
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
|
||||
.trim()
|
||||
.replace(/\/+$/u, ''),
|
||||
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
|
||||
timeoutMs: Number.parseInt(
|
||||
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
||||
10,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildVectorEngineImagesGenerationUrl(baseUrl) {
|
||||
return baseUrl.endsWith('/v1')
|
||||
? `${baseUrl}/images/generations`
|
||||
: `${baseUrl}/v1/images/generations`;
|
||||
}
|
||||
|
||||
function buildPrompt(template) {
|
||||
return [
|
||||
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
|
||||
`画面主体:${template.prompt}`,
|
||||
'要求:主体清晰集中,前中后景层次明确,边角有可辨识细节,适合切成 3x3 到 7x7 拼图。',
|
||||
'避免:文字、水印、边框、按钮、UI 元素、教程标注、低清晰度、过度模糊、杂乱构图。',
|
||||
].join('');
|
||||
}
|
||||
|
||||
function collectStringsByKey(value, targetKey, output) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((entry) => collectStringsByKey(entry, targetKey, output));
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, nested] of Object.entries(value)) {
|
||||
if (key === targetKey) {
|
||||
if (typeof nested === 'string' && nested.trim()) {
|
||||
output.push(nested.trim());
|
||||
}
|
||||
if (Array.isArray(nested)) {
|
||||
nested.forEach((entry) => {
|
||||
if (typeof entry === 'string' && entry.trim()) {
|
||||
output.push(entry.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
collectStringsByKey(nested, targetKey, output);
|
||||
}
|
||||
}
|
||||
|
||||
function extractImageUrls(payload) {
|
||||
const urls = [];
|
||||
collectStringsByKey(payload, 'url', urls);
|
||||
collectStringsByKey(payload, 'image', urls);
|
||||
collectStringsByKey(payload, 'image_url', urls);
|
||||
return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url));
|
||||
}
|
||||
|
||||
function extractBase64Images(payload) {
|
||||
const values = [];
|
||||
collectStringsByKey(payload, 'b64_json', values);
|
||||
return values;
|
||||
}
|
||||
|
||||
function inferExtensionFromContentType(contentType) {
|
||||
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
||||
if (normalized === 'image/png') {
|
||||
return 'png';
|
||||
}
|
||||
if (normalized === 'image/webp') {
|
||||
return 'webp';
|
||||
}
|
||||
if (normalized === 'image/gif') {
|
||||
return 'gif';
|
||||
}
|
||||
return 'jpg';
|
||||
}
|
||||
|
||||
function inferExtensionFromBytes(bytes) {
|
||||
if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) {
|
||||
return 'png';
|
||||
}
|
||||
if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
|
||||
return 'jpg';
|
||||
}
|
||||
if (
|
||||
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
|
||||
bytes.subarray(8, 12).toString('ascii') === 'WEBP'
|
||||
) {
|
||||
return 'webp';
|
||||
}
|
||||
return 'png';
|
||||
}
|
||||
|
||||
async function fetchJson(url, options, timeoutMs) {
|
||||
const abortController = new AbortController();
|
||||
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadUrl(url, timeoutMs) {
|
||||
const abortController = new AbortController();
|
||||
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, { signal: abortController.signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`download ${response.status}`);
|
||||
}
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
return {
|
||||
bytes,
|
||||
extension: inferExtensionFromContentType(
|
||||
response.headers.get('content-type') || 'image/jpeg',
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateOne(env, template, outDir) {
|
||||
const requestBody = {
|
||||
model: 'gpt-image-2-all',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
};
|
||||
const payload = await fetchJson(
|
||||
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
env.timeoutMs,
|
||||
);
|
||||
|
||||
const urls = extractImageUrls(payload);
|
||||
const b64Images = extractBase64Images(payload);
|
||||
|
||||
let image;
|
||||
if (urls[0]) {
|
||||
image = await downloadUrl(urls[0], env.timeoutMs);
|
||||
} else if (b64Images[0]) {
|
||||
const bytes = Buffer.from(b64Images[0], 'base64');
|
||||
image = {
|
||||
bytes,
|
||||
extension: inferExtensionFromBytes(bytes),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
}
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outputPath = path.join(outDir, `${template.id}.${image.extension}`);
|
||||
writeFileSync(outputPath, image.bytes);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
const dryRun = args.has('--dry-run') || !args.has('--live');
|
||||
const outDir = path.resolve(String(args.get('--out-dir') || defaultOutDir));
|
||||
const limit = Number.parseInt(String(args.get('--limit') || '0'), 10);
|
||||
const onlyIds = String(args.get('--only') || '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const templates = JSON.parse(readFileSync(promptsPath, 'utf8')).filter(
|
||||
(template) => !onlyIds.length || onlyIds.includes(template.id),
|
||||
);
|
||||
const selectedTemplates = limit > 0 ? templates.slice(0, limit) : templates;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
mode: 'dry-run',
|
||||
outDir,
|
||||
count: selectedTemplates.length,
|
||||
requests: selectedTemplates.map((template) => ({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
body: {
|
||||
model: 'gpt-image-2-all',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
},
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const env = resolveEnv();
|
||||
if (!env.baseUrl || !env.apiKey) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
|
||||
hasBaseUrl: Boolean(env.baseUrl),
|
||||
hasApiKey: Boolean(env.apiKey),
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const generated = [];
|
||||
for (const template of selectedTemplates) {
|
||||
console.log(`Generating ${template.id}...`);
|
||||
generated.push(await generateOne(env, template, outDir));
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
count: generated.length,
|
||||
files: generated,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
@@ -44,8 +44,8 @@ spacetime generate --lang typescript|csharp|rust|unrealcpp --out-dir ./bindings
|
||||
### Publishing & Deployment
|
||||
|
||||
```bash
|
||||
# Publish to Maincloud (default)
|
||||
spacetime publish my-database --yes
|
||||
# Publish to an explicit server
|
||||
spacetime publish my-database --server http://127.0.0.1:3101 --yes
|
||||
|
||||
# Publish to local server
|
||||
spacetime publish my-database --server local --yes
|
||||
@@ -133,8 +133,8 @@ spacetime logout
|
||||
|
||||
| Name | URL | Description |
|
||||
|------|-----|-------------|
|
||||
| `maincloud` | `https://maincloud.spacetimedb.com` | Production cloud (default) |
|
||||
| `local` | `http://127.0.0.1:3000` | Local development server |
|
||||
| `dev` | `http://127.0.0.1:3101` | Genarrative local development server |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
@@ -224,6 +224,6 @@ rustup target add wasm32-unknown-unknown
|
||||
## Notes
|
||||
|
||||
- Many commands are marked UNSTABLE and may change
|
||||
- Default server is `maincloud` unless configured otherwise
|
||||
- Genarrative scripts should pass `--server` or `--server-url` explicitly instead of relying on the CLI default
|
||||
- Use `--yes` flag in scripts to avoid interactive prompts
|
||||
- Dev mode watches files and auto-rebuilds on changes
|
||||
|
||||
5
.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# 微信小程序 web-view 登录配置。
|
||||
# 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。
|
||||
WECHAT_MINI_PROGRAM_APP_ID=""
|
||||
WECHAT_MINI_PROGRAM_APP_SECRET=""
|
||||
WECHAT_JS_CODE_SESSION_ENDPOINT=""
|
||||
12
.env.example
@@ -103,6 +103,9 @@ WECHAT_REDIRECT_PATH="/"
|
||||
WECHAT_AUTHORIZE_ENDPOINT="https://open.weixin.qq.com/connect/qrconnect"
|
||||
WECHAT_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
WECHAT_USER_INFO_ENDPOINT="https://api.weixin.qq.com/sns/userinfo"
|
||||
WECHAT_JS_CODE_SESSION_ENDPOINT="https://api.weixin.qq.com/sns/jscode2session"
|
||||
WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/cgi-bin/stable_token"
|
||||
WECHAT_PHONE_NUMBER_ENDPOINT="https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
WECHAT_STATE_TTL_MINUTES="15"
|
||||
WECHAT_MOCK_USER_ID="wx-mock-user"
|
||||
WECHAT_MOCK_UNION_ID="wx-mock-union"
|
||||
@@ -119,6 +122,11 @@ RPG_LLM_WEB_SEARCH_ENABLED="true"
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
# Server-side APIMart image generation config for optional puzzle image models.
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY="YOUR_APIMART_API_KEY"
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
@@ -168,6 +176,10 @@ VITE_SCENE_IMAGE_REQUEST_TIMEOUT_MS="150000"
|
||||
# Keep this off by default for cleaner logs.
|
||||
VITE_LLM_DEBUG_LOG="false"
|
||||
|
||||
# Optional: global frontend debug mode. When empty, it follows Vite dev mode.
|
||||
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
|
||||
VITE_DEBUG_MODE=""
|
||||
|
||||
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||
# the fixed `bge-large-zh` embedding model.
|
||||
|
||||
26
.env.local
@@ -34,6 +34,19 @@ ALIYUN_SMS_RETURN_VERIFY_CODE="false"
|
||||
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||
|
||||
|
||||
# api-server 非公共模型与可变网关配置(从 config.rs 默认值迁移到本地环境变量)
|
||||
GENARRATIVE_LLM_PROVIDER="ark"
|
||||
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
|
||||
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY=""
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
|
||||
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
|
||||
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
# 启用服务端大模型调试日志(记录所有输入输出)
|
||||
LLM_DEBUG_LOG="true"
|
||||
|
||||
@@ -43,22 +56,17 @@ LLM_DEBUG_LOG="true"
|
||||
ALIYUN_OSS_BUCKET="xushi-dev"
|
||||
ALIYUN_OSS_REGION="oss-cn-beijing"
|
||||
ALIYUN_OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com"
|
||||
ALIYUN_OSS_ACCESS_KEY_ID="LTAI5t7aiyw6uDFW4miJvU8f"
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS"
|
||||
|
||||
# Local Rust backend target for Vite dev proxy.
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:3100"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:3100"
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:8082"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:8082"
|
||||
GENARRATIVE_API_PORT="8082"
|
||||
|
||||
GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3101"
|
||||
GENARRATIVE_SPACETIME_DATABASE="xushi-p4wfr"
|
||||
GENARRATIVE_SPACETIME_TOKEN=""
|
||||
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL="https://maincloud.spacetimedb.com"
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE="xushi-p4wfr"
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMUsyN05YUjBaQkRUVEVCNlFQQjFXNzU2MiIsImlzcyI6Imh0dHBzOi8vYXV0aC5zcGFjZXRpbWVkYi5jb20iLCJhdWQiOiJzcGFjZXRpbWVkYiIsImlhdCI6MTc3NzU1NTQ1NiwiZXhwIjoxODQwNjI3NDU2fQ.iy5qN-3lGPQnkya-wsABtqEgRk1VM2XGxTfxuLV5-eTMfX8cR20sWSx7pnoZcLEwYOkz6cEOb4krhMJmTeBax9Z114o_iwISau3wjjHbeKL9or-039zfYfKb3TtJo3_DZaJSu-ECcMZNl4P1zLmtoRSwl-_AMET4sGzPw0_qR-e49_QGDJz1EEhr7aphybl1xCejCebM8XiJjaRz48vL7-lkwBl90uP-0h7Xx8ToTT2h1egmlcYAvaJalVLHIQqzyYxPUT_Zw9TW7VYExZLhJWdGpQzEm0aXZ2fbch9qVrKpZP2xQ9YjppuLxUFFJeQwhmFf6yc67s6J7LqNvL2-ZA"
|
||||
|
||||
# admin
|
||||
GENARRATIVE_ADMIN_USERNAME=admin
|
||||
GENARRATIVE_ADMIN_PASSWORD=123456
|
||||
ADMIN_API_TARGET=http://127.0.0.1:8082
|
||||
ADMIN_API_TARGET=http://127.0.0.1:3100
|
||||
|
||||
@@ -37,6 +37,12 @@ module.exports = {
|
||||
'simple-import-sort/exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/match3d-runtime/Match3DPhysicsBoard.tsx'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
|
||||
44
.github/workflows/ci.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.19.0
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check encoding
|
||||
run: npm run check:encoding
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint:eslint
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content
|
||||
run: npm run check:content
|
||||
11
.gitignore
vendored
@@ -29,3 +29,14 @@ temp*build*/
|
||||
/public/generated-characters
|
||||
/.codex-temp
|
||||
/target/
|
||||
/logs
|
||||
.worktrees/
|
||||
.env.secrets.local
|
||||
|
||||
# Local load-test data extracted from private migration files
|
||||
scripts/loadtest/data/*.local.json
|
||||
|
||||
# Local load-test run artifacts
|
||||
scripts/loadtest/data/k6-*.log
|
||||
scripts/loadtest/data/k6-*summary*.md
|
||||
scripts/loadtest/data/latest-*-prefix.txt
|
||||
|
||||
92
.hermes/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Genarrative 团队 Hermes 共享记忆
|
||||
|
||||
本目录用于在仓库内共享团队级 Hermes 上下文,供 3 名开发人员在各自本地 Hermes 中读取、更新和同步。
|
||||
|
||||
## 使用原则
|
||||
|
||||
- `.hermes/` 中只保存可以进入 Git 的团队共享内容。
|
||||
- 不提交个人配置、API Key、会话转录、模型密钥、本地路径密钥等敏感内容。
|
||||
- 个人 Hermes 的 `~/.hermes/config.yaml`、`~/.hermes/.env`、`~/.hermes/sessions/` 不应复制到本仓库。
|
||||
- 开发前先阅读本目录下与任务相关的记忆文件;开发后如产生稳定知识,更新对应文档。
|
||||
- 若本目录内容与 `docs/` 或代码事实冲突,以当前代码和最新 `docs/` 为准,并同步修正过期记忆。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
.hermes/
|
||||
├─ README.md # 本说明
|
||||
├─ shared-memory/
|
||||
│ ├─ project-overview.md # 项目概览与当前技术路线
|
||||
│ ├─ team-conventions.md # 团队协作约定
|
||||
│ ├─ development-workflow.md # 开发、测试、提交流程
|
||||
│ ├─ document-map.md # README / AGENTS / docs 阅读索引
|
||||
│ ├─ decision-log.md # 长期决策记录
|
||||
│ ├─ pitfalls.md # 踩坑与排障记录
|
||||
│ └─ handoff-template.md # 任务交接模板
|
||||
├─ plans/ # 阶段性计划与实施方案
|
||||
├─ skills/ # 仓库级 Hermes skills
|
||||
└─ plugins/ # 仓库级 Hermes plugins(需显式启用项目 plugin)
|
||||
```
|
||||
|
||||
## 仓库级 Plugins
|
||||
|
||||
本仓库可共享的 Hermes plugin 放在 `.hermes/plugins/<plugin-name>/`。当前已包含:
|
||||
|
||||
- `.hermes/plugins/game-studio/`:浏览器游戏设计、原型、2D/3D 技术栈、素材管线与 playtest 相关工作流。
|
||||
|
||||
Hermes 的项目级 plugin 默认不会自动加载。团队成员拉取仓库后,如需使用本仓库内 plugin,请在仓库根目录启动 Hermes 前设置:
|
||||
|
||||
```bash
|
||||
export HERMES_ENABLE_PROJECT_PLUGINS=1
|
||||
```
|
||||
|
||||
然后确认当前 Hermes 配置的 `plugins.enabled` 中包含 `game-studio`。如果成员本机尚未启用过该 plugin,当前 Hermes 的 `hermes plugins enable` 只识别用户级或内置 plugin,可能不会识别项目级 plugin;可用以下命令写入个人配置:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
from hermes_cli.config import load_config, save_config
|
||||
config = load_config()
|
||||
plugins = config.setdefault('plugins', {})
|
||||
enabled = set(plugins.get('enabled') or [])
|
||||
disabled = set(plugins.get('disabled') or [])
|
||||
enabled.add('game-studio')
|
||||
disabled.discard('game-studio')
|
||||
plugins['enabled'] = sorted(enabled)
|
||||
plugins['disabled'] = sorted(disabled)
|
||||
save_config(config)
|
||||
PY
|
||||
```
|
||||
|
||||
启用后重新进入一个新 Hermes 会话。`hermes plugins list` 当前主要展示内置和用户级 plugin,未必列出项目级 plugin;如需验证项目级扫描,可在仓库根目录运行:
|
||||
|
||||
```bash
|
||||
HERMES_ENABLE_PROJECT_PLUGINS=1 HERMES_PLUGINS_DEBUG=1 hermes chat -q "请读取 game-studio:game-studio skill 并概括它的用途"
|
||||
```
|
||||
|
||||
该 plugin 注册的是带命名空间的 plugin skills,可用类似 `game-studio:phaser-2d-game` 的名称显式加载。
|
||||
|
||||
## 推荐给 Hermes 的启动提示
|
||||
|
||||
在本仓库中开始复杂任务时,可以先对 Hermes 说:
|
||||
|
||||
```text
|
||||
请先读取 AGENTS.md 以及 .hermes/shared-memory/ 下与本任务相关的团队共享记忆,再开始分析。若任务完成后产生稳定项目知识,请更新 .hermes/shared-memory/ 对应文件。
|
||||
```
|
||||
|
||||
## 需要沉淀到这里的内容
|
||||
|
||||
- 长期有效的架构约定
|
||||
- 反复会用到的本地开发/测试流程
|
||||
- 已确认的接口契约或模块边界
|
||||
- 重要技术决策及原因
|
||||
- 踩坑、排障方式、验证命令
|
||||
- 团队协作规则和任务交接规范
|
||||
|
||||
## 不应沉淀到这里的内容
|
||||
|
||||
- API Key、Token、Cookie、私有密钥
|
||||
- 个人账号、个人本地绝对路径、个人隐私信息
|
||||
- 大段临时聊天记录
|
||||
- 尚未确认的一次性猜测
|
||||
- 构建产物、日志、缓存、数据库 dump
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
# 埋点系统新增周、月、季、年维度映射表计划
|
||||
|
||||
## 目标
|
||||
|
||||
在 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 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。
|
||||
@@ -0,0 +1,271 @@
|
||||
# 邀请码有效期与后台二次确认实施计划
|
||||
|
||||
> **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 的二次确认与表单展示。
|
||||
|
||||
---
|
||||
|
||||
**结论:** 这是一个适合分阶段落地的改动,建议先做“邀请码时间窗 + 后台统一二次确认”的基础能力,再补交互细节。
|
||||
584
.hermes/plans/2026-05-08_120646-profile-feedback-entry.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# 我的页签反馈入口与反馈页 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 约束落地。
|
||||
@@ -0,0 +1,561 @@
|
||||
# 声控狗叫对战 2D 浏览器游戏设计与实现计划
|
||||
|
||||
## 目标
|
||||
|
||||
基于用户提供的视频:
|
||||
|
||||
`C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`
|
||||
|
||||
提取其中“双方比狗叫”的核心玩法,并按照 BDD / TDD / DDD 的方法,为 Genarrative 中可运行于浏览器的 2D 游戏方案生成一份可落地设计与实现思路。实现方向遵循仓库内 `game-studio` 插件工作流,默认采用 2D Phaser + TypeScript + Vite + DOM HUD 的浏览器游戏架构。
|
||||
|
||||
本计划仅做方案设计,不直接编码。
|
||||
|
||||
## 当前上下文与输入分析
|
||||
|
||||
### 已识别视频核心画面
|
||||
|
||||
通过抽帧观察,视频中的游戏呈现出以下稳定特征:
|
||||
|
||||
- 画面是横版 2D 手绘舞台,场景包括公园、海边等固定关卡背景。
|
||||
- 双方各有一只狗作为对战角色,站在左右两侧。
|
||||
- 中央有明显倒计时,例如 `30`、`28`。
|
||||
- 顶部有红蓝双方拉锯式能量条 / 进度条。
|
||||
- 中央提示出现:`对着麦克风汪一声`、`用声音大小 + 叫声次数推动能量条!`
|
||||
- 玩家输入不是传统键鼠,而是麦克风声音。
|
||||
- 玩家需要模仿狗叫,系统根据声音大小与叫声次数推动能量条。
|
||||
- 屏幕会根据叫声出现 `BARK`、`WOOF`、`WAN`、`WANGOOF` 等拟声词与冲击波视觉反馈。
|
||||
- 回合结束时,根据能量条偏向或推进结果判定胜负。
|
||||
|
||||
### 提炼出的核心玩法
|
||||
|
||||
这是一个“声控拔河式狗叫对战”小游戏:
|
||||
|
||||
- 两名玩家 / 一名玩家对 AI 分别代表左右两只狗。
|
||||
- 每局限时 30 秒。
|
||||
- 玩家通过麦克风持续发出狗叫声。
|
||||
- 游戏实时分析音量峰值、叫声次数、叫声节奏。
|
||||
- 声音越大、叫声越密集,己方推动力越强。
|
||||
- 顶部能量条在双方推动力差值下左右移动。
|
||||
- 时间结束后,能量条偏向哪一方,哪一方获胜。
|
||||
|
||||
### 需要合理抽象的地方
|
||||
|
||||
视频中存在直播弹幕、贴图、表情包、遮挡层,这些不是游戏本体机制。本方案只吸收游戏本体核心:
|
||||
|
||||
- 双方狗狗对叫
|
||||
- 麦克风输入
|
||||
- 声音强度 + 次数判定
|
||||
- 红蓝拉锯能量条
|
||||
- 限时回合
|
||||
- 夸张拟声词与冲击波反馈
|
||||
|
||||
## game-studio 插件路线
|
||||
|
||||
根据仓库内 `.hermes/plugins/game-studio` 技能:
|
||||
|
||||
- 早期游戏工作先走 `game-studio` 总入口。
|
||||
- 2D 浏览器游戏默认选择 Phaser。
|
||||
- 架构上需要分离 simulation 与 renderer。
|
||||
- HUD / 菜单 / 设置优先使用 DOM overlay,不把密集文字塞进 canvas。
|
||||
- 玩法状态不应由 Phaser Scene 直接持有,Scene 只负责渲染、动画、相机、输入适配。
|
||||
|
||||
因此本方案采用:
|
||||
|
||||
- Runtime:Phaser 3
|
||||
- Language:TypeScript
|
||||
- Build:Vite
|
||||
- UI:React/DOM HUD overlay 或项目现有 DOM UI 层
|
||||
- Audio input:Web Audio API + MediaDevices.getUserMedia
|
||||
- Simulation:纯 TS domain/service 层
|
||||
- Renderer:Phaser Scene 读取 simulation snapshot 并播放动画/特效
|
||||
|
||||
## 游戏概念设计
|
||||
|
||||
### 游戏名建议
|
||||
|
||||
- 中文:`汪汪声浪大作战`
|
||||
- 英文代号:`bark-battle`
|
||||
- Play type ID 建议:`bark-battle`
|
||||
|
||||
### 玩家幻想
|
||||
|
||||
玩家不是通过按键战斗,而是真的对着麦克风“汪汪叫”,把自己的狗狗声浪推向对手。游戏目标是在倒计时结束前用更响、更密集、更有节奏的叫声赢得声浪拔河。
|
||||
|
||||
### 核心动词
|
||||
|
||||
- 叫:对麦克风发出狗叫声。
|
||||
- 推:通过叫声推动能量条。
|
||||
- 压制:让能量条持续向对手方向倾斜。
|
||||
- 爆发:短时间内连续高质量叫声触发冲击波。
|
||||
- 防守:对手强势时通过持续叫声把能量条拉回。
|
||||
|
||||
### 单局流程
|
||||
|
||||
1. 准备阶段
|
||||
- 展示双方狗狗、地图、麦克风权限提示。
|
||||
- 用户授权麦克风。
|
||||
- 系统检测环境噪音并校准阈值。
|
||||
|
||||
2. 倒计时阶段
|
||||
- 3、2、1 或中央 `30` 倒计时开始。
|
||||
- 玩家看到提示:`对着麦克风汪一声`。
|
||||
|
||||
3. 对战阶段
|
||||
- 每帧或固定 tick 采集麦克风音量。
|
||||
- 根据音量峰值与短促叫声次数计算本方 barkPower。
|
||||
- AI 或远端对手产生 opponentPower。
|
||||
- 能量条根据 `playerPower - opponentPower` 拉锯。
|
||||
- 狗狗张嘴动画、拟声词、冲击波按声音强度生成。
|
||||
|
||||
4. 结算阶段
|
||||
- 30 秒结束。
|
||||
- 能量条偏玩家侧则胜利,偏对手侧则失败,接近中线则平局。
|
||||
- 展示叫声次数、最大音量、平均节奏、声浪评分。
|
||||
|
||||
5. 重开 / 返回
|
||||
- 支持再来一局。
|
||||
- 支持返回玩法入口或结果页。
|
||||
|
||||
## 规则设计
|
||||
|
||||
### 关键状态
|
||||
|
||||
```ts
|
||||
type BarkBattlePhase = 'permission' | 'calibration' | 'countdown' | 'playing' | 'finished'
|
||||
|
||||
type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
remainingMs: number
|
||||
energy: number // -100 到 100,负数偏对手,正数偏玩家
|
||||
player: BarkSideState
|
||||
opponent: BarkSideState
|
||||
winner: 'player' | 'opponent' | 'draw' | null
|
||||
}
|
||||
|
||||
type BarkSideState = {
|
||||
barkCount: number
|
||||
currentVolume: number
|
||||
recentPeak: number
|
||||
combo: number
|
||||
power: number
|
||||
isBarking: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 输入判定
|
||||
|
||||
#### 音量采样
|
||||
|
||||
- 使用 Web Audio API 创建 `AnalyserNode`。
|
||||
- 每个 simulation tick 读取频域或时域数据。
|
||||
- 计算 RMS 或 peak volume。
|
||||
- 根据校准后的环境噪音设置动态阈值。
|
||||
|
||||
#### 一次“叫声”的判定
|
||||
|
||||
一次有效叫声建议满足:
|
||||
|
||||
- 音量超过 `barkThreshold`。
|
||||
- 与上一次叫声峰值至少间隔 `minBarkGapMs`,避免持续噪音被无限计数。
|
||||
- 持续时长在合理范围,例如 80ms 到 1200ms。
|
||||
- 可选:频谱能量集中在中高频,不强制做复杂语音识别,MVP 先用音量 + 峰值节奏。
|
||||
|
||||
#### 推动力计算
|
||||
|
||||
```text
|
||||
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
|
||||
opponentPower = aiPower 或远端玩家 power
|
||||
energyDelta = (playerPower - opponentPower) * deltaTime * balanceFactor
|
||||
energy = clamp(energy + energyDelta, -100, 100)
|
||||
```
|
||||
|
||||
### AI 对手 MVP
|
||||
|
||||
若先做单机浏览器版,右侧对手可由 AI 模拟:
|
||||
|
||||
- 简单难度:周期性小叫,power 低。
|
||||
- 普通难度:有节奏地爆发,power 中等。
|
||||
- 困难难度:根据玩家领先程度自适应追赶,但不得作弊到不可赢。
|
||||
|
||||
后续可扩展为多人实时对战。
|
||||
|
||||
## BDD 行为场景
|
||||
|
||||
### 功能: 麦克风授权与准备
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战麦克风准备
|
||||
为了让玩家能用声音参与对战
|
||||
作为浏览器玩家
|
||||
我希望游戏在开局前明确请求麦克风权限并完成环境校准
|
||||
|
||||
场景: 玩家允许麦克风权限后进入准备倒计时
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家同意浏览器麦克风授权
|
||||
那么系统应进入环境噪音校准阶段
|
||||
而且校准完成后应显示开局倒计时
|
||||
|
||||
场景: 玩家拒绝麦克风权限
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家拒绝浏览器麦克风授权
|
||||
那么系统应显示无法声控游玩的提示
|
||||
而且应提供重试授权入口
|
||||
而且不应直接开始对战
|
||||
```
|
||||
|
||||
### 功能: 声音推动能量条
|
||||
|
||||
```gherkin
|
||||
功能: 声音大小和叫声次数推动能量条
|
||||
为了复刻双方比狗叫的核心体验
|
||||
作为玩家
|
||||
我希望自己的叫声能实时推动顶部能量条
|
||||
|
||||
场景: 玩家发出一次有效狗叫
|
||||
假如游戏处于 playing 阶段
|
||||
而且麦克风输入音量超过有效叫声阈值
|
||||
当系统检测到一次新的叫声峰值
|
||||
那么玩家叫声次数应增加 1
|
||||
而且玩家狗狗应播放张嘴吠叫动画
|
||||
而且画面应出现拟声词反馈
|
||||
|
||||
场景: 玩家连续大声狗叫压制对手
|
||||
假如游戏处于 playing 阶段
|
||||
而且玩家在短时间内产生多次有效叫声
|
||||
当玩家推动力高于对手推动力
|
||||
那么顶部能量条应向玩家侧移动
|
||||
而且玩家侧声浪特效应增强
|
||||
|
||||
场景: 环境噪音低于阈值不计入叫声
|
||||
假如游戏处于 playing 阶段
|
||||
当麦克风只有低于阈值的背景噪音
|
||||
那么玩家叫声次数不应增加
|
||||
而且能量条不应因为背景噪音明显移动
|
||||
```
|
||||
|
||||
### 功能: 限时胜负结算
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战胜负结算
|
||||
为了让单局对抗有明确目标
|
||||
作为玩家
|
||||
我希望倒计时结束后根据能量条位置判定胜负
|
||||
|
||||
场景: 倒计时结束时玩家侧占优
|
||||
假如游戏剩余时间归零
|
||||
而且能量条位于玩家侧
|
||||
当系统进入结算阶段
|
||||
那么系统应判定玩家胜利
|
||||
而且展示玩家叫声次数、最大音量和声浪评分
|
||||
|
||||
场景: 倒计时结束时双方接近平衡
|
||||
假如游戏剩余时间归零
|
||||
而且能量条处于平局阈值范围内
|
||||
当系统进入结算阶段
|
||||
那么系统应判定为平局
|
||||
而且展示再来一局入口
|
||||
```
|
||||
|
||||
### 功能: 移动端与无麦克风降级
|
||||
|
||||
```gherkin
|
||||
功能: 声控游戏移动端与无麦克风降级
|
||||
为了让不同设备玩家都能理解当前状态
|
||||
作为移动端或无麦克风环境玩家
|
||||
我希望系统给出清晰、可操作的降级路径
|
||||
|
||||
场景: 当前浏览器不支持麦克风 API
|
||||
假如玩家设备不支持 getUserMedia
|
||||
当玩家进入狗叫对战页面
|
||||
那么系统应显示设备不支持麦克风输入
|
||||
而且提供返回入口
|
||||
|
||||
场景: 移动端进入对战页面
|
||||
假如玩家使用移动端浏览器
|
||||
当玩家进入狗叫对战页面
|
||||
那么主要能量条、倒计时和狗狗角色应保持可见
|
||||
而且非关键设置应收起到菜单中
|
||||
```
|
||||
|
||||
## DDD 领域划分
|
||||
|
||||
### 领域层:bark-battle domain
|
||||
|
||||
职责:只处理玩法规则,不依赖 Phaser、DOM、Web Audio、后端。
|
||||
|
||||
建议模块:
|
||||
|
||||
- `BarkBattleSession`
|
||||
- 管理 phase、remainingMs、energy、winner。
|
||||
- `BarkDetector`
|
||||
- 根据音量样本判断是否形成一次有效叫声。
|
||||
- `EnergyTugOfWar`
|
||||
- 根据双方 power 更新能量条。
|
||||
- `BarkBattleScoring`
|
||||
- 计算最大音量、叫声次数、combo、评分。
|
||||
- `OpponentStrategy`
|
||||
- 单机 AI 对手策略接口。
|
||||
|
||||
领域规则必须可用纯单元测试验证。
|
||||
|
||||
### 应用层:use case / controller
|
||||
|
||||
职责:编排麦克风输入、simulation tick、AI 对手、结果输出。
|
||||
|
||||
建议用例:
|
||||
|
||||
- `requestMicrophonePermission()`
|
||||
- `calibrateAmbientNoise()`
|
||||
- `startBarkBattleSession()`
|
||||
- `submitAudioSample(sample)`
|
||||
- `tickBarkBattle(deltaMs)`
|
||||
- `finishBarkBattle()`
|
||||
|
||||
### 基础设施层
|
||||
|
||||
职责:浏览器 API 与引擎适配。
|
||||
|
||||
- `BrowserMicrophoneInput`
|
||||
- 封装 `navigator.mediaDevices.getUserMedia`。
|
||||
- 输出 normalized volume samples。
|
||||
- `PhaserBarkBattleScene`
|
||||
- 渲染狗狗、背景、拟声词、冲击波。
|
||||
- 不持有核心玩法规则。
|
||||
- `DomBarkBattleHud`
|
||||
- 展示倒计时、能量条、权限提示、结算面板。
|
||||
|
||||
### 表现层
|
||||
|
||||
- Phaser Canvas:地图、狗狗、声浪、粒子、拟声词。
|
||||
- DOM HUD:顶部能量条、倒计时、权限/结算/设置面板。
|
||||
|
||||
## TDD 落地顺序
|
||||
|
||||
### 第一轮:领域规则 RED-GREEN-REFACTOR
|
||||
|
||||
先写纯 TS 单元测试,不接 Phaser,不接麦克风。
|
||||
|
||||
目标测试:
|
||||
|
||||
- `BarkDetector`:超过阈值且间隔足够时计为一次叫声。
|
||||
- `BarkDetector`:持续噪音不会无限增加叫声次数。
|
||||
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
|
||||
- `EnergyTugOfWar`:energy 被 clamp 在 -100 到 100。
|
||||
- `BarkBattleSession`:倒计时归零后进入 finished。
|
||||
- `BarkBattleSession`:根据 energy 判定 player/opponent/draw。
|
||||
|
||||
### 第二轮:应用层测试
|
||||
|
||||
- 模拟音频 sample 输入,验证 session snapshot 更新。
|
||||
- 模拟 AI 对手 power,验证能量条拉锯。
|
||||
- 模拟权限失败,验证 phase 不进入 playing。
|
||||
|
||||
### 第三轮:组件 / 集成测试
|
||||
|
||||
- HUD 根据 snapshot 显示倒计时。
|
||||
- HUD 根据 energy 渲染红蓝能量条比例。
|
||||
- 权限拒绝时显示重试入口。
|
||||
- 结算阶段显示胜负与再来一局。
|
||||
|
||||
### 第四轮:浏览器 smoke / playtest
|
||||
|
||||
- 本地启动页面。
|
||||
- 授权麦克风。
|
||||
- 对麦克风发声后看到拟声词与能量条变化。
|
||||
- 移动端宽度下主游戏画面不被 HUD 遮挡。
|
||||
|
||||
## 建议文件结构
|
||||
|
||||
如果作为独立前端玩法原型,可采用:
|
||||
|
||||
```text
|
||||
src/games/bark-battle/
|
||||
domain/
|
||||
BarkBattleSession.ts
|
||||
BarkDetector.ts
|
||||
EnergyTugOfWar.ts
|
||||
BarkBattleScoring.ts
|
||||
OpponentStrategy.ts
|
||||
application/
|
||||
BarkBattleController.ts
|
||||
BrowserMicrophoneInput.ts
|
||||
phaser/
|
||||
BarkBattleScene.ts
|
||||
BarkBattlePreloadScene.ts
|
||||
barkBattleAssets.ts
|
||||
ui/
|
||||
BarkBattleHud.tsx
|
||||
BarkBattleResultPanel.tsx
|
||||
BarkBattlePermissionPanel.tsx
|
||||
tests/
|
||||
BarkDetector.test.ts
|
||||
EnergyTugOfWar.test.ts
|
||||
BarkBattleSession.test.ts
|
||||
```
|
||||
|
||||
如果接入 Genarrative 玩法类型闭环,后续还需要按 `genarrative-play-type-integration` 扩展:
|
||||
|
||||
```text
|
||||
src/components/bark-battle-runtime/BarkBattleRuntimeShell.tsx
|
||||
src/components/bark-battle-result/BarkBattleResultView.tsx
|
||||
src/services/barkBattleRuntimeClient.ts
|
||||
packages/shared/src/contracts/barkBattle.ts
|
||||
server-rs/crates/shared-contracts/src/bark_battle.rs
|
||||
```
|
||||
|
||||
MVP 阶段建议先做浏览器单机 runtime 原型,再决定是否进入创作入口、作品发布、广场和后端持久化。
|
||||
|
||||
## UI / 视觉方向
|
||||
|
||||
### 画面
|
||||
|
||||
- 横版固定舞台。
|
||||
- 左右两只狗对峙。
|
||||
- 背景可先做公园一张图,后续扩展海边、街区等地图。
|
||||
- 狗狗用 2D sprite 或简单骨架帧动画。
|
||||
|
||||
### HUD
|
||||
|
||||
- 顶部:红蓝声浪能量条。
|
||||
- 中央:大号倒计时,只在开局和关键时间突出显示。
|
||||
- 左右:双方狗狗状态,不堆叠复杂面板。
|
||||
- 底部或角落:麦克风状态、小型重试按钮。
|
||||
- 结算:居中弹出简洁面板,显示胜负和关键数据。
|
||||
|
||||
### 动效
|
||||
|
||||
- 叫声触发狗狗张嘴。
|
||||
- 声音越大,拟声词越大,冲击波越宽。
|
||||
- combo 时触发短暂屏幕震动,但不能遮挡能量条。
|
||||
- 尊重 reduced motion,非必要动画可降级。
|
||||
|
||||
## 测试映射
|
||||
|
||||
| BDD 场景 | 测试层级 | 目标文件 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 玩家允许麦克风权限后进入准备倒计时 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家拒绝麦克风权限 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家发出一次有效狗叫 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 玩家连续大声狗叫压制对手 | unit/integration | `EnergyTugOfWar.test.ts`, `BarkBattleController.test.ts` | planned |
|
||||
| 环境噪音低于阈值不计入叫声 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 倒计时结束时玩家侧占优 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 倒计时结束时双方接近平衡 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 当前浏览器不支持麦克风 API | component | `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 移动端进入对战页面 | visual/smoke | Playwright 或人工 playtest 清单 | planned |
|
||||
|
||||
## 验证命令建议
|
||||
|
||||
具体命令以后续实际落地位置为准,建议包括:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.ts
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
若接入 Genarrative 后端或玩法配置,还需要追加:
|
||||
|
||||
```bash
|
||||
cd server-rs && cargo check -p api-server -p shared-contracts --no-default-features
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
|
||||
```
|
||||
|
||||
## 实施阶段拆分
|
||||
|
||||
### Phase 0:产品与技术定稿
|
||||
|
||||
- 确认玩法 ID:`bark-battle`。
|
||||
- 确认 MVP 只做单机玩家 vs AI,不做实时多人。
|
||||
- 确认是否只做 runtime 原型,还是接入 Genarrative 创作入口。
|
||||
- 确认是否允许浏览器麦克风权限作为核心输入。
|
||||
|
||||
### Phase 1:纯领域模型
|
||||
|
||||
- 建立 bark-battle domain。
|
||||
- 按 TDD 写 `BarkDetector`、`EnergyTugOfWar`、`BarkBattleSession` 测试。
|
||||
- 实现最小规则让测试通过。
|
||||
|
||||
### Phase 2:麦克风输入适配
|
||||
|
||||
- 封装 Web Audio API。
|
||||
- 支持权限请求、权限失败、环境噪音校准。
|
||||
- 使用 mock input 完成自动化测试,真实麦克风做 smoke。
|
||||
|
||||
### Phase 3:Phaser 2D runtime
|
||||
|
||||
- 新建 Phaser Scene。
|
||||
- 绘制或占位加载公园背景、左右狗狗、声浪特效。
|
||||
- Scene 只消费 snapshot,不写规则。
|
||||
- 接入 DOM HUD。
|
||||
|
||||
### Phase 4:反馈与结算
|
||||
|
||||
- 加入拟声词、冲击波、狗狗张嘴动画。
|
||||
- 加入结算面板。
|
||||
- 加入再来一局与返回入口。
|
||||
|
||||
### Phase 5:Genarrative 集成可选项
|
||||
|
||||
若要正式接入玩法类型:
|
||||
|
||||
- 补 `shared-contracts` 中 bark-battle runtime/result DTO。
|
||||
- 补前端 service 与 runtime shell。
|
||||
- 补入口配置数据库 seed。
|
||||
- 补作品架 / 发布 / 广场链路,若需要持久化成绩或作品。
|
||||
- 按 `genarrative-play-type-integration` 执行完整闭环验证。
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
### 麦克风权限风险
|
||||
|
||||
浏览器麦克风权限受 HTTPS、浏览器策略、用户设置影响。MVP 需要明确:
|
||||
|
||||
- 本地开发可在 localhost 使用。
|
||||
- 线上必须 HTTPS。
|
||||
- 权限拒绝需要可恢复。
|
||||
|
||||
### 声音识别准确性风险
|
||||
|
||||
MVP 不建议做复杂“是否真的是狗叫”的 AI 识别,否则实现成本高、误判多。建议先用:
|
||||
|
||||
- 音量阈值
|
||||
- 峰值次数
|
||||
- 节奏间隔
|
||||
- 环境噪音校准
|
||||
|
||||
后续再考虑加入频谱特征或 ML 分类。
|
||||
|
||||
### 噪音作弊风险
|
||||
|
||||
玩家可以喊叫、拍桌子或播放音频。若是娱乐派对玩法可以接受;若要竞技公平,需要后续加入:
|
||||
|
||||
- 频谱特征
|
||||
- 输入冷却
|
||||
- 异常持续噪音削弱
|
||||
- 本地/服务端反作弊策略
|
||||
|
||||
### 移动端兼容风险
|
||||
|
||||
移动端 Web Audio 可能需要用户手势激活 AudioContext。计划中需把“开始”按钮作为显式用户手势,避免自动启动失败。
|
||||
|
||||
### UI 遮挡风险
|
||||
|
||||
视频原型中的核心可读信息非常少:倒计时、能量条、狗狗、拟声词。实现时应避免把说明文案、复杂面板长期铺在画面上。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. MVP 是“玩家 vs AI”,还是需要从第一版开始支持双人同屏 / 联机?
|
||||
2. 是否要作为 Genarrative 新玩法入口完整接入,还是先做独立 runtime 原型?
|
||||
3. 是否需要记录成绩、发布作品、进入作品架和广场?
|
||||
4. 狗狗与背景素材是使用临时占位、AI 生成,还是需要复用项目既有素材系统?
|
||||
5. 是否允许游戏强依赖麦克风权限,还是必须提供键盘备用输入?
|
||||
|
||||
## 推荐下一步
|
||||
|
||||
建议下一步先执行 Phase 0 + Phase 1:
|
||||
|
||||
1. 明确 MVP 边界:单机玩家 vs AI。
|
||||
2. 写 `BarkDetector` / `EnergyTugOfWar` / `BarkBattleSession` 的 BDD 对应单元测试。
|
||||
3. 不接 Phaser、不接麦克风,先把核心规则用 TDD 跑通。
|
||||
4. 规则稳定后再接 Web Audio 与 Phaser runtime。
|
||||
@@ -0,0 +1,709 @@
|
||||
# bark-battle 三阶段实施计划:浏览器原型 → AI 创作入口 → 数据库落地
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 按“三阶段”推进 `bark-battle / 汪汪声浪大作战`:第一阶段先做纯浏览器可运行游戏原型并验证玩法跑通;第二阶段接入 Genarrative 创作入口,用 AI 生成可试玩内容;第三阶段再打通后端数据库、发布、成绩和作品闭环。
|
||||
|
||||
**Architecture:** 第一阶段只在前端 runtime 内闭环,优先落 `src/games/bark-battle/` 与直达路由,不依赖后端和 SpacetimeDB。第二阶段在已有创作入口、Agent flow controller、结果页和 runtime shell 上接入 `bark-battle`,AI 只生成配置化草稿,不承接正式业务真相。第三阶段按 `server-rs + Axum + SpacetimeDB` DDD 分层落库,前端只展示后端投影和调用后端 API。
|
||||
|
||||
**Tech Stack:** React 19、TypeScript、Vite、Vitest、Testing Library;第一阶段优先 DOM/Canvas 原型,可在验证玩法后再引入 Phaser 3;后端阶段使用 `server-rs`、Axum、SpacetimeDB、shared-contracts。
|
||||
|
||||
---
|
||||
|
||||
## 0. 当前上下文 / 假设
|
||||
|
||||
- 现有需求与技术文档:
|
||||
- `docs/prd/BARK_BATTLE_BDD_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- `docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- 用户明确要求阶段顺序:
|
||||
1. 第一阶段:先制作纯浏览器运行的游戏原型,需要测试游戏功能是否能跑通。
|
||||
2. 第二阶段:打通创作入口,使用 AI 赋能游戏内容创作。
|
||||
3. 第三阶段:最后打通数据库落地。
|
||||
- 因此本计划调整原技术方案中的落地优先级:
|
||||
- 第一阶段不新增后端表、不接发布、不接作品架。
|
||||
- 第一阶段可以用 mock / local draft 配置与直达路由 `/bark-battle` 完成 playable prototype。
|
||||
- 第一阶段若 Phaser 依赖尚未安装,优先用 React DOM + Canvas/CSS 2D 原型跑通功能;待核心规则验证后再决定是否引入 Phaser,避免第一阶段被依赖安装和素材管线阻塞。
|
||||
- 当前仓库 `package.json` 还没有 `phaser` 依赖;如实现者选择 Phaser,需要单独评估依赖引入、包体和测试影响。
|
||||
- 本计划只写计划,不直接实现代码。
|
||||
|
||||
## 1. 总体分阶段验收口径
|
||||
|
||||
### Phase 1:纯浏览器游戏原型
|
||||
|
||||
目标:打开本地前端路由即可玩到一局 `bark-battle`,并通过自动测试确认核心规则跑通。
|
||||
|
||||
必须满足:
|
||||
- 可从 `/bark-battle` 进入独立原型页面。
|
||||
- 不登录、不请求后端、不依赖数据库。
|
||||
- 支持开发 mock input:点击/按键/按钮可模拟音量峰值;有真实麦克风时可走 Web Audio。
|
||||
- 能完成:权限/开始 → 校准或 mock 准备 → 倒计时 → 30 秒 playing → 结算 → 再来一局。
|
||||
- 低于阈值输入不计数;有效叫声计数;能量条向玩家或对手移动;结算胜/负/平。
|
||||
- 移动端至少能看到能量条、倒计时、双方狗狗、主要按钮和结算。
|
||||
|
||||
### Phase 2:AI 创作入口
|
||||
|
||||
目标:创作者能从创作中心选择 `bark-battle`,用 AI 生成玩法配置草稿,并进入结果页试玩。
|
||||
|
||||
必须满足:
|
||||
- 后端入口配置中出现 `bark-battle`,按开关展示/可点击。
|
||||
- 前端类型分流、SelectionStage、工作台、结果页、runtime 入口齐全。
|
||||
- AI 生成内容仅限配置化草稿:标题、主题、狗狗外观描述、背景风格、难度、局长、AI 对手参数、提示文案 key 等。
|
||||
- 生成结果可在本地 runtime 中试玩。
|
||||
- 未落库前可先用 session/local state 保存草稿,但要清楚标识为“未发布草稿”。
|
||||
|
||||
### Phase 3:数据库落地与正式作品闭环
|
||||
|
||||
目标:`bark-battle` 草稿、发布态配置、runtime start/finish、成绩和作品级游玩埋点都进入后端 DDD / SpacetimeDB 链路。
|
||||
|
||||
必须满足:
|
||||
- `shared-contracts`、`module-bark-battle`、`spacetime-module`、`spacetime-client`、`api-server` 分层清晰。
|
||||
- 发布为稳定作品 ID,runtime 从后端读取发布态 config。
|
||||
- start 成功写 `work_play_start`:`scope_kind=work`、`scope_id=稳定作品 ID`、metadata 包含 `playType/workId/sourceRoute/userId`。
|
||||
- finish 只上传派生指标,不保存原始麦克风音频、波形或可还原语音内容。
|
||||
- 作品架/广场/分享/排行榜如启用,均来自后端投影。
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 1:纯浏览器运行游戏原型
|
||||
|
||||
### Task 1.1:补齐阶段边界文档
|
||||
|
||||
**Objective:** 在现有技术方案中明确“先浏览器原型,后 AI 创作,最后数据库”的落地顺序,避免实现时过早接后端。
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`
|
||||
- Modify: `docs/prd/BARK_BATTLE_BDD_2026-05-11.md`
|
||||
|
||||
**Steps:**
|
||||
1. 在 runtime 技术方案中新增“三阶段落地顺序”小节。
|
||||
2. 明确 Phase 1 不接后端、不接数据库、不接创作入口事实源。
|
||||
3. 在 BDD 中补充“浏览器原型 smoke”验收场景。
|
||||
4. 运行:
|
||||
```bash
|
||||
npm run check:encoding -- docs/prd/BARK_BATTLE_BDD_2026-05-11.md docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
git diff --check
|
||||
```
|
||||
|
||||
**Expected:** 编码检查和 diff 空白检查通过。
|
||||
|
||||
### Task 1.2:建立 Phase 1 目录骨架和类型
|
||||
|
||||
**Objective:** 建立不依赖 React/DOM/Web Audio 的核心类型,后续所有测试和 UI 都围绕这些类型。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleTypes.ts`
|
||||
- Create: `src/games/bark-battle/application/BarkBattleConfig.ts`
|
||||
|
||||
**Key design:**
|
||||
- `BarkBattlePhase = 'permission' | 'calibration' | 'countdown' | 'playing' | 'finished' | 'unavailable'`
|
||||
- `MicrophoneFailureReason` 覆盖已有文档中的 9 类失败原因。
|
||||
- `BarkBattleSnapshot` 包含 `phase/uiState/errorReason/statusMessageKey/remainingMs/energy/player/opponent/winner/result/lastEvents`。
|
||||
- `BarkBattleConfig` 包含 `roundDurationMs/drawThreshold/minBarkGapMs/minBarkDurationMs/maxBarkDurationMs/balanceFactor/calibrationMaxWaitMs`。
|
||||
|
||||
**Tests:**
|
||||
- 本任务可先不写运行时逻辑,但需要让 typecheck 能引用这些类型。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run check:encoding -- src/games/bark-battle/domain/BarkBattleTypes.ts src/games/bark-battle/application/BarkBattleConfig.ts
|
||||
```
|
||||
|
||||
### Task 1.3:TDD 实现叫声检测 BarkDetector
|
||||
|
||||
**Objective:** 用纯函数/纯类把音频样本转换为有效叫声事件。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkDetector.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkDetector.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 超过阈值、持续时长合规、间隔足够时计为一次有效叫声。
|
||||
2. 持续噪音不在每个 tick 无限计数。
|
||||
3. 低于阈值的背景噪音不计数。
|
||||
4. `minBarkGapMs` 内连续峰值不重复计数。
|
||||
5. 过短脉冲不计数;过长持续声削弱为单段输入。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.4:TDD 实现能量条 EnergyTugOfWar
|
||||
|
||||
**Objective:** 验证玩家/对手推动力能稳定改变 `energy`,并 clamp 到 `-100..100`。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/EnergyTugOfWar.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/EnergyTugOfWar.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 玩家 power 高于对手时 `energy` 增加。
|
||||
2. 对手 power 高于玩家时 `energy` 减少。
|
||||
3. energy 不超过 `100`。
|
||||
4. energy 不低于 `-100`。
|
||||
5. power 相等时变化不超过浮点误差。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/EnergyTugOfWar.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.5:TDD 实现单局状态机 BarkBattleSession
|
||||
|
||||
**Objective:** 跑通 permission/calibration/countdown/playing/finished/unavailable 状态流转和结算。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleSession.ts`
|
||||
- Create: `src/games/bark-battle/domain/BarkBattleScoring.ts`
|
||||
- Create: `src/games/bark-battle/domain/OpponentStrategy.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkBattleSession.test.ts`
|
||||
- Create: `src/games/bark-battle/domain/__tests__/BarkBattleScoring.test.ts`
|
||||
|
||||
**Test cases:**
|
||||
1. 校准完成后进入 countdown。
|
||||
2. countdown 结束后进入 playing。
|
||||
3. playing 中 `remainingMs` 随 tick 递减。
|
||||
4. `remainingMs <= 0` 后进入 finished。
|
||||
5. `energy > drawThreshold` 判定玩家胜。
|
||||
6. `energy < -drawThreshold` 判定对手胜。
|
||||
7. `abs(energy) <= drawThreshold` 判定平局。
|
||||
8. finished 后新输入不再改变本局计数和能量。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/__tests__/BarkBattleSession.test.ts src/games/bark-battle/domain/__tests__/BarkBattleScoring.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.6:实现 mock-first Application Controller
|
||||
|
||||
**Objective:** 不依赖真实麦克风,先用 mock audio sample 驱动完整 snapshot。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/application/BarkBattleController.ts`
|
||||
- Create: `src/games/bark-battle/application/BarkBattleSnapshotStore.ts`
|
||||
- Create: `src/games/bark-battle/application/__tests__/BarkBattleController.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- `startWithMockInput()` 进入校准完成或直接 countdown。
|
||||
- `submitMockSample(sample)` 更新玩家输入。
|
||||
- `tick(deltaMs)` 推进对手、能量条、倒计时。
|
||||
- `restart()` 重置状态。
|
||||
- `failMicrophone(reason)` 进入 `phase: 'unavailable'`,并设置 `errorReason/statusMessageKey`。
|
||||
|
||||
**Test cases:**
|
||||
1. mock start 后能进入 countdown/playing。
|
||||
2. 提交 mock 峰值后 bark count 增加。
|
||||
3. tick 后 energy 可变化。
|
||||
4. finish 后生成 result。
|
||||
5. `failMicrophone('permission-denied')` 不进入 playing。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/application/__tests__/BarkBattleController.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.7:实现浏览器原型 UI Shell(不接平台)
|
||||
|
||||
**Objective:** 提供 `/bark-battle` 可访问的 playable prototype。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/BarkBattlePlaygroundApp.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleHud.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleResultPanel.tsx`
|
||||
- Create: `src/games/bark-battle/ui/BarkBattleHud.css`
|
||||
- Modify: `src/routing/appRoutes.tsx`
|
||||
|
||||
**Behavior:**
|
||||
- 新增路由匹配:`/bark-battle`。
|
||||
- 首屏只有清爽开始面板,不常驻大段规则。
|
||||
- 提供开发原型按钮:开始、模拟叫声、模拟对手增强、再来一局。
|
||||
- playing 画面展示:顶部能量条、倒计时、玩家/对手狗狗、叫声次数、麦克风/mock 状态。
|
||||
- 结算面板独立居中,不追加在当前面板下方。
|
||||
|
||||
**UI constraints:**
|
||||
- 移动端优先。
|
||||
- 正常 playing 阶段不在 playfield 常驻规则说明。
|
||||
- 大动效不遮挡顶部能量条和倒计时。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 访问 /bark-battle → 开始 → 模拟叫声 → energy 变化 → 结算 → 再来一局
|
||||
```
|
||||
|
||||
### Task 1.8:实现 HUD 组件测试
|
||||
|
||||
**Objective:** 自动验证核心 UI 状态,不只依赖人工试玩。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/ui/__tests__/BarkBattleHud.test.tsx`
|
||||
- Create: `src/games/bark-battle/ui/__tests__/BarkBattleResultPanel.test.tsx`
|
||||
|
||||
**Test cases:**
|
||||
1. playing 阶段展示倒计时和能量条。
|
||||
2. energy 正值时玩家侧占比更大。
|
||||
3. energy 负值时对手侧占比更大。
|
||||
4. permission-denied 展示重试授权入口。
|
||||
5. unsupported 不展示开始声控按钮。
|
||||
6. finished 展示胜负、叫声次数、再来一局。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/__tests__/BarkBattleHud.test.tsx src/games/bark-battle/ui/__tests__/BarkBattleResultPanel.test.tsx
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 1.9:接入真实 Web Audio(可晚于 mock 原型)
|
||||
|
||||
**Objective:** 在支持麦克风的浏览器中真实采样并驱动 controller,同时保留 mock fallback 便于测试。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/games/bark-battle/infrastructure/BrowserMicrophoneInput.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/AudioAnalyserSampler.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/MicrophonePermission.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts`
|
||||
- Create: `src/games/bark-battle/infrastructure/__tests__/AudioAnalyserSampler.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 用户点击开始后才请求麦克风。
|
||||
- 用户手势后创建/resume `AudioContext`。
|
||||
- 输出归一化 `BarkAudioSample`。
|
||||
- 捕获并映射:unsupported、permission-denied、non-secure-context、not-found、not-readable、audio-context-blocked、unknown。
|
||||
- stop/restart/page unload 时停止 tracks。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts src/games/bark-battle/infrastructure/__tests__/AudioAnalyserSampler.test.ts
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 真实麦克风授权 → 校准 → 发声 → 结算
|
||||
```
|
||||
|
||||
### Task 1.10:Phase 1 收口验证
|
||||
|
||||
**Objective:** 确认“纯浏览器原型”已经可以交给产品/测试试玩。
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/**/*.test.ts src/games/bark-battle/application/**/*.test.ts src/games/bark-battle/infrastructure/**/*.test.ts src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
npm run dev:web
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] `/bark-battle` 能打开。
|
||||
- [ ] mock 模式可完整完成一局。
|
||||
- [ ] 真实麦克风模式可授权、校准、发声、结算。
|
||||
- [ ] 拒绝权限后不会进入 playing。
|
||||
- [ ] 移动端窄屏能看到核心信息并能点击主要按钮。
|
||||
- [ ] 再来一局不会继承上一局 energy/barkCount/result。
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2:打通创作入口,用 AI 赋能内容创作
|
||||
|
||||
### Task 2.1:定义 `bark-battle` 草稿契约(前端本地版)
|
||||
|
||||
**Objective:** 在接后端前,先定义 AI 可生成的 runtime draft shape。
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/shared/src/contracts/barkBattle.ts`(临时前端共享类型,后端阶段再对齐 Rust shared-contracts)
|
||||
- Create: `src/services/bark-battle-creation/barkBattleDraftDefaults.ts`
|
||||
- Create: `src/services/bark-battle-creation/barkBattleDraftValidation.ts`
|
||||
|
||||
**Draft fields:**
|
||||
- `title`
|
||||
- `description`
|
||||
- `themePrompt`
|
||||
- `playerDogName`
|
||||
- `opponentDogName`
|
||||
- `backgroundStyle`
|
||||
- `difficulty`
|
||||
- `roundDurationMs`
|
||||
- `drawThreshold`
|
||||
- `opponentConfig`
|
||||
- `audioSensitivityPreset`
|
||||
- `visualStyle`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.2:新增创作入口配置
|
||||
|
||||
**Objective:** 让 `bark-battle` 出现在创作中心入口中,但可通过后端入口配置开关控制。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
|
||||
- `server-rs/crates/module-runtime/src/domain.rs`
|
||||
- `server-rs/crates/module-runtime/src/application.rs`
|
||||
- `server-rs/crates/shared-contracts/src/creation_entry_config.rs`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.test.ts`
|
||||
|
||||
**Plan:**
|
||||
1. 在入口 seed 中新增 `bark-battle`,首轮可设:`visible: true`、`open: true`(若需要灰度则 `open: false`)。
|
||||
2. 前端展示派生只消费 API 返回,不恢复旧静态入口事实源。
|
||||
3. 更新排序和锁定态测试。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
|
||||
npm run typecheck
|
||||
cd server-rs && cargo check -p api-server -p spacetime-module --no-default-features
|
||||
```
|
||||
|
||||
### Task 2.3:扩展 SelectionStage 与流程分流
|
||||
|
||||
**Objective:** 点击 `bark-battle` 入口后进入对应创作工作台。
|
||||
|
||||
**Files likely to change:**
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`(如复用通用 agent flow)
|
||||
|
||||
**Stages:**
|
||||
- `bark-battle-agent-workspace`
|
||||
- `bark-battle-generating`(可选)
|
||||
- `bark-battle-result`
|
||||
- `bark-battle-runtime`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/**/*.test.tsx src/components/platform-entry/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.4:实现 AI 创作工作台
|
||||
|
||||
**Objective:** 用对话式或表单式输入生成 `BarkBattleDraft`。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-creation/BarkBattleAgentWorkspace.tsx`
|
||||
- Create: `src/services/bark-battle-creation/barkBattleCreationClient.ts`
|
||||
- Create: `src/services/bark-battle-creation/barkBattlePromptBuilder.ts`
|
||||
- Create: `src/services/bark-battle-creation/__tests__/barkBattlePromptBuilder.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 用户输入一句主题,例如“柴犬在赛博公园比谁叫得响”。
|
||||
- AI 返回结构化草稿。
|
||||
- 前端校验并填默认值,不让非法 roundDuration/difficulty 进入 runtime。
|
||||
- 错误时保留用户输入和已生成草稿。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Task 2.5:实现结果页与试玩入口
|
||||
|
||||
**Objective:** AI 草稿生成后可查看、返回编辑、进入 Phase 1 runtime 试玩。
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- Create: `src/components/bark-battle-result/BarkBattleResultView.test.tsx`
|
||||
- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`(允许传入 draft config)
|
||||
|
||||
**Behavior:**
|
||||
- 展示标题、主题、狗狗名、背景风格、难度、局长。
|
||||
- 提供“返回编辑”“试玩”按钮。
|
||||
- 暂不展示发布按钮,或发布按钮显示为后端阶段能力。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- --run src/components/bark-battle-result/BarkBattleResultView.test.tsx
|
||||
npm run typecheck
|
||||
npm run dev:web
|
||||
# 手动 smoke: 创作入口 → AI 草稿 → 结果页 → 试玩 → 返回编辑
|
||||
```
|
||||
|
||||
### Task 2.6:Phase 2 收口验证
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/bark-battle-result/BarkBattleResultView.test.tsx src/services/bark-battle-creation/**/*.test.ts src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] 创作中心展示 `bark-battle`。
|
||||
- [ ] 点击入口进入工作台。
|
||||
- [ ] AI 可生成草稿。
|
||||
- [ ] 草稿结果页可展示并返回编辑。
|
||||
- [ ] 试玩使用草稿配置影响 runtime 表现。
|
||||
- [ ] 未接数据库前不会假装发布成功。
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 3:数据库落地与正式作品闭环
|
||||
|
||||
### Task 3.1:补齐 shared contracts
|
||||
|
||||
**Objective:** 前后端共享 bark-battle DTO,避免前端手写正式契约漂移。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
- `server-rs/crates/shared-contracts/src/lib.rs`
|
||||
- `packages/shared/src/contracts/barkBattle.ts`
|
||||
|
||||
**DTO:**
|
||||
- `BarkBattleDraft`
|
||||
- `BarkBattlePublishedConfig`
|
||||
- `CreateBarkBattleSessionRequest/Response`
|
||||
- `BarkBattleRuntimeStartRequest/Response`
|
||||
- `BarkBattleRuntimeFinishRequest/Response`
|
||||
- `BarkBattleRunResult`
|
||||
- `BarkBattleScoreSummary`
|
||||
- `BarkBattleLeaderboardEntry`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run typecheck
|
||||
cd server-rs && cargo check -p shared-contracts --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.2:新增 `module-bark-battle` 纯领域模块
|
||||
|
||||
**Objective:** 后端正式分数、配置校验、提交合法性不写在 api-server handler 里。
|
||||
|
||||
**Files:**
|
||||
- Create: `server-rs/crates/module-bark-battle/`
|
||||
- Modify: `server-rs/Cargo.toml`
|
||||
|
||||
**Responsibilities:**
|
||||
- 配置合法性校验。
|
||||
- run start/finish 状态约束。
|
||||
- 派生指标范围校验。
|
||||
- 分数与排行榜排序分计算。
|
||||
- 不接 Axum、不接 SpacetimeDB、不接 HTTP。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
cd server-rs && cargo test -p module-bark-battle --no-default-features
|
||||
cd server-rs && cargo check -p module-bark-battle --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.3:SpacetimeDB 表、reducer、migration
|
||||
|
||||
**Objective:** 保存草稿、发布态配置、run、result、leaderboard 投影。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-module/src/runtime/bark_battle.rs`(或按现有模块目录命名)
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- `server-rs/crates/spacetime-module/src/lib.rs`
|
||||
- 生成绑定目录(通过命令生成,不手改生成物)
|
||||
|
||||
**Tables draft:**
|
||||
- `bark_battle_draft`
|
||||
- `bark_battle_published_config`
|
||||
- `bark_battle_run`
|
||||
- `bark_battle_run_result`
|
||||
- `bark_battle_leaderboard_entry`
|
||||
|
||||
**Reducers/procedures:**
|
||||
- `create_bark_battle_draft`
|
||||
- `publish_bark_battle_config`
|
||||
- `start_bark_battle_run`
|
||||
- `finish_bark_battle_run`
|
||||
- `list_bark_battle_leaderboard`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
cd server-rs && cargo check -p spacetime-module --no-default-features
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
### Task 3.4:spacetime-client facade
|
||||
|
||||
**Objective:** api-server 通过 facade 调用 SpacetimeDB,不直接散落 reducer 细节。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- `server-rs/crates/spacetime-client/src/lib.rs`
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
cd server-rs && cargo check -p spacetime-client --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.5:api-server BFF 路由
|
||||
|
||||
**Objective:** 提供创作、发布态 runtime start/finish、leaderboard API。
|
||||
|
||||
**Files likely to change:**
|
||||
- `server-rs/crates/api-server/src/bark_battle.rs`
|
||||
- `server-rs/crates/api-server/src/main.rs` 或路由注册文件
|
||||
|
||||
**Routes draft:**
|
||||
- `POST /api/bark-battle/sessions`
|
||||
- `GET /api/bark-battle/sessions/:sessionId`
|
||||
- `POST /api/bark-battle/runtime/start`
|
||||
- `POST /api/bark-battle/runtime/finish`
|
||||
- `GET /api/bark-battle/works/:workId/runtime-config`
|
||||
- `GET /api/bark-battle/works/:workId/leaderboard`
|
||||
|
||||
**Tracking:**
|
||||
- runtime start 成功后主动写 `work_play_start`。
|
||||
- `scope_kind=work`。
|
||||
- `scope_id=稳定作品 ID`。
|
||||
- metadata 包含 `playType=bark-battle`、`workId`、`sourceRoute`、`userId`。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run api-server
|
||||
# 另一个终端检查 /healthz,并执行对应 API smoke
|
||||
cd server-rs && cargo check -p api-server --no-default-features
|
||||
```
|
||||
|
||||
### Task 3.6:前端正式 client 与 runtime 切换
|
||||
|
||||
**Objective:** runtime 从本地草稿模式升级为可读取后端发布态 config,并提交正式派生结果。
|
||||
|
||||
**Files likely to change:**
|
||||
- Create: `src/services/bark-battle-runtime/barkBattleRuntimeClient.ts`
|
||||
- Create: `src/services/bark-battle-works/barkBattleWorksClient.ts`
|
||||
- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- Modify: `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- Modify: `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- Modify: `src/services/publicWorkCode.ts`
|
||||
|
||||
**Behavior:**
|
||||
- 本地 preview 仍可使用 draft config。
|
||||
- 正式作品 runtime 必须先调用 start API,拿 run token/session。
|
||||
- finish 只提交派生 metrics。
|
||||
- 发布后刷新作品架/广场。
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
npm run test -- src/services/bark-battle-runtime/**/*.test.ts src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
### Task 3.7:Phase 3 收口验证
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
npm run test -- src/games/bark-battle/**/*.test.ts src/games/bark-battle/**/*.test.tsx src/services/bark-battle-runtime/**/*.test.ts src/services/bark-battle-creation/**/*.test.ts
|
||||
npm run typecheck
|
||||
npm run lint:eslint
|
||||
npm run check:encoding
|
||||
npm run check:server-rs-ddd
|
||||
cd server-rs && cargo test -p module-bark-battle --no-default-features
|
||||
cd server-rs && cargo check -p api-server -p spacetime-module -p spacetime-client -p shared-contracts --no-default-features
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
**Manual smoke checklist:**
|
||||
- [ ] 创作者生成并发布 bark-battle 作品。
|
||||
- [ ] 玩家从作品页进入 runtime。
|
||||
- [ ] start API 成功并写 `work_play_start`。
|
||||
- [ ] 浏览器本地完成一局。
|
||||
- [ ] finish API 只上传派生指标。
|
||||
- [ ] 成绩/排行榜/作品架刷新来自后端投影。
|
||||
- [ ] 拒绝麦克风权限时不会创建非法 finished result。
|
||||
|
||||
---
|
||||
|
||||
## 5. 文件清单总览
|
||||
|
||||
### Phase 1 likely files
|
||||
|
||||
- `src/routing/appRoutes.tsx`
|
||||
- `src/BarkBattlePlaygroundApp.tsx`
|
||||
- `src/games/bark-battle/domain/BarkBattleTypes.ts`
|
||||
- `src/games/bark-battle/domain/BarkDetector.ts`
|
||||
- `src/games/bark-battle/domain/EnergyTugOfWar.ts`
|
||||
- `src/games/bark-battle/domain/BarkBattleSession.ts`
|
||||
- `src/games/bark-battle/domain/BarkBattleScoring.ts`
|
||||
- `src/games/bark-battle/domain/OpponentStrategy.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleConfig.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleController.ts`
|
||||
- `src/games/bark-battle/application/BarkBattleSnapshotStore.ts`
|
||||
- `src/games/bark-battle/infrastructure/BrowserMicrophoneInput.ts`
|
||||
- `src/games/bark-battle/infrastructure/AudioAnalyserSampler.ts`
|
||||
- `src/games/bark-battle/infrastructure/MicrophonePermission.ts`
|
||||
- `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleHud.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleResultPanel.tsx`
|
||||
- `src/games/bark-battle/ui/BarkBattleHud.css`
|
||||
|
||||
### Phase 2 likely files
|
||||
|
||||
- `packages/shared/src/contracts/barkBattle.ts`
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
- `src/components/bark-battle-creation/BarkBattleAgentWorkspace.tsx`
|
||||
- `src/components/bark-battle-result/BarkBattleResultView.tsx`
|
||||
- `src/services/bark-battle-creation/*`
|
||||
|
||||
### Phase 3 likely files
|
||||
|
||||
- `server-rs/crates/shared-contracts/src/bark_battle.rs`
|
||||
- `server-rs/crates/module-bark-battle/*`
|
||||
- `server-rs/crates/spacetime-module/src/runtime/bark_battle.rs`
|
||||
- `server-rs/crates/spacetime-module/src/migration.rs`
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- `server-rs/crates/api-server/src/bark_battle.rs`
|
||||
- `src/services/bark-battle-runtime/*`
|
||||
- `src/services/bark-battle-works/*`
|
||||
- `src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- `src/services/publicWorkCode.ts`
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险、取舍与开放问题
|
||||
|
||||
### 风险
|
||||
|
||||
1. **麦克风权限和移动端 AudioContext 差异大。** 需要 mock input 保底,否则自动化和本地开发会被真实设备阻塞。
|
||||
2. **第一阶段过早引入 Phaser 可能拖慢验证。** 当前仓库没有 `phaser` 依赖;建议先用 DOM/Canvas 跑通玩法,再决定是否引入 Phaser。
|
||||
3. **AI 草稿和正式发布配置容易漂移。** Phase 2 临时 TS 类型必须在 Phase 3 与 Rust shared-contracts 对齐。
|
||||
4. **不能保存原始音频。** 后端阶段只能保存派生指标,任何音频片段、波形、频谱明细都不应落库。
|
||||
5. **入口配置事实源在后端/SpacetimeDB。** Phase 2 接入口时不要恢复旧前端静态入口配置。
|
||||
|
||||
### 取舍
|
||||
|
||||
- Phase 1 先把“游戏是否好玩、功能是否跑通”作为第一目标,不追求正式作品闭环。
|
||||
- Phase 2 让 AI 生成内容配置,而不是让 AI 直接生成任意代码或不受控规则。
|
||||
- Phase 3 再把正式业务真相交给后端,避免前端 runtime 先背上发布、成绩、排行榜的复杂度。
|
||||
|
||||
### 开放问题
|
||||
|
||||
1. Phase 1 是否必须使用 Phaser?如果只是验证玩法,可先使用 DOM/CSS/Canvas 原型,后续再替换 renderer。
|
||||
2. `bark-battle` 的正式中文名是否固定为“汪汪声浪大作战”?如果名称要改,需先统一文档、入口配置和分享标题。
|
||||
3. AI 创作阶段是否需要生成图片/狗狗视觉资产,还是只生成风格描述和使用占位素材?
|
||||
4. 是否需要排行榜作为 Phase 3 必选,还是作为数据库落地后的增强项?
|
||||
5. 真实麦克风 smoke 需要哪些目标设备:Chrome 桌面、Android Chrome、iOS Safari 是否都纳入首批验收?
|
||||
|
||||
---
|
||||
|
||||
## 7. 建议执行方式
|
||||
|
||||
1. 先按 Phase 1 执行,且每个 domain/application task 坚持 TDD:先失败测试,再实现。
|
||||
2. Phase 1 合并前不要接数据库,不要新增后端表,不要把入口配置切到 open。
|
||||
3. Phase 1 验证通过后,让产品/团队试玩 `/bark-battle`,确认玩法数值和 UI 方向。
|
||||
4. 再进入 Phase 2,把 AI 创作工作台接到同一个 runtime draft config。
|
||||
5. 最后进入 Phase 3,按后端 DDD 文档做数据库、发布、成绩和追踪闭环。
|
||||
|
||||
310
.hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# K6 作品列表压测计划(使用 spacetime-migration-7.json 作为数据源)
|
||||
|
||||
## 目标
|
||||
|
||||
使用 K6 对 Genarrative 的“作品列表”相关接口进行压测,并将用户提供的 `spacetime-migration-7.json` 作为压测数据源;数据处理时**只导入作品列表相关数据**,不导入用户、会话、钱包、埋点、运行存档等非作品表,避免把敏感或无关数据带入压测环境。
|
||||
|
||||
## 当前上下文
|
||||
|
||||
- 工作区:`/c/proj/Genarrative`
|
||||
- 原始迁移文件:`C:\Users\DSK\AppData\Local\hermes\cache\documents\doc_150e84029b2d_spacetime-migration-7.json`
|
||||
- 已确认原始迁移文件结构:
|
||||
- `schema_version = 1`
|
||||
- `tables = 53`
|
||||
- 作品相关表中当前有数据的重点表:
|
||||
- `puzzle_work_profile`:80 行
|
||||
- `custom_world_profile`:1 行
|
||||
- `match3d_work_profile`:0 行
|
||||
- `big_fish_*`:当前样本中相关表为 0 行
|
||||
- 原始文件还包含 `user_account`、`auth_identity`、`refresh_session`、`profile_wallet_ledger`、`asset_object`、运行记录等数据,压测导入时必须过滤。
|
||||
- 当前仓库未发现现成 K6 脚本或 `k6` 相关文件,需要新增压测脚本与数据提取脚本。
|
||||
- `package.json` 当前有 `dev/dev:rust/test/check` 等脚本,未发现 K6 npm script。
|
||||
|
||||
## 范围约束
|
||||
|
||||
### 本次只导入/使用
|
||||
|
||||
1. 作品列表表:
|
||||
- `puzzle_work_profile`
|
||||
- `custom_world_profile`
|
||||
- 后续若接口覆盖其他玩法,可扩展:
|
||||
- `match3d_work_profile`
|
||||
- `square_hole_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
- `big_fish_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
- `visual_novel_work_profile`(以实际 SpacetimeDB 表名为准)
|
||||
2. 为作品列表卡片展示所需的最小字段:
|
||||
- 稳定 ID:`profile_id`、`work_id` 或 `public_work_code`
|
||||
- 标题:`work_title` / `level_name` / `world_name`
|
||||
- 描述:`work_description` / `summary` / `summary_text` / `subtitle`
|
||||
- 作者:`owner_user_id`、`author_display_name`、`author_public_user_code`
|
||||
- 封面:`cover_image_src`、`cover_asset_id`(如果接口只返回 asset id,则压测阶段不额外导入二进制 asset)
|
||||
- 状态与计数:`publication_status`、`published_at`、`play_count`、`like_count`、`remix_count`
|
||||
- 作品内容摘要:`levels_json`、`profile_payload_json`、`theme_tags_json` 等列表渲染或进入作品详情可能需要的 JSON 字段
|
||||
|
||||
### 本次不导入/不使用
|
||||
|
||||
- 认证与账号:`user_account`、`auth_identity`、`refresh_session`、`auth_store_snapshot`
|
||||
- 用户资产与钱包:`profile_wallet_ledger`、`profile_dashboard_state`、`profile_redeem_*`、`profile_invite_*`
|
||||
- 游玩历史/存档/运行态:`profile_played_world`、`public_work_play_daily_stat`、`puzzle_runtime_run`、`profile_save_archive`、`runtime_snapshot` 等
|
||||
- AI 任务过程:`ai_task`、`ai_task_stage`、`ai_text_chunk`
|
||||
- asset 二进制与绑定:`asset_object`、`asset_entity_binding`,除非后续确认作品列表接口强依赖它们;即便需要,也只导入作品列表封面所需的最小 metadata,不导入原始大对象。
|
||||
|
||||
## 推荐目录与文件
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
.hermes/plans/2026-05-11_195214-k6-works-list-load-test-plan.md # 本计划
|
||||
scripts/loadtest/extract-works-list-data.mjs # 从迁移文件提取作品列表数据
|
||||
scripts/loadtest/k6-works-list.js # K6 压测脚本
|
||||
scripts/loadtest/data/works-list.sample.json # 过滤后的样例数据(不要提交敏感原始迁移全量)
|
||||
scripts/loadtest/README.md # 执行说明与指标阈值
|
||||
```
|
||||
|
||||
可选新增 npm scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"loadtest:extract-works": "node scripts/loadtest/extract-works-list-data.mjs",
|
||||
"loadtest:k6:works": "k6 run scripts/loadtest/k6-works-list.js"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据提取方案
|
||||
|
||||
### 输入
|
||||
|
||||
默认读取:
|
||||
|
||||
```bash
|
||||
node scripts/loadtest/extract-works-list-data.mjs \
|
||||
--input "C:/Users/DSK/AppData/Local/hermes/cache/documents/doc_150e84029b2d_spacetime-migration-7.json" \
|
||||
--output scripts/loadtest/data/works-list.local.json
|
||||
```
|
||||
|
||||
### 输出结构
|
||||
|
||||
建议输出为 K6 直接可读的 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "spacetime-migration-7.json",
|
||||
"generatedAt": "<iso datetime>",
|
||||
"tables": {
|
||||
"puzzle_work_profile": [
|
||||
{
|
||||
"profile_id": "...",
|
||||
"work_id": "...",
|
||||
"owner_user_id": "...",
|
||||
"work_title": "...",
|
||||
"work_description": "...",
|
||||
"publication_status": "Published",
|
||||
"published_at": { "__timestamp_micros_since_unix_epoch__": 0 },
|
||||
"play_count": 0,
|
||||
"like_count": 0,
|
||||
"levels_json": "..."
|
||||
}
|
||||
],
|
||||
"custom_world_profile": []
|
||||
},
|
||||
"workIds": {
|
||||
"puzzle": ["<profile_id>"],
|
||||
"customWorld": ["<profile_id>"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 过滤原则
|
||||
|
||||
1. 按 `tables[].name` 白名单过滤,只保留作品 profile 表。
|
||||
2. 对每个 row 再按字段白名单过滤,避免误带账号、手机号、token、钱包流水等字段。
|
||||
3. 对特别大的字段进行处理:
|
||||
- `cover_image_src` 如果是 `data:image/...base64`,默认替换为占位符或截断,避免压测数据文件过大。
|
||||
- `levels_json`、`profile_payload_json` 保留原文,但可以记录大小;如果过大,再提供 `--compact` 选项只保留摘要。
|
||||
4. 输出 `.local.json` 默认加入 `.gitignore`;如果要提交样例数据,只提交脱敏/裁剪后的 `works-list.sample.json`。
|
||||
|
||||
## K6 压测接口矩阵
|
||||
|
||||
需要先确认本地 api-server 实际端口。默认以 `http://127.0.0.1:8787` 为例,实际运行时通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:<actual-api-port> k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
初版建议覆盖以下“作品列表”读接口,具体路径以仓库服务端路由为准,实施时需要通过搜索 api-server 路由确认:
|
||||
|
||||
| 场景 | 目的 | 候选路径 |
|
||||
| --- | --- | --- |
|
||||
| 拼图作品列表 | 作品列表主场景之一,当前数据量最多 | `/api/creation/puzzle/works` 或实际 puzzle works list route |
|
||||
| RPG/自定义世界作品列表 | 使用 `custom_world_profile` 数据 | `/api/creation/custom-world/works` 或实际 custom world works route |
|
||||
| 作品详情/启动前读取 | 模拟用户从列表点进作品 | `/api/creation/*/works/:profileId` 或 `/api/runtime/*/works/:profileId` |
|
||||
| 公开作品库 | 如果首页/发现页依赖 | `/api/runtime/*/works` 或 gallery/list route |
|
||||
|
||||
> 注意:不要凭空固定 endpoint。实施阶段先用 `search_files` / 路由源码确认真实路径,再写入 K6 脚本。
|
||||
|
||||
## K6 场景设计
|
||||
|
||||
### 阶段 1:基线 smoke
|
||||
|
||||
目的:确认脚本、数据和目标服务可用。
|
||||
|
||||
```js
|
||||
export const options = {
|
||||
scenarios: {
|
||||
smoke: {
|
||||
executor: 'constant-vus',
|
||||
vus: 1,
|
||||
duration: '30s'
|
||||
}
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ['rate<0.01'],
|
||||
http_req_duration: ['p(95)<800']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 阶段 2:常规读压
|
||||
|
||||
目的:模拟日常列表浏览。
|
||||
|
||||
- `constant-vus`: 10/25/50 三档
|
||||
- 每个 VU 随机选择作品类型和列表分页参数
|
||||
- `sleep(0.5~2s)` 模拟用户停留
|
||||
- 阈值建议:
|
||||
- `http_req_failed < 1%`
|
||||
- `p95 < 800ms`
|
||||
- `p99 < 1500ms`
|
||||
|
||||
### 阶段 3:峰值/突刺
|
||||
|
||||
目的:模拟首页入口或活动导致的作品列表突增。
|
||||
|
||||
- `ramping-arrival-rate`
|
||||
- 从 5 RPS 增长到 100 RPS,维持 2~5 分钟,再降回
|
||||
- 单独输出 `checks`:列表接口状态码、响应 JSON shape、items 数量
|
||||
|
||||
### 阶段 4:容量探索
|
||||
|
||||
目的:找瓶颈,不作为每次回归必跑。
|
||||
|
||||
- 每轮提升 RPS 或 VU
|
||||
- 观察:api-server CPU/内存、SpacetimeDB 日志、错误率、p95/p99
|
||||
- 一旦 `http_req_failed >= 5%` 或 p95 持续超过 2s,停止继续加压并记录容量点。
|
||||
|
||||
## K6 脚本设计要点
|
||||
|
||||
1. 使用 `SharedArray` 加载 `works-list.local.json`,避免每个 VU 重复解析大 JSON。
|
||||
2. 基于数据源里的 `profile_id` / `work_id` 随机抽样,保证请求覆盖真实作品 ID。
|
||||
3. 对列表接口添加分页/排序 query,例如:
|
||||
- `?limit=20&offset=0`
|
||||
- `?pageSize=20&cursor=...`(以真实 API 为准)
|
||||
4. 使用 `check()` 验证:
|
||||
- HTTP 200
|
||||
- 响应体是 JSON
|
||||
- `items` 或 `works` 是数组
|
||||
- 列表项包含 `profileId/profile_id`、标题字段、状态字段
|
||||
5. 使用 `Trend` / `Rate` 细分指标:
|
||||
- `works_list_duration`
|
||||
- `works_detail_duration`
|
||||
- `works_list_shape_error_rate`
|
||||
6. 支持环境变量:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:8787 \
|
||||
WORKS_DATA=scripts/loadtest/data/works-list.local.json \
|
||||
SCENARIO=baseline \
|
||||
k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. **确认路由**
|
||||
- 搜索 api-server / BFF 的作品列表路由。
|
||||
- 明确各玩法对应 endpoint、鉴权要求、分页参数、返回字段。
|
||||
2. **实现数据提取脚本**
|
||||
- 新增 `scripts/loadtest/extract-works-list-data.mjs`。
|
||||
- 只按表白名单读取作品列表 profile 表。
|
||||
- 对字段做白名单与脱敏/截断。
|
||||
- 输出 `works-list.local.json`。
|
||||
3. **生成本地压测数据**
|
||||
- 用用户提供的迁移文件生成 `scripts/loadtest/data/works-list.local.json`。
|
||||
- 验证输出只包含作品表和作品字段。
|
||||
4. **实现 K6 脚本**
|
||||
- 新增 `scripts/loadtest/k6-works-list.js`。
|
||||
- 支持 `BASE_URL`、`WORKS_DATA`、`SCENARIO`。
|
||||
- 覆盖列表接口,必要时增加详情/启动前读取接口。
|
||||
5. **新增执行说明**
|
||||
- 在 `scripts/loadtest/README.md` 写明:安装 K6、启动本地 dev 栈、提取数据、运行 smoke/baseline/spike、查看结果。
|
||||
6. **本地验证**
|
||||
- 启动 Genarrative dev 栈;注意端口可能漂移,使用实际 api-server 端口。
|
||||
- 跑 smoke:`SCENARIO=smoke`。
|
||||
- 确认失败率、p95、响应 shape。
|
||||
7. **可选集成 npm scripts**
|
||||
- 如果团队希望标准化入口,再加入 `package.json` scripts。
|
||||
8. **记录结果**
|
||||
- 将 smoke/baseline/spike 的结果摘要追加到 `scripts/loadtest/README.md` 或单独保存到 `.hermes/plans/` 的结果文档中。
|
||||
|
||||
## 启动与运行建议
|
||||
|
||||
本地服务启动按当前 Genarrative dev 栈约定:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
如果 SpacetimeDB/API/Vite 端口被占用,项目脚本会寻找可用端口;压测时必须从启动日志中读取实际 api-server 地址,并传给 K6:
|
||||
|
||||
```bash
|
||||
BASE_URL=http://127.0.0.1:<actual-api-port> \
|
||||
WORKS_DATA=scripts/loadtest/data/works-list.local.json \
|
||||
SCENARIO=smoke \
|
||||
k6 run scripts/loadtest/k6-works-list.js
|
||||
```
|
||||
|
||||
## 验证标准
|
||||
|
||||
### 数据源验证
|
||||
|
||||
- `works-list.local.json` 中只出现作品 profile 表。
|
||||
- 不出现以下字段或内容:
|
||||
- `password_hash`
|
||||
- `refresh_token_hash`
|
||||
- `phone_number_e164`
|
||||
- `phone_number_masked`
|
||||
- `wallet_ledger_id`
|
||||
- `auth_identity`
|
||||
- `user_account`
|
||||
- `puzzle_work_profile` 行数应接近原始文件中的 80 行。
|
||||
- `custom_world_profile` 行数应接近原始文件中的 1 行。
|
||||
|
||||
### K6 smoke 验证
|
||||
|
||||
- 所有目标接口返回 2xx。
|
||||
- `http_req_failed < 1%`。
|
||||
- 响应 JSON shape 与 shared contracts 对齐:`items` 或 `works` 数组。
|
||||
- K6 输出中能区分不同 endpoint 的耗时。
|
||||
|
||||
### 性能阈值初稿
|
||||
|
||||
- Smoke:`p95 < 800ms`,失败率 `< 1%`
|
||||
- Baseline:`p95 < 1000ms`,`p99 < 2000ms`,失败率 `< 1%`
|
||||
- Spike:允许短暂 p95 抖动,但 1 分钟内应恢复;失败率 `< 5%`
|
||||
|
||||
阈值后续需要结合本地机器性能、SpacetimeDB 本地模式和正式部署规格调整。
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **原始迁移文件包含敏感数据。** 必须只提取作品列表白名单字段,禁止把原始 JSON 全量提交到仓库。
|
||||
2. **base64 封面可能导致压测数据膨胀。** 默认截断或替换为占位符,除非本次明确要测封面 payload 对响应体积的影响。
|
||||
3. **本地 SpacetimeDB 与 api-server 端口会漂移。** 不要硬编码端口,运行时通过 `BASE_URL` 注入。
|
||||
4. **列表接口可能需要鉴权。** 若实际接口要求登录,不要导入真实 refresh session;应使用本地测试账号或专门的压测 token 生成流程。
|
||||
5. **作品表名/接口路径可能与候选名称不完全一致。** 实施前必须以源码路由为准。
|
||||
6. **本计划仅保存压测方案,不执行实际压测。** 后续执行时再创建/修改脚本、导出过滤数据、跑 K6 并记录结果。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. 压测目标是本地 dev 栈、测试环境,还是预发/生产只读接口?不同环境阈值和安全边界不同。
|
||||
2. “作品列表”是否只包含拼图和自定义世界,还是要覆盖 match3d、square-hole、big-fish、visual-novel 的统一列表入口?
|
||||
3. 是否允许使用专门压测账号/token?如果接口无鉴权则无需处理。
|
||||
4. 是否需要测封面/asset 加载,还是只测作品列表 JSON API?
|
||||
447
.hermes/plans/2026-05-11_205645-genarrative-disaster-recovery.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Genarrative 容灾方案设计计划
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 基于当前 Genarrative 单机生产部署、Jenkins 流水线、SpacetimeDB 与 Rust `api-server` 架构,补齐一套可落地、可演练、可审计的容灾方案。
|
||||
|
||||
**Architecture:** 首版容灾不引入复杂多活系统,优先围绕现有 `systemd + Nginx + SpacetimeDB + api-server + Jenkins` 单机生产推荐方案做“备份可恢复、版本可回滚、故障可切换、演练可复盘”。方案采用分层容灾:入口层、静态资源层、API 服务层、SpacetimeDB 数据层、外部服务与密钥层、Jenkins/发布链路层。
|
||||
|
||||
**Tech Stack:** Nginx、systemd、SpacetimeDB self-hosting、Rust `api-server` / Axum、Jenkins Pipeline、Shell/Node.js 运维脚本、仓库 `deploy/` 与 `docs/technical/` 文档体系。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前上下文与已确认事实
|
||||
|
||||
### 1.1 当前生产部署口径
|
||||
|
||||
来自 `docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 的现状:
|
||||
|
||||
- 生产为单机推荐方案,不使用 Docker。
|
||||
- 公网入口为 Nginx,负责 HTTPS、静态站点、后台静态页面、维护页、`/admin/api/` 与临时 `/api/*` 反向代理。
|
||||
- SpacetimeDB 作为 systemd 服务运行:
|
||||
- `spacetimedb.service`
|
||||
- 监听:`127.0.0.1:3101`
|
||||
- 数据根目录:`/stdb`
|
||||
- Rust `api-server` 作为 systemd 服务运行:
|
||||
- `genarrative-api.service`
|
||||
- 监听:`127.0.0.1:8082`
|
||||
- 环境文件:`/etc/genarrative/api-server.env`
|
||||
- 静态站点发布到 release/current 目录:
|
||||
- `/opt/genarrative/releases/<version>/`
|
||||
- `/opt/genarrative/current`
|
||||
- `/srv/genarrative/web`
|
||||
- 已有维护模式:
|
||||
- 开关文件:`/var/lib/genarrative/maintenance/enabled`
|
||||
- API 发布、SpacetimeDB 模块发布、数据库导入、服务器配置变更必须进入维护模式。
|
||||
- 已有数据库导入导出 Jenkins Job:
|
||||
- `Genarrative-Database-Export`
|
||||
- `Genarrative-Database-Import`
|
||||
- 对应文件:`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`
|
||||
- 已有回滚基本口径:
|
||||
- Web 回滚:切 `/srv/genarrative/web` 或 `/opt/genarrative/current` 到上一版本并 reload Nginx。
|
||||
- API 回滚:切 `/opt/genarrative/current` 到上一版本并重启 `genarrative-api.service`。
|
||||
- SpacetimeDB 模块回滚:发布上一版本 `spacetime_module.wasm`。
|
||||
- 数据回滚:使用导入流水线恢复指定备份,必须进入维护模式。
|
||||
|
||||
### 1.2 关键风险
|
||||
|
||||
- 当前是单机生产拓扑,单机磁盘、系统盘、`/stdb`、Nginx 或公网 IP 故障会造成整体不可用。
|
||||
- SpacetimeDB 是核心业务真相,容灾重点必须围绕 `/stdb`、数据库导出产物、schema 迁移与导入验证。
|
||||
- `/etc/genarrative/api-server.env` 持有生产密钥,不能进入 Git,也不能写进普通备份明文归档。
|
||||
- Jenkins controller/agent 同时承担构建、发布、备份、导入导出编排;Jenkins 不可用时仍需要有最小人工恢复路径。
|
||||
- 外部 LLM、图片、语音、3D 网关不是本仓库可控系统,容灾只能做到配置降级、超时隔离、能力熔断与可观测告警。
|
||||
|
||||
---
|
||||
|
||||
## 2. 容灾目标
|
||||
|
||||
### 2.1 恢复目标建议
|
||||
|
||||
| 灾难类型 | 目标 RTO | 目标 RPO | 首版策略 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Web 静态资源发布失败 | 5 分钟 | 0 | release/current 原子切换回滚 |
|
||||
| API 发布失败 | 10 分钟 | 0 | 维护模式 + 上一版二进制回滚 |
|
||||
| SpacetimeDB wasm 发布失败 | 15 分钟 | 0 或按迁移前备份 | 发布前导出 + 上一版 wasm 回滚 |
|
||||
| 数据误写 / 迁移失败 | 30-60 分钟 | 最近一次导出点 | 导入流水线从备份恢复 |
|
||||
| 生产机磁盘损坏 | 2-4 小时 | 最近一次异地备份 | 新机器 provision + 拉取 release 包 + 恢复数据库 |
|
||||
| Jenkins controller 不可用 | 1-2 小时 | 不影响线上数据 | 手工脚本恢复 + Jenkins 备份恢复 |
|
||||
| 第三方模型网关不可用 | 5-15 分钟内降级 | 不丢核心数据 | 配置切换 / 功能熔断 / 队列失败可重试 |
|
||||
|
||||
### 2.2 首版不做
|
||||
|
||||
- 不做跨地域双活写入。
|
||||
- 不做 SpacetimeDB 在线主从复制,除非后续官方能力与项目压测验证支持。
|
||||
- 不让前端绕过 `api-server` 直接承担正式业务真相。
|
||||
- 不把生产密钥、Token、数据库 dump、Jenkins secret 写入 Git。
|
||||
- 不恢复旧 `server-node`、Express、PostgreSQL 或 Docker 一体化部署方案。
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体容灾设计
|
||||
|
||||
### 3.1 分层策略
|
||||
|
||||
1. **入口层:Nginx / DNS / HTTPS**
|
||||
- 保留 Nginx 配置模板在 Git:`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`。
|
||||
- 为 release 环境建立 Nginx 配置备份与证书恢复流程。
|
||||
- 明确 DNS 切换预案:生产机不可恢复时,将域名指向灾备机公网 IP。
|
||||
|
||||
2. **静态资源层:Web / Admin Web**
|
||||
- 依赖 `web.tar.gz`、`web.tar.gz.sha256`、`release-manifest.json`。
|
||||
- 保留最近 N 个 release 目录与构建产物指针。
|
||||
- 回滚只切软链,不重新构建。
|
||||
|
||||
3. **API 服务层:Rust `api-server`**
|
||||
- 依赖归档的 `api-server` 二进制、checksum、`release-manifest.json`。
|
||||
- `/etc/genarrative/api-server.env` 通过加密备份或密钥管理恢复,不进入 release 包。
|
||||
- systemd unit 由 `deploy/systemd/genarrative-api.service` 重新安装。
|
||||
|
||||
4. **数据层:SpacetimeDB**
|
||||
- 每次高风险发布前强制导出数据库。
|
||||
- 定时导出:建议每天至少 1 次;高活跃期可每 4 小时 1 次。
|
||||
- 导出产物同时保存在:Jenkins 归档 + 生产机 `SERVER_BACKUP_DIRECTORY` + 异地对象存储/备份机。
|
||||
- 导入前自动生成安全备份,保留当前实现口径。
|
||||
|
||||
5. **发布编排层:Jenkins**
|
||||
- Jenkins Job、Jenkinsfile 在 Git 中可恢复。
|
||||
- Jenkins controller 配置、凭据、插件清单需要额外备份。
|
||||
- 发布 agent 使用 inbound + systemd 自恢复,agent secret 仅存在目标机或 Jenkins 凭据。
|
||||
|
||||
6. **密钥与外部服务层**
|
||||
- `/etc/genarrative/api-server.env`、Jenkins Secret Text、SSH PEM、agent secret 不进 Git。
|
||||
- 制定密钥清单和恢复责任人,但不在仓库记录明文。
|
||||
- 外部服务配置按 `docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md` 维护必配项。
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议新增/更新的文档
|
||||
|
||||
### Task 1: 新增生产容灾技术方案文档
|
||||
|
||||
**Objective:** 形成团队可共享、可执行的容灾总纲。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Modify: `docs/technical/README.md`(若已有技术索引,应加入该文档入口)
|
||||
- Optional Modify: `.hermes/shared-memory/project-overview.md`(只加稳定索引,不写敏感信息)
|
||||
|
||||
**文档必须覆盖:**
|
||||
|
||||
1. 容灾目标:RTO/RPO 表。
|
||||
2. 生产资产清单:Nginx、systemd、release/current、`/stdb`、`/etc/genarrative/api-server.env`、Jenkins、构建产物。
|
||||
3. 备份策略:
|
||||
- 数据库导出。
|
||||
- release 产物保留。
|
||||
- Nginx/systemd/env 配置备份。
|
||||
- Jenkins 配置备份。
|
||||
4. 恢复流程:
|
||||
- Web 回滚。
|
||||
- API 回滚。
|
||||
- Stdb module 回滚。
|
||||
- 数据恢复。
|
||||
- 整机重建。
|
||||
5. 演练计划:每月一次数据库恢复演练,每季度一次整机重建演练。
|
||||
6. 安全边界:密钥不进 Git,备份加密,最小权限。
|
||||
7. 验收命令与人工检查清单。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS,无中文乱码、无 BOM/CRLF 问题。
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议新增/更新的脚本与流水线
|
||||
|
||||
### Task 2: 增强数据库定时备份流水线
|
||||
|
||||
**Objective:** 把现有人工导出扩展为可定时执行、可异地保存、可审计的备份流程。
|
||||
|
||||
**Files:**
|
||||
- Modify: `jenkins/Jenkinsfile.production-database-export`
|
||||
- Modify: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Optional Create: `scripts/deploy/production-backup-sync.sh`
|
||||
|
||||
**Implementation notes:**
|
||||
|
||||
- 在 Jenkins Job 中保留人工触发能力,同时建议配置 cron:
|
||||
- development:每天凌晨。
|
||||
- release:每天凌晨或业务低峰。
|
||||
- 增加备份命名规范:
|
||||
- `spacetime-migration-<database>-<yyyyMMdd-HHmmss>-<source_commit>.json`
|
||||
- 增加 `SERVER_BACKUP_DIRECTORY` 默认建议:
|
||||
- `/var/backups/genarrative/spacetimedb/<database>/`
|
||||
- 增加备份保留策略:
|
||||
- 本机保留 7-14 天。
|
||||
- 异地保留 30-90 天。
|
||||
- 如实现 `production-backup-sync.sh`,只做同步框架,不硬编码真实 bucket、账号、endpoint 或密钥。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/production-backup-sync.sh
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: shell 语法通过;文档编码检查通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 增加灾备恢复 Runbook
|
||||
|
||||
**Objective:** 在真正故障时不依赖临场推理,按清单执行恢复。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/operations/PRODUCTION_DR_RUNBOOK_2026-05-11.md`
|
||||
- Modify: `docs/operations/README.md`(如果存在)
|
||||
|
||||
**Runbook sections:**
|
||||
|
||||
1. 故障分级:P0/P1/P2。
|
||||
2. 第一响应:
|
||||
- 判断 Nginx 是否在线。
|
||||
- 判断 `genarrative-api.service` 是否在线。
|
||||
- 判断 `spacetimedb.service` 是否在线。
|
||||
- 判断磁盘是否满。
|
||||
- 判断 Jenkins agent 是否在线。
|
||||
3. 快速止血:
|
||||
- 开维护模式。
|
||||
- 禁止继续发布。
|
||||
- 保留现场日志。
|
||||
4. 回滚流程:
|
||||
- Web 回滚命令。
|
||||
- API 回滚命令。
|
||||
- Stdb wasm 回滚命令。
|
||||
5. 数据恢复流程:
|
||||
- 选择备份。
|
||||
- dry-run 导入。
|
||||
- 确认导入。
|
||||
- smoke test。
|
||||
6. 整机重建流程:
|
||||
- 新机器 provision。
|
||||
- 恢复 `/etc/genarrative/api-server.env`。
|
||||
- 恢复 SpacetimeDB 数据。
|
||||
- 发布最近稳定 release。
|
||||
- DNS 切换。
|
||||
7. 复盘模板。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 增加备份健康检查与恢复演练记录模板
|
||||
|
||||
**Objective:** 防止“有备份但不可恢复”。
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/operations/DR_DRILL_REPORT_TEMPLATE.md`
|
||||
- Optional Create: `scripts/deploy/verify-database-backup.sh`
|
||||
- Modify: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
|
||||
**建议检查项:**
|
||||
|
||||
- 备份文件存在且大小非 0。
|
||||
- 备份文件 checksum 可验证。
|
||||
- 备份文件可被 `Genarrative-Database-Import` dry-run 解析。
|
||||
- 最近一次备份时间未超过 RPO 阈值。
|
||||
- 导入后 `/healthz` 可用。
|
||||
- 首页、后台登录页、关键 API smoke 可用。
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/verify-database-backup.sh
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
## 6. 具体恢复流程草案
|
||||
|
||||
### 6.1 Web 静态资源回滚
|
||||
|
||||
1. 进入目标机。
|
||||
2. 查看 release 目录:`/opt/genarrative/releases/`。
|
||||
3. 选择上一个稳定版本。
|
||||
4. 切换 `/srv/genarrative/web` 或 `/opt/genarrative/current` 软链。
|
||||
5. 执行 Nginx 配置检查与 reload。
|
||||
6. 访问首页与后台静态入口。
|
||||
|
||||
验收:
|
||||
|
||||
- `/` 返回最新稳定页面。
|
||||
- `/admin/` 返回后台页面。
|
||||
- 静态资源无 404。
|
||||
|
||||
### 6.2 API 回滚
|
||||
|
||||
1. 开维护模式。
|
||||
2. 切 `/opt/genarrative/current` 到上一版包含稳定 `api-server` 的 release。
|
||||
3. 重启 `genarrative-api.service`。
|
||||
4. 本机检查 `http://127.0.0.1:8082/healthz`。
|
||||
5. 检查 Nginx 反代路径。
|
||||
6. 解除维护模式。
|
||||
|
||||
验收:
|
||||
|
||||
- `systemctl status genarrative-api.service` 正常。
|
||||
- `/healthz` 正常。
|
||||
- 后台 `/admin/api/*` 基础接口正常。
|
||||
|
||||
### 6.3 SpacetimeDB 模块回滚
|
||||
|
||||
1. 开维护模式。
|
||||
2. 确认目标数据库名与当前 API 环境一致:`GENARRATIVE_SPACETIME_DATABASE`。
|
||||
3. 选择上一版 `spacetime_module.wasm`。
|
||||
4. 使用 `spacetimedb` 服务用户发布上一版 wasm。
|
||||
5. 重启或检查 `spacetimedb.service`。
|
||||
6. 检查 `api-server` 对目标数据库访问。
|
||||
7. 解除维护模式。
|
||||
|
||||
注意:如果 schema 已迁移且旧 wasm 不兼容当前数据,需要走数据恢复,不应直接盲目发布旧 wasm。
|
||||
|
||||
### 6.4 数据恢复
|
||||
|
||||
1. 开维护模式。
|
||||
2. 从 Jenkins 归档或 `SERVER_BACKUP_DIRECTORY` 选择备份。
|
||||
3. 先执行导入 dry-run。
|
||||
4. 真正导入前生成当前数据库安全备份。
|
||||
5. 执行导入。
|
||||
6. 执行 smoke test。
|
||||
7. 解除维护模式。
|
||||
|
||||
必须记录:
|
||||
|
||||
- 备份文件名。
|
||||
- 来源 Job/build number。
|
||||
- 恢复目标 database。
|
||||
- 恢复开始/结束时间。
|
||||
- 恢复后验证结果。
|
||||
|
||||
### 6.5 整机重建
|
||||
|
||||
1. 准备新 Linux 机器。
|
||||
2. 接入 Jenkins release deploy agent,或准备人工 SSH 运维路径。
|
||||
3. 运行 `Genarrative-Server-Provision`:
|
||||
- 创建用户和目录。
|
||||
- 安装 SpacetimeDB。
|
||||
- 安装 systemd unit。
|
||||
- 安装 Nginx 配置。
|
||||
4. 恢复 `/etc/genarrative/api-server.env`。
|
||||
5. 发布最近稳定 Web/API/Stdb 产物。
|
||||
6. 导入最近一次有效数据库备份。
|
||||
7. smoke test。
|
||||
8. 切 DNS。
|
||||
9. 观察 30-60 分钟。
|
||||
|
||||
---
|
||||
|
||||
## 7. 文件可能变更清单
|
||||
|
||||
首版落地建议按以下文件收口:
|
||||
|
||||
- Create: `docs/technical/PRODUCTION_DISASTER_RECOVERY_PLAN_2026-05-11.md`
|
||||
- Create: `docs/operations/PRODUCTION_DR_RUNBOOK_2026-05-11.md`
|
||||
- Create: `docs/operations/DR_DRILL_REPORT_TEMPLATE.md`
|
||||
- Modify: `docs/technical/README.md`
|
||||
- Modify: `docs/operations/README.md`(若存在)
|
||||
- Modify: `.hermes/shared-memory/project-overview.md`(仅增加文档索引)
|
||||
- Optional Modify: `jenkins/Jenkinsfile.production-database-export`
|
||||
- Optional Modify: `jenkins/Jenkinsfile.production-database-import`
|
||||
- Optional Create: `scripts/deploy/production-backup-sync.sh`
|
||||
- Optional Create: `scripts/deploy/verify-database-backup.sh`
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试与验收
|
||||
|
||||
### 8.1 文档与编码
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### 8.2 Shell 脚本语法
|
||||
|
||||
如新增 shell 脚本:
|
||||
|
||||
```bash
|
||||
bash -n scripts/deploy/production-backup-sync.sh
|
||||
bash -n scripts/deploy/verify-database-backup.sh
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
### 8.3 Jenkinsfile 静态检查
|
||||
|
||||
建议在 Jenkins UI 或本地 Jenkins Pipeline Linter 中检查:
|
||||
|
||||
- `jenkins/Jenkinsfile.production-database-export`
|
||||
- `jenkins/Jenkinsfile.production-database-import`
|
||||
|
||||
Expected: Pipeline syntax valid。
|
||||
|
||||
### 8.4 演练验收
|
||||
|
||||
至少完成一次 development 目标演练:
|
||||
|
||||
1. 触发 `Genarrative-Database-Export`。
|
||||
2. 确认备份产物存在并归档。
|
||||
3. 使用 `Genarrative-Database-Import` dry-run 验证备份可解析。
|
||||
4. 不覆盖生产数据的前提下,记录演练报告。
|
||||
|
||||
release 目标演练应在业务低峰进行,并先确认通知渠道可用。
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险、取舍与开放问题
|
||||
|
||||
### 9.1 风险
|
||||
|
||||
- 单机生产仍存在物理机级单点故障,首版只能通过“快速重建 + 异地备份”降低恢复时间。
|
||||
- SpacetimeDB schema 回滚不一定可逆,必须把发布前备份作为强约束。
|
||||
- Jenkins controller 若在本地 Windows,controller 自身备份和恢复需要单独制定,不应只依赖 agent 自恢复。
|
||||
- 外部模型网关失败可能影响创作能力,但不应影响已发布作品浏览和后台基础能力。
|
||||
|
||||
### 9.2 取舍
|
||||
|
||||
- 选择先做可执行 runbook 和备份恢复演练,而不是直接引入复杂多活。
|
||||
- 选择继续复用现有 Jenkins 导入导出流水线,降低工程改造风险。
|
||||
- 选择不把密钥恢复细节写死到 Git 文档,避免泄露。
|
||||
|
||||
### 9.3 开放问题
|
||||
|
||||
1. release 环境是否已经有独立备份机或对象存储?如果有,需要补充备份同步目标,但不能提交密钥。
|
||||
2. Jenkins controller 的 `JENKINS_HOME` 当前实际部署在哪里?是否已有周期备份?
|
||||
3. 生产域名 DNS TTL 当前是多少?是否可降低到适合故障切换的值?
|
||||
4. `/stdb` 所在磁盘是否独立于系统盘?是否已有磁盘水位告警?
|
||||
5. release 环境的通知渠道除邮件外是否需要接入企业微信/飞书/Telegram?
|
||||
|
||||
---
|
||||
|
||||
## 10. 推荐实施顺序
|
||||
|
||||
1. 先只落文档:技术方案 + runbook + 演练模板。
|
||||
2. 在 development 目标做一次数据库导出 + dry-run 导入演练。
|
||||
3. 根据演练结果补脚本:备份同步、备份健康检查。
|
||||
4. 再把 release 备份设置为定时任务。
|
||||
5. 最后规划整机重建演练与 DNS 切换演练。
|
||||
|
||||
首版完成标准:
|
||||
|
||||
- 团队任一成员打开 runbook,即可在 30 分钟内完成 Web/API 回滚或数据库备份 dry-run 恢复。
|
||||
- 最近一次数据库备份时间、备份位置、checksum、恢复演练结果可追溯。
|
||||
- 生产密钥仍只存在于服务器/Jenkins 凭据/加密备份中,不进入 Git。
|
||||
403
.hermes/plans/2026-05-11_205658-security-vulnerability-scan.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# 当前项目安全漏洞检查计划
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill only if the user later asks to execute this plan. 本计划当前仅用于规划,不实施代码修改。
|
||||
|
||||
**Goal:** 对 Genarrative 当前工作区做一次可复现的安全漏洞基线检查,覆盖依赖漏洞、密钥泄露、常见高风险代码模式、后端 Rust crate 风险和前端/Node 供应链风险,并输出可落地的整改清单。
|
||||
|
||||
**Architecture:** 采用“只读扫描 → 结果归档 → 人工分级 → 最小修复建议”的方式推进。先不直接升级依赖或改代码,避免安全扫描引入不可控 breaking change;执行阶段只在用户确认后运行扫描命令,并把报告保存到 `docs/audits/` 或 `.hermes/plans/` 附件中。
|
||||
|
||||
**Tech Stack:** Node/Vite/React/TypeScript、Rust workspace/Axum/SpacetimeDB、npm lockfile、Cargo.lock、Git worktree。
|
||||
|
||||
---
|
||||
|
||||
## 当前上下文 / 假设
|
||||
|
||||
- 当前有效工作区:`C:/proj/Genarrative/.worktrees/hermes-3337436a`。
|
||||
- 本次用户以 `/plan` 模式要求“检查一下当前项目的安全漏洞”,因此本轮只制定计划,不执行会产生报告、安装工具、修改依赖、提交或推送的操作。
|
||||
- 已确认项目包含:
|
||||
- 根 `package.json`,脚本包括 `npm run lint`、`npm run test`、`npm run build`、`npm run check:encoding`。
|
||||
- 根 `package-lock.json`。
|
||||
- `server-rs/Cargo.toml` 和 `server-rs/Cargo.lock`。
|
||||
- `apps/admin-web/package.json`、`packages/shared/package.json`。
|
||||
- `.hermes/shared-memory/development-workflow.md` 要求开发前读取共享记忆,并以当前代码、`docs/`、`AGENTS.md` 为准。
|
||||
- 安全扫描不应把真实密钥写入仓库;发现疑似密钥时只记录文件位置、变量名、脱敏片段和处置建议。
|
||||
|
||||
## 总体策略
|
||||
|
||||
1. 先做仓库状态和范围确认,避免扫描其他 worktree 或错误路径。
|
||||
2. 优先运行不会修改文件的安全检查:`npm audit --json`、`cargo audit`、密钥扫描、危险代码模式扫描。
|
||||
3. 分前端供应链、后端供应链、源码安全、配置/脚本安全四类归档。
|
||||
4. 对结果按严重级别分层:Critical / High / Medium / Low / Informational。
|
||||
5. 对每个真实问题给出:影响范围、证据、可行修复、验证命令、是否需要业务回归。
|
||||
6. 只有在用户确认进入执行/修复阶段后,才做依赖升级、代码修复、文档更新、测试和提交。
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step Plan
|
||||
|
||||
### Task 1: 确认扫描工作区和基线状态
|
||||
|
||||
**Objective:** 确保后续扫描针对当前 worktree,且不会误把既有未提交变更当成安全修复结果。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `AGENTS.md`
|
||||
- Read-only: `.hermes/README.md`
|
||||
- Read-only: `.hermes/shared-memory/development-workflow.md`
|
||||
- Read-only: `package.json`
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git status --short
|
||||
git branch --show-current
|
||||
git rev-parse --show-toplevel
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- `pwd` / `git rev-parse --show-toplevel` 指向 `C:/proj/Genarrative/.worktrees/hermes-3337436a` 对应路径。
|
||||
- 分支为当前隔离 worktree 分支。
|
||||
- 记录是否已有未提交变更;如存在,扫描报告需标注“基于含未提交变更的工作区”。
|
||||
|
||||
**Validation:**
|
||||
- 不修改任何项目文件。
|
||||
- 如发现路径不是当前 worktree,停止并重新确认路径。
|
||||
|
||||
### Task 2: 生成依赖清单和锁文件基线
|
||||
|
||||
**Objective:** 明确 Node 与 Rust 依赖入口,避免漏扫子包或 admin web。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `package.json`
|
||||
- Read-only: `package-lock.json`
|
||||
- Read-only: `apps/admin-web/package.json`
|
||||
- Read-only: `packages/shared/package.json`
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
- Read-only: `server-rs/Cargo.lock`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
npm --version
|
||||
node --version
|
||||
cargo --version
|
||||
rustc --version
|
||||
```
|
||||
|
||||
可选只读清单:
|
||||
|
||||
```bash
|
||||
npm ls --all --json > /tmp/genarrative-npm-ls.json
|
||||
cargo metadata --manifest-path server-rs/Cargo.toml --format-version 1 > /tmp/genarrative-cargo-metadata.json
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- 明确 npm / Node / Rust / Cargo 版本。
|
||||
- 若 `npm ls` 因 peer dependency 或历史依赖问题非 0,保留输出并继续 audit。
|
||||
|
||||
**Validation:**
|
||||
- `/tmp` 输出不进入 Git。
|
||||
- 不运行 `npm install`、`npm update`、`cargo update`。
|
||||
|
||||
### Task 3: Node 供应链漏洞扫描
|
||||
|
||||
**Objective:** 检查根 lockfile 覆盖的前端、脚本和 admin web 依赖漏洞。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `package-lock.json`
|
||||
- Read-only: `package.json`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
npm audit --json > /tmp/genarrative-npm-audit.json
|
||||
npm audit --audit-level=moderate
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- `npm audit --json` 生成机器可读结果。
|
||||
- 第二条命令给出人类可读摘要;如返回非 0,按漏洞严重度记录,不直接执行 `npm audit fix`。
|
||||
|
||||
**Result fields to extract:**
|
||||
- package name
|
||||
- vulnerable versions
|
||||
- installed version
|
||||
- severity
|
||||
- CVE / GHSA
|
||||
- via chain
|
||||
- fixAvailable 是否为 major/breaking
|
||||
- affected direct dependency or transitive dependency
|
||||
|
||||
**Validation:**
|
||||
- 不执行 `npm audit fix`。
|
||||
- 如 npm registry 网络不可用,记录阻塞原因和可重试命令。
|
||||
|
||||
### Task 4: Rust 供应链漏洞扫描
|
||||
|
||||
**Objective:** 检查 `server-rs` workspace 的 Cargo 依赖漏洞、弃用 crate 和 yanked crate。
|
||||
|
||||
**Files:**
|
||||
- Read-only: `server-rs/Cargo.toml`
|
||||
- Read-only: `server-rs/Cargo.lock`
|
||||
|
||||
**Commands:**
|
||||
|
||||
优先:
|
||||
|
||||
```bash
|
||||
cargo audit --json --manifest-path server-rs/Cargo.toml > /tmp/genarrative-cargo-audit.json
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
如果本机没有 `cargo audit`:
|
||||
|
||||
```bash
|
||||
cargo install cargo-audit --locked
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
**Execution note:**
|
||||
- 安装 `cargo-audit` 会改变用户 Cargo 工具目录,不属于纯只读扫描;执行前需用户确认。
|
||||
- 如果用户不希望安装工具,则记录“Rust 漏洞扫描未完成”,并给出本地安装或 CI 执行建议。
|
||||
|
||||
**Result fields to extract:**
|
||||
- advisory id
|
||||
- package
|
||||
- version
|
||||
- patched versions
|
||||
- unaffected versions
|
||||
- severity / CVSS if available
|
||||
- dependency path
|
||||
- whether it is runtime reachable in `api-server` / `spacetime-module`
|
||||
|
||||
**Validation:**
|
||||
- 不运行 `cargo update`。
|
||||
- 不改 `Cargo.lock`。
|
||||
|
||||
### Task 5: 密钥和敏感配置泄露扫描
|
||||
|
||||
**Objective:** 检查仓库中是否误提交 API key、token、私钥、cookie、`.env` 类文件或个人 Hermes 配置。
|
||||
|
||||
**Files / paths to scan:**
|
||||
- Full repo excluding `.git/`, `node_modules/`, `target/`, `dist/`, build artifacts。
|
||||
- 特别关注:`.hermes/`、`scripts/`、`server-rs/`、`apps/admin-web/`、`src/`、`docs/`。
|
||||
|
||||
**Preferred commands:**
|
||||
|
||||
如果有 gitleaks:
|
||||
|
||||
```bash
|
||||
gitleaks detect --source . --no-git --redact --report-format json --report-path /tmp/genarrative-gitleaks.json
|
||||
```
|
||||
|
||||
如果没有 gitleaks,先用只读 grep/ripgrep 兜底:
|
||||
|
||||
```bash
|
||||
git ls-files -z | xargs -0 grep -nIE "(api[_-]?key|secret|password|passwd|token|private[_-]?key|BEGIN (RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|AKIA[0-9A-Z]{16}|xox[baprs]-|sk-[A-Za-z0-9_-]{20,})" > /tmp/genarrative-secret-grep.txt || true
|
||||
```
|
||||
|
||||
**Execution note:**
|
||||
- 安装 gitleaks 需要用户确认。
|
||||
- grep 结果包含 false positive,必须人工分级,不得直接当作泄露结论。
|
||||
|
||||
**Validation:**
|
||||
- 报告中对值做脱敏,只保留前后 3-4 位或完全不记录值。
|
||||
- 如果发现 `.env.local` 或真实 token 被跟踪,立即标为 Critical。
|
||||
|
||||
### Task 6: 常见源码安全模式扫描
|
||||
|
||||
**Objective:** 快速发现高风险代码模式:命令注入、动态执行、路径穿越、危险反序列化、XSS、日志泄密、宽松 CORS 等。
|
||||
|
||||
**Files / paths:**
|
||||
- `src/**/*.{ts,tsx,js,mjs,cjs}`
|
||||
- `apps/admin-web/**/*.{ts,tsx,js,mjs,cjs}`
|
||||
- `scripts/**/*.{js,mjs,cjs,ts}`
|
||||
- `server-rs/crates/**/*.rs`
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# JS/TS 动态执行与 HTML 注入
|
||||
rg -n "\beval\(|new Function\(|dangerouslySetInnerHTML|innerHTML\s*=|document\.write\(" src apps scripts packages
|
||||
|
||||
# Node 命令执行风险
|
||||
rg -n "exec\(|execSync\(|spawn\(|spawnSync\(|shell:\s*true|child_process" scripts src apps packages
|
||||
|
||||
# Rust 命令、文件路径、unwrap 风险热点
|
||||
rg -n "Command::new|std::process|\.unwrap\(|\.expect\(|fs::|File::open|PathBuf|set_header|cors|CorsLayer" server-rs/crates
|
||||
|
||||
# 宽松 CORS / Cookie / Auth 相关热点
|
||||
rg -n "allow_origin|Any|cookie|Authorization|Bearer|refresh|access_token|set_cookie|SameSite|Secure|HttpOnly" server-rs/crates src apps scripts
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
- 输出作为“热点清单”,不等同于漏洞。
|
||||
- 对 auth/session、文件上传、OSS 签名、外部 LLM/图片服务请求、SpacetimeDB 访问 facade 做人工复核。
|
||||
|
||||
**Validation:**
|
||||
- 每个疑似问题必须能说明可利用条件,无法说明则降级为 Informational。
|
||||
|
||||
### Task 7: Web/API 安全配置人工复核
|
||||
|
||||
**Objective:** 对项目特有的安全边界做代码审阅,补足工具扫描无法覆盖的业务风险。
|
||||
|
||||
**Likely files to review:**
|
||||
- `server-rs/crates/api-server/src/**`
|
||||
- `server-rs/crates/platform-auth/src/**`
|
||||
- `server-rs/crates/platform-oss/src/**`
|
||||
- `server-rs/crates/platform-llm/src/**`
|
||||
- `server-rs/crates/spacetime-client/src/**`
|
||||
- `src/services/**`
|
||||
- `apps/admin-web/src/**`
|
||||
- `scripts/*deploy*`
|
||||
- `scripts/*api-server*`
|
||||
- `.github/workflows/**` if present
|
||||
|
||||
**Checklist:**
|
||||
- Auth / session:access token 与 refresh cookie 的生命周期、SameSite/Secure/HttpOnly、错误日志是否泄露 token。
|
||||
- CORS:开发环境与生产环境是否区分,是否存在生产 `Any`。
|
||||
- SSRF / outbound:LLM、图片生成、OSS、任意 URL 下载是否校验协议和大小。
|
||||
- Upload / Data URL:大小限制、MIME 校验、base64 解析错误处理。
|
||||
- Path traversal:脚本和后端是否拼接用户输入路径。
|
||||
- Admin:后台接口是否有权限校验,是否复用普通用户 token。
|
||||
- SpacetimeDB:private table / reducer 是否绕过 api-server facade 暴露敏感数据。
|
||||
- Logging:日志是否打印 API key、token、cookie、用户私密内容。
|
||||
|
||||
**Validation:**
|
||||
- 对每个命中的真实风险,记录具体文件路径和函数名。
|
||||
- 对“需要运行环境才能验证”的风险,列出 smoke 或单测建议。
|
||||
|
||||
### Task 8: 汇总漏洞分级与整改建议
|
||||
|
||||
**Objective:** 把扫描结果转成团队可执行的安全整改报告。
|
||||
|
||||
**Deliverable candidates:**
|
||||
- `docs/audits/SECURITY_VULNERABILITY_SCAN_YYYY-MM-DD.md`
|
||||
- 或如果用户只要临时报告:`.hermes/plans/assets/security-scan-YYYY-MM-DD.md`
|
||||
|
||||
**Report structure:**
|
||||
|
||||
```markdown
|
||||
# 安全漏洞扫描报告 YYYY-MM-DD
|
||||
|
||||
## 扫描范围
|
||||
## 扫描命令与环境
|
||||
## 摘要
|
||||
## Critical
|
||||
## High
|
||||
## Medium
|
||||
## Low
|
||||
## Informational / False Positive
|
||||
## 依赖升级建议
|
||||
## 代码修复建议
|
||||
## 需要人工确认的问题
|
||||
## 验证命令
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- 报告不包含真实密钥。
|
||||
- 每条问题都有“证据、影响、建议、验证”。
|
||||
- 明确哪些是工具扫描结果,哪些是人工判断。
|
||||
|
||||
### Task 9: 如用户要求修复,再分批执行最小修复
|
||||
|
||||
**Objective:** 避免一次性大规模升级导致回归,把修复拆为可验证的小批次。
|
||||
|
||||
**Suggested order:**
|
||||
1. Critical secrets:立即移除、轮换密钥、补 `.gitignore`/文档约束(注意项目约束:不要在 `.gitignore` 中添加 `.env.local`)。
|
||||
2. Critical/High direct dependencies:优先升级 direct dependency,运行最小测试。
|
||||
3. Critical/High transitive dependencies:评估是否由 direct dependency patch/minor 升级带出。
|
||||
4. 源码漏洞:按入口编写回归测试,再修复。
|
||||
5. Medium/Low:按风险和 breaking change 代价排期。
|
||||
|
||||
**Required verification after fixes:**
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
cd server-rs && cargo test --workspace
|
||||
```
|
||||
|
||||
后端 API 或 auth 修复涉及运行态时,还需要:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
# 另一个终端检查 /healthz 并执行对应 smoke
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- 修复后重新跑对应 audit / secret scan。
|
||||
- 走 `requesting-code-review` 的独立安全复核流程。
|
||||
|
||||
---
|
||||
|
||||
## Files likely to change(仅修复阶段)
|
||||
|
||||
本计划阶段不修改以下文件;只有用户确认执行修复时才可能变化:
|
||||
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `apps/admin-web/package.json`
|
||||
- `server-rs/Cargo.toml`
|
||||
- `server-rs/Cargo.lock`
|
||||
- `server-rs/crates/api-server/src/**`
|
||||
- `server-rs/crates/platform-auth/src/**`
|
||||
- `server-rs/crates/platform-oss/src/**`
|
||||
- `server-rs/crates/platform-llm/src/**`
|
||||
- `src/services/**`
|
||||
- `apps/admin-web/src/**`
|
||||
- `scripts/**`
|
||||
- `docs/audits/SECURITY_VULNERABILITY_SCAN_YYYY-MM-DD.md`
|
||||
- `.hermes/shared-memory/pitfalls.md`(仅当发现长期有效、会反复踩的安全排障经验时更新)
|
||||
|
||||
## Tests / Validation
|
||||
|
||||
安全扫描执行阶段:
|
||||
|
||||
```bash
|
||||
npm audit --json > /tmp/genarrative-npm-audit.json
|
||||
npm audit --audit-level=moderate
|
||||
cargo audit --manifest-path server-rs/Cargo.toml
|
||||
rg -n "\beval\(|new Function\(|dangerouslySetInnerHTML|innerHTML\s*=|document\.write\(" src apps scripts packages
|
||||
rg -n "exec\(|execSync\(|spawn\(|spawnSync\(|shell:\s*true|child_process" scripts src apps packages
|
||||
rg -n "Command::new|std::process|\.unwrap\(|\.expect\(|fs::|File::open|PathBuf|set_header|cors|CorsLayer" server-rs/crates
|
||||
```
|
||||
|
||||
修复执行阶段:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build
|
||||
cd server-rs && cargo test --workspace
|
||||
```
|
||||
|
||||
如变更后端运行态、安全中间件、auth/session:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
# 检查 /healthz
|
||||
# 执行相关 auth / API smoke
|
||||
```
|
||||
|
||||
## Risks, tradeoffs, and open questions
|
||||
|
||||
- `npm audit fix` 可能升级 major version,破坏 Vite/React/ESLint/Vitest 兼容性;必须先人工审查 `fixAvailable`。
|
||||
- `cargo audit` 可能需要安装 `cargo-audit`;安装工具属于用户环境变更,应先确认。
|
||||
- 密钥扫描极易产生 false positive;必须人工复核,报告中禁止输出真实密钥。
|
||||
- Rust `unwrap/expect` 不是天然漏洞;只有对外部输入、网络、文件、数据库响应等不可信数据造成 panic/DoS 时才升级为真实风险。
|
||||
- Web 安全检查需要区分开发环境和生产环境;开发 CORS 放宽不等于生产漏洞,但生产配置必须有明确边界。
|
||||
- 如果扫描发现历史提交中曾泄露密钥,删除当前文件不够,必须轮换密钥并考虑历史清理策略。
|
||||
- 当前计划未直接访问 CI/Jenkins/生产配置;若用户希望覆盖 CI/CD、镜像、部署主机和运行时端口,需要补充 Jenkins console、部署脚本和生产环境配置的只读访问方式。
|
||||
|
||||
## Missing artifacts / follow-up checkpoints
|
||||
|
||||
- 尚未获得用户确认是否允许安装 `cargo-audit` / `gitleaks` 等工具。
|
||||
- 尚未执行真实扫描,因此当前没有漏洞结论;执行后需要生成正式报告。
|
||||
- 如果用户希望“检查当前项目”包含远端仓库历史 secrets、Docker 镜像、Jenkins 凭据和生产运行时配置,需要另行确认访问范围和凭据边界。
|
||||
@@ -0,0 +1,206 @@
|
||||
# 远端作品列表压测排查报告
|
||||
|
||||
时间:2026-05-12 06:16 CST
|
||||
目标:`http://82.157.175.59`
|
||||
SSH:远端生产机 root 账号(具体私钥路径仅保留在本机环境,不写入仓库)
|
||||
|
||||
## 背景
|
||||
|
||||
远端 `k6-works-list.js` 压测中:
|
||||
|
||||
- smoke 通过。
|
||||
- baseline 10 VU:无 HTTP 错误,但 p95/p99 超阈值。
|
||||
- 50 RPS spike:`http_req_failed` / `works_list_shape_error_rate` 约 21.99%。
|
||||
- 100 RPS spike:`http_req_failed` / `works_list_shape_error_rate` 约 25.47%。
|
||||
- 从 k6 check 看,失败主要集中在 `puzzle_gallery_list`,`custom_world_gallery_list` 基本正常。
|
||||
|
||||
## 已完成排查
|
||||
|
||||
### 1. 服务器进程与资源
|
||||
|
||||
远端服务监听:
|
||||
|
||||
- Rust api-server:`127.0.0.1:8082`,systemd 服务 `genarrative-api.service`。
|
||||
- SpacetimeDB:`127.0.0.1:3101`,systemd 服务 `spacetimedb.service`。
|
||||
- Nginx:公网 80 反代 `/api/*` 到 `127.0.0.1:8082`。
|
||||
|
||||
服务器规格/状态:
|
||||
|
||||
- 2 vCPU。
|
||||
- 内存约 1.9GiB。
|
||||
- Swap 约 1.9GiB,已有约 600MiB 使用。
|
||||
- `/` 磁盘约 69%。
|
||||
- Rust api-server 当前 CPU 不高。
|
||||
- SpacetimeDB 当前 CPU 不高。
|
||||
|
||||
发现一个独立异常:
|
||||
|
||||
- PM2 下旧 `server-node` 进程 `genarrative` 正在重启风暴。
|
||||
- cwd:`/work/Genarrative/server-node`
|
||||
- 错误:连接 `127.0.0.1:5432` PostgreSQL 被拒绝。
|
||||
- PM2 restart 次数已超过 33 万。
|
||||
- 该进程不是当前公网 `/api/*` 使用的 Rust api-server,但会制造额外 CPU/内存/日志抖动。
|
||||
|
||||
### 2. 压测窗口服务端日志
|
||||
|
||||
子任务聚合了 2026-05-12 04:50-05:05 的 nginx 与 api-server 日志。
|
||||
|
||||
nginx access:
|
||||
|
||||
- `/api/runtime/puzzle/gallery`:4661 次,全部 200。
|
||||
- `/api/runtime/custom-world-gallery`:4659 次,全部 200。
|
||||
|
||||
api-server journal:
|
||||
|
||||
`/api/runtime/puzzle/gallery`:
|
||||
|
||||
- completed:4661
|
||||
- status:200 全部
|
||||
- slow_request:0
|
||||
- latency_ms:min 13 / p50 30 / p90 43 / p95 50 / p99 62 / max 88
|
||||
|
||||
`/api/runtime/custom-world-gallery`:
|
||||
|
||||
- completed:4659
|
||||
- status:200 全部
|
||||
- slow_request:0
|
||||
- latency_ms:min 0 / p50 1 / p90 5 / p95 7 / p99 13 / max 49
|
||||
|
||||
结论:
|
||||
|
||||
- 在服务端视角,两个接口在该窗口都没有 5xx,也没有慢请求。
|
||||
- 这与 k6 客户端侧 30s timeout / failed check 存在明显不一致。
|
||||
- 需要进一步区分:客户端侧网络/连接耗尽/本机 k6 执行环境问题,还是 k6 统计混合/响应解析问题。
|
||||
|
||||
### 3. k6 脚本行为
|
||||
|
||||
文件:`scripts/loadtest/k6-works-list.js`
|
||||
|
||||
无 `AUTH_TOKEN` 时,每轮 iteration 顺序请求两个接口:
|
||||
|
||||
1. `GET /api/runtime/puzzle/gallery`
|
||||
2. `GET /api/runtime/custom-world-gallery`
|
||||
|
||||
`DETAIL_RATIO=0` 时不会请求详情。
|
||||
|
||||
`works_list_shape_error_rate` 不只代表字段结构错误,只要下面任意 check 失败都会计入:
|
||||
|
||||
- status is 200
|
||||
- returns json object
|
||||
- has collection
|
||||
- list item shape
|
||||
|
||||
因此 timeout、非 JSON、非 200、响应结构不符合都会表现为 shape error。
|
||||
|
||||
数据文件实际路径:
|
||||
|
||||
- `scripts/loadtest/data/works-list.local.json`
|
||||
|
||||
脚本里 `data/works-list.local.json` 是相对 k6 脚本文件解析的,因此本身合理。
|
||||
|
||||
### 4. 代码层疑似瓶颈
|
||||
|
||||
虽然这次远端服务端日志没有复现慢请求,但代码层仍发现一个真实性能隐患。
|
||||
|
||||
`/api/runtime/puzzle/gallery` 调用链:
|
||||
|
||||
- `server-rs/crates/api-server/src/app.rs:1192`
|
||||
- `server-rs/crates/api-server/src/puzzle.rs:1385-1409`
|
||||
- `server-rs/crates/spacetime-client/src/puzzle.rs:367-381`
|
||||
- `server-rs/crates/spacetime-module/src/puzzle.rs:430-443`
|
||||
- `server-rs/crates/spacetime-module/src/puzzle.rs:1393-1404`
|
||||
|
||||
关键实现:
|
||||
|
||||
- `list_puzzle_gallery_tx` 对 `puzzle_work_profile().iter()` 全表扫描。
|
||||
- 再过滤 `publication_status == Published`。
|
||||
- 对每个公开作品调用 `build_puzzle_work_profile_from_row_with_recent_count`。
|
||||
- 该函数调用 `count_recent_public_work_plays(ctx, "puzzle", &row.profile_id, now_micros)`。
|
||||
|
||||
`count_recent_public_work_plays`:
|
||||
|
||||
- 文件:`server-rs/crates/spacetime-module/src/runtime/profile.rs:1296-1321`
|
||||
- 当前实现对 `public_work_play_daily_stat().iter()` 全表扫描过滤。
|
||||
- 但表定义已有复合索引:
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs:242-248`
|
||||
- `by_public_work_play_daily_stat_work_day(source_type, profile_id, played_day)`
|
||||
- 当前统计函数未使用该索引。
|
||||
|
||||
复杂度风险:
|
||||
|
||||
```text
|
||||
puzzle gallery ~= O(puzzle_work_profile 全表扫描 + Published作品数 * public_work_play_daily_stat 全表扫描)
|
||||
```
|
||||
|
||||
`custom-world-gallery` 与 puzzle 的差异:
|
||||
|
||||
- custom-world 使用 `CustomWorldGalleryEntry` 公开读模型表。
|
||||
- puzzle 直接从 `puzzle_work_profile` 即席拼装。
|
||||
- 两者都调用 recent count,但 puzzle 更容易受作品表规模和统计表规模影响。
|
||||
|
||||
## 当前判断
|
||||
|
||||
本次排查有两个层面的结论:
|
||||
|
||||
1. 生产服务端日志没有证明 `puzzle/gallery` 在 04:50-05:05 窗口真的 30s 慢或 5xx。
|
||||
- api-server 记录的 p95 只有 50ms。
|
||||
- nginx 看到两个接口都是 200。
|
||||
- 所以 k6 侧的 30s timeout 需要进一步从客户端网络、连接池、Windows/k6 执行环境、summary 混合统计角度验证。
|
||||
|
||||
2. 代码层确实存在可修的性能隐患。
|
||||
- `count_recent_public_work_plays` 未使用已有索引。
|
||||
- puzzle gallery 对每个作品重复做 recent count。
|
||||
- puzzle gallery 未使用 `publication_status` 索引或读模型。
|
||||
|
||||
## 建议下一步
|
||||
|
||||
### A. 先处理服务器 PM2 重启风暴
|
||||
|
||||
建议确认旧 Node 服务是否仍需要。
|
||||
|
||||
如果不需要,应停止并禁用 PM2 中的旧 `server-node`:
|
||||
|
||||
```bash
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 stop genarrative
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 delete genarrative
|
||||
PM2_HOME=/home/ubuntu/.pm2 pm2 save
|
||||
```
|
||||
|
||||
这是生产侧操作,执行前需要确认。
|
||||
|
||||
### B. 单接口短压验证客户端/服务端不一致
|
||||
|
||||
不要继续用混合脚本大压。
|
||||
|
||||
建议新增或临时使用单接口 k6 脚本,分别只测:
|
||||
|
||||
- `/api/runtime/puzzle/gallery`
|
||||
- `/api/runtime/custom-world-gallery`
|
||||
|
||||
并在同一时间窗口并行采集:
|
||||
|
||||
- k6 客户端 summary
|
||||
- nginx access 请求数/状态码
|
||||
- api-server journal latency
|
||||
- 本机到服务器网络错误/timeout
|
||||
|
||||
目标是确认 timeout 是不是发生在客户端侧连接/网络,而不是服务端处理慢。
|
||||
|
||||
### C. 修复代码性能隐患
|
||||
|
||||
优先级建议:
|
||||
|
||||
1. `count_recent_public_work_plays` 改为使用 `by_public_work_play_daily_stat_work_day` 复合索引,或至少改成批量统计,避免 N 次全表扫描。
|
||||
2. `list_puzzle_gallery_tx` 使用 `by_puzzle_work_publication_status` 索引查询 Published,或参考 custom-world 建立 `puzzle_gallery_entry` 公开读模型。
|
||||
3. gallery 列表页不要实时逐条扫描统计表,可维护读模型或批量聚合 `recent_play_count_7d`。
|
||||
|
||||
### D. 调整 k6 脚本输出
|
||||
|
||||
建议 k6 summary 按 endpoint tag 输出或新增单接口模式,否则 overall 指标会把 puzzle/custom-world 混在一起。
|
||||
|
||||
建议增加:
|
||||
|
||||
- `ENDPOINT=puzzle_gallery_list`
|
||||
- `ENDPOINT=custom_world_gallery_list`
|
||||
|
||||
让脚本只跑一个 endpoint,避免诊断时混淆。
|
||||
@@ -0,0 +1,343 @@
|
||||
# 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。
|
||||
BIN
.hermes/plans/assets/profile-feedback-reference-2026-05-08.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
.hermes/plans/frame_003.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
.hermes/plans/frame_010.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
.hermes/plans/frame_020.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
.hermes/plans/frame_035.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
46
.hermes/plugins/game-studio/.codex-plugin/plugin.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "game-studio",
|
||||
"version": "0.1.0",
|
||||
"description": "Design, prototype, and ship browser games with guided 2D and 3D workflows, asset pipelines, and playtesting support.",
|
||||
"author": {
|
||||
"name": "OpenAI",
|
||||
"email": "support@openai.com",
|
||||
"url": "https://openai.com/"
|
||||
},
|
||||
"homepage": "https://openai.com/",
|
||||
"repository": "https://github.com/openai/plugins",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"games",
|
||||
"phaser",
|
||||
"threejs",
|
||||
"react-three-fiber",
|
||||
"gltf",
|
||||
"rapier",
|
||||
"webgl",
|
||||
"sprites",
|
||||
"playtest"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "Game Studio",
|
||||
"shortDescription": "Design, prototype, and ship browser games",
|
||||
"longDescription": "Plan, prototype, and build browser games with guided workflows for gameplay systems, UI, asset pipelines, and playtesting across 2D and 3D projects.",
|
||||
"developerName": "OpenAI",
|
||||
"category": "Coding",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://openai.com/",
|
||||
"privacyPolicyURL": "https://openai.com/policies/privacy-policy/",
|
||||
"termsOfServiceURL": "https://openai.com/policies/terms-of-use/",
|
||||
"defaultPrompt": [
|
||||
"Design a browser game and plan the core loop"
|
||||
],
|
||||
"brandColor": "#0F766E",
|
||||
"composerIcon": "./assets/game-studio.svg",
|
||||
"logo": "./assets/app-icon.png",
|
||||
"screenshots": []
|
||||
}
|
||||
}
|
||||
38
.hermes/plugins/game-studio/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Hermes wrapper for the OpenAI Codex Game Studio plugin.
|
||||
|
||||
This plugin was imported from a Codex curated plugin cache. It exposes the
|
||||
plugin's bundled SKILL.md files as Hermes plugin skills using qualified names
|
||||
like `game-studio:phaser-2d-game`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _read_description(skill_md: Path) -> str:
|
||||
try:
|
||||
text = skill_md.read_text(encoding="utf-8")[:4000]
|
||||
except Exception:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
frontmatter = text[3:end]
|
||||
for line in frontmatter.splitlines():
|
||||
if line.strip().startswith("description:"):
|
||||
return line.split(":", 1)[1].strip().strip("\"'")
|
||||
return ""
|
||||
|
||||
|
||||
def register(ctx) -> None:
|
||||
root = Path(__file__).resolve().parent
|
||||
skills_root = root / "skills"
|
||||
if not skills_root.exists():
|
||||
return
|
||||
for skill_md in sorted(skills_root.glob("*/SKILL.md")):
|
||||
ctx.register_skill(
|
||||
name=skill_md.parent.name,
|
||||
path=skill_md,
|
||||
description=_read_description(skill_md),
|
||||
)
|
||||
BIN
.hermes/plugins/game-studio/assets/app-icon.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
25
.hermes/plugins/game-studio/assets/game-studio.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Game Studio</title>
|
||||
<desc id="desc">A stylized browser game plugin icon with a viewport frame, a d-pad, and layered tiles.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#134E4A"/>
|
||||
<stop offset="100%" stop-color="#0F766E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screen" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CCFBF1"/>
|
||||
<stop offset="100%" stop-color="#5EEAD4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="52" fill="url(#bg)"/>
|
||||
<rect x="40" y="46" width="176" height="124" rx="20" fill="#062F2D" stroke="#7DD3C7" stroke-width="8"/>
|
||||
<rect x="58" y="62" width="140" height="92" rx="12" fill="url(#screen)"/>
|
||||
<path d="M76 132h28l14-18 18 20 24-30 18 18v18H76z" fill="#0F766E" opacity="0.9"/>
|
||||
<rect x="62" y="178" width="50" height="50" rx="18" fill="#0B2F2D" stroke="#7DD3C7" stroke-width="6"/>
|
||||
<rect x="144" y="178" width="50" height="50" rx="18" fill="#0B2F2D" stroke="#7DD3C7" stroke-width="6"/>
|
||||
<rect x="81" y="189" width="12" height="28" rx="4" fill="#CCFBF1"/>
|
||||
<rect x="73" y="197" width="28" height="12" rx="4" fill="#CCFBF1"/>
|
||||
<circle cx="160" cy="203" r="8" fill="#FDE68A"/>
|
||||
<circle cx="178" cy="203" r="8" fill="#FCA5A5"/>
|
||||
<circle cx="169" cy="192" r="8" fill="#BFDBFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
6
.hermes/plugins/game-studio/plugin.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: game-studio
|
||||
version: 0.1.0
|
||||
description: Design, prototype, and ship browser games with guided 2D and 3D workflows,
|
||||
asset pipelines, and playtesting support.
|
||||
author: OpenAI
|
||||
kind: standalone
|
||||
@@ -0,0 +1,50 @@
|
||||
# Alternative 3D Engines
|
||||
|
||||
This plugin defaults to Three.js and React Three Fiber for code generation. Babylon.js and PlayCanvas still matter, but they are reference-only alternatives in the current plugin shape.
|
||||
|
||||
## Babylon.js
|
||||
|
||||
Useful sources:
|
||||
|
||||
- [Babylon.js home](https://babylonjs.com/)
|
||||
- [Engine specifications](https://www.babylonjs.com/specifications/)
|
||||
- [Babylon.js Editor](https://editor.babylonjs.com/)
|
||||
|
||||
Choose Babylon.js when:
|
||||
|
||||
- the user explicitly wants Babylon.js
|
||||
- the team wants a more engine-heavy stack with scene, material, viewer, and editor tooling built around one ecosystem
|
||||
- WebGPU, Havok, node-based rendering or material tooling, or Babylon-specific runtime features are part of the reason for the choice
|
||||
|
||||
What Babylon.js is good at:
|
||||
|
||||
- full-engine 3D workflows
|
||||
- strong built-in tooling and editor surfaces
|
||||
- WebGL and WebGPU support inside one ecosystem
|
||||
- integrated viewer and inspection-oriented workflows
|
||||
|
||||
## PlayCanvas
|
||||
|
||||
Useful sources:
|
||||
|
||||
- [PlayCanvas graphics overview](https://developer.playcanvas.com/user-manual/graphics/)
|
||||
- [Supported formats](https://developer.playcanvas.com/user-manual/assets/supported-formats/)
|
||||
- [PlayCanvas React GLTF API](https://developer.playcanvas.com/user-manual/react/api/gltf/)
|
||||
- [PlayCanvas Web Components](https://developer.playcanvas.com/user-manual/web-components/)
|
||||
|
||||
Choose PlayCanvas when:
|
||||
|
||||
- the user explicitly wants PlayCanvas
|
||||
- the team prefers an editor-centric browser engine workflow
|
||||
- GLB import, runtime tooling, React bindings, or web-component-based embedding are central to the project
|
||||
|
||||
What PlayCanvas is good at:
|
||||
|
||||
- editor and engine working together
|
||||
- GLB-centric browser asset workflows
|
||||
- strong web embedding patterns
|
||||
- WebGL and WebGPU support with browser-focused runtime tooling
|
||||
|
||||
## Default recommendation
|
||||
|
||||
If the user has not already chosen Babylon.js or PlayCanvas, prefer Three.js or React Three Fiber in this plugin because they give the best balance of portability, ecosystem depth, and predictable code generation across normal browser-game repos.
|
||||
53
.hermes/plugins/game-studio/references/engine-selection.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Engine Selection
|
||||
|
||||
Use this table to choose the default implementation path for browser games in this plugin.
|
||||
|
||||
## Default choices
|
||||
|
||||
- Choose Phaser for 2D games unless the user explicitly asks for another stack.
|
||||
- Choose vanilla Three.js for explicit 3D or WebGL-first experiences in plain TypeScript or Vite apps.
|
||||
- Choose React Three Fiber when the 3D scene is part of a React application and shared app state or declarative composition matters.
|
||||
- Choose raw WebGL only when engine abstractions are the problem, not because WebGL sounds more advanced.
|
||||
- Treat Babylon.js and PlayCanvas as alternative ecosystems, not the default path in this plugin.
|
||||
|
||||
## Phaser is the best fit when
|
||||
|
||||
- the game is sprite- or tile-based
|
||||
- the game is top-down, side-view, or grid tactics
|
||||
- you need camera, sprite, and scene primitives quickly
|
||||
- the UI will mostly live in DOM overlays
|
||||
- the game loop is gameplay-first rather than renderer-first
|
||||
|
||||
## Three.js is the best fit when
|
||||
|
||||
- the game is genuinely 3D
|
||||
- camera movement and depth are central to play
|
||||
- materials, lighting, or scene composition matter more than sprite tooling
|
||||
- the user explicitly asks for Three.js or WebGL-based 3D work
|
||||
- the team wants direct control over scene setup, loaders, physics integration, and the game loop
|
||||
|
||||
## React Three Fiber is the best fit when
|
||||
|
||||
- the project already lives in React
|
||||
- the 3D scene needs to share app state with the rest of the product
|
||||
- declarative scene composition is more valuable than a fully imperative loop
|
||||
- the team benefits from pmndrs tooling such as Drei, React Postprocessing, or `@react-three/rapier`
|
||||
|
||||
## Babylon.js or PlayCanvas are the best fit when
|
||||
|
||||
- the user explicitly asks for those engines
|
||||
- the team already has engine-specific tooling or editor workflows in those ecosystems
|
||||
- the project wants engine-heavy runtime features, editor-first workflows, or platform-specific tooling that Three.js does not provide by default
|
||||
|
||||
## Raw WebGL is the best fit when
|
||||
|
||||
- the project is shader-heavy
|
||||
- you need a custom renderer or post-processing pipeline
|
||||
- the user explicitly wants low-level rendering control
|
||||
|
||||
## Avoid these mismatches
|
||||
|
||||
- Do not choose a 3D stack for a normal 2D tactics or platformer game.
|
||||
- Do not choose raw WebGL for a game that mostly needs engine conveniences.
|
||||
- Do not force HUD and menus into canvas or WebGL when DOM would be clearer.
|
||||
- Do not default to Babylon.js or PlayCanvas when the user mainly wants portable TypeScript code generation across browser-game repos.
|
||||
97
.hermes/plugins/game-studio/references/frontend-prompts.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Frontend Prompts
|
||||
|
||||
Use these prompt shapes to keep browser-game UI intentional instead of generic.
|
||||
|
||||
## Prompt ingredients
|
||||
|
||||
- game genre and fantasy
|
||||
- camera or viewpoint
|
||||
- player verbs
|
||||
- HUD zones
|
||||
- menu surfaces
|
||||
- motion tone
|
||||
- desktop and mobile expectations
|
||||
- playfield protection and disclosure strategy
|
||||
- anti-patterns to avoid
|
||||
|
||||
## HUD implementation prompt
|
||||
|
||||
```text
|
||||
Design and implement the HUD for a browser game.
|
||||
|
||||
Game fantasy: <genre and world>
|
||||
Viewpoint: <top-down, side-view, tactical grid, third-person, first-person>
|
||||
Primary verbs: <attack, move, cast, build, dodge, inspect>
|
||||
HUD zones: <top status, bottom command bar, side objectives, modal panels>
|
||||
Tone: <ornate, rugged, clean sci-fi, painterly, arcade>
|
||||
Motion: <restrained, snappy, dramatic only on important state changes>
|
||||
Platforms: desktop and mobile
|
||||
Constraints: readable over active gameplay, DOM-based overlays, CSS variables, no generic dashboard look
|
||||
Playfield protection: keep the central play area clear during normal play, prefer one primary persistent HUD cluster, and move long-form notes or controls behind menus
|
||||
Avoid: flat admin UI, default font stack, cluttered overlays, constant micro-animation, equal-weight cards around every edge, broad always-on panels that cover the world
|
||||
```
|
||||
|
||||
## Menu implementation prompt
|
||||
|
||||
```text
|
||||
Build the shell UI for a browser game with the following surfaces:
|
||||
- title screen
|
||||
- pause menu
|
||||
- settings panel
|
||||
- game-over or victory screen
|
||||
|
||||
Keep the menus visually tied to the game world, not to a SaaS app aesthetic. Use strong hierarchy, intentional typography, meaningful motion, and responsive layout.
|
||||
```
|
||||
|
||||
## Low-chrome 3D starter prompt
|
||||
|
||||
```text
|
||||
Design the initial playable HUD for a browser 3D game.
|
||||
|
||||
Goal: the first screen should feel playable in under 3 seconds, not like a dashboard.
|
||||
Camera mode: <third-person, first-person, orbit, rail>
|
||||
Primary verbs: <move, inspect, interact, attack, build>
|
||||
Persistent UI budget:
|
||||
- one compact objective chip or status cluster
|
||||
- one optional small secondary surface
|
||||
- one transient controls or interaction hint
|
||||
|
||||
Interaction rules:
|
||||
- keep the center of the playfield clear
|
||||
- keep the lower-middle playfield mostly clear during normal play
|
||||
- lore, notes, quest details, and long control lists live behind a drawer, pause menu, or toggle
|
||||
- modal and pause states must gate camera input correctly
|
||||
|
||||
Avoid:
|
||||
- giant title cards over live gameplay
|
||||
- field notes, controls, and objectives all open at once
|
||||
- equally weighted glass panels in every corner
|
||||
- full-screen overlay chrome during normal movement
|
||||
```
|
||||
|
||||
## 3D overlay prompt
|
||||
|
||||
```text
|
||||
Design and implement the HUD and menu overlays for a browser 3D game.
|
||||
|
||||
Engine context: <vanilla Three.js or React Three Fiber>
|
||||
Camera mode: <third-person, first-person, orbit, rail>
|
||||
Primary verbs: <move, inspect, interact, attack, build>
|
||||
Overlay surfaces: <reticle, quest log, inventory, pause menu, settings>
|
||||
Interaction constraints:
|
||||
- DOM overlays, not in-scene UI by default
|
||||
- modal and menu states must suspend or gate camera input correctly
|
||||
- keyboard and pointer states must be explicit
|
||||
- reduced-motion support for non-essential transitions
|
||||
- keep the center of the screen clear during normal play
|
||||
- keep the lower-middle playfield mostly clear during normal play
|
||||
- start with one compact objective surface and transient hints rather than multiple permanent cards
|
||||
- secondary content such as notes, lore, and full control references should be collapsed by default
|
||||
Avoid:
|
||||
- dashboard UI
|
||||
- cluttered full-screen overlays
|
||||
- boxed panels around every edge of the viewport
|
||||
- full-width top-and-bottom panel stacks
|
||||
- permanent text-heavy cards competing with the scene
|
||||
- camera movement continuing under active menus
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# GLB Loading Starter
|
||||
|
||||
Use this as the canonical minimal pattern for loading shipped 3D content.
|
||||
|
||||
## Vanilla Three.js
|
||||
|
||||
```ts
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
||||
|
||||
const draco = new DRACOLoader();
|
||||
draco.setDecoderPath("/draco/");
|
||||
|
||||
const gltfLoader = new GLTFLoader();
|
||||
gltfLoader.setDRACOLoader(draco);
|
||||
|
||||
gltfLoader.load("/assets/hero.glb", (gltf) => {
|
||||
const root = gltf.scene;
|
||||
root.traverse((node) => {
|
||||
if ("castShadow" in node) {
|
||||
node.castShadow = true;
|
||||
node.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
scene.add(root);
|
||||
});
|
||||
```
|
||||
|
||||
## React Three Fiber
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
|
||||
function HeroModel() {
|
||||
const gltf = useGLTF("/assets/hero.glb");
|
||||
return <primitive object={gltf.scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Default shipping format is GLB or glTF 2.0.
|
||||
- Keep optimization upstream in the asset pipeline; loader code should stay boring.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Phaser Architecture
|
||||
|
||||
This is the default 2D structure for the plugin.
|
||||
|
||||
## Recommended module split
|
||||
|
||||
```text
|
||||
src/
|
||||
game/
|
||||
simulation/
|
||||
state.ts
|
||||
systems/
|
||||
rules/
|
||||
content/
|
||||
encounters/
|
||||
items/
|
||||
maps/
|
||||
input/
|
||||
actions.ts
|
||||
bindings.ts
|
||||
assets/
|
||||
manifest.ts
|
||||
phaser/
|
||||
boot/
|
||||
scenes/
|
||||
BootScene.ts
|
||||
MenuScene.ts
|
||||
BattleScene.ts
|
||||
view/
|
||||
sprites/
|
||||
fx/
|
||||
camera/
|
||||
adapters/
|
||||
sceneBridge.ts
|
||||
ui/
|
||||
hud/
|
||||
menus/
|
||||
overlays/
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- `simulation/`: source of truth for rules and saveable state
|
||||
- `content/`: authored data and encounter configuration
|
||||
- `input/`: action map and physical control bindings
|
||||
- `assets/`: stable manifest keys and asset metadata
|
||||
- `phaser/scenes/`: scene orchestration, not game rules
|
||||
- `phaser/view/`: render and effect helpers
|
||||
- `ui/`: DOM HUD, menus, and narrative panels
|
||||
|
||||
## Rules
|
||||
|
||||
- Phaser scenes read from and write to the simulation through a defined bridge.
|
||||
- Game state changes should not depend on sprite or tween lifetime.
|
||||
- Camera behavior should be isolated from combat or movement rules.
|
||||
- Use DOM for dense text and settings surfaces.
|
||||
51
.hermes/plugins/game-studio/references/playtest-checklist.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Playtest Checklist
|
||||
|
||||
Use this checklist for browser-game QA.
|
||||
|
||||
## Universal checks
|
||||
|
||||
- Does the game boot into a useful first state?
|
||||
- Are the main verbs obvious and responsive?
|
||||
- Does the HUD remain readable over gameplay?
|
||||
- Does the first playable screen prioritize play over dashboard chrome?
|
||||
- Does the central playfield stay mostly clear during normal play?
|
||||
- Do pause, failure, and recovery states work?
|
||||
- Does the game survive viewport changes?
|
||||
|
||||
## 2D checks
|
||||
|
||||
- sprite baseline consistency
|
||||
- hit, hurt, and attack timing
|
||||
- command menu focus and input state
|
||||
- tile or platform readability
|
||||
- particle or camera effects obscuring gameplay
|
||||
|
||||
## 3D checks
|
||||
|
||||
- camera control stability
|
||||
- camera and menu-state handoff
|
||||
- depth readability
|
||||
- persistent overlay weight versus scene readability
|
||||
- secondary notes, controls, and quest details collapsed by default
|
||||
- resize and aspect-ratio handling
|
||||
- renderer fallback or context-loss handling
|
||||
- material and lighting stability across states
|
||||
- GLB asset and texture streaming behavior
|
||||
- collision proxy alignment
|
||||
- GPU bottlenecks isolated with capture tools when needed
|
||||
|
||||
## Browser checks
|
||||
|
||||
- desktop and mobile viewports
|
||||
- input modality differences
|
||||
- reduced-motion behavior
|
||||
- pause behavior when focus changes
|
||||
- pointer-lock and camera-input release when overlays open
|
||||
- transient onboarding hints dismiss or fade once the player is moving
|
||||
|
||||
## Reporting
|
||||
|
||||
- Capture screenshots for visual findings.
|
||||
- Put findings in severity order.
|
||||
- Include reproduction steps.
|
||||
- Call out whether the likely owner is simulation, renderer, frontend, or asset pipeline.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Rapier Integration Starter
|
||||
|
||||
Use this as the smallest canonical pattern for adding physics without letting it take over the whole runtime.
|
||||
|
||||
## Vanilla Three.js
|
||||
|
||||
```ts
|
||||
import RAPIER from "@dimforge/rapier3d-compat";
|
||||
|
||||
await RAPIER.init();
|
||||
|
||||
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
|
||||
const body = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 2, 0));
|
||||
world.createCollider(RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5), body);
|
||||
|
||||
renderer.setAnimationLoop(() => {
|
||||
world.step();
|
||||
const p = body.translation();
|
||||
mesh.position.set(p.x, p.y, p.z);
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
```
|
||||
|
||||
## React Three Fiber
|
||||
|
||||
```tsx
|
||||
import { Physics, RigidBody } from "@react-three/rapier";
|
||||
|
||||
<Physics gravity={[0, -9.81, 0]}>
|
||||
<RigidBody colliders="cuboid">
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#3dd9b8" />
|
||||
</mesh>
|
||||
</RigidBody>
|
||||
</Physics>;
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep physics state synchronized through an explicit bridge.
|
||||
- Do not bury gameplay rules inside render or physics callbacks.
|
||||
@@ -0,0 +1,42 @@
|
||||
# React Three Fiber Stack
|
||||
|
||||
This is the default React-native 3D stack for the plugin.
|
||||
|
||||
## Primary components
|
||||
|
||||
- [React Three Fiber](https://r3f.docs.pmnd.rs/getting-started/introduction) for declarative Three.js rendering in React.
|
||||
- [Drei](https://drei.docs.pmnd.rs/controls/introduction) for controls, loaders, helpers, environments, and common scene utilities.
|
||||
- [React Postprocessing](https://react-postprocessing.docs.pmnd.rs/introduction) for effect composition in React-hosted scenes.
|
||||
- [React Three Rapier](https://pmndrs.github.io/react-three-rapier/) for physics integration.
|
||||
- [React Three A11y](https://a11y.docs.pmnd.rs/introduction) when scene interaction benefits from accessibility-aware patterns.
|
||||
- [glTF Transform](https://gltf-transform.dev/) and the [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) for shipped assets.
|
||||
|
||||
## Default stack choices
|
||||
|
||||
- Runtime: `@react-three/fiber` + `three`
|
||||
- Helper ecosystem: `@react-three/drei`
|
||||
- Physics: `@react-three/rapier`
|
||||
- Effects: `@react-three/postprocessing`
|
||||
- Accessibility: `@react-three/a11y` when appropriate
|
||||
- Assets: GLB or glTF 2.0
|
||||
|
||||
## Choose this stack when
|
||||
|
||||
- the 3D scene lives inside a React app
|
||||
- the UI shell, settings, or product flow already uses React
|
||||
- the team benefits from declarative scene composition
|
||||
- the scene must share app state with non-canvas UI
|
||||
|
||||
## Avoid this stack when
|
||||
|
||||
- the project wants a cleaner imperative loop with minimal React coordination
|
||||
- the whole game runtime would be easier to reason about in plain TypeScript
|
||||
|
||||
## Companion references
|
||||
|
||||
- `threejs-stack.md`
|
||||
- `react-three-fiber-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
- `web-3d-asset-pipeline.md`
|
||||
- `webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,51 @@
|
||||
# React Three Fiber Starter
|
||||
|
||||
Use this as the smallest canonical starting point for a React-hosted 3D scene.
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
src/
|
||||
App.tsx
|
||||
```
|
||||
|
||||
## `src/App.tsx`
|
||||
|
||||
```tsx
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<mesh rotation={[0.4, 0.6, 0]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#3dd9b8" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Canvas camera={{ position: [0, 1.5, 4], fov: 60 }}>
|
||||
<color attach="background" args={["#101418"]} />
|
||||
<ambientLight intensity={0.7} />
|
||||
<directionalLight position={[4, 6, 3]} intensity={1.2} />
|
||||
<Spinner />
|
||||
</Canvas>
|
||||
<div className="hud">
|
||||
<div className="objective-chip">Reach the lantern bridge.</div>
|
||||
<div className="hint-pill">WASD to move. Hold mouse to look.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Start here when the 3D scene lives inside an existing React app.
|
||||
- Keep the initial HUD sparse. One compact objective surface and one transient hint is usually enough for a first playable scaffold.
|
||||
- Put lore, notes, map, and settings behind drawers or modals instead of opening them all by default.
|
||||
- Add GLB loading with `gltf-loading-starter.md`.
|
||||
- Add physics with `rapier-integration-starter.md`.
|
||||
- Use `three-hud-layout-patterns.md` for low-chrome 3D overlay defaults.
|
||||
57
.hermes/plugins/game-studio/references/sprite-pipeline.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Sprite Pipeline
|
||||
|
||||
This is the default 2D animation workflow for the plugin.
|
||||
|
||||
## Principles
|
||||
|
||||
- Start from one approved in-game frame.
|
||||
- Generate the animation as one strip, not isolated frames.
|
||||
- Normalize the whole strip with one shared scale.
|
||||
- Use one shared anchor, typically bottom-center.
|
||||
- Preview before approving the asset.
|
||||
|
||||
## Why this works
|
||||
|
||||
- The approved seed frame preserves identity.
|
||||
- Strip-first generation reduces frame-to-frame drift.
|
||||
- Shared-scale normalization prevents one tall pose from making the character feel smaller.
|
||||
- Locking frame 01 back to the shipped sprite preserves continuity for idle-to-action transitions.
|
||||
|
||||
## Prompt template
|
||||
|
||||
```text
|
||||
Intended use: candidate production spritesheet for a 2D browser game animation review.
|
||||
Edit the provided transparent reference canvas into a single horizontal <N>-frame spritesheet.
|
||||
|
||||
The existing sprite in the leftmost slot is the anchor frame and must remain the same character:
|
||||
- same facing direction
|
||||
- same silhouette family
|
||||
- same palette family
|
||||
- same proportions
|
||||
- same readable face or key features
|
||||
- same outfit details
|
||||
|
||||
Composition:
|
||||
- transparent canvas
|
||||
- exactly one row of <N> equal frame slots
|
||||
- no extra characters
|
||||
- no labels
|
||||
- no scenery
|
||||
- no poster layout
|
||||
|
||||
Action:
|
||||
- describe the specific animation beat from frame 1 through frame N
|
||||
|
||||
Style:
|
||||
- authentic pixel-art production asset
|
||||
- crisp pixel clusters
|
||||
- restrained palette
|
||||
- not concept art
|
||||
```
|
||||
|
||||
## Normalization notes
|
||||
|
||||
- Use the union of detected sprite bounds per slot.
|
||||
- Compute one scale from the largest detected frame and anchor.
|
||||
- Bottom-align frames into the target canvas.
|
||||
- Reuse the exact shipped frame for frame 01 when `--lock-frame1` is appropriate.
|
||||
@@ -0,0 +1,61 @@
|
||||
# 3D HUD Layout Patterns
|
||||
|
||||
Use these defaults for initial 3D browser-game scaffolds. The first screen should be playable before it is informational.
|
||||
|
||||
## Layout Budget
|
||||
|
||||
- Keep the center of the screen clear during normal play.
|
||||
- On desktop, prefer one primary persistent cluster and one small secondary cluster.
|
||||
- On mobile, prefer one compact persistent cluster and transient prompts.
|
||||
- Secondary information belongs in drawers, toggles, pause menus, or contextual popovers.
|
||||
|
||||
## Good Default Patterns
|
||||
|
||||
### Objective chip
|
||||
|
||||
- One short objective in a compact top-corner chip.
|
||||
- One optional sublabel for location or mode.
|
||||
- No giant hero banner over the live scene.
|
||||
|
||||
### Contextual interaction prompt
|
||||
|
||||
- Bottom-center or lower-corner pill.
|
||||
- Appears only near interactables or during onboarding.
|
||||
- Dismisses after first use or fades once the player is moving confidently.
|
||||
|
||||
### Small status strip
|
||||
|
||||
- Health, energy, party count, or beacon progress in a narrow edge-aligned strip.
|
||||
- Use icons, short labels, and compact meters instead of stacked cards.
|
||||
|
||||
### Collapsible journal or quest log
|
||||
|
||||
- Closed by default.
|
||||
- Opened by a hotkey, button, or pause state.
|
||||
- Holds longer prose, lore, map notes, and multi-step objective details.
|
||||
|
||||
### Pause and settings modal
|
||||
|
||||
- Explicit modal state.
|
||||
- Suspends pointer-lock, drag-look, or camera input while active.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- four to six glass cards permanently framing the viewport
|
||||
- large lore or field-notes panels open during normal movement
|
||||
- controls lists permanently pinned to the screen
|
||||
- symmetric dashboard composition that competes with the scene
|
||||
- oversized title panels staying visible after the first second of play
|
||||
|
||||
## Example UI Budget
|
||||
|
||||
- top-left: objective chip
|
||||
- top-right: compact status strip
|
||||
- bottom-center: transient interaction or controls hint
|
||||
- pause menu or drawer: map, notes, inventory, settings
|
||||
|
||||
## Prompt Add-On
|
||||
|
||||
```text
|
||||
Default to a low-chrome playable HUD. Keep the central playfield clear. Use one compact objective chip, one small status surface, and transient prompts. Put lore, field notes, full controls, and long checklists behind a drawer or pause menu. Avoid equal-weight boxed panels in every corner.
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Three WebGL Architecture
|
||||
|
||||
This is the default 3D structure for the plugin.
|
||||
|
||||
## Recommended module split
|
||||
|
||||
```text
|
||||
src/
|
||||
game/
|
||||
simulation/
|
||||
content/
|
||||
input/
|
||||
save/
|
||||
render/
|
||||
app/
|
||||
createRenderer.ts
|
||||
createScene.ts
|
||||
createCamera.ts
|
||||
createLoop.ts
|
||||
loaders/
|
||||
loadGltf.ts
|
||||
loadEnvironment.ts
|
||||
loadTextures.ts
|
||||
objects/
|
||||
materials/
|
||||
lights/
|
||||
post/
|
||||
adapters/
|
||||
renderBridge.ts
|
||||
physics/
|
||||
world.ts
|
||||
colliders.ts
|
||||
sync.ts
|
||||
diagnostics/
|
||||
debugFlags.ts
|
||||
perf.ts
|
||||
ui/
|
||||
hud/
|
||||
menus/
|
||||
overlays/
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- `simulation/`: rules, state, AI, progression, save data
|
||||
- `render/app/`: renderer, scene, camera, resize, context lifecycle
|
||||
- `render/loaders/`: GLTF, compression, texture, and environment loading
|
||||
- `render/objects/`: scene graph construction and disposal
|
||||
- `render/materials/`: material setup and shader boundaries
|
||||
- `physics/`: Rapier world and simulation bridge
|
||||
- `diagnostics/`: performance probes and GPU debugging hooks
|
||||
- `ui/`: DOM HUD and menus
|
||||
|
||||
## Rules
|
||||
|
||||
- Scene graph objects are not the source of truth for game rules.
|
||||
- Keep camera logic explicit and testable.
|
||||
- Handle resize and context-loss as real browser concerns.
|
||||
- Keep high-density UI in DOM even when the world is fully 3D.
|
||||
- Treat GLB or glTF 2.0 as the default content format.
|
||||
- Add physics and diagnostics as real subsystems, not temporary one-off utilities.
|
||||
41
.hermes/plugins/game-studio/references/threejs-stack.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Three.js Stack
|
||||
|
||||
This is the default non-React 3D runtime stack for the plugin.
|
||||
|
||||
## Primary components
|
||||
|
||||
- [Three.js documentation](https://threejs.org/docs/) for the core renderer, scene graph, materials, cameras, loaders, and examples.
|
||||
- [Rapier JavaScript guide](https://rapier.rs/docs/user_guides/javascript/getting_started_js/) for physics integration.
|
||||
- [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) for the default shipping asset format.
|
||||
- [glTF Transform](https://gltf-transform.dev/) for optimization, packaging, and compression workflows.
|
||||
- [SpectorJS](https://spector.babylonjs.com/) for WebGL frame capture and GPU debugging.
|
||||
|
||||
## Default stack choices
|
||||
|
||||
- Runtime: `three`
|
||||
- Tooling: TypeScript + Vite
|
||||
- Assets: GLB or glTF 2.0
|
||||
- Loaders: `GLTFLoader`, `DRACOLoader`, `KTX2Loader` when the asset pipeline requires them
|
||||
- Physics: Rapier JS
|
||||
- UI: DOM overlays, not in-scene UI by default
|
||||
|
||||
## Choose this stack when
|
||||
|
||||
- the project is not React-first
|
||||
- the team wants direct control over the render loop
|
||||
- scene composition, loader setup, or custom render behavior needs imperative structure
|
||||
- the game code should feel engine-like without a React abstraction layer
|
||||
|
||||
## Avoid this stack when
|
||||
|
||||
- the surrounding app is already React-heavy and wants shared declarative state
|
||||
- the project needs an editor-first engine workflow more than a portable TypeScript runtime
|
||||
|
||||
## Companion references
|
||||
|
||||
- `three-webgl-architecture.md`
|
||||
- `threejs-vanilla-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
- `web-3d-asset-pipeline.md`
|
||||
- `webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,58 @@
|
||||
# Three.js Vanilla Starter
|
||||
|
||||
Use this as the smallest canonical starting point for a plain TypeScript or Vite Three.js app.
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
src/
|
||||
main.ts
|
||||
```
|
||||
|
||||
## `src/main.ts`
|
||||
|
||||
```ts
|
||||
import * as THREE from "three";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color("#101418");
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
|
||||
camera.position.set(0, 1.5, 4);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
|
||||
const light = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
light.position.set(4, 6, 3);
|
||||
scene.add(light);
|
||||
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshStandardMaterial({ color: "#3dd9b8" }),
|
||||
);
|
||||
scene.add(mesh);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
renderer.setAnimationLoop(() => {
|
||||
mesh.rotation.y += 0.01;
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Start here for direct loop and renderer control.
|
||||
- If the scaffold needs UI, start with one compact objective chip and one transient controls hint rather than multiple permanent cards.
|
||||
- Keep notes, codex, maps, and settings behind on-demand surfaces. The starter scene should stay readable while moving the camera.
|
||||
- Add GLB loading with `gltf-loading-starter.md`.
|
||||
- Add physics sync with `rapier-integration-starter.md`.
|
||||
- Use `three-hud-layout-patterns.md` for low-chrome 3D overlay defaults.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Web 3D Asset Pipeline
|
||||
|
||||
This is the default 3D asset shipping guidance for the plugin.
|
||||
|
||||
## Primary sources
|
||||
|
||||
- [Blender glTF exporter manual](https://docs.blender.org/manual/en/latest/addons/import_export/scene_gltf2.html)
|
||||
- [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html)
|
||||
- [glTF Transform](https://gltf-transform.dev/)
|
||||
- [PlayCanvas supported formats](https://developer.playcanvas.com/user-manual/assets/supported-formats/) for a good reference on why GLB is the recommended runtime format in browser engines
|
||||
|
||||
## Default output
|
||||
|
||||
- Ship GLB when possible.
|
||||
- Use `.gltf` with external files only when the asset pipeline or delivery strategy genuinely needs that shape.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Clean the source asset in the DCC tool.
|
||||
2. Export to GLB or glTF 2.0.
|
||||
3. Run glTF Transform for validation, pruning, deduplication, and size reduction.
|
||||
4. Apply the chosen geometry and texture compression strategy.
|
||||
5. Verify pivots, scale, collision assumptions, and hierarchy naming.
|
||||
6. Test the asset in the runtime before treating it as final.
|
||||
|
||||
## Compression and optimization
|
||||
|
||||
- Use Draco or Meshopt deliberately, not both by default.
|
||||
- Use KTX2 or BasisU when the runtime stack supports GPU-friendly compressed textures.
|
||||
- Keep texture resolution aligned with actual on-screen use.
|
||||
- Reuse materials and avoid unnecessary texture uniqueness.
|
||||
|
||||
## Runtime checks
|
||||
|
||||
- scale is consistent across assets
|
||||
- pivots match gameplay expectations
|
||||
- node names are stable
|
||||
- collision proxy needs are handled
|
||||
- animation clips and variants load correctly
|
||||
- memory and load time are reasonable for the scene
|
||||
|
||||
## Starter patterns
|
||||
|
||||
- `threejs-vanilla-starter.md`
|
||||
- `react-three-fiber-starter.md`
|
||||
- `gltf-loading-starter.md`
|
||||
- `rapier-integration-starter.md`
|
||||
@@ -0,0 +1,36 @@
|
||||
# WebGL Debugging and Performance
|
||||
|
||||
Use this reference when a browser 3D scene is visually wrong, unstable, or slower than expected.
|
||||
|
||||
## Primary tools
|
||||
|
||||
- [SpectorJS](https://spector.babylonjs.com/) for frame capture, pipeline inspection, draw-call review, and shader debugging.
|
||||
- Browser performance tooling for main-thread work, asset decode stalls, and memory pressure.
|
||||
- Engine-native debug views and stats surfaces where available.
|
||||
|
||||
## What to inspect first
|
||||
|
||||
- draw-call count
|
||||
- shader compilation churn
|
||||
- texture memory pressure
|
||||
- geometry count and material count
|
||||
- post-processing cost
|
||||
- asset decode and streaming stalls
|
||||
- WebGL context loss or fallback behavior
|
||||
|
||||
## Common causes of poor performance
|
||||
|
||||
- too many unique materials
|
||||
- oversized textures
|
||||
- heavy GLB assets loaded without optimization
|
||||
- complex post-processing on top of an already expensive scene
|
||||
- physics and render state fighting for ownership
|
||||
- React and scene state updating each other too frequently in React-hosted 3D apps
|
||||
|
||||
## Debugging rules
|
||||
|
||||
- Capture first, then guess.
|
||||
- Reduce the scene until the perf cliff becomes obvious.
|
||||
- Disable post-processing before rewriting core scene code.
|
||||
- Verify the asset pipeline before blaming the renderer.
|
||||
- Treat context-loss handling as a browser requirement, not an edge case.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a transparent edit canvas around a shipped seed sprite frame."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Upscale a seed sprite with nearest-neighbor sampling and place it into "
|
||||
"the leftmost slot of a larger transparent edit canvas."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--seed", required=True, help="Path to the approved seed frame.")
|
||||
parser.add_argument("--out", required=True, help="Path to the output PNG.")
|
||||
parser.add_argument(
|
||||
"--frames",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of horizontal frame slots to reserve. Default: 4.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slot-size",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Size of each square frame slot in pixels. Default: 256.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--canvas-size",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Size of the square transparent canvas in pixels. Default: 1024.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resize_seed(seed: Image.Image, slot_size: int) -> Image.Image:
|
||||
max_dim = max(seed.size)
|
||||
scale = slot_size / max_dim
|
||||
if scale >= 1:
|
||||
scale = max(1, int(scale))
|
||||
width = max(1, int(round(seed.width * scale)))
|
||||
height = max(1, int(round(seed.height * scale)))
|
||||
return seed.resize((width, height), Image.Resampling.NEAREST)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.frames < 1:
|
||||
raise SystemExit("--frames must be at least 1.")
|
||||
if args.slot_size < 1 or args.canvas_size < 1:
|
||||
raise SystemExit("--slot-size and --canvas-size must be positive.")
|
||||
|
||||
strip_width = args.frames * args.slot_size
|
||||
if strip_width > args.canvas_size or args.slot_size > args.canvas_size:
|
||||
raise SystemExit("Frame slots do not fit inside the requested canvas size.")
|
||||
|
||||
seed = Image.open(args.seed).convert("RGBA")
|
||||
seed = resize_seed(seed, args.slot_size)
|
||||
|
||||
canvas = Image.new("RGBA", (args.canvas_size, args.canvas_size), (0, 0, 0, 0))
|
||||
strip_left = (args.canvas_size - strip_width) // 2
|
||||
strip_top = (args.canvas_size - args.slot_size) // 2
|
||||
slot_left = strip_left
|
||||
slot_top = strip_top
|
||||
paste_x = slot_left + (args.slot_size - seed.width) // 2
|
||||
paste_y = slot_top + (args.slot_size - seed.height) // 2
|
||||
canvas.alpha_composite(seed, (paste_x, paste_y))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
152
.hermes/plugins/game-studio/scripts/normalize_sprite_strip.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Normalize a raw animation strip into fixed-size transparent frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Extract one horizontal strip into fixed-size frames using a shared "
|
||||
"global scale and bottom-center alignment."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--input", required=True, help="Path to the raw strip image.")
|
||||
parser.add_argument("--out-dir", required=True, help="Output directory for frames.")
|
||||
parser.add_argument(
|
||||
"--frames",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Number of horizontal frames in the strip.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frame-size",
|
||||
type=int,
|
||||
default=64,
|
||||
help="Output square frame size in pixels. Default: 64.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--anchor",
|
||||
help="Optional anchor frame used to stabilize global scale and frame 01 output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lock-frame1",
|
||||
action="store_true",
|
||||
help="Replace frame 01 with the provided anchor frame after normalization.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alpha-threshold",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Pixels with alpha above this threshold count as sprite content. Default: 8.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def threshold_bbox(image: Image.Image, alpha_threshold: int) -> tuple[int, int, int, int] | None:
|
||||
alpha = image.getchannel("A").point(lambda value: 255 if value > alpha_threshold else 0)
|
||||
return alpha.getbbox()
|
||||
|
||||
|
||||
def crop_to_content(image: Image.Image, alpha_threshold: int) -> Image.Image | None:
|
||||
bbox = threshold_bbox(image, alpha_threshold)
|
||||
if bbox is None:
|
||||
return None
|
||||
return image.crop(bbox)
|
||||
|
||||
|
||||
def split_strip(strip: Image.Image, frames: int) -> list[Image.Image]:
|
||||
if frames < 1:
|
||||
raise ValueError("frames must be at least 1")
|
||||
step = strip.width / frames
|
||||
slots: list[Image.Image] = []
|
||||
for index in range(frames):
|
||||
left = int(round(index * step))
|
||||
right = int(round((index + 1) * step))
|
||||
slots.append(strip.crop((left, 0, right, strip.height)))
|
||||
return slots
|
||||
|
||||
|
||||
def max_content_size(images: Iterable[Image.Image | None]) -> tuple[int, int]:
|
||||
widths: list[int] = []
|
||||
heights: list[int] = []
|
||||
for image in images:
|
||||
if image is None:
|
||||
continue
|
||||
widths.append(image.width)
|
||||
heights.append(image.height)
|
||||
if not widths or not heights:
|
||||
raise SystemExit("No sprite content was detected in the provided strip.")
|
||||
return max(widths), max(heights)
|
||||
|
||||
|
||||
def compose_frame(
|
||||
image: Image.Image | None,
|
||||
frame_size: int,
|
||||
scale: float,
|
||||
) -> Image.Image:
|
||||
canvas = Image.new("RGBA", (frame_size, frame_size), (0, 0, 0, 0))
|
||||
if image is None:
|
||||
return canvas
|
||||
|
||||
width = max(1, int(round(image.width * scale)))
|
||||
height = max(1, int(round(image.height * scale)))
|
||||
resized = image.resize((width, height), Image.Resampling.NEAREST)
|
||||
offset_x = (frame_size - width) // 2
|
||||
offset_y = frame_size - height
|
||||
canvas.alpha_composite(resized, (offset_x, offset_y))
|
||||
return canvas
|
||||
|
||||
|
||||
def load_anchor(path: str | None, alpha_threshold: int) -> tuple[Image.Image | None, Image.Image | None]:
|
||||
if path is None:
|
||||
return None, None
|
||||
anchor = Image.open(path).convert("RGBA")
|
||||
cropped = crop_to_content(anchor, alpha_threshold)
|
||||
return anchor, cropped
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.frames < 1:
|
||||
raise SystemExit("--frames must be at least 1.")
|
||||
if args.frame_size < 1:
|
||||
raise SystemExit("--frame-size must be positive.")
|
||||
if args.lock_frame1 and not args.anchor:
|
||||
raise SystemExit("--lock-frame1 requires --anchor.")
|
||||
|
||||
strip = Image.open(args.input).convert("RGBA")
|
||||
slots = split_strip(strip, args.frames)
|
||||
contents = [crop_to_content(slot, args.alpha_threshold) for slot in slots]
|
||||
anchor_image, anchor_content = load_anchor(args.anchor, args.alpha_threshold)
|
||||
max_width, max_height = max_content_size([*contents, anchor_content])
|
||||
scale = min(args.frame_size / max_width, args.frame_size / max_height)
|
||||
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for index, content in enumerate(contents, start=1):
|
||||
if index == 1 and args.lock_frame1:
|
||||
assert anchor_image is not None
|
||||
if anchor_image.width == args.frame_size and anchor_image.height == args.frame_size:
|
||||
frame = anchor_image
|
||||
else:
|
||||
frame = compose_frame(anchor_content, args.frame_size, scale)
|
||||
else:
|
||||
frame = compose_frame(content, args.frame_size, scale)
|
||||
frame.save(out_dir / f"{index:02d}.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a simple contact sheet from a directory of normalized sprite frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"Pillow is required. Install it with `python3 -m pip install pillow`."
|
||||
) from exc
|
||||
|
||||
|
||||
NUMBER_RE = re.compile(r"(\d+)")
|
||||
|
||||
|
||||
def natural_key(path: Path) -> list[int | str]:
|
||||
parts: list[int | str] = []
|
||||
for chunk in NUMBER_RE.split(path.stem):
|
||||
if not chunk:
|
||||
continue
|
||||
if chunk.isdigit():
|
||||
parts.append(int(chunk))
|
||||
else:
|
||||
parts.append(chunk)
|
||||
parts.append(path.suffix)
|
||||
return parts
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a preview contact sheet from a directory of sprite frames."
|
||||
)
|
||||
parser.add_argument("--frames-dir", required=True, help="Directory containing PNG frames.")
|
||||
parser.add_argument("--out", required=True, help="Output PNG path.")
|
||||
parser.add_argument(
|
||||
"--columns",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of columns in the preview sheet. Default: 4.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gap",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Gap between frames in pixels. Default: 8.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def paint_checkerboard(image: Image.Image, tile: int = 16) -> None:
|
||||
draw = ImageDraw.Draw(image)
|
||||
colors = ((240, 243, 246, 255), (225, 230, 235, 255))
|
||||
for top in range(0, image.height, tile):
|
||||
for left in range(0, image.width, tile):
|
||||
color = colors[((left // tile) + (top // tile)) % 2]
|
||||
draw.rectangle((left, top, left + tile, top + tile), fill=color)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.columns < 1:
|
||||
raise SystemExit("--columns must be at least 1.")
|
||||
if args.gap < 0:
|
||||
raise SystemExit("--gap cannot be negative.")
|
||||
|
||||
frame_dir = Path(args.frames_dir)
|
||||
frames = sorted(frame_dir.glob("*.png"), key=natural_key)
|
||||
if not frames:
|
||||
raise SystemExit("No PNG frames were found in --frames-dir.")
|
||||
|
||||
images = [Image.open(path).convert("RGBA") for path in frames]
|
||||
frame_width = max(image.width for image in images)
|
||||
frame_height = max(image.height for image in images)
|
||||
rows = math.ceil(len(images) / args.columns)
|
||||
sheet_width = args.columns * frame_width + max(0, args.columns - 1) * args.gap
|
||||
sheet_height = rows * frame_height + max(0, rows - 1) * args.gap
|
||||
sheet = Image.new("RGBA", (sheet_width, sheet_height), (255, 255, 255, 255))
|
||||
paint_checkerboard(sheet)
|
||||
|
||||
for index, image in enumerate(images):
|
||||
row = index // args.columns
|
||||
column = index % args.columns
|
||||
left = column * (frame_width + args.gap) + (frame_width - image.width) // 2
|
||||
top = row * (frame_height + args.gap) + (frame_height - image.height) // 2
|
||||
sheet.alpha_composite(image, (left, top))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
76
.hermes/plugins/game-studio/skills/game-playtest/SKILL.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: game-playtest
|
||||
description: Run browser-game playtests and frontend QA. Use when the user asks for smoke tests, screenshot-based verification, browser automation, HUD or overlay review, or structured issue-finding in a browser game.
|
||||
---
|
||||
|
||||
# Game Playtest
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to test browser games the way players experience them: through boot, input, scene transitions, HUD readability, and visual state changes. Prefer browser automation and screenshot review when the project supports it.
|
||||
|
||||
## Preferred Workflow
|
||||
|
||||
1. Boot the game and confirm the first actionable screen.
|
||||
2. Exercise the main verbs.
|
||||
3. Capture screenshots from representative states.
|
||||
4. Check the UI layer independently from the render layer.
|
||||
5. Report findings in severity order with reproduction steps.
|
||||
|
||||
## Tooling Guidance
|
||||
|
||||
- Prefer Playwright or equivalent browser automation already available in the repo.
|
||||
- When the game is canvas or WebGL heavy, screenshots are mandatory because DOM assertions alone miss visual regressions.
|
||||
- Use screenshots to judge playfield obstruction and HUD weight, not just correctness of text or layout.
|
||||
- When deterministic automation is not practical, do a structured manual pass and capture evidence.
|
||||
- For 3D rendering bugs or unexplained frame cost, use SpectorJS and browser performance tooling rather than guessing from code alone.
|
||||
|
||||
## Common Checks
|
||||
|
||||
### 2D checks
|
||||
|
||||
- sprite alignment and baseline consistency
|
||||
- hit or hurt animation readability
|
||||
- HUD overlap with the playfield
|
||||
- command menu state changes
|
||||
- tile or platform readability
|
||||
- input-state feedback and turn-state clarity
|
||||
|
||||
### 3D checks
|
||||
|
||||
- first-load playability versus dashboard-like chrome
|
||||
- persistent overlay weight versus playfield visibility
|
||||
- camera control and camera reset behavior
|
||||
- pointer-lock or drag-look transitions when menus and overlays open
|
||||
- depth readability and silhouette clarity
|
||||
- secondary panels collapsed or dismissible during normal play
|
||||
- resize behavior
|
||||
- WebGL context loss or renderer fallback behavior
|
||||
- material or lighting regressions
|
||||
- GLB or texture streaming stalls
|
||||
- collision proxy or physics mismatch
|
||||
- performance cliffs tied to post-processing or asset load
|
||||
|
||||
## Responsive and Browser Checks
|
||||
|
||||
- desktop and mobile viewport sanity
|
||||
- safe-area and notch issues where relevant
|
||||
- reduced-motion behavior for UI transitions
|
||||
- keyboard, pointer, and pause-state handling
|
||||
- React state and scene state synchronization when the project uses React Three Fiber
|
||||
|
||||
## Reporting Standard
|
||||
|
||||
Lead with findings. Keep each finding concrete:
|
||||
|
||||
- what the user sees
|
||||
- how to reproduce it
|
||||
- why it matters
|
||||
- what likely subsystem owns it
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend review cues: `../game-ui-frontend/SKILL.md`
|
||||
- 3D debugging notes: `../../references/webgl-debugging-and-performance.md`
|
||||
- Full checklist: `../../references/playtest-checklist.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game Playtest"
|
||||
short_description: "Run browser-game playtests and QA"
|
||||
default_prompt: "Playtest the browser game, check core interactions and visual state changes, and report concrete issues."
|
||||
94
.hermes/plugins/game-studio/skills/game-studio/SKILL.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: game-studio
|
||||
description: Route early browser-game work. Use when the user needs stack selection and workflow planning across design, implementation, assets, and playtesting before moving to a specialist skill.
|
||||
---
|
||||
|
||||
# Game Studio
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill as the umbrella entrypoint for browser-game work. Default to a 2D Phaser path unless the user explicitly asks for 3D, Three.js, React Three Fiber, shader-heavy rendering, or another WebGL-first direction.
|
||||
|
||||
This plugin is intentionally asymmetric:
|
||||
|
||||
- 2D is the strongest execution path in v1.
|
||||
- 3D has one opinionated default ecosystem: vanilla Three.js for plain TypeScript or Vite apps, React Three Fiber for React-hosted 3D apps, and GLB or glTF 2.0 as the default shipping asset format.
|
||||
- Shared architecture, UI, and playtest practices apply to both.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the user is still choosing a stack
|
||||
- the request spans multiple domains such as runtime, UI, asset pipeline, and QA
|
||||
- the user says "help me build a game" without naming the implementation path
|
||||
|
||||
## Do Not Stay Here When
|
||||
|
||||
- the runtime is clearly plain Three.js
|
||||
- the runtime is clearly React Three Fiber
|
||||
- the task is clearly a shipped-asset problem
|
||||
- the task is clearly frontend-only or QA-only
|
||||
|
||||
Once the intent is clear, route to the most specific specialist skill and continue from there.
|
||||
|
||||
## Routing Rules
|
||||
|
||||
1. Classify the request before designing or coding:
|
||||
- `2D default`: Phaser, sprites, tilemaps, top-down, side-view, grid tactics, action platformers.
|
||||
- `3D + plain TS/Vite`: imperative scene control, engine-like loops, non-React apps, direct Three.js work.
|
||||
- `3D + React`: React-hosted product surfaces, declarative scene composition, shared React state, UI-heavy 3D apps.
|
||||
- `3D asset pipeline`: GLB, glTF, texture packaging, compression, LOD, runtime asset size.
|
||||
- `Alternative engine`: Babylon.js or PlayCanvas requests, usually as comparison or ecosystem fit questions.
|
||||
- `Shared`: core loop design, frontend direction, save/debug/perf boundaries, browser QA.
|
||||
2. Route to the specialist skills immediately after classification:
|
||||
- Shared architecture and engine choice: `../web-game-foundations/SKILL.md`
|
||||
- Deep 2D implementation: `../phaser-2d-game/SKILL.md`
|
||||
- Vanilla Three.js implementation: `../three-webgl-game/SKILL.md`
|
||||
- React-hosted 3D implementation: `../react-three-fiber-game/SKILL.md`
|
||||
- 3D asset shipping and optimization: `../web-3d-asset-pipeline/SKILL.md`
|
||||
- HUD and menu direction: `../game-ui-frontend/SKILL.md`
|
||||
- 2D sprite generation and normalization: `../sprite-pipeline/SKILL.md`
|
||||
- Browser QA and visual review: `../game-playtest/SKILL.md`
|
||||
3. Keep one coherent plan across the routed skills. Do not let engine, UI, asset, and QA decisions drift apart.
|
||||
|
||||
## Default Workflow
|
||||
|
||||
1. Lock the game fantasy and player verbs.
|
||||
2. Define the core loop, failure states, progression, and target play session length.
|
||||
3. Choose the implementation track:
|
||||
- Default to Phaser for 2D browser games.
|
||||
- Choose vanilla Three.js when the project is explicitly 3D and wants direct render-loop control in a plain TypeScript or Vite app.
|
||||
- Choose React Three Fiber when the project already lives in React or wants declarative scene composition with shared React state.
|
||||
- Choose raw WebGL only when the user explicitly wants a custom renderer or shader-first surface.
|
||||
4. Define the UI surface early. Browser games usually need a DOM HUD and menu layer even when the playfield is canvas or WebGL.
|
||||
- For 3D starter scaffolds, default to a low-chrome HUD that preserves the playfield and keeps secondary panels collapsed.
|
||||
5. Decide the asset workflow:
|
||||
- 2D characters and effects: use `sprite-pipeline`.
|
||||
- 3D models, textures, and shipping format: use `web-3d-asset-pipeline`.
|
||||
6. Close with a playtest loop before calling the work production-ready.
|
||||
|
||||
## Output Expectations
|
||||
|
||||
- For planning requests, return a game-specific plan with stack choice, gameplay loop, UI surface, asset workflow, and test approach.
|
||||
- For implementation requests, keep the chosen stack obvious in the file structure and code boundaries.
|
||||
- For mixed requests, preserve the plugin default: 2D Phaser first unless the user asks for something else.
|
||||
- When the user asks about Babylon.js or PlayCanvas, compare them honestly but keep Three.js and R3F as the primary code-generation defaults unless the user explicitly chooses another engine.
|
||||
|
||||
## References
|
||||
|
||||
- Engine selection: `../../references/engine-selection.md`
|
||||
- Three.js stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- Vanilla Three.js starter: `../../references/threejs-vanilla-starter.md`
|
||||
- React Three Fiber starter: `../../references/react-three-fiber-starter.md`
|
||||
- Frontend prompting patterns: `../../references/frontend-prompts.md`
|
||||
- Playtest checklist: `../../references/playtest-checklist.md`
|
||||
|
||||
## Examples
|
||||
|
||||
- "Help me prototype a browser tactics game."
|
||||
- "I need a Phaser-based action game loop with a HUD and menus."
|
||||
- "I want a Three.js exploration demo with WebGL lighting and browser-safe UI."
|
||||
- "I want a React-based 3D configurator with React Three Fiber."
|
||||
- "Optimize my GLB assets for the web and keep the file sizes under control."
|
||||
- "Set up the asset workflow for consistent 2D sprite animations."
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game Studio"
|
||||
short_description: "Route browser-game work to the right path"
|
||||
default_prompt: "Help me choose the right browser-game stack and workflow before implementation starts."
|
||||
112
.hermes/plugins/game-studio/skills/game-ui-frontend/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: game-ui-frontend
|
||||
description: Design UI surfaces for browser games. Use when the user asks for HUDs, menus, overlays, responsive layouts, or visual direction that must protect the playfield.
|
||||
---
|
||||
|
||||
# Game UI Frontend
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill whenever the game needs a visible interface layer. The job is not to produce generic dashboard UI. The job is to produce a readable, thematic browser-game interface that supports the play experience.
|
||||
|
||||
Default assumption: build the game world in canvas or WebGL, and build text-heavy UI in DOM.
|
||||
|
||||
## Frontend Standards
|
||||
|
||||
1. Establish visual direction before coding.
|
||||
- Genre and fantasy
|
||||
- Material language
|
||||
- Typography
|
||||
- Palette
|
||||
- Motion tone
|
||||
2. Use CSS variables for the UI theme.
|
||||
3. Build clear hierarchy.
|
||||
- Critical combat or survival information first
|
||||
- Secondary tools second
|
||||
- Rarely used settings behind menus or drawers
|
||||
4. Protect the playfield first, especially in 3D.
|
||||
- The initial screen should feel playable within a few seconds.
|
||||
- Default to one primary persistent HUD cluster and at most one small secondary cluster.
|
||||
- Keep the center of the playfield clear during normal play.
|
||||
- Keep the lower-middle playfield mostly clear during normal play.
|
||||
- Put lore, field notes, quest details, and long control lists behind drawers, toggles, or pause surfaces.
|
||||
- Prefer contextual prompts and transient hints over permanent boxed panels.
|
||||
5. Keep overlays readable over motion.
|
||||
- Use backing panels, edge treatment, contrast, and restrained blur where needed.
|
||||
6. Design for both desktop and mobile from the start.
|
||||
7. Design 3D UI around camera and input control boundaries.
|
||||
- Pause or gate camera-control input when menus, dialogs, or pointer-driven UI are active.
|
||||
- Keep pointer-lock, drag-to-look, and menu interaction states explicit.
|
||||
|
||||
## 3D Starter Defaults
|
||||
|
||||
For exploration, traversal, or third-person starter scaffolds, prefer this UI budget:
|
||||
|
||||
- one compact objective chip or status strip at the edge
|
||||
- one transient controls hint or interaction prompt
|
||||
- one optional collapsible secondary surface such as a journal, map, or quest log
|
||||
|
||||
Do not open every informational surface on first load. The scene should be readable before the user opens any deeper UI.
|
||||
|
||||
As a default implementation constraint for 3D browser games:
|
||||
|
||||
- no always-on full-width header plus multi-card body plus full-width footer layout
|
||||
- no large center-screen or lower-middle overlays during normal movement
|
||||
- no more than roughly 20-25% of the viewport covered by persistent HUD on desktop unless the user explicitly requests a denser layout
|
||||
- on mobile, collapse to a narrow stack or contextual chips before covering the playfield with larger panels
|
||||
|
||||
## Prompting Rules
|
||||
|
||||
When asking the model to design or implement game UI, include:
|
||||
|
||||
- the game fantasy
|
||||
- the camera or viewpoint
|
||||
- the player verbs
|
||||
- the HUD layers
|
||||
- the camera or control mode when the game is 3D
|
||||
- the tone of motion
|
||||
- desktop and mobile expectations
|
||||
- playfield protection and disclosure strategy
|
||||
- explicit anti-patterns to avoid
|
||||
|
||||
Use `../../references/frontend-prompts.md` for concrete prompt shapes.
|
||||
|
||||
## Motion Rules
|
||||
|
||||
- Prefer a few meaningful transitions over constant micro-animation.
|
||||
- Reserve strong motion for state change, reward, danger, and onboarding.
|
||||
- Respect reduced-motion settings for non-essential animation.
|
||||
- Keep 3D HUD motion from competing with camera motion.
|
||||
|
||||
## What Good Looks Like
|
||||
|
||||
- HUD elements are legible without flattening the scene.
|
||||
- Menus feel native to the game world, not like a SaaS admin panel.
|
||||
- Layout adapts cleanly across breakpoints.
|
||||
- Pointer, keyboard, and game-state feedback are obvious.
|
||||
- In 3D games, menu and HUD states do not fight camera control or pointer-lock.
|
||||
- In 3D games, the first playable view keeps most of the viewport available for movement, aiming, and spatial reading.
|
||||
- Persistent information density is low enough that screenshots still read as game scenes, not UI comps.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Generic app dashboard layouts
|
||||
- Flat placeholder styling with no theme
|
||||
- Default font stacks without intent
|
||||
- Dense overlays that obscure the playfield
|
||||
- Large title cards or multi-paragraph notes sitting over a live playable scene
|
||||
- Equal-weight boxed panels distributed around every edge of the viewport
|
||||
- Controls, objectives, notes, and lore all expanded at once on first load
|
||||
- Full-width top-and-bottom chrome with large always-on center or body panels in 3D play
|
||||
- Excessive motion on every element
|
||||
- Canvas-only UI when DOM would be clearer and cheaper
|
||||
- Forcing HUD controls into the 3D scene when standard DOM would be clearer
|
||||
- Letting camera input remain active under modals or inventory panels
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Prompt recipes: `../../references/frontend-prompts.md`
|
||||
- Low-chrome 3D layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- React-hosted 3D UI context: `../react-three-fiber-game/SKILL.md`
|
||||
- Playtest review: `../../references/playtest-checklist.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Game UI Frontend"
|
||||
short_description: "Design browser-game HUDs, menus, and overlays"
|
||||
default_prompt: "Design a browser-game UI layer that supports the play experience without crowding the playfield."
|
||||
88
.hermes/plugins/game-studio/skills/phaser-2d-game/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: phaser-2d-game
|
||||
description: Implement 2D browser games with Phaser. Use when the user wants a Phaser, TypeScript, and Vite stack for scenes, gameplay systems, cameras, sprite animation, and DOM-overlay HUD patterns.
|
||||
---
|
||||
|
||||
# Phaser 2D Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for the main execution path in this plugin. Phaser is the default stack for 2D browser games here because it handles rendering, timing, sprites, cameras, and scene orchestration well without forcing gameplay rules into the framework.
|
||||
|
||||
Preferred stack:
|
||||
|
||||
- Phaser
|
||||
- TypeScript
|
||||
- Vite
|
||||
- DOM-based HUD or menus layered over the game canvas
|
||||
|
||||
## Architecture
|
||||
|
||||
1. Keep gameplay state outside Phaser scenes.
|
||||
- Systems own rules, turn order, movement, combat, inventory, objectives, and progression.
|
||||
- Phaser scenes adapt system state into sprites, camera motion, animation playback, and effects.
|
||||
2. Make scenes thin.
|
||||
- Boot and asset preload
|
||||
- Menu or shell scene
|
||||
- Gameplay scene
|
||||
- Optional overlay or debug scene
|
||||
3. Keep renderer-facing objects disposable.
|
||||
- Sprite containers, emitters, tweens, and camera rigs are view state, not source of truth.
|
||||
4. Favor stable asset manifest keys over direct file-path references throughout gameplay code.
|
||||
|
||||
## Implementation Guidance
|
||||
|
||||
- Use one integration boundary where the scene reads simulation state and emits input actions back.
|
||||
- Prefer deterministic system updates over scene-local mutation.
|
||||
- Treat HUD and menus as DOM when text, status density, or responsiveness matter.
|
||||
- Keep animation state derived from gameplay state rather than ad hoc sprite flags.
|
||||
|
||||
## 2D Modes Covered Well
|
||||
|
||||
- Turn-based grids and tactics
|
||||
- Top-down exploration
|
||||
- Side-view action platformers
|
||||
- Character-action combat with sprite animation
|
||||
- Lightweight management or deck-driven battle scenes
|
||||
|
||||
## Camera and Presentation
|
||||
|
||||
- Choose the camera model early: locked, follow, room-based, or tactical-pan.
|
||||
- Keep camera logic separate from game rules.
|
||||
- Use restrained screen shake, hit-stop, and parallax. Effects should improve readability, not obscure it.
|
||||
|
||||
## UI Integration
|
||||
|
||||
- Use DOM overlays for HUD, command menus, settings, and narrative panels.
|
||||
- Keep the canvas responsible for the world, combat readability, and motion.
|
||||
- Avoid shoving dense text or complex settings UIs into Phaser unless the project explicitly needs an in-canvas presentation.
|
||||
|
||||
## Asset Organization
|
||||
|
||||
- `characters/`
|
||||
- `environment/`
|
||||
- `ui/`
|
||||
- `fx/`
|
||||
- `audio/`
|
||||
- `data/`
|
||||
|
||||
Keep manifest keys human-readable and stable.
|
||||
|
||||
## Default Directory Shape
|
||||
|
||||
See `../../references/phaser-architecture.md` for a concrete module split.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Game rules inside `update()` loops without a system boundary
|
||||
- Scene-to-scene state passed through mutable global objects
|
||||
- HUD text rendered in the game canvas just because it is convenient
|
||||
- Asset paths embedded everywhere instead of a manifest layer
|
||||
- Overusing generic React dashboard patterns for game UI
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- Sprite workflow: `../sprite-pipeline/SKILL.md`
|
||||
- Phaser structure: `../../references/phaser-architecture.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Phaser 2D Game"
|
||||
short_description: "Build 2D browser games with Phaser"
|
||||
default_prompt: "Implement this 2D browser game with Phaser, TypeScript, and a clear gameplay architecture."
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: react-three-fiber-game
|
||||
description: Build React-hosted 3D browser games with React Three Fiber. Use when the user wants pmndrs-based scene composition, shared React state, and 3D HUD integration inside a React app.
|
||||
---
|
||||
|
||||
# React Three Fiber Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when the 3D runtime lives inside a React application. This is the default React-native 3D path in the plugin and should be preferred over vanilla Three.js when the app shell, settings, storefront, editor surface, or surrounding product already uses React.
|
||||
|
||||
Recommended stack:
|
||||
|
||||
- `@react-three/fiber`
|
||||
- `three`
|
||||
- `@react-three/drei`
|
||||
- `@react-three/rapier`
|
||||
- `@react-three/postprocessing`
|
||||
- `@react-three/a11y` when accessibility-sensitive interaction matters
|
||||
- DOM overlays in the normal React tree
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the project already uses React
|
||||
- the 3D scene must share state with the rest of the app
|
||||
- declarative scene composition is a net gain
|
||||
- the team wants pmndrs helpers instead of building every helper layer by hand
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the app is not React-based
|
||||
- the project wants a cleaner imperative runtime with minimal React coordination
|
||||
- the problem is asset packaging rather than runtime composition
|
||||
|
||||
## Best Fit Scenarios
|
||||
|
||||
- 3D configurators and tool-rich browser products
|
||||
- React apps with embedded game or scene surfaces
|
||||
- 3D menus, editors, or world maps in an existing React app
|
||||
- 3D game UIs that depend on shared app state and non-canvas shells
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. Keep simulation state outside render components.
|
||||
- React components should describe scene composition, not become the source of truth for gameplay rules.
|
||||
2. Use React state and scene state deliberately.
|
||||
- Shared UI state can live in app state.
|
||||
- High-frequency simulation should not force the whole app through unnecessary React churn.
|
||||
3. Use pmndrs helpers intentionally.
|
||||
- Drei for controls, loaders, helpers, environments, and common scene primitives.
|
||||
- `@react-three/rapier` for physics integration.
|
||||
- `@react-three/postprocessing` for optional effects.
|
||||
- `@react-three/a11y` when the interaction model benefits from accessible scene semantics.
|
||||
4. Keep HUD, settings, and menus in DOM by default.
|
||||
5. Keep starter scaffolds visually restrained.
|
||||
- Start with one compact objective or status surface and transient prompts.
|
||||
- Keep notes, maps, and multi-step checklists collapsed until opened.
|
||||
- Do not surround the `Canvas` with equally weighted glass cards.
|
||||
|
||||
## Architectural Guidance
|
||||
|
||||
- Use a dedicated scene root component that owns the `Canvas`.
|
||||
- Keep camera rigs and control components isolated from gameplay systems.
|
||||
- Keep loader and asset wrappers predictable.
|
||||
- Keep DOM overlays and the 3D scene coordinated through explicit state boundaries.
|
||||
- If a system needs tight imperative control, isolate it rather than forcing everything into declarative patterns.
|
||||
- If the scene is immediately playable, keep the initial overlay budget low and let the world do more of the onboarding.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Treating React components as the gameplay state store
|
||||
- Pushing heavy per-frame mutation through broad app state
|
||||
- Using R3F only because React is available, even when the project needs a cleaner imperative runtime
|
||||
- Building HUD or inventory UI inside the 3D scene by default
|
||||
- Shipping an initial scaffold with large cards occupying every side of the viewport
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- 3D HUD layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- React starter: `../../references/react-three-fiber-starter.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- WebGL debugging and perf: `../../references/webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "React Three Fiber Game"
|
||||
short_description: "Build React-hosted 3D browser games"
|
||||
default_prompt: "Build this 3D browser game with React Three Fiber and keep the 3D runtime aligned with the React app shell."
|
||||
102
.hermes/plugins/game-studio/skills/sprite-pipeline/SKILL.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: sprite-pipeline
|
||||
description: Generate and normalize 2D sprite animations. Use when the user asks for full-strip generation from approved source frames, consistent anchor and scale normalization, or preview assets for browser-game animation.
|
||||
---
|
||||
|
||||
# Sprite Pipeline
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for 2D sprite generation and normalization. This workflow is intentionally anchored around one approved frame and a whole-strip generation pass because frame-by-frame generation drifts too easily.
|
||||
|
||||
This skill is 2D-specific. If the request is for 3D characters, meshes, or materials, route back through `../game-studio/SKILL.md`.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. Start from an approved in-game seed frame.
|
||||
- The seed frame should already reflect the right silhouette, palette, costume, and proportions.
|
||||
2. Build a larger transparent reference canvas around that frame.
|
||||
- Use `../../scripts/build_sprite_edit_canvas.py`.
|
||||
3. Ask for the full animation strip in one edit request.
|
||||
- Do not generate each frame independently unless the user explicitly accepts lower consistency.
|
||||
4. Normalize the result into fixed-size game frames.
|
||||
- Use `../../scripts/normalize_sprite_strip.py`.
|
||||
- Use one shared scale across the whole strip.
|
||||
- Align frames with one shared anchor, typically bottom-center.
|
||||
5. Optionally lock frame 01 back to the shipped seed frame.
|
||||
- Do this when the animation should begin from the exact idle or base pose already in game.
|
||||
6. Render a preview sheet and inspect the animation in-engine before approving it.
|
||||
- Use `../../scripts/render_sprite_preview_sheet.py`.
|
||||
|
||||
## Prompting Rules
|
||||
|
||||
Always preserve these invariants in the prompt:
|
||||
|
||||
- same character
|
||||
- same facing direction
|
||||
- same palette family
|
||||
- same silhouette family
|
||||
- same readable face or key features
|
||||
- same outfit proportions
|
||||
- transparent background
|
||||
- exact frame count and slot layout
|
||||
|
||||
Always ask for:
|
||||
|
||||
- one strip at once
|
||||
- a transparent canvas
|
||||
- no scenery, labels, or poster composition
|
||||
- crisp pixel-art clusters for pixel work
|
||||
- production asset tone, not concept art
|
||||
|
||||
## Using Image Generation
|
||||
|
||||
For live asset generation or edits, use the installed `imagegen` skill in this workspace. This skill defines the game-specific process; `imagegen` handles the API-backed generation or edit execution.
|
||||
|
||||
## Script Recipes
|
||||
|
||||
Create a reference canvas:
|
||||
|
||||
```bash
|
||||
python3 scripts/build_sprite_edit_canvas.py \
|
||||
--seed output/sprites/idle-01.png \
|
||||
--out output/sprites/hurt-edit-canvas.png \
|
||||
--frames 4 \
|
||||
--slot-size 256 \
|
||||
--canvas-size 1024
|
||||
```
|
||||
|
||||
Normalize a raw strip:
|
||||
|
||||
```bash
|
||||
python3 scripts/normalize_sprite_strip.py \
|
||||
--input output/sprites/hurt-raw.png \
|
||||
--out-dir output/sprites/hurt \
|
||||
--frames 4 \
|
||||
--frame-size 64 \
|
||||
--anchor output/sprites/idle-01.png \
|
||||
--lock-frame1
|
||||
```
|
||||
|
||||
Render a preview sheet:
|
||||
|
||||
```bash
|
||||
python3 scripts/render_sprite_preview_sheet.py \
|
||||
--frames-dir output/sprites/hurt \
|
||||
--out output/sprites/hurt-preview.png \
|
||||
--columns 4
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- proportions stay stable across frames
|
||||
- frame-to-frame size does not drift
|
||||
- action reads clearly at game scale
|
||||
- transparency is preserved
|
||||
- frame 01 matches the shipped sprite when lockback is enabled
|
||||
- preview looks correct before any in-engine asset index update
|
||||
|
||||
## References
|
||||
|
||||
- Detailed workflow: `../../references/sprite-pipeline.md`
|
||||
- Shared frontend context: `../game-ui-frontend/SKILL.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Sprite Pipeline"
|
||||
short_description: "Generate and normalize 2D sprite animations"
|
||||
default_prompt: "Create and normalize 2D sprite animation assets for a browser game with consistent scale and anchors."
|
||||
124
.hermes/plugins/game-studio/skills/three-webgl-game/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: three-webgl-game
|
||||
description: Implement browser-game runtimes with plain Three.js. Use when the user wants imperative scene control in TypeScript or Vite with GLB assets, loaders, physics, and low-level WebGL debugging.
|
||||
---
|
||||
|
||||
# Three WebGL Game
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for the default non-React 3D path in the plugin. This is not generic WebGL advice. It is an opinionated stack for browser 3D work:
|
||||
|
||||
- `three`
|
||||
- TypeScript
|
||||
- Vite
|
||||
- GLB or glTF 2.0 assets
|
||||
- Three.js loaders such as `GLTFLoader`, `DRACOLoader`, and `KTX2Loader`
|
||||
- Rapier JS for physics
|
||||
- SpectorJS for GPU and frame debugging
|
||||
- DOM overlays for HUD, menus, and settings
|
||||
|
||||
Use this skill when the project wants direct scene, camera, renderer, and game-loop control. If the app already lives in React, route to `../react-three-fiber-game/SKILL.md` instead.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the app is plain TypeScript or Vite rather than React-first
|
||||
- the project wants direct imperative control over the render loop
|
||||
- the user asks for Three.js specifically
|
||||
- the runtime needs engine-like control over scene, camera, loaders, and physics
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the 3D scene lives inside an existing React app
|
||||
- the main problem is shipped-asset optimization rather than runtime code
|
||||
- the user explicitly chose Babylon.js or PlayCanvas
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. Keep simulation state outside Three.js objects.
|
||||
- Game rules, AI, quest state, timers, and progression should not live inside meshes or materials.
|
||||
2. Treat the render graph as an adapter.
|
||||
- Scene graph, cameras, materials, loaders, and post-processing are view concerns layered over simulation state.
|
||||
3. Keep camera behavior explicit.
|
||||
- Orbit, follow, chase, rail, and first-person styles each need their own control boundary.
|
||||
4. Keep UI out of WebGL unless the presentation absolutely depends on it.
|
||||
- Menus, HUD, inventories, and settings should default to DOM.
|
||||
5. Use GLB or glTF 2.0 as the default shipping model format.
|
||||
- Do not build the runtime around DCC-native formats.
|
||||
6. Use Rapier instead of ad hoc collision code when the game has meaningful 3D physics or collision response.
|
||||
7. Keep the first playable view low-chrome.
|
||||
- Default to one compact objective or status cluster plus transient prompts.
|
||||
- Long notes, lore, and controls references should be collapsed until asked for.
|
||||
- Do not frame the scene with multiple equal-weight cards during normal play.
|
||||
|
||||
## Initial Scaffold UX
|
||||
|
||||
For exploration, traversal, and character-control prototypes, start with a sparse shell:
|
||||
|
||||
- one edge-aligned objective chip
|
||||
- one transient controls hint
|
||||
- one optional compact status strip
|
||||
|
||||
Only add larger UI surfaces when the game loop truly requires them. Journal, quest log, codex, map, and settings surfaces should open on demand, not occupy the viewport by default.
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
Use the module shape in `../../references/three-webgl-architecture.md`, then keep these boundaries clean:
|
||||
|
||||
- `simulation/`: rules, progression, state, and AI
|
||||
- `render/app/`: renderer, scene, camera, resize, context lifecycle
|
||||
- `render/loaders/`: GLTF, Draco, KTX2, texture, and environment loading
|
||||
- `render/objects/`: mesh instantiation and disposal
|
||||
- `render/materials/`: material setup and shader boundaries
|
||||
- `physics/`: Rapier world, bodies, colliders, and simulation bridge
|
||||
- `ui/`: DOM overlays and menus
|
||||
- `diagnostics/`: debug toggles, perf probes, and capture hooks
|
||||
|
||||
## Good Fit Scenarios
|
||||
|
||||
- Exploration demos
|
||||
- Lightweight 3D combat prototypes
|
||||
- Vehicle or traversal prototypes
|
||||
- Scene-driven product or world showcases with gameplay
|
||||
- Material, lighting, or post-process-led experiences
|
||||
- 3D games where camera movement and depth readability are central
|
||||
|
||||
## Loaders, Assets, and Post-Processing
|
||||
|
||||
- Start with `GLTFLoader` for shipped 3D content.
|
||||
- Add `DRACOLoader` or Meshopt-compatible optimization as part of the asset pipeline, not as a random runtime patch.
|
||||
- Use `KTX2Loader` for compressed textures when the asset pipeline provides them.
|
||||
- Prefer built-in Three.js render and post-processing utilities first. Add heavier abstraction only when the project actually needs it.
|
||||
- Keep post-processing optional and measurable. Bloom and color effects should not hide gameplay readability.
|
||||
|
||||
## Shader and Material Guidance
|
||||
|
||||
- Start with standard Three.js materials and correct lighting before reaching for custom shaders.
|
||||
- Use custom shaders only when the visual target genuinely needs them.
|
||||
- Keep shader parameters driven by game state, not by incidental scene mutations.
|
||||
- If a material stack gets complex, isolate it behind material factories instead of scattering shader setup across scene code.
|
||||
|
||||
## Browser Safety
|
||||
|
||||
- Handle resize explicitly.
|
||||
- Expect WebGL context loss and recovery.
|
||||
- Keep a fallback or degraded mode in mind for fragile GPU paths.
|
||||
- Watch texture size, geometry count, draw-call growth, and post-processing cost.
|
||||
- Use SpectorJS when the scene behaves incorrectly or frame cost is unclear.
|
||||
|
||||
## Scope Warning
|
||||
|
||||
Do not claim that this plugin offers equal 3D depth to the Phaser track. It supports serious 3D implementation, but the plugin is still 2D-first overall.
|
||||
|
||||
## References
|
||||
|
||||
- Shared architecture: `../web-game-foundations/SKILL.md`
|
||||
- Frontend direction: `../game-ui-frontend/SKILL.md`
|
||||
- 3D HUD layout patterns: `../../references/three-hud-layout-patterns.md`
|
||||
- Three.js ecosystem: `../../references/threejs-stack.md`
|
||||
- Three.js structure: `../../references/three-webgl-architecture.md`
|
||||
- Vanilla starter: `../../references/threejs-vanilla-starter.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline: `../../references/web-3d-asset-pipeline.md`
|
||||
- WebGL debugging and perf: `../../references/webgl-debugging-and-performance.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Three WebGL Game"
|
||||
short_description: "Build browser-game runtimes with Three.js"
|
||||
default_prompt: "Implement this browser-game runtime with plain Three.js and keep the scene architecture easy to debug."
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: web-3d-asset-pipeline
|
||||
description: Prepare and optimize browser-game 3D assets. Use when the user asks for GLB or glTF shipping work, including Blender cleanup and export, collision or LOD setup, compression, texture packaging, and runtime validation.
|
||||
---
|
||||
|
||||
# Web 3D Asset Pipeline
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill for shipped 3D assets, not runtime scene code. The default output format for browser 3D work in this plugin is GLB or glTF 2.0. The goal is predictable runtime assets, not whatever the DCC tool happened to export first.
|
||||
|
||||
This guidance is engine-agnostic and can serve Three.js, React Three Fiber, Babylon.js, or PlayCanvas.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the task is about GLB or glTF shipping format
|
||||
- the task is about model cleanup, texture packaging, compression, LOD, or collision proxies
|
||||
- the runtime stack is already chosen and the remaining problem is asset quality or size
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- the task is about scene, camera, renderer, or game-loop structure
|
||||
- the task is purely about React versus vanilla Three.js routing
|
||||
- the user is still deciding between runtime engines
|
||||
|
||||
## Default Pipeline
|
||||
|
||||
1. Author and clean the source asset in a DCC tool such as Blender.
|
||||
2. Export to GLB or glTF 2.0.
|
||||
3. Optimize with glTF Transform.
|
||||
4. Validate naming, pivots, transforms, material reuse, and texture budgets.
|
||||
5. Add collision proxies, LOD strategy, and baked-lighting assumptions as needed.
|
||||
6. Ship the optimized asset and load it with engine-native GLTF support.
|
||||
|
||||
## Format Rules
|
||||
|
||||
- Default shipping format: GLB or glTF 2.0.
|
||||
- Do not treat FBX, OBJ, or DCC-native formats as the long-term runtime contract.
|
||||
- Apply or normalize transforms before shipping.
|
||||
- Keep units, pivots, and orientation conventions consistent across the whole asset set.
|
||||
|
||||
## Optimization Rules
|
||||
|
||||
- Use glTF Transform for pruning, deduplication, simplification, and packaging.
|
||||
- Use geometry compression intentionally.
|
||||
- Draco is a valid option when decode cost and compatibility fit the runtime.
|
||||
- Meshopt is often a strong default for web delivery.
|
||||
- Compress textures deliberately.
|
||||
- Use KTX2 or BasisU when the runtime stack supports it.
|
||||
- Use WebP or AVIF where they make sense in the broader asset pipeline.
|
||||
- Reuse materials and textures where possible to cut memory and draw-call cost.
|
||||
|
||||
## Runtime-Ready Asset Rules
|
||||
|
||||
- Keep model hierarchy names stable and meaningful.
|
||||
- Set pivots and origins for gameplay interaction, not just for DCC convenience.
|
||||
- Author explicit collision proxies for physics-heavy scenes.
|
||||
- Decide whether lighting is dynamic, baked, or hybrid before final export.
|
||||
- Plan LODs for large environments or repeated props.
|
||||
- Keep texture resolution proportional to on-screen use, not source-art ambition.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
- Shipping raw DCC exports without cleanup
|
||||
- Too many unique materials
|
||||
- Texture sizes far above visible need
|
||||
- Missing collision proxies
|
||||
- Scale or pivot mismatches between assets
|
||||
- Runtime code compensating for asset mistakes that should be fixed upstream
|
||||
|
||||
## References
|
||||
|
||||
- Three.js stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- GLB loader starter: `../../references/gltf-loading-starter.md`
|
||||
- Rapier starter: `../../references/rapier-integration-starter.md`
|
||||
- 3D asset pipeline reference: `../../references/web-3d-asset-pipeline.md`
|
||||
- Alternative engines: `../../references/alternative-3d-engines.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Web 3D Asset Pipeline"
|
||||
short_description: "Prepare and optimize browser-game 3D assets"
|
||||
default_prompt: "Prepare these browser-game 3D assets for shipping as predictable runtime-ready GLB or glTF files."
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: web-game-foundations
|
||||
description: Set browser-game architecture before implementation. Use when the user needs engine choice, simulation and render boundaries, input model, asset organization, or save/debug/performance strategy.
|
||||
---
|
||||
|
||||
# Web Game Foundations
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to establish the non-negotiable architecture before implementation starts. Browser games degrade quickly when simulation, rendering, UI, asset loading, and input handling are mixed together.
|
||||
|
||||
Default rule: simulation state is owned outside the renderer, browser UI is not forced into the canvas unless there is a clear reason, and shipped 3D assets default to GLB or glTF 2.0 rather than ad hoc model formats.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- the user has not settled the engine or renderer choice
|
||||
- the task is about boundaries, module shape, state ownership, or asset policy
|
||||
- multiple specialist skills need one shared architectural frame
|
||||
|
||||
## Do Not Stay Here When
|
||||
|
||||
- the runtime track is clearly Phaser
|
||||
- the runtime track is clearly vanilla Three.js
|
||||
- the runtime track is clearly React Three Fiber
|
||||
- the task is purely about shipped 3D assets
|
||||
|
||||
Once the stack is clear, hand off to the runtime or asset specialist skill.
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
1. Separate simulation from rendering.
|
||||
- Simulation owns entities, turns, timers, collisions, progression, and saveable state.
|
||||
- The renderer owns scene composition, animation playback, camera, particles, and input plumbing.
|
||||
2. Keep input mapping explicit.
|
||||
- Define actions such as `move`, `confirm`, `cancel`, `ability-1`, and `pause`.
|
||||
- Map physical inputs to actions in one place.
|
||||
3. Treat asset loading as a first-class system.
|
||||
- Use stable manifest keys.
|
||||
- Group by domain: characters, environment, UI, audio, FX.
|
||||
- For 3D content, standardize on GLB or glTF 2.0 unless the chosen engine ecosystem requires another format upstream.
|
||||
4. Define save/debug/perf boundaries up front.
|
||||
- Save serializable simulation state, not renderer objects.
|
||||
- Keep debug overlays and perf probes easy to toggle.
|
||||
5. Use DOM overlays for menus and HUD by default.
|
||||
- Canvas or WebGL should handle the playfield.
|
||||
- DOM should handle text-heavy HUD, menus, settings, and accessibility-sensitive controls.
|
||||
- In 3D, keep the persistent UI budget small so the scene stays readable and interactive.
|
||||
6. Lock 3D runtime conventions early.
|
||||
- Choose consistent units, origins, pivots, and naming conventions.
|
||||
- Decide how collision proxies, LODs, and baked lighting data are authored before runtime integration starts.
|
||||
|
||||
## Engine Selection
|
||||
|
||||
- Default to Phaser for 2D games with sprites, tilemaps, top-down or side-view action, turn-based grids, and classic browser arcade flows.
|
||||
- Default to vanilla Three.js for explicit 3D scenes that want direct scene, camera, renderer, and loop control in plain TypeScript or Vite.
|
||||
- Default to React Three Fiber when the 3D scene lives inside a React application and needs declarative composition, shared app state, or React-first UI coordination.
|
||||
- Use raw WebGL only for shader-heavy or renderer-first projects where engine abstractions would get in the way.
|
||||
- Keep Babylon.js and PlayCanvas as alternative-engine paths rather than the default code-generation target.
|
||||
|
||||
See `../../references/engine-selection.md` for the default decision table.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
Define these before writing core code:
|
||||
|
||||
- Player fantasy and primary verbs
|
||||
- Core loop and loss or reset states
|
||||
- Camera model
|
||||
- Input action map
|
||||
- Simulation modules
|
||||
- Renderer modules
|
||||
- Asset manifest layout
|
||||
- 3D asset format and optimization rules
|
||||
- HUD and menu surfaces
|
||||
- Save data boundary
|
||||
- Debug and perf surfaces
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Mixing gameplay rules directly into scene callbacks
|
||||
- Treating the renderer as the source of truth for game state
|
||||
- Putting all HUD and menu UI into the canvas by default
|
||||
- Letting asset filenames become the public API instead of manifest keys
|
||||
- Shipping unoptimized 3D assets straight from the DCC tool into the browser
|
||||
- Mixing camera-control state and menu or modal state without an explicit input boundary
|
||||
- Rebuilding architecture every time the game changes genre
|
||||
|
||||
## References
|
||||
|
||||
- Engine selection: `../../references/engine-selection.md`
|
||||
- Phaser structure: `../../references/phaser-architecture.md`
|
||||
- Three.js structure: `../../references/three-webgl-architecture.md`
|
||||
- Three.js ecosystem stack: `../../references/threejs-stack.md`
|
||||
- React Three Fiber stack: `../../references/react-three-fiber-stack.md`
|
||||
- 3D asset shipping: `../../references/web-3d-asset-pipeline.md`
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Web Game Foundations"
|
||||
short_description: "Set browser-game architecture before implementation"
|
||||
default_prompt: "Establish the core architecture for this browser game before implementation starts."
|
||||
411
.hermes/shared-memory/decision-log.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# 决策记录
|
||||
|
||||
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
||||
|
||||
## 记录格式
|
||||
|
||||
```md
|
||||
## YYYY-MM-DD 决策标题
|
||||
|
||||
- 背景:为什么需要这个决策
|
||||
- 决策:最终决定是什么
|
||||
- 影响范围:涉及哪些模块/文档/流程
|
||||
- 验证方式:如何确认决策仍有效
|
||||
- 关联文档:相关 PRD、技术文档、提交或 Issue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-13 微信小程序支付以后端通知为唯一入账事实
|
||||
|
||||
- 背景:“我的”账户充值需要接入微信小程序支付,同时保留本地 / H5 mock 支付联调能力。
|
||||
- 决策:`paymentChannel = "mock"` 继续创建即 paid 订单并立即入账;`paymentChannel = "wechat_mp"` 先在 `profile_recharge_order` 写入 `pending` 订单,再由 `api-server` 调微信支付 JSAPI 下单并返回小程序 `wx.requestPayment` 参数。小程序或 H5 的支付成功回调只触发刷新,不直接发放光点或会员;最终入账只由 `/api/profile/recharge/wechat/notify` 验签、解密并确认 `trade_state = SUCCESS` 后完成。`provider_transaction_id` 保存微信支付平台交易号,用于对账、查单、退款和客服排障。
|
||||
- 影响范围:`profile_recharge_order` 表、SpacetimeDB 充值 procedure、`api-server` 微信支付客户端、小程序 native 支付页、H5 充值弹窗与共享 contract。
|
||||
- 验证方式:执行 `npm run typecheck`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`cargo test -p module-runtime recharge --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat_pay --manifest-path server-rs/Cargo.toml`,后端联调仍用 `npm run api-server` 和 `/healthz`。
|
||||
- 关联文档:`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## 2026-05-13 修改密码后全设备强制下线
|
||||
|
||||
- 背景:修改密码原本只递增 `token_version`,旧 access token 会失效,但旧 refresh cookie 仍可通过 `/api/auth/refresh` 重新签发新 token,不符合“改密后全设备强制下线”的账号安全预期。
|
||||
- 决策:`POST /api/auth/password/change` 成功后必须在同一认证真相更新中撤销该用户全部 active `refresh_session`,继续递增 `token_version`,响应清除当前 refresh cookie;前端 `changePassword` 成功后清空本地 access token 并回到未登录态。用户需要使用新密码重新登录。
|
||||
- 影响范围:`module-auth` 修改密码用例、`api-server` password management route、`AuthGate`、`authService`、密码登录/重置技术文档。
|
||||
- 验证方式:执行 `cargo test -p api-server --manifest-path server-rs/Cargo.toml password_change_allows_login_with_new_password_only -- --nocapture`、`npm run test -- AuthGate.test.tsx authService.test.ts`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md`、`docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md`。
|
||||
|
||||
## 2026-05-13 refresh_session 会话组后端聚合与远端踢下线
|
||||
|
||||
- 背景:账号安全页中同设备同 IP 的多条 active `refresh_session` 会重复展示;退出登录没有稳定撤销当前 refresh session;前端“踢下线”只做本地状态变化,未真正让远端设备失效。
|
||||
- 决策:`GET /api/auth/sessions` 由后端按“同设备 + 同 IP”聚合 active refresh sessions,响应保留代表 `sessionId` 并新增 `sessionIds/sessionCount`;组内包含当前 refresh hash 或 Bearer `sid` 时整组视为当前设备组,前端不展示踢下线。新增 `POST /api/auth/sessions/{session_id}/revoke`,只允许撤销当前用户自己的非当前会话,不递增 `token_version`,但认证中间件会校验 access token `sid` 对应 active refresh session,使被踢设备立即失效。`/api/auth/logout` 在 refresh cookie 缺失时回退用 Bearer `sid` 撤销当前 session,并继续递增 `token_version`。
|
||||
- 影响范围:`module-auth` refresh session service、`api-server` auth middleware/logout/sessions route、`shared-contracts`/TS auth contract、`AuthGate`、`AccountModal`、认证会话技术文档和路由/埋点索引。
|
||||
- 验证方式:执行 `cargo test -p module-auth --manifest-path server-rs/Cargo.toml refresh_session`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml auth_sessions -- --nocapture`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml revoke_auth_session -- --nocapture`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml logout_succeeds_without_refresh_cookie_when_bearer_token_is_valid -- --nocapture`、`npm run test -- AuthGate.test.tsx AccountModal.test.tsx authService.test.ts`、`npm run check:encoding`、`git diff --check`,并用 `npm run api-server` 检查 `/healthz`。
|
||||
- 关联文档:`docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md`、`docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md`、`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`。
|
||||
|
||||
## 2026-05-12 抓大鹅入口素材风格改为 2D 常见素材风格
|
||||
|
||||
- 背景:抓大鹅草稿素材生成已经收敛为多视角 2D 图片素材,但入口页和旧参考图仍沿用黏土、低多边形、塑料、木雕、体素、金属等偏 3D 素材语言,容易让后续生成链路和用户预期继续漂移。
|
||||
- 决策:抓大鹅创作入口 `2D素材风格` 固定为 `扁平图标 / 赛璐璐卡通 / 像素复古 / 手绘水彩 / 贴纸描边 / 厚涂图标 / 自定义`;默认风格为 `flat-icon`。入口参考图统一由 `npm run assets:match3d-style-references -- --live` 调用 VectorEngine `gpt-image-2-all` 生成,输出到 `public/match3d-style-references/`。旧 3D 风格参考图不再保留为入口资产。
|
||||
- 影响范围:`Match3DAgentWorkspace`、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。
|
||||
- 验证方式:执行 `npm run test -- src\components\match3d-creation\Match3DAgentWorkspace.interaction.test.tsx`、`cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`、`docs/technical/MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md`。
|
||||
|
||||
## 2026-05-12 拼图与抓大鹅草稿背景音乐按纯音乐自动生成
|
||||
|
||||
- 背景:拼图和抓大鹅需要在草稿生成阶段直接产出可试听、可重生成、可进入运行态循环播放的背景音乐。
|
||||
- 决策:复用通用 VectorEngine Suno 创作音频链路,不新增 SpacetimeDB 表;拼图音乐保存到首关 `PuzzleDraftLevel.backgroundMusic`,运行态通过 `PuzzleRuntimeLevelSnapshot.backgroundMusic` 下发;抓大鹅音乐保存到首个 `generatedItemAssets[].backgroundMusic`。两者草稿生成都使用 `title` 驱动、`prompt = ""`、`make_instrumental = true`,失败只降级记录 warning,结果页允许重新生成。
|
||||
- 影响范围:`api-server` 音频生成、拼图草稿编译、抓大鹅草稿编译、Puzzle/Match3D 结果页和运行态音频播放。
|
||||
- 验证方式:检查草稿 response / work detail 中的 `backgroundMusic.audioSrc`,运行态开局后隐藏 audio 循环播放;执行音频相关后端 check、前端 typecheck 和编码检查。
|
||||
- 关联文档:`docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 2026-05-12 拼图 UI 背景图复用 levels_json 持久化
|
||||
|
||||
- 背景:拼图草稿结果页需要像抓大鹅一样支持 UI 背景生成,但首版只需要作品级/首关背景,不应为图片生成结果新增 SpacetimeDB 表结构。
|
||||
- 决策:拼图 UI 背景字段存入首关 `levels_json`,字段为 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`;`compile_puzzle_draft` 草稿编译阶段在首图和背景音乐后自动生成首关 UI 背景,失败只记录 warning 并允许结果页重试;结果页新增 `UI` Tab,可编辑提示词并触发 `generate_puzzle_ui_background`。`api-server` 读取 `public/ui-previews/puzzle-image-compact-ui-2026-05-08.png` 作为非拼图 UI 参考图,调用 VectorEngine `gpt-image-2-all` 生成 9:16 背景并要求中央正方形拼图区与外部 UI 背景边界清晰。SpacetimeDB 只保存结果,不做外部 I/O。
|
||||
- 影响范围:拼图结果页、拼图运行态背景渲染、拼图 agent action、`module-puzzle` / `spacetime-module` / `spacetime-client` 的拼图关卡 JSON 映射、拼图流程技术文档。
|
||||
- 验证方式:执行 `npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_ui_background --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。
|
||||
|
||||
## 2026-05-12 抓大鹅结果页素材编辑统一走作品级资产面板
|
||||
|
||||
- 背景:抓大鹅结果页需要支持碰面图上传 / AI 重绘、物品素材独立预览、单项删除和批量新增,且不能把素材编辑继续做成列表内联展开或前端临时状态。
|
||||
- 决策:结果页 `作品信息` 的碰面图点击打开独立面板,参考图可来自本地上传、物品素材和 UI 素材;AI 重绘统一调用 `POST /api/creation/match3d/works/{profileId}/cover-image` 并转存到 `generated-match3d-assets`。`素材配置 > 物品` 列表项点击打开独立预览面板,不再提供单项重新生成按钮;单项删除和批量新增都写回同一份 `generated_item_assets_json`。批量新增调用 `POST /api/creation/match3d/works/{profileId}/item-assets`,复用草稿生成的 2D 素材图、5x5 切图、OSS 上传和可选点击音效链路,仅作用于新增物品,不新增 SpacetimeDB 表。
|
||||
- 影响范围:Match3D 结果页、Match3D works shared contracts、`api-server` Match3D 作品路由、生成资产历史类型和草稿恢复路径。
|
||||
- 验证方式:执行 `npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`npm run typecheck`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 2026-05-12 平台法律文档入口与登录协议确认
|
||||
|
||||
- 背景:生产发布需要在个人页展示用户协议、隐私政策、免责声明和备案号;登录页首次登录需要显式确认法律协议。
|
||||
- 决策:法律文档内容读取 `media/files/*.md`,统一通过 `LegalDocumentModal` 独立弹窗展示;“我的”页常用功能区固定 3 列,设置入口下方展示法律信息和 `京ICP备2026025677号` 外链。登录弹窗用 `genarrative.auth.legal-consent.v1` 记录本机确认,首次未勾选时短信 / 密码登录按钮禁用,法律链接不自动勾选。
|
||||
- 影响范围:平台个人页、登录弹窗、法律 Markdown 渲染和前端认证交互测试。
|
||||
- 验证方式:执行 `npm run test -- src/components/auth/AuthGate.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、触碰文件 ESLint、`npm run check:encoding`。
|
||||
- 关联文档:`docs/prd/PROFILE_LEGAL_INFO_AND_AUTH_AGREEMENT_PRD_2026-05-12.md`。
|
||||
## 2026-05-12 微信小程序待绑定手机号优先走原生手机号授权
|
||||
|
||||
- 背景:微信小程序 `web-view` 壳登录后若返回 `pending_bind_phone`,H5 仍会展示手输手机号和短信验证码绑定页,体验上多了一步。
|
||||
- 决策:小程序壳在 `pending_bind_phone` 时暂不打开 H5,先展示原生 `button open-type="getPhoneNumber"`;用户同意后把 `bindgetphonenumber` 返回的 `code` 作为 `wechatPhoneCode` 调用 `/api/auth/wechat/bind-phone`。后端通过微信 `stable_token` 与 `getuserphonenumber` 换取平台验证后的手机号,再复用现有微信待绑定账号合并逻辑并重新签发 active 系统 token。H5 旧短信验证码绑定流程继续作为非小程序环境兜底。
|
||||
- 影响范围:`miniprogram/pages/web-view/index.*`、`server-rs/crates/platform-auth`、`server-rs/crates/api-server/src/wechat_auth.rs`、认证共享契约、微信小程序 web-view 壳技术文档。
|
||||
- 验证方式:执行 `npm run check:encoding`、`node scripts/check-wechat-miniprogram-auth-smoke.mjs`、`cargo test -p shared-contracts wechat_bind_phone_request_accepts_mini_program_phone_code --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat_miniprogram_bind_phone_code_activates_pending_user --manifest-path server-rs/Cargo.toml -- --nocapture`。
|
||||
- 关联文档:`docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md`。
|
||||
|
||||
## 2026-05-11 拼图与抓大鹅结果页音频资产复用通用创作音频链路
|
||||
|
||||
- 背景:拼图和抓大鹅结果页需要接入 Suno 背景音乐,抓大鹅还需要物体点击音效,但当前两类作品没有独立的作品级音频表或 metadata 字段。
|
||||
- 决策:新增 `/api/creation/audio/*` 通用创作音频路由,后端统一负责 VectorEngine 音频任务、OSS 转存、`asset_object` 与 `asset_entity_binding` 写入;视觉小说旧路由保留并复用同一持久化逻辑。拼图背景音乐暂存到首关 `levels_json[0].backgroundMusic/background_music`;抓大鹅背景音乐暂存到 `generated_item_assets_json[0].backgroundMusic/background_music`,单物体点击音效存到对应 item 的 `clickSound/click_sound`。本轮不新增 SpacetimeDB 表和字段。
|
||||
- 2026-05-12 补充:抓大鹅入口页新增 `generateClickSound` 开关,默认关闭;开启时 `match3d_compile_draft` 在生成首批 2D 物品素材后并行生成各物品点击音效,并继续复用通用创作音频路由的 OSS、资产绑定和扣费口径。
|
||||
- 影响范围:拼图结果页、抓大鹅结果页、抓大鹅运行态音频播放、通用创作音频 shared contracts、`api-server` 音频路由和资产绑定。
|
||||
- 验证方式:执行拼图/抓大鹅结果页定向测试、`npm run typecheck`、`cargo test -p api-server vector_engine_audio_generation`、`cargo test -p shared-contracts creation_audio`、`cargo check -p api-server`,真实生成需配置 VectorEngine 与 OSS 私密环境。
|
||||
- 关联文档:`docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md`。
|
||||
|
||||
## 2026-05-11 寓教于乐公开作品使用独立 `edutainment` 来源接入
|
||||
|
||||
- 背景:`宝贝识物` 首关需要通过创作模板发布后进入寓教于乐板块,同时关闭入口时必须从发现页、搜索、详情深链、作品号和历史入口完全不可见;若继续落入 RPG 默认公共作品链路,容易出现误启动、误改造或近似标签误归类。
|
||||
- 决策:寓教于乐公开作品在前端公共作品模型中使用 `sourceType = edutainment`,当前只承接 `templateId = baby-object-match`、`templateName = 宝贝识物`;进入“发现 / 寓教于乐”频道仍必须携带精确等于 `寓教于乐` 的公开标签,不因模板名或近似标签自动归类。公开详情、推荐运行态、改造、编辑、点赞和分享链路都必须显式识别 `edutainment`,不得回落到 RPG 默认处理。
|
||||
- 影响范围:公开作品卡、发现页频道、作品号搜索、公开详情深链、分享、作品架聚合、后续儿童动作 Demo 模板的发布结果展示。
|
||||
- 验证方式:执行第4线程定向单测、前端类型检查、ESLint 与编码检查;关闭 `VITE_ENABLE_EDUTAINMENT_ENTRY` 时确认精确 `寓教于乐` 作品不可通过任何公开入口访问。
|
||||
- 关联文档:`docs/design/CHILD_MOTION_EDUTAINMENT_DISCOVER_ENTRY_2026-05-09.md`、`docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md`、`docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md`。
|
||||
|
||||
## 2026-05-10 儿童动作 Demo 视觉资产统一为绘本草地舞台
|
||||
|
||||
- 背景:儿童动作 Demo 需要从暗色科技风切换到更适合儿童互动的卡通绘本草地风格,并且要让背景、地面、UI、地面指示环和用户轮廓使用同一套 image-2 资源口径。
|
||||
- 决策:热身舞台及后续儿童动作 Demo 场景、物品、UI 资源统一采用明亮卡通绘本草地视觉语言。真实资源默认输出到 `public/child-motion-demo/`。背景沿用 `picture-book-grass-stage.png`;地面、指示环、角色轮廓和 UI 已拆分为 v2 用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v2.png`、`picture-book-character-outline-v2.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png` 和 `picture-book-ui-button-v2.png`。生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 调用 VectorEngine `gpt-image-2-all`;透明资源使用品红底生成后本地去背,中间源图仅保存在 `tmp/child-motion-demo-assets/`。在缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时,只允许 dry-run 和 CSS 兜底,不伪造 live 生图结果。
|
||||
- 影响范围:`src/index.css`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 的舞台视觉层、儿童动作 Demo 技术文档、后续 image-2 资产生成流程。
|
||||
- 验证方式:检查 `/child-motion-demo` 舞台是否在未生成资产时仍有可用草地绘本兜底;补齐 VectorEngine 私密配置后运行 `npm run assets:child-motion-demo -- --live` 或 `--live --only <asset-id>` 应能写出对应 PNG,并确认页面静态资源返回 `image/png`。若只调整透明去背、裁切或品红边缘,可运行 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用源图后处理。页面接入时必须按资源原始比例等比使用,不得把方形软纸面板拉伸成 HUD、状态条或底部草坪。
|
||||
- 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。
|
||||
|
||||
## 2026-05-10 方洞挑战从创作页入口和作品架隐藏
|
||||
|
||||
- 背景:运营节奏要求创作页完全隐藏方洞挑战,不能只隐藏新建入口后仍从创作页作品架暴露已有方洞草稿或已发布作品。
|
||||
- 决策:SpacetimeDB `creation_entry_type_config` 中 `square-hole.visible=false` 作为创作页统一开关;创作 Tab 模板入口、旧选择弹层、创作 Hub 卡带和创作页作品架都基于该开关隐藏方洞挑战。既有方洞详情、作品号、广场和运行态链路暂不删除,api-server 路由熔断只按 `open=false` 禁用玩法 API。
|
||||
- 影响范围:SpacetimeDB 入口配置默认种子、`platformEntryCreationTypes`、`CustomWorldCreationHub`、`PlatformEntryFlowShellImpl` 以及创作入口相关文档和回归测试。
|
||||
- 验证方式:执行入口配置、创作 Hub 和平台入口交互定向测试,确认看不到“方洞挑战” Tab、按钮和作品架条目。
|
||||
- 关联文档:`docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md`、`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。
|
||||
|
||||
## 2026-05-10 运行态输入设备抽象层全项目通用化
|
||||
|
||||
- 背景:拼图运行态接入 mocap 后,鼠标/触控和 mocap 各自维护输入逻辑会导致合并大块、拖拽语义和取消会话行为不一致;后续其他玩法也需要复用体感、摇杆、键盘等设备输入。
|
||||
- 决策:前端运行态输入统一通过 `src/services/input-devices/` 承接,设备适配层只输出 `press / move / release / tap / drop` 等通用语义和通用坐标;玩法组件自己解释目标对象、落点和业务动作,输入层不得写拼图等玩法专用规则。
|
||||
- 影响范围:拼图运行态鼠标/触控/mocap 输入、后续运行态设备接入、运行态输入技术文档与相关前端回归测试。
|
||||
- 验证方式:执行 `npm run test -- src\services\input-devices\runtimeDragInputController.test.ts`、`npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。
|
||||
- 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`、`docs/technical/PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md`。
|
||||
|
||||
## 2026-05-11 前端调试模式统一判断
|
||||
|
||||
- 背景:拼图 mocap 调试面板此前在运行态常驻展示,生产构建和正式体验里容易遮挡棋盘内容;后续其它局部诊断 UI 也需要统一的调试模式入口。
|
||||
- 决策:前端新增 `src/config/debugMode.ts` 作为全局调试模式判断,默认跟随 Vite 开发态,允许 `VITE_DEBUG_MODE=true/false` 显式覆盖。拼图运行态 mocap 调试面板只在调试模式下渲染,并默认折叠,只保留连接状态行。
|
||||
- 影响范围:前端局部调试 UI、拼图运行态 mocap 诊断面板、`.env.example` 和运行态输入技术文档。
|
||||
- 验证方式:执行 `npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。
|
||||
- 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`。
|
||||
|
||||
## 2026-05-10 儿童动作热身关直接消费 mocap 数据源
|
||||
|
||||
- 背景:儿童动作 Demo 不能只依赖浏览器摄像头状态和键鼠调试输入,否则真实硬件接入后会出现“mocap 在线但页面提示摄像头不可用”或“能看到画面但动作不推进”的卡点。
|
||||
- 决策:热身关全流程直接接入 `useMocapInput`,通过本地 mocap WebSocket `/stream` 消费 `general.body.center_norm` 身体中心、`actions/action/gesture/gestures/event/name/type` 动作名,以及 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 手部坐标;位置步骤由身体中心推进,`wave_greeting`、`wave_left_hand`、`wave_right_hand` 和 `jump_once` 由 mocap 手势/轨迹推进。浏览器摄像头只作为背景层,动作数据源状态优先展示,键鼠仍作为本地调试兜底。
|
||||
- 影响范围:`src/services/useMocapInput.ts`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx`、对应单测与热身关技术文档。
|
||||
- 验证方式:执行 `npx vitest run src/services/useMocapInput.test.ts src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/components/child-motion-demo/childMotionWarmupModel.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts`、`npx eslint ...`、`npm run typecheck`、`npm run check:encoding`,并确认 `http://127.0.0.1:8876/stream` WebSocket 可握手、`http://127.0.0.1:3000/child-motion-demo` 可访问。
|
||||
- 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## 2026-05-09 GPT-image-2 图片生成统一迁移到 VectorEngine
|
||||
|
||||
- 背景:仓库内 RPG、拼图、方洞和本地模板脚本的 GPT-image-2 生图此前依赖 APIMart 图片网关;团队要求参考 VectorEngine Apifox `api-448710071`,后续不再使用 APIMart 执行 GPT-image-2 图片生成。
|
||||
- 决策:所有 GPT-image-2 生图请求统一走 VectorEngine `POST /v1/images/generations`,基础配置读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,上游模型使用 `gpt-image-2-all`,请求体不再携带 `official_fallback`,参考图字段改为 `image`。APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 影响范围:`api-server` 共享图片 helper、拼图图片生成、角色主图、RPG 场景图、开局 CG 故事板、方洞视觉资产、生产环境示例、gpt-image-2 本地 skill 和相关技术文档。
|
||||
- 验证方式:执行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`,并用 `npm run api-server` + `/healthz` 做后端 smoke。
|
||||
- 关联文档:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 Hyper3D Rodin Gen-2 只通过后端安全代理接入
|
||||
|
||||
- 背景:需要接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型,但供应商 API Key 不能进入前端、文档或 Git;本次只是外部副作用代理,不需要新增平台真相表。
|
||||
- 决策:Hyper3D 统一走 `api-server` 的 `/api/assets/hyper3d/*` 鉴权路由,配置只读取 `HYPER3D_BASE_URL` / `HYPER3D_API_KEY` / `HYPER3D_MODEL_REQUEST_TIMEOUT_MS` 及兼容 `RODIN_*` 变量;生成提交、状态查询和下载列表都由后端代理。首版不写 SpacetimeDB、不确认 `asset_object`,下载链接后续由调用方决定是否进入 OSS 资产链。
|
||||
- 影响范围:`api-server` 外部服务配置、Hyper3D route、`shared-contracts` / TS contract、前端 service、生产环境示例和外部服务环境变量文档。
|
||||
- 验证方式:执行 `cargo test -p api-server hyper3d --manifest-path server-rs/Cargo.toml`、`cargo test -p shared-contracts hyper3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck` 和编码检查;真实 API smoke 只在本地私密环境配置 key 后手动执行。
|
||||
- 关联文档:`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 APIMart 接口统一携带 `official_fallback`
|
||||
|
||||
> 2026-05-09 追认:本决策中的图片生成部分已被“GPT-image-2 图片生成统一迁移到 VectorEngine”覆盖;当前只保留 APIMart `gpt-5` Responses 文本/多模态链路继续显式携带 `official_fallback`。
|
||||
|
||||
- 背景:APIMart 的图片生成和 Responses 接口在仓库内分散于 `api-server`、`platform-llm` 和本地 skill 脚本,若只修单点,容易出现不同入口的上游请求体不一致。
|
||||
- 决策:凡是仓库内调用 APIMart 的 OpenAI 兼容接口,请求体统一携带 `official_fallback: true`;其中图片生成请求直接固定写入,`platform-llm` 的 APIMart GPT-5 client 通过显式开关开启,不默认扩散到 Ark 等其它 provider。
|
||||
- 影响范围:`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/puzzle.rs`、`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/platform-llm/src/lib.rs`、`.codex/skills/gpt-image-2-apimart/` 和相关技术文档。
|
||||
- 验证方式:图片生成与 creative-agent APIMart 路径的单测都应断言 `official_fallback` 已写入请求 JSON;编码检查和相关 Rust 测试应持续通过。
|
||||
- 关联文档:`docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md`、`docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md`、`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`。
|
||||
|
||||
## 2026-05-07 server-rs Cargo 依赖集中到 workspace
|
||||
|
||||
- 背景:`server-rs` 多 crate 已稳定成 DDD workspace,成员 `Cargo.toml` 中重复散写第三方版本和本地 path 依赖,升级 SpacetimeDB SDK、`serde`、`reqwest`、`tokio` 等依赖时容易漂移。
|
||||
- 决策:`server-rs/Cargo.toml` 的 `[workspace.dependencies]` 统一维护第三方依赖版本和 workspace 内部 crate path;成员 crate 默认使用 `{ workspace = true }`,只保留自身 feature、optional 或 target-specific 差异;不再新增 `sha1`,OSS 与阿里云 OpenAPI 签名统一走 `sha2::Sha256` 对应的 V4/V3 口径。
|
||||
- 影响范围:`server-rs/Cargo.toml`、所有 `server-rs/crates/*/Cargo.toml`、`platform-oss`、`platform-auth`、后续新增 Rust crate 或新增 Rust 依赖的开发流程。
|
||||
- 验证方式:修改 Cargo 配置后先执行 `cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps`,再按影响范围执行 `cargo check`、DDD 边界检查和编码检查。
|
||||
- 关联文档:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 资料页反馈提交必须走 Rust 后端与 SpacetimeDB
|
||||
|
||||
- 背景:`/profile/feedback` 首版页面曾只做前端成功态,无法沉淀到用户账号和数据库,也容易与主站平台主题脱节。
|
||||
- 决策:反馈提交统一走鉴权 HTTP 路由 `POST /api/profile/feedback`,由 `api-server` 取当前 access token 用户,调用 `spacetime-client` facade,再通过 `spacetime-module` procedure 写入私有表 `profile_feedback_submission`;前端只负责输入采集、Data URL 预览和提交元数据,不再保存 `File[]` 作为外部契约。
|
||||
- 影响范围:`src/components/platform-entry/PlatformFeedbackView.tsx`、`src/services/rpg-entry/rpgProfileClient.ts`、`packages/shared/src/contracts/runtime.ts`、`server-rs/crates/shared-contracts`、`api-server`、`module-runtime`、`spacetime-client`、`spacetime-module`、表目录与 bindings。
|
||||
- 验证方式:前端定向测试应覆盖 Data URL 预览与 `/api/profile/feedback` 请求体;后端变更需同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和生成绑定;API smoke 使用 `npm run api-server` 和 `/healthz`。
|
||||
- 关联文档:`docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`、`docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md`。
|
||||
|
||||
## 2026-05-06 Maincloud 历史残留引用禁止再使用
|
||||
|
||||
- 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
||||
- 决策:`maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求全部视为历史残留,后续禁止新增、运行或引用;后端 API smoke 统一使用 `npm run api-server` 并检查 `/healthz`。
|
||||
- 影响范围:`AGENTS.md`、`docs/technical/`、`.hermes/shared-memory/`、后端启动脚本、测试支撑和所有后续工程文档。
|
||||
- 验证方式:新增或修改后端相关文档时,检查不得要求 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`;触碰历史残留时同步删除或改名。
|
||||
- 关联文档:`docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`、`docs/technical/SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md`。
|
||||
|
||||
## 2026-05-05 新手引导首版复用拼图本地运行时
|
||||
|
||||
- 背景:首次打开产品的新用户需要先体验输入想法、生成拼图、通关、登录保存、回到首页的闭环,但首版不应引入新的持久化表或独立玩法运行时。
|
||||
- 决策:未登录首次访问由前端 localStorage 标记触发;生成入口走公开 BFF `POST /api/runtime/puzzle/onboarding/generate` 生成 1 关临时拼图;登录后保存走鉴权 BFF `POST /api/runtime/puzzle/onboarding/save`,由服务端创建当前用户拼图 agent session 并更新其草稿作品 profile;游玩阶段复用现有本地拼图运行时。
|
||||
- 影响范围:平台入口首屏、新手引导 PRD、拼图 BFF、拼图作品契约与前端 puzzle runtime。
|
||||
- 验证方式:未登录首次访问应展示新手引导;生成后只进入 1 关本地拼图;通关后登录保存应在当前用户拼图作品架出现草稿作品;不应产生 SpacetimeDB schema 变更。
|
||||
- 关联文档:`docs/prd/FIRST_LAUNCH_PUZZLE_ONBOARDING_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 text-game 作为百梦幕间文字游戏模板接入
|
||||
|
||||
- 背景:团队希望参考 MOKU / 幕间类 AI 文游,设计可在百梦内落地的 AI 文字游戏模板,但不能把外部平台社区、支付、榜单、论坛、账号或私有存档迁入 Genarrative。
|
||||
- 决策:新增 `text-game` 作为百梦 AI 原生文字游戏模板口径,展示名可用“幕间”或“幕间文字”;它与 `visual-novel` 分离,重点是 AI GM、自由行动、状态后果、长期记忆、章节目标和轻量剧本模拟器;入口、作品、发布、资产、钱包、埋点、存档和广场全部复用百梦平台接口;禁止新增 replay、外部社区、外部支付、外部榜单和私有存档系统。
|
||||
- 影响范围:后续 `text-game` shared contracts、`module-text-game`、SpacetimeDB 表、`api-server` 路由、前端入口 / workspace / result / runtime、平台作品架和发现聚合。
|
||||
- 验证方式:后续落地时确认路由使用 `/api/creation/text-game/*` 与 `/api/runtime/text-game/*`;确认正式业务真相在 Rust / SpacetimeDB 后端;确认没有 `replay` 能力和外部平台功能误入;确认 `text-game` 不复用 `visual-novel` step 契约作为运行态真相。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 2048 玩法模板采用 `twenty-forty-eight` 工程域
|
||||
|
||||
- 背景:平台计划新增 2048 游戏玩法模板,需要同时适配前端 stage、HTTP 路由、Rust 模块、SpacetimeDB 表和公开作品号;裸 `2048` 不适合作为模块或文件命名前缀。
|
||||
- 决策:面向用户展示名保持 `2048`,工程玩法 ID 固定为 `twenty-forty-eight`,Rust 模块与表前缀使用 `twenty_forty_eight`,公开作品号前缀使用 `TF-`;玩法按完整闭环设计,包含 Agent 创作、结果页、试玩、发布、公开运行、后端棋盘裁决、排行榜和作品架 / 广场接入。
|
||||
- 影响范围:后续 SpacetimeDB 创作入口配置、平台 `SelectionStage`、前端 `twenty-forty-eight-*` 组件与 service、`module-twenty-forty-eight`、`shared-contracts`、`spacetime-module` 表、`spacetime-client` facade、`api-server` 路由、作品号和 PRD 索引。
|
||||
- 验证方式:后续落地时确认用户可见标题为 `2048`,代码、路由和表统一使用 `twenty-forty-eight` / `twenty_forty_eight`;移动、合并、生成新方块、目标达成、失败和榜单成绩由后端正式裁决,前端不伪造分数或目标达成。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_2048_GAMEPLAY_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 幸存者类玩法作为平台模板接入
|
||||
|
||||
- 背景:平台继续扩展新玩法模板,需要把幸存者 / 割草 / 轻度 Roguelite 类玩法纳入统一创作中心、作品架、广场和运行态体系,避免再起一套独立小游戏工程。
|
||||
- 决策:新增 `survivor` 作为 Genarrative 平台玩法模板,统一使用 `server-rs + Axum + SpacetimeDB`,创作端、结果页、试玩、发布和运行态都复用平台接口;前端只负责表现和高频模拟,不承接正式规则真相。
|
||||
- 影响范围:`docs/prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md`、后续 `survivor` shared contracts、前端入口 / result / runtime、`server-rs` DDD 分层、SpacetimeDB 表设计和平台作品闭环。
|
||||
- 验证方式:后续落地时检查 `survivor` 入口、session、work profile、runtime run、checkpoint、升级候选和结算接口是否都落在平台统一链路内,并确认没有新增独立小游戏壳层。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 TXT 玩法只作为平台模板接入且删除回放
|
||||
|
||||
- 背景:`Interactive-fiction-backend` 与 `Interactive-fiction-frontend` 是完整平台类工程,其中 TXT / Galgame 玩法可借鉴,但账号、商城、后台、公开市场、回放等平台能力不能迁入 Genarrative。
|
||||
- 决策:`visual-novel` 只作为 Genarrative 视觉小说模板接入,保留想法 / 文档 / 空白创建、世界观 / 角色 / 场景 / 剧情阶段编辑、视觉小说 step 运行时、历史和重生成等模板能力;入口、作品、发布、资产、钱包、存档和广场全部使用 Genarrative 平台接口;彻底删除回放、分享回放、回放编译、回放路由、回放表和回放 UI。
|
||||
- 影响范围:视觉小说 PRD、旧 TXT 文档口径、后续 `visual-novel` shared contracts、前端入口 / result / runtime、`server-rs` DDD 分层、SpacetimeDB 表设计和平台存档接入。
|
||||
- 验证方式:后续落地时扫描前端、后端、契约、表和文档,确认不存在 `replay` 能力;确认视觉小说没有迁入外部平台账号、订单、会员、促销、后台、公开市场或私有存档系统;确认后端落在 `server-rs + Axum + SpacetimeDB`。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/prd/TXT_MODE_CORE_GAMEPLAY_PRD_2026-04-20.md`、`docs/technical/TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 VN-02 表与 spacetime-client facade 收口
|
||||
|
||||
- 背景:`visual-novel` 后续 API、创作工作台和运行时需要稳定的 SpacetimeDB schema 与 Rust facade,且必须延续“无回放、无私有存档”的产品边界。
|
||||
- 决策:视觉小说首批数据库只落六张表:`visual_novel_agent_session`、`visual_novel_agent_message`、`visual_novel_work_profile`、`visual_novel_runtime_run`、`visual_novel_runtime_history_entry`、`visual_novel_runtime_event`;`visual_novel_runtime_event` 是 `public event` 审计事件表,不是 replay 数据源;运行历史只保存继续体验与历史重生成需要的 typed step 和快照哈希。`api-server` 后续接入必须经 `spacetime-client/src/visual_novel.rs` typed facade,不直接依赖生成 bindings。
|
||||
- 影响范围:`server-rs/crates/spacetime-module/src/visual_novel.rs`、`migration.rs`、`server-rs/crates/spacetime-client/src/visual_novel.rs`、`module_bindings/`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`、VN-05 API 联调。
|
||||
- 验证方式:执行 `npm run spacetime:generate -- --rust-only`、`cargo check -p spacetime-module`、`cargo check -p spacetime-client`、`npm run check:encoding`;扫描视觉小说 schema / facade / 表目录确认没有 `replay` 表、路由或私有 save 表。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## 2026-05-05 视觉小说 VN-07 前端创作闭环按阶段边界落地
|
||||
|
||||
- 背景:`visual-novel` 模板需要先完成创作工作台与结果页,真实生成和正式玩家 runtime 仍依赖 VN-05 后端路由。
|
||||
- 决策:VN-07 前端只接入口、Agent 工作台、可编辑 `VisualNovelResultDraft` 结果页和测试 run;`blank` 起点直接生成本地空白草稿进入结果页,`idea` / `document` 继续调用 `/api/creation/visual-novel/sessions`;结果页保存先更新当前 session 草稿,显式“编译草稿”才调用 `/compile`,测试 run 在真实 runtime 不可用时降级为本地 test run。
|
||||
- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/visual-novel-creation/`、`src/components/visual-novel-result/`、`packages/shared/src/contracts/visualNovel.ts`、视觉小说 PRD。
|
||||
- 验证方式:执行前端 typecheck、视觉小说工作台 / 结果页定向测试和编码检查;确认未新增 replay、作品聚合或正式 runtime 能力。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-11 负向扫描门禁
|
||||
|
||||
- 背景:视觉小说 TXT 模板进入收口后,需要一个可重复执行的守门方式,避免工程代码误入回放能力或外部平台功能。
|
||||
- 决策:新增 `npm run check:visual-novel-vn11`,由 `scripts/check-visual-novel-vn11-negative-scan.mjs` 扫描 `src/`、`packages/shared/src/`、`server-rs/crates/`、`docs/` 与 `.hermes/shared-memory/`;工程代码中不允许出现 replay / 回放 / 录制 / 复盘类直出命中;外部平台能力误入只在视觉小说实现路径内检查,避免把平台已有账号、会员、后台等能力误判为视觉小说迁入。
|
||||
- 影响范围:视觉小说 VN-11 验收、后续 `visual-novel` 增量改动、同类新玩法负向扫描脚本。
|
||||
- 验证方式:执行 `npm run check:visual-novel-vn11`,报告写入 `docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`;当前扫描结论为工程代码无回放类直出命中,视觉小说实现路径无外部平台能力误入。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-12 采用单独验收门禁脚本
|
||||
|
||||
- 背景:VN-12 是视觉小说模板的全链路联调与自动化验收收口任务,需要把关键路径、API smoke、前端测试和报告输出固化成可复跑门禁,避免后续改动只靠手工口述结论。
|
||||
- 决策:新增 `npm run check:visual-novel-vn12`,由 `scripts/check-visual-novel-vn12-acceptance.mjs` 校验 PRD、VN-11 报告、关键前端测试、视觉小说 service client、`api-server` / `module-visual-novel` / `shared-contracts` 相关文件和路由命中,并生成 `docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md`。
|
||||
- 影响范围:VN-12 验收、视觉小说后续回归、同类玩法的收口门禁模式。
|
||||
- 验证方式:执行 `npm run check:visual-novel-vn12 -- --write-report`,报告应覆盖自动化验收清单、API smoke、前端关键路径、桌面/移动端检查说明和已执行命令;若脚本失败,直接回流到对应 owner 修复。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/audits/VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-13 文档与交接收口
|
||||
|
||||
- 背景:视觉小说模板主链已经落地完成,需要把 PRD、表目录、prompt 工具说明、负向扫描报告和维护经验收成新开发者可直接接手的一组文档,避免后续仍回头查旧 TXT 迁移方案。
|
||||
- 决策:视觉小说后续维护的正式入口固定为 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`SPACETIMEDB_TABLE_CATALOG.md`、`VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`、`VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md` 和 `VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`;旧 TXT 迁移文档仅保留历史参考地位。
|
||||
- 影响范围:视觉小说 PRD 收口、技术文档索引、经验文档索引、Hermes 共享记忆和后续维护阅读顺序。
|
||||
- 验证方式:打开上述文档即可获得当前实现边界、表目录、Prompt 口径、负向扫描和维护经验;后续维护不需要把旧 TXT 平台工程文档重新当作实现目标。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`docs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md`。
|
||||
|
||||
## 2026-05-05 平台移动端一级 Tab 改为推荐/发现/创作/草稿/我的
|
||||
|
||||
- 背景:移动端平台入口需要从旧“首页/排行/创作/存档/我的”调整为更直接的推荐流和应用式底部导航。
|
||||
- 决策:前端内部继续复用 `PlatformHomeTab` 的 `home/category/create/saves/profile` 状态值,但用户看到的一级 Tab 分别为“推荐/发现/创作/草稿/我的”;`home` 直接展示公开推荐流,`category` 承载发现页及排行子 Tab,`saves` 承载草稿作品架,原存档结构并入“我的-玩过”弹层。
|
||||
- 影响范围:平台入口导航、移动端推荐页、发现页子 Tab、创作中心作品架、个人页玩过弹层、相关设计文档。
|
||||
- 验证方式:检查移动端底部导航文案和顺序,确认登录态为“推荐/发现/创作/草稿/我的”,未登录态为“推荐/创作/发现”且创作居中;“推荐”无搜索/频道栏直出作品流,“发现”包含搜索/推荐/今日/分类/排行,“创作”只显示新建入口,“草稿”显示作品架,“我的-玩过”可恢复存档。
|
||||
- 关联文档:`docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 创作 Tab 固定为智能创作首页,草稿 Tab 承接旧作品架
|
||||
|
||||
- 背景:创作首页需要变成面向对话式生成的智能创作页,旧模板卡和作品架继续保留但不应再占据创作首屏。
|
||||
- 决策:`create` 只承载 `CreativeAgentHome` 智能创作首页与会话流,顶部品牌栏、问候、快捷胶囊、底部输入框和左侧抽屉是主结构;旧的新建作品类型卡不再在 `create` 里展示。原本的 RPG / 拼图 / 大鱼 / Match3D / 方洞 / 视觉小说作品架统一归到 `saves` 草稿 Tab。
|
||||
- 影响范围:平台创作页布局、创作首页抽屉、草稿页作品架、相关交互测试、旧创作入口 helper。
|
||||
- 验证方式:移动端点击“创作”直接看到智能创作首页;点击“草稿”看到旧作品架;旧模板入口不再从创作页出现。
|
||||
- 关联文档:`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 创意互动内容生成 Agent 采用 LangChain-Rust 六模块闭环
|
||||
|
||||
- 背景:需要支持用户输入文字、图片或文档后,先理解创作意图,再从多个模板候选中选择一个,并把内容填入拼图等目标玩法草稿契约中。
|
||||
- 决策:新增方案改为基于 LangChain-Rust 的六模块 Agent 架构,核心模块是感知、思考、记忆、行动、反思、协作;首版只支持拼图玩法,但必须先展示多个拼图子模板候选,用户选择某个模板后,再确认该模板下的关卡模式、关卡数和预计积分范围,确认后才生成草稿;Agent 理解、规划和修订统一使用 APIMart Responses `gpt-5` 并支持文本/图像多模态输入;Agent 创作方式就是填充和修订模板草稿字段,表单化创作页与 Agent 自然语言修订都操作同一份 `PuzzleResultDraft`,且草稿可编辑字段只收敛为 `workTitle`、`workDescription`、`workTags`、`levels[].levelName`、`levels[].pictureDescription`、`levels[].pictureReference`;其中 `pictureReference` 已采用 `PuzzleDraftLevel.pictureReference` / Rust `picture_reference` 正式字段方案,不再走 metadata 过渡;单关卡/多关卡图片生成通过拼图模块 Tool 与模板协议实现;生成好的内容必须可立即试玩。
|
||||
- 影响范围:创作中心入口、`platform-agent`、`module-creative-agent`、`module-puzzle` 拼图模板协议和工具、`shared-contracts`、`api-server` creative facade、SpacetimeDB creative agent 表、拼图玩法工具。
|
||||
- 验证方式:后续落地时以创意互动内容生成 Agent 技术方案和 Phase 1 PRD 为编码依据,优先完成拼图 Phase 1,并执行 shared contracts、module、platform-agent、api-server、前端 typecheck 与编码检查。
|
||||
- 关联文档:`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`、`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 creative-agent Task C 首版平台 PoC 已落地
|
||||
|
||||
- 背景:Phase 1 的平台侧需要先把 LangChain-Rust 适配层、APIMart `gpt-5` 多模态 Responses 请求和工具注册边界立起来,才能继续接 API facade。
|
||||
- 决策:新增 `server-rs/crates/platform-agent` 作为独立 workspace crate,保留项目自有 `CreativeAgentExecutor`、工具注册表、回调事件和 mock executor;`platform-llm` 的 Responses 请求体扩展为可序列化 `input_text` / `input_image` content part。
|
||||
- 影响范围:`server-rs/Cargo.toml`、`server-rs/crates/platform-agent`、`server-rs/crates/platform-llm`、任务 C 的后续 API / SSE 接入。
|
||||
- 验证方式:`cargo check -p platform-agent`、`cargo test -p platform-agent`、`cargo test -p platform-llm responses_multimodal` 已通过。
|
||||
- 关联文档:`docs/technical/CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md`。
|
||||
|
||||
## 2026-05-05 creative-agent Task E API / SSE facade 已落地
|
||||
|
||||
- 背景:Phase 1 需要先把创意 Agent 的 HTTP/SSE 门面接入 Rust `api-server`,用于前端工作区调用和拼图模板确认闭环。
|
||||
- 决策:`api-server` 挂载 `/api/runtime/creative-agent/*` 六个鉴权路由;creative session 在 Task D 表未收口前暂存在 `api-server` 运行态并按 authenticated user 校验 owner;未确认模板前不创建拼图 session,`confirm-template` 后才通过既有 `spacetime-client` 创建/编译 `puzzle_agent_session`;`gpt-5` 请求只从 `APIMART_BASE_URL` / `APIMART_API_KEY` 构造专用 Responses client,不复用通用 `GENARRATIVE_LLM_API_KEY`。
|
||||
- 影响范围:`server-rs/crates/api-server/src/creative_agent.rs`、`creative_agent_sse.rs`、`app.rs`、`state.rs`、`module-puzzle` creative template/tool、Phase 1 PRD。
|
||||
- 验证方式:`cargo check -p api-server`、`cargo test -p module-puzzle creative`、`cargo test -p api-server creative_agent`、`npm run api-server` 后检查 `/healthz`、`POST /api/runtime/creative-agent/sessions`、`POST /api/runtime/creative-agent/sessions/{sessionId}/messages/stream`。
|
||||
- 关联文档:`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-10 视觉小说入口收敛为单句创作 + 画风选择
|
||||
|
||||
- 背景:视觉小说入口页要对齐抓大鹅式的线性创作入口,只保留最小可用输入,避免再暴露文档 / 空白 / 对话式工作台。
|
||||
- 决策:入口页只展示一句话创作输入框和横向视觉画风卡片;画风通过 `seedText` 追加 `视觉画风` 和 `画风要求` 两行透传给既有创作链路;点击生成后先进入 `visual-novel-generating` 过程页,再自动进入 `visual-novel-result`。画风卡片主视觉固定消费 `public/visual-novel-style-references/` 下由 VectorEngine `gpt-image-2-all` 生成的静态参考图,不在前端运行时现场调用生图接口。
|
||||
- 影响范围:`VisualNovelAgentWorkspace`、`visualNovelEntryGeneration`、`PlatformEntryFlowShellImpl`、视觉小说 PRD 和创作 Tab 设计文档;不新增后端字段或数据库结构。
|
||||
- 验证方式:执行 `npm run test -- VisualNovelAgentWorkspace`、视觉小说工作台相关 ESLint、`npx prettier --check` 和 `npm run check:encoding`;`npm run typecheck` 若失败需先区分是否来自无关 Match3D / RPG 既有改动。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。
|
||||
|
||||
## 2026-05-10 用户标签只做后端白名单投影
|
||||
|
||||
- 背景:运营邀请码需要给账号打标签,但标签默认不能暴露到前端通用用户资料;拼图排行榜仅需展示特定标签。
|
||||
- 决策:`user_account.user_tags` 保存账号标签,数据库默认 `None`,业务按空数组读取;后台预置邀请码使用后授予的标签不再使用独立列,统一存放并解析自 `profile_invite_code.metadata_json.userTags`,兼容读取 `user_tags`。通用登录态和个人资料不返回原始标签。首版只在拼图排行榜 `visibleTags` 中白名单投影 `北科`。
|
||||
- 影响范围:用户认证表、邀请码后台、邀请兑换事务、拼图排行榜响应和 UI。
|
||||
- 验证方式:表结构变更需同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 SpacetimeDB bindings;后端运行 `cargo check -p api-server`,后台运行 `npm run admin-web:typecheck`。
|
||||
- 关联文档:`docs/technical/USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.md`。
|
||||
|
||||
## 2026-05-10 抓大鹅草稿元信息由 gpt-4o 生成
|
||||
|
||||
- 背景:抓大鹅草稿生成需要基于入口题材设定生成作品名称,结果页作品信息要对齐拼图草稿,不再把封面和作品名称拆成两个模块。
|
||||
- 决策:`match3d_compile_draft` 使用 `gpt-4o` 生成 `gameName` 与 3 到 6 个标签;`summary` 默认保持空字符串;标签可由结果页 `作品信息` Tab 手动编辑或再次 AI 生成。草稿生成会按难度产出多视角 2D 物品图片并写入 `generated_item_assets_json`,运行态必须优先消费 `generatedItemAssets[].imageViews[]`,默认积木只做兜底。
|
||||
- 影响范围:`api-server` Match3D 编译、Match3D works 标签接口、结果页 `作品信息` 与 `素材配置` Tab、运行态 `Match3DRuntimeShell` / `Match3DPhysicsBoard`、生成进度和 Match3D 技术文档。
|
||||
- 验证方式:执行 `npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`,并用 `npm run api-server` 检查 `/healthz`。
|
||||
- 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`;`docs/technical/MATCH3D_RODIN_ASSET_TAB_2026-05-10.md` 仅作历史参考。
|
||||
|
||||
## 2026-05-12 抓大鹅物品种类从消除次数中拆出并改为 2D 五视角素材
|
||||
|
||||
- 背景:结果页草稿素材已经能生成和预览,但标准 / 硬核难度仍可能按 `clearCount` 误判需要 12 / 20 种素材,且继续生产 GLB 会拉长草稿生成耗时。
|
||||
- 决策:难度配置统一使用 `物品种类`:轻松 3、标准 9、进阶 15、硬核 21;历史硬核 `clearCount=20` 在运行态升为 21 组三消。新草稿和批量新增不再调用 Rodin、不再生成 GLB。每个物品生成 5 个不同 2D 视角,单张 1K 素材图固定按 5x5 切割,最多承载 5 个物品;超过 5 个物品时由 `api-server` 自动分批并行生图。发布必须校验已生成 `image_ready` 且有 `imageViews[]` 或首图引用的素材数量满足当前难度;试玩通过 `itemTypeCountOverride` 自动降到可用 2D 素材数量。历史模型字段只作为旧数据兼容,不再进入新生产链路。
|
||||
- 影响范围:Match3D 结果页、运行态启动契约、`module-match3d` 初始 run 生成、SpacetimeDB start input / restart、发布校验和 Match3D 技术文档。
|
||||
- 验证方式:`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`cargo test -p module-match3d --manifest-path server-rs\Cargo.toml`、相关后端 check / tests。
|
||||
- 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 2026-05-07 移动端整页缩放由入口统一锁定
|
||||
|
||||
- 背景:移动端游戏式页面如果允许浏览器整页缩放,容易把固定画布、HUD 和底部操作区一起放大或缩小,破坏操作节奏。
|
||||
- 决策:主站入口统一使用 `viewport` 锁定 `minimum-scale=1.0`、`maximum-scale=1.0`、`user-scalable=no` 和 `viewport-fit=cover`,并在应用启动时调用 `lockMobileViewportZoom()` 拦截 iOS `gesture*` 与多指 `touchmove` 触发的页面级缩放。
|
||||
- 影响范围:主站 `index.html`、`src/main.tsx`、后续所有依赖主入口的移动端游戏/画布页面;不要求每个画布组件重复实现缩放锁定。
|
||||
- 验证方式:移动端打开主站后,双指捏合和快速双击不应再缩放整页;单指滚动、点击和组件内交互保持正常。
|
||||
- 关联文档:`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。
|
||||
|
||||
## 2026-05-07 视觉小说 VN-10 资产引用统一走平台资产对象
|
||||
|
||||
- 背景:视觉小说文档输入、封面、场景背景、角色立绘和音乐需要接入平台资产链路,不能在前端状态或 SpacetimeDB 中保存大 Data URL、二进制对象或外部 R2 路径。
|
||||
- 决策:VN 上传统一复用 `/api/assets/direct-upload-tickets`、OSS 直传、`/api/assets/objects/confirm`、`/api/assets/read-url`。文档上传后只把 `assetObjectId` 放入 `sourceAssetIds`,`seedText` 仅放截断摘要;封面、场景、角色、音乐只写 `/generated-*` 引用和平台 asset id。角色立绘写入 `imageAssets[].source = platform_asset`。运行时图片渲染统一使用 `ResolvedAssetImage` 换签。
|
||||
- 影响范围:`src/services/visual-novel-creation/visualNovelAssetClient.ts`、`VisualNovelAgentWorkspace`、`VisualNovelResultView`、`VisualNovelRuntimeShell`、`server-rs/crates/api-server/src/visual_novel.rs`。
|
||||
- 验证方式:VN 定向前端测试、`npm run typecheck`、`npm run check:encoding`、`cargo test -p api-server visual_novel`、`cargo test -p api-server creation_agent_document_input`。
|
||||
- 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 2026-05-04 在仓库 `.hermes/` 中建立团队共享记忆
|
||||
|
||||
- 背景:团队有 3 名开发人员,均在各自本地安装 Hermes,并需要独立拉取仓库、修改代码、本地测试;团队希望形成共享的长期项目记忆。
|
||||
- 决策:不共享个人 `~/.hermes`,先在 Genarrative 仓库内使用 `.hermes/` 保存可 Git 同步的团队共享记忆、计划和未来 skills。
|
||||
- 影响范围:`AGENTS.md`、`.hermes/README.md`、`.hermes/shared-memory/`。
|
||||
- 验证方式:任一开发者拉取仓库后,在项目根目录启动 Hermes,均可读取同一套 `.hermes/shared-memory/` 文件。
|
||||
- 关联文档:`.hermes/README.md`、`.hermes/shared-memory/team-conventions.md`。
|
||||
|
||||
## 2026-04-25 后端唯一落地口径固定为 Rust / SpacetimeDB
|
||||
|
||||
- 背景:项目经历过 Node/Express/PostgreSQL、Go 试验、Rust/SpacetimeDB 等多条后端路线,旧路线文档容易造成开发歧义。
|
||||
- 决策:新功能以后端当前基线为准:HTTP 门面使用 Rust `api-server` / Axum,业务真相使用 SpacetimeDB,领域和契约在 `server-rs` 多 crate 分层维护。
|
||||
- 影响范围:所有后端、数据真相、运行时状态、创作结果、用户系统、资产、任务、埋点、后台 API 等相关开发。
|
||||
- 验证方式:开发前优先阅读 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`;旧 `server-node`、Express、PostgreSQL、Go 方向只允许作为迁移参考。
|
||||
- 关联文档:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`、`AGENTS.md`。
|
||||
|
||||
## 2026-04-28/29 server-rs DDD 分层与契约矩阵冻结
|
||||
|
||||
- 背景:server-rs 模块多、上下文多,需防止领域规则、SpacetimeDB 表、HTTP BFF、前端临时逻辑互相污染。
|
||||
- 决策:按 DDD 总纲和 G1 契约/路由矩阵开发:`module-*` 承载领域,`spacetime-module` 承载表和事务,`spacetime-client` 承载 facade,`api-server` 承载 HTTP/SSE/BFF,`platform-*` 承载外部副作用,`shared-contracts` 承载 DTO。
|
||||
- 影响范围:server-rs 全部 crate、前端 API client、SpacetimeDB schema、旧接口清理。
|
||||
- 验证方式:执行任务前对照 DDD 总纲、并行任务清单、G1 矩阵;提交前运行相关 DDD 边界检查和定向测试。
|
||||
- 关联文档:`SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`、`SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`、`SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`。
|
||||
|
||||
## SpacetimeDB 表结构变更必须显式维护迁移与表目录
|
||||
|
||||
- 背景:SpacetimeDB 的 schema 迁移模型不同于 PostgreSQL,部分变更会触发冲突或拒绝自动迁移。
|
||||
- 决策:凡涉及 table、reducer、procedure、row shape 或 binding 变化,必须同步 `migration.rs`、表目录和生成绑定;涉及 private 表迁移时按 JSON 导入导出和分片导入流程处理。
|
||||
- 影响范围:`server-rs/crates/spacetime-module`、`spacetime-client` bindings、`SPACETIMEDB_TABLE_CATALOG.md`、部署/发布脚本。
|
||||
- 验证方式:发布前检查 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 清单,更新 `SPACETIMEDB_TABLE_CATALOG.md`,执行生成绑定和相关测试。
|
||||
- 关联文档:`SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`SPACETIMEDB_TABLE_CATALOG.md`、`SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md`。
|
||||
|
||||
## 生产部署切换到 systemd + Nginx + 自托管 SpacetimeDB
|
||||
|
||||
- 背景:旧一体化启动脚本和历史 Jenkinsfile 已不再是生产发布唯一入口。
|
||||
- 决策:生产部署以 systemd 托管 SpacetimeDB 与 Rust `api-server`,Nginx 负责站点和代理,生产 Jenkinsfile 按 web/api/stdB module/build/deploy/publish 拆分。
|
||||
- 影响范围:部署脚本、服务器目录、维护模式、Jenkins、Nginx、systemd 服务。
|
||||
- 验证方式:生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
- 关联文档:`PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## 个人任务与埋点首版边界冻结
|
||||
|
||||
- 背景:“我的”Tab、任务、奖励、钱包和埋点涉及用户、运营、分析多条链路,需要避免范围泛化。
|
||||
- 决策:埋点原始事实进入 `tracking_event`,聚合投影进入 `tracking_daily_stat`;个人任务配置/进度/领奖/钱包分别进入 `profile_task_config`、`profile_task_progress`、`profile_task_reward_claim`、`profile_wallet_ledger`;首版个人任务 scope 仅支持 `user`。
|
||||
- 影响范围:用户侧任务中心、后台任务配置、运营查询、埋点查询、钱包流水。
|
||||
- 验证方式:非 `user` scope 的个人任务配置应被 API 和领域构造层拒绝;任务查询与埋点查询分别放在 `docs/operations/` 和 `docs/tracking/`。
|
||||
- 关联文档:`PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`、`RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。
|
||||
208
.hermes/shared-memory/development-workflow.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 开发工作流
|
||||
|
||||
> 用途:给本地 Hermes 和开发人员提供统一的开发、测试、提交流程。具体命令以 `package.json`、`server-rs/Cargo.toml`、`AGENTS.md` 和相关 `docs/` 最新文档为准。
|
||||
|
||||
## 标准任务流程
|
||||
|
||||
```text
|
||||
同步代码 → 读取 AGENTS.md → 读取 .hermes/shared-memory → 查找/完善 docs → 制定计划 → 小步实现 → 本地验证 → 更新文档/记忆 → 提交
|
||||
```
|
||||
|
||||
## 建议启动方式
|
||||
|
||||
在项目根目录启动 Hermes:
|
||||
|
||||
```bash
|
||||
cd /path/to/Genarrative
|
||||
hermes
|
||||
```
|
||||
|
||||
在本机当前常见路径为:
|
||||
|
||||
```bash
|
||||
/home/dsk/workspace/Genarrative
|
||||
```
|
||||
|
||||
其他开发者以自己本地实际路径为准,不要把个人绝对路径写入共享文档作为通用规则。
|
||||
|
||||
## 开发前检查清单
|
||||
|
||||
- [ ] 当前分支是否正确
|
||||
- [ ] 是否已拉取最新代码
|
||||
- [ ] 是否阅读 `AGENTS.md`
|
||||
- [ ] 是否阅读 `.hermes/shared-memory/` 相关文件
|
||||
- [ ] 是否阅读 `README.md` 中的运行和检查命令
|
||||
- [ ] 是否阅读 `docs/README.md` 及任务相关分类 README
|
||||
- [ ] 是否存在足够具体的 PRD / 设计 / 技术文档
|
||||
- [ ] 是否明确测试、验收和文档更新方式
|
||||
|
||||
## 本地运行命令
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
完整联调开发环境:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
该命令会启动:
|
||||
|
||||
- SpacetimeDB standalone
|
||||
- Rust `api-server`
|
||||
- 主站 Vite
|
||||
- 后台 Vite
|
||||
|
||||
单独启动前端:
|
||||
|
||||
```bash
|
||||
npm run dev:web
|
||||
```
|
||||
|
||||
单独启动 Rust API server:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
查看本地 Rust/SpacetimeDB 日志:
|
||||
|
||||
```bash
|
||||
npm run dev:rust:logs
|
||||
```
|
||||
|
||||
后台管理前端:
|
||||
|
||||
```bash
|
||||
npm run admin-web:dev
|
||||
npm run admin-web:build
|
||||
npm run admin-web:typecheck
|
||||
```
|
||||
|
||||
SpacetimeDB bindings 生成:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
```
|
||||
|
||||
## 常用检查命令
|
||||
|
||||
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。
|
||||
|
||||
编码检查:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
ESLint:
|
||||
|
||||
```bash
|
||||
npm run lint:eslint
|
||||
```
|
||||
|
||||
类型检查:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
综合 lint:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
测试:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
生产构建:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
内容检查:
|
||||
|
||||
```bash
|
||||
npm run check:data
|
||||
npm run check:overrides
|
||||
npm run check:smoke
|
||||
npm run check:content
|
||||
```
|
||||
|
||||
全量检查:
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
DDD 边界检查:
|
||||
|
||||
```bash
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
## 后端相关默认验证
|
||||
|
||||
后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时:
|
||||
|
||||
- 使用 `npm run api-server` 重新拉起后端。
|
||||
- 禁止使用 `npm run api-server:maincloud`、`npm.cmd run api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径;这些只属于历史残留。
|
||||
- 检查 `/healthz`。
|
||||
- 执行对应自动测试。
|
||||
- 涉及 SpacetimeDB 表、reducer、procedure、row shape 或绑定变化时,同步更新 `migration.rs`、表目录和生成绑定。
|
||||
|
||||
关键文档:
|
||||
|
||||
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
- `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
- `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
|
||||
- `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
|
||||
- `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||
- `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
|
||||
- `docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`
|
||||
|
||||
## 前端相关默认验证
|
||||
|
||||
前端修改后,应根据修改范围选择:
|
||||
|
||||
- `npm run check:encoding`
|
||||
- `npm run lint:eslint`
|
||||
- `npm run typecheck`
|
||||
- `npm run test`
|
||||
- 页面交互 smoke
|
||||
- 移动端视口检查
|
||||
|
||||
前端原则:
|
||||
|
||||
- 移动端优先,再兼容网页端。
|
||||
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
|
||||
- 创作中心入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;前端只在 `platformEntryCreationTypes.ts` 做展示派生,api-server 路由熔断也使用同一份配置,禁止恢复前端硬编码入口配置文件。
|
||||
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
|
||||
- 不在 UI 中默认写功能说明类文本。
|
||||
- 弹出独立面板的交互不要实现成在当前面板下方追加内容。
|
||||
|
||||
## 文档更新规则
|
||||
|
||||
- 工程修改要同步更新对应文档。
|
||||
- 如果没有现成文档,新文档统一放入 `docs/` 下合适分类。
|
||||
- `.hermes/shared-memory/` 只记录高频、长期、团队共享的摘要和索引,不替代完整 PRD/技术文档。
|
||||
- 如果 `.hermes/shared-memory/` 与代码或 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正共享记忆。
|
||||
|
||||
## 提交前建议让 Hermes 执行
|
||||
|
||||
```text
|
||||
请检查当前 git diff,指出:
|
||||
1. 是否违反 AGENTS.md 或 .hermes/shared-memory 约定;
|
||||
2. 是否需要补充 docs;
|
||||
3. 是否有长期知识需要写入 .hermes/shared-memory;
|
||||
4. 建议的测试命令和提交信息。
|
||||
```
|
||||
100
.hermes/shared-memory/document-map.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 文档地图与阅读索引
|
||||
|
||||
> 用途:根据 `README.md`、`AGENTS.md` 和 `docs/` 下文档索引整理团队记忆入口,帮助本地 Hermes 快速选择应该读哪些资料。
|
||||
|
||||
## 全局入口
|
||||
|
||||
| 场景 | 优先阅读 |
|
||||
| --- | --- |
|
||||
| 建立项目背景 | `README.md`、`AGENTS.md`、`.hermes/shared-memory/project-overview.md` |
|
||||
| 找文档分类 | `docs/README.md` |
|
||||
| 开发方法论 | `docs/experience/README.md` |
|
||||
| 查风险与历史问题 | `docs/audits/README.md` |
|
||||
| 做玩法/交互/系统设计 | `docs/design/README.md` |
|
||||
| 做技术实现/后端/部署 | `docs/technical/README.md` |
|
||||
| 排期与拆阶段 | `docs/planning/README.md` |
|
||||
| 查脚本/Function/prompt/职责地图 | `docs/reference/README.md` |
|
||||
| 查埋点 SQL | `docs/tracking/README.md` |
|
||||
| 查运营/任务/钱包对账 SQL | `docs/operations/README.md` |
|
||||
| 查 PRD | `docs/prd/` |
|
||||
|
||||
## docs 分类规则
|
||||
|
||||
- `experience/`:方法论、交接经验、长期有效的开发结论。
|
||||
- `audits/`:现状扫描、问题定位、是否达标的审查类文档。
|
||||
- `design/`:玩法机制、叙事关系、系统结构设计。
|
||||
- `technical/`:技术选型、实现路线、竞品/产品形态拆解。
|
||||
- `planning/`:阶段优先级与推进顺序。
|
||||
- `reference/`:目录、速查、检索辅助。
|
||||
- `tracking/`:埋点原始事实和聚合投影查询。
|
||||
- `operations/`:后台运营核查、对账和排障查询。
|
||||
- `prd/`:产品需求与阶段计划。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
通用复杂任务:
|
||||
|
||||
1. `AGENTS.md`
|
||||
2. `.hermes/shared-memory/`
|
||||
3. `docs/README.md`
|
||||
4. `docs/experience/README.md`
|
||||
5. `docs/audits/README.md`
|
||||
6. 任务对应分类下的 README 和具体文档
|
||||
|
||||
后端 / 数据真相 / SpacetimeDB:
|
||||
|
||||
1. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
2. `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
3. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
|
||||
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
|
||||
5. `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||
6. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
|
||||
7. 具体模块方案文档
|
||||
|
||||
生产部署 / 服务器 / Jenkins:
|
||||
|
||||
1. `docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
|
||||
2. 需要迁移时再看 `SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md`
|
||||
3. 历史 Jenkins / CORS / 本地远端脚本文档只作追溯,不作为当前入口
|
||||
|
||||
RPG 创作与运行时链路:
|
||||
|
||||
1. `docs/reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md`
|
||||
2. `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
|
||||
3. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
|
||||
4. 相关工作包 progress / closure 文档
|
||||
|
||||
移动端 UI / 游戏 UI:
|
||||
|
||||
1. `docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`
|
||||
2. `UI_CODING_STANDARD.md`
|
||||
3. 相关 `docs/design/` 文档
|
||||
|
||||
创作 Agent / 自定义世界:
|
||||
|
||||
1. `docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md`
|
||||
2. `docs/design/CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md`
|
||||
3. `docs/design/CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md`
|
||||
4. `docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md`
|
||||
5. 相关 `SPACETIMEDB_CUSTOM_WORLD_*` 技术方案
|
||||
|
||||
拼图 / 大鱼 / Match3D:
|
||||
|
||||
- 拼图:优先看 `PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md`、`PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md` 和相关 Puzzle 技术文档。
|
||||
- 大鱼吃小鱼:优先看 `BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md` 和相关 Big Fish 技术/经验文档。
|
||||
- 抓大鹅 Match3D:优先看 `docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md`、`MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md` 和相关 Match3D 技术文档。
|
||||
|
||||
个人任务 / 埋点 / 运营查询:
|
||||
|
||||
1. `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
|
||||
2. `docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`
|
||||
3. `docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`
|
||||
4. `docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md`
|
||||
5. `docs/operations/PROFILE_TASK_QUERY_PLAYBOOK_2026-05-03.md`
|
||||
|
||||
## 文档维护规则
|
||||
|
||||
- 新增工程实现时,如果已有对应文档,必须同步更新。
|
||||
- 如果没有对应文档,新文档放入 `docs/` 下合适分类。
|
||||
- `.hermes/shared-memory/` 只保留跨任务、跨成员、高频使用的摘要和索引。
|
||||
- 如果文档与代码冲突,先确认代码事实,再更新过期文档和共享记忆。
|
||||
53
.hermes/shared-memory/handoff-template.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 任务交接模板
|
||||
|
||||
> 用途:当一名开发者把任务交给另一名开发者,或让 Hermes 接续上下文时,复制本模板并填写。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 任务名称:
|
||||
- 负责人:
|
||||
- 当前分支:
|
||||
- 相关需求/Issue:
|
||||
- 相关文档:
|
||||
|
||||
## 背景
|
||||
|
||||
简要说明为什么做这个任务,以及业务/技术目标。
|
||||
|
||||
## 已完成
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## 未完成
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `path/to/file`:说明
|
||||
- `path/to/file`:说明
|
||||
|
||||
## 当前问题/风险
|
||||
|
||||
-
|
||||
|
||||
## 已执行验证
|
||||
|
||||
```bash
|
||||
# 粘贴已执行命令和结果摘要
|
||||
```
|
||||
|
||||
## 建议下一步
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## 是否需要更新团队记忆
|
||||
|
||||
- [ ] 不需要
|
||||
- [ ] 需要,建议更新:`.hermes/shared-memory/...`
|
||||
590
.hermes/shared-memory/pitfalls.md
Normal file
@@ -0,0 +1,590 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
> 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
|
||||
|
||||
## 记录格式
|
||||
|
||||
```md
|
||||
## 问题标题
|
||||
|
||||
- 现象:看到什么错误或异常行为
|
||||
- 原因:确认后的根因
|
||||
- 处理:具体修复步骤
|
||||
- 验证:如何确认修复有效
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## OSS V4 签名时间和 bucket/object_key 兼容
|
||||
|
||||
- 现象:OSS V4 私有读签名在部分时间点失败,可能出现 `OSS V4 签名时间格式化失败` 或服务端判定签名格式错误;排查用例中 bucket 为 `xushi-dev`,object_key 为 `generated-square-hole-assets/.../image.png`。
|
||||
- 原因:旧逻辑依赖 `time::Time::to_string()` 再去掉冒号,小时小于 10 时输出不稳定补零;同时排查时容易把 bucket 名误当成 object_key 的一部分。
|
||||
- 处理:OSS V4 `x-oss-date` 使用固定宽度 `yyyyMMdd'T'HHmmss'Z'` 格式化;调用读签名或 `HEAD Object` 时只传 object_key,不要传 `bucket/object_key` 拼接路径。
|
||||
- 验证:运行 `cd server-rs && cargo test -p platform-oss -- --nocapture`,并用 bucket=`xushi-dev`、object_key=`generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png` 覆盖签名生成。
|
||||
- 关联:`server-rs/crates/platform-oss/src/lib.rs`、`server-rs/crates/platform-oss/README.md`。
|
||||
|
||||
## generated 音频路径进运行态前要先换签
|
||||
|
||||
- 现象:草稿页 audio 控件能播放背景音乐,但拼图或抓大鹅运行态开局后背景音乐不响,Network 可能出现裸 `/generated-*-assets/...mp3` 私有路径 403。
|
||||
- 原因:生成音乐转存到 OSS 私有对象后,`audioSrc` 是 generated legacy path;浏览器 `<audio>` 不能像公开静态资源一样直接请求裸路径。
|
||||
- 处理:运行态隐藏 `<audio>` 设置 `src` 前,先通过 `useResolvedAssetReadUrl` 或 `resolveAssetReadUrl` 换签;播放失败只静默兜底,不阻断局内交互。拼图读取 `currentLevel.backgroundMusic.audioSrc`,抓大鹅读取 `generatedItemAssets[].backgroundMusic.audioSrc`。
|
||||
- 验证:运行态开局后 `<audio loop>` 的 `src` 为签名 URL 或公开 URL;`npm run typecheck` 不报契约字段缺失,后端 run response 带 `backgroundMusic`。
|
||||
- 关联:`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md`。
|
||||
|
||||
## 中文乱码与编码风险
|
||||
|
||||
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
|
||||
- 原因:Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
|
||||
- 处理:
|
||||
- 不要直接沿用乱码文本。
|
||||
- 不要用英文替换中文,除非用户明确要求翻译。
|
||||
- 在 PowerShell 5.1 中显式使用 UTF-8。
|
||||
- 优先用 Python/Node 或 `Get-Content -Encoding UTF8` 核对原文。
|
||||
- 修改中文文件时优先局部补丁,避免无关内容重写。
|
||||
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
|
||||
- 关联:`AGENTS.md`、`npm run check:encoding`。
|
||||
|
||||
## 忘记密码后仍提示手机号或密码错误先查认证快照同步
|
||||
|
||||
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 `Bearer JWT 版本已失效`,日志里的 token version 与本地快照不一致。
|
||||
- 原因:重置/修改密码会更新 `password_hash`、`password_login_enabled` 和 `token_version`,如果 API 层只更新本地 `InMemoryAuthStore`,没有调用 `sync_auth_store_snapshot_to_spacetime()`,`api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态。
|
||||
- 处理:`POST /api/auth/password/change` 与 `POST /api/auth/password/reset` 成功后必须同步认证快照;启动恢复时从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照,本地文件更新时尝试回写 SpacetimeDB。
|
||||
- 验证:执行 `cargo test -p module-auth password --manifest-path server-rs/Cargo.toml` 与 `cargo test -p api-server password --manifest-path server-rs/Cargo.toml`;手测时重设密码后旧密码应失败,新密码应成功,重启后仍应保持。
|
||||
- 关联:`server-rs/crates/api-server/src/password_management.rs`、`server-rs/crates/api-server/src/state.rs`、`docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md`。
|
||||
|
||||
## `.hermes` 只放共享内容,不放个人 Hermes 配置
|
||||
|
||||
- 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
|
||||
- 原因:仓库 `.hermes/` 与个人 `~/.hermes/` 名称相似。
|
||||
- 处理:仓库 `.hermes/` 只放 Markdown 共享记忆、计划和可公开 skills;不提交 `.env`、`config.yaml`、`sessions/`、`auth.json`。
|
||||
- 验证:提交前检查 `git diff -- .hermes`,确认没有密钥、会话记录或个人路径敏感信息。
|
||||
- 关联:`.hermes/README.md`。
|
||||
|
||||
## 儿童动作 Demo 卡在摄像头不可用或挥手不推进先查 mocap 消费链路
|
||||
|
||||
- 现象:`/child-motion-demo` 打开后即使 `http://127.0.0.1:8876/` 已启动,页面仍提示“摄像头暂不可用”,或到“打个招呼”、左右手挥动、站位步骤时真实硬件动作无法检测通过,只能用鼠标拖拽或键盘调试继续。
|
||||
- 原因:浏览器摄像头视频流只是舞台背景;如果热身关把 `getUserMedia` 状态当成主动作数据源,或只在 gesture 阶段消费 `useMocapInput`,就会错过 mocap 的身体中心、动作名和手部坐标。
|
||||
- 处理:确认 `src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 全热身流程启用 `useMocapInput`,页面主提示展示 mocap 动作数据源状态而不是浏览器摄像头状态;确认 `src/services/useMocapInput.ts` 能解析 `/stream` 包里的 `general.body.center_norm`、`actions/action/gesture/gestures/event/name/type`、`hands[]`、`leftHand/rightHand`、`left_hand/right_hand`、左右手标记和 `open_palm/grab` 状态。`/stream` 是 WebSocket,普通 HTTP 访问返回 404 不能当成服务不可用。
|
||||
- 验证:运行 `npx vitest run src\services\useMocapInput.test.ts src\components\child-motion-demo\ChildMotionWarmupDemo.test.tsx`,并在本地硬件服务启动后进入 `/child-motion-demo` 实测站位、招手、左右手挥动和跳跃阶段。
|
||||
- 关联:`src/services/useMocapInput.ts`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## 宝贝识物选篮误触发先查多套判定和残余轨迹
|
||||
|
||||
- 现象:`宝贝识物` 运行态打开礼物盒或反馈结束后,当前物品被连续送入左侧或右侧篮子,或硬件动作名偶发命中导致未做明确横移动作也触发选篮。
|
||||
- 原因:选篮如果同时消费 `wave_left_hand` / `wave_right_hand` / `wave` 动作名和手部轨迹,或在 `correct` / `wrong` 反馈阶段继续累计手部路径,会把抓握、反馈期间残留移动或未知侧别手部误算成下一次选篮。
|
||||
- 处理:宝贝识物选篮只使用明确 `leftHand` / `rightHand` 的连续横向轨迹阈值;侧别为 `unknown` 的手部轨迹不参与选篮;礼物盒打开和反馈阶段清空轨迹,不在非 `active` 阶段累计路径。礼物盒激活仍使用 `open_palm -> grab` 抓握序列。
|
||||
- 补充:当前本地 mocap 的 handedness 是摄像头视角,宝贝识物选篮前需要换算为用户身体视角;`rightHand` 轨迹代表玩家左手并进入左篮,`leftHand` 轨迹代表玩家右手并进入右篮。键鼠调试不走该换算,仍保持鼠标左键=左篮、右键=右篮。
|
||||
- 验证:运行 `npm run test -- src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/services/useMocapInput.test.ts`,确认动作名负向测试、未知侧别负向测试和左右手横向轨迹测试通过。
|
||||
- 关联:`src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx`、`docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md`。
|
||||
|
||||
## 儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置
|
||||
|
||||
- 现象:`/child-motion-demo` 已经呈现绘本草地风格,但 `public/child-motion-demo/picture-book-grass-stage.png`、`picture-book-grass-floor.png`、`picture-book-ground-ring.png`、`picture-book-character-outline.png`、`picture-book-ui-panel.png` 或 `picture-book-ui-button.png` 不存在,Network 里对应图片返回 404,或运行 `npm run assets:child-motion-demo -- --live` 返回缺少 VectorEngine 配置。
|
||||
- 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine `gpt-image-2-all` 生成,脚本只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和可选 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。
|
||||
- 处理:在本地私密环境补齐 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai` 与 `VECTOR_ENGINE_API_KEY`,不要把 key 写入 Git;先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt,再运行 `npm run assets:child-motion-demo -- --live` 或 `npm run assets:child-motion-demo -- --live --only ui-panel` 等小批量命令生成资源。透明资源的品红底源图写入 `tmp/child-motion-demo-assets/`,不要把源图或预览图放入 `public/child-motion-demo/` 作为正式资产。
|
||||
- 验证:生成后确认 `public/child-motion-demo/` 只保留页面引用的最终 PNG,重新打开 `/child-motion-demo` 可看到真实绘本草地背景、地面、圆环、角色轮廓和 UI 资源;`npm run check:encoding` 仍通过。
|
||||
- 关联:`scripts/generate-child-motion-demo-assets.mjs`、`src/index.css`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## 儿童动作 Demo 绘本资源变形先查用途拆分和透明后处理
|
||||
|
||||
- 现象:`/child-motion-demo` 背景风格正确,但底部草坪被拉成厚色块、顶部 HUD 或右下状态条像方形面板被横向拉伸,或旧 `picture-book-ui-panel.png` 与新资源叠在一起。
|
||||
- 原因:早期资源中 `picture-book-ui-panel.png` 是接近方形画布,`picture-book-grass-floor.png` 也含大量透明边界;若 CSS 用 `background-size: 100% 100%` 把同一资源强行铺成 HUD、状态条、开始面板或底部地板,就会出现变形和层叠观感。
|
||||
- 处理:使用 v2 用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v2.png`、`picture-book-character-outline-v2.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png`、`picture-book-ui-button-v2.png`;CSS 按资源比例等比缩放,底部草坪只覆盖下沿,HUD / 状态条 / 开始托盘分别引用各自资源。若只需修透明裁切或品红边,运行 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>`,不重新请求 image-2。
|
||||
- 验证:用横屏截图检查没有新旧资源叠加、没有方形面板拉成长条、角色和地面指示环不被前景草坪埋住;同时运行 `npm run check:encoding`。
|
||||
- 关联:`scripts/generate-child-motion-demo-assets.mjs`、`src/index.css`、`public/child-motion-demo/`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。
|
||||
|
||||
## GPT-image-2 不再读 APIMart 图片配置
|
||||
|
||||
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。
|
||||
- 原因:2026-05-09 后 GPT-image-2 图片生成已切到 VectorEngine `gpt-image-2-all`,APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认路径为 `/v1/images/generations`、模型为 `gpt-image-2-all`、参考图字段为 `image`。
|
||||
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
|
||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。
|
||||
|
||||
## 拼图参考图没有影响生成时先查 action payload 和阶段日志
|
||||
|
||||
- 现象:拼图上传参考图后生成出的画面明显不像参考图,或结果页重新生成没有按保存的参考图走图生图。
|
||||
- 原因:首图生成只通过 `compile_puzzle_draft.referenceImageSrc` 临时传 Data URL,不持久化到 SpacetimeDB;结果页重新生成则要把当前上传图或关卡 `pictureReference` 作为 `generate_puzzle_images.referenceImageSrc` 继续传给后端。
|
||||
- 处理:浏览器 Network 里确认 action payload 带 `referenceImageSrc`;api-server 日志按同一 `session_id` 查看 `拼图参考图解析完成`、`拼图 VectorEngine 图片生成 HTTP 返回`、`拼图 VectorEngine 图片下载完成`、`拼图生成图片已写入 OSS 与资产索引`,可定位慢在参考图读取、VectorEngine、下载或 OSS。
|
||||
- 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的 `pictureReference` 重新生成;后端单测覆盖 VectorEngine 请求体 `image` 字段。
|
||||
- 关联:`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle.rs`。
|
||||
|
||||
## 拼图首图生成后要把入口参考图写回 `pictureReference`
|
||||
|
||||
- 现象:入口页上传图后,首图看着像没吃到参考图;结果页重新生成时默认只沿用关卡旧图,没有继续带入口上传图。
|
||||
- 原因:首图生成请求虽然已经把 `referenceImageSrc` 传给 VectorEngine,但如果后端只更新 `cover_image_src` / `selected_candidate_id` 而不回写首关 `pictureReference`,结果页后续重绘就会丢失参考图。
|
||||
- 处理:在 `compile_puzzle_draft` 和 `generate_puzzle_images` 的成功与 SpacetimeDB 降级快照路径里,都把本次入口参考图写入首关 `pictureReference`。
|
||||
- 验证:后端单测覆盖 `build_puzzle_levels_with_primary_update` 和 `apply_generated_puzzle_candidates_to_session_snapshot`;结果页重新生成应在未重新上传时继续带入 `level.pictureReference`。
|
||||
- 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/components/puzzle-result/PuzzleResultView.tsx`。
|
||||
|
||||
## 拼图图生图仍不像参考图时先看是否走了 edits
|
||||
|
||||
- 现象:Network payload 已带 `referenceImageSrc`,但 VectorEngine 生成结果仍明显不像上传图。
|
||||
- 原因:`gpt-image-2-all` 的 `/v1/images/generations` 更适合纯文生图;有参考图且需要重绘时应切到 `/v1/images/edits` 的 multipart 图生图接口。
|
||||
- 处理:`referenceImageSrc` 存在且 `aiRedraw = true` 时直接走 edits,prompt 仍保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。
|
||||
- 验证:后端单测应覆盖 `images/edits` 路由、`b64_json` 响应解码和参考图强提示;真实联调先看日志里是否命中 `拼图 VectorEngine 图片编辑 HTTP 返回`。
|
||||
- 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/services/puzzleReferenceImage.ts`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。
|
||||
|
||||
## 旧后端路线文档造成判断漂移
|
||||
|
||||
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
|
||||
- 原因:项目历史文档较多,部分旧方案仍保留作迁移参考。
|
||||
- 处理:涉及服务端、数据真相、SpacetimeDB、运行时状态时,先看 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`,再看 DDD 总纲和具体技术方案。
|
||||
- 验证:代码改动应落在 `server-rs + Axum + SpacetimeDB` 主线;旧路线只作为迁移参考,不作为兼容目标。
|
||||
- 关联:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`、`AGENTS.md`。
|
||||
|
||||
## SpacetimeDB 表结构变更不能按 PostgreSQL 迁移直觉处理
|
||||
|
||||
- 现象:发布时 schema 冲突、自动迁移拒绝、旧客户端调用 reducer 失败、private 表数据迁移遗漏。
|
||||
- 原因:SpacetimeDB 对字段删除、类型变化、索引/主键/RLS/reducer 变化有不同自动迁移边界。
|
||||
- 处理:变更前阅读 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`;涉及表变化时同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 bindings;必要时走 JSON 导入导出与分片导入迁移流程。
|
||||
- 验证:发布前完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
|
||||
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
|
||||
|
||||
- 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`。
|
||||
- 原因:SpacetimeDB module 的 wasm32 构建树被间接带入原生/网页依赖;已验证链路是 `reqwest -> platform-oss -> shared-contracts -> module-runtime -> spacetime-module`,由共享契约默认启用资产 OSS 契约触发。
|
||||
- 处理:让 `shared-contracts` 的 OSS 资产契约走 `oss-contracts` feature,workspace 根依赖保持 `default-features = false`;`api-server` 这类原生后端需要资产 DTO 时在自身 `Cargo.toml` 显式启用 `features = ["oss-contracts"]`。
|
||||
- 验证:执行 `cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown` 应显示 nothing to print;再执行 `cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown`。
|
||||
- 关联:`server-rs/crates/shared-contracts/Cargo.toml`、`server-rs/crates/api-server/Cargo.toml`、`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`。
|
||||
|
||||
## 本地 SpacetimeDB replica identity 不匹配
|
||||
|
||||
- 现象:本地 standalone 启动时报 `mismatched database identity`。
|
||||
- 原因:本地 SpacetimeDB 数据目录中的 replica 数据残留与当前数据库身份不一致。
|
||||
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
|
||||
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
|
||||
- 关联:`docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md`。
|
||||
|
||||
## 本地 SpacetimeDB publish 403 优先查 CLI 身份和目标库
|
||||
|
||||
- 现象:`spacetime publish` 在 `Pre-publish check` 阶段返回 `403 Forbidden`,提示当前 identity 无权对目标 database identity 执行 `update database`。
|
||||
- 原因:当前 CLI 登录态不是目标数据库的创建者或授权身份,或 `.env.local` / publish 命令指向了另一个数据库或 SpacetimeDB 服务。
|
||||
- 处理:除 CI/CD 脚本内部受控用法外,不再使用 `spacetime --root-dir` 排障或发布。先执行 `spacetime login show`、`spacetime server list`,再用 `spacetime list --server http://127.0.0.1:3101` 或实际 `--server-url` 确认当前身份是否能看到目标库;本地开发发布优先使用 `npm run dev:rust` 或从 `server-rs` 目录执行显式 `--server` 的 `spacetime publish`。如果身份不对,重新登录正确身份、使用项目脚本重新生成本地库,或在 SpacetimeDB 侧补授权。
|
||||
- 验证:`spacetime list --server http://127.0.0.1:3101` 能看到目标库;重新发布不再使用无权限 identity。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`。
|
||||
|
||||
## `npm run dev` 本地 SpacetimeDB 401 / 403 可重置默认 local 身份
|
||||
|
||||
- 现象:`npm run dev` 启动本地开发栈时,SpacetimeDB 在登录、发布或预检查阶段返回 `401` / `403`,清理后仍像在使用旧 token 或旧本地库。
|
||||
- 原因:本机 `spacetime` CLI 保存的旧 token、默认 server、正在运行的 standalone 进程或默认 local 数据库与当前发布身份不一致。
|
||||
- 处理:确认只是本地测试库且数据可丢弃后,先查看并停止本地 `spacetimedb-standalone`,执行 `spacetime logout`,确认并设置 `spacetime server set-default local`,停 server 后用 `spacetime server clear -y` 清空默认本地库,再 `spacetime start`,另开终端执行 `spacetime login --server-issued-login local`,最后用 `spacetime publish --server local A` 或项目脚本重新发布。
|
||||
- 验证:`spacetime server list` 默认目标为 local;重新登录后发布不再返回 `401` / `403`;`npm run dev` 可以完成 SpacetimeDB publish 并继续启动 `api-server`。
|
||||
- 关联:`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`、`scripts/dev-rust-stack.sh`。
|
||||
|
||||
## 本地 SpacetimeDB 联调可按阶段跳过宿主或发布
|
||||
|
||||
- 现象:本地 `npm run dev` 因 `3101` 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 `spacetime-module` 语法而被完整联调链路拖慢。
|
||||
- 原因:`npm run dev` 默认同时启动 SpacetimeDB standalone、发布 `server-rs/crates/spacetime-module`、启动 Rust `api-server`、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。
|
||||
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 `3101`,被占用则输出占用进程并选择最近可用端口,保证 publish 与 `api-server` 都连接同一个实际 SpacetimeDB URL。`api-server` 启动前也会检测 `8082` 并选择最近可用端口。Windows / Git Bash 下不要用 `tr/head/xargs` 管道读取 `spacetime.pid` 或 URL 记录,脚本应使用 Node 读取并短重试,避免 `tr: read error: Device or resource busy`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。`npm run dev` 会在启动前检查 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,不可用时自动寻找后续可用端口,并把实际端口传给 publish、后端环境变量和前端代理目标。
|
||||
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101`;`3101` 或 `8082` 被其他进程占用时,脚本输出占用进程并使用最近可用端口;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。端口漂移时控制台会打印 `[dev:ports] ... 不可用,改用 ...`,后续 `[dev:rust] web/admin web/rust api/spacetime` 地址应与实际端口一致。
|
||||
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`、`scripts/dev-rust-stack.sh`。
|
||||
|
||||
## 本地 SpacetimeDB publish 401 可清本地库重发
|
||||
|
||||
- 现象:本地 `spacetime publish` 显示 `401` 无权限,或重新发布仍像是在更新旧库。
|
||||
- 原因:本地开发数据目录中保留的数据库、控制库身份或发布身份与当前目标不一致。
|
||||
- 处理:确认本地开发数据可以丢弃后,停止本地 SpacetimeDB,备份或删除 `server-rs/.spacetimedb/local/data`,再重新运行 `npm run dev` 或本地 publish;不要用 `--root-dir` 手工清库。
|
||||
- 验证:重新发布日志应显示创建新的数据库,而不是更新旧数据库;若仍显示更新或继续 `401`,继续检查数据目录、库名和 CLI 身份。
|
||||
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`、`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`。
|
||||
|
||||
## SpacetimeDB 模块 publish 报 `wasm-bindgen detected`
|
||||
|
||||
- 现象:`spacetime publish` 已经完成 Rust 编译,但随后报 `wasm-bindgen detected`,提示依赖树里有面向 Web 平台的 wasm-bindgen。
|
||||
- 原因:SpacetimeDB 模块是数据库内 WASM,不允许拉入 Web/HTTP client 链路;常见误因是 `spacetime-module -> module-* -> shared-contracts -> platform-* -> reqwest -> wasm-bindgen` 这类反向依赖。
|
||||
- 处理:执行 `cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown` 找到链路;把平台实现类型从 `shared-contracts` 或 `module-*` 中移除,只保留公开 DTO,平台响应到 DTO 的转换放回 `api-server` 等 adapter 层。
|
||||
- 验证:上述 `cargo tree` 输出 `warning: nothing to print`;`cargo check -p shared-contracts`、`cargo check -p api-server` 通过;重新 `spacetime publish ... --module-path server-rs/crates/spacetime-module` 不再报 wasm-bindgen。
|
||||
- 关联:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`、`server-rs/crates/shared-contracts/src/assets.rs`、`server-rs/crates/api-server/src/assets.rs`。
|
||||
|
||||
## Vite SPA fallback 吞掉 API 请求
|
||||
|
||||
- 现象:本地请求 `/api/profile/*` 等接口时返回 HTML,被前端当 JSON 解析报错。
|
||||
- 原因:Vite 代理缺少对应 `/api/*` 前缀,API 请求落到 SPA fallback。
|
||||
- 处理:补齐 Vite 代理,让 API 请求转发到 Rust `api-server`。
|
||||
- 验证:请求返回 JSON,相关页面不再出现 HTML parse 错误。
|
||||
- 关联:`docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md`。
|
||||
|
||||
## 反馈页清空 file input 前必须先拷贝 FileList
|
||||
|
||||
- 现象:点击上传凭证会打开文件选择框,但选择图片后页面没有展示预览,提交时也没有携带图片凭证。
|
||||
- 原因:浏览器传入的 `FileList` 可能跟 `<input type="file">` 保持 live 绑定;如果先执行 `input.value = ''`,再从参数里的 `FileList` 读取文件,列表可能已经为空。
|
||||
- 处理:在清空 file input 前先执行 `const selectedFiles = files ? Array.from(files) : []`,后续图片类型、大小、Data URL 读取和预览都基于这个普通数组。
|
||||
- 验证:`PlatformFeedbackView.test.tsx` 用 mock `FileReader` 断言选择图片后出现 `反馈凭证预览`,且提交 payload 带 `evidenceItems[].dataUrl`。
|
||||
- 关联:`src/components/platform-entry/PlatformFeedbackView.tsx`、`docs/technical/PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md`。
|
||||
|
||||
## 拼图 VectorEngine 图片生成密钥不能复用 DashScope / ARK key
|
||||
|
||||
- 现象:拼图新手引导或拼图创作点击生成后返回 `VectorEngine 图片生成密钥未配置`。
|
||||
- 原因:拼图 `gpt-image-2` / 历史 `nanobanana2` 图片生成已统一走 VectorEngine;后端只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,不会用 `DASHSCOPE_API_KEY`、`LLM_API_KEY`、`ARK_API_KEY` 或 `APIMART_API_KEY` 兜底。
|
||||
- 处理:在本机私密配置 `.env.secrets.local` 或进程环境中配置真实 `VECTOR_ENGINE_API_KEY`,不要提交到 Git;填入后必须重启 `api-server` / `npm run dev`,运行中的进程不会自动加载新 env。
|
||||
- 验证:不打印密钥内容,只检查 `VECTOR_ENGINE_API_KEY` 非空;重启后触发拼图生成不再返回本地配置缺失的 503。
|
||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`.codex/skills/gpt-image-2-apimart/SKILL.md`。
|
||||
|
||||
## `npm run api-server` 读取 env 的顺序必须让 `.env.secrets.local` 最后覆盖
|
||||
|
||||
- 现象:`POST /api/assets/hyper3d/text-to-model` 在本地返回 503,详情里提示 `HYPER3D_API_KEY 未配置`,但开发者明明已经在本地私密文件里写了 key。
|
||||
- 原因:`scripts/api-server-dev.mjs` 之前按 `.env.secrets.local → .env.local → .env` 合并,结果仓库里的 `.env` 空示例值会把前面已经设置好的私密 key 覆盖掉。
|
||||
- 处理:`npm run api-server` / `npm run dev:rust` / `npm run dev` 统一按“外层 shell 变量优先,其后 `.env`、`.env.local`、`.env.secrets.local` 逐层覆盖”的顺序加载;真实密钥优先放 `.env.secrets.local`。
|
||||
- 验证:本地加入临时测试后,`HYPER3D_API_KEY` 应能被 `.env.secrets.local` 覆盖,且 shell 变量仍然最高优先级。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/hyper3d_generation.rs`、`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`。
|
||||
|
||||
## 拼图图片生成 98% 后报 OSS V4 签名时间格式化失败
|
||||
|
||||
- 现象:拼图创作表单生成进度卡在 98%,`POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions` 返回 `502 Bad Gateway`,前端提示 `拼图图片生成失败:OSS V4 签名时间格式化失败`。
|
||||
- 原因:`platform-oss` 曾用 `OffsetDateTime::time().to_string()` 拼接 `x-oss-date`,UTC 小时、分钟或秒为个位数时可能缺少前导零,导致 V4 签名时间不是固定 `YYYYMMDDTHHMMSSZ`。
|
||||
- 处理:OSS V4 签名日期统一显式补零格式化;签名 scope 用 `YYYYMMDD`,完整签名时间用 `YYYYMMDDTHHMMSSZ`,不要再依赖 `time().to_string()`。
|
||||
- 验证:运行 `cargo test -p platform-oss` 和 `cargo check -p api-server`;重启 `npm run api-server` 后检查 `/healthz`,再重新触发拼图生成。
|
||||
- 关联:`server-rs/crates/platform-oss/src/lib.rs`、`server-rs/crates/api-server/src/assets.rs`、`docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md`。
|
||||
|
||||
## 拼图生成完成后图片只显示破图或 alt 文案
|
||||
|
||||
- 现象:拼图结果页生成完成后,“画面图”区域出现破图图标和作品名,图片无法正常预览;但打开历史拼图素材时同一张图可能可以正常预览。
|
||||
- 原因:拼图正式图保存为 `/generated-puzzle-assets/*` 兼容标识,旧 `/generated-*` 直读代理已删除;如果前端没有通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签,或收到无前导斜杠的 `generated-puzzle-assets/*` object key 后未识别为 generated 私有资源,浏览器会直接请求裸路径并失败。生成完成后的结果图还会传入 `refreshKey`,它只能用于重新请求 `/api/assets/read-url`,不能给 OSS V4 签名 URL 追加 `_v`;OSS 会把 query 纳入签名,额外参数会让签名失效,历史素材常因未传 `refreshKey` 而表现正常。
|
||||
- 处理:拼图结果页、发布预览、运行态和历史素材预览都走 `ResolvedAssetImage` 或 `useResolvedAssetReadUrl`;`isGeneratedLegacyPath(...)` 必须同时识别 `/generated-*` 和 `generated-*`;`refreshKey` 只绕过前端签名缓存并重新换签,不修改已返回的 OSS 签名 URL;禁止恢复 `/generated-puzzle-assets` 直读代理。
|
||||
- 验证:运行 `npm run test -- src\services\assetReadUrlService.test.ts src\hooks\useResolvedAssetReadUrl.test.tsx src\components\puzzle-result\PuzzleResultView.test.tsx`,再触发一次真实生成确认 Network 中先请求 `/api/assets/read-url`,图片 `src` 为未追加 `_v` 的签名 URL。
|
||||
- 关联:`src/services/assetReadUrlService.ts`、`src/components/ResolvedAssetImage.tsx`、`docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md`。
|
||||
|
||||
## 本地短信登录页签突然消失
|
||||
|
||||
- 现象:登录弹窗只剩密码登录,短信登录页签看起来像被删掉,但 `LoginScreen` 中手机号验证码表单仍存在。
|
||||
- 原因:前端根据 `GET /api/auth/login-options` 返回的 `availableLoginMethods` 渲染页签;常见根因有两类:
|
||||
- 本地启动脚本没有让 `.env.local` 覆盖 `.env`,`SMS_AUTH_ENABLED=true` 不生效,后端只返回 `["password"]`。
|
||||
- Rust API 直连已返回 `["phone","password"]`,但 Vite 代理目标指向未监听端口,导致 3000 域名下的 `login-options` 返回 `500`,`AuthGate` 降级成 `["password"]`。
|
||||
- 3000 端口被旧 `dev:web` 占用后,新的完整栈 Vite 自动漂移到 3001/3002;浏览器仍打开旧 3000 页面,旧页面继续代理到已经下线的端口。
|
||||
- 单独 `npm run dev:web` 启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
|
||||
- 处理:优先用 `npm run api-server`、`npm run dev:rust` 或 `npm run dev` 启动,这些入口应保持 shell 环境变量最高优先级,并允许 `.env.local` 覆盖 `.env`;完整栈启动时还要确保脚本计算出的 `RUST_SERVER_TARGET` 不被 `.env.local` 里的旧值覆盖。排查时先请求 3000 域名下的 `/api/auth/login-options`,再直连 Rust API 目标,并核对 `.env.local` 的 `SMS_AUTH_ENABLED` 与代理端口;若 3001/3002 才返回正确结果,说明当前 3000 是旧前端进程,应清理旧进程后重启。
|
||||
- 验证:`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`scripts/api-server-maincloud.mjs`、`scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`。
|
||||
|
||||
## 本地短信收不到验证码先查 provider
|
||||
|
||||
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
|
||||
- 原因:本地 `.env.local` 里如果是 `SMS_AUTH_PROVIDER="mock"`,后端不会发真实短信,只会返回固定 mock 验证码;另外 `npm run api-server` 过去曾让 `.env` 覆盖 `.env.local`,导致本地真实短信配置被错误压回默认值。
|
||||
- 处理:真实短信联调时把 `.env.local` 的 `SMS_AUTH_PROVIDER` 显式设为 `aliyun`,然后重启 `api-server`;如果只想验证 UI 和账号链路,则保留 `mock` 并使用 `SMS_AUTH_MOCK_VERIFY_CODE`。
|
||||
- 验证:`GET /api/auth/login-options` 返回 `["phone","password"]`,`api-server` 日志里 `provider=aliyun` 才说明真实短信链路已生效。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`、`docs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md`。
|
||||
|
||||
## 手机验证码登录 500 先查短信 provider 语义
|
||||
|
||||
- 现象:登录弹窗手机号验证码登录失败,浏览器看到 `POST /api/auth/phone/login 500`,后端日志里同时出现阿里云短信 `UNKNOWN`、`biz.FREQUENCY` 或 `check frequency failed`。
|
||||
- 原因:真实短信 provider 的配置错误或上游失败曾被 `module-auth` 折叠成 `PhoneAuthError::Store`,HTTP 层只能按内部错误返回 `500`,掩盖了 provider 失败。
|
||||
- 处理:保留 provider 错误语义,配置错误映射 `503 Service Unavailable`,上游短信失败映射 `502 Bad Gateway`;本地只验证 UI/账号链路时可用 shell 临时覆盖 `SMS_AUTH_PROVIDER=mock` 后启动 `npm run api-server`。
|
||||
- 验证:`cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml`,真实 provider 频控时接口不再返回 `500`。
|
||||
- 关联:`server-rs/crates/module-auth/src/errors.rs`、`server-rs/crates/api-server/src/phone_auth.rs`、`docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md`。
|
||||
|
||||
## 手机验证码登录成功后又瞬间回到未登录
|
||||
|
||||
- 现象:手机号验证码登录先成功,随后 UI 又闪回“未登录”,登录弹窗可能重新出现。
|
||||
- 原因:`AuthGate` 首次 hydrate 会异步轮换 refresh cookie 并请求 `/api/auth/me`。如果用户在 hydrate 完成前已经登录,晚到的旧 hydrate 仍可能把刚写入的 `user` 覆盖成 `null`。
|
||||
- 处理:给 `AuthGate` 的 hydrate 增加版本号保护;登录成功、退出登录和全局 auth 事件都会推进版本号,旧 hydrate 结果到达后直接丢弃。
|
||||
- 验证:`npm run test -- src/components/auth/AuthGate.test.tsx`,新增用例应覆盖“旧 guest hydrate 不覆盖新登录态”。
|
||||
- 关联:`src/components/auth/AuthGate.tsx`、`src/components/auth/AuthGate.test.tsx`、`docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md`。
|
||||
|
||||
## 刷新网页后登录态失效
|
||||
|
||||
- 现象:刷新网页后,用户明明有本地 access token,却回到未登录状态。
|
||||
- 原因:`AuthGate` hydrate 曾先强制调用 `refreshStoredAccessToken()`;当 refresh cookie 临时失效、代理错配或后端返回 `401` 时,该方法会先清空本地 access token,随后 `/api/auth/me` 只能恢复成未登录。
|
||||
- 处理:`refreshStoredAccessToken()` 增加 `clearOnFailure` 选项;`AuthGate` 在已有本地 access token 时先用 `/api/auth/me` 确认用户,确认成功后再后台 refresh 续期与写每日登录埋点,后台 refresh 失败不清 token。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx -t "explicit refresh opts out|auth gate keeps a valid local token login"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/components/auth/AuthGate.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## 登录后推荐页加载出作品又回到未登录
|
||||
|
||||
- 现象:前端登录成功后进入推荐页,推荐页自动加载出一个作品,随后瞬间回到未登录;停留在其他页面或推荐页没加载出作品时不复现。
|
||||
- 原因:推荐页 embedded 运行态会自动发起受保护写请求。若这些卡片级后台请求遇到 `401` 或 refresh 失败,默认请求层曾清空 access token 并广播全局 auth 事件,导致 `AuthGate` 重新 hydrate 成未登录态。更隐蔽的是,`refreshAccessToken()` 自身曾在 refresh 失败时静默清 token,即便调用方关闭了 `clearAuthOnUnauthorized`,也可能让后续 hydrate 变成未登录。
|
||||
- 处理:请求层统一使用 `authImpact: 'global' | 'local'` 区分账号权威请求与局部后台请求;推荐页自动运行态、图片换签、公开拼图运行态和平台 bootstrap 私有投影刷新统一使用 `BACKGROUND_AUTH_REQUEST_OPTIONS` / `RUNTIME_BACKGROUND_AUTH_OPTIONS`,并等 `canReadProtectedData` 为 true 后再启动;用户主动点击的账号动作仍保留默认全局鉴权失败处理。
|
||||
- 追加处理:generated 私有图片换签 `/api/assets/read-url` 也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。
|
||||
- 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,`startPuzzleRun`、通关自动 `submitPuzzleLeaderboard`、下一关 `advancePuzzleNextLevel` 和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。
|
||||
- 追加处理:通关后 `refreshSaveArchives()`、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/services/assetReadUrlService.ts`、`src/services/puzzle-runtime/puzzleRuntimeClient.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md`。
|
||||
|
||||
## 推荐页作品卡一直显示加载中
|
||||
|
||||
- 现象:推荐页有公开作品,但主视口一直停在“加载中...”,没有进入作品,也没有显示可操作错误。
|
||||
- 原因:推荐页自动启动嵌入运行态时先设置 `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,但失败或并发切换时外层缺少稳定错误态和请求版本保护,旧启动请求可能晚到覆盖新状态。
|
||||
- 处理:`selectRecommendRuntimeEntry` 使用启动请求版本号丢弃旧请求;启动失败统一设置 `activeRecommendRuntimeError = "作品暂时无法进入,请稍后再试。"` 并关闭 `isStartingRecommendEntry`。
|
||||
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation surfaces start failure"`。
|
||||
- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## 推荐页未登录入口误打开公开详情
|
||||
|
||||
- 现象:新用户默认在发现页,但点击推荐页或推荐封面后,如果复用公开作品详情入口,可能绕过推荐页“登录后游玩”的产品门禁。
|
||||
- 原因:`RpgEntryHomeView` 曾只有 `onOpenGalleryDetail` 一个回调,同时服务发现页公开详情和推荐页作品入口;一旦为发现页保留公开浏览能力,推荐页也会跟着打开详情。
|
||||
- 处理:公开详情与推荐页入口分离为 `onOpenGalleryDetail` 和 `onOpenRecommendGalleryDetail`。发现页、搜索和排行榜保留公开详情;推荐 Tab、推荐封面、推荐运行态错误重试和桌面推荐模块统一走登录门禁。未登录推荐页只显示封面,点击封面只弹登录窗,不携带登录后自动打开详情的回调。
|
||||
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend"`。
|
||||
- 关联:`src/components/rpg-entry/RpgEntryHomeView.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md`。
|
||||
|
||||
## Rust 冷编译导致 api-server 健康检查误超时
|
||||
|
||||
- 现象:`npm run dev:rust` 在 Windows 冷编译/链接阶段误判 `/healthz` 等待超时并杀掉 `cargo run`。
|
||||
- 原因:脚本把 SpacetimeDB 与 api-server 等待窗口混在一起,未考虑 Rust 冷编译耗时。
|
||||
- 处理:按冷编译超时修复文档拆分等待窗口。
|
||||
- 验证:冷启动时不再误杀仍在编译的 api-server。
|
||||
- 关联:`docs/technical/API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md`。
|
||||
|
||||
## Windows debug api-server 主线程栈溢出
|
||||
|
||||
- 现象:`cargo check -p api-server` 和 `build_router` 测试通过,但 `npm run api-server` 在 Windows debug 启动时 `thread 'main' has overflowed its stack`。
|
||||
- 原因:`api-server` Axum 路由树已经很深,debug 主线程默认栈偏小,初始化状态和构造路由时容易触顶。
|
||||
- 处理:入口 `main` 用显式 16MB 栈线程启动 Tokio runtime,并把实际服务逻辑放入 `run_server()`;新增路由时优先用小 router `.merge()`,避免继续拉长主链。
|
||||
- 验证:`npm run api-server` 后 `/healthz` 返回 200,相关路由冒烟通过。
|
||||
- 关联:`server-rs/crates/api-server/src/main.rs`、`server-rs/crates/api-server/src/app.rs`。
|
||||
|
||||
## Windows debug api-server.exe 锁文件与强杀退出码容易混淆
|
||||
|
||||
- 现象:`cargo run -p api-server` 或 `npm run api-server` 报 `failed to remove file ... target\debug\api-server.exe`;清理旧进程后,旧终端可能继续打印 `process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)`。
|
||||
- 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条 `npm run api-server` 链路仍在运行,进程树为 `npm run api-server -> node scripts/api-server-dev.mjs -> cargo run -> api-server.exe`。`0xffffffff` 常见于排障时用 `Stop-Process -Force` 强制结束旧 `api-server.exe` 后由 Cargo 回显,不一定代表新启动失败。
|
||||
- 处理:先按目标路径确认并停止本仓库的旧 `api-server.exe` 及其父级 `cargo/node/cmd` 启动链路,再重新启动;不要同时开多个 `npm run api-server`。
|
||||
- 验证:确认没有匹配 `C:\Genarrative\server-rs\target\debug\api-server.exe` 的进程后,`Remove-Item` 能删除旧 exe;随后 `npm run api-server` 启动并访问 `/healthz` 返回 200。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/main.rs`。
|
||||
|
||||
## dev-rust-stack 端口被旧进程占用时会误判健康检查
|
||||
|
||||
- 现象:`node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh --skip-spacetime` 输出 `Port 3000 is in use, trying another one...`,随后 `api-server.exe` 报 `AddrInUse` / `code: 10048`。
|
||||
- 原因:旧 `api-server` 仍监听默认 `8082` 时,脚本的 `/healthz` 探测会命中旧进程并误判新服务已就绪;旧 Vite 占住 `3000` 时,Vite 默认漂移到新端口,浏览器仍可能打开旧页面。
|
||||
- 处理:`scripts/dev-rust-stack.sh` 已在 publish / 编译前预检 `api-server`、主站 Vite、后台 Vite 端口,并让 Vite 使用 `--strictPort`;遇到端口占用时按脚本打印的 PID 停止旧进程,或显式传入 `--api-port` / `--web-port` / `--admin-web-port`。
|
||||
- 验证:默认端口被占用时,完整栈应在发布模块前直接失败并打印监听进程;清理端口后重新启动不再漂移端口或命中旧 `/healthz`。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`docs/technical/DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md`。
|
||||
|
||||
## Windows debug 长 SSE Future 触发 api-server 断连
|
||||
|
||||
- 现象:前端 Vite 代理请求 `/api/runtime/creative-agent/sessions/{sessionId}/messages/stream` 报 `read ECONNRESET`,随后 `api-server.exe` 以 `0xffffffff` 退出,`dev:rust` 回收 SpacetimeDB、Vite 和后台 Vite。
|
||||
- 原因:单个 `async_stream::stream!` 中塞入 Agent 执行、外部模型请求、会话更新和大量 SSE 事件,会在 Windows debug 下生成很大的 Future;真实消费 SSE body 时容易触发 worker 线程栈压力或进程级中断,单元测试若只测函数和路由状态会漏掉。
|
||||
- 处理:长 SSE 路由优先使用 `tokio::spawn` 跑业务流程,通过 `mpsc` + `UnboundedReceiverStream` 向 Axum 返回轻量 stream;失败时更新会话为 `failed` 并发送 SSE `error`,不要把大段执行逻辑内联到路由返回的 stream future 中。
|
||||
- 验证:补充实际 `collect()` SSE body 的路由测试,确认首轮包含 `stage`、`puzzle_template_catalog` 和 `done`,且不会提前发送 `puzzle_template_selection` / `puzzle_cost_range`;再执行 `cargo check -p api-server`、`cargo test -p api-server creative_agent`,联调时用 `npm run api-server` 检查 `/healthz`。
|
||||
- 关联:`server-rs/crates/api-server/src/creative_agent.rs`、`server-rs/crates/api-server/src/app.rs`。
|
||||
|
||||
## creative-agent 过程项不要把历史事件渲染成运行中
|
||||
|
||||
- 现象:智能创作页过程中多个阶段从一开始同时转圈,生成结束或进入模板确认后仍有过程项保持转圈。
|
||||
- 原因:前端把历史 `stage`、`tool_started` 和 `thought_summary_delta` 都按 active 渲染;后端工具开始/完成事件如果 `toolCallId` 不一致,也会导致开始事件无法收口。
|
||||
- 处理:
|
||||
- 只有最新且仍在执行的 stage 可为 active;等待确认、等待用户、target ready 和 failed 都是静态状态。
|
||||
- 工具开始事件必须等同一 `toolCallId` 的 `tool_completed` 收口;兼容旧流时可按后续同名完成事件兜底。
|
||||
- 思考摘要只展示用户可见摘要,且流结束或会话进入等待/完成/失败态后必须改成 done。
|
||||
- 验证:前端测试断言完成后 `CreativeAgentProcessItem` 不再存在 `tone === 'active'`;后端测试确认工具开始/完成事件使用相同 `toolCallId`。
|
||||
- 关联:`src/components/creative-agent/creativeAgentViewModel.ts`、`server-rs/crates/api-server/src/creative_agent.rs`、`docs/prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md`。
|
||||
|
||||
## creative-agent 会话切换要清理本地待确认模板
|
||||
|
||||
- 现象:用户在一个智能创作会话中点开模板确认面板后,立即切到另一条创作会话,可能看到上一会话的确认面板残留。
|
||||
- 原因:模板确认面板的 `pendingSelection` 是 `CreativeAgentWorkspace` 本地 UI 状态,不属于后端 session 快照;组件复用时如果不监听 `sessionId` 清理,会跨会话泄漏。
|
||||
- 处理:工作区以 `session?.sessionId` 为边界清空 `pendingSelection`;服务端仍以 `puzzleTemplateSelection` / `targetBinding` 作为正式业务状态。
|
||||
- 验证:前端测试先点开模板确认面板,再 rerender 到另一 session,断言确认面板消失。
|
||||
- 关联:`src/components/creative-agent/CreativeAgentWorkspace.tsx`、`src/components/creative-agent/CreativeAgentWorkspace.test.tsx`。
|
||||
|
||||
## 视觉小说 VN-10 不要绕过平台资产引用
|
||||
|
||||
- 现象:文档、封面、场景背景、角色立绘或音乐为了预览方便被写成 Data URL、裸对象路径、外部 URL 或本地临时文件路径。
|
||||
- 原因:前端上传与预览容易混在一起,若不走平台资产对象,SpacetimeDB 和长期草稿会被大文本或大二进制污染。
|
||||
- 处理:VN 资产统一用 `/api/assets/direct-upload-tickets`、OSS 直传、`/api/assets/objects/confirm`,长期状态只保存 `assetObjectId` 和 `/generated-*` 引用;运行时图片用 `ResolvedAssetImage` 换签。
|
||||
- 验证:文档模式 `sourceAssetIds` 为平台资产 id;草稿中不出现 `data:`;图片和音乐字段为平台 generated 引用或 null。
|
||||
- 关联:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`src/services/visual-novel-creation/visualNovelAssetClient.ts`。
|
||||
|
||||
## 视觉小说 VN-13 交接时不要再回头找旧迁移方案
|
||||
|
||||
- 现象:接手视觉小说的人容易重新打开旧 TXT 迁移文档,把“外部平台工程迁入”误当成当前实现目标。
|
||||
- 原因:视觉小说历史资料里保留了很多迁移阶段的讨论,而当前真正的实现口径已经收口到 PRD、表目录、Prompt 工具说明、实现收口文档和负向扫描报告。
|
||||
- 处理:维护视觉小说时优先看 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`SPACETIMEDB_TABLE_CATALOG.md`、`VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`、`VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md` 和 `VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md`。
|
||||
- 验证:新开发者只读这组文档即可继续维护,不需要把旧 TXT 迁移方案重新当作编码依据。
|
||||
- 关联:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`、`docs/experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md`。
|
||||
|
||||
## 视觉小说公开广场不要触发登录刷新
|
||||
|
||||
- 现象:未登录用户进入平台公开广场或从推荐流读取视觉小说公开作品时,前端可能先尝试 `/api/auth/refresh`,失败后再读取公开列表,导致无意义的鉴权噪声或 401 状态刷新。
|
||||
- 原因:公开只读接口如果复用默认 `requestJson` 选项,缺少 access token 时会先走静默 refresh。
|
||||
- 处理:视觉小说公开广场列表使用 `skipAuth: true` 与 `skipRefresh: true`;鉴权 mutation 仍保持默认鉴权链路。
|
||||
- 验证:执行 `src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts`,确认 `/api/runtime/visual-novel/gallery` 请求携带 `skipAuth` / `skipRefresh`,而 run、重生成和存档 mutation 仍走受保护路由。
|
||||
- 关联:`src/services/visual-novel-runtime/visualNovelRuntimeClient.ts`、`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`。
|
||||
|
||||
## 创作 Tab 语义迁移后,旧“新建作品”测试要改看智能创作首页
|
||||
|
||||
- 现象:把 `create` 从旧创作中心切到 `CreativeAgentHome` 后,旧测试仍尝试在创作页找“新建作品”类型卡,导致用例失败或定位不到元素。
|
||||
- 原因:产品语义已经变成“创作 = 智能创作首页,草稿 = 旧作品架”,但测试夹具和 helper 还沿用旧入口。
|
||||
- 处理:把这类测试改成验证智能创作首页、快捷胶囊、抽屉与草稿 Tab;同时给 `useRpgEntryLibraryDetail` 这类恢复路径补上 `setPlatformTabToDraft`。
|
||||
- 验证:定向 `vitest`、`eslint`、`typecheck`、`check:encoding` 都通过。
|
||||
- 关联:`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx`、`src/components/rpg-entry/useRpgEntryLibraryDetail.ts`。
|
||||
|
||||
## server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
|
||||
|
||||
- 现象:在 `server-rs` 下无参数 `cargo build` 期望同时构建 `spacetime-module`,导致链接或构建范围误判。
|
||||
- 原因:workspace default-members 当前只包含 `crates/api-server`;SpacetimeDB module 有独立构建/发布方式。
|
||||
- 处理:默认 Rust 构建只覆盖原生 `api-server`;本地模块发布继续走 `spacetime publish --module-path ... --build-options="--debug"` / bindings 生成流程。
|
||||
- 验证:查看 `server-rs/Cargo.toml` default-members,并按相关 SpacetimeDB 文档执行模块构建。
|
||||
- 关联:`server-rs/Cargo.toml`、`docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md`。
|
||||
|
||||
## Windows 原生 `spacetime-module` 单测会链接缺失 SpacetimeDB 宿主符号
|
||||
|
||||
- 现象:在 Windows 上执行 `cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml` 可能编译到链接阶段后失败,出现 `LNK2019` / `LNK1120`,缺失 `datastore_insert_bsatn`、`procedure_start_mut_tx`、`console_log` 等 SpacetimeDB 宿主符号。
|
||||
- 原因:`spacetime-module` 依赖的 SpacetimeDB runtime API 面向 wasm 宿主环境,原生 test exe 链接不到这些宿主导出。
|
||||
- 处理:日常语法和类型验证使用 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`;需要验证模块行为时走 SpacetimeDB publish/dev 或模块域纯 Rust crate 的单测,不把该原生链接错误当作业务测试失败。
|
||||
- 验证:`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能通过;原生 `cargo test` 若仍报上述宿主符号缺失,按当前限制记录为未执行。
|
||||
- 关联:`server-rs/crates/spacetime-module`、`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`。
|
||||
|
||||
## Rust 构建不要让不可用的 sccache 阻断 rustc
|
||||
|
||||
- 现象:Cargo 报 `could not execute process sccache ... rustc.exe -vV (never executed)`、`sccache: error: Timed out waiting for server startup`,或 `sccache: caused by: Failed to send data to or receive data from server / Failed to read response header / failed to fill whole buffer`;真实 `rustc -Vv` 可以执行,但构建在调用包装器时失败。
|
||||
- 原因:环境、Jenkinsfile 或 `server-rs/.cargo/config.toml` 启用了 `sccache` wrapper,但当前 agent 没有可执行的 `sccache`、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。Windows 本机若配置了 `SCCACHE_OSS_*`,sccache daemon 冷启动会先经 OSS/本机代理完成缓存读写检查,再监听 `127.0.0.1:4226`;代理或 OSS 链路慢时,Cargo 的 `sccache rustc -vV` 可能先超时。
|
||||
- 处理:保留 `server-rs/.cargo/config.toml` 的 `rustc-wrapper = "sccache"`;Windows 本机优先在 `%APPDATA%\Mozilla\sccache\config\config` 写入 `server_startup_timeout_ms = 60000`,拉长 client 等待 daemon 完成 OSS 初始化的时间,然后删除 `server-rs/target/.rustc_info.json` 里缓存的失败探测结果并重跑原始 Cargo 命令。冷启动验证优先用 `sccache --stop-server`,不要在另一个 `cargo` / `rustc` 仍在编译时 `taskkill /F /IM sccache.exe /T`,否则 proc-macro crate 可能被打断并表现为 `serde_derive` / `spacetimedb-bindings-macro` 的 `sccache ... exit code: 1`。若只做临时排障,可在 Git Bash 中执行 `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build ...`,或在 PowerShell 用 `cargo check -p api-server --config "build.rustc-wrapper=''"` 一次性绕过 wrapper;生产流水线必须先实际执行 `sccache --version`,失败时移除 `RUSTC_WRAPPER` 并回退到直接 `rustc`。
|
||||
- 验证:`rustc -Vv` 能输出版本;冷启动后原始 `cargo check -p api-server` 和 `cargo check -p spacetime-module` 能通过;`sccache --show-stats` 显示 `Cache location oss, name: genarrative-sccache`,证明仍在使用 sccache/OSS 缓存;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## 生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
|
||||
|
||||
- 现象:部署、回滚或 Jenkins Job 重建时参考旧发布文档,导致 systemd、Nginx、SpacetimeDB 自托管和生产包拆分不一致。
|
||||
- 原因:旧 Jenkins / 旧本地远端部署脚本文档仍作为历史经验保留。
|
||||
- 处理:生产相关操作先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`,再按需追溯旧文档。
|
||||
- 验证:发布链路使用当前 `deploy/systemd`、`deploy/nginx`、`scripts/deploy` 和 `jenkins/Jenkinsfile.production-*`。
|
||||
- 关联:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## Release Web 产物通过内网 rsync 拉取
|
||||
|
||||
- 现象:`Genarrative-Web-Deploy` 发布到 `release` 目标时,release agent 本地没有 `/var/cache/genarrative-build/web-artifacts/<job>/<build>/<version>/web.tar.gz`,但 Jenkins controller 又只归档轻量元数据,导致发布阶段找不到 Web 大包。
|
||||
- 原因:Web 大包为了避免从 Linux 构建机拉回 Jenkins controller,默认留在构建机稳定缓存目录;development 目标与构建机同机可直接读取,release 目标是独立机器,需要内网同步。
|
||||
- 处理:release 服务器的 Jenkins 运行用户配置 SSH Host `genarrative-build-internal` 指向构建机内网地址,`Genarrative-Web-Deploy` 在 `DEPLOY_TARGET=release` 且本地缺少大包时默认执行 `rsync` 拉取同一路径内容;真实内网 IP、用户和私钥路径只放在服务器本机 SSH config,不写入 Jenkinsfile。
|
||||
- 验证:在 release 服务器上先手工跑通 `rsync -av --progress "genarrative-build-internal:${SRC}/" "${DST}/"`,再运行 Web Deploy;流水线会继续执行 `web.tar.gz.sha256` 校验。
|
||||
- 关联:`jenkins/Jenkinsfile.production-web-deploy`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## Jenkins 生产流水线拉 Git 先本机再域名备用
|
||||
|
||||
- 现象:生产发布、数据库导入导出或服务器配置流水线在目标 Linux agent 上执行 `GitSCM checkout` 时,`http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` 不可达,导致脚本还没拉下来就失败;若 fallback 到公网 Git 时没有限制 refspec、浅克隆和 tags,还可能在约 10 分钟后出现 `git-remote-https died of signal 15`、`early EOF`、`invalid index-pack output`。
|
||||
- 原因:`127.0.0.1` 只代表当前执行阶段的 agent 自身;当 release agent 与 Git 服务不在同一台机器,或本机 Git/Web 服务临时不可用时,固定写死 localhost 会阻断 Jenkinsfile 内部源码/脚本 checkout。
|
||||
- 处理:Jenkins Job 的 `Pipeline script from SCM` 由 Windows controller 执行,SCM URL 使用公网域名 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`。运行于 Linux agent 的 Jenkinsfile 首次 `checkout([$class: 'GitSCM', ...])` 层先尝试 `GIT_REMOTE_URL=http://127.0.0.1:3000/GenarrativeAI/Genarrative.git`,失败后直接尝试 `GIT_REMOTE_FALLBACK_URL=https://git.genarrative.world/GenarrativeAI/Genarrative.git`,不再配置内网 IP fallback;首次 checkout 必须使用目标分支 refspec、`CloneOption shallow=true depth=1 noTags=true honorRefspec=true`。后续统一走 `scripts/jenkins-checkout-source.sh`,该脚本也按主地址、域名备用地址顺序重新 fetch 并把 `origin` 切到实际可用地址;`COMMIT_HASH` 为空时继续 `--depth=1 --no-tags`,只有指定 commit 时才允许加深历史做分支归属校验。
|
||||
- 验证:扫描本地 Jenkins live job `config.xml`,确认 SCM `<url>` 都是 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;扫描所有以 `127.0.0.1:3000` 拉 Git 且运行在 Linux agent 的生产 Jenkinsfile,确认存在 `GIT_REMOTE_FALLBACK_URL`、`EFFECTIVE_GIT_REMOTE_URL` 和脚本层 `GIT_REMOTE_FALLBACK_URL` 透传;运行 `bash -n scripts/jenkins-checkout-source.sh`。
|
||||
- 关联:`jenkins/Jenkinsfile.production-web-deploy`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-stdb-module-publish`、`jenkins/Jenkinsfile.production-server-provision`、`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`、`scripts/jenkins-checkout-source.sh`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## Jenkins 可选参数在 set -u 下不能裸读
|
||||
|
||||
- 现象:数据库导入或导出流水线报 `INCLUDE_TABLES: unbound variable`,或其它可选参数在 Bash 中未定义即退出。
|
||||
- 原因:Jenkins string/boolean 参数留空时不一定会导出同名环境变量,而生产数据库导入导出脚本块启用了 `set -u`。
|
||||
- 处理:进入 Bash 执行块后先使用 `${VAR:-}` 或 `${VAR:-默认值}` 收敛成本地变量;必填项使用 `${VAR:?中文错误}` 明确失败原因。
|
||||
- 验证:扫描 `jenkins/Jenkinsfile.production-database-export` 与 `jenkins/Jenkinsfile.production-database-import`,确认 `INCLUDE_TABLES`、`CHUNK_SIZE`、`SERVER_BACKUP_DIRECTORY`、`SMOKE_HEALTH_URL` 等可选参数不再裸读。
|
||||
- 关联:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`、`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`。
|
||||
|
||||
## 个人任务 scope 不得扩成 work/site/module
|
||||
|
||||
- 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。
|
||||
- 原因:首版个人任务只支持用户维度,非 user scope 会造成任务进度读取语义错误。
|
||||
- 处理:Admin 任务配置页不展示范围选择,保存时固定 `scopeKind: 'user'`;API 和领域构造层拒绝非 `User`。
|
||||
- 验证:非 `user` scope 返回错误;相关测试覆盖 `Site` / `Module` / `Work` 被拒绝。
|
||||
- 关联:`docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。
|
||||
|
||||
## 拼图发布 409 不一定是接口故障
|
||||
|
||||
- 现象:拼图结果页点击发布后,控制台出现 `POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions 409 (Conflict)`,用户只看到发布失败。
|
||||
- 原因:`publish_puzzle_work` 是资产操作发布入口,发布前会预扣 `1` 枚光点;余额不足时后端按业务冲突返回 `409 CONFLICT`,`details.message` 为 `光点余额不足`。
|
||||
- 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
|
||||
- 验证:`PuzzleResultView` 单测覆盖发布弹窗内展示 `光点余额不足`。
|
||||
- 关联:`src/components/puzzle-result/PuzzleResultView.tsx`、`docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md`、`docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md`。
|
||||
|
||||
## WebGL 画布在高 DPR 移动端放大溢出
|
||||
|
||||
- 现象:抓大鹅试玩入口进入后,3D 锅体和物体从中心圆形区域向右下溢出,顶部状态和底部备选栏也可能看起来被右侧裁切。
|
||||
- 原因:`WebGLRenderer.setPixelRatio(...)` 会把绘图缓冲区乘上设备 DPR;如果没有给 `renderer.domElement` 单独设置 CSS `width/height: 100%` 和绝对铺满,浏览器可能把高 DPR 缓冲区尺寸当成页面显示尺寸。
|
||||
- 处理:中心棋盘和托盘预览的 WebGL canvas 统一套用 `position:absolute; inset:0; width:100%; height:100%; display:block`,`renderer.setSize(..., false)` 只负责同步绘图缓冲区。
|
||||
- 验证:强制移动端 `390x844`、DPR 2 截图,确认棋盘左右边界在视口内,canvas CSS 尺寸等于容器尺寸,内部 `width/height` 属性可大于 CSS 尺寸。
|
||||
- 关联:`src/components/match3d-runtime/Match3DPhysicsBoard.tsx`、`docs/technical/MATCH3D_RUNTIME_3D_GEOMETRY_EXPERIMENT_2026-05-02.md`。
|
||||
|
||||
## Hyper3D subscriptionKey 不要按固定短文本限长
|
||||
|
||||
- 现象:抓大鹅生成草稿时,内联 Rodin 图生 3D 模型提交成功后,状态轮询报 `subscriptionKey 超过 256 字符`,导致 `/api/creation/match3d/sessions/{sessionId}/actions` 返回 400。
|
||||
- 原因:`subscriptionKey` 是 Hyper3D 返回的 opaque token,长度由上游决定;后端状态查询曾复用普通文本校验,把它限制在 256 字符。
|
||||
- 处理:`query_task_status` 对 `subscriptionKey` 只做 trim 和非空校验,不做固定长度限制;前端临时任务和 Match3D 草稿响应可继续展示该 token,但不要把它当作可编辑短文本。
|
||||
- 验证:`cargo test -p api-server accepts_opaque_subscription_key_without_length_cap --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`server-rs/crates/api-server/src/hyper3d_generation.rs`、`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`。
|
||||
|
||||
## 抓大鹅新草稿不要再接回 Rodin 或 GLB 生成
|
||||
|
||||
- 现象:修改抓大鹅素材时容易沿用旧 Rodin/GLB 方案,导致新草稿生成耗时变长、进度停在模型阶段,或运行态等待不存在的 GLB。
|
||||
- 原因:仓库里保留了 Hyper3D 通用代理和历史模型字段,旧文档也曾要求草稿阶段同步生成 GLB。当前产品口径已经改为 2D 多视角素材。
|
||||
- 处理:新 `match3d_compile_draft` 与批量新增只生成 2D 图片:每个物品 5 个视角,单张 1K 素材图固定 5x5 切割,一行对应一个物品,超过 5 个物品自动分批并行生图。`generatedItemAssets[].status` 使用 `image_ready`,发布校验看 `imageViews[]` 或首图引用。`generated-models` 仅用于历史外部模型链接转存,不能作为新生产链路。
|
||||
- 验证:`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`npm run test -- src\services\miniGameDraftGenerationProgress.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx`。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅切图路径不能只用中文物品名
|
||||
|
||||
- 现象:草稿页 `素材配置 > 物品` 中多个素材名称不同,但预览图片完全一样。
|
||||
- 原因:中文物品名经过 OSS 路径段清洗后都可能退化成 `item`,多张切割图片写到同一个 object key,后写入覆盖先写入。
|
||||
- 处理:切割图上传路径必须带稳定唯一 `itemId` 前缀,例如 `items/match3d-item-1-item/views/view-01.png`;运行态读取 generated 私有图片时通过同源 `/api/assets/read-url` 换签,不直接请求裸 OSS 路径。
|
||||
- 验证:后端单测覆盖中文名路径唯一,前端运行态测试覆盖 generated 图片源解析。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅生成素材不能只挂在 compile response
|
||||
|
||||
- 现象:抓大鹅草稿生成完成后停留在结果页能看到切割好的物品图片;退出后从草稿 Tab 重新进入同一草稿,素材列表变回默认占位或为空,已生成的物品名称和图片丢失。
|
||||
- 原因:`generatedItemAssets` 如果只附加在 `match3d_compile_draft` 的 HTTP response draft 上,刷新或重进时 `getMatch3DWorkDetail` 只能读取 SpacetimeDB 中的 `match3d_work_profile`;旧 mapper 返回空数组,自然无法恢复素材。拼图链路已经通过 `save_puzzle_generated_images` 把候选图和 levels 写回 work profile,抓大鹅也必须同样写持久字段。
|
||||
- 处理:compile 成功时把独立物品图片列表序列化写入 `match3d_work_profile.generated_item_assets_json`;`update_match3d_work` / `publish_match3d_work` 保留该字段;API work summary/detail 映射反序列化为 `generatedItemAssets`。前端保持“本次 draft 优先,重进 profile 兜底”的读取顺序。
|
||||
- 验证:`cargo test -p spacetime-client match3d --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/match3d/*`、`server-rs/crates/spacetime-client/src/mapper.rs`、`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅试玩和正式运行态不要只读草稿页本地素材预览
|
||||
|
||||
- 现象:结果页能看到生成的物品图片,但点击试玩或从推荐 / 公开作品进入正式抓大鹅时,局内仍显示默认积木素材。
|
||||
- 原因:结果页本地 `assetDrafts` 和作品 profile 的 `generatedItemAssets` 可能不同步;推荐流内嵌运行态若只读卡片摘要,卡片缺素材时会把已持久化 profile 素材丢掉;点击试玩时 React state 异步更新也可能让运行态第一帧读取旧 `match3dProfile`。
|
||||
- 处理:删除、批量新增、音效生成或封面引用物品素材后,都把当前 `generatedItemAssets` 写回作品 profile;`Match3DResultView` 合并同 `itemId` 的 draft/profile 素材,用 profile 已有 `imageViews[]` 或首图引用补齐旧 draft;点击试玩前把试玩可用物品种类通过 `itemTypeCountOverride` 降到已生成 2D 素材数量;推荐流内嵌运行态启动前若卡片摘要没有生成素材,补读 `getMatch3DWorkDetail(profileId)` 并把详情资产传给 `Match3DRuntimeShell`。`PlatformEntryFlowShellImpl` 需要维护 `match3dRuntimeProfile`,在 `startMatch3DRunFromProfile` 创建 run 后立即锁定本次完整 profile,runtime 渲染时优先按 `run.profileId` 使用这份 profile,而不是等待普通 `match3dProfile` state 下一轮刷新。
|
||||
- 验证:执行 `npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`,并检查历史草稿和公开 M3 作品的 Network 响应里 `generatedItemAssets[].imageViews/imageSrc/imageObjectKey`。
|
||||
- 关联:`src/components/match3d-result/Match3DResultView.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/match3d-runtime/Match3DPhysicsBoard.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 法律文档弹窗通过 portal 挂载时要显式带平台主题
|
||||
|
||||
- 现象:登录弹窗内点击协议链接打开法律文档时,弹窗可能继承不到 `platform-theme--light/dark` 变量,或者层级低于登录遮罩导致不可见。
|
||||
- 原因:`UnifiedModal` 默认通过 portal 挂到 `document.body`,不再处于原页面的主题容器内;登录弹窗自身又使用较高 z-index。
|
||||
- 处理:法律文档弹窗组件应支持传入 `platformTheme`,overlay 上显式挂 `platform-theme platform-theme--*`,并使用高于登录遮罩的层级。法律内容必须作为独立面板打开,不要在当前个人页或登录面板下方内联展开。
|
||||
- 验证:登录页协议链接、个人页法律入口均能打开可滚动 `LegalDocumentModal`,亮色 / 暗色主题文本和按钮可读。
|
||||
|
||||
## 生成页完成回调不能只依赖异步 React state
|
||||
|
||||
- 现象:抓大鹅或拼图点击生成后,进度页已经显示 100% / 生成完成,但没有自动进入试玩或结果页。
|
||||
- 原因:完成回调用 `selectionStageRef.current` 判断用户是否仍在生成页;如果执行 compile 前只调用 `setSelectionStage('*-generating')`,action 很快返回时 ref 仍可能是旧 stage。
|
||||
- 处理:进入各玩法生成页时同步写 `selectionStageRef.current = '*-generating'`,再调用 `setSelectionStage('*-generating')`。这不是为渲染服务,而是给同一异步链路里的完成回调提供即时事实。
|
||||
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx` 覆盖抓大鹅和拼图生成后自动试玩 / 返回结果页。
|
||||
- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 微信支付回调验签不要用商户私钥
|
||||
|
||||
- 现象:微信小程序支付下单能返回 `prepay_id`,但真实支付通知验签失败,或者本地实现误把商户 API 私钥当作回调验签 key。
|
||||
- 原因:商户私钥只用于商户请求微信支付和生成小程序 `paySign`;微信支付通知的 `Wechatpay-Signature` 需要使用微信支付平台公钥或平台证书公钥验签,并按通知头里的平台序列号匹配。
|
||||
- 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:`WECHAT_PAY_PRIVATE_KEY_*` 用于签名,`WECHAT_PAY_PLATFORM_PUBLIC_KEY_*` 与 `WECHAT_PAY_PLATFORM_SERIAL_NO` 用于通知验签,`WECHAT_PAY_API_V3_KEY` 只用于解密通知 resource。支付成功后只通过通知里的 `out_trade_no` 确认本地 pending 订单,并保存 `transaction_id` 到 `profile_recharge_order.provider_transaction_id`。
|
||||
- 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
||||
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
||||
|
||||
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
||||
|
||||
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
||||
- 原因:历史结果页手动 `重新生成` 会把 Hyper3D/Rodin 的外部 CDN 下载链接直接保存到 `generatedItemAssets[].modelSrc`,同时 `modelObjectKey` 为空。外部链接可能过期、跨域、返回 HTML 错误页或非 GLB 内容;前端预览和运行态不能把它当作稳定私有资产。
|
||||
- 处理:该问题只适用于旧数据。结果页发现 `status = model_ready`、`modelSrc = https://...` 且无 `modelObjectKey` 时,可调用 `POST /api/creation/match3d/works/{profileId}/generated-models` 做一次性转存;新草稿和批量新增不得继续生成或依赖 GLB。若历史半修复数据同时保留外部 `modelSrc` 和平台 `modelObjectKey`,旧模型预览读取层优先用 `modelObjectKey`。
|
||||
- 验证:`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx`、`npm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsx`、`npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx`、`cargo test -p api-server match3d_model_download --manifest-path server-rs\Cargo.toml`,并检查修复后响应中的 `generatedItemAssets[].modelObjectKey` 不为空。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-result/Match3DResultView.tsx`、`src/components/match3d-result/Match3DModelPreview.tsx`、`src/components/match3d-runtime/Match3DPhysicsBoard.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅难度配置的物品种类和消除次数必须分离
|
||||
|
||||
- 现象:历史草稿选择标准 / 硬核难度后,系统可能把 `clearCount` 当成局内物品种类数量,导致标准需要 12 种、硬核需要 20/21 种;素材不足时发布或试玩行为不一致。
|
||||
- 原因:旧运行态把消除次数和类型数量绑在一起,结果页文案又同时展示“素材图片 / 局内类型”,导致前端、发布校验和 run start 口径不一致。
|
||||
- 处理:统一使用 `物品种类` 口径:轻松 3、标准 9、进阶 15、硬核 21;历史 `clearCount=20` 且难度为硬核的运行态按新硬核升为 21 组三消,避免 20 组却要求 21 种素材。发布前按 `image_ready` 且有 `imageViews[]` 或 `imageSrc/imageObjectKey` 的生成素材数量阻断不足难度;试玩不阻断,但通过 `itemTypeCountOverride` 自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。
|
||||
- 验证:`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx`、`cargo test -p module-match3d --manifest-path server-rs\Cargo.toml`,涉及发布 reducer 时补跑 `cargo test -p spacetime-module match3d --manifest-path server-rs\Cargo.toml`。
|
||||
- 关联:`src/components/match3d-result/Match3DResultView.tsx`、`src/services/match3d-runtime/match3dRuntimeClient.ts`、`server-rs/crates/module-match3d/src/application.rs`、`server-rs/crates/spacetime-module/src/match3d/mod.rs`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||
|
||||
## 抓大鹅标签清洗不要把 `3D素材` 当编号剥掉
|
||||
|
||||
- 现象:AI 或兜底生成的 `3D素材` 标签在后端规范化后变成 `D素材`。
|
||||
- 原因:标签清洗在去掉编号列表前缀后,又无条件剥离开头数字和标点,把合法标签中的 `3D` 当成列表编号处理。
|
||||
- 处理:只移除明确的编号列表前缀,例如 `1. 标签`、`1、标签`、`1) 标签`;不要对普通标签开头数字做二次剥离。
|
||||
- 验证:`cargo test -p api-server match3d_tag_normalization --manifest-path server-rs/Cargo.toml`,并保留 `normalize_match3d_tag("3D素材") == "3D素材"` 的单测。
|
||||
- 关联:`server-rs/crates/api-server/src/match3d.rs`。
|
||||
|
||||
## 用户标签不要直接外显,SpacetimeDB Vec 字段不要写 default 宏
|
||||
|
||||
- 现象:给 `user_account.user_tags` 或邀请码独立标签列写 `#[default(Vec::<String>::new())]` 时,SpacetimeDB WASM 构建报 `destructor of Vec<String> cannot be evaluated at compile-time`。
|
||||
- 原因:SpacetimeDB 的 table default 宏会走编译期常量求值,不能直接使用有析构逻辑的堆分配类型默认值。
|
||||
- 处理:`user_account.user_tags` 使用 `Option<Vec<String>>` + `#[default(None::<Vec<String>>)]` 表达数据库默认空,业务层统一把 `None` 归一化为空数组;邀请码授予标签复用 `metadata_json.userTags` 存储和解析,不再新增独立 Vec 列。用户标签原始值不得进入登录态、个人资料等通用响应,只能在明确业务白名单里投影,例如拼图排行榜 `visibleTags` 首版仅允许 `北科`。
|
||||
- 验证:`npm run spacetime:generate -- --rust-only` 能通过;`user_account` 旧迁移 JSON 缺字段时能导入,`profile_invite_code` 缺 `metadata_json` 时按 `{}` 兼容。
|
||||
- 关联:`docs/technical/USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
|
||||
## 公开作品详情深链找不到作品不能停在空详情页
|
||||
|
||||
- 现象:直接访问 `/works/detail?work=PZ-...`,作品不存在或已下架时会弹出“作品不存在或已下架,将返回首页。”;关闭提示后仍可能停在大白屏。
|
||||
- 原因:旧恢复逻辑只覆盖 `/runtime/...`,没有覆盖 `/works/detail`。同时 `selectionStage === 'work-detail'` 且 `selectedPublicWorkDetail === null` 时没有兜底渲染,详情数据为空就只剩空页面。
|
||||
- 处理:公开详情失效统一走 `resolveWorkNotFoundRecoveryAction(...)`,覆盖 `/works/detail`、`/gallery/puzzle/detail` 和 `/gallery/visual-novel/detail`;搜索失败和拼图详情 404 分支清理详情/运行态临时状态并回首页;`work-detail` 空数据阶段显示轻量读取态,避免异步间隙白屏。
|
||||
- 验证:`npm run test -- src/routing/runtimeNotFoundRecovery.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"`。
|
||||
- 关联:`docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md`、`src/routing/runtimeNotFoundRecovery.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`。
|
||||
143
.hermes/shared-memory/project-overview.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Genarrative 项目共享概览
|
||||
|
||||
> 用途:给团队成员本地 Hermes 快速建立项目背景。内容应保持高层、稳定、可验证;细节以代码、`README.md`、`AGENTS.md` 和 `docs/` 最新文档为准。
|
||||
|
||||
## 一句话定位
|
||||
|
||||
Genarrative / AI Native Visual RPG 是一个以 **AI 叙事 + 本地规则 + 像素演出** 为核心的视觉 RPG 与 AI 原生游戏创作平台原型。
|
||||
|
||||
项目当前不只是单一 RPG demo,而是在同一平台内同时承载:
|
||||
|
||||
- RPG / 自定义世界创作与运行时
|
||||
- 拼图玩法创作与运行时
|
||||
- 大鱼吃小鱼玩法链路
|
||||
- 抓大鹅 Match3D 玩法链路
|
||||
- 用户账号、存档、钱包、任务、埋点、后台管理与生产部署链路
|
||||
|
||||
## 已具备的主要能力
|
||||
|
||||
来自根目录 `README.md` 的当前主能力:
|
||||
|
||||
- 世界与角色选择
|
||||
- AI 剧情推进与流式对话
|
||||
- 战斗演出、NPC 战斗、切磋
|
||||
- NPC 交易、送礼、求助、招募
|
||||
- 宝藏交互
|
||||
- 同伴跟随与战斗
|
||||
- 游戏主流程内嵌的角色资产工坊、自定义世界实体编辑与角色形象编辑
|
||||
- 自动存档与继续游戏
|
||||
|
||||
## 当前前端与平台入口
|
||||
|
||||
- 主站默认地址:`http://127.0.0.1:3000`
|
||||
- 后台可从 `http://127.0.0.1:3000/admin/` 进入,也可直连 `http://127.0.0.1:3102`
|
||||
- 主站、后台和 Rust 后端联调默认走 `npm run dev`
|
||||
- 只启动前端页面可用 `npm run dev:web`,默认代理到本地 Rust `api-server`
|
||||
- 后台管理独立前端工程为 `apps/admin-web`,管理端只做表现,数据和写操作走 `server-rs` 的 `/admin/api/*`
|
||||
|
||||
## 当前后端唯一落地口径
|
||||
|
||||
后端主线已经切到:
|
||||
|
||||
```text
|
||||
server-rs + Axum + SpacetimeDB
|
||||
```
|
||||
|
||||
当前唯一有效后端方向:
|
||||
|
||||
- HTTP 门面:Rust `api-server` / Axum
|
||||
- 实时状态与业务真相:`server-rs/crates/spacetime-module` / SpacetimeDB
|
||||
- 共享领域与契约:`server-rs` 多 crate 分层维护
|
||||
- 前端职责:表现、输入采集、临时 UI 状态、服务端结果渲染
|
||||
|
||||
明确不再作为正式兼容目标:
|
||||
|
||||
- `server-node` / Express / PostgreSQL 正式后端路线
|
||||
- Go 服务端试验路线
|
||||
- 浏览器侧承担正式运行时逻辑、正式生成编排或正式数据真相的路线
|
||||
|
||||
## server-rs DDD 分层边界
|
||||
|
||||
DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`、`SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md` 和 `AGENTS.md` 为准:
|
||||
|
||||
- `module-*`:领域模型、命令、应用编排结果、领域事件、领域错误
|
||||
- `spacetime-module`:SpacetimeDB 表、reducer、procedure、事务 adapter、mapper
|
||||
- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade
|
||||
- `api-server`:HTTP / SSE / BFF adapter 与外部平台服务编排
|
||||
- `platform-*`:LLM、OSS、SMS、微信等外部副作用
|
||||
- `shared-contracts`:前后端 DTO 与公开协议
|
||||
- `shared-kernel`:跨纯领域 crate 复用的基础字符串、ID、时间和归一化能力
|
||||
- `tests-support`:`server-rs` workspace 共享测试支撑
|
||||
|
||||
## 当前 Rust workspace 主要 crate
|
||||
|
||||
以 `server-rs/Cargo.toml` 为准,当前主要成员包括:
|
||||
|
||||
- 业务领域:`module-ai`、`module-assets`、`module-auth`、`module-big-fish`、`module-combat`、`module-inventory`、`module-custom-world`、`module-match3d`、`module-npc`、`module-puzzle`、`module-progression`、`module-quest`、`module-runtime`、`module-runtime-story`、`module-runtime-item`、`module-story`
|
||||
- 平台副作用:`platform-oss`、`platform-auth`、`platform-llm`
|
||||
- 共享层:`shared-contracts`、`shared-kernel`、`shared-logging`
|
||||
- SpacetimeDB 接入:`spacetime-client`、`spacetime-module`
|
||||
- HTTP 服务与测试:`api-server`、`tests-support`
|
||||
|
||||
注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`,本地 SpacetimeDB 模块发布继续走 `spacetime publish --module-path ... --build-options="--debug"`。
|
||||
|
||||
Cargo 依赖口径:第三方依赖版本和 workspace 内部 crate path 统一维护在 `server-rs/Cargo.toml` 的 `[workspace.dependencies]`,成员 crate 默认继承 workspace 依赖,只保留自身 `features`、`optional` 或 target-specific 差异。
|
||||
|
||||
Rust 加密摘要依赖口径:新代码不再引入 `sha1`;OSS V4 签名、阿里云 OpenAPI V3 签名和 refresh session token 摘要统一使用 `sha2::Sha256`。
|
||||
|
||||
## SpacetimeDB 表域总览
|
||||
|
||||
以 `docs/technical/SPACETIMEDB_TABLE_CATALOG.md` 为持续维护入口。当前表域包括:
|
||||
|
||||
- 运维迁移:`database_migration_operator`、`database_migration_import_chunk`
|
||||
- 认证:`auth_store_snapshot`、`user_account`、`auth_identity`、`refresh_session`
|
||||
- 运行时档案:`runtime_setting`、`runtime_snapshot`、`user_browse_history`、`profile_dashboard_state`、`profile_wallet_ledger`、`analytics_date_dimension`、`tracking_event`、`tracking_daily_stat`、`profile_task_config`、`profile_task_progress`、`profile_task_reward_claim`
|
||||
- RPG 运行时:`story_session`、`story_event`、`npc_state`、`inventory_slot`、`battle_state`、`treasure_record`、`quest_record`、`quest_log`、`player_progression`、`chapter_progression`
|
||||
- 世界创作:`custom_world_profile`、`custom_world_session`、`custom_world_agent_session`、`custom_world_agent_message`、`custom_world_agent_operation`、`custom_world_draft_card`、`custom_world_gallery_entry`
|
||||
- 拼图:`puzzle_agent_session`、`puzzle_agent_message`、`puzzle_work_profile`、`puzzle_event`、`puzzle_runtime_run`、`puzzle_leaderboard_entry`
|
||||
- 抓大鹅 Match3D:`match3d_agent_session`、`match3d_agent_message`、`match3d_work_profile`、`match3d_runtime_run`
|
||||
- 大鱼吃小鱼:`big_fish_creation_session`、`big_fish_agent_message`、`big_fish_asset_slot`、`big_fish_event`、`big_fish_runtime_run`
|
||||
- 资产:`asset_object`、`asset_entity_binding`、`asset_event`
|
||||
- AI 任务:`ai_task`、`ai_task_stage`、`ai_text_chunk`、`ai_result_reference`、`ai_task_event`
|
||||
|
||||
## 产品命名与运营口径
|
||||
|
||||
以 `docs/technical/PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md` 为准:
|
||||
|
||||
- 产品展示名:百梦
|
||||
- 消费单位:光点
|
||||
- 公开账号标识:百梦号
|
||||
- 创作侧称谓:百梦主
|
||||
|
||||
个人任务与埋点系统首版边界:
|
||||
|
||||
- 埋点原始事实写入 `tracking_event`
|
||||
- 聚合投影写入 `tracking_daily_stat`
|
||||
- 任务配置写入 `profile_task_config`
|
||||
- 任务进度写入 `profile_task_progress`
|
||||
- 领奖记录写入 `profile_task_reward_claim`
|
||||
- 钱包流水写入 `profile_wallet_ledger`
|
||||
- “星光”奖励复用现有“光点”钱包,不新增第二种货币
|
||||
- 个人任务 scope 首版仅支持 `user`
|
||||
|
||||
## 关键文档入口
|
||||
|
||||
- 根项目说明:`README.md`
|
||||
- 项目总约束:`AGENTS.md`
|
||||
- 文档总入口:`docs/README.md`
|
||||
- 经验沉淀:`docs/experience/README.md`
|
||||
- 审计与复盘:`docs/audits/README.md`
|
||||
- 系统设计:`docs/design/README.md`
|
||||
- 技术方案:`docs/technical/README.md`
|
||||
- 规划与优先级:`docs/planning/README.md`
|
||||
- 参考目录:`docs/reference/README.md`
|
||||
- 埋点查询:`docs/tracking/README.md`
|
||||
- 运营查询:`docs/operations/README.md`
|
||||
- 后端当前基线:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
- 后端 DDD 总纲:`docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
- 后端并行任务清单:`docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
|
||||
- 契约与路由矩阵:`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
|
||||
- SpacetimeDB 表结构变更约束:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||
- SpacetimeDB 表目录:`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
|
||||
- Rust workspace 依赖集中配置:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`
|
||||
- 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
|
||||
97
.hermes/shared-memory/team-conventions.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 团队协作约定
|
||||
|
||||
> 用途:约定 3 名开发人员在各自本地 Hermes 中协作开发、共享项目记忆的方式。
|
||||
|
||||
## 基本模式
|
||||
|
||||
- 每位开发人员在自己的电脑上使用本地 Hermes。
|
||||
- 每位开发人员本地拉取同一个项目仓库,独立修改代码、运行测试、提交分支。
|
||||
- 团队共享内容优先放在本仓库 `.hermes/` 与 `docs/` 中,通过 Git 同步。
|
||||
- 不共享个人 `~/.hermes` 目录。
|
||||
|
||||
## 共享与禁止共享
|
||||
|
||||
推荐共享:
|
||||
|
||||
- `.hermes/shared-memory/` 团队级长期记忆
|
||||
- `.hermes/plans/` 阶段性实施计划
|
||||
- `.hermes/skills/` 未来可复用仓库级 skills
|
||||
- `docs/` 中 PRD、设计、技术、经验、审计、查询手册
|
||||
- `AGENTS.md` 项目级 Agent 约束
|
||||
|
||||
禁止提交:
|
||||
|
||||
- 个人 `~/.hermes/config.yaml`
|
||||
- 个人 `~/.hermes/.env`
|
||||
- 个人 `~/.hermes/sessions/`
|
||||
- API Key、Token、Cookie、认证文件
|
||||
- 个人本地私密路径和个人隐私信息
|
||||
- 构建产物、日志、缓存、数据库 dump
|
||||
|
||||
## 开发前
|
||||
|
||||
1. 拉取最新代码。
|
||||
2. 阅读 `AGENTS.md`。
|
||||
3. 阅读 `.hermes/shared-memory/` 中与任务相关的文件。
|
||||
4. 阅读 `docs/README.md` 和任务相关分类 README。
|
||||
5. 阅读对应 PRD、设计、技术、经验或审计文档。
|
||||
6. 如果文档不足以指导编码,先补充或修正文档。
|
||||
|
||||
## 开发中
|
||||
|
||||
- 保持修改范围聚焦,不做无关重构。
|
||||
- 复用、修改、扩展现有系统优先,避免新建重复系统或页面。
|
||||
- 涉及中文文本时注意 UTF-8 编码和乱码排查。
|
||||
- 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run api-server` 与 `/healthz`。
|
||||
- 涉及 SpacetimeDB 表结构、发布或迁移时,先看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 和 `SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
- 涉及生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## 开发后
|
||||
|
||||
1. 运行与修改范围匹配的测试或验证命令。
|
||||
2. 更新相关 `docs/` 文档。
|
||||
3. 若产生长期有效知识,更新 `.hermes/shared-memory/`。
|
||||
4. 若形成可复用流程,考虑沉淀到 `.hermes/skills/`。
|
||||
5. 在提交信息中区分代码变更与文档/记忆变更。
|
||||
|
||||
## 文档阅读顺序
|
||||
|
||||
通用任务建议:
|
||||
|
||||
1. `README.md`
|
||||
2. `AGENTS.md`
|
||||
3. `.hermes/shared-memory/`
|
||||
4. `docs/README.md`
|
||||
5. `docs/experience/README.md`
|
||||
6. `docs/audits/README.md`
|
||||
7. 任务所属分类:`docs/design/`、`docs/technical/`、`docs/planning/`、`docs/prd/`、`docs/reference/`、`docs/tracking/`、`docs/operations/`
|
||||
|
||||
后端任务建议:
|
||||
|
||||
1. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
2. `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
|
||||
3. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
|
||||
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
|
||||
5. `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||
6. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
|
||||
7. `docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`
|
||||
|
||||
## 共享记忆更新准则
|
||||
|
||||
适合更新:
|
||||
|
||||
- 新增稳定架构约定
|
||||
- 新增长期开发流程
|
||||
- 已验证的踩坑和排障步骤
|
||||
- 重要接口契约变化
|
||||
- 团队协作规范变化
|
||||
- 文档索引或阅读顺序变化
|
||||
|
||||
不适合更新:
|
||||
|
||||
- 一次性临时计划
|
||||
- 未验证猜测
|
||||
- 个人偏好和个人路径
|
||||
- 敏感信息
|
||||
- 大段聊天记录
|
||||
27
.hermes/skills/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 仓库级 Hermes Skills
|
||||
|
||||
本目录预留给未来可共享的仓库级 Hermes skills。
|
||||
|
||||
## 什么时候沉淀为 Skill
|
||||
|
||||
当某个流程满足以下条件之一时,可以考虑从普通 Markdown 升级为 skill:
|
||||
|
||||
- 需要反复执行,且步骤稳定。
|
||||
- 涉及多个目录、命令或验证步骤。
|
||||
- 曾经踩过坑,需要明确规避步骤。
|
||||
- 新成员容易做错。
|
||||
- Hermes 在执行时需要强制加载专门知识。
|
||||
|
||||
## 建议结构
|
||||
|
||||
```text
|
||||
.hermes/skills/
|
||||
└─ skill-name/
|
||||
└─ SKILL.md
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- 不要把 API Key、Token、账号密码写入 skill。
|
||||
- 如果 skill 与 `AGENTS.md` 或 `docs/` 冲突,先更新冲突来源再使用。
|
||||
- Skill 应包含触发条件、步骤、坑点和验证方式。
|
||||
392
.hermes/skills/behavior-driven-development/SKILL.md
Normal file
@@ -0,0 +1,392 @@
|
||||
---
|
||||
name: behavior-driven-development
|
||||
description: 在 Genarrative 中需要用 BDD/行为驱动方式把 PRD、用户故事、验收标准转成可执行场景、Gherkin 用例、测试计划或 TDD 落地顺序时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [BDD, Gherkin, 验收标准, 用户故事, 测试, Genarrative]
|
||||
related_skills: [writing-plans, test-driven-development, systematic-debugging, requesting-code-review]
|
||||
---
|
||||
|
||||
# BDD 行为驱动开发流程
|
||||
|
||||
用于在 Genarrative 项目中,把产品需求、用户故事、业务规则和验收标准沉淀成清晰、可讨论、可验证的行为场景,并进一步映射到前端测试、API 测试、领域服务测试或 E2E 测试。
|
||||
|
||||
BDD 的重点不是“先写一堆 UI 自动化脚本”,而是让团队先对“用户在什么上下文下做什么,系统应该给出什么可观察结果”达成一致。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 从 PRD、设计文档、用户故事中提炼验收标准。
|
||||
- 功能需求容易产生理解偏差,需要先把行为边界说清楚。
|
||||
- 涉及前端、后端、运行态、异步任务、权限、埋点或状态流转的跨层功能。
|
||||
- 需要把“完成标准”写成 Given / When / Then 场景。
|
||||
- 需要在编码前规划测试覆盖:单元测试、组件测试、API 测试、E2E 测试。
|
||||
- 希望和 TDD 配合:先写行为场景,再把场景拆到 RED-GREEN-REFACTOR。
|
||||
- 需要给产品、测试、前后端开发共同评审的一份中文验收说明。
|
||||
|
||||
不适用:
|
||||
|
||||
- 纯重构且对外行为不变,只需要 characterization tests 或回归测试。
|
||||
- 一次性小改动,验收规则非常明确且无跨层状态。
|
||||
- 只想记录实现细节、代码结构或技术方案;这类内容更适合技术设计文档。
|
||||
|
||||
## 必读约束
|
||||
|
||||
1. 先描述用户可观察行为,再讨论实现细节。
|
||||
2. 场景必须可验证,避免“体验更好”“更智能”“合理展示”等不可判定表述。
|
||||
3. 不要把 Gherkin 写成低层 UI 点击脚本;UI 细节只在确实属于业务行为时出现。
|
||||
4. 中文 PRD、中文 UI 文案、中文剧情和注释不要擅自改成英文。
|
||||
5. 如果项目文档不足以支持准确落地,应先补齐 `docs/` 下的 PRD/设计/技术文档,再进入编码。
|
||||
6. BDD 场景要覆盖主要成功路径、关键失败路径、权限/登录态、边界条件和回归风险。
|
||||
|
||||
## 核心格式
|
||||
|
||||
推荐使用中文 Gherkin:
|
||||
|
||||
```gherkin
|
||||
功能: <业务能力名称>
|
||||
为了 <用户/业务价值>
|
||||
作为 <角色>
|
||||
我希望 <能力>
|
||||
|
||||
背景:
|
||||
假如 <所有场景共享的前置条件>
|
||||
|
||||
场景: <具体行为名称>
|
||||
假如 <上下文/已有状态>
|
||||
当 <用户动作或系统事件>
|
||||
那么 <可观察结果>
|
||||
而且 <额外可观察结果>
|
||||
|
||||
场景大纲: <带参数的行为名称>
|
||||
假如 <上下文中包含 <变量>>
|
||||
当 <动作>
|
||||
那么 <结果>
|
||||
|
||||
例子:
|
||||
| 变量 | 期望 |
|
||||
| A | X |
|
||||
| B | Y |
|
||||
```
|
||||
|
||||
英文关键字也可以使用:
|
||||
|
||||
```gherkin
|
||||
Feature: Work publish permission
|
||||
Scenario: Anonymous user attempts to publish a draft
|
||||
Given an anonymous user has a generated draft
|
||||
When the user clicks publish
|
||||
Then the login modal should be shown
|
||||
And the draft should remain unchanged
|
||||
```
|
||||
|
||||
在 Genarrative 项目内,若参与评审的人主要使用中文,优先中文场景;测试框架要求英文命名时,可以保留中文场景标题并在测试文件中使用英文 describe/it。
|
||||
|
||||
## 从需求提炼 BDD 场景
|
||||
|
||||
### Step 1: 识别角色和业务目标
|
||||
|
||||
先回答:
|
||||
|
||||
- 谁在使用?游客、已登录用户、创作者、管理员、审核人员、系统任务?
|
||||
- 用户想完成什么?创建、生成、保存、发布、试玩、查看、兑换、导出?
|
||||
- 业务价值是什么?降低创作门槛、保护权限、保证数据一致性、提升运营可见性?
|
||||
|
||||
### Step 2: 抽取领域词汇
|
||||
|
||||
建立统一术语,避免同一概念多种叫法:
|
||||
|
||||
- work / 作品
|
||||
- draft / 草稿
|
||||
- session / 创作会话
|
||||
- runtime / 运行态
|
||||
- publish / 发布
|
||||
- profile / 我的页签
|
||||
- invite code / 邀请码
|
||||
- analytics event / 埋点事件
|
||||
|
||||
场景中优先使用业务词,不要直接写组件名、函数名、数据库表名,除非这些就是用户可见对象。
|
||||
|
||||
### Step 3: 列出行为切片
|
||||
|
||||
按用户旅程切分:
|
||||
|
||||
1. 入口是否出现、是否可点击。
|
||||
2. 进入页面或工作台后的初始状态。
|
||||
3. 用户提交输入后的成功路径。
|
||||
4. 失败路径:未登录、参数无效、权限不足、网络/API 失败、异步任务失败。
|
||||
5. 状态持久化:刷新、返回、重新进入、跨设备或重新登录。
|
||||
6. 对外副作用:保存、发布、埋点、通知、导出、生成资产。
|
||||
7. 回归风险:旧入口、旧数据、移动端布局、中文编码。
|
||||
|
||||
### Step 4: 把每个切片写成 Given / When / Then
|
||||
|
||||
检查每个场景:
|
||||
|
||||
- Given 只描述前置状态,不写动作过程。
|
||||
- When 只描述一个主要触发动作或事件。
|
||||
- Then 描述可观察结果,可以被测试或人工验收。
|
||||
- 一个场景只验证一个核心行为;不要把完整长流程塞进一个巨型场景。
|
||||
|
||||
## Genarrative 场景模板
|
||||
|
||||
### 前端入口 / 页面行为
|
||||
|
||||
```gherkin
|
||||
功能: 我的页签反馈入口
|
||||
为了让用户能从个人中心提交问题
|
||||
作为已登录用户
|
||||
我希望在我的页签打开独立的帮助与反馈页面
|
||||
|
||||
场景: 已登录用户从我的页签进入反馈页面
|
||||
假如用户已登录并停留在我的页签
|
||||
当用户点击“帮助与反馈”入口
|
||||
那么系统应进入独立的反馈页面
|
||||
而且底部 tab 应保持选中“我的”
|
||||
而且页面不应展开在我的页签当前面板下方
|
||||
|
||||
场景: 用户从反馈页面返回我的页签
|
||||
假如用户正在反馈页面
|
||||
当用户点击返回按钮
|
||||
那么系统应回到平台主页面
|
||||
而且当前 tab 应为“我的”
|
||||
```
|
||||
|
||||
### 登录态 / 权限行为
|
||||
|
||||
```gherkin
|
||||
功能: 需要登录的发布能力
|
||||
为了保护作品归属和发布链路
|
||||
作为游客
|
||||
我不能在未登录时发布作品
|
||||
|
||||
场景: 游客尝试发布生成草稿
|
||||
假如游客已经生成一个草稿
|
||||
当游客点击发布按钮
|
||||
那么系统应打开登录弹窗
|
||||
而且不应创建正式作品
|
||||
而且草稿内容应保留在当前会话中
|
||||
```
|
||||
|
||||
### 后端 API / 领域规则
|
||||
|
||||
```gherkin
|
||||
功能: 作品正式游玩开始埋点
|
||||
为了统计不同玩法的正式游玩行为
|
||||
作为数据分析人员
|
||||
我希望每次用户进入正式作品运行态时记录统一事件
|
||||
|
||||
场景大纲: 支持的玩法进入正式游玩
|
||||
假如存在一个已发布的 <玩法> 作品
|
||||
当用户从作品详情进入正式游玩
|
||||
那么后端应记录 work_play_start 事件
|
||||
而且 scope_kind 应为 work
|
||||
而且 metadata 应包含 playType、workId、sourceRoute 和 userId
|
||||
|
||||
例子:
|
||||
| 玩法 |
|
||||
| puzzle |
|
||||
| match3d |
|
||||
| square-hole |
|
||||
| custom-world |
|
||||
| big-fish |
|
||||
| visual-novel |
|
||||
```
|
||||
|
||||
### 异步生成 / SSE 行为
|
||||
|
||||
```gherkin
|
||||
功能: AI 创作会话流式回复
|
||||
为了让用户看到生成进度
|
||||
作为创作者
|
||||
我希望提交创作指令后能收到流式反馈并最终得到可编辑草稿
|
||||
|
||||
场景: 成功生成草稿
|
||||
假如用户已登录并创建了创作会话
|
||||
当用户提交有效的创作指令
|
||||
那么系统应开始展示流式回复
|
||||
而且生成结束后应展示草稿结果
|
||||
而且会话快照应包含最新用户输入和 AI 回复
|
||||
|
||||
场景: 生成失败
|
||||
假如用户已登录并创建了创作会话
|
||||
当用户提交创作指令但后端生成失败
|
||||
那么系统应展示可理解的失败状态
|
||||
而且用户应能够重试
|
||||
而且不应覆盖上一次成功生成的草稿
|
||||
```
|
||||
|
||||
## 映射到测试类型
|
||||
|
||||
| BDD 场景关注点 | 推荐测试层级 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| 纯领域规则、状态机、校验 | Rust/TS 单元测试 | reducer、module-*、schema validator |
|
||||
| DTO 契约、API 请求响应 | API/contract 测试 | Axum handler、shared-contracts serde |
|
||||
| 页面渲染、按钮状态、表单校验 | 组件测试 | Vitest + Testing Library |
|
||||
| 路由、tab、页面阶段切换 | 前端集成测试 | appPageRoutes、FlowShell 行为 |
|
||||
| 登录态、发布、运行态完整链路 | E2E/smoke | Playwright 或项目 smoke 脚本 |
|
||||
| 埋点、副作用、后台导出 | 后端集成/API 测试 | tracking event、admin export |
|
||||
|
||||
原则:
|
||||
|
||||
- 不是每个 BDD 场景都必须落成 E2E。
|
||||
- 能在低层稳定验证的规则,不要强行放到脆弱的浏览器自动化里。
|
||||
- E2E 只覆盖最关键的用户旅程和跨层集成风险。
|
||||
|
||||
## 与 TDD 的配合方式
|
||||
|
||||
BDD 先回答“行为是什么”,TDD 再推动“代码怎么长出来”。
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 写 BDD 场景,确认业务行为和验收标准。
|
||||
2. 给每个场景标注测试层级:unit / component / API / E2E。
|
||||
3. 选择一个最小场景进入 TDD。
|
||||
4. RED:先写失败测试,测试名称对应场景标题。
|
||||
5. GREEN:实现最小代码让测试通过。
|
||||
6. REFACTOR:清理重复、命名、边界和文档。
|
||||
7. 回到下一个场景,直到主要路径和关键失败路径覆盖。
|
||||
|
||||
测试命名建议:
|
||||
|
||||
```ts
|
||||
describe('帮助与反馈入口', () => {
|
||||
it('已登录用户从我的页签进入独立反馈页面', () => {
|
||||
// Given ...
|
||||
// When ...
|
||||
// Then ...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Rust 测试命名建议:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn anonymous_user_cannot_publish_generated_draft() {
|
||||
// Given
|
||||
// When
|
||||
// Then
|
||||
}
|
||||
```
|
||||
|
||||
## 推荐产物
|
||||
|
||||
根据任务复杂度选择产物位置。使用本 skill 产出 Gherkin/BDD 场景时,必须先决定落点,不要把正式验收场景随手写在聊天记录里。
|
||||
|
||||
### Gherkin/BDD 场景默认落点
|
||||
|
||||
| 产物类型 | 推荐路径 | 适用场景 |
|
||||
| --- | --- | --- |
|
||||
| 实施前分析 / 临时计划 | `.hermes/plans/<task-name>-bdd-scenarios.md` | 某次 Hermes 开发任务前,用于澄清行为、拆测试、辅助实现;不一定作为长期产品依据。 |
|
||||
| 正式产品验收 / PRD 场景 | `docs/prd/<FEATURE>_BDD_YYYY-MM-DD.md` | 产品、测试、开发都需要长期参考的验收标准、用户故事、功能边界。 |
|
||||
| 技术/API/领域行为场景 | `docs/technical/<FEATURE>_BDD_YYYY-MM-DD.md` | 后端 API、领域规则、状态机、SpacetimeDB reducer/table、SSE/异步任务、埋点副作用。 |
|
||||
| 自动化 Gherkin feature 文件 | `tests/features/*.feature` 或 `e2e/features/*.feature` | 项目已接入 Cucumber/Playwright BDD 等 Gherkin runner 时。未接入前不要随意新建测试 runner 目录。 |
|
||||
| 稳定流程或团队经验 | `.hermes/shared-memory/` 或 `.hermes/skills/` | 不是某个功能验收,而是长期可复用的团队流程、坑点、执行规范。 |
|
||||
|
||||
默认规则:
|
||||
|
||||
1. 用户只说“先用 BDD 梳理一下/写场景/写 Gherkin”,默认写到 `.hermes/plans/<task-name>-bdd-scenarios.md`。
|
||||
2. 用户说“正式验收标准/PRD/产品文档/给测试验收”,写到 `docs/prd/<FEATURE>_BDD_YYYY-MM-DD.md`。
|
||||
3. 用户说“API 行为/后端规则/状态机/埋点/异步任务/SpacetimeDB”,写到 `docs/technical/<FEATURE>_BDD_YYYY-MM-DD.md`。
|
||||
4. 用户明确要求“可执行 feature 文件”且项目已有 runner,再写 `.feature` 文件;否则先写 Markdown BDD 文档,并在测试映射中标注未来自动化落点。
|
||||
5. 如果 BDD 场景会作为编码依据,文档中必须包含“测试映射”表,标注场景要落到哪些测试文件。
|
||||
|
||||
命名建议:
|
||||
|
||||
```text
|
||||
.hermes/plans/profile-feedback-bdd-scenarios.md
|
||||
docs/prd/PROFILE_FEEDBACK_BDD_2026-05-11.md
|
||||
docs/technical/WORK_PLAY_TRACKING_BDD_2026-05-11.md
|
||||
tests/features/profile-feedback.feature
|
||||
e2e/features/invite-code.feature
|
||||
```
|
||||
|
||||
### 其他配套产物
|
||||
|
||||
除 BDD/Gherkin 场景外,相关配套内容可放在:
|
||||
|
||||
- 实施计划:`.hermes/plans/<task-name>.md`
|
||||
- 产品/验收文档:`docs/prd/<FEATURE>_PRD_YYYY-MM-DD.md`
|
||||
- 技术设计:`docs/technical/<FEATURE>_TECHNICAL_YYYY-MM-DD.md`
|
||||
- 共享经验或稳定流程:`.hermes/shared-memory/` 或 `.hermes/skills/`
|
||||
|
||||
BDD 文档建议包含:
|
||||
|
||||
```markdown
|
||||
# <功能名> BDD 验收场景
|
||||
|
||||
## 背景
|
||||
- 需求来源:
|
||||
- 相关文档:
|
||||
- 相关入口/接口:
|
||||
|
||||
## 角色与目标
|
||||
- 角色:
|
||||
- 目标:
|
||||
- 非目标:
|
||||
|
||||
## 场景清单
|
||||
|
||||
### 功能: <能力>
|
||||
|
||||
```gherkin
|
||||
场景: <场景名>
|
||||
假如 ...
|
||||
当 ...
|
||||
那么 ...
|
||||
```
|
||||
|
||||
## 测试映射
|
||||
|
||||
| 场景 | 测试层级 | 目标文件 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| ... | component | ... | planned |
|
||||
|
||||
## 开放问题
|
||||
- ...
|
||||
```
|
||||
|
||||
注意:上面的 Markdown 模板中如果嵌套代码块,需要在真实文档里调整围栏长度,避免代码块提前闭合。
|
||||
|
||||
## 评审检查清单
|
||||
|
||||
- [ ] 每个场景都有清晰角色或业务上下文。
|
||||
- [ ] Given / When / Then 没有混入过多实现细节。
|
||||
- [ ] Then 都是可观察、可测试、可人工验收的结果。
|
||||
- [ ] 覆盖成功路径、失败路径、权限/登录态、边界条件。
|
||||
- [ ] 明确哪些场景需要自动化,哪些只需人工验收。
|
||||
- [ ] 自动化测试层级合理,没有把所有行为都塞进 E2E。
|
||||
- [ ] 中文文案、剧情、注释、文档没有被无意翻译或改写成英文。
|
||||
- [ ] 涉及中文文件修改时计划运行编码检查。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. **把 BDD 写成 UI 操作流水账。** 例如“点击第一个按钮,再点第二个按钮”。应改为用户意图和业务结果。
|
||||
2. **Then 不可验证。** “体验更顺滑”不是验收标准;要写成加载状态、错误提示、数据状态、页面阶段等可观察结果。
|
||||
3. **一个场景塞太多断言。** 长流程应拆成多个小场景,避免失败时不知道真正坏在哪里。
|
||||
4. **只写 happy path。** Genarrative 常见风险在登录态、刷新恢复、异步失败、端口/后端不可用、旧数据兼容和移动端布局。
|
||||
5. **把实现方案当成业务规则。** “调用某函数”通常不是用户行为;除非是 API/技术验收,否则放到技术设计或测试实现里。
|
||||
6. **BDD 和 TDD 脱节。** 写完场景后要映射测试层级和目标文件,否则场景容易停留在文档层。
|
||||
7. **场景词汇不统一。** work、draft、session、runtime、publish 等概念要和项目现有文档/代码保持一致。
|
||||
8. **忽略文档先行约束。** 若 PRD 不足以编码落地,先补文档,再开始工程修改。
|
||||
|
||||
## 验证与收口
|
||||
|
||||
执行 BDD 相关任务后,至少确认:
|
||||
|
||||
- [ ] 已产出或更新 BDD 场景文档/计划。
|
||||
- [ ] 场景已映射到具体测试层级和目标文件。
|
||||
- [ ] 若进入编码,已按 TDD 或等价方式先补测试。
|
||||
- [ ] 已运行相关验证命令,例如:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run test -- --run <相关测试文件>
|
||||
```
|
||||
|
||||
- [ ] 若涉及后端 Rust/API,按相关 DDD/SpacetimeDB 文档运行对应 cargo/npm/API smoke 验证。
|
||||
- [ ] 若产生长期有效经验,已同步到 `.hermes/shared-memory/` 或合适的仓库级 skill。
|
||||
225
.hermes/skills/genarrative-admin-backoffice/SKILL.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: genarrative-admin-backoffice
|
||||
short_description: 在 Genarrative/百梦后台新增或修改管理页、后台只读/写接口、导出能力时使用。
|
||||
description: 在 Genarrative/百梦后台新增或修改管理页、后台 BFF 接口、shared-contracts/admin DTO、admin-web 路由导航、Excel/表格导出与验证发布时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, 百梦后台, admin-web, 后台接口, Excel导出, Rust, Axum, SpacetimeDB]
|
||||
related_skills: [genarrative-play-type-integration]
|
||||
---
|
||||
|
||||
# Genarrative / 百梦后台管理功能接入流程
|
||||
|
||||
用于在 Genarrative 项目中新增或修改百梦后台管理端能力,包括后台页面、后台 API、管理端 DTO、导航路由、表格明细、导出、鉴权与验证。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增百梦后台页面或导航项,例如“埋点数据”“任务配置”“邀请码”。
|
||||
- 新增 `/admin/api/*` 接口。
|
||||
- 修改 `apps/admin-web` 的后台页面、API client、路由、Shell 导航。
|
||||
- 在后台展示 SpacetimeDB 表明细或统计数据。
|
||||
- 新增“总览 → 单表查询”这类表统计跳转与查询页联动能力时,优先复用现有总览页的表统计作为入口,不另造第二套表目录。
|
||||
- 后台导出 CSV / Excel / `.xls` 表格文件。
|
||||
- 后台数据页中与业务事件、任务、登录等链路相关的问题,不能只看后台页面;要追到对应前台/API/reducer 写入点,确认“数据何时产生”。例如排查 `daily_login` 时,不要假设它一定由认证登录接口写入;先核对当前分支实现。历史实现曾在 `GET /api/profile/tasks` 打开任务中心时写入、`POST /api/profile/tasks/{task_id}/claim` 领奖时兜底写入;后续方案A把“任务中心读取写埋点”拆出为独立 procedure,任务中心只读取/刷新进度,登录成功链路应显式调用每日登录埋点入口。
|
||||
|
||||
## 标准落地顺序
|
||||
|
||||
### 0. 先确认现有后台入口
|
||||
|
||||
在新增后台页或回答“后台某个数据在哪里”前,先核对是否已有入口,避免重复造页:
|
||||
|
||||
- 数据库表统计当前在后台“总览”页,不是独立页面:`apps/admin-web/src/pages/AdminOverviewPage.tsx` 的“表统计”面板。
|
||||
- 表统计行可直接跳转到表查询页:点击后设置 `window.location.hash = #tables?table=<tableName>`,由单独的 `#tables` 页接收参数并查询。
|
||||
- `#tables` 页应在首次加载和 `hashchange` 时都重新读取 `table` 参数,避免只在初次 mount 时生效。
|
||||
- 前端通过 `apps/admin-web/src/api/adminApiClient.ts` 的 `getAdminOverview(token)` 请求 `GET /admin/api/overview`。
|
||||
- 后端路由在 `server-rs/crates/api-server/src/app.rs` 挂载 `/admin/api/overview`,handler 为 `admin_overview`。
|
||||
- 表统计逻辑在 `server-rs/crates/api-server/src/admin.rs` 的 `fetch_database_overview`:先读 SpacetimeDB schema 表名,再逐表执行 `SELECT COUNT(*) AS row_count FROM {table_name}`;private 或当前身份不可见会显示“不可统计(private 或当前身份不可见)”。
|
||||
- DTO 在 `server-rs/crates/shared-contracts/src/admin.rs` 的 `AdminOverviewResponse` / `AdminDatabaseOverviewPayload` / `AdminDatabaseTableStatPayload`,前端对应类型在 `apps/admin-web/src/api/adminApiTypes.ts`。
|
||||
- 如果本次需求是“每张表都能查”,优先新增 `GET /admin/api/database/tables` 与 `GET /admin/api/database/tables/{tableName}/rows` 两个只读接口,并在前端新建统一的表查询页,而不是把查询逻辑塞回总览页。
|
||||
|
||||
### 1. 先补技术方案文档
|
||||
|
||||
项目要求工程修改前先检查/补充落地文档。若没有明确文档,先写到 `docs/technical/`,至少说明:
|
||||
|
||||
- 后台页面目标。
|
||||
- 后端接口路径、鉴权、query/body、response。
|
||||
- 数据来源和是否修改 SpacetimeDB schema。
|
||||
- 前端页面字段、筛选项、导出格式。
|
||||
- 验收命令。
|
||||
|
||||
示例参考:
|
||||
|
||||
- `references/admin-tracking-events-export-2026-05-07.md`
|
||||
- `references/admin-database-table-query-2026-05-08.md`
|
||||
|
||||
### 2. 后端 DTO 放 shared-contracts/admin
|
||||
|
||||
文件:
|
||||
|
||||
- `server-rs/crates/shared-contracts/src/admin.rs`
|
||||
|
||||
做法:
|
||||
|
||||
- 新增 request/query/response DTO。
|
||||
- 使用 `#[serde(rename_all = "camelCase")]`。
|
||||
- 添加中文注释。
|
||||
- 字段名与前端管理端类型保持一致。
|
||||
|
||||
如果 `apps/admin-web` 当前没有直接消费 Rust shared-contracts 生成物,还要同步:
|
||||
|
||||
- `apps/admin-web/src/api/adminApiTypes.ts`
|
||||
|
||||
### 3. 后端 handler 放 api-server/admin.rs
|
||||
|
||||
文件:
|
||||
|
||||
- `server-rs/crates/api-server/src/admin.rs`
|
||||
- `server-rs/crates/api-server/src/app.rs`
|
||||
|
||||
要求:
|
||||
|
||||
- Handler 使用 `Extension(_admin): Extension<AuthenticatedAdmin>`,并在 router 中套 `require_admin_auth`。
|
||||
- 只读接口也必须走后台鉴权。
|
||||
- query 参数使用 `Query<T>`。
|
||||
- 返回 `json_success_body(Some(&request_context), payload)`。
|
||||
- 在 `app.rs` 挂到 `/admin/api/...`。
|
||||
|
||||
### 4. 读取 SpacetimeDB 表明细时优先 HTTP SQL 只读
|
||||
|
||||
适合后台只读运营页:
|
||||
|
||||
- 不改表结构。
|
||||
- 不新增 reducer。
|
||||
- API Server 通过 SpacetimeDB HTTP SQL 读取真实数据。
|
||||
|
||||
注意:
|
||||
|
||||
- SQL 字段固定白名单,不要 `SELECT *`。
|
||||
- 用户输入只允许有限筛选字段,手动 trim、白名单枚举、字符串转义。
|
||||
- limit 必须 clamp,例如默认 200、最大 1000。
|
||||
- SpacetimeDB 2.2 HTTP SQL 不支持 `ORDER BY`;如果后台需要倒序展示明细,SQL 中不要拼 `ORDER BY`,先查有限 `LIMIT`,再在 api-server 内按时间字段排序,否则会返回 `HTTP 400 Unsupported: SELECT ... ORDER BY ... LIMIT ...`。
|
||||
- 如果 HTTP SQL 返回 `no such table ... If the table exists, it may be marked private`,不要急着改表名或新增 reducer;先确认本地 CLI 是否以当前 standalone 的 identity/token 登录。清空本地数据库或重建 standalone 后,旧 CLI token 可能看不到 private table。按“本地 private table SQL 权限修复”流程用 `/v1/identity` 获取 token,再 `spacetime login --token` 登录。
|
||||
- SQL 解析要兼容 SpacetimeDB HTTP SQL 的 statement array + rows 形态。
|
||||
- SpacetimeDB HTTP SQL 读取 private table 时,enum / Option / Timestamp 可能以 SATS 原始 JSON 返回,例如 `scope_kind=[3,[]]`、`Some("user")=[0,"user"]`、`None=[1,[]]`、`Timestamp=[1778207451731746]`。后台列表、详情弹窗和 Excel 导出不要直接展示这些原始形态;应在 api-server 解析层或前端展示层转换为人可读值:enum 映射为业务字符串,Option 的 None 显示 `-`,微秒级 Timestamp 格式化为本地可读时间。
|
||||
- 可复用已有 `/v1/database/{db}/sql` 请求风格和 token 配置。
|
||||
|
||||
### 5. 前端接入 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/app/AdminShell.tsx`
|
||||
- `apps/admin-web/src/app/AdminApp.tsx`
|
||||
- `apps/admin-web/src/pages/<AdminXxxPage>.tsx`
|
||||
- `apps/admin-web/src/styles/admin.css`
|
||||
|
||||
接入步骤:
|
||||
|
||||
1. 在 `adminApiTypes.ts` 增加 query/entry/list 类型。
|
||||
2. 在 `adminApiClient.ts` 增加 API 方法;用 `URLSearchParams` 拼非空 query。
|
||||
3. 在 `adminRoutes.ts` 增加 route id、label、hash。
|
||||
4. 在 `AdminShell.tsx` 增加 route icon,`routeIcons` 必须覆盖全部 `AdminRouteId`。
|
||||
5. 在 `AdminApp.tsx` import 并按 routeId 渲染页面。
|
||||
6. 新增页面组件,保持 UI 简洁,不写大段规则说明。
|
||||
7. 如果页面通过 hash 携带子参数,路由解析和页内参数解析要分开:`resolveAdminRoute()` 只负责路由片段,页面组件自己解析 `?table=` 之类的查询参数;同时要监听 `hashchange`,避免切页后参数不同步。
|
||||
8. 列表行点击跳转优先用 hash,不要额外引入全局路由库或重新发明一套页面状态系统。
|
||||
|
||||
## Excel 导出推荐做法
|
||||
|
||||
后台运营导出不一定要引入 `xlsx` 依赖;简单表格可用浏览器端 HTML table + `.xls`:
|
||||
|
||||
- Blob MIME:`application/vnd.ms-excel;charset=utf-8`
|
||||
- 文件扩展名:`.xls`
|
||||
- 文本前加 UTF-8 BOM / `<meta charset="UTF-8">`。
|
||||
- 所有单元格做 HTML escape。
|
||||
- ID、大数字、日期类字段使用 `mso-number-format:'\@';` 保持文本格式,避免 Excel 科学计数法。
|
||||
- 导出当前筛选结果,避免后端新增 Excel 库依赖。
|
||||
|
||||
## 本地启动与联调
|
||||
|
||||
后台改完后如需本地查看页面和接口,优先按本次联调范围选择脚本:
|
||||
|
||||
```bash
|
||||
# 只看后台页面 + api-server,不要求 SpacetimeDB 真实数据
|
||||
npm run api-server
|
||||
npm run admin-web:dev -- --host 127.0.0.1
|
||||
|
||||
# 完整 Rust 本地栈:SpacetimeDB + 发布模块 + api-server + 主站 + 后台
|
||||
npm run dev
|
||||
```
|
||||
|
||||
验证地址通常为:
|
||||
|
||||
- `npm run api-server` 单独启动:api-server `http://127.0.0.1:3100/healthz`,后台前端 `http://127.0.0.1:5173/admin/`。
|
||||
- `npm run dev` 完整栈:SpacetimeDB `http://127.0.0.1:3101/v1/ping`,api-server `http://127.0.0.1:8082/healthz`,主站 `http://127.0.0.1:3000/`,后台 `http://127.0.0.1:3102/admin/`。
|
||||
|
||||
注意:
|
||||
|
||||
- `npm run api-server` 首次启动可能先编译 Rust,后台进程短时间内无完整日志;等待编译完成后再查端口。
|
||||
- 不要默认用 `3200` 验证 api-server;当前脚本环境变量常见为 `GENARRATIVE_API_PORT=3100`。不确定时用 `ss -ltnp | grep api-server` 或读取进程环境核对,敏感值输出必须打码。
|
||||
- `admin-web` 的 `/` 可能返回 302 跳转到 `/admin/`;验证前端时直接请求 `/admin/`。
|
||||
- api-server 启动日志中 SpacetimeDB `127.0.0.1:3101` 连接被拒绝,不一定代表 api-server 没起来;只表示依赖的本地 SpacetimeDB 不可用。后台中需要读 SpacetimeDB 的页面(如埋点明细、表查询)要等 SpacetimeDB 可用后才能返回真实数据。
|
||||
- `npm run dev` 依赖 `spacetime` CLI;先用 `command -v spacetime && spacetime --version` 确认可用。
|
||||
- 本地和人工排障不再使用 `spacetime --root-dir`。如果看到 `bin/current/spacetimedb-cli` 缺失类错误,优先确认是否仍在运行旧脚本或旧发布包;本地开发应使用 `npm run dev` / `npm run dev:rust`,通过项目脚本和 `--data-dir` 隔离 SpacetimeDB 数据目录,不再把用户级 SpacetimeDB 安装同步到项目目录。
|
||||
|
||||
- `scripts/dev-rust-stack.sh` 默认 `api timeout: 300s`. 合并 master 后首次 Rust 依赖/工作区重编译可能超过 300s,导致完整 `npm run dev` 在 api-server 就绪前超时并回收 SpacetimeDB。先让 Rust 编译完成,或临时用 `bash scripts/dev-rust-stack.sh --skip-spacetime --skip-publish --api-timeout-seconds 900` 预热 api-server 编译;之后再重新跑完整 `npm run dev`。
|
||||
- 用户贴出的 Hermes background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
# Rust 格式化检查
|
||||
cd server-rs
|
||||
cargo fmt -p api-server -p shared-contracts --check
|
||||
|
||||
# 后端相关测试,按测试名过滤
|
||||
cargo test -p api-server admin_tracking -- --nocapture
|
||||
|
||||
# 前端后台类型检查 / 构建
|
||||
cd ..
|
||||
npm run admin-web:typecheck
|
||||
npm run admin-web:build
|
||||
|
||||
# 中文/编码检查
|
||||
npm run check:encoding
|
||||
|
||||
# diff 空白检查
|
||||
git diff --check
|
||||
```
|
||||
|
||||
如果 `npm run admin-web:typecheck` 报 `Cannot find module .../node_modules/typescript/bin/tsc`,说明当前 worktree 未安装 npm 依赖;先运行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
不要把该错误误判成 TypeScript 代码错误。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 只在 `app.rs` import handler 不够,必须实际 `.route(...)` 挂载,并套 `require_admin_auth`。
|
||||
2. `cargo fmt --manifest-path server-rs/Cargo.toml` 在该 workspace 可能报 `Failed to find targets`;进入 `server-rs` 后用 `cargo fmt --all` 或 `cargo fmt -p api-server -p shared-contracts --check`。
|
||||
3. `cargo fmt --all` 可能格式化不相关 Rust 文件;提交前用 `git status` 检查并 revert 非本任务文件。
|
||||
4. patch 工具对 Rust 单文件 lint 可能用 Rust 2015 edition 误报 `async fn is not permitted in Rust 2015`;以 `cargo test/check` 为准。
|
||||
5. `adminRoutes` 新增 route id 后,`AdminShell.routeIcons` 必须同步,否则 TypeScript 会因 `satisfies Record<AdminRouteId, ...>` 报错。
|
||||
- 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`。
|
||||
- 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。
|
||||
- 若用户追问“之前不是说要把 npm run dev 修好吗”这类已承诺的 dev 启动问题,不要只解释;先复现 `npm run dev`,再按启动日志修脚本并验证到服务就绪。WSL/Linux 下本地开发应走 `spacetime start --data-dir=server-rs/.spacetimedb/local/data` 这一类数据目录隔离,不再用项目级 `--root-dir`,详见 `references/dev-rust-stack-startup-2026-05-08.md`。
|
||||
- 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `references/admin-database-table-query-2026-05-08.md`:本次后台数据库表查询接入的实现要点、校验规则与验证结果。
|
||||
- `references/admin-tracking-events-export-2026-05-07.md`:本次新增后台“埋点数据”页、SpacetimeDB HTTP SQL 只读明细、前端 `.xls` 导出的实现细节。
|
||||
- `references/private-table-sql-token-refresh.md`:本地清库/重建 standalone 后,用 `/v1/identity` + `spacetime login --token` 刷新 CLI token,以便 HTTP SQL 读取 private table。
|
||||
- `references/spacetimedb-http-sql-sats-display.md`:通过 HTTP SQL 读取 private table 时,enum / Option / Timestamp 的 SATS 原始 rows 如何转换为后台列表、详情和 Excel 可读值。
|
||||
- `references/daily-login-tracking-trigger-points.md`:排查后台 `daily_login` 埋点为何不是登录接口写入,而是任务中心读取/领奖兜底写入的触发点记录。
|
||||
- `references/daily-login-auth-closure.md`:将方案A拆出的每日登录埋点入口接入真实认证成功链路时的推荐接入点、非阻断语义、测试和提交注意事项。
|
||||
- `references/dev-rust-stack-startup-2026-05-08.md`:`npm run dev` / `scripts/dev-rust-stack.sh` 在 WSL/Linux 下改用用户级 SpacetimeDB CLI、项目数据目录和显式 publish server,避免项目级 `--root-dir` 与冷编译超时的修复记录。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 本次后台表查询接入的可复用经验
|
||||
|
||||
## 需求落点
|
||||
- 后台“总览”页的表统计仍保留,只把每张表的表名改成可点击跳转到 `#tables?table=<name>`。
|
||||
- 新增独立 `#tables` 页承载表选择、关键词搜索、JSON filters、limit、行详情弹窗。
|
||||
|
||||
## 后端实现要点
|
||||
- 新增只读接口:
|
||||
- `GET /admin/api/database/tables`
|
||||
- `GET /admin/api/database/tables/{table_name}/rows`
|
||||
- 表名必须来自 schema 白名单;再加一层 identifier 校验,避免任意 SQL 表名注入。
|
||||
- `limit` 必须 clamp;本次实现使用默认 100、最大 500。
|
||||
- `search` / `filters` 不进入 SQL 字符串:
|
||||
- SQL 只负责 `SELECT * FROM {table_name} LIMIT {limit}`
|
||||
- 返回后在 api-server 内存中过滤
|
||||
- `filters` 仅接受 JSON object,按列名匹配;非 object 直接 400
|
||||
- SpacetimeDB HTTP SQL 返回可能是 statement array + rows,解析时要兼容这一层结构。
|
||||
|
||||
## 前端实现要点
|
||||
- `adminRoutes` 必须新增 `tables`,`AdminShell.routeIcons` 也要同步覆盖。
|
||||
- `AdminApp` 需要显式渲染 `AdminDatabaseTablesPage`。
|
||||
- worktree 下可能没有本地 `node_modules/typescript/bin/tsc`,而根目录有依赖;在验证前可以临时把根目录 `node_modules` 软链到 worktree 再执行 `npm run admin-web:typecheck`,验证后删除软链,避免污染 git 状态。
|
||||
|
||||
## 验证结果
|
||||
- `cargo test -p api-server admin_database -- --nocapture` 通过。
|
||||
- `cargo fmt --manifest-path Cargo.toml -p api-server -p shared-contracts --check` 通过。
|
||||
- `npm run admin-web:typecheck` 通过。
|
||||
- `npm run admin-web:build` 通过。
|
||||
- `npm run check:encoding` 通过。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 后台埋点数据页与本地启动验证记录(2026-05-07)
|
||||
|
||||
## 背景
|
||||
|
||||
本次在 Genarrative/百梦后台新增“埋点数据”页:
|
||||
|
||||
- 后端新增 `GET /admin/api/tracking/events`。
|
||||
- shared-contracts 新增 admin tracking query/list/entry DTO。
|
||||
- 前端新增 `#tracking` 路由、导航、表格、详情面板与 `.xls` 导出。
|
||||
- 导出使用浏览器端 HTML table + Excel MIME,不引入 `xlsx` 依赖。
|
||||
|
||||
## 关键实现点
|
||||
|
||||
- 后台只读接口仍必须套 `require_admin_auth`。
|
||||
- SpacetimeDB 明细读取使用 HTTP SQL,不新增 reducer、不改 schema。
|
||||
- SQL 固定白名单列,不用 `SELECT *`。
|
||||
- Query 只允许 `eventKey/userId/scopeKind/scopeId/limit`。
|
||||
- `scopeKind` 只允许 `site/work/module/user`。
|
||||
- limit 默认 200,最大 1000。
|
||||
- SpacetimeDB HTTP SQL 响应要兼容 statement array + `rows`,Option 可能表现为 `{ "some": value }`。
|
||||
- 前端导出 `.xls` 时给单元格加 `mso-number-format:'\\@';`,防止 Excel 把 ID 转科学计数法。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
npm install # 若 node_modules 缺失
|
||||
npm run admin-web:typecheck
|
||||
npm run admin-web:build
|
||||
npm run check:encoding
|
||||
|
||||
cd server-rs
|
||||
cargo fmt -p api-server -p shared-contracts --check
|
||||
cargo test -p api-server admin_tracking -- --nocapture
|
||||
```
|
||||
|
||||
## 本地启动观察
|
||||
|
||||
启动命令:
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
npm run api-server
|
||||
npm run admin-web:dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
实际验证:
|
||||
|
||||
- api-server 监听 `127.0.0.1:3100`,健康检查为 `http://127.0.0.1:3100/healthz`。
|
||||
- admin-web 监听 `127.0.0.1:5173`,后台地址为 `http://127.0.0.1:5173/admin/`。
|
||||
- 请求 `http://127.0.0.1:5173/` 会 302 到 `/admin/`。
|
||||
- 不能默认用 3200 检查 api-server;本地脚本通过 `GENARRATIVE_API_PORT=3100` 启动。
|
||||
- 如果启动日志出现 SpacetimeDB `127.0.0.1:3101` connection refused,api-server 仍可能已正常监听;这是依赖的本地 SpacetimeDB 未启动,埋点页读真实数据会受影响。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 真实登录成功链路接入每日登录埋点(2026-05-08)
|
||||
|
||||
## 背景
|
||||
|
||||
后台“埋点数据”页要能看到真实登录产生的 `daily_login`。此前已完成方案 A:把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure/client 方法,避免后台查看或刷新任务中心污染登录数据。
|
||||
|
||||
闭环时不要再把写入点放回任务中心读取流程;应在认证成功且会话签发后显式调用每日登录埋点入口。
|
||||
|
||||
## 推荐接入点
|
||||
|
||||
在 `api-server` 认证成功路径中,先创建/签发会话,再非阻断记录埋点,再同步认证快照并返回:
|
||||
|
||||
1. `create_auth_session` / `create_password_auth_session` 成功。
|
||||
2. 调用统一 helper:`record_daily_login_tracking_event_after_auth_success(...)`。
|
||||
3. helper 调用 `state.spacetime_client().record_daily_login_tracking_event(user_id.to_string()).await`。
|
||||
4. 成功写 `info`,失败写 `warn`,不能把埋点失败返回给用户。
|
||||
|
||||
已验证的真实登录链路包括:
|
||||
|
||||
- 手机验证码登录:`server-rs/crates/api-server/src/phone_auth.rs` 的 `phone_login`。
|
||||
- 密码登录入口:`server-rs/crates/api-server/src/password_entry.rs` 的 `password_entry`。
|
||||
- 重置密码后自动登录:`server-rs/crates/api-server/src/password_management.rs` 的 `reset_password`。
|
||||
- 微信 OAuth 回调登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `handle_wechat_callback`。
|
||||
- 微信绑定手机号后自动登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `bind_wechat_phone`。
|
||||
- refresh cookie 续期:`server-rs/crates/api-server/src/refresh_session.rs` 的 `refresh_session`。在 `rotate_session` 成功并签发新 access token 后记录,`login_method` 应使用 `rotated.session.issued_by_provider.clone()`,不要固定写成 Password。
|
||||
|
||||
## 关键实现约束
|
||||
|
||||
- 埋点是运营数据,必须保持非阻断:SpacetimeDB 调用失败只记录日志,不影响登录成功返回。
|
||||
- helper 建议放在 `auth_session.rs`,避免各登录 handler 重复错误处理。
|
||||
- refresh cookie 续期也被产品视为一次每日登录触发;接入 `refresh_session.rs` 时必须放在 token rotate 和 access token 签发成功之后,且保持非阻断,避免刷新失败或缺 cookie 时误写埋点。
|
||||
- `handle_wechat_callback` 如果要记录 `request_id/operation`,需要在 handler 参数中补 `Extension<RequestContext>`;确认路由层已注入 RequestContext。
|
||||
- 单元测试默认不启动 SpacetimeDB。若直接调用真实 `spacetime_client` 会让既有认证测试依赖外部服务;可在 `#[cfg(test)]` 下让 helper no-op,仅用编译和现有登录成功测试覆盖调用点不破坏返回。
|
||||
- 后续如需要严格断言“helper 被调用”,应优先为 Spacetime client 引入可注入 trait/mock,而不是让 API 单测连接真实 SpacetimeDB。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo fmt -p api-server --check
|
||||
cargo check -p api-server
|
||||
cargo check -p spacetime-client
|
||||
cargo test -p api-server auth_session -- --nocapture
|
||||
cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture
|
||||
cargo test -p api-server password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie -- --nocapture
|
||||
cargo test -p api-server phone_login_creates_user_and_sets_refresh_cookie -- --nocapture
|
||||
cd ..
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 提交注意
|
||||
|
||||
- 不要提交 `.env.local`、`.env.secrets.local` 或任何 token/密码/连接串。
|
||||
- 若工作区里有本地敏感文件,只提交明确改动的 Rust 文件和 `docs/technical/*` 文档。
|
||||
@@ -0,0 +1,97 @@
|
||||
# Genarrative daily_login 埋点触发点排查记录
|
||||
|
||||
## 背景
|
||||
|
||||
用户在后台“埋点数据”页看到 `daily_login` 事件后询问:为什么每日登录埋点看起来只有在用户领取每日登录任务奖励后才记录,而不是登录时记录。
|
||||
|
||||
## 结论
|
||||
|
||||
当前代码口径里,`daily_login` 不是认证登录成功瞬间写入的事件。它挂在个人任务链路:
|
||||
|
||||
- `GET /api/profile/tasks`:读取任务中心时会记录当日 `daily_login`,并刷新任务进度。
|
||||
- `POST /api/profile/tasks/{task_id}/claim`:领取任务奖励时,如果任务配置是 daily_login,会兜底记录当日 `daily_login`。
|
||||
|
||||
因为 `record_daily_login_tracking_event` 用 `daily-login:<user_id>:<day_key>` 作为 event id,并先查重,所以同一用户同一北京自然日最多写一条。
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`
|
||||
- 第 47 行左右写明:用户打开任务中心时后端幂等记录当日 `daily_login`;点击领取时校验进度和领奖记录。
|
||||
- 接口说明中写明 `GET /api/profile/tasks` 会读取任务中心并记录当日登录埋点。
|
||||
- `server-rs/crates/api-server/src/runtime_profile.rs`
|
||||
- `get_profile_task_center` 调用 `state.spacetime_client().get_profile_task_center(user_id)`。
|
||||
- `claim_profile_task_reward` 调用 `state.spacetime_client().claim_profile_task_reward(user_id, task_id)`。
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- `get_profile_task_center_snapshot(..., record_login_event: bool)` 在 `record_login_event` 为 true 时调用 `record_daily_login_tracking_event`。
|
||||
- `claim_profile_task_reward_record` 对 daily_login 任务调用 `record_daily_login_tracking_event` 作为兜底。
|
||||
- `record_daily_login_tracking_event` 负责生成 event id、查重、写入 `tracking_event` 和更新 `tracking_daily_stat`。
|
||||
- `server-rs/crates/api-server/src/phone_auth.rs`
|
||||
- 手机号登录成功后做验证码校验、新用户奖励、邀请码绑定、session 签发、认证快照同步;当前没有写入 `daily_login`,也没有调用任务中心接口。
|
||||
|
||||
## 排查方法
|
||||
|
||||
1. 不要只看后台埋点页。先搜事件 key 和任务接口:
|
||||
|
||||
```bash
|
||||
git grep -n "daily_login\|tracking_event\|get_profile_task_center\|claim_profile_task_reward" -- server-rs apps docs
|
||||
```
|
||||
|
||||
2. 对照设计文档中的事件口径:
|
||||
|
||||
```bash
|
||||
sed -n '35,58p' docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
|
||||
```
|
||||
|
||||
3. 追 API handler 到 SpacetimeDB reducer:
|
||||
|
||||
- `api-server/src/runtime_profile.rs`
|
||||
- `spacetime-client` 对应 procedure wrapper
|
||||
- `spacetime-module/src/runtime/profile.rs`
|
||||
|
||||
4. 再看真实登录接口是否写入同一事件。手机号登录入口是:
|
||||
|
||||
- `server-rs/crates/api-server/src/phone_auth.rs::phone_login`
|
||||
|
||||
## 常见误判
|
||||
|
||||
- 后台只是在展示 `tracking_event`,不是事件产生点。
|
||||
- “每日登录”这个中文名容易让人以为它必然在 auth 登录成功时写入;当前实现不是这样。
|
||||
- 如果用户登录后没有打开“我的/任务中心”,只在领奖时触发 claim 接口,就会表现为“领奖时才出现埋点”。
|
||||
- 领取接口里的写入是兜底,避免用户直接点击领取时因为未先打开任务中心而无法完成每日任务。
|
||||
|
||||
## 后续方案A落地记录
|
||||
|
||||
在后续修复中,采用“方案A”:把“读取任务中心时顺手记录每日登录埋点”拆成独立 SpacetimeDB procedure,使任务中心读取只负责读取/刷新进度,避免后台查看或刷新任务中心时污染埋点数据。
|
||||
|
||||
关键变化:
|
||||
|
||||
- `server-rs/crates/module-runtime/src/domain.rs`
|
||||
- 新增 `RuntimeTrackingEventProcedureResult { ok, error_message }`,用于返回纯事件写入结果。
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- 新增 `record_daily_login_tracking_event_and_return(ctx, input)` procedure。
|
||||
- `get_profile_task_center` 注释和行为调整为只读取/刷新任务进度,不再作为每日登录埋点产生点。
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- 新增 `record_daily_login_tracking_event(user_id)` client 方法,调用新 procedure。
|
||||
- `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- 新增 `map_runtime_tracking_event_procedure_result`,把 `ok=false` 映射为 `procedure_failed`。
|
||||
|
||||
落地注意:
|
||||
|
||||
- 这一步只拆出后端写入入口,不等于所有登录方式已经接入;接入手机号/微信/密码等认证成功链路前,需确认产品口径:统计登录成功是否应覆盖所有登录方式,以及事件失败是否阻断登录。
|
||||
- 修改 `spacetime-module` procedure 后,通常需要重新生成/同步 SpacetimeDB 绑定;若直接手补 `spacetime-client/src/module_bindings`,要非常谨慎,因为该目录声明为自动生成。
|
||||
- patch 工具可能对 Rust 单文件使用 2015 edition lint,看到 `async fn is not permitted in Rust 2015` 时不要立即按该误报改代码,应以 `cd server-rs && cargo test/check ...` 为准。
|
||||
|
||||
验证记录:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p module-runtime runtime_profile_task_status_matches_progress_and_claim -- --nocapture
|
||||
```
|
||||
|
||||
该测试通过可验证任务中心领域进度/领取逻辑未被破坏;完整接入认证链路后还应补 api-server 层登录成功埋点测试。
|
||||
|
||||
## 后续设计建议
|
||||
|
||||
如果产品口径要求“登录成功就算每日登录”,应把 `daily_login` 写入点前移到统一 auth 登录成功链路,并覆盖手机号/微信/密码等登录方式;任务中心只读取进度或最多保留幂等兜底。
|
||||
|
||||
如果需要同时分析真实登录和任务完成,建议新增独立事件,例如 `auth_login_success` / `user_login_success`,让 `daily_login` 继续表示每日任务完成条件。
|
||||
@@ -0,0 +1,46 @@
|
||||
# `npm run dev` / `scripts/dev-rust-stack.sh` 启动修复记录
|
||||
|
||||
## 症状
|
||||
- 多个 worktree 同时本地开发时,SpacetimeDB 数据库名可能相同,早期曾用项目级 CLI root 隔离 CLI 状态来规避冲突。
|
||||
- 实测后确认:真正需要隔离的是 standalone 的 `data-dir`,不需要把 publish 也绑到项目级 CLI root。
|
||||
- 早期脚本曾通过把用户级 SpacetimeDB 可执行文件目录同步到 `server-rs/.spacetimedb/local/bin/current` 来满足 standalone 回调需求,但这会把整套可执行文件复制进项目本地目录,维护成本高,也容易和用户级 CLI 版本漂移。
|
||||
- 多个 worktree 同时启动时,SpacetimeDB 端口可能冲突;CLI 会询问是否使用最近的可用端口。
|
||||
- 若 `npm run dev` 从仓库根目录直接执行 `spacetime publish --module-path server-rs/crates/spacetime-module`,内部 Cargo 不一定读取 `server-rs/.cargo/config.toml`,可能绕过 sccache/linker/target 缓存策略,表现为 spacetime-module 每次像全量重编译。
|
||||
- `api-server` 首次冷编译时,默认 300 秒超时不够,容易在就绪前被回收。
|
||||
|
||||
## 当前方案
|
||||
1. SpacetimeDB 可执行文件继续使用用户环境里的 `spacetime` 命令
|
||||
- 启动 standalone 时不再复制 `spacetimedb-cli`、版本目录或 `bin/current`。
|
||||
- `spacetime start` 不再通过工程内 CLI root 寻找可执行文件。
|
||||
2. 数据目录显式指定到项目本地
|
||||
- 默认 `SPACETIME_DATA_DIR=${SERVER_RS_DIR}/.spacetimedb/local/data`。
|
||||
- 启动命令使用 `spacetime start --data-dir "${SPACETIME_DATA_DIR}" --listen-addr ...`。
|
||||
- 如需临时切换数据目录,可传 `--spacetime-data-dir <path>`。
|
||||
3. 端口冲突时自动接受 SpacetimeDB 建议端口
|
||||
- 启动时不传 `--non-interactive`。
|
||||
- 脚本向 `spacetime start` 发送回车,接受“最近可用端口”的默认建议。
|
||||
- 随后从启动日志中的 `Starting SpacetimeDB listening on ...` 解析实际端口。
|
||||
- 解析出的实际端口会覆盖 `SPACETIME_SERVER`,后续 publish、api-server、前端代理统一使用这个端口。
|
||||
4. publish 不再使用项目级 CLI root,但要从 `server-rs` 目录执行
|
||||
- 发布模块改为在 `server-rs` 下执行 `spacetime publish ... --server "${SPACETIME_SERVER}" ...`。
|
||||
- 这样 publish 使用用户级 CLI 默认身份/配置,不再依赖 worktree 内 CLI root。
|
||||
- 同时确保内部 Cargo 能读取 `server-rs/.cargo/config.toml`,复用项目级 sccache/linker/target 缓存策略,避免 `npm run dev` 比手动 publish 更容易触发慢速重编译。
|
||||
5. 提高 api-server 就绪等待时间
|
||||
- `API_SERVER_TIMEOUT_SECONDS` 保持 600,降低首次冷编译误判失败概率。
|
||||
|
||||
## 复现 / 验证
|
||||
- 运行脚本语法检查:`bash -n scripts/dev-rust-stack.sh`。
|
||||
- 运行帮助检查:`bash scripts/dev-rust-stack.sh --help`,确认有 `--spacetime-data-dir`。
|
||||
- 运行 `npm run dev` 后观察日志:
|
||||
- 输出 `spacetime data: .../server-rs/.spacetimedb/local/data`。
|
||||
- 不再出现同步/复制本机 SpacetimeDB 安装到项目 CLI root 的日志。
|
||||
- SpacetimeDB 能正常监听,并输出 `spacetime actual: http://127.0.0.1:<实际端口>`。
|
||||
- 若默认端口被占用,脚本应自动接受 SpacetimeDB 建议端口,并用实际端口发布模块、启动 api-server 和前端代理。
|
||||
- 模块发布成功。
|
||||
- 第二次无改动 publish 应接近手动 `cd server-rs && spacetime publish ...` 的增量速度。
|
||||
- api-server 进入健康检查等待并最终可访问 `/healthz`。
|
||||
|
||||
## 相关文件
|
||||
- `scripts/dev-rust-stack.sh`
|
||||
- `server-rs/.spacetimedb/local/data/`
|
||||
- `server-rs/.cargo/config.toml`
|
||||
@@ -0,0 +1,37 @@
|
||||
# 本地 private table SQL 权限修复
|
||||
|
||||
场景:
|
||||
- 后台或 api-server 通过 SpacetimeDB HTTP SQL 读取 `tracking_event` 这类 private table。
|
||||
- 本地清库、重建 standalone 或重新发布模块后,原 CLI token 失效,SQL 可能报 `no such table ... If the table exists, it may be marked private`。
|
||||
|
||||
操作步骤:
|
||||
|
||||
1. 清空本地 SpacetimeDB 数据目录
|
||||
- 使用项目脚本停止本地实例后,备份或删除 `server-rs/.spacetimedb/local/data`。
|
||||
- 只清本地开发环境,不要误伤远端或其他 worktree。
|
||||
|
||||
2. 启动本地 standalone
|
||||
- 用项目约定的 `scripts/dev-rust-stack.sh` 或等价命令启动 `spacetime`。
|
||||
- 确认 `/v1/ping` 可访问后再取 identity。
|
||||
|
||||
3. 通过 `/v1/identity` 获取新 token 和 identity
|
||||
- 使用 `POST http://127.0.0.1:3101/v1/identity`
|
||||
- 只记录 identity,不要在日志中打印 token 明文。
|
||||
|
||||
4. 用新 token 登录 CLI
|
||||
- 运行:`spacetime login --token <token>`
|
||||
- 这会把 token 写到本地 CLI 配置,后续 HTTP SQL 可读 private table。
|
||||
|
||||
5. 重新验证 SQL
|
||||
- 使用带 token 的 `POST /v1/database/<db>/sql`
|
||||
- 先尝试 `SELECT ... FROM tracking_event LIMIT 1`
|
||||
- 若成功,再让 api-server 走同样 token。
|
||||
|
||||
6. 如果 api-server 需要复用 token
|
||||
- 优先读取项目内本地 CLI 配置中的 token,而不是硬编码或回填到 `.env`。
|
||||
- 输出日志时统一 `[REDACTED]`。
|
||||
|
||||
排查要点:
|
||||
- `ORDER BY` 和 private table 是两个独立问题,先分开修。
|
||||
- 清库后旧 token 很可能不再能看见 private table,不代表表不存在。
|
||||
- 若 `/v1/identity` 返回的 token 没权限,再检查当前 standalone 是否就是刚启动的本地实例、database 名是否一致、模块是否已重新发布。
|
||||
@@ -0,0 +1,43 @@
|
||||
# SpacetimeDB HTTP SQL SATS 值后台展示处理
|
||||
|
||||
本参考用于 Genarrative 后台通过 SpacetimeDB HTTP SQL 读取表明细并展示/导出时,处理 SQL rows 中的 SATS 原始 JSON 值。
|
||||
|
||||
## 典型现象
|
||||
|
||||
读取 private table(例如 `tracking_event`)后,HTTP SQL 可能返回如下原始形态:
|
||||
|
||||
- enum:`RuntimeTrackingScopeKind::User` 返回 `[3, []]`
|
||||
- `Option<String>::Some("user_00000001")` 返回 `[0, "user_00000001"]`
|
||||
- `Option<String>::None` 返回 `[1, []]`
|
||||
- `Timestamp` 返回 `[1778207451731746]`
|
||||
|
||||
如果直接 `value.to_string()` 展示,后台会出现 `[3,[]]`、`[0,"..."]`、`[1,[]]`、`[1778207451731746]`,运营不可读。
|
||||
|
||||
## 推荐处理
|
||||
|
||||
1. 后端解析层优先标准化:
|
||||
- Option:`[0, value] -> value`,`[1, []] -> None`
|
||||
- enum:按生成 binding 的 variant 顺序映射,例如 `RuntimeTrackingScopeKind` 为 `site/work/module/user`,索引 `0/1/2/3` 分别对应这些字符串
|
||||
- Timestamp:单元素数组 `[micros] -> "micros"`
|
||||
2. 前端展示层再格式化时间:
|
||||
- 纯数字时间戳按微秒处理:`Date(Math.floor(micros / 1000))`
|
||||
- ISO 字符串用 `new Date(value)`
|
||||
- 展示为 `YYYY-MM-DD HH:mm:ss`
|
||||
3. 列表、详情弹窗、Excel 导出必须使用同一套格式化结果,避免导出仍残留 SATS 原始值。
|
||||
4. 增加单测覆盖 SATS 原始 rows,至少断言:
|
||||
- `[3, []] -> user`
|
||||
- `[0, "user"] -> Some("user")`
|
||||
- `[1, []] -> None`
|
||||
- `[1778207451731746] -> "1778207451731746"`
|
||||
|
||||
## 验收建议
|
||||
|
||||
- `cargo test -p api-server admin_tracking -- --nocapture`
|
||||
- `npm run admin-web:typecheck`
|
||||
- `npm run admin-web:build`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
## 注意
|
||||
|
||||
不同 enum 的 variant 顺序必须以生成 binding 或 module 源码为准,不能复用其他 enum 的索引映射。
|
||||
132
.hermes/skills/genarrative-auth-session-flow/SKILL.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: genarrative-auth-session-flow
|
||||
description: 在 Genarrative 中排查或修改登录、access token、refresh cookie、AuthGate 会话恢复、登录态刷新、认证埋点链路时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, auth, session, cookie, refresh-token, AuthGate, tracking]
|
||||
related_skills: [systematic-debugging, test-driven-development, genarrative-profile-features]
|
||||
---
|
||||
|
||||
# Genarrative 认证会话与登录埋点链路
|
||||
|
||||
用于 Genarrative 中登录、会话恢复、refresh cookie 续期、access token 补票、AuthGate 恢复登录态,以及每日登录/认证相关埋点的排查与修改。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 用户反馈登录态、cookie、自动续期、刷新页面后状态异常。
|
||||
- 修改 `AuthGate`、`apiClient`、`authService` 或 Rust `api-server` 认证接口。
|
||||
- 排查“已登录但打开网页没有触发登录埋点”等 session restore 场景。
|
||||
- 修改手机验证码登录、密码登录、微信登录、重置密码后自动登录、refresh session rotate。
|
||||
- 需要判断某个前端动作是否真正调用了后端 refresh/session 或埋点 procedure。
|
||||
|
||||
## 关键代码路径
|
||||
|
||||
前端:
|
||||
|
||||
- `src/components/auth/AuthGate.tsx`
|
||||
- 登录态 hydrate / restore 的入口。
|
||||
- 监听 `AUTH_STATE_EVENT` 后重新 hydrate。
|
||||
- 是否先 refresh、再 `/api/auth/me`,决定打开页面是否进入后端 refresh 链路。
|
||||
- `src/services/apiClient.ts`
|
||||
- access token 本地保存、`ensureStoredAccessToken()`、`refreshStoredAccessToken()`、`fetchWithApiAuth()`。
|
||||
- `ensureStoredAccessToken()` 有 token 时会直接复用,不一定触发后端 refresh。
|
||||
- `refreshStoredAccessToken()` 应直接调用 refresh 接口,用于必须轮换 cookie / 写续期埋点的场景。
|
||||
- `src/services/authService.ts`
|
||||
- `getCurrentAuthUser()` 请求 `/api/auth/me`。
|
||||
- 登录、登出、账号安全相关 API client。
|
||||
|
||||
后端:
|
||||
|
||||
- `server-rs/crates/api-server/src/auth_session.rs`
|
||||
- 创建 refresh cookie / access token。
|
||||
- `record_daily_login_tracking_event_after_auth_success(...)` 统一写每日登录埋点;失败 warning,不阻断认证流程。
|
||||
- `server-rs/crates/api-server/src/refresh_session.rs`
|
||||
- `POST /api/auth/session/refresh`。
|
||||
- rotate refresh session、签发新 access token、记录每日登录埋点。
|
||||
- `server-rs/crates/api-server/src/auth_me.rs`
|
||||
- `/api/auth/me` 只读取当前 access token 对应用户,不应假设它会触发 refresh 或登录埋点。
|
||||
- `server-rs/crates/api-server/src/phone_auth.rs`
|
||||
- `server-rs/crates/api-server/src/password_entry.rs`
|
||||
- `server-rs/crates/api-server/src/password_management.rs`
|
||||
- `server-rs/crates/api-server/src/wechat_auth.rs`
|
||||
- 各真实认证成功入口。
|
||||
- `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||
- `record_daily_login_tracking_event(user_id)` 调用 SpacetimeDB procedure。
|
||||
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- `record_daily_login_tracking_event_and_return` procedure。
|
||||
- 任务中心读取不应污染每日登录埋点;如看到 `get_profile_task_center` 顺手写 `daily_login`,优先复核是否回归。
|
||||
|
||||
## 调试顺序
|
||||
|
||||
1. 先明确用户场景属于哪类:
|
||||
- 新登录成功。
|
||||
- cookie/access token 已过期后的自动刷新。
|
||||
- 已登录且 cookie/access token 未过期时打开网页。
|
||||
- 只调用 `/api/auth/me` 或某个受保护业务接口。
|
||||
2. 查前端实际调用链,不要只看后端埋点点位:
|
||||
- `AuthGate` hydrate 是否调用 `refreshStoredAccessToken()`?
|
||||
- 是否只是 `ensureStoredAccessToken()` + `/api/auth/me`?
|
||||
- `fetchWithApiAuth()` 是否因为已有 access token 而跳过 refresh?
|
||||
3. 查后端实际埋点点位:
|
||||
- 登录成功入口是否在 session 创建后调用 helper。
|
||||
- refresh session 是否在 rotate 与 access token 签发成功后调用 helper。
|
||||
- 失败策略是否只 warning、不阻断响应。
|
||||
4. 如涉及 SpacetimeDB procedure/table/binding,按项目 SpacetimeDB skills 与文档同步检查绑定生成、`migration.rs`、private table 限制。
|
||||
5. 修改前补齐 `docs/technical/` 中对应方案/根因;修改后同步更新。
|
||||
|
||||
## 关键经验:已登录打开网页也要主动 refresh 才能写登录埋点
|
||||
|
||||
常见误判:后端已经在 refresh cookie 续期时写每日登录埋点,就以为“打开网页”会触发埋点。
|
||||
|
||||
实际链路中,如果用户已经登录且本地 access token 还有效:
|
||||
|
||||
1. `ensureStoredAccessToken()` 会直接返回已有 token。
|
||||
2. `AuthGate` 随后请求 `/api/auth/me`。
|
||||
3. `/api/auth/me` 只校验/读取用户,不会 rotate refresh session。
|
||||
4. 因此后端 refresh/session 埋点不会触发。
|
||||
|
||||
若产品要求“已登录且 cookie 没过期时打开网页也记录登录埋点”,`AuthGate` 的 restore/hydrate 应主动调用 `refreshStoredAccessToken()`,再调用 `getCurrentAuthUser()`。
|
||||
|
||||
## 每日登录埋点原则
|
||||
|
||||
- 真实登录成功:在 refresh session / access token 创建成功后记录。
|
||||
- cookie refresh 续期:在 rotate refresh session 成功且新 access token 签发成功后记录。
|
||||
- 已登录打开网页:前端必须主动走 refresh 续期链路,不能只请求 `/api/auth/me`。
|
||||
- `login_method` 对于 refresh 场景使用 refresh session 保存的 `issued_by_provider`。
|
||||
- 埋点失败不阻断登录、续期、会话恢复或 token 返回,只记录 warning。
|
||||
- 任务中心读取不应作为登录埋点来源,避免后台查看/刷新任务中心污染登录数据。
|
||||
|
||||
## 测试与验证命令
|
||||
|
||||
按改动范围选择:
|
||||
|
||||
```bash
|
||||
npm run test -- AuthGate.test.tsx
|
||||
npm run typecheck
|
||||
cd server-rs && cargo test -p api-server auth_session -- --nocapture
|
||||
cd server-rs && cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture
|
||||
cd server-rs && cargo check -p api-server
|
||||
cd server-rs && cargo check -p spacetime-client
|
||||
cd server-rs && cargo check -p spacetime-module
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
注意:Vitest 0.34 不支持 Jest 的 `--runInBand`;不要把 `--runInBand` 加到 `npm run test -- AuthGate.test.tsx` 后面。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 把 `/api/auth/me` 当作 refresh:它只读当前 access token,不会写 refresh 埋点。
|
||||
2. 只在后端 refresh handler 加埋点,但前端有有效 access token 时根本不调用 refresh。
|
||||
3. `ensureStoredAccessToken()` 有 token 时会直接返回;需要强制 refresh 时应使用 `refreshStoredAccessToken()`。
|
||||
4. 在埋点 helper 中返回错误并阻断登录/续期,会破坏认证主链路。
|
||||
5. refresh 场景把 `login_method` 写死为 password,会丢失手机/微信来源。
|
||||
6. 修改中文文件后忘记 `npm run check:encoding`。
|
||||
7. `cargo fmt -p api-server` 或前端测试可能让 `.env.local`、`.gitignore` 出现非业务改动;提交前用 `git status --short` 检查并撤回无关敏感/环境文件。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `references/session-restore-daily-login-tracking-2026-05-08.md`:已登录且 cookie 未过期时打开网页未触发每日登录埋点的根因与修复案例。
|
||||
@@ -0,0 +1,75 @@
|
||||
# Session restore 每日登录埋点案例(2026-05-08)
|
||||
|
||||
## 现象
|
||||
|
||||
用户反馈:已经登录且 cookie 没过期时,打开网页没有触发每日登录埋点。
|
||||
|
||||
## 根因
|
||||
|
||||
当本地 access token 仍有效时,前端 `AuthGate` 恢复登录态只会复用现有 token 并请求 `/api/auth/me`。`/api/auth/me` 只读取当前用户,不会进入 `POST /api/auth/session/refresh`,因此后端 refresh handler 中的每日登录埋点不会执行。
|
||||
|
||||
关键误判点:后端已经在 refresh cookie 续期写埋点,不等于“打开网页”一定会触发。前端必须实际调用 refresh/session 接口。
|
||||
|
||||
## 修复模式
|
||||
|
||||
1. 在 `src/services/apiClient.ts` 暴露强制 refresh 方法,例如:
|
||||
|
||||
```ts
|
||||
export async function refreshStoredAccessToken() {
|
||||
return refreshAccessToken();
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `src/components/auth/AuthGate.tsx` 的 hydrate/restore 中,使用:
|
||||
|
||||
```ts
|
||||
await refreshStoredAccessToken();
|
||||
const nextSession = await getCurrentAuthUser();
|
||||
```
|
||||
|
||||
而不是只调用:
|
||||
|
||||
```ts
|
||||
await ensureStoredAccessToken();
|
||||
const nextSession = await getCurrentAuthUser();
|
||||
```
|
||||
|
||||
3. 保留 `ensureStoredAccessToken()` 给普通受保护请求兜底;不要把所有请求都改成强制 refresh。
|
||||
|
||||
4. 确认 `server-rs/crates/api-server/src/refresh_session.rs` 在 rotate refresh session 成功且新 access token 签发成功后调用每日登录埋点 helper。
|
||||
|
||||
5. 确认 `server-rs/crates/spacetime-module/src/runtime/profile.rs` 中 `get_profile_task_center` 不再顺手写 `daily_login`,避免任务中心读取污染登录埋点。
|
||||
|
||||
## 测试
|
||||
|
||||
前端测试重点:
|
||||
|
||||
- `AuthGate` 会等待 `refreshStoredAccessToken()` 完成后才暴露已恢复用户内容。
|
||||
- `AUTH_STATE_EVENT` 触发 hydrate 时仍保持已挂载平台内容和本地 tab 状态。
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
npm run test -- AuthGate.test.tsx
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
后端/SpacetimeDB 编译:
|
||||
|
||||
```bash
|
||||
cd server-rs && cargo check -p spacetime-module
|
||||
cd server-rs && cargo check -p api-server
|
||||
```
|
||||
|
||||
全局检查:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- Vitest 0.34 不支持 Jest 的 `--runInBand` 参数;命令里不要加。
|
||||
- 埋点失败只能 warning,不能阻断登录态恢复。
|
||||
- 如果后续发现打开页面产生过多 refresh 请求,需要在产品口径和埋点口径之间重新设计节流;但不能退回“只读 `/api/auth/me` 却期待写登录埋点”的状态。
|
||||
127
.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: genarrative-dev-stack-port-routing
|
||||
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
|
||||
description: 在 Genarrative 中修改 npm run dev / npm run dev:rust / npm run dev:web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, dev-stack, 端口探测, Vite, api-server, SpacetimeDB, npm-run-dev]
|
||||
related_skills: [genarrative-admin-backoffice]
|
||||
---
|
||||
|
||||
# Genarrative 本地 dev 启动端口与代理目标串联流程
|
||||
|
||||
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev` / `npm run dev:rust` / `npm run dev:web` 的端口检查、端口漂移和后续流程目标传递。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 修改 `scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`scripts/dev-stack-port-utils.mjs`。
|
||||
- 处理 `3000`、`3101`、`3102`、`8082` 等端口被占用导致本地开发栈启动失败。
|
||||
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
|
||||
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
|
||||
- 修改本地联调文档或 `.hermes/shared-memory/pitfalls.md` 中的 dev 启动口径。
|
||||
|
||||
## 当前端口职责
|
||||
|
||||
默认优先端口:
|
||||
|
||||
1. 主站 Vite:`3000`,对浏览器通常展示为 `http://127.0.0.1:<web-port>/`。
|
||||
2. Rust `api-server`:`8082`,健康检查为 `http://127.0.0.1:<api-port>/healthz`。
|
||||
3. SpacetimeDB standalone:`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`。
|
||||
4. 后台 Vite:`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`。
|
||||
|
||||
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
|
||||
|
||||
## 实现入口
|
||||
|
||||
- `package.json`
|
||||
- `dev` 和 `dev:rust`:执行 `node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh`。
|
||||
- `dev:web`:执行 `node scripts/dev-web-rust.mjs`。
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `isPortAvailable(...)`:探测端口是否可监听。
|
||||
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
|
||||
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
|
||||
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102`。
|
||||
- `scripts/dev-rust-stack.sh`
|
||||
- 解析 CLI 参数后,先计算 `API_TARGET_HOST` 与 `ADMIN_WEB_TARGET_HOST`。
|
||||
- 调用 `resolve_dev_stack_ports` 覆盖 `SPACETIME_PORT`、`API_PORT`、`WEB_PORT`、`ADMIN_WEB_PORT`。
|
||||
- 再构造 `SPACETIME_SERVER` 和 `RUST_SERVER_TARGET`。
|
||||
- `scripts/dev-web-rust.mjs`
|
||||
- 单独启动主站前端时,也先用 `findAvailablePort` 检查 `WEB_PORT` / 默认 `3000`。
|
||||
|
||||
## 必须保持的传递链路
|
||||
|
||||
`npm run dev` / `npm run dev:rust` 中端口解析后,必须同步到以下位置:
|
||||
|
||||
1. SpacetimeDB 启动:`spacetime start --listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"`。
|
||||
2. SpacetimeDB 发布:`spacetime publish ... --server "${SPACETIME_SERVER}"`。
|
||||
3. Rust api-server:`GENARRATIVE_API_HOST`、`GENARRATIVE_API_PORT`、`GENARRATIVE_SPACETIME_SERVER_URL`、`GENARRATIVE_SPACETIME_DATABASE`。
|
||||
4. api-server 健康检查:`wait_for_api_server "${RUST_SERVER_TARGET}/healthz" ...`。
|
||||
5. 主站 Vite:`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET`、`ADMIN_WEB_TARGET`、`ADMIN_WEB_PORT`、`--port=${WEB_PORT}`、`--host=${WEB_HOST}`。
|
||||
6. 后台 Vite:`ADMIN_API_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_API_PORT`、`--port=${ADMIN_WEB_PORT}`。
|
||||
7. 控制台日志:`[dev:ports]` 和 `[dev:rust] web/admin web/rust api/spacetime` 必须显示最终实际地址。
|
||||
|
||||
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
|
||||
|
||||
## 修改流程
|
||||
|
||||
1. 先读当前脚本和文档:
|
||||
- `scripts/dev-stack-port-utils.mjs`
|
||||
- `scripts/dev-rust-stack.sh`
|
||||
- `scripts/dev-web-rust.mjs`
|
||||
- `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
|
||||
- `.hermes/shared-memory/pitfalls.md`
|
||||
2. 优先改公共端口工具,不要把端口探测逻辑复制到多个脚本。
|
||||
3. 对 Bash 脚本只做局部补丁,避免整文件重写导致中文注释或换行大面积变化。
|
||||
4. 修改 `dev-rust-stack.sh` 时确认变量顺序:
|
||||
- 先有 `REPO_ROOT`。
|
||||
- 再计算 `API_TARGET_HOST` / `ADMIN_WEB_TARGET_HOST`。
|
||||
- 再调用端口解析工具。
|
||||
- 再构造 `SPACETIME_SERVER` / `RUST_SERVER_TARGET`。
|
||||
5. 修改 `dev:web` 时不要自动改后端目标策略;`dev:web` 只负责主站 Vite 端口可用性与已有后端目标选择。
|
||||
6. 同步更新技术文档和团队共享记忆。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
最小验证:
|
||||
|
||||
```bash
|
||||
bash -n scripts/dev-rust-stack.sh
|
||||
npm run test -- scripts/dev-stack-port-utils.test.ts
|
||||
npm run check:encoding
|
||||
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0
|
||||
```
|
||||
|
||||
端口冲突回归测试建议:
|
||||
|
||||
1. 用测试或临时 Node server 占用某个优先端口。
|
||||
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
|
||||
3. 调用 `resolveDevStackPorts`,断言四个结果互不相同。
|
||||
4. 如果实际启动完整栈,观察控制台:
|
||||
- `[dev:ports] ... 不可用,改用 ...`
|
||||
- `[dev:rust] rust api: http://...:<actual-api-port>`
|
||||
- `[dev:rust] spacetime: http://...:<actual-spacetime-port>`
|
||||
- 主站和后台 Vite 启动端口与日志一致。
|
||||
|
||||
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 `/healthz`、`/v1/ping` 和页面端口;不要等待 `npm run dev` 自然退出。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. **只让 Vite 自己漂移端口。** 这样终端可能出现可访问前端,但脚本和文档仍认为是 `3000`,后台目标或日志会错。
|
||||
2. **只改 SpacetimeDB start,不改 publish。** standalone 可能监听新端口,但 publish 仍连旧 `3101`。
|
||||
3. **只改 `GENARRATIVE_API_PORT`,不改 `RUST_SERVER_TARGET`。** api-server 已在新端口监听,但 Vite 代理仍打旧端口。
|
||||
4. **使用 `0.0.0.0` 作为浏览器访问地址。** 监听可以是 `0.0.0.0`,展示给用户和健康检查通常用 `127.0.0.1`。
|
||||
5. **端口探测和实际启动之间存在竞态。** 已经探测可用的端口仍可能被外部进程抢占;SpacetimeDB 启动后仍要解析实际监听地址,api-server 和 Vite 失败时要打印清晰日志。
|
||||
6. **运行全仓库 lint 误判。** 当前仓库可能有既有 lint 问题。验证本功能时优先运行定向测试、Bash 语法检查、编码检查,并在最终说明中区分既有 lint 失败与本次改动。
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] 端口工具有测试覆盖端口被占用和多端口互斥解析。
|
||||
- [ ] `dev-rust-stack.sh` 通过 `bash -n`。
|
||||
- [ ] `npm run dev` / `npm run dev:rust` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
|
||||
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
|
||||
- [ ] 文档同步更新 `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`。
|
||||
- [ ] 长期踩坑同步更新 `.hermes/shared-memory/pitfalls.md`。
|
||||
- [ ] 修改中文文件后运行 `npm run check:encoding`。
|
||||
348
.hermes/skills/genarrative-play-type-integration/SKILL.md
Normal file
@@ -0,0 +1,348 @@
|
||||
---
|
||||
name: genarrative-play-type-integration
|
||||
description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, 玩法接入, 创作入口, 前端, 后端, contracts, runtime]
|
||||
related_skills: []
|
||||
---
|
||||
|
||||
# Genarrative 新增玩法类型接入流程
|
||||
|
||||
用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增一个游戏玩法入口
|
||||
- 让某个玩法从“敬请期待”变为可创建
|
||||
- 为新玩法补齐创作工作台、结果页、发布与试玩链路
|
||||
- 将新玩法接入创作中心作品架与广场
|
||||
|
||||
## 先判断接入级别
|
||||
|
||||
### 1. 只做入口占位
|
||||
|
||||
只需要新增入口配置,不接 session/workspace/result/runtime。
|
||||
|
||||
适合:
|
||||
- 敬请期待
|
||||
- 灰度占位
|
||||
|
||||
### 2. 可进入创作工作台
|
||||
|
||||
需要补齐前端分流、session、工作台、结果页,至少能生成草稿。
|
||||
|
||||
### 3. 完整玩法闭环
|
||||
|
||||
需要补齐:
|
||||
- 创作入口
|
||||
- 工作台
|
||||
- 草稿生成
|
||||
- 结果页
|
||||
- 发布
|
||||
- 试玩 runtime
|
||||
- 作品架 / 广场 / 分享
|
||||
|
||||
## 推荐接入顺序
|
||||
|
||||
### Step 1: 先定玩法 ID 和能力边界
|
||||
|
||||
先明确:
|
||||
- `id` 是什么
|
||||
- 入口是否可见
|
||||
- 是否可点击创建
|
||||
- 是否需要对话式创作
|
||||
- 是否需要生成中页面
|
||||
- 是否需要 result/runtime/gallery/share
|
||||
|
||||
不要先随便起临时 ID 再改名。
|
||||
|
||||
### Step 2: 新增入口配置
|
||||
|
||||
文件:
|
||||
- `src/config/newWorkEntryConfig.ts`
|
||||
|
||||
在 `NEW_WORK_ENTRY_CONFIG.creationTypes` 中新增或调整:
|
||||
- `id`
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `badge`
|
||||
- `visible`
|
||||
- `open`
|
||||
|
||||
字段语义:
|
||||
- `visible: true`:在创作页签 / 新建作品入口中展示。
|
||||
- `visible: false`:不在平台入口展示,但不删除既有玩法路由和能力。
|
||||
- `open: true`:可点击进入创作流程。
|
||||
- `open: false`:展示为锁定 / 敬请期待,不应进入创建流程。
|
||||
|
||||
如果只是占位:
|
||||
- `visible: true`
|
||||
- `open: false`
|
||||
|
||||
相关渲染与过滤位置:
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts`:将 `NEW_WORK_ENTRY_CONFIG.creationTypes` 映射为平台入口卡片,`getVisiblePlatformCreationTypes()` 会过滤隐藏项,并把可创建模板排在敬请期待模板前面。
|
||||
- `src/components/custom-world-home/CustomWorldCreationStartCard.tsx`:创作页签首屏模板入口卡片的实际渲染位置。
|
||||
- `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`:选择创作类型弹层的渲染位置。
|
||||
|
||||
注意:当前项目工作区通常已经是 `<repo-root>`,路径不要再额外拼接 `./Genarrative/`。
|
||||
|
||||
### Step 3: 确认类型过滤逻辑
|
||||
|
||||
文件:
|
||||
- `./Genarrative/src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
|
||||
检查:
|
||||
- `getVisiblePlatformCreationTypes()` 是否能展示新类型
|
||||
- `isPlatformCreationTypeVisible()` 是否能识别新类型
|
||||
- `locked` / `hidden` 是否正确映射
|
||||
|
||||
### Step 4: 扩展页面阶段
|
||||
|
||||
文件:
|
||||
- `./Genarrative/src/components/platform-entry/platformEntryTypes.ts`
|
||||
|
||||
为新玩法补充 `SelectionStage`:
|
||||
- `*-agent-workspace`
|
||||
- `*-generating`(可选)
|
||||
- `*-result`
|
||||
- `*-runtime`(可选)
|
||||
- `*-gallery-detail`(可选)
|
||||
|
||||
### Step 5: 在总流程中加类型分流
|
||||
|
||||
文件:
|
||||
- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
在 `handleCreationHubCreateType(type)` 中新增分支,确保:
|
||||
- 能进入对应工作台
|
||||
- 能设置对应 `selectionStage`
|
||||
- 能关闭类型弹层
|
||||
|
||||
同时补:
|
||||
- `open<Play>AgentWorkspace()`
|
||||
- `leave<Play>Flow()`
|
||||
- `submit<Play>Message()`(对话式玩法)
|
||||
- `execute<Play>Action()`
|
||||
|
||||
### Step 6: 接入通用 Agent flow controller
|
||||
|
||||
文件:
|
||||
- `./Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
|
||||
|
||||
如果是 Agent 型玩法,复用通用控制器:
|
||||
- `createSession`
|
||||
- `getSession`
|
||||
- `streamMessage`
|
||||
- `executeAction`
|
||||
- `isBusy`
|
||||
- `error`
|
||||
- `streamingReplyText`
|
||||
- `selectionStage` 切换
|
||||
|
||||
### Step 7: 定义 shared contracts
|
||||
|
||||
前端:
|
||||
- `./Genarrative/packages/shared/src/contracts/`
|
||||
|
||||
后端:
|
||||
- `./Genarrative/server-rs/crates/shared-contracts/src/`
|
||||
|
||||
至少补齐:
|
||||
- session snapshot
|
||||
- create session request/response
|
||||
- message request/response
|
||||
- action request/response
|
||||
- draft/result 结构
|
||||
- work summary / gallery 结构(如果需要)
|
||||
- runtime 结构(如果需要)
|
||||
|
||||
### Step 8: 实现前端 service client
|
||||
|
||||
目录参考:
|
||||
- `./Genarrative/src/services/`
|
||||
|
||||
按玩法补:
|
||||
- creation client
|
||||
- runtime client(可选)
|
||||
- works client(可选)
|
||||
- gallery client(可选)
|
||||
|
||||
建议保持和现有玩法一致的 API base 与命名风格。
|
||||
|
||||
### Step 9: 接后端 API
|
||||
|
||||
文件参考:
|
||||
- `./Genarrative/server-rs/crates/api-server/src/puzzle.rs`
|
||||
- `./Genarrative/server-rs/crates/api-server/src/puzzle_agent_turn.rs`
|
||||
- `./Genarrative/server-rs/crates/api-server/src/match3d.rs`
|
||||
|
||||
通常需要:
|
||||
- create session
|
||||
- get session
|
||||
- send message
|
||||
- stream message
|
||||
- execute action
|
||||
- publish / save / delete
|
||||
- runtime start / action(可选)
|
||||
- gallery / detail(可选)
|
||||
|
||||
后端设计优先按 Genarrative 的 DDD 分层拆开,不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里:
|
||||
- `module-<play>`:纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。
|
||||
- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。
|
||||
- `spacetime-module`:表定义、reducer/procedure、事务编排、migration;表结构变化要同步生成绑定。
|
||||
- `spacetime-client`:api-server 到 SpacetimeDB 的 facade,隐藏 reducer 调用细节。
|
||||
- `api-server`:Axum 路由、鉴权、SSE/stream、应用层编排。
|
||||
- `platform-*`:LLM、资产上传、鉴权、第三方服务等副作用。
|
||||
|
||||
建议按四条线设计后端能力:
|
||||
- Agent 创作线:session、turn、stream、compile action。
|
||||
- Works 作品线:保存、发布、删除、草稿恢复。
|
||||
- Gallery 广场线:公开列表、详情、like/remix/share。
|
||||
- Runtime 运行态线:开始试玩、提交动作、读取状态。
|
||||
|
||||
### Step 10: 新增工作台组件
|
||||
|
||||
目录建议:
|
||||
- `./Genarrative/src/components/<play>-creation/<Play>AgentWorkspace.tsx`
|
||||
|
||||
两种形态:
|
||||
|
||||
#### 对话式
|
||||
适合设定逐轮补齐。
|
||||
|
||||
参考:
|
||||
- `BigFishAgentWorkspace.tsx`
|
||||
- `Match3DAgentWorkspace.tsx`
|
||||
|
||||
#### 表单式
|
||||
适合输入结构明确的玩法。
|
||||
|
||||
参考:
|
||||
- `PuzzleAgentWorkspace.tsx`
|
||||
|
||||
### Step 11: 在渲染树中挂载新页面
|
||||
|
||||
文件:
|
||||
- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
补齐:
|
||||
- workspace 分支
|
||||
- generating 分支(如需要)
|
||||
- result 分支
|
||||
- runtime 分支(如需要)
|
||||
|
||||
### Step 12: 新增结果页
|
||||
|
||||
目录建议:
|
||||
- `./Genarrative/src/components/<play>-result/<Play>ResultView.tsx`
|
||||
|
||||
结果页至少支持:
|
||||
- 展示 draft
|
||||
- 返回编辑
|
||||
- 发布
|
||||
- 试玩
|
||||
- 错误展示
|
||||
|
||||
### Step 13: 需要试玩就补 runtime
|
||||
|
||||
目录建议:
|
||||
- `./Genarrative/src/components/<play>-runtime/<Play>RuntimeShell.tsx`
|
||||
|
||||
如果玩法是游戏类,建议补完整 runtime 闭环。
|
||||
|
||||
### Step 14: 接入作品架 / 广场 / 分享
|
||||
|
||||
需要改:
|
||||
- `./Genarrative/src/components/custom-world-home/creationWorkShelf.ts`
|
||||
- `./Genarrative/src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- `./Genarrative/src/services/publicWorkCode.ts`
|
||||
|
||||
如果玩法支持发布,还要补:
|
||||
- public work code
|
||||
- public detail
|
||||
- publish share modal
|
||||
- like/remix(可选)
|
||||
|
||||
### Step 15: 处理登录态与草稿恢复
|
||||
|
||||
要考虑:
|
||||
- 刷新恢复草稿
|
||||
- 退出登录清空私有状态
|
||||
- result/draft 缺失时回退
|
||||
- busy / generating / runtime 中断恢复
|
||||
|
||||
### Step 16: 补测试
|
||||
|
||||
至少覆盖:
|
||||
- 入口展示
|
||||
- 类型分流
|
||||
- 工作台打开
|
||||
- session 创建
|
||||
- compile action
|
||||
- result 页切换
|
||||
- 发布后刷新作品架
|
||||
- runtime 进入与退出
|
||||
|
||||
## 最小改动清单
|
||||
|
||||
### 只做占位
|
||||
|
||||
只改:
|
||||
- `./Genarrative/src/config/newWorkEntryConfig.ts`
|
||||
|
||||
### 做到可进入工作台
|
||||
|
||||
至少改:
|
||||
- `newWorkEntryConfig.ts`
|
||||
- `platformEntryTypes.ts`
|
||||
- `PlatformEntryFlowShellImpl.tsx`
|
||||
- 新玩法 service client
|
||||
- 新玩法工作台组件
|
||||
- shared contracts
|
||||
- 后端 API
|
||||
|
||||
### 做到完整闭环
|
||||
|
||||
还要补:
|
||||
- result 页
|
||||
- runtime
|
||||
- works / gallery
|
||||
- public code
|
||||
- share
|
||||
- 作品架聚合
|
||||
- 测试
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 只加入口配置不够,类型分流和页面阶段也要补。
|
||||
2. `SelectionStage` 不扩展,前端无法安全切页。
|
||||
3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。
|
||||
4. 发布后不刷新 works/gallery,用户会看不到新作品。
|
||||
5. 如果走 SpacetimeDB,表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。
|
||||
6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO;先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤),RED 后再补领域类型与聚合函数。
|
||||
7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucket;bucket 输出要有稳定排序,并显式携带 `bucketKey`、`bucketStartDateKey`、`bucketEndDateKey`、`value`。
|
||||
8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type union;admin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。
|
||||
9. 退出登录时要清空新玩法私有状态,避免串用户。
|
||||
10. 移动端入口卡片增多后要检查布局和滚动体验。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `references/genarrative-analytics-tracking-runtime.md`:analytics/tracking runtime 粒度聚合、contracts 同步与 SpacetimeDB 生成物注意事项。
|
||||
|
||||
## 验证标准
|
||||
|
||||
一个玩法算真正接入成功,至少要满足:
|
||||
|
||||
- 入口能展示
|
||||
- 能进入对应工作台
|
||||
- 能创建 session
|
||||
- 能生成草稿
|
||||
- 能进入结果页
|
||||
- 能返回编辑
|
||||
- 如果需要,可试玩
|
||||
- 如果需要,可发布
|
||||
- 发布后能回到作品架 / 广场 / 分享链路
|
||||
@@ -0,0 +1,57 @@
|
||||
# Genarrative analytics/tracking runtime 接入经验
|
||||
|
||||
本参考来自 analytics time dimension / tracking daily stat 粒度聚合实现与一次“只提交了 bindings/tests、后端链路未补齐”的纠偏。
|
||||
|
||||
## 推荐顺序
|
||||
|
||||
1. 先读仓库 `README.md`、`AGENTS.md`、相关 `/docs/technical` 与 `.hermes/plans`,确认当前阶段范围。
|
||||
2. 遵循 TDD:先在 `server-rs/crates/module-runtime/tests/` 写纯函数测试,验证缺失类型/函数导致 RED。
|
||||
3. 在 `module-runtime/src/domain.rs` 增加领域类型,例如:
|
||||
- `AnalyticsGranularity`:`day | week | month | quarter | year`
|
||||
- daily stat snapshot
|
||||
- bucket metric
|
||||
- query request/response/input
|
||||
- Spacetime procedure result(例如 `AnalyticsMetricQueryProcedureResult { ok, buckets, error_message }`),否则 module procedure 无法生成 client bindings。
|
||||
4. 在 `module-runtime/src/application.rs` 复用已有 `build_analytics_date_dimension_from_date_key`,实现 daily stat 到 day/week/month/quarter/year bucket 的聚合。
|
||||
5. 输出 bucket 应稳定排序,可用 `BTreeMap`;聚合前按 `event_key + scope_kind + scope_id` 过滤。
|
||||
6. 在 `module-runtime/src/commands.rs` 增加 query input builder,复用现有字段错误(如 missing event key、missing scope id)。
|
||||
7. 同步 contracts:
|
||||
- Rust:`server-rs/crates/shared-contracts/src/runtime.rs`
|
||||
- 前端共享:`packages/shared/src/contracts/runtime.ts`
|
||||
- 管理端若有本地 API 类型,也同步 `apps/admin-web/src/api/adminApiTypes.ts`
|
||||
8. 接 Spacetime module 源头:在 `server-rs/crates/spacetime-module/src/runtime/profile.rs` 增加只读 procedure(例如 `query_analytics_metric`),从 `tracking_daily_stat` 读 rows,映射成 `RuntimeAnalyticsDailyStatSnapshot` 后调用领域聚合函数。
|
||||
9. 重新生成 `server-rs/crates/spacetime-client/src/module_bindings/`。如果当前环境没有 `spacetime`/`spacetimedb` CLI,可临时按现有生成物风格手补 type/procedure/mod.rs,但必须在文档和最终说明中标注“临时手补生成物”,并要求后续在有 CLI 的机器用项目脚本重新生成覆盖。
|
||||
10. 接 `spacetime-client`:
|
||||
- `src/mapper.rs`:module binding procedure result → `module_runtime` record/response;补 tracking scope/granularity 映射。
|
||||
- `src/runtime.rs`:新增 facade 方法(例如 `SpacetimeClient::query_analytics_metric`),调用生成的 procedure。
|
||||
- `src/lib.rs`:若 facade/mapper 需要领域类型或 builder,补导入;注意同名 binding 类型会造成误解析。
|
||||
11. 接 `api-server`:
|
||||
- `src/runtime_profile.rs`:Query params / parser / handler / response builder。
|
||||
- `src/app.rs`:挂路由,例如 profile 或 admin analytics endpoint;选择路径前确认产品定位。
|
||||
12. 最后更新 docs/plan,并确认 diff 不只是生成物。
|
||||
|
||||
## 验证命令示例
|
||||
|
||||
```bash
|
||||
cd <repo-root>/server-rs
|
||||
cargo test -p module-runtime --test analytics_granularity
|
||||
cargo check -p shared-contracts
|
||||
cargo check -p spacetime-module
|
||||
cargo check -p spacetime-client
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
If terminal output is compacted by the tool, rerun the specific command directly (without `head`) or capture full output before concluding; `cargo check` exit code 0 with warnings is acceptable when warnings are pre-existing and documented.
|
||||
|
||||
## 坑
|
||||
|
||||
- 不要手改 `server-rs/crates/spacetime-client/src/module_bindings/` 生成物;若缺 procedure/type,回源头改 Spacetime module 后重新生成。若当前环境没有 `spacetime`/`spacetimedb` CLI 且必须临时手补生成物,要在最终说明中明确这是临时替代,并尽快在有 CLI 的环境重新生成。
|
||||
- `spacetime-client/src/runtime.rs` 同时能看到 `module_bindings::*` 和领域层 `module_runtime` 类型。新增 facade 方法参数要使用领域别名(如 `DomainRuntimeTrackingScopeKind`、`module_runtime::AnalyticsGranularity`),不要误用 binding 里的 `RuntimeTrackingScopeKind`;否则 `build_*_input` 会报 “expected module_runtime::..., found binding ...”。
|
||||
- 如果缺少 SpacetimeDB CLI,手补 bindings 的最小集合通常包括:`*_type.rs`(input、enum、bucket、procedure result)、`*_procedure.rs`、`module_bindings/mod.rs` 的 `pub mod`/`pub use`/procedure re-export。完成后必须跑 `cargo check -p spacetime-client` 验证 SDK trait、procedure 名称与 result 字段是否匹配。
|
||||
- `spacetime-client/src/mapper.rs` 新增 query 链路时通常要同时补四类映射:领域 input → binding input、binding enum 映射、binding procedure result → 领域 response、binding bucket/item → 领域 bucket/item。只在 `runtime.rs` 加 facade 会出现 unused import 或类型不匹配。
|
||||
- 修改 Rust import 时注意 `serde::Deserialize` 与 `serde_json` 的排序/使用:如果只加了 `Query`/`Deserialize` 但 handler 尚未实现,`cargo check -p api-server` 会暴露 unused import;不要把 import 作为完成信号。
|
||||
- 只补 shared-contracts 不够;`packages/shared` 和 admin-web 本地类型可能各有一份。
|
||||
- 周粒度应按 ISO week/date dimension,而不是简单 `day_key / 7`。
|
||||
- bucket response 建议显式包含 start/end date key,避免前端再推导时间边界。
|
||||
- `spacetime-module` 可能能编译通过,但如果没有重新生成 bindings,`spacetime-client` 仍不会有新 procedure 方法;必须以 client facade/API handler 可调用为完成标准。
|
||||
- api-server 中新增 `Query`、`Deserialize` 等 import 后要立即补 handler,否则容易留下 unused import。
|
||||
99
.hermes/skills/genarrative-profile-features/SKILL.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: genarrative-profile-features
|
||||
description: 在 Genarrative “我的”页签新增或修改个人中心入口、独立 profile 路由、反馈/记录/设置类页面时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, profile, 我的页签, 前端, 路由, 反馈]
|
||||
related_skills: [writing-plans, test-driven-development]
|
||||
---
|
||||
|
||||
# Genarrative “我的”页签功能接入
|
||||
|
||||
用于在 Genarrative 平台“我的”页签新增或修改入口,以及把入口接到独立页面阶段/路由。例如:帮助与反馈、反馈记录、个人设置、账号相关轻量页面。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 在“我的”页签新增快捷入口或卡片按钮。
|
||||
- 点击入口后进入独立页面,而不是在当前面板下方展开内容。
|
||||
- 新增 `/profile/...` 路由或 `SelectionStage`。
|
||||
- 新增移动端优先的个人中心子页面组件。
|
||||
- 修改 `RpgEntryHomeView`、`PlatformEntryFlowShellImpl`、`appPageRoutes` 等前端 profile 链路。
|
||||
|
||||
## 必读约束
|
||||
|
||||
1. 按项目约束:先检查/补齐文档,再落地工程修改。
|
||||
2. UI 面板保持清爽,不要默认堆功能说明文案。
|
||||
3. 点击按钮弹出/进入独立面板的设计,不要实现成在当前面板下方追加内容。
|
||||
4. 移动端优先,同时兼顾网页端容器宽度。
|
||||
5. 包含中文的文件优先局部补丁,修改后运行编码检查。
|
||||
6. 非必要不新建系统;优先复用现有平台入口、阶段和路由机制。
|
||||
|
||||
## 代码接入路径
|
||||
|
||||
常见文件:
|
||||
|
||||
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
- “我的”页签 UI 主入口通常在此。
|
||||
- 新增入口时优先扩展 props,例如 `onOpenFeedback?: () => void`。
|
||||
- 在现有快捷入口区新增 `ProfileShortcutButton`,保持图标、label、subLabel 风格一致。
|
||||
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- 若需要独立页面阶段,扩展 `SelectionStage` union。
|
||||
- 例如新增 `'profile-feedback'`。
|
||||
|
||||
- `src/routing/appPageRoutes.ts`
|
||||
- 在 `STAGE_ROUTE_ENTRIES` 添加 `/profile/...` 路由映射。
|
||||
- 验证 `resolveSelectionStageFromPath()` 与 `resolvePathForSelectionStage()` 双向一致。
|
||||
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- 引入新页面组件。
|
||||
- 新增打开函数:必要时先检查登录态,未登录调用 `authUi?.openLoginModal()`。
|
||||
- 打开 profile 子页时同步 `setPlatformTab('profile')`,再 `setSelectionStage(...)`。
|
||||
- 当 `selectionStage` 直接由路由进入 profile 子页时,用 `useEffect` 同步当前 tab 到 `profile`。
|
||||
- 在主渲染分支中为新阶段渲染独立 `<motion.div>` 页面;返回时回到 `platform` 阶段并保持 `profile` tab。
|
||||
|
||||
- `src/components/platform-entry/<FeatureView>.tsx`
|
||||
- 页面组件可放在 platform-entry 下,与 shell 阶段渲染保持一致。
|
||||
- 表单首版没有后端接口时,可通过可选 `onSubmit` prop 暴露提交 payload,并在组件内展示成功/失败态;注释说明后续替换为 API 调用。
|
||||
|
||||
## 推荐实施顺序
|
||||
|
||||
1. 读取 `.hermes/plans/...` 或产品文档,确认入口、路由、页面行为。
|
||||
2. 若现有文档不足,先在 `docs/prd/` 增加可编码落地的 PRD。
|
||||
3. 增加 `SelectionStage` 与 `appPageRoutes` 映射,并先跑 `npm run typecheck`。
|
||||
4. 新建独立页面组件,尽量通过 props 暴露 `onBack`/`onSubmit`,避免直接耦合全局状态。
|
||||
5. 在 `RpgEntryHomeView.tsx` 增加入口 prop 与按钮。
|
||||
6. 在 `PlatformEntryFlowShellImpl.tsx` 串联导航、登录态、阶段渲染和返回逻辑。
|
||||
7. 增加基础测试:路由解析、页面字段渲染、关键交互/校验、返回按钮。
|
||||
8. 跑编码检查、类型检查和相关 vitest。
|
||||
9. 分阶段 commit;用户要求更新工作区时再 push。
|
||||
|
||||
## 测试与验证命令
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/<FeatureView>.test.tsx
|
||||
# 或项目脚本:
|
||||
npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/<FeatureView>.test.tsx
|
||||
```
|
||||
|
||||
如果新增/修改中文文档或中文 UI,`check:encoding` 必跑。
|
||||
|
||||
## 参考案例
|
||||
|
||||
- `references/profile-feedback-entry-2026-05-08.md`:帮助与反馈入口案例,覆盖文档、路由阶段、独立页面组件、“我的”页签按钮、shell 导航、测试和验证命令。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 只在 `RpgEntryHomeView` 新增按钮但没有接 shell 导航,导致点击无效果。
|
||||
2. 只新增 `SelectionStage` 但忘记 `appPageRoutes`,导致刷新/直达路由不能恢复页面。
|
||||
3. 直达 `/profile/...` 时没有同步 `setPlatformTab('profile')`,底部 tab 状态与页面不一致。
|
||||
4. 把反馈/设置表单插到“我的”面板下方,违背独立页面体验。
|
||||
5. 没有测试 `resolveSelectionStageFromPath`/`resolvePathForSelectionStage`,后续路由改动容易回归。
|
||||
6. 中文页面或文档改动后忘记编码检查。
|
||||
@@ -0,0 +1,80 @@
|
||||
# 帮助与反馈入口案例(2026-05-08)
|
||||
|
||||
本案例来自 Genarrative “我的”页签新增反馈入口与独立反馈页实现。
|
||||
|
||||
## 目标
|
||||
|
||||
- 在“我的”页签新增“反馈”快捷入口。
|
||||
- 点击后进入独立路由 `/profile/feedback`。
|
||||
- 页面按移动端参考图实现“帮助与反馈”表单:问题描述、上传凭证、联系电话、提交、查看反馈与投诉记录。
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`
|
||||
- `src/components/platform-entry/platformEntryTypes.ts`
|
||||
- `src/routing/appPageRoutes.ts`
|
||||
- `src/components/platform-entry/PlatformFeedbackView.tsx`
|
||||
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `src/routing/appPageRoutes.test.ts`
|
||||
- `src/components/platform-entry/PlatformFeedbackView.test.tsx`
|
||||
|
||||
## 落地顺序
|
||||
|
||||
1. 先补 PRD,避免从参考图直接猜代码细节。
|
||||
2. 在 `SelectionStage` 中新增 `'profile-feedback'`。
|
||||
3. 在 `STAGE_ROUTE_ENTRIES` 添加:
|
||||
- `['profile-feedback', '/profile/feedback']`
|
||||
4. 新建 `PlatformFeedbackView`:
|
||||
- `onBack: () => void`
|
||||
- `onSubmit?: (payload) => void | Promise<void>`
|
||||
- 内部维护描述、联系电话、上传图片预览、错误态、成功态。
|
||||
- 首版没有后端接口时,`onSubmit` 为后续 API 接入点。
|
||||
5. 在 `RpgEntryHomeViewProps` 添加 `onOpenFeedback?: () => void`。
|
||||
6. 在“我的”页签快捷入口区新增 `ProfileShortcutButton`:
|
||||
- `label="反馈"`
|
||||
- `subLabel="帮助与反馈"`
|
||||
- `onClick={onOpenFeedback}`
|
||||
7. 在 `PlatformEntryFlowShellImpl`:
|
||||
- import `PlatformFeedbackView`
|
||||
- 新增 `openProfileFeedback`
|
||||
- 未登录则 `authUi?.openLoginModal()`
|
||||
- 已登录则 `setPlatformTab('profile')` + `setSelectionStage('profile-feedback')`
|
||||
- 直达阶段时 `useEffect` 同步 `profile` tab
|
||||
- 渲染 `selectionStage === 'profile-feedback'` 的独立 `<motion.div>`
|
||||
8. 增加测试:
|
||||
- 路由双向解析
|
||||
- 页面字段渲染
|
||||
- 描述低于 10 字不提交
|
||||
- 提交时 trim payload
|
||||
- 顶部返回按钮调用 `onBack`
|
||||
|
||||
## UI 参考要点
|
||||
|
||||
- 移动端优先,页面最大宽度约 30rem。
|
||||
- 浅灰背景,白色圆角卡片。
|
||||
- 顶部标题:`帮助与反馈`。
|
||||
- 分区标题:`反馈问题`。
|
||||
- 问题描述 placeholder:提示填写 10 字以上,勿填身份证号等隐私。
|
||||
- 字数计数:`0/200`。
|
||||
- 上传凭证:最多四张,上传占位显示 `上传凭证` 和 `(最多四张)`。
|
||||
- 联系电话:选填。
|
||||
- 主按钮:蓝色圆角 `提交`。
|
||||
- 底部链接:`查看反馈与投诉记录`。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
|
||||
# 或
|
||||
npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
|
||||
```
|
||||
|
||||
## 已踩坑/经验
|
||||
|
||||
- `appPageRoutes.test.ts` 若已有历史内容,写入测试时注意不要误删旧用例;最终 commit 里出现 “insertions + deletions” 时要检查是否覆盖了既有测试。
|
||||
- 页面组件里的图片预览需要在移除或卸载时 `URL.revokeObjectURL`,避免泄漏。
|
||||
- 对隐藏 file input 的测试可以先不强行覆盖,首版覆盖表单字段、校验和 payload 更稳定。
|
||||
- 如果用 `authUi` 对象作为 callback 依赖,需确认引用稳定性;现有 shell 中可接受,但复杂化时考虑拆出具体字段依赖。
|
||||
149
.hermes/skills/genarrative-profile-invite-flow/SKILL.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: genarrative-profile-invite-flow
|
||||
description: 在 Genarrative 中排查或修改邀请码、邀请好友、首次登录后填写邀请码、我的页签邀请码兑换链路时使用。
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Genarrative, 邀请码, referral, auth, profile, query-params, 前端]
|
||||
related_skills: []
|
||||
---
|
||||
|
||||
# Genarrative 邀请码与邀请好友流程
|
||||
|
||||
用于排查或修改 Genarrative 的邀请码读取、填写、兑换、邀请中心与“我的”页签相关能力。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 判断 URL query 参数中的邀请码是否被读取。
|
||||
- 修改邀请码填写入口、首次登录后引导或“我的”页签兑换入口。
|
||||
- 排查邀请码预填、兑换、已填写状态、邀请好友复制链接。
|
||||
- 修改邀请中心 API client 或前端 referral UI。
|
||||
- 回答用户关于“邀请码在哪里填 / 从哪里配置 / query 是否支持”的问题。
|
||||
|
||||
## 先做代码核对,不要只凭旧记忆回答
|
||||
|
||||
邀请码流程近期发生过迁移:不要默认认为登录窗口可填写邀请码。回答前优先搜索并核对当前代码,尤其是:
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
root=Path('src')
|
||||
terms=['RegistrationInviteModal','readInviteCodeFromLocation','referralRedeemCode','redeemRpgProfileReferralInviteCode','邀请码','inviteCode']
|
||||
for term in terms:
|
||||
print('\n---', term)
|
||||
for p in root.rglob('*'):
|
||||
if p.is_file() and p.suffix in ['.ts', '.tsx']:
|
||||
try:
|
||||
txt=p.read_text('utf-8')
|
||||
except Exception:
|
||||
continue
|
||||
if term in txt:
|
||||
for i, line in enumerate(txt.splitlines(), 1):
|
||||
if term in line:
|
||||
print(f'{p}:{i}:{line.strip()[:180]}')
|
||||
```
|
||||
|
||||
## 当前前端链路口径
|
||||
|
||||
### 1. AuthGate 中仍有旧 query 读取逻辑
|
||||
|
||||
文件:
|
||||
- `src/components/auth/AuthGate.tsx`
|
||||
|
||||
重点函数 / 状态:
|
||||
- `readInviteCodeFromLocation()`
|
||||
- `pendingInviteCode`
|
||||
- `showRegistrationInviteModal`
|
||||
- `RegistrationInviteModal`
|
||||
|
||||
当前旧逻辑会读取:
|
||||
- `?inviteCode=...`
|
||||
- `?invite_code=...`
|
||||
|
||||
并把值清洗为大写字母数字形式。
|
||||
|
||||
### 2. 登录窗口本身不再填写邀请码
|
||||
|
||||
不要回答“登录窗口可填写邀请码”。当前登录弹窗 `LoginScreen` 只负责登录 / 注册账号;邀请码填写已迁移到登录后的流程。
|
||||
|
||||
### 3. 新版“我的”页签兑换入口在 RpgEntryHomeView
|
||||
|
||||
文件:
|
||||
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
|
||||
重点常量 / 函数 / 状态:
|
||||
- `PROFILE_INVITE_QUERY_KEYS`:新版 query 支持 `inviteCode` / `invite_code`。
|
||||
- `normalizeProfileInviteQueryCode()`:去掉非字母数字并转大写。
|
||||
- `readProfileInviteCodeFromLocationSearch()`:从 `window.location.search` 读取并 normalize。
|
||||
- `pendingProfileInviteCode`:组件初始化时读取 query 邀请码。
|
||||
- `referralCenter`
|
||||
- `referralRedeemCode`
|
||||
- `setReferralRedeemCode`
|
||||
- `openProfilePopupPanel('redeem')`
|
||||
- `submitReferralRedeemCode()`
|
||||
- `canShowReferralRedeemShortcut`
|
||||
- `isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt)`
|
||||
|
||||
UI 中“填邀请码”面板会使用 `referralRedeemCode` 作为输入值,并通过 `submitReferralRedeemCode()` 提交。当前新版实现会在首次打开“填邀请码”面板时用 `pendingProfileInviteCode` 预填输入框;例如 `/?inviteCode=spring-2026` 会预填为 `SPRING2026`。
|
||||
|
||||
### 4. 新版兑换 API client
|
||||
|
||||
文件:
|
||||
- `src/services/rpg-entry/rpgProfileClient.ts`
|
||||
|
||||
函数:
|
||||
- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center`
|
||||
- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code`
|
||||
|
||||
## 判断 query 参数是否真正接入新版流程
|
||||
|
||||
回答这类问题时要区分两层:
|
||||
|
||||
1. “是否存在旧 query 读取代码”:看 `AuthGate.tsx` 的 `readInviteCodeFromLocation()`。
|
||||
2. “query 是否接到新版填写入口”:看 `RpgEntryHomeView.tsx` 是否存在 `pendingProfileInviteCode` / `readProfileInviteCodeFromLocationSearch()`,以及打开 `openProfilePopupPanel('redeem')` 时是否把该值写回 `referralRedeemCode`。
|
||||
|
||||
当前新版流程已经支持 `inviteCode` / `invite_code` query 预填“我的”页签的“填邀请码”弹窗;登录窗口仍不填写邀请码。
|
||||
|
||||
如果未来代码只看到 AuthGate 读 query,但没有看到 `RpgEntryHomeView` 的 `referralRedeemCode` 从 query 初始化,就应回答:
|
||||
|
||||
> 代码里仍支持读取 `inviteCode` / `invite_code`,但新版“第一次登录后 / 我的页签”的填写入口未必已经完整接入该 query 值;需要继续把 query 值传入新版 profile referral redeem 流程。
|
||||
|
||||
## 修改建议顺序
|
||||
|
||||
如果要把 query 邀请码完整接入新版流程,建议按这个顺序做:
|
||||
|
||||
1. 先确定 query 参数规范:继续支持 `inviteCode` / `invite_code`,并统一 normalize。
|
||||
2. 在 `RpgEntryHomeView.tsx` 内用 `readProfileInviteCodeFromLocationSearch(window.location.search)` 初始化 `pendingProfileInviteCode`。
|
||||
3. 用 `pendingProfileInviteCode` 初始化 `referralRedeemCode`,并在 `openProfilePopupPanel('redeem')` 时重新写回,避免关闭后再次打开被清空。
|
||||
4. 如产品要求自动弹出:
|
||||
- 有 `pendingProfileInviteCode` 且未登录时,自动调用 `authUi?.openLoginModal()` 打开登录窗口;登录窗口仍不承接邀请码输入。
|
||||
- 有 `pendingProfileInviteCode` 且已登录时,自动将 `referralRedeemCode` 设为该 query 邀请码,并 `setProfilePopupPanel('redeem')` 直接打开“填邀请码”面板。
|
||||
- 用 `useRef` 记录是否已处理过当前 query,避免组件重渲染或 `authUi` 对象变化导致重复弹窗。
|
||||
- 当前项目实现已从“只预填、不自动弹”调整为上述行为。
|
||||
5. 兑换成功后清理输入态;是否清理 URL query 需由产品决定,避免破坏分享链接归因。
|
||||
6. 补测试覆盖:未登录访问带 query、已登录访问带 query 自动打开填写面板、我的页签手动打开、已填写邀请码、过期窗口、空/非法 query。当前已有 `RpgEntryHomeView.recharge.test.tsx` 覆盖:
|
||||
- `invite query opens login modal for logged out users`
|
||||
- `invite query opens redeem modal directly for logged in users`
|
||||
- `profile redeem invite modal reads query invite code after login`
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 不要把旧 `RegistrationInviteModal` 误认为当前唯一入口。
|
||||
2. 不要说“登录窗口可以填写邀请码”,除非当前代码重新把邀请码输入放回 `LoginScreen`。
|
||||
3. `AuthGate` 读到 query 不等于新版 `RpgEntryHomeView` 已经预填。
|
||||
4. “第一次登录后”与“我的页签”可能是两个入口;修改时要同时检查自动引导和手动入口。
|
||||
5. `canShowReferralRedeemShortcut` 受登录态、创建时间窗口、邀请中心初始化、已兑换状态共同影响。
|
||||
6. 邀请码 URL 通常由 `inviteLinkPath` 生成,复制逻辑在 `copyInviteInfo()`,不要只改兑换入口而忘记分享链接格式。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `references/query-invite-code-flow-2026-05-07.md`:本次会话确认的邀请码 query 与新版 profile referral 入口关系。
|
||||
|
||||
## 验证标准
|
||||
|
||||
- 能明确回答当前 query 参数读取位置与参数名。
|
||||
- 能区分旧 AuthGate 邀请弹窗与新版“我的”页签 referral redeem。
|
||||
- 若实现改动,测试覆盖带 query 的登录后预填/弹窗行为,以及已填写邀请码时不再提示。
|
||||
@@ -0,0 +1,101 @@
|
||||
# Query 邀请码与新版 profile referral 入口关系(2026-05-07)
|
||||
|
||||
## 会话结论
|
||||
|
||||
用户指出:邀请码填写流程已经修改,登录窗口目前填写不了邀请码;邀请码填写被挪到了第一次登录后以及“我的”页签中。
|
||||
|
||||
因此后续回答或修改时不能只根据 `AuthGate` 里的旧逻辑判断“已支持”。
|
||||
|
||||
## 当前代码观察
|
||||
|
||||
### 旧 AuthGate 逻辑
|
||||
|
||||
文件:`src/components/auth/AuthGate.tsx`
|
||||
|
||||
- `readInviteCodeFromLocation()` 读取 `window.location.search`。
|
||||
- 支持 `inviteCode` / `invite_code`。
|
||||
- 会 normalize 为大写字母数字。
|
||||
- 写入 `pendingInviteCode`,传给 `RegistrationInviteModal`。
|
||||
|
||||
这只能说明“旧层仍有 query 读取”。
|
||||
|
||||
### 新版 profile referral 入口
|
||||
|
||||
文件:`src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
|
||||
- “我的”页签标签为 `profile`。
|
||||
- 兑换输入状态:`referralRedeemCode`。
|
||||
- 打开填邀请码面板:`openProfilePopupPanel('redeem')`。
|
||||
- 提交兑换:`submitReferralRedeemCode()`。
|
||||
- 可显示快捷入口受 `canShowReferralRedeemShortcut` 控制。
|
||||
|
||||
文件:`src/services/rpg-entry/rpgProfileClient.ts`
|
||||
|
||||
- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center`
|
||||
- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code`
|
||||
|
||||
## 回答口径
|
||||
|
||||
如果被问“是否支持 query 参数读取邀请码”,应回答:
|
||||
|
||||
- 代码里仍有 query 读取,支持 `inviteCode` / `invite_code`。
|
||||
- 但登录窗口不再填写邀请码。
|
||||
- 新版入口在第一次登录后 / 我的页签;需要检查 `referralRedeemCode` 是否从 query 初始化。
|
||||
- 若没有该连接,就不能说新版流程完整支持 query 预填。
|
||||
|
||||
## 本次实现后的状态
|
||||
|
||||
已将 query 邀请码读取接入新版 `RpgEntryHomeView`:
|
||||
|
||||
- `PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code']`
|
||||
- `normalizeProfileInviteQueryCode()`:去除非字母数字并转大写。
|
||||
- `readProfileInviteCodeFromLocationSearch(window.location.search)`:读取 query 邀请码。
|
||||
- `pendingProfileInviteCode`:组件初始化时读取 query。
|
||||
- `referralRedeemCode`:用 `pendingProfileInviteCode` 初始化。
|
||||
- `openProfilePopupPanel('redeem')`:打开“填邀请码”时重新写入 `pendingProfileInviteCode`,避免首次打开或重新打开时丢失 query 预填。
|
||||
|
||||
当前行为:只预填,不自动弹出“填邀请码”面板;用户仍需在“我的”页签点击“填邀请码”。
|
||||
|
||||
验证测试:
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
|
||||
npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0
|
||||
node scripts/check-encoding.mjs
|
||||
```
|
||||
|
||||
测试用例:`profile redeem invite modal reads query invite code after login` 覆盖 `/?inviteCode=spring-2026` 预填为 `SPRING2026`。
|
||||
|
||||
## 后续调整:带邀请码链接自动开窗
|
||||
|
||||
用户进一步明确期望:
|
||||
|
||||
- 如果用户未登录,直接打开登录窗口。
|
||||
- 如果用户已登录,直接打开邀请码填写窗口。
|
||||
|
||||
实现要点:
|
||||
|
||||
- 在 `RpgEntryHomeView.tsx` 中保留 `pendingProfileInviteCode` 作为 query 邀请码来源。
|
||||
- 新增 `autoOpenedInviteQueryRef = useRef(false)`,防止 effect 重复触发弹窗。
|
||||
- 新增 `useEffect`:
|
||||
- 无 query 邀请码或已处理过则 return。
|
||||
- 未登录:调用 `authUi?.openLoginModal()`。
|
||||
- 已登录:设置 `referralRedeemCode`、清空 referral 错误/成功提示、`setProfilePopupPanel('redeem')`。
|
||||
- 登录窗口仍不接收邀请码;邀请码只在登录后的 profile referral redeem 面板显示。
|
||||
- 仍然只自动打开和预填,不自动提交兑换。
|
||||
|
||||
补充测试:
|
||||
|
||||
- `invite query opens login modal for logged out users`
|
||||
- `invite query opens redeem modal directly for logged in users`
|
||||
- 原 `profile redeem invite modal reads query invite code after login` 同步调整为直接断言自动打开后的输入值。
|
||||
|
||||
验证命令仍为:
|
||||
|
||||
```bash
|
||||
cd <repo-root>
|
||||
npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
|
||||
npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0
|
||||
node scripts/check-encoding.mjs
|
||||
```
|
||||
1
.worktreeinclude
Normal file
@@ -0,0 +1 @@
|
||||
.env.secrets.local
|
||||
43
AGENTS.md
@@ -1,7 +1,32 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 团队 Hermes 共享记忆
|
||||
- 本仓库的团队级 Hermes 共享内容位于 [`.hermes/`](.hermes/),用于在 3 名开发人员各自本地 Hermes 之间同步长期项目记忆。
|
||||
- 开始复杂开发任务前,除阅读本文件外,还应优先读取:
|
||||
- [`.hermes/README.md`](.hermes/README.md)
|
||||
- [`.hermes/shared-memory/project-overview.md`](.hermes/shared-memory/project-overview.md)
|
||||
- [`.hermes/shared-memory/team-conventions.md`](.hermes/shared-memory/team-conventions.md)
|
||||
- [`.hermes/shared-memory/development-workflow.md`](.hermes/shared-memory/development-workflow.md)
|
||||
- 与任务相关的 [`.hermes/shared-memory/decision-log.md`](.hermes/shared-memory/decision-log.md) 和 [`.hermes/shared-memory/pitfalls.md`](.hermes/shared-memory/pitfalls.md)
|
||||
- 如果本次任务产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则,应同步更新 `.hermes/shared-memory/` 中对应文件。
|
||||
- 仓库 `.hermes/` 只保存可进入 Git 的团队共享内容;禁止提交个人 `~/.hermes` 配置、`.env`、API Key、Token、会话记录、认证文件和本地私密路径。
|
||||
- 若 `.hermes/shared-memory/` 与当前代码或 `docs/` 最新文档冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆。
|
||||
|
||||
## Agent skills
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues are tracked in the self-hosted Gitea remote for this repo. Use Gitea Issues via the configured Gitea UI/API or `tea` CLI when available; do not use GitHub `gh` or GitLab `glab` unless the repo is migrated. See `docs/agents/issue-tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
Use the default canonical triage labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context layout: read root `CONTEXT.md` when present and architecture decisions from `docs/adr/`. See `docs/agents/domain.md`.
|
||||
|
||||
## 项目约束
|
||||
- 在修改server-rs的内容时,不要去兼容server-node中的任何内容,只允许参考,以及把server-node中未迁移到server-rs的内容迁移过来
|
||||
- 代码需要有完善的中文注释
|
||||
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
|
||||
- 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中
|
||||
@@ -14,23 +39,33 @@
|
||||
- UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。
|
||||
- 不要在gitignore中添加.env.local文件。
|
||||
- 严格遵循简洁的代码风格
|
||||
- 前端只负责做表现,所有的逻辑、数据都放到后端工程,后端使用server-rs中用Rust+spacetimeDB的方案实现,禁止继续使用server-node(Express)和postgreSQL
|
||||
- 后端采用多crate设计
|
||||
- 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。
|
||||
- 禁止将功能说明描述类的文本默认写入UI界面中。
|
||||
- prd文档中每个模块的描述要落地设计到可以精准编码到位,不能出现需求落地漂移。
|
||||
- 点击按钮弹出独立的面板的设计不要实现成在当前面板下面显示内容。
|
||||
- 每个阶段任务完成后自动压缩上下文,确保后续阶段在清晰、低噪音的上下文基础上继续推进。
|
||||
|
||||
## 后端技术约束
|
||||
- 后端最新技术约束以 [`docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`](docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md) 为总纲;执行和收口状态以 [`docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`](docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md) 为准。
|
||||
- 契约、路由、DTO 去留和 breaking change 以 [`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`](docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md) 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口。
|
||||
- SpacetimeDB 表结构、自动迁移限制和冲突处理以 [`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`](docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md) 为准;涉及 table、reducer、procedure、row shape 或绑定变化时,必须同步 `migration.rs`、表目录和生成绑定。
|
||||
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`。旧 `server-node`、Express、PostgreSQL 不再作为兼容目标;历史实现只能作为迁移参考,若旧文档与 DDD 约束冲突,先修正文档和方案再编码。
|
||||
- DDD 分层边界按总纲执行:领域规则沉到 `module-*`,SpacetimeDB 表和事务编排留在 `spacetime-module`,后端访问 SpacetimeDB 统一经 `spacetime-client` facade,HTTP/SSE/BFF 留在 `api-server`,外部副作用留在 `platform-*`,前后端 DTO 留在 `shared-contracts`。
|
||||
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影或后端 API 直接实现业务规则。
|
||||
- 修改后端代码后,按对应 DDD 文档中的验收命令执行测试;涉及 API smoke 时使用 `npm run api-server` 重新拉起后端并执行相应自动测试,同时确认 `/healthz`。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关脚本、环境变量、测试、文档要求和命名全部视为历史残留,禁止新增、运行或引用;若旧文档仍要求 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`,以 [`docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`](docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md) 和本文件为准,并先修正文档口径。
|
||||
- 除 CI/CD 脚本内部受控用法外,人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`。本地数据隔离使用项目脚本或 `--data-dir`,发布目标必须显式传 `--server` / `--server-url`,身份问题通过同一 CLI 登录态、专用运行用户或显式 token 处理;若旧文档仍推荐 `--root-dir`,先修正文档口径再执行。
|
||||
- 凡是涉及 SpacetimeDB 的设计、实现、脚本、调试、前端绑定接入,统一显式使用以下 skill 作为执行依据:
|
||||
- [$spacetimedb-cli](.codex\\skills\\spacetimedb-cli\\SKILL.md)
|
||||
- [$spacetimedb-rust](.codex\\skills\\spacetimedb-rust\\SKILL.md)
|
||||
- [$spacetimedb-concepts](.codex\\skills\\spacetimedb-concepts\\SKILL.md)
|
||||
- [$spacetimedb-typescript](.codex\\skills\\spacetimedb-typescript\\SKILL.md)
|
||||
- 涉及 `spacetime` CLI、发布、绑定生成、本地联调时,按 `spacetimedb-cli` 执行。
|
||||
- 涉及 `npm run dev` / `npm run dev:rust` / `npm run dev:web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
|
||||
- 涉及 `crates/spacetime-module` 的表、reducer、view、Rust API 使用时,按 `spacetimedb-rust` 与 `spacetimedb-concepts` 执行。
|
||||
- 涉及前端或 Node 侧的 SpacetimeDB TypeScript SDK、订阅、绑定使用时,按 `spacetimedb-typescript` 与 `spacetimedb-concepts` 执行。
|
||||
- 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
|
||||
- 修改后端代码后,必须使用 `npm run api-server:maincloud` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
|
||||
- 修改后端代码后,必须使用 `npm run api-server` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
|
||||
- 数据库表结构更改后,需要对齐migration.rs
|
||||
|
||||
## 文档图谱
|
||||
|
||||
@@ -44,7 +44,8 @@ npm run dev
|
||||
|
||||
补充说明:
|
||||
|
||||
- `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server` 与 Vite 前端,适合完整联调。
|
||||
- `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server`、主站 Vite 与后台 Vite,适合完整联调。
|
||||
- 主站默认地址是 `http://127.0.0.1:3000`,后台可从 `http://127.0.0.1:3000/admin/` 进入,也可直连 `http://127.0.0.1:3102`。
|
||||
- 如果只想单独启动前端页面,可使用 `npm run dev:web`,默认代理到本地 Rust `api-server`。
|
||||
|
||||
构建生产包:
|
||||
|
||||