feat: complete M5 custom world and agent chain

This commit is contained in:
2026-04-22 14:15:27 +08:00
parent 209e924403
commit 0773a0d0ca
27 changed files with 3359 additions and 159 deletions

View File

@@ -11,21 +11,28 @@ use module_custom_world::{
use serde_json::{Map, Value, json};
use shared_contracts::runtime::{
CreateCustomWorldAgentSessionRequest, CustomWorldAgentCheckpointResponse,
CustomWorldAgentCardDetailResponse,
CustomWorldAgentMessageResponse, CustomWorldAgentOperationResponse,
CustomWorldAgentSessionResponse, CustomWorldAgentSessionSnapshotResponse,
CustomWorldDraftCardDetailResponse, CustomWorldDraftCardDetailSectionResponse,
CustomWorldDraftCardSummaryResponse, CustomWorldGalleryCardResponse,
CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryEntryResponse,
CustomWorldLibraryMutationResponse, CustomWorldLibraryResponse,
CustomWorldProfileUpsertRequest, CustomWorldSupportedActionResponse,
SendCustomWorldAgentMessageRequest,
CustomWorldPublishGateResponse, CustomWorldResultPreviewBlockerResponse,
CustomWorldWorkSummaryResponse, CustomWorldWorksResponse,
ExecuteCustomWorldAgentActionRequest, SendCustomWorldAgentMessageRequest,
};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
CustomWorldAgentActionExecuteRecordInput,
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageRecord,
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationRecord,
CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord,
CustomWorldDraftCardDetailRecord, CustomWorldDraftCardDetailSectionRecord,
CustomWorldDraftCardRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord,
CustomWorldProfileUpsertRecordInput, CustomWorldPublishWorldRecordInput,
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
CustomWorldResultPreviewBlockerRecord, CustomWorldWorkSummaryRecord,
CustomWorldSupportedActionRecord, SpacetimeClientError,
};
@@ -386,6 +393,66 @@ pub async fn get_custom_world_agent_session(
))
}
pub async fn get_custom_world_works(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
let items = state
.spacetime_client()
.list_custom_world_works(authenticated.claims().user_id().to_string())
.await
.map_err(|error| {
custom_world_error_response(&request_context, map_custom_world_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
CustomWorldWorksResponse {
items: items
.into_iter()
.map(map_custom_world_work_summary_response)
.collect(),
},
))
}
pub async fn get_custom_world_agent_card_detail(
State(state): State<AppState>,
Path((session_id, card_id)): Path<(String, String)>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
if session_id.trim().is_empty() || card_id.trim().is_empty() {
return Err(custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": "sessionId and cardId are required",
})),
));
}
let card = state
.spacetime_client()
.get_custom_world_agent_card_detail(
session_id,
authenticated.claims().user_id().to_string(),
card_id,
)
.await
.map_err(|error| {
custom_world_error_response(&request_context, map_custom_world_client_error(error))
})?;
Ok(json_success_body(
Some(&request_context),
CustomWorldAgentCardDetailResponse {
card: map_custom_world_draft_card_detail_response(card),
},
))
}
pub async fn submit_custom_world_agent_message(
State(state): State<AppState>,
Path(session_id): Path<String>,
@@ -569,7 +636,7 @@ pub async fn execute_custom_world_agent_action(
Path(session_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<Value>, JsonRejection>,
payload: Result<Json<ExecuteCustomWorldAgentActionRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
custom_world_error_response(
@@ -581,84 +648,46 @@ pub async fn execute_custom_world_agent_action(
)
})?;
let action = payload
.get("action")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if action != "publish_world" {
if session_id.trim().is_empty() {
return Err(custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::NOT_IMPLEMENTED).with_details(json!({
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": "当前 Stage 5 仅支持 publish_world action",
"message": "sessionId is required",
})),
));
}
let profile_id = payload
.get("profileId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("agent-draft-{session_id}"));
let draft_profile = payload.get("draftProfile").cloned().ok_or_else(|| {
let action = payload.action.trim().to_string();
if action.is_empty() {
return Err(custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": "action is required",
})),
));
}
let payload_json = serde_json::to_string(&payload).map_err(|error| {
custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": "publish_world 当前必须显式提供 draftProfile",
"message": format!("action payload JSON 序列化失败:{error}"),
})),
)
})?;
let setting_text = payload
.get("settingText")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.ok_or_else(|| {
custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": "publish_world 当前必须显式提供 settingText",
})),
)
})?;
let publish_result = state
let result = state
.spacetime_client()
.publish_custom_world_world(CustomWorldPublishWorldRecordInput {
session_id: session_id.clone(),
profile_id,
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
session_id,
owner_user_id: authenticated.claims().user_id().to_string(),
draft_profile_json: serde_json::to_string(&draft_profile).map_err(|error| {
custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": format!("draftProfile JSON 序列化失败:{error}"),
})),
)
})?,
legacy_result_profile_json: payload
.get("legacyResultProfile")
.map(serde_json::to_string)
.transpose()
.map_err(|error| {
custom_world_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "custom-world-agent",
"message": format!("legacyResultProfile JSON 序列化失败:{error}"),
})),
)
})?,
setting_text,
author_display_name: resolve_author_display_name(&authenticated),
published_at_micros: current_utc_micros(),
operation_id: build_prefixed_uuid_id("operation-"),
action,
payload_json: Some(payload_json),
submitted_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
@@ -668,15 +697,7 @@ pub async fn execute_custom_world_agent_action(
Ok(json_success_body(
Some(&request_context),
json!({
"operation": {
"operationId": format!("publish-world-{session_id}"),
"type": "publish_world",
"status": "completed",
"phaseLabel": "世界已发布",
"phaseDetail": format!("正式世界档案已写入作品库:{}。", publish_result.entry.profile_id),
"progress": 100,
"error": Value::Null,
}
"operation": map_custom_world_agent_operation_response(result.operation),
}),
))
}
@@ -722,6 +743,37 @@ fn map_custom_world_gallery_card_response(
}
}
fn map_custom_world_work_summary_response(
item: CustomWorldWorkSummaryRecord,
) -> CustomWorldWorkSummaryResponse {
CustomWorldWorkSummaryResponse {
work_id: item.work_id,
source_type: item.source_type,
status: item.status,
title: item.title,
subtitle: item.subtitle,
summary: item.summary,
cover_image_src: item.cover_image_src,
cover_render_mode: item.cover_render_mode,
cover_character_image_srcs: item.cover_character_image_srcs,
updated_at: item.updated_at,
published_at: item.published_at,
stage: item.stage,
stage_label: item.stage_label,
playable_npc_count: item.playable_npc_count,
landmark_count: item.landmark_count,
role_visual_ready_count: item.role_visual_ready_count,
role_animation_ready_count: item.role_animation_ready_count,
role_asset_summary_label: item.role_asset_summary_label,
session_id: item.session_id,
profile_id: item.profile_id,
can_resume: item.can_resume,
can_enter_world: item.can_enter_world,
blocker_count: item.blocker_count,
publish_ready: item.publish_ready,
}
}
fn map_custom_world_agent_session_response(
session: CustomWorldAgentSessionRecord,
) -> CustomWorldAgentSessionSnapshotResponse {
@@ -763,11 +815,28 @@ fn map_custom_world_agent_session_response(
.into_iter()
.map(map_custom_world_supported_action_response)
.collect(),
publish_gate: session.publish_gate.map(map_custom_world_publish_gate_response),
result_preview: session.result_preview,
updated_at: session.updated_at,
}
}
fn map_custom_world_publish_gate_response(
gate: CustomWorldPublishGateRecord,
) -> CustomWorldPublishGateResponse {
CustomWorldPublishGateResponse {
profile_id: gate.profile_id,
blockers: gate
.blockers
.into_iter()
.map(map_custom_world_result_preview_blocker_response)
.collect(),
blocker_count: gate.blocker_count,
publish_ready: gate.publish_ready,
can_enter_world: gate.can_enter_world,
}
}
fn map_custom_world_agent_message_response(
message: CustomWorldAgentMessageRecord,
) -> CustomWorldAgentMessageResponse {
@@ -812,6 +881,38 @@ fn map_custom_world_draft_card_response(
}
}
fn map_custom_world_draft_card_detail_response(
card: CustomWorldDraftCardDetailRecord,
) -> CustomWorldDraftCardDetailResponse {
CustomWorldDraftCardDetailResponse {
id: card.card_id,
kind: card.kind,
title: card.title,
sections: card
.sections
.into_iter()
.map(map_custom_world_draft_card_detail_section_response)
.collect(),
linked_ids: card.linked_ids,
locked: card.locked,
editable: card.editable,
editable_section_ids: card.editable_section_ids,
warning_messages: card.warning_messages,
asset_status: card.asset_status,
asset_status_label: card.asset_status_label,
}
}
fn map_custom_world_draft_card_detail_section_response(
section: CustomWorldDraftCardDetailSectionRecord,
) -> CustomWorldDraftCardDetailSectionResponse {
CustomWorldDraftCardDetailSectionResponse {
id: section.section_id,
label: section.label,
value: section.value,
}
}
fn map_custom_world_agent_checkpoint_response(
checkpoint: CustomWorldAgentCheckpointRecord,
) -> CustomWorldAgentCheckpointResponse {
@@ -832,6 +933,16 @@ fn map_custom_world_supported_action_response(
}
}
fn map_custom_world_result_preview_blocker_response(
blocker: CustomWorldResultPreviewBlockerRecord,
) -> CustomWorldResultPreviewBlockerResponse {
CustomWorldResultPreviewBlockerResponse {
id: blocker.id,
code: blocker.code,
message: blocker.message,
}
}
fn resolve_stream_reply_text(session: &CustomWorldAgentSessionSnapshotResponse) -> String {
session
.last_assistant_reply