fix: sync rust api-server runtime and bindings

This commit is contained in:
2026-04-23 20:32:06 +08:00
parent 9d25a47b23
commit 27e84c46a0
82 changed files with 9534 additions and 2222 deletions

View File

@@ -13,6 +13,10 @@ 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,
},
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,
@@ -26,10 +30,11 @@ use crate::{
require_bearer_auth,
},
auth_me::auth_me,
auth_public_user::get_public_user_by_code,
auth_sessions::auth_sessions,
big_fish::{
create_big_fish_session, execute_big_fish_action, get_big_fish_run, get_big_fish_session,
start_big_fish_run, stream_big_fish_message, submit_big_fish_input,
get_big_fish_works, start_big_fish_run, stream_big_fish_message, submit_big_fish_input,
submit_big_fish_message,
},
character_animation_assets::{
@@ -44,8 +49,9 @@ use crate::{
create_custom_world_agent_session, delete_custom_world_library_profile,
execute_custom_world_agent_action, get_custom_world_agent_card_detail,
get_custom_world_agent_operation, get_custom_world_agent_session,
get_custom_world_gallery_detail, get_custom_world_library, get_custom_world_library_detail,
get_custom_world_works, list_custom_world_gallery, publish_custom_world_library_profile,
get_custom_world_gallery_detail, 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,
},
@@ -105,6 +111,29 @@ 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",
get(admin_me).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/overview",
get(admin_overview).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/debug/http",
post(admin_debug_http).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/healthz",
get(|Extension(request_context): Extension<_>| async move {
@@ -126,6 +155,10 @@ pub fn build_router(state: AppState) -> Router {
)),
)
.route("/api/auth/login-options", get(auth_login_options))
.route(
"/api/auth/public-users/by-code/{code}",
get(get_public_user_by_code),
)
.route(
"/generated-character-drafts/{*path}",
get(proxy_generated_character_drafts),
@@ -391,6 +424,10 @@ 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/by-code/{code}",
get(get_custom_world_gallery_detail_by_code),
)
.route(
"/api/runtime/custom-world/agent/sessions",
post(create_custom_world_agent_session).route_layer(middleware::from_fn_with_state(
@@ -482,6 +519,13 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/works",
get(get_big_fish_works).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/big-fish/sessions/{session_id}/runs",
post(start_big_fish_run).route_layer(middleware::from_fn_with_state(
@@ -2905,4 +2949,168 @@ mod tests {
.is_some_and(|value| value.contains("Max-Age=0"))
);
}
#[tokio::test]
async fn admin_login_returns_token_when_configured() {
let mut config = AppConfig::default();
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let app = build_router(AppState::new(config).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "root",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert!(payload["token"].as_str().is_some());
assert_eq!(payload["admin"]["username"], Value::String("root".to_string()));
}
#[tokio::test]
async fn admin_route_rejects_regular_user_token() {
let mut config = AppConfig::default();
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let state = AppState::new(config).expect("state should build");
let app = build_router(state.clone());
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_admin_forbidden",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login should succeed");
let login_body = login_response
.into_body()
.collect()
.await
.expect("login body should collect")
.to_bytes();
let login_payload: Value =
serde_json::from_slice(&login_body).expect("login payload should be json");
let access_token = login_payload["token"]
.as_str()
.expect("token should exist")
.to_string();
let response = app
.oneshot(
Request::builder()
.uri("/admin/api/me")
.header("authorization", format!("Bearer {access_token}"))
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn admin_debug_http_can_probe_healthz() {
let mut config = AppConfig::default();
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let app = build_router(AppState::new(config).expect("state should build"));
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "root",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login should succeed");
let login_body = login_response
.into_body()
.collect()
.await
.expect("login body should collect")
.to_bytes();
let login_payload: Value =
serde_json::from_slice(&login_body).expect("login payload should be json");
let access_token = login_payload["token"]
.as_str()
.expect("token should exist")
.to_string();
let debug_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/debug/http")
.header("authorization", format!("Bearer {access_token}"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"method": "GET",
"path": "/healthz",
"headers": [],
"body": ""
})
.to_string(),
))
.expect("debug request should build"),
)
.await
.expect("debug request should succeed");
assert_eq!(debug_response.status(), StatusCode::OK);
let body = debug_response
.into_body()
.collect()
.await
.expect("debug body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("debug payload should be json");
assert_eq!(payload["status"], Value::Number(200.into()));
}
}