diff --git a/.codex/skills/genarrative-gameplay-entry-type/agents/openai.yaml b/.codex/skills/genarrative-gameplay-entry-type/agents/openai.yaml deleted file mode 100644 index 1fb1aa12..00000000 --- a/.codex/skills/genarrative-gameplay-entry-type/agents/openai.yaml +++ /dev/null @@ -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." diff --git a/.codex/skills/genarrative-play-type-integration/SKILL.md b/.codex/skills/genarrative-play-type-integration/SKILL.md new file mode 100644 index 00000000..d4bcb86a --- /dev/null +++ b/.codex/skills/genarrative-play-type-integration/SKILL.md @@ -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` +- 能关闭类型弹层 + +同时按玩法补齐: + +- `openAgentWorkspace()` +- `leaveFlow()` +- `submitMessage()`(对话式玩法) +- `executeAction()` + +### 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-`:纯领域规则、状态机、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/-creation/AgentWorkspace.tsx` + +两种形态: + +#### 对话式 + +适合设定逐轮补齐。 + +参考: + +- `BigFishAgentWorkspace.tsx` +- `Match3DAgentWorkspace.tsx` + +#### 表单式 + +适合输入结构明确的玩法。 + +参考: + +- `PuzzleAgentWorkspace.tsx` + +### Step 11: 在渲染树中挂载新页面 + +文件: + +- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + +补齐: + +- workspace 分支 +- generating 分支(如需要) +- result 分支 +- runtime 分支(如需要) + +### Step 12: 新增结果页 + +目录建议: + +- `src/components/-result/ResultView.tsx` + +结果页至少支持: + +- 展示 draft +- 返回编辑 +- 发布 +- 试玩 +- 错误展示 + +### Step 13: 需要试玩就补 runtime + +目录建议: + +- `src/components/-runtime/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/单元测试。 diff --git a/.hermes/README.md b/.hermes/README.md new file mode 100644 index 00000000..f88c5076 --- /dev/null +++ b/.hermes/README.md @@ -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 diff --git a/.hermes/plans/2026-05-04_022223-analytics-time-dimension-mapping.md b/.hermes/plans/2026-05-04_022223-analytics-time-dimension-mapping.md new file mode 100644 index 00000000..cbd0c8fb --- /dev/null +++ b/.hermes/plans/2026-05-04_022223-analytics-time-dimension-mapping.md @@ -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` 或 `Option`。 +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 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。 diff --git a/.hermes/plans/2026-05-04_032321-invite-code-validity-and-admin-confirmation.md b/.hermes/plans/2026-05-04_032321-invite-code-validity-and-admin-confirmation.md new file mode 100644 index 00000000..4a72ef2a --- /dev/null +++ b/.hermes/plans/2026-05-04_032321-invite-code-validity-and-admin-confirmation.md @@ -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` +- `expires_at: Option` + +校验规则: +- 当前时间 `< 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 的二次确认与表单展示。 + +--- + +**结论:** 这是一个适合分阶段落地的改动,建议先做“邀请码时间窗 + 后台统一二次确认”的基础能力,再补交互细节。 diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md new file mode 100644 index 00000000..3eea40a8 --- /dev/null +++ b/.hermes/shared-memory/decision-log.md @@ -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`。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md new file mode 100644 index 00000000..36de9840 --- /dev/null +++ b/.hermes/shared-memory/development-workflow.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. 建议的测试命令和提交信息。 +``` diff --git a/.hermes/shared-memory/document-map.md b/.hermes/shared-memory/document-map.md new file mode 100644 index 00000000..409eee36 --- /dev/null +++ b/.hermes/shared-memory/document-map.md @@ -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/` 只保留跨任务、跨成员、高频使用的摘要和索引。 +- 如果文档与代码冲突,先确认代码事实,再更新过期文档和共享记忆。 diff --git a/.hermes/shared-memory/handoff-template.md b/.hermes/shared-memory/handoff-template.md new file mode 100644 index 00000000..29c17505 --- /dev/null +++ b/.hermes/shared-memory/handoff-template.md @@ -0,0 +1,53 @@ +# 任务交接模板 + +> 用途:当一名开发者把任务交给另一名开发者,或让 Hermes 接续上下文时,复制本模板并填写。 + +## 基本信息 + +- 任务名称: +- 负责人: +- 当前分支: +- 相关需求/Issue: +- 相关文档: + +## 背景 + +简要说明为什么做这个任务,以及业务/技术目标。 + +## 已完成 + +- [ ] +- [ ] +- [ ] + +## 未完成 + +- [ ] +- [ ] +- [ ] + +## 关键文件 + +- `path/to/file`:说明 +- `path/to/file`:说明 + +## 当前问题/风险 + +- + +## 已执行验证 + +```bash +# 粘贴已执行命令和结果摘要 +``` + +## 建议下一步 + +1. +2. +3. + +## 是否需要更新团队记忆 + +- [ ] 不需要 +- [ ] 需要,建议更新:`.hermes/shared-memory/...` diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md new file mode 100644 index 00000000..aaf655cd --- /dev/null +++ b/.hermes/shared-memory/pitfalls.md @@ -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`。 diff --git a/.hermes/shared-memory/project-overview.md b/.hermes/shared-memory/project-overview.md new file mode 100644 index 00000000..5918d97b --- /dev/null +++ b/.hermes/shared-memory/project-overview.md @@ -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` diff --git a/.hermes/shared-memory/team-conventions.md b/.hermes/shared-memory/team-conventions.md new file mode 100644 index 00000000..5815cb93 --- /dev/null +++ b/.hermes/shared-memory/team-conventions.md @@ -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` + +## 共享记忆更新准则 + +适合更新: + +- 新增稳定架构约定 +- 新增长期开发流程 +- 已验证的踩坑和排障步骤 +- 重要接口契约变化 +- 团队协作规范变化 +- 文档索引或阅读顺序变化 + +不适合更新: + +- 一次性临时计划 +- 未验证猜测 +- 个人偏好和个人路径 +- 敏感信息 +- 大段聊天记录 diff --git a/.hermes/skills/README.md b/.hermes/skills/README.md new file mode 100644 index 00000000..3464aa0e --- /dev/null +++ b/.hermes/skills/README.md @@ -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 应包含触发条件、步骤、坑点和验证方式。 diff --git a/AGENTS.md b/AGENTS.md index babe33e5..f6bf6e0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/` 为准,并同步修正过期共享记忆。 + ## 项目约束 - 代码需要有完善的中文注释 - 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。 diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index cd7bc74c..60be224b 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -122,6 +122,8 @@ export interface AdminUpsertProfileRedeemCodeRequest { export interface AdminUpsertProfileInviteCodeRequest { inviteCode: string; metadata?: Record; + startsAt?: string | null; + expiresAt?: string | null; } export interface AdminDisableProfileRedeemCodeRequest { @@ -166,6 +168,9 @@ export interface ProfileInviteCodeAdminResponse { userId: string; inviteCode: string; metadata: Record; + startsAt?: string | null; + expiresAt?: string | null; + status: 'pending' | 'active' | 'expired'; createdAt: string; updatedAt: string; } diff --git a/apps/admin-web/src/components/useAdminWriteConfirm.tsx b/apps/admin-web/src/components/useAdminWriteConfirm.tsx new file mode 100644 index 00000000..6f62abfe --- /dev/null +++ b/apps/admin-web/src/components/useAdminWriteConfirm.tsx @@ -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(null); + const cancelButtonRef = useRef(null); + + const confirmWrite = useCallback((options: AdminWriteConfirmOptions) => { + return new Promise((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 ? ( +
{ + if (event.target === event.currentTarget) { + closeConfirm(false); + } + }} + > +
+
+

确认操作

+ {pendingConfirm.action} +
+
+
+
操作
+
{pendingConfirm.action}
+
+
+
对象
+
{pendingConfirm.target}
+
+
+
该操作会立即影响线上数据
+
+ + +
+
+
+ ) : null; + + return {confirmWrite, confirmDialog}; +} diff --git a/apps/admin-web/src/pages/AdminDebugHttpPage.tsx b/apps/admin-web/src/pages/AdminDebugHttpPage.tsx index a5ac8ad1..1dd48c54 100644 --- a/apps/admin-web/src/pages/AdminDebugHttpPage.tsx +++ b/apps/admin-web/src/pages/AdminDebugHttpPage.tsx @@ -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(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({ )} + {confirmDialog} ); } diff --git a/apps/admin-web/src/pages/AdminInviteCodePage.tsx b/apps/admin-web/src/pages/AdminInviteCodePage.tsx index a507ef4d..dfbf119a 100644 --- a/apps/admin-web/src/pages/AdminInviteCodePage.tsx +++ b/apps/admin-web/src/pages/AdminInviteCodePage.tsx @@ -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([]); 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 (
@@ -127,6 +155,25 @@ export function AdminInviteCodePage({ /> +
+ + +
+