@ynhcj/xiaoyi 0.0.3-beta → 0.0.5-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 +8 -0
- package/dist/websocket.js +76 -6
- package/dist/xy-monitor.js +7 -14
- 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,13 +13,21 @@ 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?;
|
|
19
21
|
private stableConnectionTimer2?;
|
|
20
22
|
private static readonly STABLE_CONNECTION_THRESHOLD;
|
|
21
23
|
private activeTasks;
|
|
24
|
+
private onHealthEvent?;
|
|
22
25
|
constructor(config: XiaoYiChannelConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
28
|
+
* This callback is invoked on heartbeat success to update lastEventAt.
|
|
29
|
+
*/
|
|
30
|
+
setHealthEventCallback(callback: () => void): void;
|
|
23
31
|
/**
|
|
24
32
|
* Check if URL is wss + IP format (skip certificate verification)
|
|
25
33
|
*/
|
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) {
|
|
@@ -42,6 +43,14 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
42
43
|
console.log(` Server 1: ${this.config.wsUrl1}`);
|
|
43
44
|
console.log(` Server 2: ${this.config.wsUrl2}`);
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
48
|
+
* This callback is invoked on heartbeat success to update lastEventAt.
|
|
49
|
+
*/
|
|
50
|
+
setHealthEventCallback(callback) {
|
|
51
|
+
this.onHealthEvent = callback;
|
|
52
|
+
console.log("[WS Manager] Health event callback registered");
|
|
53
|
+
}
|
|
45
54
|
/**
|
|
46
55
|
* Check if URL is wss + IP format (skip certificate verification)
|
|
47
56
|
*/
|
|
@@ -141,9 +150,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
141
150
|
async connectToServer1() {
|
|
142
151
|
console.log(`[Server1] Connecting to ${this.config.wsUrl1}...`);
|
|
143
152
|
try {
|
|
144
|
-
// ✅ Close existing connection before creating new one
|
|
153
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
145
154
|
if (this.ws1) {
|
|
146
155
|
console.log(`[Server1] Closing existing connection before reconnect`);
|
|
156
|
+
if (this.heartbeat1) {
|
|
157
|
+
this.heartbeat1.stop();
|
|
158
|
+
this.heartbeat1 = undefined;
|
|
159
|
+
}
|
|
147
160
|
try {
|
|
148
161
|
this.ws1.removeAllListeners();
|
|
149
162
|
this.ws1.close();
|
|
@@ -163,6 +176,26 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
163
176
|
headers: authHeaders,
|
|
164
177
|
rejectUnauthorized: !skipCertVerify,
|
|
165
178
|
});
|
|
179
|
+
// ✅ Initialize HeartbeatManager for server1
|
|
180
|
+
this.heartbeat1 = new heartbeat_js_1.HeartbeatManager(this.ws1, {
|
|
181
|
+
interval: 30000, // 30 seconds
|
|
182
|
+
timeout: 10000, // 10 seconds timeout
|
|
183
|
+
message: JSON.stringify({
|
|
184
|
+
msgType: "heartbeat",
|
|
185
|
+
agentId: this.config.agentId,
|
|
186
|
+
timestamp: Date.now(),
|
|
187
|
+
}),
|
|
188
|
+
}, () => {
|
|
189
|
+
console.log(`[Server1] Heartbeat timeout, reconnecting...`);
|
|
190
|
+
if (this.ws1 && (this.ws1.readyState === ws_1.default.OPEN || this.ws1.readyState === ws_1.default.CONNECTING)) {
|
|
191
|
+
this.ws1.close();
|
|
192
|
+
}
|
|
193
|
+
}, "server1", console.log, console.error, () => {
|
|
194
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
195
|
+
this.emit("heartbeat", "server1");
|
|
196
|
+
// ✅ Report liveness to OpenClaw framework to prevent stale-socket detection
|
|
197
|
+
this.onHealthEvent?.();
|
|
198
|
+
});
|
|
166
199
|
this.setupWebSocketHandlers(this.ws1, 'server1');
|
|
167
200
|
await new Promise((resolve, reject) => {
|
|
168
201
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -183,8 +216,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
183
216
|
this.scheduleStableConnectionCheck('server1');
|
|
184
217
|
// Send init message
|
|
185
218
|
this.sendInitMessage(this.ws1, 'server1');
|
|
186
|
-
// Start
|
|
187
|
-
this.
|
|
219
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
220
|
+
this.heartbeat1.start();
|
|
221
|
+
console.log(`[Server1] Heartbeat started (30s interval, 10s timeout)`);
|
|
188
222
|
}
|
|
189
223
|
catch (error) {
|
|
190
224
|
console.error(`[Server1] Connection failed:`, error);
|
|
@@ -200,9 +234,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
200
234
|
async connectToServer2() {
|
|
201
235
|
console.log(`[Server2] Connecting to ${this.config.wsUrl2}...`);
|
|
202
236
|
try {
|
|
203
|
-
// ✅ Close existing connection before creating new one
|
|
237
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
204
238
|
if (this.ws2) {
|
|
205
239
|
console.log(`[Server2] Closing existing connection before reconnect`);
|
|
240
|
+
if (this.heartbeat2) {
|
|
241
|
+
this.heartbeat2.stop();
|
|
242
|
+
this.heartbeat2 = undefined;
|
|
243
|
+
}
|
|
206
244
|
try {
|
|
207
245
|
this.ws2.removeAllListeners();
|
|
208
246
|
this.ws2.close();
|
|
@@ -222,6 +260,26 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
222
260
|
headers: authHeaders,
|
|
223
261
|
rejectUnauthorized: !skipCertVerify,
|
|
224
262
|
});
|
|
263
|
+
// ✅ Initialize HeartbeatManager for server2
|
|
264
|
+
this.heartbeat2 = new heartbeat_js_1.HeartbeatManager(this.ws2, {
|
|
265
|
+
interval: 30000, // 30 seconds
|
|
266
|
+
timeout: 10000, // 10 seconds timeout
|
|
267
|
+
message: JSON.stringify({
|
|
268
|
+
msgType: "heartbeat",
|
|
269
|
+
agentId: this.config.agentId,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
}),
|
|
272
|
+
}, () => {
|
|
273
|
+
console.log(`[Server2] Heartbeat timeout, reconnecting...`);
|
|
274
|
+
if (this.ws2 && (this.ws2.readyState === ws_1.default.OPEN || this.ws2.readyState === ws_1.default.CONNECTING)) {
|
|
275
|
+
this.ws2.close();
|
|
276
|
+
}
|
|
277
|
+
}, "server2", console.log, console.error, () => {
|
|
278
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
279
|
+
this.emit("heartbeat", "server2");
|
|
280
|
+
// ✅ Report liveness to OpenClaw framework to prevent stale-socket detection
|
|
281
|
+
this.onHealthEvent?.();
|
|
282
|
+
});
|
|
225
283
|
this.setupWebSocketHandlers(this.ws2, 'server2');
|
|
226
284
|
await new Promise((resolve, reject) => {
|
|
227
285
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -242,8 +300,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
242
300
|
this.scheduleStableConnectionCheck('server2');
|
|
243
301
|
// Send init message
|
|
244
302
|
this.sendInitMessage(this.ws2, 'server2');
|
|
245
|
-
// Start
|
|
246
|
-
this.
|
|
303
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
304
|
+
this.heartbeat2.start();
|
|
305
|
+
console.log(`[Server2] Heartbeat started (30s interval, 10s timeout)`);
|
|
247
306
|
}
|
|
248
307
|
catch (error) {
|
|
249
308
|
console.error(`[Server2] Connection failed:`, error);
|
|
@@ -259,6 +318,17 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
259
318
|
disconnect() {
|
|
260
319
|
console.log("[WS Manager] Disconnecting from all servers...");
|
|
261
320
|
this.clearTimers();
|
|
321
|
+
// ✅ Stop heartbeat managers
|
|
322
|
+
if (this.heartbeat1) {
|
|
323
|
+
console.log("[Server1] Stopping heartbeat manager");
|
|
324
|
+
this.heartbeat1.stop();
|
|
325
|
+
this.heartbeat1 = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (this.heartbeat2) {
|
|
328
|
+
console.log("[Server2] Stopping heartbeat manager");
|
|
329
|
+
this.heartbeat2.stop();
|
|
330
|
+
this.heartbeat2 = undefined;
|
|
331
|
+
}
|
|
262
332
|
// ✅ Properly cleanup WebSocket connections to prevent ghost connections
|
|
263
333
|
if (this.ws1) {
|
|
264
334
|
try {
|
package/dist/xy-monitor.js
CHANGED
|
@@ -51,10 +51,11 @@ async function monitorXYProvider(opts = {}) {
|
|
|
51
51
|
// diagnoseAllManagers();
|
|
52
52
|
// Get WebSocket manager (cached)
|
|
53
53
|
const wsManager = (0, xy_client_js_1.getXYWebSocketManager)(account);
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// ✅ Set health event callback for heartbeat reporting
|
|
55
|
+
// This ensures OpenClaw's health-monitor sees activity and doesn't trigger stale-socket restarts
|
|
56
|
+
if (trackEvent) {
|
|
57
|
+
wsManager.setHealthEventCallback(trackEvent);
|
|
58
|
+
}
|
|
58
59
|
// Track logged servers to avoid duplicate logs
|
|
59
60
|
const loggedServers = new Set();
|
|
60
61
|
// Track active message processing to detect duplicates
|
|
@@ -125,9 +126,6 @@ async function monitorXYProvider(opts = {}) {
|
|
|
125
126
|
};
|
|
126
127
|
const cleanup = () => {
|
|
127
128
|
log("XY gateway: cleaning up...");
|
|
128
|
-
// // 🔍 Diagnose before cleanup
|
|
129
|
-
// console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
|
|
130
|
-
// diagnoseAllManagers();
|
|
131
129
|
// Stop health check interval
|
|
132
130
|
if (healthCheckInterval) {
|
|
133
131
|
clearInterval(healthCheckInterval);
|
|
@@ -139,17 +137,12 @@ async function monitorXYProvider(opts = {}) {
|
|
|
139
137
|
wsManager.off("connected", connectedHandler);
|
|
140
138
|
wsManager.off("disconnected", disconnectedHandler);
|
|
141
139
|
wsManager.off("error", errorHandler);
|
|
142
|
-
// ✅
|
|
143
|
-
//
|
|
144
|
-
wsManager.disconnect();
|
|
145
|
-
// ✅ Remove manager from cache to prevent reusing dirty state
|
|
140
|
+
// ✅ Remove manager from cache - this will also disconnect
|
|
141
|
+
// removeXYWebSocketManager internally calls manager.disconnect()
|
|
146
142
|
(0, xy_client_js_1.removeXYWebSocketManager)(account);
|
|
147
143
|
loggedServers.clear();
|
|
148
144
|
activeMessages.clear();
|
|
149
145
|
log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
|
|
150
|
-
// // 🔍 Diagnose after cleanup
|
|
151
|
-
// console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
|
|
152
|
-
// diagnoseAllManagers();
|
|
153
146
|
};
|
|
154
147
|
const handleAbort = () => {
|
|
155
148
|
log("XY gateway: abort signal received, stopping");
|