205 lines
6.7 KiB
JavaScript
205 lines
6.7 KiB
JavaScript
'use strict';
|
||
|
||
const WebSocket = require('ws');
|
||
const config = require('./config');
|
||
const { getBoxId } = require('./fingerprint');
|
||
const { collect } = require('./metrics');
|
||
const { getDashboardInfo, FrpcManager } = require('./frpc');
|
||
|
||
const MAX_BACKOFF_MS = 60_000;
|
||
|
||
class ClawClient {
|
||
constructor() {
|
||
this._cfg = config.load();
|
||
this._boxId = getBoxId();
|
||
this._ws = null;
|
||
this._hbTimer = null; // 心跳定时器
|
||
this._backoff = 1_000; // 重连等待(ms)
|
||
this._stopped = false;
|
||
this._frpc = new FrpcManager();
|
||
this._dashInfo = {}; // { dashboard_token, dashboard_port }
|
||
}
|
||
|
||
start() {
|
||
console.log(`[clawd] 启动中... 服务器 = ${this._cfg.server}`);
|
||
// 启动前提取 openclaw dashboard 信息(耗时操作放后台,不阻塞连接)
|
||
this._dashInfo = getDashboardInfo();
|
||
this._connect();
|
||
}
|
||
|
||
stop() {
|
||
this._stopped = true;
|
||
this._clearHeartbeat();
|
||
this._frpc.stop();
|
||
if (this._ws) this._ws.terminate();
|
||
console.log('[clawd] 已停止');
|
||
}
|
||
|
||
// ── 连接 ──────────────────────────────────────────────────────────────────
|
||
|
||
_connect() {
|
||
if (this._stopped) return;
|
||
|
||
console.log(`[clawd] 正在连接 ${this._cfg.server} ...`);
|
||
const ws = new WebSocket(this._cfg.server, {
|
||
handshakeTimeout: 10_000,
|
||
});
|
||
this._ws = ws;
|
||
|
||
ws.on('open', () => {
|
||
console.log('[clawd] WebSocket 已连接');
|
||
this._backoff = 1_000;
|
||
this._sendConnect();
|
||
});
|
||
|
||
ws.on('message', (data) => {
|
||
try {
|
||
this._handleMessage(JSON.parse(data.toString()));
|
||
} catch (e) {
|
||
console.error('[clawd] 消息解析失败:', e.message);
|
||
}
|
||
});
|
||
|
||
ws.on('close', (code, reason) => {
|
||
this._clearHeartbeat();
|
||
if (!this._stopped) {
|
||
console.warn(`[clawd] 连接断开 (${code}),${this._backoff / 1000}s 后重连...`);
|
||
setTimeout(() => this._connect(), this._backoff);
|
||
this._backoff = Math.min(this._backoff * 2, MAX_BACKOFF_MS);
|
||
}
|
||
});
|
||
|
||
ws.on('error', (err) => {
|
||
console.error('[clawd] 连接错误:', err.message);
|
||
// close 事件会在 error 之后触发,重连逻辑在 close 里处理
|
||
});
|
||
}
|
||
|
||
// ── 发送 connect ──────────────────────────────────────────────────────────
|
||
|
||
_sendConnect() {
|
||
const msg = {
|
||
type: 'connect',
|
||
box_id: this._boxId,
|
||
claw_id: this._cfg.claw_id ?? null,
|
||
token: this._cfg.token ?? null,
|
||
// dashboard 信息(可选,openclaw 未安装时为空)
|
||
...this._dashInfo,
|
||
};
|
||
this._send(msg);
|
||
}
|
||
|
||
// ── 消息处理 ──────────────────────────────────────────────────────────────
|
||
|
||
_handleMessage(msg) {
|
||
switch (msg.type) {
|
||
case 'connected':
|
||
this._onConnected(msg);
|
||
break;
|
||
case 'heartbeat_ack':
|
||
// 正常回包,静默处理
|
||
break;
|
||
case 'error':
|
||
console.error(`[clawd] 服务器错误: ${msg.msg}`);
|
||
if (msg.msg === 'hardware_mismatch') {
|
||
// box_id 与库中不符:硬件变更或凭证泄露
|
||
// 清空本地凭证,下次重连走全新注册流程
|
||
console.warn('[clawd] 硬件指纹与服务器不符(硬件变更或凭证泄露),清除本地凭证重新注册...');
|
||
this._cfg.claw_id = null;
|
||
this._cfg.token = null;
|
||
config.save(this._cfg);
|
||
} else if (msg.msg && msg.msg.includes('invalid')) {
|
||
console.warn('[clawd] 凭证无效,清除本地凭证并重新注册...');
|
||
this._cfg.claw_id = null;
|
||
this._cfg.token = null;
|
||
config.save(this._cfg);
|
||
}
|
||
break;
|
||
default:
|
||
console.warn('[clawd] 未知消息类型:', msg.type);
|
||
}
|
||
}
|
||
|
||
_onConnected(msg) {
|
||
const isNew = !this._cfg.claw_id;
|
||
|
||
// 保存 claw_id + token
|
||
this._cfg.claw_id = msg.claw_id;
|
||
this._cfg.token = msg.token;
|
||
config.save(this._cfg);
|
||
|
||
if (isNew) {
|
||
console.log(`[clawd] 注册成功!claw_id = ${msg.claw_id}`);
|
||
}
|
||
|
||
if (msg.status === 'inactive') {
|
||
const id = String(msg.claw_id).padEnd(6);
|
||
const pin = String(msg.pin);
|
||
console.log('');
|
||
console.log('╔════════════════════════════════════╗');
|
||
console.log(`║ Claw ID : ${id} ║`);
|
||
console.log(`║ PIN 码 : ${pin} ║`);
|
||
console.log('║ 请在网页前台「添加设备」中输入 ║');
|
||
console.log('╚════════════════════════════════════╝');
|
||
console.log('');
|
||
console.log('[clawd] 等待激活,心跳正常运行...');
|
||
} else {
|
||
console.log(`[clawd] 已激活 claw_id = ${msg.claw_id}`);
|
||
}
|
||
|
||
// 启动 frpc(如果 VPS 下发了 frp 配置)
|
||
if (msg.frp && msg.frp.server && msg.frp.auth_token) {
|
||
this._frpc.start(msg.claw_id, msg.frp).catch(e => {
|
||
console.error('[frpc] 启动失败:', e.message);
|
||
});
|
||
}
|
||
|
||
// 开始心跳
|
||
this._startHeartbeat();
|
||
}
|
||
|
||
// ── 心跳 ─────────────────────────────────────────────────────────────────
|
||
|
||
_startHeartbeat() {
|
||
this._clearHeartbeat();
|
||
const interval = (this._cfg.heartbeat_interval || 30) * 1000;
|
||
|
||
// 立即发一次
|
||
this._sendHeartbeat();
|
||
|
||
this._hbTimer = setInterval(() => this._sendHeartbeat(), interval);
|
||
}
|
||
|
||
async _sendHeartbeat() {
|
||
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
||
try {
|
||
const metrics = await collect();
|
||
this._send({
|
||
type: 'heartbeat',
|
||
claw_id: this._cfg.claw_id,
|
||
token: this._cfg.token,
|
||
metrics,
|
||
});
|
||
} catch (e) {
|
||
console.error('[clawd] 心跳发送失败:', e.message);
|
||
}
|
||
}
|
||
|
||
_clearHeartbeat() {
|
||
if (this._hbTimer) {
|
||
clearInterval(this._hbTimer);
|
||
this._hbTimer = null;
|
||
}
|
||
}
|
||
|
||
// ── 工具 ──────────────────────────────────────────────────────────────────
|
||
|
||
_send(obj) {
|
||
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
||
this._ws.send(JSON.stringify(obj));
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = { ClawClient };
|