fix: sync rust api-server runtime and bindings
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user