feat(chat): #155 paste image depuis presse-papier dans le composer custom (QA verte)
- frontend: onPaste sur CustomAgentChatView, détection MIME image, chip/preview d'attachment, envoi via le contrat #154 (adapters/agent, ports) - backend: import d'image par bytes dans le store attachments (commands, chat_attachments app+infra, dto, ports) + tests
This commit is contained in:
@ -30,14 +30,28 @@ pub struct ImportChatAttachmentsInput {
|
||||
/// One source attachment import request.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ImportChatAttachmentItem {
|
||||
/// Local source path supplied by the driving adapter.
|
||||
pub path: String,
|
||||
/// Source payload supplied by the driving adapter.
|
||||
pub source: ImportChatAttachmentSource,
|
||||
/// Optional MIME type supplied by the driving adapter.
|
||||
pub mime: Option<String>,
|
||||
/// Origin kind.
|
||||
pub source_kind: ChatAttachmentSourceKind,
|
||||
}
|
||||
|
||||
/// Source payload for a chat attachment import.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ImportChatAttachmentSource {
|
||||
/// Local source path supplied by the driving adapter.
|
||||
Path(String),
|
||||
/// In-memory bytes supplied by the driving adapter.
|
||||
Bytes {
|
||||
/// Display filename to persist in metadata and storage name.
|
||||
filename: String,
|
||||
/// Raw content bytes.
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Output of [`ImportChatAttachments::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ImportChatAttachmentsOutput {
|
||||
@ -70,28 +84,44 @@ impl ImportChatAttachments {
|
||||
) -> Result<ImportChatAttachmentsOutput, AppError> {
|
||||
let mut imported = Vec::with_capacity(input.attachments.len());
|
||||
for item in input.attachments {
|
||||
let filename = filename_from_path(&item.path)?;
|
||||
let filename = filename_from_source(&item.source)?;
|
||||
validate_attachment_filename(&filename)?;
|
||||
let mime = sanitize_mime(item.mime.as_deref(), &filename)?;
|
||||
let id = ChatAttachmentId::new(self.ids.new_uuid().to_string())
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||
let attachment = self
|
||||
.store
|
||||
.import_from_path(
|
||||
&input.project.root,
|
||||
input.project.id,
|
||||
input.agent_id,
|
||||
input.session_id,
|
||||
&LocalPath::new(item.path),
|
||||
ChatAttachmentImport {
|
||||
id,
|
||||
filename,
|
||||
mime,
|
||||
source_kind: item.source_kind,
|
||||
created_at: now(&self.clock),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let import = ChatAttachmentImport {
|
||||
id,
|
||||
filename,
|
||||
mime,
|
||||
source_kind: item.source_kind,
|
||||
created_at: now(&self.clock),
|
||||
};
|
||||
let attachment = match item.source {
|
||||
ImportChatAttachmentSource::Path(path) => {
|
||||
self.store
|
||||
.import_from_path(
|
||||
&input.project.root,
|
||||
input.project.id,
|
||||
input.agent_id,
|
||||
input.session_id,
|
||||
&LocalPath::new(path),
|
||||
import,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
ImportChatAttachmentSource::Bytes { bytes, .. } => {
|
||||
self.store
|
||||
.import_from_bytes(
|
||||
&input.project.root,
|
||||
input.project.id,
|
||||
input.agent_id,
|
||||
input.session_id,
|
||||
&bytes,
|
||||
import,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
imported.push(attachment);
|
||||
}
|
||||
Ok(ImportChatAttachmentsOutput {
|
||||
@ -112,6 +142,13 @@ fn filename_from_path(path: &str) -> Result<String, AppError> {
|
||||
.ok_or_else(|| AppError::Invalid("invalid attachment filename".to_owned()))
|
||||
}
|
||||
|
||||
fn filename_from_source(source: &ImportChatAttachmentSource) -> Result<String, AppError> {
|
||||
match source {
|
||||
ImportChatAttachmentSource::Path(path) => filename_from_path(path),
|
||||
ImportChatAttachmentSource::Bytes { filename, .. } => Ok(filename.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_attachment_filename(filename: &str) -> Result<(), AppError> {
|
||||
let lowered = filename.to_ascii_lowercase();
|
||||
let blocked = [
|
||||
|
||||
@ -72,8 +72,8 @@ pub use background::{
|
||||
SpawnBackgroundCommandOutput,
|
||||
};
|
||||
pub use chat_attachments::{
|
||||
ImportChatAttachmentItem, ImportChatAttachments, ImportChatAttachmentsInput,
|
||||
ImportChatAttachmentsOutput, CHAT_ATTACHMENT_MAX_BYTES,
|
||||
ImportChatAttachmentItem, ImportChatAttachmentSource, ImportChatAttachments,
|
||||
ImportChatAttachmentsInput, ImportChatAttachmentsOutput, CHAT_ATTACHMENT_MAX_BYTES,
|
||||
};
|
||||
pub use conversation::{
|
||||
ConversationArchiveProvider, ReadConversationPage, ReadConversationPageInput, RecordTurn,
|
||||
|
||||
Reference in New Issue
Block a user