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.
- package/LICENSE +201 -0
- package/README.md +144 -0
- package/dist/adapter-spi/artifact/index.js +1 -0
- package/dist/adapter-spi/command/index.js +1 -0
- package/dist/adapter-spi/install/index.js +1 -0
- package/dist/adapter-spi/lifecycle/index.js +1 -0
- package/dist/adapter-spi/resource/index.js +1 -0
- package/dist/adapter-spi/snapshot/index.js +1 -0
- package/dist/application/agent/AgentCommandPollLoop.js +48 -0
- package/dist/application/agent/AgentRuntimeService.js +27 -0
- package/dist/application/app-package/AppPackageApplicationService.js +97 -0
- package/dist/application/artifact/ArtifactApplicationService.js +13 -0
- package/dist/application/debug/DebugApplicationService.js +117 -0
- package/dist/application/health/HealthMetricsService.js +47 -0
- package/dist/application/lease/LeaseApplicationService.js +79 -0
- package/dist/application/query/ObservationQueryService.js +48 -0
- package/dist/application/reporter/ReporterApplicationService.js +41 -0
- package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
- package/dist/application/resource/ResourceRegistryService.js +113 -0
- package/dist/application/session/SessionApplicationService.js +39 -0
- package/dist/application/task/TaskApplicationService.js +378 -0
- package/dist/client/agentAppPackageClient.js +91 -0
- package/dist/domain/agent/AgentNode.js +12 -0
- package/dist/domain/agent/AgentRuntime.js +6 -0
- package/dist/domain/artifact/ArtifactPipeline.js +6 -0
- package/dist/domain/artifact/ArtifactRef.js +12 -0
- package/dist/domain/event/AgentEvent.js +10 -0
- package/dist/domain/event/Reporter.js +6 -0
- package/dist/domain/health/HealthMetrics.js +8 -0
- package/dist/domain/lease/Lease.js +28 -0
- package/dist/domain/lease/LeaseManager.js +6 -0
- package/dist/domain/repositories/index.js +1 -0
- package/dist/domain/resource/DeviceDetails.js +23 -0
- package/dist/domain/resource/DeviceResource.js +16 -0
- package/dist/domain/resource/ResourceRegistry.js +6 -0
- package/dist/domain/runtime/interfaces.js +1 -0
- package/dist/domain/session/BaseSession.js +16 -0
- package/dist/domain/session/DebugSession.js +3 -0
- package/dist/domain/session/ExecutionSession.js +3 -0
- package/dist/domain/session/SessionManager.js +8 -0
- package/dist/domain/task/TaskRecord.js +14 -0
- package/dist/domain/task/TaskSpec.js +12 -0
- package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
- package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
- package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
- package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
- package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
- package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
- package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
- package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
- package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
- package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
- package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
- package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
- package/dist/infrastructure/config/agentConfigFile.js +96 -0
- package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
- package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
- package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
- package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
- package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
- package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
- package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
- package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
- package/dist/infrastructure/midscene/videoRecorder.js +365 -0
- package/dist/infrastructure/midscene/zipReportDir.js +36 -0
- package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
- package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
- package/dist/infrastructure/system/CommandRunner.js +128 -0
- package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
- package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
- package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
- package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
- package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
- package/dist/infrastructure/transport/midscenePaths.js +28 -0
- package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
- package/dist/infrastructure/transport/ws/WsClient.js +182 -0
- package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
- package/dist/interfaces/http/HttpServer.js +227 -0
- package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
- package/dist/main.js +368 -0
- package/dist/mcp/agentHttpClient.js +82 -0
- package/dist/mcp/agentRuntime.js +184 -0
- package/dist/mcp/cli.js +36 -0
- package/dist/mcp/doctor.js +124 -0
- package/dist/mcp/evidence.js +57 -0
- package/dist/mcp/exploration/index.js +129 -0
- package/dist/mcp/exploration/sessionManager.js +122 -0
- package/dist/mcp/exploration/tools-atomic.js +34 -0
- package/dist/mcp/exploration/tools-intelligent.js +33 -0
- package/dist/mcp/exploration/tools-session.js +276 -0
- package/dist/mcp/exploration/types.js +1 -0
- package/dist/mcp/flowStepEvents.js +114 -0
- package/dist/mcp/liveViewer.js +156 -0
- package/dist/mcp/reportReader.js +157 -0
- package/dist/mcp/runManager.js +161 -0
- package/dist/mcp/runSummary.js +72 -0
- package/dist/mcp/runtimeInstall.js +44 -0
- package/dist/mcp/server.js +260 -0
- package/dist/mcp/setup.js +185 -0
- package/dist/mcp/types.js +1 -0
- package/dist/mcp/userConfig.js +45 -0
- package/dist/mcp/visual-flow/codegen.js +576 -0
- package/dist/mcp/visual-flow/index.js +14 -0
- package/dist/mcp/visual-flow/types.js +5 -0
- package/dist/mcp/visual-flow/validate.js +617 -0
- package/dist/protocol-contracts/commands/envelope.js +24 -0
- package/dist/protocol-contracts/commands/index.js +1 -0
- package/dist/protocol-contracts/dto/index.js +1 -0
- package/dist/protocol-contracts/events/index.js +1 -0
- package/dist/protocol-contracts/queries/index.js +1 -0
- package/dist/shared-kernel/enums/index.js +70 -0
- package/dist/shared-kernel/errors/index.js +21 -0
- package/dist/shared-kernel/ids/index.js +18 -0
- package/dist/shared-kernel/time/index.js +3 -0
- package/dist/shared-kernel/value-objects/index.js +1 -0
- package/dist/utils/appPackageLocalPath.js +75 -0
- package/dist/utils/deviceResourceRouting.js +24 -0
- package/dist/utils/harmonyAgentDebugDevice.js +60 -0
- package/dist/utils/harmonyHdcDeviceId.js +134 -0
- package/dist/utils/iosAgentDebugDevice.js +72 -0
- package/dist/utils/iosMjpegCapture.js +90 -0
- package/dist/utils/liveDebugForegroundParse.js +71 -0
- package/dist/utils/midscene-device-session.js +353 -0
- package/dist/utils/midscene-task-cache-env.js +15 -0
- package/dist/utils/midsceneReportConstants.js +49 -0
- package/dist/utils/seedMidsceneTaskCache.js +61 -0
- package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
- package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
- package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
- package/dist/utils/task-runners/taskAppPackage.js +20 -0
- package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
- package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
- package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
- package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
- package/package.json +46 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class TaskRecord {
|
|
2
|
+
id;
|
|
3
|
+
spec;
|
|
4
|
+
status;
|
|
5
|
+
sessionId;
|
|
6
|
+
message;
|
|
7
|
+
constructor(id, spec, status, sessionId, message) {
|
|
8
|
+
this.id = id;
|
|
9
|
+
this.spec = spec;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.sessionId = sessionId;
|
|
12
|
+
this.message = message;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class TaskSpec {
|
|
2
|
+
requiredPlatform;
|
|
3
|
+
script;
|
|
4
|
+
scriptKind;
|
|
5
|
+
airtest;
|
|
6
|
+
constructor(requiredPlatform, script, scriptKind = "midscene", airtest) {
|
|
7
|
+
this.requiredPlatform = requiredPlatform;
|
|
8
|
+
this.script = script;
|
|
9
|
+
this.scriptKind = scriptKind;
|
|
10
|
+
this.airtest = airtest;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class AdapterRegistry {
|
|
2
|
+
resourceAdapters;
|
|
3
|
+
commandExecutor;
|
|
4
|
+
snapshotProvider;
|
|
5
|
+
constructor(resourceAdapters, commandExecutor, snapshotProvider) {
|
|
6
|
+
this.resourceAdapters = resourceAdapters;
|
|
7
|
+
this.commandExecutor = commandExecutor;
|
|
8
|
+
this.snapshotProvider = snapshotProvider;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DeviceResource } from "../../../domain/resource/DeviceResource.js";
|
|
2
|
+
import { PlatformType, ResourceStatus } from "../../../shared-kernel/enums/index.js";
|
|
3
|
+
import { asResourceId } from "../../../shared-kernel/ids/index.js";
|
|
4
|
+
import { probeAndroidDeviceDetails } from "../deviceDetailsProbe.js";
|
|
5
|
+
export function parseAndroidDevices(stdout) {
|
|
6
|
+
return stdout
|
|
7
|
+
.split("\n")
|
|
8
|
+
.map((line) => line.trim())
|
|
9
|
+
.filter((line) => line.length > 0 && !line.startsWith("List of devices attached"))
|
|
10
|
+
.map((line) => {
|
|
11
|
+
const [serial, state] = line.split(/\s+/);
|
|
12
|
+
return { serial: serial ?? "", state: state ?? "" };
|
|
13
|
+
})
|
|
14
|
+
.filter((row) => row.serial.length > 0);
|
|
15
|
+
}
|
|
16
|
+
function toAndroidResourceId(serial) {
|
|
17
|
+
return `android:${serial}`;
|
|
18
|
+
}
|
|
19
|
+
export class AndroidResourceAdapter {
|
|
20
|
+
commandRunner;
|
|
21
|
+
discoveryCommand;
|
|
22
|
+
adbServerHost;
|
|
23
|
+
adbServerPort;
|
|
24
|
+
constructor(commandRunner, discoveryCommand, adbServerHost, adbServerPort) {
|
|
25
|
+
this.commandRunner = commandRunner;
|
|
26
|
+
this.discoveryCommand = discoveryCommand;
|
|
27
|
+
this.adbServerHost = adbServerHost;
|
|
28
|
+
this.adbServerPort = adbServerPort;
|
|
29
|
+
}
|
|
30
|
+
adapterName() {
|
|
31
|
+
return "AndroidResourceAdapter";
|
|
32
|
+
}
|
|
33
|
+
async discover() {
|
|
34
|
+
const result = await this.commandRunner.run(this.discoveryCommand, 15_000);
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
return [];
|
|
37
|
+
const targets = parseAndroidDevices(result.stdout).filter((item) => item.state === "device");
|
|
38
|
+
const rows = await Promise.all(targets.map(async (target) => {
|
|
39
|
+
const deviceDetails = await probeAndroidDeviceDetails(this.commandRunner, target.serial, 12_000, {
|
|
40
|
+
host: this.adbServerHost,
|
|
41
|
+
port: this.adbServerPort,
|
|
42
|
+
});
|
|
43
|
+
return new DeviceResource(asResourceId(toAndroidResourceId(target.serial)), PlatformType.ANDROID, ResourceStatus.ONLINE, {
|
|
44
|
+
platform: PlatformType.ANDROID,
|
|
45
|
+
supportsDebug: true,
|
|
46
|
+
labels: ["real", "android", target.serial],
|
|
47
|
+
}, deviceDetails);
|
|
48
|
+
}));
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { compactDeviceDetails } from "../../domain/resource/DeviceDetails.js";
|
|
2
|
+
export function shSingleQuote(value) {
|
|
3
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
4
|
+
}
|
|
5
|
+
/** 供 hdc 探测子命令与会话参数一致(可执行路径含空格时加引号) */
|
|
6
|
+
export function buildHarmonyHdcShellPrefix(hdcPath, hdcHost, hdcPort) {
|
|
7
|
+
const trimmedPath = hdcPath?.trim();
|
|
8
|
+
const rawExe = trimmedPath || "hdc";
|
|
9
|
+
const exe = /[^\w@%+=:,./-]/.test(rawExe) || rawExe.includes(" ")
|
|
10
|
+
? `"${rawExe.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
11
|
+
: rawExe;
|
|
12
|
+
const host = hdcHost?.trim();
|
|
13
|
+
if (host && hdcPort != null && Number.isFinite(hdcPort) && hdcPort > 0) {
|
|
14
|
+
return `${exe} -s ${host}:${Math.floor(hdcPort)}`;
|
|
15
|
+
}
|
|
16
|
+
return exe;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 解析 `adb shell` 多行 getprop 输出。
|
|
20
|
+
* - 7 行:manufacturer, brand, model, device, release, sdk, fingerprint(与 ANDROID_PROP_SHELL 一致)
|
|
21
|
+
* - 4 行:兼容旧版 manufacturer, model, release, sdk
|
|
22
|
+
*/
|
|
23
|
+
export function parseAndroidGetpropBlock(stdout) {
|
|
24
|
+
const lines = stdout
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map((l) => l.trim())
|
|
27
|
+
.filter((l) => l.length > 0);
|
|
28
|
+
let manufacturer;
|
|
29
|
+
let brand;
|
|
30
|
+
let model;
|
|
31
|
+
let deviceCodename;
|
|
32
|
+
let release;
|
|
33
|
+
let sdk;
|
|
34
|
+
let fingerprint;
|
|
35
|
+
if (lines.length >= 7) {
|
|
36
|
+
manufacturer = lines[0];
|
|
37
|
+
brand = lines[1];
|
|
38
|
+
model = lines[2];
|
|
39
|
+
deviceCodename = lines[3];
|
|
40
|
+
release = lines[4];
|
|
41
|
+
sdk = lines[5];
|
|
42
|
+
fingerprint = lines[6];
|
|
43
|
+
}
|
|
44
|
+
else if (lines.length >= 4) {
|
|
45
|
+
manufacturer = lines[0];
|
|
46
|
+
model = lines[1];
|
|
47
|
+
release = lines[2];
|
|
48
|
+
sdk = lines[3];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
const parts = [];
|
|
54
|
+
if (release)
|
|
55
|
+
parts.push(release);
|
|
56
|
+
if (sdk && /^\d+$/.test(sdk))
|
|
57
|
+
parts.push(`API ${sdk}`);
|
|
58
|
+
const osVersion = parts.length ? parts.join(" · ") : undefined;
|
|
59
|
+
return {
|
|
60
|
+
manufacturer: manufacturer || undefined,
|
|
61
|
+
brand: brand || undefined,
|
|
62
|
+
model: model || undefined,
|
|
63
|
+
deviceCodename: deviceCodename || undefined,
|
|
64
|
+
osVersion,
|
|
65
|
+
buildFingerprint: fingerprint || undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** 与 MIDSCENE_ANDROID_ADB_HOST / MIDSCENE_ANDROID_ADB_PORT 对齐;非本机默认 daemon 时加 `-H` `-P`。 */
|
|
69
|
+
export function buildAndroidAdbCliPrefix(adbHost, adbPort) {
|
|
70
|
+
const host = (adbHost ?? "").trim() || "127.0.0.1";
|
|
71
|
+
const p = adbPort != null && Number.isFinite(adbPort) && adbPort > 0 ? Math.floor(adbPort) : 5037;
|
|
72
|
+
if ((host === "127.0.0.1" || host === "localhost") && p === 5037) {
|
|
73
|
+
return "adb";
|
|
74
|
+
}
|
|
75
|
+
const safeHost = /[\s'"\\]/.test(host) ? shSingleQuote(host) : host;
|
|
76
|
+
return `adb -H ${safeHost} -P ${p}`;
|
|
77
|
+
}
|
|
78
|
+
export function parseBatteryLevelFromDumpsys(stdout) {
|
|
79
|
+
const m = stdout.match(/^\s*level:\s*(\d+)\s*$/im);
|
|
80
|
+
if (m?.[1]) {
|
|
81
|
+
const n = Number(m[1]);
|
|
82
|
+
if (Number.isFinite(n) && n >= 0 && n <= 100)
|
|
83
|
+
return n;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
export function parseHarmonyBatteryCapacity(stdout) {
|
|
88
|
+
const m = stdout.match(/^\s*capacity:\s*(\d+)\s*$/im);
|
|
89
|
+
if (!m?.[1])
|
|
90
|
+
return null;
|
|
91
|
+
const n = Number(m[1]);
|
|
92
|
+
if (Number.isFinite(n) && n >= 0 && n <= 100)
|
|
93
|
+
return n;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const ANDROID_PROP_SHELL = "getprop ro.product.manufacturer; getprop ro.product.brand; getprop ro.product.model; getprop ro.product.device; getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.build.fingerprint";
|
|
97
|
+
/** Android / Harmony:系统设置中的本机名称(优先 global device_name,否则 secure bluetooth_name)。 */
|
|
98
|
+
const SHELL_USER_DEVICE_NAME = 'n=$(settings get global device_name); case "$n" in ""|null|NULL) settings get secure bluetooth_name;; *) printf %s "$n";; esac';
|
|
99
|
+
const HARMONY_PARAM_SHELL = "param get const.product.manufacturer; param get const.product.brand; param get const.product.name; param get const.product.model; param get const.build.product; param get const.product.os.dist.name; param get const.product.os.dist.version; param get const.product.os.dist.apiversion; param get const.product.software.version";
|
|
100
|
+
/** 解析 `settings get` 类单行输出,忽略 null/空。 */
|
|
101
|
+
export function normalizeShellSettingsDeviceName(stdout) {
|
|
102
|
+
const line = stdout
|
|
103
|
+
.split("\n")
|
|
104
|
+
.map((l) => l.trim())
|
|
105
|
+
.find((l) => l.length > 0);
|
|
106
|
+
if (!line || /^null$/i.test(line) || line === "N/A")
|
|
107
|
+
return undefined;
|
|
108
|
+
return line;
|
|
109
|
+
}
|
|
110
|
+
export function parseHarmonyParamBlock(stdout) {
|
|
111
|
+
const lines = stdout.split("\n").map((l) => l.trim().replace(/^"(.*)"$/, "$1"));
|
|
112
|
+
const [manufacturer, brand, productName, model, deviceCodename, osName, osDistVersion, osDistApiVersion, softwareVersion] = lines;
|
|
113
|
+
const osParts = [];
|
|
114
|
+
if (osDistVersion)
|
|
115
|
+
osParts.push(osDistVersion);
|
|
116
|
+
if (osDistApiVersion && /^\d+$/.test(osDistApiVersion))
|
|
117
|
+
osParts.push(`API ${osDistApiVersion}`);
|
|
118
|
+
return {
|
|
119
|
+
manufacturer: manufacturer || undefined,
|
|
120
|
+
brand: brand || undefined,
|
|
121
|
+
deviceName: productName || undefined,
|
|
122
|
+
model: model || undefined,
|
|
123
|
+
deviceCodename: deviceCodename || undefined,
|
|
124
|
+
osName: osName || undefined,
|
|
125
|
+
osVersion: osParts.length ? osParts.join(" · ") : undefined,
|
|
126
|
+
buildFingerprint: softwareVersion || undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function probeAndroidDeviceDetails(runner, serial, timeoutMs = 12_000, adbServer) {
|
|
130
|
+
const adbCli = buildAndroidAdbCliPrefix(adbServer?.host, adbServer?.port);
|
|
131
|
+
const sq = shSingleQuote(serial);
|
|
132
|
+
const propsCmd = `${adbCli} -s ${sq} shell ${JSON.stringify(ANDROID_PROP_SHELL)}`;
|
|
133
|
+
const batCmd = `${adbCli} -s ${sq} shell dumpsys battery 2>/dev/null | head -n 40`;
|
|
134
|
+
const nameCmd = `${adbCli} -s ${sq} shell ${JSON.stringify(SHELL_USER_DEVICE_NAME)}`;
|
|
135
|
+
const [propsRes, batRes, nameRes] = await Promise.all([
|
|
136
|
+
runner.run(propsCmd, timeoutMs),
|
|
137
|
+
runner.run(batCmd, timeoutMs),
|
|
138
|
+
runner.run(nameCmd, timeoutMs),
|
|
139
|
+
]);
|
|
140
|
+
if (!propsRes.ok && !batRes.ok)
|
|
141
|
+
return undefined;
|
|
142
|
+
const base = { osName: "Android" };
|
|
143
|
+
if (propsRes.ok)
|
|
144
|
+
Object.assign(base, parseAndroidGetpropBlock(propsRes.stdout));
|
|
145
|
+
if (batRes.ok) {
|
|
146
|
+
const level = parseBatteryLevelFromDumpsys(batRes.stdout);
|
|
147
|
+
if (level != null)
|
|
148
|
+
base.batteryPercent = level;
|
|
149
|
+
}
|
|
150
|
+
if (nameRes.ok) {
|
|
151
|
+
const deviceName = normalizeShellSettingsDeviceName(nameRes.stdout);
|
|
152
|
+
if (deviceName)
|
|
153
|
+
base.deviceName = deviceName;
|
|
154
|
+
}
|
|
155
|
+
return compactDeviceDetails(base);
|
|
156
|
+
}
|
|
157
|
+
const IOS_VERSION_IN_NAME = /\((\d{1,2}\.\d+(?:\.\d+)?)\)\s*$/;
|
|
158
|
+
/**
|
|
159
|
+
* 仅从 xctrace 展示名解析 iOS 版本(型号须用 ideviceinfo -k ProductType,勿把用户设备名当型号)。
|
|
160
|
+
*/
|
|
161
|
+
export function buildIosDeviceDetailsFromDisplayName(displayName) {
|
|
162
|
+
const name = displayName.trim();
|
|
163
|
+
const m = IOS_VERSION_IN_NAME.exec(name);
|
|
164
|
+
const osVersion = m?.[1];
|
|
165
|
+
return compactDeviceDetails({
|
|
166
|
+
manufacturer: "Apple",
|
|
167
|
+
osName: "iOS",
|
|
168
|
+
osVersion: osVersion || undefined,
|
|
169
|
+
}) ?? { manufacturer: "Apple", osName: "iOS" };
|
|
170
|
+
}
|
|
171
|
+
/** 硬件型号标识,如 iPhone15,2 */
|
|
172
|
+
export async function probeIosProductType(runner, udid, timeoutMs = 8_000) {
|
|
173
|
+
const sq = shSingleQuote(udid);
|
|
174
|
+
const cmd = `ideviceinfo -u ${sq} -k ProductType 2>/dev/null`;
|
|
175
|
+
const res = await runner.run(cmd, timeoutMs);
|
|
176
|
+
if (!res.ok)
|
|
177
|
+
return undefined;
|
|
178
|
+
const line = (res.stdout.trim().split(/\s+/)[0] ?? "").trim();
|
|
179
|
+
if (!line || line === "N/A")
|
|
180
|
+
return undefined;
|
|
181
|
+
return line;
|
|
182
|
+
}
|
|
183
|
+
export async function probeIosBatteryPercent(runner, udid, timeoutMs = 8_000) {
|
|
184
|
+
const sq = shSingleQuote(udid);
|
|
185
|
+
const cmd = `ideviceinfo -u ${sq} -q com.apple.mobile.battery -k BatteryCurrentCapacity 2>/dev/null`;
|
|
186
|
+
const res = await runner.run(cmd, timeoutMs);
|
|
187
|
+
if (!res.ok)
|
|
188
|
+
return null;
|
|
189
|
+
const raw = res.stdout.trim();
|
|
190
|
+
if (!raw)
|
|
191
|
+
return null;
|
|
192
|
+
const token = raw.split(/\s+/)[0] ?? "";
|
|
193
|
+
const n = Number(token);
|
|
194
|
+
if (!Number.isFinite(n))
|
|
195
|
+
return null;
|
|
196
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
197
|
+
}
|
|
198
|
+
/** iOS 设置中的设备名称(与 xctrace 列表展示名不同)。 */
|
|
199
|
+
export async function probeIosDeviceName(runner, udid, timeoutMs = 8_000) {
|
|
200
|
+
const sq = shSingleQuote(udid);
|
|
201
|
+
const cmd = `ideviceinfo -u ${sq} -k DeviceName 2>/dev/null`;
|
|
202
|
+
const res = await runner.run(cmd, timeoutMs);
|
|
203
|
+
if (!res.ok)
|
|
204
|
+
return undefined;
|
|
205
|
+
const line = (res.stdout.trim().split(/\n/)[0] ?? "").trim();
|
|
206
|
+
if (!line || line === "N/A")
|
|
207
|
+
return undefined;
|
|
208
|
+
return line;
|
|
209
|
+
}
|
|
210
|
+
export async function probeHarmonyDeviceDetails(runner, hdcShellPrefix, target, timeoutMs = 12_000) {
|
|
211
|
+
const prefix = hdcShellPrefix.trim() || "hdc";
|
|
212
|
+
const tq = shSingleQuote(target);
|
|
213
|
+
const propsCmd = `${prefix} -t ${tq} shell ${JSON.stringify(HARMONY_PARAM_SHELL)}`;
|
|
214
|
+
const batCmd = `${prefix} -t ${tq} shell hidumper -s BatteryService -a -i`;
|
|
215
|
+
const [propsRes, batRes] = await Promise.all([
|
|
216
|
+
runner.run(propsCmd, timeoutMs),
|
|
217
|
+
runner.run(batCmd, timeoutMs),
|
|
218
|
+
]);
|
|
219
|
+
if (!propsRes.ok)
|
|
220
|
+
return undefined;
|
|
221
|
+
const base = { osName: "HarmonyOS" };
|
|
222
|
+
Object.assign(base, parseHarmonyParamBlock(propsRes.stdout));
|
|
223
|
+
if (batRes.ok) {
|
|
224
|
+
const level = parseHarmonyBatteryCapacity(batRes.stdout);
|
|
225
|
+
if (level != null)
|
|
226
|
+
base.batteryPercent = level;
|
|
227
|
+
}
|
|
228
|
+
return compactDeviceDetails(base);
|
|
229
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DeviceResource } from "../../../domain/resource/DeviceResource.js";
|
|
2
|
+
import { PlatformType, ResourceStatus } from "../../../shared-kernel/enums/index.js";
|
|
3
|
+
import { asResourceId } from "../../../shared-kernel/ids/index.js";
|
|
4
|
+
import { probeHarmonyDeviceDetails } from "../deviceDetailsProbe.js";
|
|
5
|
+
export function parseHarmonyTargets(stdout) {
|
|
6
|
+
return stdout
|
|
7
|
+
.split("\n")
|
|
8
|
+
.map((line) => line.trim())
|
|
9
|
+
.filter((line) => line.length > 0 && !line.includes("[Empty]"));
|
|
10
|
+
}
|
|
11
|
+
function toHarmonyResourceId(target) {
|
|
12
|
+
return `harmony:${target}`;
|
|
13
|
+
}
|
|
14
|
+
export class HarmonyResourceAdapter {
|
|
15
|
+
commandRunner;
|
|
16
|
+
discoveryCommand;
|
|
17
|
+
hdcShellPrefix;
|
|
18
|
+
constructor(commandRunner, discoveryCommand, hdcShellPrefix = "hdc") {
|
|
19
|
+
this.commandRunner = commandRunner;
|
|
20
|
+
this.discoveryCommand = discoveryCommand;
|
|
21
|
+
this.hdcShellPrefix = hdcShellPrefix;
|
|
22
|
+
}
|
|
23
|
+
adapterName() {
|
|
24
|
+
return "HarmonyResourceAdapter";
|
|
25
|
+
}
|
|
26
|
+
async discover() {
|
|
27
|
+
const result = await this.commandRunner.run(this.discoveryCommand, 15_000);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
return [];
|
|
30
|
+
const targets = parseHarmonyTargets(result.stdout);
|
|
31
|
+
return Promise.all(targets.map(async (target) => {
|
|
32
|
+
const deviceDetails = await probeHarmonyDeviceDetails(this.commandRunner, this.hdcShellPrefix, target);
|
|
33
|
+
return new DeviceResource(asResourceId(toHarmonyResourceId(target)), PlatformType.HARMONY, ResourceStatus.ONLINE, {
|
|
34
|
+
platform: PlatformType.HARMONY,
|
|
35
|
+
supportsDebug: true,
|
|
36
|
+
labels: ["real", "harmony", target],
|
|
37
|
+
}, deviceDetails);
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { compactDeviceDetails } from "../../../domain/resource/DeviceDetails.js";
|
|
2
|
+
import { DeviceResource } from "../../../domain/resource/DeviceResource.js";
|
|
3
|
+
import { PlatformType, ResourceStatus } from "../../../shared-kernel/enums/index.js";
|
|
4
|
+
import { asResourceId } from "../../../shared-kernel/ids/index.js";
|
|
5
|
+
import { buildIosDeviceDetailsFromDisplayName, probeIosBatteryPercent, probeIosDeviceName, probeIosProductType, } from "../deviceDetailsProbe.js";
|
|
6
|
+
const DEVICE_LINE_PATTERN = /(.*)\(([-A-Za-z0-9]+)\)\s*$/;
|
|
7
|
+
const IOS_DEVICE_NAME_HINTS = ["iphone", "ipad", "ipod"];
|
|
8
|
+
const NON_IOS_HOST_HINTS = [
|
|
9
|
+
"macbook",
|
|
10
|
+
"mac mini",
|
|
11
|
+
"imac",
|
|
12
|
+
"mac studio",
|
|
13
|
+
"mac pro",
|
|
14
|
+
"my mac",
|
|
15
|
+
];
|
|
16
|
+
/** xctrace 真机常见展示:`别名 (16.7.8) (udid)`,与主机 `名称 (UUID)` 区分 */
|
|
17
|
+
const XCTRACE_IOS_VERSION_IN_DISPLAY_NAME = /\(\d{1,2}\.\d+(?:\.\d+)?\)/;
|
|
18
|
+
/** 匹配 simctl 设备行:` iPhone 15 Pro (UUID) (Booted)` */
|
|
19
|
+
const SIMCTL_DEVICE_LINE = /^\s+(.+?)\s+\(([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})\)\s+\((\w+)\)\s*$/;
|
|
20
|
+
/** 匹配 simctl Section Header:`-- iOS 17.4 --` 或 `-- iOS 17.4 (21F90) --` */
|
|
21
|
+
const SIMCTL_SECTION_HEADER = /^--\s+(.+?)\s+--$/;
|
|
22
|
+
function isLegacy40CharHexUdid(udid) {
|
|
23
|
+
return /^[0-9a-f]{40}$/i.test(udid.trim());
|
|
24
|
+
}
|
|
25
|
+
/** 如 `00008110-001C11111111111E`(8-4-12),与标准五段主机 UUID 区分 */
|
|
26
|
+
function isAppleMobileUdidThreeSegment(udid) {
|
|
27
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(udid.trim());
|
|
28
|
+
}
|
|
29
|
+
/** Instruments 列表里本机常为 RFC4122 五段式 */
|
|
30
|
+
function isStandardFivePartUuid(udid) {
|
|
31
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(udid.trim());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 判断 `xcrun xctrace list devices` 的「== Devices ==」行是否为已连接的真机。
|
|
35
|
+
* 不能仅靠名称里的 iphone/ipad:用户自定义设备名时仍会带系统版本号括号或 40 位 hex UDID。
|
|
36
|
+
*/
|
|
37
|
+
function isLikelyPhysicalIosDevice(name, udid) {
|
|
38
|
+
const normalized = name.trim().toLowerCase();
|
|
39
|
+
const id = udid.trim();
|
|
40
|
+
if (!normalized || !id)
|
|
41
|
+
return false;
|
|
42
|
+
if (normalized.includes("simulator"))
|
|
43
|
+
return false;
|
|
44
|
+
if (NON_IOS_HOST_HINTS.some((hint) => normalized.includes(hint)))
|
|
45
|
+
return false;
|
|
46
|
+
if (IOS_DEVICE_NAME_HINTS.some((hint) => normalized.includes(hint)))
|
|
47
|
+
return true;
|
|
48
|
+
if (isLegacy40CharHexUdid(id))
|
|
49
|
+
return true;
|
|
50
|
+
if (isAppleMobileUdidThreeSegment(id))
|
|
51
|
+
return true;
|
|
52
|
+
if (XCTRACE_IOS_VERSION_IN_DISPLAY_NAME.test(name))
|
|
53
|
+
return true;
|
|
54
|
+
if (isStandardFivePartUuid(id))
|
|
55
|
+
return false;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function extractOnlineDeviceLines(stdout) {
|
|
59
|
+
const lines = stdout.split("\n").map((line) => line.trim());
|
|
60
|
+
const headingIndexes = lines
|
|
61
|
+
.map((line, index) => ({ line, index }))
|
|
62
|
+
.filter((item) => item.line.startsWith("== ") && item.line.endsWith(" =="));
|
|
63
|
+
const devicesHeading = headingIndexes.find((item) => item.line === "== Devices ==");
|
|
64
|
+
// Fallback: unknown format, keep previous permissive behavior.
|
|
65
|
+
if (!devicesHeading) {
|
|
66
|
+
return lines.filter((line) => line.length > 0);
|
|
67
|
+
}
|
|
68
|
+
const nextHeading = headingIndexes.find((item) => item.index > devicesHeading.index);
|
|
69
|
+
const end = nextHeading ? nextHeading.index : lines.length;
|
|
70
|
+
return lines.slice(devicesHeading.index + 1, end).filter((line) => line.length > 0);
|
|
71
|
+
}
|
|
72
|
+
export function parseIOSDevices(stdout) {
|
|
73
|
+
return extractOnlineDeviceLines(stdout)
|
|
74
|
+
.map((line) => {
|
|
75
|
+
const match = DEVICE_LINE_PATTERN.exec(line);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const name = match[1]?.trim() ?? "";
|
|
79
|
+
const udid = match[2]?.trim() ?? "";
|
|
80
|
+
if (!isLikelyPhysicalIosDevice(name, udid))
|
|
81
|
+
return null;
|
|
82
|
+
return { name, udid };
|
|
83
|
+
})
|
|
84
|
+
.filter((item) => item !== null && item.udid.length > 0);
|
|
85
|
+
}
|
|
86
|
+
function toIOSResourceId(udid) {
|
|
87
|
+
return `ios:${udid}`;
|
|
88
|
+
}
|
|
89
|
+
export function parseBootedSimulators(stdout) {
|
|
90
|
+
const lines = stdout.split("\n");
|
|
91
|
+
const result = [];
|
|
92
|
+
let currentRuntime = null;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const headerMatch = line.match(SIMCTL_SECTION_HEADER);
|
|
95
|
+
if (headerMatch) {
|
|
96
|
+
currentRuntime = headerMatch[1].trim();
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const deviceMatch = line.match(SIMCTL_DEVICE_LINE);
|
|
100
|
+
if (deviceMatch && currentRuntime && deviceMatch[3] === "Booted") {
|
|
101
|
+
result.push({
|
|
102
|
+
name: deviceMatch[1].trim(),
|
|
103
|
+
udid: deviceMatch[2],
|
|
104
|
+
runtime: currentRuntime,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function osNameFromRuntime(runtime) {
|
|
111
|
+
const lower = runtime.toLowerCase();
|
|
112
|
+
if (lower.startsWith("tvos"))
|
|
113
|
+
return "tvOS";
|
|
114
|
+
if (lower.startsWith("watchos"))
|
|
115
|
+
return "watchOS";
|
|
116
|
+
if (lower.startsWith("visionos"))
|
|
117
|
+
return "visionOS";
|
|
118
|
+
return "iOS";
|
|
119
|
+
}
|
|
120
|
+
function osVersionFromRuntime(runtime) {
|
|
121
|
+
const m = runtime.match(/(\d[\d.]*)/);
|
|
122
|
+
return m ? m[1] : undefined;
|
|
123
|
+
}
|
|
124
|
+
export class IOSResourceAdapter {
|
|
125
|
+
commandRunner;
|
|
126
|
+
discoveryCommand;
|
|
127
|
+
constructor(commandRunner, discoveryCommand) {
|
|
128
|
+
this.commandRunner = commandRunner;
|
|
129
|
+
this.discoveryCommand = discoveryCommand;
|
|
130
|
+
}
|
|
131
|
+
adapterName() {
|
|
132
|
+
return "IOSResourceAdapter";
|
|
133
|
+
}
|
|
134
|
+
async discover() {
|
|
135
|
+
const [xctraceResult, simctlResult] = await Promise.all([
|
|
136
|
+
this.commandRunner.run(this.discoveryCommand, 15_000),
|
|
137
|
+
this.commandRunner.run("xcrun simctl list devices booted", 10_000),
|
|
138
|
+
]);
|
|
139
|
+
const resources = [];
|
|
140
|
+
// 1) Real iOS devices via xctrace
|
|
141
|
+
if (xctraceResult.ok) {
|
|
142
|
+
const targets = parseIOSDevices(xctraceResult.stdout);
|
|
143
|
+
const realDevices = await Promise.all(targets.map(async (target) => {
|
|
144
|
+
const base = buildIosDeviceDetailsFromDisplayName(target.name);
|
|
145
|
+
const [productType, batteryPercent, deviceName] = await Promise.all([
|
|
146
|
+
probeIosProductType(this.commandRunner, target.udid),
|
|
147
|
+
probeIosBatteryPercent(this.commandRunner, target.udid),
|
|
148
|
+
probeIosDeviceName(this.commandRunner, target.udid),
|
|
149
|
+
]);
|
|
150
|
+
const merged = compactDeviceDetails({
|
|
151
|
+
...base,
|
|
152
|
+
...(productType ? { model: productType } : {}),
|
|
153
|
+
...(batteryPercent != null ? { batteryPercent } : {}),
|
|
154
|
+
...(deviceName ? { deviceName } : {}),
|
|
155
|
+
});
|
|
156
|
+
return new DeviceResource(asResourceId(toIOSResourceId(target.udid)), PlatformType.IOS, ResourceStatus.ONLINE, {
|
|
157
|
+
platform: PlatformType.IOS,
|
|
158
|
+
supportsDebug: true,
|
|
159
|
+
labels: ["real", "ios", target.name],
|
|
160
|
+
}, merged);
|
|
161
|
+
}));
|
|
162
|
+
resources.push(...realDevices);
|
|
163
|
+
}
|
|
164
|
+
// 2) Booted simulators via simctl
|
|
165
|
+
if (simctlResult.ok) {
|
|
166
|
+
const simTargets = parseBootedSimulators(simctlResult.stdout);
|
|
167
|
+
for (const target of simTargets) {
|
|
168
|
+
resources.push(new DeviceResource(asResourceId(toIOSResourceId(target.udid)), PlatformType.IOS, ResourceStatus.ONLINE, {
|
|
169
|
+
platform: PlatformType.IOS,
|
|
170
|
+
supportsDebug: true,
|
|
171
|
+
labels: ["simulator", "ios", target.name],
|
|
172
|
+
}, compactDeviceDetails({
|
|
173
|
+
manufacturer: "Apple",
|
|
174
|
+
osName: osNameFromRuntime(target.runtime),
|
|
175
|
+
osVersion: osVersionFromRuntime(target.runtime),
|
|
176
|
+
model: target.name,
|
|
177
|
+
})));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return resources;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function renderTemplate(template, resourceId, command) {
|
|
2
|
+
return template
|
|
3
|
+
.replaceAll("{resourceId}", resourceId)
|
|
4
|
+
.replaceAll("{command}", command ?? "");
|
|
5
|
+
}
|
|
6
|
+
export class ShellCommandExecutor {
|
|
7
|
+
commandRunner;
|
|
8
|
+
commandTemplate;
|
|
9
|
+
constructor(commandRunner, commandTemplate) {
|
|
10
|
+
this.commandRunner = commandRunner;
|
|
11
|
+
this.commandTemplate = commandTemplate;
|
|
12
|
+
}
|
|
13
|
+
async execute(resourceId, command) {
|
|
14
|
+
const shellCommand = renderTemplate(this.commandTemplate, resourceId, command);
|
|
15
|
+
const result = await this.commandRunner.run(shellCommand, 30_000);
|
|
16
|
+
if (!result.ok) {
|
|
17
|
+
throw new Error(`debug command failed: ${result.stderr || result.stdout}`);
|
|
18
|
+
}
|
|
19
|
+
return { output: result.stdout.trim() || "ok" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ShellSnapshotProvider {
|
|
23
|
+
commandRunner;
|
|
24
|
+
snapshotTemplate;
|
|
25
|
+
constructor(commandRunner, snapshotTemplate) {
|
|
26
|
+
this.commandRunner = commandRunner;
|
|
27
|
+
this.snapshotTemplate = snapshotTemplate;
|
|
28
|
+
}
|
|
29
|
+
async snapshot(resourceId) {
|
|
30
|
+
const shellCommand = renderTemplate(this.snapshotTemplate, resourceId);
|
|
31
|
+
const result = await this.commandRunner.run(shellCommand, 30_000);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
throw new Error(`snapshot failed: ${result.stderr || result.stdout}`);
|
|
34
|
+
}
|
|
35
|
+
const uri = result.stdout.trim();
|
|
36
|
+
if (!uri) {
|
|
37
|
+
throw new Error("snapshot command returned empty uri");
|
|
38
|
+
}
|
|
39
|
+
return { uri };
|
|
40
|
+
}
|
|
41
|
+
}
|