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,576 @@
|
|
|
1
|
+
import { tryParseVisualFlow } from './validate.js';
|
|
2
|
+
/**
|
|
3
|
+
* 注入到生成脚本顶部;与调试下发的 `MIDSCENE_FLOW_*` 配合。
|
|
4
|
+
*/
|
|
5
|
+
export const FLOW_STEP_RUNTIME_PRELUDE = `
|
|
6
|
+
function __flowStepLog(type: 'start' | 'end' | 'error', stepIndex: number, extra?: Record<string, unknown>): void {
|
|
7
|
+
try {
|
|
8
|
+
console.log('__FLOW_STEP_EVENT__' + JSON.stringify({ type, stepIndex, ts: Date.now(), ...(extra ?? {}) }))
|
|
9
|
+
} catch {
|
|
10
|
+
// ignore logging failures
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function __flowStep(
|
|
15
|
+
stepIndex: number,
|
|
16
|
+
fn: () => Promise<void>,
|
|
17
|
+
meta?: { iteration?: number; subtreeSpan?: { min: number; max: number } },
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const mode = String(
|
|
20
|
+
process.env.MIDSCENE_FLOW_EXECUTION_MODE ?? 'full',
|
|
21
|
+
).toLowerCase()
|
|
22
|
+
const untilRaw = process.env.MIDSCENE_FLOW_UNTIL_STEP ?? ''
|
|
23
|
+
const targetRaw = process.env.MIDSCENE_FLOW_TARGET_STEP ?? ''
|
|
24
|
+
const until = Number.parseInt(untilRaw, 10)
|
|
25
|
+
const target = Number.parseInt(targetRaw, 10)
|
|
26
|
+
|
|
27
|
+
const span = meta?.subtreeSpan
|
|
28
|
+
const subtreeContains = (n: number): boolean =>
|
|
29
|
+
span != null &&
|
|
30
|
+
Number.isFinite(span.min) &&
|
|
31
|
+
Number.isFinite(span.max) &&
|
|
32
|
+
Number.isFinite(n) &&
|
|
33
|
+
n >= span.min &&
|
|
34
|
+
n <= span.max
|
|
35
|
+
|
|
36
|
+
const shouldRun = (): boolean => {
|
|
37
|
+
if (mode === 'single') {
|
|
38
|
+
if (!Number.isFinite(target)) return false
|
|
39
|
+
if (stepIndex === target) return true
|
|
40
|
+
if (subtreeContains(target)) return true
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
if (mode === 'run_to') return Number.isFinite(until) ? stepIndex <= until : true
|
|
44
|
+
if (mode === 'from_current') {
|
|
45
|
+
if (!Number.isFinite(target)) return false
|
|
46
|
+
if (stepIndex >= target) return true
|
|
47
|
+
if (subtreeContains(target)) return true
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
if (!shouldRun()) return
|
|
53
|
+
|
|
54
|
+
/** 供 Agent midscene-device-session 在 dump 回调里写入各 task 的 flowStepIndex,与编排步骤 100% 对齐 */
|
|
55
|
+
process.env.MIDSCENE_FLOW_STEP_INDEX = String(stepIndex)
|
|
56
|
+
|
|
57
|
+
const startedAt = Date.now()
|
|
58
|
+
const iter = meta?.iteration
|
|
59
|
+
const iterExtra = iter != null && Number.isFinite(iter) ? { iteration: iter } : {}
|
|
60
|
+
__flowStepLog('start', stepIndex, iterExtra)
|
|
61
|
+
try {
|
|
62
|
+
await fn()
|
|
63
|
+
__flowStepLog('end', stepIndex, { ...iterExtra, durationMs: Math.max(0, Date.now() - startedAt) })
|
|
64
|
+
} catch (e) {
|
|
65
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
66
|
+
__flowStepLog('error', stepIndex, { ...iterExtra, message, durationMs: Math.max(0, Date.now() - startedAt) })
|
|
67
|
+
throw e
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (mode === 'single' && stepIndex === target) {
|
|
71
|
+
process.exit(0)
|
|
72
|
+
}
|
|
73
|
+
if (mode === 'run_to' && Number.isFinite(until) && stepIndex === until) {
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const __flowVars = Object.create(null) as Record<string, unknown>
|
|
78
|
+
|
|
79
|
+
function __flowLogVar(name: string, value: unknown): void {
|
|
80
|
+
try {
|
|
81
|
+
const rendered =
|
|
82
|
+
value == null
|
|
83
|
+
? String(value)
|
|
84
|
+
: typeof value === 'string'
|
|
85
|
+
? value
|
|
86
|
+
: JSON.stringify(value)
|
|
87
|
+
console.log('__FLOW_VAR__' + JSON.stringify({ name, value: rendered, ts: Date.now() }))
|
|
88
|
+
} catch {
|
|
89
|
+
console.log('__FLOW_VAR__' + JSON.stringify({ name, value: String(value), ts: Date.now() }))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function __flowCutText(source: string, start: string, end: string): string {
|
|
94
|
+
let out = source
|
|
95
|
+
if (start) {
|
|
96
|
+
const idx = out.indexOf(start)
|
|
97
|
+
if (idx >= 0) out = out.slice(idx + start.length)
|
|
98
|
+
}
|
|
99
|
+
if (end) {
|
|
100
|
+
const idx = out.indexOf(end)
|
|
101
|
+
if (idx >= 0) out = out.slice(0, idx)
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function __flowJsonPath(source: string, path: string): string {
|
|
107
|
+
if (!path) return source
|
|
108
|
+
try {
|
|
109
|
+
let cur: unknown = JSON.parse(source)
|
|
110
|
+
const parts = path
|
|
111
|
+
.replace(/^\\$\\.?/, '')
|
|
112
|
+
.replace(/\\[(\\d+)\\]/g, '.$1')
|
|
113
|
+
.split('.')
|
|
114
|
+
.map((x) => x.trim())
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
for (const p of parts) {
|
|
117
|
+
if (cur == null) return ''
|
|
118
|
+
if (Array.isArray(cur)) cur = cur[Number.parseInt(p, 10)]
|
|
119
|
+
else if (typeof cur === 'object') cur = (cur as Record<string, unknown>)[p]
|
|
120
|
+
else return ''
|
|
121
|
+
}
|
|
122
|
+
return cur == null ? '' : typeof cur === 'string' ? cur : JSON.stringify(cur)
|
|
123
|
+
} catch {
|
|
124
|
+
return ''
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function __flowHandleAmount(source: string): string {
|
|
129
|
+
const cleaned = String(source ?? '').replace(/[^\\d.-]+/g, '')
|
|
130
|
+
return cleaned
|
|
131
|
+
}
|
|
132
|
+
`.trim();
|
|
133
|
+
/** @deprecated 使用 {@link FLOW_STEP_RUNTIME_PRELUDE} */
|
|
134
|
+
export const DEPRECATED_STEP_RUNTIME_PRELUDE = FLOW_STEP_RUNTIME_PRELUDE;
|
|
135
|
+
function lit(s) {
|
|
136
|
+
return JSON.stringify(s);
|
|
137
|
+
}
|
|
138
|
+
export function wrapCalledScriptBlockMarkers(title, scriptId, scopeId, body) {
|
|
139
|
+
const safeName = title.replace(/\r|\n|\*/g, ' ').trim() || '未命名';
|
|
140
|
+
const content = (body ?? '').replace(/\r\n/g, '\n');
|
|
141
|
+
const lines = [`// --- 调用脚本「${safeName}」 id=${scriptId} scope=${scopeId} ---`];
|
|
142
|
+
if (content.trim())
|
|
143
|
+
lines.push(content.trimEnd());
|
|
144
|
+
lines.push(`// --- 结束「${safeName}」 ---`);
|
|
145
|
+
return `\n${lines.join('\n')}\n`;
|
|
146
|
+
}
|
|
147
|
+
/** 读取编排变量;支持 `{{var}}` / `{{var[0]}}` / `{{var.1}}` / `{{var.length}}` */
|
|
148
|
+
function emitVarRead(varName, index, useLength = false) {
|
|
149
|
+
const k = JSON.stringify(varName);
|
|
150
|
+
if (useLength) {
|
|
151
|
+
return `String(Array.isArray(__flowVars[${k}]) ? (__flowVars[${k}] as unknown[]).length : 0)`;
|
|
152
|
+
}
|
|
153
|
+
if (index == null) {
|
|
154
|
+
return `String(__flowVars[${k}] ?? '')`;
|
|
155
|
+
}
|
|
156
|
+
return `String((Array.isArray(__flowVars[${k}]) ? (__flowVars[${k}] as unknown[])[${index}] : undefined) ?? '')`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 将 `{{var}}`、`{{var[0]}}`、`{{var.1}}`、`{{var.length}}` 展开为读取 `__flowVars` 的 TS 表达式(与 Midscene 无耦合,仅字符串拼接)。
|
|
160
|
+
*/
|
|
161
|
+
function emitInterpolatedExpr(s) {
|
|
162
|
+
const re = /\{\{([$_\p{L}][\p{L}\p{N}_$]*)(?:\[(\d+)\]|\.(\d+|length))?\}\}/gu;
|
|
163
|
+
if (!re.test(s)) {
|
|
164
|
+
return lit(s);
|
|
165
|
+
}
|
|
166
|
+
re.lastIndex = 0;
|
|
167
|
+
const parts = [];
|
|
168
|
+
let last = 0;
|
|
169
|
+
let m;
|
|
170
|
+
while ((m = re.exec(s)) !== null) {
|
|
171
|
+
const stat = s.slice(last, m.index);
|
|
172
|
+
if (stat.length > 0)
|
|
173
|
+
parts.push(lit(stat));
|
|
174
|
+
const id = m[1];
|
|
175
|
+
const bracket = m[2];
|
|
176
|
+
const dot = m[3];
|
|
177
|
+
const useLength = dot === 'length';
|
|
178
|
+
const rawIdx = bracket != null && bracket !== ''
|
|
179
|
+
? Number.parseInt(bracket, 10)
|
|
180
|
+
: dot != null && dot !== '' && dot !== 'length'
|
|
181
|
+
? Number.parseInt(dot, 10)
|
|
182
|
+
: NaN;
|
|
183
|
+
const idx = Number.isFinite(rawIdx) ? rawIdx : null;
|
|
184
|
+
parts.push(emitVarRead(id, idx, useLength));
|
|
185
|
+
last = m.index + m[0].length;
|
|
186
|
+
}
|
|
187
|
+
const tail = s.slice(last);
|
|
188
|
+
if (tail.length > 0)
|
|
189
|
+
parts.push(lit(tail));
|
|
190
|
+
if (parts.length === 0)
|
|
191
|
+
return lit('');
|
|
192
|
+
if (parts.length === 1)
|
|
193
|
+
return parts[0];
|
|
194
|
+
return `(${parts.join(' + ')})`;
|
|
195
|
+
}
|
|
196
|
+
/** aiQuery 第一个参数:JSON demand 内联为对象/数组字面量,否则为(可插值的)字符串 */
|
|
197
|
+
function emitAiQueryDataDemandExpr(expr) {
|
|
198
|
+
const t = expr.trim();
|
|
199
|
+
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
|
|
200
|
+
try {
|
|
201
|
+
const obj = JSON.parse(t);
|
|
202
|
+
return JSON.stringify(obj);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* fall through */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return emitInterpolatedExpr(expr);
|
|
209
|
+
}
|
|
210
|
+
/** 按 Midscene `aiQuery<T>(dataDemand: string | object, options?)` 推断常用 T */
|
|
211
|
+
function inferAiQueryGeneric(expr) {
|
|
212
|
+
const t = expr.trim();
|
|
213
|
+
if (!t)
|
|
214
|
+
return '';
|
|
215
|
+
if (t.startsWith('{') && t.endsWith('}')) {
|
|
216
|
+
try {
|
|
217
|
+
JSON.parse(t);
|
|
218
|
+
return '<Record<string, unknown>>';
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (t.startsWith('[') && t.endsWith(']')) {
|
|
225
|
+
try {
|
|
226
|
+
JSON.parse(t);
|
|
227
|
+
return '<unknown[]>';
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const first = (t.split(/[,,]/)[0] ?? '').trim().toLowerCase();
|
|
234
|
+
if (first.startsWith('string[]') || first === 'string[]')
|
|
235
|
+
return '<string[]>';
|
|
236
|
+
if (first.startsWith('string'))
|
|
237
|
+
return '<string>';
|
|
238
|
+
if (first.startsWith('number'))
|
|
239
|
+
return '<number>';
|
|
240
|
+
if (first.startsWith('boolean'))
|
|
241
|
+
return '<boolean>';
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
/** 生成 `await agent.aiQuery<…>(dataDemand)`,满足泛型签名 */
|
|
245
|
+
function emitAiQueryCall(expr) {
|
|
246
|
+
const inner = emitAiQueryDataDemandExpr(expr);
|
|
247
|
+
const gen = inferAiQueryGeneric(expr);
|
|
248
|
+
return gen ? `await agent.aiQuery${gen}(${inner})` : `await agent.aiQuery(${inner})`;
|
|
249
|
+
}
|
|
250
|
+
function emitRootScriptVars(flow) {
|
|
251
|
+
const vars = flow.scriptVars ?? [];
|
|
252
|
+
if (!vars.length)
|
|
253
|
+
return '';
|
|
254
|
+
return vars
|
|
255
|
+
.map((v) => `__flowVars[${JSON.stringify(v.name)}] = __str(${JSON.stringify(v.name)});`)
|
|
256
|
+
.join('\n');
|
|
257
|
+
}
|
|
258
|
+
function emitCalledFlowScriptVarScope(flow, step, ctx, indent, inner, inheritedOverrides) {
|
|
259
|
+
const vars = flow.scriptVars ?? [];
|
|
260
|
+
if (!vars.length)
|
|
261
|
+
return inner;
|
|
262
|
+
const token = `__qsv${++ctx.tempVarCounter.n}`;
|
|
263
|
+
const isolatedVars = vars.filter((v) => v.scope !== 'global');
|
|
264
|
+
const isolatedNames = isolatedVars.map((v) => v.name);
|
|
265
|
+
const assigned = vars
|
|
266
|
+
.map((v) => {
|
|
267
|
+
const name = v.name;
|
|
268
|
+
const key = JSON.stringify(name);
|
|
269
|
+
const raw = step.varBindings?.[name] ?? inheritedOverrides[name];
|
|
270
|
+
if (raw != null)
|
|
271
|
+
return `${indent} __flowVars[${key}] = ${emitInterpolatedExpr(raw)};`;
|
|
272
|
+
const fallback = JSON.stringify(v.defaultValue ?? '');
|
|
273
|
+
if (v.scope === 'global') {
|
|
274
|
+
return `${indent} if (!Object.prototype.hasOwnProperty.call(__flowVars, ${key})) __flowVars[${key}] = ${fallback};`;
|
|
275
|
+
}
|
|
276
|
+
return `${indent} __flowVars[${key}] = ${fallback};`;
|
|
277
|
+
})
|
|
278
|
+
.join('\n');
|
|
279
|
+
const restore = isolatedNames
|
|
280
|
+
.map((name) => {
|
|
281
|
+
const key = JSON.stringify(name);
|
|
282
|
+
return `${indent} if (Object.prototype.hasOwnProperty.call(${token}, ${key})) __flowVars[${key}] = ${token}[${key}]; else delete __flowVars[${key}];`;
|
|
283
|
+
})
|
|
284
|
+
.join('\n');
|
|
285
|
+
if (!isolatedNames.length) {
|
|
286
|
+
return `${assigned}\n${inner}`;
|
|
287
|
+
}
|
|
288
|
+
return (`${indent}{\n` +
|
|
289
|
+
`${indent} const ${token} = Object.create(null) as Record<string, unknown>;\n` +
|
|
290
|
+
isolatedNames.map((name) => `${indent} ${token}[${JSON.stringify(name)}] = __flowVars[${JSON.stringify(name)}];`).join('\n') +
|
|
291
|
+
`\n${assigned}\n` +
|
|
292
|
+
`${indent} try {\n` +
|
|
293
|
+
`${inner}\n` +
|
|
294
|
+
`${indent} } finally {\n` +
|
|
295
|
+
`${restore}\n` +
|
|
296
|
+
`${indent} }\n` +
|
|
297
|
+
`${indent}}`);
|
|
298
|
+
}
|
|
299
|
+
function emitLeaf(step, indent) {
|
|
300
|
+
switch (step.type) {
|
|
301
|
+
case 'launch':
|
|
302
|
+
return `${indent}await agent.launch(${emitInterpolatedExpr(step.packageName)});`;
|
|
303
|
+
case 'installApp':
|
|
304
|
+
return `${indent}await installApp(${emitInterpolatedExpr(step.appRef)});`;
|
|
305
|
+
case 'uninstallApp':
|
|
306
|
+
return `${indent}await uninstallApp(${emitInterpolatedExpr(step.bundleId)});`;
|
|
307
|
+
case 'assert':
|
|
308
|
+
return `${indent}await agent.aiAssert(${emitInterpolatedExpr(step.prompt)});`;
|
|
309
|
+
case 'sleep':
|
|
310
|
+
return `${indent}await sleep(${step.ms});`;
|
|
311
|
+
case 'aiAct':
|
|
312
|
+
return `${indent}await agent.aiAct(${emitInterpolatedExpr(step.prompt)});`;
|
|
313
|
+
case 'setAIActContext':
|
|
314
|
+
return `${indent}await agent.setAIActContext(${emitInterpolatedExpr(step.prompt)});`;
|
|
315
|
+
case 'recordToReport': {
|
|
316
|
+
const titleT = step.title.trim();
|
|
317
|
+
const contentT = step.content.trim();
|
|
318
|
+
if (!titleT && !contentT) {
|
|
319
|
+
return `${indent}await agent.recordToReport();`;
|
|
320
|
+
}
|
|
321
|
+
if (titleT && contentT) {
|
|
322
|
+
return `${indent}await agent.recordToReport(${emitInterpolatedExpr(step.title)}, { content: ${emitInterpolatedExpr(step.content)} });`;
|
|
323
|
+
}
|
|
324
|
+
if (titleT) {
|
|
325
|
+
return `${indent}await agent.recordToReport(${emitInterpolatedExpr(step.title)});`;
|
|
326
|
+
}
|
|
327
|
+
return `${indent}await agent.recordToReport(undefined, { content: ${emitInterpolatedExpr(step.content)} });`;
|
|
328
|
+
}
|
|
329
|
+
case 'assignVar':
|
|
330
|
+
return `${indent}__flowVars[${JSON.stringify(step.name)}] = ${emitInterpolatedExpr(step.value)};`;
|
|
331
|
+
case 'transformVar': {
|
|
332
|
+
const key = JSON.stringify(step.name);
|
|
333
|
+
const source = step.source != null ? emitInterpolatedExpr(step.source) : `String(__flowVars[${key}] ?? '')`;
|
|
334
|
+
if (step.rule === 'onlyNumber') {
|
|
335
|
+
return `${indent}__flowVars[${key}] = String(${source}).replace(/\\D+/g, '');`;
|
|
336
|
+
}
|
|
337
|
+
if (step.rule === 'cut') {
|
|
338
|
+
return `${indent}__flowVars[${key}] = __flowCutText(String(${source}), ${emitInterpolatedExpr(step.start ?? '')}, ${emitInterpolatedExpr(step.end ?? '')});`;
|
|
339
|
+
}
|
|
340
|
+
if (step.rule === 'jsonPath') {
|
|
341
|
+
return `${indent}__flowVars[${key}] = __flowJsonPath(String(${source}), ${emitInterpolatedExpr(step.jsonPath ?? '')});`;
|
|
342
|
+
}
|
|
343
|
+
if (step.rule === 'replace') {
|
|
344
|
+
return `${indent}__flowVars[${key}] = String(${source}).replace(new RegExp(${emitInterpolatedExpr(step.pattern ?? '')}, 'g'), ${emitInterpolatedExpr(step.replacement ?? '')});`;
|
|
345
|
+
}
|
|
346
|
+
return `${indent}__flowVars[${key}] = __flowHandleAmount(String(${source}));`;
|
|
347
|
+
}
|
|
348
|
+
case 'closeApp':
|
|
349
|
+
return (`${indent}const __pkg = ${emitInterpolatedExpr(step.packageName)};\n` +
|
|
350
|
+
`${indent}if (typeof agent.terminate === 'function') {\n` +
|
|
351
|
+
`${indent} await agent.terminate(__pkg);\n` +
|
|
352
|
+
`${indent}} else {\n` +
|
|
353
|
+
`${indent} await agent.runAdbShell(\`am force-stop \${__pkg}\`);\n` +
|
|
354
|
+
`${indent}}`);
|
|
355
|
+
case 'if':
|
|
356
|
+
case 'whileLoop':
|
|
357
|
+
case 'forLoop':
|
|
358
|
+
case 'setVar':
|
|
359
|
+
case 'assignVar':
|
|
360
|
+
case 'transformVar':
|
|
361
|
+
case 'callScript':
|
|
362
|
+
case 'ifDeviceType':
|
|
363
|
+
return `${indent}// unexpected compound step`;
|
|
364
|
+
default:
|
|
365
|
+
return `${indent}// unexpected leaf`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function mergeFlowStepMeta(loopCtx, subtreeSpan) {
|
|
369
|
+
const iterationVar = loopCtx?.iterationVar;
|
|
370
|
+
const span = subtreeSpan && subtreeSpan.min <= subtreeSpan.max ? subtreeSpan : undefined;
|
|
371
|
+
if (!iterationVar && !span)
|
|
372
|
+
return undefined;
|
|
373
|
+
return { iterationVar, subtreeSpan: span };
|
|
374
|
+
}
|
|
375
|
+
function subtreeSpanAfterChildren(boundaryIndex, counter) {
|
|
376
|
+
if (counter.n > boundaryIndex)
|
|
377
|
+
return { min: boundaryIndex + 1, max: counter.n };
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
function emitFlowStepBlock(stepIndex, indent, innerBody, meta) {
|
|
381
|
+
const parts = [];
|
|
382
|
+
if (meta?.iterationVar)
|
|
383
|
+
parts.push(`iteration: ${meta.iterationVar}`);
|
|
384
|
+
if (meta?.subtreeSpan && meta.subtreeSpan.min <= meta.subtreeSpan.max) {
|
|
385
|
+
parts.push(`subtreeSpan: { min: ${meta.subtreeSpan.min}, max: ${meta.subtreeSpan.max} }`);
|
|
386
|
+
}
|
|
387
|
+
const metaSuffix = parts.length > 0 ? `, { ${parts.join(', ')} }` : '';
|
|
388
|
+
return `${indent}await __flowStep(${stepIndex}, async () => {\n${innerBody}\n${indent}}${metaSuffix});`;
|
|
389
|
+
}
|
|
390
|
+
async function emitStepsAsync(steps, counter, indent, ctx, loopCtx) {
|
|
391
|
+
const lines = [];
|
|
392
|
+
for (const step of steps) {
|
|
393
|
+
if (step.type === 'if') {
|
|
394
|
+
const idx = ++counter.n;
|
|
395
|
+
const boundary = counter.n;
|
|
396
|
+
const thenBlock = await emitStepsAsync(step.thenSteps, counter, `${indent} `, ctx, loopCtx);
|
|
397
|
+
const elseBlock = step.elseSteps && step.elseSteps.length > 0
|
|
398
|
+
? await emitStepsAsync(step.elseSteps, counter, `${indent} `, ctx, loopCtx)
|
|
399
|
+
: '';
|
|
400
|
+
const elseClause = elseBlock.trim().length > 0 ? ` else {\n${elseBlock}\n${indent} }` : '';
|
|
401
|
+
const inner = `${indent} const __c = await agent.aiBoolean(${emitInterpolatedExpr(step.conditionPrompt)});\n` +
|
|
402
|
+
`${indent} if (__c) {\n` +
|
|
403
|
+
`${thenBlock}\n` +
|
|
404
|
+
`${indent} }${elseClause}`;
|
|
405
|
+
const span = subtreeSpanAfterChildren(boundary, counter);
|
|
406
|
+
lines.push(emitFlowStepBlock(idx, indent, inner, mergeFlowStepMeta(loopCtx, span)));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (step.type === 'ifDeviceType') {
|
|
410
|
+
const idx = ++counter.n;
|
|
411
|
+
const boundary = counter.n;
|
|
412
|
+
const thenBlock = await emitStepsAsync(step.thenSteps, counter, `${indent} `, ctx, loopCtx);
|
|
413
|
+
const elseBlock = step.elseSteps && step.elseSteps.length > 0
|
|
414
|
+
? await emitStepsAsync(step.elseSteps, counter, `${indent} `, ctx, loopCtx)
|
|
415
|
+
: '';
|
|
416
|
+
const elseClause = elseBlock.trim().length > 0 ? ` else {\n${elseBlock}\n${indent} }` : '';
|
|
417
|
+
const want = JSON.stringify(step.interfaceType);
|
|
418
|
+
const inner = `${indent} const __plat = String(agent.interface?.interfaceType ?? '');\n` +
|
|
419
|
+
`${indent} if (__plat === ${want}) {\n` +
|
|
420
|
+
`${thenBlock}\n` +
|
|
421
|
+
`${indent} }${elseClause}`;
|
|
422
|
+
const span = subtreeSpanAfterChildren(boundary, counter);
|
|
423
|
+
lines.push(emitFlowStepBlock(idx, indent, inner, mergeFlowStepMeta(loopCtx, span)));
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (step.type === 'whileLoop') {
|
|
427
|
+
const idx = ++counter.n;
|
|
428
|
+
const boundary = counter.n;
|
|
429
|
+
const iterVar = `__qi${idx}`;
|
|
430
|
+
const bodyBlock = await emitStepsAsync(step.bodySteps, counter, `${indent} `, ctx, { iterationVar: iterVar });
|
|
431
|
+
const inner = `${indent} const __maxIter${idx} = ${step.maxIterations};\n` +
|
|
432
|
+
`${indent} for (let ${iterVar} = 0; ${iterVar} < __maxIter${idx}; ${iterVar}++) {\n` +
|
|
433
|
+
`${indent} const __cont = await agent.aiBoolean(${emitInterpolatedExpr(step.conditionPrompt)});\n` +
|
|
434
|
+
`${indent} if (!__cont) break;\n` +
|
|
435
|
+
`${bodyBlock}\n` +
|
|
436
|
+
`${indent} }`;
|
|
437
|
+
const span = subtreeSpanAfterChildren(boundary, counter);
|
|
438
|
+
lines.push(emitFlowStepBlock(idx, indent, inner, mergeFlowStepMeta(loopCtx, span)));
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (step.type === 'forLoop') {
|
|
442
|
+
const idx = ++counter.n;
|
|
443
|
+
const boundary = counter.n;
|
|
444
|
+
const iterVar = `__fi${idx}`;
|
|
445
|
+
const bodyBlock = await emitStepsAsync(step.bodySteps, counter, `${indent} `, ctx, { iterationVar: iterVar });
|
|
446
|
+
const inner = `${indent} const __cnt${idx} = ${step.count};\n` +
|
|
447
|
+
`${indent} for (let ${iterVar} = 0; ${iterVar} < __cnt${idx}; ${iterVar}++) {\n` +
|
|
448
|
+
`${bodyBlock}\n` +
|
|
449
|
+
`${indent} }`;
|
|
450
|
+
const span = subtreeSpanAfterChildren(boundary, counter);
|
|
451
|
+
lines.push(emitFlowStepBlock(idx, indent, inner, mergeFlowStepMeta(loopCtx, span)));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (step.type === 'setVar') {
|
|
455
|
+
const idx = ++counter.n;
|
|
456
|
+
const keyLit = JSON.stringify(step.name);
|
|
457
|
+
let rhs;
|
|
458
|
+
if (step.method === 'aiQuery') {
|
|
459
|
+
rhs = emitAiQueryCall(step.expression);
|
|
460
|
+
}
|
|
461
|
+
else if (step.method === 'aiAsk') {
|
|
462
|
+
rhs = `await agent.aiAsk(${emitInterpolatedExpr(step.expression)})`;
|
|
463
|
+
}
|
|
464
|
+
else if (step.method === 'aiBoolean') {
|
|
465
|
+
rhs = `await agent.aiBoolean(${emitInterpolatedExpr(step.expression)})`;
|
|
466
|
+
}
|
|
467
|
+
else if (step.method === 'aiNumber') {
|
|
468
|
+
rhs = `await agent.aiNumber(${emitInterpolatedExpr(step.expression)})`;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
rhs = `await agent.aiString(${emitInterpolatedExpr(step.expression)})`;
|
|
472
|
+
}
|
|
473
|
+
const inner = `${indent} __flowVars[${keyLit}] = ${rhs};\n` +
|
|
474
|
+
`${indent} __flowLogVar(${keyLit}, __flowVars[${keyLit}]);`;
|
|
475
|
+
lines.push(emitFlowStepBlock(idx, indent, inner, mergeFlowStepMeta(loopCtx, undefined)));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (step.type === 'callScript') {
|
|
479
|
+
const tid = step.targetTestCaseId.trim();
|
|
480
|
+
if (ctx.rootCaseId && tid === ctx.rootCaseId) {
|
|
481
|
+
throw new Error('「调用子脚本」指向了当前脚本自身');
|
|
482
|
+
}
|
|
483
|
+
if (ctx.calleeStack.has(tid)) {
|
|
484
|
+
throw new Error(`子脚本引用形成环: ${[...ctx.calleeStack, tid].join(' → ')}`);
|
|
485
|
+
}
|
|
486
|
+
const loaded = await ctx.loadCase(tid);
|
|
487
|
+
if (!loaded) {
|
|
488
|
+
throw new Error(`子脚本不存在或不可读: ${tid}`);
|
|
489
|
+
}
|
|
490
|
+
const calleeTitle = (step.targetName?.trim() || loaded.name || tid).replace(/\r|\n|\*/g, ' ').trim();
|
|
491
|
+
ctx.calleeStack.add(tid);
|
|
492
|
+
try {
|
|
493
|
+
const vfParsed = loaded.visualFlow != null ? tryParseVisualFlow(loaded.visualFlow) : { ok: false };
|
|
494
|
+
if (vfParsed.ok) {
|
|
495
|
+
const prevOverrides = ctx.scriptInputOverrides;
|
|
496
|
+
ctx.scriptInputOverrides = { ...ctx.scriptInputOverrides, ...(step.varBindings ?? {}) };
|
|
497
|
+
try {
|
|
498
|
+
const inner = await emitStepsAsync(vfParsed.value.steps, counter, `${indent} `, ctx, loopCtx);
|
|
499
|
+
if (inner.trim().length > 0) {
|
|
500
|
+
lines.push(emitCalledFlowScriptVarScope(vfParsed.value, step, ctx, indent, inner, prevOverrides));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
ctx.scriptInputOverrides = prevOverrides;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
const raw = (loaded.scriptContent ?? '').trim();
|
|
509
|
+
if (!raw) {
|
|
510
|
+
throw new Error(`子脚本「${calleeTitle}」无编排且无脚本正文`);
|
|
511
|
+
}
|
|
512
|
+
const idx = ++counter.n;
|
|
513
|
+
const block = wrapCalledScriptBlockMarkers(calleeTitle, loaded.id, step.scopeId, raw);
|
|
514
|
+
const innerLines = block
|
|
515
|
+
.split('\n')
|
|
516
|
+
.map((ln) => `${indent} ${ln}`)
|
|
517
|
+
.join('\n');
|
|
518
|
+
lines.push(emitFlowStepBlock(idx, indent, innerLines, mergeFlowStepMeta(loopCtx, undefined)));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
ctx.calleeStack.delete(tid);
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const idx = ++counter.n;
|
|
527
|
+
const body = emitLeaf(step, `${indent} `);
|
|
528
|
+
lines.push(emitFlowStepBlock(idx, indent, body, mergeFlowStepMeta(loopCtx, undefined)));
|
|
529
|
+
}
|
|
530
|
+
return lines.join('\n');
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* 生成可下发 Agent 的业务脚本(假定运行环境已注入 `agent` / `sleep` / `installApp` / `uninstallApp`)。
|
|
534
|
+
* 含 `callScript` 步骤时须提供 `loadCase` 以解析子用例;`rootCaseId` 用于识别自调用。
|
|
535
|
+
*/
|
|
536
|
+
export async function generateScriptFromVisualFlow(flow, opts) {
|
|
537
|
+
const counter = { n: 0 };
|
|
538
|
+
const ctx = {
|
|
539
|
+
rootCaseId: opts.rootCaseId,
|
|
540
|
+
calleeStack: new Set(),
|
|
541
|
+
loadCase: opts.loadCase,
|
|
542
|
+
scriptInputOverrides: {},
|
|
543
|
+
tempVarCounter: { n: 0 },
|
|
544
|
+
};
|
|
545
|
+
const scriptVarPrelude = emitRootScriptVars(flow);
|
|
546
|
+
const body = await emitStepsAsync(flow.steps, counter, '', ctx);
|
|
547
|
+
return `${FLOW_STEP_RUNTIME_PRELUDE}${scriptVarPrelude ? `\n${scriptVarPrelude}` : ''}\n\n${body}\n`;
|
|
548
|
+
}
|
|
549
|
+
/** 将脚本变量默认值与各 `callScript` 步骤的 `varBindings` 合并为 `scriptTemplateVars` 片段 */
|
|
550
|
+
export function aggregateCallScriptVarBindingsFromFlow(flow) {
|
|
551
|
+
const out = {};
|
|
552
|
+
for (const v of flow.scriptVars ?? []) {
|
|
553
|
+
out[v.name] = v.defaultValue ?? '';
|
|
554
|
+
}
|
|
555
|
+
const walk = (steps) => {
|
|
556
|
+
for (const s of steps) {
|
|
557
|
+
if (s.type === 'callScript') {
|
|
558
|
+
const sid = s.scopeId;
|
|
559
|
+
const vb = s.varBindings ?? {};
|
|
560
|
+
const prev = out[sid];
|
|
561
|
+
const prevObj = prev && typeof prev === 'object' && !Array.isArray(prev) ? prev : {};
|
|
562
|
+
out[sid] = { ...prevObj, ...vb };
|
|
563
|
+
}
|
|
564
|
+
else if (s.type === 'if' || s.type === 'ifDeviceType') {
|
|
565
|
+
walk(s.thenSteps);
|
|
566
|
+
if (s.elseSteps?.length)
|
|
567
|
+
walk(s.elseSteps);
|
|
568
|
+
}
|
|
569
|
+
else if (s.type === 'whileLoop' || s.type === 'forLoop') {
|
|
570
|
+
walk(s.bodySteps);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
walk(flow.steps ?? []);
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { generateScriptFromVisualFlow } from "./codegen.js";
|
|
2
|
+
import { tryParseVisualFlow } from "./validate.js";
|
|
3
|
+
export function validateVisualFlow(raw) {
|
|
4
|
+
return tryParseVisualFlow(raw);
|
|
5
|
+
}
|
|
6
|
+
export async function compileVisualFlow(raw) {
|
|
7
|
+
const parsed = tryParseVisualFlow(raw);
|
|
8
|
+
if (!parsed.ok) {
|
|
9
|
+
throw new Error(parsed.message);
|
|
10
|
+
}
|
|
11
|
+
return generateScriptFromVisualFlow(parsed.value, {
|
|
12
|
+
loadCase: async () => null,
|
|
13
|
+
});
|
|
14
|
+
}
|