Files
clawd/lib/client.js
2026-03-14 20:41:26 +08:00

184 lines
5.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 WebSocket = require('ws');
const config = require('./config');
const { getBoxId } = require('./fingerprint');
const { collect } = require('./metrics');
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;
}
start() {
console.log(`[clawd] 启动中...`);
console.log(`[clawd] box_id = ${this._boxId}`);
console.log(`[clawd] 服务器 = ${this._cfg.server}`);
console.log(`[clawd] 配置文件 = ${config.getConfigPath()}`);
this._connect();
}
stop() {
this._stopped = true;
this._clearHeartbeat();
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,
};
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 && 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') {
console.log('');
console.log('╔══════════════════════════════════╗');
console.log(`║ 激活 PIN 码: ${msg.pin}`);
console.log('║ 请在管理后台或前台输入此 PIN 码 ║');
console.log('╚══════════════════════════════════╝');
console.log('');
console.log('[clawd] 等待激活中,心跳正常运行...');
} else {
console.log(`[clawd] 设备已激活claw_id = ${msg.claw_id}`);
}
// 开始心跳
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 };