@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 +9 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/types.d.ts +20 -1
- package/dist/types.js +4 -0
- package/dist/websocket.d.ts +71 -57
- package/dist/websocket.js +506 -313
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
* "
|
|
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
|
-
*
|
|
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
|
-
* "
|
|
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
|
|
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";
|
package/dist/websocket.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
8
|
-
private
|
|
9
|
-
private appHeartbeatInterval
|
|
10
|
-
private
|
|
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
|
-
*
|
|
19
|
+
* Resolve configuration with defaults and backward compatibility
|
|
15
20
|
*/
|
|
16
|
-
|
|
21
|
+
private resolveConfig;
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
23
|
+
* Connect to both WebSocket servers
|
|
19
24
|
*/
|
|
20
|
-
|
|
25
|
+
connect(): Promise<void>;
|
|
21
26
|
/**
|
|
22
|
-
*
|
|
27
|
+
* Connect to server 1
|
|
23
28
|
*/
|
|
24
|
-
private
|
|
29
|
+
private connectToServer1;
|
|
25
30
|
/**
|
|
26
|
-
*
|
|
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
|
-
|
|
33
|
+
private connectToServer2;
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
37
|
-
* Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
|
|
35
|
+
* Disconnect from all servers
|
|
38
36
|
*/
|
|
39
|
-
|
|
37
|
+
disconnect(): void;
|
|
40
38
|
/**
|
|
41
|
-
* Send
|
|
42
|
-
* Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
|
|
39
|
+
* Send init message to specific server
|
|
43
40
|
*/
|
|
44
|
-
|
|
41
|
+
private sendInitMessage;
|
|
45
42
|
/**
|
|
46
|
-
*
|
|
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
|
|
45
|
+
private setupWebSocketHandlers;
|
|
53
46
|
/**
|
|
54
|
-
*
|
|
47
|
+
* Handle incoming message from specific server
|
|
55
48
|
*/
|
|
56
|
-
private
|
|
49
|
+
private handleIncomingMessage;
|
|
57
50
|
/**
|
|
58
|
-
*
|
|
51
|
+
* Send A2A response message with automatic routing
|
|
59
52
|
*/
|
|
60
|
-
|
|
53
|
+
sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string, isFinal?: boolean, append?: boolean): Promise<void>;
|
|
61
54
|
/**
|
|
62
|
-
*
|
|
55
|
+
* Send clear context response to specific server
|
|
63
56
|
*/
|
|
64
|
-
|
|
57
|
+
sendClearContextResponse(requestId: string, sessionId: string, success?: boolean, targetServer?: ServerId): Promise<void>;
|
|
65
58
|
/**
|
|
66
|
-
*
|
|
59
|
+
* Send tasks cancel response to specific server
|
|
67
60
|
*/
|
|
68
|
-
|
|
61
|
+
sendTasksCancelResponse(requestId: string, sessionId: string, success?: boolean, targetServer?: ServerId): Promise<void>;
|
|
69
62
|
/**
|
|
70
|
-
* Handle
|
|
63
|
+
* Handle clearContext method
|
|
71
64
|
*/
|
|
72
|
-
private
|
|
65
|
+
private handleClearContext;
|
|
73
66
|
/**
|
|
74
|
-
* Handle
|
|
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
|
|
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
|
-
*
|
|
75
|
+
* Convert A2AResponseMessage to JSON-RPC 2.0 format
|
|
89
76
|
*/
|
|
90
|
-
|
|
77
|
+
private convertToJsonRpcFormat;
|
|
91
78
|
/**
|
|
92
|
-
*
|
|
79
|
+
* Check if at least one server is ready
|
|
93
80
|
*/
|
|
94
|
-
|
|
81
|
+
isReady(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Get combined connection state
|
|
84
|
+
*/
|
|
85
|
+
getState(): WebSocketConnectionState;
|
|
95
86
|
/**
|
|
96
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
this.
|
|
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
|
-
|
|
20
|
+
ready: false,
|
|
23
21
|
lastHeartbeat: 0,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
178
|
+
if (this.ws1) {
|
|
179
|
+
this.ws1.close();
|
|
180
|
+
this.ws1 = null;
|
|
75
181
|
}
|
|
76
|
-
this.
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
148
|
-
console.log(`
|
|
149
|
-
console.log(`
|
|
150
|
-
console.log(`
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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,
|
|
234
|
-
lastChunk: isFinal,
|
|
235
|
-
final: isFinal,
|
|
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
|
-
*
|
|
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
|
|
268
|
-
this.
|
|
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
|
|
523
|
+
* Get combined connection state
|
|
272
524
|
*/
|
|
273
525
|
getState() {
|
|
274
|
-
|
|
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
|
-
*
|
|
538
|
+
* Get individual server states
|
|
278
539
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
*
|
|
547
|
+
* Start protocol-level heartbeat for specific server
|
|
313
548
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
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
|
-
|
|
566
|
+
this.heartbeatTimeout2 = interval;
|
|
361
567
|
}
|
|
362
568
|
}
|
|
363
569
|
/**
|
|
364
|
-
*
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
|
613
|
+
* Schedule reconnection for specific server
|
|
470
614
|
*/
|
|
471
|
-
scheduleReconnect() {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
496
|
-
clearInterval(this.
|
|
497
|
-
this.
|
|
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 =
|
|
661
|
+
this.appHeartbeatInterval = undefined;
|
|
662
|
+
}
|
|
663
|
+
if (this.reconnectTimeout1) {
|
|
664
|
+
clearTimeout(this.reconnectTimeout1);
|
|
665
|
+
this.reconnectTimeout1 = undefined;
|
|
502
666
|
}
|
|
503
|
-
if (this.
|
|
504
|
-
clearTimeout(this.
|
|
505
|
-
this.
|
|
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;
|