fix custom world agent draft profile identity

This commit is contained in:
2026-04-23 13:54:38 +08:00
parent 1e200ec5ba
commit da7c1ff0c5
8 changed files with 356 additions and 6 deletions

View File

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

View File

@@ -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",
));
}
}