Add attachment text extraction, auto web news skill, and chat/voice UI updates.
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>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user