Lines 35-120javascript
35 // 误选 2.7 会被缓存 → 后续所有 py()/runScript 全用 2.7 跑 → setup.py 直接失败。
36 // 检查 --version 输出以 "Python 3." 开头, 且次版本 >=8。
37 for (const c of ["python", "python3"]) {
39 const out = execSync(`${c} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
40 const m = /Python\s+3\.(\d+)/i.exec((out || "").trim());
41 if (m && parseInt(m[1], 10) >= 8) {
45 // 是 python 但版本不够 (2.7 或 3.0-3.7) → 跳过试下一个候选
46 } catch { /* try next */ }
52function py(script, args = []) {
53 // v3.x: 不再直接找扁平路径 (scripts/<script>), 改为转发 wlkj.py 单一真源调度器。
54 // 根治 "引擎分层重组后 cli.js 路径全部失效" 的载体漂移。
55 const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
56 if (!fs.existsSync(wlkj)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
57 const pyCmd = detectPyCmd();
58 if (!pyCmd) { console.log("Python 未安装, 无法运行脚本。跑: npx @hupan56/wlkj install-env"); return ""; }
59 // 把脚本名当命令传给 wlkj.py (DISPATCH 命中则路由, 否则 run fallback 递归找)
61 const cmdName = script.replace(/\.py$/, "").replace(/\//g, "-");
62 return execSync(`${pyCmd} "${wlkj}" ${cmdName} ${args.map(a => `"${a}"`).join(" ")}`, { cwd: process.cwd(), encoding: "utf-8", timeout: 15000, env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } });
63 } catch (e) { return (e.stdout || e.message); }
HighRuntime Package Install
Package source invokes a package manager install command at runtime.
bin/cli.jsView on unpkg · L55 66// 透传执行: 直接 spawn (不捕获输出, 实时显示, 支持长超时和交互式)
67// 用于 kg_build/eval_prd/autotest 等耗时脚本, 以及需要交互的场景
68function runScript(scriptName, args = [], timeoutMs = 600000) {
69 // v3.x: 转发 wlkj.py 单一真源 (不再直接找扁平 scripts/<scriptName>)
70 const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
71 if (!fs.existsSync(wlkj)) { console.log("引擎未安装。跑: npx @hupan56/wlkj init"); process.exit(1); }
72 const pyCmd = detectPyCmd();
73 if (!pyCmd) { console.log("Python 未安装。跑: npx @hupan56/wlkj install-env"); process.exit(1); }
74 const { spawnSync } = require("child_process");
75 const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
82// 带镜像兜底的 pip 安装 —— 复用 Python 的 common/pip_install.py (单一事实源)
83// 为什么不复用 Python: 上一轮给 init_doctor 做了 common/pip_install.py (失败切清华/阿里/腾讯)。
84// cli.js 的 install-env/update 是独立 JS 路径, 之前没享受镜像兜底 → 50 人里弱网用户全崩。
85// 这里调用同一个 Python 工具, 保证两路径行为一致。
87// 优先: 引擎已装 → 调 foundation.utils.pip_install (有镜像兜底)
88// 回退: 引擎没装 → 裸 pip install (install-env 首次场景, 无 Python 工具可用)
90// 实现注意 (修 2.7.0 的真 bug): 不能用字符串拼接构造 Python -c 代码 ——
91// Windows 路径含反斜杠, 拼进 Python 字符串后 \Y \n 等成了非法转义 + 引号丢失,
92// 导致 SyntaxError → 镜像兜底静默失败 → 回退裸 pip → 弱网用户全崩。
93// 改用环境变量传参 (WLKJ_REQ_PATH), Python 侧 os.environ 读, 彻底避开转义地狱。
94function installPipDeps(pyCmd, reqPath) {
95 // ⚠ v3.x 重写: 直接用 Node spawnSync 跑 pip (不嵌套 Python 子进程)。
96 // 原方案 `python -c "...install_with_fallback..."` 在 Anaconda 3.9+ Windows 下,
97 // pip 进镜像重试(长时间编译 tree-sitter/playwright)退出时会触发
98 // "PyEval_SaveThread: GIL released" 致命崩溃 → Python 死、结果丢失、回退裸 pip、
99 // 弱网新机装不上。Node 直接跑 pip 不嵌套 Python, 无此 GIL 问题。
100 // 镜像兜底逻辑在 JS 侧复刻同一策略 (默认源→清华→阿里→腾讯), 行为与 Python 版一致。
101 const { spawnSync } = require("child_process");
103 ["清华", "https://pypi.tuna.tsinghua.edu.cn/simple"],
104 ["阿里", "https://mirrors.aliyun.com/pypi/simple/"],
105 ["腾讯", "https://mirrors.cloud.tencent.com/simple"],
107 const installEnv = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
108
HighSame File Env Network Execution
A single source file combines environment access, network access, and code or shell execution; review context before blocking.
bin/cli.jsView on unpkg · L100 109 function runPip(extraArgs, timeoutMs) {
110 // stdio inherit 让用户看到进度; 不捕获输出 = 不撑管道、不触发任何崩溃
111 const r = spawnSync(pyCmd, ["-m", "pip", "install", "--no-input", "--progress-bar", "off", ...extraArgs, "-r", reqPath],
112 { stdio: "inherit", timeout: timeoutMs || 300000, encoding: "utf-8", env: installEnv });
113 return r.status === 0;
116 function pkgsInstalled() {
117 // 轻量 import 验证: 不跑 pip 子进程, 只 python -c "import ...", 无 GIL 风险。
118 // 核心包都 import 成功就算装好 (即使某些可选包编译失败 pip 返回非0)。
119 const coreMods = ["duckdb", "pymysql", "yaml", "requests"];
120 const mod = coreMods.map(m => "__import__('%s')".replace("%s", m)).join(";");