Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-05 14:58:07 +08:00
238 changed files with 29234 additions and 144 deletions
@@ -1,4 +0,0 @@
interface:
display_name: "新增玩法入口"
short_description: "把新增玩法入口的文档、配置、路由和验证流程一次收口"
default_prompt: "Use $genarrative-gameplay-entry-type to add a new gameplay entry type end to end in Genarrative."
@@ -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 bucketbucket 输出要有稳定排序,并显式携带 `bucketKey``bucketStartDateKey``bucketEndDateKey``value`
8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type unionadmin-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/单元测试。
+53
View File
@@ -0,0 +1,53 @@
# 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
```
## 推荐给 Hermes 的启动提示
在本仓库中开始复杂任务时,可以先对 Hermes 说:
```text
请先读取 AGENTS.md 以及 .hermes/shared-memory/ 下与本任务相关的团队共享记忆,再开始分析。若任务完成后产生稳定项目知识,请更新 .hermes/shared-memory/ 对应文件。
```
## 需要沉淀到这里的内容
- 长期有效的架构约定
- 反复会用到的本地开发/测试流程
- 已确认的接口契约或模块边界
- 重要技术决策及原因
- 踩坑、排障方式、验证命令
- 团队协作规则和任务交接规范
## 不应沉淀到这里的内容
- API Key、Token、Cookie、私有密钥
- 个人账号、个人本地绝对路径、个人隐私信息
- 大段临时聊天记录
- 尚未确认的一次性猜测
- 构建产物、日志、缓存、数据库 dump
File diff suppressed because it is too large Load Diff
@@ -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 的二次确认与表单展示。
---
**结论:** 这是一个适合分阶段落地的改动,建议先做“邀请码时间窗 + 后台统一二次确认”的基础能力,再补交互细节。
+65
View File
@@ -0,0 +1,65 @@
# 决策记录
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
## 记录格式
```md
## YYYY-MM-DD 决策标题
- 背景:为什么需要这个决策
- 决策:最终决定是什么
- 影响范围:涉及哪些模块/文档/流程
- 验证方式:如何确认决策仍有效
- 关联文档:相关 PRD、技术文档、提交或 Issue
```
---
## 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`
@@ -0,0 +1,203 @@
# 开发工作流
> 用途:给本地 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
```
## 常用检查命令
编码检查:
```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` 重新拉起后端。
- 检查 `/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`
## 前端相关默认验证
前端修改后,应根据修改范围选择:
- `npm run check:encoding`
- `npm run lint:eslint`
- `npm run typecheck`
- `npm run test`
- 页面交互 smoke
- 移动端视口检查
前端原则:
- 移动端优先,再兼容网页端。
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
- 不在 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
View 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
View File
@@ -0,0 +1,53 @@
# 任务交接模板
> 用途:当一名开发者把任务交给另一名开发者,或让 Hermes 接续上下文时,复制本模板并填写。
## 基本信息
- 任务名称:
- 负责人:
- 当前分支:
- 相关需求/Issue
- 相关文档:
## 背景
简要说明为什么做这个任务,以及业务/技术目标。
## 已完成
- [ ]
- [ ]
- [ ]
## 未完成
- [ ]
- [ ]
- [ ]
## 关键文件
- `path/to/file`:说明
- `path/to/file`:说明
## 当前问题/风险
-
## 已执行验证
```bash
# 粘贴已执行命令和结果摘要
```
## 建议下一步
1.
2.
3.
## 是否需要更新团队记忆
- [ ] 不需要
- [ ] 需要,建议更新:`.hermes/shared-memory/...`
+102
View File
@@ -0,0 +1,102 @@
# 踩坑与排障记录
> 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。
## 记录格式
```md
## 问题标题
- 现象:看到什么错误或异常行为
- 原因:确认后的根因
- 处理:具体修复步骤
- 验证:如何确认修复有效
- 关联:相关文件、文档、提交或 Issue
```
---
## 中文乱码与编码风险
- 现象:中文文案、注释、剧情或文档显示为乱码,或被改写成英文。
- 原因:Windows/PowerShell/终端编码不一致,或整文件重写导致编码变化。
- 处理:
- 不要直接沿用乱码文本。
- 不要用英文替换中文,除非用户明确要求翻译。
- 在 PowerShell 5.1 中显式使用 UTF-8。
- 优先用 Python/Node 或 `Get-Content -Encoding UTF8` 核对原文。
- 修改中文文件时优先局部补丁,避免无关内容重写。
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:`AGENTS.md``npm run check:encoding`
## `.hermes` 只放共享内容,不放个人 Hermes 配置
- 现象:团队成员误把个人 Hermes 配置、会话或密钥复制进仓库。
- 原因:仓库 `.hermes/` 与个人 `~/.hermes/` 名称相似。
- 处理:仓库 `.hermes/` 只放 Markdown 共享记忆、计划和可公开 skills;不提交 `.env``config.yaml``sessions/``auth.json`
- 验证:提交前检查 `git diff -- .hermes`,确认没有密钥、会话记录或个人路径敏感信息。
- 关联:`.hermes/README.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 replica identity 不匹配
- 现象:本地 standalone 启动时报 `mismatched database identity`
- 原因:root-dir / replica 数据残留与当前数据库身份不一致。
- 处理:按本地 replica identity mismatch 文档进行备份、重建和脚本诊断。
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
- 关联:`docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md`
## 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`
## 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`
## server-rs 默认 cargo build 不能等同于构建 SpacetimeDB 模块
- 现象:在 `server-rs` 下无参数 `cargo build` 期望同时构建 `spacetime-module`,导致链接或构建范围误判。
- 原因:workspace default-members 当前只包含 `crates/api-server`SpacetimeDB module 有独立构建/发布方式。
- 处理:默认 Rust 构建只覆盖原生 `api-server`;模块产物继续走 `spacetime build` / publish / bindings 生成流程。
- 验证:查看 `server-rs/Cargo.toml` default-members,并按相关 SpacetimeDB 文档执行模块构建。
- 关联:`server-rs/Cargo.toml``docs/technical/RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.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`
## 个人任务 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`
+138
View File
@@ -0,0 +1,138 @@
# 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 build` / 发布链路。
## 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`
- 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
+95
View File
@@ -0,0 +1,95 @@
# 团队协作约定
> 用途:约定 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 分层,不把业务真相下沉到前端或临时兼容层。
- 涉及 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`
## 共享记忆更新准则
适合更新:
- 新增稳定架构约定
- 新增长期开发流程
- 已验证的踩坑和排障步骤
- 重要接口契约变化
- 团队协作规范变化
- 文档索引或阅读顺序变化
不适合更新:
- 一次性临时计划
- 未验证猜测
- 个人偏好和个人路径
- 敏感信息
- 大段聊天记录
+27
View 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 应包含触发条件、步骤、坑点和验证方式。
+12
View File
@@ -1,5 +1,17 @@
# 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/` 为准,并同步修正过期共享记忆。
## 项目约束
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
+5
View File
@@ -122,6 +122,8 @@ export interface AdminUpsertProfileRedeemCodeRequest {
export interface AdminUpsertProfileInviteCodeRequest {
inviteCode: string;
metadata?: Record<string, unknown>;
startsAt?: string | null;
expiresAt?: string | null;
}
export interface AdminDisableProfileRedeemCodeRequest {
@@ -166,6 +168,9 @@ export interface ProfileInviteCodeAdminResponse {
userId: string;
inviteCode: string;
metadata: Record<string, unknown>;
startsAt?: string | null;
expiresAt?: string | null;
status: 'pending' | 'active' | 'expired';
createdAt: string;
updatedAt: string;
}
@@ -0,0 +1,105 @@
import {useCallback, useEffect, useRef, useState} from 'react';
interface AdminWriteConfirmOptions {
action: string;
target: string;
}
interface PendingConfirm extends AdminWriteConfirmOptions {
resolve: (confirmed: boolean) => void;
}
export function useAdminWriteConfirm() {
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(null);
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
const confirmWrite = useCallback((options: AdminWriteConfirmOptions) => {
return new Promise<boolean>((resolve) => {
setPendingConfirm((current) => {
if (current) {
current.resolve(false);
}
return {...options, resolve};
});
});
}, []);
const closeConfirm = useCallback(
(confirmed: boolean) => {
const current = pendingConfirm;
if (!current) {
return;
}
setPendingConfirm(null);
current.resolve(confirmed);
},
[pendingConfirm],
);
useEffect(() => {
if (!pendingConfirm) {
return;
}
cancelButtonRef.current?.focus();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
closeConfirm(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [closeConfirm, pendingConfirm]);
const confirmDialog = pendingConfirm ? (
<div
aria-modal="true"
aria-labelledby="admin-write-confirm-title"
className="admin-confirm-backdrop"
role="dialog"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
closeConfirm(false);
}
}}
>
<section className="admin-confirm-panel">
<div className="admin-panel-heading">
<h3 id="admin-write-confirm-title"></h3>
<span>{pendingConfirm.action}</span>
</div>
<dl className="admin-info-list">
<div>
<dt></dt>
<dd>{pendingConfirm.action}</dd>
</div>
<div>
<dt></dt>
<dd>{pendingConfirm.target}</dd>
</div>
</dl>
<div className="admin-confirm-warning">线</div>
<div className="admin-confirm-actions">
<button
className="admin-secondary-button"
ref={cancelButtonRef}
type="button"
onClick={() => closeConfirm(false)}
>
</button>
<button
className="admin-danger-button"
type="button"
onClick={() => closeConfirm(true)}
>
</button>
</div>
</section>
</div>
) : null;
return {confirmWrite, confirmDialog};
}
@@ -7,6 +7,7 @@ import type {
AdminDebugHttpMethod,
AdminDebugHttpResponse,
} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {formatUnknownJson, handlePageError} from './pageUtils';
interface AdminDebugHttpPageProps {
@@ -33,6 +34,7 @@ export function AdminDebugHttpPage({
const [result, setResult] = useState<AdminDebugHttpResponse | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
const jsonPreview = useMemo(
() => formatUnknownJson(result?.bodyJson),
@@ -46,6 +48,16 @@ export function AdminDebugHttpPage({
}
setErrorMessage('');
if (method !== 'GET') {
const confirmed = await confirmWrite({
action: `${method} 调试请求`,
target: path.trim(),
});
if (!confirmed) {
return;
}
}
setIsSubmitting(true);
try {
const response = await debugAdminHttp(token, {
@@ -209,6 +221,7 @@ export function AdminDebugHttpPage({
)}
</section>
</div>
{confirmDialog}
</section>
);
}
@@ -5,7 +5,11 @@ import {
listProfileInviteCodes,
upsertProfileInviteCode,
} from '../api/adminApiClient';
import type {ProfileInviteCodeAdminResponse} from '../api/adminApiTypes';
import type {
AdminUpsertProfileInviteCodeRequest,
ProfileInviteCodeAdminResponse,
} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {handlePageError} from './pageUtils';
interface AdminInviteCodePageProps {
@@ -22,12 +26,15 @@ export function AdminInviteCodePage({
onResultChange,
}: AdminInviteCodePageProps) {
const [inviteCode, setInviteCode] = useState('');
const [startsAt, setStartsAt] = useState('');
const [expiresAt, setExpiresAt] = useState('');
const [metadataText, setMetadataText] = useState('{}');
const [errorMessage, setErrorMessage] = useState('');
const [listErrorMessage, setListErrorMessage] = useState('');
const [entries, setEntries] = useState<ProfileInviteCodeAdminResponse[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
useEffect(() => {
void refreshInviteCodes();
@@ -54,12 +61,29 @@ export function AdminInviteCodePage({
}
setErrorMessage('');
const validityError = validateValidityWindow(startsAt, expiresAt);
if (validityError) {
setErrorMessage(validityError);
return;
}
const confirmed = await confirmWrite({
action: '保存邀请码',
target: inviteCode.trim(),
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
const response = await upsertProfileInviteCode(token, {
const payload: AdminUpsertProfileInviteCodeRequest = {
inviteCode: inviteCode.trim(),
metadata: parseMetadata(metadataText),
});
startsAt: startsAt ? toIsoDateTime(startsAt) : null,
expiresAt: expiresAt ? toIsoDateTime(expiresAt) : null,
};
const response = await upsertProfileInviteCode(token, payload);
onResultChange(response);
upsertEntry(response);
fillForm(response);
@@ -89,9 +113,13 @@ export function AdminInviteCodePage({
function fillForm(entry: ProfileInviteCodeAdminResponse) {
setInviteCode(entry.inviteCode);
setStartsAt(toDateTimeLocalValue(entry.startsAt));
setExpiresAt(toDateTimeLocalValue(entry.expiresAt));
setMetadataText(JSON.stringify(entry.metadata, null, 2));
}
const validityError = validateValidityWindow(startsAt, expiresAt);
return (
<section className="admin-page">
<div className="admin-page-heading">
@@ -127,6 +155,25 @@ export function AdminInviteCodePage({
/>
</label>
<div className="admin-form-row">
<label className="admin-field">
<span></span>
<input
type="datetime-local"
value={startsAt}
onChange={(event) => setStartsAt(event.target.value)}
/>
</label>
<label className="admin-field">
<span></span>
<input
type="datetime-local"
value={expiresAt}
onChange={(event) => setExpiresAt(event.target.value)}
/>
</label>
</div>
<label className="admin-field">
<span>Metadata JSON</span>
<textarea
@@ -142,10 +189,20 @@ export function AdminInviteCodePage({
{errorMessage}
</div>
) : null}
{validityError ? (
<div className="admin-alert" role="status">
{validityError}
</div>
) : null}
<button
className="admin-primary-button"
disabled={isSaving || !inviteCode.trim() || !isMetadataReady(metadataText)}
disabled={
isSaving ||
!inviteCode.trim() ||
!isMetadataReady(metadataText) ||
Boolean(validityError)
}
type="submit"
>
<Save size={17} aria-hidden="true" />
@@ -165,8 +222,8 @@ export function AdminInviteCodePage({
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@@ -181,8 +238,13 @@ export function AdminInviteCodePage({
{entry.inviteCode}
</button>
</td>
<td>
<span className={`admin-status ${inviteValidityClass(entry)}`}>
{inviteValidityLabel(entry)}
</span>
<small>{formatValidityWindow(entry)}</small>
</td>
<td>{entry.createdAt}</td>
<td>{entry.updatedAt}</td>
</tr>
))}
</tbody>
@@ -206,6 +268,10 @@ export function AdminInviteCodePage({
<dt></dt>
<dd>{result.inviteCode}</dd>
</div>
<div>
<dt></dt>
<dd>{formatValidityWindow(result)}</dd>
</div>
<div>
<dt></dt>
<dd>{result.createdAt}</dd>
@@ -229,6 +295,7 @@ export function AdminInviteCodePage({
</section>
</div>
</div>
{confirmDialog}
</section>
);
}
@@ -255,6 +322,84 @@ function isMetadataReady(value: string) {
}
}
function validateValidityWindow(startsAt: string, expiresAt: string) {
if (!startsAt || !expiresAt) {
return '';
}
const startsAtTime = Date.parse(toIsoDateTime(startsAt));
const expiresAtTime = Date.parse(toIsoDateTime(expiresAt));
if (!Number.isFinite(startsAtTime) || !Number.isFinite(expiresAtTime)) {
return '有效期时间无效';
}
return startsAtTime < expiresAtTime ? '' : '截止时间必须晚于开始时间';
}
function toIsoDateTime(value: string) {
const time = Date.parse(value);
if (!Number.isFinite(time)) {
throw new Error('有效期时间无效');
}
return new Date(time).toISOString();
}
function toDateTimeLocalValue(value?: string | null) {
if (!value) {
return '';
}
const date = new Date(value);
if (!Number.isFinite(date.getTime())) {
return '';
}
const offsetMs = date.getTimezoneOffset() * 60 * 1000;
return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);
}
function inviteValidityLabel(entry: ProfileInviteCodeAdminResponse) {
const now = Date.now();
const startsAtTime = entry.startsAt ? Date.parse(entry.startsAt) : null;
const expiresAtTime = entry.expiresAt ? Date.parse(entry.expiresAt) : null;
if (startsAtTime && Number.isFinite(startsAtTime) && now < startsAtTime) {
return '未生效';
}
if (expiresAtTime && Number.isFinite(expiresAtTime) && now >= expiresAtTime) {
return '已过期';
}
if (entry.startsAt || entry.expiresAt) {
return '有效';
}
return '长期有效';
}
function inviteValidityClass(entry: ProfileInviteCodeAdminResponse) {
const label = inviteValidityLabel(entry);
if (label === '已过期') {
return 'admin-status-error';
}
if (label === '未生效') {
return 'admin-status-pending';
}
return 'admin-status-ok';
}
function formatValidityWindow(entry: ProfileInviteCodeAdminResponse) {
const startsAt = entry.startsAt ? formatDateTime(entry.startsAt) : '立即';
const expiresAt = entry.expiresAt ? formatDateTime(entry.expiresAt) : '长期';
return `${startsAt} / ${expiresAt}`;
}
function formatDateTime(value: string) {
const date = new Date(value);
if (!Number.isFinite(date.getTime())) {
return value;
}
return date.toLocaleString('zh-CN', {hour12: false});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -10,6 +10,7 @@ import type {
ProfileRedeemCodeAdminResponse,
ProfileRedeemCodeMode,
} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {handlePageError, splitLines} from './pageUtils';
interface AdminRedeemCodePageProps {
@@ -46,6 +47,7 @@ export function AdminRedeemCodePage({
const [isSaving, setIsSaving] = useState(false);
const [isDisabling, setIsDisabling] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
useEffect(() => {
void refreshRedeemCodes();
@@ -72,6 +74,14 @@ export function AdminRedeemCodePage({
}
setErrorMessage('');
const confirmed = await confirmWrite({
action: '保存兑换码',
target: code.trim(),
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
const response = await upsertProfileRedeemCode(token, {
@@ -101,6 +111,14 @@ export function AdminRedeemCodePage({
}
setDisableErrorMessage('');
const confirmed = await confirmWrite({
action: '停用兑换码',
target: disableCode.trim(),
});
if (!confirmed) {
return;
}
setIsDisabling(true);
try {
const response = await disableProfileRedeemCode(token, {
@@ -376,6 +394,7 @@ export function AdminRedeemCodePage({
</section>
</div>
</div>
{confirmDialog}
</section>
);
}
@@ -11,6 +11,7 @@ import type {
ProfileTaskCycle,
TrackingScopeKind,
} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {
filterAdminTrackingEventDefinitions,
findAdminTrackingEventDefinition,
@@ -28,12 +29,7 @@ const taskCycles: Array<{value: ProfileTaskCycle; label: string}> = [
{value: 'daily', label: '每日'},
];
const scopeKinds: Array<{value: TrackingScopeKind; label: string}> = [
{value: 'user', label: '用户'},
{value: 'site', label: '整站'},
{value: 'work', label: '作品'},
{value: 'module', label: '模块'},
];
const profileTaskScopeKind = 'user' satisfies TrackingScopeKind;
export function AdminTaskConfigPage({
token,
@@ -49,7 +45,6 @@ export function AdminTaskConfigPage({
const [eventKeySearch, setEventKeySearch] = useState('每日登录');
const [isEventKeyPickerOpen, setIsEventKeyPickerOpen] = useState(false);
const [cycle, setCycle] = useState<ProfileTaskCycle>('daily');
const [scopeKind, setScopeKind] = useState<TrackingScopeKind>('user');
const [threshold, setThreshold] = useState('1');
const [rewardPoints, setRewardPoints] = useState('10');
const [sortOrder, setSortOrder] = useState('10');
@@ -61,6 +56,7 @@ export function AdminTaskConfigPage({
const [isSaving, setIsSaving] = useState(false);
const [isDisabling, setIsDisabling] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
useEffect(() => {
void refreshTaskConfigs();
@@ -102,6 +98,14 @@ export function AdminTaskConfigPage({
}
setErrorMessage('');
const confirmed = await confirmWrite({
action: '保存任务配置',
target: taskId.trim(),
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
const response = await upsertProfileTaskConfig(token, {
@@ -110,7 +114,7 @@ export function AdminTaskConfigPage({
description,
eventKey: eventKey.trim(),
cycle,
scopeKind,
scopeKind: profileTaskScopeKind,
threshold: parsePositiveInteger(threshold),
rewardPoints: parsePositiveInteger(rewardPoints),
enabled,
@@ -132,6 +136,14 @@ export function AdminTaskConfigPage({
}
setDisableErrorMessage('');
const confirmed = await confirmWrite({
action: '停用任务配置',
target: disableTaskId.trim(),
});
if (!confirmed) {
return;
}
setIsDisabling(true);
try {
const response = await disableProfileTaskConfig(token, {
@@ -165,7 +177,6 @@ export function AdminTaskConfigPage({
setDescription(entry.description);
setEventKey(entry.eventKey);
setCycle(entry.cycle);
setScopeKind(entry.scopeKind);
setThreshold(String(entry.threshold));
setRewardPoints(String(entry.rewardPoints));
setSortOrder(String(entry.sortOrder));
@@ -181,7 +192,6 @@ export function AdminTaskConfigPage({
setEventKey(nextEventKey);
if (nextDefinition) {
setEventKeySearch(nextDefinition.title);
setScopeKind(nextDefinition.scopeKind);
} else {
setEventKeySearch(nextEventKey);
}
@@ -349,21 +359,6 @@ export function AdminTaskConfigPage({
))}
</select>
</label>
<label className="admin-field">
<span></span>
<select
value={scopeKind}
onChange={(event) =>
setScopeKind(event.target.value as TrackingScopeKind)
}
>
{scopeKinds.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
</div>
<div className="admin-form-row">
@@ -530,6 +525,7 @@ export function AdminTaskConfigPage({
</section>
</div>
</div>
{confirmDialog}
</section>
);
}
+54
View File
@@ -457,6 +457,47 @@ button:disabled {
font-weight: 700;
}
.admin-confirm-backdrop {
position: fixed;
inset: 0;
z-index: 80;
display: grid;
place-items: center;
background: rgba(23, 33, 43, 0.42);
padding: 16px;
}
.admin-confirm-panel {
display: grid;
width: min(100%, 420px);
gap: 16px;
border: 1px solid #d8e2e8;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 22px 60px rgba(23, 33, 43, 0.24);
padding: 18px;
}
.admin-confirm-warning {
border: 1px solid #efc894;
border-radius: 8px;
color: #8a5a1b;
background: #fffaf3;
padding: 10px 12px;
font-size: 13px;
font-weight: 650;
}
.admin-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.admin-confirm-actions button {
min-width: 92px;
}
.admin-primary-button {
color: #ffffff;
background: #126e82;
@@ -575,6 +616,11 @@ button:disabled {
background: #e6f5ed;
}
.admin-status-pending {
color: #8a5a1b;
background: #fff4df;
}
.admin-status-error {
color: #8a2f2f;
background: #fff1ef;
@@ -755,6 +801,14 @@ button:disabled {
padding: 16px;
}
.admin-confirm-panel {
padding: 16px;
}
.admin-confirm-actions {
display: grid;
}
.admin-login-brand h1,
.admin-page-heading h2 {
font-size: 22px;
+3 -1
View File
@@ -13,12 +13,14 @@
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
- [埋点查询](./tracking/README.md):埋点原始事件与聚合投影的本地 SQL 查询。
- [运营查询](./operations/README.md):任务、领奖、钱包对账等后台核查查询。
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md)。
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md)
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
创作 Agent 问答流式失败时保留已显示回复、并透出更具体上游错误的契约见 [CREATION_AGENT_STREAM_FAILURE_RETENTION_FIX_2026-05-05.md](./technical/CREATION_AGENT_STREAM_FAILURE_RETENTION_FIX_2026-05-05.md)。
## 推荐阅读顺序
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
@@ -0,0 +1,20 @@
# AdminTaskConfigPage 埋点范围收口记录(2026-05-04
## 变更目标
- 前端隐藏「埋点范围」选择项。
- 保存任务配置时固定传递 `scopeKind=user`,避免运营在后台选择其他范围。
## 落地结果
- `apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
- 移除了「埋点范围」下拉选择 UI。
- 保存时改为固定提交 `scopeKind: 'user'`
- 回填表单时不再读取或维护 `scopeKind` 的可编辑状态。
## 验证结果
- 已检查页面源码,确认不再存在「埋点范围」字段与可编辑逻辑。
- 已执行 `npm run admin-web:typecheck`
- 当前验证失败原因为环境缺少 `node_modules/typescript/bin/tsc`,属于依赖安装状态问题,不是本次前端改动引入的代码错误。
## 遗留说明
- `apps/admin-web/src/api/adminApiTypes.ts` 中仍保留 `TrackingScopeKind` 类型,供接口契约与其他前端模块兼容使用。
- `apps/admin-web/src/config/trackingEventDefinitions.ts` 仍保留事件定义中的 `scopeKind` 元数据,但页面不再允许运营修改。
@@ -0,0 +1,278 @@
# AI 原生方洞挑战玩法创作工具与玩法系统 PRD
更新时间:`2026-05-04`
## 0. 文档目的
这份 PRD 用于在当前平台内新增一条“方洞挑战”玩法类型,并先冻结它从入口占位到完整可创建闭环的产品边界。
本玩法来自参考视频中的核心反差:玩家看到不同形状的积木与多个洞口,会本能判断“形状应放入对应洞口”,但演示不断把不同形状都放进同一个方洞,形成“规则预期被打破”的喜剧张力。
本次不是简单复制视频内容,也不是只新增一个前端小游戏。正式版本需要把这种反直觉机制抽象成平台内可创作、可试玩、可发布的玩法类型。
---
## 1. 一句话定义
“方洞挑战”是一个反直觉形状分拣玩法:百梦主通过 Agent 设定形状主题、洞口规则、误导节奏和反差演出,系统生成一个移动端优先的单局挑战;玩家在限时内把连续出现的形状投入正确洞口,后端根据当前关卡的真实兼容规则裁决成功、失败和连击。
---
## 2. 当前接入级别
根据 `genarrative-play-type-integration` 的接入分级,本次落地到 **完整玩法闭环**
1. 新增玩法 ID`square-hole`
2. 新增展示名称:`方洞挑战`
3. 新增子标题:`反直觉形状分拣`
4. 在新建作品入口、创作类型弹层、结果页、运行态和作品架展示。
5. 支持 Agent 创作、草稿生成、结果页编辑、试玩、发布和公开运行。
6. 后端以 `server-rs + Axum + SpacetimeDB` 为真相源,前端只渲染快照与交互。
不允许把运行规则临时写成前端本地真相,也不复用 `server-node`、Express 或 PostgreSQL。
---
## 3. 产品定位
## 3.1 模板名称
1. 对外模板名称:`方洞挑战`
2. 对外子标题:`反直觉形状分拣`
3. 开发代号:`SquareHole`
4. 工程玩法域:`square-hole`
5. 后端模块命名预期:`square_hole`
## 3.2 核心乐趣
1. 玩家先根据形状轮廓做直觉判断。
2. 关卡通过洞口、提示、视觉遮挡和演出制造误导。
3. 真实规则可以是“方洞万能”“指定洞口万能”“颜色优先于形状”“本轮只看尺寸”等反直觉规则。
4. 玩家通过连续试探和反馈理解规则,形成短局重复挑战。
## 3.3 与现有玩法的区别
1. 不等同于拼图:不切图、不交换、不合并拼块。
2. 不等同于抓大鹅:不做三消备选栏,不做堆叠遮挡点击。
3. 不等同于大鱼吃小鱼:不做摇杆移动、吞噬、成长等级。
4. 不复用 RPG 的世界、角色、章节或剧情推进结构。
5. 运行态只保留“当前形状 + 洞口选择 + 后端裁决”,不在前端写正式规则真相。
---
## 4. 完整闭环目标
本次完整闭环必须补齐:
1. 平台创作入口选择“方洞挑战”。
2. Agent 对话收集玩法锚点。
3. 生成方洞挑战草稿。
4. 进入结果页编辑作品名、标签、封面、形状数量、反差规则和视觉主题。
5. 支持发布前试玩。
6. 发布作品。
7. 玩家从作品详情或广场进入运行态。
8. 后端初始化单局形状队列、洞口兼容规则和计分状态。
9. 玩家拖拽或点击形状投入洞口。
10. 后端裁决投入结果、连击、扣时、失败、胜利和成绩。
11. 前端只渲染后端快照与即时反馈,不承接正式规则真相。
---
## 5. 创作锚点设计
Agent 型创作版本至少收集下面 5 个锚点:
| 锚点 | 字段建议 | 用途 |
| --- | --- | --- |
| 主题外观 | `themePrompt` | 决定玩具、洞板、背景、形状材质和色彩风格。 |
| 反差规则 | `twistRule` | 决定“为什么不是按形状匹配”的真实判定规则。 |
| 洞口组 | `holeSet` | 决定本局出现的洞口种类、数量、位置和视觉误导强度。 |
| 形状队列 | `shapeSequence` | 决定连续出现的形状、颜色、大小和难度递增。 |
| 反馈节奏 | `feedbackRhythm` | 决定成功、错误、连击、惊讶和结算演出风格。 |
Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问低价值字段。
## 5.1 Agent AI 生成契约
方洞挑战的创作对话必须接入 `api-server` 的现有 LLM 能力,不能把用户输入解析成固定模板后直接写回会话。工程实现以 `state.llm_client()` 为唯一模型入口,通过方洞专属 Agent turn 生成回复与下一轮配置。
单轮模型输出必须是严格 JSON
```json
{
"replyText": "",
"progressPercent": 0,
"nextConfig": {
"themeText": "",
"twistRule": "",
"shapeCount": 12,
"difficulty": 4
}
}
```
落地约束:
1. `replyText` 是直接展示给百梦主的中文回复,不得出现 JSON、字段名、内部结构等说明。
2. `nextConfig` 必须是完整配置,不允许只输出 patch;缺失字段只能由后端用当前会话配置兜底。
3. `shapeCount` 由后端限制在 `6``24``difficulty` 限制在 `1``10`
4. `quickFillRequested=true` 时,模型应直接补齐剩余配置,后端把 `progressPercent` 固定为 `100`
5. 模型不可用或结果无法解析时,接口返回明确错误,不允许用确定性模板伪装成 AI 回复。
6. 非流式消息接口和 SSE 流式消息接口都必须走同一套方洞 Agent turn,SSE 只额外负责把 `replyText` 增量回传。
---
## 6. 运行规则设计
## 6.1 单局结构
1. 单局默认 `60` 秒。
2. 每局默认 `12` 个形状。
3. 洞口数量默认 `4``6` 个。
4. 玩家每次只能操作当前形状。
5. 正确投入后进入下一个形状。
6. 错误投入扣除时间或清空连击。
7. 全部形状完成即胜利。
8. 时间归零即失败。
## 6.2 真实兼容规则
首版可支持下面几类规则:
1. `shape_match`:形状轮廓匹配。
2. `square_hole_priority`:方洞兼容所有形状,其他洞口只作为误导。
3. `color_match`:颜色优先于形状。
4. `size_match`:尺寸优先于形状。
5. `round_prompt`:本轮按后端给出的短提示规则判定。
其中 `square_hole_priority` 是参考视频核心反差的首选默认规则。
## 6.3 前端表现
1. 竖屏优先,桌面端居中显示游戏台。
2. 当前形状位于屏幕下半区域,洞板位于上半区域。
3. 只显示必要状态:剩余时间、连击、当前进度。
4. 不默认展示长篇规则说明。
5. 错误反馈用短促动画、颜色闪烁和轻量文字状态,不堆解释。
6. 点击按钮弹出的配置或结算必须使用独立面板,不在当前面板下方展开。
---
## 7. 后端分层边界
完整实现时必须遵循当前 `server-rs + Axum + SpacetimeDB` 路线:
1. `server-rs/crates/module-square-hole`
- 纯领域规则、形状队列生成、兼容性裁决、分数计算。
- 不依赖 Axum、SpacetimeDB、OSS 或 LLM。
2. `server-rs/crates/shared-contracts`
- 暴露 Agent、作品、运行态 DTO。
3. `server-rs/crates/spacetime-module`
- 存储 session、message、work profile、runtime run。
- 表结构变化必须同步 `migration.rs` 与表目录。
4. `server-rs/crates/spacetime-client`
- 提供 api-server 调用 SpacetimeDB 的 typed facade。
5. `server-rs/crates/api-server`
- 暴露 `/api/creation/square-hole/*``/api/runtime/square-hole/*`
- 处理鉴权、错误 envelope、LLM turn 和 HTTP facade。
---
## 8. 数据模型
## 8.1 创作 session
1. `sessionId`
2. `ownerUserId`
3. `currentTurn`
4. `progressPercent`
5. `stage`
6. `config`
7. `draft`
8. `messages`
9. `lastAssistantReply`
10. `publishedProfileId`
## 8.2 结果页 work profile
1. `workId`
2. `profileId`
3. `ownerUserId`
4. `sourceSessionId`
5. `gameName`
6. `themeText`
7. `twistRule`
8. `summary`
9. `tags`
10. `coverImageSrc`
11. `shapeCount`
12. `difficulty`
13. `publicationStatus`
14. `playCount`
15. `updatedAt`
16. `publishedAt`
## 8.3 运行态 run snapshot
1. `runId`
2. `profileId`
3. `ownerUserId`
4. `status`
5. `snapshotVersion`
6. `startedAtMs`
7. `durationLimitMs`
8. `remainingMs`
9. `totalShapeCount`
10. `completedShapeCount`
11. `combo`
12. `bestCombo`
13. `score`
14. `ruleLabel`
15. `currentShape`
16. `holes`
17. `lastFeedback`
---
## 9. API 设计
## 9.1 创作接口
1. `POST /api/creation/square-hole/sessions`
2. `GET /api/creation/square-hole/sessions/{sessionId}`
3. `POST /api/creation/square-hole/sessions/{sessionId}/messages`
4. `POST /api/creation/square-hole/sessions/{sessionId}/messages/stream`
5. `POST /api/creation/square-hole/sessions/{sessionId}/actions`
6. `POST /api/creation/square-hole/sessions/{sessionId}/compile`
7. `GET /api/creation/square-hole/works`
8. `GET /api/creation/square-hole/works/{profileId}`
9. `PUT /api/creation/square-hole/works/{profileId}`
10. `POST /api/creation/square-hole/works/{profileId}/publish`
11. `DELETE /api/creation/square-hole/works/{profileId}`
## 9.2 运行接口
1. `GET /api/runtime/square-hole/gallery`
2. `GET /api/runtime/square-hole/gallery/{profileId}`
3. `POST /api/runtime/square-hole/works/{profileId}/runs`
4. `GET /api/runtime/square-hole/runs/{runId}`
5. `POST /api/runtime/square-hole/runs/{runId}/drop`
6. `POST /api/runtime/square-hole/runs/{runId}/stop`
7. `POST /api/runtime/square-hole/runs/{runId}/restart`
8. `POST /api/runtime/square-hole/runs/{runId}/time-up`
---
## 10. 验收标准
1. `src/config/newWorkEntryConfig.ts` 中存在 `square-hole` 类型且开放创建。
2. 新建作品入口和创作类型弹层能展示“方洞挑战”。
3. 能进入 `square-hole` Agent 工作台。
4. 能生成草稿并进入结果页。
5. 能编辑结果页并保存、发布。
6. 能从作品详情或广场进入运行态。
7. 能点击或拖拽当前形状投入洞口。
8. 后端裁决命中规则、连击、失败和胜利。
9. 刷新后可恢复作品与运行态快照。
10. `docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md` 记录该入口开放状态。
11. 后端改动完成后必须执行 `npm run api-server:maincloud`,以 `GET /healthz` 返回 `200` 作为主云配置启动 smoke 通过标准,并在 smoke 后清理本次启动进程。
@@ -192,6 +192,8 @@ export interface AdminDisableProfileRedeemCodeRequest {
export interface AdminUpsertProfileInviteCodeRequest {
inviteCode: string;
startsAt?: string | null;
expiresAt?: string | null;
metadata?: Record<string, unknown>;
}
@@ -215,6 +217,9 @@ export interface ProfileRedeemCodeAdminListResponse {
export interface ProfileInviteCodeAdminResponse {
userId: string;
inviteCode: string;
startsAt: string | null;
expiresAt: string | null;
status: 'pending' | 'active' | 'expired';
metadata: Record<string, unknown>;
createdAt: string;
updatedAt: string;
@@ -376,6 +381,9 @@ export interface ProfileInviteCodeAdminListResponse {
{
"userId": "admin:root:SPRING2026",
"inviteCode": "SPRING2026",
"startsAt": "2026-05-01T00:00:00Z",
"expiresAt": "2026-06-01T00:00:00Z",
"status": "active",
"metadata": {
"batch": "spring"
},
@@ -393,6 +401,8 @@ export interface ProfileInviteCodeAdminListResponse {
```json
{
"inviteCode": "SPRING2026",
"startsAt": "2026-05-01T00:00:00Z",
"expiresAt": "2026-06-01T00:00:00Z",
"metadata": {
"batch": "spring"
}
@@ -405,6 +415,9 @@ export interface ProfileInviteCodeAdminListResponse {
{
"userId": "admin",
"inviteCode": "SPRING2026",
"startsAt": "2026-05-01T00:00:00Z",
"expiresAt": "2026-06-01T00:00:00Z",
"status": "active",
"metadata": {
"batch": "spring"
},
@@ -415,6 +428,118 @@ export interface ProfileInviteCodeAdminListResponse {
邀请码页的 metadata 输入必须先在前端解析为 JSON 对象;空字符串按 `{}` 处理,数组、字符串、数字等非对象值直接提示错误。最终标准化、长度限制和邀请码合法性以 `server-rs` 为准。
#### 4.8.1 邀请码有效期语义
邀请码仍然是“用户稳定邀请身份码”,不做删除或软删除。本轮只增加时间窗字段,用于控制**新填写邀请码**是否允许绑定:
1. `startsAt` / 后端 `starts_at`:邀请码开始生效时间;为空表示立即生效。
2. `expiresAt` / 后端 `expires_at`:邀请码截止时间;为空表示长期有效。
3. 两个字段都为空时,邀请码视为长期有效。
4. `expiresAt` 采用左闭右开语义:当前时间 `>= expiresAt` 时视为已过期。
5. 时间字段在管理 API JSON 中统一使用 ISO 8601 UTC 字符串或 `null`SpacetimeDB 内部仍按 `Timestamp` 存储,契约层负责转换,前端不得自行假设微秒/毫秒整数。
6. 有效期只影响用户之后调用填写邀请码接口建立新邀请关系;已绑定的邀请关系、历史奖励、统计和审计记录不回溯修改。
字段合法性要求:
1. `startsAt``expiresAt` 均允许为空。
2. 若两者都存在,必须满足 `startsAt < expiresAt`;相等或开始晚于截止应由后端拒绝,前端可提前提示但不能替代后端校验。
3. 后台编辑已有邀请码时,空值代表清空该边界;不要用空字符串写入契约。
#### 4.8.2 用户填写邀请码的错误优先级与校验逻辑
填写邀请码时,后端是唯一业务真相。前端只展示后端错误,不复制完整业务规则。推荐校验优先级如下:
1. **请求身份与输入基础校验**:未登录、空邀请码、格式不合法等请求级错误优先返回。
2. **用户自身状态校验**:用户不存在、用户资料不可用、已绑定过邀请关系等与当前用户直接相关的错误优先于邀请码时间窗。
3. **邀请码查找**:按标准化后的邀请码查找记录;不存在时返回“邀请码不存在或不可用”。
4. **自邀请校验**:邀请码归属用户等于当前用户时,返回“不能填写自己的邀请码”。
5. **时间窗校验**
- `starts_at` 存在且当前时间 `< starts_at`,返回“邀请码未生效”。
- `expires_at` 存在且当前时间 `>= expires_at`,返回“邀请码已过期”。
6. **绑定写入与奖励发放**:只有以上校验全部通过,才写入邀请绑定、奖励或相关流水。
该顺序的目标是避免用“未生效/已过期”泄露不该暴露的用户状态,同时保证用户看到的错误与实际阻断原因一致。若后续新增风控、封禁、黑名单等规则,应在写入前补入,并在本节同步明确优先级。
#### 4.8.3 后台邀请码列表状态展示规则
后台列表状态可由后端返回 `status`,也可在前端用同一规则从 `startsAt` / `expiresAt` 派生;如果两者同时存在,列表展示以后端 `status` 为准,并仅把前端派生结果用于兜底。
| 条件 | 状态值 | 中文标签 | 展示建议 |
| --- | --- | --- | --- |
| `startsAt` 存在且当前时间 `< startsAt` | `pending` | 未生效 | 展示开始时间,提示尚不能被新用户填写 |
| `expiresAt` 存在且当前时间 `>= expiresAt` | `expired` | 已过期 | 展示截止时间,提示不再允许新绑定 |
| 其他情况 | `active` | 有效 | 正常高亮展示 |
补充展示规则:
1. 两个字段都为空时状态为 `active`,中文可展示为“长期有效”。
2. `startsAt` 为空、`expiresAt` 未来存在时状态为 `active`,中文可展示为“有效至 YYYY-MM-DD HH:mm”。
3. `startsAt` 未来、`expiresAt` 为空时状态为 `pending`,中文可展示为“YYYY-MM-DD HH:mm 生效”。
4. 列表至少展示邀请码、状态、开始时间、截止时间、更新时间;metadata 可保留折叠/摘要展示,避免挤占移动端宽度。
5. 列表状态只用于运营理解,不作为安全边界;真正是否可填写仍以后端 redeem 校验为准。
### 4.9 后台写操作二次确认规范
后台所有会修改线上数据的操作,在真正调用 API 前必须二次确认;取消确认时不得发送任何请求。该规范覆盖当前和未来新增的管理写入口,不限于 profile 模块。
必须二次确认的操作包括但不限于:
1. 创建/更新兑换码:`POST /admin/api/profile/redeem-codes`
2. 停用兑换码:`POST /admin/api/profile/redeem-codes/disable`
3. 创建/更新邀请码:`POST /admin/api/profile/invite-codes`
4. 创建/更新个人任务配置:`POST /admin/api/profile/tasks`
5. 停用个人任务配置:`POST /admin/api/profile/tasks/disable`
6. 后续任何 `POST` / `PATCH` / `PUT` / `DELETE` 管理接口,只要会修改数据、触发任务、写审计或影响线上配置,均默认纳入确认。
交互要求:
1. 确认弹窗必须在 API 调用前出现,确认后才进入 loading 和提交状态。
2. 弹窗必须展示操作类型(新增、更新、停用、删除、发布等)、对象标识(如 `code``inviteCode``taskId`)和影响说明。
3. 默认按钮顺序为“取消 / 确认”,取消不应有危险色;危险操作(停用、删除、覆盖线上配置)确认按钮使用警示样式。
4. 弹窗文案统一提示“该操作会立即影响线上数据”,但不要在页面常驻展示大段规则说明。
5. 支持键盘和移动端:Esc 或取消按钮关闭;移动端弹窗宽度自适应,不遮挡关键对象信息。
6. loading 期间锁定确认按钮和原页面提交按钮,避免重复写入。
7. 成功后按现有页面规则刷新列表或合并返回记录;失败时展示后端错误,不能静默关闭为成功。
建议抽象通用确认能力,例如 `confirmAdminWriteAction({ actionLabel, targetLabel, riskLevel, onConfirm })` 或通用 `AdminConfirmDialog`,页面只传入对象与回调,避免每个页面重复实现不同交互。
#### 4.9.1 二次确认文案模板
```text
标题:确认{操作类型}{对象类型}
正文:即将{操作类型}「{对象标识}」。该操作会立即影响线上数据。
取消按钮:取消
确认按钮:确认{操作类型}
```
示例:
1. `确认更新邀请码`:即将更新「SPRING2026」的有效期与 metadata。该操作会立即影响线上数据。
2. `确认停用兑换码`:即将停用「WELCOME2026」。该操作会立即影响线上数据。
3. `确认更新任务配置`:即将更新「daily_login」。该操作会立即影响线上数据。
### 4.10 邀请码有效期与二次确认改动范围
实现本设计时预期改动范围如下,未列出的层级不要擅自承接业务规则:
1. `server-rs/crates/spacetime-module/src/runtime/profile.rs`:邀请码表结构、upsert、redeem 时间窗校验与后台列表投影。
2. `server-rs/crates/spacetime-module/src/migration.rs`:旧邀请码记录迁移,默认 `starts_at = None``expires_at = None`
3. `server-rs/crates/shared-contracts/src/**`:管理请求/响应 DTO 增加 `startsAt``expiresAt``status` 等字段。
4. `server-rs/crates/spacetime-client/src/module_bindings/**` 与 mapper:按表结构变更重新生成/补齐绑定字段。
5. `server-rs/crates/api-server/src/runtime_profile.rs`:接收、校验、转发并返回邀请码时间窗字段;保持错误 envelope 兼容后台读取逻辑。
6. `apps/admin-web/src/api/adminApiTypes.ts``adminApiClient.ts`:同步契约字段,不在 client 层写业务判断。
7. `apps/admin-web/src/pages/AdminInviteCodePage.tsx`:有效期表单、列表状态展示、保存前确认。
8. `apps/admin-web/src/pages/AdminRedeemCodePage.tsx``AdminTaskConfigPage.tsx` 及后续写页面:统一接入写操作二次确认。
9. `apps/admin-web/src/styles/admin.css`:状态标签、确认弹窗与移动端样式。
验证建议:
1. 服务端单测覆盖:未生效邀请码拒绝、已过期邀请码拒绝、有效时间窗可绑定、空时间窗长期有效、已绑定关系不受后续过期影响。
2. 管理 API 覆盖:upsert 能写入/清空 `startsAt``expiresAt`;列表返回状态正确;`startsAt >= expiresAt` 被拒绝。
3. 前端交互覆盖:点击保存/停用不会直接请求,取消确认不请求,确认后只请求一次,失败展示后端错误。
4. 回归兑换码与任务配置页面,确认所有写操作均有统一二次确认。
5. 修改后端时按项目约束运行对应 Rust 测试、`npm run api-server` 联调和 `/healthz`;修改前端时运行 `npm run admin-web:typecheck``npm run admin-web:build`;文档或中文改动后运行 `npm run check:encoding`
## 5. 鉴权与会话
1. token key 固定为 `genarrative_admin_token`

Some files were not shown because too many files have changed in this diff Show More