From 7e35231dfec2f4bfd1846079c1f6c2e239454e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=86=E5=86=B0=E9=83=81-hermes=E7=89=88?= Date: Fri, 8 May 2026 17:39:49 +0800 Subject: [PATCH] docs: sync genarrative shared skills --- .../genarrative-admin-backoffice/SKILL.md | 8 +- ...admin-tracking-events-export-2026-05-07.md | 4 +- .../dev-rust-stack-startup-2026-05-08.md | 34 ++ .../genarrative-auth-session-flow/SKILL.md | 132 +++++++ ...restore-daily-login-tracking-2026-05-08.md | 75 ++++ .../SKILL.md | 348 ++++++++++++++++++ .../genarrative-analytics-tracking-runtime.md | 57 +++ .../genarrative-profile-features/SKILL.md | 99 +++++ .../profile-feedback-entry-2026-05-08.md | 80 ++++ .../genarrative-profile-invite-flow/SKILL.md | 149 ++++++++ .../query-invite-code-flow-2026-05-07.md | 101 +++++ 11 files changed, 1082 insertions(+), 5 deletions(-) create mode 100644 .hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md create mode 100644 .hermes/skills/genarrative-auth-session-flow/SKILL.md create mode 100644 .hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md create mode 100644 .hermes/skills/genarrative-play-type-integration/SKILL.md create mode 100644 .hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md create mode 100644 .hermes/skills/genarrative-profile-features/SKILL.md create mode 100644 .hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md create mode 100644 .hermes/skills/genarrative-profile-invite-flow/SKILL.md create mode 100644 .hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md diff --git a/.hermes/skills/genarrative-admin-backoffice/SKILL.md b/.hermes/skills/genarrative-admin-backoffice/SKILL.md index 86881118..c710112a 100644 --- a/.hermes/skills/genarrative-admin-backoffice/SKILL.md +++ b/.hermes/skills/genarrative-admin-backoffice/SKILL.md @@ -217,9 +217,10 @@ npm install 3. `cargo fmt --all` 可能格式化不相关 Rust 文件;提交前用 `git status` 检查并 revert 非本任务文件。 4. patch 工具对 Rust 单文件 lint 可能用 Rust 2015 edition 误报 `async fn is not permitted in Rust 2015`;以 `cargo test/check` 为准。 5. `adminRoutes` 新增 route id 后,`AdminShell.routeIcons` 必须同步,否则 TypeScript 会因 `satisfies Record` 报错。 -6. 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`。 -7. 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。 -8. 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`。 +- 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`。 +- 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。 +- 若用户追问“之前不是说要把 npm run dev 修好吗”这类已承诺的 dev 启动问题,不要只解释;先复现 `npm run dev`,再按启动日志修脚本并验证到服务就绪。WSL/Linux 下 `spacetime start --root-dir=server-rs/.spacetimedb/local` 可能需要把用户级 SpacetimeDB 版本目录同步到项目 root-dir 的 `bin/` 并建立 `bin/current`,详见 `references/dev-rust-stack-startup-2026-05-08.md`。 +- 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`。 ## 参考资料 @@ -229,3 +230,4 @@ npm install - `references/spacetimedb-http-sql-sats-display.md`:通过 HTTP SQL 读取 private table 时,enum / Option / Timestamp 的 SATS 原始 rows 如何转换为后台列表、详情和 Excel 可读值。 - `references/daily-login-tracking-trigger-points.md`:排查后台 `daily_login` 埋点为何不是登录接口写入,而是任务中心读取/领奖兜底写入的触发点记录。 - `references/daily-login-auth-closure.md`:将方案A拆出的每日登录埋点入口接入真实认证成功链路时的推荐接入点、非阻断语义、测试和提交注意事项。 +- `references/dev-rust-stack-startup-2026-05-08.md`:`npm run dev` / `scripts/dev-rust-stack.sh` 在 WSL/Linux 下同步 SpacetimeDB root-dir 安装、避免 `bin/current/spacetimedb-cli` 缺失和冷编译超时的修复记录。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md b/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md index ec986e6b..9c181569 100644 --- a/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md +++ b/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md @@ -23,7 +23,7 @@ ## 验证命令 ```bash -cd /.worktrees/hermes-996d586b +cd npm install # 若 node_modules 缺失 npm run admin-web:typecheck npm run admin-web:build @@ -39,7 +39,7 @@ cargo test -p api-server admin_tracking -- --nocapture 启动命令: ```bash -cd /.worktrees/hermes-996d586b +cd npm run api-server npm run admin-web:dev -- --host 127.0.0.1 ``` diff --git a/.hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md b/.hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md new file mode 100644 index 00000000..aa78daa7 --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md @@ -0,0 +1,34 @@ +# `npm run dev` / `scripts/dev-rust-stack.sh` 启动修复记录 + +## 症状 +- `npm run dev` 在 WSL/Linux 下直接失败: + - `It seems like the spacetime version set as current may not exist` + - `exec failed for .../.spacetimedb/local/bin/current/spacetimedb-cli` +- 失败位置通常在 `sync_local_spacetime_install` 后、等待 SpacetimeDB 就绪阶段。 + +## 根因 +- `server-rs/.spacetimedb/local` 是空 root-dir 时,`spacetime start` 仍会尝试回调 `bin/current/spacetimedb-cli`。 +- 旧脚本只按 Windows/Git Bash 思路同步 `spacetimedb-cli.exe`,WSL/Linux 下没有把用户级安装同步到项目 root-dir。 +- `api-server` 首次冷编译时,默认 300 秒超时不够,容易在就绪前被回收。 + +## 修复要点 +1. 同步本机 SpacetimeDB 安装到项目 root-dir + - 从 `spacetime --version` 解析真实 CLI 路径。 + - 将对应版本目录复制到 `server-rs/.spacetimedb/local/bin/`。 + - 重新建立 `bin/current` 指向版本目录。 +2. 兼容 WSL/Linux 与 Windows + - 不再只判断 `OSTYPE=msys*|cygwin*`。 + - 同时检查 `spacetimedb-cli` 与 `spacetimedb-cli.exe`。 +3. 提高 api-server 就绪等待时间 + - `API_SERVER_TIMEOUT_SECONDS` 从 300 提升到 600。 + +## 复现 / 验证 +- 运行 `npm run dev`。 +- 观察日志: + - SpacetimeDB 能正常启动到 `Listening on 127.0.0.1:3101` + - 模块发布成功 + - api-server 进入健康检查等待并最终可访问 `/healthz` + +## 相关文件 +- `scripts/dev-rust-stack.sh` +- `server-rs/.spacetimedb/local/` diff --git a/.hermes/skills/genarrative-auth-session-flow/SKILL.md b/.hermes/skills/genarrative-auth-session-flow/SKILL.md new file mode 100644 index 00000000..e9a82629 --- /dev/null +++ b/.hermes/skills/genarrative-auth-session-flow/SKILL.md @@ -0,0 +1,132 @@ +--- +name: genarrative-auth-session-flow +description: 在 Genarrative 中排查或修改登录、access token、refresh cookie、AuthGate 会话恢复、登录态刷新、认证埋点链路时使用。 +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [Genarrative, auth, session, cookie, refresh-token, AuthGate, tracking] + related_skills: [systematic-debugging, test-driven-development, genarrative-profile-features] +--- + +# Genarrative 认证会话与登录埋点链路 + +用于 Genarrative 中登录、会话恢复、refresh cookie 续期、access token 补票、AuthGate 恢复登录态,以及每日登录/认证相关埋点的排查与修改。 + +## 适用场景 + +- 用户反馈登录态、cookie、自动续期、刷新页面后状态异常。 +- 修改 `AuthGate`、`apiClient`、`authService` 或 Rust `api-server` 认证接口。 +- 排查“已登录但打开网页没有触发登录埋点”等 session restore 场景。 +- 修改手机验证码登录、密码登录、微信登录、重置密码后自动登录、refresh session rotate。 +- 需要判断某个前端动作是否真正调用了后端 refresh/session 或埋点 procedure。 + +## 关键代码路径 + +前端: + +- `src/components/auth/AuthGate.tsx` + - 登录态 hydrate / restore 的入口。 + - 监听 `AUTH_STATE_EVENT` 后重新 hydrate。 + - 是否先 refresh、再 `/api/auth/me`,决定打开页面是否进入后端 refresh 链路。 +- `src/services/apiClient.ts` + - access token 本地保存、`ensureStoredAccessToken()`、`refreshStoredAccessToken()`、`fetchWithApiAuth()`。 + - `ensureStoredAccessToken()` 有 token 时会直接复用,不一定触发后端 refresh。 + - `refreshStoredAccessToken()` 应直接调用 refresh 接口,用于必须轮换 cookie / 写续期埋点的场景。 +- `src/services/authService.ts` + - `getCurrentAuthUser()` 请求 `/api/auth/me`。 + - 登录、登出、账号安全相关 API client。 + +后端: + +- `server-rs/crates/api-server/src/auth_session.rs` + - 创建 refresh cookie / access token。 + - `record_daily_login_tracking_event_after_auth_success(...)` 统一写每日登录埋点;失败 warning,不阻断认证流程。 +- `server-rs/crates/api-server/src/refresh_session.rs` + - `POST /api/auth/session/refresh`。 + - rotate refresh session、签发新 access token、记录每日登录埋点。 +- `server-rs/crates/api-server/src/auth_me.rs` + - `/api/auth/me` 只读取当前 access token 对应用户,不应假设它会触发 refresh 或登录埋点。 +- `server-rs/crates/api-server/src/phone_auth.rs` +- `server-rs/crates/api-server/src/password_entry.rs` +- `server-rs/crates/api-server/src/password_management.rs` +- `server-rs/crates/api-server/src/wechat_auth.rs` + - 各真实认证成功入口。 +- `server-rs/crates/spacetime-client/src/runtime.rs` + - `record_daily_login_tracking_event(user_id)` 调用 SpacetimeDB procedure。 +- `server-rs/crates/spacetime-module/src/runtime/profile.rs` + - `record_daily_login_tracking_event_and_return` procedure。 + - 任务中心读取不应污染每日登录埋点;如看到 `get_profile_task_center` 顺手写 `daily_login`,优先复核是否回归。 + +## 调试顺序 + +1. 先明确用户场景属于哪类: + - 新登录成功。 + - cookie/access token 已过期后的自动刷新。 + - 已登录且 cookie/access token 未过期时打开网页。 + - 只调用 `/api/auth/me` 或某个受保护业务接口。 +2. 查前端实际调用链,不要只看后端埋点点位: + - `AuthGate` hydrate 是否调用 `refreshStoredAccessToken()`? + - 是否只是 `ensureStoredAccessToken()` + `/api/auth/me`? + - `fetchWithApiAuth()` 是否因为已有 access token 而跳过 refresh? +3. 查后端实际埋点点位: + - 登录成功入口是否在 session 创建后调用 helper。 + - refresh session 是否在 rotate 与 access token 签发成功后调用 helper。 + - 失败策略是否只 warning、不阻断响应。 +4. 如涉及 SpacetimeDB procedure/table/binding,按项目 SpacetimeDB skills 与文档同步检查绑定生成、`migration.rs`、private table 限制。 +5. 修改前补齐 `docs/technical/` 中对应方案/根因;修改后同步更新。 + +## 关键经验:已登录打开网页也要主动 refresh 才能写登录埋点 + +常见误判:后端已经在 refresh cookie 续期时写每日登录埋点,就以为“打开网页”会触发埋点。 + +实际链路中,如果用户已经登录且本地 access token 还有效: + +1. `ensureStoredAccessToken()` 会直接返回已有 token。 +2. `AuthGate` 随后请求 `/api/auth/me`。 +3. `/api/auth/me` 只校验/读取用户,不会 rotate refresh session。 +4. 因此后端 refresh/session 埋点不会触发。 + +若产品要求“已登录且 cookie 没过期时打开网页也记录登录埋点”,`AuthGate` 的 restore/hydrate 应主动调用 `refreshStoredAccessToken()`,再调用 `getCurrentAuthUser()`。 + +## 每日登录埋点原则 + +- 真实登录成功:在 refresh session / access token 创建成功后记录。 +- cookie refresh 续期:在 rotate refresh session 成功且新 access token 签发成功后记录。 +- 已登录打开网页:前端必须主动走 refresh 续期链路,不能只请求 `/api/auth/me`。 +- `login_method` 对于 refresh 场景使用 refresh session 保存的 `issued_by_provider`。 +- 埋点失败不阻断登录、续期、会话恢复或 token 返回,只记录 warning。 +- 任务中心读取不应作为登录埋点来源,避免后台查看/刷新任务中心污染登录数据。 + +## 测试与验证命令 + +按改动范围选择: + +```bash +npm run test -- AuthGate.test.tsx +npm run typecheck +cd server-rs && cargo test -p api-server auth_session -- --nocapture +cd server-rs && cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture +cd server-rs && cargo check -p api-server +cd server-rs && cargo check -p spacetime-client +cd server-rs && cargo check -p spacetime-module +npm run check:encoding +git diff --check +``` + +注意:Vitest 0.34 不支持 Jest 的 `--runInBand`;不要把 `--runInBand` 加到 `npm run test -- AuthGate.test.tsx` 后面。 + +## 常见坑 + +1. 把 `/api/auth/me` 当作 refresh:它只读当前 access token,不会写 refresh 埋点。 +2. 只在后端 refresh handler 加埋点,但前端有有效 access token 时根本不调用 refresh。 +3. `ensureStoredAccessToken()` 有 token 时会直接返回;需要强制 refresh 时应使用 `refreshStoredAccessToken()`。 +4. 在埋点 helper 中返回错误并阻断登录/续期,会破坏认证主链路。 +5. refresh 场景把 `login_method` 写死为 password,会丢失手机/微信来源。 +6. 修改中文文件后忘记 `npm run check:encoding`。 +7. `cargo fmt -p api-server` 或前端测试可能让 `.env.local`、`.gitignore` 出现非业务改动;提交前用 `git status --short` 检查并撤回无关敏感/环境文件。 + +## 参考资料 + +- `references/session-restore-daily-login-tracking-2026-05-08.md`:已登录且 cookie 未过期时打开网页未触发每日登录埋点的根因与修复案例。 diff --git a/.hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md b/.hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md new file mode 100644 index 00000000..3efb6ca5 --- /dev/null +++ b/.hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md @@ -0,0 +1,75 @@ +# Session restore 每日登录埋点案例(2026-05-08) + +## 现象 + +用户反馈:已经登录且 cookie 没过期时,打开网页没有触发每日登录埋点。 + +## 根因 + +当本地 access token 仍有效时,前端 `AuthGate` 恢复登录态只会复用现有 token 并请求 `/api/auth/me`。`/api/auth/me` 只读取当前用户,不会进入 `POST /api/auth/session/refresh`,因此后端 refresh handler 中的每日登录埋点不会执行。 + +关键误判点:后端已经在 refresh cookie 续期写埋点,不等于“打开网页”一定会触发。前端必须实际调用 refresh/session 接口。 + +## 修复模式 + +1. 在 `src/services/apiClient.ts` 暴露强制 refresh 方法,例如: + +```ts +export async function refreshStoredAccessToken() { + return refreshAccessToken(); +} +``` + +2. 在 `src/components/auth/AuthGate.tsx` 的 hydrate/restore 中,使用: + +```ts +await refreshStoredAccessToken(); +const nextSession = await getCurrentAuthUser(); +``` + +而不是只调用: + +```ts +await ensureStoredAccessToken(); +const nextSession = await getCurrentAuthUser(); +``` + +3. 保留 `ensureStoredAccessToken()` 给普通受保护请求兜底;不要把所有请求都改成强制 refresh。 + +4. 确认 `server-rs/crates/api-server/src/refresh_session.rs` 在 rotate refresh session 成功且新 access token 签发成功后调用每日登录埋点 helper。 + +5. 确认 `server-rs/crates/spacetime-module/src/runtime/profile.rs` 中 `get_profile_task_center` 不再顺手写 `daily_login`,避免任务中心读取污染登录埋点。 + +## 测试 + +前端测试重点: + +- `AuthGate` 会等待 `refreshStoredAccessToken()` 完成后才暴露已恢复用户内容。 +- `AUTH_STATE_EVENT` 触发 hydrate 时仍保持已挂载平台内容和本地 tab 状态。 + +命令: + +```bash +npm run test -- AuthGate.test.tsx +npm run typecheck +``` + +后端/SpacetimeDB 编译: + +```bash +cd server-rs && cargo check -p spacetime-module +cd server-rs && cargo check -p api-server +``` + +全局检查: + +```bash +npm run check:encoding +git diff --check +``` + +## 注意 + +- Vitest 0.34 不支持 Jest 的 `--runInBand` 参数;命令里不要加。 +- 埋点失败只能 warning,不能阻断登录态恢复。 +- 如果后续发现打开页面产生过多 refresh 请求,需要在产品口径和埋点口径之间重新设计节流;但不能退回“只读 `/api/auth/me` 却期待写登录埋点”的状态。 diff --git a/.hermes/skills/genarrative-play-type-integration/SKILL.md b/.hermes/skills/genarrative-play-type-integration/SKILL.md new file mode 100644 index 00000000..a5b3ee45 --- /dev/null +++ b/.hermes/skills/genarrative-play-type-integration/SKILL.md @@ -0,0 +1,348 @@ +--- +name: genarrative-play-type-integration +description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。 +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [Genarrative, 玩法接入, 创作入口, 前端, 后端, contracts, runtime] + related_skills: [] +--- + +# Genarrative 新增玩法类型接入流程 + +用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。 + +## 适用场景 + +- 新增一个游戏玩法入口 +- 让某个玩法从“敬请期待”变为可创建 +- 为新玩法补齐创作工作台、结果页、发布与试玩链路 +- 将新玩法接入创作中心作品架与广场 + +## 先判断接入级别 + +### 1. 只做入口占位 + +只需要新增入口配置,不接 session/workspace/result/runtime。 + +适合: +- 敬请期待 +- 灰度占位 + +### 2. 可进入创作工作台 + +需要补齐前端分流、session、工作台、结果页,至少能生成草稿。 + +### 3. 完整玩法闭环 + +需要补齐: +- 创作入口 +- 工作台 +- 草稿生成 +- 结果页 +- 发布 +- 试玩 runtime +- 作品架 / 广场 / 分享 + +## 推荐接入顺序 + +### Step 1: 先定玩法 ID 和能力边界 + +先明确: +- `id` 是什么 +- 入口是否可见 +- 是否可点击创建 +- 是否需要对话式创作 +- 是否需要生成中页面 +- 是否需要 result/runtime/gallery/share + +不要先随便起临时 ID 再改名。 + +### Step 2: 新增入口配置 + +文件: +- `src/config/newWorkEntryConfig.ts` + +在 `NEW_WORK_ENTRY_CONFIG.creationTypes` 中新增或调整: +- `id` +- `title` +- `subtitle` +- `badge` +- `visible` +- `open` + +字段语义: +- `visible: true`:在创作页签 / 新建作品入口中展示。 +- `visible: false`:不在平台入口展示,但不删除既有玩法路由和能力。 +- `open: true`:可点击进入创作流程。 +- `open: false`:展示为锁定 / 敬请期待,不应进入创建流程。 + +如果只是占位: +- `visible: true` +- `open: false` + +相关渲染与过滤位置: +- `src/components/platform-entry/platformEntryCreationTypes.ts`:将 `NEW_WORK_ENTRY_CONFIG.creationTypes` 映射为平台入口卡片,`getVisiblePlatformCreationTypes()` 会过滤隐藏项,并把可创建模板排在敬请期待模板前面。 +- `src/components/custom-world-home/CustomWorldCreationStartCard.tsx`:创作页签首屏模板入口卡片的实际渲染位置。 +- `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`:选择创作类型弹层的渲染位置。 + +注意:当前项目工作区通常已经是 ``,路径不要再额外拼接 `./Genarrative/`。 + +### Step 3: 确认类型过滤逻辑 + +文件: +- `./Genarrative/src/components/platform-entry/platformEntryCreationTypes.ts` + +检查: +- `getVisiblePlatformCreationTypes()` 是否能展示新类型 +- `isPlatformCreationTypeVisible()` 是否能识别新类型 +- `locked` / `hidden` 是否正确映射 + +### Step 4: 扩展页面阶段 + +文件: +- `./Genarrative/src/components/platform-entry/platformEntryTypes.ts` + +为新玩法补充 `SelectionStage`: +- `*-agent-workspace` +- `*-generating`(可选) +- `*-result` +- `*-runtime`(可选) +- `*-gallery-detail`(可选) + +### Step 5: 在总流程中加类型分流 + +文件: +- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + +在 `handleCreationHubCreateType(type)` 中新增分支,确保: +- 能进入对应工作台 +- 能设置对应 `selectionStage` +- 能关闭类型弹层 + +同时补: +- `openAgentWorkspace()` +- `leaveFlow()` +- `submitMessage()`(对话式玩法) +- `executeAction()` + +### Step 6: 接入通用 Agent flow controller + +文件: +- `./Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.ts` + +如果是 Agent 型玩法,复用通用控制器: +- `createSession` +- `getSession` +- `streamMessage` +- `executeAction` +- `isBusy` +- `error` +- `streamingReplyText` +- `selectionStage` 切换 + +### Step 7: 定义 shared contracts + +前端: +- `./Genarrative/packages/shared/src/contracts/` + +后端: +- `./Genarrative/server-rs/crates/shared-contracts/src/` + +至少补齐: +- session snapshot +- create session request/response +- message request/response +- action request/response +- draft/result 结构 +- work summary / gallery 结构(如果需要) +- runtime 结构(如果需要) + +### Step 8: 实现前端 service client + +目录参考: +- `./Genarrative/src/services/` + +按玩法补: +- creation client +- runtime client(可选) +- works client(可选) +- gallery client(可选) + +建议保持和现有玩法一致的 API base 与命名风格。 + +### Step 9: 接后端 API + +文件参考: +- `./Genarrative/server-rs/crates/api-server/src/puzzle.rs` +- `./Genarrative/server-rs/crates/api-server/src/puzzle_agent_turn.rs` +- `./Genarrative/server-rs/crates/api-server/src/match3d.rs` + +通常需要: +- create session +- get session +- send message +- stream message +- execute action +- publish / save / delete +- runtime start / action(可选) +- gallery / detail(可选) + +后端设计优先按 Genarrative 的 DDD 分层拆开,不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里: +- `module-`:纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。 +- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。 +- `spacetime-module`:表定义、reducer/procedure、事务编排、migration;表结构变化要同步生成绑定。 +- `spacetime-client`:api-server 到 SpacetimeDB 的 facade,隐藏 reducer 调用细节。 +- `api-server`:Axum 路由、鉴权、SSE/stream、应用层编排。 +- `platform-*`:LLM、资产上传、鉴权、第三方服务等副作用。 + +建议按四条线设计后端能力: +- Agent 创作线:session、turn、stream、compile action。 +- Works 作品线:保存、发布、删除、草稿恢复。 +- Gallery 广场线:公开列表、详情、like/remix/share。 +- Runtime 运行态线:开始试玩、提交动作、读取状态。 + +### Step 10: 新增工作台组件 + +目录建议: +- `./Genarrative/src/components/-creation/AgentWorkspace.tsx` + +两种形态: + +#### 对话式 +适合设定逐轮补齐。 + +参考: +- `BigFishAgentWorkspace.tsx` +- `Match3DAgentWorkspace.tsx` + +#### 表单式 +适合输入结构明确的玩法。 + +参考: +- `PuzzleAgentWorkspace.tsx` + +### Step 11: 在渲染树中挂载新页面 + +文件: +- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + +补齐: +- workspace 分支 +- generating 分支(如需要) +- result 分支 +- runtime 分支(如需要) + +### Step 12: 新增结果页 + +目录建议: +- `./Genarrative/src/components/-result/ResultView.tsx` + +结果页至少支持: +- 展示 draft +- 返回编辑 +- 发布 +- 试玩 +- 错误展示 + +### Step 13: 需要试玩就补 runtime + +目录建议: +- `./Genarrative/src/components/-runtime/RuntimeShell.tsx` + +如果玩法是游戏类,建议补完整 runtime 闭环。 + +### Step 14: 接入作品架 / 广场 / 分享 + +需要改: +- `./Genarrative/src/components/custom-world-home/creationWorkShelf.ts` +- `./Genarrative/src/components/custom-world-home/CustomWorldCreationHub.tsx` +- `./Genarrative/src/services/publicWorkCode.ts` + +如果玩法支持发布,还要补: +- public work code +- public detail +- publish share modal +- like/remix(可选) + +### Step 15: 处理登录态与草稿恢复 + +要考虑: +- 刷新恢复草稿 +- 退出登录清空私有状态 +- result/draft 缺失时回退 +- busy / generating / runtime 中断恢复 + +### Step 16: 补测试 + +至少覆盖: +- 入口展示 +- 类型分流 +- 工作台打开 +- session 创建 +- compile action +- result 页切换 +- 发布后刷新作品架 +- runtime 进入与退出 + +## 最小改动清单 + +### 只做占位 + +只改: +- `./Genarrative/src/config/newWorkEntryConfig.ts` + +### 做到可进入工作台 + +至少改: +- `newWorkEntryConfig.ts` +- `platformEntryTypes.ts` +- `PlatformEntryFlowShellImpl.tsx` +- 新玩法 service client +- 新玩法工作台组件 +- shared contracts +- 后端 API + +### 做到完整闭环 + +还要补: +- result 页 +- runtime +- works / gallery +- public code +- share +- 作品架聚合 +- 测试 + +## 常见坑 + +1. 只加入口配置不够,类型分流和页面阶段也要补。 +2. `SelectionStage` 不扩展,前端无法安全切页。 +3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。 +4. 发布后不刷新 works/gallery,用户会看不到新作品。 +5. 如果走 SpacetimeDB,表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。 +6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO;先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤),RED 后再补领域类型与聚合函数。 +7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucket;bucket 输出要有稳定排序,并显式携带 `bucketKey`、`bucketStartDateKey`、`bucketEndDateKey`、`value`。 +8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type union;admin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。 +9. 退出登录时要清空新玩法私有状态,避免串用户。 +10. 移动端入口卡片增多后要检查布局和滚动体验。 + +## 参考资料 + +- `references/genarrative-analytics-tracking-runtime.md`:analytics/tracking runtime 粒度聚合、contracts 同步与 SpacetimeDB 生成物注意事项。 + +## 验证标准 + +一个玩法算真正接入成功,至少要满足: + +- 入口能展示 +- 能进入对应工作台 +- 能创建 session +- 能生成草稿 +- 能进入结果页 +- 能返回编辑 +- 如果需要,可试玩 +- 如果需要,可发布 +- 发布后能回到作品架 / 广场 / 分享链路 diff --git a/.hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md b/.hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md new file mode 100644 index 00000000..4fa72a77 --- /dev/null +++ b/.hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md @@ -0,0 +1,57 @@ +# Genarrative analytics/tracking runtime 接入经验 + +本参考来自 analytics time dimension / tracking daily stat 粒度聚合实现与一次“只提交了 bindings/tests、后端链路未补齐”的纠偏。 + +## 推荐顺序 + +1. 先读仓库 `README.md`、`AGENTS.md`、相关 `/docs/technical` 与 `.hermes/plans`,确认当前阶段范围。 +2. 遵循 TDD:先在 `server-rs/crates/module-runtime/tests/` 写纯函数测试,验证缺失类型/函数导致 RED。 +3. 在 `module-runtime/src/domain.rs` 增加领域类型,例如: + - `AnalyticsGranularity`:`day | week | month | quarter | year` + - daily stat snapshot + - bucket metric + - query request/response/input + - Spacetime procedure result(例如 `AnalyticsMetricQueryProcedureResult { ok, buckets, error_message }`),否则 module procedure 无法生成 client bindings。 +4. 在 `module-runtime/src/application.rs` 复用已有 `build_analytics_date_dimension_from_date_key`,实现 daily stat 到 day/week/month/quarter/year bucket 的聚合。 +5. 输出 bucket 应稳定排序,可用 `BTreeMap`;聚合前按 `event_key + scope_kind + scope_id` 过滤。 +6. 在 `module-runtime/src/commands.rs` 增加 query input builder,复用现有字段错误(如 missing event key、missing scope id)。 +7. 同步 contracts: + - Rust:`server-rs/crates/shared-contracts/src/runtime.rs` + - 前端共享:`packages/shared/src/contracts/runtime.ts` + - 管理端若有本地 API 类型,也同步 `apps/admin-web/src/api/adminApiTypes.ts` +8. 接 Spacetime module 源头:在 `server-rs/crates/spacetime-module/src/runtime/profile.rs` 增加只读 procedure(例如 `query_analytics_metric`),从 `tracking_daily_stat` 读 rows,映射成 `RuntimeAnalyticsDailyStatSnapshot` 后调用领域聚合函数。 +9. 重新生成 `server-rs/crates/spacetime-client/src/module_bindings/`。如果当前环境没有 `spacetime`/`spacetimedb` CLI,可临时按现有生成物风格手补 type/procedure/mod.rs,但必须在文档和最终说明中标注“临时手补生成物”,并要求后续在有 CLI 的机器用项目脚本重新生成覆盖。 +10. 接 `spacetime-client`: + - `src/mapper.rs`:module binding procedure result → `module_runtime` record/response;补 tracking scope/granularity 映射。 + - `src/runtime.rs`:新增 facade 方法(例如 `SpacetimeClient::query_analytics_metric`),调用生成的 procedure。 + - `src/lib.rs`:若 facade/mapper 需要领域类型或 builder,补导入;注意同名 binding 类型会造成误解析。 +11. 接 `api-server`: + - `src/runtime_profile.rs`:Query params / parser / handler / response builder。 + - `src/app.rs`:挂路由,例如 profile 或 admin analytics endpoint;选择路径前确认产品定位。 +12. 最后更新 docs/plan,并确认 diff 不只是生成物。 + +## 验证命令示例 + +```bash +cd /server-rs +cargo test -p module-runtime --test analytics_granularity +cargo check -p shared-contracts +cargo check -p spacetime-module +cargo check -p spacetime-client +cargo check -p api-server +``` + +If terminal output is compacted by the tool, rerun the specific command directly (without `head`) or capture full output before concluding; `cargo check` exit code 0 with warnings is acceptable when warnings are pre-existing and documented. + +## 坑 + +- 不要手改 `server-rs/crates/spacetime-client/src/module_bindings/` 生成物;若缺 procedure/type,回源头改 Spacetime module 后重新生成。若当前环境没有 `spacetime`/`spacetimedb` CLI 且必须临时手补生成物,要在最终说明中明确这是临时替代,并尽快在有 CLI 的环境重新生成。 +- `spacetime-client/src/runtime.rs` 同时能看到 `module_bindings::*` 和领域层 `module_runtime` 类型。新增 facade 方法参数要使用领域别名(如 `DomainRuntimeTrackingScopeKind`、`module_runtime::AnalyticsGranularity`),不要误用 binding 里的 `RuntimeTrackingScopeKind`;否则 `build_*_input` 会报 “expected module_runtime::..., found binding ...”。 +- 如果缺少 SpacetimeDB CLI,手补 bindings 的最小集合通常包括:`*_type.rs`(input、enum、bucket、procedure result)、`*_procedure.rs`、`module_bindings/mod.rs` 的 `pub mod`/`pub use`/procedure re-export。完成后必须跑 `cargo check -p spacetime-client` 验证 SDK trait、procedure 名称与 result 字段是否匹配。 +- `spacetime-client/src/mapper.rs` 新增 query 链路时通常要同时补四类映射:领域 input → binding input、binding enum 映射、binding procedure result → 领域 response、binding bucket/item → 领域 bucket/item。只在 `runtime.rs` 加 facade 会出现 unused import 或类型不匹配。 +- 修改 Rust import 时注意 `serde::Deserialize` 与 `serde_json` 的排序/使用:如果只加了 `Query`/`Deserialize` 但 handler 尚未实现,`cargo check -p api-server` 会暴露 unused import;不要把 import 作为完成信号。 +- 只补 shared-contracts 不够;`packages/shared` 和 admin-web 本地类型可能各有一份。 +- 周粒度应按 ISO week/date dimension,而不是简单 `day_key / 7`。 +- bucket response 建议显式包含 start/end date key,避免前端再推导时间边界。 +- `spacetime-module` 可能能编译通过,但如果没有重新生成 bindings,`spacetime-client` 仍不会有新 procedure 方法;必须以 client facade/API handler 可调用为完成标准。 +- api-server 中新增 `Query`、`Deserialize` 等 import 后要立即补 handler,否则容易留下 unused import。 diff --git a/.hermes/skills/genarrative-profile-features/SKILL.md b/.hermes/skills/genarrative-profile-features/SKILL.md new file mode 100644 index 00000000..4507f7d0 --- /dev/null +++ b/.hermes/skills/genarrative-profile-features/SKILL.md @@ -0,0 +1,99 @@ +--- +name: genarrative-profile-features +description: 在 Genarrative “我的”页签新增或修改个人中心入口、独立 profile 路由、反馈/记录/设置类页面时使用。 +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [Genarrative, profile, 我的页签, 前端, 路由, 反馈] + related_skills: [writing-plans, test-driven-development] +--- + +# Genarrative “我的”页签功能接入 + +用于在 Genarrative 平台“我的”页签新增或修改入口,以及把入口接到独立页面阶段/路由。例如:帮助与反馈、反馈记录、个人设置、账号相关轻量页面。 + +## 适用场景 + +- 在“我的”页签新增快捷入口或卡片按钮。 +- 点击入口后进入独立页面,而不是在当前面板下方展开内容。 +- 新增 `/profile/...` 路由或 `SelectionStage`。 +- 新增移动端优先的个人中心子页面组件。 +- 修改 `RpgEntryHomeView`、`PlatformEntryFlowShellImpl`、`appPageRoutes` 等前端 profile 链路。 + +## 必读约束 + +1. 按项目约束:先检查/补齐文档,再落地工程修改。 +2. UI 面板保持清爽,不要默认堆功能说明文案。 +3. 点击按钮弹出/进入独立面板的设计,不要实现成在当前面板下方追加内容。 +4. 移动端优先,同时兼顾网页端容器宽度。 +5. 包含中文的文件优先局部补丁,修改后运行编码检查。 +6. 非必要不新建系统;优先复用现有平台入口、阶段和路由机制。 + +## 代码接入路径 + +常见文件: + +- `src/components/rpg-entry/RpgEntryHomeView.tsx` + - “我的”页签 UI 主入口通常在此。 + - 新增入口时优先扩展 props,例如 `onOpenFeedback?: () => void`。 + - 在现有快捷入口区新增 `ProfileShortcutButton`,保持图标、label、subLabel 风格一致。 + +- `src/components/platform-entry/platformEntryTypes.ts` + - 若需要独立页面阶段,扩展 `SelectionStage` union。 + - 例如新增 `'profile-feedback'`。 + +- `src/routing/appPageRoutes.ts` + - 在 `STAGE_ROUTE_ENTRIES` 添加 `/profile/...` 路由映射。 + - 验证 `resolveSelectionStageFromPath()` 与 `resolvePathForSelectionStage()` 双向一致。 + +- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + - 引入新页面组件。 + - 新增打开函数:必要时先检查登录态,未登录调用 `authUi?.openLoginModal()`。 + - 打开 profile 子页时同步 `setPlatformTab('profile')`,再 `setSelectionStage(...)`。 + - 当 `selectionStage` 直接由路由进入 profile 子页时,用 `useEffect` 同步当前 tab 到 `profile`。 + - 在主渲染分支中为新阶段渲染独立 `` 页面;返回时回到 `platform` 阶段并保持 `profile` tab。 + +- `src/components/platform-entry/.tsx` + - 页面组件可放在 platform-entry 下,与 shell 阶段渲染保持一致。 + - 表单首版没有后端接口时,可通过可选 `onSubmit` prop 暴露提交 payload,并在组件内展示成功/失败态;注释说明后续替换为 API 调用。 + +## 推荐实施顺序 + +1. 读取 `.hermes/plans/...` 或产品文档,确认入口、路由、页面行为。 +2. 若现有文档不足,先在 `docs/prd/` 增加可编码落地的 PRD。 +3. 增加 `SelectionStage` 与 `appPageRoutes` 映射,并先跑 `npm run typecheck`。 +4. 新建独立页面组件,尽量通过 props 暴露 `onBack`/`onSubmit`,避免直接耦合全局状态。 +5. 在 `RpgEntryHomeView.tsx` 增加入口 prop 与按钮。 +6. 在 `PlatformEntryFlowShellImpl.tsx` 串联导航、登录态、阶段渲染和返回逻辑。 +7. 增加基础测试:路由解析、页面字段渲染、关键交互/校验、返回按钮。 +8. 跑编码检查、类型检查和相关 vitest。 +9. 分阶段 commit;用户要求更新工作区时再 push。 + +## 测试与验证命令 + +常用命令: + +```bash +npm run check:encoding +npm run typecheck +npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/.test.tsx +# 或项目脚本: +npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/.test.tsx +``` + +如果新增/修改中文文档或中文 UI,`check:encoding` 必跑。 + +## 参考案例 + +- `references/profile-feedback-entry-2026-05-08.md`:帮助与反馈入口案例,覆盖文档、路由阶段、独立页面组件、“我的”页签按钮、shell 导航、测试和验证命令。 + +## 常见坑 + +1. 只在 `RpgEntryHomeView` 新增按钮但没有接 shell 导航,导致点击无效果。 +2. 只新增 `SelectionStage` 但忘记 `appPageRoutes`,导致刷新/直达路由不能恢复页面。 +3. 直达 `/profile/...` 时没有同步 `setPlatformTab('profile')`,底部 tab 状态与页面不一致。 +4. 把反馈/设置表单插到“我的”面板下方,违背独立页面体验。 +5. 没有测试 `resolveSelectionStageFromPath`/`resolvePathForSelectionStage`,后续路由改动容易回归。 +6. 中文页面或文档改动后忘记编码检查。 diff --git a/.hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md b/.hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md new file mode 100644 index 00000000..a3dc5d62 --- /dev/null +++ b/.hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md @@ -0,0 +1,80 @@ +# 帮助与反馈入口案例(2026-05-08) + +本案例来自 Genarrative “我的”页签新增反馈入口与独立反馈页实现。 + +## 目标 + +- 在“我的”页签新增“反馈”快捷入口。 +- 点击后进入独立路由 `/profile/feedback`。 +- 页面按移动端参考图实现“帮助与反馈”表单:问题描述、上传凭证、联系电话、提交、查看反馈与投诉记录。 + +## 关键文件 + +- `docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md` +- `src/components/platform-entry/platformEntryTypes.ts` +- `src/routing/appPageRoutes.ts` +- `src/components/platform-entry/PlatformFeedbackView.tsx` +- `src/components/rpg-entry/RpgEntryHomeView.tsx` +- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` +- `src/routing/appPageRoutes.test.ts` +- `src/components/platform-entry/PlatformFeedbackView.test.tsx` + +## 落地顺序 + +1. 先补 PRD,避免从参考图直接猜代码细节。 +2. 在 `SelectionStage` 中新增 `'profile-feedback'`。 +3. 在 `STAGE_ROUTE_ENTRIES` 添加: + - `['profile-feedback', '/profile/feedback']` +4. 新建 `PlatformFeedbackView`: + - `onBack: () => void` + - `onSubmit?: (payload) => void | Promise` + - 内部维护描述、联系电话、上传图片预览、错误态、成功态。 + - 首版没有后端接口时,`onSubmit` 为后续 API 接入点。 +5. 在 `RpgEntryHomeViewProps` 添加 `onOpenFeedback?: () => void`。 +6. 在“我的”页签快捷入口区新增 `ProfileShortcutButton`: + - `label="反馈"` + - `subLabel="帮助与反馈"` + - `onClick={onOpenFeedback}` +7. 在 `PlatformEntryFlowShellImpl`: + - import `PlatformFeedbackView` + - 新增 `openProfileFeedback` + - 未登录则 `authUi?.openLoginModal()` + - 已登录则 `setPlatformTab('profile')` + `setSelectionStage('profile-feedback')` + - 直达阶段时 `useEffect` 同步 `profile` tab + - 渲染 `selectionStage === 'profile-feedback'` 的独立 `` +8. 增加测试: + - 路由双向解析 + - 页面字段渲染 + - 描述低于 10 字不提交 + - 提交时 trim payload + - 顶部返回按钮调用 `onBack` + +## UI 参考要点 + +- 移动端优先,页面最大宽度约 30rem。 +- 浅灰背景,白色圆角卡片。 +- 顶部标题:`帮助与反馈`。 +- 分区标题:`反馈问题`。 +- 问题描述 placeholder:提示填写 10 字以上,勿填身份证号等隐私。 +- 字数计数:`0/200`。 +- 上传凭证:最多四张,上传占位显示 `上传凭证` 和 `(最多四张)`。 +- 联系电话:选填。 +- 主按钮:蓝色圆角 `提交`。 +- 底部链接:`查看反馈与投诉记录`。 + +## 验证命令 + +```bash +npm run check:encoding +npm run typecheck +npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx +# 或 +npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx +``` + +## 已踩坑/经验 + +- `appPageRoutes.test.ts` 若已有历史内容,写入测试时注意不要误删旧用例;最终 commit 里出现 “insertions + deletions” 时要检查是否覆盖了既有测试。 +- 页面组件里的图片预览需要在移除或卸载时 `URL.revokeObjectURL`,避免泄漏。 +- 对隐藏 file input 的测试可以先不强行覆盖,首版覆盖表单字段、校验和 payload 更稳定。 +- 如果用 `authUi` 对象作为 callback 依赖,需确认引用稳定性;现有 shell 中可接受,但复杂化时考虑拆出具体字段依赖。 diff --git a/.hermes/skills/genarrative-profile-invite-flow/SKILL.md b/.hermes/skills/genarrative-profile-invite-flow/SKILL.md new file mode 100644 index 00000000..784998a1 --- /dev/null +++ b/.hermes/skills/genarrative-profile-invite-flow/SKILL.md @@ -0,0 +1,149 @@ +--- +name: genarrative-profile-invite-flow +description: 在 Genarrative 中排查或修改邀请码、邀请好友、首次登录后填写邀请码、我的页签邀请码兑换链路时使用。 +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [Genarrative, 邀请码, referral, auth, profile, query-params, 前端] + related_skills: [] +--- + +# Genarrative 邀请码与邀请好友流程 + +用于排查或修改 Genarrative 的邀请码读取、填写、兑换、邀请中心与“我的”页签相关能力。 + +## 适用场景 + +- 判断 URL query 参数中的邀请码是否被读取。 +- 修改邀请码填写入口、首次登录后引导或“我的”页签兑换入口。 +- 排查邀请码预填、兑换、已填写状态、邀请好友复制链接。 +- 修改邀请中心 API client 或前端 referral UI。 +- 回答用户关于“邀请码在哪里填 / 从哪里配置 / query 是否支持”的问题。 + +## 先做代码核对,不要只凭旧记忆回答 + +邀请码流程近期发生过迁移:不要默认认为登录窗口可填写邀请码。回答前优先搜索并核对当前代码,尤其是: + +```bash +cd +python3 - <<'PY' +from pathlib import Path +root=Path('src') +terms=['RegistrationInviteModal','readInviteCodeFromLocation','referralRedeemCode','redeemRpgProfileReferralInviteCode','邀请码','inviteCode'] +for term in terms: + print('\n---', term) + for p in root.rglob('*'): + if p.is_file() and p.suffix in ['.ts', '.tsx']: + try: + txt=p.read_text('utf-8') + except Exception: + continue + if term in txt: + for i, line in enumerate(txt.splitlines(), 1): + if term in line: + print(f'{p}:{i}:{line.strip()[:180]}') +``` + +## 当前前端链路口径 + +### 1. AuthGate 中仍有旧 query 读取逻辑 + +文件: +- `src/components/auth/AuthGate.tsx` + +重点函数 / 状态: +- `readInviteCodeFromLocation()` +- `pendingInviteCode` +- `showRegistrationInviteModal` +- `RegistrationInviteModal` + +当前旧逻辑会读取: +- `?inviteCode=...` +- `?invite_code=...` + +并把值清洗为大写字母数字形式。 + +### 2. 登录窗口本身不再填写邀请码 + +不要回答“登录窗口可填写邀请码”。当前登录弹窗 `LoginScreen` 只负责登录 / 注册账号;邀请码填写已迁移到登录后的流程。 + +### 3. 新版“我的”页签兑换入口在 RpgEntryHomeView + +文件: +- `src/components/rpg-entry/RpgEntryHomeView.tsx` + +重点常量 / 函数 / 状态: +- `PROFILE_INVITE_QUERY_KEYS`:新版 query 支持 `inviteCode` / `invite_code`。 +- `normalizeProfileInviteQueryCode()`:去掉非字母数字并转大写。 +- `readProfileInviteCodeFromLocationSearch()`:从 `window.location.search` 读取并 normalize。 +- `pendingProfileInviteCode`:组件初始化时读取 query 邀请码。 +- `referralCenter` +- `referralRedeemCode` +- `setReferralRedeemCode` +- `openProfilePopupPanel('redeem')` +- `submitReferralRedeemCode()` +- `canShowReferralRedeemShortcut` +- `isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt)` + +UI 中“填邀请码”面板会使用 `referralRedeemCode` 作为输入值,并通过 `submitReferralRedeemCode()` 提交。当前新版实现会在首次打开“填邀请码”面板时用 `pendingProfileInviteCode` 预填输入框;例如 `/?inviteCode=spring-2026` 会预填为 `SPRING2026`。 + +### 4. 新版兑换 API client + +文件: +- `src/services/rpg-entry/rpgProfileClient.ts` + +函数: +- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center` +- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code` + +## 判断 query 参数是否真正接入新版流程 + +回答这类问题时要区分两层: + +1. “是否存在旧 query 读取代码”:看 `AuthGate.tsx` 的 `readInviteCodeFromLocation()`。 +2. “query 是否接到新版填写入口”:看 `RpgEntryHomeView.tsx` 是否存在 `pendingProfileInviteCode` / `readProfileInviteCodeFromLocationSearch()`,以及打开 `openProfilePopupPanel('redeem')` 时是否把该值写回 `referralRedeemCode`。 + +当前新版流程已经支持 `inviteCode` / `invite_code` query 预填“我的”页签的“填邀请码”弹窗;登录窗口仍不填写邀请码。 + +如果未来代码只看到 AuthGate 读 query,但没有看到 `RpgEntryHomeView` 的 `referralRedeemCode` 从 query 初始化,就应回答: + +> 代码里仍支持读取 `inviteCode` / `invite_code`,但新版“第一次登录后 / 我的页签”的填写入口未必已经完整接入该 query 值;需要继续把 query 值传入新版 profile referral redeem 流程。 + +## 修改建议顺序 + +如果要把 query 邀请码完整接入新版流程,建议按这个顺序做: + +1. 先确定 query 参数规范:继续支持 `inviteCode` / `invite_code`,并统一 normalize。 +2. 在 `RpgEntryHomeView.tsx` 内用 `readProfileInviteCodeFromLocationSearch(window.location.search)` 初始化 `pendingProfileInviteCode`。 +3. 用 `pendingProfileInviteCode` 初始化 `referralRedeemCode`,并在 `openProfilePopupPanel('redeem')` 时重新写回,避免关闭后再次打开被清空。 +4. 如产品要求自动弹出: + - 有 `pendingProfileInviteCode` 且未登录时,自动调用 `authUi?.openLoginModal()` 打开登录窗口;登录窗口仍不承接邀请码输入。 + - 有 `pendingProfileInviteCode` 且已登录时,自动将 `referralRedeemCode` 设为该 query 邀请码,并 `setProfilePopupPanel('redeem')` 直接打开“填邀请码”面板。 + - 用 `useRef` 记录是否已处理过当前 query,避免组件重渲染或 `authUi` 对象变化导致重复弹窗。 + - 当前项目实现已从“只预填、不自动弹”调整为上述行为。 +5. 兑换成功后清理输入态;是否清理 URL query 需由产品决定,避免破坏分享链接归因。 +6. 补测试覆盖:未登录访问带 query、已登录访问带 query 自动打开填写面板、我的页签手动打开、已填写邀请码、过期窗口、空/非法 query。当前已有 `RpgEntryHomeView.recharge.test.tsx` 覆盖: + - `invite query opens login modal for logged out users` + - `invite query opens redeem modal directly for logged in users` + - `profile redeem invite modal reads query invite code after login` + +## 常见坑 + +1. 不要把旧 `RegistrationInviteModal` 误认为当前唯一入口。 +2. 不要说“登录窗口可以填写邀请码”,除非当前代码重新把邀请码输入放回 `LoginScreen`。 +3. `AuthGate` 读到 query 不等于新版 `RpgEntryHomeView` 已经预填。 +4. “第一次登录后”与“我的页签”可能是两个入口;修改时要同时检查自动引导和手动入口。 +5. `canShowReferralRedeemShortcut` 受登录态、创建时间窗口、邀请中心初始化、已兑换状态共同影响。 +6. 邀请码 URL 通常由 `inviteLinkPath` 生成,复制逻辑在 `copyInviteInfo()`,不要只改兑换入口而忘记分享链接格式。 + +## 参考资料 + +- `references/query-invite-code-flow-2026-05-07.md`:本次会话确认的邀请码 query 与新版 profile referral 入口关系。 + +## 验证标准 + +- 能明确回答当前 query 参数读取位置与参数名。 +- 能区分旧 AuthGate 邀请弹窗与新版“我的”页签 referral redeem。 +- 若实现改动,测试覆盖带 query 的登录后预填/弹窗行为,以及已填写邀请码时不再提示。 diff --git a/.hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md b/.hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md new file mode 100644 index 00000000..91bdf58a --- /dev/null +++ b/.hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md @@ -0,0 +1,101 @@ +# Query 邀请码与新版 profile referral 入口关系(2026-05-07) + +## 会话结论 + +用户指出:邀请码填写流程已经修改,登录窗口目前填写不了邀请码;邀请码填写被挪到了第一次登录后以及“我的”页签中。 + +因此后续回答或修改时不能只根据 `AuthGate` 里的旧逻辑判断“已支持”。 + +## 当前代码观察 + +### 旧 AuthGate 逻辑 + +文件:`src/components/auth/AuthGate.tsx` + +- `readInviteCodeFromLocation()` 读取 `window.location.search`。 +- 支持 `inviteCode` / `invite_code`。 +- 会 normalize 为大写字母数字。 +- 写入 `pendingInviteCode`,传给 `RegistrationInviteModal`。 + +这只能说明“旧层仍有 query 读取”。 + +### 新版 profile referral 入口 + +文件:`src/components/rpg-entry/RpgEntryHomeView.tsx` + +- “我的”页签标签为 `profile`。 +- 兑换输入状态:`referralRedeemCode`。 +- 打开填邀请码面板:`openProfilePopupPanel('redeem')`。 +- 提交兑换:`submitReferralRedeemCode()`。 +- 可显示快捷入口受 `canShowReferralRedeemShortcut` 控制。 + +文件:`src/services/rpg-entry/rpgProfileClient.ts` + +- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center` +- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code` + +## 回答口径 + +如果被问“是否支持 query 参数读取邀请码”,应回答: + +- 代码里仍有 query 读取,支持 `inviteCode` / `invite_code`。 +- 但登录窗口不再填写邀请码。 +- 新版入口在第一次登录后 / 我的页签;需要检查 `referralRedeemCode` 是否从 query 初始化。 +- 若没有该连接,就不能说新版流程完整支持 query 预填。 + +## 本次实现后的状态 + +已将 query 邀请码读取接入新版 `RpgEntryHomeView`: + +- `PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code']` +- `normalizeProfileInviteQueryCode()`:去除非字母数字并转大写。 +- `readProfileInviteCodeFromLocationSearch(window.location.search)`:读取 query 邀请码。 +- `pendingProfileInviteCode`:组件初始化时读取 query。 +- `referralRedeemCode`:用 `pendingProfileInviteCode` 初始化。 +- `openProfilePopupPanel('redeem')`:打开“填邀请码”时重新写入 `pendingProfileInviteCode`,避免首次打开或重新打开时丢失 query 预填。 + +当前行为:只预填,不自动弹出“填邀请码”面板;用户仍需在“我的”页签点击“填邀请码”。 + +验证测试: + +```bash +cd +npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0 +node scripts/check-encoding.mjs +``` + +测试用例:`profile redeem invite modal reads query invite code after login` 覆盖 `/?inviteCode=spring-2026` 预填为 `SPRING2026`。 + +## 后续调整:带邀请码链接自动开窗 + +用户进一步明确期望: + +- 如果用户未登录,直接打开登录窗口。 +- 如果用户已登录,直接打开邀请码填写窗口。 + +实现要点: + +- 在 `RpgEntryHomeView.tsx` 中保留 `pendingProfileInviteCode` 作为 query 邀请码来源。 +- 新增 `autoOpenedInviteQueryRef = useRef(false)`,防止 effect 重复触发弹窗。 +- 新增 `useEffect`: + - 无 query 邀请码或已处理过则 return。 + - 未登录:调用 `authUi?.openLoginModal()`。 + - 已登录:设置 `referralRedeemCode`、清空 referral 错误/成功提示、`setProfilePopupPanel('redeem')`。 +- 登录窗口仍不接收邀请码;邀请码只在登录后的 profile referral redeem 面板显示。 +- 仍然只自动打开和预填,不自动提交兑换。 + +补充测试: + +- `invite query opens login modal for logged out users` +- `invite query opens redeem modal directly for logged in users` +- 原 `profile redeem invite modal reads query invite code after login` 同步调整为直接断言自动打开后的输入值。 + +验证命令仍为: + +```bash +cd +npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0 +node scripts/check-encoding.mjs +```