Prune stale docs and update .hermes content

Delete a large set of outdated documentation (many files under docs/ and .hermes/plans/, including audits, design, prd, technical, planning, assets, and todos). Update and consolidate .hermes content: refresh shared-memory pages (decision-log, development-workflow, document-map, pitfalls, project-overview, team-conventions) and several skills/references under .hermes/skills. Also modify AGENTS.md, README.md, UI_CODING_STANDARD.md, docs/README.md and .encoding-check-ignore. Purpose: clean up stale planning/audit material and keep current hermes documentation and related top-level docs in sync.
This commit is contained in:
2026-05-15 06:24:07 +08:00
parent 2eded08bc7
commit 3cb3efb4d0
708 changed files with 4033 additions and 142328 deletions

View File

@@ -1,235 +0,0 @@
# 后台管理服务设计
日期:`2026-04-23`
更新:`2026-04-30`
> 状态说明:本文件中的管理员鉴权、`/admin/api/*` 管理接口、数据库概览与受控 API 调试设计继续有效;同源内嵌 HTML/CSS/JS 后台页面已废弃。后续后台 UI 迁移到独立前端工程,当前 `api-server` 不再挂载 `GET /admin` 页面入口。独立后台前端的产品边界见 [`../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md`](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),技术方案见 [`ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md`](./ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
## 1. 目标
为当前 Rust `api-server` 增加一套同源后台管理服务,满足以下首版目标:
1. 支持管理员用户名密码登录。
2. 支持独立的管理员鉴权,不允许普通玩家 JWT 越权访问。
3. 支持在后台查看当前服务与数据库概览信息。
4. 支持在后台测试当前 `api-server` 已挂载接口。
5. 保持管理能力继续收口在 `server-rs`,管理 UI 由独立后台前端工程承接。
## 2. 背景与约束
当前仓库已具备:
1. Rust `api-server` 主链。
2. 基于 JWT + refresh session 的普通用户登录体系。
3. `SpacetimeDB + spacetime-client` 的主数据面。
本次后台管理服务必须继续遵守:
1. 后端统一落在 `server-rs`,不回退到 `server-node`
2. 不额外新起独立管理服务进程。
3. 管理 API 继续作为受保护管理域挂载在 `api-server`
4. 数据库信息必须尽量读取真实数据库侧信息,不能只展示硬编码假数据。
## 3. 首版范围
### 3.1 包含
1. `POST /admin/api/login`:管理员用户名密码登录。
2. `GET /admin/api/me`:当前管理员会话信息。
3. `GET /admin/api/overview`:服务与数据库概览。
4. `POST /admin/api/debug/http`:受控 HTTP 接口调试。
5. `POST /admin/api/profile/redeem-codes`:兑换码创建/更新。
6. `POST /admin/api/profile/redeem-codes/disable`:兑换码停用。
7. 基于 Bearer JWT 的管理员鉴权中间件。
### 3.2 不包含
1. 多角色管理员体系。
2. 管理员 refresh cookie / 多端会话管理。
3. 后台直接写库、删库、执行 reducer。
4. 任意 SQL 执行器。
5. `api-server` 内嵌 HTML/CSS/JS 后台页面。
## 4. 总体方案
### 4.1 部署形态
后台管理服务直接挂载在现有 `server-rs/crates/api-server` 内,作为同一个 Axum 进程的一部分。
原因:
1. 当前 `api-server` 已具备配置、JWT、错误包裹、日志与同源路由能力。
2. 后台本质上是服务运维与调试面,不值得单独再起一个网关或 BFF。
3. 同源可以避免开发期额外 CORS 和 cookie 域问题。
### 4.2 页面形态
后台管理页面不再由 `api-server` 直接返回内嵌 HTML/CSS/JS。`api-server` 仅保留管理 API页面由独立后台前端工程调用这些接口。
原因:
1. 管理 UI 需要独立演进,不应继续堆在 Rust 源码字符串中。
2. `server-rs` 继续负责鉴权、聚合和写操作,符合前端只做表现的工程约束。
3. 删除 `GET /admin` 后,当前服务访问该路径应返回 `404`
### 4.3 数据库信息来源
数据库概览不走本地 CLI shell也不依赖前端直接访问数据库。
首版采用两类信息源:
1. 服务端配置与连接信息:来自 `api-server` 当前 `AppConfig`
2. SpacetimeDB 真正的数据库元信息与表行数:由 `api-server` 通过 SpacetimeDB 官方 HTTP API 读取。
读取口径:
1. `/v1/database/{database}`:读取数据库基础信息。
2. `/v1/database/{database}/schema`:读取 schema 信息。
3. `/v1/database/{database}/sql`:对受控表执行 `SELECT COUNT(*)` 统计。
说明:
1. 首版只做只读概览,不暴露任意 SQL 输入。
2. 表清单由后端显式维护,避免用户在后台拼接任意查询。
## 5. 管理员鉴权设计
### 5.1 管理员账号来源
首版不复用普通玩家账号仓储,不把管理员账号混进 `module-auth` 用户表。
管理员账号来自环境变量:
1. `GENARRATIVE_ADMIN_USERNAME`
2. `GENARRATIVE_ADMIN_PASSWORD`
原因:
1. 管理员是平台运维身份,不等于玩家账号。
2. 首版目标是尽快落地可靠后台,不引入额外管理员表迁移。
3. 环境变量方案最适合当前阶段的单后台入口。
### 5.2 管理员 JWT
后台登录成功后签发独立管理员 Bearer JWT。
claims 设计:
1. 继续复用 `platform-auth::AccessTokenClaims`
2. `roles` 固定包含 `admin`
3. `sub` 使用稳定管理员主体,例如 `admin:<username>`
4. `sid` 使用后台会话 ID。
5. 不写 refresh cookie。
### 5.3 权限校验
新增 `require_admin_auth` 中间件,校验规则如下:
1. Bearer token 必须可被当前 JWT 配置正确验签。
2. `roles` 中必须包含 `admin`
3. `sub` 必须匹配当前管理员配置主体。
普通用户 token 即使同样由本服务签发,只要不带 `admin` 角色,也一律拒绝访问后台接口。
## 6. 后台页面设计
本节已由独立后台前端工程方案接管。历史同源页面曾包含三个主区域:
1. 登录卡片。
2. 数据库概览面板。
3. API 调试面板。
交互原则:
1. 页面简洁,不默认塞说明性长文案。
2. 移动端优先,窄屏下卡片改纵向堆叠。
3. API 调试结果在独立结果面板展示,不在按钮下方临时插一段文本。
## 7. 数据库概览设计
`GET /admin/api/overview` 返回以下信息:
1. 当前服务监听信息。
2. 当前 `SpacetimeDB server/database` 配置。
3. `SpacetimeDB` 数据库基础信息。
4. 当前 schema 表清单。
5. schema 表清单对应的逐表行数统计。
表统计必须以 SpacetimeDB schema 返回的表名为唯一来源,`schemaTableNames` 的数量必须与 `tableStats` 的行数一致。后台服务只对 schema 中符合安全 SQL 标识符格式的表名发起 `SELECT COUNT(*)`,不提供任意 SQL 输入能力。
返回中的计数失败项必须带错误信息不能静默吞掉。SpacetimeDB private 表或当前身份不可见的表可能在 `/sql` 下返回 `no such table` / `marked private`这类项统一展示为“不可统计private 或当前身份不可见)”,不作为整页读取失败处理。
## 8. API 调试设计
`POST /admin/api/debug/http` 提供一个受控 HTTP 调试代理。
请求参数:
1. `method`
2. `path`
3. `headers`
4. `body`
限制:
1. 只允许访问当前服务同源相对路径。
2. 调试回环地址由服务端按当前 `bind_host` 解析;若服务监听在 `0.0.0.0``::`,后台自动改走 loopback避免把通配监听地址直接当成调试目标。
2. 禁止调 `/admin/api/login` 本身,避免自套娃。
3. 禁止覆盖 `host``content-length` 等危险头。
4. 请求超时固定收口。
5. 返回调试结果时回显状态码、响应头、响应文本预览。
该能力用于验证当前服务端接口,不等价于通用代理工具。
## 9. 配置项
新增以下环境变量:
1. `GENARRATIVE_ADMIN_USERNAME`
2. `GENARRATIVE_ADMIN_PASSWORD`
3. `GENARRATIVE_ADMIN_TOKEN_TTL_SECONDS`
默认策略:
1. 若未配置用户名或密码,则后台登录接口返回 `503`,独立后台前端自行展示未启用状态。
2. 默认管理员 token TTL 为 `4` 小时。
## 10. 测试要求
至少覆盖:
1. 管理员登录成功。
2. 管理员密码错误返回 `401`
3. 普通用户 token 访问后台接口返回 `403`
4. 未登录访问后台接口返回 `401`
5. 后台概览接口在未启用管理员配置时返回 `503`
6. API 调试接口能成功访问 `/healthz`
7. API 调试接口拒绝绝对 URL 和后台自身登录接口。
## 11. 路由清单
当前保留的管理 API 路由:
1. `POST /admin/api/login`
2. `GET /admin/api/me`
3. `GET /admin/api/overview`
4. `POST /admin/api/debug/http`
5. `POST /admin/api/profile/redeem-codes`
6. `POST /admin/api/profile/redeem-codes/disable`
`GET /admin` 已取消挂载,后续由独立后台前端工程承接页面入口。
## 12. 完成定义
当前管理 API 保留与内嵌页面移除满足以下条件时,本任务视为完成:
1. `api-server` 内存在受保护后台管理域。
2. 管理员用户名密码可登录。
3. 普通用户 token 无法访问后台接口。
4. 后台能看到服务和数据库真实概览。
5. 后台能调试当前服务 HTTP 接口。
6. 兑换码管理 API 可由管理员 token 调用。
7. `GET /admin` 不再挂载,访问返回 `404`
8. 独立后台前端 PRD 与技术方案已补齐。
9. 路由索引与技术文档已同步更新。

View File

@@ -1,76 +0,0 @@
# 后台创作入口开关操作入口
日期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`
当前默认配置中,`visual-novel` 暂时从创作页隐藏并关闭入口,默认种子为 `visible=false``open=false`。如果后续只想恢复已发布作品试玩而不恢复创作入口,需要先明确 API 熔断范围,再通过后台入口开关调整,不能在前端硬编码恢复模板 Tab。
## 注意
- 前端后台页面只做管理表单,不成为配置事实源。
- `src/config/newWorkEntryConfig.ts` 不应恢复。
- SpacetimeDB client bindings 当前新增了对应临时 binding 文件;后续执行标准 bindings regenerate 时应覆盖并保持同名 procedure/type。

View File

@@ -1,98 +0,0 @@
# 后台数据库表查询技术方案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 值统一转成人可读 JSONOption None 为 nullSome 展开为内部值Timestamp 单元素数组展开为内部值;已知业务枚举列应在 API Server 按表名和列名转换为业务字符串,例如 `profile_recharge_order.kind` 转为 `points` / `membership``profile_recharge_order.status` 转为 `pending` / `paid` / `failed` / `closed` / `refunded`
## 前端页面
路由:`#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`

View File

@@ -1,170 +0,0 @@
# 后台埋点数据明细与 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. 修复问题后提交并推送当前分支。

View File

@@ -1,641 +0,0 @@
# 后台管理独立前端工程技术方案
日期:`2026-04-30`
对应 PRD[后台管理独立前端工程 PRD](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md)
落地状态:`2026-04-30` 已创建 `apps/admin-web` 独立前端工程包含登录、总览、API 调试、兑换码管理和注册邀请码管理首版页面;根工程已补 `admin-web:*` 转发脚本。`2026-05-01` 起,根构建与 Ubuntu 发布包会同步构建后台前端,并在发布包 Web 网关中以同域 `/admin/` 暴露。
## 1. 结论
后台管理端采用独立前端工程,路径固定为 `apps/admin-web`。它只负责 UI 表现、输入采集、请求发起和结果渲染所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 `server-rs/crates/api-server`
本方案接管旧 `api-server` 内嵌 HTML/CSS/JS 页面Rust `api-server` 直连时旧 `GET /admin` 不再挂载。部署态后台入口由发布包内 `web-server.mjs` 承接:`/admin/` 返回独立前端静态产物,`/admin/api/*` 继续反代到 `api-server`
## 2. 工程结构
建议首版结构:
```text
apps/
└─ admin-web/
├─ index.html
├─ package.json
├─ tsconfig.json
├─ vite.config.ts
└─ src/
├─ main.tsx
├─ app/
│ ├─ AdminApp.tsx
│ ├─ AdminShell.tsx
│ └─ adminRoutes.ts
├─ api/
│ ├─ adminApiClient.ts
│ └─ adminApiTypes.ts
├─ auth/
│ └─ adminAuthStore.ts
├─ pages/
│ ├─ AdminLoginPage.tsx
│ ├─ AdminOverviewPage.tsx
│ ├─ AdminDebugHttpPage.tsx
│ ├─ AdminRedeemCodePage.tsx
│ └─ AdminInviteCodePage.tsx
└─ styles/
└─ admin.css
```
首版可使用独立 `package.json`,不要求立刻把根工程改成 npm workspace。后续如果根工程统一 workspace再把 `apps/admin-web` 纳入统一脚本。
## 3. 技术栈
1. React + TypeScript + Vite。
2. 图标使用 `lucide-react`
3. 样式首版使用普通 CSS 或 CSS Modules不引入新的大型 UI 组件库。
4. 请求使用浏览器 `fetch` 封装,不新增状态管理库。
5. 不引入 SpacetimeDB TypeScript SDK管理端不直连 SpacetimeDB。
## 4. API 边界
### 4.1 基础约定
所有管理端请求使用同一个 `adminApiClient`
1. base URL 由 `VITE_ADMIN_API_BASE_URL` 配置。
2. 未配置时默认同源空前缀。
3. 有 token 时附加 `Authorization: Bearer <token>`
4. 后端统一响应 envelope 时,前端读取 `data`;错误优先读取 `error.details.message`,再读 `error.message`,最后回退到 HTTP 状态。
前端统一按以下响应形状解析,不在页面组件里重复拆 envelope
```ts
export interface ApiSuccessEnvelope<T> {
data: T;
meta?: unknown;
}
export interface ApiErrorEnvelope {
error?: {
code?: string;
message?: string;
details?: {
message?: string;
[key: string]: unknown;
} | null;
};
meta?: unknown;
}
```
`adminApiClient` 暴露 `request<T>()``get<T>()``post<T>()` 三层即可。页面只拿到成功数据或抛出的中文错误消息,不直接处理 `Response`
### 4.2 已有管理接口
| 功能 | 方法与路径 | 鉴权 |
| --- | --- | --- |
| 管理员登录 | `POST /admin/api/login` | 无 |
| 当前管理员 | `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 前端类型命名
后台前端首版不引入自动生成 contract。为了避免字段漂移`apps/admin-web/src/api/adminApiTypes.ts` 必须按 `shared-contracts` 的 camelCase JSON 字段命名:
```ts
export interface AdminSessionPayload {
subject: string;
username: string;
displayName: string;
roles: string[];
issuedAt: string;
expiresAt: string;
}
export interface AdminLoginResponse {
token: string;
admin: AdminSessionPayload;
}
export interface AdminOverviewResponse {
service: AdminServiceOverviewPayload;
database: AdminDatabaseOverviewPayload;
}
export interface AdminServiceOverviewPayload {
bindHost: string;
bindPort: number;
jwtIssuer: string;
adminEnabled: boolean;
spacetimeServerUrl: string;
spacetimeDatabase: string;
}
export interface AdminDatabaseOverviewPayload {
databaseIdentity: string | null;
ownerIdentity: string | null;
hostType: string | null;
schemaTableNames: string[];
tableStats: AdminDatabaseTableStatPayload[];
fetchErrors: string[];
}
export interface AdminDatabaseTableStatPayload {
tableName: string;
rowCount: number | null;
errorMessage: string | null;
}
export interface AdminDebugHeaderInput {
name: string;
value: string;
}
export interface AdminDebugHttpRequest {
method: string;
path: string;
headers?: AdminDebugHeaderInput[];
body?: string;
}
export interface AdminDebugHttpResponse {
status: number;
statusText: string;
headers: AdminDebugHeaderInput[];
bodyText: string;
bodyJson: unknown | null;
}
```
兑换码类型同样保持 camelCase
```ts
export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
enabled: boolean;
allowedUserIds: string[];
allowedPublicUserCodes: string[];
}
export interface AdminDisableProfileRedeemCodeRequest {
code: string;
}
export interface AdminUpsertProfileInviteCodeRequest {
inviteCode: string;
startsAt?: string | null;
expiresAt?: string | null;
metadata?: Record<string, unknown>;
}
export interface ProfileRedeemCodeAdminResponse {
code: string;
mode: ProfileRedeemCodeMode;
rewardPoints: number;
maxUses: number;
globalUsedCount: number;
enabled: boolean;
allowedUserIds: string[];
createdBy: string;
createdAt: string;
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
请求:
```json
{
"username": "root",
"password": "secret123"
}
```
成功数据:
```json
{
"token": "<admin bearer token>",
"admin": {
"subject": "admin:root",
"username": "root",
"displayName": "root",
"roles": ["admin"],
"issuedAt": "2026-04-30T00:00:00Z",
"expiresAt": "2026-04-30T04:00:00Z"
}
}
```
`503` 表示后台未启用;`401` 表示用户名或密码错误。
### 4.5 总览 contract
`GET /admin/api/overview` 返回:
1. `service``bindHost``bindPort``jwtIssuer``adminEnabled``spacetimeServerUrl``spacetimeDatabase`
2. `database``databaseIdentity``ownerIdentity``hostType``schemaTableNames``tableStats``fetchErrors`
后端读取 SpacetimeDB schema 时必须请求 `/v1/database/{database}/schema?version=9`。SpacetimeDB 2.x schema HTTP API 缺少 `version` query 会返回 `400 missing field version`,后台页面只能展示读取异常,不能拿到真实表名。
`schemaTableNames``tableStats` 必须采用同一份 schema 表清单生成不能再用硬编码关键表白名单补齐统计项。后台右上角显示的表数量必须等于统计表格实际行数schema 读取失败时两者均为空,并通过 `fetchErrors` 暴露读取失败原因。
后端读取表行数时必须按 SpacetimeDB 2.x `/sql` 响应解析:接口返回 statement result 数组,单条结果内的 `schema.elements` 描述列名,`rows` 是按列顺序排列的数组行,例如 `rows: [[0]]`。后台服务不能再假设响应是 `{ rows: [{ row_count: 0 }] }` 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。
`tableStats` 中单表失败必须展示 `errorMessage`不能让整页变成空白。SpacetimeDB private 表或当前身份不可见的表在 `/sql` 下可能返回 `no such table` / `marked private`后台服务必须将这类错误归一为“不可统计private 或当前身份不可见)”,避免把预期的访问边界展示成原始 HTTP 400 故障。
线上如果大量表都显示“不可统计private 或当前身份不可见)”,优先检查 `api-server` 启动环境中的 `GENARRATIVE_SPACETIME_TOKEN` 是否存在且属于目标库 owner。Jenkins 覆盖发布包时必须保留部署目录已有运行 token只带迁移 token 不能让后台概览读取 private 表。
### 4.6 API 调试 contract
请求:
```json
{
"method": "GET",
"path": "/healthz",
"headers": [],
"body": ""
}
```
限制由后端执行:
1. `path` 只允许同源相对路径。
2. 禁止绝对 URL。
3. 禁止调试 `/admin/api/login`
4. 禁止覆盖危险请求头。
5. 请求体大小和超时由后端收口。
### 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
{
"code": "WELCOME2026",
"mode": "public",
"rewardPoints": 100,
"maxUses": 1,
"enabled": true,
"allowedUserIds": [],
"allowedPublicUserCodes": []
}
```
停用请求:
```json
{
"code": "WELCOME2026"
}
```
成功返回兑换码记录:
```json
{
"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"
}
```
兑换码管理页进入时必须通过 `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"
}
}
```
成功返回邀请码记录:
```json
{
"userId": "admin",
"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"
}
```
邀请码页的 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`
2. token 首版存 localStorage。
3. 应用启动时如果存在 token先调用 `GET /admin/api/me`
4. `401` 时清空 token 并回到登录页。
5. `403` 时展示无权限状态,不自动重试。
6. 退出登录只清理本地 token首版没有后台 refresh session 和服务端会话吊销。
## 6. 页面实现要点
1. `AdminShell` 承载导航、当前管理员、退出按钮和页面容器。
2. 登录页不进入 `AdminShell`,避免未登录时展示后台导航。
3. 总览页加载失败时展示后端错误,不吞掉 `fetchErrors`
4. API 调试页的 headers 使用键值行编辑,提交前转为 `[{ name, value }]`
5. 兑换码页的 `mode=private` 时展示允许用户输入区;其他模式提交空数组。
6. 兑换码页和邀请码页进入时加载数据库列表,保存后合并返回记录,点击列表项回填表单进入编辑态。
7. 邀请码页只提交 `inviteCode` 与 JSON 对象 metadata不在前端复制后端邀请码规则。
8. 所有按钮的 loading 状态必须锁定重复提交。
9. 移动端优先:表单单列,导航紧凑,结果面板可横向/纵向滚动。
## 7. 部署与联调
### 7.1 本地联调
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 构建部署
当前发布形态固定为同域 `/admin/`
1. 本地单独执行 `npm run admin-web:build` 时,后台构建产物默认输出到 `apps/admin-web/dist`
2. 根工程执行 `npm run build` 时,会先构建主前端,再构建后台前端;任一构建失败或输出 warning 都会让构建门禁失败。
3. Ubuntu 发布包执行 `npm run deploy:rust:remote` 时,后台前端以 Vite `--base /admin/` 构建到发布包 `web/admin/`
4. 发布包 `web-server.mjs``/admin` 返回 301 到 `/admin/`,对 `/admin/``/admin/*` 提供后台 SPA fallback`/admin/api/*` 优先反代到 `api-server`
该形态不新增后台静态端口和后台专用后端。`server-rs` 仍然是唯一管理 API 后端,后台前端不直连 SpacetimeDB。
### 7.3 后台工程脚本
`apps/admin-web/package.json` 首版至少提供以下脚本:
```json
{
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "node ../../scripts/admin-web-build.mjs build",
"typecheck": "node ../../scripts/admin-web-build.mjs typecheck",
"preview": "vite preview --host 127.0.0.1"
}
}
```
如果后续接入根 npm workspace再在根 `package.json` 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。
当前工程没有启用 npm workspace因此后台构建脚本必须从仓库根目录调用 root toolchain。`scripts/admin-web-build.mjs` 统一执行 `tsc --noEmit -p apps/admin-web/tsconfig.json` 与 Vite 构建,避免 `npm --prefix apps/admin-web` 在子目录找不到 `tsc`
当前根工程同步提供以下转发脚本:
1. `npm run admin-web:dev`
2. `npm run admin-web:typecheck`
3. `npm run admin-web:build`
4. `npm run admin-web:preview`
## 8. 测试计划
1. `apps/admin-web`
- 登录成功和失败。
- token 恢复、过期清理、退出登录。
- 总览页正常数据、部分表统计失败、整体请求失败。
- API 调试成功访问 `/healthz`,绝对 URL 被后端拒绝。
- 兑换码数据库列表加载、列表点击回填、public/unique/private 表单提交和停用。
- 邀请码数据库列表加载、普通用户邀请码不展示、列表点击回填、metadata JSON 对象校验和结果展示。
2. 根工程:
- `npm run check:encoding`
- 后续接入根 workspace 后,补充后台工程 build/typecheck 脚本。
3. 后端:
- 继续保留 `cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`
- 修改后端管理 API 后必须运行 `npm run api-server` 并手动验证 `/admin` 为 404、`/admin/api/login` 可用。
## 9. 后续扩展边界
后续新增用户管理、作品审核、资产审核、订单/充值管理时,必须先补对应 PRD 和技术方案,并在 `server-rs` 增加受保护管理 API。不要让 `apps/admin-web` 直接读取 SpacetimeDB 或复制业务规则。
## 10. 实施顺序
1. 先创建 `apps/admin-web` 工程骨架,确保空应用可 `dev/build`
2. 再实现 `adminApiTypes``adminApiClient`,用 `/admin/api/login` 做第一条真实链路。
3. 接入 `adminAuthStore` 和启动恢复逻辑,确认 `401` 会清理本地 token。
4. 完成 `AdminShell` 与四页路由再分别接入总览、API 调试、兑换码和邀请码接口。
5. 最后补测试、运行 `npm run check:encoding`,并确认 `GET /admin` 仍由 `api-server` 返回 `404`
当前实现已完成第 1 至第 4 步。验证以实际命令输出为准。

View File

@@ -1,45 +0,0 @@
# 冒险实体详情 NPC 预览修复记录2026-04-26
## 背景
RPG 运行态点击画面中的对面 NPC 角色形象时,详情弹窗的立绘与画布上实际显示的 NPC 不一致,并伴随 React 报错:
`Encountered two children with the same key, ``.`
## 问题定位
1. 画布层 `GameCanvasEntityLayer` 渲染 NPC 时,会优先使用当前 `Encounter` 实例上的 `visual``imageSrc``monsterPresetId`,再回退到 `characterId` 对应的预设角色。
2. 详情弹窗 `AdventureEntityModal` 原本优先按 `characterId` 渲染预设角色,导致运行时遭遇已经携带独立形象时,点击后弹窗显示成另一个角色内容。
3. `AdventureEntityModal` 内部存在多个浮层共用同一个 `AnimatePresence`,直系子节点没有显式稳定 key同时 NPC 运行时背包物品如果传入空 `id`,会把空字符串直接交给物品格列表作为 React key。
## 落地约束
1. NPC 详情立绘必须与画布点击对象一致:
- `encounter.visual`
- `encounter.imageSrc`
- `encounter.monsterPresetId`
- `encounter.characterId`
- 通用 NPC 生成形象
2. 前端只做展示优先级和 key 稳定性处理,不新增剧情规则、不改写运行时 NPC 数据来源。
3. 所有列表和并列浮层都必须具备稳定、非空、可区分的渲染 key。
## 本次修改
1. `src/components/AdventureEntityModal.tsx`
- 新增 `NpcEncounterPortrait`,让弹窗立绘优先使用遭遇实例形象,与画布渲染策略对齐。
- 新增 `selectionRenderKey`,给实体详情、标签详情、技能详情浮层提供稳定 key。
- 新增 NPC 背包物品渲染 id 规范化,避免空 id 或重复 id 触发 React key 冲突,并避免点击物品时选中错误项。
- 技能附带状态标签 key 增加兜底字段,避免空 buff id 冲突。
2. `src/components/AdventureEntityModal.test.tsx`
- 覆盖“有 `characterId` 但遭遇实例提供 `imageSrc` 时,详情立绘必须显示遭遇图像”。
- 覆盖“NPC 背包物品空 id 不再触发重复 key 警告”。
## 验证
已执行:
```bash
npm run test -- AdventureEntityModal.test.tsx CharacterInfoShared.test.tsx
```
结果5 个测试全部通过。

View File

@@ -1,99 +0,0 @@
# Agent 对话框与结果页精修职责边界修正
更新时间:`2026-04-21`
## 1. 结论
本次修正把“Agent 对话框”和“结果页精修”重新拆清楚:
1. `CustomWorldAgentWorkspace` 只负责八锚点信息收集、八锚点进度展示、八锚点完成后的“整理世界底稿”动作。
2. “精修”不是 Agent 对话框里的概念,不再通过 Agent 建议动作进入角色、地点、世界总卡的局部修整。
3. 已经生成底稿的草稿,从创作中心点击后进入结果页继续完善。
4. 尚未生成底稿的草稿,从创作中心点击后才恢复 Agent 对话框继续补齐八锚点。
5. 结果页负责成稿后的编辑、补全、进入世界前确认和自动保存,并通过 `sync_result_profile` 回写到当前 Agent session。
一句话:
**Agent 收八锚点,结果页做精修。**
---
## 2. 为什么要修正
旧实现把 `object_refining` 草稿卡片显示成“继续精修”,但点击后直接恢复 Agent 工作区。
这个行为会让用户产生两个误解:
1. 以为精修是 Agent 对话框里的下一阶段。
2. 以为 Agent 对话框不仅负责收集八锚点,还负责后续对象级编辑。
这和当前产品边界不一致。Agent 对话框应该保持轻量,只用于拿到足够稳定的八锚点输入;对象、场景、封面、世界档案的修整都应该在结果页完成。
---
## 3. 当前落地规则
### 3.1 创作中心草稿点击分流
`custom-world/works` 返回 `agent_session` 草稿后,前端按草稿是否已有底稿内容分流:
1. `playableNpcCount <= 0 && landmarkCount <= 0`
- 视为八锚点仍未整理成底稿。
- 点击进入 `agent-workspace`
2. `playableNpcCount > 0 || landmarkCount > 0`
- 视为已有可编辑底稿。
- 点击读取对应 Agent session编译为 `CustomWorldProfile`,进入 `custom-world-result`
### 3.2 Agent 对话框动作边界
Agent 会话建议动作只保留:
1. 总结当前设定 / 总结当前世界底稿。
2. 八锚点准备完成后的“整理一版世界底稿”。
不再在 Agent 会话快照里继续生成或兼容展示:
1. `refine_focus_target`
2. “精修角色”
3. “继续补地点”
4. “先看世界总卡”
旧 session 快照如果仍带有 `refine_focus_target`,服务端兼容层会过滤掉,避免旧数据把精修入口重新塞回 Agent 对话框。
### 3.3 结果页精修边界
Agent 来源结果页不再是冻结预览态。
当前允许在结果页继续进行成稿精修,包括:
1. 编辑世界信息。
2. 编辑角色、场景、封面等对象档案。
3. 删除或调整已有对象。
4. 自动保存到作品草稿。
5. 进入世界前通过 `sync_result_profile` 写回 Agent session。
为了保持主链简洁Agent 来源结果页仍不重新打开“通过 Agent 对话精修对象”的入口。
---
## 4. 对历史文档口径的覆盖
这份文档覆盖 [AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md](./AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md) 中“Agent 来源结果页冻结为预览态”的阶段性口径。
新的主口径是:
1. Agent 来源结果页可以编辑,因为精修本来就应该发生在结果页。
2. 需要收紧的是 Agent 对话框,不是结果页。
3. 结果页编辑后仍必须同步回 Agent session保持进入世界前的数据真相源一致。
---
## 5. 验收标准
本次修正完成后应满足:
1. 创作中心已有底稿草稿按钮文案为“继续完善”,点击进入结果页。
2. 创作中心未成稿草稿按钮仍为“继续创作”,点击进入 Agent 对话框。
3. Agent 对话框不出现“精修角色 / 补地点 / 看世界总卡”类对象精修入口。
4. Agent 来源结果页可以打开编辑弹窗进行精修。
5. 返回创作从结果页回到创作中心,不回到 Agent 对话框。

View File

@@ -1,109 +0,0 @@
# Agent 草稿编译后重复生成草稿修复 2026-04-23
更新时间:`2026-04-23`
## 1. 问题现象
当前创作链里,用户打开一个已有 Agent 草稿,继续编辑并触发结果页编译或自动保存后,作品库里会新增一份 draft而不是更新原来的那一份。
这会带来两个直接问题:
1. 同一个会话在作品库里出现多份草稿
2. 原草稿状态没有被正确推进,导致发布、恢复、继续创作都可能命中旧条目
## 2. 根因拆解
本次问题不是单点,而是两段身份链没有收口到同一套规则:
### 2.1 `sync_result_profile` 会直接覆盖 session 里的 `draft_profile_json.id`
当前 `sync_result_profile` 会把结果页传回来的 `profile` 直接写回 `draft_profile_json`
如果结果页上的 `profile.id` 已经不是原草稿 id那么
1. session 主链中的 `draft_profile_json.id` 会被改成新 id
2. `resultPreview.preview.id` 也会跟着变成新 id
3. 前端 autosave 会拿着这个新 id 去调作品库 `PUT /custom-world-library/:profileId`
### 2.2 作品库 upsert 只按 `profile_id` 命中,不按 `source_agent_session_id` 兜底
当前作品库落库路径:
`结果页 autosave -> PUT /custom-world-library/:profileId -> upsert_custom_world_profile_record`
其中普通 autosave 路径此前存在两个问题:
1. 前端没有透传 `sourceAgentSessionId`
2. 后端普通 upsert 只按 `(owner_user_id, profile_id)` 查旧记录
所以一旦 `profile.id` 漂移,后端就会把它当作一条新的 draft 插入。
## 3. 修复目标
本轮修复要求同时满足下面两条:
1. Agent 草稿结果页继续编辑时,必须优先继承当前 session 已有的稳定 `profileId`
2. 即使前端传来的 `profile.id` 已经漂移,作品库 upsert 也要能按 `source_agent_session_id` 命中同一份 draft 并更新
## 4. 本轮落地策略
### 4.1 session 主链侧:稳定保留草稿 id
`sync_result_profile` 中,若当前 session 已经存在草稿身份:
1. 优先读取 `draft_profile_json.legacyResultProfile.id`
2. 其次读取 `draft_profile_json.id`
3. 若命中稳定 id则把传入 profile 的:
- 顶层 `id`
- `legacyResultProfile.id`
都强制回写为这个稳定 id
这样可以保证:
1. `draft_profile_json.id` 不会被结果页里的漂移 id 覆盖
2. `resultPreview.preview.id` 会持续稳定
3. 前端后续 autosave 会继续更新原草稿
### 4.2 作品库保存侧:透传 `sourceAgentSessionId`
前端 `upsertRpgWorldProfile(...)` 新增可选参数:
`sourceAgentSessionId?: string | null`
结果页属于 Agent 草稿视图时autosave 会把 `activeAgentSessionId` 一并传给作品库接口。
### 4.3 后端 upsert 侧:按 session 命中旧 draft
普通作品库 `PUT /custom-world-library/:profileId` 接口新增读取 `sourceAgentSessionId`
Spacetime `upsert_custom_world_profile_record(...)` 在按 `profile_id` 未命中时,新增二级兜底:
1. `owner_user_id` 相同
2. `publication_status == draft`
3. `deleted_at == None`
4. `source_agent_session_id == input.source_agent_session_id`
若命中这条旧 draft
1. 删除旧 row
2. 使用旧 row 的 `profile_id`
3. 更新 payload / metadata / updated_at
这样即使前端 path 参数已经是新 id也仍然会命中原草稿并更新而不是再插入第二份草稿。
## 5. 验收标准
修复后应满足:
1. 打开已有 Agent draft 后继续编译,不会新增第二份 draft
2. 原 draft 的 `profileId` 保持不变
3. `resultPreview.preview.id` 与作品库 `profileId` 一致
4. 自动保存、继续创作、进入世界、发布前检查都围绕同一份草稿身份工作
## 6. 结论
这次问题的本质不是“自动保存重复调用”,而是:
**Agent 草稿在 session 主链和作品库 upsert 两端都缺少稳定身份约束。**
本轮通过“session 保 id + 作品库按 session 兜底命中”双保险,把同一份草稿重新收口为单一身份。

View File

@@ -1,119 +0,0 @@
# Agent 草稿结果页资产合并修复 2026-04-21
更新时间:`2026-04-21`
## 1. 问题现象
当前创作流程里,用户在“生成草稿”后反馈:
1. 角色主图没有稳定出现在结果页
2. 场景背景图有时可见,有时角色图缺失
3. 自动保存后的作品库条目里,分幕图可能已经存在,但场景角色主图仍为空
## 2. 本次真实排查结论
本轮不是单一的“没写数据库”问题,而是 `agent draft -> result profile` 桥接层存在一类更隐蔽的集合漂移问题。
排查后确认:
1. 最新 `custom_world_sessions.payload_json` 里的 `draftProfile.storyNpcs[].imageSrc` 已经存在
2. 最新 `draftProfile.sceneChapters[].acts[].backgroundImageSrc` 也已经存在
3. 对应图片文件也真实存在于仓库根 `public/`
4. 最新 `custom_world_profiles.payload_json` 里,分幕图通常已保存成功
5. 但场景角色主图可能仍为空
根因在于:
1. 结果页桥接层在 `draftProfile.legacyResultProfile` 存在时,仍把 `legacyResultProfile` 视为主列表
2. 旧逻辑只会按 `id``draftProfile` 里的图片字段回贴到 `legacyResultProfile`
3. 一旦后续草稿精修导致 `draftProfile` 的角色集合、角色 id 或角色命名发生漂移
4.`legacyResultProfile` 就会继续主导结果页和自动保存对象列表
5. 最新角色主图虽然已在 `draftProfile` 里生成完成,但会因为匹配失败而被整批吞掉
这类问题在场景角色上最明显,因为角色集合最容易在后续精修中替换。
## 3. 修复策略
本轮在:
- `src/services/customWorldAgentDraftResult.ts`
调整桥接规则:
1. `legacyResultProfile` 仍保留,继续提供运行时富字段
2. 但角色、场景、分幕等对象集合不再默认由 `legacyResultProfile` 主导
3. 最新 `draftProfile` 成为结果页对象列表的主来源
4. `legacyResultProfile` 只负责给命中的对象补运行时富字段
5. 匹配优先级为:
- 先按 `id`
- 再按名称兜底
具体规则:
1. `playableNpcs`:以最新 draft 集合为主legacy 只补富字段与旧运行时字段
2. `storyNpcs`:同上,避免旧角色列表吞掉新角色主图
3. `sceneChapterBlueprints`:以最新 draft 幕列表为主legacy 只补章节/幕已有运行时字段
4. `landmarks`:优先更新最新 draft 命中的场景对象,但保留 legacy 中未被命中的剩余运行时场景,避免丢连接与残留信息
5. `camp`:保留 legacy 基础信息,但优先取 draft 最新图片字段
## 4. 修复后的链路意义
修复后:
1. 草稿自动资产服务生成的角色主图不会再因为旧 `legacyResultProfile` 的角色集合过时而丢失
2. 分幕图继续可以稳定进入结果页与自动保存
3. 作品库自动保存时,结果页编译出的 profile 更接近“当前草稿真实快照”,而不是历史 legacy 快照
## 5. 新增验证
本轮补了前端桥接测试:
- `src/services/customWorldAgentDraftResult.test.ts`
新增验证点:
1.`draftProfile.storyNpcs``legacyResultProfile.storyNpcs` 集合漂移时
2. 结果页仍应优先展示最新 draft 角色
3. 最新角色主图与最新分幕图不能被旧 legacy 快照吞掉
## 6. 当前状态
本轮修复后,本地已验证:
1. `src/services/customWorldAgentDraftResult.test.ts`
2. `src/components/CustomWorldResultView.test.tsx`
3. `src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
4. `npm run check:encoding`
均通过。
## 7. 后续建议
这次问题再次说明:
1. `legacyResultProfile` 继续长期作为结果页主列表来源风险很高
2. 只做“按 id 回贴图片字段”在对象集合会漂移的链路里不够稳
3. 后续如果继续推进后端单一真相源,应优先把结果页对象集合完全改为服务端最新 compile/preview 结果,而不是继续让前端桥接层承担最终裁决
---
## 8. 2026-04-21 补充:新建共创会话 500 根因
后续联调里又发现一条与资产合并无关、但会直接阻断创作入口的后端问题:
1. 点击“创建新 RPG 游戏”时,`POST /api/runtime/custom-world/agent/sessions` 返回 `500`
2. 表面上前端只看到“服务器内部错误”,实际根因在路由层
本次补查确认:
1. `server-node/src/routes/customWorldAgent.ts` 这组路由内部直接使用 `request.userId!`
2. 但路由文件本身在 `2026-04-21` 修复前没有挂 `requireJwtAuth(...)`
3. 结果是 HTTP 请求虽然带了登录 token`request.userId` 并不会被注入
4. 后端继续拿 `undefined` 作为 `userId` 创建 / 读取 agent session最终在仓储写库阶段触发 `500`
修复方式:
1.`createCustomWorldAgentRoutes(...)` 顶部统一补上 `router.use(requireJwtAuth(context.config, context.userRepository))`
2. 让 session 创建、读取、发消息、执行 action、读取 operation / card 详情全部走同一层鉴权注入
这次补丁的意义不是新增功能,而是把 `custom-world agent` 路由和其他 `rpg-entry / rpg-profile / rpg-runtime` 受保护接口重新对齐,避免再出现“路由里依赖 `request.userId`,但入口没挂鉴权”的低层断线问题。

View File

@@ -1,119 +0,0 @@
# 创作流程草稿/图片/动作自动保存数据库检查 2026-04-21
更新时间:`2026-04-21`
## 1. 本次检查范围
本次检查只聚焦当前创作流程里下面这条链路:
`结果页前端编辑 -> 自动保存 -> Agent session 主链同步 -> 作品库落库`
重点核对三类内容:
1. 草稿文本类修改
2. 生成后的角色图片、地点图片、分幕图
3. 角色动作相关资产字段
## 2. 当前实际自动保存链路
当前前端主入口在:
- `src/components/game-shell/PreGameSelectionFlow.tsx`
实际行为如下:
1. 结果页编辑统一通过 `onProfileChange` 更新 `generatedCustomWorldProfile`
2. 当结果页停留在 `custom-world-result` 阶段时,前端会对 profile 做防抖自动保存
3. 如果当前结果页来源是 `agent-draft`,自动保存前会先执行 `sync_result_profile`
4. `sync_result_profile` 完成后,前端不直接保存旧内存 profile而是优先保存“从最新 session 重编译出的 profile”
5. 作品库保存最终走 `PUT /api/runtime/custom-world-library/:profileId`
6. Express 后端通过 `runtimeRepository.upsertCustomWorldProfile(...)` 把 profile 写入 `custom_world_profiles.payload_json`
所以数据库层本身是有正常落库能力的。
## 3. 本次检查前确认成立的部分
以下能力在本次检查前已经成立:
1. 结果页普通草稿字段编辑会触发自动保存
2. 自动保存会真正调用后端作品库接口并更新数据库
3. 返回创作、进入世界两条路径也会优先同步 Agent session
4. `legacyResultProfile` 已作为阶段一桥接快照保留在 session 中
## 4. 本次发现的真实风险
风险不在数据库写入本身,而在:
`sync_result_profile -> session 重编译结果页 profile`
此前 `sync_result_profile` 只回写:
1. 基础摘要字段
2. `legacyResultProfile`
但没有把结果页里已经确认过的资产字段同步回 foundation draft 对应节点。
这会导致一个阶段性风险:
1. 用户在结果页换了新的角色图
2. 或者结果页里刚确认了新的动作资产字段
3. 或者结果页里刚确认了新的地点图、分幕图
4. 自动保存前前端先做一次 session 同步
5. 同步完成后又从 session 重编译结果页 profile
6. 重编译过程会把 draft 层旧资产字段再次并入结果 profile
这样就可能出现:
**数据库自动保存成功了,但保存进去的是“被旧 draft 资产字段回退过的版本”,不是用户刚在结果页看到的最新图/动作。**
## 5. 本轮修复
本轮在:
- `server-node/src/services/customWorldAgentOrchestrator.ts`
补了一个收窄修复:
1. `sync_result_profile` 仍然保持阶段一边界,不做整套 runtime -> foundation draft 反解
2. 但会按相同 id把结果页里已确认的资产字段同步回 draft 层已有对象
3. 同步范围包括:
- 角色 `imageSrc`
- 角色 `generatedVisualAssetId`
- 角色 `generatedAnimationSetId`
- 角色 `animationMap`
- 地点 `imageSrc`
- 分幕 `backgroundImageSrc`
- 分幕 `backgroundAssetId`
这样后续再从 session 重编译结果页 profile 时,最新资产字段不会再被旧 draft 值回退。
## 6. 验证补充
本轮补了服务端测试:
- `server-node/src/services/customWorldAgentPhase4.test.ts`
新增验证点:
1. `sync_result_profile` 后,最新角色主图会写回 draft
2. 最新角色动作资产字段会写回 draft
3. 最新地点图会写回 draft
4. 最新分幕图会写回 draft
## 7. 结论
截至本轮修复后,当前创作流程里:
1. 草稿文本修改可以自动保存到数据库
2. 结果页中确认后的角色图、地点图、分幕图可以随自动保存稳定进入数据库
3. 角色动作相关资产字段可以随 session 同步和自动保存稳定保留
但仍需注意:
1. 当前仍是阶段一兼容链路,核心桥接字段仍然是 `legacyResultProfile`
2. 正式发布链 `publish_world` 还没有在当前阶段打通
3. 前端仍依赖 `buildCustomWorldProfileFromAgentDraft()` 作为 session -> 结果页兼容编译层
因此本轮结论是:
**当前“前端修改 -> 自动保存 -> 数据库”主链可用;本次已补上图片与动作资产在 session 重编译阶段的回退风险。**

View File

@@ -1,212 +0,0 @@
# Agent 结果页深度编辑回写主链方案(阶段一)
更新时间:`2026-04-20`
## 1. 这次阶段一先改什么
这次阶段一不做结果页只读化。
结果页继续保留当前已经可用、而且用户已经满意的这些能力:
1. 结果页继续允许深度编辑世界设定
2. 结果页继续允许编辑角色、场景、营地、封面
3. 结果页继续允许直接新增角色与地点
4. 结果页继续保留当前已有的浏览、自动保存、进入世界体验
这次真正要补的是:
**把结果页里产出的完整 `CustomWorldProfile`,同步回 `Agent session`,让结果页编辑不再游离在主链之外。**
---
## 2. 当前真正的问题
当前链路里,结果页虽然还能深度编辑,但数据职责是分裂的:
```text
Agent session
-> 前端 buildCustomWorldProfileFromAgentDraft()
-> 结果页本地 profile
-> 结果页继续深度编辑
-> 自动保存到 custom-world-library
-> 进入世界
```
这里最大的问题不是“结果页能编辑”,而是:
1. 结果页编辑后的最新世界结构,没有稳定回写到 `Agent session`
2. 用户从结果页返回 Agent 工作区后session 侧仍可能停留在较旧的草稿状态
3. “结果页当前看到的世界”“Agent session 当前保存的草稿”“作品库里自动保存的 profile”可能不是同一份东西
4. 进入世界时如果直接吃当前前端内存态,也会继续放大这个分叉
所以阶段一要解决的是:
**结果页仍然是深度编辑器,但它编辑的是 Agent 主链里的当前结果快照,不是脱链的本地副本。**
---
## 3. 阶段一目标状态
阶段一把链路先收成下面这样:
```text
Agent session
-> 前端 buildCustomWorldProfileFromAgentDraft() 生成结果页初始 profile
-> 用户在结果页继续深度编辑 profile
-> 前端调用新的 Agent action把完整结果 profile 同步回 session
-> session 保留:
- 当前 foundation draft
- 当前 legacyResultProfile 结果快照
- 重编译后的 draftCards / assetCoverage / suggestedActions
-> 自动保存与进入世界都优先基于已同步的 session 结果快照执行
```
这一步仍然是过渡态,不是最终态。
因为:
1. 阶段一还不打通 `publish_world`
2. 阶段一也不把结果页改造成完全原生的 draft 编辑器
3. 阶段一允许继续保留 `draftProfile.legacyResultProfile` 作为兼容桥接字段
但至少要做到:
**结果页的深度编辑,必须进入 Agent session 的单一主链。**
---
## 4. 阶段一具体实现边界
## 4.1 新增 Agent action`sync_result_profile`
阶段一新增一个面向结果页的 Agent action
```ts
{ action: 'sync_result_profile'; profile: CustomWorldProfileRecord }
```
用途只有一个:
把结果页当前完整 `CustomWorldProfile` 快照同步回 `CustomWorldAgentSessionRecord`
它不是发布动作,也不是世界编译动作。
它只是把结果页当前编辑结果认回主链。
---
## 4.2 服务端写回策略
服务端接到 `sync_result_profile` 后,按下面规则处理:
1. 读取当前 session
2. 取当前 `draftProfile`
3. 保留当前 draft 层已有的结构化字段:
- `playableNpcs / storyNpcs / landmarks / camp`
- `factions / threads / chapters / sceneChapters`
- `worldHook / playerPremise / openingSituation / iconicElements`
- 以及现有资产、scene chapter 等字段
4. 把结果页传来的完整 `CustomWorldProfile` 写入 `draftProfile.legacyResultProfile`
5. 对于 draft 层里本来就和结果页一一对应、且结果页已经改动的字段,同步覆盖基础摘要字段:
- `name`
- `subtitle`
- `summary`
- `tone`
- `playerGoal`
- `majorFactions`
- `coreConflicts`
6. 重新编译 `draftCards`
7. 重建 `assetCoverage`
8. 刷新 `suggestedActions`
9. 写入 action result message 和 checkpoint
这里故意不在阶段一做“把完整 runtime profile 反解成一整套全量 foundation draft 结构”的大重构。
原因是:
1. 结果页当前已经支持很多深度编辑字段
2. 如果现在硬做全量反编译,最容易把场景章节、多幕、资产字段写坏
3. 阶段一应该先保证“结果页编辑不脱链”,而不是一次性重做所有模型映射
---
## 4.3 前端触发策略
前端只在 `customWorldResultViewSource === 'agent-draft'` 时走这条同步链。
具体规则:
1. 结果页 profile 每次发生变化时,继续允许本地即时更新
2. 但在自动保存前,先把 profile 通过 `sync_result_profile` 同步到 Agent session
3. 返回创作时,如果要重新读 Agent 草稿,也应优先以最新 session 为准
4. 点击“进入世界”时,先拉取最新 session再重新 `buildCustomWorldProfileFromAgentDraft()`,避免吃到旧的前端缓存 profile
这样阶段一就能做到:
1. 结果页编辑体验不变
2. Agent session 成为结果页编辑后的可恢复真相源
3. 自动保存、返回创作、进入世界三条路都围绕同一份 session-backed 结果快照
---
## 5. 阶段一明确不做什么
这次阶段一明确不做:
1. 不关闭结果页当前已有的编辑器能力
2. 不删除结果页当前已有的 AI 新增角色/地点能力
3. 不打通 `publish_world`
4. 不把 `legacyResultProfile` 直接删掉
5. 不把结果页整个改写成只操作 draft card 的新系统
6. 不把旧 `custom-world/sessions` 链在本阶段直接物理移除
---
## 6. 验收标准
阶段一做完后,至少要满足下面这些结果:
1. Agent 草稿结果页继续保持当前深度编辑体验不变
2. 结果页发生编辑后Agent session 中能看到同步后的最新结果快照
3. 从结果页返回创作后,不会明显回退到较旧的草稿态
4. 点击“进入世界”时,会优先使用最新 session 重新编译结果,而不是只依赖前端旧内存态
5. 自动保存到作品库的 profile 与当前 session 结果快照保持一致
---
## 7. 一句话结论
阶段一不是收掉结果页,而是把结果页继续保留为深度编辑器,同时补上一条正式的 session 回写链,让它不再游离在 Agent 主链之外。
---
## 8. 2026-04-20 实际落地结果
本轮已经按阶段一目标完成下面这些收口:
1. 前端结果页自动保存时,若当前来源是 `agent-draft`,会先执行 `sync_result_profile`
2. `sync_result_profile` 完成后,自动保存不再直接写旧的前端内存 profile而是优先保存从最新 session 重新 `buildCustomWorldProfileFromAgentDraft()` 得到的结果快照
3. 点击“进入世界”时,仍会先同步 session再基于最新 session 重编译 profile 后进入世界
4. 点击“返回创作”时,也会先做一次结果页到 session 的同步兜底,再返回 Agent 工作区
5. 为避免用户刚从结果页返回工作区又被自动重开逻辑顶回结果页,前端补了一层显式返回抑制标记
6. 服务端 `sync_result_profile` 现已按阶段一边界收窄为“保留 foundation draft 结构,只更新基础摘要字段和 `legacyResultProfile`”,没有提前做整套 runtime -> draft 反解
这意味着阶段一当前已经把下面三条路径收回到同一条 session 主链:
1. 自动保存到作品库
2. 返回 Agent 工作区继续创作
3. 从结果页直接进入世界
## 9. 本轮仍然保留的阶段性边界
这次落地后,仍然保留文档原先约定的过渡边界:
1. 结果页深度编辑能力不做收缩
2. `draftProfile.legacyResultProfile` 继续作为兼容桥接字段保留
3. `publish_world` 仍未在这一轮打通
4. 前端仍然使用 `buildCustomWorldProfileFromAgentDraft()` 作为 session -> 结果页的兼容编译层
所以下一阶段如果要继续推进,重点应转向:
1. 降低前端对 legacy profile 编译桥接的依赖
2. 继续把发布链路收口到 Agent session / service 侧
3. 逐步缩减结果页直改 legacy profile 的历史职责

View File

@@ -1,74 +0,0 @@
# Agent 结果页与平台入口收口方案(阶段二)
更新时间:`2026-04-20`
## 1. 阶段二目标
阶段一已经把 Agent 结果页编辑快照同步回 session 主链。阶段二不继续扩大结果页编辑能力,而是把入口和职责继续收紧:
1. 平台“创作”入口统一读取 `custom-world/works` 聚合列表
2. Agent 草稿和已保存作品在同一个入口里展示
3. 草稿点击后恢复 Agent session已保存作品点击后进入作品详情
4. Agent 结果页不再暴露“继续在结果页补世界结构”的新增入口
一句话目标:
**让用户从平台创作入口能稳定找回草稿和作品,同时让结果页更像收口预览,而不是另一套编辑器。**
---
## 2. 本阶段不做什么
阶段二明确不做:
1. 不物理删除旧 `custom-world/sessions`
2. 不打通 `publish_world`
3. 不重做结果页 UI
4. 不删除已保存作品的继续编辑入口
5. 不把结果页整体改成只读
这些事项留给后续阶段继续拆。
---
## 3. 平台入口落地规则
平台“创作”Tab 改为优先展示 `listCustomWorldWorks()` 的聚合结果:
1. `agent_session` 类型展示为草稿,可点击恢复 Agent 工作区
2. `published_profile` 类型展示为作品,可点击进入作品详情
3. 聚合接口失败时保留现有作品库 `myEntries` 兜底
4. 不新增平行页面,复用已有 `CustomWorldCreationHub`
这样用户不再需要依赖隐藏 sessionId 或旧作品库入口才能找回创作。
---
## 4. 结果页职责收口规则
Agent 来源结果页继续保留:
1. 浏览世界、角色、场景
2. 自动保存
3. 返回 Agent 工作区
4. 进入世界
Agent 来源结果页本阶段收紧:
1. 不再显示直接新增可扮演角色、场景角色、场景的入口
2. 不再把“去 Agent 调整设定”设计成结果页内部继续补世界结构
3. 如需继续调整,返回 Agent 工作区
已保存作品的结果页仍保持现有编辑能力,避免破坏作品库已有体验。
---
## 5. 验收标准
阶段二完成后应满足:
1. 平台“创作”Tab 能看到 Agent 草稿和已保存作品的统一列表
2. 点击 Agent 草稿能恢复对应 Agent 工作区
3. 点击已保存作品能进入原有作品详情
4. Agent 结果页不再显示直接新增角色/地点的入口
5. 已保存作品的结果页编辑能力不受影响

View File

@@ -1,148 +0,0 @@
# Agent 结果页旧链降级与预览冻结方案(阶段三)
更新时间:`2026-04-20`
## 1. 阶段三目标
阶段一已经把结果页编辑同步回 Agent session 主链。
阶段二已经把平台“创作”入口统一到 `custom-world/works` 聚合列表,并收紧了 Agent 结果页里的新增入口。
阶段三不继续扩功能,而是继续做两件事:
1. 让旧 pipeline 在主入口里进一步降级,不再和 Agent 主链抢“草稿”职责
2. 让 Agent 来源结果页进一步冻结为“预览/收口层”,不再继续承担 legacy profile 直改编辑器职责
一句话目标:
**把还在和 Agent 主链并行的旧职责继续降级,避免系统自己和自己打架。**
---
## 2. 当前剩余问题
虽然阶段一、二已经把主链收紧了不少,但当前还保留两个明显的并行口:
### 2.1 创作中心里旧 library 草稿仍可能继续冒充主草稿
当前 `listCustomWorldWorkSummaries()` 会把 runtime library 里的所有 profile 都折成 `published_profile` 类型返回。
这意味着:
1. `visibility = 'draft'` 的 library 草稿仍会继续出现在创作中心
2. 创作中心里同时存在:
- Agent session 草稿
- library 草稿
- 已发布作品
3. 用户看到的“草稿”概念仍然可能混成两套
阶段三需要明确:
**创作中心主入口只认 Agent session 草稿 和 已发布作品,不再继续把 library draft 当主草稿展示。**
---
### 2.2 Agent 结果页仍能继续打开旧 legacy 编辑器
当前 Agent 来源结果页虽然已经不再暴露“新增角色/新增地点”入口,但仍然保留下面这些旧编辑链:
1. 点击世界概述/基本设定仍能打开 legacy world editor
2. 点击角色、场景、封面仍能继续进入旧 profile 编辑弹窗
3. 这些编辑器本质上仍然是在改 legacy `CustomWorldProfile`
这会带来两个问题:
1. Agent 结果页继续像一套“旧编辑器”
2. “去 Agent 调整设定”和“结果页直接改 legacy profile”两条路仍然并行存在
阶段三需要明确:
**Agent 来源结果页继续保留浏览、自动保存、返回创作、进入世界,但不再继续承担 legacy profile 深编辑职责。**
---
## 3. 阶段三落地规则
## 3.1 创作中心只展示两类主入口内容
`custom-world/works` 在阶段三只保留下面两类条目:
1. `agent_session`
- 统一视为草稿
- 点击后恢复 Agent 工作区
2. `published_profile`
- 统一视为已发布作品
- 点击后进入现有作品详情
明确不再把下面这类内容继续塞进创作中心主入口:
1. library 中 `visibility = 'draft'` 的兼容草稿
这些兼容草稿仍然保留在作品库/详情链路里,不在本阶段物理删除,但不再继续占创作中心“草稿主入口”。
---
## 3.2 Agent 来源结果页冻结为预览态
`customWorldResultViewSource === 'agent-draft'` 时,结果页阶段三继续保留:
1. 浏览世界信息
2. 浏览角色、地点、场景结构
3. 自动保存
4. 返回 Agent 工作区
5. 进入世界
同时阶段三进一步收紧:
1. 不再打开世界/角色/场景/封面的 legacy 编辑弹窗
2. 不再提供删除角色、删除场景等旧 profile 直改入口
3. Agent 来源结果页上的对象卡统一作为“查看详情”预览卡使用
已保存作品的结果页编辑能力继续保留,不在本阶段收缩,避免破坏已有作品库编辑体验。
---
## 3.3 结果页同步动作只在真的发生差异时执行
阶段一补的 `sync_result_profile` 仍然保留,但阶段三补一个行为约束:
1. 如果当前 Agent 结果页 profile 和最新 session 重编译结果签名一致
2. 那么返回创作、进入世界、自动保存前不再重复触发一次 `sync_result_profile`
目的不是省接口,而是明确:
**结果页同步是“有改动才回写”的主链动作,不是每次离开页面都机械重放。**
---
## 4. 阶段三明确不做什么
这次阶段三明确不做:
1. 不物理删除旧 `custom-world/sessions` 相关服务与兼容代码
2. 不打通 `publish_world`
3. 不把前端 `buildCustomWorldProfileFromAgentDraft()` 兼容编译层移除
4. 不删除 `draftProfile.legacyResultProfile`
5. 不收缩已保存作品的 legacy 编辑器能力
阶段三只做主入口降级与 Agent 结果页职责冻结,不做更大的模型替换。
---
## 5. 验收标准
阶段三完成后应满足:
1. 创作中心不再把 library draft 兼容作品继续显示为“草稿主入口”
2. 创作中心里只保留 Agent 草稿和已发布作品两类主入口内容
3. Agent 来源结果页不再能继续打开 legacy 世界/角色/场景编辑弹窗
4. 已保存作品结果页编辑能力不受影响
5. Agent 结果页在未发生改动时,返回创作/进入世界/自动保存不会重复触发无意义的 `sync_result_profile`
---
## 6. 一句话结论
阶段三不是删除兼容层,而是把它们继续降级到不会抢主流程职责的位置上:
**创作中心只认 Agent 草稿和已发布作品Agent 结果页只负责预览与收口,不再继续充当旧编辑器。**

View File

@@ -1,53 +0,0 @@
# AI 生成过程草稿持久化设计2026-04-24
## 1. 背景
当前创作类模板已经具备 session / message / operation 级别的最终态落库能力,但部分流式生成只把模型增量推给前端。若 HTTP/SSE 连接、浏览器页面或 LLM 请求在最终解析前中断,用户只能看到短暂流式文本,服务端缺少可恢复的生成中间态。
本设计补齐“生成过程中已经生成的内容必须持续持久化”的机制,并要求该机制对所有创作模板统一生效。
## 2. 目标
1. 每次模板生成开始前创建或绑定一个 `ai_task`
2. 模型每次产出可见文本增量时,写入 `ai_text_chunk`,并同步更新 `ai_task.latest_text_output` 与对应 stage 的 `text_output`
3. 生成失败或连接中断时,不丢弃已经落库的 chunk后续可用 `ai_task.latest_text_output` 作为续写上下文。
4. 成功解析并 finalize 后,将最终结构化结果继续写回各模板原有 session 表,保持现有业务快照不变。
## 3. 统一落库边界
### 3.1 真相表
- `ai_task`:记录一次模板生成任务的业务来源、状态、最新聚合文本、结构化结果。
- `ai_task_stage`:记录模板生成阶段状态;当前创作对话统一使用 `DraftGeneration`
- `ai_text_chunk`:按 `sequence` 追加保存模型增量文本,是断点恢复的最小粒度。
### 3.2 适用模板
- 自定义世界创作 Agent。
- 解谜游戏创作 Agent。
- 大鱼吃小鱼创作 Agent。
- 后续新增模板必须复用同一生成草稿持久化工具,不允许只在 UI 内存保存流式文本。
## 4. 续写策略
1. 发起生成时,后端根据 `template_key + session_id + operation_id` 创建稳定 `task_id`
2. LLM 流式回调收到 `replyText` 的最新可见文本后,计算相对上一次文本的增量;只有非空增量写入 `ai_text_chunk`
3. 写入失败不应阻断当前生成主流程,但必须记录 warn 日志,避免因持久化瞬时失败导致用户生成直接失败。
4. 若最终解析失败,`ai_task` 保持 `Running` 或显式 `Failed`,已写入的 `latest_text_output` 仍可作为下一轮 prompt 的“已生成草稿”。
5. 下一轮续写 prompt 应优先带上最近未完成任务的 `latest_text_output`;本次先落地服务端 chunk 持久化能力,后续模板 prompt 可逐步消费该草稿。
## 5. 编码要求
1. 持久化逻辑放在 `server-rs/crates/api-server` 的通用工具中,由各模板路由接入。
2. 不引入 `server-node` 兼容分支。
3. SpacetimeDB 写入必须通过 `spacetime-client` 已生成绑定,不在 reducer 中访问网络或文件系统。
4. 所有新增 Rust 代码保留中文注释,且只做局部修改,避免重写包含中文的大文件。
## 6. 失败排查原文日志
1. RPG 草稿生成链路的模型输入与模型输出原文日志统一收口在 `platform-llm` 网关层,避免每个模板调用点重复实现。
2. 只有发生请求失败、上游非 2xx、响应读取失败、JSON/SSE 解析失败或空响应时,才将本次模型输入与已拿到的模型输出原文分别写入文件;正常成功生成不默认落盘原文,避免日志体积不可控。
3. 日志目录默认使用仓库运行目录下的 `logs/llm-raw`,可通过 `LLM_RAW_LOG_DIR` 覆盖;每次失败写成同一 trace 前缀下的 `*.input.json``*.output.txt` 两个 UTF-8 文件。
4. `*.input.json` 记录 provider、model、stream、attempt、maxTokens 与完整 messages`*.output.txt` 记录上游 HTTP 原文、非流式响应原文、SSE 原始事件文本,或请求尚未到达上游时的错误摘要。
5. 文件名只使用时间戳、进程号、递增序号与安全化错误阶段不包含用户输入、sessionId 或 API key输入 JSON 不写入 API key。
6. 文件日志失败只写 warn不影响草稿生成主错误返回该日志仅用于本地开发与排障不作为 SpacetimeDB 真相态。

View File

@@ -1,647 +0,0 @@
# 阿里云 NPC 角色形象与动作动画编辑器实验方案2026-04-07
## 1. 文档目的
本文不是再写一份泛化的“AI 角色动画大方案”,而是专门回答当前编辑器里要怎么实验这条链路:
- 接入阿里云百炼的文生图、图生图、图生视频、参考视频动作模型
-**NPC 角色形象 + 动作动画资产化** 为目标
- 最终产物仍然要落回当前项目的 `CharacterAssetPanel -> publish -> CharacterAnimator`
本文把方案拆成 4 条实验线:
1. 先文生角色形象图,再图生动作序列帧图并解析
2. 先文生角色形象图,再图生视频
3. 先文生角色形象图,再走“参考视频驱动”的动作模板链
4. 先文生角色形象图,再走“参考生视频 / 剧情演出”链
查阅与核对时间:`2026-04-07`
---
## 1.1 当前实现状态2026-04-07
当前仓库已经把下面这些能力接进 `CharacterAssetPanel`
- 阶段 A`wan2.7-image-pro / wan2.7-image` 主形象候选生成
- 阶段 B4 条动作方案都已接入真实模型
- 阶段 C方案四单独拆成“演出片段”预览区
- 方案三增加了“内置模板库”入口,可直接把项目现有角色序列帧合成为参考视频
- 最近一次主形象任务 / 动作任务状态会回显到编辑器
- 已补动作模板列表接口与视频导入接口
当前实现的本地接口为:
- `POST /api/character-visual/generate`
- `GET /api/character-visual/jobs/:id`
- `POST /api/character-visual/publish`
- `POST /api/animation/generate`
- `GET /api/animation/jobs/:id`
- `GET /api/animation/templates`
- `POST /api/animation/import-video`
- `POST /api/animation/publish`
当前视频后处理采用:
- 模型端生成真实视频
- 浏览器端抽帧、缩放、简单绿幕抠像
- 发布阶段再写入 `public/generated-animations`
也就是说,这份文档里原先一些“推荐下一步”已经落地,但还有一部分“更重的任务化路由”尚未继续拆开。
---
## 2. 当前仓库里的可复用基础
这次实验不应该另起炉灶,因为仓库里已经有 3 个很关键的基础。
### 2.1 编辑器入口已经存在
- 路由 `/character-asset-studio` 已经接到 `PresetEditor`,说明“角色资产工坊”入口是现成的。
- 当前核心页面是 `src/components/preset-editor/CharacterAssetPanel.tsx`
### 2.2 主形象 / 动作两段式 UI 已经存在
当前 `CharacterAssetPanel` 已经分成:
- 阶段 A主形象
- 阶段 B基础动作
- 阶段 C演出片段
旧版本里生成逻辑确实是本地 mock
- 主形象候选来自 `buildVisualCandidatesFromSource`
- 动作草稿来自 `buildAnimationClipFromMaster`
现在这层已经被真实模型链路替换,但仍然保留了这些本地能力作为后处理工具:
- 参考视频模板合成
- 视频抽帧
- 简单绿幕抠像
- 生成发布用帧集
### 2.3 本地 API 插件里已经有 DashScope 接入样板
本文撰写时,旧 `scripts/dev-server/localApiPlugins.ts` 里已经接了自定义世界场景图。
截至 `2026-04-19`,该文件已从仓库删除,对应样板能力应改为参考 `server-node/src/modules/assets/**``server-node/src/modules/ai/**`
- 默认 DashScope base URL 已经存在
- 已经有异步任务创建、轮询、下载、落盘、写 manifest 的完整样板
这意味着这次实验最合理的做法是:
- 继续沿用 `/api/*` 本地代理模式
- 新增角色图 / 角色动作的 job 路由
- 复用现有的任务轮询和文件落盘思路
---
## 3. 阿里云当前可直接利用的模型能力
基于 2026-04-07 查阅的阿里云官方文档,当前和本实验最相关的是下面几类能力。
| 能力 | 推荐模型 | 适合用途 | 备注 |
| --- | --- | --- | --- |
| 文生图 / 图生图 / 图像编辑 | `wan2.7-image-pro``wan2.7-image` | 生成 NPC 主形象图、做风格统一、生成组图候选 | 官方文档明确支持多图参考与组图输出 |
| 图生视频 | `wan2.7-i2v` | 单角色主形象转动作视频 | 支持首帧、首尾帧、续写片段 |
| 参考生视频 | `wan2.7-r2v``wan2.6-r2v-flash` | 多参考图/参考视频驱动剧情演出 | 更适合演出,不是最优基础动作线 |
| 图生动作 | `wan2.2-animate-move` | 主形象 + 参考动作视频 -> 标准动作视频 | 动作控制更强,适合模板动作库 |
| 视频换人 | `wan2.2-animate-mix` | 模板视频里的角色替换成 NPC 形象 | 适合动作模板“复刻” |
需要特别说明:
- 方案一会用到 `wan2.7-image-pro` 的组图 / 顺序组图能力,但 **官方并没有把它定义为“动作逐帧模型”**
- 所以方案一是“利用图像模型能力去逼近动作帧生产”的实验线,不是官方标准动作生产线。
- 方案二、三、四更贴近阿里云官方为视频生成准备的主线能力。
---
## 4. 方案一:文生角色形象图 -> 图生动作序列帧图 -> 解析成动画
## 4.1 目标
直接得到 `png` 帧集,尽量少碰视频编解码。
## 4.2 模型链路
1.`wan2.7-image-pro` 生成 NPC 主形象图
2. 再把主形象图作为参考图输入 `wan2.7-image-pro`
3. 对每个动作槽位生成一组候选图片
4. 打开组图输出,必要时启用 `enable_sequential`
5. 本地按动作顺序解析这些图,写回帧序列
## 4.3 为什么它成立
阿里云图像生成与编辑 API 当前明确支持:
- 文生图
- 图生图
- 多图参考
- 一次输出多张图
- 顺序组图输出 `enable_sequential`
因此可以在编辑器里做这样的实验:
- 输入:主形象图 + 动作描述 + 固定 seed
- 输出:同一动作的一组关键帧候选
- 后处理:按姿态差异、角色一致性、武器完整度排序,补成帧集
## 4.4 编辑器里的具体玩法
建议在当前“阶段 B基础动作”里加一个策略选项
- `帧序列实验(图像组图)`
每次动作生成时:
1. 选择动作槽位,如 `idle / run / attack / hurt`
2. 选择目标帧数,如 `4 / 6 / 8`
3. 传入主形象图
4. 拼出动作提示词,例如“同一角色,侧身朝右,单人,全身,武器完整,连续 6 帧,跑步动作,从预备到迈步再到回收”
5. 请求组图结果
6. 本地做帧序评分
7. 生成 `frames/*.png + manifest.json`
## 4.5 优点
- 直接产出图片,天然适合当前项目的帧资产结构
- 不需要先生成视频再解帧
- 某些短动作可以直接人工挑帧,编辑器可控性高
-`idle``acquire``hurt` 这种短动作实验门槛较低
## 4.6 风险
- 最大风险是帧间一致性,特别容易出现衣摆、武器、手部、头发抖动
- 组图的“顺序性”不等于真正的视频时序连续性
- `run``jump``dash` 这类长动作很可能不稳定
- 如果没有额外姿态评分和人工筛选,最后帧序会很跳
## 4.7 结论
这是 **低基础设施成本、高人工筛选成本** 的方案。
适合:
- 编辑器里先做原型实验
- 验证 NPC 主形象一致性能不能维持到多帧
- 生成短动作关键帧
不适合直接作为第一版唯一主线。
---
## 5. 方案二:文生角色形象图 -> 图生视频 -> 解帧资产化
## 5.1 目标
先让视频模型负责动作连续性,再由本地后处理把视频转成项目动画资产。
## 5.2 模型链路
1.`wan2.7-image-pro` 生成 NPC 主形象图
2.`wan2.7-i2v` 基于主形象图生成动作视频
3. 下载视频结果
4. 本地抽帧
5. 做裁切、稳帧、像素化、去闪烁
6. 输出序列帧、Sprite Sheet、manifest
## 5.3 方案二里的两种子模式
### A. 首帧生视频
适合:
- `attack`
- `hurt`
- `die`
- `cast`
特点:
- 主形象图作为 `first_frame`
- 文本控制动作
- 最快接入,链路最短
### B. 首尾帧生视频
适合:
- `idle`
- `run`
- 循环站姿
特点:
- `first_frame` 是起始站姿
- `last_frame` 是回正后的收尾姿态
- 更利于做循环动作和回到可衔接状态
## 5.4 编辑器里的具体玩法
建议在“阶段 B基础动作”里加
- `图生视频(首帧)`
- `图生视频(首尾帧)`
参数建议:
- 时长:`2s / 3s / 4s`
- 目标 FPS先统一导入到本地后再重采样
- 循环动作:是否要求首尾近似
- 提示词模板:按动作槽位固化
## 5.5 优点
- 动作连续性通常明显强于方案一
- `wan2.7-i2v` 是官方主线能力,兼容性和迭代空间更好
- 很适合作为当前编辑器的第一条“真实动作生成”主线
- 本地后处理完成后,仍然能回到当前项目的帧资源体系
## 5.6 风险
- 需要稳定的视频后处理链
- 解帧后仍要处理轮廓闪烁、脚底漂移、武器变形
- 主形象复杂时,单图生视频可能会有角色漂移
- 相比方案一I/O 和处理耗时更高
## 5.7 结论
这是 **最适合作为编辑器第一版正式实验主线** 的方案。
原因:
- 模型能力更贴近官方主线
- 动作连续性通常更稳定
- 生成结果仍可资产化
---
## 6. 方案三:文生角色形象图 -> 参考视频驱动动作模板链
## 6.1 目标
不是只靠文本“想象动作”,而是给动作一个明确模板视频,让模型做可控迁移。
## 6.2 模型链路
推荐两条可选子线:
### A. `wan2.2-animate-move`
输入:
- NPC 主形象图
- 参考动作视频
输出:
- NPC 执行该动作的视频
### B. `wan2.2-animate-mix`
输入:
- NPC 主形象图
- 模板视频
输出:
- 保留模板视频场景/动作,但把角色替换成 NPC
## 6.3 它和方案二的本质区别
方案二是:
- 主形象图 + 文本描述 -> 视频
方案三是:
- 主形象图 + 模板动作视频 -> 视频
因此方案三最大的价值不是“更自由”,而是“更可控”。
## 6.4 编辑器里的具体玩法
在“阶段 B基础动作”里新增
- `动作模板库`
每个动作槽位先配一份官方/自制模板:
- `idle_loop`
- `run_side`
- `attack_slash`
- `hurt_back`
- `die_fall`
工作流:
1. 先锁定 NPC 主形象
2. 选择动作槽位
3. 选择一个模板视频
4. 调用 `animate-move``animate-mix`
5. 下载视频
6. 解帧、稳帧、裁切
7. 发布为该动作槽位正式资产
## 6.5 优点
- 可控性明显高于纯文本图生视频
- 非常适合做“基础动作槽位不能为空”的项目要求
- 一旦模板库建立起来,多角色批量复用效率很高
-`run``attack``hurt` 这种标准动作尤其友好
## 6.6 风险
- 要先建设动作模板库
- `wan2.2-animate-move` 官方输入更偏“单人清晰主体”,对严格侧视游戏素材要额外测试
- 模板视频如果镜头、背景、构图不统一,后处理成本会增加
- 模板库前期准备成本高于方案二
## 6.7 结论
这是 **最适合做战斗基础动作标准化生产** 的方案。
如果只看“当前项目需要补齐 `idle / run / attack / hurt / die` 这些基础槽位”,方案三的长期价值甚至高于方案二。
建议排序:
- 第一阶段先做方案二跑通链路
- 第二阶段尽快把方案三补成稳定模板库主线
---
## 7. 方案四:文生角色形象图 -> 参考生视频 / 剧情演出链
## 7.1 目标
这条线不是优先服务“战斗基础动作”,而是服务:
- 剧情演出
- 招募演出
- NPC 说话/表态
- 立绘转小段表演视频
## 7.2 模型链路
推荐:
- `wan2.7-r2v`
- 成本敏感或无声短片可考虑 `wan2.6-r2v-flash`
参考生视频支持把图片、视频作为参考条件输入,再结合文本生成视频。
## 7.3 它和方案三的区别
方案三更像:
- 我已经知道动作模板,就要把它迁过去
方案四更像:
- 我给你角色参考和演出参考,请你生成一段新的镜头表达
所以它更适合:
- NPC 出场特写
- 对话演出
- 剧情镜头
- 情绪表演
不适合优先用于:
- 项目所有基础战斗动作槽位
## 7.4 编辑器里的具体玩法
当前已单独拆成:
- `演出片段`
字段建议:
- 角色主形象
- 参考图最多若干张
- 参考视频片段
- 台词或情绪提示
- 是否保留音频
输出:
- `preview.mp4`
- 关键帧截图
- 可选封面图
## 7.5 优点
- 角色一致性上限更高
- 更适合做剧情演出而不是纯动作片段
- 后续和 `CharacterChatModal`、NPC 招募、事件特写更容易联动
## 7.6 风险
- 对当前战斗帧资产体系帮助没有前三条直接
- 更容易产出“好看的视频”,但不一定容易切成稳定序列帧
- 这条线如果过早投入,会稀释基础动作资产生产的主线
## 7.7 结论
这是 **剧情演出增强线**,不建议抢在方案二、三之前做。
---
## 8. 四种方案横向对比
| 方案 | 动作连续性 | 可控性 | 资产化难度 | 适合基础动作 | 适合剧情演出 | 推荐阶段 |
| --- | --- | --- | --- | --- | --- | --- |
| 方案一:组图帧序列 | 低到中 | 中 | 低到中 | 中 | 低 | 研究线 |
| 方案二:图生视频 | 中到高 | 中 | 中到高 | 高 | 中 | 第一阶段主线 |
| 方案三:模板视频驱动 | 高 | 高 | 中到高 | 很高 | 中 | 第一阶段后半 / 第二阶段主线 |
| 方案四:参考生视频 | 中到高 | 中到高 | 高 | 中 | 很高 | 第三阶段增强 |
一句话总结:
- 要最快落地:先做 **方案二**
- 要把基础动作做稳:尽快补 **方案三**
- 要低成本试帧:可以并行试 **方案一**
- 要做剧情镜头:后续再做 **方案四**
---
## 9. 面向当前编辑器的落地状态与下一步
## 9.1 第一轮
这一轮已经完成:
- 阶段 A`wan2.7-image-pro` 主形象生成
- 阶段 B`wan2.7-i2v` 图生视频
原因:
- 最少改 UI
- 最快复用当前 `CharacterAssetPanel`
- 最容易复用现已迁入 `server-node` 的 DashScope 异步任务模式
## 9.2 第二轮
这一轮已经完成:
- `图生视频`
- `模板视频驱动`
- `帧序列实验`
并且已经补上:
- 方案三的内置模板库入口
- 方案四的独立“演出片段”区
## 9.3 第三轮
下一步仍然值得继续做的是:
- 把当前同步 `generate` 继续拆成显式 `jobs`
- 把视频导入后处理继续拆成独立 `import-video`
- 给方案三补更多正式模板素材与模板清单管理
- 给方案四补关键帧归档、封面和片段列表
---
## 10. 推荐的编辑器任务路由
当前已落地接口:
- `POST /api/character-visual/generate`
- `GET /api/character-visual/jobs/:id`
- `POST /api/character-visual/publish`
- `POST /api/animation/generate`
- `GET /api/animation/jobs/:id`
- `GET /api/animation/templates`
- `POST /api/animation/import-video`
- `POST /api/animation/publish`
当前职责:
### `POST /api/character-visual/generate`
负责:
-`wan2.7-image-pro`
- 生成主形象候选
- 下载并落盘
- 返回草稿图路径
### `GET /api/character-visual/jobs/:id`
负责:
- 返回最近一次主形象任务状态
- 返回模型、提示词、结果草稿等任务记录
### `POST /api/animation/generate`
负责:
- 按策略调不同模型
- `i2v`
- `animate-move`
- `animate-mix`
- `r2v`
- 返回顺序组图或视频草稿
### `GET /api/animation/jobs/:id`
负责:
- 返回最近一次动作任务状态
- 返回策略、模型、输出草稿路径和错误信息
### `GET /api/animation/templates`
负责:
- 返回方案三内置模板库清单
- 供编辑器选择 `idle_loop / run_side / attack_slash / hurt_back / die_fall`
### `POST /api/animation/import-video`
负责:
- 把浏览器侧生成或上传的视频导入本地草稿目录
- 返回可复用的本地视频路径
### `POST /api/animation/publish`
负责:
- 把草稿帧写入 `public/generated-animations`
- 生成动作 manifest
- 更新 `characterOverrides.json`
### 仍建议后续继续加强的部分
- 把当前“同步 generate + 立即返回结果”继续拆成更完整的异步 job 生命周期
-`import-video` 增加更重的服务端后处理,而不只是导入草稿
- 给模板库补正式素材管理与模板清单编辑
---
## 11. 第一批建议验证的动作
不要一上来就跑全量 12 个基础动作,先验证 4 个最关键动作:
1. `idle`
2. `run`
3. `attack`
4. `hurt`
原因:
- 这 4 个已经能覆盖循环动作、位移动作、攻击动作、受击动作
- 最容易测出“主形象一致性 + 动作连续性 + 贴地稳定性”
---
## 12. 具体推荐结论
如果只给当前编辑器实验一个最务实的建议:
1. **主形象统一先接 `wan2.7-image-pro`**
2. **动作第一条真链路先接方案二:`wan2.7-i2v`**
3. **基础动作标准化的主线尽快切到方案三:`wan2.2-animate-move / animate-mix`**
4. **方案一保留为低成本帧序实验线,方案四保留为剧情演出增强线**
换句话说:
- 方案二负责“尽快跑通”
- 方案三负责“真正稳定生产”
- 方案一负责“低成本试错”
- 方案四负责“后续演出升级”
---
## 13. 资料来源
阿里云官方文档:
- 图像生成与编辑 API 参考:
[https://help.aliyun.com/zh/model-studio/wan-image-generation-and-editing-api-reference](https://help.aliyun.com/zh/model-studio/wan-image-generation-and-editing-api-reference)
- 图生视频 API 参考:
[https://help.aliyun.com/zh/model-studio/image-to-video-api-reference/](https://help.aliyun.com/zh/model-studio/image-to-video-api-reference/)
- 参考生视频 API 参考:
[https://help.aliyun.com/zh/model-studio/reference-to-video-api-reference/](https://help.aliyun.com/zh/model-studio/reference-to-video-api-reference/)
- 视频生成总览:
[https://help.aliyun.com/zh/model-studio/use-video-generation](https://help.aliyun.com/zh/model-studio/use-video-generation)
- 图生动作 API 参考:
[https://help.aliyun.com/zh/model-studio/wan-video-to-video-api-reference](https://help.aliyun.com/zh/model-studio/wan-video-to-video-api-reference)
仓库内相关代码与文档:
- `src/components/preset-editor/CharacterAssetPanel.tsx`
- `src/components/preset-editor/characterAssetStudioModel.ts`
- `src/components/preset-editor/characterAssetStudioPersistence.ts`
- `src/routing/appRoutes.tsx`
- `src/services/ai.ts`
- `server-node/src/modules/assets/**`
- `server-node/src/modules/ai/**`
- `docs/technical/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md`

View File

@@ -1,69 +0,0 @@
# 短信验证码阿里云时间戳格式修复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
```
预期:相关测试通过,格式检查通过。

View File

@@ -1,367 +0,0 @@
# 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。

View File

@@ -1,37 +0,0 @@
# API 错误 details.message 展示修复
## 背景
`POST /api/runtime/big-fish/agent/sessions/{sessionId}/actions` 在执行 `big_fish_publish_game`Rust `api-server` 会把 SpacetimeDB 发布校验失败映射为统一 API envelope
```json
{
"ok": false,
"data": null,
"error": {
"code": "BAD_REQUEST",
"message": "请求参数不合法",
"details": {
"message": "big_fish 发布校验未通过:还缺少 16 个基础动作",
"provider": "spacetimedb"
}
}
}
```
其中 `error.message` 是通用错误分类文案,`error.details.message` 才是当前业务动作的可定位失败原因。前端通用请求解析此前只读取 `error.message`,导致界面只显示“请求参数不合法”。
## 落地口径
1. 所有通过 `parseApiErrorMessage(...)` 解析的 API 错误,优先展示 `error.details.message`
2.`error.details.message` 不存在或为空时,再回退到 `error.message`
3. 当 envelope 外层也不存在有效文案时,继续沿用原有的顶层 `message`、错误码和原始响应兜底逻辑。
4. `unwrapApiResponse(...)` 处理 `ok: false` envelope 时也复用同一优先级,避免成功响应解析路径和 HTTP 非 2xx 路径展示不一致。
5. Big Fish 结果页发布失败属于阻断性动作错误,展示为独立模态窗口,不再挤在结果页内容流里,关闭后只清掉当前错误状态,不改变草稿与资源数据。
## 验收
1. `big_fish_publish_game` 返回发布校验失败时,界面应显示 `big_fish 发布校验未通过:还缺少 16 个基础动作`
2. 没有 `details.message` 的旧错误响应仍显示原 `error.message`
3. 非 JSON 错误响应仍显示原始响应文本。
4. Big Fish 结果页错误以居中模态窗口展示,并可通过关闭按钮回到结果页继续补资源。

View File

@@ -1,124 +0,0 @@
# API Server 角色主形象真实外部生成运行修复记录
日期:`2026-04-23`
## 1. 文档目的
这份文档用于记录本次为了恢复 `api-server` 角色主形象真实外部生成链路而做的最小修复项,避免后续再次出现“源码已切到真实 DashScope + OSS但实际运行的仍是旧二进制占位链”的误判。
## 2. 背景
在人工验证 `POST /api/assets/character-visual/generate` 时,运行中的本地 `api-server` 返回了 `.svg` 候选图,这与当前 `server-rs/crates/api-server/src/character_visual_assets.rs` 已切到 DashScope 真实图片生成的源码状态不一致。
进一步核查发现,问题不在角色主形象实现本身,而在于当前工作区存在若干增量改动没有补齐编译链,导致 Rust `api-server` 无法重新编译启动,本地仍在运行旧版本二进制。
## 3. 本次最小修复项
### 3.1 `spacetime-client` 缺少 `serde` 依赖
文件:
`server-rs/crates/spacetime-client/Cargo.toml`
现象:
1. 新增 `BigFishWorkSummaryRecord` 时使用了 `serde::Serialize / serde::Deserialize`
2. `Cargo.toml` 未声明 `serde`
3. 导致 `cargo check -p api-server --bin api-server` 在依赖阶段直接失败
修复:
1.`spacetime-client` 补充 `serde = { version = "1", features = ["derive"] }`
### 3.2 `password_entry` 错误映射漏掉 `InvalidPublicUserCode`
文件:
`server-rs/crates/api-server/src/password_entry.rs`
现象:
1. `module-auth``PasswordEntryError` 新增了 `InvalidPublicUserCode`
2. `api-server` 侧的错误映射 `match` 未覆盖该分支
3. 导致 `api-server` 编译失败
修复:
1.`map_password_entry_error(...)` 中补充 `InvalidPublicUserCode`
2. 返回中文错误文案 `陶泥号格式不正确`
### 3.3 `module-custom-world` 的 `Display` 分支未覆盖新字段错误
文件:
`server-rs/crates/module-custom-world/src/lib.rs`
现象:
1. `CustomWorldFieldError` 新增了 `MissingPublicWorkCode`
2. `impl fmt::Display for CustomWorldFieldError` 未覆盖该枚举分支
3. 导致依赖 `module-custom-world``api-server` 编译链继续失败
修复:
1.`MissingPublicWorkCode` 补充显示文案
2. 文案口径为 `custom_world_gallery_detail.public_work_code 不能为空`
### 3.4 `spacetime-module / spacetime-client` 绑定链路需要重新同步
文件:
1. `server-rs/crates/spacetime-module/src/lib.rs`
2. `server-rs/crates/spacetime-module/src/big_fish/*.rs`
3. `server-rs/crates/spacetime-client/src/lib.rs`
4. `server-rs/crates/spacetime-client/src/module_bindings/*`
现象:
1. `custom_world` 新增 `public_work_code / author_public_user_code` 后,`spacetime-module``spacetime-client` 的手写 facade / 自动生成 bindings 不一致
2. `spacetime generate` 无法顺利完成,导致 `spacetime-client` 继续引用过期 schema
3. `Big Fish` 子模块拆分后,子文件缺少表 accessor trait 导入,阻断 wasm 构建与 bindings 生成
修复:
1. 补齐 `Big Fish` 子模块对表 accessor trait 的导入
2. 补齐 `CustomWorldPublishWorldInput` 在 agent 发布动作中的新字段
3. 补齐 `spacetime-client``publish_custom_world_profile``get_custom_world_gallery_detail_by_code` 的 facade 映射
4. 重新执行:
```powershell
npm run spacetime:generate
```
说明:
1. 这一步完成后,`spacetime-client` 已重新拿到最新 `custom_world_*` / `big_fish_*` bindings
2. `wasm-opt` 缺失只影响优化,不影响 bindings 生成与本地运行验证
## 4. 修复后结论
修复完成后,执行:
```powershell
cargo check -p api-server --bin api-server
```
已通过。
## 5. 新运行结果
在新的本地 `api-server` 实例上执行:
`POST /api/assets/character-visual/generate`
返回结果已经从旧 `.svg` 候选切换为真实 `.png`
`/generated-character-drafts/codex-direct-test-character-v2/visual/aitask_6501f99c694c3/candidate-01.png`
同时通过 OSS 签名读取再次确认:
1. `HTTP 200`
2. `Content-Type: image/png`
3. PNG 文件头校验通过
这说明当前源码级的 Rust `api-server` 已具备重新启动并承载角色主形象真实外部图片生成链的条件,本地旧 SVG 返回问题的根因就是运行进程落后于当前源码与 bindings 状态。

View File

@@ -1,56 +0,0 @@
# api-server 本地 Rust 栈冷编译等待修复记录
日期:`2026-04-25`
## 1. 背景
本地执行 `npm run dev:rust` 时,日志出现:
```text
[dev:rust] 等待 api-server 就绪
Compiling api-server v0.1.0
[dev:rust] 等待 api-server 就绪超时: http://127.0.0.1:8082/healthz
[dev:rust] 停止 api-server
error: linking with `link.exe` failed: exit code: 143
```
这类失败发生在 `api-server` 仍处于 `cargo run` 的冷编译或链接阶段时,`/healthz` 还没有机会监听端口。
## 2. 根因
根目录 `scripts/dev-rust-stack.sh` 同时使用 `SPACETIME_TIMEOUT_SECONDS=60` 控制:
1. SpacetimeDB standalone 的启动等待。
2. Rust `api-server``/healthz` 就绪等待。
SpacetimeDB 的本地启动通常较快,但 `api-server` 在 Windows MSVC 链接、依赖增量失效、首次构建或新增大依赖后可能超过 60 秒。脚本在超时后执行清理逻辑,主动杀掉仍在运行的 `cargo run` 子进程,因此 `link.exe exit code: 143` 是被本地栈脚本中断后的表现,不应优先判断为 Visual Studio Build Tools 损坏。
## 3. 修复口径
`scripts/dev-rust-stack.sh` 将 SpacetimeDB 与 `api-server` 的等待窗口拆开:
1. `SPACETIME_TIMEOUT_SECONDS` 继续只控制 SpacetimeDB 就绪等待,默认 `60` 秒。
2. 新增 `API_SERVER_TIMEOUT_SECONDS` 控制 `api-server` `/healthz` 就绪等待,默认 `300` 秒。
3. 新增命令行参数 `--api-timeout-seconds <seconds>` 便于本地低性能机器或全量重编译时临时放宽。
4. `api-server` 进程如果在等待窗口内自行退出,仍立即报错,不吞掉真实编译错误。
## 4. 使用方式
常规本地启动继续使用:
```bash
npm run dev:rust
```
如本地需要更长冷编译窗口,可执行:
```bash
npm run dev:rust -- --api-timeout-seconds 600
```
## 5. 验收标准
1. 冷编译期间脚本不会在 60 秒时误杀 `cargo run -p api-server`
2. `/healthz` 真正可访问后,脚本继续启动 Vite。
3. 如果 `api-server` 编译失败或运行时提前退出,脚本仍能快速停止并输出原始错误。
4. SpacetimeDB 启动异常仍使用独立的 `--spacetime-timeout-seconds` 判断。

View File

@@ -1,133 +0,0 @@
# 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=
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
# VectorEngine / Gemini 原生图片 / GPT-image-2 / Suno / Vidu 生成网关
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn
VECTOR_ENGINE_API_KEY=
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000
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 与抓大鹅 Gemini 素材 sheet 图片生成额外读取 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;不复用 `APIMART_*``GENARRATIVE_LLM_*` 或前端变量。图片请求默认超时窗口为 `1000000ms`,且 api-server 会把旧环境中较小的 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 提升到该下限,避免 500 秒级生图被提前截断。拼图 Agent 的生成 action 不做前端自动重试,避免一次点击在上游超时后重复触发外部生图与钱包扣退费;若 VectorEngine 请求达到该超时窗口api-server 返回 `504 Gateway Timeout``error.details.provider``vector-engine`,并保留具体超时 message。
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 文本/多模态理解链路;抓大鹅物品素材 sheet、GPT-image-2 图片生成和音频生成都不得读取 APIMart 配置。
10. 本地 `npm run api-server``npm run dev:rust``npm run dev``npm run dev:web` 的环境文件优先级固定为非空外层 shell 变量最高,其后 `.env``.env.local``.env.secrets.local` 逐层覆盖;真实密钥建议放在 `.env.secrets.local`,防止 `.env` 中的空示例值覆盖私密配置。外层 shell 变量如果是空字符串或全空白,不再遮蔽本地 env 文件中的真实值。
11. OSS 客户端只在 `ALIYUN_OSS_BUCKET``ALIYUN_OSS_ENDPOINT``ALIYUN_OSS_ACCESS_KEY_ID``ALIYUN_OSS_ACCESS_KEY_SECRET` 四项齐全时初始化。四项全部缺失表示未启用 OSS部分缺失时 `api-server` 记录 warning 并继续启动,具体上传、换签或读取 generated 私有资产的接口返回 `OSS 未完成环境变量配置`,并在 `error.details.missingEnv` 中列出缺失变量。
12. 抓大鹅 2D 草稿素材生成需要同时具备 VectorEngine 与 OSS 配置VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent` 负责生成 5x5 物品素材 sheet封面和 `9:16` 背景图走 VectorEngine `/v1/images/generations``gpt-image-2-all` JSON 链路;`1:1` 容器 UI 图走 VectorEngine `/v1/images/edits` multipart 链路,并把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,不能再用 generations `image` 数组弱参考。OSS 负责保存切割后的五视角图片及其它生成图。缺少 VectorEngine 或 OSS 时应通过 `error.details.reason` 向前端暴露具体缺项,不能只显示泛化“服务暂不可用”。素材图、封面图和背景图生成在调用外部生图前必须先预检 OSS避免已消耗外部生图后才发现无法落库。
13. 拼图有参考图且开启 AI 重绘时使用 VectorEngine `POST /v1/images/edits` multipart 接口。若返回 `error sending request for url`,代表后端未收到 HTTP 响应;响应 `details` 会带 `reason``source``connect``body``timeout``endpoint`,前端展示优先使用 `details.reason`排查时优先检查服务器网络、DNS、防火墙、代理和参考图大小。拼图图片客户端强制 HTTP/1.1,以降低上游 multipart HTTP/2 连接中断风险。
14. 本地排查 `OSS 未完成环境变量配置` 时必须核对键名是否精确为 `ALIYUN_OSS_ACCESS_KEY_SECRET`。常见误写是把 `OSS` 的首字母 `O` 写成数字 `0`,例如 `ALIYUN_0SS_ACCESS_KEY_SECRET`;该键不会被 `api-server` 读取。
## 本地配置检查
拼图真实生成同时依赖 VectorEngine 与 OSS。触发生成前可先运行
```bash
npm run check:api-server-env
```
该命令只输出配置项是否存在,不打印密钥值。若显示 `ALIYUN_0SS_*`,说明把 `OSS` 的字母 `O` 写成了数字 `0`。修正 env 文件后必须重启 `npm run api-server``npm run dev`,已经运行中的 `api-server` 进程不会自动读取新的环境变量。
## 示例文件
生产示例环境变量维护在:
```text
deploy/env/api-server.env.example
```
真实密钥、内部网关 URL 和具体模型名只应写入服务器 `/etc/genarrative/api-server.env` 或本地未提交的 `.env.local` / `.env.secrets.local`,不得提交到仓库。

View File

@@ -1,66 +0,0 @@
# api-server 合并后编译修复记录
日期:`2026-05-02`
## 背景
`codex/ddd` 合入 `master` 后,`api-server` 编译失败。问题集中在合并后的跨 crate 契约缺口:`api-server` 已引用新接口或新字段,但对应的领域 crate 与 HTTP 转接层没有同步补齐。
## 修复范围
1. `module-auth` 补齐个人资料更新契约:
- 新增 `UpdateProfileInput``UpdateProfileResult`
- `AuthUser` 增加 `avatar_url``created_at`,并通过 `serde(default)` 兼容旧认证快照。
- `PasswordEntryService::update_profile` 统一校验昵称与头像 data URL并写回认证快照。
2. 微信绑定手机号结果补齐 `activated_new_user`
- 待绑定微信账号绑定新手机号时返回 `true`,用于注册奖励发放。
- 待绑定微信账号合并到已有手机号账号时返回 `false`
3. 拼图运行态补齐 HTTP 转接:
- `POST /api/runtime/puzzle/runs/{run_id}/drag` 读取 `DragPuzzlePieceRequest`
- 转发到 SpacetimeDB client 的 `drag_puzzle_piece_or_group` procedure 包装。
4. runtime story 聊天接口改用当前 shared contract
-`runtime_story::RuntimeStorySnapshotPayload` 已删除。
- `api-server` 侧临时别名到 `story::StoryRuntimeSnapshotPayload`,保持现有请求结构不漂移。
5. `api-server` 全量测试修复:
- `custom_world_foundation_draft` 的 mock LLM 响应仍是旧 Chat Completions 结构。
- 当前 `LlmClient` 默认走 Responses API测试 mock 已改为 `output[].content[].text` 结构。
6. 前端全量测试期望补齐:
- 自定义世界结果页的第二幕场景预览断言改为校验第二幕生成图。
- 拼图下一关交互测试保留后端下一关调用断言,并明确只调用一次。
- 拼图正式 run 客户端补回 `/drag` 调用包装,测试 mock 同步走正式 run 的 `swap/drag` 服务路径。
7. 前端门禁合并缺口修复:
- 拼图测试运行前更新作品时同步提交 `levels`,对齐当前 `updatePuzzleWork` 契约。
- 大鱼和 Match3D 测试 mock 对齐当前共享契约,避免 typecheck 阻塞。
- 移除 Vite dev proxy 中重复的 `/api/creation` key避免 build gate 将 warning 视为失败。
## 验证
本次修复应至少通过:
```powershell
cargo check -p module-auth --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
cargo test -p module-auth --manifest-path server-rs\Cargo.toml
cargo test -p api-server --manifest-path server-rs\Cargo.toml
npm run check:encoding
npm test
npm run typecheck
npm run build
npm run check:content
```
后端代码变更后,按项目约束还需要用 `npm run api-server` 做一次启动验证。
本轮最终结果:
- `cargo test -p module-auth --manifest-path server-rs\Cargo.toml` 已通过,结果为 `17 passed; 0 failed`
- `cargo test -p api-server --manifest-path server-rs\Cargo.toml` 已通过,结果为 `237 passed; 0 failed; 4 ignored`
- `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` 已完成启动烟测,`/healthz` 返回 `200`;期间 Maincloud 订阅恢复出现 `503` warning但未阻止服务启动。
仍需单独处理的非本轮阻塞:
- `cargo test --workspace --manifest-path server-rs\Cargo.toml` 在 Windows 原生测试链接 SpacetimeDB module crate 时失败,缺失 `bytes_sink_write``console_log``table_id_from_name``identity``datastore_table_scan_bsatn` 等 SpacetimeDB 宿主符号;这是 module crate 原生 Windows test 链接环境问题。
- `npm run check` 当前仍会停在全仓 `lint:eslint`,涉及大量既有 import 排序、未使用符号和 hook dependency lint debt本轮触碰文件已清掉 lint error`PlatformEntryFlowShellImpl.tsx` 保留既有 hook dependency warnings。

View File

@@ -1,142 +0,0 @@
# `api-server` 接入 `platform-llm` 最小代理设计2026-04-21
## 1. 目标
`platform-llm` 已落成真实 Rust crate 后,`api-server` 需要尽快拥有一条可正式消费的平台接线面,避免平台层只停留在“可编译但未接入”状态。
本次目标只做最小闭环:
1.`api-server` 配置层补齐 LLM 文本网关环境变量
2.`AppState` 注入 `platform-llm::LlmClient`
3. 提供 `/api/llm/chat/completions` 非流式兼容代理
4. 保持与旧 Node 路由的鉴权位置和基本请求形态一致
## 2. 本次范围
### 2.1 本次实现
1. `AppConfig` 新增 LLM provider / base url / api key / model / timeout / retry 配置
2. `AppState` 初始化 `LlmClient`
3. 新增 `shared-contracts::llm`
4. 新增 `api-server/src/llm.rs`
5. 路由挂载到 `/api/llm/chat/completions`
### 2.2 本次不实现
1. 不实现 SSE 流式透传
2. 不实现通用原样 body 转发
3. 不实现媒体模型路由
4. 不把 `module-ai` 编排接进来
## 3. 兼容口径
保持与旧 Node `POST /api/llm/chat/completions` 一致的基本语义:
1. 需要登录态
2. 接收 `model? + stream + messages[]`
3. 当前 `stream=true` 明确返回 `501`,避免伪装支持
4. 非流式返回统一后的文本结果,而不是原样上游 JSON
## 4. 返回结构
Rust 首版返回:
1. `id`
2. `model`
3. `content`
4. `finishReason`
原因:
1. 当前 Rust 平台层已经把上游 `choices[0].message.content` 归一完成
2. `api-server` 首版先保持稳定、可消费的文本结果接口
3. 真正需要 OpenAI 完全兼容响应体时,再单独补“原样代理模式”
## 5. 验收
1. `api-server` 能在配置合法时成功构建 `AppState`
2. `/api/llm/chat/completions` 能通过测试打到 mock 上游
3. `stream=true` 返回明确错误
4. crate 级 `check/test` 通过
## 6. 环境变量与默认值
`api-server` 首版按以下优先级解析 LLM 配置,保证兼容仓库现有 `.env` 口径:
1. provider`GENARRATIVE_LLM_PROVIDER` -> `LLM_PROVIDER`
2. base url`GENARRATIVE_LLM_BASE_URL` -> `LLM_BASE_URL`
3. api key`GENARRATIVE_LLM_API_KEY` -> `LLM_API_KEY` -> `ARK_API_KEY`
4. model`GENARRATIVE_LLM_MODEL` -> `LLM_MODEL` -> `VITE_LLM_MODEL`
5. timeout`GENARRATIVE_LLM_REQUEST_TIMEOUT_MS` -> `LLM_REQUEST_TIMEOUT_MS`
6. max retries`GENARRATIVE_LLM_MAX_RETRIES` -> `LLM_MAX_RETRIES`
7. retry backoff`GENARRATIVE_LLM_RETRY_BACKOFF_MS` -> `LLM_RETRY_BACKOFF_MS`
默认值统一对齐 `platform-llm`
1. provider`ark`
2. base url`https://ark.cn-beijing.volces.com/api/v3`
3. model`doubao-1-5-pro-32k-character-250715`
4. request timeout`30000`
5. max retries`1`
6. retry backoff`500`
补充约束:
1. 如果 `api key` 未配置,`api-server` 允许继续启动,但 `/api/llm/chat/completions` 返回 `503`
2. 如果 provider 字符串非法,回退到默认 `ark`,避免因为环境变量拼写问题阻断开发态服务
## 7. 错误映射
`platform-llm` 到 HTTP 的错误映射固定如下:
1. `InvalidRequest` -> `400 BAD_REQUEST`
2. `InvalidConfig` -> `503 SERVICE_UNAVAILABLE`
3. `Timeout` / `Connectivity` / `Transport` / `Deserialize` / `EmptyResponse` / `StreamUnavailable` -> `502 BAD_GATEWAY`
4. `Upstream(status=429)` -> `429 TOO_MANY_REQUESTS`
5. 其他 `Upstream` -> `502 BAD_GATEWAY`
6. `stream=true` 首版直接返回 `501 NOT_IMPLEMENTED`
## 8. 角色扮演模型联网搜索补充2026-04-25
### 8.1 目标
角色扮演运行时调用文本模型生成剧情正文、NPC 对话、战斗演出文本时,需要默认允许模型使用上游联网搜索能力,提升现实题材、时代背景、地名器物、文化细节的准确度。
### 8.2 落地范围
1. `platform-llm``LlmTextRequest` 增加 `enable_web_search` 布尔开关,默认 `false`,避免影响普通平台代理和非剧情调用。
2. `api-server` 配置增加 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` / `RPG_LLM_WEB_SEARCH_ENABLED`,默认 `true`
3.`runtime_story` 兼容链路中的角色扮演剧情文本请求按配置开启联网搜索。
4. `/api/llm/chat/completions` 通用代理不默认开启联网搜索,避免外部调用方在无感情况下产生额外成本或不可预期内容来源。
### 8.3 上游请求口径
1. 当前默认文本模型走火山方舟 OpenAI 兼容 Chat Completions 路由。
2. 联网搜索开启时,请求体追加 `web_search_options: {}`;关闭时不序列化该字段。
3. 若后续迁移到 Responses API 或更换 provider`platform-llm` 统一收口字段映射,业务层仍只使用 `enable_web_search` 语义开关。
### 8.4 验收
1. `platform-llm` 单测能捕获开启搜索时上游 JSON 包含 `web_search_options`
2. `api-server` 配置单测能验证角色扮演搜索开关默认开启、环境变量可关闭。
3. 角色扮演剧情、NPC 对话、推理战斗文本请求都通过同一辅助函数设置搜索开关,避免漏接。
## 9. AgentSession 创作问答联网搜索补充2026-04-26
### 9.1 目标
AgentSession 页面中的 RPG 世界共创、拼图共创、大鱼吃小鱼共创都属于创作问答链路。用户在这些页面里会要求模型补充现实题材、历史文化、地理器物、玩法参照与美术风格依据,因此创作 Agent 的文本问答默认开启上游联网搜索能力。
### 9.2 落地范围
1. `api-server` 配置增加 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` / `CREATION_AGENT_LLM_WEB_SEARCH_ENABLED`,默认 `true`
2. `creation_agent_llm_turn` 作为三类 Agent 共用 LLM 骨架,必须接收显式 `enable_web_search` 参数,并在 `LlmTextRequest` 上设置该值。
3. RPG 世界共创、拼图共创、大鱼吃小鱼共创的普通消息接口与 SSE 流式消息接口都传入同一配置值,避免只有某一种入口开启。
4. RPG 世界共创里的动态状态推断属于对当前聊天状态的结构化判断,不需要联网搜索,继续保持默认关闭。
5. `/api/llm/chat/completions` 通用代理继续不默认开启联网搜索。
### 9.3 验收
1. `api-server` 配置单测覆盖创作 Agent 联网搜索开关默认开启、环境变量可关闭。
2. 创作 Agent 的共用 LLM 单测覆盖开启搜索时 `LlmTextRequest.enable_web_search``true`
3. 三类 Agent turn request 均包含 `enable_web_search` 字段,调用点全部来自 `state.config.creation_agent_llm_web_search_enabled`

View File

@@ -1,141 +0,0 @@
# api-server SpacetimeClient 连接池化设计 2026-04-23
更新时间:`2026-04-23`
## 1. 背景
当前 `api-server` 虽然在 `AppState` 中只持有一个 `SpacetimeClient` 实例,但 `spacetime-client` 内部仍然是:
1. 每次 procedure / reducer 调用都执行一次 `DbConnection::builder().build()`
2. 建连后立即 `run_threaded()`
3. 拿到结果后立刻 `disconnect()`
也就是说,当前问题不是 `api-server` 每次请求都 new 一个 client而是
**每次 client 调用都新建并销毁一条 SpacetimeDB 连接。**
## 2. 本轮目标
本轮不继续维持“每次 HTTP 请求一条短连接”的阶段性策略。
本轮目标改为:
1. `api-server` 进程内预热并持有一组可复用的 SpacetimeDB 连接
2. 每次 HTTP 请求只从池里借一个可用连接执行 procedure / reducer
3. 请求完成后归还连接,不主动断开
4. 连接失效时自动剔除并按需重建
## 3. 为什么不直接引第三方池库
当前仓库使用的是 `spacetimedb-sdk` 生成的 `DbConnection`,不是传统 SQL client。
它的连接模型包含:
1. `on_connect`
2. `on_disconnect`
3. `run_threaded`
4. reducer / procedure callback
这类对象不是标准的 `bb8` / `deadpool` 资源接口。
当前仓库也没有已经接入的通用资源池库,因此本轮优先在 `spacetime-client` 内实现最小可控池化层,而不是强行套第三方 SQL 风格池库。
## 4. 池化设计
## 4.1 结构
`SpacetimeClient` 内新增一个共享池状态:
1. `pool_size`
2. `Semaphore`
3. `Vec<Mutex<Option<PooledConnection>>>`
其中 `PooledConnection` 持有:
1. `DbConnection`
2. `run_threaded` 返回的后台线程句柄
3. 连接唯一 id
## 4.2 借还模型
每次调用 procedure / reducer 时:
1. 先获取 `Semaphore permit`
2. 选取一个空闲槽位
3. 若槽位已有健康连接,则直接复用
4. 若槽位为空或连接已坏,则现场重建
5. 调用完成后归还槽位,但不主动断开连接
## 4.3 健康判断
当前阶段不做复杂心跳表。
最小健康策略如下:
1. procedure / reducer callback 正常完成:连接保持在池中
2. 连接在调用期间触发 `on_disconnect`:标记该槽位失效
3. 下次借用该槽位时重建连接
## 4.4 并发策略
不共享同一个 `DbConnection` 给多个并发请求同时发 procedure。
原因:
1. SDK callback 是异步回调模型
2. 当前仓库调用层没有 request id 级别的统一 dispatcher
3. 多请求共用一条连接容易把回调和调用方绑定关系搞乱
所以本轮采取:
**一个池槽位同一时刻只服务一个请求。**
这本质上是“连接池”,不是“多路复用单连接”。
## 5. 默认规模
默认池大小取小值,避免本地开发和轻量部署浪费连接:
1. 默认 `4`
2. 允许通过环境变量覆盖,例如 `GENARRATIVE_SPACETIME_POOL_SIZE`
## 6. 错误与超时策略
沿用现有 `SpacetimeClientError` 口径:
1. 建连失败:`Build` / `Runtime`
2. 连接在返回前断开:`ConnectDropped``Procedure`
3. 调用超时:`Timeout`
新增规则:
1. 借用池槽位超时,也映射为 `Timeout`
2. 某槽位一旦确认断线,必须在池中清空,不能继续复用脏连接
3. procedure / reducer 等待结果无论成功、失败还是超时,都必须先归还租约再向上层返回,避免槽位泄漏把池卡死
4. 调用期间若连接先收到 `on_disconnect`,当前阶段只标记坏连接;若业务回调未及时返回,则最终由调用超时路径统一清槽并回传错误
## 7. 与现有文档的关系
之前 [`AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md`](D:/Genarrative/docs/technical/AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md)
中写明“当前阶段每次 HTTP 请求可以建立一条短连接,待真实链路验证稳定后再评估连接池或长连接复用”。
本轮就是进入这个“下一阶段”:
1. 保留 `on_connect` 后再发请求的约束
2. 去掉“请求完成立即断开”的短连接策略
3. 改成 `spacetime-client` 进程内连接池
## 8. 验收标准
落地后至少满足:
1. `api-server` 启动后,`SpacetimeClient` 不再为每次调用单独建连
2. 同一进程内连续多个 API 请求可以复用池中连接
3. 单个连接断开后不会污染后续请求
4. `api-server` 调用侧无需修改业务 handler
## 9. 一句话结论
本轮不引第三方 SQL 风格池库,而是在 `spacetime-client` 内实现一层:
**面向 `DbConnection` 的最小连接池,让 `api-server` 复用长活连接,而不是每次调用都单独建连。**

View File

@@ -1,140 +0,0 @@
# 资产对象业务实体绑定 reducer 设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于冻结 `M6` 中“对象绑定业务实体 reducer”的首版落地方案。
当前已经完成:
1. 浏览器可通过 `PostObject` 把文件直传到私有 OSS。
2. `POST /api/assets/objects/confirm` 已能确认对象存在。
3. `asset_object` 已按 `bucket + object_key` 写入 SpacetimeDB。
下一步要补上的最小闭环是:
1. 已确认的 `asset_object` 能绑定到某个业务实体。
2. 绑定关系由 SpacetimeDB 持久化。
3. Axum 提供最小 HTTP facade避免前端直接拼 SpacetimeDB reducer 参数。
## 2. 当前阶段不直接创建强业务表的原因
当前先落通用 `asset_entity_binding`,不直接创建 `character_visual_asset / scene_image_asset / sprite_sheet_asset`
原因固定如下:
1. 角色、场景、精灵等强业务表的完整字段还没有冻结。
2. 当前最紧急的工程闭环是“确认后的对象能被实体引用”,不是完整发布模型。
3. 通用绑定表可以先承接旧接口迁移中的 `entityId + slot` 关系,后续再由强业务表逐步替换或派生。
## 3. 表设计
首版新增 private table
1. `asset_entity_binding`
字段如下:
| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `binding_id` | `String` | 是 | 主键,固定 `assetbind_` 前缀。 |
| `asset_object_id` | `String` | 是 | 被绑定的 `asset_object.asset_object_id`。 |
| `entity_kind` | `String` | 是 | 业务实体类型,例如 `character``scene``profile`。 |
| `entity_id` | `String` | 是 | 业务实体 ID。 |
| `slot` | `String` | 是 | 实体上的资产槽位,例如 `primary_visual``cover``sprite_sheet`。 |
| `asset_kind` | `String` | 是 | 资产类型,例如 `character_visual`。 |
| `owner_user_id` | `Option<String>` | 否 | 归属用户,当前仅作为服务端传入的记录字段。 |
| `profile_id` | `Option<String>` | 否 | 归属 profile。 |
| `created_at` | `Timestamp` | 是 | 首次绑定时间。 |
| `updated_at` | `Timestamp` | 是 | 最近绑定更新时间。 |
索引如下:
1. `entity_kind + entity_id + slot`
用于按实体槽位查当前绑定。
2. `asset_object_id`
用于按对象反查被哪些业务实体引用。
## 4. 幂等规则
绑定写入按以下规则执行:
1. `asset_object_id` 必须已存在于 `asset_object`
2. `entity_kind + entity_id + slot` 作为首版幂等定位键。
3. 同一实体槽位重复绑定时,不新增第二行。
4. 重复绑定会复用原 `binding_id``created_at`,更新 `asset_object_id / asset_kind / owner_user_id / profile_id / updated_at`
5. 不同槽位可以绑定同一个 `asset_object_id`
## 5. reducer / procedure 设计
SpacetimeDB 新增:
1. `bind_asset_object_to_entity`
reducer只返回 `Result<(), String>`,供后续模块内部复用。
2. `bind_asset_object_to_entity_and_return`
procedure面向 Axum 同步接口返回最终绑定快照。
procedure 返回结构采用:
1. `ok`
2. `record`
3. `error_message`
`asset_object` 确认 procedure 保持一致,便于 `spacetime-client` 做统一错误映射。
## 6. Axum HTTP facade
首版新增接口:
`POST /api/assets/objects/bind`
请求体:
| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `assetObjectId` | `String` | 是 | 已确认对象 ID。 |
| `entityKind` | `String` | 是 | 业务实体类型。 |
| `entityId` | `String` | 是 | 业务实体 ID。 |
| `slot` | `String` | 是 | 资产槽位。 |
| `assetKind` | `String` | 是 | 资产类型。 |
| `ownerUserId` | `String` | 否 | 当前阶段由后端调用方显式传入。 |
| `profileId` | `String` | 否 | 当前阶段由后端调用方显式传入。 |
响应体核心字段:
1. `bindingId`
2. `assetObjectId`
3. `entityKind`
4. `entityId`
5. `slot`
6. `assetKind`
7. `ownerUserId`
8. `profileId`
9. `createdAt`
10. `updatedAt`
## 7. 当前阶段安全边界
当前接口是 Axum facade不是前端直接调用 SpacetimeDB reducer 的最终权限模型。
约束如下:
1. 当前不把长期 OSS AK/SK 下发给客户端。
2. 当前不让客户端直接写 private table。
3. `owner_user_id` 当前只作为记录字段,不作为可信授权依据。
4. 后续接入 SpacetimeDB 身份透传后,绑定 reducer 的授权必须改为基于可信身份,不信任客户端传入的用户 ID。
## 8. 完成定义
首版完成条件:
1. `module-assets` 提供绑定输入、快照、结果结构与字段校验。
2. `spacetime-module` 新增 `asset_entity_binding` 表与绑定 reducer/procedure。
3. `spacetime-client` 生成最新 Rust bindings 并封装绑定 procedure。
4. `api-server` 暴露 `POST /api/assets/objects/bind`
5. 本地测试覆盖字段错误与 “asset_object 不存在不能绑定”。
## 9. 一句话结论
当前阶段先用通用 `asset_entity_binding` 把已确认 OSS 对象绑定到业务实体槽位,强业务资产表等字段稳定后再继续拆分。

View File

@@ -1,316 +0,0 @@
# 图片、视频、动作外部生成手动验证运行手册
日期:`2026-04-23`
## 1. 文档目的
这份文档用于冻结 `验证清单.md` 第四项“图片、视频、动作的生成要真实走到外部服务的生成服务上,而不是用占位符来敷衍”的验证口径。
本次先解决两个问题:
1. 当前仓库里“真实外部生成链”和“Stage 1 占位兼容链”同时存在,若不先写清楚,很容易把占位产物误记为通过。
2. 现有技术设计文档描述了多条资产链,但没有一份面向人工联调的统一运行手册,导致每次验证都要重新猜入口、猜日志、猜通过标准。
## 2. 当前结论总览
截至 `2026-04-23` 当前代码状态,第 4 项仍不能整体直接判定“已通过”,原因是不同资产链状态不同。
### 2.1 当前已经接入真实外部图片生成的入口
以下入口当前会真实请求外部图片生成服务,而不是只生成本地占位图:
1. `Big Fish` 结果页:
- `生成背景`
- `生成并应用正式图` -> `Lv.x 主图`
- `生成并应用正式图` -> `Lv.x 动作工坊`
2. `custom world / RPG 创作`
- 场景图生成
- 作品封面 AI 生成
这些入口当前统一会走 Rust `api-server`,并向 DashScope 图片生成接口发起请求,再落到 OSS 与兼容读路径。
### 2.2 当前仍未完全闭环的入口
以下入口当前仍不能直接判定为“动作资产全后端闭环”:
1. 角色资产工坊 `image-sequence`
- 当前生成的是服务端 SVG 帧,不是真实外部序列图模型结果。
2. 角色资产工坊 `motion-transfer / reference-to-video`
- 当前仍未接入真实外部模型主链。
3. 角色资产工坊 `image-to-video`
- 当前已真实请求 Ark 生成 OSS 草稿区 `preview.mp4`
- 但正式帧抽取和去绿幕仍在前端浏览器完成,再回传后端发布。
因此:
1. 第 4 项里“图片真实外部生成”目前可以做人工验证。
2. 第 4 项里“视频真实外部生成”已有 `image-to-video` 主链证据,但“动作正式资产全后端闭环”仍需要继续验证与收口,不能把前端抽帧回传链直接记成完全通过。
## 3. 代码级判定依据
### 3.1 已接真实外部图片服务的依据
#### 3.1.1 Big Fish 正式图片链
`server-rs/crates/api-server/src/big_fish.rs`
当前 `generate_big_fish_formal_asset(...)` 会执行:
1. 读取 Big Fish 草稿 prompt
2. 调用 `require_big_fish_dashscope_settings(...)`
3. 调用 `create_big_fish_text_to_image_generation(...)`
4. 向 DashScope `text2image/image-synthesis` 发起异步任务请求
5. 下载远端生成图片
6. 上传 OSS
7. 确认 `asset_object`
8. 绑定到 Big Fish 槽位
这条链已经不是占位图写盘。
#### 3.1.2 Custom World 场景图与封面图
`server-rs/crates/api-server/src/custom_world_ai.rs`
当前 `create_text_to_image_generation(...)``create_reference_image_generation(...)` 会:
1. 真实请求 DashScope 图片生成接口
2. 轮询任务状态或解析生成结果
3. 下载远端图片
4. 上传 OSS
5. 生成 `asset_object` 与实体绑定
因此场景图、AI 封面图当前属于“真实外部图片生成”。
### 3.2 仍未完全闭环的依据
#### 3.2.1 角色动作资产工坊
`server-rs/crates/api-server/src/character_animation_assets.rs`
当前链路现状:
1. `image-to-video` 已真实请求 Ark 生成视频
2. 成功结果会下载并写入 `generated-character-drafts/*/preview.mp4`
3. `publish` 当前仍读取前端传入的 `framesDataUrls`
4. 前端仍通过 `HTMLVideoElement + canvas` 自行抽帧并做去绿幕
因此当前状态应判定为“真实外部视频生成主链已完成,但正式动作资产后端闭环尚未完成”。
## 4. 本次验证范围
本次人工验证分成两部分。
### 4.1 可直接操作并验证通过/失败的范围
1. Big Fish 主图生成是否真实打到 DashScope
2. Big Fish 动作工坊静态关键帧图是否真实打到 DashScope
3. Big Fish 背景图是否真实打到 DashScope
4. Custom World 场景图是否真实打到 DashScope
5. Custom World AI 封面图是否真实打到 DashScope
### 4.2 本次要明确记录为“未通过”的范围
1. 角色资产工坊 `生成角色形象`
2. 角色资产工坊 `生成动作`
3. 任何依赖仓库内占位视频或 SVG 帧的动作生成入口
这些入口本次可以操作,但只能用于确认“当前仍未完全闭环”的具体断点,不能把前端抽帧回传链计入“动作资产全后端闭环”通过证据。
## 5. 前置条件
开始验证前,必须同时满足以下条件:
1. 仓库根目录 `.env.local` 已配置:
- `DASHSCOPE_API_KEY`
- `ALIYUN_OSS_BUCKET`
- `ALIYUN_OSS_ENDPOINT`
- `ALIYUN_OSS_ACCESS_KEY_ID`
- `ALIYUN_OSS_ACCESS_KEY_SECRET`
2. 本机已安装:
- `cargo`
- `node`
- `spacetime`
- `ffmpeg`
- `ffprobe`
3. 本地端口可用或已有可复用 Rust 栈:
- Web`3000`
- Rust API`8082`
- SpacetimeDB`3101`
4. 必须使用 Rust 栈,而不是旧 Node 栈。
说明:
1. 当前 Vite 前端必须指向 Rust `api-server`,否则会把验证结果混入旧链路。
2. 验证时必须能实时查看 Rust `api-server` 日志。
## 6. 启动方式
推荐统一使用:
```powershell
npm run dev:rust
```
该命令会完成以下动作:
1. 启动本地 `SpacetimeDB standalone`
2. 发布 `server-rs/crates/spacetime-module`
3. 启动 Rust `api-server`
4. 启动 Vite Web 开发服务器
若已有栈在运行,至少确认:
1. Web 可访问:`http://127.0.0.1:3000`
2. Rust API 为当前前端的实际代理目标
3. `api-server` 正在输出日志
## 7. 手动验证入口
### 7.1 Big Fish 正式图片链
前端路径:
1. 打开 `http://127.0.0.1:3000`
2. 进入平台创作入口
3. 选择 `Big Fish`
4. 先完成草稿编译
5. 进入结果页
6. 在结果页依次操作:
- `生成背景`
- 打开某个等级的 `主图工坊`,点击 `生成并应用正式图`
- 打开某个等级的 `动作工坊`,点击 `生成并应用正式图`
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有 Big Fish 正式图片生成请求
3. 有 DashScope 任务创建或轮询相关日志
4. 生成成功后出现 OSS 写入或正式路径返回
前端期望结果:
1. 资源 URL 不再是 `/generated-big-fish/...`
2. 而是 `/generated-big-fish-assets/...`
3. 结果页状态显示为 `已生成`,而不是 `占位已生成`
4. `Lv.x 主图``idle_float / move_swim` 正式图若下载结果为 PNG后端会在写 OSS 前复用 RPG 角色主图透明背景 alpha 后处理;`生成背景` 不走该处理
### 7.2 Custom World 场景图
前端路径:
1. 进入 RPG / Custom World 创作流程
2. 打开场景或地标编辑入口
3. 点击场景图生成相关操作
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有图片生成任务创建与轮询
3. 成功后有 OSS 对象写入和读取兼容路径
前端期望结果:
1. 返回图片不是本地 SVG 占位
2. 保存后场景主图可稳定显示
### 7.3 Custom World AI 封面图
前端路径:
1. 进入作品编辑页
2. 打开 `编辑作品封面`
3. 选择 `AI 生成作品封面`
4. 输入封面氛围提示词
5. 点击生成并保存
期望日志特征:
1. Rust `api-server` 中出现 `provider = dashscope`
2. 有封面图生成任务
3. 成功后有 OSS 上传与对象确认日志
前端期望结果:
1. 生成结果可预览
2. 保存后作品封面更新为正式图
### 7.4 角色资产工坊反向验证
前端路径:
1. 打开任一角色的 AI 资产工坊
2. 点击 `生成角色形象`
3. 再点击 `生成动作`
本入口的验证目标不是“通过”,而是确认它当前仍未接真实外部视频/图片服务。
期望证据:
1. `生成角色形象` 返回的是 SVG 草稿候选
2. `生成动作` 若未导入参考视频,会回退预置占位视频
3. 日志或结果模型字段不应被当作真实外部视频生成通过证据
## 8. 通过标准
第 4 项只有在以下条件全部满足时,才能勾成通过:
1. 至少一条图片生成入口已拿到真实外部服务调用证据。
2. 至少一条视频或动作生成入口已拿到真实外部服务调用证据。
3. 这些证据不能依赖 SVG 占位、仓库内预置视频或本地占位文件。
4. 前端结果能与日志中的正式链路一一对应。
换言之:
1. 仅图片链通过,不代表第 4 项整体通过。
2. 仅 Big Fish 动作工坊生成出一张静态图,也不等于“视频/动作真实生成”通过。
## 9. 当前预判结论
按当前代码基线,本次更可能得到以下结论:
1. 图片真实外部生成:可以拿到通过证据。
2. 视频、动作真实外部生成:`image-to-video` 主链已可拿到真实外部视频生成证据,但正式动作资产后端闭环仍需要继续收口。
因此本次人工验证完成后,建议把第 4 项拆成至少两条独立清单:
1. 图片生成真实外部服务验证
2. 视频生成真实外部服务验证
3. 动作正式资产后端闭环验证
否则会把“已完成的图片链 / 视频生成链”与“仍未完成的正式动作发布后端闭环”混成一个模糊状态。
## 10. 失败判定与排查
### 10.1 图片入口失败
优先看 Rust `api-server` 日志中的错误文本:
1. `dashscope api key 未配置`
- 说明环境变量缺失。
2. `构造 DashScope HTTP 客户端失败`
- 说明本地网络或 TLS 运行环境异常。
3. `读取生成响应失败`
- 说明上游请求已发出,但响应解析失败。
4. `下载远端图片失败`
- 说明上游已生成图片,但下载或签名读链出错。
5. OSS 相关错误
- 说明生成已成功,但落 OSS 或确认对象失败。
### 10.2 角色资产工坊“看起来成功”
若角色工坊前端看起来成功,不应立刻视为通过,需要先核对:
1. 当前策略是否是 `image-sequence / motion-transfer / reference-to-video`
2. 若是 `image-to-video``preview.mp4` 是否来自真实 Ark 生成
3. 正式发布是否仍要求前端回传 `framesDataUrls`
若只是“后端出真实视频、前端再抽帧回传”,则只能记为“视频生成主链通过,正式动作发布后端闭环未完成”,不能直接把整条动作资产链记为完全通过。
## 11. 关联文档
1. [BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md](./BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md)
2. [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
3. [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md)
4. [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md)
5. [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)
6. [M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md](./M6_CHARACTER_ANIMATION_BACKEND_FRAME_EXTRACTION_AND_PUBLISH_STAGE3_2026-04-23.md)

View File

@@ -1,85 +0,0 @@
# 资产操作泥点消耗接入方案
## 背景
当前泥点钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层:
- SpacetimeDB 负责钱包余额和流水的原子变更。
- Axum 资产操作服务负责在执行业务资产操作前扣费,并在生成、持久化或发布失败时补偿退款。
## 首期范围
首期接入带 Bearer 身份、能明确归属真实用户的资产操作入口:
- `POST /api/custom-world/scene-image`
- `POST /api/custom-world/cover-image`
- `POST /api/runtime/custom-world/cover-image`
- `POST /api/runtime/custom-world-library/{profile_id}/publish`
- 自定义世界 Agent 动作 `publish_world`
- Big Fish Agent 正式图片生成动作 `big_fish_generate_level_main_image``big_fish_generate_level_motion``big_fish_generate_stage_background`
- Big Fish Agent 动作 `big_fish_publish_game`
- Puzzle Agent 图片生成动作 `compile_puzzle_draft``generate_puzzle_images`
- Puzzle Agent 动作 `publish_puzzle_work`
- Match3D / 抓大鹅草稿生成动作 `match3d_compile_draft`
- 拼图 / 抓大鹅结果页 UI 背景与抓大鹅批量新增物品素材;背景音乐和点击音效生成入口当前临时关闭,不进入计费范围
暂不接入以下入口:
- 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。
- 手动上传封面:不调用外部生成模型,不消耗泥点。
- 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。
- 文本实体、NPC 生成:本次需求聚焦图片资产和发布资产操作,首期只覆盖可明确归属的入口。
## 计费规则
- 每次可计费资产操作消耗 `1` 枚泥点。
- 例外Match3D / 抓大鹅草稿生成是一次完整草稿外部生成动作,固定消耗 `10` 枚泥点;流水仍复用 `asset_operation_consume` / `asset_operation_refund``asset_kind = match3d_draft_generation`
- 例外:拼图 / 抓大鹅 UI 背景重新生成固定消耗 `2` 枚泥点。
- 例外:抓大鹅结果页批量新增物品素材按实际可新增物品名计费,每 `5` 个消耗 `2` 枚泥点,不足 `5` 个向上按 `5` 个计。重复名称、作品中已有名称和超过容量上限的名称不进入计费数量。
- 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。
- 在调用外部图片生成或发布 mutation 前预扣,余额不足时直接返回业务错误,不继续调用后续资产操作。
- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。
- 如果退款失败,原始错误仍返回给调用方,同时服务端日志记录退款失败,便于后续人工核对。
## 前端确认交互
所有前端可见且会消耗泥点的按钮,点击后必须先弹出独立确认面板,面板标题使用 `确认消耗泥点`,正文只展示本次消耗数量,例如 `消耗 2 泥点`。用户点击 `确定` 后才允许调用后端扣费动作;点击 `取消` 或关闭面板不得触发接口。
2026-05-14 补充:创作页入口点击确认后,前端必须先刷新 `/api/profile/dashboard` 钱包余额;余额大于等于本次消耗时才允许创建玩法 session / 草稿,余额不足时停留在创作页并展示不足提示。该预检不替代后端扣费原子校验,只用于避免余额明显不足时先生成半成品草稿。
2026-05-14 当前已覆盖的草稿页入口包括:
- 拼图入口 `AI重绘=true``生成拼图游戏草稿``2` 泥点;`AI重绘=false` 直接使用上传图,不显示泥点确认。
- 拼图结果页关卡 `生成画面` / `重新生成画面``2` 泥点。
- 拼图结果页 `素材配置 > UI``生成UI背景` / `重新生成``2` 泥点。
- 拼图结果页发布按钮:`1` 泥点,发布确认面板必须显示本次消耗数量。
- 抓大鹅入口 `生成抓大鹅草稿``10` 泥点。
- 抓大鹅结果页 `素材配置 > 物品` 的批量新增与批量重新生成:按实际计费数量展示动态泥点数。
- 抓大鹅结果页 `素材配置 > UI``素材配置 > 容器形象``重新生成`:各 `2` 泥点。
## 钱包流水
公开两个流水来源类型,统一覆盖“资产生成”和“资产发布”这两类资产操作。流水金额由具体资产操作成本决定,不再假定所有资产操作都是 `1` 枚泥点:
- `asset_operation_consume`:资产操作预扣,`amount_delta = -points_cost`
- `asset_operation_refund`:资产操作失败退款,`amount_delta = +points_cost`
`wallet_ledger_id` 由 Axum 传入,格式:
- 扣费:`asset_operation_consume:{user_id}:{asset_kind}:{asset_id}`
- 退款:`asset_operation_refund:{user_id}:{asset_kind}:{asset_id}`
SpacetimeDB procedure 对 `ledger_id` 做幂等保护:如果同一个流水 ID 已存在,则直接返回当前钱包快照,不重复变更余额。
## 工程落点
- `module-runtime`:新增钱包调整输入、钱包调整结果、流水来源枚举。
- `spacetime-module`:新增 `consume_profile_wallet_points_and_return``refund_profile_wallet_points_and_return` procedure并扩展钱包变更 helper 支持负数。
- `spacetime-client`:新增对应调用方法和绑定类型。
- `api-server`资产操作服务提供统一可计费执行入口自定义世界、Big Fish、Puzzle 业务 handler 只声明资产操作,不直接调用钱包扣费或退款。
- `shared-contracts`:新增 API 流水来源常量,保证“我的-钱包流水”输出使用稳定契约字符串。
- `packages/shared` 与前端:统一使用 `asset_operation_consume` / `asset_operation_refund` 展示钱包流水。
## 非目标
本次不做分档价格、不做会员免扣,也不迁移旧 `server-node` 逻辑。旧资产工坊角色主形象/动作生成与发布接口仍需要先补齐 Bearer 身份归属后再纳入扣费范围。旧资产生成流水 source 不再作为公开契约兼容。

View File

@@ -1,30 +0,0 @@
# 资产历史接口补齐拼图封面素材类型
日期:`2026-04-27`
## 背景
拼图结果页会通过 `/api/assets/history?kind=puzzle_cover_image` 读取历史封面素材,供“生成或更换图片”面板复用旧图。
该链路与角色主视觉、场景图共用同一资产历史接口,因此后端白名单一旦漏掉 `puzzle_cover_image`,前端就会收到 `400 Bad Request`,表现为拼图封面历史素材列表无法打开。
## 本次口径
1. `server-rs/crates/api-server/src/assets.rs` 中的历史素材类型白名单统一收口为单一常量源。
2. HTTP 层错误文案与实际支持列表由同一函数生成,避免后续再出现“校验改了但提示文案还是旧口径”的漂移。
3. 增加 `puzzle_cover_image` 的回归测试,确保拼图封面素材不会再次被历史接口遗漏。
4. `ownerLabel` 只表示资产归属账号,不是历史图片标题;前端历史素材卡片标题必须从 `imageSrc` 的路径末尾推导,例如 `/generated-puzzle-assets/history/image.png` 展示为 `image.png`
5. `createdAt / updatedAt` 可能来自 SpacetimeDB / shared-kernel 的秒级字符串,例如 `1713686400.000000Z`,前端不得只用 `new Date(value)` 解析后把它显示成未知时间。
6. 历史素材选中后仍把 `imageSrc` 作为 `referenceImageSrc` 传给生成链路;创作页和关卡详情页的预览必须通过 `ResolvedAssetImage` 换签展示,不直接请求裸 `/generated-*` 路径。
## 后续约束
1. 新增历史素材类型时,必须同时更新:
- `api-server``SUPPORTED_ASSET_HISTORY_KINDS`
- `spacetime-module` 的历史素材白名单
- 对应前端调用常量与测试
2. 如果运行态仍返回旧白名单错误,优先检查本地 `api-server.exe` 是否已按最新源码重新编译并重启,而不是先回退前端类型参数。
3. 历史素材列表的 UI 回归测试应覆盖:
- 卡片标题不使用 `账号 user-1` 这类归属文案。
- `1713686400.000000Z` 能显示为可读生成时间。
- 选中素材后工作台 / 关卡详情展示 `历史素材 · image.png`,并继续提交原始 `imageSrc`

View File

@@ -1,185 +0,0 @@
# 资产对象上传完成确认接口设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于把 `M6` 中“上传完成后的对象确认接口”冻结到可直接编码的级别。
当前要解决的不是完整资产发布链,而是最小闭环:
1. 浏览器先通过 `PostObject` 把文件上传到私有 OSS
2. 服务端确认对象真实存在
3. 服务端把对象元数据写入当前阶段的 `asset_object` 真相存储
4. 后续业务绑定与 SpacetimeDB reducer 再基于这条已确认对象继续扩展
## 2. 当前前提
已落地事实:
1. `POST /api/assets/direct-upload-tickets` 已能签发浏览器 `PostObject` 直传票据。
2. `platform-oss` 已能生成私有读签名 URL。
3. `asset_object` 已在 `spacetime-module` 中落下首版表骨架。
当前仍未落地:
1. 上传完成确认接口
2. 对象 HEAD 校验
3. `asset_object` 实际写入路径
4. 业务实体绑定
## 3. 接口职责
`POST /api/assets/objects/confirm` 当前阶段只负责三件事:
1. 校验请求给出的 `bucket + object_key` 是否合法
2. 调 OSS 做一次私有 `HEAD Object` 校验,确认对象真实存在
3. 把对象元数据写入当前阶段的 `asset_object` 进程内存储,并返回确认结果
当前阶段明确不做:
1. 不做业务实体绑定
2. 不做图片尺寸探测
3. 不做 hash 计算
4. 不做重复对象合并
5. 不直接调用 SpacetimeDB reducer
## 4. 请求体设计
请求路径:
`POST /api/assets/objects/confirm`
请求体:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `bucket` | `String` | 否 | 当前阶段允许不传;不传时默认回落到服务端 OSS bucket。 |
| `objectKey` | `String` | 是 | 正式对象路径真相字段。 |
| `contentType` | `String` | 否 | 客户端已知 MIME可回写到对象元数据。 |
| `contentLength` | `u64` | 否 | 客户端可传期望大小;当前仅用于一致性校验,不作为唯一真相来源。 |
| `contentHash` | `String` | 否 | 后续内容摘要预留字段。 |
| `assetKind` | `String` | 是 | 业务资产类型,例如 `character_visual`。 |
| `accessPolicy` | `String` | 否 | 默认 `private`。 |
| `sourceJobId` | `String` | 否 | 来源任务 ID。 |
| `ownerUserId` | `String` | 否 | 归属用户 ID。 |
| `profileId` | `String` | 否 | 归属 profile ID。 |
| `entityId` | `String` | 否 | 归属业务实体 ID。 |
补充约束:
1. `bucket` 当前若传入,必须与服务端已配置 bucket 一致。
2. `objectKey` 必须落在受支持的 `generated-*` 前缀下。
3. `assetKind` 当前不能为空。
## 5. 校验顺序
接口校验顺序固定如下:
1. 检查 OSS 配置是否存在
2. 校验请求参数基础合法性
3. 校验 `bucket` 与服务端配置 bucket 是否一致
4. 调用 OSS `HEAD Object`
5. 若客户端传了 `contentLength`,则与 OSS 返回的真实 `Content-Length` 做一致性校验
6. 通过后写入 `asset_object`
## 6. OSS 校验结果口径
OSS `HEAD Object` 当前至少回填:
1. `content_length`
2. `content_type`
3. `last_modified_at`
4. `etag`
当前阶段以 OSS 返回值为准:
1. `content_length` 真相取 OSS
2. `content_type` 优先取 OSSOSS 未返回时再回退请求体
3. `content_hash` 暂不强行等于 `etag`
原因:
1. `etag` 对 multipart 上传和不同上传模式并不总等价于内容 hash
2. 当前阶段先留出字段,不把错误假设固化进 schema
## 7. 写入规则
确认成功后写入 `asset_object`
1. `asset_object_id` 由服务端生成,固定 `assetobj_` 前缀
2. `bucket``object_key` 按正式真相写入
3. `access_policy` 当前默认 `private`
4. `content_length` 以 OSS HEAD 为准
5. `content_type` 优先 OSS HEAD
6. `version` 当前固定为 `1`
7. `created_at` / `updated_at` 在确认时写当前 UTC 时间
当前阶段重复确认同一 `bucket + object_key` 的行为固定为:
1. 若已存在,则返回已存在记录并更新 `updated_at`
2. 不生成第二条重复对象记录
## 8. 响应体设计
成功响应核心字段:
1. `assetObjectId`
2. `bucket`
3. `objectKey`
4. `accessPolicy`
5. `contentType`
6. `contentLength`
7. `contentHash`
8. `assetKind`
9. `sourceJobId`
10. `ownerUserId`
11. `profileId`
12. `entityId`
13. `version`
14. `createdAt`
15. `updatedAt`
## 9. 错误口径
### 9.1 请求参数错误
返回 `400`
1. `objectKey` 为空
2. `objectKey` 不在受支持前缀下
3. `assetKind` 为空
4. `bucket` 与当前配置 bucket 不一致
5. 客户端声明的 `contentLength` 与 OSS HEAD 不一致
### 9.2 OSS 未配置
返回 `503`
### 9.3 OSS 对象不存在
返回 `404`
### 9.4 OSS 探测失败
返回 `502`
## 10. 当前阶段实现边界
当前阶段实现固定为:
1. `platform-oss` 增加服务端 `HEAD Object` helper
2. `module-assets` 提供进程内 `asset_object` 确认服务
3. `api-server` 接入 `POST /api/assets/objects/confirm`
下一阶段再继续:
1. 对接真实 SpacetimeDB reducer
2. 业务实体绑定 reducer
3. 更细的元数据探测
## 11. 关联文档
1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
2. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md)
3. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)

View File

@@ -1,37 +0,0 @@
# `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`

View File

@@ -1,155 +0,0 @@
# `/api/auth/login-options` 登录方式选项设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于冻结 Rust `api-server` 首版 `GET /api/auth/login-options` 的返回 contract、配置来源与当前阶段边界确保前端在登录页读取“当前可用登录方式”时不需要依赖硬编码开关。
## 2. 当前目标
当前阶段只解决一件事:
1.`Axum` 根据服务端配置,返回当前环境启用的登录方式列表。
2. 密码登录入口由 Rust `password_entry` 固定承载,作为登录弹窗的保底入口。
本阶段明确不包含:
1. 短信或微信登录链路本身是否已经完整落地
2. 对前端返回更细粒度的 provider 配置
3. 第三方登录按钮文案、图标或 UI 布局规则
## 3. 接口 contract
### 3.1 请求
1. 方法:`GET`
2. 路径:`/api/auth/login-options`
3. 鉴权:不需要
4. 请求体:空
### 3.2 成功响应
```json
{
"availableLoginMethods": ["phone", "password", "wechat"]
}
```
字段说明:
1. `availableLoginMethods` 为字符串数组
2. 当前阶段只允许出现:
- `phone`
- `password`
- `wechat`
### 3.3 返回顺序
返回顺序固定为:
1.`phone`
2.`password`
3.`wechat`
这样可以保证前端按钮顺序稳定,不因配置解析顺序变化而漂移。
## 4. 配置来源
`api-server` 只读取以下布尔配置:
1. `SMS_AUTH_ENABLED`
2. `WECHAT_AUTH_ENABLED`
映射规则固定为:
1. `SMS_AUTH_ENABLED=true` 时返回 `phone`
2. Rust 密码登录主链可用时固定返回 `password`
3. `WECHAT_AUTH_ENABLED=true` 时返回 `wechat`
4. 短信与微信都关闭时仍返回 `["password"]`
## 5. crate 边界
### 5.1 `api-server`
负责:
1. 读取 `AppState.config`
2. 组装 `availableLoginMethods`
3. 返回项目兼容的响应 envelope
### 5.2 `module-auth`
本接口当前阶段不依赖 `module-auth`
### 5.3 前端
负责:
1. 根据 `availableLoginMethods` 决定是否展示手机号 / 微信入口
2. 不再假设某种登录方式一定存在
3.`/api/auth/login-options` 联调失败或返回空数组,前端仍保留 `password` 入口,避免登录弹窗显示“当前登录入口暂不可用”后无法继续操作。
## 6. 测试要求
至少覆盖:
1. 默认配置下返回 `["password"]`
2. 同时启用短信、密码与微信时返回 `["phone", "password", "wechat"]`
3. 前端在 `login-options` 读取失败或返回空数组时,仍展示密码登录表单
## 7. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 已提供 `GET /api/auth/login-options`
2. 响应字段命名与前端约定一致
3. 配置开关可稳定映射到返回数组
4. 文档、任务清单与测试已同步更新
## 8. 2026-05-01 前端降级修复记录
本地联调时若 `api-server` 未启动或 Vite 代理暂时返回 `500``GET /api/auth/login-options` 会失败。前端必须继续遵循第 5.3 节约束:
1. `AuthGate``login-options` 读取失败时设置 `availableLoginMethods = ["password"]`
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` 为准。

View File

@@ -1,177 +0,0 @@
# `/api/auth/logout-all` 全端登出落地设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2``实现全端登出` 的首版落地,冻结:
1. `POST /api/auth/logout-all` 的请求与响应 contract
2. 全部 refresh session 吊销与 `token_version` 递增的组合语义
3. Rust 首版在进程内鉴权真相中的最小实现边界
4.`/logout``/sessions/:sessionId/revoke` 的职责切分
## 2. 当前基线
当前 Node `/api/auth/logout-all` 已具备以下稳定语义:
1. 必须先通过 Bearer JWT 校验
2. 对当前用户执行 `token_version + 1`
3. 吊销该用户全部未吊销 refresh session
4. 响应成功时始终清理 refresh cookie
因此Node 的“退出全部设备”同样是两层组合动作:
1. 会话级:吊销同一账号全部 refresh session
2. 用户级:递增 `token_version`,让全部旧 access token 立即失效
Rust 首版必须保留这个语义。
## 3. 当前阶段范围
本阶段只落以下内容:
1. `module-auth` 增加按 `user_id` 吊销全部 refresh session 的能力
2. `api-server` 暴露 `POST /api/auth/logout-all`
3. 成功场景统一清理 refresh cookie
本阶段明确不包含:
1. `/api/auth/sessions/:sessionId/revoke`
2. 审计日志正式落表
3. SpacetimeDB reducer 真正写表
## 4. contract
### 4.1 请求
1. 方法:`POST`
2. 路径:`/api/auth/logout-all`
3. 请求体:空
4. 鉴权:
- Bearer JWT 必填
- refresh cookie 选填
### 4.2 成功响应
```json
{
"ok": true
}
```
同时响应头必须写回清理后的 refresh cookie。
### 4.3 失败响应
以下情况返回 `401 UNAUTHORIZED`
1. Bearer JWT 缺失或非法
2. JWT 对应用户不存在
## 5. 固定语义
### 5.1 动作顺序
`POST /api/auth/logout-all` 固定按以下顺序执行:
1. 从 Bearer JWT 解析当前用户
2. 批量吊销当前用户全部 refresh session
3. 对当前用户执行 `token_version + 1`
4. 返回 `ok: true`
5. 始终清理 refresh cookie
### 5.2 `token_version` 只递增一次
无论当前用户存在多少会话:
1. `logout-all` 只递增一次 `token_version`
2. 不为每条 session 单独递增版本号
### 5.3 缺少 refresh cookie 不影响成功
`logout-all` 是账号级动作,不依赖当前 refresh cookie 命中:
1. 即使当前设备没有 refresh cookie也要允许完成全端登出
2. 成功响应仍然统一清理 cookie
## 6. 与其他接口的职责切分
### 6.1 `/api/auth/logout`
负责:
1. 当前设备退出
2. 当前 refresh session 尽力吊销
3. `token_version` 递增一次
### 6.2 `/api/auth/logout-all`
负责:
1. 全部设备退出
2. 当前用户全部 refresh session 吊销
3. `token_version` 递增一次
### 6.3 `/api/auth/sessions/:sessionId/revoke`
后续负责:
1. 只吊销指定远端设备 refresh session
2. 不递增 `token_version`
## 7. crate 边界
### 7.1 `module-auth`
负责:
1.`user_id` 吊销全部 refresh session
2. 递增当前用户 `token_version`
3. 返回最新用户快照
### 7.2 `platform-auth`
负责:
1. 构造清理 cookie 的 `Set-Cookie`
### 7.3 `api-server`
负责:
1. Bearer JWT 读取与校验
2. 调用 `module-auth` 执行全端登出
3. 始终回写清理 cookie
## 8. 进程内实现策略
当前阶段 `module-auth` 继续使用进程内真相,新增以下最小能力:
1. `revoke_all_sessions_by_user_id`
2. `logout_all_sessions`
其中:
1. 批量吊销只改 `revoked_at`
2. 用户版本递增继续直接修改内存用户快照
## 9. 测试策略
至少覆盖:
1. 登录两次后调用 `/api/auth/logout-all` 返回 `ok: true`
2. `/logout-all` 成功后清理 refresh cookie
3. `/logout-all` 成功后旧 Bearer token 访问 `/api/auth/me` 返回 `401`
4. `/logout-all` 成功后旧 refresh cookie 调用 `/api/auth/refresh` 返回 `401`
5. 缺少 refresh cookie 时,只要 Bearer token 有效,`/logout-all` 仍返回 `ok: true`
## 10. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 侧已提供 `POST /api/auth/logout-all`
2. 同一用户全部 refresh session 可被吊销
3. 用户 `token_version` 会在全端登出时递增
4. `/logout-all` 总会清理 refresh cookie
5. 文档、任务清单与测试已同步更新

View File

@@ -1,209 +0,0 @@
# `/api/auth/logout` 当前会话吊销落地设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2``实现会话吊销` 任务的第一段首版落地,冻结:
1. `POST /api/auth/logout` 的请求与响应 contract。
2. 当前设备退出时 refresh session 吊销与 `token_version` 递增的组合语义。
3. Rust 首版在进程内鉴权真相中的最小实现边界。
4. 与后续 `logout-all``sessions/:sessionId/revoke` 的职责切分。
## 2. 当前基线
当前 Node `/api/auth/logout` 已具备以下稳定语义:
1. 必须先通过 Bearer JWT 校验。
2. 从 cookie 中读取当前 refresh token并尝试吊销对应 refresh session。
3. 无论当前 refresh session 是否已存在,只要用户存在,仍继续执行“退出当前设备”。
4. 对当前用户执行 `token_version + 1`,使当前 access token 全局失效。
5. 响应成功时始终清理 refresh cookie。
因此Node 的“退出当前设备”实际是两层组合动作:
1. 设备级:吊销当前 refresh session
2. 用户级:递增 `token_version`,让当前 access token 立即失效
Rust 首版必须保留这个语义。
## 3. 当前阶段范围
本阶段只落以下内容:
1. `module-auth` 增加当前 refresh session 吊销能力。
2. `module-auth` 增加用户 `token_version` 递增能力。
3. `api-server` 暴露 `POST /api/auth/logout`
4. 成功或已失效场景统一清理 refresh cookie。
本阶段明确不包含:
1. `/api/auth/logout-all`
2. `/api/auth/sessions`
3. `/api/auth/sessions/:sessionId/revoke`
4. 审计日志与风控日志正式落表
5. SpacetimeDB reducer 真正写表
## 4. contract
### 4.1 请求
1. 方法:`POST`
2. 路径:`/api/auth/logout`
3. 请求体:空
4. 鉴权:
- Bearer JWT 必填
- refresh cookie 选填但应尽量提供
### 4.2 成功响应
```json
{
"ok": true
}
```
同时响应头必须写回清理后的 refresh cookie。
### 4.3 失败响应
以下情况返回 `401 UNAUTHORIZED`
1. Bearer JWT 缺失或非法
2. JWT 对应用户不存在
说明:
1. 当前 refresh cookie 缺失本身不构成 `/logout` 失败。
2. 因为当前设备可能已经没有 refresh cookie但 access token 仍应允许执行显式退出。
## 5. 固定语义
### 5.1 当前设备退出的动作顺序
`POST /api/auth/logout` 固定按以下顺序执行:
1. 从 Bearer JWT 解析当前用户。
2. 尝试按当前 refresh cookie 吊销 refresh session。
3. 对当前用户执行 `token_version + 1`
4. 返回 `ok: true`
5. 始终清理 refresh cookie。
### 5.2 refresh session 吊销是“尽力而为”
当 refresh cookie 缺失、refresh token 无法命中 session、session 已吊销时:
1. 不把这些情况视为 `/logout` 失败。
2. 继续执行用户级 `token_version` 递增。
原因:
1. 当前设备退出的主目标是让“现在这份 access token”立刻失效。
2. refresh session 丢失不应该阻断显式退出。
### 5.3 `token_version` 必须递增
当前阶段固定规则:
1. `/logout` 必须递增 `user.token_version`
2. 后续 Bearer JWT 校验必须比对当前用户最新 `token_version`
说明:
1. 如果不递增,当前 access token 直到自然过期前仍可继续访问。
2. 这与 Node 当前行为不一致,也会让“退出登录”在用户感知上失真。
## 6. 与其他接口的职责切分
### 6.1 `/api/auth/logout`
负责:
1. 当前设备退出
2. 当前 access token 立即失效
3. 当前 refresh session 尽力吊销
### 6.2 `/api/auth/logout-all`
后续负责:
1. 吊销同一用户全部 refresh session
2. 递增一次 `token_version`
### 6.3 `/api/auth/sessions/:sessionId/revoke`
后续负责:
1. 只吊销指定远端设备 refresh session
2. 不递增 `token_version`
## 7. crate 边界
### 7.1 `module-auth`
负责:
1. 按 refresh token hash 吊销当前 session。
2. 递增当前用户 `token_version`
3. 返回退出后最新用户快照,供后续 access token 校验使用。
### 7.2 `platform-auth`
负责:
1. refresh token 哈希
2. 构造清理 cookie 的 `Set-Cookie`
### 7.3 `api-server`
负责:
1. Bearer JWT 与 refresh cookie 的读取
2. 调用 `module-auth` 组合执行当前设备退出
3. 始终回写清理 cookie
## 8. 进程内实现策略
当前阶段 `module-auth` 继续使用进程内真相,新增以下最小能力:
1. `revoke_session_by_refresh_token_hash`
2. `increment_user_token_version`
其中:
1. session 吊销要写入 `revoked_at`
2. 用户版本递增要直接修改内存中用户快照
## 9. Bearer JWT 校验补强
为了让 `/logout` 后“旧 access token 立即失效”真正成立,当前阶段需要补一条约束:
1. Bearer JWT 校验通过签名后,还必须比对 claims 里的 `ver`
2.`claims.ver != 当前用户 token_version`,返回 `401`
说明:
1. 这是当前 Rust 鉴权链路必须补上的一致性校验。
2. 否则 `logout` 虽然递增了用户版本,但旧 JWT 仍能继续访问。
## 10. 测试策略
至少覆盖:
1. 登录成功后调用 `/api/auth/logout` 返回 `ok: true`
2. `/logout` 成功后会清理 refresh cookie
3. `/logout` 成功后旧 Bearer token 再访问 `/api/auth/me` 返回 `401`
4. refresh cookie 缺失时,只要 Bearer token 有效,`/logout` 仍返回 `ok: true`
5. 用户不存在时 `/logout` 返回 `401`
## 11. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 侧已提供 `POST /api/auth/logout`
2. 当前 refresh session 可按 cookie 对应关系被吊销
3. 用户 `token_version` 会在退出时递增
4. Bearer JWT 已补充版本比对
5. `/logout` 总会清理 refresh cookie
6. 文档、任务清单与测试已同步更新

View File

@@ -1,121 +0,0 @@
# `/api/auth/me` 查询落地设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2``实现 me 查询` 任务的首版落地,冻结:
1. `GET /api/auth/me` 的请求与响应 contract。
2. 当前阶段 Bearer JWT 与用户快照读取的衔接方式。
3. `availableLoginMethods` 的返回口径。
4. JWT 有效但本地用户不存在时的错误处理语义。
## 2. 当前基线
当前 Node `/api/auth/me` 具备以下最小语义:
1. 必须先通过 Bearer JWT 校验。
2.`sub = user_id` 读取当前用户。
3. 返回 `user + availableLoginMethods`
4. `availableLoginMethods` 只返回当前对外开启的补充登录方式,不包含 `password`
Rust 首版需要保留这条最小 contract但当前阶段允许继续使用进程内仓储承接用户真相。
## 3. 当前阶段范围
本阶段只落以下内容:
1. `module-auth` 增加按 `user_id` 查询当前用户能力。
2. `api-server` 暴露 `GET /api/auth/me`
3. 返回与当前前端兼容的 `user + availableLoginMethods`
本阶段不包含:
1. `refresh token` 轮换。
2. 会话列表、审计、风控等扩展信息。
3. `SpacetimeDB` 真正的身份表读取。
## 4. contract
### 4.1 请求
1. 方法:`GET`
2. 路径:`/api/auth/me`
3. 鉴权:`Authorization: Bearer <token>`
### 4.2 成功响应
```json
{
"user": {
"id": "user_00000001",
"username": "guest_001",
"displayName": "guest_001",
"phoneNumberMasked": null,
"loginMethod": "password",
"bindingStatus": "active",
"wechatBound": false
},
"availableLoginMethods": []
}
```
说明:
1. 当前阶段 `user` 字段固定返回当前登录用户快照,不返回 `null`
2. `availableLoginMethods` 只按当前对外配置返回:
- `SMS_AUTH_ENABLED=true` 时包含 `phone`
- `WECHAT_AUTH_ENABLED=true` 时包含 `wechat`
3. `password` 不进入 `availableLoginMethods`,保持和 Node 现状一致。
## 5. 错误语义
### 5.1 缺少或非法 Bearer token
1. 返回 `401 UNAUTHORIZED`
### 5.2 JWT 有效但用户不存在
1. 返回 `401 UNAUTHORIZED`
2. 语义视为“当前登录态已失效,需要重新登录”
说明:
1. 当前阶段不把这种情况返回为 `404`
2. 这样可以与后续 `token_version`、会话吊销和用户禁用策略保持同一类恢复路径。
## 6. crate 边界
### 6.1 `module-auth`
负责:
1. 提供按 `user_id` 查询当前用户快照的能力。
2. 继续复用密码登录阶段已经建立的同一份进程内用户真相。
### 6.2 `api-server`
负责:
1. 复用现有 Bearer JWT 中间件拿到 `sub`
2. 调用 `module-auth` 查询用户。
3. 组装 `AuthMeResponse`
## 7. 测试策略
至少覆盖:
1. 已登录用户可通过 `/api/auth/me` 取回当前用户。
2. 当短信/微信开关开启时,`availableLoginMethods` 返回对应值。
3. JWT 有效但用户不存在时返回 `401`
## 8. 后续衔接
这条任务完成后,下一步顺序固定为:
1. refresh token 轮换
2. 会话吊销
3. 手机验证码登录
微信登录继续按“暂缓执行”处理。

View File

@@ -1,70 +0,0 @@
# 新账号短信登录后置邀请码弹窗设计
日期:`2026-05-01`
## 1. 目标
账号入口不再展示独立注册入口。用户统一从短信登录进入,后端通过 `POST /api/auth/phone/login` 返回的 `created` 字段判断本次是否创建了新账号。
`created=true` 时,前端在登录成功后额外弹出独立邀请码面板:
1. 标题固定为 `请填写邀请码`
2. 标题下方展示邀请码输入框。
3. 输入为空时主按钮显示 `跳过`,点击后关闭面板。
4. 输入非空时主按钮显示 `提交`,点击后提交邀请码。
5. 面板右上角提供取消按钮,点击后关闭面板。
## 2. 入口调整
登录弹窗只保留可用登录方式:
1. 短信登录。
2. 密码登录。
3. 微信登录。
不得再展示 `注册` 页签、注册按钮或注册表单。邀请码不再出现在短信验证码表单中,避免用户把登录和注册理解成两套入口。
## 3. 邀请码提交
后置弹窗提交邀请码时调用已登录接口:
```text
POST /api/profile/referrals/redeem-code
```
请求体:
```json
{
"inviteCode": "SPRING2026"
}
```
后端继续使用 SpacetimeDB 的 `redeem_profile_referral_invite_code` procedure 作为唯一真相源。该 procedure 已负责校验:
1. 每个用户最多只能填写一个邀请码。
2. 邀请码必须存在。
3. 用户不能填写自己的邀请码。
4. 双方奖励与钱包流水在同一事务内落地。
## 4. URL 邀请码
若地址中存在 `inviteCode``invite_code`,前端只将其作为新账号后置弹窗的默认输入值。它不会触发注册页签,也不会在短信登录请求中提前提交。
若用户登录的是已有账号,则不会弹出新账号邀请码面板。
## 5. 新账号泥点初始化
当短信登录、开发密码入口或微信激活流程创建新账号时,后端注册链路必须调用 `grant_new_user_registration_wallet_reward`,为该用户写入 `10` 个初始泥点。
该赠送落在 `profile_dashboard_state.wallet_balance``profile_wallet_ledger` 中,流水来源为 `new_user_registration_reward`,流水 ID 固定为 `new-user-registration:{user_id}`,用于保证重复调用不重复发放。已有账号登录不得再次发放。
## 6. 完成定义
1. 登录弹窗内不可见注册入口。
2. 短信登录创建新账号后弹出邀请码面板。
3. 邀请码为空时按钮为 `跳过`,非空时按钮为 `提交`
4. 取消按钮可关闭面板。
5. 已登录邀请码接口允许提交,并继续由 SpacetimeDB procedure 兜底业务校验。
6. 新账号创建成功后默认获得 `10` 个泥点,且重复登录或重试不得重复发放。
7. 前端测试覆盖注册入口删除、新账号弹窗、URL 邀请码预填与提交。

View File

@@ -1,205 +0,0 @@
# `/api/auth/refresh` 轮换落地设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2``实现 refresh token 轮换` 任务的首版落地,冻结:
1. `POST /api/auth/refresh` 的请求与响应 contract。
2. refresh cookie、服务端 refresh session 与 access token 三者的职责边界。
3. “会话 ID 稳定、refresh token 可轮换”的固定语义。
4. Rust 首版在未切入 SpacetimeDB 前的临时进程内实现方式。
## 2. 当前基线
当前 Node `/api/auth/refresh` 已具备以下稳定语义:
1. 从 HttpOnly cookie 中读取原始 refresh token。
2. 服务端只按 `refresh_token_hash` 查找当前活跃会话。
3. refresh 成功后,不新建第二条会话,而是在原会话上轮换 refresh token。
4. 轮换时会更新 `expires_at``last_seen_at`
5. 成功后返回新的 access token并写回新的 refresh cookie。
6. 失败时会主动清空 refresh cookie要求前端重新登录。
Rust 首版必须保留以上语义。
## 3. 当前阶段范围
本阶段只落以下内容:
1. `module-auth` 增加进程内 refresh session 真相与轮换服务。
2. `api-server` 暴露 `POST /api/auth/refresh`
3. 登录成功时创建 refresh session。
4. refresh 成功时在原 session 上轮换 refresh token。
5. access token 的 `sid` 固定改为稳定 `session_id`,不再直接复用 refresh token。
本阶段不包含:
1. `/api/auth/logout`
2. `/api/auth/logout-all`
3. `/api/auth/sessions`
4. `/api/auth/sessions/:sessionId/revoke`
5. SpacetimeDB reducer 真正写表
## 4. contract
### 4.1 请求
1. 方法:`POST`
2. 路径:`/api/auth/refresh`
3. 请求体:空
4. 鉴权来源refresh cookie
### 4.2 成功响应
```json
{
"token": "<access-token>"
}
```
同时响应头必须写回新的 refresh cookie。
### 4.3 失败响应
当 refresh token 缺失、会话不存在、会话已过期或用户不存在时:
1. 返回 `401 UNAUTHORIZED`
2. 同时清理 refresh cookie
## 5. 固定语义
### 5.1 session_id 与 refresh token 必须拆开
从本任务开始固定以下规则:
1. `session_id` 是稳定会话主键。
2. refresh token 是可轮换的会话凭证。
3. access token 的 `sid` 必须写入 `session_id`
4. refresh 轮换只更新 refresh token不更改 `session_id`
禁止继续把 refresh token 直接塞进 JWT `sid`
### 5.2 refresh 是“原会话轮换”
refresh 成功后:
1. 保留原 `session_id`
2. 生成新的原始 refresh token
3. 用新的 `refresh_token_hash` 覆盖旧值
4. 更新 `expires_at`
5. 更新 `last_seen_at`
不允许新建第二条 session。
## 6. crate 边界
### 6.1 `module-auth`
负责:
1. 管理 refresh session 进程内真相。
2. 提供创建 refresh session 与轮换 refresh session 的用例。
3. 提供按 `user_id` 查询用户快照的能力,供 refresh 成功后重新签发 access token。
不负责:
1. 生成原始 refresh token。
2. 读写 cookie。
3. 签发 JWT。
### 6.2 `platform-auth`
负责:
1. 生成原始 refresh token。
2. 对 refresh token 做哈希。
3. 构造 refresh cookie 的 `Set-Cookie` 头。
4. 从 cookie header 中读取 refresh token。
### 6.3 `api-server`
负责:
1. 从请求 cookie 中提取 refresh token。
2. 调用 `module-auth` 执行 refresh session 轮换。
3. 根据用户快照与稳定 `session_id` 重新签发 access token。
4. refresh 失败时清理 cookie。
## 7. 进程内存储模型
当前阶段 `module-auth` 继续使用进程内内存仓储承接 refresh session字段至少包括
1. `session_id`
2. `user_id`
3. `refresh_token_hash`
4. `issued_by_provider`
5. `expires_at`
6. `created_at`
7. `updated_at`
8. `last_seen_at`
9. `revoked_at`
说明:
1. 这只是 SpacetimeDB 正式落地前的阶段性实现。
2. 字段命名与语义继续对齐 [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)。
## 8. 流程
### 8.1 登录创建 session
密码登录成功后:
1. `api-server` 生成原始 refresh token。
2. `api-server` 计算 `refresh_token_hash`
3. `module-auth` 创建一条新 session并返回稳定 `session_id`
4. `api-server` 用该 `session_id` 写入 access token 的 `sid`
5. `api-server` 把原始 refresh token 写回 cookie。
### 8.2 refresh 轮换 session
当请求 `POST /api/auth/refresh` 时:
1. 从 cookie 中读取原始 refresh token。
2. 计算 `refresh_token_hash`
3. `module-auth` 查找当前活跃 session。
4. 校验 `expires_at > now``revoked_at == null`
5. 读取该 session 对应用户。
6. 生成新的原始 refresh token。
7. 用新 hash 更新同一条 session。
8. 返回新的 access token 与新的 refresh cookie。
## 9. 错误语义
以下情况统一返回 `401`
1. 缺少 refresh cookie
2. refresh token 命中不到 session
3. refresh session 已过期
4. refresh session 已吊销
5. session 对应用户不存在
错误文案统一保持中文,并沿用“当前登录态已失效,请重新登录”这类恢复导向语义。
## 10. 测试策略
至少覆盖:
1. 登录成功后可用 cookie 调用 `/api/auth/refresh`
2. refresh 成功会写回新的 cookie
3. refresh 成功返回新的 access token
4. refresh 后旧 refresh token 立即失效
5. 缺少 cookie 时返回 `401`
6. 无效 refresh token 时返回 `401` 且清理 cookie
## 11. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 侧已提供 `POST /api/auth/refresh`
2. access token `sid` 已改为稳定 `session_id`
3. refresh token 轮换成功时不创建新会话。
4. refresh 失败时会清理 cookie。
5. 文档、任务清单与测试已同步更新。

View File

@@ -1,66 +0,0 @@
# Auth refresh session 持久化热修方案
日期:`2026-04-24`
## 1. 背景
当前 Rust 鉴权链路已经具备 refresh cookie 自动续签能力access token 过期后,前端会调用 `POST /api/auth/refresh`,后端轮换 refresh token 并返回新的 access token。
`module-auth` 当前仍使用进程内 `InMemoryAuthStore` 保存账号与 refresh session。只要 `server-rs` 在 access token 生命周期内发生重启,浏览器侧 HttpOnly cookie 仍然存在,服务端却找不到对应账号与 session最终表现为约 `JWT_EXPIRES_IN` 后需要重新登录。
## 2. 本次目标
本次先落一个低风险持久化闭环,解决“后端重启导致 2 小时后必须重新登录”的线上体验问题:
1. 为当前 `InMemoryAuthStore` 增加 UTF-8 JSON 快照文件。
2. 在账号、手机号索引、微信身份、refresh session 发生变更后自动保存快照。
3. `api-server` 启动时从配置路径恢复快照。
4. 保持现有 `/api/auth/refresh``logout``sessions` 语义不变。
## 3. 非目标
本次不把完整认证域一次性迁入 SpacetimeDB 表,原因是 refresh session 独立持久化不足以解决问题refresh 成功后还需要按 `user_id` 读取账号快照重新签发 access token因此账号主数据也必须同源恢复。
SpacetimeDB 正式表接管仍按以下既有文档继续推进:
1. `docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md`
2. `docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md`
3. `docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md`
## 4. 配置
新增环境变量:
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `GENARRATIVE_AUTH_STORE_PATH` | `server-rs/.data/auth-store.json` | 当前 Rust 鉴权快照文件路径。相对路径按进程工作目录解析。 |
## 5. 数据边界
快照文件保存当前 Rust 鉴权服务已经在内存中维护的最小真相:
1. `next_user_id`
2. `users_by_username`
3. `phone_to_user_id`
4. `sessions_by_id`
5. `session_id_by_refresh_token_hash`
6. `wechat_identity_by_provider_uid`
7. `user_id_by_provider_union_id`
短信验证码和微信 OAuth state 不持久化,原因是它们是短生命周期挑战数据,重启后失效是可接受行为。
## 6. 安全约束
1. refresh token 原文仍只存在浏览器 HttpOnly cookie快照只保存 `sha256(refresh_token)`
2. 快照包含 `password_hash`、手机号映射和 refresh token hash部署时必须放在服务端私有目录不允许暴露到静态资源目录。
3. 快照写入必须使用 UTF-8并通过临时文件原子替换降低写坏风险。
## 7. 后续 SpacetimeDB 接管点
`user_account``auth_identity``refresh_session` 表及 reducer 全部落地后,替换策略如下:
1. 保留 `module-auth` 用例语义。
2. 把当前快照仓储替换为 SpacetimeDB 仓储适配器。
3. 启动时可提供一次性导入脚本,把 JSON 快照导入 SpacetimeDB 表。
4. 导入完成后禁用 `GENARRATIVE_AUTH_STORE_PATH` 快照写入。

View File

@@ -1,42 +0,0 @@
# 登录恢复与推荐页加载态收口修复
日期:`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`

View File

@@ -1,226 +0,0 @@
# `/api/auth/sessions` 会话列表与多端标识查询设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2``兼容 /api/auth/sessions` 的首版落地,冻结:
1. `GET /api/auth/sessions` 的请求与响应 contract
2. 当前设备识别方式与 `isCurrent` 语义
3. 多端登录识别字段如何从 `refresh_session` 派生到 DTO
4. Rust 首版在 Axum + 进程内 `module-auth` 下的最小实现边界
5. `2026-05-13` 会话组合并展示与远端踢下线闭环修复口径
## 2. 当前基线
当前 Node `/api/auth/sessions` 已具备以下稳定行为:
1. 依赖 Bearer JWT 确认用户身份
2. 从 refresh cookie 识别当前设备
3. 返回当前账号全部未吊销活跃会话
4. 每条记录给出端侧标签、最近活跃时间、到期时间、IP 脱敏信息与是否当前设备
当前问题是:
1. 旧实现只能粗略给出“网页端浏览器 / 移动端浏览器”
2. 无法稳定区分同设备不同浏览器
3. 无法区分微信内 H5 与微信小程序、小程序平台来源
因此本次 `/api/auth/sessions` 首版落地必须直接承接多端会话身份模型。
## 3. 设计输入
本任务直接受以下文档约束:
1. [MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md](./MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md)
2. [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)
3. [AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md](./AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md)
4. [AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md](./AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md)
## 4. 首版落地范围
本阶段只落以下内容:
1. `module-auth` 提供按 `user_id` 读取活跃 refresh session 列表的能力
2. `api-server` 暴露 `GET /api/auth/sessions`
3. 登录创建 session 时落库结构化客户端身份字段
4. 会话列表返回多端识别所需字段,并兼容旧 `clientLabel`
`2026-05-13` 起,本接口同时承担账号安全页的会话组读模型:
1. 后端按“同设备 + 同 IP”聚合活跃 `refresh_session`
2. 前端只消费后端聚合结果,不自行推断合并
3. `POST /api/auth/sessions/{sessionId}/revoke` 已纳入 Rust 实现,用于踢下线非当前会话
本阶段仍明确不包含:
1. SpacetimeDB reducer / view 正式读表
2. 登录方式、refresh token 轮换策略或账号安全页整体重设计
## 5. 请求与响应 contract
### 5.1 请求
1. 方法:`GET`
2. 路径:`/api/auth/sessions`
3. 请求体:空
4. 鉴权:
- Bearer JWT 必填
- refresh cookie 选填但建议携带,用于判断 `isCurrent`
### 5.2 成功响应
```json
{
"sessions": [
{
"sessionId": "usess_xxx",
"sessionIds": ["usess_xxx", "usess_yyy"],
"sessionCount": 2,
"clientType": "web_browser",
"clientRuntime": "chrome",
"clientPlatform": "windows",
"clientLabel": "Windows / Chrome",
"deviceDisplayName": "Windows / Chrome",
"miniProgramAppId": null,
"miniProgramEnv": null,
"userAgent": "Mozilla/5.0 ...",
"ipMasked": "203.0.*.*",
"isCurrent": true,
"createdAt": "2026-04-21T10:00:00Z",
"lastSeenAt": "2026-04-21T10:05:00Z",
"expiresAt": "2026-05-21T10:00:00Z"
}
]
}
```
字段说明:
1. `sessionId` 是聚合组代表会话 ID若组内包含当前 `sid`,代表 ID 必须使用当前会话 ID
2. `sessionIds` 是该聚合组内全部活跃 session ID前端批量踢下线时逐个调用 revoke
3. `sessionCount` 是聚合组内 session 数量
4. `clientLabel` 当前阶段继续兼容旧前端字段,值固定与 `deviceDisplayName` 保持一致
5. `clientRuntime``clientPlatform``deviceDisplayName` 是多端识别首版最小新增字段
6. 小程序来源额外暴露 `miniProgramAppId``miniProgramEnv`
### 5.3 失败响应
以下情况返回 `401 UNAUTHORIZED`
1. Bearer JWT 缺失或非法
2. Bearer JWT 对应用户不存在
仓储读取失败返回 `500 INTERNAL_SERVER_ERROR`
## 6. 当前设备识别规则
`isCurrent` 固定按以下规则判断:
1. 从 refresh cookie 读取当前原始 refresh token
2. 在 Axum 侧计算 `sha256(refresh_token)`
3. 与会话列表中的 `refresh_token_hash` 比较
4. 同时读取 Bearer access token claims 中的 `sid`
5. 聚合组内任意 session 命中当前 refresh hash 或当前 `sid`,则整组 `isCurrent = true`
说明:
1. 如果请求没有携带 refresh cookie本接口仍可返回会话列表
2. 此时仍可通过 Bearer `sid` 标记当前组
3. 当前组不允许在前端显示“踢下线”,当前设备退出必须走 `/api/auth/logout`
## 6.1 会话组合并规则
同设备同 IP 的 active refresh sessions 在后端合并为一条 DTO
1. 优先使用 `device_fingerprint + ip` 作为聚合 key
2.`device_fingerprint` 时退化为 `client_type + client_runtime + client_platform + device_display_name + user_agent + ip`
3. `createdAt` 取组内最早 `created_at`
4. `lastSeenAt` 取组内最新 `last_seen_at`
5. `expiresAt` 取组内最新 `expires_at`
6. `ipMasked` 仍只返回脱敏 IP
## 7. 多端标识派生规则
### 7.1 后端入库字段
登录创建会话时Axum 必须先解析并写入:
1. `client_type`
2. `client_runtime`
3. `client_platform`
4. `client_instance_id`
5. `device_fingerprint`
6. `device_display_name`
7. `mini_program_app_id`
8. `mini_program_env`
9. `user_agent`
10. `ip`
### 7.2 DTO 派生规则
会话列表返回时:
1. `clientType = client_type`
2. `clientRuntime = client_runtime`
3. `clientPlatform = client_platform`
4. `deviceDisplayName = device_display_name`
5. `clientLabel = device_display_name`
6. `miniProgramAppId = mini_program_app_id`
7. `miniProgramEnv = mini_program_env`
## 8. crate 边界
### 8.1 `module-auth`
负责:
1. 保存 refresh session 客户端身份快照
2.`user_id` 返回活跃会话列表
3. 保持 refresh 轮换后 `session_id` 稳定、客户端身份字段不漂移
### 8.2 `api-server`
负责:
1. 读取 Bearer JWT 与 refresh cookie
2. 按同设备同 IP 聚合活跃会话
3. 把活跃会话组映射成旧接口兼容 DTO
4. 派生 `ipMasked``isCurrent`
5. 暴露 `POST /api/auth/sessions/{sessionId}/revoke`
## 8.3 指定会话吊销接口
`POST /api/auth/sessions/{sessionId}/revoke` 固定规则:
1. Bearer JWT 必填
2. 仅允许吊销当前用户自己的非当前会话
3. 当前会话自吊销返回业务错误,提示使用退出登录
4. 只撤销目标 `refresh_session`,不递增 `token_version`
5. 撤销后同步 auth store 到 SpacetimeDB
6. 认证中间件会校验 access token `sid` 对应 active `refresh_session`,因此被踢设备已有 access token 会立即失效
## 9. 测试策略
至少覆盖:
1. 同一账号在同平台不同浏览器登录后,会话列表能返回两条不同运行时记录
2. 微信内 H5 登录后,会话列表返回 `wechat_h5 + wechat_embedded_browser`
3. 显式小程序头优先于 `User-Agent` 判断
4. 请求携带当前 refresh cookie 时,只有当前会话 `isCurrent = true`
5. 同设备同 IP 会话会合并,并返回 `sessionIds/sessionCount`
6. 合并组包含当前 `sid` 或当前 refresh hash 时,整组 `isCurrent = true`
7. 指定远端会话吊销后,被踢设备 access token 立即无法通过认证
## 10. 完成定义
满足以下条件时,本任务视为完成:
1. Rust 侧已提供 `GET /api/auth/sessions`
2. 会话列表可区分普通浏览器、微信内 H5、小程序来源
3. 同设备不同浏览器可在会话列表中清晰区分
4. `clientLabel` 与新增多端字段都已稳定返回
5. 同设备同 IP 的重复 active refresh sessions 已合并展示
6. 非当前会话可通过真实 revoke 接口踢下线
7. 文档、任务清单与测试已同步更新

View File

@@ -1,122 +0,0 @@
# 认证快照同步与抓大鹅本地联调修复记录
日期:`2026-05-01`
## 1. 现场问题
本地访问 `http://127.0.0.1:3000` 时出现两类失败:
1. 验证码登录成功后,接口返回 `同步认证快照失败`
2. 抓大鹅创作页请求报 `Failed to initiate WebSocket connection ... HTTP error: 503 Service Unavailable`,或同源创作接口直接 `404`
## 2. 根因
### 2.1 远端目标库挂起
CLI 直接查询 `xushi-p4wfr` 返回:
```text
Error: database is suspended
HTTP status server error (503 Service Unavailable)
```
这说明远端 SpacetimeDB 入口在线,但具体数据库 `xushi-p4wfr` 当前不可订阅、不可查 schema、不可执行 SQL。所有依赖该库的 procedure 都会失败。
### 2.2 认证快照同步被当成硬失败
手机号、密码、刷新、退出等认证流程会先更新本地 `auth_store`,然后调用 SpacetimeDB 同步认证快照。旧逻辑把同步失败直接转为 HTTP 500导致本地会话已经创建成功响应却被远端快照同步失败阻断。
### 2.3 Vite 未代理 `/api/creation`
抓大鹅创作接口挂在:
```text
/api/creation/match3d/*
```
但 Vite 代理只覆盖了 `/api/auth``/api/runtime` 等路径,未覆盖 `/api/creation`,因此浏览器同源请求会被 Vite 返回 `404`,没有进入 Rust `api-server`
## 3. 修复
### 3.1 认证快照同步改为非阻断
`AppState::sync_auth_store_snapshot_to_spacetime` 保持先导出本地认证快照,但运行期会直接调用 `import_auth_store_snapshot_json` 覆盖导入 SpacetimeDB 正式认证表,不再刷新 `auth_store_snapshot/default`;当远端导入失败时只写 warn 日志并返回 `Ok(())`
设计边界:
1. 当前认证请求的即时真相源是本地 `auth_store`
2. SpacetimeDB 正式认证表用于跨进程恢复;`auth_store_snapshot/default` 只保留为历史迁移和兜底恢复记录。
3. 远端库挂起或网络异常只降级远端恢复能力,不回滚已经成功的登录、刷新、退出和资料更新。
### 3.2 Vite 补齐创作接口代理
`vite.config.ts` 新增:
```ts
'/api/creation': {
target: runtimeServerTarget,
changeOrigin: true,
secure: false,
},
```
前端仍只请求同源 `/api/creation/match3d/*`,不直连 Rust 端口。
## 4. 本地可跑链路
远端 `xushi-p4wfr` 挂起期间,抓大鹅本地体验应使用本地 SpacetimeDB
```powershell
npm run dev:rust
$env:GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="codex-local-bootstrap-secret-20260501"
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 指向本地库:
```powershell
$env:GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3101"
$env:GENARRATIVE_SPACETIME_DATABASE="xushi-p4wfr"
$env:GENARRATIVE_SPACETIME_TOKEN=""
npm run api-server
```
最后重启前端:
```powershell
$env:RUST_SERVER_TARGET="http://127.0.0.1:3100"
$env:GENARRATIVE_RUNTIME_SERVER_TARGET="http://127.0.0.1:3100"
npm run dev:web
```
## 5. 验证结果
已验证:
1. `GET http://127.0.0.1:3000/api/auth/login-options` 返回 `["phone","password"]`
2. `GET http://127.0.0.1:3000/api/runtime/match3d/gallery` 返回 `{"items":[]}`,不再返回 SpacetimeDB 503。
3. 未登录请求 `POST http://127.0.0.1:3000/api/creation/match3d/sessions` 返回 `401`,说明同源请求已进入 Rust 鉴权层,不再被 Vite `404`
4. 隔离端口指向挂起的远端库并使用 mock 短信时,手机号验证码登录返回 `200` 和 token日志只记录“认证快照导入 SpacetimeDB 正式表失败,当前认证流程继续”。
## 6. 后续
1. 远端 `xushi-p4wfr` 仍需恢复数据库挂起状态,否则对应玩法 procedure 仍不可用。
2. 本地开发如只为体验抓大鹅,可继续使用本地 SpacetimeDB 链路。
3. 认证快照同步失败会影响进程重启后的远端恢复完整性,需要在目标库恢复后重新完成一次成功同步。
## 7. 2026-05-13 补充:服务暂不可用的分层排查
抓大鹅生成页只看到“服务暂不可用”时,不应先回退旧 3D / Rodin 链路,应按 2D 素材生成链路逐层定位:
1. 前端通用 API 错误展示必须读取 `error.details.reason`。VectorEngine、OSS 等配置缺失类错误常把具体原因写在 `details.reason`,如果只读 `details.message`,用户只能看到泛化的“服务暂不可用”。
2. `api-server` 启动不应被 OSS 半配置阻断。`ALIYUN_OSS_BUCKET` / `ALIYUN_OSS_ENDPOINT` 已配置但 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET` 缺失时,服务应记录 warning 并跳过 OSS 客户端初始化;需要上传或读取 generated 私有资产的接口继续返回 `OSS 未完成环境变量配置`,并通过 `details.missingEnv` 明确缺少哪几项。
3. 启动阶段从 SpacetimeDB 恢复认证快照只能降级远端恢复能力,不能长期卡住 `/healthz`。本地库未发布、连接后立即 close 或远端库挂起时,`api-server` 应在启动恢复超时后使用本地 `auth_store` 继续进入监听。
4. 抓大鹅真实生成仍依赖两组私密配置:`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 用于 `gpt-image-2-all` 生成 1K 素材图;完整 `ALIYUN_OSS_*` 四件套用于上传切割后的 `generated-match3d-assets` 五视角图片。缺任一组都应返回明确 `details.reason`;抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS不应先消耗生图调用再失败也不应恢复 GLB 生成。
验证时以实际 `GENARRATIVE_API_PORT` 为准;本地 `.env.local` 可能覆盖脚本默认 `3100`。例如当前本地端口为 `8082` 时,应请求:
```powershell
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8082/healthz
```

View File

@@ -1,54 +0,0 @@
# Auth SpacetimeDB 正式表恢复 Stage 3
## 1. 阶段目标
本阶段把认证持久化从“只依赖整包快照恢复”推进到“正式认证表优先恢复”。
落地口径:
- `user_account``auth_identity``refresh_session` 作为 SpacetimeDB 中的正式认证持久化表。
- `auth_store_projection_meta` 只记录正式认证表最近一次由认证快照导入的时间,不保存用户快照内容。
- API 启动时优先从正式表导出兼容 `module-auth` 的认证快照,再恢复到内存认证服务。
- 运行期认证变更仍先复用现有 `module-auth` 逻辑生成一致快照,随后调用 `import_auth_store_snapshot_json` 直接覆盖导入正式表;不再继续刷新 `auth_store_snapshot/default`
- 本阶段不重写登录、刷新、登出内部业务规则,避免在 JWT、refresh rotation、微信绑定合并等复杂语义中引入行为漂移。
## 2. 非目标
- 不在本阶段把 `PasswordEntryService``PhoneAuthService``RefreshSessionService` 改造成直接调用 SpacetimeDB reducer。
- 不在前端增加认证规则说明文案。
- 不删除 Stage 1 快照表;快照表继续作为导入载体与回滚兜底。
## 3. 运行流程
### 3.1 启动恢复
1. API 调用 `export_auth_store_snapshot_from_tables`
2. 若正式表已有用户、身份或会话数据,则返回兼容 `module-auth` 的 JSON 快照,并带上 `auth_store_projection_meta/default.updated_at`
3. API 用 `InMemoryAuthStore::from_snapshot_json` 恢复认证服务。
4. 若正式表为空或调用失败,则回退到 Stage 1 的 `auth_store_snapshot`
5. 若 Stage 1 也不可用,则回退本地 JSON 热修复文件。
### 3.2 运行期同步
1. 登录、刷新、登出等路径继续调用当前内存认证服务。
2. 每次认证状态变更后导出当前内存认证快照 JSON。
3. API 调用 `import_auth_store_snapshot_json`,在同一 SpacetimeDB transaction 中清空并重建 `user_account/auth_identity/refresh_session`,同时更新 `auth_store_projection_meta/default.updated_at`
4. `upsert_auth_store_snapshot``import_auth_store_snapshot` 保留为旧库迁移入口,只服务 `auth_store_snapshot/default` 到正式认证表的历史导入,不作为运行期同步路径。
5. 远端导入失败只记录 warn 并继续当前认证响应,避免远端库挂起时回滚已经成功的登录、刷新、退出和资料更新。
## 4. 数据重建规则
- `users_by_username``user_account.username` 作为 key 重建。
- `phone_to_user_id` 由 provider 为 `phone``auth_identity` 重建。
- `wechat_identity_by_provider_uid` 由 provider 为 `wechat``auth_identity` 重建。
- `user_id_by_provider_union_id` 由微信身份中非空 `provider_union_id` 重建。
- `sessions_by_id``refresh_session.session_id` 重建。
- `session_id_by_refresh_token_hash``refresh_session.refresh_token_hash` 重建。
- `next_user_id` 取现有 `user_id``user_数字` 的最大值加一,若不存在则为 1。
## 5. 完成定义
- SpacetimeDB 模块能 wasm 编译。
- Rust bindings 已重新生成并包含正式表导出 procedure。
- `spacetime-client` 暴露正式表导出 facade。
- `api-server` 启动恢复优先正式表,认证变更同步后导入正式表。
- `module-auth` 测试保持通过。

View File

@@ -1,83 +0,0 @@
# Auth SpacetimeDB 快照迁移 Stage 1
日期:`2026-04-24`
## 1. 背景
`AUTH_REFRESH_SESSION_PERSISTENCE_HOTFIX_2026-04-24.md` 已把 Rust 鉴权内存态落到本地 JSON 快照,解决 `server-rs` 重启后 refresh session 丢失导致 `JWT_EXPIRES_IN` 到期后必须重新登录的问题。
本阶段继续把该快照迁入 SpacetimeDB作为正式 `user_account/auth_identity/refresh_session` 表完全拆分前的过渡真相源。
## 2. 阶段目标
1.`spacetime-module` 新增私有 `auth_store_snapshot` 表。
2. 表内只保存一条当前 Axum 鉴权快照 JSON主键固定为 `default`
3. 新增 `get_auth_store_snapshot``upsert_auth_store_snapshot` procedure`api-server` 同步读写。
4. `module-auth` 继续拥有鉴权业务语义SpacetimeDB 只承接当前阶段的持久化真相。
## 3. 为什么先做快照表
只迁 `refresh_session` 表无法恢复登录态,因为 refresh 成功后仍必须按 `user_id` 找到账号快照重新签发 access token。因此正式拆表必须同时完成账号、身份、会话三组 reducer。
本阶段先把已经验证过的 JSON 快照从本地文件迁到 SpacetimeDB收益是
1. 后端进程重启后可恢复登录态。
2. 多实例部署时可共享同一份鉴权快照。
3. 后续正式拆表时有统一导入来源。
## 4. 表设计
表名:`auth_store_snapshot`
访问级别private table
字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `snapshot_id` | `String` | 主键,固定为 `default`。 |
| `snapshot_json` | `String` | `module-auth` 当前持久化快照 JSON。 |
| `updated_at` | `Timestamp` | 最近写入时间。 |
## 5. Procedure 设计
### 5.1 `get_auth_store_snapshot`
输入:无。
输出:
```json
{
"ok": true,
"snapshotJson": "...",
"updatedAtMicros": 123456789,
"errorMessage": null
}
```
当记录不存在时,`snapshotJson = null`
### 5.2 `upsert_auth_store_snapshot`
输入:
```json
{
"snapshotJson": "...",
"updatedAtMicros": 123456789
}
```
输出同 `get_auth_store_snapshot`
## 6. 后续拆表迁移点
Stage 2 再把 `auth_store_snapshot.snapshot_json` 导入并拆分为:
1. `user_account`
2. `auth_identity`
3. `refresh_session`
拆分完成后,`auth_store_snapshot` 只保留为迁移备份,不再作为运行时写入目标。

View File

@@ -1,96 +0,0 @@
# Auth SpacetimeDB 拆表 Stage 2
日期:`2026-04-24`
## 1. 阶段目标
Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot` 表。本阶段继续把该快照导入正式认证表,建立后续运行时细粒度读写的表结构基础。
本阶段落地范围:
1. 新增 `user_account` 表。
2. 新增 `auth_identity` 表。
3. 新增 `refresh_session` 表。
4. 新增 `import_auth_store_snapshot` procedure把当前 `auth_store_snapshot.snapshot_json` 拆入三张表。
5. 保留 Stage 1 快照表作为导入来源与回滚备份。
## 2. 非目标
本阶段不立即把 `api-server` 的登录、refresh、logout 写入切换到细粒度 reducer。原因是要避免同时改动认证业务语义、导入逻辑和运行时写路径。
运行时切换放到 Stage 3
1. `POST /api/auth/refresh` 改写 `refresh_session` 表。
2. 登录成功写 `user_account/auth_identity/refresh_session`
3. `logout/logout-all/revoke-session` 改写细粒度表。
4. `auth_store_snapshot` 退化为迁移备份;运行期若仍复用内存认证快照,也应通过 `import_auth_store_snapshot_json` 直接导入正式认证表,不再刷新 `auth_store_snapshot/default`
## 3. 表设计落地口径
### 3.1 `user_account`
字段先覆盖当前 `module-auth` 快照可提供的账号主数据:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `String` | 主键。 |
| `public_user_code` | `String` | 公开陶泥号。 |
| `username` | `String` | 当前账号用户名。 |
| `display_name` | `String` | 展示名。 |
| `phone_number_masked` | `Option<String>` | 脱敏手机号。 |
| `phone_number_e164` | `Option<String>` | 内部手机号索引。 |
| `login_method` | `String` | `password/phone/wechat`。 |
| `binding_status` | `String` | `active/pending_bind_phone`。 |
| `wechat_bound` | `bool` | 是否绑定微信身份。 |
| `password_hash` | `String` | 密码哈希。 |
| `password_login_enabled` | `bool` | 是否允许密码登录。 |
| `token_version` | `u64` | access token 统一失效版本。 |
### 3.2 `auth_identity`
当前只导入已有快照中的微信身份与手机号身份:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `identity_id` | `String` | 主键。 |
| `user_id` | `String` | 归属账号。 |
| `provider` | `String` | `phone/wechat`。 |
| `provider_uid` | `String` | provider 主体键。 |
| `provider_union_id` | `Option<String>` | 微信 unionid。 |
| `phone_e164` | `Option<String>` | 手机号身份。 |
| `display_name` | `Option<String>` | provider 显示名。 |
| `avatar_url` | `Option<String>` | provider 头像。 |
### 3.3 `refresh_session`
字段对齐现有 refresh session 记录:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `session_id` | `String` | 主键。 |
| `user_id` | `String` | 归属账号。 |
| `refresh_token_hash` | `String` | 当前 refresh token hash。 |
| `issued_by_provider` | `String` | 创建来源。 |
| `client_info_json` | `String` | 当前客户端身份 JSON。 |
| `expires_at` | `String` | RFC3339。 |
| `revoked_at` | `Option<String>` | RFC3339。 |
| `created_at` | `String` | RFC3339。 |
| `updated_at` | `String` | RFC3339。 |
| `last_seen_at` | `String` | RFC3339。 |
## 4. 导入语义
`import_auth_store_snapshot` 固定行为:
1. 读取 `auth_store_snapshot/default`
2. JSON 解析失败返回 `ok=false` 和中文错误。
3. 导入前清空三张正式 auth 表,避免重复导入产生脏数据。
4. 按快照内容重建账号、身份、refresh session。
5. 返回导入计数,便于本地验证。
## 5. 完成定义
1. `spacetime-module` wasm check 通过。
2. Rust bindings 已刷新。
3. `spacetime-client` 暴露导入 procedure facade。
4. `api-server/spacetime-client/module-auth` 定向检查通过。

View File

@@ -1,237 +0,0 @@
# Axum 到 SpacetimeDB 的资产对象确认调用方案
日期:`2026-04-21`
## 1. 文档目的
这份文档用于冻结 `POST /api/assets/objects/confirm` 从当前“进程内确认写入”切换到“真实 SpacetimeDB 持久化”的最小落地方案。
当前要解决的问题只有一个:
1. `api-server` 在完成 OSS `HEAD Object` 校验后,如何把确认结果稳定写入 `spacetime-module.asset_object`
这份文档需要把以下信息冻结到可以直接编码的级别:
1. 本地 SpacetimeDB server 口径
2. 本地数据库名
3. `spacetime-module` 内部 reducer / procedure 的职责分工
4. `spacetime-client` 在当前阶段的最小实现方式
5. `api-server` 的切换边界
## 2. 当前约束
已确认事实如下:
1. 阿里云 OSS 当前按私有 bucket 接入。
2. `api-server` 当前已经完成:
- `POST /api/assets/direct-upload-tickets`
- `GET /api/assets/read-url`
- `POST /api/assets/objects/confirm`
3. `platform-oss` 已具备:
- `PostObject` 直传签名
- 私有 `GET` 签名 URL
- 私有 `HEAD Object` 探测
4. `spacetime-module` 当前已具备:
- `asset_object` 首版表骨架
- `bucket + object_key` 双列定位索引
5. 当前 `module-assets` 的对象确认仍然写入进程内 store不是正式数据库真相。
## 3. 当前阶段的职责拆分
### 3.1 Axum 负责的部分
`api-server` 当前阶段继续负责以下职责:
1. 接收 HTTP 请求
2. 校验请求体字段
3. 校验 `bucket` 与服务端配置的一致性
4. 调用 OSS `HEAD Object`
5. 组装“已确认对象元数据”
6. 调用 SpacetimeDB 持久化入口
7. 把持久化结果转换成当前 HTTP 响应 contract
### 3.2 SpacetimeDB 负责的部分
`spacetime-module` 当前阶段只负责以下纯状态职责:
1. 依据 `bucket + object_key` 查重
2. 已存在则复用原 `asset_object_id``created_at`
3. 已存在则更新 `updated_at` 与最新元数据
4. 不存在则插入新对象行
5. 返回持久化后的对象记录
### 3.3 不允许的职责漂移
当前阶段明确不允许:
1. 在 reducer / procedure 内直接访问 OSS
2. 在 reducer / procedure 内直接访问 HTTP 请求头、Cookie 或 Axum context
3.`api-server` 内重新实现第二套 `asset_object` 去重规则
4. 通过 CLI 文本解析做正式持久化主链
## 4. 本地开发口径冻结
### 4.1 本地 server 地址
从当前版本开始,`server-rs/scripts/spacetime-dev.ps1``server-rs/scripts/spacetime-dev.sh` 的默认监听口径统一为:
1. `127.0.0.1:3000`
原因固定如下:
1. `spacetime` CLI 的默认 `local` server 昵称当前指向 `http://127.0.0.1:3000`
2. 若脚本默认改到 `3001`,则 `publish / call / generate` 与本地调试口径会长期错位
3. `api-server` 默认占用 `3000` 仅限当前进程,不影响 SpacetimeDB 独立开发脚本通过单独终端启动
补充说明:
1. 若需要与本地 Axum 同时运行,可显式传参改端口。
2. 默认口径必须先回到 CLI 约定,避免文档、脚本和发布命令长期分叉。
### 4.2 本地数据库名
当前资产对象确认链路的本地数据库名固定为:
1. `genarrative-dev`
原因固定如下:
1. 名称满足 `spacetime publish` 的数据库命名规则
2. 后续 auth / runtime / asset 的 schema 可以先统一聚合到同一开发数据库
3. 当前仓库还没有按模块拆分多个独立数据库的明确方案
### 4.3 当前阶段的标准命令
本地开发标准命令固定如下:
```bash
spacetime start --listen-addr 127.0.0.1:3000
spacetime publish genarrative-dev --server local --yes --module-path server-rs/crates/spacetime-module
npm run spacetime:generate
```
## 5. 为什么当前阶段不用 CLI 文本解析
当前阶段不采用 `spacetime call / spacetime sql` 的正式主链方案,原因固定如下:
1. CLI 输出更适合人工调试,不适合作为稳定业务协议层
2. `api-server` 若依赖命令行文本解析,会把错误处理、超时、返回值解析和平台兼容复杂度放大
3. 当前 `asset_object` 是 private table不适合继续叠加一层 CLI 查询拼装返回值
因此当前阶段改为:
1. 用 SpacetimeDB Rust SDK + codegen bindings 做正式调用
2.`procedure` 返回确认结果
3.`try_with_tx` 在 procedure 内完成原子 upsert
## 6. `spacetime-module` 的调用面设计
### 6.1 reducer
当前阶段保留一个内部 reducer
1. `confirm_asset_object`
职责:
1. 承载真实 upsert 规则
2. 便于后续被其他 reducer 或 scheduled logic 复用
3. 明确 `asset_object` 的唯一写入规则在模块内收口
返回规则:
1. reducer 只返回 `Result<(), String>`
2. 不直接返回对象记录
### 6.2 procedure
当前阶段新增一个对 Axum 友好的 procedure
1. `confirm_asset_object_and_return`
职责:
1. 接收 Axum 已经确认好的对象元数据
2.`try_with_tx` 中调用共享 upsert 逻辑
3. 返回持久化后的 `asset_object` DTO
原因固定如下:
1. `POST /api/assets/objects/confirm` 是同步确认接口,需要立即返回 `assetObjectId` 等字段
2. reducer 本身不返回业务数据
3. procedure 可以在不做外部 IO 的前提下返回 `SpacetimeType` 结果
## 7. `spacetime-client` 当前阶段设计
`spacetime-client` 当前阶段只实现一条最小链路:
1. 连接指定 SpacetimeDB server
2. 等待 SDK `on_connect` 回调确认连接已经收到 `IdentityToken`
3.`on_connect` 后调用 `confirm_asset_object_and_return`
4. 获取返回 DTO
实现约束固定如下:
1. 不允许在 `DbConnection::build()` 返回后立刻发 procedure因为 build 只代表 WebSocket 初始化完成,不代表 SpacetimeDB 身份握手已经完成。
2. procedure 调用、异步连接失败、断线和超时必须收口到同一个结果通道,避免 HTTP 请求在 SDK idle timeout 后才失败。
3. 当前阶段每次 HTTP 确认请求可以建立一条短连接,待真实链路验证稳定后再评估连接池或长连接复用。
当前阶段不做:
1. 通用订阅框架
2. 多数据库路由
3. 通用 reducer / procedure 适配器
4. 前端直连复用层
## 8. `api-server` 的切换规则
从当前版本开始,`POST /api/assets/objects/confirm` 的真实主链改为:
1. `api-server`
2. `platform-oss HEAD Object`
3. `spacetime-client`
4. `spacetime-module.confirm_asset_object_and_return`
5. `asset_object`
当前进程内 `AssetObjectService` 退化为:
1. 共享字段校验
2. 共享 Axum 侧对象确认编排
3. 本地无 SpacetimeDB 配置时的临时 fallback 不再作为默认主链
## 9. 环境变量口径
当前阶段新增以下环境变量:
1. `GENARRATIVE_SPACETIME_SERVER_URL`
默认 `http://127.0.0.1:3000`
2. `GENARRATIVE_SPACETIME_DATABASE`
默认 `genarrative-dev`
3. `GENARRATIVE_SPACETIME_TOKEN`
可选;未配置时默认匿名连接
补充说明:
1. 当前本地开发可以先匿名连接。
2. 后续若要做 Axum -> SpacetimeDB 的身份透传,再单独冻结 JWT / OIDC token 的传递策略。
## 10. 当前阶段验收标准
当以下条件满足时,本方案视为落地完成:
1. `[x]` `spacetime-module` 新增 `confirm_asset_object` reducer 与 `confirm_asset_object_and_return` procedure
2. `[x]` `spacetime-client` 能调用该 procedure 并拿到返回值
3. `[x]` `api-server` 默认不再依赖进程内对象 store 作为正式真相
4. `[x]` `server-rs/scripts/spacetime-dev.*` 默认端口回到 `3000`
5. `[x]` 本地可以通过 publish + API 测试跑通真实 `asset_object` 写入
`2026-04-21` 已完成验收:
1. `cargo test -p api-server confirm_asset_object_live_roundtrip_persists_confirmed_record --manifest-path server-rs/Cargo.toml -- --ignored --nocapture --test-threads=1` 通过。
2. `spacetime sql genarrative-dev --server local -y "SELECT asset_object_id, bucket, object_key, asset_kind, content_length FROM asset_object"` 可查到 `bucket = "xushi-dev"``generated-characters/confirm-live-test/.../master.txt` 的确认记录。
## 11. 一句话结论
当前阶段 `POST /api/assets/objects/confirm` 的正式主链应当是:
**Axum 完成 OSS 校验,再通过 Rust SDK 调用 SpacetimeDB procedure在模块内部用事务 upsert `asset_object` 并把最终对象记录同步返回。**

View File

@@ -1,202 +0,0 @@
# 宝贝爱画本地 Demo 运行态实现方案 2026-05-13
## 1. 范围
本方案落地寓教于乐独立关卡:
```text
baby-love-drawing / 宝贝爱画
```
当前范围只做本地 Demo 闭环:
1. 寓教于乐频道默认关卡卡片;
2. 独立运行态;
3. mocap 与开发者调试输入;
4. Canvas 绘制和擦除;
5. image-2 绘画魔法后端代理;
6. localStorage 本地保存;
7. 直达路由开关保护。
本阶段不接正式持久化表,不新增作品发布、作品号、公开详情或搜索入口。
## 2. 前端接入点
已新增页面阶段:
```text
baby-love-drawing-runtime
```
已新增路由:
```text
/runtime/baby-love-drawing
```
已新增文件:
```text
packages/shared/src/contracts/edutainmentBabyDrawing.ts
src/services/edutainment-baby-drawing/babyDrawingClient.ts
src/components/edutainment-runtime/babyLoveDrawingModel.ts
src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx
server-rs/crates/api-server/src/edutainment_baby_drawing.rs
```
已接入:
1. `src/components/rpg-entry/RpgEntryHomeView.tsx`:寓教于乐频道默认展示宝贝爱画卡片;
2. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`:启动宝贝爱画运行态;
3. `src/components/platform-entry/platformEntryTypes.ts`:扩展 `SelectionStage`
4. `src/routing/appPageRoutes.ts`:扩展路由;
5. `src/routing/appRoutes.tsx`:直达路由开关保护;
6. `src/index.css`:补齐寓教于乐默认关卡卡片和宝贝爱画运行态样式;
7. `server-rs/crates/api-server/src/app.rs`:挂载绘画魔法后端路由。
## 3. 契约
契约放在:
```text
packages/shared/src/contracts/edutainmentBabyDrawing.ts
```
核心字段:
1. `templateId = "baby-love-drawing"`
2. `templateName = "宝贝爱画"`
3. `originalImageSrc` 保存原始画布图;
4. `magicImageSrc` 保存 image-2 魔法图,可为 `null`
5. `strokeTrace` 保存画笔和橡皮轨迹;
6. `saveMode = "original-only" | "original-and-magic"` 记录保存结果。
## 4. 运行态模型
运行态状态:
```text
drawing
finished
magicPending
magicReady
saved
```
工具:
```text
brush
eraser
```
颜色:
```text
红、橙、黄、绿、青、蓝、紫
```
按钮悬停:
1. 颜色选择只接受左手悬停,阈值 1500ms
2. 按钮选择接受任一手悬停,阈值 2000ms
3. 工具切换只接受右手在工具区域握拳。
4. 画笔 / 橡皮光标位置只接受右手坐标;左手缺帧或左手移动不得重置、替换或驱动画笔位置。
5. 左手需要显示独立位置指示器,帮助用户确认当前是否悬停在目标颜色上;该指示器只表达左手位置,不参与画笔 / 橡皮操作。
6. 本地 mocap handedness 当前按摄像头视角输出,宝贝爱画运行态消费前需要换算为用户身体视角:`rightHand` 作为用户左手,`leftHand` 作为用户右手。键鼠调试输入不做该换算。
7. 真实硬件短暂缺失某只手时,显示层保留上一帧位置约 320ms 并做轻微坐标平滑;绘制层仍只在当前帧确认用户右手存在时生效。
8. 为避免左手抢画笔,本关不做动态 handedness 换手纠正;`rightHand` 永远只进入用户左手选色通道,`leftHand` 永远只进入用户右手画笔通道。若硬件侧 handedness 继续抖动,宁可右手画笔短暂停住,也不允许左手驱动画笔。
9. 右手画笔通道增加单帧最大位移门禁;若 camera-left 候选点相对上一帧右手位置出现不合理大跳,判定为不可信帧,只保留上一帧光标并停止绘制。
## 5. Canvas 绘制
画板使用 DOM Canvas。
绘制规则:
1. 右手在画板内且状态为 `grab` 时生效;
2. 工具为 `brush` 时,以当前颜色绘制连续线段;
3. 工具为 `eraser` 时,以 `destination-out` 擦除;
4. 右手状态为 `open_palm` 或离开画板时结束当前笔画;
5. 当前帧没有右手坐标时只结束当前笔画,不把左手坐标用于绘制、擦除或光标定位;
6. 每条笔画记录工具、颜色、点位和时间。
## 6. 绘画魔法
前端 service
```text
createBabyDrawingMagicImage(payload)
```
后端接口:
```text
POST /api/creation/edutainment/baby-love-drawing/magic
```
请求体:
```json
{
"originalImageSrc": "data:image/png;base64,...",
"strokeTrace": []
}
```
响应体:
```json
{
"magicImageSrc": "data:image/png;base64,...",
"generationProvider": "vector-engine-gpt-image-2",
"prompt": "..."
}
```
后端使用 VectorEngine `gpt-image-2-all`,把原始画布图作为参考图,生成绘本风格图片。
本地未配置 VectorEngine 或接口失败时,前端允许提示错误并保留原图保存能力;不得把失败伪装成正式魔法图。
后端接入约束:
1. 接口需要 Bearer 鉴权;
2. 请求体限制为 8MB
3. `originalImageSrc` 只接受图片 Data URL
4. 笔触数量上限为 600 条;
5. 上游参考图字段使用 VectorEngine 统一契约 `image`
6. 关闭入口时,`creation_entry_config` 路由熔断可识别 `baby-love-drawing`
## 7. 本地保存
本地保存使用:
```text
localStorage key = genarrative.edutainmentBabyDrawing.localDrawings.v1
```
保存策略:
1. 魔法生成前保存:`saveMode = "original-only"`,只保存 `originalImageSrc`
2. 未保存原图直接生成魔法后保存:`saveMode = "original-and-magic"`,保存 `originalImageSrc``magicImageSrc`
3. 保存后展示“再画一张”和“返回”。
## 8. 验收命令
```bash
npm run test -- src/components/edutainment-runtime/babyLoveDrawingModel.test.ts src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx src/services/edutainment-baby-drawing/babyDrawingClient.test.ts src/routing/appRoutes.test.ts
cargo test -p api-server edutainment_baby_drawing --manifest-path server-rs/Cargo.toml
cargo test -p api-server resolves_runtime_paths_to_creation_type_ids --manifest-path server-rs/Cargo.toml
npx eslint src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx src/components/edutainment-runtime/babyLoveDrawingModel.ts src/services/edutainment-baby-drawing/babyDrawingClient.ts src/routing/appRoutes.tsx --ext .ts,.tsx --max-warnings 0
npm run typecheck
npm run check:encoding
```
## 9. 已覆盖测试
1. `src/components/edutainment-runtime/babyLoveDrawingModel.test.ts`:颜色 / 按钮悬停阈值、坐标归一化、笔触追加;
2. `src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx`:画板、七色、画笔 / 橡皮、完成保存、返回按钮、左手位置指示器、mocap 摄像头视角到用户身体视角换算、左手输入不替换画笔光标位置、左手短暂缺帧不闪烁、用户左手不能抢占右手画笔、camera-left 大跳不接入画笔;
3. `src/services/edutainment-baby-drawing/babyDrawingClient.test.ts`:原图保存、原图加魔法图保存、后端魔法接口请求;
4. `src/routing/appRoutes.test.ts``/runtime/baby-love-drawing` 开启可达、关闭回落主应用;
5. `server-rs/crates/api-server/src/edutainment_baby_drawing.rs` 内部单测prompt、Data URL 校验、PNG 输出和轨迹范围摘要;
6. `server-rs/crates/api-server/src/creation_entry_config.rs` 路由映射单测:确认后端熔断可识别 `baby-love-drawing`

View File

@@ -1,252 +0,0 @@
# 宝贝识物创作发布实现方案 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. `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
2. `src/components/platform-entry/platformEntryCreationTypes.ts`
3. `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
4. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
`src/config/newWorkEntryConfig.ts` 已迁移删除,不再作为入口事实源。`baby-object-match` 必须存在于 SpacetimeDB `creation_entry_type_config` 默认种子中,默认展示名为 `宝贝识物``visible=true``open=true``sortOrder=90`;前端只通过 `GET /api/creation-entry/config` 读取后端配置并在 `platformEntryCreationTypes.ts` 做展示派生。
`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. `BabyObjectMatchDraft.visualPackage` 可选承载背景环境、UI 装饰框、礼物盒、篮子和烟雾弹出特效五类视觉资源;
6. `BabyObjectMatchPublishRequest.draft.themeTags` 发布前必须归一化补齐 `寓教于乐`
## 4. Service 边界
前端 service 放在:
```text
src/services/edutainment-baby-object/babyObjectMatchClient.ts
```
首版提供:
1. `createBabyObjectMatchDraft(payload)`
2. `saveBabyObjectMatchDraft(draft)`
3. `publishBabyObjectMatchWork(payload)`
4. `deleteLocalBabyObjectMatchDraft(profileId)`
5. `regenerateBabyObjectMatchDraftAssets(draft)`
6. `hasBabyObjectMatchPlaceholderAssets(draft)`
当前后端正式作品持久化接口未在本线程扩表落地,因此 service 仍使用本地 Demo 存储草稿和发布状态。由于 image-2 会返回多张 base64 PNG 大图,本地 Demo 草稿必须优先写入 IndexedDB `genarrative-edutainment-baby-object-drafts/drafts`,不得把完整草稿 JSON 写入 `localStorage``localStorage` 仅作为旧版小草稿迁移读取来源,读取后迁移到 IndexedDB 并清理旧 key避免触发浏览器 `Storage` 配额错误。
物品图片生成已接入后端 image-2 接口:
```text
POST /api/creation/edutainment/baby-object-match/assets
```
请求体:
```json
{
"itemNames": ["苹果", "香蕉"]
}
```
响应体:
```json
{
"assets": [
{
"itemId": "baby-object-item-1",
"itemName": "苹果",
"imageSrc": "data:image/png;base64,...",
"assetObjectId": null,
"generationProvider": "vector-engine-gpt-image-2",
"prompt": "..."
}
],
"visualPackage": {
"themePrompt": "...",
"assets": [
{
"assetId": "baby-object-visual-background",
"assetKind": "background",
"imageSrc": "data:image/png;base64,...",
"assetObjectId": null,
"generationProvider": "vector-engine-gpt-image-2",
"prompt": "..."
}
]
}
}
```
该接口返回物品透明 PNG data URL以及同一次创作生成的视觉主题包。本地 Demo 阶段暂不写入 OSS 或 SpacetimeDB `asset_object`。当前创作链路必须真实拿到 `generationProvider = "vector-engine-gpt-image-2"` 的物品图和视觉主题包后才允许进入结果页;若本地未配置 VectorEngine、登录态失效、接口返回 401/5xx、上游生成失败或响应缺少任一资源前端 service 必须抛出错误并停留在生成失败状态,不得静默回退到占位图。
由于一次创作会生成 2 张物品图和 `background``ui-frame``gift-box``basket``smoke-puff` 5 张视觉包装图,该请求属于长耗时 image-2 链路。前端 `babyObjectMatchClient` 对该 POST 使用 10 分钟请求超时,且不做自动重试,避免第一次生成仍在后端执行时又发起第二次重复生成。后端同时启动物品图与视觉主题包生成,并把该路由的 VectorEngine 单图请求等待预算提升到至少 8 分钟,避免某张图 3 分钟附近仍在生成时被后端提前断开。后端日志记录每类资源的开始、完成和耗时,排查时优先按同一次 HTTP 请求查看 `宝贝识物 image-2 物品资源生成完成``宝贝识物 image-2 视觉资源生成完成``VectorEngine 图片生成上游错误`
历史本地草稿中若已保存 `generationProvider = "placeholder"` 的旧占位资源,结果页必须提示“重新生成 image-2 资源”,并禁用试玩和发布。用户点击重新生成、发布或试玩前,前端统一调用 `regenerateBabyObjectMatchDraftAssets(draft)` 补齐资源;补齐失败时保留在结果页并展示错误。
后续正式作品持久化接入时,应补齐:
```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`,不得从前端直接调用外部图片接口。
后端 image-2 prompt 约束:
1. 锁定寓教于乐板块统一的卡通绘本草地舞台插画风;
2. 每张图只能围绕对应关键词生成一个单一物品;
3. 不生成背景、场景、氛围渲染、人物、手、篮子、礼物盒、文字、水印或 UI
4. 优先要求纯白或透明抠图友好的干净背景,服务端再统一转透明 PNG 并执行背景 alpha 清理;
5. 返回 `generationProvider = "vector-engine-gpt-image-2"` 的素材必须已经完成透明抠图。
后端视觉主题包 prompt 约束:
1. 同一次请求根据两个物品关键词生成 `background``ui-frame``gift-box``basket``smoke-puff` 五类资源;
2. 总风格继续锁定寓教于乐明亮卡通绘本插画风;
3. 若关键词偏动漫角色、玩具或公仔,背景环境和 UI 元素匹配动漫、玩具主题;若关键词偏水果,匹配果园、自然主题;其它关键词按语义匹配合适主题;
4. 背景环境图使用非透明 16:9 图,但必须保证中间、中下方和底部左右篮子区域清爽,给放大后的礼物盒、中央物品和左右篮子预留空间,不画入礼物盒、篮子、物品、人物、文字或操作 UI
5. UI 装饰框、礼物盒、篮子和烟雾弹出特效使用透明 PNG 后处理,不生成文字、数字、按钮、人物或待分类物品;
6. `gift-box` 提示词必须面向运行态约 2 倍视觉尺寸生成主体饱满的大号礼物盒,`basket` 提示词必须面向运行态约 1.5 倍视觉尺寸生成可读性高的大号篮子;
7. `smoke-puff` 只生成礼物盒打开瞬间使用的柔和烟雾云朵特效,不生成礼物盒、篮子、物品或文字;
8. 左右篮子的固定选项规则不受主题包影响,运行态只把 `basket` 作为篮子造型包装复用。
## 5. UI 边界
工作台只展示两个必填输入和生成按钮。
结果页只展示草稿核心信息、两个物品、保存草稿、发布、试玩。不在 UI 内写玩法说明长文案。
移动端优先:表单和结果页使用单列布局,桌面端自然扩展为双列。
## 6. 运行态边界
前端运行态放在:
```text
src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.tsx
```
运行态直接消费 `BabyObjectMatchDraft`,必须使用草稿中的两个物品名称和物品图。
每轮只随机当前从礼物盒跳出的物品;左右篮子不随机交换,左侧固定为草稿 `itemAssets[0]`,右侧固定为草稿 `itemAssets[1]`
若草稿包含 `visualPackage`运行态通过背景图片层、CSS 变量和图片节点消费:
1. `background`:作为舞台最底层 `ResolvedAssetImage` 背景图;存在该资源时必须关闭默认草地兜底层,避免生成场景被 CSS 草地遮住或弱化;
2. `ui-frame`:作为字幕条和计数器装饰背景;
3. `gift-box`:替换 CSS 礼物盒主体,按旧视觉约 2 倍尺寸展示,只在礼盒入场和打开阶段存在;
4. `basket`:替换篮子主体造型,按旧视觉约 1.5 倍尺寸展示,左右两侧复用同一张主题篮子图;
5. `smoke-puff`:作为礼物盒打开和中央物品弹出期间的透明烟雾特效资源。
旧草稿或接口失败时 `visualPackage = null`,运行态继续使用现有 CSS 绘本风兜底。
首关状态机:
1. `gift-entering`:礼物盒从上方落下入场动画阶段,不接受动作判定;
2. `gift-opening`:礼物盒打开并播放烟雾特效阶段,不接受动作判定;
3. `item-appearing`:礼物盒从舞台移除,当前物品从烟雾中出现并停稳,不接受动作判定;
4. `active`:物品彻底出现后才开放选篮判定;
5. `correct`:展示“真棒”反馈,对应篮筐播放正确特效并停顿,成功次数加 1特效完全结束后重新进入 `gift-entering`,下一轮礼物盒从上方落下;
6. `wrong`:展示“再想一想吧”反馈,物品弹回中央;反馈结束后回到 `active`,不重新随机物品;
7. `complete`:成功次数达到 20展示“恭喜你小朋友”和按钮。
动作输入:
1. 左手连续横向移动达到阈值:将当前物品送入左侧篮子;
2. 右手连续横向移动达到阈值:将当前物品送入右侧篮子。
运行态直接通过 `useMocapInput` 消费本地 mocap WebSocket `/stream`。选篮只使用明确 `leftHand``rightHand` 的连续横向轨迹阈值,不再通过 `wave_left_hand``wave_right_hand``wave` 等动作名触发;侧别为 `unknown` 的手部轨迹也不参与选篮,以避免多套判定误命中和连续误触发。动作判定只在 `active` 阶段开放,礼盒入场、礼盒打开、物品出现、正确反馈和错误反馈阶段收到的动作包必须清空轨迹并忽略,不允许跨阶段补判定。当前本地 mocap 输出的 handedness 按摄像头视角标记,宝贝识物运行态必须先换算为用户身体视角:`rightHand` 轨迹映射玩家左手并进入左侧篮子,`leftHand` 轨迹映射玩家右手并进入右侧篮子。草稿试玩、发布后正式体验和热身关后的本地 Demo 都复用同一个运行态,因此三条入口都必须具备同一套动作控制能力。
开发者调试输入:
1. 鼠标左键按下并拖动:映射左手轨迹,抬起后将当前物品送入左侧篮子;
2. 鼠标右键按下并拖动:映射右手轨迹,抬起后将当前物品送入右侧篮子。
运行态不得新增计时、失败次数、分数、体力或难度递增规则。
音效和语音播报当前只保留接口预留边界,正式语音接口后续接入。
## 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
cargo test -p api-server edutainment_baby_object --manifest-path server-rs/Cargo.toml
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 表目录更新。

View File

@@ -1,46 +0,0 @@
# 后端创作 Agent LLM Turn 公共化 2026-04-25
## 背景
RPG、大鱼吃小鱼、拼图三条创作 Agent 后端 turn 已经统一使用 `platform-llm`,但在 `api-server` 内仍重复维护以下流程:
1. 检查 `LlmClient` 是否可用。
2. 构造 `system + user` 两段消息。
3. 调用 `stream_text` 并从增量 JSON 中抽取 `replyText` 给 SSE 前端。
4. 从模型最终文本中截取并解析 JSON。
5. 把模型调用失败、JSON 解析失败映射成中文业务错误。
这些逻辑属于 turn 级基础设施,不应散落在不同玩法文件里;但各玩法的 prompt、anchor pack 解析、stage 推进、SpacetimeDB finalize 写回仍是领域逻辑,不在本轮合并。
## 目标
1. 新增 `api-server` 内部公共模块 `creation_agent_llm_turn`
2. 公共化流式 JSON turn 调用、非流式 JSON turn 调用、`replyText` 增量解析、最终 JSON 截取解析。
3. 大鱼、拼图、RPG Agent turn 复用公共调用骨架。
4. 保留各玩法原有结果结构、中文错误文案和持久化写回契约。
## 非目标
1. 不统一 RPG、大鱼、拼图的 prompt。
2. 不统一三类 anchor pack / draft profile schema。
3. 不改变 SpacetimeDB reducer/procedure 或 session 表结构。
4. 不改变前端 SSE contract。
## 落地边界
1. `server-rs/crates/api-server/src/creation_agent_llm_turn.rs`
- 提供 `stream_creation_agent_json_turn(...)`
- 提供 `request_creation_agent_json_turn(...)`
- 提供 `parse_json_response_text(...)``extract_reply_text_from_partial_json(...)`
2. `custom_world_agent_turn.rs`
- 保留 RPG 动态状态判断、八锚点解析、结果写回。
- 将正式单轮生成和状态识别的 LLM 请求改走公共模块。
3. `big_fish_agent_turn.rs` / `puzzle_agent_turn.rs`
- 将 LLM 流式请求和 JSON 解析改走公共模块。
- 继续保留玩法自己的 anchor pack 解析和 quick fill 规则。
## 验收
1. `cargo test -p api-server` 中相关 turn 单测通过。
2. `cargo check -p api-server` 不引入新的编译错误。
3. 编码检查通过。

View File

@@ -1,137 +0,0 @@
# 后端重写横向治理规则2026-04-22
更新时间:`2026-04-22`
## 1. 文档目标
本文件冻结 `SpacetimeDB + Axum + OSS` 后端重写收口阶段的横向规则,覆盖:
1. 前端 TypeScript contract 与 Rust DTO 的映射策略。
2. SpacetimeDB table / reducer / procedure 的演进规则。
3. 大对象、manifest、workflow cache 的存储边界。
4. 阶段文档与 API 索引的维护规则。
这些规则用于减少 M4/M5/M6/M7 后续并行推进时的 contract 漂移。
## 2. Contract 与前端兼容
### 2.1 映射原则
1. `packages/shared/src/contracts/*` 是前端消费 contract 的现有事实来源。
2. `server-rs/crates/shared-contracts/src/*.rs` 是 Rust `api-server` 返回 DTO 的事实来源。
3. 两侧字段名必须继续使用当前前端已消费的 JSON 命名,不因 Rust 字段命名风格改变外部 shape。
4. Rust DTO 必须通过 `serde(rename_all = "camelCase")`、显式 `rename` 或兼容枚举值保持旧 contract。
5. 临时兼容字段只能标记为 optional不能在没有迁移说明和测试前直接删除。
### 2.2 当前映射面
| 前端 contract | Rust DTO 模块 | 当前用途 |
| --- | --- | --- |
| `packages/shared/src/contracts/auth.ts` | `shared-contracts::auth` | 登录方式、用户信息、会话、审计、验证码与微信登录 |
| `packages/shared/src/contracts/runtime.ts` | `shared-contracts::runtime` | profile dashboard、play stats、wallet ledger、browse history、settings、inventory |
| `packages/shared/src/contracts/rpgRuntimeStoryAction.ts` | `shared-contracts::runtime_story` | runtime story action request / response、state resolve、view model |
| `packages/shared/src/contracts/rpgRuntimeStoryState.ts` | `shared-contracts::runtime_story` | runtime story state / presentation 兼容 |
| `packages/shared/src/contracts/rpgAgent*.ts` | `shared-contracts::runtime``custom_world` 相关 DTO | custom world agent session、message、operation、action |
| `packages/shared/src/contracts/rpgCreation*.ts` | `shared-contracts::runtime``custom_world` 相关 DTO | result preview、works、library、published profile |
| `packages/shared/src/contracts/common.ts` | `shared-contracts::api` | 统一 success / error envelope |
### 2.3 变更流程
1. 扩字段:先加 Rust optional 字段和 contract test再接前端消费。
2. 改字段语义:必须新增技术方案说明旧语义、新语义、迁移期兼容逻辑和回退方式。
3. 删字段或删枚举必须先证明前端调用、Node 兼容层、历史 fixture 和测试都不再消费。
4. breaking change 必须在任务清单和设计文档中显式标注,不允许只靠 PR diff 表达。
5. 所有 shared contract 变更至少运行 `cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml`
## 3. SpacetimeDB Schema 演进治理
本节按 SpacetimeDB 约束执行:
1. reducer 是事务性写入口,不依赖 reducer 返回值读取数据。
2. reducer 必须确定性执行,不做网络、文件系统、外部随机数或时间副作用。
3. 客户端读取依赖 table / subscription / procedure 返回的显式 DTO不把 Axum 进程内缓存当真相。
4. 用户身份以后续接入 SpacetimeDB 直连时的 `ctx.sender()` 为准,不信任客户端传入 owner 字段。
### 3.1 命名规则
1. table 使用稳定单数 snake_case 名称,例如 `story_session``asset_object``custom_world_agent_session`
2. reducer 使用动作动词 + 领域对象,例如 `upsert_runtime_snapshot``confirm_asset_object``turn_in_quest`
3. 需要同步返回 DTO 的 procedure 统一使用 `_and_return``get_ / list_ / compile_` 语义。
4. public table 只暴露客户端确实需要订阅或查询的状态内部审计、token、风控等默认不 public。
5. event table 只用于事件广播,不替代持久状态表。
### 3.2 列演进规则
1. 优先追加 optional 字段,不直接改名、改类型或删除列。
2. 必须删除语义时,先软废弃字段并让读模型停止依赖,再在独立迁移窗口清理。
3. 状态类枚举新增值时,前端必须有 unknown / fallback 处理。
4. 需要唯一约束或索引时,先补设计文档说明查询路径,再改 schema。
5. 大规模重排表结构必须拆成新表 + 双写 / 读模型迁移,不在原表上做破坏性变更。
### 3.3 软删除规则
1. 用户可见业务实体优先使用 `status``deleted_at``archived_at` 表达生命周期。
2. 会话、作品、资产绑定、审计和任务记录默认不物理删除。
3. 物理删除只用于临时草稿、过期验证码、过期 OAuth state 等明确可丢弃数据。
4. 删除 reducer 必须写清是否幂等,重复调用不能造成不可恢复错误。
## 4. 大对象与缓存治理
### 4.1 OSS 存储边界
必须进入 OSS
1. 图片、视频、动作帧、封面图、场景图。
2. 大型 JSON manifest。
3. 角色工作流缓存 JSON。
4. 导入视频和生成过程草稿资源。
只进入 SpacetimeDB 元数据:
1. `bucket``object_key``asset_kind``content_type``content_length``content_hash``version`
2. `asset_entity_binding` 的业务实体、槽位、owner 和 profile 绑定关系。
3. AI task、asset task、publish gate 等状态字段。
4. 可用于列表和权限判断的轻量 summary。
### 4.2 本地缓存边界
1. 生产主链不得把仓库 `public/generated-*` 作为资产真相。
2.`/generated-*` 仅作为同源代理兼容路径,读取私有 OSS 对象。
3. 测试环境允许使用 `#[cfg(test)]` 内存兜底,但必须在文档中注明不进入生产链。
4. workflow cache 当前真相是 OSS JSON 草稿对象,不落本地文件。
5. 临时生成文件如需存在,必须限制在进程临时目录,并在任务完成后清理。
### 4.3 Manifest 与版本
1. 多文件资产集合使用 OSS manifest 表达,不重复新增结构化表,除非已证明查询需求需要。
2. `asset_object.version` 当前默认 `1`,版本升级必须说明兼容读取规则。
3. `content_hash` 可为空,但一旦用于去重,必须先补冲突处理和重算策略。
4. 强业务资产表只有在需要领域查询、审核、回滚或权限策略时再新增。
## 5. 文档维护规则
1. 工程修改必须同步对应阶段任务清单。
2. 新增或改变接口时,同步更新 [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。
3. 仍存在旧能力差异时,同步更新 [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md) 或新增 Rust 侧补充索引。
4. M4 结构变更同步维护 RPG runtime 链路文档。
5. M5 结构变更同步维护 creation flow 链路文档。
6. M6 资产链路变更同步维护 OSS / asset_object / generated path 文档。
7. M7 切流相关变更同步维护部署、预检、smoke 与回滚文档。
## 6. 验收门禁
横向治理完成不等价于真实切流完成。当前可本地验收的门禁是:
1. `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
2. `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
3. `cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml`
4. `cargo test -p api-server --manifest-path server-rs/Cargo.toml --no-run`
5. `node scripts/check-encoding.mjs ...`
真实切流前仍必须单独完成:
1. OSS 真实读写 smoke。
2. LLM / DashScope 真实生成 smoke。
3. 关键 SSE 接口联调。
4. SpacetimeDB publish / rollback 演练。
5. 灰度环境双跑对比。

View File

@@ -1,246 +0,0 @@
# 后端用户行为埋点覆盖方案
更新时间:`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_revoke_session` | `POST /api/auth/sessions/{session_id}/revoke` |
| `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 不包含凭证、请求体或敏感业务字段。

View File

@@ -1,729 +0,0 @@
# 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 HUDCanvas 保持负责 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正式成绩、排行榜、发布和奖励后续再交给后端链路。

View File

@@ -1,111 +0,0 @@
# 战斗直结算与表现修复记录2026-04-27
更新时间:`2026-04-27`
## 1. 问题背景
本次修复针对 RPG 运行态战斗里两类直接可感知的问题:
1. 战斗面板中的“普通攻击”“技能释放”等选项点击后,体验上像是又走了一次剧情/模型推演,而不是立刻播动作并结算伤害。
2. 伤害飘字与敌方头顶血条在战斗里没有稳定可见,尤其是最后一击和脱战前后的几帧容易直接丢失。
## 2. 本次约束
为避免后续回归,本次明确补充以下工程约束:
1. 只要已经处于 `inBattle = true` 的战斗面板阶段,`battle_*` 与战斗内 `inventory_use` 都必须走前端现有的本地确定性战斗播放链。
2. 这类动作只负责“播放动作 -> 扣血/扣蓝/冷却 -> 刷新下一轮 options / 脱战收尾”,不能再回到服务端剧情推演分流。
3. 脱战前最后一拍的画布表现必须保留,不能因为 `inBattle` 提前变成 `false` 就把伤害飘字、敌方血条和受击反馈一起关掉。
## 3. 落地改动
### 3.1 战斗动作分流
文件:`src/hooks/rpg-runtime-story/choiceActions.ts`
调整:
1. 新增“战斗直结算动作”判定。
2. 当运行态已经处于战斗中时,`battle_*``inventory_use` 不再命中 `runServerRuntimeChoiceAction()`
3. 这些动作统一回到 `runLocalStoryChoiceContinuation()`,沿用现有 `buildBattlePlan + playback` 本地确定性链路。
结果:
1. 普通攻击、技能释放、调息、逃跑、战斗内用物品都会直接播动作并结算。
2. UI 不再把这类按钮误导成“剧情推演中”的体验。
### 3.2 战斗画布表现保留
文件:`src/components/game-canvas/GameCanvasEntityLayer.tsx`
调整:
1. 新增 `hasCombatAfterimage` 判定,用于识别“虽然 `inBattle` 已经开始收尾,但敌方仍处于受击/死亡帧或刚产生过伤害反馈”的阶段。
2. 伤害反馈样本采集、飘字事件保留、敌我血条展示统一改为依赖 `shouldRenderCombatPresentation`,而不是只看 `inBattle`
结果:
1. 伤害数值飘字不会在最后一击前被提前清空。
2. 敌方头顶血条能跟着受击与死亡过渡完整显示出来。
### 3.3 面板文案修正
文件:`src/components/rpg-runtime-panels/RpgAdventurePanel.tsx`
调整:
1. 运行态仍需锁输入的短暂阶段,如果当前处于战斗中,加载提示改为“战斗结算中...”。
2. 非战斗态保留“剧情推演中...”。
结果:
1. 战斗按钮点击后的反馈语义与真实执行链路一致。
2. 用户不会再误判为“点击普通攻击/技能后又触发了一次模型推理”。
## 4. 回归关注点
后续若继续改 `server-rs` / Runtime Story 的战斗 option 下发,需要继续遵守:
1. 服务端可以负责下发合法战斗选项和最终快照结构。
2. 但战斗内直接动作的逐帧播放与即时结算,默认仍以前端确定性链路为准。
3. 若未来要把这条链路整体迁回 `server-rs`,必须先补齐“无推理、可逐帧播放、可保留最终受击帧”的等价表现方案,再替换当前实现。
## 5. 追加修复2026-04-27 晚)
本日后续排查又发现一处更直接的战斗跳过根因:
1. `src/hooks/combat/battlePlan.ts` 旧实现会在一次点击里继续跑完整段 `turnOrder`,而不是只结算“玩家一次声明动作 + 最多一次敌方反击”。
2. `battle_attack_basic` 还会复用技能挑选逻辑,导致“普通攻击”可能被本地随机映射成别的技能动作。
本轮已追加修复:
1. 本地战斗计划严格收束为单回合结算,不再一次点击连跑多轮。
2. `battle_attack_basic` 固定走基础攻击,不再随机选技能。
3. 战斗内 `inventory_use` 在本地计划中按单次动作消费物品、应用恢复/冷却收益,不再落回攻击型分支。
追加验收口径:
1. 战斗中点击一次普通攻击 / 技能 / 物品 / 调息,不会直接把整场战斗过程跳过。
2. 若敌人未被这一击打死,当前次结算结束后仍停留在战斗态,并刷新下一轮战斗选项。
3. “普通攻击”不会显示成其他技能的动作与耗蓝结果。
## 6. 再追加修复2026-04-27 夜间二次排查)
用户复测后又暴露出一类更隐蔽的问题:
1. 面板展示层可能仍在显示战斗选项,但逻辑态 `gameState.inBattle` 已短暂回落为 `false`
2. 旧实现里,`src/hooks/rpg-runtime-story/choiceActions.ts` 只要看到 `inBattle === false`,就会把 `battle_*` / `inventory_use` 重新分流回服务端 runtime。
3. 这会让用户在“视觉上仍处于战斗”的窗口点击选项时,直接命中服务端快照结果,体感仍然是“点一下就把战斗过程跳过”。
本轮追加修正:
1. 战斗直结算判定不再只依赖 `gameState.inBattle`
2. 只要当前选项本身属于 `battle_*` / `inventory_use`,并且任一战斗上下文仍然存在
`currentBattleNpcId``currentNpcBattleMode`、存活敌人、当前故事仍在显示战斗选项
就必须继续走本地逐帧战斗链。
3. 为此补充了回归测试,覆盖“`inBattle` 已短暂变成 `false`,但战斗展示仍在”的残留窗口。
新增验收口径:
1. 即使 `inBattle` 出现一帧级别的提前回落,只要玩家看到的仍是战斗选项,点击后也不会跳回服务端直结算。
2. 战斗面板残留显示期间点击 `battle_*` / `inventory_use`,仍应播放本地战斗过程,而不是直接跳到结果快照。

View File

@@ -1,134 +0,0 @@
# 大鱼吃小鱼结果页主图占位预览修复说明 2026-04-23
日期:`2026-04-23`
## 1. 问题现象
在“深海谜境 / 大鱼吃小鱼”结果页中,等级卡片会显示:
1. `主图 已生成`
2. 操作后会出现“已应用主图”感知
但实际结果页看不到角色主图,卡片里只有一张蓝色底图。
## 2. 排查结论
本次沿着“结果页展示 -> API action -> SpacetimeDB procedure -> 资产路径”全链检查后确认:
1. 前端确实成功触发了 `big_fish_generate_level_main_image`
2. SpacetimeDB 侧确实把资产槽位状态写成了 `ready`
3. 但这条链路没有接真实图像模型,也没有接 OSS 真实资产对象
4. 旧实现只回写一个 `/generated-big-fish/...png` 占位 URL
5. 同时仓库里没有实际把这张占位图写到 `public/generated-big-fish/...`
6. 因此前端读到的是一个“看起来像图片地址、实际上没有真实文件”的路径
7. `<img>` 加载失败后,卡片底层蓝色渐变背景暴露出来,于是用户只能看到蓝色图块
## 3. 根因拆解
### 3.1 状态成功过早
`generate_big_fish_asset` 当前最小实现只负责:
1.`asset slot`
2.`prompt snapshot`
3. 标记 `status = ready`
它并不代表真实主图已经生成完成。
### 3.2 预览资源未真正落盘
旧实现会构造:
`/generated-big-fish/{asset_kind}/{level_part}/{seed}.png`
但没有同步在 `public/generated-big-fish/...` 写出对应文件。
### 3.3 结果页直接吃裸 `assetUrl`
Big Fish 结果页主图卡之前直接:
1.`slot.assetUrl`
2. 塞进 `<img src=...>`
一旦文件不存在,就只剩下卡片自己的蓝色渐变背景。
### 3.4 UI 文案误导
旧文案把当前阶段写成:
1. `已生成`
2. `已生成并设为正式资产`
这会让用户自然理解为“真实主图已经出来了”,与当前最小实现不一致。
## 4. 本次修复策略
本轮不直接接真实模型生成,而是先把最小可见闭环补完整。
### 4.1 API action 写出可预览占位图
在 Rust `api-server` 的 Big Fish action 处理中:
1. 复用拼图玩法“写本地可预览占位图”的方式
2. 在调用 `generate_big_fish_asset` 之前,先把 Big Fish 占位图真正写到:
`public/generated-big-fish/...`
3. 保证 SpacetimeDB 里写入的占位 URL 至少对应一个真实可访问文件
### 4.2 结果页改用统一图片渲染组件
Big Fish 结果页主图和背景预览改为:
1. 使用 `ResolvedAssetImage`
2. 统一走现有图片渲染链路
3. 避免后续接 OSS / 旧 generated 路径兼容时再重复返工
### 4.3 状态文案改准
当前还是占位资产阶段,因此把结果页状态文案改为:
1. `占位已生成`
2. `生成并应用占位图`
避免继续把“槽位 ready”误说成“真实主图已完成”。
### 4.4 结果页露出背景预览
除了等级主图卡,本轮顺手把场地背景卡也接上真实预览图渲染,避免出现:
1. 状态显示完成
2. 右侧仍只是一块纯渐变底图
## 5. 修复后的链路语义
修复后 Big Fish 当前资产链路语义明确为:
1. 点击生成
2. `api-server` 先写出本地可访问占位图
3. `spacetime-module` 写正式资产槽位和提示词快照
4. 前端读取 `assetUrl` 并真实渲染预览
也就是说:
1. 当前可以保证“用户看得见”
2. 但仍然不是“真实模型图像生成”
3. 后续真实模型 / OSS worker 接入后,再把占位图链替换成正式资产真相链
## 6. 验收标准
本次修复后需要满足:
1. 结果页等级卡在主图生成后能直接看到真实可加载图片,不再只剩蓝色底图
2. 场地背景生成后右侧卡片能看到真实可加载图片
3. Big Fish 结果页图片渲染统一走现有图片组件,不再直接裸 `<img>` 吃易失路径
4. 当前 UI 文案不再把占位图误称为真实主图
5. 若本地占位图写盘失败,则 action 不能继续回到“ready 成功”状态
## 7. 后续建议
下一阶段继续补 Big Fish 真实资产链时,建议按下面顺序推进:
1. 先引入 Big Fish `asset_object + asset_entity_binding` 正式槽位设计
2. 再接真实图片 / 动作生成 worker
3. 最后把当前 `/generated-big-fish/...` 本地占位路径迁为兼容层,而不是继续作为真相路径

View File

@@ -1,302 +0,0 @@
# 大鱼吃小鱼玩法创作与运行态最小落地技术方案
日期:`2026-04-22`
## 1. 文档目的
本文件承接 PRD《AI 原生 Agent-First 大鱼吃小鱼玩法创作工具与玩法系统》,冻结本轮工程落地的最小完整闭环。
本轮目标不是抽象一个通用街机玩法引擎,而是在现有平台内新增一个独立 `big_fish` 玩法域,跑通:
1. 平台创作入口选择大鱼吃小鱼玩法
2. Agent 会话创建、消息提交和 SSE 兼容返回
3. 基于 4 个高杠杆锚点编译玩法草稿
4. 结果页生成等级主图、等级动作、场地背景的正式资产槽位,并同步提供可预览资源
5. 发布校验
6. 启动测试运行态
7. 后端推进摇杆输入、刷怪、吞噬收编、三合一、屏外清理和胜负裁决
### 1.1 2026-04-27 公开游玩次数补充
正式发布的大鱼吃小鱼作品需要记录公开游玩次数,落地口径如下:
1. `big_fish_creation_session.play_count` 保存该作品被正式启动的次数,默认值为 `0`
2. 只有平台作品详情、作品架等正式入口启动已发布作品时递增;创作结果页内的测试运行不计入。
3. 前端作品摘要 contract 暴露 `playCount`,作品架展示与拼图一致使用该后端值。
4. 本轮仅记录“进入玩法”次数,不记录大鱼吃小鱼总时长;个人 profile 的 RPG 时长统计仍由 runtime snapshot 负责。
5. schema 变更需要同步 `migration.rs` 已纳入的 `big_fish_creation_session` 导入导出结构。
## 2. 本轮明确不做
1. 不在本文件内展开正式图片模型链、OSS 真相链和占位兼容层的细节;相关正式出图方案以 `BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md` 为准。
2. 不新增 WebSocket 依赖;首版运行态使用 `POST input + GET snapshot` 的有限 HTTP 辅助接口,后续再升级长连接。
3. 不把 Big Fish 写回 `custom_world``rpgCreation` 或 RPG runtime 旧语义。
4. 不新增作品市场、排行榜、复盘、局外成长、PvP。
5. 不要求前端本地模拟真相;前端只渲染后端 snapshot。
## 3. Rust crate 边界
新增:
1. `server-rs/crates/module-big-fish`
- 纯领域模型、输入校验、草稿编译、资产覆盖率、运行态规则推进。
- 可开启 `spacetime-types` feature`spacetime-module` 派生 SpacetimeDB 类型。
接入:
1. `server-rs/crates/spacetime-module`
- 新增 Big Fish 表与 procedure。
- 只存状态与结构化引用,不做 OSS / LLM 外部 IO。
2. `server-rs/crates/spacetime-client`
- 新增 Big Fish facade隐藏 generated bindings。
3. `server-rs/crates/shared-contracts`
- 新增 HTTP DTO。
4. `server-rs/crates/api-server`
- 新增 `big_fish_creation.rs``big_fish_assets.rs``big_fish_runtime.rs` 或最小合并的 `big_fish.rs`
## 4. SpacetimeDB 表
本轮只新增必要表,所有表主键使用 Axum 生成的显式业务 ID。
### 4.1 `big_fish_creation_session`
字段:
1. `session_id: String`
2. `owner_user_id: String`
3. `seed_text: String`
4. `current_turn: u32`
5. `progress_percent: u32`
6. `stage: BigFishCreationStage`
7. `anchor_pack_json: String`
8. `draft_json: Option<String>`
9. `asset_coverage_json: String`
10. `last_assistant_reply: Option<String>`
11. `publish_ready: bool`
12. `created_at: Timestamp`
13. `updated_at: Timestamp`
索引:
1. `by_big_fish_session_owner_user_id(owner_user_id)`
### 4.2 `big_fish_agent_message`
字段:
1. `message_id: String`
2. `session_id: String`
3. `role: BigFishAgentMessageRole`
4. `kind: BigFishAgentMessageKind`
5. `text: String`
6. `created_at: Timestamp`
索引:
1. `by_big_fish_message_session_id(session_id)`
### 4.3 `big_fish_asset_slot`
字段:
1. `slot_id: String`
2. `session_id: String`
3. `asset_kind: BigFishAssetKind`
4. `level: Option<u32>`
5. `motion_key: Option<String>`
6. `status: BigFishAssetStatus`
7. `asset_url: Option<String>`
8. `prompt_snapshot: String`
9. `updated_at: Timestamp`
索引:
1. `by_big_fish_asset_session_id(session_id)`
2. `by_big_fish_asset_slot(session_id, asset_kind, level, motion_key)`
### 4.4 `big_fish_runtime_run`
字段:
1. `run_id: String`
2. `session_id: String`
3. `owner_user_id: String`
4. `status: BigFishRunStatus`
5. `snapshot_json: String`
6. `last_input_x: f32`
7. `last_input_y: f32`
8. `tick: u64`
9. `created_at: Timestamp`
10. `updated_at: Timestamp`
索引:
1. `by_big_fish_run_owner_user_id(owner_user_id)`
2. `by_big_fish_run_session_id(session_id)`
## 5. SpacetimeDB procedure
本轮全部使用 procedure 同步返回快照,避免 Axum 额外拼读模型。
1. `create_big_fish_session(input) -> BigFishSessionProcedureResult`
2. `get_big_fish_session(input) -> BigFishSessionProcedureResult`
3. `submit_big_fish_message(input) -> BigFishSessionProcedureResult`
4. `compile_big_fish_draft(input) -> BigFishSessionProcedureResult`
5. `generate_big_fish_asset(input) -> BigFishSessionProcedureResult`
6. `publish_big_fish_game(input) -> BigFishSessionProcedureResult`
7. `start_big_fish_run(input) -> BigFishRunProcedureResult`
8. `submit_big_fish_input(input) -> BigFishRunProcedureResult`
9. `get_big_fish_run(input) -> BigFishRunProcedureResult`
说明:
1. `submit_big_fish_message` 只做 deterministic 锚点补全,不调用 LLM。
2. `generate_big_fish_asset` 的槽位写入语义允许 `api-server` 传入正式 `asset_url`;若未传则回退为占位路径,保证最小链与正式链共存。
3. `submit_big_fish_input` 每次至少推进 1 个后端 tick前端不能本地裁决。
4. 运行态所有“持续时间”语义按真实秒数累计,前端即使摇杆静止也要持续以当前输入心跳驱动后端推进,避免刷怪与屏外 `3` 秒清理依赖手速或提交频率。
## 6. HTTP contract
所有接口挂在 `/api/runtime/big-fish/*`。创作、私有作品列表、删除、运行态启动与输入推进需要 Bearer 鉴权;公开广场读取接口不要求登录,只返回已发布作品。
开发态本地链路补充约定:
1. 浏览器仍只请求同源 `/api/runtime/big-fish/*`
2. `vite -> Rust api-server:3100` 是默认开发链路,禁止把新运行态接口继续接回 `server-node`
3. Rust `api-server` 作为 Big Fish 真相后端,正式处理鉴权、会话、草稿、资产动作和运行态规则。
4. 若本机端口不同,只能通过 `RUST_SERVER_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 显式覆盖 Vite 代理目标。
5. 本地默认端口:
- `vite`: `3000`
- Rust `api-server`: `3100`
- `SpacetimeDB standalone`: `3001`
6. `GENARRATIVE_SPACETIME_DATABASE` 本地开发优先跟随仓库根目录 `spacetime.local.json``database` 字段,避免 `api-server` 默认连到错误数据库名。
- `.env.local` 或进程环境显式配置 `GENARRATIVE_SPACETIME_DATABASE` 时可覆盖本地配置。
- `.env.example` 只提供示例默认值,不得压过 `spacetime.local.json`
### 6.1 创作会话
1. `POST /api/runtime/big-fish/agent/sessions`
2. `GET /api/runtime/big-fish/agent/sessions/{sessionId}`
3. `POST /api/runtime/big-fish/agent/sessions/{sessionId}/messages/stream`
4. `POST /api/runtime/big-fish/agent/sessions/{sessionId}/actions`
`messages/stream` 首版兼容当前前端 SSE 解析方式,只输出:
1. `reply_delta`
2. `session`
3. `done`
4. `error`
`actions` 首版支持:
1. `big_fish_compile_draft`
2. `big_fish_generate_level_main_image`
3. `big_fish_generate_level_motion`
4. `big_fish_generate_stage_background`
5. `big_fish_publish_game`
### 6.2 运行态
1. `POST /api/runtime/big-fish/sessions/{sessionId}/runs`
2. `GET /api/runtime/big-fish/runs/{runId}`
3. `POST /api/runtime/big-fish/runs/{runId}/input`
运行态启动规则:
1. 当前用户启动自己未发布草稿时,`session.owner_user_id` 必须等于当前登录用户。
2. 当前用户启动别人作品时,只允许启动 `stage = published` 的公开作品。
3. 新建的 `big_fish_runtime_run.owner_user_id` 始终写入当前游玩用户,不能写入作品作者,后续 run 查询与输入推进仍按游玩用户隔离。
### 6.3 作品列表
1. `GET /api/runtime/big-fish/works`
开发态 Vite 必须把该同源接口代理到 Rust `api-server`;前端作品页只调用同源 `/api/runtime/big-fish/works`,不得直连 Rust 端口或回退到 `server-node`
### 6.4 公开广场
1. `GET /api/runtime/big-fish/gallery`
公开广场只返回 `status = published` 的大鱼吃小鱼作品。响应复用 `BigFishWorksResponse`,每个条目必须包含 `ownerUserId`,供前端生成稳定广场卡片 key 与后续运行态权限判断。发布动作完成后,前端必须同时刷新私有作品列表和公开广场列表,保证发布结果能立即出现在首页与分类页。
`input` 请求体:
```json
{
"x": 0.4,
"y": -0.2
}
```
## 7. 运行态最小规则
后端推进规则固定:
1. 开局拥有 1 个 `level = 1` 己方实体。
2. 开局视野内生成至少 2 个同级野生实体。
3. 己方实体碰撞低于或等于自己等级的野生实体时收编。
4. 高于己方等级的野生实体碰撞己方实体时吃掉该己方实体。
5. 每次结算后从低等级开始做三合一连锁合成。
6. 野生实体池围绕玩家最高己方等级维持低 1~2 级与高 1~2 级。
7. 同等级、高 3 级及以上、低 3 级及以下的野生实体,屏外连续 3 秒后删除。
8. 玩家首次拥有最高等级实体时立即胜利。
9. 己方实体归零时失败。
## 8. 前端接入边界
新增目录:
1. `src/services/big-fish-creation/`
2. `src/components/big-fish-creation/`
3. `src/components/big-fish-result/`
4. `src/components/big-fish-runtime/`
复用现有平台入口壳层,但入口脚本必须使用通用平台命名,禁止把 Big Fish 业务状态写进 `rpg-entry` 命名脚本:
1.`src/components/platform-entry/PlatformEntryCreationTypeModal.tsx` 新增“大鱼吃小鱼”选项。
2.`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` 中新增 Big Fish 专属 stage。
3. Big Fish 不使用 `RpgCreationResultView`,使用 `BigFishResultView`
4. `src/components/rpg-entry/*` 只能保留兼容导出或 RPG 专属组件,不允许承载 Big Fish 业务分支。
前端只允许:
1. 展示会话、草稿、资产槽位、运行快照。
2. 发送聊天、action 和摇杆输入。
3. 根据后端 snapshot 渲染实体。
4. 当后端 snapshot 返回 `won``failed` 时,必须在玩法舞台中央显示清晰结算浮层;通关与失败都不能只依赖顶部状态标签或事件日志。
5. 结算浮层必须提供可继续操作的出口:`failed` 至少包含“重来”和“退出”,`won` 至少包含“退出”。“重来”只能重新启动当前大鱼作品的一局后端 run不能在前端本地篡改旧 run snapshot“退出”回到当前作品结果页或直达入口的上级页面。
前端禁止:
1. 自行决定刷怪。
2. 自行决定吞噬 / 合成 / 清理 / 胜负。
## 9. 本轮验收
完成后至少执行:
1. `cargo fmt -p module-big-fish -p shared-contracts -p spacetime-module -p spacetime-client -p api-server`
2. `cargo check -p module-big-fish`
3. `cargo check -p shared-contracts`
4. `cargo check -p spacetime-module`
5. `spacetime generate` 刷新 Rust bindings
6. `cargo check -p spacetime-client`
7. `cargo check -p api-server`
8. 前端类型 / 构建检查
9. `npm run check:encoding`
## 10. 本地开发补充
为避免再次出现 `POST /api/runtime/big-fish/agent/sessions` 命中旧 Node 后端 `404`,本轮额外冻结以下联调口径:
1. `npm run dev` 需要同时拉起:
- `server-node`
- Rust `api-server`
- `vite`
2. `server-node` 新增 Big Fish 专用兼容网关路由,不在 Node 内复制 Big Fish 玩法逻辑。
3. Node -> Rust 转发使用内部桥接头:
- `x-genarrative-authenticated-user-id`
- `x-genarrative-internal-api-secret`
4. Rust 侧只对带正确内部密钥的本地桥接请求接受该用户头,不对普通外部请求放开匿名身份伪造。
如检查发现本轮主链缺口,继续补齐;如已经满足上述验收,不继续扩展额外玩法能力。

View File

@@ -1,35 +0,0 @@
# 大鱼吃小鱼方向触控操作优化说明
## 背景
当前大鱼运行时使用左下固定虚拟摇杆,玩家必须点到摇杆区域才能移动。移动端实际体验应改为屏幕任意位置触控:按住屏幕时不再用“触点相对按下原点”的距离判断方向,而是按固定采样间隔比较“上一次触点位置”和“当前触点位置”的位移方向,角色按恒定速度朝采样方向行动。
## 交互规则
1. 玩家在玩法舞台内按下时,记录当前触点坐标为采样起点。
2. 按下瞬间提交 `{ x: 0, y: 0 }`,保证一开始玩家不动。
3. 按住期间每 `0.1` 秒采样一次当前触点坐标,并用“当前触点 - 上一次采样触点”的位移计算方向。
4. 输入只表达方向,不表达速度;超过采样死区后归一化为单位方向向量,未超过死区则沿用上一段有效方向。
5. 每次采样完成后,把当前触点写为下一次采样的“上一次位置”。
6. 松开或取消触控后,清空采样状态并提交 `{ x: 0, y: 0 }`
7. 前端继续定时提交当前方向,即使没有玩家输入也提交零向量,让后端或本地直达局持续推进世界 tick。
## 本地直达局边界
- 大鱼吃小鱼的最终游玩部分统一放在前端本地 runtime不再调用后端 start/input/get run 接口。
- 后端只保留 Agent 创作、草稿编译、资产生成、发布和作品列表;不负责移动、碰撞、刷鱼、合成、屏外清理或胜负模拟。
- `/big-fish` 的本地直达局和平台内测试玩法必须共用前端本地 runtime并在玩家未操作时继续移动野生对象。
- 玩家速度保持恒定,只由方向决定移动方向。
- 野生对象使用确定性游动规则,避免直达入口看起来像静态截图。
## 验收口径
1. 在舞台任意位置按下时玩家不立即移动。
2. 按住并拖动后,玩家方向来自最近 `0.1` 秒位移,而不是来自按下原点。
3. 松开后玩家停止。
4. 不操作时野生对象仍会持续游动。
## 资产生成补充口径
- 大鱼实体主图、`idle_float``move_swim` 动作图都按 RPG 角色资产口径处理:单体完整入镜、中心构图、轮廓清晰、透明背景、无 UI/文字/水印。
- 场地背景只生成环境不生成规则说明、UI 或巨大主体遮挡;画面元素要少,中央活动区域要大,边缘只保留少量出生区提示。

View File

@@ -1,26 +0,0 @@
# 大鱼吃小鱼玩法直达路由说明
## 背景
现有前端已经包含 `BigFishRuntimeShell`,正式链路从创作中心或作品卡启动后端运行局。为了便于快速验收玩法手感,需要补一个不依赖后端会话的直达入口。
## 路由设计
- `/big-fish`:进入大鱼吃小鱼玩法直达页。
- 路由挂在 `src/routing/appRoutes.tsx`,与 `/puzzle` 一样走现有轻量路由解析层,不新增独立路由系统。
- 每个玩法仅保留一个直达入口,避免 `/play` 这类重复路径造成维护分叉。
## 运行态边界
- 直达页复用 `BigFishRuntimeShell`,不复制运行时 UI。
- 初始快照由前端本地构造,背景使用内联 SVG 占位图。
- 摇杆输入在本地推进角色位置、碰撞与成长等级,仅用于直达体验。
- 该入口不改变正式 `api-server` 运行局、作品发布、资产生成和 SpacetimeDB 持久化链路。
## 验收口径
1. 浏览器访问 `/big-fish` 后直接显示竖屏大鱼吃小鱼舞台。
2. 屏幕任意位置按下并拖动可移动玩家实体。
3. 玩家碰到不高于自身等级的实体后成长,并在事件日志显示成长结果。
4. 左上返回按钮在直达页语义为退出到平台首页。
5. 直达页通关或失败后,结算浮层继续复用正式运行态出口;失败态点击“重来”重开本地占位局,点击“退出”回到平台首页。

View File

@@ -1,57 +0,0 @@
# 大鱼吃小鱼草稿进度与会话超时兜底修复 2026-04-28
## 背景
大鱼吃小鱼在 `2026-04-28` 完成草稿结构化升级后,结果页草稿已经不再是单纯模板壳,而是会生成等级文本、形象描述、动作描述与运行参数。
但当前链路仍暴露出两个直接体验问题:
1. 前端草稿进度页仍把大鱼吃小鱼展示成单个 `compile` 步骤,用户会感觉“整个生成过程只有一步,而且一直卡在第一步”。
2. 前端在打开大鱼草稿或结果页时,会通过 `GET /api/runtime/big-fish/agent/sessions/:sessionId` 拉取完整会话;当 SpacetimeDB 上游偶发抖动时Rust `spacetime-client` 统一 10 秒超时会直接映射成 `502`,用户会看到反复报错。
## 修复口径
### 1. 草稿进度页改为多阶段感知
大鱼吃小鱼的 `big_fish_compile_draft` 仍然保持为一次后端 compile action不拆成多个新的后端接口。
但前端进度读模型不再把它渲染成单步,而是拆成下面三段:
1. `整理玩法骨架`
- 收拢玩法承诺、成长阶梯与风险节奏。
2. `编译等级蓝图`
- 生成每级角色描述、形象描述与动作描述。
3. `校准场地与参数`
- 整理背景蓝图与运行参数,准备结果页。
这样做的边界是:
1. 不把动作正式出图重新塞回 compile action。
2. 只增强生成中的阶段反馈,不改动现有结果页资产工坊分工。
3. 进度阶段属于前端展示语义,不要求后端额外维护细粒度 procedure 状态。
### 2. 会话读取增加短重试与超时语义收口
大鱼会话读取现在补充两层守卫:
1. `api-server` 在读取大鱼 session 时,对 `SpacetimeClientError::Timeout``SpacetimeClientError::ConnectDropped` 做一次短重试。
2. 若最终仍然超时,则错误状态码从泛化 `502` 收口为更准确的 `504 Gateway Timeout`
这样可以覆盖两类常见情况:
1. SpacetimeDB 连接偶发抖动,第一次 procedure 超时但第二次马上恢复。
2. 用户打开草稿页时碰到短暂断链,不再被立即判定成稳定的坏网关故障。
## 落地范围
1. `src/services/miniGameDraftGenerationProgress.ts`
2. `src/services/miniGameDraftGenerationProgress.test.ts`
3. `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
4. `server-rs/crates/api-server/src/big_fish.rs`
## 验收口径
1. 用户点击大鱼吃小鱼“生成草稿”后,进度页至少能看到三个结构化阶段,而不是单个 compile 步骤。
2. 这三个阶段都只描述草稿编译,不出现“生成动作素材”之类结果页资产动作。
3. `GET /api/runtime/big-fish/agent/sessions/:sessionId` 遇到短暂 SpacetimeDB 抖动时,会先做一次短重试。
4. 如果最终仍超时,接口返回语义应体现为超时,而不是继续统一落成泛化 `502`

View File

@@ -1,168 +0,0 @@
# 大鱼吃小鱼正式图片生成接入方案 2026-04-23
日期:`2026-04-23`
## 1. 文档目的
`2026-04-23` 早些时候,我们已经修复了 Big Fish 结果页“显示已生成但只能看到蓝色底图”的问题,先让占位图真正可见。
这份文档继续冻结下一步方案:把 Big Fish 结果页从“占位可见”升级为“模型正式出图”,并且复用仓库现有的 Rust 图片生成与 OSS 真相链,不再为 Big Fish 单独发明一套新资产系统。
## 2. 当前问题复盘
上一阶段虽然解决了“看不见图”的问题,但本质仍是占位链:
1. `api-server` 先写本地 `public/generated-big-fish/*` 占位 PNG。
2. `spacetime-module``big_fish_asset_slot.status` 写成 `ready`
3. `asset_url` 写的是 `/generated-big-fish/...`
4. 结果页能看到图片,但那只是占位预览,不是真实模型图。
这意味着:
1. 用户在结果页看到的“主图 / 动作 / 背景”仍不是正式资产。
2. `ready` 语义对 Big Fish 来说仍然偏弱,只表示“槽位上已有可预览资源”,不等同于“模型正式资产已落 OSS 真相链”。
## 3. 本次目标
本次把以下三类 Big Fish 资产切到正式图片生成链:
1. `level_main_image`
2. `level_motion`
3. `stage_background`
当前只接“正式静态图片生成”,不在这一轮扩视频、逐帧序列或动作 manifest。
原因是:
1. Big Fish 结果页当前只消费单个 `assetUrl`
2. 运行态和结果页目前都按静态图预览设计。
3. 先把正式主图链闭合,比提前引入一套未被消费的视频协议更稳。
## 4. 统一真相链
Big Fish 正式图片生成统一复用现有 Rust 主链:
1. `api-server` 根据 Big Fish 草稿 prompt 调用 DashScope 文生图。
2. Rust 下载远端图片二进制。
3. Rust 上传到私有 OSS。
4. Rust 调用 `confirm_asset_object` 确认正式对象。
5. Rust 调用 `bind_asset_object_to_entity` 绑定 Big Fish 业务槽位。
6. Rust 再调用 Big Fish procedure`big_fish_asset_slot.asset_url` 写成正式兼容路径。
7. 前端继续通过 `ResolvedAssetImage``/api/assets/read-url` 消费图片。
## 5. 路径策略
### 5.1 占位路径继续保留
占位图路径继续保持:
`/generated-big-fish/*`
它只代表:
1. 本地开发态占位可见资源。
2. 旧的最小预览兼容层。
### 5.2 正式图片使用新前缀
正式 Big Fish 图片统一写到新的 OSS legacy 兼容前缀:
`/generated-big-fish-assets/*`
这样可以同时满足:
1. 与仓库现有 `/generated-*` 兼容代理体系一致。
2. 不会被前端继续误判成占位图。
3. 后续可继续通过 `LegacyAssetPrefix``/api/assets/read-url``ResolvedAssetImage` 复用现有链路。
## 6. SpacetimeDB 语义调整
### 6.1 Big Fish 资产生成输入补充 `asset_url`
当前 `BigFishAssetGenerateInput` 只有:
1. `asset_kind`
2. `level`
3. `motion_key`
4. `generated_at_micros`
这会导致 procedure 无法知道 API 层是否已经拿到了正式 OSS 兼容路径。
因此本次补充:
1. `asset_url: Option<String>`
### 6.2 槽位写入规则
`build_generated_asset_slot(...)` 改为:
1. 若输入提供 `asset_url`,则直接写正式路径。
2. 若输入未提供 `asset_url`,才回退为 `/generated-big-fish/...` 占位路径。
这样做的原因是:
1. 允许同一个 Big Fish procedure 兼容“占位生成”和“正式生成”两种调用方式。
2. 不需要为了正式图片再新增一条平行 procedure。
## 7. Big Fish 与 `asset_object` 的绑定语义
Big Fish 不新增专门资产表,继续复用:
1. `asset_object`
2. `asset_entity_binding`
绑定原则:
1. `entity_kind` 使用 Big Fish 会话实体语义。
2. `entity_id` 使用 `session_id`
3. `slot` 使用稳定可重建的槽位名。
推荐槽位命名:
1. `level_main_image:level-{n}`
2. `level_motion:level-{n}:{motion_key}`
3. `stage_background`
这样做可以:
1.`big_fish_asset_slot` 一一对应。
2. 让后续真正做“重新生成覆盖旧资产”时有稳定槽位。
## 8. 前端识别语义
当前 `BigFishResultView` 仍用路径前缀判断是否为占位图:
1. 包含 `/generated-big-fish/` -> `占位已生成`
2. 否则 -> `已生成`
本轮先保留这个最小判定方式,原因是:
1. 正式图片会改走 `/generated-big-fish-assets/`
2. 前端无需立即扩 contract 字段也能正确显示状态。
长期建议仍然是给 Big Fish 资产槽位补显式来源字段,但这不阻塞本轮正式出图。
## 9. 本轮验收标准
完成后需要满足:
1. `big_fish_generate_level_main_image` 会实际触发模型生成,并返回正式 Big Fish 图片。
2. `big_fish_generate_level_motion` 会实际触发模型生成,并返回静态动作预览图。
3. `big_fish_generate_stage_background` 会实际触发模型生成,并返回正式背景图。
4. SpacetimeDB 中对应 `big_fish_asset_slot.asset_url` 不再是 `/generated-big-fish/*`,而是 `/generated-big-fish-assets/*`
5. 结果页状态从“占位已生成”切到“已生成”。
6. `/generated-big-fish-assets/*` 能通过 Rust 同源代理正确读取 OSS 私有对象。
7. `cargo check -p api-server`
8. `cargo check -p module-big-fish`
9. `cargo check -p spacetime-module`
10. `spacetime generate`
11. `cargo check -p spacetime-client`
12. `npm run check:encoding`
## 10. 本轮明确暂不做
1. 不做视频动作生成。
2. 不做序列帧 manifest。
3. 不新增 Big Fish 专属资产数据库表。
4. 不把 Big Fish 结果页改成复杂工作流编辑器。
5. 不修改现有占位图路径的兼容职责。

View File

@@ -1,82 +0,0 @@
# 大鱼吃小鱼角色主图透明背景后处理对齐说明 2026-04-28
## 背景
当前大鱼吃小鱼的等级主图与动作关键帧 prompt 已经明确要求“按 RPG 角色资产口径生成透明背景”,但正式图片链实际仍主要依赖供应商出图结果本身。
这会带来一个问题:
1. prompt 约束只能提高透明背景命中率,不能保证每次都没有残留底色。
2. RPG 角色主图链已经在 Rust 后端落了一层 PNG alpha 后处理,大鱼链没有对齐,导致两条“角色主图口径”资产在最终成品一致性上仍有差异。
## 本次目标
把“大鱼吃小鱼生成的角色主图后处理流程”对齐到 RPG 角色主图链:
1. 等级主图正式图生成后,若下载结果为 PNG则复用 RPG 现有透明背景 alpha 后处理。
2. `idle_float` / `move_swim` 动作关键帧静态图同样复用这套处理。
3. 场地背景图不走这套处理,避免误把 9:16 场景背景做成透明底。
## 落地方案
### 1. 复用 RPG 透明背景后处理能力
`server-rs/crates/api-server/src/character_visual_assets.rs`
冻结现有 `try_apply_background_alpha_to_png(...)``pub(crate)` 复用入口,继续由 RPG 主图链维护这套“绿底/白底/软边缘”透明背景清理逻辑。
### 2. Big Fish 正式图链按资产类型决定是否启用后处理
`server-rs/crates/api-server/src/big_fish.rs`
`BigFishFormalAssetContext` 中新增:
1. `apply_transparent_background_post_process`
映射规则如下:
1. `level_main_image``true`
2. `level_motion``true`
3. `stage_background``false`
### 3. 下载完成后、写 OSS 前执行统一处理
`download_big_fish_remote_image(...)` 新增布尔开关参数。
当满足以下条件时执行后处理:
1. 当前资产槽位需要透明背景后处理
2. 上游下载结果 `mime_type == image/png`
执行顺序冻结为:
1. DashScope 出图
2. 下载远端 PNG
3. 复用 RPG `try_apply_background_alpha_to_png(...)`
4. 再写入 Big Fish 正式 OSS 对象
5. 确认 `asset_object`
6. 绑定到 Big Fish 槽位
## 为什么这样做
1. 这次需求说的是“生成后处理流程和 RPG 角色主图一致”,因此不能只继续加强 prompt必须把后处理链对齐。
2. 直接复用 RPG 已有实现,比在 Big Fish 再复制一份抠图算法更稳,也更符合仓库“默认复用现有系统”的约束。
3. 背景图是环境资产,不属于“角色主图口径”,如果也启用透明背景后处理,会造成错误裁底风险。
## 验收口径
1. 在 Big Fish 结果页点击 `生成并应用正式图 -> Lv.x 主图` 后,若 DashScope 返回 PNG正式落库前会执行和 RPG 主图相同的透明背景 alpha 处理。
2. 在 Big Fish 动作工坊点击 `生成并应用正式图` 后,`idle_float` / `move_swim` 的静态关键帧图同样执行该处理。
3. `生成背景` 仍保持完整场景图,不走透明背景后处理。
4. 编码检查通过Rust `api-server` 定向编译通过。
## 影响范围
1. `server-rs/crates/api-server/src/character_visual_assets.rs`
2. `server-rs/crates/api-server/src/big_fish.rs`
## 风险与边界
1. 当前后处理只在下载结果本身是 PNG 时生效;若供应商返回 JPEG/WebP则仍按原始格式入库。
2. 本次不新增新的 Big Fish 专属抠图算法,不改变 DashScope prompt 和 OSS 绑定协议。
3. 本次不修改 SpacetimeDB schema也不涉及 `migration.rs` 变更。

View File

@@ -1,126 +0,0 @@
# 大鱼吃小鱼提示词脚本拆分 2026-04-28
## 背景
大鱼吃小鱼当前在 `server-rs/crates/api-server/src/big_fish.rs``server-rs/crates/api-server/src/big_fish_agent_turn.rs` 中同时承载了三类不同职责的提示词:
1. Agent 聊天阶段的草稿生成提示词。
2. 结果页主图 / 生图提示词。
3. 结果页动作关键帧提示词。
这会带来两个直接问题:
1. 聊天共创脚本和正式资产脚本混在路由业务文件中,后续继续调词时很容易顺手改到状态编排逻辑。
2. 大鱼吃小鱼已经明确要求“草稿编译”和“结果页资产工坊”分离,如果提示词仍散落在业务实现里,后续很容易再次把动作资产逻辑误塞回 compile action。
## 本轮目标
把下面三类提示词显式拆到独立 prompt 脚本中:
1. 草稿生成提示词。
2. 生图提示词。
3. 动作提示词。
并保持以下边界不变:
1. 不改变 Big Fish 的会话表、草稿表、资产表结构。
2. 不改变 compile action 只编译草稿、不串行生成资产的现有口径。
3. 不改写当前中文提示词语义,只做脚本落位和调用收口。
## 落位方案
新增文件:
```text
server-rs/crates/api-server/src/prompt/big_fish.rs
```
该文件统一收口:
1. `BIG_FISH_AGENT_SYSTEM_PROMPT`
2. `build_big_fish_agent_prompt(...)`
3. `build_big_fish_level_main_image_prompt(...)`
4. `build_big_fish_level_motion_prompt(...)`
5. `build_big_fish_stage_background_prompt(...)`
6. `BIG_FISH_DEFAULT_NEGATIVE_PROMPT`
7. `BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT`
同时把 `prompt/mod.rs` 补齐为正式导出入口,和现有:
1. `puzzle/image.rs`
2. `character_visual.rs`
3. `character_animation.rs`
4. `scene_background.rs`
保持同一层级。
## 调用边界
### 1. 草稿生成
`server-rs/crates/api-server/src/big_fish_agent_turn.rs`
改为只负责:
1. 调用公共 LLM turn 执行器。
2. 解析 `replyText / progressPercent / nextAnchorPack`
3. 组装 finalize input。
不再内联维护大段 system prompt / output contract / chat prompt 拼接逻辑。
### 2. 生图与动作
`server-rs/crates/api-server/src/big_fish.rs`
改为只负责:
1. 读取当前 session 与 draft。
2. 根据 `asset_kind` 构造正式资产上下文。
3. 调用 DashScope 出图。
4. 下载、后处理、持久化并写入资产绑定。
主图、动作关键帧、背景图的正式提示词脚本都从 `crate::prompt::big_fish` 引入,不再内联在路由业务脚本中。
## 为什么三类脚本要继续分开
### 草稿生成提示词
它的职责是把玩法灵感收束成:
1. 玩法承诺
2. 生态视觉主题
3. 成长阶梯
4. 风险节奏
它面向的是 LLM 的结构化共创,不面向图片模型。
### 生图提示词
它的职责是把已经落库的等级蓝图翻译成“单体鱼形主图”的正式图片提示词。
它面向的是透明背景主体资产,需要强调:
1. 单体主体
2. 透明背景
3. 中心构图
4. 不出现 UI / 场景 / 多主体
### 动作提示词
它的职责是把等级蓝图和动作槽位翻译成“静态关键帧预览图”的正式图片提示词。
它和主图区别在于:
1. 需要显式带入 `motion_key`
2. 需要区分 `idle_float / move_swim`
3. 需要强调动作方向和关键帧姿态
因此不能继续复用同一段文本拼接后靠 if 分支临时改句子。
## 本轮验收
1. 大鱼吃小鱼草稿生成提示词已从 `big_fish_agent_turn.rs` 抽离。
2. 大鱼吃小鱼主图、动作、背景提示词已从 `big_fish.rs` 抽离。
3. 路由业务文件只保留编排、鉴权、调用与错误映射职责。
4. 新增 prompt 文件具备最小测试覆盖。
5. `npm run check:encoding` 通过,确保新增中文文档与 Rust 注释未被写坏。

View File

@@ -1,46 +0,0 @@
# 大鱼吃小鱼运行页规则入口说明 2026-04-26
## 背景
大鱼吃小鱼玩法规则已经在 PRD 与运行态技术方案中定义,但网站运行页没有给玩家查看规则的入口。玩家进入 `/big-fish` 或正式运行页后,只能看到当前等级、状态和事件日志,无法在游玩前快速理解吞噬、合成、胜负条件。
## 设计结论
1. 规则入口放在运行页顶部操作区,使用 `CircleHelp` 图标按钮。
2. 默认界面不直接铺规则长文案,点击按钮后打开独立模态窗口。
3. 模态窗口只保留玩家决策所需的核心规则:
- 拖动方向控制移动。
- 吃掉低级或同级野生实体并收编。
- 碰到更高级野生实体时,己方实体会被吃掉。
- 3 个同级己方实体自动合成更高一级。
- 拥有最高等级后通关,己方实体归零后失败。
4. 入口必须在移动端单手可点,不遮挡舞台主体。
5. 规则内容只做说明,不参与任何前端裁决;真实规则仍以后端运行快照为准。
## 游玩统计规则
所有作品都需要对自身以及用户做游玩统计。
大鱼吃小鱼正式运行时必须遵守:
1. 正式开始游玩已发布作品时,更新作品自身播放统计。
2. 已登录用户写入 `profile_played_world``world_key = big-fish:{session_id}`
3. `profile_id` 保存大鱼作品号/会话号,`world_type = BIG_FISH`
4. `world_title` 使用玩法草稿标题,`world_subtitle` 优先使用副标题,其次使用核心乐趣。
5. `owner_user_id` 使用大鱼作品归属用户 ID。
6. 退出或结算上报 `elapsedMs` 后,后端按增量刷新 `profile_dashboard_state.total_play_time_ms` 和明细中的 `last_observed_play_time_ms`
## 落地范围
1. `src/components/big-fish-runtime/BigFishRuntimeShell.tsx`
- 增加规则按钮与规则模态窗口。
- 复用 `UnifiedModal`,避免在当前玩法舞台内容流里展开说明。
2. `src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx`
- 覆盖规则入口打开与关闭。
## 验收口径
1. 进入大鱼吃小鱼运行页后,右上角可看到规则图标入口。
2. 点击规则入口后出现独立弹窗。
3. 弹窗能展示核心吞噬、合成、通关与失败规则。
4. 关闭弹窗后回到玩法舞台,不改变当前运行快照。

View File

@@ -1,55 +0,0 @@
# 角色主形象 IP 审核失败兜底修复2026-04-25
## 1. 问题
自动生成草稿素材时,角色「艾瑞克」主形象连续 3 次失败,供应商返回:
```json
{
"code": "IPInfringementSuspect",
"message": "Input data is suspected of being involved in IP infringement."
}
```
这类失败不是网络抖动。原样重试会把同一份包含角色姓名、长设定和可能触发审核的专名继续提交给 DashScope容易稳定失败。
## 2. 参考日志与文档
- 用户提供的失败日志:`request_id=a18fb05d-d3be-9b9c-8d37-e0427397789e``task_id=cb768c95-13b7-4790-9f18-35a8a8761b31``task_status=FAILED``code=IPInfringementSuspect`。该日志确认失败源于供应商 IP/内容审核,而不是 OSS、SpacetimeDB 或下载链路。
- `docs/audits/CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md`:确认角色主形象正式模型 prompt 层由后端 prompt builder 编译,不能只改前端默认描述文本。
- `docs/technical/CUSTOM_WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md`:确认当前主链已迁到 `server-rs/crates/api-server/src/custom_world_asset_prompts.rs``server-rs/crates/api-server/src/custom_world_ai.rs`,不再修改旧 `server-node`
- `docs/technical/ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md`:确认角色主形象真实生成入口走 Rust `api-server`、DashScope、OSS 与资产绑定链路。
- `server-rs/crates/api-server/src/character_animation_assets.rs` 现有日志/代码经验:动作生成已经有审核失败时的安全兜底 prompt 策略,本次沿用到角色主形象。
## 3. 修复方案
1. 在角色主图 prompt 层新增安全兜底 prompt
- 不携带角色姓名、作品名、长设定原文。
- 保留职业原型,如原创近战剑士、原创法术职业冒险者。
- 明确要求不参考现有动漫、游戏、影视、小说角色,不使用可识别 IP 元素。
2. 在 DashScope 角色主形象请求层识别审核类错误:
- `IPInfringementSuspect`
- `inappropriate`
- `sensitive`
- `risk`
- 中文内容审核、疑似侵权、知识产权等关键词。
3. 首次提交遇到上述错误时,后端自动用安全兜底 prompt 再提交一次。
4. 非审核类错误仍按原错误返回不隐藏模型、网络、OSS、超时等真实问题。
5. 错误映射保留 `raw` 字段,便于后续从日志直接看到供应商原始 `code/message/task_id`
## 4. 落地文件
- `server-rs/crates/api-server/src/prompt/character_visual.rs`
- 新增 `build_fallback_moderation_safe_character_visual_prompt`
- `server-rs/crates/api-server/src/custom_world_asset_prompts.rs`
- 导出角色主图审核兜底 prompt builder。
- `server-rs/crates/api-server/src/character_visual_assets.rs`
- 角色主形象 DashScope 请求增加审核兜底提交。
- `RequestModel` 阶段结构化日志增加 `moderationFallbackApplied`
- DashScope 上游错误保留 `raw`
## 5. 验收口径
- 用户不需要手动改「艾瑞克」这个名字;后端遇到 `IPInfringementSuspect` 会自动切换到原创安全 prompt 再试一次。
- 若兜底 prompt 生成成功,后续 OSS 草稿、发布和资产绑定链路保持原样。
- 若兜底 prompt 仍失败,错误中仍能看到 DashScope 原始失败内容,方便继续定位具体触发项。

View File

@@ -1,731 +0,0 @@
# 儿童动作识别互动玩法 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. 每个步骤进入时需要先展示本步骤文字字幕和语音播报入口,约 1 秒后再进入可交互阶段并展示绿色圆环、手势引导等检测提示。
7. 步骤完成后需要先进入完成停顿阶段,当前停顿约 0.8 秒;停顿期间保留完成反馈位置,后续可在该阶段补充完成特效或音效,再切换到下一步骤。
8. 入场等待和完成停顿阶段不消费动作完成判定,避免用户上一步残留动作直接触发下一步。
### 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
挥动左手
```
#### 检测目标
用户完成挥动左手。
当前本地 mocap 的 handedness 按摄像头视角输出,热身关内需要先换算成用户身体视角再判断:摄像头右侧手对应用户左手。挥动左手不是普通横向轨迹检测,而是用于确认现实环境中用户左侧手臂打开空间足够和安全。
完成条件必须同时满足:
1. 使用用户身体左手轨迹。
2. 手腕在左肩外侧达到最小外展距离。
3. 手腕不能处于自然下垂低位。
4. 最近连续有效帧中,手臂存在足够上下摆动幅度。
5. 最近连续有效帧中,肩膀到手腕向量的角度变化达到阈值。
6. 至少出现一次上下摆动方向变化。
#### 完成反馈
```text
真棒
```
#### 数据记录
记录用户挥动左手的轨迹、空间包络、角度范围和最大外展距离,保存为该用户对应的行为坐标。
---
### 7.9 挥动右手
#### 展示内容
播放伸展手臂挥动右手的手势引导。
用户进入该步骤 3 秒仍未完成动作时,可以播放引导动画。
#### 文案与语音
```text
挥动右手
```
#### 检测目标
用户完成挥动右手。
当前本地 mocap 的 handedness 按摄像头视角输出,热身关内需要先换算成用户身体视角再判断:摄像头左侧手对应用户右手。挥动右手不是普通横向轨迹检测,而是用于确认现实环境中用户右侧手臂打开空间足够和安全。
完成条件必须同时满足:
1. 使用用户身体右手轨迹。
2. 手腕在右肩外侧达到最小外展距离。
3. 手腕不能处于自然下垂低位。
4. 最近连续有效帧中,手臂存在足够上下摆动幅度。
5. 最近连续有效帧中,肩膀到手腕向量的角度变化达到阈值。
6. 至少出现一次上下摆动方向变化。
#### 完成反馈
```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. 空格键映射原地跳跃。
6. 调试输入只在步骤可交互阶段触发步骤完成;步骤入场字幕阶段和完成停顿阶段会忽略完成判定,便于观察节奏和后续补充特效。
当前硬件和动作检测接口接入:
1. 浏览器摄像头视频流已接入舞台背景。
2. 热身关全流程已通过 `src/services/useMocapInput.ts` 接入本地 mocap WebSocket `/stream`;动作数据源状态优先于浏览器背景摄像头状态展示。
3. mocap 包支持从 `general.body.center_norm` 读取身体中心,位置类步骤使用该身体中心更新角色剪影横向位置并完成圆环保持检测。
4. 身体中心横向坐标进入角色剪影前必须做输入稳定化处理:先 clamp 到 `0..1`,再使用小幅死区、低通阻尼和单包最大步长限制,避免硬件噪声造成角色左右误判、画面抽搐或视觉上的忽大忽小。当前实现参数为死区 `0.012`、阻尼系数 `0.28`、单包最大步长 `0.035`;位置保持检测使用稳定化后的角色坐标。
5. 角色剪影渲染需要把水平位移和跳跃表现拆开:外层只负责横向定位,内层资源只负责轮廓图和跳跃位移,避免 `left``transform` 同时抢占导致半透明资源重采样抖动。
6. mocap 包支持从 `actions/action/gesture/gestures/event/name/type` 读取动作名,并支持 `hands[]``leftHand/rightHand``left_hand/right_hand` 读取左右手坐标。
7. `hands[].landmarks` 存在时优先用手腕和 MCP 点计算掌心中心;掌心点不足时退回 wrist landmark再退回 hand 直出坐标。
8. `wave_greeting` 只消费左手、右手或未知单手的连续横向挥手轨迹,不再使用 `wave``hand_wave``open_palm`、张手状态或动作名直接完成判定;进入轨迹判定前必须先满足抬手有效区:优先使用 `hands[].landmarks.wrist``general.limb_nodes` 的同侧 `*_elbow` / `*_shoulder` 判断,当前阈值为 `wrist.y <= elbow.y + 0.04`,缺少肘部时使用 `wrist.y <= shoulder.y + 0.08`;缺少同侧肘部和肩膀参考时不允许招呼通过,不再使用身体中心兜底判断抬手。轨迹阈值为至少 5 个连续抬手点,横向 `x` 范围差值不小于 `0.075`,且至少出现 1 次横向方向变化,避免“手刚露出画面”或“手自然下垂抖动”被误判为招手。
9. `wave_left_hand``wave_right_hand` 只消费用户身体侧对应手的连续坐标轨迹,不再使用动作名、张手状态或 primary hand 兜底完成判定;本地 mocap handedness 当前按摄像头视角输出,因此用户左手使用 camera-right用户右手使用 camera-left。完成判定必须同时满足对应肩肘腕外展、手腕非自然下垂、连续有效帧、横向范围、上下摆动范围、肩腕角度范围和上下方向变化当前阈值为连续外展点不少于 5 个、横向 `x` 范围不小于 `0.055`、垂直 `y` 范围不小于 `0.08`、肩腕角度范围不小于 `28°`、外展距离不小于 `0.12`、手腕相对肩膀外侧距离不小于 `0.1`;后续以真实体验结果继续调参。
10. `jump_once` 消费 `jump/jump_once/hop` 等跳跃动作事件完成。
11. 键盘 `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-v3.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`:开始按钮绘本风按钮底图。
- `public/child-motion-demo/picture-book-wave-cat-body-guide-v6.png`:招手阶段中央猫咪身体底座资源,按可动纸偶结构只包含猫头、短身体和肩部连接点,不再和旧猫头、胸口或猫爪资源叠加。
- `public/child-motion-demo/picture-book-wave-cat-arm-guide-v6.png`:招手阶段左右独立手臂资源,也用于左右手阶段单手提示;网页用同一拆件镜像复用,并围绕肩部挂点做挥手摆动动画。
6. v2 资源按最终用途拆分CSS 必须按资源原始比例、`aspect-ratio``background-size: contain / auto` 等方式等比使用;禁止把方形面板强行拉伸为 HUD、状态条或地板也禁止把底部草坪扩展成覆盖角色脚下的大色块。
7. 猫咪招手引导资源使用 `cat-guide` 透明后处理:先由 image-2 生成品红底源图,再通过边缘背景连通区域去背,避免把浅粉、淡橘和暖棕主体误删。源图只保存在 `tmp/child-motion-demo-assets/`,正式页面只引用 `public/child-motion-demo/` 下的最终 PNG。
8. 若后续补充或重绘资源,应先运行 `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
```

View File

@@ -1,66 +0,0 @@
# 创作模板 Agent 聊天共用化设计2026-04-24
## 背景
当前创作模板的 Agent 聊天链路不一致RPG 世界共创与拼图共创已经由后端调用模型推理生成回复和锚点状态,大鱼吃小鱼仍在 SpacetimeDB 过程内用规则推断锚点并返回固定回复。这样会导致同一入口下不同模板的共创体验不一致,也会让后续新增模板重复实现聊天流程。
## 目标
1. 所有创作模板的 Agent 聊天都必须由后端模型推理生成回复与下一轮锚点状态。
2. Agent 聊天能力对齐 RPG 世界共创的功能效果:后端负责模型调用、锚点更新、进度推进与落库,前端只消费 session snapshot。
3. 不同模板在 Agent 聊天环节只允许通过“锚点问题配置”体现差异其余提示词骨架、流式回复、JSON 解析、失败兜底流程复用。
4. 锚点配置必须从代码逻辑中抽离为独立配置文件,新增模板时优先新增配置而不是复制聊天代码。
## 首版落地范围
| 模板 | 现状 | 本次处理 |
| --- | --- | --- |
| RPG 世界共创 | 已走模型推理,八锚点结构最完整 | 保持现有流程,作为统一体验标杆 |
| 拼图共创 | 已走模型推理,但锚点问题硬编码在 Rust 提示词中 | 把 5 个锚点问题迁入配置文件,提示词读取配置 |
| 大鱼吃小鱼 | 规则推断 + 固定回复 | 新增模型推理 turn读取 4 个锚点配置,提交消息后再 finalize 落库 |
## 配置设计
配置文件路径固定为:`server-rs/crates/api-server/src/creation_agent_anchor_templates.json`
每个模板配置包含:
- `templateId`:模板唯一标识。
- `displayName`:用于后端提示词,不直接展示在 UI。
- `creationGoal`:模板最终要收束出的可玩结果。
- `anchorQuestions`:锚点问题列表,包含 `key / label / question / requiredEffect`
首版配置只存“问题与效果”,不存模型输出 schema。原因是各模板 session 的锚点结构不同schema 仍由各自 domain 类型约束,避免为了统一而牺牲类型安全。
## 共用聊天骨架
后端新增 `creation_agent_anchor_templates` 模块,提供:
1. 读取并缓存配置文件。
2.`templateId` 返回模板配置。
3. 渲染统一的“锚点问题段落”。
各模板 agent turn 共用以下模型推理约束:
1. 系统提示词说明当前模板目标。
2. 插入配置文件中的锚点问题段落。
3. 插入当前锚点状态与最近聊天记录。
4. 要求模型只输出 JSON。
5. 流式过程中只把 `replyText` 增量给前端。
6. 完整响应解析后,由模板自身 domain 类型反序列化并落库。
## 大鱼吃小鱼落库调整
SpacetimeDB module 仍保持数据真相源职责,但不再在 `submit_big_fish_message` 内生成 assistant 回复与新锚点。流程调整为:
1. `submit_big_fish_message` 只写入用户消息,保留原锚点与进度。
2. api-server 调用模型生成 `replyText / progressPercent / nextAnchorPack`
3. 新增 `finalize_big_fish_agent_message_turn` procedure把模型结果写回 assistant 消息、锚点、进度与 `last_assistant_reply`
4. 模型失败时通过 finalize 记录失败提示,避免用户消息丢失。
## 约束
- 前端不新增逻辑分支,不在 UI 中展示规则说明类文本。
- 后端 prompt、配置与 Rust 代码必须保留中文注释或中文语义说明。
- 配置文件是后端资产,不依赖前端动态编辑。
- 若未来模板锚点结构统一,可以再把输出 schema 迁入配置;首版不做过度抽象。

View File

@@ -1,78 +0,0 @@
# 创作 Agent 聊天区滚动跟随策略修复
日期:`2026-04-23`
## 1. 背景
当前统一创作聊天工作区 [`src/components/creation-agent/CreationAgentWorkspace.tsx`](D:/Genarrative/src/components/creation-agent/CreationAgentWorkspace.tsx) 在以下任一变化时都会强制执行一次滚动到底部:
1. `session.messages`
2. `streamingReplyText`
3. `isStreamingReply`
原实现直接对底部占位节点执行:
1. `scrollIntoView`
2. `behavior: 'smooth'`
这会导致:
1. RPG 创作聊天在 SSE 流式回复期间持续被强拉到底部。
2. 用户手动上滑查看历史消息后,只要流式文本继续更新,就会再次被抢回到底部。
3. 统一聊天工作区已经复用到多条创作链,这个问题会同时影响所有使用 `CreationAgentWorkspace` 的品类。
## 2. 目标
把统一聊天区的滚动策略改成“条件跟随”,而不是“无条件强制到底”:
1. 用户本来就在底部附近时,新消息和流式回复继续跟随到底部。
2. 用户主动上滑离开底部后,不再因为流式更新被强制拉回底部。
3. 用户自己再次发送消息或点击推荐回复时,允许重新进入“跟随底部”状态。
4. 流式阶段不再对每个增量使用 `smooth scroll` 动画,避免持续抖动。
## 3. 方案
## 3.1 工作区组件内部维护滚动跟随态
`CreationAgentWorkspace` 内新增:
1. 聊天滚动容器 `ref`
2. `shouldAutoScrollRef`
判定规则:
1. 当滚动容器距离底部小于等于 `96px` 时,视为“仍在底部附近”。
2. 只有在 `shouldAutoScrollRef=true` 时,消息/流式文本更新才自动滚到底。
## 3.2 用户手动滚动优先
聊天列表监听 `onScroll`
1. 用户上滑离开底部后,把 `shouldAutoScrollRef` 置为 `false`
2. 之后流式 `reply_delta` 继续到来,也不再改写用户当前阅读位置
## 3.3 用户主动发送可重新挂到底部
以下动作视为用户主动回到当前对话尾部:
1. 输入框发送消息
2. 点击推荐回复
执行这些动作前,把 `shouldAutoScrollRef` 重新置为 `true`,保证用户主动推进对话后仍能看到最新回复。
## 3.4 适用范围
本修复落在统一工作区层,因此会同时覆盖:
1. RPG / Custom World Agent
2. Big Fish Agent
3. Puzzle Agent
不需要在各品类 controller 内重复补滚动判断。
## 4. 验收标准
1. 用户在聊天区手动上滑后,流式回复继续生成时,页面不再被持续拉到底部。
2. 用户停留在底部附近时,新消息仍能自然跟随到最新位置。
3. 用户发送新消息后,聊天区仍能回到最新对话尾部。
4. `CreationAgentWorkspace` 定向测试补齐并通过。

View File

@@ -1,41 +0,0 @@
# 创作 Agent Client 与平台流程 Controller 复用方案 2026-04-25
## 背景
RPG、自定义大鱼吃小鱼、拼图三类作品创作都已经采用 Agent-first 主链,但前端仍存在两类重复:
1. 三类 Agent client 都手写 `createSession / getSession / sendMessage / streamMessage / executeAction`
2. 平台入口壳层内直接维护大鱼、拼图的 session、流式消息、忙碌态、错误态与草稿恢复逻辑。
聊天 UI、SSE 解析、快捷补齐已经有共用模块,本轮只补齐 client 与平台流程编排层的复用,不改玩法领域数据结构。
## 目标
1. 新增通用 `createCreationAgentClient` 工厂统一请求、SSE POST、retry、超时与中文错误文案配置。
2. 大鱼、拼图 client 保留原导出函数名但内部先改走通用工厂RPG client 暂保留既有成熟实现,后续只有在不影响自动保存/结果预览语义时再接入。
3. 新增平台创作流程 controller hook收口轻量玩法的创建会话、恢复草稿、发送流式消息与执行 action。
4. 平台壳层只保留玩法差异动作:运行态启动、作品删除、拼图公开详情跳转等。
## 非目标
1. 不统一 RPG、大鱼、拼图的 session schema。
2. 不把大鱼或拼图强行接入 RPG 的发布门禁、自动保存、运行时协议。
3. 不改后端 SpacetimeDB 表、procedure 或现有路由。
## 落地边界
1. `src/services/creation-agent/creationAgentClientFactory.ts`
- 负责统一 HTTP/SSE client 骨架。
- 允许每个玩法配置 basePath、runtime 前缀、错误文案、返回值提取方式。
2. `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
- 负责通用前端流程态session、busy、error、streaming、open、restore、submit、execute。
- 通过 adapter 接收玩法差异client、stage、compile action、session 是否已有 draft。
3. `PlatformEntryFlowShellImpl.tsx`
- 大鱼与拼图切到 controller。
- 保留 RPG 现有成熟 controller不在本轮合并避免把 RPG 复杂自动保存链拉进轻量玩法抽象。
## 验收
1. 已接入工厂的 Agent client 公开 API 不变RPG client 公开 API 与既有实现都不变。
2. 大鱼、拼图仍能从平台入口新建、恢复草稿、发送消息、生成结果页。
3. 现有定向测试通过,编码检查通过。

View File

@@ -1,76 +0,0 @@
# Agent 创作页文档输入上传方案
更新时间:`2026-04-25`
## 1. 目标
Agent 创作页需要支持用户上传文档,并把文档内容解析成当前输入框里的文本,让用户可以继续编辑后再发送给 Agent。
本次只解决“文档作为输入内容”的轻量闭环,不把文件作为资产入库,也不改变 Agent 会话、消息、草稿生成的后端主链。
## 2. 职责边界
1. 前端 `CreationAgentWorkspace` 负责展示上传入口、先做文件格式与大小预检、读取浏览器选择的文件为 base64、调用解析接口、把返回文本追加到输入框。
2. Rust `api-server` 负责文件类型、大小、编码与文本抽取规则,前端不直接承载文档解析逻辑。
3. 解析完成后仍走现有 `onSubmitText` 消息提交,不新增 Agent 消息结构。
4. 该能力覆盖所有复用 `CreationAgentWorkspace` 的 Agent 创作页RPG / 自定义世界、拼图、大鱼吃小鱼。
## 3. 首版支持范围
支持扩展名:
1. `.txt`
2. `.md`
3. `.markdown`
4. `.csv`
5. `.json`
大小限制:单文件最大 `256 KiB`
编码限制:首版按 UTF-8 文本处理。若文件不是 UTF-8服务端返回 `400`,由前端展示错误。
暂不支持 `.pdf` / `.doc` / `.docx` 的二进制结构解析;后续扩展时只需要在 Rust 解析接口内部补 extractor不改变前端输入框接入方式。
## 4. 接口设计
路径:`POST /api/runtime/creation-agent/document-inputs/parse`
鉴权Bearer 登录态。
请求:
```json
{
"fileName": "世界设定.md",
"contentType": "text/markdown",
"contentBase64": "..."
}
```
响应:
```json
{
"document": {
"fileName": "世界设定.md",
"contentType": "text/markdown",
"sizeBytes": 128,
"text": "..."
}
}
```
## 5. UI 规则
1. 输入框左侧新增文件图标按钮,使用图标与 hover title 表达,不在页面铺功能说明文本。
2. 上传前先在浏览器侧拒绝不支持格式、空文件和超限文件,避免无意义读取大文件。
3. 上传处理中禁用按钮和发送按钮,避免同一输入框状态并发写入。
4. 文件解析成功后追加到当前草稿文本后方,若当前草稿非空则用两个换行分隔。
5. 错误展示复用输入区附近的短错误条,不弹独立面板。
## 6. 验收标准
1. 上传 `.txt``.md` 后,输入框出现文档内容。
2. 输入框已有内容时,解析文本追加在末尾,并用空行分隔。
3. 上传不支持格式或超限文件时,页面展示中文错误,不发送 Agent 消息。
4. 定向测试覆盖解析客户端、Rust 接口和 `CreationAgentWorkspace` 上传交互。

View File

@@ -1,24 +0,0 @@
# 创作 Agent 发送后即时等待点动画修复
日期:`2026-04-25`
## 1. 背景
统一创作 Agent 工作区已经用 `CreationAgentWorkspace` 承载 RPG / Custom World、大鱼吃小鱼、拼图三条聊天链路。旧展示条件只有在 `streamingReplyText` 已经收到文本时才追加临时 assistant 气泡,因此用户发送消息后到首个 SSE token 到达前,聊天区会短暂没有任何等待反馈。
## 2. 设计
本轮只改前端表现层不改变后端会话、SSE、消息落库或推荐回复语义
1. `CreationAgentWorkspace``isStreamingReply` 作为临时 assistant 气泡的展示条件。
2.`streamingReplyText` 为空时,临时气泡内部展示三个脉冲点。
3. 当首个流式文本到达后,同一个临时气泡切换为文本内容与光标。
4. 最终 session 回写后,临时气泡消失,由正式 assistant 消息接管原位置。
5. 大鱼吃小鱼与拼图适配层显式透传 `isStreamingReply`,不再用 `Boolean(streamingReplyText)` 推断等待状态。
## 3. 验收
1. 用户发送消息后,聊天列表底部立即出现三点等待动画。
2. 首个 SSE 文本到达前等待动画持续存在。
3. 流式文本到达后等待动画切换为正常流式回复。
4. `CreationAgentWorkspace` 定向测试覆盖空文本流式等待态。

View File

@@ -1,35 +0,0 @@
# 创作 Agent 发布门槛结果页归一化回写修正
日期:`2026-04-24`
## 1. 问题现象
`custom_world.publish_gate` 诊断日志显示:
1. `has_draft_profile=true`
2. `has_result_preview=true`
3. `has_world_hook=true`
4. `has_core_conflicts=true`
5. 但仍存在 `publish_missing_player_premise / publish_missing_main_chapter / publish_missing_first_act`
这说明接口可正常读取 session问题不在 `GET /api/runtime/custom-world/agent/sessions/:sessionId` 本身,而在结果页 profile 回写到 session 时,发布门槛需要的部分结构字段没有稳定保留下来。
## 2. 根因
前端结果页通过 `normalizeCustomWorldProfileRecord``resultPreview.preview` 转成 `CustomWorldProfile`。该归一化模型原本主要服务作品库与运行时展示,只保留了 `settingText / summary / playerGoal / creatorIntent / anchorContent / sceneChapterBlueprints` 等字段,没有把后端发布门槛直接读取的顶层 `worldHook / playerPremise` 纳入 `CustomWorldProfile` 稳定字段。
当自动保存或发布前执行 `sync_result_profile` 时,前端会把归一化后的 profile 传回 SpacetimeDB。若这份 profile 中缺少顶层 `playerPremise`,且 `creatorIntent / anchorContent` 又未包含可读玩家切入字段,后端最终 publish gate 会继续报 `publish_missing_player_premise`
## 3. 修复口径
1. `CustomWorldProfile` 显式声明 `worldHook / playerPremise` 为 Agent 发布快照兼容字段。
2. `normalizeCustomWorldProfileRecord` 保留顶层 `worldHook / playerPremise`,并在缺失时从 `creatorIntent.worldHook / creatorIntent.playerPremise / summary / playerGoal` 做最小回填。
3. 不在 UI 新增规则说明文案;这两个字段只作为后端发布门槛与 session 回写的稳定数据槽位。
4. 后端 publish gate 继续以 SpacetimeDB 中的 `draft_profile_json` 为最终真相源,前端只负责把结果页当前 profile 完整同步回去。
## 4. 验收标准
1.`resultPreview.preview` 构建结果页 profile 后,`worldHook / playerPremise` 不会被前端归一化丢弃。
2. 自动保存或点击发布前执行 `sync_result_profile` 时,传回后端的 profile 保留发布门槛所需顶层字段。
3. 若当前草稿确实包含玩家切入与 `sceneChapterBlueprints[*].acts`,后端诊断日志不应再出现对应结构 blocker。
4. 若草稿真实缺失章节或第一幕,`publish_missing_main_chapter / publish_missing_first_act` 仍应保留,不做前端假放行。

View File

@@ -1,96 +0,0 @@
# 创作 Agent 发布门槛字段对齐修复
日期:`2026-04-23`
## 1. 问题现象
RPG 创作结果页已经能看到完整草稿内容,但页面底部仍然持续显示旧的发布阻断项,例如:
1. 缺少 `world hook`
2. 缺少 `player premise`
3. 缺少主线章节草稿
4. 缺少主线第一幕
同时“发布并进入世界”按钮保持禁用,无法实际发布。
## 2. 根因
这不是单纯的前端提示未刷新,而是 Rust `publish gate` 仍在按旧 schema 校验 `draft_profile_json`
当前前端结果页、自动保存和 session preview 主链的真实结构已经演进为:
1. 世界一句话与玩家切入信息优先存放在 `anchorContent``creatorIntent`
2. 场景章节主链字段为 `sceneChapterBlueprints`
3. `settingText` 也会承载世界总体一句话设定
问题最初在拆分后的 `server-rs/crates/spacetime-module/src/custom_world/mod.rs` 中被修过一版,但当前线上实际执行入口仍保留在 `server-rs/crates/spacetime-module/src/lib.rs`
也就是说,真正参与 Agent session snapshot、结果页 publish gate 刷新和 `publish_world` 动作校验的,仍然是 `lib.rs` 里的历史实现;而它还只检查旧字段:
1. `worldHook`
2. `playerPremise`
3. `chapters`
4. `sceneChapters`
结果导致:
1. 结果页展示的是新 preview
2. 发布门槛检查读的是旧字段
3. 同一个草稿在 UI 看起来“已经有内容”,但 gate 仍然误判为缺失
因此会出现“拆分模块里的代码已经对齐,但页面实际 blocker 仍然不消失”的假象。
此外,`lib.rs` 里的最小草稿兜底结构也没有补上 `sceneChapterBlueprints` 默认槽位,导致部分恢复、回滚和草稿兜底链路继续偏向旧 schema。
## 3. 修复策略
本轮统一把实际入口 `server-rs/crates/spacetime-module/src/lib.rs` 的发布门槛与最小草稿结构对齐到当前前端主链 schema
1. `world hook` 检查同时兼容:
- `worldHook`
- `creatorIntent.worldHook`
- `anchorContent.worldPromise.hook`
- `settingText`
2. `player premise` 检查同时兼容:
- `playerPremise`
- `creatorIntent.playerPremise`
- `anchorContent.playerEntryPoint.openingIdentity`
- `anchorContent.playerEntryPoint.openingProblem`
- `anchorContent.playerEntryPoint.entryMotivation`
3. 主线章节检查同时兼容:
- `chapters`
- `sceneChapterBlueprints`
- `sceneChapters`
4. 主线第一幕检查优先读取:
- `sceneChapterBlueprints[*].acts`
- `sceneChapters[*].acts`
5. 最小草稿兜底结构同时补上 `sceneChapterBlueprints` 空数组,避免恢复链路重新回落到旧字段集合。
## 4. 验收标准
1. 结果页已包含 `anchorContent / creatorIntent / sceneChapterBlueprints` 的草稿,不再被旧 blocker 误判。
2. `publishReady` 会随当前 session 最新 preview 正确刷新。
3. “发布并进入世界”在 blocker 清空后恢复可点击。
4. `ensure_minimal_draft_profile(...)` 生成的兜底草稿也包含 `sceneChapterBlueprints`
5. 新增 Rust 单测,覆盖“当前 Agent 结果 schema 不应再误报 blocker”与“最小草稿必须保留 `sceneChapterBlueprints` 默认槽位”。
## 5. 亮色主题阻断项弹窗配色修复
日期:`2026-04-24`
### 5.1 问题现象
点击“发布并进入世界”但仍存在阻断项时,`PublishBlockersDialog` 会通过 portal 挂载到 `document.body`。在亮色主题下,弹窗面板使用平台浅色面板变量,但标题、分隔线和阻断项标签仍混用暗色主题下的 `text-white``border-white/10``text-amber-100/78` 等硬编码类名,导致局部对比度和色相不一致。
### 5.2 修复口径
1. portal 根节点显式补上 `platform-theme platform-theme--${platformTheme}`,避免弹窗脱离原页面主题变量继承。
2. 弹窗标题、上下分隔线统一改为 `--platform-text-strong``--platform-subpanel-border`
3. 阻断项卡片复用 `platform-banner platform-banner--warning`,由平台主题变量决定亮色和暗色下的边框、背景与警示文字色。
4. 阻断项正文保持 `--platform-text-strong`,保证亮色主题下可读性,不再依赖暗色主题的琥珀色文本。
### 5.3 验收标准
1. 亮色主题下阻断项弹窗标题、正文、分隔线和按钮均保持平台浅色视觉体系。
2. 阻断项卡片呈现柔和警示底色,不出现白字或过浅琥珀字落在浅底上的情况。
3. 暗色主题下弹窗仍保持原有平台暗色 warning banner 风格。

View File

@@ -1,31 +0,0 @@
# 创作 Agent 结果页 SSE 断开修复
日期:`2026-04-24`
## 1. 问题
RPG 世界共创草稿进入生成结果页后,前端仍可能保留上一条聊天消息的 `/messages/stream` 连接。该连接继续接收 `reply_delta` 时,会让结果页阶段仍表现为“聊天还在连着 SSE”。
## 2. 原因
当前聊天流式请求由 `streamRpgCreationMessage` 发起,底层使用 `fetch` 读取 SSE `ReadableStream`。旧实现只在请求自然结束后清理 `isStreamingAgentReply`,没有在以下 UI 生命周期主动中止网络流:
1.`agent-workspace` 跳到 `custom-world-result`
2. 清空或切换 `activeAgentSessionId`
3. 当前入口组件卸载。
因此,结果页虽然不再展示聊天工作区,但浏览器侧仍可能持有未完成的流读取器。
## 3. 修复设计
1. `TextStreamOptions` 增加 `signal?: AbortSignal`,让所有创作 Agent 流式读取都具备统一取消入口。
2. RPG 共创 `/messages/stream``fetch` 透传该 `signal`
3. `useRpgCreationSessionController` 持有当前聊天流的 `AbortController`
4.`selectionStage` 离开 `agent-workspace` / `custom-world-generating`,立即 `abort()` 当前聊天 SSE并清空临时流式文本。
5. session 切换、未登录清理、组件卸载时同样中止旧 SSE避免慢响应回写旧工作区状态。
## 4. 验收
1. 聊天中触发草稿生成并进入结果页后,浏览器 Network 中旧 `/messages/stream` 请求应变为 canceled/aborted 或结束。
2. 结果页不再继续追加聊天 `reply_delta`
3. 回到 Agent 工作区后,新的聊天消息会创建新的 SSE不复用已中止连接。

View File

@@ -1,45 +0,0 @@
# 创作 Agent Session 同步渲染环修复
日期:`2026-04-23`
## 1. 问题现象
本地联调时,前端在打开 RPG 创作 Agent 工作区后,会持续高频请求:
- `GET /api/runtime/custom-world/agent/sessions/:sessionId`
`api-server.log` 可以看到,同一个 `sessionId` 会在几毫秒到几十毫秒间隔内被重复读取很多次,明显高于正常 operation 轮询的 `1200ms` 周期。
## 2. 根因
这不是后端主动重试,也不是 session client 自带重试,而是前端 `useEffect` 被不稳定依赖反复触发。
触发链如下:
1. `PlatformEntryFlowShellImpl.tsx` 内部把 `enterCreateTab` 定义为:
- 依赖整个 `platformBootstrap` 对象。
2. `usePlatformEntryBootstrap()` 虽然内部的 `setPlatformTab` 是稳定回调,但返回值是一个新的对象字面量。
3. 组件每次 render 时,`platformBootstrap` 引用都会变化,导致 `enterCreateTab` 也变成新的函数引用。
4. `useRpgCreationSessionController.ts` 中“同步当前 Agent session 快照”的 `useEffect` 依赖了 `enterCreateTab`
5. 该 effect 每次重跑都会调用 `syncAgentSessionSnapshot(activeAgentSessionId)`,进而触发一次新的 `GET /agent/sessions/:sessionId`
6. `syncAgentSessionSnapshot(...)` 成功后会 `setAgentSession(...)`,又导致页面 render从而形成新的 render -> 新 `enterCreateTab` -> effect 重跑 -> 再次 GET 的闭环。
因此,真正的根因是:
- `session 同步 effect` 被一个与业务无关、且每次 render 都变化的函数依赖错误地牵连进了渲染环。
## 3. 修复策略
本轮不改后端语义,只收紧前端依赖稳定性:
1. `PlatformEntryFlowShellImpl.tsx` 不再让 `enterCreateTab` 依赖整个 `platformBootstrap` 对象。
2. 先解构稳定的 `setPlatformTab`,再用它生成 `enterCreateTab`
3. 保持 `useRpgCreationSessionController.ts` 现有 effect 逻辑不变,只让它接收到稳定的 `enterCreateTab` 引用。
4. 增加前端回归测试,确保打开 RPG Agent 工作区后session 快照不会因为 render 抖动而被重复拉取。
## 4. 验收标准
1. 打开 RPG 创作工作区后,允许出现首轮必要的 session 同步请求,但不能进入高频重复 GET。
2. 未启动 operation 轮询时,不应出现毫秒级连续读取同一 `sessionId` 的现象。
3. 存在 `activeAgentOperationId` 时,只保留原有 `1200ms` 轮询与完成态后的单次 session 刷新。
4. 创作工作区、草稿结果页、作品详情等原有导航语义保持不变。

View File

@@ -1,71 +0,0 @@
# 创作 Agent 流式消息与草稿切换稳定性修复
日期:`2026-04-23`
## 1. 背景
统一创作工作区已经承载 RPG 世界共创、大鱼吃小鱼和拼图等 Agent 对话。当前 RPG 世界共创在本地联调中暴露出以下前端状态抖动:
1. AI 流式回复过程中,中文内容会先出现乱码,随后又被正常文本覆盖。
2. 玩家刚发送的消息会在聊天列表中短暂出现,随后消失又重新出现。
3. AI 回复会短暂插在玩家消息中间,之后又跳回底部。
4. 曾经打开过某个草稿后,再打开另一个草稿或创建新对话时,结果页可能在旧草稿和当前内容之间来回闪烁。
这些现象的共同原因不是单个滚动动作,而是同一 UI 区域同时被多套不同来源的状态驱动本地乐观消息、SSE 临时回复、服务端最终 session 快照、旧草稿结果页缓存和异步恢复结果。
## 2. 目标
本轮修复只收敛前端展示稳定性,不改变后端业务语义:
1. 聊天列表只展示一条稳定的玩家消息,不因最终 session 回写而闪消。
2. AI 流式回复始终作为当前尾部 assistant 消息呈现,不和正式消息互相插队。
3. SSE 中文文本按 UTF-8 流式边界安全解码,流结束时刷新解码器尾部缓存。
4. 草稿切换、打开已有草稿、新建对话时先清理旧结果页缓存,旧异步恢复结果不得覆盖当前视图。
5. 继续保留“用户主动上滑后不强制滚到底部”的聊天区滚动策略。
## 3. 设计
## 3.1 SSE 事件读取
`src/services/creation-agent/creationAgentSse.ts` 继续作为统一 SSE 读取器,但需要补齐以下边界:
1. 使用 UTF-8 `TextDecoder` 的 streaming 模式接收 chunk。
2. `reader.read()` 结束后调用 `decoder.decode()` 刷新尾部缓冲,避免多字节中文字符残留在解码器内部。
3. 事件分隔同时兼容 `\n\n``\r\n\r\n`
4. `reply_delta``text` 字段按“当前可展示文本”传给 UI不在读取器内追加避免累计文本和增量文本语义混用。
## 3.2 玩家消息展示
RPG Agent 发送消息时,本地乐观玩家消息仍保留,但最终 session 回写时必须做稳定合并:
1. 若服务端快照已包含同一个 `clientMessageId`,以服务端消息为准。
2. 若服务端快照暂未包含该消息,临时保留本地消息,直到后续快照补齐。
3. 合并只按消息 `id` 去重,不整包丢弃本地尾部消息。
## 3.3 AI 流式回复展示
统一聊天工作区不再把流式回复作为独立于列表之外的气泡随意附加,而是在展示消息数组中合成一个稳定的尾部临时 assistant 消息:
1. session 正式消息仍是基础列表。
2. 有流式文本时,追加或替换尾部临时 assistant 消息。
3. 最终 session 到来后,临时消息消失,由正式 assistant 消息接管同一视觉位置。
4. 推荐回复只挂在正式最后一条 assistant 消息上,流式临时消息不展示推荐回复。
## 3.4 草稿切换
打开已有草稿、打开 Agent 草稿、新建 RPG Agent 对话前,必须先清理旧结果页相关缓存:
1. `generatedCustomWorldProfile`
2. `customWorldGenerationViewSource`
3. `customWorldResultViewSource`
4. 自动保存状态
异步读取 session 时要以本次打开的 `sessionId` 作为准入条件,防止上一个草稿的慢响应覆盖当前草稿。
## 4. 验收标准
1. 玩家输入发送后在聊天列表中只稳定出现一次,不再闪消。
2. AI 流式回复只在底部连续更新,不插入玩家消息中间。
3. 中文流式回复不再出现先乱码后正常的过渡。
4. 从一个草稿切换到另一个草稿或新建对话时,不再短暂显示旧草稿结果页。
5. 用户手动上滑聊天区后,流式更新仍不强制抢回底部。

View File

@@ -1,53 +0,0 @@
# 创作 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` 与编码检查通过。

View File

@@ -1,89 +0,0 @@
# 创作类别开启超时兜底修复记录
日期:`2026-04-22`
## 1. 问题现象
创作中心点击某个创作类别后,入口卡片会进入 `正在开启` 状态,但在后端创建会话迟迟不返回时,界面没有明确失败反馈,用户体感就是“卡死”。
本次定位覆盖入口:
1. `角色扮演 RPG`
2. `大鱼吃小鱼`
3. `拼图玩法`
## 2. 根因结论
这次问题不是前端少写了 `finally`
真实根因是:
1. 创作类别点击后会立即把入口置为 busy。
2. 会话创建请求如果在运行时后端 / Rust 代理 / Spacetime client 这一段长时间无返回,前端 Promise 就不会及时结束。
3. 旧实现缺少“创作入口启动阶段”的独立超时兜底,所以 busy 会持续停留在 `正在开启`
## 3. 本次修复口径
本轮只修“类别开启阶段不能无限等待”,不改创作工作区内部消息流与生成流的超时策略。
冻结口径如下:
1. 创作类别创建会话请求统一增加启动超时。
2. 超时后必须退出 `正在开启` busy 状态。
3. UI 必须展示中文可读错误,不能直接显示底层 `TimeoutError` 或毫秒数字。
4. Node 代理转发 Rust 新玩法接口时也必须有上游超时,避免代理层持续悬挂。
## 4. 具体落地
### 4.1 前端请求层
`src/services/apiClient.ts` 增加 `timeoutMs` 能力:
1. 请求可选传入超时毫秒数。
2. 到达超时后通过 `AbortController` 中断请求。
3. 向上抛出统一 `TimeoutError`
### 4.2 创作类别入口
以下创建会话入口统一使用 `15000ms` 启动超时:
1. `src/services/rpg-creation/rpgCreationAgentClient.ts`
2. `src/services/big-fish-creation/bigFishCreationClient.ts`
3. `src/services/puzzle-agent/puzzleAgentClient.ts`
### 4.3 错误文案
`src/components/rpg-entry/rpgEntryShared.ts` 中统一把超时错误映射为中文提示:
1. RPG`开启创作工作台超时,请确认运行时后端已启动后重试。`
2. Big Fish`开启大鱼吃小鱼创作工作台超时,请确认运行时后端已启动后重试。`
3. 拼图:`开启拼图创作工作台超时,请确认运行时后端已启动后重试。`
### 4.4 Node 代理
以下代理路由新增上游超时:
1. `server-node/src/routes/bigFishProxyRoutes.ts`
2. `server-node/src/routes/puzzleProxyRoutes.ts`
超时后返回:
1. `大鱼吃小鱼后端响应超时`
2. `拼图后端响应超时`
## 5. 验收标准
修复后需要满足:
1. 点击创作类别时,后端长时间无返回不会无限停留在 `正在开启`
2. 超时后入口按钮恢复可点击。
3. 页面展示中文错误提示。
4. Big Fish / 拼图的新玩法代理链同样不会无限挂起。
## 6. 本轮回归
本轮至少补以下回归:
1. `apiClient` 请求超时回归。
2. Big Fish 类别开启超时回归。
3. 拼图类别开启超时回归。

View File

@@ -1,85 +0,0 @@
# 创作入口鉴权错误串味修复
日期:`2026-04-22`
## 1. 问题现象
平台首页点击“创作”后,用户在创作入口浮层或创作中心起始卡片中会看到:
- `缺少 Authorization Bearer Token`
该文案直接暴露了后端鉴权实现细节,不符合平台入口的产品语义,也会让用户误以为“点击创作弹窗本身就失败了”。
## 2. 根因拆解
本次问题实际由两层叠加造成:
1. `useRpgCreationSessionController` 把“恢复旧 Agent 会话失败”的错误写入 `creationTypeError`
2. `PlatformEntryFlowShellImpl` 又把 `creationTypeError` 同时透传给:
- 创作中心起始卡片 `createError`
- 创作类型浮窗 `error`
- 平台首页 `platformError`
结果是:
- 旧会话恢复失败
- 未登录态残留会话恢复
- 本地 access token 丢失但 refresh cookie 仍在
这些与“当前点击新建创作”并不完全等价的错误,被错误地展示到了新建创作入口上。
## 3. 修复策略
### 3.1 错误分层
`useRpgCreationSessionController` 中新增:
- `agentWorkspaceRestoreError`
约束:
1. 旧 Agent 会话恢复失败只写入 `agentWorkspaceRestoreError`
2. 用户主动点击新建创作失败才写入 `creationTypeError`
3. 创作中心起始卡片和创作类型浮窗只展示“新建入口错误”
4. 平台页和工作区恢复占位文案展示“恢复态错误”
### 3.2 鉴权兜底
`fetchWithApiAuth` 中补充规则:
1. 受保护请求若本地没有 bearer token
2. 且请求未声明 `skipAuth / skipRefresh`
3. 先尝试 `ensureStoredAccessToken()` 静默补票
4. 补票失败再继续原始请求
这样可以覆盖“refresh cookie 仍有效,但本地 access token 丢失”的场景,避免后端直接返回“缺少 Authorization Bearer Token”。
### 3.3 用户态错误文案收敛
`resolveRpgEntryErrorMessage``401 UNAUTHORIZED``缺少 Authorization Bearer Token` 统一映射为:
- `当前登录状态已失效,请重新登录后继续。`
目标是把后端实现细节收束成平台用户可理解的恢复动作。
## 4. 影响范围
本轮覆盖:
1. RPG / Custom World 创作入口
2. 平台创作中心起始卡片
3. 平台创作类型浮窗
4. 统一前端 API 鉴权请求层
本轮不改:
1. 后端 `401` 契约
2. 登录弹窗交互
3. Big Fish / Puzzle 的后端路由鉴权策略
## 5. 验收
1. 点击“创作”后,不再出现原始 `Authorization Bearer Token` 报错文案
2. 旧会话恢复失败时,错误只停留在恢复上下文,不污染新建创作入口
3. 本地 token 丢失但 refresh 仍有效时,前端可自动补票后继续请求
4. 相关测试与编码检查通过

View File

@@ -1,113 +0,0 @@
# 创作入口配置数据库化与 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
```

View File

@@ -1,84 +0,0 @@
# 创作链路重构工作包 A 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 A命名规范与目录骨架**,约束如下:
1. 先建立 RPG 创作域的新命名落点。
2. 先提供 façade 和 barrel不迁移主流程行为。
3. 不提前修改工作包 B 到 H 的大块业务逻辑。
## 2. 本次已落地内容
## 2.1 前端目录骨架
已新增以下目录与 façade
1. `src/components/game-shell/rpg-creation-flow/`
2. `src/components/rpg-creation-result/`
3. `src/components/rpg-creation-editor/`
4. `src/services/rpg-creation/`
当前策略:
1. `RpgCreationShell` 继续桥接旧的 `PreGameSelectionFlow`
2. `RpgCreationResultView` 继续桥接旧的 `CustomWorldResultView`
3. `RpgCreationEntityEditorModal` 继续桥接旧的 `CustomWorldEntityEditorModal`
4. `rpgCreation*Client` 继续桥接 `aiService.ts``storageService.ts``customWorldCoverAssetService.ts`
5. `rpgCreationPreviewAdapter` 继续桥接旧的前端草稿编译函数,明确它只是过渡层。
## 2.2 后端目录骨架
已新增以下 RPG 创作域 façade
1. `server-node/src/routes/rpgCreationAgentRoutes.ts`
2. `server-node/src/routes/rpgWorldWorksRoutes.ts`
3. `server-node/src/routes/rpgWorldLibraryRoutes.ts`
4. `server-node/src/routes/rpgWorldGalleryRoutes.ts`
5. `server-node/src/services/RpgAgentOrchestrator.ts`
6. `server-node/src/services/RpgAgentSessionStore.ts`
7. `server-node/src/services/RpgWorldPreviewCompiler.ts`
8. `server-node/src/services/RpgWorldWorkSummaryService.ts`
当前策略:
1. Agent route 与 orchestrator/session store 先用新命名 façade 对齐。
2. works/library/gallery 路由先建立空骨架和基础 path 常量,避免下一轮迁移继续回落到旧命名。
3. `RpgWorldPreviewCompiler` 先桥接旧 `runtimeProfile.ts` 编译能力,为工作包 G 的目录化拆分预留落点。
## 2.3 共享契约骨架
已新增以下共享契约入口:
1. `packages/shared/src/contracts/rpgAgentAnchors.ts`
2. `packages/shared/src/contracts/rpgAgentDraft.ts`
3. `packages/shared/src/contracts/rpgAgentSession.ts`
4. `packages/shared/src/contracts/rpgAgentActions.ts`
5. `packages/shared/src/contracts/rpgCreationPreview.ts`
6. `packages/shared/src/contracts/rpgCreationWorkSummary.ts`
当前策略:
1. 会话、动作、作品摘要先从旧 `customWorldAgent.ts` 做类型级兼容导出。
2. `rpgAgentDraft.ts` 先把 foundation draft、draft card 等草稿相关类型收口成独立入口,给工作包 H 后续物理拆分预留稳定导入点。
3. `packages/shared/src/index.ts` 已补上对 RPG 草稿契约骨架的根导出,避免后续工作包继续回退到旧 `customWorldAgent.ts` 取类型。
4. `rpgCreationPreview.ts` 明确标记当前 preview 仍是 legacy profile 兼容载体,避免误认为 preview contract 已经完成。
## 3. 本次没有做的事
以下内容仍保持原状,留给后续工作包:
1. 没有拆 `PreGameSelectionFlow.tsx` 内部编排。
2. 没有拆 `CustomWorldResultView.tsx``CustomWorldEntityEditorModal.tsx``CustomWorldRoleAssetStudioModal.tsx` 内部 section。
3. 没有把 `runtimeRoutes.ts` 中的 works/library/gallery 真正迁出。
4. 没有改 `customWorldAgentOrchestrator.ts``customWorldAgentSessionStore.ts``runtimeProfile.ts` 的内部职责。
5. 没有改变任何线上行为或接口语义。
## 4. 对后续工作包的直接收益
1. 工作包 B 可以直接把平台壳层 hooks 落到 `src/components/game-shell/rpg-creation-flow/`
2. 工作包 C 可以直接把结果页与编辑器 section 落到新目录,而不用先讨论命名。
3. 工作包 D 可以直接从 `rpgCreation*Client` 开始迁移导入链。
4. 工作包 E、F、G、H 可以基于 `RpgAgent*``RpgWorld*``rpg*` 契约骨架继续拆分,而不需要再回头统一首轮命名。

View File

@@ -1,106 +0,0 @@
# 创作流程链路重构工作包 B 完成记录
更新时间:`2026-04-21`
## 1. 本轮目标
工作包 B 聚焦前端平台壳层与流程编排拆分,本轮目标是把平台壳层从“大编排文件”收口成“页面壳层 + 独立 hooks / coordinator”
1. `PreGameSelectionFlow.tsx` 退化为兼容入口。
2. `RpgCreationShellImpl.tsx` 只保留 stage 切换、组件装配、视觉级 loading / error。
3. 平台 bootstrap、session controller、operation polling、detail navigation、result autosave、enter-world 逻辑全部迁入 `src/components/game-shell/rpg-creation-flow/` 新目录。
4. 保证现有交互测试继续通过,不引入主链行为回退。
---
## 2. 已完成内容
### 2.1 旧入口已退化为兼容层
`src/components/game-shell/PreGameSelectionFlow.tsx` 现在只保留:
1. 旧类型导出兼容:`PreGameSelectionFlowProps``SelectionStage`
2. 旧组件名兼容:`PreGameSelectionFlow`
3. 对新实现 `RpgCreationShellImpl` 的桥接
这样现有调用方和测试仍可继续走旧路径,不会因为命名迁移立即破坏主链。
### 2.2 新目录已承接真实实现与流程 hooks
已新增或更新以下文件:
1. `src/components/game-shell/rpg-creation-flow/RpgCreationShellImpl.tsx`
2. `src/components/game-shell/rpg-creation-flow/RpgCreationShell.tsx`
3. `src/components/game-shell/rpg-creation-flow/index.ts`
4. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowTypes.ts`
5. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowShared.ts`
6. `src/components/game-shell/rpg-creation-flow/useRpgCreationPlatformBootstrap.ts`
7. `src/components/game-shell/rpg-creation-flow/useRpgCreationSessionController.ts`
8. `src/components/game-shell/rpg-creation-flow/useRpgCreationAgentOperationPolling.ts`
9. `src/components/game-shell/rpg-creation-flow/useRpgCreationDetailNavigation.ts`
10. `src/components/game-shell/rpg-creation-flow/useRpgCreationResultAutosave.ts`
11. `src/components/game-shell/rpg-creation-flow/useRpgCreationEnterWorld.ts`
其中:
1. `RpgCreationShell.tsx` 已不再桥接旧 `PreGameSelectionFlow`,而是直接桥接 `RpgCreationShellImpl.tsx`
2. `index.ts` 已开始从新目录导出 `SelectionStage`,为后续调用迁移准备统一出口。
### 2.3 平台编排已全部拆入独立 coordinator
本轮已经把原 `PreGameSelectionFlow` / `RpgCreationShellImpl` 中的主链编排拆到以下 hook
1. `useRpgCreationPlatformBootstrap.ts`
- 平台首页 works / library / gallery / history / save / dashboard 拉取
- 浏览历史写入与存档恢复
2. `useRpgCreationSessionController.ts`
- Agent session 创建 / 恢复
- 消息流、action 执行、草稿生成态与结果页自动打开
3. `useRpgCreationAgentOperationPolling.ts`
- Agent operation 轮询
- 完成态 session 刷新与失败兜底
4. `useRpgCreationDetailNavigation.ts`
- 作品详情、创作作品恢复、草稿结果页打开
- 详情页发布 / 下架 / 删除
5. `useRpgCreationResultAutosave.ts`
- 结果页自动保存
- `sync_result_profile` 协调
- 保存签名去重与延时保存
6. `useRpgCreationEnterWorld.ts`
- 进入世界前的最终草稿同步
当前 `RpgCreationShellImpl.tsx` 只保留:
1. hooks 组合
2. stage 级视图切换
3. 组件 props 装配
4. 视觉级 loading / error 展示
---
## 3. 当前状态判断
工作包 B 已达到执行方案中的验收口径:
1. `PreGameSelectionFlow.tsx` 只剩兼容导出与新壳层桥接。
2. `RpgCreationShellImpl.tsx` 不再直接持有平台请求编排、operation 轮询、自动保存或进入世界同步细节。
3. 平台侧主链已经切成壳层 + hooks / coordinator。
4. 现有 `PreGameSelectionFlow.agent.interaction.test.tsx` 的 14 个场景全部通过。
---
## 4. 本轮刻意未做
1. 还没有物理删除 `PreGameSelectionFlow.tsx`,当前继续保留旧入口兼容层,避免影响并行工作包的调用路径。
2. 还没有让所有调用方统一显式改走 `RpgCreationShell` 新入口,当前仍允许旧入口桥接到新壳层。
3. 还没有把结果页 preview 数据源从前端兼容 adapter 切到服务端正式 preview contract当前仍使用 `rpgCreationPreviewAdapter` 作为阶段性兼容层,这属于后续工作包 G / H 与 Phase 3 范围。
4. 还没有清理所有 legacy 兼容导出与 façade当前优先稳定主链与测试口径。
---
## 5. 验证结果
1. `npx eslint "src/components/game-shell/PreGameSelectionFlow.tsx" "src/components/game-shell/rpg-creation-flow/*.ts" "src/components/game-shell/rpg-creation-flow/*.tsx"`
2. `npx vitest run src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
3. `npm run check:encoding`
以上检查在本轮修改后均已通过。

View File

@@ -1,106 +0,0 @@
# 创作链路重构工作包 D 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 D前端 custom world client 收口**,约束如下:
1. 把创作链主路径依赖的 custom world 请求从 `aiService.ts``storageService.ts` 中迁入 `src/services/rpg-creation/`
2. 首轮允许旧 service 兼容导出,追加清理轮必须删除已无调用方的旧命名导出。
3. 不改后端接口语义,不扩写结果页 UI 逻辑,不借机重构工作包 B / C 的内部状态编排。
## 2. 本次已落地内容
## 2.1 RPG 创作域请求基座已独立
已新增以下请求基座文件:
1. `src/services/rpg-creation/rpgCreationRuntimeClient.ts`
2. `src/services/rpg-creation/rpgCreationRequestHelpers.ts`
当前策略:
1. runtime 读写重试策略不再散落在 `storageService.ts` 内部,而是作为 RPG 创作域专属 runtime client 复用。
2. Agent SSE、POST JSON 请求辅助能力收口到 `rpgCreationRequestHelpers.ts`,避免再把流式解析细节写回通用 service。
## 2.2 五类 rpgCreation client 已持有真实请求实现
以下 client 已不再桥接旧 service而是直接持有真实网络实现
1. `src/services/rpg-creation/rpgCreationAgentClient.ts`
2. `src/services/rpg-creation/rpgCreationWorkClient.ts`
3. `src/services/rpg-creation/rpgCreationLibraryClient.ts`
4. `src/services/rpg-creation/rpgCreationAssetClient.ts`
5. `src/services/rpg-creation/rpgCreationGenerationClient.ts`
本轮已完成的具体收口:
1. Agent session 创建、读取、消息发送、消息流、action 执行、operation 查询、card detail 查询已经正式迁入 `rpgCreationAgentClient.ts`
2. works 列表查询已经正式迁入 `rpgCreationWorkClient.ts`
3. library / publish / unpublish / gallery / gallery detail 已经正式迁入 `rpgCreationLibraryClient.ts`
4. 结果页与编辑器依赖的场景图、场景 NPC、可扮演角色、场景角色、场景生成请求已经正式迁入 `rpgCreationAssetClient.ts`
5. `generateCustomWorldProfile()` 已正式迁入 `rpgCreationGenerationClient.ts`,世界生成入口也已进入 RPG 创作域 client。
6. `src/services/rpg-creation/index.ts` 已收口为 RPG 命名导出,创作主链不再从 barrel 暴露 `createCustomWorldAgentSession / listCustomWorldWorks / upsertCustomWorldProfile` 等旧命名入口。
## 2.3 旧 service 兼容导出已删除
追加清理轮已完成以下删除:
1. `src/services/aiService.ts` 不再 re-export RPG 创作 Agent / works / 结果页生成接口,继续只服务 story/chat 等通用 AI 运行时能力。
2. `src/services/storageService.ts` 已物理删除,运行时存档、设置、资料、浏览历史能力已迁入 `src/services/rpg-entry/``src/services/rpg-runtime/`
3. `rpgCreationAgentClient.ts``rpgCreationWorkClient.ts``rpgCreationLibraryClient.ts``rpgCreationAssetClient.ts` 已删除 `CustomWorld*` 兼容具名导出,只保留 `Rpg*` 主命名。
4. 源码扫描已确认不再存在 `createCustomWorldAgentSession / executeCustomWorldAgentAction / listCustomWorldWorks / upsertCustomWorldProfile` 等旧主链函数引用。
## 2.4 主链调用已开始直接使用 RPG 创作域 client
本轮已把以下主链入口切到 `src/services/rpg-creation/`
1. `src/components/rpg-entry/useRpgCreationSessionController.ts`
2. `src/components/rpg-entry/useRpgCreationResultAutosave.ts`
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
4. `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx`
5. 新增世界生成入口 `generateRpgWorldProfile()` 通过 `src/services/rpg-creation/` barrel 暴露,后续新代码不必再从旧 `aiService.ts` 进入。
配套收口:
1. 结果页与编辑器相关测试 mock 已改到 `rpgCreationAssetClient`,不再盯住 `aiService.ts` 的兼容层。
2. `CustomWorldResultView.test.tsx``CustomWorldEntityEditorModal.test.tsx` 已改为直接消费 `RpgCreationResultView / RpgCreationEntityEditorModal` 新入口,不再通过旧组件 façade。
## 2.5 本轮验证结果
已完成以下针对性验证:
1. `npm run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts src/services/storageService.test.ts src/components/CustomWorldEntityEditorModal.test.tsx src/components/CustomWorldResultView.test.tsx src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
2. `npm run check:encoding`
验证结果:
1. 上述 5 组定向测试全部通过。
2. 编码检查通过,未写坏中文文件。
追加清理轮已完成以下验证:
1. `npm run test -- src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts src/components/CustomWorldResultView.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
结果:通过,`42` 项测试全部通过。
2. `npm run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts`
结果:通过,`2` 项测试全部通过。
3. `npm run check:encoding`
结果:通过,`1929` 个文件编码检查通过。
4. 源码扫描确认 `src / packages / server-node` 中不再存在本轮删除的旧主链函数与旧组件入口符号引用。
## 3. 本次刻意未做的事
以下内容明确留给后续工作包,不在本轮越界处理:
1. 没有改后端 works/library/gallery/agent route 的语义与 contract。
2. 没有拆 `PreGameSelectionFlow.tsx` 内部编排;这部分仍属于工作包 B。
3. 没有继续物理拆散 `RpgCreationEntityEditorShared.tsx`;这部分仍属于工作包 C 后续细拆。
4. 没有强行重命名历史数据结构类型,例如 `CustomWorldProfile` 与 runtime contract response 名称;这些仍是现有契约类型,不等同于旧脚本依赖。
5. 没有删除旧 `src/services/ai.ts` 中的 legacy 世界生成实现;它已不在当前 RPG 创作主链 client 上,后续应按独立 dead code 批次评估。
## 4. 对后续工作包的直接收益
1. 工作包 B 后续拆平台壳层时,可以直接从 `src/services/rpg-creation/` 消费 Agent / works / library / gallery 请求,不必继续回到旧 service 文件找接口。
2. 工作包 C 后续继续拆结果页和编辑器时,资产生成请求已经有稳定的 RPG 创作域入口。
3. 后续清理 `aiService.ts``storageService.ts` 时,创作链主路径已经完成真实迁出,不会再被“通用 service 同时承载创作域请求”拖住。

View File

@@ -1,150 +0,0 @@
# 创作链路重构工作包 E 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 E后端 Agent 编排拆分**,并严格遵守这一轮的写入边界:
1. 只改后端应用服务层,不动前端壳层。
2. 先把 `customWorldAgentOrchestrator.ts` 从“大分支调度 + 派生状态重建 + 结果回写细节”里拆薄。
3. 补齐 action executor 真实落点与 `supportedActions` 主链字段,但不在这一轮顺手重构 session store 和 runtime compiler。
## 2. 本次已落地内容
## 2.1 orchestrator 已退化为应用服务 façade
本轮后,`server-node/src/services/customWorldAgentOrchestrator.ts` 的职责开始收口为:
1. session 级入口方法保留。
2. 创建 operation 记录。
3. 调用 action registry 拿到执行计划。
4. 把消息轮转、foundation 生成、实体生成、角色资产同步等主链事务串起来。
这轮明确移出的内容:
1. `action -> executor` 的分支校验和分发。
2. `sync_result_profile` 的字段回写细节。
3. 多个 action 共用的 draftCards / assetCoverage / suggestedActions / qualityFindings 派生重建逻辑。
## 2.2 已新增 action registry 与 executor 目录,并完成真实执行迁移
已新增:
1. `server-node/src/services/customWorldAgentActionRegistry.ts`
2. `server-node/src/services/customWorldAgentActionExecutors/index.ts`
3. `server-node/src/services/customWorldAgentActionExecutors/types.ts`
本轮收口结果:
1. registry 统一处理 `draft_foundation``update_draft_card``sync_result_profile``generate_characters``generate_landmarks``generate_role_assets``sync_role_assets` 的可用性校验。
2. `publish_world``generate_scene_assets``sync_scene_assets``expand_long_tail``revert_checkpoint` 已完成真实 executor 装配,不再只是 registry 层面的“已声明但未开放”动作。
3. `lock_cards``unlock_cards``regenerate_scope` 仍统一通过 registry 返回禁用原因,不再继续堆在 orchestrator 分支里。
4. `customWorldAgentActionExecutors/` 已补 `draftFoundationExecutor.ts``updateDraftCardExecutor.ts``syncResultProfileExecutor.ts``generateCharactersExecutor.ts``generateLandmarksExecutor.ts``generateRoleAssetsExecutor.ts``syncRoleAssetsExecutor.ts``generateSceneAssetsExecutor.ts``syncSceneAssetsExecutor.ts``expandLongTailExecutor.ts``publishWorldExecutor.ts``revertCheckpointExecutor.ts`,真实 action 执行已从 orchestrator 物理迁入目录。
5. `customWorldAgentActionExecutors/helpers.ts``executorShared.ts` 已收口 action_result / summary message 构造、operation 更新和 session 读取共用逻辑,避免 executor 间重复堆样板代码。
## 2.3 已新增 message turn / suggested action / snapshot / quality gate / result sync service
已新增:
1. `server-node/src/services/customWorldAgentMessageTurnService.ts`
1. `server-node/src/services/customWorldAgentSuggestedActionService.ts`
2. `server-node/src/services/customWorldAgentSnapshotBuilder.ts`
3. `server-node/src/services/customWorldAgentQualityGateService.ts`
4. `server-node/src/services/customWorldAgentResultSyncService.ts`
本轮收口结果:
1. `CustomWorldAgentMessageTurnService` 已接管 session 初始派生状态与 message turn 的真实执行,`customWorldAgentOrchestrator.ts` 只保留 façade 委托。
1. `CustomWorldAgentSuggestedActionService` 统一维护 `foundation_review``object_refining``visual_refining` 的建议动作生成,不再散落在 orchestrator 和 session compatibility。
2. `CustomWorldAgentSnapshotBuilder` 统一承接 message turn、foundation draft、结果页回写、角色/地点追加、角色资产同步后的派生字段重建。
3. `CustomWorldAgentQualityGateService` 已形成独立 finding 入口,当前先输出角色缺失、地点缺失、玩家目标缺失、角色资产待补齐、场景资产待补齐等基础 gate finding。
4. `CustomWorldAgentResultSyncService` 接管了 `sync_result_profile` 的字段回写细节,明确这一轮只允许“摘要 + 资产确认结果 + legacyResultProfile 快照”回写进 draft profile。
## 2.4 `supportedActions` 已接入 session snapshot 主链
这一轮已把 registry 产出的能力矩阵正式装配到 `CustomWorldAgentSessionSnapshot.supportedActions`
1. `createSession``getSessionSnapshot`、stream message 完成态、各 action 完成后的 session 拉取都会返回真实 `supportedActions`
2. `supportedActions` 的启用状态按 session 当前阶段与草稿可用性计算,不再由前端根据 action 字面量自行猜测。
3. 具体 payload 校验仍保留在 action 执行阶段,能力矩阵只表达“当前阶段是否允许发起这类动作”。
## 2.5 action 主链行为保持不变,但派生状态已开始统一
这一轮没有改变现有 action contract也没有新增前端依赖字段但已经把以下重复派生逻辑统一改走 snapshot builder
1. `draft_foundation`
2. `update_draft_card`
3. `sync_result_profile`
4. `generate_characters`
5. `generate_landmarks`
6. `generate_role_assets`
7. `sync_role_assets`
8. message turn 结束后的 stage / suggested actions / quality findings / asset coverage 重建
这意味着:
1. 后续新增 action 时,不必再复制一整段 `draftCards + assetCoverage + suggestedActions + recommendedReplies` patch 拼装代码。
2. `qualityFindings` 已开始成为真实后端派生字段,而不只是 session store 中的空占位。
3. `sync_result_profile` 的边界已经能单独测试和继续收缩。
## 2.6 工作包 E 第三轮已补齐的真实闭环
本轮把工作包 E 前两轮遗留的 5 个动作补成了真实后端闭环:
1. `generate_scene_assets`
已通过 `CustomWorldAgentAssetBridgeService.buildSceneAssetStudioContext()` 打通场景图工坊上下文准备,支持营地与地点单场景进入。
2. `sync_scene_assets`
已通过 `applySceneAssetPublishResult()` 写回营地/地点正式场景图,并同步刷新对应 `sceneChapters[].acts` 的背景图与背景资产 ID。
3. `expand_long_tail`
已接入实体生成服务与 snapshot builder能真实追加长尾角色、地点并把阶段推进到 `long_tail_review`
4. `publish_world`
已改为走 `CustomWorldAgentPublishingService + RpgWorldProfileRepository` 主链,正式把 draft session 编译、写入并发布到作品库。
5. `revert_checkpoint`
已依赖 checkpoint snapshot 元数据与 `restoreCheckpoint()` 主链完成真实回滚,不再只是开放 action 名称。
这一轮同时补齐了 4 个关键收口:
1. 发布链已经统一改走 `CustomWorldAgentPublishingService``customWorldAgentOrchestrator.ts``customWorldAgentActionExecutors/index.ts``publishWorldExecutor.ts``server.ts` 的注入口径已经对齐;作者展示名优先走 `resolveAuthorDisplayName`,同时保留 `userRepository` 兼容兜底。
2. `publish_world` 的 readiness 与正式发布已经收口到同一个服务,`profileId` 固定优先沿用 legacy 结果页 ID否则回退为 `agent-draft-${sessionId}`,避免发布产物继续使用临时时间戳。
3. `buildCheckpointSnapshot()` 已接入 `draft_foundation``update_draft_card``sync_result_profile``generate_characters``generate_landmarks``sync_role_assets``sync_scene_assets``expand_long_tail``publish_world` 等关键 executorcheckpoint 现在保存的是真正可恢复的派生快照,而不是只记一段残缺 patch。
4. `rebuildRoleAssetCoverage()` 已补营地 / 地点正式场景资产 fallback 汇总,并收口为“只有真实正式场景图已存在时才补 standalone summary”这样 `sync_scene_assets` 写回后的 camp/landmark asset coverage 在 snapshot 重建、works 读模型与 checkpoint 回放里都不会丢失,也不会误伤 phase3 自动资产回归。
## 2.7 本轮验证结果
已完成以下验证:
1. `npm --prefix server-node run build`
2. `npm --prefix server-node run test -- customWorldAgentPhase3.test.ts customWorldAgentActionRegistry.test.ts customWorldAgentPhase5.test.ts`
本轮重点关注的回归范围:
1. `customWorldAgentActionRegistry.test.ts`
2. `customWorldAgentPhase3.test.ts`
3. `customWorldAgentPhase5.test.ts`
4. `publish_world`
5. `generate_scene_assets / sync_scene_assets`
6. `expand_long_tail`
7. `revert_checkpoint`
验证结果:
1. `server-node` 构建通过。
2. 定向回归通过,共 `208` 项测试全部通过。
3. Phase 3 与 Phase 5 已同时确认通过,说明这轮对 `sceneAssets` fallback summary 的收口没有打坏前序自动资产链。
## 3. 本次刻意未做的事
以下内容明确留给后续工作包或下一轮工作包 E不在本轮越界处理
1. 还没有进入 Phase 4 的“进入世界统一走发布态 gate”收口当前这轮只完成了发布动作本身的后端闭环。
2. 还没有改 `customWorldAgentSessionStore.ts` 内部 compatibility / snapshot 输出结构,这部分仍属于工作包 F。
3. 还没有把 result preview 正式接到 `resultPreview` 主链字段,这部分仍需要和工作包 G / H 协作。
4.`customWorldAgentPublishGateService.ts``customWorldAgentPublishService.ts` 仍作为历史兼容文件保留,但工作包 E 主链已经不再走它们;这一轮没有继续做物理删除与引用清扫,避免越界碰到 Phase 4/Phase 5 之外的兼容入口。
## 4. 对后续工作包的直接收益
1. 工作包 F 可以在不碰 orchestrator 大分支的前提下,继续拆 session/store/repository。
2. 工作包 G 可以直接围绕 `CustomWorldAgentResultSyncService``CustomWorldAgentQualityGateService` 对接服务端 preview compiler 与 publish gate。
3. 工作包 H 可以基于已落地的 `supportedActions`、action registry 和 quality gate 继续推进 preview contract 与 contract tests。
4. 后续继续拆 action executor 时,已经有 `customWorldAgentActionExecutors/` 目录和注册表,不需要再回到 orchestrator 里重新铺路。

View File

@@ -1,92 +0,0 @@
# 创作链路重构工作包 F 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 F后端 session/store/repository 拆分**,约束如下:
1. 不改动现有主链接口与行为语义。
2. 保留 `customWorldAgentSessionStore.ts``runtimeRepository.ts``customWorldWorkSummaryService.ts` 作为兼容 façade。
3. 把 session 兼容补齐、session 持久化、profile 持久化、works 读模型组装从大文件中物理拆出。
## 2. 本次已落地内容
## 2.1 session store 内部分层
已新增以下 RPG Agent session 拆分文件:
1. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionRecord.ts`
2. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionCompatibility.ts`
3. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionFactory.ts`
4. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionRepositoryAdapter.ts`
当前策略:
1. `customWorldAgentSessionStore.ts` 继续保留旧类名和旧方法签名。
2. sessionId 前缀、snapshot 输出结构、operation/checkpoint 写入语义保持兼容。
3. 旧 session 的兼容补齐逻辑集中收口到 `rpgAgentSessionCompatibility.ts`,不再继续堆在 store 主文件里。
4. `customWorldAgentSessionStore.ts` 已正式改为依赖 `RpgAgentSessionRepositoryPort`phase2~5 与 works 集成测试也已切到新的 session 仓储端口。
## 2.2 custom world 仓储从 runtime 大仓储中拆出
已新增以下 RPG 世界仓储文件:
1. `server-node/src/repositories/RpgAgentSessionRepository.ts`
2. `server-node/src/repositories/RpgWorldProfileRepository.ts`
3. `server-node/src/repositories/rpgWorldRepositoryShared.ts`
当前策略:
1. `RuntimeRepositoryPort` 继续保留兼容 façade`context.ts``server.ts``runtimeRoutes.ts`、同步脚本已开始直接注入并使用 `RpgAgentSessionRepository``RpgWorldProfileRepository`
2. `runtimeRepository.ts` 内的 custom world session/profile/gallery SQL 已改成委托新仓储。
3. `runtimeRepository.ts` 继续只保留 runtime 快照、设置、浏览历史、档案等通用能力,以及少量尚未迁走的快照同步编排。
## 2.3 works 读模型拆分
已新增以下 works 读模型相关文件:
1. `server-node/src/services/RpgWorldWorkCoverResolver.ts`
2. `server-node/src/services/RpgWorldWorkSummaryAssembler.ts`
3. `server-node/src/services/RpgWorldWorkSummaryService.ts`
并将:
1. `server-node/src/services/customWorldWorkSummaryService.ts`
退化为兼容入口,仅负责桥接新 `RpgWorldWorkSummaryService`
当前策略:
1. works service 只保留服务入口,不再内嵌标题、摘要、封面、资产覆盖率等全部组装细节。
2. 草稿封面与发布态封面解析统一走 resolver避免后续重复理解封面规则。
3. 草稿态与发布态 work summary 的字段语义保持不变,继续支持“继续创作”和“进入世界”入口判定。
4. `runtimeRoutes.ts` 中的 works/library/gallery 路由已切到 `rpgWorldWorkSummaryService``rpgWorldProfileRepository` 直接注入,不再经由 `runtimeRepository` 中转 custom world 读模型。
## 3. 验证结果
本次已完成以下定向回归:
1. 运行 `node --test --test-concurrency=1 --import tsx src/services/customWorldAgentPhase2.test.ts src/services/customWorldAgentPhase3.test.ts src/services/customWorldAgentPhase4.test.ts src/services/customWorldAgentPhase5.test.ts src/services/customWorldWorkSummaryService.integration.test.ts`
2. 以上 21 个 custom world / agent / works 相关测试全部通过。
同时确认:
1. 全量 `npx tsc -p server-node/tsconfig.json --noEmit` 当前仍被仓库里既有的跨模块类型问题阻塞。
2. 这些全量类型错误大多与本工作包无关,因此本轮仍以 custom world 定向测试通过作为主验证口径。
3. 工作包 F 本轮新增的 `RpgWorldWorkSummaryService.ts`、新仓储注入链和测试 helper未在定向回归中引入新的行为回归。
## 4. 当前兼容保留项
以下内容属于阶段性兼容保留,不再视为工作包 F 未完成项:
1. `RuntimeRepositoryPort` 仍保留 custom world 相关兼容方法,避免一次性冲击 story/runtime 其他调用方。
2. `customWorldAgentSessionStore.ts``customWorldWorkSummaryService.ts` 仍保留旧文件名 façade后续统一命名治理时再清理。
3. runtime 快照同步与 custom world profile 自动回写的进一步解耦,仍留待后续围绕 `runtimeRepository.ts` 继续收口。
## 5. 对后续工作包的直接收益
1. 工作包 E 可以在不继续挤压 `customWorldAgentSessionStore.ts` 的情况下,把 orchestrator 的 result sync / snapshot builder 接到更清晰的 session 持久化边界。
2. 工作包 G 后续若需要让 preview compiler / publish gate 落库,不必再继续往 `runtimeRepository.ts` 堆 custom world SQL。
3. 工作包 H 已能直接围绕 `rpg-agent-session-store/``RpgWorldWorkSummaryAssembler.ts``RpgWorldWorkSummaryService.ts` 与新仓储端口补充更细粒度回归,而不必穿透大文件。
4. 后续若继续拆 route 命名或清理旧 façade已有 `context -> server -> runtimeRoutes -> script -> tests` 的新仓储注入链可直接复用。

View File

@@ -1,92 +0,0 @@
# 创作链路重构工作包 G 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 G后端 preview compiler 与 runtime profile 目录化**,并把目录化拆分推进到文档目标结构:
1. 先把 `runtimeProfile.ts` 退化成兼容 façade。
2.`runtime-profile/` 真正拆成 `normalize/build/schema/creatorIntentBridge` 等独立模块。
3. 把服务端 result preview compiler 从 foundation draft 流程中抽出独立入口。
4. 不直接改路由层,不直接接前端结果页。
## 2. 本次已落地内容
## 2.1 runtime profile 已完成目录化完整拆分
已完成以下结构调整:
1. 新增 `server-node/src/modules/custom-world/runtime-profile/index.ts` 作为目录入口。
2.`server-node/src/modules/custom-world/runtimeProfile.ts` 已退化为兼容 façade只负责 re-export。
3. `server-node/src/modules/custom-world/runtime-profile/runtimeProfileCompiler.ts` 已退化为兼容 façade不再承载主实现。
4. 已新增并落地以下目标模块:
1. `normalizeShared.ts`
2. `normalizeRole.ts`
3. `normalizeLandmark.ts`
4. `normalizeSceneChapter.ts`
5. `normalizeCamp.ts`
6. `buildCompiledProfile.ts`
7. `buildAttributeSchema.ts`
8. `creatorIntentBridge.ts`
当前策略:
1. 先保证旧导入路径不失效,避免放大工作包 G 首轮改动范围。
2. 新代码优先改走 `runtime-profile/` 目录入口。
3. `runtimeProfile.ts``runtimeProfileCompiler.ts` 后续只允许继续收缩,不再接受新增主逻辑。
## 2.2 服务端 preview compiler 已从 foundation draft 流程中抽出
已完成以下收口:
1. `server-node/src/services/RpgWorldPreviewCompiler.ts` 不再只是别名导出,已提供:
1. `buildRpgWorldPreviewProfile()`
2. `normalizeRpgWorldPreviewProfile()`
3. `buildRpgWorldPreviewEnvelope()`
4. `normalizeRpgWorldPreviewEnvelope()`
2. `packages/shared/src/contracts/rpgCreationPreview.ts` 已补 `RpgCreationPreviewSource`,把 preview 来源语义显式化。
3. `customWorldAgentFoundationDraftService.ts` 已把 LLM foundation draft 主生成链改成“直接组装 foundation draft + 单独保留 `legacyResultProfile` 兼容快照”,不再通过 preview compiler 反解草稿主字段。
这轮的边界变化是:
1. foundation draft 主字段已经不再依赖“先编 legacy runtime profile再转回 draft”的双重编译。
2. `legacyResultProfile` 仍保留,但只作为结果页兼容快照,不再主导 foundation draft 生成。
3. “服务端 preview 编译入口”继续独立存在,并在 Phase 5 后补上 `rpgCreationPreviewProfileBuilder.ts`,统一承接 preview 与 publish 的兼容合并规则。
4. preview source 已在 Phase 5 后正式收口为 `session_preview`,不再继续沿用兼容期的 `legacy_custom_world_profile` 标记。
## 2.3 已补最小测试与目录化回归验证
本次新增:
1. `server-node/src/services/RpgWorldPreviewCompiler.test.ts`
2. `server-node/src/services/customWorldAgentFoundationDraftService.test.ts`
当前覆盖重点:
1. 验证 preview compiler 可以输出服务端兼容预览 envelope。
2. 验证 envelope 的 `source` 保持为 `session_preview`
3. 验证 preview profile 仍保留 runtime 编译生成的关键字段,例如 `scenarioPackId``campaignPackId`
4. 验证 Phase 5 新增的 preview builder 可以在服务端保留 `legacyResultProfile` 富字段并合并最新草稿资产。
5. 验证 foundation draft service 的 LLM 路径已经直接生成 draft 主字段,不再依赖 preview compiler 反解。
6. 验证 `runtimeProfile.ts` façade 在目录化拆分后仍保持旧调用兼容。
本轮额外验证已通过:
1. `npm run check:encoding`
2. `node --test --test-concurrency=1 --import tsx server-node/src/services/customWorldAgentFoundationDraftService.test.ts server-node/src/modules/custom-world/runtimeProfile.test.ts server-node/src/services/RpgWorldPreviewCompiler.test.ts`
## 3. 本次刻意没有做的事
以下内容仍留给后续阶段:
1. 还没有让 `RpgWorldPreviewCompiler` 输出真正独立于 legacy profile 的 preview view model。
2. 还没有把 `RpgWorldPreviewCompiler` 的 preview 载体从当前 runtime-profile 兼容对象升级成真正独立的 preview view model。
3. `legacyResultProfile` 仍保留为兼容快照,结果页与自动保存链还没有完全脱离 legacy profile 富字段。
4. 还没有删除 `runtimeProfile.ts``runtimeProfileCompiler.ts` 这两个兼容 façade。
## 4. 对后续工作包的直接收益
1. 工作包 E 可以围绕 `RpgWorldPreviewCompiler` 继续补 result sync / snapshot builder 的 preview 接口。
2. 工作包 H 可以基于 `RpgCreationPreviewEnvelope` 继续细化正式 preview contract 和 contract tests。
3. Phase 3 把结果页切到服务端 preview 时,已经有稳定的后端编译入口和目录化 normalize/build 模块,不需要再回头拆 `runtimeProfile.ts` 大文件。

View File

@@ -1,137 +0,0 @@
# 创作链路重构工作包 H 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 H共享契约与测试基建**,约束如下:
1. 把 RPG 创作域共享契约从“类型别名骨架”推进到“真实定义 + 兼容出口”。
2. 补齐可复用的 fixture避免前后端测试继续各自复制一套假数据。
3. 补齐 unit / contract / integration / regression 最小闭环,不越界重构 UI、路由和仓储主逻辑。
## 2. 本次已落地内容
## 2.1 共享契约已完成物理拆分与兼容收口
本轮已把以下文件从工作包 A 的骨架态推进为真实定义:
1. `packages/shared/src/contracts/rpgAgentAnchors.ts`
2. `packages/shared/src/contracts/rpgAgentDraft.ts`
3. `packages/shared/src/contracts/rpgAgentActions.ts`
4. `packages/shared/src/contracts/rpgAgentSession.ts`
5. `packages/shared/src/contracts/rpgCreationPreview.ts`
6. `packages/shared/src/contracts/rpgCreationWorkSummary.ts`
本轮收口重点:
1. `rpgAgent*``rpgCreation*` 文件不再只是从旧 `customWorldAgent.ts` 做类型别名转发,而是承载真实契约定义。
2. `rpgAgentSession.ts` 已显式加入 `supportedActions?``resultPreview?` 可选字段,为工作包 E/G 后续正式接入 registry 与服务端 preview compiler 预留稳定契约入口。
3. `rpgCreationPreview.ts` 已补 `source / generatedAt / qualityFindings / blockers`,把“预览载体”和“预览来源/质量门槛”拆开。
4. `rpgCreationWorkSummary.ts` 已收口 works 列表稳定字段,明确 `canResume / canEnterWorld` 的读模型语义。
## 2.2 旧 `customWorld*` 契约已补齐兼容分文件
本轮没有直接删除旧入口,而是把旧命名收口成“聚合出口 + 分文件兼容层”:
1. 当前旧 `customWorldAgent.ts` 不再承载主定义,而是统一聚合:
- `customWorldAgentAnchors.ts`
- `customWorldAgentDraft.ts`
- `customWorldAgentActions.ts`
- `customWorldAgentSession.ts`
- `customWorldResultPreview.ts`
- `customWorldWorkSummary.ts`
2. 现有前后端直接导入 `customWorldAgent.ts` 的代码不需要在本轮一起大改,避免把工作包 H 扩成全仓导入迁移。
3. 后续工作包可以逐步把新代码改到 `rpgAgent* / rpgCreation*` 路径;如果暂时仍需旧命名,也可以先切到更细的兼容分文件,而不是继续依赖单一大聚合文件。
## 2.3 已补共享 fixture总线样本开始统一
本轮新增:
1. `packages/shared/src/contracts/rpgCreationFixtures.ts`
当前已提供并复用的样本包括:
1. 八锚点 fixture
2. foundation draft fixture
3. session snapshot fixture
4. preview envelope fixture
5. published profile fixture
6. library entry fixture
7. works response fixture
这些样本的作用是:
1. 前端 contract test、后端 integration test、后续 preview/compiler 回归可以共用同一批样本。
2. 避免继续在各测试文件里手写不一致的 session/profile/works 假数据。
3. 把工作包 H 文档中要求的“最小 eight-anchor / preview / published profile / works 样本”先落成统一入口。
## 2.4 已补 unit / contract / integration / regression 最小闭环
本轮新增测试:
1. `packages/shared/src/contracts/rpgContracts.test.ts`
2. `server-node/src/services/customWorldWorkSummaryService.integration.test.ts`
3. `server-node/src/services/RpgWorldPreviewCompiler.fixture.test.ts`
4. `server-node/src/services/RpgWorldWorkSummaryAssembler.fixture.test.ts`
5. `server-node/src/services/customWorldAgentActionRegistry.test.ts`
6. `server-node/src/services/customWorldAgentResultSyncService.test.ts`
同时补充:
1. `vitest.config.ts` 已把 `packages/shared/src/**/*.test.ts` 纳入前端 Vitest 测试入口。
2. shared contract test 当前覆盖:
- session fixture、preview fixture、published profile fixture、works/library fixture 对齐关系
- `supportedActions` 能力矩阵样本
- 旧命名兼容分文件的类型消费
- 角色动作资产、分幕背景、works 门槛字段不会在 fixture 演进时悄悄回退
3. server unit / regression test 当前覆盖:
- preview compiler 可以直接消费 shared fixture
- works assembler 输出与 shared works fixture 保持一致
- 角色主图、动作集、分幕背景资产字段在 normalize / assemble 后仍能保留
- action registry 的 capability enable/disable 与 payload validate/normalize
- result sync service 只回写摘要与匹配资产,不让 runtime-only 结构反向污染 foundation draft
4. server integration test 当前验证共享 fixture 可以被 `customWorldWorkSummaryService` 正常消费,并输出和共享 works 响应样本一致的草稿/发布条目。
## 2.5 根导出已补齐
本轮已把:
1. `packages/shared/src/contracts/rpgCreationFixtures.ts`
2. `packages/shared/src/contracts/customWorldAgent.ts`
接入:
1. `packages/shared/src/index.ts`
这样后续前端和后端若要消费共享 fixture 或新契约,不需要再回退到旧单文件入口。
## 3. 本次验证结果
已完成以下定向验证:
1. `npm run test -- packages/shared/src/contracts/rpgContracts.test.ts`
2. `node --test --test-concurrency=1 --import tsx src/services/customWorldAgentActionRegistry.test.ts src/services/customWorldAgentResultSyncService.test.ts src/services/customWorldWorkSummaryService.integration.test.ts src/services/RpgWorldPreviewCompiler.fixture.test.ts src/services/RpgWorldWorkSummaryAssembler.fixture.test.ts`
3. `npm run check:encoding`
验证重点:
1. shared 契约样本可直接通过 Vitest 执行。
2. preview compiler、works assembler、works service 三层都可以直接消费 shared fixture不需要额外复制一套测试数据。
3. 中文文档与代码文件经过编码检查,没有把文本写坏。
## 4. 本次刻意未做的事
以下内容明确留给后续工作包或下一轮继续推进:
1. 还没有把仓库里所有 `customWorldAgent.ts` 旧导入物理迁成 `rpgAgent* / rpgCreation*` 新导入。
2. 还没有让后端 session snapshot 真正填充 `supportedActions`
3. 还没有让服务端 preview compiler 真正把 `resultPreview` 写入主链 snapshot。
4. 没有改 UI、路由、数据库仓储或 orchestrator 主逻辑,严格控制在 shared contracts 与测试基建写入边界内。
## 5. 对后续工作包的直接收益
1. 工作包 E 可以直接复用 `supportedActions` 契约入口,把 action registry 的真实能力矩阵接进 session snapshot。
2. 工作包 G 可以直接复用 `resultPreview``RpgCreationPreviewEnvelope`,继续把服务端 preview compiler 接回主链。
3. 后续前后端测试都可以从 shared fixture 取样本,不需要继续维护多套彼此漂移的 session/profile 假数据。
4. 旧命名导入可以先切到兼容分文件,再逐步替换到 `rpg*` 新契约,迁移路径更平滑。

View File

@@ -1,63 +0,0 @@
# 创作中心作品卡操作入口落地说明
日期:`2026-04-22`
## 1. 本次目标
创作中心作品卡需要补齐两个直接操作入口:
1. **体验**:对已经满足运行条件的作品,直接从卡片启动对应玩法,不再必须先进详情页。
2. **删除**:对已有正式删除契约的 RPG 已发布作品,直接从卡片删除并刷新创作中心。
## 2. 操作语义
| 作品类型 | 状态 | 主按钮 | 体验入口 | 删除入口 |
| --- | --- | --- | --- | --- |
| RPG Agent 草稿 | `draft` | `继续创作` / `继续完善` | 不展示,草稿需要先走发布链 | 不展示,本轮不新增 Agent session 物理删除 |
| RPG 已发布作品 | `published``canEnterWorld=true` | `查看详情` | 展示 `体验`,直接调用现有进入世界链 | 展示 `删除`,走 owner-only 软删除 |
| Big Fish 草稿 | `draft` | `继续创作` | 不展示,草稿需要先回到聊天或结果页继续完善 | 不展示,本轮不新增 Big Fish 草稿删除 |
| Big Fish 已发布作品 | `published` | `查看详情` | 展示 `体验`,直接调用现有 Big Fish 运行态 | 不展示,本轮不新增 Big Fish 删除契约 |
| 拼图草稿 | `draft` | `继续创作` | 不展示 | 不展示,本轮不新增拼图删除契约 |
| 拼图已发布作品 | `published` | `查看详情` | 展示 `体验`,直接调用 `startPuzzleRun` | 不展示,本轮不新增拼图删除契约 |
## 3. 后端边界
RPG 删除必须继续遵守后端治理里的软删除规则:
1. `custom_world_profile` 增加 `deleted_at` 语义字段。
2. 删除时不物理删除 profile只设置 `deleted_at`、把发布态回退为 `draft`、清空 `published_at`,并删除公开 gallery projection。
3. `library / gallery detail / works` 读取默认过滤 `deleted_at != null` 的作品。
4. 重复删除同一 profile 保持幂等,返回当前可见作品列表。
## 4. 前端边界
1. 卡片只做表现和动作分发,不在前端拼删除逻辑。
2. 删除前使用浏览器确认,避免移动端误触。
3. 卡片按钮移动端优先换行铺开,避免小屏幕上三个按钮拥挤。
4. 不在 UI 中默认展示大段规则说明,失败信息沿用创作中心现有错误 banner。
## 5. 本轮不做
1. 不新增 Agent session 草稿删除。
2. 不新增拼图作品删除。
3. 不新增独立删除面板。
4. 不新建创作页或运行时页面,只复用现有 `CustomWorldCreationHub`、RPG 进入世界链和拼图运行时链。
5. Big Fish 草稿恢复链补齐时,只补创作中心 works 投影和恢复入口,不新建独立 Big Fish 作品系统。
## 6. 已落地结果
1. 创作中心 RPG 已发布作品卡主按钮统一调整为 `查看详情`,避免和直接进入玩法的动作混淆。
2. RPG 与拼图已发布作品卡新增独立 `体验` 入口,直接复用各自现有运行时进入链路。
3. RPG 已发布作品卡新增 `删除` 入口,调用 `/api/runtime/custom-world-library/{profile_id}``DELETE` 路由,按 owner-only 软删除规则刷新作品列表与公开广场。
4. 创作中心详情页原有删除链路继续保留,和卡片删除共用同一后端删除契约。
5. 后续拼图草稿恢复链补齐后,拼图 `draft` 卡主按钮语义收口为 `继续创作`,通过 `sourceSessionId` 恢复 Agent session而不是进入详情页。
6. 后续 Big Fish 草稿恢复链补齐后Big Fish `draft` 卡主按钮同样收口为 `继续创作`,通过 `sourceSessionId` 恢复 Agent session而不是重新创建会话。
## 7. 已验证
1. `corepack pnpm vitest run src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
2. 交互测试已覆盖:
- 创作卡点击 `查看详情` 进入详情页。
- 创作卡点击 `体验` 直接进入世界选择链路。
- 创作卡点击 `删除` 直接从作品列表移除。
- 详情页删除入口在新卡片动作语义下仍然可用。

View File

@@ -1,26 +0,0 @@
# 创作中心退出登录私有缓存清理修复 2026-04-25
## 问题
点击退出登录后,页面未刷新时仍能切到创作中心,并看到上一位登录用户的作品。刷新页面后才恢复正常。
## 根因
1. `AuthGate` 的退出动作先等待 `/api/auth/logout` 完成,再通过全局鉴权事件重新 hydrate期间前端 context 仍可能暴露旧用户。
2. 平台创作入口里的 RPG works 会在 `canReadProtectedData=false` 时清空,但大鱼吃小鱼与拼图 works 是 `PlatformEntryFlowShellImpl` 内部 state没有在退出登录时同步清空。
3. 创作 Tab 会保持挂载以降低闪烁,因此私有作品数组只要留在内存里,就会继续被货架组件渲染。
## 修复口径
1. 用户触发退出当前设备或退出全部设备时,前端必须先本地收回 `user / canAccessProtectedData`,再等待后端吊销会话。
2. `canReadProtectedData``true` 变为未登录态 `false` 时,创作中心必须清空所有私有作品缓存:
- RPG works / library 由 `useRpgEntryBootstrap` 清空。
- Big Fish works、Puzzle works 由 `PlatformEntryFlowShellImpl` 清空。
- 当前创作工作区、结果页、删除忙碌态与生成态一并复位。
3. 公开广场与分类数据不受影响,仍按匿名公开接口读取。
## 验收
1. 点击退出登录后,不刷新页面进入创作 Tab只能看到空作品货架不再出现上一账号作品。
2. 退出登录瞬间 `AuthUiContext.user``null``canAccessProtectedData=false`
3. 重新登录后按新账号重新拉取作品列表,不复用旧账号内存缓存。

View File

@@ -1,36 +0,0 @@
# 创作页移动端 UI 修复记录
日期:`2026-04-21`
## 问题定位
本轮修复只处理创作页表现层,不新增创作流程。
当前移动端问题主要来自三处:
1. 平台页在 `platformTab === 'create'` 时直接渲染 `CustomWorldCreationHub`,绕过了 `PlatformHomeView` 的移动端外壳,导致底部 Tab 栏没有挂载。
2. 创作中心内部仍混用 `pixel-*` 九宫格样式、`bg-black/*``text-white``border-white/*` 等暗色 Tailwind 类,亮色主题下会出现深色块和低对比文字。
3. 创作中心根节点自带 `h-full overflow-y-auto`,放回平台页后容易与平台页主滚动区抢滚动权,手机上会显得布局混乱。
## 落地约束
1. 创作页仍复用现有平台首页,不新增页面和新系统。
2. 移动端底部 Tab 必须始终由 `PlatformHomeView` 统一渲染,创作页只作为 `create` Tab 的内容。
3. 创作中心内部不再使用深色硬编码作为默认底色,普通卡片、筛选 Tab、空状态和按钮统一使用 `platform-*` token。
4. 创作中心不再自建整页滚动,只把内容交给平台页主滚动区,避免嵌套滚动。
5. UI 中不增加规则说明类文案,只保留必要入口、状态和作品信息。
## 编码方案
1.`PlatformHomeView` 增加可选的 `createTabContent`,让当前 Agent 创作中心接回平台页统一外壳。
2. `PreGameSelectionFlow` 不再在 `platformTab === 'create'` 时绕过 `PlatformHomeView`,而是把 `CustomWorldCreationHub` 作为创作 Tab 内容传入。
3. `CustomWorldCreationHub` 改为无内部整页滚动的内容容器,标题、返回、计数、错误、加载骨架都使用平台 token。
4. `CustomWorldCreationStartCard``CustomWorldWorkCard` 从像素暗色面板切换为平台卡片样式,保留游戏化主视觉但跟随亮暗主题。
5. `CustomWorldWorkTabs` 改用 `platform-tab`,并保持横向滚动与清晰选中态。
## 验收要点
1. 手机宽度下进入“创作”后,底部“首页 / 创作 / 存档 / 我的”Tab 始终可见。
2. 亮色主题下创作页默认卡片不出现大面积黑色底板。
3. 创作页只有平台页主内容区滚动,底部 Tab 不随作品列表滚走。
4. 桌面端仍可通过左侧平台导航进入创作页。

View File

@@ -1,63 +0,0 @@
# 创作页公开广场与 Agent 恢复指针兜底修复 2026-04-28
## 1. 问题现象
浏览器控制台同时出现两类请求错误:
1. `GET /api/runtime/custom-world/agent/sessions/:sessionId` 返回 `404`
2. `GET /api/runtime/big-fish/gallery` 返回 `400`
第一类错误发生在平台页尝试恢复 RPG / Custom World Agent 旧工作区时。旧 URL 或旧 sessionStorage 指针里可能只有 `customWorldSessionId`,没有本机保存的 `ownerUserId`,登录后前端仍会直接读取受保护 session后端按 `owner_user_id + session_id` 查不到后返回 `404`
第二类错误发生在首页读取大鱼吃小鱼公开广场时。公开广场是平台首屏的可选展示数据,即使 SpacetimeDB procedure 暂未就绪、连接短暂断开或旧环境缺少对应 procedure也不应该阻断平台主界面。
## 2. 落地原则
1. URL 中的 `customWorldSessionId` 只用于深链恢复,不作为鉴权凭据。
2. 受保护 Agent session 恢复必须能确认本机 `ownerUserId` 与当前登录用户一致。
3. 未登录状态仍保留登录弹窗流程,不提前丢弃深链;登录完成后若仍无法确认归属,再清空恢复指针。
4. Big Fish 公开广场只展示 `published` 作品;读取失败时允许空态降级,不把错误写成 UI 主错误。
5. 私有作品列表、会话详情、发布、删除仍保持严格错误,不复用公开广场的软降级策略。
## 3. 本次修改
### 3.1 RPG Agent 恢复指针
`src/services/customWorldAgentUiState.ts` 读取 URL query 时,会尝试从 sessionStorage 中匹配同一个 `activeSessionId``ownerUserId`
如果 URL 指针和本机存储匹配:
1. 返回 `activeSessionId`
2. 同时带回本机 `ownerUserId`
如果 URL 指针没有对应本机归属:
1. 只返回 session 指针。
2. 登录后 `useRpgCreationSessionController` 会清空指针。
3. 不调用 `getRpgCreationSession()`,避免向后端发起必然 404 的失效恢复请求。
### 3.2 Big Fish 公开广场
前端 `listBigFishGallery()``400 / 404` 返回 `{ items: [] }`,让平台首页可以继续渲染空广场。
Rust `api-server``list_big_fish_gallery()` 对以下 SpacetimeDB 读取问题做服务端空态降级:
1. `SpacetimeClientError::Runtime(_)`
2. `SpacetimeClientError::ConnectDropped`
3. 明确指向 `list_big_fish_works` procedure 缺失或不可用的 procedure 错误
服务端会保留 `warn` 日志,便于部署环境继续排查 schema / publish 状态。`Timeout` 不降级,仍按网关超时暴露,避免长时间卡死被误认为正常空广场。
## 4. 验收标准
1. 已登录用户打开只带旧 `customWorldSessionId`、但本机没有匹配 `ownerUserId` 的页面时,不再请求 `GET /api/runtime/custom-world/agent/sessions/:sessionId`
2. 未登录用户打开带 `customWorldSessionId` 的深链时,仍先打开登录弹窗。
3. Big Fish 公开广场返回 `400 / 404` 时,前端展示空列表,不把“读取大鱼吃小鱼广场失败”写入主错误态。
4. 服务端遇到 Big Fish gallery 可降级 SpacetimeDB 错误时返回成功 envelope`items` 为空,并记录 warn 日志。
## 5. 回归范围
1. `src/services/customWorldAgentUiState.test.ts`
2. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
3. `src/services/big-fish-gallery/bigFishGalleryClient.test.ts`
4. `cargo test -p api-server big_fish_gallery`

View File

@@ -1,32 +0,0 @@
# 创作页场景世界地图面板修复设计2026-04-25
## 背景
创作结果页进入“场景”编辑面板后,底部“查看世界地图”弹出的面板存在两个问题:
1. 面板仍使用偏运行时的深色地图容器,放在浅色创作页主题下时配色割裂,节点文字与背景层次也不稳定。
2. 地图只按传入的地标列表渲染,普通场景编辑时容易漏掉开局场景,无法形成完整“世界地图”视角。
## 落地范围
- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx`
- `src/components/CustomWorldEntityEditorModal.test.tsx`
## 设计约束
1. 不新增说明类大段 UI 文案,只保留必要的节点名、方向标签和空状态。
2. 地图面板继续作为独立弹窗,不在当前场景连接面板下方展开。
3. 地图数据必须使用当前编辑中的草稿状态:
- 普通场景编辑:开局场景 + 已保存场景列表,并用当前 `draft` 替换正在编辑的场景。
- 新增普通场景:开局场景 + 已保存场景列表 + 当前 `draft`
- 开局场景编辑:当前 `draft` 开局场景 + 已保存场景列表。
4. 地图节点要标记当前编辑场景,连接线要展示方向短标签,避免用户只能看到无语义的线。
5. 配色使用 `platform-*` 主题变量,适配浅色与深色创作页主题。
## 验收点
1. 在普通场景编辑器点击“查看世界地图”后,弹窗中能同时看到开局场景和当前场景。
2. 未保存的场景连接关系会立刻体现在地图弹窗里。
3. 当前编辑场景节点有明确高亮。
4. 地图容器和节点不再固定为深色运行时风格。
5. 相关前端测试覆盖普通场景与开局场景两条入口。

View File

@@ -1,58 +0,0 @@
# 作品货架统一 2026-04-25
## 背景
创作中心目前已经把 RPG、大鱼吃小鱼、拼图三类作品展示在同一个网格里但前端组件仍直接消费三类原始 works
1. RPG 使用 `status``title``subtitle``canEnterWorld`
2. 大鱼使用 `status``title`、资源完成度字段。
3. 拼图使用 `publicationStatus``levelName``authorDisplayName``themeTags`
这导致筛选、计数、按钮文案、卡片标题、副标题、标签、删除忙碌态都在 UI 组件里做三套判断。后续再接新作品类型时,货架组件会继续膨胀。
## 目标
1. 新增前端统一作品货架视图模型 `CreationWorkShelfItem`
2. 由归一化函数把 RPG / Big Fish / Puzzle works 映射成统一字段。
3. `CustomWorldCreationHub` 只负责筛选、空态和动作分发。
4. `CustomWorldWorkCard` 只负责渲染统一字段,不再理解三类原始 schema。
## 非目标
1. 本轮不新增后端统一 works 聚合接口。
2. 不改变三类现有 works API contract。
3. 不改变平台首页公开广场的 gallery 合并逻辑。
4. 不改变删除、体验、恢复草稿的业务规则。
## 统一字段
`CreationWorkShelfItem` 至少包含:
1. `id`:稳定货架 id。
2. `kind``rpg | big-fish | puzzle`
3. `status``draft | published`
4. `title / subtitle / summary / updatedAt`
5. `coverImageSrc / coverRenderMode / coverCharacterImageSrcs`
6. `badges`:状态、类型、阶段、标签等展示徽标。
7. `metrics`:角色数、地点数、素材完成度、游玩数等底部指标。
8. `openActionLabel`:卡片无障碍文案与主动作语义。
9. `source`:保留原始 work用于平台壳层执行动作。
## 验收
1. 创作中心三类作品仍在同一个网格展示。
2. 草稿 / 已发布筛选计数统一从 `CreationWorkShelfItem.status` 读取。
3. 卡片渲染不再直接判断 `publicationStatus` 或不同 works schema 的标题字段。
4. 统一货架按 `updatedAt` 倒序排序,兼容 ISO 字符串和 `seconds.microsZ` 后端时间文本。
5. 作品卡片以 `coverImageSrc` 作为整卡背景;若 `coverImageSrc` 为空,允许从同一作品已有的关卡图、背景图或素材图兜底,避免草稿页退回普通面板视觉。
6. 卡片不展示最后修改时间,`updatedAt` 只参与排序。
7. 现有创作中心交互测试通过。
## 2026-05-14 封面兜底补充
1. 货架视图模型仍只保存作品真实 `coverImageSrc` 或同作品真实素材兜底,不把玩法参考图写进数据模型,避免把 UI 兜底误认为作品资产。
2. `CustomWorldWorkCard``CreationWorkShelfKind``CustomWorldCoverArtwork` 传入本地玩法参考图;`ResolvedAssetImage` 在私有资源换签失败、普通图片 404 或真实封面缺失时使用该参考图作为卡片背景。
3. 兜底背景底色跟随百梦浅粉、暖白和珊瑚色调,不能继续使用深黑或暗蓝渐变作为草稿卡默认视觉。
4. 拼图作品列表摘要必须下发 `levels`,草稿页优先用关卡 `coverImageSrc`,再用选中候选图或最后一张候选图作为真实作品封面兜底。
5. 抓大鹅作品列表摘要必须保留 `generatedBackgroundAsset``generatedItemAssets` 中的 `imageObjectKey``containerImageObjectKey``imageViews[].imageObjectKey`;前端拿到 object key 后统一交给 `ResolvedAssetImage` 换签,不能因为缺少公开 URL 而退回黑卡。
6. `coverImageSrc` 若指向 `/creation-type-references/*`,只能视为玩法参考图兜底,不能当作作品真实封面。草稿页遇到这类值时必须继续向下解析同作品真实素材:拼图优先第一关正式图,再取选中候选图或最后一张候选图;抓大鹅优先 UI 背景图 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset.image*`,再取容器图,最后才取物品视角图或物品主图。

View File

@@ -1,950 +0,0 @@
# 创意互动内容生成 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 和 LangGraphMemory 层包括 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 URISpacetimeDB 不保存大图 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 1LangChain-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. 规则只保留为安全、契约、成本和权限边界。

View File

@@ -1,115 +0,0 @@
# Agent 创作流四阶段收口检查与旧链清理边界
更新时间:`2026-04-21`
补充修正:`2026-04-21` 本文档的“草稿恢复优先回 Agent 工作区”和“Agent 来源结果页冻结为预览收口层”属于阶段性收口口径,已被 [AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md](./AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md) 覆盖。当前主口径是Agent 对话框只收集八锚点,已有底稿的草稿从创作中心进入结果页继续完善。
## 1. 结论先行
当前这条 Agent 创作流已经完成阶段一到阶段三的主要收口。
阶段四中的“文档清理”已经开始做,但还没有形成独立、完整的新主链审计闭环。
因此这轮可以执行的清理现在有两类:
1. 删除已经不再从当前主入口可达的旧 `custom-world/sessions` 世界生成链
2. 删除已经完全脱离 `CustomWorldAgentWorkspace` 主链、只剩孤立互相引用与自测覆盖的 `custom-world-agent` 旧面板
3. 保留仍在服务 `Agent session` 主链或已保存作品兼容编辑体验的底层能力
这轮不做:
1. 不删 `Agent session` 的底层持久化能力
2. 不删已保存作品结果页的 legacy 编辑器兼容能力
3. 不删 `custom-world/works` 聚合入口
---
## 2. 阶段完成度
### 2.1 阶段一
已完成。
证据:
1. 结果页新增了 `sync_result_profile`
2. 结果页编辑后的快照可以回写到 `Agent session`
3. 自动保存、返回创作、进入世界都优先走 session 主链
### 2.2 阶段二
已完成。
证据:
1. 平台创作入口已切到 `custom-world/works`
2. 草稿恢复优先回 Agent 工作区
3. Agent 结果页不再继续新增旧编辑入口
### 2.3 阶段三
已完成。
证据:
1. 创作中心不再把 library draft 当主草稿入口
2. Agent 来源结果页冻结为预览收口层
3. 重复同步动作已收敛为有差异才执行
### 2.4 阶段四
未完全完成。
原因:
1. 文档清理已经开始,但还没有完整收束到单一结论文档
2.`custom-world/sessions` 生成链已经完成物理清理,但与之相关的审计/PRD/知识图谱文档仍需继续统一口径
3. `custom-world-agent` 孤岛面板已经完成第二轮物理清理,但阶段四文档总收口仍未完全覆盖所有历史 PRD 口径
---
## 3. 本轮允许删除的旧链
允许删除:
1. `src/services/aiService.ts` 里的旧 `custom-world/sessions` 请求函数
2. `server-node/src/routes/runtimeRoutes.ts` 里的旧 `custom-world/sessions` 路由
3. `server-node/src/services/customWorldGenerationService.ts`
4. 与这条旧链对应的测试
5. `server-node/src/services/customWorldSessionStore.ts`
6. `src/components/custom-world-agent/CustomWorldAgentLockBar.tsx`
7. `src/components/custom-world-agent/CustomWorldAgentQuickActions.tsx`
8. `src/components/custom-world-agent/CustomWorldAgentSummaryPanel.tsx`
9. `src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.tsx`
10. `src/components/custom-world-agent/CustomWorldAgentClarificationPanel.tsx`
11. `src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx`
12. `src/components/custom-world-agent/CustomWorldDraftCardDetailModal.tsx`
13. `src/components/custom-world-agent/CustomWorldDraftEditPanel.tsx`
14. `src/components/custom-world-agent/CustomWorldGenerateEntityModal.tsx`
15. 仅为上述孤岛面板存在的对应测试文件
不允许删除:
1. `server-node/src/repositories/runtimeRepository.ts` 中被 Agent session 复用的 session 持久化能力
2. `src/services/aiService.ts` 里仍在使用的 `generateCustomWorldProfile` 及其现代封装
3. 已保存作品结果页仍在使用的 legacy 编辑器兼容能力
4. `src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx` 及其仍在主链上的 5 个子模块:
- `CustomWorldAgentHeader`
- `EightAnchorProgressBar`
- `CustomWorldAgentOperationBanner`
- `CustomWorldAgentThread`
- `CustomWorldAgentComposer`
---
## 4. 删除完成后的判断标准
如果旧链清理成功,应满足:
1. `src/services/aiService.ts` 不再暴露旧 `custom-world/sessions` 请求函数
2. `server-node/src/routes/runtimeRoutes.ts` 不再挂旧 session 路由
3. `server-node/src/services/customWorldSessionStore.ts``server-node/src/services/customWorldGenerationService.ts` 已物理删除
4. 仓库里不再有主流程可达的旧世界生成入口
5. `CustomWorldAgentWorkspace.tsx` 只保留当前正式主链需要的 5 个子模块
6. 与旧 Agent 草稿面板相关的孤岛 UI 与自测不再继续占据正式目录注意力
7. Agent 主链与已保存作品编辑链仍然可用

View File

@@ -1,42 +0,0 @@
# 当前后端实现基线2026-04-25
## 1. 当前唯一落地口径
后续正式后端实现统一以 `server-rs` 为准:
- HTTP 门面Rust `api-server` / Axum。
- 实时状态与业务真相:`crates/spacetime-module` / SpacetimeDB。
- 共享领域与契约:`server-rs` 多 crate 分层维护。
- 前端职责:只做表现、输入采集、临时 UI 状态与服务端结果渲染。
涉及 SpacetimeDB 的表、reducer、绑定生成、发布、本地联调必须按仓库内 SpacetimeDB skills 执行。
## 2. 已替代的旧方向
以下旧方向不再作为新功能设计和编码依据:
- `server-node` / Express / PostgreSQL 正式后端路线。
- Go 服务端试验路线。
- 浏览器侧承担正式运行时逻辑、正式生成编排或正式数据真相的路线。
旧实现只允许作为迁移参考:可以阅读其 contract、提示词、测试用例和边界经验但不得为了兼容旧服务端继续扩展新代码。
## 3. 新文档落点
后续补充后端方案时优先落到这些文档族:
- Rust / SpacetimeDB 架构与切流:`SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md``BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md``M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md`
- SpacetimeDB 模块拆分:`SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md`
- Rust API 路由索引:`RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md`
- 本地与远端部署:`RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md``JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md`
如果旧文档与本基线冲突,以本基线和更新日期更近的 Rust / SpacetimeDB 文档为准。
## 4. DDD 重构总纲补充
`2026-04-28` 起,`server-rs` 后续后端改动还必须同时遵循 [SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md](./SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md)
- `module-*` 只承载领域模型、命令、应用编排结果、领域事件和领域错误。
- `spacetime-module` 只承载 SpacetimeDB 表、reducer、procedure、事务 adapter 与 mapper。
- `api-server` 只承载 HTTP / SSE / BFF adapter 和外部平台服务编排。
- 任何表结构变化仍必须同步 `migration.rs` 与 [SPACETIMEDB_TABLE_CATALOG.md](./SPACETIMEDB_TABLE_CATALOG.md)。

View File

@@ -1,113 +0,0 @@
# Custom World Agent 删除作品 / 新增 NPC / 新增场景 Rust 迁移记录
日期:`2026-04-24`
## 范围
本次继续检查 RPG 创作 Agent 从旧 `server-node` 迁到 `server-rs` 后的功能缺口,重点覆盖:
1. 删除作品。
2. 新增 NPC`generate_characters`)。
3. 新增场景 / 地点(`generate_landmarks`)。
## 结论
### 删除作品
Rust 链路已经存在:
```text
DELETE /api/runtime/custom-world/library/:profileId
-> api-server.delete_custom_world_library_profile
-> spacetime-client.delete_custom_world_profile
-> SpacetimeDB delete_custom_world_profile_and_return
-> owner-only 软删除 profile并从 gallery 读模型移除
```
本次未改删除作品实现,只确认它已走 Rust + SpacetimeDB不再依赖 Node。
### 新增 NPC / 新增场景
迁移前,`spacetime-module``generate_characters``generate_landmarks` 只走 `execute_placeholder_custom_world_action(...)`,不会真的调用 AI也不会更新 `draftProfile` / draft card。
本次迁移后链路变为:
```text
前端 action
-> api-server execute_custom_world_agent_action
-> api-server 读取 session snapshot
-> platform-llm 使用旧 Node prompt 生成 JSON 数组
-> payload 注入 generatedCharacters / generatedLandmarks
-> spacetime-client.execute_custom_world_agent_action
-> SpacetimeDB 更新 draftProfile
-> SpacetimeDB upsert 对应 draft card
-> SpacetimeDB 更新 publishGate / resultPreview / checkpoint / operation / action result message
```
## Node 对齐点
新增 NPC 保留旧 Node 的 system prompt 与 user prompt 字段约束:
```text
name, role, publicMask, hiddenHook, relationToPlayer, summary, threadIds
```
新增场景保留旧 Node 的 system prompt 与 user prompt 字段约束:
```text
name, purpose, mood, secret, summary, threadIds, characterIds
```
Rust 侧只做最小归一化:补 `id`、去除重名、限制数量 `1..=3`,不改写提示词原文语义。
## 落库设计
1. `generate_characters` 默认追加到 `draftProfile.storyNpcs`
2. `generate_landmarks` 追加到 `draftProfile.landmarks`
3. 每个新增对象都会生成 / 更新一张 `custom_world_draft_card`
4. 操作完成后同步更新:
- `last_assistant_reply`
- `publish_gate_json`
- `result_preview_json`
- `checkpoints_json`
- `custom_world_agent_operation`
- `custom_world_agent_message`
## 验证
已运行:
```bash
cargo test -p api-server custom_world_agent_entities --no-default-features
cargo check -p spacetime-module
```
结果:通过。`spacetime-module` 仅保留仓库既有 glob re-export warning。
## 2026-04-24 追加:结果页删除与资产动作闭环
本次继续补齐除长尾补全外的结果页可见动作:
1. `delete_characters` / `delete_landmarks` 已由 Rust SpacetimeDB reducer 直接更新 `draft_profile_json``result_preview_json``publish_gate_json``checkpoints_json``custom_world_draft_card``custom_world_agent_operation``custom_world_agent_message`
2. `generate_characters` 增加 `roleType`,可扮演角色写入 `playableNpcs`,场景角色写入 `storyNpcs`,不再把可扮演角色落到场景角色列表。
3. `generate_role_assets` / `generate_scene_assets` 不再走占位动作Rust 会校验目标对象、切到 `visual_refining`、设置 `focus_card_id`,并记录 operation/message。
4. `sync_role_assets` / `sync_scene_assets` 已迁移 Node 的 profile 字段写回逻辑:角色写回 `imageSrc``generatedVisualAssetId``generatedAnimationSetId``animationMap`;场景写回 `imageSrc``generatedSceneAssetId``generatedScenePrompt``generatedSceneModel`,并同步 `sceneChapters.acts` 背景字段。
5. 前端结果页角色卡展示“生成资产”,场景卡展示“生成场景图”,均通过 `autosaveCoordinator.executeAgentActionAndWait` 调 Rust API 并用最新 session 重建预览。
本轮仍不迁移 `expand_long_tail`,保持后续单独设计。
## 2026-04-24 追加:创作 Tab 删除作品入口
用户在 `http://127.0.0.1:3000/` 的“创作”Tab 看不到删除作品入口,原因是 `RpgEntryHomeView``CreationLibraryCard` 只支持整卡打开详情,没有接收删除回调。已补齐:
1. `CreationLibraryCard` 右上角展示“删除”按钮,点击时阻止整卡打开详情。
2. `RpgEntryHomeView` 新增 `onDeleteLibraryEntry``deletingLibraryEntryId` props。
3. `PlatformEntryFlowShellImpl` 复用 `deleteRpgEntryWorldProfile`,删除后刷新我的作品列表与公开广场。
链路保持为:前端创作 Tab -> `deleteRpgEntryWorldProfile` -> Rust runtime API -> SpacetimeDB 软删除 profile / 移除 gallery 读模型。
## 2026-04-24 追加:创作 Hub 草稿删除入口修正
截图中的“创作”Tab 实际渲染的是 `CustomWorldCreationHub` / `CustomWorldWorkCard`,不是默认 `RpgEntryHomeView``CreationLibraryCard`。此前 Hub 只给 `status=published` 的 RPG 作品传入删除回调,导致草稿卡片没有“删除”按钮。
修正后:只要 RPG 创作条目存在 `profileId`,无论 `draft` 还是 `published`,都会在卡片底部动作区展示“删除”。删除继续复用 `PlatformEntryFlowShellImpl.handleDeletePublishedWork`,走 `deleteRpgEntryWorldProfile` -> Rust runtime API -> SpacetimeDB 软删除。

View File

@@ -1,23 +0,0 @@
# 世界共创聊天最终回复时机调整2026-04-24
## 背景
创作聊天页顶部进度条来自后端会话快照 `progressPercent`,而助手文本此前通过 SSE `reply_delta` 在模型生成过程中提前展示。这样会导致玩家先看到完整或接近完整的文本回复,但进度、锚点、阶段与推荐操作仍停留在上一轮状态。
## 本次约束
- 玩家可继续看到自己的输入被乐观插入聊天线程。
- 助手回复不再通过中途 `reply_delta` 展示。
- 本轮模型输出解析、SpacetimeDB finalize、最终 session 快照读取全部完成后,助手回复才随最终 session 一次性显示。
- 进度条、阶段、锚点内容、推荐动作和助手回复在同一次 session 替换中同步刷新。
## 落地方案
1. `server-rs/crates/api-server/src/custom_world.rs``stream_custom_world_agent_message` 保留 SSE 响应格式,但不再发送 `reply_delta` 事件。
2. 同一接口仍等待 `run_custom_world_agent_turn` 完成,再调用 `finalize_custom_world_agent_message` 写入 SpacetimeDB。
3. finalize 成功后读取最终 session并通过 `session` 事件一次性返回给前端。
4. `src/components/creation-agent/CreationAgentWorkspace.tsx` 仅在确实存在流式文本时显示临时助手气泡,避免无 `reply_delta` 时出现空回复。
## 预期体验
发送消息后,玩家会先看到自己的消息和忙碌状态;助手文本会在进度、阶段与会话数据全部更新后一次性出现,避免“回复已经到了但进度还没动”的错位感。

View File

@@ -1,195 +0,0 @@
# Custom World Agent 大模型对话恢复设计
日期:`2026-04-22`
## 1. 背景
当前 Rust `server-rs` 里的 Custom World Agent 聊天链路已经接上了会话、消息、operation 与 SSE 外壳,但**并没有真正调用大模型生成聊天回复**。
现状问题:
1. `submit_custom_world_agent_message``spacetime-module` 中直接写死 assistant 回复。
2. `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` 只是把最后一条 assistant 文案一次性回放给前端。
3.`server-node` 已经实现过完整的大模型单轮推理链,包括:
- 动态状态识别
- 原样提示词拼装
- 流式 `replyText` 截取
- 回合结束后的 anchor / creator intent / readiness / clarification / suggested action 派生
用户本轮要求是:
1. 恢复 Agent 聊天对话使用大模型推理生成回复。
2. 把之前 Node 的提示词和后台流程恢复到 Rust 后端。
3. **禁止修改提示词正文。**
## 2. 目标
本轮只恢复 Custom World Agent 聊天主链的真实推理闭环:
1. 用户发消息后assistant 回复必须来自大模型。
2. SSE `reply_delta` 必须来自真实流式推理增量。
3. 回合结束后要把 session 派生状态一次性回写到 SpacetimeDB。
4. 旧 Node `eightAnchorPrompts.ts` 的提示词正文保持原样,不改中文文案。
## 3. 约束
### 3.1 SpacetimeDB 约束
SpacetimeDB reducer / procedure 必须保持确定性,因此:
1. 禁止在 `spacetime-module` 内直接发起 LLM 网络请求。
2. LLM 调用必须放在 `api-server`
3. `spacetime-module` 只负责提交消息、记录 operation、回写最终结果。
### 3.2 提示词冻结约束
本轮严格复用旧 Node 的以下提示词内容:
1. `server-node/src/prompts/eightAnchorPrompts.ts`
2. `BASE_SYSTEM_PROMPT`
3. `GLOBAL_HARD_RULES`
4. `MODE_RULES`
5. `USER_SIGNAL_RULES`
6. `QUICK_FILL_EXTRA_RULES`
7. `STATE_INFERENCE_SYSTEM_PROMPT`
8. `STATE_INFERENCE_OUTPUT_CONTRACT`
9. `OUTPUT_CONTRACT_REMINDER`
允许做的只有:
1. Rust 字符串字面量搬运
2. Rust 函数式重组
3. Rust/Serde 语法等价改写
不允许:
1. 修改提示词正文
2. 调整规则措辞
3. 替换成新的 prompt 版本
## 4. 目标链路
恢复后的消息链路改成两阶段:
### 4.1 阶段 A提交消息
`submit_custom_world_agent_message`
职责:
1. 校验 session / message / operation id。
2. 只写入 user message。
3. 创建 `process_message` operation。
4. operation 初始状态写为 `running`
5. 不直接写 assistant message。
6. 不直接推进 progress / stage / current_turn。
### 4.2 阶段 B完成单轮推理
`finalize_custom_world_agent_message_turn`
职责:
1. 校验 session 与 operation 所属关系。
2. 追加 assistant message。
3. 回写 session 聚合字段:
- `current_turn`
- `progress_percent`
- `stage`
- `focus_card_id`
- `anchor_content_json`
- `creator_intent_json`
- `creator_intent_readiness_json`
- `anchor_pack_json`
- `draft_profile_json`
- `last_assistant_reply`
- `pending_clarifications_json`
- `suggested_actions_json`
- `recommended_replies_json`
- `quality_findings_json`
- `asset_coverage_json`
3. 更新对应 operation 为 `completed``failed`
## 5. `api-server` 责任
`api-server` 新增 Custom World Agent turn service负责
1. 读取当前 session 快照。
2. 按旧 Node 逻辑构造 chat history。
3. 先走“动态状态识别”推理。
4. 再走“正式单轮输出”推理。
5. 流式阶段从 JSON 片段里增量截取 `replyText`,持续往 SSE 发 `reply_delta`
6. 回合结束后派生:
- creator intent
- readiness
- pending clarifications
- suggested actions
- anchor pack
- draft profile
- quality findings
- asset coverage
7. 最后调用 SpacetimeDB finalize procedure 回写真相。
## 6. 旧 Node 逻辑恢复范围
本轮恢复以下旧 Node 行为:
1. `eightAnchorPrompts.ts`
2. `eightAnchorSingleTurnService.ts`
3. `customWorldAgentMessageTurnService.ts`
4. `customWorldAgentClarificationService.ts`
5. `customWorldAgentIntentExtractionService.ts`
6. `customWorldAgentSuggestedActionService.ts`
7. `eightAnchorCompatibilityService.ts` 中聊天链需要的 anchor / progress 派生
本轮不强制一比一恢复整条结果页重编译链,只恢复聊天链真正依赖的最小派生结果。
## 7. SSE 口径
恢复后 `/messages/stream` 必须按以下顺序输出:
1. 多个 `reply_delta`
2. 一个 `session`
3. 一个 `done`
错误时输出:
1. `error`
要求:
1. `reply_delta.text` 来源于 `platform-llm.stream_text(...)` 的真实增量。
2. `session` 必须来自 finalize 完成后的最新 session 真相。
## 8. 验收
1. 用户发一条 Agent 消息后assistant 回复不再是固定文案。
2. `quickFillRequested=true` 时,推理结果仍遵循旧 Node 的 `force_complete` 逻辑。
3. SSE 能先看到逐步增长的 `reply_delta`,而不是一次性整段返回。
4. finalize 完成后,前端拿到的 session 中:
- `lastAssistantReply`
- `messages`
- `currentTurn`
- `progressPercent`
- `stage`
- `pendingClarifications`
- `suggestedActions`
已被真实更新。
5. 提示词正文未被改写。
## 9. 当前实现状态
截至 `2026-04-22`,本方案已在 `server-rs` 完成以下落地:
1. `submit_custom_world_agent_message` 已改为只提交 user message 与 running operation不再写死 assistant 回复。
2. `api-server` 的 Custom World Agent turn service 已恢复:
- 动态状态识别
- 正式单轮推理
- `replyText/progressPercent/nextAnchorContent` JSON 解析
- creator intent / readiness / clarification / suggested action 等最小派生
3. `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` 已改为真实 SSE 流:
- 推理进行中实时输出累计 `reply_delta`
- finalize 后输出最新 `session`
- 最后输出 `done`
4. 普通提交接口在 finalize 完成后返回最终 operation 快照,而不是 submit 阶段的 running 快照。
5. `finalize_custom_world_agent_message_turn` 已负责把 assistant message、session 聚合字段与 operation 最终状态一次性回写到 SpacetimeDB。

View File

@@ -1,68 +0,0 @@
# 自定义世界资产 Prompt 与默认描述配置说明2026-04-24
## 1. 目标
本说明记录生成世界草稿时,角色形象图像、角色动作视频、每一幕场景背景图像三类资产的默认描述与正式模型 prompt 的配置位置,避免后续继续误改旧 `server-node` 链路。
## 2. 世界草稿默认描述字段
生成世界草稿时,后端会要求模型在角色与幕级剧情结构阶段直接产出资产默认描述字段:
- 角色:`visualDescription`,用于打开角色形象图像生成面板时默认填入角色形象描述框。
- 角色:`actionDescription`,用于打开角色动作视频生成面板时默认填入各动作描述框;当前每个动作会从同一角色默认动作描述起步,用户切换动作后可分别编辑并缓存。
- 角色:`sceneVisualDescription`,用于描述角色常出现或关联的场景画面。三个角色默认描述字段必须在角色 outline 阶段同一次模型调用中产出;若模型遗漏,只允许后端本地兜底补字段,不再额外发起独立修复模型调用。
- 每一幕:`sceneChapterBlueprints[*].acts[*].backgroundPromptText`,用于打开该幕背景图像生成面板时默认填入场景描述框。
- 场景:`visualDescription` 只作为旧场景图或没有幕级描述时的兜底,不再从角色 AI 形象生成面板维护场景背景描述。
- 场景:`actNPCNames``connectedLandmarkNames``entryHook` 必须在关键场景生成阶段同一次模型调用中产出,并由原场景解析链路写入 `landmarks` 与幕级 `primaryNpcId / oppositeNpcId / encounterNpcIds`;不再使用独立的场景网络补全提示词。旧草稿中的 `sceneNpcNames` 仅作为兼容读取兜底,不作为新生成字段。
草稿生成契约位置:
- `server-rs/crates/api-server/src/custom_world_foundation_draft.rs`
- `build_custom_world_role_outline_batch_prompt`
- `build_custom_world_landmark_seed_batch_prompt`
- `build_foundation_draft_user_prompt`
- `normalize_scene_act_blueprint`
前端默认框映射位置:
- `src/prompts/customWorldRolePromptDefaults.ts`
- `visualPromptText` 优先取 `role.visualDescription`
- `animationPromptText` 优先取 `role.actionDescription`
- `src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx`
- 角色形象与动作工坊初始化默认文本。
- `animationPromptTextByKey` 负责分动作保存动作描述。
- 当角色本身已有 `visualDescription/actionDescription` 时,必须优先使用这批世界草稿新生成字段,不能让旧 workflow cache 覆盖当前草稿默认文本。
- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx`
- 幕背景图像生成弹窗优先使用 `act.backgroundPromptText`
- 普通场景图像生成弹窗仍可使用 `landmark.visualDescription` 兜底。
## 3. 正式模型 Prompt 配置
正式生成图片或视频时,不直接使用默认描述字段作为完整 prompt而是在 `server-rs` 继续编译:
- 角色主图:`server-rs/crates/api-server/src/custom_world_asset_prompts.rs`
- `build_character_visual_prompt`
- 内部使用 `build_master_prompt`
- 只拼入用户可见的 `promptText` / `visualPromptText`,不再拼入 `characterBriefText` 或角色摘要字段。
- 角色动作视频:`server-rs/crates/api-server/src/custom_world_asset_prompts.rs`
- `build_character_animation_prompt`
- 图生视频分支使用 `build_video_action_prompt`
- 场景背景图:`server-rs/crates/api-server/src/custom_world_ai.rs`
- `build_custom_world_scene_image_prompt`
## 4. 当前约束
- 不再把 `server-node/src/prompts/characterAssetPrompts.ts` 作为主链修改目标。
- 默认描述字段必须由世界草稿生成阶段写入,前端只负责把字段填入输入框并允许用户编辑。
- UI 不默认展示规则解释文案,正式约束只进入后端 prompt。
## 5. 自动草稿素材回写约束
- 世界草稿自动素材生成与草稿页手动生成使用同一套 `server-rs/crates/api-server/src/custom_world_ai.rs` 场景图接口和 OSS/SpacetimeDB 资产持久化链路。
- 自动批量生成幕背景时,后端必须把已成功生成的 `backgroundImageSrc/backgroundAssetId/generatedScenePrompt/generatedSceneModel` 写回 `sceneChapterBlueprints[*].acts[*]`,不能因为同批某一幕失败而丢弃已成功图片。
- 某一幕连续重试仍失败时,只允许在该幕写入 `backgroundGenerationError` 作为诊断字段;只要至少一幕成功,草稿仍应完成并让前端展示成功图片。
- 只有全部幕背景均失败时,才把“生成幕背景图失败”作为草稿素材阶段失败原因保存。
- Rust 服务实际生图模型读取 `DASHSCOPE_SCENE_IMAGE_MODEL` / `DASHSCOPE_COVER_IMAGE_MODEL` / `DASHSCOPE_REFERENCE_IMAGE_MODEL`;兼容旧 `DASHSCOPE_IMAGE_MODEL`,避免 `.env.example` 中配置了模型但服务端仍使用硬编码模型。
- 自动草稿幕背景不能把 `backgroundPromptText` 直接作为最终 `prompt` 传给 DashScope它必须像草稿页手动生成一样把幕级描述作为 `userPrompt`,并用同一个地点对象的 `name/description` 作为场景上下文,再由 `build_custom_world_scene_image_prompt` 统一拼入世界名、世界摘要、风格、玩家目标、场景名、场景描述和负面词。用户不修改默认描述直接点生成时,手动生成与自动草稿生成的正式生图上下文必须一致。
- 自动草稿幕背景的默认尺寸必须与草稿页手动生成默认尺寸一致,当前统一为 `1280*720`;不能在自动链路中单独改成 `1600*900`,否则同一 prompt 在同一模型下也可能因供应商尺寸支持或耗时不同而表现不一致。
- 批量自动生图失败日志必须保留 `AppError.details.message` 中的供应商真实原因,不能只记录 `AppError.message()` 的 HTTP 泛化文案,否则排查时只能看到“上游服务请求失败”,无法确认是尺寸、模型、限流、超时还是内容审核失败。

View File

@@ -1,174 +0,0 @@
# 世界草稿自动资产可见性修复说明 2026-04-20
更新时间:`2026-04-20`
## 1. 问题现象
在世界草稿生成完成后,用户反馈:
1. 草稿里看不到角色主形象
2. 场景里看不到每一幕的背景图
这类反馈容易被误判成“自动资产没有生成”,但实际排查后发现,问题主要集中在**结果页展示链路**,同时叠加了一个**fallback 资源不可预览**的问题。
## 2. 链路排查结论
本轮检查后确认:
1. 服务端自动资产服务会把角色主形象写回 `draftProfile.playableNpcs[].imageSrc / generatedVisualAssetId`
2. 服务端自动资产服务会把幕背景图写回 `draftProfile.sceneChapters[].acts[].backgroundImageSrc / backgroundAssetId`
3. `agent draft -> result profile` 的适配层也会保留这些字段
真正的问题出在后续两个环节。
## 3. 根因
### 3.1 结果页可扮演角色卡优先用了运行时预览
结果页 `CustomWorldEntityCatalog` 的可扮演角色卡,之前优先显示:
1. `previewCharacter`
2. 再回退到 `role.imageSrc`
这会导致:
1. 草稿里已经有真实生成主图
2. 但界面仍优先渲染模板/运行时预览角色
3. 用户视觉上看不到最新生成主形象
### 3.2 场景页没有把多幕背景图真正展示出来
结果页 `场景` Tab 之前只展示:
1. 开局场景
2. 地点卡
但没有把:
`sceneChapterBlueprints[].acts[].backgroundImageSrc`
按可见结构渲染到结果页中。
因此即使后端已经生成并回写每一幕背景图,用户仍然只能看到“场景主图/地点图”,看不到“每一幕的图”。
### 3.3 fallback 自动资产写回的是 `.txt`
在没有 DashScope 图像能力时,`CustomWorldAgentAutoAssetService` 的 fallback 生成器之前会写:
1. 角色主形象:`master.txt`
2. 幕背景图:`scene.txt`
这虽然保证了字段被回写,但前端无法把 `.txt` 当图片展示,于是会进一步加重“好像没生成”的感知。
### 3.4 Agent 结果页入口优先读取 legacyResultProfile遮蔽了最新资产字段
世界草稿结果页不是直接读取当前 `draftProfile`,而是先经过:
1. `buildCustomWorldProfileFromAgentDraft`
2. `normalizeCustomWorldProfileRecord`
如果 `draftProfile.legacyResultProfile` 存在,旧逻辑会直接优先返回这份历史编译结果。
但自动资产服务在 Phase3/Phase4 后续补齐时,更新的是当前 `draftProfile` 中的:
1. `playableNpcs[].imageSrc / generatedVisualAssetId`
2. `storyNpcs[].imageSrc / generatedVisualAssetId`
3. `landmarks[].imageSrc`
4. `sceneChapters[].acts[].backgroundImageSrc / backgroundAssetId`
这会导致:
1. 服务端真实已经生成并回写了最新角色主图和分幕图
2. 结果页入口却仍然取到一份更早的 `legacyResultProfile`
3. 页面看到的是“旧草稿快照”,不是“当前带资产的草稿结果”
因此用户会表现为“完全看不到这轮刚生成出来的图片”。
## 4. 修复策略
### 4.1 结果页角色卡优先显示真实生成主图
`src/components/CustomWorldEntityCatalog.tsx` 中调整逻辑:
1.`role.imageSrc` 已存在,则优先显示该图片
2. 只有在缺失真实主图时,才回退到运行时角色预览
这样可扮演角色卡能直接展示当前草稿回写的角色主形象。
### 4.2 场景列表改为只展示场景卡,章节内容留在二级页
结合后续体验反馈,本轮又进一步收口了结果页结构:
1. `结果页 -> 场景列表` 不再直接展开章节与分幕内容
2. 场景列表卡片只负责展示:
- 场景名
- 场景摘要
- 场景图
3. 场景卡图片优先取该场景章节的首幕 `backgroundImageSrc`
4. 若首幕图缺失,再回退到场景主图 / 地标图
5. 章节标题、幕标题、幕目标等信息只在点击场景后的二级编辑页中查看
这样结果页列表保持清爽,但用户仍然能在列表里直接看到当前场景已生成的图片。
### 4.3 fallback 改为可显示 PNG
`server-node/src/services/customWorldAgentAutoAssetService.ts` 中调整 fallback
1. 不再写 `master.txt / scene.txt`
2. 改为写合法可显示的占位 `png`
3. prompt 信息单独写进 `manifest.json`
4. 角色主形象 fallback PNG 统一输出为 `1:1`
这样即使当前环境没有真实图像生成能力,草稿层也仍然会回写“前端能直接显示的图片资源”。
### 4.4 结果页读取 legacy profile 时强制合并当前草稿的最新资产字段
`src/services/customWorldAgentDraftResult.ts` 中补上合并逻辑:
1. 若存在 `legacyResultProfile`,继续保留它的完整运行时字段
2. 但会把当前 `draftProfile` 里最新回写的角色主图、地标图、分幕图再覆盖回结果页 profile
3. 这样结果页既不会丢失旧 runtime profile 的完整结构,也不会再被旧快照遮蔽最新图片资产
这一层修的是结果页真实入口,而不是仅修展示组件。
## 5. 影响范围
本次修复涉及:
1. `src/components/CustomWorldEntityCatalog.tsx`
2. `src/components/CustomWorldResultView.test.tsx`
3. `src/services/customWorldAgentDraftResult.test.ts`
4. `server-node/src/services/customWorldAgentAutoAssetService.ts`
5. `server-node/src/services/customWorldAgentAutoAssetService.test.ts`
6. `docs/technical/CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md`
7. 历史 saved profile 资产同步脚本 / 数据修复动作
## 6. 验收标准
修复后需要满足:
1. 世界草稿结果页的可扮演角色卡能直接看到生成主形象
2. 世界草稿结果页的场景列表能直接看到场景图片,且优先展示首幕背景图
3. 场景章节与分幕内容只在场景二级页中展示
3. `agent draft -> result profile` 不会丢失角色主图与幕背景字段
4. fallback 环境下回写的仍是前端可显示图片,而不是文本文件
5. 角色主形象 fallback PNG 尺寸必须满足 `1:1`
6. 即使存在 `legacyResultProfile`,结果页也必须展示当前草稿最新同步的角色主图与幕背景图
## 6.1 历史保存档案补充结论
本轮在真实 PostgreSQL 数据中又确认了一类历史问题:
1. `agent session` 中的草稿资产字段可能已经补齐
2. 但较早时刻自动保存过的 `custom_world_profiles.payload_json` 仍停留在旧路径
3. 用户如果从作品库打开的是 saved profile就会继续看到旧图或空图
因此这次修复除了改默认生成与展示逻辑,还需要对受影响的历史 saved profile 做一次同步刷新。
## 7. 后续建议
后续继续迭代这条链路时,建议保持:
1. “资产已生成”必须和“用户已看见”同时验证,不能只验证字段回写
2. 结果页与草稿工作区都要把多幕背景视为正式资产,不要只停留在编辑弹层里
3. 所有 fallback 资源都应保持为 UI 可直接消费的媒体格式

Some files were not shown because too many files have changed in this diff Show More