Compare commits
1
Commits
7de0777748
...
clawd_win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c995db83 |
+13
-8
@@ -11,11 +11,12 @@ const { ClawClient } = require('../lib/client');
|
||||
const config = require('../lib/config');
|
||||
const log = require('../lib/logger');
|
||||
const { pollSms } = require('../drivers/sim/sms-reader');
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// 每次启动同步 3588s demo 到 /usr/bin/demo(idempotent,失败不影响主流程)
|
||||
const demoBin = path.join(__dirname, "..", "lib/resource/3588s/demo");
|
||||
const demoDst = "/usr/bin/demo";
|
||||
if (fs.existsSync(demoBin)) {
|
||||
if (!IS_WINDOWS && fs.existsSync(demoBin)) {
|
||||
exec(`install -m 0755 "${demoBin}" "${demoDst}"`, (err) => {
|
||||
if (err) log.warn("clawd", `demo sync failed: ${err.message}`);
|
||||
else log.info("clawd", "demo synced to /usr/bin/demo");
|
||||
@@ -24,14 +25,16 @@ if (fs.existsSync(demoBin)) {
|
||||
|
||||
// 每次启动绑定 Quectel 串口驱动(失败不影响主流程)
|
||||
const bindScript = path.join(__dirname, '..', 'tools', 'bind-quectel-serial.sh');
|
||||
exec(`bash "${bindScript}"`, (err, stdout, stderr) => {
|
||||
if (err) log.warn('clawd', `bind-quectel-serial: ${stderr || err.message}`);
|
||||
else log.info('clawd', `bind-quectel-serial: ok`);
|
||||
});
|
||||
if (!IS_WINDOWS) {
|
||||
exec(`bash "${bindScript}"`, (err, stdout, stderr) => {
|
||||
if (err) log.warn('clawd', `bind-quectel-serial: ${stderr || err.message}`);
|
||||
else log.info('clawd', `bind-quectel-serial: ok`);
|
||||
});
|
||||
}
|
||||
|
||||
// 同步 Samba 共享密码(idempotent,失败不影响主流程)
|
||||
const cfg = config.load();
|
||||
if (cfg.share_key) {
|
||||
if (!IS_WINDOWS && cfg.share_key) {
|
||||
const shareKey = cfg.share_key.replace(/'/g, "'\\''");
|
||||
exec(`printf '%s\\n%s\\n' '${shareKey}' '${shareKey}' | smbpasswd -a sts -s 2>/dev/null`, (err) => {
|
||||
if (err) log.warn('clawd', `smbpasswd sync failed (samba not installed?): ${err.message}`);
|
||||
@@ -58,8 +61,10 @@ async function pollSmsSafe() {
|
||||
const client = new ClawClient();
|
||||
client.start();
|
||||
|
||||
pollSmsSafe();
|
||||
smsTimer = setInterval(pollSmsSafe, 15_000);
|
||||
if (!IS_WINDOWS) {
|
||||
pollSmsSafe();
|
||||
smsTimer = setInterval(pollSmsSafe, 15_000);
|
||||
}
|
||||
|
||||
let stopping = false;
|
||||
|
||||
|
||||
+24
-2
@@ -19,6 +19,7 @@ const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = re
|
||||
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
||||
const sysCall = require('./sys-call');
|
||||
const led = require('./led');
|
||||
const { IS_WINDOWS } = require('./platform-paths');
|
||||
|
||||
const MAX_BACKOFF_MS = 60_000;
|
||||
/** 连续若干轮 ping 后仍无 pong 才判定死链(单轮易因调度/弱网误判) */
|
||||
@@ -125,7 +126,7 @@ class ClawClient {
|
||||
led.lan.start();
|
||||
|
||||
// 蓝牙状态监控(bluetoothctl);默认不启用,见 btMonitorEnabled()
|
||||
if (btMonitorEnabled()) {
|
||||
if (!IS_WINDOWS && btMonitorEnabled()) {
|
||||
this._btMonitor = new BtMonitor();
|
||||
this._btMonitor.start();
|
||||
} else {
|
||||
@@ -317,7 +318,8 @@ class ClawClient {
|
||||
this._wsFailCount++;
|
||||
log.warn('clawd', `连接断开 (${code}),失败次数=${this._wsFailCount},${this._backoff / 1000}s 后重连...`);
|
||||
if (this._hasEverConnected && this._wsFailCount >= 3) {
|
||||
led.display.showAP();
|
||||
if (IS_WINDOWS) led.display.showErr0();
|
||||
else led.display.showAP();
|
||||
}
|
||||
if (this._certTimeError) {
|
||||
// NTP 未同步:固定 5s 重试,等时钟校正
|
||||
@@ -388,6 +390,16 @@ class ClawClient {
|
||||
local_networks: getLocalNetworks(),
|
||||
external_ip: this._externalIp ?? null,
|
||||
location: this._location ?? null,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
device_model: IS_WINDOWS ? 'windows-x86' : null,
|
||||
capabilities: IS_WINDOWS ? {
|
||||
wifi_provisioning: false,
|
||||
display: 'terminal',
|
||||
dashboard_proxy: true,
|
||||
terminal_proxy: false,
|
||||
ap_mode: false,
|
||||
} : undefined,
|
||||
...this._dashInfo,
|
||||
};
|
||||
this._send(msg);
|
||||
@@ -515,6 +527,11 @@ class ClawClient {
|
||||
// ── Hostname ─────────────────────────────────────────────────────────────────
|
||||
|
||||
_setHostname(clawId) {
|
||||
if (IS_WINDOWS) {
|
||||
log.info('clawd', `Windows/x86 mode: skip hostname change for claw-${clawId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hostname = `claw-${clawId}`;
|
||||
|
||||
// 运行时 hostname(无需文件权限)
|
||||
@@ -683,6 +700,11 @@ class ClawClient {
|
||||
}
|
||||
|
||||
async _handleUpgrade(msg) {
|
||||
if (IS_WINDOWS) {
|
||||
this._sendUpgradeProgress(0, 'failed', true, 'Windows/x86 self-upgrade is not implemented yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetVersion = msg.version;
|
||||
const installDir = path.dirname(__dirname); // /opt/clawd 或同等安装目录
|
||||
const scriptPath = path.join(installDir, 'tools', 'update-clawd.sh');
|
||||
|
||||
+2
-5
@@ -2,13 +2,10 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { getConfigDir } = require('./platform-paths');
|
||||
|
||||
// 生产环境用 /etc/clawd/,开发环境用 ~/.clawd/
|
||||
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
||||
|| (process.getuid && process.getuid() === 0
|
||||
? '/etc/clawd'
|
||||
: path.join(os.homedir(), '.clawd'));
|
||||
const CONFIG_DIR = getConfigDir();
|
||||
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||
|
||||
|
||||
+57
-1
@@ -4,6 +4,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
const os = require('os');
|
||||
const { getPersistentFile } = require('./platform-paths');
|
||||
|
||||
/**
|
||||
* 生成硬件唯一指纹作为 box_id。
|
||||
@@ -19,7 +21,59 @@ const { execSync } = require('child_process');
|
||||
* 有线 MAC 适用于嵌入式设备(网卡焊在主板,由固件烧录,不会更换)。
|
||||
*/
|
||||
|
||||
const PERSIST_FILE = '/etc/clawd/.box_id';
|
||||
const PERSIST_FILE = getPersistentFile('.box_id');
|
||||
|
||||
function readCommand(cmd, opts = {}) {
|
||||
try {
|
||||
return execSync(cmd, {
|
||||
timeout: opts.timeout || 5000,
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getWindowsMachineGuid() {
|
||||
const out = readCommand('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid');
|
||||
const m = out && out.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/i);
|
||||
return m ? m[1].trim().toLowerCase() : null;
|
||||
}
|
||||
|
||||
function getWindowsBiosUuid() {
|
||||
const ps = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "(Get-CimInstance Win32_ComputerSystemProduct).UUID"';
|
||||
const uuid = readCommand(ps);
|
||||
if (!uuid) return null;
|
||||
const clean = uuid.trim().toLowerCase();
|
||||
if (clean === '00000000-0000-0000-0000-000000000000') return null;
|
||||
if (clean === 'ffffffff-ffff-ffff-ffff-ffffffffffff') return null;
|
||||
return clean.replace(/-/g, '');
|
||||
}
|
||||
|
||||
function getWindowsMac() {
|
||||
const ifaces = os.networkInterfaces();
|
||||
const macs = [];
|
||||
for (const addrs of Object.values(ifaces)) {
|
||||
for (const addr of addrs || []) {
|
||||
const mac = String(addr.mac || '').replace(/:/g, '').toLowerCase();
|
||||
if (mac && mac.length === 12 && mac !== '000000000000') macs.push(mac);
|
||||
}
|
||||
}
|
||||
return macs.sort()[0] || null;
|
||||
}
|
||||
|
||||
function getWindowsBoxId() {
|
||||
const machineGuid = getWindowsMachineGuid();
|
||||
const biosUuid = getWindowsBiosUuid();
|
||||
const mac = getWindowsMac();
|
||||
if (machineGuid || biosUuid || mac) {
|
||||
const raw = [machineGuid || '', biosUuid || '', mac || ''].join(':');
|
||||
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32);
|
||||
}
|
||||
return getPersistentUUID();
|
||||
}
|
||||
|
||||
// ── 1. /etc/machine-id ───────────────────────────────────────────────────────
|
||||
function getMachineId() {
|
||||
@@ -119,6 +173,8 @@ function getPersistentUUID() {
|
||||
|
||||
// ── 主函数 ────────────────────────────────────────────────────────────────────
|
||||
function getBoxId() {
|
||||
if (process.platform === 'win32') return getWindowsBoxId();
|
||||
|
||||
const machineId = getMachineId();
|
||||
const cpuSerial = getCpuSerial();
|
||||
const ethMac = getEthMac();
|
||||
|
||||
+40
-22
@@ -1,16 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const { execSync, execFileSync, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const log = require('./logger');
|
||||
const { Watchdog } = require('./watchdog');
|
||||
const { IS_WINDOWS, getConfigDir, getOpenClawConfigCandidates } = require('./platform-paths');
|
||||
|
||||
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
||||
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
||||
const FRPC_BIN = path.join(CONFIG_DIR, 'frpc');
|
||||
const CONFIG_DIR = getConfigDir();
|
||||
const FRPC_BIN = path.join(CONFIG_DIR, IS_WINDOWS ? 'frpc.exe' : 'frpc');
|
||||
const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml');
|
||||
|
||||
const FRP_VERSION = '0.62.0';
|
||||
@@ -26,9 +26,7 @@ function findTtydBin() {
|
||||
|
||||
/** openclaw 持久化配置(JSON),结构与原 YAML 解析结果一致。 */
|
||||
const OPENCLAW_JSON_CANDIDATES = [
|
||||
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
||||
'/home/sts/.openclaw/openclaw.json',
|
||||
'/root/.openclaw/openclaw.json',
|
||||
...getOpenClawConfigCandidates(),
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -77,23 +75,37 @@ async function downloadFrpc() {
|
||||
};
|
||||
const frpArch = archMap[arch] || 'amd64';
|
||||
|
||||
const filename = `frp_${FRP_VERSION}_${platform}_${frpArch}.tar.gz`;
|
||||
const ext = IS_WINDOWS ? 'zip' : 'tar.gz';
|
||||
const filename = `frp_${FRP_VERSION}_${platform}_${frpArch}.${ext}`;
|
||||
const releaseBase = 'https://git.cutos.ai/claw-daemon/fatedier/releases/download';
|
||||
const attachmentMap = {
|
||||
'linux/arm64': 'https://git.cutos.ai/attachments/071748ed-955b-44c0-8dfb-38396b5dae6e',
|
||||
};
|
||||
const url = attachmentMap[`${platform}/${frpArch}`] || `${releaseBase}/v${FRP_VERSION}/${filename}`;
|
||||
const tmpFile = `/tmp/${filename}`;
|
||||
const tmpFile = path.join(os.tmpdir(), filename);
|
||||
|
||||
log.info('frpc', `下载 frpc ${FRP_VERSION} (${platform}/${frpArch})...`);
|
||||
|
||||
await downloadFile(url, tmpFile);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
execSync(`tar -xzf ${tmpFile} -C /tmp && cp /tmp/frp_${FRP_VERSION}_${platform}_${frpArch}/frpc ${FRPC_BIN}`, {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
fs.chmodSync(FRPC_BIN, 0o755);
|
||||
const packageDir = `frp_${FRP_VERSION}_${platform}_${frpArch}`;
|
||||
if (IS_WINDOWS) {
|
||||
const extractDir = path.join(os.tmpdir(), `clawd-frp-${FRP_VERSION}-${Date.now()}`);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
execFileSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
`Expand-Archive -Force -LiteralPath '${tmpFile.replace(/'/g, "''")}' -DestinationPath '${extractDir.replace(/'/g, "''")}'`,
|
||||
], { stdio: 'inherit', windowsHide: true });
|
||||
fs.copyFileSync(path.join(extractDir, packageDir, 'frpc.exe'), FRPC_BIN);
|
||||
} else {
|
||||
execSync(`tar -xzf ${tmpFile} -C /tmp && cp /tmp/${packageDir}/frpc ${FRPC_BIN}`, {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
fs.chmodSync(FRPC_BIN, 0o755);
|
||||
}
|
||||
log.info('frpc', `frpc 已安装到 ${FRPC_BIN}`);
|
||||
}
|
||||
|
||||
@@ -103,6 +115,11 @@ async function downloadFrpc() {
|
||||
* ttyd 绑定 127.0.0.1:7681,供 frpc 代理。
|
||||
*/
|
||||
async function startTtyd() {
|
||||
if (IS_WINDOWS) {
|
||||
log.info('ttyd', 'Windows/x86 mode: terminal proxy is disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
const ttydBin = findTtydBin();
|
||||
if (!ttydBin) {
|
||||
log.warn('ttyd', '未找到 ttyd,请重新运行 install.sh');
|
||||
@@ -150,13 +167,20 @@ function downloadFile(url, dest) {
|
||||
function writeFrpcConfig(clawId, frpConfig, sshSecretKey) {
|
||||
const { auth_token, dashboard_local_port = 18789 } = frpConfig;
|
||||
const ttyRemotePort = 10000 + Number(clawId);
|
||||
const stcpBlock = sshSecretKey ? `
|
||||
const stcpBlock = (!IS_WINDOWS && sshSecretKey) ? `
|
||||
[[proxies]]
|
||||
name = "ssh-${clawId}-secret"
|
||||
type = "stcp"
|
||||
secretKey = "${sshSecretKey}"
|
||||
localPort = 22
|
||||
` : '';
|
||||
const ttyBlock = IS_WINDOWS ? '' : `
|
||||
[[proxies]]
|
||||
name = "tty-${clawId}"
|
||||
type = "tcp"
|
||||
localPort = ${TTYD_PORT}
|
||||
remotePort = ${ttyRemotePort}
|
||||
`;
|
||||
const toml = `# 由 clawd 自动生成,请勿手动修改
|
||||
serverAddr = "frp.claw.cutos.ai"
|
||||
serverPort = 443
|
||||
@@ -173,16 +197,10 @@ name = "dashboard-${clawId}"
|
||||
type = "http"
|
||||
localPort = ${dashboard_local_port}
|
||||
subdomain = "${clawId}"
|
||||
|
||||
[[proxies]]
|
||||
name = "tty-${clawId}"
|
||||
type = "tcp"
|
||||
localPort = ${TTYD_PORT}
|
||||
remotePort = ${ttyRemotePort}
|
||||
${stcpBlock}`;
|
||||
${ttyBlock}${stcpBlock}`;
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
|
||||
log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}${sshSecretKey ? ', ssh stcp=enabled' : ''}`);
|
||||
log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}${IS_WINDOWS ? '' : `, tty tcp-port=${ttyRemotePort}`}${stcpBlock ? ', ssh stcp=enabled' : ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,11 @@ const log = require('./logger');
|
||||
const { isRK3566, isRK3588, readDeviceModel } = require('./led/detect');
|
||||
|
||||
function loadImpl() {
|
||||
if (process.platform === 'win32') {
|
||||
log.info('led', 'LED/VFD backend -> terminal (Windows/x86)');
|
||||
return require('./led/terminal');
|
||||
}
|
||||
|
||||
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
||||
const model = readDeviceModel();
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('../logger');
|
||||
|
||||
function box(lines) {
|
||||
const width = Math.max(...lines.map((line) => line.length), 28);
|
||||
const top = `+${'-'.repeat(width + 2)}+`;
|
||||
const body = lines.map((line) => `| ${line.padEnd(width)} |`);
|
||||
return ['', top, ...body, top, ''].join('\n');
|
||||
}
|
||||
|
||||
class BasicLed {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this._current = null;
|
||||
}
|
||||
on() { this._set('on'); }
|
||||
off() { this._set('off'); }
|
||||
blink() { this._set('blink'); }
|
||||
destroy() { this._set('off'); }
|
||||
_set(next) {
|
||||
if (this._current === next) return;
|
||||
this._current = next;
|
||||
log.debug('led', `[terminal] ${this.name} ${next}`);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusLed {
|
||||
setSetup() { log.info('status', 'SETUP / waiting for activation'); }
|
||||
setApps() { log.info('status', 'APPS / activated'); }
|
||||
off() { log.debug('status', 'off'); }
|
||||
}
|
||||
|
||||
class Display {
|
||||
showAP() {
|
||||
log.info('display', box([
|
||||
'Claw Daemon',
|
||||
'Waiting for network',
|
||||
'Windows/x86 has no AP mode',
|
||||
]));
|
||||
}
|
||||
|
||||
showConn() {
|
||||
log.info('display', box([
|
||||
'Claw Daemon',
|
||||
'Connecting to claw cloud...',
|
||||
]));
|
||||
}
|
||||
|
||||
showErr0() {
|
||||
log.info('display', box([
|
||||
'Claw Daemon',
|
||||
'Connection error',
|
||||
]));
|
||||
}
|
||||
|
||||
showTime() {
|
||||
log.info('display', box([
|
||||
'Claw Daemon',
|
||||
'Running on Windows/x86',
|
||||
]));
|
||||
}
|
||||
|
||||
showPin(pin) {
|
||||
const s = String(pin || '').trim();
|
||||
log.info('display', box([
|
||||
'Claw Daemon Activation',
|
||||
`PIN: ${s}`,
|
||||
'Enter this PIN in the web console',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class LanLed {
|
||||
start() { log.debug('led', '[terminal] LAN monitor ignored'); }
|
||||
stop() { log.debug('led', '[terminal] LAN monitor stopped'); }
|
||||
}
|
||||
|
||||
const led = new BasicLed('network');
|
||||
led.bt = new BasicLed('bt');
|
||||
led.status = new StatusLed();
|
||||
led.display = new Display();
|
||||
led.lan = new LanLed();
|
||||
|
||||
module.exports = led;
|
||||
+2
-3
@@ -2,12 +2,11 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { getConfigDir } = require('./platform-paths');
|
||||
|
||||
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
|
||||
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
||||
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
||||
const CONFIG_DIR = getConfigDir();
|
||||
|
||||
const LOG_DIR = process.env.CLAWD_LOG_DIR || path.join(CONFIG_DIR, 'logs');
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
+82
-38
@@ -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 → 立即 false(nmcli 有缓存,不可信)
|
||||
@@ -143,8 +154,9 @@ function hasInternet() {
|
||||
* 获取默认 WiFi 接口名(wlan0 等)。
|
||||
* 必须 TYPE 精确为 wifi,不能用 grep wifi(会误匹配 wifi-p2p,导致选到 p2p-dev-wlan0,STA/热点均失败)。
|
||||
*/
|
||||
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 });
|
||||
|
||||
@@ -5,9 +5,10 @@ const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { exec } = require('child_process');
|
||||
const log = require('./logger');
|
||||
const { resolveOpenclawConfigFile } = require('./frpc');
|
||||
const { exec } = require('child_process');
|
||||
const log = require('./logger');
|
||||
const { resolveOpenclawConfigFile } = require('./frpc');
|
||||
const { IS_WINDOWS } = require('./platform-paths');
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://api.cutos.ai/v1';
|
||||
const FETCH_TIMEOUT_MS = 10_000;
|
||||
@@ -89,8 +90,21 @@ function writeJsonFile(filePath, obj) {
|
||||
* 每次写盘 openclaw.json 成功后应调用一次。
|
||||
* 使用异步 exec,不阻塞 Node.js 事件循环,避免干扰 LED / VFD 等后续操作。
|
||||
*/
|
||||
function restartGateway() {
|
||||
exec('pkill -9 -x openclaw-gateway', (err) => {
|
||||
function restartGateway() {
|
||||
if (IS_WINDOWS) {
|
||||
exec('taskkill /IM openclaw-gateway.exe /F', (err) => {
|
||||
if (err && err.code !== 128 && err.code !== 1) {
|
||||
log.warn('openclaw-provider', `restartGateway: ${err.message}`);
|
||||
} else if (!err) {
|
||||
log.info('openclaw-provider', 'openclaw-gateway.exe stopped; waiting for supervisor to restart it');
|
||||
} else {
|
||||
log.info('openclaw-provider', 'openclaw-gateway.exe is not running');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
exec('pkill -9 -x openclaw-gateway', (err) => {
|
||||
if (err && err.code !== 1) {
|
||||
log.warn('openclaw-provider', `restartGateway: ${err.message}`);
|
||||
} else if (!err) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
function windowsProgramData() {
|
||||
return process.env.PROGRAMDATA || 'C:\\ProgramData';
|
||||
}
|
||||
|
||||
function getConfigDir() {
|
||||
if (process.env.CLAWD_CONFIG_DIR) return process.env.CLAWD_CONFIG_DIR;
|
||||
if (IS_WINDOWS) return path.join(windowsProgramData(), 'OpenClaw', 'clawd');
|
||||
return process.getuid && process.getuid() === 0
|
||||
? '/etc/clawd'
|
||||
: path.join(os.homedir(), '.clawd');
|
||||
}
|
||||
|
||||
function getPersistentFile(name) {
|
||||
return path.join(getConfigDir(), name);
|
||||
}
|
||||
|
||||
function getOpenClawConfigCandidates() {
|
||||
if (IS_WINDOWS) {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
||||
path.join(windowsProgramData(), 'OpenClaw', 'openclaw.json'),
|
||||
path.join(windowsProgramData(), 'OpenClaw', 'config', 'openclaw.json'),
|
||||
];
|
||||
if (process.env.OPENCLAW_CONFIG) candidates.unshift(process.env.OPENCLAW_CONFIG);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
return [
|
||||
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
|
||||
'/home/sts/.openclaw/openclaw.json',
|
||||
'/root/.openclaw/openclaw.json',
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
IS_WINDOWS,
|
||||
getConfigDir,
|
||||
getPersistentFile,
|
||||
getOpenClawConfigCandidates,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const { hasInternet, hasWiredInternetProbe, hasSavedWifiConnection, connectSaved
|
||||
const { DnsHijack } = require('./dns-hijack');
|
||||
const { CaptiveServer } = require('./captive-server');
|
||||
const led = require('./led');
|
||||
const { IS_WINDOWS } = require('./platform-paths');
|
||||
|
||||
const MONITOR_INTERVAL_MS = 15_000;
|
||||
const WIFI_RECONNECT_MAX_ROUNDS = 3;
|
||||
@@ -40,6 +41,16 @@ class ProvisionManager extends EventEmitter {
|
||||
isApMode() { return this._state === 'ap'; }
|
||||
|
||||
async start() {
|
||||
if (IS_WINDOWS) {
|
||||
led.off();
|
||||
this._state = hasInternet() ? 'wired' : 'idle';
|
||||
log.info('provision', 'Windows/x86 mode: AP provisioning is disabled; waiting for existing network');
|
||||
if (this._state === 'wired') this._emitNetworkReady();
|
||||
else led.display.showAP();
|
||||
this._startMonitor();
|
||||
return;
|
||||
}
|
||||
|
||||
led.off(); // WiFi 灯初始状态:熄灭
|
||||
|
||||
// WiFi STA 已连接 → 直接进入 STA 模式
|
||||
@@ -228,6 +239,19 @@ class ProvisionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
async _monitorTick() {
|
||||
if (IS_WINDOWS) {
|
||||
if (hasInternet()) {
|
||||
if (this._state !== 'wired') {
|
||||
this._state = 'wired';
|
||||
this._emitNetworkReady();
|
||||
}
|
||||
} else if (this._state !== 'idle') {
|
||||
this._state = 'idle';
|
||||
led.display.showAP();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === 'connecting') return;
|
||||
|
||||
const wifiUp = isWifiStaConnected();
|
||||
|
||||
Reference in New Issue
Block a user