@ynhcj/xiaoyi 1.7.0 → 1.8.0
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/auth.d.ts +5 -1
- package/dist/auth.js +14 -1
- package/dist/channel.d.ts +7 -6
- package/dist/channel.js +60 -48
- package/dist/config-schema.d.ts +2 -2
- package/dist/index.d.ts +5 -9
- package/dist/index.js +5 -9
- package/dist/runtime.d.ts +33 -15
- package/dist/runtime.js +78 -38
- package/dist/types.d.ts +26 -4
- package/dist/websocket.d.ts +42 -14
- package/dist/websocket.js +194 -61
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -22,7 +22,11 @@ export declare class XiaoYiAuth {
|
|
|
22
22
|
*/
|
|
23
23
|
verifyCredentials(credentials: AuthCredentials): boolean;
|
|
24
24
|
/**
|
|
25
|
-
* Generate authentication
|
|
25
|
+
* Generate authentication headers for WebSocket connection
|
|
26
|
+
*/
|
|
27
|
+
generateAuthHeaders(): Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate authentication message for WebSocket (legacy, kept for compatibility)
|
|
26
30
|
*/
|
|
27
31
|
generateAuthMessage(): any;
|
|
28
32
|
}
|
package/dist/auth.js
CHANGED
|
@@ -76,7 +76,20 @@ class XiaoYiAuth {
|
|
|
76
76
|
return credentials.signature === expectedSignature;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Generate authentication
|
|
79
|
+
* Generate authentication headers for WebSocket connection
|
|
80
|
+
*/
|
|
81
|
+
generateAuthHeaders() {
|
|
82
|
+
const timestamp = Date.now();
|
|
83
|
+
const signature = this.generateSignature(timestamp);
|
|
84
|
+
return {
|
|
85
|
+
"x-access-key": this.ak,
|
|
86
|
+
"x-sign": signature,
|
|
87
|
+
"x-ts": timestamp.toString(),
|
|
88
|
+
"x-agent-id": this.agentId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate authentication message for WebSocket (legacy, kept for compatibility)
|
|
80
93
|
*/
|
|
81
94
|
generateAuthMessage() {
|
|
82
95
|
const credentials = this.generateAuthCredentials();
|
package/dist/channel.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { ChannelOutboundContext, OutboundDeliveryResult, ChannelGatewayStartAccountContext, ChannelGatewayStopAccountContext, ChannelGatewayProbeAccountContext, ChannelMessagingNormalizeTargetContext, ChannelStatusGetAccountStatusContext } from "openclaw";
|
|
2
|
-
import {
|
|
2
|
+
import { XiaoYiChannelConfig } from "./types";
|
|
3
3
|
/**
|
|
4
|
-
* Resolved XiaoYi account configuration
|
|
4
|
+
* Resolved XiaoYi account configuration (single account mode)
|
|
5
5
|
*/
|
|
6
6
|
export interface ResolvedXiaoYiAccount {
|
|
7
7
|
accountId: string;
|
|
8
|
-
config:
|
|
8
|
+
config: XiaoYiChannelConfig;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* XiaoYi Channel Plugin
|
|
12
12
|
* Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
|
|
13
|
+
* Single account mode only
|
|
13
14
|
*/
|
|
14
15
|
export declare const xiaoyiPlugin: {
|
|
15
16
|
id: string;
|
|
@@ -30,7 +31,7 @@ export declare const xiaoyiPlugin: {
|
|
|
30
31
|
nativeCommands: boolean;
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
|
-
* Config adapter -
|
|
34
|
+
* Config adapter - single account mode
|
|
34
35
|
*/
|
|
35
36
|
config: {
|
|
36
37
|
listAccountIds: (cfg: any) => string[];
|
|
@@ -45,11 +46,11 @@ export declare const xiaoyiPlugin: {
|
|
|
45
46
|
};
|
|
46
47
|
enabled: boolean;
|
|
47
48
|
};
|
|
48
|
-
defaultAccountId: (cfg: any) =>
|
|
49
|
+
defaultAccountId: (cfg: any) => "default" | undefined;
|
|
49
50
|
isConfigured: (account: any) => boolean;
|
|
50
51
|
describeAccount: (account: any) => {
|
|
51
52
|
accountId: any;
|
|
52
|
-
name:
|
|
53
|
+
name: string;
|
|
53
54
|
enabled: any;
|
|
54
55
|
configured: boolean;
|
|
55
56
|
};
|
package/dist/channel.js
CHANGED
|
@@ -5,6 +5,7 @@ const runtime_1 = require("./runtime");
|
|
|
5
5
|
/**
|
|
6
6
|
* XiaoYi Channel Plugin
|
|
7
7
|
* Implements OpenClaw ChannelPlugin interface for XiaoYi A2A protocol
|
|
8
|
+
* Single account mode only
|
|
8
9
|
*/
|
|
9
10
|
exports.xiaoyiPlugin = {
|
|
10
11
|
id: "xiaoyi",
|
|
@@ -14,7 +15,7 @@ exports.xiaoyiPlugin = {
|
|
|
14
15
|
selectionLabel: "XiaoYi (小艺)",
|
|
15
16
|
docsPath: "/channels/xiaoyi",
|
|
16
17
|
blurb: "小艺 A2A 协议支持,通过 WebSocket 连接。",
|
|
17
|
-
aliases: ["
|
|
18
|
+
aliases: ["xiaoyi"],
|
|
18
19
|
},
|
|
19
20
|
capabilities: {
|
|
20
21
|
chatTypes: ["direct"],
|
|
@@ -25,37 +26,24 @@ exports.xiaoyiPlugin = {
|
|
|
25
26
|
nativeCommands: false,
|
|
26
27
|
},
|
|
27
28
|
/**
|
|
28
|
-
* Config adapter -
|
|
29
|
+
* Config adapter - single account mode
|
|
29
30
|
*/
|
|
30
31
|
config: {
|
|
31
32
|
listAccountIds: (cfg) => {
|
|
32
33
|
const channelConfig = cfg?.channels?.xiaoyi;
|
|
33
|
-
if (!channelConfig || !channelConfig.
|
|
34
|
+
if (!channelConfig || !channelConfig.enabled) {
|
|
34
35
|
return [];
|
|
35
36
|
}
|
|
36
|
-
return
|
|
37
|
+
// Single account mode: always return "default"
|
|
38
|
+
return ["default"];
|
|
37
39
|
},
|
|
38
40
|
resolveAccount: (cfg, accountId) => {
|
|
39
|
-
|
|
41
|
+
// Single account mode: always use "default"
|
|
42
|
+
const resolvedAccountId = "default";
|
|
40
43
|
// Access channel config from cfg.channels.xiaoyi
|
|
41
44
|
const channelConfig = cfg?.channels?.xiaoyi;
|
|
42
45
|
// If channel is not configured yet, return empty config
|
|
43
|
-
if (!channelConfig
|
|
44
|
-
return {
|
|
45
|
-
accountId: resolvedAccountId,
|
|
46
|
-
config: {
|
|
47
|
-
enabled: false,
|
|
48
|
-
wsUrl: "",
|
|
49
|
-
ak: "",
|
|
50
|
-
sk: "",
|
|
51
|
-
agentId: "",
|
|
52
|
-
},
|
|
53
|
-
enabled: false,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
const accountConfig = channelConfig.accounts[resolvedAccountId];
|
|
57
|
-
// If specific account not found, return empty config
|
|
58
|
-
if (!accountConfig) {
|
|
46
|
+
if (!channelConfig) {
|
|
59
47
|
return {
|
|
60
48
|
accountId: resolvedAccountId,
|
|
61
49
|
config: {
|
|
@@ -70,17 +58,17 @@ exports.xiaoyiPlugin = {
|
|
|
70
58
|
}
|
|
71
59
|
return {
|
|
72
60
|
accountId: resolvedAccountId,
|
|
73
|
-
config:
|
|
74
|
-
enabled:
|
|
61
|
+
config: channelConfig,
|
|
62
|
+
enabled: channelConfig.enabled !== false,
|
|
75
63
|
};
|
|
76
64
|
},
|
|
77
65
|
defaultAccountId: (cfg) => {
|
|
78
66
|
const channelConfig = cfg?.channels?.xiaoyi;
|
|
79
|
-
if (!channelConfig || !channelConfig.
|
|
67
|
+
if (!channelConfig || !channelConfig.enabled) {
|
|
80
68
|
return undefined;
|
|
81
69
|
}
|
|
82
|
-
|
|
83
|
-
return
|
|
70
|
+
// Single account mode: always return "default"
|
|
71
|
+
return "default";
|
|
84
72
|
},
|
|
85
73
|
isConfigured: (account) => {
|
|
86
74
|
// Safely check if all required fields are present and non-empty
|
|
@@ -97,7 +85,7 @@ exports.xiaoyiPlugin = {
|
|
|
97
85
|
},
|
|
98
86
|
describeAccount: (account) => ({
|
|
99
87
|
accountId: account.accountId,
|
|
100
|
-
name:
|
|
88
|
+
name: 'XiaoYi',
|
|
101
89
|
enabled: account.enabled,
|
|
102
90
|
configured: Boolean(account.config?.wsUrl && account.config?.ak && account.config?.sk && account.config?.agentId),
|
|
103
91
|
}),
|
|
@@ -110,16 +98,20 @@ exports.xiaoyiPlugin = {
|
|
|
110
98
|
textChunkLimit: 4000,
|
|
111
99
|
sendText: async (ctx) => {
|
|
112
100
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
113
|
-
const connection = runtime.getConnection(
|
|
101
|
+
const connection = runtime.getConnection();
|
|
114
102
|
if (!connection || !connection.isReady()) {
|
|
115
|
-
throw new Error(
|
|
103
|
+
throw new Error("XiaoYi channel not connected");
|
|
116
104
|
}
|
|
117
105
|
// Get account config to retrieve agentId
|
|
118
106
|
const resolvedAccount = ctx.account;
|
|
119
107
|
const agentId = resolvedAccount.config.agentId;
|
|
108
|
+
// Use 'to' as sessionId (it's set from incoming message's sessionId)
|
|
109
|
+
const sessionId = ctx.to;
|
|
110
|
+
// Get taskId from runtime's session mapping
|
|
111
|
+
const taskId = runtime.getTaskIdForSession(sessionId) || `task_${Date.now()}`;
|
|
120
112
|
// Build A2A response message
|
|
121
113
|
const response = {
|
|
122
|
-
sessionId:
|
|
114
|
+
sessionId: sessionId,
|
|
123
115
|
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
124
116
|
timestamp: Date.now(),
|
|
125
117
|
agentId: agentId,
|
|
@@ -137,26 +129,30 @@ exports.xiaoyiPlugin = {
|
|
|
137
129
|
} : undefined,
|
|
138
130
|
status: "success",
|
|
139
131
|
};
|
|
140
|
-
// Send via WebSocket
|
|
141
|
-
await connection.sendResponse(response);
|
|
132
|
+
// Send via WebSocket with taskId and sessionId
|
|
133
|
+
await connection.sendResponse(response, taskId, sessionId);
|
|
142
134
|
return {
|
|
143
135
|
channel: "xiaoyi",
|
|
144
136
|
messageId: response.messageId,
|
|
145
|
-
conversationId:
|
|
137
|
+
conversationId: sessionId,
|
|
146
138
|
timestamp: response.timestamp,
|
|
147
139
|
};
|
|
148
140
|
},
|
|
149
141
|
sendMedia: async (ctx) => {
|
|
150
142
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
151
|
-
const connection = runtime.getConnection(
|
|
143
|
+
const connection = runtime.getConnection();
|
|
152
144
|
if (!connection || !connection.isReady()) {
|
|
153
|
-
throw new Error(
|
|
145
|
+
throw new Error("XiaoYi channel not connected");
|
|
154
146
|
}
|
|
155
147
|
const resolvedAccount = ctx.account;
|
|
156
148
|
const agentId = resolvedAccount.config.agentId;
|
|
149
|
+
// Use 'to' as sessionId
|
|
150
|
+
const sessionId = ctx.to;
|
|
151
|
+
// Get taskId from runtime's session mapping
|
|
152
|
+
const taskId = runtime.getTaskIdForSession(sessionId) || `task_${Date.now()}`;
|
|
157
153
|
// Build A2A response message with media
|
|
158
154
|
const response = {
|
|
159
|
-
sessionId:
|
|
155
|
+
sessionId: sessionId,
|
|
160
156
|
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
161
157
|
timestamp: Date.now(),
|
|
162
158
|
agentId: agentId,
|
|
@@ -175,11 +171,11 @@ exports.xiaoyiPlugin = {
|
|
|
175
171
|
} : undefined,
|
|
176
172
|
status: "success",
|
|
177
173
|
};
|
|
178
|
-
await connection.sendResponse(response);
|
|
174
|
+
await connection.sendResponse(response, taskId, sessionId);
|
|
179
175
|
return {
|
|
180
176
|
channel: "xiaoyi",
|
|
181
177
|
messageId: response.messageId,
|
|
182
|
-
conversationId:
|
|
178
|
+
conversationId: sessionId,
|
|
183
179
|
timestamp: response.timestamp,
|
|
184
180
|
};
|
|
185
181
|
},
|
|
@@ -191,12 +187,14 @@ exports.xiaoyiPlugin = {
|
|
|
191
187
|
startAccount: async (ctx) => {
|
|
192
188
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
193
189
|
const resolvedAccount = ctx.account;
|
|
194
|
-
// Start WebSocket connection
|
|
195
|
-
await runtime.
|
|
190
|
+
// Start WebSocket connection (single account mode)
|
|
191
|
+
await runtime.start(resolvedAccount.config);
|
|
196
192
|
// Setup message handler
|
|
197
|
-
const connection = runtime.getConnection(
|
|
193
|
+
const connection = runtime.getConnection();
|
|
198
194
|
if (connection) {
|
|
199
195
|
connection.on("message", async (message) => {
|
|
196
|
+
// Store sessionId -> taskId mapping in runtime
|
|
197
|
+
runtime.setTaskIdForSession(message.sessionId, message.id);
|
|
200
198
|
// Convert A2A message to OpenClaw inbound message format
|
|
201
199
|
await ctx.handleInboundMessage({
|
|
202
200
|
channel: "xiaoyi",
|
|
@@ -207,24 +205,38 @@ exports.xiaoyiPlugin = {
|
|
|
207
205
|
timestamp: message.timestamp,
|
|
208
206
|
peer: {
|
|
209
207
|
kind: "dm",
|
|
210
|
-
id: message.
|
|
208
|
+
id: message.sessionId, // Use sessionId as peer id for routing responses
|
|
211
209
|
},
|
|
212
|
-
// Store sessionId for response
|
|
213
210
|
meta: {
|
|
214
211
|
sessionId: message.sessionId,
|
|
215
|
-
|
|
212
|
+
taskId: message.id,
|
|
216
213
|
},
|
|
217
214
|
});
|
|
218
215
|
});
|
|
216
|
+
// Setup cancel handler
|
|
217
|
+
connection.on("cancel", async (data) => {
|
|
218
|
+
console.log(`Handling cancel request for task: ${data.taskId}`);
|
|
219
|
+
// Emit cancel event to OpenClaw runtime
|
|
220
|
+
if (runtime.getRuntime()) {
|
|
221
|
+
runtime.getRuntime().emit("task:cancel", {
|
|
222
|
+
channel: "xiaoyi",
|
|
223
|
+
accountId: resolvedAccount.accountId,
|
|
224
|
+
taskId: data.taskId,
|
|
225
|
+
sessionId: data.sessionId,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Send success response
|
|
229
|
+
await connection.sendCancelSuccessResponse(data.sessionId, data.taskId, data.id);
|
|
230
|
+
});
|
|
219
231
|
}
|
|
220
232
|
},
|
|
221
233
|
stopAccount: async (ctx) => {
|
|
222
234
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
223
|
-
runtime.
|
|
235
|
+
runtime.stop();
|
|
224
236
|
},
|
|
225
237
|
probeAccount: async (ctx) => {
|
|
226
238
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
227
|
-
const isConnected = runtime.
|
|
239
|
+
const isConnected = runtime.isConnected();
|
|
228
240
|
return {
|
|
229
241
|
status: isConnected ? "healthy" : "unhealthy",
|
|
230
242
|
message: isConnected ? "Connected" : "Disconnected",
|
|
@@ -247,7 +259,7 @@ exports.xiaoyiPlugin = {
|
|
|
247
259
|
status: {
|
|
248
260
|
getAccountStatus: async (ctx) => {
|
|
249
261
|
const runtime = (0, runtime_1.getXiaoYiRuntime)();
|
|
250
|
-
const connection = runtime.getConnection(
|
|
262
|
+
const connection = runtime.getConnection();
|
|
251
263
|
if (!connection) {
|
|
252
264
|
return {
|
|
253
265
|
status: "offline",
|
|
@@ -270,7 +282,7 @@ exports.xiaoyiPlugin = {
|
|
|
270
282
|
else {
|
|
271
283
|
return {
|
|
272
284
|
status: "offline",
|
|
273
|
-
message: `Reconnect attempts: ${state.reconnectAttempts}`,
|
|
285
|
+
message: `Reconnect attempts: ${state.reconnectAttempts}/${state.maxReconnectAttempts}`,
|
|
274
286
|
};
|
|
275
287
|
}
|
|
276
288
|
},
|
package/dist/config-schema.d.ts
CHANGED
|
@@ -23,14 +23,13 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
|
|
|
23
23
|
}, "strip", z.ZodTypeAny, {
|
|
24
24
|
enabled: boolean;
|
|
25
25
|
debug: boolean;
|
|
26
|
-
accounts?: Record<string, unknown> | undefined;
|
|
27
26
|
name?: string | undefined;
|
|
28
27
|
wsUrl?: string | undefined;
|
|
29
28
|
ak?: string | undefined;
|
|
30
29
|
sk?: string | undefined;
|
|
31
30
|
agentId?: string | undefined;
|
|
32
|
-
}, {
|
|
33
31
|
accounts?: Record<string, unknown> | undefined;
|
|
32
|
+
}, {
|
|
34
33
|
name?: string | undefined;
|
|
35
34
|
enabled?: boolean | undefined;
|
|
36
35
|
wsUrl?: string | undefined;
|
|
@@ -38,5 +37,6 @@ export declare const XiaoYiConfigSchema: z.ZodObject<{
|
|
|
38
37
|
sk?: string | undefined;
|
|
39
38
|
agentId?: string | undefined;
|
|
40
39
|
debug?: boolean | undefined;
|
|
40
|
+
accounts?: Record<string, unknown> | undefined;
|
|
41
41
|
}>;
|
|
42
42
|
export type XiaoYiConfig = z.infer<typeof XiaoYiConfigSchema>;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,21 +3,17 @@ 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
7
|
*
|
|
7
8
|
* Configuration example in openclaw.json:
|
|
8
9
|
* {
|
|
9
10
|
* "channels": {
|
|
10
11
|
* "xiaoyi": {
|
|
11
12
|
* "enabled": true,
|
|
12
|
-
* "
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* "ak": "your-access-key",
|
|
17
|
-
* "sk": "your-secret-key",
|
|
18
|
-
* "agentId": "your-agent-id"
|
|
19
|
-
* }
|
|
20
|
-
* }
|
|
13
|
+
* "wsUrl": "ws://localhost:8765/ws/link",
|
|
14
|
+
* "ak": "test_ak",
|
|
15
|
+
* "sk": "test_sk",
|
|
16
|
+
* "agentId": "your-agent-id"
|
|
21
17
|
* }
|
|
22
18
|
* }
|
|
23
19
|
* }
|
package/dist/index.js
CHANGED
|
@@ -7,21 +7,17 @@ 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
11
|
*
|
|
11
12
|
* Configuration example in openclaw.json:
|
|
12
13
|
* {
|
|
13
14
|
* "channels": {
|
|
14
15
|
* "xiaoyi": {
|
|
15
16
|
* "enabled": true,
|
|
16
|
-
* "
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* "ak": "your-access-key",
|
|
21
|
-
* "sk": "your-secret-key",
|
|
22
|
-
* "agentId": "your-agent-id"
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
17
|
+
* "wsUrl": "ws://localhost:8765/ws/link",
|
|
18
|
+
* "ak": "test_ak",
|
|
19
|
+
* "sk": "test_sk",
|
|
20
|
+
* "agentId": "your-agent-id"
|
|
25
21
|
* }
|
|
26
22
|
* }
|
|
27
23
|
* }
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { XiaoYiWebSocketManager } from "./websocket";
|
|
2
|
-
import {
|
|
2
|
+
import { XiaoYiChannelConfig } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Runtime state for XiaoYi channel
|
|
5
|
-
* Manages WebSocket
|
|
5
|
+
* Manages single WebSocket connection (single account mode)
|
|
6
6
|
*/
|
|
7
7
|
export declare class XiaoYiRuntime {
|
|
8
|
-
private
|
|
8
|
+
private connection;
|
|
9
9
|
private runtime;
|
|
10
|
+
private config;
|
|
11
|
+
private sessionToTaskIdMap;
|
|
10
12
|
/**
|
|
11
13
|
* Set OpenClaw runtime
|
|
12
14
|
*/
|
|
@@ -16,33 +18,49 @@ export declare class XiaoYiRuntime {
|
|
|
16
18
|
*/
|
|
17
19
|
getRuntime(): any;
|
|
18
20
|
/**
|
|
19
|
-
* Start account
|
|
21
|
+
* Start connection (single account mode)
|
|
20
22
|
*/
|
|
21
|
-
|
|
23
|
+
start(config: XiaoYiChannelConfig): Promise<void>;
|
|
22
24
|
/**
|
|
23
|
-
* Stop
|
|
25
|
+
* Stop connection
|
|
24
26
|
*/
|
|
25
|
-
|
|
27
|
+
stop(): void;
|
|
26
28
|
/**
|
|
27
|
-
* Get WebSocket manager
|
|
29
|
+
* Get WebSocket manager
|
|
28
30
|
*/
|
|
29
|
-
getConnection(
|
|
31
|
+
getConnection(): XiaoYiWebSocketManager | null;
|
|
30
32
|
/**
|
|
31
|
-
* Check if
|
|
33
|
+
* Check if connected
|
|
32
34
|
*/
|
|
33
|
-
|
|
35
|
+
isConnected(): boolean;
|
|
34
36
|
/**
|
|
35
|
-
* Get
|
|
37
|
+
* Get configuration
|
|
36
38
|
*/
|
|
37
|
-
|
|
39
|
+
getConfig(): XiaoYiChannelConfig | null;
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
41
|
+
* Set taskId for a session
|
|
40
42
|
*/
|
|
41
|
-
|
|
43
|
+
setTaskIdForSession(sessionId: string, taskId: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get taskId for a session
|
|
46
|
+
*/
|
|
47
|
+
getTaskIdForSession(sessionId: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Clear taskId for a session
|
|
50
|
+
*/
|
|
51
|
+
clearTaskIdForSession(sessionId: string): void;
|
|
42
52
|
/**
|
|
43
53
|
* Handle incoming A2A message
|
|
44
54
|
*/
|
|
45
55
|
private handleIncomingMessage;
|
|
56
|
+
/**
|
|
57
|
+
* Handle clear event
|
|
58
|
+
*/
|
|
59
|
+
private handleClearEvent;
|
|
60
|
+
/**
|
|
61
|
+
* Handle cancel event
|
|
62
|
+
*/
|
|
63
|
+
private handleCancelEvent;
|
|
46
64
|
}
|
|
47
65
|
export declare function getXiaoYiRuntime(): XiaoYiRuntime;
|
|
48
66
|
export declare function setXiaoYiRuntime(runtime: any): void;
|
package/dist/runtime.js
CHANGED
|
@@ -6,12 +6,14 @@ exports.setXiaoYiRuntime = setXiaoYiRuntime;
|
|
|
6
6
|
const websocket_1 = require("./websocket");
|
|
7
7
|
/**
|
|
8
8
|
* Runtime state for XiaoYi channel
|
|
9
|
-
* Manages WebSocket
|
|
9
|
+
* Manages single WebSocket connection (single account mode)
|
|
10
10
|
*/
|
|
11
11
|
class XiaoYiRuntime {
|
|
12
12
|
constructor() {
|
|
13
|
-
this.
|
|
13
|
+
this.connection = null;
|
|
14
14
|
this.runtime = null;
|
|
15
|
+
this.config = null;
|
|
16
|
+
this.sessionToTaskIdMap = new Map(); // Map sessionId to taskId
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Set OpenClaw runtime
|
|
@@ -26,78 +28,95 @@ class XiaoYiRuntime {
|
|
|
26
28
|
return this.runtime;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* Start account
|
|
31
|
+
* Start connection (single account mode)
|
|
30
32
|
*/
|
|
31
|
-
async
|
|
32
|
-
if (this.
|
|
33
|
-
console.log(
|
|
33
|
+
async start(config) {
|
|
34
|
+
if (this.connection) {
|
|
35
|
+
console.log("XiaoYi channel already connected");
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
38
|
+
this.config = config;
|
|
36
39
|
const manager = new websocket_1.XiaoYiWebSocketManager(config);
|
|
37
40
|
// Setup event handlers
|
|
38
41
|
manager.on("message", (message) => {
|
|
39
|
-
this.handleIncomingMessage(
|
|
42
|
+
this.handleIncomingMessage(message);
|
|
40
43
|
});
|
|
41
44
|
manager.on("error", (error) => {
|
|
42
|
-
console.error(
|
|
45
|
+
console.error("XiaoYi channel error:", error);
|
|
43
46
|
});
|
|
44
47
|
manager.on("disconnected", () => {
|
|
45
|
-
console.log(
|
|
48
|
+
console.log("XiaoYi channel disconnected");
|
|
46
49
|
});
|
|
47
50
|
manager.on("authenticated", () => {
|
|
48
|
-
console.log(
|
|
51
|
+
console.log("XiaoYi channel authenticated");
|
|
52
|
+
});
|
|
53
|
+
manager.on("clear", (data) => {
|
|
54
|
+
this.handleClearEvent(data);
|
|
55
|
+
});
|
|
56
|
+
manager.on("cancel", (data) => {
|
|
57
|
+
this.handleCancelEvent(data);
|
|
49
58
|
});
|
|
50
59
|
manager.on("maxReconnectAttemptsReached", () => {
|
|
51
|
-
console.error(
|
|
52
|
-
this.
|
|
60
|
+
console.error("XiaoYi channel max reconnect attempts reached");
|
|
61
|
+
this.stop();
|
|
53
62
|
});
|
|
54
63
|
// Connect
|
|
55
64
|
await manager.connect();
|
|
56
|
-
this.
|
|
57
|
-
console.log(
|
|
65
|
+
this.connection = manager;
|
|
66
|
+
console.log("XiaoYi channel started");
|
|
58
67
|
}
|
|
59
68
|
/**
|
|
60
|
-
* Stop
|
|
69
|
+
* Stop connection
|
|
61
70
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(`XiaoYi account ${accountId} stopped`);
|
|
71
|
+
stop() {
|
|
72
|
+
if (this.connection) {
|
|
73
|
+
this.connection.disconnect();
|
|
74
|
+
this.connection = null;
|
|
75
|
+
console.log("XiaoYi channel stopped");
|
|
68
76
|
}
|
|
77
|
+
// Clear session mappings
|
|
78
|
+
this.sessionToTaskIdMap.clear();
|
|
69
79
|
}
|
|
70
80
|
/**
|
|
71
|
-
* Get WebSocket manager
|
|
81
|
+
* Get WebSocket manager
|
|
72
82
|
*/
|
|
73
|
-
getConnection(
|
|
74
|
-
return this.
|
|
83
|
+
getConnection() {
|
|
84
|
+
return this.connection;
|
|
75
85
|
}
|
|
76
86
|
/**
|
|
77
|
-
* Check if
|
|
87
|
+
* Check if connected
|
|
78
88
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return manager ? manager.isReady() : false;
|
|
89
|
+
isConnected() {
|
|
90
|
+
return this.connection ? this.connection.isReady() : false;
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
84
|
-
* Get
|
|
93
|
+
* Get configuration
|
|
85
94
|
*/
|
|
86
|
-
|
|
87
|
-
return
|
|
95
|
+
getConfig() {
|
|
96
|
+
return this.config;
|
|
88
97
|
}
|
|
89
98
|
/**
|
|
90
|
-
*
|
|
99
|
+
* Set taskId for a session
|
|
91
100
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
setTaskIdForSession(sessionId, taskId) {
|
|
102
|
+
this.sessionToTaskIdMap.set(sessionId, taskId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get taskId for a session
|
|
106
|
+
*/
|
|
107
|
+
getTaskIdForSession(sessionId) {
|
|
108
|
+
return this.sessionToTaskIdMap.get(sessionId);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear taskId for a session
|
|
112
|
+
*/
|
|
113
|
+
clearTaskIdForSession(sessionId) {
|
|
114
|
+
this.sessionToTaskIdMap.delete(sessionId);
|
|
96
115
|
}
|
|
97
116
|
/**
|
|
98
117
|
* Handle incoming A2A message
|
|
99
118
|
*/
|
|
100
|
-
handleIncomingMessage(
|
|
119
|
+
handleIncomingMessage(message) {
|
|
101
120
|
if (!this.runtime) {
|
|
102
121
|
console.error("Runtime not set, cannot handle message");
|
|
103
122
|
return;
|
|
@@ -105,10 +124,31 @@ class XiaoYiRuntime {
|
|
|
105
124
|
// Dispatch message to OpenClaw's message handling system
|
|
106
125
|
// This will be called by the channel plugin's gateway adapter
|
|
107
126
|
this.runtime.emit("xiaoyi:message", {
|
|
108
|
-
accountId,
|
|
109
127
|
message,
|
|
110
128
|
});
|
|
111
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Handle clear event
|
|
132
|
+
*/
|
|
133
|
+
handleClearEvent(data) {
|
|
134
|
+
if (!this.runtime) {
|
|
135
|
+
console.error("Runtime not set, cannot handle clear event");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Emit clear event for OpenClaw to handle
|
|
139
|
+
this.runtime.emit("xiaoyi:clear", data);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Handle cancel event
|
|
143
|
+
*/
|
|
144
|
+
handleCancelEvent(data) {
|
|
145
|
+
if (!this.runtime) {
|
|
146
|
+
console.error("Runtime not set, cannot handle cancel event");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Emit cancel event for OpenClaw to handle
|
|
150
|
+
this.runtime.emit("xiaoyi:cancel", data);
|
|
151
|
+
}
|
|
112
152
|
}
|
|
113
153
|
exports.XiaoYiRuntime = XiaoYiRuntime;
|
|
114
154
|
// Global runtime instance
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface A2ARequestMessage {
|
|
2
|
+
agentId: string;
|
|
2
3
|
sessionId: string;
|
|
4
|
+
id: string;
|
|
3
5
|
messageId: string;
|
|
4
6
|
timestamp: number;
|
|
5
7
|
sender: {
|
|
@@ -54,11 +56,30 @@ export interface A2AWebSocketMessage {
|
|
|
54
56
|
type: "message" | "heartbeat" | "auth" | "error";
|
|
55
57
|
data: A2ARequestMessage | A2AResponseMessage | any;
|
|
56
58
|
}
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
export type OutboundMessageType = "clawd_bot_init" | "agent_response" | "heartbeat";
|
|
60
|
+
export interface OutboundWebSocketMessage {
|
|
61
|
+
msgType: OutboundMessageType;
|
|
62
|
+
agentId: string;
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
taskId?: string;
|
|
65
|
+
msgDetail?: string;
|
|
60
66
|
}
|
|
61
|
-
export interface
|
|
67
|
+
export interface A2AClearMessage {
|
|
68
|
+
agentId: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
id: string;
|
|
71
|
+
action: "clear";
|
|
72
|
+
timestamp: number;
|
|
73
|
+
}
|
|
74
|
+
export interface A2ATasksCancelMessage {
|
|
75
|
+
agentId: string;
|
|
76
|
+
sessionId: string;
|
|
77
|
+
id: string;
|
|
78
|
+
action: "tasks/cancel";
|
|
79
|
+
taskId: string;
|
|
80
|
+
timestamp: number;
|
|
81
|
+
}
|
|
82
|
+
export interface XiaoYiChannelConfig {
|
|
62
83
|
enabled: boolean;
|
|
63
84
|
wsUrl: string;
|
|
64
85
|
ak: string;
|
|
@@ -75,6 +96,7 @@ export interface WebSocketConnectionState {
|
|
|
75
96
|
connected: boolean;
|
|
76
97
|
authenticated: boolean;
|
|
77
98
|
lastHeartbeat: number;
|
|
99
|
+
lastAppHeartbeat: number;
|
|
78
100
|
reconnectAttempts: number;
|
|
79
101
|
maxReconnectAttempts: number;
|
|
80
102
|
}
|
package/dist/websocket.d.ts
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
import { A2AResponseMessage, WebSocketConnectionState,
|
|
2
|
+
import { A2AResponseMessage, WebSocketConnectionState, XiaoYiChannelConfig } from "./types";
|
|
3
3
|
export declare class XiaoYiWebSocketManager extends EventEmitter {
|
|
4
4
|
private ws;
|
|
5
5
|
private auth;
|
|
6
6
|
private config;
|
|
7
7
|
private state;
|
|
8
|
-
private
|
|
8
|
+
private protocolHeartbeatInterval;
|
|
9
|
+
private appHeartbeatInterval;
|
|
9
10
|
private reconnectTimeout;
|
|
10
|
-
|
|
11
|
+
private activeTasks;
|
|
12
|
+
constructor(config: XiaoYiChannelConfig);
|
|
11
13
|
/**
|
|
12
|
-
* Connect to XiaoYi WebSocket server
|
|
14
|
+
* Connect to XiaoYi WebSocket server with header authentication
|
|
13
15
|
*/
|
|
14
16
|
connect(): Promise<void>;
|
|
15
17
|
/**
|
|
16
18
|
* Disconnect from WebSocket server
|
|
17
19
|
*/
|
|
18
20
|
disconnect(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Send clawd_bot_init message on connection/reconnection
|
|
23
|
+
*/
|
|
24
|
+
private sendInitMessage;
|
|
19
25
|
/**
|
|
20
26
|
* Send A2A response message
|
|
21
27
|
*/
|
|
22
|
-
sendResponse(response: A2AResponseMessage): Promise<void>;
|
|
28
|
+
sendResponse(response: A2AResponseMessage, taskId: string, sessionId: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Send generic outbound message
|
|
31
|
+
*/
|
|
32
|
+
private sendMessage;
|
|
23
33
|
/**
|
|
24
34
|
* Check if connection is ready for sending messages
|
|
25
35
|
*/
|
|
@@ -37,27 +47,45 @@ export declare class XiaoYiWebSocketManager extends EventEmitter {
|
|
|
37
47
|
*/
|
|
38
48
|
private handleMessage;
|
|
39
49
|
/**
|
|
40
|
-
*
|
|
50
|
+
* Handle A2A clear message
|
|
51
|
+
* Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
|
|
41
52
|
*/
|
|
42
|
-
private
|
|
53
|
+
private handleClearMessage;
|
|
54
|
+
/**
|
|
55
|
+
* Handle A2A tasks/cancel message
|
|
56
|
+
* Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
|
|
57
|
+
*/
|
|
58
|
+
private handleTasksCancelMessage;
|
|
43
59
|
/**
|
|
44
|
-
*
|
|
60
|
+
* Send tasks/cancel success response
|
|
45
61
|
*/
|
|
46
|
-
|
|
62
|
+
sendCancelSuccessResponse(sessionId: string, taskId: string, requestId: string): Promise<void>;
|
|
47
63
|
/**
|
|
48
|
-
*
|
|
64
|
+
* Type guard for A2A request messages
|
|
65
|
+
*/
|
|
66
|
+
private isA2ARequestMessage;
|
|
67
|
+
/**
|
|
68
|
+
* Start protocol-level heartbeat (ping/pong)
|
|
49
69
|
*/
|
|
50
|
-
private
|
|
70
|
+
private startProtocolHeartbeat;
|
|
51
71
|
/**
|
|
52
|
-
*
|
|
72
|
+
* Start application-level heartbeat
|
|
53
73
|
*/
|
|
54
|
-
private
|
|
74
|
+
private startAppHeartbeat;
|
|
55
75
|
/**
|
|
56
|
-
* Schedule reconnection attempt
|
|
76
|
+
* Schedule reconnection attempt with exponential backoff
|
|
57
77
|
*/
|
|
58
78
|
private scheduleReconnect;
|
|
59
79
|
/**
|
|
60
80
|
* Clear all timers
|
|
61
81
|
*/
|
|
62
82
|
private clearTimers;
|
|
83
|
+
/**
|
|
84
|
+
* Get active tasks
|
|
85
|
+
*/
|
|
86
|
+
getActiveTasks(): Map<string, any>;
|
|
87
|
+
/**
|
|
88
|
+
* Remove task from active tasks
|
|
89
|
+
*/
|
|
90
|
+
removeActiveTask(taskId: string): void;
|
|
63
91
|
}
|
package/dist/websocket.js
CHANGED
|
@@ -11,38 +11,51 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
11
11
|
constructor(config) {
|
|
12
12
|
super();
|
|
13
13
|
this.ws = null;
|
|
14
|
-
this.
|
|
14
|
+
this.protocolHeartbeatInterval = null;
|
|
15
|
+
this.appHeartbeatInterval = null;
|
|
15
16
|
this.reconnectTimeout = null;
|
|
17
|
+
this.activeTasks = new Map(); // Track active tasks for cancellation
|
|
16
18
|
this.config = config;
|
|
17
19
|
this.auth = new auth_1.XiaoYiAuth(config.ak, config.sk, config.agentId);
|
|
18
20
|
this.state = {
|
|
19
21
|
connected: false,
|
|
20
22
|
authenticated: false,
|
|
21
23
|
lastHeartbeat: 0,
|
|
24
|
+
lastAppHeartbeat: 0,
|
|
22
25
|
reconnectAttempts: 0,
|
|
23
|
-
maxReconnectAttempts: 10
|
|
26
|
+
maxReconnectAttempts: 50, // Increased from 10 to 50
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
|
-
* Connect to XiaoYi WebSocket server
|
|
30
|
+
* Connect to XiaoYi WebSocket server with header authentication
|
|
28
31
|
*/
|
|
29
32
|
async connect() {
|
|
30
33
|
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
33
36
|
try {
|
|
34
|
-
|
|
37
|
+
// Generate authentication headers
|
|
38
|
+
const authHeaders = this.auth.generateAuthHeaders();
|
|
39
|
+
// Create WebSocket connection with headers
|
|
40
|
+
this.ws = new ws_1.default(this.config.wsUrl, {
|
|
41
|
+
headers: authHeaders,
|
|
42
|
+
});
|
|
35
43
|
this.setupWebSocketHandlers();
|
|
36
44
|
return new Promise((resolve, reject) => {
|
|
37
45
|
const timeout = setTimeout(() => {
|
|
38
46
|
reject(new Error("Connection timeout"));
|
|
39
|
-
},
|
|
47
|
+
}, 30000); // Increased timeout to 30 seconds
|
|
40
48
|
this.ws.once("open", () => {
|
|
41
49
|
clearTimeout(timeout);
|
|
42
50
|
this.state.connected = true;
|
|
51
|
+
this.state.authenticated = true; // Authenticated via headers
|
|
43
52
|
this.state.reconnectAttempts = 0;
|
|
44
53
|
this.emit("connected");
|
|
45
|
-
|
|
54
|
+
// Send clawd_bot_init message
|
|
55
|
+
this.sendInitMessage();
|
|
56
|
+
// Start heartbeats
|
|
57
|
+
this.startProtocolHeartbeat();
|
|
58
|
+
this.startAppHeartbeat();
|
|
46
59
|
resolve();
|
|
47
60
|
});
|
|
48
61
|
this.ws.once("error", (error) => {
|
|
@@ -67,23 +80,51 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
67
80
|
}
|
|
68
81
|
this.state.connected = false;
|
|
69
82
|
this.state.authenticated = false;
|
|
83
|
+
this.activeTasks.clear();
|
|
70
84
|
this.emit("disconnected");
|
|
71
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Send clawd_bot_init message on connection/reconnection
|
|
88
|
+
*/
|
|
89
|
+
sendInitMessage() {
|
|
90
|
+
const initMessage = {
|
|
91
|
+
msgType: "clawd_bot_init",
|
|
92
|
+
agentId: this.config.agentId,
|
|
93
|
+
};
|
|
94
|
+
this.sendMessage(initMessage);
|
|
95
|
+
console.log("Sent clawd_bot_init message");
|
|
96
|
+
}
|
|
72
97
|
/**
|
|
73
98
|
* Send A2A response message
|
|
74
99
|
*/
|
|
75
|
-
async sendResponse(response) {
|
|
100
|
+
async sendResponse(response, taskId, sessionId) {
|
|
76
101
|
if (!this.isReady()) {
|
|
77
102
|
throw new Error("WebSocket not ready");
|
|
78
103
|
}
|
|
79
104
|
const message = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
msgType: "agent_response",
|
|
106
|
+
agentId: this.config.agentId,
|
|
107
|
+
sessionId: sessionId,
|
|
108
|
+
taskId: taskId,
|
|
109
|
+
msgDetail: JSON.stringify(response),
|
|
85
110
|
};
|
|
86
|
-
this.
|
|
111
|
+
this.sendMessage(message);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Send generic outbound message
|
|
115
|
+
*/
|
|
116
|
+
sendMessage(message) {
|
|
117
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
118
|
+
console.error("Cannot send message: WebSocket not open");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
this.ws.send(JSON.stringify(message));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error("Failed to send message:", error);
|
|
126
|
+
this.emit("error", error);
|
|
127
|
+
}
|
|
87
128
|
}
|
|
88
129
|
/**
|
|
89
130
|
* Check if connection is ready for sending messages
|
|
@@ -137,64 +178,130 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
137
178
|
* Handle incoming WebSocket messages
|
|
138
179
|
*/
|
|
139
180
|
handleMessage(message) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.startHeartbeat();
|
|
150
|
-
this.emit("authenticated");
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
this.emit("authError", message.data.error);
|
|
154
|
-
}
|
|
155
|
-
break;
|
|
156
|
-
case "heartbeat":
|
|
157
|
-
this.handleHeartbeat();
|
|
158
|
-
break;
|
|
159
|
-
case "error":
|
|
160
|
-
this.emit("error", new Error(message.data.message || "Unknown error"));
|
|
161
|
-
break;
|
|
162
|
-
default:
|
|
163
|
-
console.warn("Unknown message type:", message.type);
|
|
181
|
+
// Validate agentId
|
|
182
|
+
if (message.agentId && message.agentId !== this.config.agentId) {
|
|
183
|
+
console.warn(`Received message with mismatched agentId: ${message.agentId}, expected: ${this.config.agentId}. Discarding.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Check if it's a clear message
|
|
187
|
+
if (message.action === "clear") {
|
|
188
|
+
this.handleClearMessage(message);
|
|
189
|
+
return;
|
|
164
190
|
}
|
|
191
|
+
// Check if it's a tasks/cancel message
|
|
192
|
+
if (message.action === "tasks/cancel") {
|
|
193
|
+
this.handleTasksCancelMessage(message);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Handle regular A2A request message
|
|
197
|
+
if (this.isA2ARequestMessage(message)) {
|
|
198
|
+
// Store task for potential cancellation
|
|
199
|
+
this.activeTasks.set(message.id, {
|
|
200
|
+
sessionId: message.sessionId,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
this.emit("message", message);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
console.warn("Received unknown message format:", message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Handle A2A clear message
|
|
211
|
+
* Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
|
|
212
|
+
*/
|
|
213
|
+
handleClearMessage(message) {
|
|
214
|
+
console.log(`Received clear message for session: ${message.sessionId}`);
|
|
215
|
+
// Send success response according to A2A spec
|
|
216
|
+
const response = {
|
|
217
|
+
sessionId: message.sessionId,
|
|
218
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
agentId: this.config.agentId,
|
|
221
|
+
sender: {
|
|
222
|
+
id: this.config.agentId,
|
|
223
|
+
name: "OpenClaw Agent",
|
|
224
|
+
type: "agent",
|
|
225
|
+
},
|
|
226
|
+
content: {
|
|
227
|
+
type: "text",
|
|
228
|
+
text: "Context cleared successfully",
|
|
229
|
+
},
|
|
230
|
+
status: "success",
|
|
231
|
+
};
|
|
232
|
+
// Send response
|
|
233
|
+
this.sendResponse(response, message.id, message.sessionId).catch(error => {
|
|
234
|
+
console.error("Failed to send clear response:", error);
|
|
235
|
+
});
|
|
236
|
+
// Emit clear event for application to handle
|
|
237
|
+
this.emit("clear", {
|
|
238
|
+
sessionId: message.sessionId,
|
|
239
|
+
id: message.id,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Handle A2A tasks/cancel message
|
|
244
|
+
* Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
|
|
245
|
+
*/
|
|
246
|
+
handleTasksCancelMessage(message) {
|
|
247
|
+
console.log(`Received tasks/cancel message for task: ${message.taskId}`);
|
|
248
|
+
// Emit cancel event for application to handle
|
|
249
|
+
this.emit("cancel", {
|
|
250
|
+
sessionId: message.sessionId,
|
|
251
|
+
taskId: message.taskId,
|
|
252
|
+
id: message.id,
|
|
253
|
+
});
|
|
254
|
+
// Note: We'll send the success response after OpenClaw confirms cancellation
|
|
255
|
+
// This will be handled by the channel plugin
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Send tasks/cancel success response
|
|
259
|
+
*/
|
|
260
|
+
async sendCancelSuccessResponse(sessionId, taskId, requestId) {
|
|
261
|
+
const response = {
|
|
262
|
+
sessionId: sessionId,
|
|
263
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
agentId: this.config.agentId,
|
|
266
|
+
sender: {
|
|
267
|
+
id: this.config.agentId,
|
|
268
|
+
name: "OpenClaw Agent",
|
|
269
|
+
type: "agent",
|
|
270
|
+
},
|
|
271
|
+
content: {
|
|
272
|
+
type: "text",
|
|
273
|
+
text: "Task cancelled successfully",
|
|
274
|
+
},
|
|
275
|
+
status: "success",
|
|
276
|
+
};
|
|
277
|
+
await this.sendResponse(response, requestId, sessionId);
|
|
278
|
+
// Remove from active tasks
|
|
279
|
+
this.activeTasks.delete(taskId);
|
|
165
280
|
}
|
|
166
281
|
/**
|
|
167
282
|
* Type guard for A2A request messages
|
|
168
283
|
*/
|
|
169
284
|
isA2ARequestMessage(data) {
|
|
170
285
|
return data &&
|
|
286
|
+
typeof data.agentId === "string" &&
|
|
171
287
|
typeof data.sessionId === "string" &&
|
|
288
|
+
typeof data.id === "string" &&
|
|
172
289
|
typeof data.messageId === "string" &&
|
|
173
290
|
typeof data.timestamp === "number" &&
|
|
174
291
|
data.sender &&
|
|
175
292
|
data.content;
|
|
176
293
|
}
|
|
177
294
|
/**
|
|
178
|
-
*
|
|
179
|
-
*/
|
|
180
|
-
authenticate() {
|
|
181
|
-
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const authMessage = this.auth.generateAuthMessage();
|
|
185
|
-
this.ws.send(JSON.stringify(authMessage));
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Start heartbeat mechanism
|
|
295
|
+
* Start protocol-level heartbeat (ping/pong)
|
|
189
296
|
*/
|
|
190
|
-
|
|
191
|
-
this.
|
|
297
|
+
startProtocolHeartbeat() {
|
|
298
|
+
this.protocolHeartbeatInterval = setInterval(() => {
|
|
192
299
|
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
193
300
|
this.ws.ping();
|
|
194
301
|
// Check if we haven't received a pong in too long
|
|
195
302
|
const now = Date.now();
|
|
196
|
-
if (this.state.lastHeartbeat > 0 && now - this.state.lastHeartbeat >
|
|
197
|
-
console.warn("
|
|
303
|
+
if (this.state.lastHeartbeat > 0 && now - this.state.lastHeartbeat > 90000) {
|
|
304
|
+
console.warn("Protocol heartbeat timeout, reconnecting...");
|
|
198
305
|
this.disconnect();
|
|
199
306
|
this.scheduleReconnect();
|
|
200
307
|
}
|
|
@@ -202,13 +309,22 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
202
309
|
}, 30000); // Send ping every 30 seconds
|
|
203
310
|
}
|
|
204
311
|
/**
|
|
205
|
-
*
|
|
312
|
+
* Start application-level heartbeat
|
|
206
313
|
*/
|
|
207
|
-
|
|
208
|
-
this.
|
|
314
|
+
startAppHeartbeat() {
|
|
315
|
+
this.appHeartbeatInterval = setInterval(() => {
|
|
316
|
+
if (this.isReady()) {
|
|
317
|
+
const heartbeatMessage = {
|
|
318
|
+
msgType: "heartbeat",
|
|
319
|
+
agentId: this.config.agentId,
|
|
320
|
+
};
|
|
321
|
+
this.sendMessage(heartbeatMessage);
|
|
322
|
+
this.state.lastAppHeartbeat = Date.now();
|
|
323
|
+
}
|
|
324
|
+
}, 20000); // Send application heartbeat every 20 seconds
|
|
209
325
|
}
|
|
210
326
|
/**
|
|
211
|
-
* Schedule reconnection attempt
|
|
327
|
+
* Schedule reconnection attempt with exponential backoff
|
|
212
328
|
*/
|
|
213
329
|
scheduleReconnect() {
|
|
214
330
|
if (this.state.reconnectAttempts >= this.state.maxReconnectAttempts) {
|
|
@@ -216,9 +332,10 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
216
332
|
this.emit("maxReconnectAttemptsReached");
|
|
217
333
|
return;
|
|
218
334
|
}
|
|
219
|
-
|
|
335
|
+
// Exponential backoff with longer intervals: 2s, 4s, 8s, 16s, 32s, 60s (max)
|
|
336
|
+
const delay = Math.min(2000 * Math.pow(2, this.state.reconnectAttempts), 60000);
|
|
220
337
|
this.state.reconnectAttempts++;
|
|
221
|
-
console.log(`Scheduling reconnect attempt ${this.state.reconnectAttempts} in ${delay}ms`);
|
|
338
|
+
console.log(`Scheduling reconnect attempt ${this.state.reconnectAttempts}/${this.state.maxReconnectAttempts} in ${delay}ms`);
|
|
222
339
|
this.reconnectTimeout = setTimeout(async () => {
|
|
223
340
|
try {
|
|
224
341
|
await this.connect();
|
|
@@ -233,14 +350,30 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
233
350
|
* Clear all timers
|
|
234
351
|
*/
|
|
235
352
|
clearTimers() {
|
|
236
|
-
if (this.
|
|
237
|
-
clearInterval(this.
|
|
238
|
-
this.
|
|
353
|
+
if (this.protocolHeartbeatInterval) {
|
|
354
|
+
clearInterval(this.protocolHeartbeatInterval);
|
|
355
|
+
this.protocolHeartbeatInterval = null;
|
|
356
|
+
}
|
|
357
|
+
if (this.appHeartbeatInterval) {
|
|
358
|
+
clearInterval(this.appHeartbeatInterval);
|
|
359
|
+
this.appHeartbeatInterval = null;
|
|
239
360
|
}
|
|
240
361
|
if (this.reconnectTimeout) {
|
|
241
362
|
clearTimeout(this.reconnectTimeout);
|
|
242
363
|
this.reconnectTimeout = null;
|
|
243
364
|
}
|
|
244
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Get active tasks
|
|
368
|
+
*/
|
|
369
|
+
getActiveTasks() {
|
|
370
|
+
return new Map(this.activeTasks);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Remove task from active tasks
|
|
374
|
+
*/
|
|
375
|
+
removeActiveTask(taskId) {
|
|
376
|
+
this.activeTasks.delete(taskId);
|
|
377
|
+
}
|
|
245
378
|
}
|
|
246
379
|
exports.XiaoYiWebSocketManager = XiaoYiWebSocketManager;
|