fix custom world agent draft profile identity
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
# Agent 草稿编译后重复生成草稿修复 2026-04-23
|
||||||
|
|
||||||
|
更新时间:`2026-04-23`
|
||||||
|
|
||||||
|
## 1. 问题现象
|
||||||
|
|
||||||
|
当前创作链里,用户打开一个已有 Agent 草稿,继续编辑并触发结果页编译或自动保存后,作品库里会新增一份 draft,而不是更新原来的那一份。
|
||||||
|
|
||||||
|
这会带来两个直接问题:
|
||||||
|
|
||||||
|
1. 同一个会话在作品库里出现多份草稿
|
||||||
|
2. 原草稿状态没有被正确推进,导致发布、恢复、继续创作都可能命中旧条目
|
||||||
|
|
||||||
|
## 2. 根因拆解
|
||||||
|
|
||||||
|
本次问题不是单点,而是两段身份链没有收口到同一套规则:
|
||||||
|
|
||||||
|
### 2.1 `sync_result_profile` 会直接覆盖 session 里的 `draft_profile_json.id`
|
||||||
|
|
||||||
|
当前 `sync_result_profile` 会把结果页传回来的 `profile` 直接写回 `draft_profile_json`。
|
||||||
|
|
||||||
|
如果结果页上的 `profile.id` 已经不是原草稿 id,那么:
|
||||||
|
|
||||||
|
1. session 主链中的 `draft_profile_json.id` 会被改成新 id
|
||||||
|
2. `resultPreview.preview.id` 也会跟着变成新 id
|
||||||
|
3. 前端 autosave 会拿着这个新 id 去调作品库 `PUT /custom-world-library/:profileId`
|
||||||
|
|
||||||
|
### 2.2 作品库 upsert 只按 `profile_id` 命中,不按 `source_agent_session_id` 兜底
|
||||||
|
|
||||||
|
当前作品库落库路径:
|
||||||
|
|
||||||
|
`结果页 autosave -> PUT /custom-world-library/:profileId -> upsert_custom_world_profile_record`
|
||||||
|
|
||||||
|
其中普通 autosave 路径此前存在两个问题:
|
||||||
|
|
||||||
|
1. 前端没有透传 `sourceAgentSessionId`
|
||||||
|
2. 后端普通 upsert 只按 `(owner_user_id, profile_id)` 查旧记录
|
||||||
|
|
||||||
|
所以一旦 `profile.id` 漂移,后端就会把它当作一条新的 draft 插入。
|
||||||
|
|
||||||
|
## 3. 修复目标
|
||||||
|
|
||||||
|
本轮修复要求同时满足下面两条:
|
||||||
|
|
||||||
|
1. Agent 草稿结果页继续编辑时,必须优先继承当前 session 已有的稳定 `profileId`
|
||||||
|
2. 即使前端传来的 `profile.id` 已经漂移,作品库 upsert 也要能按 `source_agent_session_id` 命中同一份 draft 并更新
|
||||||
|
|
||||||
|
## 4. 本轮落地策略
|
||||||
|
|
||||||
|
### 4.1 session 主链侧:稳定保留草稿 id
|
||||||
|
|
||||||
|
在 `sync_result_profile` 中,若当前 session 已经存在草稿身份:
|
||||||
|
|
||||||
|
1. 优先读取 `draft_profile_json.legacyResultProfile.id`
|
||||||
|
2. 其次读取 `draft_profile_json.id`
|
||||||
|
3. 若命中稳定 id,则把传入 profile 的:
|
||||||
|
- 顶层 `id`
|
||||||
|
- `legacyResultProfile.id`
|
||||||
|
都强制回写为这个稳定 id
|
||||||
|
|
||||||
|
这样可以保证:
|
||||||
|
|
||||||
|
1. `draft_profile_json.id` 不会被结果页里的漂移 id 覆盖
|
||||||
|
2. `resultPreview.preview.id` 会持续稳定
|
||||||
|
3. 前端后续 autosave 会继续更新原草稿
|
||||||
|
|
||||||
|
### 4.2 作品库保存侧:透传 `sourceAgentSessionId`
|
||||||
|
|
||||||
|
前端 `upsertRpgWorldProfile(...)` 新增可选参数:
|
||||||
|
|
||||||
|
`sourceAgentSessionId?: string | null`
|
||||||
|
|
||||||
|
结果页属于 Agent 草稿视图时,autosave 会把 `activeAgentSessionId` 一并传给作品库接口。
|
||||||
|
|
||||||
|
### 4.3 后端 upsert 侧:按 session 命中旧 draft
|
||||||
|
|
||||||
|
普通作品库 `PUT /custom-world-library/:profileId` 接口新增读取 `sourceAgentSessionId`。
|
||||||
|
|
||||||
|
Spacetime `upsert_custom_world_profile_record(...)` 在按 `profile_id` 未命中时,新增二级兜底:
|
||||||
|
|
||||||
|
1. `owner_user_id` 相同
|
||||||
|
2. `publication_status == draft`
|
||||||
|
3. `deleted_at == None`
|
||||||
|
4. `source_agent_session_id == input.source_agent_session_id`
|
||||||
|
|
||||||
|
若命中这条旧 draft:
|
||||||
|
|
||||||
|
1. 删除旧 row
|
||||||
|
2. 使用旧 row 的 `profile_id`
|
||||||
|
3. 更新 payload / metadata / updated_at
|
||||||
|
|
||||||
|
这样即使前端 path 参数已经是新 id,也仍然会命中原草稿并更新,而不是再插入第二份草稿。
|
||||||
|
|
||||||
|
## 5. 验收标准
|
||||||
|
|
||||||
|
修复后应满足:
|
||||||
|
|
||||||
|
1. 打开已有 Agent draft 后继续编译,不会新增第二份 draft
|
||||||
|
2. 原 draft 的 `profileId` 保持不变
|
||||||
|
3. `resultPreview.preview.id` 与作品库 `profileId` 一致
|
||||||
|
4. 自动保存、继续创作、进入世界、发布前检查都围绕同一份草稿身份工作
|
||||||
|
|
||||||
|
## 6. 结论
|
||||||
|
|
||||||
|
这次问题的本质不是“自动保存重复调用”,而是:
|
||||||
|
|
||||||
|
**Agent 草稿在 session 主链和作品库 upsert 两端都缺少稳定身份约束。**
|
||||||
|
|
||||||
|
本轮通过“session 保 id + 作品库按 session 兜底命中”双保险,把同一份草稿重新收口为单一身份。
|
||||||
@@ -148,7 +148,7 @@ pub async fn put_custom_world_library_profile(
|
|||||||
.upsert_custom_world_profile(CustomWorldProfileUpsertRecordInput {
|
.upsert_custom_world_profile(CustomWorldProfileUpsertRecordInput {
|
||||||
profile_id: profile_id.clone(),
|
profile_id: profile_id.clone(),
|
||||||
owner_user_id: owner_user_id.clone(),
|
owner_user_id: owner_user_id.clone(),
|
||||||
source_agent_session_id: None,
|
source_agent_session_id: payload.source_agent_session_id.clone(),
|
||||||
world_name: metadata.world_name,
|
world_name: metadata.world_name,
|
||||||
subtitle: metadata.subtitle,
|
subtitle: metadata.subtitle,
|
||||||
summary_text: metadata.summary_text,
|
summary_text: metadata.summary_text,
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ pub struct RuntimeInventoryStateResponse {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CustomWorldProfileUpsertRequest {
|
pub struct CustomWorldProfileUpsertRequest {
|
||||||
pub profile: serde_json::Value,
|
pub profile: serde_json::Value,
|
||||||
|
pub source_agent_session_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
|||||||
@@ -993,7 +993,18 @@ fn upsert_custom_world_profile_record(
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.profile_id()
|
.profile_id()
|
||||||
.find(&input.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 {
|
let next_row = match current {
|
||||||
Some(existing) => {
|
Some(existing) => {
|
||||||
@@ -1798,11 +1809,16 @@ fn execute_sync_result_profile_action(
|
|||||||
payload: &JsonMap<String, JsonValue>,
|
payload: &JsonMap<String, JsonValue>,
|
||||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||||
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
||||||
let profile = payload
|
let mut profile = payload
|
||||||
.get("profile")
|
.get("profile")
|
||||||
.and_then(JsonValue::as_object)
|
.and_then(JsonValue::as_object)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| "sync_result_profile requires profile".to_string())?;
|
.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 draft_profile = ensure_minimal_draft_profile(profile, &session.seed_text);
|
||||||
let gate = summarize_publish_gate_from_json(
|
let gate = summarize_publish_gate_from_json(
|
||||||
&session.session_id,
|
&session.session_id,
|
||||||
@@ -1854,6 +1870,35 @@ fn execute_sync_result_profile_action(
|
|||||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
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(
|
fn execute_publish_world_action(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
session: &CustomWorldAgentSession,
|
session: &CustomWorldAgentSession,
|
||||||
|
|||||||
@@ -3954,7 +3954,18 @@ fn upsert_custom_world_profile_record(
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.profile_id()
|
.profile_id()
|
||||||
.find(&input.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 {
|
let next_row = match current {
|
||||||
Some(existing) => {
|
Some(existing) => {
|
||||||
@@ -4790,11 +4801,16 @@ fn execute_sync_result_profile_action(
|
|||||||
payload: &JsonMap<String, JsonValue>,
|
payload: &JsonMap<String, JsonValue>,
|
||||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||||
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
ensure_refining_stage(session.stage, "sync_result_profile")?;
|
||||||
let profile = payload
|
let mut profile = payload
|
||||||
.get("profile")
|
.get("profile")
|
||||||
.and_then(JsonValue::as_object)
|
.and_then(JsonValue::as_object)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| "sync_result_profile requires profile".to_string())?;
|
.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 draft_profile = ensure_minimal_draft_profile(profile, &session.seed_text);
|
||||||
let gate = summarize_publish_gate_from_json(
|
let gate = summarize_publish_gate_from_json(
|
||||||
&session.session_id,
|
&session.session_id,
|
||||||
@@ -4850,6 +4866,35 @@ fn execute_sync_result_profile_action(
|
|||||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
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(
|
fn execute_publish_world_action(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
session: &CustomWorldAgentSession,
|
session: &CustomWorldAgentSession,
|
||||||
@@ -9111,3 +9156,133 @@ fn append_big_fish_system_message(
|
|||||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
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",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1763,10 +1763,16 @@ test('agent draft result auto-save persists the latest profile rebuilt from sync
|
|||||||
const latestSavedProfile = vi
|
const latestSavedProfile = vi
|
||||||
.mocked(upsertRpgWorldProfile)
|
.mocked(upsertRpgWorldProfile)
|
||||||
.mock.calls.at(-1)?.[0];
|
.mock.calls.at(-1)?.[0];
|
||||||
|
const latestSaveRequest = vi
|
||||||
|
.mocked(upsertRpgWorldProfile)
|
||||||
|
.mock.calls.at(-1)?.[1];
|
||||||
expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
|
expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
|
||||||
expect(latestSavedProfile?.summary).toBe(
|
expect(latestSavedProfile?.summary).toBe(
|
||||||
'作品库应该保存这份同步后的最新快照。',
|
'作品库应该保存这份同步后的最新快照。',
|
||||||
);
|
);
|
||||||
|
expect(latestSaveRequest).toEqual({
|
||||||
|
sourceAgentSessionId: 'custom-world-agent-session-1',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('agent draft result can open from server result preview without embedded legacyResultProfile', async () => {
|
test('agent draft result can open from server result preview without embedded legacyResultProfile', async () => {
|
||||||
|
|||||||
@@ -126,7 +126,15 @@ export function useRpgCreationResultAutosave(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const mutation =
|
const mutation =
|
||||||
await upsertRpgWorldProfile(normalizedProfile);
|
await upsertRpgWorldProfile(
|
||||||
|
normalizedProfile,
|
||||||
|
{
|
||||||
|
sourceAgentSessionId:
|
||||||
|
isAgentDraftResultView && activeAgentSessionId
|
||||||
|
? activeAgentSessionId
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
||||||
return mutation;
|
return mutation;
|
||||||
}
|
}
|
||||||
@@ -159,7 +167,9 @@ export function useRpgCreationResultAutosave(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
activeAgentSessionId,
|
||||||
generatedCustomWorldProfile,
|
generatedCustomWorldProfile,
|
||||||
|
isAgentDraftResultView,
|
||||||
refreshCustomWorldWorks,
|
refreshCustomWorldWorks,
|
||||||
setSavedCustomWorldEntries,
|
setSavedCustomWorldEntries,
|
||||||
setSelectedDetailEntry,
|
setSelectedDetailEntry,
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export async function listRpgWorldLibrary(
|
|||||||
|
|
||||||
export async function upsertRpgWorldProfile(
|
export async function upsertRpgWorldProfile(
|
||||||
profile: CustomWorldProfile,
|
profile: CustomWorldProfile,
|
||||||
|
request: {
|
||||||
|
sourceAgentSessionId?: string | null;
|
||||||
|
} = {},
|
||||||
options: RpgCreationRuntimeRequestOptions = {},
|
options: RpgCreationRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
const response = await requestRpgCreationRuntimeJson<
|
const response = await requestRpgCreationRuntimeJson<
|
||||||
@@ -39,6 +42,7 @@ export async function upsertRpgWorldProfile(
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
profile,
|
profile,
|
||||||
|
sourceAgentSessionId: request.sourceAgentSessionId ?? null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'保存自定义世界失败',
|
'保存自定义世界失败',
|
||||||
|
|||||||
Reference in New Issue
Block a user