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,70 @@
|
|
|
1
|
+
export var PlatformType;
|
|
2
|
+
(function (PlatformType) {
|
|
3
|
+
PlatformType["HARMONY"] = "HARMONY";
|
|
4
|
+
PlatformType["ANDROID"] = "ANDROID";
|
|
5
|
+
PlatformType["IOS"] = "IOS";
|
|
6
|
+
PlatformType["WEB"] = "WEB";
|
|
7
|
+
})(PlatformType || (PlatformType = {}));
|
|
8
|
+
export var ResourceStatus;
|
|
9
|
+
(function (ResourceStatus) {
|
|
10
|
+
ResourceStatus["OFFLINE"] = "OFFLINE";
|
|
11
|
+
ResourceStatus["ONLINE"] = "ONLINE";
|
|
12
|
+
ResourceStatus["LEASED"] = "LEASED";
|
|
13
|
+
ResourceStatus["RUNNING"] = "RUNNING";
|
|
14
|
+
ResourceStatus["DEBUGGING"] = "DEBUGGING";
|
|
15
|
+
ResourceStatus["ERROR"] = "ERROR";
|
|
16
|
+
})(ResourceStatus || (ResourceStatus = {}));
|
|
17
|
+
export var LeaseStatus;
|
|
18
|
+
(function (LeaseStatus) {
|
|
19
|
+
LeaseStatus["PENDING"] = "PENDING";
|
|
20
|
+
LeaseStatus["ACTIVE"] = "ACTIVE";
|
|
21
|
+
LeaseStatus["RELEASED"] = "RELEASED";
|
|
22
|
+
LeaseStatus["EXPIRED"] = "EXPIRED";
|
|
23
|
+
LeaseStatus["REJECTED"] = "REJECTED";
|
|
24
|
+
})(LeaseStatus || (LeaseStatus = {}));
|
|
25
|
+
export var SessionStatus;
|
|
26
|
+
(function (SessionStatus) {
|
|
27
|
+
SessionStatus["CREATED"] = "CREATED";
|
|
28
|
+
SessionStatus["RUNNING"] = "RUNNING";
|
|
29
|
+
SessionStatus["CLOSED"] = "CLOSED";
|
|
30
|
+
SessionStatus["FAILED"] = "FAILED";
|
|
31
|
+
})(SessionStatus || (SessionStatus = {}));
|
|
32
|
+
export var TaskStatus;
|
|
33
|
+
(function (TaskStatus) {
|
|
34
|
+
TaskStatus["CREATED"] = "CREATED";
|
|
35
|
+
TaskStatus["DISPATCHED"] = "DISPATCHED";
|
|
36
|
+
TaskStatus["RUNNING"] = "RUNNING";
|
|
37
|
+
TaskStatus["SUCCESS"] = "SUCCESS";
|
|
38
|
+
TaskStatus["FAILED"] = "FAILED";
|
|
39
|
+
TaskStatus["CANCELLED"] = "CANCELLED";
|
|
40
|
+
})(TaskStatus || (TaskStatus = {}));
|
|
41
|
+
export var ArtifactType;
|
|
42
|
+
(function (ArtifactType) {
|
|
43
|
+
ArtifactType["LOG"] = "LOG";
|
|
44
|
+
ArtifactType["TRACE"] = "TRACE";
|
|
45
|
+
ArtifactType["SCREENSHOT"] = "SCREENSHOT";
|
|
46
|
+
ArtifactType["VIDEO"] = "VIDEO";
|
|
47
|
+
ArtifactType["REPORT"] = "REPORT";
|
|
48
|
+
})(ArtifactType || (ArtifactType = {}));
|
|
49
|
+
export var EventType;
|
|
50
|
+
(function (EventType) {
|
|
51
|
+
EventType["AGENT_REGISTERED"] = "AgentRegisteredEvent";
|
|
52
|
+
EventType["HEARTBEAT"] = "HeartbeatEvent";
|
|
53
|
+
EventType["RESOURCE_CHANGED"] = "ResourceChangedEvent";
|
|
54
|
+
EventType["LEASE_CHANGED"] = "LeaseChangedEvent";
|
|
55
|
+
EventType["SESSION_CHANGED"] = "SessionChangedEvent";
|
|
56
|
+
EventType["LIVE_DEBUG_SESSION_CHANGED"] = "LiveDebugSessionChangedEvent";
|
|
57
|
+
EventType["LIVE_DEBUG_FRAME"] = "LiveDebugFrameEvent";
|
|
58
|
+
EventType["LIVE_DEBUG_INPUT_ACK"] = "LiveDebugInputAckEvent";
|
|
59
|
+
EventType["TASK_UPDATED"] = "TaskUpdatedEvent";
|
|
60
|
+
EventType["ARTIFACT_READY"] = "ArtifactReadyEvent";
|
|
61
|
+
EventType["HEALTH_WARNING"] = "HealthWarningEvent";
|
|
62
|
+
/** 本机 http(s) 安装包 URL 缓存列表变化(新增下载、启动时索引加载、幽灵文件清理等) */
|
|
63
|
+
EventType["APP_PACKAGE_CACHE_CHANGED"] = "AppPackageCacheChangedEvent";
|
|
64
|
+
})(EventType || (EventType = {}));
|
|
65
|
+
export var OwnerType;
|
|
66
|
+
(function (OwnerType) {
|
|
67
|
+
OwnerType["PLATFORM_TASK"] = "PLATFORM_TASK";
|
|
68
|
+
OwnerType["DEBUG_CLIENT"] = "DEBUG_CLIENT";
|
|
69
|
+
OwnerType["SYSTEM_MAINTENANCE"] = "SYSTEM_MAINTENANCE";
|
|
70
|
+
})(OwnerType || (OwnerType = {}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class DomainError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "DomainError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class LeaseRequiredError extends DomainError {
|
|
8
|
+
constructor(resourceId) {
|
|
9
|
+
super(`resource ${resourceId} requires active lease`);
|
|
10
|
+
this.name = "LeaseRequiredError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** 设备已被其他租约占用,无法再 Acquire */
|
|
14
|
+
export class LeaseConflictError extends DomainError {
|
|
15
|
+
resourceId;
|
|
16
|
+
constructor(resourceId) {
|
|
17
|
+
super(`设备 ${resourceId} 已被占用(已有租约)`);
|
|
18
|
+
this.resourceId = resourceId;
|
|
19
|
+
this.name = "LeaseConflictError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function asAgentId(value) {
|
|
2
|
+
return value;
|
|
3
|
+
}
|
|
4
|
+
export function asResourceId(value) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
export function asLeaseId(value) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function asSessionId(value) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function asTaskId(value) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function asArtifactId(value) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { access, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { constants as FsConstants } from "node:fs";
|
|
7
|
+
import { PlatformType } from "../shared-kernel/enums/index.js";
|
|
8
|
+
function isProbablyHttpUrl(ref) {
|
|
9
|
+
return /^https?:\/\//i.test(ref.trim());
|
|
10
|
+
}
|
|
11
|
+
function extensionForPlatform(platform) {
|
|
12
|
+
switch (platform) {
|
|
13
|
+
case PlatformType.ANDROID:
|
|
14
|
+
return ".apk";
|
|
15
|
+
case PlatformType.IOS:
|
|
16
|
+
return ".ipa";
|
|
17
|
+
case PlatformType.HARMONY:
|
|
18
|
+
return ".hap";
|
|
19
|
+
default:
|
|
20
|
+
return ".pkg";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function guessDownloadFilename(sourceUrl, platform) {
|
|
24
|
+
try {
|
|
25
|
+
const u = new URL(sourceUrl);
|
|
26
|
+
const base = u.pathname.split("/").pop() ?? "";
|
|
27
|
+
const m = /\.(apk|ipa|hap)$/i.exec(base);
|
|
28
|
+
if (m)
|
|
29
|
+
return `download${m[0].toLowerCase()}`;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
return `download${extensionForPlatform(platform)}`;
|
|
35
|
+
}
|
|
36
|
+
async function ensureDownloadParent(dir) {
|
|
37
|
+
await mkdir(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* - `http(s)://`:下载到临时目录(文件名尽量取自 URL 后缀)
|
|
41
|
+
* - `file:///path`:转为本地路径
|
|
42
|
+
* - 其它:视为本地路径(须已存在)
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveAppRefToLocalFile(appRef, platform, downloadDir) {
|
|
45
|
+
const raw = appRef.trim();
|
|
46
|
+
if (!raw) {
|
|
47
|
+
throw new Error("appRef is empty");
|
|
48
|
+
}
|
|
49
|
+
if (isProbablyHttpUrl(raw)) {
|
|
50
|
+
const parent = (downloadDir?.trim() || tmpdir()).replace(/\/+$/, "");
|
|
51
|
+
await ensureDownloadParent(parent);
|
|
52
|
+
const name = `${Date.now()}-${randomBytes(8).toString("hex")}-${guessDownloadFilename(raw, platform)}`;
|
|
53
|
+
const dest = path.join(parent, name);
|
|
54
|
+
const res = await fetch(raw, { redirect: "follow" });
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(`download failed: HTTP ${res.status} ${res.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
59
|
+
await writeFile(dest, buf);
|
|
60
|
+
return {
|
|
61
|
+
localPath: dest,
|
|
62
|
+
cleanupTemp: async () => {
|
|
63
|
+
await rm(dest, { force: true });
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const local = raw.startsWith("file://") ?
|
|
68
|
+
fileURLToPath(raw)
|
|
69
|
+
: path.resolve(raw);
|
|
70
|
+
await access(local, FsConstants.R_OK);
|
|
71
|
+
return {
|
|
72
|
+
localPath: local,
|
|
73
|
+
cleanupTemp: async () => { },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PlatformType } from "../shared-kernel/enums/index.js";
|
|
2
|
+
/** 与 ResourceAdapter 生成的 `android:` / `ios:` / `harmony:` 前缀一致 */
|
|
3
|
+
export function parseDeviceTargetFromResourceId(resourceId) {
|
|
4
|
+
const id = resourceId.trim();
|
|
5
|
+
if (id.startsWith("android:")) {
|
|
6
|
+
const serial = id.slice("android:".length).trim();
|
|
7
|
+
if (!serial)
|
|
8
|
+
return null;
|
|
9
|
+
return { platform: PlatformType.ANDROID, serial };
|
|
10
|
+
}
|
|
11
|
+
if (id.startsWith("ios:")) {
|
|
12
|
+
const udid = id.slice("ios:".length).trim();
|
|
13
|
+
if (!udid)
|
|
14
|
+
return null;
|
|
15
|
+
return { platform: PlatformType.IOS, udid };
|
|
16
|
+
}
|
|
17
|
+
if (id.startsWith("harmony:")) {
|
|
18
|
+
const deviceId = id.slice("harmony:".length).trim();
|
|
19
|
+
if (!deviceId)
|
|
20
|
+
return null;
|
|
21
|
+
return { platform: PlatformType.HARMONY, deviceId };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { HarmonyDevice, getConnectedDevices } from "@midscene/harmony";
|
|
4
|
+
import { hdcListTargetsSimpleLines, hdcListTargetsVerboseStdout, looksLikeHdcTcpConnectKey, pickHarmonyDeviceIdFromList, resolveHarmonyDeviceIdFromVerboseList, resolveHdcCliExecutable, } from "./harmonyHdcDeviceId.js";
|
|
5
|
+
/** 与 midscene-device-session 一致:远程 hdc 时 Midscene 执行该脚本以注入 `hdc -s host:port`。 */
|
|
6
|
+
export function getHarmonyHdcBridgeScriptPath() {
|
|
7
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts", "hdc-bridge.sh");
|
|
8
|
+
}
|
|
9
|
+
export function extractHarmonyDeviceIdFromResourceId(resourceId) {
|
|
10
|
+
const raw = String(resourceId).trim();
|
|
11
|
+
const idx = raw.indexOf(":");
|
|
12
|
+
return idx >= 0 ? raw.slice(idx + 1).trim() : raw;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 创建并 connect 与任务执行(createHarmonySession)一致的 HarmonyDevice,供实时看屏 / 点击。
|
|
16
|
+
*/
|
|
17
|
+
export async function connectHarmonyAgentDebugDevice(params) {
|
|
18
|
+
const hdcPath = params.harmonyHdcPath?.trim();
|
|
19
|
+
let deviceId = extractHarmonyDeviceIdFromResourceId(params.resourceId);
|
|
20
|
+
const hasRemoteTconn = typeof params.harmonyHdcHost === "string" &&
|
|
21
|
+
params.harmonyHdcHost.trim() &&
|
|
22
|
+
typeof params.harmonyHdcPort === "number" &&
|
|
23
|
+
Number.isFinite(params.harmonyHdcPort) &&
|
|
24
|
+
params.harmonyHdcPort > 0;
|
|
25
|
+
if (hasRemoteTconn) {
|
|
26
|
+
const host = params.harmonyHdcHost.trim();
|
|
27
|
+
const port = params.harmonyHdcPort;
|
|
28
|
+
const server = { host, port };
|
|
29
|
+
const hdcBin = resolveHdcCliExecutable(hdcPath);
|
|
30
|
+
const remoteLines = await hdcListTargetsSimpleLines(hdcBin, 20_000, server);
|
|
31
|
+
let resolved = pickHarmonyDeviceIdFromList(remoteLines, deviceId || undefined);
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
const verbose = await hdcListTargetsVerboseStdout(hdcBin, 20_000, server);
|
|
34
|
+
resolved = resolveHarmonyDeviceIdFromVerboseList(verbose, host, port, deviceId || undefined);
|
|
35
|
+
}
|
|
36
|
+
if (!resolved) {
|
|
37
|
+
throw new Error(`鸿蒙实时看屏:hdc -s ${host}:${port} list targets 未解析到设备序列号。请核对 hdc 与 MIDSCENE_HARMONY_HDC_HOST/PORT,并在设备管理选择 harmony: 序列号。`);
|
|
38
|
+
}
|
|
39
|
+
deviceId = resolved;
|
|
40
|
+
process.env.HDC_S = `${host}:${port}`;
|
|
41
|
+
process.env.HDC_REAL = hdcBin;
|
|
42
|
+
}
|
|
43
|
+
else if (!deviceId) {
|
|
44
|
+
const devices = await getConnectedDevices(hdcPath);
|
|
45
|
+
if (!devices.length) {
|
|
46
|
+
throw new Error("鸿蒙实时看屏:没有可用鸿蒙设备(hdc list targets 为空)");
|
|
47
|
+
}
|
|
48
|
+
deviceId = devices[0].deviceId;
|
|
49
|
+
}
|
|
50
|
+
if (deviceId && looksLikeHdcTcpConnectKey(deviceId)) {
|
|
51
|
+
throw new Error("鸿蒙设备 ID 不能为 ip:port 连接串;请在设备管理选择序列号,或配置 MIDSCENE_HARMONY_HDC_HOST 与 MIDSCENE_HARMONY_HDC_PORT。");
|
|
52
|
+
}
|
|
53
|
+
const realHdc = resolveHdcCliExecutable(hdcPath);
|
|
54
|
+
const hdcPathForMidscene = hasRemoteTconn ? getHarmonyHdcBridgeScriptPath() : hdcPath ? realHdc : undefined;
|
|
55
|
+
const device = new HarmonyDevice(deviceId, {
|
|
56
|
+
...(hdcPathForMidscene ? { hdcPath: hdcPathForMidscene } : {}),
|
|
57
|
+
});
|
|
58
|
+
await device.connect();
|
|
59
|
+
return device;
|
|
60
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* hdc 简单 `list targets` 可能把 TCP 连接键单独成行(如 192.168.1.1:8710),
|
|
6
|
+
* 不能作为 @midscene/harmony 里 HdcClient 的 deviceId(需为序列号等真实 target)。
|
|
7
|
+
*/
|
|
8
|
+
export function looksLikeHdcTcpConnectKey(id) {
|
|
9
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(id.trim());
|
|
10
|
+
}
|
|
11
|
+
/** 与 Midscene 一致:可用完整 hdc 路径或依赖 PATH 上的 `hdc` */
|
|
12
|
+
export function resolveHdcCliExecutable(hdcPath) {
|
|
13
|
+
const p = typeof hdcPath === 'string' ? hdcPath.trim() : '';
|
|
14
|
+
return p || 'hdc';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 远程设备:与命令行 `hdc -s 172.23.x.x:8710 list targets` 一致(OpenHarmony hdc 会话参数 `-s`)。
|
|
18
|
+
* 不同于仅 `tconn`:部分环境必须用 `-s` 才能列出/操作该 TCP 上的设备。
|
|
19
|
+
*/
|
|
20
|
+
export function hdcRemoteServerPrefixArgs(host, port) {
|
|
21
|
+
return ['-s', `${host.trim()}:${port}`];
|
|
22
|
+
}
|
|
23
|
+
export function parseHdcListTargetsStdout(stdout) {
|
|
24
|
+
return stdout
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter((line) => line.length > 0 && !line.startsWith('['));
|
|
28
|
+
}
|
|
29
|
+
export async function hdcListTargetsSimpleLines(hdcExecutable = 'hdc', timeoutMs = 20000, server) {
|
|
30
|
+
const tail = ['list', 'targets'];
|
|
31
|
+
const argv = server ? [...hdcRemoteServerPrefixArgs(server.host, server.port), ...tail] : [...tail];
|
|
32
|
+
const { stdout } = await execFileAsync(hdcExecutable, argv, {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
timeout: timeoutMs,
|
|
35
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
return parseHdcListTargetsStdout(stdout);
|
|
38
|
+
}
|
|
39
|
+
export async function hdcListTargetsVerboseStdout(hdcExecutable = 'hdc', timeoutMs = 20000, server) {
|
|
40
|
+
const tail = ['list', 'targets', '-v'];
|
|
41
|
+
const argv = server ? [...hdcRemoteServerPrefixArgs(server.host, server.port), ...tail] : [...tail];
|
|
42
|
+
const { stdout } = await execFileAsync(hdcExecutable, argv, {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: timeoutMs,
|
|
45
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
46
|
+
});
|
|
47
|
+
return stdout;
|
|
48
|
+
}
|
|
49
|
+
/** 远程 `hdc -s ip:port list targets` 下列出的序列号(或本地 list)中选取设备 ID */
|
|
50
|
+
export function pickHarmonyDeviceIdFromList(lineIds, preferredSerial) {
|
|
51
|
+
const cleaned = lineIds.map((l) => l.trim()).filter((l) => l.length > 0 && !looksLikeHdcTcpConnectKey(l));
|
|
52
|
+
const pref = preferredSerial?.trim();
|
|
53
|
+
if (pref && !looksLikeHdcTcpConnectKey(pref) && cleaned.includes(pref))
|
|
54
|
+
return pref;
|
|
55
|
+
if (cleaned.length === 1)
|
|
56
|
+
return cleaned[0];
|
|
57
|
+
if (cleaned.length > 1 && pref && cleaned.includes(pref))
|
|
58
|
+
return pref;
|
|
59
|
+
if (cleaned.length > 0)
|
|
60
|
+
return cleaned[0];
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function splitVerboseLines(verboseStdout) {
|
|
64
|
+
return verboseStdout
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((l) => l.trim())
|
|
67
|
+
.filter((l) => l.length > 0 && !l.startsWith('['));
|
|
68
|
+
}
|
|
69
|
+
/** 部分 hdc 版本在 -v 中不重复打印 IP,仅标 TCP —— 用行内 TCP 标记兜底 */
|
|
70
|
+
function collectVerboseFirstColumnFromTcpLines(verboseStdout) {
|
|
71
|
+
const lines = splitVerboseLines(verboseStdout);
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (!/\bTCP\b/i.test(line))
|
|
76
|
+
continue;
|
|
77
|
+
const first = line.split(/\t+/)[0]?.trim();
|
|
78
|
+
if (first && !looksLikeHdcTcpConnectKey(first) && !seen.has(first)) {
|
|
79
|
+
seen.add(first);
|
|
80
|
+
out.push(first);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 从 `hdc list targets -v` 输出中解析与某次 tconn(host:port)相关的设备序列号。
|
|
87
|
+
*/
|
|
88
|
+
export function resolveHarmonyDeviceIdFromVerboseList(verboseStdout, hostIp, hdcPort, preferredDeviceId) {
|
|
89
|
+
const lines = splitVerboseLines(verboseStdout);
|
|
90
|
+
const tcpKey = `${hostIp}:${hdcPort}`;
|
|
91
|
+
if (preferredDeviceId && !looksLikeHdcTcpConnectKey(preferredDeviceId)) {
|
|
92
|
+
const pref = preferredDeviceId.trim();
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const first = line.split(/\t+/)[0]?.trim();
|
|
95
|
+
if (first === pref)
|
|
96
|
+
return pref;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (!line.includes(hostIp) && !line.includes(tcpKey))
|
|
101
|
+
continue;
|
|
102
|
+
const first = line.split(/\t+/)[0]?.trim();
|
|
103
|
+
if (first && !looksLikeHdcTcpConnectKey(first))
|
|
104
|
+
return first;
|
|
105
|
+
}
|
|
106
|
+
const loose = collectVerboseFirstColumnFromTcpLines(verboseStdout);
|
|
107
|
+
if (loose.length === 1)
|
|
108
|
+
return loose[0];
|
|
109
|
+
if (preferredDeviceId && !looksLikeHdcTcpConnectKey(preferredDeviceId)) {
|
|
110
|
+
const pref = preferredDeviceId.trim();
|
|
111
|
+
if (loose.includes(pref))
|
|
112
|
+
return pref;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** 探测列表:同一主机下可能多台 */
|
|
117
|
+
export function listHarmonyDeviceIdsFromVerboseForHost(verboseStdout, hostIp, hdcPort) {
|
|
118
|
+
const lines = splitVerboseLines(verboseStdout);
|
|
119
|
+
const tcpKey = `${hostIp}:${hdcPort}`;
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (!line.includes(hostIp) && !line.includes(tcpKey))
|
|
124
|
+
continue;
|
|
125
|
+
const first = line.split(/\t+/)[0]?.trim();
|
|
126
|
+
if (first && !looksLikeHdcTcpConnectKey(first) && !seen.has(first)) {
|
|
127
|
+
seen.add(first);
|
|
128
|
+
out.push(first);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (out.length > 0)
|
|
132
|
+
return out;
|
|
133
|
+
return collectVerboseFirstColumnFromTcpLines(verboseStdout);
|
|
134
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export function extractIosUdidFromResourceId(resourceId) {
|
|
3
|
+
const raw = String(resourceId).trim();
|
|
4
|
+
const idx = raw.indexOf(":");
|
|
5
|
+
return idx >= 0 ? raw.slice(idx + 1).trim() : raw;
|
|
6
|
+
}
|
|
7
|
+
export function normalizeIosWdaPortRange(start, end) {
|
|
8
|
+
const s = Number.isFinite(start) && start > 0 ? Math.floor(start) : 8200;
|
|
9
|
+
const e = Number.isFinite(end) && end > 0 ? Math.floor(end) : 8399;
|
|
10
|
+
return s <= e ? { start: s, end: e } : { start: e, end: s };
|
|
11
|
+
}
|
|
12
|
+
export async function readMappedWdaPortForUdid(mapFilePath, udid) {
|
|
13
|
+
const path = mapFilePath?.trim();
|
|
14
|
+
if (!path)
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const raw = await readFile(path, "utf8");
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
const portRaw = parsed.portsByUdid?.[udid];
|
|
20
|
+
const port = Number(portRaw);
|
|
21
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
return Math.floor(port);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function isWdaHealthy(commandRunner, wdaHost, wdaPort, timeoutMs = 2_000) {
|
|
30
|
+
const host = wdaHost.trim() || "127.0.0.1";
|
|
31
|
+
const url = `http://${host}:${wdaPort}/status`;
|
|
32
|
+
const curlTimeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1_000));
|
|
33
|
+
const command = `curl -fsS --max-time ${curlTimeoutSeconds} ${JSON.stringify(url)}`;
|
|
34
|
+
const result = await commandRunner.run(command, timeoutMs + 500);
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
return false;
|
|
37
|
+
return result.stdout.trim().length > 0;
|
|
38
|
+
}
|
|
39
|
+
async function findFirstHealthyWdaPortInRange(commandRunner, wdaHost, rangeStart, rangeEnd) {
|
|
40
|
+
const { start, end } = normalizeIosWdaPortRange(rangeStart, rangeEnd);
|
|
41
|
+
for (let port = start; port <= end; port += 1) {
|
|
42
|
+
if (await isWdaHealthy(commandRunner, wdaHost, port, 300)) {
|
|
43
|
+
return port;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 解析实时看屏可用的 WDA 端口(与 MidsceneRuntimeReal.prepareIosSession 优先级对齐:显式端口 → 映射文件 → 区间扫描)。
|
|
50
|
+
* 不在此函数内启动 WDA;需先由 watchdog / 任务 / 手动拉起 WDA。
|
|
51
|
+
*/
|
|
52
|
+
export async function resolveIosWdaPortForLiveDebug(commandRunner, params) {
|
|
53
|
+
const wdaHost = params.wdaHost.trim() || "127.0.0.1";
|
|
54
|
+
const udid = extractIosUdidFromResourceId(params.resourceId);
|
|
55
|
+
const explicit = params.explicitWdaPort;
|
|
56
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
57
|
+
const p = Math.floor(explicit);
|
|
58
|
+
if (await isWdaHealthy(commandRunner, wdaHost, p))
|
|
59
|
+
return p;
|
|
60
|
+
throw new Error(`iOS 实时看屏:环境变量指定的 WDA 端口 ${p} 不可用(${wdaHost})。请检查 WDA 是否已启动或端口是否正确。`);
|
|
61
|
+
}
|
|
62
|
+
const mapped = await readMappedWdaPortForUdid(params.portMapFilePath, udid);
|
|
63
|
+
if (mapped != null) {
|
|
64
|
+
if (await isWdaHealthy(commandRunner, wdaHost, mapped))
|
|
65
|
+
return mapped;
|
|
66
|
+
throw new Error(`iOS 实时看屏:端口映射中 ${udid} 对应端口 ${mapped} 的 WDA 不可访问(${wdaHost})。请检查 WDA 或 IOS_WDA_WATCHDOG,勿在未映射时使用区间扫描以免连错设备。`);
|
|
67
|
+
}
|
|
68
|
+
const scanned = await findFirstHealthyWdaPortInRange(commandRunner, wdaHost, params.portRangeStart, params.portRangeEnd);
|
|
69
|
+
if (scanned != null)
|
|
70
|
+
return scanned;
|
|
71
|
+
throw new Error(`iOS 实时看屏:未在 ${wdaHost} 上找到可用 WDA(端口区间 ${params.portRangeStart}-${params.portRangeEnd})。请启动 WDA、配置 IOS_WDA_PORT_MAP_FILE_PATH,或设置 MIDSCENE_IOS_WDA_PORT。`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
/** 从已缓冲的字节中截取第一张 JPEG(SOI … EOI) */
|
|
4
|
+
export function extractFirstJpegFromBuffer(buf) {
|
|
5
|
+
const soi = buf.indexOf(Buffer.from([0xff, 0xd8]));
|
|
6
|
+
if (soi === -1)
|
|
7
|
+
return null;
|
|
8
|
+
const eoi = buf.indexOf(Buffer.from([0xff, 0xd9]), soi + 2);
|
|
9
|
+
if (eoi === -1)
|
|
10
|
+
return null;
|
|
11
|
+
return buf.subarray(soi, eoi + 2);
|
|
12
|
+
}
|
|
13
|
+
function mjpegStreamUrl(wdaHost, mjpegPort) {
|
|
14
|
+
const h = wdaHost.trim() || "127.0.0.1";
|
|
15
|
+
const hostForUrl = h.includes(":") && !h.startsWith("[") ? `[${h}]` : h;
|
|
16
|
+
return `http://${hostForUrl}:${mjpegPort}/`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 从 WDA MJPEG 服务(默认 9100,与 WebDriver 会话端口独立)读取流直到拼出第一张 JPEG。
|
|
20
|
+
* 不创建 WebDriver 会话,可与 Midscene 任务子进程并行,避免顶掉任务 session。
|
|
21
|
+
*/
|
|
22
|
+
export async function captureFirstJpegFromWdaMjpegStream(wdaHost, mjpegPort, options) {
|
|
23
|
+
const timeoutMs = options?.timeoutMs ?? 8_000;
|
|
24
|
+
const maxBytes = options?.maxBytes ?? 6 * 1024 * 1024;
|
|
25
|
+
const urlString = mjpegStreamUrl(wdaHost, mjpegPort);
|
|
26
|
+
const u = new URL(urlString);
|
|
27
|
+
const lib = u.protocol === "https:" ? https : http;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = lib.request({
|
|
30
|
+
hostname: u.hostname,
|
|
31
|
+
port: u.port || 80,
|
|
32
|
+
path: `${u.pathname}${u.search}` || "/",
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers: { Connection: "close", Accept: "*/*" },
|
|
35
|
+
timeout: timeoutMs,
|
|
36
|
+
}, (res) => {
|
|
37
|
+
if (res.statusCode != null && res.statusCode >= 400) {
|
|
38
|
+
res.resume();
|
|
39
|
+
reject(new Error(`MJPEG HTTP ${res.statusCode}`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let total = 0;
|
|
44
|
+
let settled = false;
|
|
45
|
+
const settle = (fn) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
try {
|
|
50
|
+
res.destroy();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* ignore */
|
|
54
|
+
}
|
|
55
|
+
fn();
|
|
56
|
+
};
|
|
57
|
+
res.on("data", (chunk) => {
|
|
58
|
+
if (settled)
|
|
59
|
+
return;
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
total += chunk.length;
|
|
62
|
+
if (total > maxBytes) {
|
|
63
|
+
settle(() => reject(new Error("MJPEG: exceeded maxBytes before first JPEG")));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const buf = Buffer.concat(chunks, total);
|
|
67
|
+
const jpeg = extractFirstJpegFromBuffer(buf);
|
|
68
|
+
if (jpeg) {
|
|
69
|
+
settle(() => resolve(jpeg));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
res.on("end", () => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settle(() => reject(new Error("MJPEG: stream ended before first complete JPEG")));
|
|
76
|
+
});
|
|
77
|
+
res.on("error", (err) => {
|
|
78
|
+
settle(() => reject(err instanceof Error ? err : new Error(String(err))));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.on("error", (err) => {
|
|
82
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
83
|
+
});
|
|
84
|
+
req.on("timeout", () => {
|
|
85
|
+
req.destroy();
|
|
86
|
+
reject(new Error("MJPEG: request timeout"));
|
|
87
|
+
});
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 `adb shell dumpsys activity …` / `dumpsys window …` 等**小片段** stdout 解析当前前台包名(Android 包名写入 `bundleId` 字段以与 iOS 对齐)。
|
|
3
|
+
*/
|
|
4
|
+
export function parseAndroidForegroundFromDumpsys(text) {
|
|
5
|
+
const lines = text.split("\n");
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
if (!line.includes("ResumedActivity") && !line.includes("resumed"))
|
|
8
|
+
continue;
|
|
9
|
+
const m = line.match(/ActivityRecord\{[^ ]+\s+u\d+\s+([^/\s]+)\//);
|
|
10
|
+
if (m?.[1])
|
|
11
|
+
return { bundleId: m[1] };
|
|
12
|
+
}
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (!line.includes("mCurrentFocus"))
|
|
15
|
+
continue;
|
|
16
|
+
const m = line.match(/mCurrentFocus=Window\{[^}]+\s(?:u0|u\d+)\s+([^/\s}]+)\//);
|
|
17
|
+
if (m?.[1])
|
|
18
|
+
return { bundleId: m[1] };
|
|
19
|
+
}
|
|
20
|
+
const act = text.match(/^\s*ACTIVITY\s+([A-Za-z0-9_.]+)\//m);
|
|
21
|
+
if (act?.[1])
|
|
22
|
+
return { bundleId: act[1] };
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function pickHarmonyBundleFromChunk(chunk) {
|
|
26
|
+
const bundle = chunk.match(/"bundleName"\s*:\s*"([^"]+)"/)?.[1] ??
|
|
27
|
+
chunk.match(/\bbundleName\s*=\s*"([^"]+)"/)?.[1] ??
|
|
28
|
+
chunk.match(/\bbundleName\s*[:=]\s*([A-Za-z0-9_.]+)\b/i)?.[1];
|
|
29
|
+
if (!bundle)
|
|
30
|
+
return undefined;
|
|
31
|
+
const name = chunk.match(/"appName"\s*:\s*"([^"]+)"/)?.[1] ??
|
|
32
|
+
chunk.match(/\bappName\s*=\s*"([^"]+)"/)?.[1] ??
|
|
33
|
+
chunk.match(/\bappName\s*[:=]\s*([^\r\n]+)/i)?.[1]?.trim();
|
|
34
|
+
const pidM = chunk.match(/\b(?:pid|processId)\s*[:=]\s*(\d{2,8})\b/i);
|
|
35
|
+
const pid = pidM?.[1] ? Number.parseInt(pidM[1], 10) : undefined;
|
|
36
|
+
const out = { bundleId: bundle };
|
|
37
|
+
if (name)
|
|
38
|
+
out.name = name.slice(0, 400);
|
|
39
|
+
if (pid !== undefined && Number.isFinite(pid))
|
|
40
|
+
out.pid = pid;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 从鸿蒙 `aa dump` / `hidumper` 等 stdout 解析前台 bundle(写入 `bundleId`)。
|
|
45
|
+
*/
|
|
46
|
+
export function parseHarmonyForegroundFromShellDump(text) {
|
|
47
|
+
const t = text.length > 800_000 ? text.slice(0, 800_000) : text;
|
|
48
|
+
const fgIter = t.matchAll(/\b(?:FOREGROUND|STATE_FOREGROUND)\b/gi);
|
|
49
|
+
for (const m of fgIter) {
|
|
50
|
+
const pos = m.index ?? 0;
|
|
51
|
+
const chunk = t.slice(Math.max(0, pos - 2500), Math.min(t.length, pos + 2500));
|
|
52
|
+
const hit = pickHarmonyBundleFromChunk(chunk);
|
|
53
|
+
if (hit)
|
|
54
|
+
return hit;
|
|
55
|
+
}
|
|
56
|
+
const focusIdx = t.search(/(?:Focus|focus)(?:ed)?Window/i);
|
|
57
|
+
if (focusIdx >= 0) {
|
|
58
|
+
const w = t.slice(focusIdx, focusIdx + 1400);
|
|
59
|
+
const bundle = w.match(/"bundleName"\s*:\s*"([^"]+)"/)?.[1] ??
|
|
60
|
+
w.match(/\bbundleName\s*=\s*"([^"]+)"/)?.[1];
|
|
61
|
+
if (bundle) {
|
|
62
|
+
const name = w.match(/"appName"\s*:\s*"([^"]+)"/)?.[1] ?? w.match(/\bappName\s*=\s*"([^"]+)"/)?.[1];
|
|
63
|
+
return name ? { bundleId: bundle, name: name.slice(0, 400) } : { bundleId: bundle };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const top = t.match(/\btop(?:Mission|Ability)[^\n\r]{0,240}?(?:bundleName|bundle)\s*[:=]\s*\[?([A-Za-z0-9_.]+)\]?/i)?.[1] ??
|
|
67
|
+
t.match(/\bmain(?:Ability)?BundleName\s*[:=]\s*\[?([A-Za-z0-9_.]+)\]?/i)?.[1];
|
|
68
|
+
if (top)
|
|
69
|
+
return { bundleId: top };
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|