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,47 @@
|
|
|
1
|
+
export class HealthMetricsService {
|
|
2
|
+
heartbeats = 0;
|
|
3
|
+
wsReconnectCount = 0;
|
|
4
|
+
wsLastReconnectDelayMs = null;
|
|
5
|
+
wsPendingDroppedTotal = 0;
|
|
6
|
+
wsConnectionState = "disconnected";
|
|
7
|
+
pendingSendQueueDepth = 0;
|
|
8
|
+
pollLastOkAt = null;
|
|
9
|
+
pollLastErrorAt = null;
|
|
10
|
+
markHeartbeat() {
|
|
11
|
+
this.heartbeats += 1;
|
|
12
|
+
}
|
|
13
|
+
setWsConnectionState(state) {
|
|
14
|
+
this.wsConnectionState = state;
|
|
15
|
+
}
|
|
16
|
+
setPendingSendDepth(depth) {
|
|
17
|
+
this.pendingSendQueueDepth = depth;
|
|
18
|
+
}
|
|
19
|
+
onReconnectScheduled(delayMs) {
|
|
20
|
+
this.wsReconnectCount += 1;
|
|
21
|
+
this.wsLastReconnectDelayMs = delayMs;
|
|
22
|
+
}
|
|
23
|
+
onPendingDropped(count) {
|
|
24
|
+
this.wsPendingDroppedTotal += count;
|
|
25
|
+
}
|
|
26
|
+
markPollOk() {
|
|
27
|
+
this.pollLastOkAt = new Date().toISOString();
|
|
28
|
+
this.pollLastErrorAt = null;
|
|
29
|
+
}
|
|
30
|
+
markPollError() {
|
|
31
|
+
this.pollLastErrorAt = new Date().toISOString();
|
|
32
|
+
}
|
|
33
|
+
snapshot() {
|
|
34
|
+
return {
|
|
35
|
+
healthy: true,
|
|
36
|
+
heartbeats: this.heartbeats,
|
|
37
|
+
wsConnected: this.wsConnectionState === "open",
|
|
38
|
+
wsConnectionState: this.wsConnectionState,
|
|
39
|
+
wsReconnectCount: this.wsReconnectCount,
|
|
40
|
+
wsLastReconnectDelayMs: this.wsLastReconnectDelayMs,
|
|
41
|
+
wsPendingDroppedTotal: this.wsPendingDroppedTotal,
|
|
42
|
+
pendingSendQueueDepth: this.pendingSendQueueDepth,
|
|
43
|
+
pollLastOkAt: this.pollLastOkAt,
|
|
44
|
+
pollLastErrorAt: this.pollLastErrorAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Lease } from "../../domain/lease/Lease.js";
|
|
2
|
+
import { LeaseConflictError, LeaseRequiredError } from "../../shared-kernel/errors/index.js";
|
|
3
|
+
import { EventType, LeaseStatus } from "../../shared-kernel/enums/index.js";
|
|
4
|
+
import { asLeaseId, asResourceId, } from "../../shared-kernel/ids/index.js";
|
|
5
|
+
export class LeaseApplicationService {
|
|
6
|
+
leaseRepository;
|
|
7
|
+
reporter;
|
|
8
|
+
constructor(leaseRepository, reporter) {
|
|
9
|
+
this.leaseRepository = leaseRepository;
|
|
10
|
+
this.reporter = reporter;
|
|
11
|
+
}
|
|
12
|
+
async emitLeaseChanged(lease) {
|
|
13
|
+
if (!this.reporter)
|
|
14
|
+
return;
|
|
15
|
+
await this.reporter.emit(EventType.LEASE_CHANGED, {
|
|
16
|
+
leaseId: lease.id,
|
|
17
|
+
resourceId: lease.resourceId,
|
|
18
|
+
status: lease.status,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async acquire(leaseId, resourceId, ownerId, ownerType, ttlSeconds, occupant) {
|
|
22
|
+
const rid = asResourceId(resourceId);
|
|
23
|
+
const activeLease = await this.leaseRepository.getActiveByResourceId(rid);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
if (activeLease && activeLease.isActive(now)) {
|
|
26
|
+
throw new LeaseConflictError(resourceId);
|
|
27
|
+
}
|
|
28
|
+
if (activeLease) {
|
|
29
|
+
activeLease.status = LeaseStatus.REJECTED;
|
|
30
|
+
await this.leaseRepository.save(activeLease);
|
|
31
|
+
await this.emitLeaseChanged(activeLease);
|
|
32
|
+
}
|
|
33
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
34
|
+
const ou = occupant?.username?.trim();
|
|
35
|
+
const od = occupant?.displayName?.trim();
|
|
36
|
+
const lease = new Lease(asLeaseId(leaseId), rid, ownerId, ownerType, LeaseStatus.ACTIVE, expiresAt, ou || undefined, od || undefined);
|
|
37
|
+
await this.leaseRepository.save(lease);
|
|
38
|
+
await this.emitLeaseChanged(lease);
|
|
39
|
+
return lease;
|
|
40
|
+
}
|
|
41
|
+
async release(leaseId) {
|
|
42
|
+
const lease = await this.leaseRepository.getById(leaseId);
|
|
43
|
+
if (!lease)
|
|
44
|
+
return null;
|
|
45
|
+
lease.status = LeaseStatus.RELEASED;
|
|
46
|
+
await this.leaseRepository.save(lease);
|
|
47
|
+
await this.emitLeaseChanged(lease);
|
|
48
|
+
return lease;
|
|
49
|
+
}
|
|
50
|
+
async renew(leaseId, ttlSeconds) {
|
|
51
|
+
const lease = await this.leaseRepository.getById(leaseId);
|
|
52
|
+
if (!lease)
|
|
53
|
+
return null;
|
|
54
|
+
lease.expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
55
|
+
await this.leaseRepository.save(lease);
|
|
56
|
+
await this.emitLeaseChanged(lease);
|
|
57
|
+
return lease;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 按资源撤销当前活跃租约(与 ReleaseLeaseCommand 等效,但只需 resourceId)。
|
|
61
|
+
* 无活跃租约时返回 null(调用方仍可视为成功)。
|
|
62
|
+
*/
|
|
63
|
+
async revokeByResourceId(resourceId) {
|
|
64
|
+
const rid = asResourceId(resourceId);
|
|
65
|
+
const lease = await this.leaseRepository.getActiveByResourceId(rid);
|
|
66
|
+
if (!lease)
|
|
67
|
+
return null;
|
|
68
|
+
lease.status = LeaseStatus.RELEASED;
|
|
69
|
+
await this.leaseRepository.save(lease);
|
|
70
|
+
await this.emitLeaseChanged(lease);
|
|
71
|
+
return lease;
|
|
72
|
+
}
|
|
73
|
+
async ensureActive(resourceId) {
|
|
74
|
+
const lease = await this.leaseRepository.getActiveByResourceId(resourceId);
|
|
75
|
+
if (!lease || !lease.isActive(new Date()))
|
|
76
|
+
throw new LeaseRequiredError(resourceId);
|
|
77
|
+
return lease;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { asLeaseId, asSessionId, asTaskId } from "../../shared-kernel/ids/index.js";
|
|
2
|
+
export class ObservationQueryService {
|
|
3
|
+
taskRepository;
|
|
4
|
+
sessionRepository;
|
|
5
|
+
leaseRepository;
|
|
6
|
+
artifactRepository;
|
|
7
|
+
eventRepository;
|
|
8
|
+
constructor(taskRepository, sessionRepository, leaseRepository, artifactRepository, eventRepository) {
|
|
9
|
+
this.taskRepository = taskRepository;
|
|
10
|
+
this.sessionRepository = sessionRepository;
|
|
11
|
+
this.leaseRepository = leaseRepository;
|
|
12
|
+
this.artifactRepository = artifactRepository;
|
|
13
|
+
this.eventRepository = eventRepository;
|
|
14
|
+
}
|
|
15
|
+
async getTask(taskId) {
|
|
16
|
+
return this.taskRepository.getById(asTaskId(taskId));
|
|
17
|
+
}
|
|
18
|
+
async getSession(sessionId) {
|
|
19
|
+
const id = asSessionId(sessionId);
|
|
20
|
+
const execution = await this.sessionRepository.getExecutionById(id);
|
|
21
|
+
if (execution)
|
|
22
|
+
return { sessionType: "execution", session: execution };
|
|
23
|
+
const debug = await this.sessionRepository.getDebugById(id);
|
|
24
|
+
if (debug)
|
|
25
|
+
return { sessionType: "debug", session: debug };
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
async getLease(leaseId) {
|
|
29
|
+
return this.leaseRepository.getById(asLeaseId(leaseId));
|
|
30
|
+
}
|
|
31
|
+
async listArtifacts(taskId) {
|
|
32
|
+
return this.artifactRepository.listByTaskId(asTaskId(taskId));
|
|
33
|
+
}
|
|
34
|
+
async listEvents(params) {
|
|
35
|
+
const events = await this.eventRepository.list();
|
|
36
|
+
const filtered = events.filter((event) => {
|
|
37
|
+
const byType = params.type ? event.type === params.type : true;
|
|
38
|
+
const payloadTaskId = event.payload && typeof event.payload === "object" && "taskId" in event.payload
|
|
39
|
+
? String(event.payload.taskId ?? "")
|
|
40
|
+
: "";
|
|
41
|
+
const byTaskId = params.taskId ? payloadTaskId === params.taskId : true;
|
|
42
|
+
return byType && byTaskId;
|
|
43
|
+
});
|
|
44
|
+
if (!params.limit || params.limit <= 0)
|
|
45
|
+
return filtered;
|
|
46
|
+
return filtered.slice(-params.limit);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AgentEvent } from "../../domain/event/AgentEvent.js";
|
|
2
|
+
import { nowIso } from "../../shared-kernel/time/index.js";
|
|
3
|
+
import { EventType } from "../../shared-kernel/enums/index.js";
|
|
4
|
+
export class ReporterApplicationService {
|
|
5
|
+
eventRepository;
|
|
6
|
+
publisher;
|
|
7
|
+
constructor(eventRepository, publisher) {
|
|
8
|
+
this.eventRepository = eventRepository;
|
|
9
|
+
this.publisher = publisher;
|
|
10
|
+
}
|
|
11
|
+
async emit(type, payload) {
|
|
12
|
+
const event = new AgentEvent(type, nowIso(), payload);
|
|
13
|
+
await this.eventRepository.append(event);
|
|
14
|
+
await this.publisher.publish(event);
|
|
15
|
+
}
|
|
16
|
+
async emitLiveDebugFrame(payload) {
|
|
17
|
+
const capturedAt = payload.capturedAt ?? nowIso();
|
|
18
|
+
const frameData = Buffer.from(payload.dataBase64, "base64");
|
|
19
|
+
const event = new AgentEvent(EventType.LIVE_DEBUG_FRAME, nowIso(), {
|
|
20
|
+
sessionId: payload.sessionId,
|
|
21
|
+
resourceId: payload.resourceId,
|
|
22
|
+
mimeType: payload.mimeType,
|
|
23
|
+
capturedAt,
|
|
24
|
+
byteLength: frameData.byteLength,
|
|
25
|
+
sourceUri: payload.sourceUri ?? "",
|
|
26
|
+
...(payload.foregroundApp ? { foregroundApp: payload.foregroundApp } : {}),
|
|
27
|
+
});
|
|
28
|
+
await this.eventRepository.append(event);
|
|
29
|
+
await this.publisher.publish(event);
|
|
30
|
+
if (this.publisher.publishLiveDebugFrame) {
|
|
31
|
+
await this.publisher.publishLiveDebugFrame(event, {
|
|
32
|
+
mimeType: payload.mimeType,
|
|
33
|
+
data: frameData,
|
|
34
|
+
capturedAt,
|
|
35
|
+
sessionId: payload.sessionId,
|
|
36
|
+
resourceId: payload.resourceId,
|
|
37
|
+
...(payload.foregroundApp ? { foregroundApp: payload.foregroundApp } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SessionStatus, TaskStatus } from "../../shared-kernel/enums/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* 与 `/resources` 合并逻辑对齐:占用可能来自调试会话、RUNNING 任务、或仅有租约。
|
|
4
|
+
* 强制解除须三者一并回收,否则列表仍显示 DEBUGGING / RUNNING。
|
|
5
|
+
*/
|
|
6
|
+
export class ResourceOccupationReleaseService {
|
|
7
|
+
taskService;
|
|
8
|
+
debugService;
|
|
9
|
+
leaseService;
|
|
10
|
+
sessionRepo;
|
|
11
|
+
taskRepo;
|
|
12
|
+
constructor(taskService, debugService, leaseService, sessionRepo, taskRepo) {
|
|
13
|
+
this.taskService = taskService;
|
|
14
|
+
this.debugService = debugService;
|
|
15
|
+
this.leaseService = leaseService;
|
|
16
|
+
this.sessionRepo = sessionRepo;
|
|
17
|
+
this.taskRepo = taskRepo;
|
|
18
|
+
}
|
|
19
|
+
async forceRelease(resourceIdRaw) {
|
|
20
|
+
const resourceId = resourceIdRaw.trim();
|
|
21
|
+
if (!resourceId)
|
|
22
|
+
throw new Error("resourceId required");
|
|
23
|
+
const matchesResource = (stored) => String(stored) === resourceId;
|
|
24
|
+
const listDebug = this.sessionRepo.listDebugSessions?.bind(this.sessionRepo);
|
|
25
|
+
if (listDebug) {
|
|
26
|
+
for (const s of await listDebug()) {
|
|
27
|
+
if (s.status === SessionStatus.CLOSED)
|
|
28
|
+
continue;
|
|
29
|
+
if (!matchesResource(s.resourceId))
|
|
30
|
+
continue;
|
|
31
|
+
await this.debugService.close(s.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const listTasks = this.taskRepo.list?.bind(this.taskRepo);
|
|
35
|
+
if (listTasks) {
|
|
36
|
+
for (const t of await listTasks()) {
|
|
37
|
+
if (t.status !== TaskStatus.RUNNING || !t.sessionId)
|
|
38
|
+
continue;
|
|
39
|
+
const exec = await this.sessionRepo.getExecutionById(t.sessionId);
|
|
40
|
+
if (!exec)
|
|
41
|
+
continue;
|
|
42
|
+
if (!matchesResource(exec.resourceId))
|
|
43
|
+
continue;
|
|
44
|
+
await this.taskService.cancel(t.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await this.leaseService.revokeByResourceId(resourceId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { DeviceResource, } from "../../domain/resource/DeviceResource.js";
|
|
2
|
+
import { EventType, ResourceStatus, SessionStatus, TaskStatus } from "../../shared-kernel/enums/index.js";
|
|
3
|
+
export class ResourceRegistryService {
|
|
4
|
+
resourceRepository;
|
|
5
|
+
adapters;
|
|
6
|
+
reporter;
|
|
7
|
+
leaseRepository;
|
|
8
|
+
taskRepository;
|
|
9
|
+
sessionRepository;
|
|
10
|
+
refreshInFlight = null;
|
|
11
|
+
constructor(resourceRepository, adapters, reporter, leaseRepository, taskRepository, sessionRepository) {
|
|
12
|
+
this.resourceRepository = resourceRepository;
|
|
13
|
+
this.adapters = adapters;
|
|
14
|
+
this.reporter = reporter;
|
|
15
|
+
this.leaseRepository = leaseRepository;
|
|
16
|
+
this.taskRepository = taskRepository;
|
|
17
|
+
this.sessionRepository = sessionRepository;
|
|
18
|
+
}
|
|
19
|
+
async refresh() {
|
|
20
|
+
if (this.refreshInFlight)
|
|
21
|
+
return this.refreshInFlight;
|
|
22
|
+
this.refreshInFlight = (async () => {
|
|
23
|
+
const previous = await this.resourceRepository.list();
|
|
24
|
+
const previousMap = new Map(previous.map((item) => [item.id, item.status]));
|
|
25
|
+
const discovered = await Promise.all(this.adapters.map((adapter) => adapter.discover()));
|
|
26
|
+
const resources = discovered.flat();
|
|
27
|
+
await this.resourceRepository.saveAll(resources);
|
|
28
|
+
if (this.reporter) {
|
|
29
|
+
for (const resource of resources) {
|
|
30
|
+
const prevStatus = previousMap.get(resource.id);
|
|
31
|
+
if (prevStatus == null || prevStatus !== resource.status) {
|
|
32
|
+
await this.reporter.emit(EventType.RESOURCE_CHANGED, {
|
|
33
|
+
resourceId: resource.id,
|
|
34
|
+
status: resource.status,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return resources;
|
|
40
|
+
})();
|
|
41
|
+
try {
|
|
42
|
+
return await this.refreshInFlight;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
this.refreshInFlight = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async list() {
|
|
49
|
+
const raw = await this.resourceRepository.list();
|
|
50
|
+
return this.mergeActivityIntoResourceStatus(raw);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `/resources` 的物理发现结果始终为 ONLINE;租约与任务/调试状态单独存放。
|
|
54
|
+
* 列出时合并:**调试会话未结束 → DEBUGGING**,**任务 RUNNING → RUNNING**,**仅有活跃租约 → LEASED**。
|
|
55
|
+
*/
|
|
56
|
+
async mergeActivityIntoResourceStatus(resources) {
|
|
57
|
+
const leaseRepo = this.leaseRepository;
|
|
58
|
+
const taskRepo = this.taskRepository;
|
|
59
|
+
const sessionRepo = this.sessionRepository;
|
|
60
|
+
if (!leaseRepo || !taskRepo || !sessionRepo) {
|
|
61
|
+
return resources;
|
|
62
|
+
}
|
|
63
|
+
const listTasks = taskRepo.list?.bind(taskRepo);
|
|
64
|
+
const listDebugSessions = sessionRepo.listDebugSessions?.bind(sessionRepo);
|
|
65
|
+
if (!listTasks || !listDebugSessions) {
|
|
66
|
+
return resources;
|
|
67
|
+
}
|
|
68
|
+
const byResource = new Map();
|
|
69
|
+
for (const s of await listDebugSessions()) {
|
|
70
|
+
if (s.status === SessionStatus.CLOSED)
|
|
71
|
+
continue;
|
|
72
|
+
byResource.set(String(s.resourceId), ResourceStatus.DEBUGGING);
|
|
73
|
+
}
|
|
74
|
+
for (const t of await listTasks()) {
|
|
75
|
+
if (t.status !== TaskStatus.RUNNING || !t.sessionId)
|
|
76
|
+
continue;
|
|
77
|
+
const exec = await sessionRepo.getExecutionById(t.sessionId);
|
|
78
|
+
if (!exec)
|
|
79
|
+
continue;
|
|
80
|
+
const rid = String(exec.resourceId);
|
|
81
|
+
if (byResource.get(rid) === ResourceStatus.DEBUGGING)
|
|
82
|
+
continue;
|
|
83
|
+
byResource.set(rid, ResourceStatus.RUNNING);
|
|
84
|
+
}
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const r of resources) {
|
|
88
|
+
const rid = String(r.id);
|
|
89
|
+
let next = byResource.get(rid);
|
|
90
|
+
const lease = await leaseRepo.getActiveByResourceId(r.id);
|
|
91
|
+
if (!next && lease?.isActive(now)) {
|
|
92
|
+
next = ResourceStatus.LEASED;
|
|
93
|
+
}
|
|
94
|
+
let occupancy;
|
|
95
|
+
if (lease?.isActive(now)) {
|
|
96
|
+
occupancy = {
|
|
97
|
+
leaseId: String(lease.id),
|
|
98
|
+
ownerId: lease.ownerId,
|
|
99
|
+
ownerType: lease.ownerType,
|
|
100
|
+
...(lease.occupantUsername?.trim()
|
|
101
|
+
? { occupantUsername: lease.occupantUsername.trim() }
|
|
102
|
+
: {}),
|
|
103
|
+
...(lease.occupantDisplayName?.trim()
|
|
104
|
+
? { occupantDisplayName: lease.occupantDisplayName.trim() }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const mergedStatus = next && next !== r.status ? next : r.status;
|
|
109
|
+
out.push(new DeviceResource(r.id, r.platform, mergedStatus, r.capability, r.deviceDetails, occupancy));
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DebugSession } from "../../domain/session/DebugSession.js";
|
|
2
|
+
import { ExecutionSession } from "../../domain/session/ExecutionSession.js";
|
|
3
|
+
import { asLeaseId, asResourceId, asSessionId, } from "../../shared-kernel/ids/index.js";
|
|
4
|
+
import { SessionStatus } from "../../shared-kernel/enums/index.js";
|
|
5
|
+
export class SessionApplicationService {
|
|
6
|
+
sessionRepository;
|
|
7
|
+
constructor(sessionRepository) {
|
|
8
|
+
this.sessionRepository = sessionRepository;
|
|
9
|
+
}
|
|
10
|
+
async createExecution(sessionId, resourceId, leaseId, ownerId, ownerType) {
|
|
11
|
+
const session = new ExecutionSession(asSessionId(sessionId), resourceId, leaseId, ownerId, ownerType, SessionStatus.CREATED);
|
|
12
|
+
await this.sessionRepository.saveExecution(session);
|
|
13
|
+
return session;
|
|
14
|
+
}
|
|
15
|
+
async createDebug(sessionId, resourceId, leaseId, ownerId, ownerType) {
|
|
16
|
+
const session = new DebugSession(asSessionId(sessionId), asResourceId(resourceId), asLeaseId(leaseId), ownerId, ownerType, SessionStatus.CREATED);
|
|
17
|
+
await this.sessionRepository.saveDebug(session);
|
|
18
|
+
return session;
|
|
19
|
+
}
|
|
20
|
+
async closeExecution(sessionId) {
|
|
21
|
+
const session = await this.sessionRepository.getExecutionById(sessionId);
|
|
22
|
+
if (!session)
|
|
23
|
+
return null;
|
|
24
|
+
session.status = SessionStatus.CLOSED;
|
|
25
|
+
await this.sessionRepository.saveExecution(session);
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
async closeDebug(sessionId) {
|
|
29
|
+
const session = await this.sessionRepository.getDebugById(sessionId);
|
|
30
|
+
if (!session)
|
|
31
|
+
return null;
|
|
32
|
+
session.status = SessionStatus.CLOSED;
|
|
33
|
+
await this.sessionRepository.saveDebug(session);
|
|
34
|
+
return session;
|
|
35
|
+
}
|
|
36
|
+
async getDebug(sessionId) {
|
|
37
|
+
return this.sessionRepository.getDebugById(sessionId);
|
|
38
|
+
}
|
|
39
|
+
}
|