收口后端创作游玩流程主干
新增 play_flow 统一承接创作游玩与支撑路由 将 app.rs 的逐玩法挂载改为统一主干分发 将平台与资产及个人侧游玩支撑路由迁入 play_flow 抽出 visual_novel 路由模块并复用原有 handler 统一入口熔断路径解析并补充目标回归测试 更新后端契约、玩法链路和团队决策记录
This commit is contained in:
@@ -24,6 +24,14 @@
|
|||||||
- 验证方式:执行 `cargo check --manifest-path server-rs/Cargo.toml -p platform-wechat`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server`、微信相关定向测试和编码检查;新增微信协议细节优先落到 `platform-wechat`。
|
- 验证方式:执行 `cargo check --manifest-path server-rs/Cargo.toml -p platform-wechat`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server`、微信相关定向测试和编码检查;新增微信协议细节优先落到 `platform-wechat`。
|
||||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【技术方案】微信虚拟支付接入-2026-05-26.md`。
|
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【技术方案】微信虚拟支付接入-2026-05-26.md`。
|
||||||
|
|
||||||
|
## 2026-06-08 后端创作 / 游玩流程先统一主干再领域分发
|
||||||
|
|
||||||
|
- 背景:前端平台入口、作品架、公开详情和推荐运行态已经持续收口,但 `api-server` 仍在 `app.rs` 逐玩法合并创作 / 运行态路由,入口开关路径判断也独立维护,新增玩法容易复制出平行链路。
|
||||||
|
- 决策:后端所有创作 / 游玩相关 HTTP 路由先进入 `server-rs/crates/api-server/src/modules/play_flow.rs` 统一主干;主干注册 `playId`、领域模块 key、创作路由前缀、运行态路由前缀和新建创作入口开关匹配规则,并在进入领域 handler 前统一挂载 `PlayFlowRequestContext`,再在最后一步分发到各玩法领域 HTTP Adapter。创作入口配置、AI task、runtime chat、运行态设置 / 存档、运行态库存、游玩历史、存档归档、游玩统计、历史素材、角色资产工坊、角色图像 / 动画生成和 Hyper3D 代理也作为创作 / 游玩支撑能力从 `play_flow` 进入;`modules/platform.rs` 只保留通用 LLM / 语音代理。`app.rs` 只合并 `modules::play_flow::router(state)`,不再逐玩法 merge;`creation_entry_config.rs` 复用 `play_flow` 的入口开关解析,不维护第二份路径表。
|
||||||
|
- 影响范围:`api-server` 路由组织、入口开关、玩法接入 SOP、后端契约文档、后续新增 / 迁移玩法。
|
||||||
|
- 验证方式:`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`,并确认旧 `/api/creation/<play>/*`、历史 `/api/runtime/<play>/agent/*` 与公开 runtime 路由外部契约不变。
|
||||||
|
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
## 2026-06-07 推荐页运行态先封面预载再 ready 渐隐
|
## 2026-06-07 推荐页运行态先封面预载再 ready 渐隐
|
||||||
|
|
||||||
- 背景:移动端推荐页上下切换公开作品时,如果运行态和封面资源没有明确准备边界,用户会看到未加载完成的 runtime、黑底闪动,或切卡后反向回弹。
|
- 背景:移动端推荐页上下切换公开作品时,如果运行态和封面资源没有明确准备边界,用户会看到未加载完成的 runtime、黑底闪动,或切卡后反向回弹。
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ npm run check:server-rs-ddd
|
|||||||
- 健康检查:`GET /healthz`。
|
- 健康检查:`GET /healthz`。
|
||||||
- 后台管理:`/admin/api/*`,包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、作品可见性、兑换码、邀请码、任务配置和充值商品配置。
|
- 后台管理:`/admin/api/*`,包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、作品可见性、兑换码、邀请码、任务配置和充值商品配置。
|
||||||
- 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
|
- 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
|
||||||
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请、兑换、存档、历史浏览和游玩统计。
|
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请和兑换等账号侧能力。
|
||||||
- LLM 与语音:`/api/llm/*`、`/api/speech/volcengine/*`。
|
- 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
|
||||||
- 资产:`/api/assets/*`,包括直传票据、STS、对象确认、实体绑定、读签名、读 bytes、历史资产、角色图像/动画和 Hyper3D 代理。
|
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-*`,负责直传、确认、绑定和读取。
|
||||||
- 创作入口配置:`/api/creation-entry/config`,后台 `/admin/api/creation-entry/config` 和 `/admin/api/creation-entry/config/banners`。
|
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`。
|
||||||
|
- 后台入口配置:`/admin/api/creation-entry/config` 和 `/admin/api/creation-entry/config/banners`。
|
||||||
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
||||||
- 拼图:`/api/runtime/puzzle/*`。
|
- 拼图:`/api/runtime/puzzle/*`。
|
||||||
- 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。
|
- 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。
|
||||||
@@ -70,9 +71,20 @@ npm run check:server-rs-ddd
|
|||||||
- 跳一跳:`/api/creation/jump-hop/*`、`/api/runtime/jump-hop/*`。
|
- 跳一跳:`/api/creation/jump-hop/*`、`/api/runtime/jump-hop/*`。
|
||||||
- 汪汪声浪:`/api/runtime/bark-battle/*`。
|
- 汪汪声浪:`/api/runtime/bark-battle/*`。
|
||||||
- 儿童向创作:`/api/creation/edutainment/*`。
|
- 儿童向创作:`/api/creation/edutainment/*`。
|
||||||
- AI task:`/api/ai/tasks*`。
|
|
||||||
|
|
||||||
需要新增路由时,先确认玩法入口配置和 tracking 分类,不要绕过 `app.rs` 的统一中间件、鉴权和入口开关。
|
需要新增路由时,先确认玩法入口配置和 tracking 分类,不要绕过 `app.rs` 的统一中间件、鉴权和入口开关。涉及创作、生成、作品、公开详情、试玩、正式运行态、运行态库存、运行态设置 / 存档、游玩历史、存档归档、游玩统计、AI task、角色资产工坊或玩法生成支撑资产的路由,不再直接在 `app.rs` 逐玩法 `.merge(...)`,也不挂到 `modules/platform.rs`;必须先进入 `server-rs/crates/api-server/src/modules/play_flow.rs` 的统一玩法流程主干,再由主干注册表分发到各领域 HTTP Adapter 或支撑能力 handler。
|
||||||
|
|
||||||
|
### 创作 / 游玩统一流程主干
|
||||||
|
|
||||||
|
`modules/play_flow.rs` 是后端创作与游玩流程的统一入口。现有外部 URL、DTO、错误 envelope、鉴权方式、入口开关语义和 SpacetimeDB schema 默认不变,但路由组织必须遵循:
|
||||||
|
|
||||||
|
1. `app.rs` 只合并 `modules::play_flow::router(state)`,不直接合并 RPG、拼图、抓大鹅、跳一跳、敲木鱼、拼消消、汪汪声浪、视觉小说或儿童向创作等逐玩法模块。
|
||||||
|
2. `play_flow` 统一注册每个玩法的 `playId`、领域模块 key、创作路由前缀和运行态路由前缀;后续新增玩法或迁移旧玩法时,先补这个注册表,再挂具体领域模块路由。
|
||||||
|
3. 新建创作、首次生成和 Remix 成草稿等会产生新创作的入口开关匹配规则同样归 `play_flow` 管理;`creation_entry_config.rs` 只复用该规则执行 `open=false` 熔断,不再维护第二份路径判断。
|
||||||
|
4. `play_flow` 在进入领域 handler 前先解析并挂载 `PlayFlowRequestContext`,统一标记请求处于 `Creation`、`Runtime`、`CreationEntryConfig`、`CreationSupport`、`RuntimeSupport`、`AiTask`、`PublicReadModel` 或 `RuntimeInventory` 阶段,并记录目标 `playId` / 领域模块 key;领域 handler 可以读取该上下文做后续收口,但不能绕过主干自建平行流程。
|
||||||
|
5. `play_flow` 只做平台共性编排和领域 Adapter 组合,不下沉玩法规则;最后一步的草稿编译、资产生成、发布、运行态 start/action/finish、计分和排行榜仍交给对应 `module-*`、`spacetime-module` procedure 和玩法 HTTP handler 处理。
|
||||||
|
6. 公开作品聚合、作品详情、运行态库存、运行态设置 / 存档、游玩历史、存档归档、游玩统计、历史素材、AI task、runtime chat、文档解析、角色资产工坊、角色图像 / 动画生成和 Hyper3D 代理属于跨玩法或玩法支撑流程,也从 `play_flow` 主干挂入;`modules/platform.rs` 只保留通用 LLM / 语音代理,不再承接创作 / 游玩支撑路由。
|
||||||
|
7. 如果某个旧玩法仍使用历史 `/api/runtime/<play>/agent/*` 作为创作命名空间,只保留外部兼容路径;新增实现和文档仍按“统一主干 -> 领域 Adapter”的语义描述,不把历史路径当新架构模板。
|
||||||
|
|
||||||
### 认证态用户与会话摘要下发口径
|
### 认证态用户与会话摘要下发口径
|
||||||
|
|
||||||
@@ -87,8 +99,8 @@ npm run check:server-rs-ddd
|
|||||||
|
|
||||||
路由模块化规则:
|
路由模块化规则:
|
||||||
|
|
||||||
1. 每个能力 Module 只暴露 `router(state) -> Router<AppState>`,由 `app.rs` 统一 `.merge(...)`。
|
1. 每个能力 Module 只暴露 `router(state) -> Router<AppState>`;平台创作 / 游玩相关 Module 和支撑能力由 `modules/play_flow.rs` 统一 `.merge(...)` 或在支撑 router 内挂载,其它账号、资产基础、后台和平台基础能力再由 `app.rs` 直接合并。
|
||||||
2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue。
|
2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue;不得重新恢复逐玩法 creation/runtime merge 列表。
|
||||||
3. 能力 Module 可在路由内部用 `FromRef<AppState>` 派生自己的 Feature State,例如 `PuzzleApiState`。全局 `AppState` 仍作为进程组合根、鉴权层和全局中间件状态,但业务 handler 优先只提取对应 Feature State,不直接暴露完整 `AppState`。
|
3. 能力 Module 可在路由内部用 `FromRef<AppState>` 派生自己的 Feature State,例如 `PuzzleApiState`。全局 `AppState` 仍作为进程组合根、鉴权层和全局中间件状态,但业务 handler 优先只提取对应 Feature State,不直接暴露完整 `AppState`。
|
||||||
4. Feature State 只暴露该能力实际需要的 facade / adapter / 配置快照;若必须复用仍要求 `AppState` 的横切 helper(例如计费、外部失败审计或通用 tracking),应通过 Feature State 的窄方法或显式 `root_state()` 过渡,并在后续继续收窄。
|
4. Feature State 只暴露该能力实际需要的 facade / adapter / 配置快照;若必须复用仍要求 `AppState` 的横切 helper(例如计费、外部失败审计或通用 tracking),应通过 Feature State 的窄方法或显式 `root_state()` 过渡,并在后续继续收窄。
|
||||||
5. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现,再收窄 handler 可见状态。
|
5. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现,再收窄 handler 可见状态。
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts`
|
|||||||
创作入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态
|
创作入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态
|
||||||
```
|
```
|
||||||
|
|
||||||
|
后端链路也按同一条平台主干组织:所有创作、生成、作品回读、发布、试玩、正式 runtime、公开详情、作品架、运行态设置 / 存档、游玩历史、存档归档、游玩统计、历史素材、AI task、runtime chat、文档解析、角色资产工坊和玩法生成支撑资产相关 HTTP 路由,先注册到 `server-rs/crates/api-server/src/modules/play_flow.rs`,由主干在进入领域 handler 前统一解析 `PlayFlowRequestContext`,再在最后一步分发给对应领域模块或支撑能力 handler 处理。`app.rs` 不再逐玩法挂载创作 / 运行态路由,`modules/platform.rs` 只保留通用 LLM / 语音代理;新增玩法、补齐旧玩法或迁移旧路径时,必须先补 `play_flow` 的 `playId`、领域模块 key、创作路由前缀、运行态路由前缀和入口开关匹配规则,再补具体 handler。领域规则、胜负裁决、计分、发布状态、资产完整性和排行榜仍留在各自 `module-*` 与 SpacetimeDB procedure 中,不把平台主干写成某个玩法的新业务真相。
|
||||||
|
|
||||||
默认工作台只提交结构化表单、图片槽位和配置 payload,不默认增加聊天输入区、流式消息区或轻输入 Agent。确需偏离该模式时,必须先在 PRD 和本文档写明例外原因、影响范围和回退方式,再进入编码。
|
默认工作台只提交结构化表单、图片槽位和配置 payload,不默认增加聊天输入区、流式消息区或轻输入 Agent。确需偏离该模式时,必须先在 PRD 和本文档写明例外原因、影响范围和回退方式,再进入编码。
|
||||||
|
|
||||||
单图资产编辑统一通过 `CreativeImageInputPanel` 承载上传、AI 重绘、参考图、历史图、主图预览和删除确认;新玩法页面不得重复手写这些交互。主图已有图片时,默认点击图片打开全屏预览,上传 / 更换收口到右下角 `ImagePlus` 图标按钮;无图时仍允许点击空图卡上传。调用方只能通过 `canUploadMainImage`、`canUseImageHistory` 等受控参数开关上传和历史入口,不得用复制组件或样式遮挡改行为。系列素材图集生成统一走“批量规划 -> sheet 生图 -> 后端切图 -> 透明化 -> OSS 持久化 -> 状态回写 -> 局部重生成”流程,玩法只提供 `sheetSpec`、`slotSpecs`、提示词和字段映射,不把任一玩法专属素材 DTO 当作平台通用模型。
|
单图资产编辑统一通过 `CreativeImageInputPanel` 承载上传、AI 重绘、参考图、历史图、主图预览和删除确认;新玩法页面不得重复手写这些交互。主图已有图片时,默认点击图片打开全屏预览,上传 / 更换收口到右下角 `ImagePlus` 图标按钮;无图时仍允许点击空图卡上传。调用方只能通过 `canUploadMainImage`、`canUseImageHistory` 等受控参数开关上传和历史入口,不得用复制组件或样式遮挡改行为。系列素材图集生成统一走“批量规划 -> sheet 生图 -> 后端切图 -> 透明化 -> OSS 持久化 -> 状态回写 -> 局部重生成”流程,玩法只提供 `sheetSpec`、`slotSpecs`、提示词和字段映射,不把任一玩法专属素材 DTO 当作平台通用模型。
|
||||||
|
|||||||
@@ -542,8 +542,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_ai_task_returns_bad_gateway_when_spacetime_not_published() {
|
async fn create_ai_task_returns_bad_gateway_when_spacetime_not_published() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -605,8 +605,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn start_ai_task_returns_bad_gateway_when_spacetime_not_published() {
|
async fn start_ai_task_returns_bad_gateway_when_spacetime_not_published() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -652,8 +652,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ai_task_mutation_routes_return_bad_gateway_when_spacetime_not_published() {
|
async fn ai_task_mutation_routes_return_bad_gateway_when_spacetime_not_published() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
for route in ai_task_mutation_route_cases() {
|
for route in ai_task_mutation_route_cases() {
|
||||||
@@ -763,21 +763,20 @@ mod tests {
|
|||||||
(status, payload)
|
(status, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_authenticated_state() -> AppState {
|
async fn seed_authenticated_state() -> (AppState, String) {
|
||||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||||
state
|
let user_id = state
|
||||||
.seed_test_phone_user_with_password("13800138100", "secret123")
|
.seed_test_phone_user_with_password("13800138100", "secret123")
|
||||||
.await
|
.await
|
||||||
.id;
|
.id;
|
||||||
state
|
(state, user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_access_token(state: &AppState) -> String {
|
fn issue_access_token(state: &AppState, user_id: &str) -> String {
|
||||||
let claims = AccessTokenClaims::from_input(
|
let claims = AccessTokenClaims::from_input(
|
||||||
AccessTokenClaimsInput {
|
AccessTokenClaimsInput {
|
||||||
user_id: "user_00000001".to_string(),
|
user_id: user_id.to_string(),
|
||||||
session_id: state
|
session_id: state.seed_test_refresh_session_for_user_id(user_id, "sess_ai_tasks"),
|
||||||
.seed_test_refresh_session_for_user_id("user_00000001", "sess_ai_tasks"),
|
|
||||||
provider: AuthProvider::Password,
|
provider: AuthProvider::Password,
|
||||||
roles: vec!["user".to_string()],
|
roles: vec!["user".to_string()],
|
||||||
token_version: 2,
|
token_version: 2,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use tower_http::{
|
|||||||
use tracing::{Level, Span, error, info_span};
|
use tracing::{Level, Span, error, info_span};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{AuthenticatedAccessToken, require_bearer_auth},
|
auth::AuthenticatedAccessToken,
|
||||||
backpressure::limit_concurrent_requests,
|
backpressure::limit_concurrent_requests,
|
||||||
creation_entry_config::require_creation_entry_route_enabled,
|
creation_entry_config::require_creation_entry_route_enabled,
|
||||||
error_middleware::normalize_error_response,
|
error_middleware::normalize_error_response,
|
||||||
@@ -23,24 +23,9 @@ use crate::{
|
|||||||
modules,
|
modules,
|
||||||
request_context::{RequestContext, attach_request_context, resolve_request_id},
|
request_context::{RequestContext, attach_request_context, resolve_request_id},
|
||||||
response_headers::propagate_request_id_header,
|
response_headers::propagate_request_id_header,
|
||||||
runtime_inventory::get_runtime_inventory_state,
|
|
||||||
state::{AppState, BackpressureState},
|
state::{AppState, BackpressureState},
|
||||||
telemetry::record_http_observability,
|
telemetry::record_http_observability,
|
||||||
tracking::record_route_tracking_event_after_success,
|
tracking::record_route_tracking_event_after_success,
|
||||||
vector_engine_audio_generation::{
|
|
||||||
create_background_music_task, create_sound_effect_task,
|
|
||||||
create_visual_novel_background_music_task, create_visual_novel_sound_effect_task,
|
|
||||||
publish_background_music_asset, publish_sound_effect_asset,
|
|
||||||
publish_visual_novel_background_music_asset, publish_visual_novel_sound_effect_asset,
|
|
||||||
},
|
|
||||||
visual_novel::{
|
|
||||||
compile_visual_novel_session, create_visual_novel_session, delete_visual_novel_work,
|
|
||||||
execute_visual_novel_action, get_visual_novel_run, get_visual_novel_session,
|
|
||||||
get_visual_novel_work, list_visual_novel_gallery, list_visual_novel_history,
|
|
||||||
list_visual_novel_works, publish_visual_novel_work, regenerate_visual_novel_run,
|
|
||||||
start_visual_novel_run, stream_visual_novel_action, stream_visual_novel_message,
|
|
||||||
submit_visual_novel_message, update_visual_novel_work,
|
|
||||||
},
|
|
||||||
wechat::pay::{
|
wechat::pay::{
|
||||||
handle_wechat_pay_notify, handle_wechat_virtual_payment_message_push_verify,
|
handle_wechat_pay_notify, handle_wechat_virtual_payment_message_push_verify,
|
||||||
handle_wechat_virtual_payment_notify,
|
handle_wechat_virtual_payment_notify,
|
||||||
@@ -57,19 +42,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.merge(modules::profile::router(state.clone()))
|
.merge(modules::profile::router(state.clone()))
|
||||||
.merge(modules::assets::router(state.clone()))
|
.merge(modules::assets::router(state.clone()))
|
||||||
.merge(modules::platform::router(state.clone()))
|
.merge(modules::platform::router(state.clone()))
|
||||||
.merge(modules::story::router(state.clone()))
|
.merge(modules::play_flow::router(state.clone()))
|
||||||
.merge(modules::edutainment::router(state.clone()))
|
|
||||||
.merge(modules::custom_world::router(state.clone()))
|
|
||||||
.merge(modules::big_fish::router(state.clone()))
|
|
||||||
.merge(modules::bark_battle::router(state.clone()))
|
|
||||||
.merge(modules::match3d::router(state.clone()))
|
|
||||||
.merge(modules::square_hole::router(state.clone()))
|
|
||||||
.merge(modules::jump_hop::router(state.clone()))
|
|
||||||
.merge(modules::wooden_fish::router(state.clone()))
|
|
||||||
.merge(modules::public_work::router(state.clone()))
|
|
||||||
.merge(modules::puzzle_clear::router(state.clone()))
|
|
||||||
.merge(modules::puzzle::router(state.clone()))
|
|
||||||
.merge(visual_novel_router(state.clone()))
|
|
||||||
.route(
|
.route(
|
||||||
"/api/profile/recharge/wechat/notify",
|
"/api/profile/recharge/wechat/notify",
|
||||||
post(handle_wechat_pay_notify),
|
post(handle_wechat_pay_notify),
|
||||||
@@ -79,13 +52,6 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
get(handle_wechat_virtual_payment_message_push_verify)
|
get(handle_wechat_virtual_payment_message_push_verify)
|
||||||
.post(handle_wechat_virtual_payment_notify),
|
.post(handle_wechat_virtual_payment_notify),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/runtime/sessions/{runtime_session_id}/inventory",
|
|
||||||
get(get_runtime_inventory_state).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
// 后端创作/运行态 API 路由只按 open 做熔断;visible 仅控制创作页入口展示。
|
// 后端创作/运行态 API 路由只按 open 做熔断;visible 仅控制创作页入口展示。
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
@@ -290,166 +256,6 @@ async fn record_api_tracking_after_success(
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visual_novel_router(state: AppState) -> Router<AppState> {
|
|
||||||
Router::new()
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions",
|
|
||||||
post(create_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions/{session_id}",
|
|
||||||
get(get_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions/{session_id}/messages",
|
|
||||||
post(submit_visual_novel_message).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions/{session_id}/messages/stream",
|
|
||||||
post(stream_visual_novel_message).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions/{session_id}/actions",
|
|
||||||
post(execute_visual_novel_action).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/sessions/{session_id}/compile",
|
|
||||||
post(compile_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/works",
|
|
||||||
get(list_visual_novel_works).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/works/{profile_id}",
|
|
||||||
get(get_visual_novel_work)
|
|
||||||
.put(update_visual_novel_work)
|
|
||||||
.patch(update_visual_novel_work)
|
|
||||||
.delete(delete_visual_novel_work)
|
|
||||||
.route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/works/{profile_id}/publish",
|
|
||||||
post(publish_visual_novel_work).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/audio/background-music",
|
|
||||||
post(create_visual_novel_background_music_task).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/audio/background-music/{task_id}/asset",
|
|
||||||
post(publish_visual_novel_background_music_asset).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/audio/sound-effect",
|
|
||||||
post(create_visual_novel_sound_effect_task).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/visual-novel/audio/sound-effect/{task_id}/asset",
|
|
||||||
post(publish_visual_novel_sound_effect_asset).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/audio/background-music",
|
|
||||||
post(create_background_music_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/audio/background-music/{task_id}/asset",
|
|
||||||
post(publish_background_music_asset).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/audio/sound-effect",
|
|
||||||
post(create_sound_effect_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation/audio/sound-effect/{task_id}/asset",
|
|
||||||
post(publish_sound_effect_asset).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/gallery",
|
|
||||||
get(list_visual_novel_gallery),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/works/{profile_id}/runs",
|
|
||||||
post(start_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/runs/{run_id}",
|
|
||||||
get(get_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/runs/{run_id}/actions/stream",
|
|
||||||
post(stream_visual_novel_action).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/runs/{run_id}/history",
|
|
||||||
get(list_visual_novel_history).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/visual-novel/runs/{run_id}/regenerate",
|
|
||||||
post(regenerate_visual_novel_run)
|
|
||||||
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use serde_json::{Value, json};
|
|||||||
use module_runtime::build_creation_entry_config_response;
|
use module_runtime::build_creation_entry_config_response;
|
||||||
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||||
|
|
||||||
|
pub use crate::modules::play_flow::resolve_creation_entry_route_id;
|
||||||
use crate::{
|
use crate::{
|
||||||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -70,62 +71,6 @@ pub async fn require_creation_entry_route_enabled(
|
|||||||
next.run(request).await
|
next.run(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
|
|
||||||
let normalized = path.trim_end_matches('/');
|
|
||||||
if normalized == "/api/runtime/puzzle/agent/sessions"
|
|
||||||
|| normalized == "/api/runtime/puzzle/onboarding/generate"
|
|
||||||
{
|
|
||||||
return Some("puzzle");
|
|
||||||
}
|
|
||||||
if normalized.starts_with("/api/runtime/puzzle/gallery/") && normalized.ends_with("/remix") {
|
|
||||||
return Some("puzzle");
|
|
||||||
}
|
|
||||||
if normalized == "/api/runtime/big-fish/agent/sessions" {
|
|
||||||
return Some("big-fish");
|
|
||||||
}
|
|
||||||
if normalized.starts_with("/api/runtime/big-fish/gallery/") && normalized.ends_with("/remix") {
|
|
||||||
return Some("big-fish");
|
|
||||||
}
|
|
||||||
if normalized == "/api/runtime/custom-world/agent/sessions"
|
|
||||||
|| normalized == "/api/runtime/custom-world/profile"
|
|
||||||
{
|
|
||||||
return Some("rpg");
|
|
||||||
}
|
|
||||||
if normalized.starts_with("/api/runtime/custom-world-gallery/")
|
|
||||||
&& normalized.ends_with("/remix")
|
|
||||||
{
|
|
||||||
return Some("rpg");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/match3d/sessions" {
|
|
||||||
return Some("match3d");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/square-hole/sessions" {
|
|
||||||
return Some("square-hole");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/bark-battle/drafts" {
|
|
||||||
return Some("bark-battle");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/wooden-fish/sessions" {
|
|
||||||
return Some("wooden-fish");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/jump-hop/sessions" {
|
|
||||||
return Some("jump-hop");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/puzzle-clear/sessions" {
|
|
||||||
return Some("puzzle-clear");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/visual-novel/sessions" {
|
|
||||||
return Some("visual-novel");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/edutainment/baby-object-match/assets" {
|
|
||||||
return Some("baby-object-match");
|
|
||||||
}
|
|
||||||
if normalized == "/api/creation/edutainment/baby-love-drawing/magic" {
|
|
||||||
return Some("baby-love-drawing");
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn resolve_creation_entry_mud_point_cost_from_config(
|
pub(crate) fn resolve_creation_entry_mud_point_cost_from_config(
|
||||||
config: &CreationEntryConfigResponse,
|
config: &CreationEntryConfigResponse,
|
||||||
creation_type_id: &str,
|
creation_type_id: &str,
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ pub mod internal;
|
|||||||
pub mod jump_hop;
|
pub mod jump_hop;
|
||||||
pub mod match3d;
|
pub mod match3d;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
pub mod play_flow;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod public_work;
|
pub mod public_work;
|
||||||
pub mod puzzle;
|
pub mod puzzle;
|
||||||
pub mod puzzle_clear;
|
pub mod puzzle_clear;
|
||||||
pub mod square_hole;
|
pub mod square_hole;
|
||||||
pub mod story;
|
pub mod story;
|
||||||
|
pub mod visual_novel;
|
||||||
pub mod wooden_fish;
|
pub mod wooden_fish;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
assets::{
|
assets::{
|
||||||
bind_asset_object_to_entity, confirm_asset_object, create_direct_upload_ticket,
|
bind_asset_object_to_entity, confirm_asset_object, create_direct_upload_ticket,
|
||||||
create_sts_upload_credentials, get_asset_history, get_asset_read_bytes, get_asset_read_url,
|
create_sts_upload_credentials, get_asset_read_bytes, get_asset_read_url,
|
||||||
},
|
},
|
||||||
auth::require_bearer_auth,
|
auth::require_bearer_auth,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -44,11 +44,4 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
)
|
)
|
||||||
.route("/api/assets/read-url", get(get_asset_read_url))
|
.route("/api/assets/read-url", get(get_asset_read_url))
|
||||||
.route("/api/assets/read-bytes", get(get_asset_read_bytes))
|
.route("/api/assets/read-bytes", get(get_asset_read_bytes))
|
||||||
.route(
|
|
||||||
"/api/assets/history",
|
|
||||||
get(get_asset_history).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,11 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router, middleware,
|
||||||
extract::DefaultBodyLimit,
|
|
||||||
middleware,
|
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ai_tasks::{
|
|
||||||
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
|
|
||||||
complete_ai_task, create_ai_task, fail_ai_task, start_ai_task, start_ai_task_stage,
|
|
||||||
},
|
|
||||||
auth::require_bearer_auth,
|
auth::require_bearer_auth,
|
||||||
character_animation_assets::{
|
|
||||||
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
|
||||||
import_character_animation_video, list_character_animation_templates,
|
|
||||||
publish_character_animation, put_role_asset_workflow, resolve_role_asset_workflow,
|
|
||||||
save_character_workflow_cache,
|
|
||||||
},
|
|
||||||
character_visual_assets::{
|
|
||||||
generate_character_visual, get_character_visual_job, publish_character_visual,
|
|
||||||
},
|
|
||||||
creation_agent_document_input::parse_creation_agent_document_input,
|
|
||||||
creation_entry_config::get_creation_entry_config_handler,
|
|
||||||
hyper3d_generation::{
|
|
||||||
get_hyper3d_downloads, get_hyper3d_task_status, submit_hyper3d_image_to_model,
|
|
||||||
submit_hyper3d_text_to_model,
|
|
||||||
},
|
|
||||||
llm::proxy_llm_chat_completions,
|
llm::proxy_llm_chat_completions,
|
||||||
runtime_chat::stream_runtime_npc_chat_turn,
|
|
||||||
runtime_chat_plain::{
|
|
||||||
generate_runtime_character_chat_suggestions, generate_runtime_character_chat_summary,
|
|
||||||
stream_runtime_character_chat_reply, stream_runtime_npc_chat_dialogue,
|
|
||||||
stream_runtime_npc_recruit_dialogue,
|
|
||||||
},
|
|
||||||
runtime_save::{delete_runtime_snapshot, get_runtime_snapshot, put_runtime_snapshot},
|
|
||||||
runtime_settings::{get_runtime_settings, put_runtime_settings},
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
volcengine_speech::{
|
volcengine_speech::{
|
||||||
get_volcengine_speech_config, stream_volcengine_asr, stream_volcengine_tts_bidirection,
|
get_volcengine_speech_config, stream_volcengine_asr, stream_volcengine_tts_bidirection,
|
||||||
@@ -42,8 +13,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES: usize = 56 * 1024 * 1024;
|
|
||||||
|
|
||||||
pub fn router(state: AppState) -> Router<AppState> {
|
pub fn router(state: AppState) -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
@@ -81,213 +50,4 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/character/suggestions",
|
|
||||||
post(generate_runtime_character_chat_suggestions).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/character/summary",
|
|
||||||
post(generate_runtime_character_chat_summary).route_layer(
|
|
||||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/character/reply/stream",
|
|
||||||
post(stream_runtime_character_chat_reply).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/npc/dialogue/stream",
|
|
||||||
post(stream_runtime_npc_chat_dialogue).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/npc/turn/stream",
|
|
||||||
post(stream_runtime_npc_chat_turn).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/chat/npc/recruit/stream",
|
|
||||||
post(stream_runtime_npc_recruit_dialogue).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/creation-agent/document-inputs/parse",
|
|
||||||
post(parse_creation_agent_document_input).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks",
|
|
||||||
post(create_ai_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/start",
|
|
||||||
post(start_ai_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/stages/{stage_kind}/start",
|
|
||||||
post(start_ai_task_stage).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/chunks",
|
|
||||||
post(append_ai_text_chunk).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/stages/{stage_kind}/complete",
|
|
||||||
post(complete_ai_stage).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/references",
|
|
||||||
post(attach_ai_result_reference).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/complete",
|
|
||||||
post(complete_ai_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/fail",
|
|
||||||
post(fail_ai_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/ai/tasks/{task_id}/cancel",
|
|
||||||
post(cancel_ai_task).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-visual/generate",
|
|
||||||
post(generate_character_visual),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-visual/jobs/{task_id}",
|
|
||||||
get(get_character_visual_job),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-visual/publish",
|
|
||||||
post(publish_character_visual),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-animation/generate",
|
|
||||||
post(generate_character_animation),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-animation/jobs/{task_id}",
|
|
||||||
get(get_character_animation_job),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-animation/publish",
|
|
||||||
post(publish_character_animation),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-animation/import-video",
|
|
||||||
post(import_character_animation_video),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-animation/templates",
|
|
||||||
get(list_character_animation_templates),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-workflow-cache",
|
|
||||||
post(save_character_workflow_cache),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/character-workflow-cache/{character_id}",
|
|
||||||
get(get_character_workflow_cache),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/custom-world/asset-studio/role/{character_id}/workflow",
|
|
||||||
post(resolve_role_asset_workflow).put(put_role_asset_workflow),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/hyper3d/text-to-model",
|
|
||||||
post(submit_hyper3d_text_to_model).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/hyper3d/image-to-model",
|
|
||||||
post(submit_hyper3d_image_to_model)
|
|
||||||
.layer(DefaultBodyLimit::max(
|
|
||||||
HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES,
|
|
||||||
))
|
|
||||||
.route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/hyper3d/status",
|
|
||||||
post(get_hyper3d_task_status).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/assets/hyper3d/download",
|
|
||||||
post(get_hyper3d_downloads).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/creation-entry/config",
|
|
||||||
get(get_creation_entry_config_handler),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/settings",
|
|
||||||
get(get_runtime_settings)
|
|
||||||
.put(put_runtime_settings)
|
|
||||||
.route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/runtime/save/snapshot",
|
|
||||||
get(get_runtime_snapshot)
|
|
||||||
.put(put_runtime_snapshot)
|
|
||||||
.delete(delete_runtime_snapshot)
|
|
||||||
.route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
1028
server-rs/crates/api-server/src/modules/play_flow.rs
Normal file
1028
server-rs/crates/api-server/src/modules/play_flow.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,18 +6,13 @@ use axum::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
auth::require_bearer_auth,
|
auth::require_bearer_auth,
|
||||||
profile_identity::update_profile_identity,
|
profile_identity::update_profile_identity,
|
||||||
runtime_browse_history::{
|
|
||||||
delete_runtime_browse_history, get_runtime_browse_history, post_runtime_browse_history,
|
|
||||||
},
|
|
||||||
runtime_profile::{
|
runtime_profile::{
|
||||||
claim_profile_task_reward, confirm_wechat_profile_recharge_order,
|
claim_profile_task_reward, confirm_wechat_profile_recharge_order,
|
||||||
create_profile_recharge_order, get_profile_analytics_metric, get_profile_dashboard,
|
create_profile_recharge_order, get_profile_analytics_metric, get_profile_dashboard,
|
||||||
get_profile_play_stats, get_profile_recharge_center, get_profile_referral_invite_center,
|
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_task_center,
|
||||||
get_profile_task_center, get_profile_wallet_ledger, redeem_profile_referral_invite_code,
|
get_profile_wallet_ledger, redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||||
redeem_profile_reward_code, stream_wechat_profile_recharge_order_events,
|
stream_wechat_profile_recharge_order_events, submit_profile_feedback,
|
||||||
submit_profile_feedback,
|
|
||||||
},
|
},
|
||||||
runtime_save::{list_profile_save_archives, resume_profile_save_archive},
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,16 +25,6 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/profile/browse-history",
|
|
||||||
get(get_runtime_browse_history)
|
|
||||||
.post(post_runtime_browse_history)
|
|
||||||
.delete(delete_runtime_browse_history)
|
|
||||||
.route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/profile/dashboard",
|
"/api/profile/dashboard",
|
||||||
get(get_profile_dashboard).route_layer(middleware::from_fn_with_state(
|
get(get_profile_dashboard).route_layer(middleware::from_fn_with_state(
|
||||||
@@ -131,25 +116,4 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/profile/save-archives",
|
|
||||||
get(list_profile_save_archives).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/profile/save-archives/{world_key}",
|
|
||||||
post(resume_profile_save_archive).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/profile/play-stats",
|
|
||||||
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
require_bearer_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
183
server-rs/crates/api-server/src/modules/visual_novel.rs
Normal file
183
server-rs/crates/api-server/src/modules/visual_novel.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
use axum::{
|
||||||
|
Router, middleware,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::require_bearer_auth,
|
||||||
|
state::AppState,
|
||||||
|
vector_engine_audio_generation::{
|
||||||
|
create_background_music_task, create_sound_effect_task,
|
||||||
|
create_visual_novel_background_music_task, create_visual_novel_sound_effect_task,
|
||||||
|
publish_background_music_asset, publish_sound_effect_asset,
|
||||||
|
publish_visual_novel_background_music_asset, publish_visual_novel_sound_effect_asset,
|
||||||
|
},
|
||||||
|
visual_novel::{
|
||||||
|
compile_visual_novel_session, create_visual_novel_session, delete_visual_novel_work,
|
||||||
|
execute_visual_novel_action, get_visual_novel_run, get_visual_novel_session,
|
||||||
|
get_visual_novel_work, list_visual_novel_gallery, list_visual_novel_history,
|
||||||
|
list_visual_novel_works, publish_visual_novel_work, regenerate_visual_novel_run,
|
||||||
|
start_visual_novel_run, stream_visual_novel_action, stream_visual_novel_message,
|
||||||
|
submit_visual_novel_message, update_visual_novel_work,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn router(state: AppState) -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions",
|
||||||
|
post(create_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions/{session_id}",
|
||||||
|
get(get_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions/{session_id}/messages",
|
||||||
|
post(submit_visual_novel_message).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions/{session_id}/messages/stream",
|
||||||
|
post(stream_visual_novel_message).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions/{session_id}/actions",
|
||||||
|
post(execute_visual_novel_action).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/sessions/{session_id}/compile",
|
||||||
|
post(compile_visual_novel_session).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/works",
|
||||||
|
get(list_visual_novel_works).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/works/{profile_id}",
|
||||||
|
get(get_visual_novel_work)
|
||||||
|
.put(update_visual_novel_work)
|
||||||
|
.patch(update_visual_novel_work)
|
||||||
|
.delete(delete_visual_novel_work)
|
||||||
|
.route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/works/{profile_id}/publish",
|
||||||
|
post(publish_visual_novel_work).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/audio/background-music",
|
||||||
|
post(create_visual_novel_background_music_task).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/audio/background-music/{task_id}/asset",
|
||||||
|
post(publish_visual_novel_background_music_asset).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/audio/sound-effect",
|
||||||
|
post(create_visual_novel_sound_effect_task).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/visual-novel/audio/sound-effect/{task_id}/asset",
|
||||||
|
post(publish_visual_novel_sound_effect_asset).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/audio/background-music",
|
||||||
|
post(create_background_music_task).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/audio/background-music/{task_id}/asset",
|
||||||
|
post(publish_background_music_asset).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/audio/sound-effect",
|
||||||
|
post(create_sound_effect_task).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/creation/audio/sound-effect/{task_id}/asset",
|
||||||
|
post(publish_sound_effect_asset).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/gallery",
|
||||||
|
get(list_visual_novel_gallery),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/works/{profile_id}/runs",
|
||||||
|
post(start_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/runs/{run_id}",
|
||||||
|
get(get_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/runs/{run_id}/actions/stream",
|
||||||
|
post(stream_visual_novel_action).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/runs/{run_id}/history",
|
||||||
|
get(list_visual_novel_history).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/visual-novel/runs/{run_id}/regenerate",
|
||||||
|
post(regenerate_visual_novel_run)
|
||||||
|
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -270,8 +270,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_browse_history_rejects_blank_required_fields() {
|
async fn runtime_browse_history_rejects_blank_required_fields() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -316,8 +316,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_browse_history_accepts_batch_shape_and_surfaces_backend_failure_as_bad_gateway()
|
async fn runtime_browse_history_accepts_batch_shape_and_surfaces_backend_failure_as_bad_gateway()
|
||||||
{
|
{
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -361,23 +361,21 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_authenticated_state() -> AppState {
|
async fn seed_authenticated_state() -> (AppState, String) {
|
||||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||||
state
|
let user_id = state
|
||||||
.seed_test_phone_user_with_password("13800138102", "secret123")
|
.seed_test_phone_user_with_password("13800138102", "secret123")
|
||||||
.await
|
.await
|
||||||
.id;
|
.id;
|
||||||
state
|
(state, user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_access_token(state: &AppState) -> String {
|
fn issue_access_token(state: &AppState, user_id: &str) -> String {
|
||||||
let claims = AccessTokenClaims::from_input(
|
let claims = AccessTokenClaims::from_input(
|
||||||
AccessTokenClaimsInput {
|
AccessTokenClaimsInput {
|
||||||
user_id: "user_00000001".to_string(),
|
user_id: user_id.to_string(),
|
||||||
session_id: state.seed_test_refresh_session_for_user_id(
|
session_id: state
|
||||||
"user_00000001",
|
.seed_test_refresh_session_for_user_id(user_id, "sess_runtime_browse_history"),
|
||||||
"sess_runtime_browse_history",
|
|
||||||
),
|
|
||||||
provider: AuthProvider::Password,
|
provider: AuthProvider::Password,
|
||||||
roles: vec!["user".to_string()],
|
roles: vec!["user".to_string()],
|
||||||
token_version: 2,
|
token_version: 2,
|
||||||
|
|||||||
@@ -347,8 +347,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_snapshot_checkpoint_rejects_legacy_full_snapshot_upload() {
|
async fn runtime_snapshot_checkpoint_rejects_legacy_full_snapshot_upload() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -379,8 +379,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_snapshot_checkpoint_requires_existing_server_snapshot() {
|
async fn runtime_snapshot_checkpoint_requires_existing_server_snapshot() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -407,9 +407,9 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_snapshot_checkpoint_rejects_session_mismatch() {
|
async fn runtime_snapshot_checkpoint_rejects_session_mismatch() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
seed_runtime_snapshot(&state, "runtime-server", "adventure").await;
|
seed_runtime_snapshot(&state, user_id.as_str(), "runtime-server", "adventure").await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -436,9 +436,9 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_snapshot_checkpoint_uses_persisted_server_snapshot() {
|
async fn runtime_snapshot_checkpoint_uses_persisted_server_snapshot() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
seed_runtime_snapshot(&state, "runtime-main", "adventure").await;
|
seed_runtime_snapshot(&state, user_id.as_str(), "runtime-main", "adventure").await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -509,8 +509,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resume_profile_save_archive_rejects_blank_world_key() {
|
async fn resume_profile_save_archive_rejects_blank_world_key() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -529,21 +529,26 @@ mod tests {
|
|||||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_authenticated_state() -> AppState {
|
async fn seed_authenticated_state() -> (AppState, String) {
|
||||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||||
state
|
let user_id = state
|
||||||
.seed_test_phone_user_with_password("13800138105", "secret123")
|
.seed_test_phone_user_with_password("13800138105", "secret123")
|
||||||
.await
|
.await
|
||||||
.id;
|
.id;
|
||||||
state
|
(state, user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_runtime_snapshot(state: &AppState, session_id: &str, bottom_tab: &str) {
|
async fn seed_runtime_snapshot(
|
||||||
|
state: &AppState,
|
||||||
|
user_id: &str,
|
||||||
|
session_id: &str,
|
||||||
|
bottom_tab: &str,
|
||||||
|
) {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let now_micros = shared_kernel::offset_datetime_to_unix_micros(now);
|
let now_micros = shared_kernel::offset_datetime_to_unix_micros(now);
|
||||||
state
|
state
|
||||||
.put_runtime_snapshot_record(
|
.put_runtime_snapshot_record(
|
||||||
"user_00000001".to_string(),
|
user_id.to_string(),
|
||||||
now_micros - 2_000_000,
|
now_micros - 2_000_000,
|
||||||
bottom_tab.to_string(),
|
bottom_tab.to_string(),
|
||||||
json!({
|
json!({
|
||||||
@@ -571,12 +576,12 @@ mod tests {
|
|||||||
.expect("runtime snapshot should seed");
|
.expect("runtime snapshot should seed");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_access_token(state: &AppState) -> String {
|
fn issue_access_token(state: &AppState, user_id: &str) -> String {
|
||||||
let claims = AccessTokenClaims::from_input(
|
let claims = AccessTokenClaims::from_input(
|
||||||
AccessTokenClaimsInput {
|
AccessTokenClaimsInput {
|
||||||
user_id: "user_00000001".to_string(),
|
user_id: user_id.to_string(),
|
||||||
session_id: state
|
session_id: state
|
||||||
.seed_test_refresh_session_for_user_id("user_00000001", "sess_runtime_save"),
|
.seed_test_refresh_session_for_user_id(user_id, "sess_runtime_save"),
|
||||||
provider: AuthProvider::Password,
|
provider: AuthProvider::Password,
|
||||||
roles: vec!["user".to_string()],
|
roles: vec!["user".to_string()],
|
||||||
token_version: 2,
|
token_version: 2,
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_settings_returns_bad_gateway_when_spacetime_not_published() {
|
async fn runtime_settings_returns_bad_gateway_when_spacetime_not_published() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -221,8 +221,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_settings_rejects_invalid_theme_with_envelope() {
|
async fn runtime_settings_rejects_invalid_theme_with_envelope() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
@@ -266,8 +266,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore = "需要本地 SpacetimeDB xushi-p4wfr 已启动并发布当前 module;验证 PUT/GET settings 主链"]
|
#[ignore = "需要本地 SpacetimeDB xushi-p4wfr 已启动并发布当前 module;验证 PUT/GET settings 主链"]
|
||||||
async fn runtime_settings_round_trip_against_local_spacetimedb() {
|
async fn runtime_settings_round_trip_against_local_spacetimedb() {
|
||||||
let state = seed_authenticated_state().await;
|
let (state, user_id) = seed_authenticated_state().await;
|
||||||
let token = issue_access_token(&state);
|
let token = issue_access_token(&state, user_id.as_str());
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
|
|
||||||
let put_response = app
|
let put_response = app
|
||||||
@@ -337,23 +337,21 @@ mod tests {
|
|||||||
assert_eq!(get_payload["data"]["musicVolume"], json!(1.0));
|
assert_eq!(get_payload["data"]["musicVolume"], json!(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn seed_authenticated_state() -> AppState {
|
async fn seed_authenticated_state() -> (AppState, String) {
|
||||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||||
state
|
let user_id = state
|
||||||
.seed_test_phone_user_with_password("13800138106", "secret123")
|
.seed_test_phone_user_with_password("13800138106", "secret123")
|
||||||
.await
|
.await
|
||||||
.id;
|
.id;
|
||||||
state
|
(state, user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_access_token(state: &AppState) -> String {
|
fn issue_access_token(state: &AppState, user_id: &str) -> String {
|
||||||
let claims = AccessTokenClaims::from_input(
|
let claims = AccessTokenClaims::from_input(
|
||||||
AccessTokenClaimsInput {
|
AccessTokenClaimsInput {
|
||||||
user_id: "user_00000001".to_string(),
|
user_id: user_id.to_string(),
|
||||||
session_id: state.seed_test_refresh_session_for_user_id(
|
session_id: state
|
||||||
"user_00000001",
|
.seed_test_refresh_session_for_user_id(user_id, "sess_runtime_settings"),
|
||||||
"sess_runtime_settings",
|
|
||||||
),
|
|
||||||
provider: AuthProvider::Password,
|
provider: AuthProvider::Password,
|
||||||
roles: vec!["user".to_string()],
|
roles: vec!["user".to_string()],
|
||||||
token_version: 2,
|
token_version: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user