Files
clawd/lib/provisioning.js
stswangzhiping eb9f4ab1c3 feat: add WiFi provisioning for headless devices (AP + Captive Portal)
- Add lib/network.js: WiFi scan, connect, AP hotspot via nmcli
- Add lib/dns-hijack.js: dnsmasq management for DNS hijack + DHCP
- Add lib/captive-server.js: embedded HTTP captive portal with WiFi setup page
- Add lib/provisioning.js: orchestrator (detect network -> AP mode -> wait -> exit)
- Update client.js: call ensureNetwork() before WS connection
- Update install.sh: auto-install dnsmasq dependency

Made-with: Cursor
2026-03-16 08:58:51 +08:00

94 lines
2.7 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 log = require('./logger');
const { hasInternet, startAP, stopAP, AP_IP } = require('./network');
const { DnsHijack } = require('./dns-hijack');
const { CaptiveServer } = require('./captive-server');
const config = require('./config');
const MAX_RETRIES = 3; // 配网连接失败后最多重新进入 AP 模式次数
/**
* 确保设备有互联网连接。
* 已联网 → 直接返回
* 未联网 → 进入 AP 配网模式 → 等待用户配网 → 成功后返回
*
* @param {object} opts
* @param {string|number} opts.clawId - 设备 ID用于 AP SSID
* @returns {Promise<void>}
*/
async function ensureNetwork(opts = {}) {
// 先检测是否已联网
if (hasInternet()) {
log.info('provision', '网络已就绪,跳过配网');
return;
}
log.warn('provision', '未检测到网络,进入配网模式...');
const cfg = config.load();
const clawId = opts.clawId || cfg.claw_id || 'Setup';
let retries = 0;
while (retries < MAX_RETRIES) {
try {
await runProvisioningRound(clawId);
// 配网成功,再验证一次
if (hasInternet()) {
log.info('provision', '配网完成,网络已就绪');
return;
}
log.warn('provision', '配网后仍无网络,重新进入配网模式...');
} catch (e) {
log.error('provision', `配网异常: ${e.message}`);
}
retries++;
if (retries < MAX_RETRIES) {
log.info('provision', `重试配网 (${retries}/${MAX_RETRIES})...`);
// 等一会再重试,避免过快循环
await sleep(3000);
}
}
log.error('provision', `配网失败 ${MAX_RETRIES} 次,将以离线模式继续启动(等待网络恢复后重连)`);
}
/**
* 单轮配网流程:开 AP → 启动 DNS + HTTP → 等待用户配网 → 清理
*/
async function runProvisioningRound(clawId) {
const dns = new DnsHijack();
const server = new CaptiveServer({ clawId });
try {
// 1. 启动 WiFi AP
const ap = startAP(clawId);
// 2. 启动 DNS 劫持
dns.start(ap.iface, AP_IP);
// 3. 启动 HTTP 配网页面,等待用户完成配网
// server.start() 返回 Promise配网成功时 resolve
log.info('provision', '配网页面已就绪,等待用户操作...');
log.info('provision', `用户请连接 WiFi "${ap.ssid}" 并访问 http://ap.cutos.ai`);
const result = await server.start();
log.info('provision', `用户已连接 WiFi: ${result.ssid}`);
} finally {
// 清理:无论成功失败都关闭 AP / DNS / HTTP
server.stop();
dns.stop();
stopAP();
}
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
module.exports = { ensureNetwork };