Refactor server-rs runtime and update related docs

This commit is contained in:
2026-04-25 14:29:44 +08:00
parent 019dd9efba
commit 6be3afe45a
56 changed files with 1561 additions and 1158 deletions

View File

@@ -0,0 +1,145 @@
use axum::{
Json,
extract::Extension,
http::{StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{http_error::AppError, request_context::RequestContext};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NpcChatTurnRequest {
encounter: Value,
player_message: String,
#[serde(default)]
npc_initiates_conversation: bool,
#[serde(default)]
chat_directive: Option<Value>,
}
pub async fn stream_runtime_npc_chat_turn(
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<NpcChatTurnRequest>,
) -> Result<Response, Response> {
let npc_name = read_string_field(&payload.encounter, "npcName")
.or_else(|| read_string_field(&payload.encounter, "name"))
.unwrap_or_else(|| "对方".to_string());
let player_message = payload.player_message.trim();
if player_message.is_empty() {
return Err(runtime_chat_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "runtime-chat",
"message": "playerMessage 不能为空",
})),
));
}
let npc_reply = build_deterministic_npc_reply(
npc_name.as_str(),
player_message,
payload.npc_initiates_conversation,
);
let suggestions = build_deterministic_chat_suggestions(npc_name.as_str(), player_message);
let complete_payload = json!({
"npcReply": npc_reply,
"affinityDelta": 0,
"affinityText": "关系暂未变化",
"suggestions": suggestions,
"pendingQuestOffer": null,
"chatDirective": build_completion_directive(payload.chat_directive.as_ref()),
});
let mut body = String::new();
append_sse_event(&request_context, &mut body, "reply_delta", &json!({ "text": npc_reply }))?;
append_sse_event(&request_context, &mut body, "complete", &complete_payload)?;
Ok(build_event_stream_response(body))
}
fn build_deterministic_npc_reply(
npc_name: &str,
player_message: &str,
npc_initiates_conversation: bool,
) -> String {
// Rust API 尚未迁入旧 Node 的完整 LLM NPC 聊天编排前,先由后端提供稳定兜底,保证相遇与选项聊天链路不断。
if npc_initiates_conversation {
return format!("{npc_name}看向你,先开口说道:“你来了。先别急着走,我正有话想和你说。”");
}
format!("{npc_name}听完你的话,回应道:“{player_message}。我明白你的意思,我们继续说。”")
}
fn build_deterministic_chat_suggestions(npc_name: &str, player_message: &str) -> Vec<String> {
// 建议只承载玩家可点选的行动意图,不在 UI 里额外塞说明文案。
vec![
format!("继续询问{npc_name}的近况"),
"追问这里发生了什么".to_string(),
if player_message.contains('帮') || player_message.contains('忙') {
"请对方说清需要什么帮助".to_string()
} else {
"换个轻松的话题".to_string()
},
]
}
fn build_completion_directive(chat_directive: Option<&Value>) -> Value {
let Some(directive) = chat_directive else {
return Value::Null;
};
json!({
"turnLimit": directive.get("turnLimit").cloned().unwrap_or(Value::Null),
"remainingTurns": directive.get("remainingTurns").cloned().unwrap_or(Value::Null),
"forceExit": directive.get("forceExitAfterTurn").and_then(Value::as_bool).unwrap_or(false),
"closingMode": directive.get("closingMode").cloned().unwrap_or(Value::Null),
})
}
fn read_string_field(value: &Value, field: &str) -> Option<String> {
value
.get(field)
.and_then(Value::as_str)
.map(str::trim)
.filter(|text| !text.is_empty())
.map(ToOwned::to_owned)
}
fn append_sse_event(
request_context: &RequestContext,
body: &mut String,
event: &str,
payload: &Value,
) -> Result<(), Response> {
let payload_text = serde_json::to_string(payload).map_err(|error| {
runtime_chat_error_response(
request_context,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "runtime-chat",
"message": format!("SSE payload 序列化失败:{error}"),
})),
)
})?;
body.push_str("event: ");
body.push_str(event);
body.push('\n');
body.push_str("data: ");
body.push_str(&payload_text);
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"),
],
body,
)
.into_response()
}
fn runtime_chat_error_response(request_context: &RequestContext, error: AppError) -> Response {
error.into_response_with_context(Some(request_context))
}