This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -1,7 +1,7 @@
use axum::{
Router,
body::Body,
extract::Extension,
extract::{DefaultBodyLimit, Extension},
http::Request,
middleware,
routing::{delete, get, post},
@@ -34,8 +34,9 @@ use crate::{
auth_sessions::auth_sessions,
big_fish::{
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, record_big_fish_play,
remix_big_fish_gallery_work, stream_big_fish_message, submit_big_fish_message,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery,
record_big_fish_gallery_like, record_big_fish_play, remix_big_fish_gallery_work,
stream_big_fish_message, submit_big_fish_message,
},
character_animation_assets::{
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
@@ -56,9 +57,9 @@ use crate::{
get_custom_world_gallery_detail_by_code, get_custom_world_library,
get_custom_world_library_detail, get_custom_world_works, list_custom_world_gallery,
publish_custom_world_library_profile, put_custom_world_library_profile,
record_custom_world_gallery_play, remix_custom_world_gallery_profile,
stream_custom_world_agent_message, submit_custom_world_agent_message,
unpublish_custom_world_library_profile,
record_custom_world_gallery_like, record_custom_world_gallery_play,
remix_custom_world_gallery_profile, stream_custom_world_agent_message,
submit_custom_world_agent_message, unpublish_custom_world_library_profile,
},
custom_world_ai::{
generate_custom_world_cover_image, generate_custom_world_entity,
@@ -85,9 +86,10 @@ use crate::{
advance_local_puzzle_next_level, advance_puzzle_next_level, create_puzzle_agent_session,
delete_puzzle_work, execute_puzzle_agent_action, get_puzzle_agent_session,
get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_work_detail, get_puzzle_works,
list_puzzle_gallery, put_puzzle_work, remix_puzzle_gallery_work, start_puzzle_run,
stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard,
swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop,
list_puzzle_gallery, put_puzzle_work, record_puzzle_gallery_like,
remix_puzzle_gallery_work, start_puzzle_run, stream_puzzle_agent_message,
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
update_puzzle_run_pause, use_puzzle_runtime_prop,
},
refresh_session::refresh_session,
request_context::{attach_request_context, resolve_request_id},
@@ -126,6 +128,8 @@ use crate::{
wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login},
};
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
pub fn build_router(state: AppState) -> Router {
let slow_request_threshold_ms = state.config.slow_request_threshold_ms;
@@ -544,6 +548,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/like",
post(record_custom_world_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/custom-world-gallery/by-code/{code}",
get(get_custom_world_gallery_detail_by_code),
@@ -663,6 +674,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/gallery/{session_id}/like",
post(record_big_fish_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/works/{session_id}",
delete(delete_big_fish_work).route_layer(middleware::from_fn_with_state(
@@ -686,10 +704,15 @@ pub fn build_router(state: AppState) -> Router {
)
.route(
"/api/runtime/puzzle/agent/sessions",
post(create_puzzle_agent_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(create_puzzle_agent_session)
// 中文注释:拼图表单会携带单张参考图 Data URL需只给该写入入口放宽 body 上限。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/agent/sessions/{session_id}",
@@ -714,10 +737,15 @@ pub fn build_router(state: AppState) -> Router {
)
.route(
"/api/runtime/puzzle/agent/sessions/{session_id}/actions",
post(execute_puzzle_agent_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(execute_puzzle_agent_action)
// 中文注释:生成草稿/重新出图会复用 referenceImageSrc避免默认 2MB JSON limit 拦截。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/works",
@@ -748,6 +776,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/gallery/{profile_id}/like",
post(record_puzzle_gallery_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs",
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
@@ -1239,6 +1274,30 @@ mod tests {
.await
}
fn sign_test_user_token(
state: &AppState,
user: &module_auth::AuthUser,
session_id: &str,
) -> String {
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: user.id.clone(),
session_id: session_id.to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: user.token_version,
phone_verified: false,
binding_status: BindingStatus::Active,
display_name: Some(user.display_name.clone()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
)
.expect("claims should build");
sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign")
}
async fn password_login_request(
app: Router,
phone_number: &str,
@@ -1496,6 +1555,88 @@ mod tests {
);
}
#[tokio::test]
async fn puzzle_agent_actions_accept_reference_image_body_above_default_limit() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138024", TEST_PASSWORD).await;
let token = sign_test_user_token(&state, &seed_user, "sess_puzzle_reference_body");
let app = build_router(state);
let reference_image_src = format!("data:image/png;base64,{}", "A".repeat(3 * 1024 * 1024));
let request_body = serde_json::json!({
"action": "unsupported_large_reference_test",
"referenceImageSrc": reference_image_src,
})
.to_string();
assert!(request_body.len() > 2 * 1024 * 1024);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions/puzzle-session-large/actions")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from(request_body))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("unsupported_large_reference_test"),
"handler should parse the oversized reference payload before rejecting the action: {body_text}"
);
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn puzzle_agent_session_creation_accepts_reference_image_body_above_default_limit() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138025", TEST_PASSWORD).await;
let token = sign_test_user_token(&state, &seed_user, "sess_puzzle_form_reference_body");
let app = build_router(state);
let request_body = format!(
"{{\"seedText\":\"大参考图拼图\",\"pictureDescription\":\"一张用于验证 body limit 的参考图。\",\"referenceImageSrc\":\"data:image/png;base64,{}\"",
"A".repeat(3 * 1024 * 1024)
);
assert!(request_body.len() > 2 * 1024 * 1024);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from(request_body))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("EOF") || body_text.contains("expected"),
"handler should parse the oversized form payload before rejecting malformed JSON: {body_text}"
);
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn password_entry_rejects_unknown_phone_without_registration() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));