file-parser.js 4.01 KB
/**
 * File Parser Service
 *
 * 解析上传文件内容,支持文本、PDF、DOCX 等格式。
 * 返回 { text, mimeType, truncated }
 */
const fs = require('fs');
const path = require('path');

const MAX_TEXT_LENGTH = 50000; // 最大解析字符数

// 文本类扩展名
const TEXT_EXTENSIONS = new Set([
    '.txt', '.md', '.log', '.csv', '.json', '.yaml', '.yml',
    '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs',
    '.html', '.css', '.scss', '.xml', '.sql', '.sh', '.bat',
    '.env', '.ini', '.toml', '.conf',
]);

// 图片类扩展名
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg']);

/**
 * 解析文件内容
 * @param {string} filePath - 文件绝对路径
 * @returns {Promise<{text: string, mimeType: string, truncated: boolean}>}
 */
async function parseFileContent(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    const fileName = path.basename(filePath);

    // 1. 文本类文件
    if (TEXT_EXTENSIONS.has(ext)) {
        const raw = fs.readFileSync(filePath, 'utf-8');
        const truncated = raw.length > MAX_TEXT_LENGTH;
        const text = truncated ? raw.substring(0, MAX_TEXT_LENGTH) + '\n\n... [文件内容过长,已截断]' : raw;
        return { text, mimeType: getMimeType(ext), truncated };
    }

    // 2. PDF
    if (ext === '.pdf') {
        try {
            const pdfParse = require('pdf-parse');
            const buffer = fs.readFileSync(filePath);
            const data = await pdfParse(buffer);
            const raw = data.text || '';
            const truncated = raw.length > MAX_TEXT_LENGTH;
            const text = truncated ? raw.substring(0, MAX_TEXT_LENGTH) + '\n\n... [PDF 内容过长,已截断]' : raw;
            return { text, mimeType: 'application/pdf', truncated };
        } catch (err) {
            return { text: `[PDF 解析失败: ${err.message}]`, mimeType: 'application/pdf', truncated: false };
        }
    }

    // 3. DOCX
    if (ext === '.docx') {
        try {
            const mammoth = require('mammoth');
            const result = await mammoth.extractRawText({ path: filePath });
            const raw = result.value || '';
            const truncated = raw.length > MAX_TEXT_LENGTH;
            const text = truncated ? raw.substring(0, MAX_TEXT_LENGTH) + '\n\n... [DOCX 内容过长,已截断]' : raw;
            return { text, mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', truncated };
        } catch (err) {
            return { text: `[DOCX 解析失败: ${err.message}]`, mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', truncated: false };
        }
    }

    // 4. 图片
    if (IMAGE_EXTENSIONS.has(ext)) {
        return {
            text: `[用户上传了图片: ${fileName}]`,
            mimeType: getMimeType(ext),
            truncated: false,
        };
    }

    // 5. 其他:尝试当文本读取
    try {
        const raw = fs.readFileSync(filePath, 'utf-8');
        const truncated = raw.length > MAX_TEXT_LENGTH;
        const text = truncated ? raw.substring(0, MAX_TEXT_LENGTH) + '\n\n... [文件内容过长,已截断]' : raw;
        return { text, mimeType: 'application/octet-stream', truncated };
    } catch {
        return { text: `[不支持的文件格式: ${ext}]`, mimeType: 'application/octet-stream', truncated: false };
    }
}

function getMimeType(ext) {
    const map = {
        '.txt': 'text/plain', '.md': 'text/markdown', '.log': 'text/plain',
        '.csv': 'text/csv', '.json': 'application/json',
        '.js': 'text/javascript', '.ts': 'text/typescript',
        '.py': 'text/x-python', '.html': 'text/html', '.css': 'text/css',
        '.xml': 'text/xml', '.yaml': 'text/yaml', '.yml': 'text/yaml',
        '.pdf': 'application/pdf',
        '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
        '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
    };
    return map[ext] || 'application/octet-stream';
}

module.exports = { parseFileContent };