fix(config): use standalone bundled Node for runtime compatibility checks

Standalone OpenClaw ships its own Node.js and openclaw.cmd prefers that
binary over PATH. check_node() was inspecting the first system Node
(often an older nvm install), marking compatible=false and blocking
Gateway startup via ensure_node_runtime_compatible().

Prefer the bundled node next to the active standalone CLI when present,
and mirror the same logic in dev-api web mode.

Co-authored-by: 晴天 <1186258278@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-06-10 11:05:46 +00:00
parent 5aa09f4bb7
commit 92f67849cb
2 changed files with 101 additions and 14 deletions

View File

@@ -730,6 +730,13 @@ function decorateNodeDetection(base) {
}
}
function standaloneBundledNodePath(cliPath) {
if (!cliPath) return null
const dir = path.dirname(cliPath)
const nodeBin = path.join(dir, isWindows ? 'node.exe' : 'node')
return fs.existsSync(nodeBin) ? nodeBin : null
}
function ensureNodeRuntimeCompatibleWeb() {
const node = handlers.check_node()
if (!node.installed) throw new Error('Node.js 未安装或未检测到,请先安装 Node.js 后重新检测')
@@ -10851,6 +10858,19 @@ const handlers = {
check_node() {
try {
const cliPath = resolveOpenclawCliPath()
if (cliPath && classifyCliSource(cliPath) === 'standalone') {
const bundled = standaloneBundledNodePath(cliPath)
if (bundled) {
const ver = execSync(`"${bundled}" --version 2>&1`, { windowsHide: true }).toString().trim()
return decorateNodeDetection({
installed: true,
version: ver,
path: bundled,
detectedFrom: 'standalone-bundled',
})
}
}
const ver = execSync('node --version 2>&1', { windowsHide: true }).toString().trim()
return decorateNodeDetection({ installed: true, version: ver, path: findCommandPath('node') })
} catch {

View File

@@ -250,6 +250,50 @@ pub(crate) fn openclaw_node_requirement() -> Option<String> {
read_package_json_field(&pkg_json, "/engines/node")
}
fn standalone_bundled_node_bin(cli_path: &str) -> Option<PathBuf> {
let dir = std::path::Path::new(cli_path).parent()?;
#[cfg(target_os = "windows")]
let node_bin = dir.join("node.exe");
#[cfg(not(target_os = "windows"))]
let node_bin = dir.join("node");
node_bin.is_file().then_some(node_bin)
}
fn node_version_from_bin(node_bin: &std::path::Path) -> Option<String> {
let mut cmd = Command::new(node_bin);
cmd.arg("--version");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
let output = cmd.output().ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
}
fn populate_node_detection_result(
result: &mut serde_json::Map<String, Value>,
version: String,
path: String,
detected_from: String,
) {
let required_version = openclaw_node_requirement();
let compatible = required_version
.as_deref()
.map(|req| node_version_satisfies_requirement(&version, req))
.unwrap_or(true);
result.insert("installed".into(), Value::Bool(true));
result.insert("version".into(), Value::String(version));
result.insert("path".into(), Value::String(path));
result.insert("detectedFrom".into(), Value::String(detected_from));
result.insert("compatible".into(), Value::Bool(compatible));
result.insert(
"requiredVersion".into(),
required_version.map(Value::String).unwrap_or(Value::Null),
);
}
pub(crate) fn ensure_node_runtime_compatible() -> Result<(), String> {
let node = check_node()?;
let installed = node
@@ -4771,6 +4815,23 @@ pub fn check_node() -> Result<Value, String> {
let mut result = serde_json::Map::new();
let enhanced = super::enhanced_path();
// standalone 自带 Node.js其 openclaw.cmd 优先使用同目录 node.exe与 PATH 中的旧版 Node 无关。
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
if crate::utils::classify_cli_source(&cli_path) == "standalone" {
if let Some(bundled) = standalone_bundled_node_bin(&cli_path) {
if let Some(ver) = node_version_from_bin(&bundled) {
populate_node_detection_result(
&mut result,
ver,
bundled.to_string_lossy().to_string(),
"standalone-bundled".into(),
);
return Ok(Value::Object(result));
}
}
}
}
// 尝试通过 which/where 命令找到 node 的实际路径
let node_path = find_node_path(&enhanced);
@@ -4783,20 +4844,7 @@ pub fn check_node() -> Result<Value, String> {
Ok(o) if o.status.success() => {
let ver = String::from_utf8_lossy(&o.stdout).trim().to_string();
let detected_from = detect_node_source(&path);
let required_version = openclaw_node_requirement();
let compatible = required_version
.as_deref()
.map(|req| node_version_satisfies_requirement(&ver, req))
.unwrap_or(true);
result.insert("installed".into(), Value::Bool(true));
result.insert("version".into(), Value::String(ver));
result.insert("path".into(), Value::String(path));
result.insert("detectedFrom".into(), Value::String(detected_from));
result.insert("compatible".into(), Value::Bool(compatible));
result.insert(
"requiredVersion".into(),
required_version.map(Value::String).unwrap_or(Value::Null),
);
populate_node_detection_result(&mut result, ver, path, detected_from);
}
_ => {
result.insert("installed".into(), Value::Bool(false));
@@ -7639,4 +7687,23 @@ mod write_openclaw_config_merge_tests {
"^22.19.0 || >=24.0.0"
));
}
#[test]
fn standalone_bundled_node_bin_resolves_next_to_cli() {
let dir = unique_temp_dir("standalone-bundled-node");
std::fs::create_dir_all(&dir).unwrap();
let cli_path = dir.join("openclaw.cmd");
std::fs::write(&cli_path, "@echo off\r\n").unwrap();
#[cfg(target_os = "windows")]
let node_name = "node.exe";
#[cfg(not(target_os = "windows"))]
let node_name = "node";
let node_bin = dir.join(node_name);
std::fs::write(&node_bin, "").unwrap();
let resolved = super::standalone_bundled_node_bin(&cli_path.to_string_lossy());
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(resolved, Some(node_bin));
}
}