收口后端创作游玩流程主干

新增 play_flow 统一承接创作游玩与支撑路由
将 app.rs 的逐玩法挂载改为统一主干分发
将平台与资产及个人侧游玩支撑路由迁入 play_flow
抽出 visual_novel 路由模块并复用原有 handler
统一入口熔断路径解析并补充目标回归测试
更新后端契约、玩法链路和团队决策记录
This commit is contained in:
2026-06-09 15:33:57 +08:00
parent facbb2074c
commit 7eae91d7d3
15 changed files with 1312 additions and 609 deletions

View File

@@ -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`
- 关联文档:`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 渐隐
- 背景:移动端推荐页上下切换公开作品时,如果运行态和封面资源没有明确准备边界,用户会看到未加载完成的 runtime、黑底闪动或切卡后反向回弹。

View File

@@ -56,10 +56,11 @@ npm run check:server-rs-ddd
- 健康检查:`GET /healthz`
- 后台管理:`/admin/api/*`包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、作品可见性、兑换码、邀请码、任务配置和充值商品配置。
- 认证与账号:`/api/auth/*``/api/profile/me`包括短信、密码、微信、refresh session、多端会话和登出。
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请兑换、存档、历史浏览和游玩统计
- LLM 与语音`/api/llm/*``/api/speech/volcengine/*`
- 资产:`/api/assets/*`包括直传票据、STS、对象确认、实体绑定、读签名、读 bytes、历史资产、角色图像/动画和 Hyper3D 代理
- 创作入口配置`/api/creation-entry/config`,后台 `/admin/api/creation-entry/config``/admin/api/creation-entry/config/banners`
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请兑换等账号侧能力
- 平台基础能力`/api/llm/*``/api/speech/volcengine/*`,只保留通用 LLM 和语音代理
- 资产基础能力`/api/assets/direct-upload-tickets``/api/assets/sts-upload-credentials``/api/assets/objects/*``/api/assets/read-*`,负责直传、确认、绑定和读取
- 创作 / 游玩支撑能力`/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/*`
- 拼图:`/api/runtime/puzzle/*`
- 抓大鹅 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/runtime/bark-battle/*`
- 儿童向创作:`/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(...)`
2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue。
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;不得重新恢复逐玩法 creation/runtime merge 列表
3. 能力 Module 可在路由内部用 `FromRef<AppState>` 派生自己的 Feature State例如 `PuzzleApiState`。全局 `AppState` 仍作为进程组合根、鉴权层和全局中间件状态,但业务 handler 优先只提取对应 Feature State不直接暴露完整 `AppState`
4. Feature State 只暴露该能力实际需要的 facade / adapter / 配置快照;若必须复用仍要求 `AppState` 的横切 helper例如计费、外部失败审计或通用 tracking应通过 Feature State 的窄方法或显式 `root_state()` 过渡,并在后续继续收窄。
5. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现,再收窄 handler 可见状态。

View File

@@ -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 和本文档写明例外原因、影响范围和回退方式,再进入编码。
单图资产编辑统一通过 `CreativeImageInputPanel` 承载上传、AI 重绘、参考图、历史图、主图预览和删除确认;新玩法页面不得重复手写这些交互。主图已有图片时,默认点击图片打开全屏预览,上传 / 更换收口到右下角 `ImagePlus` 图标按钮;无图时仍允许点击空图卡上传。调用方只能通过 `canUploadMainImage``canUseImageHistory` 等受控参数开关上传和历史入口,不得用复制组件或样式遮挡改行为。系列素材图集生成统一走“批量规划 -> sheet 生图 -> 后端切图 -> 透明化 -> OSS 持久化 -> 状态回写 -> 局部重生成”流程,玩法只提供 `sheetSpec``slotSpecs`、提示词和字段映射,不把任一玩法专属素材 DTO 当作平台通用模型。

View File

@@ -542,8 +542,8 @@ mod tests {
#[tokio::test]
async fn create_ai_task_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -605,8 +605,8 @@ mod tests {
#[tokio::test]
async fn start_ai_task_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -652,8 +652,8 @@ mod tests {
#[tokio::test]
async fn ai_task_mutation_routes_return_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
for route in ai_task_mutation_route_cases() {
@@ -763,21 +763,20 @@ mod tests {
(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");
state
let user_id = state
.seed_test_phone_user_with_password("13800138100", "secret123")
.await
.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(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
session_id: state
.seed_test_refresh_session_for_user_id("user_00000001", "sess_ai_tasks"),
user_id: user_id.to_string(),
session_id: state.seed_test_refresh_session_for_user_id(user_id, "sess_ai_tasks"),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 2,

View File

@@ -15,7 +15,7 @@ use tower_http::{
use tracing::{Level, Span, error, info_span};
use crate::{
auth::{AuthenticatedAccessToken, require_bearer_auth},
auth::AuthenticatedAccessToken,
backpressure::limit_concurrent_requests,
creation_entry_config::require_creation_entry_route_enabled,
error_middleware::normalize_error_response,
@@ -23,24 +23,9 @@ use crate::{
modules,
request_context::{RequestContext, attach_request_context, resolve_request_id},
response_headers::propagate_request_id_header,
runtime_inventory::get_runtime_inventory_state,
state::{AppState, BackpressureState},
telemetry::record_http_observability,
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::{
handle_wechat_pay_notify, handle_wechat_virtual_payment_message_push_verify,
handle_wechat_virtual_payment_notify,
@@ -57,19 +42,7 @@ pub fn build_router(state: AppState) -> Router {
.merge(modules::profile::router(state.clone()))
.merge(modules::assets::router(state.clone()))
.merge(modules::platform::router(state.clone()))
.merge(modules::story::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()))
.merge(modules::play_flow::router(state.clone()))
.route(
"/api/profile/recharge/wechat/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)
.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 仅控制创作页入口展示。
.layer(middleware::from_fn_with_state(
state.clone(),
@@ -290,166 +256,6 @@ async fn record_api_tracking_after_success(
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)]
mod tests {
use axum::{

View File

@@ -11,6 +11,7 @@ use serde_json::{Value, json};
use module_runtime::build_creation_entry_config_response;
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
pub use crate::modules::play_flow::resolve_creation_entry_route_id;
use crate::{
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
state::AppState,
@@ -70,62 +71,6 @@ pub async fn require_creation_entry_route_enabled(
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(
config: &CreationEntryConfigResponse,
creation_type_id: &str,

View File

@@ -10,10 +10,12 @@ pub mod internal;
pub mod jump_hop;
pub mod match3d;
pub mod platform;
pub mod play_flow;
pub mod profile;
pub mod public_work;
pub mod puzzle;
pub mod puzzle_clear;
pub mod square_hole;
pub mod story;
pub mod visual_novel;
pub mod wooden_fish;

View File

@@ -6,7 +6,7 @@ use axum::{
use crate::{
assets::{
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,
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-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,
)),
)
}

View File

@@ -1,40 +1,11 @@
use axum::{
Router,
extract::DefaultBodyLimit,
middleware,
Router, middleware,
routing::{get, post},
};
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,
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,
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,
volcengine_speech::{
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> {
Router::new()
.route(
@@ -81,213 +50,4 @@ pub fn router(state: AppState) -> Router<AppState> {
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,
)),
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,18 +6,13 @@ use axum::{
use crate::{
auth::require_bearer_auth,
profile_identity::update_profile_identity,
runtime_browse_history::{
delete_runtime_browse_history, get_runtime_browse_history, post_runtime_browse_history,
},
runtime_profile::{
claim_profile_task_reward, confirm_wechat_profile_recharge_order,
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_task_center, get_profile_wallet_ledger, redeem_profile_referral_invite_code,
redeem_profile_reward_code, stream_wechat_profile_recharge_order_events,
submit_profile_feedback,
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_task_center,
get_profile_wallet_ledger, redeem_profile_referral_invite_code, redeem_profile_reward_code,
stream_wechat_profile_recharge_order_events, submit_profile_feedback,
},
runtime_save::{list_profile_save_archives, resume_profile_save_archive},
state::AppState,
};
@@ -30,16 +25,6 @@ pub fn router(state: AppState) -> Router<AppState> {
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(
"/api/profile/dashboard",
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,
)),
)
.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,
)),
)
}

View 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)),
)
}

View File

@@ -270,8 +270,8 @@ mod tests {
#[tokio::test]
async fn runtime_browse_history_rejects_blank_required_fields() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -316,8 +316,8 @@ mod tests {
#[tokio::test]
async fn runtime_browse_history_accepts_batch_shape_and_surfaces_backend_failure_as_bad_gateway()
{
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
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");
state
let user_id = state
.seed_test_phone_user_with_password("13800138102", "secret123")
.await
.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(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
session_id: state.seed_test_refresh_session_for_user_id(
"user_00000001",
"sess_runtime_browse_history",
),
user_id: user_id.to_string(),
session_id: state
.seed_test_refresh_session_for_user_id(user_id, "sess_runtime_browse_history"),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 2,

View File

@@ -347,8 +347,8 @@ mod tests {
#[tokio::test]
async fn runtime_snapshot_checkpoint_rejects_legacy_full_snapshot_upload() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -379,8 +379,8 @@ mod tests {
#[tokio::test]
async fn runtime_snapshot_checkpoint_requires_existing_server_snapshot() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -407,9 +407,9 @@ mod tests {
#[tokio::test]
async fn runtime_snapshot_checkpoint_rejects_session_mismatch() {
let state = seed_authenticated_state().await;
seed_runtime_snapshot(&state, "runtime-server", "adventure").await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
seed_runtime_snapshot(&state, user_id.as_str(), "runtime-server", "adventure").await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -436,9 +436,9 @@ mod tests {
#[tokio::test]
async fn runtime_snapshot_checkpoint_uses_persisted_server_snapshot() {
let state = seed_authenticated_state().await;
seed_runtime_snapshot(&state, "runtime-main", "adventure").await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
seed_runtime_snapshot(&state, user_id.as_str(), "runtime-main", "adventure").await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -509,8 +509,8 @@ mod tests {
#[tokio::test]
async fn resume_profile_save_archive_rejects_blank_world_key() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -529,21 +529,26 @@ mod tests {
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");
state
let user_id = state
.seed_test_phone_user_with_password("13800138105", "secret123")
.await
.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_micros = shared_kernel::offset_datetime_to_unix_micros(now);
state
.put_runtime_snapshot_record(
"user_00000001".to_string(),
user_id.to_string(),
now_micros - 2_000_000,
bottom_tab.to_string(),
json!({
@@ -571,12 +576,12 @@ mod tests {
.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(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
user_id: user_id.to_string(),
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,
roles: vec!["user".to_string()],
token_version: 2,

View File

@@ -184,8 +184,8 @@ mod tests {
#[tokio::test]
async fn runtime_settings_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -221,8 +221,8 @@ mod tests {
#[tokio::test]
async fn runtime_settings_rejects_invalid_theme_with_envelope() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let response = app
@@ -266,8 +266,8 @@ mod tests {
#[tokio::test]
#[ignore = "需要本地 SpacetimeDB xushi-p4wfr 已启动并发布当前 module验证 PUT/GET settings 主链"]
async fn runtime_settings_round_trip_against_local_spacetimedb() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, user_id.as_str());
let app = build_router(state);
let put_response = app
@@ -337,23 +337,21 @@ mod tests {
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");
state
let user_id = state
.seed_test_phone_user_with_password("13800138106", "secret123")
.await
.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(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
session_id: state.seed_test_refresh_session_for_user_id(
"user_00000001",
"sess_runtime_settings",
),
user_id: user_id.to_string(),
session_id: state
.seed_test_refresh_session_for_user_id(user_id, "sess_runtime_settings"),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 2,