@ynhcj/xiaoyi 0.0.3-beta → 0.0.4-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/heartbeat.d.ts +39 -0
- package/dist/heartbeat.js +102 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +64 -6
- package/dist/xy-monitor.js +2 -10
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
export interface HeartbeatConfig {
|
|
3
|
+
interval: number;
|
|
4
|
+
timeout: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Manages heartbeat for a WebSocket connection.
|
|
9
|
+
* Supports both application-level and protocol-level heartbeats.
|
|
10
|
+
*/
|
|
11
|
+
export declare class HeartbeatManager {
|
|
12
|
+
private ws;
|
|
13
|
+
private config;
|
|
14
|
+
private onTimeout;
|
|
15
|
+
private serverName;
|
|
16
|
+
private onHeartbeatSuccess?;
|
|
17
|
+
private intervalTimer;
|
|
18
|
+
private timeoutTimer;
|
|
19
|
+
private lastPongTime;
|
|
20
|
+
private log;
|
|
21
|
+
private error;
|
|
22
|
+
constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
|
|
23
|
+
/**
|
|
24
|
+
* Start heartbeat monitoring.
|
|
25
|
+
*/
|
|
26
|
+
start(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Stop heartbeat monitoring.
|
|
29
|
+
*/
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Send a heartbeat ping.
|
|
33
|
+
*/
|
|
34
|
+
private sendHeartbeat;
|
|
35
|
+
/**
|
|
36
|
+
* Check if connection is healthy based on last pong time.
|
|
37
|
+
*/
|
|
38
|
+
isHealthy(): boolean;
|
|
39
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HeartbeatManager = void 0;
|
|
7
|
+
// Heartbeat management for WebSocket connections
|
|
8
|
+
const ws_1 = __importDefault(require("ws"));
|
|
9
|
+
/**
|
|
10
|
+
* Manages heartbeat for a WebSocket connection.
|
|
11
|
+
* Supports both application-level and protocol-level heartbeats.
|
|
12
|
+
*/
|
|
13
|
+
class HeartbeatManager {
|
|
14
|
+
constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // ✅ 心跳成功回调,向 OpenClaw 报告健康状态
|
|
15
|
+
) {
|
|
16
|
+
this.ws = ws;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.onTimeout = onTimeout;
|
|
19
|
+
this.serverName = serverName;
|
|
20
|
+
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
21
|
+
this.intervalTimer = null;
|
|
22
|
+
this.timeoutTimer = null;
|
|
23
|
+
this.lastPongTime = 0;
|
|
24
|
+
this.log = logFn ?? console.log;
|
|
25
|
+
this.error = errorFn ?? console.error;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start heartbeat monitoring.
|
|
29
|
+
*/
|
|
30
|
+
start() {
|
|
31
|
+
this.stop(); // Clear any existing timers
|
|
32
|
+
this.lastPongTime = Date.now();
|
|
33
|
+
// Setup ping/pong for protocol-level heartbeat
|
|
34
|
+
this.ws.on("pong", () => {
|
|
35
|
+
this.lastPongTime = Date.now();
|
|
36
|
+
if (this.timeoutTimer) {
|
|
37
|
+
clearTimeout(this.timeoutTimer);
|
|
38
|
+
this.timeoutTimer = null;
|
|
39
|
+
}
|
|
40
|
+
// ✅ Report health: heartbeat successful - notify OpenClaw framework
|
|
41
|
+
this.onHeartbeatSuccess?.();
|
|
42
|
+
this.log(`[${this.serverName}] Heartbeat pong received, health reported to OpenClaw`);
|
|
43
|
+
});
|
|
44
|
+
// Start interval timer
|
|
45
|
+
this.intervalTimer = setInterval(() => {
|
|
46
|
+
this.sendHeartbeat();
|
|
47
|
+
}, this.config.interval);
|
|
48
|
+
this.log(`[${this.serverName}] Heartbeat started: interval=${this.config.interval}ms, timeout=${this.config.timeout}ms`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Stop heartbeat monitoring.
|
|
52
|
+
*/
|
|
53
|
+
stop() {
|
|
54
|
+
if (this.intervalTimer) {
|
|
55
|
+
clearInterval(this.intervalTimer);
|
|
56
|
+
this.intervalTimer = null;
|
|
57
|
+
}
|
|
58
|
+
if (this.timeoutTimer) {
|
|
59
|
+
clearTimeout(this.timeoutTimer);
|
|
60
|
+
this.timeoutTimer = null;
|
|
61
|
+
}
|
|
62
|
+
// Remove pong listener
|
|
63
|
+
this.ws.off("pong", () => { });
|
|
64
|
+
this.log(`[${this.serverName}] Heartbeat stopped`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Send a heartbeat ping.
|
|
68
|
+
*/
|
|
69
|
+
sendHeartbeat() {
|
|
70
|
+
if (this.ws.readyState !== ws_1.default.OPEN) {
|
|
71
|
+
console.warn(`[${this.serverName}] Cannot send heartbeat: WebSocket not open`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
// Send application-level heartbeat message
|
|
76
|
+
this.log(`[${this.serverName}] Sending heartbeat frame:`, this.config.message.substring(0, 100));
|
|
77
|
+
this.ws.send(this.config.message);
|
|
78
|
+
// Send protocol-level ping
|
|
79
|
+
this.ws.ping();
|
|
80
|
+
this.log(`[${this.serverName}] Protocol-level ping sent`);
|
|
81
|
+
// Setup timeout timer
|
|
82
|
+
this.timeoutTimer = setTimeout(() => {
|
|
83
|
+
this.error(`[${this.serverName}] Heartbeat timeout - no pong received`);
|
|
84
|
+
this.onTimeout();
|
|
85
|
+
}, this.config.timeout);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.error(`[${this.serverName}] Failed to send heartbeat:`, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if connection is healthy based on last pong time.
|
|
93
|
+
*/
|
|
94
|
+
isHealthy() {
|
|
95
|
+
if (this.lastPongTime === 0) {
|
|
96
|
+
return true; // Not started yet
|
|
97
|
+
}
|
|
98
|
+
const timeSinceLastPong = Date.now() - this.lastPongTime;
|
|
99
|
+
return timeSinceLastPong < this.config.interval + this.config.timeout;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.HeartbeatManager = HeartbeatManager;
|
package/dist/websocket.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
|
|
|
13
13
|
private heartbeatTimeout1?;
|
|
14
14
|
private heartbeatTimeout2?;
|
|
15
15
|
private appHeartbeatInterval?;
|
|
16
|
+
private heartbeat1?;
|
|
17
|
+
private heartbeat2?;
|
|
16
18
|
private reconnectTimeout1?;
|
|
17
19
|
private reconnectTimeout2?;
|
|
18
20
|
private stableConnectionTimer1?;
|
package/dist/websocket.js
CHANGED
|
@@ -8,6 +8,7 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
8
8
|
const events_1 = require("events");
|
|
9
9
|
const url_1 = require("url");
|
|
10
10
|
const auth_js_1 = require("./auth.js");
|
|
11
|
+
const heartbeat_js_1 = require("./heartbeat.js");
|
|
11
12
|
const types_js_1 = require("./types.js");
|
|
12
13
|
class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
13
14
|
constructor(config) {
|
|
@@ -141,9 +142,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
141
142
|
async connectToServer1() {
|
|
142
143
|
console.log(`[Server1] Connecting to ${this.config.wsUrl1}...`);
|
|
143
144
|
try {
|
|
144
|
-
// ✅ Close existing connection before creating new one
|
|
145
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
145
146
|
if (this.ws1) {
|
|
146
147
|
console.log(`[Server1] Closing existing connection before reconnect`);
|
|
148
|
+
if (this.heartbeat1) {
|
|
149
|
+
this.heartbeat1.stop();
|
|
150
|
+
this.heartbeat1 = undefined;
|
|
151
|
+
}
|
|
147
152
|
try {
|
|
148
153
|
this.ws1.removeAllListeners();
|
|
149
154
|
this.ws1.close();
|
|
@@ -163,6 +168,24 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
163
168
|
headers: authHeaders,
|
|
164
169
|
rejectUnauthorized: !skipCertVerify,
|
|
165
170
|
});
|
|
171
|
+
// ✅ Initialize HeartbeatManager for server1
|
|
172
|
+
this.heartbeat1 = new heartbeat_js_1.HeartbeatManager(this.ws1, {
|
|
173
|
+
interval: 30000, // 30 seconds
|
|
174
|
+
timeout: 10000, // 10 seconds timeout
|
|
175
|
+
message: JSON.stringify({
|
|
176
|
+
msgType: "heartbeat",
|
|
177
|
+
agentId: this.config.agentId,
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
}),
|
|
180
|
+
}, () => {
|
|
181
|
+
console.log(`[Server1] Heartbeat timeout, reconnecting...`);
|
|
182
|
+
if (this.ws1 && (this.ws1.readyState === ws_1.default.OPEN || this.ws1.readyState === ws_1.default.CONNECTING)) {
|
|
183
|
+
this.ws1.close();
|
|
184
|
+
}
|
|
185
|
+
}, "server1", console.log, console.error, () => {
|
|
186
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
187
|
+
this.emit("heartbeat", "server1");
|
|
188
|
+
});
|
|
166
189
|
this.setupWebSocketHandlers(this.ws1, 'server1');
|
|
167
190
|
await new Promise((resolve, reject) => {
|
|
168
191
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -183,8 +206,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
183
206
|
this.scheduleStableConnectionCheck('server1');
|
|
184
207
|
// Send init message
|
|
185
208
|
this.sendInitMessage(this.ws1, 'server1');
|
|
186
|
-
// Start
|
|
187
|
-
this.
|
|
209
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
210
|
+
this.heartbeat1.start();
|
|
211
|
+
console.log(`[Server1] Heartbeat started (30s interval, 10s timeout)`);
|
|
188
212
|
}
|
|
189
213
|
catch (error) {
|
|
190
214
|
console.error(`[Server1] Connection failed:`, error);
|
|
@@ -200,9 +224,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
200
224
|
async connectToServer2() {
|
|
201
225
|
console.log(`[Server2] Connecting to ${this.config.wsUrl2}...`);
|
|
202
226
|
try {
|
|
203
|
-
// ✅ Close existing connection before creating new one
|
|
227
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
204
228
|
if (this.ws2) {
|
|
205
229
|
console.log(`[Server2] Closing existing connection before reconnect`);
|
|
230
|
+
if (this.heartbeat2) {
|
|
231
|
+
this.heartbeat2.stop();
|
|
232
|
+
this.heartbeat2 = undefined;
|
|
233
|
+
}
|
|
206
234
|
try {
|
|
207
235
|
this.ws2.removeAllListeners();
|
|
208
236
|
this.ws2.close();
|
|
@@ -222,6 +250,24 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
222
250
|
headers: authHeaders,
|
|
223
251
|
rejectUnauthorized: !skipCertVerify,
|
|
224
252
|
});
|
|
253
|
+
// ✅ Initialize HeartbeatManager for server2
|
|
254
|
+
this.heartbeat2 = new heartbeat_js_1.HeartbeatManager(this.ws2, {
|
|
255
|
+
interval: 30000, // 30 seconds
|
|
256
|
+
timeout: 10000, // 10 seconds timeout
|
|
257
|
+
message: JSON.stringify({
|
|
258
|
+
msgType: "heartbeat",
|
|
259
|
+
agentId: this.config.agentId,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
}),
|
|
262
|
+
}, () => {
|
|
263
|
+
console.log(`[Server2] Heartbeat timeout, reconnecting...`);
|
|
264
|
+
if (this.ws2 && (this.ws2.readyState === ws_1.default.OPEN || this.ws2.readyState === ws_1.default.CONNECTING)) {
|
|
265
|
+
this.ws2.close();
|
|
266
|
+
}
|
|
267
|
+
}, "server2", console.log, console.error, () => {
|
|
268
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
269
|
+
this.emit("heartbeat", "server2");
|
|
270
|
+
});
|
|
225
271
|
this.setupWebSocketHandlers(this.ws2, 'server2');
|
|
226
272
|
await new Promise((resolve, reject) => {
|
|
227
273
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -242,8 +288,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
242
288
|
this.scheduleStableConnectionCheck('server2');
|
|
243
289
|
// Send init message
|
|
244
290
|
this.sendInitMessage(this.ws2, 'server2');
|
|
245
|
-
// Start
|
|
246
|
-
this.
|
|
291
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
292
|
+
this.heartbeat2.start();
|
|
293
|
+
console.log(`[Server2] Heartbeat started (30s interval, 10s timeout)`);
|
|
247
294
|
}
|
|
248
295
|
catch (error) {
|
|
249
296
|
console.error(`[Server2] Connection failed:`, error);
|
|
@@ -259,6 +306,17 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
259
306
|
disconnect() {
|
|
260
307
|
console.log("[WS Manager] Disconnecting from all servers...");
|
|
261
308
|
this.clearTimers();
|
|
309
|
+
// ✅ Stop heartbeat managers
|
|
310
|
+
if (this.heartbeat1) {
|
|
311
|
+
console.log("[Server1] Stopping heartbeat manager");
|
|
312
|
+
this.heartbeat1.stop();
|
|
313
|
+
this.heartbeat1 = undefined;
|
|
314
|
+
}
|
|
315
|
+
if (this.heartbeat2) {
|
|
316
|
+
console.log("[Server2] Stopping heartbeat manager");
|
|
317
|
+
this.heartbeat2.stop();
|
|
318
|
+
this.heartbeat2 = undefined;
|
|
319
|
+
}
|
|
262
320
|
// ✅ Properly cleanup WebSocket connections to prevent ghost connections
|
|
263
321
|
if (this.ws1) {
|
|
264
322
|
try {
|
package/dist/xy-monitor.js
CHANGED
|
@@ -125,9 +125,6 @@ async function monitorXYProvider(opts = {}) {
|
|
|
125
125
|
};
|
|
126
126
|
const cleanup = () => {
|
|
127
127
|
log("XY gateway: cleaning up...");
|
|
128
|
-
// // 🔍 Diagnose before cleanup
|
|
129
|
-
// console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
130
|
-
// diagnoseAllManagers();
|
|
131
128
|
// Stop health check interval
|
|
132
129
|
if (healthCheckInterval) {
|
|
133
130
|
clearInterval(healthCheckInterval);
|
|
@@ -139,17 +136,12 @@ async function monitorXYProvider(opts = {}) {
|
|
|
139
136
|
wsManager.off("connected", connectedHandler);
|
|
140
137
|
wsManager.off("disconnected", disconnectedHandler);
|
|
141
138
|
wsManager.off("error", errorHandler);
|
|
142
|
-
// ✅
|
|
143
|
-
//
|
|
144
|
-
wsManager.disconnect();
|
|
145
|
-
// ✅ Remove manager from cache to prevent reusing dirty state
|
|
139
|
+
// ✅ Remove manager from cache - this will also disconnect
|
|
140
|
+
// removeXYWebSocketManager internally calls manager.disconnect()
|
|
146
141
|
(0, xy_client_js_1.removeXYWebSocketManager)(account);
|
|
147
142
|
loggedServers.clear();
|
|
148
143
|
activeMessages.clear();
|
|
149
144
|
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
|
|
150
|
-
// // 🔍 Diagnose after cleanup
|
|
151
|
-
// console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
152
|
-
// diagnoseAllManagers();
|
|
153
145
|
};
|
|
154
146
|
const handleAbort = () => {
|
|
155
147
|
log("XY gateway: abort signal received, stopping");
|