fix:修复无消息草稿保存
优化首次加载速度
This commit is contained in:
@@ -182,8 +182,7 @@ pub(crate) fn create_big_fish_session_tx(
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
seed_text: input.seed_text.trim().to_string(),
|
||||
current_turn: 0,
|
||||
// 中文注释:欢迎语和种子推断只是初始上下文,不代表创作者已经推进了共创流程。
|
||||
progress_percent: 0,
|
||||
progress_percent: 20,
|
||||
stage: BigFishCreationStage::CollectingAnchors,
|
||||
anchor_pack_json: serialize_anchor_pack(&anchor_pack)
|
||||
.map_err(|error| error.to_string())?,
|
||||
@@ -239,7 +238,9 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||
})
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -252,6 +253,24 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSession) -> bool {
|
||||
if big_fish_session_has_direct_work_content(row) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db.big_fish_agent_message().iter().any(|message| {
|
||||
message.session_id == row.session_id
|
||||
&& matches!(message.role, BigFishAgentMessageRole::User)
|
||||
})
|
||||
}
|
||||
|
||||
fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool {
|
||||
// 助手欢迎语和默认 anchorPack 只是工作台初始状态,不应被当成草稿作品。
|
||||
!row.seed_text.trim().is_empty()
|
||||
|| row.draft_json.is_some()
|
||||
|| row.stage == BigFishCreationStage::Published
|
||||
}
|
||||
|
||||
pub(crate) fn delete_big_fish_work_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishWorkDeleteInput,
|
||||
@@ -688,3 +707,53 @@ pub(crate) fn append_big_fish_system_message(
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn build_test_big_fish_session(
|
||||
seed_text: &str,
|
||||
draft_json: Option<&str>,
|
||||
stage: BigFishCreationStage,
|
||||
) -> BigFishCreationSession {
|
||||
BigFishCreationSession {
|
||||
session_id: "big-fish-session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
seed_text: seed_text.to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 20,
|
||||
stage,
|
||||
anchor_pack_json: "{}".to_string(),
|
||||
draft_json: draft_json.map(str::to_string),
|
||||
asset_coverage_json: "{}".to_string(),
|
||||
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||
publish_ready: false,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_direct_work_content_ignores_empty_created_session() {
|
||||
let empty_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::CollectingAnchors);
|
||||
let seeded_session = build_test_big_fish_session(
|
||||
"想做深海吞噬成长",
|
||||
None,
|
||||
BigFishCreationStage::CollectingAnchors,
|
||||
);
|
||||
let drafted_session = build_test_big_fish_session(
|
||||
"",
|
||||
Some(r#"{"title":"深海吞噬"}"#),
|
||||
BigFishCreationStage::DraftReady,
|
||||
);
|
||||
let published_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::Published);
|
||||
|
||||
assert!(!big_fish_session_has_direct_work_content(&empty_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&seeded_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&drafted_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&published_session));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = custom_world_profile,
|
||||
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
||||
@@ -1457,10 +1459,14 @@ fn list_custom_world_work_snapshots(
|
||||
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut active_agent_session_ids = HashSet::new();
|
||||
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.stage != RpgAgentStage::Published
|
||||
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||
}) {
|
||||
active_agent_session_ids.insert(session.session_id.clone());
|
||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
||||
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
||||
@@ -1504,6 +1510,7 @@ fn list_custom_world_work_snapshots(
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||
.filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids))
|
||||
{
|
||||
items.push(CustomWorldWorkSummarySnapshot {
|
||||
work_id: format!("published:{}", profile.profile_id),
|
||||
@@ -1558,6 +1565,63 @@ fn list_custom_world_work_snapshots(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_custom_world_agent_session_work(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
if custom_world_agent_session_has_direct_work_content(session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||
message.session_id == session.session_id && matches!(message.role, RpgAgentMessageRole::User)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.any(|card| card.session_id == session.session_id)
|
||||
}
|
||||
|
||||
fn custom_world_agent_session_has_direct_work_content(
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||
!session.seed_text.trim().is_empty()
|
||||
|| matches!(
|
||||
session.stage,
|
||||
RpgAgentStage::ObjectRefining
|
||||
| RpgAgentStage::VisualRefining
|
||||
| RpgAgentStage::LongTailReview
|
||||
| RpgAgentStage::ReadyToPublish
|
||||
| RpgAgentStage::Published
|
||||
)
|
||||
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.as_ref()
|
||||
.is_some_and(|profile| !profile.is_empty())
|
||||
}
|
||||
|
||||
fn should_include_custom_world_profile_work(
|
||||
row: &CustomWorldProfile,
|
||||
active_agent_session_ids: &HashSet<String>,
|
||||
) -> bool {
|
||||
// 已发布 profile 是正式作品;即使来源会话还存在,也必须保留独立入口。
|
||||
if row.publication_status == CustomWorldPublicationStatus::Published {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未发布 profile 若来源于仍可继续聊天的 Agent 会话,只是同一草稿的编译产物,
|
||||
// works 里保留 agent_session 即可,避免草稿分组显示两份同名作品。
|
||||
row.source_agent_session_id
|
||||
.as_ref()
|
||||
.map_or(true, |session_id| {
|
||||
!active_agent_session_ids.contains(session_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_custom_world_agent_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentSessionGetInput,
|
||||
@@ -3708,7 +3772,7 @@ fn parse_json_array_or_empty(raw: &str) -> Vec<JsonValue> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||
payload.get(array_key).and_then(JsonValue::as_array).and_then(|values| values.first()).and_then(JsonValue::as_str)
|
||||
.or_else(|| payload.get(scalar_key).and_then(JsonValue::as_str))
|
||||
.map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned)
|
||||
|
||||
@@ -2930,7 +2930,9 @@ fn list_custom_world_work_snapshots(
|
||||
let mut active_agent_session_ids = HashSet::new();
|
||||
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.stage != RpgAgentStage::Published
|
||||
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||
}) {
|
||||
active_agent_session_ids.insert(session.session_id.clone());
|
||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||
@@ -3031,6 +3033,44 @@ fn list_custom_world_work_snapshots(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_custom_world_agent_session_work(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
if custom_world_agent_session_has_direct_work_content(session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||
message.session_id == session.session_id
|
||||
&& matches!(message.role, RpgAgentMessageRole::User)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.any(|card| card.session_id == session.session_id)
|
||||
}
|
||||
|
||||
fn custom_world_agent_session_has_direct_work_content(session: &CustomWorldAgentSession) -> bool {
|
||||
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||
!session.seed_text.trim().is_empty()
|
||||
|| matches!(
|
||||
session.stage,
|
||||
RpgAgentStage::ObjectRefining
|
||||
| RpgAgentStage::VisualRefining
|
||||
| RpgAgentStage::LongTailReview
|
||||
| RpgAgentStage::ReadyToPublish
|
||||
| RpgAgentStage::Published
|
||||
)
|
||||
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.as_ref()
|
||||
.is_some_and(|profile| !profile.is_empty())
|
||||
}
|
||||
|
||||
fn should_include_custom_world_profile_work(
|
||||
row: &CustomWorldProfile,
|
||||
active_agent_session_ids: &HashSet<String>,
|
||||
@@ -6248,25 +6288,25 @@ fn build_npc_state_snapshot_from_row(row: &NpcState) -> NpcStateSnapshot {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||
let session = CustomWorldAgentSession {
|
||||
fn build_test_custom_world_agent_session(
|
||||
seed_text: &str,
|
||||
stage: RpgAgentStage,
|
||||
draft_profile_json: Option<&str>,
|
||||
) -> CustomWorldAgentSession {
|
||||
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,
|
||||
seed_text: seed_text.to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 0,
|
||||
stage,
|
||||
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(),
|
||||
),
|
||||
draft_profile_json: draft_profile_json.map(str::to_string),
|
||||
last_assistant_reply: None,
|
||||
publish_gate_json: None,
|
||||
result_preview_json: None,
|
||||
@@ -6278,7 +6318,16 @@ mod tests {
|
||||
checkpoints_json: "[]".to_string(),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||
let session = build_test_custom_world_agent_session(
|
||||
"seed",
|
||||
RpgAgentStage::ObjectRefining,
|
||||
Some(r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_stable_agent_draft_profile_id(&session),
|
||||
@@ -6286,6 +6335,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_world_agent_session_direct_work_content_ignores_empty_created_session() {
|
||||
let empty_session =
|
||||
build_test_custom_world_agent_session("", RpgAgentStage::CollectingIntent, Some("{}"));
|
||||
let seeded_session = build_test_custom_world_agent_session(
|
||||
"想做一个海雾群岛",
|
||||
RpgAgentStage::CollectingIntent,
|
||||
Some("{}"),
|
||||
);
|
||||
let drafted_session =
|
||||
build_test_custom_world_agent_session("", RpgAgentStage::ObjectRefining, Some("{}"));
|
||||
let profile_session = build_test_custom_world_agent_session(
|
||||
"",
|
||||
RpgAgentStage::CollectingIntent,
|
||||
Some(r#"{"worldHook":"海雾会吞掉记错航线的人。"}"#),
|
||||
);
|
||||
|
||||
assert!(!custom_world_agent_session_has_direct_work_content(
|
||||
&empty_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&seeded_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&drafted_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&profile_session,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_agent_draft_profile_candidate_requires_same_owner_active_draft_and_session() {
|
||||
let matching = CustomWorldProfile {
|
||||
|
||||
Reference in New Issue
Block a user