@mobileaidev/ai-app-bridge-web 0.3.5 → 0.3.7
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/README.md +11 -7
- package/package.json +1 -1
- package/src/index.js +44 -5
package/README.md
CHANGED
|
@@ -115,10 +115,11 @@ For SSR frameworks, initialize it only in client/browser code.
|
|
|
115
115
|
</script>
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
##
|
|
118
|
+
## Bounded UI Observation
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
UI observation is off by default, including after `bridge.start()`.
|
|
121
|
+
`capture.ui` configures an explicitly requested window; `false` disables this
|
|
122
|
+
capability. Set it to `true` for defaults, or pass an object to bound its work:
|
|
122
123
|
|
|
123
124
|
```js
|
|
124
125
|
capture: {
|
|
@@ -142,10 +143,13 @@ only `changed`, `length`, and `sensitive: true`. Non-sensitive input values are
|
|
|
142
143
|
limited to 300 characters. DOM body text contributes only to the fingerprint
|
|
143
144
|
hash and is not included in the UI event payload.
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
Open a window with `bridge.uiObservation({ operation: 'start', durationMs: 1500 })`
|
|
147
|
+
or the Host `web-ui-observation` command. Durations must be 100–5000 ms.
|
|
148
|
+
`{ operation: 'stop', leaseId }` releases that owner's window early;
|
|
149
|
+
`{ operation: 'status' }` reads its state. Expiry, disconnect and SDK stop flush
|
|
150
|
+
pending events, remove UI listeners and restore History methods. A later SDK
|
|
151
|
+
start leaves observation off. Missing events outside a window cannot establish
|
|
152
|
+
UI stability or action success. No screenshots or video are captured by this API.
|
|
149
153
|
|
|
150
154
|
This package is intended for debug and test builds. Keep command handlers
|
|
151
155
|
whitelisted and do not enable it in production without a deliberate security
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
sessionId: options.sessionId ?? storedSessionId(options.storageKey ?? 'ai_app_bridge_web_session_id'),
|
|
28
28
|
actions: new Map(), stateProviders: new Map(), restores: [], active: null, completion: null,
|
|
29
29
|
dropped: 0, rejected: 0, lastReceipt: null, lastError: null, reconnectDelay: 1000,
|
|
30
|
-
captureActionId: null, flushUi: null,
|
|
30
|
+
captureActionId: null, flushUi: null, uiWindow: null,
|
|
31
31
|
captures: Object.fromEntries(captureStreams.map(stream => [stream, { sequence: 0, losses: 0, pending: 0 }])),
|
|
32
32
|
};
|
|
33
33
|
if (typeof state.sessionId !== 'string' || !state.sessionId || state.sessionId.length > 1024)
|
|
34
34
|
throw new Error('invalid_web_session_id');
|
|
35
35
|
const api = { start, stop: disconnect, disconnect, recordLog, recordNetwork, recordState, recordEvent,
|
|
36
|
-
registerAction, unregisterAction, registerStateProvider, snapshotDom: observeDom,
|
|
36
|
+
registerAction, unregisterAction, registerStateProvider, snapshotDom: observeDom, uiObservation,
|
|
37
37
|
sessionId: () => state.sessionId, runtimeEpoch: () => state.runtimeEpoch,
|
|
38
38
|
isConnected: () => state.connected,
|
|
39
39
|
transportStatus: () => ({ connected: state.connected, queuedRecords: state.queue.length, queuedBytes: state.queueBytes,
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
state.started = true; state.restores.push(trackDocument()); installCaptures(); connect(); return api;
|
|
48
48
|
}
|
|
49
49
|
function disconnect() {
|
|
50
|
+
stopUiObservation();
|
|
50
51
|
state.active?.cancel('web_sdk_stopped');
|
|
51
52
|
for (const restore of state.restores.splice(0).reverse()) {
|
|
52
53
|
try { restore(); } catch (error) { state.lastError = error.message; }
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
};
|
|
74
75
|
socket.onclose = event => {
|
|
75
76
|
if (state.socket !== socket) return;
|
|
77
|
+
stopUiObservation();
|
|
76
78
|
state.connected = false; state.binding = null; state.socket = null;
|
|
77
79
|
if (event?.code === 1008) { state.lastError = 'web_protocol_rejected'; disconnect(); return; }
|
|
78
80
|
if (state.started && options.reconnect !== false && state.reconnectTimer === null) {
|
|
@@ -174,6 +176,7 @@
|
|
|
174
176
|
return;
|
|
175
177
|
}
|
|
176
178
|
if (message.type === 'read') {
|
|
179
|
+
if (message.payload?.name === 'uiObservation') { reply(message, uiObservation(message.payload.args)); return; }
|
|
177
180
|
if (message.payload?.name === 'captureBarrier') {
|
|
178
181
|
const stream = message.payload.args?.stream;
|
|
179
182
|
if (!captureStreams.includes(stream)) { reply(message, { ok: false, error: 'invalid_web_capture_stream' }); return; }
|
|
@@ -275,13 +278,43 @@
|
|
|
275
278
|
const dom = snapshotDom(options);
|
|
276
279
|
return dom.ok ? { ...dom, pageRef: documentPageRef(target()) } : dom;
|
|
277
280
|
}
|
|
281
|
+
function uiObservation(request = {}) {
|
|
282
|
+
if (!state.started) return { ok: false, error: 'web_sdk_stopped' };
|
|
283
|
+
if (state.uiWindow && performance.now() - state.uiWindow.startedAt >= state.uiWindow.durationMs) stopUiObservation();
|
|
284
|
+
const fields = { start: ['operation', 'durationMs'], stop: ['operation', 'leaseId'], status: ['operation'] }[request.operation];
|
|
285
|
+
if (!fields || Object.keys(request).length !== fields.length || !Object.keys(request).every(key => fields.includes(key)))
|
|
286
|
+
return { ok: false, error: 'invalid_ui_observation_request' };
|
|
287
|
+
if (request.operation === 'start') {
|
|
288
|
+
if (!Number.isInteger(request.durationMs) || request.durationMs < 100 || request.durationMs > 5000)
|
|
289
|
+
return { ok: false, error: 'invalid_ui_observation_duration' };
|
|
290
|
+
if (state.uiWindow) return { ok: false, error: 'ui_observation_busy' };
|
|
291
|
+
const startedAt = performance.now();
|
|
292
|
+
const cleanup = installUiCapture(state, recordEvent, options.capture?.ui ?? {});
|
|
293
|
+
if (!cleanup) return { ok: false, error: 'web_ui_observation_unavailable' };
|
|
294
|
+
state.uiWindow = { leaseId: newIdentity(), startedAt, durationMs: request.durationMs,
|
|
295
|
+
cleanup, timer: setTimeout(stopUiObservation, Math.max(0, request.durationMs - (performance.now() - startedAt))) };
|
|
296
|
+
} else if (request.operation === 'stop') {
|
|
297
|
+
if (typeof request.leaseId !== 'string' || request.leaseId !== state.uiWindow?.leaseId)
|
|
298
|
+
return { ok: false, error: 'ui_observation_lease_mismatch' };
|
|
299
|
+
stopUiObservation();
|
|
300
|
+
}
|
|
301
|
+
return { ok: true, schemaVersion: 'aab.ui-observation/v1', mode: 'on-demand', active: Boolean(state.uiWindow),
|
|
302
|
+
leaseId: state.uiWindow?.leaseId ?? null,
|
|
303
|
+
remainingMs: state.uiWindow ? Math.max(0, Math.ceil(state.uiWindow.durationMs - (performance.now() - state.uiWindow.startedAt))) : 0,
|
|
304
|
+
maxDurationMs: 5000 };
|
|
305
|
+
}
|
|
306
|
+
function stopUiObservation() {
|
|
307
|
+
const window = state.uiWindow;
|
|
308
|
+
if (!window) return;
|
|
309
|
+
state.uiWindow = null; clearTimeout(window.timer); window.cleanup();
|
|
310
|
+
}
|
|
278
311
|
function installCaptures() {
|
|
279
312
|
const captureOptions = options.capture ?? {};
|
|
280
313
|
if (captureOptions.console) installConsoleCapture(state, recordLog);
|
|
281
314
|
if (captureOptions.errors) installErrorCapture(state, recordLog);
|
|
282
315
|
if (captureOptions.fetch) installFetchCapture(state, recordNetwork, options);
|
|
283
316
|
if (captureOptions.xhr) installXhrCapture(state, recordNetwork, options);
|
|
284
|
-
|
|
317
|
+
// capture.ui configures explicit observation windows; start() never scans DOM.
|
|
285
318
|
}
|
|
286
319
|
return api;
|
|
287
320
|
}
|
|
@@ -759,6 +792,8 @@
|
|
|
759
792
|
}
|
|
760
793
|
|
|
761
794
|
function installUiCapture(state, recordEvent, captureOption) {
|
|
795
|
+
const restores = [];
|
|
796
|
+
let capturing = true;
|
|
762
797
|
const options = captureOption === true ? {} : captureOption;
|
|
763
798
|
if (!options || options.enabled === false) return;
|
|
764
799
|
const doc = options.document || globalValue('document');
|
|
@@ -808,6 +843,7 @@
|
|
|
808
843
|
if (pending.length) scheduleFlush();
|
|
809
844
|
};
|
|
810
845
|
const enqueue = (event) => {
|
|
846
|
+
if (!capturing) return;
|
|
811
847
|
if (!state.started) return;
|
|
812
848
|
if (pending.length >= maxPendingEvents) {
|
|
813
849
|
pending.shift();
|
|
@@ -824,7 +860,7 @@
|
|
|
824
860
|
scheduleFlush();
|
|
825
861
|
};
|
|
826
862
|
state.flushUi = () => flush(true);
|
|
827
|
-
|
|
863
|
+
restores.push(() => { state.flushUi = null; });
|
|
828
864
|
const onClick = (event) => {
|
|
829
865
|
enqueue({
|
|
830
866
|
type: 'interaction.click',
|
|
@@ -869,6 +905,8 @@
|
|
|
869
905
|
const onHashChange = () => onRoute('hashchange');
|
|
870
906
|
const onMutations = (records) => {
|
|
871
907
|
if (!state.started) return;
|
|
908
|
+
const window = state.uiWindow;
|
|
909
|
+
if (!window || performance.now() - window.startedAt >= window.durationMs) return;
|
|
872
910
|
const previousFingerprint = domFingerprint;
|
|
873
911
|
const nextFingerprint = fingerprintDom(doc, win, options, fingerprintSalt);
|
|
874
912
|
const fingerprintChanged = previousFingerprint.hash !== nextFingerprint.hash;
|
|
@@ -947,7 +985,7 @@
|
|
|
947
985
|
win.addEventListener('popstate', onPopState);
|
|
948
986
|
win.addEventListener('hashchange', onHashChange);
|
|
949
987
|
}
|
|
950
|
-
|
|
988
|
+
restores.push(() => {
|
|
951
989
|
doc.removeEventListener('click', onClick, true);
|
|
952
990
|
doc.removeEventListener('input', onInput, true);
|
|
953
991
|
doc.removeEventListener('change', onChange, true);
|
|
@@ -965,6 +1003,7 @@
|
|
|
965
1003
|
if (mutationObserver) mutationObserver.disconnect();
|
|
966
1004
|
flush(true);
|
|
967
1005
|
});
|
|
1006
|
+
return () => { capturing = false; for (const restore of restores.reverse()) restore(); };
|
|
968
1007
|
}
|
|
969
1008
|
|
|
970
1009
|
function fingerprintDom(doc, win, options, salt) {
|