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,617 @@
1
+ import { VISUAL_FLOW_VERSION } from './types.js';
2
+ const SET_VAR_METHODS = new Set(['aiQuery', 'aiAsk', 'aiBoolean', 'aiNumber', 'aiString']);
3
+ const TRANSFORM_VAR_RULES = new Set(['onlyNumber', 'cut', 'jsonPath', 'replace', 'handleAmount']);
4
+ const RESERVED_SET_VAR_NAMES = new Set([
5
+ '__flowVars',
6
+ '__flowStep',
7
+ '__flowStepLog',
8
+ '__v1Vars',
9
+ '__v1Step',
10
+ '__v1StepLog',
11
+ 'process',
12
+ 'agent',
13
+ 'sleep',
14
+ 'installApp',
15
+ 'uninstallApp',
16
+ 'page',
17
+ 'console',
18
+ 'Object',
19
+ ]);
20
+ function isValidSetVarName(name) {
21
+ if (name.length === 0 || name.length > 64)
22
+ return false;
23
+ if (RESERVED_SET_VAR_NAMES.has(name))
24
+ return false;
25
+ // 变量名用作 `__flowVars[name]` 的字符串 key;这里校验为兼容 JS 标识符字符集的 Unicode 子集。
26
+ // 支持中文/其它 Unicode 字母:`{{变量名}}`、`{{变量名.length}}` 等可正常解析。
27
+ return /^[$_\p{L}][\p{L}\p{N}_$]*$/u.test(name);
28
+ }
29
+ function parseScriptVars(raw) {
30
+ if (raw === undefined)
31
+ return { ok: true, value: [] };
32
+ if (!Array.isArray(raw))
33
+ return { ok: false, message: 'visualFlow.scriptVars 须为数组' };
34
+ if (raw.length > 100)
35
+ return { ok: false, message: '脚本变量数量超过上限 100' };
36
+ const seen = new Set();
37
+ const out = [];
38
+ for (let i = 0; i < raw.length; i++) {
39
+ const path = `scriptVars[${i}]`;
40
+ const item = raw[i];
41
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
42
+ return { ok: false, message: `${path} 须为对象` };
43
+ }
44
+ const o = item;
45
+ if (!isNonEmptyString(o.name))
46
+ return { ok: false, message: `${path}.name 必填` };
47
+ const name = o.name.trim();
48
+ if (!isValidSetVarName(name)) {
49
+ return { ok: false, message: `${path}.name 须为合法标识符(字母/数字/_$,且非保留名)` };
50
+ }
51
+ if (seen.has(name))
52
+ return { ok: false, message: `脚本变量「${name}」重复声明` };
53
+ seen.add(name);
54
+ const description = typeof o.description === 'string' && o.description.trim() ? o.description.trim().slice(0, 500) : undefined;
55
+ const defaultValue = o.defaultValue == null ? '' : typeof o.defaultValue === 'string' ? o.defaultValue : String(o.defaultValue);
56
+ const scopeRaw = typeof o.scope === 'string' ? o.scope.trim() : '';
57
+ const scope = scopeRaw === 'global' || scopeRaw === 'local' || scopeRaw === 'temp'
58
+ ? scopeRaw
59
+ : undefined;
60
+ out.push({ name, ...(scope ? { scope } : {}), ...(description ? { description } : {}), ...(defaultValue ? { defaultValue } : {}) });
61
+ }
62
+ return { ok: true, value: out };
63
+ }
64
+ function isNonEmptyString(v) {
65
+ return typeof v === 'string' && v.trim().length > 0;
66
+ }
67
+ function parseStep(raw, path) {
68
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
69
+ return { ok: false, message: `${path} 须为对象` };
70
+ }
71
+ const o = raw;
72
+ const t = o.type;
73
+ if (!isNonEmptyString(t)) {
74
+ return { ok: false, message: `${path}.type 无效` };
75
+ }
76
+ switch (t.trim()) {
77
+ case 'launch': {
78
+ if (!isNonEmptyString(o.packageName))
79
+ return { ok: false, message: `${path}.packageName 必填` };
80
+ return { ok: true, step: { type: 'launch', packageName: o.packageName.trim() } };
81
+ }
82
+ case 'installApp': {
83
+ if (!isNonEmptyString(o.appRef))
84
+ return { ok: false, message: `${path}.appRef 必填` };
85
+ return { ok: true, step: { type: 'installApp', appRef: o.appRef.trim() } };
86
+ }
87
+ case 'uninstallApp': {
88
+ if (!isNonEmptyString(o.bundleId))
89
+ return { ok: false, message: `${path}.bundleId 必填` };
90
+ return { ok: true, step: { type: 'uninstallApp', bundleId: o.bundleId.trim() } };
91
+ }
92
+ case 'closeApp': {
93
+ if (!isNonEmptyString(o.packageName))
94
+ return { ok: false, message: `${path}.packageName 必填` };
95
+ return { ok: true, step: { type: 'closeApp', packageName: o.packageName.trim() } };
96
+ }
97
+ case 'setAIActContext': {
98
+ if (!isNonEmptyString(o.prompt))
99
+ return { ok: false, message: `${path}.prompt 必填` };
100
+ return { ok: true, step: { type: 'setAIActContext', prompt: o.prompt.trim() } };
101
+ }
102
+ case 'recordToReport': {
103
+ const title = o.title == null ? '' : String(o.title);
104
+ const content = o.content == null ? '' : String(o.content);
105
+ if (title.length > 500)
106
+ return { ok: false, message: `${path}.title 过长` };
107
+ if (content.length > 20_000)
108
+ return { ok: false, message: `${path}.content 过长` };
109
+ return { ok: true, step: { type: 'recordToReport', title, content } };
110
+ }
111
+ case 'assert': {
112
+ if (!isNonEmptyString(o.prompt))
113
+ return { ok: false, message: `${path}.prompt 必填` };
114
+ return { ok: true, step: { type: 'assert', prompt: o.prompt.trim() } };
115
+ }
116
+ case 'sleep': {
117
+ const ms = Number(o.ms);
118
+ if (!Number.isFinite(ms) || ms < 0 || ms > 3_600_000) {
119
+ return { ok: false, message: `${path}.ms 须为 0~3600000 的数字` };
120
+ }
121
+ return { ok: true, step: { type: 'sleep', ms: Math.floor(ms) } };
122
+ }
123
+ case 'aiAct': {
124
+ if (!isNonEmptyString(o.prompt))
125
+ return { ok: false, message: `${path}.prompt 必填` };
126
+ return { ok: true, step: { type: 'aiAct', prompt: o.prompt.trim() } };
127
+ }
128
+ case 'if': {
129
+ if (!isNonEmptyString(o.conditionPrompt)) {
130
+ return { ok: false, message: `${path}.conditionPrompt 必填` };
131
+ }
132
+ if (!Array.isArray(o.thenSteps) || o.thenSteps.length === 0) {
133
+ return { ok: false, message: `${path}.thenSteps 须为非空数组` };
134
+ }
135
+ const thenSteps = [];
136
+ for (let i = 0; i < o.thenSteps.length; i++) {
137
+ const r = parseStep(o.thenSteps[i], `${path}.thenSteps[${i}]`);
138
+ if (!r.ok)
139
+ return r;
140
+ thenSteps.push(r.step);
141
+ }
142
+ let elseSteps;
143
+ // `null` 与缺省等价:部分 JSON/序列化会把「无 else」写成 elseSteps:null,`!== undefined` 会误判为「有字段」进而报「须为数组」
144
+ if (o.elseSteps != null) {
145
+ if (!Array.isArray(o.elseSteps))
146
+ return { ok: false, message: `${path}.elseSteps 须为数组` };
147
+ elseSteps = [];
148
+ for (let i = 0; i < o.elseSteps.length; i++) {
149
+ const r = parseStep(o.elseSteps[i], `${path}.elseSteps[${i}]`);
150
+ if (!r.ok)
151
+ return r;
152
+ elseSteps.push(r.step);
153
+ }
154
+ }
155
+ return {
156
+ ok: true,
157
+ step: {
158
+ type: 'if',
159
+ conditionPrompt: o.conditionPrompt.trim(),
160
+ thenSteps,
161
+ ...(elseSteps && elseSteps.length > 0 ? { elseSteps } : {}),
162
+ },
163
+ };
164
+ }
165
+ case 'ifDeviceType': {
166
+ const it = typeof o.interfaceType === 'string' ? o.interfaceType.trim() : '';
167
+ if (!['android', 'ios', 'harmony'].includes(it)) {
168
+ return { ok: false, message: `${path}.interfaceType 须为 android|ios|harmony` };
169
+ }
170
+ if (!Array.isArray(o.thenSteps) || o.thenSteps.length === 0) {
171
+ return { ok: false, message: `${path}.thenSteps 须为非空数组` };
172
+ }
173
+ const thenSteps = [];
174
+ for (let i = 0; i < o.thenSteps.length; i++) {
175
+ const r = parseStep(o.thenSteps[i], `${path}.thenSteps[${i}]`);
176
+ if (!r.ok)
177
+ return r;
178
+ thenSteps.push(r.step);
179
+ }
180
+ let elseSteps;
181
+ // `null` 与缺省等价:部分 JSON/序列化会把「无 else」写成 elseSteps:null,`!== undefined` 会误判为「有字段」进而报「须为数组」
182
+ if (o.elseSteps != null) {
183
+ if (!Array.isArray(o.elseSteps))
184
+ return { ok: false, message: `${path}.elseSteps 须为数组` };
185
+ elseSteps = [];
186
+ for (let i = 0; i < o.elseSteps.length; i++) {
187
+ const r = parseStep(o.elseSteps[i], `${path}.elseSteps[${i}]`);
188
+ if (!r.ok)
189
+ return r;
190
+ elseSteps.push(r.step);
191
+ }
192
+ }
193
+ return {
194
+ ok: true,
195
+ step: {
196
+ type: 'ifDeviceType',
197
+ interfaceType: it,
198
+ thenSteps,
199
+ ...(elseSteps && elseSteps.length > 0 ? { elseSteps } : {}),
200
+ },
201
+ };
202
+ }
203
+ case 'whileLoop': {
204
+ if (!isNonEmptyString(o.conditionPrompt)) {
205
+ return { ok: false, message: `${path}.conditionPrompt 必填` };
206
+ }
207
+ const maxIterations = Number(o.maxIterations);
208
+ if (!Number.isFinite(maxIterations) || maxIterations < 1 || maxIterations > 1000) {
209
+ return { ok: false, message: `${path}.maxIterations 须为 1~1000 的整数` };
210
+ }
211
+ if (!Array.isArray(o.bodySteps) || o.bodySteps.length === 0) {
212
+ return { ok: false, message: `${path}.bodySteps 须为非空数组` };
213
+ }
214
+ const bodySteps = [];
215
+ for (let i = 0; i < o.bodySteps.length; i++) {
216
+ const r = parseStep(o.bodySteps[i], `${path}.bodySteps[${i}]`);
217
+ if (!r.ok)
218
+ return r;
219
+ bodySteps.push(r.step);
220
+ }
221
+ return {
222
+ ok: true,
223
+ step: {
224
+ type: 'whileLoop',
225
+ conditionPrompt: o.conditionPrompt.trim(),
226
+ maxIterations: Math.floor(maxIterations),
227
+ bodySteps,
228
+ },
229
+ };
230
+ }
231
+ case 'forLoop': {
232
+ const count = Number(o.count);
233
+ if (!Number.isFinite(count) || count < 1 || count > 500) {
234
+ return { ok: false, message: `${path}.count 须为 1~500 的整数` };
235
+ }
236
+ if (!Array.isArray(o.bodySteps) || o.bodySteps.length === 0) {
237
+ return { ok: false, message: `${path}.bodySteps 须为非空数组` };
238
+ }
239
+ const bodySteps = [];
240
+ for (let i = 0; i < o.bodySteps.length; i++) {
241
+ const r = parseStep(o.bodySteps[i], `${path}.bodySteps[${i}]`);
242
+ if (!r.ok)
243
+ return r;
244
+ bodySteps.push(r.step);
245
+ }
246
+ return {
247
+ ok: true,
248
+ step: {
249
+ type: 'forLoop',
250
+ count: Math.floor(count),
251
+ bodySteps,
252
+ },
253
+ };
254
+ }
255
+ case 'setVar': {
256
+ if (!isNonEmptyString(o.name))
257
+ return { ok: false, message: `${path}.name 必填` };
258
+ const name = o.name.trim();
259
+ if (!isValidSetVarName(name)) {
260
+ return {
261
+ ok: false,
262
+ message: `${path}.name 须为合法标识符(字母/数字/_$,且非保留名如 agent、page)`,
263
+ };
264
+ }
265
+ const method = o.method;
266
+ if (typeof method !== 'string' || !SET_VAR_METHODS.has(method.trim())) {
267
+ return { ok: false, message: `${path}.method 须为 aiQuery|aiAsk|aiBoolean|aiNumber|aiString` };
268
+ }
269
+ if (!isNonEmptyString(o.expression))
270
+ return { ok: false, message: `${path}.expression 必填` };
271
+ const expression = o.expression.trim();
272
+ if (expression.length > 50_000) {
273
+ return { ok: false, message: `${path}.expression 过长` };
274
+ }
275
+ return {
276
+ ok: true,
277
+ step: {
278
+ type: 'setVar',
279
+ name,
280
+ method: method.trim(),
281
+ expression,
282
+ },
283
+ };
284
+ }
285
+ case 'assignVar': {
286
+ if (!isNonEmptyString(o.name))
287
+ return { ok: false, message: `${path}.name 必填` };
288
+ const name = o.name.trim();
289
+ if (!isValidSetVarName(name)) {
290
+ return { ok: false, message: `${path}.name 须为合法标识符(字母/数字/_$,且非保留名)` };
291
+ }
292
+ const value = o.value == null ? '' : String(o.value);
293
+ if (value.length > 50_000)
294
+ return { ok: false, message: `${path}.value 过长` };
295
+ return { ok: true, step: { type: 'assignVar', name, value } };
296
+ }
297
+ case 'transformVar': {
298
+ if (!isNonEmptyString(o.name))
299
+ return { ok: false, message: `${path}.name 必填` };
300
+ const name = o.name.trim();
301
+ if (!isValidSetVarName(name)) {
302
+ return { ok: false, message: `${path}.name 须为合法标识符(字母/数字/_$,且非保留名)` };
303
+ }
304
+ const rule = typeof o.rule === 'string' ? o.rule.trim() : '';
305
+ if (!TRANSFORM_VAR_RULES.has(rule)) {
306
+ return { ok: false, message: `${path}.rule 须为 onlyNumber|cut|jsonPath|replace|handleAmount` };
307
+ }
308
+ const str = (v) => (v == null ? undefined : String(v));
309
+ return {
310
+ ok: true,
311
+ step: {
312
+ type: 'transformVar',
313
+ name,
314
+ rule: rule,
315
+ ...(str(o.source) !== undefined ? { source: str(o.source) } : {}),
316
+ ...(str(o.start) !== undefined ? { start: str(o.start) } : {}),
317
+ ...(str(o.end) !== undefined ? { end: str(o.end) } : {}),
318
+ ...(str(o.jsonPath) !== undefined ? { jsonPath: str(o.jsonPath) } : {}),
319
+ ...(str(o.pattern) !== undefined ? { pattern: str(o.pattern) } : {}),
320
+ ...(str(o.replacement) !== undefined ? { replacement: str(o.replacement) } : {}),
321
+ },
322
+ };
323
+ }
324
+ case 'callScript': {
325
+ if (!isNonEmptyString(o.targetTestCaseId)) {
326
+ return { ok: false, message: `${path}.targetTestCaseId 必填` };
327
+ }
328
+ const tid = o.targetTestCaseId.trim();
329
+ if (!/^[a-f\d]{24}$/i.test(tid)) {
330
+ return { ok: false, message: `${path}.targetTestCaseId 须为 24 位 hex 的脚本 id` };
331
+ }
332
+ const scopeId = typeof o.scopeId === 'string' ? o.scopeId.trim() : '';
333
+ if (!/^sub[a-f0-9]{12}$/i.test(scopeId)) {
334
+ return { ok: false, message: `${path}.scopeId 须为 sub 后跟 12 位十六进制` };
335
+ }
336
+ const targetName = typeof o.targetName === 'string' && o.targetName.trim() ? o.targetName.trim() : undefined;
337
+ const vbRaw = o.varBindings;
338
+ const varBindings = {};
339
+ if (vbRaw != null && typeof vbRaw === 'object' && !Array.isArray(vbRaw)) {
340
+ for (const [k, v] of Object.entries(vbRaw)) {
341
+ const kk = String(k).trim();
342
+ if (!isValidSetVarName(kk))
343
+ continue;
344
+ varBindings[kk] = v == null ? '' : typeof v === 'string' ? v : String(v);
345
+ }
346
+ }
347
+ return {
348
+ ok: true,
349
+ step: {
350
+ type: 'callScript',
351
+ targetTestCaseId: tid,
352
+ ...(targetName ? { targetName } : {}),
353
+ scopeId,
354
+ varBindings,
355
+ },
356
+ };
357
+ }
358
+ default:
359
+ return { ok: false, message: `${path}.type 应使用 IR 步骤类型表中的枚举` };
360
+ }
361
+ }
362
+ function validateSetVarSequential(steps, inherited, pathPrefix) {
363
+ const seen = new Set(inherited);
364
+ for (let i = 0; i < steps.length; i++) {
365
+ const s = steps[i];
366
+ const path = `${pathPrefix}[${i}]`;
367
+ if (s.type === 'setVar') {
368
+ if (seen.has(s.name)) {
369
+ return { ok: false, message: `变量「${s.name}」在同一线性序列中重复声明(${path})` };
370
+ }
371
+ seen.add(s.name);
372
+ }
373
+ else if (s.type === 'if' || s.type === 'ifDeviceType') {
374
+ const rThen = validateSetVarSequential(s.thenSteps, new Set(seen), `${path}.thenSteps`);
375
+ if (!rThen.ok)
376
+ return rThen;
377
+ if (s.elseSteps && s.elseSteps.length > 0) {
378
+ const rElse = validateSetVarSequential(s.elseSteps, new Set(seen), `${path}.elseSteps`);
379
+ if (!rElse.ok)
380
+ return rElse;
381
+ }
382
+ }
383
+ else if (s.type === 'whileLoop') {
384
+ const r = validateSetVarSequential(s.bodySteps, new Set(seen), `${path}.bodySteps`);
385
+ if (!r.ok)
386
+ return r;
387
+ }
388
+ else if (s.type === 'forLoop') {
389
+ const r = validateSetVarSequential(s.bodySteps, new Set(seen), `${path}.bodySteps`);
390
+ if (!r.ok)
391
+ return r;
392
+ }
393
+ }
394
+ return { ok: true };
395
+ }
396
+ function collectDeclaredVarNames(steps, out) {
397
+ for (const step of steps) {
398
+ if (step.type === 'setVar' || step.type === 'assignVar' || step.type === 'transformVar') {
399
+ out.add(step.name);
400
+ }
401
+ else if (step.type === 'if' || step.type === 'ifDeviceType') {
402
+ collectDeclaredVarNames(step.thenSteps, out);
403
+ if (step.elseSteps)
404
+ collectDeclaredVarNames(step.elseSteps, out);
405
+ }
406
+ else if (step.type === 'whileLoop' || step.type === 'forLoop') {
407
+ collectDeclaredVarNames(step.bodySteps, out);
408
+ }
409
+ }
410
+ }
411
+ function identifierCharRe() {
412
+ // 这里只用于区分 `time` 与 `timestamp` 这类英文标识符前缀;中文自然语言经常紧贴变量名,
413
+ // 例如 `timeBefore和timeAfter不同`,因此边界判断以 ASCII 标识符字符为准。
414
+ return /[$_A-Za-z0-9]/;
415
+ }
416
+ function interpolationRanges(text) {
417
+ const ranges = [];
418
+ const re = /\{\{[^}]+\}\}/g;
419
+ let m;
420
+ while ((m = re.exec(text)) !== null) {
421
+ ranges.push({ start: m.index, end: m.index + m[0].length });
422
+ }
423
+ return ranges;
424
+ }
425
+ function inAnyRange(index, ranges) {
426
+ return ranges.some((range) => index >= range.start && index < range.end);
427
+ }
428
+ function findBareVariableReference(text, varNames) {
429
+ if (!text || varNames.size === 0)
430
+ return undefined;
431
+ const ranges = interpolationRanges(text);
432
+ const idChar = identifierCharRe();
433
+ const names = [...varNames].sort((a, b) => b.length - a.length);
434
+ for (const name of names) {
435
+ let from = 0;
436
+ while (from < text.length) {
437
+ const index = text.indexOf(name, from);
438
+ if (index < 0)
439
+ break;
440
+ from = index + name.length;
441
+ if (inAnyRange(index, ranges))
442
+ continue;
443
+ const prev = index > 0 ? text[index - 1] : '';
444
+ const next = index + name.length < text.length ? text[index + name.length] : '';
445
+ if ((prev && idChar.test(prev)) || (next && idChar.test(next)))
446
+ continue;
447
+ return name;
448
+ }
449
+ }
450
+ return undefined;
451
+ }
452
+ function validateInterpolatedVariableReferences(steps, declaredNames, pathPrefix) {
453
+ const check = (value, path) => {
454
+ if (value == null)
455
+ return { ok: true };
456
+ const name = findBareVariableReference(value, declaredNames);
457
+ if (!name)
458
+ return { ok: true };
459
+ return { ok: false, message: `${path} 中变量「${name}」请写成 {{${name}}}` };
460
+ };
461
+ for (let i = 0; i < steps.length; i++) {
462
+ const s = steps[i];
463
+ const path = `${pathPrefix}[${i}]`;
464
+ switch (s.type) {
465
+ case 'assert':
466
+ case 'aiAct':
467
+ case 'setAIActContext': {
468
+ const r = check(s.prompt, `${path}.prompt`);
469
+ if (!r.ok)
470
+ return r;
471
+ break;
472
+ }
473
+ case 'recordToReport': {
474
+ const title = check(s.title, `${path}.title`);
475
+ if (!title.ok)
476
+ return title;
477
+ const content = check(s.content, `${path}.content`);
478
+ if (!content.ok)
479
+ return content;
480
+ break;
481
+ }
482
+ case 'if': {
483
+ const cond = check(s.conditionPrompt, `${path}.conditionPrompt`);
484
+ if (!cond.ok)
485
+ return cond;
486
+ const thenResult = validateInterpolatedVariableReferences(s.thenSteps, declaredNames, `${path}.thenSteps`);
487
+ if (!thenResult.ok)
488
+ return thenResult;
489
+ if (s.elseSteps) {
490
+ const elseResult = validateInterpolatedVariableReferences(s.elseSteps, declaredNames, `${path}.elseSteps`);
491
+ if (!elseResult.ok)
492
+ return elseResult;
493
+ }
494
+ break;
495
+ }
496
+ case 'whileLoop': {
497
+ const cond = check(s.conditionPrompt, `${path}.conditionPrompt`);
498
+ if (!cond.ok)
499
+ return cond;
500
+ const body = validateInterpolatedVariableReferences(s.bodySteps, declaredNames, `${path}.bodySteps`);
501
+ if (!body.ok)
502
+ return body;
503
+ break;
504
+ }
505
+ case 'forLoop': {
506
+ const body = validateInterpolatedVariableReferences(s.bodySteps, declaredNames, `${path}.bodySteps`);
507
+ if (!body.ok)
508
+ return body;
509
+ break;
510
+ }
511
+ case 'setVar': {
512
+ const r = check(s.expression, `${path}.expression`);
513
+ if (!r.ok)
514
+ return r;
515
+ break;
516
+ }
517
+ case 'assignVar': {
518
+ const r = check(s.value, `${path}.value`);
519
+ if (!r.ok)
520
+ return r;
521
+ break;
522
+ }
523
+ case 'transformVar': {
524
+ const source = check(s.source, `${path}.source`);
525
+ if (!source.ok)
526
+ return source;
527
+ const start = check(s.start, `${path}.start`);
528
+ if (!start.ok)
529
+ return start;
530
+ const end = check(s.end, `${path}.end`);
531
+ if (!end.ok)
532
+ return end;
533
+ const jsonPath = check(s.jsonPath, `${path}.jsonPath`);
534
+ if (!jsonPath.ok)
535
+ return jsonPath;
536
+ const pattern = check(s.pattern, `${path}.pattern`);
537
+ if (!pattern.ok)
538
+ return pattern;
539
+ const replacement = check(s.replacement, `${path}.replacement`);
540
+ if (!replacement.ok)
541
+ return replacement;
542
+ break;
543
+ }
544
+ case 'callScript': {
545
+ for (const [key, value] of Object.entries(s.varBindings)) {
546
+ const r = check(value, `${path}.varBindings.${key}`);
547
+ if (!r.ok)
548
+ return r;
549
+ }
550
+ break;
551
+ }
552
+ case 'launch':
553
+ case 'installApp':
554
+ case 'uninstallApp':
555
+ case 'closeApp':
556
+ case 'sleep':
557
+ case 'ifDeviceType':
558
+ if (s.type === 'ifDeviceType') {
559
+ const thenResult = validateInterpolatedVariableReferences(s.thenSteps, declaredNames, `${path}.thenSteps`);
560
+ if (!thenResult.ok)
561
+ return thenResult;
562
+ if (s.elseSteps) {
563
+ const elseResult = validateInterpolatedVariableReferences(s.elseSteps, declaredNames, `${path}.elseSteps`);
564
+ if (!elseResult.ok)
565
+ return elseResult;
566
+ }
567
+ }
568
+ break;
569
+ }
570
+ }
571
+ return { ok: true };
572
+ }
573
+ export function tryParseVisualFlow(raw) {
574
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
575
+ return { ok: false, message: 'visualFlow 须为对象' };
576
+ }
577
+ const o = raw;
578
+ const ver = o.version;
579
+ const verOk = ver === VISUAL_FLOW_VERSION ||
580
+ (typeof ver === 'number' && Number.isFinite(ver) && Math.trunc(ver) === VISUAL_FLOW_VERSION) ||
581
+ (typeof ver === 'string' && String(ver).trim() === String(VISUAL_FLOW_VERSION));
582
+ if (!verOk) {
583
+ return { ok: false, message: `visualFlow.version 须为 ${VISUAL_FLOW_VERSION}` };
584
+ }
585
+ if (!Array.isArray(o.steps)) {
586
+ return { ok: false, message: 'visualFlow.steps 须为数组' };
587
+ }
588
+ if (o.steps.length > 500) {
589
+ return { ok: false, message: '步骤数量超过上限 500' };
590
+ }
591
+ const scriptVarsParsed = parseScriptVars(o.scriptVars);
592
+ if (!scriptVarsParsed.ok)
593
+ return scriptVarsParsed;
594
+ const steps = [];
595
+ for (let i = 0; i < o.steps.length; i++) {
596
+ const r = parseStep(o.steps[i], `steps[${i}]`);
597
+ if (!r.ok)
598
+ return r;
599
+ steps.push(r.step);
600
+ }
601
+ const seq = validateSetVarSequential(steps, new Set(scriptVarsParsed.value.map((v) => v.name)), 'steps');
602
+ if (!seq.ok)
603
+ return seq;
604
+ const declaredVarNames = new Set(scriptVarsParsed.value.map((v) => v.name));
605
+ collectDeclaredVarNames(steps, declaredVarNames);
606
+ const interpolated = validateInterpolatedVariableReferences(steps, declaredVarNames, 'steps');
607
+ if (!interpolated.ok)
608
+ return interpolated;
609
+ return {
610
+ ok: true,
611
+ value: {
612
+ version: VISUAL_FLOW_VERSION,
613
+ ...(scriptVarsParsed.value.length ? { scriptVars: scriptVarsParsed.value } : {}),
614
+ steps,
615
+ },
616
+ };
617
+ }
@@ -0,0 +1,24 @@
1
+ function isCommandShape(value) {
2
+ if (!value || typeof value !== "object")
3
+ return false;
4
+ const t = value.type;
5
+ return typeof t === "string" && t.length > 0;
6
+ }
7
+ /**
8
+ * 解析入站 JSON:支持信封 `{ deliveryId, command }` 与旧版裸命令(仅 `command`)。
9
+ */
10
+ export function parseInboundCommandJson(raw) {
11
+ const parsed = JSON.parse(raw);
12
+ if (parsed && typeof parsed === "object" && "command" in parsed) {
13
+ const o = parsed;
14
+ const cmd = o.command;
15
+ const idRaw = o.deliveryId;
16
+ if (isCommandShape(cmd) && typeof idRaw === "string" && idRaw.trim()) {
17
+ return { deliveryId: idRaw.trim(), command: cmd };
18
+ }
19
+ }
20
+ if (!isCommandShape(parsed)) {
21
+ throw new Error("invalid command payload: missing type");
22
+ }
23
+ return { command: parsed };
24
+ }
@@ -0,0 +1 @@
1
+ export { parseInboundCommandJson } from "./envelope.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};