assistant/scripts/setup-backend.mjs

82 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}