迁移后端认证与拆分 Spacetime 客户端

This commit is contained in:
2026-04-24 14:10:11 +08:00
parent ef53028be5
commit 4f369617c7
55 changed files with 9206 additions and 343 deletions

View File

@@ -21,9 +21,11 @@ use module_quest::{
pub(crate) use serde_json::{Map as JsonMap, Value as JsonValue, json};
pub(crate) use shared_kernel::format_timestamp_micros;
pub use spacetimedb::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp};
use std::collections::HashSet;
mod ai;
mod asset_metadata;
mod auth;
mod big_fish;
mod domain_types;
mod entry;
@@ -32,6 +34,7 @@ mod runtime;
pub use ai::*;
pub use asset_metadata::*;
pub use auth::*;
pub use big_fish::*;
pub use domain_types::*;
pub use entry::*;
@@ -2733,10 +2736,12 @@ 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
}) {
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());
@@ -2780,6 +2785,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),
@@ -2834,6 +2840,24 @@ fn list_custom_world_work_snapshots(
Ok(items)
}
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 get_custom_world_agent_card_detail_tx(
ctx: &ReducerContext,
input: CustomWorldAgentCardDetailGetInput,
@@ -5996,6 +6020,113 @@ mod tests {
));
}
#[test]
fn custom_world_works_hides_compiled_draft_profile_when_agent_session_is_active() {
fn build_test_custom_world_profile(
profile_id: &str,
source_agent_session_id: Option<&str>,
publication_status: CustomWorldPublicationStatus,
) -> CustomWorldProfile {
CustomWorldProfile {
profile_id: profile_id.to_string(),
owner_user_id: "user-1".to_string(),
public_work_code: if publication_status == CustomWorldPublicationStatus::Published {
Some("CW-00000001".to_string())
} else {
None
},
author_public_user_code: None,
source_agent_session_id: source_agent_session_id.map(str::to_string),
publication_status,
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: if publication_status == CustomWorldPublicationStatus::Published {
Some(Timestamp::from_micros_since_unix_epoch(2))
} else {
None
},
deleted_at: None,
created_at: Timestamp::from_micros_since_unix_epoch(1),
updated_at: Timestamp::from_micros_since_unix_epoch(1),
}
}
let draft_profile = build_test_custom_world_profile(
"profile-1",
Some("session-1"),
CustomWorldPublicationStatus::Draft,
);
let orphan_draft_profile = build_test_custom_world_profile(
"profile-2",
Some("session-2"),
CustomWorldPublicationStatus::Draft,
);
let published_profile = build_test_custom_world_profile(
"profile-3",
Some("session-1"),
CustomWorldPublicationStatus::Published,
);
let mut active_agent_session_ids = HashSet::new();
active_agent_session_ids.insert("session-1".to_string());
assert!(!should_include_custom_world_profile_work(
&draft_profile,
&active_agent_session_ids,
));
assert!(should_include_custom_world_profile_work(
&orphan_draft_profile,
&active_agent_session_ids,
));
assert!(should_include_custom_world_profile_work(
&published_profile,
&active_agent_session_ids,
));
}
#[test]
fn custom_world_works_keeps_compiled_draft_profile_without_active_agent_session() {
let draft_profile = CustomWorldProfile {
profile_id: "profile-1".to_string(),
owner_user_id: "user-1".to_string(),
public_work_code: None,
author_public_user_code: None,
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 mut active_agent_session_ids = HashSet::new();
assert!(should_include_custom_world_profile_work(
&draft_profile,
&active_agent_session_ids,
));
active_agent_session_ids.insert("session-2".to_string());
assert!(should_include_custom_world_profile_work(
&draft_profile,
&active_agent_session_ids,
));
}
#[test]
fn summarize_publish_gate_accepts_current_agent_result_schema() {
let draft_profile = serde_json::from_str::<JsonValue>(