1
This commit is contained in:
@@ -12,7 +12,14 @@ use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::convert::Infallible;
|
||||
|
||||
use module_runtime_story_compat::{
|
||||
RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context, current_world_type,
|
||||
normalize_required_string, read_array_field, read_field, read_i32_field, read_object_field,
|
||||
read_optional_string_field, read_runtime_session_id,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::AuthenticatedAccessToken,
|
||||
http_error::AppError,
|
||||
prompt::runtime_chat::{
|
||||
NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT, NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT,
|
||||
@@ -28,6 +35,8 @@ use crate::{
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NpcChatTurnRequest {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
world_type: String,
|
||||
#[serde(default)]
|
||||
@@ -53,14 +62,25 @@ pub struct NpcChatTurnRequest {
|
||||
#[serde(default)]
|
||||
npc_initiates_conversation: bool,
|
||||
#[serde(default)]
|
||||
quest_offer_context: Option<Value>,
|
||||
#[serde(default)]
|
||||
chat_directive: Option<Value>,
|
||||
}
|
||||
|
||||
pub async fn stream_runtime_npc_chat_turn(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Json(payload): Json<NpcChatTurnRequest>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(mut payload): Json<NpcChatTurnRequest>,
|
||||
) -> Result<Response, Response> {
|
||||
hydrate_npc_chat_turn_request_from_session(
|
||||
&state,
|
||||
&request_context,
|
||||
authenticated.claims().user_id().to_string(),
|
||||
&mut payload,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let npc_name = read_string_field(&payload.encounter, "npcName")
|
||||
.or_else(|| read_string_field(&payload.encounter, "name"))
|
||||
.unwrap_or_else(|| "对方".to_string());
|
||||
@@ -258,6 +278,112 @@ where
|
||||
Some((npc_reply, suggestions, function_suggestions, force_exit))
|
||||
}
|
||||
|
||||
async fn hydrate_npc_chat_turn_request_from_session(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
user_id: String,
|
||||
payload: &mut NpcChatTurnRequest,
|
||||
) -> Result<(), Response> {
|
||||
let Some(session_id) = payload
|
||||
.session_id
|
||||
.as_deref()
|
||||
.and_then(normalize_required_string)
|
||||
else {
|
||||
// 中文注释:旧调用没有 sessionId 时继续使用请求体字段;正式运行态由后端快照投影上下文。
|
||||
return Ok(());
|
||||
};
|
||||
let record = state
|
||||
.get_runtime_snapshot_record(user_id)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": error.to_string(),
|
||||
})),
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
runtime_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"message": "运行时快照不存在,请先初始化并保存一次游戏",
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
let game_state = record.game_state;
|
||||
let snapshot_session_id =
|
||||
read_runtime_session_id(&game_state).unwrap_or_else(|| session_id.clone());
|
||||
if snapshot_session_id != session_id {
|
||||
return Err(runtime_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"message": "请求的运行时会话与服务端快照不一致,请重新进入游戏",
|
||||
"sessionId": session_id,
|
||||
"snapshotSessionId": snapshot_session_id,
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
payload.world_type = current_world_type(&game_state).unwrap_or_default();
|
||||
payload.character = read_field(&game_state, "playerCharacter").cloned();
|
||||
payload.player = payload.character.clone();
|
||||
payload.encounter = read_field(&game_state, "currentEncounter")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| payload.encounter.clone());
|
||||
payload.monsters = read_array_field(&game_state, "sceneHostileNpcs")
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
payload.history = read_array_field(&game_state, "storyHistory")
|
||||
.into_iter()
|
||||
.rev()
|
||||
.take(12)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect();
|
||||
payload.context = build_runtime_story_prompt_context(
|
||||
&game_state,
|
||||
RuntimeStoryPromptContextExtras {
|
||||
last_function_id: Some("npc_chat".to_string()),
|
||||
..RuntimeStoryPromptContextExtras::default()
|
||||
},
|
||||
);
|
||||
payload.npc_state =
|
||||
resolve_current_request_npc_state(&game_state).unwrap_or_else(|| payload.npc_state.clone());
|
||||
if let Some(quest_context) = payload.quest_offer_context.as_mut() {
|
||||
if let Some(object) = quest_context.as_object_mut() {
|
||||
object.insert("state".to_string(), game_state);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_current_request_npc_state(game_state: &Value) -> Option<Value> {
|
||||
let encounter = read_object_field(game_state, "currentEncounter")?;
|
||||
let npc_name = read_optional_string_field(encounter, "npcName")
|
||||
.or_else(|| read_optional_string_field(encounter, "name"))
|
||||
.unwrap_or_else(|| "当前遭遇".to_string());
|
||||
let npc_id = read_optional_string_field(encounter, "id").unwrap_or_else(|| npc_name.clone());
|
||||
let state = read_object_field(game_state, "npcStates").and_then(|states| {
|
||||
states
|
||||
.get(npc_id.as_str())
|
||||
.or_else(|| states.get(npc_name.as_str()))
|
||||
})?;
|
||||
|
||||
Some(json!({
|
||||
"affinity": read_i32_field(state, "affinity").unwrap_or(0),
|
||||
"chattedCount": read_i32_field(state, "chattedCount").unwrap_or(0),
|
||||
"recruited": state.get("recruited").and_then(Value::as_bool).unwrap_or(false),
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_completion_directive(chat_directive: Option<&Value>, force_exit: bool) -> Value {
|
||||
let Some(directive) = chat_directive else {
|
||||
return Value::Null;
|
||||
|
||||
Reference in New Issue
Block a user