@honor-claw/yoyo 0.0.1-beta.2 → 0.0.1-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +2 -2
- package/openclaw.plugin.json +7 -0
- package/package.json +20 -20
- package/skills/search/SKILL.md +182 -0
- package/skills/search/scripts/search.sh +69 -0
- package/skills/yoyo-control/SKILL.md +105 -120
- package/skills/yoyo-control/references/alarm-create.md +473 -0
- package/skills/yoyo-control/references/app-close.md +183 -0
- package/skills/yoyo-control/references/app-open.md +178 -0
- package/skills/yoyo-control/references/call-phone.md +250 -0
- package/skills/yoyo-control/references/capture-screenshot.md +205 -54
- package/skills/yoyo-control/references/contact-search.md +235 -0
- package/skills/yoyo-control/references/hotspot.md +208 -0
- package/skills/yoyo-control/references/local-search.md +224 -15
- package/skills/yoyo-control/references/message-send.md +246 -0
- package/skills/yoyo-control/references/mobile-data.md +248 -0
- package/skills/yoyo-control/references/no-disturb.md +239 -0
- package/skills/yoyo-control/references/quiet-mode.md +228 -0
- package/skills/yoyo-control/references/ringing-mode.md +223 -0
- package/skills/yoyo-control/references/screen-record.md +220 -0
- package/skills/yoyo-control/references/vibration-mode.md +235 -0
- package/skills/yoyo-control/references/volume-operate.md +274 -0
- package/skills/yoyo-control/scripts/invoke.js +33 -111
- package/src/agent/copy-templates.ts +56 -0
- package/src/agent/index.ts +3 -0
- package/src/agent/templates/AGENTS.md +223 -0
- package/src/apis/claw-cloud.ts +70 -23
- package/src/apis/honor-auth.ts +20 -10
- package/src/apis/types.ts +24 -1
- package/src/cloud-channel/channel.ts +245 -58
- package/src/cloud-channel/client.ts +87 -12
- package/src/cloud-channel/types.ts +30 -0
- package/src/commands/env/impl.ts +58 -0
- package/src/commands/env/index.ts +1 -0
- package/src/commands/index.ts +11 -1
- package/src/commands/login/impl.ts +17 -8
- package/src/commands/logout/impl.ts +23 -0
- package/src/commands/logout/index.ts +1 -53
- package/src/commands/status/index.ts +172 -42
- package/src/gateway-client/client.deprecated.ts +1 -1
- package/src/gateway-client/client.ts +15 -20
- package/src/gateway-client/types.ts +2 -2
- package/src/honor-auth/browser.ts +12 -15
- package/src/honor-auth/callback-server.ts +3 -6
- package/src/honor-auth/cloud.ts +65 -12
- package/src/honor-auth/config.ts +25 -17
- package/src/honor-auth/index.ts +1 -0
- package/src/honor-auth/token-manager.ts +24 -14
- package/src/modules/claw-configs/config-manager.ts +211 -11
- package/src/modules/claw-configs/hosts.ts +48 -0
- package/src/modules/claw-configs/index.ts +1 -0
- package/src/modules/claw-configs/types.ts +4 -0
- package/src/modules/device/device-info.ts +20 -9
- package/src/modules/device/providers/linux.ts +128 -0
- package/src/modules/device/providers/macos.ts +123 -0
- package/src/modules/device/providers/pad.ts +0 -16
- package/src/modules/device/registry.ts +12 -3
- package/src/modules/login/impl.ts +38 -16
- package/src/runtime.ts +44 -0
- package/src/schemas.ts +4 -1
- package/src/services/connection/impl.ts +89 -9
- package/src/services/connection/status-tracker/events.ts +127 -0
- package/src/services/connection/status-tracker/index.ts +31 -0
- package/src/services/connection/status-tracker/storage.ts +133 -0
- package/src/services/connection/status-tracker/tracker.ts +370 -0
- package/src/services/connection/status-tracker/types.ts +131 -0
- package/src/types.ts +0 -4
- package/src/utils/fs-safe.ts +544 -0
- package/src/utils/version.ts +29 -0
- package/src/utils/ws.ts +21 -0
- package/skills/yoyo-control/references/open-app.md +0 -54
- package/skills/yoyo-control/references/phone-call.md +0 -217
- package/skills/yoyo-control/references/schedule.md +0 -107
- package/skills/yoyo-control/references/screen-recorder.md +0 -67
- package/skills/yoyo-control/references/search-contact.md +0 -37
- package/skills/yoyo-control/references/send-message.md +0 -155
- package/skills/yoyo-control/references/volume.md +0 -536
- package/skills/yoyo-control/scripts/README.md +0 -103
- package/skills/yoyo-control/scripts/volume-up.json +0 -7
|
@@ -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
|
|
21
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
logger.debug("Using test user info");
|
|
69
|
+
logger.debug(`User: ${userInfo.userId}`);
|
|
70
|
+
logger.debug(`Device ID: ${deviceInfo?.deviceId}`);
|
|
49
71
|
} else {
|
|
50
|
-
|
|
72
|
+
logger.debug("Starting OAuth2 authentication...");
|
|
51
73
|
userInfo = await performOAuth2AuthWithBrowser(deviceInfo);
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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("📡
|
|
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
|
|
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("❌
|
|
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
|
|
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
|
|
258
|
-
return
|
|
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
|
+
}
|