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

@@ -8,7 +8,10 @@ use axum::{
Json,
extract::{Extension, Path as AxumPath, State, rejection::JsonRejection},
http::{HeaderName, StatusCode, header},
response::{IntoResponse, Response},
response::{
IntoResponse, Response,
sse::{Event, Sse},
},
};
use serde_json::{Value, json};
use shared_contracts::{
@@ -46,9 +49,14 @@ use spacetime_client::{
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
SpacetimeClientError,
};
use std::convert::Infallible;
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
puzzle_agent_turn::{
PuzzleAgentTurnRequest, build_failed_finalize_record_input, build_finalize_record_input,
run_puzzle_agent_turn,
},
request_context::RequestContext, state::AppState,
};
@@ -169,11 +177,12 @@ pub async fn submit_puzzle_agent_message(
));
}
let session = state
let owner_user_id = authenticated.claims().user_id().to_string();
let submitted_session = state
.spacetime_client()
.submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput {
session_id,
owner_user_id: authenticated.claims().user_id().to_string(),
session_id: session_id.clone(),
owner_user_id: owner_user_id.clone(),
user_message_id: client_message_id,
user_message_text: message_text,
submitted_at_micros: current_utc_micros(),
@@ -186,6 +195,41 @@ pub async fn submit_puzzle_agent_message(
map_puzzle_client_error(error),
)
})?;
let turn_result = run_puzzle_agent_turn(
PuzzleAgentTurnRequest {
llm_client: state.llm_client(),
session: &submitted_session,
},
|_| {},
)
.await;
let finalize_input = match turn_result {
Ok(turn_result) => build_finalize_record_input(
session_id.clone(),
owner_user_id.clone(),
format!("assistant-{session_id}-{}", current_utc_micros()),
turn_result,
current_utc_micros(),
),
Err(error) => build_failed_finalize_record_input(
session_id.clone(),
owner_user_id.clone(),
&submitted_session,
error.to_string(),
current_utc_micros(),
),
};
let session = state
.spacetime_client()
.finalize_puzzle_agent_message(finalize_input)
.await
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
map_puzzle_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
@@ -219,11 +263,12 @@ pub async fn stream_puzzle_agent_message(
"sessionId",
)?;
let owner_user_id = authenticated.claims().user_id().to_string();
let session = state
.spacetime_client()
.submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput {
session_id,
owner_user_id: authenticated.claims().user_id().to_string(),
session_id: session_id.clone(),
owner_user_id: owner_user_id.clone(),
user_message_id: payload.client_message_id.trim().to_string(),
user_message_text: payload.text.trim().to_string(),
submitted_at_micros: current_utc_micros(),
@@ -236,32 +281,100 @@ pub async fn stream_puzzle_agent_message(
map_puzzle_client_error(error),
)
})?;
let state = state.clone();
let session_id_for_stream = session_id.clone();
let owner_user_id_for_stream = owner_user_id.clone();
let stream = async_stream::stream! {
let (reply_tx, mut reply_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
let turn_result = {
let run_turn = run_puzzle_agent_turn(
PuzzleAgentTurnRequest {
llm_client: state.llm_client(),
session: &session,
},
move |text| {
let _ = reply_tx.send(text.to_string());
},
);
tokio::pin!(run_turn);
let session_response = map_puzzle_agent_session_response(session);
let reply_text = session_response
.last_assistant_reply
.clone()
.unwrap_or_else(|| "拼图锚点已更新。".to_string());
let mut sse_body = String::new();
append_sse_event(
&request_context,
&mut sse_body,
"reply_delta",
&json!({ "text": reply_text }),
)?;
append_sse_event(
&request_context,
&mut sse_body,
"session",
&json!({ "session": session_response }),
)?;
append_sse_event(
&request_context,
&mut sse_body,
"done",
&json!({ "ok": true }),
)?;
Ok(build_event_stream_response(sse_body))
loop {
tokio::select! {
result = &mut run_turn => break result,
maybe_text = reply_rx.recv() => {
if let Some(text) = maybe_text {
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
}
}
}
};
while let Some(text) = reply_rx.recv().await {
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
let finalize_input = match turn_result {
Ok(turn_result) => build_finalize_record_input(
session_id_for_stream.clone(),
owner_user_id_for_stream.clone(),
format!("assistant-{session_id_for_stream}-{}", current_utc_micros()),
turn_result,
current_utc_micros(),
),
Err(error) => build_failed_finalize_record_input(
session_id_for_stream.clone(),
owner_user_id_for_stream.clone(),
&session,
error.to_string(),
current_utc_micros(),
),
};
let finalize_result = state
.spacetime_client()
.finalize_puzzle_agent_message(finalize_input)
.await;
let _final_session = match finalize_result {
Ok(session) => session,
Err(error) => {
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"error",
json!({ "message": error.to_string() }),
));
return;
}
};
let final_session = match state
.spacetime_client()
.get_puzzle_agent_session(session_id_for_stream, owner_user_id_for_stream)
.await
{
Ok(session) => session,
Err(error) => {
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"error",
json!({ "message": error.to_string() }),
));
return;
}
};
let session_response = map_puzzle_agent_session_response(final_session);
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"session",
json!({ "session": session_response }),
));
yield Ok::<Event, Infallible>(puzzle_sse_json_event_or_error(
"done",
json!({ "ok": true }),
));
};
Ok(Sse::new(stream).into_response())
}
pub async fn execute_puzzle_agent_action(
@@ -413,13 +526,15 @@ pub async fn execute_puzzle_agent_action(
)
}
"publish_puzzle_work" => {
let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id);
let profile = state
.spacetime_client()
.publish_puzzle_work(PuzzlePublishRecordInput {
session_id: session_id.clone(),
owner_user_id: owner_user_id.clone(),
work_id: build_prefixed_uuid_id("puzzle-work-"),
profile_id: build_prefixed_uuid_id("puzzle-profile-"),
// 发布沿用 session 派生的稳定作品 ID避免草稿卡与已发布卡重复。
work_id,
profile_id,
author_display_name: resolve_author_display_name(&state, &authenticated),
level_name: payload.level_name.clone(),
summary: payload.summary.clone(),
@@ -1153,6 +1268,14 @@ fn build_puzzle_welcome_text(seed_text: &str) -> String {
"我先接住你的画面灵感,再一起把它收束成正式拼图关卡。".to_string()
}
fn build_stable_puzzle_work_ids(session_id: &str) -> (String, String) {
let stable_suffix = session_id.strip_prefix("puzzle-session-").unwrap_or(session_id);
(
format!("puzzle-work-{stable_suffix}"),
format!("puzzle-profile-{stable_suffix}"),
)
}
fn ensure_non_empty(
request_context: &RequestContext,
provider: &str,
@@ -1193,6 +1316,14 @@ fn map_puzzle_client_error(error: SpacetimeClientError) -> AppError {
{
StatusCode::NOT_FOUND
}
SpacetimeClientError::Procedure(message)
if message.contains("当前模型不可用")
|| message.contains("生成失败")
|| message.contains("解析失败")
|| message.contains("缺少有效回复") =>
{
StatusCode::BAD_GATEWAY
}
_ => StatusCode::BAD_GATEWAY,
};
@@ -1216,41 +1347,32 @@ fn puzzle_error_response(
response
}
fn append_sse_event(
request_context: &RequestContext,
body: &mut String,
event_name: &str,
payload: &Value,
) -> Result<(), Response> {
let payload = serde_json::to_string(payload).map_err(|error| {
puzzle_error_response(
request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
fn puzzle_sse_json_event(event_name: &str, payload: Value) -> Result<Event, AppError> {
Event::default()
.event(event_name)
.json_data(payload)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
"provider": "sse",
"message": format!("SSE payload 序列化失败:{error}"),
})),
)
})?;
body.push_str("event: ");
body.push_str(event_name);
body.push('\n');
body.push_str("data: ");
body.push_str(&payload);
body.push_str("\n\n");
Ok(())
}))
})
}
fn build_event_stream_response(body: String) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache, no-transform"),
(header::CONNECTION, "keep-alive"),
],
body,
)
.into_response()
fn puzzle_sse_json_event_or_error(event_name: &str, payload: Value) -> Event {
match puzzle_sse_json_event(event_name, payload) {
Ok(event) => event,
Err(_) => puzzle_sse_error_event_message("SSE payload 序列化失败".to_string()),
}
}
fn puzzle_sse_error_event_message(message: String) -> Event {
let payload = format!(
"{{\"message\":{}}}",
serde_json::to_string(&message)
.unwrap_or_else(|_| "\"SSE 错误事件序列化失败\"".to_string())
);
Event::default().event("error").data(payload)
}
fn build_placeholder_puzzle_candidates(