@midscene/harmony 1.9.8 → 1.10.0
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/dist/es/cli.mjs +6 -6
- package/dist/es/index.mjs +5 -5
- package/dist/lib/cli.js +6 -6
- package/dist/lib/index.js +5 -5
- package/dist/types/index.d.ts +4 -4
- package/package.json +4 -9
- package/static/index.html +1 -1
- package/static/static/js/index.9c67d55f.js +951 -0
- package/static/static/js/index.9c67d55f.js.map +1 -0
- package/dist/es/mcp-server.mjs +0 -1116
- package/dist/lib/mcp-server.js +0 -1167
- package/dist/types/mcp-server.d.ts +0 -217
- package/static/static/js/index.44b83907.js +0 -948
- package/static/static/js/index.44b83907.js.map +0 -1
- /package/static/static/js/{index.44b83907.js.LICENSE.txt → index.9c67d55f.js.LICENSE.txt} +0 -0
package/dist/lib/mcp-server.js
DELETED
|
@@ -1,1167 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __webpack_require__ = {};
|
|
3
|
-
(()=>{
|
|
4
|
-
__webpack_require__.n = (module)=>{
|
|
5
|
-
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
-
__webpack_require__.d(getter, {
|
|
7
|
-
a: getter
|
|
8
|
-
});
|
|
9
|
-
return getter;
|
|
10
|
-
};
|
|
11
|
-
})();
|
|
12
|
-
(()=>{
|
|
13
|
-
__webpack_require__.d = (exports1, definition)=>{
|
|
14
|
-
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
15
|
-
enumerable: true,
|
|
16
|
-
get: definition[key]
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
})();
|
|
20
|
-
(()=>{
|
|
21
|
-
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
-
})();
|
|
23
|
-
(()=>{
|
|
24
|
-
__webpack_require__.r = (exports1)=>{
|
|
25
|
-
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
26
|
-
value: 'Module'
|
|
27
|
-
});
|
|
28
|
-
Object.defineProperty(exports1, '__esModule', {
|
|
29
|
-
value: true
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
})();
|
|
33
|
-
var __webpack_exports__ = {};
|
|
34
|
-
__webpack_require__.r(__webpack_exports__);
|
|
35
|
-
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
-
mcpKitForAgent: ()=>mcpKitForAgent,
|
|
37
|
-
HarmonyMCPServer: ()=>HarmonyMCPServer,
|
|
38
|
-
mcpServerForAgent: ()=>mcpServerForAgent
|
|
39
|
-
});
|
|
40
|
-
const mcp_namespaceObject = require("@midscene/shared/mcp");
|
|
41
|
-
const core_namespaceObject = require("@midscene/core");
|
|
42
|
-
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
43
|
-
const agent_behavior_init_args_namespaceObject = require("@midscene/shared/mcp/agent-behavior-init-args");
|
|
44
|
-
const base_tools_namespaceObject = require("@midscene/shared/mcp/base-tools");
|
|
45
|
-
const agent_namespaceObject = require("@midscene/core/agent");
|
|
46
|
-
const utils_namespaceObject = require("@midscene/shared/utils");
|
|
47
|
-
const defaultAppNameMapping = {
|
|
48
|
-
设置: 'com.huawei.hmos.settings',
|
|
49
|
-
Settings: 'com.huawei.hmos.settings',
|
|
50
|
-
相机: 'com.huawei.hmos.camera',
|
|
51
|
-
Camera: 'com.huawei.hmos.camera',
|
|
52
|
-
图库: 'com.huawei.hmos.photos',
|
|
53
|
-
Gallery: 'com.huawei.hmos.photos',
|
|
54
|
-
日历: 'com.huawei.hmos.calendar',
|
|
55
|
-
Calendar: 'com.huawei.hmos.calendar',
|
|
56
|
-
时钟: 'com.huawei.hmos.clock',
|
|
57
|
-
Clock: 'com.huawei.hmos.clock',
|
|
58
|
-
计算器: 'com.huawei.hmos.calculator',
|
|
59
|
-
Calculator: 'com.huawei.hmos.calculator',
|
|
60
|
-
文件管理: 'com.huawei.hmos.filemanager',
|
|
61
|
-
备忘录: 'com.huawei.hmos.notepad',
|
|
62
|
-
联系人: 'com.huawei.hmos.contacts',
|
|
63
|
-
电话: 'com.huawei.hmos.phone',
|
|
64
|
-
信息: 'com.huawei.hmos.message',
|
|
65
|
-
邮件: 'com.huawei.hmos.email',
|
|
66
|
-
浏览器: 'com.huawei.hmos.browser',
|
|
67
|
-
Browser: 'com.huawei.hmos.browser',
|
|
68
|
-
应用市场: 'com.huawei.appmarket',
|
|
69
|
-
AppGallery: 'com.huawei.appmarket',
|
|
70
|
-
华为音乐: 'com.huawei.hmsapp.music',
|
|
71
|
-
Music: 'com.huawei.hmsapp.music',
|
|
72
|
-
华为视频: 'com.huawei.hmos.video',
|
|
73
|
-
天气: 'com.huawei.hmos.weather',
|
|
74
|
-
Weather: 'com.huawei.hmos.weather',
|
|
75
|
-
抖音: 'com.ss.hm.ugc.aweme',
|
|
76
|
-
支付宝: 'com.alipay.mobile.client',
|
|
77
|
-
高德地图: 'com.amap.hmapp',
|
|
78
|
-
百度: 'com.baidu.baiduapp',
|
|
79
|
-
携程: 'com.ctrip.harmonynext'
|
|
80
|
-
};
|
|
81
|
-
const external_node_assert_namespaceObject = require("node:assert");
|
|
82
|
-
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
83
|
-
const external_node_fs_namespaceObject = require("node:fs");
|
|
84
|
-
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
|
|
85
|
-
const device_namespaceObject = require("@midscene/core/device");
|
|
86
|
-
const core_utils_namespaceObject = require("@midscene/core/utils");
|
|
87
|
-
const img_namespaceObject = require("@midscene/shared/img");
|
|
88
|
-
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
89
|
-
const external_node_util_namespaceObject = require("node:util");
|
|
90
|
-
function _define_property(obj, key, value) {
|
|
91
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
92
|
-
value: value,
|
|
93
|
-
enumerable: true,
|
|
94
|
-
configurable: true,
|
|
95
|
-
writable: true
|
|
96
|
-
});
|
|
97
|
-
else obj[key] = value;
|
|
98
|
-
return obj;
|
|
99
|
-
}
|
|
100
|
-
const execFileAsync = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
101
|
-
const debugHdc = (0, logger_namespaceObject.getDebug)('harmony:hdc');
|
|
102
|
-
function resolveHdcPath(hdcPath) {
|
|
103
|
-
if (hdcPath) return hdcPath;
|
|
104
|
-
if (process.env.HDC_HOME) {
|
|
105
|
-
const envPath = `${process.env.HDC_HOME}/hdc`;
|
|
106
|
-
debugHdc(`Using HDC from HDC_HOME: ${envPath}`);
|
|
107
|
-
return envPath;
|
|
108
|
-
}
|
|
109
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
110
|
-
const commonPaths = [
|
|
111
|
-
`${homeDir}/Library/HarmonyOS/next/command-line-tools/sdk/default/openharmony/toolchains/hdc`,
|
|
112
|
-
`${homeDir}/Library/HarmonyOS/sdk/hmscore/3.1.0/toolchains/hdc`
|
|
113
|
-
];
|
|
114
|
-
for (const p of commonPaths)try {
|
|
115
|
-
(0, external_node_fs_namespaceObject.accessSync)(p, external_node_fs_namespaceObject.constants.X_OK);
|
|
116
|
-
debugHdc(`Found HDC at: ${p}`);
|
|
117
|
-
return p;
|
|
118
|
-
} catch {}
|
|
119
|
-
return 'hdc';
|
|
120
|
-
}
|
|
121
|
-
class HdcClient {
|
|
122
|
-
buildArgs(args) {
|
|
123
|
-
if (this.deviceId) return [
|
|
124
|
-
'-t',
|
|
125
|
-
this.deviceId,
|
|
126
|
-
...args
|
|
127
|
-
];
|
|
128
|
-
return args;
|
|
129
|
-
}
|
|
130
|
-
async exec(...args) {
|
|
131
|
-
let release;
|
|
132
|
-
const prev = this.execMutex;
|
|
133
|
-
this.execMutex = new Promise((r)=>{
|
|
134
|
-
release = r;
|
|
135
|
-
});
|
|
136
|
-
await prev;
|
|
137
|
-
const fullArgs = this.buildArgs(args);
|
|
138
|
-
debugHdc(`hdc ${fullArgs.join(' ')}`);
|
|
139
|
-
try {
|
|
140
|
-
const { stdout, stderr } = await execFileAsync(this.hdcPath, fullArgs, {
|
|
141
|
-
timeout: this.timeout,
|
|
142
|
-
maxBuffer: 52428800
|
|
143
|
-
});
|
|
144
|
-
if (stderr?.trim()) debugHdc(`hdc stderr: ${stderr.trim()}`);
|
|
145
|
-
debugHdc(`hdc ${fullArgs.join(' ')} end`);
|
|
146
|
-
return stdout;
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (error.killed && error.stdout?.trim()) {
|
|
149
|
-
debugHdc('hdc process was killed but stdout is available, treating as success');
|
|
150
|
-
return error.stdout;
|
|
151
|
-
}
|
|
152
|
-
debugHdc(`hdc error: ${error.message}`);
|
|
153
|
-
throw new Error(`HDC command failed: hdc ${fullArgs.join(' ')}: ${error.message}`, {
|
|
154
|
-
cause: error
|
|
155
|
-
});
|
|
156
|
-
} finally{
|
|
157
|
-
release();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
async shell(command) {
|
|
161
|
-
return this.exec('shell', command);
|
|
162
|
-
}
|
|
163
|
-
async fileSend(localPath, remotePath) {
|
|
164
|
-
await this.exec('file', 'send', localPath, remotePath);
|
|
165
|
-
}
|
|
166
|
-
async fileRecv(remotePath, localPath) {
|
|
167
|
-
await this.exec('file', 'recv', remotePath, localPath);
|
|
168
|
-
}
|
|
169
|
-
async screenshot(remotePath) {
|
|
170
|
-
return await this.shell(`snapshot_display -f ${remotePath}`);
|
|
171
|
-
}
|
|
172
|
-
async dumpLayout() {
|
|
173
|
-
const remotePath = '/data/local/tmp/midscene_layout.json';
|
|
174
|
-
const output = await this.shell(`uitest dumpLayout -p ${remotePath} && cat ${remotePath}`);
|
|
175
|
-
const jsonStart = output.indexOf('{');
|
|
176
|
-
if (jsonStart < 0) throw new Error(`dumpLayout: no JSON body in output: ${output.slice(0, 200)}`);
|
|
177
|
-
return output.slice(jsonStart);
|
|
178
|
-
}
|
|
179
|
-
async click(x, y) {
|
|
180
|
-
await this.shell(`uitest uiInput click ${Math.round(x)} ${Math.round(y)}`);
|
|
181
|
-
}
|
|
182
|
-
async doubleClick(x, y) {
|
|
183
|
-
await this.shell(`uitest uiInput doubleClick ${Math.round(x)} ${Math.round(y)}`);
|
|
184
|
-
}
|
|
185
|
-
async longClick(x, y) {
|
|
186
|
-
await this.shell(`uitest uiInput longClick ${Math.round(x)} ${Math.round(y)}`);
|
|
187
|
-
}
|
|
188
|
-
async swipe(fromX, fromY, toX, toY, speed) {
|
|
189
|
-
const args = [
|
|
190
|
-
Math.round(fromX),
|
|
191
|
-
Math.round(fromY),
|
|
192
|
-
Math.round(toX),
|
|
193
|
-
Math.round(toY)
|
|
194
|
-
];
|
|
195
|
-
if (void 0 !== speed) args.push(Math.round(speed));
|
|
196
|
-
await this.shell(`uitest uiInput swipe ${args.join(' ')}`);
|
|
197
|
-
}
|
|
198
|
-
async fling(fromX, fromY, toX, toY, speed) {
|
|
199
|
-
const args = [
|
|
200
|
-
Math.round(fromX),
|
|
201
|
-
Math.round(fromY),
|
|
202
|
-
Math.round(toX),
|
|
203
|
-
Math.round(toY)
|
|
204
|
-
];
|
|
205
|
-
if (void 0 !== speed) args.push(Math.round(speed));
|
|
206
|
-
await this.shell(`uitest uiInput fling ${args.join(' ')}`);
|
|
207
|
-
}
|
|
208
|
-
async drag(fromX, fromY, toX, toY, speed) {
|
|
209
|
-
const args = [
|
|
210
|
-
Math.round(fromX),
|
|
211
|
-
Math.round(fromY),
|
|
212
|
-
Math.round(toX),
|
|
213
|
-
Math.round(toY)
|
|
214
|
-
];
|
|
215
|
-
if (void 0 !== speed) args.push(Math.round(speed));
|
|
216
|
-
await this.shell(`uitest uiInput drag ${args.join(' ')}`);
|
|
217
|
-
}
|
|
218
|
-
async inputText(x, y, text) {
|
|
219
|
-
const escapedText = text.replace(/'/g, "'\\''");
|
|
220
|
-
await this.shell(`uitest uiInput inputText ${Math.round(x)} ${Math.round(y)} '${escapedText}'`);
|
|
221
|
-
}
|
|
222
|
-
async keyEvent(...keys) {
|
|
223
|
-
await this.shell(`uitest uiInput keyEvent ${keys.join(' ')}`);
|
|
224
|
-
}
|
|
225
|
-
async clearTextField(length = 100) {
|
|
226
|
-
if (length <= 0) return;
|
|
227
|
-
const MAX_KEYS_PER_CALL = 3;
|
|
228
|
-
const cmds = [];
|
|
229
|
-
let remaining = length;
|
|
230
|
-
while(remaining > 0){
|
|
231
|
-
const n = Math.min(MAX_KEYS_PER_CALL, remaining);
|
|
232
|
-
const codes = Array(n).fill('2055').join(' ');
|
|
233
|
-
cmds.push(`uitest uiInput keyEvent ${codes}`);
|
|
234
|
-
remaining -= n;
|
|
235
|
-
}
|
|
236
|
-
await this.shell(cmds.join(';'));
|
|
237
|
-
}
|
|
238
|
-
async startAbility(bundleName, abilityName) {
|
|
239
|
-
const output = await this.shell(`aa start -a ${abilityName} -b ${bundleName}`);
|
|
240
|
-
if (output.includes('error:')) throw new Error(`Failed to start ${bundleName}/${abilityName}: ${output.trim()}`);
|
|
241
|
-
}
|
|
242
|
-
async queryMainAbility(bundleName) {
|
|
243
|
-
const output = await this.shell(`bm dump -n ${bundleName}`);
|
|
244
|
-
const names = [];
|
|
245
|
-
for (const match of output.matchAll(/"name"\s*:\s*"([^"]+)"/g))names.push(match[1]);
|
|
246
|
-
for (const candidate of [
|
|
247
|
-
'EntryAbility',
|
|
248
|
-
'MainAbility',
|
|
249
|
-
`${bundleName}.MainAbility`
|
|
250
|
-
])if (names.includes(candidate)) return candidate;
|
|
251
|
-
return names.find((n)=>n !== bundleName && n.endsWith('Ability') && !n.includes('Extension') && !n.includes('Service') && !n.includes('Form') && !n.includes('Dialog'));
|
|
252
|
-
}
|
|
253
|
-
async forceStop(bundleName) {
|
|
254
|
-
const output = await this.shell(`aa force-stop ${bundleName}`);
|
|
255
|
-
if (output.includes('error:')) throw new Error(`Failed to force stop ${bundleName}: ${output.trim()}`);
|
|
256
|
-
}
|
|
257
|
-
async getScreenInfo() {
|
|
258
|
-
const stdout = await this.shell('hidumper -s RenderService -a screen');
|
|
259
|
-
const renderDimensionPattern = 'render\\s+(?:size|resolution)\\s*[:=]\\s*(\\d{3,5})x(\\d{3,5})';
|
|
260
|
-
const activeFoldMatch = stdout.match(/foldScreenId:(\d+),\s*isConnected:\d+,\s*isPowerOn:1/);
|
|
261
|
-
if (activeFoldMatch) {
|
|
262
|
-
const activeId = activeFoldMatch[1];
|
|
263
|
-
const screenRegex = new RegExp(`screen\\[\\d+\\]:\\s*id=${activeId},.*?${renderDimensionPattern}`);
|
|
264
|
-
const screenMatch = stdout.match(screenRegex);
|
|
265
|
-
if (screenMatch) {
|
|
266
|
-
debugHdc(`Foldable screen detected, active screen id=${activeId}: ${screenMatch[1]}x${screenMatch[2]}`);
|
|
267
|
-
return {
|
|
268
|
-
width: Number.parseInt(screenMatch[1], 10),
|
|
269
|
-
height: Number.parseInt(screenMatch[2], 10)
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const renderSizeMatch = stdout.match(new RegExp(renderDimensionPattern));
|
|
274
|
-
if (renderSizeMatch) return {
|
|
275
|
-
width: Number.parseInt(renderSizeMatch[1], 10),
|
|
276
|
-
height: Number.parseInt(renderSizeMatch[2], 10)
|
|
277
|
-
};
|
|
278
|
-
const displayStdout = await this.shell('hidumper -s DisplayManagerService -a');
|
|
279
|
-
const displayMatch = displayStdout.match(/activeModes.*?(\d{3,5}),\s*(\d{3,5})/);
|
|
280
|
-
if (displayMatch) return {
|
|
281
|
-
width: Number.parseInt(displayMatch[1], 10),
|
|
282
|
-
height: Number.parseInt(displayMatch[2], 10)
|
|
283
|
-
};
|
|
284
|
-
throw new Error(`Failed to get screen size from HDC. RenderService output: ${stdout}`);
|
|
285
|
-
}
|
|
286
|
-
async listTargets() {
|
|
287
|
-
const stdout = await this.exec('list', 'targets');
|
|
288
|
-
return stdout.trim().split('\n').map((line)=>line.trim()).filter((line)=>line.length > 0 && !line.startsWith('['));
|
|
289
|
-
}
|
|
290
|
-
constructor(options){
|
|
291
|
-
_define_property(this, "hdcPath", void 0);
|
|
292
|
-
_define_property(this, "deviceId", void 0);
|
|
293
|
-
_define_property(this, "timeout", void 0);
|
|
294
|
-
_define_property(this, "execMutex", Promise.resolve());
|
|
295
|
-
this.hdcPath = resolveHdcPath(options.hdcPath);
|
|
296
|
-
this.deviceId = options.deviceId ?? '';
|
|
297
|
-
this.timeout = options.timeout ?? 60000;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
function device_define_property(obj, key, value) {
|
|
301
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
302
|
-
value: value,
|
|
303
|
-
enumerable: true,
|
|
304
|
-
configurable: true,
|
|
305
|
-
writable: true
|
|
306
|
-
});
|
|
307
|
-
else obj[key] = value;
|
|
308
|
-
return obj;
|
|
309
|
-
}
|
|
310
|
-
const defaultScrollUntilTimes = 10;
|
|
311
|
-
const defaultFastSwipeSpeed = 2000;
|
|
312
|
-
const maxScrollDistance = 9999999;
|
|
313
|
-
const scrollQuadrantDivisions = 4;
|
|
314
|
-
const screenEdgeMargin = 50;
|
|
315
|
-
const debugDevice = (0, logger_namespaceObject.getDebug)('harmony:device');
|
|
316
|
-
let screenshotResizeScaleWarned = false;
|
|
317
|
-
const INPUT_FIELD_TYPES = new Set([
|
|
318
|
-
'TextInput',
|
|
319
|
-
'TextArea',
|
|
320
|
-
'SearchField'
|
|
321
|
-
]);
|
|
322
|
-
function parseBounds(raw) {
|
|
323
|
-
if ('string' != typeof raw) return null;
|
|
324
|
-
const m = raw.match(/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/);
|
|
325
|
-
if (!m) return null;
|
|
326
|
-
return {
|
|
327
|
-
x1: Number(m[1]),
|
|
328
|
-
y1: Number(m[2]),
|
|
329
|
-
x2: Number(m[3]),
|
|
330
|
-
y2: Number(m[4])
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
function collectInputFields(layout) {
|
|
334
|
-
const fields = [];
|
|
335
|
-
const visit = (node)=>{
|
|
336
|
-
if (!node || 'object' != typeof node) return;
|
|
337
|
-
const n = node;
|
|
338
|
-
const attrs = n.attributes ?? {};
|
|
339
|
-
if (INPUT_FIELD_TYPES.has(String(attrs.type))) {
|
|
340
|
-
const bounds = parseBounds(attrs.bounds);
|
|
341
|
-
if (bounds) fields.push({
|
|
342
|
-
text: String(attrs.text ?? ''),
|
|
343
|
-
bounds
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
for (const child of n.children ?? [])visit(child);
|
|
347
|
-
};
|
|
348
|
-
visit(layout);
|
|
349
|
-
return fields;
|
|
350
|
-
}
|
|
351
|
-
function pickFieldByPoint(fields, point) {
|
|
352
|
-
const [px, py] = point;
|
|
353
|
-
return fields.find(({ bounds: b })=>px >= b.x1 && px <= b.x2 && py >= b.y1 && py <= b.y2);
|
|
354
|
-
}
|
|
355
|
-
function pickLongestField(fields) {
|
|
356
|
-
return fields.reduce((a, b)=>b.text.length > a.text.length ? b : a);
|
|
357
|
-
}
|
|
358
|
-
const harmonyKeyCodeMap = {
|
|
359
|
-
Enter: '2054',
|
|
360
|
-
Backspace: '2055',
|
|
361
|
-
Tab: '2049',
|
|
362
|
-
Escape: '2070',
|
|
363
|
-
Home: 'Home',
|
|
364
|
-
ArrowUp: '2012',
|
|
365
|
-
ArrowDown: '2013',
|
|
366
|
-
ArrowLeft: '2014',
|
|
367
|
-
ArrowRight: '2015',
|
|
368
|
-
Space: '2050',
|
|
369
|
-
Delete: '2071'
|
|
370
|
-
};
|
|
371
|
-
const keyNameAliasMap = {
|
|
372
|
-
enter: 'Enter',
|
|
373
|
-
backspace: 'Backspace',
|
|
374
|
-
tab: 'Tab',
|
|
375
|
-
escape: 'Escape',
|
|
376
|
-
esc: 'Escape',
|
|
377
|
-
home: 'Home',
|
|
378
|
-
space: 'Space',
|
|
379
|
-
delete: 'Delete',
|
|
380
|
-
arrowup: 'ArrowUp',
|
|
381
|
-
arrowdown: 'ArrowDown',
|
|
382
|
-
arrowleft: 'ArrowLeft',
|
|
383
|
-
arrowright: 'ArrowRight',
|
|
384
|
-
up: 'ArrowUp',
|
|
385
|
-
down: 'ArrowDown',
|
|
386
|
-
left: 'ArrowLeft',
|
|
387
|
-
right: 'ArrowRight'
|
|
388
|
-
};
|
|
389
|
-
class HarmonyDevice {
|
|
390
|
-
actionSpace() {
|
|
391
|
-
const mobileActionContext = {
|
|
392
|
-
input: this.inputPrimitives,
|
|
393
|
-
size: ()=>this.size(),
|
|
394
|
-
sleep: async (timeMs)=>{
|
|
395
|
-
await (0, core_utils_namespaceObject.sleep)(timeMs);
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
const defaultActions = [
|
|
399
|
-
...(0, device_namespaceObject.createDefaultMobileActions)(mobileActionContext)
|
|
400
|
-
];
|
|
401
|
-
const platformSpecificActions = Object.values(createPlatformActions(this));
|
|
402
|
-
const customActions = this.customActions ?? [];
|
|
403
|
-
return [
|
|
404
|
-
...defaultActions,
|
|
405
|
-
...platformSpecificActions,
|
|
406
|
-
...customActions
|
|
407
|
-
];
|
|
408
|
-
}
|
|
409
|
-
async performActionScroll(param) {
|
|
410
|
-
const element = param.locate;
|
|
411
|
-
const startingPoint = element ? {
|
|
412
|
-
left: element.center[0],
|
|
413
|
-
top: element.center[1]
|
|
414
|
-
} : void 0;
|
|
415
|
-
const scrollToEventName = param?.scrollType;
|
|
416
|
-
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
|
|
417
|
-
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
|
|
418
|
-
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
|
|
419
|
-
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
|
|
420
|
-
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
421
|
-
else {
|
|
422
|
-
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance ?? void 0, startingPoint);
|
|
423
|
-
else if ('left' === param.direction) await this.scrollLeft(param.distance ?? void 0, startingPoint);
|
|
424
|
-
else if ('right' === param.direction) await this.scrollRight(param.distance ?? void 0, startingPoint);
|
|
425
|
-
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
426
|
-
else await this.scrollDown(param?.distance ?? void 0, startingPoint);
|
|
427
|
-
await (0, core_utils_namespaceObject.sleep)(500);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
describe() {
|
|
431
|
-
return this.descriptionText || `DeviceId: ${this.deviceId}`;
|
|
432
|
-
}
|
|
433
|
-
async connect() {
|
|
434
|
-
const hdc = await this.getHdc();
|
|
435
|
-
return hdc;
|
|
436
|
-
}
|
|
437
|
-
async getHdc() {
|
|
438
|
-
if (this.destroyed) throw new Error(`HarmonyDevice ${this.deviceId} has been destroyed and cannot execute HDC commands`);
|
|
439
|
-
if (this.hdc) return this.hdc;
|
|
440
|
-
if (this.connecting) return this.connecting;
|
|
441
|
-
this.connecting = (async ()=>{
|
|
442
|
-
debugDevice(`Initializing HDC with device ID: ${this.deviceId}`);
|
|
443
|
-
try {
|
|
444
|
-
this.hdc = new HdcClient({
|
|
445
|
-
hdcPath: this.options?.hdcPath,
|
|
446
|
-
deviceId: this.deviceId
|
|
447
|
-
});
|
|
448
|
-
const screenInfo = await this.hdc.getScreenInfo();
|
|
449
|
-
this.cachedScreenSize = screenInfo;
|
|
450
|
-
this.descriptionText = `DeviceId: ${this.deviceId}\nScreenSize: ${screenInfo.width}x${screenInfo.height}`;
|
|
451
|
-
debugDevice('HDC initialized successfully', this.descriptionText);
|
|
452
|
-
return this.hdc;
|
|
453
|
-
} catch (e) {
|
|
454
|
-
debugDevice(`Failed to initialize HDC: ${e}`);
|
|
455
|
-
throw new Error(`Unable to connect to device ${this.deviceId}: ${e}`);
|
|
456
|
-
} finally{
|
|
457
|
-
this.connecting = null;
|
|
458
|
-
}
|
|
459
|
-
})();
|
|
460
|
-
return this.connecting;
|
|
461
|
-
}
|
|
462
|
-
setAppNameMapping(mapping) {
|
|
463
|
-
this.appNameMapping = mapping;
|
|
464
|
-
}
|
|
465
|
-
resolvePackageName(appName) {
|
|
466
|
-
const normalizedAppName = (0, utils_namespaceObject.normalizeForComparison)(appName);
|
|
467
|
-
return this.appNameMapping[normalizedAppName];
|
|
468
|
-
}
|
|
469
|
-
async launch(uri) {
|
|
470
|
-
const hdc = await this.getHdc();
|
|
471
|
-
this.uri = uri;
|
|
472
|
-
try {
|
|
473
|
-
debugDevice(`Launching app: ${uri}`);
|
|
474
|
-
if (uri.startsWith('http://') || uri.startsWith('https://') || uri.includes('://')) {
|
|
475
|
-
const sanitizedUri = uri.replace(/[`$\\;"'|&<>(){}]/g, '');
|
|
476
|
-
await hdc.shell(`aa start -U ${sanitizedUri}`);
|
|
477
|
-
} else if (uri.includes('/')) {
|
|
478
|
-
const [bundleName, abilityName] = uri.split('/');
|
|
479
|
-
await hdc.startAbility(bundleName, abilityName);
|
|
480
|
-
} else {
|
|
481
|
-
const bundleName = this.resolvePackageName(uri) ?? uri;
|
|
482
|
-
try {
|
|
483
|
-
await hdc.startAbility(bundleName, 'EntryAbility');
|
|
484
|
-
} catch (e) {
|
|
485
|
-
if (!e.message?.includes('resolve ability')) throw e;
|
|
486
|
-
const mainAbility = await hdc.queryMainAbility(bundleName);
|
|
487
|
-
if (!mainAbility) throw new Error(`Cannot find a launchable ability for ${bundleName}`);
|
|
488
|
-
debugDevice(`EntryAbility not found, using discovered ability: ${mainAbility}`);
|
|
489
|
-
await hdc.startAbility(bundleName, mainAbility);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
debugDevice(`Successfully launched: ${uri}`);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
debugDevice(`Error launching ${uri}: ${error}`);
|
|
495
|
-
throw new Error(`Failed to launch ${uri}: ${error.message}`, {
|
|
496
|
-
cause: error
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
return this;
|
|
500
|
-
}
|
|
501
|
-
async terminate(uri) {
|
|
502
|
-
const bundlePart = uri.includes('/') ? uri.split('/')[0] : uri;
|
|
503
|
-
const resolved = this.resolvePackageName(bundlePart) ?? bundlePart;
|
|
504
|
-
const hdc = await this.getHdc();
|
|
505
|
-
try {
|
|
506
|
-
debugDevice(`Terminating app: ${resolved}`);
|
|
507
|
-
await hdc.forceStop(resolved);
|
|
508
|
-
debugDevice(`Successfully terminated: ${resolved}`);
|
|
509
|
-
} catch (error) {
|
|
510
|
-
debugDevice(`Error terminating ${resolved}: ${error}`);
|
|
511
|
-
throw new Error(`Failed to terminate ${resolved}: ${error.message}`, {
|
|
512
|
-
cause: error
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
async getScreenSize() {
|
|
517
|
-
if (this.cachedScreenSize) return this.cachedScreenSize;
|
|
518
|
-
const hdc = await this.getHdc();
|
|
519
|
-
const screenInfo = await hdc.getScreenInfo();
|
|
520
|
-
this.cachedScreenSize = screenInfo;
|
|
521
|
-
return screenInfo;
|
|
522
|
-
}
|
|
523
|
-
async size() {
|
|
524
|
-
const screenInfo = await this.getScreenSize();
|
|
525
|
-
return {
|
|
526
|
-
width: screenInfo.width,
|
|
527
|
-
height: screenInfo.height
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
async screenshotBase64() {
|
|
531
|
-
debugDevice('screenshotBase64 begin');
|
|
532
|
-
const hdc = await this.getHdc();
|
|
533
|
-
if (!this.localScreenshotPath) this.localScreenshotPath = (0, core_utils_namespaceObject.getTmpFile)('jpeg');
|
|
534
|
-
const maxAttempts = 2;
|
|
535
|
-
for(let attempt = 1; attempt <= maxAttempts; attempt++){
|
|
536
|
-
const snapshotOutput = await hdc.screenshot(this.remoteScreenshotPath);
|
|
537
|
-
const dimMatch = snapshotOutput.match(/width\s+(\d+),\s*height\s+(\d+)/);
|
|
538
|
-
if (dimMatch) {
|
|
539
|
-
const w = Number.parseInt(dimMatch[1], 10);
|
|
540
|
-
const h = Number.parseInt(dimMatch[2], 10);
|
|
541
|
-
if (this.cachedScreenSize && (this.cachedScreenSize.width !== w || this.cachedScreenSize.height !== h)) {
|
|
542
|
-
debugDevice(`Screen size changed: ${this.cachedScreenSize.width}x${this.cachedScreenSize.height} -> ${w}x${h}`);
|
|
543
|
-
this.cachedScreenSize = {
|
|
544
|
-
width: w,
|
|
545
|
-
height: h
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
await hdc.fileRecv(this.remoteScreenshotPath, this.localScreenshotPath);
|
|
550
|
-
const screenshotBuffer = await external_node_fs_default().promises.readFile(this.localScreenshotPath);
|
|
551
|
-
if (screenshotBuffer && screenshotBuffer.length > 0) {
|
|
552
|
-
debugDevice(`Screenshot captured: ${screenshotBuffer.length} bytes`);
|
|
553
|
-
return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
|
|
554
|
-
}
|
|
555
|
-
debugDevice(`Screenshot buffer empty (attempt ${attempt}/${maxAttempts})`);
|
|
556
|
-
if (attempt < maxAttempts) await (0, core_utils_namespaceObject.sleep)(200);
|
|
557
|
-
}
|
|
558
|
-
throw new Error('Screenshot buffer is empty after retries');
|
|
559
|
-
}
|
|
560
|
-
async tapPoint(point) {
|
|
561
|
-
this.lastTapPosition = {
|
|
562
|
-
x: point.x,
|
|
563
|
-
y: point.y
|
|
564
|
-
};
|
|
565
|
-
const hdc = await this.getHdc();
|
|
566
|
-
await hdc.click(point.x, point.y);
|
|
567
|
-
}
|
|
568
|
-
async doubleTapPoint(point) {
|
|
569
|
-
const hdc = await this.getHdc();
|
|
570
|
-
await hdc.doubleClick(point.x, point.y);
|
|
571
|
-
}
|
|
572
|
-
async longPressPoint(point) {
|
|
573
|
-
const hdc = await this.getHdc();
|
|
574
|
-
await hdc.longClick(point.x, point.y);
|
|
575
|
-
}
|
|
576
|
-
async typeText(text, element, shouldReplace, options) {
|
|
577
|
-
if (!text) return;
|
|
578
|
-
const hdc = await this.getHdc();
|
|
579
|
-
let x;
|
|
580
|
-
let y;
|
|
581
|
-
if (element) [x, y] = element.center;
|
|
582
|
-
else if (this.lastTapPosition) {
|
|
583
|
-
x = this.lastTapPosition.x;
|
|
584
|
-
y = this.lastTapPosition.y;
|
|
585
|
-
} else {
|
|
586
|
-
const { width, height } = await this.size();
|
|
587
|
-
x = Math.round(width / 2);
|
|
588
|
-
y = Math.round(height / 2);
|
|
589
|
-
}
|
|
590
|
-
if (shouldReplace) {
|
|
591
|
-
await hdc.click(x, y);
|
|
592
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
593
|
-
const length = await this.resolveClearLength(element);
|
|
594
|
-
if (length > 0) await hdc.clearTextField(length);
|
|
595
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
596
|
-
}
|
|
597
|
-
await hdc.inputText(x, y, text);
|
|
598
|
-
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
599
|
-
if (shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
600
|
-
}
|
|
601
|
-
async clearInput(element) {
|
|
602
|
-
const hdc = await this.getHdc();
|
|
603
|
-
if (element) {
|
|
604
|
-
await hdc.click(element.center[0], element.center[1]);
|
|
605
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
606
|
-
}
|
|
607
|
-
const length = await this.resolveClearLength(element);
|
|
608
|
-
if (length > 0) await hdc.clearTextField(length);
|
|
609
|
-
}
|
|
610
|
-
async resolveClearLength(element) {
|
|
611
|
-
const PADDING = 2;
|
|
612
|
-
const FALLBACK_LENGTH = 100;
|
|
613
|
-
try {
|
|
614
|
-
const hdc = await this.getHdc();
|
|
615
|
-
const layoutJson = await hdc.dumpLayout();
|
|
616
|
-
const layout = JSON.parse(layoutJson);
|
|
617
|
-
const fields = collectInputFields(layout);
|
|
618
|
-
if (0 === fields.length) return FALLBACK_LENGTH;
|
|
619
|
-
const target = element ? pickFieldByPoint(fields, element.center) ?? pickLongestField(fields) : pickLongestField(fields);
|
|
620
|
-
return target.text.length + PADDING;
|
|
621
|
-
} catch (e) {
|
|
622
|
-
debugDevice(`resolveClearLength: layout probe failed, falling back to ${FALLBACK_LENGTH}: ${e}`);
|
|
623
|
-
return FALLBACK_LENGTH;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
async pressKey(key) {
|
|
627
|
-
const normalizedKey = keyNameAliasMap[key.toLowerCase()] ?? key;
|
|
628
|
-
const harmonyKey = harmonyKeyCodeMap[normalizedKey] ?? key;
|
|
629
|
-
const hdc = await this.getHdc();
|
|
630
|
-
await hdc.keyEvent(harmonyKey);
|
|
631
|
-
}
|
|
632
|
-
async scroll(deltaX, deltaY, speed) {
|
|
633
|
-
if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
|
|
634
|
-
const { width, height } = await this.size();
|
|
635
|
-
const n = scrollQuadrantDivisions;
|
|
636
|
-
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
637
|
-
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
638
|
-
const maxPositiveDeltaX = startX;
|
|
639
|
-
const maxNegativeDeltaX = width - startX;
|
|
640
|
-
const maxPositiveDeltaY = startY;
|
|
641
|
-
const maxNegativeDeltaY = height - startY;
|
|
642
|
-
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
643
|
-
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
644
|
-
const endX = Math.round(Math.max(screenEdgeMargin, Math.min(width - screenEdgeMargin, startX - deltaX)));
|
|
645
|
-
const endY = Math.round(Math.max(screenEdgeMargin, Math.min(height - screenEdgeMargin, startY - deltaY)));
|
|
646
|
-
const hdc = await this.getHdc();
|
|
647
|
-
await hdc.fling(startX, startY, endX, endY, speed ?? defaultFastSwipeSpeed);
|
|
648
|
-
}
|
|
649
|
-
async scrollInDirection(direction, distance, startPoint) {
|
|
650
|
-
const { width, height } = await this.size();
|
|
651
|
-
const isVertical = 'up' === direction || 'down' === direction;
|
|
652
|
-
const scrollDistance = Math.round(distance ?? (isVertical ? height : width));
|
|
653
|
-
if (startPoint) {
|
|
654
|
-
const hdc = await this.getHdc();
|
|
655
|
-
const sx = Math.round(startPoint.left);
|
|
656
|
-
const sy = Math.round(startPoint.top);
|
|
657
|
-
const endPoints = {
|
|
658
|
-
down: {
|
|
659
|
-
x: sx,
|
|
660
|
-
y: Math.max(screenEdgeMargin, sy - scrollDistance)
|
|
661
|
-
},
|
|
662
|
-
up: {
|
|
663
|
-
x: sx,
|
|
664
|
-
y: Math.min(height - screenEdgeMargin, sy + scrollDistance)
|
|
665
|
-
},
|
|
666
|
-
left: {
|
|
667
|
-
x: Math.min(width - screenEdgeMargin, sx + scrollDistance),
|
|
668
|
-
y: sy
|
|
669
|
-
},
|
|
670
|
-
right: {
|
|
671
|
-
x: Math.max(screenEdgeMargin, sx - scrollDistance),
|
|
672
|
-
y: sy
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
const end = endPoints[direction];
|
|
676
|
-
await hdc.fling(sx, sy, end.x, end.y, defaultFastSwipeSpeed);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const deltas = {
|
|
680
|
-
down: [
|
|
681
|
-
0,
|
|
682
|
-
scrollDistance
|
|
683
|
-
],
|
|
684
|
-
up: [
|
|
685
|
-
0,
|
|
686
|
-
-scrollDistance
|
|
687
|
-
],
|
|
688
|
-
left: [
|
|
689
|
-
-scrollDistance,
|
|
690
|
-
0
|
|
691
|
-
],
|
|
692
|
-
right: [
|
|
693
|
-
scrollDistance,
|
|
694
|
-
0
|
|
695
|
-
]
|
|
696
|
-
};
|
|
697
|
-
const [dx, dy] = deltas[direction];
|
|
698
|
-
await this.scroll(dx, dy);
|
|
699
|
-
}
|
|
700
|
-
async scrollDown(distance, startPoint) {
|
|
701
|
-
await this.scrollInDirection('down', distance, startPoint);
|
|
702
|
-
}
|
|
703
|
-
async scrollUp(distance, startPoint) {
|
|
704
|
-
await this.scrollInDirection('up', distance, startPoint);
|
|
705
|
-
}
|
|
706
|
-
async scrollLeft(distance, startPoint) {
|
|
707
|
-
await this.scrollInDirection('left', distance, startPoint);
|
|
708
|
-
}
|
|
709
|
-
async scrollRight(distance, startPoint) {
|
|
710
|
-
await this.scrollInDirection('right', distance, startPoint);
|
|
711
|
-
}
|
|
712
|
-
async scrollUntilEdge(direction, startPoint) {
|
|
713
|
-
if (startPoint) {
|
|
714
|
-
const { width, height } = await this.size();
|
|
715
|
-
const hdc = await this.getHdc();
|
|
716
|
-
const sx = Math.round(startPoint.left);
|
|
717
|
-
const sy = Math.round(startPoint.top);
|
|
718
|
-
const flingTargets = {
|
|
719
|
-
up: {
|
|
720
|
-
x: sx,
|
|
721
|
-
y: Math.round(height) - screenEdgeMargin
|
|
722
|
-
},
|
|
723
|
-
down: {
|
|
724
|
-
x: sx,
|
|
725
|
-
y: screenEdgeMargin
|
|
726
|
-
},
|
|
727
|
-
left: {
|
|
728
|
-
x: Math.round(width) - screenEdgeMargin,
|
|
729
|
-
y: sy
|
|
730
|
-
},
|
|
731
|
-
right: {
|
|
732
|
-
x: screenEdgeMargin,
|
|
733
|
-
y: sy
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
const target = flingTargets[direction];
|
|
737
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>hdc.fling(sx, sy, target.x, target.y, defaultFastSwipeSpeed));
|
|
738
|
-
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const deltas = {
|
|
742
|
-
up: [
|
|
743
|
-
0,
|
|
744
|
-
-maxScrollDistance
|
|
745
|
-
],
|
|
746
|
-
down: [
|
|
747
|
-
0,
|
|
748
|
-
maxScrollDistance
|
|
749
|
-
],
|
|
750
|
-
left: [
|
|
751
|
-
-maxScrollDistance,
|
|
752
|
-
0
|
|
753
|
-
],
|
|
754
|
-
right: [
|
|
755
|
-
maxScrollDistance,
|
|
756
|
-
0
|
|
757
|
-
]
|
|
758
|
-
};
|
|
759
|
-
const [dx, dy] = deltas[direction];
|
|
760
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(dx, dy, defaultFastSwipeSpeed));
|
|
761
|
-
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
762
|
-
}
|
|
763
|
-
async scrollUntilTop(startPoint) {
|
|
764
|
-
await this.scrollUntilEdge('up', startPoint);
|
|
765
|
-
}
|
|
766
|
-
async scrollUntilBottom(startPoint) {
|
|
767
|
-
await this.scrollUntilEdge('down', startPoint);
|
|
768
|
-
}
|
|
769
|
-
async scrollUntilLeft(startPoint) {
|
|
770
|
-
await this.scrollUntilEdge('left', startPoint);
|
|
771
|
-
}
|
|
772
|
-
async scrollUntilRight(startPoint) {
|
|
773
|
-
await this.scrollUntilEdge('right', startPoint);
|
|
774
|
-
}
|
|
775
|
-
async back() {
|
|
776
|
-
const hdc = await this.getHdc();
|
|
777
|
-
await hdc.keyEvent('Back');
|
|
778
|
-
}
|
|
779
|
-
async home() {
|
|
780
|
-
const hdc = await this.getHdc();
|
|
781
|
-
await hdc.keyEvent('Home');
|
|
782
|
-
}
|
|
783
|
-
async recentApps() {
|
|
784
|
-
const hdc = await this.getHdc();
|
|
785
|
-
await hdc.keyEvent('RecentApps');
|
|
786
|
-
}
|
|
787
|
-
async hideKeyboard(options) {
|
|
788
|
-
const hdc = await this.getHdc();
|
|
789
|
-
const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
|
|
790
|
-
const key = 'back-first' === keyboardDismissStrategy ? 'Back' : harmonyKeyCodeMap.Escape;
|
|
791
|
-
await hdc.keyEvent(key);
|
|
792
|
-
}
|
|
793
|
-
async getDeviceLocalTimeString(format = 'YYYY-MM-DD HH:mm:ss') {
|
|
794
|
-
const hdc = await this.getHdc();
|
|
795
|
-
try {
|
|
796
|
-
const stdout = await hdc.shell('date +%Y-%m-%dT%H:%M:%S');
|
|
797
|
-
const match = stdout.trim().match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/);
|
|
798
|
-
if (!match) throw new Error(`Invalid device time format: ${stdout}`);
|
|
799
|
-
const [, year, month, day, hours, minutes, seconds] = match;
|
|
800
|
-
const timeString = format.replace('YYYY', year).replace('MM', month).replace('DD', day).replace('HH', hours).replace('mm', minutes).replace('ss', seconds);
|
|
801
|
-
debugDevice(`Got device local time: ${timeString}`);
|
|
802
|
-
return `${timeString} (${format})`;
|
|
803
|
-
} catch (error) {
|
|
804
|
-
debugDevice(`Failed to get device local time: ${error}`);
|
|
805
|
-
throw new Error(`Failed to get device local time: ${error}`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
async destroy() {
|
|
809
|
-
if (this.destroyed) return;
|
|
810
|
-
this.destroyed = true;
|
|
811
|
-
this.cachedScreenSize = null;
|
|
812
|
-
this.hdc = null;
|
|
813
|
-
this.connecting = null;
|
|
814
|
-
}
|
|
815
|
-
constructor(deviceId, options){
|
|
816
|
-
device_define_property(this, "deviceId", void 0);
|
|
817
|
-
device_define_property(this, "hdc", null);
|
|
818
|
-
device_define_property(this, "connecting", null);
|
|
819
|
-
device_define_property(this, "destroyed", false);
|
|
820
|
-
device_define_property(this, "descriptionText", void 0);
|
|
821
|
-
device_define_property(this, "customActions", void 0);
|
|
822
|
-
device_define_property(this, "cachedScreenSize", null);
|
|
823
|
-
device_define_property(this, "appNameMapping", {});
|
|
824
|
-
device_define_property(this, "lastTapPosition", null);
|
|
825
|
-
device_define_property(this, "interfaceType", 'harmony');
|
|
826
|
-
device_define_property(this, "uri", void 0);
|
|
827
|
-
device_define_property(this, "options", void 0);
|
|
828
|
-
device_define_property(this, "inputPrimitives", {
|
|
829
|
-
pointer: {
|
|
830
|
-
tap: (point)=>this.tapPoint(point),
|
|
831
|
-
doubleClick: (point)=>this.doubleTapPoint(point),
|
|
832
|
-
longPress: (point)=>this.longPressPoint(point),
|
|
833
|
-
dragAndDrop: async (from, to)=>{
|
|
834
|
-
const hdc = await this.getHdc();
|
|
835
|
-
await hdc.drag(from.x, from.y, to.x, to.y);
|
|
836
|
-
}
|
|
837
|
-
},
|
|
838
|
-
keyboard: {
|
|
839
|
-
keyboardPress: (keyName)=>this.pressKey(keyName),
|
|
840
|
-
typeText: (value, opts)=>{
|
|
841
|
-
const harmonyOpts = opts;
|
|
842
|
-
return harmonyOpts?.focusOnly ? Promise.resolve() : this.typeText(value, harmonyOpts?.target, harmonyOpts?.replace ?? true, {
|
|
843
|
-
autoDismissKeyboard: harmonyOpts?.autoDismissKeyboard,
|
|
844
|
-
keyboardDismissStrategy: harmonyOpts?.keyboardDismissStrategy
|
|
845
|
-
});
|
|
846
|
-
},
|
|
847
|
-
clearInput: (target)=>this.clearInput(target),
|
|
848
|
-
cursorMove: async (direction, times = 1)=>{
|
|
849
|
-
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
850
|
-
for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
touch: {
|
|
854
|
-
swipe: async (start, end, opts)=>{
|
|
855
|
-
const duration = opts?.duration;
|
|
856
|
-
const repeatCount = opts?.repeat ?? 1;
|
|
857
|
-
const hdc = await this.getHdc();
|
|
858
|
-
for(let i = 0; i < repeatCount; i++)await hdc.swipe(start.x, start.y, end.x, end.y, duration ? Math.round(duration) : void 0);
|
|
859
|
-
}
|
|
860
|
-
},
|
|
861
|
-
scroll: {
|
|
862
|
-
scroll: (param)=>this.performActionScroll(param)
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
device_define_property(this, "remoteScreenshotPath", '/data/local/tmp/ms_screen.jpeg');
|
|
866
|
-
device_define_property(this, "localScreenshotPath", null);
|
|
867
|
-
external_node_assert_default()(deviceId, 'deviceId is required for HarmonyDevice');
|
|
868
|
-
this.deviceId = deviceId;
|
|
869
|
-
this.options = options;
|
|
870
|
-
this.customActions = options?.customActions;
|
|
871
|
-
if (options?.screenshotResizeScale !== void 0 && !screenshotResizeScaleWarned) {
|
|
872
|
-
screenshotResizeScaleWarned = true;
|
|
873
|
-
console.warn('[midscene] screenshotResizeScale is deprecated. Use screenshotShrinkFactor in AgentOpt instead.');
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
const runHdcShellParamSchema = core_namespaceObject.z.object({
|
|
878
|
-
command: core_namespaceObject.z.string().describe('HDC shell command to execute')
|
|
879
|
-
});
|
|
880
|
-
const launchParamSchema = core_namespaceObject.z.object({
|
|
881
|
-
uri: core_namespaceObject.z.string().describe('App name, bundle name, or URL to launch. Prioritize using the exact bundle name or URL the user has provided. If none provided, use the accurate app name.')
|
|
882
|
-
});
|
|
883
|
-
const terminateParamSchema = core_namespaceObject.z.object({
|
|
884
|
-
uri: core_namespaceObject.z.string().describe('Bundle name or app name to terminate. Prioritize using the exact bundle name the user provided. If the bundle is unknown, use the accurate app name shown on screen, such as Settings or Music.')
|
|
885
|
-
});
|
|
886
|
-
const createPlatformActions = (device)=>({
|
|
887
|
-
RunHdcShell: (0, device_namespaceObject.defineAction)({
|
|
888
|
-
name: 'RunHdcShell',
|
|
889
|
-
description: 'Execute HDC shell command on HarmonyOS device',
|
|
890
|
-
interfaceAlias: 'runHdcShell',
|
|
891
|
-
paramSchema: runHdcShellParamSchema,
|
|
892
|
-
sample: {
|
|
893
|
-
command: 'hidumper -s WindowManagerService -a'
|
|
894
|
-
},
|
|
895
|
-
call: async (param)=>{
|
|
896
|
-
if (!param.command || '' === param.command.trim()) throw new Error('RunHdcShell requires a non-empty command parameter');
|
|
897
|
-
const hdc = await device.getHdc();
|
|
898
|
-
return await hdc.shell(param.command);
|
|
899
|
-
}
|
|
900
|
-
}),
|
|
901
|
-
Launch: (0, device_namespaceObject.defineAction)({
|
|
902
|
-
name: 'Launch',
|
|
903
|
-
description: 'Launch a HarmonyOS app or URL',
|
|
904
|
-
interfaceAlias: 'launch',
|
|
905
|
-
paramSchema: launchParamSchema,
|
|
906
|
-
sample: {
|
|
907
|
-
uri: 'com.example.app'
|
|
908
|
-
},
|
|
909
|
-
call: async (param)=>{
|
|
910
|
-
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
911
|
-
await device.launch(param.uri);
|
|
912
|
-
}
|
|
913
|
-
}),
|
|
914
|
-
Terminate: (0, device_namespaceObject.defineAction)({
|
|
915
|
-
name: 'Terminate',
|
|
916
|
-
description: 'Terminate (force-stop) a HarmonyOS app by bundle name or mapped app name',
|
|
917
|
-
interfaceAlias: 'terminate',
|
|
918
|
-
paramSchema: terminateParamSchema,
|
|
919
|
-
call: async (param)=>{
|
|
920
|
-
if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
|
|
921
|
-
await device.terminate(param.uri);
|
|
922
|
-
}
|
|
923
|
-
}),
|
|
924
|
-
HarmonyBackButton: (0, device_namespaceObject.defineAction)({
|
|
925
|
-
name: 'HarmonyBackButton',
|
|
926
|
-
description: 'Trigger the system "back" operation on HarmonyOS devices',
|
|
927
|
-
call: async ()=>{
|
|
928
|
-
await device.back();
|
|
929
|
-
}
|
|
930
|
-
}),
|
|
931
|
-
HarmonyHomeButton: (0, device_namespaceObject.defineAction)({
|
|
932
|
-
name: 'HarmonyHomeButton',
|
|
933
|
-
description: 'Trigger the system "home" operation on HarmonyOS devices',
|
|
934
|
-
call: async ()=>{
|
|
935
|
-
await device.home();
|
|
936
|
-
}
|
|
937
|
-
}),
|
|
938
|
-
HarmonyRecentAppsButton: (0, device_namespaceObject.defineAction)({
|
|
939
|
-
name: 'HarmonyRecentAppsButton',
|
|
940
|
-
description: 'Trigger the system "recent apps" operation on HarmonyOS devices',
|
|
941
|
-
call: async ()=>{
|
|
942
|
-
await device.recentApps();
|
|
943
|
-
}
|
|
944
|
-
})
|
|
945
|
-
});
|
|
946
|
-
const debugUtils = (0, logger_namespaceObject.getDebug)('harmony:utils');
|
|
947
|
-
async function getConnectedDevices(hdcPath, options = {}) {
|
|
948
|
-
try {
|
|
949
|
-
const hdc = new HdcClient({
|
|
950
|
-
hdcPath,
|
|
951
|
-
timeout: options.timeout
|
|
952
|
-
});
|
|
953
|
-
const targets = await hdc.listTargets();
|
|
954
|
-
const devices = targets.map((deviceId)=>({
|
|
955
|
-
deviceId
|
|
956
|
-
}));
|
|
957
|
-
debugUtils(`Found ${devices.length} connected devices: `, devices);
|
|
958
|
-
return devices;
|
|
959
|
-
} catch (error) {
|
|
960
|
-
console.error('Failed to get device list:', error);
|
|
961
|
-
throw new Error(`Unable to get connected HarmonyOS device list, please ensure HDC is properly configured: ${error.message}`, {
|
|
962
|
-
cause: error
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
function agent_define_property(obj, key, value) {
|
|
967
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
968
|
-
value: value,
|
|
969
|
-
enumerable: true,
|
|
970
|
-
configurable: true,
|
|
971
|
-
writable: true
|
|
972
|
-
});
|
|
973
|
-
else obj[key] = value;
|
|
974
|
-
return obj;
|
|
975
|
-
}
|
|
976
|
-
const debugAgent = (0, logger_namespaceObject.getDebug)('harmony:agent');
|
|
977
|
-
class HarmonyAgent extends agent_namespaceObject.Agent {
|
|
978
|
-
async launch(uri) {
|
|
979
|
-
const action = this.wrapActionInActionSpace('Launch');
|
|
980
|
-
return action({
|
|
981
|
-
uri
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
async terminate(uri) {
|
|
985
|
-
const action = this.wrapActionInActionSpace('Terminate');
|
|
986
|
-
return action({
|
|
987
|
-
uri
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
async runHdcShell(command) {
|
|
991
|
-
const action = this.wrapActionInActionSpace('RunHdcShell');
|
|
992
|
-
return action({
|
|
993
|
-
command
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
createActionWrapper(name) {
|
|
997
|
-
const action = this.wrapActionInActionSpace(name);
|
|
998
|
-
return (...args)=>action(args[0]);
|
|
999
|
-
}
|
|
1000
|
-
constructor(device, opts){
|
|
1001
|
-
super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0);
|
|
1002
|
-
this.appNameMapping = (0, utils_namespaceObject.mergeAndNormalizeAppNameMapping)(defaultAppNameMapping, opts?.appNameMapping);
|
|
1003
|
-
device.setAppNameMapping(this.appNameMapping);
|
|
1004
|
-
this.back = this.createActionWrapper('HarmonyBackButton');
|
|
1005
|
-
this.home = this.createActionWrapper('HarmonyHomeButton');
|
|
1006
|
-
this.recentApps = this.createActionWrapper('HarmonyRecentAppsButton');
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
async function agentFromHdcDevice(deviceId, opts) {
|
|
1010
|
-
if (!deviceId) {
|
|
1011
|
-
const devices = await getConnectedDevices(opts?.hdcPath);
|
|
1012
|
-
if (0 === devices.length) throw new Error('No HarmonyOS devices found. Please connect a HarmonyOS device and ensure HDC is properly configured. Run `hdc list targets` to verify device connection.');
|
|
1013
|
-
deviceId = devices[0].deviceId;
|
|
1014
|
-
debugAgent('deviceId not specified, will use the first device (id = %s)', deviceId);
|
|
1015
|
-
}
|
|
1016
|
-
const device = new HarmonyDevice(deviceId, opts || {});
|
|
1017
|
-
await device.connect();
|
|
1018
|
-
return new HarmonyAgent(device, opts);
|
|
1019
|
-
}
|
|
1020
|
-
function mcp_tools_define_property(obj, key, value) {
|
|
1021
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
1022
|
-
value: value,
|
|
1023
|
-
enumerable: true,
|
|
1024
|
-
configurable: true,
|
|
1025
|
-
writable: true
|
|
1026
|
-
});
|
|
1027
|
-
else obj[key] = value;
|
|
1028
|
-
return obj;
|
|
1029
|
-
}
|
|
1030
|
-
const debug = (0, logger_namespaceObject.getDebug)('mcp:harmony-tools');
|
|
1031
|
-
function adaptHarmonyInitArgs(extracted) {
|
|
1032
|
-
if (!extracted) return;
|
|
1033
|
-
const initArgs = {
|
|
1034
|
-
...'string' == typeof extracted.deviceId ? {
|
|
1035
|
-
deviceId: extracted.deviceId
|
|
1036
|
-
} : {},
|
|
1037
|
-
...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
|
|
1038
|
-
};
|
|
1039
|
-
return Object.keys(initArgs).length > 0 ? initArgs : void 0;
|
|
1040
|
-
}
|
|
1041
|
-
class HarmonyMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools {
|
|
1042
|
-
getCliReportSessionName() {
|
|
1043
|
-
return 'midscene-harmony';
|
|
1044
|
-
}
|
|
1045
|
-
createTemporaryDevice() {
|
|
1046
|
-
return new HarmonyDevice('temp-for-action-space', {});
|
|
1047
|
-
}
|
|
1048
|
-
async ensureAgent(initArgs) {
|
|
1049
|
-
const deviceId = initArgs?.deviceId;
|
|
1050
|
-
const nextSignature = (0, agent_behavior_init_args_namespaceObject.getAgentInitArgsSignature)(initArgs);
|
|
1051
|
-
if (this.agent && (0, agent_behavior_init_args_namespaceObject.shouldRebuildAgentForInitArgs)(this.lastInitArgsSignature, nextSignature)) {
|
|
1052
|
-
try {
|
|
1053
|
-
await this.agent.destroy?.();
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
debug('Failed to destroy agent during cleanup:', error);
|
|
1056
|
-
}
|
|
1057
|
-
this.agent = void 0;
|
|
1058
|
-
}
|
|
1059
|
-
if (this.agent) return this.agent;
|
|
1060
|
-
debug('Creating Harmony agent with deviceId:', deviceId || 'auto-detect');
|
|
1061
|
-
const reportOptions = this.readCliReportAgentOptions();
|
|
1062
|
-
const agent = await agentFromHdcDevice(deviceId, {
|
|
1063
|
-
autoDismissKeyboard: false,
|
|
1064
|
-
...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(initArgs) ?? {},
|
|
1065
|
-
...reportOptions ?? {}
|
|
1066
|
-
});
|
|
1067
|
-
this.agent = agent;
|
|
1068
|
-
this.lastInitArgsSignature = nextSignature;
|
|
1069
|
-
return agent;
|
|
1070
|
-
}
|
|
1071
|
-
preparePlatformTools() {
|
|
1072
|
-
return [
|
|
1073
|
-
{
|
|
1074
|
-
name: 'harmony_connect',
|
|
1075
|
-
description: 'Connect to HarmonyOS device via HDC. If deviceId not provided, uses the first available device.',
|
|
1076
|
-
schema: this.getAgentInitArgSchema(),
|
|
1077
|
-
cli: this.getAgentInitArgCliMetadata(),
|
|
1078
|
-
handler: async (args)=>{
|
|
1079
|
-
const initArgs = this.extractAgentInitParam(args);
|
|
1080
|
-
const deviceId = initArgs?.deviceId;
|
|
1081
|
-
const reportSession = this.createNewCliReportSession(deviceId ?? 'auto');
|
|
1082
|
-
this.commitCliReportSession(reportSession);
|
|
1083
|
-
if (this.agent) {
|
|
1084
|
-
try {
|
|
1085
|
-
await this.agent.destroy?.();
|
|
1086
|
-
} catch (error) {
|
|
1087
|
-
debug('Failed to destroy agent during connect:', error);
|
|
1088
|
-
}
|
|
1089
|
-
this.agent = void 0;
|
|
1090
|
-
this.lastInitArgsSignature = void 0;
|
|
1091
|
-
}
|
|
1092
|
-
const agent = await this.ensureAgent(initArgs);
|
|
1093
|
-
const screenshot = await agent.page.screenshotBase64();
|
|
1094
|
-
return {
|
|
1095
|
-
content: [
|
|
1096
|
-
{
|
|
1097
|
-
type: 'text',
|
|
1098
|
-
text: `Connected to HarmonyOS device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}`
|
|
1099
|
-
},
|
|
1100
|
-
...this.buildScreenshotContent(screenshot)
|
|
1101
|
-
],
|
|
1102
|
-
isError: false
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
name: 'harmony_disconnect',
|
|
1108
|
-
description: 'Disconnect from current HarmonyOS device and release HDC resources',
|
|
1109
|
-
schema: {},
|
|
1110
|
-
handler: this.createDisconnectHandler('HarmonyOS device')
|
|
1111
|
-
}
|
|
1112
|
-
];
|
|
1113
|
-
}
|
|
1114
|
-
constructor(...args){
|
|
1115
|
-
super(...args), mcp_tools_define_property(this, "lastInitArgsSignature", void 0), mcp_tools_define_property(this, "initArgSpec", {
|
|
1116
|
-
namespace: 'harmony',
|
|
1117
|
-
shape: {
|
|
1118
|
-
deviceId: core_namespaceObject.z.string().optional().describe('HarmonyOS device ID (from hdc list targets)'),
|
|
1119
|
-
...agent_behavior_init_args_namespaceObject.agentBehaviorInitArgShape
|
|
1120
|
-
},
|
|
1121
|
-
cli: {
|
|
1122
|
-
preferBareKeys: true
|
|
1123
|
-
},
|
|
1124
|
-
adapt: adaptHarmonyInitArgs
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
class HarmonyMCPServer extends mcp_namespaceObject.BaseMCPServer {
|
|
1129
|
-
createToolsManager() {
|
|
1130
|
-
return new HarmonyMidsceneTools();
|
|
1131
|
-
}
|
|
1132
|
-
constructor(toolsManager){
|
|
1133
|
-
super({
|
|
1134
|
-
name: '@midscene/harmony-mcp',
|
|
1135
|
-
version: "1.9.8",
|
|
1136
|
-
description: 'Control the HarmonyOS device using natural language commands'
|
|
1137
|
-
}, toolsManager);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function mcpServerForAgent(agent) {
|
|
1141
|
-
return (0, mcp_namespaceObject.createMCPServerLauncher)({
|
|
1142
|
-
agent,
|
|
1143
|
-
platformName: 'HarmonyOS',
|
|
1144
|
-
ToolsManagerClass: HarmonyMidsceneTools,
|
|
1145
|
-
MCPServerClass: HarmonyMCPServer
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
async function mcpKitForAgent(agent) {
|
|
1149
|
-
const toolsManager = new HarmonyMidsceneTools();
|
|
1150
|
-
toolsManager.setAgent(agent);
|
|
1151
|
-
await toolsManager.initTools();
|
|
1152
|
-
return {
|
|
1153
|
-
description: 'Midscene MCP Kit for HarmonyOS automation',
|
|
1154
|
-
tools: toolsManager.getToolDefinitions()
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
exports.HarmonyMCPServer = __webpack_exports__.HarmonyMCPServer;
|
|
1158
|
-
exports.mcpKitForAgent = __webpack_exports__.mcpKitForAgent;
|
|
1159
|
-
exports.mcpServerForAgent = __webpack_exports__.mcpServerForAgent;
|
|
1160
|
-
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
1161
|
-
"HarmonyMCPServer",
|
|
1162
|
-
"mcpKitForAgent",
|
|
1163
|
-
"mcpServerForAgent"
|
|
1164
|
-
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
1165
|
-
Object.defineProperty(exports, '__esModule', {
|
|
1166
|
-
value: true
|
|
1167
|
-
});
|