@honor-claw/yoyo 0.0.1-beta.2 → 0.0.1-beta.21

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.
Files changed (79) hide show
  1. package/index.ts +2 -2
  2. package/openclaw.plugin.json +7 -0
  3. package/package.json +20 -20
  4. package/skills/search/SKILL.md +182 -0
  5. package/skills/search/scripts/search.sh +69 -0
  6. package/skills/yoyo-control/SKILL.md +105 -120
  7. package/skills/yoyo-control/references/alarm-create.md +473 -0
  8. package/skills/yoyo-control/references/app-close.md +183 -0
  9. package/skills/yoyo-control/references/app-open.md +178 -0
  10. package/skills/yoyo-control/references/call-phone.md +250 -0
  11. package/skills/yoyo-control/references/capture-screenshot.md +205 -54
  12. package/skills/yoyo-control/references/contact-search.md +235 -0
  13. package/skills/yoyo-control/references/hotspot.md +208 -0
  14. package/skills/yoyo-control/references/local-search.md +224 -15
  15. package/skills/yoyo-control/references/message-send.md +246 -0
  16. package/skills/yoyo-control/references/mobile-data.md +248 -0
  17. package/skills/yoyo-control/references/no-disturb.md +239 -0
  18. package/skills/yoyo-control/references/quiet-mode.md +228 -0
  19. package/skills/yoyo-control/references/ringing-mode.md +223 -0
  20. package/skills/yoyo-control/references/screen-record.md +220 -0
  21. package/skills/yoyo-control/references/vibration-mode.md +235 -0
  22. package/skills/yoyo-control/references/volume-operate.md +274 -0
  23. package/skills/yoyo-control/scripts/invoke.js +33 -111
  24. package/src/agent/copy-templates.ts +56 -0
  25. package/src/agent/index.ts +3 -0
  26. package/src/agent/templates/AGENTS.md +223 -0
  27. package/src/apis/claw-cloud.ts +70 -23
  28. package/src/apis/honor-auth.ts +20 -10
  29. package/src/apis/types.ts +24 -1
  30. package/src/cloud-channel/channel.ts +245 -58
  31. package/src/cloud-channel/client.ts +87 -12
  32. package/src/cloud-channel/types.ts +30 -0
  33. package/src/commands/env/impl.ts +58 -0
  34. package/src/commands/env/index.ts +1 -0
  35. package/src/commands/index.ts +11 -1
  36. package/src/commands/login/impl.ts +17 -8
  37. package/src/commands/logout/impl.ts +23 -0
  38. package/src/commands/logout/index.ts +1 -53
  39. package/src/commands/status/index.ts +172 -42
  40. package/src/gateway-client/client.deprecated.ts +1 -1
  41. package/src/gateway-client/client.ts +15 -20
  42. package/src/gateway-client/types.ts +2 -2
  43. package/src/honor-auth/browser.ts +12 -15
  44. package/src/honor-auth/callback-server.ts +3 -6
  45. package/src/honor-auth/cloud.ts +65 -12
  46. package/src/honor-auth/config.ts +25 -17
  47. package/src/honor-auth/index.ts +1 -0
  48. package/src/honor-auth/token-manager.ts +24 -14
  49. package/src/modules/claw-configs/config-manager.ts +211 -11
  50. package/src/modules/claw-configs/hosts.ts +48 -0
  51. package/src/modules/claw-configs/index.ts +1 -0
  52. package/src/modules/claw-configs/types.ts +4 -0
  53. package/src/modules/device/device-info.ts +20 -9
  54. package/src/modules/device/providers/linux.ts +128 -0
  55. package/src/modules/device/providers/macos.ts +123 -0
  56. package/src/modules/device/providers/pad.ts +0 -16
  57. package/src/modules/device/registry.ts +12 -3
  58. package/src/modules/login/impl.ts +38 -16
  59. package/src/runtime.ts +44 -0
  60. package/src/schemas.ts +4 -1
  61. package/src/services/connection/impl.ts +89 -9
  62. package/src/services/connection/status-tracker/events.ts +127 -0
  63. package/src/services/connection/status-tracker/index.ts +31 -0
  64. package/src/services/connection/status-tracker/storage.ts +133 -0
  65. package/src/services/connection/status-tracker/tracker.ts +370 -0
  66. package/src/services/connection/status-tracker/types.ts +131 -0
  67. package/src/types.ts +0 -4
  68. package/src/utils/fs-safe.ts +544 -0
  69. package/src/utils/version.ts +29 -0
  70. package/src/utils/ws.ts +21 -0
  71. package/skills/yoyo-control/references/open-app.md +0 -54
  72. package/skills/yoyo-control/references/phone-call.md +0 -217
  73. package/skills/yoyo-control/references/schedule.md +0 -107
  74. package/skills/yoyo-control/references/screen-recorder.md +0 -67
  75. package/skills/yoyo-control/references/search-contact.md +0 -37
  76. package/skills/yoyo-control/references/send-message.md +0 -155
  77. package/skills/yoyo-control/references/volume.md +0 -536
  78. package/skills/yoyo-control/scripts/README.md +0 -103
  79. package/skills/yoyo-control/scripts/volume-up.json +0 -7
@@ -1,10 +1,12 @@
1
- import type { ClawChannelConfig, YoyoClawMessage, ClawSocketSourceInfo } from './types.js';
2
- import { ClawCloudSocketClient } from './client.js';
3
- import { GatewayClient } from '../gateway-client/client.js';
4
- import { useClawLogger } from '../utils/logger.js';
5
-
6
- const DEFAULT_WS_SERVER_URL =
7
- 'ws://omni-pre-drcn.hiboard.hihonorcloud.com/aicloud/yoyo-claw-service/v1/yoyoclaw/fullduplex';
1
+ import type {
2
+ ClawChannelConfig,
3
+ YoyoClawMessage,
4
+ DeviceSessionInfo,
5
+ } from "./types.js";
6
+ import { ClawCloudSocketClient } from "./client.js";
7
+ import { GatewayClient } from "../gateway-client/client.js";
8
+ import { useClawLogger } from "../utils/logger.js";
9
+ import { takeApiHost } from "../modules/claw-configs/hosts.js";
8
10
 
9
11
  /**
10
12
  * 新的Channel实现
@@ -15,14 +17,22 @@ const DEFAULT_WS_SERVER_URL =
15
17
  export class ClawChannel {
16
18
  private cloudClient: ClawCloudSocketClient;
17
19
  private gatewayClientsMap = new Map<string, GatewayClient>();
18
- private deviceIdToSourceInfoMap = new Map<string, ClawSocketSourceInfo>();
20
+ private deviceSessionsMap = new Map<string, DeviceSessionInfo[]>();
21
+ private sessionIdToHardwareDeviceMap = new Map<string, string>();
19
22
  private config: ClawChannelConfig;
20
23
 
21
24
  constructor(config: ClawChannelConfig) {
22
25
  this.config = config;
23
26
 
27
+ const hosts = takeApiHost();
28
+
29
+ // 如果有灰度标签,设置额外的请求头
30
+ const extraHeaders = hosts.grayTag ? {
31
+ 'x-gray': hosts.grayTag
32
+ } : undefined;
33
+
24
34
  this.cloudClient = new ClawCloudSocketClient({
25
- serverUrl: DEFAULT_WS_SERVER_URL,
35
+ serverUrl: `wss://${hosts.clawCloud}/aicloud/yoyo-claw-service/v1/yoyoclaw/fullduplex`,
26
36
  deviceInfo: config.deviceInfo,
27
37
  userInfo: config.userInfo,
28
38
  onMessage: this.handleCloudMessage,
@@ -30,6 +40,8 @@ export class ClawChannel {
30
40
  onClose: this.handleCloudClose,
31
41
  onRemoteDeviceOffline: this.handleRemoteDeviceOffline,
32
42
  onDeviceNotRegistered: this.handleDeviceNotRegistered,
43
+ extraHeaders,
44
+ onStatusEvent: this.handleStatusEvent,
33
45
  });
34
46
  }
35
47
 
@@ -37,7 +49,7 @@ export class ClawChannel {
37
49
  * 启动连接
38
50
  */
39
51
  start(): void {
40
- useClawLogger().info('[yoyoclaw-channel] starting connection');
52
+ useClawLogger().info("[yoyoclaw-channel] starting connection");
41
53
  this.cloudClient.connect();
42
54
  }
43
55
 
@@ -45,7 +57,7 @@ export class ClawChannel {
45
57
  * 关闭连接
46
58
  */
47
59
  destroy(): void {
48
- useClawLogger().info('[yoyoclaw-channel] closing connection');
60
+ useClawLogger().info("[yoyoclaw-channel] closing connection");
49
61
  this.closeAllGatewayClients();
50
62
  this.cloudClient.close();
51
63
  }
@@ -54,7 +66,7 @@ export class ClawChannel {
54
66
  * 处理云侧连接打开
55
67
  */
56
68
  private handleCloudOpen = () => {
57
- useClawLogger().info('[yoyoclaw-channel] cloud connection established');
69
+ useClawLogger().info("[yoyoclaw-channel] cloud connection established");
58
70
  this.config.onOpen?.();
59
71
  };
60
72
 
@@ -62,7 +74,7 @@ export class ClawChannel {
62
74
  * 处理云侧连接关闭
63
75
  */
64
76
  private handleCloudClose = () => {
65
- useClawLogger().info('[yoyoclaw-channel] cloud connection closed');
77
+ useClawLogger().info("[yoyoclaw-channel] cloud connection closed");
66
78
  this.config.onClose?.();
67
79
  };
68
80
 
@@ -70,38 +82,38 @@ export class ClawChannel {
70
82
  * 处理对向设备离线
71
83
  */
72
84
  private handleRemoteDeviceOffline = (sourceDeviceId: string) => {
73
- // 关闭对应的 GatewayClient 连接
74
- const gatewayClient = this.gatewayClientsMap.get(sourceDeviceId);
75
- if (gatewayClient) {
76
- gatewayClient.stop();
77
- this.gatewayClientsMap.delete(sourceDeviceId);
78
- this.deviceIdToSourceInfoMap.delete(sourceDeviceId);
79
- useClawLogger().info(`[yoyoclaw-channel] closed gateway client for offline device: ${sourceDeviceId}`);
80
- } else {
81
- useClawLogger().warn(`[yoyoclaw-channel] no gateway client found for offline device: ${sourceDeviceId}`);
82
- }
85
+ this.closeGatewayClient(sourceDeviceId);
83
86
  };
84
87
 
85
88
  /**
86
89
  * 处理当前设备未注册
87
90
  */
88
91
  private handleDeviceNotRegistered = () => {
89
- useClawLogger().info('[yoyoclaw-channel] device not registered, notifying connection layer');
92
+ useClawLogger().info(
93
+ "[yoyoclaw-channel] device not registered, notifying connection layer"
94
+ );
90
95
  this.config.onDeviceNotRegistered?.();
91
96
  };
92
97
 
98
+ /**
99
+ * 处理状态事件(转发到上层)
100
+ */
101
+ private handleStatusEvent = (event: any) => {
102
+ this.config.onStatusEvent?.(event);
103
+ };
104
+
93
105
  /**
94
106
  * 处理来自云侧的消息
95
107
  */
96
108
  private handleCloudMessage = (message: YoyoClawMessage) => {
97
109
  // 处理配对消息
98
- if (message.msgType === 'devicePairMessage') {
110
+ if (message.msgType === "devicePairMessage") {
99
111
  this.handlePairMessage(message);
100
112
  return;
101
113
  }
102
114
 
103
115
  // 处理用户消息,转发到对应的GatewayClient
104
- if (message.msgType === 'userMessage') {
116
+ if (message.msgType === "userMessage") {
105
117
  this.handleUserMessage(message);
106
118
  }
107
119
  };
@@ -110,35 +122,109 @@ export class ClawChannel {
110
122
  * 处理配对消息,创建GatewayClient连接
111
123
  */
112
124
  private handlePairMessage(message: YoyoClawMessage): void {
113
- if (message.msgType !== 'devicePairMessage') {
125
+ if (message.msgType !== "devicePairMessage") {
114
126
  return;
115
127
  }
116
128
 
117
129
  try {
118
- const { sourceDeviceId, sourceDeviceInfo, targetDeviceId, port } = message;
130
+ const {
131
+ sourceDeviceId,
132
+ sourceDeviceInfo,
133
+ targetDeviceId,
134
+ port,
135
+ sessionInfo,
136
+ } = message;
137
+
138
+ if (
139
+ sourceDeviceId &&
140
+ targetDeviceId &&
141
+ port &&
142
+ sourceDeviceInfo?.deviceId
143
+ ) {
144
+ const hardwareDeviceId = sourceDeviceInfo.deviceId;
145
+ const newTimestamp = sessionInfo?.nodeConnectTimestamp;
146
+
147
+ if (!newTimestamp) {
148
+ // 兜底旧版本,异常场景,暂时不移除旧连接
149
+ useClawLogger().warn(
150
+ `[yoyoclaw-channel] pair message missing timestamp for device: ${sourceDeviceId}`
151
+ );
152
+ } else {
153
+ // 关闭该硬件设备ID对应的所有前序 GatewayClient(timestamp 小于新 timestamp)
154
+ this.closePreviousGatewayClients(hardwareDeviceId, newTimestamp);
155
+ }
119
156
 
120
- if (sourceDeviceId && targetDeviceId && port) {
121
157
  useClawLogger().info(
122
- `[yoyoclaw-channel] received pair message, creating gateway client for device: ${sourceDeviceId}`
158
+ `[yoyoclaw-channel] received pair message, creating gateway client for device: ${sourceDeviceId}, hardware device: ${hardwareDeviceId}, timestamp: ${newTimestamp}`
123
159
  );
124
160
 
125
- // 保存设备ID对应的源信息
126
- this.deviceIdToSourceInfoMap.set(sourceDeviceId, {
127
- sourceRole: sourceDeviceInfo?.role ?? 'node',
128
- sourceDeviceId: sourceDeviceId,
129
- sourceDeviceInfo: sourceDeviceInfo,
161
+ // 创建会话信息
162
+ const sessionInfoData: DeviceSessionInfo = {
163
+ sessionId: sourceDeviceId,
164
+ timestamp: newTimestamp,
165
+ sourceInfo: {
166
+ sourceRole: sourceDeviceInfo?.role ?? "node",
167
+ sourceDeviceId: sourceDeviceId,
168
+ sourceDeviceInfo: sourceDeviceInfo,
169
+ },
170
+ };
171
+
172
+ // 保存会话信息
173
+ const sessions = this.deviceSessionsMap.get(hardwareDeviceId) || [];
174
+ sessions.push(sessionInfoData);
175
+ this.deviceSessionsMap.set(hardwareDeviceId, sessions);
176
+ this.sessionIdToHardwareDeviceMap.set(sourceDeviceId, hardwareDeviceId);
177
+
178
+ // 通知状态跟踪器:设备配对
179
+ this.config.onStatusEvent?.({
180
+ type: "device_pairing",
181
+ timestamp: Date.now(),
182
+ data: {
183
+ sessionId: sourceDeviceId,
184
+ hardwareDeviceId: hardwareDeviceId,
185
+ deviceInfo: sourceDeviceInfo,
186
+ pairedAt: new Date().toISOString(),
187
+ },
130
188
  });
131
189
 
132
190
  // 创建GatewayClient
133
191
  const gatewayClient = new GatewayClient({
134
192
  onOpen: () => {
135
- useClawLogger().info(`[yoyoclaw-channel] gateway client connected for device: ${sourceDeviceId}`);
193
+ useClawLogger().info(
194
+ `[yoyoclaw-channel] gateway client connected for device: ${sourceDeviceId}`
195
+ );
196
+
197
+ // 通知状态跟踪器:Gateway连接成功
198
+ this.config.onStatusEvent?.({
199
+ type: "gateway_client_connected",
200
+ timestamp: Date.now(),
201
+ data: {
202
+ sessionId: sourceDeviceId,
203
+ hardwareDeviceId: hardwareDeviceId,
204
+ deviceInfo: sourceDeviceInfo,
205
+ connectedAt: new Date().toISOString(),
206
+ },
207
+ });
136
208
  },
137
- onMessage: data => {
209
+ onMessage: (data) => {
138
210
  this.handleGatewayMessage(sourceDeviceId, data);
139
211
  },
140
- onClose: () => {
141
- useClawLogger().info(`[yoyoclaw-channel] gateway client closed for device: ${sourceDeviceId}`);
212
+ onClose: (reason) => {
213
+ useClawLogger().info(
214
+ `[yoyoclaw-channel] gateway client closed for device: ${sourceDeviceId}, with ${reason}`
215
+ );
216
+
217
+ // 通知状态跟踪器:Gateway连接断开
218
+ this.config.onStatusEvent?.({
219
+ type: "gateway_client_disconnected",
220
+ timestamp: Date.now(),
221
+ data: {
222
+ sessionId: sourceDeviceId,
223
+ hardwareDeviceId: hardwareDeviceId,
224
+ reason: reason,
225
+ disconnectedAt: new Date().toISOString(),
226
+ },
227
+ });
142
228
  },
143
229
  });
144
230
 
@@ -146,7 +232,9 @@ export class ClawChannel {
146
232
  gatewayClient.start();
147
233
  }
148
234
  } catch (error) {
149
- useClawLogger().error(`[yoyoclaw-channel] failed to handle pair message: ${String(error)}`);
235
+ useClawLogger().error(
236
+ `[yoyoclaw-channel] failed to handle pair message: ${String(error)}`
237
+ );
150
238
  }
151
239
  }
152
240
 
@@ -155,24 +243,38 @@ export class ClawChannel {
155
243
  */
156
244
  private handleUserMessage(message: YoyoClawMessage): void {
157
245
  if (!message.sourceDeviceId) {
158
- useClawLogger().warn('[yoyoclaw-channel] user message missing sourceDeviceId');
246
+ useClawLogger().warn(
247
+ "[yoyoclaw-channel] user message missing sourceDeviceId"
248
+ );
159
249
  return;
160
250
  }
161
251
 
252
+ const hardwareDeviceId = this.sessionIdToHardwareDeviceMap.get(
253
+ message.sourceDeviceId
254
+ );
255
+
162
256
  try {
163
257
  const gatewayClient = this.gatewayClientsMap.get(message.sourceDeviceId);
164
258
  if (!gatewayClient) {
165
- useClawLogger().warn(`[yoyoclaw-channel] no gateway client found for source device: ${message.sourceDeviceId}`);
259
+ useClawLogger().warn(
260
+ `[yoyoclaw-channel] no gateway client found for source device: ${hardwareDeviceId}, session: ${message.sourceDeviceId}`
261
+ );
166
262
  return;
167
263
  }
168
264
 
169
265
  // 只发送data部分
170
266
  if (message.data) {
171
- useClawLogger().info(`[yoyoclaw-channel] forwarding user message to gateway from ${message.sourceDeviceId}`);
267
+ useClawLogger().info(
268
+ `[yoyoclaw-channel] forwarding user message to gateway from ${hardwareDeviceId}, session: ${message.sourceDeviceId}`
269
+ );
172
270
  gatewayClient.send(message.data);
173
271
  }
174
272
  } catch (error) {
175
- useClawLogger().error(`[yoyoclaw-channel] failed to handle user message: ${String(error)}`);
273
+ useClawLogger().error(
274
+ `[yoyoclaw-channel] failed to send gateway message to ${hardwareDeviceId}: ${String(
275
+ error
276
+ )}, session: ${message.sourceDeviceId}`
277
+ );
176
278
  }
177
279
  }
178
280
 
@@ -180,39 +282,62 @@ export class ClawChannel {
180
282
  * 处理来自Gateway的消息,转发到云侧
181
283
  */
182
284
  private handleGatewayMessage(sourceDeviceId: string, data: string): void {
183
- const sourceInfo = this.deviceIdToSourceInfoMap.get(sourceDeviceId);
285
+ const hardwareDeviceId =
286
+ this.sessionIdToHardwareDeviceMap.get(sourceDeviceId);
287
+ if (!hardwareDeviceId) {
288
+ useClawLogger().warn(
289
+ `[yoyoclaw-channel] gateway source node offline, session: ${sourceDeviceId}`
290
+ );
291
+ return;
292
+ }
184
293
 
185
- if (!sourceInfo) {
186
- useClawLogger().warn('[yoyoclaw-channel] gateway source node offline');
294
+ const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
295
+ const sessionInfo = sessions?.find((s) => s.sessionId === sourceDeviceId);
296
+
297
+ if (!sessionInfo) {
298
+ useClawLogger().warn(
299
+ `[yoyoclaw-channel] gateway source node offline, session: ${sourceDeviceId}`
300
+ );
187
301
  return;
188
302
  }
189
303
 
304
+ useClawLogger().debug?.(
305
+ `[yoyoclaw-channel] received gateway message from ${
306
+ sessionInfo.sourceInfo.sourceDeviceInfo?.deviceId
307
+ } to cloud:, ${data.slice(0, 1000)}`
308
+ );
309
+
190
310
  try {
191
311
  // 将Gateway的消息封装成YoyoClawMessage发送到云侧
192
312
  // 使用当前接收到的socket对应deviceId的来源信息
193
313
  const cloudMessage: YoyoClawMessage = {
194
- msgType: 'userMessage',
195
- sourceRole: 'yoyoclaw',
314
+ msgType: "userMessage",
315
+ sourceRole: "yoyoclaw",
196
316
  sourceDeviceId: this.config.deviceInfo.deviceId,
197
- targetRole: 'node',
198
- targetDeviceId: sourceInfo.sourceDeviceId,
317
+ targetRole: "node",
318
+ targetDeviceId: sessionInfo.sourceInfo.sourceDeviceId,
199
319
  port: this.config.deviceInfo.port,
200
320
  data,
201
321
  };
202
322
 
203
- const result = this.cloudClient.send(cloudMessage);
323
+ const result = this.cloudClient.send(
324
+ cloudMessage,
325
+ sessionInfo.sourceInfo.sourceDeviceInfo?.deviceId
326
+ );
204
327
 
205
328
  if (!result) {
206
329
  // 云socket挂了,这个gateway连接就需要取消掉
207
330
  // 最好不要一次性把所有的gateway连接都干掉,让惰性关闭
208
331
  useClawLogger().error(
209
- `[yoyoclaw-channel] failed to send message, cloud socket closed, from: ${sourceInfo.sourceDeviceId}`
332
+ `[yoyoclaw-channel] failed to send message, cloud socket closed, from: ${sessionInfo.sourceInfo.sourceDeviceId}`
210
333
  );
211
334
 
212
- this.handleRemoteDeviceOffline(sourceInfo.sourceDeviceId);
335
+ this.handleRemoteDeviceOffline(sessionInfo.sourceInfo.sourceDeviceId);
213
336
  }
214
337
  } catch (error) {
215
- useClawLogger().error(`[yoyoclaw-channel] failed to handle gateway message: ${String(error)}`);
338
+ useClawLogger().error(
339
+ `[yoyoclaw-channel] failed to handle gateway message: ${String(error)}`
340
+ );
216
341
  }
217
342
  }
218
343
 
@@ -220,11 +345,73 @@ export class ClawChannel {
220
345
  * 关闭所有GatewayClient连接
221
346
  */
222
347
  private closeAllGatewayClients(): void {
223
- for (const [deviceId, client] of this.gatewayClientsMap.entries()) {
348
+ for (const [sessionId, client] of this.gatewayClientsMap.entries()) {
224
349
  client.stop();
225
- useClawLogger().info(`[yoyoclaw-channel] closed gateway client for device: ${deviceId}`);
350
+ useClawLogger().info(
351
+ `[yoyoclaw-channel] closed gateway client for session: ${sessionId}`
352
+ );
226
353
  }
227
354
  this.gatewayClientsMap.clear();
228
- this.deviceIdToSourceInfoMap.clear();
355
+ this.deviceSessionsMap.clear();
356
+ this.sessionIdToHardwareDeviceMap.clear();
357
+ }
358
+
359
+ /**
360
+ * 关闭硬件设备的所有前序 GatewayClient(timestamp 小于指定值)
361
+ */
362
+ private closePreviousGatewayClients(
363
+ hardwareDeviceId: string,
364
+ newTimestamp: number
365
+ ): void {
366
+ const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
367
+ if (!sessions || sessions.length === 0) {
368
+ return;
369
+ }
370
+
371
+ // 找出所有 timestamp 小于新 timestamp 的会话
372
+ const previousSessions = sessions.filter((s) => s.timestamp < newTimestamp);
373
+
374
+ if (previousSessions.length > 0) {
375
+ useClawLogger().info(
376
+ `[yoyoclaw-channel] closing ${previousSessions.length} previous gateway client(s) for hardware device: ${hardwareDeviceId}, new timestamp: ${newTimestamp}`
377
+ );
378
+
379
+ for (const session of previousSessions) {
380
+ this.closeGatewayClient(session.sessionId);
381
+ }
382
+ }
383
+ }
384
+
385
+ /**
386
+ * 关闭单个 GatewayClient
387
+ */
388
+ private closeGatewayClient(sessionId: string): void {
389
+ const gatewayClient = this.gatewayClientsMap.get(sessionId);
390
+ if (gatewayClient) {
391
+ gatewayClient.stop();
392
+ this.gatewayClientsMap.delete(sessionId);
393
+
394
+ // 从会话映射中移除
395
+ const hardwareDeviceId = this.sessionIdToHardwareDeviceMap.get(sessionId);
396
+ if (hardwareDeviceId) {
397
+ const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
398
+ if (sessions) {
399
+ const index = sessions.findIndex((s) => s.sessionId === sessionId);
400
+ if (index !== -1) {
401
+ sessions.splice(index, 1);
402
+ }
403
+
404
+ // 如果没有会话了,清理该硬件设备的记录
405
+ if (sessions.length === 0) {
406
+ this.deviceSessionsMap.delete(hardwareDeviceId);
407
+ }
408
+ }
409
+ this.sessionIdToHardwareDeviceMap.delete(sessionId);
410
+ }
411
+
412
+ useClawLogger().info(
413
+ `[yoyoclaw-channel] closed gateway client for session: ${sessionId}`
414
+ );
415
+ }
229
416
  }
230
417
  }
@@ -2,7 +2,6 @@
2
2
  * WebSocket Client 实现
3
3
  * 使用轻量级的 ws 库连接到远程服务器
4
4
  */
5
-
6
5
  import WebSocket from "ws";
7
6
  import type {
8
7
  ClawSocketClientOptions,
@@ -11,8 +10,8 @@ import type {
11
10
  } from "./types.js";
12
11
  import { createWebSocketProxyAgent } from "../utils/proxy.js";
13
12
  import { generateAuthorization } from "../utils/jwt.js";
14
- import { rawDataToString } from "openclaw/plugin-sdk";
15
13
  import { useClawLogger } from "../utils/logger.js";
14
+ import { rawDataToString } from "../utils/ws.js";
16
15
 
17
16
  const PING_INTERVAL = 30000;
18
17
  const MAX_RETRIES = 5;
@@ -44,12 +43,21 @@ export class ClawCloudSocketClient {
44
43
  // 清除可能存在的重试定时器
45
44
  this.clearRetryTimer();
46
45
 
47
- const { serverUrl, port, deviceInfo, userInfo } = this.options;
46
+ const { serverUrl, port, deviceInfo, userInfo, extraHeaders } = this.options;
48
47
 
49
48
  // 确保 URL 包含路由
50
49
  let url = serverUrl.replace(/\/$/, "");
51
50
  url = port ? `${url}:${port}` : url;
52
51
 
52
+ // 通知状态跟踪器:开始连接
53
+ this.options.onStatusEvent?.({
54
+ type: "cloud_socket_connecting",
55
+ timestamp: Date.now(),
56
+ data: {
57
+ url: url,
58
+ },
59
+ });
60
+
53
61
  // 构建请求头
54
62
  const headers: Record<string, string> = {
55
63
  ["x-role"]: "yoyoclaw",
@@ -68,6 +76,11 @@ export class ClawCloudSocketClient {
68
76
  headers["x-port"] = String(deviceInfo.port);
69
77
  }
70
78
 
79
+ // 添加额外的请求头(如灰度标签)
80
+ if (extraHeaders) {
81
+ Object.assign(headers, extraHeaders);
82
+ }
83
+
71
84
  // 配置代理,优先使用配置的 proxy,否则使用环境变量
72
85
  const agent = createWebSocketProxyAgent(url);
73
86
  const options: WebSocket.ClientOptions = { headers };
@@ -80,7 +93,9 @@ export class ClawCloudSocketClient {
80
93
 
81
94
  this.ws.on("open", () => {
82
95
  const connectionType = isRetry ? "reconnected" : "connected";
83
- useClawLogger().info(`[claw-cloud-socket] ${connectionType} to ${url}`);
96
+ useClawLogger().info(
97
+ `[claw-cloud-socket] ${connectionType} to ${url.slice(0, 15)}`
98
+ );
84
99
 
85
100
  // 清除重试定时器
86
101
  this.clearRetryTimer();
@@ -90,6 +105,16 @@ export class ClawCloudSocketClient {
90
105
  // 启动 ping 定时器
91
106
  this.startPingTimer();
92
107
 
108
+ // 通知状态跟踪器
109
+ this.options.onStatusEvent?.({
110
+ type: "cloud_socket_connected",
111
+ timestamp: Date.now(),
112
+ data: {
113
+ url: url,
114
+ connectedAt: new Date().toISOString(),
115
+ },
116
+ });
117
+
93
118
  this.options.onOpen?.();
94
119
  });
95
120
 
@@ -106,10 +131,22 @@ export class ClawCloudSocketClient {
106
131
  });
107
132
 
108
133
  this.ws.on("close", (code: number, reason: Buffer) => {
134
+ const reasonText = reason.toString();
109
135
  useClawLogger().info(
110
- `[claw-cloud-socket] connection closed: ${code} - ${reason.toString()}`
136
+ `[claw-cloud-socket] connection closed: ${code} - ${reasonText}`
111
137
  );
112
138
 
139
+ // 通知状态跟踪器
140
+ this.options.onStatusEvent?.({
141
+ type: "cloud_socket_disconnected",
142
+ timestamp: Date.now(),
143
+ data: {
144
+ reason: reasonText,
145
+ code: code,
146
+ disconnectedAt: new Date().toISOString(),
147
+ },
148
+ });
149
+
113
150
  // 如果不是手动关闭,尝试重连
114
151
  if (!this.isManualClose) {
115
152
  this.scheduleReconnect();
@@ -123,13 +160,23 @@ export class ClawCloudSocketClient {
123
160
  useClawLogger().error(
124
161
  `[claw-cloud-socket] connect errorred: ${error.message}`
125
162
  );
163
+
164
+ // 通知状态跟踪器
165
+ this.options.onStatusEvent?.({
166
+ type: "cloud_socket_error",
167
+ timestamp: Date.now(),
168
+ data: {
169
+ error: error.message,
170
+ timestamp: new Date().toISOString(),
171
+ },
172
+ });
126
173
  });
127
174
  }
128
175
 
129
176
  /**
130
177
  * 发送消息
131
178
  */
132
- send(message: YoyoClawMessage): boolean {
179
+ send(message: YoyoClawMessage, toDeviceId?: string): boolean {
133
180
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
134
181
  useClawLogger().error(
135
182
  "[claw-cloud-socket] cannot send message: connection not open"
@@ -138,11 +185,21 @@ export class ClawCloudSocketClient {
138
185
  }
139
186
 
140
187
  try {
141
- this.ws.send(JSON.stringify(message));
188
+ const msgText = JSON.stringify(message);
189
+
190
+ useClawLogger().debug?.(
191
+ `[yoyoclaw-cloud] send message to cloud session ${
192
+ message.targetDeviceId
193
+ }, device: ${toDeviceId}:, ${msgText.slice(0, 1000)}`
194
+ );
195
+
196
+ this.ws.send(msgText);
142
197
  return true;
143
198
  } catch (error) {
144
199
  useClawLogger().error(
145
- `[claw-cloud-socket] failed to send message: ${
200
+ `[claw-cloud-socket] failed to send message to cloud session ${
201
+ message.targetDeviceId
202
+ }, device: ${toDeviceId}: ${
146
203
  error instanceof Error ? error.message : String(error)
147
204
  }`
148
205
  );
@@ -182,11 +239,19 @@ export class ClawCloudSocketClient {
182
239
  const message: YoyoClawSocketMessage = JSON.parse(dataText);
183
240
 
184
241
  useClawLogger().debug?.(
185
- `[yoyoclaw-channel] received cloud message: ${dataText.slice(0, 500)}`
242
+ `[yoyoclaw-channel] received cloud message from session ${
243
+ message.wsOutputEvent?.sourceDeviceId
244
+ }, deviceId ${
245
+ message.wsOutputEvent?.sourceDeviceInfo?.deviceId
246
+ }: ${dataText.slice(0, 1000)}`
186
247
  );
187
248
 
188
249
  if (message.code === "YOYO_CLAW_100000") {
189
250
  if (message.wsOutputEvent) {
251
+ // 将 sessionInfo 从 wrapper 复制到 message 中
252
+ if (message.sessionInfo) {
253
+ message.wsOutputEvent.sessionInfo = message.sessionInfo;
254
+ }
190
255
  this.options.onMessage?.(message.wsOutputEvent);
191
256
  return;
192
257
  }
@@ -194,7 +259,7 @@ export class ClawCloudSocketClient {
194
259
  // 对向设备离线,当前设备对应的gateway连接都可以销毁掉
195
260
  if (message.extData?.offlineSocketId) {
196
261
  useClawLogger().info(
197
- `[claw-cloud-socket] remote device offline: ${message.extData.offlineSocketId}`
262
+ `[claw-cloud-socket] remote device offline, session: ${message.extData.offlineSocketId}`
198
263
  );
199
264
  this.options.onRemoteDeviceOffline?.(message.extData.offlineSocketId);
200
265
 
@@ -259,6 +324,17 @@ export class ClawCloudSocketClient {
259
324
  `[claw-cloud-socket] scheduling reconnect attempt ${this.retryCount}/${MAX_RETRIES} in ${delay}ms`
260
325
  );
261
326
 
327
+ // 通知状态跟踪器:重试
328
+ this.options.onStatusEvent?.({
329
+ type: "cloud_socket_retry",
330
+ timestamp: Date.now(),
331
+ data: {
332
+ retryCount: this.retryCount,
333
+ maxRetries: MAX_RETRIES,
334
+ delay: delay,
335
+ },
336
+ });
337
+
262
338
  this.retryTimer = setTimeout(() => {
263
339
  this.connect(true);
264
340
  }, delay);
@@ -279,8 +355,7 @@ export class ClawCloudSocketClient {
279
355
  */
280
356
  private calculateRetryDelay(): number {
281
357
  const delay =
282
- START_RETRY_DELAY *
283
- Math.pow(RETRY_BACKOFF_FACTOR, this.retryCount - 1);
358
+ START_RETRY_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, this.retryCount - 1);
284
359
  return Math.min(delay, MAX_RETRY_DELAY);
285
360
  }
286
361