diff --git a/admin/slices/bridle/stores/bridle.ts b/admin/slices/bridle/stores/bridle.ts index 71da2859..9d1a2ecc 100644 --- a/admin/slices/bridle/stores/bridle.ts +++ b/admin/slices/bridle/stores/bridle.ts @@ -730,7 +730,7 @@ export const useBridleStore = defineStore('bridle', { for (const file of list) { const mimeType = resolveMimeType(file) const problem = !isAllowedMimeType(mimeType) - ? `${file.name} is not a supported file type. Try an image, a PDF, or a text file.` + ? `${file.name} is not a supported file type. Try an image, a PDF, an Office document, or a text file.` : this._rejectReason(file, pendingBytes) if (problem) { diff --git a/admin/slices/bridle/utils/attachment.ts b/admin/slices/bridle/utils/attachment.ts index 8e26d7c4..360a34e8 100644 --- a/admin/slices/bridle/utils/attachment.ts +++ b/admin/slices/bridle/utils/attachment.ts @@ -26,7 +26,16 @@ export const TEXT_MIME_TYPES = [ 'application/json', ] as const -export const BINARY_MIME_TYPES = ['application/pdf'] as const +export const BINARY_MIME_TYPES = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +] as const export const ALLOWED_MIME_TYPES: readonly string[] = [ ...IMAGE_MIME_TYPES, @@ -45,6 +54,13 @@ export const FILE_PICKER_ACCEPT = [ '.csv', '.txt', '.json', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.xlsm', + '.ppt', + '.pptx', ].join(',') /** Same extension fallback the server uses when `file.type` is blank. */ @@ -60,6 +76,15 @@ export const MIME_BY_EXTENSION: Readonly> = { '.markdown': 'text/markdown', '.csv': 'text/csv', '.json': 'application/json', + '.doc': 'application/msword', + '.docx': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', } export enum BridleAttachmentKinds { diff --git a/api/src/slices/bridle/domain/attachment.constants.ts b/api/src/slices/bridle/domain/attachment.constants.ts index d4da466e..1c77783e 100644 --- a/api/src/slices/bridle/domain/attachment.constants.ts +++ b/api/src/slices/bridle/domain/attachment.constants.ts @@ -44,8 +44,19 @@ export const TEXT_MIME_TYPES = [ 'application/json', ] as const; -/** Accepted but not readable by the agent: delivered as a named reference. */ -export const BINARY_MIME_TYPES = ['application/pdf'] as const; +/** Accepted but not readable by the agent: delivered as a named reference. + * Office formats are zip/OLE containers — extracting their text is a + * separate feature; until then they travel like PDFs, name and bytes only. */ +export const BINARY_MIME_TYPES = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +] as const; export const ALLOWED_MIME_TYPES: readonly string[] = [ ...IMAGE_MIME_TYPES, @@ -68,6 +79,15 @@ export const EXTENSION_BY_MIME: Readonly> = { 'text/markdown': '.md', 'text/csv': '.csv', 'application/json': '.json', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + '.docx', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm', + 'application/vnd.ms-powerpoint': '.ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + '.pptx', }; /** Fallback when a file arrives with no usable MIME type but a known extension. */ @@ -83,4 +103,13 @@ export const MIME_BY_EXTENSION: Readonly> = { '.markdown': 'text/markdown', '.csv': 'text/csv', '.json': 'application/json', + '.doc': 'application/msword', + '.docx': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; diff --git a/api/src/slices/bridle/domain/attachment.service.spec.ts b/api/src/slices/bridle/domain/attachment.service.spec.ts index c4ecaca5..8095220e 100644 --- a/api/src/slices/bridle/domain/attachment.service.spec.ts +++ b/api/src/slices/bridle/domain/attachment.service.spec.ts @@ -83,6 +83,47 @@ describe('BridleAttachmentService — upload validation', () => { expect(result.readableByAgent).toBe(false); }); + it('accepts Office documents as binary references', async () => { + const { service } = makeService(); + const cases = [ + ['macros.xlsm', 'application/vnd.ms-excel.sheet.macroEnabled.12'], + [ + 'sheet.xlsx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ], + [ + 'letter.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ], + ['deck.ppt', 'application/vnd.ms-powerpoint'], + ] as const; + + for (const [name, mimeType] of cases) { + const result = await service.upload({ + agentId: AGENT, + name, + mimeType, + body: Buffer.from('PK'), + }); + expect(result.kind).toBe(BridleAttachmentKinds.Binary); + expect(result.readableByAgent).toBe(false); + expect(result.mimeType).toBe(mimeType); + } + }); + + it('resolves an Office file by extension when the browser reports a blank type', async () => { + const { service } = makeService(); + const result = await service.upload({ + agentId: AGENT, + name: 'quarterly.xlsm', + mimeType: '', + body: Buffer.from('PK'), + }); + + expect(result.mimeType).toBe('application/vnd.ms-excel.sheet.macroEnabled.12'); + expect(result.kind).toBe(BridleAttachmentKinds.Binary); + }); + it('rejects a zero-byte file', async () => { const { service } = makeService(); await expect( diff --git a/app/i18n.sync.json b/app/i18n.sync.json index ddc87d3f..99c6e10c 100644 --- a/app/i18n.sync.json +++ b/app/i18n.sync.json @@ -61,7 +61,7 @@ "chat.error_empty": "0b50d48fcbac", "chat.error_size": "5dd5c28ddbbd", "chat.error_total": "2a28704f7f5c", - "chat.error_type": "b5e7f88b3582", + "chat.error_type": "8c3cacb2300a", "chat.input_hint": "8a8c17c82d18", "chat.placeholder": "57ef13894e3b", "chat.send": "f6f4688ff23d", diff --git a/app/slices/bridle/domain/attachment.constants.ts b/app/slices/bridle/domain/attachment.constants.ts index 7ef028a6..bd537b33 100644 --- a/app/slices/bridle/domain/attachment.constants.ts +++ b/app/slices/bridle/domain/attachment.constants.ts @@ -26,7 +26,16 @@ export const TEXT_MIME_TYPES = [ 'application/json', ] as const; -export const BINARY_MIME_TYPES = ['application/pdf'] as const; +export const BINARY_MIME_TYPES = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +] as const; export const ALLOWED_MIME_TYPES: readonly string[] = [ ...IMAGE_MIME_TYPES, @@ -43,6 +52,13 @@ export const FILE_PICKER_ACCEPT = [ '.csv', '.txt', '.json', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.xlsm', + '.ppt', + '.pptx', ].join(','); /** Same extension fallback the server uses when `file.type` is blank. */ @@ -58,6 +74,15 @@ export const MIME_BY_EXTENSION: Readonly> = { '.markdown': 'text/markdown', '.csv': 'text/csv', '.json': 'application/json', + '.doc': 'application/msword', + '.docx': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }; /** Human-readable byte size for chips and error copy. */ diff --git a/app/slices/bridle/i18n/locales/en.json b/app/slices/bridle/i18n/locales/en.json index 7d434df2..bbb86681 100644 --- a/app/slices/bridle/i18n/locales/en.json +++ b/app/slices/bridle/i18n/locales/en.json @@ -21,7 +21,7 @@ "attachment_failed": "Couldn't upload {name}", "attachment_unavailable": "This file is no longer available", "attachment_not_readable": "The agent sees the name, not the contents", - "error_type": "{name} isn't a supported file type. Try an image, a PDF, or a text file.", + "error_type": "{name} isn't a supported file type. Try an image, a PDF, an Office document, or a text file.", "error_size": "{name} is larger than {limit}", "error_total": "Those files add up to more than {limit} in one message", "error_empty": "{name} is empty" diff --git a/app/slices/bridle/i18n/locales/ru.json b/app/slices/bridle/i18n/locales/ru.json index 2cc6b259..0313d10b 100644 --- a/app/slices/bridle/i18n/locales/ru.json +++ b/app/slices/bridle/i18n/locales/ru.json @@ -21,7 +21,7 @@ "attachment_failed": "Не удалось загрузить {name}", "attachment_unavailable": "Этот файл больше недоступен", "attachment_not_readable": "Агент видит имя файла, но не содержимое", - "error_type": "{name} — неподдерживаемый тип файла. Попробуйте изображение, PDF или текстовый файл.", + "error_type": "{name} — неподдерживаемый тип файла. Попробуйте изображение, PDF, документ Office или текстовый файл.", "error_size": "{name} больше, чем {limit}", "error_total": "Суммарный размер файлов в одном сообщении превышает {limit}", "error_empty": "{name} пуст"