70492d9eba
Simplify asset upload temp paths, refresh deploy docs for Aliyun DNS topology, and ship MindSpace content-scan and auth improvements. Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
import zlib from 'node:zlib';
|
|
|
|
function escapeXml(text) {
|
|
return String(text ?? '')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function extractZipEntry(buffer, targetName) {
|
|
let offset = 0;
|
|
while (offset + 30 <= buffer.length) {
|
|
if (buffer.subarray(offset, offset + 2).toString('ascii') !== 'PK') break;
|
|
const compressionMethod = buffer.readUInt16LE(offset + 8);
|
|
const compressedSize = buffer.readUInt32LE(offset + 18);
|
|
const nameLength = buffer.readUInt16LE(offset + 26);
|
|
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
const name = buffer.subarray(offset + 30, offset + 30 + nameLength).toString('utf8');
|
|
const dataStart = offset + 30 + nameLength + extraLength;
|
|
if (name === targetName) {
|
|
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
if (compressionMethod === 0) return compressed;
|
|
if (compressionMethod === 8) return zlib.inflateRawSync(compressed);
|
|
return null;
|
|
}
|
|
offset = dataStart + compressedSize;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extractXmlText(xml) {
|
|
const paragraphs = [];
|
|
for (const block of String(xml ?? '').split('</w:p>')) {
|
|
const texts = [...block.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)].map((match) =>
|
|
escapeXml(match[1]),
|
|
);
|
|
const line = texts.join('').replace(/\s+/g, ' ').trim();
|
|
if (line) paragraphs.push(line);
|
|
}
|
|
return paragraphs.join('\n\n');
|
|
}
|
|
|
|
function extractDocxText(buffer) {
|
|
const xmlBuffer = extractZipEntry(buffer, 'word/document.xml');
|
|
return xmlBuffer ? extractXmlText(xmlBuffer.toString('utf8')) : '';
|
|
}
|
|
|
|
function extractXlsxText(buffer) {
|
|
const sharedStringsXml = extractZipEntry(buffer, 'xl/sharedStrings.xml');
|
|
const sharedStrings = sharedStringsXml
|
|
? [...sharedStringsXml.toString('utf8').matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((match) =>
|
|
escapeXml(match[1]),
|
|
)
|
|
: [];
|
|
const sheetEntries = [];
|
|
let index = 1;
|
|
while (true) {
|
|
const sheet = extractZipEntry(buffer, `xl/worksheets/sheet${index}.xml`);
|
|
if (!sheet) break;
|
|
const rows = [];
|
|
for (const rowBlock of sheet.toString('utf8').split('</row>')) {
|
|
const cells = [];
|
|
for (const cell of rowBlock.matchAll(/<c\b[^>]*?(?:t="([^"]+)")?[^>]*>(?:[\s\S]*?<v>([\s\S]*?)<\/v>)?/g)) {
|
|
const type = cell[1] ?? '';
|
|
const value = cell[2] ?? '';
|
|
if (type === 's') {
|
|
const sharedIndex = Number(value);
|
|
cells.push(sharedStrings[sharedIndex] ?? value);
|
|
} else {
|
|
cells.push(escapeXml(value));
|
|
}
|
|
}
|
|
if (cells.length) rows.push(cells.join('\t'));
|
|
}
|
|
if (rows.length) {
|
|
sheetEntries.push(`Sheet ${index}\n${rows.join('\n')}`);
|
|
}
|
|
index += 1;
|
|
}
|
|
return sheetEntries.join('\n\n');
|
|
}
|
|
|
|
function extractPptxText(buffer) {
|
|
const slides = [];
|
|
let index = 1;
|
|
while (true) {
|
|
const slide = extractZipEntry(buffer, `ppt/slides/slide${index}.xml`);
|
|
if (!slide) break;
|
|
const texts = [...slide.toString('utf8').matchAll(/<a:t[^>]*>([\s\S]*?)<\/a:t>/g)].map((match) =>
|
|
escapeXml(match[1]),
|
|
);
|
|
const text = texts.join(' ').replace(/\s+/g, ' ').trim();
|
|
if (text) slides.push(`Slide ${index}\n${text}`);
|
|
index += 1;
|
|
}
|
|
return slides.join('\n\n');
|
|
}
|
|
|
|
function extractPdfText(buffer) {
|
|
const text = buffer.toString('latin1');
|
|
const segments = [];
|
|
for (const streamMatch of text.matchAll(/stream\r?\n([\s\S]*?)\r?\nendstream/g)) {
|
|
const stream = streamMatch[1];
|
|
for (const textMatch of stream.matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)\s*Tj/g)) {
|
|
const raw = textMatch[1]
|
|
.replace(/\\n/g, '\n')
|
|
.replace(/\\r/g, '\r')
|
|
.replace(/\\t/g, '\t')
|
|
.replace(/\\\\/g, '\\')
|
|
.replace(/\\\(/g, '(')
|
|
.replace(/\\\)/g, ')');
|
|
const cleaned = raw.replace(/\s+/g, ' ').trim();
|
|
if (cleaned) segments.push(cleaned);
|
|
}
|
|
}
|
|
return segments.join('\n');
|
|
}
|
|
|
|
export function extractAttachmentText(buffer, mimeType, filename = '') {
|
|
const normalizedMimeType = String(mimeType ?? '');
|
|
const extension = String(filename ?? '').toLowerCase();
|
|
if (normalizedMimeType === 'application/pdf') {
|
|
const text = extractPdfText(buffer);
|
|
return { text, format: 'pdf', warnings: text ? [] : ['pdf_text_not_detected'] };
|
|
}
|
|
if (
|
|
normalizedMimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
extension.endsWith('.docx')
|
|
) {
|
|
const text = extractDocxText(buffer);
|
|
return { text, format: 'docx', warnings: text ? [] : ['docx_text_not_detected'] };
|
|
}
|
|
if (
|
|
normalizedMimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
|
extension.endsWith('.xlsx')
|
|
) {
|
|
const text = extractXlsxText(buffer);
|
|
return { text, format: 'xlsx', warnings: text ? [] : ['xlsx_text_not_detected'] };
|
|
}
|
|
if (
|
|
normalizedMimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
|
|
extension.endsWith('.pptx')
|
|
) {
|
|
const text = extractPptxText(buffer);
|
|
return { text, format: 'pptx', warnings: text ? [] : ['pptx_text_not_detected'] };
|
|
}
|
|
if (normalizedMimeType.startsWith('text/')) {
|
|
return { text: buffer.toString('utf8'), format: 'text', warnings: [] };
|
|
}
|
|
return { text: '', format: 'unsupported', warnings: ['unsupported_attachment_type'] };
|
|
}
|
|
|
|
export const attachmentTextInternals = {
|
|
extractZipEntry,
|
|
extractDocxText,
|
|
extractXlsxText,
|
|
extractPptxText,
|
|
extractPdfText,
|
|
};
|