@honor-claw/yoyo 1.1.4-beta.5 → 1.1.4-beta.8

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.
@@ -26,7 +26,19 @@
26
26
  "device": {
27
27
  "type": "object",
28
28
  "properties": {
29
- "deviceType": {
29
+ "type": {
30
+ "type": "string"
31
+ },
32
+ "manufacture": {
33
+ "type": "string"
34
+ },
35
+ "brand": {
36
+ "type": "string"
37
+ },
38
+ "name": {
39
+ "type": "string"
40
+ },
41
+ "model": {
30
42
  "type": "string"
31
43
  }
32
44
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honor-claw/yoyo",
3
- "version": "1.1.4-beta.5",
3
+ "version": "1.1.4-beta.8",
4
4
  "description": "OpenClaw Honor Yoyo connection plugin",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,10 +1,7 @@
1
- /**
2
- * Claw Cloud API 接口封装
3
- */
1
+ import { takeApiHost } from "../modules/configs/hosts.js";
4
2
  import type { GatewayAuthConfig } from "../modules/configs/types.js";
5
3
  import type { DeviceInfo, HonorUserInfo } from "../types.js";
6
4
  import { uuid } from "../utils/id.js";
7
- import { generateAuthorization } from "../utils/jwt.js";
8
5
  import { HttpClient, type HttpResponse, type HttpClientOptions } from "./http-client.js";
9
6
  import type {
10
7
  DeviceRegistryHeaders,
@@ -13,7 +10,13 @@ import type {
13
10
  UserLogoutHeaders,
14
11
  UserLogoutRequest,
15
12
  UserLogoutResponse,
13
+ ExchangeTokenRequest,
14
+ ExchangeTokenHeaders,
15
+ TokenResponseV2,
16
+ HttpApiWrapper,
17
+ ExchangeTokenParams,
16
18
  } from "./types.js";
19
+ import { isOKResponse } from "./helpers.js";
17
20
 
18
21
  /**
19
22
  * Claw Cloud API 客户端
@@ -40,24 +43,17 @@ export class ClawCloudClient {
40
43
  // 构造请求头
41
44
  const headers: DeviceRegistryHeaders = {
42
45
  "x-trace-id": uuid(),
46
+ "x-jwt-token": userInfo.token,
47
+ "x-device-id": deviceInfo.deviceId,
43
48
  };
44
49
 
45
- if (userInfo.userId) {
46
- headers["x-agw-userId"] = userInfo.userId;
47
- headers["authorization"] = generateAuthorization(userInfo.userId);
48
- headers["x-udid"] = deviceInfo.deviceId;
49
- } else if (userInfo.token) {
50
- headers["x-jwt-token"] = userInfo.token;
51
- headers["x-device-id"] = deviceInfo.deviceId;
52
- }
53
-
54
50
  const requestBody: DeviceRegistryRequest = {
55
51
  businessTag: "YOYO_CLAW",
56
52
  role: "yoyoclaw",
57
53
  deviceInfo: {
58
54
  ...deviceInfo,
59
- manufacture: "unknown",
60
- brand: deviceInfo.brand || "unknown",
55
+ manufacture: deviceInfo.manufacture,
56
+ brand: deviceInfo.brand,
61
57
  bizExtInfo: gatewayAuthConfig,
62
58
  },
63
59
  };
@@ -81,24 +77,16 @@ export class ClawCloudClient {
81
77
  // 构造请求头
82
78
  const headers: UserLogoutHeaders = {
83
79
  "x-trace-id": uuid(),
80
+ "x-jwt-token": userInfo.token,
81
+ "x-device-id": deviceInfo.deviceId,
84
82
  };
85
83
 
86
- // 如果有 userId,添加 x-agw-userId
87
- if (userInfo.userId) {
88
- headers["x-agw-userId"] = userInfo.userId;
89
- headers["x-udid"] = deviceInfo.deviceId;
90
- headers["authorization"] = generateAuthorization(userInfo.userId);
91
- } else if (userInfo.token) {
92
- headers["x-jwt-token"] = userInfo.token;
93
- headers["x-device-id"] = deviceInfo.deviceId;
94
- }
95
-
96
84
  const requestBody: UserLogoutRequest = {
97
85
  businessTag: "YOYO_CLAW",
98
86
  deviceInfo: {
99
87
  ...deviceInfo,
100
- manufacture: "unknown",
101
- brand: "unknown",
88
+ manufacture: deviceInfo.manufacture,
89
+ brand: deviceInfo.brand,
102
90
  },
103
91
  };
104
92
 
@@ -107,14 +95,71 @@ export class ClawCloudClient {
107
95
  body: requestBody,
108
96
  });
109
97
  }
98
+
99
+ /**
100
+ * 使用授权码换取Token
101
+ * @param deviceInfo 设备信息
102
+ * @param params 支持两种模式:
103
+ * 1. userId模式: { userId: string } - 通过 x-agw-userId header 认证
104
+ * 2. auth模式: { code: string; authConfig: HonorAuthConfig } - 通过 code + clientId 认证
105
+ * @returns Token响应
106
+ */
107
+ async exchangeToken(
108
+ deviceInfo: DeviceInfo,
109
+ params: ExchangeTokenParams,
110
+ ): Promise<TokenResponseV2> {
111
+ // 构建请求头
112
+ const headers: ExchangeTokenHeaders = {
113
+ "Content-Type": "application/json",
114
+ "x-device-id": deviceInfo.deviceId,
115
+ "x-trace-id": uuid(),
116
+ };
117
+
118
+ let data: ExchangeTokenRequest = {};
119
+
120
+ if ("userId" in params) {
121
+ // userId模式: 通过 header 认证,body为空
122
+ headers["x-agw-userId"] = params.userId;
123
+ } else {
124
+ // auth模式: 通过 code + clientId 认证
125
+ data = {
126
+ clientId: params.authConfig.clientId,
127
+ code: params.code,
128
+ };
129
+ }
130
+
131
+ const response = await this.httpClient.post<HttpApiWrapper<TokenResponseV2>>(
132
+ "/v1/user/jwtToken",
133
+ {
134
+ body: data,
135
+ headers: headers as unknown as Record<string, string>,
136
+ timeout: 15000,
137
+ },
138
+ );
139
+
140
+ if (isOKResponse(response) && response.data.data) {
141
+ return response.data.data;
142
+ }
143
+
144
+ throw new Error(`failed to get token: ${response.data?.cnMessage}`);
145
+ }
110
146
  }
111
147
 
112
148
  /**
113
149
  * 创建 Claw Cloud 客户端实例
114
150
  */
115
- export function createClawCloudClient(
116
- baseUrl: string,
117
- options?: HttpClientOptions,
118
- ): ClawCloudClient {
151
+ export function createClawCloudClient(): ClawCloudClient {
152
+ const hosts = takeApiHost();
153
+ const baseUrl = `https://${hosts.clawCloud}/aicloud/yoyo-claw-service`;
154
+
155
+ // 如果有灰度标签,设置默认headers
156
+ const options = hosts.grayTag
157
+ ? {
158
+ defaultHeaders: {
159
+ "x-gray": hosts.grayTag,
160
+ },
161
+ }
162
+ : undefined;
163
+
119
164
  return new ClawCloudClient(baseUrl, options);
120
165
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { request, type Dispatcher, ProxyAgent } from "undici";
7
+ import { wrapError } from "../utils/error.js";
7
8
  import { getProxyUrl, shouldUseProxy } from "../utils/proxy.js";
8
9
 
9
10
  /**
@@ -170,9 +171,7 @@ export class HttpClient {
170
171
  if (error instanceof HttpError) {
171
172
  throw error;
172
173
  }
173
- throw new Error(
174
- `HTTP request failed: ${error instanceof Error ? error.message : String(error)}`,
175
- );
174
+ throw wrapError(error, "HTTP request failed");
176
175
  }
177
176
  }
178
177
 
package/src/apis/types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * 设备注册接口相关类型定义
3
3
  */
4
4
  import type { ClawDeviceInfo, DeviceRole } from "../types.js";
5
+ import type { HonorAuthConfig } from "../honor-auth/types.js";
5
6
 
6
7
  /**
7
8
  * 设备注册请求体
@@ -75,3 +76,38 @@ export interface UserLogoutHeaders {
75
76
  authorization?: string;
76
77
  "x-agw-userId"?: string;
77
78
  }
79
+
80
+ /**
81
+ * Token响应
82
+ */
83
+ export type TokenResponseV2 = {
84
+ jwtToken?: string;
85
+ };
86
+
87
+ /**
88
+ * 换取Token请求体
89
+ */
90
+ export interface ExchangeTokenRequest {
91
+ clientId?: string;
92
+ code?: string;
93
+ }
94
+
95
+ /**
96
+ * 换取Token参数 - 支持两种模式
97
+ * 1. userId模式: 通过 x-agw-userId header 认证,body为空
98
+ * 2. auth模式: 通过 code + clientId 认证
99
+ */
100
+ export type ExchangeTokenParams =
101
+ | { userId: string }
102
+ | { code: string; authConfig: HonorAuthConfig };
103
+
104
+ /**
105
+ * 换取Token请求头
106
+ */
107
+ export interface ExchangeTokenHeaders {
108
+ "Content-Type": "application/json";
109
+ "x-device-id": string;
110
+ "x-trace-id": string;
111
+ 'x-agw-userId'?: string;
112
+ "x-gray"?: string;
113
+ }
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import WebSocket from "ws";
6
6
  import { uuid } from "../utils/id.js";
7
- import { generateAuthorization } from "../utils/jwt.js";
8
7
  import { useClawLogger } from "../utils/logger.js";
9
8
  import { createWebSocketProxyAgent } from "../utils/proxy.js";
10
9
  import { rawDataToString } from "../utils/ws.js";
@@ -69,20 +68,10 @@ export class ClawCloudSocketClient {
69
68
  const headers: Record<string, string> = {
70
69
  ["x-role"]: "yoyoclaw",
71
70
  ["x-trace-id"]: traceId,
71
+ "x-jwt-token": userInfo.token,
72
+ "x-device-id": deviceInfo.deviceId,
72
73
  };
73
74
 
74
- if (deviceInfo.deviceId) {
75
- headers["x-device-id"] = deviceInfo.deviceId;
76
- }
77
-
78
- if (userInfo.userId) {
79
- headers["x-agw-userId"] = userInfo.userId;
80
- headers["authorization"] = generateAuthorization(userInfo.userId);
81
- } else if (userInfo.token) {
82
- // v2版本,基于jwt token
83
- headers["x-jwt-token"] = userInfo.token;
84
- }
85
-
86
75
  if (deviceInfo.port !== undefined) {
87
76
  headers["x-port"] = String(deviceInfo.port);
88
77
  }
@@ -444,7 +444,7 @@ export class MessageHandler {
444
444
  this.sendContextResponse(msgType, sourceDeviceId, traceInfo, reqContext, {
445
445
  ok: false,
446
446
  error: result.error,
447
- ...(result?.data || {}),
447
+ ...result?.data,
448
448
  ...(isUpdate && { name: contextName }),
449
449
  });
450
450
  }
@@ -19,7 +19,7 @@ export interface YOYOClawServiceEvent {
19
19
  sourceDeviceInfo?: ClawDeviceInfo;
20
20
  targetRole?: DeviceRole;
21
21
  targetDeviceId: string;
22
- traceInfo: Object;
22
+ traceInfo: object;
23
23
  port: number | string;
24
24
  data?: string; // openclaw原生消息
25
25
  msgType: "userMessage" | "devicePairMessage" | "deviceUnPairMessage" | "fetchContexts" | "updateContexts";
@@ -1,24 +1,19 @@
1
1
  import { type Command } from "commander";
2
2
  import { type OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { performLogin } from "../../modules/login/impl.js";
4
- import { isBetaVersion } from "../../utils/version.js";
5
4
 
6
5
  export function registerLoginCommand(api: OpenClawPluginApi, command: Command) {
7
6
  let nextCommand = command.command("login").description("login to yoyoclaw and register devices");
8
7
 
9
- if (isBetaVersion()) {
10
- nextCommand = nextCommand.option("--skip-auth", "debug mode, no auth required");
11
- }
12
-
13
8
  nextCommand = nextCommand
14
9
  .option("-u, --uid <userId>", "user ID for direct login")
15
10
  .option("--token <token>", "token for direct login")
16
11
  .action(async (options) => {
17
- const { uid: userId, token, skipAuth } = options;
12
+ const { uid: userId, token } = options;
18
13
 
19
14
  api.logger.debug?.("honor login CLI command called");
20
15
 
21
- await performLogin({ userId, token, noAuth: skipAuth });
16
+ await performLogin({ userId, token });
22
17
  });
23
18
 
24
19
  return nextCommand;
@@ -111,36 +111,6 @@ function displayDetailedStatus(statusData: ConnectionStatusData | null): void {
111
111
  console.log(` ${formatDateTime(statusData.updatedAt)}`);
112
112
  }
113
113
 
114
- /**
115
- * Get emoji for connection status
116
- */
117
- function getStateEmoji(status: "idle" | "connecting" | "connected"): string {
118
- switch (status) {
119
- case "connected":
120
- return "✅";
121
- case "connecting":
122
- return "⏳";
123
- case "idle":
124
- default:
125
- return "⚪";
126
- }
127
- }
128
-
129
- /**
130
- * Get text for connection status
131
- */
132
- function getStateText(status: "idle" | "connecting" | "connected"): string {
133
- switch (status) {
134
- case "connected":
135
- return "Connected";
136
- case "connecting":
137
- return "Connecting";
138
- case "idle":
139
- default:
140
- return "Disconnected";
141
- }
142
- }
143
-
144
114
  /**
145
115
  * Format date time (YYYY-MM-DD HH:mm:ss)
146
116
  */
@@ -7,12 +7,13 @@ import { startCallbackServer } from "./callback-server.js";
7
7
  import { getConfig } from "./config.js";
8
8
  import { saveToken } from "./token-manager.js";
9
9
  import type { HonorAuthConfig } from "./types.js";
10
+ import { createClawCloudClient } from "../apis/claw-cloud.js";
10
11
 
11
12
  /**
12
13
  * 执行OAuth2授权流程(自动打开浏览器)
13
14
  */
14
15
  export async function performOAuth2AuthWithBrowser(
15
- _: DeviceInfo,
16
+ deviceInfo: DeviceInfo,
16
17
  configOverrides?: Partial<HonorAuthConfig>,
17
18
  ) {
18
19
  const config = getConfig(configOverrides);
@@ -63,19 +64,18 @@ export async function performOAuth2AuthWithBrowser(
63
64
 
64
65
  // 使用授权码换取Token
65
66
  try {
66
- const tokenInfo = await authClient.exchangeToken(receivedCode);
67
+ const client = createClawCloudClient();
68
+ const tokenInfo = await client.exchangeToken(deviceInfo, { code: receivedCode, authConfig: config });
67
69
 
68
- if (!tokenInfo?.token || !tokenInfo.userInfo?.userId) {
69
- throw new Error("failed to get user info");
70
+ if (!tokenInfo?.jwtToken) {
71
+ throw new Error("failed to get jwt token");
70
72
  }
71
73
 
72
74
  // 保存Token
73
- await saveToken(tokenInfo, config.saveToFile !== false);
75
+ await saveToken({ jwtToken: tokenInfo.jwtToken }, config.saveToFile !== false);
74
76
 
75
77
  return {
76
- userId: tokenInfo.userInfo.userId,
77
- token: tokenInfo.token,
78
- userName: tokenInfo.userInfo.displayName,
78
+ token: tokenInfo.jwtToken
79
79
  } as HonorUserInfo;
80
80
  } catch (error) {
81
81
  throw wrapError(error, "get token failed");
@@ -1,8 +1,9 @@
1
1
  import { createClawCloudClient } from "../apis/claw-cloud.js";
2
2
  import { isOKResponse } from "../apis/index.js";
3
- import { getConfigManager, takeApiHost } from "../modules/configs/index.js";
3
+ import { getConfigManager } from "../modules/configs/index.js";
4
4
  import { getDeviceInfo } from "../modules/device/device-info.js";
5
5
  import type { HonorUserInfo } from "../types.js";
6
+ import { wrapError } from "../utils/error.js";
6
7
  import { clearToken } from "./token-manager.js";
7
8
 
8
9
  /**
@@ -22,24 +23,10 @@ export async function performLogout(): Promise<void> {
22
23
  const deviceInfo = await getDeviceInfo();
23
24
 
24
25
  // 调用登出接口
25
- const hosts = takeApiHost();
26
- const baseUrl = `https://${hosts.clawCloud}/aicloud/yoyo-claw-service`;
27
-
28
- // 如果有灰度标签,设置默认headers
29
- const options = hosts.grayTag
30
- ? {
31
- defaultHeaders: {
32
- "x-gray": hosts.grayTag,
33
- },
34
- }
35
- : undefined;
36
-
37
- const client = createClawCloudClient(baseUrl, options);
26
+ const client = createClawCloudClient();
38
27
 
39
28
  const userInfo: HonorUserInfo = {
40
- userId: userConfig.userId,
41
29
  token: userConfig.token,
42
- userName: userConfig.userName,
43
30
  };
44
31
 
45
32
  const response = await client.logoutDevice(deviceInfo, userInfo);
@@ -51,7 +38,6 @@ export async function performLogout(): Promise<void> {
51
38
  // 清除Token
52
39
  await clearToken();
53
40
  } catch (error) {
54
- const errorMessage = error instanceof Error ? error.message : String(error);
55
- throw new Error(`failed to logout: ${errorMessage}`);
41
+ throw wrapError(error, "failed to logout");
56
42
  }
57
43
  }
@@ -4,13 +4,7 @@
4
4
  */
5
5
 
6
6
  import { createHash, randomBytes } from "crypto";
7
- import type { TokenResponse, HonorAuthConfig } from "./types.js";
8
- import { takeApiHost } from "../modules/configs/hosts.js";
9
- import { wrapError } from "../utils/error.js";
10
- import { uuid } from "../utils/id.js";
11
- import { isOKResponse } from "../apis/helpers.js";
12
- import { HttpClient, type HttpClientOptions } from "../apis/http-client.js";
13
- import type { HttpApiWrapper } from "../apis/types.js";
7
+ import type { HonorAuthConfig } from "./types.js";
14
8
 
15
9
  /**
16
10
  * PKCE参数
@@ -24,16 +18,10 @@ export interface PKCEParams {
24
18
  * 荣耀认证客户端
25
19
  */
26
20
  export class HonorAuthClient {
27
- private httpClient: HttpClient;
28
21
  private config: HonorAuthConfig;
29
22
 
30
23
  constructor(config: HonorAuthConfig) {
31
24
  this.config = config;
32
- // 从 HonorAuthConfig 中提取 HttpClientOptions
33
- const httpClientOptions: HttpClientOptions = {
34
- proxy: config.proxy,
35
- };
36
- this.httpClient = new HttpClient(config.authHost, httpClientOptions);
37
25
  }
38
26
 
39
27
  /**
@@ -99,50 +87,6 @@ export class HonorAuthClient {
99
87
  return `${this.config.authHost}/oauth2/v3/authorize?${params.toString()}`;
100
88
  }
101
89
 
102
- /**
103
- * 使用授权码换取Token
104
- */
105
- async exchangeToken(code: string) {
106
- const hosts = takeApiHost();
107
- const tokenUrl = `https://${hosts.ics}/honorboard/auth/v1/oauth/jwtToken`;
108
-
109
- const data = {
110
- clientId: this.config.clientId,
111
- code,
112
- redirectUri: this.config.redirectUri,
113
- };
114
-
115
- try {
116
- // 构建请求头,包含灰度标签(如果有)
117
- const headers: Record<string, string> = {
118
- "Content-Type": "application/json",
119
- "x-origin-udid": this.config.clientId,
120
- "x-app-pkg": "com.hihonor.pc.openclaw",
121
- "x-request-nonce": uuid(),
122
- "x-timestamp": String(Date.now()),
123
- };
124
-
125
- // 添加灰度标签
126
- if (hosts.grayTag) {
127
- headers["x-gray"] = hosts.grayTag;
128
- }
129
-
130
- const response = await this.httpClient.post<HttpApiWrapper<TokenResponse>>(tokenUrl, {
131
- body: data,
132
- headers,
133
- timeout: 15000,
134
- });
135
-
136
- if (isOKResponse(response)) {
137
- return response.data.data;
138
- }
139
-
140
- throw new Error(`failed to get token: ${response.data?.cnMessage}`);
141
- } catch (error) {
142
- throw wrapError(error, "failed to get token");
143
- }
144
- }
145
-
146
90
  /**
147
91
  * 获取配置
148
92
  */
@@ -2,3 +2,4 @@ export * from "./browser.js";
2
2
  export * from "./cloud.js";
3
3
  export * from "./token-manager.js";
4
4
  export { createHonorAuthClient, HonorAuthClient, type PKCEParams } from "./honor-auth-client.js";
5
+ export * from "./types.js";
@@ -2,65 +2,130 @@
2
2
  * Token管理器 - 负责token的读写缓存
3
3
  */
4
4
 
5
+ import { createClawCloudClient } from "../apis/claw-cloud.js";
5
6
  import { getConfigManager } from "../modules/configs/index.js";
7
+ import { getDeviceInfo } from "../modules/device/index.js";
6
8
  import type { HonorUserInfo } from "../types.js";
9
+ import { getEnvUserId } from "../utils/env.js";
10
+ import { wrapError } from "../utils/error.js";
7
11
  import { useClawLogger } from "../utils/logger.js";
8
- import type { TokenResponse } from "./types.js";
12
+ import { SaveTokenParams } from "./types.js";
9
13
 
10
14
  /**
11
- * 保存Token到openclaw配置文件
15
+ * 保存Token到openclaw配置文件(支持UserId/token双模)
16
+ * - 如果有 jwtToken,直接保存
17
+ * - 如果只有 userId,调用 exchangeToken 通过 userId 换取 jwtToken
12
18
  */
13
- export async function saveToken(token: TokenResponse, saveToFile: boolean = true): Promise<void> {
19
+ export async function saveToken(
20
+ params: SaveTokenParams,
21
+ saveToFile: boolean = true,
22
+ ): Promise<HonorUserInfo> {
14
23
  try {
24
+ const configManager = getConfigManager();
25
+ let jwtToken = params.jwtToken;
26
+
27
+ // 如果没有 jwtToken 但有 userId,通过 exchangeToken 获取
28
+ if (!jwtToken && params.userId) {
29
+ const deviceInfo = await getDeviceInfo();
30
+ const client = createClawCloudClient();
31
+ const tokenInfo = await client.exchangeToken(deviceInfo, { userId: params.userId });
32
+ jwtToken = tokenInfo.jwtToken;
33
+ }
34
+
35
+ if (!jwtToken) {
36
+ throw new Error("no token available");
37
+ }
38
+
15
39
  if (!saveToFile) {
16
40
  useClawLogger().info("💾 token got, but not to save");
17
- return;
41
+ return { token: jwtToken };
18
42
  }
19
43
 
20
- const configManager = getConfigManager();
21
-
22
44
  // 计算过期时间(假设token有效期为30天)
23
45
  const expired = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60;
24
46
 
25
47
  // 更新用户配置
26
48
  await configManager.updateUserConfig({
27
- token: token.token,
49
+ token: jwtToken,
50
+ userId: params.userId,
28
51
  expired,
29
- userId: token.userInfo?.userId,
30
- userName: token.userInfo?.displayName,
31
52
  });
53
+
54
+ return {
55
+ token: jwtToken,
56
+ };
32
57
  } catch (error) {
33
- throw new Error(`保存Token失败: ${error instanceof Error ? error.message : String(error)}`);
58
+ throw wrapError(error, "保存Token失败");
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 刷新并保存Token
64
+ */
65
+ async function refreshToken(userId: string): Promise<HonorUserInfo | null> {
66
+ const deviceInfo = await getDeviceInfo();
67
+ const client = createClawCloudClient();
68
+ const tokenInfo = await client.exchangeToken(deviceInfo, { userId });
69
+
70
+ if (tokenInfo.jwtToken) {
71
+ const expired = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60;
72
+ await getConfigManager().updateUserConfig({
73
+ token: tokenInfo.jwtToken,
74
+ userId: undefined,
75
+ userName: undefined,
76
+ expired,
77
+ });
78
+ return { token: tokenInfo.jwtToken };
34
79
  }
80
+ await getConfigManager().clearUserConfig();
81
+ return null;
35
82
  }
36
83
 
37
84
  /**
38
85
  * 从openclaw配置文件加载Token
86
+ * - 配置有 userId 时,优先使用 env userId 换取 token
87
+ * - 有 token 且未过期,直接返回
39
88
  */
40
89
  export async function loadToken(): Promise<HonorUserInfo | null> {
90
+ const logger = useClawLogger();
91
+
41
92
  try {
42
93
  const configManager = getConfigManager();
43
94
  const userConfig = configManager.getUserConfig();
95
+ const envUserId = getEnvUserId();
44
96
 
45
- // todo 上线前移除
46
- if (!userConfig?.token) {
47
- return null;
97
+ logger.debug?.(
98
+ `[yoyoclaw-auth] env userId: ${envUserId ? "present" : "absent"}, config userId: ${userConfig?.userId ? "present" : "absent"}, config token: ${userConfig?.token ? "present" : "absent"}`,
99
+ );
100
+
101
+ // 配置有 userId,优先使用 env userId 换取 token
102
+ if (userConfig?.userId) {
103
+ return await refreshToken(envUserId || userConfig.userId);
104
+ }
105
+
106
+ // 有 token 且未过期,直接返回
107
+ if (userConfig?.token) {
108
+ if (!userConfig.expired || userConfig.expired >= Math.floor(Date.now() / 1000)) {
109
+ return { token: userConfig.token };
110
+ }
111
+ logger.debug?.("[yoyoclaw-auth] cached token expired");
48
112
  }
49
113
 
50
- // 检查是否过期
51
- if (userConfig.expired && userConfig.expired < Math.floor(Date.now() / 1000)) {
52
- // Token已过期,从配置中移除
114
+ // 没有token或者已经过期,拿envUserId重新获取
115
+ if (envUserId) {
116
+ logger.debug?.("[yoyoclaw-auth] token expired, using env userId to exchange token");
117
+ return await refreshToken(envUserId);
118
+ }
119
+
120
+ // 没有可用的鉴权信息且token过期了,清除token信息
121
+ if (userConfig?.token) {
122
+ logger.debug?.("[yoyoclaw-auth] clearing expired token");
53
123
  await configManager.clearUserConfig();
54
- return null;
55
124
  }
56
125
 
57
- return {
58
- userId: userConfig.userId || "",
59
- token: userConfig.token,
60
- userName: userConfig.userName,
61
- };
126
+ return null;
62
127
  } catch (error) {
63
- throw new Error(`加载Token失败: ${error instanceof Error ? error.message : String(error)}`);
128
+ throw wrapError(error, "加载Token失败");
64
129
  }
65
130
  }
66
131
 
@@ -73,6 +138,6 @@ export async function clearToken(): Promise<void> {
73
138
  await configManager.clearUserConfig();
74
139
  useClawLogger().info("[yoyoclaw-auth] token cleared");
75
140
  } catch (error) {
76
- throw new Error(`清除Token失败: ${error instanceof Error ? error.message : String(error)}`);
141
+ throw wrapError(error, "清除Token失败");
77
142
  }
78
143
  }
@@ -26,25 +26,7 @@ export interface HonorAuthConfig {
26
26
  saveToFile?: boolean;
27
27
  }
28
28
 
29
- /**
30
- * Token响应
31
- */
32
- export type TokenResponse = {
33
- token?: string;
34
- userInfo?: ApiUserInfo;
35
- };
36
-
37
- /**
38
- * Token响应
39
- */
40
- export type TokenResponseV2 = {
29
+ export interface SaveTokenParams {
30
+ userId?: string;
41
31
  jwtToken?: string;
42
- };
43
-
44
- /**
45
- * 用户信息
46
- */
47
- export interface ApiUserInfo {
48
- userId: string;
49
- displayName: string;
50
32
  }
@@ -8,7 +8,7 @@ import { wrapError } from "../../utils/error.js";
8
8
  import { useClawLogger } from "../../utils/logger.js";
9
9
  import { isBetaVersion } from "../../utils/version.js";
10
10
  import { detectProvidersToRename, updateProviderReferences } from "./provider.js";
11
- import type { GatewayAuthConfig, UserConfig } from "./types.ts";
11
+ import type { DeviceConfig, GatewayAuthConfig, UserConfig } from "./types.ts";
12
12
 
13
13
  const PLUGIN_YOYO_ID = "yoyo";
14
14
  const YOYO_ALLOW_COMMANDS = [
@@ -167,6 +167,60 @@ export class ConfigManager {
167
167
  }
168
168
  }
169
169
 
170
+ /**
171
+ * 获取设备配置
172
+ */
173
+ getDeviceConfig(): DeviceConfig | undefined {
174
+ try {
175
+ const config = this.loadConfig();
176
+ const deviceConfig = config.plugins?.entries?.[PLUGIN_YOYO_ID]?.config?.device as
177
+ | DeviceConfig
178
+ | undefined;
179
+ return deviceConfig;
180
+ } catch (error) {
181
+ console.error(`[claw-configs] Failed to read device config: ${error}`);
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * 更新设备配置
188
+ */
189
+ async updateDeviceConfig(deviceConfig: DeviceConfig): Promise<void> {
190
+ try {
191
+ const currentConfig = this.loadConfig();
192
+
193
+ // 合并现有设备配置
194
+ const existingDeviceConfig =
195
+ (currentConfig.plugins?.entries?.[PLUGIN_YOYO_ID]?.config?.device as DeviceConfig | undefined) || {};
196
+
197
+ const updatedConfig: OpenClawConfig = {
198
+ ...currentConfig,
199
+ plugins: {
200
+ ...currentConfig.plugins,
201
+ entries: {
202
+ ...currentConfig.plugins?.entries,
203
+ [PLUGIN_YOYO_ID]: {
204
+ ...currentConfig.plugins?.entries?.[PLUGIN_YOYO_ID],
205
+ enabled: true,
206
+ config: {
207
+ ...currentConfig.plugins?.entries?.[PLUGIN_YOYO_ID]?.config,
208
+ device: {
209
+ ...existingDeviceConfig,
210
+ ...deviceConfig,
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ };
217
+
218
+ await this.saveConfig(updatedConfig);
219
+ } catch (error) {
220
+ throw wrapError(error, "Failed to update device config");
221
+ }
222
+ }
223
+
170
224
  /**
171
225
  * 更新运行环境配置
172
226
  */
@@ -19,7 +19,7 @@ export interface UserConfig {
19
19
  token?: string;
20
20
  expired?: number;
21
21
  userId?: string;
22
- userName?: string;
22
+ userName?: undefined;
23
23
  }
24
24
 
25
25
  /**
@@ -33,5 +33,18 @@ export interface YoyoClawPluginConfig {
33
33
  env?: "dev" | "test" | "production";
34
34
  /** 灰度标签 */
35
35
  gray?: string;
36
+ /** 设备配置 */
37
+ device?: DeviceConfig;
36
38
  };
37
39
  }
40
+
41
+ /**
42
+ * 设备配置信息
43
+ */
44
+ export interface DeviceConfig {
45
+ brand?: string;
46
+ type?: string;
47
+ manufacture?: string;
48
+ name?: string;
49
+ model?: string;
50
+ }
@@ -2,6 +2,7 @@
2
2
  * 设备信息获取模块
3
3
  */
4
4
  import type { DeviceInfo } from "../../types.js";
5
+ import { getDeviceEnvVars } from "../../utils/env.js";
5
6
  import { useClawLogger } from "../../utils/logger.js";
6
7
  import { getConfigManager } from "../configs/config-manager.js";
7
8
  import { loadOrCreateDeviceIdentity } from "./identity.js";
@@ -9,6 +10,7 @@ import { getDeviceInfoProvider } from "./providers/index.js";
9
10
 
10
11
  /**
11
12
  * 获取设备信息(异步版本)
13
+ * 优先级: env > config > provider
12
14
  */
13
15
  export async function getDeviceInfo(): Promise<DeviceInfo> {
14
16
  const provider = getDeviceInfoProvider();
@@ -31,17 +33,57 @@ export async function getDeviceInfo(): Promise<DeviceInfo> {
31
33
  deviceId = await provider.getDeviceIdAsync();
32
34
  }
33
35
 
36
+ // 获取设备信息,优先级: env > config > provider
37
+ const envVars = getDeviceEnvVars();
38
+ const deviceConfig = configManager.getDeviceConfig();
39
+
40
+ useClawLogger().debug?.(`[yoyoclaw-device] env: ${JSON.stringify(envVars)}, config: ${JSON.stringify(deviceConfig)}`);
41
+
42
+ // 按优先级获取,并限制 128 字符
43
+ const brand = envVars.brand || deviceConfig?.brand || provider.getDeviceBrand() || "unknown";
44
+ const deviceType = (envVars.deviceType ||
45
+ deviceConfig?.type ||
46
+ provider.getDeviceType()) as DeviceInfo["deviceType"];
47
+ const manufacture = (envVars.manufacture || deviceConfig?.manufacture || brand).slice(0, 128);
48
+ const deviceName = (deviceConfig?.name || provider.getDeviceName()).slice(0, 128);
49
+ const deviceModel = (deviceConfig?.model || provider.getDeviceModel()).slice(0, 128);
50
+
34
51
  const deviceInfo: DeviceInfo = {
35
52
  deviceId,
36
- deviceName: provider.getDeviceName().slice(0, 128),
37
- deviceType: provider.getDeviceType(),
38
- deviceModel: provider.getDeviceModel().slice(0, 128),
39
- brand: provider.getDeviceBrand(),
53
+ deviceName,
54
+ deviceType,
55
+ deviceModel,
56
+ brand,
57
+ manufacture,
40
58
  port: configManager.getGatewayPort(),
41
59
  };
42
60
 
43
- useClawLogger().debug?.(
44
- `[yoyoclaw-device] device info: ${JSON.stringify(deviceInfo)} (deviceId source: ${deviceIdSource})`,
61
+ // 记录 deviceInfo 整体来源
62
+ const source = envVars.brand || envVars.deviceType || envVars.manufacture ? "env" : "config";
63
+ useClawLogger().info(
64
+ `[yoyoclaw-device] device info: ${JSON.stringify(deviceInfo)} (deviceId source: ${deviceIdSource}, source: ${source})`,
65
+ );
66
+
67
+ // env 与 config 的差异检测(key 映射: brand, manufacture, deviceType -> type)
68
+ const hasEnvDiff = !!(
69
+ (envVars.brand && envVars.brand !== deviceConfig?.brand) ||
70
+ (envVars.manufacture && envVars.manufacture !== deviceConfig?.manufacture) ||
71
+ (envVars.deviceType && envVars.deviceType !== deviceConfig?.type)
45
72
  );
73
+
74
+ if (!deviceConfig || hasEnvDiff) {
75
+ const configUpdate = {
76
+ brand,
77
+ type: deviceType,
78
+ manufacture,
79
+ name: deviceName,
80
+ model: deviceModel,
81
+ };
82
+
83
+ configManager.updateDeviceConfig(configUpdate).catch((error) => {
84
+ useClawLogger().warn(`[yoyoclaw-device] failed to persist device config: ${String(error)}`);
85
+ });
86
+ }
87
+
46
88
  return deviceInfo;
47
89
  }
@@ -1,72 +1,99 @@
1
- import { execSync } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { createHash } from "crypto";
3
3
  import fs from "fs";
4
4
  import os from "os";
5
+ import { promisify } from "util";
5
6
  import type { DeviceType } from "../../../types.js";
6
7
  import type { DeviceInfoProvider } from "./base.js";
7
8
 
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ /**
12
+ * 设备信息缓存
13
+ */
14
+ interface DeviceInfoCache {
15
+ androidId: string;
16
+ brand: string;
17
+ model: string;
18
+ device: string;
19
+ deviceId: string;
20
+ }
21
+
8
22
  /**
9
23
  * Pad 设备信息提供者
10
24
  */
11
25
  export class PadDeviceInfoProvider implements DeviceInfoProvider {
26
+ private cache: DeviceInfoCache = {
27
+ androidId: "",
28
+ brand: "",
29
+ model: "",
30
+ device: "",
31
+ deviceId: "",
32
+ };
33
+
34
+ private initPromise: Promise<void>;
35
+
36
+ constructor() {
37
+ this.initPromise = this._initializeCache();
38
+ }
39
+
12
40
  /**
13
- * 确保 provider 初始化完成(Pad 无需异步初始化)
41
+ * 确保 provider 初始化完成(异步预取设备信息)
14
42
  */
15
43
  async ensureInitialized(): Promise<void> {
16
- // Pad provider 同步执行,无需初始化
44
+ await this.initPromise;
17
45
  }
18
46
 
19
47
  /**
20
- * 通过 /system/bin/getprop 获取 Android 系统属性
48
+ * 异步执行 Android shell 命令
21
49
  */
22
- private getAndroidProp(prop: string): string {
50
+ private async execAndroidCmd(cmd: string, timeout = 5000): Promise<string> {
23
51
  try {
24
- const result = execSync(`/system/bin/getprop ${prop}`, {
52
+ const { stdout } = await execFileAsync("/system/bin/sh", ["-c", cmd], {
53
+ timeout,
25
54
  encoding: "utf-8",
26
- timeout: 5000,
27
55
  });
28
- const value = result.trim();
29
- return value || "";
56
+ return stdout.trim();
30
57
  } catch {
31
58
  return "";
32
59
  }
33
60
  }
34
61
 
35
62
  /**
36
- * 获取 Android ID
63
+ * 初始化缓存(异步预取所有设备信息)
37
64
  */
38
- private getAndroidId(): string {
65
+ private async _initializeCache(): Promise<void> {
39
66
  try {
40
- const result = execSync("/system/bin/settings get secure android_id", {
41
- encoding: "utf-8",
42
- timeout: 5000,
43
- });
44
- const value = result.trim();
45
- return value && value !== "null" ? value : "";
67
+ // 并行获取所有属性
68
+ const [androidId, brand, model, device] = await Promise.all([
69
+ this.execAndroidCmd("/system/bin/settings get secure android_id"),
70
+ this.execAndroidCmd("/system/bin/getprop ro.product.brand"),
71
+ this.execAndroidCmd("/system/bin/getprop ro.product.model"),
72
+ this.execAndroidCmd("/system/bin/getprop ro.product.device"),
73
+ ]);
74
+
75
+ this.cache.androidId = androidId && androidId !== "null" ? androidId : "";
76
+ this.cache.brand = brand || "";
77
+ this.cache.model = model || "";
78
+ this.cache.device = device || "";
79
+
80
+ // 计算 deviceId
81
+ const raw = `${this.cache.androidId}_${this.cache.brand}_${this.cache.model}_${this.cache.device}`;
82
+ this.cache.deviceId = createHash("sha256").update(raw, "utf-8").digest("hex");
46
83
  } catch {
47
- return "";
84
+ // 初始化失败,保持默认值
48
85
  }
49
86
  }
50
87
 
51
88
  /**
52
- * 获取 Android 设备唯一ID (SHA256 哈希)
53
- */
54
- private getAndroidDeviceId(): string {
55
- const androidId = this.getAndroidId();
56
- const brand = this.getAndroidProp("ro.product.brand");
57
- const model = this.getAndroidProp("ro.product.model");
58
- const device = this.getAndroidProp("ro.product.device");
59
-
60
- const raw = `${androidId}_${brand}_${model}_${device}`;
61
- return createHash("sha256").update(raw, "utf-8").digest("hex");
62
- }
63
-
64
- /**
65
- * 获取 Android 设备型号
89
+ * 获取 Android 设备型号(兜底读取 build.prop)
66
90
  */
67
91
  private getAndroidDeviceModel(): string {
92
+ if (this.cache.model) {
93
+ return this.cache.model;
94
+ }
95
+
68
96
  try {
69
- // 尝试直接解析系统属性文件
70
97
  const buildPropPath = "/system/build.prop";
71
98
  if (fs.existsSync(buildPropPath)) {
72
99
  const content = fs.readFileSync(buildPropPath, "utf8");
@@ -79,7 +106,6 @@ export class PadDeviceInfoProvider implements DeviceInfoProvider {
79
106
  // ignore
80
107
  }
81
108
 
82
- // 兜底方案:使用架构信息
83
109
  return `Android(${os.arch()})`;
84
110
  }
85
111
 
@@ -94,8 +120,8 @@ export class PadDeviceInfoProvider implements DeviceInfoProvider {
94
120
  * 获取设备唯一ID(异步)
95
121
  */
96
122
  async getDeviceIdAsync(): Promise<string> {
97
- const deviceId = this.getAndroidDeviceId();
98
- return deviceId || "invalid";
123
+ await this.initPromise;
124
+ return this.cache.deviceId || "invalid";
99
125
  }
100
126
 
101
127
  /**
@@ -123,8 +149,7 @@ export class PadDeviceInfoProvider implements DeviceInfoProvider {
123
149
  * 获取设备品牌
124
150
  */
125
151
  getDeviceBrand(): string {
126
- const brand = this.getAndroidProp("ro.product.brand");
127
-
152
+ const brand = this.cache.brand;
128
153
  return brand.toLowerCase() === "honor" ? "HONOR" : "";
129
154
  }
130
155
  }
@@ -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, takeApiHost } from "../configs/index.js";
4
+ import { getConfigManager } from "../configs/index.js";
5
5
 
6
6
  /**
7
7
  * 注册设备到 Claw Cloud
@@ -16,19 +16,7 @@ export async function registerDevice(deviceInfo: DeviceInfo, userInfo: HonorUser
16
16
  const gatewayAuthConfig = configManager.getGatewayAuthConfig();
17
17
 
18
18
  // 创建 Claw Cloud 客户端
19
- const hosts = takeApiHost();
20
- const baseUrl = `https://${hosts.clawCloud}/aicloud/yoyo-claw-service`;
21
-
22
- // 如果有灰度标签,设置默认headers
23
- const options = hosts.grayTag
24
- ? {
25
- defaultHeaders: {
26
- "x-gray": hosts.grayTag,
27
- },
28
- }
29
- : undefined;
30
-
31
- const client = createClawCloudClient(baseUrl, options);
19
+ const client = createClawCloudClient();
32
20
 
33
21
  // 调用注册接口,传入认证信息
34
22
  const response = await client.registerDevice(deviceInfo, userInfo, gatewayAuthConfig);
@@ -12,8 +12,6 @@ import { getDeviceInfo, registerDevice } from "../device/index.js";
12
12
  * 登录选项
13
13
  */
14
14
  export interface LoginOptions {
15
- /** 是否跳过认证流程,默认为 false */
16
- noAuth?: boolean;
17
15
  /** 用户 ID,如果提供则直接使用此 ID 登录 */
18
16
  userId?: string;
19
17
  /** Token,如果提供则直接使用此 Token 登录 */
@@ -30,7 +28,7 @@ export interface LoginOptions {
30
28
  * @param options 登录选项
31
29
  */
32
30
  export async function performLogin(options: LoginOptions = {}) {
33
- const { noAuth = false, userId, token } = options;
31
+ const { userId } = options;
34
32
  const logger = useClawLogger();
35
33
  try {
36
34
  logger.debug?.("Starting login process...");
@@ -53,43 +51,18 @@ export async function performLogin(options: LoginOptions = {}) {
53
51
  let userInfo: HonorUserInfo;
54
52
  // 检查是否提供了 userId 参数
55
53
  if (userId) {
56
- // 暂时使用 vm-mock 来解决 token 没有的问题,后面需要等云侧接口就绪后获取到这个 jwtToken
57
- const authToken = token || "vm-mock";
58
- logger.debug?.("Using provided userId for direct login...");
59
- userInfo = {
54
+ // 保存 Token,内部会基于userId换token
55
+ userInfo = await saveToken({
60
56
  userId,
61
- token: authToken,
62
- };
63
-
64
- // 保存 Token
65
- await saveToken({
66
- token: authToken,
67
- userInfo: { userId, displayName: "" },
68
57
  });
69
58
  logger.debug?.("Using provided user info");
70
- logger.debug?.(`User: ${userInfo.userId}`);
71
- logger.debug?.(`Device ID: ${deviceInfo?.deviceId}`);
72
- } else if (noAuth) {
73
- logger.debug?.("Skipping OAuth2 auth, using test user info...");
74
- userInfo = {
75
- userId: "test",
76
- token: "",
77
- };
78
-
79
- // 保存 Token
80
- await saveToken({
81
- token: "test",
82
- userInfo: { userId: "test", displayName: "Test User" },
83
- });
84
- logger.debug?.("Using test user info");
85
- logger.debug?.(`User: ${userInfo.userId}`);
59
+ logger.debug?.(`User: ${userId}`);
86
60
  logger.debug?.(`Device ID: ${deviceInfo?.deviceId}`);
87
61
  } else {
88
62
  logger.debug?.("Starting OAuth2 authentication...");
89
63
  userInfo = await performOAuth2AuthWithBrowser(deviceInfo);
90
64
 
91
65
  logger.debug?.("OAuth2 authentication successful");
92
- logger.debug?.(`User: ${userInfo?.userId || "Unknown"}`);
93
66
  logger.debug?.(`Device ID: ${deviceInfo?.deviceId}`);
94
67
  }
95
68
 
package/src/schemas.ts CHANGED
@@ -14,7 +14,11 @@ const UserInfoSchema = z.object({
14
14
  * 设备信息配置
15
15
  */
16
16
  const DeviceInfoSchema = z.object({
17
- deviceType: z.string().optional(),
17
+ type: z.string().optional(),
18
+ manufacture: z.string().optional(),
19
+ brand: z.string().optional(),
20
+ name: z.string().optional(),
21
+ model: z.string().optional(),
18
22
  });
19
23
 
20
24
  /**
package/src/types.ts CHANGED
@@ -22,6 +22,10 @@ export interface DeviceInfo {
22
22
  * 品牌信息
23
23
  */
24
24
  brand: string;
25
+ /**
26
+ * 制造商信息
27
+ */
28
+ manufacture: string;
25
29
  port: number;
26
30
  }
27
31
 
@@ -34,9 +38,7 @@ export type DeviceRole = "yoyoclaw" | "node";
34
38
  * 用户信息
35
39
  */
36
40
  export interface HonorUserInfo {
37
- userId?: string;
38
- token?: string;
39
- userName?: string;
41
+ token: string;
40
42
  }
41
43
 
42
44
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 环境变量工具模块
3
+ */
4
+ const DEVICE_ENV_VARS = {
5
+ brand: "YOYO_CLAW_BRAND",
6
+ manufacture: "YOYO_CLAW_MANUFACTURER",
7
+ deviceType: "YOYO_CLAW_DEVICE_TYPE",
8
+ userId: "YOYO_CLAW_USER_ID",
9
+ } as const;
10
+
11
+ /**
12
+ * 设备相关的环境变量
13
+ */
14
+ export interface DeviceEnvVars {
15
+ brand?: string;
16
+ manufacture?: string;
17
+ deviceType?: string;
18
+ userId?: string;
19
+ }
20
+
21
+ /**
22
+ * 批量获取所有设备相关的环境变量
23
+ */
24
+ export function getDeviceEnvVars(): DeviceEnvVars {
25
+ return {
26
+ brand: process.env[DEVICE_ENV_VARS.brand],
27
+ manufacture: process.env[DEVICE_ENV_VARS.manufacture],
28
+ deviceType: process.env[DEVICE_ENV_VARS.deviceType]
29
+ };
30
+ }
31
+
32
+ /**
33
+ * 获取用户ID
34
+ */
35
+ export function getEnvUserId(): string | undefined {
36
+ return process.env[DEVICE_ENV_VARS.userId];
37
+ }