master #14

Merged
kdletters merged 226 commits from master into release 2026-05-13 13:23:09 +08:00
11 changed files with 1082 additions and 5 deletions
Showing only changes of commit 7e35231dfe - Show all commits

View File

@@ -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<AdminRouteId, ...>` 报错。
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/<version>` 并建立 `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` 缺失和冷编译超时的修复记录。

View File

@@ -23,7 +23,7 @@
## 验证命令
```bash
cd <repo-root>/.worktrees/hermes-996d586b
cd <repo-root>
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 <repo-root>/.worktrees/hermes-996d586b
cd <repo-root>
npm run api-server
npm run admin-web:dev -- --host 127.0.0.1
```

View File

@@ -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/`

View File

@@ -0,0 +1,132 @@
---
name: genarrative-auth-session-flow
description: 在 Genarrative 中排查或修改登录、access token、refresh cookie、AuthGate 会话恢复、登录态刷新、认证埋点链路时使用。
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [Genarrative, auth, session, cookie, refresh-token, AuthGate, tracking]
related_skills: [systematic-debugging, test-driven-development, genarrative-profile-features]
---
# Genarrative 认证会话与登录埋点链路
用于 Genarrative 中登录、会话恢复、refresh cookie 续期、access token 补票、AuthGate 恢复登录态,以及每日登录/认证相关埋点的排查与修改。
## 适用场景
- 用户反馈登录态、cookie、自动续期、刷新页面后状态异常。
- 修改 `AuthGate``apiClient``authService` 或 Rust `api-server` 认证接口。
- 排查“已登录但打开网页没有触发登录埋点”等 session restore 场景。
- 修改手机验证码登录、密码登录、微信登录、重置密码后自动登录、refresh session rotate。
- 需要判断某个前端动作是否真正调用了后端 refresh/session 或埋点 procedure。
## 关键代码路径
前端:
- `src/components/auth/AuthGate.tsx`
- 登录态 hydrate / restore 的入口。
- 监听 `AUTH_STATE_EVENT` 后重新 hydrate。
- 是否先 refresh、再 `/api/auth/me`,决定打开页面是否进入后端 refresh 链路。
- `src/services/apiClient.ts`
- access token 本地保存、`ensureStoredAccessToken()``refreshStoredAccessToken()``fetchWithApiAuth()`
- `ensureStoredAccessToken()` 有 token 时会直接复用,不一定触发后端 refresh。
- `refreshStoredAccessToken()` 应直接调用 refresh 接口,用于必须轮换 cookie / 写续期埋点的场景。
- `src/services/authService.ts`
- `getCurrentAuthUser()` 请求 `/api/auth/me`
- 登录、登出、账号安全相关 API client。
后端:
- `server-rs/crates/api-server/src/auth_session.rs`
- 创建 refresh cookie / access token。
- `record_daily_login_tracking_event_after_auth_success(...)` 统一写每日登录埋点;失败 warning不阻断认证流程。
- `server-rs/crates/api-server/src/refresh_session.rs`
- `POST /api/auth/session/refresh`
- rotate refresh session、签发新 access token、记录每日登录埋点。
- `server-rs/crates/api-server/src/auth_me.rs`
- `/api/auth/me` 只读取当前 access token 对应用户,不应假设它会触发 refresh 或登录埋点。
- `server-rs/crates/api-server/src/phone_auth.rs`
- `server-rs/crates/api-server/src/password_entry.rs`
- `server-rs/crates/api-server/src/password_management.rs`
- `server-rs/crates/api-server/src/wechat_auth.rs`
- 各真实认证成功入口。
- `server-rs/crates/spacetime-client/src/runtime.rs`
- `record_daily_login_tracking_event(user_id)` 调用 SpacetimeDB procedure。
- `server-rs/crates/spacetime-module/src/runtime/profile.rs`
- `record_daily_login_tracking_event_and_return` procedure。
- 任务中心读取不应污染每日登录埋点;如看到 `get_profile_task_center` 顺手写 `daily_login`,优先复核是否回归。
## 调试顺序
1. 先明确用户场景属于哪类:
- 新登录成功。
- cookie/access token 已过期后的自动刷新。
- 已登录且 cookie/access token 未过期时打开网页。
- 只调用 `/api/auth/me` 或某个受保护业务接口。
2. 查前端实际调用链,不要只看后端埋点点位:
- `AuthGate` hydrate 是否调用 `refreshStoredAccessToken()`
- 是否只是 `ensureStoredAccessToken()` + `/api/auth/me`
- `fetchWithApiAuth()` 是否因为已有 access token 而跳过 refresh
3. 查后端实际埋点点位:
- 登录成功入口是否在 session 创建后调用 helper。
- refresh session 是否在 rotate 与 access token 签发成功后调用 helper。
- 失败策略是否只 warning、不阻断响应。
4. 如涉及 SpacetimeDB procedure/table/binding按项目 SpacetimeDB skills 与文档同步检查绑定生成、`migration.rs`、private table 限制。
5. 修改前补齐 `docs/technical/` 中对应方案/根因;修改后同步更新。
## 关键经验:已登录打开网页也要主动 refresh 才能写登录埋点
常见误判:后端已经在 refresh cookie 续期时写每日登录埋点,就以为“打开网页”会触发埋点。
实际链路中,如果用户已经登录且本地 access token 还有效:
1. `ensureStoredAccessToken()` 会直接返回已有 token。
2. `AuthGate` 随后请求 `/api/auth/me`
3. `/api/auth/me` 只校验/读取用户,不会 rotate refresh session。
4. 因此后端 refresh/session 埋点不会触发。
若产品要求“已登录且 cookie 没过期时打开网页也记录登录埋点”,`AuthGate` 的 restore/hydrate 应主动调用 `refreshStoredAccessToken()`,再调用 `getCurrentAuthUser()`
## 每日登录埋点原则
- 真实登录成功:在 refresh session / access token 创建成功后记录。
- cookie refresh 续期:在 rotate refresh session 成功且新 access token 签发成功后记录。
- 已登录打开网页:前端必须主动走 refresh 续期链路,不能只请求 `/api/auth/me`
- `login_method` 对于 refresh 场景使用 refresh session 保存的 `issued_by_provider`
- 埋点失败不阻断登录、续期、会话恢复或 token 返回,只记录 warning。
- 任务中心读取不应作为登录埋点来源,避免后台查看/刷新任务中心污染登录数据。
## 测试与验证命令
按改动范围选择:
```bash
npm run test -- AuthGate.test.tsx
npm run typecheck
cd server-rs && cargo test -p api-server auth_session -- --nocapture
cd server-rs && cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture
cd server-rs && cargo check -p api-server
cd server-rs && cargo check -p spacetime-client
cd server-rs && cargo check -p spacetime-module
npm run check:encoding
git diff --check
```
注意Vitest 0.34 不支持 Jest 的 `--runInBand`;不要把 `--runInBand` 加到 `npm run test -- AuthGate.test.tsx` 后面。
## 常见坑
1.`/api/auth/me` 当作 refresh它只读当前 access token不会写 refresh 埋点。
2. 只在后端 refresh handler 加埋点,但前端有有效 access token 时根本不调用 refresh。
3. `ensureStoredAccessToken()` 有 token 时会直接返回;需要强制 refresh 时应使用 `refreshStoredAccessToken()`
4. 在埋点 helper 中返回错误并阻断登录/续期,会破坏认证主链路。
5. refresh 场景把 `login_method` 写死为 password会丢失手机/微信来源。
6. 修改中文文件后忘记 `npm run check:encoding`
7. `cargo fmt -p api-server` 或前端测试可能让 `.env.local``.gitignore` 出现非业务改动;提交前用 `git status --short` 检查并撤回无关敏感/环境文件。
## 参考资料
- `references/session-restore-daily-login-tracking-2026-05-08.md`:已登录且 cookie 未过期时打开网页未触发每日登录埋点的根因与修复案例。

View File

@@ -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` 却期待写登录埋点”的状态。

View File

@@ -0,0 +1,348 @@
---
name: genarrative-play-type-integration
description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [Genarrative, 玩法接入, 创作入口, 前端, 后端, contracts, runtime]
related_skills: []
---
# Genarrative 新增玩法类型接入流程
用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。
## 适用场景
- 新增一个游戏玩法入口
- 让某个玩法从“敬请期待”变为可创建
- 为新玩法补齐创作工作台、结果页、发布与试玩链路
- 将新玩法接入创作中心作品架与广场
## 先判断接入级别
### 1. 只做入口占位
只需要新增入口配置,不接 session/workspace/result/runtime。
适合:
- 敬请期待
- 灰度占位
### 2. 可进入创作工作台
需要补齐前端分流、session、工作台、结果页至少能生成草稿。
### 3. 完整玩法闭环
需要补齐:
- 创作入口
- 工作台
- 草稿生成
- 结果页
- 发布
- 试玩 runtime
- 作品架 / 广场 / 分享
## 推荐接入顺序
### Step 1: 先定玩法 ID 和能力边界
先明确:
- `id` 是什么
- 入口是否可见
- 是否可点击创建
- 是否需要对话式创作
- 是否需要生成中页面
- 是否需要 result/runtime/gallery/share
不要先随便起临时 ID 再改名。
### Step 2: 新增入口配置
文件:
- `src/config/newWorkEntryConfig.ts`
`NEW_WORK_ENTRY_CONFIG.creationTypes` 中新增或调整:
- `id`
- `title`
- `subtitle`
- `badge`
- `visible`
- `open`
字段语义:
- `visible: true`:在创作页签 / 新建作品入口中展示。
- `visible: false`:不在平台入口展示,但不删除既有玩法路由和能力。
- `open: true`:可点击进入创作流程。
- `open: false`:展示为锁定 / 敬请期待,不应进入创建流程。
如果只是占位:
- `visible: true`
- `open: false`
相关渲染与过滤位置:
- `src/components/platform-entry/platformEntryCreationTypes.ts`:将 `NEW_WORK_ENTRY_CONFIG.creationTypes` 映射为平台入口卡片,`getVisiblePlatformCreationTypes()` 会过滤隐藏项,并把可创建模板排在敬请期待模板前面。
- `src/components/custom-world-home/CustomWorldCreationStartCard.tsx`:创作页签首屏模板入口卡片的实际渲染位置。
- `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`:选择创作类型弹层的渲染位置。
注意:当前项目工作区通常已经是 `<repo-root>`,路径不要再额外拼接 `./Genarrative/`
### Step 3: 确认类型过滤逻辑
文件:
- `./Genarrative/src/components/platform-entry/platformEntryCreationTypes.ts`
检查:
- `getVisiblePlatformCreationTypes()` 是否能展示新类型
- `isPlatformCreationTypeVisible()` 是否能识别新类型
- `locked` / `hidden` 是否正确映射
### Step 4: 扩展页面阶段
文件:
- `./Genarrative/src/components/platform-entry/platformEntryTypes.ts`
为新玩法补充 `SelectionStage`
- `*-agent-workspace`
- `*-generating`(可选)
- `*-result`
- `*-runtime`(可选)
- `*-gallery-detail`(可选)
### Step 5: 在总流程中加类型分流
文件:
- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
`handleCreationHubCreateType(type)` 中新增分支,确保:
- 能进入对应工作台
- 能设置对应 `selectionStage`
- 能关闭类型弹层
同时补:
- `open<Play>AgentWorkspace()`
- `leave<Play>Flow()`
- `submit<Play>Message()`(对话式玩法)
- `execute<Play>Action()`
### Step 6: 接入通用 Agent flow controller
文件:
- `./Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
如果是 Agent 型玩法,复用通用控制器:
- `createSession`
- `getSession`
- `streamMessage`
- `executeAction`
- `isBusy`
- `error`
- `streamingReplyText`
- `selectionStage` 切换
### Step 7: 定义 shared contracts
前端:
- `./Genarrative/packages/shared/src/contracts/`
后端:
- `./Genarrative/server-rs/crates/shared-contracts/src/`
至少补齐:
- session snapshot
- create session request/response
- message request/response
- action request/response
- draft/result 结构
- work summary / gallery 结构(如果需要)
- runtime 结构(如果需要)
### Step 8: 实现前端 service client
目录参考:
- `./Genarrative/src/services/`
按玩法补:
- creation client
- runtime client可选
- works client可选
- gallery client可选
建议保持和现有玩法一致的 API base 与命名风格。
### Step 9: 接后端 API
文件参考:
- `./Genarrative/server-rs/crates/api-server/src/puzzle.rs`
- `./Genarrative/server-rs/crates/api-server/src/puzzle_agent_turn.rs`
- `./Genarrative/server-rs/crates/api-server/src/match3d.rs`
通常需要:
- create session
- get session
- send message
- stream message
- execute action
- publish / save / delete
- runtime start / action可选
- gallery / detail可选
后端设计优先按 Genarrative 的 DDD 分层拆开不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里:
- `module-<play>`纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。
- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。
- `spacetime-module`表定义、reducer/procedure、事务编排、migration表结构变化要同步生成绑定。
- `spacetime-client`api-server 到 SpacetimeDB 的 facade隐藏 reducer 调用细节。
- `api-server`Axum 路由、鉴权、SSE/stream、应用层编排。
- `platform-*`LLM、资产上传、鉴权、第三方服务等副作用。
建议按四条线设计后端能力:
- Agent 创作线session、turn、stream、compile action。
- Works 作品线:保存、发布、删除、草稿恢复。
- Gallery 广场线公开列表、详情、like/remix/share。
- Runtime 运行态线:开始试玩、提交动作、读取状态。
### Step 10: 新增工作台组件
目录建议:
- `./Genarrative/src/components/<play>-creation/<Play>AgentWorkspace.tsx`
两种形态:
#### 对话式
适合设定逐轮补齐。
参考:
- `BigFishAgentWorkspace.tsx`
- `Match3DAgentWorkspace.tsx`
#### 表单式
适合输入结构明确的玩法。
参考:
- `PuzzleAgentWorkspace.tsx`
### Step 11: 在渲染树中挂载新页面
文件:
- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
补齐:
- workspace 分支
- generating 分支(如需要)
- result 分支
- runtime 分支(如需要)
### Step 12: 新增结果页
目录建议:
- `./Genarrative/src/components/<play>-result/<Play>ResultView.tsx`
结果页至少支持:
- 展示 draft
- 返回编辑
- 发布
- 试玩
- 错误展示
### Step 13: 需要试玩就补 runtime
目录建议:
- `./Genarrative/src/components/<play>-runtime/<Play>RuntimeShell.tsx`
如果玩法是游戏类,建议补完整 runtime 闭环。
### Step 14: 接入作品架 / 广场 / 分享
需要改:
- `./Genarrative/src/components/custom-world-home/creationWorkShelf.ts`
- `./Genarrative/src/components/custom-world-home/CustomWorldCreationHub.tsx`
- `./Genarrative/src/services/publicWorkCode.ts`
如果玩法支持发布,还要补:
- public work code
- public detail
- publish share modal
- like/remix可选
### Step 15: 处理登录态与草稿恢复
要考虑:
- 刷新恢复草稿
- 退出登录清空私有状态
- result/draft 缺失时回退
- busy / generating / runtime 中断恢复
### Step 16: 补测试
至少覆盖:
- 入口展示
- 类型分流
- 工作台打开
- session 创建
- compile action
- result 页切换
- 发布后刷新作品架
- runtime 进入与退出
## 最小改动清单
### 只做占位
只改:
- `./Genarrative/src/config/newWorkEntryConfig.ts`
### 做到可进入工作台
至少改:
- `newWorkEntryConfig.ts`
- `platformEntryTypes.ts`
- `PlatformEntryFlowShellImpl.tsx`
- 新玩法 service client
- 新玩法工作台组件
- shared contracts
- 后端 API
### 做到完整闭环
还要补:
- result 页
- runtime
- works / gallery
- public code
- share
- 作品架聚合
- 测试
## 常见坑
1. 只加入口配置不够,类型分流和页面阶段也要补。
2. `SelectionStage` 不扩展,前端无法安全切页。
3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。
4. 发布后不刷新 works/gallery用户会看不到新作品。
5. 如果走 SpacetimeDB表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。
6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤RED 后再补领域类型与聚合函数。
7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucketbucket 输出要有稳定排序,并显式携带 `bucketKey``bucketStartDateKey``bucketEndDateKey``value`
8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type unionadmin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。
9. 退出登录时要清空新玩法私有状态,避免串用户。
10. 移动端入口卡片增多后要检查布局和滚动体验。
## 参考资料
- `references/genarrative-analytics-tracking-runtime.md`analytics/tracking runtime 粒度聚合、contracts 同步与 SpacetimeDB 生成物注意事项。
## 验证标准
一个玩法算真正接入成功,至少要满足:
- 入口能展示
- 能进入对应工作台
- 能创建 session
- 能生成草稿
- 能进入结果页
- 能返回编辑
- 如果需要,可试玩
- 如果需要,可发布
- 发布后能回到作品架 / 广场 / 分享链路

View File

@@ -0,0 +1,57 @@
# Genarrative analytics/tracking runtime 接入经验
本参考来自 analytics time dimension / tracking daily stat 粒度聚合实现与一次“只提交了 bindings/tests、后端链路未补齐”的纠偏。
## 推荐顺序
1. 先读仓库 `README.md``AGENTS.md`、相关 `/docs/technical``.hermes/plans`,确认当前阶段范围。
2. 遵循 TDD先在 `server-rs/crates/module-runtime/tests/` 写纯函数测试,验证缺失类型/函数导致 RED。
3.`module-runtime/src/domain.rs` 增加领域类型,例如:
- `AnalyticsGranularity``day | week | month | quarter | year`
- daily stat snapshot
- bucket metric
- query request/response/input
- Spacetime procedure result例如 `AnalyticsMetricQueryProcedureResult { ok, buckets, error_message }`),否则 module procedure 无法生成 client bindings。
4.`module-runtime/src/application.rs` 复用已有 `build_analytics_date_dimension_from_date_key`,实现 daily stat 到 day/week/month/quarter/year bucket 的聚合。
5. 输出 bucket 应稳定排序,可用 `BTreeMap`;聚合前按 `event_key + scope_kind + scope_id` 过滤。
6.`module-runtime/src/commands.rs` 增加 query input builder复用现有字段错误如 missing event key、missing scope id
7. 同步 contracts
- Rust`server-rs/crates/shared-contracts/src/runtime.rs`
- 前端共享:`packages/shared/src/contracts/runtime.ts`
- 管理端若有本地 API 类型,也同步 `apps/admin-web/src/api/adminApiTypes.ts`
8. 接 Spacetime module 源头:在 `server-rs/crates/spacetime-module/src/runtime/profile.rs` 增加只读 procedure例如 `query_analytics_metric`),从 `tracking_daily_stat` 读 rows映射成 `RuntimeAnalyticsDailyStatSnapshot` 后调用领域聚合函数。
9. 重新生成 `server-rs/crates/spacetime-client/src/module_bindings/`。如果当前环境没有 `spacetime`/`spacetimedb` CLI可临时按现有生成物风格手补 type/procedure/mod.rs但必须在文档和最终说明中标注“临时手补生成物”并要求后续在有 CLI 的机器用项目脚本重新生成覆盖。
10.`spacetime-client`
- `src/mapper.rs`module binding procedure result → `module_runtime` record/response补 tracking scope/granularity 映射。
- `src/runtime.rs`:新增 facade 方法(例如 `SpacetimeClient::query_analytics_metric`),调用生成的 procedure。
- `src/lib.rs`:若 facade/mapper 需要领域类型或 builder补导入注意同名 binding 类型会造成误解析。
11.`api-server`
- `src/runtime_profile.rs`Query params / parser / handler / response builder。
- `src/app.rs`:挂路由,例如 profile 或 admin analytics endpoint选择路径前确认产品定位。
12. 最后更新 docs/plan并确认 diff 不只是生成物。
## 验证命令示例
```bash
cd <repo-root>/server-rs
cargo test -p module-runtime --test analytics_granularity
cargo check -p shared-contracts
cargo check -p spacetime-module
cargo check -p spacetime-client
cargo check -p api-server
```
If terminal output is compacted by the tool, rerun the specific command directly (without `head`) or capture full output before concluding; `cargo check` exit code 0 with warnings is acceptable when warnings are pre-existing and documented.
## 坑
- 不要手改 `server-rs/crates/spacetime-client/src/module_bindings/` 生成物;若缺 procedure/type回源头改 Spacetime module 后重新生成。若当前环境没有 `spacetime`/`spacetimedb` CLI 且必须临时手补生成物,要在最终说明中明确这是临时替代,并尽快在有 CLI 的环境重新生成。
- `spacetime-client/src/runtime.rs` 同时能看到 `module_bindings::*` 和领域层 `module_runtime` 类型。新增 facade 方法参数要使用领域别名(如 `DomainRuntimeTrackingScopeKind``module_runtime::AnalyticsGranularity`),不要误用 binding 里的 `RuntimeTrackingScopeKind`;否则 `build_*_input` 会报 “expected module_runtime::..., found binding ...”。
- 如果缺少 SpacetimeDB CLI手补 bindings 的最小集合通常包括:`*_type.rs`input、enum、bucket、procedure result`*_procedure.rs``module_bindings/mod.rs``pub mod`/`pub use`/procedure re-export。完成后必须跑 `cargo check -p spacetime-client` 验证 SDK trait、procedure 名称与 result 字段是否匹配。
- `spacetime-client/src/mapper.rs` 新增 query 链路时通常要同时补四类映射:领域 input → binding input、binding enum 映射、binding procedure result → 领域 response、binding bucket/item → 领域 bucket/item。只在 `runtime.rs` 加 facade 会出现 unused import 或类型不匹配。
- 修改 Rust import 时注意 `serde::Deserialize``serde_json` 的排序/使用:如果只加了 `Query`/`Deserialize` 但 handler 尚未实现,`cargo check -p api-server` 会暴露 unused import不要把 import 作为完成信号。
- 只补 shared-contracts 不够;`packages/shared` 和 admin-web 本地类型可能各有一份。
- 周粒度应按 ISO week/date dimension而不是简单 `day_key / 7`
- bucket response 建议显式包含 start/end date key避免前端再推导时间边界。
- `spacetime-module` 可能能编译通过,但如果没有重新生成 bindings`spacetime-client` 仍不会有新 procedure 方法;必须以 client facade/API handler 可调用为完成标准。
- api-server 中新增 `Query``Deserialize` 等 import 后要立即补 handler否则容易留下 unused import。

View File

@@ -0,0 +1,99 @@
---
name: genarrative-profile-features
description: 在 Genarrative “我的”页签新增或修改个人中心入口、独立 profile 路由、反馈/记录/设置类页面时使用。
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [Genarrative, profile, 我的页签, 前端, 路由, 反馈]
related_skills: [writing-plans, test-driven-development]
---
# Genarrative “我的”页签功能接入
用于在 Genarrative 平台“我的”页签新增或修改入口,以及把入口接到独立页面阶段/路由。例如:帮助与反馈、反馈记录、个人设置、账号相关轻量页面。
## 适用场景
- 在“我的”页签新增快捷入口或卡片按钮。
- 点击入口后进入独立页面,而不是在当前面板下方展开内容。
- 新增 `/profile/...` 路由或 `SelectionStage`
- 新增移动端优先的个人中心子页面组件。
- 修改 `RpgEntryHomeView``PlatformEntryFlowShellImpl``appPageRoutes` 等前端 profile 链路。
## 必读约束
1. 按项目约束:先检查/补齐文档,再落地工程修改。
2. UI 面板保持清爽,不要默认堆功能说明文案。
3. 点击按钮弹出/进入独立面板的设计,不要实现成在当前面板下方追加内容。
4. 移动端优先,同时兼顾网页端容器宽度。
5. 包含中文的文件优先局部补丁,修改后运行编码检查。
6. 非必要不新建系统;优先复用现有平台入口、阶段和路由机制。
## 代码接入路径
常见文件:
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
- “我的”页签 UI 主入口通常在此。
- 新增入口时优先扩展 props例如 `onOpenFeedback?: () => void`
- 在现有快捷入口区新增 `ProfileShortcutButton`保持图标、label、subLabel 风格一致。
- `src/components/platform-entry/platformEntryTypes.ts`
- 若需要独立页面阶段,扩展 `SelectionStage` union。
- 例如新增 `'profile-feedback'`
- `src/routing/appPageRoutes.ts`
-`STAGE_ROUTE_ENTRIES` 添加 `/profile/...` 路由映射。
- 验证 `resolveSelectionStageFromPath()``resolvePathForSelectionStage()` 双向一致。
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- 引入新页面组件。
- 新增打开函数:必要时先检查登录态,未登录调用 `authUi?.openLoginModal()`
- 打开 profile 子页时同步 `setPlatformTab('profile')`,再 `setSelectionStage(...)`
-`selectionStage` 直接由路由进入 profile 子页时,用 `useEffect` 同步当前 tab 到 `profile`
- 在主渲染分支中为新阶段渲染独立 `<motion.div>` 页面;返回时回到 `platform` 阶段并保持 `profile` tab。
- `src/components/platform-entry/<FeatureView>.tsx`
- 页面组件可放在 platform-entry 下,与 shell 阶段渲染保持一致。
- 表单首版没有后端接口时,可通过可选 `onSubmit` prop 暴露提交 payload并在组件内展示成功/失败态;注释说明后续替换为 API 调用。
## 推荐实施顺序
1. 读取 `.hermes/plans/...` 或产品文档,确认入口、路由、页面行为。
2. 若现有文档不足,先在 `docs/prd/` 增加可编码落地的 PRD。
3. 增加 `SelectionStage``appPageRoutes` 映射,并先跑 `npm run typecheck`
4. 新建独立页面组件,尽量通过 props 暴露 `onBack`/`onSubmit`,避免直接耦合全局状态。
5.`RpgEntryHomeView.tsx` 增加入口 prop 与按钮。
6.`PlatformEntryFlowShellImpl.tsx` 串联导航、登录态、阶段渲染和返回逻辑。
7. 增加基础测试:路由解析、页面字段渲染、关键交互/校验、返回按钮。
8. 跑编码检查、类型检查和相关 vitest。
9. 分阶段 commit用户要求更新工作区时再 push。
## 测试与验证命令
常用命令:
```bash
npm run check:encoding
npm run typecheck
npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/<FeatureView>.test.tsx
# 或项目脚本:
npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/<FeatureView>.test.tsx
```
如果新增/修改中文文档或中文 UI`check:encoding` 必跑。
## 参考案例
- `references/profile-feedback-entry-2026-05-08.md`帮助与反馈入口案例覆盖文档、路由阶段、独立页面组件、“我的”页签按钮、shell 导航、测试和验证命令。
## 常见坑
1. 只在 `RpgEntryHomeView` 新增按钮但没有接 shell 导航,导致点击无效果。
2. 只新增 `SelectionStage` 但忘记 `appPageRoutes`,导致刷新/直达路由不能恢复页面。
3. 直达 `/profile/...` 时没有同步 `setPlatformTab('profile')`,底部 tab 状态与页面不一致。
4. 把反馈/设置表单插到“我的”面板下方,违背独立页面体验。
5. 没有测试 `resolveSelectionStageFromPath`/`resolvePathForSelectionStage`,后续路由改动容易回归。
6. 中文页面或文档改动后忘记编码检查。

View File

@@ -0,0 +1,80 @@
# 帮助与反馈入口案例2026-05-08
本案例来自 Genarrative “我的”页签新增反馈入口与独立反馈页实现。
## 目标
- 在“我的”页签新增“反馈”快捷入口。
- 点击后进入独立路由 `/profile/feedback`
- 页面按移动端参考图实现“帮助与反馈”表单:问题描述、上传凭证、联系电话、提交、查看反馈与投诉记录。
## 关键文件
- `docs/prd/PROFILE_FEEDBACK_ENTRY_PRD_2026-05-08.md`
- `src/components/platform-entry/platformEntryTypes.ts`
- `src/routing/appPageRoutes.ts`
- `src/components/platform-entry/PlatformFeedbackView.tsx`
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- `src/routing/appPageRoutes.test.ts`
- `src/components/platform-entry/PlatformFeedbackView.test.tsx`
## 落地顺序
1. 先补 PRD避免从参考图直接猜代码细节。
2.`SelectionStage` 中新增 `'profile-feedback'`
3.`STAGE_ROUTE_ENTRIES` 添加:
- `['profile-feedback', '/profile/feedback']`
4. 新建 `PlatformFeedbackView`
- `onBack: () => void`
- `onSubmit?: (payload) => void | Promise<void>`
- 内部维护描述、联系电话、上传图片预览、错误态、成功态。
- 首版没有后端接口时,`onSubmit` 为后续 API 接入点。
5.`RpgEntryHomeViewProps` 添加 `onOpenFeedback?: () => void`
6. 在“我的”页签快捷入口区新增 `ProfileShortcutButton`
- `label="反馈"`
- `subLabel="帮助与反馈"`
- `onClick={onOpenFeedback}`
7.`PlatformEntryFlowShellImpl`
- import `PlatformFeedbackView`
- 新增 `openProfileFeedback`
- 未登录则 `authUi?.openLoginModal()`
- 已登录则 `setPlatformTab('profile')` + `setSelectionStage('profile-feedback')`
- 直达阶段时 `useEffect` 同步 `profile` tab
- 渲染 `selectionStage === 'profile-feedback'` 的独立 `<motion.div>`
8. 增加测试:
- 路由双向解析
- 页面字段渲染
- 描述低于 10 字不提交
- 提交时 trim payload
- 顶部返回按钮调用 `onBack`
## UI 参考要点
- 移动端优先,页面最大宽度约 30rem。
- 浅灰背景,白色圆角卡片。
- 顶部标题:`帮助与反馈`
- 分区标题:`反馈问题`
- 问题描述 placeholder提示填写 10 字以上,勿填身份证号等隐私。
- 字数计数:`0/200`
- 上传凭证:最多四张,上传占位显示 `上传凭证``(最多四张)`
- 联系电话:选填。
- 主按钮:蓝色圆角 `提交`
- 底部链接:`查看反馈与投诉记录`
## 验证命令
```bash
npm run check:encoding
npm run typecheck
npx vitest run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
# 或
npm run test -- --run src/routing/appPageRoutes.test.ts src/components/platform-entry/PlatformFeedbackView.test.tsx
```
## 已踩坑/经验
- `appPageRoutes.test.ts` 若已有历史内容,写入测试时注意不要误删旧用例;最终 commit 里出现 “insertions + deletions” 时要检查是否覆盖了既有测试。
- 页面组件里的图片预览需要在移除或卸载时 `URL.revokeObjectURL`,避免泄漏。
- 对隐藏 file input 的测试可以先不强行覆盖,首版覆盖表单字段、校验和 payload 更稳定。
- 如果用 `authUi` 对象作为 callback 依赖,需确认引用稳定性;现有 shell 中可接受,但复杂化时考虑拆出具体字段依赖。

View File

@@ -0,0 +1,149 @@
---
name: genarrative-profile-invite-flow
description: 在 Genarrative 中排查或修改邀请码、邀请好友、首次登录后填写邀请码、我的页签邀请码兑换链路时使用。
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [Genarrative, 邀请码, referral, auth, profile, query-params, 前端]
related_skills: []
---
# Genarrative 邀请码与邀请好友流程
用于排查或修改 Genarrative 的邀请码读取、填写、兑换、邀请中心与“我的”页签相关能力。
## 适用场景
- 判断 URL query 参数中的邀请码是否被读取。
- 修改邀请码填写入口、首次登录后引导或“我的”页签兑换入口。
- 排查邀请码预填、兑换、已填写状态、邀请好友复制链接。
- 修改邀请中心 API client 或前端 referral UI。
- 回答用户关于“邀请码在哪里填 / 从哪里配置 / query 是否支持”的问题。
## 先做代码核对,不要只凭旧记忆回答
邀请码流程近期发生过迁移:不要默认认为登录窗口可填写邀请码。回答前优先搜索并核对当前代码,尤其是:
```bash
cd <repo-root>
python3 - <<'PY'
from pathlib import Path
root=Path('src')
terms=['RegistrationInviteModal','readInviteCodeFromLocation','referralRedeemCode','redeemRpgProfileReferralInviteCode','邀请码','inviteCode']
for term in terms:
print('\n---', term)
for p in root.rglob('*'):
if p.is_file() and p.suffix in ['.ts', '.tsx']:
try:
txt=p.read_text('utf-8')
except Exception:
continue
if term in txt:
for i, line in enumerate(txt.splitlines(), 1):
if term in line:
print(f'{p}:{i}:{line.strip()[:180]}')
```
## 当前前端链路口径
### 1. AuthGate 中仍有旧 query 读取逻辑
文件:
- `src/components/auth/AuthGate.tsx`
重点函数 / 状态:
- `readInviteCodeFromLocation()`
- `pendingInviteCode`
- `showRegistrationInviteModal`
- `RegistrationInviteModal`
当前旧逻辑会读取:
- `?inviteCode=...`
- `?invite_code=...`
并把值清洗为大写字母数字形式。
### 2. 登录窗口本身不再填写邀请码
不要回答“登录窗口可填写邀请码”。当前登录弹窗 `LoginScreen` 只负责登录 / 注册账号;邀请码填写已迁移到登录后的流程。
### 3. 新版“我的”页签兑换入口在 RpgEntryHomeView
文件:
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
重点常量 / 函数 / 状态:
- `PROFILE_INVITE_QUERY_KEYS`:新版 query 支持 `inviteCode` / `invite_code`
- `normalizeProfileInviteQueryCode()`:去掉非字母数字并转大写。
- `readProfileInviteCodeFromLocationSearch()`:从 `window.location.search` 读取并 normalize。
- `pendingProfileInviteCode`:组件初始化时读取 query 邀请码。
- `referralCenter`
- `referralRedeemCode`
- `setReferralRedeemCode`
- `openProfilePopupPanel('redeem')`
- `submitReferralRedeemCode()`
- `canShowReferralRedeemShortcut`
- `isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt)`
UI 中“填邀请码”面板会使用 `referralRedeemCode` 作为输入值,并通过 `submitReferralRedeemCode()` 提交。当前新版实现会在首次打开“填邀请码”面板时用 `pendingProfileInviteCode` 预填输入框;例如 `/?inviteCode=spring-2026` 会预填为 `SPRING2026`
### 4. 新版兑换 API client
文件:
- `src/services/rpg-entry/rpgProfileClient.ts`
函数:
- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center`
- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code`
## 判断 query 参数是否真正接入新版流程
回答这类问题时要区分两层:
1. “是否存在旧 query 读取代码”:看 `AuthGate.tsx``readInviteCodeFromLocation()`
2. “query 是否接到新版填写入口”:看 `RpgEntryHomeView.tsx` 是否存在 `pendingProfileInviteCode` / `readProfileInviteCodeFromLocationSearch()`,以及打开 `openProfilePopupPanel('redeem')` 时是否把该值写回 `referralRedeemCode`
当前新版流程已经支持 `inviteCode` / `invite_code` query 预填“我的”页签的“填邀请码”弹窗;登录窗口仍不填写邀请码。
如果未来代码只看到 AuthGate 读 query但没有看到 `RpgEntryHomeView``referralRedeemCode` 从 query 初始化,就应回答:
> 代码里仍支持读取 `inviteCode` / `invite_code`,但新版“第一次登录后 / 我的页签”的填写入口未必已经完整接入该 query 值;需要继续把 query 值传入新版 profile referral redeem 流程。
## 修改建议顺序
如果要把 query 邀请码完整接入新版流程,建议按这个顺序做:
1. 先确定 query 参数规范:继续支持 `inviteCode` / `invite_code`,并统一 normalize。
2.`RpgEntryHomeView.tsx` 内用 `readProfileInviteCodeFromLocationSearch(window.location.search)` 初始化 `pendingProfileInviteCode`
3.`pendingProfileInviteCode` 初始化 `referralRedeemCode`,并在 `openProfilePopupPanel('redeem')` 时重新写回,避免关闭后再次打开被清空。
4. 如产品要求自动弹出:
-`pendingProfileInviteCode` 且未登录时,自动调用 `authUi?.openLoginModal()` 打开登录窗口;登录窗口仍不承接邀请码输入。
-`pendingProfileInviteCode` 且已登录时,自动将 `referralRedeemCode` 设为该 query 邀请码,并 `setProfilePopupPanel('redeem')` 直接打开“填邀请码”面板。
-`useRef` 记录是否已处理过当前 query避免组件重渲染或 `authUi` 对象变化导致重复弹窗。
- 当前项目实现已从“只预填、不自动弹”调整为上述行为。
5. 兑换成功后清理输入态;是否清理 URL query 需由产品决定,避免破坏分享链接归因。
6. 补测试覆盖:未登录访问带 query、已登录访问带 query 自动打开填写面板、我的页签手动打开、已填写邀请码、过期窗口、空/非法 query。当前已有 `RpgEntryHomeView.recharge.test.tsx` 覆盖:
- `invite query opens login modal for logged out users`
- `invite query opens redeem modal directly for logged in users`
- `profile redeem invite modal reads query invite code after login`
## 常见坑
1. 不要把旧 `RegistrationInviteModal` 误认为当前唯一入口。
2. 不要说“登录窗口可以填写邀请码”,除非当前代码重新把邀请码输入放回 `LoginScreen`
3. `AuthGate` 读到 query 不等于新版 `RpgEntryHomeView` 已经预填。
4. “第一次登录后”与“我的页签”可能是两个入口;修改时要同时检查自动引导和手动入口。
5. `canShowReferralRedeemShortcut` 受登录态、创建时间窗口、邀请中心初始化、已兑换状态共同影响。
6. 邀请码 URL 通常由 `inviteLinkPath` 生成,复制逻辑在 `copyInviteInfo()`,不要只改兑换入口而忘记分享链接格式。
## 参考资料
- `references/query-invite-code-flow-2026-05-07.md`:本次会话确认的邀请码 query 与新版 profile referral 入口关系。
## 验证标准
- 能明确回答当前 query 参数读取位置与参数名。
- 能区分旧 AuthGate 邀请弹窗与新版“我的”页签 referral redeem。
- 若实现改动,测试覆盖带 query 的登录后预填/弹窗行为,以及已填写邀请码时不再提示。

View File

@@ -0,0 +1,101 @@
# Query 邀请码与新版 profile referral 入口关系2026-05-07
## 会话结论
用户指出:邀请码填写流程已经修改,登录窗口目前填写不了邀请码;邀请码填写被挪到了第一次登录后以及“我的”页签中。
因此后续回答或修改时不能只根据 `AuthGate` 里的旧逻辑判断“已支持”。
## 当前代码观察
### 旧 AuthGate 逻辑
文件:`src/components/auth/AuthGate.tsx`
- `readInviteCodeFromLocation()` 读取 `window.location.search`
- 支持 `inviteCode` / `invite_code`
- 会 normalize 为大写字母数字。
- 写入 `pendingInviteCode`,传给 `RegistrationInviteModal`
这只能说明“旧层仍有 query 读取”。
### 新版 profile referral 入口
文件:`src/components/rpg-entry/RpgEntryHomeView.tsx`
- “我的”页签标签为 `profile`
- 兑换输入状态:`referralRedeemCode`
- 打开填邀请码面板:`openProfilePopupPanel('redeem')`
- 提交兑换:`submitReferralRedeemCode()`
- 可显示快捷入口受 `canShowReferralRedeemShortcut` 控制。
文件:`src/services/rpg-entry/rpgProfileClient.ts`
- `getRpgProfileReferralInviteCenter()` -> `GET /profile/referrals/invite-center`
- `redeemRpgProfileReferralInviteCode(inviteCode)` -> `POST /profile/referrals/redeem-code`
## 回答口径
如果被问“是否支持 query 参数读取邀请码”,应回答:
- 代码里仍有 query 读取,支持 `inviteCode` / `invite_code`
- 但登录窗口不再填写邀请码。
- 新版入口在第一次登录后 / 我的页签;需要检查 `referralRedeemCode` 是否从 query 初始化。
- 若没有该连接,就不能说新版流程完整支持 query 预填。
## 本次实现后的状态
已将 query 邀请码读取接入新版 `RpgEntryHomeView`
- `PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code']`
- `normalizeProfileInviteQueryCode()`:去除非字母数字并转大写。
- `readProfileInviteCodeFromLocationSearch(window.location.search)`:读取 query 邀请码。
- `pendingProfileInviteCode`:组件初始化时读取 query。
- `referralRedeemCode`:用 `pendingProfileInviteCode` 初始化。
- `openProfilePopupPanel('redeem')`:打开“填邀请码”时重新写入 `pendingProfileInviteCode`,避免首次打开或重新打开时丢失 query 预填。
当前行为:只预填,不自动弹出“填邀请码”面板;用户仍需在“我的”页签点击“填邀请码”。
验证测试:
```bash
cd <repo-root>
npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0
node scripts/check-encoding.mjs
```
测试用例:`profile redeem invite modal reads query invite code after login` 覆盖 `/?inviteCode=spring-2026` 预填为 `SPRING2026`
## 后续调整:带邀请码链接自动开窗
用户进一步明确期望:
- 如果用户未登录,直接打开登录窗口。
- 如果用户已登录,直接打开邀请码填写窗口。
实现要点:
-`RpgEntryHomeView.tsx` 中保留 `pendingProfileInviteCode` 作为 query 邀请码来源。
- 新增 `autoOpenedInviteQueryRef = useRef(false)`,防止 effect 重复触发弹窗。
- 新增 `useEffect`
- 无 query 邀请码或已处理过则 return。
- 未登录:调用 `authUi?.openLoginModal()`
- 已登录:设置 `referralRedeemCode`、清空 referral 错误/成功提示、`setProfilePopupPanel('redeem')`
- 登录窗口仍不接收邀请码;邀请码只在登录后的 profile referral redeem 面板显示。
- 仍然只自动打开和预填,不自动提交兑换。
补充测试:
- `invite query opens login modal for logged out users`
- `invite query opens redeem modal directly for logged in users`
-`profile redeem invite modal reads query invite code after login` 同步调整为直接断言自动打开后的输入值。
验证命令仍为:
```bash
cd <repo-root>
npm test -- --run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx --max-warnings=0
node scripts/check-encoding.mjs
```