feat: support Office documents in chat attachments
Users can now attach Microsoft Office documents (.docx, .pptx, and .xlsx) and OpenDocument files (.odt, .odp, and .ods) from the shared web, desktop, mobile, and VS Code chat surfaces. Document text is extracted locally and sent as a text/plain file part with the original filename, keeping the visible user message clean. Supported embedded PNG, JPEG, GIF, and WebP images are sent as separate image parts, with matching [filename] citations preserved near their source paragraph, slide object, spreadsheet cell anchor, or OpenDocument position. Presentation notes, spreadsheet values, headers, and footers are included where available. Document expansion is metadata-validated and bounded against oversized entries, excessive uncompressed data, unsafe paths, invalid image signatures, attachment-name races, and dangling citations after truncation. Generated document parts are published to the composer atomically. Add fflate for worker-backed ZIP extraction and narrowly allow blob workers in the VS Code webview CSP without permitting blob scripts. Include focused fixtures for every supported format, extraction limits, positional citations, collision recovery, atomic attachment state, and CSP behavior.
This commit is contained in:
@@ -6,6 +6,12 @@ const ACCEPTED_ATTACHMENT_TYPES = [
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"text/*",
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
@@ -27,6 +33,7 @@ const ACCEPTED_ATTACHMENT_TYPES = [
|
||||
".cts",
|
||||
".dart",
|
||||
".diff",
|
||||
".docx",
|
||||
".drawio",
|
||||
".env",
|
||||
".erl",
|
||||
@@ -64,9 +71,13 @@ const ACCEPTED_ATTACHMENT_TYPES = [
|
||||
".mjs",
|
||||
".mts",
|
||||
".ndjson",
|
||||
".odp",
|
||||
".ods",
|
||||
".odt",
|
||||
".patch",
|
||||
".php",
|
||||
".proto",
|
||||
".pptx",
|
||||
".ps1",
|
||||
".py",
|
||||
".r",
|
||||
@@ -88,6 +99,7 @@ const ACCEPTED_ATTACHMENT_TYPES = [
|
||||
".txt",
|
||||
".vue",
|
||||
".xml",
|
||||
".xlsx",
|
||||
".yaml",
|
||||
".yml",
|
||||
".zig",
|
||||
@@ -104,6 +116,12 @@ const PICKER_MIME_EXTENSIONS = new Map<string, string>([
|
||||
["image/heic", "heic"],
|
||||
["image/heif", "heif"],
|
||||
["application/pdf", "pdf"],
|
||||
["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"],
|
||||
["application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"],
|
||||
["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"],
|
||||
["application/vnd.oasis.opendocument.text", "odt"],
|
||||
["application/vnd.oasis.opendocument.presentation", "odp"],
|
||||
["application/vnd.oasis.opendocument.spreadsheet", "ods"],
|
||||
["application/json", "json"],
|
||||
["application/ld+json", "jsonld"],
|
||||
["application/toml", "toml"],
|
||||
@@ -157,11 +175,12 @@ const TEXT_MIMES = new Set([
|
||||
"image/svg+xml",
|
||||
])
|
||||
const ATTACHMENT_SAMPLE_BYTES = 4096
|
||||
const DOCUMENT_EXTENSIONS = new Set(["docx", "pptx", "xlsx", "odt", "odp", "ods"])
|
||||
const REDACTED = "[REDACTED]"
|
||||
const OMITTED = "[OMITTED BY OPENCHAMBER]"
|
||||
const SENSITIVE_NAMES = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[-_]?key|client[-_]?secret|password|secret|access[-_]?token|refresh[-_]?token|id[-_]?token|token)$/i
|
||||
|
||||
export type PreparedAttachmentFile = {
|
||||
type PreparedAttachmentFile = {
|
||||
file: File
|
||||
mimeType: string
|
||||
}
|
||||
@@ -341,3 +360,26 @@ export const prepareAttachmentFile = (
|
||||
if (typeof mime === "string") return { file, mimeType: mime }
|
||||
return mime?.then((mimeType) => mimeType ? { file, mimeType } : undefined)
|
||||
}
|
||||
|
||||
export const prepareAttachmentFiles = (
|
||||
file: File,
|
||||
reservedFilenames: Iterable<string> = [],
|
||||
): PreparedAttachmentFile[] | Promise<PreparedAttachmentFile[] | undefined> | undefined => {
|
||||
if (!DOCUMENT_EXTENSIONS.has(extensionOf(file.name))) {
|
||||
const prepared = prepareAttachmentFile(file)
|
||||
if (prepared instanceof Promise) return prepared.then((output) => output ? [output] : undefined)
|
||||
return prepared ? [prepared] : undefined
|
||||
}
|
||||
|
||||
return import("./document-attachments").then(async ({ extractDocumentAttachments }) => {
|
||||
const extracted = await extractDocumentAttachments(file, reservedFilenames)
|
||||
if (!extracted) return
|
||||
const prepared: PreparedAttachmentFile[] = [{ file: extracted.textFile, mimeType: "text/plain" }]
|
||||
for (const image of extracted.images) {
|
||||
const output = await prepareAttachmentFile(image)
|
||||
if (!output || !output.mimeType.startsWith("image/")) return
|
||||
prepared.push(output)
|
||||
}
|
||||
return prepared
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user