feat: add simplified windows x86 support

This commit is contained in:
yankun
2026-06-27 18:33:59 +08:00
parent 7de0777748
commit 67c995db83
12 changed files with 400 additions and 84 deletions
+82 -38
View File
@@ -1,9 +1,10 @@
'use strict';
const { execSync, spawnSync, spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const log = require('./logger');
const fs = require('fs');
const os = require('os');
const log = require('./logger');
const { IS_WINDOWS } = require('./platform-paths');
const AP_SSID_PREFIX = 'ClawBox-';
const AP_IP = '10.42.0.1';
@@ -68,8 +69,13 @@ function _firstScanWiredIfaceWithCarrier() {
* 返回当前可用于「有线 ping / 路由」的网卡名。
* 优先级:CLAWD_ETH_IFACE → 存在 end0 则只用 end0 → 否则扫描 sysfs。
*/
function getWiredIfaceWithCarrier() {
const explicit = process.env.CLAWD_ETH_IFACE;
function getWiredIfaceWithCarrier() {
if (IS_WINDOWS) {
const entry = _localNetworkEntries()[0];
return entry ? entry.iface : null;
}
const explicit = process.env.CLAWD_ETH_IFACE;
if (explicit) {
return _netIfaceExists(explicit) && _sysfsCarrierUp(explicit) ? explicit : null;
}
@@ -79,16 +85,18 @@ function getWiredIfaceWithCarrier() {
return _firstScanWiredIfaceWithCarrier();
}
function hasWiredCarrier() {
return getWiredIfaceWithCarrier() !== null;
}
function hasWiredCarrier() {
if (IS_WINDOWS) return _localNetworkEntries().length > 0;
return getWiredIfaceWithCarrier() !== null;
}
/**
* LAN 面板灯:只反映 RJ45 对应口,与 `cat /sys/class/net/end0/carrier 2>/dev/null` 同源(仅读 carrier)。
* 若配置的接口在 sysfs 中不存在(常见为开发机无 end0),则退回与 hasWiredCarrier() 一致,避免灯永远灭。
*/
function hasLanCableCarrier() {
const iface = _ethIfaceEnvOrDefault();
function hasLanCableCarrier() {
if (IS_WINDOWS) return hasWiredCarrier();
const iface = _ethIfaceEnvOrDefault();
if (_netIfaceExists(iface)) return _sysfsCarrierUp(iface);
return hasWiredCarrier();
}
@@ -114,16 +122,19 @@ function _tryPingWiredInternet() {
/**
* 仅经有线口 ping 公网(不依赖默认路由)。
*/
function hasWiredInternetProbe() {
return _tryPingWiredInternet();
}
function hasWiredInternetProbe() {
if (IS_WINDOWS) return hasInternet();
return _tryPingWiredInternet();
}
/**
* 检测是否有真实互联网连接。
* 注意:NetworkManager 的 limited/local 可能只是 AP 本地网络或 captive 状态,不能当公网可用。
*/
function hasInternet() {
const wifiSta = isWifiStaConnected();
function hasInternet() {
if (IS_WINDOWS) return _localNetworkEntries().length > 0;
const wifiSta = isWifiStaConnected();
const wired = getWiredIfaceWithCarrier();
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 falsenmcli 有缓存,不可信)
@@ -143,8 +154,9 @@ function hasInternet() {
* 获取默认 WiFi 接口名(wlan0 等)。
* 必须 TYPE 精确为 wifi,不能用 grep wifi(会误匹配 wifi-p2p,导致选到 p2p-dev-wlan0STA/热点均失败)。
*/
function getWifiIface() {
if (AP_IFACE) return AP_IFACE;
function getWifiIface() {
if (IS_WINDOWS) return '';
if (AP_IFACE) return AP_IFACE;
try {
const out = run('nmcli -t -f DEVICE,TYPE device');
let fallback = '';
@@ -173,8 +185,9 @@ function getWifiIface() {
/**
* 扫描周围 WiFi,返回 [{ ssid, signal, security }]
*/
function scanWifi() {
const iface = getWifiIface();
function scanWifi() {
if (IS_WINDOWS) return [];
const iface = getWifiIface();
try {
// 先触发一次扫描
try { run(`nmcli device wifi rescan ifname ${iface}`); } catch (_) {}
@@ -265,7 +278,11 @@ function nmcliAsync(args, timeoutMs = 60000) {
* 必须异步:同步 spawnSync + execSync(sleep) 会卡住主线程,导致 systemd WatchdogSec 内收不到 WATCHDOG=1。
* @returns {Promise<{ success: boolean, error?: string }>}
*/
async function connectWifi(ssid, password) {
async function connectWifi(ssid, password) {
if (IS_WINDOWS) {
return { success: false, error: 'Windows/x86 clawd does not manage WiFi credentials' };
}
cancelHotspotRadioRetry(`准备连接 WiFi: ${ssid}`);
const iface = getWifiIface();
log.info('network', `尝试连接 WiFi: ${ssid}ifname=${iface}`);
@@ -521,7 +538,11 @@ function _activateHotspot(ssid, iface, timeoutMs = 8000) {
/**
* 启动 WiFi AP 热点
*/
function startAP(clawId) {
function startAP(clawId) {
if (IS_WINDOWS) {
throw new Error('Windows/x86 clawd does not support AP provisioning');
}
const iface = getWifiIface();
const ssid = `${AP_SSID_PREFIX}${clawId || 'Setup'}`;
@@ -608,7 +629,9 @@ function _parseNmcliTerseLine(line) {
/**
* 列出已保存的 WiFi STA 连接(排除自身热点),按 autoconnect-priority 从高到低排序。
*/
function listSavedWifiConnections() {
function listSavedWifiConnections() {
if (IS_WINDOWS) return [];
const profiles = [];
try {
const out = run('nmcli -t -f NAME,UUID,TYPE,AUTOCONNECT,AUTOCONNECT-PRIORITY connection show');
@@ -663,7 +686,11 @@ async function _ensureActiveWifiAutoconnect() {
* 主动让 NetworkManager 尝试已保存 WiFi。
* clawd 只做调度;真正的认证、DHCP、重连细节仍交给 NM。
*/
async function connectSavedWifiConnections() {
async function connectSavedWifiConnections() {
if (IS_WINDOWS) {
return { success: false, error: 'Windows/x86 clawd does not manage saved WiFi profiles' };
}
cancelHotspotRadioRetry('准备连接已保存 WiFi');
const iface = getWifiIface();
const profiles = listSavedWifiConnections();
@@ -701,7 +728,9 @@ async function connectSavedWifiConnections() {
* 是否已以 STA 连上某 WiFi(排除自身热点)。
* 不用 device 列表按 `:` 拆字段(连接名含冒号会错;state 含 connecting 勿误匹配 connected)。
*/
function isWifiStaConnected() {
function isWifiStaConnected() {
if (IS_WINDOWS) return false;
const iface = getWifiIface();
let state;
let conn;
@@ -718,22 +747,37 @@ function isWifiStaConnected() {
return true;
}
function _ifaceNetworkType(name) {
function _ifaceNetworkType(name) {
if (IS_WINDOWS) {
return /wi-?fi|wlan|wireless/i.test(name) ? 'wifi' : 'lan';
}
const wifi = getWifiIface();
if (name === wifi || name.startsWith('wl')) return 'wifi';
if (name === DEFAULT_ETH_IFACE || name.startsWith('en') || name.startsWith('eth')) return 'lan';
return null;
}
function _localNetworkEntries() {
const ifaces = os.networkInterfaces();
const entries = [];
for (const [name, addrs] of Object.entries(ifaces)) {
if (!addrs) continue;
const type = _ifaceNetworkType(name);
if (!type) continue;
for (const addr of addrs) {
if (addr.family !== 'IPv4' || addr.internal) continue;
if (name === DEFAULT_ETH_IFACE || name.startsWith('en') || name.startsWith('eth')) return 'lan';
return null;
}
function _isWindowsExcludedIface(name) {
return /tailscale|zerotier|vethernet|virtual|vmware|virtualbox|docker|wsl|hyper-v|loopback|npcap|meta/i.test(name);
}
function _isWindowsExcludedAddress(ip) {
return /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)
|| /^198\.(18|19)\./.test(ip);
}
function _localNetworkEntries() {
const ifaces = os.networkInterfaces();
const entries = [];
for (const [name, addrs] of Object.entries(ifaces)) {
if (!addrs) continue;
if (IS_WINDOWS && _isWindowsExcludedIface(name)) continue;
const type = _ifaceNetworkType(name);
if (!type) continue;
for (const addr of addrs) {
if (addr.family !== 'IPv4' || addr.internal) continue;
if (IS_WINDOWS && _isWindowsExcludedAddress(addr.address)) continue;
// clawd-hotspot 的 AP 管理网段只用于配网,不上报为 BOX 可访问地址。
if (addr.address.startsWith('10.42.')) continue;
entries.push({ ip: addr.address, type, iface: name });