// npm run backend:setup —— 用 uv 建 / 同步后端 Python 环境(.venv,Python 3.12) // // - 有 backend/requirements.lock(uv pip compile 生成、带哈希)时按锁精确同步(会卸掉锁外的包) // - 没有锁时按 requirements.txt + requirements-build.txt 安装 // - 找不到 uv 时用系统 Python 的 pip 把 uv 装进 native/build/uv-bootstrap,不动系统环境 // - 结束时核对 browser-use 0.13.10 和 PyInstaller 版本 import { spawn, spawnSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { homedir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const win = process.platform === 'win32'; const python = path.join(root, '.venv', win ? 'Scripts/python.exe' : 'bin/python'); const lock = path.join(root, 'backend/requirements.lock'); const requirements = path.join(root, 'backend/requirements.txt'); const buildRequirements = path.join(root, 'backend/requirements-build.txt'); const bootstrap = path.join(root, 'native/build/uv-bootstrap'); const uvName = win ? 'uv.exe' : 'uv'; function run(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: root, stdio: 'inherit', windowsHide: true }); child.once('error', reject); child.once('exit', code => (code === 0 ? resolve() : reject(new Error(`${command} ${args.join(' ')} 退出码 ${code}`)))); }); } function available(command, args) { return spawnSync(command, args, { encoding: 'utf8', windowsHide: true }).status === 0; } async function findUv() { const override = process.argv.slice(2).find(argument => argument.startsWith('--uv=')); if (override) { const executable = path.resolve(override.slice('--uv='.length)); if (!available(executable, ['--version'])) throw new Error(`指定的 uv 不可用: ${executable}`); return executable; } const candidates = [ 'uv', path.join(homedir(), '.local/bin', uvName), path.join(homedir(), '.cargo/bin', uvName), ...['bin', 'Scripts', 'uv'].map(folder => path.join(bootstrap, folder, uvName)), ]; for (const executable of candidates) if (available(executable, ['--version'])) return executable; // 只把 uv 装到本项目忽略目录里,不改系统 Python 的 site-packages const pythons = win ? [['py', ['-3.12']], ['python', []]] : [['python3.12', []], ['python3', []]]; const found = pythons.find(([command, args]) => available(command, [...args, '-m', 'pip', '--version'])); if (!found) throw new Error('没有 uv,也没有带 pip 的 Python 3.12,先装其中一个再跑 backend:setup'); await mkdir(bootstrap, { recursive: true }); await run(found[0], [...found[1], '-m', 'pip', '--isolated', '--disable-pip-version-check', 'install', '--no-compile', '--no-deps', '--target', bootstrap, 'uv>=0.12']); const bundled = ['bin', 'Scripts', 'uv'].map(folder => path.join(bootstrap, folder, uvName)).find(exe => available(exe, ['--version'])); if (!bundled) throw new Error('uv 引导安装没有产出可执行文件'); return bundled; } try { const uv = await findUv(); console.log(`[backend:setup] uv: ${uv}`); if (!existsSync(python)) await run(uv, ['venv', '--python', '3.12', path.join(root, '.venv')]); const version = spawnSync(python, ['-c', 'import sys; assert sys.version_info[:2] == (3, 12), sys.version'], { encoding: 'utf8', windowsHide: true }); if (version.status !== 0) throw new Error(`.venv 必须是 Python 3.12:${version.stderr.trim()}`); if (existsSync(lock)) { await run(uv, ['pip', 'sync', '--python', python, '--require-hashes', lock]); } else { console.warn('[backend:setup] 没有 backend/requirements.lock,按 requirements.txt 安装(建议之后运行 npm run backend:lock)'); await run(uv, ['pip', 'install', '--python', python, '-r', requirements, '-r', buildRequirements]); } await run(python, ['-c', [ 'import importlib.metadata as m', "assert m.version('browser-use') == '0.13.10', m.version('browser-use')", "print('[backend:setup] browser-use', m.version('browser-use'), '/ PyInstaller', m.version('pyinstaller'), '/ psycopg', m.version('psycopg'))", ].join('; ')]); } catch (error) { console.error(`[backend:setup] 失败:${error.message}`); process.exitCode = 1; }