Merge remote-tracking branch 'origin/master' into codex/ddd
# Conflicts: # docs/technical/README.md # docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md # docs/technical/SPACETIMEDB_TABLE_CATALOG.md # scripts/generate-spacetime-bindings.mjs # server-rs/crates/api-server/src/app.rs # server-rs/crates/api-server/src/assets.rs # server-rs/crates/api-server/src/big_fish.rs # server-rs/crates/api-server/src/custom_world_ai.rs # server-rs/crates/api-server/src/llm.rs # server-rs/crates/api-server/src/main.rs # server-rs/crates/api-server/src/puzzle.rs # server-rs/crates/api-server/src/runtime_profile.rs # server-rs/crates/api-server/src/runtime_story/compat/ai.rs # server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs # server-rs/crates/api-server/src/runtime_story/compat/presentation.rs # server-rs/crates/api-server/src/runtime_story/compat/tests.rs # server-rs/crates/api-server/src/state.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/module-big-fish/src/lib.rs # server-rs/crates/module-custom-world/src/lib.rs # server-rs/crates/module-puzzle/src/lib.rs # server-rs/crates/module-runtime/src/lib.rs # server-rs/crates/spacetime-client/src/big_fish.rs # server-rs/crates/spacetime-client/src/lib.rs # server-rs/crates/spacetime-client/src/mapper.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs # server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_custom_world_published_profile_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_stage_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/confirm_asset_object_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/consume_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/continue_story_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_battle_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_profile_recharge_order_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_big_fish_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/execute_custom_world_agent_action_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_auth_store_snapshot_from_tables_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_database_migration_to_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/fail_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_big_fish_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_custom_world_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_puzzle_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_battle_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_chapter_progression_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_card_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_operation_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_by_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_library_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_player_progression_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_dashboard_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_play_stats_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_recharge_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_inventory_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_setting_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_story_session_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/grant_player_progression_experience_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_incremental_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_asset_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_big_fish_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_gallery_entries_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_profiles_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_platform_browse_history_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_save_archives_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_wallet_ledger_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/mod.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_world_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/record_big_fish_play_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_reward_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/refund_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_combat_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_battle_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_social_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_treasure_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resume_profile_save_archive_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/revoke_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_custom_world_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_leaderboard_entry_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/unpublish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_chapter_progression_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_agent_operation_progress_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_npc_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_setting_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # server-rs/crates/spacetime-module/src/custom_world/mod.rs # server-rs/crates/spacetime-module/src/lib.rs # server-rs/crates/spacetime-module/src/migration.rs # server-rs/crates/spacetime-module/src/puzzle.rs # server-rs/crates/spacetime-module/src/runtime/profile.rs # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx # src/services/aiService.ts # src/services/puzzle-runtime/puzzleRuntimeClient.ts
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Router,
|
||||
body::Body,
|
||||
extract::Extension,
|
||||
extract::{DefaultBodyLimit, Extension},
|
||||
http::Request,
|
||||
middleware,
|
||||
routing::{delete, get, post},
|
||||
@@ -13,10 +13,7 @@ use tower_http::{
|
||||
use tracing::{Level, Span, error, info, info_span, warn};
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
admin_console_page, admin_debug_http, admin_login, admin_me, admin_overview,
|
||||
require_admin_auth,
|
||||
},
|
||||
admin::{admin_debug_http, admin_login, admin_me, admin_overview, require_admin_auth},
|
||||
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,
|
||||
@@ -34,7 +31,8 @@ use crate::{
|
||||
auth_sessions::auth_sessions,
|
||||
big_fish::{
|
||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
|
||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, record_big_fish_play,
|
||||
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,
|
||||
start_big_fish_run, stream_big_fish_message, submit_big_fish_input,
|
||||
submit_big_fish_message,
|
||||
},
|
||||
@@ -57,8 +55,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,
|
||||
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,
|
||||
@@ -71,15 +70,26 @@ use crate::{
|
||||
login_options::auth_login_options,
|
||||
logout::logout,
|
||||
logout_all::logout_all,
|
||||
match3d::{
|
||||
click_match3d_item, compile_match3d_agent_draft, create_match3d_agent_session,
|
||||
delete_match3d_work, execute_match3d_agent_action, finish_match3d_time_up,
|
||||
get_match3d_agent_session, get_match3d_run, get_match3d_work_detail, get_match3d_works,
|
||||
list_match3d_gallery, publish_match3d_work, put_match3d_work, restart_match3d_run,
|
||||
start_match3d_run, stop_match3d_run, stream_match3d_agent_message,
|
||||
submit_match3d_agent_message,
|
||||
},
|
||||
password_entry::password_entry,
|
||||
password_management::{change_password, reset_password},
|
||||
phone_auth::{phone_login, send_phone_code},
|
||||
profile_identity::update_profile_identity,
|
||||
puzzle::{
|
||||
advance_puzzle_next_level, create_puzzle_agent_session, delete_puzzle_work,
|
||||
drag_puzzle_piece_or_group, 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, start_puzzle_run, stream_puzzle_agent_message,
|
||||
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
|
||||
advance_puzzle_next_level, claim_puzzle_work_point_incentive, create_puzzle_agent_session,
|
||||
delete_puzzle_work, drag_puzzle_piece_or_group, 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,
|
||||
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},
|
||||
@@ -95,10 +105,10 @@ use crate::{
|
||||
},
|
||||
runtime_inventory::get_runtime_inventory_state,
|
||||
runtime_profile::{
|
||||
admin_disable_profile_redeem_code, admin_upsert_profile_redeem_code,
|
||||
create_profile_recharge_order, get_profile_dashboard, get_profile_play_stats,
|
||||
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_wallet_ledger,
|
||||
redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||
admin_disable_profile_redeem_code, admin_upsert_profile_invite_code,
|
||||
admin_upsert_profile_redeem_code, create_profile_recharge_order, get_profile_dashboard,
|
||||
get_profile_play_stats, get_profile_recharge_center, get_profile_referral_invite_center,
|
||||
get_profile_wallet_ledger, redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||
},
|
||||
runtime_save::{
|
||||
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
|
||||
@@ -116,12 +126,13 @@ 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;
|
||||
|
||||
Router::new()
|
||||
.route("/admin", get(admin_console_page))
|
||||
.route("/admin/api/login", post(admin_login))
|
||||
.route(
|
||||
"/admin/api/me",
|
||||
@@ -158,6 +169,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/invite-codes",
|
||||
post(admin_upsert_profile_invite_code).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/healthz",
|
||||
get(|Extension(request_context): Extension<_>| async move {
|
||||
@@ -206,6 +224,12 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/me",
|
||||
axum::routing::patch(update_profile_identity).route_layer(
|
||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/refresh",
|
||||
post(refresh_session).route_layer(middleware::from_fn_with_state(
|
||||
@@ -482,6 +506,27 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}",
|
||||
get(get_custom_world_gallery_detail),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/remix",
|
||||
post(remix_custom_world_gallery_profile).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/play",
|
||||
post(record_custom_world_gallery_play).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
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),
|
||||
@@ -594,6 +639,20 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)),
|
||||
)
|
||||
.route("/api/runtime/big-fish/gallery", get(list_big_fish_gallery))
|
||||
.route(
|
||||
"/api/runtime/big-fish/gallery/{session_id}/remix",
|
||||
post(remix_big_fish_gallery_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
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(
|
||||
@@ -637,12 +696,127 @@ 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(
|
||||
"/api/creation/match3d/sessions",
|
||||
post(create_match3d_agent_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/sessions/{session_id}",
|
||||
get(get_match3d_agent_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/sessions/{session_id}/messages",
|
||||
post(submit_match3d_agent_message).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/sessions/{session_id}/messages/stream",
|
||||
post(stream_match3d_agent_message).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/sessions/{session_id}/actions",
|
||||
post(execute_match3d_agent_action).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/sessions/{session_id}/compile",
|
||||
post(compile_match3d_agent_draft).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/works",
|
||||
get(get_match3d_works).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/works/{profile_id}",
|
||||
get(get_match3d_work_detail)
|
||||
.patch(put_match3d_work)
|
||||
.put(put_match3d_work)
|
||||
.delete(delete_match3d_work)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/works/{profile_id}/publish",
|
||||
post(publish_match3d_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route("/api/runtime/match3d/gallery", get(list_match3d_gallery))
|
||||
.route(
|
||||
"/api/runtime/match3d/works/{profile_id}/runs",
|
||||
post(start_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}",
|
||||
get(get_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/click",
|
||||
post(click_match3d_item).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/stop",
|
||||
post(stop_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/restart",
|
||||
post(restart_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/time-up",
|
||||
post(finish_match3d_time_up).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/agent/sessions",
|
||||
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}",
|
||||
get(get_puzzle_agent_session).route_layer(middleware::from_fn_with_state(
|
||||
@@ -666,10 +840,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",
|
||||
@@ -688,11 +867,32 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/works/{profile_id}/point-incentive/claim",
|
||||
post(claim_puzzle_work_point_incentive).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route("/api/runtime/puzzle/gallery", get(list_puzzle_gallery))
|
||||
.route(
|
||||
"/api/runtime/puzzle/gallery/{profile_id}",
|
||||
get(get_puzzle_gallery_detail),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/gallery/{profile_id}/remix",
|
||||
post(remix_puzzle_gallery_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
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(
|
||||
@@ -728,6 +928,20 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/pause",
|
||||
post(update_puzzle_run_pause).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/props",
|
||||
post(use_puzzle_runtime_prop).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/leaderboard",
|
||||
post(submit_puzzle_leaderboard).route_layer(middleware::from_fn_with_state(
|
||||
@@ -1041,6 +1255,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,
|
||||
@@ -1424,6 +1662,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"));
|
||||
@@ -1494,6 +1814,10 @@ mod tests {
|
||||
payload["user"]["loginMethod"],
|
||||
Value::String("password".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["user"]["createdAt"],
|
||||
Value::String(seed_user.created_at)
|
||||
);
|
||||
assert!(payload["token"].as_str().is_some());
|
||||
}
|
||||
|
||||
@@ -1773,6 +2097,9 @@ mod tests {
|
||||
payload["user"]["phoneNumberMasked"],
|
||||
Value::String("138****8000".to_string())
|
||||
);
|
||||
assert!(payload["user"]["createdAt"].as_str().is_some());
|
||||
assert_eq!(payload["created"], Value::Bool(true));
|
||||
assert!(payload["referral"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1879,6 +2206,175 @@ mod tests {
|
||||
serde_json::from_slice(&second_body).expect("second login payload should be json");
|
||||
|
||||
assert_eq!(first_payload["user"]["id"], second_payload["user"]["id"]);
|
||||
assert_eq!(first_payload["created"], Value::Bool(true));
|
||||
assert_eq!(second_payload["created"], Value::Bool(false));
|
||||
assert!(second_payload["referral"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn phone_login_invite_code_failure_does_not_block_created_user() {
|
||||
let config = AppConfig {
|
||||
sms_auth_enabled: true,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
|
||||
let send_code_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/send-code")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13600136000",
|
||||
"scene": "login"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("send code request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("send code request should succeed");
|
||||
assert_eq!(send_code_response.status(), StatusCode::OK);
|
||||
|
||||
let login_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/login")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13600136000",
|
||||
"code": "123456",
|
||||
"inviteCode": "SPRING2026"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login request should succeed");
|
||||
|
||||
assert_eq!(login_response.status(), StatusCode::OK);
|
||||
let body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("login body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value = serde_json::from_slice(&body).expect("login payload should be json");
|
||||
|
||||
assert!(payload["token"].as_str().is_some());
|
||||
assert_eq!(payload["created"], Value::Bool(true));
|
||||
assert_eq!(payload["referral"]["ok"], Value::Bool(false));
|
||||
assert_eq!(
|
||||
payload["referral"]["message"],
|
||||
Value::String("邀请码无效,已继续注册".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn phone_login_existing_user_ignores_invite_code() {
|
||||
let config = AppConfig {
|
||||
sms_auth_enabled: true,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
|
||||
let first_send_code_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/send-code")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13500135000",
|
||||
"scene": "login"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("send code request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("send code request should succeed");
|
||||
assert_eq!(first_send_code_response.status(), StatusCode::OK);
|
||||
|
||||
let first_login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/login")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13500135000",
|
||||
"code": "123456"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("first login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("first login request should succeed");
|
||||
assert_eq!(first_login_response.status(), StatusCode::OK);
|
||||
|
||||
let second_send_code_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/send-code")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13500135000",
|
||||
"scene": "login"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("send code request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("send code request should succeed");
|
||||
assert_eq!(second_send_code_response.status(), StatusCode::OK);
|
||||
|
||||
let second_login_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/phone/login")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": "13500135000",
|
||||
"code": "123456",
|
||||
"inviteCode": "SPRING2026"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("second login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("second login request should succeed");
|
||||
|
||||
assert_eq!(second_login_response.status(), StatusCode::OK);
|
||||
let body = second_login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("second login body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("second login payload should be json");
|
||||
|
||||
assert_eq!(payload["created"], Value::Bool(false));
|
||||
assert!(payload["referral"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3094,6 +3590,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_page_route_is_not_mounted() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/admin")
|
||||
.body(Body::empty())
|
||||
.expect("admin page request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("admin page request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_login_returns_token_when_configured() {
|
||||
let mut config = AppConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user