preflite 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +144 -0
  3. package/dist/adapter-spi/artifact/index.js +1 -0
  4. package/dist/adapter-spi/command/index.js +1 -0
  5. package/dist/adapter-spi/install/index.js +1 -0
  6. package/dist/adapter-spi/lifecycle/index.js +1 -0
  7. package/dist/adapter-spi/resource/index.js +1 -0
  8. package/dist/adapter-spi/snapshot/index.js +1 -0
  9. package/dist/application/agent/AgentCommandPollLoop.js +48 -0
  10. package/dist/application/agent/AgentRuntimeService.js +27 -0
  11. package/dist/application/app-package/AppPackageApplicationService.js +97 -0
  12. package/dist/application/artifact/ArtifactApplicationService.js +13 -0
  13. package/dist/application/debug/DebugApplicationService.js +117 -0
  14. package/dist/application/health/HealthMetricsService.js +47 -0
  15. package/dist/application/lease/LeaseApplicationService.js +79 -0
  16. package/dist/application/query/ObservationQueryService.js +48 -0
  17. package/dist/application/reporter/ReporterApplicationService.js +41 -0
  18. package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
  19. package/dist/application/resource/ResourceRegistryService.js +113 -0
  20. package/dist/application/session/SessionApplicationService.js +39 -0
  21. package/dist/application/task/TaskApplicationService.js +378 -0
  22. package/dist/client/agentAppPackageClient.js +91 -0
  23. package/dist/domain/agent/AgentNode.js +12 -0
  24. package/dist/domain/agent/AgentRuntime.js +6 -0
  25. package/dist/domain/artifact/ArtifactPipeline.js +6 -0
  26. package/dist/domain/artifact/ArtifactRef.js +12 -0
  27. package/dist/domain/event/AgentEvent.js +10 -0
  28. package/dist/domain/event/Reporter.js +6 -0
  29. package/dist/domain/health/HealthMetrics.js +8 -0
  30. package/dist/domain/lease/Lease.js +28 -0
  31. package/dist/domain/lease/LeaseManager.js +6 -0
  32. package/dist/domain/repositories/index.js +1 -0
  33. package/dist/domain/resource/DeviceDetails.js +23 -0
  34. package/dist/domain/resource/DeviceResource.js +16 -0
  35. package/dist/domain/resource/ResourceRegistry.js +6 -0
  36. package/dist/domain/runtime/interfaces.js +1 -0
  37. package/dist/domain/session/BaseSession.js +16 -0
  38. package/dist/domain/session/DebugSession.js +3 -0
  39. package/dist/domain/session/ExecutionSession.js +3 -0
  40. package/dist/domain/session/SessionManager.js +8 -0
  41. package/dist/domain/task/TaskRecord.js +14 -0
  42. package/dist/domain/task/TaskSpec.js +12 -0
  43. package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
  44. package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
  45. package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
  46. package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
  47. package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
  48. package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
  49. package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
  50. package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
  51. package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
  52. package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
  53. package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
  54. package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
  55. package/dist/infrastructure/config/agentConfigFile.js +96 -0
  56. package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
  57. package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
  58. package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
  59. package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
  60. package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
  61. package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
  62. package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
  63. package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
  64. package/dist/infrastructure/midscene/videoRecorder.js +365 -0
  65. package/dist/infrastructure/midscene/zipReportDir.js +36 -0
  66. package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
  67. package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
  68. package/dist/infrastructure/system/CommandRunner.js +128 -0
  69. package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
  70. package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
  71. package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
  72. package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
  73. package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
  74. package/dist/infrastructure/transport/midscenePaths.js +28 -0
  75. package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
  76. package/dist/infrastructure/transport/ws/WsClient.js +182 -0
  77. package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
  78. package/dist/interfaces/http/HttpServer.js +227 -0
  79. package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
  80. package/dist/main.js +368 -0
  81. package/dist/mcp/agentHttpClient.js +82 -0
  82. package/dist/mcp/agentRuntime.js +184 -0
  83. package/dist/mcp/cli.js +36 -0
  84. package/dist/mcp/doctor.js +124 -0
  85. package/dist/mcp/evidence.js +57 -0
  86. package/dist/mcp/exploration/index.js +129 -0
  87. package/dist/mcp/exploration/sessionManager.js +122 -0
  88. package/dist/mcp/exploration/tools-atomic.js +34 -0
  89. package/dist/mcp/exploration/tools-intelligent.js +33 -0
  90. package/dist/mcp/exploration/tools-session.js +276 -0
  91. package/dist/mcp/exploration/types.js +1 -0
  92. package/dist/mcp/flowStepEvents.js +114 -0
  93. package/dist/mcp/liveViewer.js +156 -0
  94. package/dist/mcp/reportReader.js +157 -0
  95. package/dist/mcp/runManager.js +161 -0
  96. package/dist/mcp/runSummary.js +72 -0
  97. package/dist/mcp/runtimeInstall.js +44 -0
  98. package/dist/mcp/server.js +260 -0
  99. package/dist/mcp/setup.js +185 -0
  100. package/dist/mcp/types.js +1 -0
  101. package/dist/mcp/userConfig.js +45 -0
  102. package/dist/mcp/visual-flow/codegen.js +576 -0
  103. package/dist/mcp/visual-flow/index.js +14 -0
  104. package/dist/mcp/visual-flow/types.js +5 -0
  105. package/dist/mcp/visual-flow/validate.js +617 -0
  106. package/dist/protocol-contracts/commands/envelope.js +24 -0
  107. package/dist/protocol-contracts/commands/index.js +1 -0
  108. package/dist/protocol-contracts/dto/index.js +1 -0
  109. package/dist/protocol-contracts/events/index.js +1 -0
  110. package/dist/protocol-contracts/queries/index.js +1 -0
  111. package/dist/shared-kernel/enums/index.js +70 -0
  112. package/dist/shared-kernel/errors/index.js +21 -0
  113. package/dist/shared-kernel/ids/index.js +18 -0
  114. package/dist/shared-kernel/time/index.js +3 -0
  115. package/dist/shared-kernel/value-objects/index.js +1 -0
  116. package/dist/utils/appPackageLocalPath.js +75 -0
  117. package/dist/utils/deviceResourceRouting.js +24 -0
  118. package/dist/utils/harmonyAgentDebugDevice.js +60 -0
  119. package/dist/utils/harmonyHdcDeviceId.js +134 -0
  120. package/dist/utils/iosAgentDebugDevice.js +72 -0
  121. package/dist/utils/iosMjpegCapture.js +90 -0
  122. package/dist/utils/liveDebugForegroundParse.js +71 -0
  123. package/dist/utils/midscene-device-session.js +353 -0
  124. package/dist/utils/midscene-task-cache-env.js +15 -0
  125. package/dist/utils/midsceneReportConstants.js +49 -0
  126. package/dist/utils/seedMidsceneTaskCache.js +61 -0
  127. package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
  128. package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
  129. package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
  130. package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
  131. package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
  132. package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
  133. package/dist/utils/task-runners/taskAppPackage.js +20 -0
  134. package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
  135. package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
  136. package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
  137. package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
  138. package/package.json +46 -0
@@ -0,0 +1,94 @@
1
+ import { LeaseStatus } from "../../shared-kernel/enums/index.js";
2
+ export class InMemoryAgentRepository {
3
+ agent = null;
4
+ async save(agent) {
5
+ this.agent = agent;
6
+ }
7
+ async get() {
8
+ return this.agent;
9
+ }
10
+ }
11
+ export class InMemoryResourceRepository {
12
+ resources = new Map();
13
+ async list() {
14
+ return Array.from(this.resources.values());
15
+ }
16
+ async getById(id) {
17
+ return this.resources.get(id) ?? null;
18
+ }
19
+ async saveAll(resources) {
20
+ this.resources = new Map(resources.map((resource) => [resource.id, resource]));
21
+ }
22
+ async save(resource) {
23
+ this.resources.set(resource.id, resource);
24
+ }
25
+ }
26
+ export class InMemoryLeaseRepository {
27
+ leases = new Map();
28
+ async save(lease) {
29
+ this.leases.set(lease.id, lease);
30
+ }
31
+ async getById(id) {
32
+ return this.leases.get(id) ?? null;
33
+ }
34
+ async getActiveByResourceId(resourceId) {
35
+ const lease = Array.from(this.leases.values()).find((item) => item.resourceId === resourceId &&
36
+ item.status === LeaseStatus.ACTIVE &&
37
+ new Date(item.expiresAt).getTime() > Date.now());
38
+ return lease ?? null;
39
+ }
40
+ }
41
+ export class InMemorySessionRepository {
42
+ executionSessions = new Map();
43
+ debugSessions = new Map();
44
+ async saveExecution(session) {
45
+ this.executionSessions.set(session.id, session);
46
+ }
47
+ async saveDebug(session) {
48
+ this.debugSessions.set(session.id, session);
49
+ }
50
+ async getExecutionById(id) {
51
+ return this.executionSessions.get(id) ?? null;
52
+ }
53
+ async getDebugById(id) {
54
+ return this.debugSessions.get(id) ?? null;
55
+ }
56
+ async listDebugSessions() {
57
+ return Array.from(this.debugSessions.values());
58
+ }
59
+ }
60
+ export class InMemoryTaskRepository {
61
+ tasks = new Map();
62
+ async save(task) {
63
+ this.tasks.set(task.id, task);
64
+ }
65
+ async getById(id) {
66
+ return this.tasks.get(id) ?? null;
67
+ }
68
+ async list() {
69
+ return Array.from(this.tasks.values());
70
+ }
71
+ }
72
+ export class InMemoryArtifactRepository {
73
+ artifacts = new Map();
74
+ async saveMany(artifacts) {
75
+ if (artifacts.length === 0)
76
+ return;
77
+ const taskId = artifacts[0].taskId;
78
+ const list = this.artifacts.get(taskId) ?? [];
79
+ list.push(...artifacts);
80
+ this.artifacts.set(taskId, list);
81
+ }
82
+ async listByTaskId(taskId) {
83
+ return this.artifacts.get(taskId) ?? [];
84
+ }
85
+ }
86
+ export class InMemoryEventRepository {
87
+ events = [];
88
+ async append(event) {
89
+ this.events.push(event);
90
+ }
91
+ async list() {
92
+ return this.events;
93
+ }
94
+ }
@@ -0,0 +1,26 @@
1
+ /** 近期已确认处理过的 deliveryId,用于幂等;LRU 淘汰 */
2
+ export class DeliveryIdDeduper {
3
+ maxSize;
4
+ order = [];
5
+ seen = new Set();
6
+ constructor(maxSize) {
7
+ this.maxSize = maxSize;
8
+ }
9
+ isProcessed(deliveryId) {
10
+ return deliveryId !== "" && this.seen.has(deliveryId);
11
+ }
12
+ /** 标记为已处理(成功处理路径上调用) */
13
+ markProcessed(deliveryId) {
14
+ if (!deliveryId)
15
+ return;
16
+ if (this.seen.has(deliveryId))
17
+ return;
18
+ this.seen.add(deliveryId);
19
+ this.order.push(deliveryId);
20
+ while (this.order.length > this.maxSize) {
21
+ const evict = this.order.shift();
22
+ if (evict)
23
+ this.seen.delete(evict);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,128 @@
1
+ import { spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * 在子进程 PATH 前插入常见 CLI 目录(adb / ideviceinfo / brew 等),避免 GUI/Cursor 启动的 Node
6
+ * 未继承 zsh PATH 时找不到命令。最前可由 `AGENT_EXTRA_PATH` 或 `AUTOMATION_AGENT_EXTRA_PATH` 覆盖(与 OS 相同的 PATH 分隔符)。
7
+ * 若 `options.env` 显式传入 `PATH`,则不再改写,由调用方完全控制。
8
+ */
9
+ export function augmentPathForShellCommands(basePath) {
10
+ const sep = path.delimiter;
11
+ const extraEnv = process.env.AGENT_EXTRA_PATH?.trim() || process.env.AUTOMATION_AGENT_EXTRA_PATH?.trim();
12
+ const customHead = extraEnv
13
+ ? extraEnv
14
+ .split(sep)
15
+ .map((s) => s.trim())
16
+ .filter(Boolean)
17
+ : [];
18
+ const home = homedir();
19
+ const sdkRoot = process.env.ANDROID_HOME?.trim() || process.env.ANDROID_SDK_ROOT?.trim();
20
+ const prepend = [
21
+ ...customHead,
22
+ "/opt/homebrew/bin",
23
+ "/usr/local/bin",
24
+ ...(sdkRoot ? [path.join(sdkRoot, "platform-tools")] : []),
25
+ path.join(home, "Library/Android/sdk/platform-tools"),
26
+ "/usr/bin",
27
+ "/bin",
28
+ ];
29
+ const seen = new Set();
30
+ const ordered = [];
31
+ for (const p of prepend) {
32
+ const n = path.normalize(p);
33
+ if (!n || seen.has(n))
34
+ continue;
35
+ seen.add(n);
36
+ ordered.push(n);
37
+ }
38
+ for (const p of (basePath ?? "").split(sep)) {
39
+ const n = p.trim();
40
+ if (!n)
41
+ continue;
42
+ const norm = path.normalize(n);
43
+ if (seen.has(norm))
44
+ continue;
45
+ seen.add(norm);
46
+ ordered.push(n);
47
+ }
48
+ return ordered.join(sep);
49
+ }
50
+ export class NodeCommandRunner {
51
+ async run(command, timeoutMs = 60_000, signal, options) {
52
+ return new Promise((resolve) => {
53
+ const optEnv = options?.env ?? {};
54
+ const merged = {
55
+ ...process.env,
56
+ ...optEnv,
57
+ };
58
+ if (!("PATH" in optEnv)) {
59
+ merged.PATH = augmentPathForShellCommands(process.env.PATH);
60
+ }
61
+ const child = spawn("sh", ["-lc", command], {
62
+ stdio: ["ignore", "pipe", "pipe"],
63
+ env: merged,
64
+ });
65
+ let stdout = "";
66
+ let stderr = "";
67
+ let killedByTimeout = false;
68
+ let killedByAbort = false;
69
+ const timer = setTimeout(() => {
70
+ killedByTimeout = true;
71
+ child.kill("SIGTERM");
72
+ }, timeoutMs);
73
+ const abortHandler = () => {
74
+ killedByAbort = true;
75
+ child.kill("SIGTERM");
76
+ };
77
+ signal?.addEventListener("abort", abortHandler, { once: true });
78
+ child.stdout.on("data", (chunk) => {
79
+ const t = chunk.toString();
80
+ stdout += t;
81
+ options?.onStdoutChunk?.(t);
82
+ });
83
+ child.stderr.on("data", (chunk) => {
84
+ const t = chunk.toString();
85
+ stderr += t;
86
+ options?.onStderrChunk?.(t);
87
+ });
88
+ child.on("close", (code) => {
89
+ clearTimeout(timer);
90
+ signal?.removeEventListener("abort", abortHandler);
91
+ if (killedByAbort) {
92
+ resolve({
93
+ ok: false,
94
+ exitCode: -1,
95
+ stdout,
96
+ stderr: `${stderr}\ncommand cancelled`,
97
+ });
98
+ return;
99
+ }
100
+ if (killedByTimeout) {
101
+ resolve({
102
+ ok: false,
103
+ exitCode: -1,
104
+ stdout,
105
+ stderr: `${stderr}\ncommand timeout after ${timeoutMs}ms`,
106
+ });
107
+ return;
108
+ }
109
+ resolve({
110
+ ok: code === 0,
111
+ exitCode: code ?? -1,
112
+ stdout,
113
+ stderr,
114
+ });
115
+ });
116
+ child.on("error", (error) => {
117
+ clearTimeout(timer);
118
+ signal?.removeEventListener("abort", abortHandler);
119
+ resolve({
120
+ ok: false,
121
+ exitCode: -1,
122
+ stdout,
123
+ stderr: `${stderr}\n${error.message}`,
124
+ });
125
+ });
126
+ });
127
+ }
128
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * WS 不可用时的 Agent 事件 HTTP 投递(与 WS 文本帧同构 JSON)。
3
+ * 实况调试二进制帧不走此通道,仍仅通过 {@link WsEventPublisher}。
4
+ */
5
+ export class AgentEventHttpIngestClient {
6
+ opts;
7
+ base;
8
+ fetchImpl;
9
+ timeoutMs;
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ this.base = opts.baseUrl?.trim() || undefined;
13
+ this.fetchImpl = opts.fetchImpl ?? fetch;
14
+ const t = opts.timeoutMs;
15
+ this.timeoutMs = t != null && Number.isFinite(t) && t > 0 ? t : 12_000;
16
+ }
17
+ isEnabled() {
18
+ return !!this.base;
19
+ }
20
+ async postEvent(event) {
21
+ if (!this.base)
22
+ return;
23
+ const url = `${this.base.replace(/\/+$/, "")}/api/agents/${encodeURIComponent(this.opts.agentId)}/events`;
24
+ const headers = {
25
+ Accept: "application/json",
26
+ "Content-Type": "application/json",
27
+ };
28
+ if (this.opts.authToken)
29
+ headers.Authorization = `Bearer ${this.opts.authToken}`;
30
+ const body = JSON.stringify({
31
+ type: event.type,
32
+ timestamp: event.timestamp,
33
+ payload: event.payload,
34
+ });
35
+ const ac = new AbortController();
36
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
37
+ try {
38
+ const resp = await this.fetchImpl(url, {
39
+ method: "POST",
40
+ headers,
41
+ body,
42
+ signal: ac.signal,
43
+ });
44
+ if (!resp.ok) {
45
+ throw new Error(`event ingest HTTP ${resp.status}`);
46
+ }
47
+ }
48
+ finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,106 @@
1
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { isCallbackNotFoundError, } from "./PlatformCallbackClient.js";
5
+ const OUTBOX_NAME = "callback-outbox";
6
+ /** 出站队列某条记录连续失败后,下次最早尝试时间(指数退避,避免平台短时不可用仍高频 POST) */
7
+ function outboxBackoffBaseMs() {
8
+ const raw = Number(process.env.CALLBACK_OUTBOX_BACKOFF_BASE_MS ?? "2000");
9
+ return Number.isFinite(raw) && raw >= 200 ? Math.floor(raw) : 2000;
10
+ }
11
+ function outboxBackoffMaxMs() {
12
+ const raw = Number(process.env.CALLBACK_OUTBOX_BACKOFF_MAX_MS ?? "300000");
13
+ return Number.isFinite(raw) && raw >= 1000 ? Math.floor(raw) : 300000;
14
+ }
15
+ function defaultOutboxDir() {
16
+ return path.join(process.env.CALLBACK_OUTBOX_DIR?.trim() || path.join(homedir(), ".preflight-agent"), OUTBOX_NAME);
17
+ }
18
+ export class CallbackOutboxStore {
19
+ dir;
20
+ nextEligibleAt = new Map();
21
+ consecutiveFails = new Map();
22
+ constructor(dir = defaultOutboxDir()) {
23
+ this.dir = dir;
24
+ }
25
+ async pathFor(id) {
26
+ return path.join(this.dir, id);
27
+ }
28
+ async ensureReady() {
29
+ await mkdir(this.dir, { recursive: true });
30
+ }
31
+ async enqueue(record) {
32
+ const id = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.json`;
33
+ const full = { v: 1, ...record };
34
+ await this.ensureReady();
35
+ const filePath = await this.pathFor(id);
36
+ await writeFile(filePath, JSON.stringify(full), "utf8");
37
+ return { id };
38
+ }
39
+ async processWith(client, limit = 50) {
40
+ let processed = 0;
41
+ let failed = 0;
42
+ let files;
43
+ try {
44
+ files = await readdir(this.dir);
45
+ }
46
+ catch {
47
+ return { processed, failed };
48
+ }
49
+ const sorted = files.filter((f) => f.endsWith(".json")).sort();
50
+ for (const name of sorted) {
51
+ if (processed + failed >= limit)
52
+ break;
53
+ const fp = path.join(this.dir, name);
54
+ const eligibleAt = this.nextEligibleAt.get(fp) ?? 0;
55
+ if (Date.now() < eligibleAt)
56
+ continue;
57
+ let raw;
58
+ try {
59
+ raw = await readFile(fp, "utf8");
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ let rec;
65
+ try {
66
+ rec = JSON.parse(raw);
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ try {
72
+ if (rec.kind === "log") {
73
+ await client.pushTaskLog(rec.taskId, rec.payload);
74
+ }
75
+ else if (rec.kind === "report") {
76
+ await client.pushTaskReport(rec.taskId, rec.payload);
77
+ }
78
+ else {
79
+ await client.pushTaskStatus(rec.taskId, rec.payload);
80
+ }
81
+ await rm(fp, { force: true });
82
+ this.nextEligibleAt.delete(fp);
83
+ this.consecutiveFails.delete(fp);
84
+ processed += 1;
85
+ }
86
+ catch (e) {
87
+ if (isCallbackNotFoundError(e)) {
88
+ await rm(fp, { force: true });
89
+ this.nextEligibleAt.delete(fp);
90
+ this.consecutiveFails.delete(fp);
91
+ processed += 1;
92
+ console.warn(`[CallbackOutbox] dropped ${name} taskId=${rec.taskId} (callback 404, removed from outbox)`);
93
+ continue;
94
+ }
95
+ failed += 1;
96
+ const n = (this.consecutiveFails.get(fp) ?? 0) + 1;
97
+ this.consecutiveFails.set(fp, n);
98
+ const rawDelay = outboxBackoffBaseMs() * 2 ** Math.min(n - 1, 14);
99
+ const delay = Math.min(outboxBackoffMaxMs(), rawDelay);
100
+ this.nextEligibleAt.set(fp, Date.now() + delay);
101
+ console.warn(`[CallbackOutbox] redelivery failed ${name} taskId=${rec.taskId} fail#=${n} nextEligibleIn=${delay}ms`);
102
+ }
103
+ }
104
+ return { processed, failed };
105
+ }
106
+ }
@@ -0,0 +1,113 @@
1
+ function sleep(ms) {
2
+ return new Promise((r) => setTimeout(r, ms));
3
+ }
4
+ /** 单次请求超时(毫秒);大报告体可调大 */
5
+ function callbackHttpTimeoutMs() {
6
+ const raw = Number(process.env.PLATFORM_CALLBACK_HTTP_TIMEOUT_MS ?? "120000");
7
+ return Number.isFinite(raw) && raw >= 5000 ? Math.floor(raw) : 120000;
8
+ }
9
+ /** 同一请求内失败后的最大尝试次数(含首次) */
10
+ function callbackMaxAttempts() {
11
+ const raw = Number(process.env.PLATFORM_CALLBACK_HTTP_MAX_ATTEMPTS ?? "3");
12
+ const n = Math.floor(raw);
13
+ return n >= 1 && n <= 10 ? n : 3;
14
+ }
15
+ /** 重试基础间隔(毫秒),实际为指数退避:base * 2^attempt */
16
+ function callbackRetryBaseMs() {
17
+ const raw = Number(process.env.PLATFORM_CALLBACK_HTTP_RETRY_BASE_MS ?? "500");
18
+ return Number.isFinite(raw) && raw >= 50 ? Math.floor(raw) : 500;
19
+ }
20
+ function shouldRetryHttpStatus(status) {
21
+ if (status === 408 || status === 429)
22
+ return true;
23
+ if (status >= 500 && status <= 504)
24
+ return true;
25
+ return false;
26
+ }
27
+ /** 平台回调 HTTP 404(如 unknown agent task id):无需再重试或保留 Outbox */
28
+ export function isCallbackNotFoundError(err) {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ return /\bcallback failed: 404\b/.test(msg);
31
+ }
32
+ export class PlatformCallbackClient {
33
+ endpoint;
34
+ authToken;
35
+ constructor(options) {
36
+ this.endpoint = options.endpoint?.trim() || undefined;
37
+ this.authToken = options.authToken?.trim() || undefined;
38
+ }
39
+ async post(path, body) {
40
+ if (!this.endpoint)
41
+ return;
42
+ const url = `${this.endpoint.replace(/\/+$/, "")}${path}`;
43
+ const headers = {
44
+ "Content-Type": "application/json",
45
+ };
46
+ if (this.authToken)
47
+ headers.Authorization = `Bearer ${this.authToken}`;
48
+ const timeoutMs = callbackHttpTimeoutMs();
49
+ const maxAttempts = callbackMaxAttempts();
50
+ const baseMs = callbackRetryBaseMs();
51
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
52
+ const signal = AbortSignal.timeout(timeoutMs);
53
+ try {
54
+ const resp = await fetch(url, {
55
+ method: "POST",
56
+ headers,
57
+ body: JSON.stringify(body),
58
+ signal,
59
+ });
60
+ if (resp.ok)
61
+ return;
62
+ const snippet = await resp.text().catch(() => "");
63
+ const status = resp.status;
64
+ const hint = status === 401
65
+ ? "(请核对平台生成的 Token 与 Agent 侧 AGENT_HTTP_TOKEN 一致;PLATFORM_WS_TOKEN / PLATFORM_AGENT_CALLBACK_TOKEN 未填时会复用 AGENT_HTTP_TOKEN)"
66
+ : status === 503
67
+ ? "(平台未配置回调鉴权 token)"
68
+ : "";
69
+ console.warn(`[PlatformCallback] POST ${path} -> ${status}${hint}${snippet ? ` body=${snippet.slice(0, 200)}` : ""}`);
70
+ if (!shouldRetryHttpStatus(status)) {
71
+ throw new Error(`callback failed: ${status}`);
72
+ }
73
+ if (attempt < maxAttempts - 1) {
74
+ const wait = baseMs * 2 ** attempt;
75
+ console.warn(`[PlatformCallback] POST ${path} HTTP ${status} 可重试,${wait}ms 后进行第 ${attempt + 2}/${maxAttempts} 次尝试`);
76
+ await sleep(wait);
77
+ continue;
78
+ }
79
+ throw new Error(`callback failed: ${status}`);
80
+ }
81
+ catch (e) {
82
+ const err = e instanceof Error ? e : new Error(String(e));
83
+ const hf = /^callback failed: (\d+)$/.exec(err.message);
84
+ if (hf) {
85
+ const code = Number(hf[1]);
86
+ if (!shouldRetryHttpStatus(code))
87
+ throw err;
88
+ if (attempt >= maxAttempts - 1)
89
+ throw err;
90
+ }
91
+ const transient = err.name === "AbortError" ||
92
+ err.name === "TimeoutError" ||
93
+ /fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT/i.test(err.message);
94
+ if (!transient)
95
+ throw err;
96
+ if (attempt >= maxAttempts - 1)
97
+ throw err;
98
+ const wait = baseMs * 2 ** attempt;
99
+ console.warn(`[PlatformCallback] POST ${path} 传输失败 (${err.message}),${wait}ms 后重试 (${attempt + 2}/${maxAttempts})`);
100
+ await sleep(wait);
101
+ }
102
+ }
103
+ }
104
+ async pushTaskStatus(taskId, payload) {
105
+ await this.post(`/api/agent/callbacks/tasks/${encodeURIComponent(taskId)}/status`, payload);
106
+ }
107
+ async pushTaskLog(taskId, payload) {
108
+ await this.post(`/api/agent/callbacks/tasks/${encodeURIComponent(taskId)}/log`, { ...payload, taskId });
109
+ }
110
+ async pushTaskReport(taskId, payload) {
111
+ await this.post(`/api/agent/callbacks/tasks/${encodeURIComponent(taskId)}/report`, { ...payload, taskId });
112
+ }
113
+ }
@@ -0,0 +1,89 @@
1
+ export class PlatformCommandPollClient {
2
+ opts;
3
+ base;
4
+ fetchImpl;
5
+ timeoutMs;
6
+ constructor(opts) {
7
+ this.opts = opts;
8
+ this.base = opts.baseUrl?.trim() || undefined;
9
+ this.fetchImpl = opts.fetchImpl ?? fetch;
10
+ const t = opts.timeoutMs;
11
+ this.timeoutMs = t != null && Number.isFinite(t) && t > 0 ? t : 15_000;
12
+ }
13
+ isEnabled() {
14
+ return !!this.base;
15
+ }
16
+ url(path) {
17
+ return `${this.base.replace(/\/+$/, "")}${path}`;
18
+ }
19
+ async fetchPending(limit) {
20
+ if (!this.base)
21
+ return [];
22
+ const lim = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 20;
23
+ const path = `/api/agents/${encodeURIComponent(this.opts.agentId)}/commands/pending?limit=${lim}`;
24
+ const headers = { Accept: "application/json" };
25
+ if (this.opts.authToken)
26
+ headers.Authorization = `Bearer ${this.opts.authToken}`;
27
+ const ac = new AbortController();
28
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
29
+ try {
30
+ const resp = await this.fetchImpl(this.url(path), {
31
+ method: "GET",
32
+ headers,
33
+ signal: ac.signal,
34
+ });
35
+ if (!resp.ok) {
36
+ throw new Error(`poll pending failed: ${resp.status}`);
37
+ }
38
+ const body = (await resp.json());
39
+ const items = Array.isArray(body.items) ? body.items : [];
40
+ const out = [];
41
+ for (const row of items) {
42
+ if (!row || typeof row !== "object")
43
+ continue;
44
+ const r = row;
45
+ const deliveryId = typeof r.deliveryId === "string" ? r.deliveryId.trim() : "";
46
+ const cmd = r.command;
47
+ if (!deliveryId || !cmd || typeof cmd !== "object" || typeof cmd.type !== "string") {
48
+ continue;
49
+ }
50
+ out.push({
51
+ deliveryId,
52
+ issuedAt: typeof r.issuedAt === "string" ? r.issuedAt : undefined,
53
+ command: cmd,
54
+ });
55
+ }
56
+ return out;
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }
62
+ async ack(deliveryIds) {
63
+ if (!this.base || deliveryIds.length === 0)
64
+ return;
65
+ const path = `/api/agents/${encodeURIComponent(this.opts.agentId)}/commands/ack`;
66
+ const headers = {
67
+ Accept: "application/json",
68
+ "Content-Type": "application/json",
69
+ };
70
+ if (this.opts.authToken)
71
+ headers.Authorization = `Bearer ${this.opts.authToken}`;
72
+ const ac = new AbortController();
73
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
74
+ try {
75
+ const resp = await this.fetchImpl(this.url(path), {
76
+ method: "POST",
77
+ headers,
78
+ body: JSON.stringify({ deliveryIds }),
79
+ signal: ac.signal,
80
+ });
81
+ if (!resp.ok) {
82
+ throw new Error(`ack failed: ${resp.status}`);
83
+ }
84
+ }
85
+ finally {
86
+ clearTimeout(timer);
87
+ }
88
+ }
89
+ }