Merge origin/master into codex/wechat
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# 后台创作入口开关操作入口
|
||||
|
||||
日期:2026-05-11
|
||||
|
||||
## 背景
|
||||
|
||||
创作中心入口配置已经迁移到 SpacetimeDB,前端创作中心与 api-server 路由熔断共用同一份入口配置。为了让运营/管理员可以调整这张开关表,需要在后台提供显式操作入口,而不是只通过数据库表查询或手工 reducer 操作。
|
||||
|
||||
## 后台入口
|
||||
|
||||
后台新增导航:
|
||||
|
||||
- 名称:入口开关
|
||||
- hash:`#creation-entry`
|
||||
- 页面:`apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx`
|
||||
|
||||
页面能力:
|
||||
|
||||
- 读取当前入口开关表。
|
||||
- 编辑单个入口:
|
||||
- `id`
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `badge`
|
||||
- `imageSrc`
|
||||
- `visible`
|
||||
- `open`
|
||||
- `sortOrder`
|
||||
- 保存前复用后台写操作确认弹窗。
|
||||
- 保存后重新用后端返回的配置刷新列表。
|
||||
|
||||
## API
|
||||
|
||||
后台新增受管理员鉴权保护的接口:
|
||||
|
||||
```text
|
||||
GET /admin/api/creation-entry/config
|
||||
POST /admin/api/creation-entry/config
|
||||
```
|
||||
|
||||
POST body:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "puzzle",
|
||||
"title": "拼图",
|
||||
"subtitle": "拼图关卡创作",
|
||||
"badge": "可创建",
|
||||
"imageSrc": "/creation-type-references/puzzle.webp",
|
||||
"visible": true,
|
||||
"open": true,
|
||||
"sortOrder": 30
|
||||
}
|
||||
```
|
||||
|
||||
响应统一返回当前全量入口列表,方便后台页面直接刷新本地状态。
|
||||
|
||||
## 入库链路
|
||||
|
||||
```text
|
||||
Admin Web
|
||||
-> api-server /admin/api/creation-entry/config
|
||||
-> AppState::upsert_creation_entry_type_config
|
||||
-> spacetime-client procedure upsert_creation_entry_type_config
|
||||
-> spacetime-module creation_entry_type_config 表
|
||||
```
|
||||
|
||||
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态,并让 api-server 熔断对应玩法创作 / 运行态 API。隐藏入口但仍保留既有作品号、广场详情或试玩链路时,应只关闭 `visible`,不要关闭 `open`。
|
||||
|
||||
## 注意
|
||||
|
||||
- 前端后台页面只做管理表单,不成为配置事实源。
|
||||
- `src/config/newWorkEntryConfig.ts` 不应恢复。
|
||||
- SpacetimeDB client bindings 当前新增了对应临时 binding 文件;后续执行标准 bindings regenerate 时应覆盖并保持同名 procedure/type。
|
||||
98
docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md
Normal file
98
docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 后台数据库表查询技术方案(2026-05-08)
|
||||
|
||||
## 背景
|
||||
|
||||
后台“总览”页已经通过 `/admin/api/overview` 展示 SpacetimeDB 表统计,但只能看到表名、行数和统计状态。运营和排障时需要从统计行直接进入单表查询页,按基础条件快速查看真实行数据。
|
||||
|
||||
## 目标
|
||||
|
||||
- 在后台新增“表查询”页,支持所有 schema 表的只读查询。
|
||||
- “总览 / 表统计”中的每一行可点击跳转到对应表的查询页。
|
||||
- 提供基础查询能力:表选择、关键词搜索、JSON 条件过滤、条数限制、刷新、查看行详情。
|
||||
- 不修改 SpacetimeDB 表结构,不新增 reducer,不引入写操作。
|
||||
|
||||
## 后续增强
|
||||
|
||||
- 查询页增加“重置条件”快捷操作,便于运营快速回到默认筛选状态。
|
||||
- 行详情支持一键复制完整 JSON,减少人工选中复制的操作成本。
|
||||
- 查询页顶部增加轻量摘要,显示当前选表和可见列数,方便移动端快速确认上下文。
|
||||
|
||||
## 后端接口
|
||||
|
||||
### `GET /admin/api/database/tables`
|
||||
|
||||
鉴权:沿用 `require_admin_auth`。
|
||||
|
||||
数据来源:SpacetimeDB schema HTTP API。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"tables": ["tracking_event", "user_account"],
|
||||
"fetchErrors": []
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/api/database/tables/{tableName}/rows`
|
||||
|
||||
鉴权:沿用 `require_admin_auth`。
|
||||
|
||||
Query:
|
||||
|
||||
- `limit`:默认 100,范围 1-500。
|
||||
- `search`:可选,前端关键词;后端返回行后在 JSON 文本中大小写不敏感过滤。
|
||||
- `filters`:可选 JSON object 字符串,例如 `{"user_id":"u1","enabled":true}`;后端返回行后按字段等值过滤。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"tableName": "tracking_event",
|
||||
"columns": ["event_id", "event_key"],
|
||||
"rows": [
|
||||
{
|
||||
"cells": {
|
||||
"event_id": "event-1",
|
||||
"event_key": "daily_login"
|
||||
},
|
||||
"raw": ["event-1", "daily_login"]
|
||||
}
|
||||
],
|
||||
"totalReturned": 1,
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
||||
实现约束:
|
||||
|
||||
- 表名必须来自 schema 且通过标识符安全校验,避免任意 SQL 注入。
|
||||
- SQL 固定为 `SELECT * FROM {tableName} LIMIT {limit}`;SpacetimeDB 2.2 HTTP SQL 不拼 `ORDER BY`。
|
||||
- 用户输入不直接拼入 SQL;关键词和条件在 API Server 内存中过滤。
|
||||
- private 表或 token 不可见时返回后台可读错误信息。
|
||||
- SpacetimeDB SQL 行和 SATS 值统一转成人可读 JSON:Option None 为 null,Some 展开为内部值,Timestamp 单元素数组展开为内部值,enum 可保留 tag/name 或原始数组文本。
|
||||
|
||||
## 前端页面
|
||||
|
||||
路由:`#tables`,导航名“表查询”。
|
||||
|
||||
页面能力:
|
||||
|
||||
- 表选择下拉展示中文表名并保留原始表名,支持 URL hash `#tables?table=xxx` 直达指定表。
|
||||
- 查询表单:表名、关键词、JSON 条件、条数。
|
||||
- 查询结果表格横向滚动,移动端不撑坏布局。
|
||||
- 查询结果标题和已选表摘要展示中文表名,鼠标悬浮显示原始表名和表说明,方便运营识别真实数据域。
|
||||
- 表头支持点击排序,排序只作用于当前已拉取的行数据,不改变后端 SQL。
|
||||
- 表头展示中文字段名,鼠标悬浮显示原始字段名、字段说明和排序提示,方便运营阅读且保留排障所需的真实列名。
|
||||
- 单元格内容过长时在表格内单行省略,完整内容可通过悬浮标题或行详情弹层查看。
|
||||
- 每行提供“详情”按钮,以独立弹层展示完整 JSON。
|
||||
- 总览表统计行点击后跳转到 `#tables?table={tableName}`。
|
||||
|
||||
## 验收
|
||||
|
||||
- `cd server-rs && cargo fmt -p api-server -p shared-contracts --check`
|
||||
- `cd server-rs && cargo test -p api-server admin_database -- --nocapture`
|
||||
- `npm run admin-web:typecheck`
|
||||
- `npm run admin-web:build`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
170
docs/technical/ADMIN_TRACKING_EVENT_DETAIL_EXPORT_2026-05-07.md
Normal file
170
docs/technical/ADMIN_TRACKING_EVENT_DETAIL_EXPORT_2026-05-07.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 后台埋点数据明细与 Excel 导出方案
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在百梦后台新增“埋点数据”页,展示每条埋点原始事件的详细字段,并支持导出为 Excel 可直接打开的表格文件。
|
||||
|
||||
**Architecture:** 后端继续由 `api-server` 作为后台 BFF,经 SpacetimeDB HTTP SQL 只读查询 `tracking_event`,不改变表结构和 reducer。前端在 `apps/admin-web` 中新增独立路由与页面,页面渲染后端返回的原始明细,并在浏览器侧导出 Excel 兼容的 `.xls` HTML 表格,避免新增依赖。
|
||||
|
||||
**Tech Stack:** Rust Axum、SpacetimeDB HTTP SQL、shared-contracts、React 19、TypeScript、Vite。
|
||||
|
||||
---
|
||||
|
||||
## 范围
|
||||
|
||||
本次只做后台只读能力:
|
||||
|
||||
- 展示 `tracking_event` 原始事件明细。
|
||||
- 每条埋点展示:事件 ID、Event Key、事件名称、Scope、Scope ID、Day Key、用户 ID、作品拥有者、Profile ID、模块、metadata、发生时间。
|
||||
- 支持按 Event Key、用户 ID、Scope Kind、Scope ID 筛选。
|
||||
- 支持导出当前筛选结果为 Excel 可打开文件。
|
||||
|
||||
不做:
|
||||
|
||||
- 不新增或修改 SpacetimeDB 表结构。
|
||||
- 不在后台写入或删除埋点。
|
||||
- 不把埋点聚合口径下沉到前端计算。
|
||||
|
||||
## 后端契约
|
||||
|
||||
新增接口:
|
||||
|
||||
```text
|
||||
GET /admin/api/tracking/events?eventKey=&userId=&scopeKind=&scopeId=&limit=
|
||||
```
|
||||
|
||||
鉴权:复用后台 `require_admin_auth`。
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"eventId": "daily-login:user:xxx:123",
|
||||
"eventKey": "daily_login",
|
||||
"eventTitle": "每日登录",
|
||||
"scopeKind": "user",
|
||||
"scopeId": "xxx",
|
||||
"dayKey": 20580,
|
||||
"userId": "xxx",
|
||||
"ownerUserId": null,
|
||||
"profileId": null,
|
||||
"moduleKey": "profile",
|
||||
"metadataJson": "{}",
|
||||
"occurredAt": "2026-05-07T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
后端实现要点:
|
||||
|
||||
1. DTO 放在 `shared-contracts/src/admin.rs`,避免 Rust 与前端口径分叉。
|
||||
2. Handler 放在 `api-server/src/admin.rs`,使用当前已有 SpacetimeDB HTTP SQL helper 思路。
|
||||
3. SQL 只读 `tracking_event`,固定白名单列;由于 SpacetimeDB 2.2 HTTP SQL 不支持 `ORDER BY`,后端取回默认 200 / 最大 1000 条后在 API 层按 `occurred_at` 倒序排序。
|
||||
4. 查询条件只通过字符串转义函数拼接,禁止直接拼接未转义用户输入。
|
||||
5. `eventTitle` 由后端根据已知事件 key 映射,未知事件返回 `eventKey`。
|
||||
|
||||
## 前端页面
|
||||
|
||||
新增路由:`#tracking`,导航标题为“埋点数据”。
|
||||
|
||||
页面能力:
|
||||
|
||||
1. 顶部筛选区:Event Key、用户 ID、Scope Kind、Scope ID、刷新、导出 Excel。Event Key 候选来自后台前端的埋点定义注册表,需覆盖 `BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md` 中已接入的通用埋点事件,不能只保留 `daily_login`。
|
||||
2. 列表区:移动端可横向滚动,桌面端表格展示。
|
||||
3. 详情区:每行有“详情”按钮,弹出独立面板展示完整字段与格式化后的 metadata JSON。
|
||||
4. 导出:导出当前页面已加载结果,文件名形如 `tracking-events-2026-05-07.xls`。
|
||||
|
||||
导出实现:
|
||||
|
||||
- 使用 HTML table + Excel MIME:`application/vnd.ms-excel;charset=utf-8`。
|
||||
- 文件扩展名使用 `.xls`,Excel/WPS 可直接打开。
|
||||
- 所有单元格做 HTML 转义。
|
||||
- metadata 保留原始 JSON 文本,便于运营继续筛选。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run admin-web:typecheck
|
||||
cargo test -p shared-contracts -p api-server admin_tracking -- --nocapture
|
||||
```
|
||||
|
||||
如后端接口改动较大,再补充:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
curl http://127.0.0.1:<port>/healthz
|
||||
```
|
||||
|
||||
## 实施任务
|
||||
|
||||
### Task 1: 补充 shared-contracts 后台埋点 DTO
|
||||
|
||||
**Files:**
|
||||
- Modify: `server-rs/crates/shared-contracts/src/admin.rs`
|
||||
|
||||
**Steps:**
|
||||
1. 新增 `AdminTrackingEventListQuery`。
|
||||
2. 新增 `AdminTrackingEventEntryPayload`。
|
||||
3. 新增 `AdminTrackingEventListResponse`。
|
||||
4. 为 DTO 添加中文注释。
|
||||
|
||||
### Task 2: 增加后端后台埋点查询接口
|
||||
|
||||
**Files:**
|
||||
- Modify: `server-rs/crates/api-server/src/admin.rs`
|
||||
- Modify: `server-rs/crates/api-server/src/app.rs`
|
||||
|
||||
**Steps:**
|
||||
1. 在 `admin.rs` 新增 query 解析与 SQL 构造。
|
||||
2. 复用 SpacetimeDB HTTP SQL 调用风格读取 rows。
|
||||
3. 新增 `admin_list_tracking_events` handler。
|
||||
4. 在 `app.rs` 挂载 `/admin/api/tracking/events`。
|
||||
5. 添加单元测试覆盖 SQL 字符串转义、limit clamp、SQL 响应解析。
|
||||
|
||||
### Task 3: 增加前端 API 类型与客户端方法
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/admin-web/src/api/adminApiTypes.ts`
|
||||
- Modify: `apps/admin-web/src/api/adminApiClient.ts`
|
||||
|
||||
**Steps:**
|
||||
1. 新增埋点 entry/list/query 类型。
|
||||
2. 新增 `listAdminTrackingEvents(token, query)`。
|
||||
3. 使用 `URLSearchParams` 拼接非空查询字段。
|
||||
|
||||
### Task 4: 新增后台埋点数据页面
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/admin-web/src/pages/AdminTrackingEventsPage.tsx`
|
||||
- Modify: `apps/admin-web/src/styles/admin.css`
|
||||
|
||||
**Steps:**
|
||||
1. 实现筛选、刷新、错误状态。
|
||||
2. 实现明细表格。
|
||||
3. 实现独立详情面板。
|
||||
4. 实现 Excel `.xls` 导出。
|
||||
5. 保持 UI 简洁,不添加说明类大段文案。
|
||||
|
||||
### Task 5: 接入后台路由与导航
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/admin-web/src/app/adminRoutes.ts`
|
||||
- Modify: `apps/admin-web/src/app/AdminShell.tsx`
|
||||
- Modify: `apps/admin-web/src/app/AdminApp.tsx`
|
||||
|
||||
**Steps:**
|
||||
1. 增加 `tracking` 路由。
|
||||
2. 导航增加图标。
|
||||
3. `AdminApp` 渲染新页面。
|
||||
|
||||
### Task 6: 验证并提交
|
||||
|
||||
**Steps:**
|
||||
1. 运行 `npm run check:encoding`。
|
||||
2. 运行 `npm run admin-web:typecheck`。
|
||||
3. 运行后端相关 cargo test。
|
||||
4. 修复问题后提交并推送当前分支。
|
||||
@@ -96,8 +96,10 @@ export interface ApiErrorEnvelope {
|
||||
| 当前管理员 | `GET /admin/api/me` | 管理员 Bearer |
|
||||
| 服务与数据库概览 | `GET /admin/api/overview` | 管理员 Bearer |
|
||||
| 受控 HTTP 调试 | `POST /admin/api/debug/http` | 管理员 Bearer |
|
||||
| 读取兑换码列表 | `GET /admin/api/profile/redeem-codes` | 管理员 Bearer |
|
||||
| 创建/更新兑换码 | `POST /admin/api/profile/redeem-codes` | 管理员 Bearer |
|
||||
| 停用兑换码 | `POST /admin/api/profile/redeem-codes/disable` | 管理员 Bearer |
|
||||
| 读取后台邀请码列表 | `GET /admin/api/profile/invite-codes` | 管理员 Bearer |
|
||||
| 创建/更新注册邀请码 | `POST /admin/api/profile/invite-codes` | 管理员 Bearer |
|
||||
|
||||
### 4.3 前端类型命名
|
||||
@@ -190,6 +192,8 @@ export interface AdminDisableProfileRedeemCodeRequest {
|
||||
|
||||
export interface AdminUpsertProfileInviteCodeRequest {
|
||||
inviteCode: string;
|
||||
startsAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -206,13 +210,24 @@ export interface ProfileRedeemCodeAdminResponse {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProfileRedeemCodeAdminListResponse {
|
||||
entries: ProfileRedeemCodeAdminResponse[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ProfileInviteCodeAdminListResponse {
|
||||
entries: ProfileInviteCodeAdminResponse[];
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 登录 contract
|
||||
@@ -284,6 +299,31 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
|
||||
### 4.7 兑换码管理 contract
|
||||
|
||||
列表请求:
|
||||
|
||||
`GET /admin/api/profile/redeem-codes`
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"code": "WELCOME2026",
|
||||
"mode": "public",
|
||||
"rewardPoints": 100,
|
||||
"maxUses": 1,
|
||||
"globalUsedCount": 0,
|
||||
"enabled": true,
|
||||
"allowedUserIds": [],
|
||||
"createdBy": "admin:root",
|
||||
"createdAt": "2026-04-30T00:00:00Z",
|
||||
"updatedAt": "2026-04-30T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
创建/更新请求:
|
||||
|
||||
```json
|
||||
@@ -300,8 +340,6 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
|
||||
停用请求:
|
||||
|
||||
兑换码管理页的最近一次接口返回记录由 `AdminApp` 维护为管理端会话态,并传入 `AdminRedeemCodePage` 渲染。页面页签通过 hash 切换时子页面会卸载,不能把最近记录只放在兑换码页面内部 `useState` 中,否则切换到其他页签再返回会展示“暂无记录”。该会话态只用于保留当前操作结果,不作为兑换码历史列表;退出登录或重新登录时清空。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "WELCOME2026"
|
||||
@@ -325,15 +363,46 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
}
|
||||
```
|
||||
|
||||
兑换码管理页进入时必须通过 `GET /admin/api/profile/redeem-codes` 加载数据库已有记录。最近一次接口返回记录仍由 `AdminApp` 维护为管理端会话态,用于展示当前操作结果;历史列表不得依赖该会话态,刷新页面后必须从后端列表接口恢复。列表项点击后回填表单,继续通过同一个 `POST /admin/api/profile/redeem-codes` 修改原记录。
|
||||
|
||||
前端只做基础输入约束,最终标准化、私有码用户解析、次数和奖励合法性以 `server-rs` 为准。
|
||||
|
||||
### 4.8 邀请码管理 contract
|
||||
|
||||
列表请求:
|
||||
|
||||
`GET /admin/api/profile/invite-codes`
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"userId": "admin:root:SPRING2026",
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"status": "active",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
},
|
||||
"createdAt": "2026-04-30T00:00:00Z",
|
||||
"updatedAt": "2026-04-30T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
后台邀请码列表只返回后台运营预置码。后端按 `profile_invite_code.user_id` 的 `admin:` 前缀过滤,普通用户在邀请中心生成的个人邀请码不得展示在后台列表中。
|
||||
|
||||
创建/更新请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
}
|
||||
@@ -346,6 +415,9 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
{
|
||||
"userId": "admin",
|
||||
"inviteCode": "SPRING2026",
|
||||
"startsAt": "2026-05-01T00:00:00Z",
|
||||
"expiresAt": "2026-06-01T00:00:00Z",
|
||||
"status": "active",
|
||||
"metadata": {
|
||||
"batch": "spring"
|
||||
},
|
||||
@@ -356,6 +428,118 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
|
||||
邀请码页的 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`。
|
||||
@@ -372,19 +556,22 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
3. 总览页加载失败时展示后端错误,不吞掉 `fetchErrors`。
|
||||
4. API 调试页的 headers 使用键值行编辑,提交前转为 `[{ name, value }]`。
|
||||
5. 兑换码页的 `mode=private` 时展示允许用户输入区;其他模式提交空数组。
|
||||
6. 邀请码页只提交 `inviteCode` 与 JSON 对象 metadata,不在前端复制后端邀请码规则。
|
||||
7. 所有按钮的 loading 状态必须锁定重复提交。
|
||||
8. 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。
|
||||
6. 兑换码页和邀请码页进入时加载数据库列表,保存后合并返回记录,点击列表项回填表单进入编辑态。
|
||||
7. 邀请码页只提交 `inviteCode` 与 JSON 对象 metadata,不在前端复制后端邀请码规则。
|
||||
8. 所有按钮的 loading 状态必须锁定重复提交。
|
||||
9. 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。
|
||||
|
||||
## 7. 部署与联调
|
||||
|
||||
### 7.1 本地联调
|
||||
|
||||
1. 启动后端:`npm run api-server`。
|
||||
2. 启动后台前端:在 `apps/admin-web` 执行 `npm run dev`。
|
||||
3. 后台 dev server 通过 Vite proxy 转发 `/admin/api` 到 `ADMIN_API_TARGET`;未配置时默认 `http://127.0.0.1:3100`。
|
||||
4. 若使用非 3100 端口,在仓库根目录 `.env.local` 设置 `ADMIN_API_TARGET=http://127.0.0.1:<api-server-port>`,并重启后台前端 dev server。
|
||||
5. `GENARRATIVE_API_PORT` 控制 Rust `api-server` 监听端口;`ADMIN_API_TARGET` 只控制后台前端 dev proxy 目标,二者需要指向同一个端口。
|
||||
1. 完整本地栈直接在仓库根目录执行 `npm run dev`。
|
||||
2. `npm run dev` 默认启动 SpacetimeDB standalone、Rust `api-server`、主站 Vite 和后台 Vite。
|
||||
3. 主站默认地址为 `http://127.0.0.1:3000`,后台可从主站 `http://127.0.0.1:3000/admin/` 进入,也可直连 `http://127.0.0.1:3102`。
|
||||
4. 主站 Vite 会把 `/admin/` 转发到后台 dev server,贴近生产同域 `/admin/` 入口。
|
||||
5. 后台 dev server 通过 Vite proxy 转发 `/admin/api` 到当前 Rust API 地址;`--api-port` 改动时脚本会同步注入 `ADMIN_API_TARGET`。
|
||||
6. 如需单独启动后台前端,可继续执行根脚本 `npm run admin-web:dev`,或在 `apps/admin-web` 执行 `npm run dev`;单独启动时未配置 `ADMIN_API_TARGET` 会默认代理到 `http://127.0.0.1:3100`。
|
||||
7. 后台 dev 端口可用 `npm run dev -- --admin-web-port <port>` 覆盖。
|
||||
|
||||
### 7.2 构建部署
|
||||
|
||||
@@ -430,8 +617,8 @@ export interface ProfileInviteCodeAdminResponse {
|
||||
- token 恢复、过期清理、退出登录。
|
||||
- 总览页正常数据、部分表统计失败、整体请求失败。
|
||||
- API 调试成功访问 `/healthz`,绝对 URL 被后端拒绝。
|
||||
- 兑换码 public/unique/private 表单提交和停用。
|
||||
- 邀请码表单提交、metadata JSON 对象校验和结果展示。
|
||||
- 兑换码数据库列表加载、列表点击回填、public/unique/private 表单提交和停用。
|
||||
- 邀请码数据库列表加载、普通用户邀请码不展示、列表点击回填、metadata JSON 对象校验和结果展示。
|
||||
2. 根工程:
|
||||
- `npm run check:encoding`。
|
||||
- 后续接入根 workspace 后,补充后台工程 build/typecheck 脚本。
|
||||
|
||||
69
docs/technical/ALIYUN_SMS_TIMESTAMP_FORMAT_FIX_2026-05-07.md
Normal file
69
docs/technical/ALIYUN_SMS_TIMESTAMP_FORMAT_FIX_2026-05-07.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 短信验证码阿里云时间戳格式修复(2026-05-07)
|
||||
|
||||
## 背景
|
||||
|
||||
使用阿里云短信验证码真实 provider 发送验证码时,接口返回:
|
||||
|
||||
```text
|
||||
短信验证码发送失败:Specified time stamp or date value is not well formatted.
|
||||
```
|
||||
|
||||
该错误来自阿里云 OpenAPI 网关对签名请求头 `x-acs-date` 的格式校验。
|
||||
|
||||
## 根因
|
||||
|
||||
`server-rs/crates/platform-auth/src/lib.rs` 中阿里云 ACS3 签名逻辑会构造 `x-acs-date` 请求头。
|
||||
|
||||
原实现使用 `time::format_description::well_known::Rfc3339`,当 `OffsetDateTime::now_utc()` 带纳秒时会生成形如:
|
||||
|
||||
```text
|
||||
2026-05-07T14:23:59.364767Z
|
||||
```
|
||||
|
||||
阿里云 ACS3 签名要求 `x-acs-date` 使用不带小数秒的 UTC ISO 8601 格式:
|
||||
|
||||
```text
|
||||
yyyy-MM-dd'T'HH:mm:ss'Z'
|
||||
```
|
||||
|
||||
即:
|
||||
|
||||
```text
|
||||
2026-05-07T14:23:59Z
|
||||
```
|
||||
|
||||
带小数秒的时间戳会被阿里云网关判定为格式非法,从而返回 `Specified time stamp or date value is not well formatted.`。
|
||||
|
||||
## 修复方案
|
||||
|
||||
将 `current_aliyun_timestamp()` 改为手动输出不带小数秒的 UTC ISO 8601 格式:
|
||||
|
||||
```text
|
||||
yyyy-MM-dd'T'HH:mm:ss'Z'
|
||||
```
|
||||
|
||||
并新增单元测试,确保:
|
||||
|
||||
- 长度等于 `2026-05-07T12:34:56Z`;
|
||||
- 固定位置包含 `-`、`T`、`:`、`Z`;
|
||||
- 不包含小数点;
|
||||
- 除固定分隔符外均为数字。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅影响阿里云短信验证码 provider 的请求签名头 `x-acs-date`。
|
||||
- 不改动短信模板、签名、验证码业务参数。
|
||||
- 不改动 mock 短信 provider。
|
||||
- 不涉及前端接口契约变化。
|
||||
|
||||
## 验收
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p platform-auth aliyun -- --nocapture
|
||||
cargo fmt -p platform-auth --check
|
||||
```
|
||||
|
||||
预期:相关测试通过,格式检查通过。
|
||||
@@ -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,118 @@
|
||||
# api-server 外部服务环境变量配置 2026-05-07
|
||||
|
||||
## 背景
|
||||
|
||||
`server-rs/crates/api-server/src/config.rs` 统一收口 api-server 启动配置。外部服务分为两类:
|
||||
|
||||
1. 公共服务:阿里云、腾讯云、微信等对外公开且接口域名稳定的服务。
|
||||
2. 非公共服务:团队自选模型网关、图片网关、视频模型、内部兼容服务等,URL 与模型名可能随部署、供应商或账号策略变化。
|
||||
|
||||
本次约定:公共服务 URL 可以保留代码默认值;非公共服务的 URL 与模型名必须通过环境变量提供,不再在 `config.rs` 写死具体模型名称或私有网关地址。
|
||||
|
||||
## 公共服务默认值
|
||||
|
||||
以下默认值属于公共服务稳定接口,可继续保留在代码或示例环境中:
|
||||
|
||||
```text
|
||||
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
ALIYUN_SMS_ENDPOINT=dypnsapi.aliyuncs.com
|
||||
WECHAT_AUTHORIZE_ENDPOINT=https://open.weixin.qq.com/connect/qrconnect
|
||||
WECHAT_ACCESS_TOKEN_ENDPOINT=https://api.weixin.qq.com/sns/oauth2/access_token
|
||||
WECHAT_USER_INFO_ENDPOINT=https://api.weixin.qq.com/sns/userinfo
|
||||
```
|
||||
|
||||
说明:DashScope 属于阿里云公开服务,基础 URL 可保留;具体图片模型名不属于稳定公共接口,必须由环境变量配置。
|
||||
|
||||
## 非公共服务必配项
|
||||
|
||||
生产环境或真实联调使用到对应能力时,应显式配置以下变量:
|
||||
|
||||
```text
|
||||
# 文本 LLM 网关
|
||||
GENARRATIVE_LLM_PROVIDER=openai-compatible
|
||||
GENARRATIVE_LLM_BASE_URL=
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=
|
||||
|
||||
# APIMart / OpenAI 兼容 Responses 文本网关
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
|
||||
# VectorEngine / GPT-image-2 / Suno / Vidu 生成网关
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# Hyper3D Rodin Gen-2 3D 模型生成
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# 火山引擎豆包语音 ASR / TTS
|
||||
VOLCENGINE_SPEECH_API_KEY=
|
||||
VOLCENGINE_SPEECH_APP_ID=
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY=
|
||||
VOLCENGINE_SPEECH_ASR_RESOURCE_ID=volc.seedasr.sauc.concurrent
|
||||
VOLCENGINE_SPEECH_TTS_RESOURCE_ID=seed-tts-2.0
|
||||
VOLCENGINE_SPEECH_REQUEST_TIMEOUT_MS=180000
|
||||
VOLCENGINE_SPEECH_ASR_WS_URL=wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
|
||||
VOLCENGINE_SPEECH_TTS_BIDIRECTION_WS_URL=wss://openspeech.bytedance.com/api/v3/tts/bidirection
|
||||
VOLCENGINE_SPEECH_TTS_SSE_URL=https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse
|
||||
|
||||
# DashScope 图片模型名
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL=
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL=
|
||||
DASHSCOPE_COVER_IMAGE_MODEL=
|
||||
|
||||
# Ark / 角色视频模型网关
|
||||
ARK_CHARACTER_VIDEO_BASE_URL=
|
||||
ARK_CHARACTER_VIDEO_API_KEY=
|
||||
ARK_CHARACTER_VIDEO_MODEL=
|
||||
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
```
|
||||
|
||||
## 兼容变量
|
||||
|
||||
为降低部署切换成本,当前代码仍兼容部分历史变量:
|
||||
|
||||
```text
|
||||
GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
|
||||
GENARRATIVE_LLM_MODEL / LLM_MODEL / VITE_LLM_MODEL
|
||||
GENARRATIVE_LLM_API_KEY / LLM_API_KEY / ARK_API_KEY
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL / DASHSCOPE_IMAGE_EDIT_MODEL
|
||||
DASHSCOPE_COVER_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
|
||||
ARK_CHARACTER_VIDEO_BASE_URL / ARK_BASE_URL / GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
|
||||
ARK_CHARACTER_VIDEO_API_KEY / ARK_API_KEY / GENARRATIVE_LLM_API_KEY / LLM_API_KEY
|
||||
ARK_CHARACTER_VIDEO_MODEL / DASHSCOPE_CHARACTER_VIDEO_MODEL
|
||||
VOLCENGINE_SPEECH_API_KEY / VOLCENGINE_API_KEY
|
||||
VOLCENGINE_SPEECH_APP_ID / VOLCENGINE_ACCESS_KEY_ID
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY / VOLCENGINE_SECRET_ACCESS_KEY
|
||||
HYPER3D_BASE_URL / RODIN_BASE_URL
|
||||
HYPER3D_API_KEY / RODIN_API_KEY
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS / RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
```
|
||||
|
||||
## 运行时行为
|
||||
|
||||
1. `AppConfig::default()` 不再包含具体非公共模型名或私有网关 URL。
|
||||
2. `AppConfig::from_env()` 会从环境变量读取非公共模型名和 URL。
|
||||
3. 文本 LLM provider 为 `ark` 且未配置 `GENARRATIVE_LLM_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
4. 角色视频 provider 复用 Ark 且未配置 `ARK_CHARACTER_VIDEO_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
5. 具体模型名缺失时不在配置层伪造默认模型,调用到对应能力时由下游配置校验返回缺配置错误。
|
||||
6. VectorEngine 图片与音频生成只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 图片生成额外读取 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;不复用 `APIMART_*`、`GENARRATIVE_LLM_*` 或前端变量。
|
||||
7. 火山引擎语音能力由 `platform-speech` 收口协议帧与上游鉴权,`api-server` 只暴露平台鉴权后的代理路由,不向前端返回任何密钥字段。
|
||||
8. Hyper3D Rodin Gen-2 使用公开默认 `https://api.hyper3d.com/api/v2`,API Key 只读取 `HYPER3D_API_KEY` / `RODIN_API_KEY`,不复用文本 LLM、图片或音频网关密钥。
|
||||
9. APIMart 当前只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路;GPT-image-2 图片生成不得再读取 APIMart 配置。
|
||||
10. 本地 `npm run api-server`、`npm run dev:rust` 与 `npm run dev` 的环境文件优先级固定为外层 shell 变量最高,其后 `.env`、`.env.local`、`.env.secrets.local` 逐层覆盖;真实密钥建议放在 `.env.secrets.local`,防止 `.env` 中的空示例值覆盖私密配置。
|
||||
|
||||
## 示例文件
|
||||
|
||||
生产示例环境变量维护在:
|
||||
|
||||
```text
|
||||
deploy/env/api-server.env.example
|
||||
```
|
||||
|
||||
真实密钥、内部网关 URL 和具体模型名只应写入服务器 `/etc/genarrative/api-server.env` 或本地未提交的 `.env.local` / `.env.secrets.local`,不得提交到仓库。
|
||||
@@ -49,7 +49,7 @@ npm run build
|
||||
npm run check:content
|
||||
```
|
||||
|
||||
后端代码变更后,按项目约束还需要用 `npm run api-server:maincloud` 做一次启动验证。
|
||||
后端代码变更后,按项目约束还需要用 `npm run api-server` 做一次启动验证。
|
||||
|
||||
本轮最终结果:
|
||||
|
||||
@@ -58,7 +58,7 @@ npm run check:content
|
||||
- `cargo test --manifest-path server-rs\Cargo.toml` 已通过,结果同 `api-server` 默认测试。
|
||||
- `npm test` 已通过,结果为 `160 passed` 个测试文件、`704 passed` 个用例。
|
||||
- `npm run typecheck`、`npm run build`、`npm run check:content`、`npm run check:encoding`、`git diff --check` 已通过。
|
||||
- `npm run api-server:maincloud` 已完成启动烟测,`/healthz` 返回 `200`;期间 Maincloud 订阅恢复出现 `503` warning,但未阻止服务启动。
|
||||
- `npm run api-server` 已完成启动烟测,`/healthz` 返回 `200`;期间 Maincloud 订阅恢复出现 `503` warning,但未阻止服务启动。
|
||||
|
||||
仍需单独处理的非本轮阻塞:
|
||||
|
||||
|
||||
37
docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md
Normal file
37
docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# `AuthGate` 登录后又回到未登录状态修复
|
||||
|
||||
日期:`2026-05-09`
|
||||
|
||||
## 背景
|
||||
|
||||
本地联调中,手机号验证码登录有时会先显示登录成功,随后又瞬间回到未登录态。
|
||||
|
||||
## 根因
|
||||
|
||||
`AuthGate` 首次挂载时会异步 hydrate:
|
||||
|
||||
1. 先轮换 refresh cookie
|
||||
2. 再请求 `/api/auth/me`
|
||||
3. 再根据结果写入 `user` 和 `status`
|
||||
|
||||
如果用户在这轮 hydrate 尚未完成时已经完成了登录,后到达的旧 hydrate 结果仍可能把刚写入的 `user` 覆盖回 `null`,导致登录态闪回未登录。
|
||||
|
||||
## 修复
|
||||
|
||||
`AuthGate` 增加 hydrate 版本号保护:
|
||||
|
||||
1. 每次启动 hydrate 都分配独立版本号。
|
||||
2. 登录成功、退出登录、收到全局 auth state 事件时递增版本号。
|
||||
3. 旧版本 hydrate 的结果到达后直接丢弃,不再覆盖当前 `user` / `status`。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `npm run test -- src/components/auth/AuthGate.test.tsx`
|
||||
2. `npm run test -- src/services/apiClient.test.ts src/services/authService.test.ts`
|
||||
3. `npm run check:encoding`
|
||||
|
||||
## 关联
|
||||
|
||||
- `src/components/auth/AuthGate.tsx`
|
||||
- `src/components/auth/AuthGate.test.tsx`
|
||||
- `.hermes/shared-memory/pitfalls.md`
|
||||
@@ -115,3 +115,41 @@
|
||||
2. 该失败只代表登录方式配置探测失败,不代表登录功能不可用,因此不把 `读取登录方式失败` 写入登录弹窗错误条。
|
||||
3. 登录弹窗仍展示密码登录表单,玩家可继续登录后进入创作链路。
|
||||
4. 本地仍需要启动 `api-server`,否则后续 `POST /api/auth/entry` 等真实登录请求无法完成。
|
||||
|
||||
## 9. 2026-05-07 本地短信入口恢复记录
|
||||
|
||||
如果登录弹窗里短信登录页签“像是被删了”,先不要改前端表单,优先检查本地登录方式探测结果:
|
||||
|
||||
1. 仓库根目录 `.env.local` 里必须显式保留 `SMS_AUTH_ENABLED=true`。
|
||||
2. 本地启动请优先使用 `npm run api-server`、`npm run dev:rust` 或 `npm run dev`,这些脚本会按“shell 环境优先、`.env.local` 覆盖 `.env`”合并配置。
|
||||
3. 若 `GET /api/auth/login-options` 只返回 `["password"]`,说明短信入口没有被服务端配置打开,前端只是按 contract 正常降级。
|
||||
4. 当 `SMS_AUTH_ENABLED=true` 生效时,`GET /api/auth/login-options` 至少应返回 `["phone", "password"]`,短信登录页签才会重新出现。
|
||||
|
||||
## 10. 2026-05-07 前端代理端口错配修复记录
|
||||
|
||||
如果 Rust API 直连返回 `["phone", "password"]`,但从前端域名请求 `GET /api/auth/login-options` 返回 `500`,短信页签同样会消失。此时不是登录 UI 被删除,而是 `AuthGate` 按第 5.3 节降级成 `["password"]`。
|
||||
|
||||
本地排查顺序固定为:
|
||||
|
||||
1. 先请求 `http://127.0.0.1:3000/api/auth/login-options`,确认前端代理是否成功返回 JSON。
|
||||
2. 再请求当前 Rust API 目标,例如 `http://127.0.0.1:3100/api/auth/login-options` 或 `http://127.0.0.1:8082/api/auth/login-options`。
|
||||
3. 若直连 API 成功而 3000 返回 `500`,检查 `RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 是否指向仍在监听的 API 端口。
|
||||
4. `npm run dev` / `npm run dev:rust` 完整栈默认由脚本计算 API 端口;加载 `.env.local` 给后端使用后,脚本必须重新固定 `RUST_SERVER_TARGET`,避免 `.env.local` 中的旧代理目标覆盖本次启动的实际 API 端口。
|
||||
5. `npm run dev:web` 只启动前端,不会自动拉起 Rust API;如果 `.env.local` / 当前环境已经显式声明 `GENARRATIVE_RUNTIME_SERVER_TARGET`、`RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET` 或 `GENARRATIVE_API_PORT`,脚本必须固定使用该目标。目标当下不可用时只打印警告,不自动切到另一个端口,避免前端进程长时间绑定到随后会停掉的临时 API。
|
||||
6. 如果 `3000` 仍然返回 `500`,先确认浏览器是不是还开着旧的前端进程。当前脚本如果因为端口占用漂移到 `3001` / `3002`,应直接关掉旧进程后重启,而不是继续用旧的 3000 页面判断登录入口状态。
|
||||
|
||||
## 11. 2026-05-10 `npm run api-server` 环境加载与短信 provider 排查记录
|
||||
|
||||
本地单独启动 `api-server` 时,环境变量合并顺序固定为:
|
||||
|
||||
```text
|
||||
外层 shell > .env > .env.local > .env.secrets.local
|
||||
```
|
||||
|
||||
这保证 `.env.local` 能覆盖 `.env.example` 派生出的默认值,`.env.secrets.local` 能继续覆盖本地私密密钥配置。`scripts/api-server-dev.mjs` 不得让 `.env` 后加载并覆盖 `.env.local`,否则 `SMS_AUTH_ENABLED` 或 `SMS_AUTH_PROVIDER` 可能被压回错误值。
|
||||
|
||||
排查“点击获取验证码但手机收不到短信”时,除了确认 `availableLoginMethods` 包含 `phone`,还必须确认当前进程实际使用的 provider:
|
||||
|
||||
1. `SMS_AUTH_PROVIDER="mock"` 只用于本地 UI / 账号链路联调,不会向手机发送真实短信;此时应使用 `SMS_AUTH_MOCK_VERIFY_CODE`,默认 `123456`。
|
||||
2. 真实短信链路必须使用 `SMS_AUTH_PROVIDER="aliyun"`,并在修改 `.env.local` 后重启 `api-server`,运行中的进程不会自动切换 provider。
|
||||
3. 真实 provider 是否被使用,以 `api-server` 日志中的 `provider=aliyun`、`provider_request_id` 和 `provider_out_id` 为准。
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 登录恢复与推荐页加载态收口修复
|
||||
|
||||
日期:`2026-05-09`
|
||||
|
||||
## 背景
|
||||
|
||||
推荐页作品卡偶发一直停留在“加载中...”,同时刷新网页后可能从已登录态回到未登录态。两类问题都发生在平台入口首屏和登录态恢复链路中,表现上像推荐页问题,实际涉及 `AuthGate`、请求层 token 处理和推荐页嵌入运行态的启动状态机。
|
||||
|
||||
## 根因
|
||||
|
||||
刷新网页掉线的关键根因是 `AuthGate` hydrate 先强制调用 `refreshStoredAccessToken()`。如果 refresh cookie 临时不可用、代理错配或后端短暂返回 `401`,该方法会清掉本地 access token,随后 `/api/auth/me` 只能恢复成未登录。
|
||||
|
||||
推荐页作品卡卡住加载中的根因是推荐页自动启动运行态时,`activeRecommendEntryKey` 和 `activeRecommendRuntimeKind` 先被设置,失败时只把 kind 置空或由玩法内部写错误;外层没有稳定的 `activeRecommendRuntimeError` 收口。并发切换作品时,旧启动请求也可能晚到覆盖新启动状态。
|
||||
|
||||
## 修复
|
||||
|
||||
1. `refreshStoredAccessToken()` 增加 `clearOnFailure` 选项,默认保持原全局恢复语义;显式传 `false` 时 refresh 失败不会清空现有 access token。
|
||||
2. `AuthGate` 已有本地 access token 时,先用 `/api/auth/me` 确认当前用户;确认成功后再后台调用 refresh 续期与写每日登录埋点。后台 refresh 失败只静默忽略,不再把已确认账号改成未登录。
|
||||
3. 本地没有 access token 时,`AuthGate` 仍通过 refresh cookie 补票;该路径失败会清 token 并落到未登录,保持原有安全语义。
|
||||
4. 推荐页 `selectRecommendRuntimeEntry` 增加启动请求版本号。旧请求晚到后直接丢弃,不能覆盖当前作品。
|
||||
5. 推荐页运行态启动失败时统一写入 `activeRecommendRuntimeError = "作品暂时无法进入,请稍后再试。"`,并关闭 `isStartingRecommendEntry`,避免作品卡永久显示加载态。
|
||||
6. 推荐页入口继续保持登录门禁。未登录用户点击推荐 Tab 只切到推荐封面并弹出登录弹窗;未登录状态下点击推荐封面再次弹出登录弹窗,不打开详情、不启动运行态。
|
||||
7. `RpgEntryHomeView` 将公开作品详情入口与推荐页入口拆成 `onOpenGalleryDetail` 和 `onOpenRecommendGalleryDetail`:发现页、搜索结果和排行榜仍可按公开浏览能力打开详情;推荐页封面、推荐运行态错误重试、桌面推荐模块统一走推荐门禁入口。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "explicit refresh opts out|auth gate keeps a valid local token login|home recommendation"`
|
||||
2. 后续完整收口仍建议执行:
|
||||
- `npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx src/components/auth/AuthGate.test.tsx src/services/apiClient.test.ts`
|
||||
- `npm run test -- src/services/apiClient.test.ts src/components/auth/AuthGate.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding`
|
||||
|
||||
## 关联文件
|
||||
|
||||
1. `src/services/apiClient.ts`
|
||||
2. `src/components/auth/AuthGate.tsx`
|
||||
3. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
4. `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
5. `src/components/auth/AuthGate.test.tsx`
|
||||
6. `src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`
|
||||
7. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
|
||||
@@ -67,9 +67,11 @@ HTTP status server error (503 Service Unavailable)
|
||||
远端 `xushi-p4wfr` 挂起期间,抓大鹅本地体验应使用本地 SpacetimeDB:
|
||||
|
||||
```powershell
|
||||
spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101
|
||||
npm run dev:rust
|
||||
$env:GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="codex-local-bootstrap-secret-20260501"
|
||||
spacetime --root-dir=server-rs/.spacetimedb/local publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes
|
||||
Push-Location server-rs
|
||||
spacetime publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path crates/spacetime-module -c=on-conflict --yes
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
再让 Rust API 指向本地库:
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# 宝贝识物创作发布实现方案 2026-05-11
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案对应第 2 线程:创作发布线程。
|
||||
|
||||
本线程落地:
|
||||
|
||||
1. 创作入口配置;
|
||||
2. 模板表单;
|
||||
3. 本地草稿生成 service;
|
||||
4. 结果页;
|
||||
5. 发布 payload 约束;
|
||||
6. 本地 Demo 运行态;
|
||||
7. 后端 image-2 / 作品持久化 / 运行态接口预留形状。
|
||||
|
||||
本阶段运行态先做浏览器本地 Demo,并消费现有本地 mocap 动作数据源;正式硬件接口和摄像头调教在后续接口稳定后继续接入。
|
||||
|
||||
## 2. 前端接入点
|
||||
|
||||
新增玩法 ID:
|
||||
|
||||
```text
|
||||
baby-object-match
|
||||
```
|
||||
|
||||
用户展示名:
|
||||
|
||||
```text
|
||||
宝贝识物
|
||||
```
|
||||
|
||||
入口文件:
|
||||
|
||||
1. `src/config/newWorkEntryConfig.ts`
|
||||
2. `src/components/platform-entry/platformEntryCreationTypes.ts`
|
||||
3. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
|
||||
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
`baby-object-match` 必须复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时,创作类型弹层不展示 `宝贝识物`,创作页作品架不展示本地宝贝识物草稿或已发布作品卡,公开发现、搜索、详情、作品号和浏览历史也继续完全不可见。
|
||||
|
||||
新增阶段:
|
||||
|
||||
```text
|
||||
baby-object-match-workspace
|
||||
baby-object-match-generating
|
||||
baby-object-match-result
|
||||
baby-object-match-runtime
|
||||
```
|
||||
|
||||
## 3. 契约
|
||||
|
||||
前端共享契约放在:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/edutainmentBabyObject.ts
|
||||
```
|
||||
|
||||
核心字段:
|
||||
|
||||
1. `BabyObjectMatchDraft.templateId = "baby-object-match"`;
|
||||
2. `BabyObjectMatchDraft.templateName = "宝贝识物"`;
|
||||
3. `BabyObjectMatchDraft.themeTags` 必须包含精确 `寓教于乐`;
|
||||
4. `BabyObjectMatchItemAsset.generationProvider` 首版允许为 `vector-engine-gpt-image-2` 或 `placeholder`;
|
||||
5. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`。
|
||||
|
||||
## 4. Service 边界
|
||||
|
||||
前端 service 放在:
|
||||
|
||||
```text
|
||||
src/services/edutainment-baby-object/babyObjectMatchClient.ts
|
||||
```
|
||||
|
||||
首版提供:
|
||||
|
||||
1. `createBabyObjectMatchDraft(payload)`;
|
||||
2. `saveBabyObjectMatchDraft(draft)`;
|
||||
3. `publishBabyObjectMatchWork(payload)`。
|
||||
|
||||
当前后端正式接口未在本线程扩表落地,因此 service 先走本地 Demo 存储,并把 asset 结果标记为 `placeholder`。后续后端接入时,应替换为:
|
||||
|
||||
```text
|
||||
POST /api/creation/edutainment/baby-object-match/drafts
|
||||
PUT /api/creation/edutainment/baby-object-match/drafts/{draftId}
|
||||
POST /api/creation/edutainment/baby-object-match/drafts/{draftId}/publish
|
||||
```
|
||||
|
||||
图片生成必须在后端调用 VectorEngine `gpt-image-2-all`,不得从前端直接调用外部图片接口。
|
||||
|
||||
## 5. UI 边界
|
||||
|
||||
工作台只展示两个必填输入和生成按钮。
|
||||
|
||||
结果页只展示草稿核心信息、两个物品、保存草稿、发布、试玩。不在 UI 内写玩法说明长文案。
|
||||
|
||||
移动端优先:表单和结果页使用单列布局,桌面端自然扩展为双列。
|
||||
|
||||
## 6. 运行态边界
|
||||
|
||||
前端运行态放在:
|
||||
|
||||
```text
|
||||
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
|
||||
```
|
||||
|
||||
运行态直接消费 `BabyObjectMatchDraft`,必须使用草稿中的两个物品名称和物品图。
|
||||
每轮只随机当前从礼物盒跳出的物品;左右篮子不随机交换,左侧固定为草稿 `itemAssets[0]`,右侧固定为草稿 `itemAssets[1]`。
|
||||
|
||||
首关状态机:
|
||||
|
||||
1. `waiting`:礼物盒关闭,等待任意手抬起;
|
||||
2. `active`:当前物品停留在屏幕中央;
|
||||
3. `correct`:展示“真棒”反馈,成功次数加 1;
|
||||
4. `wrong`:展示“再想一想吧”反馈,当前物品回到中央;
|
||||
5. `complete`:成功次数达到 20,展示“恭喜你!小朋友!”和按钮。
|
||||
|
||||
动作输入:
|
||||
|
||||
1. 任意手完成一次 `open_palm -> grab` 抓握序列:打开礼物盒并生成当前物品;
|
||||
2. 左手连续横向移动达到阈值:将当前物品送入左侧篮子;
|
||||
3. 右手连续横向移动达到阈值:将当前物品送入右侧篮子。
|
||||
|
||||
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。选篮只使用明确 `leftHand` 或 `rightHand` 的连续横向轨迹阈值,不再通过 `wave_left_hand`、`wave_right_hand`、`wave` 等动作名触发;侧别为 `unknown` 的手部轨迹也不参与选篮,以避免多套判定误命中和连续误触发。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:`rightHand` 轨迹映射玩家左手并进入左侧篮子,`leftHand` 轨迹映射玩家右手并进入右侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
|
||||
|
||||
开发者调试输入:
|
||||
|
||||
1. `F`:映射任意手抬起,打开礼物盒并生成当前物品;
|
||||
2. 鼠标左键按下并拖动:映射左手轨迹,抬起后将当前物品送入左侧篮子;
|
||||
3. 鼠标右键按下并拖动:映射右手轨迹,抬起后将当前物品送入右侧篮子。
|
||||
|
||||
运行态不得新增计时、失败次数、分数、体力或难度递增规则。
|
||||
|
||||
音效和语音播报当前只保留接口预留边界,正式语音接口后续接入。
|
||||
|
||||
## 7. 发布约束
|
||||
|
||||
发布前必须执行:
|
||||
|
||||
1. 两个物品名非空;
|
||||
2. 两个物品名对应的 asset 存在;
|
||||
3. 标签补齐精确 `寓教于乐`;
|
||||
4. `publicationStatus` 从 `draft` 变为 `published`。
|
||||
|
||||
发布后首版本地响应返回 `publicWorkCode`,用于分享弹窗;正式后端接入时 public code 生成规则需要纳入统一作品号服务。
|
||||
|
||||
## 8. 热身关衔接
|
||||
|
||||
`/child-motion-demo` 热身完成后的“开始游戏”按钮进入同一个 `BabyObjectMatchRuntimeShell`。
|
||||
|
||||
热身关独立 Demo 没有创作者草稿上下文,因此使用固定本地 Demo 草稿承载两个物品,仅用于热身关后验证首关体验;正式平台体验仍必须从 `宝贝识物` 模板创作发布后进入寓教于乐板块。
|
||||
|
||||
## 9. 验收命令
|
||||
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/services/edutainment-baby-object/babyObjectMatchClient.test.ts
|
||||
npx vitest run src/components/platform-entry/platformEdutainmentVisibility.test.ts src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/custom-world-home/creationWorkShelf.test.ts src/services/useMocapInput.test.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts
|
||||
npx eslint src/components/platform-entry/platformEntryCreationTypes.ts src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --ext .ts,.tsx --max-warnings 0
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
npm run build:raw
|
||||
```
|
||||
|
||||
若后续接入真实 Rust API 和 SpacetimeDB 表,再补充 `npm run api-server`、`/healthz`、Rust contract / api-server / spacetime-client 定向测试和 migration 表目录更新。
|
||||
245
docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md
Normal file
245
docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# 后端用户行为埋点覆盖方案
|
||||
|
||||
更新时间:`2026-05-09`
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于补齐后端可直接观测的用户行为埋点入口,统一写入 SpacetimeDB 的 `tracking_event` 与 `tracking_daily_stat`,为任务系统、运营看板与后续漏斗分析提供事实数据。
|
||||
|
||||
本轮明确不纳入以下范围:
|
||||
|
||||
- 后台管理入口:`/admin/...`
|
||||
- RPG 相关入口
|
||||
- 大鱼吃小鱼相关入口
|
||||
- Visual Novel 相关入口
|
||||
- Story 相关入口
|
||||
- Combat 相关入口
|
||||
|
||||
上述范围后续若需要埋点,应单独定义事件口径,避免把后台运营审计或特定玩法内行为混入本轮通用用户行为埋点。
|
||||
|
||||
## 2. 写入链路
|
||||
|
||||
### 2.1 SpacetimeDB 通用 procedure
|
||||
|
||||
新增通用 procedure:
|
||||
|
||||
- `record_tracking_event_and_return(input: RuntimeTrackingEventInput)`
|
||||
|
||||
该入口复用既有运行态埋点写入能力:
|
||||
|
||||
1. 按 `occurred_at_micros` 计算北京时间业务日 `day_key`。
|
||||
2. 按同一 `day_key` 幂等补齐 `analytics_date_dimension`,保证周/月/季/年聚合查询有日期 bucket 映射。
|
||||
3. 写入原始事实 `tracking_event`。
|
||||
4. 更新聚合投影 `tracking_daily_stat`。
|
||||
5. 触发依赖事件进度的个人任务刷新。
|
||||
|
||||
每日登录 `daily_login` 也必须走该通用 procedure:认证链路仍保留 `record_daily_login_tracking_event_after_auth_success(...)` 作为业务语义 helper,但 helper 内部构造 `TrackingEventDraft` 后调用 `record_tracking_event_after_success(...)`,不再绕到每日登录专用 SpacetimeDB procedure。
|
||||
|
||||
### 2.2 spacetime-client 封装
|
||||
|
||||
`spacetime-client` 提供薄封装:
|
||||
|
||||
- `SpacetimeRuntimeClient::record_tracking_event(...)`
|
||||
|
||||
API Server 只依赖该 facade,不在 handler 中直接拼接 SpacetimeDB procedure 调用。
|
||||
|
||||
### 2.3 api-server helper 与中间件
|
||||
|
||||
API Server 新增统一 helper:
|
||||
|
||||
- `tracking::TrackingEventDraft`
|
||||
- `tracking::record_tracking_event_after_success(...)`
|
||||
- `tracking::record_route_tracking_event_after_success(...)`
|
||||
|
||||
路由级中间件 `record_api_tracking_after_success` 挂在最终响应链路上,只在最终 HTTP status 为 2xx 时写入埋点。埋点失败只写 `warn` 日志,不阻断认证、充值、发布、任务领取等主业务流程。
|
||||
|
||||
## 3. metadata 口径
|
||||
|
||||
当前通用路由埋点仅记录低敏字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `route` | 请求路径,不包含 query string |
|
||||
| `method` | HTTP Method |
|
||||
| `status` | 最终成功响应状态码 |
|
||||
| `operation` | `RequestContext` 中的操作名 |
|
||||
| `asset` | 仅资产类事件写入的低敏资产/操作信息,包含 `operation`、`operationFamily`、`assetObjectId`、`assetKind`、`objectKey`、`bucket`、`contentType`、`contentLength`、`version`、`bindingId`、`entityKind`、`entityId`、`slot`、`ownerUserId`、`profileId` 等可用于定位资产事实的字段;不写签名 URL、表单签名、OSS policy、token 或完整请求体。 |
|
||||
| `assetOperation` | 资产类路由兜底事件的操作 key,用于不读取请求体时仍能按操作族聚合。 |
|
||||
|
||||
禁止在通用埋点 metadata 中写入手机号、token、cookie、邀请码、请求体、密钥、连接串、外部凭证、OSS 签名 URL、PostObject policy 或签名表单字段。
|
||||
|
||||
### 3.1 作品级游玩埋点
|
||||
|
||||
所有已接入后端正式试玩/播放入口的作品类型统一写 `work_play_start`:
|
||||
|
||||
- `scope_kind = work`。
|
||||
- `scope_id = 稳定作品 ID`,优先使用 `profile_id`;大鱼吃小鱼沿用 `session_id` 作为作品 ID。
|
||||
- `user_id = 当前认证用户`。
|
||||
- `owner_user_id = 作品作者/拥有者`,无法从入口直接确认作者时可为空,但 `metadata.userId` 仍保留当前玩家。
|
||||
- `profile_id = 作品 profile_id`,大鱼吃小鱼这类 session 型作品可为空。
|
||||
- `module_key = play_type`,例如 `puzzle`、`match3d`、`square-hole`、`custom-world`、`big-fish`、`visual-novel`。
|
||||
- `metadata` 固定包含 `operation = work_play_start`、`playType`、`workId`、`sourceRoute`,并按入口补充 `runId`、`ownerUserId`、`profileId`、`levelId`、`mode` 等低敏字段。
|
||||
|
||||
该事件用于“某个作品被多少不同用户玩过”等作品级分析;权威去重统计仍建议优先使用业务投影(如 `profile_played_world`),埋点侧用于分析与漏斗联动。
|
||||
|
||||
## 4. 事件清单
|
||||
|
||||
### 4.1 认证与会话
|
||||
|
||||
| 事件 | 入口 |
|
||||
| --- | --- |
|
||||
| `auth_login_options_view` | `GET /api/auth/login-options` |
|
||||
| `auth_phone_code_send` | `POST /api/auth/phone/send-code` |
|
||||
| `daily_login` | 认证成功与 refresh 续期后由 `record_daily_login_tracking_event_after_auth_success(...)` 主动写入,事件 ID 按 `daily-login:{user_id}:{day_key}` 幂等 |
|
||||
| `auth_phone_login_success` | `POST /api/auth/phone/login` |
|
||||
| `auth_me_view` | `GET /api/auth/me` |
|
||||
| `auth_sessions_view` | `GET /api/auth/sessions` |
|
||||
| `auth_refresh_success` | `POST /api/auth/refresh` |
|
||||
| `auth_logout` | `POST /api/auth/logout` |
|
||||
| `auth_logout_all` | `POST /api/auth/logout-all` |
|
||||
| `auth_wechat_bind_phone_success` | `POST /api/auth/wechat/bind-phone` |
|
||||
|
||||
### 4.2 个人中心、账户运营与任务
|
||||
|
||||
| 事件 | 入口 |
|
||||
| --- | --- |
|
||||
| `profile_identity_update` | `PATCH /api/profile/me` |
|
||||
| `profile_dashboard_view` | `GET /api/profile/dashboard` |
|
||||
| `wallet_ledger_view` | `GET /api/profile/wallet-ledger` |
|
||||
| `recharge_center_view` | `GET /api/profile/recharge-center` |
|
||||
| `recharge_order_create` | `POST /api/profile/recharge/orders` |
|
||||
| `feedback_submit` | `POST /api/profile/feedback` |
|
||||
| `invite_center_view` | `GET /api/profile/referrals/invite-center` |
|
||||
| `referral_invite_code_redeem` | `POST /api/profile/referrals/redeem-code` |
|
||||
| `redeem_code_submit` | `POST /api/profile/redeem-codes/redeem` |
|
||||
| `task_center_view` | `GET /api/profile/tasks` |
|
||||
| `task_reward_claim` | `POST /api/profile/tasks/{task_id}/claim` |
|
||||
| `save_archive_list_view` | `GET /api/profile/save-archives` |
|
||||
| `save_archive_detail_view` | `GET /api/profile/save-archives/{archive_id}` |
|
||||
| `browse_history_view` | `GET /api/profile/browse-history` |
|
||||
| `browse_history_record` | `POST /api/profile/browse-history` |
|
||||
| `browse_history_clear` | `DELETE /api/profile/browse-history` |
|
||||
| `play_stats_view` | `GET /api/profile/play-stats` |
|
||||
| `profile_analytics_metric_view` | `GET /api/profile/analytics/metric` |
|
||||
|
||||
### 4.3 AI、资产、LLM 与语音
|
||||
|
||||
资产操作统一按用户级事件写入:`scope_kind = user`、`scope_id = 当前认证 user_id`、`user_id/owner_user_id = 当前认证 user_id`。其中 `asset_upload_ticket_create`、`asset_upload_confirm`、`asset_bind` 在 handler 成功后主动记录资产 metadata,避免只依赖路由兜底;其余资产工坊入口通过路由级兜底保留用户级操作事实。
|
||||
|
||||
| 事件 | 入口 |
|
||||
| --- | --- |
|
||||
| `ai_task_create` | `POST /api/ai/tasks` |
|
||||
| `ai_task_start` | `POST /api/ai/tasks/{task_id}/start` |
|
||||
| `ai_task_stage_start` | `POST /api/ai/tasks/{task_id}/stages/{stage_id}/start` |
|
||||
| `ai_task_chunk_append` | `POST /api/ai/tasks/{task_id}/chunks` |
|
||||
| `ai_task_stage_complete` | `POST /api/ai/tasks/{task_id}/stages/{stage_id}/complete` |
|
||||
| `ai_task_reference_attach` | `POST /api/ai/tasks/{task_id}/references` |
|
||||
| `ai_task_complete` | `POST /api/ai/tasks/{task_id}/complete` |
|
||||
| `ai_task_fail` | `POST /api/ai/tasks/{task_id}/fail` |
|
||||
| `ai_task_cancel` | `POST /api/ai/tasks/{task_id}/cancel` |
|
||||
| `asset_upload_ticket_create` | `POST /api/assets/direct-upload-tickets` |
|
||||
| `asset_sts_credentials_create` | `POST /api/assets/sts-upload-credentials` |
|
||||
| `asset_upload_confirm` | `POST /api/assets/objects/confirm` |
|
||||
| `asset_bind` | `POST /api/assets/objects/bind` |
|
||||
| `asset_character_visual_generate` | `POST /api/assets/character-visual/generate` |
|
||||
| `asset_character_visual_publish` | `POST /api/assets/character-visual/publish` |
|
||||
| `asset_character_animation_generate` | `POST /api/assets/character-animation/generate` |
|
||||
| `asset_character_animation_publish` | `POST /api/assets/character-animation/publish` |
|
||||
| `asset_character_animation_import` | `POST /api/assets/character-animation/import-video` |
|
||||
| `asset_character_workflow_cache_save` | `POST /api/assets/character-workflow-cache` |
|
||||
| `asset_history_view` | `GET /api/assets/history` |
|
||||
| `llm_request` | `POST /api/llm/chat/completions` |
|
||||
| `speech_config_view` | `GET /api/speech/volcengine/config` |
|
||||
| `asr_stream_start` | `GET /api/speech/volcengine/asr/stream` |
|
||||
| `tts_bidirection_start` | `GET /api/speech/volcengine/tts/bidirection` |
|
||||
| `tts_sse_start` | `POST /api/speech/volcengine/tts/sse` |
|
||||
|
||||
### 4.4 运行态与创作入口
|
||||
|
||||
| 事件 | 入口 |
|
||||
| --- | --- |
|
||||
| `runtime_settings_view` | `GET /api/runtime/settings` |
|
||||
| `runtime_settings_update` | `PUT /api/runtime/settings` |
|
||||
| `runtime_snapshot_view` | `GET /api/runtime/save/snapshot` |
|
||||
| `runtime_snapshot_save` | `PUT /api/runtime/save/snapshot` |
|
||||
| `runtime_snapshot_delete` | `DELETE /api/runtime/save/snapshot` |
|
||||
| `puzzle_route_success` | `/api/runtime/puzzle/...` 成功响应兜底 |
|
||||
| `match3d_route_success` | `/api/creation/match3d/...` 与 `/api/runtime/match3d/...` 成功响应兜底 |
|
||||
| `square_hole_route_success` | `/api/creation/square-hole/...` 与 `/api/runtime/square-hole/...` 成功响应兜底 |
|
||||
| `custom_world_route_success` | `/api/runtime/custom-world...` 成功响应兜底 |
|
||||
| `creative_agent_route_success` | `/api/runtime/creative-agent...` 成功响应兜底 |
|
||||
| `work_play_start` | 拼图、抓大鹅、方洞挑战、自定义世界、大鱼吃小鱼、Visual Novel 的正式开始游玩/播放入口;写 `scope_kind = work`、`scope_id = 作品 ID` |
|
||||
|
||||
2048、Survivor、Moku 等未被排除的模板/玩法,如果经由上述 runtime、creative、custom-world、puzzle、match3d 或 square-hole 后端入口,会被路由级兜底事件覆盖。
|
||||
|
||||
## 5. 查询与验收建议
|
||||
|
||||
按每日登录核查原始事实:
|
||||
|
||||
```sql
|
||||
SELECT event_id, event_key, scope_kind, scope_id, user_id, module_key, metadata_json, occurred_at
|
||||
FROM tracking_event
|
||||
WHERE event_key = 'daily_login'
|
||||
ORDER BY occurred_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
按作品级游玩核查原始事实:
|
||||
|
||||
```sql
|
||||
SELECT event_key, scope_kind, scope_id, user_id, owner_user_id, profile_id, module_key, metadata_json, occurred_at
|
||||
FROM tracking_event
|
||||
WHERE event_key = 'work_play_start'
|
||||
ORDER BY occurred_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
按某个作品统计不同游玩用户:
|
||||
|
||||
```sql
|
||||
SELECT scope_id, COUNT(DISTINCT user_id) AS player_count
|
||||
FROM tracking_event
|
||||
WHERE event_key = 'work_play_start'
|
||||
AND scope_kind = 'work'
|
||||
AND scope_id = '<profile_id_or_work_id>'
|
||||
GROUP BY scope_id;
|
||||
```
|
||||
|
||||
按资产操作核查原始事实:
|
||||
|
||||
```sql
|
||||
SELECT event_key, scope_kind, scope_id, user_id, owner_user_id, module_key, metadata_json, occurred_at
|
||||
FROM tracking_event
|
||||
WHERE module_key = 'asset'
|
||||
ORDER BY occurred_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
按事件核查原始事实:
|
||||
|
||||
```sql
|
||||
SELECT event_key, scope_kind, scope_id, user_id, module_key, metadata_json, occurred_at
|
||||
FROM tracking_event
|
||||
WHERE event_key = 'task_center_view'
|
||||
ORDER BY occurred_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
按日聚合核查:
|
||||
|
||||
```sql
|
||||
SELECT day_key, event_key, scope_kind, scope_id, count
|
||||
FROM tracking_daily_stat
|
||||
WHERE event_key = 'task_center_view'
|
||||
ORDER BY day_key DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
验收重点:
|
||||
|
||||
1. 成功请求写入 `tracking_event` 并刷新 `tracking_daily_stat`。
|
||||
2. `daily_login` 由认证成功/refresh 续期链路主动写入,且走 `record_tracking_event_and_return` 通用 procedure。
|
||||
3. 非 2xx 响应不记录通用成功事件。
|
||||
4. 后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 路由不写入本轮通用埋点。
|
||||
5. 埋点写入失败时主接口仍返回原业务结果,只记录后端 warning。
|
||||
6. metadata 不包含凭证、请求体或敏感业务字段。
|
||||
@@ -0,0 +1,729 @@
|
||||
# bark-battle 2D Runtime 前端技术方案(2026-05-11)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
本方案基于 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md`,为“汪汪声浪大作战 / bark-battle”细化前端与浏览器游戏 runtime 的技术实现路线。
|
||||
|
||||
本任务只产出技术方案,不直接实现代码。后续编码应以本文作为 runtime 层设计约束,再按 TDD 小步落地。
|
||||
|
||||
### 1.1 玩法定位
|
||||
|
||||
`bark-battle` 是一个声控拔河式 2D 浏览器小游戏:玩家对麦克风发出狗叫声,系统根据音量峰值、有效叫声次数和节奏计算本方声浪推动力,在限时 30 秒内推动顶部红蓝能量条,时间结束后按能量条偏向判定胜负。
|
||||
|
||||
### 1.2 本文范围
|
||||
|
||||
本文覆盖:
|
||||
|
||||
- Phaser + TypeScript + Vite 栈选择
|
||||
- simulation / render / HUD 边界
|
||||
- 建议目录结构
|
||||
- 核心 domain 类型
|
||||
- Web Audio 输入适配
|
||||
- Phaser Scene 切分
|
||||
- DOM HUD 设计
|
||||
- 移动端与权限降级
|
||||
- 测试与验证命令
|
||||
|
||||
本文不覆盖:
|
||||
|
||||
- 后端表结构、持久化成绩、作品发布、广场接入
|
||||
- 实时多人对战协议
|
||||
- 复杂 AI 狗叫识别模型
|
||||
- 美术素材正式生产流程
|
||||
|
||||
## 2. 技术栈选择
|
||||
|
||||
### 2.1 推荐栈
|
||||
|
||||
```text
|
||||
Runtime Renderer: Phaser 3
|
||||
Language: TypeScript
|
||||
Build: Vite
|
||||
Host UI: React / DOM overlay
|
||||
Audio Input: Web Audio API + MediaDevices.getUserMedia
|
||||
Test: Vitest + Testing Library + 浏览器 smoke / Playwright 可选
|
||||
```
|
||||
|
||||
### 2.2 选择理由
|
||||
|
||||
1. 玩法是横版 2D 舞台,核心表现是狗狗 sprite、声浪、拟声词、粒子、屏幕震动与能量条反馈,Phaser 对 2D 渲染、时间循环、sprite animation、camera、粒子和 Scene 生命周期支持成熟。
|
||||
2. 当前项目主前端已使用 TypeScript + Vite,继续复用现有构建和测试体系,避免引入独立构建链。
|
||||
3. 文字密集、权限提示、结算、设置和移动端响应式布局适合 DOM HUD;Canvas 保持负责 playfield 和动态特效。
|
||||
4. Web Audio API 可以在浏览器端完成 MVP 所需音量采样、RMS/peak 计算、环境噪音校准和输入归一化,不需要首版接入后端音频处理。
|
||||
|
||||
### 2.3 不选择其它路线的原因
|
||||
|
||||
- 不使用 Three.js / 3D:当前玩法画面是 2D 横版舞台,不需要 3D 相机、模型和材质管线。
|
||||
- 不把 HUD 全部塞入 Phaser Canvas:权限说明、重试、结算、移动端布局和可访问性更适合 DOM。
|
||||
- 不在前端实现正式业务真相:浏览器 runtime 可承载单局即时 simulation,但若后续涉及成绩、作品、排行榜、发布和奖励,必须交给后端投影/API 裁决。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 分层总览
|
||||
|
||||
```text
|
||||
React Runtime Shell
|
||||
├─ DOM HUD / Panels
|
||||
│ ├─ PermissionPanel
|
||||
│ ├─ TopEnergyBar
|
||||
│ ├─ TimerChip
|
||||
│ └─ ResultPanel
|
||||
│
|
||||
├─ Application Controller
|
||||
│ ├─ permission / calibration orchestration
|
||||
│ ├─ simulation tick
|
||||
│ ├─ audio sample submission
|
||||
│ └─ snapshot publish
|
||||
│
|
||||
├─ Pure Domain / Simulation
|
||||
│ ├─ BarkBattleSession
|
||||
│ ├─ BarkDetector
|
||||
│ ├─ EnergyTugOfWar
|
||||
│ ├─ BarkBattleScoring
|
||||
│ └─ OpponentStrategy
|
||||
│
|
||||
├─ Infrastructure Adapters
|
||||
│ ├─ BrowserMicrophoneInput
|
||||
│ ├─ AudioAnalyserSampler
|
||||
│ └─ PhaserGameHost
|
||||
│
|
||||
└─ Phaser Renderer
|
||||
├─ BootScene
|
||||
├─ PreloadScene
|
||||
├─ BattleScene
|
||||
├─ FxScene / DebugScene(可选)
|
||||
└─ Asset manifest
|
||||
```
|
||||
|
||||
### 3.2 强制边界
|
||||
|
||||
1. `domain/` 不依赖 Phaser、Web Audio、DOM、React、浏览器全局对象或后端 API。
|
||||
2. `domain/` 只接收 plain data,例如时间增量、归一化音量样本、对手 power、配置参数。
|
||||
3. `application/` 负责编排权限、校准、音频输入、AI 对手、tick 和 snapshot 分发。
|
||||
4. Phaser Scene 只消费 `BarkBattleSnapshot`,把 snapshot 映射成 sprite、动画、粒子、camera 和 sound effect;不得持有核心胜负、计数和能量条规则。
|
||||
5. DOM HUD 只消费 snapshot 和少量 runtime UI 状态,负责展示、按钮和弹层;不得重复实现核心胜负规则。
|
||||
6. 若后续接入平台作品/成绩/排行榜,前端只调用后端 API 和展示投影,不在本地绕过后端生成正式结论。
|
||||
|
||||
## 4. 建议目录结构
|
||||
|
||||
首版建议以独立 runtime 原型落在 `src/games/bark-battle/`,避免提前侵入平台创作链路。
|
||||
|
||||
```text
|
||||
src/games/bark-battle/
|
||||
domain/
|
||||
BarkBattleTypes.ts
|
||||
BarkBattleSession.ts
|
||||
BarkDetector.ts
|
||||
EnergyTugOfWar.ts
|
||||
BarkBattleScoring.ts
|
||||
OpponentStrategy.ts
|
||||
__tests__/
|
||||
BarkDetector.test.ts
|
||||
EnergyTugOfWar.test.ts
|
||||
BarkBattleSession.test.ts
|
||||
BarkBattleScoring.test.ts
|
||||
|
||||
application/
|
||||
BarkBattleController.ts
|
||||
BarkBattleConfig.ts
|
||||
BarkBattleSnapshotStore.ts
|
||||
__tests__/
|
||||
BarkBattleController.test.ts
|
||||
|
||||
infrastructure/
|
||||
BrowserMicrophoneInput.ts
|
||||
AudioAnalyserSampler.ts
|
||||
MicrophonePermission.ts
|
||||
__tests__/
|
||||
BrowserMicrophoneInput.test.ts
|
||||
AudioAnalyserSampler.test.ts
|
||||
|
||||
phaser/
|
||||
BarkBattleGameHost.ts
|
||||
scenes/
|
||||
BarkBattleBootScene.ts
|
||||
BarkBattlePreloadScene.ts
|
||||
BarkBattleScene.ts
|
||||
BarkBattleFxScene.ts
|
||||
assets/
|
||||
barkBattleAssetManifest.ts
|
||||
|
||||
ui/
|
||||
BarkBattleRuntimeShell.tsx
|
||||
BarkBattleHud.tsx
|
||||
BarkBattlePermissionPanel.tsx
|
||||
BarkBattleResultPanel.tsx
|
||||
BarkBattleMobileControls.tsx
|
||||
BarkBattleHud.css
|
||||
__tests__/
|
||||
BarkBattleHud.test.tsx
|
||||
BarkBattlePermissionPanel.test.tsx
|
||||
BarkBattleResultPanel.test.tsx
|
||||
```
|
||||
|
||||
若后续进入 Genarrative 正式玩法类型闭环,再按 `genarrative-play-type-integration` 扩展到:
|
||||
|
||||
```text
|
||||
src/components/bark-battle-runtime/BarkBattleRuntimeShell.tsx
|
||||
src/components/bark-battle-result/BarkBattleResultView.tsx
|
||||
src/services/barkBattleRuntimeClient.ts
|
||||
packages/shared/src/contracts/barkBattle.ts
|
||||
server-rs/crates/shared-contracts/src/bark_battle.rs
|
||||
```
|
||||
|
||||
首版不建议直接新增后端表或正式作品链路,除非产品明确要求成绩、发布和广场能力。
|
||||
|
||||
## 5. 核心 Domain 类型
|
||||
|
||||
### 5.1 基础枚举与数值约定
|
||||
|
||||
```ts
|
||||
export type BarkBattlePhase =
|
||||
| 'permission'
|
||||
| 'calibration'
|
||||
| 'countdown'
|
||||
| 'playing'
|
||||
| 'finished'
|
||||
| 'unavailable'
|
||||
|
||||
export type BarkBattleSide = 'player' | 'opponent'
|
||||
|
||||
export type BarkBattleWinner = BarkBattleSide | 'draw' | null
|
||||
|
||||
export type BarkBattleDifficulty = 'easy' | 'normal' | 'hard'
|
||||
|
||||
export type BarkBattleUiState =
|
||||
| 'idle'
|
||||
| 'permission-ready'
|
||||
| 'microphone-authorized'
|
||||
| 'calibrating'
|
||||
| 'ready-countdown'
|
||||
| 'playing'
|
||||
| 'finished'
|
||||
| 'microphone-unavailable'
|
||||
|
||||
export type MicrophoneFailureReason =
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'non-secure-context'
|
||||
| 'not-found'
|
||||
| 'not-readable'
|
||||
| 'audio-context-blocked'
|
||||
| 'calibration-timeout'
|
||||
| 'calibration-sample-unreadable'
|
||||
| 'unknown'
|
||||
```
|
||||
|
||||
关键数值:
|
||||
|
||||
- `energy`: `-100..100`,正数偏玩家侧,负数偏对手侧。
|
||||
- `currentVolume`: `0..1`,音频采样归一化后的瞬时音量。
|
||||
- `recentPeak`: `0..1`,短窗口内峰值。
|
||||
- `power`: `0..1` 或 `0..100` 二选一,建议 domain 内统一 `0..1`,HUD 显示再转百分比。
|
||||
- `remainingMs`: 单局剩余毫秒。
|
||||
|
||||
### 5.2 Snapshot
|
||||
|
||||
```ts
|
||||
export type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
uiState: BarkBattleUiState
|
||||
errorReason: MicrophoneFailureReason | null
|
||||
statusMessageKey: BarkBattleStatusMessageKey | null
|
||||
elapsedMs: number
|
||||
remainingMs: number
|
||||
countdownMs: number
|
||||
energy: number
|
||||
player: BarkSideState
|
||||
opponent: BarkSideState
|
||||
winner: BarkBattleWinner
|
||||
result: BarkBattleResult | null
|
||||
lastEvents: BarkBattleVisualEvent[]
|
||||
}
|
||||
|
||||
export type BarkSideState = {
|
||||
side: BarkBattleSide
|
||||
barkCount: number
|
||||
currentVolume: number
|
||||
recentPeak: number
|
||||
combo: number
|
||||
power: number
|
||||
isBarking: boolean
|
||||
lastBarkAtMs: number | null
|
||||
maxVolume: number
|
||||
}
|
||||
|
||||
export type BarkBattleResult = {
|
||||
winner: BarkBattleWinner
|
||||
finalEnergy: number
|
||||
playerBarkCount: number
|
||||
playerMaxVolume: number
|
||||
playerAveragePower: number
|
||||
score: number
|
||||
}
|
||||
|
||||
export type BarkBattleStatusMessageKey =
|
||||
| 'microphone-unsupported'
|
||||
| 'microphone-permission-denied'
|
||||
| 'microphone-non-secure-context'
|
||||
| 'microphone-not-found'
|
||||
| 'microphone-not-readable'
|
||||
| 'microphone-audio-context-blocked'
|
||||
| 'microphone-calibration-timeout'
|
||||
| 'microphone-calibration-sample-unreadable'
|
||||
| 'microphone-unknown-error'
|
||||
```
|
||||
|
||||
### 5.3 输入样本与叫声事件
|
||||
|
||||
```ts
|
||||
export type BarkAudioSample = {
|
||||
atMs: number
|
||||
volume: number
|
||||
peak: number
|
||||
rms: number
|
||||
}
|
||||
|
||||
export type BarkDetectedEvent = {
|
||||
id: string
|
||||
atMs: number
|
||||
side: BarkBattleSide
|
||||
volume: number
|
||||
strength: number
|
||||
combo: number
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 视觉事件
|
||||
|
||||
视觉事件由 domain 或 application 生成,但只包含 plain data,不包含 Phaser 对象:
|
||||
|
||||
```ts
|
||||
export type BarkBattleVisualEvent =
|
||||
| {
|
||||
type: 'bark-word'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
strength: number
|
||||
text: 'BARK' | 'WOOF' | 'WAN' | 'WANGOOF'
|
||||
}
|
||||
| {
|
||||
type: 'shockwave'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
strength: number
|
||||
}
|
||||
| {
|
||||
type: 'combo-burst'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
combo: number
|
||||
}
|
||||
```
|
||||
|
||||
Phaser 只根据这些事件播放一次性特效,并维护已消费事件 ID,避免重复播放。
|
||||
|
||||
## 6. Domain 模块职责
|
||||
|
||||
### 6.1 BarkDetector
|
||||
|
||||
职责:把连续音频样本转换为有效叫声事件。
|
||||
|
||||
输入:
|
||||
|
||||
- `BarkAudioSample`
|
||||
- 校准后的 `ambientNoiseFloor`
|
||||
- `barkThreshold`
|
||||
- `minBarkGapMs`
|
||||
- `minBarkDurationMs`
|
||||
- `maxBarkDurationMs`
|
||||
|
||||
规则建议:
|
||||
|
||||
1. 音量超过动态阈值进入 candidate 状态。
|
||||
2. 峰值回落到阈值以下或持续时长达到上限时结束 candidate。
|
||||
3. candidate 持续时间在 `80ms..1200ms` 且与上一次有效叫声间隔足够时,记为一次叫声。
|
||||
4. 长时间持续噪音不应无限计数,只能按冷却和峰值回落形成新事件。
|
||||
5. MVP 不要求识别“是否真狗叫”,先基于音量峰值、时长和间隔判断。
|
||||
|
||||
### 6.2 EnergyTugOfWar
|
||||
|
||||
职责:更新红蓝拉锯条。
|
||||
|
||||
建议公式:
|
||||
|
||||
```text
|
||||
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
|
||||
opponentPower = opponentStrategy.tick(...)
|
||||
energyDelta = (playerPower - opponentPower) * deltaSeconds * balanceFactor
|
||||
energy = clamp(energy + energyDelta, -100, 100)
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `EnergyTugOfWar` 不知道玩家来自麦克风还是 mock input。
|
||||
- `EnergyTugOfWar` 不知道 Phaser 能量条宽度。
|
||||
- 平衡参数集中在 `BarkBattleConfig`,不要散落在 Scene 或 HUD 中。
|
||||
|
||||
### 6.3 BarkBattleSession
|
||||
|
||||
职责:管理局内 phase、计时、胜负和 snapshot。
|
||||
|
||||
状态机建议:
|
||||
|
||||
```text
|
||||
permission → calibration → countdown → playing → finished
|
||||
↘ unavailable
|
||||
```
|
||||
|
||||
`phase` 只表达 runtime 是否可继续参与局内流程;所有麦克风不可用、权限失败、非安全上下文和校准失败都统一收敛到 `phase: 'unavailable'`,再通过 `uiState: 'microphone-unavailable'` 与 `errorReason` 区分 HUD 展示和重试策略,避免把基础设施错误枚举直接扩散成 domain 阶段。
|
||||
|
||||
关键规则:
|
||||
|
||||
- `countdown` 结束才进入 `playing`。
|
||||
- `playing` 时 `remainingMs` 随 tick 递减。
|
||||
- `remainingMs <= 0` 后进入 `finished`。
|
||||
- `energy > drawThreshold` 判定玩家胜利。
|
||||
- `energy < -drawThreshold` 判定对手胜利。
|
||||
- `abs(energy) <= drawThreshold` 判定平局。
|
||||
|
||||
### 6.4 OpponentStrategy
|
||||
|
||||
职责:为单机 MVP 提供对手推动力。
|
||||
|
||||
```ts
|
||||
export interface OpponentStrategy {
|
||||
tick(input: OpponentTickInput): OpponentTickOutput
|
||||
}
|
||||
```
|
||||
|
||||
普通难度建议:
|
||||
|
||||
- 周期性小叫声提供基础压力。
|
||||
- 每 3~6 秒一次短爆发。
|
||||
- 玩家大幅领先时可轻微增强,但不能追到不可赢。
|
||||
|
||||
## 7. Web Audio 输入适配
|
||||
|
||||
### 7.1 BrowserMicrophoneInput
|
||||
|
||||
职责:封装浏览器麦克风权限与音频流生命周期。
|
||||
|
||||
建议 API:
|
||||
|
||||
```ts
|
||||
export interface MicrophoneInputPort {
|
||||
isSupported(): boolean
|
||||
requestPermission(): Promise<MicrophoneSession>
|
||||
stop(): void
|
||||
}
|
||||
|
||||
export interface MicrophoneSession {
|
||||
sample(atMs: number): BarkAudioSample
|
||||
stop(): void
|
||||
}
|
||||
```
|
||||
|
||||
实现要点:
|
||||
|
||||
1. 使用 `navigator.mediaDevices?.getUserMedia({ audio: true })`。
|
||||
2. 在用户点击“开始”后创建或 resume `AudioContext`,避免移动端自动播放策略拦截。
|
||||
3. 使用 `AnalyserNode` 读取时域数据,计算 RMS 与 peak。
|
||||
4. 输出归一化样本,不把 `MediaStream`、`AudioContext`、`AnalyserNode` 泄漏到 domain。
|
||||
5. 退出、重开、页面卸载时停止 track,避免麦克风占用残留。
|
||||
|
||||
### 7.2 校准流程
|
||||
|
||||
`calibration` 阶段建议持续 `800ms..1500ms`:
|
||||
|
||||
1. 收集静默环境样本。
|
||||
2. 计算 `ambientNoiseFloor`,例如 `p75` 或均值 + 标准差。
|
||||
3. 设置动态阈值:
|
||||
|
||||
```text
|
||||
barkThreshold = clamp(ambientNoiseFloor + 0.12, 0.18, 0.55)
|
||||
```
|
||||
|
||||
4. 若环境噪音过高,HUD 给出简短提示和“继续 / 重新校准”入口,但不要把长说明常驻在画面上。
|
||||
|
||||
### 7.3 权限与错误分类
|
||||
|
||||
```ts
|
||||
export type MicrophoneFailureReason =
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'non-secure-context'
|
||||
| 'not-found'
|
||||
| 'not-readable'
|
||||
| 'audio-context-blocked'
|
||||
| 'calibration-timeout'
|
||||
| 'calibration-sample-unreadable'
|
||||
| 'unknown'
|
||||
```
|
||||
|
||||
错误来源与分层归属:
|
||||
|
||||
| 失败原因 | 主要检测位置 | controller snapshot 表达 | HUD 可区分状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 浏览器无 `mediaDevices.getUserMedia` | `BrowserMicrophoneInput.isSupported()` | `phase: 'unavailable'`, `uiState: 'microphone-unavailable'`, `errorReason: 'unsupported'` | 设备或浏览器不支持麦克风输入,只提供返回入口,不展示可开始声控按钮 |
|
||||
| 非安全上下文 | `BrowserMicrophoneInput.isSupported()` 或 `MicrophonePermission` 预检 `window.isSecureContext` | `phase: 'unavailable'`, `errorReason: 'non-secure-context'` | 当前环境无法使用麦克风,提示使用受支持的安全环境或返回 |
|
||||
| 用户拒绝授权 | `BrowserMicrophoneInput.requestPermission()` 捕获 `NotAllowedError` / `SecurityError` | `phase: 'unavailable'`, `errorReason: 'permission-denied'` | 提供重新授权或返回入口,不进入 calibration/countdown/playing |
|
||||
| 未检测到设备 | `getUserMedia` 捕获 `NotFoundError` / `DevicesNotFoundError` | `phase: 'unavailable'`, `errorReason: 'not-found'` | 展示麦克风不可用,可重试授权或返回 |
|
||||
| 设备被占用或不可读 | `getUserMedia` 捕获 `NotReadableError` / `TrackStartError` | `phase: 'unavailable'`, `errorReason: 'not-readable'` | 展示麦克风不可用,可重试授权或返回 |
|
||||
| AudioContext 被移动端策略拦截 | 用户手势后创建 / resume `AudioContext` 失败 | `phase: 'unavailable'`, `errorReason: 'audio-context-blocked'` | 提示点击重试,不自动循环请求 |
|
||||
| 校准超时 | `BarkBattleController` 在 calibration 阶段等待样本超出 `calibrationMaxWaitMs` | `phase: 'unavailable'`, `errorReason: 'calibration-timeout'` | 展示麦克风输入不可用,提供重试校准入口 |
|
||||
| 校准样本不可读 | `AudioAnalyserSampler.sample()` 持续返回空样本、NaN 或无法读取 buffer | `phase: 'unavailable'`, `errorReason: 'calibration-sample-unreadable'` | 展示麦克风输入不可用,提供重试校准入口 |
|
||||
|
||||
前端只根据错误分类展示可操作状态:重试授权、重试校准、返回、或使用调试备用输入。不要把浏览器原始错误堆栈展示给玩家。
|
||||
|
||||
## 8. Phaser Scene 切分
|
||||
|
||||
### 8.1 BarkBattleBootScene
|
||||
|
||||
职责:
|
||||
|
||||
- 初始化 Phaser 全局配置。
|
||||
- 注册 scale、background color、全局事件桥。
|
||||
- 不加载重资源,不处理玩法规则。
|
||||
|
||||
### 8.2 BarkBattlePreloadScene
|
||||
|
||||
职责:
|
||||
|
||||
- 根据 `barkBattleAssetManifest` 加载背景、狗狗 sprite、声浪 FX、拟声词 bitmap / atlas、轻量音效。
|
||||
- 使用稳定 manifest key,不在 gameplay 代码中散写文件路径。
|
||||
- 加载完成后进入 `BarkBattleScene`。
|
||||
|
||||
### 8.3 BarkBattleScene
|
||||
|
||||
职责:
|
||||
|
||||
- 创建横版舞台、左右狗狗、背景层、声浪层、拟声词层。
|
||||
- 每帧读取最新 `BarkBattleSnapshot`。
|
||||
- 根据 snapshot 更新:
|
||||
- 狗狗 idle / bark / win / lose 动画
|
||||
- 声浪强度
|
||||
- camera shake
|
||||
- transient bark words
|
||||
- shockwave
|
||||
- 把可选的调试输入 action 传给 controller,但不处理麦克风和规则。
|
||||
|
||||
不得在 Scene 中实现:
|
||||
|
||||
- 叫声计数
|
||||
- 胜负判定
|
||||
- 能量条规则
|
||||
- 权限流程
|
||||
- 结算数据计算
|
||||
|
||||
### 8.4 BarkBattleFxScene(可选)
|
||||
|
||||
如果特效复杂,可拆出叠加 Scene:
|
||||
|
||||
- 专门处理拟声词、粒子、冲击波和 camera shake。
|
||||
- 通过视觉事件 ID 去重。
|
||||
- 对 `prefers-reduced-motion` 或低端设备降级。
|
||||
|
||||
首版也可以先把 FX 保持在 `BarkBattleScene` 内,但必须仍然只消费 snapshot / visual events。
|
||||
|
||||
## 9. DOM HUD 设计
|
||||
|
||||
### 9.1 HUD 层级
|
||||
|
||||
DOM HUD 建议覆盖在 Phaser Canvas 上方:
|
||||
|
||||
```text
|
||||
BarkBattleRuntimeShell
|
||||
├─ <div className="bark-battle-canvas-host" />
|
||||
└─ <BarkBattleHud snapshot={snapshot} uiState={uiState} />
|
||||
```
|
||||
|
||||
HUD 分区:
|
||||
|
||||
- 顶部:红蓝声浪能量条 + 小型剩余时间。
|
||||
- 中央:仅在倒计时、关键提示或结算时短暂展示大号状态。
|
||||
- 左右边缘:双方简洁状态,例如叫声次数 / combo chip。
|
||||
- 底部角落:麦克风状态、重试、小菜单。
|
||||
- 结算:独立居中面板,显示胜负、叫声次数、最大音量、评分、再来一局、返回。
|
||||
|
||||
### 9.2 Playfield 保护
|
||||
|
||||
遵循 game UI 约束:
|
||||
|
||||
1. 正常 playing 阶段保持中心和下中部 playfield 清爽,不常驻长文案。
|
||||
2. 不把规则说明、长控制说明、多段提示默认铺在画面上。
|
||||
3. 权限、设置、结算使用独立面板或弹层,不在当前面板下面展开一大块内容。
|
||||
4. 移动端优先保证顶部能量条、倒计时、狗狗和重试入口可见可点。
|
||||
5. 大动效不能遮挡顶部能量条和倒计时。
|
||||
|
||||
### 9.3 CSS 设计建议
|
||||
|
||||
- 使用局部 CSS class 或 CSS module,避免污染全站。
|
||||
- 使用 CSS 变量定义主题:
|
||||
- `--bark-player-color`
|
||||
- `--bark-opponent-color`
|
||||
- `--bark-panel-bg`
|
||||
- `--bark-safe-bottom`
|
||||
- 使用 `dvh` / `svh` 和 safe-area inset 处理移动端地址栏与刘海。
|
||||
- `pointer-events` 分层:HUD 容器默认 `pointer-events: none`,按钮和面板恢复 `pointer-events: auto`。
|
||||
|
||||
## 10. 移动端与权限降级
|
||||
|
||||
### 10.1 移动端输入约束
|
||||
|
||||
移动端浏览器通常要求用户手势才能启动 AudioContext。开局流程必须是:
|
||||
|
||||
```text
|
||||
玩家点击“开始” → requestPermission → 创建/恢复 AudioContext → calibration → countdown → playing
|
||||
```
|
||||
|
||||
不要在页面加载时自动请求或自动启动 AudioContext。
|
||||
|
||||
### 10.2 响应式布局
|
||||
|
||||
移动端建议:
|
||||
|
||||
- 横屏优先呈现完整舞台;竖屏可保持舞台居中并缩小 HUD。
|
||||
- 顶部能量条高度保持可读,但不要占满大面积。
|
||||
- 结算面板宽度使用 `min(92vw, 420px)`。
|
||||
- 底部按钮避开 `env(safe-area-inset-bottom)`。
|
||||
- 非关键设置折叠进小菜单。
|
||||
|
||||
### 10.3 权限失败降级
|
||||
|
||||
权限失败时:
|
||||
|
||||
- `unsupported`:展示“当前浏览器不支持麦克风输入”,提供返回入口,不展示开始声控按钮。
|
||||
- `non-secure-context`:展示“当前环境无法使用麦克风”,提示切换到受支持的安全环境或返回。
|
||||
- `permission-denied`:展示简短说明和“重新授权”入口。
|
||||
- `not-found`:提示未检测到麦克风,提供重试授权或返回入口。
|
||||
- `not-readable`:提示麦克风被占用或暂时不可读,提供重试授权或返回入口。
|
||||
- `audio-context-blocked`:提示点击重试。
|
||||
- `calibration-timeout` / `calibration-sample-unreadable`:提示麦克风输入不可用,提供“重新校准”和返回入口。
|
||||
|
||||
可选开发调试降级:
|
||||
|
||||
- 本地 dev 可启用键盘 mock input,例如按住空格模拟音量峰值。
|
||||
- mock input 必须标记为开发/调试能力,不作为正式竞技能力。
|
||||
|
||||
## 11. 测试策略
|
||||
|
||||
### 11.1 Domain 单元测试(优先)
|
||||
|
||||
目标:不接 Phaser、不接 DOM、不接 Web Audio。
|
||||
|
||||
建议测试:
|
||||
|
||||
- `BarkDetector`:超过阈值且间隔足够时计为一次有效叫声。
|
||||
- `BarkDetector`:持续噪音不会无限计数。
|
||||
- `BarkDetector`:低于环境噪音阈值不计入叫声。
|
||||
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
|
||||
- `EnergyTugOfWar`:energy clamp 在 `-100..100`。
|
||||
- `BarkBattleSession`:倒计时结束进入 playing。
|
||||
- `BarkBattleSession`:剩余时间归零进入 finished。
|
||||
- `BarkBattleSession`:按 energy 和 drawThreshold 判定胜 / 负 / 平。
|
||||
|
||||
### 11.2 Application 测试
|
||||
|
||||
目标:验证输入样本、AI、tick 和 snapshot 编排。
|
||||
|
||||
建议测试:
|
||||
|
||||
- 权限允许后进入校准,再进入倒计时。
|
||||
- 权限拒绝后 `phase` 为 `unavailable`、`errorReason` 为 `permission-denied`,不进入 playing。
|
||||
- 非安全上下文、设备未找到、设备不可读、AudioContext 被拦截时,controller snapshot 都进入 `phase: 'unavailable'`,并保留可供 HUD 区分的 `errorReason`。
|
||||
- 校准超时或样本持续不可读时,controller snapshot 使用 `errorReason: 'calibration-timeout'` 或 `calibration-sample-unreadable`,并提供重试校准动作。
|
||||
- 提交 mock audio sample 后 snapshot 中玩家状态更新。
|
||||
- AI 对手 power 参与能量条拉锯。
|
||||
- `lastEvents` 只发布新增视觉事件。
|
||||
|
||||
### 11.3 HUD 组件测试
|
||||
|
||||
目标:验证 snapshot 到 DOM 的展示映射。
|
||||
|
||||
建议测试:
|
||||
|
||||
- playing 阶段展示倒计时和能量条。
|
||||
- energy 正负值映射到玩家 / 对手侧比例。
|
||||
- `errorReason: 'permission-denied'` 展示重试授权入口。
|
||||
- `errorReason: 'unsupported'` 展示返回入口且不展示开始声控按钮。
|
||||
- `not-found`、`not-readable`、`non-secure-context`、`audio-context-blocked`、`calibration-timeout`、`calibration-sample-unreadable` 分别映射到可区分的简短状态文案和对应操作。
|
||||
- finished 展示胜负、叫声次数和再来一局。
|
||||
- 移动端 class / 结构不依赖 Phaser Canvas 才能渲染。
|
||||
|
||||
### 11.4 Phaser 集成与 smoke
|
||||
|
||||
自动化层面不强行在 Vitest 中完整启动 Phaser。建议:
|
||||
|
||||
- 用 adapter mock 测试 Scene 消费 snapshot 的纯映射函数。
|
||||
- 浏览器 smoke 验证真实 Canvas、Web Audio 和动画。
|
||||
- 若后续引入 Playwright,再做最小视觉和交互 smoke。
|
||||
|
||||
## 12. 验证命令建议
|
||||
|
||||
文档阶段只需要编码检查和 diff 检查:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff -- docs/prd/BARK_BATTLE_BDD_2026-05-11.md docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
```
|
||||
|
||||
后续实现 domain 后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/**/*.test.ts
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续实现 infrastructure/application 错误状态后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续实现 HUD 后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续接入浏览器 runtime 后建议:
|
||||
|
||||
```bash
|
||||
npm run dev:web
|
||||
# 人工 smoke:授权麦克风 → 校准 → 发声 → 能量条变化 → 结算 → 再来一局
|
||||
```
|
||||
|
||||
若未来接入 Genarrative 正式玩法类型、后端持久化或发布链路,再追加对应契约、api-server 和 SpacetimeDB 验证;首版 runtime 原型不应提前新增这些命令作为门槛。
|
||||
|
||||
## 13. 后续落地顺序
|
||||
|
||||
建议后续实现按以下顺序推进:
|
||||
|
||||
1. 先建 `domain/` 和纯单元测试。
|
||||
2. 实现 `BarkDetector`、`EnergyTugOfWar`、`BarkBattleSession` 的最小规则。
|
||||
3. 建 `application/` controller,用 mock audio sample 跑通 snapshot。
|
||||
4. 实现 DOM HUD 的 permission / energy / timer / result 展示。
|
||||
5. 接入 `BrowserMicrophoneInput` 和校准流程。
|
||||
6. 接入 Phaser host、Scene 和 asset manifest,占位素材先跑通视觉反馈。
|
||||
7. 做移动端视口和权限失败 smoke。
|
||||
8. 产品确认后再决定是否进入正式玩法类型、作品发布和后端真相链。
|
||||
|
||||
## 14. 关键技术决策
|
||||
|
||||
1. 默认采用 Phaser 3 + TypeScript + Vite,符合 2D 浏览器游戏默认路线。
|
||||
2. 核心 simulation 放在纯 TypeScript domain,严格不依赖 Phaser / Web Audio / DOM。
|
||||
3. Web Audio 只作为输入 adapter,输出归一化 `BarkAudioSample`。
|
||||
4. Phaser Scene 是 renderer,只消费 snapshot 和 visual events,不承载规则真相。
|
||||
5. HUD 使用 DOM overlay,承载权限、能量条、倒计时、结算和移动端响应式布局。
|
||||
6. MVP 不做复杂狗叫语义识别,先用音量峰值、持续时长、冷却和环境噪音校准。
|
||||
7. MVP 建议玩家 vs AI 单机 runtime,正式成绩、排行榜、发布和奖励后续再交给后端链路。
|
||||
1055
docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md
Normal file
1055
docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
# 儿童动作识别互动玩法 Demo 热身关开发规格文档
|
||||
|
||||
> 日期:2026-05-09
|
||||
> 关联设计文档:[CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md](../design/CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md)
|
||||
> 适用范围:儿童动作识别互动玩法 Demo 固定启动热身关
|
||||
> 文档性质:开发落地规格
|
||||
> 说明:本文只将已确认的热身关设计内容拆解为工程可执行规格,不新增未确认的玩法、文案或视觉设计。
|
||||
|
||||
## 1. 开发目标
|
||||
|
||||
热身关作为 Demo 启动后的固定流程,需要完成以下开发目标:
|
||||
|
||||
1. 调用摄像头并识别用户和环境。
|
||||
2. 使用横屏比例展示热身关。
|
||||
3. 在屏幕中央地面生成绿色圆环,引导用户到达建议位置。
|
||||
4. 将用户实际位置生成角色剪影。
|
||||
5. 只对摄像头背景做虚化处理,表达隐私保护、屏蔽环境干扰,并营造空间感。
|
||||
6. 按固定步骤完成站位、招手、左右移动、挥动左右手、原地跳跃检测。
|
||||
7. 记录用户左右移动距离、挥动手臂空间和跳跃空间。
|
||||
8. 将记录结果仅保存在当前 Demo 体验会话内。
|
||||
9. 后续关卡使用热身记录的边界进行安全提醒和暂停恢复。
|
||||
10. 热身结束后进入关卡选择。
|
||||
|
||||
当前阶段先落浏览器本地 Demo。浏览器摄像头视频流仅作为舞台背景;热身动作检测以本地 mocap 动作数据源为准,通过 `useMocapInput` 连接 `http://127.0.0.1:8876/stream` 对应的 WebSocket 流,消费 `general.body.center_norm` 身体中心、手势、左右手坐标和跳跃事件推进站位、招手、左右手挥动与原地跳跃步骤。正式语音播报接口继续预留适配层,不阻塞前端热身流程、调试输入和页面表现骨架落地。
|
||||
|
||||
## 2. 非目标范围
|
||||
|
||||
热身关当前不包含以下内容:
|
||||
|
||||
1. 不接入创作模块。
|
||||
2. 不作为可配置玩法模板提供给创作者。
|
||||
3. 不允许跳过步骤。
|
||||
4. 不允许系统自动进入下一步。
|
||||
5. 不设置动作检测最长等待时间。
|
||||
6. 不做特定用户识别。
|
||||
7. 不跨会话保存左右空间边界、手臂挥动空间和跳跃空间。
|
||||
8. 不对手部细节进行识别,只对肢体进行区分。
|
||||
9. 本阶段不处理无硬件、拒绝摄像头、多人入镜、识别丢失等异常流程;这些问题记录为待决策事项,后续硬件与摄像头方案稳定后再重新设计。
|
||||
|
||||
## 3. 运行入口与流向
|
||||
|
||||
### 3.1 入口
|
||||
|
||||
用户进入 Demo 后,先进入热身关。
|
||||
|
||||
### 3.2 出口
|
||||
|
||||
用户完成热身关所有步骤后,进入关卡选择。
|
||||
|
||||
热身结束后展示“开始游戏”按钮,用户点击后进入宝贝识物首关本地 Demo。该入口只用于热身关后的本地体验验证;正式平台体验仍必须通过“宝贝识物”创作模板发布后,在寓教于乐板块进入。
|
||||
|
||||
### 3.3 固定流程顺序
|
||||
|
||||
热身关必须按照以下顺序执行:
|
||||
|
||||
```text
|
||||
进入热身关
|
||||
↓
|
||||
到达中央绿色圆环并保持 2 秒
|
||||
↓
|
||||
招手 / 摆手
|
||||
↓
|
||||
热身说明
|
||||
↓
|
||||
向左一步,到达左侧绿色圆环并保持 2 秒
|
||||
↓
|
||||
回到中间,到达中央绿色圆环并保持 2 秒
|
||||
↓
|
||||
向右一步,到达右侧绿色圆环并保持 2 秒
|
||||
↓
|
||||
回到中间,到达中央绿色圆环并保持 2 秒
|
||||
↓
|
||||
挥动左手
|
||||
↓
|
||||
挥动右手
|
||||
↓
|
||||
原地跳一下
|
||||
↓
|
||||
播放热身结束特效和结束语音
|
||||
↓
|
||||
进入关卡选择
|
||||
```
|
||||
|
||||
## 4. 页面基础表现规格
|
||||
|
||||
### 4.1 横屏比例
|
||||
|
||||
热身关需要使用横屏比例制作和展示,适用于电视屏幕、电脑屏幕等环境。
|
||||
|
||||
### 4.2 摄像头画面处理
|
||||
|
||||
用户进入热身关时调用摄像头。
|
||||
|
||||
摄像头画面处理要求:
|
||||
|
||||
1. 识别用户和环境。
|
||||
2. 将用户实际位置生成角色剪影。
|
||||
3. 只对摄像头背景做虚化处理。
|
||||
4. 用户角色剪影用于表达用户在画面中的实际位置。
|
||||
5. 背景虚化用于表达对用户隐私的保护、屏蔽周围环境干扰,并营造空间感。
|
||||
|
||||
### 4.3 绿色圆环
|
||||
|
||||
绿色圆环用于指引用户到达指定位置。
|
||||
|
||||
绿色圆环出现位置包括:
|
||||
|
||||
1. 屏幕中央位置的地面。
|
||||
2. 屏幕中心向左一个身位,约半米的地面位置。
|
||||
3. 屏幕中心向右一个身位,约半米的地面位置。
|
||||
|
||||
“约半米”技术上以角色剪影移动距离为准,后续根据体验调校。
|
||||
|
||||
### 4.4 绿色圆环选中状态
|
||||
|
||||
用户到达绿色圆环后,绿色圆环进入 2 秒选中状态。
|
||||
|
||||
用户需要在绿色圆环内保持 2 秒,才算完成该位置检测。
|
||||
|
||||
## 5. 通用交互规则
|
||||
|
||||
### 5.1 不允许跳过
|
||||
|
||||
每个步骤都必须由用户完成。
|
||||
|
||||
系统不提供跳过,也不自动进入下一步。
|
||||
|
||||
### 5.2 引导动画规则
|
||||
|
||||
每个动作等待 3 秒后可以播放对应引导动画。
|
||||
|
||||
当前不设置最长等待时间。
|
||||
|
||||
### 5.3 手势检测规则
|
||||
|
||||
招手 / 摆手、挥动左手、挥动右手三类动作需要有动作区分。
|
||||
|
||||
检测只区分肢体,不识别手部细节。
|
||||
|
||||
### 5.4 手势引导规则
|
||||
|
||||
挥动哪只手,就使用对应手的引导。
|
||||
|
||||
## 6. 状态机规格
|
||||
|
||||
### 6.1 状态列表
|
||||
|
||||
热身关至少需要支持以下流程状态:
|
||||
|
||||
| 状态 ID | 状态名称 | 进入条件 | 完成条件 | 下一状态 |
|
||||
|---|---|---|---|---|
|
||||
| warmup_enter | 进入热身关 | 用户进入 Demo | 摄像头调用并展示中央绿色圆环 | center_arrive |
|
||||
| center_arrive | 到达中央圆环 | 中央绿色圆环出现 | 用户到达中央圆环并保持 2 秒 | wave_greeting |
|
||||
| wave_greeting | 招手教学 | 中央圆环完成并播放圆圈消失特效 | 用户完成招手 / 摆手 | warmup_intro |
|
||||
| warmup_intro | 热身说明 | 招手 / 摆手完成 | 播放热身说明文案与语音 | move_left |
|
||||
| move_left | 向左一步 | 热身说明完成 | 用户到达左侧圆环并保持 2 秒 | return_center_1 |
|
||||
| return_center_1 | 回到中间(一) | 向左一步完成 | 用户到达中央圆环并保持 2 秒 | move_right |
|
||||
| move_right | 向右一步 | 回到中间(一)完成 | 用户到达右侧圆环并保持 2 秒 | return_center_2 |
|
||||
| return_center_2 | 回到中间(二) | 向右一步完成 | 用户到达中央圆环并保持 2 秒 | wave_left_hand |
|
||||
| wave_left_hand | 挥动左手 | 回到中间(二)完成 | 用户完成挥动左手 | wave_right_hand |
|
||||
| wave_right_hand | 挥动右手 | 挥动左手完成 | 用户完成挥动右手 | jump_once |
|
||||
| jump_once | 原地跳一下 | 挥动右手完成 | 用户完成原地跳一下 | warmup_finish |
|
||||
| warmup_finish | 热身结束 | 原地跳一下完成 | 播放热身结束特效和结束语音 | level_select |
|
||||
| level_select | 关卡选择 | 热身结束 | 进入关卡选择 | - |
|
||||
|
||||
### 6.2 状态推进约束
|
||||
|
||||
1. 状态必须按顺序推进。
|
||||
2. 用户未完成当前状态检测目标时,不进入下一状态。
|
||||
3. 位置类状态必须满足“到达绿色圆环并保持 2 秒”。
|
||||
4. 动作类状态没有最长等待时间。
|
||||
5. 动作类状态等待 3 秒后可以播放对应引导动画。
|
||||
|
||||
### 6.3 开发者调试输入
|
||||
|
||||
本地 Demo 需要支持开发者调试模式,用于无摄像头和自动化验证场景。
|
||||
|
||||
调试映射如下:
|
||||
|
||||
1. `A` 键映射用户向左移动。
|
||||
2. `D` 键映射用户向右移动。
|
||||
3. 鼠标左键按下并拖动映射左手轨迹。
|
||||
4. 鼠标右键按下并拖动映射右手轨迹。
|
||||
5. 空格键映射原地跳跃。
|
||||
|
||||
调试输入只作为本地 Demo 与测试辅助,不代表正式动作识别硬件口径。正式摄像头接入后,位置、手势和跳跃判断需要按摄像头硬件调教结果重新校准。
|
||||
|
||||
## 7. 分步骤开发规格
|
||||
|
||||
### 7.1 进入热身关
|
||||
|
||||
#### 展示内容
|
||||
|
||||
- 调用摄像头。
|
||||
- 识别用户和环境。
|
||||
- 屏幕中央地面显示绿色圆环。
|
||||
- 用户实际位置显示为角色剪影。
|
||||
- 只对摄像头背景做虚化。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
欢迎你,小朋友,见到你真开心
|
||||
请你来到圆圈这里和我打个招呼吧
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户到达中央绿色圆环并保持 2 秒。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
播放圆圈消失特效。
|
||||
|
||||
---
|
||||
|
||||
### 7.2 招手教学
|
||||
|
||||
#### 展示内容
|
||||
|
||||
播放招手的手势引导。
|
||||
|
||||
用户进入该步骤 3 秒仍未完成动作时,可以播放引导动画。
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户完成招手 / 摆手手势。
|
||||
|
||||
#### 完成后
|
||||
|
||||
进入热身说明。
|
||||
|
||||
---
|
||||
|
||||
### 7.3 热身说明
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧
|
||||
```
|
||||
|
||||
#### 完成后
|
||||
|
||||
进入“向左一步”。
|
||||
|
||||
---
|
||||
|
||||
### 7.4 向左一步
|
||||
|
||||
#### 展示内容
|
||||
|
||||
屏幕中心向左一个身位,约半米的地面位置出现新的绿色圆圈。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
向左一步
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户到达左侧绿色圆环并保持 2 秒。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录本次向左移动距离,作为后续关卡中的左侧空间边界参考。
|
||||
|
||||
---
|
||||
|
||||
### 7.5 回到中间来(一)
|
||||
|
||||
#### 展示内容
|
||||
|
||||
场地中心位置出现绿色圆圈。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
回到中间来
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户到达中央绿色圆环并保持 2 秒。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.6 向右一步
|
||||
|
||||
#### 展示内容
|
||||
|
||||
屏幕中心向右一个身位,约半米的地面位置出现新的绿色圆圈。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
向右一步
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户到达右侧绿色圆环并保持 2 秒。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录本次向右移动距离,作为后续关卡中的右侧空间边界参考。
|
||||
|
||||
---
|
||||
|
||||
### 7.7 回到中间来(二)
|
||||
|
||||
#### 展示内容
|
||||
|
||||
场地中心位置出现绿色圆圈。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
回到中间来
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户到达中央绿色圆环并保持 2 秒。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.8 挥动左手
|
||||
|
||||
#### 展示内容
|
||||
|
||||
播放伸展手臂挥动左手的手势引导。
|
||||
|
||||
用户进入该步骤 3 秒仍未完成动作时,可以播放引导动画。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
挥动左手
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户完成挥动左手。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录用户挥动左手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
---
|
||||
|
||||
### 7.9 挥动右手
|
||||
|
||||
#### 展示内容
|
||||
|
||||
播放伸展手臂挥动右手的手势引导。
|
||||
|
||||
用户进入该步骤 3 秒仍未完成动作时,可以播放引导动画。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
挥动右手
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户完成挥动右手。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
```text
|
||||
真棒
|
||||
```
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录用户挥动右手的空间,保存为该用户对应的行为坐标。
|
||||
|
||||
---
|
||||
|
||||
### 7.10 原地跳一下
|
||||
|
||||
#### 展示内容
|
||||
|
||||
播放跳跃姿势引导。
|
||||
|
||||
用户进入该步骤 3 秒仍未完成动作时,可以播放引导动画。
|
||||
|
||||
#### 文案与语音
|
||||
|
||||
```text
|
||||
原地跳一下
|
||||
```
|
||||
|
||||
#### 检测目标
|
||||
|
||||
用户完成原地跳一下。
|
||||
|
||||
#### 数据记录
|
||||
|
||||
记录用户跳跃空间,保存为该用户对应的行为坐标。
|
||||
|
||||
#### 完成反馈
|
||||
|
||||
播放热身结束特效、上浮字幕和语音:
|
||||
|
||||
```text
|
||||
真厉害,你是我见过最聪明的小朋友
|
||||
别走开,现在开始我们的游戏吧
|
||||
```
|
||||
|
||||
#### 完成后
|
||||
|
||||
进入关卡选择。
|
||||
|
||||
## 8. 当前 Demo 体验会话数据
|
||||
|
||||
### 8.1 保存范围
|
||||
|
||||
以下数据仅在当前 Demo 体验会话内保存:
|
||||
|
||||
1. 左侧空间边界。
|
||||
2. 右侧空间边界。
|
||||
3. 左手挥动空间。
|
||||
4. 右手挥动空间。
|
||||
5. 跳跃空间。
|
||||
|
||||
当前 Demo 体验会话数据需要满足:
|
||||
|
||||
1. 用户刷新产品或退出产品后失效。
|
||||
2. 用户只关闭当前游戏关卡并重新进入时,可以直接来到开始游戏界面,不强制重复热身。
|
||||
3. 首版可使用前端运行时内存或同等生命周期容器保存;不得跨产品刷新持久化保存。
|
||||
|
||||
### 8.2 当前 Demo 体验会话定义
|
||||
|
||||
“当前 Demo 体验会话”指用户本次打开并体验 Demo 的过程。
|
||||
|
||||
当用户关闭 Demo、刷新页面、退出当前体验流程、重新进入 Demo,或更换设备后,系统不再沿用上一次热身记录的数据,需要重新完成热身关并重新记录。
|
||||
|
||||
### 8.3 仅会话内保存原因
|
||||
|
||||
采用仅当前 Demo 体验会话内保存的原因:
|
||||
|
||||
1. 每名用户的身高、体型、动作幅度不同,安全边界和行为坐标会发生变化。
|
||||
2. 当前 Demo 不做特定用户识别,无法确认下一次体验的是否仍是同一名用户。
|
||||
3. 用户所处的体验环境可能变化,包括房间大小、摄像头位置、屏幕位置和站立距离。
|
||||
4. 为保证安全,每次体验都需要重新对环境和距离进行安全检查。
|
||||
|
||||
## 9. 后续关卡安全边界使用规则
|
||||
|
||||
后续关卡需要使用热身关记录的左右空间边界进行安全判断。
|
||||
|
||||
### 9.1 覆盖安全边界线
|
||||
|
||||
当用户身体主体覆盖安全边界线时,对应侧屏幕边缘出现虚影提醒。
|
||||
|
||||
### 9.2 超出安全边界线
|
||||
|
||||
当用户身体主体超出安全边界线时:
|
||||
|
||||
1. 关卡内容暂停。
|
||||
2. 屏幕虚化。
|
||||
3. 屏幕中央地面出现绿色圆圈。
|
||||
4. 屏幕提示文案:
|
||||
|
||||
```text
|
||||
小朋友,要注意安全哦
|
||||
```
|
||||
|
||||
5. 用户需要回到中心绿色圆圈并保持 2 秒后,才能继续游戏内容。
|
||||
|
||||
## 10. 识别能力清单
|
||||
|
||||
热身关需要接入或实现以下识别能力:
|
||||
|
||||
1. 摄像头调用。
|
||||
2. 用户识别。
|
||||
3. 环境识别。
|
||||
4. 用户实际位置识别。
|
||||
5. 用户是否到达中央绿色圆环位置。
|
||||
6. 用户是否在绿色圆环内持续保持 2 秒。
|
||||
7. 用户是否到达左侧约半米绿色圆环位置。
|
||||
8. 用户是否到达右侧约半米绿色圆环位置。
|
||||
9. 招手 / 摆手手势识别。
|
||||
10. 挥动左手识别。
|
||||
11. 挥动右手识别。
|
||||
12. 原地跳跃姿势识别。
|
||||
13. 用户左右移动距离记录。
|
||||
14. 用户挥动手臂空间记录。
|
||||
15. 用户跳跃空间记录。
|
||||
16. 用户身体主体覆盖安全边界线判断。
|
||||
17. 用户身体主体超出安全边界线判断。
|
||||
18. 用户回到中心绿色圆环并保持 2 秒判断。
|
||||
|
||||
## 11. 表现能力清单
|
||||
|
||||
热身关需要实现以下表现能力:
|
||||
|
||||
1. 横屏比例显示。
|
||||
2. 摄像头背景虚化。
|
||||
3. 用户位置生成角色剪影。
|
||||
4. 屏幕中央地面绿色圆环。
|
||||
5. 左侧约半米地面绿色圆环。
|
||||
6. 右侧约半米地面绿色圆环。
|
||||
7. 绿色圆环 2 秒选中状态。
|
||||
8. 圆圈消失特效。
|
||||
9. 招手手势引导。
|
||||
10. 伸展手臂挥动左手手势引导。
|
||||
11. 伸展手臂挥动右手手势引导。
|
||||
12. 跳跃姿势引导。
|
||||
13. 热身结束特效。
|
||||
14. 上浮字幕。
|
||||
15. 语音播报。
|
||||
16. 安全边界虚影提醒。
|
||||
17. 关卡暂停时屏幕虚化。
|
||||
18. 关卡暂停时屏幕中央地面绿色圆圈。
|
||||
19. 关卡暂停提示文案。
|
||||
|
||||
角色剪影、绿色圆环、虚影提醒、圆圈消失特效、手势引导动画和热身结束特效的正式视觉资源将通过 gpt-image-2 设计和生成。本地 Demo 阶段可以先使用 CSS、Canvas 或临时占位资源实现相同交互位置与状态,不把占位资源写死为正式资产。
|
||||
|
||||
## 12. 固定文案与语音清单
|
||||
|
||||
以下文案需要作为屏幕中上方浮现文字,并同步语音播报。
|
||||
|
||||
正式语音播报后续接入语音播报功能接口。本地 Demo 阶段保留播报适配层与调用点,可先只展示文字,不强制生成或播放正式语音资产。
|
||||
|
||||
```text
|
||||
欢迎你,小朋友,见到你真开心
|
||||
请你来到圆圈这里和我打个招呼吧
|
||||
你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧
|
||||
向左一步
|
||||
真棒
|
||||
回到中间来
|
||||
真棒
|
||||
向右一步
|
||||
真棒
|
||||
回到中间来
|
||||
真棒
|
||||
挥动左手
|
||||
真棒
|
||||
挥动右手
|
||||
真棒
|
||||
原地跳一下
|
||||
真厉害,你是我见过最聪明的小朋友
|
||||
别走开,现在开始我们的游戏吧
|
||||
小朋友,要注意安全哦
|
||||
```
|
||||
|
||||
## 13. 开发验收标准
|
||||
|
||||
### 13.1 热身流程验收
|
||||
|
||||
1. 用户进入 Demo 后先进入热身关。
|
||||
2. 热身关使用横屏比例展示。
|
||||
3. 摄像头被调用。
|
||||
4. 用户位置显示为角色剪影。
|
||||
5. 摄像头背景被虚化。
|
||||
6. 中央、左侧、右侧绿色圆环可以按流程出现。
|
||||
7. 用户到达每个绿色圆环后,需要保持 2 秒才算完成。
|
||||
8. 每个步骤未完成时不能跳过,也不能自动进入下一步。
|
||||
9. 动作等待 3 秒后可以播放对应引导动画。
|
||||
10. 所有固定文案可以展示并语音播报。
|
||||
11. 完成全部热身步骤后进入关卡选择。
|
||||
|
||||
### 13.2 数据记录验收
|
||||
|
||||
1. 完成向左一步后,可以记录左侧空间边界。
|
||||
2. 完成向右一步后,可以记录右侧空间边界。
|
||||
3. 完成挥动左手后,可以记录左手挥动空间。
|
||||
4. 完成挥动右手后,可以记录右手挥动空间。
|
||||
5. 完成原地跳一下后,可以记录跳跃空间。
|
||||
6. 以上数据仅在当前 Demo 体验会话内保存。
|
||||
7. 重新进入 Demo 后,不沿用上一次热身记录,需要重新完成热身关。
|
||||
|
||||
### 13.3 后续关卡安全边界验收
|
||||
|
||||
1. 用户身体主体覆盖安全边界线时,对应侧屏幕边缘出现虚影提醒。
|
||||
2. 用户身体主体超出安全边界线时,关卡内容暂停。
|
||||
3. 关卡暂停时,屏幕虚化。
|
||||
4. 关卡暂停时,屏幕中央地面出现绿色圆圈。
|
||||
5. 关卡暂停时,展示提示文案:
|
||||
|
||||
```text
|
||||
小朋友,要注意安全哦
|
||||
```
|
||||
|
||||
6. 用户回到中心绿色圆圈并保持 2 秒后,游戏内容继续。
|
||||
|
||||
## 14. 不确定项与补充确认
|
||||
|
||||
当前需求已明确本文所需的热身关开发规格。
|
||||
|
||||
以下内容作为待决策事项保留,后续硬件、摄像头和正式关卡设计稳定后再补充:
|
||||
|
||||
1. 具体接入的动作识别 SDK、硬件接口和摄像头接口。
|
||||
2. 无硬件、摄像头拒绝授权、多人入镜、识别不到用户、跟踪丢失等异常流程。
|
||||
3. 角色剪影、圆环、虚影提醒、特效、手势引导动画的正式资源文件命名。
|
||||
4. 绿色圆环、角色剪影、安全边界在线性空间或屏幕坐标中的正式计算公式。
|
||||
5. 正式关卡选择页与后续游戏关卡的具体页面结构。
|
||||
|
||||
## 15. 第 3 项本地 Demo 落地记录
|
||||
|
||||
本地浏览器 Demo 入口已落在:
|
||||
|
||||
```text
|
||||
/child-motion-demo
|
||||
```
|
||||
|
||||
当前实现范围:
|
||||
|
||||
1. `src/ChildMotionDemoApp.tsx` 挂载独立 Demo 应用壳。
|
||||
2. `src/components/child-motion-demo/childMotionWarmupModel.ts` 维护热身步骤、圆环目标、2 秒保持判定、热身校准记录和当前运行时会话完成标记。
|
||||
3. `src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 实现横屏舞台、背景虚化占位层、角色剪影、绿色圆环、手势引导、热身记录面板、热身完成后的“开始游戏”按钮,并复用宝贝识物运行态进入首关本地 Demo。
|
||||
4. `src/services/child-motion-demo/childMotionDebugInput.ts` 保留开发者调试输入适配层,后续可被正式动作识别 SDK 适配层替换或并行接入。
|
||||
5. `src/routing/appRoutes.tsx` 新增 `/child-motion-demo` 独立路由,并复用 `VITE_ENABLE_EDUTAINMENT_ENTRY` 开关;开关关闭时不允许通过该直达路径进入 Demo。
|
||||
|
||||
当前调试输入:
|
||||
|
||||
1. `A` 键映射用户向左移动,松开后回到中心。
|
||||
2. `D` 键映射用户向右移动,松开后回到中心。
|
||||
3. 鼠标左键按下并拖动映射左手轨迹。
|
||||
4. 鼠标右键按下并拖动映射右手轨迹。
|
||||
5. 空格键映射原地跳跃。
|
||||
|
||||
当前硬件和动作检测接口接入:
|
||||
|
||||
1. 浏览器摄像头视频流已接入舞台背景。
|
||||
2. 热身关全流程已通过 `src/services/useMocapInput.ts` 接入本地 mocap WebSocket `/stream`;动作数据源状态优先于浏览器背景摄像头状态展示。
|
||||
3. mocap 包支持从 `general.body.center_norm` 读取身体中心,位置类步骤使用该身体中心更新角色剪影横向位置并完成圆环保持检测。
|
||||
4. mocap 包支持从 `actions/action/gesture/gestures/event/name/type` 读取动作名,并支持 `hands[]`、`leftHand/rightHand`、`left_hand/right_hand` 读取左右手坐标。
|
||||
5. `hands[].landmarks` 存在时优先用手腕和 MCP 点计算掌心中心;掌心点不足时退回 wrist landmark,再退回 hand 直出坐标。
|
||||
6. `wave_greeting` 可由 `wave/wave_greeting/hand_wave/open_palm` 等动作或 open palm 手势完成。
|
||||
7. `wave_left_hand` 和 `wave_right_hand` 优先消费对应左右手动作名;当硬件只持续输出手部坐标时,也可以根据连续手部横向轨迹完成挥手检测。
|
||||
8. `jump_once` 消费 `jump/jump_once/hop` 等跳跃动作事件完成。
|
||||
9. 键盘 `A/D/Space` 与鼠标左右键拖拽仍保留为本地 Demo 调试兜底,不代表正式硬件口径。
|
||||
|
||||
当前未接入但已保留边界:
|
||||
|
||||
1. 正式语音播报接口暂不接入,当前先展示热身文案。
|
||||
2. 后续关卡安全边界暂停逻辑暂未落地,当前只完成热身记录和宝贝识物首关本地 Demo 衔接。
|
||||
|
||||
## 16. 当前视觉资产与生图口径补充
|
||||
|
||||
儿童动作 Demo 的视觉口径已经统一收敛到绘本风格草地舞台:
|
||||
|
||||
1. 舞台主环境采用卡通绘本风格、明亮草地、天空、小山坡和树木的组合,默认背景环境需要保证中心与下方前景留空,便于角色轮廓和地面指示环叠加。
|
||||
2. 该卡通绘本草地风格是儿童动作 Demo 后续场景、物品、UI 资源的全局风格要求;新增资源不得切回暗色科技风、真实照片风或后台面板风。
|
||||
3. `src/index.css` 中的热身舞台、摄像头背景层、地面、角色轮廓、地面圆环、开始按钮和横屏提示均按绘本草地风格接入真实资源;资源加载失败时保留 CSS 兜底。
|
||||
4. 生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 触发;脚本使用 `gpt-image-2-all` 调用 VectorEngine `POST /v1/images/generations`,透明资源先生成品红底源图,再在本地移除色键,源图写入 `tmp/child-motion-demo-assets/`。
|
||||
5. 当前已生成并接入以下正式 Demo 资源:
|
||||
- `public/child-motion-demo/picture-book-grass-stage.png`:默认草地舞台背景。
|
||||
- `public/child-motion-demo/picture-book-foreground-grass-v2.png`:底部前景草坪条,只覆盖舞台下沿,不作为整块地板拉伸。
|
||||
- `public/child-motion-demo/picture-book-ground-ring-v2.png`:已按透视绘制的地面椭圆指示环,CSS 只等比缩放。
|
||||
- `public/child-motion-demo/picture-book-character-outline-v2.png`:半透明用户角色轮廓,使用独立去背后处理避免内部填充被误删。
|
||||
- `public/child-motion-demo/picture-book-hud-strip-v2.png`:顶部 HUD 细长软纸条。
|
||||
- `public/child-motion-demo/picture-book-calibration-strip-v2.png`:右下角五格热身状态条。
|
||||
- `public/child-motion-demo/picture-book-start-panel-v2.png`:开始按钮背后的轻盈托盘。
|
||||
- `public/child-motion-demo/picture-book-ui-button-v2.png`:开始按钮绘本风按钮底图。
|
||||
6. v2 资源按最终用途拆分,CSS 必须按资源原始比例、`aspect-ratio` 或 `background-size: contain / auto` 等方式等比使用;禁止把方形面板强行拉伸为 HUD、状态条或地板,也禁止把底部草坪扩展成覆盖角色脚下的大色块。
|
||||
7. 若后续补充或重绘资源,应先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt 和输出路径,再使用 `--live --only <asset-id>` 小批量生成;仅调整透明去背、裁切、画布归一或品红边缘时,可用 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only <asset-id>` 复用 `tmp/child-motion-demo-assets/` 中的源图,不额外请求 image-2;不得把 `VECTOR_ENGINE_API_KEY`、源图或中间预览图提交到仓库。
|
||||
|
||||
已执行的定向验证命令:
|
||||
|
||||
```bash
|
||||
npx eslint src/components/child-motion-demo/ChildMotionWarmupDemo.tsx src/components/child-motion-demo/childMotionWarmupModel.ts src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/components/child-motion-demo/childMotionWarmupModel.test.ts src/services/child-motion-demo/childMotionDebugInput.ts src/services/child-motion-demo/childMotionDebugInput.test.ts src/services/child-motion-demo/index.ts src/ChildMotionDemoApp.tsx src/routing/appRoutes.tsx src/routing/appRoutes.test.ts --ext .ts,.tsx --max-warnings 0
|
||||
npx vitest run src/components/child-motion-demo/childMotionWarmupModel.test.ts src/components/child-motion-demo/ChildMotionWarmupDemo.test.tsx src/services/child-motion-demo/childMotionDebugInput.test.ts src/routing/appRoutes.test.ts
|
||||
npm run check:encoding
|
||||
```
|
||||
@@ -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` 与编码检查通过。
|
||||
@@ -0,0 +1,113 @@
|
||||
# 创作入口配置数据库化与 Runtime 缺失作品返回首页
|
||||
|
||||
日期:2026-05-10
|
||||
|
||||
## 背景
|
||||
|
||||
前端创作中心原本把新建作品入口配置保存在前端代码中,导致入口是否展示、是否开放、卡片文案和 api-server 路由可用性无法使用同一份事实源控制。
|
||||
|
||||
同时,用户直接访问 `/runtime/<玩法>?work=<作品号>` 时,如果作品不存在,运行态会先弹出错误提示;但弹窗关闭后仍停留在无运行数据的页面,容易出现空白页。
|
||||
|
||||
## 设计结论
|
||||
|
||||
1. 创作入口配置事实源迁入 SpacetimeDB。
|
||||
2. 前端只通过 `GET /api/creation-entry/config` 读取配置并派生展示卡片。
|
||||
3. api-server 使用同一份配置对相关运行时路由做熔断。
|
||||
4. 直接 runtime 深链找不到作品时,弹窗确认后回到首页 `/`,避免停留在空白运行态。
|
||||
|
||||
## 数据模型
|
||||
|
||||
SpacetimeDB 新增两张表:
|
||||
|
||||
- `creation_entry_config`
|
||||
- 全局配置头,保存创作入口主卡片和类型选择弹窗文案。
|
||||
- `creation_entry_type_config`
|
||||
- 每个玩法入口的展示与开关配置。
|
||||
- 关键字段:
|
||||
- `id`
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `badge`
|
||||
- `image_src`
|
||||
- `visible`
|
||||
- `open`
|
||||
- `sort_order`
|
||||
|
||||
其中:
|
||||
|
||||
- `visible=false`:前端隐藏入口。
|
||||
- `open=false`:前端展示为锁定/暂不可创建,api-server 据此熔断对应玩法 API;只隐藏创作页入口但保留既有作品链路时不要关闭 `open`。
|
||||
- `sort_order`:数据库排序字段,前端只做可见/锁定分组派生。
|
||||
|
||||
## API
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
GET /api/creation-entry/config
|
||||
```
|
||||
|
||||
返回 shared-contracts 中的 `CreationEntryConfigResponse`:
|
||||
|
||||
```json
|
||||
{
|
||||
"startCard": {
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"idleBadge": "...",
|
||||
"busyBadge": "..."
|
||||
},
|
||||
"typeModal": {
|
||||
"title": "...",
|
||||
"description": "..."
|
||||
},
|
||||
"creationTypes": [
|
||||
{
|
||||
"id": "puzzle",
|
||||
"title": "拼图",
|
||||
"subtitle": "拼图关卡创作",
|
||||
"badge": "可创建",
|
||||
"imageSrc": "/creation-type-references/puzzle.webp",
|
||||
"visible": true,
|
||||
"open": true,
|
||||
"sortOrder": 30,
|
||||
"updatedAtMicros": 0
|
||||
}
|
||||
],
|
||||
"updatedAtMicros": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 前端约束
|
||||
|
||||
- 禁止再新增 `src/config/newWorkEntryConfig.ts` 这类入口事实源。
|
||||
- 创作入口 UI 使用 `src/services/creationEntryConfigService.ts` 拉取后端配置。
|
||||
- `src/components/platform-entry/platformEntryCreationTypes.ts` 只保留展示派生:
|
||||
- `visible -> hidden`
|
||||
- `open -> locked`
|
||||
- `sortOrder -> 初始顺序`
|
||||
- 缺失作品的 runtime 深链恢复策略放在 `src/routing/runtimeNotFoundRecovery.ts`。
|
||||
|
||||
## Runtime 缺失作品恢复
|
||||
|
||||
当路径是以下任意 runtime 深链,并且作品读取/启动返回 404 或 NOT_FOUND:
|
||||
|
||||
- `/runtime/puzzle`
|
||||
- `/runtime/match3d`
|
||||
- `/runtime/big-fish`
|
||||
- `/runtime/square-hole`
|
||||
- `/runtime/visual-novel`
|
||||
|
||||
前端执行:
|
||||
|
||||
1. 清理当前运行态选择和错误状态。
|
||||
2. 弹出“作品不存在或已下架,将返回首页。”。
|
||||
3. 跳转首页 `/`。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/routing/runtimeNotFoundRecovery.test.ts
|
||||
npm run typecheck
|
||||
cd server-rs && cargo check -p api-server -p spacetime-module --no-default-features
|
||||
```
|
||||
@@ -0,0 +1,950 @@
|
||||
# 创意互动内容生成 Agent 技术方案 2026-05-05
|
||||
|
||||
## 1. 目标
|
||||
|
||||
构建一个基于 LangChain-Rust 的创意互动内容生成 Agent。用户输入文字、图片、文档或混合素材后,Agent 不通过规则分类硬选玩法,而是以模型为核心完成理解、判断、规划和执行:先理解用户真正想表达的创作意图,再选择当前可用的互动内容模板,最后调用拼图模块工具把内容填入草稿契约中。
|
||||
|
||||
当前版本只支持拼图模板。RPG 世界、大鱼吃小鱼、抓大鹅 Match3D、方洞挑战等模板必须在 Agent 可见能力中标记为 `unsupported`,不能创建这些玩法的目标 session。即便只有拼图可用,Agent 仍必须先展示多个拼图模板候选,用户选择某个模板后,再确认该模板下的关卡模式、关卡数和预计积分范围,确认后才进入草稿生成。
|
||||
|
||||
本方案不再把 Agent 设计成“规则路由 workflow”。规则只作为安全护栏、契约校验和成本控制。真正的模板选择、素材理解、草稿构思、行动顺序和补问判断由模型通过工具调用和反思循环完成。
|
||||
|
||||
## 2. LangChain-Rust 选型依据
|
||||
|
||||
当前可参考 `langchainrust` crate 作为 Rust 侧 Agent 编排底座。官方 crate 页面显示 `0.2.18` 支持 Agents、Tools、Memory、Chains、RAG、BM25、Hybrid Retrieval、LangGraph、Typed/JSON output parser、Function Calling、Callbacks 等能力;其中 Agent 层包括 ReActAgent、FunctionCallingAgent、AgentExecutor 和 LangGraph,Memory 层包括 Buffer、Window、Summary、SummaryBuffer、Persistent。
|
||||
|
||||
落地时建议先以 `langchainrust = "0.2.18"` 做隔离性 PoC,不直接替换现有 `platform-llm`。若 crate API 与 docs.rs 最新构建存在差异,以源码和本地编译结果为准,先封装一层 `platform-agent` adapter,再接入 `api-server`。
|
||||
|
||||
参考:
|
||||
|
||||
- `langchainrust` crates.io/docs.rs 页面:https://docs.rs/crate/langchainrust/latest
|
||||
- 仓库主页:https://github.com/atliliw/langchainrust
|
||||
|
||||
## 2.1 Agent 模型与多模态输入
|
||||
|
||||
创意互动内容 Agent 的感知、思考、反思和自然语言草稿修订统一使用 APIMart OpenAI 兼容 Responses API 的 `gpt-5`。这里的 `gpt-5` 负责理解用户文字和图片、选择拼图模板、规划草稿字段和生成结构化工具调用参数;拼图图片生成仍由拼图模块图片工具使用 `gpt-image-2`,不要把“理解/规划模型”和“生图模型”混在同一个配置里。
|
||||
|
||||
请求协议以 APIMart 文档 `OpenAI 多模态响应接口` 为准:
|
||||
|
||||
```text
|
||||
POST https://api.apimart.ai/v1/responses
|
||||
model: gpt-5
|
||||
official_fallback: true
|
||||
input[].content[].type: input_text | input_image
|
||||
```
|
||||
|
||||
Phase 1 的 `platform-agent` / `platform-llm` 必须支持下面的项目内请求结构:
|
||||
|
||||
```ts
|
||||
export interface CreativeAgentMultimodalInputPart {
|
||||
type: 'input_text' | 'input_image';
|
||||
text?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface CreativeAgentGpt5Request {
|
||||
model: 'gpt-5';
|
||||
official_fallback: true;
|
||||
input: Array<{
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: CreativeAgentMultimodalInputPart[];
|
||||
}>;
|
||||
stream: boolean;
|
||||
tools?: CreativeAgentToolSchema[];
|
||||
}
|
||||
```
|
||||
|
||||
落地约束:
|
||||
|
||||
1. Agent 入口支持文本 + 图片多模态输入,首版至少支持 1 张图片,协议层预留多图。
|
||||
2. 图片必须先进入资产系统,Agent 请求使用可访问的 `readUrl` 或受控 Data URI;SpacetimeDB 不保存大图 base64。
|
||||
3. `platform-llm` 当前已有 Responses 协议骨架,但 Phase 1 需要把 content part 从纯文本扩展成 `input_text` / `input_image` 两类;APIMart GPT-5 client 必须显式开启 `official_fallback = true`,该供应商字段不默认扩散到 Ark 等非 APIMart provider。
|
||||
4. `CREATION_TEMPLATE_LLM_MODEL` 等旧文本创作模型不能作为创意互动内容 Agent 的默认模型;本 Agent 必须显式使用 `gpt-5`。
|
||||
5. 如果 LangChain-Rust adapter 暂时无法直接表达多模态 Responses 请求,应在 `platform-agent` 内桥接到 `platform-llm` 的多模态 Responses client,不能退回纯文本摘要替代图片理解。
|
||||
6. 模型工具调用可用 APIMart Responses 的 `tools` 能力承载;工具真正执行仍由 `platform-agent` 注册表和后端 typed Tool 控制。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
Agent 由六大核心模块组成,形成“感知 -> 思考 -> 记忆 -> 行动 -> 反思 -> 协作”的闭环。
|
||||
|
||||
```text
|
||||
用户图文输入
|
||||
-> 感知 Perception
|
||||
-> 思考 Reasoning
|
||||
-> 记忆 Memory
|
||||
-> 行动 Action
|
||||
-> 反思 Reflection
|
||||
-> 协作 Collaboration
|
||||
-> 目标玩法草稿 / 追问 / 人工确认
|
||||
```
|
||||
|
||||
Rust 分层建议:
|
||||
|
||||
```text
|
||||
server-rs/crates/platform-agent
|
||||
langchain_adapter.rs LangChain-Rust 封装,屏蔽第三方 API 变化
|
||||
agent_graph.rs 六模块 LangGraph / AgentExecutor 编排
|
||||
tools.rs 工具注册与权限边界
|
||||
output_parsers.rs Typed / JSON 输出解析
|
||||
callbacks.rs Trace、SSE、成本与错误事件
|
||||
|
||||
server-rs/crates/module-creative-agent
|
||||
domain.rs Agent 会话、目标、模板语义、计划、反思记录
|
||||
commands.rs 创建会话、写入输入、确认计划、保存结果
|
||||
application.rs 纯领域校验、阶段迁移、契约门槛
|
||||
errors.rs 字段错误与领域错误
|
||||
|
||||
server-rs/crates/api-server/src/creative_agent.rs
|
||||
HTTP / SSE facade,调用 platform-agent 和 spacetime-client
|
||||
```
|
||||
|
||||
DDD 边界保持不变:
|
||||
|
||||
- `platform-agent` 负责 Agent 编排和工具调用抽象,不保存业务真相。
|
||||
- `module-creative-agent` 只放纯领域类型、阶段、校验和决策记录结构。
|
||||
- `api-server` 负责鉴权、SSE、LLM/视觉/工具编排。
|
||||
- `spacetime-module` 保存会话、输入、记忆索引、目标玩法绑定和审计事件。
|
||||
- 当前目标玩法只允许拼图。拼图模板协议、积分范围、单关卡/多关卡图片生成计划、草稿校验和工具实现全部封装在 `module-puzzle` / 拼图相关 facade 中;通用 Agent 不复制拼图字段推导逻辑。
|
||||
|
||||
## 4. 六大核心模块
|
||||
|
||||
### 4.1 感知模块 Perception
|
||||
|
||||
职责:把用户输入的字、图、文档和上下文变成模型可推理的多模态语义状态。
|
||||
|
||||
它不是关键词分类器。它要做的是“看懂素材”和“看懂用户想要什么”。
|
||||
|
||||
输入:
|
||||
|
||||
```ts
|
||||
export interface CreativePerceptionInput {
|
||||
text: string;
|
||||
documents: CreativeTextAttachment[];
|
||||
images: CreativeImageAttachment[];
|
||||
currentUserProfile?: CreativeUserPreferenceSnapshot | null;
|
||||
entryContext: 'creation_home' | 'puzzle_workspace' | 'gallery_remix' | 'draft_restore';
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```ts
|
||||
export interface CreativePerceptionState {
|
||||
userIntent: string;
|
||||
emotionalTone: string;
|
||||
targetAudience: string | null;
|
||||
sourceMaterials: CreativeMaterialSummary[];
|
||||
visualUnderstanding: CreativeImageUnderstanding[];
|
||||
constraints: CreativeConstraint[];
|
||||
uncertainties: CreativeUncertainty[];
|
||||
}
|
||||
```
|
||||
|
||||
实现方式:
|
||||
|
||||
1. 文档输入复用 `creationAgentDocumentInput`,再交给 LangChain-Rust 的 text splitter / document chain 做摘要。
|
||||
2. 图片输入必须先上传为资产,Agent 只拿 `readUrl`、缩略图、尺寸和视觉摘要,不把大 data URL 存入 SpacetimeDB。
|
||||
3. 图像理解首版直接通过 APIMart Responses API 的 `gpt-5` 多模态输入完成,返回主体、场景、风格、OCR、构图线索和安全风险;后续如独立 `platform-vision`,也必须保持相同的文本/图像内容块契约。
|
||||
4. 模板和玩法说明通过 RAG 检索注入,而不是写死规则。检索源包括玩法模板注册表、拼图草稿契约、已有优秀作品摘要和玩法适配说明。
|
||||
|
||||
LangChain-Rust 对应能力:
|
||||
|
||||
- Document loader / splitter / summarization chain
|
||||
- RAG、BM25、Hybrid retrieval
|
||||
- Typed / JSON output parser
|
||||
- Callback 将感知进度推给前端 SSE
|
||||
|
||||
### 4.2 思考模块 Reasoning
|
||||
|
||||
职责:让模型在可用工具、用户意图和玩法知识之间主动做计划。
|
||||
|
||||
思考模块不是 `if playType == puzzle` 的路由函数,而是一个 FunctionCallingAgent 或 LangGraph 中的 planner 节点。模型可以自主选择:
|
||||
|
||||
- 先调用拼图模板知识检索
|
||||
- 先返回拼图模板目录
|
||||
- 请求用户选择模板
|
||||
- 用户选中模板后,再确认该模板的关卡模式、关卡数和积分范围
|
||||
- 确认后生成单关卡或多关卡草稿计划
|
||||
- 先调用图片理解
|
||||
- 先问用户一个关键问题
|
||||
- 委托拼图专家 Agent
|
||||
|
||||
当前版本的思考模块不能选择非拼图玩法作为行动目标。如果模型认为输入更适合 RPG、Match3D、大鱼或方洞挑战,应输出“当前仅支持拼图模板”的说明,并尝试给出可转化为拼图的创意方案;若无法转化,应进入 `waiting_user`,不能创建非拼图 session。
|
||||
|
||||
核心状态:
|
||||
|
||||
```ts
|
||||
export interface CreativeReasoningState {
|
||||
goal: string;
|
||||
candidatePlayTypes: CreativePlayCandidate[];
|
||||
selectedPlayType: CreativePlayType | null;
|
||||
selectedTemplateId: string | null;
|
||||
selectedPuzzleTemplate: PuzzleCreativeTemplateSelection | null;
|
||||
selectedImageGenerationPlan: PuzzleImageGenerationPlan | null;
|
||||
plan: CreativeAgentPlanStep[];
|
||||
confidence: number;
|
||||
needUserClarification: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
```
|
||||
|
||||
建议使用 LangChain-Rust:
|
||||
|
||||
- `FunctionCallingAgent` 作为主决策 Agent,所有玩法能力以工具形式暴露。
|
||||
- `AgentExecutor` 控制最大迭代次数、超时、工具调用错误回传。
|
||||
- `LangGraph` 表达长链路:`perceive -> plan -> act -> reflect -> finalize`,允许循环和人工确认。
|
||||
- `JsonOutputParser` / `TypedOutputParser` 保证模型最终输出能落到 Rust/TS shared contract。
|
||||
|
||||
系统提示词要明确:
|
||||
|
||||
1. 你是创意互动内容生成 Agent,不是分类器。
|
||||
2. 你可以相信自己的多模态理解能力。
|
||||
3. 你应选择能最大化互动体验的玩法,而不是机械匹配关键词。
|
||||
4. 当前产品只开放拼图模板,非拼图模板只能解释为暂不支持,不能调用非拼图工具。
|
||||
5. 即便只有拼图玩法可用,也必须先显式展示多个拼图子模板;用户选中模板后,才展示选择理由、关卡配置和预计积分范围。
|
||||
6. 当输入足够明确时不要过度追问。
|
||||
7. 当合规、素材权属、人物肖像或儿童内容存在风险时进入确认或降级。
|
||||
|
||||
### 4.3 记忆模块 Memory
|
||||
|
||||
职责:让 Agent 能利用当前会话、用户偏好、历史作品和反思经验,而不是每次从零判断。
|
||||
|
||||
记忆分四层:
|
||||
|
||||
```text
|
||||
短期记忆:当前会话消息、工具调用、草稿状态
|
||||
工作记忆:本次任务的目标、计划、候选、未解决问题
|
||||
长期记忆:用户偏好、常用玩法、作品风格、发布反馈
|
||||
反思记忆:过去失败原因、模板误选案例、修正策略
|
||||
```
|
||||
|
||||
推荐结构:
|
||||
|
||||
```ts
|
||||
export interface CreativeAgentMemorySnapshot {
|
||||
shortTermSummary: string;
|
||||
workingPlan: CreativeAgentPlanStep[];
|
||||
retrievedUserPreferences: CreativeUserPreference[];
|
||||
retrievedTemplateMemories: CreativeTemplateMemory[];
|
||||
retrievedReflections: CreativeReflectionMemory[];
|
||||
}
|
||||
```
|
||||
|
||||
落地方式:
|
||||
|
||||
1. LangChain-Rust Memory 用于单次 AgentExecutor 内的短期上下文,可用 Buffer / Window / SummaryBuffer。
|
||||
2. SpacetimeDB 保存长期真相:`creative_agent_session`、`creative_agent_message`、`creative_agent_reflection`、`creative_agent_target_binding`。
|
||||
3. 向量/混合检索保存可召回记忆:用户偏好、模板选择结果、发布后反馈、失败反思。首版可用 SQLite 或 Redis feature,生产再评估 Qdrant/Redis。
|
||||
4. 每次生成结束后写一条反思记忆:选择了什么模板、为什么、哪些字段由模型推断、用户是否接受、是否返回编辑。
|
||||
|
||||
记忆使用原则:
|
||||
|
||||
- 记忆给模型参考,不替代用户本轮输入。
|
||||
- 用户本轮明确要求优先级最高。
|
||||
- 任何长期记忆都要带来源、时间和置信度。
|
||||
- 涉及个人隐私、图片内容和未发布作品时只在用户私有 namespace 检索。
|
||||
|
||||
### 4.4 行动模块 Action
|
||||
|
||||
职责:把 Agent 的计划变成可审计、可回滚、可测试的工具调用。
|
||||
|
||||
所有对系统产生影响的操作都必须以 LangChain-Rust Tool 的形式注册。模型通过 function calling 选择工具,工具内部再调用现有服务或 SpacetimeDB facade。
|
||||
|
||||
首批工具:
|
||||
|
||||
```text
|
||||
perceive_image(imageAssetId)
|
||||
retrieve_puzzle_template_catalog(query)
|
||||
retrieve_user_creation_memory(userId, query)
|
||||
create_puzzle_agent_session(payload)
|
||||
compile_puzzle_draft(sessionId, payload)
|
||||
plan_puzzle_level_images(payload)
|
||||
generate_puzzle_level_images(sessionId, payload)
|
||||
select_puzzle_template(payload)
|
||||
confirm_puzzle_template(payload)
|
||||
apply_puzzle_draft_natural_language_edit(sessionId, payload)
|
||||
start_puzzle_draft_test_run(sessionId, payload)
|
||||
ask_user_clarification(question, options?)
|
||||
request_user_confirmation(summary, candidates)
|
||||
validate_target_draft(playType, draft)
|
||||
save_creative_reflection(payload)
|
||||
```
|
||||
|
||||
工具设计要求:
|
||||
|
||||
1. 工具描述要清楚告诉模型何时使用,而不是由外层规则决定。
|
||||
2. 工具输入必须是 JSON Schema / Rust typed struct,禁止自由字符串拼接。
|
||||
3. 工具只做一件事。比如“创建拼图 session”和“编译拼图草稿”分开。
|
||||
4. 工具返回结构化结果,包含 `ok`、`summary`、`nextSuggestedTools`、`warnings`。
|
||||
5. 所有写操作必须鉴权,不能信任模型传入的 `ownerUserId`。
|
||||
6. 工具调用审计写入 SpacetimeDB,便于排障和反思。
|
||||
7. 当前工具注册表只能暴露拼图工具。非拼图工具即使已有实现,也不能注册给当前 Agent。
|
||||
|
||||
拼图草稿不是路由器手填,而是 Action 模块通过工具让 Agent 产出 typed payload。这里的“草稿字段”只指用户表单和 Agent 自然语言都共同编辑的那一组字段:
|
||||
|
||||
```ts
|
||||
export interface CreativePuzzleDraftToolInput {
|
||||
templateId: string;
|
||||
templateCostRange: PuzzleTemplateCostRange;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
workTags: string[];
|
||||
levels: CreativePuzzleLevelDraftInput[];
|
||||
}
|
||||
|
||||
export interface CreativePuzzleLevelDraftInput {
|
||||
levelName: string;
|
||||
pictureDescription: string;
|
||||
pictureReference?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
工具内部负责映射到现有 `PuzzleAgentActionRequest` 和 `PuzzleResultDraft`,并调用拼图领域校验。`workTitle`、`workDescription`、`workTags`、`levels[].levelName`、`levels[].pictureDescription`、`levels[].pictureReference` 是 Agent 直接写入的草稿真相;`summary`、`anchorPack`、`forbiddenDirectives`、`imagePrompt`、候选图等都属于拼图模块内部派生或生成结果,不作为 Agent 的直接填表目标。
|
||||
|
||||
拼图模块必须额外暴露模板和多关卡图片协议:
|
||||
|
||||
```ts
|
||||
export interface PuzzleCreativeTemplateProtocol {
|
||||
templateId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
supportedLevelMode: 'single' | 'multi' | 'single_or_multi';
|
||||
defaultLevelCount: number;
|
||||
minLevelCount: number;
|
||||
maxLevelCount: number;
|
||||
costRange: PuzzleTemplateCostRange;
|
||||
requiredDraftFields: string[];
|
||||
imageGenerationPolicy: PuzzleTemplateImageGenerationPolicy;
|
||||
}
|
||||
|
||||
export interface PuzzleTemplateCostRange {
|
||||
minPoints: number;
|
||||
maxPoints: number;
|
||||
pricingUnit: 'point';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface PuzzleTemplateImageGenerationPolicy {
|
||||
allowUploadedImageDirectly: boolean;
|
||||
allowGeneratedImages: boolean;
|
||||
allowPerLevelReferenceImage: boolean;
|
||||
defaultCandidateCountPerLevel: number;
|
||||
}
|
||||
|
||||
export interface PuzzleImageGenerationPlan {
|
||||
mode: 'single_level' | 'multi_level';
|
||||
levels: CreativePuzzleLevelDraftInput[];
|
||||
estimatedCostRange: PuzzleTemplateCostRange;
|
||||
}
|
||||
```
|
||||
|
||||
积分范围由拼图模板协议提供,Agent 只能解释和选择,不能自行发明价格。真实扣费仍以后端钱包/任务系统最终结算为准。
|
||||
|
||||
### 4.5 反思模块 Reflection
|
||||
|
||||
职责:让 Agent 在交付前检查自己的选择是否真的适合用户,而不是一次模型输出就结束。
|
||||
|
||||
反思节点运行在每次关键行动后:
|
||||
|
||||
1. 模板选择后:检查是否已经向用户显式展示拼图模板和积分范围。
|
||||
2. 用户确认前:检查是否误承诺了非拼图模板或非真实价格。
|
||||
3. 草稿填充后:检查字段是否完整、玩法体验是否成立。
|
||||
4. 图片使用前:检查单关卡/多关卡图片计划是否与模板协议一致。
|
||||
5. 最终交付前:检查是否需要用户确认。
|
||||
|
||||
反思输出:
|
||||
|
||||
```ts
|
||||
export interface CreativeReflectionReport {
|
||||
pass: boolean;
|
||||
score: number;
|
||||
issues: CreativeReflectionIssue[];
|
||||
revisionInstruction?: string | null;
|
||||
shouldAskUser: boolean;
|
||||
shouldTryAlternativePlayType: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
实现方式:
|
||||
|
||||
- 用 LangGraph 增加 `reflect` 节点。
|
||||
- 反思模型拿到感知状态、计划、工具调用结果和目标草稿。
|
||||
- 如果 `pass=false` 且迭代次数未超限,回到 `plan` 或 `act`。
|
||||
- 如果问题是用户偏好缺失,调用 `ask_user_clarification`。
|
||||
- 如果问题是契约字段缺失,调用目标玩法 draft 修复工具。
|
||||
- 如果问题是未展示模板选择或积分范围,回到模板确认节点。
|
||||
- 如果问题是多关卡计划超出模板 `maxLevelCount`,调用拼图计划修复工具。
|
||||
|
||||
硬性终止条件:
|
||||
|
||||
- 最大反思循环 2 次。
|
||||
- 同一工具同一参数失败 2 次后停止并返回可读错误。
|
||||
- 预算超限时返回当前可用草稿和补救建议。
|
||||
|
||||
反思记忆要沉淀:
|
||||
|
||||
- 模板误选原因。
|
||||
- 用户是否接受模板积分范围。
|
||||
- 单关卡或多关卡图片生成计划是否被用户调整。
|
||||
- 用户手动改选的玩法。
|
||||
- 结果页返回编辑最多的字段。
|
||||
- 发布失败 blockers。
|
||||
|
||||
### 4.6 协作模块 Collaboration
|
||||
|
||||
职责:把复杂创意任务拆给多个专长 Agent,而不是让单个提示词吞掉所有任务。
|
||||
|
||||
当前首版只开放拼图协作,不开放其它玩法子 Agent。建议四个子 Agent:
|
||||
|
||||
```text
|
||||
创意导演 Agent:理解用户目标,决定整体方向和互动体验。
|
||||
视觉解读 Agent:理解图片、构图、主体、风格和可交互线索。
|
||||
拼图模板策展 Agent:基于拼图模板协议和历史作品选择候选拼图模板,并读取积分范围。
|
||||
拼图专家 Agent:生成单关卡或多关卡拼图草稿 payload 和图片生成计划。
|
||||
契约审校 Agent:检查字段、发布门槛、积分展示、安全边界和可恢复性。
|
||||
```
|
||||
|
||||
LangChain-Rust 落地:
|
||||
|
||||
- 用 LangGraph 的 subgraph 表达子 Agent。
|
||||
- 子 Agent 共享 `CreativeAgentState`,但只能写自己负责的字段。
|
||||
- 主 Agent 通过 handoff 工具委托子任务。
|
||||
- 必要时并行执行视觉解读和拼图模板知识检索,再由创意导演合并。
|
||||
- 协作结果由契约审校 Agent 最终检查。
|
||||
|
||||
协作不是增加 UI 复杂度。前端仍只看到一个 Agent,但后端内部有多个可观测步骤,SSE 推送简短阶段即可。
|
||||
|
||||
## 5. Agent 状态机
|
||||
|
||||
LangGraph 状态建议:
|
||||
|
||||
```rust
|
||||
pub struct CreativeAgentGraphState {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub perception: Option<CreativePerceptionState>,
|
||||
pub memory: Option<CreativeAgentMemorySnapshot>,
|
||||
pub reasoning: Option<CreativeReasoningState>,
|
||||
pub tool_results: Vec<CreativeToolResult>,
|
||||
pub reflection: Option<CreativeReflectionReport>,
|
||||
pub target_binding: Option<CreativeTargetSessionBinding>,
|
||||
pub final_response: Option<CreativeAgentFinalResponse>,
|
||||
pub iteration_count: u32,
|
||||
}
|
||||
```
|
||||
|
||||
Graph 节点:
|
||||
|
||||
```text
|
||||
load_memory
|
||||
perceive_input
|
||||
plan_with_agent
|
||||
act_with_tools
|
||||
reflect_result
|
||||
collaborate_if_needed
|
||||
finalize_or_ask_user
|
||||
persist_memory
|
||||
```
|
||||
|
||||
边:
|
||||
|
||||
```text
|
||||
load_memory -> perceive_input
|
||||
perceive_input -> plan_with_agent
|
||||
plan_with_agent -> act_with_tools
|
||||
act_with_tools -> reflect_result
|
||||
reflect_result(pass) -> finalize_or_ask_user
|
||||
reflect_result(revise) -> plan_with_agent
|
||||
reflect_result(collaborate) -> collaborate_if_needed
|
||||
collaborate_if_needed -> act_with_tools
|
||||
finalize_or_ask_user -> persist_memory
|
||||
```
|
||||
|
||||
## 6. 数据契约
|
||||
|
||||
新增 shared contracts:
|
||||
|
||||
```text
|
||||
packages/shared/src/contracts/creativeAgent.ts
|
||||
server-rs/crates/shared-contracts/src/creative_agent.rs
|
||||
```
|
||||
|
||||
核心 DTO:
|
||||
|
||||
```ts
|
||||
export type CreativeAgentStage =
|
||||
| 'perceiving'
|
||||
| 'thinking'
|
||||
| 'remembering'
|
||||
| 'selecting_puzzle_template'
|
||||
| 'waiting_template_confirmation'
|
||||
| 'planning_puzzle_levels'
|
||||
| 'acting'
|
||||
| 'reflecting'
|
||||
| 'collaborating'
|
||||
| 'waiting_user'
|
||||
| 'target_ready'
|
||||
| 'failed';
|
||||
|
||||
export interface CreativeAgentSessionSnapshot {
|
||||
sessionId: string;
|
||||
stage: CreativeAgentStage;
|
||||
perception: CreativePerceptionState | null;
|
||||
reasoning: CreativeReasoningState | null;
|
||||
puzzleTemplateCatalog: PuzzleCreativeTemplateProtocol[];
|
||||
puzzleTemplateSelection: PuzzleCreativeTemplateSelection | null;
|
||||
puzzleImageGenerationPlan: PuzzleImageGenerationPlan | null;
|
||||
reflection: CreativeReflectionReport | null;
|
||||
targetBinding: CreativeTargetSessionBinding | null;
|
||||
messages: CreativeAgentMessage[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PuzzleCreativeTemplateSelection {
|
||||
templateId: string;
|
||||
title: string;
|
||||
reason: string;
|
||||
costRange: PuzzleTemplateCostRange;
|
||||
supportedLevelMode: 'single' | 'multi' | 'single_or_multi';
|
||||
selectedLevelMode: 'single_level' | 'multi_level';
|
||||
plannedLevelCount: number;
|
||||
requiresUserConfirmation: true;
|
||||
}
|
||||
```
|
||||
|
||||
HTTP facade:
|
||||
|
||||
```text
|
||||
POST /api/runtime/creative-agent/sessions
|
||||
GET /api/runtime/creative-agent/sessions/{sessionId}
|
||||
POST /api/runtime/creative-agent/sessions/{sessionId}/messages/stream
|
||||
POST /api/runtime/creative-agent/sessions/{sessionId}/confirm
|
||||
POST /api/runtime/creative-agent/sessions/{sessionId}/cancel
|
||||
```
|
||||
|
||||
SSE 事件:
|
||||
|
||||
```text
|
||||
stage
|
||||
agent_message_delta
|
||||
puzzle_template_catalog
|
||||
puzzle_template_selection
|
||||
puzzle_cost_range
|
||||
puzzle_level_plan
|
||||
tool_started
|
||||
tool_completed
|
||||
reflection
|
||||
target_session
|
||||
need_user_input
|
||||
session
|
||||
error
|
||||
```
|
||||
|
||||
## 7. SpacetimeDB 持久化
|
||||
|
||||
新增表:
|
||||
|
||||
```text
|
||||
creative_agent_session
|
||||
creative_agent_message
|
||||
creative_agent_input_asset
|
||||
creative_agent_tool_call
|
||||
creative_agent_reflection
|
||||
creative_agent_target_binding
|
||||
creative_agent_memory_index
|
||||
puzzle_creative_template_snapshot
|
||||
puzzle_creative_level_generation_plan
|
||||
```
|
||||
|
||||
关键约束:
|
||||
|
||||
1. SpacetimeDB 保存结构化真相和 JSON 快照,不保存大图片 data URL。
|
||||
2. 工具调用和反思必须可追溯,便于排查模型为什么选择某个模板。
|
||||
3. `creative_agent_memory_index` 只保存记忆元数据和向量索引引用,不直接承担向量数据库职责。
|
||||
4. 表结构变更必须同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 bindings。
|
||||
5. `creative_agent_session` 必须保存当前拼图模板目录、已确认的模板选择快照和积分范围,保证刷新后仍能恢复“先选模板、再确认配置”的两段式状态。
|
||||
6. `puzzle_creative_level_generation_plan` 保存单关卡/多关卡图片生成计划,包括每关 `level_id`、`level_name`、`picture_description`、`image_prompt`、`generation_status`、`candidate_count` 和 `estimated_cost_points`。
|
||||
7. 非拼图玩法不新增 target binding,避免后续误恢复到暂不支持的玩法。
|
||||
8. 自然语言修订草稿字段要记录工具调用、原始用户指令、结构化 patch 和修改后的 draft 版本,便于撤销、审计和反思。
|
||||
9. 试玩 run 与草稿 session 需要有绑定关系,确保“立即玩”后返回结果页能恢复同一草稿。
|
||||
|
||||
## 8. 拼图首版落地
|
||||
|
||||
拼图首版不是“规则判断为拼图”,而是模型通过工具和反思得出选择。但产品边界明确:当前只支持拼图模板。Agent 必须把非拼图创意转化为拼图可承接的方案,或告诉用户当前不支持该模板。
|
||||
|
||||
### 8.1 强制模板选择与积分展示
|
||||
|
||||
任何草稿生成前都必须先进入 `selecting_puzzle_template` 和 `waiting_template_confirmation`。
|
||||
|
||||
前端至少展示:
|
||||
|
||||
- 拼图模板标题
|
||||
- 模板缩略图或示意图
|
||||
- Agent 选择理由
|
||||
- 支持单关卡、多关卡或二者皆可
|
||||
- 计划关卡数
|
||||
- 预计积分范围,例如 `预计消耗 8 到 18 光点`
|
||||
|
||||
积分范围来自拼图模板协议:
|
||||
|
||||
```ts
|
||||
export interface PuzzleTemplateCostRange {
|
||||
minPoints: number;
|
||||
maxPoints: number;
|
||||
pricingUnit: 'point';
|
||||
reason: string;
|
||||
}
|
||||
```
|
||||
|
||||
Agent 可以解释 `reason`,但不能修改 `minPoints` / `maxPoints`。如果模板协议没有积分范围,该模板不能展示为可选项。
|
||||
|
||||
Agent 可调用工具:
|
||||
|
||||
```text
|
||||
retrieve_puzzle_template_catalog
|
||||
inspect_puzzle_draft_contract
|
||||
select_puzzle_template
|
||||
confirm_puzzle_template
|
||||
plan_puzzle_level_images
|
||||
create_puzzle_agent_session
|
||||
compile_puzzle_draft
|
||||
apply_puzzle_draft_natural_language_edit
|
||||
validate_puzzle_result_preview
|
||||
select_uploaded_image_as_puzzle_cover
|
||||
generate_puzzle_images
|
||||
start_puzzle_draft_test_run
|
||||
return_to_puzzle_result
|
||||
```
|
||||
|
||||
### 8.2 拼图模板协议
|
||||
|
||||
拼图模板协议应封装在拼图模块,不放在通用 Agent:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-puzzle/src/creative_templates.rs
|
||||
server-rs/crates/module-puzzle/src/creative_tools.rs
|
||||
server-rs/crates/shared-contracts/src/puzzle_creative_template.rs
|
||||
packages/shared/src/contracts/puzzleCreativeTemplate.ts
|
||||
```
|
||||
|
||||
协议至少包含:
|
||||
|
||||
```ts
|
||||
export interface PuzzleCreativeTemplateProtocol {
|
||||
templateId: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
previewImageSrc: string | null;
|
||||
supportedLevelMode: 'single' | 'multi' | 'single_or_multi';
|
||||
minLevelCount: number;
|
||||
maxLevelCount: number;
|
||||
defaultLevelCount: number;
|
||||
costRange: PuzzleTemplateCostRange;
|
||||
imagePolicy: PuzzleTemplateImageGenerationPolicy;
|
||||
draftFieldHints: PuzzleTemplateDraftFieldHints;
|
||||
}
|
||||
```
|
||||
|
||||
模板示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"templateId": "puzzle.family-keepsake",
|
||||
"title": "家庭纪念拼图",
|
||||
"supportedLevelMode": "single_or_multi",
|
||||
"minLevelCount": 1,
|
||||
"maxLevelCount": 6,
|
||||
"defaultLevelCount": 3,
|
||||
"costRange": {
|
||||
"minPoints": 8,
|
||||
"maxPoints": 24,
|
||||
"pricingUnit": "point",
|
||||
"reason": "按关卡数、每关候选图数量和是否使用上传图估算"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 单关卡与多关卡图片生成
|
||||
|
||||
拼图草稿必须支持两种计划:
|
||||
|
||||
```ts
|
||||
export type PuzzleLevelGenerationMode = 'single_level' | 'multi_level';
|
||||
|
||||
export interface PuzzleImageGenerationPlan {
|
||||
mode: PuzzleLevelGenerationMode;
|
||||
templateId: string;
|
||||
estimatedCostRange: PuzzleTemplateCostRange;
|
||||
levels: PuzzleImageGenerationPlanLevel[];
|
||||
}
|
||||
|
||||
export interface PuzzleImageGenerationPlanLevel {
|
||||
levelId: string;
|
||||
levelName: string;
|
||||
pictureDescription: string;
|
||||
imagePrompt: string;
|
||||
pictureReference?: string | null;
|
||||
candidateCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
单关卡:
|
||||
|
||||
- `levels.length = 1`
|
||||
- 可使用上传图直出,也可生成一张候选图。
|
||||
- 适合纪念照、商品图、单张海报、单主题知识图。
|
||||
|
||||
多关卡:
|
||||
|
||||
- `levels.length` 必须在模板 `minLevelCount` 到 `maxLevelCount` 之间。
|
||||
- 每关有独立 `levelName`、`pictureDescription` 和 `imagePrompt`。
|
||||
- 可选择每关生成一张图,也可第一关使用上传图、后续关卡生图。
|
||||
- 适合故事型照片组、知识步骤、活动流程、系列商品、节日卡片组。
|
||||
|
||||
生成工具建议:
|
||||
|
||||
```ts
|
||||
export interface GeneratePuzzleLevelImagesToolInput {
|
||||
sessionId: string;
|
||||
plan: PuzzleImageGenerationPlan;
|
||||
imageModel: 'gpt-image-2';
|
||||
}
|
||||
|
||||
export interface GeneratePuzzleLevelImagesToolOutput {
|
||||
draft: PuzzleResultDraft;
|
||||
levels: PuzzleDraftLevel[];
|
||||
costEstimate: PuzzleTemplateCostRange;
|
||||
generatedCount: number;
|
||||
uploadedCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
工具内部可以复用现有 `generate_puzzle_images` action,但必须以 `levelId` 为粒度逐关执行,或新增拼图后端 action `generate_puzzle_level_images` 批量处理。批量 action 仍归属拼图模块,通用 Agent 只负责调用。
|
||||
|
||||
### 8.5 模板草稿字段填充与自然语言修订
|
||||
|
||||
Agent 创作互动内容的本质就是向模板中的草稿字段填充内容。拼图模板草稿字段是唯一创作真相:表单化创作页是这些字段的可视化编辑器,Agent 对话是这些字段的自然语言编辑器,二者属于同一条创作流程。
|
||||
|
||||
通用 Agent 只负责把用户自然语言转成“草稿字段填充 / 修订意图”,真正的字段写入仍通过拼图模块 Tool 完成:
|
||||
|
||||
```ts
|
||||
export interface PuzzleDraftFieldEditInstruction {
|
||||
scope: 'work' | 'level' | 'tags' | 'cover' | 'images' | 'all';
|
||||
operation: 'set' | 'append' | 'replace' | 'remove' | 'reorder';
|
||||
fieldPath: string;
|
||||
value: string | string[] | boolean | number | null;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface ApplyPuzzleDraftNaturalLanguageEditToolInput {
|
||||
sessionId: string;
|
||||
userInstruction: string;
|
||||
currentDraftSnapshot: PuzzleResultDraft;
|
||||
}
|
||||
|
||||
export interface ApplyPuzzleDraftNaturalLanguageEditToolOutput {
|
||||
updatedDraft: PuzzleResultDraft;
|
||||
editInstructions: PuzzleDraftFieldEditInstruction[];
|
||||
needsUserConfirmation: boolean;
|
||||
confirmationSummary: string;
|
||||
}
|
||||
```
|
||||
|
||||
这个工具由拼图模块封装,内部要做三步:
|
||||
|
||||
1. 用模型把自然语言改写成结构化草稿字段 patch。
|
||||
2. 用拼图领域规则校验 patch 是否会破坏草稿约束。
|
||||
3. 通过现有草稿保存接口写回同一份 `PuzzleResultDraft` 或 `formDraft`。
|
||||
|
||||
典型自然语言字段修订场景:
|
||||
|
||||
- “把这张图改成更适合家庭纪念。”
|
||||
- “第二关再多加一张风景图。”
|
||||
- “标题别太正式,轻松一点。”
|
||||
- “主题标签里去掉商品感,增加温暖和节日感。”
|
||||
|
||||
Agent 不能直接篡改结果页 DOM,也不能绕过拼图草稿字段写最终发布数据。用户在表单里手动编辑、用户用自然语言让 Agent 编辑,本质上都必须落到同一份草稿字段 patch。
|
||||
|
||||
### 8.6 立即玩与试玩闭环
|
||||
|
||||
生成好的互动内容必须能立即玩到。对拼图来说,Agent 完成草稿后必须直接导向现有 `puzzle-result`,并提供明确的“立即玩”入口。
|
||||
|
||||
闭环要求:
|
||||
|
||||
1. 草稿创建成功后,结果页首屏就能看到 `Play` 或“立即玩”按钮。
|
||||
2. 点击后直接启动 `puzzle-runtime`,不需要用户再手动跳过别的中间页。
|
||||
3. 如果当前草稿还在生成更多关卡图片,已完成关卡可先试玩,后续图片再逐关补齐。
|
||||
4. 试玩入口必须复用现有 `PuzzleRuntimeShell` 和 `startPuzzleRun` 链路。
|
||||
5. 试玩后返回结果页时,仍然停留在同一草稿上下文,不丢失表单草稿状态。
|
||||
|
||||
结果页到运行态之间的切换不由通用 Agent 再次判断,而是由拼图模块暴露的 `start_run` / `resume_run` / `return_to_result` 工具完成。
|
||||
|
||||
### 8.4 拼图专家输出
|
||||
|
||||
拼图专家 Agent 需要产出:
|
||||
|
||||
```ts
|
||||
export interface PuzzleCreativeDraftIntent {
|
||||
templateSelection: PuzzleCreativeTemplateSelection;
|
||||
imageGenerationPlan: PuzzleImageGenerationPlan;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
workTags: string[];
|
||||
levels: CreativePuzzleLevelDraftInput[];
|
||||
}
|
||||
```
|
||||
|
||||
字段映射仍必须对齐现有契约:
|
||||
|
||||
- `PuzzleResultDraft.workTitle`
|
||||
- `PuzzleResultDraft.workDescription`
|
||||
- `PuzzleResultDraft.workTags`
|
||||
- `PuzzleResultDraft.levels[].levelName`
|
||||
- `PuzzleResultDraft.levels[].pictureDescription`
|
||||
- `PuzzleResultDraft.levels[].pictureReference`
|
||||
|
||||
领域校验仍由 `module-puzzle` 负责。Agent 可以创造内容,但不能绕过发布 blockers。表单化创作页与 Agent 自然语言修订都只是这些字段的不同编辑界面。
|
||||
|
||||
## 9. 前端体验
|
||||
|
||||
前端只呈现一个“智能创作 Agent”入口:
|
||||
|
||||
1. 用户输入文字、上传图片或文档。
|
||||
2. 前端显示 Agent 正在理解素材、构思玩法、生成草稿。
|
||||
3. Agent 必须先展示拼图模板目录。
|
||||
4. 用户选择模板并确认关卡模式、关卡数和预计积分范围后,Agent 才能生成草稿。
|
||||
5. 生成完成后进入拼图结果页,并提供“立即玩”入口,点击后直接进入拼图运行态。
|
||||
6. 结果页保留原来的表单化创作能力,包括作品标题、作品描述、作品标签、关卡名称、关卡图面描述、关卡图面参考、关卡图片生成和选图;这些控件编辑的是同一份模板草稿字段。
|
||||
7. Agent 对话区继续可用,用户可以用自然语言补填或修订模板草稿字段,后端通过拼图模块 Tool 生成结构化 patch 并回写同一份草稿;可写字段仅限 `workTitle`、`workDescription`、`workTags`、`levels[].levelName`、`levels[].pictureDescription`、`levels[].pictureReference`。
|
||||
8. 若 Agent 有关键不确定点,弹出独立确认面板。
|
||||
9. 用户确认或回答后,Agent 继续执行并进入拼图结果页或保持在当前草稿上下文。
|
||||
|
||||
UI 不展示大段规则说明,只展示:
|
||||
|
||||
- 当前阶段
|
||||
- 简短 Agent 回复
|
||||
- 拼图模板选择和积分范围
|
||||
- 单关卡 / 多关卡计划
|
||||
- 需要确认的问题
|
||||
- 最终草稿入口
|
||||
- 立即玩入口
|
||||
- 表单化草稿字段编辑入口
|
||||
- Agent 自然语言补填 / 修订入口
|
||||
|
||||
移动端优先,上传图片预览使用横向缩略图条,确认面板用底部抽屉。
|
||||
|
||||
## 10. 安全与治理
|
||||
|
||||
模型能力是核心,但不能没有边界:
|
||||
|
||||
1. 工具权限边界:模型只能调用已注册工具,不能直接写库。
|
||||
2. 契约边界:Typed parser 和目标玩法 validator 必须通过。
|
||||
3. 成本边界:AgentExecutor 设置最大迭代、最大工具调用、超时和预算。
|
||||
4. 内容安全:人物肖像、儿童内容、版权图、隐私图进入确认或拒绝。
|
||||
5. 记忆安全:长期记忆按用户 namespace 隔离。
|
||||
6. 可观测性:Callbacks 记录每个节点、工具、token、耗时和错误。
|
||||
|
||||
## 11. 分阶段落地
|
||||
|
||||
### Phase 1:LangChain-Rust PoC + 拼图闭环
|
||||
|
||||
- 新增 `platform-agent` PoC。
|
||||
- 封装 LangChain-Rust FunctionCallingAgent / AgentExecutor。
|
||||
- 注册拼图相关工具。
|
||||
- 只支持拼图模板;非拼图模板在 Agent 能力中标记为暂不支持。
|
||||
- 强制展示拼图模板选择、选择理由和预计积分范围。
|
||||
- 支持文字 + 单图输入,模型自主选择拼图模板并填入草稿字段。
|
||||
- 支持单关卡图片生成计划和多关卡图片生成计划。
|
||||
- 生成后的拼图内容点击“立即玩”可直接进入 `puzzle-runtime`。
|
||||
- 保留结果页原有表单化草稿字段编辑入口。
|
||||
- 支持 Agent 自然语言补填 / 修订模板草稿字段,并通过拼图模块 Tool 回写草稿。
|
||||
- SSE 推送六模块阶段。
|
||||
- 结果进入现有 `puzzle-result`。
|
||||
|
||||
### Phase 2:记忆与反思闭环
|
||||
|
||||
- 增加 `creative_agent_tool_call`、`creative_agent_reflection`、`creative_agent_memory_index`。
|
||||
- 引入短期 Memory 和长期检索。
|
||||
- 记录用户改选、发布失败、返回编辑等反馈。
|
||||
- 反思循环支持自动修正草稿。
|
||||
|
||||
### Phase 3:多玩法协作
|
||||
|
||||
- 在产品开放后再增加 Match3D、大鱼吃小鱼、方洞挑战、RPG 世界专家 Agent。
|
||||
- 使用 LangGraph subgraph 做多 Agent 协作。
|
||||
- 支持多候选玩法对比和人工确认。
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
功能验收:
|
||||
|
||||
1. 用户输入含图文材料时,Agent 能说出它理解到的创作意图。
|
||||
2. Agent 能调用拼图模板知识检索,而不是靠硬编码规则选模板。
|
||||
3. Agent 必须显式展示拼图模板、选择理由和预计积分范围,用户确认后才生成草稿。
|
||||
4. 非拼图输入不会创建其它玩法 session,只能转成拼图方案或提示暂不支持。
|
||||
5. Agent 能自主选择拼图模板并生成可通过契约校验的 `PuzzleResultDraft`。
|
||||
6. 当图片更适合直接作为拼图图面时,Agent 能选择 uploaded candidate,而不是强制生图。
|
||||
7. Agent 能生成单关卡图片计划,也能生成多关卡图片计划。
|
||||
8. 多关卡计划中每关都有独立 `levelName`、`pictureDescription` 和 `imagePrompt`。
|
||||
9. 多关卡图片生成逻辑通过拼图模块工具执行,通用 Agent 不直接拼接 `levelsJson`。
|
||||
10. 生成好的拼图内容点击“立即玩”后能直接进入 `puzzle-runtime`。
|
||||
11. 从 `puzzle-runtime` 返回时仍恢复同一 `puzzle-result` 草稿上下文。
|
||||
12. 结果页保留表单化草稿字段编辑能力,用户可以修改作品标题、作品描述、作品标签、关卡名称、关卡图面描述和关卡图面参考。
|
||||
13. 用户用自然语言提出“把标题改轻松一点”“第二关加一张风景图”等请求时,Agent 能生成结构化草稿字段 patch 并通过拼图模块 Tool 回写同一份草稿。
|
||||
14. 自然语言修订草稿字段后必须重新通过拼图草稿校验和结果预览校验。
|
||||
15. 当不确定时,Agent 只问一个关键问题,而不是把所有字段丢给用户填写。
|
||||
16. 反思节点能发现未展示积分范围、关卡数越界、草稿字段缺失或自然语言字段 patch 风险,并自动修正一次。
|
||||
17. 工具调用、反思报告、模板选择、草稿字段 patch 和目标拼图 session 绑定能恢复和审计。
|
||||
|
||||
建议命令:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts creative_agent
|
||||
cargo test -p module-creative-agent
|
||||
cargo check -p platform-agent
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
涉及 SpacetimeDB schema 后:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate -- --rust-only
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
前端:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
## 13. 当前实现状态
|
||||
|
||||
截至 `2026-05-05`,任务 C 已完成首版 PoC 落地:
|
||||
|
||||
1. `server-rs/crates/platform-agent` 已新增为 workspace member。
|
||||
2. `platform-agent` 已提供项目自有的 `CreativeAgentExecutor`、工具注册表、回调事件、`gpt-5` 多模态请求适配器和 mock executor。
|
||||
3. `platform-llm` 已支持 Responses 多模态输入块,`input_text` 与 `input_image` 会按 content part 序列化到请求体。
|
||||
4. 任务 C 的验收命令已通过:`cargo check -p platform-agent`、`cargo test -p platform-agent`、`cargo test -p platform-llm responses_multimodal`。
|
||||
|
||||
截至 `2026-05-05`,任务 E 的 API / SSE facade 已补充 Windows debug 稳定性修复:
|
||||
|
||||
1. `/api/runtime/creative-agent/sessions/{sessionId}/messages/stream` 不再把 Agent 执行、模型请求、会话更新和所有 SSE 事件内联到单个大型 `async_stream` 中。
|
||||
2. 当前实现使用后台 `tokio::spawn` 执行业务流程,并通过 `mpsc` / `UnboundedReceiverStream` 向 Axum 返回轻量 SSE stream;执行失败会更新会话为 `failed` 并发送 SSE `error`。
|
||||
3. 已补实际消费 SSE body 的回归测试,覆盖 `stage`、`puzzle_template_catalog` 与 `done` 事件;`puzzle_template_selection`、`puzzle_cost_range`、`puzzle_level_plan` 只在用户确认后进入后续快照或流程。
|
||||
|
||||
## 14. 本方案相对旧方案的变化
|
||||
|
||||
旧方案偏“规则预筛 + workflow + Adapter”。新方案调整为:
|
||||
|
||||
1. 模型负责理解素材、整理候选和草稿构思;最终模板由用户从多个候选中主动选择。
|
||||
2. LangChain-Rust AgentExecutor / FunctionCallingAgent 承担工具调用决策。
|
||||
3. LangGraph 承担六模块闭环和反思循环。
|
||||
4. Memory/RAG 让 Agent 学习用户偏好和模板经验。
|
||||
5. 当前只开放拼图玩法,但模板选择仍是显式 Agent 步骤,不因只有一个玩法而跳过。
|
||||
6. 拼图模板协议必须携带积分范围,用户选择模板并确认配置后才进入草稿生成。
|
||||
7. 单关卡/多关卡图片生成通过拼图模块 Tool 和模板协议实现,不写进通用 Agent。
|
||||
8. Agent 创作方式就是填充和修订模板草稿字段;表单化创作页和 Agent 自然语言修订都操作同一份 `PuzzleResultDraft`,并围绕 `workTitle`、`workDescription`、`workTags`、`levels[].levelName`、`levels[].pictureDescription`、`levels[].pictureReference` 这一组字段协同。
|
||||
9. Adapter 从“路由实现”降级为 Agent action tool。
|
||||
10. 规则只保留为安全、契约、成本和权限边界。
|
||||
@@ -0,0 +1,75 @@
|
||||
# 登录成功每日登录埋点闭环方案(2026-05-08)
|
||||
|
||||
## 背景
|
||||
|
||||
后台“埋点数据”需要能看到真实登录触发的 `daily_login` 埋点。此前方案 A 已把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure:
|
||||
|
||||
- `record_daily_login_tracking_event_and_return`
|
||||
- `spacetime-client` 方法:`record_daily_login_tracking_event(user_id)`
|
||||
|
||||
但认证成功链路当时还没有调用该方法,因此当时只完成了“任务中心读取不污染登录埋点”,没有完成“用户真实登录写入每日登录埋点”。后续后端通用埋点能力落地后,`daily_login` 已进一步改为通过统一 `record_tracking_event_and_return(RuntimeTrackingEventInput)` procedure 写入,旧 `record_daily_login_tracking_event_and_return` 不再作为认证链路的目标入口。
|
||||
|
||||
## 现象
|
||||
|
||||
用户已经登录、cookie 未过期时,直接打开网页并不会触发每日登录埋点。原因是前端恢复登录态只读取 `/api/auth/me`,这条链路不会主动走 refresh cookie 续期,因此后端新的埋点写入点不会被触发。
|
||||
|
||||
## 修复思路
|
||||
|
||||
在 `AuthGate` 恢复已登录会话时,先主动调用一次 refresh 接口轮换 refresh cookie,再调用 `/api/auth/me` 读取当前会话。这样无论本地 access token 是否仍然有效,打开页面都会进入 refresh 续期链路,从而触发后端的 `daily_login` 埋点写入。
|
||||
|
||||
## 目标
|
||||
|
||||
在用户认证成功并创建 refresh session / access token 后,异步尝试写入每日登录埋点。
|
||||
|
||||
覆盖入口:
|
||||
|
||||
- 手机验证码登录:`POST /api/auth/phone/login`
|
||||
- 密码入口登录:`POST /api/auth/entry`
|
||||
- 重置密码后自动登录:`POST /api/auth/password/reset`
|
||||
- 微信 OAuth callback 登录:`GET /api/auth/wechat/callback`
|
||||
- 微信绑定手机号后激活/登录态刷新:`POST /api/auth/wechat/bind-phone`
|
||||
- refresh cookie 续期:`POST /api/auth/session/refresh`
|
||||
|
||||
## 设计约束
|
||||
|
||||
1. 埋点写入不能阻断登录成功响应。
|
||||
2. 只有认证成功并已创建会话后,或 refresh session rotate 成功并签发新 access token 后才记录。
|
||||
3. 失败只记 warning,继续返回 token / cookie。
|
||||
4. 写入统一收口,避免多个登录 handler 各自拼 procedure 调用。
|
||||
5. 不修改 SpacetimeDB 表结构,不需要更新 `migration.rs`。
|
||||
|
||||
## 实现方案
|
||||
|
||||
新增 `api-server` 内部 helper:
|
||||
|
||||
```rust
|
||||
record_daily_login_tracking_event_after_auth_success(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
user_id: &str,
|
||||
login_method: AuthLoginMethod,
|
||||
).await;
|
||||
```
|
||||
|
||||
该 helper:
|
||||
|
||||
- 构造 `TrackingEventDraft::user("daily_login", "profile", user_id)`
|
||||
- 使用 `daily-login:{user_id}:{day_key}` 作为事件 ID,保持北京时间自然日幂等
|
||||
- 调用统一 `record_tracking_event_after_success(...)`,最终进入 `record_tracking_event_and_return(RuntimeTrackingEventInput)`
|
||||
- 成功时记录 info
|
||||
- 失败时记录 warn,并明确“登录流程继续”
|
||||
|
||||
在各登录入口 `create_auth_session` 成功后调用该 helper。refresh cookie 续期在 `rotate_session` 和 `sign_access_token_for_user` 成功后调用同一个 helper,`login_method` 使用 refresh session 上保存的 `issued_by_provider`,避免把续期统一误标成 password。
|
||||
|
||||
## 验收
|
||||
|
||||
- `cargo test -p api-server auth_session -- --nocapture`
|
||||
- `cargo check -p api-server`
|
||||
- `cargo check -p spacetime-client`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
- `npm run test -- AuthGate.test.tsx`
|
||||
|
||||
## 注意
|
||||
|
||||
`npm run dev` 是长期运行进程;如需本地 smoke,应启动后用 `/healthz` 和后台页面验证,不要等待该命令退出。
|
||||
@@ -0,0 +1,74 @@
|
||||
# 本地 Rust 栈端口冲突预检
|
||||
|
||||
日期:`2026-05-09`
|
||||
|
||||
## 问题
|
||||
|
||||
执行完整本地栈启动命令时:
|
||||
|
||||
```bash
|
||||
node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh --spacetime-port 3101 --skip-spacetime
|
||||
```
|
||||
|
||||
如果本机已有旧的 `api-server`、主站 Vite 或后台 Vite 进程仍在监听默认端口,脚本可能出现混合日志:
|
||||
|
||||
```text
|
||||
Port 3000 is in use, trying another one...
|
||||
Error: Os { code: 10048, kind: AddrInUse }
|
||||
process didn't exit successfully: `server-rs\target\debug\api-server.exe`
|
||||
```
|
||||
|
||||
其中 `wait_for_api_server` 只探测 `http://127.0.0.1:<api-port>/healthz`。当旧 `api-server` 仍监听 `8082` 时,健康检查会命中旧进程并误判新服务已就绪;随后新 `cargo run` 真正绑定 `8082` 时失败。与此同时,Vite 默认会在 `3000` 被占用时漂移到下一个端口,导致浏览器仍可能打开旧前端。
|
||||
|
||||
## 处理
|
||||
|
||||
`scripts/dev-rust-stack.sh` 在进入 SpacetimeDB publish 和 Rust 编译前,先检查三类端口是否可绑定:
|
||||
|
||||
1. Rust `api-server`:默认 `127.0.0.1:8082`。
|
||||
2. 主站 Vite:默认 `0.0.0.0:3000`。
|
||||
3. 后台 Vite:默认 `127.0.0.1:3102`。
|
||||
|
||||
端口被占用时,脚本会直接失败并打印监听进程。Windows 本地会通过 `Get-NetTCPConnection` 与 `Win32_Process` 输出 `pid / name / address / command`,方便精确停止旧进程。
|
||||
|
||||
主站和后台 Vite 也追加 `--strictPort`,避免默认漂移到 `3001`、`3103` 等端口后让浏览器继续访问旧页面。
|
||||
|
||||
## 排障步骤
|
||||
|
||||
PowerShell 查看默认端口占用:
|
||||
|
||||
```powershell
|
||||
Get-NetTCPConnection -State Listen -LocalPort 3000,3102,8082,3101 -ErrorAction SilentlyContinue |
|
||||
Select-Object LocalAddress,LocalPort,OwningProcess |
|
||||
Sort-Object LocalPort
|
||||
```
|
||||
|
||||
查看进程命令行:
|
||||
|
||||
```powershell
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.ProcessId -in @(3000端口PID, 8082端口PID) } |
|
||||
Select-Object ProcessId,Name,CommandLine
|
||||
```
|
||||
|
||||
停止确认可丢弃的旧本地开发进程:
|
||||
|
||||
```powershell
|
||||
Stop-Process -Id <pid> -Force
|
||||
```
|
||||
|
||||
如果确实需要保留旧栈,可显式换端口启动新栈:
|
||||
|
||||
```bash
|
||||
node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh \
|
||||
--skip-spacetime \
|
||||
--spacetime-port 3101 \
|
||||
--api-port 8090 \
|
||||
--web-port 3001 \
|
||||
--admin-web-port 3103
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
1. `bash -n scripts/dev-rust-stack.sh` 通过。
|
||||
2. 默认端口被占用时重新运行完整栈,脚本应在 publish 前失败并打印占用进程。
|
||||
3. 清理占用进程或换端口后,重新启动时不再出现 Vite 端口漂移或 `api-server` `AddrInUse`。
|
||||
137
docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md
Normal file
137
docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Hyper3D Rodin Gen-2 3D 模型生成接入方案 2026-05-08
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型能力。
|
||||
|
||||
本次只做后端安全代理与前端可复用 client,不新增 SpacetimeDB 表,不落正式资产对象,不把 Hyper3D API Key 下发到前端。生成结果仍由调用方在拿到下载链接后决定是否进入 OSS / `asset_object` 的正式资产链。
|
||||
|
||||
## 2. 参考接口
|
||||
|
||||
参考文档:
|
||||
|
||||
- `https://developer.hyper3d.ai/api-specification/rodin-generation-gen2`
|
||||
- `https://developer.hyper3d.ai/api-specification/check-status`
|
||||
- `https://developer.hyper3d.ai/api-specification/download-results`
|
||||
- `https://developer.hyper3d.ai/api-specification/check-status_reset_v`
|
||||
- `https://developer.hyper3d.ai/api-specification/download-results_reset_v`
|
||||
|
||||
上游接口:
|
||||
|
||||
```text
|
||||
POST https://api.hyper3d.com/api/v2/rodin
|
||||
POST https://api.hyper3d.com/api/v2/status
|
||||
POST https://api.hyper3d.com/api/v2/download
|
||||
```
|
||||
|
||||
Rodin Gen-2 提交接口必须使用 `multipart/form-data`。文本生成时提交 `prompt`;图片生成时提交一个或多个 `images` 文件,可选 `prompt` 作为辅助描述。两种模式均固定提交 `tier=Gen-2`。
|
||||
|
||||
官方 `*_reset_v` 文档对状态和下载有两个关键约束:
|
||||
|
||||
1. 生成接口返回的顶层 `uuid` 是后续下载接口的 `task_uuid`,不要使用 `jobs.uuids` 中的子任务 uuid 作为下载参数。
|
||||
2. 状态接口使用 `subscription_key` 查询,并返回 `jobs[]`;只有所有 job 的 `status` 都为 `Done` 才能进入下载,任一 job `Failed` 都应视为任务失败。
|
||||
|
||||
## 3. 环境变量
|
||||
|
||||
```text
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
兼容变量:
|
||||
|
||||
```text
|
||||
RODIN_BASE_URL
|
||||
RODIN_API_KEY
|
||||
RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. `HYPER3D_API_KEY` / `RODIN_API_KEY` 只允许写入本地或生产私密环境,不提交到 Git。
|
||||
2. 缺少 API Key 时,后端返回 `503 SERVICE_UNAVAILABLE`。
|
||||
3. 本地真实联调推荐把 key 放在 `.env.secrets.local`;`npm run api-server`、`npm run dev:rust` 与 `npm run dev` 均应保持“外层 shell 变量优先,随后 `.env`、`.env.local`、`.env.secrets.local` 逐层覆盖”的口径,避免 `.env` 里的空示例值压掉私密 key。
|
||||
4. `HYPER3D_BASE_URL` 默认使用公开 API 基础地址;如果团队后续改用代理网关,可通过环境变量覆盖。
|
||||
|
||||
## 4. 后端路由
|
||||
|
||||
新增 4 个鉴权路由:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/assets/hyper3d/text-to-model` | 提交 Rodin Gen-2 文生模型任务 |
|
||||
| `POST` | `/api/assets/hyper3d/image-to-model` | 提交 Rodin Gen-2 图生模型任务 |
|
||||
| `POST` | `/api/assets/hyper3d/status` | 使用 `subscriptionKey` 查询任务状态 |
|
||||
| `POST` | `/api/assets/hyper3d/download` | 使用 `taskUuid` 获取模型下载列表 |
|
||||
|
||||
文生模型请求最小体:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "一只低多边形宝箱,适合 RPG 游戏资产",
|
||||
"geometryFileFormat": "glb",
|
||||
"material": "PBR",
|
||||
"quality": "medium",
|
||||
"meshMode": "Quad",
|
||||
"previewRender": true
|
||||
}
|
||||
```
|
||||
|
||||
图生模型请求最小体:
|
||||
|
||||
```json
|
||||
{
|
||||
"imageDataUrls": ["data:image/png;base64,..."],
|
||||
"prompt": "保留主体轮廓,生成游戏可用 3D 模型",
|
||||
"conditionMode": "concat",
|
||||
"geometryFileFormat": "glb"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 约束
|
||||
|
||||
1. 图片只接受 `data:image/png|jpeg|webp;base64,...`,最多 5 张。
|
||||
2. 单张图片解码后不超过 10MB。
|
||||
3. `geometryFileFormat` 限定为 `glb/usdz/fbx/obj/stl`,默认 `glb`。
|
||||
4. `material` 限定为 `PBR/Shaded/All`,默认 `PBR`。
|
||||
5. `quality` 限定为 `high/medium/low/extra-low`,默认 `medium`。
|
||||
6. `meshMode` 限定为 `Quad/Raw`,默认 `Quad`。
|
||||
7. `addons` 首版只允许 `HighPack`。
|
||||
8. `bboxCondition` 必须为 3 个正数,按上游要求序列化为 JSON 字符串。
|
||||
9. `subscriptionKey` 是 Hyper3D 返回的 opaque token,状态查询只校验非空,不在本地做 256 字符等固定长度限制,避免长 token 阻断抓大鹅草稿内联模型生成。
|
||||
|
||||
## 6. 返回语义
|
||||
|
||||
提交任务成功后返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"provider": "hyper3d-rodin",
|
||||
"mode": "text-to-model",
|
||||
"taskUuid": "task-uuid",
|
||||
"subscriptionKey": "subscription-key",
|
||||
"jobUuids": ["job-uuid"],
|
||||
"message": "Submitted.",
|
||||
"tier": "Gen-2"
|
||||
}
|
||||
```
|
||||
|
||||
状态查询会把上游 `Waiting / Generating / Done / Failed` 归一化为 `waiting / generating / done / failed / unknown`,整体状态必须以 `jobs[]` 聚合结果为准。下载接口只返回上游 `list.name` 与 `list.url`,不在 Hyper3D 代理路由中转存文件;具体玩法若需要持久化模型,应在玩法编排层等待 `Done` 后再下载并转存。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts hyper3d
|
||||
cargo test -p api-server hyper3d
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
真实 API smoke 只在本地私密环境设置 `HYPER3D_API_KEY` 后执行。提交生成任务会消耗 Hyper3D Credit,默认验证不自动调用真实生成接口。
|
||||
@@ -108,6 +108,12 @@ Node 旧链路对上传封面有明确处理:
|
||||
|
||||
Rust 本批必须保持这组兼容约束。
|
||||
|
||||
2026-05-08 交互补充:
|
||||
|
||||
1. 前端裁剪面板不再展示 `缩放 / 左右位置 / 上下位置` 参数滑杆。
|
||||
2. 作者直接在图片上拖拽裁剪框内部移动区域,或拖拽四边、四角调整裁剪范围。
|
||||
3. 调整过程中前端持续锁定 `16:9`,确认时仍只提交后端兼容的 `cropRect`。
|
||||
|
||||
## 4. 请求与响应 contract
|
||||
|
||||
### 4.1 `POST /api/custom-world/scene-image`
|
||||
|
||||
@@ -122,6 +122,12 @@
|
||||
2. AI worker 绕过确认链路写出不完整记录
|
||||
3. 把 OSS 响应中的派生 URL 当成对象真相
|
||||
|
||||
### 5.1 OSS V4 签名时间格式
|
||||
|
||||
`platform-oss` 的 OSS V4 签名时间必须显式格式化为 `YYYYMMDDTHHMMSSZ`,签名 scope 日期必须显式格式化为 `YYYYMMDD`。实现中不要依赖 `OffsetDateTime::time().to_string()` 或 `date().to_string()` 再替换字符,因为 UTC 小时、分钟、秒为个位数时可能不会保留前导零,导致 AI 生图已完成但上传 OSS 阶段报 `OSS V4 签名时间格式化失败`。
|
||||
|
||||
拼图、视觉小说、自定义世界等所有服务端生成图片上传链路都复用同一套 `platform-oss` 签名 helper;新增签名逻辑时必须覆盖个位时间分量的测试样例,例如 `05:03:09` 应输出 `T050309Z`。
|
||||
|
||||
## 6. 与 Web 端的边界
|
||||
|
||||
Web 端当前只允许:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Maincloud 残留引用移除策略
|
||||
|
||||
## 背景
|
||||
|
||||
项目后端、发布和本地联调已经切到 `server-rs + Axum + SpacetimeDB` 当前基线,并以本地或显式配置的 SpacetimeDB 服务为运行目标。历史上用于连接 SpacetimeDB Maincloud 的脚本、环境变量、测试名和文档口径已经不再代表当前工程约束。
|
||||
|
||||
## 决策
|
||||
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关命名、脚本、测试、环境变量、文档要求和启动命令全部视为历史残留。
|
||||
- 后续禁止新增、运行或引用 `maincloud` 相关代码、测试、脚本、文档要求、环境变量和命令。
|
||||
- 旧文档若要求执行 `npm run api-server:maincloud`、`npm.cmd run api-server:maincloud` 或读取 `GENARRATIVE_SPACETIME_MAINCLOUD_*`,一律以本策略和 `AGENTS.md` 最新约束为准,并在触碰该文档或代码时同步修正。
|
||||
- 后端 API smoke 统一使用当前非 Maincloud 启动入口:`npm run api-server`。服务就绪以 `GET /healthz` 返回成功为准。
|
||||
- SpacetimeDB 运行目标必须来自本地开发服务、生产自托管服务,或显式 `SERVER_URL` 配置;不得再回退到 Maincloud 默认值。
|
||||
|
||||
## 落地规则
|
||||
|
||||
1. 修改后端代码后,按对应 DDD 文档执行定向测试;涉及 API smoke 时执行 `npm run api-server` 并探测 `/healthz`。
|
||||
2. 触碰历史脚本、测试支撑或文档时,优先删除或改名其中的 `maincloud` 口径,改为当前本地或显式服务配置口径。
|
||||
3. 新增文档不得把 `api-server:maincloud` 写成验收命令,也不得要求配置 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
||||
4. 新增测试不得使用 `DEFAULT_MAINCLOUD_*` 这类历史命名;测试辅助应使用通用 `api-server`、`healthz` 或明确的本地 SpacetimeDB 命名。
|
||||
5. 如需保留历史文档中的旧执行记录,只能作为归档事实存在,不得作为当前执行清单、验收命令或开发约束继续引用。
|
||||
|
||||
## 验证方式
|
||||
|
||||
常规检查:
|
||||
|
||||
```bash
|
||||
rg -n "maincloud|Maincloud|MAINCLOUD|api-server:maincloud|GENARRATIVE_SPACETIME_MAINCLOUD" AGENTS.md docs .hermes package.json scripts server-rs -S
|
||||
```
|
||||
|
||||
验收口径:
|
||||
|
||||
- `AGENTS.md`、`.hermes/shared-memory/` 和当前任务相关文档不得要求使用 Maincloud。
|
||||
- 活跃脚本、测试和配置不得依赖 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
||||
- 后端启动和 smoke 以 `npm run api-server` 与 `/healthz` 为准。
|
||||
|
||||
## 关联文档
|
||||
|
||||
- [SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md](./SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md)
|
||||
- [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md)
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)
|
||||
@@ -518,15 +518,25 @@ totalItemCount = clearCount * 3
|
||||
|
||||
每种 `itemTypeId` 的数量必须是 `3` 的倍数。
|
||||
|
||||
消除物类型数按创作输入的 `clearCount` 计算:
|
||||
|
||||
```text
|
||||
itemTypeCount = clearCount <= 25 ? clearCount : 25
|
||||
```
|
||||
|
||||
当 `clearCount <= 25` 时,运行态快照中的不同 `itemTypeId` 数量必须等于 `clearCount`;当 `clearCount > 25` 时,不同 `itemTypeId` 数量必须等于 `25`。超过 `25` 组的消除目标按这 `25` 种类型轮转生成,确保每种类型的最终数量仍是 `3` 的倍数。
|
||||
|
||||
这 `25` 组在同一局内还必须对应 25 套不同的形状和颜色签名,不能有两组视觉上撞型。
|
||||
|
||||
## 9.3 demo 视觉素材
|
||||
|
||||
首版使用内置视觉键和前端内置几何图形资产,不接真实图片生成。
|
||||
首版使用 25 个内置积木件视觉键和前端内置几何图形资产,不接真实图片生成。
|
||||
|
||||
1. 水果题材必须使用 `watermelon-green / apple-red / banana-yellow / grape-purple / melon-green / berry-blue / peach-pink / plum-indigo / lime-lime / orange-orange` 这组内置水果视觉键;前端首版将其映射为纯色几何体,不渲染水果写实图,也不能显示为带文字或透明气泡的小球。
|
||||
2. 非水果题材暂使用 `red_circle / yellow_triangle / purple_diamond / green_square / blue_star / orange_hexagon / cyan_capsule / pink_heart / lime_leaf / white_moon` 这组兜底颜色形状视觉键。
|
||||
3. `visualKey` 不允许在前端统一兜底为同一个素材;未知 key 至少要有稳定的颜色差异,避免多个不同 `itemTypeId` 被玩家误认为同一种物品。
|
||||
4. 运行态图案必须使用实心、高饱和、无文字的几何 SVG,至少覆盖圆形、三角形、菱形、方形、五角星、六边形、胶囊、心形、梯形、平行四边形等多种轮廓;外层命中按钮不得再显示半透明气泡底。
|
||||
5. 水果题材的相对尺寸由后端权威半径决定,首版要求西瓜明显大于苹果,苹果、橙子、桃子等中型水果大于葡萄、李子、青柠等小型水果;前端不得自行改写规则半径,只负责按快照表现。
|
||||
1. 当前 demo 使用 25 个积木件视觉键作为默认素材池;前端首版将其映射为无文字的 2D 图标和程序化 3D 积木模型,不渲染写实图,也不能显示为带文字或透明气泡的小球。
|
||||
2. `visualKey` 不允许在前端统一兜底为同一个素材;未知 key 至少要有稳定的颜色差异,避免多个不同 `itemTypeId` 被玩家误认为同一种物品。
|
||||
3. 运行态图案必须使用实心、高饱和、无文字的几何 SVG,并保持与 3D 模型同一批 `visualKey` 对应关系;外层命中按钮不得再显示半透明气泡底。
|
||||
4. 每局按使用类型数量分配五档相对体积:XL 型 `1.60~2.30` 占 `20%`,L 型 `1.25~1.60` 占 `30%`,M 型固定 `1.00` 占 `30%`,XS 型 `0.65~0.85` 占 `15%`,S 型 `0.35~0.50` 占 `5%`。非整数配额按最大余数补齐,总数必须等于本局使用类型数量。
|
||||
5. 同一局内同一个颜色和造型的 `visualKey` 只能对应一个尺寸档位和一个半径,不能出现同一物品类型三件副本大小不同,也不能出现同一视觉键在复用时被分配到两种大小。前端不得自行改写规则半径,只负责按快照表现。
|
||||
6. 后续接入真实题材图片素材前,必须另补资产生成方案。
|
||||
|
||||
## 9.4 难度
|
||||
@@ -639,6 +649,8 @@ src/components/match3d-runtime/
|
||||
|
||||
1. 名称:`抓大鹅`
|
||||
2. 子标题:`经典消除玩法`
|
||||
3. `src/config/newWorkEntryConfig.ts` 中 `match3d` 必须保持 `visible: true` 与 `open: true`,平台首屏卡带和创作类型弹层都从该配置派生,不允许只保留路由能力却隐藏创作入口。
|
||||
4. 入口点击后进入 `match3d-agent-workspace`,对应前端路径为 `/creation/match3d/agent`,并通过 `/api/creation/match3d/sessions` 创建正式 Agent 会话;如果公开广场读取失败,只降级广场列表,不能阻断或隐藏抓大鹅创作入口。
|
||||
|
||||
## 11.4 运行态 UI
|
||||
|
||||
@@ -646,9 +658,10 @@ src/components/match3d-runtime/
|
||||
|
||||
1. 圆形空间占据主要区域。
|
||||
2. 备选栏固定 `7` 格。
|
||||
3. 倒计时清晰但不遮挡物品。
|
||||
4. 物品点击区域稳定,不因动画造成布局跳动。
|
||||
5. 胜利/失败结算使用独立面板,不在当前面板下方展开。
|
||||
3. 3D 模式下,备选栏格子使用与圆形空间内一致的程序化 3D 模型预览,固定斜 `45` 度视角,且不接入场内物理碰撞;托盘预览必须共享一个 WebGL renderer,按实际容器宽高更新正交相机,并以独立 pivot 居中每个模型后定位到对应格子;不能每格创建独立 renderer;仅 WebGL 回退或 `2D` 模式使用 2D 图标。
|
||||
4. 倒计时清晰但不遮挡物品。
|
||||
5. 物品点击区域稳定,不因动画造成布局跳动。
|
||||
6. 胜利/失败结算使用独立面板,不在当前面板下方展开。
|
||||
|
||||
## 11.5 本地 mock 口径
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 抓大鹅创作入口开放与错误隔离 2026-05-01
|
||||
|
||||
> 2026-05-08 更新:抓大鹅创作端入口已重新开放,`match3d.visible` 调整为 `true`。当前入口状态以 `NEW_WORK_ENTRY_CONFIG_2026-05-01.md` 和 `src/config/newWorkEntryConfig.ts` 为准。
|
||||
|
||||
## 1. 背景
|
||||
|
||||
抓大鹅 Match3D 玩法域已完成当前 demo 主链接入,本轮恢复创作页入口,使玩家可以从创作中心直接进入抓大鹅共创工作台。同时,平台首页会并行读取 RPG、拼图、抓大鹅等公开广场数据,公开广场接口未就绪、空表或临时失败不应污染创作入口错误态,也不应表现成登录异常。
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# 抓大鹅草稿素材生成流水线 2026-05-10
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于改造 `生成抓大鹅草稿` 的首版生成链路:点击按钮后先进入独立生成过程页,生成结束后自动进入抓大鹅草稿页,并在草稿页 `3D素材` Tab 预览本次生成的 3D 模型。
|
||||
|
||||
本次只把任意难度都收敛为 `3` 件物品。后续难度曲线恢复时,再把物品数、网格数和手动 3D 任务数量从配置中放开。
|
||||
|
||||
## 2. 前端流程
|
||||
|
||||
入口仍复用 `Match3DAgentWorkspace` 表单。点击 `生成抓大鹅草稿` 后:
|
||||
|
||||
1. 创建 Match3D session。
|
||||
2. 后端先用当前题材和本地兜底元信息创建同一个 Match3D 草稿 profile,草稿 Tab 必须立即能看到这份存档。
|
||||
3. 进入 `match3d-generating` 生成过程页。
|
||||
4. 过程页复用拼图生成页的 `CustomWorldGenerationView` 结构。
|
||||
5. 生成成功后自动进入 `match3d-result`。
|
||||
6. 生成失败时停留在生成过程页,允许重新生成或返回创作中心;重新生成必须复用同一个 session / profile,并从缺失的素材阶段继续,不新建第二份草稿。
|
||||
|
||||
生成页步骤固定为:
|
||||
|
||||
```text
|
||||
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 生成3D模型 -> 写入草稿页
|
||||
```
|
||||
|
||||
生成页只展示题材和物品数量,不展示玩法规则说明。
|
||||
|
||||
当前 `match3d-generating` 进度页不是后端 task 状态订阅页,而是一个覆盖 `match3d_compile_draft` 长 action 的本地时间进度页:前端每 500ms 以本地时间刷新阶段展示,真正的生成完成仍以 action 返回为准。为避免长 action 未返回时页面完全无感,生成页在 `match3d_compile_draft` 执行期间每 3 秒旁路读取一次 session 和 work detail,并用 profile 中已写回的 `generatedItemAssets` 更新 `生成3D模型` 的完成数量。Hyper3D 控制台中看到 3 个 Rodin 任务已经 `Done` 后,页面仍可能继续停留在 `生成3D模型`,此时通常表示后端还在等待下载列表、下载 GLB、转存 OSS 或写回 `generated_item_assets_json`;若 `generatedItemAssets` 已出现 `model_ready`,前端应逐步显示完成数量。排查时应看 api-server 日志中的 `抓大鹅 Rodin 状态轮询返回`、`抓大鹅 Rodin 下载列表轮询返回`、`抓大鹅 Rodin GLB 下载完成` 和 `抓大鹅 Rodin GLB 转存 OSS 完成`。
|
||||
|
||||
## 3. 后端编排边界
|
||||
|
||||
外部模型和 OSS 上传全部由 `api-server` 编排,不进入 SpacetimeDB reducer。SpacetimeDB 继续只负责 Match3D 会话、草稿和作品 profile 的确定性写入。
|
||||
|
||||
`match3d_compile_draft` action 的后端顺序为:
|
||||
|
||||
1. 读取 session config。
|
||||
2. 将本次 MVP 的 `clearCount` 固定为 `3`,并同步用于草稿编译。
|
||||
3. 先调用 SpacetimeDB compile procedure 写入草稿。首次执行使用新 `profileId`;重试时复用 session draft / work profile 中已有 `profileId`。这一步不能等待 LLM、图片、OSS 或 Rodin 成功后才执行。
|
||||
4. 基于入口页题材设定文本调用文本模型生成作品元信息。模型固定请求 `gpt-4o`,只返回 JSON,其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`tags` 为 3 到 6 个中文短标签;`summary` 首版必须保持空字符串,结果页 `作品描述` 默认留给用户填写。文本模型不可用时保留第 3 步的本地兜底,不阻断草稿。
|
||||
5. 调用文本模型生成 `3` 个题材下的短物品名称。
|
||||
6. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt`。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine,不重新接入 APIMart 图片网关。
|
||||
7. 将素材图按 `n*n` 网格切割成独立图片。当前 `3` 件物品使用 `2*2` 网格,取前 `3` 格。
|
||||
8. 将素材图和每张独立图片上传到 OSS,其中独立图片作为草稿页素材预览和 Rodin 图生模型参考图;每次获得可恢复的图片资产后,都要回写 `match3d_work_profile.generated_item_assets_json`。
|
||||
9. 使用每张独立图片作为参考图,并行调用 Hyper3D Rodin 图生模型;所有 3D 模型任务必须在同一阶段同时提交、同时轮询状态、同时下载并转存 OSS,禁止逐个物品串行等待模型完成。每个任务按官方 `check-status_reset_v` / `download-results_reset_v` 文档轮询状态和下载:状态查询使用 `subscription_key`,整体完成态以 `jobs[]` 聚合为准;下载查询使用生成响应顶层 `uuid` 作为 `task_uuid`,不能使用 `jobs.uuids` 子任务 uuid。只有 `jobs` 全部进入 `Done` 才能视为任务完成,任一 job `Failed` 则失败。完成后选择 `.glb` 下载文件,并把 GLB 转存到 OSS。Rodin 的 `subscriptionKey` 是上游 opaque token,不做 256 字符这类短文本长度限制。Rodin 任务状态进入完成态后,下载列表仍可能延迟发布;后端必须对下载列表继续轮询,并兼容 `url`、`downloadUrl`、`fileUrl`、`signedUrl` 等下载字段别名,只有预览图而没有模型文件时不能伪装成 GLB 成功。
|
||||
10. Rodin 每批完成后继续回写 `generated_item_assets_json`。成功素材状态为 `model_ready`;失败素材保留图片引用并记录 `error`,下次 `match3d_compile_draft` 只继续缺失模型的素材,不重复生成已完成的 GLB。
|
||||
11. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材。
|
||||
|
||||
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅` 与本地标签兜底,不阻断素材生成;但描述仍保持空字符串。
|
||||
|
||||
草稿生成阶段会调用 Hyper3D Rodin 并等待 GLB 下载完成;前端 `match3d_compile_draft` action 请求超时必须覆盖该长耗时链路,当前 Match3D client 使用 20 分钟超时。Rodin 单模型状态轮询预算为 10 分钟,下载列表发布轮询预算为 5 分钟;GLB 下载和 OSS PutObject 各自设置 3 分钟 HTTP 超时,避免上游下载或转存连接长期悬挂。由于 3 个模型并行生成,总耗时按最慢模型计算,不能按模型数量线性叠加。结果页 `3D素材` Tab 直接加载已生成模型;用户点击 `重新生成` 时再复用 Rodin 安全代理,首版重新生成只更新当前页面内预览状态,后续正式资产绑定以独立技术方案为准。
|
||||
|
||||
## 4. 图片提示词
|
||||
|
||||
素材图提示词必须显式包含:
|
||||
|
||||
```text
|
||||
生成一张1:1图片
|
||||
生成2*2网格素材图
|
||||
整体画风遵循:...
|
||||
只绘制这些物品:...
|
||||
不要出现文字、水印、UI、边框
|
||||
```
|
||||
|
||||
`包含若干个物品名称` 在落地中解释为“按生成出的物品名称绘制对应主体”,不要求图片上写出物品名称。这样可以避免文字渲染污染切图和后续手动 3D 模型参考。
|
||||
|
||||
入口页内置风格参考图通过同一 VectorEngine `gpt-image-2-all` 能力生成,保存路径固定为:
|
||||
|
||||
```text
|
||||
public/match3d-style-references/clay-toy.png
|
||||
public/match3d-style-references/low-poly.png
|
||||
public/match3d-style-references/toy-plastic.png
|
||||
public/match3d-style-references/wood-carved.png
|
||||
public/match3d-style-references/voxel-block.png
|
||||
public/match3d-style-references/metal-mecha.png
|
||||
```
|
||||
|
||||
这些图片只作为入口页风格选择的视觉参考,不进入用户草稿资产,不替代生成时的物品素材图。
|
||||
|
||||
## 5. OSS 路径
|
||||
|
||||
新增 generated legacy prefix:
|
||||
|
||||
```text
|
||||
generated-match3d-assets
|
||||
```
|
||||
|
||||
建议对象分组:
|
||||
|
||||
```text
|
||||
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
|
||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image/image.png
|
||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/model/{taskUuid}/model.glb
|
||||
```
|
||||
|
||||
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key,导致草稿页预览图全部一致。
|
||||
|
||||
HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、`modelSrc`、`modelObjectKey`、`modelFileName`、`taskUuid`、`subscriptionKey` 和 `status`。模型生成成功后 `status = model_ready`;若后续允许部分模型失败降级,失败素材必须带 `error`,且不能伪装成可预览模型。前端模型预览必须通过 `/api/assets/read-bytes` 读取私有 GLB 字节并转成 Blob URL 后交给 Three.js,不直接请求裸 `/generated-match3d-assets/...` 路径。
|
||||
|
||||
## 5.1 运行态模型消费
|
||||
|
||||
生成模型不仅用于结果页预览,也必须进入游戏运行态。运行态入口的传递链路为:
|
||||
|
||||
```text
|
||||
Match3DWorkProfile / PlatformMatch3DGalleryCard
|
||||
-> Match3DRuntimeShell(generatedItemAssets)
|
||||
-> Match3DPhysicsBoard / Match3DTrayPreviewBoard
|
||||
```
|
||||
|
||||
`Match3DPhysicsBoard` 与 `Match3DTrayPreviewBoard` 按运行快照中的 `itemTypeId` 稳定排序后,把生成出的模型顺序映射到对应类型。当前 MVP 固定 `clearCount = 3`,因此 `match3d-type-01/02/03` 分别对应生成列表的第 `1/2/3` 个模型;后续恢复更多物品生成时,后端必须继续保证 `generatedItemAssets` 顺序与类型编号一致。
|
||||
|
||||
前端加载规则:
|
||||
|
||||
1. 优先读取 `modelSrc`;为空时使用 `modelObjectKey`。
|
||||
2. 通过 `readAssetBytes` 调用 `/api/assets/read-bytes`,由同源后端读取 OSS 私有对象字节。
|
||||
3. 使用 Three.js `GLTFLoader.parseAsync` 解析 GLB 字节,并按物品类型缓存模板。
|
||||
4. 场内每个物品和备选栏预览都从模板 clone 独立对象,点击命中继续写入 `itemInstanceId`。
|
||||
5. 物理碰撞和边界仍沿用现有 `visualKey` 的程序化几何,生成 GLB 只替换视觉模型,不承接规则真相。
|
||||
6. 模型缺失、读取失败或 WebGL 回退时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算;调试模式下需要输出加载失败的 `itemTypeId`、模型来源和错误信息,便于区分“资产没有传入”和“GLB 字节读取或解析失败”。
|
||||
|
||||
结果页点击 `试玩` 时,前端必须把当前结果页可见的 `generatedItemAssets` 带入运行态启动入参。`PUT /api/runtime/match3d/works/{profileId}` 若因为并发或旧快照返回了缺少素材的 profile,`Match3DResultView` 需要把当前 draft / profile 的素材重新合并到运行态 profile,并在启动试玩前调用生成素材保存接口把当前可见的 `generatedItemAssets` 写回作品 profile;不能只在内存里把素材补到 `onStartTestRun(profile)`。发布同理必须先落库当前素材,再调用 `publish_match3d_work`,否则公开推荐流和正式运行态只能读到旧 profile 快照,历史草稿尤其容易表现为结果页有 3D 模型、正式游戏仍是默认积木。若历史草稿同时存在旧 `draft.generatedItemAssets` 和较新的 `profile.generatedItemAssets`,同 `itemId` 下以 profile 中已有的 `modelSrc` / `modelObjectKey` 补齐 draft,不能让旧 draft 把模型状态覆盖回 `image_ready`。`PlatformEntryFlowShellImpl` 在渲染 `match3d-runtime` 时按 `run.profileId` 优先使用当前 `match3dProfile.generatedItemAssets`,只有 profileId 不匹配时才读取 `selectedPublicWorkDetail.generatedItemAssets`。推荐流内嵌正式运行态也必须走同一解析器;当推荐卡片摘要缺少素材时,启动前补读 `getMatch3DWorkDetail(profileId)`,把详情里的生成模型写入 `match3dProfile` 后再传给运行态。这样可以避免从公开详情页残留状态或推荐卡片旧摘要进入试玩 / 正式游戏时,把已生成草稿的 3D 模型覆盖成空列表。
|
||||
|
||||
## 6. 自动保存与草稿恢复
|
||||
|
||||
点击 `生成抓大鹅草稿` 后,草稿存档创建与素材生成解耦:
|
||||
|
||||
1. 首次 compile 必须先写 `match3d_work_profile` 草稿行,即使后续卡在文本模型、图片生成、OSS 上传、Rodin 生成或下载转存任意阶段。
|
||||
2. 失败态前端要重新读取 session / work detail,并刷新草稿作品架,保证用户离开生成页后仍能在草稿 Tab 找到这份作品。
|
||||
3. 重新生成时优先使用当前 session 的 `draft.profileId` 或 `publishedProfileId`,不得重新创建 session;后端读取同一 profile 的 `generated_item_assets_json` 后,只补齐缺失图片或缺失模型的阶段。
|
||||
4. 已有 `status = model_ready` 且带 `modelSrc` / `modelObjectKey` 的素材视为完成,不再重复调用 Rodin。
|
||||
|
||||
抓大鹅结果页的基础信息自动保存继续调用 `PUT /api/runtime/match3d/works/{profileId}` 更新名称、题材、描述、标签、封面、消除数和难度;该保存不得清空 `generated_item_assets_json`。结果页 `3D素材` Tab 手动点击 `重新生成` 并拿到 GLB 下载文件后,必须把当前素材草稿重新序列化成 `generatedItemAssets` 并写回作品 profile;否则页面内预览会显示新模型,但试玩、发布和重进草稿仍会读取旧的空模型快照。SpacetimeDB `update_match3d_work` / `publish_match3d_work` 必须保留当前行的生成素材 JSON。
|
||||
|
||||
草稿架重进路径为:
|
||||
|
||||
```text
|
||||
草稿 Tab -> getMatch3DWorkDetail(profileId) -> Match3DResultView(profile.generatedItemAssets)
|
||||
```
|
||||
|
||||
因此 `map_match3d_work_summary_response` / `map_match3d_work_profile_response` 需要从 work profile snapshot 反序列化 `generated_item_assets_json` 并输出 `generatedItemAssets`。前端 `Match3DResultView` 的读取顺序为:有 `draft.generatedItemAssets` 时先用 draft 保留本次生成顺序和图片;同 `itemId` 在 `profile.generatedItemAssets` 中已有模型字段时,用 profile 模型字段补齐 draft;从草稿架重进没有 draft 时,用 `profile.generatedItemAssets`;两者都没有才回退到默认 3D 素材占位。
|
||||
|
||||
结果页 `作品信息` Tab 字段命名对齐拼图草稿:
|
||||
|
||||
1. `作品名称` 对应 Match3D `gameName`。
|
||||
2. `作品描述` 对应 Match3D `summary`,草稿生成默认空。
|
||||
3. `作品标签` 对应 Match3D `tags`,可由 AI 首次生成并允许用户继续编辑。
|
||||
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选上传入口,避免和作品基础信息割裂。
|
||||
|
||||
`3D素材` 详情页只保留:
|
||||
|
||||
1. 模型预览区:优先加载 `modelSrc` 对应 GLB,缺失时加载 `modelObjectKey`,支持拖动旋转;没有模型时展示空预览。
|
||||
2. 素材名称输入。
|
||||
3. `重新生成` 按钮。
|
||||
|
||||
详情页不再展示参考图、用途、提示词、文生/图生切换、状态查询、下载列表、taskUuid 或 subscriptionKey。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```powershell
|
||||
npm run check:encoding
|
||||
npm run test -- src\services\miniGameDraftGenerationProgress.test.ts
|
||||
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx
|
||||
npm run test -- src\components\match3d-runtime\Match3DRuntimeShell.test.tsx
|
||||
npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx
|
||||
npm run typecheck
|
||||
cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p spacetime-client match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p platform-oss --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
|
||||
```
|
||||
|
||||
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY`、`HYPER3D_API_KEY` 和 OSS 访问变量。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||
@@ -1,5 +1,11 @@
|
||||
# 抓大鹅 Match3D F1 创作入口与 Agent UI 落地记录 2026-04-30
|
||||
|
||||
> 2026-05-08 更新:抓大鹅创作端入口已重新开放,当前 `match3d.visible` 为 `true`。本文件记录 F1 接入能力,入口是否展示以 `NEW_WORK_ENTRY_CONFIG_2026-05-01.md` 和 `src/config/newWorkEntryConfig.ts` 为准。
|
||||
>
|
||||
> 2026-05-10 更新:抓大鹅入口页对齐拼图入口页,直接嵌入创作页模板 Tab。入口表单不再展示参考图、消除次数输入、难度数值滑杆和题材/物品/难度摘要框,仅保留题材主题大输入框和难度选项。难度选项负责派生 `clearCount` 与 `difficulty`,生成按钮必须展示 `消耗20光点`。
|
||||
>
|
||||
> 2026-05-10 补充:入口页新增 `3D素材风格` 横向滑动选择,首批风格参考图通过 VectorEngine `gpt-image-2-all` 生成并保存到 `public/match3d-style-references/`。最后一个选项为 `自定义`,点击后弹出独立面板填写画风描述。
|
||||
|
||||
## 1. 阶段边界
|
||||
|
||||
本文件承接《MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md》的 F1 包。
|
||||
@@ -28,19 +34,38 @@ badge: 可创建
|
||||
|
||||
入口来源统一走 `getVisiblePlatformCreationTypes()`,因此创作首页首屏卡带与“选择创作类型”弹层会同时出现抓大鹅。
|
||||
|
||||
## 4. Agent 工作区
|
||||
## 4. 入口表单工作区
|
||||
|
||||
新增 `Match3DAgentWorkspace`,复用通用 `CreationAgentWorkspace`。
|
||||
新增 `Match3DAgentWorkspace`,组件名继续兼容既有路由、草稿恢复和父层分流;当前主入口已从固定 Agent 追问收口为拼图式表单。
|
||||
|
||||
Agent 只收集三类锚点:
|
||||
创作页 `选择模板` Tab 中切换到 `抓大鹅` 时,直接渲染该表单,不创建会话,也不跳到独立工作台。点击生成后才创建 Match3D 会话并执行 `match3d_compile_draft`。
|
||||
|
||||
1. 题材主题。
|
||||
2. 需要消除次数。
|
||||
3. 难度。
|
||||
表单只展示三个输入块:
|
||||
|
||||
工作区支持参考图片上传入口。图片在 F1 中先以 Data URL 形式随消息 payload 带给 mock client;B5 接入后由后端 facade 替换为正式资产上传与引用。
|
||||
1. `想做一个什么题材的抓大鹅?`:大文本输入框,收集 `themeText`。
|
||||
2. `3D素材风格`:横向滑动风格卡,选择会写入 `assetStyleId`、`assetStyleLabel` 与 `assetStylePrompt`。
|
||||
3. `难度`:四个选项按钮,选项内部派生消除次数和难度数值。
|
||||
|
||||
UI 中不默认展示玩法规则长文,只展示进度、锚点、聊天内容和必要按钮。
|
||||
当前难度映射固定为:
|
||||
|
||||
```text
|
||||
轻松 -> clearCount 8, difficulty 2
|
||||
标准 -> clearCount 12, difficulty 4
|
||||
进阶 -> clearCount 16, difficulty 6
|
||||
硬核 -> clearCount 20, difficulty 8
|
||||
```
|
||||
|
||||
入口页不再上传参考图,提交 payload 中 `referenceImageSrc` 固定为 `null`。如果从旧会话或旧草稿恢复,前端只根据已有 `difficulty` 选择最接近的难度选项,并按当前选项重新派生 `clearCount` 与 `difficulty`。
|
||||
|
||||
内置风格选项为:
|
||||
|
||||
```text
|
||||
黏土手作 / 低多边形 / 玩具塑料 / 木质雕刻 / 体素积木 / 金属机甲 / 自定义
|
||||
```
|
||||
|
||||
自定义风格必须在弹出面板中填写描述后才能应用。入口表单必须在移动端创作页可视区内完成题材、风格、难度和生成按钮的展示,页面自身不产生纵向滚动;风格卡只允许横向滑动。
|
||||
|
||||
生成按钮文案为 `生成抓大鹅草稿`,按钮内必须同时展示 `消耗20光点`。UI 中不默认展示玩法规则长文,也不展示隐藏派生数值的摘要框。
|
||||
|
||||
## 5. mock client
|
||||
|
||||
@@ -82,10 +107,13 @@ POST /api/creation/match3d/sessions/:sessionId/compile
|
||||
|
||||
## 8. 验收口径
|
||||
|
||||
1. 创作首页能看到“抓大鹅 / 经典消除玩法”。
|
||||
2. 弹层选择“抓大鹅”能进入 Agent 工作区。
|
||||
3. 输入题材、消除次数、难度后进度到 `100%`。
|
||||
4. 点击“生成结果页”进入草稿承接页。
|
||||
5. 可从草稿承接页返回 Agent 修改。
|
||||
6. `npm run check:encoding` 通过。
|
||||
7. `npm run typecheck` 通过。
|
||||
1. 创作页 `选择模板` Tab 能看到 `抓大鹅 / 经典消除玩法`。
|
||||
2. 切换到 `抓大鹅` Tab 后,页面内直接显示抓大鹅入口表单,不提前创建会话。
|
||||
3. 表单不展示参考图、`需要消除次数`、`难度数值`、`题材`、`物品`、`难度`摘要框。
|
||||
4. 输入题材、选择风格和难度后,提交 payload 包含派生后的 `clearCount` 与 `difficulty`,`referenceImageSrc` 为 `null`,并包含 `assetStyleId`、`assetStyleLabel` 与 `assetStylePrompt`。
|
||||
5. 生成按钮展示 `消耗20光点`。
|
||||
6. 点击 `自定义` 风格弹出独立面板,填写后应用到提交 payload;未填写时不能应用空自定义风格。
|
||||
7. 移动端创作页内抓大鹅入口内容不产生纵向滚动,风格卡横向滑动。
|
||||
8. 点击生成后创建会话并进入草稿生成/结果页链路。
|
||||
9. `npm run check:encoding` 通过。
|
||||
10. `npm run typecheck` 通过。
|
||||
|
||||
75
docs/technical/MATCH3D_RODIN_ASSET_TAB_2026-05-10.md
Normal file
75
docs/technical/MATCH3D_RODIN_ASSET_TAB_2026-05-10.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 抓大鹅 Rodin 3D 素材 Tab 接入方案 2026-05-10
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于把抓大鹅结果页调整为多 Tab 工作台,并新增 `3D素材` Tab。该 Tab 专门服务抓大鹅玩法内物件、奖励、障碍等 3D 素材的 Rodin 生成试验。
|
||||
|
||||
本次复用已有 Hyper3D Rodin Gen-2 后端安全代理,不新增 SpacetimeDB 表;草稿生成链路会把 Rodin 模型转存到 OSS,并通过 Match3D 作品 profile 的 `generatedItemAssets` 恢复。不得把 Hyper3D API Key、上游下载 URL 或本地 Data URL 写成正式资产真相。
|
||||
|
||||
## 2. 页面结构
|
||||
|
||||
抓大鹅结果页拆为三个 Tab:
|
||||
|
||||
1. `作品信息`:承接封面、游戏名称、标签、简介。
|
||||
2. `玩法配置`:承接题材、消除次数、难度、参考图与局内数量摘要。
|
||||
3. `3D素材`:展示 Rodin 素材列表,点击列表项进入详情生成页。
|
||||
|
||||
`3D素材` Tab 的列表布局参考 RPG 结果页角色列表:移动端纵向卡片,桌面端多列紧凑卡片;卡片展示素材名和状态。详情页只保留模型预览、素材名称和 `重新生成` 按钮。
|
||||
|
||||
## 3. Rodin 任务边界
|
||||
|
||||
前端只维护当前页面内的临时重新生成任务状态;草稿生成得到的正式模型资产从 `generatedItemAssets.modelSrc` 恢复:
|
||||
|
||||
1. 素材槽位名称。
|
||||
2. 模型预览。草稿生成的 `/generated-match3d-assets/...` GLB 必须通过同源 `/api/assets/read-bytes` 由后端换签并读取字节,前端再转成 Blob URL 后交给 Three.js GLTFLoader,避免浏览器直接 `fetch` OSS 签名 URL 时被 CORS 拦截。
|
||||
3. 图生模型参考图只作为重新生成的隐藏输入来源,不在详情页展示。上传图片在前端直接读成 Data URL;草稿生成的 `/generated-match3d-assets/...` 图片必须通过 `/api/assets/read-bytes` 转成 Data URL 后提交给 Hyper3D。
|
||||
4. Hyper3D `taskUuid` 与 `subscriptionKey` 仅用于重新生成过程,不在详情页展示。
|
||||
5. 查询到的状态、进度与下载文件列表仅作为内部状态,不在详情页展示。
|
||||
|
||||
正式资产链后续再接:
|
||||
|
||||
1. 下载文件转存 OSS。
|
||||
2. `asset_object` 确认。
|
||||
3. `asset_entity_binding` 绑定到 Match3D 作品或物件槽位。
|
||||
|
||||
首版不得把上游下载 URL 直接写入 Match3D profile,也不得把 Data URL 持久化到 SpacetimeDB。
|
||||
|
||||
## 4. 接口复用
|
||||
|
||||
继续使用既有前端 client:
|
||||
|
||||
```text
|
||||
src/services/hyper3dModelGenerationService.ts
|
||||
```
|
||||
|
||||
对应后端路由:
|
||||
|
||||
```text
|
||||
POST /api/assets/hyper3d/text-to-model
|
||||
POST /api/assets/hyper3d/image-to-model
|
||||
POST /api/assets/hyper3d/status
|
||||
POST /api/assets/hyper3d/download
|
||||
```
|
||||
|
||||
请求参数默认使用:
|
||||
|
||||
```text
|
||||
geometryFileFormat = glb
|
||||
material = PBR
|
||||
quality = medium
|
||||
meshMode = Quad
|
||||
previewRender = true
|
||||
conditionMode = concat
|
||||
```
|
||||
|
||||
## 5. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```powershell
|
||||
npm run check:encoding
|
||||
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
真实 Rodin 生成会消耗 Hyper3D Credit,只在本地私密环境配置 `HYPER3D_API_KEY` 或 `RODIN_API_KEY` 后手动验证。
|
||||
@@ -19,7 +19,7 @@
|
||||
1. 现有 `Match3DVisualIcon`、`Match3DToken` 和托盘 2D 图案渲染代码必须保留。
|
||||
2. 新增 3D 表现层只作为运行态棋盘的可选渲染分支。
|
||||
3. 当浏览器不支持 WebGL、3D 依赖加载失败或实验开关关闭时,运行态必须自动回到现有 2D 图案表现。
|
||||
4. 托盘继续使用当前 2D 图标,便于玩家识别已选物品,也便于实验失败时快速回滚。
|
||||
4. 3D 模式下,托盘直接复用场内同一套程序化 3D 模型,以固定斜 `45` 度识别视角展示已选物品;托盘内物品不进入物理世界,不参与碰撞。WebGL 不可用或实验回退时,托盘继续使用当前 2D 图标。
|
||||
|
||||
## 3. 工程落点
|
||||
|
||||
@@ -50,7 +50,7 @@ cannon-es
|
||||
|
||||
3D 分支只读取后端快照中的物品坐标、层级、可点击状态和视觉键。物理碰撞、轻微堆叠和几何体姿态只作为前端表现层,不改变消除规则、备选栏规则、胜负判定或最终权威快照。
|
||||
|
||||
`match3dVisualAssets.tsx` 保留 2D 纯色几何图案映射,运行态托盘继续使用该 2D 图标;`match3dRuntimePresentation.ts` 收口显示层坐标和状态兼容,避免异常旧坐标把 2D 或 3D 物体推到圆形边界外。
|
||||
`match3dVisualAssets.tsx` 保留 2D 纯色几何图案映射,运行态托盘在 3D 模式下通过 `Match3DTrayPreviewBoard` 使用单个共享 WebGL 预览层复用 `createMatch3DItemMesh` 生成同款 3D 模型,不能为每个托盘格单独创建 `WebGLRenderer`。托盘预览层必须按实际容器宽高更新正交相机,并把每个模型放入独立 pivot 后再沿相机屏幕横轴定位到对应格子中心;托盘预览不能把所有模型统一缩放到同一外观尺寸,必须保留场内相对尺寸差异,否则会让点击后入槽的模型和场内物件对应关系失真。WebGL 不可用或 2D 回退时继续使用该 2D 图标;`match3dRuntimePresentation.ts` 收口显示层坐标和状态兼容,避免异常旧坐标把 2D 或 3D 物体推到圆形边界外。
|
||||
|
||||
## 4. 验收口径
|
||||
|
||||
@@ -58,8 +58,10 @@ cannon-es
|
||||
2. 3D 几何体保持在圆形区域内,不被圆形边界裁切到不可点。
|
||||
3. 物体进入场景后有轻微物理碰撞和堆叠稳定过程。
|
||||
4. 点击 3D 物体后仍执行原有乐观入槽、后端确认、三消反馈和结算。
|
||||
5. 单元测试仍覆盖 2D 回退图案,确保回退路径没有被删除。
|
||||
6. 390px 移动端与桌面端均不能出现横向溢出,顶部状态、圆形棋盘和 7 格备选栏都要完整可见。
|
||||
5. 被取出的 3D 物体必须立即从棋盘物理世界移除;备选栏展示的是无碰撞、固定角度的独立预览模型,不允许继续受场内碰撞、重力或堆叠影响。
|
||||
6. 托盘 3D 预览必须共享一个 renderer,避免多个 WebGL 上下文导致中心棋盘上下文被浏览器回收;中心棋盘监听 `webglcontextlost`,丢失时自动回退 2D 表现,禁止出现模型不可见但仍可点击的状态。
|
||||
7. 单元测试仍覆盖 2D 回退图案,确保回退路径没有被删除。
|
||||
8. 390px 移动端与桌面端均不能出现横向溢出,顶部状态、圆形棋盘和 7 格备选栏都要完整可见。
|
||||
|
||||
## 5. 锅型容器优化
|
||||
|
||||
@@ -72,3 +74,202 @@ cannon-es
|
||||
3. 物理世界使用同一个锅内半径作为水平活动边界,所有可消除物体的初始位置和运行中位置都必须被约束在圆形锅内。
|
||||
4. 物体受到重力后只允许在锅内碰撞、滑动、翻滚和向上堆叠,不能因为碰撞或初始坐标散落到圆形区域外。
|
||||
5. 该优化仍只属于前端 3D 表现层,不改变后端运行态坐标、点击权威判定、备选栏、消除和胜负规则。
|
||||
|
||||
## 6. 中心引力优化
|
||||
|
||||
2026-05-02 追加中心引力,用来解决高消除次数下 3D 物体过于松散、贴边后被圆形场地裁切的问题。体验后发现默认向心力会让模型过度挤压成团,因此当前先关闭默认引力,只保留代码开关,后续如需再尝试可重新调参。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 中心引力默认系数为 `0`,默认不对物理 body 施加水平向心力。
|
||||
2. 引力只作用在 X/Z 平面,不改变垂直重力,物体仍会自然落到锅底或堆叠在其他物体上。
|
||||
3. 引力在越靠近锅边时越明显,避免大量物体碰撞后形成稀疏外环;靠近中心时力度收敛,避免所有物体被吸成单点。
|
||||
4. 锅内活动边界继续作为硬约束;高数量物体应被锅边挡住并向上堆叠,不允许散落到圆形场地外。
|
||||
5. `/match3d?clearCount=100` 可作为本地直达压力测试入口,用于验证 300 个物体时仍在锅内聚拢。
|
||||
|
||||
## 7. 正交俯视与真实场地边界
|
||||
|
||||
2026-05-02 针对高堆叠时 3D 物体被 DOM 圆形裁切的问题,明确中心圆形区域不是裁切蒙版,而是游戏实际游玩场地。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 3D 棋盘使用正交俯视相机,避免高处物体因为透视放大而投影到圆形场地外。
|
||||
2. 圆形场地的内圈圆环对应 3D 世界里的锅内空气墙,物体范围由物理约束控制,不再依赖 DOM `overflow-hidden` 裁切。
|
||||
3. 外层圆形 UI 只负责显示锅沿和场地外观,不能把物体裁成半截;如果物体看起来越界,优先修正相机、物理半径和空气墙。
|
||||
4. 高数量压力测试以 `/match3d?clearCount=100` 为基准,物体可以在场地内向上堆叠,但不能被圆形边缘压住或切掉。
|
||||
|
||||
## 8. 类型数量与样式池历史口径
|
||||
|
||||
2026-05-03 曾调整消除物类型生成规则,解决 3D 关卡中可消除物类型和样式过少的问题。该节为历史口径,后续实际实现以第 11 节 25 个积木件资源池为准。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 历史版本曾使用 20 类形状颜色组合。
|
||||
2. 当前版本已替换为 25 个积木件,旧 20 类上限不再作为编码依据。
|
||||
3. 3D 与 2D 回退仍共用视觉键映射,新增样式不能破坏 `?match3dRender=2d` 回退路径。
|
||||
|
||||
## 9. 特殊形状 3D 可读性修正
|
||||
|
||||
2026-05-03 针对 20 组关卡中看不到十字、圆环、盾形、闪电、月牙、箭头等新形状的问题,补充 3D 几何体渲染口径。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 数据层仍使用 `visualKey` 决定类型,不新增贴图素材或文本标识。
|
||||
2. 十字、心形、星形、圆环、盾形、闪电、月牙、箭头、V 形等特殊形状不能继续使用普通盒子、球体或锥体代理,必须生成俯视角可辨认的 3D 轮廓。
|
||||
3. 特殊形状使用 Three.js 程序化轮廓挤出生成,保持当前 3D 实验可快速回退,不影响现有 2D 图案分支。
|
||||
4. 特殊形状的物理碰撞可以继续使用近似碰撞体,但显示网格需要固定为俯视可读姿态,避免落地翻滚后又变成长方块或普通三角体。
|
||||
5. 当前特殊形状已被 25 个积木件资源池替换;不能为了让玩家开局肉眼看到全部类型而改动初始层级、物理堆叠、遮挡、边界或可点击规则。
|
||||
|
||||
## 10. 15 组中档局面的类型唯一性修正
|
||||
|
||||
2026-05-03 针对 `clearCount=15` 时可消除物类型不足 15 种的问题,补充中档局面的规则验收口径。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. `clearCount=15` 时,运行态数据中必须生成 `15` 种不同 `itemTypeId`,且首个 `15` 个 `visualKey` 必须分别对应不同几何形状。
|
||||
2. 每种 `itemTypeId` 在 `clearCount=15` 时只对应 1 次消除目标,即恰好生成 `3` 件物体;同一种视觉模型在同局中不应出现超过 3 件。
|
||||
3. 不为了展示 15 种而修改初始层级、物理堆叠、遮挡、边界或可点击规则;被盖住、堆叠和局部不可见是正常玩法效果。
|
||||
4. 当前版本已改为第 11 节的 `itemTypeCount = clearCount <= 25 ? clearCount : 25` 规则。
|
||||
|
||||
## 11. 25 个积木件资源池替换
|
||||
|
||||
2026-05-03 根据新的参考图,把可消除物体替换为 25 个积木件类型,并调整本局类型抽取规则。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 默认 `visualKey` 资源池改为 25 个积木件,覆盖长条、短条、2x2、2x3、2x4、1x1、光板、斜坡、圆柱、透明圆环、拱门和锥形件等差异化模型。
|
||||
2. 前端 3D 表现继续使用 Three.js 程序化几何体生成,不引入外部贴图或 GLB;托盘和 2D 回退继续使用同一批 `visualKey` 的简化图标。
|
||||
3. `clearCount <= 25` 时,本局从 25 个类型中按确定性随机顺序抽取 `clearCount` 种类型,不允许同局刷新重复类型。
|
||||
4. `clearCount > 25` 时,本局最多使用 25 种类型,额外消除组在这 25 种中轮转复用;每种类型最终数量仍必须是 3 的倍数。
|
||||
5. 该随机抽取只决定本局使用哪些类型和使用顺序,不改变物理堆叠、遮挡、边界、可点击判定、备选栏和胜负规则。
|
||||
6. 前端本地试玩、创作后试玩和后端权威运行态必须使用同一套 `itemTypeCount = clearCount <= 25 ? clearCount : 25` 口径。
|
||||
|
||||
## 12. 五档体积规则
|
||||
|
||||
2026-05-03 追加可消除物模型大小规则,把每局可消除物按五档相对体积分配。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. M 型作为标准体积 `1.00`。
|
||||
2. XL 型相对体积范围为 `1.60~2.30`,占本局可消除类型数的 `20%`。
|
||||
3. L 型相对体积范围为 `1.25~1.60`,占本局可消除类型数的 `30%`。
|
||||
4. M 型相对体积固定为 `1.00`,占本局可消除类型数的 `30%`。
|
||||
5. XS 型相对体积范围为 `0.65~0.85`,占本局可消除类型数的 `15%`。
|
||||
6. S 型相对体积范围为 `0.35~0.50`,占本局可消除类型数的 `5%`。
|
||||
7. 本局使用类型数仍按第 11 节计算,即 `clearCount <= 25 ? clearCount : 25`。比例遇到非整数时按最大余数补齐,确保五档数量之和等于本局使用类型数。
|
||||
8. 体积档位分配绑定到本局选中的 `visualKey`,同一局内同一个颜色和造型只能有一个尺寸档位和一个半径;当 `clearCount > 25` 轮转复用类型时,复用的同一 `visualKey` 继续沿用同一尺寸。
|
||||
9. 前端本地试玩、创作后试玩和后端权威运行态必须使用同一套五档体积分配口径。
|
||||
|
||||
## 13. 可点击物整体显示倍率
|
||||
|
||||
2026-05-04 追加一轮点击手感优化,解决当前玩家点击可消除物偏困难的问题。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 运行态表现层使用 `MATCH3D_RENDER_ITEM_SCALE = 2`,把后端快照中的 `item.radius` 统一乘 `2` 后再进入显示层坐标收束。
|
||||
2. 该倍率只影响前端 2D 回退图标、3D 场内模型、碰撞体、射线点击命中区域和托盘 3D 预览测量。
|
||||
3. 五档体积规则、每局类型数量、每种物品的唯一尺寸关系、后端权威快照和消除判定不做变化;所有物体之间的相对大小比例保持不变。
|
||||
4. 放大后的物体仍必须通过圆形场地显示层收束和 3D 锅内空气墙约束,不允许重新依赖 DOM 圆形裁切。
|
||||
5. 2026-05-04 追加修正:碰撞体必须和当前视觉模型使用同一套尺寸公式。长条、光板、斜坡、圆柱、圆环、拱门和锥形件不能再只按 `shape + radius` 粗略生成统一碰撞体;不得借此调整整体显示倍率、点击倍率、锅体尺寸或物理步进。
|
||||
|
||||
## 14. 两位数消除局的点击命中与旧模型复用修正
|
||||
|
||||
2026-05-05 针对 `clearCount >= 10` 时出现“点击到的 3D 模型和下方备选栏模型不一致”的问题,补充运行态复用口径。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 运行态 3D 棋盘的物理条目不能只按 `itemInstanceId` 复用,还必须结合 `runId`、`itemTypeId`、`visualKey`、`radius` 和 `layer` 生成当前渲染签名。
|
||||
2. 当同一个 `itemInstanceId` 出现在新的 run 快照里,但渲染签名已经变化时,旧 mesh 和 body 必须先销毁,再按当前快照重建。
|
||||
3. 这条修正只针对前端 3D 表现层,不改变后端权威快照、点击判定、备选栏规则和三消规则。
|
||||
4. 底部备选栏预览继续沿用按当前 run 快照重建的视觉键,不允许把上一局的旧 3D 资源误复用到新一局。
|
||||
|
||||
## 15. 备选栏 3D 模型可读性优化
|
||||
|
||||
2026-05-05 针对备选栏中的 3D 模型偏小、部分积木件难以辨认的问题,补充 UI 预览层展示口径。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 备选栏 3D 预览可以使用比场内更紧凑的显示尺度,让模型在单格 UI 中占用更大可读面积。
|
||||
2. 托盘相机和模型姿态只服务 UI 识别;当前采用偏强的俯视 `3/4` 立体角,并通过更明显的光照对比突出顶面与侧面差异,避免退化成纯平面旋转。
|
||||
3. 该调整不能改变中心场地 3D 模型的物理姿态、碰撞体、点击判定和后端权威快照。
|
||||
4. 托盘仍使用共享 `WebGLRenderer`,继续按当前 `visualKey` 和尺寸关系生成同款模型;不得新增每格独立 renderer。
|
||||
5. 托盘缩放不能继续只按本局最大模型统一压缩所有物体;小尺寸模型需要保留最低可读显示尺寸,但仍不能改动场内真实尺寸、碰撞尺寸和后端权威尺寸。
|
||||
6. 备选栏单格高度可大于宽度,优先保证局内 3D 预览的识别面积;不得为了适配旧正方形格子把模型再次压小。
|
||||
|
||||
## 16. 中心场地隐藏纵深与动态上顶
|
||||
|
||||
2026-05-05 针对中心场地高数量局面穿模严重、消除后中下层物体长期陷在深处的问题,追加隐藏纵深与动态上顶表现修正。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 该纵深只存在于 3D 物理表现层,不修改锅体图案、锅壁模型、托盘表现、后端快照、点击权威判定、消除和胜负规则。
|
||||
2. 物体生成高度不再使用固定极小层级步长,而是按本局总物体数计算一个隐藏初始纵深;物体总量越大,初始逻辑纵深越深,用来减少大量放大后模型被挤进同一高度区间导致的穿模。
|
||||
3. 当前剩余场内物体数会动态缩短可用纵深;随着玩家持续消除,下层物体的目标高度逐步上移,表现为中下层物体陆续向上顶到表面层。
|
||||
4. 动态上顶只通过向上托举力和目标高度调整完成,不增加中心引力,不修改水平约束半径,不改变碰撞体尺寸倍率。
|
||||
5. 表面层高度保持稳定,避免越消除越显得物体掉进深处或视觉尺寸异常变小。
|
||||
|
||||
## 17. 高数量局面物理稳定与动态锅容量
|
||||
|
||||
2026-05-06 继续按方案 C 和方案 D 优化 `clearCount=100` 等高数量局面的稳定性。
|
||||
|
||||
方案 C 编码口径:
|
||||
|
||||
1. 只调整 3D 表现层的物理稳定参数,包括求解迭代次数、接触摩擦、接触弹性、线性阻尼、角阻尼、睡眠阈值和速度上限。
|
||||
2. 物体数量越大,物理世界越偏向高摩擦、低弹性和更强阻尼,减少大量物体同时生成后的持续弹跳、穿插和边界挤压。
|
||||
3. 速度保护只限制极端水平速度和垂直速度,不改物体位置生成规则、点击判定、备选栏、消除和胜负规则。
|
||||
|
||||
方案 D 编码口径:
|
||||
|
||||
1. 隐藏锅容量的纵深按本局总物体数,也就是用户配置的消除次数乘 `3` 后动态计算;消除次数越大,锅内容量纵深越深。
|
||||
2. 动态纵深只影响 3D 物理层的生成高度、目标层高度和消除后的上顶回补;锅底、锅壁、锅沿和 DOM 场地外观不随纵深变化。
|
||||
3. 高数量局面需要降低单层容量,让更多物体分散到纵向层级中,避免 `300` 个物体被压进少量高度层。
|
||||
4. 随着消除进度推进,当前可用纵深继续按剩余物体数收缩,确保下层物体逐步向表面回补,保持中心场地表层稳定可见。
|
||||
5. 本节不改变中心引力默认值,不改水平活动半径,不改碰撞体与视觉模型的尺寸一致性规则。
|
||||
|
||||
## 18. 原型入场节奏与创建限流
|
||||
|
||||
2026-05-07 根据原型视频补充创建过程优化。原型不是在同一帧把全部物体摆进容器,而是先短暂空场,再用连续小批量把物体投放到容器中,批与批之间留出自然沉降时间,最后再进入可操作局面。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 该优化只作用于前端 3D 表现层的物理 body 创建节奏,不改变后端快照、消除目标数量、点击权威判定、备选栏、三消和胜负规则。
|
||||
2. `totalItemCount < 30` 时保留较快创建节奏;`30 <= totalItemCount <= 50` 进入中速波次投放,降低每波数量并增加波次沉降窗口,避免最后一层物体压进尚未稳定的表层堆叠。
|
||||
3. `totalItemCount > 50` 后进入更强限流投放,单帧创建数量下降,避免同一帧把过多碰撞体塞入物理世界。
|
||||
4. 随着总物体数增加,投放初始等待、层级间隔和同层错峰间隔都要逐步变长,模拟原型中“持续落入、短暂沉降、继续补入”的节奏。
|
||||
5. `clearCount=100` 对应 `300` 个物体时,投放节奏应接近连续数秒完成,而不是在一秒左右完成全量创建。
|
||||
6. 该节不允许通过缩小碰撞体、扩大锅半径、开启中心引力或修改模型尺寸来掩盖穿模;如果后续仍需调整,只继续围绕创建节拍和物理沉降窗口处理。
|
||||
|
||||
## 19. 生成高度避让已有堆叠
|
||||
|
||||
2026-05-07 继续按方案 2 优化 `30` 件左右局面最后一层或最后一波物体仍会穿进已有堆叠的问题。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 该优化只作用于前端 3D 表现层的新物体创建高度,不改变后端快照、物品数量、模型尺寸、碰撞体尺寸、锅半径、点击判定、备选栏、三消和胜负规则。
|
||||
2. 新物体进入物理世界前,先根据当前同一水平区域附近已有物体的碰撞体顶部高度,计算一个不低于原计划高度的生成高度。
|
||||
3. 只有水平外接半径发生重叠的已有物体会影响本次生成高度;远处物体不能把新物体整体抬高,避免破坏原有随机洒落和分层节奏。
|
||||
4. 该避让只解决“直接创建在已有模型内部”的初始穿插,后续沉降、翻滚、堆叠仍交给 cannon-es 物理模拟。
|
||||
5. 本节不允许额外引入中心引力、扩大锅容量或修改模型生成规则;若后续仍需优化,只继续围绕生成高度、入场节拍和沉降窗口做局部迭代。
|
||||
|
||||
## 20. 从小到大的生成动画
|
||||
|
||||
2026-05-08 追加生成动画优化,参考原型中物体逐个出现、从小到大补入容器的观感。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 该优化只作用于前端 3D 表现层的可见 mesh 缩放,不改变后端快照、碰撞体尺寸、物品数量、锅半径、点击判定、备选栏、三消和胜负规则。
|
||||
2. 物理 body 在创建时仍使用最终尺寸碰撞体,并立即加入 cannon-es 物理世界,确保生成动画过程中碰撞已经按完整体积稳定占位。
|
||||
3. 可见 mesh 初始以较小比例显示,再用缓动动画放大到完整尺寸;视觉缩放不得反向修改 body shape、质量、边界半径或生成高度避让计算。
|
||||
4. 入场动画继续服从第 18 节的创建限流和第 19 节的生成高度避让;不能为了动画效果把物体直接放进已有堆叠内部。
|
||||
5. 动画结束后 mesh 缩放必须回到 `1`,避免影响后续点击可读性和托盘对应关系。
|
||||
|
||||
## 21. 高 DPR 移动端 WebGL 画布尺寸锁定
|
||||
|
||||
2026-05-10 针对移动端试玩入口中 3D 锅体和物体从中心区域向右下溢出的问题,补充 WebGL canvas 布局口径。
|
||||
|
||||
编码口径:
|
||||
|
||||
1. 中心 3D 棋盘和托盘 3D 预览都允许通过 `renderer.setPixelRatio(...)` 提升绘制清晰度,但 `WebGLRenderer.domElement` 的 CSS 显示尺寸必须单独锁定为 `position: absolute; inset: 0; width: 100%; height: 100%; display: block`。
|
||||
2. `renderer.setSize(width, height, false)` 只负责绘图缓冲区与当前容器尺寸同步,不能依赖 canvas 默认属性尺寸承载 CSS 布局;否则高 DPR 设备会把绘图缓冲区尺寸当成页面尺寸显示,导致棋盘内容放大并从容器右下溢出。
|
||||
3. 移动端验收仍以 `390px` 竖屏为基准:顶部状态、圆形棋盘和 `7` 格备选栏都必须完整可见,锅体内容应落在屏幕中间的圆形区域内。
|
||||
4. 该修复只影响 WebGL 画布 CSS 布局,不改变相机、物理世界、碰撞体、点击命中、后端权威快照或托盘规则。
|
||||
|
||||
@@ -2,32 +2,39 @@
|
||||
|
||||
## 背景
|
||||
|
||||
创作中心顶部“新建作品”入口和平台创作类型弹层都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在 `src/components/platform-entry/platformEntryCreationTypes.ts` 与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。
|
||||
创作中心的模板 Tab、平台创作类型弹层和旧“新建作品”卡片配置都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在前端配置与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。当前入口配置事实源已经迁移到 SpacetimeDB,由 `api-server` 通过 `GET /api/creation-entry/config` 下发。
|
||||
|
||||
## 落地规则
|
||||
|
||||
1. 新建作品入口配置统一放在 `src/config/newWorkEntryConfig.ts`。
|
||||
2. `visible` 控制玩法是否展示在新建作品入口和创作类型弹层中。
|
||||
3. `open` 控制玩法是否允许点击创建;`open: false` 时入口保持展示但禁用。
|
||||
1. 新建作品入口配置统一存放在 SpacetimeDB 的 `creation_entry_config` / `creation_entry_type_config` 表;默认种子位于 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`。
|
||||
2. `visible` 控制玩法是否展示在创作 Tab 模板入口、新建作品入口和创作类型弹层中。
|
||||
3. `open` 控制玩法是否允许点击创建以及对应创作 / runtime API 路由是否放行;`open: false` 时入口保持展示但禁用,并由 `api-server` 熔断对应玩法 API。
|
||||
4. `title`、`subtitle`、`badge` 控制玩法卡片文案。
|
||||
5. `startCard` 控制创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案。
|
||||
5. `startCard` 控制旧创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案;当前创作 Tab 首屏标题固定在 `PlatformEntryFlowShellImpl.tsx`,不再由 `startCard` 控制。
|
||||
6. `typeModal` 控制平台创作类型弹层标题和描述。
|
||||
7. 入口排序仍遵循“可创建玩法在前,未开放玩法在后”;同组内部沿用配置顺序。
|
||||
8. `creative-agent` 可以继续保留运行链路,但默认 `visible: false`,不出现在创作 Tab 模板入口。
|
||||
9. 前端 `src/components/platform-entry/platformEntryCreationTypes.ts` 只做展示派生,不再承载默认入口配置。
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 玩法 | 展示 | 开放 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 角色扮演 | 是 | 是 | 点击后进入 RPG Agent 共创工作台 |
|
||||
| 角色扮演 | 否 | 是 | 暂时从创作端入口下线,既有链路与作品能力保留 |
|
||||
| 大鱼吃小鱼 | 否 | 是 | 功能仍保留,不在新建作品入口展示 |
|
||||
| 拼图 | 是 | 是 | 点击后进入拼图 Agent 共创工作台 |
|
||||
| 抓大鹅 | 是 | 是 | 点击后进入抓大鹅 Agent 共创工作台 |
|
||||
| 拼图 | 是 | 是 | 创作 Tab 默认选中并内嵌展示拼图创作表单,提交后进入拼图草稿生成 |
|
||||
| 抓大鹅 | 是 | 是 | 点击后进入抓大鹅 Match3D Agent 共创工作台 |
|
||||
| 方洞挑战 | 否 | 是 | 创作页入口暂时完全隐藏,既有草稿、结果页、发布、试玩、作品架与广场链路保留 |
|
||||
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待,暂不允许创建视觉小说草稿 |
|
||||
| 智能创作 | 否 | 是 | 入口隐藏,既有 `creative-agent` 链路保留 |
|
||||
|
||||
## 验收
|
||||
|
||||
1. 修改 `src/config/newWorkEntryConfig.ts` 后,创作中心顶部卡带和平台创作类型弹层应同步变化。
|
||||
1. 修改 SpacetimeDB 入口配置后,创作 Tab 模板入口、创作中心顶部卡带和平台创作类型弹层应同步变化。
|
||||
2. 隐藏玩法不触发入口预加载,也不出现在新建作品入口中。
|
||||
3. 未开放玩法点击态保持禁用,不应进入鉴权或创建会话链路。
|
||||
4. 已开放玩法点击后必须进入对应创建链路;若用户未登录,先走登录保护。
|
||||
5. 创作 Tab 首屏应显示“10分钟创作一个精品互动玩法”,并默认展示拼图创作表单。
|
||||
6. 智能创作入口隐藏后,不应出现“Hi, 朋友”“问一问百梦”或“一句话生成闪应用”等旧首页入口。
|
||||
7. 方洞挑战入口隐藏后,不应出现在创作 Tab 模板入口、创作中心顶部卡带、平台创作类型弹层和创作页作品架中;既有 `SH-` 作品号、广场详情和试玩 runtime 链路不因此删除。
|
||||
|
||||
@@ -76,3 +76,16 @@
|
||||
3. 只有用户显式修改或重置密码后,才允许密码登录。
|
||||
|
||||
后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力,验证码登录 reducer 承担“无账号则自动注册”的唯一注册入口。
|
||||
|
||||
## 5. 2026-05-12 快照同步修复
|
||||
|
||||
重置密码和修改密码都会改变认证真相:`password_hash`、`password_login_enabled`、`token_version`,重置密码还会立即创建新的 refresh session。因此 API 层在 `POST /api/auth/password/change` 与 `POST /api/auth/password/reset` 成功后,必须和密码登录、手机号登录、刷新、退出一样调用 `sync_auth_store_snapshot_to_spacetime()`。
|
||||
|
||||
若只更新本地 `InMemoryAuthStore` 而不同步 SpacetimeDB 认证快照,`api-server` 重启时可能从旧的 SpacetimeDB 表或旧快照恢复账号状态,表现为用户已通过忘记密码重设成功,但再次密码登录仍返回“手机号或密码错误”。启动恢复时应从 SpacetimeDB 表、SpacetimeDB 快照记录和本地 `GENARRATIVE_AUTH_STORE_PATH` 文件中选择可判断的最新快照;当本地文件更新且远端表无更新时间戳时,优先使用本地文件并尝试回写 SpacetimeDB,避免旧远端状态覆盖刚重设的密码。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cargo test -p module-auth password --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server password --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
@@ -104,9 +104,11 @@
|
||||
|
||||
1. 发送验证码调用 `SendSmsVerifyCode`。
|
||||
2. 校验验证码调用 `CheckSmsVerifyCode`。
|
||||
3. 使用阿里云 RPC 签名口径:
|
||||
- `SignatureMethod=HMAC-SHA1`
|
||||
- `SignatureVersion=1.0`
|
||||
3. 使用阿里云 OpenAPI V3 请求头签名口径:
|
||||
- `Authorization: ACS3-HMAC-SHA256 ...`
|
||||
- `x-acs-action`
|
||||
- `x-acs-version`
|
||||
- `x-acs-content-sha256`
|
||||
4. 当前仍只支持中国大陆手机号。
|
||||
|
||||
## 7. 状态与快照
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 手机验证码短信 Provider 错误 HTTP 映射修复
|
||||
|
||||
日期:`2026-05-08`
|
||||
|
||||
## 背景
|
||||
|
||||
本地登录弹窗点击手机号验证码登录时,浏览器报:
|
||||
|
||||
```text
|
||||
POST /api/auth/phone/login 500
|
||||
```
|
||||
|
||||
排查发现当前 `.env.local` 使用:
|
||||
|
||||
```text
|
||||
SMS_AUTH_PROVIDER=aliyun
|
||||
```
|
||||
|
||||
因此 `send-code` 会走真实阿里云短信 provider。真实 provider 返回 `UNKNOWN` 或 `biz.FREQUENCY / check frequency failed` 时,`module-auth` 曾把 provider 失败统一折叠成 `PhoneAuthError::Store`,`api-server` 再映射为 `500 Internal Server Error`,前端只能看到登录失败。
|
||||
|
||||
## 根因
|
||||
|
||||
短信 provider 失败不是认证仓储内部错误:
|
||||
|
||||
1. 阿里云配置缺失或配置非法属于服务配置问题。
|
||||
2. 阿里云返回频控、网关失败或业务失败属于上游短信 provider 问题。
|
||||
3. 这些错误不应被映射成 `Store`,否则 HTTP 层无法区分真实内部错误与外部 provider 失败。
|
||||
|
||||
## 修复
|
||||
|
||||
`module-auth` 新增短信 provider 错误分类:
|
||||
|
||||
1. `PhoneAuthError::SmsProviderInvalidConfig`
|
||||
2. `PhoneAuthError::SmsProviderUpstream`
|
||||
|
||||
`api-server` 映射规则调整为:
|
||||
|
||||
1. provider 配置错误返回 `503 Service Unavailable`
|
||||
2. provider 上游失败返回 `502 Bad Gateway`
|
||||
3. 验证码不存在、错误、过期仍返回 `400`
|
||||
4. 本地仓储或签发错误仍返回 `500`
|
||||
|
||||
## 本地排查
|
||||
|
||||
如果本地只想验证登录 UI 和账号链路,可以临时用 shell 环境覆盖真实短信 provider:
|
||||
|
||||
```powershell
|
||||
$env:SMS_AUTH_PROVIDER="mock"
|
||||
npm run api-server
|
||||
```
|
||||
|
||||
若要验证真实短信链路,保持 `SMS_AUTH_PROVIDER=aliyun`,并查看 `api-server` 日志中的:
|
||||
|
||||
1. `阿里云短信发送接口返回响应`
|
||||
2. `阿里云短信发送接口返回业务失败`
|
||||
3. `手机号验证码发送失败`
|
||||
|
||||
看到 `biz.FREQUENCY` / `check frequency failed` 时,说明请求已到达短信 provider,但被 provider 频控或业务规则拒绝。
|
||||
|
||||
## 验收
|
||||
|
||||
1. `cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml`
|
||||
2. `cargo test -p api-server send_phone --manifest-path server-rs/Cargo.toml`
|
||||
3. `cargo test -p api-server phone_login_creates_user_and_sets_refresh_cookie --manifest-path server-rs/Cargo.toml`
|
||||
4. `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
5. `npm run check:encoding`
|
||||
@@ -17,6 +17,7 @@
|
||||
- `scripts/deploy/maintenance-status.sh`
|
||||
- `scripts/build-production-release.sh`
|
||||
- `scripts/jenkins-checkout-source.sh`
|
||||
- `scripts/jenkins-server-provision.sh`
|
||||
- `scripts/deploy/production-web-deploy.sh`
|
||||
- `scripts/deploy/production-api-deploy.sh`
|
||||
- `scripts/deploy/production-stdb-publish.sh`
|
||||
@@ -93,6 +94,21 @@
|
||||
|
||||
`api-server` 不放入 Docker,也不直接暴露公网端口。发布时替换版本目录并重启 `genarrative-api.service`。
|
||||
|
||||
全量发布流水线的 `DATABASE` 参数必须同时传给 Stdb 发布和 API 发布:Stdb 发布负责把 wasm 发布到目标数据库,API 发布必须在重启 `genarrative-api.service` 前把同一个库名写入 `/etc/genarrative/api-server.env` 的 `GENARRATIVE_SPACETIME_DATABASE`,并同步 `GENARRATIVE_SPACETIME_SERVER_URL`。否则 api-server 会继续读取环境文件中的旧库名,出现 wasm 已发布到新库但 HTTP facade 仍访问旧库的错位。
|
||||
|
||||
API 发布阶段只使用上游 API 构建产物,不应回退到上游源码 commit 执行部署脚本;部署脚本应始终取 `SOURCE_BRANCH` 最新提交。否则全量流水线在修复部署脚本后仍可能按旧 `COMMIT_HASH` checkout,继续执行不认识新参数的旧版 `production-api-deploy.sh`。
|
||||
|
||||
### 服务器配置流水线
|
||||
|
||||
`Genarrative-Server-Provision` 的 Jenkinsfile 只负责参数、节点路由与调用脚本;服务器配置主体逻辑放在 `scripts/jenkins-server-provision.sh`。不要再把数百行 Bash 内联进 Jenkins `sh ''' ... '''` 或 `bash -lc '...'`,否则 Jenkins/Groovy/sh/bash 多层转义会把 `\"`、`${...}`、sed 表达式等内容二次改写,容易在运行时出现 `syntax error near unexpected token '}'` 这类难定位错误。
|
||||
|
||||
该脚本负责安装构建依赖、同步 SpacetimeDB current 目录、安装 systemd/Nginx 配置、创建或保留 `/etc/genarrative/api-server.env`、维护模式配置以及首次服务启动前的 SpacetimeDB client token 初始化。修改后应至少执行:
|
||||
|
||||
```bash
|
||||
bash -n scripts/jenkins-server-provision.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Nginx 规则
|
||||
|
||||
生产正式入口只保留必要路由:
|
||||
@@ -132,6 +148,7 @@ Nginx 配置文件分为两类:
|
||||
- `api-server` 发布、SpacetimeDB 模块发布、数据库导入、服务器配置变更必须进入维护模式。
|
||||
- 普通页面在维护模式下展示 `/maintenance.html`。
|
||||
- `/admin/api/*` 在维护模式下返回 503。
|
||||
- `/v1/database/<database>/subscribe` 与 `/v1/identity` 在维护模式下返回 503,阻断已打开前端继续通过 SpacetimeDB SDK 访问运行时数据。
|
||||
- 静态资源仍允许访问,避免维护页样式和资源加载失败。
|
||||
- 发布成功后自动解除维护模式。
|
||||
- 发布失败时保持维护模式,并通过邮件通知人工处理。
|
||||
@@ -195,7 +212,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 +227,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 +263,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 连接卡住时阻塞发布队列。
|
||||
@@ -381,7 +408,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
|
||||
并发与清理规则:
|
||||
|
||||
- 同一个 Rust 构建 Job 建议使用 `disableConcurrentBuilds()`,避免同一组件的多个 release 构建同时写入同一最终产物路径。
|
||||
- 如果 Linux agent 未安装 `sccache`,应先运行 `Genarrative-Server-Provision` 补齐缓存工具;Rust 构建流水线仍必须自动取消 `RUSTC_WRAPPER`,回退到直接使用 `rustc`,不能因为缺少可选缓存工具阻断真实构建。
|
||||
- 如果 Linux/Windows agent 未安装 `sccache`,或 `sccache --version` 无法实际执行,应先补齐缓存工具;Rust 构建流水线仍必须自动取消 `RUSTC_WRAPPER`,回退到直接使用 `rustc`,不能因为缺少可选缓存工具阻断真实构建。
|
||||
- 生产发布流水线只能消费 `build/<version>/` 或 Jenkins 归档产物,不允许从共享 `cargo-target` 目录直接发布。
|
||||
- `SCCACHE_CACHE_SIZE` 必须设置上限,避免编译缓存无限增长。
|
||||
- 对 `/var/cache/genarrative-build/*/cargo-target` 或数据盘对应目录配置定期清理,建议保留最近 14 到 30 天。
|
||||
@@ -403,6 +430,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
|
||||
- `COMMIT_HASH` 非空时,先解析到完整 commit,再用 `git merge-base --is-ancestor <commit> refs/remotes/origin/<SOURCE_BRANCH>` 校验该提交属于指定分支,校验通过后 detached checkout。
|
||||
- 流水线日志必须输出最终 `SOURCE_BRANCH` 与实际 `SOURCE_COMMIT`。
|
||||
- 构建产物必须写入 `release-manifest.json`,至少包含 `version`、`source_branch`、`source_commit`、`built_at` 和组件类型,供发布、回滚和审计使用。
|
||||
- Windows 构建 Job 写入 `.jenkins-source-commit` 时必须使用 UTF-8 无 BOM;部署脚本在校验 `COMMIT_HASH` 前也会剥离 UTF-8 BOM 和 CRLF,避免上游 PowerShell 5.1 `Set-Content -Encoding UTF8` 产生的不可见 BOM 让下游发布误判 commit hash 非法。
|
||||
|
||||
构建流水线使用上述参数决定实际构建源码。发布流水线也暴露同名参数,但只用于选择本次发布使用的部署脚本、配置模板和 smoke test 逻辑;被发布的应用文件仍必须来自 Jenkins 归档产物或指定 release 包,不允许在发布流水线中重新构建。
|
||||
|
||||
@@ -482,8 +510,10 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
|
||||
构建:
|
||||
|
||||
- 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 目标源码,默认构建 `origin/master` 最新 commit。
|
||||
- 使用 `spacetime build` 构建 `spacetime_module.wasm`。
|
||||
- 归档 wasm、发布脚本和 `release-manifest.json`。
|
||||
- 构建 `spacetime_module.wasm` 前默认生成 32 字节随机 hex 迁移引导密钥,注入 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`,并把同一份密钥写入 `build/<version>/migration-bootstrap-secret.txt`。构建日志只输出密钥来源和长度,不打印明文。
|
||||
- `Genarrative-Stdb-Module-Build` 提供 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 参数:留空时自动生成新密钥;填写 Jenkins Secret Text 凭据 ID 时,构建环境注入 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 并复用该值。仅在明确传 `--no-migration-bootstrap-secret` 时才构建不带引导密钥的 wasm。
|
||||
- 使用 Rust wasm target 构建 `spacetime_module.wasm`。
|
||||
- 归档 wasm、`migration-bootstrap-secret.txt` 和 `release-manifest.json`。`migration-bootstrap-secret.txt` 属于敏感产物,只用于创建首个迁移操作员或录入数据库导入/导出流水线的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的 Jenkins Secret Text;授权完成后不要把明文留在公开归档或聊天记录中。
|
||||
|
||||
发布:
|
||||
|
||||
@@ -493,6 +523,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
|
||||
- 在生产实例本机执行 `spacetime --root-dir=/stdb publish <database-name> --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes --no-config`。
|
||||
- 发布动作默认以 `spacetimedb` 服务用户执行,避免 root 默认 CLI 身份对自托管数据库验签失败,也避免 root 写入 `/stdb/config` 造成后续服务用户启动权限错误。
|
||||
- `Stdb publish` 固定追加 `--no-config`,只依赖显式传入的 `--root-dir`、`--server`、`--bin-path` 与数据库名,避免 agent 工作区、本机用户目录或仓库内 `spacetime` 配置干扰发布目标。
|
||||
- 首次迁移操作员授权时,使用本次 Stdb module 构建归档的 `migration-bootstrap-secret.txt` 创建 Jenkins Secret Text,然后在 `Genarrative-Database-Export` / `Genarrative-Database-Import` 的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 中填写该凭据 ID。后续已有迁移操作员时优先改用 `TOKEN_CREDENTIAL_ID`。
|
||||
- 成功后执行必要 smoke test。
|
||||
- 成功后解除维护模式。
|
||||
- 失败时保留维护模式并发邮件。
|
||||
@@ -520,6 +551,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
|
||||
- 从目标机器本机 SpacetimeDB 导出指定数据库数据,默认连接 `SPACETIME_SERVER_URL=http://127.0.0.1:3101`,自托管 `root-dir` 默认 `/stdb`。
|
||||
- 产物归档到 Jenkins,并可额外保存到 `SERVER_BACKUP_DIRECTORY`。
|
||||
- 敏感 token 与 bootstrap secret 只通过 Jenkins Secret Text 凭据 ID 注入,不作为明文 Job 参数。
|
||||
- 导出和导入流水线的 Bash 执行块启用 `set -u`;所有可选 Jenkins 参数必须先通过 `${VAR:-}` 收敛成本地默认值,再传给 Node 迁移脚本,避免空参数没有导出时触发 `unbound variable`。
|
||||
- 成功后解除维护模式。
|
||||
- 失败时保留维护模式并邮件通知。
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 个人反馈后端接入方案
|
||||
|
||||
更新时间:`2026-05-08`
|
||||
|
||||
## 目标
|
||||
|
||||
`/profile/feedback` 不再停留在前端成功态,提交时必须经过 `api-server` 鉴权、`spacetime-client` facade、`spacetime-module` procedure,并持久化到 SpacetimeDB。
|
||||
|
||||
## 接口
|
||||
|
||||
- 路由:`POST /api/profile/feedback`
|
||||
- 鉴权:必须登录,用户 ID 取 `AuthenticatedAccessToken`,前端不得上传或伪造 `userId`。
|
||||
- 请求体:
|
||||
- `description`:必填,trim 后 10 至 200 字符。
|
||||
- `contactPhone`:选填,trim 后最多 40 字符。
|
||||
- `evidenceItems`:选填,最多 4 张图片。
|
||||
- 每张凭证包含 `fileName`、`contentType`、`sizeBytes`、`dataUrl`。
|
||||
- 响应体:
|
||||
- `feedback.feedbackId`
|
||||
- `feedback.status`
|
||||
- `feedback.createdAt`
|
||||
- `feedback.evidenceItems` 只回传凭证元数据,不回显 Data URL。
|
||||
|
||||
## 表结构
|
||||
|
||||
新增私有表 `profile_feedback_submission`:
|
||||
|
||||
- `feedback_id PK: String`
|
||||
- `user_id: String`
|
||||
- `description: String`
|
||||
- `contact_phone: Option<String>`
|
||||
- `evidence_json: String`
|
||||
- `status: RuntimeProfileFeedbackStatus`
|
||||
- `created_at: Timestamp`
|
||||
- `updated_at: Timestamp`
|
||||
- 索引:`user_id`、`(user_id, created_at)`
|
||||
|
||||
`evidence_json` 保存首版图片凭证快照,后续如果迁移 OSS,应保持 HTTP 契约不变,仅替换内部 evidence 存储字段。
|
||||
|
||||
## 分层落点
|
||||
|
||||
- `shared-contracts`:冻结 HTTP DTO。
|
||||
- `module-runtime`:负责输入归一化、长度限制、图片数量/大小/Data URL 前缀校验、反馈 ID 和 evidence ID 生成、响应记录组装。
|
||||
- `spacetime-module`:新增 table 与 `submit_profile_feedback_and_return` procedure;只做事务写入和表到快照映射。
|
||||
- `spacetime-client`:新增 `submit_profile_feedback` facade,不让 `api-server` 直接依赖生成绑定。
|
||||
- `api-server`:新增鉴权 POST route,并对该 route 单独放宽 JSON body 上限。
|
||||
- 前端:`PlatformFeedbackView` 只负责临时表单状态、图片预览和调用 profile client;正式提交结果以后端返回为准。
|
||||
- 绑定生成:Windows 本地如遇 `sccache` 远端缓存被网络沙箱拦截,可临时使用仓库内短路径 `GENARRATIVE_BINDGEN_TEMP_ROOT` 并设置 `CARGO_BUILD_RUSTC_WRAPPER` 到本地 rustc passthrough wrapper 后重跑生成,不修改 `server-rs/.cargo/config.toml`。
|
||||
|
||||
## 验收
|
||||
|
||||
- 图片选择后能在页面看到缩略图。
|
||||
- 有效表单调用 `POST /api/profile/feedback` 并写入 `profile_feedback_submission`。
|
||||
- 未登录提交返回 `401`。
|
||||
- 超过图片数量、单张大小、总大小或非法 Data URL 时返回清晰校验错误。
|
||||
- `migration.rs`、SpacetimeDB 表目录、生成绑定同步更新。
|
||||
- 定向前端测试、Rust 领域测试和 API 认证测试通过。
|
||||
@@ -0,0 +1,83 @@
|
||||
# 个人任务与埋点系统技术方案
|
||||
|
||||
更新时间:`2026-05-03`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本轮新增一套可配置的个人任务系统,并补齐任务依赖的埋点统计能力。首个任务为“每日登录”,奖励 `10` 光点,入口放在“我的”页签;后台可修改任务配置。
|
||||
|
||||
## 2. 核心边界
|
||||
|
||||
- 埋点原始事实写入 `tracking_event`,这是实际存在的 SpacetimeDB 表。
|
||||
- 聚合投影写入 `tracking_daily_stat`,这也是后端维护的真实表,不是 view。
|
||||
- 任务配置写入 `profile_task_config`,默认配置包含 `daily_login`,后台修改后不得被默认初始化覆盖。
|
||||
- 任务进度写入 `profile_task_progress`,用于任务中心快速读取状态。
|
||||
- 领奖记录写入 `profile_task_reward_claim`,与钱包流水 `profile_wallet_ledger` 同事务写入。
|
||||
- “星光”奖励复用现有“光点”钱包,不新增第二种货币。
|
||||
|
||||
## 3. 埋点分层
|
||||
|
||||
| 层级 | scope_kind | scope_id 口径 |
|
||||
| --- | --- | --- |
|
||||
| 整站 | `site` | 固定为 `site` 或站点分区 key |
|
||||
| 作品 | `work` | 作品 profile_id / work_id |
|
||||
| 模块 | `module` | 模块 key,例如 `profile`、`puzzle` |
|
||||
| 用户 | `user` | 用户 id |
|
||||
|
||||
每条埋点可同时记录 `user_id`、`owner_user_id`、`profile_id`、`module_key` 与 `metadata_json`。任务首版只依赖用户层 `daily_login`,表结构先保留四层统计能力。
|
||||
|
||||
## 4. 日期桶
|
||||
|
||||
任务统计使用北京时间自然日:`day_key = floor((occurred_at_micros + 8h) / 1d)`。
|
||||
|
||||
这样存储仍是 UTC 时间戳,日切规则固定为业务口径,不依赖服务器本地时区。`tracking_event.occurred_at` 保存精确发生时间,`tracking_daily_stat.day_key` 只承担聚合桶职责。
|
||||
|
||||
## 5. 首版任务
|
||||
|
||||
| 字段 | 默认值 |
|
||||
| --- | --- |
|
||||
| task_id | `daily_login` |
|
||||
| title | `每日登录` |
|
||||
| event_key | `daily_login` |
|
||||
| cycle | `daily` |
|
||||
| threshold | `1` |
|
||||
| reward_points | `10` |
|
||||
| enabled | `true` |
|
||||
|
||||
用户成功登录时,认证链路会通过统一后端埋点 helper 幂等记录当日 `daily_login` 并刷新任务进度;用户打开任务中心只记录 `task_center_view` 浏览事件,不再承担每日登录事实写入。用户点击领取时,后端校验当日进度、领奖记录和配置状态,然后同事务写入领奖记录与钱包流水。
|
||||
|
||||
后台任务配置页的 `Event Key` 使用可搜索下拉控件,选项来自前端后台的埋点定义注册表。后台全量埋点筛选候选应对齐 `BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md` 的事件清单;任务配置页只展示标记为个人任务可用的事件,当前仅开放 `daily_login`,展示中文名称和备注。后续新增任务依赖的埋点时,应先补充注册表并显式标记任务可用,再开放运营配置。
|
||||
|
||||
## 6. 接口
|
||||
|
||||
### 用户侧
|
||||
|
||||
- `GET /api/profile/tasks`:读取任务中心,并记录 `task_center_view` 浏览事件;不在此入口写入 `daily_login`。
|
||||
- `POST /api/profile/tasks/{task_id}/claim`:领取任务奖励,并记录 `task_reward_claim` 成功事件。
|
||||
|
||||
### 后台侧
|
||||
|
||||
- `GET /admin/api/profile/tasks`:读取任务配置列表。
|
||||
- `POST /admin/api/profile/tasks`:新增或更新任务配置。
|
||||
- `POST /admin/api/profile/tasks/disable`:停用任务配置。
|
||||
|
||||
后台任务配置页进入时从 `profile_task_config` 对应的列表接口读取已有配置,点击列表项回填表单后仍通过同一个 upsert 接口修改原配置。最近一次保存结果可以保留为会话态提示,但不得作为任务配置列表的唯一来源。
|
||||
|
||||
## 7. 查询文档边界
|
||||
|
||||
- `docs/tracking/` 只存放具体埋点与埋点聚合查询,例如 `tracking_event`、`tracking_daily_stat` 的站点/作品/模块/用户查询。
|
||||
- `docs/operations/` 存放运营核查查询,例如任务进度、领奖记录、钱包流水对账。
|
||||
|
||||
不要把任务进度、领奖记录或钱包对账查询塞进 `docs/tracking/`,它们不是埋点系统本身。
|
||||
|
||||
## 8. 通用后端埋点覆盖
|
||||
|
||||
后端用户行为埋点统一按 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md` 执行。该文档维护通用 procedure、api-server 中间件、事件清单、排除范围与查询验收口径;每日登录也走该统一路径,仅保留认证 helper 作为业务语义入口。
|
||||
|
||||
## 9. 验收
|
||||
|
||||
1. `profile_task_config` 默认存在 `daily_login`,后台可修改奖励、阈值、标题和启用状态。
|
||||
2. “我的”页可以打开每日任务面板,登录后任务可领取 `10` 光点。
|
||||
3. 登录成功会幂等记录 `daily_login`;重复打开任务中心只记录 `task_center_view`,不会重复增加领取资格。
|
||||
4. 重复领奖不会重复发放。
|
||||
5. 表目录、迁移白名单、Rust/TypeScript 契约和前端入口同步更新。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 公开作品详情失效回首页修复
|
||||
|
||||
日期:`2026-05-11`
|
||||
|
||||
## 背景
|
||||
|
||||
直接访问 `/works/detail?work=<公开作品号>` 时,如果作品已经删除、下架或当前公开列表无法命中该作品,统一作品详情会先进入 `work-detail` 阶段。此前该阶段在没有 `selectedPublicWorkDetail` 时不会渲染任何内容;用户关闭“作品不存在或已下架”的提示后,页面可能只剩空白区域。
|
||||
|
||||
## 修复
|
||||
|
||||
1. `resolveWorkNotFoundRecoveryAction(...)` 覆盖 `/works/detail`、拼图公开详情和视觉小说公开详情,并复用运行态深链失效的回首页策略。
|
||||
2. 拼图公开详情、拼图运行态启动和拼图详情页读取的 `404/NOT_FOUND` 分支改为统一走公开作品失效恢复逻辑。
|
||||
3. 直接打开 `/works/detail?work=...` 的搜索失败分支会清理详情态、运行态临时数据,切回首页并清掉 URL query。
|
||||
4. `work-detail` 阶段在详情数据为空时渲染轻量读取态,避免异步间隙或异常分支出现纯白屏。
|
||||
|
||||
## 验证
|
||||
|
||||
- `npm run test -- src/routing/runtimeNotFoundRecovery.test.ts`
|
||||
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct missing public work detail alert returns to platform home"`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding -- src/routing/runtimeNotFoundRecovery.ts src/routing/runtimeNotFoundRecovery.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx docs/technical/PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md`
|
||||
|
||||
## 关联文件
|
||||
|
||||
1. `src/routing/runtimeNotFoundRecovery.ts`
|
||||
2. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
|
||||
@@ -1,15 +1,16 @@
|
||||
# 拼图 APIMart 图片模型路由接入 2026-05-01
|
||||
# 拼图图片模型路由接入 2026-05-01
|
||||
|
||||
> 2026-05-09 更新:GPT-image-2 图片生成已从 APIMart 迁移到 VectorEngine。本文保留前端模型选择和拼图扣费/持久化历史设计,图片上游接口、环境变量和请求体以 `VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md` 为准。
|
||||
|
||||
## 背景
|
||||
|
||||
拼图创作已收口为填表式流程,首图生成和结果页关卡重新生成都由 `server-rs/crates/api-server/src/puzzle.rs` 执行外部图片 I/O,再把正式图写入 OSS 与 SpacetimeDB。新的模型选择只影响图片生成上游,不改变 SpacetimeDB 表结构、拼图草稿结构或前端运行时规则。
|
||||
|
||||
本轮参考 APIMart 文档:
|
||||
历史版本曾参考 APIMart 文档;当前 GPT-image-2 图片生成参考 VectorEngine Apifox 文档:
|
||||
|
||||
1. `https://docs.apimart.ai/cn/api-reference/images/gpt-image-2/generation`
|
||||
2. `https://docs.apimart.ai/cn/api-reference/images/gemini-3.1-flash/generation`
|
||||
1. `https://vectorengine.apifox.cn/api-448710071`
|
||||
|
||||
两条文档均指向 OpenAI 兼容风格的图片生成入口:`POST https://api.apimart.ai/v1/images/generations`,头部使用 `Authorization: Bearer {APIMART_API_KEY}`。请求体至少包含 `model`、`prompt`、`n`、`size`。返回体按 OpenAI images 兼容格式优先读取 `data[].url`,若供应商返回异步任务结构,则继续按 `task_id` / `tasks/{task_id}` 轮询并提取图片 URL。
|
||||
当前图片生成入口:`POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations`,头部使用 `Authorization: Bearer {VECTOR_ENGINE_API_KEY}`。请求体至少包含 `model = gpt-image-2-all`、`prompt`、`n`、`size`,参考图使用 `image` 数组。返回体按同步 OpenAI images 结构读取 `data[].url` 或 `data[].b64_json`,不再轮询 APIMart `tasks/{task_id}`。
|
||||
|
||||
## 模型选项
|
||||
|
||||
@@ -17,8 +18,8 @@
|
||||
|
||||
| 前端显示 | 请求值 | 上游 |
|
||||
| --- | --- | --- |
|
||||
| `gpt-image-2` | `gpt-image-2` | APIMart `/v1/images/generations` |
|
||||
| `nanobanana2` | `gemini-3.1-flash-image-preview` | APIMart `/v1/images/generations` |
|
||||
| `gpt-image-2` | `gpt-image-2` | VectorEngine `/v1/images/generations`,上游模型 `gpt-image-2-all` |
|
||||
| `nanobanana2` | `gemini-3.1-flash-image-preview` | 历史兼容选项,后端回落到 VectorEngine `gpt-image-2-all` |
|
||||
|
||||
默认值为 `gpt-image-2`。前端只负责展示和传递所选模型,不能把模型路由逻辑、上游请求体拼装或 API Key 暴露到浏览器。历史草稿或旧请求中的空值、`original`、未知值统一按 `gpt-image-2` 处理,不再把拼图生图路由回 DashScope 原模型。
|
||||
|
||||
@@ -40,36 +41,44 @@
|
||||
2. `compile_puzzle_draft_with_initial_cover` 与 `generate_puzzle_image_candidates` 增加图片模型参数。
|
||||
3. `imageModel` 归一化规则:
|
||||
- 空值、`original` 或未知值统一回落为 `gpt-image-2`;
|
||||
- `gpt-image-2` 走 APIMart;
|
||||
- `gemini-3.1-flash-image-preview` 走 APIMart,前端显示名为 `nanobanana2`。
|
||||
4. APIMart 文生图和图生图共用 `POST /v1/images/generations`。有参考图时,后端将参考图 Data URL 作为 `image_urls` 数组传入;若上游不接受该字段,错误按上游失败返回,不在前端降级伪造结果。
|
||||
5. APIMart 尺寸使用文档要求的比例写法 `1:1`。`gemini-3.1-flash-image-preview` 额外带 `resolution = "1K"`,对齐约 1024px 的拼图正方形素材。
|
||||
6. APIMart 生成成功后仍下载远程图片,沿用现有 OSS 私有对象、`asset_object` 和 `asset_entity_binding` 写入流程。若图片已成功上传 OSS,但 Maincloud / SpacetimeDB 短暂返回 `503 Service Unavailable`,资产索引写入允许降级跳过,并返回本次生成图片;日志必须记录 `拼图图片资产索引写入因 SpacetimeDB 连接不可用而降级跳过`。
|
||||
7. `save_puzzle_generated_images` 写回草稿时若遇到 Maincloud 连接级 `503` 或断线,API 层基于本次生成结果合成 session 快照返回给前端,避免 APIMart 已成功出图却被后置持久化误报成服务不可用。余额不足、参数错误、上游生图失败仍按原错误返回,不做伪成功。
|
||||
8. 结果页 `generate_puzzle_images` 会携带当前作品信息和 `levelsJson`。当 Maincloud / SpacetimeDB 在读取 session 阶段就返回连接级 `503` 或断线时,后端必须先用这份结果页快照构造最小内存 session,再继续调用 APIMart;外部图片已经生成后仍按第 6、7 条处理持久化降级。余额不足、参数错误、缺少草稿快照、关卡不存在等业务错误不走此降级。
|
||||
9. APIMart 异步任务轮询按文档口径在提交后先等待 `10s`,再调用 `GET /v1/tasks/{task_id}`;图片地址提取同时支持 `url: "..."` 与 `url: ["..."]` 两种结构。
|
||||
10. APIMart 错误统一映射为 `502 UPSTREAM_ERROR`,`details.provider = "apimart"`,保留上游状态码、业务 message 和截断后的 raw excerpt。
|
||||
11. 拼图首图生成 `compile_puzzle_draft` 与关卡图片生成 `generate_puzzle_images` 每次预扣 `2` 光点;余额不足仍返回 `409 CONFLICT`,Maincloud 连接级 503 仍按既有降级策略处理。
|
||||
- `gpt-image-2` 走 VectorEngine;
|
||||
- `gemini-3.1-flash-image-preview` 不再走 APIMart,前端显示名仍为 `nanobanana2`,后端统一回落到 VectorEngine `gpt-image-2-all`。
|
||||
4. VectorEngine 文生图和图生图共用 `POST /v1/images/generations`。有参考图时,后端将参考图 Data URL 作为 `image` 数组传入;若上游不接受该字段,错误按上游失败返回,不在前端降级伪造结果。
|
||||
5. VectorEngine 拼图尺寸使用 `1024x1024`,请求体不携带 `official_fallback`。
|
||||
6. VectorEngine 生成成功后仍下载远程图片,沿用现有 OSS 私有对象、`asset_object` 和 `asset_entity_binding` 写入流程。若图片已成功上传 OSS,但 SpacetimeDB 短暂返回 `503 Service Unavailable`,资产索引写入允许降级跳过,并返回本次生成图片;日志必须记录 `拼图图片资产索引写入因 SpacetimeDB 连接不可用而降级跳过`。
|
||||
7. `save_puzzle_generated_images` 写回草稿时若遇到 SpacetimeDB 连接级 `503` 或断线,API 层基于本次生成结果合成 session 快照返回给前端,避免 VectorEngine 已成功出图却被后置持久化误报成服务不可用。余额不足、参数错误、上游生图失败仍按原错误返回,不做伪成功。
|
||||
8. 结果页 `generate_puzzle_images` 会携带当前作品信息和 `levelsJson`。当 SpacetimeDB 在读取 session 阶段就返回连接级 `503` 或断线时,后端必须先用这份结果页快照构造最小内存 session,再继续调用 VectorEngine;外部图片已经生成后仍按第 6、7 条处理持久化降级。余额不足、参数错误、缺少草稿快照、关卡不存在等业务错误不走此降级。
|
||||
9. VectorEngine 错误统一映射为 `502 UPSTREAM_ERROR`,`details.provider = "vector-engine"`,保留上游状态码、业务 message 和截断后的 raw excerpt。
|
||||
10. 拼图首图生成 `compile_puzzle_draft` 与关卡图片生成 `generate_puzzle_images` 每次预扣 `2` 光点;余额不足仍返回 `409 CONFLICT`,SpacetimeDB 连接级 503 仍按既有降级策略处理。
|
||||
|
||||
## 关卡名多模态生成
|
||||
|
||||
1. 第一关和结果页关卡重新生图的最终关卡名统一由 APIMart Chat Completions `gpt-4o-mini` 生成。
|
||||
2. 输入必须同时包含生成完成后的正式图片和当前关卡 `pictureDescription`;图片由 `api-server` 从生成结果字节压缩为最多 768 边长的 PNG Data URL 后,以 OpenAI 兼容 `messages[].content[]` 的 `image_url` 形式传入。
|
||||
3. 文本模型仍只输出 `{"levelName":"..."}`,并继续复用现有 2 到 8 个中文字符、禁用“画面 / 拼图 / 作品”等泛词的解析与归一化规则。
|
||||
4. `gpt-4o-mini` 调用失败、返回非法或 APIMart 文本配置缺失时,不阻断图片生成;后端保留图片生成前的文本关卡名或确定性兜底名。
|
||||
5. 关卡名与候选图在同一次 `save_puzzle_generated_images` 中写回 `levels_json` 和正式候选图,避免图片与关卡名不同步。
|
||||
|
||||
## 环境变量
|
||||
|
||||
新增服务端环境变量:
|
||||
|
||||
```text
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY="YOUR_APIMART_API_KEY"
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai"
|
||||
VECTOR_ENGINE_API_KEY="YOUR_VECTOR_ENGINE_API_KEY"
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
`APIMART_API_KEY` 只能存在于本地或部署环境,不写入 Git 跟踪文件。若选择 APIMart 模型但缺少 key,后端返回服务不可用错误,前端展示现有错误面板。
|
||||
`VECTOR_ENGINE_API_KEY` 只能存在于本地或部署环境,不写入 Git 跟踪文件。若缺少 key,后端返回服务不可用错误,前端展示现有错误面板。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 创作表单和关卡详情的画面描述框左下角能切换 `gpt-image-2`、`nanobanana2`,默认显示 `gpt-image-2`。
|
||||
2. 点击“生成草稿”时,后端首图生成使用当前表单选择的模型。
|
||||
3. 点击“生成画面 / 重新生成画面”时,后端当前关卡图片生成使用关卡详情选择的模型。
|
||||
4. 历史 `original` 或空模型值不会再触发 DashScope,统一按 `gpt-image-2` 请求 APIMart。
|
||||
5. 选择 APIMart 模型时,请求 `POST {APIMART_BASE_URL}/images/generations`,使用 `Authorization: Bearer {APIMART_API_KEY}`,`model` 等于请求值,`size = 1:1`。
|
||||
6. “生成草稿”和关卡详情生图按钮展示 `消耗2光点`;关卡详情确认后展示 30 秒预计剩余进度条。
|
||||
7. 不改 SpacetimeDB 表结构,因此无需更新 `migration.rs` 或重新生成 bindings。
|
||||
8. 后端改动后运行对应 Rust 测试,并按项目约束用 `npm run api-server:maincloud` 重启验证。
|
||||
4. 历史 `original` 或空模型值不会再触发 DashScope,统一按 `gpt-image-2` 请求 VectorEngine。
|
||||
5. 选择图片模型时,请求 `POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations`,使用 `Authorization: Bearer {VECTOR_ENGINE_API_KEY}`,上游 `model = gpt-image-2-all`,不携带 `official_fallback`,`size = 1024x1024`。
|
||||
6. 首图和结果页关卡重新生图成功后,Network 中应先完成 VectorEngine 图片生成,再调用 APIMart `POST {APIMART_BASE_URL}/chat/completions`,请求模型为 `gpt-4o-mini`,消息同时包含画面描述文本和正式图 `image_url` Data URL。
|
||||
7. “生成草稿”和关卡详情生图按钮展示 `消耗2光点`;关卡详情确认后展示 30 秒预计剩余进度条。
|
||||
8. 不改 SpacetimeDB 表结构,因此无需更新 `migration.rs` 或重新生成 bindings。
|
||||
9. 后端改动后运行对应 Rust 测试,并按项目约束用 `npm run api-server` 重启验证。
|
||||
|
||||
@@ -20,7 +20,7 @@ RPG 在点击生成草稿后会离开聊天工作区,进入独立的生成进
|
||||
|
||||
- 前端只负责展示生成进度与触发已有后端动作,不新增 server-node 或 PostgreSQL 链路。
|
||||
- 后端继续沿用 `server-rs` + `SpacetimeDB` 的会话、草稿与资产写入能力。
|
||||
- 拼图生成草稿链路仍包含:结果页草稿、候选图生成、正式图确认。
|
||||
- 拼图生成草稿链路仍包含:首关草稿编译、首关画面生成、正式草稿写入。
|
||||
- 大鱼吃小鱼生成草稿链路只包含:玩法草稿、等级蓝图、背景蓝图与运行参数编译。
|
||||
- 大鱼吃小鱼的主图、动作、背景都在结果页工坊单独触发,不再属于草稿编译阶段。
|
||||
- 生成过程中展示的“角色描述、角色图片、动作”等,统一映射为锚点、草稿蓝图与资产步骤,不把规则说明类文本写成默认 UI 文案。
|
||||
@@ -38,9 +38,9 @@ RPG 在点击生成草稿后会离开聊天工作区,进入独立的生成进
|
||||
|
||||
### 拼图
|
||||
|
||||
- `compile_puzzle_draft`:在 `server-rs` 内整理主题、主体、构图与标签,写入结果页草稿。
|
||||
- `compile_puzzle_draft`:同一次后端 action 内根据草稿摘要生成候选图。
|
||||
- `compile_puzzle_draft`:同一次后端 action 内自动选择第一张候选图作为正式图。
|
||||
- `compile_puzzle_draft`:在 `server-rs` 内根据入口画面描述生成首关名称和结果页草稿。
|
||||
- `compile_puzzle_draft`:同一次后端 action 内根据画面描述、参考图和当前图片模型生成首关画面。
|
||||
- `compile_puzzle_draft`:同一次后端 action 内自动把首图设为正式图,并同步到结果页草稿。
|
||||
- `ready`:进入拼图结果页。
|
||||
|
||||
### 大鱼吃小鱼
|
||||
|
||||
@@ -4,13 +4,23 @@
|
||||
|
||||
拼图创作入口不再使用 Agent 对话收集题材锚点。新流程让玩家填写作品名称、作品描述、画面描述三类信息,其中画面描述只服务首关画面生成与关卡画面语义,不再作为作品详情页的作品描述。画面描述支持上传参考图。玩家确认后直接进入草稿生成进度页,后续草稿生成、首图生成、正式图选择、结果页编辑和发布沿用现有后端编排。
|
||||
|
||||
2026-05-03 后入口进一步收口为画面描述直创:入口表单只保留画面描述、参考图和图片模型选择;作品名称、作品描述、作品标签全部进入结果页补全。若本文件早期段落仍提到入口必填作品名称或作品描述,以 `PUZZLE_PICTURE_ONLY_CREATION_AND_AI_TAGS_2026-05-03.md` 为准。
|
||||
|
||||
## 入口表单
|
||||
|
||||
### 2026-05-03 画面描述直创补充
|
||||
|
||||
1. 入口表单只展示 `画面描述`、参考图和图片模型选择;`画面描述` 是唯一必填字段。
|
||||
2. 表单自动保存只保存 `pictureDescription`,不再保存入口作品名称、作品描述或推断标签。
|
||||
3. 点击“生成草稿”后进入生成进度页,步骤固定为“编译首关草稿 -> 生成首关画面 -> 写入正式草稿”。
|
||||
4. 生成进度页“当前拼图信息”只展示画面描述;不得展示空作品名称、空作品描述或旧五锚点结构。
|
||||
5. 结果页打开后,作品名称默认使用首关名称,作品描述与作品标签保持为空,等待用户在作品信息 Tab 补全或触发 AI 标签生成。
|
||||
|
||||
### 2026-04-30 初始表单草稿保存补充
|
||||
|
||||
1. 玩家在创作页点击“拼图”入口时,前端必须立即创建一个新的拼图 Agent session,并同步生成一条 `publicationStatus = draft` 的拼图作品卡;此时不触发 `compile_puzzle_draft`,不生成图片,不进入生成进度页。
|
||||
2. 新 session 的 `seedText` 允许为空;SpacetimeDB 侧用空锚点和空表单草稿初始化,不得把默认题材文案写入玩家草稿字段。
|
||||
3. 初始表单输入自动保存到 session 的 `draft_json` 与 `puzzle_work_profile` 投影。保存字段只包含 `workTitle`、`workDescription`、`pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡;草稿设置阶段默认关卡名称必须为空,不得写入“第一关”“第1关”或作品名称作为默认值。参考图只保存在当前前端会话内,不落入 SpacetimeDB。
|
||||
3. 初始表单输入自动保存到 session 的 `draft_json` 与 `puzzle_work_profile` 投影。保存字段只包含 `workTitle`、`workDescription`、`pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡;草稿设置阶段默认关卡名称必须为空,不得写入“第一关”“第1关”或作品名称作为默认值。生成前的参考图只保存在当前前端会话内;一旦用于首图生成并成功返回,后端必须把该参考图写入首关 `pictureReference`,供结果页后续重新生成继续复用。
|
||||
4. 玩家在生成草稿前退出,再次从创作中心点击这条拼图草稿时,必须恢复到填表页,并回填之前自动保存的作品名称、作品描述和画面描述;只有执行 `compile_puzzle_draft` 且生成结果页草稿后,草稿入口才进入结果页。
|
||||
5. 表单自动保存走 `save_puzzle_form_draft` action,不消耗光点,不生成图片,不改变 `stage = collecting_anchors`;生成草稿按钮仍单独触发 `compile_puzzle_draft` 并进入进度页。
|
||||
6. 点击拼图入口始终创建新草稿,不复用上一次未完成 session;恢复旧草稿只通过“我的创作”中的草稿卡进入。
|
||||
@@ -66,13 +76,14 @@
|
||||
4. 首图文生图 prompt 由 api-server 拼接固定拼图约束后统一压缩到 `500` 字符以内,避免玩家长画面描述触发 DashScope 参数非法;进度页和结果页仍展示玩家原始画面描述,不展示压缩后的内部 prompt。
|
||||
5. 图片生成仍在 api-server 内完成,遵守 SpacetimeDB reducer 不做网络 I/O 的约束。
|
||||
6. 参考图以 Data URL 进入 `POST /api/runtime/puzzle/agent/sessions` 和 `POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions`,这两条路由必须单独放宽 JSON body 上限;不要放大全局默认 body limit。
|
||||
7. 前端仍应优先压缩参考图;后端 body 上限只用于容纳合理尺寸的单张参考图,超大原图不应直接落入 SpacetimeDB 或作为作品字段持久化。
|
||||
8. 作品更新接口 `PUT /api/runtime/puzzle/works/{profileId}` 必须支持作品信息和关卡列表一起写入,前端自动保存不得只写旧单关字段。
|
||||
9. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。
|
||||
10. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt,再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。
|
||||
11. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时,api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
|
||||
12. `compile_puzzle_draft` 前置光点预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`,SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
|
||||
13. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs`;`puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化,不再直接拼草稿 prompt 文本。
|
||||
7. 前端仍应优先压缩参考图,入口上传图和裁剪图统一压到单边 1024 以内;后端 body 上限只用于容纳合理尺寸的单张参考图,超大原图不应直接落入 SpacetimeDB 或作为作品字段持久化,后端解析后会拒绝超过 8MB 字节的参考图。
|
||||
8. `aiRedraw = true` 且存在 `referenceImageSrc` 时,api-server 必须走 VectorEngine `POST /v1/images/edits` multipart 图生图接口;无参考图或入口页 `aiRedraw = false` 时不走图生图,关闭 AI 重绘会直接应用上传图为首关正式图。
|
||||
9. 作品更新接口 `PUT /api/runtime/puzzle/works/{profileId}` 必须支持作品信息和关卡列表一起写入,前端自动保存不得只写旧单关字段。
|
||||
10. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。
|
||||
11. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt,再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。
|
||||
12. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时,api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
|
||||
13. `compile_puzzle_draft` 前置光点预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`,SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
|
||||
14. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs`;`puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化,不再直接拼草稿 prompt 文本。
|
||||
|
||||
## 结果页
|
||||
|
||||
@@ -94,10 +105,23 @@
|
||||
4. 底部吸底操作区只承载动作按钮,不默认写玩法说明或规则解释,避免压缩移动端编辑空间。
|
||||
5. 关卡详情面板内触发生成画面时,前端必须把当前编辑态完整 `levelsJson` 随 `generate_puzzle_images` action 一起提交。这样新建关卡在自动保存完成前立即生成,也能由后端写回目标关卡。
|
||||
6. api-server 处理 `generate_puzzle_images` 时,若 action 带有 `levelsJson`,必须用这份关卡快照覆盖本次生成的草稿关卡视图后再定位 `levelId`。若请求明确传入 `levelId` 但关卡列表中不存在该关卡,必须返回错误,不得静默回退第一关。
|
||||
7. 历史拼图素材入口只在已有正式图的 `画面图` 区域右下角展示,不再放在 `画面描述` 输入区;本地上传参考图入口仍保留在画面描述输入区右下角。
|
||||
7. 历史拼图素材入口和本地上传参考图入口统一收口到 `画面图` 图卡右下角,避免 `画面描述` 输入区同时承载文本编辑和素材入口;无正式图时也展示空图态图卡。
|
||||
8. 历史拼图素材列表必须由服务端按当前登录账号过滤,只返回 `asset_kind = puzzle_cover_image` 且 `owner_user_id = 当前账号` 的资产;不得依赖前端过滤,也不得展示其他账号素材。
|
||||
9. `画面图` 图卡本身就是上传热区,详情页不再保留右下角独立“上传参考图”按钮;历史入口统一使用带 `History` 图标和 `历史` 小字的按钮。入口页空图态的“点击上传拼图图片”只作为图卡内轻量提示,不使用胶囊按钮、边框或背景样式。
|
||||
|
||||
画面描述区域不再展示候选图实际 prompt 或“请生成一张适合……”之类内部提示词模块。参考图入口保留在画面描述编辑区域内,便于重新生成时继续带入。结果页编辑关卡画面描述时只同步该关卡 `pictureDescription`;作品描述只在作品信息 Tab 编辑,作品详情页不得再回退使用画面描述。
|
||||
### 2026-05-10 关卡生图交互补充
|
||||
|
||||
1. 关卡详情页的 `画面图` 与 `画面描述` 模块对齐入口页拼图表单:画面图使用稳定正方形图卡,画面描述使用固定高度输入区并保留图片模型选择。
|
||||
2. 新建关卡或无正式图关卡也展示 `画面图` 图卡;空图态只保留图标化占位和生成中状态,不追加规则说明文案。
|
||||
3. 关卡详情页删除手填 `参考图链接或资产ID` 输入框。参考图只能通过本地上传或历史拼图素材选择进入本次生成请求;字段 `levels[].pictureReference` 继续作为后端生成后的复用字段透传,不作为用户可手填表单项。
|
||||
4. 单关生成等待估算从 `30` 秒调整为 `90` 秒;生成按钮内展示小字 `等待时间可以制作更多关卡哦~`,不得另起说明面板。
|
||||
5. 触发某一关生成时,前端必须立即把该关 `generationStatus` 标为 `generating` 并随当前 `levelsJson` 写入草稿自动保存链路;后端生成完成后再写回 `ready`。
|
||||
6. `generationStatus = generating` 的关卡在详情弹窗关闭后仍保留进度展示,再次打开同一关详情能继续看到生成进度;关卡列表卡片也必须展示生成中的轻量状态。
|
||||
7. 单关图片生成必须作为后台 action 执行,不占用拼图结果页全局 busy 状态;生成期间仍允许编辑作品信息、编辑关卡、新增关卡、删除其他关卡、关卡测试和继续触发其他可并行动作。
|
||||
8. 发布是唯一必须等待全部图片生成完成的草稿结果页动作;发布检查需要把 `generationStatus = generating` 的关卡列为 blocker,避免未完成资源进入广场。
|
||||
9. 后台生图回包只合并对应关卡的图片候选、正式图、资产 ID 与生成状态,不得覆盖用户等待期间对关卡名、画面描述、作品信息或新增关卡所做的本地编辑。
|
||||
|
||||
画面描述区域不再展示候选图实际 prompt 或“请生成一张适合……”之类内部提示词模块。参考图入口统一放在画面图图卡内,便于重新生成时继续带入,同时保持画面描述输入区只负责文本编辑。结果页编辑关卡画面描述时只同步该关卡 `pictureDescription`;作品描述只在作品信息 Tab 编辑,作品详情页不得再回退使用画面描述。
|
||||
|
||||
## 验收
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
### 1. 图片生成
|
||||
|
||||
1. 拼图默认使用 APIMart `gpt-image-2` 生成图,外部请求尺寸固定为 `1:1`;`nanobanana2` 仍映射为 `gemini-3.1-flash-image-preview`。
|
||||
1. 拼图默认使用 VectorEngine `gpt-image-2-all` 生成图,外部请求尺寸固定为 `1024x1024`;前端历史 `nanobanana2` 选项只保留兼容展示,后端同样回落到 VectorEngine GPT-image-2-all,不再调用 APIMart 图片网关。
|
||||
2. 历史 `original` 或空模型值只做兼容输入,不再进入 DashScope 原模型链路,统一按 `gpt-image-2` 路由。
|
||||
3. 文生图和参考图生图共用同一个正方形尺寸口径,禁止一条链路仍生成竖屏或横版图。
|
||||
4. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、主体清晰、层次明确、无文字水印等约束。
|
||||
5. 文生图正向 prompt 必须由后端压缩到 `500` 字符以内,优先保留玩家画面描述开头与固定拼图约束,避免上游把超长 prompt 判为“请求参数不合法”。
|
||||
6. APIMart 上游失败时,api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
|
||||
6. VectorEngine 上游失败时,api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
|
||||
7. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
|
||||
8. 光点预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
|
||||
## 验收
|
||||
|
||||
1. 点击拼图草稿生成或重新生成画面时,后端请求 APIMart 的 `size` 为 `1:1`,默认模型为 `gpt-image-2`。
|
||||
1. 点击拼图草稿生成或重新生成画面时,后端请求 VectorEngine 的 `size` 为 `1024x1024`,上游模型为 `gpt-image-2-all`。
|
||||
2. 图片提示词包含 `1:1 正方形拼图关卡`。
|
||||
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、`避免文字、水印、边框和 UI 元素` 等玩法约束不能丢。
|
||||
4. APIMart 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message,后端日志能看到 `upstreamStatus` 和 `rawExcerpt`。
|
||||
4. VectorEngine 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message,后端日志能看到 `upstreamStatus` 和 `rawExcerpt`。
|
||||
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`。
|
||||
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
|
||||
7. 基础单块和合并块都能看到圆角,合并块的外凸角与内凹角都不是直角,且图片不会溢出圆角裁剪。
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
# 拼图生成图片资源代理修复
|
||||
# 拼图生成图片读取链路修复
|
||||
|
||||
日期:`2026-04-27`
|
||||
|
||||
更新:`2026-05-08`
|
||||
|
||||
## 背景
|
||||
|
||||
拼图结果页的“生成或更换图片”会在 `api-server` 中调用 DashScope 生成图片,再把候选图上传到 OSS,最终以 `/generated-puzzle-assets/...` 旧兼容路径写回 `PuzzleGeneratedImageCandidate.image_src` 与草稿封面字段。
|
||||
|
||||
本次排查发现拼图图片写入路径已经进入 `platform-oss::LegacyAssetPrefix::PuzzleAssets`,但后端 Axum 旧资源代理和 Vite 本地代理没有挂载 `/generated-puzzle-assets`。这会导致候选图或正式图无法读取;后续如果把已有候选图作为参考图继续更换图片,也会让参考图读取链路失效。
|
||||
历史排查曾发现拼图图片写入路径已经进入 `platform-oss::LegacyAssetPrefix::PuzzleAssets`,但后端 Axum 旧资源代理和 Vite 本地代理没有挂载 `/generated-puzzle-assets`。当时的处理口径是补旧资源代理。
|
||||
|
||||
当前 `WP-DEL` 后,旧 `/generated-*` 直读代理已经物理删除;`/generated-puzzle-assets/...` 只允许作为 `legacyPublicPath` / OSS object key 标识。浏览器不能再直接请求该路径,必须通过 `/api/assets/read-url?legacyPublicPath=...` 换取短期签名 URL 后预览。
|
||||
|
||||
## 修复口径
|
||||
|
||||
1. `server-rs/crates/api-server/src/legacy_generated_assets.rs` 增加 `proxy_generated_puzzle_assets(...)`,复用统一的 OSS 签名读取逻辑。
|
||||
2. `server-rs/crates/api-server/src/app.rs` 挂载 `/generated-puzzle-assets/{*path}`,与角色、大鱼、自定义世界图片资源前缀保持一致。
|
||||
3. `vite.config.ts` 增加 `/generated-puzzle-assets` dev proxy,保证本地网页端不会因为 Vite 代理缺口读不到后端资源。
|
||||
1. `platform-oss::LEGACY_PUBLIC_PREFIXES` 必须包含 `generated-puzzle-assets`,保持直传票据、服务端上传、read-url 支持列表和错误提示同一口径。
|
||||
2. `src/services/assetReadUrlService.ts` 的 `isGeneratedLegacyPath(...)` 需要同时识别 `/generated-puzzle-assets/...` 和 `generated-puzzle-assets/...`。后者可能来自 object key 形态的历史字段。
|
||||
3. `ResolvedAssetImage` / `useResolvedAssetReadUrl` 在签名 URL 返回前不能把裸 `/generated-*` 或 `generated-*` 写进 `<img src>`。
|
||||
4. `refreshKey` 只能用于跳过前端签名 URL 缓存并重新请求 `/api/assets/read-url`,不能再给 OSS V4 签名 URL 追加 `_v` 等额外 query;OSS 会把 query 纳入签名,额外参数会让新生成图变成 403/破图。
|
||||
5. 历史素材被选为参考图后,参考图小预览也必须走 `ResolvedAssetImage`,不能使用裸 `<img src="/generated-*">`。
|
||||
6. 禁止恢复 `/generated-puzzle-assets/{*path}` Axum 路由或 Vite 直读代理;正式读取统一走 `/api/assets/read-url`。
|
||||
|
||||
## 后续约束
|
||||
|
||||
1. 任何新增 `LegacyAssetPrefix` 都必须同时检查:
|
||||
- `platform-oss` 前缀枚举
|
||||
- `api-server` 旧资源代理路由
|
||||
- Vite dev proxy
|
||||
- `platform-oss::LEGACY_PUBLIC_PREFIXES`
|
||||
- `/api/assets/read-url` 入参解析
|
||||
- 前端 `isGeneratedLegacyPath(...)` 是否能识别
|
||||
2. 拼图候选图 JSON 仍保持 SpacetimeDB 持久化结构 `PuzzleGeneratedImageCandidate` 的 snake_case 字段,不把 HTTP camelCase 响应结构写入 `draft_json`。
|
||||
3. 图片生成、OSS 读写和外部参考图解析继续留在 `api-server`,不能下沉到 SpacetimeDB reducer。
|
||||
4. 如果图片组件需要刷新 generated 私有资源,优先让 `refreshKey` 触发重新换签;不要修改已返回的 `signedUrl`。
|
||||
|
||||
## 验收
|
||||
|
||||
1. `npm run check:encoding`
|
||||
2. `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
3. `npm run api-server` 重启后,点击拼图结果页“生成或更换图片”,候选图应能写回并正常展示。
|
||||
1. `npm run test -- src\services\assetReadUrlService.test.ts src\hooks\useResolvedAssetReadUrl.test.tsx src\components\puzzle-result\PuzzleResultView.test.tsx`
|
||||
2. `cargo test -p platform-oss --manifest-path server-rs\Cargo.toml`
|
||||
3. `npm run check:encoding`
|
||||
4. `npm run api-server` 重启后检查 `/healthz`,再点击拼图结果页“生成或更换图片”,候选图应能写回并正常展示。
|
||||
|
||||
89
docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md
Normal file
89
docs/technical/PUZZLE_MATCH3D_RESULT_AUDIO_TAB_2026-05-11.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 拼图与抓大鹅结果页音乐 Tab 2026-05-11
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案把 VectorEngine 音频生成能力从视觉小说结果页扩展到拼图与抓大鹅结果页:
|
||||
|
||||
1. 拼图结果页新增 `音乐` Tab,支持通过 Suno 生成作品背景音乐。
|
||||
2. 抓大鹅结果页新增 `音乐` Tab,支持通过 Suno 生成作品背景音乐。
|
||||
3. 抓大鹅 `3D素材` Tab 支持为每个生成物体通过 Vidu 生成点击音效。
|
||||
|
||||
本轮不新增 SpacetimeDB 表,不修改表字段,不把供应商密钥下发到前端。
|
||||
|
||||
## 2. 通用音频接口
|
||||
|
||||
后端在既有视觉小说音频路由外新增通用创作音频路由:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/creation/audio/background-music` | 提交 Suno 背景音乐任务 |
|
||||
| `POST` | `/api/creation/audio/background-music/{task_id}/asset` | 查询并转存 Suno 音频资产 |
|
||||
| `POST` | `/api/creation/audio/sound-effect` | 提交 Vidu 音效任务 |
|
||||
| `POST` | `/api/creation/audio/sound-effect/{task_id}/asset` | 查询并转存 Vidu 音效资产 |
|
||||
|
||||
通用转存请求由前端传入 `entityKind`、`entityId`、`slot`、`assetKind`、`profileId`。后端仍负责:
|
||||
|
||||
1. 校验 VectorEngine 与 OSS 环境变量。
|
||||
2. 轮询供应商任务结果。
|
||||
3. 下载音频字节。
|
||||
4. 写入 OSS 私有对象。
|
||||
5. 确认 `asset_object` 并绑定 `asset_entity_binding`。
|
||||
|
||||
视觉小说原路由保持兼容,内部继续复用同一套提交、轮询、转存逻辑。
|
||||
|
||||
## 3. 数据落点
|
||||
|
||||
### 3.1 拼图
|
||||
|
||||
拼图作品没有独立作品级 metadata 字段。背景音乐随 `levels_json` 保存到首个 `PuzzleDraftLevel.backgroundMusic`:
|
||||
|
||||
```json
|
||||
{
|
||||
"levelId": "puzzle-level-1",
|
||||
"backgroundMusic": {
|
||||
"taskId": "suno-task",
|
||||
"provider": "vector-engine-suno",
|
||||
"assetObjectId": "assetobj_1",
|
||||
"assetKind": "puzzle_background_music",
|
||||
"audioSrc": "/generated-puzzle-assets/..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行态后续可从当前关卡快照或作品详情读取该字段作为背景音乐源;若字段为空,继续使用现有程序化背景音乐兜底。
|
||||
|
||||
### 3.2 抓大鹅
|
||||
|
||||
抓大鹅作品级音频与物体点击音效复用 `generated_item_assets_json` 数组保存,不新增表字段:
|
||||
|
||||
1. 作品背景音乐暂存到第一个 `Match3DGeneratedItemAsset.backgroundMusic`,表示当前 work profile 的作品级背景音乐。
|
||||
2. 单个物体点击音效保存到对应 `Match3DGeneratedItemAsset.clickSound`。
|
||||
|
||||
这是一个兼容性折中:当前 Match3D work profile 没有 work-level metadata 字段,而 `generated_item_assets_json` 已经随作品详情、草稿架、运行态入口稳定传递。后续若新增正式作品 metadata 表达,应迁移 `backgroundMusic` 到作品级字段。
|
||||
|
||||
## 4. 前端交互
|
||||
|
||||
结果页 UI 保持轻量:
|
||||
|
||||
1. `音乐` Tab 只展示必要输入、生成按钮、状态与音频预览,不展示供应商规则说明。
|
||||
2. 生成完成后立即写回本地草稿状态,并触发既有保存链路或专用保存接口。
|
||||
3. 抓大鹅每个物体音效生成入口放在对应素材详情面板内,不在列表下方展开大段配置。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```powershell
|
||||
npm run check:encoding
|
||||
npm run test -- src\components\puzzle-result\PuzzleResultView.test.tsx
|
||||
npm run test -- src\components\match3d-result\Match3DResultView.test.tsx
|
||||
npm run typecheck
|
||||
cargo test -p shared-contracts creation_audio --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p shared-contracts puzzle --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p api-server vector_engine_audio_generation --manifest-path server-rs\Cargo.toml
|
||||
cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
```
|
||||
|
||||
真实生成 smoke 需要本地私密环境配置 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 与 OSS 变量。后端改动后使用 `npm run api-server` 启动,并确认 `/healthz`。
|
||||
@@ -0,0 +1,75 @@
|
||||
# 拼图画面描述直创与 AI 标签生成调整 2026-05-03
|
||||
|
||||
## 背景
|
||||
|
||||
拼图创作入口继续保留填表式体验,但入口表单不再要求百梦主提前填写作品名称和作品描述。入口只收集“拼图画面描述”,后端用该描述完成首图生成和第一关关卡名生成;进入结果页后再补作品信息。
|
||||
|
||||
## 入口表单
|
||||
|
||||
1. 点击“开始创作”后的拼图表单只展示 `画面描述`、参考图和图片模型选择。
|
||||
2. `画面描述` 是唯一必填字段,提交时写入 `pictureDescription`,并作为 `promptText` 传给 `compile_puzzle_draft`。
|
||||
3. `workTitle`、`workDescription` 不再从入口表单传入;`seedText` 只由画面描述组成,格式为 `画面描述:...`。
|
||||
4. 表单自动保存只保存画面描述,不生成图片,不消耗光点。
|
||||
5. 生成进度页“当前拼图信息”只展示画面描述,不再展示空作品名称或空作品描述。
|
||||
|
||||
## 生成进度步骤
|
||||
|
||||
1. `compile` 展示为“编译首关草稿”:根据画面描述生成首关名称和结果页草稿,不在本步骤生成作品标签。
|
||||
2. `puzzle-images` 展示为“生成首关画面”:按画面描述、参考图和当前图片模型生成第一张拼图图。
|
||||
3. `puzzle-select-image` 展示为“写入正式草稿”:把首图设为第一关正式图,并同步到结果页草稿。
|
||||
4. `ready` 文案提示进入结果页补作品信息;不得暗示作品名称、作品描述或作品标签已经完整生成。
|
||||
|
||||
### 2026-05-08 进度页预计等待与步骤动效补充
|
||||
|
||||
1. 拼图草稿生成进度页预计等待时间固定按 `60` 秒展示和倒计时,后端真实完成后立即进入结果页,不强制等满 60 秒。
|
||||
2. 60 秒进度拆成三段:`compile` 约 12 秒,`puzzle-images` 约 42 秒,`puzzle-select-image` 约 6 秒。
|
||||
3. 生成中即使后端 `compile_puzzle_draft` 仍是一次同步 action,前端也必须按本地计时推进总进度和当前步骤进度,避免页面停在静态等待态。
|
||||
4. 每个步骤卡片都展示独立进度条;已完成步骤显示 100%,当前步骤按该段预计时长推进,后续步骤保留 0% 待处理状态。
|
||||
5. 后端未返回前总进度最多推进到 98%,防止 UI 提前宣称生成完成;只有 action 成功并写回 `ready` 后才显示 100%。
|
||||
|
||||
## 草稿默认值
|
||||
|
||||
1. 后端先由 `module-puzzle` 生成可回滚的确定性草稿,再由 `api-server` 生成第一关关卡名。图片生成前可先基于画面描述生成临时关卡名;正式图片生成完成后,必须使用 APIMart Chat Completions 的 `gpt-4o-mini`,把正式图片 data URL 与画面描述一起传入模型,生成最终关卡名。
|
||||
2. 最终关卡名生成后,必须写回首关 `levelName`,并在入口直创默认场景下作为 `workTitle` 同步写入草稿和作品草稿卡;模型不可用、图片压缩失败或返回非法时,才保留前一步文本名或确定性兜底名。
|
||||
3. `workDescription` 默认保持空字符串,不再回退为画面描述。
|
||||
4. `themeTags` 默认保持空数组,不再由入口画面描述自动推断为正式作品标签。
|
||||
5. `formDraft` 只保留 `pictureDescription`,`workTitle` 与 `workDescription` 为空。
|
||||
|
||||
## 作品标签
|
||||
|
||||
1. 作品信息 Tab 继续支持手动新增、删除标签。
|
||||
2. 作品标签合法数量仍为 `3~6` 个,发布前和后端发布逻辑都要检查。
|
||||
3. 新增 `generate_puzzle_tags` action:
|
||||
- 前端点击 AI 生成标签时先检查作品名称和作品描述。
|
||||
- 若任一为空,前端直接提示先填写,不请求后端。
|
||||
- 两者都不为空时,后端基于作品名称和作品描述调用文本模型,生成 6 个中文短标签。
|
||||
- 生成结果回写 session draft 与 puzzle work profile,前端直接使用返回 session 更新界面。
|
||||
4. AI 标签生成失败时可以降级为确定性关键词标签,但仍必须返回去重后的 6 个标签,保证用户能继续编辑。
|
||||
|
||||
## 保存与发布
|
||||
|
||||
1. 用户在结果页修改作品名称、作品描述、作品标签、关卡名称或画面描述时,继续通过 `PUT /api/runtime/puzzle/works/{profileId}` 自动保存。
|
||||
2. 自动保存允许标签为空,用于支持初始草稿和用户清空标签后的继续编辑。
|
||||
3. 发布前必须检查:
|
||||
- 每个关卡名称非空。
|
||||
- 作品名称非空。
|
||||
- 作品描述非空。
|
||||
- 作品标签数量为 `3~6`。
|
||||
- 每关正式图存在。
|
||||
4. `publish_puzzle_work` 仍由 SpacetimeDB procedure 执行最终校验和发布,前端不能绕过后端门禁。
|
||||
|
||||
## 结果页返回
|
||||
|
||||
1. 从拼图草稿结果页点击左上角返回时,直接回到平台创作页。
|
||||
2. 结果页返回不回到上一页填表工作区;表单页只作为发起新草稿或恢复纯表单草稿的入口。
|
||||
3. 返回创作页时清理拼图生成态、运行态和临时操作态,保留后端已保存的草稿,用户后续从作品卡继续完善。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 拼图入口表单不再出现作品名称和作品描述输入框。
|
||||
2. 只填写画面描述即可生成草稿、图片和第一关关卡名。
|
||||
3. 进入结果页后作品名称默认为模型生成的第一关关卡名,作品描述为空,作品标签为空。
|
||||
4. 点击 AI 生成标签时,作品名称或作品描述为空会先提示补齐。
|
||||
5. 作品名称和作品描述都不为空时,AI 生成 6 个作品标签,并自动保存到后端。
|
||||
6. 手动增删标签仍可用,发布前标签必须至少 3 个且最多 6 个。
|
||||
7. 拼图草稿结果页左上角返回直接回到创作页,不再显示上一页表单。
|
||||
@@ -56,3 +56,9 @@
|
||||
3. 标签少于 `3` 个时,发布弹窗明确提示“正式标签数量必须在 3 到 6 之间”。
|
||||
4. 标签补到 `3~6` 个后,无需刷新页面即可通过前端发布校验。
|
||||
5. 结果页顶部能看到轻量自动保存状态,不额外堆叠说明文案。
|
||||
|
||||
## 2026-05-09 发布失败提示补充
|
||||
|
||||
`publish_puzzle_work` 属于资产操作发布入口,按 `ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md` 会在发布 mutation 前预扣 `1` 枚光点。余额不足时后端返回 `409 CONFLICT`,响应 `details.message` 为 `光点余额不足`,这属于业务拒绝,不是拼图发布接口不可用。
|
||||
|
||||
结果页发布弹窗必须在用户点击发布后继续展示后端错误原因,不能只把错误写到弹窗背后的页面 banner。这样余额不足、SpacetimeDB 发布门禁或其他后端业务错误都会在当前独立发布面板中直接可见。
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
7. 当前作品没有下一关时,通关弹窗展示后端 handoff 返回的相似作品;用户点击具体候选作品时直接 `startPuzzleRun(profileId, null)`,从目标作品第 `1` 关重新开始。
|
||||
8. 失败状态点击“重新开始”时,正式 run 使用当前关 `levelId` 重新 `startPuzzleRun`,草稿/本地 run 使用本地重建,二者都保留当前失败关卡。
|
||||
9. 结果页草稿试玩没有正式后端 run 时,继续使用本地 run、local leaderboard 和本地下一关兜底。
|
||||
10. 运行态输入采用全项目通用的 `src/services/input-devices/` 抽象层承接,指针、触控、mocap 等设备都先归一为 `press / move / release / tap / drop` 拖拽语义,再由拼图运行态解析具体拼块和落点。
|
||||
11. mocap `grab` 不是点击选中语义,而是持续拖拽语义;松手时按当前棋盘归一坐标提交 drop。合并大块只需要提交其中任一成员拼块 `pieceId`,本地拼图运行时会按 `mergedGroupId` 解析整组平移。
|
||||
12. 拼图作品详情或开局遇到后端 `404 / NOT_FOUND / 资源不存在` 时,平台入口不再停留在空详情或运行态错误页,而是清理当前拼图详情/run 状态并返回首页。
|
||||
|
||||
## 工程落点
|
||||
|
||||
@@ -38,6 +41,15 @@
|
||||
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
|
||||
- 公开拼图玩法交互测试断言前端本地交换函数被调用。
|
||||
- 同时断言后端 `swap / drag` 不参与棋盘交互,后端 `leaderboard / next-level` 继续参与非即时链路。
|
||||
4. `src/services/input-devices/`
|
||||
- `runtimeDragInputController` 提供设备无关的拖拽会话状态机。
|
||||
- `runtimeInputGeometry` 提供屏幕坐标、归一坐标和网格命中的通用转换能力。
|
||||
- 玩法组件只传入“这个点对应哪个目标”和“drop 到哪个目标”的玩法解释,不在输入层写拼图专用规则。
|
||||
5. `src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`
|
||||
- 鼠标/触控与 mocap 共用同一个 runtime drag controller。
|
||||
- 合并块成员不再被 mocap 路径过滤;mocap 可从合并块任一占用格抓取,并复用本地运行时的大块拖拽规则。
|
||||
6. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- `openPuzzleDetail`、`openPuzzlePublicWorkDetail`、`startPuzzleRunFromProfile` 对拼图作品缺失统一回首页。
|
||||
|
||||
## 边界
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
不能继续写到仓库本地 `public/generated-puzzle-covers/*`。
|
||||
|
||||
这些路径只是前后端 DTO 里的兼容标识,不是浏览器可以直接裸读的公开资源地址。实际图片对象存放在私有 OSS 中,前端渲染前必须先通过 `/api/assets/read-url?legacyPublicPath=...` 换取签名读 URL;签名 URL 未返回或换签失败时,图片组件不能把 `/generated-puzzle-assets/*` 直接写入 `<img src>`,避免浏览器发起无签名、无鉴权请求。
|
||||
这些路径只是前后端 DTO 里的兼容标识,不是浏览器可以直接裸读的公开资源地址。实际图片对象存放在私有 OSS 中,前端渲染前必须先通过 `/api/assets/read-url?legacyPublicPath=...` 换取签名读 URL;签名 URL 未返回或换签失败时,图片组件不能把 `/generated-puzzle-assets/*` 或无前导斜杠的 `generated-puzzle-assets/*` 直接写入 `<img src>`,避免浏览器发起无签名、无鉴权请求。
|
||||
|
||||
### 4.2 运行态边界
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# 拼图创作模板表单与 gpt-image-2 Skill 封装 2026-05-03
|
||||
|
||||
## 背景
|
||||
|
||||
拼图创作入口已经从对话式 Agent 收口为填表式表单。本次改版目标是让“点击拼图创作”后的表单更接近图像创作工具的单屏体验:优先上传参考图或填写画面描述,然后直接生成首关草稿与首张拼图图。2026-05-07 起,入口表单删除 Template 模块,模板参考图只保留为历史资产,不再作为表单首屏交互。
|
||||
|
||||
## 落地范围
|
||||
|
||||
1. `src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`
|
||||
- 改为顶部标题、超大参考图上传区、大输入框、底部操作区的布局。
|
||||
- 保留参考图上传、模型切换和生成草稿。
|
||||
- 删除 Template 模块与模板卡切换入口。
|
||||
- 不再提供输入框底部的 `try` 示例入口。
|
||||
2. `src/components/puzzle-agent/puzzleCreationTemplates.ts`
|
||||
- 保留历史拼图创作模板数据,当前表单不再消费。
|
||||
- 后续若重新开放模板入口,必须先更新本文档和移动端首屏验收。
|
||||
3. `public/puzzle-creation-templates/`
|
||||
- 存放历史模板样例图。
|
||||
- 样例图不作为正式拼图作品资产,当前也不再出现在拼图入口表单。
|
||||
4. `public/creation-type-references/`
|
||||
- 存放平台创作入口和玩法类型弹层的参考图。
|
||||
- 每个玩法一个参考图,首版用于视觉识别,不承载规则说明。
|
||||
- 当前创作 Tab 顶部玩法卡带必须直接渲染这些图片,避免参考图只出现在隐藏弹层里。
|
||||
5. `.codex/skills/gpt-image-2-apimart/`
|
||||
- 历史目录名保留,实际封装仓库内 `gpt-image-2` 的 VectorEngine 调用流程。
|
||||
- Skill 默认读取本地环境变量,不把密钥写入代码、文档或前端。
|
||||
|
||||
## UI 规则
|
||||
|
||||
1. 顶部主标题展示“想做个什么玩法?”和轻量状态标识,不写玩法规则说明。
|
||||
2. 上传拼图图片区优先占据首屏左侧/上方的大块区域,移动端也必须完整露出。
|
||||
- 上传区自身就是图片卡片,不再额外封装为 `platform-subpanel` 模块壳。
|
||||
- 亮色主题下上传卡片必须使用白色或暖浅色卡面,不得显示整块黑色底。
|
||||
- 上传卡片固定为 1:1 正方形,避免拼图主画面在首屏出现非正方形预期。
|
||||
- 移动端表单主体不可依赖纵向拖动查看核心控件;玩法卡带、描述输入框和底部生成按钮占位固定后,上传卡片必须按剩余高度等比例缩放,仍保持 1:1。
|
||||
- 上传卡片底部不再叠加文件名 bar;`点击上传拼图图片` 入口必须显示在拼图画面卡片内部。
|
||||
- 上传卡片上方固定展示 `拼图画面` 标题。
|
||||
- 无图状态下,上传卡片内部、`点击上传拼图图片` 按钮上方展示 11px 级辅助提示 `若没有合适的图片可以通过填写画面描述生成画面`,提示用户可不上传图片、直接填写画面描述生成画面。
|
||||
- 上传成功后,`AI重绘` 开关显示在卡片左下角,右上角显示移除拼图图片图标按钮;移除必须先弹出二次确认。
|
||||
- 叠在上传卡片上的 `AI重绘`、移除图标和上传入口必须和卡面保持足够对比,避免浅色主题重映射后不可读。
|
||||
3. 画面描述输入框高度固定,移动端保持约 `6rem`,不随剩余屏幕高度变大或变小,避免把上传参考图和提交区挤出首屏。
|
||||
4. 创作 Tab 顶部玩法卡带的选中态只使用卡内暗色蒙版、细描边或内描边,不使用粉色外发光、外扩阴影或会从卡片边缘突出的高饱和边。
|
||||
5. 输入区保留:
|
||||
- 上传拼图图片按钮。
|
||||
- 图片模型切换按钮。
|
||||
6. 输入区不保留:
|
||||
- `try` 文本。
|
||||
- 示例 prompt chip。
|
||||
- 画面描述输入框默认提示词或占位示例。
|
||||
- 玩法规则说明。
|
||||
- Template 模块和模板卡切换。
|
||||
|
||||
## 历史模板抽样
|
||||
|
||||
以下模板曾用于表单 Template 模块。2026-05-07 后入口表单不再展示这些模板,列表仅作为历史资产索引:
|
||||
|
||||
1. 情侣合照拼图
|
||||
2. 家庭纪念拼图
|
||||
3. 朋友聚会拼图
|
||||
4. 节日贺卡拼图
|
||||
5. 知识点总结拼图
|
||||
6. 商品细节拼图
|
||||
7. 治愈风景拼图
|
||||
8. 宠物可爱拼图
|
||||
9. 热点海报拼图
|
||||
10. 活动邀请拼图
|
||||
11. 每日挑战拼图
|
||||
12. 儿童认知拼图
|
||||
|
||||
模板提示词必须是可直接送入拼图生图链路的画面描述,不写 UI、按钮、教程、规则或营销解释。
|
||||
|
||||
## gpt-image-2 Skill 规则
|
||||
|
||||
Skill 封装仓库现有后端口径:
|
||||
|
||||
```text
|
||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
model = gpt-image-2-all
|
||||
n = 1
|
||||
size = 1024x1024
|
||||
```
|
||||
|
||||
响应兼容:
|
||||
|
||||
1. `data[].url`
|
||||
2. `data[].b64_json`
|
||||
|
||||
本次 Skill 只封装生成样例图和研发复用流程,不改变正式后端接口、扣费、OSS、SpacetimeDB 写入和发布链路。
|
||||
|
||||
## 2026-05-07 AI 重绘与上传直用
|
||||
|
||||
拼图入口上传区左下角展示 `AI重绘` 开关,默认打开;未上传拼图图片前不显示开关,上传成功后才显示。上传成功后右上角展示移除图标按钮,点击后必须二次确认。
|
||||
|
||||
1. `AI重绘=true`
|
||||
- 上传区文案为 `点击上传拼图图片`,上传图作为生图参考图。
|
||||
- 未上传图片时,输入框标题为 `画面描述`。
|
||||
- 已上传图片时,输入框标题为 `画面AI重绘要求(提示词)`。
|
||||
- 展示图片模型切换。
|
||||
- `compile_puzzle_draft` 携带 `aiRedraw: true`,继续走 VectorEngine 生图与 `PUZZLE_IMAGE_GENERATION_POINTS_COST = 2` 扣费链路。
|
||||
- 生成按钮展示 `消耗2光点`。
|
||||
2. `AI重绘=false`
|
||||
- 隐藏画面描述输入框和模型切换。
|
||||
- 必须上传拼图图片,按钮不展示 `消耗2光点`。
|
||||
- `compile_puzzle_draft` 携带 `aiRedraw: false`,后端只编译草稿和生成首关名,不调用 VectorEngine,不进入光点扣费 wrapper。
|
||||
- 后端把上传图片 Data URL 按拼图资产路径持久化,构造 `sourceType=uploaded` 的候选图并直接选为第一关正式图。
|
||||
3. 上传裁剪
|
||||
- 前端读取上传图原始宽高。
|
||||
- 非 1:1 图片必须先弹出正方形裁剪工具,裁剪完成后再进入表单状态和提交 payload。
|
||||
- 裁剪工具必须在完整原图上展示正方形裁剪框,支持拖拽框内区域移动,以及拖拽四边或四角调整裁剪边界,不再展示 `缩放 / 横向 / 纵向` 参数滑杆。
|
||||
- 裁剪输出仍按参考图体积预算压缩,避免上传图撑爆 JSON body。
|
||||
|
||||
契约字段同步:
|
||||
|
||||
```ts
|
||||
CreatePuzzleAgentSessionRequest.aiRedraw?: boolean
|
||||
PuzzleAgentActionRequest.compile_puzzle_draft.aiRedraw?: boolean
|
||||
```
|
||||
|
||||
Rust 共享契约使用 `ai_redraw: Option<bool>` 并按 camelCase 序列化为 `aiRedraw`。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 点击拼图创作后,移动端表单无需纵向拖动即可看到大参考图区、固定高度文本输入框和 `生成拼图游戏草稿` 按钮。
|
||||
2. 输入框里没有 `try` 示例功能。
|
||||
3. 图片模型切换仍可打开并选择 `gpt-image-2` / `nanobanana2`。
|
||||
4. 历史模板样例图文件可保留,但不出现在拼图入口表单。
|
||||
5. 当前创作 Tab 顶部的拼图、方洞挑战、视觉小说和 AIRP 卡片能看到对应 `creation-type-references` 图片。
|
||||
6. 默认 `AI重绘` 打开时,无图状态展示 `画面描述` 与 `消耗2光点`;上传图片后输入框标题改为 `画面AI重绘要求(提示词)`。
|
||||
7. 关闭 `AI重绘` 后隐藏画面描述输入框,生成按钮不展示 `消耗2光点`,后端直接应用上传图片为第一关图片。
|
||||
8. 上传非 1:1 图片时必须先通过拖拽裁剪框完成正方形裁剪。
|
||||
9. gpt-image-2 Skill 校验通过,且脚本 dry-run 能输出计划请求而不泄露密钥。
|
||||
10. `npm run check:encoding` 通过。
|
||||
@@ -5,10 +5,37 @@
|
||||
## 文档列表
|
||||
|
||||
- [WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md](./WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md):记录微信小程序 `web-view` 壳的最小接入范围、需要填写的 H5 业务域名、微信后台配置和后续原生化边界。
|
||||
- [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”后端 DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界、shared contracts、作品配置、runtime run、派生成绩、排行榜、`work_play_start` 埋点、migration/绑定生成策略,以及不保存原始麦克风音频的隐私与反作弊约束。
|
||||
- [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”2D 浏览器 runtime 技术方案,明确 Phaser + TypeScript + Vite 选型、纯 TS simulation 与 Phaser renderer/DOM HUD 边界、Web Audio 输入适配、移动端权限降级和后续测试验证命令。
|
||||
- [PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md](./PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md):记录直接访问公开作品详情深链时作品不存在或已下架的回首页修复,避免关闭提示后停在 `work-detail` 空状态白屏。
|
||||
- [BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md](./BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md):冻结寓教于乐 `宝贝识物` 模板创作发布线程的前端入口、契约、service、结果页、发布标签和后端 image-2 接口预留边界。
|
||||
- [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开发落地规格,覆盖横屏展示、摄像头背景虚化、角色剪影、绿色圆环 2 秒保持、动作教学、当前会话内空间边界记录和后续关卡安全暂停规则。
|
||||
- [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md):记录运行态输入设备抽象层,明确鼠标、触控、mocap 等设备统一归一为通用拖拽语义,玩法组件只负责解释目标和落点。
|
||||
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异;同时冻结 `shared-contracts` 不得反向依赖 `platform-*`,避免 SpacetimeDB 模块发布时拉入 `wasm-bindgen`。
|
||||
- [DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md](./DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md):记录本地完整 Rust 栈启动时 `api-server`、主站 Vite 和后台 Vite 端口占用的误判根因、脚本预检策略和 Windows 排障命令。
|
||||
- [VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md](./VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md):记录 GPT-image-2 图片生成从 APIMart 迁移到 VectorEngine `gpt-image-2-all` 的接口、环境变量、尺寸映射、错误口径和验收命令。
|
||||
- [SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md](./SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md):记录本地 `spacetime publish` 被 sccache wrapper 通信异常阻断时的根因、debug 构建参数口径和手动排障命令。
|
||||
- [AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md](./AUTH_RESTORE_AND_RECOMMEND_LOADING_FIX_2026-05-09.md):记录刷新网页后登录态失效和推荐页作品卡卡在加载中的联合修复,覆盖 `AuthGate` 本地 token 优先恢复、refresh 失败不清 token、推荐页启动请求版本保护和错误态收口。
|
||||
- [USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.md](./USER_TAG_INVITE_AND_PUZZLE_LEADERBOARD_2026-05-10.md):冻结用户标签字段、后台邀请码授予标签、用户填写邀请码后合并账号标签,以及拼图排行榜只展示白名单标签 `北科` 的落地口径。
|
||||
- [RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md](./RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md):记录平台推荐页自动加载作品、公开拼图作品完整运行态、平台 bootstrap 私有投影刷新和展示层图片换签的局部请求 `401` 不应扩散成全局登出的修复,覆盖 `authImpact: local` 请求策略、推荐页 embedded 运行态启动、拼图开局/排行榜/下一关和回归测试。
|
||||
- [AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md](./AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md):记录 `AuthGate` 登录成功后又被旧 hydrate 覆盖回未登录态的竞态根因、版本号保护修复与回归测试。
|
||||
- [HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md](./HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md):记录 Hyper3D Rodin Gen-2 文生 3D 模型、图生 3D 模型、状态查询和下载列表的后端代理、环境变量、请求约束与验收边界。
|
||||
- [MATCH3D_RODIN_ASSET_TAB_2026-05-10.md](./MATCH3D_RODIN_ASSET_TAB_2026-05-10.md):记录抓大鹅结果页多 Tab 改造与 Rodin 3D 素材列表/详情页的前端接入边界,明确首版只复用 Hyper3D 后端代理,不新增表或正式资产写入。
|
||||
- [MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md](./MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md):冻结抓大鹅草稿生成过程页、3 件物品名称生成、VectorEngine 1:1 素材图、n*n 切图、并行 Rodin 图生 3D 与 OSS 回填草稿页的端到端边界。
|
||||
- [VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md](./VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md):记录火山引擎大模型 ASR 双向流式、TTS WebSocket 双向流式和 TTS HTTP SSE 单向流式的后端代理、环境变量、协议帧和验收边界。
|
||||
- [VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md](./VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md):记录视觉小说结果页接入 VectorEngine Suno 文生背景音乐与 Vidu 文生音效的接口、环境变量、后端路由、OSS 资产回写和前端弹层交互边界。
|
||||
- [PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md](./PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md):冻结“我的”页签帮助与反馈入口的后端接入方案,覆盖 `POST /api/profile/feedback`、`profile_feedback_submission`、凭证图片 Data URL 校验和前端预览/提交边界。
|
||||
- [API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md](./API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md):冻结 api-server 外部服务配置边界,公共服务 URL 可保留代码默认值,非公共模型名和私有网关 URL 统一通过环境变量注入。
|
||||
- [CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md](./CREATIVE_INTERACTIVE_CONTENT_AGENT_TECHNICAL_SOLUTION_2026-05-05.md):冻结基于 LangChain-Rust 的创意互动内容生成 Agent 技术方案,明确首版只支持拼图模板、必须显式展示模板选择和积分范围,通过拼图模块 Tool/模板协议填充同一份草稿字段,支持单关卡与多关卡图片生成、立即试玩、表单化编辑和 Agent 自然语言修订草稿字段。
|
||||
- [VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md](./VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md):记录视觉小说模板 `VN-03` Prompt / LLM 工具落地,包含创作底稿 Prompt、运行时 GM Prompt、repair Prompt、工具参数 schema、Responses 请求口径和定向验证结果。
|
||||
- [VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md](./VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md):记录视觉小说模板 `VN-13` 实现收口、当前正式入口、表目录、路由、作品 / 运行 / 资产和负向扫描口径。
|
||||
- [PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md](./PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md):冻结个人任务与埋点系统首版方案,明确 `tracking_event`、`tracking_daily_stat`、`profile_task_config`、任务进度、领奖记录和光点钱包流水的边界。
|
||||
- [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。
|
||||
- [MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md](./MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md):冻结 Maincloud 历史残留引用禁用策略,明确后续不得新增、运行或引用 `api-server:maincloud`、`GENARRATIVE_SPACETIME_MAINCLOUD_*` 和相关测试/文档口径。
|
||||
- [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md):冻结单机生产部署目标,从旧一体化启动脚本切到 Nginx、systemd 托管 SpacetimeDB 与 Rust `api-server`,并记录生产 Jenkins 流水线拆分计划和首批部署骨架。
|
||||
- [PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md](./PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md):记录拼图正式平台入口移动、交换、合并、拆分和通关裁决收回前端即时运行态,排行榜、下一关和游玩记录继续由后端持久化处理。
|
||||
- [RPG_FOUNDATION_DRAFT_ROLE_DOSSIER_TIMEOUT_FALLBACK_2026-05-02.md](./RPG_FOUNDATION_DRAFT_ROLE_DOSSIER_TIMEOUT_FALLBACK_2026-05-02.md):记录 `agent-foundation-*-dossier-batch-*` 无搜索 Responses 请求超时后的本地养成档案兜底,避免底稿主链被尾部角色润色阶段阻断。
|
||||
- [RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md](./RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md):记录 RPG 角色主图与场景幕背景图统一迁移到 APIMart OpenAI 兼容 `gpt-image-2` 生图入口的边界、配置和验收口径。
|
||||
- [RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md](./RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md):记录 RPG 角色主图与场景幕背景图统一迁移到 `gpt-image-2` 生图入口的边界、配置和验收口径;2026-05-09 起实际上游以 VectorEngine 迁移文档为准。
|
||||
- [RPG_FOUNDATION_DRAFT_LANDMARK_SEED_BATCH_TIMEOUT_FIX_2026-05-02.md](./RPG_FOUNDATION_DRAFT_LANDMARK_SEED_BATCH_TIMEOUT_FIX_2026-05-02.md):记录 `agent-foundation-landmark-seed-batch-1` 无搜索 Responses 请求超时的根因,并将场景骨架批次收敛为单场景生成。
|
||||
- [PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md](./PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md):记录“我的”和“存档”页面在本地把 `/api/profile/*` 请求落到 Vite SPA fallback、导致 HTML 被当 JSON 解析的根因,以及 `/api/profile` 代理补齐与回归测试。
|
||||
- [SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md](./SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md):记录 `WP-DEL 删除旧层与命名收口`,物理删除旧 runtime story HTTP DTO、前端 `Rpg*` alias、旧 `/api/custom-world/*` 非 runtime 前缀、Puzzle `local-next-level` 入口和 `/generated-*` 资产直读代理;生成资产读取统一走 OSS read-url 链路。
|
||||
@@ -21,7 +48,7 @@
|
||||
- [SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md](./SERVER_RS_DDD_WP_RS_RUNTIME_STORY_CLOSURE_2026-05-01.md):记录 `WP-RS Runtime Story` 写链路收尾,补齐 `/api/story/sessions/runtime` 与 `/api/story/sessions/{storySessionId}/actions/resolve`,统一返回 `StoryRuntimeMutationResponse.projection`,并保持旧 `/api/runtime/story/*` 未挂载。
|
||||
- [SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md](./SERVER_RS_DDD_WP_CW_ACTION_AND_DOMAIN_SPLIT_2026-04-30.md):记录 `WP-CW Custom World` 的领域拆分与 Agent action 收口,将 `module-custom-world` 大 `lib.rs` 拆入 DDD 骨架,并移除 Custom World 运行代码中的最小兼容占位动作。
|
||||
- [SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md](./SERVER_RS_DDD_WP_BF_AND_G2_DRIFT_CLEANUP_2026-04-30.md):记录 `WP-BF Big Fish` 物理拆分漂移和 G2 迁移期口径清理,将 Big Fish 创作域类型、命令、应用规则和错误层拆入 DDD 文件,并清理剩余 `过渡落位` 注释。
|
||||
- [SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md](./SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md):记录 `tests-support` 从目录占位收口为 `server-rs` workspace 共享测试支撑 crate,首版提供 Maincloud healthz 与 HTTP smoke 通用断言。
|
||||
- [SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md](./SERVER_RS_DDD_TESTS_SUPPORT_CRATE_CLOSURE_2026-04-30.md):记录 `tests-support` 从目录占位收口为 `server-rs` workspace 共享测试支撑 crate;该历史文档中的旧 Maincloud 口径不再作为当前执行依据,当前 smoke 以通用 `/healthz` 为准。
|
||||
- [SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md](./SERVER_RS_DDD_WP_BF_RUNTIME_BACKEND_TRUTH_2026-04-29.md):记录 `WP-BF Big Fish` 运行态从前端本地规则切到 Rust 领域真相源、SpacetimeDB run 表、API facade 和前端新接口接入的关闭口径。
|
||||
- [SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md](./SERVER_RS_DDD_WP_PF_PLATFORM_ERROR_CLASSIFICATION_2026-04-29.md):记录 `WP-PF platform side effects` 平台副作用收口,统一 LLM、OSS、SMS、微信平台错误分类与 API 映射,并将微信 OAuth provider 下沉到 `platform-auth`。
|
||||
- [SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md](./SERVER_RS_DDD_WP_RT_ADAPTER_API_CLOSURE_2026-04-29.md):记录 `WP-RT Runtime/Profile/Save` Adapter/API 收口,将 checkpoint、profile/save archive meta、充值/邀请/兑换/钱包等剩余纯规则迁入 `module-runtime`,移除 `/api/runtime/profile/*` 旧兼容挂载并对齐前端 `/api/profile/*` 请求路径。
|
||||
@@ -57,13 +84,13 @@
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md):冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”。
|
||||
- [SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md](./SPACETIMEDB_CLOUD_CONFIG_REMOVAL_2026-05-02.md):记录旧云端 SpacetimeDB 配置、发布脚本和默认文档口径的移除结果,冻结后续仅使用本地或显式 `SERVER_URL` 的运维规则。
|
||||
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
|
||||
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的数据目录/replica 数据残留根因、备份重建步骤和脚本诊断口径。
|
||||
- [AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md](./AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md):记录远端库挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。
|
||||
- [LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md](./LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md):冻结 RPG 运行时剧情推理使用 `doubao-seed-character-251128` 的 `/chat/completions`,以及所有模板创作大模型推理使用 `deepseek-v3-2-251201` 的 `/responses`。
|
||||
- [PROFILE_INVITE_CODE_REGISTRATION_AND_ADMIN_2026-04-30.md](./PROFILE_INVITE_CODE_REGISTRATION_AND_ADMIN_2026-04-30.md):冻结邀请码从“我的 Tab 填写”迁到注册环节的前后端边界、`profile_invite_code.metadata_json` 表结构扩展、管理员邀请码虚拟主体和奖励规则。
|
||||
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
|
||||
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts,以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
|
||||
- [MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md](./MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md):记录抓大鹅 F1 创作入口、Agent 工作区、参考图入口、本地 mock client 与后续 B5 HTTP facade 替换点。
|
||||
- [MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md](./MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md):记录抓大鹅 F1 创作入口与当前拼图式内嵌 Tab 表单,明确入口页仅保留题材大输入框和难度选项,由难度选项派生消除次数与难度数值。
|
||||
- [MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md](./MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md):冻结抓大鹅 F2 结果页、基础信息编辑、发布前试玩入口、发布门槛、自动保存和已发布作品二次编辑恢复口径。
|
||||
- [MATCH3D_SPACETIME_CLIENT_AND_API_FACADE_2026-04-30.md](./MATCH3D_SPACETIME_CLIENT_AND_API_FACADE_2026-04-30.md):记录抓大鹅 B4+B5 已落地的 SpacetimeDB bindings、`spacetime-client` facade、`api-server` HTTP 路由、shared contract 对齐和验收命令。
|
||||
- [MATCH3D_CREATION_ENTRY_COMING_SOON_2026-05-01.md](./MATCH3D_CREATION_ENTRY_COMING_SOON_2026-05-01.md):记录抓大鹅创作页入口重新开放、首屏与弹层分流一致,以及公开广场失败不污染创作错误态的边界。
|
||||
@@ -82,6 +109,8 @@
|
||||
- [BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md](./BIG_FISH_MAIN_IMAGE_TRANSPARENT_BACKGROUND_ALIGNMENT_2026-04-28.md):记录大鱼吃小鱼等级主图与动作关键帧正式图在 Rust 后端复用 RPG 角色主图透明背景 alpha 后处理的对齐口径,并明确场地背景不走该处理。
|
||||
- [PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md](./PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md):记录拼图生成图片回到 1:1,运行时拖动、交换、合并与拆分由前端即时裁决,以及移动端棋盘贴近屏幕边缘的落地边界。
|
||||
- [PUZZLE_FORM_CREATION_FLOW_2026-04-29.md](./PUZZLE_FORM_CREATION_FLOW_2026-04-29.md):冻结拼图填表式创作入口、初始表单自动保存草稿、生成前退出后的表单恢复,以及草稿编译/首图生成的前后端边界。
|
||||
- [PUZZLE_PICTURE_ONLY_CREATION_AND_AI_TAGS_2026-05-03.md](./PUZZLE_PICTURE_ONLY_CREATION_AND_AI_TAGS_2026-05-03.md):记录拼图入口只填写画面描述、首关名默认作品名、作品描述和标签初始为空、AI 生成 6 个作品标签以及发布前校验的落地规则。
|
||||
- [PUZZLE_TEMPLATE_FORM_AND_GPT_IMAGE_SKILL_2026-05-03.md](./PUZZLE_TEMPLATE_FORM_AND_GPT_IMAGE_SKILL_2026-05-03.md):记录拼图入口模板样例图与 gpt-image-2 Skill 约定,2026-05-07 起表单不再展示 Template 模块,改为大参考图区 + 大输入框的单屏布局。
|
||||
- [PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md](./PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md):记录拼图第二关排行榜提交以前端当前关卡为准、不被 SpacetimeDB 旧 run 快照误杀,以及 RPG 创作入口改为敬请期待的落地边界。
|
||||
- [PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md](./PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md):记录拼图通关后优先同作品下一关、无下一关时按 RPG/build 标签语义相似度返回三个候选作品,并在跨作品时只切换到候选作品第 1 张图、运行时关卡序号继续累进的落地规则。
|
||||
- [PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md](./PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md):记录拼图失败后重新开始/付费续时,以及进入作品与过关后同步存档页投影的落地规则。
|
||||
@@ -89,11 +118,11 @@
|
||||
- [RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md](./RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md):记录编辑器幕预览卡在“正在载入这一幕”时的启动态根因,收口预览本地运行态装配与禁持久化首段 story 注入。
|
||||
- [PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](./PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md):记录拼图结果页名称与标签编辑自动保存、发布门槛统一到 `3~6` 标签,以及前端发布校验不再被旧 session blocker 卡死的修复口径。
|
||||
- [WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md](./WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md):记录作品作者以 `owner_user_id` 为真相源,API 按用户 ID 解析最新昵称与公开用户码,历史 `author_display_name` 仅作为兼容回退。
|
||||
- [SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md](./SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md):记录发布包 `start.sh` 只输出“SpacetimeDB 进程在就绪前退出”时的诊断补强,启动失败或超时时自动回显 `logs/spacetimedb.log`、`server ping`、端口监听和 root-dir 相关进程。
|
||||
- [SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md](./SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md):历史事故记录,保留旧发布包 `start.sh` 只输出“SpacetimeDB 进程在就绪前退出”时的诊断补强;该文档不再作为当前发布或人工排障依据。
|
||||
- [RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md](./RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md):记录 RPG 运行时 NPC 聊天、RPG/自定义世界 Agent 与大鱼 Agent 从“拼完整 SSE 字符串后一次性返回”改为 `mpsc + Sse<Event>` 真流式输出的后端落地口径。
|
||||
- [SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md](./SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md):记录发布包 `start.sh` root-dir 占用检测把 `grep -F .../.spacetimedb` 误判为 SpacetimeDB 实例的根因、脚本修复和现场处理方式。
|
||||
- [SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md](./SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md):历史事故记录,保留旧发布包 `start.sh` 占用检测把 `grep -F .../.spacetimedb` 误判为 SpacetimeDB 实例的根因;该文档不再作为当前发布或人工排障依据。
|
||||
- [RPG_BATTLE_HEALTHBAR_AND_ACTION_PRESENTATION_FIX_2026-04-26.md](./RPG_BATTLE_HEALTHBAR_AND_ACTION_PRESENTATION_FIX_2026-04-26.md):记录 RPG 战斗血条安全锚点、服务端战斗回包前端短表现,以及 `battle_use_skill` 指定技能兜底结算的修复口径。
|
||||
- [SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md](./SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md):记录发布包 `start.sh` 执行 `spacetime publish` 遇到 `403 Forbidden` 的身份根因、`.spacetimedb/` root-dir 隔离修复和排查步骤。
|
||||
- [SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md](./SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md):历史事故记录,保留旧发布包执行 `spacetime publish` 遇到 `403 Forbidden` 的身份根因;当前人工命令禁止使用 `spacetime --root-dir`,CI/CD 脚本内部受控用法除外。
|
||||
- [SPACETIMEDB_TABLE_CATALOG.md](./SPACETIMEDB_TABLE_CATALOG.md):持续维护当前 SpacetimeDB 表目录,按领域说明每张表的作用、字段结构、索引和常用 `spacetime sql` 查询模板。
|
||||
- [RPG_OPENING_SCENE_ACT_IMAGE_PRESENTATION_SYNC_2026-04-26.md](./RPG_OPENING_SCENE_ACT_IMAGE_PRESENTATION_SYNC_2026-04-26.md):记录开局场景与普通场景复用同一场景展示解析服务,修复列表幕缩略图和详情幕背景预览图片不一致的问题。
|
||||
- [FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md](./FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md):记录网站启动后首次加载约三分钟的前端根因,收口 `RouteImageReadyGate` 首屏图片门控和 Vite dev server 无关文件监听范围。
|
||||
@@ -112,7 +141,7 @@
|
||||
- [CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md](./CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md):冻结创作中心作品货架统一视图模型,先在前端归一 RPG、大鱼、拼图 works 的展示字段、筛选状态和卡片动作语义,不新增后端聚合接口。
|
||||
- [PUZZLE_BIG_FISH_DRAFT_PROGRESS_AND_ASSET_CHAIN_2026-04-25.md](./PUZZLE_BIG_FISH_DRAFT_PROGRESS_AND_ASSET_CHAIN_2026-04-25.md):冻结拼图与大鱼吃小鱼点击生成草稿后进入独立进度页,并一次性生成草稿、图片与动作资产的前端编排边界。
|
||||
- [BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md](./BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md):记录大鱼吃小鱼从固定摇杆改为屏幕首触点方向控制,并要求本地直达局在未操作时保持对象运动。
|
||||
- [RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md](./RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md):记录 `server-rs` 无参数 `cargo build` 链接 `spacetime-module` 失败的根因,并冻结默认只构建原生 `api-server`、模块产物继续走 `spacetime build` 的命令边界。
|
||||
- [RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md](./RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md):记录 `server-rs` 无参数 `cargo build` 链接 `spacetime-module` 失败的根因,并冻结默认只构建原生 `api-server`、模块产物继续走 `spacetime publish --build-options="--debug"` 或发布脚本的命令边界。
|
||||
- [BIG_FISH_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md](./BIG_FISH_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md):记录 `/big-fish` 大鱼吃小鱼玩法直达入口,明确复用现有 `BigFishRuntimeShell` 和本地占位运行态的调试边界。
|
||||
- [PUZZLE_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md](./PUZZLE_DIRECT_ROUTE_PLAYGROUND_2026-04-24.md):记录 `/puzzle` 拼图玩法直达入口,明确复用现有 `PuzzleRuntimeShell` 和本地占位图运行态的调试边界。
|
||||
- [FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md](./FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md):记录平台入口、RPG 创作、拼图创作和大鱼吃小鱼创作各页面的独立前端路径,以及与 `/puzzle`、`/big-fish` 调试直达入口的边界。
|
||||
@@ -151,6 +180,7 @@
|
||||
- [PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md](./PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md):冻结 Rust `api-server + module-auth + platform-auth` 接入真实阿里云短信 provider 的 crate 边界、发送与校验职责、配置项和错误语义。
|
||||
- [PHONE_SMS_ALIYUN_RESPONSE_FIELD_MAPPING_FIX_2026-04-23.md](./PHONE_SMS_ALIYUN_RESPONSE_FIELD_MAPPING_FIX_2026-04-23.md):记录 Rust `platform-auth` 把阿里云 PascalCase 响应字段误判成空值的问题根因,并冻结字段映射修复与回归标准。
|
||||
- [PHONE_SMS_SEND_CODE_OBSERVABILITY_FIX_2026-04-23.md](./PHONE_SMS_SEND_CODE_OBSERVABILITY_FIX_2026-04-23.md):冻结手机号验证码发送链路的日志补强口径,确保 `api-server`、`module-auth`、`platform-auth` 能直接暴露发送前后与错误分类关键字段。
|
||||
- [PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md](./PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md):记录真实短信 provider 返回 `UNKNOWN` / `biz.FREQUENCY` 时被误映射成登录 `500` 的根因,冻结 provider 配置错误 `503`、上游失败 `502` 的 HTTP 映射。
|
||||
- [PHONE_SMS_DELIVERY_OBSERVABILITY_AND_RECEIPT_DESIGN_2026-04-22.md](./PHONE_SMS_DELIVERY_OBSERVABILITY_AND_RECEIPT_DESIGN_2026-04-22.md):冻结短信平台受理成功与最终送达状态的区分方式、追踪字段、送达回执接口和前端提示文案边界。
|
||||
- [PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md):冻结验证清单第一项“真实短信验证码链路”的本地启动、前端操作、日志观察点、通过标准与失败排查步骤。
|
||||
- [ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md):冻结验证清单第四项“图片、视频、动作真实外部生成”的人工联调口径,明确哪些入口已接真实外部图片服务、哪些入口仍是 Stage 1 占位链,以及前端点击路径、日志观察点和通过标准。
|
||||
@@ -256,7 +286,7 @@
|
||||
- [CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-工作包-c前端结果页与编辑器拆分](./CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-%E5%B7%A5%E4%BD%9C%E5%8C%85-c%E5%89%8D%E7%AB%AF%E7%BB%93%E6%9E%9C%E9%A1%B5%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8%E6%8B%86%E5%88%86):记录工作包 C 已完成的结果页壳层拆分、编辑器目标分发与 mapper 收口、角色资产工坊 section/workflow 拆分,以及仍保留的阶段性 shared 实现边界。
|
||||
- [CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md](./CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md):创作页移动端底部 Tab、亮色主题 token 与滚动权责修复记录。
|
||||
- [RPG_FOUNDATION_DRAFT_EIGHT_ANCHOR_SEED_FIX_2026-04-25.md](./RPG_FOUNDATION_DRAFT_EIGHT_ANCHOR_SEED_FIX_2026-04-25.md):记录 RPG 创作 Agent session 八锚点进入 foundation draft seed 时被旧字段压缩的根因、修复和后续约束。
|
||||
- [TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md](./TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md):把外部仓库 TXT 模式完整迁入当前项目的冻结边界、模块映射、分阶段计划与验收清单。
|
||||
- [TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md](./TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md):旧 TXT 模式迁移方案的历史参考;视觉小说模板最新落地口径以 PRD [`AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`](../prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md) 为准,不再按外部平台工程完整迁入执行。
|
||||
- [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md):AI 生成角色形象与角色动画的技术路线。
|
||||
- [ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md](./ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md):面向编辑器的阿里云 NPC 形象与动作实验方案,按 4 条生成链路对比。
|
||||
- [PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md](./PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md):PixelMotion 产品形态与能力拆解。
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 推荐页运行态鉴权失败隔离修复
|
||||
|
||||
日期:`2026-05-09`
|
||||
|
||||
## 背景
|
||||
|
||||
登录成功进入平台推荐页后,推荐页会自动加载一个公开作品并启动嵌入式运行态。实际联调中出现过:作品刚加载出来,前端又瞬间回到未登录状态;停留在其他页面,或推荐页没有成功加载出作品时不会复现。
|
||||
|
||||
后续复测又发现:登录成功后,从推荐页点进拼图公开作品详情并启动完整拼图运行态,也可能在开局或通关后瞬间退回未登录。两类现象的底层问题一致,都是玩法/展示层局部请求把 `401` 扩散成全局鉴权事件。
|
||||
|
||||
## 根因
|
||||
|
||||
推荐页首屏的作品运行态启动是后台自动副作用,不是用户主动点击的账号操作。它会触发多条受保护请求,例如:
|
||||
|
||||
1. 拼图、抓大鹅、方洞挑战、视觉小说的 `start run`。
|
||||
2. 大鱼吃小鱼的 `start run` 与游玩记录上报。
|
||||
3. 视觉小说运行前的作品详情读取。
|
||||
|
||||
这些请求一旦遇到本地代理错配、后端短暂不可用或 token 刷新失败,原请求层会按普通受保护请求处理 `401`,清空 access token 并广播全局鉴权变更。`AuthGate` 收到事件后重新 hydrate,于是当前用户界面被切回未登录态。
|
||||
|
||||
再次复测确认还有更深一层根因:即使单个业务请求显式传了 `clearAuthOnUnauthorized: false`,`refreshAccessToken()` 自身在 refresh 失败时也会先静默清空本地 access token。这样局部请求可能没有广播事件,却已经把本地凭证掏空;后续任意一次默认鉴权探测或 `AuthGate` hydrate 都会变成未登录。
|
||||
|
||||
推荐页进入公开拼图作品后还会伴随平台侧私有投影刷新,例如存档列表、浏览历史、个人看板和作品架列表。这些请求用于页面展示与局部缓存同步,不是账号会话权威;其中任意一个 401 都不应把整站登录态改写为未登录。
|
||||
|
||||
推荐页里还有一类更隐蔽的触发点:`ResolvedAssetImage` / `useResolvedAssetReadUrl` 在挂载时会请求 `/api/assets/read-url` 给 generated 私有图片换签。它本质上也是展示层后台请求,若按普通受保护请求处理 `401`,同样会把一次图片换签失败放大成全局掉线。
|
||||
|
||||
公开拼图作品的完整运行态还会在用户进入作品后自动发起 `startPuzzleRun`,通关后自动 `submitPuzzleLeaderboard`,点击下一关时 `advancePuzzleNextLevel`。这些请求属于当前玩法的运行态同步,失败时应该落到当前拼图错误态;它们不能清空全局 access token,也不能触发 `AuthGate` 重新 hydrate。
|
||||
|
||||
## 修复
|
||||
|
||||
本次把推荐页自动运行态请求定义为“卡片级后台请求”:
|
||||
|
||||
1. `apiClient` 增加 `authImpact: 'global' | 'local'` 策略,并导出 `BACKGROUND_AUTH_REQUEST_OPTIONS`。`local` 请求统一跳过 refresh,不清空 token,不广播 `AUTH_STATE_EVENT`。
|
||||
2. `refreshAccessToken()` 不再自行清空 token;只有 `refreshStoredAccessToken()` 这类全局会话恢复入口和默认全局请求策略能决定清 token。
|
||||
3. 推荐页嵌入式运行态请求统一使用 `BACKGROUND_AUTH_REQUEST_OPTIONS`。
|
||||
3. 推荐页自动启动作品前必须满足 `canReadProtectedData`,避免 `AuthGate` 仍在恢复阶段就提前发起受保护写请求。
|
||||
4. generated 图片换签请求同样使用局部后台鉴权选项并跳过 refresh,失败只让当前图片为空,不触发全局登录态清理。
|
||||
5. 公开拼图作品进入完整运行态后,把本次 run 标记为 `isolated` 鉴权模式;开局、重开、排行榜提交和下一关推进都沿用局部鉴权选项。
|
||||
6. 平台 bootstrap 的私有投影读写,包括个人看板、私有作品架、创作作品列表、浏览历史写入和存档列表刷新,也统一作为局部后台请求处理。
|
||||
7. Remix、发布、点赞、账号设置、退出登录等真正账号动作继续保留默认全局鉴权处理。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`
|
||||
2. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`
|
||||
3. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`
|
||||
4. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`
|
||||
5. `npm run typecheck`
|
||||
6. `npm run check:encoding`
|
||||
|
||||
## 关联文件
|
||||
|
||||
1. `src/services/apiClient.ts`
|
||||
2. `src/services/rpg-runtime/rpgRuntimeRequest.ts`
|
||||
3. `src/services/rpg-creation/rpgCreationRuntimeClient.ts`
|
||||
4. `src/components/rpg-entry/useRpgEntryBootstrap.ts`
|
||||
5. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
6. `src/services/*-runtime/*RuntimeClient.ts`
|
||||
7. `src/services/visual-novel-works/visualNovelWorksClient.ts`
|
||||
@@ -42,4 +42,4 @@ Your account has not activated web search.
|
||||
2. 降级重试请求体不再包含 `tools` / `web_search`。
|
||||
3. `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false` 时,foundation draft 全流程直接不带搜索工具。
|
||||
4. `cargo test -p api-server custom_world_foundation_draft --manifest-path server-rs/Cargo.toml` 通过。
|
||||
5. 修改后按项目约束使用 `npm run api-server:maincloud` 重启后端。
|
||||
5. 修改后按项目约束使用 `npm run api-server` 重启后端。
|
||||
|
||||
@@ -7,7 +7,7 @@ RPG 创作链路里有两类正式图片资产需要统一模型:
|
||||
1. 角色主图候选生成。
|
||||
2. 场景幕背景图生成。
|
||||
|
||||
旧实现中角色主图默认使用 `wan2.7-image-pro`,场景图根据是否有参考图分别使用 DashScope 文生图与图生图模型。拼图链路已经接入 APIMart 的 OpenAI 兼容 `/images/generations`,并以 `gpt-image-2` 作为默认图片模型,因此本次 RPG 图片迁移复用同一类服务端配置与请求口径。
|
||||
旧实现中角色主图默认使用 `wan2.7-image-pro`,场景图根据是否有参考图分别使用 DashScope 文生图与图生图模型。拼图链路已经接入 GPT-image-2 图片生成,因此本次 RPG 图片迁移复用同一类服务端配置与请求口径。2026-05-09 起,GPT-image-2 图片生成上游统一迁移到 VectorEngine,具体接口以 `VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md` 为准。
|
||||
|
||||
## 落地范围
|
||||
|
||||
@@ -26,9 +26,9 @@ RPG 创作链路里有两类正式图片资产需要统一模型:
|
||||
服务端使用:
|
||||
|
||||
```text
|
||||
POST {APIMART_BASE_URL}/images/generations
|
||||
Authorization: Bearer {APIMART_API_KEY}
|
||||
model = gpt-image-2
|
||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
model = gpt-image-2-all
|
||||
```
|
||||
|
||||
请求体统一包含:
|
||||
@@ -37,14 +37,14 @@ model = gpt-image-2
|
||||
2. `prompt`
|
||||
3. `n`
|
||||
4. `size`
|
||||
5. 有参考图时增加 `image_urls`
|
||||
5. 有参考图时增加 `image`
|
||||
|
||||
尺寸归一规则:
|
||||
|
||||
1. `1024*1024`、`1024x1024`、`1:1` -> `1:1`
|
||||
2. `1280*720`、`1600*900`、`16:9` -> `16:9`
|
||||
1. `1024*1024`、`1024x1024`、`1:1` -> `1024x1024`
|
||||
2. `1280*720`、`1600*900`、`16:9` -> `1536x1024`
|
||||
|
||||
响应解析兼容同步 `data[].url`、`data[].b64_json` 与异步 `task_id` / `GET /tasks/{task_id}` 结构。
|
||||
响应解析同步 `data[].url` 与 `data[].b64_json`;VectorEngine GPT-image-2-all 当前不再使用 APIMart 异步 `task_id` / `GET /tasks/{task_id}` 结构。
|
||||
|
||||
## 非范围
|
||||
|
||||
@@ -55,22 +55,22 @@ model = gpt-image-2
|
||||
|
||||
## 配置
|
||||
|
||||
本次复用已有 APIMart 配置:
|
||||
本次复用 VectorEngine 图片配置:
|
||||
|
||||
```text
|
||||
APIMART_BASE_URL=https://api.apimart.ai/v1
|
||||
APIMART_API_KEY=...
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=...
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
`APIMART_API_KEY` 缺失时,角色主图与场景图返回 `SERVICE_UNAVAILABLE`,`details.provider = "apimart"`。
|
||||
`VECTOR_ENGINE_API_KEY` 缺失时,角色主图与场景图返回 `SERVICE_UNAVAILABLE`,`details.provider = "vector-engine"`。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 角色主图生成请求上游 `model` 为 `gpt-image-2`。
|
||||
2. 场景图生成请求上游 `model` 为 `gpt-image-2`。
|
||||
1. 角色主图生成请求上游 `model` 为 `gpt-image-2-all`,且不携带 `official_fallback`。
|
||||
2. 场景图生成请求上游 `model` 为 `gpt-image-2-all`,且不携带 `official_fallback`。
|
||||
3. 旧前端或历史草稿传 `wan2.7-image-pro` 时不会回退旧模型。
|
||||
4. 场景参考图生成仍能把参考图 Data URL 放入 `image_urls`。
|
||||
4. 场景参考图生成仍能把参考图 Data URL 放入 `image`。
|
||||
5. 角色主图生成后仍执行原有 PNG 透明背景处理与 OSS 写入。
|
||||
6. `cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml` 通过。
|
||||
7. `cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml` 通过。
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# RPG 聊天退出后继续冒险过场方案(2026-05-03)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
玩家退出 NPC 聊天后点击“继续冒险”,不能直接瞬间切到下一幕或下一场景。继续冒险必须先完成一段清晰的角色退场与入场演出,再让新对面角色主动开启对话。
|
||||
|
||||
## 2. 时序约束
|
||||
|
||||
点击“继续冒险”后的顺序固定为:
|
||||
|
||||
1. 保持旧场景画面,隐藏当前场景对面的所有角色。
|
||||
2. 主角色与同行角色播放行走动画,向右走出屏幕。
|
||||
3. 点击后可以先更新真实 `gameState/currentStory`,但画布继续使用过场模型缓存的旧可见态;退场完成前不得把新幕画面展示出来。
|
||||
4. 新场景或新幕画面展示后,主角色从左侧走到默认站位。
|
||||
5. 新场景对面角色从屏幕左侧走入到指定对面站位。
|
||||
6. 入场完成后,如果后续选项里存在 `npc_preview_talk` 或 `npc_chat`,自动执行该选项,直接开启主角色与对面角色的对话。
|
||||
|
||||
## 3. 代码落点
|
||||
|
||||
1. `src/hooks/rpg-runtime-story/choiceActions.ts`
|
||||
- 点击 `story_continue_adventure` 时只提交延迟状态与选项,不直接进入对话。
|
||||
- 若延迟故事标记了自动执行,则把目标 option 放到新的 `deferredAutoChoice`。
|
||||
|
||||
2. `src/components/rpg-runtime-shell/useRpgSceneTransitionModel.ts`
|
||||
- `story_continue_adventure` 也纳入 `content-change` 过场。
|
||||
- 入场动画结束后触发 `deferredAutoChoice`,避免在角色尚未走到位前开聊。
|
||||
- 自动触发时通过最新回调读取当前运行态,避免计时器拿到点击“继续冒险”前的旧状态。
|
||||
|
||||
3. `src/components/game-canvas/GameCanvasEntityLayer.tsx`
|
||||
- 退场期隐藏旧对面角色。
|
||||
- 入场期让新对面角色从左侧走入到右侧指定站位。
|
||||
- 对面角色入场期使用移动动画,完成后恢复 idle 与对话气泡。
|
||||
|
||||
4. `src/components/rpg-runtime-shell/useRpgRuntimeShellViewModel.ts`
|
||||
- `story_continue_adventure` 只要携带 `deferredRuntimeState` 或 `deferredAutoChoice`,就先进入过场,再交给 story choice 处理真实状态。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
1. 退出 NPC 聊天后点击“继续冒险”,不会在同一帧瞬间切换到下一幕对话。
|
||||
2. 退场时旧对面角色不可见,主角色向右走出画面。
|
||||
3. 入场时新对面角色从左侧进入右侧站位。
|
||||
4. 入场完成后自动进入新对面角色对话。
|
||||
5. 移动端与桌面端都不新增说明类 UI 文案,只保留游戏内演出。
|
||||
171
docs/technical/RPG_OPENING_CG_MANUAL_GENERATION_2026-05-03.md
Normal file
171
docs/technical/RPG_OPENING_CG_MANUAL_GENERATION_2026-05-03.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# RPG 世界草稿开局 CG 手动生成技术方案(2026-05-03)
|
||||
|
||||
## 1. 背景与本次口径
|
||||
|
||||
本方案落地“RPG 游戏开场 CG”第一版。它继承 `docs/prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md` 的资产化方向,但本次不采用旧 PRD 中“4 张关键帧 + 3 段视频拼接”的方案,而采用更短的两阶段链路:
|
||||
|
||||
```text
|
||||
世界草稿
|
||||
-> GPT Image 2 生成 3*4 故事板图,2k,16:9
|
||||
-> Seedance 使用故事板作为参考图生成单段 15 秒视频,480p,16:9
|
||||
-> OSS 保存故事板与成片
|
||||
-> 前端把 openingCg 回写到当前世界草稿 profile
|
||||
```
|
||||
|
||||
本次明确不在生成世界草稿时自动生成开局 CG。入口只放在世界草稿结果页的世界 Tab,由用户手动触发。
|
||||
|
||||
## 2. 用户体验
|
||||
|
||||
1. 世界 Tab 展示一个轻量的“开局 CG”资产槽。
|
||||
2. 未生成时只提供手动生成按钮。
|
||||
3. 生成中展示阶段状态和进度,不把规则说明长文写进 UI。
|
||||
4. 生成成功后展示视频预览和重新生成按钮。
|
||||
5. 每次点击生成扣 `80` 积分,失败自动退款。
|
||||
6. UI 预计等待文案为 `预计 10 分钟`,真实等待由后端同步请求完成后返回。
|
||||
7. 只在世界草稿中手动生成;世界底稿、角色图、幕背景图自动补齐流程不生成开局 CG。
|
||||
|
||||
## 3. 数据结构
|
||||
|
||||
在 `CustomWorldProfile` 新增可选字段:
|
||||
|
||||
```ts
|
||||
type CustomWorldOpeningCgStatus =
|
||||
| 'not_started'
|
||||
| 'storyboard_generating'
|
||||
| 'video_generating'
|
||||
| 'ready'
|
||||
| 'failed';
|
||||
|
||||
type CustomWorldOpeningCgProfile = {
|
||||
id: string;
|
||||
status: CustomWorldOpeningCgStatus;
|
||||
storyboardImageSrc?: string | null;
|
||||
storyboardAssetId?: string | null;
|
||||
videoSrc?: string | null;
|
||||
videoAssetId?: string | null;
|
||||
posterImageSrc?: string | null;
|
||||
posterAssetId?: string | null;
|
||||
storyboardPrompt?: string | null;
|
||||
videoPrompt?: string | null;
|
||||
imageModel: 'gpt-image-2';
|
||||
videoModel: string;
|
||||
aspectRatio: '16:9';
|
||||
imageSize: '2k';
|
||||
videoResolution: '480p';
|
||||
durationSeconds: 15;
|
||||
pointCost: 80;
|
||||
estimatedWaitMinutes: 10;
|
||||
generatedAt?: string | null;
|
||||
updatedAt: string;
|
||||
errorMessage?: string | null;
|
||||
};
|
||||
```
|
||||
|
||||
该字段保存在现有 profile JSON 内,不新增 SpacetimeDB 表字段。发布与保存沿用当前 `profile_payload_json` 整包存储能力。
|
||||
|
||||
## 4. 后端接口
|
||||
|
||||
新增接口:
|
||||
|
||||
```text
|
||||
POST /api/runtime/custom-world/opening-cg
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```ts
|
||||
type GenerateCustomWorldOpeningCgRequest = {
|
||||
profile: CustomWorldProfile;
|
||||
};
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```ts
|
||||
type GenerateCustomWorldOpeningCgResponse = {
|
||||
openingCg: CustomWorldOpeningCgProfile;
|
||||
};
|
||||
```
|
||||
|
||||
接口职责:
|
||||
|
||||
1. 校验登录态与 profile 基本结构。
|
||||
2. 校验至少存在可扮演角色、世界基调、世界概述、核心冲突和首个场景第一幕背景图。
|
||||
3. 使用 `execute_billable_asset_operation_with_cost(..., 80, ...)` 做预扣和失败退款。
|
||||
4. 生成故事板图片并持久化为 `custom_world_opening_cg_storyboard` 资产。
|
||||
5. 使用故事板图作为 Seedance 参考图生成视频并持久化为 `custom_world_opening_cg_video` 资产。
|
||||
6. 返回可直接合并进 profile 的 `openingCg`。
|
||||
|
||||
## 5. 提示词
|
||||
|
||||
### 5.1 故事板
|
||||
|
||||
图片模型固定使用 `gpt-image-2`,尺寸语义为 `2k`、`16:9`;2026-05-09 起实际请求 VectorEngine `gpt-image-2-all`,下游 size 按迁移文档归一为 `1536x1024`。
|
||||
|
||||
模板:
|
||||
|
||||
```text
|
||||
以3*4网格格式创建故事板,16:9。像素风角色扮演游戏开场动画CG。
|
||||
|
||||
故事流程:先展示角色,展示故事背景,然后表现核心冲突,最后衔接开局场景
|
||||
故事基调:{世界草稿.tone}
|
||||
|
||||
玩家扮演:将玩家扮演角色作为角色参考图并引用世界草稿中的角色简介
|
||||
故事背景:{世界草稿.summary}
|
||||
核心冲突:{世界草稿.coreConflicts}
|
||||
开局场景:将首个场景的第一幕背景图作为参考图
|
||||
```
|
||||
|
||||
参考图:
|
||||
|
||||
1. 玩家扮演角色使用第一个可扮演角色的 `imageSrc`。
|
||||
2. 开局场景使用 `sceneChapterBlueprints[0].acts[0].backgroundImageSrc`。
|
||||
3. 若缺少任一参考图,返回可理解错误,不降级到无参考图生成。
|
||||
|
||||
### 5.2 视频
|
||||
|
||||
视频模型复用当前 Ark Seedance 配置,分辨率 `480p`,比例 `16:9`,时长 `15` 秒。
|
||||
|
||||
提示词固定:
|
||||
|
||||
```text
|
||||
利用参考图作为故事板,生成一段连贯的动画,没有旁白
|
||||
```
|
||||
|
||||
请求参数必须开启生成音频与联网搜索。若当前上游字段名存在差异,后端在 Ark 请求体中以 `audio` / `generate_audio` / `web_search` 的兼容布尔字段表达,保证不会影响现有角色动画接口。
|
||||
|
||||
开局 CG 视频链路的上游等待窗口不得低于 `10` 分钟,以匹配产品侧“预计 10 分钟”的展示口径。
|
||||
|
||||
## 6. 资产与计费
|
||||
|
||||
资产写入:
|
||||
|
||||
| 产物 | assetKind | entityKind | slot |
|
||||
| --- | --- | --- | --- |
|
||||
| 故事板图 | `custom_world_opening_cg_storyboard` | `custom_world_profile` | `opening_cg_storyboard` |
|
||||
| 成片视频 | `custom_world_opening_cg_video` | `custom_world_profile` | `opening_cg_video` |
|
||||
|
||||
计费:
|
||||
|
||||
1. 每次点击生成消耗 `80` 积分。
|
||||
2. 故事板生成失败、视频任务创建失败、轮询失败、下载失败或 OSS 持久化失败都退款。
|
||||
3. 扣费流水的 asset id 使用本次 opening CG id,避免同一次请求重试重复扣费。
|
||||
|
||||
## 7. 前端落点
|
||||
|
||||
1. `CustomWorldEntityCatalog` 在世界 Tab 增加开局 CG 槽。
|
||||
2. `rpgCreationAssetClient` 新增 `generateOpeningCg`。
|
||||
3. `RpgCreationResultViewImpl` 持有生成中状态,生成完成后 `onProfileChange({ ...profile, openingCg })`。
|
||||
4. 视频展示使用签名 URL 读取组件,不把签名 URL 写入 profile。
|
||||
5. 草稿生成时不调用该接口。
|
||||
|
||||
## 8. 验收点
|
||||
|
||||
1. 新草稿生成完成后 `openingCg` 为空或不存在。
|
||||
2. 世界 Tab 可以手动生成开局 CG。
|
||||
3. 生成请求 payload 包含角色参考图与开局第一幕背景图。
|
||||
4. 故事板请求使用 `gpt-image-2`、`2048x1152`/`2k` 语义、`16:9`。
|
||||
5. 视频请求使用 Seedance、`480p`、`16:9`、`15` 秒,并传入故事板参考图。
|
||||
6. 单次生成扣 `80` 积分,任一失败路径退款。
|
||||
7. 生成成功后 profile 内出现 `openingCg.videoSrc`,刷新/保存/发布后能保留。
|
||||
8. 视频链路上游超时不低于 `10` 分钟,避免低于产品展示的预计等待时长。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 运行态输入设备抽象层 2026-05-10
|
||||
|
||||
## 背景
|
||||
|
||||
拼图运行态接入 mocap 后,鼠标/触控和 mocap 曾各自维护一套选择、坐标换算和拖拽提交逻辑。这样会让新设备只能在单个玩法里打补丁,也容易出现同一动作在不同设备下语义不一致的问题:例如 mocap `grab` 只触发选中,而不是像鼠标按住一样持续拖拽。
|
||||
|
||||
后续运行态还会接摇杆、键盘、体感、摄像头手势等输入来源,因此输入设备接入必须收口到全项目通用层。
|
||||
|
||||
## 决策
|
||||
|
||||
新增 `src/services/input-devices/` 作为前端运行态通用输入设备抽象层:
|
||||
|
||||
1. `runtimeDragInputController` 只维护设备无关的拖拽会话状态机。
|
||||
2. `runtimeInputGeometry` 只处理 client 坐标、归一坐标、元素边界和网格命中换算。
|
||||
3. 设备适配层把鼠标、触控、mocap 等输入归一为 `press / move / release / tap / drop`。
|
||||
4. 玩法组件负责把通用输入点解释成自己的目标对象和落点,不把拼图、方洞或大鱼等玩法规则写进输入层。
|
||||
|
||||
## 当前接入
|
||||
|
||||
`useMocapInput` 解析 mocap `hands[].landmarks` 时应优先用 MediaPipe 21 点里的 `wrist / index_mcp / middle_mcp / ring_mcp / pinky_mcp` 加权计算掌心派生点;少于 3 个掌心关键点时才回退到 `wrist` 或直出 `hand.x/y`。这样运行态光标不会直接贴在腕部或指尖。
|
||||
|
||||
拼图运行态已接入该层:
|
||||
|
||||
- 鼠标/触控 `pointerdown / pointermove / pointerup` 进入同一个 drag controller。
|
||||
- mocap `grab` 进入同一个 drag controller,并强制使用持续拖拽语义。
|
||||
- mocap 光标按 60Hz 插值更新 UI 位置,并在拖拽中用插值后的当前点持续驱动输入层,避免输入包帧率低或抖动时出现明显跳变。
|
||||
- 合并大块由拼图运行态把手部坐标命中到任一成员拼块;本地拼图运行时再按 `mergedGroupId` 执行整组平移。
|
||||
|
||||
## 调试模式
|
||||
|
||||
前端全局调试模式统一通过 `src/config/debugMode.ts` 判断。默认跟随 Vite 开发态:`import.meta.env.DEV` 为真时开启,生产构建默认关闭;如需显式覆盖,可设置 `VITE_DEBUG_MODE=true` 或 `VITE_DEBUG_MODE=false`。
|
||||
|
||||
拼图运行态的 mocap 调试面板只在全局调试模式下渲染。面板默认折叠,只保留一行连接状态,展开后才显示动作、手势、解析告警和原始包预览,避免开发诊断信息遮挡拼图棋盘和底部操作。
|
||||
|
||||
## 接入规则
|
||||
|
||||
新玩法或新设备接入时遵循以下边界:
|
||||
|
||||
1. 输入层可以知道设备类型和几何换算,但不能知道玩法业务规则。
|
||||
2. 设备适配层只负责把原始输入转换成通用输入事件。
|
||||
3. 玩法壳层负责从通用输入点解析本玩法目标,例如拼块、洞口、角色或实体。
|
||||
4. 玩法壳层负责决定 drop 后调用哪个本地运行态函数或后端接口。
|
||||
5. 需要取消输入时优先按 `inputId` 取消,避免 mocap 丢帧误伤正在进行的鼠标/触控会话。
|
||||
|
||||
## 验证
|
||||
|
||||
基础抽象层验证:
|
||||
|
||||
```bash
|
||||
npm run test -- src\services\input-devices\runtimeDragInputController.test.ts src\services\useMocapInput.test.ts
|
||||
```
|
||||
|
||||
拼图接入验证:
|
||||
|
||||
```bash
|
||||
npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx
|
||||
```
|
||||
|
||||
跨平台入口缺失作品兜底验证:
|
||||
|
||||
```bash
|
||||
npm run test -- src\components\rpg-entry\RpgEntryFlowShell.agent.interaction.test.tsx -t "missing puzzle public detail returns to platform home"
|
||||
```
|
||||
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`。
|
||||
@@ -25,23 +25,33 @@ npm run dev:rust
|
||||
|
||||
默认端口:
|
||||
|
||||
1. Web 前端:`http://127.0.0.1:3000`
|
||||
2. Rust `api-server`:`http://127.0.0.1:8082`
|
||||
3. SpacetimeDB standalone:`http://127.0.0.1:3101`
|
||||
4. SpacetimeDB database:优先读取仓库根目录 `spacetime.local.json` 的 `database` 字段;没有该字段时才回退到 `genarrative-dev`
|
||||
5. SpacetimeDB 本地数据与日志目录:`server-rs/.spacetimedb/local`
|
||||
1. Web 前端:优先 `http://127.0.0.1:3000`
|
||||
2. Rust `api-server`:优先 `http://127.0.0.1:8082`
|
||||
3. SpacetimeDB standalone:优先 `http://127.0.0.1:3101`
|
||||
4. 后台 Web 前端:优先 `http://127.0.0.1:3102`
|
||||
5. SpacetimeDB database:优先读取仓库根目录 `spacetime.local.json` 的 `database` 字段;没有该字段时才回退到 `genarrative-dev`
|
||||
6. SpacetimeDB 本地数据与日志目录:`server-rs/.spacetimedb/local`
|
||||
|
||||
启动前端口处理:
|
||||
|
||||
1. `npm run dev` / `npm run dev:rust` 会先检查 SpacetimeDB、Rust `api-server`、主站 Vite、后台 Vite 需要使用的端口。
|
||||
2. 如果优先端口不可用,脚本会从该端口开始向后寻找可用端口,并将解析后的端口覆盖到后续 `spacetime start`、`spacetime publish --server`、`GENARRATIVE_API_PORT`、`RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET`、`ADMIN_API_TARGET` 与 Vite 启动参数。
|
||||
3. 控制台会打印 `[dev:ports] ... 可用` 或 `[dev:ports] ... 不可用,改用 ...`,排查代理错配时以该日志和后续 `[dev:rust] web/admin web/rust api/spacetime` 实际地址为准。
|
||||
4. 单独 `npm run dev:web` 也会检查主站 Vite 端口;`WEB_PORT` 或默认 `3000` 不可用时,会自动切到后续可用端口并继续严格端口启动。
|
||||
|
||||
默认流程:
|
||||
|
||||
1. 检查 `cargo`、`node` 与 `spacetime` CLI。
|
||||
2. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/` 与 `spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`。
|
||||
3. 启动 `spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志不会落到开发者全局目录。
|
||||
4. 等待 SpacetimeDB 就绪:优先接受 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:3101/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`。
|
||||
5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
6. 注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`。
|
||||
8. 注入 `RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
9. 任一子进程退出时,脚本回收其余子进程。
|
||||
2. 检查并解析本次联调需要使用的端口;端口不可用时先寻找可用端口,再把实际端口传给后续流程。
|
||||
3. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/` 与 `spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`。
|
||||
4. 启动 SpacetimeDB 前先检查 `server-rs/.spacetimedb/local/data/spacetime.pid`:如果 pid 对应进程仍存在,且同目录 `dev-rust-spacetime-url` 中记录的 URL 可被 `spacetime server ping` 判定在线,则直接复用该宿主;如果 URL 记录缺失,会依次尝试从 `logs/dev-rust-spacetime-start.log` 和 `logs/spacetime-standalone.log` 中解析最近一次监听地址兜底。否则按正常流程重新启动。
|
||||
5. 如果确认需要新启动 SpacetimeDB,脚本会先检测 `127.0.0.1:3101` 是否可监听;若已占用,输出占用进程并选择从 `3101` 起向后的最近可用端口,再执行 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr <实际地址>`。启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`,后续 publish 与 `api-server` 都使用同一个实际 URL。
|
||||
6. 等待 SpacetimeDB 就绪:优先接受 `spacetime server ping http://127.0.0.1:<spacetime-port>` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:<spacetime-port>/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`。
|
||||
7. 执行 `spacetime publish <本地数据库名> --server <实际 SpacetimeDB URL> --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
8. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
9. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`。
|
||||
10. 注入 `RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
11. 任一子进程退出时,脚本回收其余子进程。
|
||||
|
||||
Vite 代理覆盖范围:
|
||||
|
||||
@@ -58,14 +68,23 @@ Vite 代理覆盖范围:
|
||||
4. 如只想启动进程不发布模块,可传 `--skip-publish`。
|
||||
5. 后续进入正式版本前,涉及表结构变化时必须在开发阶段补齐迁移表与迁移函数,不能依赖清库发布作为正式升级策略。
|
||||
|
||||
本地联调跳过策略:
|
||||
|
||||
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,脚本会优先按 `spacetime.pid` 与 `dev-rust-spacetime-url` 复用该宿主;如果确认不是可复用宿主,则会先输出占用进程并选择最近可用端口。也可显式使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,或用 `--spacetime-port` 指定起始探测端口。
|
||||
2. 如果当前没有修改 `server-rs/crates/spacetime-module`,可使用 `npm run dev -- --skip-publish` 跳过数据库发布,降低本地启动时的 SpacetimeDB wasm 编译耗时。
|
||||
3. 如果当前阶段只需要检查 `spacetime-module` 语法,不需要重新发布本地数据库,可执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。该命令只做 Rust 编译检查,不生成新数据库,也不刷新 bindings。
|
||||
|
||||
常用示例:
|
||||
|
||||
```bash
|
||||
npm run dev:rust
|
||||
npm run dev -- --skip-spacetime
|
||||
npm run dev -- --skip-publish
|
||||
./scripts/dev-rust-stack.sh
|
||||
./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 --database genarrative-dev
|
||||
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
|
||||
./scripts/dev-rust-stack.sh --preserve-database
|
||||
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
bindings 生成:
|
||||
@@ -95,7 +114,7 @@ npm run dev:rust:logs -- --follow
|
||||
|
||||
日志提取规则:
|
||||
|
||||
1. SpacetimeDB 模块日志以 `spacetime --root-dir=server-rs/.spacetimedb/local logs <database>` 为唯一提取入口,脚本不直接读取内部日志文件结构。
|
||||
1. SpacetimeDB 模块日志以 `spacetime logs <database> --server <实际本地 server>` 或 `npm run dev:rust:logs` 为提取入口,脚本不直接读取内部日志文件结构。
|
||||
2. 默认读取 `spacetime.local.json` 的 `database` 字段,默认 server 为 `http://127.0.0.1:3101`。
|
||||
3. 默认输出到 `logs/spacetime/<database>-<timestamp>.log`,并通过 `tee` 同步显示在终端。
|
||||
4. `--follow` 仅用于本地追踪,会持续追加到同一个输出文件;停止时用 `Ctrl+C`。
|
||||
@@ -103,10 +122,12 @@ npm run dev:rust:logs -- --follow
|
||||
联调排错补充:
|
||||
|
||||
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。
|
||||
2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`。
|
||||
2. `spacetime list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`。
|
||||
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
|
||||
4. 如果 Vite 输出 `/api/auth/refresh`、`/api/auth/login-options` 或 `/api/runtime/custom-world-gallery` 的 `ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
|
||||
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0` 也不能直接视为已就绪;本地脚本会继续探测 `/v1/ping`。若 `/v1/ping` 返回 `200`,说明 standalone 已经可用,可以继续发布模块;若 `/v1/ping` 也失败,脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。
|
||||
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0` 也不能直接视为已就绪;本地脚本会继续探测 `/v1/ping`。若 `/v1/ping` 返回 `200`,说明 standalone 已经可用,可以继续发布模块;若 `/v1/ping` 也失败,脚本会继续等待新启动实例,或在本地数据目录已被其他实例占用时输出占用进程。
|
||||
6. 如果本地 `spacetime publish` 显示 `401` 无权限,且确认本地开发数据可以丢弃,先停止本地 SpacetimeDB,再备份或删除 `server-rs/.spacetimedb/local/data` 后重新运行 `npm run dev`。重新发布时日志应表现为创建新的数据库,而不是更新旧数据库;如果仍显示更新旧库或继续无权限,说明数据目录、库名或 CLI 身份仍未对齐。除 CI/CD 脚本内部受控用法外,人工清理不要使用 `spacetime --root-dir`。
|
||||
7. Windows / Git Bash 下读取 `spacetime.pid` 或 `dev-rust-spacetime-url` 时,如果文件正被 SpacetimeDB 更新,不能用 `tr/head/xargs` 管道直接读;脚本使用 Node 读取并短重试,避免出现 `tr: read error: Device or resource busy` 后直接中断。
|
||||
|
||||
编译警告治理:
|
||||
|
||||
@@ -116,7 +137,7 @@ npm run dev:rust:logs -- --follow
|
||||
|
||||
api-server 单独重启补充:
|
||||
|
||||
1. `npm run api-server` 会先读取 `.env`、`.env.local`,使用 `GENARRATIVE_SPACETIME_*` 启动 `cargo run -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||
1. `npm run api-server` 会先读取 `.env`、`.env.local`,使用 `GENARRATIVE_SPACETIME_*` 启动默认 debug profile 的 `cargo run -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||
2. Windows 下脚本会尽力停止本仓库 `server-rs/target/debug/api-server.exe` 对应的旧进程,避免 cargo 重新编译时 exe 被占用。
|
||||
3. 旧进程已经退出或清理过程中出现瞬时等待失败时,不应阻断新的 `api-server` 启动;脚本只记录清理失败并继续启动。
|
||||
|
||||
@@ -145,12 +166,12 @@ npm run deploy:rust:remote
|
||||
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
|
||||
6. 把仓库根目录的 `.env` 与 `.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF,并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。
|
||||
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 与 `web/admin/`;其中 `/admin` 跳转到 `/admin/`,`/admin/` 提供后台 SPA,`/admin/api/*`、`/api/*`、`/generated-*`、`/healthz` 反代到本包内的 `api-server`。
|
||||
8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
|
||||
8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;SpacetimeDB 启动、探活和发布由发布脚本内部统一编排。脚本内部如保留 `--root-dir`,只属于 CI/CD 或发布包受控用法,不作为人工命令模板。普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
|
||||
9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
|
||||
|
||||
SpacetimeDB database 名称必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会触发 `spacetime publish` 的 `invalid characters in database name`。发布包构建脚本和 `start.sh` 都会提前拦截这类非法名称。
|
||||
|
||||
发布包构建日志会输出 `SpacetimeDB 发布数据库: <database>`;目标服务器执行 `start.sh` 时会在发布前输出最终加载后的 `database/server/root-dir`,用于确认 `.env.local` 或 Jenkins 参数覆盖后的实际发布目标。
|
||||
发布包构建日志会输出 `SpacetimeDB 发布数据库: <database>`;目标服务器执行 `start.sh` 时会在发布前输出最终加载后的 `database/server/数据目录或脚本运行目录`,用于确认 `.env.local` 或 Jenkins 参数覆盖后的实际发布目标。
|
||||
|
||||
发布包结构:
|
||||
|
||||
@@ -204,8 +225,8 @@ cd build/<timestamp>
|
||||
5. 自动迁移导出旧库时优先读取 `deploy-state/migration-bootstrap-secret.previous.txt`,导入新库时读取当前发布包 `migration-bootstrap-secret.txt`;Jenkins 部署脚本会在覆盖发布包前保存旧密钥。该快照属于部署状态,不放入 `run/`,避免启停 hook 通过 `sudo` 运行后把部署阶段要写的文件变成 root 私有。手工覆盖发布包时,也应在覆盖前保留旧模块的引导密钥,否则旧库导出可能无法授权。
|
||||
6. 自动迁移 JSON 默认写入发布目录下 `database-migrations/<database>/`;可通过 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 改写。该目录属于运行态,不应被 Jenkins 覆盖部署删除。
|
||||
7. 只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,该模式代表人工确认清库,不执行导出和回灌。
|
||||
8. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB;如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime` 或 `spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例。
|
||||
9. 如果 `spacetime publish` 报 `403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权。
|
||||
8. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB;如果同一个 `.spacetimedb/` 运行目录已被其他未就绪实例占用,则只输出真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例。
|
||||
9. 如果 `spacetime publish` 报 `403 Forbidden`,优先确认 `spacetime login show` 输出的身份是否有权更新目标库,并确认 `GENARRATIVE_SPACETIME_DATABASE` / `GENARRATIVE_SPACETIME_SERVER_URL` 未指向错误环境;`--clear-database` 不能绕过身份授权。除 CI/CD 脚本内部受控用法外,人工排障不要使用 `spacetime --root-dir`。
|
||||
10. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。
|
||||
11. 如只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传。
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
1. `cargo build` 默认只构建原生服务入口 `api-server`。
|
||||
2. `spacetime-module` 保留为 workspace member,便于 `cargo check --workspace --all-targets` 做类型检查。
|
||||
3. `spacetime-module` 的可发布产物必须继续通过 SpacetimeDB CLI 构建,不走无参数 `cargo build` 的原生链接路径。
|
||||
3. `spacetime-module` 的可发布产物必须继续通过 SpacetimeDB CLI 或仓库发布脚本生成,不走无参数 `cargo build` 的原生链接路径。
|
||||
|
||||
## 落地
|
||||
|
||||
@@ -29,11 +29,11 @@ default-members = [
|
||||
cd D:\Genarrative\server-rs
|
||||
cargo build
|
||||
cargo check --workspace --all-targets
|
||||
spacetime build --module-path crates/spacetime-module
|
||||
spacetime publish <database> --module-path crates/spacetime-module --build-options="--debug" --yes
|
||||
```
|
||||
|
||||
## 后续约束
|
||||
|
||||
- 日常本地编译原生后端用 `cargo build` 或 `cargo build -p api-server`。
|
||||
- 验证全部 Rust 目标用 `cargo check --workspace --all-targets`。
|
||||
- 构建 / 发布 SpacetimeDB 模块用 `spacetime build --module-path crates/spacetime-module` 或发布脚本,不要用原生 `cargo build -p spacetime-module`。
|
||||
- 构建 / 发布 SpacetimeDB 模块用 `spacetime publish <database> --module-path server-rs/crates/spacetime-module --build-options="--debug" --yes` 或发布脚本,不要用无目标参数的原生 `cargo build -p spacetime-module`。
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Rust workspace 依赖集中配置记录
|
||||
|
||||
日期:`2026-05-07`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`server-rs` workspace 已经包含 `api-server`、`spacetime-module`、`spacetime-client`、多个 `module-*` 领域 crate、`platform-*` 适配 crate 和共享 crate。随着 DDD 收口推进,成员 `Cargo.toml` 中重复散写了第三方 crate 版本和本地 path 依赖,后续升级 `serde`、`reqwest`、`tokio`、`time`、SpacetimeDB SDK 或内部 crate 路径时容易出现漂移。
|
||||
|
||||
本次只做 Cargo 配置收敛,不改变业务代码、表结构、reducer/procedure 签名、HTTP contract 或前端绑定。
|
||||
|
||||
## 2. 配置规则
|
||||
|
||||
1. 共享第三方依赖版本统一维护在 `server-rs/Cargo.toml` 的 `[workspace.dependencies]`。
|
||||
2. workspace 内部 crate 的 `path` 也统一维护在根 `server-rs/Cargo.toml`。
|
||||
3. 成员 crate 默认使用 `{ workspace = true }` 继承依赖。
|
||||
4. 成员 crate 只保留自身需要表达的差异,例如 `features`、`optional = true` 或 target-specific dependency。
|
||||
5. 需要关闭 default features 的依赖,应优先在 workspace 根依赖中声明;成员 crate 不再重复覆盖同一项。
|
||||
6. `module-assets` 这类有默认服务端 feature 的领域 crate,在 workspace 根内按 `default-features = false` 维护;需要服务端 OSS/HTTP 能力的 adapter crate 显式启用 `features = ["server-service"]`。
|
||||
7. `shared-contracts` 只能承载前后端公开 DTO 和轻量枚举,禁止直接依赖 `platform-*` 服务实现 crate;需要把平台实现响应转换为公开 DTO 时,转换函数放在 `api-server` 等 adapter 层。
|
||||
8. 面向 SpacetimeDB WASM 的依赖链不得隐式启用原生 HTTP / OSS / Web 平台依赖;例如 `shared-contracts` 的 `assets` 模块通过不依赖 `platform-oss` 的 `oss-contracts` feature 暴露给 `api-server`,`spacetime-module` 路径只消费关闭默认 feature 后的纯 DTO 子集。
|
||||
9. `spacetime-module` 的传递依赖不能包含 `reqwest`、`web-sys`、`js-sys`、`wasm-bindgen` 等 Web/HTTP 客户端链路;发布前可用 `cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown` 排查。
|
||||
|
||||
## 3. 本次收敛范围
|
||||
|
||||
已上提到 workspace 根的依赖包括:
|
||||
|
||||
1. 本地路径依赖:`module-*`、`platform-*`、`shared-*`、`spacetime-client`。
|
||||
2. 常用第三方依赖:`serde`、`serde_json`、`serde_urlencoded`、`reqwest`、`tokio`、`time`、`tracing`、`base64`、`hmac`、`sha2`、`uuid`、`url` 等。
|
||||
3. SpacetimeDB 相关依赖:`spacetimedb`、`spacetimedb-sdk`、`spacetimedb-lib`。
|
||||
|
||||
`spacetimedb-lib` 在 workspace 根统一关闭 default features,`spacetime-module` 只继承并补充 `features = ["serde"]`。这样避免成员 crate 尝试覆盖 workspace default-feature 设定导致 manifest 解析失败。
|
||||
|
||||
阿里云 OSS 相关签名不再依赖不推荐的 `sha1` crate,统一使用 `sha2::Sha256`:
|
||||
|
||||
1. 浏览器直传 ticket 使用 OSS V4 表单签名字段:`x-oss-signature-version=OSS4-HMAC-SHA256`、`x-oss-credential`、`x-oss-date`、`x-oss-signature`。
|
||||
2. 服务端 OSS 读写请求和测试辅助签名统一使用 `OSS4-HMAC-SHA256` Authorization。
|
||||
3. 阿里云短信 OpenAPI 请求统一使用 `ACS3-HMAC-SHA256` 请求头签名,不再在表单中传旧 `SignatureMethod=HMAC-SHA1` / `SignatureVersion=1.0`。
|
||||
|
||||
## 4. 不在本次范围
|
||||
|
||||
1. 不新增或删除 crate。
|
||||
2. 不修改 `server-rs` workspace `members` / `default-members` 语义。
|
||||
3. 不修改 SpacetimeDB 表、reducer、procedure、migration 白名单或生成绑定。
|
||||
4. 不改变 `module-*` 的 DDD 依赖方向。
|
||||
|
||||
## 5. 验收口径
|
||||
|
||||
配置改动后至少执行:
|
||||
|
||||
```powershell
|
||||
cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
|
||||
npm.cmd run check:server-rs-ddd
|
||||
npm.cmd run check:encoding -- docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md docs/technical/README.md server-rs/README.md .hermes/shared-memory/decision-log.md .hermes/shared-memory/project-overview.md
|
||||
```
|
||||
|
||||
若仅改 Cargo 依赖配置且未触碰 API smoke 相关代码,不强制启动 `npm run api-server`;若后续改动同时涉及 API 路由、SpacetimeDB facade 或运行时行为,仍按 `AGENTS.md` 和 DDD 文档执行后端 smoke。
|
||||
|
||||
## 6. SpacetimeDB WASM 依赖边界
|
||||
|
||||
2026-05-11 本地重置 SpacetimeDB 并重新发布 `xushi-p4wfr` 时,`spacetime publish` 在 Rust 编译成功后报 `wasm-bindgen detected`。排查命令显示链路为:
|
||||
|
||||
```text
|
||||
spacetime-module -> module-runtime -> shared-contracts -> platform-oss -> reqwest -> wasm-bindgen
|
||||
```
|
||||
|
||||
根因是 `shared-contracts` 为了复用 OSS 直传/读签名返回类型,直接依赖了 `platform-oss`。这违反 DDD 分层边界:契约 crate 不能依赖平台副作用实现,否则所有引用契约的纯领域和 SpacetimeDB 模块都会被迫拉入 HTTP client。
|
||||
|
||||
`spacetime publish` 会构建 `spacetime-module` 的 `wasm32-unknown-unknown` 目标。这个目标不能包含 `wasm-bindgen`,也不应通过 DTO crate 间接拉入 `reqwest`、`web-sys` 或浏览器 WebAssembly 平台依赖。
|
||||
|
||||
修正口径:
|
||||
|
||||
1. `shared-contracts::assets` 定义独立的公开 DTO 和 `DirectUploadObjectAccess` 轻量枚举。
|
||||
2. `platform-oss` 保持 OSS 签名、读写请求和错误分类实现,不被契约层引用。
|
||||
3. `api-server::assets` 负责把 `platform_oss::OssPostObjectResponse` / `OssSignedGetObjectUrlResponse` 转成 `shared-contracts` DTO。
|
||||
4. 后续新增外部平台能力时,重复使用这个边界:平台 crate 不得被 `shared-contracts`、`module-*` 或 `spacetime-module` 反向依赖。
|
||||
|
||||
已验证的排查命令:
|
||||
|
||||
```powershell
|
||||
cargo tree -i wasm-bindgen --manifest-path server-rs\Cargo.toml -p spacetime-module --target wasm32-unknown-unknown
|
||||
cargo tree -i wasm-bindgen --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown
|
||||
cargo tree --manifest-path server-rs\crates\spacetime-module\Cargo.toml --target wasm32-unknown-unknown | Select-String -Pattern 'wasm-bindgen|platform-oss|reqwest'
|
||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml --target wasm32-unknown-unknown
|
||||
cargo check -p shared-contracts --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p api-server --manifest-path server-rs\Cargo.toml
|
||||
spacetime publish xushi-p4wfr --server local --module-path server-rs\crates\spacetime-module --build-options="--debug" -c=on-conflict --yes
|
||||
```
|
||||
|
||||
若反向树显示 `reqwest -> platform-oss -> shared-contracts -> module-* -> spacetime-module`,优先检查新增的 `shared-contracts` 或领域 crate 依赖是否忘记关闭默认 feature,或 `shared-contracts` feature 是否错误依赖了平台实现 crate。原生 `api-server` 需要资产上传契约时,应在自身 `Cargo.toml` 显式启用 `shared-contracts` 的 `oss-contracts` feature,而不是让 workspace 根依赖默认启用。
|
||||
@@ -386,8 +386,8 @@ node scripts/check-server-rs-ddd-boundaries.mjs
|
||||
cargo fmt --all --check --manifest-path server-rs/Cargo.toml
|
||||
cargo test --workspace --manifest-path server-rs/Cargo.toml
|
||||
cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||
npm run api-server:maincloud
|
||||
npm run api-server
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
若 `npm run api-server:maincloud` 因本机未配置 Maincloud 数据库或令牌失败,必须记录具体错误;不能改用旧后端重启命令。
|
||||
若 `npm run api-server` 因本机未配置 Maincloud 数据库或令牌失败,必须记录具体错误;不能改用旧后端重启命令。
|
||||
|
||||
@@ -1818,7 +1818,7 @@ npm.cmd run api-server:maincloud
|
||||
npm.cmd run api-server:maincloud
|
||||
```
|
||||
|
||||
结果:命令在 60 秒观察窗口内超时。随后探测 `http://127.0.0.1:3100/healthz` 未连通;进程检查发现存在两组遗留的 `npm run api-server:maincloud -> scripts/api-server-maincloud.mjs -> cargo run -p api-server` 链路,同时还有并行 `module-assets` 测试和 `spacetime-module` 检查在运行。该结果不是本次 WP-API route 编译错误,需清理本次遗留 api-server 启动链后再做一次干净启动。
|
||||
结果:命令在 60 秒观察窗口内超时。随后探测 `http://127.0.0.1:3100/healthz` 未连通;进程检查发现存在两组遗留的 `npm run api-server -> scripts/api-server-maincloud.mjs -> cargo run -p api-server` 链路,同时还有并行 `module-assets` 测试和 `spacetime-module` 检查在运行。该结果不是本次 WP-API route 编译错误,需清理本次遗留 api-server 启动链后再做一次干净启动。
|
||||
|
||||
### 2026-04-29 WP-API runtime projection 接线
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@
|
||||
- 后端调用 `module-runtime-story` 纯规则结算动作,推进 `runtimeActionVersion`,写回 runtime snapshot,并用 `continue_story` 记录本轮 narrative event。
|
||||
- 响应同样返回 `StoryRuntimeMutationResponse { projection }`,不返回旧 `viewModel / presentation / patches / snapshot` 组合。
|
||||
|
||||
## 版本口径
|
||||
|
||||
`StoryRuntimeProjectionResponse.serverVersion` 只表示动作并发版本,必须与 `projection.gameState.runtimeActionVersion` 保持一致。前端点击运行时选项时把该值作为 `clientVersion` 提交,后端只用它防止基于旧动作快照重复结算。
|
||||
|
||||
以下字段不能参与 `serverVersion` 计算:
|
||||
|
||||
1. `runtime_snapshot.version`:这是保存快照结构版本,当前由 `SAVE_SNAPSHOT_VERSION` 固定维护,写快照不会把它当作动作轮次递增。
|
||||
2. `story_session.version`:这是故事事件流版本,`continue_story` 会推进它,但它不一定等同于当前运行时动作快照版本。
|
||||
|
||||
读取 `/runtime-projection` 和写入 `/actions/resolve` 的回包都必须从持久化 `gameState.runtimeActionVersion` 解析 `serverVersion`。如果旧快照缺少该字段,才允许回退到 `storySession.version` 或本轮 resolver 输出版本,避免历史存档无法恢复;不得再使用 `runtime_snapshot.version.max(story_session.version)` 这类混合口径。
|
||||
|
||||
## 契约收口
|
||||
|
||||
本轮新增 story contract 下的运行时写侧 DTO:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# WP-SC Story runtime legacy option scope 兼容修复(2026-05-03)
|
||||
|
||||
## 背景
|
||||
|
||||
`/api/story/sessions/{storySessionId}/runtime-projection` 读取侧在解析历史 `currentStory.options` 时,曾直接把 option JSON 反序列化为后端投影类型,并要求 `scope` 必填。
|
||||
|
||||
但旧快照里的 `currentStory.options` 只保证 `functionId` / `actionText` / `text` 等基础字段,`scope` 并不是历史存档的稳定字段。于是旧存档在读取 runtime inventory view 时会报:
|
||||
|
||||
`currentStory.options 无法映射为后端选项投影: missing field 'scope'`
|
||||
|
||||
## 修复口径
|
||||
|
||||
1. `spacetime-client` 的 story runtime projection 读取不再直接反序列化 `currentStory.options`。
|
||||
2. 改为复用 `module-runtime-story::build_runtime_story_options(...)`,让历史快照通过领域 helper 统一补齐 `story / combat / npc` 作用域。
|
||||
3. 保持 `StoryRuntimeProjectionSource` 与 `StoryRuntimeProjectionResponse` 输出结构不变,不改 SpacetimeDB schema,不改 reducer,不改 API route。
|
||||
|
||||
## 验收
|
||||
|
||||
```powershell
|
||||
cargo test -p spacetime-client story_runtime --manifest-path server-rs\Cargo.toml
|
||||
cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
|
||||
```
|
||||
@@ -39,4 +39,4 @@ GENARRATIVE_SPACETIME_TOKEN
|
||||
|
||||
1. 新增 SpacetimeDB 运维脚本时,不允许把云端服务写成默认值。
|
||||
2. 文档中的验证命令统一使用 `npm run api-server`。
|
||||
3. 如果某次任务需要连接非本地 SpacetimeDB,必须在文档和验证记录中写清楚实际 `SERVER_URL`、数据库名和 root-dir。
|
||||
3. 如果某次任务需要连接非本地 SpacetimeDB,必须在文档和验证记录中写清楚实际 `SERVER_URL`、数据库名和身份来源;除 CI/CD 脚本内部受控用法外,不再把 `--root-dir` 写入人工命令。
|
||||
|
||||
@@ -69,8 +69,9 @@ node scripts/spacetime-revoke-migration-operator.mjs \
|
||||
|
||||
当前会构建或发布 `spacetime-module` 的脚本默认都会生成并显示迁移引导密钥:
|
||||
|
||||
- `npm run dev:rust`:在本地 `spacetime publish --module-path` 前生成密钥,控制台输出 `[dev:rust] 迁移引导密钥: ...`。
|
||||
- `npm run dev:rust`:在本地 `spacetime publish --module-path ... --build-options="--debug"` 前生成密钥,控制台输出 `[dev:rust] 迁移引导密钥: ...`;SpacetimeDB CLI 会在 publish 内部按 debug 构建参数编译模块。
|
||||
- `npm run deploy:rust:remote`:在构建发布包 wasm 前生成密钥,控制台输出 `[deploy:rust] 迁移引导密钥: ...`,并把同一份密钥写入发布包根目录的 `migration-bootstrap-secret.txt`。服务器执行 `./start.sh` 发布 wasm 时也会再次显示该文件里的密钥。
|
||||
- `npm run build:production-release -- --component spacetime-module`:在生产 Stdb module 构建前默认生成或复用 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`,注入 `spacetime_module.wasm`,并写入 `build/<version>/migration-bootstrap-secret.txt`。生产构建日志只显示密钥来源和长度,不打印明文;该文件应保存为 Jenkins Secret Text,供 `Genarrative-Database-Export` / `Genarrative-Database-Import` 的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 使用。
|
||||
|
||||
如果迁移完成后不希望 wasm 继续携带引导密钥,重新发布时传 `--no-migration-bootstrap-secret`。远端发布包若使用 `--skip-spacetime-build`,必须同时传 `--no-migration-bootstrap-secret`,否则脚本会拒绝生成一个无法注入旧 wasm 的新密钥。
|
||||
|
||||
|
||||
@@ -16,17 +16,17 @@ error starting database: failed to init replica 1 for <new-database-identity>: m
|
||||
2. `replica 1` 的持久化数据仍带有旧库 `c20037fcfaac4e5c4b1f492f026a4f6119a98f56319b77f21ef021ededf8b7ae`。
|
||||
3. SpacetimeDB 因同一个副本目录中 identity 不一致而拒绝继续启动。
|
||||
|
||||
这不是 Rust 编译错误,也不是 `api-server` 的 token 错误。只要错误来自 `server-rs/.spacetimedb/local/.../spacetime-standalone.log`,优先按本地 root-dir 数据目录污染处理。
|
||||
这不是 Rust 编译错误,也不是 `api-server` 的 token 错误。只要错误来自 `server-rs/.spacetimedb/local/.../spacetime-standalone.log`,优先按本地 SpacetimeDB 数据目录污染处理。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
`spacetime start --edition standalone` 会在同一个 `--root-dir` 下保存控制库、程序字节、WAL 与 replica 数据。当前仓库默认本地 root-dir 是:
|
||||
`spacetime start --edition standalone` 会在本地数据目录中保存控制库、程序字节、WAL 与 replica 数据。当前仓库默认本地数据目录是:
|
||||
|
||||
```text
|
||||
server-rs/.spacetimedb/local
|
||||
server-rs/.spacetimedb/local/data
|
||||
```
|
||||
|
||||
当这个目录曾经启动并发布过旧 database identity,之后又用同一个 root-dir 初始化或发布到另一个 database identity 时,可能出现:
|
||||
当这个目录曾经启动并发布过旧 database identity,之后又用同一个数据目录初始化或发布到另一个 database identity 时,可能出现:
|
||||
|
||||
1. `control-db` 记录的是新库。
|
||||
2. `data/replicas/1` 里仍残留旧库 WAL 或快照。
|
||||
@@ -36,8 +36,8 @@ server-rs/.spacetimedb/local
|
||||
|
||||
1. 不在脚本里默认删除 `.spacetimedb` 数据,避免误删本地开发数据。
|
||||
2. 如果只是本地开发库且数据可丢弃,优先备份后重建 `data` 目录。
|
||||
3. 如果数据必须保留,不要清理目录;应改回创建旧库时使用的 database/root-dir,或先导出迁移数据。
|
||||
4. 本地 standalone root-dir 与其它部署目标是两条链路;不要通过切回 `server-node` 或 PostgreSQL 绕过。
|
||||
3. 如果数据必须保留,不要清理目录;应改回创建旧库时使用的 database/server,或先导出迁移数据。
|
||||
4. 本地 standalone 数据目录与其它部署目标是两条链路;不要通过切回 `server-node` 或 PostgreSQL 绕过。
|
||||
|
||||
## 4. 本地可丢弃数据时的修复
|
||||
|
||||
@@ -73,10 +73,10 @@ npm run dev:rust
|
||||
|
||||
## 5. 需要保留数据时的处理
|
||||
|
||||
不要移动或删除 `server-rs/.spacetimedb/local/data`。先确认旧库 identity 对应的数据库名、root-dir 与发布命令,然后选择:
|
||||
不要移动或删除 `server-rs/.spacetimedb/local/data`。先确认旧库 identity 对应的数据库名、server 与发布命令,然后选择:
|
||||
|
||||
1. 用旧库对应的 database/root-dir 重新启动。
|
||||
2. 使用迁移导出脚本导出旧数据,再清理本地 root-dir 并导入到新库。
|
||||
1. 用旧库对应的 database/server 重新启动或连接。
|
||||
2. 使用迁移导出脚本导出旧数据,再清理本地数据目录并导入到新库。
|
||||
3. 如目标其实是其它已运行的 SpacetimeDB 服务,改用 `GENARRATIVE_SPACETIME_SERVER_URL` 指向该服务,避免误启动本地 standalone。
|
||||
|
||||
## 6. 脚本诊断
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# SpacetimeDB publish sccache 降级处理
|
||||
|
||||
## 背景
|
||||
|
||||
Windows 本地执行 `npm run dev:rust` 或 `spacetime publish` 时,`spacetime` 会在内部调用 Cargo 构建 `server-rs/crates/spacetime-module`。当前本地开发 publish 会追加 `--build-options="--debug"`,让 SpacetimeDB CLI 用 debug 构建参数编译模块。因为 `server-rs/.cargo/config.toml` 配置了 `rustc-wrapper = "sccache"`,即使当前 shell 没有设置 `RUSTC_WRAPPER`,Cargo 仍会先执行 `sccache rustc -vV`。
|
||||
|
||||
当本机 sccache server 状态损坏、client/server 通信异常或版本残留不一致时,可能出现:
|
||||
|
||||
```text
|
||||
sccache: error: Timed out waiting for server startup. Maybe the remote service is unreachable?
|
||||
sccache: error: failed to execute compile
|
||||
sccache: caused by: Failed to send data to or receive data from server
|
||||
sccache: caused by: Failed to read response header
|
||||
sccache: caused by: failed to fill whole buffer
|
||||
```
|
||||
|
||||
这类错误发生在 rustc wrapper 层,不能说明 SpacetimeDB module 代码本身编译失败。
|
||||
|
||||
## 2026-05-11 本机根因定位
|
||||
|
||||
本机 `cargo check -p api-server` 失败时,Cargo 还没有进入业务 crate 编译,而是在读取 `server-rs/.cargo/config.toml` 后执行 `sccache rustc -vV` 探测编译器版本。失败的 stderr 会被写入 `server-rs/target/.rustc_info.json`,内容为 `Timed out waiting for server startup`。
|
||||
|
||||
当前 PowerShell 环境设置了 `SCCACHE_OSS_BUCKET=genarrative-sccache`、`SCCACHE_OSS_ENDPOINT=https://oss-rg-china-mainland.aliyuncs.com` 和 `SCCACHE_OSS_KEY_PREFIX=genarrative`,且没有设置本地 `SCCACHE_DIR`。因此 sccache daemon 冷启动时会先初始化 OSS 远端缓存,并执行 `.sccache_check` 的读写检查;日志中可见 `Init oss cache ...`、`proxy(http://127.0.0.1:7897/) intercepts ...`,随后才出现 `server started, listening on 127.0.0.1:4226`。
|
||||
|
||||
本次排查的结论是:冷启动失败主要发生在 sccache client 等待 daemon 启动的握手窗口内,而 daemon 启动又依赖 OSS/本机代理链路先完成缓存可读写检查。代理或 OSS 链路稍慢时,Cargo 调用的 `sccache rustc -vV` 会先超时;daemon 预热后直接执行同一条 `sccache rustc -vV` 又可能成功,所以这是冷启动/通道状态问题,不是 `api-server` 或 Rust 代码错误。
|
||||
|
||||
辅助证据:
|
||||
|
||||
1. `rustc -vV` 可直接输出版本,说明 Rust 工具链本身可用。
|
||||
2. `tasklist` 曾只看到 `sccache --show-stats` 客户端进程,`netstat` 只出现到 `127.0.0.1:4226` 的 `SYN_SENT`,没有真正的 `LISTEN`,说明当时 client 正在等一个尚未成功监听的 daemon。
|
||||
3. 在子进程中临时清掉 `SCCACHE_OSS_*` 并设置本地 `SCCACHE_DIR` 后,sccache 退回本地磁盘缓存,日志显示 `Init disk cache ...`,`rustc -vV` 和 `sccache --show-stats` 均能完成。
|
||||
4. `C:\Users\DSK\AppData\Roaming\Mozilla\sccache\config\config` 缺失只是非致命 warning,本机实际配置来自环境变量。
|
||||
|
||||
## 本地开发处理
|
||||
|
||||
`scripts/dev-rust-stack.sh` 的 publish 阶段继续由 SpacetimeDB CLI 内部调用 Cargo,并通过 `--build-options="--debug"` 使用 debug 构建参数。遇到 sccache 冷启动超时时,优先保留 `sccache` wrapper,并修复 sccache daemon 的启动等待时间;只有在排除 sccache 本身问题时,才临时绕过 wrapper 验证 rustc 本身可用。
|
||||
|
||||
该处理不修改 `server-rs/.cargo/config.toml`,也不删除本地 target 缓存。
|
||||
|
||||
## 手动排障命令
|
||||
|
||||
优先确认 rustc 本身可用:
|
||||
|
||||
```bash
|
||||
rustc -vV
|
||||
```
|
||||
|
||||
如果要保留 sccache 并修复冷启动等待时间,在 PowerShell 中创建或更新 sccache 默认配置:
|
||||
|
||||
```powershell
|
||||
$configDir = Join-Path $env:APPDATA "Mozilla\sccache\config"
|
||||
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
||||
@(
|
||||
"# Windows 本机 sccache 冷启动需要先完成 OSS 缓存读写检查。"
|
||||
"# 拉长 client 等待 daemon 启动的时间,避免 Cargo 在 rustc -vV 阶段误判超时。"
|
||||
"server_startup_timeout_ms = 60000"
|
||||
) | Set-Content -Encoding UTF8 -Path (Join-Path $configDir "config")
|
||||
```
|
||||
|
||||
随后清掉 Cargo 曾缓存的失败探测结果,并从冷启动验证:
|
||||
|
||||
```powershell
|
||||
cd C:\proj\Genarrative\server-rs
|
||||
sccache --stop-server
|
||||
Remove-Item -Force target\.rustc_info.json -ErrorAction SilentlyContinue
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
注意:不要在另一个 `cargo` / `rustc` 仍在编译时执行 `taskkill /F /IM sccache.exe /T`。sccache 对 proc-macro crate 会显示 `Server sent UnhandledCompile` 并把请求转交给真实 rustc;如果此时强杀 sccache client/server,可能让 `serde_derive`、`spacetimedb-bindings-macro` 等 proc-macro 编译直接以 `sccache ... exit code: 1` 失败,而 stderr 里看不到真正的 Rust 诊断。这是排障动作打断编译,不是 `spacetime-module` 源码错误。
|
||||
|
||||
如果只想临时绕过本次 Cargo 构建的 sccache wrapper,可在 Git Bash 中执行:
|
||||
|
||||
```bash
|
||||
cd server-rs/crates/spacetime-module
|
||||
RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo check --target=wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
PowerShell 原生 Cargo 的一次性 wrapper 绕过命令是:
|
||||
|
||||
```powershell
|
||||
cd C:\proj\Genarrative\server-rs
|
||||
cargo check -p api-server --config "build.rustc-wrapper=''"
|
||||
```
|
||||
|
||||
如果需要验证是否为 OSS/代理冷启动问题,可只在当前 PowerShell 进程中切到本地缓存做对照:
|
||||
|
||||
```powershell
|
||||
$env:SCCACHE_LOG = "debug"
|
||||
$env:SCCACHE_ERROR_LOG = "C:\proj\Genarrative\logs\sccache-local-start-error.log"
|
||||
$env:SCCACHE_DIR = Join-Path $env:TEMP "genarrative-sccache-local-test"
|
||||
Remove-Item Env:SCCACHE_OSS_BUCKET -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:SCCACHE_OSS_ENDPOINT -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:SCCACHE_OSS_KEY_PREFIX -ErrorAction SilentlyContinue
|
||||
sccache "C:\Users\DSK\.rustup\toolchains\stable-x86_64-pc-windows-msvc\bin\rustc.exe" -vV
|
||||
sccache --show-stats
|
||||
```
|
||||
|
||||
如果需要排查 sccache server 状态:
|
||||
|
||||
```bash
|
||||
sccache --show-stats
|
||||
sccache --stop-server
|
||||
sccache --start-server
|
||||
```
|
||||
|
||||
`sccache --stop-server` 本身也可能因为 server 通道已损坏而失败;只有确认当前没有 `cargo`、`rustc`、`link` 进程后,才用 `taskkill /F /IM sccache.exe /T` 清理残留进程。此时不应阻断本地开发 publish,先使用 wrapper 降级完成验证。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `bash -n scripts/dev-rust-stack.sh`
|
||||
2. 冷启动后直接执行 `cargo check -p api-server`,确认不再出现 `Timed out waiting for server startup`。
|
||||
3. 执行 `cargo check -p spacetime-module`,确认 proc-macro 依赖和 SpacetimeDB module 都能在 sccache wrapper 下通过。
|
||||
4. `sccache --show-stats` 显示 `Cache location oss, name: genarrative-sccache`,确认仍在使用 sccache/OSS 缓存。
|
||||
5. 重新运行 `npm run dev:rust`,确认 publish 命令带有 `--build-options="--debug"`。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-27`
|
||||
|
||||
状态:历史事故记录。本文针对旧发布包 `start.sh` 的诊断补强,相关 `start.sh` 运行细节已过时,不再作为当前发布或人工排障依据。当前 SpacetimeDB 人工命令不得使用 `--root-dir`,CI/CD 脚本内部受控用法除外。
|
||||
|
||||
## 1. 问题
|
||||
|
||||
执行发布包内 `start.sh` 时,可能只看到:
|
||||
@@ -16,10 +18,10 @@
|
||||
## 2. 常见根因
|
||||
|
||||
1. `GENARRATIVE_SPACETIME_PORT` 对应端口已被其他进程占用。
|
||||
2. `.spacetimedb/` root-dir 权限不正确,当前用户无法写入数据、bin 或日志目录。
|
||||
2. `.spacetimedb/` 运行目录权限不正确,当前用户无法写入数据或日志目录。
|
||||
3. 目标机 `spacetime` 安装不完整,发布包同步不到可执行的 `bin/current/spacetimedb-cli`。
|
||||
4. 目标机上的 `spacetime` 版本与脚本启动参数不兼容。
|
||||
5. 旧 SpacetimeDB 进程仍持有同一 root-dir 或数据锁,但当前 `GENARRATIVE_SPACETIME_SERVER_URL` 指向的端口未就绪。
|
||||
5. 旧 SpacetimeDB 进程仍持有同一运行目录或数据锁,但当前 `GENARRATIVE_SPACETIME_SERVER_URL` 指向的端口未就绪。
|
||||
6. `.spacetimedb/bin/current/` 下只有 `spacetimedb-cli`,缺少 `spacetimedb-standalone`,日志会显示 `exec failed for .../spacetimedb-standalone`。
|
||||
|
||||
## 3. 落地修复
|
||||
@@ -31,11 +33,11 @@
|
||||
3. 当 SpacetimeDB 进程提前退出或等待超时时,自动打印:
|
||||
- `GENARRATIVE_SPACETIME_SERVER_URL` 对应的目标地址。
|
||||
- `GENARRATIVE_SPACETIME_HOST:GENARRATIVE_SPACETIME_PORT` 对应的监听地址。
|
||||
- 当前 `GENARRATIVE_SPACETIME_ROOT_DIR`。
|
||||
- 当前 `.spacetimedb/` 运行目录。
|
||||
- `logs/spacetimedb.log` 最近 120 行。
|
||||
- `spacetime server ping` 的原始输出。
|
||||
- `ss` 或 `netstat` 中当前端口的监听情况。
|
||||
- 同一 root-dir 下仍在运行的 SpacetimeDB 进程。
|
||||
- 同一运行目录下仍在运行的 SpacetimeDB 进程。
|
||||
|
||||
## 4. 现场排查
|
||||
|
||||
@@ -43,7 +45,7 @@
|
||||
|
||||
```bash
|
||||
tail -n 120 logs/spacetimedb.log
|
||||
spacetime --root-dir ./.spacetimedb server ping "${GENARRATIVE_SPACETIME_SERVER_URL:-http://127.0.0.1:3101}"
|
||||
spacetime server ping "${GENARRATIVE_SPACETIME_SERVER_URL:-http://127.0.0.1:3101}"
|
||||
ss -ltnp | grep ':3101' || true
|
||||
```
|
||||
|
||||
@@ -54,7 +56,7 @@ exec failed for /var/lib/jenkins/deploy/Genarrative/.spacetimedb/bin/current/spa
|
||||
No such file or directory (os error 2)
|
||||
```
|
||||
|
||||
说明发布目录的 SpacetimeDB root-dir 中同步了 CLI,但没有同步 standalone。现场可先执行:
|
||||
说明发布目录的 SpacetimeDB 运行目录中同步了 CLI,但没有同步 standalone。该段仅适用于仍保留 CI/CD 内部受控 `--root-dir` 的旧发布包;新人工排障不要引入 `--root-dir`。现场可先执行:
|
||||
|
||||
```bash
|
||||
cd /var/lib/jenkins/deploy/Genarrative
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# start.sh 发布 SpacetimeDB 遇到 403 的处理方案
|
||||
# SpacetimeDB publish 遇到 403 的处理方案
|
||||
|
||||
日期:`2026-04-26`
|
||||
|
||||
状态:历史事故记录。本文涉及发布包 `start.sh` 的描述已过时,不再作为当前发布或排障入口。当前人工命令约束以 `AGENTS.md`、`.hermes/shared-memory/pitfalls.md` 和现行本地/生产技术文档为准;除 CI/CD 脚本内部受控用法外,不再使用 `spacetime --root-dir`。
|
||||
|
||||
## 1. 问题
|
||||
|
||||
执行发布包内 `start.sh` 时,`spacetime publish` 可能在 `Checking for breaking changes...` 后失败:
|
||||
@@ -23,31 +25,75 @@ SpacetimeDB 的数据库更新权限绑定到创建或被授权的身份。只
|
||||
3. `GENARRATIVE_SPACETIME_SERVER_URL` 指向其它 SpacetimeDB 服务,而当前 CLI 身份不是该数据库的所有者或授权成员。
|
||||
4. `.env.local` 中的 `GENARRATIVE_SPACETIME_DATABASE` 指向了另一个环境的数据库名或数据库 identity。
|
||||
|
||||
## 3. 落地修复
|
||||
## 3. 当前处理口径
|
||||
|
||||
发布包生成的 `start.sh` 使用发布目录下的 `.spacetimedb/` 作为 SpacetimeDB root:
|
||||
除 CI/CD 脚本内部受控用法外,人工命令、本地联调、临时排障和文档示例不再使用 `spacetime --root-dir`。后续处理遵循:
|
||||
|
||||
```bash
|
||||
GENARRATIVE_SPACETIME_ROOT_DIR="${SCRIPT_DIR}/.spacetimedb"
|
||||
```
|
||||
1. 本地开发优先使用项目脚本 `npm run dev` / `npm run dev:rust`。
|
||||
2. 手工发布必须显式传 `--server` 或 `--server-url`,不依赖 CLI 默认 server。
|
||||
3. 身份问题通过同一 CLI 登录态、专用运行用户、显式 token 或 SpacetimeDB 侧授权处理。
|
||||
4. 本地数据隔离使用项目脚本维护的数据目录,必要时通过 `--data-dir` 启动 standalone;不要用 `--root-dir` 作为人工排障手段。
|
||||
|
||||
启动、探活和发布统一使用:
|
||||
|
||||
```bash
|
||||
spacetime --root-dir="${GENARRATIVE_SPACETIME_ROOT_DIR}" ...
|
||||
```
|
||||
|
||||
`spacetime start` 不再额外设置 `--data-dir`,启动前会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`;当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`。启动参数、探活和 root-dir 占用判定都使用同一个 `.spacetimedb/`。这样可以把发布包与部署机全局 `~/.spacetime` 隔离,避免后续人工 `spacetime login` 影响本地发布包。但如果旧 `.spacetimedb/` 已经由另一个身份创建,仍需要按第 4 节处理。
|
||||
历史上曾用项目级 CLI root 隔离身份;该方案已废弃。CI/CD 脚本如因生产运行用户隔离仍保留内部受控用法,不得复制为人工命令模板。
|
||||
|
||||
## 4. 排查与处理
|
||||
|
||||
先在执行 `start.sh` 的同一台机器、同一用户下确认身份:
|
||||
|
||||
```bash
|
||||
spacetime --root-dir ./.spacetimedb login show
|
||||
spacetime --root-dir ./.spacetimedb list --server http://127.0.0.1:3101
|
||||
spacetime login show
|
||||
spacetime server list
|
||||
spacetime list --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
本地开发栈排查时确认项目脚本记录的实际 server,再用同一个 server 验证:
|
||||
|
||||
```powershell
|
||||
Get-Content -Encoding UTF8 server-rs/.spacetimedb/local/data/dev-rust-spacetime-url
|
||||
spacetime login show
|
||||
spacetime list --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
如果 `spacetime login show` 的身份无权更新目标库,不要尝试通过 `--root-dir` 绕过。应选择以下路径之一:
|
||||
|
||||
1. 重新登录目标库 owner 或被授权的身份。
|
||||
2. 在 SpacetimeDB 侧给当前身份补授权。
|
||||
3. 如果只是本地开发库且数据可丢弃,按 4.1 重置本地库后重新发布。
|
||||
4. 如果是连错服务或库名,修正 `.env.local` 中的 `GENARRATIVE_SPACETIME_DATABASE` / `GENARRATIVE_SPACETIME_SERVER_URL`。
|
||||
|
||||
### 4.1 `npm run dev` 本地 401 / 403 快速恢复
|
||||
|
||||
如果 `npm run dev` 启动本地开发栈时,SpacetimeDB 在登录、发布或预检查阶段返回 `401` / `403`,且确认本地测试库可以丢弃,可以按下面顺序重置本机默认 local server、旧 CLI token 和本地数据库:
|
||||
|
||||
```powershell
|
||||
# 1. 先停掉正在跑的本地 server
|
||||
Get-Process spacetimedb-standalone, spacetime -ErrorAction SilentlyContinue
|
||||
|
||||
# 如确认只是本地测试 server,可结束它
|
||||
Stop-Process -Name spacetimedb-standalone -ErrorAction SilentlyContinue
|
||||
|
||||
# 2. 清掉 CLI 保存的旧 token
|
||||
spacetime logout
|
||||
|
||||
# 3. 确认默认目标是本地,或发布时显式指定 local
|
||||
spacetime server list
|
||||
spacetime server set-default local
|
||||
|
||||
# 4. 如要彻底清空默认本地库,停 server 后再清
|
||||
spacetime server clear -y
|
||||
|
||||
# 5. 重新启动默认本地 server
|
||||
spacetime start
|
||||
|
||||
# 6. 另开一个终端,向当前 local server 重新拿本地 token
|
||||
spacetime login --server-issued-login local
|
||||
|
||||
# 7. 再发布
|
||||
spacetime publish --server local A
|
||||
```
|
||||
|
||||
这条流程适合“本地 `npm run dev` 因旧 token、旧本地库或 CLI 默认 server 混乱导致无法继续”的场景。若当前目标库需要保留数据,不要执行 `spacetime server clear -y`,先确认 `login show`、目标 server、数据库名和数据库所有者身份。
|
||||
|
||||
如果目标是本地部署库,且允许清空本地数据:
|
||||
|
||||
```bash
|
||||
@@ -60,7 +106,7 @@ mv .spacetimedb ".spacetimedb.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
1. 不要删除 `.spacetimedb/`。
|
||||
2. 找到创建该数据库的 SpacetimeDB 身份。
|
||||
3. 用该身份对应的 CLI root 执行发布,或在 SpacetimeDB 侧补授权后再发布。
|
||||
3. 用该身份登录当前 CLI,或在 SpacetimeDB 侧补授权后再发布。
|
||||
|
||||
如果目标是其它 SpacetimeDB 服务:
|
||||
|
||||
@@ -71,5 +117,6 @@ mv .spacetimedb ".spacetimedb.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
## 5. 约束
|
||||
|
||||
1. `--clear-database` 只处理 schema 冲突时的数据清理,不会绕过 SpacetimeDB 身份授权。
|
||||
2. 不要通过切回旧 `server-node` 或 PostgreSQL 绕过发布错误。
|
||||
3. 前端与 `api-server` 的数据库名必须和 `start.sh` 发布的库名一致,否则后续接口会连到未发布或无权限的库。
|
||||
2. 除 CI/CD 脚本内部受控用法外,不要在人工排障或文档示例中使用 `spacetime --root-dir`。
|
||||
3. 不要通过切回旧 `server-node` 或 PostgreSQL 绕过发布错误。
|
||||
4. 前端与 `api-server` 的数据库名必须和 `start.sh` 发布的库名一致,否则后续接口会连到未发布或无权限的库。
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-27`
|
||||
|
||||
状态:历史事故记录。本文只解释旧发布包 `start.sh` 的一次误报修复,相关 `root-dir` 检测不再作为当前发布或人工排障口径。当前人工命令不得使用 `spacetime --root-dir`,CI/CD 脚本内部受控用法除外。
|
||||
|
||||
## 1. 问题
|
||||
|
||||
执行发布包内 `start.sh` 时,可能出现:
|
||||
|
||||
@@ -24,11 +24,13 @@ 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`, `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_feedback_submission`, `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` |
|
||||
| 视觉小说 | `visual_novel_agent_session`, `visual_novel_agent_message`, `visual_novel_work_profile`, `visual_novel_runtime_run`, `visual_novel_runtime_history_entry`, `visual_novel_runtime_event` |
|
||||
| 大鱼吃小鱼 | `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` |
|
||||
@@ -69,8 +71,9 @@ SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
|
||||
|
||||
### `user_account`
|
||||
|
||||
- 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关和 token 版本。
|
||||
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: Option<String>`, `password_login_enabled: bool`, `token_version: u64`。
|
||||
- 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关、token 版本和默认不前端展示的运营标签。
|
||||
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: String`, `password_login_enabled: bool`, `token_version: u64`, `user_tags: Option<Vec<String>>`。
|
||||
- 说明:`user_tags` 数据库默认 `None`,业务读取时按空数组归一化;只允许后端白名单投影到特定业务接口,不得在登录态、个人资料等通用前端响应中直接暴露。
|
||||
- 索引:`username`, `public_user_code`。
|
||||
|
||||
```sql
|
||||
@@ -158,14 +161,86 @@ 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` 天;通用埋点写入 `record_tracking_event_and_return` 会在写入 `tracking_event` / `tracking_daily_stat` 前按同一 `day_key` 自动幂等补齐当日维表。
|
||||
- 口径:`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`
|
||||
|
||||
- 作用:埋点原始事件表,保存整站、作品、模块和用户层的原始事实。
|
||||
- 结构:`event_id PK: String`, `event_key: String`, `scope_kind: RuntimeTrackingScopeKind`, `scope_id: String`, `day_key: i64`, `user_id: Option<String>`, `owner_user_id: Option<String>`, `profile_id: Option<String>`, `module_key: Option<String>`, `metadata_json: String`, `occurred_at: Timestamp`。
|
||||
- 索引:`event_key`, `(scope_kind, scope_id)`, `(user_id, occurred_at)`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM tracking_event WHERE event_id = '<event_id>';
|
||||
SELECT * FROM tracking_event WHERE event_key = '<event_key>' ORDER BY occurred_at DESC;
|
||||
SELECT * FROM tracking_event WHERE scope_kind = 'User' AND scope_id = '<user_id>' ORDER BY occurred_at DESC;
|
||||
```
|
||||
|
||||
### `tracking_daily_stat`
|
||||
|
||||
- 作用:埋点按北京时间自然日聚合后的真实表,供任务统计和快速查询使用。
|
||||
- 结构:`stat_id PK: String`, `event_key: String`, `scope_kind: RuntimeTrackingScopeKind`, `scope_id: String`, `day_key: i64`, `count: u32`, `first_occurred_at: Timestamp`, `last_occurred_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`(event_key, day_key)`, `(scope_kind, scope_id, day_key)`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM tracking_daily_stat WHERE stat_id = '<stat_id>';
|
||||
SELECT * FROM tracking_daily_stat WHERE scope_kind = 'User' AND scope_id = '<user_id>' ORDER BY day_key DESC;
|
||||
```
|
||||
|
||||
### `profile_task_config`
|
||||
|
||||
- 作用:个人任务配置表,后台可修改每日登录等任务的奖励、阈值、启用状态和排序。
|
||||
- 结构:`task_id PK: String`, `title: String`, `description: String`, `event_key: String`, `cycle: RuntimeProfileTaskCycle`, `scope_kind: RuntimeTrackingScopeKind`, `threshold: u32`, `reward_points: u64`, `enabled: bool`, `sort_order: i32`, `created_by: String`, `created_at: Timestamp`, `updated_by: String`, `updated_at: Timestamp`。
|
||||
- 索引:主键 `task_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_task_config WHERE task_id = 'daily_login';
|
||||
SELECT * FROM profile_task_config ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `profile_task_progress`
|
||||
|
||||
- 作用:个人任务进度表,保存用户在某个自然日的任务进度和状态快照。
|
||||
- 结构:`progress_id PK: String`, `user_id: String`, `task_id: String`, `day_key: i64`, `progress_count: u32`, `threshold: u32`, `status: RuntimeProfileTaskStatus`, `updated_at: Timestamp`。
|
||||
- 索引:`user_id`, `(user_id, task_id)`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_task_progress WHERE user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM profile_task_progress WHERE user_id = '<user_id>' AND task_id = 'daily_login' ORDER BY day_key DESC;
|
||||
```
|
||||
|
||||
### `profile_task_reward_claim`
|
||||
|
||||
- 作用:个人任务领奖记录表,记录用户、任务、自然日、奖励和对应钱包流水。
|
||||
- 结构:`claim_id PK: String`, `user_id: String`, `task_id: String`, `day_key: i64`, `reward_points: u64`, `wallet_ledger_id: String`, `claimed_at: Timestamp`。
|
||||
- 索引:`user_id`, `(user_id, task_id)`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_task_reward_claim WHERE user_id = '<user_id>' ORDER BY claimed_at DESC;
|
||||
SELECT * FROM profile_task_reward_claim WHERE claim_id = '<user_id>:daily_login:<day_key>';
|
||||
```
|
||||
|
||||
### `profile_redeem_code`
|
||||
|
||||
- 作用:运营发放的光点兑换码,支持公共码、唯一码和私有码。
|
||||
- 结构:`code PK: String`, `mode: RuntimeProfileRedeemCodeMode`, `reward_points: u64`, `max_uses: u32`, `global_used_count: u32`, `enabled: bool`, `allowed_user_ids: Vec<String>`, `created_by: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:主键 `code`。
|
||||
- 后台读取:`GET /admin/api/profile/redeem-codes` 从该表返回已有兑换码,后台列表点击后通过 upsert 修改同一条记录。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_redeem_code WHERE code = '<CODE>';
|
||||
SELECT * FROM profile_redeem_code ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `profile_redeem_code_usage`
|
||||
@@ -181,13 +256,16 @@ SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>';
|
||||
|
||||
### `profile_invite_code`
|
||||
|
||||
- 作用:用户邀请中心的邀请码主表,保存用户当前稳定邀请码。
|
||||
- 结构:`user_id PK: String`, `invite_code: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 作用:用户邀请中心的邀请码主表,也承载后台运营预置邀请码和使用后授予账号的运营标签配置。
|
||||
- 结构:`user_id PK: String`, `invite_code: String`, `metadata_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`, `starts_at: Option<Timestamp>`, `expires_at: Option<Timestamp>`。
|
||||
- 索引:主键 `user_id`,唯一索引 `invite_code`。
|
||||
- 后台读取:`GET /admin/api/profile/invite-codes` 只返回 `user_id` 以 `admin:` 开头的后台预置码;普通用户自己的邀请码不得进入后台运营列表。
|
||||
- 说明:使用该邀请码后授予的标签存放在 `metadata_json.userTags`,服务端兼容读取 `metadata_json.user_tags`;用户注册填写该邀请码后合并进 `user_account.user_tags`,不回改历史用户。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_invite_code WHERE user_id = '<user_id>';
|
||||
SELECT * FROM profile_invite_code WHERE invite_code = '<invite_code>';
|
||||
SELECT * FROM profile_invite_code WHERE user_id LIKE 'admin:%' ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `profile_referral_relation`
|
||||
@@ -258,6 +336,18 @@ SELECT * FROM profile_recharge_order WHERE order_id = '<order_id>';
|
||||
SELECT * FROM profile_recharge_order WHERE user_id = '<user_id>' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### `profile_feedback_submission`
|
||||
|
||||
- 作用:保存“我的”页签帮助与反馈提交记录,关联当前登录用户的问题描述、选填联系电话和图片凭证快照。
|
||||
- 结构:`feedback_id PK: String`, `user_id: String`, `description: String`, `contact_phone: Option<String>`, `evidence_json: String`, `status: RuntimeProfileFeedbackStatus`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`user_id`, `(user_id, created_at)`。
|
||||
- 访问边界:私有表。前端只通过 `POST /api/profile/feedback` 提交,HTTP 回包只返回凭证元数据,不回显 `evidence_json` 中的 Data URL。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_feedback_submission WHERE feedback_id = '<feedback_id>';
|
||||
SELECT * FROM profile_feedback_submission WHERE user_id = '<user_id>' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### `profile_save_archive`
|
||||
|
||||
- 作用:用户存档列表,保存世界信息、封面、当前状态 JSON 和剧情 JSON。
|
||||
@@ -573,8 +663,9 @@ SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY c
|
||||
|
||||
### `match3d_work_profile`
|
||||
|
||||
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态和游玩次数。
|
||||
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`。
|
||||
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态、游玩次数和草稿生成出的独立物品素材引用。
|
||||
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`, `generated_item_assets_json: Option<String>`。
|
||||
- 说明:`generated_item_assets_json` 保存 `Match3DGeneratedItemAsset` 数组 JSON,用于草稿页退出后从作品架重进时恢复 `3D素材` Tab 中的切割图片和 GLB 模型预览;运行态也通过该字段拿到 `modelSrc` / `modelObjectKey` 并优先渲染生成模型。基础信息自动保存和发布必须保留该字段。
|
||||
- 索引:`owner_user_id`, `publication_status`。
|
||||
|
||||
```sql
|
||||
@@ -595,6 +686,125 @@ 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>';
|
||||
```
|
||||
|
||||
## 视觉小说表
|
||||
|
||||
> VN-13 复核:当前视觉小说首版只保留本节 6 张表;`visual_novel_runtime_history_entry` 和 `visual_novel_runtime_event` 均不得扩展成回放数据源。维护入口见 [视觉小说模板实现收口与交接说明](./VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md)。
|
||||
|
||||
### `visual_novel_agent_session`
|
||||
|
||||
- 作用:视觉小说创作 Agent 会话主表,保存创建起点、源资产、当前阶段、底稿 JSON、待执行 action 和发布 profile 指针。
|
||||
- 结构:`session_id PK: String`, `owner_user_id: String`, `source_mode: String`, `status: String`, `seed_text: String`, `source_asset_ids_json: String`, `current_turn: u32`, `progress_percent: u32`, `draft_json: String`, `pending_action_json: String`, `last_assistant_reply: String`, `published_profile_id: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_agent_session WHERE session_id = '<session_id>';
|
||||
SELECT * FROM visual_novel_agent_session WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `visual_novel_agent_message`
|
||||
|
||||
- 作用:视觉小说创作 Agent 消息流水,保存用户输入和模型回复。
|
||||
- 结构:`message_id PK: String`, `session_id: String`, `role: String`, `kind: String`, `text: String`, `created_at: Timestamp`。
|
||||
- 索引:`session_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_agent_message WHERE session_id = '<session_id>' ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
### `visual_novel_work_profile`
|
||||
|
||||
- 作用:视觉小说作品草稿 / 发布 profile 表,保存平台作品摘要字段、源资产引用、完整 `VisualNovelResultDraft` JSON、发布状态和游玩次数。
|
||||
- 结构:`profile_id PK: String`, `work_id: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `work_title: String`, `work_description: String`, `tags_json: String`, `cover_image_src: String`, `source_asset_ids_json: String`, `draft_json: String`, `publication_status: String`, `publish_ready: bool`, `play_count: u32`, `created_at: Timestamp`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`。
|
||||
- 索引:`owner_user_id`, `publication_status`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_work_profile WHERE profile_id = '<profile_id>';
|
||||
SELECT * FROM visual_novel_work_profile WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM visual_novel_work_profile WHERE publication_status = 'published';
|
||||
```
|
||||
|
||||
### `visual_novel_runtime_run`
|
||||
|
||||
- 作用:视觉小说测试或正式运行态表,保存当前场景、阶段、可见角色、旗标、指标、可选项和运行快照 JSON。
|
||||
- 结构:`run_id PK: String`, `owner_user_id: String`, `profile_id: String`, `mode: String`, `status: String`, `current_scene_id: String`, `current_phase_id: String`, `visible_character_ids_json: String`, `flags_json: String`, `metrics_json: String`, `available_choices_json: String`, `text_mode_enabled: bool`, `snapshot_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`, `profile_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_runtime_run WHERE run_id = '<run_id>';
|
||||
SELECT * FROM visual_novel_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM visual_novel_runtime_run WHERE profile_id = '<profile_id>';
|
||||
```
|
||||
|
||||
### `visual_novel_runtime_history_entry`
|
||||
|
||||
- 作用:视觉小说运行时历史表,保存每轮玩家 / 模型 / 系统事实、step JSON 和快照哈希,用于继续体验与历史重生成边界;不是回放表。
|
||||
- 结构:`entry_id PK: String`, `run_id: String`, `owner_user_id: String`, `profile_id: String`, `turn_index: u32`, `source: String`, `action_text: String`, `steps_json: String`, `snapshot_before_hash: String`, `snapshot_after_hash: String`, `created_at: Timestamp`。
|
||||
- 索引:`run_id`, `owner_user_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_runtime_history_entry WHERE run_id = '<run_id>' ORDER BY turn_index ASC;
|
||||
SELECT * FROM visual_novel_runtime_history_entry WHERE owner_user_id = '<user_id>' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### `visual_novel_runtime_event`
|
||||
|
||||
- 作用:视觉小说运行时审计事件表,用于订阅端、BFF 或排障流程感知 run 事实;该表明确不是 replay、分享回放或片段回放数据源。
|
||||
- 可见性:`public event`。
|
||||
- 结构:`event_id PK: String`, `run_id: String`, `owner_user_id: String`, `profile_id: String`, `event_kind: String`, `client_event_id: String`, `history_entry_id: String`, `payload_json: String`, `occurred_at: Timestamp`。
|
||||
- 索引:`run_id`, `owner_user_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM visual_novel_runtime_event WHERE run_id = '<run_id>' ORDER BY occurred_at ASC;
|
||||
SELECT * FROM visual_novel_runtime_event WHERE owner_user_id = '<user_id>' ORDER BY occurred_at DESC;
|
||||
```
|
||||
|
||||
## 大鱼吃小鱼表
|
||||
|
||||
### `big_fish_creation_session`
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# 方洞挑战 Agent LLM 超时兜底修复 2026-05-05
|
||||
|
||||
## 1. 问题
|
||||
|
||||
现场错误:
|
||||
|
||||
```text
|
||||
方洞挑战聊天生成失败,请稍后重试。:LLM 请求超时,累计尝试 1 次
|
||||
```
|
||||
|
||||
方洞挑战创作 Agent 在同一轮流式 JSON 中需要返回 `replyText`、玩法配置、形状选项、洞口选项和图片提示词。模型可能先返回可见回复,再继续输出完整 JSON;如果上游在流式读取阶段超过通用 LLM 请求超时,后端会发送 SSE `error`,前端只能保留本地 warning 消息,本轮后端会话不会成功推进。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
`platform-llm` 的 `LlmTextRequest` 只有全局 `AppConfig.llm_request_timeout_ms`。创作 Agent 统一走 Responses 流式协议,方洞提示词扩展为视觉资产配置后,单轮输出长度明显增加;通用 30 秒超时更适合普通聊天,不适合结构化创作 Agent 的完整 JSON 流。
|
||||
|
||||
`request_text` 的初始 HTTP 请求会按 `max_retries` 重试,但 `stream_text` 已经进入 `response.chunk()` 读取后,当前错误路径固定记录为一次读取超时,所以用户看到“累计尝试 1 次”。
|
||||
|
||||
## 3. 落地策略
|
||||
|
||||
1. 在 `platform-llm::LlmTextRequest` 增加请求级 `request_timeout_ms` 覆写。
|
||||
2. `execute_request` 优先使用请求级超时,没有覆写时继续使用全局配置。
|
||||
3. `creation_agent_llm_turn` 的流式 JSON 请求统一使用更长的创作 Agent 超时窗口。
|
||||
4. 该超时窗口只影响创作 Agent 的结构化流式 turn,不改变 RPG 运行时聊天、图片生成、SpacetimeDB procedure 或方洞玩法判定。
|
||||
5. 不新增 SpacetimeDB 表结构,不修改 `migration.rs`。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
1. `platform-llm` 测试覆盖请求级 timeout 会让慢响应提前超时。
|
||||
2. `creation_agent_llm_turn` 测试覆盖流式 JSON 请求带创作 Agent timeout。
|
||||
3. `cargo test -p platform-llm -p api-server creation_agent --manifest-path server-rs/Cargo.toml` 通过。
|
||||
4. 后端代码变更后按项目约束运行 `npm run api-server` 并确认 `/healthz`。
|
||||
|
||||
## 5. 追加:视觉资产动作 503 降级
|
||||
|
||||
现场后续日志:
|
||||
|
||||
```text
|
||||
status=503 method=POST uri=/api/creation/square-hole/sessions/{sessionId}/actions
|
||||
```
|
||||
|
||||
该请求落在方洞 `/actions`,草稿编译成功后前端会自动追加执行 `square_hole_generate_visual_assets`。视觉资产生成依赖 VectorEngine GPT-image-2 图片入口;当本地或部署环境缺少 `VECTOR_ENGINE_API_KEY` 时,后端会在 `require_openai_image_settings()` 阶段快速返回 `503 SERVICE_UNAVAILABLE`,因此日志 latency 只有个位毫秒。
|
||||
|
||||
这类错误只表示“图片自动生成服务不可用”,不代表方洞草稿编译失败。前端处理规则:
|
||||
|
||||
1. `square_hole_compile_draft` 成功后,如果自动图片生成失败,保留错误横幅。
|
||||
2. 立即进入方洞结果页,展示已生成的形状/洞口配置。
|
||||
3. 保留封面图、背景图、形状贴图和洞口选项图上传入口。
|
||||
4. 进度页仍可通过“重新生成图片”在配置补齐后重试。
|
||||
|
||||
## 6. 追加:结果页图片重生成入口
|
||||
|
||||
方洞结果页需要保留用户上传入口,同时提供 `AI重生成图片` 操作。该按钮先保存当前编辑内容,再复用 `/api/creation/square-hole/sessions/{sessionId}/actions` 的 `square_hole_generate_visual_assets` 动作,并额外传入 `regenerateVisualAssets=true`。
|
||||
|
||||
后端收到该标记后,不再按已有 `coverImageSrc`、`backgroundImageSrc` 或形状 `imageSrc` 跳过图片生成,而是重新生成封面图、背景图和所有形状贴图。自动编译后的图片补齐流程仍保持默认 `false`,只补缺失图片,避免覆盖创作者刚上传的素材。
|
||||
|
||||
## 7. 追加:图片槽位查看模式与历史素材
|
||||
|
||||
方洞结果页的封面图、背景图、每个形状贴图和每个洞口选项图不再把上传入口直接散落在卡片上。点击任一图片槽位后打开独立查看面板,面板内负责:
|
||||
|
||||
1. 展示当前槽位图片。
|
||||
2. 展示当前账号历史生成的方洞图片素材。
|
||||
3. 提供本地上传入口,上传后只替换当前槽位。
|
||||
4. 提供 `AI生成图片` 入口,触发 `square_hole_generate_visual_assets` 并带 `regenerateVisualAssets=true`、`visualAssetSlot` 与可选 `visualAssetOptionId`。
|
||||
5. 后端按槽位定向生成:封面面板只替换封面图,背景面板只替换背景图,形状贴图面板只替换当前形状选项的贴图,洞口选项图面板只替换当前洞口选项图。草稿编译后的自动图片补齐仍不传槽位,保持“只补缺失图片”的原逻辑。
|
||||
|
||||
历史素材来源必须是真实资产索引,不允许只从前端当前草稿拼假列表。方洞图片生成成功后,API 层需要像拼图封面一样写入 OSS 与 `asset_object`:
|
||||
|
||||
1. 封面图使用 `asset_kind = square_hole_cover_image`。
|
||||
2. 背景图使用 `asset_kind = square_hole_background_image`。
|
||||
3. 形状贴图使用 `asset_kind = square_hole_shape_image`。
|
||||
4. 洞口选项图使用 `asset_kind = square_hole_hole_image`。
|
||||
5. 资产历史接口 `/api/assets/history` 与 SpacetimeDB 侧历史素材白名单同步放行这四类。
|
||||
6. 如果 OSS 或 SpacetimeDB 资产索引不可用,本次生成仍允许以 Data URL 回写作品,历史素材列表只降级为空或缺少本次记录。
|
||||
|
||||
## 8. 追加:选项图片不再局限于形状
|
||||
|
||||
方洞挑战的可展示图片不再只挂在 `shapeOptions`。创作者配置中的 `holeOptions` 也需要具备:
|
||||
|
||||
1. `imagePrompt`:洞口选项图的生成提示词,默认由题材主题和洞口标签补齐。
|
||||
2. `imageSrc`:洞口选项图地址,可来自 AI 生成、历史素材套用或本地上传 Data URL。
|
||||
3. 结果页需要把洞口选项图做成与形状贴图相同的图片槽位,打开查看面板后支持历史、上传和 AI 生成。
|
||||
4. 运行态洞口按钮优先展示 `hole.imageSrc`,没有图片时再回落到几何剪影。
|
||||
5. `visualAssetSlot = hole` 时必须携带 `visualAssetOptionId = holeId`,后端只重生成该洞口选项图;未传槽位的自动补齐需要同时补缺失的形状贴图和洞口选项图。
|
||||
@@ -0,0 +1,49 @@
|
||||
# 方洞挑战拖拽玩法重构 2026-05-05
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把方洞挑战从“按洞口按钮点选”改成“拖拽形状到目标洞口”的玩法,同时把洞口选项与形状选项的编辑关系改成稳定 ID + 名称联动。
|
||||
|
||||
## 2. 编辑态规则
|
||||
|
||||
1. 洞口选项不再暴露 `square / circle / triangle` 之类的下拉框。
|
||||
2. 每个洞口选项只保留稳定 `holeId`、可编辑名称 `label`,以及可选图片字段。
|
||||
3. 洞口名称改动后,所有引用该洞口的形状选项下拉框必须同步显示新名称。
|
||||
4. 形状选项不再直接绑定洞口“形状类型”,而是绑定一个目标洞口 `targetHoleId`。
|
||||
5. 形状选项下拉框展示的是洞口名称,底层值是 `targetHoleId`。
|
||||
6. 形状选项仍保留自己的视觉配置字段,如 `shapeKind`、`imagePrompt`、`imageSrc`。
|
||||
7. 去掉加分选项,所有洞口选项一律平权。
|
||||
|
||||
## 3. 运行态规则
|
||||
|
||||
1. 运行态上半区展示全部洞口,布局成一块平面。
|
||||
2. 下半区展示当前轮次的形状选项。
|
||||
3. 每轮随机选择一个形状选项作为当前轮显示项,并以它绑定的 `targetHoleId` 作为目标洞口。
|
||||
4. 箭头动画只负责给出随机引导洞口,不绑定正确答案;多洞口时优先避开当前正确洞口,避免直接剧透。
|
||||
5. 用户把当前形状拖到任意洞口上松开后,前端把该洞口的 `holeId` 提交给后端。
|
||||
6. 后端以 `holeId === currentShape.targetHoleId` 作为唯一判定标准。
|
||||
7. 命中后进入下一轮;未命中时后端返回错误反馈、清空连击并保留当前运行态。
|
||||
8. 计分不再包含任何加分洞口分支。
|
||||
9. 游戏模式下洞口和当前选项只展示图片卡片,不再按 `shapeKind` / `holeKind` 裁剪成圆形、三角形、星形等几何形状。
|
||||
|
||||
## 4. 图片槽位
|
||||
|
||||
1. 封面图、背景图、形状图、洞口图都使用独立图片槽位。
|
||||
2. 洞口图对应新的历史素材类型 `square_hole_hole_image`。
|
||||
3. 图片槽位查看面板内保留历史图片、上传入口和 AI 生成入口。
|
||||
|
||||
## 5. 后端契约要求
|
||||
|
||||
1. 共享契约里补充形状到洞口的目标引用字段。
|
||||
2. 共享契约里补充洞口图片字段。
|
||||
3. 运行态快照里必须包含当前形状对应的目标洞口信息,便于前端画箭头。
|
||||
4. 作品和草稿返回的洞口数据必须和运行态保持同一组 ID 与名称。
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. 编辑洞口名称后,形状下拉框立即显示新名称。
|
||||
2. 洞口编辑不再出现 `square / circle / triangle` 下拉框。
|
||||
3. 运行态不再是按钮点选洞口,而是拖拽命中。
|
||||
4. 每轮都能看到箭头或高亮引导洞口,但该引导不等同于正确答案。
|
||||
5. 计分中不再出现 `bonus` 相关加分。
|
||||
6. 游戏模式中洞口、当前选项和拖拽影子不再显示几何形状,只显示对应图片或中性图片占位。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 方洞挑战图片槽位与运行态交互修正
|
||||
|
||||
## 背景
|
||||
|
||||
方洞挑战结果页把封面图、背景图、形状图和洞口图统一放进查看模式后,图片面板里的“AI生成图片”仍复用了 Agent action:`square_hole_generate_visual_assets`。这条链路会进入生成进度页,并按草稿视觉资产流程更新会话,用户点击任意图片槽位时看起来像触发了整份草稿重编译。
|
||||
|
||||
同时,洞口图已经有独立资产类型 `square_hole_hole_image`,但 HTTP 历史素材白名单未放通,导致洞口图片面板无法读取历史图片。运行态方面,预期是拖拽当前选项到洞口松开,移动端同时支持点击当前选项后点洞口;现有洞口按钮没有点击提交,拖拽也缺少稳定的释放兜底。
|
||||
|
||||
## 修正决策
|
||||
|
||||
1. 结果页图片查看模式里的“AI生成图片”只允许生成当前图片槽位,不切换到草稿生成进度页。
|
||||
2. 新增作品级槽位重生成接口,前端传 `slot.kind` 和对应 option id,后端只更新当前作品的目标图片字段。
|
||||
3. `square_hole_hole_image` 必须加入资产历史 HTTP 白名单,与 SpacetimeDB 侧历史素材白名单保持一致。
|
||||
4. 洞口图片查看模式必须和封面、背景、形状一样支持历史图片、上传图片和 AI 生成图片。
|
||||
5. 运行态交互统一为:
|
||||
- 拖动下方当前选项到上方任一洞口松开,提交该洞口 `holeId`。
|
||||
- 点击下方当前选项进入待投放状态,再点击任一洞口,也提交该洞口 `holeId`。
|
||||
- 直接点击洞口时,如果当前局正在运行且没有待处理操作,也提交该洞口 `holeId`。
|
||||
6. 箭头和高亮只表示随机引导洞口,不表示正确答案;多洞口时优先避开当前正确洞口。
|
||||
7. 游戏模式只展示图片卡片,不再按形状字段裁剪出几何图形;没有图片时使用中性图片占位。
|
||||
8. `difficulty` 当前不参与运行态裁决、计时、计分或队列生成,不应作为结果页显性玩法调参继续误导创作者;后端字段保留兼容,前端不再突出展示和编辑。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 在结果页点击任一图片槽位,只打开图片查看面板,不触发生成进度页。
|
||||
2. 图片面板点击“AI生成图片”后,只刷新当前槽位图片,结果页仍停留在当前面板。
|
||||
3. 洞口图片面板可以读取 `square_hole_hole_image` 历史图片。
|
||||
4. 运行态拖拽当前选项到洞口松开会调用 drop API。
|
||||
5. 运行态点击当前选项后点击洞口、或直接点击洞口,都会调用 drop API。
|
||||
6. 运行态洞口、当前选项和拖拽影子只显示图片或图片占位,不显示圆形、三角形、星形等几何形状。
|
||||
7. 运行态箭头和高亮不会默认指向当前正确洞口。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
更新时间:`2026-04-20`
|
||||
|
||||
> 2026-05-05 更新口径:本文保留为历史迁移参考。视觉小说模板后续落地以 [`../prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`](../prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md) 为准;冲突时不再迁入外部平台工程、不使用 `server-node` 作为新功能落点、不保留回放,统一接入 Genarrative `server-rs + Axum + SpacetimeDB` 与平台接口。
|
||||
|
||||
## 0. 文档目的
|
||||
|
||||
这份执行方案用于指导 `Genarrative` 在**不改动外部 TXT 模式提示词正文、不改动外部 TXT 模式功能需求**的前提下,把下面两个仓库中已经跑通的 TXT 模式创作流程与运行机制完整迁入当前项目:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# 用户标签、邀请码授予与拼图榜单展示方案
|
||||
|
||||
更新时间:`2026-05-11`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本次新增用户标签系统的最小闭环:
|
||||
|
||||
1. `user_account` 增加账号标签字段,数据库默认空,业务读取时按空数组处理。
|
||||
2. 后台预置邀请码可通过原有 `metadata` 字段配置授予标签。
|
||||
3. 用户填写带标签的邀请码后,把标签合并到自己的账号。
|
||||
4. 标签默认不在前端资料页、邀请中心或通用接口展示。
|
||||
5. 拼图排行榜仅对白名单标签做展示,首版只展示 `北科`。
|
||||
|
||||
## 2. 数据字段
|
||||
|
||||
### `user_account.user_tags`
|
||||
|
||||
- 类型:`Option<Vec<String>>`。
|
||||
- 默认:`None`,业务层读取时统一按空数组处理。
|
||||
- 语义:账号级运营标签,属于后台与服务端投影数据,不作为普通前端个人资料字段。
|
||||
- 写入:首版只由邀请码兑换链路合并写入。
|
||||
- 迁移:旧迁移包和旧数据库按 `null` 兼容,再由业务层归一化为空数组。
|
||||
|
||||
### `profile_invite_code.metadata_json.userTags`
|
||||
|
||||
- 类型:`metadata_json` 对象里的 `userTags: string[]`。
|
||||
- 默认:字段缺失或空数组时不授予标签。
|
||||
- 语义:使用该邀请码后授予被邀请账号的标签列表。
|
||||
- 范围:后台运营预置码和普通用户个人邀请码都可存字段,但后台表单首版只允许管理员配置预置码。
|
||||
- 存储:不再新增或使用独立的邀请码标签列;后台保存时把用户标签写回 `metadata.userTags`。
|
||||
- 解析:服务端优先读取 `metadata_json.userTags`,并兼容解析 `metadata_json.user_tags`。
|
||||
- 迁移:旧邀请码缺少 `metadata_json` 时按 `{}` 兼容;旧迁移包里已废弃的独立字段会在导入时丢弃。
|
||||
|
||||
## 3. 标签归一化
|
||||
|
||||
标签由 `module-runtime` 提供统一归一化:
|
||||
|
||||
1. trim 后为空的标签丢弃。
|
||||
2. 同一邀请码或同一账号内标签去重,保留首次出现顺序。
|
||||
3. 单个标签最长 `16` 字符,单次最多 `8` 个标签。
|
||||
4. 当前不做中英文互译,不把中文标签改写为英文。
|
||||
|
||||
## 4. 邀请码授予流程
|
||||
|
||||
用户填写邀请码时,后端按原有校验顺序完成身份、绑定状态、邀请码存在、自邀请与时间窗校验。校验通过后:
|
||||
|
||||
1. 写入 `profile_referral_relation`。
|
||||
2. 发放原有双方奖励。
|
||||
3. 从 `profile_invite_code.metadata_json` 解析 `userTags` / `user_tags`。
|
||||
4. 将这些标签合并进 `user_account.user_tags`。
|
||||
|
||||
管理员更新邀请码时,后台表单里的用户标签会覆盖写入 `metadata.userTags`;空数组代表不授予标签。更新邀请码不会回溯修改已经使用过该邀请码的账号。
|
||||
|
||||
## 5. API 契约
|
||||
|
||||
后台邀请码 upsert 请求继续只提交 `metadata`,标签写在 `metadata.userTags` 中:
|
||||
|
||||
```json
|
||||
{
|
||||
"inviteCode": "BEIKE2026",
|
||||
"metadata": {
|
||||
"userTags": ["北科"]
|
||||
},
|
||||
"startsAt": null,
|
||||
"expiresAt": null
|
||||
}
|
||||
```
|
||||
|
||||
后台邀请码列表和 upsert 返回继续回传 `metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"inviteCode": "BEIKE2026",
|
||||
"metadata": {
|
||||
"userTags": ["北科"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后台表单展示时从 `metadata.userTags` 回显用户标签;用户侧邀请中心、账号资料、登录返回和普通 profile 接口不返回 `userTags`。
|
||||
|
||||
## 6. 拼图排行榜展示
|
||||
|
||||
拼图排行榜返回项增加 `visibleTags: string[]`。
|
||||
|
||||
服务端构造排行榜时,从 `user_account.user_tags` 中仅投影允许公开展示的标签。首版公开展示白名单:
|
||||
|
||||
```text
|
||||
北科
|
||||
```
|
||||
|
||||
前端拼图通关弹窗按如下规则展示:
|
||||
|
||||
1. 昵称保持第一行。
|
||||
2. `visibleTags` 非空时在昵称下方显示小标签。
|
||||
3. 没有 `visibleTags` 时不占额外文案。
|
||||
4. 本地兜底 run 的榜单 `visibleTags` 始终为空。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 新账号 `user_account.user_tags` 数据库默认为 `None`,业务读取为空数组。
|
||||
2. 后台创建邀请码时可填写 `北科`,请求和返回的 `metadata.userTags` 可回显。
|
||||
3. 用户填写该邀请码后,账号表 `user_tags` 包含 `北科`。
|
||||
4. 不带标签的邀请码不改变账号标签。
|
||||
5. 拼图排行榜中带 `北科` 的用户昵称下方显示 `北科`,其它标签不显示。
|
||||
6. 生成 SpacetimeDB bindings 后,不再出现独立的邀请码标签字段。
|
||||
7. 执行 `npm run check:encoding`、`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`,并按影响范围执行后台 typecheck / 拼图前端测试。
|
||||
@@ -0,0 +1,156 @@
|
||||
# VectorEngine 音频生成接入方案 2026-05-08
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于把 VectorEngine / Apifox 文档中的 Suno 文生背景音乐与 Vidu 文生音效接入视觉小说结果页。
|
||||
|
||||
本次只接入 `visual-novel` 现有场景资产槽位,不新增独立音频系统、不新增 SpacetimeDB 表、不把供应商密钥下发到前端。
|
||||
|
||||
## 2. 参考接口
|
||||
|
||||
### 2.1 Suno 文生背景音乐
|
||||
|
||||
参考文档:
|
||||
|
||||
- `https://vectorengine.apifox.cn/api-349239190`
|
||||
- `https://vectorengine.apifox.cn/api-349239199`
|
||||
|
||||
接口:
|
||||
|
||||
```text
|
||||
POST /suno/submit/music
|
||||
GET /suno/fetch/{task_id}
|
||||
```
|
||||
|
||||
提交请求头:
|
||||
|
||||
```text
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
```
|
||||
|
||||
自定义模式请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "音乐描述或歌词",
|
||||
"mv": "chirp-v4",
|
||||
"title": "曲名",
|
||||
"tags": "风格标签",
|
||||
"continue_at": 120,
|
||||
"continue_clip_id": "",
|
||||
"task": ""
|
||||
}
|
||||
```
|
||||
|
||||
首版只使用 `prompt`、`mv`、`title`、`tags`。返回体按 `code = success` 且 `data` 为供应商任务 ID 处理。
|
||||
|
||||
### 2.2 Vidu 文生音效
|
||||
|
||||
参考文档:
|
||||
|
||||
- `https://vectorengine.apifox.cn/api-417728889`
|
||||
- `https://vectorengine.apifox.cn/api-417728893`
|
||||
|
||||
接口:
|
||||
|
||||
```text
|
||||
POST /ent/v2/text2audio
|
||||
GET /ent/v2/tasks/{id}/creations
|
||||
```
|
||||
|
||||
提交请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "audio1.0",
|
||||
"prompt": "雨滴落在窗户上的声音,伴随着轻柔的雷声",
|
||||
"duration": 5,
|
||||
"seed": 0
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `prompt` 最长 1500 字符。
|
||||
- `duration` 范围为 2 到 10 秒,默认 5 秒。
|
||||
- `model` 首版固定为 `audio1.0`。
|
||||
|
||||
## 3. 环境变量
|
||||
|
||||
```text
|
||||
VECTOR_ENGINE_BASE_URL=
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. `VECTOR_ENGINE_BASE_URL` 只保存供应商代理 API 基础地址,不在代码中写死私有网关。
|
||||
2. `VECTOR_ENGINE_API_KEY` 只能进入本地或生产私密环境文件,不提交到 Git。
|
||||
3. 缺少任一必配项时,后端返回 `503 SERVICE_UNAVAILABLE`,前端沿用现有错误展示。
|
||||
|
||||
## 4. 后端路由
|
||||
|
||||
视觉小说创作链新增 4 个鉴权路由:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/creation/visual-novel/audio/background-music` | 提交 Suno 背景音乐任务 |
|
||||
| `POST` | `/api/creation/visual-novel/audio/background-music/{task_id}/asset` | 查询 Suno 任务,完成后下载并写入平台资产 |
|
||||
| `POST` | `/api/creation/visual-novel/audio/sound-effect` | 提交 Vidu 音效任务 |
|
||||
| `POST` | `/api/creation/visual-novel/audio/sound-effect/{task_id}/asset` | 查询 Vidu 任务,完成后下载并写入平台资产 |
|
||||
|
||||
生成资产回包写入既有视觉小说字段:
|
||||
|
||||
- Suno 背景音乐:`VisualNovelSceneDraft.musicSrc`
|
||||
- Vidu 文生音效:`VisualNovelSceneDraft.ambientSoundSrc`
|
||||
|
||||
## 5. 资产落点
|
||||
|
||||
音频文件由后端下载后通过 `OssClient::put_object` 写入平台 OSS,并确认 `asset_object` 与 `asset_entity_binding`。
|
||||
|
||||
对象规划:
|
||||
|
||||
| 类型 | `assetKind` | `entityKind` | `slot` | 旧路径前缀 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 背景音乐 | `visual_novel_music` | `visual_novel_scene` | `music` | `generated-custom-world-scenes` |
|
||||
| 场景音效 | `visual_novel_ambient_sound` | `visual_novel_scene` | `ambient_sound` | `generated-custom-world-scenes` |
|
||||
|
||||
确认后的 `audioSrc` 使用 OSS 返回的 legacy public path,继续由前端 `resolveAssetReadUrl` 换签播放。
|
||||
|
||||
## 6. 前端交互
|
||||
|
||||
视觉小说场景编辑弹层新增两类音频能力:
|
||||
|
||||
1. `音乐` 保留上传能力,并新增 Suno 生成按钮。
|
||||
2. `音效` 使用 `ambientSoundSrc`,支持上传和 Vidu 生成。
|
||||
|
||||
交互要求:
|
||||
|
||||
1. 生成参数放在独立弹层中,不在当前场景面板下方展开。
|
||||
2. 弹层只保留必要字段、提交、关闭和状态反馈,不展示供应商规则说明。
|
||||
3. 任务提交成功后前端轮询资产接口;若供应商仍在处理,保持弹层状态。
|
||||
4. 资产生成完成后自动写回当前场景字段。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts visual_novel
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
涉及真实 API smoke 时:
|
||||
|
||||
1. 只在本地私密环境设置 `VECTOR_ENGINE_API_KEY`。
|
||||
2. 使用 `npm run api-server` 重启后端。
|
||||
3. 确认 `/healthz`。
|
||||
4. 在视觉小说结果页提交背景音乐或音效生成,生成完成后确认场景音频槽位可播放。
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# VectorEngine GPT-image-2 图片生成迁移 2026-05-09
|
||||
|
||||
## 背景
|
||||
|
||||
GPT-image-2 图片生成此前通过 APIMart OpenAI 兼容入口执行。为统一供应商网关,本次参考 VectorEngine Apifox 文档 `https://vectorengine.apifox.cn/api-448710071`,把仓库内所有 GPT-image-2 生图调用迁移到 VectorEngine,不再使用 APIMart 图片网关。
|
||||
|
||||
APIMart 仍只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路;不要把该文本链路与 GPT-image-2 图片生成配置混用。
|
||||
|
||||
## 参考接口
|
||||
|
||||
VectorEngine 正式环境基础地址来自 Apifox 项目环境:
|
||||
|
||||
```text
|
||||
https://api.vectorengine.ai
|
||||
```
|
||||
|
||||
GPT-image-2-all 生图接口:
|
||||
|
||||
```text
|
||||
POST /v1/images/generations
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-image-2-all",
|
||||
"size": "1024x1024",
|
||||
"n": 1,
|
||||
"prompt": "生成一只猫"
|
||||
}
|
||||
```
|
||||
|
||||
参考图场景可按文档字段追加:
|
||||
|
||||
```json
|
||||
{
|
||||
"image": ["data:image/png;base64,..."]
|
||||
}
|
||||
```
|
||||
|
||||
响应体按同步 OpenAI Images 结构读取:
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1776909189,
|
||||
"data": [
|
||||
{
|
||||
"revised_prompt": "",
|
||||
"url": "https://pro.filesystem.site/cdn/20260423/example.webp"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 尺寸映射
|
||||
|
||||
VectorEngine 文档要求使用像素尺寸,不再使用 APIMart 的比例写法:
|
||||
|
||||
| 旧输入 | VectorEngine 请求值 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `1:1`、`1024*1024`、`1024x1024` | `1024x1024` | 拼图、方洞局部贴图、角色主形象 |
|
||||
| `16:9`、`1280*720`、`1600*900`、`1536x1024` | `1536x1024` | 场景图、封面图、方洞横版背景 |
|
||||
| `1024x1536` | `1024x1536` | 竖版图 |
|
||||
| `2048x1152` | `1536x1024` | 开局 CG 故事板首版降为文档明确支持的横版尺寸 |
|
||||
|
||||
若调用方传入其它非空尺寸,后端先透传,方便后续跟随 VectorEngine 文档扩展;空值统一回落到 `1024x1024`。
|
||||
|
||||
## 后端落点
|
||||
|
||||
1. `server-rs/crates/api-server/src/openai_image_generation.rs`
|
||||
- 保留当前共享 helper 文件名与函数名,减少 RPG、方洞等调用方改动面。
|
||||
- 内部配置改读 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`。
|
||||
- 请求模型固定为 `gpt-image-2-all`,不再写 `official_fallback`。
|
||||
- 请求路径改为 `/v1/images/generations`,响应直接解析 `data[].url` / `data[].b64_json`,不再轮询 `/tasks/{task_id}`。
|
||||
2. `server-rs/crates/api-server/src/puzzle.rs`
|
||||
- 拼图默认 `gpt-image-2` 前端值继续兼容,但上游请求模型统一映射到 `gpt-image-2-all`。
|
||||
- `nanobanana2` / `gemini-3.1-flash-image-preview` 不再走 APIMart;当前阶段统一回落到 VectorEngine GPT-image-2-all,避免保留旧图片网关。
|
||||
- 错误 `details.provider` 改为 `vector-engine`。
|
||||
- 入口页上传图若用于首图生成,后端必须在生成成功后同步写入首关 `pictureReference`,保证结果页重新生成默认继续带同一张参考图。
|
||||
3. `.codex/skills/gpt-image-2-apimart/`
|
||||
- 目录名暂不强制迁移,避免本地插件索引漂移;Skill 文案与脚本行为改为 VectorEngine。
|
||||
|
||||
## 环境变量
|
||||
|
||||
```text
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. GPT-image-2 图片生成不读取 `APIMART_BASE_URL`、`APIMART_API_KEY` 或 `APIMART_IMAGE_REQUEST_TIMEOUT_MS`。
|
||||
2. `VECTOR_ENGINE_BASE_URL` 仍允许部署环境覆盖,不在代码中绑定私有网关。
|
||||
3. `VECTOR_ENGINE_API_KEY` 只能进入本地或生产私密环境文件,不提交到 Git。
|
||||
|
||||
## 非范围
|
||||
|
||||
1. 不迁移创意 Agent 的 APIMart `gpt-5` Responses 链路。
|
||||
2. 不改变 SpacetimeDB 表结构、migration 或 bindings。
|
||||
3. 不改前端 UI 文案和模型选择控件展示。
|
||||
4. 不新增新的图片资产表或图片代理路由。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 所有 GPT-image-2 生图请求都走 `POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations`。
|
||||
2. 请求体 `model = gpt-image-2-all`,尺寸为 VectorEngine 支持的像素尺寸。
|
||||
3. 请求体不再包含 `official_fallback`。
|
||||
4. 无参考图时使用 `POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations`;有参考图且处于 AI 重绘时改走 `POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits`;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成。
|
||||
5. 拼图入口页上传图生成首图后,返回的首关 `pictureReference` 保留该 Data URL;结果页重新生成在用户未重新上传参考图时会继续把 `pictureReference` 作为 `referenceImageSrc` 传给后端。
|
||||
6. 拼图有参考图时,后端 prompt 会显式要求以参考图为第一优先级,保留主体、构图、视角、姿态、配色和光影氛围;入口页会把参考图压到单边 1024 以内,后端拒绝超过 8MB 的参考图字节。
|
||||
7. 缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时返回 `503 SERVICE_UNAVAILABLE`,`details.provider = "vector-engine"`。
|
||||
8. 上游错误映射为 `502 UPSTREAM_ERROR`,保留 `upstreamStatus`、业务 message 和截断后的 raw excerpt。
|
||||
9. 运行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`。
|
||||
10. 后端改动后使用 `npm run api-server` 重启,并确认 `/healthz`。
|
||||
|
||||
## 拼图链路排障日志
|
||||
|
||||
拼图 `compile_puzzle_draft` 与 `generate_puzzle_images` 进入图片生成时,api-server 会输出分阶段耗时日志。排查“是谁慢”时按同一 `session_id` 串联:
|
||||
|
||||
1. `拼图参考图解析完成` / `拼图参考图解析跳过`:确认前端是否传入参考图,以及 Data URL 解析或旧 `/generated-*` OSS 读取耗时;日志只记录 `reference_mime` 与 `reference_bytes`,不记录图片内容。
|
||||
2. `拼图 VectorEngine 图片生成 HTTP 返回`:VectorEngine `POST /v1/images/generations` 的同步上游耗时,若这一段长,慢点在 VectorEngine 生图接口。
|
||||
3. `拼图 VectorEngine 图片下载完成`:从 VectorEngine 返回的 `data[].url` 下载正式图耗时。
|
||||
4. `拼图生成图片已写入 OSS 与资产索引`:正式图上传 OSS、确认资产对象与实体绑定耗时。
|
||||
5. `拼图图片候选生成完成`:整段候选图生成总耗时。
|
||||
@@ -0,0 +1,76 @@
|
||||
# 视觉小说模板实现收口与交接说明 2026-05-07
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本文记录 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md` 的 `VN-13` 收口结果,作为视觉小说模板后续维护的一页式入口。
|
||||
|
||||
本文只总结当前已经落地且需要长期遵守的实现边界,不再把视觉小说描述成外部 TXT 平台迁移,也不重复旧迁移方案里的临时讨论。
|
||||
|
||||
## 2. 当前正式入口
|
||||
|
||||
后续维护时优先看这些文档:
|
||||
|
||||
1. [AI 原生视觉小说模板 PRD](../prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md)
|
||||
2. [SpacetimeDB 表说明与查询目录](./SPACETIMEDB_TABLE_CATALOG.md)
|
||||
3. [视觉小说 VN-03 Prompt 与 LLM 工具实现说明](./VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md)
|
||||
4. [视觉小说 VN-11 负向扫描报告](../audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md)
|
||||
5. [视觉小说模板交接与维护经验](../experience/VISUAL_NOVEL_HANDOFF_AND_MAINTENANCE_2026-05-07.md)
|
||||
|
||||
旧的 `TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md` 只保留历史参考意义,不再作为实现口径。
|
||||
|
||||
## 3. 已收口的实现边界
|
||||
|
||||
### 3.1 创作链路
|
||||
|
||||
- 创作链统一走 `/api/creation/visual-novel/*`。
|
||||
- 入口、工作台、结果页和服务端 prompt 已围绕 `visual-novel` 模板闭环对齐。
|
||||
- `VisualNovelResultDraft`、`VisualNovelCreationSessionSnapshot` 等共享契约已经成型。
|
||||
|
||||
### 3.2 运行链路
|
||||
|
||||
- 运行链统一走 `/api/runtime/visual-novel/*`。
|
||||
- 运行时只认 typed step 和快照,不让前端从 `raw_text` 猜业务真相。
|
||||
- `visual_novel_runtime_history_entry` 只用于继续体验和历史重生成边界,不是回放表。
|
||||
|
||||
### 3.3 数据链路
|
||||
|
||||
- SpacetimeDB 首版只保留 6 张视觉小说表。
|
||||
- `visual_novel_runtime_event` 是 `public event` 审计事件表,不是 replay 数据源。
|
||||
- 表结构变化必须同步 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 Rust bindings。
|
||||
|
||||
### 3.4 作品与发布
|
||||
|
||||
- 作品架、广场和分享都复用平台现有链路。
|
||||
- 公开作品码统一使用 `VN-` 前缀。
|
||||
- 发布后要刷新作品架和公开聚合。
|
||||
|
||||
### 3.5 资产与登录态
|
||||
|
||||
- 文档、封面、背景、角色和音乐都只保留平台资产对象引用。
|
||||
- 不保存大 Data URL、不走独立对象存储、不新增视觉小说私有存档系统。
|
||||
- 退出登录时要清空视觉小说私有 session、work、run 和错误状态。
|
||||
|
||||
## 4. 长期维护规则
|
||||
|
||||
1. 看到视觉小说相关改动,先确认是不是契约改动;如果是,先同步 TS / Rust shared contracts。
|
||||
2. 看到表结构改动,先同步 `migration.rs` 和表目录,再补 bindings。
|
||||
3. 看到资产链路改动,只改平台资产引用,不回退到本地路径或二进制直存。
|
||||
4. 看到运行时历史改动,只维护 typed history 和审计事件,不把它改成回放能力。
|
||||
5. 看到旧 TXT 文档时,只把它当历史来源,不把其中的平台工程目标重新带回实现。
|
||||
|
||||
## 5. 验证口径
|
||||
|
||||
收口后建议按下面顺序检查:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run check:visual-novel-vn11
|
||||
npm run typecheck
|
||||
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts
|
||||
cargo test -p module-visual-novel
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
如果本轮没有改代码,只要编码检查和负向扫描通过,通常就说明 VN-13 的文档收口已经站稳。
|
||||
@@ -0,0 +1,111 @@
|
||||
# 视觉小说 VN-03 Prompt 与 LLM 工具实现说明
|
||||
|
||||
日期:`2026-05-05`
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本文记录 `AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md` 中 `VN-03` 的工程落地口径。
|
||||
|
||||
本次只实现视觉小说模板的 Prompt / LLM 工具层:
|
||||
|
||||
1. 创作底稿生成 Prompt,目标输出 `VisualNovelResultDraft`。
|
||||
2. 运行时 GM Prompt,目标输出 `VisualNovelRuntimeStep[]`。
|
||||
3. 结构化 repair Prompt,用于解析失败后的后端修复。
|
||||
4. 创作 action 与图片生成 action 的工具参数 schema。
|
||||
5. `platform-llm` Responses 请求构造口径。
|
||||
|
||||
本次不保存数据、不新增 HTTP 路由、不写前端 UI、不触碰 SpacetimeDB schema。
|
||||
|
||||
## 2. 代码落点
|
||||
|
||||
主要实现位于:
|
||||
|
||||
1. `server-rs/crates/api-server/src/prompt/visual_novel.rs`
|
||||
2. `server-rs/crates/api-server/src/prompt/mod.rs`
|
||||
|
||||
为了让当前工作树中已有的 creative-agent / puzzle 并行改动可继续编译,本次还补了两个编译闭口:
|
||||
|
||||
1. `server-rs/crates/module-puzzle/Cargo.toml` 增加 `serde_json = "1"`。
|
||||
2. `server-rs/crates/platform-agent/src/puzzle_phase1_agent.rs` 给 `MockLangChainRustAgentExecutor` 补 `Debug` 派生。
|
||||
|
||||
## 3. Prompt 约束
|
||||
|
||||
### 3.1 创作底稿
|
||||
|
||||
创作系统 Prompt 明确要求:
|
||||
|
||||
1. 只输出一个 JSON 对象。
|
||||
2. 内容必须是中文视觉小说底稿。
|
||||
3. 补齐世界观、玩家身份、角色、场景、剧情阶段和开场。
|
||||
4. 角色必须有可生成立绘的 `appearance`。
|
||||
5. 场景必须有可生成背景图的 `description`。
|
||||
6. 不输出回放、商城、会员、后台、平台活动、促销、订单或独立账号字段。
|
||||
7. 不生成第二套存档、发布、钱包、广场或资产系统。
|
||||
|
||||
### 3.2 运行时 GM
|
||||
|
||||
运行时系统 Prompt 明确要求:
|
||||
|
||||
1. 只输出 `VisualNovelRuntimeStep[]` JSON 数组。
|
||||
2. step 数量不超过输入的 `maxAssistantStepCountPerTurn`。
|
||||
3. 场景变化必须输出 `scene_change`。
|
||||
4. 旁白、对白、转场、选项、flag、metric 分别使用契约内 step 类型。
|
||||
5. 前端不得从 `raw_text` 猜业务 step。
|
||||
6. 不输出回放、录制、商城、会员、后台、平台活动或独立存档元数据。
|
||||
|
||||
### 3.3 Repair
|
||||
|
||||
repair Prompt 只负责把坏格式修成目标 JSON:
|
||||
|
||||
1. `VisualNovelResultDraft`
|
||||
2. `VisualNovelRuntimeStep[]`
|
||||
|
||||
repair 仍失败时,由后续 `VN-05` API 接入层返回可重试错误。
|
||||
|
||||
## 4. LLM 请求口径
|
||||
|
||||
视觉小说创作、运行和 repair 请求统一使用 `platform-llm`:
|
||||
|
||||
1. model:`CREATION_TEMPLATE_LLM_MODEL`,当前为 `deepseek-v3-2-251201`。
|
||||
2. protocol:`LlmTextProtocol::Responses`。
|
||||
3. 创作底稿请求可按 API 配置开启 web search。
|
||||
4. 运行时 GM 与 repair 默认不开 web search,避免运行态引入外部噪声。
|
||||
|
||||
## 5. 工具参数
|
||||
|
||||
本次定义两个工具描述:
|
||||
|
||||
1. `visual_novel_apply_creation_action`
|
||||
- 支持 `generate_draft`、`patch_world`、`patch_character`、`patch_scene`、`patch_story_phase`、`compile_work_profile`。
|
||||
- 只写回视觉小说底稿或编译平台 work profile 草稿。
|
||||
2. `visual_novel_generate_image_asset`
|
||||
- 支持 `generate_scene_image`、`generate_character_image`。
|
||||
- 输出应接后续平台资产引用,不保存二进制或大 Data URL。
|
||||
|
||||
工具 schema 不包含 replay / Replay 字段。
|
||||
|
||||
## 6. 验证结果
|
||||
|
||||
已执行:
|
||||
|
||||
```bash
|
||||
cargo test -p shared-contracts visual_novel --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p module-puzzle creative_tools --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p platform-agent puzzle_phase1_agent --manifest-path server-rs/Cargo.toml
|
||||
cargo test -p api-server prompt::visual_novel --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
结果:全部通过。
|
||||
|
||||
## 7. 后续接入点
|
||||
|
||||
`VN-05` 接 API Server 时直接复用:
|
||||
|
||||
1. `build_visual_novel_creation_llm_request`
|
||||
2. `build_visual_novel_runtime_llm_request`
|
||||
3. `build_visual_novel_repair_llm_request`
|
||||
4. `parse_visual_novel_result_draft_fixture`
|
||||
5. `parse_visual_novel_runtime_steps_fixture`
|
||||
6. `visual_novel_tool_descriptors`
|
||||
|
||||
若后续 `VN-01` 契约发生破坏性变更,必须同步更新本 Prompt 模块的 output contract、fixture 和解析测试。
|
||||
@@ -0,0 +1,225 @@
|
||||
# 火山引擎大模型语音流式接入 2026-05-08
|
||||
|
||||
## 背景
|
||||
|
||||
本次接入火山引擎豆包语音能力,覆盖两类运行态语音链路:
|
||||
|
||||
1. 大模型流式语音识别 ASR,使用 WebSocket 双向流式优化模式。
|
||||
2. 大模型语音合成 TTS,使用实时交互场景的 WebSocket 双向流式接口,并提供一次性文本输入的 HTTP SSE 单向流式接口。
|
||||
|
||||
语音能力属于外部副作用,按 server-rs DDD 分层落在 `platform-speech`;`api-server` 只负责平台账号鉴权、环境配置校验、协议代理和错误映射。前端不得直接持有火山引擎密钥。
|
||||
|
||||
## 官方文档依据
|
||||
|
||||
- ASR:`https://www.volcengine.com/docs/6561/1354869?lang=zh`
|
||||
- TTS WebSocket 双向流式:`https://www.volcengine.com/docs/6561/1329505?lang=zh`
|
||||
- TTS WebSocket 单向流式:`https://www.volcengine.com/docs/6561/1719100?lang=zh`
|
||||
- TTS HTTP Chunked / SSE 单向流式:`https://www.volcengine.com/docs/6561/1598757?lang=zh`
|
||||
|
||||
## 环境变量
|
||||
|
||||
真实值只能放在本地未提交的 `.env.local` / `.env.secrets.local` 或生产服务器环境文件,禁止提交到仓库。
|
||||
|
||||
```text
|
||||
VOLCENGINE_SPEECH_API_KEY=
|
||||
VOLCENGINE_SPEECH_APP_ID=
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY=
|
||||
VOLCENGINE_SPEECH_ASR_RESOURCE_ID=volc.seedasr.sauc.concurrent
|
||||
VOLCENGINE_SPEECH_TTS_RESOURCE_ID=seed-tts-2.0
|
||||
VOLCENGINE_SPEECH_REQUEST_TIMEOUT_MS=180000
|
||||
VOLCENGINE_SPEECH_ASR_WS_URL=wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
|
||||
VOLCENGINE_SPEECH_TTS_BIDIRECTION_WS_URL=wss://openspeech.bytedance.com/api/v3/tts/bidirection
|
||||
VOLCENGINE_SPEECH_TTS_SSE_URL=https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse
|
||||
```
|
||||
|
||||
配置规则:
|
||||
|
||||
1. 优先使用新版控制台 `VOLCENGINE_SPEECH_API_KEY`,上游请求头写 `X-Api-Key`。
|
||||
2. 若只配置旧版控制台信息,则使用 `VOLCENGINE_SPEECH_APP_ID` 和 `VOLCENGINE_SPEECH_ACCESS_KEY`,上游请求头写 `X-Api-App-Key` 与 `X-Api-Access-Key`。
|
||||
3. ASR 默认资源 ID 选 ASR 2.0 并发版;如账号是小时版,部署时改成 `volc.seedasr.sauc.duration`。
|
||||
4. TTS 默认资源 ID 选 `seed-tts-2.0`;旧音色或 1.0 计费资源由部署环境覆盖。
|
||||
|
||||
## ASR 协议边界
|
||||
|
||||
客户端连接:
|
||||
|
||||
```text
|
||||
GET /api/speech/volcengine/asr/stream
|
||||
Authorization: Bearer <Genarrative JWT>
|
||||
```
|
||||
|
||||
浏览器与 `api-server` 使用 WebSocket 二进制帧透传:
|
||||
|
||||
1. 首包必须是 JSON 文本,表示 ASR full client request 的业务参数。
|
||||
2. 后续二进制帧是音频分片。
|
||||
3. 浏览器发送文本帧 `{"type":"finish"}` 时,后端把最后一个空音频包按负包发送给火山。
|
||||
4. 后端把火山 full server response 解析成 JSON 文本帧发回浏览器。
|
||||
|
||||
ASR 上游连接:
|
||||
|
||||
```text
|
||||
wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
|
||||
X-Api-Key: <VOLCENGINE_SPEECH_API_KEY>
|
||||
X-Api-Resource-Id: <VOLCENGINE_SPEECH_ASR_RESOURCE_ID>
|
||||
X-Api-Request-Id: <uuid>
|
||||
X-Api-Sequence: -1
|
||||
```
|
||||
|
||||
ASR 二进制协议:
|
||||
|
||||
1. 4 字节 header,大端整数。
|
||||
2. full client request:message type `0b0001`,JSON 序列化,gzip 压缩。
|
||||
3. audio only request:message type `0b0010`,raw payload,gzip 压缩。
|
||||
4. 最后一包音频使用 flags `0b0010`。
|
||||
5. full server response:message type `0b1001`,payload 为 gzip JSON。
|
||||
6. error response:message type `0b1111`,payload 为错误 JSON 或 UTF-8 文本。
|
||||
|
||||
首包参数由前端传入,但后端会兜底:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": { "uid": "current-user-id" },
|
||||
"audio": {
|
||||
"format": "pcm",
|
||||
"codec": "raw",
|
||||
"rate": 16000,
|
||||
"bits": 16,
|
||||
"channel": 1
|
||||
},
|
||||
"request": {
|
||||
"model_name": "bigmodel",
|
||||
"enable_itn": true,
|
||||
"enable_punc": true,
|
||||
"show_utterances": true,
|
||||
"result_type": "full"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TTS 协议边界
|
||||
|
||||
### WebSocket 双向流式
|
||||
|
||||
客户端连接:
|
||||
|
||||
```text
|
||||
GET /api/speech/volcengine/tts/bidirection
|
||||
Authorization: Bearer <Genarrative JWT>
|
||||
```
|
||||
|
||||
浏览器向后端发送 JSON 文本帧:
|
||||
|
||||
```json
|
||||
{ "type": "start_connection" }
|
||||
{ "type": "start_session", "sessionId": "...", "payload": { "user": {}, "req_params": {} } }
|
||||
{ "type": "task_request", "sessionId": "...", "payload": { "req_params": { "text": "..." } } }
|
||||
{ "type": "finish_session", "sessionId": "..." }
|
||||
{ "type": "finish_connection" }
|
||||
```
|
||||
|
||||
后端转成火山 WebSocket V3 二进制帧,并把上游返回帧统一解析成 JSON 文本或音频二进制帧回传浏览器。
|
||||
|
||||
TTS 双向上游连接:
|
||||
|
||||
```text
|
||||
wss://openspeech.bytedance.com/api/v3/tts/bidirection
|
||||
X-Api-Key: <VOLCENGINE_SPEECH_API_KEY>
|
||||
X-Api-Resource-Id: <VOLCENGINE_SPEECH_TTS_RESOURCE_ID>
|
||||
X-Api-Connect-Id: <uuid>
|
||||
```
|
||||
|
||||
V3 事件帧:
|
||||
|
||||
1. Full-client request + event number 用于 `StartConnection`、`StartSession`、`TaskRequest`、`FinishSession`、`FinishConnection`。
|
||||
2. Full-server response + event number 用于 `ConnectionStarted`、`SessionStarted`、`SessionFinished` 等状态事件。
|
||||
3. Audio-only response + event number 用于返回音频二进制。
|
||||
4. 错误帧必须转成结构化 JSON 错误,不把上游密钥或完整请求头写入日志。
|
||||
|
||||
### HTTP SSE 单向流式
|
||||
|
||||
客户端请求:
|
||||
|
||||
```text
|
||||
POST /api/speech/volcengine/tts/sse
|
||||
Authorization: Bearer <Genarrative JWT>
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "你好,欢迎来到百梦。",
|
||||
"speaker": "zh_female_cancan_mars_bigtts",
|
||||
"audioParams": {
|
||||
"format": "mp3",
|
||||
"sampleRate": 24000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后端转换为火山 HTTP SSE 请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": { "uid": "current-user-id" },
|
||||
"req_params": {
|
||||
"text": "...",
|
||||
"speaker": "...",
|
||||
"audio_params": {
|
||||
"format": "mp3",
|
||||
"sample_rate": 24000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上游 SSE 的常见事件:
|
||||
|
||||
1. `352`:TTSResponse,`data` 为 base64 音频片段。
|
||||
2. `351`:TTSSentenceEnd,`sentence` 为字幕或时间戳数据。
|
||||
3. `152`:SessionFinish,合成完成,可含 `usage.text_words`。
|
||||
4. `153`:SessionFailed,合成失败。
|
||||
|
||||
后端保持 SSE 形态透传,但会补齐平台 `requestId` 与上游 `X-Tt-Logid` 作为排障信息。
|
||||
|
||||
## api-server 路由
|
||||
|
||||
| 方法 | 路由 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/speech/volcengine/config` | 返回前端可见的默认资源和推荐音频参数,不返回密钥 |
|
||||
| `GET` | `/api/speech/volcengine/asr/stream` | ASR WebSocket 双向流式代理 |
|
||||
| `GET` | `/api/speech/volcengine/tts/bidirection` | TTS WebSocket 双向流式代理 |
|
||||
| `POST` | `/api/speech/volcengine/tts/sse` | TTS HTTP SSE 单向流式代理 |
|
||||
|
||||
所有路由必须走 `require_bearer_auth`。
|
||||
|
||||
## 验收
|
||||
|
||||
代码级验收:
|
||||
|
||||
```bash
|
||||
cargo fmt --manifest-path server-rs/Cargo.toml --all --check
|
||||
cargo test --manifest-path server-rs/Cargo.toml -p platform-speech
|
||||
cargo test --manifest-path server-rs/Cargo.toml -p api-server volcengine_speech
|
||||
cargo check --manifest-path server-rs/Cargo.toml -p api-server
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
联调验收:
|
||||
|
||||
1. 启动 `npm run api-server`。
|
||||
2. 检查 `/healthz` 返回 200。
|
||||
3. 未登录访问语音路由返回 401。
|
||||
4. 已登录后 `/api/speech/volcengine/config` 不返回任何密钥字段。
|
||||
5. ASR WebSocket 发送首包和 200ms PCM 分片后能收到识别 JSON。
|
||||
6. TTS SSE 能收到 `352` 音频事件与最终 `152` 完成事件。
|
||||
7. TTS 双向 WebSocket 能复用连接完成至少一个 session。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 不把 `VOLCENGINE_ACCESS_KEY_ID`、`VOLCENGINE_SECRET_ACCESS_KEY`、API Key、Access Token 或完整 Authorization 写入文档、日志、测试快照或前端状态。
|
||||
2. 中文语音默认使用 16k 单声道 PCM ASR;TTS 默认使用 24k mp3,运行时可按玩法需要改为 pcm。
|
||||
3. 火山返回的 `X-Tt-Logid` 是排障关键信息,应记录 logid,但不能记录密钥。
|
||||
4. 语音流式能力是平台副作用,不涉及 SpacetimeDB 表结构变更,本次无需修改 `migration.rs`。
|
||||
@@ -37,12 +37,14 @@
|
||||
|
||||
本次只做前端分享引导,不接入微信、QQ、抖音的原生 SDK。点击渠道 icon 与主“分享”按钮保持一致,复制同一份分享文本。
|
||||
|
||||
仓库现有 `media/social-media-group/wechat.png` 与 `qq.png` 是社群二维码,不作为本面板渠道 icon 使用。渠道 icon 采用轻量圆形文字标识,避免误导用户进入社群。
|
||||
仓库现有 `media/social-media-group/wechat.png` 与 `qq.png` 是社群二维码,不作为本面板渠道 icon 使用。渠道 icon 必须使用微信、QQ、抖音的品牌 SVG 轮廓,外层保持圆形触控底座;不能用通用聊天气泡、音乐符号或纯文字替代 logo。
|
||||
|
||||
## 面板样式约束
|
||||
|
||||
分享面板通过 `UnifiedModal` portal 挂载到页面根部时,需要在遮罩层补齐当前平台主题类,避免主题变量脱离页面容器后丢失。面板外壳继续使用 `platform-modal-shell` 的 `--platform-modal-fill` 背景,并在移动端覆盖平台弹窗默认底部抽屉布局,保持居中显示。
|
||||
|
||||
同类平台弹窗,包括删除作品等确认面板,也必须遵守同一条约束:portal 挂载时遮罩层必须带 `platform-theme platform-theme--light/dark`,面板必须保留 `platform-modal-shell` 的实体背景,不能把主面板做成透明或只依赖 backdrop blur。移动端高风险确认弹窗必须显式居中显示,避免被底部导航、安全区或底部抽屉布局遮住。
|
||||
|
||||
## 接入范围
|
||||
|
||||
- `RpgCreationResultActionBar`:RPG 发布成功后由父层回传分享数据并打开面板。
|
||||
|
||||
Reference in New Issue
Block a user