Files
clawd/lib/provisioning.js
stswangzhiping b42e59fab8 feat: AP always-on mode - hotspot stays until WiFi STA connects
Redesign provisioning from one-shot blocking to persistent background manager:
- AP hotspot starts at boot regardless of eth0 status
- Captive portal runs alongside AP for WiFi configuration
- AP automatically shuts down only when WiFi STA connects
- WiFi drops at runtime -> AP auto-restarts
- WiFi connect fails -> AP auto-restarts for retry
- client.js no longer blocks on network; connects WS when ready

Made-with: Cursor
2026-03-16 12:18:35 +08:00

148 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.
'use strict';
const EventEmitter = require('events');
const log = require('./logger');
const { hasInternet, isWifiStaConnected, startAP, stopAP, connectWifi, AP_IP } = require('./network');
const { DnsHijack } = require('./dns-hijack');
const { CaptiveServer } = require('./captive-server');
const MONITOR_INTERVAL_MS = 30_000;
/**
* AP 常驻配网管理器。
*
* 规则:
* - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP
* - 运行中 WiFi 断开 → 自动重新开 AP
* - WiFi 已连接 → AP 关闭
*/
class ProvisionManager extends EventEmitter {
constructor(clawId) {
super();
this._clawId = clawId || 'Setup';
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta'
this._dns = null;
this._server = null;
this._monitorTimer = null;
}
start() {
if (isWifiStaConnected()) {
this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动');
} else {
this._enterAP();
}
this._startMonitor();
if (hasInternet()) {
this.emit('network-ready');
}
}
stop() {
this._stopMonitor();
this._stopAll();
this._state = 'idle';
}
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
_enterAP() {
if (this._state === 'ap') return;
try {
const ap = startAP(this._clawId);
this._dns = new DnsHijack();
this._dns.start(ap.iface, AP_IP);
this._server = new CaptiveServer({
clawId: this._clawId,
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
});
this._server.startListening();
this._state = 'ap';
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
log.info('provision', `配网地址: http://ap.cutos.ai`);
} catch (e) {
log.error('provision', `AP 启动失败: ${e.message}`);
}
}
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
async _handleWifiConnect(ssid, password) {
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
this._state = 'connecting';
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
this._stopAPServices();
const result = connectWifi(ssid, password);
if (result.success) {
this._state = 'sta';
log.info('provision', `WiFi 已连接: ${ssid}`);
this.emit('network-ready');
return result;
}
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
this._enterAP();
return result;
}
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
_startMonitor() {
this._monitorTimer = setInterval(() => {
if (this._state === 'connecting') return;
const wifiUp = isWifiStaConnected();
if (this._state === 'sta' && !wifiUp) {
log.warn('provision', 'WiFi 连接已断开,重新启动 AP');
this._enterAP();
}
if (this._state === 'ap' && wifiUp) {
log.info('provision', 'WiFi 已外部连接,关闭 AP');
this._stopAPServices();
this._state = 'sta';
this.emit('network-ready');
}
}, MONITOR_INTERVAL_MS);
}
_stopMonitor() {
if (this._monitorTimer) {
clearInterval(this._monitorTimer);
this._monitorTimer = null;
}
}
// ── 清理 ──────────────────────────────────────────────────────────────────
_stopAPServices() {
if (this._server) {
this._server.stop();
this._server = null;
}
if (this._dns) {
this._dns.stop();
this._dns = null;
}
stopAP();
}
_stopAll() {
this._stopAPServices();
}
}
module.exports = { ProvisionManager };