@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 CHANGED
@@ -22,7 +22,11 @@ export declare class XiaoYiAuth {
22
22
  */
23
23
  verifyCredentials(credentials: AuthCredentials): boolean;
24
24
  /**
25
- * Generate authentication message for WebSocket
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 message for WebSocket
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 { XiaoYiAccountConfig } from "./types";
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: XiaoYiAccountConfig;
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 - manage accounts
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) => string | undefined;
49
+ defaultAccountId: (cfg: any) => "default" | undefined;
49
50
  isConfigured: (account: any) => boolean;
50
51
  describeAccount: (account: any) => {
51
52
  accountId: any;
52
- name: any;
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: ["xy"],
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 - manage accounts
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.accounts) {
34
+ if (!channelConfig || !channelConfig.enabled) {
34
35
  return [];
35
36
  }
36
- return Object.keys(channelConfig.accounts);
37
+ // Single account mode: always return "default"
38
+ return ["default"];
37
39
  },
38
40
  resolveAccount: (cfg, accountId) => {
39
- const resolvedAccountId = accountId || "default";
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 || !channelConfig.accounts) {
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: accountConfig,
74
- enabled: accountConfig.enabled !== false,
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.accounts) {
67
+ if (!channelConfig || !channelConfig.enabled) {
80
68
  return undefined;
81
69
  }
82
- const accountIds = Object.keys(channelConfig.accounts);
83
- return accountIds.includes("default") ? "default" : accountIds[0];
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: account.config?.name || 'XiaoYi',
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(ctx.accountId);
101
+ const connection = runtime.getConnection();
114
102
  if (!connection || !connection.isReady()) {
115
- throw new Error(`XiaoYi account ${ctx.accountId} not connected`);
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: ctx.to, // Use 'to' as 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: ctx.to,
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(ctx.accountId);
143
+ const connection = runtime.getConnection();
152
144
  if (!connection || !connection.isReady()) {
153
- throw new Error(`XiaoYi account ${ctx.accountId} not connected`);
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: ctx.to,
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: ctx.to,
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.startAccount(resolvedAccount.accountId, resolvedAccount.config);
190
+ // Start WebSocket connection (single account mode)
191
+ await runtime.start(resolvedAccount.config);
196
192
  // Setup message handler
197
- const connection = runtime.getConnection(resolvedAccount.accountId);
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.sender.id,
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
- conversationId: message.context?.conversationId,
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.stopAccount(ctx.accountId);
235
+ runtime.stop();
224
236
  },
225
237
  probeAccount: async (ctx) => {
226
238
  const runtime = (0, runtime_1.getXiaoYiRuntime)();
227
- const isConnected = runtime.isAccountConnected(ctx.accountId);
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(ctx.accountId);
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
  },
@@ -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
- * "accounts": {
13
- * "default": {
14
- * "enabled": true,
15
- * "wsUrl": "wss://hag.com/ws/link",
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
- * "accounts": {
17
- * "default": {
18
- * "enabled": true,
19
- * "wsUrl": "wss://hag.com/ws/link",
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 { XiaoYiAccountConfig } from "./types";
2
+ import { XiaoYiChannelConfig } from "./types";
3
3
  /**
4
4
  * Runtime state for XiaoYi channel
5
- * Manages WebSocket connections for each account
5
+ * Manages single WebSocket connection (single account mode)
6
6
  */
7
7
  export declare class XiaoYiRuntime {
8
- private connections;
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 connection
21
+ * Start connection (single account mode)
20
22
  */
21
- startAccount(accountId: string, config: XiaoYiAccountConfig): Promise<void>;
23
+ start(config: XiaoYiChannelConfig): Promise<void>;
22
24
  /**
23
- * Stop account connection
25
+ * Stop connection
24
26
  */
25
- stopAccount(accountId: string): void;
27
+ stop(): void;
26
28
  /**
27
- * Get WebSocket manager for account
29
+ * Get WebSocket manager
28
30
  */
29
- getConnection(accountId: string): XiaoYiWebSocketManager | undefined;
31
+ getConnection(): XiaoYiWebSocketManager | null;
30
32
  /**
31
- * Check if account is connected
33
+ * Check if connected
32
34
  */
33
- isAccountConnected(accountId: string): boolean;
35
+ isConnected(): boolean;
34
36
  /**
35
- * Get all connected account IDs
37
+ * Get configuration
36
38
  */
37
- getConnectedAccounts(): string[];
39
+ getConfig(): XiaoYiChannelConfig | null;
38
40
  /**
39
- * Stop all accounts
41
+ * Set taskId for a session
40
42
  */
41
- stopAll(): void;
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 connections for each account
9
+ * Manages single WebSocket connection (single account mode)
10
10
  */
11
11
  class XiaoYiRuntime {
12
12
  constructor() {
13
- this.connections = new Map();
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 connection
31
+ * Start connection (single account mode)
30
32
  */
31
- async startAccount(accountId, config) {
32
- if (this.connections.has(accountId)) {
33
- console.log(`Account ${accountId} already connected`);
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(accountId, message);
42
+ this.handleIncomingMessage(message);
40
43
  });
41
44
  manager.on("error", (error) => {
42
- console.error(`XiaoYi account ${accountId} error:`, error);
45
+ console.error("XiaoYi channel error:", error);
43
46
  });
44
47
  manager.on("disconnected", () => {
45
- console.log(`XiaoYi account ${accountId} disconnected`);
48
+ console.log("XiaoYi channel disconnected");
46
49
  });
47
50
  manager.on("authenticated", () => {
48
- console.log(`XiaoYi account ${accountId} authenticated`);
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(`XiaoYi account ${accountId} max reconnect attempts reached`);
52
- this.stopAccount(accountId);
60
+ console.error("XiaoYi channel max reconnect attempts reached");
61
+ this.stop();
53
62
  });
54
63
  // Connect
55
64
  await manager.connect();
56
- this.connections.set(accountId, manager);
57
- console.log(`XiaoYi account ${accountId} started`);
65
+ this.connection = manager;
66
+ console.log("XiaoYi channel started");
58
67
  }
59
68
  /**
60
- * Stop account connection
69
+ * Stop connection
61
70
  */
62
- stopAccount(accountId) {
63
- const manager = this.connections.get(accountId);
64
- if (manager) {
65
- manager.disconnect();
66
- this.connections.delete(accountId);
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 for account
81
+ * Get WebSocket manager
72
82
  */
73
- getConnection(accountId) {
74
- return this.connections.get(accountId);
83
+ getConnection() {
84
+ return this.connection;
75
85
  }
76
86
  /**
77
- * Check if account is connected
87
+ * Check if connected
78
88
  */
79
- isAccountConnected(accountId) {
80
- const manager = this.connections.get(accountId);
81
- return manager ? manager.isReady() : false;
89
+ isConnected() {
90
+ return this.connection ? this.connection.isReady() : false;
82
91
  }
83
92
  /**
84
- * Get all connected account IDs
93
+ * Get configuration
85
94
  */
86
- getConnectedAccounts() {
87
- return Array.from(this.connections.keys()).filter((accountId) => this.isAccountConnected(accountId));
95
+ getConfig() {
96
+ return this.config;
88
97
  }
89
98
  /**
90
- * Stop all accounts
99
+ * Set taskId for a session
91
100
  */
92
- stopAll() {
93
- for (const accountId of this.connections.keys()) {
94
- this.stopAccount(accountId);
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(accountId, message) {
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 interface XiaoYiChannelConfig {
58
- enabled: boolean;
59
- accounts: Record<string, XiaoYiAccountConfig>;
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 XiaoYiAccountConfig {
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
  }
@@ -1,25 +1,35 @@
1
1
  import { EventEmitter } from "events";
2
- import { A2AResponseMessage, WebSocketConnectionState, XiaoYiAccountConfig } from "./types";
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 heartbeatInterval;
8
+ private protocolHeartbeatInterval;
9
+ private appHeartbeatInterval;
9
10
  private reconnectTimeout;
10
- constructor(config: XiaoYiAccountConfig);
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
- * Type guard for A2A request messages
50
+ * Handle A2A clear message
51
+ * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
41
52
  */
42
- private isA2ARequestMessage;
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
- * Authenticate with the server
60
+ * Send tasks/cancel success response
45
61
  */
46
- private authenticate;
62
+ sendCancelSuccessResponse(sessionId: string, taskId: string, requestId: string): Promise<void>;
47
63
  /**
48
- * Start heartbeat mechanism
64
+ * Type guard for A2A request messages
65
+ */
66
+ private isA2ARequestMessage;
67
+ /**
68
+ * Start protocol-level heartbeat (ping/pong)
49
69
  */
50
- private startHeartbeat;
70
+ private startProtocolHeartbeat;
51
71
  /**
52
- * Handle heartbeat response
72
+ * Start application-level heartbeat
53
73
  */
54
- private handleHeartbeat;
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.heartbeatInterval = null;
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
- this.ws = new ws_1.default(this.config.wsUrl);
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
- }, 10000);
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
- this.authenticate();
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
- type: "message",
81
- data: {
82
- ...response,
83
- agentId: this.config.agentId, // Add agentId to response
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.ws.send(JSON.stringify(message));
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
- switch (message.type) {
141
- case "message":
142
- if (this.isA2ARequestMessage(message.data)) {
143
- this.emit("message", message.data);
144
- }
145
- break;
146
- case "auth":
147
- if (message.data.success) {
148
- this.state.authenticated = true;
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
- * Authenticate with the server
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
- startHeartbeat() {
191
- this.heartbeatInterval = setInterval(() => {
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 > 60000) {
197
- console.warn("Heartbeat timeout, reconnecting...");
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
- * Handle heartbeat response
312
+ * Start application-level heartbeat
206
313
  */
207
- handleHeartbeat() {
208
- this.state.lastHeartbeat = Date.now();
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
- const delay = Math.min(1000 * Math.pow(2, this.state.reconnectAttempts), 30000);
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.heartbeatInterval) {
237
- clearInterval(this.heartbeatInterval);
238
- this.heartbeatInterval = null;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "XiaoYi channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",