@mobileaidev/ai-app-bridge-web 0.4.5 → 0.4.6
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/package.json +1 -1
- package/src/index.js +58 -23
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -187,8 +187,11 @@
|
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
189
|
if (message.payload?.name !== 'domSnapshot') { reply(message, { ok: false, error: 'unknown_web_read' }); return; }
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
try {
|
|
191
|
+
const dom = observeDom(message.payload.args ?? {});
|
|
192
|
+
reply(message, dom.ok ? { ok: true, dom } : dom);
|
|
193
|
+
} catch (error) { reply(message, { ok: false, error: 'web_dom_query_failed', message: error.message }); }
|
|
194
|
+
return;
|
|
192
195
|
}
|
|
193
196
|
if (message.type === 'cancel') {
|
|
194
197
|
const identity = message.payload;
|
|
@@ -326,7 +329,8 @@
|
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
function documentTarget(doc) {
|
|
329
|
-
if (!documentTargets.has(doc)) documentTargets.set(doc, { navigationId: newIdentity(), available: true,
|
|
332
|
+
if (!documentTargets.has(doc)) documentTargets.set(doc, { navigationId: newIdentity(), available: true,
|
|
333
|
+
elements: new WeakMap(), elementsById: new Map(), nextElement: 0 });
|
|
330
334
|
return documentTargets.get(doc);
|
|
331
335
|
}
|
|
332
336
|
function documentPageRef(binding) {
|
|
@@ -358,6 +362,7 @@
|
|
|
358
362
|
function elementIdentity(element, doc = element.ownerDocument || globalValue('document')) {
|
|
359
363
|
const state = documentTarget(doc);
|
|
360
364
|
if (!state.elements.has(element)) state.elements.set(element, `e${++state.nextElement}`);
|
|
365
|
+
state.elementsById.set(state.elements.get(element), element);
|
|
361
366
|
const sensitive = isSensitiveInput(element);
|
|
362
367
|
return { elementId: state.elements.get(element), tag: text(element.tagName).toLowerCase(), id: text(element.id),
|
|
363
368
|
name: attr(element, 'name'), type: attr(element, 'type'), role: attr(element, 'role'), ariaLabel: attr(element, 'aria-label'),
|
|
@@ -414,16 +419,22 @@
|
|
|
414
419
|
return { ok: false, error: 'document_unavailable' };
|
|
415
420
|
}
|
|
416
421
|
const selector = options.selector || defaultControlSelector;
|
|
422
|
+
if (options.query && options.selector) return { ok: false, error: 'invalid_web_dom_query' };
|
|
423
|
+
const state = documentTarget(doc);
|
|
424
|
+
for (const [id, element] of state.elementsById) {
|
|
425
|
+
if (!element.isConnected || element.ownerDocument !== doc) state.elementsById.delete(id);
|
|
426
|
+
}
|
|
417
427
|
const maxControls = Number(options.maxControls || 200);
|
|
428
|
+
const offset = options.offset ?? 0;
|
|
418
429
|
const controls = [];
|
|
419
|
-
const matched = doc.querySelectorAll(selector);
|
|
420
|
-
const elements = Array.prototype.slice.call(matched,
|
|
430
|
+
const matched = options.query ? matchingElements(options.query, doc) : doc.querySelectorAll(selector);
|
|
431
|
+
const elements = Array.prototype.slice.call(matched, offset, offset + maxControls);
|
|
421
432
|
elements.forEach((element, index) => {
|
|
422
433
|
const sensitive = isSensitiveInput(element);
|
|
423
434
|
const elementValue = text(element.value);
|
|
424
435
|
controls.push({
|
|
425
436
|
...elementIdentity(element, doc),
|
|
426
|
-
index,
|
|
437
|
+
index: offset + index,
|
|
427
438
|
sensitive,
|
|
428
439
|
...(sensitive ? { valueLength: elementValue.length } : {}),
|
|
429
440
|
disabled: elementDisabled(element),
|
|
@@ -445,7 +456,10 @@
|
|
|
445
456
|
bodyTextTruncated: text(doc.body && doc.body.innerText).length > 20000,
|
|
446
457
|
controls,
|
|
447
458
|
controlCount: controls.length,
|
|
448
|
-
|
|
459
|
+
totalControls: matched.length, offset,
|
|
460
|
+
nextOffset: offset + controls.length < matched.length ? offset + controls.length : null,
|
|
461
|
+
...(options.query ? { query: options.query } : {}),
|
|
462
|
+
truncated: offset > 0 || matched.length > controls.length,
|
|
449
463
|
updatedAtMs: Date.now(),
|
|
450
464
|
};
|
|
451
465
|
}
|
|
@@ -453,7 +467,7 @@
|
|
|
453
467
|
function clickElement(args, task) {
|
|
454
468
|
const selected = actionTarget(args, task);
|
|
455
469
|
ensureTarget(selected, task, { hit: true }); task.permit(); selected.element.click();
|
|
456
|
-
return { ok: true, resolved: selected.ref };
|
|
470
|
+
return { ok: true, resolved: selected.ref, ...targetWarnings(selected) };
|
|
457
471
|
}
|
|
458
472
|
|
|
459
473
|
function inputElement(args, task) {
|
|
@@ -484,6 +498,7 @@
|
|
|
484
498
|
return {
|
|
485
499
|
ok: true,
|
|
486
500
|
resolved: selected.ref,
|
|
501
|
+
...targetWarnings(selected),
|
|
487
502
|
sensitive,
|
|
488
503
|
valueLength: (richText ? element.innerText : element.value).length,
|
|
489
504
|
...(sensitive ? {} : { value: richText ? element.innerText : element.value }),
|
|
@@ -499,7 +514,7 @@
|
|
|
499
514
|
key: args.key, code: args.key, bubbles: true, cancelable: true, composed: true,
|
|
500
515
|
});
|
|
501
516
|
task.permit(); element.dispatchEvent(event);
|
|
502
|
-
return { ok: true, resolved: selected.ref, key: args.key, eventType: 'keydown',
|
|
517
|
+
return { ok: true, resolved: selected.ref, ...targetWarnings(selected), key: args.key, eventType: 'keydown',
|
|
503
518
|
trusted: event.isTrusted, defaultPrevented: event.defaultPrevented };
|
|
504
519
|
}
|
|
505
520
|
|
|
@@ -539,12 +554,12 @@
|
|
|
539
554
|
if (args.mode === 'into-view') {
|
|
540
555
|
if (!selected) return { ok: false, error: 'web_selector_required' };
|
|
541
556
|
ensureTarget(selected, task); task.permit(); selected.element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
542
|
-
return { ok: true, resolved: selected.ref };
|
|
557
|
+
return { ok: true, resolved: selected.ref, ...targetWarnings(selected) };
|
|
543
558
|
}
|
|
544
559
|
if (args.mode !== 'by') return { ok: false, error: 'invalid_web_scroll_mode' };
|
|
545
560
|
if (selected) {
|
|
546
561
|
ensureTarget(selected, task); task.permit(); selected.element.scrollBy({ left: deltaX, top: deltaY, behavior: 'instant' });
|
|
547
|
-
return { ok: true, resolved: selected.ref };
|
|
562
|
+
return { ok: true, resolved: selected.ref, ...targetWarnings(selected) };
|
|
548
563
|
}
|
|
549
564
|
const win = globalValue('window');
|
|
550
565
|
if (win && win.scrollBy) {
|
|
@@ -554,20 +569,28 @@
|
|
|
554
569
|
return { ok: false, error: 'scroll_unavailable' };
|
|
555
570
|
}
|
|
556
571
|
|
|
557
|
-
function
|
|
558
|
-
const doc = globalValue('document');
|
|
559
|
-
if (!doc) return null;
|
|
572
|
+
function matchingElements(selector, doc) {
|
|
560
573
|
if (!selector || typeof selector !== 'object') throw new Error('invalid_web_selector');
|
|
561
574
|
const identities = ['elementId', 'text', 'ariaLabel', 'css'].filter(key => typeof selector[key] === 'string' && selector[key].length > 0);
|
|
562
|
-
if (identities.length
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (!elementVisible(element)) return false;
|
|
575
|
+
if (!identities.length) throw new Error('invalid_web_selector');
|
|
576
|
+
const known = selector.elementId === undefined ? null : documentTarget(doc).elementsById.get(selector.elementId);
|
|
577
|
+
const elements = selector.elementId === undefined ? doc.querySelectorAll(selector.css ?? defaultControlSelector) : known ? [known] : [];
|
|
578
|
+
return Array.from(elements).filter(element => {
|
|
579
|
+
if (element.ownerDocument !== doc || !elementVisible(element) || selector.elementId !== undefined && selector.css !== undefined && !element.matches(selector.css)) return false;
|
|
567
580
|
const ref = elementIdentity(element);
|
|
568
581
|
return ['elementId', 'text', 'ariaLabel', 'tag', 'role'].every(key => selector[key] === undefined || selector[key] === ref[key]);
|
|
569
582
|
});
|
|
570
|
-
|
|
583
|
+
}
|
|
584
|
+
function findElement(selector) {
|
|
585
|
+
const doc = globalValue('document');
|
|
586
|
+
if (!doc) return null;
|
|
587
|
+
const matches = matchingElements(selector, doc);
|
|
588
|
+
if (matches.length > 1) throw Object.assign(new Error('web_element_ambiguous'), { details: {
|
|
589
|
+
selectionRequired: true, matched: matches.length, candidatesTruncated: matches.length > 20,
|
|
590
|
+
...(matches.length > 20 ? { candidateQuery: { query: selector, offset: 20, maxControls: 200 } } : {}),
|
|
591
|
+
candidates: matches.slice(0, 20).map(element => { const ref = elementIdentity(element); return { text: ref.text, tag: ref.tag,
|
|
592
|
+
role: ref.role, ariaLabel: ref.ariaLabel, disabled: elementDisabled(element), bounds: bounds(element), selector: { elementId: ref.elementId } }; }),
|
|
593
|
+
} });
|
|
571
594
|
return matches[0] ?? null;
|
|
572
595
|
}
|
|
573
596
|
|
|
@@ -576,8 +599,19 @@
|
|
|
576
599
|
if (!element) throw new Error('web_element_not_found');
|
|
577
600
|
const ref = { pageRef: task.pageRef(), element: elementIdentity(element) };
|
|
578
601
|
if (args.expectedTarget && (!samePage(ref.pageRef, args.expectedTarget.pageRef)
|
|
579
|
-
||
|
|
580
|
-
|
|
602
|
+
|| ref.element.elementId !== args.expectedTarget.element.elementId)) throw new Error('reobserve_required');
|
|
603
|
+
const selected = { element, ref, observed: args.expectedTarget?.element ?? ref.element, changedFields: new Set() };
|
|
604
|
+
recordTargetChanges(selected, ref.element);
|
|
605
|
+
return selected;
|
|
606
|
+
}
|
|
607
|
+
function recordTargetChanges(selected, current, edited = false) {
|
|
608
|
+
for (const key of elementKeys) {
|
|
609
|
+
if ((!edited || key !== 'text') && current[key] !== selected.observed[key]) selected.changedFields.add(key);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function targetWarnings(selected) {
|
|
613
|
+
return selected.changedFields.size ? { warnings: [{ code: 'target_attributes_changed', fields: [...selected.changedFields],
|
|
614
|
+
message: 'The observed DOM element is still present; its attributes changed.' }] } : {};
|
|
581
615
|
}
|
|
582
616
|
function samePage(a, b) {
|
|
583
617
|
return ['schemaVersion', 'sessionId', 'runtimeEpoch', 'targetId', 'navigationId', 'url'].every(key => a[key] === b?.[key]);
|
|
@@ -590,7 +624,8 @@
|
|
|
590
624
|
if (!element.isConnected || element.ownerDocument !== doc || elementDisabled(element) || !elementVisible(element)
|
|
591
625
|
|| (focus && doc.activeElement !== element)) throw new Error('web_element_changed');
|
|
592
626
|
const current = elementIdentity(element);
|
|
593
|
-
if (
|
|
627
|
+
if (current.elementId !== ref.element.elementId) throw new Error('reobserve_required');
|
|
628
|
+
recordTargetChanges(selected, current, edited);
|
|
594
629
|
if (hit) {
|
|
595
630
|
const interaction = interactionState(element);
|
|
596
631
|
if (interaction.status !== 'ready') throw Object.assign(new Error(interaction.status === 'outside-viewport'
|