diff --git a/AGENTS.md b/AGENTS.md index 6815ea28..b75313a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # AGENTS.md ## 项目约束 +- 在修改server-rs的内容时,不要去兼容server-node中的任何内容,只允许参考,以及把server-node中未迁移到server-rs的内容迁移过来 - 代码需要有完善的中文注释 - 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。 - 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中 diff --git a/docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md b/docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md index 5f1184ff..20669207 100644 --- a/docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md +++ b/docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md @@ -261,6 +261,12 @@ | `created_at` | `Timestamp` | 是 | 创建时间 | | `updated_at` | `Timestamp` | 是 | 更新时间 | +### 主键约束 + +1. `card_id` 是 SpacetimeDB 表级全局主键,不能只使用 `world-foundation` 这类跨会话固定值。 +2. Agent 自动生成的世界底稿卡统一使用 `custom-world:{session_id}:world-foundation`,确保同一会话内稳定 upsert、不同会话间不会发生唯一键冲突。 +3. 不保留历史 `world-foundation` 主键兼容逻辑;线上旧脏数据如需清理,应通过一次性运维脚本处理,不进入 reducer 主链。 + ### 索引 1. `session_id` diff --git a/server-rs/crates/spacetime-module/src/custom_world/mod.rs b/server-rs/crates/spacetime-module/src/custom_world/mod.rs index 9fe9ef5b..69a6ed13 100644 --- a/server-rs/crates/spacetime-module/src/custom_world/mod.rs +++ b/server-rs/crates/spacetime-module/src/custom_world/mod.rs @@ -2770,7 +2770,13 @@ fn upsert_world_foundation_card( draft_profile: &JsonMap, updated_at_micros: i64, ) -> Result<(), String> { - let card_id = "world-foundation".to_string(); + let card_id = build_world_foundation_card_id(session_id); + let existing_card = ctx + .db + .custom_world_draft_card() + .card_id() + .find(&card_id) + .filter(|row| row.session_id == session_id); let title = read_optional_text_field(draft_profile, &["name", "title"]) .unwrap_or_else(|| "世界底稿".to_string()); let subtitle = read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(); @@ -2792,13 +2798,7 @@ fn upsert_world_foundation_card( "warningMessages": [], }))?; - if let Some(existing) = ctx - .db - .custom_world_draft_card() - .card_id() - .find(&card_id) - .filter(|row| row.session_id == session_id) - { + if let Some(existing) = existing_card { replace_custom_world_draft_card( ctx, &existing, @@ -2849,6 +2849,11 @@ fn upsert_world_foundation_card( Ok(()) } +fn build_world_foundation_card_id(session_id: &str) -> String { + // `custom_world_draft_card.card_id` 是全局主键,世界底稿卡必须带上会话维度,避免多会话写入时触发唯一键冲突。 + format!("custom-world:{session_id}:world-foundation") +} + fn sync_session_draft_profile_from_card_update( session: &CustomWorldAgentSession, card: &CustomWorldDraftCard, diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 33ef5f07..6df7ab03 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -4144,7 +4144,13 @@ fn upsert_world_foundation_card( draft_profile: &JsonMap, updated_at_micros: i64, ) -> Result<(), String> { - let card_id = "world-foundation".to_string(); + let card_id = build_world_foundation_card_id(session_id); + let existing_card = ctx + .db + .custom_world_draft_card() + .card_id() + .find(&card_id) + .filter(|row| row.session_id == session_id); let title = read_optional_text_field(draft_profile, &["name", "title"]) .unwrap_or_else(|| "世界底稿".to_string()); let subtitle = read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(); @@ -4166,13 +4172,7 @@ fn upsert_world_foundation_card( "warningMessages": [], }))?; - if let Some(existing) = ctx - .db - .custom_world_draft_card() - .card_id() - .find(&card_id) - .filter(|row| row.session_id == session_id) - { + if let Some(existing) = existing_card { replace_custom_world_draft_card( ctx, &existing, @@ -4223,6 +4223,11 @@ fn upsert_world_foundation_card( Ok(()) } +fn build_world_foundation_card_id(session_id: &str) -> String { + // `custom_world_draft_card.card_id` 是全局主键,世界底稿卡必须带上会话维度,避免多会话写入时触发唯一键冲突。 + format!("custom-world:{session_id}:world-foundation") +} + fn sync_session_draft_profile_from_card_update( session: &CustomWorldAgentSession, card: &CustomWorldDraftCard,