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.
@@ -338,7 +338,7 @@ function isExtensionContextInvalidated(error) {
338
338
  }
339
339
 
340
340
  function contextInvalidatedResponse() {
341
- return { ok: false, error: "插件已重新加载,请刷新 browser page恢复连接。" }
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
- const element = mustFind(args.selector)
542
- element.scrollIntoView({ block: "center", inline: "center" })
543
- element.click()
544
- return { clicked: true, element: describeElement(element) }
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 = mustFind(args.selector)
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 { filled: true, element: describeElement(element), valueLength: value.length }
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 = mustFind(args.selector)
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
- element.value = String(args.value ?? "")
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 { selected: true, element: describeElement(element), value: element.value }
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
- Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))
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 target = findScrollableAncestor(coordinateTarget) || document.scrollingElement || document.documentElement
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")