diff --git a/.hermes/skills/genarrative-admin-backoffice/SKILL.md b/.hermes/skills/genarrative-admin-backoffice/SKILL.md new file mode 100644 index 00000000..86881118 --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/SKILL.md @@ -0,0 +1,231 @@ +--- +name: genarrative-admin-backoffice +short_description: 在 Genarrative/百梦后台新增或修改管理页、后台只读/写接口、导出能力时使用。 +description: 在 Genarrative/百梦后台新增或修改管理页、后台 BFF 接口、shared-contracts/admin DTO、admin-web 路由导航、Excel/表格导出与验证发布时使用。 +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [Genarrative, 百梦后台, admin-web, 后台接口, Excel导出, Rust, Axum, SpacetimeDB] + related_skills: [genarrative-play-type-integration] +--- + +# Genarrative / 百梦后台管理功能接入流程 + +用于在 Genarrative 项目中新增或修改百梦后台管理端能力,包括后台页面、后台 API、管理端 DTO、导航路由、表格明细、导出、鉴权与验证。 + +## 适用场景 + +- 新增百梦后台页面或导航项,例如“埋点数据”“任务配置”“邀请码”。 +- 新增 `/admin/api/*` 接口。 +- 修改 `apps/admin-web` 的后台页面、API client、路由、Shell 导航。 +- 在后台展示 SpacetimeDB 表明细或统计数据。 +- 新增“总览 → 单表查询”这类表统计跳转与查询页联动能力时,优先复用现有总览页的表统计作为入口,不另造第二套表目录。 +- 后台导出 CSV / Excel / `.xls` 表格文件。 +- 后台数据页中与业务事件、任务、登录等链路相关的问题,不能只看后台页面;要追到对应前台/API/reducer 写入点,确认“数据何时产生”。例如排查 `daily_login` 时,不要假设它一定由认证登录接口写入;先核对当前分支实现。历史实现曾在 `GET /api/profile/tasks` 打开任务中心时写入、`POST /api/profile/tasks/{task_id}/claim` 领奖时兜底写入;后续方案A把“任务中心读取写埋点”拆出为独立 procedure,任务中心只读取/刷新进度,登录成功链路应显式调用每日登录埋点入口。 + +## 标准落地顺序 + +### 0. 先确认现有后台入口 + +在新增后台页或回答“后台某个数据在哪里”前,先核对是否已有入口,避免重复造页: + +- 数据库表统计当前在后台“总览”页,不是独立页面:`apps/admin-web/src/pages/AdminOverviewPage.tsx` 的“表统计”面板。 +- 表统计行可直接跳转到表查询页:点击后设置 `window.location.hash = #tables?table=`,由单独的 `#tables` 页接收参数并查询。 +- `#tables` 页应在首次加载和 `hashchange` 时都重新读取 `table` 参数,避免只在初次 mount 时生效。 +- 前端通过 `apps/admin-web/src/api/adminApiClient.ts` 的 `getAdminOverview(token)` 请求 `GET /admin/api/overview`。 +- 后端路由在 `server-rs/crates/api-server/src/app.rs` 挂载 `/admin/api/overview`,handler 为 `admin_overview`。 +- 表统计逻辑在 `server-rs/crates/api-server/src/admin.rs` 的 `fetch_database_overview`:先读 SpacetimeDB schema 表名,再逐表执行 `SELECT COUNT(*) AS row_count FROM {table_name}`;private 或当前身份不可见会显示“不可统计(private 或当前身份不可见)”。 +- DTO 在 `server-rs/crates/shared-contracts/src/admin.rs` 的 `AdminOverviewResponse` / `AdminDatabaseOverviewPayload` / `AdminDatabaseTableStatPayload`,前端对应类型在 `apps/admin-web/src/api/adminApiTypes.ts`。 +- 如果本次需求是“每张表都能查”,优先新增 `GET /admin/api/database/tables` 与 `GET /admin/api/database/tables/{tableName}/rows` 两个只读接口,并在前端新建统一的表查询页,而不是把查询逻辑塞回总览页。 + +### 1. 先补技术方案文档 + +项目要求工程修改前先检查/补充落地文档。若没有明确文档,先写到 `docs/technical/`,至少说明: + +- 后台页面目标。 +- 后端接口路径、鉴权、query/body、response。 +- 数据来源和是否修改 SpacetimeDB schema。 +- 前端页面字段、筛选项、导出格式。 +- 验收命令。 + +示例参考: + +- `references/admin-tracking-events-export-2026-05-07.md` +- `references/admin-database-table-query-2026-05-08.md` + +### 2. 后端 DTO 放 shared-contracts/admin + +文件: + +- `server-rs/crates/shared-contracts/src/admin.rs` + +做法: + +- 新增 request/query/response DTO。 +- 使用 `#[serde(rename_all = "camelCase")]`。 +- 添加中文注释。 +- 字段名与前端管理端类型保持一致。 + +如果 `apps/admin-web` 当前没有直接消费 Rust shared-contracts 生成物,还要同步: + +- `apps/admin-web/src/api/adminApiTypes.ts` + +### 3. 后端 handler 放 api-server/admin.rs + +文件: + +- `server-rs/crates/api-server/src/admin.rs` +- `server-rs/crates/api-server/src/app.rs` + +要求: + +- Handler 使用 `Extension(_admin): Extension`,并在 router 中套 `require_admin_auth`。 +- 只读接口也必须走后台鉴权。 +- query 参数使用 `Query`。 +- 返回 `json_success_body(Some(&request_context), payload)`。 +- 在 `app.rs` 挂到 `/admin/api/...`。 + +### 4. 读取 SpacetimeDB 表明细时优先 HTTP SQL 只读 + +适合后台只读运营页: + +- 不改表结构。 +- 不新增 reducer。 +- API Server 通过 SpacetimeDB HTTP SQL 读取真实数据。 + +注意: + +- SQL 字段固定白名单,不要 `SELECT *`。 +- 用户输入只允许有限筛选字段,手动 trim、白名单枚举、字符串转义。 +- limit 必须 clamp,例如默认 200、最大 1000。 +- SpacetimeDB 2.2 HTTP SQL 不支持 `ORDER BY`;如果后台需要倒序展示明细,SQL 中不要拼 `ORDER BY`,先查有限 `LIMIT`,再在 api-server 内按时间字段排序,否则会返回 `HTTP 400 Unsupported: SELECT ... ORDER BY ... LIMIT ...`。 +- 如果 HTTP SQL 返回 `no such table ... If the table exists, it may be marked private`,不要急着改表名或新增 reducer;先确认本地 CLI 是否以当前 standalone 的 identity/token 登录。清空本地数据库或重建 standalone 后,旧 CLI token 可能看不到 private table。按“本地 private table SQL 权限修复”流程用 `/v1/identity` 获取 token,再 `spacetime login --token` 登录。 +- SQL 解析要兼容 SpacetimeDB HTTP SQL 的 statement array + rows 形态。 +- SpacetimeDB HTTP SQL 读取 private table 时,enum / Option / Timestamp 可能以 SATS 原始 JSON 返回,例如 `scope_kind=[3,[]]`、`Some("user")=[0,"user"]`、`None=[1,[]]`、`Timestamp=[1778207451731746]`。后台列表、详情弹窗和 Excel 导出不要直接展示这些原始形态;应在 api-server 解析层或前端展示层转换为人可读值:enum 映射为业务字符串,Option 的 None 显示 `-`,微秒级 Timestamp 格式化为本地可读时间。 +- 可复用已有 `/v1/database/{db}/sql` 请求风格和 token 配置。 + +### 5. 前端接入 admin-web + +常改文件: + +- `apps/admin-web/src/api/adminApiTypes.ts` +- `apps/admin-web/src/api/adminApiClient.ts` +- `apps/admin-web/src/app/adminRoutes.ts` +- `apps/admin-web/src/app/AdminShell.tsx` +- `apps/admin-web/src/app/AdminApp.tsx` +- `apps/admin-web/src/pages/.tsx` +- `apps/admin-web/src/styles/admin.css` + +接入步骤: + +1. 在 `adminApiTypes.ts` 增加 query/entry/list 类型。 +2. 在 `adminApiClient.ts` 增加 API 方法;用 `URLSearchParams` 拼非空 query。 +3. 在 `adminRoutes.ts` 增加 route id、label、hash。 +4. 在 `AdminShell.tsx` 增加 route icon,`routeIcons` 必须覆盖全部 `AdminRouteId`。 +5. 在 `AdminApp.tsx` import 并按 routeId 渲染页面。 +6. 新增页面组件,保持 UI 简洁,不写大段规则说明。 +7. 如果页面通过 hash 携带子参数,路由解析和页内参数解析要分开:`resolveAdminRoute()` 只负责路由片段,页面组件自己解析 `?table=` 之类的查询参数;同时要监听 `hashchange`,避免切页后参数不同步。 +8. 列表行点击跳转优先用 hash,不要额外引入全局路由库或重新发明一套页面状态系统。 + +## Excel 导出推荐做法 + +后台运营导出不一定要引入 `xlsx` 依赖;简单表格可用浏览器端 HTML table + `.xls`: + +- Blob MIME:`application/vnd.ms-excel;charset=utf-8` +- 文件扩展名:`.xls` +- 文本前加 UTF-8 BOM / ``。 +- 所有单元格做 HTML escape。 +- ID、大数字、日期类字段使用 `mso-number-format:'\@';` 保持文本格式,避免 Excel 科学计数法。 +- 导出当前筛选结果,避免后端新增 Excel 库依赖。 + +## 本地启动与联调 + +后台改完后如需本地查看页面和接口,优先按本次联调范围选择脚本: + +```bash +# 只看后台页面 + api-server,不要求 SpacetimeDB 真实数据 +npm run api-server +npm run admin-web:dev -- --host 127.0.0.1 + +# 完整 Rust 本地栈:SpacetimeDB + 发布模块 + api-server + 主站 + 后台 +npm run dev +``` + +验证地址通常为: + +- `npm run api-server` 单独启动:api-server `http://127.0.0.1:3100/healthz`,后台前端 `http://127.0.0.1:5173/admin/`。 +- `npm run dev` 完整栈:SpacetimeDB `http://127.0.0.1:3101/v1/ping`,api-server `http://127.0.0.1:8082/healthz`,主站 `http://127.0.0.1:3000/`,后台 `http://127.0.0.1:3102/admin/`。 + +注意: + +- `npm run api-server` 首次启动可能先编译 Rust,后台进程短时间内无完整日志;等待编译完成后再查端口。 +- 不要默认用 `3200` 验证 api-server;当前脚本环境变量常见为 `GENARRATIVE_API_PORT=3100`。不确定时用 `ss -ltnp | grep api-server` 或读取进程环境核对,敏感值输出必须打码。 +- `admin-web` 的 `/` 可能返回 302 跳转到 `/admin/`;验证前端时直接请求 `/admin/`。 +- api-server 启动日志中 SpacetimeDB `127.0.0.1:3101` 连接被拒绝,不一定代表 api-server 没起来;只表示依赖的本地 SpacetimeDB 不可用。后台中需要读 SpacetimeDB 的页面(如埋点明细、表查询)要等 SpacetimeDB 可用后才能返回真实数据。 +- `npm run dev` 依赖 `spacetime` CLI;先用 `command -v spacetime && spacetime --version` 确认可用。 +- WSL/Linux 下 SpacetimeDB CLI 2.2.0 使用项目内 `--root-dir` 时,standalone 可能会回调 `${root_dir}/bin/current/spacetimedb-cli`。如果报 `It seems like the spacetime version set as current may not exist` 或 `exec failed for .../.spacetimedb/local/bin/current/spacetimedb-cli`,把用户级安装同步到项目 root-dir: + +```bash +rm -rf server-rs/.spacetimedb/local/bin +mkdir -p server-rs/.spacetimedb/local/bin +cp -a ~/.local/share/spacetime/bin/2.2.0 server-rs/.spacetimedb/local/bin/2.2.0 +ln -sfn 2.2.0 server-rs/.spacetimedb/local/bin/current +server-rs/.spacetimedb/local/bin/current/spacetimedb-cli --version +``` + +- `scripts/dev-rust-stack.sh` 默认 `api timeout: 300s`. 合并 master 后首次 Rust 依赖/工作区重编译可能超过 300s,导致完整 `npm run dev` 在 api-server 就绪前超时并回收 SpacetimeDB。先让 Rust 编译完成,或临时用 `bash scripts/dev-rust-stack.sh --skip-spacetime --skip-publish --api-timeout-seconds 900` 预热 api-server 编译;之后再重新跑完整 `npm run dev`。 +- 用户贴出的 Hermes background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。 + +## 测试与验证 + +常用命令: + +```bash +# Rust 格式化检查 +cd server-rs +cargo fmt -p api-server -p shared-contracts --check + +# 后端相关测试,按测试名过滤 +cargo test -p api-server admin_tracking -- --nocapture + +# 前端后台类型检查 / 构建 +cd .. +npm run admin-web:typecheck +npm run admin-web:build + +# 中文/编码检查 +npm run check:encoding + +# diff 空白检查 +git diff --check +``` + +如果 `npm run admin-web:typecheck` 报 `Cannot find module .../node_modules/typescript/bin/tsc`,说明当前 worktree 未安装 npm 依赖;先运行: + +```bash +npm install +``` + +不要把该错误误判成 TypeScript 代码错误。 + +## 常见坑 + +1. 只在 `app.rs` import handler 不够,必须实际 `.route(...)` 挂载,并套 `require_admin_auth`。 +2. `cargo fmt --manifest-path server-rs/Cargo.toml` 在该 workspace 可能报 `Failed to find targets`;进入 `server-rs` 后用 `cargo fmt --all` 或 `cargo fmt -p api-server -p shared-contracts --check`。 +3. `cargo fmt --all` 可能格式化不相关 Rust 文件;提交前用 `git status` 检查并 revert 非本任务文件。 +4. patch 工具对 Rust 单文件 lint 可能用 Rust 2015 edition 误报 `async fn is not permitted in Rust 2015`;以 `cargo test/check` 为准。 +5. `adminRoutes` 新增 route id 后,`AdminShell.routeIcons` 必须同步,否则 TypeScript 会因 `satisfies Record` 报错。 +6. 后台页面中的中文和 JSON 预览要避免整文件重写导致编码问题;修改后运行 `npm run check:encoding`。 +7. 后台数据页移动端要保证表格横向滚动,不要让整页布局撑坏。 +8. 涉及敏感配置、token、密码、连接串时,输出和文档中统一写 `[REDACTED]`。 + +## 参考资料 + +- `references/admin-database-table-query-2026-05-08.md`:本次后台数据库表查询接入的实现要点、校验规则与验证结果。 +- `references/admin-tracking-events-export-2026-05-07.md`:本次新增后台“埋点数据”页、SpacetimeDB HTTP SQL 只读明细、前端 `.xls` 导出的实现细节。 +- `references/private-table-sql-token-refresh.md`:本地清库/重建 standalone 后,用 `/v1/identity` + `spacetime login --token` 刷新 CLI token,以便 HTTP SQL 读取 private table。 +- `references/spacetimedb-http-sql-sats-display.md`:通过 HTTP SQL 读取 private table 时,enum / Option / Timestamp 的 SATS 原始 rows 如何转换为后台列表、详情和 Excel 可读值。 +- `references/daily-login-tracking-trigger-points.md`:排查后台 `daily_login` 埋点为何不是登录接口写入,而是任务中心读取/领奖兜底写入的触发点记录。 +- `references/daily-login-auth-closure.md`:将方案A拆出的每日登录埋点入口接入真实认证成功链路时的推荐接入点、非阻断语义、测试和提交注意事项。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md b/.hermes/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md new file mode 100644 index 00000000..63ba3436 --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md @@ -0,0 +1,29 @@ +# 本次后台表查询接入的可复用经验 + +## 需求落点 +- 后台“总览”页的表统计仍保留,只把每张表的表名改成可点击跳转到 `#tables?table=`。 +- 新增独立 `#tables` 页承载表选择、关键词搜索、JSON filters、limit、行详情弹窗。 + +## 后端实现要点 +- 新增只读接口: + - `GET /admin/api/database/tables` + - `GET /admin/api/database/tables/{table_name}/rows` +- 表名必须来自 schema 白名单;再加一层 identifier 校验,避免任意 SQL 表名注入。 +- `limit` 必须 clamp;本次实现使用默认 100、最大 500。 +- `search` / `filters` 不进入 SQL 字符串: + - SQL 只负责 `SELECT * FROM {table_name} LIMIT {limit}` + - 返回后在 api-server 内存中过滤 + - `filters` 仅接受 JSON object,按列名匹配;非 object 直接 400 +- SpacetimeDB HTTP SQL 返回可能是 statement array + rows,解析时要兼容这一层结构。 + +## 前端实现要点 +- `adminRoutes` 必须新增 `tables`,`AdminShell.routeIcons` 也要同步覆盖。 +- `AdminApp` 需要显式渲染 `AdminDatabaseTablesPage`。 +- worktree 下可能没有本地 `node_modules/typescript/bin/tsc`,而根目录有依赖;在验证前可以临时把根目录 `node_modules` 软链到 worktree 再执行 `npm run admin-web:typecheck`,验证后删除软链,避免污染 git 状态。 + +## 验证结果 +- `cargo test -p api-server admin_database -- --nocapture` 通过。 +- `cargo fmt --manifest-path Cargo.toml -p api-server -p shared-contracts --check` 通过。 +- `npm run admin-web:typecheck` 通过。 +- `npm run admin-web:build` 通过。 +- `npm run check:encoding` 通过。 \ No newline at end of file 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 new file mode 100644 index 00000000..ec986e6b --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md @@ -0,0 +1,53 @@ +# 后台埋点数据页与本地启动验证记录(2026-05-07) + +## 背景 + +本次在 Genarrative/百梦后台新增“埋点数据”页: + +- 后端新增 `GET /admin/api/tracking/events`。 +- shared-contracts 新增 admin tracking query/list/entry DTO。 +- 前端新增 `#tracking` 路由、导航、表格、详情面板与 `.xls` 导出。 +- 导出使用浏览器端 HTML table + Excel MIME,不引入 `xlsx` 依赖。 + +## 关键实现点 + +- 后台只读接口仍必须套 `require_admin_auth`。 +- SpacetimeDB 明细读取使用 HTTP SQL,不新增 reducer、不改 schema。 +- SQL 固定白名单列,不用 `SELECT *`。 +- Query 只允许 `eventKey/userId/scopeKind/scopeId/limit`。 +- `scopeKind` 只允许 `site/work/module/user`。 +- limit 默认 200,最大 1000。 +- SpacetimeDB HTTP SQL 响应要兼容 statement array + `rows`,Option 可能表现为 `{ "some": value }`。 +- 前端导出 `.xls` 时给单元格加 `mso-number-format:'\\@';`,防止 Excel 把 ID 转科学计数法。 + +## 验证命令 + +```bash +cd /.worktrees/hermes-996d586b +npm install # 若 node_modules 缺失 +npm run admin-web:typecheck +npm run admin-web:build +npm run check:encoding + +cd server-rs +cargo fmt -p api-server -p shared-contracts --check +cargo test -p api-server admin_tracking -- --nocapture +``` + +## 本地启动观察 + +启动命令: + +```bash +cd /.worktrees/hermes-996d586b +npm run api-server +npm run admin-web:dev -- --host 127.0.0.1 +``` + +实际验证: + +- api-server 监听 `127.0.0.1:3100`,健康检查为 `http://127.0.0.1:3100/healthz`。 +- admin-web 监听 `127.0.0.1:5173`,后台地址为 `http://127.0.0.1:5173/admin/`。 +- 请求 `http://127.0.0.1:5173/` 会 302 到 `/admin/`。 +- 不能默认用 3200 检查 api-server;本地脚本通过 `GENARRATIVE_API_PORT=3100` 启动。 +- 如果启动日志出现 SpacetimeDB `127.0.0.1:3101` connection refused,api-server 仍可能已正常监听;这是依赖的本地 SpacetimeDB 未启动,埋点页读真实数据会受影响。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md b/.hermes/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md new file mode 100644 index 00000000..1c4f872a --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md @@ -0,0 +1,55 @@ +# 真实登录成功链路接入每日登录埋点(2026-05-08) + +## 背景 + +后台“埋点数据”页要能看到真实登录产生的 `daily_login`。此前已完成方案 A:把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure/client 方法,避免后台查看或刷新任务中心污染登录数据。 + +闭环时不要再把写入点放回任务中心读取流程;应在认证成功且会话签发后显式调用每日登录埋点入口。 + +## 推荐接入点 + +在 `api-server` 认证成功路径中,先创建/签发会话,再非阻断记录埋点,再同步认证快照并返回: + +1. `create_auth_session` / `create_password_auth_session` 成功。 +2. 调用统一 helper:`record_daily_login_tracking_event_after_auth_success(...)`。 +3. helper 调用 `state.spacetime_client().record_daily_login_tracking_event(user_id.to_string()).await`。 +4. 成功写 `info`,失败写 `warn`,不能把埋点失败返回给用户。 + +已验证的真实登录链路包括: + +- 手机验证码登录:`server-rs/crates/api-server/src/phone_auth.rs` 的 `phone_login`。 +- 密码登录入口:`server-rs/crates/api-server/src/password_entry.rs` 的 `password_entry`。 +- 重置密码后自动登录:`server-rs/crates/api-server/src/password_management.rs` 的 `reset_password`。 +- 微信 OAuth 回调登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `handle_wechat_callback`。 +- 微信绑定手机号后自动登录:`server-rs/crates/api-server/src/wechat_auth.rs` 的 `bind_wechat_phone`。 +- refresh cookie 续期:`server-rs/crates/api-server/src/refresh_session.rs` 的 `refresh_session`。在 `rotate_session` 成功并签发新 access token 后记录,`login_method` 应使用 `rotated.session.issued_by_provider.clone()`,不要固定写成 Password。 + +## 关键实现约束 + +- 埋点是运营数据,必须保持非阻断:SpacetimeDB 调用失败只记录日志,不影响登录成功返回。 +- helper 建议放在 `auth_session.rs`,避免各登录 handler 重复错误处理。 +- refresh cookie 续期也被产品视为一次每日登录触发;接入 `refresh_session.rs` 时必须放在 token rotate 和 access token 签发成功之后,且保持非阻断,避免刷新失败或缺 cookie 时误写埋点。 +- `handle_wechat_callback` 如果要记录 `request_id/operation`,需要在 handler 参数中补 `Extension`;确认路由层已注入 RequestContext。 +- 单元测试默认不启动 SpacetimeDB。若直接调用真实 `spacetime_client` 会让既有认证测试依赖外部服务;可在 `#[cfg(test)]` 下让 helper no-op,仅用编译和现有登录成功测试覆盖调用点不破坏返回。 +- 后续如需要严格断言“helper 被调用”,应优先为 Spacetime client 引入可注入 trait/mock,而不是让 API 单测连接真实 SpacetimeDB。 + +## 验证命令 + +```bash +cd server-rs +cargo fmt -p api-server --check +cargo check -p api-server +cargo check -p spacetime-client +cargo test -p api-server auth_session -- --nocapture +cargo test -p api-server refresh_session_rotates_cookie_and_returns_new_access_token -- --nocapture +cargo test -p api-server password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie -- --nocapture +cargo test -p api-server phone_login_creates_user_and_sets_refresh_cookie -- --nocapture +cd .. +npm run check:encoding +git diff --check +``` + +## 提交注意 + +- 不要提交 `.env.local`、`.env.secrets.local` 或任何 token/密码/连接串。 +- 若工作区里有本地敏感文件,只提交明确改动的 Rust 文件和 `docs/technical/*` 文档。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md b/.hermes/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md new file mode 100644 index 00000000..6c868fee --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md @@ -0,0 +1,97 @@ +# Genarrative daily_login 埋点触发点排查记录 + +## 背景 + +用户在后台“埋点数据”页看到 `daily_login` 事件后询问:为什么每日登录埋点看起来只有在用户领取每日登录任务奖励后才记录,而不是登录时记录。 + +## 结论 + +当前代码口径里,`daily_login` 不是认证登录成功瞬间写入的事件。它挂在个人任务链路: + +- `GET /api/profile/tasks`:读取任务中心时会记录当日 `daily_login`,并刷新任务进度。 +- `POST /api/profile/tasks/{task_id}/claim`:领取任务奖励时,如果任务配置是 daily_login,会兜底记录当日 `daily_login`。 + +因为 `record_daily_login_tracking_event` 用 `daily-login::` 作为 event id,并先查重,所以同一用户同一北京自然日最多写一条。 + +## 关键文件 + +- `docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md` + - 第 47 行左右写明:用户打开任务中心时后端幂等记录当日 `daily_login`;点击领取时校验进度和领奖记录。 + - 接口说明中写明 `GET /api/profile/tasks` 会读取任务中心并记录当日登录埋点。 +- `server-rs/crates/api-server/src/runtime_profile.rs` + - `get_profile_task_center` 调用 `state.spacetime_client().get_profile_task_center(user_id)`。 + - `claim_profile_task_reward` 调用 `state.spacetime_client().claim_profile_task_reward(user_id, task_id)`。 +- `server-rs/crates/spacetime-module/src/runtime/profile.rs` + - `get_profile_task_center_snapshot(..., record_login_event: bool)` 在 `record_login_event` 为 true 时调用 `record_daily_login_tracking_event`。 + - `claim_profile_task_reward_record` 对 daily_login 任务调用 `record_daily_login_tracking_event` 作为兜底。 + - `record_daily_login_tracking_event` 负责生成 event id、查重、写入 `tracking_event` 和更新 `tracking_daily_stat`。 +- `server-rs/crates/api-server/src/phone_auth.rs` + - 手机号登录成功后做验证码校验、新用户奖励、邀请码绑定、session 签发、认证快照同步;当前没有写入 `daily_login`,也没有调用任务中心接口。 + +## 排查方法 + +1. 不要只看后台埋点页。先搜事件 key 和任务接口: + +```bash +git grep -n "daily_login\|tracking_event\|get_profile_task_center\|claim_profile_task_reward" -- server-rs apps docs +``` + +2. 对照设计文档中的事件口径: + +```bash +sed -n '35,58p' docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md +``` + +3. 追 API handler 到 SpacetimeDB reducer: + +- `api-server/src/runtime_profile.rs` +- `spacetime-client` 对应 procedure wrapper +- `spacetime-module/src/runtime/profile.rs` + +4. 再看真实登录接口是否写入同一事件。手机号登录入口是: + +- `server-rs/crates/api-server/src/phone_auth.rs::phone_login` + +## 常见误判 + +- 后台只是在展示 `tracking_event`,不是事件产生点。 +- “每日登录”这个中文名容易让人以为它必然在 auth 登录成功时写入;当前实现不是这样。 +- 如果用户登录后没有打开“我的/任务中心”,只在领奖时触发 claim 接口,就会表现为“领奖时才出现埋点”。 +- 领取接口里的写入是兜底,避免用户直接点击领取时因为未先打开任务中心而无法完成每日任务。 + +## 后续方案A落地记录 + +在后续修复中,采用“方案A”:把“读取任务中心时顺手记录每日登录埋点”拆成独立 SpacetimeDB procedure,使任务中心读取只负责读取/刷新进度,避免后台查看或刷新任务中心时污染埋点数据。 + +关键变化: + +- `server-rs/crates/module-runtime/src/domain.rs` + - 新增 `RuntimeTrackingEventProcedureResult { ok, error_message }`,用于返回纯事件写入结果。 +- `server-rs/crates/spacetime-module/src/runtime/profile.rs` + - 新增 `record_daily_login_tracking_event_and_return(ctx, input)` procedure。 + - `get_profile_task_center` 注释和行为调整为只读取/刷新任务进度,不再作为每日登录埋点产生点。 +- `server-rs/crates/spacetime-client/src/runtime.rs` + - 新增 `record_daily_login_tracking_event(user_id)` client 方法,调用新 procedure。 +- `server-rs/crates/spacetime-client/src/mapper.rs` + - 新增 `map_runtime_tracking_event_procedure_result`,把 `ok=false` 映射为 `procedure_failed`。 + +落地注意: + +- 这一步只拆出后端写入入口,不等于所有登录方式已经接入;接入手机号/微信/密码等认证成功链路前,需确认产品口径:统计登录成功是否应覆盖所有登录方式,以及事件失败是否阻断登录。 +- 修改 `spacetime-module` procedure 后,通常需要重新生成/同步 SpacetimeDB 绑定;若直接手补 `spacetime-client/src/module_bindings`,要非常谨慎,因为该目录声明为自动生成。 +- patch 工具可能对 Rust 单文件使用 2015 edition lint,看到 `async fn is not permitted in Rust 2015` 时不要立即按该误报改代码,应以 `cd server-rs && cargo test/check ...` 为准。 + +验证记录: + +```bash +cd server-rs +cargo test -p module-runtime runtime_profile_task_status_matches_progress_and_claim -- --nocapture +``` + +该测试通过可验证任务中心领域进度/领取逻辑未被破坏;完整接入认证链路后还应补 api-server 层登录成功埋点测试。 + +## 后续设计建议 + +如果产品口径要求“登录成功就算每日登录”,应把 `daily_login` 写入点前移到统一 auth 登录成功链路,并覆盖手机号/微信/密码等登录方式;任务中心只读取进度或最多保留幂等兜底。 + +如果需要同时分析真实登录和任务完成,建议新增独立事件,例如 `auth_login_success` / `user_login_success`,让 `daily_login` 继续表示每日任务完成条件。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md b/.hermes/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md new file mode 100644 index 00000000..1dd7a829 --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md @@ -0,0 +1,37 @@ +# 本地 private table SQL 权限修复 + +场景: +- 后台或 api-server 通过 SpacetimeDB HTTP SQL 读取 `tracking_event` 这类 private table。 +- 本地清库、重建 standalone 或重新发布模块后,原 CLI token 失效,SQL 可能报 `no such table ... If the table exists, it may be marked private`。 + +操作步骤: + +1. 清空本地 SpacetimeDB 数据目录 + - 使用:`spacetime --root-dir= server clear --yes` + - 只清本地开发环境,不要误伤远端或其他 worktree。 + +2. 启动本地 standalone + - 用项目约定的 `scripts/dev-rust-stack.sh` 或等价命令启动 `spacetime`。 + - 确认 `/v1/ping` 可访问后再取 identity。 + +3. 通过 `/v1/identity` 获取新 token 和 identity + - 使用 `POST http://127.0.0.1:3101/v1/identity` + - 只记录 identity,不要在日志中打印 token 明文。 + +4. 用新 token 登录 CLI + - 运行:`spacetime --root-dir= login --token ` + - 这会把 token 写到本地 CLI 配置,后续 HTTP SQL 可读 private table。 + +5. 重新验证 SQL + - 使用带 token 的 `POST /v1/database//sql` + - 先尝试 `SELECT ... FROM tracking_event LIMIT 1` + - 若成功,再让 api-server 走同样 token。 + +6. 如果 api-server 需要复用 token + - 优先读取项目内本地 CLI 配置中的 token,而不是硬编码或回填到 `.env`。 + - 输出日志时统一 `[REDACTED]`。 + +排查要点: +- `ORDER BY` 和 private table 是两个独立问题,先分开修。 +- 清库后旧 token 很可能不再能看见 private table,不代表表不存在。 +- 若 `/v1/identity` 返回的 token 没权限,再检查当前 standalone 是否就是刚启动的本地实例、database 名是否一致、模块是否已重新发布。 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md b/.hermes/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md new file mode 100644 index 00000000..e6ba4d50 --- /dev/null +++ b/.hermes/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md @@ -0,0 +1,43 @@ +# SpacetimeDB HTTP SQL SATS 值后台展示处理 + +本参考用于 Genarrative 后台通过 SpacetimeDB HTTP SQL 读取表明细并展示/导出时,处理 SQL rows 中的 SATS 原始 JSON 值。 + +## 典型现象 + +读取 private table(例如 `tracking_event`)后,HTTP SQL 可能返回如下原始形态: + +- enum:`RuntimeTrackingScopeKind::User` 返回 `[3, []]` +- `Option::Some("user_00000001")` 返回 `[0, "user_00000001"]` +- `Option::None` 返回 `[1, []]` +- `Timestamp` 返回 `[1778207451731746]` + +如果直接 `value.to_string()` 展示,后台会出现 `[3,[]]`、`[0,"..."]`、`[1,[]]`、`[1778207451731746]`,运营不可读。 + +## 推荐处理 + +1. 后端解析层优先标准化: + - Option:`[0, value] -> value`,`[1, []] -> None` + - enum:按生成 binding 的 variant 顺序映射,例如 `RuntimeTrackingScopeKind` 为 `site/work/module/user`,索引 `0/1/2/3` 分别对应这些字符串 + - Timestamp:单元素数组 `[micros] -> "micros"` +2. 前端展示层再格式化时间: + - 纯数字时间戳按微秒处理:`Date(Math.floor(micros / 1000))` + - ISO 字符串用 `new Date(value)` + - 展示为 `YYYY-MM-DD HH:mm:ss` +3. 列表、详情弹窗、Excel 导出必须使用同一套格式化结果,避免导出仍残留 SATS 原始值。 +4. 增加单测覆盖 SATS 原始 rows,至少断言: + - `[3, []] -> user` + - `[0, "user"] -> Some("user")` + - `[1, []] -> None` + - `[1778207451731746] -> "1778207451731746"` + +## 验收建议 + +- `cargo test -p api-server admin_tracking -- --nocapture` +- `npm run admin-web:typecheck` +- `npm run admin-web:build` +- `npm run check:encoding` +- `git diff --check` + +## 注意 + +不同 enum 的 variant 顺序必须以生成 binding 或 module 源码为准,不能复用其他 enum 的索引映射。