yunti-browser-runtime 0.2.0 → 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 +20 -9
- package/docs/AGENT_WORKFLOW_CONTRACT.md +11 -0
- package/docs/EXECUTION_PLAN.md +54 -20
- package/docs/EXTENSION_DISTRIBUTION.md +1 -0
- package/docs/EXTENSION_PERMISSION_STRATEGY.md +9 -6
- package/docs/EXTENSION_STORE_COPY.md +4 -1
- package/docs/INSTALL.md +11 -6
- package/docs/NEXT_MAJOR_PLAN.md +5 -5
- package/docs/PROJECT_STATUS.md +46 -22
- package/docs/RELEASE_0_2_0_CHECKLIST.md +12 -5
- package/docs/TOOL_GUIDE.md +8 -0
- package/extension/background.js +20 -0
- package/extension/cdp.js +4 -0
- package/extension/content.js +100 -18
- package/extension/manifest.json +2 -1
- package/extension/popup.js +2 -2
- package/extension/session-manager.js +104 -4
- package/extension/tool-handlers.js +196 -288
- package/mcp/bridge-hub.js +4 -4
- package/mcp/tools.js +4 -0
- package/package.json +1 -1
- package/scripts/doctor.js +2 -1
- package/skills/yunti-browser-runtime/SKILL.md +3 -1
|
@@ -534,7 +534,30 @@ export function createToolDispatcher({
|
|
|
534
534
|
async function clickByUid(tabId, session, args = {}) {
|
|
535
535
|
const uid = String(args.uid || "").trim()
|
|
536
536
|
if (uid) {
|
|
537
|
-
|
|
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
|
+
}
|
|
538
561
|
}
|
|
539
562
|
const x = Number(args.x)
|
|
540
563
|
const y = Number(args.y)
|
|
@@ -542,7 +565,26 @@ export function createToolDispatcher({
|
|
|
542
565
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
543
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.")
|
|
544
567
|
}
|
|
545
|
-
|
|
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
|
+
}
|
|
546
588
|
}
|
|
547
589
|
const selector = String(args.selector || "").trim()
|
|
548
590
|
if (!selector) {
|
|
@@ -573,7 +615,30 @@ export function createToolDispatcher({
|
|
|
573
615
|
async function hoverByUid(tabId, session, args = {}) {
|
|
574
616
|
const uid = String(args.uid || "").trim()
|
|
575
617
|
if (uid) {
|
|
576
|
-
|
|
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
|
+
}
|
|
577
642
|
}
|
|
578
643
|
const x = Number(args.x)
|
|
579
644
|
const y = Number(args.y)
|
|
@@ -581,10 +646,16 @@ export function createToolDispatcher({
|
|
|
581
646
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
582
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.")
|
|
583
648
|
}
|
|
584
|
-
await
|
|
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
|
|
585
655
|
const roundedX = Math.round(x)
|
|
586
656
|
const roundedY = Math.round(y)
|
|
587
657
|
return {
|
|
658
|
+
...result,
|
|
588
659
|
hovered: true,
|
|
589
660
|
x: roundedX,
|
|
590
661
|
y: roundedY,
|
|
@@ -594,29 +665,30 @@ export function createToolDispatcher({
|
|
|
594
665
|
target: { method: "coordinate", x: roundedX, y: roundedY },
|
|
595
666
|
ok: true,
|
|
596
667
|
recoverable: false,
|
|
597
|
-
nextStepHint: "Coordinate hover dispatched. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
|
|
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.",
|
|
598
669
|
}
|
|
599
670
|
}
|
|
600
671
|
const selector = String(args.selector || "").trim()
|
|
601
672
|
if (!selector) {
|
|
602
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.")
|
|
603
674
|
}
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
608
681
|
return {
|
|
682
|
+
...result,
|
|
609
683
|
hovered: true,
|
|
610
684
|
selector,
|
|
611
|
-
x: roundedX,
|
|
612
|
-
y: roundedY,
|
|
613
685
|
browserSessionId: session.browserSessionId,
|
|
614
686
|
method: "selector",
|
|
615
687
|
action: "hover",
|
|
616
|
-
target: { selector, method: "selector"
|
|
688
|
+
target: { selector, method: "selector" },
|
|
617
689
|
ok: true,
|
|
618
690
|
recoverable: false,
|
|
619
|
-
nextStepHint: "Selector hover dispatched. Observe again, read page state, or use a fresh uid when possible to verify menus, tooltips, or hover-only controls.",
|
|
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.",
|
|
620
692
|
}
|
|
621
693
|
}
|
|
622
694
|
|
|
@@ -626,187 +698,45 @@ export function createToolDispatcher({
|
|
|
626
698
|
const value = String(args.value)
|
|
627
699
|
if (uid) {
|
|
628
700
|
try {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
await delayCdp(100)
|
|
632
|
-
// Clear existing value
|
|
633
|
-
const elements = await chromeDebuggerSendCommand(
|
|
634
|
-
{ tabId },
|
|
635
|
-
"Runtime.evaluate",
|
|
636
|
-
{ expression: `(() => {
|
|
637
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
638
|
-
if (!el) return 'not found';
|
|
639
|
-
const tag = el.tagName.toLowerCase();
|
|
640
|
-
const rect = el.getBoundingClientRect();
|
|
641
|
-
const style = getComputedStyle(el);
|
|
642
|
-
const type = String(el.type || '').toLowerCase();
|
|
643
|
-
const disabled = Boolean(el.disabled || el.getAttribute('aria-disabled') === 'true');
|
|
644
|
-
const readOnly = Boolean(el.readOnly || el.getAttribute('aria-readonly') === 'true');
|
|
645
|
-
const hidden = Boolean(el.hidden || style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0 || rect.width <= 0 || rect.height <= 0);
|
|
646
|
-
const valueEditable = tag === 'input' || tag === 'textarea';
|
|
647
|
-
const blockedInputType = tag === 'input' && ['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(type);
|
|
648
|
-
const editable = el.isContentEditable || tag === 'select' || (valueEditable && !blockedInputType);
|
|
649
|
-
if (tag === 'select') {
|
|
650
|
-
const opts = Array.from(el.options);
|
|
651
|
-
return { tag: 'select', type, disabled, readOnly, hidden, editable, options: opts.map(o => ({ value: o.value, text: o.text.slice(0, 80) })), selectedIndex: el.selectedIndex };
|
|
652
|
-
}
|
|
653
|
-
const beforeText = el.isContentEditable ? String(el.textContent || '') : '';
|
|
654
|
-
if (el.isContentEditable) {
|
|
655
|
-
el.focus();
|
|
656
|
-
el.textContent = '';
|
|
657
|
-
} else if (valueEditable && !blockedInputType && !disabled && !readOnly && !hidden) {
|
|
658
|
-
el.focus();
|
|
659
|
-
el.select();
|
|
660
|
-
}
|
|
661
|
-
return {
|
|
662
|
-
tag,
|
|
663
|
-
type,
|
|
664
|
-
contentEditable: el.isContentEditable,
|
|
665
|
-
disabled,
|
|
666
|
-
readOnly,
|
|
667
|
-
hidden,
|
|
668
|
-
editable,
|
|
669
|
-
before: el.isContentEditable ? { textLength: beforeText.length } : undefined,
|
|
670
|
-
};
|
|
671
|
-
})()`,
|
|
672
|
-
returnByValue: true,
|
|
673
|
-
}
|
|
674
|
-
)
|
|
675
|
-
const elInfo = elements?.result?.value
|
|
676
|
-
|
|
677
|
-
if (elInfo === "not found") {
|
|
678
|
-
return buildUidFillFailureResult(session, uid, {
|
|
679
|
-
code: "ELEMENT_NOT_FOUND",
|
|
680
|
-
error: `No element found at uid ${uid} coordinates`,
|
|
681
|
-
})
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (elInfo?.hidden || elInfo?.disabled || elInfo?.readOnly || elInfo?.editable === false) {
|
|
685
|
-
return buildUidFillFailureResult(session, uid, {
|
|
686
|
-
code: "TARGET_NOT_EDITABLE",
|
|
687
|
-
error: buildFillEditabilityError(uid, elInfo),
|
|
688
|
-
element: summarizeFillTarget(elInfo),
|
|
689
|
-
})
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Handle select element
|
|
693
|
-
if (elInfo?.tag === "select") {
|
|
694
|
-
const targetOption = elInfo.options?.find(
|
|
695
|
-
o => o.value === value || o.text === value
|
|
696
|
-
)
|
|
697
|
-
if (targetOption) {
|
|
698
|
-
await chromeDebuggerSendCommand(
|
|
699
|
-
{ tabId },
|
|
700
|
-
"Runtime.evaluate",
|
|
701
|
-
{
|
|
702
|
-
expression: `(() => {
|
|
703
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
704
|
-
if (el) el.value = ${JSON.stringify(targetOption.value)};
|
|
705
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
706
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
707
|
-
})()`,
|
|
708
|
-
}
|
|
709
|
-
)
|
|
710
|
-
return {
|
|
711
|
-
filled: true,
|
|
712
|
-
uid,
|
|
713
|
-
method: "select",
|
|
714
|
-
value: targetOption.value,
|
|
715
|
-
browserSessionId: session.browserSessionId,
|
|
716
|
-
action: "fill",
|
|
717
|
-
target: { uid, method: "select" },
|
|
718
|
-
ok: true,
|
|
719
|
-
recoverable: false,
|
|
720
|
-
nextStepHint: "Select value dispatched. Observe again, read page state, or evaluate the select value to verify the intended change.",
|
|
721
|
-
}
|
|
722
|
-
}
|
|
701
|
+
const point = resolveObservedUidCenter(session, uid)
|
|
702
|
+
if (!point.ok) {
|
|
723
703
|
return buildUidFillFailureResult(session, uid, {
|
|
724
|
-
code: "
|
|
725
|
-
error:
|
|
726
|
-
availableValues: elInfo.options?.map((option) => option.value).slice(0, 50),
|
|
727
|
-
availableTexts: elInfo.options?.map((option) => option.text).slice(0, 50),
|
|
704
|
+
code: "UID_COORDINATES_UNAVAILABLE",
|
|
705
|
+
error: point.error,
|
|
728
706
|
})
|
|
729
707
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
{ type: "char", text: char, unmodifiedText: char }
|
|
738
|
-
)
|
|
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)
|
|
739
715
|
}
|
|
740
|
-
const method =
|
|
741
|
-
|
|
742
|
-
await delayCdp(50)
|
|
743
|
-
const verification = await chromeDebuggerSendCommand(
|
|
744
|
-
{ tabId },
|
|
745
|
-
"Runtime.evaluate",
|
|
746
|
-
{
|
|
747
|
-
expression: `(() => {
|
|
748
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
749
|
-
if (!el) return { found: false };
|
|
750
|
-
const tag = el.tagName.toLowerCase();
|
|
751
|
-
const type = String(el.type || '').toLowerCase();
|
|
752
|
-
const expected = ${JSON.stringify(text)};
|
|
753
|
-
const actual = el.isContentEditable
|
|
754
|
-
? String(el.textContent || '')
|
|
755
|
-
: (tag === 'input' || tag === 'textarea')
|
|
756
|
-
? String(el.value || '')
|
|
757
|
-
: undefined;
|
|
758
|
-
if (actual === undefined) {
|
|
759
|
-
return { found: true, tag, type, contentEditable: el.isContentEditable };
|
|
760
|
-
}
|
|
761
|
-
return {
|
|
762
|
-
found: true,
|
|
763
|
-
tag,
|
|
764
|
-
type,
|
|
765
|
-
contentEditable: el.isContentEditable,
|
|
766
|
-
valueMatches: actual === expected,
|
|
767
|
-
valueLength: actual.length,
|
|
768
|
-
expectedLength: expected.length,
|
|
769
|
-
};
|
|
770
|
-
})()`,
|
|
771
|
-
returnByValue: true,
|
|
772
|
-
}
|
|
773
|
-
)
|
|
774
|
-
const verificationInfo = verification?.result?.value
|
|
775
|
-
const verifiedAfter = verificationInfo?.found && Number.isFinite(Number(verificationInfo.valueLength))
|
|
776
|
-
? { textLength: Number(verificationInfo.valueLength) }
|
|
777
|
-
: undefined
|
|
778
|
-
const after = verifiedAfter || (elInfo?.contentEditable ? { textLength: text.length } : undefined)
|
|
779
|
-
if (verificationInfo?.found && verificationInfo.valueMatches === false) {
|
|
716
|
+
const method = result.method || "dom"
|
|
717
|
+
if (result.valueApplied === false) {
|
|
780
718
|
return buildUidFillFailureResult(session, uid, {
|
|
719
|
+
...result,
|
|
781
720
|
code: "VALUE_NOT_APPLIED",
|
|
782
721
|
error: `Filled value did not remain on uid ${uid}`,
|
|
783
722
|
method,
|
|
784
|
-
expectedValueLength:
|
|
785
|
-
actualValueLength: Number(verificationInfo.valueLength),
|
|
786
|
-
...(before ? { before } : {}),
|
|
787
|
-
after,
|
|
788
|
-
element: summarizeFillTarget({
|
|
789
|
-
tag: verificationInfo.tag,
|
|
790
|
-
type: verificationInfo.type,
|
|
791
|
-
contentEditable: verificationInfo.contentEditable,
|
|
792
|
-
}),
|
|
723
|
+
expectedValueLength: value.length,
|
|
793
724
|
})
|
|
794
725
|
}
|
|
795
726
|
return {
|
|
727
|
+
...result,
|
|
796
728
|
filled: true,
|
|
797
729
|
uid,
|
|
798
730
|
method,
|
|
799
|
-
value
|
|
800
|
-
...(before ? { before } : {}),
|
|
801
|
-
...(after ? { after } : {}),
|
|
731
|
+
value,
|
|
802
732
|
browserSessionId: session.browserSessionId,
|
|
803
733
|
action: "fill",
|
|
804
734
|
target: { uid, method },
|
|
805
735
|
ok: true,
|
|
806
736
|
recoverable: false,
|
|
807
|
-
nextStepHint:
|
|
737
|
+
nextStepHint: method === "contenteditable"
|
|
808
738
|
? "Contenteditable fill dispatched. Observe again, read page text, or evaluate textContent to verify the intended change."
|
|
809
|
-
: "Fill dispatched. Observe again, read page state, or evaluate the field value 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.",
|
|
810
740
|
}
|
|
811
741
|
} catch (error) {
|
|
812
742
|
return buildUidFillFailureResult(session, uid, {
|
|
@@ -1208,65 +1138,27 @@ export function createToolDispatcher({
|
|
|
1208
1138
|
if (uid) {
|
|
1209
1139
|
const matchMode = text ? "text" : "value"
|
|
1210
1140
|
const targetOption = text || value
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
const option = options.find((item) => {
|
|
1225
|
-
if (${JSON.stringify(matchMode)} === "text") return item.text.trim() === ${JSON.stringify(targetOption)};
|
|
1226
|
-
return item.value === ${JSON.stringify(targetOption)};
|
|
1227
|
-
});
|
|
1228
|
-
if (!option) {
|
|
1229
|
-
return {
|
|
1230
|
-
ok: false,
|
|
1231
|
-
code: "OPTION_NOT_FOUND",
|
|
1232
|
-
error: ${JSON.stringify(matchMode)} === "text" ? "Option text not found" : "Option value not found",
|
|
1233
|
-
availableValues: options.map((item) => item.value).slice(0, 50),
|
|
1234
|
-
availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
if (option.disabled) {
|
|
1238
|
-
return {
|
|
1239
|
-
ok: false,
|
|
1240
|
-
code: "OPTION_DISABLED",
|
|
1241
|
-
error: ${JSON.stringify(matchMode)} === "text" ? "Option text is disabled" : "Option value is disabled",
|
|
1242
|
-
disabledValue: option.value,
|
|
1243
|
-
disabledText: option.text.trim(),
|
|
1244
|
-
availableValues: options.map((item) => item.value).slice(0, 50),
|
|
1245
|
-
availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
el.value = option.value;
|
|
1249
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1250
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1251
|
-
return {
|
|
1252
|
-
ok: true,
|
|
1253
|
-
value: option.value,
|
|
1254
|
-
selectedIndex: el.selectedIndex,
|
|
1255
|
-
optionText: option.text.slice(0, 80),
|
|
1256
|
-
};
|
|
1257
|
-
})()`,
|
|
1258
|
-
returnByValue: true,
|
|
1259
|
-
}
|
|
1260
|
-
)
|
|
1261
|
-
const selectResult = result?.result?.value
|
|
1262
|
-
if (!selectResult?.ok) {
|
|
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) {
|
|
1263
1154
|
return buildUidSelectFailureResult(session, uid, matchMode, targetOption, selectResult)
|
|
1264
1155
|
}
|
|
1265
1156
|
return {
|
|
1157
|
+
...selectResult,
|
|
1266
1158
|
selected: true,
|
|
1267
1159
|
uid,
|
|
1268
1160
|
value: selectResult.value,
|
|
1269
|
-
text: selectResult.
|
|
1161
|
+
text: selectResult.text,
|
|
1270
1162
|
selectedIndex: selectResult.selectedIndex,
|
|
1271
1163
|
browserSessionId: session.browserSessionId,
|
|
1272
1164
|
action: "select",
|
|
@@ -1274,8 +1166,8 @@ export function createToolDispatcher({
|
|
|
1274
1166
|
ok: true,
|
|
1275
1167
|
recoverable: false,
|
|
1276
1168
|
nextStepHint: matchMode === "text"
|
|
1277
|
-
? "Uid select dispatched by visible option text. Observe again, read page state, or evaluate the select value to verify the intended change."
|
|
1278
|
-
: "Uid select dispatched by option value. Observe again, read page state, or evaluate the select value to verify the intended change.",
|
|
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.",
|
|
1279
1171
|
}
|
|
1280
1172
|
}
|
|
1281
1173
|
|
|
@@ -1529,39 +1421,72 @@ export function createToolDispatcher({
|
|
|
1529
1421
|
}
|
|
1530
1422
|
|
|
1531
1423
|
async function resolveUidCenter(tabId, session, uid) {
|
|
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) {
|
|
1532
1430
|
const uidState = pageUidStore.get(session.browserSessionId)
|
|
1533
1431
|
const uidMap = uidState?.uidMap
|
|
1534
1432
|
if (!uidMap || !uidMap[uid]) {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
+
}
|
|
1538
1437
|
}
|
|
1539
1438
|
const el = uidMap[uid]
|
|
1540
1439
|
|
|
1541
1440
|
if (el.rect && el.rect.width > 0 && el.rect.height > 0) {
|
|
1542
1441
|
return {
|
|
1442
|
+
ok: true,
|
|
1543
1443
|
x: el.rect.x + el.rect.width / 2,
|
|
1544
1444
|
y: el.rect.y + el.rect.height / 2,
|
|
1545
1445
|
}
|
|
1546
1446
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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.`,
|
|
1562
1489
|
}
|
|
1563
|
-
|
|
1564
|
-
throw new Error(`Cannot resolve coordinates for uid ${uid}. Element may be off-screen, hidden, or stale. Run yunti_observe_page again before retrying.`)
|
|
1565
1490
|
}
|
|
1566
1491
|
|
|
1567
1492
|
async function mouseClick(tabId, x, y, clickCount = 1) {
|
|
@@ -2034,31 +1959,27 @@ export function createToolDispatcher({
|
|
|
2034
1959
|
const text = String(args.text || "")
|
|
2035
1960
|
if (!text) throw new Error("text is required")
|
|
2036
1961
|
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
await
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
{ tabId },
|
|
2046
|
-
"Input.dispatchKeyEvent",
|
|
2047
|
-
{ type: "char", text: char, unmodifiedText: char }
|
|
2048
|
-
)
|
|
2049
|
-
}
|
|
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
|
|
2050
1970
|
|
|
2051
1971
|
return {
|
|
1972
|
+
...result,
|
|
2052
1973
|
typed: true,
|
|
2053
1974
|
uid,
|
|
2054
1975
|
text,
|
|
2055
|
-
method:
|
|
1976
|
+
method: result.method || "dom",
|
|
2056
1977
|
browserSessionId: session.browserSessionId,
|
|
2057
1978
|
action: "type_text",
|
|
2058
|
-
target: { uid, method:
|
|
1979
|
+
target: { uid, method: result.method || "dom" },
|
|
2059
1980
|
ok: true,
|
|
2060
1981
|
recoverable: false,
|
|
2061
|
-
nextStepHint: "Type text dispatched. Observe again, read page state, or evaluate the field value to verify the intended change.",
|
|
1982
|
+
nextStepHint: "Type text dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended change.",
|
|
2062
1983
|
}
|
|
2063
1984
|
}
|
|
2064
1985
|
|
|
@@ -2099,39 +2020,26 @@ export function createToolDispatcher({
|
|
|
2099
2020
|
const key = String(args.key || "").trim()
|
|
2100
2021
|
if (!key) throw new Error("key is required")
|
|
2101
2022
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
await
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
const keyDef = normalizeKey(key)
|
|
2109
|
-
|
|
2110
|
-
await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
|
|
2111
|
-
type: "keyDown",
|
|
2112
|
-
key: keyDef.key,
|
|
2113
|
-
code: keyDef.code,
|
|
2114
|
-
windowsVirtualKeyCode: keyDef.keyCode,
|
|
2115
|
-
nativeVirtualKeyCode: keyDef.keyCode,
|
|
2116
|
-
})
|
|
2117
|
-
await chromeDebuggerSendCommand({ tabId }, "Input.dispatchKeyEvent", {
|
|
2118
|
-
type: "keyUp",
|
|
2119
|
-
key: keyDef.key,
|
|
2120
|
-
code: keyDef.code,
|
|
2121
|
-
windowsVirtualKeyCode: keyDef.keyCode,
|
|
2122
|
-
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 },
|
|
2123
2029
|
})
|
|
2030
|
+
if (!result || typeof result !== "object" || result.pressed !== true) return result
|
|
2124
2031
|
|
|
2125
2032
|
return {
|
|
2033
|
+
...result,
|
|
2126
2034
|
pressed: true,
|
|
2127
2035
|
uid,
|
|
2128
2036
|
key,
|
|
2129
2037
|
browserSessionId: session.browserSessionId,
|
|
2130
2038
|
action: "press_key",
|
|
2131
|
-
target: { uid, method: "
|
|
2039
|
+
target: { uid, method: "dom" },
|
|
2132
2040
|
ok: true,
|
|
2133
2041
|
recoverable: false,
|
|
2134
|
-
nextStepHint: "Key press dispatched. Observe again, read page state, or evaluate the field value to verify the intended effect.",
|
|
2042
|
+
nextStepHint: "Key press dispatched without attaching Chrome debugger. Observe again, read page state, or evaluate the field value to verify the intended effect.",
|
|
2135
2043
|
}
|
|
2136
2044
|
}
|
|
2137
2045
|
|
package/mcp/bridge-hub.js
CHANGED
|
@@ -810,11 +810,11 @@ export class BridgeHub {
|
|
|
810
810
|
}),
|
|
811
811
|
guidance: {
|
|
812
812
|
noSessions:
|
|
813
|
-
"Keep this bridge running, load or reload the extension, open an http/https page
|
|
813
|
+
"Keep this bridge running, load or reload the extension, and open an http/https page. Yunti will auto-register accessible tabs; refresh the target page only if it remains invisible.",
|
|
814
814
|
staleSession:
|
|
815
|
-
"
|
|
815
|
+
"Call yunti_list_browser_targets to refresh live routes; Yunti will try to auto-register accessible tabs before listing. Refresh the page only as a fallback.",
|
|
816
816
|
versionMismatch:
|
|
817
|
-
"Reload the unpacked extension from the current package directory
|
|
817
|
+
"Reload the unpacked extension from the current package directory. It will auto-register accessible open http/https pages; refresh only if a page remains invisible.",
|
|
818
818
|
cancellation:
|
|
819
819
|
"Cancel only clears runtime pending/queued requests; it does not undo browser-side effects that already happened.",
|
|
820
820
|
},
|
|
@@ -829,7 +829,7 @@ function consoleWarnings(sessions, { expectedExtensionVersion = "" } = {}) {
|
|
|
829
829
|
code: "NO_CONNECTED_PAGES",
|
|
830
830
|
severity: "warning",
|
|
831
831
|
message:
|
|
832
|
-
"No browser pages are connected. Load or reload the extension
|
|
832
|
+
"No browser pages are connected. Load or reload the extension and open an http/https page; Yunti will auto-register accessible pages. Refresh only if the page remains invisible.",
|
|
833
833
|
})
|
|
834
834
|
return warnings
|
|
835
835
|
}
|