Files
Genarrative/server-rs/crates/module-editor-agent/src/application.rs
T
kdletters 8dad52f040 实现画布Agent侧边栏聊天
新增画布Agent会话契约、领域规则、SpacetimeDB元数据表和生成绑定。
新增api-server画布Agent会话CRUD、OSS消息文档读写、SSE消息流和生成工具编排。
新增前端右侧Agent对话面板、会话状态hook、SSE客户端、BFF客户端和相关测试。
调整画布侧栏、任务栏、小地图默认状态和面板互斥交互。
补齐画布Agent、OSS消息存储、SSE收口、后端契约和项目记忆文档。
2026-07-04 12:03:58 +08:00

128 lines
4.4 KiB
Rust

use shared_kernel::normalize_required_string;
use crate::{
EDITOR_AGENT_MAX_ATTACHMENTS, EditorAgentConversationCreateInput,
EditorAgentConversationDeleteInput, EditorAgentConversationTouchInput, EditorAgentError,
};
pub fn validate_conversation_key(
conversation_id: &str,
owner_user_id: &str,
) -> Result<(String, String), EditorAgentError> {
let conversation_id = normalize_required_string(conversation_id)
.ok_or(EditorAgentError::MissingConversationId)?;
let owner_user_id =
normalize_required_string(owner_user_id).ok_or(EditorAgentError::MissingOwnerUserId)?;
Ok((conversation_id, owner_user_id))
}
pub fn validate_create_conversation(
input: &EditorAgentConversationCreateInput,
) -> Result<(), EditorAgentError> {
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
if normalize_required_string(&input.project_id).is_none() {
return Err(EditorAgentError::MissingProjectId);
}
Ok(())
}
pub fn validate_touch_conversation(
input: &EditorAgentConversationTouchInput,
) -> Result<(), EditorAgentError> {
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
Ok(())
}
pub fn validate_delete_conversation(
input: &EditorAgentConversationDeleteInput,
) -> Result<(), EditorAgentError> {
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
Ok(())
}
/// 校验会话访问权:拥有者匹配且未软删。
pub fn ensure_conversation_accessible(
snapshot_owner_user_id: &str,
snapshot_deleted: bool,
requester_user_id: &str,
) -> Result<(), EditorAgentError> {
if snapshot_owner_user_id != requester_user_id {
return Err(EditorAgentError::NotConversationOwner);
}
if snapshot_deleted {
return Err(EditorAgentError::ConversationDeleted);
}
Ok(())
}
/// 校验用户消息:文本与附件不可同时为空,附件数量不超过上限,附件引用需带资源标识。
pub fn validate_user_message(
text: &str,
attachment_reference_ids: &[String],
) -> Result<(), EditorAgentError> {
let has_text = normalize_required_string(text).is_some();
if !has_text && attachment_reference_ids.is_empty() {
return Err(EditorAgentError::EmptyMessage);
}
if attachment_reference_ids.len() > EDITOR_AGENT_MAX_ATTACHMENTS {
return Err(EditorAgentError::TooManyAttachments);
}
if attachment_reference_ids
.iter()
.any(|reference_id| normalize_required_string(reference_id).is_none())
{
return Err(EditorAgentError::InvalidAttachmentReference);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::derive_conversation_title;
#[test]
fn derive_title_truncates_and_normalizes_whitespace() {
assert_eq!(
derive_conversation_title(" 帮我生成\n一张森林背景 "),
"帮我生成 一张森林背景"
);
assert_eq!(derive_conversation_title(""), "新对话");
assert_eq!(derive_conversation_title(" \n "), "新对话");
let long_text = "一二三四五六七八九十一二三四五六七八九十超出部分";
assert_eq!(derive_conversation_title(long_text).chars().count(), 20);
}
#[test]
fn validate_user_message_rules() {
assert_eq!(
validate_user_message("", &[]),
Err(EditorAgentError::EmptyMessage)
);
assert!(validate_user_message("", &["resource-1".to_string()]).is_ok());
assert!(validate_user_message("画一棵树", &[]).is_ok());
let too_many: Vec<String> = (0..10).map(|i| format!("resource-{i}")).collect();
assert_eq!(
validate_user_message("画一棵树", &too_many),
Err(EditorAgentError::TooManyAttachments)
);
assert_eq!(
validate_user_message("画一棵树", &[" ".to_string()]),
Err(EditorAgentError::InvalidAttachmentReference)
);
}
#[test]
fn ensure_conversation_accessible_rules() {
assert!(ensure_conversation_accessible("user-1", false, "user-1").is_ok());
assert_eq!(
ensure_conversation_accessible("user-1", false, "user-2"),
Err(EditorAgentError::NotConversationOwner)
);
assert_eq!(
ensure_conversation_accessible("user-1", true, "user-1"),
Err(EditorAgentError::ConversationDeleted)
);
}
}