Close DDD cleanup and tests-support closure
This commit is contained in:
@@ -1826,13 +1826,19 @@ 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" => {
|
||||
execute_generate_characters_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"generate_landmarks" => execute_generate_landmarks_action(ctx, &session, &input, &payload),
|
||||
"generate_role_assets" => {
|
||||
execute_generate_role_assets_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"sync_role_assets" => execute_sync_role_assets_action(ctx, &session, &input, &payload),
|
||||
"generate_scene_assets" => {
|
||||
execute_generate_scene_assets_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"sync_scene_assets" => execute_sync_scene_assets_action(ctx, &session, &input, &payload),
|
||||
"expand_long_tail" => execute_expand_long_tail_action(ctx, &session, &input, &payload),
|
||||
other => Err(format!("custom world action `{other}` 当前尚未支持")),
|
||||
}
|
||||
}
|
||||
@@ -2378,35 +2384,763 @@ fn execute_revert_checkpoint_action(
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_placeholder_custom_world_action(
|
||||
fn execute_generate_characters_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
let operation_type = map_action_name_to_operation_type(input.action.as_str())
|
||||
.ok_or_else(|| format!("action {} 无法映射到 operation type", input.action))?;
|
||||
ensure_refining_stage(session.stage, "generate_characters")?;
|
||||
let mut draft_profile = current_custom_world_draft_profile(session);
|
||||
let inserted = upsert_draft_profile_array_from_payload(
|
||||
&mut draft_profile,
|
||||
payload,
|
||||
"characters",
|
||||
"playableNpcs",
|
||||
"character",
|
||||
RpgAgentDraftCardKind::Character,
|
||||
ctx,
|
||||
&session.session_id,
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
let inserted_story = upsert_draft_profile_array_from_payload(
|
||||
&mut draft_profile,
|
||||
payload,
|
||||
"storyNpcs",
|
||||
"storyNpcs",
|
||||
"story-npc",
|
||||
RpgAgentDraftCardKind::Character,
|
||||
ctx,
|
||||
&session.session_id,
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
let total_inserted = inserted.saturating_add(inserted_story);
|
||||
persist_custom_world_draft_profile_update(
|
||||
ctx,
|
||||
session,
|
||||
draft_profile,
|
||||
input.submitted_at_micros,
|
||||
RpgAgentStage::ObjectRefining,
|
||||
format!("已同步 {total_inserted} 个角色草稿。"),
|
||||
"generate-characters",
|
||||
"生成角色草稿",
|
||||
)?;
|
||||
append_custom_world_action_result_message(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
&format!(
|
||||
"动作 {} 已接入最小兼容占位,后续会继续补真实编排。",
|
||||
input.action
|
||||
),
|
||||
&format!("已生成并同步 {total_inserted} 个角色草稿。"),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
let operation = build_and_insert_custom_world_operation(
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
operation_type,
|
||||
"动作已完成",
|
||||
&format!("{} 当前已走最小兼容闭环。", input.action),
|
||||
RpgAgentOperationType::GenerateCharacters,
|
||||
"角色草稿已同步",
|
||||
&format!("角色草稿已写入 draft_profile 与卡片表,新增 {total_inserted} 条。"),
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_generate_landmarks_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "generate_landmarks")?;
|
||||
let mut draft_profile = current_custom_world_draft_profile(session);
|
||||
let inserted = upsert_draft_profile_array_from_payload(
|
||||
&mut draft_profile,
|
||||
payload,
|
||||
"landmarks",
|
||||
"landmarks",
|
||||
"landmark",
|
||||
RpgAgentDraftCardKind::Landmark,
|
||||
ctx,
|
||||
&session.session_id,
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
persist_custom_world_draft_profile_update(
|
||||
ctx,
|
||||
session,
|
||||
draft_profile,
|
||||
input.submitted_at_micros,
|
||||
RpgAgentStage::ObjectRefining,
|
||||
format!("已同步 {inserted} 个地标草稿。"),
|
||||
"generate-landmarks",
|
||||
"生成地标草稿",
|
||||
)?;
|
||||
append_custom_world_action_result_message(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
&format!("已生成并同步 {inserted} 个地标草稿。"),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::GenerateLandmarks,
|
||||
"地标草稿已同步",
|
||||
&format!("地标草稿已写入 draft_profile 与卡片表,新增 {inserted} 条。"),
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_generate_role_assets_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "generate_role_assets")?;
|
||||
let next_coverage = build_role_asset_coverage_json(session, payload, true)?;
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
asset_coverage_json: Some(next_coverage),
|
||||
last_assistant_reply: Some(Some(
|
||||
"角色视觉资产槽位已生成并进入视觉打磨阶段。".to_string(),
|
||||
)),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
update_role_asset_cards(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
CustomWorldRoleAssetStatus::VisualReady,
|
||||
"角色主图已就绪",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
append_custom_world_action_result_message(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
"角色视觉资产槽位已生成,角色卡片状态已刷新。",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::GenerateRoleAssets,
|
||||
"角色资产已生成",
|
||||
"asset_coverage.roleAssets 与角色卡片视觉状态已更新。",
|
||||
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_refining_stage(session.stage, "sync_role_assets")?;
|
||||
let next_coverage = build_role_asset_coverage_json(session, payload, false)?;
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
asset_coverage_json: Some(next_coverage),
|
||||
last_assistant_reply: Some(Some("角色资产状态已按外部资产结果同步。".to_string())),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
update_role_asset_cards(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
CustomWorldRoleAssetStatus::Complete,
|
||||
"角色资产已同步",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
append_custom_world_action_result_message(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
"角色资产结果已同步到会话覆盖率与角色卡片。",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::SyncRoleAssets,
|
||||
"角色资产已同步",
|
||||
"asset_coverage.roleAssets 与角色卡片完成状态已更新。",
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_generate_scene_assets_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "generate_scene_assets")?;
|
||||
let next_coverage = build_scene_asset_coverage_json(session, payload, true)?;
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
asset_coverage_json: Some(next_coverage),
|
||||
last_assistant_reply: Some(Some(
|
||||
"场景视觉资产槽位已生成并进入视觉打磨阶段。".to_string(),
|
||||
)),
|
||||
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,
|
||||
"场景视觉资产槽位已生成,等待外层资产链写回对象结果。",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::GenerateSceneAssets,
|
||||
"场景资产已生成",
|
||||
"asset_coverage.sceneAssets 已根据当前草稿刷新。",
|
||||
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_refining_stage(session.stage, "sync_scene_assets")?;
|
||||
let next_coverage = build_scene_asset_coverage_json(session, payload, false)?;
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
stage: Some(RpgAgentStage::VisualRefining),
|
||||
asset_coverage_json: Some(next_coverage),
|
||||
last_assistant_reply: Some(Some("场景资产状态已按外部资产结果同步。".to_string())),
|
||||
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,
|
||||
"场景资产结果已同步到会话覆盖率。",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::SyncSceneAssets,
|
||||
"场景资产已同步",
|
||||
"asset_coverage.sceneAssets 已更新为同步结果。",
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn execute_expand_long_tail_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_long_tail_stage(session.stage, "expand_long_tail")?;
|
||||
let mut draft_profile = current_custom_world_draft_profile(session);
|
||||
merge_long_tail_payload(&mut draft_profile, payload);
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
RpgAgentStage::LongTailReview,
|
||||
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(if gate.publish_ready {
|
||||
RpgAgentStage::ReadyToPublish
|
||||
} else {
|
||||
RpgAgentStage::LongTailReview
|
||||
}),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
|
||||
draft_profile.clone(),
|
||||
))?)),
|
||||
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("expand-long-tail", "补齐长尾内容", session),
|
||||
)?),
|
||||
last_assistant_reply: Some(Some("长尾内容已合并,并重新计算发布门禁。".to_string())),
|
||||
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,
|
||||
"长尾内容已合并到当前世界草稿,并刷新发布门禁。",
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = complete_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::ExpandLongTail,
|
||||
"长尾内容已扩展",
|
||||
"世界草稿、预览和发布门禁已同步刷新。",
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn current_custom_world_draft_profile(
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> JsonMap<String, JsonValue> {
|
||||
ensure_minimal_draft_profile(
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref()).unwrap_or_default(),
|
||||
&session.seed_text,
|
||||
)
|
||||
}
|
||||
|
||||
fn persist_custom_world_draft_profile_update(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
draft_profile: JsonMap<String, JsonValue>,
|
||||
updated_at_micros: i64,
|
||||
stage: RpgAgentStage,
|
||||
assistant_reply: String,
|
||||
checkpoint_suffix: &str,
|
||||
checkpoint_label: &str,
|
||||
) -> Result<(), String> {
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
stage,
|
||||
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(stage),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
|
||||
draft_profile.clone(),
|
||||
))?)),
|
||||
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),
|
||||
updated_at_micros,
|
||||
)?),
|
||||
checkpoints_json: Some(append_checkpoint_json(
|
||||
&session.checkpoints_json,
|
||||
&build_session_checkpoint_value(checkpoint_suffix, checkpoint_label, session),
|
||||
)?),
|
||||
last_assistant_reply: Some(Some(assistant_reply)),
|
||||
updated_at_micros: Some(updated_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upsert_draft_profile_array_from_payload(
|
||||
draft_profile: &mut JsonMap<String, JsonValue>,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
payload_key: &str,
|
||||
profile_key: &str,
|
||||
id_prefix: &str,
|
||||
card_kind: RpgAgentDraftCardKind,
|
||||
ctx: &ReducerContext,
|
||||
session_id: &str,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<u32, String> {
|
||||
let payload_items = payload
|
||||
.get(payload_key)
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
draft_profile
|
||||
.get(profile_key)
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
if payload_items.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut merged = draft_profile
|
||||
.get(profile_key)
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut inserted = 0u32;
|
||||
for (index, item) in payload_items.into_iter().enumerate() {
|
||||
let Some(mut object) = item.as_object().cloned() else {
|
||||
continue;
|
||||
};
|
||||
let id = read_optional_text_field(&object, &["id"])
|
||||
.unwrap_or_else(|| format!("{id_prefix}-{}-{}", session_id, index + 1));
|
||||
object.insert("id".to_string(), JsonValue::String(id.clone()));
|
||||
let value = JsonValue::Object(object.clone());
|
||||
upsert_json_array_object_by_id(&mut merged, value);
|
||||
upsert_custom_world_entity_card(
|
||||
ctx,
|
||||
session_id,
|
||||
card_kind,
|
||||
&id,
|
||||
&object,
|
||||
updated_at_micros,
|
||||
)?;
|
||||
inserted = inserted.saturating_add(1);
|
||||
}
|
||||
draft_profile.insert(profile_key.to_string(), JsonValue::Array(merged));
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
fn upsert_json_array_object_by_id(items: &mut Vec<JsonValue>, next: JsonValue) {
|
||||
let Some(next_id) = next
|
||||
.get("id")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
else {
|
||||
items.push(next);
|
||||
return;
|
||||
};
|
||||
if let Some(existing) = items
|
||||
.iter_mut()
|
||||
.find(|entry| entry.get("id").and_then(JsonValue::as_str) == Some(next_id.as_str()))
|
||||
{
|
||||
*existing = next;
|
||||
} else {
|
||||
items.push(next);
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_custom_world_entity_card(
|
||||
ctx: &ReducerContext,
|
||||
session_id: &str,
|
||||
kind: RpgAgentDraftCardKind,
|
||||
entity_id: &str,
|
||||
object: &JsonMap<String, JsonValue>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<(), String> {
|
||||
let card_id = format!(
|
||||
"custom-world:{}:{}:{}",
|
||||
session_id,
|
||||
kind.as_str(),
|
||||
entity_id
|
||||
);
|
||||
let title = read_optional_text_field(object, &["name", "title"])
|
||||
.unwrap_or_else(|| entity_id.to_string());
|
||||
let subtitle =
|
||||
read_optional_text_field(object, &["role", "subtitle", "purpose"]).unwrap_or_default();
|
||||
let summary = read_optional_text_field(
|
||||
object,
|
||||
&["summary", "notes", "publicGoal", "description", "mood"],
|
||||
)
|
||||
.unwrap_or_else(|| title.clone());
|
||||
let detail_payload_json = serialize_json_value(&json!({
|
||||
"id": card_id,
|
||||
"entityId": entity_id,
|
||||
"kind": kind.as_str(),
|
||||
"title": title,
|
||||
"sections": [
|
||||
{ "id": "title", "label": "标题", "value": title },
|
||||
{ "id": "subtitle", "label": "副标题", "value": subtitle },
|
||||
{ "id": "summary", "label": "摘要", "value": summary },
|
||||
],
|
||||
"linkedIds": [entity_id],
|
||||
"locked": false,
|
||||
"editable": false,
|
||||
"editableSectionIds": [],
|
||||
"warningMessages": [],
|
||||
}))?;
|
||||
let existing = ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.card_id()
|
||||
.find(&card_id)
|
||||
.filter(|row| row.session_id == session_id);
|
||||
let next = CustomWorldDraftCard {
|
||||
card_id: card_id.clone(),
|
||||
session_id: session_id.to_string(),
|
||||
kind,
|
||||
status: RpgAgentDraftCardStatus::Suggested,
|
||||
title,
|
||||
subtitle,
|
||||
summary,
|
||||
linked_ids_json: serialize_json_value(&json!([entity_id]))?,
|
||||
warning_count: 0,
|
||||
asset_status: None,
|
||||
asset_status_label: None,
|
||||
detail_payload_json: Some(detail_payload_json),
|
||||
created_at: existing
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
.unwrap_or_else(|| Timestamp::from_micros_since_unix_epoch(updated_at_micros)),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
};
|
||||
if let Some(existing) = existing {
|
||||
replace_custom_world_draft_card(ctx, &existing, next);
|
||||
} else {
|
||||
ctx.db.custom_world_draft_card().insert(next);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_role_asset_coverage_json(
|
||||
session: &CustomWorldAgentSession,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
generated: bool,
|
||||
) -> Result<String, String> {
|
||||
let mut coverage = parse_optional_session_object(Some(&session.asset_coverage_json))
|
||||
.unwrap_or_else(JsonMap::new);
|
||||
let profile = current_custom_world_draft_profile(session);
|
||||
let mut role_assets = payload
|
||||
.get("roleAssets")
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| build_role_asset_entries_from_profile(&profile, generated));
|
||||
if role_assets.is_empty() {
|
||||
role_assets = build_role_asset_entries_from_profile(&profile, generated);
|
||||
}
|
||||
let all_ready = !role_assets.is_empty()
|
||||
&& role_assets
|
||||
.iter()
|
||||
.all(|entry| asset_entry_ready(entry, &["visualReady", "animationsReady"]));
|
||||
coverage.insert("roleAssets".to_string(), JsonValue::Array(role_assets));
|
||||
coverage.insert("allRoleAssetsReady".to_string(), JsonValue::Bool(all_ready));
|
||||
coverage
|
||||
.entry("sceneAssets".to_string())
|
||||
.or_insert_with(|| JsonValue::Array(Vec::new()));
|
||||
coverage
|
||||
.entry("allSceneAssetsReady".to_string())
|
||||
.or_insert_with(|| JsonValue::Bool(false));
|
||||
serialize_json_value(&JsonValue::Object(coverage))
|
||||
}
|
||||
|
||||
fn build_scene_asset_coverage_json(
|
||||
session: &CustomWorldAgentSession,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
generated: bool,
|
||||
) -> Result<String, String> {
|
||||
let mut coverage = parse_optional_session_object(Some(&session.asset_coverage_json))
|
||||
.unwrap_or_else(JsonMap::new);
|
||||
let profile = current_custom_world_draft_profile(session);
|
||||
let mut scene_assets = payload
|
||||
.get("sceneAssets")
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| build_scene_asset_entries_from_profile(&profile, generated));
|
||||
if scene_assets.is_empty() {
|
||||
scene_assets = build_scene_asset_entries_from_profile(&profile, generated);
|
||||
}
|
||||
let all_ready = !scene_assets.is_empty()
|
||||
&& scene_assets
|
||||
.iter()
|
||||
.all(|entry| asset_entry_ready(entry, &["visualReady", "synced"]));
|
||||
coverage.insert("sceneAssets".to_string(), JsonValue::Array(scene_assets));
|
||||
coverage.insert(
|
||||
"allSceneAssetsReady".to_string(),
|
||||
JsonValue::Bool(all_ready),
|
||||
);
|
||||
coverage
|
||||
.entry("roleAssets".to_string())
|
||||
.or_insert_with(|| JsonValue::Array(Vec::new()));
|
||||
coverage
|
||||
.entry("allRoleAssetsReady".to_string())
|
||||
.or_insert_with(|| JsonValue::Bool(false));
|
||||
serialize_json_value(&JsonValue::Object(coverage))
|
||||
}
|
||||
|
||||
fn build_role_asset_entries_from_profile(
|
||||
profile: &JsonMap<String, JsonValue>,
|
||||
generated: bool,
|
||||
) -> Vec<JsonValue> {
|
||||
collect_profile_entities(profile, &["playableNpcs", "storyNpcs"])
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let id = entry
|
||||
.get("id")
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or("role");
|
||||
json!({
|
||||
"roleId": id,
|
||||
"name": read_optional_text_field(&entry, &["name", "title"]).unwrap_or_else(|| id.to_string()),
|
||||
"visualReady": generated,
|
||||
"animationsReady": !generated,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_scene_asset_entries_from_profile(
|
||||
profile: &JsonMap<String, JsonValue>,
|
||||
generated: bool,
|
||||
) -> Vec<JsonValue> {
|
||||
collect_profile_entities(profile, &["landmarks", "sceneChapters", "sceneChapterBlueprints"])
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let id = entry
|
||||
.get("id")
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or("scene");
|
||||
json!({
|
||||
"sceneId": id,
|
||||
"name": read_optional_text_field(&entry, &["name", "title"]).unwrap_or_else(|| id.to_string()),
|
||||
"visualReady": generated,
|
||||
"synced": !generated,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_profile_entities(
|
||||
profile: &JsonMap<String, JsonValue>,
|
||||
keys: &[&str],
|
||||
) -> Vec<JsonMap<String, JsonValue>> {
|
||||
let mut result = Vec::new();
|
||||
for key in keys {
|
||||
if let Some(entries) = profile.get(*key).and_then(JsonValue::as_array) {
|
||||
for entry in entries {
|
||||
if let Some(object) = entry.as_object() {
|
||||
result.push(object.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn asset_entry_ready(entry: &JsonValue, keys: &[&str]) -> bool {
|
||||
keys.iter().all(|key| {
|
||||
entry
|
||||
.get(*key)
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn update_role_asset_cards(
|
||||
ctx: &ReducerContext,
|
||||
session_id: &str,
|
||||
status: CustomWorldRoleAssetStatus,
|
||||
label: &str,
|
||||
updated_at_micros: i64,
|
||||
) {
|
||||
for card in
|
||||
ctx.db.custom_world_draft_card().iter().filter(|row| {
|
||||
row.session_id == session_id && row.kind == RpgAgentDraftCardKind::Character
|
||||
})
|
||||
{
|
||||
replace_custom_world_draft_card(
|
||||
ctx,
|
||||
&card,
|
||||
CustomWorldDraftCard {
|
||||
card_id: card.card_id.clone(),
|
||||
session_id: card.session_id.clone(),
|
||||
kind: card.kind,
|
||||
status: card.status,
|
||||
title: card.title.clone(),
|
||||
subtitle: card.subtitle.clone(),
|
||||
summary: card.summary.clone(),
|
||||
linked_ids_json: card.linked_ids_json.clone(),
|
||||
warning_count: card.warning_count,
|
||||
asset_status: Some(status),
|
||||
asset_status_label: Some(label.to_string()),
|
||||
detail_payload_json: card.detail_payload_json.clone(),
|
||||
created_at: card.created_at,
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_long_tail_payload(
|
||||
draft_profile: &mut JsonMap<String, JsonValue>,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) {
|
||||
for key in [
|
||||
"coreConflicts",
|
||||
"chapters",
|
||||
"sceneChapters",
|
||||
"sceneChapterBlueprints",
|
||||
"sidequestSeeds",
|
||||
"carrierHooks",
|
||||
] {
|
||||
if let Some(entries) = payload.get(key).and_then(JsonValue::as_array) {
|
||||
let mut merged = draft_profile
|
||||
.get(key)
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
for entry in entries {
|
||||
if let Some(object) = entry.as_object() {
|
||||
upsert_json_array_object_by_id(&mut merged, JsonValue::Object(object.clone()));
|
||||
} else if !merged.contains(entry) {
|
||||
merged.push(entry.clone());
|
||||
}
|
||||
}
|
||||
draft_profile.insert(key.to_string(), JsonValue::Array(merged));
|
||||
}
|
||||
}
|
||||
for key in ["worldHook", "playerPremise", "summary", "subtitle"] {
|
||||
if let Some(value) = payload
|
||||
.get(key)
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
draft_profile.insert(key.to_string(), JsonValue::String(value.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct CustomWorldAgentSessionPatch {
|
||||
current_turn: Option<u32>,
|
||||
@@ -3310,24 +4044,6 @@ fn ensure_publishable_stage(stage: RpgAgentStage, action: &str) -> Result<(), St
|
||||
ensure_long_tail_stage(stage, action)
|
||||
}
|
||||
|
||||
fn map_action_name_to_operation_type(action: &str) -> Option<RpgAgentOperationType> {
|
||||
match action {
|
||||
"draft_foundation" => Some(RpgAgentOperationType::DraftFoundation),
|
||||
"update_draft_card" => Some(RpgAgentOperationType::UpdateDraftCard),
|
||||
"sync_result_profile" => Some(RpgAgentOperationType::SyncResultProfile),
|
||||
"generate_characters" => Some(RpgAgentOperationType::GenerateCharacters),
|
||||
"generate_landmarks" => Some(RpgAgentOperationType::GenerateLandmarks),
|
||||
"generate_role_assets" => Some(RpgAgentOperationType::GenerateRoleAssets),
|
||||
"sync_role_assets" => Some(RpgAgentOperationType::SyncRoleAssets),
|
||||
"generate_scene_assets" => Some(RpgAgentOperationType::GenerateSceneAssets),
|
||||
"sync_scene_assets" => Some(RpgAgentOperationType::SyncSceneAssets),
|
||||
"expand_long_tail" => Some(RpgAgentOperationType::ExpandLongTail),
|
||||
"publish_world" => Some(RpgAgentOperationType::PublishWorld),
|
||||
"revert_checkpoint" => Some(RpgAgentOperationType::RevertCheckpoint),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rpg_agent_stage(value: &str) -> Option<RpgAgentStage> {
|
||||
match value.trim() {
|
||||
"collecting_intent" => Some(RpgAgentStage::CollectingIntent),
|
||||
|
||||
Reference in New Issue
Block a user