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,378 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { TaskRecord } from "../../domain/task/TaskRecord.js";
|
|
3
|
+
import { TaskSpec } from "../../domain/task/TaskSpec.js";
|
|
4
|
+
import { EventType, OwnerType, ResourceStatus, TaskStatus } from "../../shared-kernel/enums/index.js";
|
|
5
|
+
import { asResourceId, asTaskId } from "../../shared-kernel/ids/index.js";
|
|
6
|
+
import { LeaseRequiredError } from "../../shared-kernel/errors/index.js";
|
|
7
|
+
import { buildMidsceneReportStemForTask } from "../../utils/midsceneReportConstants.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { readMidsceneTaskCacheFileIfExists, seedMidsceneTaskCacheFromRuntimeEnv } from "../../utils/seedMidsceneTaskCache.js";
|
|
10
|
+
const TASK_AUTO_LEASE_TTL_SECONDS = 120;
|
|
11
|
+
function normalizeRuntimeEnv(raw) {
|
|
12
|
+
if (!raw)
|
|
13
|
+
return undefined;
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
16
|
+
if (!key.startsWith("MIDSCENE_"))
|
|
17
|
+
continue;
|
|
18
|
+
const v = String(value ?? "").trim();
|
|
19
|
+
if (!v)
|
|
20
|
+
continue;
|
|
21
|
+
out[key] = v;
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
24
|
+
}
|
|
25
|
+
export class TaskApplicationService {
|
|
26
|
+
taskRepository;
|
|
27
|
+
resourceRepository;
|
|
28
|
+
leaseService;
|
|
29
|
+
sessionService;
|
|
30
|
+
artifactService;
|
|
31
|
+
reporter;
|
|
32
|
+
midsceneRuntime;
|
|
33
|
+
callbackClient;
|
|
34
|
+
selfAgentId;
|
|
35
|
+
taskAgentHttpBaseUrl;
|
|
36
|
+
taskAgentHttpToken;
|
|
37
|
+
runningAbortControllers = new Map();
|
|
38
|
+
resourceExecutionChains = new Map();
|
|
39
|
+
toTaskFailureMessage(error) {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
async ensureTaskLease(resourceId, taskId) {
|
|
43
|
+
try {
|
|
44
|
+
return await this.leaseService.ensureActive(asResourceId(resourceId));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!(error instanceof LeaseRequiredError))
|
|
48
|
+
throw error;
|
|
49
|
+
const leaseId = `lease-task-${String(taskId)}-${Date.now()}`;
|
|
50
|
+
console.info(`[TaskApplicationService] auto acquiring lease taskId=${String(taskId)} resourceId=${resourceId} leaseId=${leaseId}`);
|
|
51
|
+
return this.leaseService.acquire(leaseId, resourceId, String(taskId), OwnerType.PLATFORM_TASK, TASK_AUTO_LEASE_TTL_SECONDS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
isResourceBusy(resourceId) {
|
|
55
|
+
return this.resourceExecutionChains.has(resourceId);
|
|
56
|
+
}
|
|
57
|
+
async enqueueOnResource(resourceId, run) {
|
|
58
|
+
const previous = this.resourceExecutionChains.get(resourceId) ?? Promise.resolve();
|
|
59
|
+
const next = previous
|
|
60
|
+
.catch(() => { })
|
|
61
|
+
.then(run)
|
|
62
|
+
.finally(() => {
|
|
63
|
+
if (this.resourceExecutionChains.get(resourceId) === next) {
|
|
64
|
+
this.resourceExecutionChains.delete(resourceId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.resourceExecutionChains.set(resourceId, next);
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
constructor(taskRepository, resourceRepository, leaseService, sessionService, artifactService, reporter, midsceneRuntime, callbackClient, selfAgentId,
|
|
71
|
+
/** 任务子进程内 `installApp`/`uninstallApp` 调本机 Agent HTTP 的根地址(无尾斜杠) */
|
|
72
|
+
taskAgentHttpBaseUrl,
|
|
73
|
+
/** 可选;未设则子进程沿用父进程 `AGENT_HTTP_TOKEN` */
|
|
74
|
+
taskAgentHttpToken) {
|
|
75
|
+
this.taskRepository = taskRepository;
|
|
76
|
+
this.resourceRepository = resourceRepository;
|
|
77
|
+
this.leaseService = leaseService;
|
|
78
|
+
this.sessionService = sessionService;
|
|
79
|
+
this.artifactService = artifactService;
|
|
80
|
+
this.reporter = reporter;
|
|
81
|
+
this.midsceneRuntime = midsceneRuntime;
|
|
82
|
+
this.callbackClient = callbackClient;
|
|
83
|
+
this.selfAgentId = selfAgentId;
|
|
84
|
+
this.taskAgentHttpBaseUrl = taskAgentHttpBaseUrl;
|
|
85
|
+
this.taskAgentHttpToken = taskAgentHttpToken;
|
|
86
|
+
}
|
|
87
|
+
buildChildMidsceneEnv(params, spec, taskId) {
|
|
88
|
+
const base = normalizeRuntimeEnv(params.runtimeEnv) ?? {};
|
|
89
|
+
const out = { ...base };
|
|
90
|
+
out.MIDSCENE_FLOW_TASK_ID = String(taskId);
|
|
91
|
+
out.MIDSCENE_FLOW_REPORT_STEM = buildMidsceneReportStemForTask(spec.requiredPlatform, String(taskId));
|
|
92
|
+
// 将父进程的 MIDSCENE_* 中未显式传入的自动继承到子进程
|
|
93
|
+
for (const key of Object.keys(process.env)) {
|
|
94
|
+
if (key.startsWith("MIDSCENE_") && !(key in out) && process.env[key]?.trim()) {
|
|
95
|
+
out[key] = process.env[key].trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!out.MIDSCENE_OUTPUT_FORMAT?.trim()) {
|
|
99
|
+
out.MIDSCENE_OUTPUT_FORMAT = "single-html";
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
async dispatch(params) {
|
|
104
|
+
const midsceneKeyCount = Object.keys(normalizeRuntimeEnv(params.runtimeEnv) ?? {}).length;
|
|
105
|
+
console.info(`[TaskApplicationService] dispatch taskId=${params.taskId} requiredPlatform=${params.requiredPlatform} resourceId=${params.resourceId ?? ""} selector=${JSON.stringify(params.selector ?? null)} scriptLength=${params.script?.length ?? 0} runtimeEnvKeys=${midsceneKeyCount}`);
|
|
106
|
+
const taskId = asTaskId(params.taskId);
|
|
107
|
+
const existing = await this.taskRepository.getById(taskId);
|
|
108
|
+
if (existing) {
|
|
109
|
+
console.warn(`[TaskApplicationService] skip duplicate dispatch taskId=${params.taskId} existingStatus=${existing.status}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const scriptKind = params.scriptKind ?? "midscene";
|
|
113
|
+
const spec = new TaskSpec(params.requiredPlatform, params.script, scriptKind, scriptKind === "airtest" ? {
|
|
114
|
+
bundleBase64: params.airtestBundleBase64 ?? "",
|
|
115
|
+
entryDir: params.airtestEntryDir ?? "",
|
|
116
|
+
archiveName: params.airtestArchiveName,
|
|
117
|
+
caseRunId: params.caseRunId,
|
|
118
|
+
caseIndex: params.caseIndex,
|
|
119
|
+
caseName: params.caseName,
|
|
120
|
+
} : undefined);
|
|
121
|
+
const task = new TaskRecord(taskId, spec, TaskStatus.CREATED);
|
|
122
|
+
await this.taskRepository.save(task);
|
|
123
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: TaskStatus.CREATED });
|
|
124
|
+
await this.callbackClient.pushTaskStatus(String(taskId), {
|
|
125
|
+
agentId: this.selfAgentId,
|
|
126
|
+
status: TaskStatus.CREATED,
|
|
127
|
+
}).catch(() => { });
|
|
128
|
+
const resources = await this.resourceRepository.list();
|
|
129
|
+
const onlineByPlatform = resources.filter((item) => item.platform === params.requiredPlatform && item.status === ResourceStatus.ONLINE);
|
|
130
|
+
const requested = params.resourceId?.trim();
|
|
131
|
+
let candidate = requested != null && requested.length > 0
|
|
132
|
+
? onlineByPlatform.find((item) => item.id === requested)
|
|
133
|
+
: undefined;
|
|
134
|
+
if (!candidate) {
|
|
135
|
+
const wantedLabels = (params.selector?.labels ?? []).map((item) => item.trim()).filter(Boolean);
|
|
136
|
+
const matched = onlineByPlatform.filter((item) => {
|
|
137
|
+
if (!wantedLabels.length)
|
|
138
|
+
return true;
|
|
139
|
+
const labels = item.capability.labels ?? [];
|
|
140
|
+
return wantedLabels.every((label) => labels.includes(label));
|
|
141
|
+
});
|
|
142
|
+
candidate = matched.find((item) => !this.isResourceBusy(item.id)) ?? matched[0];
|
|
143
|
+
}
|
|
144
|
+
if (!candidate) {
|
|
145
|
+
console.warn(`[TaskApplicationService] no candidate taskId=${params.taskId} requestedResourceId=${requested ?? ""} platform=${params.requiredPlatform}`);
|
|
146
|
+
task.status = TaskStatus.FAILED;
|
|
147
|
+
task.message = requested ? `resource not available: ${requested}` : "no online resource matched";
|
|
148
|
+
await this.taskRepository.save(task);
|
|
149
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: task.status, message: task.message });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const mergedMidsceneEnv = this.buildChildMidsceneEnv(params, spec, taskId);
|
|
153
|
+
mergedMidsceneEnv.AGENT_RESOURCE_ID = candidate.id;
|
|
154
|
+
mergedMidsceneEnv.AGENT_HTTP_BASE_URL = this.taskAgentHttpBaseUrl;
|
|
155
|
+
const qTok = this.taskAgentHttpToken?.trim();
|
|
156
|
+
if (qTok)
|
|
157
|
+
mergedMidsceneEnv.AGENT_HTTP_TOKEN = qTok;
|
|
158
|
+
await this.enqueueOnResource(candidate.id, async () => {
|
|
159
|
+
try {
|
|
160
|
+
const lease = await this.ensureTaskLease(candidate.id, taskId);
|
|
161
|
+
const session = await this.sessionService.createExecution(`exec-${taskId}`, candidate.id, lease.id, String(taskId), OwnerType.PLATFORM_TASK);
|
|
162
|
+
task.status = TaskStatus.RUNNING;
|
|
163
|
+
task.sessionId = session.id;
|
|
164
|
+
await this.taskRepository.save(task);
|
|
165
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: task.status });
|
|
166
|
+
await this.callbackClient.pushTaskStatus(String(taskId), {
|
|
167
|
+
agentId: this.selfAgentId,
|
|
168
|
+
status: task.status,
|
|
169
|
+
}).catch(() => { });
|
|
170
|
+
const abortController = new AbortController();
|
|
171
|
+
this.runningAbortControllers.set(String(taskId), abortController);
|
|
172
|
+
if (this.midsceneRuntime.prepare) {
|
|
173
|
+
await this.midsceneRuntime.prepare(spec, candidate.id, abortController.signal);
|
|
174
|
+
}
|
|
175
|
+
seedMidsceneTaskCacheFromRuntimeEnv(mergedMidsceneEnv);
|
|
176
|
+
let logBuffer = "";
|
|
177
|
+
let logTimer = null;
|
|
178
|
+
let anyLogStreamed = false;
|
|
179
|
+
let logSeq = 0;
|
|
180
|
+
const LOG_FLUSH_MS = 150;
|
|
181
|
+
const flushLogBuffer = () => {
|
|
182
|
+
if (logTimer) {
|
|
183
|
+
clearTimeout(logTimer);
|
|
184
|
+
logTimer = null;
|
|
185
|
+
}
|
|
186
|
+
const chunk = logBuffer;
|
|
187
|
+
logBuffer = "";
|
|
188
|
+
if (!chunk)
|
|
189
|
+
return;
|
|
190
|
+
anyLogStreamed = true;
|
|
191
|
+
logSeq += 1;
|
|
192
|
+
void this.callbackClient
|
|
193
|
+
.pushTaskLog(String(taskId), {
|
|
194
|
+
agentId: this.selfAgentId,
|
|
195
|
+
chunk,
|
|
196
|
+
seq: logSeq,
|
|
197
|
+
isFinal: false,
|
|
198
|
+
stream: "mixed",
|
|
199
|
+
})
|
|
200
|
+
.catch((err) => {
|
|
201
|
+
console.warn(`[TaskApplicationService] pushTaskLog failed taskId=${params.taskId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const onReportProgress = (p) => {
|
|
205
|
+
void (async () => {
|
|
206
|
+
let reportBundleBase64;
|
|
207
|
+
let zipPath;
|
|
208
|
+
if (p.reportBundleZipUri?.startsWith("file://")) {
|
|
209
|
+
zipPath = decodeURIComponent(p.reportBundleZipUri.slice("file://".length));
|
|
210
|
+
try {
|
|
211
|
+
const buf = await readFile(zipPath);
|
|
212
|
+
reportBundleBase64 = buf.toString("base64");
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.warn(`[TaskApplicationService] read report zip failed taskId=${params.taskId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await this.callbackClient.pushTaskReport(String(taskId), {
|
|
220
|
+
agentId: this.selfAgentId,
|
|
221
|
+
reportHtml: p.reportHtml ?? "",
|
|
222
|
+
reportName: p.reportName,
|
|
223
|
+
reportFormat: p.reportFormat,
|
|
224
|
+
partial: p.partial,
|
|
225
|
+
reportBundleBase64: reportBundleBase64,
|
|
226
|
+
...(p.executionDumpJson !== undefined ? { executionDumpJson: p.executionDumpJson } : {}),
|
|
227
|
+
...(p.executionDumpRevision !== undefined ? { executionDumpRevision: p.executionDumpRevision } : {}),
|
|
228
|
+
...(p.reportAssetFiles !== undefined ? { reportAssetFiles: p.reportAssetFiles } : {}),
|
|
229
|
+
});
|
|
230
|
+
if (zipPath) {
|
|
231
|
+
const zipParent = path.dirname(zipPath);
|
|
232
|
+
try {
|
|
233
|
+
await rm(zipPath, { force: true });
|
|
234
|
+
if (path.basename(zipParent).startsWith("report-zip-")) {
|
|
235
|
+
await rm(zipParent, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
/* ignore */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.warn(`[TaskApplicationService] pushTaskReport failed taskId=${params.taskId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
};
|
|
248
|
+
const result = await this.midsceneRuntime.execute(spec, candidate.id, abortController.signal, {
|
|
249
|
+
runtimeEnv: mergedMidsceneEnv,
|
|
250
|
+
taskId: String(taskId),
|
|
251
|
+
onLogChunk: (text, stream) => {
|
|
252
|
+
logBuffer += stream === "stderr" ? `[stderr] ${text}` : text;
|
|
253
|
+
if (logTimer)
|
|
254
|
+
return;
|
|
255
|
+
logTimer = setTimeout(() => {
|
|
256
|
+
logTimer = null;
|
|
257
|
+
flushLogBuffer();
|
|
258
|
+
}, LOG_FLUSH_MS);
|
|
259
|
+
},
|
|
260
|
+
onReportProgress,
|
|
261
|
+
});
|
|
262
|
+
flushLogBuffer();
|
|
263
|
+
if (anyLogStreamed) {
|
|
264
|
+
logSeq += 1;
|
|
265
|
+
void this.callbackClient
|
|
266
|
+
.pushTaskLog(String(taskId), {
|
|
267
|
+
agentId: this.selfAgentId,
|
|
268
|
+
chunk: "",
|
|
269
|
+
seq: logSeq,
|
|
270
|
+
isFinal: true,
|
|
271
|
+
stream: "mixed",
|
|
272
|
+
})
|
|
273
|
+
.catch((err) => {
|
|
274
|
+
console.warn(`[TaskApplicationService] pushTaskLog (final) failed taskId=${params.taskId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const cacheAsset = await readMidsceneTaskCacheFileIfExists(mergedMidsceneEnv);
|
|
279
|
+
if (cacheAsset) {
|
|
280
|
+
await this.callbackClient.pushTaskReport(String(taskId), {
|
|
281
|
+
agentId: this.selfAgentId,
|
|
282
|
+
reportHtml: "",
|
|
283
|
+
partial: true,
|
|
284
|
+
executionDumpRevision: 0,
|
|
285
|
+
reportAssetFiles: [cacheAsset],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
console.warn(`[TaskApplicationService] pushTaskReport(cache yaml) failed taskId=${params.taskId} error=${err instanceof Error ? err.message : String(err)}`);
|
|
291
|
+
}
|
|
292
|
+
console.info(`[TaskApplicationService] execute finished taskId=${params.taskId} resourceId=${candidate.id} ok=${result.ok} message=${result.message}`);
|
|
293
|
+
const refs = await this.artifactService.saveTaskArtifacts(taskId, result.artifacts.map((item) => ({ type: item.type, uri: item.uri })));
|
|
294
|
+
for (const ref of refs) {
|
|
295
|
+
await this.reporter.emit(EventType.ARTIFACT_READY, {
|
|
296
|
+
taskId,
|
|
297
|
+
artifactId: ref.id,
|
|
298
|
+
artifactType: ref.type,
|
|
299
|
+
uri: ref.uri,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const logArtifact = refs.find((item) => item.type === "LOG");
|
|
303
|
+
if (logArtifact?.uri?.startsWith("file://") && !anyLogStreamed) {
|
|
304
|
+
try {
|
|
305
|
+
const logPath = decodeURIComponent(logArtifact.uri.slice("file://".length));
|
|
306
|
+
const logText = await readFile(logPath, "utf8");
|
|
307
|
+
await this.callbackClient.pushTaskLog(String(taskId), {
|
|
308
|
+
agentId: this.selfAgentId,
|
|
309
|
+
chunk: logText,
|
|
310
|
+
seq: 1,
|
|
311
|
+
isFinal: true,
|
|
312
|
+
stream: "mixed",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
console.warn(`[TaskApplicationService] callback log (file) failed taskId=${params.taskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (result.reportInfo?.reportBundleZipPath) {
|
|
320
|
+
const zp = result.reportInfo.reportBundleZipPath;
|
|
321
|
+
try {
|
|
322
|
+
await rm(zp, { force: true });
|
|
323
|
+
const parent = path.dirname(zp);
|
|
324
|
+
if (path.basename(parent).startsWith("report-zip-")) {
|
|
325
|
+
await rm(parent, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
/* ignore */
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
task.status = result.ok ? TaskStatus.SUCCESS : TaskStatus.FAILED;
|
|
333
|
+
task.message = result.message;
|
|
334
|
+
await this.taskRepository.save(task);
|
|
335
|
+
await this.sessionService.closeExecution(session.id);
|
|
336
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: task.status, message: task.message });
|
|
337
|
+
await this.callbackClient.pushTaskStatus(String(taskId), {
|
|
338
|
+
agentId: this.selfAgentId,
|
|
339
|
+
status: task.status,
|
|
340
|
+
...(task.message ? { message: task.message } : {}),
|
|
341
|
+
}).catch(() => { });
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const failureMessage = this.toTaskFailureMessage(error);
|
|
345
|
+
console.error(`[TaskApplicationService] execute failed taskId=${params.taskId} error=${failureMessage}`);
|
|
346
|
+
task.status = TaskStatus.FAILED;
|
|
347
|
+
task.message = failureMessage;
|
|
348
|
+
await this.taskRepository.save(task);
|
|
349
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: task.status, message: task.message });
|
|
350
|
+
await this.callbackClient.pushTaskStatus(String(taskId), {
|
|
351
|
+
agentId: this.selfAgentId,
|
|
352
|
+
status: task.status,
|
|
353
|
+
...(task.message ? { message: task.message } : {}),
|
|
354
|
+
}).catch(() => { });
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
this.runningAbortControllers.delete(String(taskId));
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async cancel(taskId) {
|
|
362
|
+
const task = await this.taskRepository.getById(taskId);
|
|
363
|
+
if (!task)
|
|
364
|
+
return;
|
|
365
|
+
const abort = this.runningAbortControllers.get(String(taskId));
|
|
366
|
+
if (abort)
|
|
367
|
+
abort.abort("cancelled by platform");
|
|
368
|
+
task.status = TaskStatus.CANCELLED;
|
|
369
|
+
task.message = "cancelled by platform";
|
|
370
|
+
await this.taskRepository.save(task);
|
|
371
|
+
await this.reporter.emit(EventType.TASK_UPDATED, { taskId, status: task.status, message: task.message });
|
|
372
|
+
await this.callbackClient.pushTaskStatus(String(taskId), {
|
|
373
|
+
agentId: this.selfAgentId,
|
|
374
|
+
status: task.status,
|
|
375
|
+
message: task.message,
|
|
376
|
+
}).catch(() => { });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class AgentHttpCommandError extends Error {
|
|
2
|
+
statusCode;
|
|
3
|
+
body;
|
|
4
|
+
constructor(message, statusCode, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = "AgentHttpCommandError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function resolveBaseUrl(explicit) {
|
|
12
|
+
const trimmed = explicit?.trim();
|
|
13
|
+
if (trimmed)
|
|
14
|
+
return trimmed.replace(/\/$/, "");
|
|
15
|
+
const fromTask = process.env.AGENT_HTTP_BASE_URL?.trim();
|
|
16
|
+
if (fromTask)
|
|
17
|
+
return fromTask.replace(/\/$/, "");
|
|
18
|
+
const fromEnv = process.env.AGENT_HTTP_BASE_URL?.trim();
|
|
19
|
+
if (fromEnv)
|
|
20
|
+
return fromEnv.replace(/\/$/, "");
|
|
21
|
+
const port = Number(process.env.AGENT_HTTP_PORT ?? "18998");
|
|
22
|
+
const p = Number.isFinite(port) && port > 0 ? Math.floor(port) : 18998;
|
|
23
|
+
return `http://127.0.0.1:${p}`;
|
|
24
|
+
}
|
|
25
|
+
function resolveToken(explicit) {
|
|
26
|
+
const t = explicit?.trim() ||
|
|
27
|
+
process.env.AGENT_HTTP_TOKEN?.trim() ||
|
|
28
|
+
process.env.AGENT_HTTP_TOKEN?.trim();
|
|
29
|
+
return t || undefined;
|
|
30
|
+
}
|
|
31
|
+
function parseErrorMessage(status, text, body) {
|
|
32
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
33
|
+
const m = body.message;
|
|
34
|
+
if (typeof m === "string" && m.trim())
|
|
35
|
+
return m;
|
|
36
|
+
}
|
|
37
|
+
const slice = text.trim().slice(0, 500);
|
|
38
|
+
return slice || `HTTP ${status}`;
|
|
39
|
+
}
|
|
40
|
+
async function postInstallOrUninstall(command, opts) {
|
|
41
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
42
|
+
const token = resolveToken(opts.token);
|
|
43
|
+
const url = `${baseUrl}/platform/commands`;
|
|
44
|
+
const headers = {
|
|
45
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
46
|
+
};
|
|
47
|
+
if (token)
|
|
48
|
+
headers.Authorization = `Bearer ${token}`;
|
|
49
|
+
const res = await (opts.fetchImpl ?? globalThis.fetch)(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({ command }),
|
|
53
|
+
signal: opts.signal,
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = text ? JSON.parse(text) : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
body = text;
|
|
62
|
+
}
|
|
63
|
+
if (res.ok)
|
|
64
|
+
return;
|
|
65
|
+
throw new AgentHttpCommandError(parseErrorMessage(res.status, text, body), res.status, body);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 经 Agent HTTP 同步安装应用(服务端会等到 adb/ideviceinstaller/hdc 结束再响应)。
|
|
69
|
+
* 须已占用对应 `resourceId` 的租约(或依赖 Agent 侧自动租约配置)。
|
|
70
|
+
*/
|
|
71
|
+
export async function installAppOnAgent(params) {
|
|
72
|
+
const { resourceId, appRef, baseUrl, token, signal, fetchImpl } = params;
|
|
73
|
+
const command = {
|
|
74
|
+
type: "InstallAppCommand",
|
|
75
|
+
resourceId,
|
|
76
|
+
appRef,
|
|
77
|
+
};
|
|
78
|
+
await postInstallOrUninstall(command, { baseUrl, token, signal, fetchImpl });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 经 Agent HTTP 同步卸载应用。
|
|
82
|
+
*/
|
|
83
|
+
export async function uninstallAppOnAgent(params) {
|
|
84
|
+
const { resourceId, bundleId, baseUrl, token, signal, fetchImpl } = params;
|
|
85
|
+
const command = {
|
|
86
|
+
type: "UninstallAppCommand",
|
|
87
|
+
resourceId,
|
|
88
|
+
bundleId,
|
|
89
|
+
};
|
|
90
|
+
await postInstallOrUninstall(command, { baseUrl, token, signal, fetchImpl });
|
|
91
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LeaseStatus } from "../../shared-kernel/enums/index.js";
|
|
2
|
+
export class Lease {
|
|
3
|
+
id;
|
|
4
|
+
resourceId;
|
|
5
|
+
ownerId;
|
|
6
|
+
ownerType;
|
|
7
|
+
status;
|
|
8
|
+
expiresAt;
|
|
9
|
+
occupantUsername;
|
|
10
|
+
occupantDisplayName;
|
|
11
|
+
constructor(id, resourceId, ownerId, ownerType, status, expiresAt,
|
|
12
|
+
/** 平台占用者登录名(与 AcquireLeaseCommand.occupantUsername 对齐);可选兼容旧 Agent */
|
|
13
|
+
occupantUsername,
|
|
14
|
+
/** 平台占用者展示名;可选 */
|
|
15
|
+
occupantDisplayName) {
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.resourceId = resourceId;
|
|
18
|
+
this.ownerId = ownerId;
|
|
19
|
+
this.ownerType = ownerType;
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.expiresAt = expiresAt;
|
|
22
|
+
this.occupantUsername = occupantUsername;
|
|
23
|
+
this.occupantDisplayName = occupantDisplayName;
|
|
24
|
+
}
|
|
25
|
+
isActive(now) {
|
|
26
|
+
return this.status === LeaseStatus.ACTIVE && new Date(this.expiresAt).getTime() > now.getTime();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function compactDeviceDetails(details) {
|
|
2
|
+
const out = {};
|
|
3
|
+
if (details.manufacturer?.trim())
|
|
4
|
+
out.manufacturer = details.manufacturer.trim();
|
|
5
|
+
if (details.brand?.trim())
|
|
6
|
+
out.brand = details.brand.trim();
|
|
7
|
+
if (details.deviceName?.trim())
|
|
8
|
+
out.deviceName = details.deviceName.trim();
|
|
9
|
+
if (details.model?.trim())
|
|
10
|
+
out.model = details.model.trim();
|
|
11
|
+
if (details.deviceCodename?.trim())
|
|
12
|
+
out.deviceCodename = details.deviceCodename.trim();
|
|
13
|
+
if (details.osName?.trim())
|
|
14
|
+
out.osName = details.osName.trim();
|
|
15
|
+
if (details.osVersion?.trim())
|
|
16
|
+
out.osVersion = details.osVersion.trim();
|
|
17
|
+
if (details.buildFingerprint?.trim())
|
|
18
|
+
out.buildFingerprint = details.buildFingerprint.trim();
|
|
19
|
+
if (details.batteryPercent != null && Number.isFinite(details.batteryPercent)) {
|
|
20
|
+
out.batteryPercent = Math.max(0, Math.min(100, Math.round(details.batteryPercent)));
|
|
21
|
+
}
|
|
22
|
+
return Object.keys(out).length ? out : undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class DeviceResource {
|
|
2
|
+
id;
|
|
3
|
+
platform;
|
|
4
|
+
status;
|
|
5
|
+
capability;
|
|
6
|
+
deviceDetails;
|
|
7
|
+
occupancy;
|
|
8
|
+
constructor(id, platform, status, capability, deviceDetails, occupancy) {
|
|
9
|
+
this.id = id;
|
|
10
|
+
this.platform = platform;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.capability = capability;
|
|
13
|
+
this.deviceDetails = deviceDetails;
|
|
14
|
+
this.occupancy = occupancy;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class BaseSession {
|
|
2
|
+
id;
|
|
3
|
+
resourceId;
|
|
4
|
+
leaseId;
|
|
5
|
+
ownerId;
|
|
6
|
+
ownerType;
|
|
7
|
+
status;
|
|
8
|
+
constructor(id, resourceId, leaseId, ownerId, ownerType, status) {
|
|
9
|
+
this.id = id;
|
|
10
|
+
this.resourceId = resourceId;
|
|
11
|
+
this.leaseId = leaseId;
|
|
12
|
+
this.ownerId = ownerId;
|
|
13
|
+
this.ownerType = ownerType;
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|