@ynhcj/xiaoyi 2.2.1 → 2.2.2

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/channel.js CHANGED
@@ -50,6 +50,8 @@ exports.xiaoyiPlugin = {
50
50
  config: {
51
51
  enabled: false,
52
52
  wsUrl: "",
53
+ wsUrl1: "",
54
+ wsUrl2: "",
53
55
  ak: "",
54
56
  sk: "",
55
57
  agentId: "",
@@ -78,7 +80,10 @@ exports.xiaoyiPlugin = {
78
80
  }
79
81
  const config = account.config;
80
82
  // Check each field is a string and has content after trimming
81
- const hasWsUrl = typeof config.wsUrl === 'string' && config.wsUrl.trim().length > 0;
83
+ // Support both old wsUrl and new wsUrl1/wsUrl2
84
+ const hasWsUrl = ((typeof config.wsUrl === 'string' && config.wsUrl.trim().length > 0) ||
85
+ (typeof config.wsUrl1 === 'string' && config.wsUrl1.trim().length > 0) ||
86
+ (typeof config.wsUrl2 === 'string' && config.wsUrl2.trim().length > 0));
82
87
  const hasAk = typeof config.ak === 'string' && config.ak.trim().length > 0;
83
88
  const hasSk = typeof config.sk === 'string' && config.sk.trim().length > 0;
84
89
  const hasAgentId = typeof config.agentId === 'string' && config.agentId.trim().length > 0;
@@ -91,13 +96,14 @@ exports.xiaoyiPlugin = {
91
96
  return "Channel is disabled in configuration";
92
97
  },
93
98
  unconfiguredReason: (account, cfg) => {
94
- return "Missing required configuration: wsUrl, ak, sk, or agentId";
99
+ return "Missing required configuration: wsUrl/wsUrl1/wsUrl2, ak, sk, or agentId";
95
100
  },
96
101
  describeAccount: (account, cfg) => ({
97
102
  accountId: account.accountId,
98
103
  name: 'XiaoYi',
99
104
  enabled: account.enabled,
100
- configured: Boolean(account.config?.wsUrl && account.config?.ak && account.config?.sk && account.config?.agentId),
105
+ configured: Boolean((account.config?.wsUrl || account.config?.wsUrl1 || account.config?.wsUrl2) &&
106
+ account.config?.ak && account.config?.sk && account.config?.agentId),
101
107
  }),
102
108
  },
103
109
  /**
package/dist/index.d.ts CHANGED
@@ -3,20 +3,24 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  * XiaoYi Channel Plugin for OpenClaw
4
4
  *
5
5
  * This plugin enables integration with XiaoYi's A2A protocol via WebSocket.
6
- * Single account mode only.
6
+ * Supports dual server mode for high availability.
7
7
  *
8
8
  * Configuration example in openclaw.json:
9
9
  * {
10
10
  * "channels": {
11
11
  * "xiaoyi": {
12
12
  * "enabled": true,
13
- * "wsUrl": "ws://localhost:8765/ws/link",
13
+ * "wsUrl1": "ws://localhost:8765/ws/link",
14
+ * "wsUrl2": "ws://localhost:8766/ws/link",
14
15
  * "ak": "test_ak",
15
16
  * "sk": "test_sk",
16
- * "agentId": "your-agent-id"
17
+ * "agentId": "your-agent-id",
18
+ * "enableStreaming": true
17
19
  * }
18
20
  * }
19
21
  * }
22
+ *
23
+ * Backward compatibility: Can use "wsUrl" instead of "wsUrl1" (wsUrl2 will use default)
20
24
  */
21
25
  declare const plugin: {
22
26
  id: string;
package/dist/index.js CHANGED
@@ -7,20 +7,24 @@ const runtime_1 = require("./runtime");
7
7
  * XiaoYi Channel Plugin for OpenClaw
8
8
  *
9
9
  * This plugin enables integration with XiaoYi's A2A protocol via WebSocket.
10
- * Single account mode only.
10
+ * Supports dual server mode for high availability.
11
11
  *
12
12
  * Configuration example in openclaw.json:
13
13
  * {
14
14
  * "channels": {
15
15
  * "xiaoyi": {
16
16
  * "enabled": true,
17
- * "wsUrl": "ws://localhost:8765/ws/link",
17
+ * "wsUrl1": "ws://localhost:8765/ws/link",
18
+ * "wsUrl2": "ws://localhost:8766/ws/link",
18
19
  * "ak": "test_ak",
19
20
  * "sk": "test_sk",
20
- * "agentId": "your-agent-id"
21
+ * "agentId": "your-agent-id",
22
+ * "enableStreaming": true
21
23
  * }
22
24
  * }
23
25
  * }
26
+ *
27
+ * Backward compatibility: Can use "wsUrl" instead of "wsUrl1" (wsUrl2 will use default)
24
28
  */
25
29
  const plugin = {
26
30
  id: "xiaoyi",
package/dist/types.d.ts CHANGED
@@ -141,7 +141,9 @@ export interface A2ATasksCancelMessage {
141
141
  }
142
142
  export interface XiaoYiChannelConfig {
143
143
  enabled: boolean;
144
- wsUrl: string;
144
+ wsUrl?: string;
145
+ wsUrl1?: string;
146
+ wsUrl2?: string;
145
147
  ak: string;
146
148
  sk: string;
147
149
  agentId: string;
@@ -161,3 +163,20 @@ export interface WebSocketConnectionState {
161
163
  reconnectAttempts: number;
162
164
  maxReconnectAttempts: number;
163
165
  }
166
+ export declare const DEFAULT_WS_URL_1 = "ws://localhost:8080/ws";
167
+ export declare const DEFAULT_WS_URL_2 = "ws://localhost:8081/ws";
168
+ export interface InternalWebSocketConfig {
169
+ wsUrl1: string;
170
+ wsUrl2: string;
171
+ agentId: string;
172
+ ak: string;
173
+ sk: string;
174
+ enableStreaming?: boolean;
175
+ }
176
+ export type ServerId = 'server1' | 'server2';
177
+ export interface ServerConnectionState {
178
+ connected: boolean;
179
+ ready: boolean;
180
+ lastHeartbeat: number;
181
+ reconnectAttempts: number;
182
+ }
package/dist/types.js CHANGED
@@ -2,3 +2,7 @@
2
2
  // A2A Message Structure Types
3
3
  // Based on: https://developer.huawei.com/consumer/cn/doc/service/message-stream-0000002505761434
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.DEFAULT_WS_URL_2 = exports.DEFAULT_WS_URL_1 = void 0;
6
+ // Dual server configuration
7
+ exports.DEFAULT_WS_URL_1 = "ws://localhost:8080/ws";
8
+ exports.DEFAULT_WS_URL_2 = "ws://localhost:8081/ws";
@@ -1,113 +1,119 @@
1
1
  import { EventEmitter } from "events";
2
- import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig } from "./types";
2
+ import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig, ServerId, ServerConnectionState } from "./types";
3
3
  export declare class XiaoYiWebSocketManager extends EventEmitter {
4
- private ws;
4
+ private ws1;
5
+ private ws2;
6
+ private state1;
7
+ private state2;
8
+ private sessionServerMap;
5
9
  private auth;
6
10
  private config;
7
- private state;
8
- private protocolHeartbeatInterval;
9
- private appHeartbeatInterval;
10
- private reconnectTimeout;
11
+ private heartbeatTimeout1?;
12
+ private heartbeatTimeout2?;
13
+ private appHeartbeatInterval?;
14
+ private reconnectTimeout1?;
15
+ private reconnectTimeout2?;
11
16
  private activeTasks;
12
17
  constructor(config: XiaoYiChannelConfig);
13
18
  /**
14
- * Connect to XiaoYi WebSocket server with header authentication
19
+ * Resolve configuration with defaults and backward compatibility
15
20
  */
16
- connect(): Promise<void>;
21
+ private resolveConfig;
17
22
  /**
18
- * Disconnect from WebSocket server
23
+ * Connect to both WebSocket servers
19
24
  */
20
- disconnect(): void;
25
+ connect(): Promise<void>;
21
26
  /**
22
- * Send clawd_bot_init message on connection/reconnection
27
+ * Connect to server 1
23
28
  */
24
- private sendInitMessage;
29
+ private connectToServer1;
25
30
  /**
26
- * Send A2A response message (converts to JSON-RPC 2.0 format)
27
- * This method is for regular agent responses only
28
- * @param response - The response message
29
- * @param taskId - The task ID
30
- * @param sessionId - The session ID
31
- * @param isFinal - Whether this is the final frame (default: true)
32
- * @param append - Whether to append to previous content (default: false for complete content)
31
+ * Connect to server 2
33
32
  */
34
- sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string, isFinal?: boolean, append?: boolean): Promise<void>;
33
+ private connectToServer2;
35
34
  /**
36
- * Send A2A clear context response (uses specific clear context format)
37
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
35
+ * Disconnect from all servers
38
36
  */
39
- sendClearContextResponse(requestId: string, sessionId: string, success?: boolean): Promise<void>;
37
+ disconnect(): void;
40
38
  /**
41
- * Send A2A tasks cancel response (uses specific cancel format)
42
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
39
+ * Send init message to specific server
43
40
  */
44
- sendTasksCancelResponse(requestId: string, sessionId: string, success?: boolean): Promise<void>;
41
+ private sendInitMessage;
45
42
  /**
46
- * Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
47
- * @param response - The response message
48
- * @param taskId - The task ID
49
- * @param isFinal - Whether this is the final frame (default: true)
50
- * @param append - Whether to append to previous content (default: false)
43
+ * Setup WebSocket event handlers for specific server
51
44
  */
52
- private convertToJsonRpcFormat;
45
+ private setupWebSocketHandlers;
53
46
  /**
54
- * Send generic outbound message
47
+ * Handle incoming message from specific server
55
48
  */
56
- private sendMessage;
49
+ private handleIncomingMessage;
57
50
  /**
58
- * Check if connection is ready for sending messages
51
+ * Send A2A response message with automatic routing
59
52
  */
60
- isReady(): boolean;
53
+ sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string, isFinal?: boolean, append?: boolean): Promise<void>;
61
54
  /**
62
- * Get current connection state
55
+ * Send clear context response to specific server
63
56
  */
64
- getState(): WebSocketConnectionState;
57
+ sendClearContextResponse(requestId: string, sessionId: string, success?: boolean, targetServer?: ServerId): Promise<void>;
65
58
  /**
66
- * Setup WebSocket event handlers
59
+ * Send tasks cancel response to specific server
67
60
  */
68
- private setupWebSocketHandlers;
61
+ sendTasksCancelResponse(requestId: string, sessionId: string, success?: boolean, targetServer?: ServerId): Promise<void>;
69
62
  /**
70
- * Handle incoming WebSocket messages
63
+ * Handle clearContext method
71
64
  */
72
- private handleMessage;
65
+ private handleClearContext;
73
66
  /**
74
- * Handle A2A clear message
75
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
67
+ * Handle clear message (legacy format)
76
68
  */
77
69
  private handleClearMessage;
78
70
  /**
79
- * Handle A2A tasks/cancel message
80
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
81
- *
82
- * Simplified implementation similar to clearContext:
83
- * 1. Send success response immediately
84
- * 2. Emit cancel event for application to handle
71
+ * Handle tasks/cancel message
85
72
  */
86
73
  private handleTasksCancelMessage;
87
74
  /**
88
- * Send tasks/cancel success response
75
+ * Convert A2AResponseMessage to JSON-RPC 2.0 format
89
76
  */
90
- sendCancelSuccessResponse(sessionId: string, taskId: string, requestId: string): Promise<void>;
77
+ private convertToJsonRpcFormat;
91
78
  /**
92
- * Type guard for A2A request messages (JSON-RPC 2.0 format)
79
+ * Check if at least one server is ready
93
80
  */
94
- private isA2ARequestMessage;
81
+ isReady(): boolean;
82
+ /**
83
+ * Get combined connection state
84
+ */
85
+ getState(): WebSocketConnectionState;
95
86
  /**
96
- * Start protocol-level heartbeat (ping/pong)
87
+ * Get individual server states
88
+ */
89
+ getServerStates(): {
90
+ server1: ServerConnectionState;
91
+ server2: ServerConnectionState;
92
+ };
93
+ /**
94
+ * Start protocol-level heartbeat for specific server
97
95
  */
98
96
  private startProtocolHeartbeat;
99
97
  /**
100
- * Start application-level heartbeat
98
+ * Clear protocol heartbeat for specific server
99
+ */
100
+ private clearProtocolHeartbeat;
101
+ /**
102
+ * Start application-level heartbeat (shared across both servers)
101
103
  */
102
104
  private startAppHeartbeat;
103
105
  /**
104
- * Schedule reconnection attempt with exponential backoff
106
+ * Schedule reconnection for specific server
105
107
  */
106
108
  private scheduleReconnect;
107
109
  /**
108
110
  * Clear all timers
109
111
  */
110
112
  private clearTimers;
113
+ /**
114
+ * Type guard for A2A request messages
115
+ */
116
+ private isA2ARequestMessage;
111
117
  /**
112
118
  * Get active tasks
113
119
  */
@@ -116,4 +122,12 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
116
122
  * Remove task from active tasks
117
123
  */
118
124
  removeActiveTask(taskId: string): void;
125
+ /**
126
+ * Get server for a specific session
127
+ */
128
+ getServerForSession(sessionId: string): ServerId | undefined;
129
+ /**
130
+ * Remove session mapping
131
+ */
132
+ removeSession(sessionId: string): void;
119
133
  }
package/dist/websocket.js CHANGED
@@ -7,102 +7,315 @@ exports.XiaoYiWebSocketManager = void 0;
7
7
  const ws_1 = __importDefault(require("ws"));
8
8
  const events_1 = require("events");
9
9
  const auth_1 = require("./auth");
10
+ const types_1 = require("./types");
10
11
  class XiaoYiWebSocketManager extends events_1.EventEmitter {
11
12
  constructor(config) {
12
13
  super();
13
- this.ws = null;
14
- this.protocolHeartbeatInterval = null;
15
- this.appHeartbeatInterval = null;
16
- this.reconnectTimeout = null;
17
- this.activeTasks = new Map(); // Track active tasks for cancellation
18
- this.config = config;
19
- this.auth = new auth_1.XiaoYiAuth(config.ak, config.sk, config.agentId);
20
- this.state = {
14
+ // ==================== Dual WebSocket Connections ====================
15
+ this.ws1 = null;
16
+ this.ws2 = null;
17
+ // ==================== Dual Server States ====================
18
+ this.state1 = {
21
19
  connected: false,
22
- authenticated: false,
20
+ ready: false,
23
21
  lastHeartbeat: 0,
24
- lastAppHeartbeat: 0,
25
- reconnectAttempts: 0,
26
- maxReconnectAttempts: 50, // Increased from 10 to 50
22
+ reconnectAttempts: 0
23
+ };
24
+ this.state2 = {
25
+ connected: false,
26
+ ready: false,
27
+ lastHeartbeat: 0,
28
+ reconnectAttempts: 0
27
29
  };
30
+ // ==================== Session → Server Mapping ====================
31
+ this.sessionServerMap = new Map();
32
+ // ==================== Active Tasks ====================
33
+ this.activeTasks = new Map();
34
+ // Resolve configuration with defaults and backward compatibility
35
+ this.config = this.resolveConfig(config);
36
+ this.auth = new auth_1.XiaoYiAuth(this.config.ak, this.config.sk, this.config.agentId);
37
+ console.log(`[WS Manager] Initialized with dual server:`);
38
+ console.log(` Server 1: ${this.config.wsUrl1}`);
39
+ console.log(` Server 2: ${this.config.wsUrl2}`);
28
40
  }
29
41
  /**
30
- * Connect to XiaoYi WebSocket server with header authentication
42
+ * Resolve configuration with defaults and backward compatibility
43
+ */
44
+ resolveConfig(userConfig) {
45
+ // Backward compatibility: if wsUrl is provided but wsUrl1/wsUrl2 are not,
46
+ // use wsUrl for server1 and default for server2
47
+ let wsUrl1 = userConfig.wsUrl1;
48
+ let wsUrl2 = userConfig.wsUrl2;
49
+ if (!wsUrl1 && userConfig.wsUrl) {
50
+ wsUrl1 = userConfig.wsUrl;
51
+ }
52
+ // Apply defaults if not provided
53
+ if (!wsUrl1) {
54
+ console.warn(`[WS Manager] wsUrl1 not provided, using default: ${types_1.DEFAULT_WS_URL_1}`);
55
+ wsUrl1 = types_1.DEFAULT_WS_URL_1;
56
+ }
57
+ if (!wsUrl2) {
58
+ console.warn(`[WS Manager] wsUrl2 not provided, using default: ${types_1.DEFAULT_WS_URL_2}`);
59
+ wsUrl2 = types_1.DEFAULT_WS_URL_2;
60
+ }
61
+ return {
62
+ wsUrl1,
63
+ wsUrl2,
64
+ agentId: userConfig.agentId,
65
+ ak: userConfig.ak,
66
+ sk: userConfig.sk,
67
+ enableStreaming: userConfig.enableStreaming,
68
+ };
69
+ }
70
+ /**
71
+ * Connect to both WebSocket servers
31
72
  */
32
73
  async connect() {
33
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
34
- return;
74
+ console.log("[WS Manager] Connecting to both servers...");
75
+ const results = await Promise.allSettled([
76
+ this.connectToServer1(),
77
+ this.connectToServer2(),
78
+ ]);
79
+ // Check if at least one connection succeeded
80
+ const server1Success = results[0].status === 'fulfilled';
81
+ const server2Success = results[1].status === 'fulfilled';
82
+ if (!server1Success && !server2Success) {
83
+ console.error("[WS Manager] Failed to connect to both servers");
84
+ throw new Error("Failed to connect to both servers");
85
+ }
86
+ console.log(`[WS Manager] Connection results: Server1=${server1Success}, Server2=${server2Success}`);
87
+ // Start application-level heartbeat (only if at least one connection is ready)
88
+ if (this.state1.connected || this.state2.connected) {
89
+ this.startAppHeartbeat();
90
+ }
91
+ }
92
+ /**
93
+ * Connect to server 1
94
+ */
95
+ async connectToServer1() {
96
+ console.log(`[Server1] Connecting to ${this.config.wsUrl1}...`);
97
+ try {
98
+ const authHeaders = this.auth.generateAuthHeaders();
99
+ this.ws1 = new ws_1.default(this.config.wsUrl1, {
100
+ headers: authHeaders,
101
+ });
102
+ this.setupWebSocketHandlers(this.ws1, 'server1');
103
+ await new Promise((resolve, reject) => {
104
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
105
+ this.ws1.once("open", () => {
106
+ clearTimeout(timeout);
107
+ resolve();
108
+ });
109
+ this.ws1.once("error", (error) => {
110
+ clearTimeout(timeout);
111
+ reject(error);
112
+ });
113
+ });
114
+ this.state1.connected = true;
115
+ this.state1.ready = true;
116
+ this.state1.reconnectAttempts = 0;
117
+ console.log(`[Server1] Connected successfully`);
118
+ this.emit("connected", "server1");
119
+ // Send init message
120
+ this.sendInitMessage(this.ws1, 'server1');
121
+ // Start protocol heartbeat
122
+ this.startProtocolHeartbeat('server1');
123
+ }
124
+ catch (error) {
125
+ console.error(`[Server1] Connection failed:`, error);
126
+ this.state1.connected = false;
127
+ this.state1.ready = false;
128
+ this.emit("error", { serverId: 'server1', error });
129
+ throw error;
35
130
  }
131
+ }
132
+ /**
133
+ * Connect to server 2
134
+ */
135
+ async connectToServer2() {
136
+ console.log(`[Server2] Connecting to ${this.config.wsUrl2}...`);
36
137
  try {
37
- // Generate authentication headers
38
138
  const authHeaders = this.auth.generateAuthHeaders();
39
- // Create WebSocket connection with headers
40
- this.ws = new ws_1.default(this.config.wsUrl, {
139
+ this.ws2 = new ws_1.default(this.config.wsUrl2, {
41
140
  headers: authHeaders,
42
141
  });
43
- this.setupWebSocketHandlers();
44
- return new Promise((resolve, reject) => {
45
- this.ws.once("open", () => {
46
- this.state.connected = true;
47
- this.state.authenticated = true; // Authenticated via headers
48
- this.state.reconnectAttempts = 0;
49
- this.emit("connected");
50
- // Send clawd_bot_init message
51
- this.sendInitMessage();
52
- // Start heartbeats
53
- this.startProtocolHeartbeat();
54
- this.startAppHeartbeat();
142
+ this.setupWebSocketHandlers(this.ws2, 'server2');
143
+ await new Promise((resolve, reject) => {
144
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
145
+ this.ws2.once("open", () => {
146
+ clearTimeout(timeout);
55
147
  resolve();
56
148
  });
57
- this.ws.once("error", (error) => {
149
+ this.ws2.once("error", (error) => {
150
+ clearTimeout(timeout);
58
151
  reject(error);
59
152
  });
60
153
  });
154
+ this.state2.connected = true;
155
+ this.state2.ready = true;
156
+ this.state2.reconnectAttempts = 0;
157
+ console.log(`[Server2] Connected successfully`);
158
+ this.emit("connected", "server2");
159
+ // Send init message
160
+ this.sendInitMessage(this.ws2, 'server2');
161
+ // Start protocol heartbeat
162
+ this.startProtocolHeartbeat('server2');
61
163
  }
62
164
  catch (error) {
63
- this.emit("error", error);
165
+ console.error(`[Server2] Connection failed:`, error);
166
+ this.state2.connected = false;
167
+ this.state2.ready = false;
168
+ this.emit("error", { serverId: 'server2', error });
64
169
  throw error;
65
170
  }
66
171
  }
67
172
  /**
68
- * Disconnect from WebSocket server
173
+ * Disconnect from all servers
69
174
  */
70
175
  disconnect() {
176
+ console.log("[WS Manager] Disconnecting from all servers...");
71
177
  this.clearTimers();
72
- if (this.ws) {
73
- this.ws.close();
74
- this.ws = null;
178
+ if (this.ws1) {
179
+ this.ws1.close();
180
+ this.ws1 = null;
75
181
  }
76
- this.state.connected = false;
77
- this.state.authenticated = false;
182
+ if (this.ws2) {
183
+ this.ws2.close();
184
+ this.ws2 = null;
185
+ }
186
+ this.state1.connected = false;
187
+ this.state1.ready = false;
188
+ this.state2.connected = false;
189
+ this.state2.ready = false;
190
+ this.sessionServerMap.clear();
78
191
  this.activeTasks.clear();
79
192
  this.emit("disconnected");
80
193
  }
81
194
  /**
82
- * Send clawd_bot_init message on connection/reconnection
195
+ * Send init message to specific server
83
196
  */
84
- sendInitMessage() {
197
+ sendInitMessage(ws, serverId) {
85
198
  const initMessage = {
86
199
  msgType: "clawd_bot_init",
87
200
  agentId: this.config.agentId,
88
201
  };
89
- this.sendMessage(initMessage);
90
- console.log("Sent clawd_bot_init message");
202
+ try {
203
+ ws.send(JSON.stringify(initMessage));
204
+ console.log(`[${serverId}] Sent clawd_bot_init message`);
205
+ }
206
+ catch (error) {
207
+ console.error(`[${serverId}] Failed to send init message:`, error);
208
+ }
91
209
  }
92
210
  /**
93
- * Send A2A response message (converts to JSON-RPC 2.0 format)
94
- * This method is for regular agent responses only
95
- * @param response - The response message
96
- * @param taskId - The task ID
97
- * @param sessionId - The session ID
98
- * @param isFinal - Whether this is the final frame (default: true)
99
- * @param append - Whether to append to previous content (default: false for complete content)
211
+ * Setup WebSocket event handlers for specific server
212
+ */
213
+ setupWebSocketHandlers(ws, serverId) {
214
+ ws.on("open", () => {
215
+ console.log(`[${serverId}] WebSocket opened`);
216
+ });
217
+ ws.on("message", (data) => {
218
+ this.handleIncomingMessage(data, serverId);
219
+ });
220
+ ws.on("close", (code, reason) => {
221
+ console.log(`[${serverId}] WebSocket closed: ${code} ${reason.toString()}`);
222
+ if (serverId === 'server1') {
223
+ this.state1.connected = false;
224
+ this.state1.ready = false;
225
+ this.clearProtocolHeartbeat('server1');
226
+ }
227
+ else {
228
+ this.state2.connected = false;
229
+ this.state2.ready = false;
230
+ this.clearProtocolHeartbeat('server2');
231
+ }
232
+ this.emit("disconnected", serverId);
233
+ this.scheduleReconnect(serverId);
234
+ });
235
+ ws.on("error", (error) => {
236
+ console.error(`[${serverId}] WebSocket error:`, error);
237
+ this.emit("error", { serverId, error });
238
+ });
239
+ ws.on("pong", () => {
240
+ if (serverId === 'server1') {
241
+ this.state1.lastHeartbeat = Date.now();
242
+ }
243
+ else {
244
+ this.state2.lastHeartbeat = Date.now();
245
+ }
246
+ });
247
+ }
248
+ /**
249
+ * Handle incoming message from specific server
250
+ */
251
+ handleIncomingMessage(data, sourceServer) {
252
+ try {
253
+ const message = JSON.parse(data.toString());
254
+ // Log received message
255
+ console.log("\n" + "=".repeat(80));
256
+ console.log(`[${sourceServer}] Received message:`);
257
+ console.log(JSON.stringify(message, null, 2));
258
+ console.log("=".repeat(80) + "\n");
259
+ // Validate agentId
260
+ if (message.agentId && message.agentId !== this.config.agentId) {
261
+ console.warn(`[${sourceServer}] Mismatched agentId: ${message.agentId}, expected: ${this.config.agentId}. Discarding.`);
262
+ return;
263
+ }
264
+ // Record session → server mapping
265
+ if (message.sessionId) {
266
+ this.sessionServerMap.set(message.sessionId, sourceServer);
267
+ console.log(`[MAP] Session ${message.sessionId} -> ${sourceServer}`);
268
+ }
269
+ // Handle special messages (clearContext, tasks/cancel)
270
+ if (message.method === "clearContext") {
271
+ this.handleClearContext(message, sourceServer);
272
+ return;
273
+ }
274
+ if (message.action === "clear") {
275
+ this.handleClearMessage(message, sourceServer);
276
+ return;
277
+ }
278
+ if (message.method === "tasks/cancel" || message.action === "tasks/cancel") {
279
+ this.handleTasksCancelMessage(message, sourceServer);
280
+ return;
281
+ }
282
+ // Handle regular A2A request
283
+ if (this.isA2ARequestMessage(message)) {
284
+ // Store task for potential cancellation
285
+ this.activeTasks.set(message.id, {
286
+ sessionId: message.sessionId,
287
+ timestamp: Date.now(),
288
+ });
289
+ // Emit with server info
290
+ this.emit("message", message);
291
+ }
292
+ else {
293
+ console.warn(`[${sourceServer}] Unknown message format`);
294
+ }
295
+ }
296
+ catch (error) {
297
+ console.error(`[${sourceServer}] Failed to parse message:`, error);
298
+ this.emit("error", { serverId: sourceServer, error });
299
+ }
300
+ }
301
+ /**
302
+ * Send A2A response message with automatic routing
100
303
  */
101
304
  async sendResponse(response, taskId, sessionId, isFinal = true, append = false) {
102
- if (!this.isReady()) {
103
- throw new Error("WebSocket not ready");
305
+ // Find which server this session belongs to
306
+ const targetServer = this.sessionServerMap.get(sessionId);
307
+ if (!targetServer) {
308
+ console.error(`[ROUTE] Unknown server for session ${sessionId}`);
309
+ throw new Error(`Cannot route response: unknown session ${sessionId}`);
310
+ }
311
+ // Get the corresponding WebSocket connection
312
+ const ws = targetServer === 'server1' ? this.ws1 : this.ws2;
313
+ const state = targetServer === 'server1' ? this.state1 : this.state2;
314
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
315
+ console.error(`[ROUTE] ${targetServer} not connected for session ${sessionId}`);
316
+ throw new Error(`${targetServer} is not available`);
104
317
  }
105
- // Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
318
+ // Convert to JSON-RPC format
106
319
  const jsonRpcResponse = this.convertToJsonRpcFormat(response, taskId, isFinal, append);
107
320
  const message = {
108
321
  msgType: "agent_response",
@@ -111,15 +324,28 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
111
324
  taskId: taskId,
112
325
  msgDetail: JSON.stringify(jsonRpcResponse),
113
326
  };
114
- this.sendMessage(message);
327
+ try {
328
+ ws.send(JSON.stringify(message));
329
+ console.log(`[ROUTE] Response sent to ${targetServer} for session ${sessionId} (isFinal=${isFinal}, append=${append})`);
330
+ }
331
+ catch (error) {
332
+ console.error(`[ROUTE] Failed to send to ${targetServer}:`, error);
333
+ throw error;
334
+ }
115
335
  }
116
336
  /**
117
- * Send A2A clear context response (uses specific clear context format)
118
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
337
+ * Send clear context response to specific server
119
338
  */
120
- async sendClearContextResponse(requestId, sessionId, success = true) {
121
- if (!this.isReady()) {
122
- throw new Error("WebSocket not ready");
339
+ async sendClearContextResponse(requestId, sessionId, success = true, targetServer) {
340
+ const serverId = targetServer || this.sessionServerMap.get(sessionId);
341
+ if (!serverId) {
342
+ console.error(`[CLEAR] Unknown server for session ${sessionId}`);
343
+ throw new Error(`Cannot send clear response: unknown session ${sessionId}`);
344
+ }
345
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
346
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
347
+ console.error(`[CLEAR] ${serverId} not connected`);
348
+ throw new Error(`${serverId} is not available`);
123
349
  }
124
350
  const jsonRpcResponse = {
125
351
  jsonrpc: "2.0",
@@ -129,13 +355,6 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
129
355
  state: success ? "cleared" : "failed"
130
356
  }
131
357
  },
132
- error: success ? {
133
- code: 0,
134
- message: ""
135
- } : {
136
- code: -1,
137
- message: "Failed to clear context"
138
- },
139
358
  };
140
359
  const message = {
141
360
  msgType: "agent_response",
@@ -144,39 +363,41 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
144
363
  taskId: requestId,
145
364
  msgDetail: JSON.stringify(jsonRpcResponse),
146
365
  };
147
- console.log("\n[CLEAR] Sending clearContext response:");
148
- console.log(` msgType: ${message.msgType}`);
149
- console.log(` agentId: ${message.agentId}`);
150
- console.log(` sessionId: ${message.sessionId}`);
151
- console.log(` taskId: ${message.taskId}`);
152
- console.log(` msgDetail: ${message.msgDetail}`);
153
- console.log("");
154
- this.sendMessage(message);
366
+ console.log(`\n[CLEAR] Sending clearContext response to ${serverId}:`);
367
+ console.log(` sessionId: ${sessionId}`);
368
+ console.log(` requestId: ${requestId}`);
369
+ console.log(` success: ${success}\n`);
370
+ try {
371
+ ws.send(JSON.stringify(message));
372
+ }
373
+ catch (error) {
374
+ console.error(`[CLEAR] Failed to send to ${serverId}:`, error);
375
+ throw error;
376
+ }
155
377
  }
156
378
  /**
157
- * Send A2A tasks cancel response (uses specific cancel format)
158
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
379
+ * Send tasks cancel response to specific server
159
380
  */
160
- async sendTasksCancelResponse(requestId, sessionId, success = true) {
161
- if (!this.isReady()) {
162
- throw new Error("WebSocket not ready");
381
+ async sendTasksCancelResponse(requestId, sessionId, success = true, targetServer) {
382
+ const serverId = targetServer || this.sessionServerMap.get(sessionId);
383
+ if (!serverId) {
384
+ console.error(`[CANCEL] Unknown server for session ${sessionId}`);
385
+ throw new Error(`Cannot send cancel response: unknown session ${sessionId}`);
386
+ }
387
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
388
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
389
+ console.error(`[CANCEL] ${serverId} not connected`);
390
+ throw new Error(`${serverId} is not available`);
163
391
  }
164
392
  const jsonRpcResponse = {
165
393
  jsonrpc: "2.0",
166
394
  id: requestId,
167
395
  result: {
168
- id: requestId, // 使用请求中的该字段返回
396
+ id: requestId,
169
397
  status: {
170
398
  state: success ? "canceled" : "failed"
171
399
  }
172
400
  },
173
- error: success ? {
174
- code: 0,
175
- message: ""
176
- } : {
177
- code: -1,
178
- message: "Failed to cancel task"
179
- },
180
401
  };
181
402
  const message = {
182
403
  msgType: "agent_response",
@@ -185,19 +406,68 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
185
406
  taskId: requestId,
186
407
  msgDetail: JSON.stringify(jsonRpcResponse),
187
408
  };
188
- this.sendMessage(message);
409
+ try {
410
+ ws.send(JSON.stringify(message));
411
+ }
412
+ catch (error) {
413
+ console.error(`[CANCEL] Failed to send to ${serverId}:`, error);
414
+ throw error;
415
+ }
189
416
  }
190
417
  /**
191
- * Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
192
- * @param response - The response message
193
- * @param taskId - The task ID
194
- * @param isFinal - Whether this is the final frame (default: true)
195
- * @param append - Whether to append to previous content (default: false)
418
+ * Handle clearContext method
419
+ */
420
+ handleClearContext(message, sourceServer) {
421
+ console.log(`[${sourceServer}] Received clearContext for session: ${message.sessionId}`);
422
+ this.sendClearContextResponse(message.id, message.sessionId, true, sourceServer)
423
+ .catch(error => console.error(`[${sourceServer}] Failed to send clearContext response:`, error));
424
+ this.emit("clear", {
425
+ sessionId: message.sessionId,
426
+ id: message.id,
427
+ serverId: sourceServer,
428
+ });
429
+ // Remove session mapping
430
+ this.sessionServerMap.delete(message.sessionId);
431
+ }
432
+ /**
433
+ * Handle clear message (legacy format)
434
+ */
435
+ handleClearMessage(message, sourceServer) {
436
+ console.log(`[${sourceServer}] Received clear message for session: ${message.sessionId}`);
437
+ this.sendClearContextResponse(message.id, message.sessionId, true, sourceServer)
438
+ .catch(error => console.error(`[${sourceServer}] Failed to send clear response:`, error));
439
+ this.emit("clear", {
440
+ sessionId: message.sessionId,
441
+ id: message.id,
442
+ serverId: sourceServer,
443
+ });
444
+ this.sessionServerMap.delete(message.sessionId);
445
+ }
446
+ /**
447
+ * Handle tasks/cancel message
448
+ */
449
+ handleTasksCancelMessage(message, sourceServer) {
450
+ const effectiveTaskId = message.taskId || message.id;
451
+ console.log("\n" + "=".repeat(60));
452
+ console.log(`[${sourceServer}] Received cancel request`);
453
+ console.log(` Session: ${message.sessionId}`);
454
+ console.log(` Task ID: ${effectiveTaskId}`);
455
+ console.log("=".repeat(60) + "\n");
456
+ this.sendTasksCancelResponse(message.id, message.sessionId, true, sourceServer)
457
+ .catch(error => console.error(`[${sourceServer}] Failed to send cancel response:`, error));
458
+ this.emit("cancel", {
459
+ sessionId: message.sessionId,
460
+ taskId: effectiveTaskId,
461
+ id: message.id,
462
+ serverId: sourceServer,
463
+ });
464
+ this.activeTasks.delete(effectiveTaskId);
465
+ }
466
+ /**
467
+ * Convert A2AResponseMessage to JSON-RPC 2.0 format
196
468
  */
197
469
  convertToJsonRpcFormat(response, taskId, isFinal = true, append = false) {
198
- // Generate artifact ID
199
470
  const artifactId = `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
200
- // Check if there's an error
201
471
  if (response.status === "error" && response.error) {
202
472
  return {
203
473
  jsonrpc: "2.0",
@@ -208,7 +478,6 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
208
478
  },
209
479
  };
210
480
  }
211
- // Convert content to artifact parts
212
481
  const parts = [];
213
482
  if (response.content.type === "text" && response.content.text) {
214
483
  parts.push({
@@ -226,13 +495,12 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
226
495
  },
227
496
  });
228
497
  }
229
- // Create TaskArtifactUpdateEvent with configurable flags
230
498
  const artifactEvent = {
231
499
  taskId: taskId,
232
500
  kind: "artifact-update",
233
- append: append, // Controls whether to append or replace content
234
- lastChunk: isFinal, // Set based on isFinal parameter
235
- final: isFinal, // Set based on isFinal parameter
501
+ append: append,
502
+ lastChunk: isFinal,
503
+ final: isFinal,
236
504
  artifact: {
237
505
  artifactId: artifactId,
238
506
  parts: parts,
@@ -245,266 +513,179 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
245
513
  };
246
514
  }
247
515
  /**
248
- * Send generic outbound message
249
- */
250
- sendMessage(message) {
251
- if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
252
- console.error("Cannot send message: WebSocket not open");
253
- return;
254
- }
255
- try {
256
- this.ws.send(JSON.stringify(message));
257
- }
258
- catch (error) {
259
- console.error("Failed to send message:", error);
260
- this.emit("error", error);
261
- }
262
- }
263
- /**
264
- * Check if connection is ready for sending messages
516
+ * Check if at least one server is ready
265
517
  */
266
518
  isReady() {
267
- return !!(this.state.connected && this.state.authenticated &&
268
- this.ws && this.ws.readyState === ws_1.default.OPEN);
519
+ return (this.state1.ready && this.ws1?.readyState === ws_1.default.OPEN) ||
520
+ (this.state2.ready && this.ws2?.readyState === ws_1.default.OPEN);
269
521
  }
270
522
  /**
271
- * Get current connection state
523
+ * Get combined connection state
272
524
  */
273
525
  getState() {
274
- return { ...this.state };
526
+ const connected = this.state1.connected || this.state2.connected;
527
+ const authenticated = connected; // Auth via headers
528
+ return {
529
+ connected,
530
+ authenticated,
531
+ lastHeartbeat: Math.max(this.state1.lastHeartbeat, this.state2.lastHeartbeat),
532
+ lastAppHeartbeat: 0,
533
+ reconnectAttempts: Math.max(this.state1.reconnectAttempts, this.state2.reconnectAttempts),
534
+ maxReconnectAttempts: 50,
535
+ };
275
536
  }
276
537
  /**
277
- * Setup WebSocket event handlers
538
+ * Get individual server states
278
539
  */
279
- setupWebSocketHandlers() {
280
- if (!this.ws)
281
- return;
282
- this.ws.on("open", () => {
283
- console.log("XiaoYi WebSocket connected");
284
- });
285
- this.ws.on("message", (data) => {
286
- try {
287
- const message = JSON.parse(data.toString());
288
- this.handleMessage(message);
289
- }
290
- catch (error) {
291
- console.error("Failed to parse WebSocket message:", error);
292
- this.emit("error", error);
293
- }
294
- });
295
- this.ws.on("close", (code, reason) => {
296
- console.log(`XiaoYi WebSocket closed: ${code} ${reason.toString()}`);
297
- this.state.connected = false;
298
- this.state.authenticated = false;
299
- this.clearTimers();
300
- this.emit("disconnected");
301
- this.scheduleReconnect();
302
- });
303
- this.ws.on("error", (error) => {
304
- console.error("XiaoYi WebSocket error:", error);
305
- this.emit("error", error);
306
- });
307
- this.ws.on("pong", () => {
308
- this.state.lastHeartbeat = Date.now();
309
- });
540
+ getServerStates() {
541
+ return {
542
+ server1: { ...this.state1 },
543
+ server2: { ...this.state2 },
544
+ };
310
545
  }
311
546
  /**
312
- * Handle incoming WebSocket messages
547
+ * Start protocol-level heartbeat for specific server
313
548
  */
314
- handleMessage(message) {
315
- // 打印完整的接收消息
316
- console.log("\n" + "=".repeat(80));
317
- console.log(`[XiaoYi WS] Received message from server:`);
318
- console.log(JSON.stringify(message, null, 2));
319
- console.log("=".repeat(80) + "\n");
320
- // Validate agentId
321
- if (message.agentId && message.agentId !== this.config.agentId) {
322
- console.warn(`Received message with mismatched agentId: ${message.agentId}, expected: ${this.config.agentId}. Discarding.`);
323
- return;
324
- }
325
- // Handle JSON-RPC 2.0 clearContext method (直接响应,不走 OpenClaw)
326
- // Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
327
- if (message.method === "clearContext") {
328
- console.log(`[CLEAR] Received clearContext for session: ${message.sessionId}`);
329
- // 直接返回成功响应
330
- this.sendClearContextResponse(message.id, message.sessionId, true).catch(error => {
331
- console.error("Failed to send clearContext response:", error);
332
- });
333
- // 可选:通知应用清除会话上下文
334
- this.emit("clear", {
335
- sessionId: message.sessionId,
336
- id: message.id,
337
- });
338
- return;
339
- }
340
- // Check if it's a clear message (兼容旧格式)
341
- if (message.action === "clear") {
342
- this.handleClearMessage(message);
343
- return;
344
- }
345
- // Check if it's a tasks/cancel message (支持 method 和 action 两种格式)
346
- if (message.method === "tasks/cancel" || message.action === "tasks/cancel") {
347
- this.handleTasksCancelMessage(message);
348
- return;
349
- }
350
- // Handle regular A2A request message
351
- if (this.isA2ARequestMessage(message)) {
352
- // Store task for potential cancellation
353
- this.activeTasks.set(message.id, {
354
- sessionId: message.sessionId,
355
- timestamp: Date.now(),
356
- });
357
- this.emit("message", message);
549
+ startProtocolHeartbeat(serverId) {
550
+ const interval = setInterval(() => {
551
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
552
+ const state = serverId === 'server1' ? this.state1 : this.state2;
553
+ if (ws && ws.readyState === ws_1.default.OPEN) {
554
+ ws.ping();
555
+ const now = Date.now();
556
+ if (state.lastHeartbeat > 0 && now - state.lastHeartbeat > 90000) {
557
+ console.warn(`[${serverId}] Heartbeat timeout, reconnecting...`);
558
+ ws.close();
559
+ }
560
+ }
561
+ }, 30000);
562
+ if (serverId === 'server1') {
563
+ this.heartbeatTimeout1 = interval;
358
564
  }
359
565
  else {
360
- console.warn("Received unknown message format:", message);
566
+ this.heartbeatTimeout2 = interval;
361
567
  }
362
568
  }
363
569
  /**
364
- * Handle A2A clear message
365
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
366
- */
367
- handleClearMessage(message) {
368
- console.log(`Received clear message for session: ${message.sessionId}`);
369
- // Send success response according to A2A spec using the correct format
370
- this.sendClearContextResponse(message.id, message.sessionId, true).catch(error => {
371
- console.error("Failed to send clear response:", error);
372
- });
373
- // Emit clear event for application to handle
374
- this.emit("clear", {
375
- sessionId: message.sessionId,
376
- id: message.id,
377
- });
378
- }
379
- /**
380
- * Handle A2A tasks/cancel message
381
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
382
- *
383
- * Simplified implementation similar to clearContext:
384
- * 1. Send success response immediately
385
- * 2. Emit cancel event for application to handle
386
- */
387
- handleTasksCancelMessage(message) {
388
- // Use taskId if available, otherwise use id as the task identifier
389
- const effectiveTaskId = message.taskId || message.id;
390
- console.log(`\n============================================================`);
391
- console.log(`XiaoYi: [CANCEL] Received cancel request`);
392
- console.log(` Session: ${message.sessionId}`);
393
- console.log(` Task ID: ${effectiveTaskId}`);
394
- console.log(` Message ID: ${message.id}`);
395
- console.log(`===========================================================\n`);
396
- // Send success response immediately (similar to clearContext)
397
- this.sendTasksCancelResponse(message.id, message.sessionId, true).catch(error => {
398
- console.error("Failed to send tasks/cancel response:", error);
399
- });
400
- // Emit cancel event for application to handle
401
- // The application can decide how to handle the cancellation
402
- this.emit("cancel", {
403
- sessionId: message.sessionId,
404
- taskId: effectiveTaskId,
405
- id: message.id,
406
- });
407
- // Remove from active tasks
408
- this.activeTasks.delete(effectiveTaskId);
409
- }
410
- /**
411
- * Send tasks/cancel success response
412
- */
413
- async sendCancelSuccessResponse(sessionId, taskId, requestId) {
414
- // Use the dedicated tasks cancel response method with correct format
415
- await this.sendTasksCancelResponse(requestId, sessionId, true);
416
- // Remove from active tasks
417
- this.activeTasks.delete(taskId);
418
- }
419
- /**
420
- * Type guard for A2A request messages (JSON-RPC 2.0 format)
570
+ * Clear protocol heartbeat for specific server
421
571
  */
422
- isA2ARequestMessage(data) {
423
- return data &&
424
- typeof data.agentId === "string" &&
425
- typeof data.sessionId === "string" &&
426
- data.jsonrpc === "2.0" &&
427
- typeof data.id === "string" &&
428
- data.method === "message/stream" &&
429
- data.params &&
430
- typeof data.params.id === "string" &&
431
- typeof data.params.sessionId === "string" &&
432
- data.params.message &&
433
- typeof data.params.message.role === "string" &&
434
- Array.isArray(data.params.message.parts);
435
- }
436
- /**
437
- * Start protocol-level heartbeat (ping/pong)
438
- */
439
- startProtocolHeartbeat() {
440
- this.protocolHeartbeatInterval = setInterval(() => {
441
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
442
- this.ws.ping();
443
- // Check if we haven't received a pong in too long
444
- const now = Date.now();
445
- if (this.state.lastHeartbeat > 0 && now - this.state.lastHeartbeat > 90000) {
446
- console.warn("Protocol heartbeat timeout, reconnecting...");
447
- this.disconnect();
448
- this.scheduleReconnect();
449
- }
572
+ clearProtocolHeartbeat(serverId) {
573
+ const interval = serverId === 'server1' ? this.heartbeatTimeout1 : this.heartbeatTimeout2;
574
+ if (interval) {
575
+ clearInterval(interval);
576
+ if (serverId === 'server1') {
577
+ this.heartbeatTimeout1 = undefined;
578
+ }
579
+ else {
580
+ this.heartbeatTimeout2 = undefined;
450
581
  }
451
- }, 30000); // Send ping every 30 seconds
582
+ }
452
583
  }
453
584
  /**
454
- * Start application-level heartbeat
585
+ * Start application-level heartbeat (shared across both servers)
455
586
  */
456
587
  startAppHeartbeat() {
457
588
  this.appHeartbeatInterval = setInterval(() => {
458
- if (this.isReady()) {
459
- const heartbeatMessage = {
460
- msgType: "heartbeat",
461
- agentId: this.config.agentId,
462
- };
463
- this.sendMessage(heartbeatMessage);
464
- this.state.lastAppHeartbeat = Date.now();
589
+ const heartbeatMessage = {
590
+ msgType: "heartbeat",
591
+ agentId: this.config.agentId,
592
+ };
593
+ // Send to all connected servers
594
+ if (this.ws1?.readyState === ws_1.default.OPEN) {
595
+ try {
596
+ this.ws1.send(JSON.stringify(heartbeatMessage));
597
+ }
598
+ catch (error) {
599
+ console.error('[Server1] Failed to send app heartbeat:', error);
600
+ }
465
601
  }
466
- }, 20000); // Send application heartbeat every 20 seconds
602
+ if (this.ws2?.readyState === ws_1.default.OPEN) {
603
+ try {
604
+ this.ws2.send(JSON.stringify(heartbeatMessage));
605
+ }
606
+ catch (error) {
607
+ console.error('[Server2] Failed to send app heartbeat:', error);
608
+ }
609
+ }
610
+ }, 20000);
467
611
  }
468
612
  /**
469
- * Schedule reconnection attempt with exponential backoff
613
+ * Schedule reconnection for specific server
470
614
  */
471
- scheduleReconnect() {
472
- if (this.state.reconnectAttempts >= this.state.maxReconnectAttempts) {
473
- console.error("Max reconnection attempts reached");
474
- this.emit("maxReconnectAttemptsReached");
615
+ scheduleReconnect(serverId) {
616
+ const state = serverId === 'server1' ? this.state1 : this.state2;
617
+ if (state.reconnectAttempts >= 50) {
618
+ console.error(`[${serverId}] Max reconnection attempts reached`);
619
+ this.emit("maxReconnectAttemptsReached", serverId);
475
620
  return;
476
621
  }
477
- // Exponential backoff with longer intervals: 2s, 4s, 8s, 16s, 32s, 60s (max)
478
- const delay = Math.min(2000 * Math.pow(2, this.state.reconnectAttempts), 60000);
479
- this.state.reconnectAttempts++;
480
- console.log(`Scheduling reconnect attempt ${this.state.reconnectAttempts}/${this.state.maxReconnectAttempts} in ${delay}ms`);
481
- this.reconnectTimeout = setTimeout(async () => {
622
+ const delay = Math.min(2000 * Math.pow(2, state.reconnectAttempts), 60000);
623
+ state.reconnectAttempts++;
624
+ console.log(`[${serverId}] Scheduling reconnect attempt ${state.reconnectAttempts}/50 in ${delay}ms`);
625
+ const timeout = setTimeout(async () => {
482
626
  try {
483
- await this.connect();
627
+ if (serverId === 'server1') {
628
+ await this.connectToServer1();
629
+ }
630
+ else {
631
+ await this.connectToServer2();
632
+ }
633
+ console.log(`[${serverId}] Reconnected successfully`);
484
634
  }
485
635
  catch (error) {
486
- console.error("Reconnection failed:", error);
487
- this.scheduleReconnect();
636
+ console.error(`[${serverId}] Reconnection failed:`, error);
637
+ this.scheduleReconnect(serverId);
488
638
  }
489
639
  }, delay);
640
+ if (serverId === 'server1') {
641
+ this.reconnectTimeout1 = timeout;
642
+ }
643
+ else {
644
+ this.reconnectTimeout2 = timeout;
645
+ }
490
646
  }
491
647
  /**
492
648
  * Clear all timers
493
649
  */
494
650
  clearTimers() {
495
- if (this.protocolHeartbeatInterval) {
496
- clearInterval(this.protocolHeartbeatInterval);
497
- this.protocolHeartbeatInterval = null;
651
+ if (this.heartbeatTimeout1) {
652
+ clearInterval(this.heartbeatTimeout1);
653
+ this.heartbeatTimeout1 = undefined;
654
+ }
655
+ if (this.heartbeatTimeout2) {
656
+ clearInterval(this.heartbeatTimeout2);
657
+ this.heartbeatTimeout2 = undefined;
498
658
  }
499
659
  if (this.appHeartbeatInterval) {
500
660
  clearInterval(this.appHeartbeatInterval);
501
- this.appHeartbeatInterval = null;
661
+ this.appHeartbeatInterval = undefined;
662
+ }
663
+ if (this.reconnectTimeout1) {
664
+ clearTimeout(this.reconnectTimeout1);
665
+ this.reconnectTimeout1 = undefined;
502
666
  }
503
- if (this.reconnectTimeout) {
504
- clearTimeout(this.reconnectTimeout);
505
- this.reconnectTimeout = null;
667
+ if (this.reconnectTimeout2) {
668
+ clearTimeout(this.reconnectTimeout2);
669
+ this.reconnectTimeout2 = undefined;
506
670
  }
507
671
  }
672
+ /**
673
+ * Type guard for A2A request messages
674
+ */
675
+ isA2ARequestMessage(data) {
676
+ return data &&
677
+ typeof data.agentId === "string" &&
678
+ typeof data.sessionId === "string" &&
679
+ data.jsonrpc === "2.0" &&
680
+ typeof data.id === "string" &&
681
+ data.method === "message/stream" &&
682
+ data.params &&
683
+ typeof data.params.id === "string" &&
684
+ typeof data.params.sessionId === "string" &&
685
+ data.params.message &&
686
+ typeof data.params.message.role === "string" &&
687
+ Array.isArray(data.params.message.parts);
688
+ }
508
689
  /**
509
690
  * Get active tasks
510
691
  */
@@ -517,5 +698,17 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
517
698
  removeActiveTask(taskId) {
518
699
  this.activeTasks.delete(taskId);
519
700
  }
701
+ /**
702
+ * Get server for a specific session
703
+ */
704
+ getServerForSession(sessionId) {
705
+ return this.sessionServerMap.get(sessionId);
706
+ }
707
+ /**
708
+ * Remove session mapping
709
+ */
710
+ removeSession(sessionId) {
711
+ this.sessionServerMap.delete(sessionId);
712
+ }
520
713
  }
521
714
  exports.XiaoYiWebSocketManager = XiaoYiWebSocketManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "XiaoYi channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",