fix custom world agent draft profile identity
This commit is contained in:
@@ -993,7 +993,18 @@ fn upsert_custom_world_profile_record(
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id);
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.or_else(|| {
|
||||
input.source_agent_session_id.as_ref().and_then(|session_id| {
|
||||
ctx.db.custom_world_profile().iter().find(|row| {
|
||||
is_same_agent_draft_profile_candidate(
|
||||
row,
|
||||
&input.owner_user_id,
|
||||
session_id,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let next_row = match current {
|
||||
Some(existing) => {
|
||||
@@ -1798,11 +1809,16 @@ fn execute_sync_result_profile_action(
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
||||
let profile = payload
|
||||
let mut profile = payload
|
||||
.get("profile")
|
||||
.and_then(JsonValue::as_object)
|
||||
.cloned()
|
||||
.ok_or_else(|| "sync_result_profile requires profile".to_string())?;
|
||||
if let Some(stable_profile_id) = resolve_stable_agent_draft_profile_id(session) {
|
||||
// 结果页回写时必须沿用当前草稿的稳定身份,避免把同一草稿写成新条目。
|
||||
profile.insert("id".to_string(), JsonValue::String(stable_profile_id.clone()));
|
||||
upsert_nested_result_profile_id(&mut profile, &stable_profile_id);
|
||||
}
|
||||
let draft_profile = ensure_minimal_draft_profile(profile, &session.seed_text);
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
@@ -1854,6 +1870,35 @@ fn execute_sync_result_profile_action(
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn resolve_stable_agent_draft_profile_id(session: &CustomWorldAgentSession) -> Option<String> {
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref()).and_then(|profile| {
|
||||
read_optional_text_field(&profile, &["legacyResultProfile.id", "id"])
|
||||
})
|
||||
}
|
||||
|
||||
fn upsert_nested_result_profile_id(profile: &mut JsonMap<String, JsonValue>, stable_profile_id: &str) {
|
||||
let legacy_result_profile = profile
|
||||
.entry("legacyResultProfile".to_string())
|
||||
.or_insert_with(|| JsonValue::Object(JsonMap::new()));
|
||||
if let Some(object) = legacy_result_profile.as_object_mut() {
|
||||
object.insert(
|
||||
"id".to_string(),
|
||||
JsonValue::String(stable_profile_id.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_same_agent_draft_profile_candidate(
|
||||
row: &CustomWorldProfile,
|
||||
owner_user_id: &str,
|
||||
source_agent_session_id: &str,
|
||||
) -> bool {
|
||||
row.owner_user_id == owner_user_id
|
||||
&& row.deleted_at.is_none()
|
||||
&& row.publication_status == CustomWorldPublicationStatus::Draft
|
||||
&& row.source_agent_session_id.as_deref() == Some(source_agent_session_id)
|
||||
}
|
||||
|
||||
fn execute_publish_world_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
|
||||
@@ -3954,7 +3954,18 @@ fn upsert_custom_world_profile_record(
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id);
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.or_else(|| {
|
||||
input.source_agent_session_id.as_ref().and_then(|session_id| {
|
||||
ctx.db.custom_world_profile().iter().find(|row| {
|
||||
is_same_agent_draft_profile_candidate(
|
||||
row,
|
||||
&input.owner_user_id,
|
||||
session_id,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let next_row = match current {
|
||||
Some(existing) => {
|
||||
@@ -4790,11 +4801,16 @@ fn execute_sync_result_profile_action(
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
||||
let profile = payload
|
||||
let mut profile = payload
|
||||
.get("profile")
|
||||
.and_then(JsonValue::as_object)
|
||||
.cloned()
|
||||
.ok_or_else(|| "sync_result_profile requires profile".to_string())?;
|
||||
if let Some(stable_profile_id) = resolve_stable_agent_draft_profile_id(session) {
|
||||
// 结果页回写时必须沿用当前草稿的稳定身份,避免把同一草稿写成新条目。
|
||||
profile.insert("id".to_string(), JsonValue::String(stable_profile_id.clone()));
|
||||
upsert_nested_result_profile_id(&mut profile, &stable_profile_id);
|
||||
}
|
||||
let draft_profile = ensure_minimal_draft_profile(profile, &session.seed_text);
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
@@ -4850,6 +4866,35 @@ fn execute_sync_result_profile_action(
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn resolve_stable_agent_draft_profile_id(session: &CustomWorldAgentSession) -> Option<String> {
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref()).and_then(|profile| {
|
||||
read_optional_text_field(&profile, &["legacyResultProfile.id", "id"])
|
||||
})
|
||||
}
|
||||
|
||||
fn upsert_nested_result_profile_id(profile: &mut JsonMap<String, JsonValue>, stable_profile_id: &str) {
|
||||
let legacy_result_profile = profile
|
||||
.entry("legacyResultProfile".to_string())
|
||||
.or_insert_with(|| JsonValue::Object(JsonMap::new()));
|
||||
if let Some(object) = legacy_result_profile.as_object_mut() {
|
||||
object.insert(
|
||||
"id".to_string(),
|
||||
JsonValue::String(stable_profile_id.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_same_agent_draft_profile_candidate(
|
||||
row: &CustomWorldProfile,
|
||||
owner_user_id: &str,
|
||||
source_agent_session_id: &str,
|
||||
) -> bool {
|
||||
row.owner_user_id == owner_user_id
|
||||
&& row.deleted_at.is_none()
|
||||
&& row.publication_status == CustomWorldPublicationStatus::Draft
|
||||
&& row.source_agent_session_id.as_deref() == Some(source_agent_session_id)
|
||||
}
|
||||
|
||||
fn execute_publish_world_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
@@ -9111,3 +9156,133 @@ fn append_big_fish_system_message(
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||
let session = CustomWorldAgentSession {
|
||||
session_id: "session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
seed_text: "seed".to_string(),
|
||||
current_turn: 1,
|
||||
progress_percent: 100,
|
||||
stage: RpgAgentStage::ObjectRefining,
|
||||
focus_card_id: None,
|
||||
anchor_content_json: "{}".to_string(),
|
||||
creator_intent_json: None,
|
||||
creator_intent_readiness_json: "{}".to_string(),
|
||||
anchor_pack_json: None,
|
||||
lock_state_json: None,
|
||||
draft_profile_json: Some(
|
||||
r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#
|
||||
.to_string(),
|
||||
),
|
||||
last_assistant_reply: None,
|
||||
publish_gate_json: None,
|
||||
result_preview_json: None,
|
||||
pending_clarifications_json: "[]".to_string(),
|
||||
quality_findings_json: "[]".to_string(),
|
||||
suggested_actions_json: "[]".to_string(),
|
||||
recommended_replies_json: "[]".to_string(),
|
||||
asset_coverage_json: "{}".to_string(),
|
||||
checkpoints_json: "[]".to_string(),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_stable_agent_draft_profile_id(&session),
|
||||
Some("stable-profile".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_agent_draft_profile_candidate_requires_same_owner_active_draft_and_session() {
|
||||
let matching = CustomWorldProfile {
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_agent_session_id: Some("session-1".to_string()),
|
||||
publication_status: CustomWorldPublicationStatus::Draft,
|
||||
world_name: "潮雾列岛".to_string(),
|
||||
subtitle: String::new(),
|
||||
summary_text: String::new(),
|
||||
theme_mode: CustomWorldThemeMode::Mythic,
|
||||
cover_image_src: None,
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
let deleted = CustomWorldProfile {
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_agent_session_id: Some("session-1".to_string()),
|
||||
publication_status: CustomWorldPublicationStatus::Draft,
|
||||
world_name: "潮雾列岛".to_string(),
|
||||
subtitle: String::new(),
|
||||
summary_text: String::new(),
|
||||
theme_mode: CustomWorldThemeMode::Mythic,
|
||||
cover_image_src: None,
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: Some(Timestamp::from_micros_since_unix_epoch(2)),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
let published = CustomWorldProfile {
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_agent_session_id: Some("session-1".to_string()),
|
||||
publication_status: CustomWorldPublicationStatus::Published,
|
||||
world_name: "潮雾列岛".to_string(),
|
||||
subtitle: String::new(),
|
||||
summary_text: String::new(),
|
||||
theme_mode: CustomWorldThemeMode::Mythic,
|
||||
cover_image_src: None,
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
|
||||
assert!(is_same_agent_draft_profile_candidate(
|
||||
&matching,
|
||||
"user-1",
|
||||
"session-1",
|
||||
));
|
||||
assert!(!is_same_agent_draft_profile_candidate(
|
||||
&matching,
|
||||
"user-2",
|
||||
"session-1",
|
||||
));
|
||||
assert!(!is_same_agent_draft_profile_candidate(
|
||||
&matching,
|
||||
"user-1",
|
||||
"session-2",
|
||||
));
|
||||
assert!(!is_same_agent_draft_profile_candidate(
|
||||
&deleted,
|
||||
"user-1",
|
||||
"session-1",
|
||||
));
|
||||
assert!(!is_same_agent_draft_profile_candidate(
|
||||
&published,
|
||||
"user-1",
|
||||
"session-1",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user