@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2
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/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const h5Target = require('../shared-kernel/android-h5-target');
|
|
4
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
5
|
+
const { androidAppTargetKey, getProcessTargetLease } = require('../shared-kernel/target-lease-protocol');
|
|
6
|
+
const { parseXmlAttributes } = require('../shared-kernel/xml-attributes');
|
|
7
|
+
const { nativeWindow, validBounds, revalidateNativeNode, nativeTargetRequest } = require('../shared-kernel/native-target');
|
|
8
|
+
const { selectFlutterNode, flutterNodeIdentity, flutterTargetRequest } = require('../shared-kernel/flutter-target');
|
|
9
|
+
const { selectUiaNode } = require('../shared-kernel/uia-target');
|
|
10
|
+
const { observedTarget: observedUiaTarget } = require('../shared-kernel/uia-protocol');
|
|
11
|
+
const { checkExecution } = require('../shared-kernel/execution-scope');
|
|
12
|
+
|
|
13
|
+
function createProductionIntentDeviceAdapter({
|
|
14
|
+
lease = getProcessTargetLease(),
|
|
15
|
+
ports = null,
|
|
16
|
+
adb = process.env.ADB || 'adb',
|
|
17
|
+
} = {}) {
|
|
18
|
+
const impl = ports || require('../device-provider');
|
|
19
|
+
let currentActive = 0;
|
|
20
|
+
let maxActive = 0;
|
|
21
|
+
const calls = [];
|
|
22
|
+
|
|
23
|
+
async function withDevice(serial, packageName, name, fn) {
|
|
24
|
+
const waitStarted = Date.now();
|
|
25
|
+
const held = lease.acquire(androidAppTargetKey(serial, packageName));
|
|
26
|
+
if (!held.ok) {
|
|
27
|
+
return {
|
|
28
|
+
...held,
|
|
29
|
+
serial,
|
|
30
|
+
packageName,
|
|
31
|
+
targetLeaseWaitMs: Date.now() - waitStarted,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
currentActive += 1;
|
|
35
|
+
maxActive = Math.max(maxActive, currentActive);
|
|
36
|
+
calls.push({ name, serial, packageName, atMs: Date.now() });
|
|
37
|
+
try {
|
|
38
|
+
const result = await fn();
|
|
39
|
+
return {
|
|
40
|
+
...result,
|
|
41
|
+
adbTimings: [],
|
|
42
|
+
targetLeaseWaitMs: Date.now() - waitStarted,
|
|
43
|
+
};
|
|
44
|
+
} finally {
|
|
45
|
+
currentActive -= 1;
|
|
46
|
+
held.release();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function context(target) {
|
|
51
|
+
return impl.createBridgeContext({
|
|
52
|
+
serial: target.serial,
|
|
53
|
+
packageName: target.packageName,
|
|
54
|
+
port: target.port,
|
|
55
|
+
adb: target.adb || adb,
|
|
56
|
+
runtimeActionId: target.runtimeActionId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function foregroundRoute(ctx, provider, foregroundPackages) {
|
|
61
|
+
if (!Array.isArray(foregroundPackages) || foregroundPackages.some(value => typeof value !== 'string'
|
|
62
|
+
|| !/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+$/.test(value))) throw new Error('invalid_foreground_packages');
|
|
63
|
+
if (!['native', 'uia', 'flutter', 'h5'].includes(provider)) throw new Error('unsupported_provider');
|
|
64
|
+
const foreground = await impl.foregroundWindow(ctx);
|
|
65
|
+
if (!foreground?.ok) throw new Error(foreground?.error || 'foreground_probe_failed');
|
|
66
|
+
if (!foreground.packageName || !foreground.component || !foreground.activity) throw new Error('foreground_identity_required');
|
|
67
|
+
if (foreground.packageName !== ctx.packageName && !foregroundPackages.includes(foreground.packageName)) throw new Error('foreground_package_not_allowed');
|
|
68
|
+
return { provider: foreground.packageName === ctx.packageName ? provider : 'uia', packageName: foreground.packageName,
|
|
69
|
+
activity: foreground.activity, component: foreground.component, source: foreground.source, observedAtMs: Date.now() };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
calls,
|
|
74
|
+
adbTimings: [],
|
|
75
|
+
get maxActive() { return maxActive; },
|
|
76
|
+
get callCount() { return calls.length; },
|
|
77
|
+
async observe({ serial, packageName, provider, rawTreeId, port, adb: targetAdb, foregroundPackages, observationTarget }) {
|
|
78
|
+
return withDevice(serial, packageName, `observe:${provider}`, async () => {
|
|
79
|
+
const ctx = context({ serial, packageName, port, adb: targetAdb });
|
|
80
|
+
try {
|
|
81
|
+
const route = foregroundPackages === undefined ? null : await foregroundRoute(ctx, provider, foregroundPackages);
|
|
82
|
+
const activeProvider = route ? route.provider : provider;
|
|
83
|
+
const activeContext = route ? context({ serial, packageName: route.packageName, port, adb: targetAdb }) : ctx;
|
|
84
|
+
let rawTree;
|
|
85
|
+
if (activeProvider === 'uia') rawTree = await impl.uiaTreeOnce(activeContext);
|
|
86
|
+
else if (activeProvider === 'flutter') rawTree = await impl.flutterNodes(activeContext);
|
|
87
|
+
else if (activeProvider === 'h5') {
|
|
88
|
+
rawTree = await impl.h5Dom(activeContext, observationTarget || {});
|
|
89
|
+
if (rawTree.ok !== true) return rawTree;
|
|
90
|
+
}
|
|
91
|
+
else rawTree = await impl.bridgeTree(activeContext);
|
|
92
|
+
if (route) {
|
|
93
|
+
const after = await foregroundRoute(ctx, provider, foregroundPackages);
|
|
94
|
+
if (after.component !== route.component || after.provider !== route.provider) throw new Error('foreground_changed_during_observation');
|
|
95
|
+
if (activeProvider === 'uia') {
|
|
96
|
+
const root = typeof rawTree === 'string' && rawTree.match(/<node\b[^>]*>/);
|
|
97
|
+
if (!root || parseXmlAttributes(root[0]).package !== route.packageName) throw new Error('observed_foreground_package_mismatch');
|
|
98
|
+
}
|
|
99
|
+
route.verifiedAtMs = after.observedAtMs;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
provider: activeProvider,
|
|
104
|
+
rawTreeId,
|
|
105
|
+
rawTree,
|
|
106
|
+
foregroundTarget: route ? route.packageName : packageName,
|
|
107
|
+
...(route ? { route } : {}),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { ok: false, error: error.message || String(error) };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
async action({ serial, spec, packageName, port, adb: targetAdb, rawTree, actionId, foregroundPackages, route, primaryProvider }) {
|
|
115
|
+
const targetPackageName = packageName || spec.packageName;
|
|
116
|
+
return withDevice(serial, targetPackageName, 'action', async () => {
|
|
117
|
+
let ctx = context({ serial, packageName: targetPackageName, port, adb: targetAdb, runtimeActionId: actionId });
|
|
118
|
+
let dispatchAttempted = false;
|
|
119
|
+
const actionPorts = { ...impl };
|
|
120
|
+
for (const name of ['tap', 'uiaTap', 'inputText', 'nativeGesture', 'swipe', 'keyevent', 'flutterAction', 'h5Control']) {
|
|
121
|
+
if (typeof impl[name] !== 'function') continue;
|
|
122
|
+
actionPorts[name] = async (...args) => {
|
|
123
|
+
checkExecution();
|
|
124
|
+
if (route) {
|
|
125
|
+
const current = await foregroundRoute(context({ serial, packageName: targetPackageName, port, adb: targetAdb }), primaryProvider, foregroundPackages);
|
|
126
|
+
if (current.component !== route.component || current.provider !== route.provider) {
|
|
127
|
+
return { ok: false, error: 'reobserve_required', dispatched: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
checkExecution();
|
|
131
|
+
dispatchAttempted = true;
|
|
132
|
+
return impl[name](...args);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Foreground routing is explicit. Never reinterpret a prior tree after a
|
|
136
|
+
// package/activity change, and never send an SDK action to a system app.
|
|
137
|
+
if (foregroundPackages !== undefined) {
|
|
138
|
+
try {
|
|
139
|
+
if (!route) return shapeAction({ ok: false, error: 'observed_foreground_required', dispatched: false });
|
|
140
|
+
if (spec.provider !== route.provider) return shapeAction({ ok: false, error: 'observation_provider_mismatch', dispatched: false });
|
|
141
|
+
const current = await foregroundRoute(ctx, primaryProvider, foregroundPackages);
|
|
142
|
+
if (current.component !== route.component || current.provider !== route.provider) return shapeAction({ ok: false, error: 'reobserve_required', dispatched: false });
|
|
143
|
+
ctx = context({ serial, packageName: route.packageName, port, adb: targetAdb, runtimeActionId: actionId });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return shapeAction({ ok: false, error: error.message, dispatched: false });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (!['native', 'uia', 'flutter', 'h5'].includes(spec.provider)) return shapeAction({ ok: false, error: 'unsupported_provider', dispatched: false });
|
|
150
|
+
if (!['tap', 'inputText', 'back', 'keyevent', 'scroll', 'scrollBy', 'swipe', 'longPress', 'hideKeyboard'].includes(spec.action)) {
|
|
151
|
+
return shapeAction({ ok: false, error: 'unsupported_action', dispatched: false });
|
|
152
|
+
}
|
|
153
|
+
if (spec.provider === 'h5') {
|
|
154
|
+
const action = { tap: 'click', inputText: 'input', scroll: 'scroll' }[spec.action];
|
|
155
|
+
if (!action) return shapeAction({ ok: false, error: 'unsupported_android_h5_intent_action', dispatched: false });
|
|
156
|
+
const observed = h5Target.selectH5Node(rawTree, spec.selector);
|
|
157
|
+
if (!observed.ok) return shapeAction(observed);
|
|
158
|
+
return shapeAction(await actionPorts.h5Control(ctx, action, {
|
|
159
|
+
webViewId: observed.targetRef.pageRef.webViewId, selector: { elementId: observed.node.elementId },
|
|
160
|
+
expectedTarget: observed.targetRef, ...(action === 'input' ? { text: spec.value } : {}),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
if (spec.action === 'hideKeyboard') {
|
|
164
|
+
if (spec.provider !== 'flutter') return shapeAction({ ok: false, error: 'unsupported_action', dispatched: false });
|
|
165
|
+
return shapeAction(await actionPorts.flutterAction(ctx, { action: 'hideKeyboard' }, { runtimeActionId: actionId }));
|
|
166
|
+
}
|
|
167
|
+
if (spec.action === 'back' && spec.provider === 'flutter') {
|
|
168
|
+
const result = await actionPorts.flutterAction(ctx, { action: 'back' }, { runtimeActionId: actionId });
|
|
169
|
+
if (result?.handled === false) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
mechanicalStatus: 'failed',
|
|
173
|
+
providerResult: result,
|
|
174
|
+
ambiguous: false,
|
|
175
|
+
error: 'back_not_handled',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return shapeAction(result);
|
|
179
|
+
}
|
|
180
|
+
if (spec.action === 'back' || spec.action === 'keyevent') {
|
|
181
|
+
return shapeAction(await actionPorts.keyevent(ctx, spec.keyCode != null ? spec.keyCode : 4));
|
|
182
|
+
}
|
|
183
|
+
if (spec.action === 'longPress') {
|
|
184
|
+
if (spec.provider !== 'native') return shapeAction({ ok: false, error: 'long_press_provider_unsupported', dispatched: false });
|
|
185
|
+
return shapeAction(await gestureFromNativeObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
186
|
+
}
|
|
187
|
+
if (spec.action === 'swipe') {
|
|
188
|
+
if (spec.provider !== 'native') return shapeAction({ ok: false, error: 'swipe_provider_unsupported', dispatched: false });
|
|
189
|
+
return shapeAction(await gestureFromNativeObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
190
|
+
}
|
|
191
|
+
if (spec.action === 'scroll' || spec.action === 'scrollBy') {
|
|
192
|
+
if (spec.provider === 'flutter') {
|
|
193
|
+
return shapeAction(await actionFromFlutterObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
194
|
+
}
|
|
195
|
+
return shapeAction(await (spec.provider === 'native'
|
|
196
|
+
? gestureFromNativeObservedTree(actionPorts, ctx, spec, rawTree, actionId)
|
|
197
|
+
: scrollHost(actionPorts, ctx, spec, rawTree)));
|
|
198
|
+
}
|
|
199
|
+
if (spec.action === 'inputText') {
|
|
200
|
+
if (spec.provider === 'flutter') return shapeAction(await actionFromFlutterObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
201
|
+
if (spec.provider !== 'native') return shapeAction({ ok: false, error: 'inputText_provider_unsupported', dispatched: false });
|
|
202
|
+
return shapeAction(await inputFromNativeObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
203
|
+
}
|
|
204
|
+
if (spec.provider === 'flutter' && spec.action === 'tap') {
|
|
205
|
+
return shapeAction(await actionFromFlutterObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
206
|
+
}
|
|
207
|
+
if (spec.provider === 'uia' && spec.action === 'tap') {
|
|
208
|
+
return shapeAction(await tapUniqueUiaNode(actionPorts, ctx, spec, rawTree));
|
|
209
|
+
}
|
|
210
|
+
if (spec.provider === 'native') return shapeAction(await tapFromNativeObservedTree(actionPorts, ctx, spec, rawTree, actionId));
|
|
211
|
+
return shapeAction({ ok: false, error: 'unsupported_action_selector', dispatched: false });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
mechanicalStatus: 'failed',
|
|
216
|
+
providerResult: null,
|
|
217
|
+
ambiguous: error.ambiguous ?? dispatchAttempted,
|
|
218
|
+
dispatched: error.dispatched ?? dispatchAttempted,
|
|
219
|
+
settled: error.settled,
|
|
220
|
+
executionReceipt: error.executionReceipt ?? null,
|
|
221
|
+
error: error.code || error.message || String(error),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
async launch({ serial, packageName, kind, port, adb: targetAdb }) {
|
|
227
|
+
return withDevice(serial, packageName, `launch:${kind || 'app'}`, async () => {
|
|
228
|
+
const ctx = context({ serial, packageName, port, adb: targetAdb });
|
|
229
|
+
if (kind && kind !== 'app') return { ok: false, error: 'unsupported_launch_kind', dispatched: false };
|
|
230
|
+
return impl.launchApp(ctx);
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function tapUniqueUiaNode(impl, ctx, spec, rawTree) {
|
|
237
|
+
const observed = selectUiaNode(rawTree, spec, ctx.packageName);
|
|
238
|
+
if (!observed.ok) return observed;
|
|
239
|
+
const binding = observedUiaTarget(rawTree, observed.node, spec.selector || { text: spec.text }, ctx.packageName);
|
|
240
|
+
return impl.uiaTap(ctx, binding);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function tapFromNativeObservedTree(impl, ctx, spec, rawTree, actionId) {
|
|
244
|
+
const selected = await revalidateNativeNode(() => impl.bridgeTree(ctx), rawTree, spec);
|
|
245
|
+
if (!selected.ok) return selected;
|
|
246
|
+
const { x, y } = selected;
|
|
247
|
+
const guarded = nativeTargetRequest(selected.node, spec.selector || { text: spec.text });
|
|
248
|
+
if (!guarded.ok) return guarded;
|
|
249
|
+
return impl.tap(ctx, x, y, { nativeTarget: guarded.request, feedback: 'off', appLocalAction: true, runtimeActionId: actionId, requestId: actionId });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function inputFromNativeObservedTree(impl, ctx, spec, rawTree, actionId) {
|
|
253
|
+
if (typeof spec.value !== 'string') return { ok: false, error: 'input_value_required', dispatched: false };
|
|
254
|
+
const selected = await revalidateNativeNode(() => impl.bridgeTree(ctx), rawTree, spec, true);
|
|
255
|
+
if (!selected.ok) return selected;
|
|
256
|
+
const guarded = nativeTargetRequest(selected.node, spec.selector || { text: spec.text });
|
|
257
|
+
if (!guarded.ok) return guarded;
|
|
258
|
+
if (typeof impl.inputText !== 'function') return { ok: false, error: 'inputText_port_unavailable', dispatched: false };
|
|
259
|
+
return impl.inputText(ctx, spec.value, { nativeTarget: guarded.request,
|
|
260
|
+
feedback: 'off', appLocalAction: true, runtimeActionId: actionId, requestId: actionId });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function gestureFromNativeObservedTree(impl, ctx, spec, rawTree, actionId) {
|
|
264
|
+
const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
|
|
265
|
+
const durationMs = spec.action === 'scroll' && spec.durationMs === undefined ? 400 : spec.durationMs;
|
|
266
|
+
if (!Number.isSafeInteger(durationMs) || durationMs < (spec.action === 'longPress' ? 500 : 1) || durationMs > 10000) {
|
|
267
|
+
return reject(spec.action === 'longPress' ? 'invalid_long_press_duration' : 'invalid_swipe_duration');
|
|
268
|
+
}
|
|
269
|
+
if (spec.action === 'swipe' && (!Number.isFinite(spec.deltaX) || !Number.isFinite(spec.deltaY))) return reject('invalid_swipe_delta');
|
|
270
|
+
if (spec.action === 'scroll' && !['up', 'down'].includes(spec.direction)) return reject('unsupported_scroll_direction');
|
|
271
|
+
if (!spec.selector) return reject('explicit_native_selector_required');
|
|
272
|
+
const selected = await revalidateNativeNode(() => impl.bridgeTree(ctx), rawTree, spec);
|
|
273
|
+
if (!selected.ok) return selected;
|
|
274
|
+
const guarded = nativeTargetRequest(selected.node, spec.selector);
|
|
275
|
+
if (!guarded.ok) return guarded;
|
|
276
|
+
if (typeof impl.nativeGesture !== 'function') return reject('native_gesture_port_unavailable');
|
|
277
|
+
return impl.nativeGesture(ctx, { ...guarded.request, action: spec.action, actionId, durationMs,
|
|
278
|
+
...(spec.action === 'swipe' ? { deltaX: spec.deltaX, deltaY: spec.deltaY } : {}),
|
|
279
|
+
...(spec.action === 'scroll' ? { direction: spec.direction } : {}) });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function scrollHost(impl, ctx, spec, rawTree) {
|
|
283
|
+
if (!rawTree) return { ok: false, error: 'observed_tree_required', dispatched: false };
|
|
284
|
+
const currentTree = await impl.uiaTreeOnce(ctx);
|
|
285
|
+
checkExecution();
|
|
286
|
+
const viewport = impl.parseUiaViewport(currentTree);
|
|
287
|
+
if (!viewport || (!viewport.width || !viewport.height) && !validBounds(viewport)) return { ok: false, error: 'viewport_unavailable', dispatched: false };
|
|
288
|
+
const width = viewport.width || viewport.right - viewport.left;
|
|
289
|
+
const height = viewport.height || viewport.bottom - viewport.top;
|
|
290
|
+
if (spec.direction != null && !['up', 'down'].includes(spec.direction)) return { ok: false, error: 'unsupported_scroll_direction', dispatched: false };
|
|
291
|
+
const startX = Math.round(viewport.left + width / 2);
|
|
292
|
+
const startY = Math.round(viewport.top + height * (spec.direction === 'up' ? 0.22 : 0.76));
|
|
293
|
+
const endY = Math.round(viewport.top + height * (spec.direction === 'up' ? 0.76 : 0.22));
|
|
294
|
+
return impl.swipe(ctx, startX, startY, startX, endY, spec.durationMs != null ? spec.durationMs : 400);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function actionFromFlutterObservedTree(impl, ctx, spec, rawTree, actionId) {
|
|
298
|
+
const observed = selectFlutterNode(rawTree, spec);
|
|
299
|
+
if (!observed.ok) return observed;
|
|
300
|
+
const foreground = nativeWindow(await impl.bridgeTree(ctx));
|
|
301
|
+
if (!foreground || foreground.type !== 'activity') return { ok: false, error: 'native_foreground_blocks_flutter', dispatched: false };
|
|
302
|
+
const current = selectFlutterNode(await impl.flutterNodes(ctx), spec);
|
|
303
|
+
checkExecution();
|
|
304
|
+
if (!current.ok) return current;
|
|
305
|
+
if (!isDeepStrictEqual(flutterNodeIdentity(observed.node), flutterNodeIdentity(current.node))) {
|
|
306
|
+
return { ok: false, error: 'reobserve_required', dispatched: false };
|
|
307
|
+
}
|
|
308
|
+
const guarded = flutterTargetRequest(current.node, spec.selector);
|
|
309
|
+
if (!guarded.ok) return guarded;
|
|
310
|
+
const payload = spec.action === 'tap' ? { action: 'tapTarget' }
|
|
311
|
+
: spec.action === 'inputText' ? { action: 'inputText', text: spec.value }
|
|
312
|
+
: { action: 'scrollBy', delta: spec.delta };
|
|
313
|
+
return impl.flutterAction(ctx, { ...payload, ...guarded.request }, { runtimeActionId: actionId });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function shapeAction(result) {
|
|
317
|
+
if (!result || typeof result.ok !== 'boolean') return {
|
|
318
|
+
ok: false, mechanicalStatus: 'failed', providerResult: result ?? null,
|
|
319
|
+
ambiguous: true, error: 'invalid_action_receipt',
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
ok: result.ok,
|
|
323
|
+
mechanicalStatus: result.ok ? 'ok' : 'failed',
|
|
324
|
+
providerResult: result,
|
|
325
|
+
ambiguous: result.ambiguous === true,
|
|
326
|
+
dispatched: result.dispatched ?? (result.ok ? true : null),
|
|
327
|
+
executionReceipt: result.executionReceipt ?? null,
|
|
328
|
+
error: result?.error || null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
createProductionIntentDeviceAdapter,
|
|
334
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function intentProviderError(target, provider) {
|
|
4
|
+
if (!['android', 'ios', 'web'].includes(target.platform)) return {
|
|
5
|
+
error: 'unsupported_intent_platform', field: 'target.platform', platform: target.platform,
|
|
6
|
+
message: 'Intent observation/action binding is not implemented for this platform yet.',
|
|
7
|
+
};
|
|
8
|
+
if (target.platform === 'web' && provider !== 'h5') return {
|
|
9
|
+
error: 'unsupported_web_intent_provider', field: 'provider', provider,
|
|
10
|
+
message: 'Web Intent uses provider h5 with the original connected document session and runtimeEpoch.',
|
|
11
|
+
};
|
|
12
|
+
if (target.platform === 'ios' && !['native', 'h5', 'flutter'].includes(provider)) return {
|
|
13
|
+
error: 'unsupported_ios_intent_provider', field: 'provider', provider,
|
|
14
|
+
message: 'iOS Intent supports native through an explicit WDA session, and h5 or flutter through the bound SDK.',
|
|
15
|
+
};
|
|
16
|
+
if (target.platform === 'android' && !['native', 'uia', 'flutter', 'h5'].includes(provider)) return {
|
|
17
|
+
error: 'unsupported_android_intent_provider', field: 'provider', provider,
|
|
18
|
+
message: 'Android Intent supports native, uia, flutter and h5 with explicit observation binding.',
|
|
19
|
+
};
|
|
20
|
+
if (target.platform === 'ios' && provider === 'native' && (!target.wdaRunnerBundleId || !target.wdaSessionId)) return {
|
|
21
|
+
error: 'ios_wda_session_required', field: 'target.wdaSessionId',
|
|
22
|
+
message: 'Native iOS Intent requires wdaRunnerBundleId and an explicit ios-wda-session for the selected foreground App.',
|
|
23
|
+
};
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { intentProviderError };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createIntentRuntime({
|
|
4
|
+
operationId,
|
|
5
|
+
now = Date.now,
|
|
6
|
+
maxEvents = 256,
|
|
7
|
+
maxEventBytes = 256 * 1024,
|
|
8
|
+
} = {}) {
|
|
9
|
+
const events = [];
|
|
10
|
+
let sequence = 0;
|
|
11
|
+
let eventBytes = 0;
|
|
12
|
+
let droppedEvents = 0;
|
|
13
|
+
const state = {
|
|
14
|
+
operationId,
|
|
15
|
+
status: 'created',
|
|
16
|
+
revision: 1,
|
|
17
|
+
error: null,
|
|
18
|
+
lastDecisionId: null,
|
|
19
|
+
seenDecisionIds: new Set(),
|
|
20
|
+
mode: 'supervised',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function emit(type, extra = {}) {
|
|
24
|
+
sequence += 1;
|
|
25
|
+
const event = { sequence, type, atMs: now(), status: state.status, ...extra };
|
|
26
|
+
const encoded = Buffer.byteLength(JSON.stringify(event), 'utf8');
|
|
27
|
+
events.push({ event, encoded });
|
|
28
|
+
eventBytes += encoded;
|
|
29
|
+
while (events.length > maxEvents || eventBytes > maxEventBytes) {
|
|
30
|
+
const removed = events.shift();
|
|
31
|
+
if (!removed) break;
|
|
32
|
+
eventBytes -= removed.encoded;
|
|
33
|
+
droppedEvents += 1;
|
|
34
|
+
}
|
|
35
|
+
return event;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snapshot() {
|
|
39
|
+
return {
|
|
40
|
+
command: 'intent',
|
|
41
|
+
operationId,
|
|
42
|
+
status: state.status,
|
|
43
|
+
revision: state.revision,
|
|
44
|
+
error: state.error,
|
|
45
|
+
lastDecisionId: state.lastDecisionId,
|
|
46
|
+
mode: state.mode,
|
|
47
|
+
eventSequence: sequence,
|
|
48
|
+
events: events.map((item) => item.event),
|
|
49
|
+
eventGap: droppedEvents > 0,
|
|
50
|
+
droppedEvents,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
state,
|
|
56
|
+
emit,
|
|
57
|
+
snapshot,
|
|
58
|
+
get events() { return events.map((item) => item.event); },
|
|
59
|
+
get eventBytes() { return eventBytes; },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { createIntentRuntime };
|