ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

167 lines
6.0 KiB

const { spawn } = require('child_process');
const fsPromises = require('fs').promises;
const fs = require('fs');
const path = require('path');
const os = require('os');
const { convertPdfToImages } = require('../utils/pdfToImages');
const soffice = process.env.SOFFICE_PATH || 'soffice';
module.exports.docToDocx = async (ctx, next) => {
let tempDir = null;
let outputPath = null;
try {
if (!ctx.file) {
ctx.status = 400;
ctx.body = { message: '请上传一个 .doc 文件' };
return;
}
const file = ctx.file; // { fieldname, originalname, encoding, mimetype, destination, filename, path, size }
// 校验扩展名
if (!file.originalname.toLowerCase().endsWith('.doc')) {
ctx.status = 400;
ctx.body = { message: '仅支持 .doc 格式的文件' };
return;
}
// 创建专属临时目录(避免并发冲突)
tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'doc2docx-'));
const inputPath = path.join(tempDir, file.originalname);
const outputDir = tempDir;
// 移动 multer 生成的临时文件到我们的目录(可选,也可直接用 file.path)
await fsPromises.rename(file.path, inputPath);
// 调用 LibreOffice 转换
const args = ['--headless', '--convert-to', 'docx', '--outdir', outputDir, inputPath];
await new Promise((resolve, reject) => {
const child = spawn(soffice, args, { stdio: 'ignore', windowsHide: true });
child.on('error', reject);
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Exit code ${code}`))));
});
// 构造输出路径
const baseName = path.basename(file.originalname, '.doc');
outputPath = path.join(outputDir, `${baseName}.docx`);
await fsPromises.access(outputPath); // 确保文件存在
// 返回文件流
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(baseName)}.docx"`);
ctx.body = fs.createReadStream(outputPath);
// 清理
ctx.res.on('finish', () => fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => { }));
} catch (error) {
ctx.logger?.error('docToDocx error:', error);
ctx.status = 400;
ctx.body = { message: error.message || '文件转换失败' };
// 出错时立即清理
if (tempDir) fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => { });
}
};
// ========== 新增:检测 pdftoppm 是否可用 ==========
function checkPopplerAvailable() {
return new Promise((resolve) => {
const { spawn } = require('child_process');
let found = false;
const child = spawn('pdftoppm', ['-h'], { timeout: 2000 });
child.on('error', (err) => {
console.warn('[PDF2IMG] pdftoppm not found or failed to start:', err.message);
resolve(false);
});
child.on('close', (code) => {
if (code === 0) {
console.log('[PDF2IMG] ✅ Poppler (pdftoppm) is available');
resolve(true);
} else {
console.warn(`[PDF2IMG] pdftoppm exited with code ${code}`);
resolve(false);
}
});
});
}
module.exports.pdfToImage = async (ctx, next) => {
let tempDir = null;
// 记录请求开始
const requestId = Math.random().toString(36).substring(2, 10);
const logPrefix = `[REQ-${requestId}]`;
try {
console.log(`${logPrefix} 📥 Received PDF upload request`);
if (!ctx.file) {
ctx.status = 400;
ctx.body = { message: '请上传一个 .pdf 文件' };
return;
}
const file = ctx.file;
console.log(`${logPrefix} Original filename: ${file.originalname}`);
console.log(`${logPrefix} Temp file path: ${file.path}`);
console.log(`${logPrefix} File size: ${file.size} bytes`);
if (!file.originalname.toLowerCase().endsWith('.pdf')) {
ctx.status = 400;
ctx.body = { message: '仅支持 .pdf 格式的文件' };
return;
}
// 创建临时目录
tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'pdf2image-'));
console.log(`${logPrefix} Using temp dir: ${tempDir}`);
const safeFilename = `input_${Date.now()}.pdf`;
const inputPath = path.join(tempDir, safeFilename);
const outputDir = tempDir;
// 复制文件(关键修复)
console.log(`${logPrefix} Copying file to: ${inputPath}`);
await fsPromises.copyFile(file.path, inputPath);
await fsPromises.unlink(file.path); // 清理 multer 临时文件
console.log(`${logPrefix} Multer temp file deleted`);
// 可选:调试时保留临时文件(通过环境变量控制)
const keepTemp = process.env.KEEP_TEMP_FILES === '1';
if (keepTemp) {
console.warn(`${logPrefix} ⚠️ KEEP_TEMP_FILES=1: Will NOT clean up ${tempDir}`);
}
// 检查 Poppler(仅一次,可选)
// await checkPopplerAvailable(); // 取消注释可验证
// 转换
const images = await convertPdfToImages(inputPath, outputDir, 150); // 先用 150 DPI 调试
ctx.status = 200;
ctx.body = { message: '文件转换成功', data: { images } };
} catch (error) {
// 统一日志
const errMsg = error.message || 'Unknown error';
ctx.logger?.error(`${logPrefix} pdfToImage failed: ${errMsg}`, { error });
console.error(`${logPrefix} 🚨 HANDLED ERROR:`, errMsg);
ctx.status = 400;
ctx.body = { message: errMsg };
} finally {
// 清理临时目录(除非调试)
// if (tempDir && process.env.KEEP_TEMP_FILES !== '1') {
// fsPromises.rm(tempDir, { recursive: true, force: true })
// .then(() => console.log(`${logPrefix} 🧹 Cleaned up temp dir: ${tempDir}`))
// .catch(err => console.warn(`${logPrefix} Failed to clean temp dir:`, err.message));
// }
}
};