Resolve spacetime client binding merge conflicts
This commit is contained in:
@@ -1588,13 +1588,15 @@ fn execute_custom_world_agent_action_tx(
|
||||
}
|
||||
"publish_world" => execute_publish_world_action(ctx, &session, &input, &payload),
|
||||
"revert_checkpoint" => execute_revert_checkpoint_action(ctx, &session, &input, &payload),
|
||||
"generate_characters"
|
||||
| "generate_landmarks"
|
||||
| "generate_role_assets"
|
||||
| "sync_role_assets"
|
||||
| "generate_scene_assets"
|
||||
| "sync_scene_assets"
|
||||
| "expand_long_tail" => execute_placeholder_custom_world_action(ctx, &session, &input),
|
||||
"generate_characters" | "generate_landmarks" => {
|
||||
execute_generate_entities_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"generate_role_assets" | "generate_scene_assets" => {
|
||||
execute_prepare_asset_studio_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"sync_role_assets" => execute_sync_role_assets_action(ctx, &session, &input, &payload),
|
||||
"sync_scene_assets" => execute_sync_scene_assets_action(ctx, &session, &input, &payload),
|
||||
"expand_long_tail" => execute_placeholder_custom_world_action(ctx, &session, &input),
|
||||
other => Err(format!("custom world action `{other}` 当前尚未支持")),
|
||||
}
|
||||
}
|
||||
@@ -2134,6 +2136,146 @@ fn execute_revert_checkpoint_action(
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
|
||||
fn execute_prepare_asset_studio_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_draft_refining_stage(session.stage, input.action.as_str())?;
|
||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| format!("{} requires an existing draft foundation", input.action))?;
|
||||
let (focus_id, operation_type, message_text, phase_label, phase_detail) =
|
||||
if input.action == "generate_role_assets" {
|
||||
let role_id = read_first_payload_text(payload, "roleIds", "roleId")
|
||||
.ok_or_else(|| "generate_role_assets requires roleIds".to_string())?;
|
||||
let role = find_profile_entity_by_id(&draft_profile, &["playableNpcs", "storyNpcs"], &role_id)
|
||||
.ok_or_else(|| "未找到目标角色,无法进入角色资产工坊。".to_string())?;
|
||||
let role_name = read_optional_text_field(role, &["name"]).unwrap_or_else(|| "角色".to_string());
|
||||
(
|
||||
role_id,
|
||||
RpgAgentOperationType::GenerateRoleAssets,
|
||||
format!("已为「{}」准备好角色资产工坊,先生成主图候选,再补核心动作。", role_name),
|
||||
"角色资产工坊已就绪",
|
||||
format!("「{}」现在可以开始生成主图和动作。", role_name),
|
||||
)
|
||||
} else {
|
||||
let scene_id = read_first_payload_text(payload, "sceneIds", "sceneId")
|
||||
.ok_or_else(|| "generate_scene_assets requires sceneIds".to_string())?;
|
||||
let scene_kind = payload.get("sceneKind").and_then(JsonValue::as_str).map(str::trim).unwrap_or("landmark");
|
||||
let scene = if scene_kind == "camp" {
|
||||
draft_profile.get("camp").and_then(JsonValue::as_object)
|
||||
} else {
|
||||
find_profile_entity_by_id(&draft_profile, &["landmarks"], &scene_id)
|
||||
}
|
||||
.ok_or_else(|| "未找到目标场景,无法进入场景资产工坊。".to_string())?;
|
||||
let scene_name = read_optional_text_field(scene, &["name"])
|
||||
.unwrap_or_else(|| if scene_kind == "camp" { "开局营地" } else { "未命名场景" }.to_string());
|
||||
(
|
||||
scene_id,
|
||||
RpgAgentOperationType::GenerateSceneAssets,
|
||||
format!("已为「{}」准备好场景图工坊,保存生成结果后会自动同步回当前草稿。", scene_name),
|
||||
"场景资产工坊已就绪",
|
||||
format!("「{}」现在可以继续生成和确认正式场景图。", scene_name),
|
||||
)
|
||||
};
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
focus_card_id: Some(Some(focus_id)),
|
||||
last_assistant_reply: Some(Some(message_text.clone())),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
append_custom_world_action_result_message(ctx, &session.session_id, &input.operation_id, &message_text, input.submitted_at_micros);
|
||||
let operation = build_and_insert_custom_world_operation(ctx, &input.operation_id, &session.session_id, operation_type, phase_label, &phase_detail, input.submitted_at_micros);
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_sync_role_assets_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_draft_refining_stage(session.stage, "sync_role_assets")?;
|
||||
let mut draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| "sync_role_assets requires an existing draft foundation".to_string())?;
|
||||
let role_id = read_required_payload_text(payload, "roleId", "sync_role_assets requires roleId")?;
|
||||
let portrait_path = read_required_payload_text(payload, "portraitPath", "sync_role_assets requires portraitPath")?;
|
||||
let generated_visual_asset_id = read_required_payload_text(payload, "generatedVisualAssetId", "sync_role_assets requires generatedVisualAssetId")?;
|
||||
let generated_animation_set_id = payload.get("generatedAnimationSetId").and_then(JsonValue::as_str).map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned);
|
||||
let animation_map = payload.get("animationMap").cloned();
|
||||
let updated_role = apply_role_asset_publish_result(&mut draft_profile, &role_id, &portrait_path, &generated_visual_asset_id, generated_animation_set_id.as_deref(), animation_map)?;
|
||||
let role_name = read_optional_text_field(&updated_role, &["name"]).unwrap_or_else(|| "当前角色".to_string());
|
||||
let asset_status = resolve_role_asset_status(&updated_role);
|
||||
let asset_status_label = resolve_role_asset_status_label(asset_status).to_string();
|
||||
upsert_asset_role_card(ctx, &session.session_id, &role_id, &updated_role, asset_status, &asset_status_label, input.submitted_at_micros)?;
|
||||
let gate = summarize_publish_gate_from_json(&input.session_id, RpgAgentStage::VisualRefining, Some(&draft_profile), &parse_json_array_or_empty(&session.quality_findings_json));
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
focus_card_id: Some(Some(role_id.clone())),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
|
||||
last_assistant_reply: Some(Some(format!("已把「{}」的角色资产写回草稿,当前状态:{}。", role_name, asset_status_label))),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
result_preview_json: Some(build_result_preview_json(Some(&draft_profile), &gate, &parse_json_array_or_empty(&session.quality_findings_json), input.submitted_at_micros)?),
|
||||
checkpoints_json: Some(append_checkpoint_json(&session.checkpoints_json, &build_session_checkpoint_value("sync-role-assets", &format!("同步角色资产 {}", role_name), session))?),
|
||||
asset_coverage_json: Some(build_asset_coverage_json(&draft_profile)?),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
append_custom_world_action_result_message(ctx, &session.session_id, &input.operation_id, &format!("已把「{}」的角色资产写回草稿,当前状态:{}。", role_name, asset_status_label), input.submitted_at_micros);
|
||||
let operation = build_and_insert_custom_world_operation(ctx, &input.operation_id, &session.session_id, RpgAgentOperationType::SyncRoleAssets, "角色资产已同步", &format!("「{}」的资产状态已更新为{}。", role_name, asset_status_label), input.submitted_at_micros);
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_sync_scene_assets_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_draft_refining_stage(session.stage, "sync_scene_assets")?;
|
||||
let mut draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| "sync_scene_assets requires an existing draft foundation".to_string())?;
|
||||
let scene_id = read_required_payload_text(payload, "sceneId", "sync_scene_assets requires sceneId")?;
|
||||
let scene_kind = read_required_payload_text(payload, "sceneKind", "sync_scene_assets requires sceneKind")?;
|
||||
let image_src = read_required_payload_text(payload, "imageSrc", "sync_scene_assets requires imageSrc")?;
|
||||
let generated_scene_asset_id = read_required_payload_text(payload, "generatedSceneAssetId", "sync_scene_assets requires generatedSceneAssetId")?;
|
||||
let updated_scene = apply_scene_asset_publish_result(&mut draft_profile, &scene_id, &scene_kind, &image_src, &generated_scene_asset_id, payload.get("generatedScenePrompt").cloned().unwrap_or(JsonValue::Null), payload.get("generatedSceneModel").cloned().unwrap_or(JsonValue::Null))?;
|
||||
let scene_name = read_optional_text_field(&updated_scene, &["name"]).unwrap_or_else(|| if scene_kind == "camp" { "开局营地" } else { "当前场景" }.to_string());
|
||||
upsert_asset_scene_card(ctx, &session.session_id, &scene_id, &scene_kind, &updated_scene, input.submitted_at_micros)?;
|
||||
let gate = summarize_publish_gate_from_json(&input.session_id, RpgAgentStage::VisualRefining, Some(&draft_profile), &parse_json_array_or_empty(&session.quality_findings_json));
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
focus_card_id: Some(Some(scene_id.clone())),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
|
||||
last_assistant_reply: Some(Some(format!("已把「{}」的场景图写回草稿,并同步刷新地点卡与幕背景状态。", scene_name))),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
result_preview_json: Some(build_result_preview_json(Some(&draft_profile), &gate, &parse_json_array_or_empty(&session.quality_findings_json), input.submitted_at_micros)?),
|
||||
checkpoints_json: Some(append_checkpoint_json(&session.checkpoints_json, &build_session_checkpoint_value("sync-scene-assets", &format!("同步场景资产 {}", scene_name), session))?),
|
||||
asset_coverage_json: Some(build_asset_coverage_json(&draft_profile)?),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
append_custom_world_action_result_message(ctx, &session.session_id, &input.operation_id, &format!("已把「{}」的场景图写回草稿,并同步刷新地点卡与幕背景状态。", scene_name), input.submitted_at_micros);
|
||||
let operation = build_and_insert_custom_world_operation(ctx, &input.operation_id, &session.session_id, RpgAgentOperationType::SyncSceneAssets, "场景资产已同步", &format!("「{}」的场景图已经进入当前草稿。", scene_name), input.submitted_at_micros);
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
|
||||
fn execute_placeholder_custom_world_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
@@ -3278,6 +3420,121 @@ fn parse_json_array_or_empty(raw: &str) -> Vec<JsonValue> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||
payload.get(array_key).and_then(JsonValue::as_array).and_then(|values| values.first()).and_then(JsonValue::as_str)
|
||||
.or_else(|| payload.get(scalar_key).and_then(JsonValue::as_str))
|
||||
.map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn find_profile_entity_by_id<'a>(profile: &'a JsonMap<String, JsonValue>, fields: &[&str], entity_id: &str) -> Option<&'a JsonMap<String, JsonValue>> {
|
||||
for field in fields {
|
||||
if let Some(entries) = profile.get(*field).and_then(JsonValue::as_array) {
|
||||
for entry in entries {
|
||||
let Some(object) = entry.as_object() else { continue; };
|
||||
if read_optional_text_field(object, &["id"]).as_deref() == Some(entity_id) { return Some(object); }
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_role_asset_publish_result(profile: &mut JsonMap<String, JsonValue>, role_id: &str, portrait_path: &str, generated_visual_asset_id: &str, generated_animation_set_id: Option<&str>, animation_map: Option<JsonValue>) -> Result<JsonMap<String, JsonValue>, String> {
|
||||
for field in ["playableNpcs", "storyNpcs"] {
|
||||
let Some(entries) = profile.get_mut(field).and_then(JsonValue::as_array_mut) else { continue; };
|
||||
for entry in entries {
|
||||
let Some(object) = entry.as_object_mut() else { continue; };
|
||||
if read_optional_text_field(object, &["id"]).as_deref() != Some(role_id) { continue; }
|
||||
object.insert("imageSrc".to_string(), JsonValue::String(portrait_path.to_string()));
|
||||
object.insert("generatedVisualAssetId".to_string(), JsonValue::String(generated_visual_asset_id.to_string()));
|
||||
if let Some(asset_id) = generated_animation_set_id { object.insert("generatedAnimationSetId".to_string(), JsonValue::String(asset_id.to_string())); }
|
||||
if let Some(map) = animation_map { object.insert("animationMap".to_string(), map); }
|
||||
return Ok(object.clone());
|
||||
}
|
||||
}
|
||||
Err("目标角色不存在,无法同步角色资产。".to_string())
|
||||
}
|
||||
|
||||
fn apply_scene_asset_publish_result(profile: &mut JsonMap<String, JsonValue>, scene_id: &str, scene_kind: &str, image_src: &str, generated_scene_asset_id: &str, generated_scene_prompt: JsonValue, generated_scene_model: JsonValue) -> Result<JsonMap<String, JsonValue>, String> {
|
||||
let updated_scene = if scene_kind == "camp" {
|
||||
let camp = profile.get_mut("camp").and_then(JsonValue::as_object_mut).ok_or_else(|| "目标营地不存在,无法同步场景资产。".to_string())?;
|
||||
if read_optional_text_field(camp, &["id"]).as_deref() != Some(scene_id) { return Err("目标营地不存在,无法同步场景资产。".to_string()); }
|
||||
camp.insert("imageSrc".to_string(), JsonValue::String(image_src.to_string()));
|
||||
camp.insert("generatedSceneAssetId".to_string(), JsonValue::String(generated_scene_asset_id.to_string()));
|
||||
camp.insert("generatedScenePrompt".to_string(), generated_scene_prompt);
|
||||
camp.insert("generatedSceneModel".to_string(), generated_scene_model);
|
||||
camp.clone()
|
||||
} else {
|
||||
let landmarks = profile.get_mut("landmarks").and_then(JsonValue::as_array_mut).ok_or_else(|| "目标地点不存在,无法同步场景资产。".to_string())?;
|
||||
let mut updated = None;
|
||||
for entry in landmarks {
|
||||
let Some(object) = entry.as_object_mut() else { continue; };
|
||||
if read_optional_text_field(object, &["id"]).as_deref() != Some(scene_id) { continue; }
|
||||
object.insert("imageSrc".to_string(), JsonValue::String(image_src.to_string()));
|
||||
object.insert("generatedSceneAssetId".to_string(), JsonValue::String(generated_scene_asset_id.to_string()));
|
||||
object.insert("generatedScenePrompt".to_string(), generated_scene_prompt.clone());
|
||||
object.insert("generatedSceneModel".to_string(), generated_scene_model.clone());
|
||||
updated = Some(object.clone());
|
||||
break;
|
||||
}
|
||||
updated.ok_or_else(|| "目标地点不存在,无法同步场景资产。".to_string())?
|
||||
};
|
||||
update_scene_chapter_acts_for_scene(profile, scene_id, image_src, generated_scene_asset_id);
|
||||
Ok(updated_scene)
|
||||
}
|
||||
|
||||
fn update_scene_chapter_acts_for_scene(profile: &mut JsonMap<String, JsonValue>, scene_id: &str, image_src: &str, generated_scene_asset_id: &str) {
|
||||
let Some(chapters) = profile.get_mut("sceneChapters").and_then(JsonValue::as_array_mut) else { return; };
|
||||
for chapter in chapters {
|
||||
let Some(chapter_object) = chapter.as_object_mut() else { continue; };
|
||||
if read_optional_text_field(chapter_object, &["sceneId"]).as_deref() != Some(scene_id) { continue; }
|
||||
let Some(acts) = chapter_object.get_mut("acts").and_then(JsonValue::as_array_mut) else { continue; };
|
||||
for act in acts {
|
||||
if let Some(act_object) = act.as_object_mut() {
|
||||
act_object.insert("backgroundImageSrc".to_string(), JsonValue::String(image_src.to_string()));
|
||||
act_object.insert("backgroundAssetId".to_string(), JsonValue::String(generated_scene_asset_id.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_role_asset_status(role: &JsonMap<String, JsonValue>) -> CustomWorldRoleAssetStatus {
|
||||
let has_portrait = read_optional_text_field(role, &["imageSrc"]).is_some() && read_optional_text_field(role, &["generatedVisualAssetId"]).is_some();
|
||||
if !has_portrait { return CustomWorldRoleAssetStatus::Missing; }
|
||||
let has_animation_set = read_optional_text_field(role, &["generatedAnimationSetId"]).is_some();
|
||||
let has_animation_map = role.get("animationMap").and_then(JsonValue::as_object).map(|map| !map.is_empty()).unwrap_or(false);
|
||||
if has_animation_set && has_animation_map { CustomWorldRoleAssetStatus::Complete } else if has_animation_set { CustomWorldRoleAssetStatus::AnimationsReady } else { CustomWorldRoleAssetStatus::VisualReady }
|
||||
}
|
||||
|
||||
fn resolve_role_asset_status_label(status: CustomWorldRoleAssetStatus) -> &'static str {
|
||||
match status { CustomWorldRoleAssetStatus::Complete => "动作已就绪", CustomWorldRoleAssetStatus::AnimationsReady => "动作补齐中", CustomWorldRoleAssetStatus::VisualReady => "主图已就绪", CustomWorldRoleAssetStatus::Missing => "待生成主图" }
|
||||
}
|
||||
|
||||
fn build_asset_coverage_json(profile: &JsonMap<String, JsonValue>) -> Result<String, String> {
|
||||
serialize_json_value(&json!({"roleAssets": [], "sceneAssets": [], "allRoleAssetsReady": false, "allSceneAssetsReady": false, "profileId": read_optional_text_field(profile, &["id"])}))
|
||||
}
|
||||
|
||||
fn upsert_asset_role_card(ctx: &ReducerContext, session_id: &str, role_id: &str, role: &JsonMap<String, JsonValue>, asset_status: CustomWorldRoleAssetStatus, asset_status_label: &str, updated_at_micros: i64) -> Result<(), String> {
|
||||
let title = read_optional_text_field(role, &["name"]).unwrap_or_else(|| "角色".to_string());
|
||||
let subtitle = read_optional_text_field(role, &["role", "relationToPlayer", "publicMask"]).unwrap_or_else(|| asset_status_label.to_string());
|
||||
let summary = read_optional_text_field(role, &["summary", "description", "publicMask"]).unwrap_or_else(|| "角色资产已写回草稿。".to_string());
|
||||
upsert_asset_card(ctx, session_id, role_id, RpgAgentDraftCardKind::Character, &title, &subtitle, &summary, Some(asset_status), Some(asset_status_label), updated_at_micros)
|
||||
}
|
||||
|
||||
fn upsert_asset_scene_card(ctx: &ReducerContext, session_id: &str, scene_id: &str, scene_kind: &str, scene: &JsonMap<String, JsonValue>, updated_at_micros: i64) -> Result<(), String> {
|
||||
let kind = if scene_kind == "camp" { RpgAgentDraftCardKind::Camp } else { RpgAgentDraftCardKind::Landmark };
|
||||
let title = read_optional_text_field(scene, &["name"]).unwrap_or_else(|| if scene_kind == "camp" { "开局营地" } else { "场景" }.to_string());
|
||||
let subtitle = read_optional_text_field(scene, &["purpose", "mood", "dangerLevel"]).unwrap_or_else(|| "场景资产已就绪".to_string());
|
||||
let summary = read_optional_text_field(scene, &["summary", "description", "publicMask"]).unwrap_or_else(|| "场景图已写回草稿。".to_string());
|
||||
upsert_asset_card(ctx, session_id, scene_id, kind, &title, &subtitle, &summary, None, Some("场景图已就绪"), updated_at_micros)
|
||||
}
|
||||
|
||||
fn upsert_asset_card(ctx: &ReducerContext, session_id: &str, card_id: &str, kind: RpgAgentDraftCardKind, title: &str, subtitle: &str, summary: &str, asset_status: Option<CustomWorldRoleAssetStatus>, asset_status_label: Option<&str>, updated_at_micros: i64) -> Result<(), String> {
|
||||
let detail_payload = json!({"id": card_id, "kind": kind.as_str(), "title": title, "sections": [{"id": "summary", "label": "摘要", "value": summary}], "linkedIds": [], "locked": false, "editable": true, "editableSectionIds": ["summary"], "warningMessages": []});
|
||||
let next = CustomWorldDraftCard { card_id: card_id.to_string(), session_id: session_id.to_string(), kind, status: RpgAgentDraftCardStatus::Draft, title: title.to_string(), subtitle: subtitle.to_string(), summary: summary.to_string(), linked_ids_json: "[]".to_string(), warning_count: 0, asset_status, asset_status_label: asset_status_label.map(ToOwned::to_owned), detail_payload_json: Some(serialize_json_value(&detail_payload)?), created_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros) };
|
||||
if let Some(existing) = ctx.db.custom_world_draft_card().card_id().find(&card_id.to_string()).filter(|entry| entry.session_id == session_id) { replace_custom_world_draft_card(ctx, &existing, CustomWorldDraftCard { created_at: existing.created_at, ..next }); } else { ctx.db.custom_world_draft_card().insert(next); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_json_value(value: &JsonValue) -> Result<String, String> {
|
||||
serde_json::to_string(value).map_err(|error| format!("JSON 序列化失败: {error}"))
|
||||
}
|
||||
|
||||
@@ -1192,6 +1192,25 @@ pub fn get_custom_world_agent_operation(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn upsert_custom_world_agent_operation_progress(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: CustomWorldAgentOperationProgressInput,
|
||||
) -> CustomWorldAgentOperationProcedureResult {
|
||||
match ctx.try_with_tx(|tx| upsert_custom_world_agent_operation_progress_tx(tx, input.clone())) {
|
||||
Ok(operation) => CustomWorldAgentOperationProcedureResult {
|
||||
ok: true,
|
||||
operation: Some(operation),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => CustomWorldAgentOperationProcedureResult {
|
||||
ok: false,
|
||||
operation: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn continue_story_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: StoryContinueInput,
|
||||
@@ -1474,6 +1493,59 @@ fn get_custom_world_agent_operation_tx(
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn upsert_custom_world_agent_operation_progress_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentOperationProgressInput,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
validate_custom_world_agent_operation_progress_input(&input).map_err(|error| error.to_string())?;
|
||||
ctx.db
|
||||
.custom_world_agent_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "custom_world_agent_session 不存在".to_string())?;
|
||||
|
||||
let timestamp = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
let operation = if let Some(current) = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.find(&input.operation_id)
|
||||
{
|
||||
if current.session_id != input.session_id {
|
||||
return Err("custom_world_agent_operation.session_id 不匹配".to_string());
|
||||
}
|
||||
let next = rebuild_custom_world_agent_operation_row(
|
||||
¤t,
|
||||
CustomWorldAgentOperationPatch {
|
||||
status: Some(input.operation_status),
|
||||
phase_label: Some(input.phase_label.clone()),
|
||||
phase_detail: Some(input.phase_detail.clone()),
|
||||
progress: Some(input.operation_progress),
|
||||
error_message: Some(input.error_message.clone()),
|
||||
updated_at_micros: Some(input.updated_at_micros),
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_operation(ctx, ¤t, next.clone());
|
||||
next
|
||||
} else {
|
||||
ctx.db.custom_world_agent_operation().insert(CustomWorldAgentOperation {
|
||||
operation_id: input.operation_id.clone(),
|
||||
session_id: input.session_id.clone(),
|
||||
operation_type: input.operation_type,
|
||||
status: input.operation_status,
|
||||
phase_label: input.phase_label.clone(),
|
||||
phase_detail: input.phase_detail.clone(),
|
||||
progress: input.operation_progress,
|
||||
error_message: input.error_message.clone(),
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn finalize_custom_world_agent_message_turn_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentMessageFinalizeInput,
|
||||
@@ -2896,14 +2968,22 @@ fn execute_custom_world_agent_action_tx(
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "custom_world_agent_session 不存在".to_string())?;
|
||||
|
||||
if ctx
|
||||
if let Some(existing_operation) = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.find(&input.operation_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("custom_world_agent_operation.operation_id 已存在".to_string());
|
||||
let can_reuse_running_draft_operation = input.action.trim() == "draft_foundation"
|
||||
&& existing_operation.session_id == input.session_id
|
||||
&& existing_operation.operation_type == RpgAgentOperationType::DraftFoundation
|
||||
&& matches!(
|
||||
existing_operation.status,
|
||||
RpgAgentOperationStatus::Queued | RpgAgentOperationStatus::Running
|
||||
);
|
||||
if !can_reuse_running_draft_operation {
|
||||
return Err("custom_world_agent_operation.operation_id 已存在".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let payload = parse_optional_session_object(input.payload_json.as_deref()).unwrap_or_default();
|
||||
@@ -2990,7 +3070,7 @@ fn execute_draft_foundation_action(
|
||||
updated_at,
|
||||
);
|
||||
|
||||
let operation = build_and_insert_custom_world_operation(
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
@@ -2998,7 +3078,7 @@ fn execute_draft_foundation_action(
|
||||
"底稿已整理",
|
||||
"第一版 foundation draft 已写入会话与世界卡。",
|
||||
updated_at,
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
@@ -4117,6 +4197,53 @@ fn replace_custom_world_draft_card(
|
||||
ctx.db.custom_world_draft_card().insert(next);
|
||||
}
|
||||
|
||||
fn complete_custom_world_operation(
|
||||
ctx: &ReducerContext,
|
||||
operation_id: &str,
|
||||
session_id: &str,
|
||||
operation_type: RpgAgentOperationType,
|
||||
phase_label: &str,
|
||||
phase_detail: &str,
|
||||
timestamp_micros: i64,
|
||||
) -> Result<CustomWorldAgentOperation, String> {
|
||||
if let Some(current) = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.find(&operation_id.to_string())
|
||||
{
|
||||
if current.session_id != session_id {
|
||||
return Err("custom_world_agent_operation.session_id 不匹配".to_string());
|
||||
}
|
||||
if current.operation_type != operation_type {
|
||||
return Err("custom_world_agent_operation.operation_type 不匹配".to_string());
|
||||
}
|
||||
let next = rebuild_custom_world_agent_operation_row(
|
||||
¤t,
|
||||
CustomWorldAgentOperationPatch {
|
||||
status: Some(RpgAgentOperationStatus::Completed),
|
||||
phase_label: Some(phase_label.to_string()),
|
||||
phase_detail: Some(phase_detail.to_string()),
|
||||
progress: Some(100),
|
||||
error_message: Some(None),
|
||||
updated_at_micros: Some(timestamp_micros),
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_operation(ctx, ¤t, next.clone());
|
||||
return Ok(next);
|
||||
}
|
||||
|
||||
Ok(build_and_insert_custom_world_operation(
|
||||
ctx,
|
||||
operation_id,
|
||||
session_id,
|
||||
operation_type,
|
||||
phase_label,
|
||||
phase_detail,
|
||||
timestamp_micros,
|
||||
))
|
||||
}
|
||||
|
||||
fn build_and_insert_custom_world_operation(
|
||||
ctx: &ReducerContext,
|
||||
operation_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user