Socle #154 : entité attachment, store, contrat DTO et routage session. Modules : domain/chat_attachment, application/chat_attachments, infrastructure/chat_attachments, app-tauri (commands/lib), backend dto.
133 lines
4.1 KiB
Rust
133 lines
4.1 KiB
Rust
//! Agent/chat attachment domain model.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
|
|
use crate::ids::{AgentId, ProjectId, SessionId};
|
|
|
|
/// Stable chat attachment identifier.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct ChatAttachmentId(String);
|
|
|
|
impl ChatAttachmentId {
|
|
/// Builds an attachment id.
|
|
///
|
|
/// # Errors
|
|
/// [`ChatAttachmentError::InvalidId`] when the id is empty or not filename-safe.
|
|
pub fn new(value: impl Into<String>) -> Result<Self, ChatAttachmentError> {
|
|
let value = value.into();
|
|
if value.is_empty()
|
|
|| !value
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
|
{
|
|
return Err(ChatAttachmentError::InvalidId(value));
|
|
}
|
|
Ok(Self(value))
|
|
}
|
|
|
|
/// Returns the raw id.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
/// Where the attachment originally came from.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ChatAttachmentSourceKind {
|
|
/// User-selected local filesystem path.
|
|
LocalFile,
|
|
/// Clipboard paste, materialized by a driving adapter.
|
|
Clipboard,
|
|
/// Drag-and-drop, materialized by a driving adapter.
|
|
DragDrop,
|
|
/// Unknown or future source.
|
|
Other,
|
|
}
|
|
|
|
/// Durable metadata for one agent/chat attachment.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ChatAttachment {
|
|
/// Stable attachment id.
|
|
pub id: ChatAttachmentId,
|
|
/// Project that owns the managed attachment.
|
|
pub project_id: ProjectId,
|
|
/// Target agent for the chat flow.
|
|
pub agent_id: AgentId,
|
|
/// Structured/chat session that imported the file.
|
|
pub session_id: SessionId,
|
|
/// Original display filename.
|
|
pub filename: String,
|
|
/// MIME type.
|
|
pub mime: String,
|
|
/// Size in bytes.
|
|
pub size_bytes: u64,
|
|
/// Origin kind.
|
|
pub source_kind: ChatAttachmentSourceKind,
|
|
/// Stored relative path under `.ideai/attachments`.
|
|
pub storage_path: String,
|
|
/// Absolute managed path readable by the target agent sandbox.
|
|
pub readable_path: String,
|
|
/// Creation time, epoch milliseconds.
|
|
pub created_at: u64,
|
|
}
|
|
|
|
impl ChatAttachment {
|
|
/// Validates persisted metadata.
|
|
///
|
|
/// # Errors
|
|
/// [`ChatAttachmentError`] when an invariant is violated.
|
|
pub fn validate(&self) -> Result<(), ChatAttachmentError> {
|
|
if self.filename.trim().is_empty()
|
|
|| self.filename.contains('/')
|
|
|| self.filename.contains('\\')
|
|
|| self.filename == "."
|
|
|| self.filename == ".."
|
|
{
|
|
return Err(ChatAttachmentError::InvalidFilename(self.filename.clone()));
|
|
}
|
|
if self.mime.trim().is_empty() {
|
|
return Err(ChatAttachmentError::InvalidMime(self.mime.clone()));
|
|
}
|
|
if self.storage_path.starts_with('/')
|
|
|| self.storage_path.starts_with('\\')
|
|
|| self.storage_path.contains("..")
|
|
|| !self.storage_path.starts_with("agent-chat/")
|
|
{
|
|
return Err(ChatAttachmentError::InvalidStoragePath(
|
|
self.storage_path.clone(),
|
|
));
|
|
}
|
|
if self.readable_path.trim().is_empty() {
|
|
return Err(ChatAttachmentError::InvalidReadablePath(
|
|
self.readable_path.clone(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Attachment validation errors.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
|
pub enum ChatAttachmentError {
|
|
/// Invalid id.
|
|
#[error("invalid chat attachment id: {0}")]
|
|
InvalidId(String),
|
|
/// Invalid filename.
|
|
#[error("invalid chat attachment filename: {0}")]
|
|
InvalidFilename(String),
|
|
/// Invalid MIME.
|
|
#[error("invalid chat attachment mime: {0}")]
|
|
InvalidMime(String),
|
|
/// Invalid relative storage path.
|
|
#[error("invalid chat attachment storage path: {0}")]
|
|
InvalidStoragePath(String),
|
|
/// Invalid readable path.
|
|
#[error("invalid chat attachment readable path: {0}")]
|
|
InvalidReadablePath(String),
|
|
}
|