Files
Genarrative/server-rs/crates/module-editor-agent/src/application.rs
T
k88936 2fa05cd605 统一画布代理文本与附件输入约束
要求用户消息包含非空文本,附件仅作为消息上下文。
同步后端领域校验、接口错误和请求测试。
禁用前端纯附件发送并更新交互回归测试。
保留发送失败时的草稿与附件恢复行为。
2026-07-31 11:30:48 +08:00

130 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> {
if normalize_required_string(text).is_none() {
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_eq!(
validate_user_message("", &["resource-1".to_string()]),
Err(EditorAgentError::EmptyMessage)
);
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)
);
}
}