Update server.js via upload script - 2026-08-02 17:13:49
This commit is contained in:
parent
71fa9b238d
commit
50e76b2594
59
server.js
59
server.js
|
|
@ -662,6 +662,14 @@ app.post('/api/ai/draft', requireAuth, aiLimiter, async (req, res) => {
|
|||
// jsdelivr CDN下载语言包,VPS需要能访问外网)
|
||||
// 2. 系统程序 poppler-utils,只有上传PDF时才需要,用来把PDF每页渲染成图片:
|
||||
// apt install -y poppler-utils
|
||||
// 3. 系统程序 markitdown(微软开源,Python),用来读取DOCX/PPTX/XLSX的文字内容,
|
||||
// 以及给PDF做"有没有文字层"的快速探测——有文字层就直接用markitdown拿干净文本
|
||||
// (跳过OCR,更快更准,还保留表格结构),没有文字层(扫描件/纯图片排版)才退回
|
||||
// pdftoppm+OCR这条老路。装法(root跑):
|
||||
// apt install -y pipx && pipx ensurepath
|
||||
// pipx install 'markitdown[pdf,docx,pptx,xlsx]'
|
||||
// ln -s ~/.local/bin/markitdown /usr/local/bin/markitdown # 让Node能直接找到这个命令
|
||||
// 没装markitdown也不影响现有功能——探测/转换失败会静默退回原来的OCR流程。
|
||||
|
||||
const { execFile } = require('child_process');
|
||||
const Tesseract = require('tesseract.js');
|
||||
|
|
@ -671,6 +679,16 @@ const uploadMemory = multer({
|
|||
limits: { fileSize: 20 * 1024 * 1024, files: 30 }, // 20MB/文件,最多30个文件
|
||||
});
|
||||
|
||||
// 调用markitdown CLI把一个文件(DOCX/PPTX/XLSX/PDF等)转成Markdown文本
|
||||
function runMarkitdown(filePath, timeoutMs = 30000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('markitdown', [filePath], { timeout: timeoutMs, maxBuffer: 20 * 1024 * 1024 }, (err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout || '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 把一份PDF的每一页渲染成PNG图片(用poppler-utils的pdftoppm),返回图片路径数组
|
||||
function renderPdfToImages(pdfBuffer, tmpDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -860,15 +878,44 @@ app.post('/api/ai/parse-catalog', requireAuth, aiLimiter, (req, res) => {
|
|||
try {
|
||||
tmpDir = fs.mkdtempSync(path.join(OCR_TMP_ROOT, 'catalog-ocr-'));
|
||||
const imagePaths = [];
|
||||
const docTextParts = []; // DOCX/PPTX/XLSX,或者"有文字层的PDF"——MarkItDown转出来的干净文本,不用跑OCR
|
||||
const htmlTextParts = []; // HTML文件不用OCR,直接读文字内容(保留表格标签结构,AI能看懂rowspan这种分组关系)
|
||||
for (const file of req.files) {
|
||||
const isPdf = (file.mimetype || '').includes('pdf') || file.originalname.toLowerCase().endsWith('.pdf');
|
||||
const lowerName = file.originalname.toLowerCase();
|
||||
const isPdf = (file.mimetype || '').includes('pdf') || lowerName.endsWith('.pdf');
|
||||
const isImage = (file.mimetype || '').startsWith('image/');
|
||||
const isHtml = (file.mimetype || '').includes('html') || /\.html?$/i.test(file.originalname);
|
||||
if (isPdf) {
|
||||
const isOfficeDoc = /\.(docx|pptx|xlsx)$/i.test(file.originalname);
|
||||
if (isOfficeDoc) {
|
||||
const docPath = path.join(tmpDir, `doc_${docTextParts.length}${path.extname(file.originalname)}`);
|
||||
fs.writeFileSync(docPath, file.buffer);
|
||||
try {
|
||||
const md = await runMarkitdown(docPath);
|
||||
if (md && md.trim()) docTextParts.push(md.trim());
|
||||
} catch (e) {
|
||||
return res.status(400).json({
|
||||
error: `解析${file.originalname}失败,请确认服务器已安装markitdown(SSH执行:pipx install 'markitdown[docx,pptx,xlsx]'):${e.message}`,
|
||||
});
|
||||
}
|
||||
} else if (isPdf) {
|
||||
const pdfSubDir = fs.mkdtempSync(path.join(tmpDir, 'pdf-'));
|
||||
const pages = await renderPdfToImages(file.buffer, pdfSubDir);
|
||||
imagePaths.push(...pages);
|
||||
// 先试MarkItDown直接读文字层——如果这份PDF本身带干净的文字层(不是扫描件/纯图片排版的目录),
|
||||
// 这条路又快又准,还能保留表格结构;没装markitdown、或者这份PDF本来就没有文字层(返回内容
|
||||
// 短到没有意义),就静默退回老办法:整页渲染成图片再OCR,不阻断这次上传
|
||||
let usedTextLayer = false;
|
||||
try {
|
||||
const probePath = path.join(pdfSubDir, 'source.pdf');
|
||||
fs.writeFileSync(probePath, file.buffer);
|
||||
const md = await runMarkitdown(probePath);
|
||||
if (md && md.trim().length > 80) {
|
||||
docTextParts.push(md.trim());
|
||||
usedTextLayer = true;
|
||||
}
|
||||
} catch (e) { /* markitdown不可用或转换失败,退回OCR */ }
|
||||
if (!usedTextLayer) {
|
||||
const pages = await renderPdfToImages(file.buffer, pdfSubDir);
|
||||
imagePaths.push(...pages);
|
||||
}
|
||||
} else if (isImage) {
|
||||
const ext = path.extname(file.originalname) || '.png';
|
||||
const imgPath = path.join(tmpDir, `img_${imagePaths.length}${ext}`);
|
||||
|
|
@ -885,12 +932,12 @@ app.post('/api/ai/parse-catalog', requireAuth, aiLimiter, (req, res) => {
|
|||
.trim();
|
||||
if (cleaned) htmlTextParts.push(cleaned);
|
||||
} else {
|
||||
return res.status(400).json({ error: `不支持的文件类型:${file.originalname}(只支持PDF、图片或HTML)` });
|
||||
return res.status(400).json({ error: `不支持的文件类型:${file.originalname}(只支持PDF、图片、HTML、Word/PPT/Excel)` });
|
||||
}
|
||||
}
|
||||
|
||||
const ocrText = imagePaths.length ? await ocrImages(imagePaths) : '';
|
||||
const text = [ocrText, ...htmlTextParts].filter(Boolean).join('\n\n--- 下一份文件 ---\n\n');
|
||||
const text = [ocrText, ...docTextParts, ...htmlTextParts].filter(Boolean).join('\n\n--- 下一份文件 ---\n\n');
|
||||
if (!text) return res.status(400).json({ error: '没有识别/读取到任何文字内容,请确认文件清晰、内容完整' });
|
||||
|
||||
// 识别出来的文本太长就截断,避免超出模型输出预算;产品目录类文档核心信息通常靠前
|
||||
|
|
|
|||
Loading…
Reference in New Issue