preflite 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +144 -0
  3. package/dist/adapter-spi/artifact/index.js +1 -0
  4. package/dist/adapter-spi/command/index.js +1 -0
  5. package/dist/adapter-spi/install/index.js +1 -0
  6. package/dist/adapter-spi/lifecycle/index.js +1 -0
  7. package/dist/adapter-spi/resource/index.js +1 -0
  8. package/dist/adapter-spi/snapshot/index.js +1 -0
  9. package/dist/application/agent/AgentCommandPollLoop.js +48 -0
  10. package/dist/application/agent/AgentRuntimeService.js +27 -0
  11. package/dist/application/app-package/AppPackageApplicationService.js +97 -0
  12. package/dist/application/artifact/ArtifactApplicationService.js +13 -0
  13. package/dist/application/debug/DebugApplicationService.js +117 -0
  14. package/dist/application/health/HealthMetricsService.js +47 -0
  15. package/dist/application/lease/LeaseApplicationService.js +79 -0
  16. package/dist/application/query/ObservationQueryService.js +48 -0
  17. package/dist/application/reporter/ReporterApplicationService.js +41 -0
  18. package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
  19. package/dist/application/resource/ResourceRegistryService.js +113 -0
  20. package/dist/application/session/SessionApplicationService.js +39 -0
  21. package/dist/application/task/TaskApplicationService.js +378 -0
  22. package/dist/client/agentAppPackageClient.js +91 -0
  23. package/dist/domain/agent/AgentNode.js +12 -0
  24. package/dist/domain/agent/AgentRuntime.js +6 -0
  25. package/dist/domain/artifact/ArtifactPipeline.js +6 -0
  26. package/dist/domain/artifact/ArtifactRef.js +12 -0
  27. package/dist/domain/event/AgentEvent.js +10 -0
  28. package/dist/domain/event/Reporter.js +6 -0
  29. package/dist/domain/health/HealthMetrics.js +8 -0
  30. package/dist/domain/lease/Lease.js +28 -0
  31. package/dist/domain/lease/LeaseManager.js +6 -0
  32. package/dist/domain/repositories/index.js +1 -0
  33. package/dist/domain/resource/DeviceDetails.js +23 -0
  34. package/dist/domain/resource/DeviceResource.js +16 -0
  35. package/dist/domain/resource/ResourceRegistry.js +6 -0
  36. package/dist/domain/runtime/interfaces.js +1 -0
  37. package/dist/domain/session/BaseSession.js +16 -0
  38. package/dist/domain/session/DebugSession.js +3 -0
  39. package/dist/domain/session/ExecutionSession.js +3 -0
  40. package/dist/domain/session/SessionManager.js +8 -0
  41. package/dist/domain/task/TaskRecord.js +14 -0
  42. package/dist/domain/task/TaskSpec.js +12 -0
  43. package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
  44. package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
  45. package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
  46. package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
  47. package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
  48. package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
  49. package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
  50. package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
  51. package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
  52. package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
  53. package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
  54. package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
  55. package/dist/infrastructure/config/agentConfigFile.js +96 -0
  56. package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
  57. package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
  58. package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
  59. package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
  60. package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
  61. package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
  62. package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
  63. package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
  64. package/dist/infrastructure/midscene/videoRecorder.js +365 -0
  65. package/dist/infrastructure/midscene/zipReportDir.js +36 -0
  66. package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
  67. package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
  68. package/dist/infrastructure/system/CommandRunner.js +128 -0
  69. package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
  70. package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
  71. package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
  72. package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
  73. package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
  74. package/dist/infrastructure/transport/midscenePaths.js +28 -0
  75. package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
  76. package/dist/infrastructure/transport/ws/WsClient.js +182 -0
  77. package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
  78. package/dist/interfaces/http/HttpServer.js +227 -0
  79. package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
  80. package/dist/main.js +368 -0
  81. package/dist/mcp/agentHttpClient.js +82 -0
  82. package/dist/mcp/agentRuntime.js +184 -0
  83. package/dist/mcp/cli.js +36 -0
  84. package/dist/mcp/doctor.js +124 -0
  85. package/dist/mcp/evidence.js +57 -0
  86. package/dist/mcp/exploration/index.js +129 -0
  87. package/dist/mcp/exploration/sessionManager.js +122 -0
  88. package/dist/mcp/exploration/tools-atomic.js +34 -0
  89. package/dist/mcp/exploration/tools-intelligent.js +33 -0
  90. package/dist/mcp/exploration/tools-session.js +276 -0
  91. package/dist/mcp/exploration/types.js +1 -0
  92. package/dist/mcp/flowStepEvents.js +114 -0
  93. package/dist/mcp/liveViewer.js +156 -0
  94. package/dist/mcp/reportReader.js +157 -0
  95. package/dist/mcp/runManager.js +161 -0
  96. package/dist/mcp/runSummary.js +72 -0
  97. package/dist/mcp/runtimeInstall.js +44 -0
  98. package/dist/mcp/server.js +260 -0
  99. package/dist/mcp/setup.js +185 -0
  100. package/dist/mcp/types.js +1 -0
  101. package/dist/mcp/userConfig.js +45 -0
  102. package/dist/mcp/visual-flow/codegen.js +576 -0
  103. package/dist/mcp/visual-flow/index.js +14 -0
  104. package/dist/mcp/visual-flow/types.js +5 -0
  105. package/dist/mcp/visual-flow/validate.js +617 -0
  106. package/dist/protocol-contracts/commands/envelope.js +24 -0
  107. package/dist/protocol-contracts/commands/index.js +1 -0
  108. package/dist/protocol-contracts/dto/index.js +1 -0
  109. package/dist/protocol-contracts/events/index.js +1 -0
  110. package/dist/protocol-contracts/queries/index.js +1 -0
  111. package/dist/shared-kernel/enums/index.js +70 -0
  112. package/dist/shared-kernel/errors/index.js +21 -0
  113. package/dist/shared-kernel/ids/index.js +18 -0
  114. package/dist/shared-kernel/time/index.js +3 -0
  115. package/dist/shared-kernel/value-objects/index.js +1 -0
  116. package/dist/utils/appPackageLocalPath.js +75 -0
  117. package/dist/utils/deviceResourceRouting.js +24 -0
  118. package/dist/utils/harmonyAgentDebugDevice.js +60 -0
  119. package/dist/utils/harmonyHdcDeviceId.js +134 -0
  120. package/dist/utils/iosAgentDebugDevice.js +72 -0
  121. package/dist/utils/iosMjpegCapture.js +90 -0
  122. package/dist/utils/liveDebugForegroundParse.js +71 -0
  123. package/dist/utils/midscene-device-session.js +353 -0
  124. package/dist/utils/midscene-task-cache-env.js +15 -0
  125. package/dist/utils/midsceneReportConstants.js +49 -0
  126. package/dist/utils/seedMidsceneTaskCache.js +61 -0
  127. package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
  128. package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
  129. package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
  130. package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
  131. package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
  132. package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
  133. package/dist/utils/task-runners/taskAppPackage.js +20 -0
  134. package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
  135. package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
  136. package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
  137. package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
  138. package/package.json +46 -0
@@ -0,0 +1,533 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { AndroidDevice } from "@midscene/android";
4
+ import { IOSDevice } from "@midscene/ios";
5
+ import { connectHarmonyAgentDebugDevice, extractHarmonyDeviceIdFromResourceId } from "../../utils/harmonyAgentDebugDevice.js";
6
+ import { extractIosUdidFromResourceId, resolveIosWdaPortForLiveDebug } from "../../utils/iosAgentDebugDevice.js";
7
+ import { captureFirstJpegFromWdaMjpegStream } from "../../utils/iosMjpegCapture.js";
8
+ import { parseAndroidForegroundFromDumpsys, parseHarmonyForegroundFromShellDump, } from "../../utils/liveDebugForegroundParse.js";
9
+ export class DebugRuntimeImpl {
10
+ commandExecutor;
11
+ snapshotProvider;
12
+ commandRunner;
13
+ options;
14
+ androidDevices = new Map();
15
+ androidDeviceConnecting = new Map();
16
+ harmonyDevices = new Map();
17
+ harmonyDeviceConnecting = new Map();
18
+ iosDevices = new Map();
19
+ iosDeviceConnecting = new Map();
20
+ constructor(commandExecutor, snapshotProvider, commandRunner, options = {}) {
21
+ this.commandExecutor = commandExecutor;
22
+ this.snapshotProvider = snapshotProvider;
23
+ this.commandRunner = commandRunner;
24
+ this.options = options;
25
+ }
26
+ async runCommand(resourceId, command) {
27
+ return this.commandExecutor.execute(resourceId, command);
28
+ }
29
+ async snapshot(resourceId) {
30
+ return this.snapshotProvider.snapshot(resourceId);
31
+ }
32
+ async captureLiveFrame(resourceId) {
33
+ if (this.isAndroidResource(resourceId)) {
34
+ return this.captureAndroidFrame(resourceId);
35
+ }
36
+ if (this.isHarmonyResource(resourceId)) {
37
+ return this.captureHarmonyFrame(resourceId);
38
+ }
39
+ if (this.isIosResource(resourceId)) {
40
+ return this.captureIosFrame(resourceId);
41
+ }
42
+ const shot = await this.snapshotProvider.snapshot(resourceId);
43
+ const sourceUri = shot.uri.trim();
44
+ if (!sourceUri) {
45
+ throw new Error("snapshot uri is empty");
46
+ }
47
+ if (sourceUri.startsWith("data:")) {
48
+ const match = sourceUri.match(/^data:([^;]+);base64,(.+)$/);
49
+ if (!match) {
50
+ throw new Error("unsupported data uri format");
51
+ }
52
+ return {
53
+ mimeType: match[1],
54
+ dataBase64: match[2],
55
+ sourceUri,
56
+ };
57
+ }
58
+ if (!sourceUri.startsWith("file://")) {
59
+ throw new Error(`unsupported snapshot uri for live frame: ${sourceUri}`);
60
+ }
61
+ const filePath = fileURLToPath(sourceUri);
62
+ const data = await readFile(filePath);
63
+ return {
64
+ mimeType: "image/png",
65
+ dataBase64: data.toString("base64"),
66
+ sourceUri,
67
+ };
68
+ }
69
+ async sendLiveInput(resourceId, input) {
70
+ if (this.isAndroidResource(resourceId)) {
71
+ return this.sendAndroidInput(resourceId, input);
72
+ }
73
+ if (this.isHarmonyResource(resourceId)) {
74
+ return this.sendHarmonyInput(resourceId, input);
75
+ }
76
+ if (this.isIosResource(resourceId)) {
77
+ return this.sendIosInput(resourceId, input);
78
+ }
79
+ const command = this.renderLiveInputCommand(input);
80
+ return this.commandExecutor.execute(resourceId, command);
81
+ }
82
+ isAndroidResource(resourceId) {
83
+ return String(resourceId).toLowerCase().startsWith("android:");
84
+ }
85
+ isHarmonyResource(resourceId) {
86
+ return String(resourceId).toLowerCase().startsWith("harmony:");
87
+ }
88
+ isIosResource(resourceId) {
89
+ return String(resourceId).toLowerCase().startsWith("ios:");
90
+ }
91
+ getIosWdaHost() {
92
+ const fromOptions = this.options.iosWdaHost?.trim();
93
+ if (fromOptions)
94
+ return fromOptions;
95
+ return process.env.MIDSCENE_IOS_WDA_HOST?.trim() || process.env.IOS_WDA_HOST?.trim() || "127.0.0.1";
96
+ }
97
+ buildIosWdaResolveParams(resourceId) {
98
+ const wdaHost = this.getIosWdaHost();
99
+ return {
100
+ resourceId,
101
+ wdaHost,
102
+ portMapFilePath: this.options.iosWdaPortMapFilePath,
103
+ portRangeStart: Number.isFinite(this.options.iosWdaPortRangeStart) && (this.options.iosWdaPortRangeStart ?? 0) > 0
104
+ ? Math.floor(this.options.iosWdaPortRangeStart)
105
+ : 8200,
106
+ portRangeEnd: Number.isFinite(this.options.iosWdaPortRangeEnd) && (this.options.iosWdaPortRangeEnd ?? 0) > 0
107
+ ? Math.floor(this.options.iosWdaPortRangeEnd)
108
+ : 8399,
109
+ explicitWdaPort: this.options.iosWdaExplicitPort,
110
+ };
111
+ }
112
+ async resolveIosWdaCommandPortForResource(resourceId) {
113
+ return resolveIosWdaPortForLiveDebug(this.commandRunner, this.buildIosWdaResolveParams(String(resourceId)));
114
+ }
115
+ /** 显式环境覆盖;否则与 start-ios-wda:本机 MJPEG = WDA 命令端口 + 1000 */
116
+ getIosWdaMjpegPort(wdaCommandPort) {
117
+ const fromOptions = this.options.iosWdaMjpegPort;
118
+ if (Number.isFinite(fromOptions) && (fromOptions ?? 0) > 0) {
119
+ return Math.floor(fromOptions);
120
+ }
121
+ const envA = process.env.MIDSCENE_IOS_WDA_MJPEG_PORT?.trim();
122
+ const envB = process.env.IOS_WDA_MJPEG_PORT?.trim();
123
+ for (const raw of [envA, envB]) {
124
+ if (!raw)
125
+ continue;
126
+ const n = Number(raw);
127
+ if (Number.isFinite(n) && n > 0)
128
+ return Math.floor(n);
129
+ }
130
+ return wdaCommandPort + 1000;
131
+ }
132
+ async getIosDevice(resourceId) {
133
+ const key = String(resourceId);
134
+ const cached = this.iosDevices.get(key);
135
+ if (cached)
136
+ return cached;
137
+ const pending = this.iosDeviceConnecting.get(key);
138
+ if (pending)
139
+ return pending;
140
+ const creating = (async () => {
141
+ const wdaHost = this.getIosWdaHost();
142
+ const wdaPort = await resolveIosWdaPortForLiveDebug(this.commandRunner, this.buildIosWdaResolveParams(key));
143
+ const mjpegPort = this.getIosWdaMjpegPort(wdaPort);
144
+ const udid = extractIosUdidFromResourceId(key);
145
+ const device = new IOSDevice({
146
+ wdaHost,
147
+ wdaPort,
148
+ wdaMjpegPort: mjpegPort,
149
+ ...(udid ? { deviceId: udid } : {}),
150
+ });
151
+ await device.connect();
152
+ this.iosDevices.set(key, device);
153
+ return device;
154
+ })();
155
+ this.iosDeviceConnecting.set(key, creating);
156
+ try {
157
+ return await creating;
158
+ }
159
+ finally {
160
+ this.iosDeviceConnecting.delete(key);
161
+ }
162
+ }
163
+ normalizeWdaActiveAppValue(value) {
164
+ if (!value || typeof value !== "object")
165
+ return undefined;
166
+ const o = value;
167
+ const out = {};
168
+ if (typeof o.bundleId === "string" && o.bundleId.trim())
169
+ out.bundleId = o.bundleId.trim();
170
+ if (typeof o.name === "string" && o.name.trim())
171
+ out.name = o.name.trim();
172
+ if (typeof o.pid === "number" && Number.isFinite(o.pid))
173
+ out.pid = Math.floor(o.pid);
174
+ return Object.keys(out).length > 0 ? out : undefined;
175
+ }
176
+ /** Android:adb dumpsys 小片段,失败不抛错。 */
177
+ async fetchAndroidForegroundAppViaAdb(resourceId) {
178
+ const adbPrefix = this.buildAndroidAdbPrefix(resourceId);
179
+ const innerCommands = [
180
+ 'dumpsys activity activities | grep -E "mResumedActivity|topResumedActivity|ResumedActivity" | head -n 14',
181
+ 'dumpsys window | grep -E "mCurrentFocus|mFocusedApp" | head -n 18',
182
+ "dumpsys activity top | head -n 80",
183
+ ];
184
+ for (const inner of innerCommands) {
185
+ const cmd = `${adbPrefix} shell ${JSON.stringify(inner)}`;
186
+ const result = await this.commandRunner.run(cmd, 4_000);
187
+ if (!result.ok)
188
+ continue;
189
+ const parsed = parseAndroidForegroundFromDumpsys(result.stdout);
190
+ if (parsed?.bundleId)
191
+ return parsed;
192
+ }
193
+ return undefined;
194
+ }
195
+ /** 鸿蒙:aa / hidumper,失败不抛错。 */
196
+ async fetchHarmonyForegroundAppViaHdc(resourceId) {
197
+ try {
198
+ const device = await this.getHarmonyDevice(resourceId);
199
+ const hdc = await device.getHdc();
200
+ const tryShell = async (cmd) => {
201
+ const out = await hdc.shell(cmd);
202
+ if (typeof out === "string" && out.length < 600 && /error:/i.test(out))
203
+ return undefined;
204
+ return parseHarmonyForegroundFromShellDump(out);
205
+ };
206
+ const cmds = ["aa dump -l", "hidumper -s WindowManagerService -a", "aa dump -a | head -n 500", "aa dump -a"];
207
+ for (const cmd of cmds) {
208
+ const v = await tryShell(cmd);
209
+ if (v?.bundleId)
210
+ return v;
211
+ }
212
+ return undefined;
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
217
+ }
218
+ /** iOS WDA `/wda/activeAppInfo`,失败不抛错。 */
219
+ async fetchIosForegroundAppFromWda(wdaHost, wdaPort) {
220
+ const url = `http://${wdaHost}:${wdaPort}/wda/activeAppInfo`;
221
+ const controller = new AbortController();
222
+ const timer = setTimeout(() => controller.abort(), 4_000);
223
+ try {
224
+ const res = await fetch(url, { signal: controller.signal });
225
+ if (!res.ok)
226
+ return undefined;
227
+ const json = (await res.json());
228
+ return this.normalizeWdaActiveAppValue(json?.value);
229
+ }
230
+ catch {
231
+ return undefined;
232
+ }
233
+ finally {
234
+ clearTimeout(timer);
235
+ }
236
+ }
237
+ async captureIosFrame(resourceId) {
238
+ const wdaHost = this.getIosWdaHost();
239
+ const wdaCmdPort = await this.resolveIosWdaCommandPortForResource(resourceId);
240
+ const mjpegPort = this.getIosWdaMjpegPort(wdaCmdPort);
241
+ const token = extractIosUdidFromResourceId(String(resourceId));
242
+ const fgPromise = this.fetchIosForegroundAppFromWda(wdaHost, wdaCmdPort);
243
+ try {
244
+ const jpeg = await captureFirstJpegFromWdaMjpegStream(wdaHost, mjpegPort, { timeoutMs: 8_000 });
245
+ const foregroundApp = await fgPromise;
246
+ return {
247
+ mimeType: "image/jpeg",
248
+ dataBase64: jpeg.toString("base64"),
249
+ sourceUri: `mjpeg://${wdaHost}:${mjpegPort}/ios/${token}`,
250
+ ...(foregroundApp ? { foregroundApp } : {}),
251
+ };
252
+ }
253
+ catch (mjpegErr) {
254
+ try {
255
+ const device = await this.getIosDevice(resourceId);
256
+ const dataUri = await device.screenshotBase64();
257
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
258
+ if (!match) {
259
+ throw new Error("ios screenshotBase64 returned unsupported data uri");
260
+ }
261
+ const foregroundApp = await fgPromise;
262
+ return {
263
+ mimeType: match[1],
264
+ dataBase64: match[2],
265
+ sourceUri: `wda://ios/${token}/latest-frame`,
266
+ ...(foregroundApp ? { foregroundApp } : {}),
267
+ };
268
+ }
269
+ catch (shotErr) {
270
+ throw new Error(`iOS 实时看屏: MJPEG(${mjpegPort}) 失败: ${mjpegErr instanceof Error ? mjpegErr.message : String(mjpegErr)};WebDriver 截图回退失败: ${shotErr instanceof Error ? shotErr.message : String(shotErr)}`);
271
+ }
272
+ }
273
+ }
274
+ async sendIosBackGesture(device) {
275
+ const { width, height } = await device.size();
276
+ const y = Math.round(height / 2);
277
+ const x1 = Math.max(8, Math.round(width * 0.02));
278
+ const x2 = Math.min(Math.round(width * 0.35), Math.max(x1 + 40, 120));
279
+ await device.swipe(x1, y, x2, y, 280);
280
+ }
281
+ async sendIosInput(resourceId, input) {
282
+ const device = await this.getIosDevice(resourceId);
283
+ if (input.action === "tap") {
284
+ const point = await this.normalizeIosLivePoint(device, input.x, input.y, input);
285
+ await device.tap(point.x, point.y);
286
+ return { output: "ok" };
287
+ }
288
+ if (input.action === "swipe") {
289
+ const durationMs = Number.isFinite(input.durationMs) && (input.durationMs ?? 0) > 0 ? Math.floor(input.durationMs) : 300;
290
+ const from = await this.normalizeIosLivePoint(device, input.x, input.y, input);
291
+ const to = await this.normalizeIosLivePoint(device, input.x2, input.y2, input);
292
+ await device.swipe(from.x, from.y, to.x, to.y, durationMs);
293
+ return { output: "ok" };
294
+ }
295
+ const raw = typeof input.key === "string" ? input.key.trim() : "";
296
+ const upper = raw.toUpperCase();
297
+ if (upper === "HOME" || upper === "KEYCODE_HOME" || raw === "3") {
298
+ await device.home();
299
+ return { output: "ok" };
300
+ }
301
+ if (upper === "APP_SWITCH" || upper === "RECENT" || raw === "187") {
302
+ await device.appSwitcher();
303
+ return { output: "ok" };
304
+ }
305
+ if (upper === "BACK" || upper === "KEYCODE_BACK" || raw === "4") {
306
+ await this.sendIosBackGesture(device);
307
+ return { output: "ok" };
308
+ }
309
+ if (raw) {
310
+ // @ts-expect-error - pressKey is private on IOSDevice in current @midscene SDK
311
+ await device.pressKey(raw);
312
+ }
313
+ return { output: "ok" };
314
+ }
315
+ async normalizeIosLivePoint(device, x, y, meta) {
316
+ if (meta.coordinateSpace !== "image" ||
317
+ !Number.isFinite(meta.sourceWidth) ||
318
+ !Number.isFinite(meta.sourceHeight) ||
319
+ (meta.sourceWidth ?? 0) <= 0 ||
320
+ (meta.sourceHeight ?? 0) <= 0) {
321
+ return { x: Math.round(x), y: Math.round(y) };
322
+ }
323
+ const { width, height } = await device.size();
324
+ return {
325
+ x: Math.round((x / meta.sourceWidth) * width),
326
+ y: Math.round((y / meta.sourceHeight) * height),
327
+ };
328
+ }
329
+ harmonyDeviceCacheKey(resourceId) {
330
+ const host = this.options.harmonyHdcHost?.trim() ?? "";
331
+ const port = Number.isFinite(this.options.harmonyHdcPort) && (this.options.harmonyHdcPort ?? 0) > 0 ? String(this.options.harmonyHdcPort) : "";
332
+ return `${String(resourceId)}|${host}|${port}`;
333
+ }
334
+ async getHarmonyDevice(resourceId) {
335
+ const key = this.harmonyDeviceCacheKey(resourceId);
336
+ const cached = this.harmonyDevices.get(key);
337
+ if (cached)
338
+ return cached;
339
+ const pending = this.harmonyDeviceConnecting.get(key);
340
+ if (pending)
341
+ return pending;
342
+ const creating = (async () => {
343
+ const device = await connectHarmonyAgentDebugDevice({
344
+ resourceId: String(resourceId),
345
+ harmonyHdcPath: this.options.harmonyHdcPath,
346
+ harmonyHdcHost: this.options.harmonyHdcHost,
347
+ harmonyHdcPort: this.options.harmonyHdcPort,
348
+ });
349
+ this.harmonyDevices.set(key, device);
350
+ return device;
351
+ })();
352
+ this.harmonyDeviceConnecting.set(key, creating);
353
+ try {
354
+ return await creating;
355
+ }
356
+ finally {
357
+ this.harmonyDeviceConnecting.delete(key);
358
+ }
359
+ }
360
+ async captureHarmonyFrame(resourceId) {
361
+ const fgPromise = this.fetchHarmonyForegroundAppViaHdc(resourceId);
362
+ const device = await this.getHarmonyDevice(resourceId);
363
+ const dataUri = await device.screenshotBase64();
364
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
365
+ if (!match) {
366
+ throw new Error("harmony screenshotBase64 returned unsupported data uri");
367
+ }
368
+ const token = extractHarmonyDeviceIdFromResourceId(String(resourceId));
369
+ const foregroundApp = await fgPromise;
370
+ return {
371
+ mimeType: match[1],
372
+ dataBase64: match[2],
373
+ sourceUri: `hdc://harmony/${token}/latest-frame`,
374
+ ...(foregroundApp ? { foregroundApp } : {}),
375
+ };
376
+ }
377
+ async sendHarmonyInput(resourceId, input) {
378
+ const device = await this.getHarmonyDevice(resourceId);
379
+ if (input.action === "tap") {
380
+ // @ts-expect-error - tap() API changed in current @midscene SDK for HarmonyDevice
381
+ await device.tap(Math.round(input.x), Math.round(input.y));
382
+ return { output: "ok" };
383
+ }
384
+ if (input.action === "swipe") {
385
+ const hdc = await device.getHdc();
386
+ const durationMs = Number.isFinite(input.durationMs) && (input.durationMs ?? 0) > 0 ? Math.floor(input.durationMs) : 300;
387
+ await hdc.swipe(Math.round(input.x), Math.round(input.y), Math.round(input.x2), Math.round(input.y2), durationMs);
388
+ return { output: "ok" };
389
+ }
390
+ const raw = typeof input.key === "string" ? input.key.trim() : "";
391
+ const upper = raw.toUpperCase();
392
+ if (upper === "BACK" || upper === "KEYCODE_BACK" || raw === "4") {
393
+ await device.back();
394
+ return { output: "ok" };
395
+ }
396
+ if (upper === "HOME" || upper === "KEYCODE_HOME" || raw === "3") {
397
+ await device.home();
398
+ return { output: "ok" };
399
+ }
400
+ if (upper === "APP_SWITCH" || upper === "RECENT" || raw === "187") {
401
+ await device.recentApps();
402
+ return { output: "ok" };
403
+ }
404
+ // @ts-expect-error - keyboardPress() API changed in current @midscene SDK for HarmonyDevice
405
+ await device.keyboardPress(raw);
406
+ return { output: "ok" };
407
+ }
408
+ extractAndroidSerial(resourceId) {
409
+ const raw = String(resourceId);
410
+ const idx = raw.indexOf(":");
411
+ return idx >= 0 ? raw.slice(idx + 1) : raw;
412
+ }
413
+ getAndroidAdbHost() {
414
+ const fromOptions = this.options.androidAdbHost?.trim();
415
+ if (fromOptions)
416
+ return fromOptions;
417
+ const fromEnv = process.env.MIDSCENE_ANDROID_ADB_HOST?.trim() || process.env.AGENT_ANDROID_ADB_HOST?.trim();
418
+ return fromEnv || "127.0.0.1";
419
+ }
420
+ getAndroidAdbPort() {
421
+ const fromOptions = this.options.androidAdbPort;
422
+ if (Number.isFinite(fromOptions) && (fromOptions ?? 0) > 0)
423
+ return Math.floor(fromOptions);
424
+ const fromEnv = Number(process.env.MIDSCENE_ANDROID_ADB_PORT ?? process.env.AGENT_ANDROID_ADB_PORT ?? "5037");
425
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
426
+ return Math.floor(fromEnv);
427
+ return 5037;
428
+ }
429
+ buildAndroidAdbPrefix(resourceId) {
430
+ const serial = this.extractAndroidSerial(resourceId);
431
+ const host = this.getAndroidAdbHost();
432
+ const port = this.getAndroidAdbPort();
433
+ return `adb -H ${JSON.stringify(host)} -P ${port} -s ${JSON.stringify(serial)}`;
434
+ }
435
+ async getAndroidDevice(resourceId) {
436
+ const serial = this.extractAndroidSerial(resourceId);
437
+ const cached = this.androidDevices.get(serial);
438
+ if (cached)
439
+ return cached;
440
+ const connecting = this.androidDeviceConnecting.get(serial);
441
+ if (connecting)
442
+ return connecting;
443
+ const creating = (async () => {
444
+ const device = new AndroidDevice(serial, {
445
+ remoteAdbHost: this.getAndroidAdbHost(),
446
+ remoteAdbPort: this.getAndroidAdbPort(),
447
+ scrcpyConfig: {
448
+ enabled: true,
449
+ maxSize: Number.isFinite(this.options.androidScrcpyMaxSize) && (this.options.androidScrcpyMaxSize ?? -1) >= 0
450
+ ? Math.floor(this.options.androidScrcpyMaxSize)
451
+ : 0,
452
+ videoBitRate: Number.isFinite(this.options.androidScrcpyVideoBitRate) && (this.options.androidScrcpyVideoBitRate ?? 0) > 0
453
+ ? Math.floor(this.options.androidScrcpyVideoBitRate)
454
+ : 40_000_000,
455
+ idleTimeoutMs: Number.isFinite(this.options.androidScrcpyIdleTimeoutMs) && (this.options.androidScrcpyIdleTimeoutMs ?? 0) > 0
456
+ ? Math.floor(this.options.androidScrcpyIdleTimeoutMs)
457
+ : 120_000,
458
+ },
459
+ });
460
+ await device.connect();
461
+ this.androidDevices.set(serial, device);
462
+ return device;
463
+ })();
464
+ this.androidDeviceConnecting.set(serial, creating);
465
+ try {
466
+ return await creating;
467
+ }
468
+ finally {
469
+ this.androidDeviceConnecting.delete(serial);
470
+ }
471
+ }
472
+ async captureAndroidFrame(resourceId) {
473
+ const fgPromise = this.fetchAndroidForegroundAppViaAdb(resourceId);
474
+ try {
475
+ const device = await this.getAndroidDevice(resourceId);
476
+ const dataUri = await device.screenshotBase64();
477
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
478
+ if (!match) {
479
+ throw new Error("android screenshotBase64 returned unsupported data uri");
480
+ }
481
+ const foregroundApp = await fgPromise;
482
+ return {
483
+ mimeType: match[1],
484
+ dataBase64: match[2],
485
+ sourceUri: `scrcpy://${this.extractAndroidSerial(resourceId)}/latest-frame`,
486
+ ...(foregroundApp ? { foregroundApp } : {}),
487
+ };
488
+ }
489
+ catch (error) {
490
+ // scrcpy 不可用时回退到 adb screencap,保证功能可用性
491
+ const adbPrefix = this.buildAndroidAdbPrefix(resourceId);
492
+ const command = `${adbPrefix} exec-out screencap -p | base64`;
493
+ const result = await this.commandRunner.run(command, 10_000);
494
+ if (!result.ok) {
495
+ throw new Error(`android live frame capture failed (scrcpy + screencap fallback): ${error instanceof Error ? error.message : String(error)}; ${result.stderr || result.stdout}`);
496
+ }
497
+ const dataBase64 = result.stdout.replace(/\s+/g, "");
498
+ if (!dataBase64) {
499
+ throw new Error("android live frame capture returned empty data");
500
+ }
501
+ const foregroundApp = await fgPromise;
502
+ return {
503
+ mimeType: "image/png",
504
+ dataBase64,
505
+ sourceUri: `adb://${this.extractAndroidSerial(resourceId)}/screencap`,
506
+ ...(foregroundApp ? { foregroundApp } : {}),
507
+ };
508
+ }
509
+ }
510
+ async sendAndroidInput(resourceId, input) {
511
+ const adbPrefix = this.buildAndroidAdbPrefix(resourceId);
512
+ const command = input.action === "tap"
513
+ ? `${adbPrefix} shell input tap ${Math.round(input.x)} ${Math.round(input.y)}`
514
+ : input.action === "swipe"
515
+ ? `${adbPrefix} shell input swipe ${Math.round(input.x)} ${Math.round(input.y)} ${Math.round(input.x2)} ${Math.round(input.y2)} ${Number.isFinite(input.durationMs) && (input.durationMs ?? 0) > 0 ? Math.floor(input.durationMs) : 300}`
516
+ : `${adbPrefix} shell input keyevent ${JSON.stringify(input.key)}`;
517
+ const result = await this.commandRunner.run(command, 10_000);
518
+ if (!result.ok) {
519
+ throw new Error(`android live input failed: ${result.stderr || result.stdout}`);
520
+ }
521
+ return { output: result.stdout.trim() || "ok" };
522
+ }
523
+ renderLiveInputCommand(input) {
524
+ if (input.action === "tap") {
525
+ return `tap(${Math.round(input.x)},${Math.round(input.y)})`;
526
+ }
527
+ if (input.action === "swipe") {
528
+ const durationMs = Number.isFinite(input.durationMs) && (input.durationMs ?? 0) > 0 ? Math.floor(input.durationMs) : 300;
529
+ return `swipe(${Math.round(input.x)},${Math.round(input.y)},${Math.round(input.x2)},${Math.round(input.y2)},${durationMs})`;
530
+ }
531
+ return `key(${JSON.stringify(input.key)})`;
532
+ }
533
+ }
@@ -0,0 +1,22 @@
1
+ import { ArtifactType } from "../../shared-kernel/enums/index.js";
2
+ export class MidsceneRuntimeMock {
3
+ async execute(task, resourceId, signal) {
4
+ if (signal?.aborted) {
5
+ return {
6
+ ok: false,
7
+ message: "cancelled by platform",
8
+ artifacts: [],
9
+ };
10
+ }
11
+ const ok = task.script.trim().length > 0;
12
+ return {
13
+ ok,
14
+ message: ok ? "mock execution success" : "script is empty",
15
+ artifacts: [
16
+ { type: ArtifactType.LOG, uri: `mock://artifact/${resourceId}/log.txt` },
17
+ { type: ArtifactType.TRACE, uri: `mock://artifact/${resourceId}/trace.json` },
18
+ { type: ArtifactType.SCREENSHOT, uri: `mock://artifact/${resourceId}/screen.png` },
19
+ ],
20
+ };
21
+ }
22
+ }