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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/index.ts +2 -2
  2. package/openclaw.plugin.json +7 -0
  3. package/package.json +20 -20
  4. package/skills/search/SKILL.md +182 -0
  5. package/skills/search/scripts/search.sh +69 -0
  6. package/skills/yoyo-control/SKILL.md +105 -120
  7. package/skills/yoyo-control/references/alarm-create.md +473 -0
  8. package/skills/yoyo-control/references/app-close.md +183 -0
  9. package/skills/yoyo-control/references/app-open.md +178 -0
  10. package/skills/yoyo-control/references/call-phone.md +250 -0
  11. package/skills/yoyo-control/references/capture-screenshot.md +205 -54
  12. package/skills/yoyo-control/references/contact-search.md +235 -0
  13. package/skills/yoyo-control/references/hotspot.md +208 -0
  14. package/skills/yoyo-control/references/local-search.md +224 -15
  15. package/skills/yoyo-control/references/message-send.md +246 -0
  16. package/skills/yoyo-control/references/mobile-data.md +248 -0
  17. package/skills/yoyo-control/references/no-disturb.md +239 -0
  18. package/skills/yoyo-control/references/quiet-mode.md +228 -0
  19. package/skills/yoyo-control/references/ringing-mode.md +223 -0
  20. package/skills/yoyo-control/references/screen-record.md +220 -0
  21. package/skills/yoyo-control/references/vibration-mode.md +235 -0
  22. package/skills/yoyo-control/references/volume-operate.md +274 -0
  23. package/skills/yoyo-control/scripts/invoke.js +33 -111
  24. package/src/agent/copy-templates.ts +56 -0
  25. package/src/agent/index.ts +3 -0
  26. package/src/agent/templates/AGENTS.md +223 -0
  27. package/src/apis/claw-cloud.ts +70 -23
  28. package/src/apis/honor-auth.ts +20 -10
  29. package/src/apis/types.ts +24 -1
  30. package/src/cloud-channel/channel.ts +245 -58
  31. package/src/cloud-channel/client.ts +87 -12
  32. package/src/cloud-channel/types.ts +30 -0
  33. package/src/commands/env/impl.ts +58 -0
  34. package/src/commands/env/index.ts +1 -0
  35. package/src/commands/index.ts +11 -1
  36. package/src/commands/login/impl.ts +17 -8
  37. package/src/commands/logout/impl.ts +23 -0
  38. package/src/commands/logout/index.ts +1 -53
  39. package/src/commands/status/index.ts +172 -42
  40. package/src/gateway-client/client.deprecated.ts +1 -1
  41. package/src/gateway-client/client.ts +15 -20
  42. package/src/gateway-client/types.ts +2 -2
  43. package/src/honor-auth/browser.ts +12 -15
  44. package/src/honor-auth/callback-server.ts +3 -6
  45. package/src/honor-auth/cloud.ts +65 -12
  46. package/src/honor-auth/config.ts +25 -17
  47. package/src/honor-auth/index.ts +1 -0
  48. package/src/honor-auth/token-manager.ts +24 -14
  49. package/src/modules/claw-configs/config-manager.ts +211 -11
  50. package/src/modules/claw-configs/hosts.ts +48 -0
  51. package/src/modules/claw-configs/index.ts +1 -0
  52. package/src/modules/claw-configs/types.ts +4 -0
  53. package/src/modules/device/device-info.ts +20 -9
  54. package/src/modules/device/providers/linux.ts +128 -0
  55. package/src/modules/device/providers/macos.ts +123 -0
  56. package/src/modules/device/providers/pad.ts +0 -16
  57. package/src/modules/device/registry.ts +12 -3
  58. package/src/modules/login/impl.ts +38 -16
  59. package/src/runtime.ts +44 -0
  60. package/src/schemas.ts +4 -1
  61. package/src/services/connection/impl.ts +89 -9
  62. package/src/services/connection/status-tracker/events.ts +127 -0
  63. package/src/services/connection/status-tracker/index.ts +31 -0
  64. package/src/services/connection/status-tracker/storage.ts +133 -0
  65. package/src/services/connection/status-tracker/tracker.ts +370 -0
  66. package/src/services/connection/status-tracker/types.ts +131 -0
  67. package/src/types.ts +0 -4
  68. package/src/utils/fs-safe.ts +544 -0
  69. package/src/utils/version.ts +29 -0
  70. package/src/utils/ws.ts +21 -0
  71. package/skills/yoyo-control/references/open-app.md +0 -54
  72. package/skills/yoyo-control/references/phone-call.md +0 -217
  73. package/skills/yoyo-control/references/schedule.md +0 -107
  74. package/skills/yoyo-control/references/screen-recorder.md +0 -67
  75. package/skills/yoyo-control/references/search-contact.md +0 -37
  76. package/skills/yoyo-control/references/send-message.md +0 -155
  77. package/skills/yoyo-control/references/volume.md +0 -536
  78. package/skills/yoyo-control/scripts/README.md +0 -103
  79. package/skills/yoyo-control/scripts/volume-up.json +0 -7
@@ -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,8 +17,17 @@ export async function registerDevice(deviceInfo: DeviceInfo, userInfo: HonorUser
17
17
  const gatewayAuthConfig = configManager.getGatewayAuthConfig();
18
18
 
19
19
  // 创建 Claw Cloud 客户端
20
- const baseUrl = 'http://omni-pre-drcn.hiboard.hihonorcloud.com/aicloud/yoyo-claw-service';
21
- const client = createClawCloudClient(baseUrl);
20
+ const hosts = takeApiHost();
21
+ const baseUrl = `https://${hosts.clawCloud}/aicloud/yoyo-claw-service`;
22
+
23
+ // 如果有灰度标签,设置默认headers
24
+ const options = hosts.grayTag ? {
25
+ defaultHeaders: {
26
+ 'x-gray': hosts.grayTag
27
+ }
28
+ } : undefined;
29
+
30
+ const client = createClawCloudClient(baseUrl, options);
22
31
 
23
32
  // 调用注册接口,传入认证信息
24
33
  const response = await client.registerDevice(deviceInfo, userInfo, gatewayAuthConfig);
@@ -5,6 +5,7 @@ import { performOAuth2AuthWithBrowser } from "../../honor-auth/index.js";
5
5
  import { getDeviceInfo, registerDevice } from "../device/index.js";
6
6
  import type { HonorUserInfo } from "../../types.js";
7
7
  import { saveToken } from "../../honor-auth/token-manager.js";
8
+ import { useClawLogger } from "../../utils/logger.js";
8
9
 
9
10
  /**
10
11
  * 登录选项
@@ -12,6 +13,10 @@ import { saveToken } from "../../honor-auth/token-manager.js";
12
13
  export interface LoginOptions {
13
14
  /** 是否跳过认证流程,默认为 false */
14
15
  noAuth?: boolean;
16
+ /** 用户ID,如果提供则直接使用此ID登录 */
17
+ userId?: string;
18
+ /** Token,如果提供则直接使用此Token登录 */
19
+ token?: string;
15
20
  }
16
21
 
17
22
  /**
@@ -23,16 +28,33 @@ export interface LoginOptions {
23
28
  * @param options 登录选项
24
29
  */
25
30
  export async function performLogin(options: LoginOptions = {}) {
26
- const { noAuth = false } = options;
31
+ const { noAuth = false, userId, token } = options;
32
+ const logger = useClawLogger();
27
33
  try {
28
- console.log("🚀 开始登录流程...");
34
+ logger.debug("Starting login process...");
29
35
 
30
36
  // 步骤1: 调用 honor-auth 进行 OAuth2 认证
31
37
  const deviceInfo = getDeviceInfo();
32
-
38
+
33
39
  let userInfo: HonorUserInfo;
34
- if (noAuth) {
35
- console.log("🔓 跳过 OAuth2 认证,使用测试用户信息...");
40
+ // 检查是否提供了 userId 和 token 参数
41
+ if (userId && token) {
42
+ logger.debug("Using provided userId and token for direct login...");
43
+ userInfo = {
44
+ userId,
45
+ token,
46
+ };
47
+
48
+ // 保存Token,displayName 使用空字符串
49
+ await saveToken({
50
+ token,
51
+ userInfo: { userId, displayName: "" },
52
+ });
53
+ logger.debug("Using provided user info");
54
+ logger.debug(`User: ${userInfo.userId}`);
55
+ logger.debug(`Device ID: ${deviceInfo?.deviceId}`);
56
+ } else if (noAuth) {
57
+ logger.debug("Skipping OAuth2 auth, using test user info...");
36
58
  userInfo = {
37
59
  userId: "test",
38
60
  token: "",
@@ -43,28 +65,28 @@ export async function performLogin(options: LoginOptions = {}) {
43
65
  token: "test",
44
66
  userInfo: { userId: "test", displayName: "Test User" },
45
67
  });
46
- console.log(" 使用测试用户信息");
47
- console.log(`👤 用户: ${userInfo.userId}`);
48
- console.log(`📱 设备ID: ${deviceInfo?.deviceId}`);
68
+ logger.debug("Using test user info");
69
+ logger.debug(`User: ${userInfo.userId}`);
70
+ logger.debug(`Device ID: ${deviceInfo?.deviceId}`);
49
71
  } else {
50
- console.log("📱 正在启动 OAuth2 认证...");
72
+ logger.debug("Starting OAuth2 authentication...");
51
73
  userInfo = await performOAuth2AuthWithBrowser(deviceInfo);
52
74
 
53
- console.log("OAuth2 认证成功");
54
- console.log(`👤 用户: ${userInfo?.userId || "Unknown"}`);
55
- console.log(`📱 设备ID: ${deviceInfo?.deviceId}`);
75
+ logger.debug("OAuth2 authentication successful");
76
+ logger.debug(`User: ${userInfo?.userId || "Unknown"}`);
77
+ logger.debug(`Device ID: ${deviceInfo?.deviceId}`);
56
78
  }
57
79
 
58
80
  // 步骤2: 调用 claw-cloud 注册接口
59
- console.log("📡 正在注册设备到 Claw Cloud...");
81
+ console.log("📡 Registering device to Claw Cloud...");
60
82
  await registerDevice(deviceInfo, userInfo);
61
83
 
62
- console.log("✅ 设备注册成功");
84
+ console.log("✅ Device registered successfully");
63
85
 
64
86
  // 步骤3: 这里就不再处理建联了,等待主进程轮询到配置后进行建联
65
- console.log("🔗 Channel 建立中,可通过 openclaw honor status 查看设备状态");
87
+ console.log("🔗 Channel connection in progress, use 'openclaw honor status' to check device status");
66
88
  } catch (error) {
67
89
  const errorMessage = error instanceof Error ? error.message : String(error);
68
- console.error("❌ 登录流程失败:", errorMessage);
90
+ console.error("❌ Login process failed:", errorMessage);
69
91
  }
70
92
  }
package/src/runtime.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+ import { getConfigManager } from "./modules/claw-configs/config-manager.js";
2
3
 
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
@@ -12,3 +13,46 @@ export function getYoyoRuntime(): PluginRuntime {
12
13
  }
13
14
  return runtime;
14
15
  }
16
+
17
+ /**
18
+ * Yoyo 环境类型
19
+ */
20
+ export type YoyoEnv = "test" | "production" | "dev";
21
+
22
+ /**
23
+ * 获取当前运行环境
24
+ * 从配置文件动态读取环境信息
25
+ */
26
+ export function getYoyoEnv(): YoyoEnv {
27
+ try {
28
+ const configManager = getConfigManager();
29
+ return configManager.getEnv();
30
+ } catch (error) {
31
+ console.error(`[runtime] Failed to get env from config: ${error}`);
32
+ return "production"; // 默认返回生产环境
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 获取环境信息(包括环境和平灰度标签)
38
+ */
39
+ export interface YoyoEnvInfo {
40
+ env: YoyoEnv;
41
+ grayTag?: string;
42
+ }
43
+
44
+ /**
45
+ * 获取完整的运行环境信息
46
+ */
47
+ export function getYoyoEnvInfo(): YoyoEnvInfo {
48
+ try {
49
+ const configManager = getConfigManager();
50
+ return {
51
+ env: configManager.getEnv(),
52
+ grayTag: configManager.getGrayTag(),
53
+ };
54
+ } catch (error) {
55
+ console.error(`[runtime] Failed to get env info from config: ${error}`);
56
+ return { env: "production" };
57
+ }
58
+ }
package/src/schemas.ts CHANGED
@@ -16,5 +16,8 @@ const UserInfoSchema = z.object({
16
16
  */
17
17
  export const YoyoPluginConfigSchema = z.object({
18
18
  user: UserInfoSchema.optional(),
19
+ /** 运行环境:test 或 production */
20
+ env: z.enum(['dev', 'test', 'production']).optional(),
21
+ // 灰度标签
22
+ gray: z.string().optional(),
19
23
  });
20
-
@@ -1,10 +1,13 @@
1
- import { OpenClawPluginService } from "openclaw/plugin-sdk";
1
+ import { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk";
2
2
  import { ClawChannel } from "../../cloud-channel/channel.js";
3
3
  import { useClawLogger } from "../../utils/logger.js";
4
4
  import { ClawConnection, type ConnectionStatus } from "./types.js";
5
5
  import { loadToken, clearToken } from "../../honor-auth/token-manager.js";
6
6
  import { getDeviceInfo, registerDevice } from "../../modules/device/index.js";
7
7
  import { DeviceInfo, HonorUserInfo } from "../../types.js";
8
+ import { getConfigManager } from "../../modules/claw-configs/config-manager.js";
9
+ import { copyTemplateToWorkspace } from "../../agent/copy-templates.js";
10
+ import { StatusTracker, StatusEventType } from "./status-tracker/index.js";
8
11
 
9
12
  const clawConnection: ClawConnection = {
10
13
  channel: null,
@@ -14,20 +17,50 @@ const clawConnection: ClawConnection = {
14
17
  let pollTimer: NodeJS.Timeout | null = null;
15
18
  let pollInterval = 5000; // 初始轮询间隔 5 秒
16
19
 
20
+ // 状态跟踪器实例
21
+ let statusTracker: StatusTracker | null = null;
22
+
17
23
  /**
18
24
  * claw connection的连接管理服务入口
19
25
  *
20
26
  * 维护一个全局的channel,配合各个命令行、服务时机调用
21
27
  */
22
- export const ClawConnectionService: OpenClawPluginService = {
28
+ export const createClawConnectionService = (api: OpenClawPluginApi) => ({
23
29
  id: "yoyoclaw-connection",
24
30
  async start() {
25
31
  useClawLogger().info("[yoyoclaw] plugin service enabled");
26
32
 
33
+ // 初始化状态跟踪器
34
+ statusTracker = new StatusTracker(true);
35
+ try {
36
+ // 加载并清理状态:清除临时的gateway连接和物理设备数据
37
+ await statusTracker.loadAndCleanStatus();
38
+ useClawLogger().info("[yoyoclaw-conn] status tracker initialized and cleaned");
39
+ } catch (error) {
40
+ useClawLogger().warn(
41
+ `[yoyoclaw-conn] failed to load saved status: ${String(error)}`
42
+ );
43
+ }
44
+
27
45
  // 启动轮询机制
28
46
  startPolling();
47
+
48
+ // 复制智能体模板到工作目录
49
+ copyTemplateToWorkspace(api);
50
+ useClawLogger().debug?.(`[yoyoclaw] agents: ${JSON.stringify(api.config.agents?.defaults)}`);
51
+
52
+ // 修改配置文件,配置`plugins.allow`和`gateway.nodes.allowCommands`
53
+ try {
54
+ const configManager = getConfigManager();
55
+ await configManager.initializePluginConfig("yoyo");
56
+ useClawLogger().info("[yoyoclaw] plugin config initialized");
57
+ } catch (error) {
58
+ useClawLogger().error(
59
+ `[yoyoclaw] failed to initialize plugin config: ${String(error)}`
60
+ );
61
+ }
29
62
  },
30
- stop(_ctx) {
63
+ async stop(_ctx) {
31
64
  useClawLogger().info("[yoyoclaw-conn] stopping connection service");
32
65
 
33
66
  // 停止轮询
@@ -35,8 +68,14 @@ export const ClawConnectionService: OpenClawPluginService = {
35
68
 
36
69
  // 停止时销毁channel
37
70
  destroyConnection();
71
+
72
+ // 销毁状态跟踪器
73
+ if (statusTracker) {
74
+ await statusTracker.destroy();
75
+ statusTracker = null;
76
+ }
38
77
  },
39
- };
78
+ } as OpenClawPluginService);
40
79
 
41
80
  /**
42
81
  * 启动轮询机制
@@ -79,8 +118,7 @@ async function pollConnection(): Promise<void> {
79
118
  const hasToken = !!userInfo?.token;
80
119
 
81
120
  useClawLogger().debug?.(
82
- `[yoyoclaw-conn] polling - hasToken: ${hasToken}, hasChannel: ${!!clawConnection.channel}, status: ${
83
- clawConnection.status
121
+ `[yoyoclaw-conn] polling - hasToken: ${hasToken}, hasChannel: ${!!clawConnection.channel}, status: ${clawConnection.status
84
122
  }`
85
123
  );
86
124
 
@@ -122,6 +160,18 @@ async function createChannel(
122
160
  `[yoyoclaw-conn] creating new channel for device: ${deviceInfo.deviceId}`
123
161
  );
124
162
 
163
+ // 通知状态跟踪器:连接状态变更
164
+ if (statusTracker) {
165
+ await statusTracker.handleEvent({
166
+ type: StatusEventType.CONNECTION_STATUS_CHANGED,
167
+ timestamp: Date.now(),
168
+ data: {
169
+ status: "connecting",
170
+ previousStatus: clawConnection.status,
171
+ },
172
+ });
173
+ }
174
+
125
175
  return new Promise((resolve, reject) => {
126
176
  clawConnection.channel = new ClawChannel({
127
177
  deviceInfo,
@@ -130,6 +180,18 @@ async function createChannel(
130
180
  clawConnection.status = "connected";
131
181
  useClawLogger().info("[yoyoclaw-conn] channel connected successfully");
132
182
 
183
+ // 通知状态跟踪器:连接状态变更
184
+ if (statusTracker) {
185
+ statusTracker.handleEvent({
186
+ type: StatusEventType.CONNECTION_STATUS_CHANGED,
187
+ timestamp: Date.now(),
188
+ data: {
189
+ status: "connected",
190
+ previousStatus: "connecting",
191
+ },
192
+ });
193
+ }
194
+
133
195
  // 连接成功后,将轮询间隔延长到 10 秒
134
196
  if (pollInterval !== 10000) {
135
197
  pollInterval = 10000;
@@ -151,6 +213,18 @@ async function createChannel(
151
213
  clawConnection.channel = null;
152
214
  useClawLogger().info("[yoyoclaw-conn] channel closed");
153
215
 
216
+ // 通知状态跟踪器:连接状态变更
217
+ if (statusTracker) {
218
+ statusTracker.handleEvent({
219
+ type: StatusEventType.CONNECTION_STATUS_CHANGED,
220
+ timestamp: Date.now(),
221
+ data: {
222
+ status: "idle",
223
+ previousStatus: "connected",
224
+ },
225
+ });
226
+ }
227
+
154
228
  // 连接断开后,恢复轮询间隔为 5 秒
155
229
  if (pollInterval !== 5000) {
156
230
  pollInterval = 5000;
@@ -212,6 +286,12 @@ async function createChannel(
212
286
  destroyConnection();
213
287
  }
214
288
  },
289
+ onStatusEvent: async (event) => {
290
+ // 桥接所有状态事件到跟踪器
291
+ if (statusTracker) {
292
+ await statusTracker.handleEvent(event);
293
+ }
294
+ },
215
295
  });
216
296
 
217
297
  try {
@@ -252,8 +332,8 @@ export function destroyConnection(): void {
252
332
  }
253
333
 
254
334
  /**
255
- * 获取当前连接状态
335
+ * 获取状态跟踪器实例
256
336
  */
257
- export function getConnectionStatus(): ConnectionStatus {
258
- return clawConnection.status;
337
+ export function getStatusTracker(): StatusTracker | null {
338
+ return statusTracker;
259
339
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 状态跟踪事件定义
3
+ * 定义所有状态跟踪相关的事件类型
4
+ */
5
+
6
+ /**
7
+ * 事件类型枚举
8
+ */
9
+ export enum StatusEventType {
10
+ // 云侧Socket事件
11
+ CLOUD_SOCKET_CONNECTING = 'cloud_socket_connecting',
12
+ CLOUD_SOCKET_CONNECTED = 'cloud_socket_connected',
13
+ CLOUD_SOCKET_DISCONNECTED = 'cloud_socket_disconnected',
14
+ CLOUD_SOCKET_ERROR = 'cloud_socket_error',
15
+ CLOUD_SOCKET_RETRY = 'cloud_socket_retry',
16
+
17
+ // Gateway连接事件
18
+ GATEWAY_CLIENT_CONNECTED = 'gateway_client_connected',
19
+ GATEWAY_CLIENT_DISCONNECTED = 'gateway_client_disconnected',
20
+
21
+ // 设备配对事件
22
+ DEVICE_PAIRING = 'device_pairing',
23
+ DEVICE_UNPAIRING = 'device_unpairing',
24
+
25
+ // 整体连接状态事件
26
+ CONNECTION_STATUS_CHANGED = 'connection_status_changed',
27
+ }
28
+
29
+ /**
30
+ * 状态事件基础接口
31
+ */
32
+ export interface StatusEvent {
33
+ type: StatusEventType;
34
+ timestamp: number;
35
+ data: any;
36
+ }
37
+
38
+ /**
39
+ * 云侧Socket连接事件数据
40
+ */
41
+ export interface CloudSocketConnectingData {
42
+ url: string;
43
+ }
44
+
45
+ /**
46
+ * 云侧Socket已连接事件数据
47
+ */
48
+ export interface CloudSocketConnectedData {
49
+ url: string;
50
+ connectedAt: string;
51
+ }
52
+
53
+ /**
54
+ * 云侧Socket断连事件数据
55
+ */
56
+ export interface CloudSocketDisconnectedData {
57
+ reason: string;
58
+ code?: number;
59
+ disconnectedAt: string;
60
+ }
61
+
62
+ /**
63
+ * 云侧Socket错误事件数据
64
+ */
65
+ export interface CloudSocketErrorData {
66
+ error: string;
67
+ timestamp: string;
68
+ }
69
+
70
+ /**
71
+ * 云侧Socket重试事件数据
72
+ */
73
+ export interface CloudSocketRetryData {
74
+ retryCount: number;
75
+ maxRetries: number;
76
+ delay: number;
77
+ lastError?: string;
78
+ }
79
+
80
+ /**
81
+ * Gateway客户端连接事件数据
82
+ */
83
+ export interface GatewayClientConnectedData {
84
+ sessionId: string;
85
+ hardwareDeviceId: string;
86
+ deviceInfo: any;
87
+ connectedAt: string;
88
+ }
89
+
90
+ /**
91
+ * Gateway客户端断连事件数据
92
+ */
93
+ export interface GatewayClientDisconnectedData {
94
+ sessionId: string;
95
+ hardwareDeviceId: string;
96
+ reason: string;
97
+ disconnectedAt: string;
98
+ }
99
+
100
+ /**
101
+ * 设备配对事件数据
102
+ */
103
+ export interface DevicePairingData {
104
+ sessionId: string;
105
+ hardwareDeviceId: string;
106
+ deviceInfo: any;
107
+ pairedAt: string;
108
+ }
109
+
110
+ /**
111
+ * 设备取消配对事件数据
112
+ */
113
+ export interface DeviceUnpairingData {
114
+ sessionId: string;
115
+ hardwareDeviceId: string;
116
+ reason: string;
117
+ unpairedAt: string;
118
+ }
119
+
120
+ /**
121
+ * 连接状态变更事件数据
122
+ */
123
+ export interface ConnectionStatusChangedData {
124
+ status: 'idle' | 'connecting' | 'connected';
125
+ previousStatus?: 'idle' | 'connecting' | 'connected';
126
+ reason?: string;
127
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 状态跟踪模块入口
3
+ */
4
+
5
+ export { StatusTracker } from './tracker.js';
6
+ export { StatusStorage } from './storage.js';
7
+ export {
8
+ StatusEventType,
9
+ type StatusEvent,
10
+ type CloudSocketConnectingData,
11
+ type CloudSocketConnectedData,
12
+ type CloudSocketDisconnectedData,
13
+ type CloudSocketErrorData,
14
+ type CloudSocketRetryData,
15
+ type GatewayClientConnectedData,
16
+ type GatewayClientDisconnectedData,
17
+ type DevicePairingData,
18
+ type DeviceUnpairingData,
19
+ type ConnectionStatusChangedData,
20
+ } from './events.js';
21
+ export {
22
+ type ConnectionStatusData,
23
+ type ConnectionStatus,
24
+ type GatewayConnectionInfo,
25
+ type PhysicalDeviceInfo,
26
+ type CloudSocketStatus,
27
+ type GatewayStatus,
28
+ type DevicesStatus,
29
+ type ConnectionHistory,
30
+ createEmptyStatusData,
31
+ } from './types.js';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 状态文件存储
3
+ * 负责将状态数据持久化到文件系统
4
+ */
5
+
6
+ import { promises as fs, existsSync } from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { fileURLToPath } from 'url';
10
+ import type { ConnectionStatusData } from './types.js';
11
+ import { createEmptyStatusData } from './types.js';
12
+
13
+ /**
14
+ * 获取项目根目录
15
+ * 通过当前模块的路径向上查找,直到找到 package.json
16
+ */
17
+ function getProjectRoot(): string {
18
+ const currentFile = fileURLToPath(import.meta.url);
19
+ let currentDir = path.dirname(currentFile);
20
+
21
+ // 向上查找,直到找到 package.json
22
+ while (currentDir !== path.dirname(currentDir)) {
23
+ const packageJsonPath = path.join(currentDir, 'package.json');
24
+ if (existsSync(packageJsonPath)) {
25
+ return currentDir;
26
+ }
27
+ currentDir = path.dirname(currentDir);
28
+ }
29
+
30
+ // 如果找不到,使用当前目录
31
+ return currentDir;
32
+ }
33
+
34
+ /**
35
+ * 状态存储类
36
+ */
37
+ export class StatusStorage {
38
+ private statusFilePath: string;
39
+ private statusDir: string;
40
+
41
+ constructor() {
42
+ // 状态文件放在项目根目录下的 .local-tmps 文件夹
43
+ const projectRoot = getProjectRoot();
44
+ this.statusDir = path.join(projectRoot, '.local-tmps');
45
+ this.statusFilePath = path.join(this.statusDir, 'yoyo-status.json');
46
+ }
47
+
48
+ /**
49
+ * 保存状态到文件
50
+ */
51
+ async save(status: ConnectionStatusData): Promise<void> {
52
+ try {
53
+ // 确保目录存在
54
+ await fs.mkdir(this.statusDir, { recursive: true });
55
+
56
+ // 写入状态文件
57
+ const content = JSON.stringify(status, null, 2);
58
+ await fs.writeFile(this.statusFilePath, content, 'utf-8');
59
+ } catch (error) {
60
+ console.error(
61
+ `[StatusStorage] Failed to save status: ${
62
+ error instanceof Error ? error.message : String(error)
63
+ }`
64
+ );
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 从文件加载状态
71
+ */
72
+ async load(): Promise<ConnectionStatusData | null> {
73
+ try {
74
+ // 检查文件是否存在
75
+ await fs.access(this.statusFilePath);
76
+
77
+ // 读取文件内容
78
+ const content = await fs.readFile(this.statusFilePath, 'utf-8');
79
+ const status = JSON.parse(content) as ConnectionStatusData;
80
+
81
+ return status;
82
+ } catch (error) {
83
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
84
+ // 文件不存在,返回null
85
+ return null;
86
+ }
87
+
88
+ console.error(
89
+ `[StatusStorage] Failed to load status: ${
90
+ error instanceof Error ? error.message : String(error)
91
+ }`
92
+ );
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 清除状态文件
99
+ */
100
+ async clear(): Promise<void> {
101
+ try {
102
+ await fs.unlink(this.statusFilePath);
103
+ } catch (error) {
104
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
105
+ console.error(
106
+ `[StatusStorage] Failed to clear status: ${
107
+ error instanceof Error ? error.message : String(error)
108
+ }`
109
+ );
110
+ throw error;
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 获取状态文件路径
117
+ */
118
+ getFilePath(): string {
119
+ return this.statusFilePath;
120
+ }
121
+
122
+ /**
123
+ * 检查状态文件是否存在
124
+ */
125
+ async exists(): Promise<boolean> {
126
+ try {
127
+ await fs.access(this.statusFilePath);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ }