@honor-claw/yoyo 0.0.1-beta.3 → 0.0.1-beta.5

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 CHANGED
@@ -4,16 +4,20 @@ import { registerCommands } from "./src/commands/index.js";
4
4
  import { YoyoPluginConfigSchema } from "./src/schemas.js";
5
5
  import { ClawConnectionService } from "./src/services/connection/index.js";
6
6
  import { setClawLogger } from "./src/utils/logger.js";
7
+ import { copyTemplateToWorkspace } from "./src/agent/index.js";
7
8
 
8
9
  const plugin = {
9
10
  id: "yoyo",
10
11
  name: "YOYOClaw",
11
12
  description: "OpenClaw Honor Yoyo connection plugin",
12
13
  configSchema: YoyoPluginConfigSchema,
13
- register(api: OpenClawPluginApi) {
14
+ async register(api: OpenClawPluginApi) {
14
15
  setYoyoRuntime(api.runtime);
15
16
  setClawLogger(api.logger);
16
17
 
18
+ // 复制智能体模板到工作目录
19
+ await copyTemplateToWorkspace(api)
20
+
17
21
  // 利用服务来管理核心连接任务进行~
18
22
  api.registerService(ClawConnectionService);
19
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honor-claw/yoyo",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.5",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Honor Yoyo connection plugin",
6
6
  "scripts": {
@@ -5,7 +5,7 @@ description: Whenever a task requires **operating or controlling the phone**, eg
5
5
  metadata: { "openclaw": { "emoji": "📱", "always": true } }
6
6
  ---
7
7
 
8
- # Control phone with YOYO
8
+ # YOYO Control
9
9
 
10
10
  Follow the workflow to control phone or pad with YOYO, Don't guess the commands.
11
11
 
@@ -13,9 +13,9 @@ Follow the workflow to control phone or pad with YOYO, Don't guess the commands.
13
13
 
14
14
  Follow this **4-step workflow** when using the skill:
15
15
 
16
- 1. Query the device status to identify available device nodes.
17
- 2. Determine the target node, required tool, and execution parameters based on the user's intent.
18
- 3. Call the node tool using the correct parameter format.
16
+ 1. Query the nodes status to identify available device, and determine the target device.
17
+ 2. Plan the tool to use, and extract command & parameters according to the tool reference.
18
+ 3. Invoke node with correct nodeId, command and parameters.
19
19
  4. Present the result and confirm whether the operation was successful.
20
20
 
21
21
  ### Step 1. Discover Nodes
@@ -26,9 +26,11 @@ Follow this **4-step workflow** when using the skill:
26
26
 
27
27
  **Execution result**:
28
28
 
29
+ ```markdown
29
30
  | Node | ID | IP | Detail | Status | Caps |
30
31
  | ---------- | -------- | -------- | -------- | -------- | ------ |
31
32
  | <nodeName> | <nodeId> | <nodeIP> | <detail> | <status> | <caps> |
33
+ ```
32
34
 
33
35
  #### Capability Matching Logic
34
36
 
@@ -74,17 +76,19 @@ Before executing any device control operation, you **MUST** consult the correspo
74
76
 
75
77
  ### Available Tool References
76
78
 
77
- | Reference File | Capability | Description | Required Node Caps |
78
- | ---------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
79
- | `references/volume.md` | Volume Control | Adjust, set, increase, decrease, mute/unmute device volume; query current volume status; fine-grained control by stream type (media, call, ringtone, notification) | `volume.operate` |
80
- | `references/phone-call.md` | Phone Call | Initiate and manage phone calls; unified control for call operations | `phone.call` |
81
- | `references/screen-recorder.md` | Screen Recording | Start/stop screen recording; configure recording parameters; query recording status; app-specific recording control | `screen.record` |
82
- | `references/capture-screenshot.md` | Screenshot | Capture current screen content as image; support standard and scrolling screenshot modes | `capture.shot` |
83
- | `references/schedule.md` | Schedule | Provides the ability to create schedules | `schedule.create` |
84
- | `references/search-contact.md` | Contact | Provides the ability to search contacts | `search.contact ` |
85
- | `references/open-app.md` | Open Application | Used to help users open a specified app without any specific app internals. | `app.open` |
86
- | `references/send-message.md` | Send Message | Provide SMS sending services, users can send SMS content to specified numbers by providing phone numbers or contact information. | `send.message` |
87
- | `references/local-search.md` | Local Search | Search native data for documents, notes, calendars, galleries, yoyo memories, wallets, and more for direct answers to user questions or generate at-a-glance content | `message.send.message` |
79
+ | Reference File | Tool Name | Description | Required Node Caps |
80
+ | ---------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
81
+ | `references/volume.md` | volume | Adjust, set, increase, decrease, mute/unmute device volume; query current volume status; fine-grained control by stream type (media, call, ringtone, notification) | `volume.operate` |
82
+ | `references/phone-call.md` | phone_call | Initiate and manage phone calls; unified control for call operations | `phone.call` |
83
+ | `references/screen-recorder.md` | screen_recorder | Start/stop screen recording; configure recording parameters; query recording status; app-specific recording control | `screen.record` |
84
+ | `references/capture-screenshot.md` | capture_screenshot | Capture current screen content as image; support standard and scrolling screenshot modes | `capture.shot` |
85
+ | `references/schedule.md` | Schedschedule_createule | Provides the ability to create schedules | `schedule.create` |
86
+ | `references/search-contact.md` | search_contact | Provides the ability to search contacts | `search.contact ` |
87
+ | `references/open-app.md` | open_app | Used to help users open a specified app without any specific app internals. | `app.open` |
88
+ | `references/send-message.md` | send_message | Provide SMS sending services, users can send SMS content to specified numbers by providing phone numbers or contact information. | `send.message` |
89
+ | `references/local-search.md` | local_search | Search native data for documents, notes, calendars, galleries, yoyo memories, wallets, and more for direct answers to user questions or generate at-a-glance content | `loacal.search` |
90
+ | `references/hot-spot.md` | hot_spot | 手机热点控制能力 | `hot.spot` |
91
+ | `references/no-disturb.md` | hot_spot | 手机勿扰模式控制力 | `no.disturb` |
88
92
 
89
93
  ### Parameter Construction Workflow
90
94
 
@@ -181,17 +185,32 @@ Execute the following steps **in strict order** for each operation:
181
185
 
182
186
  ### Parameter Format Requirements
183
187
 
184
- | Platform | Format | Example |
185
- | ---------------------------- | --------------------------------------- | ----------------------- |
186
- | **Bash (Linux/macOS)** | Single quotes wrapping JSON | `'{"level":50}'` |
187
- | **Windows (PowerShell/CMD)** | Double quotes with escaped inner quotes | `"{\"level\":50}"` |
188
- | **Complex Objects** | Create JSON file first | `--params @params.json` |
188
+ **Platform-specific JSON parameter format:**
189
+
190
+ | Platform | Shell | Quote Style | Example |
191
+ | ----------- | ----- | ------------------------------- | ------------------------------- |
192
+ | **Windows** | CMD | Double quotes, escape inner `"` | `"{\"actionType\":\"打电话\"}"` |
193
+ | **Linux** | Bash | Single quotes | `'{"actionType":"打电话"}'` |
194
+ | **macOS** | Bash | Single quotes | `'{"actionType":"打电话"}'` |
195
+
196
+ **Windows CMD Execution:**
197
+
198
+ ```cmd
199
+ openclaw nodes invoke <nodeId> --command phone.call --params "{\"actionType\":\"打电话\",\"phoneNumber\":\"10086\"}"
200
+ ```
201
+
202
+ **Linux/macOS Bash Execution:**
203
+
204
+ ```bash
205
+ openclaw nodes invoke <nodeId> --command phone.call --params '{"actionType":"打电话","phoneNumber":"10086"}'
206
+ ```
189
207
 
190
208
  **Additional Requirements**:
191
209
 
192
210
  - Parameters must be valid JSON
193
211
  - Enum values must match documentation exactly (case-sensitive)
194
212
  - Numeric values must be within defined ranges
213
+ - On Windows, always use CMD (not PowerShell) to avoid quote parsing issues
195
214
 
196
215
  ### Examples
197
216
 
@@ -334,13 +353,3 @@ Would you like to check other nodes or retry later?"
334
353
  - ❌ **DO NOT** expose raw error codes to users
335
354
  - ❌ **DO NOT** assume user knows technical details
336
355
  - ❌ **DO NOT** leave user without next steps on failure
337
-
338
- ## 快速参考命令
339
-
340
- ```bash
341
- # 获取所有节点状态
342
- openclaw nodes status
343
-
344
- # 调用工具
345
- openclaw nodes invoke --node <id|name|ip> --command <command> --params <json>
346
- ```
@@ -0,0 +1,19 @@
1
+ # hot_spot 手机热点控制工具使用说明
2
+
3
+ ## Tool Command
4
+
5
+ ```bash
6
+ hot.spot
7
+ ```
8
+
9
+ ## Tool Parameters and Examples
10
+
11
+ ```json
12
+ {
13
+ "name": "hot_spot",
14
+ "description": "该工具提供对设备热点控制",
15
+ "parameters": {},
16
+ "required": [],
17
+ "examples": []
18
+ }
19
+ ```
@@ -0,0 +1,19 @@
1
+ # no_disturb 勿扰模式工具使用说明
2
+
3
+ ## Tool Command
4
+
5
+ ```bash
6
+ no.disturb
7
+ ```
8
+
9
+ ## Tool Parameters and Examples
10
+
11
+ ```json
12
+ {
13
+ "name": "no_disturb",
14
+ "description": "手机勿扰模式",
15
+ "parameters": {},
16
+ "required": [],
17
+ "examples": []
18
+ }
19
+ ```
@@ -65,7 +65,7 @@ phone.call
65
65
  ```json
66
66
  {
67
67
  "name": "phone_call",
68
- "description": "提供打电话能力,支持指定姓名、电话号码电话;支持选择打电话的卡;支持重拨、回拨已接听来电、回拨未接听来电、回拨电话",
68
+ "description": "提供打电话的功能,支持指定姓名、电话号码;支持选择打电话的卡;支持重拨、回拨已接听来电、回拨未接听来电、回拨电话",
69
69
  "parameters": {
70
70
  "actionType": {
71
71
  "type": "string",
@@ -91,125 +91,38 @@ phone.call
91
91
  },
92
92
  "contact": {
93
93
  "type": "string",
94
- "description": "泛化的"联系人"概念,可为:自然人名或昵称(如:张三、王总)、非自然实体(机构、部门、服务名称,如:招生办、物业客服)"
94
+ "description": "泛化的“联系人”概念,可为:自然人名或昵称(如:张三、王总)、非自然实体(机构、部门、服务名称,如:招生办、物业客服)"
95
95
  }
96
96
  },
97
97
  "examples": [
98
98
  {
99
- "scenario": "User provides phoneNumber directly",
100
- "query": "打电话给15651621029",
101
- "workflow": "Direct call (no contact search needed)",
99
+ "query": "拨打电话",
102
100
  "arguments": {
103
- "actionType": "打电话",
104
- "phoneNumber": "15651621029"
105
- }
106
- },
107
- {
108
- "scenario": "User provides contact name only",
109
- "query": "打电话给妈妈",
110
- "workflow": "search_contact → phone.call",
111
- "step1": {
112
- "tool": "search_contact",
113
- "arguments": {
114
- "contact": "妈妈"
115
- },
116
- "result": {
117
- "name": "妈妈",
118
- "phoneNumber": "13900139000"
119
- }
120
- },
121
- "step2": {
122
- "tool": "phone.call",
123
- "arguments": {
124
- "actionType": "打电话",
125
- "phoneNumber": "13900139000"
126
- }
101
+ "actionType": "打电话"
127
102
  }
128
103
  },
129
104
  {
130
- "scenario": "User provides contact with phoneCard",
131
- "query": "用移动卡打电话给张三",
132
- "workflow": "search_contact → phone.call with phoneCard",
133
- "step1": {
134
- "tool": "search_contact",
135
- "arguments": {
136
- "contact": "张三"
137
- },
138
- "result": {
139
- "name": "张三",
140
- "phoneNumber": "15651621029"
141
- }
142
- },
143
- "step2": {
144
- "tool": "phone.call",
145
- "arguments": {
146
- "actionType": "打电话",
147
- "phoneNumber": "15651621029",
148
- "phoneCard": "移动"
149
- }
150
- }
151
- },
152
- {
153
- "scenario": "User requests redial",
154
- "query": "重拨",
155
- "workflow": "Direct redial (no contact search needed)",
105
+ "query": "打电话给15651621029",
156
106
  "arguments": {
157
- "actionType": "重拨"
107
+ "actionType": "打电话",
108
+ "phoneNumber": "15651621029"
158
109
  }
159
110
  },
160
111
  {
161
- "scenario": "User requests callback missed call",
162
- "query": "回拨未接来电",
163
- "workflow": "Direct callback (no contact search needed)",
112
+ "query": "打电话给张三",
164
113
  "arguments": {
165
- "actionType": "回拨",
166
- "recallType": "回拨未接听来电"
167
- }
168
- },
169
- {
170
- "scenario": "User provides contact with multiple matches",
171
- "query": "打电话给李四",
172
- "workflow": "search_contact → user selects → phone.call",
173
- "step1": {
174
- "tool": "search_contact",
175
- "arguments": {
176
- "contact": "李四"
177
- },
178
- "result": {
179
- "matches": [
180
- { "name": "李四(公司)", "phoneNumber": "13800138001" },
181
- { "name": "李四(家里)", "phoneNumber": "13800138002" }
182
- ]
183
- }
184
- },
185
- "step2": {
186
- "action": "Ask user to select",
187
- "prompt": "找到多个李四,请选择:1. 李四(公司) 2. 李四(家里)"
188
- },
189
- "step3": {
190
- "tool": "phone.call",
191
- "arguments": {
192
- "actionType": "打电话",
193
- "phoneNumber": "13800138001"
194
- }
114
+ "actionType": "打电话",
115
+ "contact": "张三",
116
+ "phoneNumber": "15651621029"
195
117
  }
196
118
  },
197
119
  {
198
- "scenario": "User provides contact with no match",
199
- "query": "打电话给王五",
200
- "workflow": "search_contact → no match → ask for phoneNumber",
201
- "step1": {
202
- "tool": "search_contact",
203
- "arguments": {
204
- "contact": "王五"
205
- },
206
- "result": {
207
- "matches": []
208
- }
209
- },
210
- "step2": {
211
- "action": "Ask user for phoneNumber",
212
- "prompt": "未找到联系人王五,请提供电话号码"
120
+ "query": "用移动手机卡打电话给于洋",
121
+ "arguments": {
122
+ "actionType": "打电话",
123
+ "contact": "张三",
124
+ "phoneNumber": "15651621029",
125
+ "phoneCard": "移动手机卡"
213
126
  }
214
127
  }
215
128
  ]
@@ -1,6 +1,6 @@
1
1
  # volumn 音量控制工具使用说明
2
2
 
3
- ## 工具指令 command 定义
3
+ ## Tool Command
4
4
 
5
5
  ```bash
6
6
  volume.operate
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "path";
3
+ import { OpenClawPluginApi } from "openclaw/plugin-sdk";
4
+ import { safeWriteFile, SafeFsError, type WriteMode } from "../utils/fs-safe.js";
5
+
6
+ type TemplateEntry = {
7
+ file: string;
8
+ mode: WriteMode;
9
+ };
10
+
11
+ const TEMPLATE_FILE_LIST: TemplateEntry[] = [
12
+ { file: "AGENTS.md", mode: "overwrite" },
13
+ ];
14
+
15
+ function resolveWorkspaceDir(api: OpenClawPluginApi): string {
16
+ const configuredWorkspace = api.config.agents?.defaults?.workspace?.trim();
17
+ if (configuredWorkspace) {
18
+ return api.resolvePath(configuredWorkspace);
19
+ }
20
+ return api.resolvePath("~/.openclaw/workspace");
21
+ }
22
+
23
+ export async function copyTemplateToWorkspace(api: OpenClawPluginApi): Promise<void> {
24
+ const pluginDir = path.dirname(api.source);
25
+ const templateDir = path.join(pluginDir, "src", "agent", "templates");
26
+ const workspaceDir = resolveWorkspaceDir(api);
27
+
28
+ for (const entry of TEMPLATE_FILE_LIST) {
29
+ const sourcePath = path.join(templateDir, entry.file);
30
+
31
+ try {
32
+ const content = await fs.readFile(sourcePath, "utf-8");
33
+
34
+ await safeWriteFile({
35
+ rootDir: workspaceDir,
36
+ relativePath: entry.file,
37
+ data: content,
38
+ mode: entry.mode,
39
+ mkdir: true,
40
+ });
41
+ api.logger.info(`Successfully wrote ${entry.file} to workspace (${entry.mode} mode)`);
42
+ } catch (error) {
43
+ if (error instanceof SafeFsError) {
44
+ if (error.code === "not-found") {
45
+ api.logger.warn(`Source file does not exist: ${sourcePath}`);
46
+ continue;
47
+ }
48
+ api.logger.error(`Failed to write ${entry.file}: ${error.message}`);
49
+ } else {
50
+ api.logger.error(
51
+ `Failed to write ${entry.file}: ${error instanceof Error ? error.message : String(error)}`
52
+ );
53
+ }
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,3 @@
1
+ import { copyTemplateToWorkspace } from "./copy-templates.js";
2
+
3
+ export { copyTemplateToWorkspace };
@@ -1,7 +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
+ // import { registerLogoutCommand } from "./logout/index.js";
5
5
 
6
6
  export function registerCommands(api: OpenClawPluginApi) {
7
7
  api.registerCli(
@@ -10,7 +10,9 @@ export function registerCommands(api: OpenClawPluginApi) {
10
10
  .command("honor")
11
11
  .description("Commands for honor yoyoclaw");
12
12
 
13
+ // @ts-ignore
13
14
  registerLoginCommand(api, rootCommand);
15
+ // @ts-ignore
14
16
  registerStatusCommand(api, rootCommand);
15
17
  // registerLogoutCommand(api, rootCommand);
16
18
  },
@@ -21,8 +21,7 @@ export class ConfigManager {
21
21
  return runtime.config.loadConfig();
22
22
  } catch (error) {
23
23
  throw new Error(
24
- `Failed to load config: ${
25
- error instanceof Error ? error.message : String(error)
24
+ `Failed to load config: ${error instanceof Error ? error.message : String(error)
26
25
  }`
27
26
  );
28
27
  }
@@ -37,8 +36,7 @@ export class ConfigManager {
37
36
  await runtime.config.writeConfigFile(config);
38
37
  } catch (error) {
39
38
  throw new Error(
40
- `Failed to save config: ${
41
- error instanceof Error ? error.message : String(error)
39
+ `Failed to save config: ${error instanceof Error ? error.message : String(error)
42
40
  }`
43
41
  );
44
42
  }
@@ -138,8 +136,7 @@ export class ConfigManager {
138
136
  await this.saveConfig(updatedConfig);
139
137
  } catch (error) {
140
138
  throw new Error(
141
- `Failed to update user config: ${
142
- error instanceof Error ? error.message : String(error)
139
+ `Failed to update user config: ${error instanceof Error ? error.message : String(error)
143
140
  }`
144
141
  );
145
142
  }
@@ -171,8 +168,7 @@ export class ConfigManager {
171
168
  await this.saveConfig(updatedConfig);
172
169
  } catch (error) {
173
170
  throw new Error(
174
- `Failed to clear user config: ${
175
- error instanceof Error ? error.message : String(error)
171
+ `Failed to clear user config: ${error instanceof Error ? error.message : String(error)
176
172
  }`
177
173
  );
178
174
  }
@@ -212,6 +208,14 @@ export class ConfigManager {
212
208
  "schedule.update",
213
209
  "schedule.delete",
214
210
  "schedule.search",
211
+ "mobile.data",
212
+ "hot.spot",
213
+ "no.disturb",
214
+ "quiet.mode",
215
+ "sound.vibration",
216
+ "ringing.mode",
217
+ "vibration.mode",
218
+ "close.app"
215
219
  ];
216
220
 
217
221
  const currentAllowCommands =
@@ -239,8 +243,7 @@ export class ConfigManager {
239
243
  await this.saveConfig(updatedConfig);
240
244
  } catch (error) {
241
245
  throw new Error(
242
- `failed to initialize plugin config: ${
243
- error instanceof Error ? error.message : String(error)
246
+ `failed to initialize plugin config: ${error instanceof Error ? error.message : String(error)
244
247
  }`
245
248
  );
246
249
  }
@@ -0,0 +1,544 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Stats } from "node:fs";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import type { FileHandle } from "node:fs/promises";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ export type SafeFsErrorCode =
10
+ | "invalid-path"
11
+ | "not-found"
12
+ | "outside-root"
13
+ | "symlink"
14
+ | "not-file"
15
+ | "path-mismatch"
16
+ | "too-large"
17
+ | "write-failed";
18
+
19
+ export class SafeFsError extends Error {
20
+ code: SafeFsErrorCode;
21
+
22
+ constructor(code: SafeFsErrorCode, message: string, options?: ErrorOptions) {
23
+ super(message, options);
24
+ this.code = code;
25
+ this.name = "SafeFsError";
26
+ }
27
+ }
28
+
29
+ export type WriteMode = "overwrite" | "append";
30
+
31
+ export type SafeWriteOptions = {
32
+ rootDir: string;
33
+ relativePath: string;
34
+ data: string | Buffer;
35
+ encoding?: BufferEncoding;
36
+ mode?: WriteMode;
37
+ mkdir?: boolean;
38
+ maxBytes?: number;
39
+ };
40
+
41
+ export type SafeReadResult = {
42
+ buffer: Buffer;
43
+ realPath: string;
44
+ stat: Stats;
45
+ };
46
+
47
+ export type SafeWriteResult = {
48
+ realPath: string;
49
+ bytesWritten: number;
50
+ created: boolean;
51
+ };
52
+
53
+ const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
54
+ const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
55
+
56
+ function isNodeError(value: unknown): value is NodeJS.ErrnoException {
57
+ return Boolean(
58
+ value && typeof value === "object" && "code" in (value as Record<string, unknown>)
59
+ );
60
+ }
61
+
62
+ function isNotFoundPathError(value: unknown): boolean {
63
+ return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
64
+ }
65
+
66
+ function isSymlinkOpenError(value: unknown): boolean {
67
+ return isNodeError(value) && typeof value.code === "string" && SYMLINK_OPEN_CODES.has(value.code);
68
+ }
69
+
70
+ function normalizeWindowsPathForComparison(input: string): string {
71
+ let normalized = path.win32.normalize(input);
72
+ if (normalized.startsWith("\\\\?\\")) {
73
+ normalized = normalized.slice(4);
74
+ if (normalized.toUpperCase().startsWith("UNC\\")) {
75
+ normalized = `\\\\${normalized.slice(4)}`;
76
+ }
77
+ }
78
+ return normalized.replaceAll("/", "\\").toLowerCase();
79
+ }
80
+
81
+ function isPathInside(root: string, target: string): boolean {
82
+ const resolvedRoot = path.resolve(root);
83
+ const resolvedTarget = path.resolve(target);
84
+
85
+ if (process.platform === "win32") {
86
+ const rootForCompare = normalizeWindowsPathForComparison(resolvedRoot);
87
+ const targetForCompare = normalizeWindowsPathForComparison(resolvedTarget);
88
+ const relative = path.win32.relative(rootForCompare, targetForCompare);
89
+ return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
90
+ }
91
+
92
+ const relative = path.relative(resolvedRoot, resolvedTarget);
93
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
94
+ }
95
+
96
+ type FileIdentityStat = {
97
+ dev: number | bigint;
98
+ ino: number | bigint;
99
+ };
100
+
101
+ function isZero(value: number | bigint): boolean {
102
+ return value === 0 || value === 0n;
103
+ }
104
+
105
+ function sameFileIdentity(
106
+ left: FileIdentityStat,
107
+ right: FileIdentityStat,
108
+ platform: NodeJS.Platform = process.platform
109
+ ): boolean {
110
+ if (left.ino !== right.ino) {
111
+ return false;
112
+ }
113
+ if (left.dev === right.dev) {
114
+ return true;
115
+ }
116
+ return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
117
+ }
118
+
119
+ const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
120
+ const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
121
+ const OPEN_WRITE_CREATE_FLAGS =
122
+ fsConstants.O_WRONLY |
123
+ fsConstants.O_CREAT |
124
+ fsConstants.O_EXCL |
125
+ (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
126
+ const OPEN_APPEND_FLAGS =
127
+ fsConstants.O_WRONLY | fsConstants.O_APPEND | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
128
+
129
+ const ensureTrailingSep = (value: string) => (value.endsWith(path.sep) ? value : value + path.sep);
130
+
131
+ function expandHomePrefix(input: string, home?: string): string {
132
+ if (!input.startsWith("~")) {
133
+ return input;
134
+ }
135
+ const resolvedHome = home ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
136
+ if (!resolvedHome) {
137
+ return input;
138
+ }
139
+ return input.replace(/^~(?=$|[\\/])/, resolvedHome);
140
+ }
141
+
142
+ async function resolvePathWithinRoot(params: {
143
+ rootDir: string;
144
+ relativePath: string;
145
+ }): Promise<{ rootReal: string; rootWithSep: string; resolved: string }> {
146
+ let rootReal: string;
147
+ try {
148
+ rootReal = await fs.realpath(params.rootDir);
149
+ } catch (err) {
150
+ if (isNotFoundPathError(err)) {
151
+ throw new SafeFsError("not-found", "root dir not found");
152
+ }
153
+ throw err;
154
+ }
155
+ const rootWithSep = ensureTrailingSep(rootReal);
156
+ const expanded = expandHomePrefix(params.relativePath);
157
+ const resolved = path.resolve(rootWithSep, expanded);
158
+ if (!isPathInside(rootWithSep, resolved)) {
159
+ throw new SafeFsError("outside-root", "file is outside workspace root");
160
+ }
161
+ return { rootReal, rootWithSep, resolved };
162
+ }
163
+
164
+ async function openVerifiedLocalFile(
165
+ filePath: string,
166
+ options?: { rejectHardlinks?: boolean }
167
+ ): Promise<{ handle: FileHandle; realPath: string; stat: Stats }> {
168
+ try {
169
+ const preStat = await fs.lstat(filePath);
170
+ if (preStat.isDirectory()) {
171
+ throw new SafeFsError("not-file", "not a file");
172
+ }
173
+ } catch (err) {
174
+ if (err instanceof SafeFsError) {
175
+ throw err;
176
+ }
177
+ }
178
+
179
+ let handle: FileHandle;
180
+ try {
181
+ handle = await fs.open(filePath, OPEN_READ_FLAGS);
182
+ } catch (err) {
183
+ if (isNotFoundPathError(err)) {
184
+ throw new SafeFsError("not-found", "file not found");
185
+ }
186
+ if (isSymlinkOpenError(err)) {
187
+ throw new SafeFsError("symlink", "symlink open blocked", { cause: err });
188
+ }
189
+ if (isNodeError(err) && err.code === "EISDIR") {
190
+ throw new SafeFsError("not-file", "not a file");
191
+ }
192
+ throw err;
193
+ }
194
+
195
+ try {
196
+ const [stat, lstat] = await Promise.all([handle.stat(), fs.lstat(filePath)]);
197
+ if (lstat.isSymbolicLink()) {
198
+ throw new SafeFsError("symlink", "symlink not allowed");
199
+ }
200
+ if (!stat.isFile()) {
201
+ throw new SafeFsError("not-file", "not a file");
202
+ }
203
+ if (options?.rejectHardlinks && stat.nlink > 1) {
204
+ throw new SafeFsError("invalid-path", "hardlinked path not allowed");
205
+ }
206
+ if (!sameFileIdentity(stat, lstat)) {
207
+ throw new SafeFsError("path-mismatch", "path changed during read");
208
+ }
209
+
210
+ const realPath = await fs.realpath(filePath);
211
+ const realStat = await fs.stat(realPath);
212
+ if (options?.rejectHardlinks && realStat.nlink > 1) {
213
+ throw new SafeFsError("invalid-path", "hardlinked path not allowed");
214
+ }
215
+ if (!sameFileIdentity(stat, realStat)) {
216
+ throw new SafeFsError("path-mismatch", "path mismatch");
217
+ }
218
+
219
+ return { handle, realPath, stat };
220
+ } catch (err) {
221
+ await handle.close().catch(() => { });
222
+ if (err instanceof SafeFsError) {
223
+ throw err;
224
+ }
225
+ if (isNotFoundPathError(err)) {
226
+ throw new SafeFsError("not-found", "file not found");
227
+ }
228
+ throw err;
229
+ }
230
+ }
231
+
232
+ export async function safeReadFile(params: {
233
+ rootDir: string;
234
+ relativePath: string;
235
+ maxBytes?: number;
236
+ }): Promise<SafeReadResult> {
237
+ const { rootWithSep, resolved } = await resolvePathWithinRoot(params);
238
+
239
+ const opened = await openVerifiedLocalFile(resolved);
240
+
241
+ if (!isPathInside(rootWithSep, opened.realPath)) {
242
+ await opened.handle.close().catch(() => { });
243
+ throw new SafeFsError("outside-root", "file is outside workspace root");
244
+ }
245
+
246
+ try {
247
+ if (params.maxBytes !== undefined && opened.stat.size > params.maxBytes) {
248
+ throw new SafeFsError(
249
+ "too-large",
250
+ `file exceeds limit of ${params.maxBytes} bytes (got ${opened.stat.size})`
251
+ );
252
+ }
253
+ const buffer = await opened.handle.readFile();
254
+ return {
255
+ buffer,
256
+ realPath: opened.realPath,
257
+ stat: opened.stat,
258
+ };
259
+ } finally {
260
+ await opened.handle.close().catch(() => { });
261
+ }
262
+ }
263
+
264
+ function buildAtomicWriteTempPath(targetPath: string): string {
265
+ const dir = path.dirname(targetPath);
266
+ const base = path.basename(targetPath);
267
+ return path.join(dir, `.${base}.${process.pid}.${randomUUID()}.tmp`);
268
+ }
269
+
270
+ async function writeTempFileForAtomicReplace(params: {
271
+ tempPath: string;
272
+ data: string | Buffer;
273
+ encoding?: BufferEncoding;
274
+ mode: number;
275
+ }): Promise<Stats> {
276
+ const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
277
+ try {
278
+ if (typeof params.data === "string") {
279
+ await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
280
+ } else {
281
+ await tempHandle.writeFile(params.data);
282
+ }
283
+ return await tempHandle.stat();
284
+ } finally {
285
+ await tempHandle.close().catch(() => { });
286
+ }
287
+ }
288
+
289
+ async function verifyAtomicWriteResult(params: {
290
+ rootDir: string;
291
+ targetPath: string;
292
+ expectedStat: Stats;
293
+ }): Promise<void> {
294
+ const rootReal = await fs.realpath(params.rootDir);
295
+ const rootWithSep = ensureTrailingSep(rootReal);
296
+ const opened = await openVerifiedLocalFile(params.targetPath, { rejectHardlinks: true });
297
+ try {
298
+ if (!sameFileIdentity(opened.stat, params.expectedStat)) {
299
+ throw new SafeFsError("path-mismatch", "path changed during write");
300
+ }
301
+ if (!isPathInside(rootWithSep, opened.realPath)) {
302
+ throw new SafeFsError("outside-root", "file is outside workspace root");
303
+ }
304
+ } finally {
305
+ await opened.handle.close().catch(() => { });
306
+ }
307
+ }
308
+
309
+ export async function safeWriteFile(params: SafeWriteOptions): Promise<SafeWriteResult> {
310
+ const { rootWithSep, resolved } = await resolvePathWithinRoot(params);
311
+
312
+ if (params.mkdir !== false) {
313
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
314
+ }
315
+
316
+ let ioPath = resolved;
317
+ let existingFile = false;
318
+
319
+ try {
320
+ const resolvedRealPath = await fs.realpath(resolved);
321
+ if (!isPathInside(rootWithSep, resolvedRealPath)) {
322
+ throw new SafeFsError("outside-root", "file is outside workspace root");
323
+ }
324
+ ioPath = resolvedRealPath;
325
+ existingFile = true;
326
+ } catch (err) {
327
+ if (err instanceof SafeFsError) {
328
+ throw err;
329
+ }
330
+ if (!isNotFoundPathError(err)) {
331
+ throw err;
332
+ }
333
+ }
334
+
335
+ const fileMode = 0o600;
336
+ const isAppend = params.mode === "append";
337
+
338
+ if (isAppend && !existingFile) {
339
+ throw new SafeFsError("not-found", "cannot append to non-existent file");
340
+ }
341
+
342
+ if (isAppend) {
343
+ return await appendToFile({
344
+ ioPath,
345
+ rootWithSep,
346
+ data: params.data,
347
+ encoding: params.encoding,
348
+ maxBytes: params.maxBytes,
349
+ });
350
+ }
351
+
352
+ return await overwriteFile({
353
+ ioPath,
354
+ rootWithSep,
355
+ rootDir: params.rootDir,
356
+ data: params.data,
357
+ encoding: params.encoding,
358
+ existingFile,
359
+ fileMode,
360
+ });
361
+ }
362
+
363
+ async function appendToFile(params: {
364
+ ioPath: string;
365
+ rootWithSep: string;
366
+ data: string | Buffer;
367
+ encoding?: BufferEncoding;
368
+ maxBytes?: number;
369
+ }): Promise<SafeWriteResult> {
370
+ let handle: FileHandle;
371
+ try {
372
+ handle = await fs.open(params.ioPath, OPEN_APPEND_FLAGS);
373
+ } catch (err) {
374
+ if (isNotFoundPathError(err)) {
375
+ throw new SafeFsError("not-found", "file not found");
376
+ }
377
+ if (isSymlinkOpenError(err)) {
378
+ throw new SafeFsError("symlink", "symlink open blocked", { cause: err });
379
+ }
380
+ throw err;
381
+ }
382
+
383
+ try {
384
+ const stat = await handle.stat();
385
+ if (!stat.isFile()) {
386
+ throw new SafeFsError("not-file", "path is not a regular file");
387
+ }
388
+
389
+ const lstat = await fs.lstat(params.ioPath);
390
+ if (lstat.isSymbolicLink()) {
391
+ throw new SafeFsError("symlink", "path is a symlink");
392
+ }
393
+ if (!sameFileIdentity(stat, lstat)) {
394
+ throw new SafeFsError("path-mismatch", "path changed during write");
395
+ }
396
+
397
+ const realPath = await fs.realpath(params.ioPath);
398
+ if (!isPathInside(params.rootWithSep, realPath)) {
399
+ throw new SafeFsError("outside-root", "file is outside workspace root");
400
+ }
401
+
402
+ const dataSize = typeof params.data === "string"
403
+ ? Buffer.byteLength(params.data, params.encoding ?? "utf8")
404
+ : params.data.length;
405
+
406
+ if (params.maxBytes !== undefined && stat.size + dataSize > params.maxBytes) {
407
+ throw new SafeFsError(
408
+ "too-large",
409
+ `file would exceed limit of ${params.maxBytes} bytes`
410
+ );
411
+ }
412
+
413
+ if (typeof params.data === "string") {
414
+ await handle.writeFile(params.data, params.encoding ?? "utf8");
415
+ } else {
416
+ await handle.writeFile(params.data);
417
+ }
418
+
419
+ return {
420
+ realPath,
421
+ bytesWritten: dataSize,
422
+ created: false,
423
+ };
424
+ } finally {
425
+ await handle.close().catch(() => { });
426
+ }
427
+ }
428
+
429
+ async function overwriteFile(params: {
430
+ ioPath: string;
431
+ rootWithSep: string;
432
+ rootDir: string;
433
+ data: string | Buffer;
434
+ encoding?: BufferEncoding;
435
+ existingFile: boolean;
436
+ fileMode: number;
437
+ }): Promise<SafeWriteResult> {
438
+ let tempPath: string | null = null;
439
+ const destinationPath = params.ioPath;
440
+
441
+ try {
442
+ tempPath = buildAtomicWriteTempPath(destinationPath);
443
+ const writtenStat = await writeTempFileForAtomicReplace({
444
+ tempPath,
445
+ data: params.data,
446
+ encoding: params.encoding,
447
+ mode: params.fileMode,
448
+ });
449
+
450
+ await fs.rename(tempPath, destinationPath);
451
+ tempPath = null;
452
+
453
+ try {
454
+ await verifyAtomicWriteResult({
455
+ rootDir: params.rootDir,
456
+ targetPath: destinationPath,
457
+ expectedStat: writtenStat,
458
+ });
459
+ } catch (err) {
460
+ // 修复:重新抛出错误
461
+ throw new SafeFsError("write-failed", `${JSON.stringify(err)}`);
462
+ }
463
+
464
+ const dataSize = typeof params.data === "string"
465
+ ? Buffer.byteLength(params.data, params.encoding ?? "utf8")
466
+ : params.data.length;
467
+
468
+ return {
469
+ realPath: destinationPath,
470
+ bytesWritten: dataSize,
471
+ created: !params.existingFile,
472
+ };
473
+ } finally {
474
+ if (tempPath) {
475
+ await fs.rm(tempPath, { force: true }).catch(() => { });
476
+ }
477
+ }
478
+ }
479
+
480
+ export async function safeCopyFile(params: {
481
+ sourcePath: string;
482
+ rootDir: string;
483
+ relativePath: string;
484
+ maxBytes?: number;
485
+ mkdir?: boolean;
486
+ }): Promise<void> {
487
+ const source = await openVerifiedLocalFile(params.sourcePath, { rejectHardlinks: true });
488
+
489
+ if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
490
+ await source.handle.close().catch(() => { });
491
+ throw new SafeFsError(
492
+ "too-large",
493
+ `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`
494
+ );
495
+ }
496
+
497
+ const { resolved } = await resolvePathWithinRoot({
498
+ rootDir: params.rootDir,
499
+ relativePath: params.relativePath,
500
+ });
501
+
502
+ if (params.mkdir !== false) {
503
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
504
+ }
505
+
506
+ let tempPath: string | null = null;
507
+ let tempHandle: FileHandle | null = null;
508
+
509
+ try {
510
+ tempPath = buildAtomicWriteTempPath(resolved);
511
+ tempHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
512
+
513
+ const sourceStream = source.handle.createReadStream();
514
+ const targetStream = tempHandle.createWriteStream();
515
+
516
+ await new Promise<void>((resolve, reject) => {
517
+ sourceStream.pipe(targetStream);
518
+ sourceStream.on("end", resolve);
519
+ sourceStream.on("error", reject);
520
+ targetStream.on("error", reject);
521
+ });
522
+
523
+ await tempHandle.close().catch(() => { });
524
+ tempHandle = null;
525
+
526
+ const writtenStat = await fs.stat(tempPath);
527
+ await fs.rename(tempPath, resolved);
528
+ tempPath = null;
529
+
530
+ await verifyAtomicWriteResult({
531
+ rootDir: params.rootDir,
532
+ targetPath: resolved,
533
+ expectedStat: writtenStat,
534
+ });
535
+ } finally {
536
+ if (tempPath) {
537
+ await fs.rm(tempPath, { force: true }).catch(() => { });
538
+ }
539
+ if (tempHandle) {
540
+ await tempHandle.close().catch(() => { });
541
+ }
542
+ await source.handle.close().catch(() => { });
543
+ }
544
+ }
@@ -1,103 +0,0 @@
1
- # 设备控制脚本使用说明
2
-
3
- 本脚本提供统一的设备控制接口,避免 Windows PowerShell 对 JSON 参数的引号处理问题。
4
-
5
- ## 快速开始
6
-
7
- ### 1. 获取设备ID
8
-
9
- ```bash
10
- npx openclaw nodes status
11
- ```
12
-
13
- ### 2. 创建配置文件
14
-
15
- 复制并修改示例配置文件,将 `YOUR_DEVICE_ID` 替换为实际的设备ID。
16
-
17
- ```json
18
- {
19
- "nodeId": "f5f8916028aa52091fd9e97bdb77b294b73dcef339541e33d0b51a3b8d595b6c",
20
- "command": "volume.operate",
21
- "params": {
22
- "actionType": "关闭"
23
- }
24
- }
25
- ```
26
-
27
- ### 3. 执行命令
28
-
29
- ```bash
30
- cd claw-yoyo-ext
31
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/volume-close.json
32
- ```
33
-
34
- ## 使用示例
35
-
36
- ### 音量控制
37
-
38
- ```bash
39
- # 关闭音量
40
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/volume-close.json
41
-
42
- # 调大音量
43
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/volume-up.json
44
-
45
- # 设置音量为50
46
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/volume-set-50.json
47
- ```
48
-
49
- ### 电话拨打
50
-
51
- ```bash
52
- # 拨打电话
53
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/phone-call.json
54
- ```
55
-
56
- ### 屏幕录制
57
-
58
- ```bash
59
- # 开始录制
60
- node skills/devices-control/scripts/invoke.js skills/devices-control/scripts/screen-record-start.json
61
- ```
62
-
63
- ## 调试模式
64
-
65
- 启用调试模式查看详细执行信息:
66
-
67
- ```bash
68
- node skills/devices-control/scripts/invoke.js config.json --debug
69
- ```
70
-
71
- ## 配置文件格式
72
-
73
- ```json
74
- {
75
- "nodeId": "设备ID(必填)",
76
- "command": "命令标识(必填)",
77
- "params": {
78
- // 命令参数(可选)
79
- }
80
- }
81
- ```
82
-
83
- ## 常用命令标识
84
-
85
- | 功能 | 命令标识 |
86
- |------|----------|
87
- | 音量控制 | `volume.operate` |
88
- | 电话拨打 | `phone.call` |
89
- | 屏幕录制 | `screen_recorder.control` |
90
-
91
- ## 故障排查
92
-
93
- ### 问题:找不到 openclaw 命令
94
-
95
- 确保在 `claw-yoyo-ext` 目录下执行命令,或确保 `openclaw` 已全局安装。
96
-
97
- ### 问题:设备ID无效
98
-
99
- 使用 `npx openclaw nodes status` 查看有效的设备ID。
100
-
101
- ### 问题:参数格式错误
102
-
103
- 参考 `examples.json` 查看完整的参数示例。
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Device Control Invoke Script
4
- *
5
- * 封装 openclaw nodes invoke 命令,提供统一的设备控制接口
6
- * 使用配置文件方式避免 Windows PowerShell 对 JSON 参数的引号处理问题
7
- */
8
-
9
- import { exec } from "child_process";
10
- import { readFileSync } from "fs";
11
- import { resolve } from "path";
12
- import { promisify } from "util";
13
-
14
- const execAsync = promisify(exec);
15
-
16
- /**
17
- * 显示使用说明
18
- */
19
- function usage() {
20
- console.error("用法: node invoke.js <config_file> [--debug]");
21
- console.error("");
22
- console.error("参数:");
23
- console.error(" config_file - JSON 配置文件路径");
24
- console.error(" --debug - 启用调试模式");
25
- console.error("");
26
- console.error("配置文件格式:");
27
- console.error("{");
28
- console.error(' "nodeId": "设备ID",');
29
- console.error(' "command": "命令标识",');
30
- console.error(' "params": { ...参数对象 }');
31
- console.error("}");
32
- console.error("");
33
- console.error("示例:");
34
- console.error(" node invoke.js config.json");
35
- console.error(" node invoke.js config.json --debug");
36
- process.exit(2);
37
- }
38
-
39
- /**
40
- * 执行节点命令
41
- */
42
- async function invokeNode(nodeId, command, params, timeout = 15000, debug = false) {
43
- const paramsStr = JSON.stringify(params);
44
- const cmd = `npx openclaw nodes invoke --node ${nodeId} --command ${command} --params "${paramsStr.replace(/"/g, '\\"')}"`;
45
-
46
- if (debug) {
47
- console.log("=== 调试信息 ===");
48
- console.log("执行命令:", cmd);
49
- console.log("设备 ID:", nodeId);
50
- console.log("命令标识:", command);
51
- console.log("参数对象:", JSON.stringify(params, null, 2));
52
- console.log("================");
53
- }
54
-
55
- try {
56
- const { stdout, stderr } = await execAsync(cmd, {
57
- timeout,
58
- env: { ...process.env },
59
- cwd: process.cwd()
60
- });
61
-
62
- if (debug) {
63
- console.log("=== 执行结果 ===");
64
- console.log("标准输出:", stdout.trim());
65
- console.log("错误输出:", stderr.trim());
66
- console.log("================");
67
- }
68
-
69
- return {
70
- success: true,
71
- data: stdout.trim(),
72
- error: stderr.trim() || null
73
- };
74
- } catch (error) {
75
- if (debug) {
76
- console.log("=== 执行失败 ===");
77
- console.log("错误信息:", error.message);
78
- console.log("标准输出:", error.stdout?.trim() || "");
79
- console.log("错误输出:", error.stderr?.trim() || "");
80
- console.log("================");
81
- }
82
-
83
- return {
84
- success: false,
85
- data: error.stdout?.trim() || "",
86
- error: error.stderr?.trim() || error.message
87
- };
88
- }
89
- }
90
-
91
- // 主程序
92
- const args = process.argv.slice(2);
93
- const debugIndex = args.indexOf("--debug");
94
- const debug = debugIndex !== -1;
95
- if (debug) args.splice(debugIndex, 1);
96
-
97
- if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
98
- usage();
99
- }
100
-
101
- const configPath = resolve(args[0]);
102
-
103
- try {
104
- const configContent = readFileSync(configPath, "utf-8");
105
- const config = JSON.parse(configContent);
106
- const { nodeId, command, params } = config;
107
-
108
- if (!nodeId || !command) {
109
- console.error("错误: 配置文件缺少必要字段: nodeId 或 command");
110
- process.exit(1);
111
- }
112
-
113
- const result = await invokeNode(nodeId, command, params || {}, 15000, debug);
114
- console.log(JSON.stringify(result, null, 2));
115
- process.exit(result.success ? 0 : 1);
116
- } catch (error) {
117
- console.error(`错误: ${error.message}`);
118
- process.exit(1);
119
- }
@@ -1,7 +0,0 @@
1
- {
2
- "nodeId": "YOUR_DEVICE_ID",
3
- "command": "volume.operate",
4
- "params": {
5
- "actionType": "调大"
6
- }
7
- }