/** * fun 结点:根据描述词(prompt)用 python/imagedl 搜索并下载一张图片到 savePath * 入参:prompt(描述词), savePath(相对路径时基于 folderPath) */ const path = require('path') const fs = require('fs') const { spawnSync } = require('child_process') const { getPythonExeFromConfig } = require('../../../python-exe-from-config.js') const configPath = process.env.STATIC_ROOT ? path.join(path.dirname(path.resolve(process.env.STATIC_ROOT)), 'config.js') : path.join(__dirname, '..', '..', '..', '..', 'config.js') const config = fs.existsSync(configPath) ? require(configPath) : {} const projectRoot = (config.projectRoot && fs.existsSync(config.projectRoot)) ? config.projectRoot : path.dirname(path.resolve(configPath)) const scriptPath = path.join(projectRoot, 'python', 'scripts', 'download-img-by-prompt.py') const imagedlParent = path.join(projectRoot, 'python') const imagedlRequirements = path.join(projectRoot, 'python', 'imagedl', 'requirements.txt') function buildSavePath(savePath, folderPath) { if (!savePath || typeof savePath !== 'string') return null const trimmed = savePath.trim() if (path.isAbsolute(trimmed) || trimmed.match(/^[A-Za-z]:/)) return trimmed return folderPath ? path.join(folderPath, trimmed) : path.resolve(projectRoot, trimmed) } /** spawnSync 非 0 退出时拼详细日志(含启动失败、信号、截断后的 stdout/stderr) */ function formatSpawnFailure (r, pythonPath, scriptPath) { const maxLen = 1500 const trunc = (s) => { const t = String(s || '').trim() if (!t) return '' return t.length > maxLen ? `${t.slice(0, maxLen)}…(truncated)` : t } const parts = [] if (pythonPath) { const abs = path.isAbsolute(pythonPath) || /^[A-Za-z]:/.test(pythonPath) const missing = abs && !fs.existsSync(pythonPath) parts.push(`python=${pythonPath}${missing ? ' (file missing)' : ''}`) } if (scriptPath) parts.push(`script=${scriptPath}`) if (r.error) { const e = r.error const code = e.code != null ? String(e.code) : '' parts.push(`spawnError${code ? `[${code}]` : ''}: ${e.message || e}`) } if (r.signal) parts.push(`signal=${r.signal}`) if (r.status !== null && r.status !== undefined) parts.push(`exitCode=${r.status}`) else parts.push('exitCode=(null)') const stderr = trunc(r.stderr) const stdout = trunc(r.stdout) if (stderr) parts.push(`stderr: ${stderr}`) if (stdout) parts.push(`stdout: ${stdout}`) return parts.length ? parts.join(' | ') : 'download-img 执行失败(无子进程输出)' } async function executeDownloadImg({ prompt, savePath, folderPath }) { if (!prompt || typeof prompt !== 'string' || !prompt.trim()) return { success: false, error: 'download-img 缺少 prompt 参数' } if (savePath == null) return { success: false, error: 'download-img 缺少 savePath 参数' } const absolutePath = buildSavePath(String(savePath).trim(), folderPath) if (!absolutePath) return { success: false, error: 'download-img savePath 无效' } if (!fs.existsSync(scriptPath)) return { success: false, error: `脚本不存在: ${scriptPath}` } const pythonPath = getPythonExeFromConfig(config) const args = [scriptPath, '--prompt', prompt.trim(), '--save-path', absolutePath.replace(/\\/g, '/')] const runScript = () => spawnSync(pythonPath, args, { encoding: 'utf-8', timeout: 120000, env: { ...process.env, PYTHONIOENCODING: 'utf-8', IMAGEDL_PARENT: imagedlParent }, cwd: projectRoot }) let r = runScript() let out = (r.stdout || '').trim() const err = (r.stderr || '').trim() const fullOut = out + (err ? '\n' + err : '') if (r.status !== 0 && fs.existsSync(imagedlRequirements) && (fullOut.includes('No module named') || fullOut.includes('ModuleNotFoundError'))) { spawnSync(pythonPath, ['-m', 'pip', 'install', '-r', imagedlRequirements, '-q'], { encoding: 'utf-8', timeout: 180000, cwd: projectRoot }) r = runScript() out = (r.stdout || '').trim() } if (r.status !== 0) { return { success: false, error: formatSpawnFailure(r, pythonPath, scriptPath) } } // stdout 可能包含进度条等,取最后一行以 { 开头的行作为 JSON const lines = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean) let jsonStr = lines[lines.length - 1] if (!jsonStr || !jsonStr.startsWith('{')) { const lastJson = lines.filter(l => l.startsWith('{')).pop() jsonStr = lastJson || out } let result try { result = JSON.parse(jsonStr) } catch (_) { const raw = String(out || '').trim() const tail = raw.length > 800 ? raw.slice(-800) : raw return { success: false, error: `无法解析 JSON 输出 | stdout(tail): ${tail || '(empty)'}` } } if (!result.success) return { success: false, error: result.error || '未下载到图片' } return { success: true, path: result.path } } module.exports = { executeDownloadImg }