@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.
- package/index.ts +2 -2
- package/openclaw.plugin.json +7 -0
- package/package.json +20 -20
- package/skills/search/SKILL.md +182 -0
- package/skills/search/scripts/search.sh +69 -0
- package/skills/yoyo-control/SKILL.md +105 -120
- package/skills/yoyo-control/references/alarm-create.md +473 -0
- package/skills/yoyo-control/references/app-close.md +183 -0
- package/skills/yoyo-control/references/app-open.md +178 -0
- package/skills/yoyo-control/references/call-phone.md +250 -0
- package/skills/yoyo-control/references/capture-screenshot.md +205 -54
- package/skills/yoyo-control/references/contact-search.md +235 -0
- package/skills/yoyo-control/references/hotspot.md +208 -0
- package/skills/yoyo-control/references/local-search.md +224 -15
- package/skills/yoyo-control/references/message-send.md +246 -0
- package/skills/yoyo-control/references/mobile-data.md +248 -0
- package/skills/yoyo-control/references/no-disturb.md +239 -0
- package/skills/yoyo-control/references/quiet-mode.md +228 -0
- package/skills/yoyo-control/references/ringing-mode.md +223 -0
- package/skills/yoyo-control/references/screen-record.md +220 -0
- package/skills/yoyo-control/references/vibration-mode.md +235 -0
- package/skills/yoyo-control/references/volume-operate.md +274 -0
- package/skills/yoyo-control/scripts/invoke.js +33 -111
- package/src/agent/copy-templates.ts +56 -0
- package/src/agent/index.ts +3 -0
- package/src/agent/templates/AGENTS.md +223 -0
- package/src/apis/claw-cloud.ts +70 -23
- package/src/apis/honor-auth.ts +20 -10
- package/src/apis/types.ts +24 -1
- package/src/cloud-channel/channel.ts +245 -58
- package/src/cloud-channel/client.ts +87 -12
- package/src/cloud-channel/types.ts +30 -0
- package/src/commands/env/impl.ts +58 -0
- package/src/commands/env/index.ts +1 -0
- package/src/commands/index.ts +11 -1
- package/src/commands/login/impl.ts +17 -8
- package/src/commands/logout/impl.ts +23 -0
- package/src/commands/logout/index.ts +1 -53
- package/src/commands/status/index.ts +172 -42
- package/src/gateway-client/client.deprecated.ts +1 -1
- package/src/gateway-client/client.ts +15 -20
- package/src/gateway-client/types.ts +2 -2
- package/src/honor-auth/browser.ts +12 -15
- package/src/honor-auth/callback-server.ts +3 -6
- package/src/honor-auth/cloud.ts +65 -12
- package/src/honor-auth/config.ts +25 -17
- package/src/honor-auth/index.ts +1 -0
- package/src/honor-auth/token-manager.ts +24 -14
- package/src/modules/claw-configs/config-manager.ts +211 -11
- package/src/modules/claw-configs/hosts.ts +48 -0
- package/src/modules/claw-configs/index.ts +1 -0
- package/src/modules/claw-configs/types.ts +4 -0
- package/src/modules/device/device-info.ts +20 -9
- package/src/modules/device/providers/linux.ts +128 -0
- package/src/modules/device/providers/macos.ts +123 -0
- package/src/modules/device/providers/pad.ts +0 -16
- package/src/modules/device/registry.ts +12 -3
- package/src/modules/login/impl.ts +38 -16
- package/src/runtime.ts +44 -0
- package/src/schemas.ts +4 -1
- package/src/services/connection/impl.ts +89 -9
- package/src/services/connection/status-tracker/events.ts +127 -0
- package/src/services/connection/status-tracker/index.ts +31 -0
- package/src/services/connection/status-tracker/storage.ts +133 -0
- package/src/services/connection/status-tracker/tracker.ts +370 -0
- package/src/services/connection/status-tracker/types.ts +131 -0
- package/src/types.ts +0 -4
- package/src/utils/fs-safe.ts +544 -0
- package/src/utils/version.ts +29 -0
- package/src/utils/ws.ts +21 -0
- package/skills/yoyo-control/references/open-app.md +0 -54
- package/skills/yoyo-control/references/phone-call.md +0 -217
- package/skills/yoyo-control/references/schedule.md +0 -107
- package/skills/yoyo-control/references/screen-recorder.md +0 -67
- package/skills/yoyo-control/references/search-contact.md +0 -37
- package/skills/yoyo-control/references/send-message.md +0 -155
- package/skills/yoyo-control/references/volume.md +0 -536
- package/skills/yoyo-control/scripts/README.md +0 -103
- package/skills/yoyo-control/scripts/volume-up.json +0 -7
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 ===
|
|
110
|
+
if (message.msgType === "devicePairMessage") {
|
|
99
111
|
this.handlePairMessage(message);
|
|
100
112
|
return;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
// 处理用户消息,转发到对应的GatewayClient
|
|
104
|
-
if (message.msgType ===
|
|
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 !==
|
|
125
|
+
if (message.msgType !== "devicePairMessage") {
|
|
114
126
|
return;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
try {
|
|
118
|
-
const {
|
|
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
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
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:
|
|
195
|
-
sourceRole:
|
|
314
|
+
msgType: "userMessage",
|
|
315
|
+
sourceRole: "yoyoclaw",
|
|
196
316
|
sourceDeviceId: this.config.deviceInfo.deviceId,
|
|
197
|
-
targetRole:
|
|
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(
|
|
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(
|
|
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 [
|
|
348
|
+
for (const [sessionId, client] of this.gatewayClientsMap.entries()) {
|
|
224
349
|
client.stop();
|
|
225
|
-
useClawLogger().info(
|
|
350
|
+
useClawLogger().info(
|
|
351
|
+
`[yoyoclaw-channel] closed gateway client for session: ${sessionId}`
|
|
352
|
+
);
|
|
226
353
|
}
|
|
227
354
|
this.gatewayClientsMap.clear();
|
|
228
|
-
this.
|
|
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(
|
|
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} - ${
|
|
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
|
-
|
|
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
|
|
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
|
|