@honor-claw/yoyo 0.0.1-beta.6 → 0.0.1-beta.7
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/apis/honor-auth.ts +3 -1
- package/src/cloud-channel/channel.ts +196 -59
- package/src/cloud-channel/client.ts +20 -6
- package/src/cloud-channel/types.ts +21 -0
- package/src/commands/env/impl.ts +3 -3
- package/src/commands/index.ts +3 -1
- package/src/commands/logout/impl.ts +26 -0
- package/src/commands/logout/index.ts +1 -53
- package/src/gateway-client/client.ts +16 -19
- package/src/gateway-client/types.ts +2 -2
- package/src/modules/claw-configs/config-manager.ts +41 -12
- package/src/modules/claw-configs/hosts.ts +30 -0
- package/src/modules/claw-configs/index.ts +1 -0
- package/src/modules/claw-configs/types.ts +1 -1
- 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 +116 -0
- package/src/modules/device/providers/pad.ts +0 -16
- package/src/modules/device/registry.ts +3 -2
- package/src/runtime.ts +2 -2
- package/src/schemas.ts +1 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/apis/honor-auth.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { TokenResponse, HonorAuthConfig } from '../honor-auth/types.js';
|
|
|
9
9
|
import { uuid } from '../utils/id.js';
|
|
10
10
|
import { isOKResponse } from './helpers.js';
|
|
11
11
|
import type { HttpApiWrapper } from './types.js';
|
|
12
|
+
import { takeApiHost } from '../modules/claw-configs/hosts.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* PKCE参数
|
|
@@ -101,7 +102,8 @@ export class HonorAuthClient {
|
|
|
101
102
|
* 使用授权码换取Token
|
|
102
103
|
*/
|
|
103
104
|
async exchangeToken(code: string) {
|
|
104
|
-
const
|
|
105
|
+
const hosts = takeApiHost();
|
|
106
|
+
const tokenUrl = `https://${hosts.ics}/honorboard/auth/v1/oauth/jwtToken`;
|
|
105
107
|
|
|
106
108
|
const data = {
|
|
107
109
|
clientId: this.config.clientId,
|
|
@@ -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,17 @@ 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
|
+
|
|
24
29
|
this.cloudClient = new ClawCloudSocketClient({
|
|
25
|
-
serverUrl:
|
|
30
|
+
serverUrl: `wss://${hosts.clawCloud}/aicloud/yoyo-claw-service/v1/yoyoclaw/fullduplex`,
|
|
26
31
|
deviceInfo: config.deviceInfo,
|
|
27
32
|
userInfo: config.userInfo,
|
|
28
33
|
onMessage: this.handleCloudMessage,
|
|
@@ -37,7 +42,7 @@ export class ClawChannel {
|
|
|
37
42
|
* 启动连接
|
|
38
43
|
*/
|
|
39
44
|
start(): void {
|
|
40
|
-
useClawLogger().info(
|
|
45
|
+
useClawLogger().info("[yoyoclaw-channel] starting connection");
|
|
41
46
|
this.cloudClient.connect();
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -45,7 +50,7 @@ export class ClawChannel {
|
|
|
45
50
|
* 关闭连接
|
|
46
51
|
*/
|
|
47
52
|
destroy(): void {
|
|
48
|
-
useClawLogger().info(
|
|
53
|
+
useClawLogger().info("[yoyoclaw-channel] closing connection");
|
|
49
54
|
this.closeAllGatewayClients();
|
|
50
55
|
this.cloudClient.close();
|
|
51
56
|
}
|
|
@@ -54,7 +59,7 @@ export class ClawChannel {
|
|
|
54
59
|
* 处理云侧连接打开
|
|
55
60
|
*/
|
|
56
61
|
private handleCloudOpen = () => {
|
|
57
|
-
useClawLogger().info(
|
|
62
|
+
useClawLogger().info("[yoyoclaw-channel] cloud connection established");
|
|
58
63
|
this.config.onOpen?.();
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -62,7 +67,7 @@ export class ClawChannel {
|
|
|
62
67
|
* 处理云侧连接关闭
|
|
63
68
|
*/
|
|
64
69
|
private handleCloudClose = () => {
|
|
65
|
-
useClawLogger().info(
|
|
70
|
+
useClawLogger().info("[yoyoclaw-channel] cloud connection closed");
|
|
66
71
|
this.config.onClose?.();
|
|
67
72
|
};
|
|
68
73
|
|
|
@@ -70,23 +75,16 @@ export class ClawChannel {
|
|
|
70
75
|
* 处理对向设备离线
|
|
71
76
|
*/
|
|
72
77
|
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
|
-
}
|
|
78
|
+
this.closeGatewayClient(sourceDeviceId);
|
|
83
79
|
};
|
|
84
80
|
|
|
85
81
|
/**
|
|
86
82
|
* 处理当前设备未注册
|
|
87
83
|
*/
|
|
88
84
|
private handleDeviceNotRegistered = () => {
|
|
89
|
-
useClawLogger().info(
|
|
85
|
+
useClawLogger().info(
|
|
86
|
+
"[yoyoclaw-channel] device not registered, notifying connection layer"
|
|
87
|
+
);
|
|
90
88
|
this.config.onDeviceNotRegistered?.();
|
|
91
89
|
};
|
|
92
90
|
|
|
@@ -95,13 +93,13 @@ export class ClawChannel {
|
|
|
95
93
|
*/
|
|
96
94
|
private handleCloudMessage = (message: YoyoClawMessage) => {
|
|
97
95
|
// 处理配对消息
|
|
98
|
-
if (message.msgType ===
|
|
96
|
+
if (message.msgType === "devicePairMessage") {
|
|
99
97
|
this.handlePairMessage(message);
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
// 处理用户消息,转发到对应的GatewayClient
|
|
104
|
-
if (message.msgType ===
|
|
102
|
+
if (message.msgType === "userMessage") {
|
|
105
103
|
this.handleUserMessage(message);
|
|
106
104
|
}
|
|
107
105
|
};
|
|
@@ -110,35 +108,73 @@ export class ClawChannel {
|
|
|
110
108
|
* 处理配对消息,创建GatewayClient连接
|
|
111
109
|
*/
|
|
112
110
|
private handlePairMessage(message: YoyoClawMessage): void {
|
|
113
|
-
if (message.msgType !==
|
|
111
|
+
if (message.msgType !== "devicePairMessage") {
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
try {
|
|
118
|
-
const {
|
|
116
|
+
const {
|
|
117
|
+
sourceDeviceId,
|
|
118
|
+
sourceDeviceInfo,
|
|
119
|
+
targetDeviceId,
|
|
120
|
+
port,
|
|
121
|
+
sessionInfo,
|
|
122
|
+
} = message;
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
sourceDeviceId &&
|
|
126
|
+
targetDeviceId &&
|
|
127
|
+
port &&
|
|
128
|
+
sourceDeviceInfo?.deviceId
|
|
129
|
+
) {
|
|
130
|
+
const hardwareDeviceId = sourceDeviceInfo.deviceId;
|
|
131
|
+
const newTimestamp = sessionInfo?.nodeConnectTimestamp;
|
|
132
|
+
|
|
133
|
+
if (!newTimestamp) {
|
|
134
|
+
// 兜底旧版本,异常场景,暂时不移除旧连接
|
|
135
|
+
useClawLogger().warn(
|
|
136
|
+
`[yoyoclaw-channel] pair message missing timestamp for device: ${sourceDeviceId}`
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
// 关闭该硬件设备ID对应的所有前序 GatewayClient(timestamp 小于新 timestamp)
|
|
140
|
+
this.closePreviousGatewayClients(hardwareDeviceId, newTimestamp);
|
|
141
|
+
}
|
|
119
142
|
|
|
120
|
-
if (sourceDeviceId && targetDeviceId && port) {
|
|
121
143
|
useClawLogger().info(
|
|
122
|
-
`[yoyoclaw-channel] received pair message, creating gateway client for device: ${sourceDeviceId}`
|
|
144
|
+
`[yoyoclaw-channel] received pair message, creating gateway client for device: ${sourceDeviceId}, hardware device: ${hardwareDeviceId}, timestamp: ${newTimestamp}`
|
|
123
145
|
);
|
|
124
146
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
// 创建会话信息
|
|
148
|
+
const sessionInfoData: DeviceSessionInfo = {
|
|
149
|
+
sessionId: sourceDeviceId,
|
|
150
|
+
timestamp: newTimestamp,
|
|
151
|
+
sourceInfo: {
|
|
152
|
+
sourceRole: sourceDeviceInfo?.role ?? "node",
|
|
153
|
+
sourceDeviceId: sourceDeviceId,
|
|
154
|
+
sourceDeviceInfo: sourceDeviceInfo,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// 保存会话信息
|
|
159
|
+
const sessions = this.deviceSessionsMap.get(hardwareDeviceId) || [];
|
|
160
|
+
sessions.push(sessionInfoData);
|
|
161
|
+
this.deviceSessionsMap.set(hardwareDeviceId, sessions);
|
|
162
|
+
this.sessionIdToHardwareDeviceMap.set(sourceDeviceId, hardwareDeviceId);
|
|
131
163
|
|
|
132
164
|
// 创建GatewayClient
|
|
133
165
|
const gatewayClient = new GatewayClient({
|
|
134
166
|
onOpen: () => {
|
|
135
|
-
useClawLogger().info(
|
|
167
|
+
useClawLogger().info(
|
|
168
|
+
`[yoyoclaw-channel] gateway client connected for device: ${sourceDeviceId}`
|
|
169
|
+
);
|
|
136
170
|
},
|
|
137
|
-
onMessage: data => {
|
|
171
|
+
onMessage: (data) => {
|
|
138
172
|
this.handleGatewayMessage(sourceDeviceId, data);
|
|
139
173
|
},
|
|
140
|
-
onClose: () => {
|
|
141
|
-
useClawLogger().info(
|
|
174
|
+
onClose: (reason) => {
|
|
175
|
+
useClawLogger().info(
|
|
176
|
+
`[yoyoclaw-channel] gateway client closed for device: ${sourceDeviceId}, with ${reason}`
|
|
177
|
+
);
|
|
142
178
|
},
|
|
143
179
|
});
|
|
144
180
|
|
|
@@ -146,7 +182,9 @@ export class ClawChannel {
|
|
|
146
182
|
gatewayClient.start();
|
|
147
183
|
}
|
|
148
184
|
} catch (error) {
|
|
149
|
-
useClawLogger().error(
|
|
185
|
+
useClawLogger().error(
|
|
186
|
+
`[yoyoclaw-channel] failed to handle pair message: ${String(error)}`
|
|
187
|
+
);
|
|
150
188
|
}
|
|
151
189
|
}
|
|
152
190
|
|
|
@@ -155,24 +193,38 @@ export class ClawChannel {
|
|
|
155
193
|
*/
|
|
156
194
|
private handleUserMessage(message: YoyoClawMessage): void {
|
|
157
195
|
if (!message.sourceDeviceId) {
|
|
158
|
-
useClawLogger().warn(
|
|
196
|
+
useClawLogger().warn(
|
|
197
|
+
"[yoyoclaw-channel] user message missing sourceDeviceId"
|
|
198
|
+
);
|
|
159
199
|
return;
|
|
160
200
|
}
|
|
161
201
|
|
|
202
|
+
const hardwareDeviceId = this.sessionIdToHardwareDeviceMap.get(
|
|
203
|
+
message.sourceDeviceId
|
|
204
|
+
);
|
|
205
|
+
|
|
162
206
|
try {
|
|
163
207
|
const gatewayClient = this.gatewayClientsMap.get(message.sourceDeviceId);
|
|
164
208
|
if (!gatewayClient) {
|
|
165
|
-
useClawLogger().warn(
|
|
209
|
+
useClawLogger().warn(
|
|
210
|
+
`[yoyoclaw-channel] no gateway client found for source device: ${hardwareDeviceId}, session: ${message.sourceDeviceId}`
|
|
211
|
+
);
|
|
166
212
|
return;
|
|
167
213
|
}
|
|
168
214
|
|
|
169
215
|
// 只发送data部分
|
|
170
216
|
if (message.data) {
|
|
171
|
-
useClawLogger().info(
|
|
217
|
+
useClawLogger().info(
|
|
218
|
+
`[yoyoclaw-channel] forwarding user message to gateway from ${hardwareDeviceId}, session: ${message.sourceDeviceId}`
|
|
219
|
+
);
|
|
172
220
|
gatewayClient.send(message.data);
|
|
173
221
|
}
|
|
174
222
|
} catch (error) {
|
|
175
|
-
useClawLogger().error(
|
|
223
|
+
useClawLogger().error(
|
|
224
|
+
`[yoyoclaw-channel] failed to send gateway message to ${hardwareDeviceId}: ${String(
|
|
225
|
+
error
|
|
226
|
+
)}, session: ${message.sourceDeviceId}`
|
|
227
|
+
);
|
|
176
228
|
}
|
|
177
229
|
}
|
|
178
230
|
|
|
@@ -180,39 +232,62 @@ export class ClawChannel {
|
|
|
180
232
|
* 处理来自Gateway的消息,转发到云侧
|
|
181
233
|
*/
|
|
182
234
|
private handleGatewayMessage(sourceDeviceId: string, data: string): void {
|
|
183
|
-
const
|
|
235
|
+
const hardwareDeviceId =
|
|
236
|
+
this.sessionIdToHardwareDeviceMap.get(sourceDeviceId);
|
|
237
|
+
if (!hardwareDeviceId) {
|
|
238
|
+
useClawLogger().warn(
|
|
239
|
+
`[yoyoclaw-channel] gateway source node offline, session: ${sourceDeviceId}`
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
|
|
245
|
+
const sessionInfo = sessions?.find((s) => s.sessionId === sourceDeviceId);
|
|
184
246
|
|
|
185
|
-
if (!
|
|
186
|
-
useClawLogger().warn(
|
|
247
|
+
if (!sessionInfo) {
|
|
248
|
+
useClawLogger().warn(
|
|
249
|
+
`[yoyoclaw-channel] gateway source node offline, session: ${sourceDeviceId}`
|
|
250
|
+
);
|
|
187
251
|
return;
|
|
188
252
|
}
|
|
189
253
|
|
|
254
|
+
useClawLogger().debug?.(
|
|
255
|
+
`[yoyoclaw-channel] received gateway message from ${
|
|
256
|
+
sessionInfo.sourceInfo.sourceDeviceInfo?.deviceId
|
|
257
|
+
} to cloud:, ${data.slice(0, 1000)}`
|
|
258
|
+
);
|
|
259
|
+
|
|
190
260
|
try {
|
|
191
261
|
// 将Gateway的消息封装成YoyoClawMessage发送到云侧
|
|
192
262
|
// 使用当前接收到的socket对应deviceId的来源信息
|
|
193
263
|
const cloudMessage: YoyoClawMessage = {
|
|
194
|
-
msgType:
|
|
195
|
-
sourceRole:
|
|
264
|
+
msgType: "userMessage",
|
|
265
|
+
sourceRole: "yoyoclaw",
|
|
196
266
|
sourceDeviceId: this.config.deviceInfo.deviceId,
|
|
197
|
-
targetRole:
|
|
198
|
-
targetDeviceId: sourceInfo.sourceDeviceId,
|
|
267
|
+
targetRole: "node",
|
|
268
|
+
targetDeviceId: sessionInfo.sourceInfo.sourceDeviceId,
|
|
199
269
|
port: this.config.deviceInfo.port,
|
|
200
270
|
data,
|
|
201
271
|
};
|
|
202
272
|
|
|
203
|
-
const result = this.cloudClient.send(
|
|
273
|
+
const result = this.cloudClient.send(
|
|
274
|
+
cloudMessage,
|
|
275
|
+
sessionInfo.sourceInfo.sourceDeviceInfo?.deviceId
|
|
276
|
+
);
|
|
204
277
|
|
|
205
278
|
if (!result) {
|
|
206
279
|
// 云socket挂了,这个gateway连接就需要取消掉
|
|
207
280
|
// 最好不要一次性把所有的gateway连接都干掉,让惰性关闭
|
|
208
281
|
useClawLogger().error(
|
|
209
|
-
`[yoyoclaw-channel] failed to send message, cloud socket closed, from: ${sourceInfo.sourceDeviceId}`
|
|
282
|
+
`[yoyoclaw-channel] failed to send message, cloud socket closed, from: ${sessionInfo.sourceInfo.sourceDeviceId}`
|
|
210
283
|
);
|
|
211
284
|
|
|
212
|
-
this.handleRemoteDeviceOffline(sourceInfo.sourceDeviceId);
|
|
285
|
+
this.handleRemoteDeviceOffline(sessionInfo.sourceInfo.sourceDeviceId);
|
|
213
286
|
}
|
|
214
287
|
} catch (error) {
|
|
215
|
-
useClawLogger().error(
|
|
288
|
+
useClawLogger().error(
|
|
289
|
+
`[yoyoclaw-channel] failed to handle gateway message: ${String(error)}`
|
|
290
|
+
);
|
|
216
291
|
}
|
|
217
292
|
}
|
|
218
293
|
|
|
@@ -220,11 +295,73 @@ export class ClawChannel {
|
|
|
220
295
|
* 关闭所有GatewayClient连接
|
|
221
296
|
*/
|
|
222
297
|
private closeAllGatewayClients(): void {
|
|
223
|
-
for (const [
|
|
298
|
+
for (const [sessionId, client] of this.gatewayClientsMap.entries()) {
|
|
224
299
|
client.stop();
|
|
225
|
-
useClawLogger().info(
|
|
300
|
+
useClawLogger().info(
|
|
301
|
+
`[yoyoclaw-channel] closed gateway client for session: ${sessionId}`
|
|
302
|
+
);
|
|
226
303
|
}
|
|
227
304
|
this.gatewayClientsMap.clear();
|
|
228
|
-
this.
|
|
305
|
+
this.deviceSessionsMap.clear();
|
|
306
|
+
this.sessionIdToHardwareDeviceMap.clear();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 关闭硬件设备的所有前序 GatewayClient(timestamp 小于指定值)
|
|
311
|
+
*/
|
|
312
|
+
private closePreviousGatewayClients(
|
|
313
|
+
hardwareDeviceId: string,
|
|
314
|
+
newTimestamp: number
|
|
315
|
+
): void {
|
|
316
|
+
const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
|
|
317
|
+
if (!sessions || sessions.length === 0) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 找出所有 timestamp 小于新 timestamp 的会话
|
|
322
|
+
const previousSessions = sessions.filter((s) => s.timestamp < newTimestamp);
|
|
323
|
+
|
|
324
|
+
if (previousSessions.length > 0) {
|
|
325
|
+
useClawLogger().info(
|
|
326
|
+
`[yoyoclaw-channel] closing ${previousSessions.length} previous gateway client(s) for hardware device: ${hardwareDeviceId}, new timestamp: ${newTimestamp}`
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
for (const session of previousSessions) {
|
|
330
|
+
this.closeGatewayClient(session.sessionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 关闭单个 GatewayClient
|
|
337
|
+
*/
|
|
338
|
+
private closeGatewayClient(sessionId: string): void {
|
|
339
|
+
const gatewayClient = this.gatewayClientsMap.get(sessionId);
|
|
340
|
+
if (gatewayClient) {
|
|
341
|
+
gatewayClient.stop();
|
|
342
|
+
this.gatewayClientsMap.delete(sessionId);
|
|
343
|
+
|
|
344
|
+
// 从会话映射中移除
|
|
345
|
+
const hardwareDeviceId = this.sessionIdToHardwareDeviceMap.get(sessionId);
|
|
346
|
+
if (hardwareDeviceId) {
|
|
347
|
+
const sessions = this.deviceSessionsMap.get(hardwareDeviceId);
|
|
348
|
+
if (sessions) {
|
|
349
|
+
const index = sessions.findIndex((s) => s.sessionId === sessionId);
|
|
350
|
+
if (index !== -1) {
|
|
351
|
+
sessions.splice(index, 1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 如果没有会话了,清理该硬件设备的记录
|
|
355
|
+
if (sessions.length === 0) {
|
|
356
|
+
this.deviceSessionsMap.delete(hardwareDeviceId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.sessionIdToHardwareDeviceMap.delete(sessionId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
useClawLogger().info(
|
|
363
|
+
`[yoyoclaw-channel] closed gateway client for session: ${sessionId}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
229
366
|
}
|
|
230
367
|
}
|
|
@@ -80,7 +80,9 @@ export class ClawCloudSocketClient {
|
|
|
80
80
|
|
|
81
81
|
this.ws.on("open", () => {
|
|
82
82
|
const connectionType = isRetry ? "reconnected" : "connected";
|
|
83
|
-
useClawLogger().info(
|
|
83
|
+
useClawLogger().info(
|
|
84
|
+
`[claw-cloud-socket] ${connectionType} to ${url.slice(0, 15)}`
|
|
85
|
+
);
|
|
84
86
|
|
|
85
87
|
// 清除重试定时器
|
|
86
88
|
this.clearRetryTimer();
|
|
@@ -129,7 +131,7 @@ export class ClawCloudSocketClient {
|
|
|
129
131
|
/**
|
|
130
132
|
* 发送消息
|
|
131
133
|
*/
|
|
132
|
-
send(message: YoyoClawMessage): boolean {
|
|
134
|
+
send(message: YoyoClawMessage, toDeviceId?: string): boolean {
|
|
133
135
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
134
136
|
useClawLogger().error(
|
|
135
137
|
"[claw-cloud-socket] cannot send message: connection not open"
|
|
@@ -141,14 +143,18 @@ export class ClawCloudSocketClient {
|
|
|
141
143
|
const msgText = JSON.stringify(message);
|
|
142
144
|
|
|
143
145
|
useClawLogger().debug?.(
|
|
144
|
-
`[yoyoclaw-cloud] send message to cloud
|
|
146
|
+
`[yoyoclaw-cloud] send message to cloud session ${
|
|
147
|
+
message.targetDeviceId
|
|
148
|
+
}, device: ${toDeviceId}:, ${msgText.slice(0, 1000)}`
|
|
145
149
|
);
|
|
146
150
|
|
|
147
151
|
this.ws.send(msgText);
|
|
148
152
|
return true;
|
|
149
153
|
} catch (error) {
|
|
150
154
|
useClawLogger().error(
|
|
151
|
-
`[claw-cloud-socket] failed to send message
|
|
155
|
+
`[claw-cloud-socket] failed to send message to cloud session ${
|
|
156
|
+
message.targetDeviceId
|
|
157
|
+
}, device: ${toDeviceId}: ${
|
|
152
158
|
error instanceof Error ? error.message : String(error)
|
|
153
159
|
}`
|
|
154
160
|
);
|
|
@@ -188,11 +194,19 @@ export class ClawCloudSocketClient {
|
|
|
188
194
|
const message: YoyoClawSocketMessage = JSON.parse(dataText);
|
|
189
195
|
|
|
190
196
|
useClawLogger().debug?.(
|
|
191
|
-
`[yoyoclaw-channel] received cloud message
|
|
197
|
+
`[yoyoclaw-channel] received cloud message from session ${
|
|
198
|
+
message.wsOutputEvent?.sourceDeviceId
|
|
199
|
+
}, deviceId ${
|
|
200
|
+
message.wsOutputEvent?.sourceDeviceInfo?.deviceId
|
|
201
|
+
}: ${dataText.slice(0, 1000)}`
|
|
192
202
|
);
|
|
193
203
|
|
|
194
204
|
if (message.code === "YOYO_CLAW_100000") {
|
|
195
205
|
if (message.wsOutputEvent) {
|
|
206
|
+
// 将 sessionInfo 从 wrapper 复制到 message 中
|
|
207
|
+
if (message.sessionInfo) {
|
|
208
|
+
message.wsOutputEvent.sessionInfo = message.sessionInfo;
|
|
209
|
+
}
|
|
196
210
|
this.options.onMessage?.(message.wsOutputEvent);
|
|
197
211
|
return;
|
|
198
212
|
}
|
|
@@ -200,7 +214,7 @@ export class ClawCloudSocketClient {
|
|
|
200
214
|
// 对向设备离线,当前设备对应的gateway连接都可以销毁掉
|
|
201
215
|
if (message.extData?.offlineSocketId) {
|
|
202
216
|
useClawLogger().info(
|
|
203
|
-
`[claw-cloud-socket] remote device offline: ${message.extData.offlineSocketId}`
|
|
217
|
+
`[claw-cloud-socket] remote device offline, session: ${message.extData.offlineSocketId}`
|
|
204
218
|
);
|
|
205
219
|
this.options.onRemoteDeviceOffline?.(message.extData.offlineSocketId);
|
|
206
220
|
|
|
@@ -17,6 +17,12 @@ export interface YoyoClawMessage {
|
|
|
17
17
|
port: number | string;
|
|
18
18
|
data?: string;
|
|
19
19
|
msgType: "userMessage" | "devicePairMessage";
|
|
20
|
+
/**
|
|
21
|
+
* 会话轮次等追加信息,只有配对消息有
|
|
22
|
+
*/
|
|
23
|
+
sessionInfo?: {
|
|
24
|
+
nodeConnectTimestamp: number;
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
@@ -43,6 +49,12 @@ export interface YoyoClawSocketWrapper<T> {
|
|
|
43
49
|
extData?: {
|
|
44
50
|
offlineSocketId?: string;
|
|
45
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* 会话轮次等追加信息,只有配对消息有
|
|
54
|
+
*/
|
|
55
|
+
sessionInfo?: {
|
|
56
|
+
nodeConnectTimestamp: number;
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
/**
|
|
@@ -79,3 +91,12 @@ export interface ClawSocketClientOptions {
|
|
|
79
91
|
// 当前设备未注册回调
|
|
80
92
|
onDeviceNotRegistered?: () => void;
|
|
81
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 设备会话信息
|
|
97
|
+
*/
|
|
98
|
+
export interface DeviceSessionInfo {
|
|
99
|
+
sessionId: string;
|
|
100
|
+
timestamp: number;
|
|
101
|
+
sourceInfo: ClawSocketSourceInfo;
|
|
102
|
+
}
|
package/src/commands/env/impl.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { Command } from "commander";
|
|
|
5
5
|
export function registerEnvCommand(_: unknown, command: Command) {
|
|
6
6
|
const nextCommand = command
|
|
7
7
|
.command("env")
|
|
8
|
-
.description("Manage runtime environment (test/production)")
|
|
9
|
-
.option("--set <env>", "Set environment: test or production")
|
|
8
|
+
.description("Manage runtime environment (dev/test/production)")
|
|
9
|
+
.option("--set <env>", "Set environment: dev, test or production")
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
const { set: setEnv } = options;
|
|
12
12
|
const logger = useClawLogger();
|
|
@@ -16,7 +16,7 @@ export function registerEnvCommand(_: unknown, command: Command) {
|
|
|
16
16
|
|
|
17
17
|
if (setEnv) {
|
|
18
18
|
// 设置环境
|
|
19
|
-
if (setEnv !== "test" && setEnv !== "production") {
|
|
19
|
+
if (setEnv !== "test" && setEnv !== "dev" && setEnv !== "production") {
|
|
20
20
|
logger.error("❌ Invalid environment. Use 'test' or 'production'.");
|
|
21
21
|
return;
|
|
22
22
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { registerLoginCommand } from "./login/index.js";
|
|
3
3
|
import { registerStatusCommand } from "./status/index.js";
|
|
4
|
+
import { registerLogoutCommand } from "./logout/index.js";
|
|
4
5
|
import { registerEnvCommand } from "./env/index.js";
|
|
5
6
|
import { isBetaVersion } from "../utils/version.js";
|
|
6
7
|
|
|
@@ -15,7 +16,8 @@ export function registerCommands(api: OpenClawPluginApi) {
|
|
|
15
16
|
registerLoginCommand(api, rootCommand);
|
|
16
17
|
// @ts-ignore
|
|
17
18
|
registerStatusCommand(api, rootCommand);
|
|
18
|
-
//
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
registerLogoutCommand(api, rootCommand);
|
|
19
21
|
|
|
20
22
|
// 只在 beta 版本时注册 env 命令
|
|
21
23
|
if (isBetaVersion()) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { type Command } from "commander";
|
|
3
|
+
import { getConfigManager } from "../../modules/claw-configs/config-manager.js";
|
|
4
|
+
|
|
5
|
+
export function registerLogoutCommand(api: OpenClawPluginApi, command: Command) {
|
|
6
|
+
const nextCommand = command
|
|
7
|
+
.command("logout")
|
|
8
|
+
.description("退出登录并清除用户配置")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
api.logger.info("logout CLI command called");
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// 清除用户配置
|
|
14
|
+
const configManager = getConfigManager();
|
|
15
|
+
await configManager.clearUserConfig();
|
|
16
|
+
console.log("✅ 用户配置已清除");
|
|
17
|
+
console.log("✅ 去登录成功,网关将自动重启处理新配置");
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
20
|
+
console.error("❌ 去登录失败:", errorMessage);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return nextCommand;
|
|
26
|
+
}
|
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { getConnectionManager } from "../../services/connection-manager.js";
|
|
3
|
-
import { type Command } from "commander";
|
|
4
|
-
|
|
5
|
-
export function registerLogoutCommand(api: OpenClawPluginApi, command: Command) {
|
|
6
|
-
const nextCommand = command
|
|
7
|
-
.command("logout")
|
|
8
|
-
.description("Logout and disconnect from YOYOClaw")
|
|
9
|
-
.option("--force", "Force logout without confirmation")
|
|
10
|
-
.action(async (options) => {
|
|
11
|
-
api.logger.info("honor logout CLI command called");
|
|
12
|
-
|
|
13
|
-
const { force } = options;
|
|
14
|
-
const connectionManager = getConnectionManager();
|
|
15
|
-
const isConnected = connectionManager.isConnected();
|
|
16
|
-
const connectedDevices = connectionManager.getConnectedDevices();
|
|
17
|
-
|
|
18
|
-
if (!isConnected) {
|
|
19
|
-
console.log("⚠️ 当前未连接到 YOYOClaw");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 如果有活跃的设备连接且不是强制模式,提示确认
|
|
24
|
-
if (connectedDevices.length > 0 && !force) {
|
|
25
|
-
console.log(`⚠️ 当前有 ${connectedDevices.length} 个活跃的设备连接:`);
|
|
26
|
-
for (const device of connectedDevices) {
|
|
27
|
-
console.log(` - ${device}`);
|
|
28
|
-
}
|
|
29
|
-
console.log("\n⚠️ 断开连接将停止所有设备间的通信");
|
|
30
|
-
console.log('💡 使用 --force 选项可以跳过此确认');
|
|
31
|
-
|
|
32
|
-
// 注意:在命令行工具中,我们无法直接等待用户输入
|
|
33
|
-
// 这里我们给出提示,然后继续执行
|
|
34
|
-
console.log("\n🔌 正在断开连接...");
|
|
35
|
-
} else {
|
|
36
|
-
console.log("🔌 正在断开连接...");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await connectionManager.cleanup();
|
|
41
|
-
console.log("✅ WebSocket 连接已关闭");
|
|
42
|
-
console.log("✅ 所有设备连接已清理");
|
|
43
|
-
console.log("✅ 资源已释放");
|
|
44
|
-
console.log("👋 已登出");
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
|
-
console.error("❌ 登出失败:", errorMessage);
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return nextCommand;
|
|
53
|
-
}
|
|
1
|
+
export * from "./impl.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { rawDataToString } from
|
|
2
|
-
import { WebSocket } from
|
|
3
|
-
import { GatewayClientOptions } from
|
|
4
|
-
import {
|
|
1
|
+
import { rawDataToString } from 'openclaw/plugin-sdk';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { GatewayClientOptions } from './types.js';
|
|
4
|
+
import { getConfigManager } from '../modules/claw-configs/config-manager.js';
|
|
5
5
|
|
|
6
|
-
const GATEWAY_SERVER_URL =
|
|
6
|
+
const GATEWAY_SERVER_URL = 'ws://127.0.0.1:18789';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 纯透传 Gateway Client
|
|
@@ -26,29 +26,28 @@ export class GatewayClient {
|
|
|
26
26
|
if (this.closed) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
const configManager = getConfigManager();
|
|
31
|
+
const url = `ws://127.0.0.1:${configManager.getGatewayPort()}`;
|
|
30
32
|
this.ws = new WebSocket(url, { maxPayload: 25 * 1024 * 1024 });
|
|
31
33
|
|
|
32
|
-
this.ws.on(
|
|
34
|
+
this.ws.on('open', () => {
|
|
33
35
|
this.opts.onOpen?.();
|
|
34
36
|
});
|
|
35
37
|
|
|
36
|
-
this.ws.on(
|
|
38
|
+
this.ws.on('message', data => {
|
|
37
39
|
const dataText = rawDataToString(data);
|
|
38
|
-
useClawLogger().debug?.(`[yoyoclaw-gateway] received message:, ${dataText.slice(0, 500)}`);
|
|
39
40
|
this.opts.onMessage?.(dataText);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
this.ws.on(
|
|
43
|
+
this.ws.on('close', (code, reason) => {
|
|
43
44
|
const reasonText = rawDataToString(reason);
|
|
44
45
|
this.ws = null;
|
|
45
|
-
|
|
46
|
-
this.opts.onClose?.();
|
|
46
|
+
this.opts.onClose?.(`code: ${code}, reason: ${reasonText ?? ''}`);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
this.ws.on(
|
|
50
|
-
|
|
51
|
-
this.opts.onClose?.();
|
|
49
|
+
this.ws.on('error', err => {
|
|
50
|
+
this.opts.onClose?.(`socket error: ${(err as Error).message}`);
|
|
52
51
|
});
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -66,11 +65,9 @@ export class GatewayClient {
|
|
|
66
65
|
*/
|
|
67
66
|
send(data: Buffer | string): void {
|
|
68
67
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
69
|
-
|
|
70
|
-
"[yoyoclaw-gateway] send failed: gateway not connected"
|
|
71
|
-
);
|
|
72
|
-
return;
|
|
68
|
+
throw new Error('gateway not connected');
|
|
73
69
|
}
|
|
70
|
+
|
|
74
71
|
this.ws.send(data);
|
|
75
72
|
}
|
|
76
73
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
6
6
|
import { getYoyoRuntime } from "../../runtime.js";
|
|
7
|
+
import { isBetaVersion } from "../../utils/version.js";
|
|
7
8
|
import type { GatewayAuthConfig, UserConfig } from "./types.js";
|
|
8
9
|
|
|
9
10
|
const PLUGIN_YOYO_ID = "yoyo";
|
|
@@ -21,7 +22,8 @@ export class ConfigManager {
|
|
|
21
22
|
return runtime.config.loadConfig();
|
|
22
23
|
} catch (error) {
|
|
23
24
|
throw new Error(
|
|
24
|
-
`Failed to load config: ${
|
|
25
|
+
`Failed to load config: ${
|
|
26
|
+
error instanceof Error ? error.message : String(error)
|
|
25
27
|
}`
|
|
26
28
|
);
|
|
27
29
|
}
|
|
@@ -36,7 +38,8 @@ export class ConfigManager {
|
|
|
36
38
|
await runtime.config.writeConfigFile(config);
|
|
37
39
|
} catch (error) {
|
|
38
40
|
throw new Error(
|
|
39
|
-
`Failed to save config: ${
|
|
41
|
+
`Failed to save config: ${
|
|
42
|
+
error instanceof Error ? error.message : String(error)
|
|
40
43
|
}`
|
|
41
44
|
);
|
|
42
45
|
}
|
|
@@ -112,22 +115,24 @@ export class ConfigManager {
|
|
|
112
115
|
/**
|
|
113
116
|
* 获取运行环境配置
|
|
114
117
|
*/
|
|
115
|
-
getEnv():
|
|
118
|
+
getEnv(): "test" | "production" {
|
|
116
119
|
try {
|
|
117
120
|
const config = this.loadConfig();
|
|
118
|
-
const env = config.plugins?.entries?.[PLUGIN_YOYO_ID]?.config
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const env = config.plugins?.entries?.[PLUGIN_YOYO_ID]?.config?.env as
|
|
122
|
+
| "test"
|
|
123
|
+
| "production"
|
|
124
|
+
| undefined;
|
|
125
|
+
return env || "test";
|
|
121
126
|
} catch (error) {
|
|
122
127
|
console.error(`[claw-configs] Failed to read env config: ${error}`);
|
|
123
|
-
return
|
|
128
|
+
return "test";
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
/**
|
|
128
133
|
* 更新运行环境配置
|
|
129
134
|
*/
|
|
130
|
-
async updateEnv(env:
|
|
135
|
+
async updateEnv(env: "test" | "production"): Promise<void> {
|
|
131
136
|
try {
|
|
132
137
|
const currentConfig = this.loadConfig();
|
|
133
138
|
const currentUserConfig = this.getUserConfig() || {};
|
|
@@ -187,7 +192,8 @@ export class ConfigManager {
|
|
|
187
192
|
await this.saveConfig(updatedConfig);
|
|
188
193
|
} catch (error) {
|
|
189
194
|
throw new Error(
|
|
190
|
-
`Failed to update user config: ${
|
|
195
|
+
`Failed to update user config: ${
|
|
196
|
+
error instanceof Error ? error.message : String(error)
|
|
191
197
|
}`
|
|
192
198
|
);
|
|
193
199
|
}
|
|
@@ -219,7 +225,8 @@ export class ConfigManager {
|
|
|
219
225
|
await this.saveConfig(updatedConfig);
|
|
220
226
|
} catch (error) {
|
|
221
227
|
throw new Error(
|
|
222
|
-
`Failed to clear user config: ${
|
|
228
|
+
`Failed to clear user config: ${
|
|
229
|
+
error instanceof Error ? error.message : String(error)
|
|
223
230
|
}`
|
|
224
231
|
);
|
|
225
232
|
}
|
|
@@ -266,7 +273,7 @@ export class ConfigManager {
|
|
|
266
273
|
"sound.vibration",
|
|
267
274
|
"ringing.mode",
|
|
268
275
|
"vibration.mode",
|
|
269
|
-
"close.app"
|
|
276
|
+
"close.app",
|
|
270
277
|
];
|
|
271
278
|
|
|
272
279
|
const currentAllowCommands =
|
|
@@ -275,12 +282,33 @@ export class ConfigManager {
|
|
|
275
282
|
new Set([...currentAllowCommands, ...commandsToAdd])
|
|
276
283
|
);
|
|
277
284
|
|
|
285
|
+
// 处理 env 默认值(仅在未设置时设置)
|
|
286
|
+
const currentEnv =
|
|
287
|
+
currentConfig.plugins?.entries?.[pluginId]?.config?.env;
|
|
288
|
+
const shouldSetEnv = !currentEnv;
|
|
289
|
+
const defaultEnv = isBetaVersion() ? "test" : "production";
|
|
290
|
+
|
|
291
|
+
// 构建插件配置
|
|
292
|
+
const pluginConfig =
|
|
293
|
+
currentConfig.plugins?.entries?.[pluginId]?.config || {};
|
|
294
|
+
const updatedPluginConfig = shouldSetEnv
|
|
295
|
+
? { ...pluginConfig, env: defaultEnv }
|
|
296
|
+
: pluginConfig;
|
|
297
|
+
|
|
278
298
|
// 一次性更新所有配置
|
|
279
299
|
const updatedConfig: OpenClawConfig = {
|
|
280
300
|
...currentConfig,
|
|
281
301
|
plugins: {
|
|
282
302
|
...currentConfig.plugins,
|
|
283
303
|
allow: updatedAllow,
|
|
304
|
+
entries: {
|
|
305
|
+
...currentConfig.plugins?.entries,
|
|
306
|
+
[pluginId]: {
|
|
307
|
+
...currentConfig.plugins?.entries?.[pluginId],
|
|
308
|
+
enabled: true,
|
|
309
|
+
config: updatedPluginConfig,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
284
312
|
},
|
|
285
313
|
gateway: {
|
|
286
314
|
...currentConfig.gateway,
|
|
@@ -294,7 +322,8 @@ export class ConfigManager {
|
|
|
294
322
|
await this.saveConfig(updatedConfig);
|
|
295
323
|
} catch (error) {
|
|
296
324
|
throw new Error(
|
|
297
|
-
`failed to initialize plugin config: ${
|
|
325
|
+
`failed to initialize plugin config: ${
|
|
326
|
+
error instanceof Error ? error.message : String(error)
|
|
298
327
|
}`
|
|
299
328
|
);
|
|
300
329
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getYoyoEnv } from '../../runtime.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 获取当前环境的api host信息
|
|
5
|
+
*/
|
|
6
|
+
export function takeApiHost() {
|
|
7
|
+
const env = getYoyoEnv();
|
|
8
|
+
|
|
9
|
+
switch (env) {
|
|
10
|
+
case 'dev': {
|
|
11
|
+
return {
|
|
12
|
+
clawCloud: 'omni-dev-drcn.hiboard.hihonorcloud.com',
|
|
13
|
+
ics: 'api-agd-test-drcn.hiboard.hihonorcloud.com',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
case 'test': {
|
|
17
|
+
return {
|
|
18
|
+
clawCloud: 'omni-pre-drcn.hiboard.hihonorcloud.com',
|
|
19
|
+
ics: 'api-agd-test-drcn.hiboard.hihonorcloud.com',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
case 'production':
|
|
23
|
+
default: {
|
|
24
|
+
return {
|
|
25
|
+
clawCloud: 'yoyoclaw-drcn.hiboard.hihonorcloud.com',
|
|
26
|
+
ics: 'api-prd-drcn.hiboard.hihonorcloud.com',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import type { DeviceInfo } from '../../types.js';
|
|
5
5
|
import { WindowsDeviceInfoProvider } from './providers/windows.js';
|
|
6
6
|
import { PadDevice } from './providers/pad.js';
|
|
7
|
+
import { LinuxDeviceInfoProvider } from './providers/linux.js';
|
|
8
|
+
import { MacOSDeviceInfoProvider } from './providers/macos.js';
|
|
7
9
|
import type { DeviceInfoProvider } from './providers/base.js';
|
|
8
10
|
import { useClawLogger } from '../../utils/logger.js';
|
|
9
11
|
import { getConfigManager } from '../claw-configs/config-manager.js';
|
|
@@ -11,7 +13,7 @@ import { getConfigManager } from '../claw-configs/config-manager.js';
|
|
|
11
13
|
/**
|
|
12
14
|
* 检测当前设备类型
|
|
13
15
|
*/
|
|
14
|
-
function detectDeviceType(): 'windows' | '
|
|
16
|
+
function detectDeviceType(): 'windows' | 'pad' | 'linux' | 'macos' {
|
|
15
17
|
const platform = process.platform;
|
|
16
18
|
|
|
17
19
|
if (platform === 'win32') {
|
|
@@ -19,20 +21,24 @@ function detectDeviceType(): 'windows' | 'android' {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
if (platform === 'linux') {
|
|
22
|
-
// 检查是否为
|
|
24
|
+
// 检查是否为 pad 环境
|
|
23
25
|
try {
|
|
24
26
|
const fs = require('fs');
|
|
25
27
|
if (fs.existsSync('/system/bin/getprop')) {
|
|
26
|
-
return '
|
|
28
|
+
return 'pad';
|
|
27
29
|
}
|
|
28
30
|
} catch {
|
|
29
|
-
//
|
|
31
|
+
// 忽略错误,继续检测为 Linux
|
|
30
32
|
}
|
|
31
|
-
return '
|
|
33
|
+
return 'linux';
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (platform === 'darwin') {
|
|
37
|
+
return 'macos';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 默认返回 linux(兼容性)
|
|
41
|
+
return 'linux';
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
@@ -42,11 +48,16 @@ function getDeviceInfoProvider(): DeviceInfoProvider {
|
|
|
42
48
|
const deviceType = detectDeviceType();
|
|
43
49
|
|
|
44
50
|
switch (deviceType) {
|
|
45
|
-
case '
|
|
51
|
+
case 'pad':
|
|
46
52
|
return new PadDevice();
|
|
47
53
|
case 'windows':
|
|
48
|
-
default:
|
|
49
54
|
return new WindowsDeviceInfoProvider();
|
|
55
|
+
case 'linux':
|
|
56
|
+
return new LinuxDeviceInfoProvider();
|
|
57
|
+
case 'macos':
|
|
58
|
+
return new MacOSDeviceInfoProvider();
|
|
59
|
+
default:
|
|
60
|
+
return new LinuxDeviceInfoProvider();
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux 设备信息提供者
|
|
3
|
+
*/
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import type { DeviceInfoProvider } from './base.js';
|
|
9
|
+
import type { DeviceType } from '../../../types.js';
|
|
10
|
+
|
|
11
|
+
export class LinuxDeviceInfoProvider implements DeviceInfoProvider {
|
|
12
|
+
/**
|
|
13
|
+
* 执行 shell 命令并获取输出
|
|
14
|
+
*/
|
|
15
|
+
private execCommand(command: string): string {
|
|
16
|
+
try {
|
|
17
|
+
return execSync(command, { encoding: 'utf-8' }).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取 Linux 设备唯一ID
|
|
25
|
+
*/
|
|
26
|
+
private getLinuxDeviceId(): string {
|
|
27
|
+
try {
|
|
28
|
+
// 尝试获取机器ID(systemd-based systems)
|
|
29
|
+
const machineId = this.execCommand('cat /etc/machine-id 2>/dev/null');
|
|
30
|
+
if (machineId) {
|
|
31
|
+
return machineId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 尝试获取 D-Bus 机器ID
|
|
35
|
+
const dbusMachineId = this.execCommand('cat /var/lib/dbus/machine-id 2>/dev/null');
|
|
36
|
+
if (dbusMachineId) {
|
|
37
|
+
return dbusMachineId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 尝试获取主板序列号
|
|
41
|
+
const serial = this.execCommand('sudo dmidecode -s system-serial-number 2>/dev/null');
|
|
42
|
+
if (serial && serial !== 'Not Specified') {
|
|
43
|
+
return serial;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 获取 CPU 信息作为备选
|
|
47
|
+
const cpuInfo = this.execCommand('cat /proc/cpuinfo | grep "serial" | head -1 | cut -d":" -f2 | tr -d " "');
|
|
48
|
+
if (cpuInfo) {
|
|
49
|
+
return cpuInfo;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 最后备用方案:使用主机名和架构生成哈希
|
|
53
|
+
const hostname = os.hostname();
|
|
54
|
+
const arch = os.arch();
|
|
55
|
+
const raw = `${hostname}_${arch}_${os.platform()}`;
|
|
56
|
+
return createHash('sha256').update(raw, 'utf-8').digest('hex');
|
|
57
|
+
} catch {
|
|
58
|
+
return uuidv4();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取 Linux 设备型号
|
|
64
|
+
*/
|
|
65
|
+
private getLinuxDeviceModel(): string {
|
|
66
|
+
try {
|
|
67
|
+
// 尝试获取产品名称
|
|
68
|
+
const productName = this.execCommand('sudo dmidecode -s system-product-name 2>/dev/null');
|
|
69
|
+
if (productName && productName !== 'Not Specified') {
|
|
70
|
+
return productName;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 尝试获取主板信息
|
|
74
|
+
const boardName = this.execCommand('sudo dmidecode -s baseboard-product-name 2>/dev/null');
|
|
75
|
+
if (boardName && boardName !== 'Not Specified') {
|
|
76
|
+
return boardName;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 尝试从 /sys/class/dmi/id 获取信息
|
|
80
|
+
const sysProductName = this.execCommand('cat /sys/class/dmi/id/product_name 2>/dev/null');
|
|
81
|
+
if (sysProductName) {
|
|
82
|
+
return sysProductName;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 默认返回 Linux PC
|
|
86
|
+
return 'Linux PC';
|
|
87
|
+
} catch {
|
|
88
|
+
return 'Linux PC';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 获取 Linux 设备名称
|
|
94
|
+
*/
|
|
95
|
+
private getLinuxDeviceName(): string {
|
|
96
|
+
const hostname = os.hostname();
|
|
97
|
+
const model = this.getLinuxDeviceModel();
|
|
98
|
+
return `${hostname} (${model})`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取设备唯一ID
|
|
103
|
+
*/
|
|
104
|
+
getDeviceId(): string {
|
|
105
|
+
return this.getLinuxDeviceId();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取设备名称
|
|
110
|
+
*/
|
|
111
|
+
getDeviceName(): string {
|
|
112
|
+
return this.getLinuxDeviceName();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取设备型号
|
|
117
|
+
*/
|
|
118
|
+
getDeviceModel(): string {
|
|
119
|
+
return this.getLinuxDeviceModel();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 获取设备类型
|
|
124
|
+
*/
|
|
125
|
+
getDeviceType(): DeviceType {
|
|
126
|
+
return 'pc';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS 设备信息提供者
|
|
3
|
+
*/
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import type { DeviceInfoProvider } from './base.js';
|
|
9
|
+
import type { DeviceType } from '../../../types.js';
|
|
10
|
+
|
|
11
|
+
export class MacOSDeviceInfoProvider implements DeviceInfoProvider {
|
|
12
|
+
/**
|
|
13
|
+
* 执行 shell 命令并获取输出
|
|
14
|
+
*/
|
|
15
|
+
private execCommand(command: string): string {
|
|
16
|
+
try {
|
|
17
|
+
return execSync(command, { encoding: 'utf-8' }).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取 macOS 设备唯一ID
|
|
25
|
+
*/
|
|
26
|
+
private getMacOSDeviceId(): string {
|
|
27
|
+
try {
|
|
28
|
+
// 获取硬件UUID(最可靠的macOS设备标识)
|
|
29
|
+
const hardwareUuid = this.execCommand('system_profiler SPHardwareDataType | grep "Hardware UUID" | cut -d ":" -f2 | tr -d " "');
|
|
30
|
+
if (hardwareUuid) {
|
|
31
|
+
return hardwareUuid;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 获取序列号
|
|
35
|
+
const serialNumber = this.execCommand('system_profiler SPHardwareDataType | grep "Serial Number" | cut -d ":" -f2 | tr -d " "');
|
|
36
|
+
if (serialNumber) {
|
|
37
|
+
return serialNumber;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 获取主板序列号
|
|
41
|
+
const boardSerial = this.execCommand('system_profiler SPHardwareDataType | grep "Board ID" | cut -d ":" -f2 | tr -d " "');
|
|
42
|
+
if (boardSerial) {
|
|
43
|
+
return boardSerial;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 最后备用方案:使用主机名和架构生成哈希
|
|
47
|
+
const hostname = os.hostname();
|
|
48
|
+
const arch = os.arch();
|
|
49
|
+
const raw = `${hostname}_${arch}_macOS`;
|
|
50
|
+
return createHash('sha256').update(raw, 'utf-8').digest('hex');
|
|
51
|
+
} catch {
|
|
52
|
+
return uuidv4();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 获取 macOS 设备型号
|
|
58
|
+
*/
|
|
59
|
+
private getMacOSDeviceModel(): string {
|
|
60
|
+
try {
|
|
61
|
+
// 获取硬件型号
|
|
62
|
+
const modelName = this.execCommand('system_profiler SPHardwareDataType | grep "Model Name" | cut -d ":" -f2 | sed "s/^ //"');
|
|
63
|
+
if (modelName) {
|
|
64
|
+
return modelName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 获取机型标识符
|
|
68
|
+
const modelIdentifier = this.execCommand('system_profiler SPHardwareDataType | grep "Model Identifier" | cut -d ":" -f2 | tr -d " "');
|
|
69
|
+
if (modelIdentifier) {
|
|
70
|
+
return modelIdentifier;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 默认返回 Mac
|
|
74
|
+
return 'Mac';
|
|
75
|
+
} catch {
|
|
76
|
+
return 'Mac';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 获取 macOS 设备名称
|
|
82
|
+
*/
|
|
83
|
+
private getMacOSDeviceName(): string {
|
|
84
|
+
const hostname = os.hostname();
|
|
85
|
+
const model = this.getMacOSDeviceModel();
|
|
86
|
+
return `${hostname} (${model})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取设备唯一ID
|
|
91
|
+
*/
|
|
92
|
+
getDeviceId(): string {
|
|
93
|
+
return this.getMacOSDeviceId();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取设备名称
|
|
98
|
+
*/
|
|
99
|
+
getDeviceName(): string {
|
|
100
|
+
return this.getMacOSDeviceName();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 获取设备型号
|
|
105
|
+
*/
|
|
106
|
+
getDeviceModel(): string {
|
|
107
|
+
return this.getMacOSDeviceModel();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取设备类型
|
|
112
|
+
*/
|
|
113
|
+
getDeviceType(): DeviceType {
|
|
114
|
+
return 'pc';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -67,22 +67,6 @@ export class PadDevice implements DeviceInfoProvider {
|
|
|
67
67
|
return this.getAndroidDeviceModel();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* 获取 Android 品牌
|
|
72
|
-
*/
|
|
73
|
-
private getAndroidBrand(): string {
|
|
74
|
-
const brand = this.getAndroidProp('ro.product.brand');
|
|
75
|
-
return brand || 'Android';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 获取 Android 系统版本
|
|
80
|
-
*/
|
|
81
|
-
private getAndroidSysVersion(): string {
|
|
82
|
-
const version = this.getAndroidProp('ro.honor.build.display.id');
|
|
83
|
-
return version || 'Android';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
70
|
/**
|
|
87
71
|
* 获取设备唯一ID
|
|
88
72
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClawCloudClient } from '../../apis/claw-cloud.js';
|
|
2
2
|
import { isOKResponse } from '../../apis/index.js';
|
|
3
3
|
import type { DeviceInfo, HonorUserInfo } from '../../types.js';
|
|
4
|
-
import { getConfigManager } from '../claw-configs/index.js';
|
|
4
|
+
import { getConfigManager, takeApiHost } from '../claw-configs/index.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 注册设备到 Claw Cloud
|
|
@@ -17,7 +17,8 @@ export async function registerDevice(deviceInfo: DeviceInfo, userInfo: HonorUser
|
|
|
17
17
|
const gatewayAuthConfig = configManager.getGatewayAuthConfig();
|
|
18
18
|
|
|
19
19
|
// 创建 Claw Cloud 客户端
|
|
20
|
-
const
|
|
20
|
+
const hosts = takeApiHost();
|
|
21
|
+
const baseUrl = `https://${hosts.clawCloud}/aicloud/yoyo-claw-service`;
|
|
21
22
|
const client = createClawCloudClient(baseUrl);
|
|
22
23
|
|
|
23
24
|
// 调用注册接口,传入认证信息
|
package/src/runtime.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function getYoyoRuntime(): PluginRuntime {
|
|
|
17
17
|
/**
|
|
18
18
|
* Yoyo 环境类型
|
|
19
19
|
*/
|
|
20
|
-
export type YoyoEnv = "test" | "production";
|
|
20
|
+
export type YoyoEnv = "test" | "production" | "dev";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* 获取当前运行环境
|
|
@@ -29,6 +29,6 @@ export function getYoyoEnv(): YoyoEnv {
|
|
|
29
29
|
return configManager.getEnv();
|
|
30
30
|
} catch (error) {
|
|
31
31
|
console.error(`[runtime] Failed to get env from config: ${error}`);
|
|
32
|
-
return "
|
|
32
|
+
return "production"; // 默认返回生产环境
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/schemas.ts
CHANGED
|
@@ -17,5 +17,5 @@ const UserInfoSchema = z.object({
|
|
|
17
17
|
export const YoyoPluginConfigSchema = z.object({
|
|
18
18
|
user: UserInfoSchema.optional(),
|
|
19
19
|
/** 运行环境:test 或 production */
|
|
20
|
-
env: z.enum(['test', 'production']).optional(),
|
|
20
|
+
env: z.enum(['dev', 'test', 'production']).optional(),
|
|
21
21
|
});
|