fix:修复无消息草稿保存

优化首次加载速度
This commit is contained in:
2026-04-26 17:23:52 +08:00
parent 31393340e7
commit a0d1cb86f0
14 changed files with 440 additions and 43 deletions

View File

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

View File

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

View File

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