Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -13,12 +13,14 @@
|
||||
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
|
||||
- [埋点查询](./tracking/README.md):埋点原始事件与聚合投影的本地 SQL 查询。
|
||||
- [运营查询](./operations/README.md):任务、领奖、钱包对账等后台核查查询。
|
||||
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md)。
|
||||
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md)。
|
||||
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
|
||||
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
|
||||
|
||||
创作 Agent 问答流式失败时保留已显示回复、并透出更具体上游错误的契约见 [CREATION_AGENT_STREAM_FAILURE_RETENTION_FIX_2026-05-05.md](./technical/CREATION_AGENT_STREAM_FAILURE_RETENTION_FIX_2026-05-05.md)。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
||||
|
||||
20
docs/experience/ADMIN_TASK_CONFIG_SCOPE_LOCK_2026-05-04.md
Normal file
20
docs/experience/ADMIN_TASK_CONFIG_SCOPE_LOCK_2026-05-04.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# AdminTaskConfigPage 埋点范围收口记录(2026-05-04)
|
||||
|
||||
## 变更目标
|
||||
- 前端隐藏「埋点范围」选择项。
|
||||
- 保存任务配置时固定传递 `scopeKind=user`,避免运营在后台选择其他范围。
|
||||
|
||||
## 落地结果
|
||||
- `apps/admin-web/src/pages/AdminTaskConfigPage.tsx`
|
||||
- 移除了「埋点范围」下拉选择 UI。
|
||||
- 保存时改为固定提交 `scopeKind: 'user'`。
|
||||
- 回填表单时不再读取或维护 `scopeKind` 的可编辑状态。
|
||||
|
||||
## 验证结果
|
||||
- 已检查页面源码,确认不再存在「埋点范围」字段与可编辑逻辑。
|
||||
- 已执行 `npm run admin-web:typecheck`。
|
||||
- 当前验证失败原因为环境缺少 `node_modules/typescript/bin/tsc`,属于依赖安装状态问题,不是本次前端改动引入的代码错误。
|
||||
|
||||
## 遗留说明
|
||||
- `apps/admin-web/src/api/adminApiTypes.ts` 中仍保留 `TrackingScopeKind` 类型,供接口契约与其他前端模块兼容使用。
|
||||
- `apps/admin-web/src/config/trackingEventDefinitions.ts` 仍保留事件定义中的 `scopeKind` 元数据,但页面不再允许运营修改。
|
||||
@@ -0,0 +1,278 @@
|
||||
# AI 原生方洞挑战玩法创作工具与玩法系统 PRD
|
||||
|
||||
更新时间:`2026-05-04`
|
||||
|
||||
## 0. 文档目的
|
||||
|
||||
这份 PRD 用于在当前平台内新增一条“方洞挑战”玩法类型,并先冻结它从入口占位到完整可创建闭环的产品边界。
|
||||
|
||||
本玩法来自参考视频中的核心反差:玩家看到不同形状的积木与多个洞口,会本能判断“形状应放入对应洞口”,但演示不断把不同形状都放进同一个方洞,形成“规则预期被打破”的喜剧张力。
|
||||
|
||||
本次不是简单复制视频内容,也不是只新增一个前端小游戏。正式版本需要把这种反直觉机制抽象成平台内可创作、可试玩、可发布的玩法类型。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定义
|
||||
|
||||
“方洞挑战”是一个反直觉形状分拣玩法:百梦主通过 Agent 设定形状主题、洞口规则、误导节奏和反差演出,系统生成一个移动端优先的单局挑战;玩家在限时内把连续出现的形状投入正确洞口,后端根据当前关卡的真实兼容规则裁决成功、失败和连击。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前接入级别
|
||||
|
||||
根据 `genarrative-play-type-integration` 的接入分级,本次落地到 **完整玩法闭环**:
|
||||
|
||||
1. 新增玩法 ID:`square-hole`。
|
||||
2. 新增展示名称:`方洞挑战`。
|
||||
3. 新增子标题:`反直觉形状分拣`。
|
||||
4. 在新建作品入口、创作类型弹层、结果页、运行态和作品架展示。
|
||||
5. 支持 Agent 创作、草稿生成、结果页编辑、试玩、发布和公开运行。
|
||||
6. 后端以 `server-rs + Axum + SpacetimeDB` 为真相源,前端只渲染快照与交互。
|
||||
|
||||
不允许把运行规则临时写成前端本地真相,也不复用 `server-node`、Express 或 PostgreSQL。
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品定位
|
||||
|
||||
## 3.1 模板名称
|
||||
|
||||
1. 对外模板名称:`方洞挑战`。
|
||||
2. 对外子标题:`反直觉形状分拣`。
|
||||
3. 开发代号:`SquareHole`。
|
||||
4. 工程玩法域:`square-hole`。
|
||||
5. 后端模块命名预期:`square_hole`。
|
||||
|
||||
## 3.2 核心乐趣
|
||||
|
||||
1. 玩家先根据形状轮廓做直觉判断。
|
||||
2. 关卡通过洞口、提示、视觉遮挡和演出制造误导。
|
||||
3. 真实规则可以是“方洞万能”“指定洞口万能”“颜色优先于形状”“本轮只看尺寸”等反直觉规则。
|
||||
4. 玩家通过连续试探和反馈理解规则,形成短局重复挑战。
|
||||
|
||||
## 3.3 与现有玩法的区别
|
||||
|
||||
1. 不等同于拼图:不切图、不交换、不合并拼块。
|
||||
2. 不等同于抓大鹅:不做三消备选栏,不做堆叠遮挡点击。
|
||||
3. 不等同于大鱼吃小鱼:不做摇杆移动、吞噬、成长等级。
|
||||
4. 不复用 RPG 的世界、角色、章节或剧情推进结构。
|
||||
5. 运行态只保留“当前形状 + 洞口选择 + 后端裁决”,不在前端写正式规则真相。
|
||||
|
||||
---
|
||||
|
||||
## 4. 完整闭环目标
|
||||
|
||||
本次完整闭环必须补齐:
|
||||
|
||||
1. 平台创作入口选择“方洞挑战”。
|
||||
2. Agent 对话收集玩法锚点。
|
||||
3. 生成方洞挑战草稿。
|
||||
4. 进入结果页编辑作品名、标签、封面、形状数量、反差规则和视觉主题。
|
||||
5. 支持发布前试玩。
|
||||
6. 发布作品。
|
||||
7. 玩家从作品详情或广场进入运行态。
|
||||
8. 后端初始化单局形状队列、洞口兼容规则和计分状态。
|
||||
9. 玩家拖拽或点击形状投入洞口。
|
||||
10. 后端裁决投入结果、连击、扣时、失败、胜利和成绩。
|
||||
11. 前端只渲染后端快照与即时反馈,不承接正式规则真相。
|
||||
|
||||
---
|
||||
|
||||
## 5. 创作锚点设计
|
||||
|
||||
Agent 型创作版本至少收集下面 5 个锚点:
|
||||
|
||||
| 锚点 | 字段建议 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 主题外观 | `themePrompt` | 决定玩具、洞板、背景、形状材质和色彩风格。 |
|
||||
| 反差规则 | `twistRule` | 决定“为什么不是按形状匹配”的真实判定规则。 |
|
||||
| 洞口组 | `holeSet` | 决定本局出现的洞口种类、数量、位置和视觉误导强度。 |
|
||||
| 形状队列 | `shapeSequence` | 决定连续出现的形状、颜色、大小和难度递增。 |
|
||||
| 反馈节奏 | `feedbackRhythm` | 决定成功、错误、连击、惊讶和结算演出风格。 |
|
||||
|
||||
Agent 需要把玩家一句灵感收束为上述锚点,不允许逐项盘问低价值字段。
|
||||
|
||||
## 5.1 Agent AI 生成契约
|
||||
|
||||
方洞挑战的创作对话必须接入 `api-server` 的现有 LLM 能力,不能把用户输入解析成固定模板后直接写回会话。工程实现以 `state.llm_client()` 为唯一模型入口,通过方洞专属 Agent turn 生成回复与下一轮配置。
|
||||
|
||||
单轮模型输出必须是严格 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"replyText": "",
|
||||
"progressPercent": 0,
|
||||
"nextConfig": {
|
||||
"themeText": "",
|
||||
"twistRule": "",
|
||||
"shapeCount": 12,
|
||||
"difficulty": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
落地约束:
|
||||
|
||||
1. `replyText` 是直接展示给百梦主的中文回复,不得出现 JSON、字段名、内部结构等说明。
|
||||
2. `nextConfig` 必须是完整配置,不允许只输出 patch;缺失字段只能由后端用当前会话配置兜底。
|
||||
3. `shapeCount` 由后端限制在 `6` 到 `24`,`difficulty` 限制在 `1` 到 `10`。
|
||||
4. `quickFillRequested=true` 时,模型应直接补齐剩余配置,后端把 `progressPercent` 固定为 `100`。
|
||||
5. 模型不可用或结果无法解析时,接口返回明确错误,不允许用确定性模板伪装成 AI 回复。
|
||||
6. 非流式消息接口和 SSE 流式消息接口都必须走同一套方洞 Agent turn,SSE 只额外负责把 `replyText` 增量回传。
|
||||
|
||||
---
|
||||
|
||||
## 6. 运行规则设计
|
||||
|
||||
## 6.1 单局结构
|
||||
|
||||
1. 单局默认 `60` 秒。
|
||||
2. 每局默认 `12` 个形状。
|
||||
3. 洞口数量默认 `4` 到 `6` 个。
|
||||
4. 玩家每次只能操作当前形状。
|
||||
5. 正确投入后进入下一个形状。
|
||||
6. 错误投入扣除时间或清空连击。
|
||||
7. 全部形状完成即胜利。
|
||||
8. 时间归零即失败。
|
||||
|
||||
## 6.2 真实兼容规则
|
||||
|
||||
首版可支持下面几类规则:
|
||||
|
||||
1. `shape_match`:形状轮廓匹配。
|
||||
2. `square_hole_priority`:方洞兼容所有形状,其他洞口只作为误导。
|
||||
3. `color_match`:颜色优先于形状。
|
||||
4. `size_match`:尺寸优先于形状。
|
||||
5. `round_prompt`:本轮按后端给出的短提示规则判定。
|
||||
|
||||
其中 `square_hole_priority` 是参考视频核心反差的首选默认规则。
|
||||
|
||||
## 6.3 前端表现
|
||||
|
||||
1. 竖屏优先,桌面端居中显示游戏台。
|
||||
2. 当前形状位于屏幕下半区域,洞板位于上半区域。
|
||||
3. 只显示必要状态:剩余时间、连击、当前进度。
|
||||
4. 不默认展示长篇规则说明。
|
||||
5. 错误反馈用短促动画、颜色闪烁和轻量文字状态,不堆解释。
|
||||
6. 点击按钮弹出的配置或结算必须使用独立面板,不在当前面板下方展开。
|
||||
|
||||
---
|
||||
|
||||
## 7. 后端分层边界
|
||||
|
||||
完整实现时必须遵循当前 `server-rs + Axum + SpacetimeDB` 路线:
|
||||
|
||||
1. `server-rs/crates/module-square-hole`
|
||||
- 纯领域规则、形状队列生成、兼容性裁决、分数计算。
|
||||
- 不依赖 Axum、SpacetimeDB、OSS 或 LLM。
|
||||
2. `server-rs/crates/shared-contracts`
|
||||
- 暴露 Agent、作品、运行态 DTO。
|
||||
3. `server-rs/crates/spacetime-module`
|
||||
- 存储 session、message、work profile、runtime run。
|
||||
- 表结构变化必须同步 `migration.rs` 与表目录。
|
||||
4. `server-rs/crates/spacetime-client`
|
||||
- 提供 api-server 调用 SpacetimeDB 的 typed facade。
|
||||
5. `server-rs/crates/api-server`
|
||||
- 暴露 `/api/creation/square-hole/*` 与 `/api/runtime/square-hole/*`。
|
||||
- 处理鉴权、错误 envelope、LLM turn 和 HTTP facade。
|
||||
|
||||
---
|
||||
|
||||
## 8. 数据模型
|
||||
|
||||
## 8.1 创作 session
|
||||
|
||||
1. `sessionId`
|
||||
2. `ownerUserId`
|
||||
3. `currentTurn`
|
||||
4. `progressPercent`
|
||||
5. `stage`
|
||||
6. `config`
|
||||
7. `draft`
|
||||
8. `messages`
|
||||
9. `lastAssistantReply`
|
||||
10. `publishedProfileId`
|
||||
|
||||
## 8.2 结果页 work profile
|
||||
|
||||
1. `workId`
|
||||
2. `profileId`
|
||||
3. `ownerUserId`
|
||||
4. `sourceSessionId`
|
||||
5. `gameName`
|
||||
6. `themeText`
|
||||
7. `twistRule`
|
||||
8. `summary`
|
||||
9. `tags`
|
||||
10. `coverImageSrc`
|
||||
11. `shapeCount`
|
||||
12. `difficulty`
|
||||
13. `publicationStatus`
|
||||
14. `playCount`
|
||||
15. `updatedAt`
|
||||
16. `publishedAt`
|
||||
|
||||
## 8.3 运行态 run snapshot
|
||||
|
||||
1. `runId`
|
||||
2. `profileId`
|
||||
3. `ownerUserId`
|
||||
4. `status`
|
||||
5. `snapshotVersion`
|
||||
6. `startedAtMs`
|
||||
7. `durationLimitMs`
|
||||
8. `remainingMs`
|
||||
9. `totalShapeCount`
|
||||
10. `completedShapeCount`
|
||||
11. `combo`
|
||||
12. `bestCombo`
|
||||
13. `score`
|
||||
14. `ruleLabel`
|
||||
15. `currentShape`
|
||||
16. `holes`
|
||||
17. `lastFeedback`
|
||||
|
||||
---
|
||||
|
||||
## 9. API 设计
|
||||
|
||||
## 9.1 创作接口
|
||||
|
||||
1. `POST /api/creation/square-hole/sessions`
|
||||
2. `GET /api/creation/square-hole/sessions/{sessionId}`
|
||||
3. `POST /api/creation/square-hole/sessions/{sessionId}/messages`
|
||||
4. `POST /api/creation/square-hole/sessions/{sessionId}/messages/stream`
|
||||
5. `POST /api/creation/square-hole/sessions/{sessionId}/actions`
|
||||
6. `POST /api/creation/square-hole/sessions/{sessionId}/compile`
|
||||
7. `GET /api/creation/square-hole/works`
|
||||
8. `GET /api/creation/square-hole/works/{profileId}`
|
||||
9. `PUT /api/creation/square-hole/works/{profileId}`
|
||||
10. `POST /api/creation/square-hole/works/{profileId}/publish`
|
||||
11. `DELETE /api/creation/square-hole/works/{profileId}`
|
||||
|
||||
## 9.2 运行接口
|
||||
|
||||
1. `GET /api/runtime/square-hole/gallery`
|
||||
2. `GET /api/runtime/square-hole/gallery/{profileId}`
|
||||
3. `POST /api/runtime/square-hole/works/{profileId}/runs`
|
||||
4. `GET /api/runtime/square-hole/runs/{runId}`
|
||||
5. `POST /api/runtime/square-hole/runs/{runId}/drop`
|
||||
6. `POST /api/runtime/square-hole/runs/{runId}/stop`
|
||||
7. `POST /api/runtime/square-hole/runs/{runId}/restart`
|
||||
8. `POST /api/runtime/square-hole/runs/{runId}/time-up`
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
1. `src/config/newWorkEntryConfig.ts` 中存在 `square-hole` 类型且开放创建。
|
||||
2. 新建作品入口和创作类型弹层能展示“方洞挑战”。
|
||||
3. 能进入 `square-hole` Agent 工作台。
|
||||
4. 能生成草稿并进入结果页。
|
||||
5. 能编辑结果页并保存、发布。
|
||||
6. 能从作品详情或广场进入运行态。
|
||||
7. 能点击或拖拽当前形状投入洞口。
|
||||
8. 后端裁决命中规则、连击、失败和胜利。
|
||||
9. 刷新后可恢复作品与运行态快照。
|
||||
10. `docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md` 记录该入口开放状态。
|
||||
11. 后端改动完成后必须执行 `npm run api-server:maincloud`,以 `GET /healthz` 返回 `200` 作为主云配置启动 smoke 通过标准,并在 smoke 后清理本次启动进程。
|
||||
@@ -192,6 +192,8 @@ export interface AdminDisableProfileRedeemCodeRequest {
|
||||
|
||||
export interface AdminUpsertProfileInviteCodeRequest {
|
||||
inviteCode: string;
|
||||
startsAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -215,6 +217,9 @@ export interface ProfileRedeemCodeAdminListResponse {
|
||||
export interface ProfileInviteCodeAdminResponse {
|
||||
userId: string;
|
||||
inviteCode: string;
|
||||
startsAt: string | null;
|
||||
expiresAt: string | null;
|
||||
status: 'pending' | 'active' | 'expired';
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -376,6 +381,9 @@ export interface ProfileInviteCodeAdminListResponse {
|
||||
{
|
||||
"userId": "admin:root:SPRING2026",
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"status": "active",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
},
|
||||
@@ -393,6 +401,8 @@ export interface ProfileInviteCodeAdminListResponse {
|
||||
```json
|
||||
{
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
}
|
||||
@@ -405,6 +415,9 @@ export interface ProfileInviteCodeAdminListResponse {
|
||||
{
|
||||
"userId": "admin",
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"status": "active",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
},
|
||||
@@ -415,6 +428,118 @@ export interface ProfileInviteCodeAdminListResponse {
|
||||
|
||||
邀请码页的 metadata 输入必须先在前端解析为 JSON 对象;空字符串按 `{}` 处理,数组、字符串、数字等非对象值直接提示错误。最终标准化、长度限制和邀请码合法性以 `server-rs` 为准。
|
||||
|
||||
#### 4.8.1 邀请码有效期语义
|
||||
|
||||
邀请码仍然是“用户稳定邀请身份码”,不做删除或软删除。本轮只增加时间窗字段,用于控制**新填写邀请码**是否允许绑定:
|
||||
|
||||
1. `startsAt` / 后端 `starts_at`:邀请码开始生效时间;为空表示立即生效。
|
||||
2. `expiresAt` / 后端 `expires_at`:邀请码截止时间;为空表示长期有效。
|
||||
3. 两个字段都为空时,邀请码视为长期有效。
|
||||
4. `expiresAt` 采用左闭右开语义:当前时间 `>= expiresAt` 时视为已过期。
|
||||
5. 时间字段在管理 API JSON 中统一使用 ISO 8601 UTC 字符串或 `null`;SpacetimeDB 内部仍按 `Timestamp` 存储,契约层负责转换,前端不得自行假设微秒/毫秒整数。
|
||||
6. 有效期只影响用户之后调用填写邀请码接口建立新邀请关系;已绑定的邀请关系、历史奖励、统计和审计记录不回溯修改。
|
||||
|
||||
字段合法性要求:
|
||||
|
||||
1. `startsAt` 和 `expiresAt` 均允许为空。
|
||||
2. 若两者都存在,必须满足 `startsAt < expiresAt`;相等或开始晚于截止应由后端拒绝,前端可提前提示但不能替代后端校验。
|
||||
3. 后台编辑已有邀请码时,空值代表清空该边界;不要用空字符串写入契约。
|
||||
|
||||
#### 4.8.2 用户填写邀请码的错误优先级与校验逻辑
|
||||
|
||||
填写邀请码时,后端是唯一业务真相。前端只展示后端错误,不复制完整业务规则。推荐校验优先级如下:
|
||||
|
||||
1. **请求身份与输入基础校验**:未登录、空邀请码、格式不合法等请求级错误优先返回。
|
||||
2. **用户自身状态校验**:用户不存在、用户资料不可用、已绑定过邀请关系等与当前用户直接相关的错误优先于邀请码时间窗。
|
||||
3. **邀请码查找**:按标准化后的邀请码查找记录;不存在时返回“邀请码不存在或不可用”。
|
||||
4. **自邀请校验**:邀请码归属用户等于当前用户时,返回“不能填写自己的邀请码”。
|
||||
5. **时间窗校验**:
|
||||
- `starts_at` 存在且当前时间 `< starts_at`,返回“邀请码未生效”。
|
||||
- `expires_at` 存在且当前时间 `>= expires_at`,返回“邀请码已过期”。
|
||||
6. **绑定写入与奖励发放**:只有以上校验全部通过,才写入邀请绑定、奖励或相关流水。
|
||||
|
||||
该顺序的目标是避免用“未生效/已过期”泄露不该暴露的用户状态,同时保证用户看到的错误与实际阻断原因一致。若后续新增风控、封禁、黑名单等规则,应在写入前补入,并在本节同步明确优先级。
|
||||
|
||||
#### 4.8.3 后台邀请码列表状态展示规则
|
||||
|
||||
后台列表状态可由后端返回 `status`,也可在前端用同一规则从 `startsAt` / `expiresAt` 派生;如果两者同时存在,列表展示以后端 `status` 为准,并仅把前端派生结果用于兜底。
|
||||
|
||||
| 条件 | 状态值 | 中文标签 | 展示建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| `startsAt` 存在且当前时间 `< startsAt` | `pending` | 未生效 | 展示开始时间,提示尚不能被新用户填写 |
|
||||
| `expiresAt` 存在且当前时间 `>= expiresAt` | `expired` | 已过期 | 展示截止时间,提示不再允许新绑定 |
|
||||
| 其他情况 | `active` | 有效 | 正常高亮展示 |
|
||||
|
||||
补充展示规则:
|
||||
|
||||
1. 两个字段都为空时状态为 `active`,中文可展示为“长期有效”。
|
||||
2. `startsAt` 为空、`expiresAt` 未来存在时状态为 `active`,中文可展示为“有效至 YYYY-MM-DD HH:mm”。
|
||||
3. `startsAt` 未来、`expiresAt` 为空时状态为 `pending`,中文可展示为“YYYY-MM-DD HH:mm 生效”。
|
||||
4. 列表至少展示邀请码、状态、开始时间、截止时间、更新时间;metadata 可保留折叠/摘要展示,避免挤占移动端宽度。
|
||||
5. 列表状态只用于运营理解,不作为安全边界;真正是否可填写仍以后端 redeem 校验为准。
|
||||
|
||||
### 4.9 后台写操作二次确认规范
|
||||
|
||||
后台所有会修改线上数据的操作,在真正调用 API 前必须二次确认;取消确认时不得发送任何请求。该规范覆盖当前和未来新增的管理写入口,不限于 profile 模块。
|
||||
|
||||
必须二次确认的操作包括但不限于:
|
||||
|
||||
1. 创建/更新兑换码:`POST /admin/api/profile/redeem-codes`。
|
||||
2. 停用兑换码:`POST /admin/api/profile/redeem-codes/disable`。
|
||||
3. 创建/更新邀请码:`POST /admin/api/profile/invite-codes`。
|
||||
4. 创建/更新个人任务配置:`POST /admin/api/profile/tasks`。
|
||||
5. 停用个人任务配置:`POST /admin/api/profile/tasks/disable`。
|
||||
6. 后续任何 `POST` / `PATCH` / `PUT` / `DELETE` 管理接口,只要会修改数据、触发任务、写审计或影响线上配置,均默认纳入确认。
|
||||
|
||||
交互要求:
|
||||
|
||||
1. 确认弹窗必须在 API 调用前出现,确认后才进入 loading 和提交状态。
|
||||
2. 弹窗必须展示操作类型(新增、更新、停用、删除、发布等)、对象标识(如 `code`、`inviteCode`、`taskId`)和影响说明。
|
||||
3. 默认按钮顺序为“取消 / 确认”,取消不应有危险色;危险操作(停用、删除、覆盖线上配置)确认按钮使用警示样式。
|
||||
4. 弹窗文案统一提示“该操作会立即影响线上数据”,但不要在页面常驻展示大段规则说明。
|
||||
5. 支持键盘和移动端:Esc 或取消按钮关闭;移动端弹窗宽度自适应,不遮挡关键对象信息。
|
||||
6. loading 期间锁定确认按钮和原页面提交按钮,避免重复写入。
|
||||
7. 成功后按现有页面规则刷新列表或合并返回记录;失败时展示后端错误,不能静默关闭为成功。
|
||||
|
||||
建议抽象通用确认能力,例如 `confirmAdminWriteAction({ actionLabel, targetLabel, riskLevel, onConfirm })` 或通用 `AdminConfirmDialog`,页面只传入对象与回调,避免每个页面重复实现不同交互。
|
||||
|
||||
#### 4.9.1 二次确认文案模板
|
||||
|
||||
```text
|
||||
标题:确认{操作类型}{对象类型}
|
||||
正文:即将{操作类型}「{对象标识}」。该操作会立即影响线上数据。
|
||||
取消按钮:取消
|
||||
确认按钮:确认{操作类型}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
1. `确认更新邀请码`:即将更新「SPRING2026」的有效期与 metadata。该操作会立即影响线上数据。
|
||||
2. `确认停用兑换码`:即将停用「WELCOME2026」。该操作会立即影响线上数据。
|
||||
3. `确认更新任务配置`:即将更新「daily_login」。该操作会立即影响线上数据。
|
||||
|
||||
### 4.10 邀请码有效期与二次确认改动范围
|
||||
|
||||
实现本设计时预期改动范围如下,未列出的层级不要擅自承接业务规则:
|
||||
|
||||
1. `server-rs/crates/spacetime-module/src/runtime/profile.rs`:邀请码表结构、upsert、redeem 时间窗校验与后台列表投影。
|
||||
2. `server-rs/crates/spacetime-module/src/migration.rs`:旧邀请码记录迁移,默认 `starts_at = None`、`expires_at = None`。
|
||||
3. `server-rs/crates/shared-contracts/src/**`:管理请求/响应 DTO 增加 `startsAt`、`expiresAt`、`status` 等字段。
|
||||
4. `server-rs/crates/spacetime-client/src/module_bindings/**` 与 mapper:按表结构变更重新生成/补齐绑定字段。
|
||||
5. `server-rs/crates/api-server/src/runtime_profile.rs`:接收、校验、转发并返回邀请码时间窗字段;保持错误 envelope 兼容后台读取逻辑。
|
||||
6. `apps/admin-web/src/api/adminApiTypes.ts` 与 `adminApiClient.ts`:同步契约字段,不在 client 层写业务判断。
|
||||
7. `apps/admin-web/src/pages/AdminInviteCodePage.tsx`:有效期表单、列表状态展示、保存前确认。
|
||||
8. `apps/admin-web/src/pages/AdminRedeemCodePage.tsx`、`AdminTaskConfigPage.tsx` 及后续写页面:统一接入写操作二次确认。
|
||||
9. `apps/admin-web/src/styles/admin.css`:状态标签、确认弹窗与移动端样式。
|
||||
|
||||
验证建议:
|
||||
|
||||
1. 服务端单测覆盖:未生效邀请码拒绝、已过期邀请码拒绝、有效时间窗可绑定、空时间窗长期有效、已绑定关系不受后续过期影响。
|
||||
2. 管理 API 覆盖:upsert 能写入/清空 `startsAt`、`expiresAt`;列表返回状态正确;`startsAt >= expiresAt` 被拒绝。
|
||||
3. 前端交互覆盖:点击保存/停用不会直接请求,取消确认不请求,确认后只请求一次,失败展示后端错误。
|
||||
4. 回归兑换码与任务配置页面,确认所有写操作均有统一二次确认。
|
||||
5. 修改后端时按项目约束运行对应 Rust 测试、`npm run api-server` 联调和 `/healthz`;修改前端时运行 `npm run admin-web:typecheck`、`npm run admin-web:build`;文档或中文改动后运行 `npm run check:encoding`。
|
||||
|
||||
## 5. 鉴权与会话
|
||||
|
||||
1. token key 固定为 `genarrative_admin_token`。
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
# Analytics Date Dimension 与个人任务埋点范围收口记录(2026-05-04)
|
||||
|
||||
## 背景
|
||||
|
||||
本记录用于收口 `.hermes/plans/2026-05-04_022223-analytics-time-dimension-mapping.md` 中当前阶段已经落地的内容,并明确尚未执行的后续范围。
|
||||
|
||||
本阶段目标不是完整上线运营统计查询,而是先完成两件基础工作:
|
||||
|
||||
1. 收紧个人任务系统的埋点范围,避免运营或接口把个人任务错误配置为 `site`、`module`、`work` 等非用户维度。
|
||||
2. 新增统一日期维表 `analytics_date_dimension`,为后续按周、月、季、年聚合埋点数据提供稳定的日期 bucket 映射。
|
||||
|
||||
## 当前已完成范围
|
||||
|
||||
### 1. 个人任务埋点范围锁定为 User
|
||||
|
||||
当前个人任务系统首版只支持用户维度埋点。
|
||||
|
||||
已完成:
|
||||
|
||||
- Admin 任务配置页不再展示“埋点范围”选择。
|
||||
- Admin 保存任务配置时固定传 `scopeKind: 'user'`。
|
||||
- API 层拒绝非 `user` 的个人任务配置。
|
||||
- 领域输入构造层拒绝非 `User` 的个人任务配置。
|
||||
- `Work => user_id` 的错误映射已移除。
|
||||
- 任务进度刷新、任务中心快照、领奖链路遇到非 `User` 的异常个人任务配置时显式报错,不再静默按 0 进度处理。
|
||||
|
||||
相关文件:
|
||||
|
||||
```text
|
||||
apps/admin-web/src/pages/AdminTaskConfigPage.tsx
|
||||
apps/admin-web/src/api/adminApiTypes.ts
|
||||
server-rs/crates/api-server/src/runtime_profile.rs
|
||||
server-rs/crates/module-runtime/src/commands.rs
|
||||
server-rs/crates/module-runtime/src/errors.rs
|
||||
server-rs/crates/spacetime-module/src/runtime/profile.rs
|
||||
```
|
||||
|
||||
### 2. 日期维表领域模型与纯函数
|
||||
|
||||
已在 `module-runtime` 中补充日期维表快照和纯函数。
|
||||
|
||||
日期维表使用现有北京时间业务日 `day_key` 语义:
|
||||
|
||||
```text
|
||||
date_key = floor((occurred_at_micros + 8h) / 1d)
|
||||
```
|
||||
|
||||
已完成能力:
|
||||
|
||||
- 从 `YYYY-MM-DD` 解析业务日 `date_key`。
|
||||
- 从 `date_key` 构造日期维表快照。
|
||||
- 生成 ISO weekday:周一=1,周日=7。
|
||||
- 生成 ISO week key:`YYYYWW`,跨年周按 ISO week-year。
|
||||
- 生成 week/month/quarter/year 的 key 和起止 `date_key`。
|
||||
- 限制日期维表支持范围为:
|
||||
- `2000-01-01`
|
||||
- 到 `2100-12-31`
|
||||
|
||||
相关文件:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-runtime/src/domain.rs
|
||||
server-rs/crates/module-runtime/src/application.rs
|
||||
server-rs/crates/module-runtime/src/lib.rs
|
||||
```
|
||||
|
||||
### 3. SpacetimeDB 日期维表与 reducer
|
||||
|
||||
已新增 SpacetimeDB 表:
|
||||
|
||||
```text
|
||||
analytics_date_dimension
|
||||
```
|
||||
|
||||
表字段包括:
|
||||
|
||||
```text
|
||||
date_key
|
||||
calendar_date
|
||||
weekday
|
||||
iso_week_key
|
||||
week_start_date_key
|
||||
week_end_date_key
|
||||
month_key
|
||||
month_start_date_key
|
||||
month_end_date_key
|
||||
quarter_key
|
||||
quarter_start_date_key
|
||||
quarter_end_date_key
|
||||
year_key
|
||||
year_start_date_key
|
||||
year_end_date_key
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
已新增索引:
|
||||
|
||||
```text
|
||||
iso_week_key
|
||||
month_key
|
||||
quarter_key
|
||||
year_key
|
||||
```
|
||||
|
||||
已新增 reducer:
|
||||
|
||||
```text
|
||||
ensure_analytics_date_dimension_for_date
|
||||
seed_analytics_date_dimensions
|
||||
```
|
||||
|
||||
当前 reducer 行为:
|
||||
|
||||
- `ensure` 单日幂等补齐。
|
||||
- `seed` 按日期范围幂等补齐。
|
||||
- `seed` 拒绝 `start_date > end_date`。
|
||||
- `seed` 单次最多允许 `ANALYTICS_DATE_DIMENSION_MAX_SEED_DAYS = 3660` 天。
|
||||
- 裸 `date_key` 进入 ensure 前先做支持范围校验,避免极端整数进入日历算法。
|
||||
|
||||
相关文件:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-module/src/runtime/analytics_date_dimension.rs
|
||||
server-rs/crates/spacetime-module/src/runtime/mod.rs
|
||||
server-rs/crates/spacetime-module/src/migration.rs
|
||||
docs/technical/SPACETIMEDB_TABLE_CATALOG.md
|
||||
```
|
||||
|
||||
### 4. SpacetimeDB Rust client bindings
|
||||
|
||||
已按项目脚本生成 Rust bindings,并在生成参数中显式包含 private tables/functions:
|
||||
|
||||
```bash
|
||||
PATH="/tmp/spacetime-bin:$PATH" npm run spacetime:generate -- --rust-only
|
||||
```
|
||||
|
||||
本次已修改生成脚本:
|
||||
|
||||
```text
|
||||
scripts/generate-spacetime-bindings.mjs
|
||||
```
|
||||
|
||||
在 `spacetime generate` 参数中加入:
|
||||
|
||||
```text
|
||||
--include-private
|
||||
```
|
||||
|
||||
说明:SpacetimeDB CLI 2.1.0 的参数名是 `--include-private`,不是 `--non-private`。该参数含义是将 private tables/functions 也包含进生成代码,满足 api-server 通过 Rust bindings 访问 module private table/reducer 的需求。
|
||||
|
||||
```text
|
||||
spacetimedb tool version 2.1.0; spacetimedb-lib version 2.1.0
|
||||
```
|
||||
|
||||
生成脚本:
|
||||
|
||||
```text
|
||||
scripts/generate-spacetime-bindings.mjs
|
||||
```
|
||||
|
||||
已新增 analytics date dimension 相关 bindings:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-client/src/module_bindings/analytics_date_dimension_ensure_input_type.rs
|
||||
server-rs/crates/spacetime-client/src/module_bindings/analytics_date_dimension_seed_input_type.rs
|
||||
server-rs/crates/spacetime-client/src/module_bindings/analytics_date_dimension_type.rs
|
||||
server-rs/crates/spacetime-client/src/module_bindings/analytics_date_dimension_table.rs
|
||||
server-rs/crates/spacetime-client/src/module_bindings/ensure_analytics_date_dimension_for_date_reducer.rs
|
||||
server-rs/crates/spacetime-client/src/module_bindings/seed_analytics_date_dimensions_reducer.rs
|
||||
```
|
||||
|
||||
并更新了:
|
||||
|
||||
```text
|
||||
server-rs/crates/spacetime-client/src/module_bindings/mod.rs
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- `analytics_date_dimension` 表当前是 private table;由于生成脚本已加 `--include-private`,本次 codegen 已生成 `analytics_date_dimension_table.rs`,可通过 `ctx.db.analytics_date_dimension()` 访问 client cache / query builder。
|
||||
- bindings 目录是自动生成产物,本次以项目脚本整体刷新,除新增 analytics 文件外,也带来了大量已存在 table/reducer/procedure 文件的格式化/生成器输出差异。
|
||||
|
||||
### 5. 测试覆盖
|
||||
|
||||
已新增测试:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-runtime/tests/analytics_date_dimension.rs
|
||||
server-rs/crates/module-runtime/tests/profile_task_scope.rs
|
||||
server-rs/crates/shared-contracts/tests/profile_task_contract.rs
|
||||
```
|
||||
|
||||
覆盖重点:
|
||||
|
||||
- `2024-02-29` 闰年。
|
||||
- `2025-12-29` ISO week 跨年。
|
||||
- `2026-01-01` 跨年周。
|
||||
- `2026-03-31` Q1 结束。
|
||||
- `2026-04-01` Q2 开始。
|
||||
- `2026-12-31` 年末。
|
||||
- 非法日期解析失败。
|
||||
- 超出日期维表支持范围失败。
|
||||
- 个人任务 `scopeKind=user` 成功。
|
||||
- 个人任务 `scopeKind=site/module/work` 失败。
|
||||
- `work` scope 不会静默映射到 `user_id`。
|
||||
- Admin 个人任务配置 contract 保持 `scopeKind: user`。
|
||||
|
||||
## 已验证命令
|
||||
|
||||
从 `server-rs/` 执行:
|
||||
|
||||
```bash
|
||||
cargo fmt -p module-runtime -p spacetime-module -p spacetime-client
|
||||
cargo test -p spacetime-client --no-run
|
||||
cargo test -p spacetime-module --no-run
|
||||
cargo test -p module-runtime --test analytics_date_dimension
|
||||
cargo test -p module-runtime --test profile_task_scope
|
||||
cargo test -p shared-contracts --test profile_task_contract
|
||||
```
|
||||
|
||||
从项目根目录执行:
|
||||
|
||||
```bash
|
||||
npm run admin-web:typecheck
|
||||
```
|
||||
|
||||
当前结果:
|
||||
|
||||
- `spacetime-client --no-run` 编译通过。
|
||||
- `spacetime-module --no-run` 编译通过。
|
||||
- `analytics_date_dimension` 测试通过:8 passed。
|
||||
- `profile_task_scope` 测试通过:3 passed。
|
||||
- `profile_task_contract` 测试通过:2 passed。
|
||||
- `admin-web:typecheck` 通过。
|
||||
|
||||
已知非本阶段阻塞:
|
||||
|
||||
- 完整运行 `cargo test -p spacetime-module` 时,曾出现既有 puzzle 测试失败:
|
||||
|
||||
```text
|
||||
puzzle::tests::puzzle_preview_is_publishable_with_complete_draft FAILED
|
||||
assertion failed: preview.publish_ready
|
||||
```
|
||||
|
||||
该失败与当前埋点范围和日期维表改动无直接关系,本阶段以 `cargo test -p spacetime-module --no-run` 作为编译门禁。
|
||||
|
||||
## 当前未完成 / 暂缓项
|
||||
|
||||
### 1. 暂未新增 spacetime-client facade
|
||||
|
||||
当前没有新增:
|
||||
|
||||
```text
|
||||
SpacetimeClient::ensure_analytics_date_dimension_for_date
|
||||
SpacetimeClient::seed_analytics_date_dimensions
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
- 生成脚本已加入 `--include-private`,private reducer/type/table bindings 已可用于后续 facade 实现。
|
||||
- 但 Step 7/8/9 暂缓,尚未由 `api-server` 或统计查询链路调用该能力。
|
||||
- 如后续只是 SpacetimeDB module 内部写入统计时 ensure,可以直接复用 module 内部 helper,不一定需要远程 client facade。
|
||||
- 若后续需要由 API 或运维接口触发 seed/ensure,可基于本次已生成的 reducer bindings 再补 facade。
|
||||
|
||||
### 2. Step 7/8/9 暂缓
|
||||
|
||||
本阶段未接入:
|
||||
|
||||
- 事件写入链路自动 ensure 日期维表。
|
||||
- 聚合查询 API 的 `granularity = day | week | month | quarter | year`。
|
||||
- shared contracts / 前端 analytics contracts。
|
||||
- 历史事件回填。
|
||||
|
||||
这些应作为后续阶段单独设计和落地。
|
||||
|
||||
## 后续建议顺序
|
||||
|
||||
1. 如需提交本阶段改动,确认是否接受 `module_bindings` 整体刷新带来的大量生成文件 diff。
|
||||
2. 如希望 diff 更小,可评估仅提交 analytics date dimension 相关生成文件与 `mod.rs`;但需要非常谨慎,因为 `module_bindings` 是自动生成产物。
|
||||
3. 如需要由 `api-server` 触发 seed/ensure,再补 `spacetime-client` facade。
|
||||
4. 进入 Step 7/8/9:事件写入链路、聚合查询 API、前端 contracts。
|
||||
|
||||
## Step 7/8/9 后续接入记录(2026-05-04)
|
||||
|
||||
本次继续推进此前暂缓的 Step 7/8/9 中“按日期维度聚合查询 API / contracts / client facade”部分。
|
||||
|
||||
### 已新增能力
|
||||
|
||||
1. `module-runtime` 新增 analytics metric 聚合领域类型与纯函数:
|
||||
- `AnalyticsGranularity = day | week | month | quarter | year`
|
||||
- `AnalyticsMetricQueryInput`
|
||||
- `AnalyticsBucketMetric`
|
||||
- `AnalyticsMetricQueryResponse`
|
||||
- `aggregate_runtime_tracking_daily_stats(...)`
|
||||
|
||||
2. `spacetime-module` 新增 `query_analytics_metric` procedure,直接聚合 tracking daily stat,输出按 bucket 排序的统计结果。
|
||||
|
||||
3. `spacetime-client` 新增 facade:
|
||||
|
||||
```rust
|
||||
SpacetimeClient::query_analytics_metric(event_key, scope_kind, scope_id, granularity)
|
||||
```
|
||||
|
||||
4. `api-server` 新增登录态接口:
|
||||
|
||||
```http
|
||||
GET /api/profile/analytics/metric?eventKey=...&scopeKind=user&scopeId=...&granularity=day
|
||||
```
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `eventKey` | 埋点事件 key,必填 |
|
||||
| `scopeKind` | `site | work | module | user` |
|
||||
| `scopeId` | 对应范围 ID,必填 |
|
||||
| `granularity` | `day | week | month | quarter | year` |
|
||||
|
||||
响应 data:
|
||||
|
||||
```ts
|
||||
type AnalyticsMetricQueryResponse = {
|
||||
buckets: Array<{
|
||||
bucketKey: string;
|
||||
bucketStartDateKey: number;
|
||||
bucketEndDateKey: number;
|
||||
value: number;
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
5. shared contracts / 前端 shared contracts 已新增 analytics query 类型:
|
||||
- `AnalyticsMetricQueryRequest`
|
||||
- `AnalyticsMetricQueryResponse`
|
||||
- `AnalyticsBucketMetricResponse` / `AnalyticsBucketMetric`
|
||||
- `AnalyticsGranularity`
|
||||
|
||||
### 本次验证
|
||||
|
||||
从 `server-rs/` 执行通过:
|
||||
|
||||
```bash
|
||||
cargo test -p module-runtime --test analytics_granularity
|
||||
cargo check -p spacetime-module
|
||||
cargo check -p spacetime-client
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
验证结果:
|
||||
|
||||
- `analytics_granularity` 测试通过:3 passed。
|
||||
- `spacetime-module` 编译通过,仅存在既有 dead_code warnings。
|
||||
- `spacetime-client` 编译通过。
|
||||
- `api-server` 编译通过,仅存在既有 prompt dead_code warnings。
|
||||
|
||||
### 注意事项
|
||||
|
||||
当前环境未检测到 `spacetime` / `spacetimedb` CLI,因此 analytics metric 相关 `module_bindings` 是按现有生成物结构手动补齐的临时生成物。后续有 CLI 的开发机应优先通过项目脚本重新生成 bindings,并复核手写生成物是否可被正式生成输出覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 阶段结论
|
||||
|
||||
当前阶段已经完成“个人任务埋点范围收紧”和“日期维表 module 侧能力”的核心落地,并已生成 SpacetimeDB Rust client bindings。
|
||||
|
||||
剩余工作不再是 bindings 环境阻塞,而是后续业务接入范围:是否增加 `spacetime-client` facade,以及是否继续推进事件写入链路、聚合查询 API 和前端 analytics contracts。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 创作 Agent 流式失败保留可见回复修复 2026-05-05
|
||||
|
||||
## 1. 问题
|
||||
|
||||
方洞挑战等轻量玩法复用 `usePlatformCreationAgentFlowController` 与 `creationAgentSse.ts` 消费 `reply_delta / session / error`。当上游 LLM 已经返回部分 `replyText`,但后续因为超时、上游断流、SSE 解析或最终 JSON 解析失败而发送 `error` 事件时,前端会在 `finally` 里退出流式态。
|
||||
|
||||
旧 UI 只在 `isStreamingReply=true` 时展示临时 assistant 气泡,因此用户会先看到一段回答,然后回答突然消失,只剩错误提示。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
1. 已经展示给用户的流式回复不能因为最终失败从聊天区消失。
|
||||
2. SSE `error` 仍然终止本轮提交,并保留错误提示。
|
||||
3. 后端错误不能只压成 `上游服务请求失败`,应优先把 LLM 流错误原因放到业务 `message`。
|
||||
4. 不修改 SpacetimeDB schema、消息表结构或玩法运行规则。
|
||||
|
||||
## 3. 前端契约
|
||||
|
||||
`readCreationAgentSessionFromSse()` 在收到 `reply_delta` 后再收到 `error` 时,必须先触发 `onUpdate(text)`,再抛出错误。调用方可以从最近一次可见文本中恢复 UI。
|
||||
|
||||
`usePlatformCreationAgentFlowController.submitMessage()` 的失败收尾规则:
|
||||
|
||||
1. 提交时仍先追加 optimistic user message。
|
||||
2. 每次 `onUpdate` 同步更新 `streamingReplyText` 与最近可见回复引用。
|
||||
3. 如果 `streamMessage()` 抛错且最近可见回复非空,把该文本追加为本地 assistant `warning` 消息。
|
||||
4. 再设置 `error`,最后关闭 `isStreamingReply`。
|
||||
5. 成功拿到最终 session 时,以后端 session snapshot 为准,并清空最近可见回复。
|
||||
|
||||
这条本地 `warning` 消息只用于失败态 UI 保留,不代表该 assistant 消息已经写入 SpacetimeDB。
|
||||
|
||||
## 4. 后端契约
|
||||
|
||||
`creation_agent_llm_turn` 在 `LlmClient::stream_text()` 失败时,返回:
|
||||
|
||||
```text
|
||||
<玩法 generation_failed 文案>:<LlmError Display>
|
||||
```
|
||||
|
||||
同时写 `warn` 日志,便于结合 `logs/llm-raw` 定位上游原始输入输出。
|
||||
|
||||
方洞挑战 SSE 错误提取优先级:
|
||||
|
||||
1. `error.details.message`
|
||||
2. `error.message`
|
||||
3. 其它嵌套 JSON message
|
||||
4. 原始 body 文本
|
||||
5. 状态码兜底
|
||||
|
||||
## 5. 验收
|
||||
|
||||
1. `reply_delta` 后收到 `error` 时,测试应断言 `onUpdate` 已经收到可见文本。
|
||||
2. 控制器测试应断言失败后本地消息列表包含 user 消息和 assistant warning 消息。
|
||||
3. `cargo check -p api-server` 通过。
|
||||
4. `npm run typecheck` 与编码检查通过。
|
||||
@@ -22,6 +22,7 @@
|
||||
| 大鱼吃小鱼 | 否 | 是 | 功能仍保留,不在新建作品入口展示 |
|
||||
| 拼图 | 是 | 是 | 点击后进入拼图 Agent 共创工作台 |
|
||||
| 抓大鹅 | 否 | 是 | 暂时从创作端入口下线,既有链路与作品能力保留 |
|
||||
| 方洞挑战 | 是 | 是 | 点击后进入方洞挑战 Agent 共创工作台,支持草稿、结果页、发布、试玩、作品架与广场 |
|
||||
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
|
||||
@@ -31,3 +32,4 @@
|
||||
2. 隐藏玩法不触发入口预加载,也不出现在新建作品入口中。
|
||||
3. 未开放玩法点击态保持禁用,不应进入鉴权或创建会话链路。
|
||||
4. 已开放玩法点击后必须进入对应创建链路;若用户未登录,先走登录保护。
|
||||
5. 方洞挑战作品发布后应生成 `SH-` 作品号,并能从作品架、广场详情和试玩 runtime 回到同一作品详情。
|
||||
|
||||
@@ -195,7 +195,8 @@ Jenkins 可运行在 Windows 或其他机器上,本机 Windows 只作为人工
|
||||
|
||||
- Jenkins Job 参数不暴露真实节点名、IP 或带 IP 的标签。
|
||||
- 构建 Job 固定使用 label expression:`linux && genarrative-build`。
|
||||
- 当前开发/构建/开发部署 agent 必须同时配置 `linux` 与 `genarrative-build` 两个标签;非 Linux 节点不能承担构建或部署。
|
||||
- 当前开发/构建/开发部署 agent 使用脱敏节点名 `genarrative-build-01`,必须同时配置 `linux` 与 `genarrative-build` 两个标签;非 Linux 节点不能承担构建或部署。
|
||||
- 构建机 agent 启动方式统一改为 inbound agent + systemd 自守护,不再依赖 Jenkins controller 通过 SSH launcher 长期拉起。SSH 只作为首次登录和安装 systemd 服务的运维通道。
|
||||
- 用途:拉代码、安装依赖、构建主站、构建后台、构建 `api-server`、构建 SpacetimeDB wasm、归档产物,并执行 `DEPLOY_TARGET=development` 的开发环境部署。
|
||||
|
||||
### 生产/发布实例
|
||||
@@ -209,25 +210,34 @@ Jenkins 可运行在 Windows 或其他机器上,本机 Windows 只作为人工
|
||||
|
||||
### Jenkins inbound agent 自恢复
|
||||
|
||||
发布 agent 必须由目标 Linux 机器主动连接 Jenkins controller,并由 systemd 托管:
|
||||
构建 agent 与发布 agent 都必须由目标 Linux 机器主动连接 Jenkins controller,并由 systemd 托管:
|
||||
|
||||
- Jenkins 节点 Launch method 使用 inbound agent,优先启用 WebSocket。这样目标机只需要能访问 Jenkins Web 地址,不依赖 controller 每次 SSH 拉起 agent。
|
||||
- 目标机安装 `deploy/systemd/jenkins-agent@.service`、`scripts/deploy/jenkins-inbound-agent-start.sh` 与 `scripts/deploy/install-jenkins-inbound-agent.sh`。
|
||||
- systemd 服务名采用 `jenkins-agent@<node-name>.service`,例如 `jenkins-agent@genarrative-release-deploy-01.service`。
|
||||
- systemd 自身 `WorkingDirectory` 保持 `/var/lib/jenkins/agent/<node-name>`;Jenkins remoting `-workDir` 可继续使用旧 SSH agent 的 `/root/jenkins-agent`,避免迁移时 workspace 和缓存路径漂移。
|
||||
- systemd 服务名采用 `jenkins-agent@<node-name>.service`,例如 `jenkins-agent@genarrative-build-01.service`、`jenkins-agent@genarrative-release-deploy-01.service`。
|
||||
- systemd 自身 `WorkingDirectory` 保持 `/var/lib/jenkins/agent/<node-name>`;Jenkins remoting `-workDir` 可按节点拆分,例如构建机使用 `/root/jenkins-agent-build`、发布机继续使用旧 SSH agent 的 `/root/jenkins-agent`,避免多 agent 共用 remoting 根目录,同时减少发布机迁移时 workspace 和缓存路径漂移。
|
||||
- inbound secret 只能放在目标机 `/etc/jenkins-agent/<node-name>.secret` 或等价 Secret Text 注入位置,不能提交到 Git,也不能写入 Jenkinsfile 默认参数。
|
||||
- systemd unit 使用 `Restart=always` 和 `RestartSec=10`;agent Java 进程退出、网络短断或机器重启后由 systemd 自动恢复,不需要人工盯着 Jenkins 页面手动重启。
|
||||
- 当前 `Genarrative-Server-Provision` 仍负责 systemd、Nginx、`/opt/genarrative`、`/etc/genarrative` 等特权写入,因此 inbound agent 默认仍按现有 root 执行口径迁移。若后续改为 `jenkins` 用户运行 agent,必须先把生产流水线需要的特权命令收敛为精确 `NOPASSWD` sudoers 白名单。
|
||||
|
||||
如果 Jenkins controller 只运行在本地 Windows,不直接对目标机暴露公网地址,需要在本地控制机启动 `scripts/deploy/jenkins-agent-reverse-tunnel.ps1`。该脚本通过同一条 SSH 会话把远端 `127.0.0.1:18080` 转到本地 Jenkins Web `127.0.0.1:8080`,把远端 `127.0.0.1:50000` 转到本地 Jenkins inbound TCP agent port `127.0.0.1:50000`,并在隧道断开后自动重试。此时远端 agent 的 `JENKINS_URL` 固定写 `http://127.0.0.1:18080/`,不写本地 Windows 的 `127.0.0.1:8080`。
|
||||
|
||||
本地反向隧道脚本不内置目标机地址;注册 Windows 计划任务时必须显式传入 `-RemoteHost <release-agent-host>`,真实 IP 或主机名只保存在本地计划任务配置中,不提交到 Git。
|
||||
本地反向隧道脚本不内置目标机地址;注册 Windows 计划任务时必须显式传入 `-RemoteHost <agent-host>`,真实 IP 或主机名只保存在本地计划任务配置中,不提交到 Git。同一台 Linux 机器上同时运行构建与发布 agent 时,两者共用这一条反向隧道,不为每个 Jenkins 节点重复注册本地隧道任务。
|
||||
|
||||
当 Jenkins controller 以本地 Windows `java -jar jenkins.war` 方式运行时,使用 `scripts/deploy/jenkins-local-controller-watchdog.ps1` 作为本地守护脚本。该脚本只保存本机 Java、`jenkins.war`、`JENKINS_HOME` 和端口路径,不保存 Jenkins 账号、密码、token 或 agent secret;注册 Windows 计划任务后,脚本会在登录后检查 `8080` 是否已有 Jenkins 监听,若已有则监控现有 PID,若进程退出或端口空闲则重新启动 Jenkins,并固定 `--agentPort=50000` 供远端 inbound agent 连接。
|
||||
|
||||
首次迁移示例:
|
||||
|
||||
```bash
|
||||
sudo install -m 0600 /tmp/genarrative-build-01.secret /etc/jenkins-agent/genarrative-build-01.secret
|
||||
sudo scripts/deploy/install-jenkins-inbound-agent.sh \
|
||||
--agent-name genarrative-build-01 \
|
||||
--jenkins-url http://127.0.0.1:18080/ \
|
||||
--secret-file /etc/jenkins-agent/genarrative-build-01.secret \
|
||||
--workdir /root/jenkins-agent-build \
|
||||
--java-bin /usr/bin/java
|
||||
sudo systemctl status jenkins-agent@genarrative-build-01.service --no-pager -l
|
||||
|
||||
sudo install -m 0600 /tmp/genarrative-release-deploy-01.secret /etc/jenkins-agent/genarrative-release-deploy-01.secret
|
||||
sudo scripts/deploy/install-jenkins-inbound-agent.sh \
|
||||
--agent-name genarrative-release-deploy-01 \
|
||||
@@ -236,7 +246,7 @@ sudo scripts/deploy/install-jenkins-inbound-agent.sh \
|
||||
--workdir /root/jenkins-agent \
|
||||
--java-bin /usr/bin/java
|
||||
sudo systemctl status jenkins-agent@genarrative-release-deploy-01.service --no-pager -l
|
||||
journalctl -u jenkins-agent@genarrative-release-deploy-01.service -f
|
||||
journalctl -u 'jenkins-agent@*.service' -f
|
||||
```
|
||||
|
||||
如果 Jenkins controller 暂时仍配置为 SSH launcher,只能作为过渡方案使用:需要把 SSH launch timeout 拉长、增加 retry 和 retry wait、固定 Java 路径,并确认 `ssh user@host 'java -version'` 稳定返回。最终仍要切到 inbound + systemd,避免 SSH 连接卡住时阻塞发布队列。
|
||||
|
||||
18
docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md
Normal file
18
docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 个人任务 scope 限制说明(2026-05-04)
|
||||
|
||||
## 背景
|
||||
|
||||
个人任务配置首版只支持按用户维度统计进度,即 `RuntimeTrackingScopeKind::User` / API `scopeKind: "user"`。`site`、`module`、`work` 未来可作为全站、模块或作品维度任务扩展,但当前不应被个人任务配置接受。
|
||||
|
||||
## 后端约束
|
||||
|
||||
- HTTP 管理接口 `admin_upsert_profile_task_config` 在解析 `scopeKind` 后立即校验:非 `user` 返回 400,并提示“个人任务 scopeKind 首版仅支持 user”。
|
||||
- 领域构造函数 `build_runtime_profile_task_config_admin_upsert_input` 兜底校验:非 `RuntimeTrackingScopeKind::User` 返回 `RuntimeProfileFieldError::UnsupportedProfileTaskScopeKind`。
|
||||
- SpacetimeDB 模块内 `profile_task_tracking_scope_id` 不再把 `Work` 静默映射到 `user_id`;非 User scope 返回 `None`,个人任务进度读取按 0 处理,避免错误串桶。
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
`module-runtime` 单元测试覆盖:
|
||||
|
||||
- `User` scope 可成功构造个人任务配置输入。
|
||||
- `Site` / `Module` / `Work` scope 均被拒绝,错误为 `UnsupportedProfileTaskScopeKind`。
|
||||
@@ -24,11 +24,12 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
|
||||
| --- | --- |
|
||||
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
|
||||
| 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` |
|
||||
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_save_archive` |
|
||||
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `analytics_date_dimension`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_save_archive` |
|
||||
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
|
||||
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
|
||||
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_event`, `puzzle_runtime_run`, `puzzle_leaderboard_entry` |
|
||||
| 抓大鹅 Match3D | `match3d_agent_session`, `match3d_agent_message`, `match3d_work_profile`, `match3d_runtime_run` |
|
||||
| 方洞挑战 | `square_hole_agent_session`, `square_hole_agent_message`, `square_hole_work_profile`, `square_hole_runtime_run` |
|
||||
| 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_event`, `big_fish_runtime_run` |
|
||||
| 资产 | `asset_object`, `asset_entity_binding`, `asset_event` |
|
||||
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference`, `ai_task_event` |
|
||||
@@ -158,6 +159,20 @@ SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>';
|
||||
SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### `analytics_date_dimension`
|
||||
|
||||
- 作用:分析日期维表,每个北京时间业务自然日一行,用于把日桶映射到周、月、季度和年。
|
||||
- 结构:`date_key PK: i64`, `calendar_date: String`, `weekday: u8`, `iso_week_key: i32`, `week_start_date_key: i64`, `week_end_date_key: i64`, `month_key: i32`, `month_start_date_key: i64`, `month_end_date_key: i64`, `quarter_key: i32`, `quarter_start_date_key: i64`, `quarter_end_date_key: i64`, `year_key: i32`, `year_start_date_key: i64`, `year_end_date_key: i64`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:主键 `date_key`,`iso_week_key`,`month_key`,`quarter_key`,`year_key`。
|
||||
- 写入口:`ensure_analytics_date_dimension_for_date({ date_key })` 幂等补单日;`seed_analytics_date_dimensions({ start_date, end_date })` 按 `YYYY-MM-DD` 闭区间幂等批量补种,单次最多 `3660` 天。
|
||||
- 口径:`date_key` 沿用当前埋点日桶 `floor((occurred_at_micros + 8h) / 1d)`,`calendar_date` 是该北京时间业务日的公历日期。
|
||||
|
||||
```sql
|
||||
SELECT * FROM analytics_date_dimension WHERE date_key = <date_key>;
|
||||
SELECT * FROM analytics_date_dimension WHERE iso_week_key = 202501 ORDER BY date_key;
|
||||
SELECT * FROM analytics_date_dimension WHERE month_key = 202402 ORDER BY date_key;
|
||||
```
|
||||
|
||||
### `tracking_event`
|
||||
|
||||
- 作用:埋点原始事件表,保存整站、作品、模块和用户层的原始事实。
|
||||
@@ -655,6 +670,53 @@ SELECT * FROM match3d_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY upd
|
||||
SELECT * FROM match3d_runtime_run WHERE profile_id = '<profile_id>';
|
||||
```
|
||||
|
||||
## 方洞挑战表
|
||||
|
||||
### `square_hole_agent_session`
|
||||
|
||||
- 作用:方洞挑战创作 Agent 会话表,保存种子、配置 JSON、草稿 JSON 和发布 profile 指针。
|
||||
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: String`, `config_json: String`, `draft_json: String`, `last_assistant_reply: String`, `published_profile_id: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM square_hole_agent_session WHERE session_id = '<session_id>';
|
||||
SELECT * FROM square_hole_agent_session WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `square_hole_agent_message`
|
||||
|
||||
- 作用:方洞挑战创作 Agent 消息流水。
|
||||
- 结构:`message_id PK: String`, `session_id: String`, `role: String`, `kind: String`, `text: String`, `created_at: Timestamp`。
|
||||
- 索引:`session_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM square_hole_agent_message WHERE session_id = '<session_id>' ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
### `square_hole_work_profile`
|
||||
|
||||
- 作用:方洞挑战作品主表,保存作品基础信息、反直觉规则、配置、发布状态和游玩次数。
|
||||
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `twist_rule: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `shape_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`。
|
||||
- 索引:`owner_user_id`, `publication_status`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM square_hole_work_profile WHERE profile_id = '<profile_id>';
|
||||
SELECT * FROM square_hole_work_profile WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM square_hole_work_profile WHERE publication_status = 'Published';
|
||||
```
|
||||
|
||||
### `square_hole_runtime_run`
|
||||
|
||||
- 作用:方洞挑战单局运行态表,保存后端权威快照、快照版本、胜负状态和成绩基础字段。
|
||||
- 结构:`run_id PK: String`, `owner_user_id: String`, `profile_id: String`, `status: String`, `snapshot_version: u64`, `started_at_ms: i64`, `duration_limit_ms: i64`, `finished_at_ms: i64`, `elapsed_ms: i64`, `total_shape_count: u32`, `completed_shape_count: u32`, `score: u32`, `snapshot_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`, `profile_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM square_hole_runtime_run WHERE run_id = '<run_id>';
|
||||
SELECT * FROM square_hole_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM square_hole_runtime_run WHERE profile_id = '<profile_id>';
|
||||
```
|
||||
|
||||
## 大鱼吃小鱼表
|
||||
|
||||
### `big_fish_creation_session`
|
||||
|
||||
Reference in New Issue
Block a user