yunti-browser-runtime 0.1.3 → 0.2.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/README.md +41 -9
- package/bin/yunti-browser-runtime.js +6 -0
- package/docs/ACTION_RESULT_COVERAGE.md +41 -0
- package/docs/AGENT_WORKFLOW_CONTRACT.md +108 -0
- package/docs/EXECUTION_PLAN.md +1433 -5
- package/docs/EXTENSION_DISTRIBUTION.md +146 -0
- package/docs/EXTENSION_PERMISSION_STRATEGY.md +167 -0
- package/docs/EXTENSION_STORE_COPY.md +213 -0
- package/docs/INSTALL.md +13 -7
- package/docs/NEXT_MAJOR_PLAN.md +808 -0
- package/docs/PROJECT_STATUS.md +1037 -7
- package/docs/PUBLISHING_BLOCKERS.md +1 -1
- package/docs/RELEASE.md +6 -2
- package/docs/RELEASE_0_2_0_CHECKLIST.md +827 -0
- package/docs/ROADMAP.md +44 -0
- package/docs/SECURITY.md +34 -0
- package/docs/TOOL_GUIDE.md +290 -5
- package/extension/background.js +20 -0
- package/extension/cdp.js +4 -0
- package/extension/content.js +167 -16
- package/extension/dom-observer.js +588 -0
- package/extension/manifest.json +3 -2
- package/extension/popup.js +2 -2
- package/extension/session-manager.js +106 -4
- package/extension/tool-handlers.js +1090 -156
- package/mcp/bridge-hub.js +257 -3
- package/mcp/http-server.js +213 -0
- package/mcp/memory.js +5 -5
- package/mcp/redaction.js +52 -3
- package/mcp/server.js +2 -0
- package/mcp/tools.js +421 -38
- package/package.json +4 -2
- package/scripts/check-action-result-coverage.js +145 -0
- package/scripts/doctor.js +6 -1
- package/scripts/package-extension.js +1 -0
- package/scripts/release-check.js +3 -0
- package/skills/yunti-browser-runtime/SKILL.md +128 -3
|
@@ -73,12 +73,16 @@ export function createToolDispatcher({
|
|
|
73
73
|
result = await evaluateScript(tabId, session, event.arguments || {})
|
|
74
74
|
} else if (event.tool === "yunti_take_snapshot") {
|
|
75
75
|
result = await takeSnapshot(tabId, session, event.arguments || {})
|
|
76
|
+
} else if (event.tool === "yunti_observe_page") {
|
|
77
|
+
result = await observePage(tabId, session, event.arguments || {})
|
|
76
78
|
} else if (event.tool === "yunti_click") {
|
|
77
79
|
result = await clickByUid(tabId, session, event.arguments || {})
|
|
78
80
|
} else if (event.tool === "yunti_hover") {
|
|
79
81
|
result = await hoverByUid(tabId, session, event.arguments || {})
|
|
80
82
|
} else if (event.tool === "yunti_fill") {
|
|
81
83
|
result = await fillByUid(tabId, session, event.arguments || {})
|
|
84
|
+
} else if (event.tool === "yunti_select") {
|
|
85
|
+
result = await selectElement(tabId, session, event.arguments || {})
|
|
82
86
|
} else if (event.tool === "yunti_fill_form") {
|
|
83
87
|
result = await fillForm(tabId, session, event.arguments || {})
|
|
84
88
|
} else if (event.tool === "yunti_wait_for") {
|
|
@@ -105,6 +109,8 @@ export function createToolDispatcher({
|
|
|
105
109
|
result = await typeTextByUid(tabId, session, event.arguments || {})
|
|
106
110
|
} else if (event.tool === "yunti_press_key") {
|
|
107
111
|
result = await pressKeyByUid(tabId, session, event.arguments || {})
|
|
112
|
+
} else if (event.tool === "yunti_scroll") {
|
|
113
|
+
result = await scrollPage(tabId, session, event.arguments || {})
|
|
108
114
|
} else {
|
|
109
115
|
result = await chrome.tabs.sendMessage(tabId, {
|
|
110
116
|
type: "yunti_execute_tool",
|
|
@@ -343,8 +349,8 @@ export function createToolDispatcher({
|
|
|
343
349
|
}
|
|
344
350
|
}
|
|
345
351
|
|
|
346
|
-
//
|
|
347
|
-
const
|
|
352
|
+
// Latest page uid state from yunti_observe_page or yunti_take_snapshot.
|
|
353
|
+
const pageUidStore = new Map()
|
|
348
354
|
|
|
349
355
|
async function takeSnapshot(tabId, session, args = {}) {
|
|
350
356
|
const maxElements = Number.isFinite(Number(args.maxElements))
|
|
@@ -367,12 +373,7 @@ export function createToolDispatcher({
|
|
|
367
373
|
elements = await domFallbackSnapshot(tabId, maxElements)
|
|
368
374
|
}
|
|
369
375
|
|
|
370
|
-
|
|
371
|
-
const uidMap = {}
|
|
372
|
-
for (const el of elements) {
|
|
373
|
-
uidMap[el.uid] = { ...el }
|
|
374
|
-
}
|
|
375
|
-
snapshotStore.set(session.browserSessionId, uidMap)
|
|
376
|
+
storePageUidMap(session, "snapshot", elements)
|
|
376
377
|
|
|
377
378
|
return {
|
|
378
379
|
browserSessionId: session.browserSessionId,
|
|
@@ -383,6 +384,38 @@ export function createToolDispatcher({
|
|
|
383
384
|
snapshotId: `snap-${Date.now()}`,
|
|
384
385
|
}
|
|
385
386
|
}
|
|
387
|
+
|
|
388
|
+
async function observePage(tabId, session, args = {}) {
|
|
389
|
+
const observation = await chrome.tabs.sendMessage(tabId, {
|
|
390
|
+
type: "yunti_execute_tool",
|
|
391
|
+
tool: "yunti_observe_page",
|
|
392
|
+
arguments: args,
|
|
393
|
+
})
|
|
394
|
+
if (Array.isArray(observation?.elements)) {
|
|
395
|
+
storePageUidMap(session, "observe", [
|
|
396
|
+
...observation.elements,
|
|
397
|
+
...(Array.isArray(observation.scrollableContainers) ? observation.scrollableContainers : []),
|
|
398
|
+
], {
|
|
399
|
+
observationId: observation.observationId,
|
|
400
|
+
uidMapVersion: observation.uidMapVersion,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
return observation
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function storePageUidMap(session, source, elements, meta = {}) {
|
|
407
|
+
const uidMap = {}
|
|
408
|
+
for (const el of elements || []) {
|
|
409
|
+
if (!el?.uid) continue
|
|
410
|
+
uidMap[el.uid] = { ...el, source }
|
|
411
|
+
}
|
|
412
|
+
pageUidStore.set(session.browserSessionId, {
|
|
413
|
+
source,
|
|
414
|
+
uidMap,
|
|
415
|
+
storedAt: Date.now(),
|
|
416
|
+
...meta,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
386
419
|
|
|
387
420
|
function flattenAXTree(nodes, maxElements, includeHidden) {
|
|
388
421
|
const elements = []
|
|
@@ -501,7 +534,30 @@ export function createToolDispatcher({
|
|
|
501
534
|
async function clickByUid(tabId, session, args = {}) {
|
|
502
535
|
const uid = String(args.uid || "").trim()
|
|
503
536
|
if (uid) {
|
|
504
|
-
|
|
537
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
538
|
+
if (!point.ok) return buildUidPointFailureResult(session, uid, "click", point.error)
|
|
539
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
540
|
+
type: "yunti_execute_tool",
|
|
541
|
+
tool: "yunti_click",
|
|
542
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
543
|
+
})
|
|
544
|
+
if (!result || typeof result !== "object" || result.clicked !== true) {
|
|
545
|
+
return buildUidPointFailureResult(session, uid, "click", result?.error || "Uid click failed")
|
|
546
|
+
}
|
|
547
|
+
const roundedX = Math.round(point.x)
|
|
548
|
+
const roundedY = Math.round(point.y)
|
|
549
|
+
return {
|
|
550
|
+
...result,
|
|
551
|
+
uid,
|
|
552
|
+
x: roundedX,
|
|
553
|
+
y: roundedY,
|
|
554
|
+
browserSessionId: session.browserSessionId,
|
|
555
|
+
action: "click",
|
|
556
|
+
target: { uid, x: roundedX, y: roundedY },
|
|
557
|
+
ok: true,
|
|
558
|
+
recoverable: false,
|
|
559
|
+
nextStepHint: "Click dispatched without attaching Chrome debugger. Observe again or read page state to verify the intended change.",
|
|
560
|
+
}
|
|
505
561
|
}
|
|
506
562
|
const x = Number(args.x)
|
|
507
563
|
const y = Number(args.y)
|
|
@@ -509,23 +565,80 @@ export function createToolDispatcher({
|
|
|
509
565
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
510
566
|
throw new Error("yunti_click coordinate mode requires both x and y. Use yunti_click_at with x/y, or call yunti_take_snapshot and pass uid.")
|
|
511
567
|
}
|
|
512
|
-
|
|
568
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
569
|
+
type: "yunti_execute_tool",
|
|
570
|
+
tool: "yunti_click",
|
|
571
|
+
arguments: { ...args, x, y },
|
|
572
|
+
})
|
|
573
|
+
if (!result || typeof result !== "object" || result.clicked !== true) return result
|
|
574
|
+
const roundedX = Math.round(x)
|
|
575
|
+
const roundedY = Math.round(y)
|
|
576
|
+
return {
|
|
577
|
+
...result,
|
|
578
|
+
x: roundedX,
|
|
579
|
+
y: roundedY,
|
|
580
|
+
browserSessionId: session.browserSessionId,
|
|
581
|
+
method: "coordinate",
|
|
582
|
+
action: "click",
|
|
583
|
+
target: { method: "coordinate", x: roundedX, y: roundedY },
|
|
584
|
+
ok: true,
|
|
585
|
+
recoverable: false,
|
|
586
|
+
nextStepHint: "Coordinate click dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify the intended change.",
|
|
587
|
+
}
|
|
513
588
|
}
|
|
514
|
-
|
|
589
|
+
const selector = String(args.selector || "").trim()
|
|
590
|
+
if (!selector) {
|
|
515
591
|
throw new Error("yunti_click requires uid, selector, or both x and y. Call yunti_take_snapshot to get uid, pass a CSS selector, or use yunti_click_at for coordinate-only clicks.")
|
|
516
592
|
}
|
|
517
593
|
// Fall back to content script for selector-based click
|
|
518
|
-
|
|
594
|
+
const contentResult = await chrome.tabs.sendMessage(tabId, {
|
|
519
595
|
type: "yunti_execute_tool",
|
|
520
596
|
tool: "yunti_click",
|
|
521
597
|
arguments: args,
|
|
522
598
|
})
|
|
599
|
+
if (contentResult && typeof contentResult === "object" && contentResult.clicked === true) {
|
|
600
|
+
return {
|
|
601
|
+
...contentResult,
|
|
602
|
+
selector,
|
|
603
|
+
browserSessionId: session.browserSessionId,
|
|
604
|
+
method: "selector",
|
|
605
|
+
action: "click",
|
|
606
|
+
target: { selector, method: "selector" },
|
|
607
|
+
ok: true,
|
|
608
|
+
recoverable: false,
|
|
609
|
+
nextStepHint: "Selector click dispatched. Observe again, read page state, or use a fresh uid when possible to verify the intended change.",
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return contentResult
|
|
523
613
|
}
|
|
524
614
|
|
|
525
615
|
async function hoverByUid(tabId, session, args = {}) {
|
|
526
616
|
const uid = String(args.uid || "").trim()
|
|
527
617
|
if (uid) {
|
|
528
|
-
|
|
618
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
619
|
+
if (!point.ok) return buildUidPointFailureResult(session, uid, "hover", point.error)
|
|
620
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
621
|
+
type: "yunti_execute_tool",
|
|
622
|
+
tool: "yunti_hover",
|
|
623
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
624
|
+
})
|
|
625
|
+
if (!result || typeof result !== "object" || result.hovered !== true) {
|
|
626
|
+
return buildUidPointFailureResult(session, uid, "hover", result?.error || "Uid hover failed")
|
|
627
|
+
}
|
|
628
|
+
const roundedX = Math.round(point.x)
|
|
629
|
+
const roundedY = Math.round(point.y)
|
|
630
|
+
return {
|
|
631
|
+
...result,
|
|
632
|
+
uid,
|
|
633
|
+
x: roundedX,
|
|
634
|
+
y: roundedY,
|
|
635
|
+
browserSessionId: session.browserSessionId,
|
|
636
|
+
action: "hover",
|
|
637
|
+
target: { uid, x: roundedX, y: roundedY },
|
|
638
|
+
ok: true,
|
|
639
|
+
recoverable: false,
|
|
640
|
+
nextStepHint: "Hover dispatched without attaching Chrome debugger. Observe again or read page state to verify menus, tooltips, or hover-only controls.",
|
|
641
|
+
}
|
|
529
642
|
}
|
|
530
643
|
const x = Number(args.x)
|
|
531
644
|
const y = Number(args.y)
|
|
@@ -533,22 +646,49 @@ export function createToolDispatcher({
|
|
|
533
646
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
534
647
|
throw new Error("yunti_hover coordinate mode requires both x and y. Call yunti_take_snapshot to get uid, pass selector, or provide both coordinates.")
|
|
535
648
|
}
|
|
536
|
-
await
|
|
537
|
-
|
|
649
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
650
|
+
type: "yunti_execute_tool",
|
|
651
|
+
tool: "yunti_hover",
|
|
652
|
+
arguments: { ...args, x, y },
|
|
653
|
+
})
|
|
654
|
+
if (!result || typeof result !== "object" || result.hovered !== true) return result
|
|
655
|
+
const roundedX = Math.round(x)
|
|
656
|
+
const roundedY = Math.round(y)
|
|
657
|
+
return {
|
|
658
|
+
...result,
|
|
659
|
+
hovered: true,
|
|
660
|
+
x: roundedX,
|
|
661
|
+
y: roundedY,
|
|
662
|
+
browserSessionId: session.browserSessionId,
|
|
663
|
+
method: "coordinate",
|
|
664
|
+
action: "hover",
|
|
665
|
+
target: { method: "coordinate", x: roundedX, y: roundedY },
|
|
666
|
+
ok: true,
|
|
667
|
+
recoverable: false,
|
|
668
|
+
nextStepHint: "Coordinate hover dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
|
|
669
|
+
}
|
|
538
670
|
}
|
|
539
671
|
const selector = String(args.selector || "").trim()
|
|
540
672
|
if (!selector) {
|
|
541
673
|
throw new Error("yunti_hover requires uid, selector, or both x and y. Call yunti_take_snapshot to get uid or pass a CSS selector.")
|
|
542
674
|
}
|
|
543
|
-
const
|
|
544
|
-
|
|
675
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
676
|
+
type: "yunti_execute_tool",
|
|
677
|
+
tool: "yunti_hover",
|
|
678
|
+
arguments: args,
|
|
679
|
+
})
|
|
680
|
+
if (!result || typeof result !== "object" || result.hovered !== true) return result
|
|
545
681
|
return {
|
|
682
|
+
...result,
|
|
546
683
|
hovered: true,
|
|
547
684
|
selector,
|
|
548
|
-
x: Math.round(point.x),
|
|
549
|
-
y: Math.round(point.y),
|
|
550
685
|
browserSessionId: session.browserSessionId,
|
|
551
686
|
method: "selector",
|
|
687
|
+
action: "hover",
|
|
688
|
+
target: { selector, method: "selector" },
|
|
689
|
+
ok: true,
|
|
690
|
+
recoverable: false,
|
|
691
|
+
nextStepHint: "Selector hover dispatched without attaching Chrome debugger. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
|
|
552
692
|
}
|
|
553
693
|
}
|
|
554
694
|
|
|
@@ -557,76 +697,623 @@ export function createToolDispatcher({
|
|
|
557
697
|
const uid = String(args.uid || "").trim()
|
|
558
698
|
const value = String(args.value)
|
|
559
699
|
if (uid) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
{ expression: `(() => {
|
|
568
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
569
|
-
if (!el) return 'not found';
|
|
570
|
-
const tag = el.tagName.toLowerCase();
|
|
571
|
-
if (tag === 'select') {
|
|
572
|
-
const opts = Array.from(el.options);
|
|
573
|
-
return { tag: 'select', options: opts.map(o => ({ value: o.value, text: o.text.slice(0, 80) })), selectedIndex: el.selectedIndex };
|
|
574
|
-
}
|
|
575
|
-
if (el.isContentEditable) {
|
|
576
|
-
el.textContent = '';
|
|
577
|
-
} else if (tag === 'input' || tag === 'textarea') {
|
|
578
|
-
el.focus();
|
|
579
|
-
el.select();
|
|
580
|
-
}
|
|
581
|
-
return { tag, type: el.type, contentEditable: el.isContentEditable };
|
|
582
|
-
})()`,
|
|
583
|
-
returnByValue: true,
|
|
700
|
+
try {
|
|
701
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
702
|
+
if (!point.ok) {
|
|
703
|
+
return buildUidFillFailureResult(session, uid, {
|
|
704
|
+
code: "UID_COORDINATES_UNAVAILABLE",
|
|
705
|
+
error: point.error,
|
|
706
|
+
})
|
|
584
707
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const targetOption = elInfo.options?.find(
|
|
593
|
-
o => o.value === value || o.text === value
|
|
594
|
-
)
|
|
595
|
-
if (targetOption) {
|
|
596
|
-
await chromeDebuggerSendCommand(
|
|
597
|
-
{ tabId },
|
|
598
|
-
"Runtime.evaluate",
|
|
599
|
-
{
|
|
600
|
-
expression: `(() => {
|
|
601
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
602
|
-
if (el) el.value = ${JSON.stringify(targetOption.value)};
|
|
603
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
604
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
605
|
-
})()`,
|
|
606
|
-
}
|
|
607
|
-
)
|
|
608
|
-
return { filled: true, uid, method: "select", value: targetOption.value, browserSessionId: session.browserSessionId }
|
|
708
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
709
|
+
type: "yunti_execute_tool",
|
|
710
|
+
tool: "yunti_fill",
|
|
711
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
712
|
+
})
|
|
713
|
+
if (!result || typeof result !== "object" || result.filled !== true) {
|
|
714
|
+
return buildUidFillFailureResult(session, uid, result)
|
|
609
715
|
}
|
|
610
|
-
|
|
611
|
-
|
|
716
|
+
const method = result.method || "dom"
|
|
717
|
+
if (result.valueApplied === false) {
|
|
718
|
+
return buildUidFillFailureResult(session, uid, {
|
|
719
|
+
...result,
|
|
720
|
+
code: "VALUE_NOT_APPLIED",
|
|
721
|
+
error: `Filled value did not remain on uid ${uid}`,
|
|
722
|
+
method,
|
|
723
|
+
expectedValueLength: value.length,
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
...result,
|
|
728
|
+
filled: true,
|
|
729
|
+
uid,
|
|
730
|
+
method,
|
|
731
|
+
value,
|
|
732
|
+
browserSessionId: session.browserSessionId,
|
|
733
|
+
action: "fill",
|
|
734
|
+
target: { uid, method },
|
|
735
|
+
ok: true,
|
|
736
|
+
recoverable: false,
|
|
737
|
+
nextStepHint: method === "contenteditable"
|
|
738
|
+
? "Contenteditable fill dispatched. Observe again, read page text, or evaluate textContent to verify the intended change."
|
|
739
|
+
: "Fill dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended change.",
|
|
740
|
+
}
|
|
741
|
+
} catch (error) {
|
|
742
|
+
return buildUidFillFailureResult(session, uid, {
|
|
743
|
+
error: error?.message || "Uid fill failed",
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
}
|
|
612
747
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
748
|
+
const selector = String(args.selector || "").trim()
|
|
749
|
+
let result
|
|
750
|
+
try {
|
|
751
|
+
result = await chrome.tabs.sendMessage(tabId, {
|
|
752
|
+
type: "yunti_execute_tool",
|
|
753
|
+
tool: "yunti_fill",
|
|
754
|
+
arguments: args,
|
|
755
|
+
})
|
|
756
|
+
} catch (error) {
|
|
757
|
+
return buildSelectorFillFailureResult(session, selector, {
|
|
758
|
+
error: error?.message || "Selector fill failed",
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!result || typeof result !== "object" || result.filled !== true) {
|
|
763
|
+
return buildSelectorFillFailureResult(session, selector, result)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
...result,
|
|
768
|
+
selector,
|
|
769
|
+
method: result.method || "selector",
|
|
770
|
+
browserSessionId: session.browserSessionId,
|
|
771
|
+
action: "fill",
|
|
772
|
+
target: { selector, method: "selector" },
|
|
773
|
+
ok: true,
|
|
774
|
+
recoverable: false,
|
|
775
|
+
nextStepHint: "Selector fill dispatched. Observe again, read page state, or evaluate the field value to verify the intended change.",
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function buildSelectorFillFailureResult(session, selector, fillResult = {}) {
|
|
780
|
+
const error = fillResult?.error || "Selector fill failed"
|
|
781
|
+
const code = fillResult?.code || inferFillFailureCode(error, "SELECTOR_FILL_FAILED")
|
|
782
|
+
const recoveryHint = {
|
|
783
|
+
reason: "selector-fill-failed",
|
|
784
|
+
recommendedTools: ["yunti_observe_page", "yunti_take_snapshot", "yunti_evaluate_script", "yunti_fill"],
|
|
785
|
+
nextAction: code === "ELEMENT_NOT_FOUND" ? "observe-again" : "inspect-target-element",
|
|
786
|
+
decision: code === "ELEMENT_NOT_FOUND" ? "refresh-observation-or-selector-before-retry" : "inspect-editability-before-retry",
|
|
787
|
+
selector,
|
|
788
|
+
message: "The selector fill could not be completed. Inspect whether the selector still matches an editable element, observe again for a fresh uid, or evaluate the field before retrying.",
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
...(fillResult && typeof fillResult === "object" ? fillResult : {}),
|
|
793
|
+
filled: false,
|
|
794
|
+
selector,
|
|
795
|
+
browserSessionId: session.browserSessionId,
|
|
796
|
+
action: "fill",
|
|
797
|
+
target: { selector, method: "selector" },
|
|
798
|
+
ok: false,
|
|
799
|
+
recoverable: true,
|
|
800
|
+
code,
|
|
801
|
+
error,
|
|
802
|
+
recoveryHint,
|
|
803
|
+
nextStepHint: "Selector fill failed. Observe again for a fresh uid, inspect whether the target is editable, or retry with a stable selector before repeating the same fill.",
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function buildUidFillFailureResult(session, uid, fillResult = {}) {
|
|
808
|
+
const availableValues = Array.isArray(fillResult?.availableValues) ? fillResult.availableValues : undefined
|
|
809
|
+
const availableTexts = Array.isArray(fillResult?.availableTexts) ? fillResult.availableTexts : undefined
|
|
810
|
+
const error = fillResult?.error || `Cannot fill uid ${uid}`
|
|
811
|
+
const code = fillResult?.code || inferFillFailureCode(error, "UID_FILL_FAILED")
|
|
812
|
+
const recoveryHint = {
|
|
813
|
+
reason: "uid-fill-failed",
|
|
814
|
+
recommendedTools: ["yunti_observe_page", "yunti_take_snapshot", "yunti_evaluate_script", "yunti_fill"],
|
|
815
|
+
nextAction: "inspect-target-element",
|
|
816
|
+
decision: "inspect-editability-before-retry",
|
|
817
|
+
uid,
|
|
818
|
+
...(availableValues ? { availableValues } : {}),
|
|
819
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
820
|
+
message: "The uid fill could not be completed. Refresh observation if the uid may be stale, inspect whether the target is editable, or retry with selector fallback.",
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (code === "ELEMENT_NOT_FOUND") {
|
|
824
|
+
recoveryHint.nextAction = "observe-again"
|
|
825
|
+
recoveryHint.decision = "refresh-observation-before-retry"
|
|
826
|
+
} else if (code === "OPTION_NOT_FOUND") {
|
|
827
|
+
recoveryHint.nextAction = "inspect-available-options"
|
|
828
|
+
recoveryHint.decision = "inspect-options-before-retry"
|
|
829
|
+
} else if (code === "VALUE_NOT_APPLIED") {
|
|
830
|
+
recoveryHint.nextAction = "verify-field-state"
|
|
831
|
+
recoveryHint.decision = "inspect-controlled-or-masked-field-before-retry"
|
|
832
|
+
recoveryHint.message = "The fill dispatched, but the field value did not remain afterward. Inspect whether the target is framework-controlled, masked, or requires typing/press_key semantics before retrying."
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
...(fillResult && typeof fillResult === "object" ? fillResult : {}),
|
|
837
|
+
filled: false,
|
|
838
|
+
uid,
|
|
839
|
+
browserSessionId: session.browserSessionId,
|
|
840
|
+
action: "fill",
|
|
841
|
+
target: { uid, method: "uid" },
|
|
842
|
+
ok: false,
|
|
843
|
+
recoverable: true,
|
|
844
|
+
code,
|
|
845
|
+
error,
|
|
846
|
+
...(availableValues ? { availableValues } : {}),
|
|
847
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
848
|
+
recoveryHint,
|
|
849
|
+
nextStepHint: "Uid fill failed. Observe again for a fresh uid, inspect whether the target is editable or a select with available options, or retry with selector fallback before repeating the same fill.",
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function inferFillFailureCode(error, fallbackCode) {
|
|
854
|
+
const message = String(error || "").toLowerCase()
|
|
855
|
+
if (message.includes("not found") || message.includes("cannot resolve coordinates")) return "ELEMENT_NOT_FOUND"
|
|
856
|
+
if (message.includes("did not remain") || message.includes("did not stick") || message.includes("not applied")) return "VALUE_NOT_APPLIED"
|
|
857
|
+
if (
|
|
858
|
+
message.includes("not editable") ||
|
|
859
|
+
message.includes("no editable target") ||
|
|
860
|
+
message.includes("cannot accept text") ||
|
|
861
|
+
message.includes("disabled") ||
|
|
862
|
+
message.includes("read only") ||
|
|
863
|
+
message.includes("readonly") ||
|
|
864
|
+
message.includes("hidden") ||
|
|
865
|
+
message.includes("no size")
|
|
866
|
+
) return "TARGET_NOT_EDITABLE"
|
|
867
|
+
return fallbackCode
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function buildFillEditabilityError(uid, elInfo = {}) {
|
|
871
|
+
const tag = elInfo?.tag || "unknown"
|
|
872
|
+
const type = elInfo?.type ? ` type=${elInfo.type}` : ""
|
|
873
|
+
const reasons = []
|
|
874
|
+
if (elInfo?.hidden) reasons.push("hidden or has no size")
|
|
875
|
+
if (elInfo?.disabled) reasons.push("disabled")
|
|
876
|
+
if (elInfo?.readOnly) reasons.push("readonly")
|
|
877
|
+
if (elInfo?.editable === false) reasons.push("not editable")
|
|
878
|
+
const reason = reasons.length ? reasons.join(", ") : "not editable"
|
|
879
|
+
return `Target at uid ${uid} is not editable (${tag}${type}: ${reason})`
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function summarizeFillTarget(elInfo = {}) {
|
|
883
|
+
return {
|
|
884
|
+
tag: elInfo?.tag,
|
|
885
|
+
...(elInfo?.type ? { type: elInfo.type } : {}),
|
|
886
|
+
...(elInfo?.contentEditable !== undefined ? { contentEditable: Boolean(elInfo.contentEditable) } : {}),
|
|
887
|
+
...(elInfo?.disabled !== undefined ? { disabled: Boolean(elInfo.disabled) } : {}),
|
|
888
|
+
...(elInfo?.readOnly !== undefined ? { readOnly: Boolean(elInfo.readOnly) } : {}),
|
|
889
|
+
...(elInfo?.hidden !== undefined ? { hidden: Boolean(elInfo.hidden) } : {}),
|
|
890
|
+
...(elInfo?.editable !== undefined ? { editable: Boolean(elInfo.editable) } : {}),
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function scrollPage(tabId, session, args = {}) {
|
|
895
|
+
const uid = String(args.uid || "").trim()
|
|
896
|
+
let scrollArgs = args
|
|
897
|
+
if (uid) {
|
|
898
|
+
let point
|
|
899
|
+
try {
|
|
900
|
+
point = await resolveUidCenter(tabId, session, uid)
|
|
901
|
+
} catch (error) {
|
|
902
|
+
return buildUidScrollFailureResult(session, uid, {
|
|
903
|
+
error: error?.message || "Uid scroll failed",
|
|
904
|
+
})
|
|
621
905
|
}
|
|
622
|
-
|
|
906
|
+
const { x, y } = point
|
|
907
|
+
scrollArgs = { ...args, x, y }
|
|
623
908
|
}
|
|
624
|
-
|
|
625
|
-
return chrome.tabs.sendMessage(tabId, {
|
|
909
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
626
910
|
type: "yunti_execute_tool",
|
|
627
|
-
tool: "
|
|
628
|
-
arguments:
|
|
911
|
+
tool: "yunti_scroll",
|
|
912
|
+
arguments: scrollArgs,
|
|
629
913
|
})
|
|
914
|
+
|
|
915
|
+
if (!result || typeof result !== "object" || result.scrolled !== true) {
|
|
916
|
+
return result
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const hasComparableScrollPosition =
|
|
920
|
+
Number.isFinite(Number(result.before?.left)) &&
|
|
921
|
+
Number.isFinite(Number(result.before?.top)) &&
|
|
922
|
+
Number.isFinite(Number(result.after?.left)) &&
|
|
923
|
+
Number.isFinite(Number(result.after?.top))
|
|
924
|
+
const moved = hasComparableScrollPosition
|
|
925
|
+
? Number(result.before.left) !== Number(result.after.left) ||
|
|
926
|
+
Number(result.before.top) !== Number(result.after.top)
|
|
927
|
+
: undefined
|
|
928
|
+
const noMovement = moved === false
|
|
929
|
+
const edgeHint = noMovement ? inferScrollEdgeHint(result.deltaX, result.deltaY) : undefined
|
|
930
|
+
const recoveryHint = noMovement ? buildScrollRecoveryHint(session, uid, edgeHint, result) : undefined
|
|
931
|
+
const partialMovement = moved === true ? buildScrollPartialMovementHint(result, scrollArgs) : undefined
|
|
932
|
+
const coordinateFallbackHint = !uid ? buildCoordinateScrollFallbackHint(result) : undefined
|
|
933
|
+
|
|
934
|
+
return {
|
|
935
|
+
...result,
|
|
936
|
+
browserSessionId: session.browserSessionId,
|
|
937
|
+
action: "scroll",
|
|
938
|
+
...(uid ? { uid, method: "uid" } : {}),
|
|
939
|
+
...(uid ? { scrollTarget: { uid, method: "uid" } } : {}),
|
|
940
|
+
...(moved !== undefined ? { moved } : {}),
|
|
941
|
+
...(noMovement ? { code: "NO_SCROLL_MOVEMENT" } : {}),
|
|
942
|
+
...(edgeHint ? { edgeHint } : {}),
|
|
943
|
+
...(recoveryHint ? { recoveryHint } : {}),
|
|
944
|
+
...(partialMovement ? { partialMovement } : {}),
|
|
945
|
+
...(coordinateFallbackHint ? { coordinateFallbackHint } : {}),
|
|
946
|
+
ok: !noMovement,
|
|
947
|
+
recoverable: noMovement,
|
|
948
|
+
nextStepHint: noMovement
|
|
949
|
+
? `Scroll dispatched but before/after positions did not change${edgeHint ? ` (${edgeHint})` : ""}. Observe again, inspect scroll boundaries, try the nearest scrollable container uid, or stop repeating the same scroll.`
|
|
950
|
+
: partialMovement
|
|
951
|
+
? `Scroll moved partially${partialMovement.edgeHint ? ` (${partialMovement.edgeHint})` : ""}. Observe again and compare scroll positions before repeating the same scroll.`
|
|
952
|
+
: coordinateFallbackHint
|
|
953
|
+
? "Coordinate scroll fell back to document scrolling. Observe again, inspect scrollableContainers[], and prefer a fresh scrollable container uid if a nested panel was intended."
|
|
954
|
+
: uid
|
|
955
|
+
? "Uid-targeted scroll dispatched. Observe again or read page state to verify the intended container position."
|
|
956
|
+
: "Scroll dispatched. Observe again or read page state to verify the intended viewport or container position.",
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function buildCoordinateScrollFallbackHint(result = {}) {
|
|
961
|
+
if (result.coordinateScrollFallback !== "document") return undefined
|
|
962
|
+
return {
|
|
963
|
+
reason: "coordinate-scroll-document-fallback",
|
|
964
|
+
nextAction: "observe-for-scrollable-container",
|
|
965
|
+
decision: "prefer-fresh-scrollable-container-uid",
|
|
966
|
+
recommendedTools: ["yunti_observe_page", "yunti_scroll"],
|
|
967
|
+
...(result.coordinateTarget ? { coordinateTarget: result.coordinateTarget } : {}),
|
|
968
|
+
message: "The coordinate scroll did not find a nested scrollable container and fell back to document scrolling. Observe again and choose a fresh scrollableContainers[] uid when a panel or sidebar was intended.",
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function buildUidScrollFailureResult(session, uid, scrollResult = {}) {
|
|
973
|
+
const error = scrollResult?.error || `Cannot scroll uid ${uid}`
|
|
974
|
+
const code = scrollResult?.code || inferScrollFailureCode(error)
|
|
975
|
+
const recoveryHint = {
|
|
976
|
+
reason: "uid-scroll-failed",
|
|
977
|
+
recommendedTools: ["yunti_observe_page", "yunti_take_snapshot", "yunti_scroll"],
|
|
978
|
+
nextAction: "observe-again",
|
|
979
|
+
decision: "refresh-scrollable-container-uid-before-retry",
|
|
980
|
+
uid,
|
|
981
|
+
message: "The uid scroll could not resolve a current target. Refresh observation, choose a fresh scrollable container uid from scrollableContainers[], or use document/coordinate scroll fallback before retrying.",
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
...(scrollResult && typeof scrollResult === "object" ? scrollResult : {}),
|
|
986
|
+
scrolled: false,
|
|
987
|
+
uid,
|
|
988
|
+
browserSessionId: session.browserSessionId,
|
|
989
|
+
action: "scroll",
|
|
990
|
+
target: { uid, method: "uid" },
|
|
991
|
+
ok: false,
|
|
992
|
+
recoverable: true,
|
|
993
|
+
code,
|
|
994
|
+
error,
|
|
995
|
+
recoveryHint,
|
|
996
|
+
nextStepHint: "Uid scroll failed. Observe again for a fresh scrollable container uid, inspect scrollableContainers[], or retry with document/coordinate fallback before repeating the same uid scroll.",
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function inferScrollFailureCode(error) {
|
|
1001
|
+
const message = String(error || "").toLowerCase()
|
|
1002
|
+
if (message.includes("not found") || message.includes("uid") && message.includes("latest page uid map")) {
|
|
1003
|
+
return "UID_NOT_FOUND"
|
|
1004
|
+
}
|
|
1005
|
+
if (message.includes("cannot resolve coordinates") || message.includes("off-screen") || message.includes("hidden") || message.includes("stale")) {
|
|
1006
|
+
return "UID_COORDINATES_UNAVAILABLE"
|
|
1007
|
+
}
|
|
1008
|
+
return "UID_SCROLL_FAILED"
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function inferScrollEdgeHint(deltaX, deltaY) {
|
|
1012
|
+
const x = Number(deltaX)
|
|
1013
|
+
const y = Number(deltaY)
|
|
1014
|
+
if (Number.isFinite(y) && y > 0) return "possible-bottom-edge"
|
|
1015
|
+
if (Number.isFinite(y) && y < 0) return "possible-top-edge"
|
|
1016
|
+
if (Number.isFinite(x) && x > 0) return "possible-right-edge"
|
|
1017
|
+
if (Number.isFinite(x) && x < 0) return "possible-left-edge"
|
|
1018
|
+
return "no-delta"
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function buildScrollPartialMovementHint(result = {}, args = {}) {
|
|
1022
|
+
const beforeLeft = Number(result.before?.left)
|
|
1023
|
+
const beforeTop = Number(result.before?.top)
|
|
1024
|
+
const afterLeft = Number(result.after?.left)
|
|
1025
|
+
const afterTop = Number(result.after?.top)
|
|
1026
|
+
if (
|
|
1027
|
+
!Number.isFinite(beforeLeft) ||
|
|
1028
|
+
!Number.isFinite(beforeTop) ||
|
|
1029
|
+
!Number.isFinite(afterLeft) ||
|
|
1030
|
+
!Number.isFinite(afterTop)
|
|
1031
|
+
) return undefined
|
|
1032
|
+
|
|
1033
|
+
const requestedDeltaX = Number(result.deltaX ?? args.deltaX ?? 0)
|
|
1034
|
+
const requestedDeltaY = Number(result.deltaY ?? args.deltaY ?? 0)
|
|
1035
|
+
const actualDeltaX = afterLeft - beforeLeft
|
|
1036
|
+
const actualDeltaY = afterTop - beforeTop
|
|
1037
|
+
const axes = []
|
|
1038
|
+
if (isPartialScrollMovement(requestedDeltaX, actualDeltaX)) axes.push("horizontal")
|
|
1039
|
+
if (isPartialScrollMovement(requestedDeltaY, actualDeltaY)) axes.push("vertical")
|
|
1040
|
+
if (!axes.length) return undefined
|
|
1041
|
+
|
|
1042
|
+
return {
|
|
1043
|
+
reason: "partial-scroll-movement",
|
|
1044
|
+
axes,
|
|
1045
|
+
requestedDeltaX,
|
|
1046
|
+
requestedDeltaY,
|
|
1047
|
+
actualDeltaX,
|
|
1048
|
+
actualDeltaY,
|
|
1049
|
+
edgeHint: inferScrollEdgeHint(requestedDeltaX, requestedDeltaY),
|
|
1050
|
+
nextAction: "observe-again",
|
|
1051
|
+
decision: "observe-before-continuing-scroll",
|
|
1052
|
+
message: "The scroll position changed, but less than the requested delta. Observe again before repeating the same scroll to verify whether the target container hit an edge or a different container should be used.",
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function isPartialScrollMovement(requestedDelta, actualDelta) {
|
|
1057
|
+
if (!Number.isFinite(requestedDelta) || !Number.isFinite(actualDelta)) return false
|
|
1058
|
+
if (requestedDelta === 0 || actualDelta === 0) return false
|
|
1059
|
+
if (Math.sign(requestedDelta) !== Math.sign(actualDelta)) return false
|
|
1060
|
+
return Math.abs(actualDelta) < Math.abs(requestedDelta)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function buildScrollRecoveryHint(session, uid, edgeHint, result = {}) {
|
|
1064
|
+
const hint = {
|
|
1065
|
+
reason: "no-scroll-movement",
|
|
1066
|
+
recommendedTools: ["yunti_observe_page", "yunti_scroll"],
|
|
1067
|
+
nextAction: "observe-again",
|
|
1068
|
+
}
|
|
1069
|
+
if (edgeHint) hint.edgeHint = edgeHint
|
|
1070
|
+
if (uid) {
|
|
1071
|
+
hint.uid = uid
|
|
1072
|
+
hint.currentTarget = "scrollable-container"
|
|
1073
|
+
const target = pageUidStore.get(session.browserSessionId)?.uidMap?.[uid]
|
|
1074
|
+
if (target) {
|
|
1075
|
+
hint.lastObservedContainer = {
|
|
1076
|
+
uid,
|
|
1077
|
+
canScrollVertical: Boolean(target.canScrollVertical),
|
|
1078
|
+
canScrollHorizontal: Boolean(target.canScrollHorizontal),
|
|
1079
|
+
...(Number.isFinite(Number(target.pixelsAbove)) ? { pixelsAbove: Number(target.pixelsAbove) } : {}),
|
|
1080
|
+
...(Number.isFinite(Number(target.pixelsBelow)) ? { pixelsBelow: Number(target.pixelsBelow) } : {}),
|
|
1081
|
+
...(Number.isFinite(Number(target.pixelsLeft)) ? { pixelsLeft: Number(target.pixelsLeft) } : {}),
|
|
1082
|
+
...(Number.isFinite(Number(target.pixelsRight)) ? { pixelsRight: Number(target.pixelsRight) } : {}),
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
hint.currentTarget = "document-or-coordinate-container"
|
|
1087
|
+
}
|
|
1088
|
+
if (edgeHint === "possible-bottom-edge" || edgeHint === "possible-top-edge") {
|
|
1089
|
+
hint.nextAction = uid ? "try-opposite-direction-or-nearest-container" : "observe-for-scrollable-container"
|
|
1090
|
+
} else if (edgeHint === "possible-right-edge" || edgeHint === "possible-left-edge") {
|
|
1091
|
+
hint.nextAction = uid ? "try-opposite-horizontal-direction-or-nearest-container" : "observe-for-horizontal-container"
|
|
1092
|
+
} else if (edgeHint === "no-delta") {
|
|
1093
|
+
hint.nextAction = "provide-nonzero-scroll-delta"
|
|
1094
|
+
}
|
|
1095
|
+
const decision = buildScrollRecoveryDecision(uid, edgeHint)
|
|
1096
|
+
if (decision) hint.decision = decision
|
|
1097
|
+
const suggestedRetry = buildScrollSuggestedRetry(uid, edgeHint, result)
|
|
1098
|
+
if (suggestedRetry) hint.suggestedRetry = suggestedRetry
|
|
1099
|
+
hint.message = "The scroll command dispatched, but the scroll position did not change. Refresh observation before retrying, inspect scroll boundaries, or target a different scrollable container uid."
|
|
1100
|
+
return hint
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function buildScrollRecoveryDecision(uid, edgeHint) {
|
|
1104
|
+
if (edgeHint === "no-delta") return "provide-nonzero-delta"
|
|
1105
|
+
if (edgeHint === "possible-bottom-edge" || edgeHint === "possible-top-edge") {
|
|
1106
|
+
return uid ? "retry-opposite-vertical-on-same-container-once" : "observe-for-scrollable-container"
|
|
1107
|
+
}
|
|
1108
|
+
if (edgeHint === "possible-right-edge" || edgeHint === "possible-left-edge") {
|
|
1109
|
+
return uid ? "retry-opposite-horizontal-on-same-container-once" : "observe-for-horizontal-scrollable-container"
|
|
1110
|
+
}
|
|
1111
|
+
return undefined
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function buildScrollSuggestedRetry(uid, edgeHint, result = {}) {
|
|
1115
|
+
const retry = {}
|
|
1116
|
+
const deltaX = Number(result.deltaX)
|
|
1117
|
+
const deltaY = Number(result.deltaY)
|
|
1118
|
+
if (edgeHint === "possible-bottom-edge" || edgeHint === "possible-top-edge") {
|
|
1119
|
+
if (!Number.isFinite(deltaY) || deltaY === 0) return undefined
|
|
1120
|
+
retry.deltaY = -deltaY
|
|
1121
|
+
} else if (edgeHint === "possible-right-edge" || edgeHint === "possible-left-edge") {
|
|
1122
|
+
if (!Number.isFinite(deltaX) || deltaX === 0) return undefined
|
|
1123
|
+
retry.deltaX = -deltaX
|
|
1124
|
+
} else {
|
|
1125
|
+
return undefined
|
|
1126
|
+
}
|
|
1127
|
+
if (uid) retry.uid = uid
|
|
1128
|
+
retry.note = uid
|
|
1129
|
+
? "Try the opposite direction once on the same observed container, then observe again or switch to a nearer scrollable container if it still does not move."
|
|
1130
|
+
: "Observe first for a scrollable container uid; only use this opposite delta if document scrolling is still the intended target."
|
|
1131
|
+
return retry
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function selectElement(tabId, session, args = {}) {
|
|
1135
|
+
const uid = String(args.uid || "").trim()
|
|
1136
|
+
const value = String(args.value ?? "")
|
|
1137
|
+
const text = String(args.text ?? "")
|
|
1138
|
+
if (uid) {
|
|
1139
|
+
const matchMode = text ? "text" : "value"
|
|
1140
|
+
const targetOption = text || value
|
|
1141
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
1142
|
+
if (!point.ok) {
|
|
1143
|
+
return buildUidSelectFailureResult(session, uid, matchMode, targetOption, {
|
|
1144
|
+
code: "UID_COORDINATES_UNAVAILABLE",
|
|
1145
|
+
error: point.error,
|
|
1146
|
+
})
|
|
1147
|
+
}
|
|
1148
|
+
const selectResult = await chrome.tabs.sendMessage(tabId, {
|
|
1149
|
+
type: "yunti_execute_tool",
|
|
1150
|
+
tool: "yunti_select",
|
|
1151
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
1152
|
+
})
|
|
1153
|
+
if (!selectResult?.selected) {
|
|
1154
|
+
return buildUidSelectFailureResult(session, uid, matchMode, targetOption, selectResult)
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
...selectResult,
|
|
1158
|
+
selected: true,
|
|
1159
|
+
uid,
|
|
1160
|
+
value: selectResult.value,
|
|
1161
|
+
text: selectResult.text,
|
|
1162
|
+
selectedIndex: selectResult.selectedIndex,
|
|
1163
|
+
browserSessionId: session.browserSessionId,
|
|
1164
|
+
action: "select",
|
|
1165
|
+
target: { uid, method: matchMode === "text" ? "uid.text" : "uid.value" },
|
|
1166
|
+
ok: true,
|
|
1167
|
+
recoverable: false,
|
|
1168
|
+
nextStepHint: matchMode === "text"
|
|
1169
|
+
? "Uid select dispatched by visible option text without attaching Chrome debugger. Observe again, read page state, or evaluate the select value to verify the intended change."
|
|
1170
|
+
: "Uid select dispatched by option value without attaching Chrome debugger. Observe again, read page state, or evaluate the select value to verify the intended change.",
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const selector = String(args.selector || "").trim()
|
|
1175
|
+
let result
|
|
1176
|
+
try {
|
|
1177
|
+
result = await chrome.tabs.sendMessage(tabId, {
|
|
1178
|
+
type: "yunti_execute_tool",
|
|
1179
|
+
tool: "yunti_select",
|
|
1180
|
+
arguments: args,
|
|
1181
|
+
})
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
return buildSelectorSelectFailureResult(session, selector, args.value, {
|
|
1184
|
+
code: "SELECTOR_SELECT_FAILED",
|
|
1185
|
+
error: error?.message || "Selector select failed",
|
|
1186
|
+
})
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (!result || typeof result !== "object" || result.selected !== true) {
|
|
1190
|
+
return buildSelectorSelectFailureResult(session, selector, args.value, result)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const requestedValue = String(args.value ?? "")
|
|
1194
|
+
const disabledOption = Array.isArray(result.options)
|
|
1195
|
+
? result.options.find((option) => String(option?.value ?? "") === requestedValue && option.disabled === true)
|
|
1196
|
+
: undefined
|
|
1197
|
+
if (requestedValue && disabledOption) {
|
|
1198
|
+
return buildSelectorSelectFailureResult(session, selector, requestedValue, {
|
|
1199
|
+
...result,
|
|
1200
|
+
selected: false,
|
|
1201
|
+
code: "OPTION_DISABLED",
|
|
1202
|
+
error: "Option value is disabled",
|
|
1203
|
+
disabledValue: String(disabledOption.value ?? ""),
|
|
1204
|
+
disabledText: String(disabledOption.text ?? ""),
|
|
1205
|
+
})
|
|
1206
|
+
}
|
|
1207
|
+
if (requestedValue && String(result.value ?? "") !== requestedValue) {
|
|
1208
|
+
return buildSelectorSelectFailureResult(session, selector, requestedValue, {
|
|
1209
|
+
...result,
|
|
1210
|
+
code: "OPTION_NOT_FOUND",
|
|
1211
|
+
error: "Option value not found",
|
|
1212
|
+
actualValue: result.value,
|
|
1213
|
+
})
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return {
|
|
1217
|
+
...result,
|
|
1218
|
+
selector,
|
|
1219
|
+
browserSessionId: session.browserSessionId,
|
|
1220
|
+
action: "select",
|
|
1221
|
+
target: { selector, method: "selector" },
|
|
1222
|
+
ok: true,
|
|
1223
|
+
recoverable: false,
|
|
1224
|
+
nextStepHint: "Select dispatched. Observe again, read page state, or evaluate the select value to verify the intended change.",
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function buildSelectorSelectFailureResult(session, selector, targetOption, selectResult = {}) {
|
|
1229
|
+
const availableValues = Array.isArray(selectResult?.availableValues) ? selectResult.availableValues : undefined
|
|
1230
|
+
const availableTexts = Array.isArray(selectResult?.availableTexts) ? selectResult.availableTexts : undefined
|
|
1231
|
+
const disabledValue = selectResult?.disabledValue !== undefined ? String(selectResult.disabledValue) : undefined
|
|
1232
|
+
const disabledText = selectResult?.disabledText !== undefined ? String(selectResult.disabledText) : undefined
|
|
1233
|
+
const code = selectResult?.code || (selectResult?.selected === false ? "SELECTOR_SELECT_FAILED" : "SELECTOR_SELECT_FAILED")
|
|
1234
|
+
const error = selectResult?.error || "Selector select failed"
|
|
1235
|
+
const recoveryHint = {
|
|
1236
|
+
reason: "selector-select-failed",
|
|
1237
|
+
recommendedTools: ["yunti_observe_page", "yunti_take_snapshot", "yunti_evaluate_script", "yunti_select"],
|
|
1238
|
+
nextAction: code === "OPTION_NOT_FOUND" || code === "OPTION_DISABLED" ? "inspect-available-options" : "inspect-target-element",
|
|
1239
|
+
decision: code === "OPTION_DISABLED" ? "choose-enabled-option-or-unlock-field" : code === "OPTION_NOT_FOUND" ? "inspect-options-before-retry" : "use-select-element-or-uid-fallback",
|
|
1240
|
+
selector,
|
|
1241
|
+
matchMode: "value",
|
|
1242
|
+
targetOption: String(targetOption ?? ""),
|
|
1243
|
+
...(availableValues ? { availableValues } : {}),
|
|
1244
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
1245
|
+
...(disabledValue ? { disabledValue } : {}),
|
|
1246
|
+
...(disabledText ? { disabledText } : {}),
|
|
1247
|
+
message: "The selector select could not be completed. Inspect the target select element and available options before retrying, or use a fresh uid/value fallback.",
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
...(selectResult && typeof selectResult === "object" ? selectResult : {}),
|
|
1252
|
+
selected: false,
|
|
1253
|
+
selector,
|
|
1254
|
+
browserSessionId: session.browserSessionId,
|
|
1255
|
+
action: "select",
|
|
1256
|
+
target: { selector, method: "selector" },
|
|
1257
|
+
ok: false,
|
|
1258
|
+
recoverable: true,
|
|
1259
|
+
code,
|
|
1260
|
+
error,
|
|
1261
|
+
matchMode: "value",
|
|
1262
|
+
targetOption: String(targetOption ?? ""),
|
|
1263
|
+
...(availableValues ? { availableValues } : {}),
|
|
1264
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
1265
|
+
recoveryHint,
|
|
1266
|
+
nextStepHint: "Selector select failed. Inspect available options, observe again for a fresh uid, or retry with uid/value fallback before repeating the same selector select.",
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function buildUidSelectFailureResult(session, uid, matchMode, targetOption, selectResult = {}) {
|
|
1271
|
+
const availableValues = Array.isArray(selectResult.availableValues) ? selectResult.availableValues : undefined
|
|
1272
|
+
const availableTexts = Array.isArray(selectResult.availableTexts) ? selectResult.availableTexts : undefined
|
|
1273
|
+
const disabledValue = selectResult?.disabledValue !== undefined ? String(selectResult.disabledValue) : undefined
|
|
1274
|
+
const disabledText = selectResult?.disabledText !== undefined ? String(selectResult.disabledText) : undefined
|
|
1275
|
+
const recoveryHint = {
|
|
1276
|
+
reason: "uid-select-failed",
|
|
1277
|
+
recommendedTools: ["yunti_observe_page", "yunti_take_snapshot", "yunti_evaluate_script", "yunti_select"],
|
|
1278
|
+
nextAction: "inspect-available-options",
|
|
1279
|
+
decision: selectResult.code === "OPTION_DISABLED" ? "choose-enabled-option-or-unlock-field" : "inspect-options-before-retry",
|
|
1280
|
+
uid,
|
|
1281
|
+
matchMode,
|
|
1282
|
+
targetOption,
|
|
1283
|
+
...(availableValues ? { availableValues } : {}),
|
|
1284
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
1285
|
+
...(disabledValue ? { disabledValue } : {}),
|
|
1286
|
+
...(disabledText ? { disabledText } : {}),
|
|
1287
|
+
message: "The select option could not be matched. Inspect available options before retrying, refresh observation if the uid may be stale, or use selector/value fallback.",
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (selectResult.code === "ELEMENT_NOT_FOUND") {
|
|
1291
|
+
recoveryHint.nextAction = "observe-again"
|
|
1292
|
+
recoveryHint.decision = "refresh-observation-before-retry"
|
|
1293
|
+
} else if (selectResult.code === "NOT_SELECT") {
|
|
1294
|
+
recoveryHint.nextAction = "inspect-target-element"
|
|
1295
|
+
recoveryHint.decision = "use-select-element-or-selector-fallback"
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return {
|
|
1299
|
+
selected: false,
|
|
1300
|
+
uid,
|
|
1301
|
+
browserSessionId: session.browserSessionId,
|
|
1302
|
+
action: "select",
|
|
1303
|
+
target: { uid, method: matchMode === "text" ? "uid.text" : "uid.value" },
|
|
1304
|
+
ok: false,
|
|
1305
|
+
recoverable: true,
|
|
1306
|
+
code: selectResult.code || "UID_SELECT_FAILED",
|
|
1307
|
+
error: selectResult.error || `Cannot select option at uid ${uid}`,
|
|
1308
|
+
matchMode,
|
|
1309
|
+
targetOption,
|
|
1310
|
+
...(availableValues ? { availableValues } : {}),
|
|
1311
|
+
...(availableTexts ? { availableTexts } : {}),
|
|
1312
|
+
...(disabledValue ? { disabledValue } : {}),
|
|
1313
|
+
...(disabledText ? { disabledText } : {}),
|
|
1314
|
+
recoveryHint,
|
|
1315
|
+
nextStepHint: "Uid select failed. Inspect available options, observe again for a fresh uid, or retry with selector/value fallback before repeating the same select.",
|
|
1316
|
+
}
|
|
630
1317
|
}
|
|
631
1318
|
|
|
632
1319
|
function validateFillArgs(args = {}) {
|
|
@@ -648,18 +1335,57 @@ export function createToolDispatcher({
|
|
|
648
1335
|
async function clickViaSnapshotUid(tabId, session, uid) {
|
|
649
1336
|
const { x, y } = await resolveUidCenter(tabId, session, uid)
|
|
650
1337
|
await mouseClick(tabId, x, y, 1)
|
|
651
|
-
|
|
1338
|
+
const roundedX = Math.round(x)
|
|
1339
|
+
const roundedY = Math.round(y)
|
|
1340
|
+
return {
|
|
1341
|
+
clicked: true,
|
|
1342
|
+
uid,
|
|
1343
|
+
x: roundedX,
|
|
1344
|
+
y: roundedY,
|
|
1345
|
+
browserSessionId: session.browserSessionId,
|
|
1346
|
+
action: "click",
|
|
1347
|
+
target: { uid, x: roundedX, y: roundedY },
|
|
1348
|
+
ok: true,
|
|
1349
|
+
recoverable: false,
|
|
1350
|
+
nextStepHint: "Click dispatched. Observe again or read page state to verify the intended change.",
|
|
1351
|
+
}
|
|
652
1352
|
}
|
|
653
1353
|
|
|
654
1354
|
async function hoverViaSnapshotUid(tabId, session, uid) {
|
|
655
1355
|
const { x, y } = await resolveUidCenter(tabId, session, uid)
|
|
656
1356
|
await mouseMove(tabId, x, y)
|
|
657
|
-
|
|
1357
|
+
const roundedX = Math.round(x)
|
|
1358
|
+
const roundedY = Math.round(y)
|
|
1359
|
+
return {
|
|
1360
|
+
hovered: true,
|
|
1361
|
+
uid,
|
|
1362
|
+
x: roundedX,
|
|
1363
|
+
y: roundedY,
|
|
1364
|
+
browserSessionId: session.browserSessionId,
|
|
1365
|
+
action: "hover",
|
|
1366
|
+
target: { uid, x: roundedX, y: roundedY },
|
|
1367
|
+
ok: true,
|
|
1368
|
+
recoverable: false,
|
|
1369
|
+
nextStepHint: "Hover dispatched. Observe again or read page state to verify menus, tooltips, or hover-only controls.",
|
|
1370
|
+
}
|
|
658
1371
|
}
|
|
659
1372
|
|
|
660
1373
|
async function clickAtCoordinate(tabId, session, x, y) {
|
|
661
1374
|
await mouseClick(tabId, x, y, 1)
|
|
662
|
-
|
|
1375
|
+
const roundedX = Math.round(x)
|
|
1376
|
+
const roundedY = Math.round(y)
|
|
1377
|
+
return {
|
|
1378
|
+
clicked: true,
|
|
1379
|
+
x: roundedX,
|
|
1380
|
+
y: roundedY,
|
|
1381
|
+
browserSessionId: session.browserSessionId,
|
|
1382
|
+
method: "coordinate",
|
|
1383
|
+
action: "click",
|
|
1384
|
+
target: { method: "coordinate", x: roundedX, y: roundedY },
|
|
1385
|
+
ok: true,
|
|
1386
|
+
recoverable: false,
|
|
1387
|
+
nextStepHint: "Coordinate click dispatched. Observe again, read page state, or use a fresh uid when possible to verify the intended change.",
|
|
1388
|
+
}
|
|
663
1389
|
}
|
|
664
1390
|
|
|
665
1391
|
async function resolveSelectorCenter(tabId, selector) {
|
|
@@ -695,36 +1421,72 @@ export function createToolDispatcher({
|
|
|
695
1421
|
}
|
|
696
1422
|
|
|
697
1423
|
async function resolveUidCenter(tabId, session, uid) {
|
|
698
|
-
const
|
|
1424
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
1425
|
+
if (point.ok) return { x: point.x, y: point.y }
|
|
1426
|
+
throw new Error(point.error)
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function resolveObservedUidCenter(session, uid) {
|
|
1430
|
+
const uidState = pageUidStore.get(session.browserSessionId)
|
|
1431
|
+
const uidMap = uidState?.uidMap
|
|
699
1432
|
if (!uidMap || !uidMap[uid]) {
|
|
700
|
-
|
|
1433
|
+
return {
|
|
1434
|
+
ok: false,
|
|
1435
|
+
error: `uid ${uid} not found in the latest page uid map. Run yunti_observe_page again before retrying.`,
|
|
1436
|
+
}
|
|
701
1437
|
}
|
|
702
1438
|
const el = uidMap[uid]
|
|
703
1439
|
|
|
704
1440
|
if (el.rect && el.rect.width > 0 && el.rect.height > 0) {
|
|
705
1441
|
return {
|
|
1442
|
+
ok: true,
|
|
706
1443
|
x: el.rect.x + el.rect.width / 2,
|
|
707
1444
|
y: el.rect.y + el.rect.height / 2,
|
|
708
1445
|
}
|
|
709
1446
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1447
|
+
|
|
1448
|
+
return {
|
|
1449
|
+
ok: false,
|
|
1450
|
+
error: `Cannot resolve coordinates for uid ${uid} without Chrome debugger. Run yunti_observe_page with includeRects enabled, scroll the element into view, or use an explicit selector fallback.`,
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function buildUidPointFailureResult(session, uid, action, error) {
|
|
1455
|
+
const code = /not found|latest page uid map/i.test(String(error || "")) ? "UID_NOT_FOUND" : "UID_COORDINATES_UNAVAILABLE"
|
|
1456
|
+
const resultFieldByAction = {
|
|
1457
|
+
click: "clicked",
|
|
1458
|
+
hover: "hovered",
|
|
1459
|
+
type: "typed",
|
|
1460
|
+
press: "pressed",
|
|
1461
|
+
}
|
|
1462
|
+
const resultField = resultFieldByAction[action] || `${action}ed`
|
|
1463
|
+
const recommendedToolByAction = {
|
|
1464
|
+
click: "yunti_click",
|
|
1465
|
+
hover: "yunti_hover",
|
|
1466
|
+
type: "yunti_type_text",
|
|
1467
|
+
press: "yunti_press_key",
|
|
1468
|
+
}
|
|
1469
|
+
const recommendedTool = recommendedToolByAction[action] || "yunti_observe_page"
|
|
1470
|
+
return {
|
|
1471
|
+
[resultField]: false,
|
|
1472
|
+
uid,
|
|
1473
|
+
browserSessionId: session.browserSessionId,
|
|
1474
|
+
action,
|
|
1475
|
+
target: { uid, method: "uid" },
|
|
1476
|
+
ok: false,
|
|
1477
|
+
recoverable: true,
|
|
1478
|
+
code,
|
|
1479
|
+
error: error || `Cannot ${action} uid ${uid}`,
|
|
1480
|
+
recoveryHint: {
|
|
1481
|
+
reason: `uid-${action}-failed`,
|
|
1482
|
+
recommendedTools: ["yunti_observe_page", recommendedTool],
|
|
1483
|
+
nextAction: "observe-again",
|
|
1484
|
+
decision: "refresh-observation-before-retry",
|
|
1485
|
+
uid,
|
|
1486
|
+
message: "The uid could not be resolved without attaching Chrome debugger. Observe again with rects enabled, scroll the target into view, or retry with selector fallback.",
|
|
1487
|
+
},
|
|
1488
|
+
nextStepHint: `Uid ${action} failed without attaching Chrome debugger. Observe again for a fresh visible uid before retrying.`,
|
|
725
1489
|
}
|
|
726
|
-
|
|
727
|
-
throw new Error(`Cannot resolve coordinates for uid ${uid}. Element may be off-screen or hidden.`)
|
|
728
1490
|
}
|
|
729
1491
|
|
|
730
1492
|
async function mouseClick(tabId, x, y, clickCount = 1) {
|
|
@@ -756,24 +1518,60 @@ export function createToolDispatcher({
|
|
|
756
1518
|
for (const field of fields) {
|
|
757
1519
|
try {
|
|
758
1520
|
const uid = String(field.uid || "").trim()
|
|
1521
|
+
let fieldResult
|
|
759
1522
|
if (uid) {
|
|
760
|
-
await fillByUid(tabId, session, { uid, value: String(field.value || "") })
|
|
1523
|
+
fieldResult = await fillByUid(tabId, session, { uid, value: String(field.value || "") })
|
|
761
1524
|
} else {
|
|
762
|
-
await
|
|
763
|
-
type: "yunti_execute_tool",
|
|
764
|
-
tool: "yunti_fill",
|
|
765
|
-
arguments: { selector: field.selector, value: field.value },
|
|
766
|
-
})
|
|
1525
|
+
fieldResult = await fillByUid(tabId, session, { selector: field.selector, value: field.value })
|
|
767
1526
|
}
|
|
768
|
-
|
|
769
|
-
|
|
1527
|
+
const normalized = buildFillFormFieldResult(field, fieldResult)
|
|
1528
|
+
if (normalized.ok) filled++
|
|
1529
|
+
else failed++
|
|
1530
|
+
results.push(normalized)
|
|
770
1531
|
} catch (err) {
|
|
771
1532
|
failed++
|
|
772
1533
|
results.push({ uid: field.uid, selector: field.selector, ok: false, error: err?.message || String(err) })
|
|
773
1534
|
}
|
|
774
1535
|
}
|
|
775
1536
|
|
|
776
|
-
return {
|
|
1537
|
+
return {
|
|
1538
|
+
filled,
|
|
1539
|
+
failed,
|
|
1540
|
+
results,
|
|
1541
|
+
browserSessionId: session.browserSessionId,
|
|
1542
|
+
action: "fill_form",
|
|
1543
|
+
target: {
|
|
1544
|
+
fieldCount: fields.length,
|
|
1545
|
+
filled,
|
|
1546
|
+
failed,
|
|
1547
|
+
},
|
|
1548
|
+
ok: failed === 0,
|
|
1549
|
+
recoverable: failed > 0,
|
|
1550
|
+
nextStepHint: failed === 0
|
|
1551
|
+
? "Form fill dispatched. Observe again, read page state, or evaluate field values to verify the intended changes."
|
|
1552
|
+
: "Form fill partially failed. Inspect per-field results, observe again for fresh uids, or retry failed fields with selector fallback.",
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function buildFillFormFieldResult(field, fieldResult = {}) {
|
|
1557
|
+
const ok = Boolean(fieldResult?.filled || fieldResult?.ok === true)
|
|
1558
|
+
if (ok) return { uid: field.uid, selector: field.selector, ok: true }
|
|
1559
|
+
|
|
1560
|
+
const diagnostics = fieldResult && typeof fieldResult === "object" ? fieldResult : {}
|
|
1561
|
+
const preservedKeys = [
|
|
1562
|
+
"code",
|
|
1563
|
+
"error",
|
|
1564
|
+
"recoveryHint",
|
|
1565
|
+
"availableValues",
|
|
1566
|
+
"availableTexts",
|
|
1567
|
+
"nextStepHint",
|
|
1568
|
+
]
|
|
1569
|
+
const result = { uid: field.uid, selector: field.selector, ok: false }
|
|
1570
|
+
for (const key of preservedKeys) {
|
|
1571
|
+
if (diagnostics[key] !== undefined) result[key] = diagnostics[key]
|
|
1572
|
+
}
|
|
1573
|
+
if (!result.error) result.error = "fill failed"
|
|
1574
|
+
return result
|
|
777
1575
|
}
|
|
778
1576
|
|
|
779
1577
|
async function waitForCondition(tabId, session, args = {}) {
|
|
@@ -795,7 +1593,12 @@ export function createToolDispatcher({
|
|
|
795
1593
|
if (urlContains) {
|
|
796
1594
|
const tab = await chrome.tabs.get(tabId)
|
|
797
1595
|
if (tab.url && tab.url.includes(urlContains)) {
|
|
798
|
-
return
|
|
1596
|
+
return buildWaitForSuccess(
|
|
1597
|
+
session,
|
|
1598
|
+
{ text, selector, urlContains, timeoutMs },
|
|
1599
|
+
{ found: true, condition: "urlContains", value: urlContains },
|
|
1600
|
+
Date.now() - startTime
|
|
1601
|
+
)
|
|
799
1602
|
}
|
|
800
1603
|
}
|
|
801
1604
|
|
|
@@ -811,16 +1614,72 @@ export function createToolDispatcher({
|
|
|
811
1614
|
"Runtime.evaluate",
|
|
812
1615
|
{ expression, returnByValue: true }
|
|
813
1616
|
)
|
|
814
|
-
|
|
1617
|
+
|
|
815
1618
|
if (result?.result?.value) {
|
|
816
|
-
return
|
|
1619
|
+
return buildWaitForSuccess(
|
|
1620
|
+
session,
|
|
1621
|
+
{ text, selector, urlContains, timeoutMs },
|
|
1622
|
+
result.result.value,
|
|
1623
|
+
Date.now() - startTime
|
|
1624
|
+
)
|
|
817
1625
|
}
|
|
818
1626
|
}
|
|
819
1627
|
|
|
820
1628
|
await delayCdp(200)
|
|
821
1629
|
}
|
|
822
1630
|
|
|
823
|
-
return {
|
|
1631
|
+
return buildWaitForTimeout(session, { text, selector, urlContains, timeoutMs }, timeoutMs)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function buildWaitForTarget({ text, selector, urlContains, timeoutMs } = {}) {
|
|
1635
|
+
const target = {}
|
|
1636
|
+
if (text) target.text = text
|
|
1637
|
+
if (selector) target.selector = selector
|
|
1638
|
+
if (urlContains) target.urlContains = urlContains
|
|
1639
|
+
if (timeoutMs !== undefined) target.timeoutMs = timeoutMs
|
|
1640
|
+
return target
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function buildWaitForSuccess(session, args, match = {}, waitedMs = 0) {
|
|
1644
|
+
const condition = typeof match.condition === "string"
|
|
1645
|
+
? match.condition
|
|
1646
|
+
: match.found === "selector" || match.found === "text"
|
|
1647
|
+
? match.found
|
|
1648
|
+
: undefined
|
|
1649
|
+
const result = {
|
|
1650
|
+
found: true,
|
|
1651
|
+
...match,
|
|
1652
|
+
waitedMs,
|
|
1653
|
+
browserSessionId: session.browserSessionId,
|
|
1654
|
+
action: "wait_for",
|
|
1655
|
+
target: buildWaitForTarget(args),
|
|
1656
|
+
ok: true,
|
|
1657
|
+
recoverable: false,
|
|
1658
|
+
nextStepHint: "Wait condition matched. Call yunti_observe_page and continue with a fresh uid before acting on newly rendered content.",
|
|
1659
|
+
}
|
|
1660
|
+
if (condition && result.condition === undefined) result.condition = condition
|
|
1661
|
+
return result
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function buildWaitForTimeout(session, args, waitedMs) {
|
|
1665
|
+
return {
|
|
1666
|
+
found: false,
|
|
1667
|
+
waitedMs,
|
|
1668
|
+
browserSessionId: session.browserSessionId,
|
|
1669
|
+
action: "wait_for",
|
|
1670
|
+
target: buildWaitForTarget(args),
|
|
1671
|
+
ok: false,
|
|
1672
|
+
recoverable: true,
|
|
1673
|
+
code: "WAIT_TIMEOUT",
|
|
1674
|
+
recoveryHint: {
|
|
1675
|
+
reason: "condition-not-met-before-timeout",
|
|
1676
|
+
recommendedTools: ["yunti_observe_page", "yunti_get_page_snapshot", "yunti_take_screenshot"],
|
|
1677
|
+
nextAction: "observe-or-adjust-condition",
|
|
1678
|
+
decision: "observe-before-retry",
|
|
1679
|
+
message: "The expected text, selector, or URL state did not appear before the timeout. Observe the page or adjust the wait condition before repeating the same wait.",
|
|
1680
|
+
},
|
|
1681
|
+
nextStepHint: "Wait timed out. Observe the current page, inspect whether the condition changed, or adjust the wait target before retrying.",
|
|
1682
|
+
}
|
|
824
1683
|
}
|
|
825
1684
|
|
|
826
1685
|
async function handleDialog(tabId, session, args = {}) {
|
|
@@ -1000,7 +1859,20 @@ export function createToolDispatcher({
|
|
|
1000
1859
|
type: "mouseReleased", x: toX, y: toY, button, clickCount: 1,
|
|
1001
1860
|
})
|
|
1002
1861
|
|
|
1003
|
-
|
|
1862
|
+
const from = { x: Math.round(fromX), y: Math.round(fromY) }
|
|
1863
|
+
const to = { x: Math.round(toX), y: Math.round(toY) }
|
|
1864
|
+
return {
|
|
1865
|
+
dragged: true,
|
|
1866
|
+
from,
|
|
1867
|
+
to,
|
|
1868
|
+
steps,
|
|
1869
|
+
browserSessionId: session.browserSessionId,
|
|
1870
|
+
action: "drag",
|
|
1871
|
+
target: { method: "coordinate", from, to },
|
|
1872
|
+
ok: true,
|
|
1873
|
+
recoverable: false,
|
|
1874
|
+
nextStepHint: "Drag dispatched. Observe again or read page state to verify the intended movement or drop result.",
|
|
1875
|
+
}
|
|
1004
1876
|
}
|
|
1005
1877
|
|
|
1006
1878
|
async function uploadFile(tabId, session, args = {}) {
|
|
@@ -1036,77 +1908,139 @@ export function createToolDispatcher({
|
|
|
1036
1908
|
backendNodeId,
|
|
1037
1909
|
})
|
|
1038
1910
|
|
|
1039
|
-
return {
|
|
1911
|
+
return {
|
|
1912
|
+
uploaded: true,
|
|
1913
|
+
fileCount: filePaths.length,
|
|
1914
|
+
filePaths,
|
|
1915
|
+
browserSessionId: session.browserSessionId,
|
|
1916
|
+
action: "upload_file",
|
|
1917
|
+
target: uid ? { uid, method: "uid" } : { selector, method: "selector" },
|
|
1918
|
+
ok: true,
|
|
1919
|
+
recoverable: false,
|
|
1920
|
+
nextStepHint: "File upload dispatched. Observe again, read page state, or verify the selected file input before submitting any form.",
|
|
1921
|
+
}
|
|
1040
1922
|
}
|
|
1041
1923
|
|
|
1042
1924
|
async function typeTextByUid(tabId, session, args = {}) {
|
|
1043
1925
|
const uid = String(args.uid || "").trim()
|
|
1044
1926
|
if (!uid) {
|
|
1045
1927
|
// Fall back to content script for selector/coordinate-based typing
|
|
1046
|
-
|
|
1928
|
+
const selector = String(args.selector || "").trim()
|
|
1929
|
+
const hasX = Number.isFinite(Number(args.x))
|
|
1930
|
+
const hasY = Number.isFinite(Number(args.y))
|
|
1931
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
1047
1932
|
type: "yunti_execute_tool",
|
|
1048
1933
|
tool: "yunti_type_text",
|
|
1049
1934
|
arguments: args,
|
|
1050
1935
|
})
|
|
1936
|
+
|
|
1937
|
+
if (!result || typeof result !== "object" || result.typed !== true) {
|
|
1938
|
+
return result
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const target =
|
|
1942
|
+
selector
|
|
1943
|
+
? { selector, method: "selector" }
|
|
1944
|
+
: hasX && hasY
|
|
1945
|
+
? { method: "coordinate", x: Math.round(Number(args.x)), y: Math.round(Number(args.y)) }
|
|
1946
|
+
: { method: "focused" }
|
|
1947
|
+
|
|
1948
|
+
return {
|
|
1949
|
+
...result,
|
|
1950
|
+
browserSessionId: session.browserSessionId,
|
|
1951
|
+
action: "type_text",
|
|
1952
|
+
target,
|
|
1953
|
+
ok: true,
|
|
1954
|
+
recoverable: false,
|
|
1955
|
+
nextStepHint: "Type text dispatched. Observe again, read page state, or evaluate the active field value to verify the intended change.",
|
|
1956
|
+
}
|
|
1051
1957
|
}
|
|
1052
1958
|
|
|
1053
1959
|
const text = String(args.text || "")
|
|
1054
1960
|
if (!text) throw new Error("text is required")
|
|
1055
1961
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
await
|
|
1059
|
-
|
|
1962
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
1963
|
+
if (!point.ok) return buildUidPointFailureResult(session, uid, "type", point.error)
|
|
1964
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
1965
|
+
type: "yunti_execute_tool",
|
|
1966
|
+
tool: "yunti_type_text",
|
|
1967
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
1968
|
+
})
|
|
1969
|
+
if (!result || typeof result !== "object" || result.typed !== true) return result
|
|
1060
1970
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1971
|
+
return {
|
|
1972
|
+
...result,
|
|
1973
|
+
typed: true,
|
|
1974
|
+
uid,
|
|
1975
|
+
text,
|
|
1976
|
+
method: result.method || "dom",
|
|
1977
|
+
browserSessionId: session.browserSessionId,
|
|
1978
|
+
action: "type_text",
|
|
1979
|
+
target: { uid, method: result.method || "dom" },
|
|
1980
|
+
ok: true,
|
|
1981
|
+
recoverable: false,
|
|
1982
|
+
nextStepHint: "Type text dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended change.",
|
|
1068
1983
|
}
|
|
1069
|
-
|
|
1070
|
-
return { typed: true, uid, text, method: "cdp.keyboard", browserSessionId: session.browserSessionId }
|
|
1071
1984
|
}
|
|
1072
1985
|
|
|
1073
1986
|
async function pressKeyByUid(tabId, session, args = {}) {
|
|
1074
1987
|
const uid = String(args.uid || "").trim()
|
|
1075
1988
|
if (!uid) {
|
|
1076
|
-
|
|
1989
|
+
const selector = String(args.selector || "").trim()
|
|
1990
|
+
const hasX = Number.isFinite(Number(args.x))
|
|
1991
|
+
const hasY = Number.isFinite(Number(args.y))
|
|
1992
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
1077
1993
|
type: "yunti_execute_tool",
|
|
1078
1994
|
tool: "yunti_press_key",
|
|
1079
1995
|
arguments: args,
|
|
1080
1996
|
})
|
|
1997
|
+
|
|
1998
|
+
if (!result || typeof result !== "object" || result.pressed !== true) {
|
|
1999
|
+
return result
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
const target =
|
|
2003
|
+
selector
|
|
2004
|
+
? { selector, method: "selector" }
|
|
2005
|
+
: hasX && hasY
|
|
2006
|
+
? { method: "coordinate", x: Math.round(Number(args.x)), y: Math.round(Number(args.y)) }
|
|
2007
|
+
: { method: "focused" }
|
|
2008
|
+
|
|
2009
|
+
return {
|
|
2010
|
+
...result,
|
|
2011
|
+
browserSessionId: session.browserSessionId,
|
|
2012
|
+
action: "press_key",
|
|
2013
|
+
target,
|
|
2014
|
+
ok: true,
|
|
2015
|
+
recoverable: false,
|
|
2016
|
+
nextStepHint: "Key press dispatched. Observe again, read page state, or evaluate the active field value to verify the intended effect.",
|
|
2017
|
+
}
|
|
1081
2018
|
}
|
|
1082
2019
|
|
|
1083
2020
|
const key = String(args.key || "").trim()
|
|
1084
2021
|
if (!key) throw new Error("key is required")
|
|
1085
2022
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
await
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const keyDef = normalizeKey(key)
|
|
1093
|
-
|
|
1094
|
-
await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
|
|
1095
|
-
type: "keyDown",
|
|
1096
|
-
key: keyDef.key,
|
|
1097
|
-
code: keyDef.code,
|
|
1098
|
-
windowsVirtualKeyCode: keyDef.keyCode,
|
|
1099
|
-
nativeVirtualKeyCode: keyDef.keyCode,
|
|
1100
|
-
})
|
|
1101
|
-
await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
|
|
1102
|
-
type: "keyUp",
|
|
1103
|
-
key: keyDef.key,
|
|
1104
|
-
code: keyDef.code,
|
|
1105
|
-
windowsVirtualKeyCode: keyDef.keyCode,
|
|
1106
|
-
nativeVirtualKeyCode: keyDef.keyCode,
|
|
2023
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
2024
|
+
if (!point.ok) return buildUidPointFailureResult(session, uid, "press", point.error)
|
|
2025
|
+
const result = await chrome.tabs.sendMessage(tabId, {
|
|
2026
|
+
type: "yunti_execute_tool",
|
|
2027
|
+
tool: "yunti_press_key",
|
|
2028
|
+
arguments: { ...args, x: point.x, y: point.y },
|
|
1107
2029
|
})
|
|
2030
|
+
if (!result || typeof result !== "object" || result.pressed !== true) return result
|
|
1108
2031
|
|
|
1109
|
-
return {
|
|
2032
|
+
return {
|
|
2033
|
+
...result,
|
|
2034
|
+
pressed: true,
|
|
2035
|
+
uid,
|
|
2036
|
+
key,
|
|
2037
|
+
browserSessionId: session.browserSessionId,
|
|
2038
|
+
action: "press_key",
|
|
2039
|
+
target: { uid, method: "dom" },
|
|
2040
|
+
ok: true,
|
|
2041
|
+
recoverable: false,
|
|
2042
|
+
nextStepHint: "Key press dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended effect.",
|
|
2043
|
+
}
|
|
1110
2044
|
}
|
|
1111
2045
|
|
|
1112
2046
|
const KEY_MAP = {
|