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
This commit is contained in:
stswangzhiping
2026-03-16 12:18:35 +08:00
parent dac68f78b4
commit b42e59fab8
5 changed files with 207 additions and 129 deletions

View File

@@ -7,7 +7,8 @@ const log = require('./logger');
const { getBoxId } = require('./fingerprint');
const { collect } = require('./metrics');
const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc');
const { ensureNetwork } = require('./provisioning');
const { ProvisionManager } = require('./provisioning');
const { hasInternet } = require('./network');
const MAX_BACKOFF_MS = 60_000;
const PONG_TIMEOUT_MS = 15_000;
@@ -59,12 +60,27 @@ class ClawClient {
async start() {
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
// 第一步:确保网络可用(无网时进入 AP 配网模式
await ensureNetwork({ clawId: this._cfg.claw_id });
// 后台启动 AP 配网管理器WiFi 未连接时常驻热点
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
this._provisionMgr.start();
// 第二步:并行获取 dashboard 信息 + 启动 ttyd
// 有网络时直接连接云端
if (hasInternet()) {
await this._proceedWithConnection();
} else {
// 等待配网完成后再连
log.info('clawd', '等待网络就绪...');
this._provisionMgr.once('network-ready', () => {
this._proceedWithConnection().catch(e => {
log.error('clawd', '连接启动失败:', e.message);
});
});
}
}
async _proceedWithConnection() {
const [dashInfo] = await Promise.all([
getDashboardInfo(),
getDashboardInfo().catch(e => { log.warn('clawd', 'dashboard 信息获取失败:', e.message); return null; }),
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
]);
this._dashInfo = dashInfo || {};
@@ -77,6 +93,7 @@ class ClawClient {
this._clearHeartbeat();
this._clearPing();
if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; }
if (this._provisionMgr) { this._provisionMgr.stop(); this._provisionMgr = null; }
this._frpc.stop();
if (this._ws) this._ws.terminate();
this._sdNotify('STOPPING=1');