Update server.js via upload script - 2026-08-02 17:13:49

This commit is contained in:
mike 2026-08-02 17:14:08 +08:00
parent 71fa9b238d
commit 50e76b2594
1 changed files with 53 additions and 6 deletions

View File

@ -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}失败请确认服务器已安装markitdownSSH执行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: '没有识别/读取到任何文字内容,请确认文件清晰、内容完整' });
// 识别出来的文本太长就截断,避免超出模型输出预算;产品目录类文档核心信息通常靠前