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
package/extension/content.js
CHANGED
|
@@ -338,7 +338,7 @@ function isExtensionContextInvalidated(error) {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
function contextInvalidatedResponse() {
|
|
341
|
-
return { ok: false, error: "
|
|
341
|
+
return { ok: false, error: "插件已重新加载,正在等待扩展自动恢复连接;必要时再刷新页面。" }
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
function markExtensionContextInvalidated() {
|
|
@@ -366,6 +366,8 @@ function updateWidgetStatus(text, auth = null) {
|
|
|
366
366
|
|
|
367
367
|
async function executeTool(tool, args) {
|
|
368
368
|
switch (tool) {
|
|
369
|
+
case "yunti_observe_page":
|
|
370
|
+
return observePage(args)
|
|
369
371
|
case "yunti_get_page_snapshot":
|
|
370
372
|
return getPageSnapshot(args)
|
|
371
373
|
case "yunti_get_selected_context":
|
|
@@ -380,6 +382,8 @@ async function executeTool(tool, args) {
|
|
|
380
382
|
return clickElement(args)
|
|
381
383
|
case "yunti_click_at":
|
|
382
384
|
return clickAt(args)
|
|
385
|
+
case "yunti_hover":
|
|
386
|
+
return hoverElement(args)
|
|
383
387
|
case "yunti_fill":
|
|
384
388
|
return fillElement(args)
|
|
385
389
|
case "yunti_type_text":
|
|
@@ -397,6 +401,14 @@ async function executeTool(tool, args) {
|
|
|
397
401
|
}
|
|
398
402
|
}
|
|
399
403
|
|
|
404
|
+
function observePage(args = {}) {
|
|
405
|
+
if (!window.YuntiBrowserRuntimeObserver?.observePage) {
|
|
406
|
+
throw new Error("yunti_observe_page is unavailable because the DOM observer module was not loaded. Refresh the page and try again.")
|
|
407
|
+
}
|
|
408
|
+
window.__YUNTI_BROWSER_SESSION_ID__ = browserSessionId
|
|
409
|
+
return window.YuntiBrowserRuntimeObserver.observePage(args)
|
|
410
|
+
}
|
|
411
|
+
|
|
400
412
|
async function getPageSnapshot(args = {}) {
|
|
401
413
|
const mode = args.mode === "detailed" ? "detailed" : "light"
|
|
402
414
|
const includeElements = args.includeElements === undefined ? mode === "detailed" : Boolean(args.includeElements)
|
|
@@ -538,10 +550,13 @@ function rollbackPreviewPatch(patchId) {
|
|
|
538
550
|
}
|
|
539
551
|
|
|
540
552
|
function clickElement(args) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
553
|
+
if (args.selector) {
|
|
554
|
+
const element = mustFind(args.selector)
|
|
555
|
+
element.scrollIntoView({ block: "center", inline: "center" })
|
|
556
|
+
element.click()
|
|
557
|
+
return { clicked: true, element: describeElement(element) }
|
|
558
|
+
}
|
|
559
|
+
return clickAt(args)
|
|
545
560
|
}
|
|
546
561
|
|
|
547
562
|
function clickAt(args) {
|
|
@@ -559,12 +574,36 @@ function clickAt(args) {
|
|
|
559
574
|
}
|
|
560
575
|
}
|
|
561
576
|
|
|
577
|
+
function hoverElement(args) {
|
|
578
|
+
const element = resolveTargetElement(args)
|
|
579
|
+
if (!element) throw new Error("yunti_hover requires selector or x/y coordinates")
|
|
580
|
+
element.scrollIntoView({ block: "center", inline: "center" })
|
|
581
|
+
const rect = element.getBoundingClientRect()
|
|
582
|
+
const x = Number.isFinite(Number(args.x)) ? Number(args.x) : rect.left + rect.width / 2
|
|
583
|
+
const y = Number.isFinite(Number(args.y)) ? Number(args.y) : rect.top + rect.height / 2
|
|
584
|
+
focusElement(element)
|
|
585
|
+
dispatchHoverSequence(element, x, y)
|
|
586
|
+
return {
|
|
587
|
+
hovered: true,
|
|
588
|
+
x: Math.round(x),
|
|
589
|
+
y: Math.round(y),
|
|
590
|
+
element: describeElement(element),
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
562
594
|
function fillElement(args) {
|
|
563
|
-
const element =
|
|
595
|
+
const element = resolveTargetElement(args)
|
|
596
|
+
if (!element) throw new Error("yunti_fill requires selector or x/y coordinates")
|
|
564
597
|
const value = String(args.value ?? "")
|
|
565
598
|
element.scrollIntoView({ block: "center", inline: "center" })
|
|
566
599
|
setEditableText(element, value, { replace: true })
|
|
567
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
filled: true,
|
|
602
|
+
element: describeElement(element),
|
|
603
|
+
valueLength: value.length,
|
|
604
|
+
valueApplied: getEditableText(element) === value,
|
|
605
|
+
method: element.isContentEditable ? "contenteditable" : element instanceof HTMLSelectElement ? "select" : "dom",
|
|
606
|
+
}
|
|
568
607
|
}
|
|
569
608
|
|
|
570
609
|
function typeText(args) {
|
|
@@ -602,25 +641,69 @@ function pressKey(args) {
|
|
|
602
641
|
}
|
|
603
642
|
|
|
604
643
|
function selectElement(args) {
|
|
605
|
-
const element =
|
|
644
|
+
const element = resolveTargetElement(args)
|
|
645
|
+
if (!element) throw new Error("yunti_select requires selector or x/y coordinates")
|
|
606
646
|
if (!(element instanceof HTMLSelectElement)) throw new Error("Target is not a select element")
|
|
607
|
-
|
|
647
|
+
const value = String(args.value ?? "")
|
|
648
|
+
const text = String(args.text ?? "")
|
|
649
|
+
const matchMode = text ? "text" : "value"
|
|
650
|
+
const targetOption = text || value
|
|
651
|
+
const options = Array.from(element.options)
|
|
652
|
+
const option = options.find((item) => matchMode === "text" ? item.text.trim() === targetOption : item.value === targetOption)
|
|
653
|
+
if (!option) {
|
|
654
|
+
return {
|
|
655
|
+
selected: false,
|
|
656
|
+
element: describeElement(element),
|
|
657
|
+
value: element.value,
|
|
658
|
+
code: "OPTION_NOT_FOUND",
|
|
659
|
+
error: matchMode === "text" ? "Option text not found" : "Option value not found",
|
|
660
|
+
matchMode,
|
|
661
|
+
targetOption,
|
|
662
|
+
availableValues: options.map((item) => item.value).slice(0, 50),
|
|
663
|
+
availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
|
|
664
|
+
options: summarizeSelectOptions(options),
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (option?.disabled) {
|
|
668
|
+
return {
|
|
669
|
+
selected: false,
|
|
670
|
+
element: describeElement(element),
|
|
671
|
+
value: element.value,
|
|
672
|
+
code: "OPTION_DISABLED",
|
|
673
|
+
error: matchMode === "text" ? "Option text is disabled" : "Option value is disabled",
|
|
674
|
+
matchMode,
|
|
675
|
+
targetOption,
|
|
676
|
+
disabledValue: option.value,
|
|
677
|
+
disabledText: option.text.trim(),
|
|
678
|
+
availableValues: options.map((item) => item.value).slice(0, 50),
|
|
679
|
+
availableTexts: options.map((item) => item.text.trim()).slice(0, 50),
|
|
680
|
+
options: summarizeSelectOptions(options),
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
element.value = option.value
|
|
608
684
|
element.dispatchEvent(new Event("input", { bubbles: true }))
|
|
609
685
|
element.dispatchEvent(new Event("change", { bubbles: true }))
|
|
610
|
-
return {
|
|
686
|
+
return {
|
|
687
|
+
selected: true,
|
|
688
|
+
element: describeElement(element),
|
|
689
|
+
value: element.value,
|
|
690
|
+
text: option.text.trim(),
|
|
691
|
+
selectedIndex: element.selectedIndex,
|
|
692
|
+
}
|
|
611
693
|
}
|
|
612
694
|
|
|
613
695
|
function scrollPage(args) {
|
|
614
696
|
const deltaX = Number.isFinite(Number(args.deltaX)) ? Number(args.deltaX) : 0
|
|
615
697
|
const deltaY = Number.isFinite(Number(args.deltaY)) ? Number(args.deltaY) : 600
|
|
698
|
+
const hasCoordinateTarget = Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))
|
|
699
|
+
const coordinateX = hasCoordinateTarget ? clamp(Number(args.x), 0, Math.max(0, window.innerWidth - 1)) : undefined
|
|
700
|
+
const coordinateY = hasCoordinateTarget ? clamp(Number(args.y), 0, Math.max(0, window.innerHeight - 1)) : undefined
|
|
616
701
|
const coordinateTarget =
|
|
617
|
-
|
|
618
|
-
? document.elementFromPoint(
|
|
619
|
-
clamp(Number(args.x), 0, Math.max(0, window.innerWidth - 1)),
|
|
620
|
-
clamp(Number(args.y), 0, Math.max(0, window.innerHeight - 1))
|
|
621
|
-
)
|
|
702
|
+
hasCoordinateTarget
|
|
703
|
+
? document.elementFromPoint(coordinateX, coordinateY)
|
|
622
704
|
: null
|
|
623
|
-
const
|
|
705
|
+
const scrollableAncestor = findScrollableAncestor(coordinateTarget)
|
|
706
|
+
const target = scrollableAncestor || document.scrollingElement || document.documentElement
|
|
624
707
|
const before = getScrollPosition(target)
|
|
625
708
|
target.scrollBy({ left: deltaX, top: deltaY, behavior: "auto" })
|
|
626
709
|
const after = getScrollPosition(target)
|
|
@@ -629,6 +712,18 @@ function scrollPage(args) {
|
|
|
629
712
|
deltaX,
|
|
630
713
|
deltaY,
|
|
631
714
|
target: target === document.scrollingElement ? "document" : describeElement(target),
|
|
715
|
+
...(hasCoordinateTarget
|
|
716
|
+
? {
|
|
717
|
+
coordinateTarget: {
|
|
718
|
+
x: Math.round(coordinateX),
|
|
719
|
+
y: Math.round(coordinateY),
|
|
720
|
+
found: Boolean(coordinateTarget),
|
|
721
|
+
...(coordinateTarget ? { element: describeElement(coordinateTarget) } : {}),
|
|
722
|
+
},
|
|
723
|
+
scrollContainerFound: Boolean(scrollableAncestor),
|
|
724
|
+
...(scrollableAncestor ? {} : { coordinateScrollFallback: "document" }),
|
|
725
|
+
}
|
|
726
|
+
: {}),
|
|
632
727
|
before,
|
|
633
728
|
after,
|
|
634
729
|
}
|
|
@@ -650,6 +745,15 @@ function resolveTargetElement(args = {}) {
|
|
|
650
745
|
return null
|
|
651
746
|
}
|
|
652
747
|
|
|
748
|
+
function summarizeSelectOptions(options) {
|
|
749
|
+
return options.slice(0, 50).map((item) => ({
|
|
750
|
+
value: item.value,
|
|
751
|
+
text: item.text.trim(),
|
|
752
|
+
disabled: Boolean(item.disabled),
|
|
753
|
+
selected: Boolean(item.selected),
|
|
754
|
+
}))
|
|
755
|
+
}
|
|
756
|
+
|
|
653
757
|
function focusElement(element) {
|
|
654
758
|
if (element && typeof element.focus === "function") {
|
|
655
759
|
element.focus({ preventScroll: true })
|
|
@@ -677,7 +781,29 @@ function dispatchPointerMouseSequence(element, x, y) {
|
|
|
677
781
|
}
|
|
678
782
|
}
|
|
679
783
|
|
|
784
|
+
function dispatchHoverSequence(element, x, y) {
|
|
785
|
+
const common = {
|
|
786
|
+
bubbles: true,
|
|
787
|
+
cancelable: true,
|
|
788
|
+
composed: true,
|
|
789
|
+
clientX: x,
|
|
790
|
+
clientY: y,
|
|
791
|
+
view: window,
|
|
792
|
+
}
|
|
793
|
+
for (const type of ["pointerover", "pointerenter", "pointermove"]) {
|
|
794
|
+
try {
|
|
795
|
+
element.dispatchEvent(new PointerEvent(type, { ...common, pointerType: "mouse", isPrimary: true }))
|
|
796
|
+
} catch {
|
|
797
|
+
break
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const type of ["mouseover", "mouseenter", "mousemove"]) {
|
|
801
|
+
element.dispatchEvent(new MouseEvent(type, common))
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
680
805
|
function setEditableText(element, text, options = {}) {
|
|
806
|
+
assertEditableTarget(element)
|
|
681
807
|
if (element.isContentEditable) {
|
|
682
808
|
const next = options.replace ? text : `${element.textContent || ""}${text}`
|
|
683
809
|
element.textContent = next
|
|
@@ -692,6 +818,31 @@ function setEditableText(element, text, options = {}) {
|
|
|
692
818
|
element.dispatchEvent(new Event("change", { bubbles: true }))
|
|
693
819
|
}
|
|
694
820
|
|
|
821
|
+
function assertEditableTarget(element) {
|
|
822
|
+
if (!element) throw new Error("No editable target is focused")
|
|
823
|
+
const style = getComputedStyle(element)
|
|
824
|
+
const rect = element.getBoundingClientRect()
|
|
825
|
+
const tag = element.tagName.toLowerCase()
|
|
826
|
+
const type = String(element.type || "").toLowerCase()
|
|
827
|
+
if (element.hidden || style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0 || rect.width <= 0 || rect.height <= 0) {
|
|
828
|
+
throw new Error("Target element is hidden or has no size")
|
|
829
|
+
}
|
|
830
|
+
if (element.disabled || element.getAttribute("aria-disabled") === "true") {
|
|
831
|
+
throw new Error("Target element is disabled")
|
|
832
|
+
}
|
|
833
|
+
if (element.readOnly || element.getAttribute("aria-readonly") === "true") {
|
|
834
|
+
throw new Error("Target element is readonly")
|
|
835
|
+
}
|
|
836
|
+
if (element.isContentEditable) return
|
|
837
|
+
if ("value" in element) {
|
|
838
|
+
if (tag === "input" && ["button", "checkbox", "color", "file", "hidden", "image", "radio", "range", "reset", "submit"].includes(type)) {
|
|
839
|
+
throw new Error(`Target input type ${type} is not editable`)
|
|
840
|
+
}
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
throw new Error("Target element is not editable")
|
|
844
|
+
}
|
|
845
|
+
|
|
695
846
|
function setNativeValue(element, value) {
|
|
696
847
|
const prototype = Object.getPrototypeOf(element)
|
|
697
848
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, "value")
|