Files
memind/mindspace-attachment-text.mjs
john 70492d9eba 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>
2026-06-20 15:08:10 +08:00

162 lines
5.4 KiB
JavaScript

import zlib from 'node:zlib';
function escapeXml(text) {
return String(text ?? '')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/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,
};