Close DDD cleanup and tests-support closure

This commit is contained in:
2026-04-30 16:15:05 +08:00
parent 7ab0933f6d
commit fd08262bf0
81 changed files with 8415 additions and 6662 deletions

View File

@@ -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),