@coolkiller007/my-page-agent 0.1.0
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/LICENSE +22 -0
- package/README.md +89 -0
- package/UPSTREAM.md +97 -0
- package/dist/index.d.ts +873 -0
- package/dist/index.js +4436 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4436 @@
|
|
|
1
|
+
(function() { try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); elementStyle.appendChild(document.createTextNode("._wrapper_ur79v_1 {\n position: fixed;\n inset: 0;\n z-index: 2147483641; /* 确保在所有元素之上,除了 Agent UI */\n cursor: wait;\n overflow: hidden;\n\n display: none;\n}\n\n._wrapper_ur79v_1._visible_ur79v_11 {\n display: block;\n}\n/* AI 光标样式 */\n._cursor_1dgwb_2 {\n position: absolute;\n width: var(--cursor-size, 75px);\n height: var(--cursor-size, 75px);\n pointer-events: none;\n z-index: 10000;\n}\n\n._cursorBorder_1dgwb_10 {\n position: absolute;\n width: 100%;\n height: 100%;\n background: linear-gradient(45deg, rgb(57, 182, 255), rgb(189, 69, 251));\n mask-image: url(\"data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%20fill='none'%3e%3cg%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='none'%20stroke='%23000000'%20stroke-width='6'%20stroke-miterlimit='10'%20style='stroke:%20light-dark(rgb(0,%200,%200),%20rgb(255,%20255,%20255));'/%3e%3c/g%3e%3c/svg%3e\");\n mask-size: 100% 100%;\n mask-repeat: no-repeat;\n\n transform-origin: center;\n transform: rotate(-135deg) scale(1.2);\n margin-left: -10px;\n margin-top: -18px;\n}\n\n._cursorFilling_1dgwb_25 {\n position: absolute;\n width: 100%;\n height: 100%;\n background: url(\"data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3e%3cdefs%3e%3c/defs%3e%3cg%20xmlns='http://www.w3.org/2000/svg'%20style='filter:%20drop-shadow(light-dark(rgba(0,%200,%200,%200.4),%20rgba(237,%20237,%20237,%200.4))%203px%204px%204px);'%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='%23ffffff'%20stroke='none'%20style='fill:%20%23ffffff;'/%3e%3c/g%3e%3c/svg%3e\");\n background-size: 100% 100%;\n background-repeat: no-repeat;\n\n transform-origin: center;\n transform: rotate(-135deg) scale(1.2);\n margin-left: -10px;\n margin-top: -18px;\n}\n\n._cursorRipple_1dgwb_39 {\n position: absolute;\n width: 100%;\n height: 100%;\n pointer-events: none;\n margin-left: -50%;\n margin-top: -50%;\n\n &::after {\n content: '';\n opacity: 0;\n position: absolute;\n inset: 0;\n border: 4px solid rgba(57, 182, 255, 1);\n border-radius: 50%;\n }\n}\n\n._cursor_1dgwb_2._clicking_1dgwb_57 ._cursorRipple_1dgwb_39::after {\n animation: _cursor-ripple_1dgwb_1 300ms ease-out forwards;\n}\n\n@keyframes _cursor-ripple_1dgwb_1 {\n 0% {\n transform: scale(0);\n opacity: 1;\n }\n 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n._wrapper_1tu05_1 {\n position: fixed;\n bottom: 100px;\n left: 50%;\n transform: translateX(-50%) translateY(20px);\n opacity: 0;\n z-index: 2147483642; /* 比 SimulatorMask 高一层 */\n box-sizing: border-box;\n\n overflow: visible;\n\n * {\n box-sizing: border-box;\n }\n\n --width: 360px;\n --height: 40px;\n --border-radius: 12px;\n\n --side-space: 12px; /* 控制栏两侧的间距 */\n --history-width: calc(var(--width) - var(--side-space) * 2);\n\n --color-1: rgb(57, 182, 255);\n --color-2: rgb(189, 69, 251);\n --color-3: rgb(255, 87, 51);\n --color-4: rgb(255, 214, 0);\n\n width: var(--width);\n height: var(--height);\n\n transition: all 0.3s ease-in-out;\n\n /* 响应式设计 */\n @media (max-width: 480px) {\n width: calc(100vw - 40px);\n --width: calc(100vw - 40px);\n }\n\n ._background_1tu05_39 {\n position: absolute;\n inset: -2px -8px;\n border-radius: calc(var(--border-radius) + 4px);\n filter: blur(16px);\n overflow: hidden;\n /* mix-blend-mode: lighten; */\n /* display: none; */\n\n &::before {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n /* left: -100%; */\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-1),\n var(--color-2),\n var(--color-1)\n );\n animation: _mask-running_1tu05_1 2s linear infinite;\n }\n &::after {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-2),\n var(--color-1),\n var(--color-2)\n );\n animation: _mask-running_1tu05_1 2s linear infinite;\n animation-delay: 1s;\n }\n }\n}\n\n@keyframes _mask-running_1tu05_1 {\n from {\n transform: translateX(-100%);\n }\n to {\n transform: translateX(100%);\n }\n}\n\n/* 控制栏 */\n._header_1tu05_99 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n user-select: none;\n\n position: absolute;\n inset: 0;\n\n cursor: pointer;\n flex-shrink: 0; /* 防止 header 被压缩 */\n\n background: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(10px);\n border-radius: var(--border-radius);\n background-clip: padding-box;\n\n box-shadow:\n 0 0 0px 2px rgba(255, 255, 255, 0.4),\n 0 0 5px 1px rgba(255, 255, 255, 0.3);\n\n ._statusSection_1tu05_121 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-height: 24px; /* 确保垂直居中 */\n\n ._indicator_1tu05_128 {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.5);\n flex-shrink: 0;\n animation: none; /* 默认无动画 */\n\n /* 运行状态 - 有动画 */\n &._thinking_1tu05_137 {\n background: rgb(57, 182, 255);\n animation: _pulse_1tu05_1 0.8s ease-in-out infinite;\n }\n\n &._tool_executing_1tu05_142 {\n background: rgb(189, 69, 251);\n animation: _pulse_1tu05_1 0.6s ease-in-out infinite;\n }\n\n &._retry_1tu05_147 {\n background: rgb(255, 214, 0);\n animation: _retryPulse_1tu05_1 1s ease-in-out infinite;\n }\n\n /* 静止状态 - 无动画 */\n &._completed_1tu05_153,\n &._input_1tu05_154,\n &._output_1tu05_155 {\n background: rgb(34, 197, 94);\n animation: none;\n }\n\n &._error_1tu05_160 {\n background: rgb(239, 68, 68);\n animation: none;\n }\n }\n\n ._statusText_1tu05_166 {\n color: white;\n font-size: 12px;\n line-height: 1;\n font-weight: 500;\n transition: all 0.3s ease-in-out;\n position: relative;\n overflow: hidden;\n display: flex;\n align-items: center;\n min-height: 24px; /* 确保垂直居中 */\n\n &._fadeOut_1tu05_178 {\n animation: _statusTextFadeOut_1tu05_1 0.3s ease forwards;\n }\n\n &._fadeIn_1tu05_182 {\n animation: _statusTextFadeIn_1tu05_1 0.3s ease forwards;\n }\n }\n }\n\n ._controls_1tu05_188 {\n display: flex;\n align-items: center;\n gap: 4px;\n\n ._controlButton_1tu05_193 {\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 4px;\n background: rgba(255, 255, 255, 0.1);\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n line-height: 1;\n\n &:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n }\n\n ._stopButton_1tu05_212 {\n background: rgba(239, 68, 68, 0.2);\n color: rgb(255, 41, 41);\n font-weight: 600;\n\n &:hover {\n background: rgba(239, 68, 68, 0.3);\n }\n }\n }\n}\n\n@keyframes _statusTextFadeIn_1tu05_1 {\n 0% {\n opacity: 0;\n transform: translateY(5px);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes _statusTextFadeOut_1tu05_1 {\n 0% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-5px);\n }\n}\n\n._historySectionWrapper_1tu05_246 {\n position: absolute;\n width: var(--history-width);\n bottom: var(--height);\n left: var(--side-space);\n z-index: -2;\n\n padding-top: 0px;\n visibility: collapse;\n overflow: hidden;\n\n transition: all 0.2s;\n\n background: rgba(2, 0, 20, 0.5);\n /* background: rgba(186, 186, 186, 0.2); */\n backdrop-filter: blur(10px);\n\n text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);\n\n border-top-left-radius: calc(var(--border-radius) + 4px);\n border-top-right-radius: calc(var(--border-radius) + 4px);\n\n /* border: 2px solid rgba(255, 255, 255, 0.8); */\n border: 2px solid rgba(255, 255, 255, 0.4);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n\n /* @media (prefers-color-scheme: dark) {\n box-shadow:\n 0 8px 32px 0 rgba(0, 0, 0, 0.85),\n 0 2px 12px 0 rgba(57, 182, 255, 0.1);\n } */\n\n ._expanded_1tu05_278 & {\n padding-top: 8px;\n visibility: visible;\n }\n\n ._historySection_1tu05_246 {\n position: relative;\n overflow-y: auto;\n overscroll-behavior: contain;\n scrollbar-width: none;\n max-height: 0;\n padding-inline: 8px;\n\n transition: max-height 0.2s;\n\n ._expanded_1tu05_278 & {\n max-height: min(500px, calc(100vh - 200px - var(--height)));\n }\n\n ._historyItem_1tu05_297 {\n /* backdrop-filter: blur(10px); */\n padding: 8px 10px;\n margin-bottom: 6px;\n background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));\n border-radius: 8px;\n border-left: 2px solid rgba(57, 182, 255, 0.5);\n font-size: 12px;\n color: white;\n /* color: black; */\n line-height: 1.3;\n position: relative;\n overflow: hidden;\n\n /* 微妙的内阴影 */\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.1),\n 0 1px 3px rgba(0, 0, 0, 0.1);\n\n &::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);\n }\n\n &:hover {\n background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));\n /* transform: translateY(-1px); */\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.15),\n 0 2px 4px rgba(0, 0, 0, 0.15);\n }\n\n &:last-child {\n margin-bottom: 10px;\n }\n\n &._completed_1tu05_153,\n &._input_1tu05_154,\n &._output_1tu05_155 {\n border-left-color: rgb(34, 197, 94);\n background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));\n }\n\n &._error_1tu05_160 {\n border-left-color: rgb(239, 68, 68);\n background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05));\n }\n\n &._retry_1tu05_147 {\n border-left-color: rgb(255, 214, 0);\n background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));\n }\n\n &._observation_1tu05_355 {\n border-left-color: rgb(147, 51, 234);\n background: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));\n }\n\n &._question_1tu05_360 {\n border-left-color: rgb(255, 159, 67);\n background: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));\n }\n\n /* 突出显示 done 成功结果 */\n &._doneSuccess_1tu05_366 {\n background: linear-gradient(\n 135deg,\n rgba(34, 197, 94, 0.25),\n rgba(34, 197, 94, 0.15),\n rgba(34, 197, 94, 0.08)\n );\n border: none;\n border-left: 4px solid rgb(34, 197, 94);\n box-shadow:\n 0 4px 12px rgba(34, 197, 94, 0.3),\n inset 0 1px 0 rgba(255, 255, 255, 0.2),\n 0 0 20px rgba(34, 197, 94, 0.1);\n font-weight: 600;\n color: rgb(220, 252, 231);\n padding: 10px 12px;\n margin-bottom: 8px;\n border-radius: 8px;\n position: relative;\n overflow: hidden;\n\n &::before {\n background: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.4), transparent);\n }\n\n &::after {\n content: '';\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);\n animation: _shimmer_1tu05_1 2s ease-in-out infinite;\n }\n\n ._historyContent_1tu05_402 {\n ._statusIcon_1tu05_403 {\n font-size: 16px;\n animation: _celebrate_1tu05_1 0.8s ease-in-out;\n filter: drop-shadow(0 2px 4px rgba(34, 197, 94, 0.5));\n }\n }\n }\n\n /* 突出显示 done 失败结果 */\n &._doneError_1tu05_412 {\n background: linear-gradient(\n 135deg,\n rgba(239, 68, 68, 0.25),\n rgba(239, 68, 68, 0.15),\n rgba(239, 68, 68, 0.08)\n );\n border: none;\n border-left: 4px solid rgb(239, 68, 68);\n box-shadow:\n 0 4px 12px rgba(239, 68, 68, 0.3),\n inset 0 1px 0 rgba(255, 255, 255, 0.2),\n 0 0 20px rgba(239, 68, 68, 0.1);\n font-weight: 600;\n color: rgb(254, 226, 226);\n padding: 10px 12px;\n margin-bottom: 8px;\n border-radius: 8px;\n position: relative;\n overflow: hidden;\n\n &::before {\n background: linear-gradient(90deg, transparent, rgba(239, 68, 68, 0.4), transparent);\n }\n\n ._historyContent_1tu05_402 {\n ._statusIcon_1tu05_403 {\n font-size: 16px;\n filter: drop-shadow(0 2px 4px rgba(239, 68, 68, 0.5));\n }\n }\n }\n\n ._historyContent_1tu05_402 {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n\n word-break: break-all;\n white-space: pre-wrap;\n\n /* overflow-x: auto; */\n\n ._statusIcon_1tu05_403 {\n font-size: 12px;\n flex-shrink: 0;\n line-height: 1;\n transition: all 0.3s ease;\n }\n\n ._reflectionLines_1tu05_462 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n }\n\n ._historyMeta_1tu05_469 {\n font-size: 10px;\n color: rgba(255, 255, 255, 0.6);\n /* color: rgb(61, 61, 61); */\n margin-top: 8px;\n line-height: 1;\n }\n }\n }\n}\n\n/* 动画关键帧 - 更快的闪烁 */\n@keyframes _pulse_1tu05_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.4;\n transform: scale(1.3);\n }\n}\n\n/* 重试动画 - 旋转脉冲 */\n@keyframes _retryPulse_1tu05_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n }\n 25% {\n opacity: 0.6;\n transform: scale(1.2) rotate(90deg);\n }\n 50% {\n opacity: 0.8;\n transform: scale(1.1) rotate(180deg);\n }\n 75% {\n opacity: 0.6;\n transform: scale(1.2) rotate(270deg);\n }\n}\n\n/* 庆祝动画 */\n@keyframes _celebrate_1tu05_1 {\n 0%,\n 100% {\n transform: scale(1);\n }\n 25% {\n transform: scale(1.2) rotate(-5deg);\n }\n 75% {\n transform: scale(1.2) rotate(5deg);\n }\n}\n\n/* done 卡片的光泽效果 */\n@keyframes _shimmer_1tu05_1 {\n 0% {\n left: -100%;\n }\n 100% {\n left: 100%;\n }\n}\n\n/* 输入区域样式 */\n._inputSectionWrapper_1tu05_539 {\n position: absolute;\n width: var(--history-width);\n top: var(--height);\n left: var(--side-space);\n z-index: -1;\n\n visibility: visible;\n overflow: hidden;\n\n height: 48px;\n\n transition: all 0.2s;\n\n background: rgba(186, 186, 186, 0.2);\n backdrop-filter: blur(10px);\n\n border-bottom-left-radius: calc(var(--border-radius) + 4px);\n border-bottom-right-radius: calc(var(--border-radius) + 4px);\n\n border: 2px solid rgba(255, 255, 255, 0.3);\n box-shadow: 0 1px 16px rgba(0, 0, 0, 0.4);\n\n &._hidden_1tu05_562 {\n visibility: collapse;\n height: 0;\n }\n\n ._inputSection_1tu05_539 {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 8px 8px;\n\n ._taskInput_1tu05_573 {\n flex: 1;\n background: rgba(255, 255, 255, 0.4);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 10px;\n padding-inline: 10px;\n color: rgb(20, 20, 20);\n font-size: 12px;\n height: 28px;\n line-height: 1;\n outline: none;\n transition: all 0.2s ease;\n\n /* text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); */\n\n /* border-color: rgba(57, 182, 255, 0.3); */\n\n &::placeholder {\n color: rgb(53, 53, 53);\n }\n\n &:focus {\n background: rgba(255, 255, 255, 0.8);\n border-color: rgba(57, 182, 255, 0.6);\n box-shadow: 0 0 0 2px rgba(57, 182, 255, 0.2);\n }\n }\n }\n}\n/*$vite$:1*/")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); }})();
|
|
2
|
+
import { Motion } from "ai-motion";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __esmMin = (fn, res, err) => () => {
|
|
8
|
+
if (err) throw err[0];
|
|
9
|
+
try {
|
|
10
|
+
return fn && (res = fn(fn = 0)), res;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw err = [e], e;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var __exportAll = (all, no_symbols) => {
|
|
16
|
+
let target = {};
|
|
17
|
+
for (var name in all) __defProp(target, name, {
|
|
18
|
+
get: all[name],
|
|
19
|
+
enumerable: true
|
|
20
|
+
});
|
|
21
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
22
|
+
return target;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/browser/utils/index.ts
|
|
26
|
+
function isHTMLElement(el) {
|
|
27
|
+
return !!el && el.nodeType === 1;
|
|
28
|
+
}
|
|
29
|
+
function isInputElement(el) {
|
|
30
|
+
return el?.nodeType === 1 && el.tagName === "INPUT";
|
|
31
|
+
}
|
|
32
|
+
function isTextAreaElement(el) {
|
|
33
|
+
return el?.nodeType === 1 && el.tagName === "TEXTAREA";
|
|
34
|
+
}
|
|
35
|
+
function isSelectElement(el) {
|
|
36
|
+
return el?.nodeType === 1 && el.tagName === "SELECT";
|
|
37
|
+
}
|
|
38
|
+
function isAnchorElement(el) {
|
|
39
|
+
return el?.nodeType === 1 && el.tagName === "A";
|
|
40
|
+
}
|
|
41
|
+
/** Iframe offset for translating element coordinates to top-frame viewport. */
|
|
42
|
+
function getIframeOffset(element) {
|
|
43
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
44
|
+
if (!frame) return {
|
|
45
|
+
x: 0,
|
|
46
|
+
y: 0
|
|
47
|
+
};
|
|
48
|
+
const rect = frame.getBoundingClientRect();
|
|
49
|
+
return {
|
|
50
|
+
x: rect.left,
|
|
51
|
+
y: rect.top
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get native value setter from the element's own prototype (iframe-safe).
|
|
56
|
+
* @note for React
|
|
57
|
+
*/
|
|
58
|
+
function getNativeValueSetter(element) {
|
|
59
|
+
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "value").set;
|
|
60
|
+
}
|
|
61
|
+
async function waitFor$1(seconds) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Move the visual pointer to a position within an element.
|
|
66
|
+
* @param x - x coordinate in the element's document viewport
|
|
67
|
+
* @param y - y coordinate in the element's document viewport
|
|
68
|
+
*/
|
|
69
|
+
async function movePointerToElement(element, x, y) {
|
|
70
|
+
const offset = getIframeOffset(element);
|
|
71
|
+
window.dispatchEvent(new CustomEvent("PageAgent::MovePointerTo", { detail: {
|
|
72
|
+
x: x + offset.x,
|
|
73
|
+
y: y + offset.y
|
|
74
|
+
} }));
|
|
75
|
+
await waitFor$1(.3);
|
|
76
|
+
}
|
|
77
|
+
async function clickPointer() {
|
|
78
|
+
window.dispatchEvent(new CustomEvent("PageAgent::ClickPointer"));
|
|
79
|
+
}
|
|
80
|
+
async function enablePassThrough() {
|
|
81
|
+
window.dispatchEvent(new CustomEvent("PageAgent::EnablePassThrough"));
|
|
82
|
+
}
|
|
83
|
+
async function disablePassThrough() {
|
|
84
|
+
window.dispatchEvent(new CustomEvent("PageAgent::DisablePassThrough"));
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/browser/actions.ts
|
|
88
|
+
/**
|
|
89
|
+
* Get the HTMLElement by index from a selectorMap.
|
|
90
|
+
* @private Internal method, subject to change at any time.
|
|
91
|
+
*/
|
|
92
|
+
function getElementByIndex(selectorMap, index) {
|
|
93
|
+
const interactiveNode = selectorMap.get(index);
|
|
94
|
+
if (!interactiveNode) throw new Error(`No interactive element found at index ${index}`);
|
|
95
|
+
const element = interactiveNode.ref;
|
|
96
|
+
if (!element) throw new Error(`Element at index ${index} does not have a reference`);
|
|
97
|
+
if (!isHTMLElement(element)) throw new Error(`Element at index ${index} is not an HTMLElement`);
|
|
98
|
+
return element;
|
|
99
|
+
}
|
|
100
|
+
var lastClickedElement = null;
|
|
101
|
+
function blurLastClickedElement() {
|
|
102
|
+
if (lastClickedElement) {
|
|
103
|
+
lastClickedElement.dispatchEvent(new PointerEvent("pointerout", { bubbles: true }));
|
|
104
|
+
lastClickedElement.dispatchEvent(new PointerEvent("pointerleave", { bubbles: false }));
|
|
105
|
+
lastClickedElement.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
|
|
106
|
+
lastClickedElement.dispatchEvent(new MouseEvent("mouseleave", { bubbles: false }));
|
|
107
|
+
lastClickedElement.blur();
|
|
108
|
+
lastClickedElement = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Simulate a full click following W3C Pointer Events + UI Events spec order:
|
|
113
|
+
* pointerover/enter → mouseover/enter → pointerdown → mousedown → [focus] →
|
|
114
|
+
* pointerup → mouseup → click
|
|
115
|
+
*
|
|
116
|
+
* @private Internal method, subject to change at any time.
|
|
117
|
+
*/
|
|
118
|
+
async function clickElement(element) {
|
|
119
|
+
blurLastClickedElement();
|
|
120
|
+
lastClickedElement = element;
|
|
121
|
+
await scrollIntoViewIfNeeded(element);
|
|
122
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
123
|
+
if (frame) await scrollIntoViewIfNeeded(frame);
|
|
124
|
+
const rect = element.getBoundingClientRect();
|
|
125
|
+
const x = rect.left + rect.width / 2;
|
|
126
|
+
const y = rect.top + rect.height / 2;
|
|
127
|
+
await movePointerToElement(element, x, y);
|
|
128
|
+
await clickPointer();
|
|
129
|
+
await waitFor$1(.1);
|
|
130
|
+
const doc = element.ownerDocument;
|
|
131
|
+
await enablePassThrough();
|
|
132
|
+
const hitTarget = doc.elementFromPoint(x, y);
|
|
133
|
+
await disablePassThrough();
|
|
134
|
+
const target = hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element;
|
|
135
|
+
const pointerOpts = {
|
|
136
|
+
bubbles: true,
|
|
137
|
+
cancelable: true,
|
|
138
|
+
clientX: x,
|
|
139
|
+
clientY: y,
|
|
140
|
+
pointerType: "mouse"
|
|
141
|
+
};
|
|
142
|
+
const mouseOpts = {
|
|
143
|
+
bubbles: true,
|
|
144
|
+
cancelable: true,
|
|
145
|
+
clientX: x,
|
|
146
|
+
clientY: y,
|
|
147
|
+
button: 0
|
|
148
|
+
};
|
|
149
|
+
target.dispatchEvent(new PointerEvent("pointerover", pointerOpts));
|
|
150
|
+
target.dispatchEvent(new PointerEvent("pointerenter", {
|
|
151
|
+
...pointerOpts,
|
|
152
|
+
bubbles: false
|
|
153
|
+
}));
|
|
154
|
+
target.dispatchEvent(new MouseEvent("mouseover", mouseOpts));
|
|
155
|
+
target.dispatchEvent(new MouseEvent("mouseenter", {
|
|
156
|
+
...mouseOpts,
|
|
157
|
+
bubbles: false
|
|
158
|
+
}));
|
|
159
|
+
target.dispatchEvent(new PointerEvent("pointerdown", pointerOpts));
|
|
160
|
+
target.dispatchEvent(new MouseEvent("mousedown", mouseOpts));
|
|
161
|
+
element.focus({ preventScroll: true });
|
|
162
|
+
target.dispatchEvent(new PointerEvent("pointerup", pointerOpts));
|
|
163
|
+
target.dispatchEvent(new MouseEvent("mouseup", mouseOpts));
|
|
164
|
+
target.click();
|
|
165
|
+
await waitFor$1(.2);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @private Internal method, subject to change at any time.
|
|
169
|
+
*/
|
|
170
|
+
async function inputTextElement(element, text) {
|
|
171
|
+
const isContentEditable = element.isContentEditable;
|
|
172
|
+
if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) throw new Error("Element is not an input, textarea, or contenteditable");
|
|
173
|
+
await clickElement(element);
|
|
174
|
+
if (isContentEditable) {
|
|
175
|
+
if (element.dispatchEvent(new InputEvent("beforeinput", {
|
|
176
|
+
bubbles: true,
|
|
177
|
+
cancelable: true,
|
|
178
|
+
inputType: "deleteContent"
|
|
179
|
+
}))) {
|
|
180
|
+
element.innerText = "";
|
|
181
|
+
element.dispatchEvent(new InputEvent("input", {
|
|
182
|
+
bubbles: true,
|
|
183
|
+
inputType: "deleteContent"
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
if (element.dispatchEvent(new InputEvent("beforeinput", {
|
|
187
|
+
bubbles: true,
|
|
188
|
+
cancelable: true,
|
|
189
|
+
inputType: "insertText",
|
|
190
|
+
data: text
|
|
191
|
+
}))) {
|
|
192
|
+
element.innerText = text;
|
|
193
|
+
element.dispatchEvent(new InputEvent("input", {
|
|
194
|
+
bubbles: true,
|
|
195
|
+
inputType: "insertText",
|
|
196
|
+
data: text
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
if (!(element.innerText.trim() === text.trim())) {
|
|
200
|
+
element.focus();
|
|
201
|
+
const doc = element.ownerDocument;
|
|
202
|
+
const selection = (doc.defaultView || window).getSelection();
|
|
203
|
+
const range = doc.createRange();
|
|
204
|
+
range.selectNodeContents(element);
|
|
205
|
+
selection?.removeAllRanges();
|
|
206
|
+
selection?.addRange(range);
|
|
207
|
+
doc.execCommand("delete", false);
|
|
208
|
+
doc.execCommand("insertText", false, text);
|
|
209
|
+
}
|
|
210
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
211
|
+
element.blur();
|
|
212
|
+
} else getNativeValueSetter(element).call(element, text);
|
|
213
|
+
if (!isContentEditable) element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
214
|
+
await waitFor$1(.1);
|
|
215
|
+
blurLastClickedElement();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* @todo browser-use version is very complex and supports menu tags, need to follow up
|
|
219
|
+
* @private Internal method, subject to change at any time.
|
|
220
|
+
*/
|
|
221
|
+
async function selectOptionElement(selectElement, optionText) {
|
|
222
|
+
if (!isSelectElement(selectElement)) throw new Error("Element is not a select element");
|
|
223
|
+
const option = Array.from(selectElement.options).find((opt) => opt.textContent?.trim() === optionText.trim());
|
|
224
|
+
if (!option) throw new Error(`Option with text "${optionText}" not found in select element`);
|
|
225
|
+
selectElement.value = option.value;
|
|
226
|
+
selectElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
227
|
+
await waitFor$1(.1);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* @private Internal method, subject to change at any time.
|
|
231
|
+
*/
|
|
232
|
+
async function scrollIntoViewIfNeeded(element) {
|
|
233
|
+
const el = element;
|
|
234
|
+
if (typeof el.scrollIntoViewIfNeeded === "function") el.scrollIntoViewIfNeeded();
|
|
235
|
+
else element.scrollIntoView({
|
|
236
|
+
behavior: "auto",
|
|
237
|
+
block: "center",
|
|
238
|
+
inline: "nearest"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function scrollVertically(scroll_amount, element) {
|
|
242
|
+
if (element) {
|
|
243
|
+
const targetElement = element;
|
|
244
|
+
let currentElement = targetElement;
|
|
245
|
+
let scrollSuccess = false;
|
|
246
|
+
let scrolledElement = null;
|
|
247
|
+
let scrollDelta = 0;
|
|
248
|
+
let attempts = 0;
|
|
249
|
+
const dy = scroll_amount;
|
|
250
|
+
while (currentElement && attempts < 10) {
|
|
251
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
252
|
+
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
|
|
253
|
+
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight;
|
|
254
|
+
if (hasScrollableY && canScrollVertically) {
|
|
255
|
+
const beforeScroll = currentElement.scrollTop;
|
|
256
|
+
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight;
|
|
257
|
+
let scrollAmount = dy / 3;
|
|
258
|
+
if (scrollAmount > 0) scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
259
|
+
else scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
260
|
+
currentElement.scrollTop = beforeScroll + scrollAmount;
|
|
261
|
+
const actualScrollDelta = currentElement.scrollTop - beforeScroll;
|
|
262
|
+
if (Math.abs(actualScrollDelta) > .5) {
|
|
263
|
+
scrollSuccess = true;
|
|
264
|
+
scrolledElement = currentElement;
|
|
265
|
+
scrollDelta = actualScrollDelta;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (currentElement === document.body || currentElement === document.documentElement) break;
|
|
270
|
+
currentElement = currentElement.parentElement;
|
|
271
|
+
attempts++;
|
|
272
|
+
}
|
|
273
|
+
if (scrollSuccess) return `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`;
|
|
274
|
+
else return `No scrollable container found for element (${targetElement.tagName})`;
|
|
275
|
+
}
|
|
276
|
+
const dy = scroll_amount;
|
|
277
|
+
const bigEnough = (el) => el.clientHeight >= window.innerHeight * .5;
|
|
278
|
+
const canScroll = (el) => Boolean(el && /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowY) && el.scrollHeight > el.clientHeight && bigEnough(el));
|
|
279
|
+
let el = document.activeElement;
|
|
280
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
281
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
282
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
283
|
+
const scrollBefore = window.scrollY;
|
|
284
|
+
const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
|
|
285
|
+
window.scrollBy(0, dy);
|
|
286
|
+
const scrollAfter = window.scrollY;
|
|
287
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
288
|
+
if (Math.abs(scrolled) < 1) return dy > 0 ? `⚠️ Already at the bottom of the page, cannot scroll down further.` : `⚠️ Already at the top of the page, cannot scroll up further.`;
|
|
289
|
+
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
|
|
290
|
+
const reachedTop = dy < 0 && scrollAfter <= 1;
|
|
291
|
+
if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`;
|
|
292
|
+
if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`;
|
|
293
|
+
return `✅ Scrolled page by ${scrolled}px.`;
|
|
294
|
+
} else {
|
|
295
|
+
const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
|
|
296
|
+
console.log(`[BrowserController] ${warningMsg}`);
|
|
297
|
+
const scrollBefore = el.scrollTop;
|
|
298
|
+
const scrollMax = el.scrollHeight - el.clientHeight;
|
|
299
|
+
el.scrollBy({
|
|
300
|
+
top: dy,
|
|
301
|
+
behavior: "smooth"
|
|
302
|
+
});
|
|
303
|
+
await waitFor$1(.1);
|
|
304
|
+
const scrollAfter = el.scrollTop;
|
|
305
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
306
|
+
if (Math.abs(scrolled) < 1) return dy > 0 ? `⚠️ ${warningMsg} Already at the bottom of container (${el.tagName}), cannot scroll down further.` : `⚠️ ${warningMsg} Already at the top of container (${el.tagName}), cannot scroll up further.`;
|
|
307
|
+
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
|
|
308
|
+
const reachedTop = dy < 0 && scrollAfter <= 1;
|
|
309
|
+
if (reachedBottom) return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the bottom.`;
|
|
310
|
+
if (reachedTop) return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the top.`;
|
|
311
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px.`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function scrollHorizontally(scroll_amount, element) {
|
|
315
|
+
if (element) {
|
|
316
|
+
const targetElement = element;
|
|
317
|
+
let currentElement = targetElement;
|
|
318
|
+
let scrollSuccess = false;
|
|
319
|
+
let scrolledElement = null;
|
|
320
|
+
let scrollDelta = 0;
|
|
321
|
+
let attempts = 0;
|
|
322
|
+
const dx = scroll_amount;
|
|
323
|
+
while (currentElement && attempts < 10) {
|
|
324
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
325
|
+
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
|
|
326
|
+
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth;
|
|
327
|
+
if (hasScrollableX && canScrollHorizontally) {
|
|
328
|
+
const beforeScroll = currentElement.scrollLeft;
|
|
329
|
+
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth;
|
|
330
|
+
let scrollAmount = dx / 3;
|
|
331
|
+
if (scrollAmount > 0) scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
332
|
+
else scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
333
|
+
currentElement.scrollLeft = beforeScroll + scrollAmount;
|
|
334
|
+
const actualScrollDelta = currentElement.scrollLeft - beforeScroll;
|
|
335
|
+
if (Math.abs(actualScrollDelta) > .5) {
|
|
336
|
+
scrollSuccess = true;
|
|
337
|
+
scrolledElement = currentElement;
|
|
338
|
+
scrollDelta = actualScrollDelta;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (currentElement === document.body || currentElement === document.documentElement) break;
|
|
343
|
+
currentElement = currentElement.parentElement;
|
|
344
|
+
attempts++;
|
|
345
|
+
}
|
|
346
|
+
if (scrollSuccess) return `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`;
|
|
347
|
+
else return `No horizontally scrollable container found for element (${targetElement.tagName})`;
|
|
348
|
+
}
|
|
349
|
+
const dx = scroll_amount;
|
|
350
|
+
const bigEnough = (el) => el.clientWidth >= window.innerWidth * .5;
|
|
351
|
+
const canScroll = (el) => Boolean(el && /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowX) && el.scrollWidth > el.clientWidth && bigEnough(el));
|
|
352
|
+
let el = document.activeElement;
|
|
353
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
354
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
355
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
356
|
+
const scrollBefore = window.scrollX;
|
|
357
|
+
const scrollMax = document.documentElement.scrollWidth - window.innerWidth;
|
|
358
|
+
window.scrollBy(dx, 0);
|
|
359
|
+
const scrollAfter = window.scrollX;
|
|
360
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
361
|
+
if (Math.abs(scrolled) < 1) return dx > 0 ? `⚠️ Already at the right edge of the page, cannot scroll right further.` : `⚠️ Already at the left edge of the page, cannot scroll left further.`;
|
|
362
|
+
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
|
|
363
|
+
const reachedLeft = dx < 0 && scrollAfter <= 1;
|
|
364
|
+
if (reachedRight) return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`;
|
|
365
|
+
if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`;
|
|
366
|
+
return `✅ Scrolled page horizontally by ${scrolled}px.`;
|
|
367
|
+
} else {
|
|
368
|
+
const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
|
|
369
|
+
console.log(`[BrowserController] ${warningMsg}`);
|
|
370
|
+
const scrollBefore = el.scrollLeft;
|
|
371
|
+
const scrollMax = el.scrollWidth - el.clientWidth;
|
|
372
|
+
el.scrollBy({
|
|
373
|
+
left: dx,
|
|
374
|
+
behavior: "smooth"
|
|
375
|
+
});
|
|
376
|
+
await waitFor$1(.1);
|
|
377
|
+
const scrollAfter = el.scrollLeft;
|
|
378
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
379
|
+
if (Math.abs(scrolled) < 1) return dx > 0 ? `⚠️ ${warningMsg} Already at the right edge of container (${el.tagName}), cannot scroll right further.` : `⚠️ ${warningMsg} Already at the left edge of container (${el.tagName}), cannot scroll left further.`;
|
|
380
|
+
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
|
|
381
|
+
const reachedLeft = dx < 0 && scrollAfter <= 1;
|
|
382
|
+
if (reachedRight) return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the right edge.`;
|
|
383
|
+
if (reachedLeft) return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the left edge.`;
|
|
384
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) horizontally by ${scrolled}px.`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/browser/dom/dom_tree/index.js
|
|
389
|
+
/**
|
|
390
|
+
* @file port from browser-use
|
|
391
|
+
* @see https://github.com/browser-use/browser-use/commits/main/browser_use/dom/dom_tree/index.js
|
|
392
|
+
* @match 0.5.9 d51b6e73daff7165fdd3e44debd667e7f5f7fdc5
|
|
393
|
+
*
|
|
394
|
+
* search @edit for all the changed lines.
|
|
395
|
+
*
|
|
396
|
+
* @edit export
|
|
397
|
+
* @edit add interactiveBlacklist interactiveWhitelist
|
|
398
|
+
* @edit adjustable opacity
|
|
399
|
+
* @edit direct dom ref
|
|
400
|
+
* @edit @workaround input.checked
|
|
401
|
+
* @edit smaller zIndex for highlight
|
|
402
|
+
* @edit no need for xpath
|
|
403
|
+
* @edit add `extra` field for extra data
|
|
404
|
+
* @edit scrollable element detection
|
|
405
|
+
* @edit add `data-browser-use-ignore` attribute
|
|
406
|
+
* @edit improve `sampleRect`, filter out rects with 0 area
|
|
407
|
+
* @edit exclude aria-hidden elements
|
|
408
|
+
* @edit make sure attributes exist for interactive candidates.
|
|
409
|
+
* @edit fix "aria-*" attributes check
|
|
410
|
+
*/
|
|
411
|
+
var dom_tree_default = (args = {
|
|
412
|
+
doHighlightElements: true,
|
|
413
|
+
focusHighlightIndex: -1,
|
|
414
|
+
viewportExpansion: 0,
|
|
415
|
+
debugMode: false,
|
|
416
|
+
/**
|
|
417
|
+
* @edit
|
|
418
|
+
*/
|
|
419
|
+
/** @type {Element[]} */
|
|
420
|
+
interactiveBlacklist: [],
|
|
421
|
+
/** @type {Element[]} */
|
|
422
|
+
interactiveWhitelist: [],
|
|
423
|
+
highlightOpacity: .1,
|
|
424
|
+
highlightLabelOpacity: .5
|
|
425
|
+
}) => {
|
|
426
|
+
/**
|
|
427
|
+
* @edit
|
|
428
|
+
*/
|
|
429
|
+
const { interactiveBlacklist, interactiveWhitelist, highlightOpacity, highlightLabelOpacity } = args;
|
|
430
|
+
const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
|
|
431
|
+
let highlightIndex = 0;
|
|
432
|
+
/**
|
|
433
|
+
* @edit add `extra` field for extra data
|
|
434
|
+
*/
|
|
435
|
+
const extraData = /* @__PURE__ */ new WeakMap();
|
|
436
|
+
function addExtraData(element, data) {
|
|
437
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
438
|
+
extraData.set(element, {
|
|
439
|
+
...extraData.get(element),
|
|
440
|
+
...data
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const DOM_CACHE = {
|
|
444
|
+
boundingRects: /* @__PURE__ */ new WeakMap(),
|
|
445
|
+
clientRects: /* @__PURE__ */ new WeakMap(),
|
|
446
|
+
computedStyles: /* @__PURE__ */ new WeakMap(),
|
|
447
|
+
clearCache: () => {
|
|
448
|
+
DOM_CACHE.boundingRects = /* @__PURE__ */ new WeakMap();
|
|
449
|
+
DOM_CACHE.clientRects = /* @__PURE__ */ new WeakMap();
|
|
450
|
+
DOM_CACHE.computedStyles = /* @__PURE__ */ new WeakMap();
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Gets the cached bounding rect for an element.
|
|
455
|
+
*
|
|
456
|
+
* @param {HTMLElement} element - The element to get the bounding rect for.
|
|
457
|
+
* @returns {DOMRect | null} The cached bounding rect, or null if the element is not found.
|
|
458
|
+
*/
|
|
459
|
+
function getCachedBoundingRect(element) {
|
|
460
|
+
if (!element) return null;
|
|
461
|
+
if (DOM_CACHE.boundingRects.has(element)) return DOM_CACHE.boundingRects.get(element);
|
|
462
|
+
const rect = element.getBoundingClientRect();
|
|
463
|
+
if (rect) DOM_CACHE.boundingRects.set(element, rect);
|
|
464
|
+
return rect;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Gets the cached computed style for an element.
|
|
468
|
+
*
|
|
469
|
+
* @param {HTMLElement} element - The element to get the computed style for.
|
|
470
|
+
* @returns {CSSStyleDeclaration | null} The cached computed style, or null if the element is not found.
|
|
471
|
+
*/
|
|
472
|
+
function getCachedComputedStyle(element) {
|
|
473
|
+
if (!element) return null;
|
|
474
|
+
if (DOM_CACHE.computedStyles.has(element)) return DOM_CACHE.computedStyles.get(element);
|
|
475
|
+
const style = window.getComputedStyle(element);
|
|
476
|
+
if (style) DOM_CACHE.computedStyles.set(element, style);
|
|
477
|
+
return style;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Gets the cached client rects for an element.
|
|
481
|
+
*
|
|
482
|
+
* @param {HTMLElement} element - The element to get the client rects for.
|
|
483
|
+
* @returns {DOMRectList | null} The cached client rects, or null if the element is not found.
|
|
484
|
+
*/
|
|
485
|
+
function getCachedClientRects(element) {
|
|
486
|
+
if (!element) return null;
|
|
487
|
+
if (DOM_CACHE.clientRects.has(element)) return DOM_CACHE.clientRects.get(element);
|
|
488
|
+
const rects = element.getClientRects();
|
|
489
|
+
if (rects) DOM_CACHE.clientRects.set(element, rects);
|
|
490
|
+
return rects;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Hash map of DOM nodes indexed by their highlight index.
|
|
494
|
+
*
|
|
495
|
+
* @type {Object<string, any>}
|
|
496
|
+
*/
|
|
497
|
+
const DOM_HASH_MAP = {};
|
|
498
|
+
const ID = { current: 0 };
|
|
499
|
+
const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
|
|
500
|
+
/**
|
|
501
|
+
* Highlights an element in the DOM and returns the index of the next element.
|
|
502
|
+
*
|
|
503
|
+
* @param {HTMLElement} element - The element to highlight.
|
|
504
|
+
* @param {number} index - The index of the element.
|
|
505
|
+
* @param {HTMLElement | null} parentIframe - The parent iframe node.
|
|
506
|
+
* @returns {number} The index of the next element.
|
|
507
|
+
*/
|
|
508
|
+
function highlightElement(element, index, parentIframe = null) {
|
|
509
|
+
if (!element) return index;
|
|
510
|
+
const overlays = [];
|
|
511
|
+
/**
|
|
512
|
+
* @type {HTMLElement | null}
|
|
513
|
+
*/
|
|
514
|
+
let label = null;
|
|
515
|
+
let labelWidth = 20;
|
|
516
|
+
let labelHeight = 16;
|
|
517
|
+
let cleanupFn = null;
|
|
518
|
+
try {
|
|
519
|
+
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
520
|
+
if (!container) {
|
|
521
|
+
container = document.createElement("div");
|
|
522
|
+
container.id = HIGHLIGHT_CONTAINER_ID;
|
|
523
|
+
container.style.position = "fixed";
|
|
524
|
+
container.style.pointerEvents = "none";
|
|
525
|
+
container.style.top = "0";
|
|
526
|
+
container.style.left = "0";
|
|
527
|
+
container.style.width = "100%";
|
|
528
|
+
container.style.height = "100%";
|
|
529
|
+
/**
|
|
530
|
+
* @edit smaller zIndex for highlight
|
|
531
|
+
*/
|
|
532
|
+
container.style.zIndex = "2147483640";
|
|
533
|
+
container.style.backgroundColor = "transparent";
|
|
534
|
+
document.body.appendChild(container);
|
|
535
|
+
}
|
|
536
|
+
const rects = element.getClientRects();
|
|
537
|
+
if (!rects || rects.length === 0) return index;
|
|
538
|
+
const colors = [
|
|
539
|
+
"#FF0000",
|
|
540
|
+
"#00FF00",
|
|
541
|
+
"#0000FF",
|
|
542
|
+
"#FFA500",
|
|
543
|
+
"#800080",
|
|
544
|
+
"#008080",
|
|
545
|
+
"#FF69B4",
|
|
546
|
+
"#4B0082",
|
|
547
|
+
"#FF4500",
|
|
548
|
+
"#2E8B57",
|
|
549
|
+
"#DC143C",
|
|
550
|
+
"#4682B4"
|
|
551
|
+
];
|
|
552
|
+
let baseColor = colors[index % colors.length];
|
|
553
|
+
/**
|
|
554
|
+
* @edit adjustable opacity
|
|
555
|
+
*/
|
|
556
|
+
const backgroundColor = baseColor + Math.floor(highlightOpacity * 255).toString(16).padStart(2, "0");
|
|
557
|
+
baseColor = baseColor + Math.floor(highlightLabelOpacity * 255).toString(16).padStart(2, "0");
|
|
558
|
+
let iframeOffset = {
|
|
559
|
+
x: 0,
|
|
560
|
+
y: 0
|
|
561
|
+
};
|
|
562
|
+
if (parentIframe) {
|
|
563
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
564
|
+
iframeOffset.x = iframeRect.left;
|
|
565
|
+
iframeOffset.y = iframeRect.top;
|
|
566
|
+
}
|
|
567
|
+
const fragment = document.createDocumentFragment();
|
|
568
|
+
for (const rect of rects) {
|
|
569
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
570
|
+
const overlay = document.createElement("div");
|
|
571
|
+
overlay.style.position = "fixed";
|
|
572
|
+
overlay.style.border = `2px solid ${baseColor}`;
|
|
573
|
+
overlay.style.backgroundColor = backgroundColor;
|
|
574
|
+
overlay.style.pointerEvents = "none";
|
|
575
|
+
overlay.style.boxSizing = "border-box";
|
|
576
|
+
const top = rect.top + iframeOffset.y;
|
|
577
|
+
const left = rect.left + iframeOffset.x;
|
|
578
|
+
overlay.style.top = `${top}px`;
|
|
579
|
+
overlay.style.left = `${left}px`;
|
|
580
|
+
overlay.style.width = `${rect.width}px`;
|
|
581
|
+
overlay.style.height = `${rect.height}px`;
|
|
582
|
+
fragment.appendChild(overlay);
|
|
583
|
+
overlays.push({
|
|
584
|
+
element: overlay,
|
|
585
|
+
initialRect: rect
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const firstRect = rects[0];
|
|
589
|
+
label = document.createElement("div");
|
|
590
|
+
label.className = "playwright-highlight-label";
|
|
591
|
+
label.style.position = "fixed";
|
|
592
|
+
label.style.background = baseColor;
|
|
593
|
+
label.style.color = "white";
|
|
594
|
+
label.style.padding = "1px 4px";
|
|
595
|
+
label.style.borderRadius = "4px";
|
|
596
|
+
label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
|
|
597
|
+
label.textContent = index.toString();
|
|
598
|
+
labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth;
|
|
599
|
+
labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight;
|
|
600
|
+
const firstRectTop = firstRect.top + iframeOffset.y;
|
|
601
|
+
const firstRectLeft = firstRect.left + iframeOffset.x;
|
|
602
|
+
let labelTop = firstRectTop + 2;
|
|
603
|
+
let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;
|
|
604
|
+
if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
|
|
605
|
+
labelTop = firstRectTop - labelHeight - 2;
|
|
606
|
+
labelLeft = firstRectLeft + firstRect.width - labelWidth;
|
|
607
|
+
if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft;
|
|
608
|
+
}
|
|
609
|
+
labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
|
|
610
|
+
labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));
|
|
611
|
+
label.style.top = `${labelTop}px`;
|
|
612
|
+
label.style.left = `${labelLeft}px`;
|
|
613
|
+
fragment.appendChild(label);
|
|
614
|
+
const updatePositions = () => {
|
|
615
|
+
const newRects = element.getClientRects();
|
|
616
|
+
let newIframeOffset = {
|
|
617
|
+
x: 0,
|
|
618
|
+
y: 0
|
|
619
|
+
};
|
|
620
|
+
if (parentIframe) {
|
|
621
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
622
|
+
newIframeOffset.x = iframeRect.left;
|
|
623
|
+
newIframeOffset.y = iframeRect.top;
|
|
624
|
+
}
|
|
625
|
+
overlays.forEach((overlayData, i) => {
|
|
626
|
+
if (i < newRects.length) {
|
|
627
|
+
const newRect = newRects[i];
|
|
628
|
+
const newTop = newRect.top + newIframeOffset.y;
|
|
629
|
+
const newLeft = newRect.left + newIframeOffset.x;
|
|
630
|
+
overlayData.element.style.top = `${newTop}px`;
|
|
631
|
+
overlayData.element.style.left = `${newLeft}px`;
|
|
632
|
+
overlayData.element.style.width = `${newRect.width}px`;
|
|
633
|
+
overlayData.element.style.height = `${newRect.height}px`;
|
|
634
|
+
overlayData.element.style.display = newRect.width === 0 || newRect.height === 0 ? "none" : "block";
|
|
635
|
+
} else overlayData.element.style.display = "none";
|
|
636
|
+
});
|
|
637
|
+
if (newRects.length < overlays.length) for (let i = newRects.length; i < overlays.length; i++) overlays[i].element.style.display = "none";
|
|
638
|
+
if (label && newRects.length > 0) {
|
|
639
|
+
const firstNewRect = newRects[0];
|
|
640
|
+
const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
|
|
641
|
+
const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;
|
|
642
|
+
let newLabelTop = firstNewRectTop + 2;
|
|
643
|
+
let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;
|
|
644
|
+
if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
|
|
645
|
+
newLabelTop = firstNewRectTop - labelHeight - 2;
|
|
646
|
+
newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
|
|
647
|
+
if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
|
|
648
|
+
}
|
|
649
|
+
newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
|
|
650
|
+
newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));
|
|
651
|
+
label.style.top = `${newLabelTop}px`;
|
|
652
|
+
label.style.left = `${newLabelLeft}px`;
|
|
653
|
+
label.style.display = "block";
|
|
654
|
+
} else if (label) label.style.display = "none";
|
|
655
|
+
};
|
|
656
|
+
const throttleFunction = (func, delay) => {
|
|
657
|
+
let lastCall = 0;
|
|
658
|
+
return (...args) => {
|
|
659
|
+
const now = performance.now();
|
|
660
|
+
if (now - lastCall < delay) return;
|
|
661
|
+
lastCall = now;
|
|
662
|
+
return func(...args);
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
const throttledUpdatePositions = throttleFunction(updatePositions, 16);
|
|
666
|
+
window.addEventListener("scroll", throttledUpdatePositions, true);
|
|
667
|
+
window.addEventListener("resize", throttledUpdatePositions);
|
|
668
|
+
cleanupFn = () => {
|
|
669
|
+
window.removeEventListener("scroll", throttledUpdatePositions, true);
|
|
670
|
+
window.removeEventListener("resize", throttledUpdatePositions);
|
|
671
|
+
overlays.forEach((overlay) => overlay.element.remove());
|
|
672
|
+
if (label) label.remove();
|
|
673
|
+
};
|
|
674
|
+
container.appendChild(fragment);
|
|
675
|
+
return index + 1;
|
|
676
|
+
} finally {
|
|
677
|
+
if (cleanupFn) (window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(cleanupFn);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* @edit scrollable element detection
|
|
682
|
+
* Checks if an element is scrollable. if so, return the scrollable distance on each direction (left right top bottom). if not return null.
|
|
683
|
+
* @note distance smaller than 4 will be considered as not scrollable.
|
|
684
|
+
* @note only check block elements, not inline elements.
|
|
685
|
+
*/
|
|
686
|
+
function isScrollableElement(element) {
|
|
687
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
|
|
688
|
+
const style = getCachedComputedStyle(element);
|
|
689
|
+
if (!style) return null;
|
|
690
|
+
const display = style.display;
|
|
691
|
+
if (display === "inline" || display === "inline-block") return null;
|
|
692
|
+
const overflowX = style.overflowX;
|
|
693
|
+
const overflowY = style.overflowY;
|
|
694
|
+
const hasScrollbarSignal = style.scrollbarWidth && style.scrollbarWidth !== "auto" || style.scrollbarGutter && style.scrollbarGutter !== "auto";
|
|
695
|
+
const scrollableX = overflowX === "auto" || overflowX === "scroll";
|
|
696
|
+
const scrollableY = overflowY === "auto" || overflowY === "scroll";
|
|
697
|
+
if (!scrollableX && !scrollableY && !hasScrollbarSignal) return null;
|
|
698
|
+
const scrollWidth = element.scrollWidth - element.clientWidth;
|
|
699
|
+
const scrollHeight = element.scrollHeight - element.clientHeight;
|
|
700
|
+
const threshold = 4;
|
|
701
|
+
if (scrollWidth < threshold && scrollHeight < threshold) return null;
|
|
702
|
+
if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) return null;
|
|
703
|
+
if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) return null;
|
|
704
|
+
const distanceToTop = element.scrollTop;
|
|
705
|
+
const distanceToLeft = element.scrollLeft;
|
|
706
|
+
const scrollData = {
|
|
707
|
+
top: distanceToTop,
|
|
708
|
+
right: element.scrollWidth - element.clientWidth - element.scrollLeft,
|
|
709
|
+
bottom: element.scrollHeight - element.clientHeight - element.scrollTop,
|
|
710
|
+
left: distanceToLeft
|
|
711
|
+
};
|
|
712
|
+
addExtraData(element, {
|
|
713
|
+
scrollable: true,
|
|
714
|
+
scrollData
|
|
715
|
+
});
|
|
716
|
+
return scrollData;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Checks if a text node is visible.
|
|
720
|
+
*
|
|
721
|
+
* @param {Text} textNode - The text node to check.
|
|
722
|
+
* @returns {boolean} Whether the text node is visible.
|
|
723
|
+
*/
|
|
724
|
+
function isTextNodeVisible(textNode) {
|
|
725
|
+
try {
|
|
726
|
+
if (viewportExpansion === -1) {
|
|
727
|
+
const parentElement = textNode.parentElement;
|
|
728
|
+
if (!parentElement) return false;
|
|
729
|
+
try {
|
|
730
|
+
return parentElement.checkVisibility({
|
|
731
|
+
checkOpacity: true,
|
|
732
|
+
checkVisibilityCSS: true
|
|
733
|
+
});
|
|
734
|
+
} catch (e) {
|
|
735
|
+
const style = window.getComputedStyle(parentElement);
|
|
736
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const range = document.createRange();
|
|
740
|
+
range.selectNodeContents(textNode);
|
|
741
|
+
const rects = range.getClientRects();
|
|
742
|
+
if (!rects || rects.length === 0) return false;
|
|
743
|
+
let isAnyRectVisible = false;
|
|
744
|
+
let isAnyRectInViewport = false;
|
|
745
|
+
for (const rect of rects) if (rect.width > 0 && rect.height > 0) {
|
|
746
|
+
isAnyRectVisible = true;
|
|
747
|
+
if (!(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
748
|
+
isAnyRectInViewport = true;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (!isAnyRectVisible || !isAnyRectInViewport) return false;
|
|
753
|
+
const parentElement = textNode.parentElement;
|
|
754
|
+
if (!parentElement) return false;
|
|
755
|
+
try {
|
|
756
|
+
return parentElement.checkVisibility({
|
|
757
|
+
checkOpacity: true,
|
|
758
|
+
checkVisibilityCSS: true
|
|
759
|
+
});
|
|
760
|
+
} catch (e) {
|
|
761
|
+
const style = window.getComputedStyle(parentElement);
|
|
762
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
763
|
+
}
|
|
764
|
+
} catch (e) {
|
|
765
|
+
console.warn("Error checking text node visibility:", e);
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Checks if an element is accepted.
|
|
771
|
+
*
|
|
772
|
+
* @param {HTMLElement} element - The element to check.
|
|
773
|
+
* @returns {boolean} Whether the element is accepted.
|
|
774
|
+
*/
|
|
775
|
+
function isElementAccepted(element) {
|
|
776
|
+
if (!element || !element.tagName) return false;
|
|
777
|
+
const alwaysAccept = /* @__PURE__ */ new Set([
|
|
778
|
+
"body",
|
|
779
|
+
"div",
|
|
780
|
+
"main",
|
|
781
|
+
"article",
|
|
782
|
+
"section",
|
|
783
|
+
"nav",
|
|
784
|
+
"header",
|
|
785
|
+
"footer"
|
|
786
|
+
]);
|
|
787
|
+
const tagName = element.tagName.toLowerCase();
|
|
788
|
+
if (alwaysAccept.has(tagName)) return true;
|
|
789
|
+
return !(/* @__PURE__ */ new Set([
|
|
790
|
+
"svg",
|
|
791
|
+
"script",
|
|
792
|
+
"style",
|
|
793
|
+
"link",
|
|
794
|
+
"meta",
|
|
795
|
+
"noscript",
|
|
796
|
+
"template"
|
|
797
|
+
])).has(tagName);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Checks if an element is visible.
|
|
801
|
+
*
|
|
802
|
+
* @param {HTMLElement} element - The element to check.
|
|
803
|
+
* @returns {boolean} Whether the element is visible.
|
|
804
|
+
*/
|
|
805
|
+
function isElementVisible(element) {
|
|
806
|
+
const style = getCachedComputedStyle(element);
|
|
807
|
+
return element.offsetWidth > 0 && element.offsetHeight > 0 && style?.visibility !== "hidden" && style?.display !== "none";
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Checks if an element is interactive.
|
|
811
|
+
*
|
|
812
|
+
* lots of comments, and uncommented code - to show the logic of what we already tried
|
|
813
|
+
*
|
|
814
|
+
* One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)
|
|
815
|
+
*
|
|
816
|
+
* @param {HTMLElement} element - The element to check.
|
|
817
|
+
*/
|
|
818
|
+
function isInteractiveElement(element) {
|
|
819
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
820
|
+
/**
|
|
821
|
+
* @edit add interactiveBlacklist interactiveWhitelist
|
|
822
|
+
*/
|
|
823
|
+
if (interactiveBlacklist.includes(element)) return false;
|
|
824
|
+
if (interactiveWhitelist.includes(element)) return true;
|
|
825
|
+
const tagName = element.tagName.toLowerCase();
|
|
826
|
+
const style = getCachedComputedStyle(element);
|
|
827
|
+
const interactiveCursors = /* @__PURE__ */ new Set([
|
|
828
|
+
"pointer",
|
|
829
|
+
"move",
|
|
830
|
+
"text",
|
|
831
|
+
"grab",
|
|
832
|
+
"grabbing",
|
|
833
|
+
"cell",
|
|
834
|
+
"copy",
|
|
835
|
+
"alias",
|
|
836
|
+
"all-scroll",
|
|
837
|
+
"col-resize",
|
|
838
|
+
"context-menu",
|
|
839
|
+
"crosshair",
|
|
840
|
+
"e-resize",
|
|
841
|
+
"ew-resize",
|
|
842
|
+
"help",
|
|
843
|
+
"n-resize",
|
|
844
|
+
"ne-resize",
|
|
845
|
+
"nesw-resize",
|
|
846
|
+
"ns-resize",
|
|
847
|
+
"nw-resize",
|
|
848
|
+
"nwse-resize",
|
|
849
|
+
"row-resize",
|
|
850
|
+
"s-resize",
|
|
851
|
+
"se-resize",
|
|
852
|
+
"sw-resize",
|
|
853
|
+
"vertical-text",
|
|
854
|
+
"w-resize",
|
|
855
|
+
"zoom-in",
|
|
856
|
+
"zoom-out"
|
|
857
|
+
]);
|
|
858
|
+
const nonInteractiveCursors = /* @__PURE__ */ new Set([
|
|
859
|
+
"not-allowed",
|
|
860
|
+
"no-drop",
|
|
861
|
+
"wait",
|
|
862
|
+
"progress",
|
|
863
|
+
"initial",
|
|
864
|
+
"inherit"
|
|
865
|
+
]);
|
|
866
|
+
/**
|
|
867
|
+
* Checks if an element has an interactive pointer.
|
|
868
|
+
*
|
|
869
|
+
* @param {HTMLElement} element - The element to check.
|
|
870
|
+
* @returns {boolean} Whether the element has an interactive pointer.
|
|
871
|
+
*/
|
|
872
|
+
function doesElementHaveInteractivePointer(element) {
|
|
873
|
+
if (element.tagName.toLowerCase() === "html") return false;
|
|
874
|
+
if (style?.cursor && interactiveCursors.has(style.cursor)) return true;
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (doesElementHaveInteractivePointer(element)) return true;
|
|
878
|
+
const interactiveElements = /* @__PURE__ */ new Set([
|
|
879
|
+
"a",
|
|
880
|
+
"button",
|
|
881
|
+
"input",
|
|
882
|
+
"select",
|
|
883
|
+
"textarea",
|
|
884
|
+
"details",
|
|
885
|
+
"summary",
|
|
886
|
+
"label",
|
|
887
|
+
"option",
|
|
888
|
+
"optgroup",
|
|
889
|
+
"fieldset",
|
|
890
|
+
"legend"
|
|
891
|
+
]);
|
|
892
|
+
const explicitDisableTags = /* @__PURE__ */ new Set(["disabled", "readonly"]);
|
|
893
|
+
if (interactiveElements.has(tagName)) {
|
|
894
|
+
if (style?.cursor && nonInteractiveCursors.has(style.cursor)) return false;
|
|
895
|
+
for (const disableTag of explicitDisableTags) if (element.hasAttribute(disableTag) || element.getAttribute(disableTag) === "true" || element.getAttribute(disableTag) === "") return false;
|
|
896
|
+
if (element.disabled) return false;
|
|
897
|
+
if (element.readOnly) return false;
|
|
898
|
+
if (element.inert) return false;
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
const role = element.getAttribute("role");
|
|
902
|
+
const ariaRole = element.getAttribute("aria-role");
|
|
903
|
+
if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) return true;
|
|
904
|
+
if (element.classList && (element.classList.contains("button") || element.classList.contains("dropdown-toggle") || element.getAttribute("data-index") || element.getAttribute("data-toggle") === "dropdown" || element.getAttribute("aria-haspopup") === "true")) return true;
|
|
905
|
+
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
906
|
+
"button",
|
|
907
|
+
"menu",
|
|
908
|
+
"menubar",
|
|
909
|
+
"menuitem",
|
|
910
|
+
"menuitemradio",
|
|
911
|
+
"menuitemcheckbox",
|
|
912
|
+
"radio",
|
|
913
|
+
"checkbox",
|
|
914
|
+
"tab",
|
|
915
|
+
"switch",
|
|
916
|
+
"slider",
|
|
917
|
+
"spinbutton",
|
|
918
|
+
"combobox",
|
|
919
|
+
"searchbox",
|
|
920
|
+
"textbox",
|
|
921
|
+
"listbox",
|
|
922
|
+
"option",
|
|
923
|
+
"scrollbar"
|
|
924
|
+
]);
|
|
925
|
+
if (interactiveElements.has(tagName) || role && interactiveRoles.has(role) || ariaRole && interactiveRoles.has(ariaRole)) return true;
|
|
926
|
+
try {
|
|
927
|
+
if (typeof getEventListeners === "function") {
|
|
928
|
+
const listeners = getEventListeners(element);
|
|
929
|
+
for (const eventType of [
|
|
930
|
+
"click",
|
|
931
|
+
"mousedown",
|
|
932
|
+
"mouseup",
|
|
933
|
+
"dblclick"
|
|
934
|
+
]) if (listeners[eventType] && listeners[eventType].length > 0) return true;
|
|
935
|
+
}
|
|
936
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
937
|
+
if (typeof getEventListenersForNode === "function") {
|
|
938
|
+
const listeners = getEventListenersForNode(element);
|
|
939
|
+
for (const eventType of [
|
|
940
|
+
"click",
|
|
941
|
+
"mousedown",
|
|
942
|
+
"mouseup",
|
|
943
|
+
"keydown",
|
|
944
|
+
"keyup",
|
|
945
|
+
"submit",
|
|
946
|
+
"change",
|
|
947
|
+
"input",
|
|
948
|
+
"focus",
|
|
949
|
+
"blur"
|
|
950
|
+
]) for (const listener of listeners) if (listener.type === eventType) return true;
|
|
951
|
+
}
|
|
952
|
+
for (const attr of [
|
|
953
|
+
"onclick",
|
|
954
|
+
"onmousedown",
|
|
955
|
+
"onmouseup",
|
|
956
|
+
"ondblclick"
|
|
957
|
+
]) if (element.hasAttribute(attr) || typeof element[attr] === "function") return true;
|
|
958
|
+
} catch (e) {}
|
|
959
|
+
/**
|
|
960
|
+
* @edit scrollable element detection
|
|
961
|
+
*/
|
|
962
|
+
if (isScrollableElement(element)) return true;
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Checks if an element is the topmost element at its position.
|
|
967
|
+
*
|
|
968
|
+
* @param {HTMLElement} element - The element to check.
|
|
969
|
+
* @returns {boolean} Whether the element is the topmost element at its position.
|
|
970
|
+
*/
|
|
971
|
+
function isTopElement(element) {
|
|
972
|
+
if (viewportExpansion === -1) return true;
|
|
973
|
+
const rects = getCachedClientRects(element);
|
|
974
|
+
if (!rects || rects.length === 0) return false;
|
|
975
|
+
let isAnyRectInViewport = false;
|
|
976
|
+
for (const rect of rects) if (rect.width > 0 && rect.height > 0 && !(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
977
|
+
isAnyRectInViewport = true;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
if (!isAnyRectInViewport) return false;
|
|
981
|
+
if (element.ownerDocument !== window.document) return true;
|
|
982
|
+
/**
|
|
983
|
+
* @edit improve `sampleRect`, filter out rects with 0 area
|
|
984
|
+
*/
|
|
985
|
+
let rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0);
|
|
986
|
+
if (!rect) return false;
|
|
987
|
+
const shadowRoot = element.getRootNode();
|
|
988
|
+
if (shadowRoot instanceof ShadowRoot) {
|
|
989
|
+
const centerX = rect.left + rect.width / 2;
|
|
990
|
+
const centerY = rect.top + rect.height / 2;
|
|
991
|
+
try {
|
|
992
|
+
const topEl = shadowRoot.elementFromPoint(centerX, centerY);
|
|
993
|
+
if (!topEl) return false;
|
|
994
|
+
let current = topEl;
|
|
995
|
+
while (current && current !== shadowRoot) {
|
|
996
|
+
if (current === element) return true;
|
|
997
|
+
current = current.parentElement;
|
|
998
|
+
}
|
|
999
|
+
return false;
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const margin = 5;
|
|
1005
|
+
return [
|
|
1006
|
+
{
|
|
1007
|
+
x: rect.left + rect.width / 2,
|
|
1008
|
+
y: rect.top + rect.height / 2
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
x: rect.left + margin,
|
|
1012
|
+
y: rect.top + margin
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
x: rect.right - margin,
|
|
1016
|
+
y: rect.bottom - margin
|
|
1017
|
+
}
|
|
1018
|
+
].some(({ x, y }) => {
|
|
1019
|
+
try {
|
|
1020
|
+
const topEl = document.elementFromPoint(x, y);
|
|
1021
|
+
if (!topEl) return false;
|
|
1022
|
+
let current = topEl;
|
|
1023
|
+
while (current && current !== document.documentElement) {
|
|
1024
|
+
if (current === element) return true;
|
|
1025
|
+
current = current.parentElement;
|
|
1026
|
+
}
|
|
1027
|
+
return false;
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Checks if an element is within the expanded viewport.
|
|
1035
|
+
*
|
|
1036
|
+
* @param {HTMLElement} element - The element to check.
|
|
1037
|
+
* @param {number} viewportExpansion - The viewport expansion.
|
|
1038
|
+
* @returns {boolean} Whether the element is within the expanded viewport.
|
|
1039
|
+
*/
|
|
1040
|
+
function isInExpandedViewport(element, viewportExpansion) {
|
|
1041
|
+
if (viewportExpansion === -1) return true;
|
|
1042
|
+
const rects = element.getClientRects();
|
|
1043
|
+
if (!rects || rects.length === 0) {
|
|
1044
|
+
const boundingRect = getCachedBoundingRect(element);
|
|
1045
|
+
if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) return false;
|
|
1046
|
+
return !(boundingRect.bottom < -viewportExpansion || boundingRect.top > window.innerHeight + viewportExpansion || boundingRect.right < -viewportExpansion || boundingRect.left > window.innerWidth + viewportExpansion);
|
|
1047
|
+
}
|
|
1048
|
+
for (const rect of rects) {
|
|
1049
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1050
|
+
if (!(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) return true;
|
|
1051
|
+
}
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Checks if an element is an interactive candidate.
|
|
1056
|
+
*
|
|
1057
|
+
* @param {HTMLElement} element - The element to check.
|
|
1058
|
+
* @returns {boolean} Whether the element is an interactive candidate.
|
|
1059
|
+
*/
|
|
1060
|
+
const INTERACTIVE_ARIA_ATTRS = [
|
|
1061
|
+
"aria-expanded",
|
|
1062
|
+
"aria-checked",
|
|
1063
|
+
"aria-selected",
|
|
1064
|
+
"aria-pressed",
|
|
1065
|
+
"aria-haspopup",
|
|
1066
|
+
"aria-controls",
|
|
1067
|
+
"aria-owns",
|
|
1068
|
+
"aria-activedescendant",
|
|
1069
|
+
"aria-valuenow",
|
|
1070
|
+
"aria-valuetext",
|
|
1071
|
+
"aria-valuemax",
|
|
1072
|
+
"aria-valuemin",
|
|
1073
|
+
"aria-autocomplete"
|
|
1074
|
+
];
|
|
1075
|
+
function hasInteractiveAria(el) {
|
|
1076
|
+
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true;
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
function isInteractiveCandidate(element) {
|
|
1080
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1081
|
+
const tagName = element.tagName.toLowerCase();
|
|
1082
|
+
if ((/* @__PURE__ */ new Set([
|
|
1083
|
+
"a",
|
|
1084
|
+
"button",
|
|
1085
|
+
"input",
|
|
1086
|
+
"select",
|
|
1087
|
+
"textarea",
|
|
1088
|
+
"details",
|
|
1089
|
+
"summary",
|
|
1090
|
+
"label"
|
|
1091
|
+
])).has(tagName)) return true;
|
|
1092
|
+
return element.hasAttribute("onclick") || element.hasAttribute("role") || element.hasAttribute("tabindex") || hasInteractiveAria(element) || element.hasAttribute("data-action") || element.getAttribute("contenteditable") === "true";
|
|
1093
|
+
}
|
|
1094
|
+
const DISTINCT_INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
1095
|
+
"a",
|
|
1096
|
+
"button",
|
|
1097
|
+
"input",
|
|
1098
|
+
"select",
|
|
1099
|
+
"textarea",
|
|
1100
|
+
"summary",
|
|
1101
|
+
"details",
|
|
1102
|
+
"label",
|
|
1103
|
+
"option",
|
|
1104
|
+
"li"
|
|
1105
|
+
]);
|
|
1106
|
+
const DISTINCT_INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
1107
|
+
"button",
|
|
1108
|
+
"link",
|
|
1109
|
+
"menuitem",
|
|
1110
|
+
"menuitemradio",
|
|
1111
|
+
"menuitemcheckbox",
|
|
1112
|
+
"radio",
|
|
1113
|
+
"checkbox",
|
|
1114
|
+
"tab",
|
|
1115
|
+
"switch",
|
|
1116
|
+
"slider",
|
|
1117
|
+
"spinbutton",
|
|
1118
|
+
"combobox",
|
|
1119
|
+
"searchbox",
|
|
1120
|
+
"textbox",
|
|
1121
|
+
"listbox",
|
|
1122
|
+
"listitem",
|
|
1123
|
+
"treeitem",
|
|
1124
|
+
"row",
|
|
1125
|
+
"option",
|
|
1126
|
+
"scrollbar"
|
|
1127
|
+
]);
|
|
1128
|
+
/**
|
|
1129
|
+
* Heuristically determines if an element should be considered as independently interactive,
|
|
1130
|
+
* even if it's nested inside another interactive container.
|
|
1131
|
+
*
|
|
1132
|
+
* This function helps detect deeply nested actionable elements (e.g., menu items within a button)
|
|
1133
|
+
* that may not be picked up by strict interactivity checks.
|
|
1134
|
+
*
|
|
1135
|
+
* @param {HTMLElement} element - The element to check.
|
|
1136
|
+
* @returns {boolean} Whether the element is heuristically interactive.
|
|
1137
|
+
*/
|
|
1138
|
+
function isHeuristicallyInteractive(element) {
|
|
1139
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1140
|
+
if (!isElementVisible(element)) return false;
|
|
1141
|
+
const hasInteractiveAttributes = element.hasAttribute("role") || element.hasAttribute("tabindex") || element.hasAttribute("onclick") || typeof element.onclick === "function";
|
|
1142
|
+
const hasInteractiveClass = /\b(btn|clickable|menu|item|entry|link)\b/i.test(element.className || "");
|
|
1143
|
+
const isInKnownContainer = Boolean(element.closest("button,a,[role=\"button\"],.menu,.dropdown,.list,.toolbar"));
|
|
1144
|
+
const hasVisibleChildren = [...element.children].some(isElementVisible);
|
|
1145
|
+
const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);
|
|
1146
|
+
return (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) && hasVisibleChildren && isInKnownContainer && !isParentBody;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Checks if an element likely represents a distinct interaction
|
|
1150
|
+
* separate from its parent (if the parent is also interactive).
|
|
1151
|
+
*
|
|
1152
|
+
* @param {HTMLElement} element - The element to check.
|
|
1153
|
+
* @returns {boolean} Whether the element is a distinct interaction.
|
|
1154
|
+
*/
|
|
1155
|
+
function isElementDistinctInteraction(element) {
|
|
1156
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
1157
|
+
const tagName = element.tagName.toLowerCase();
|
|
1158
|
+
const role = element.getAttribute("role");
|
|
1159
|
+
if (tagName === "iframe") return true;
|
|
1160
|
+
if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) return true;
|
|
1161
|
+
if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) return true;
|
|
1162
|
+
if (element.isContentEditable || element.getAttribute("contenteditable") === "true") return true;
|
|
1163
|
+
if (element.hasAttribute("data-testid") || element.hasAttribute("data-cy") || element.hasAttribute("data-test")) return true;
|
|
1164
|
+
if (element.hasAttribute("onclick") || typeof element.onclick === "function") return true;
|
|
1165
|
+
if (hasInteractiveAria(element)) return true;
|
|
1166
|
+
try {
|
|
1167
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
1168
|
+
if (typeof getEventListenersForNode === "function") {
|
|
1169
|
+
const listeners = getEventListenersForNode(element);
|
|
1170
|
+
for (const eventType of [
|
|
1171
|
+
"click",
|
|
1172
|
+
"mousedown",
|
|
1173
|
+
"mouseup",
|
|
1174
|
+
"keydown",
|
|
1175
|
+
"keyup",
|
|
1176
|
+
"submit",
|
|
1177
|
+
"change",
|
|
1178
|
+
"input",
|
|
1179
|
+
"focus",
|
|
1180
|
+
"blur"
|
|
1181
|
+
]) for (const listener of listeners) if (listener.type === eventType) return true;
|
|
1182
|
+
}
|
|
1183
|
+
if ([
|
|
1184
|
+
"onmousedown",
|
|
1185
|
+
"onmouseup",
|
|
1186
|
+
"onkeydown",
|
|
1187
|
+
"onkeyup",
|
|
1188
|
+
"onsubmit",
|
|
1189
|
+
"onchange",
|
|
1190
|
+
"oninput",
|
|
1191
|
+
"onfocus",
|
|
1192
|
+
"onblur"
|
|
1193
|
+
].some((attr) => element.hasAttribute(attr))) return true;
|
|
1194
|
+
} catch (e) {}
|
|
1195
|
+
if (isHeuristicallyInteractive(element)) return true;
|
|
1196
|
+
if (extraData.get(element)?.scrollable) return true;
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Handles the logic for deciding whether to highlight an element and performing the highlight.
|
|
1201
|
+
* @param {
|
|
1202
|
+
{
|
|
1203
|
+
tagName: string;
|
|
1204
|
+
attributes: Record<string, string>;
|
|
1205
|
+
xpath: any;
|
|
1206
|
+
children: never[];
|
|
1207
|
+
isVisible?: boolean;
|
|
1208
|
+
isTopElement?: boolean;
|
|
1209
|
+
isInteractive?: boolean;
|
|
1210
|
+
isInViewport?: boolean;
|
|
1211
|
+
highlightIndex?: number;
|
|
1212
|
+
shadowRoot?: boolean;
|
|
1213
|
+
}} nodeData - The node data object.
|
|
1214
|
+
* @param {HTMLElement} node - The node to highlight.
|
|
1215
|
+
* @param {HTMLElement | null} parentIframe - The parent iframe node.
|
|
1216
|
+
* @param {boolean} isParentHighlighted - Whether the parent node is highlighted.
|
|
1217
|
+
* @returns {boolean} Whether the element was highlighted.
|
|
1218
|
+
*/
|
|
1219
|
+
function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
|
|
1220
|
+
if (!nodeData.isInteractive) return false;
|
|
1221
|
+
let shouldHighlight = false;
|
|
1222
|
+
if (!isParentHighlighted) shouldHighlight = true;
|
|
1223
|
+
else if (isElementDistinctInteraction(node)) shouldHighlight = true;
|
|
1224
|
+
else shouldHighlight = false;
|
|
1225
|
+
if (shouldHighlight) {
|
|
1226
|
+
nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);
|
|
1227
|
+
if (nodeData.isInViewport || viewportExpansion === -1) {
|
|
1228
|
+
nodeData.highlightIndex = highlightIndex++;
|
|
1229
|
+
if (doHighlightElements) {
|
|
1230
|
+
if (focusHighlightIndex >= 0) {
|
|
1231
|
+
if (focusHighlightIndex === nodeData.highlightIndex) highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
1232
|
+
} else highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Creates a node data object for a given node and its descendants.
|
|
1241
|
+
*
|
|
1242
|
+
* @param {HTMLElement} node - The node to process.
|
|
1243
|
+
* @param {HTMLElement | null} parentIframe - The parent iframe node.
|
|
1244
|
+
* @param {boolean} isParentHighlighted - Whether the parent node is highlighted.
|
|
1245
|
+
* @returns {string | null} The ID of the node data object, or null if the node is not processed.
|
|
1246
|
+
*/
|
|
1247
|
+
function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
|
|
1248
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID || node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) return null;
|
|
1249
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID) return null;
|
|
1250
|
+
/**
|
|
1251
|
+
* @edit add `data-browser-use-ignore` attribute
|
|
1252
|
+
*/
|
|
1253
|
+
if (node.dataset?.browserUseIgnore === "true" || node.dataset?.pageAgentIgnore === "true") return null;
|
|
1254
|
+
/**
|
|
1255
|
+
* @edit exclude aria-hidden elements
|
|
1256
|
+
*/
|
|
1257
|
+
if (node.getAttribute && node.getAttribute("aria-hidden") === "true") return null;
|
|
1258
|
+
if (node === document.body) {
|
|
1259
|
+
const nodeData = {
|
|
1260
|
+
tagName: "body",
|
|
1261
|
+
attributes: {},
|
|
1262
|
+
xpath: "/body",
|
|
1263
|
+
children: []
|
|
1264
|
+
};
|
|
1265
|
+
for (const child of node.childNodes) {
|
|
1266
|
+
const domElement = buildDomTree(child, parentIframe, false);
|
|
1267
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1268
|
+
}
|
|
1269
|
+
const id = `${ID.current++}`;
|
|
1270
|
+
DOM_HASH_MAP[id] = nodeData;
|
|
1271
|
+
return id;
|
|
1272
|
+
}
|
|
1273
|
+
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) return null;
|
|
1274
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1275
|
+
const textContent = node.textContent?.trim();
|
|
1276
|
+
if (!textContent) return null;
|
|
1277
|
+
const parentElement = node.parentElement;
|
|
1278
|
+
if (!parentElement || parentElement.tagName.toLowerCase() === "script") return null;
|
|
1279
|
+
const id = `${ID.current++}`;
|
|
1280
|
+
DOM_HASH_MAP[id] = {
|
|
1281
|
+
type: "TEXT_NODE",
|
|
1282
|
+
text: textContent,
|
|
1283
|
+
isVisible: isTextNodeVisible(node)
|
|
1284
|
+
};
|
|
1285
|
+
return id;
|
|
1286
|
+
}
|
|
1287
|
+
if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) return null;
|
|
1288
|
+
if (viewportExpansion !== -1 && !node.shadowRoot) {
|
|
1289
|
+
const rect = getCachedBoundingRect(node);
|
|
1290
|
+
const style = getCachedComputedStyle(node);
|
|
1291
|
+
const isFixedOrSticky = style && (style.position === "fixed" || style.position === "sticky");
|
|
1292
|
+
const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
|
|
1293
|
+
if (!rect || !isFixedOrSticky && !hasSize && (rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) return null;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* @type {
|
|
1297
|
+
{
|
|
1298
|
+
tagName: string;
|
|
1299
|
+
attributes: Record<string, string | null>;
|
|
1300
|
+
xpath: any;
|
|
1301
|
+
children: never[];
|
|
1302
|
+
isVisible?: boolean;
|
|
1303
|
+
isTopElement?: boolean;
|
|
1304
|
+
isInteractive?: boolean;
|
|
1305
|
+
isInViewport?: boolean;
|
|
1306
|
+
highlightIndex?: number;
|
|
1307
|
+
shadowRoot?: boolean;
|
|
1308
|
+
}
|
|
1309
|
+
} nodeData - The node data object.
|
|
1310
|
+
*/
|
|
1311
|
+
const nodeData = {
|
|
1312
|
+
tagName: node.tagName.toLowerCase(),
|
|
1313
|
+
attributes: {},
|
|
1314
|
+
/**
|
|
1315
|
+
* @edit no need for xpath
|
|
1316
|
+
*/
|
|
1317
|
+
children: []
|
|
1318
|
+
};
|
|
1319
|
+
if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === "iframe" || node.tagName.toLowerCase() === "body") {
|
|
1320
|
+
const attributeNames = node.getAttributeNames?.() || [];
|
|
1321
|
+
for (const name of attributeNames) {
|
|
1322
|
+
const value = node.getAttribute(name);
|
|
1323
|
+
nodeData.attributes[name] = value;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* @edit @workaround input.checked
|
|
1327
|
+
*/
|
|
1328
|
+
if (node.tagName.toLowerCase() === "input" && (node.type === "checkbox" || node.type === "radio")) nodeData.attributes.checked = node.checked ? "true" : "false";
|
|
1329
|
+
}
|
|
1330
|
+
let nodeWasHighlighted = false;
|
|
1331
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1332
|
+
nodeData.isVisible = isElementVisible(node);
|
|
1333
|
+
if (nodeData.isVisible) {
|
|
1334
|
+
nodeData.isTopElement = isTopElement(node);
|
|
1335
|
+
const role = node.getAttribute("role");
|
|
1336
|
+
const isMenuContainer = role === "menu" || role === "menubar" || role === "listbox";
|
|
1337
|
+
if (nodeData.isTopElement || isMenuContainer) {
|
|
1338
|
+
nodeData.isInteractive = isInteractiveElement(node);
|
|
1339
|
+
nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
|
|
1340
|
+
/**
|
|
1341
|
+
* @edit direct dom ref
|
|
1342
|
+
*/
|
|
1343
|
+
nodeData.ref = node;
|
|
1344
|
+
/**
|
|
1345
|
+
* @edit make sure attributes exist for interactive candidates.
|
|
1346
|
+
* @note if the element failed the isInteractiveCandidate, attributes would be empty.
|
|
1347
|
+
*/
|
|
1348
|
+
if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
|
|
1349
|
+
const attributeNames = node.getAttributeNames?.() || [];
|
|
1350
|
+
for (const name of attributeNames) {
|
|
1351
|
+
const value = node.getAttribute(name);
|
|
1352
|
+
nodeData.attributes[name] = value;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (node.tagName) {
|
|
1359
|
+
const tagName = node.tagName.toLowerCase();
|
|
1360
|
+
if (tagName === "iframe") try {
|
|
1361
|
+
const iframeDoc = node.contentDocument || node.contentWindow?.document;
|
|
1362
|
+
if (iframeDoc) for (const child of iframeDoc.childNodes) {
|
|
1363
|
+
const domElement = buildDomTree(child, node, false);
|
|
1364
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1365
|
+
}
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
console.warn("Unable to access iframe:", e);
|
|
1368
|
+
}
|
|
1369
|
+
else if (node.isContentEditable || node.getAttribute("contenteditable") === "true" || node.id === "tinymce" || node.classList.contains("mce-content-body") || tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_")) for (const child of node.childNodes) {
|
|
1370
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
1371
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
if (node.shadowRoot) {
|
|
1375
|
+
nodeData.shadowRoot = true;
|
|
1376
|
+
for (const child of node.shadowRoot.childNodes) {
|
|
1377
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
1378
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
for (const child of node.childNodes) {
|
|
1382
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted || isParentHighlighted);
|
|
1383
|
+
if (domElement) nodeData.children.push(domElement);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (nodeData.tagName === "a" && nodeData.children.length === 0 && !nodeData.attributes.href) {
|
|
1388
|
+
const rect = getCachedBoundingRect(node);
|
|
1389
|
+
if (!(rect && rect.width > 0 && rect.height > 0 || node.offsetWidth > 0 || node.offsetHeight > 0)) return null;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* @edit add `extra` field for extra data
|
|
1393
|
+
*/
|
|
1394
|
+
nodeData.extra = extraData.get(node) || null;
|
|
1395
|
+
const id = `${ID.current++}`;
|
|
1396
|
+
DOM_HASH_MAP[id] = nodeData;
|
|
1397
|
+
return id;
|
|
1398
|
+
}
|
|
1399
|
+
const rootId = buildDomTree(document.body);
|
|
1400
|
+
DOM_CACHE.clearCache();
|
|
1401
|
+
return {
|
|
1402
|
+
rootId,
|
|
1403
|
+
map: DOM_HASH_MAP
|
|
1404
|
+
};
|
|
1405
|
+
};
|
|
1406
|
+
//#endregion
|
|
1407
|
+
//#region src/browser/dom/index.ts
|
|
1408
|
+
/**
|
|
1409
|
+
* Viewport expansion for DOM tree extraction.
|
|
1410
|
+
* -1 means full page (no viewport restriction)
|
|
1411
|
+
* 0 means viewport only
|
|
1412
|
+
* positive values expand the viewport by that many pixels
|
|
1413
|
+
*
|
|
1414
|
+
* @note Since isTopElement depends on elementFromPoint,
|
|
1415
|
+
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
|
|
1416
|
+
*/
|
|
1417
|
+
var DEFAULT_VIEWPORT_EXPANSION = -1;
|
|
1418
|
+
function resolveViewportExpansion(viewportExpansion) {
|
|
1419
|
+
return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION;
|
|
1420
|
+
}
|
|
1421
|
+
var SEMANTIC_TAGS = /* @__PURE__ */ new Set([
|
|
1422
|
+
"nav",
|
|
1423
|
+
"menu",
|
|
1424
|
+
"header",
|
|
1425
|
+
"footer",
|
|
1426
|
+
"aside",
|
|
1427
|
+
"dialog"
|
|
1428
|
+
]);
|
|
1429
|
+
/**
|
|
1430
|
+
* 用于检测可交互元素是否是新出现的。
|
|
1431
|
+
*/
|
|
1432
|
+
var newElementsCache = /* @__PURE__ */ new WeakMap();
|
|
1433
|
+
function getFlatTree(config) {
|
|
1434
|
+
const viewportExpansion = resolveViewportExpansion(config.viewportExpansion);
|
|
1435
|
+
const interactiveBlacklist = [];
|
|
1436
|
+
for (const item of config.interactiveBlacklist || []) if (typeof item === "function") interactiveBlacklist.push(item());
|
|
1437
|
+
else interactiveBlacklist.push(item);
|
|
1438
|
+
const interactiveWhitelist = [];
|
|
1439
|
+
for (const item of config.interactiveWhitelist || []) if (typeof item === "function") interactiveWhitelist.push(item());
|
|
1440
|
+
else interactiveWhitelist.push(item);
|
|
1441
|
+
const elements = dom_tree_default({
|
|
1442
|
+
doHighlightElements: true,
|
|
1443
|
+
debugMode: true,
|
|
1444
|
+
focusHighlightIndex: -1,
|
|
1445
|
+
viewportExpansion,
|
|
1446
|
+
interactiveBlacklist,
|
|
1447
|
+
interactiveWhitelist,
|
|
1448
|
+
highlightOpacity: config.highlightOpacity ?? 0,
|
|
1449
|
+
highlightLabelOpacity: config.highlightLabelOpacity ?? .1
|
|
1450
|
+
});
|
|
1451
|
+
const currentUrl = window.location.href;
|
|
1452
|
+
/**
|
|
1453
|
+
* 标记新出现的元素
|
|
1454
|
+
* @todo browser-use 使用 hash(位置,属性等信息) 来判断是否同一个元素,
|
|
1455
|
+
* 能够解决 1. 元素被删除后重新添加 2. 页面卸载 等问题。
|
|
1456
|
+
* 这里先简单做.
|
|
1457
|
+
*/
|
|
1458
|
+
for (const nodeId in elements.map) {
|
|
1459
|
+
const node = elements.map[nodeId];
|
|
1460
|
+
if (node.isInteractive && node.ref) {
|
|
1461
|
+
const ref = node.ref;
|
|
1462
|
+
if (!newElementsCache.has(ref)) {
|
|
1463
|
+
newElementsCache.set(ref, currentUrl);
|
|
1464
|
+
node.isNew = true;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return elements;
|
|
1469
|
+
}
|
|
1470
|
+
var globRegexCache = /* @__PURE__ */ new Map();
|
|
1471
|
+
function globToRegex(pattern) {
|
|
1472
|
+
let regex = globRegexCache.get(pattern);
|
|
1473
|
+
if (!regex) {
|
|
1474
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1475
|
+
regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
1476
|
+
globRegexCache.set(pattern, regex);
|
|
1477
|
+
}
|
|
1478
|
+
return regex;
|
|
1479
|
+
}
|
|
1480
|
+
function matchAttributes(attrs, patterns) {
|
|
1481
|
+
const result = {};
|
|
1482
|
+
for (const pattern of patterns) if (pattern.includes("*")) {
|
|
1483
|
+
const regex = globToRegex(pattern);
|
|
1484
|
+
for (const key of Object.keys(attrs)) if (regex.test(key) && attrs[key].trim()) result[key] = attrs[key].trim();
|
|
1485
|
+
} else {
|
|
1486
|
+
const value = attrs[pattern];
|
|
1487
|
+
if (value && value.trim()) result[pattern] = value.trim();
|
|
1488
|
+
}
|
|
1489
|
+
return result;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* 对应 python 中的 views::clickable_elements_to_string,
|
|
1493
|
+
* 将 dom 信息处理成适合 llm 阅读的文本格式
|
|
1494
|
+
* @形如
|
|
1495
|
+
* ``` text
|
|
1496
|
+
* [0]<a aria-label=page-agent.js 首页 />
|
|
1497
|
+
* [1]<div >P />
|
|
1498
|
+
* [2]<div >page-agent.js
|
|
1499
|
+
* UI Agent in your webpage />
|
|
1500
|
+
* [3]<a >文档 />
|
|
1501
|
+
* [4]<a aria-label=查看源码(在新窗口打开)>源码 />
|
|
1502
|
+
* UI Agent in your webpage
|
|
1503
|
+
* 用户输入需求,AI 理解页面并自动操作。
|
|
1504
|
+
* [5]<a role=button>快速开始 />
|
|
1505
|
+
* [6]<a role=button>查看文档 />
|
|
1506
|
+
* 无需后端
|
|
1507
|
+
* ```
|
|
1508
|
+
* 其中可交互元素用序号标出,提示llm可以用序号操作。
|
|
1509
|
+
* 缩进代表父子关系。
|
|
1510
|
+
* 普通文本则直接列出来。
|
|
1511
|
+
*
|
|
1512
|
+
* @todo 数据脱敏过滤器
|
|
1513
|
+
*/
|
|
1514
|
+
function flatTreeToString(flatTree, includeAttributes = [], keepSemanticTags = false) {
|
|
1515
|
+
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
|
1516
|
+
"title",
|
|
1517
|
+
"type",
|
|
1518
|
+
"checked",
|
|
1519
|
+
"name",
|
|
1520
|
+
"role",
|
|
1521
|
+
"value",
|
|
1522
|
+
"placeholder",
|
|
1523
|
+
"data-date-format",
|
|
1524
|
+
"alt",
|
|
1525
|
+
"aria-label",
|
|
1526
|
+
"aria-expanded",
|
|
1527
|
+
"data-state",
|
|
1528
|
+
"aria-checked",
|
|
1529
|
+
"id",
|
|
1530
|
+
"for",
|
|
1531
|
+
"target",
|
|
1532
|
+
"aria-haspopup",
|
|
1533
|
+
"aria-controls",
|
|
1534
|
+
"aria-owns",
|
|
1535
|
+
"contenteditable"
|
|
1536
|
+
];
|
|
1537
|
+
const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES];
|
|
1538
|
+
const capTextLength = (text, maxLength) => {
|
|
1539
|
+
if (text.length > maxLength) return text.substring(0, maxLength) + "...";
|
|
1540
|
+
return text;
|
|
1541
|
+
};
|
|
1542
|
+
const buildTreeNode = (nodeId) => {
|
|
1543
|
+
const node = flatTree.map[nodeId];
|
|
1544
|
+
if (!node) return null;
|
|
1545
|
+
if (node.type === "TEXT_NODE") {
|
|
1546
|
+
const textNode = node;
|
|
1547
|
+
return {
|
|
1548
|
+
type: "text",
|
|
1549
|
+
text: textNode.text,
|
|
1550
|
+
isVisible: textNode.isVisible,
|
|
1551
|
+
parent: null,
|
|
1552
|
+
children: []
|
|
1553
|
+
};
|
|
1554
|
+
} else {
|
|
1555
|
+
const elementNode = node;
|
|
1556
|
+
const children = [];
|
|
1557
|
+
if (elementNode.children) for (const childId of elementNode.children) {
|
|
1558
|
+
const child = buildTreeNode(childId);
|
|
1559
|
+
if (child) {
|
|
1560
|
+
child.parent = null;
|
|
1561
|
+
children.push(child);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return {
|
|
1565
|
+
type: "element",
|
|
1566
|
+
tagName: elementNode.tagName,
|
|
1567
|
+
attributes: elementNode.attributes ?? {},
|
|
1568
|
+
isVisible: elementNode.isVisible ?? false,
|
|
1569
|
+
isInteractive: elementNode.isInteractive ?? false,
|
|
1570
|
+
isTopElement: elementNode.isTopElement ?? false,
|
|
1571
|
+
isNew: elementNode.isNew ?? false,
|
|
1572
|
+
highlightIndex: elementNode.highlightIndex,
|
|
1573
|
+
parent: null,
|
|
1574
|
+
children,
|
|
1575
|
+
extra: elementNode.extra ?? {}
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
const setParentReferences = (node, parent = null) => {
|
|
1580
|
+
node.parent = parent;
|
|
1581
|
+
for (const child of node.children) setParentReferences(child, node);
|
|
1582
|
+
};
|
|
1583
|
+
const rootNode = buildTreeNode(flatTree.rootId);
|
|
1584
|
+
if (!rootNode) return "";
|
|
1585
|
+
setParentReferences(rootNode);
|
|
1586
|
+
const hasParentWithHighlightIndex = (node) => {
|
|
1587
|
+
let current = node.parent;
|
|
1588
|
+
while (current) {
|
|
1589
|
+
if (current.type === "element" && current.highlightIndex !== void 0) return true;
|
|
1590
|
+
current = current.parent;
|
|
1591
|
+
}
|
|
1592
|
+
return false;
|
|
1593
|
+
};
|
|
1594
|
+
const processNode = (node, depth, result) => {
|
|
1595
|
+
let nextDepth = depth;
|
|
1596
|
+
const depthStr = " ".repeat(depth);
|
|
1597
|
+
if (node.type === "element") {
|
|
1598
|
+
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName);
|
|
1599
|
+
if (node.highlightIndex !== void 0) {
|
|
1600
|
+
nextDepth += 1;
|
|
1601
|
+
const text = getAllTextTillNextClickableElement(node);
|
|
1602
|
+
let attributesHtmlStr = "";
|
|
1603
|
+
if (includeAttrs.length > 0 && node.attributes) {
|
|
1604
|
+
const attributesToInclude = matchAttributes(node.attributes, includeAttrs);
|
|
1605
|
+
const keys = Object.keys(attributesToInclude);
|
|
1606
|
+
if (keys.length > 1) {
|
|
1607
|
+
const keysToRemove = /* @__PURE__ */ new Set();
|
|
1608
|
+
const seenValues = {};
|
|
1609
|
+
for (const key of keys) {
|
|
1610
|
+
const value = attributesToInclude[key];
|
|
1611
|
+
if (value.length > 5) {
|
|
1612
|
+
if (value in seenValues) keysToRemove.add(key);
|
|
1613
|
+
else seenValues[value] = key;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
for (const key of keysToRemove) delete attributesToInclude[key];
|
|
1617
|
+
}
|
|
1618
|
+
if (attributesToInclude.role === node.tagName) delete attributesToInclude.role;
|
|
1619
|
+
for (const attr of [
|
|
1620
|
+
"aria-label",
|
|
1621
|
+
"placeholder",
|
|
1622
|
+
"title"
|
|
1623
|
+
]) if (attributesToInclude[attr] && attributesToInclude[attr].toLowerCase().trim() === text.toLowerCase().trim()) delete attributesToInclude[attr];
|
|
1624
|
+
if (Object.keys(attributesToInclude).length > 0) attributesHtmlStr = Object.entries(attributesToInclude).map(([key, value]) => `${key}=${capTextLength(value, 20)}`).join(" ");
|
|
1625
|
+
}
|
|
1626
|
+
let line = `${depthStr}${node.isNew ? `*[${node.highlightIndex}]` : `[${node.highlightIndex}]`}<${node.tagName ?? ""}`;
|
|
1627
|
+
if (attributesHtmlStr) line += ` ${attributesHtmlStr}`;
|
|
1628
|
+
/**
|
|
1629
|
+
* @edit scrollable 数据
|
|
1630
|
+
*/
|
|
1631
|
+
if (node.extra) {
|
|
1632
|
+
if (node.extra.scrollable) {
|
|
1633
|
+
let scrollDataText = "";
|
|
1634
|
+
if (node.extra.scrollData?.left) scrollDataText += `left=${node.extra.scrollData.left}, `;
|
|
1635
|
+
if (node.extra.scrollData?.top) scrollDataText += `top=${node.extra.scrollData.top}, `;
|
|
1636
|
+
if (node.extra.scrollData?.right) scrollDataText += `right=${node.extra.scrollData.right}, `;
|
|
1637
|
+
if (node.extra.scrollData?.bottom) scrollDataText += `bottom=${node.extra.scrollData.bottom}`;
|
|
1638
|
+
line += ` data-scrollable="${scrollDataText}"`;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (text) {
|
|
1642
|
+
const trimmedText = text.trim();
|
|
1643
|
+
if (!attributesHtmlStr) line += " ";
|
|
1644
|
+
line += `>${trimmedText}`;
|
|
1645
|
+
} else if (!attributesHtmlStr) line += " ";
|
|
1646
|
+
line += " />";
|
|
1647
|
+
result.push(line);
|
|
1648
|
+
}
|
|
1649
|
+
const emitSemantic = isSemantic && node.highlightIndex === void 0;
|
|
1650
|
+
const mark = emitSemantic ? result.length : -1;
|
|
1651
|
+
if (emitSemantic) {
|
|
1652
|
+
result.push(`${depthStr}<${node.tagName}>`);
|
|
1653
|
+
nextDepth += 1;
|
|
1654
|
+
}
|
|
1655
|
+
for (const child of node.children) processNode(child, nextDepth, result);
|
|
1656
|
+
if (emitSemantic) {
|
|
1657
|
+
if (result.length === mark + 1) result.pop();
|
|
1658
|
+
else result.push(`${depthStr}</${node.tagName}>`);
|
|
1659
|
+
}
|
|
1660
|
+
} else if (node.type === "text") {
|
|
1661
|
+
if (hasParentWithHighlightIndex(node)) return;
|
|
1662
|
+
if (node.parent && node.parent.type === "element" && node.parent.isVisible && node.parent.isTopElement) result.push(`${depthStr}${node.text ?? ""}`);
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
const result = [];
|
|
1666
|
+
processNode(rootNode, 0, result);
|
|
1667
|
+
return result.join("\n");
|
|
1668
|
+
}
|
|
1669
|
+
var getAllTextTillNextClickableElement = (node, maxDepth = -1) => {
|
|
1670
|
+
const textParts = [];
|
|
1671
|
+
const collectText = (currentNode, currentDepth) => {
|
|
1672
|
+
if (maxDepth !== -1 && currentDepth > maxDepth) return;
|
|
1673
|
+
if (currentNode.type === "element" && currentNode !== node && currentNode.highlightIndex !== void 0) return;
|
|
1674
|
+
if (currentNode.type === "text" && currentNode.text) textParts.push(currentNode.text);
|
|
1675
|
+
else if (currentNode.type === "element") for (const child of currentNode.children) collectText(child, currentDepth + 1);
|
|
1676
|
+
};
|
|
1677
|
+
collectText(node, 0);
|
|
1678
|
+
return textParts.join("\n").trim();
|
|
1679
|
+
};
|
|
1680
|
+
function getSelectorMap(flatTree) {
|
|
1681
|
+
const selectorMap = /* @__PURE__ */ new Map();
|
|
1682
|
+
const keys = Object.keys(flatTree.map);
|
|
1683
|
+
for (const key of keys) {
|
|
1684
|
+
const node = flatTree.map[key];
|
|
1685
|
+
if (node.isInteractive && typeof node.highlightIndex === "number") selectorMap.set(node.highlightIndex, node);
|
|
1686
|
+
}
|
|
1687
|
+
return selectorMap;
|
|
1688
|
+
}
|
|
1689
|
+
function getElementTextMap(simplifiedHTML) {
|
|
1690
|
+
const lines = simplifiedHTML.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1691
|
+
const elementTextMap = /* @__PURE__ */ new Map();
|
|
1692
|
+
for (const line of lines) {
|
|
1693
|
+
const match = /^\[(\d+)\]<[^>]+>([^<]*)/.exec(line);
|
|
1694
|
+
if (match) {
|
|
1695
|
+
const index = parseInt(match[1], 10);
|
|
1696
|
+
elementTextMap.set(index, line);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return elementTextMap;
|
|
1700
|
+
}
|
|
1701
|
+
function cleanUpHighlights() {
|
|
1702
|
+
const cleanupFunctions = window._highlightCleanupFunctions || [];
|
|
1703
|
+
for (const cleanup of cleanupFunctions) if (typeof cleanup === "function") cleanup();
|
|
1704
|
+
window._highlightCleanupFunctions = [];
|
|
1705
|
+
}
|
|
1706
|
+
window.addEventListener("popstate", () => {
|
|
1707
|
+
cleanUpHighlights();
|
|
1708
|
+
});
|
|
1709
|
+
window.addEventListener("hashchange", () => {
|
|
1710
|
+
cleanUpHighlights();
|
|
1711
|
+
});
|
|
1712
|
+
window.addEventListener("beforeunload", () => {
|
|
1713
|
+
cleanUpHighlights();
|
|
1714
|
+
});
|
|
1715
|
+
var navigation = window.navigation;
|
|
1716
|
+
if (navigation && typeof navigation.addEventListener === "function") navigation.addEventListener("navigate", () => {
|
|
1717
|
+
cleanUpHighlights();
|
|
1718
|
+
});
|
|
1719
|
+
else {
|
|
1720
|
+
let currentUrl = window.location.href;
|
|
1721
|
+
setInterval(() => {
|
|
1722
|
+
if (window.location.href !== currentUrl) {
|
|
1723
|
+
currentUrl = window.location.href;
|
|
1724
|
+
cleanUpHighlights();
|
|
1725
|
+
}
|
|
1726
|
+
}, 500);
|
|
1727
|
+
}
|
|
1728
|
+
//#endregion
|
|
1729
|
+
//#region src/browser/dom/getPageInfo.ts
|
|
1730
|
+
function getPageInfo() {
|
|
1731
|
+
const viewport_width = window.innerWidth;
|
|
1732
|
+
const viewport_height = window.innerHeight;
|
|
1733
|
+
const page_width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0);
|
|
1734
|
+
const page_height = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0);
|
|
1735
|
+
const scroll_x = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || 0;
|
|
1736
|
+
const scroll_y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;
|
|
1737
|
+
const pixels_below = Math.max(0, page_height - (window.innerHeight + scroll_y));
|
|
1738
|
+
const pixels_right = Math.max(0, page_width - (window.innerWidth + scroll_x));
|
|
1739
|
+
return {
|
|
1740
|
+
viewport_width,
|
|
1741
|
+
viewport_height,
|
|
1742
|
+
page_width,
|
|
1743
|
+
page_height,
|
|
1744
|
+
scroll_x,
|
|
1745
|
+
scroll_y,
|
|
1746
|
+
pixels_above: scroll_y,
|
|
1747
|
+
pixels_below,
|
|
1748
|
+
pages_above: viewport_height > 0 ? scroll_y / viewport_height : 0,
|
|
1749
|
+
pages_below: viewport_height > 0 ? pixels_below / viewport_height : 0,
|
|
1750
|
+
total_pages: viewport_height > 0 ? page_height / viewport_height : 0,
|
|
1751
|
+
current_page_position: scroll_y / Math.max(1, page_height - viewport_height),
|
|
1752
|
+
pixels_left: scroll_x,
|
|
1753
|
+
pixels_right
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region src/browser/patches/react.ts
|
|
1758
|
+
function patchReact(_browserController) {
|
|
1759
|
+
const reactRootElements = document.querySelectorAll("[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^=\"root-\"], [id^=\"app-\"], #adex-wrapper, #adex-root");
|
|
1760
|
+
for (const element of reactRootElements) element.setAttribute("data-page-agent-not-interactive", "true");
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* @todo (Heavy, might have false negatives) Interaction detection, if element width/height equals body offsetWidth/Height, consider it root element and non-interactive (React often attaches many events to root elements, causing false positives)
|
|
1764
|
+
*/
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/browser/visual/checkDarkMode.ts
|
|
1767
|
+
/**
|
|
1768
|
+
* A comprehensive function to determine if the page is currently in a dark theme.
|
|
1769
|
+
* Heuristic check. Only work for common patterns. Return false by default.
|
|
1770
|
+
*/
|
|
1771
|
+
function isPageDark() {
|
|
1772
|
+
try {
|
|
1773
|
+
if (hasDarkModeClass()) return true;
|
|
1774
|
+
if (hasDarkModeDataAttribute()) return true;
|
|
1775
|
+
if (isColorSchemeDark()) return true;
|
|
1776
|
+
if (isBackgroundDark()) return true;
|
|
1777
|
+
if (isMainContentBackgroundDark()) return true;
|
|
1778
|
+
if (isTextColorLight()) return true;
|
|
1779
|
+
return false;
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
console.warn("Error determining if page is dark:", error);
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Checks for common dark mode CSS classes on the html or body elements.
|
|
1787
|
+
*/
|
|
1788
|
+
function hasDarkModeClass() {
|
|
1789
|
+
const DEFAULT_DARK_MODE_CLASSES = [
|
|
1790
|
+
"dark",
|
|
1791
|
+
"dark-mode",
|
|
1792
|
+
"theme-dark",
|
|
1793
|
+
"night",
|
|
1794
|
+
"night-mode"
|
|
1795
|
+
];
|
|
1796
|
+
const htmlElement = document.documentElement;
|
|
1797
|
+
const bodyElement = document.body || document.documentElement;
|
|
1798
|
+
for (const className of DEFAULT_DARK_MODE_CLASSES) if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) return true;
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Some UI frameworks use data attributes to indicate theme
|
|
1803
|
+
*/
|
|
1804
|
+
function hasDarkModeDataAttribute() {
|
|
1805
|
+
const htmlElement = document.documentElement;
|
|
1806
|
+
const bodyElement = document.body || document.documentElement;
|
|
1807
|
+
for (const attr of [
|
|
1808
|
+
"data-theme",
|
|
1809
|
+
"data-color-mode",
|
|
1810
|
+
"data-bs-theme",
|
|
1811
|
+
"data-mui-color-scheme"
|
|
1812
|
+
]) {
|
|
1813
|
+
const bodyValue = bodyElement?.getAttribute(attr);
|
|
1814
|
+
const htmlValue = htmlElement.getAttribute(attr);
|
|
1815
|
+
if (bodyValue?.toLowerCase() === "dark" || htmlValue?.toLowerCase() === "dark") return true;
|
|
1816
|
+
}
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Checks the CSS `color-scheme` property and `<meta name="color-scheme">` tag.
|
|
1821
|
+
* Only "dark"/"only dark" counts as dark; "light dark" is ambiguous and ignored.
|
|
1822
|
+
*/
|
|
1823
|
+
function isColorSchemeDark() {
|
|
1824
|
+
const metaContent = document.querySelector("meta[name=\"color-scheme\"]")?.content.toLowerCase();
|
|
1825
|
+
if (metaContent === "dark" || metaContent === "only dark") return true;
|
|
1826
|
+
const colorScheme = window.getComputedStyle(document.documentElement).getPropertyValue("color-scheme").trim().toLowerCase();
|
|
1827
|
+
return colorScheme === "dark" || colorScheme === "only dark";
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Checks the background color of the body element to determine if the page is dark.
|
|
1831
|
+
*/
|
|
1832
|
+
function isBackgroundDark() {
|
|
1833
|
+
const htmlStyle = window.getComputedStyle(document.documentElement);
|
|
1834
|
+
const bodyStyle = window.getComputedStyle(document.body || document.documentElement);
|
|
1835
|
+
const htmlBgColor = htmlStyle.backgroundColor;
|
|
1836
|
+
const bodyBgColor = bodyStyle.backgroundColor;
|
|
1837
|
+
if (isColorDark(bodyBgColor)) return true;
|
|
1838
|
+
else if (bodyBgColor === "transparent" || bodyBgColor.startsWith("rgba(0, 0, 0, 0)")) return isColorDark(htmlBgColor);
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Checks if the text color on the body is light, which implies a dark background.
|
|
1843
|
+
*/
|
|
1844
|
+
function isTextColorLight() {
|
|
1845
|
+
/** Luminance (0-255) above which body text is considered light */
|
|
1846
|
+
const LIGHT_TEXT_LUMINANCE = 200;
|
|
1847
|
+
const luminance = getLuminance(window.getComputedStyle(document.body || document.documentElement).color);
|
|
1848
|
+
return luminance !== null && luminance > LIGHT_TEXT_LUMINANCE;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Checks the background color of major layout elements (#app, #root, etc.).
|
|
1852
|
+
* Many SPAs render into a container that may have its own dark background while
|
|
1853
|
+
* <body> remains transparent.
|
|
1854
|
+
*/
|
|
1855
|
+
function isMainContentBackgroundDark() {
|
|
1856
|
+
const { innerWidth: vw, innerHeight: vh } = window;
|
|
1857
|
+
const minArea = vw * vh * .5;
|
|
1858
|
+
for (const selector of [
|
|
1859
|
+
"#app",
|
|
1860
|
+
"#root",
|
|
1861
|
+
"#__next"
|
|
1862
|
+
]) {
|
|
1863
|
+
const el = document.querySelector(selector);
|
|
1864
|
+
if (!el) continue;
|
|
1865
|
+
const rect = el.getBoundingClientRect();
|
|
1866
|
+
if (rect.width * rect.height < minArea) continue;
|
|
1867
|
+
if (isColorDark(window.getComputedStyle(el).backgroundColor)) return true;
|
|
1868
|
+
}
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Parses an RGB or RGBA color string and returns an object with r, g, b properties.
|
|
1873
|
+
* @param {string} colorString - e.g., "rgb(34, 34, 34)" or "rgba(0, 0, 0, 0.5)"
|
|
1874
|
+
* @returns {{r: number, g: number, b: number}|null}
|
|
1875
|
+
*/
|
|
1876
|
+
function parseRgbColor(colorString) {
|
|
1877
|
+
const rgbMatch = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colorString);
|
|
1878
|
+
if (!rgbMatch) return null;
|
|
1879
|
+
return {
|
|
1880
|
+
r: parseInt(rgbMatch[1]),
|
|
1881
|
+
g: parseInt(rgbMatch[2]),
|
|
1882
|
+
b: parseInt(rgbMatch[3])
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Calculates the perceived luminance (0-255) of a CSS color string.
|
|
1887
|
+
* @param {string} colorString - e.g., "rgb(50, 50, 50)" or "rgba(0, 0, 0, 0.5)"
|
|
1888
|
+
* @returns {number|null} - The luminance, or null if the color is transparent or unparseable.
|
|
1889
|
+
*/
|
|
1890
|
+
function getLuminance(colorString) {
|
|
1891
|
+
if (!colorString || colorString === "transparent" || colorString.startsWith("rgba(0, 0, 0, 0)")) return null;
|
|
1892
|
+
const rgb = parseRgbColor(colorString);
|
|
1893
|
+
if (!rgb) return null;
|
|
1894
|
+
return .299 * rgb.r + .587 * rgb.g + .114 * rgb.b;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Determines if a color is "dark" based on its calculated luminance.
|
|
1898
|
+
* @param {string} colorString - The CSS color string (e.g., "rgb(50, 50, 50)").
|
|
1899
|
+
* @param {number} threshold - A value between 0 and 255. Colors with luminance below this will be considered dark. Default is 128.
|
|
1900
|
+
*/
|
|
1901
|
+
function isColorDark(colorString, threshold = 128) {
|
|
1902
|
+
const luminance = getLuminance(colorString);
|
|
1903
|
+
return luminance !== null && luminance < threshold;
|
|
1904
|
+
}
|
|
1905
|
+
var init_checkDarkMode = __esmMin((() => {}));
|
|
1906
|
+
//#endregion
|
|
1907
|
+
//#region src/browser/visual/SimulatorMask.module.css
|
|
1908
|
+
var wrapper$1, visible, SimulatorMask_module_default;
|
|
1909
|
+
var init_SimulatorMask_module = __esmMin((() => {
|
|
1910
|
+
wrapper$1 = "_wrapper_ur79v_1";
|
|
1911
|
+
visible = "_visible_ur79v_11";
|
|
1912
|
+
SimulatorMask_module_default = {
|
|
1913
|
+
wrapper: wrapper$1,
|
|
1914
|
+
visible
|
|
1915
|
+
};
|
|
1916
|
+
}));
|
|
1917
|
+
//#endregion
|
|
1918
|
+
//#region src/browser/visual/cursor.module.css
|
|
1919
|
+
var cursor, cursorBorder, cursorFilling, cursorRipple, clicking, cursor_module_default;
|
|
1920
|
+
var init_cursor_module = __esmMin((() => {
|
|
1921
|
+
cursor = "_cursor_1dgwb_2";
|
|
1922
|
+
cursorBorder = "_cursorBorder_1dgwb_10";
|
|
1923
|
+
cursorFilling = "_cursorFilling_1dgwb_25";
|
|
1924
|
+
cursorRipple = "_cursorRipple_1dgwb_39";
|
|
1925
|
+
clicking = "_clicking_1dgwb_57";
|
|
1926
|
+
cursor_module_default = {
|
|
1927
|
+
cursor,
|
|
1928
|
+
cursorBorder,
|
|
1929
|
+
cursorFilling,
|
|
1930
|
+
cursorRipple,
|
|
1931
|
+
clicking,
|
|
1932
|
+
"cursor-ripple": "_cursor-ripple_1dgwb_1"
|
|
1933
|
+
};
|
|
1934
|
+
}));
|
|
1935
|
+
//#endregion
|
|
1936
|
+
//#region src/browser/visual/SimulatorMask.ts
|
|
1937
|
+
var SimulatorMask_exports = /* @__PURE__ */ __exportAll({ SimulatorMask: () => SimulatorMask });
|
|
1938
|
+
var SimulatorMask;
|
|
1939
|
+
var init_SimulatorMask = __esmMin((() => {
|
|
1940
|
+
init_checkDarkMode();
|
|
1941
|
+
init_SimulatorMask_module();
|
|
1942
|
+
init_cursor_module();
|
|
1943
|
+
SimulatorMask = class extends EventTarget {
|
|
1944
|
+
shown = false;
|
|
1945
|
+
wrapper = document.createElement("div");
|
|
1946
|
+
motion = null;
|
|
1947
|
+
#disposed = false;
|
|
1948
|
+
#cursor = document.createElement("div");
|
|
1949
|
+
#currentCursorX = 0;
|
|
1950
|
+
#currentCursorY = 0;
|
|
1951
|
+
#targetCursorX = 0;
|
|
1952
|
+
#targetCursorY = 0;
|
|
1953
|
+
constructor() {
|
|
1954
|
+
super();
|
|
1955
|
+
this.wrapper.id = "page-agent-runtime_simulator-mask";
|
|
1956
|
+
this.wrapper.className = SimulatorMask_module_default.wrapper;
|
|
1957
|
+
this.wrapper.setAttribute("data-browser-use-ignore", "true");
|
|
1958
|
+
this.wrapper.setAttribute("data-page-agent-ignore", "true");
|
|
1959
|
+
try {
|
|
1960
|
+
const motion = new Motion({
|
|
1961
|
+
mode: isPageDark() ? "dark" : "light",
|
|
1962
|
+
styles: {
|
|
1963
|
+
position: "absolute",
|
|
1964
|
+
inset: "0"
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
this.motion = motion;
|
|
1968
|
+
this.wrapper.appendChild(motion.element);
|
|
1969
|
+
motion.autoResize(this.wrapper);
|
|
1970
|
+
} catch (e) {
|
|
1971
|
+
console.warn("[SimulatorMask] Motion overlay unavailable:", e);
|
|
1972
|
+
}
|
|
1973
|
+
this.wrapper.addEventListener("click", (e) => {
|
|
1974
|
+
e.stopPropagation();
|
|
1975
|
+
e.preventDefault();
|
|
1976
|
+
});
|
|
1977
|
+
this.wrapper.addEventListener("mousedown", (e) => {
|
|
1978
|
+
e.stopPropagation();
|
|
1979
|
+
e.preventDefault();
|
|
1980
|
+
});
|
|
1981
|
+
this.wrapper.addEventListener("mouseup", (e) => {
|
|
1982
|
+
e.stopPropagation();
|
|
1983
|
+
e.preventDefault();
|
|
1984
|
+
});
|
|
1985
|
+
this.wrapper.addEventListener("mousemove", (e) => {
|
|
1986
|
+
e.stopPropagation();
|
|
1987
|
+
e.preventDefault();
|
|
1988
|
+
});
|
|
1989
|
+
this.wrapper.addEventListener("wheel", (e) => {
|
|
1990
|
+
e.stopPropagation();
|
|
1991
|
+
e.preventDefault();
|
|
1992
|
+
});
|
|
1993
|
+
this.wrapper.addEventListener("keydown", (e) => {
|
|
1994
|
+
e.stopPropagation();
|
|
1995
|
+
e.preventDefault();
|
|
1996
|
+
});
|
|
1997
|
+
this.wrapper.addEventListener("keyup", (e) => {
|
|
1998
|
+
e.stopPropagation();
|
|
1999
|
+
e.preventDefault();
|
|
2000
|
+
});
|
|
2001
|
+
this.#createCursor();
|
|
2002
|
+
document.body.appendChild(this.wrapper);
|
|
2003
|
+
this.#moveCursorToTarget();
|
|
2004
|
+
const movePointerToListener = (event) => {
|
|
2005
|
+
const { x, y } = event.detail;
|
|
2006
|
+
this.setCursorPosition(x, y);
|
|
2007
|
+
};
|
|
2008
|
+
const clickPointerListener = () => {
|
|
2009
|
+
this.triggerClickAnimation();
|
|
2010
|
+
};
|
|
2011
|
+
const enablePassThroughListener = () => {
|
|
2012
|
+
this.wrapper.style.pointerEvents = "none";
|
|
2013
|
+
};
|
|
2014
|
+
const disablePassThroughListener = () => {
|
|
2015
|
+
this.wrapper.style.pointerEvents = "auto";
|
|
2016
|
+
};
|
|
2017
|
+
window.addEventListener("PageAgent::MovePointerTo", movePointerToListener);
|
|
2018
|
+
window.addEventListener("PageAgent::ClickPointer", clickPointerListener);
|
|
2019
|
+
window.addEventListener("PageAgent::EnablePassThrough", enablePassThroughListener);
|
|
2020
|
+
window.addEventListener("PageAgent::DisablePassThrough", disablePassThroughListener);
|
|
2021
|
+
this.addEventListener("dispose", () => {
|
|
2022
|
+
window.removeEventListener("PageAgent::MovePointerTo", movePointerToListener);
|
|
2023
|
+
window.removeEventListener("PageAgent::ClickPointer", clickPointerListener);
|
|
2024
|
+
window.removeEventListener("PageAgent::EnablePassThrough", enablePassThroughListener);
|
|
2025
|
+
window.removeEventListener("PageAgent::DisablePassThrough", disablePassThroughListener);
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
#createCursor() {
|
|
2029
|
+
this.#cursor.className = cursor_module_default.cursor;
|
|
2030
|
+
const rippleContainer = document.createElement("div");
|
|
2031
|
+
rippleContainer.className = cursor_module_default.cursorRipple;
|
|
2032
|
+
this.#cursor.appendChild(rippleContainer);
|
|
2033
|
+
const fillingLayer = document.createElement("div");
|
|
2034
|
+
fillingLayer.className = cursor_module_default.cursorFilling;
|
|
2035
|
+
this.#cursor.appendChild(fillingLayer);
|
|
2036
|
+
const borderLayer = document.createElement("div");
|
|
2037
|
+
borderLayer.className = cursor_module_default.cursorBorder;
|
|
2038
|
+
this.#cursor.appendChild(borderLayer);
|
|
2039
|
+
this.wrapper.appendChild(this.#cursor);
|
|
2040
|
+
}
|
|
2041
|
+
#moveCursorToTarget() {
|
|
2042
|
+
if (this.#disposed) return;
|
|
2043
|
+
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * .2;
|
|
2044
|
+
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * .2;
|
|
2045
|
+
const xDistance = Math.abs(newX - this.#targetCursorX);
|
|
2046
|
+
if (xDistance > 0) {
|
|
2047
|
+
if (xDistance < 2) this.#currentCursorX = this.#targetCursorX;
|
|
2048
|
+
else this.#currentCursorX = newX;
|
|
2049
|
+
this.#cursor.style.left = `${this.#currentCursorX}px`;
|
|
2050
|
+
}
|
|
2051
|
+
const yDistance = Math.abs(newY - this.#targetCursorY);
|
|
2052
|
+
if (yDistance > 0) {
|
|
2053
|
+
if (yDistance < 2) this.#currentCursorY = this.#targetCursorY;
|
|
2054
|
+
else this.#currentCursorY = newY;
|
|
2055
|
+
this.#cursor.style.top = `${this.#currentCursorY}px`;
|
|
2056
|
+
}
|
|
2057
|
+
requestAnimationFrame(() => this.#moveCursorToTarget());
|
|
2058
|
+
}
|
|
2059
|
+
setCursorPosition(x, y) {
|
|
2060
|
+
if (this.#disposed) return;
|
|
2061
|
+
this.#targetCursorX = x;
|
|
2062
|
+
this.#targetCursorY = y;
|
|
2063
|
+
}
|
|
2064
|
+
triggerClickAnimation() {
|
|
2065
|
+
if (this.#disposed) return;
|
|
2066
|
+
this.#cursor.classList.remove(cursor_module_default.clicking);
|
|
2067
|
+
this.#cursor.offsetHeight;
|
|
2068
|
+
this.#cursor.classList.add(cursor_module_default.clicking);
|
|
2069
|
+
}
|
|
2070
|
+
show() {
|
|
2071
|
+
if (this.shown || this.#disposed) return;
|
|
2072
|
+
this.shown = true;
|
|
2073
|
+
this.motion?.start();
|
|
2074
|
+
this.motion?.fadeIn();
|
|
2075
|
+
this.wrapper.classList.add(SimulatorMask_module_default.visible);
|
|
2076
|
+
this.#currentCursorX = window.innerWidth / 2;
|
|
2077
|
+
this.#currentCursorY = window.innerHeight / 2;
|
|
2078
|
+
this.#targetCursorX = this.#currentCursorX;
|
|
2079
|
+
this.#targetCursorY = this.#currentCursorY;
|
|
2080
|
+
this.#cursor.style.left = `${this.#currentCursorX}px`;
|
|
2081
|
+
this.#cursor.style.top = `${this.#currentCursorY}px`;
|
|
2082
|
+
}
|
|
2083
|
+
hide() {
|
|
2084
|
+
if (!this.shown || this.#disposed) return;
|
|
2085
|
+
this.shown = false;
|
|
2086
|
+
this.motion?.fadeOut();
|
|
2087
|
+
this.motion?.pause();
|
|
2088
|
+
this.#cursor.classList.remove(cursor_module_default.clicking);
|
|
2089
|
+
setTimeout(() => {
|
|
2090
|
+
this.wrapper.classList.remove(SimulatorMask_module_default.visible);
|
|
2091
|
+
}, 800);
|
|
2092
|
+
}
|
|
2093
|
+
dispose() {
|
|
2094
|
+
this.#disposed = true;
|
|
2095
|
+
this.motion?.dispose();
|
|
2096
|
+
this.wrapper.remove();
|
|
2097
|
+
this.dispatchEvent(new Event("dispose"));
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
}));
|
|
2101
|
+
//#endregion
|
|
2102
|
+
//#region src/browser/BrowserController.ts
|
|
2103
|
+
/**
|
|
2104
|
+
* Copyright (C) 2025 Alibaba Group Holding Limited
|
|
2105
|
+
* All rights reserved.
|
|
2106
|
+
*
|
|
2107
|
+
* BrowserController - Manages DOM operations and element interactions.
|
|
2108
|
+
* Designed to be independent of LLM and can be tested in unit tests.
|
|
2109
|
+
* All public methods are async for potential remote calling support.
|
|
2110
|
+
*/
|
|
2111
|
+
/**
|
|
2112
|
+
* BrowserController manages DOM state and element interactions.
|
|
2113
|
+
* It provides async methods for all DOM operations, keeping state isolated.
|
|
2114
|
+
*
|
|
2115
|
+
* @lifecycle
|
|
2116
|
+
* - beforeUpdate: Emitted before the DOM tree is updated.
|
|
2117
|
+
* - afterUpdate: Emitted after the DOM tree is updated.
|
|
2118
|
+
*/
|
|
2119
|
+
var BrowserController = class extends EventTarget {
|
|
2120
|
+
config;
|
|
2121
|
+
/** Corresponds to eval_page in browser-use */
|
|
2122
|
+
flatTree = null;
|
|
2123
|
+
/**
|
|
2124
|
+
* All highlighted index-mapped interactive elements
|
|
2125
|
+
* Corresponds to DOMState.selector_map in browser-use
|
|
2126
|
+
*/
|
|
2127
|
+
selectorMap = /* @__PURE__ */ new Map();
|
|
2128
|
+
/** Index -> element text description mapping */
|
|
2129
|
+
elementTextMap = /* @__PURE__ */ new Map();
|
|
2130
|
+
/**
|
|
2131
|
+
* Simplified HTML for LLM consumption.
|
|
2132
|
+
* Corresponds to clickable_elements_to_string in browser-use
|
|
2133
|
+
*/
|
|
2134
|
+
simplifiedHTML = "<EMPTY>";
|
|
2135
|
+
/** last time the tree was updated */
|
|
2136
|
+
lastTimeUpdate = 0;
|
|
2137
|
+
/** Whether the tree has been indexed at least once */
|
|
2138
|
+
isIndexed = false;
|
|
2139
|
+
/** Visual mask overlay for blocking user interaction during automation */
|
|
2140
|
+
mask = null;
|
|
2141
|
+
maskReady = null;
|
|
2142
|
+
disposed = false;
|
|
2143
|
+
constructor(config = {}) {
|
|
2144
|
+
super();
|
|
2145
|
+
this.config = config;
|
|
2146
|
+
patchReact(this);
|
|
2147
|
+
if (config.enableMask) this.initMask();
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
|
|
2151
|
+
*/
|
|
2152
|
+
initMask() {
|
|
2153
|
+
if (this.disposed || this.maskReady !== null) return;
|
|
2154
|
+
this.maskReady = (async () => {
|
|
2155
|
+
const { SimulatorMask } = await Promise.resolve().then(() => (init_SimulatorMask(), SimulatorMask_exports));
|
|
2156
|
+
if (this.disposed) return;
|
|
2157
|
+
const mask = new SimulatorMask();
|
|
2158
|
+
if (this.disposed) {
|
|
2159
|
+
mask.dispose();
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
this.mask = mask;
|
|
2163
|
+
})();
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Get current page URL
|
|
2167
|
+
*/
|
|
2168
|
+
async getCurrentUrl() {
|
|
2169
|
+
return window.location.href;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Get last tree update timestamp
|
|
2173
|
+
*/
|
|
2174
|
+
async getLastUpdateTime() {
|
|
2175
|
+
return this.lastTimeUpdate;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Get structured browser state for LLM consumption.
|
|
2179
|
+
* Automatically calls updateTree() to refresh the DOM state.
|
|
2180
|
+
*/
|
|
2181
|
+
async getBrowserState() {
|
|
2182
|
+
const url = window.location.href;
|
|
2183
|
+
const title = document.title;
|
|
2184
|
+
const pi = getPageInfo();
|
|
2185
|
+
const viewportExpansion = resolveViewportExpansion(this.config.viewportExpansion);
|
|
2186
|
+
await this.updateTree();
|
|
2187
|
+
const content = this.simplifiedHTML;
|
|
2188
|
+
return {
|
|
2189
|
+
url,
|
|
2190
|
+
title,
|
|
2191
|
+
header: `${`Current Page: [${title}](${url})`}\n${`Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`}\n\n${viewportExpansion === -1 ? "Interactive elements from top layer of the current page (full page):" : "Interactive elements from top layer of the current page inside the viewport:"}\n\n${pi.pixels_above > 4 && viewportExpansion !== -1 ? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...` : "[Start of page]"}`,
|
|
2192
|
+
content,
|
|
2193
|
+
footer: pi.pixels_below > 4 && viewportExpansion !== -1 ? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...` : "[End of page]"
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Update DOM tree, returns simplified HTML for LLM.
|
|
2198
|
+
* This is the main method to refresh the page state.
|
|
2199
|
+
* Automatically bypasses mask during DOM extraction if enabled.
|
|
2200
|
+
*/
|
|
2201
|
+
async updateTree() {
|
|
2202
|
+
this.dispatchEvent(new Event("beforeUpdate"));
|
|
2203
|
+
this.lastTimeUpdate = Date.now();
|
|
2204
|
+
if (this.mask) this.mask.wrapper.style.pointerEvents = "none";
|
|
2205
|
+
cleanUpHighlights();
|
|
2206
|
+
const blacklist = [...this.config.interactiveBlacklist || [], ...Array.from(document.querySelectorAll("[data-page-agent-not-interactive]"))];
|
|
2207
|
+
this.flatTree = getFlatTree({
|
|
2208
|
+
...this.config,
|
|
2209
|
+
interactiveBlacklist: blacklist
|
|
2210
|
+
});
|
|
2211
|
+
this.simplifiedHTML = flatTreeToString(this.flatTree, this.config.includeAttributes, this.config.keepSemanticTags);
|
|
2212
|
+
this.selectorMap.clear();
|
|
2213
|
+
this.selectorMap = getSelectorMap(this.flatTree);
|
|
2214
|
+
this.elementTextMap.clear();
|
|
2215
|
+
this.elementTextMap = getElementTextMap(this.simplifiedHTML);
|
|
2216
|
+
this.isIndexed = true;
|
|
2217
|
+
if (this.mask) this.mask.wrapper.style.pointerEvents = "auto";
|
|
2218
|
+
this.dispatchEvent(new Event("afterUpdate"));
|
|
2219
|
+
return this.simplifiedHTML;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Clean up all element highlights
|
|
2223
|
+
*/
|
|
2224
|
+
async cleanUpHighlights() {
|
|
2225
|
+
console.log("[BrowserController] cleanUpHighlights");
|
|
2226
|
+
cleanUpHighlights();
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Ensure the tree has been indexed before any index-based operation.
|
|
2230
|
+
* Throws if updateTree() hasn't been called yet.
|
|
2231
|
+
*/
|
|
2232
|
+
assertIndexed() {
|
|
2233
|
+
if (!this.isIndexed) throw new Error("DOM tree not indexed yet. Can not perform actions on elements.");
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Click element by index
|
|
2237
|
+
*/
|
|
2238
|
+
async clickElement(index) {
|
|
2239
|
+
try {
|
|
2240
|
+
this.assertIndexed();
|
|
2241
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
2242
|
+
const elemText = this.elementTextMap.get(index);
|
|
2243
|
+
await clickElement(element);
|
|
2244
|
+
if (isAnchorElement(element) && element.target === "_blank") return {
|
|
2245
|
+
success: true,
|
|
2246
|
+
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`
|
|
2247
|
+
};
|
|
2248
|
+
return {
|
|
2249
|
+
success: true,
|
|
2250
|
+
message: `✅ Clicked element (${elemText ?? index}).`
|
|
2251
|
+
};
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
return {
|
|
2254
|
+
success: false,
|
|
2255
|
+
message: `❌ Failed to click element: ${error}`
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Input text into element by index
|
|
2261
|
+
*/
|
|
2262
|
+
async inputText(index, text) {
|
|
2263
|
+
try {
|
|
2264
|
+
this.assertIndexed();
|
|
2265
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
2266
|
+
const elemText = this.elementTextMap.get(index);
|
|
2267
|
+
await inputTextElement(element, text);
|
|
2268
|
+
return {
|
|
2269
|
+
success: true,
|
|
2270
|
+
message: `✅ Input text (${text}) into element (${elemText ?? index}).`
|
|
2271
|
+
};
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return {
|
|
2274
|
+
success: false,
|
|
2275
|
+
message: `❌ Failed to input text: ${error}`
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Select dropdown option by index and option text
|
|
2281
|
+
*/
|
|
2282
|
+
async selectOption(index, optionText) {
|
|
2283
|
+
try {
|
|
2284
|
+
this.assertIndexed();
|
|
2285
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
2286
|
+
const elemText = this.elementTextMap.get(index);
|
|
2287
|
+
await selectOptionElement(element, optionText);
|
|
2288
|
+
return {
|
|
2289
|
+
success: true,
|
|
2290
|
+
message: `✅ Selected option (${optionText}) in element (${elemText ?? index}).`
|
|
2291
|
+
};
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
return {
|
|
2294
|
+
success: false,
|
|
2295
|
+
message: `❌ Failed to select option: ${error}`
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Scroll vertically
|
|
2301
|
+
*/
|
|
2302
|
+
async scroll(options) {
|
|
2303
|
+
try {
|
|
2304
|
+
const { down, numPages, pixels, index } = options;
|
|
2305
|
+
this.assertIndexed();
|
|
2306
|
+
return {
|
|
2307
|
+
success: true,
|
|
2308
|
+
message: await scrollVertically((pixels ?? numPages * window.innerHeight) * (down ? 1 : -1), index !== void 0 ? getElementByIndex(this.selectorMap, index) : null)
|
|
2309
|
+
};
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
return {
|
|
2312
|
+
success: false,
|
|
2313
|
+
message: `❌ Failed to scroll: ${error}`
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Scroll horizontally
|
|
2319
|
+
*/
|
|
2320
|
+
async scrollHorizontally(options) {
|
|
2321
|
+
try {
|
|
2322
|
+
const { right, pixels, index } = options;
|
|
2323
|
+
this.assertIndexed();
|
|
2324
|
+
return {
|
|
2325
|
+
success: true,
|
|
2326
|
+
message: await scrollHorizontally(pixels * (right ? 1 : -1), index !== void 0 ? getElementByIndex(this.selectorMap, index) : null)
|
|
2327
|
+
};
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
return {
|
|
2330
|
+
success: false,
|
|
2331
|
+
message: `❌ Failed to scroll horizontally: ${error}`
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Execute arbitrary JavaScript on the page.
|
|
2337
|
+
* The optional `signal` is exposed to the script scope so cooperative code
|
|
2338
|
+
* can abort promptly when the task is stopped.
|
|
2339
|
+
*/
|
|
2340
|
+
async executeJavascript(script, signal) {
|
|
2341
|
+
try {
|
|
2342
|
+
return {
|
|
2343
|
+
success: true,
|
|
2344
|
+
message: `✅ Executed JavaScript. Result: ${await (0, eval)(`(async (signal) => { ${script} })`)(signal)}`
|
|
2345
|
+
};
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
return {
|
|
2348
|
+
success: false,
|
|
2349
|
+
message: `❌ Error executing JavaScript: ${error}`
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Show the visual mask overlay.
|
|
2355
|
+
* Only works after mask is setup.
|
|
2356
|
+
*/
|
|
2357
|
+
async showMask() {
|
|
2358
|
+
if (this.disposed) return;
|
|
2359
|
+
await this.maskReady;
|
|
2360
|
+
if (this.disposed) return;
|
|
2361
|
+
this.mask?.show();
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Hide the visual mask overlay.
|
|
2365
|
+
* Only works after mask is setup.
|
|
2366
|
+
*/
|
|
2367
|
+
async hideMask() {
|
|
2368
|
+
if (this.disposed) return;
|
|
2369
|
+
await this.maskReady;
|
|
2370
|
+
if (this.disposed) return;
|
|
2371
|
+
this.mask?.hide();
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Dispose and clean up resources
|
|
2375
|
+
*/
|
|
2376
|
+
dispose() {
|
|
2377
|
+
if (this.disposed) return;
|
|
2378
|
+
this.disposed = true;
|
|
2379
|
+
const errors = [];
|
|
2380
|
+
const cleanUp = (callback) => {
|
|
2381
|
+
try {
|
|
2382
|
+
callback();
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
errors.push(error);
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
cleanUp(() => cleanUpHighlights());
|
|
2388
|
+
cleanUp(() => {
|
|
2389
|
+
this.flatTree = null;
|
|
2390
|
+
this.selectorMap.clear();
|
|
2391
|
+
this.elementTextMap.clear();
|
|
2392
|
+
this.simplifiedHTML = "<EMPTY>";
|
|
2393
|
+
this.isIndexed = false;
|
|
2394
|
+
});
|
|
2395
|
+
cleanUp(() => this.mask?.dispose());
|
|
2396
|
+
this.mask = null;
|
|
2397
|
+
if (errors.length === 1) throw errors[0];
|
|
2398
|
+
if (errors.length > 1) throw new AggregateError(errors, "BrowserController disposal failed");
|
|
2399
|
+
}
|
|
2400
|
+
};
|
|
2401
|
+
var locales = {
|
|
2402
|
+
"en-US": { ui: {
|
|
2403
|
+
ready: "Ready",
|
|
2404
|
+
thinking: "Thinking...",
|
|
2405
|
+
taskInput: "Enter new task, describe steps in detail, press Enter to submit",
|
|
2406
|
+
userAnswerPrompt: "Please answer the question above, press Enter to submit",
|
|
2407
|
+
taskTerminated: "Task terminated",
|
|
2408
|
+
taskCompleted: "Task completed",
|
|
2409
|
+
userAnswer: "User answer: {{input}}",
|
|
2410
|
+
question: "Question: {{question}}",
|
|
2411
|
+
waitingPlaceholder: "Waiting for task to start...",
|
|
2412
|
+
stop: "Stop",
|
|
2413
|
+
close: "Close",
|
|
2414
|
+
expand: "Expand history",
|
|
2415
|
+
collapse: "Collapse history",
|
|
2416
|
+
step: "Step {{number}}",
|
|
2417
|
+
tools: {
|
|
2418
|
+
clicking: "Clicking element [{{index}}]...",
|
|
2419
|
+
inputting: "Inputting text to element [{{index}}]...",
|
|
2420
|
+
selecting: "Selecting option \"{{text}}\"...",
|
|
2421
|
+
scrolling: "Scrolling page...",
|
|
2422
|
+
waiting: "Waiting {{seconds}} seconds...",
|
|
2423
|
+
askingUser: "Asking user...",
|
|
2424
|
+
done: "Task done",
|
|
2425
|
+
clicked: "🖱️ Clicked element [{{index}}]",
|
|
2426
|
+
inputted: "⌨️ Inputted text \"{{text}}\"",
|
|
2427
|
+
selected: "☑️ Selected option \"{{text}}\"",
|
|
2428
|
+
scrolled: "🛞 Page scrolled",
|
|
2429
|
+
waited: "⌛️ Wait completed",
|
|
2430
|
+
executing: "Executing {{toolName}}...",
|
|
2431
|
+
resultSuccess: "success",
|
|
2432
|
+
resultFailure: "failed",
|
|
2433
|
+
resultError: "error"
|
|
2434
|
+
},
|
|
2435
|
+
errors: {
|
|
2436
|
+
elementNotFound: "No interactive element found at index {{index}}",
|
|
2437
|
+
taskRequired: "Task description is required",
|
|
2438
|
+
executionFailed: "Task execution failed",
|
|
2439
|
+
notInputElement: "Element is not an input or textarea",
|
|
2440
|
+
notSelectElement: "Element is not a select element",
|
|
2441
|
+
optionNotFound: "Option \"{{text}}\" not found"
|
|
2442
|
+
}
|
|
2443
|
+
} },
|
|
2444
|
+
"zh-CN": { ui: {
|
|
2445
|
+
ready: "准备就绪",
|
|
2446
|
+
thinking: "正在思考...",
|
|
2447
|
+
taskInput: "输入新任务,详细描述步骤,回车提交",
|
|
2448
|
+
userAnswerPrompt: "请回答上面问题,回车提交",
|
|
2449
|
+
taskTerminated: "任务已终止",
|
|
2450
|
+
taskCompleted: "任务结束",
|
|
2451
|
+
userAnswer: "用户回答: {{input}}",
|
|
2452
|
+
question: "询问: {{question}}",
|
|
2453
|
+
waitingPlaceholder: "等待任务开始...",
|
|
2454
|
+
stop: "终止",
|
|
2455
|
+
close: "关闭",
|
|
2456
|
+
expand: "展开历史",
|
|
2457
|
+
collapse: "收起历史",
|
|
2458
|
+
step: "步骤 {{number}}",
|
|
2459
|
+
tools: {
|
|
2460
|
+
clicking: "正在点击元素 [{{index}}]...",
|
|
2461
|
+
inputting: "正在输入文本到元素 [{{index}}]...",
|
|
2462
|
+
selecting: "正在选择选项 \"{{text}}\"...",
|
|
2463
|
+
scrolling: "正在滚动页面...",
|
|
2464
|
+
waiting: "等待 {{seconds}} 秒...",
|
|
2465
|
+
askingUser: "正在询问用户...",
|
|
2466
|
+
done: "结束任务",
|
|
2467
|
+
clicked: "🖱️ 已点击元素 [{{index}}]",
|
|
2468
|
+
inputted: "⌨️ 已输入文本 \"{{text}}\"",
|
|
2469
|
+
selected: "☑️ 已选择选项 \"{{text}}\"",
|
|
2470
|
+
scrolled: "🛞 页面滚动完成",
|
|
2471
|
+
waited: "⌛️ 等待完成",
|
|
2472
|
+
executing: "正在执行 {{toolName}}...",
|
|
2473
|
+
resultSuccess: "成功",
|
|
2474
|
+
resultFailure: "失败",
|
|
2475
|
+
resultError: "错误"
|
|
2476
|
+
},
|
|
2477
|
+
errors: {
|
|
2478
|
+
elementNotFound: "未找到索引为 {{index}} 的交互元素",
|
|
2479
|
+
taskRequired: "任务描述不能为空",
|
|
2480
|
+
executionFailed: "任务执行失败",
|
|
2481
|
+
notInputElement: "元素不是输入框或文本域",
|
|
2482
|
+
notSelectElement: "元素不是选择框",
|
|
2483
|
+
optionNotFound: "未找到选项 \"{{text}}\""
|
|
2484
|
+
}
|
|
2485
|
+
} }
|
|
2486
|
+
};
|
|
2487
|
+
//#endregion
|
|
2488
|
+
//#region src/ui/i18n/index.ts
|
|
2489
|
+
var I18n = class {
|
|
2490
|
+
language;
|
|
2491
|
+
translations;
|
|
2492
|
+
constructor(language = "en-US") {
|
|
2493
|
+
this.language = language in locales ? language : "en-US";
|
|
2494
|
+
this.translations = locales[this.language];
|
|
2495
|
+
}
|
|
2496
|
+
t(key, params) {
|
|
2497
|
+
const value = this.getNestedValue(this.translations, key);
|
|
2498
|
+
if (!value) {
|
|
2499
|
+
console.warn(`Translation key "${key}" not found for language "${this.language}"`);
|
|
2500
|
+
return key;
|
|
2501
|
+
}
|
|
2502
|
+
if (params) return this.interpolate(value, params);
|
|
2503
|
+
return value;
|
|
2504
|
+
}
|
|
2505
|
+
getNestedValue(obj, path) {
|
|
2506
|
+
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
2507
|
+
}
|
|
2508
|
+
interpolate(template, params) {
|
|
2509
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
2510
|
+
return params[key] != null ? params[key].toString() : match;
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
getLanguage() {
|
|
2514
|
+
return this.language;
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
//#endregion
|
|
2518
|
+
//#region src/ui/utils.ts
|
|
2519
|
+
function truncate$1(text, maxLength) {
|
|
2520
|
+
if (text.length > maxLength) return text.substring(0, maxLength) + "...";
|
|
2521
|
+
return text;
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Escape HTML special characters to prevent XSS and rendering issues
|
|
2525
|
+
*/
|
|
2526
|
+
function escapeHtml(text) {
|
|
2527
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2528
|
+
}
|
|
2529
|
+
var UI_module_default = {
|
|
2530
|
+
wrapper: "_wrapper_1tu05_1",
|
|
2531
|
+
"mask-running": "_mask-running_1tu05_1",
|
|
2532
|
+
background: "_background_1tu05_39",
|
|
2533
|
+
header: "_header_1tu05_99",
|
|
2534
|
+
pulse: "_pulse_1tu05_1",
|
|
2535
|
+
retryPulse: "_retryPulse_1tu05_1",
|
|
2536
|
+
statusTextFadeOut: "_statusTextFadeOut_1tu05_1",
|
|
2537
|
+
statusTextFadeIn: "_statusTextFadeIn_1tu05_1",
|
|
2538
|
+
statusSection: "_statusSection_1tu05_121",
|
|
2539
|
+
indicator: "_indicator_1tu05_128",
|
|
2540
|
+
thinking: "_thinking_1tu05_137",
|
|
2541
|
+
tool_executing: "_tool_executing_1tu05_142",
|
|
2542
|
+
retry: "_retry_1tu05_147",
|
|
2543
|
+
completed: "_completed_1tu05_153",
|
|
2544
|
+
input: "_input_1tu05_154",
|
|
2545
|
+
output: "_output_1tu05_155",
|
|
2546
|
+
error: "_error_1tu05_160",
|
|
2547
|
+
statusText: "_statusText_1tu05_166",
|
|
2548
|
+
fadeOut: "_fadeOut_1tu05_178",
|
|
2549
|
+
fadeIn: "_fadeIn_1tu05_182",
|
|
2550
|
+
controls: "_controls_1tu05_188",
|
|
2551
|
+
controlButton: "_controlButton_1tu05_193",
|
|
2552
|
+
stopButton: "_stopButton_1tu05_212",
|
|
2553
|
+
historySectionWrapper: "_historySectionWrapper_1tu05_246",
|
|
2554
|
+
shimmer: "_shimmer_1tu05_1",
|
|
2555
|
+
celebrate: "_celebrate_1tu05_1",
|
|
2556
|
+
expanded: "_expanded_1tu05_278",
|
|
2557
|
+
historySection: "_historySection_1tu05_246",
|
|
2558
|
+
historyItem: "_historyItem_1tu05_297",
|
|
2559
|
+
observation: "_observation_1tu05_355",
|
|
2560
|
+
question: "_question_1tu05_360",
|
|
2561
|
+
doneSuccess: "_doneSuccess_1tu05_366",
|
|
2562
|
+
historyContent: "_historyContent_1tu05_402",
|
|
2563
|
+
statusIcon: "_statusIcon_1tu05_403",
|
|
2564
|
+
doneError: "_doneError_1tu05_412",
|
|
2565
|
+
reflectionLines: "_reflectionLines_1tu05_462",
|
|
2566
|
+
historyMeta: "_historyMeta_1tu05_469",
|
|
2567
|
+
inputSectionWrapper: "_inputSectionWrapper_1tu05_539",
|
|
2568
|
+
hidden: "_hidden_1tu05_562",
|
|
2569
|
+
inputSection: "_inputSection_1tu05_539",
|
|
2570
|
+
taskInput: "_taskInput_1tu05_573"
|
|
2571
|
+
};
|
|
2572
|
+
//#endregion
|
|
2573
|
+
//#region src/ui/cards.ts
|
|
2574
|
+
/**
|
|
2575
|
+
* Card HTML generation utilities for UI
|
|
2576
|
+
*/
|
|
2577
|
+
/** Create a single history card */
|
|
2578
|
+
function createCard({ icon, content, meta, type }) {
|
|
2579
|
+
const typeClass = type ? UI_module_default[type] : "";
|
|
2580
|
+
const contentHtml = Array.isArray(content) ? `<div class="${UI_module_default.reflectionLines}">${content.map((line) => `<span>${escapeHtml(line)}</span>`).join("")}</div>` : `<span>${escapeHtml(content)}</span>`;
|
|
2581
|
+
return `
|
|
2582
|
+
<div class="${UI_module_default.historyItem} ${typeClass}">
|
|
2583
|
+
<div class="${UI_module_default.historyContent}">
|
|
2584
|
+
<span class="${UI_module_default.statusIcon}">${icon}</span>
|
|
2585
|
+
${contentHtml}
|
|
2586
|
+
</div>
|
|
2587
|
+
${meta ? `<div class="${UI_module_default.historyMeta}">${meta}</div>` : ""}
|
|
2588
|
+
</div>
|
|
2589
|
+
`;
|
|
2590
|
+
}
|
|
2591
|
+
/** Create reflection lines from reflection object */
|
|
2592
|
+
function createReflectionLines(reflection) {
|
|
2593
|
+
const lines = [];
|
|
2594
|
+
if (reflection.evaluation_previous_goal) lines.push(`🔍 ${reflection.evaluation_previous_goal}`);
|
|
2595
|
+
if (reflection.memory) lines.push(`💾 ${reflection.memory}`);
|
|
2596
|
+
if (reflection.next_goal) lines.push(`🎯 ${reflection.next_goal}`);
|
|
2597
|
+
return lines;
|
|
2598
|
+
}
|
|
2599
|
+
//#endregion
|
|
2600
|
+
//#region src/ui/UI.ts
|
|
2601
|
+
/**
|
|
2602
|
+
* Agent control UI
|
|
2603
|
+
*
|
|
2604
|
+
* Architecture:
|
|
2605
|
+
* - History list: renders directly from agent.history (historical events)
|
|
2606
|
+
* - Header bar: shows activity events (transient state) and agent status
|
|
2607
|
+
*
|
|
2608
|
+
* This separation ensures data consistency - history is the single source of truth
|
|
2609
|
+
* for what has been done, while activity shows what is happening now.
|
|
2610
|
+
*/
|
|
2611
|
+
var UI = class {
|
|
2612
|
+
#wrapper;
|
|
2613
|
+
#indicator;
|
|
2614
|
+
#statusText;
|
|
2615
|
+
#historySection;
|
|
2616
|
+
#expandButton;
|
|
2617
|
+
#actionButton;
|
|
2618
|
+
#inputSection;
|
|
2619
|
+
#taskInput;
|
|
2620
|
+
#agent;
|
|
2621
|
+
#config;
|
|
2622
|
+
#isExpanded = false;
|
|
2623
|
+
#i18n;
|
|
2624
|
+
#userAnswerResolver = null;
|
|
2625
|
+
#userAnswerRejecter = null;
|
|
2626
|
+
#userAnswerSignal = null;
|
|
2627
|
+
#userAnswerAbortHandler = null;
|
|
2628
|
+
#isWaitingForUserAnswer = false;
|
|
2629
|
+
#headerUpdateTimer = null;
|
|
2630
|
+
#timers = /* @__PURE__ */ new Set();
|
|
2631
|
+
#pendingHeaderText = null;
|
|
2632
|
+
#isAnimating = false;
|
|
2633
|
+
#disposed = false;
|
|
2634
|
+
#onStatusChange = () => this.#handleStatusChange();
|
|
2635
|
+
#onHistoryChange = () => this.#handleHistoryChange();
|
|
2636
|
+
#onActivity = (e) => this.#handleActivity(e.detail);
|
|
2637
|
+
#onAgentDispose = () => this.dispose();
|
|
2638
|
+
#askUserCallback = (question, options) => this.#askUser(question, options?.signal);
|
|
2639
|
+
get wrapper() {
|
|
2640
|
+
return this.#wrapper;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Create a UI bound to an agent
|
|
2644
|
+
* @param agent - Agent instance that implements UIAdapter
|
|
2645
|
+
* @param config - Optional UI configuration
|
|
2646
|
+
*/
|
|
2647
|
+
constructor(agent, config = {}) {
|
|
2648
|
+
this.#agent = agent;
|
|
2649
|
+
this.#config = config;
|
|
2650
|
+
this.#i18n = new I18n(config.language ?? "en-US");
|
|
2651
|
+
this.#agent.onAskUser = this.#askUserCallback;
|
|
2652
|
+
this.#wrapper = this.#createWrapper();
|
|
2653
|
+
this.#indicator = this.#wrapper.querySelector(`.${UI_module_default.indicator}`);
|
|
2654
|
+
this.#statusText = this.#wrapper.querySelector(`.${UI_module_default.statusText}`);
|
|
2655
|
+
this.#historySection = this.#wrapper.querySelector(`.${UI_module_default.historySection}`);
|
|
2656
|
+
this.#expandButton = this.#wrapper.querySelector(`.${UI_module_default.expandButton}`);
|
|
2657
|
+
this.#actionButton = this.#wrapper.querySelector(`.${UI_module_default.stopButton}`);
|
|
2658
|
+
this.#inputSection = this.#wrapper.querySelector(`.${UI_module_default.inputSectionWrapper}`);
|
|
2659
|
+
this.#taskInput = this.#wrapper.querySelector(`.${UI_module_default.taskInput}`);
|
|
2660
|
+
this.#agent.addEventListener("statuschange", this.#onStatusChange);
|
|
2661
|
+
this.#agent.addEventListener("historychange", this.#onHistoryChange);
|
|
2662
|
+
this.#agent.addEventListener("activity", this.#onActivity);
|
|
2663
|
+
this.#agent.addEventListener("dispose", this.#onAgentDispose);
|
|
2664
|
+
this.#setupEventListeners();
|
|
2665
|
+
this.#startHeaderUpdateLoop();
|
|
2666
|
+
this.#showInputArea();
|
|
2667
|
+
this.hide();
|
|
2668
|
+
}
|
|
2669
|
+
/** Handle agent status change */
|
|
2670
|
+
#handleStatusChange() {
|
|
2671
|
+
const status = this.#agent.status;
|
|
2672
|
+
const failed = status === "completed" && this.#agent.lastResult?.success === false;
|
|
2673
|
+
this.#updateStatusIndicator(failed ? "error" : status);
|
|
2674
|
+
if (status === "running") {
|
|
2675
|
+
this.#actionButton.textContent = "■";
|
|
2676
|
+
this.#actionButton.title = this.#i18n.t("ui.stop");
|
|
2677
|
+
} else {
|
|
2678
|
+
this.#actionButton.textContent = "X";
|
|
2679
|
+
this.#actionButton.title = this.#i18n.t("ui.close");
|
|
2680
|
+
}
|
|
2681
|
+
if (status === "running") {
|
|
2682
|
+
this.show();
|
|
2683
|
+
this.#hideInputArea();
|
|
2684
|
+
}
|
|
2685
|
+
if (status === "completed" || status === "error" || status === "stopped") {
|
|
2686
|
+
if (!this.#isExpanded) this.#expand();
|
|
2687
|
+
if (this.#shouldShowInputArea()) this.#showInputArea();
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
/** Handle agent history change - re-render history list from agent.history */
|
|
2691
|
+
#handleHistoryChange() {
|
|
2692
|
+
this.#renderHistory();
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Handle agent activity - transient state for immediate UI feedback
|
|
2696
|
+
* Activity events are NOT persisted in history, only used for header bar updates
|
|
2697
|
+
*/
|
|
2698
|
+
#handleActivity(activity) {
|
|
2699
|
+
switch (activity.type) {
|
|
2700
|
+
case "thinking":
|
|
2701
|
+
this.#pendingHeaderText = this.#i18n.t("ui.thinking");
|
|
2702
|
+
this.#updateStatusIndicator("thinking");
|
|
2703
|
+
break;
|
|
2704
|
+
case "executing":
|
|
2705
|
+
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input);
|
|
2706
|
+
this.#updateStatusIndicator("executing");
|
|
2707
|
+
break;
|
|
2708
|
+
case "executed":
|
|
2709
|
+
this.#pendingHeaderText = truncate$1(activity.output, 50);
|
|
2710
|
+
break;
|
|
2711
|
+
case "retrying":
|
|
2712
|
+
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`;
|
|
2713
|
+
this.#updateStatusIndicator("retrying");
|
|
2714
|
+
break;
|
|
2715
|
+
case "error":
|
|
2716
|
+
this.#pendingHeaderText = truncate$1(activity.message, 50);
|
|
2717
|
+
this.#updateStatusIndicator("error");
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Ask for user input (internal, called by agent via onAskUser).
|
|
2722
|
+
* Rejects when `signal` aborts (task stopped or disposed), cleaning up the
|
|
2723
|
+
* question card and pending state so the agent loop can settle.
|
|
2724
|
+
*/
|
|
2725
|
+
#askUser(question, signal) {
|
|
2726
|
+
if (this.#disposed) return Promise.reject(this.#createAbortError());
|
|
2727
|
+
this.#rejectPendingQuestion();
|
|
2728
|
+
return new Promise((resolve, reject) => {
|
|
2729
|
+
this.#isWaitingForUserAnswer = true;
|
|
2730
|
+
this.#userAnswerResolver = resolve;
|
|
2731
|
+
this.#userAnswerRejecter = reject;
|
|
2732
|
+
if (!this.#isExpanded) this.#expand();
|
|
2733
|
+
const tempCard = document.createElement("div");
|
|
2734
|
+
tempCard.innerHTML = createCard({
|
|
2735
|
+
icon: "❓",
|
|
2736
|
+
content: `Question: ${question}`,
|
|
2737
|
+
type: "question"
|
|
2738
|
+
});
|
|
2739
|
+
const cardElement = tempCard.firstElementChild;
|
|
2740
|
+
cardElement.setAttribute("data-temp-card", "true");
|
|
2741
|
+
this.#historySection.appendChild(cardElement);
|
|
2742
|
+
this.#scrollToBottom();
|
|
2743
|
+
this.#showInputArea(this.#i18n.t("ui.userAnswerPrompt"));
|
|
2744
|
+
if (signal) {
|
|
2745
|
+
this.#userAnswerSignal = signal;
|
|
2746
|
+
this.#userAnswerAbortHandler = () => this.#rejectPendingQuestion();
|
|
2747
|
+
if (signal.aborted) this.#rejectPendingQuestion();
|
|
2748
|
+
else signal.addEventListener("abort", this.#userAnswerAbortHandler, { once: true });
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
#createAbortError() {
|
|
2753
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
2754
|
+
}
|
|
2755
|
+
#clearQuestionState() {
|
|
2756
|
+
if (this.#userAnswerSignal && this.#userAnswerAbortHandler) this.#userAnswerSignal.removeEventListener("abort", this.#userAnswerAbortHandler);
|
|
2757
|
+
this.#userAnswerSignal = null;
|
|
2758
|
+
this.#userAnswerAbortHandler = null;
|
|
2759
|
+
this.#userAnswerResolver = null;
|
|
2760
|
+
this.#userAnswerRejecter = null;
|
|
2761
|
+
this.#isWaitingForUserAnswer = false;
|
|
2762
|
+
this.#removeTempCards();
|
|
2763
|
+
}
|
|
2764
|
+
#rejectPendingQuestion() {
|
|
2765
|
+
const reject = this.#userAnswerRejecter;
|
|
2766
|
+
if (!reject) return;
|
|
2767
|
+
this.#clearQuestionState();
|
|
2768
|
+
reject(this.#createAbortError());
|
|
2769
|
+
}
|
|
2770
|
+
/** Remove temporary question cards (only direct children for safety) */
|
|
2771
|
+
#removeTempCards() {
|
|
2772
|
+
Array.from(this.#historySection.children).forEach((child) => {
|
|
2773
|
+
if (child.getAttribute("data-temp-card") === "true") child.remove();
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
show() {
|
|
2777
|
+
this.wrapper.style.display = "block";
|
|
2778
|
+
this.wrapper.offsetHeight;
|
|
2779
|
+
this.wrapper.style.opacity = "1";
|
|
2780
|
+
this.wrapper.style.transform = "translateX(-50%) translateY(0)";
|
|
2781
|
+
}
|
|
2782
|
+
hide() {
|
|
2783
|
+
this.wrapper.style.opacity = "0";
|
|
2784
|
+
this.wrapper.style.transform = "translateX(-50%) translateY(20px)";
|
|
2785
|
+
this.wrapper.style.display = "none";
|
|
2786
|
+
}
|
|
2787
|
+
reset() {
|
|
2788
|
+
this.#rejectPendingQuestion();
|
|
2789
|
+
this.#statusText.textContent = this.#i18n.t("ui.ready");
|
|
2790
|
+
this.#updateStatusIndicator("thinking");
|
|
2791
|
+
this.#renderHistory();
|
|
2792
|
+
this.#collapse();
|
|
2793
|
+
this.#showInputArea();
|
|
2794
|
+
}
|
|
2795
|
+
expand() {
|
|
2796
|
+
this.#expand();
|
|
2797
|
+
}
|
|
2798
|
+
collapse() {
|
|
2799
|
+
this.#collapse();
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Dispose UI and clean up event listeners
|
|
2803
|
+
*/
|
|
2804
|
+
dispose() {
|
|
2805
|
+
if (this.#disposed) return;
|
|
2806
|
+
this.#disposed = true;
|
|
2807
|
+
this.#agent.removeEventListener("statuschange", this.#onStatusChange);
|
|
2808
|
+
this.#agent.removeEventListener("historychange", this.#onHistoryChange);
|
|
2809
|
+
this.#agent.removeEventListener("activity", this.#onActivity);
|
|
2810
|
+
this.#agent.removeEventListener("dispose", this.#onAgentDispose);
|
|
2811
|
+
if (this.#agent.onAskUser === this.#askUserCallback) this.#agent.onAskUser = void 0;
|
|
2812
|
+
this.#rejectPendingQuestion();
|
|
2813
|
+
this.#clearQuestionState();
|
|
2814
|
+
this.#stopHeaderUpdateLoop();
|
|
2815
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
2816
|
+
this.#timers.clear();
|
|
2817
|
+
this.wrapper.remove();
|
|
2818
|
+
}
|
|
2819
|
+
#getToolExecutingText(toolName, args) {
|
|
2820
|
+
const a = args;
|
|
2821
|
+
switch (toolName) {
|
|
2822
|
+
case "click_element_by_index": return this.#i18n.t("ui.tools.clicking", { index: a.index });
|
|
2823
|
+
case "input_text": return this.#i18n.t("ui.tools.inputting", { index: a.index });
|
|
2824
|
+
case "select_dropdown_option": return this.#i18n.t("ui.tools.selecting", { text: a.text });
|
|
2825
|
+
case "scroll": return this.#i18n.t("ui.tools.scrolling");
|
|
2826
|
+
case "wait": return this.#i18n.t("ui.tools.waiting", { seconds: a.seconds });
|
|
2827
|
+
case "ask_user": return this.#i18n.t("ui.tools.askingUser");
|
|
2828
|
+
case "done": return this.#i18n.t("ui.tools.done");
|
|
2829
|
+
default: return this.#i18n.t("ui.tools.executing", { toolName });
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Action button handler: stop when running, close (dispose) when idle
|
|
2834
|
+
*/
|
|
2835
|
+
#handleActionButton() {
|
|
2836
|
+
if (this.#agent.status === "running") this.#agent.stop();
|
|
2837
|
+
else this.#agent.dispose();
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Submit task
|
|
2841
|
+
*/
|
|
2842
|
+
#submitTask() {
|
|
2843
|
+
const input = this.#taskInput.value.trim();
|
|
2844
|
+
if (!input) return;
|
|
2845
|
+
this.#hideInputArea();
|
|
2846
|
+
if (this.#isWaitingForUserAnswer) this.#handleUserAnswer(input);
|
|
2847
|
+
else this.#agent.execute(input).catch((error) => {
|
|
2848
|
+
if (error?.name === "AbortError") return;
|
|
2849
|
+
console.error("[UI] Failed to execute task:", error);
|
|
2850
|
+
if (!this.#disposed) this.#showInputArea();
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Handle user answer
|
|
2855
|
+
*/
|
|
2856
|
+
#handleUserAnswer(input) {
|
|
2857
|
+
this.#removeTempCards();
|
|
2858
|
+
const resolve = this.#userAnswerResolver;
|
|
2859
|
+
this.#clearQuestionState();
|
|
2860
|
+
resolve?.(input);
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Show input area
|
|
2864
|
+
*/
|
|
2865
|
+
#showInputArea(placeholder) {
|
|
2866
|
+
this.#taskInput.value = "";
|
|
2867
|
+
this.#taskInput.placeholder = placeholder || this.#i18n.t("ui.taskInput");
|
|
2868
|
+
this.#inputSection.classList.remove(UI_module_default.hidden);
|
|
2869
|
+
this.#setTimer(() => {
|
|
2870
|
+
this.#taskInput.focus();
|
|
2871
|
+
}, 100);
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* Hide input area
|
|
2875
|
+
*/
|
|
2876
|
+
#hideInputArea() {
|
|
2877
|
+
this.#inputSection.classList.add(UI_module_default.hidden);
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Check if input area should be shown
|
|
2881
|
+
*/
|
|
2882
|
+
#shouldShowInputArea() {
|
|
2883
|
+
if (this.#isWaitingForUserAnswer) return true;
|
|
2884
|
+
if (this.#agent.history.length === 0) return true;
|
|
2885
|
+
const status = this.#agent.status;
|
|
2886
|
+
if (status === "completed" || status === "error" || status === "stopped") return this.#config.promptForNextTask ?? true;
|
|
2887
|
+
return false;
|
|
2888
|
+
}
|
|
2889
|
+
#createWrapper() {
|
|
2890
|
+
const taskInputMaxLength = 1e3;
|
|
2891
|
+
const wrapper = document.createElement("div");
|
|
2892
|
+
wrapper.id = "my-page-agent-runtime_agent-ui";
|
|
2893
|
+
wrapper.className = UI_module_default.wrapper;
|
|
2894
|
+
wrapper.setAttribute("data-browser-use-ignore", "true");
|
|
2895
|
+
wrapper.setAttribute("data-page-agent-ignore", "true");
|
|
2896
|
+
wrapper.innerHTML = `
|
|
2897
|
+
<div class="${UI_module_default.background}"></div>
|
|
2898
|
+
<div class="${UI_module_default.historySectionWrapper}">
|
|
2899
|
+
<div class="${UI_module_default.historySection}">
|
|
2900
|
+
<div class="${UI_module_default.historyItem}">
|
|
2901
|
+
<div class="${UI_module_default.historyContent}">
|
|
2902
|
+
<span class="${UI_module_default.statusIcon}">🧠</span>
|
|
2903
|
+
<span>${this.#i18n.t("ui.waitingPlaceholder")}</span>
|
|
2904
|
+
</div>
|
|
2905
|
+
</div>
|
|
2906
|
+
</div>
|
|
2907
|
+
</div>
|
|
2908
|
+
<div class="${UI_module_default.header}">
|
|
2909
|
+
<div class="${UI_module_default.statusSection}">
|
|
2910
|
+
<div class="${UI_module_default.indicator} ${UI_module_default.thinking}"></div>
|
|
2911
|
+
<div class="${UI_module_default.statusText}">${this.#i18n.t("ui.ready")}</div>
|
|
2912
|
+
</div>
|
|
2913
|
+
<div class="${UI_module_default.controls}">
|
|
2914
|
+
<button class="${UI_module_default.controlButton} ${UI_module_default.expandButton}" title="${this.#i18n.t("ui.expand")}">
|
|
2915
|
+
▼
|
|
2916
|
+
</button>
|
|
2917
|
+
<button class="${UI_module_default.controlButton} ${UI_module_default.stopButton}" title="${this.#i18n.t("ui.close")}">
|
|
2918
|
+
X
|
|
2919
|
+
</button>
|
|
2920
|
+
</div>
|
|
2921
|
+
</div>
|
|
2922
|
+
<div class="${UI_module_default.inputSectionWrapper} ${UI_module_default.hidden}">
|
|
2923
|
+
<div class="${UI_module_default.inputSection}">
|
|
2924
|
+
<input
|
|
2925
|
+
type="text"
|
|
2926
|
+
class="${UI_module_default.taskInput}"
|
|
2927
|
+
maxlength="${taskInputMaxLength}"
|
|
2928
|
+
/>
|
|
2929
|
+
</div>
|
|
2930
|
+
</div>
|
|
2931
|
+
`;
|
|
2932
|
+
document.body.appendChild(wrapper);
|
|
2933
|
+
return wrapper;
|
|
2934
|
+
}
|
|
2935
|
+
#setupEventListeners() {
|
|
2936
|
+
this.wrapper.querySelector(`.${UI_module_default.header}`).addEventListener("click", (e) => {
|
|
2937
|
+
if (e.target.closest(`.${UI_module_default.controlButton}`)) return;
|
|
2938
|
+
this.#toggle();
|
|
2939
|
+
});
|
|
2940
|
+
this.#expandButton.addEventListener("click", (e) => {
|
|
2941
|
+
e.stopPropagation();
|
|
2942
|
+
this.#toggle();
|
|
2943
|
+
});
|
|
2944
|
+
this.#actionButton.addEventListener("click", (e) => {
|
|
2945
|
+
e.stopPropagation();
|
|
2946
|
+
this.#handleActionButton();
|
|
2947
|
+
});
|
|
2948
|
+
this.#taskInput.addEventListener("keydown", (e) => {
|
|
2949
|
+
if (e.isComposing) return;
|
|
2950
|
+
if (e.key === "Enter") {
|
|
2951
|
+
e.preventDefault();
|
|
2952
|
+
this.#submitTask();
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
this.#inputSection.addEventListener("click", (e) => {
|
|
2956
|
+
e.stopPropagation();
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
#toggle() {
|
|
2960
|
+
if (this.#isExpanded) this.#collapse();
|
|
2961
|
+
else this.#expand();
|
|
2962
|
+
}
|
|
2963
|
+
#expand() {
|
|
2964
|
+
this.#isExpanded = true;
|
|
2965
|
+
this.wrapper.classList.add(UI_module_default.expanded);
|
|
2966
|
+
this.#expandButton.textContent = "▲";
|
|
2967
|
+
}
|
|
2968
|
+
#collapse() {
|
|
2969
|
+
this.#isExpanded = false;
|
|
2970
|
+
this.wrapper.classList.remove(UI_module_default.expanded);
|
|
2971
|
+
this.#expandButton.textContent = "▼";
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Start periodic header update loop
|
|
2975
|
+
*/
|
|
2976
|
+
#startHeaderUpdateLoop() {
|
|
2977
|
+
this.#headerUpdateTimer = setInterval(() => {
|
|
2978
|
+
this.#checkAndUpdateHeader();
|
|
2979
|
+
}, 450);
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Stop periodic header update loop
|
|
2983
|
+
*/
|
|
2984
|
+
#stopHeaderUpdateLoop() {
|
|
2985
|
+
if (this.#headerUpdateTimer) {
|
|
2986
|
+
clearInterval(this.#headerUpdateTimer);
|
|
2987
|
+
this.#headerUpdateTimer = null;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
#setTimer(callback, delay) {
|
|
2991
|
+
const timer = setTimeout(() => {
|
|
2992
|
+
this.#timers.delete(timer);
|
|
2993
|
+
if (!this.#disposed) callback();
|
|
2994
|
+
}, delay);
|
|
2995
|
+
this.#timers.add(timer);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Check if header needs update and trigger animation if not currently animating
|
|
2999
|
+
*/
|
|
3000
|
+
#checkAndUpdateHeader() {
|
|
3001
|
+
if (!this.#pendingHeaderText || this.#isAnimating) return;
|
|
3002
|
+
if (this.#statusText.textContent === this.#pendingHeaderText) {
|
|
3003
|
+
this.#pendingHeaderText = null;
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
const textToShow = this.#pendingHeaderText;
|
|
3007
|
+
this.#pendingHeaderText = null;
|
|
3008
|
+
this.#animateTextChange(textToShow);
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* Animate text change with fade out/in effect
|
|
3012
|
+
*/
|
|
3013
|
+
#animateTextChange(newText) {
|
|
3014
|
+
this.#isAnimating = true;
|
|
3015
|
+
this.#statusText.classList.add(UI_module_default.fadeOut);
|
|
3016
|
+
this.#setTimer(() => {
|
|
3017
|
+
this.#statusText.textContent = newText;
|
|
3018
|
+
this.#statusText.classList.remove(UI_module_default.fadeOut);
|
|
3019
|
+
this.#statusText.classList.add(UI_module_default.fadeIn);
|
|
3020
|
+
this.#setTimer(() => {
|
|
3021
|
+
this.#statusText.classList.remove(UI_module_default.fadeIn);
|
|
3022
|
+
this.#isAnimating = false;
|
|
3023
|
+
}, 300);
|
|
3024
|
+
}, 150);
|
|
3025
|
+
}
|
|
3026
|
+
#updateStatusIndicator(type) {
|
|
3027
|
+
const variant = type === "running" ? "thinking" : type === "executing" ? "tool_executing" : type === "retrying" ? "retry" : type;
|
|
3028
|
+
this.#indicator.className = UI_module_default.indicator;
|
|
3029
|
+
if (variant !== "idle" && variant !== "stopped") this.#indicator.classList.add(UI_module_default[variant]);
|
|
3030
|
+
}
|
|
3031
|
+
#scrollToBottom() {
|
|
3032
|
+
this.#setTimer(() => {
|
|
3033
|
+
this.#historySection.scrollTop = this.#historySection.scrollHeight;
|
|
3034
|
+
}, 0);
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Render history directly from agent.history
|
|
3038
|
+
*
|
|
3039
|
+
* Renders:
|
|
3040
|
+
* 1. Task (first item, from agent.task)
|
|
3041
|
+
* 2. Reflection cards (evaluation, memory, next_goal)
|
|
3042
|
+
* 3. Tool execution with output
|
|
3043
|
+
* 4. Observations
|
|
3044
|
+
*/
|
|
3045
|
+
#renderHistory() {
|
|
3046
|
+
const items = [];
|
|
3047
|
+
const task = this.#agent.task;
|
|
3048
|
+
if (task) items.push(this.#createTaskCard(task));
|
|
3049
|
+
const history = this.#agent.history;
|
|
3050
|
+
for (const event of history) items.push(...this.#createHistoryCards(event));
|
|
3051
|
+
this.#historySection.innerHTML = items.join("");
|
|
3052
|
+
this.#scrollToBottom();
|
|
3053
|
+
}
|
|
3054
|
+
#createTaskCard(task) {
|
|
3055
|
+
return createCard({
|
|
3056
|
+
icon: "🎯",
|
|
3057
|
+
content: task,
|
|
3058
|
+
type: "input"
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
/** Create cards for a history event */
|
|
3062
|
+
#createHistoryCards(event) {
|
|
3063
|
+
const cards = [];
|
|
3064
|
+
const meta = event.type === "step" && event.stepIndex !== void 0 ? this.#i18n.t("ui.step", { number: (event.stepIndex + 1).toString() }) : void 0;
|
|
3065
|
+
if (event.type === "step") {
|
|
3066
|
+
if (event.reflection) {
|
|
3067
|
+
const lines = createReflectionLines(event.reflection);
|
|
3068
|
+
if (lines.length > 0) cards.push(createCard({
|
|
3069
|
+
icon: "🧠",
|
|
3070
|
+
content: lines,
|
|
3071
|
+
meta
|
|
3072
|
+
}));
|
|
3073
|
+
}
|
|
3074
|
+
const action = event.action;
|
|
3075
|
+
if (action) cards.push(...this.#createActionCards(action, meta));
|
|
3076
|
+
} else if (event.type === "observation") cards.push(createCard({
|
|
3077
|
+
icon: "👁️",
|
|
3078
|
+
content: event.content || "",
|
|
3079
|
+
meta,
|
|
3080
|
+
type: "observation"
|
|
3081
|
+
}));
|
|
3082
|
+
else if (event.type === "user_takeover") cards.push(createCard({
|
|
3083
|
+
icon: "👤",
|
|
3084
|
+
content: "User takeover",
|
|
3085
|
+
meta,
|
|
3086
|
+
type: "input"
|
|
3087
|
+
}));
|
|
3088
|
+
else if (event.type === "retry") {
|
|
3089
|
+
const retryInfo = `${event.message || "Retrying"} (${event.attempt}/${event.maxAttempts})`;
|
|
3090
|
+
cards.push(createCard({
|
|
3091
|
+
icon: "🔄",
|
|
3092
|
+
content: retryInfo,
|
|
3093
|
+
meta,
|
|
3094
|
+
type: "observation"
|
|
3095
|
+
}));
|
|
3096
|
+
} else if (event.type === "error") cards.push(createCard({
|
|
3097
|
+
icon: "❌",
|
|
3098
|
+
content: event.message || "Error",
|
|
3099
|
+
meta,
|
|
3100
|
+
type: "observation"
|
|
3101
|
+
}));
|
|
3102
|
+
return cards;
|
|
3103
|
+
}
|
|
3104
|
+
/** Create cards for an action */
|
|
3105
|
+
#createActionCards(action, meta) {
|
|
3106
|
+
const cards = [];
|
|
3107
|
+
if (action.name === "done") {
|
|
3108
|
+
const input = action.input;
|
|
3109
|
+
const text = input.text || action.output || "";
|
|
3110
|
+
if (text) cards.push(createCard({
|
|
3111
|
+
icon: "🤖",
|
|
3112
|
+
content: text,
|
|
3113
|
+
meta,
|
|
3114
|
+
type: input.success === true ? "doneSuccess" : "doneError"
|
|
3115
|
+
}));
|
|
3116
|
+
} else if (action.name === "ask_user") {
|
|
3117
|
+
const input = action.input;
|
|
3118
|
+
const answer = action.output.replace(/^User answered:\s*/i, "");
|
|
3119
|
+
cards.push(createCard({
|
|
3120
|
+
icon: "❓",
|
|
3121
|
+
content: `Question: ${input.question || ""}`,
|
|
3122
|
+
meta,
|
|
3123
|
+
type: "question"
|
|
3124
|
+
}));
|
|
3125
|
+
cards.push(createCard({
|
|
3126
|
+
icon: "💬",
|
|
3127
|
+
content: `Answer: ${answer}`,
|
|
3128
|
+
meta,
|
|
3129
|
+
type: "input"
|
|
3130
|
+
}));
|
|
3131
|
+
} else {
|
|
3132
|
+
const toolText = this.#getToolExecutingText(action.name, action.input);
|
|
3133
|
+
cards.push(createCard({
|
|
3134
|
+
icon: "🔨",
|
|
3135
|
+
content: toolText,
|
|
3136
|
+
meta
|
|
3137
|
+
}));
|
|
3138
|
+
if (action.output?.length > 0) cards.push(createCard({
|
|
3139
|
+
icon: "🔨",
|
|
3140
|
+
content: action.output,
|
|
3141
|
+
meta,
|
|
3142
|
+
type: "output"
|
|
3143
|
+
}));
|
|
3144
|
+
}
|
|
3145
|
+
return cards;
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
//#endregion
|
|
3149
|
+
//#region src/llm/errors.ts
|
|
3150
|
+
/**
|
|
3151
|
+
* Error types and error handling for LLM invocations.
|
|
3152
|
+
*/
|
|
3153
|
+
var InvokeErrorTypes = {
|
|
3154
|
+
NETWORK_ERROR: "network_error",
|
|
3155
|
+
RATE_LIMIT: "rate_limit",
|
|
3156
|
+
SERVER_ERROR: "server_error",
|
|
3157
|
+
NO_TOOL_CALL: "no_tool_call",
|
|
3158
|
+
INVALID_TOOL_ARGS: "invalid_tool_args",
|
|
3159
|
+
TOOL_EXECUTION_ERROR: "tool_execution_error",
|
|
3160
|
+
INVALID_RESPONSE: "invalid_response",
|
|
3161
|
+
INVALID_SCHEMA: "invalid_schema",
|
|
3162
|
+
UNKNOWN: "unknown",
|
|
3163
|
+
CONFIG_ERROR: "config_error",
|
|
3164
|
+
AUTH_ERROR: "auth_error",
|
|
3165
|
+
CONTEXT_LENGTH: "context_length",
|
|
3166
|
+
CONTENT_FILTER: "content_filter"
|
|
3167
|
+
};
|
|
3168
|
+
var RETRYABLE_TYPES = [
|
|
3169
|
+
InvokeErrorTypes.NETWORK_ERROR,
|
|
3170
|
+
InvokeErrorTypes.RATE_LIMIT,
|
|
3171
|
+
InvokeErrorTypes.SERVER_ERROR,
|
|
3172
|
+
InvokeErrorTypes.NO_TOOL_CALL,
|
|
3173
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
3174
|
+
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
|
3175
|
+
InvokeErrorTypes.INVALID_RESPONSE,
|
|
3176
|
+
InvokeErrorTypes.INVALID_SCHEMA,
|
|
3177
|
+
InvokeErrorTypes.UNKNOWN
|
|
3178
|
+
];
|
|
3179
|
+
var InvokeError = class extends Error {
|
|
3180
|
+
type;
|
|
3181
|
+
retryable;
|
|
3182
|
+
statusCode;
|
|
3183
|
+
rawError;
|
|
3184
|
+
rawResponse;
|
|
3185
|
+
constructor(type, message, rawError, rawResponse) {
|
|
3186
|
+
super(message);
|
|
3187
|
+
this.name = "InvokeError";
|
|
3188
|
+
this.type = type;
|
|
3189
|
+
this.retryable = RETRYABLE_TYPES.includes(type);
|
|
3190
|
+
this.rawError = rawError;
|
|
3191
|
+
this.rawResponse = rawResponse;
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
//#endregion
|
|
3195
|
+
//#region src/llm/utils.ts
|
|
3196
|
+
/**
|
|
3197
|
+
* Utility functions for LLM integration
|
|
3198
|
+
*/
|
|
3199
|
+
var debug = console.debug.bind(console, chalk.gray("[LLM]"));
|
|
3200
|
+
/**
|
|
3201
|
+
* Convert Zod schema to OpenAI tool format
|
|
3202
|
+
* Uses Zod 4 native z.toJSONSchema()
|
|
3203
|
+
*/
|
|
3204
|
+
function zodToOpenAITool(name, tool) {
|
|
3205
|
+
return {
|
|
3206
|
+
type: "function",
|
|
3207
|
+
function: {
|
|
3208
|
+
name,
|
|
3209
|
+
description: tool.description,
|
|
3210
|
+
parameters: z.toJSONSchema(tool.inputSchema, { target: "openapi-3.0" })
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Patch model specific parameters. Only patches known models.
|
|
3216
|
+
*
|
|
3217
|
+
* @purpose
|
|
3218
|
+
* - Reconcile the differences in the parameter schema each model accepts.
|
|
3219
|
+
* - Disable thinking/reasoning, or lower it to the minimum where a full disable is impossible.
|
|
3220
|
+
* - Minimize returned tokens.
|
|
3221
|
+
* - Raise temperature for known smaller models to improve auto-recovery odds.
|
|
3222
|
+
* @note Honor temperature if explicitly set by the user
|
|
3223
|
+
*
|
|
3224
|
+
* @todo Need vendor-specific patches.
|
|
3225
|
+
* Local and 3rd-party hosted models may have different schema.
|
|
3226
|
+
*/
|
|
3227
|
+
function modelPatch(body, baseURL) {
|
|
3228
|
+
const model = body.model || "";
|
|
3229
|
+
if (!model) return body;
|
|
3230
|
+
const provider = getProvider(baseURL);
|
|
3231
|
+
const modelName = normalizeModelName(model);
|
|
3232
|
+
if (modelName.startsWith("qwen")) {
|
|
3233
|
+
debug("Patch Qwen: disable thinking");
|
|
3234
|
+
body.enable_thinking = false;
|
|
3235
|
+
if (body.temperature === void 0 && !/max|plus/.test(modelName)) {
|
|
3236
|
+
debug("Patch Qwen: raise temperature to 1.0");
|
|
3237
|
+
body.temperature = 1;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
if (modelName.startsWith("deepseek")) {
|
|
3241
|
+
debug("Patch DeepSeek: disable thinking, remove tool_choice");
|
|
3242
|
+
body.thinking = { type: "disabled" };
|
|
3243
|
+
delete body.tool_choice;
|
|
3244
|
+
}
|
|
3245
|
+
if (modelName.startsWith("gpt")) {
|
|
3246
|
+
if (modelName.startsWith("gpt-5")) body.verbosity = "low";
|
|
3247
|
+
if (modelName.includes("chat-latest")) {
|
|
3248
|
+
debug("Patch chat-latest: omit reasoning_effort and temperature");
|
|
3249
|
+
delete body.reasoning_effort;
|
|
3250
|
+
delete body.temperature;
|
|
3251
|
+
} else if (/^gpt-5[12](-|$)/.test(modelName)) {
|
|
3252
|
+
debug("Patch GPT-5.1/5.2: reasoning_effort=none");
|
|
3253
|
+
body.reasoning_effort = "none";
|
|
3254
|
+
} else if (/^gpt-5(-|$)/.test(modelName)) {
|
|
3255
|
+
debug("Patch GPT-5: reasoning_effort=minimal");
|
|
3256
|
+
body.reasoning_effort = "minimal";
|
|
3257
|
+
} else {
|
|
3258
|
+
debug("Patch GPT: omit reasoning_effort");
|
|
3259
|
+
delete body.reasoning_effort;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (modelName.startsWith("claude")) {
|
|
3263
|
+
if (/opus|sonnet|haiku/.test(modelName)) {
|
|
3264
|
+
debug("Patch Claude: disable thinking");
|
|
3265
|
+
body.thinking = { type: "disabled" };
|
|
3266
|
+
if (provider !== "openrouter") {
|
|
3267
|
+
if (body.tool_choice === "required") {
|
|
3268
|
+
debug("Applying Claude patch: convert tool_choice \"required\" to { type: \"any\" }");
|
|
3269
|
+
body.tool_choice = { type: "any" };
|
|
3270
|
+
} else if (body.tool_choice?.function?.name) {
|
|
3271
|
+
debug("Applying Claude patch: convert tool_choice format");
|
|
3272
|
+
body.tool_choice = {
|
|
3273
|
+
type: "tool",
|
|
3274
|
+
name: body.tool_choice.function.name
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
} else {
|
|
3279
|
+
debug("Patch Claude: reasoning_effort=low");
|
|
3280
|
+
body.reasoning_effort = "low";
|
|
3281
|
+
delete body.tool_choice;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
if (modelName.startsWith("gemini")) {
|
|
3285
|
+
debug("Patch Gemini: reasoning_effort=low");
|
|
3286
|
+
body.reasoning_effort = "low";
|
|
3287
|
+
if (/^gemini-25(?!.*pro)/.test(modelName)) {
|
|
3288
|
+
debug("Patch Gemini 2.5 non-Pro: reasoning_effort=none");
|
|
3289
|
+
body.reasoning_effort = "none";
|
|
3290
|
+
} else if (modelName.startsWith("gemini-35-flash") || modelName.startsWith("gemini-31-flash-lite") || modelName.startsWith("gemini-3-flash")) {
|
|
3291
|
+
debug("Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal");
|
|
3292
|
+
body.reasoning_effort = "minimal";
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
if (modelName.startsWith("glm")) {
|
|
3296
|
+
debug("Patch GLM: disable thinking");
|
|
3297
|
+
body.thinking = { type: "disabled" };
|
|
3298
|
+
}
|
|
3299
|
+
if (modelName.startsWith("hy")) {
|
|
3300
|
+
debug("Patch Hunyuan: disable thinking, reasoning_effort=low");
|
|
3301
|
+
body.thinking = { type: "disabled" };
|
|
3302
|
+
body.reasoning_effort = "low";
|
|
3303
|
+
}
|
|
3304
|
+
if (modelName.startsWith("grok")) {
|
|
3305
|
+
if (/^grok-4-?3/.test(modelName)) {
|
|
3306
|
+
debug("Patch Grok 4.3: reasoning_effort=none");
|
|
3307
|
+
body.reasoning_effort = "none";
|
|
3308
|
+
} else if (modelName.startsWith("grok-3-mini") || modelName.startsWith("grok-code-fast")) {
|
|
3309
|
+
debug("Patch Grok mini/code: reasoning_effort=low");
|
|
3310
|
+
body.reasoning_effort = "low";
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (modelName.startsWith("kimi")) {
|
|
3314
|
+
if (modelName.startsWith("kimi-k3")) {
|
|
3315
|
+
debug("Patch Kimi K3: use required tool choice, remove parallel tool calls");
|
|
3316
|
+
delete body.parallel_tool_calls;
|
|
3317
|
+
if (body.tool_choice?.function?.name) body.tool_choice = "required";
|
|
3318
|
+
} else if (!modelName.includes("code")) {
|
|
3319
|
+
debug("Patch Kimi: disable thinking");
|
|
3320
|
+
body.thinking = { type: "disabled" };
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (modelName.startsWith("minimax")) {
|
|
3324
|
+
debug("Patch MiniMax: remove parallel_tool_calls");
|
|
3325
|
+
delete body.parallel_tool_calls;
|
|
3326
|
+
if (modelName.includes("m3")) {
|
|
3327
|
+
debug("Patch MiniMax: disable thinking");
|
|
3328
|
+
body.thinking = { type: "disabled" };
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
if (provider === "openrouter") {
|
|
3332
|
+
const reasoningEffort = body.reasoning_effort;
|
|
3333
|
+
if (body.thinking?.type === "disabled" || body.enable_thinking === false || reasoningEffort === "none") body.reasoning = { enabled: false };
|
|
3334
|
+
else if (reasoningEffort) body.reasoning = {
|
|
3335
|
+
enabled: true,
|
|
3336
|
+
effort: reasoningEffort
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
return body;
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* check if a given model ID fits a specific model name
|
|
3343
|
+
*
|
|
3344
|
+
* @note
|
|
3345
|
+
* Different model providers may use different model IDs for the same model.
|
|
3346
|
+
* For example, openai's `gpt-5.2` may called:
|
|
3347
|
+
*
|
|
3348
|
+
* - `gpt-5.2-version`
|
|
3349
|
+
* - `gpt-5_2-date`
|
|
3350
|
+
* - `GPT-52-version-date`
|
|
3351
|
+
* - `openai/gpt-5.2-chat`
|
|
3352
|
+
*
|
|
3353
|
+
* They should be treated as the same model.
|
|
3354
|
+
* Normalize them to `gpt-52`
|
|
3355
|
+
*/
|
|
3356
|
+
function normalizeModelName(modelName) {
|
|
3357
|
+
let normalizedName = modelName.toLowerCase();
|
|
3358
|
+
if (normalizedName.includes("/")) normalizedName = normalizedName.split("/")[1];
|
|
3359
|
+
normalizedName = normalizedName.replace(/_/g, "");
|
|
3360
|
+
normalizedName = normalizedName.replace(/\./g, "");
|
|
3361
|
+
return normalizedName;
|
|
3362
|
+
}
|
|
3363
|
+
function getProvider(baseURL) {
|
|
3364
|
+
if (!baseURL) return void 0;
|
|
3365
|
+
try {
|
|
3366
|
+
if (new URL(baseURL).hostname === "openrouter.ai") return "openrouter";
|
|
3367
|
+
return;
|
|
3368
|
+
} catch {
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
//#endregion
|
|
3373
|
+
//#region src/llm/OpenAICompatibleClient.ts
|
|
3374
|
+
/**
|
|
3375
|
+
* OpenAI Client implementation
|
|
3376
|
+
*/
|
|
3377
|
+
/**
|
|
3378
|
+
* Client for OpenAI compatible APIs
|
|
3379
|
+
*/
|
|
3380
|
+
var OpenAICompatibleClient = class {
|
|
3381
|
+
config;
|
|
3382
|
+
fetch;
|
|
3383
|
+
constructor(config) {
|
|
3384
|
+
this.config = config;
|
|
3385
|
+
this.fetch = config.customFetch;
|
|
3386
|
+
}
|
|
3387
|
+
async invoke(messages, tools, abortSignal, options) {
|
|
3388
|
+
abortSignal?.throwIfAborted();
|
|
3389
|
+
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t));
|
|
3390
|
+
let toolChoice = "required";
|
|
3391
|
+
if (options?.toolChoiceName && !this.config.disableNamedToolChoice) toolChoice = {
|
|
3392
|
+
type: "function",
|
|
3393
|
+
function: { name: options.toolChoiceName }
|
|
3394
|
+
};
|
|
3395
|
+
const requestBody = {
|
|
3396
|
+
model: this.config.model,
|
|
3397
|
+
messages,
|
|
3398
|
+
tools: openaiTools,
|
|
3399
|
+
parallel_tool_calls: false,
|
|
3400
|
+
tool_choice: toolChoice
|
|
3401
|
+
};
|
|
3402
|
+
if (this.config.temperature !== void 0) requestBody.temperature = this.config.temperature;
|
|
3403
|
+
modelPatch(requestBody, this.config.baseURL);
|
|
3404
|
+
let transformedBody;
|
|
3405
|
+
try {
|
|
3406
|
+
transformedBody = this.config.transformRequestBody(requestBody);
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
throw new InvokeError(InvokeErrorTypes.CONFIG_ERROR, `transformRequestBody failed: ${error.message}`, error);
|
|
3409
|
+
}
|
|
3410
|
+
const finalRequestBody = transformedBody ?? requestBody;
|
|
3411
|
+
let response;
|
|
3412
|
+
try {
|
|
3413
|
+
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
|
|
3414
|
+
method: "POST",
|
|
3415
|
+
headers: {
|
|
3416
|
+
"Content-Type": "application/json",
|
|
3417
|
+
...this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }
|
|
3418
|
+
},
|
|
3419
|
+
body: JSON.stringify(finalRequestBody),
|
|
3420
|
+
signal: abortSignal
|
|
3421
|
+
});
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
if (error?.name === "AbortError") throw error;
|
|
3424
|
+
console.error(error);
|
|
3425
|
+
throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, "Network request failed", error);
|
|
3426
|
+
}
|
|
3427
|
+
if (!response.ok) {
|
|
3428
|
+
let errorData;
|
|
3429
|
+
try {
|
|
3430
|
+
errorData = await response.json();
|
|
3431
|
+
} catch (error) {
|
|
3432
|
+
if (error?.name === "AbortError") throw error;
|
|
3433
|
+
}
|
|
3434
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
3435
|
+
if (response.status === 401 || response.status === 403) throw new InvokeError(InvokeErrorTypes.AUTH_ERROR, `Authentication failed: ${errorMessage}`, errorData);
|
|
3436
|
+
if (response.status === 429) throw new InvokeError(InvokeErrorTypes.RATE_LIMIT, `Rate limit exceeded: ${errorMessage}`, errorData);
|
|
3437
|
+
if (response.status >= 500) throw new InvokeError(InvokeErrorTypes.SERVER_ERROR, `Server error: ${errorMessage}`, errorData);
|
|
3438
|
+
throw new InvokeError(InvokeErrorTypes.UNKNOWN, `HTTP ${response.status}: ${errorMessage}`, errorData);
|
|
3439
|
+
}
|
|
3440
|
+
let data;
|
|
3441
|
+
try {
|
|
3442
|
+
data = await response.json();
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
if (error?.name === "AbortError") throw error;
|
|
3445
|
+
throw new InvokeError(InvokeErrorTypes.INVALID_RESPONSE, "Response body is not valid JSON", error);
|
|
3446
|
+
}
|
|
3447
|
+
const choice = data.choices?.[0];
|
|
3448
|
+
if (!choice) throw new InvokeError(InvokeErrorTypes.INVALID_SCHEMA, "No choices in response", data);
|
|
3449
|
+
switch (choice.finish_reason) {
|
|
3450
|
+
case "tool_calls":
|
|
3451
|
+
case "function_call":
|
|
3452
|
+
case "stop": break;
|
|
3453
|
+
case "length": throw new InvokeError(InvokeErrorTypes.CONTEXT_LENGTH, "Response truncated: max tokens reached", void 0, data);
|
|
3454
|
+
case "content_filter": throw new InvokeError(InvokeErrorTypes.CONTENT_FILTER, "Content filtered by safety system", void 0, data);
|
|
3455
|
+
default: throw new InvokeError(InvokeErrorTypes.INVALID_SCHEMA, `Unexpected finish_reason: ${choice.finish_reason}`, void 0, data);
|
|
3456
|
+
}
|
|
3457
|
+
const normalizedChoice = (options?.normalizeResponse ? options.normalizeResponse(data) : data).choices?.[0];
|
|
3458
|
+
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name;
|
|
3459
|
+
if (!toolCallName) throw new InvokeError(InvokeErrorTypes.NO_TOOL_CALL, "No tool call found in response", void 0, data);
|
|
3460
|
+
const tool = tools[toolCallName];
|
|
3461
|
+
if (!tool) throw new InvokeError(InvokeErrorTypes.UNKNOWN, `Tool "${toolCallName}" not found in tools`, void 0, data);
|
|
3462
|
+
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments;
|
|
3463
|
+
if (!argString) throw new InvokeError(InvokeErrorTypes.INVALID_TOOL_ARGS, "No tool call arguments found", void 0, data);
|
|
3464
|
+
let parsedArgs;
|
|
3465
|
+
try {
|
|
3466
|
+
parsedArgs = JSON.parse(argString);
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
throw new InvokeError(InvokeErrorTypes.INVALID_TOOL_ARGS, "Failed to parse tool arguments as JSON", error, data);
|
|
3469
|
+
}
|
|
3470
|
+
const validation = tool.inputSchema.safeParse(parsedArgs);
|
|
3471
|
+
if (!validation.success) {
|
|
3472
|
+
console.error(z.prettifyError(validation.error));
|
|
3473
|
+
throw new InvokeError(InvokeErrorTypes.INVALID_TOOL_ARGS, "Tool arguments validation failed", validation.error, data);
|
|
3474
|
+
}
|
|
3475
|
+
const toolInput = validation.data;
|
|
3476
|
+
let toolResult;
|
|
3477
|
+
try {
|
|
3478
|
+
toolResult = await tool.execute(toolInput);
|
|
3479
|
+
} catch (error) {
|
|
3480
|
+
if (error?.name === "AbortError") throw error;
|
|
3481
|
+
throw new InvokeError(InvokeErrorTypes.TOOL_EXECUTION_ERROR, `Tool execution failed: ${error?.message}`, error, data);
|
|
3482
|
+
}
|
|
3483
|
+
return {
|
|
3484
|
+
toolCall: {
|
|
3485
|
+
name: toolCallName,
|
|
3486
|
+
args: toolInput
|
|
3487
|
+
},
|
|
3488
|
+
toolResult,
|
|
3489
|
+
usage: {
|
|
3490
|
+
promptTokens: data.usage?.prompt_tokens ?? 0,
|
|
3491
|
+
completionTokens: data.usage?.completion_tokens ?? 0,
|
|
3492
|
+
totalTokens: data.usage?.total_tokens ?? 0,
|
|
3493
|
+
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
|
|
3494
|
+
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens
|
|
3495
|
+
},
|
|
3496
|
+
rawResponse: data,
|
|
3497
|
+
rawRequest: finalRequestBody
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
};
|
|
3501
|
+
//#endregion
|
|
3502
|
+
//#region src/llm/LLM.ts
|
|
3503
|
+
/**
|
|
3504
|
+
* LLM module
|
|
3505
|
+
*/
|
|
3506
|
+
var LLM = class extends EventTarget {
|
|
3507
|
+
config;
|
|
3508
|
+
client;
|
|
3509
|
+
constructor(config) {
|
|
3510
|
+
super();
|
|
3511
|
+
this.config = parseLLMConfig(config);
|
|
3512
|
+
this.client = new OpenAICompatibleClient(this.config);
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* - call llm api *once*
|
|
3516
|
+
* - invoke tool call *once*
|
|
3517
|
+
* - return the result of the tool
|
|
3518
|
+
*/
|
|
3519
|
+
async invoke(messages, tools, abortSignal, options) {
|
|
3520
|
+
return await withRetry(async () => this.client.invoke(messages, tools, abortSignal, options), {
|
|
3521
|
+
maxRetries: this.config.maxRetries,
|
|
3522
|
+
onRetry: (attempt, lastError) => {
|
|
3523
|
+
this.dispatchEvent(new CustomEvent("retry", { detail: {
|
|
3524
|
+
attempt,
|
|
3525
|
+
maxAttempts: this.config.maxRetries,
|
|
3526
|
+
lastError
|
|
3527
|
+
} }));
|
|
3528
|
+
}
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
/**
|
|
3533
|
+
* Retry a function until it succeeds or reaches the maximum number of retries.
|
|
3534
|
+
*/
|
|
3535
|
+
async function withRetry(fn, settings) {
|
|
3536
|
+
let attempt = 0;
|
|
3537
|
+
while (true) try {
|
|
3538
|
+
return await fn();
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
if (error?.name === "AbortError") throw error;
|
|
3541
|
+
if (error instanceof InvokeError && !error.retryable) throw error;
|
|
3542
|
+
attempt++;
|
|
3543
|
+
if (attempt > settings.maxRetries) throw error;
|
|
3544
|
+
console.debug("[LLM] retryable failure, will retry:", error);
|
|
3545
|
+
settings.onRetry(attempt, error);
|
|
3546
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
function parseLLMConfig(config) {
|
|
3550
|
+
if (!config.baseURL || !config.model) throw new Error("[PageAgent] LLM configuration required. Please provide: baseURL, model. See: https://alibaba.github.io/page-agent/docs/features/models");
|
|
3551
|
+
if (config.temperature !== void 0) console.warn("[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. Use transformRequestBody to set it only for models you have verified accept it.");
|
|
3552
|
+
return {
|
|
3553
|
+
baseURL: config.baseURL,
|
|
3554
|
+
model: config.model,
|
|
3555
|
+
apiKey: config.apiKey || "",
|
|
3556
|
+
temperature: config.temperature,
|
|
3557
|
+
maxRetries: config.maxRetries ?? 2,
|
|
3558
|
+
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
|
3559
|
+
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
|
3560
|
+
customFetch: (config.customFetch ?? fetch).bind(globalThis)
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
//#endregion
|
|
3564
|
+
//#region src/agent/prompts/system_prompt.md?raw
|
|
3565
|
+
var system_prompt_default = "You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating complex websites and extracting precise information\n2. Automating form submissions and interactive web actions\n3. Gathering and saving information \n4. Operate effectively in an agent loop\n5. Efficiently performing diverse web tasks\n</intro>\n\n<language_settings>\n- Default working language: **English**\n- Use the language that user is using. Return in user's language.\n</language_settings>\n\n<input>\nAt every step, your input will consist of: \n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> and <step_info>.\n3. <browser_state>: Current URL, interactive elements indexed for actions, and visible page content.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is your ultimate objective and always remains visible.\n- This has the highest priority. Make the user happy.\n- If the user request is very specific - then carefully follow each step and don't skip or hallucinate steps.\n- If the task is open ended you can plan yourself how to get it done.\n</user_request>\n\n<browser_state>\n1. Browser State will be given as:\n\nCurrent URL: URL of the page you are currently viewing.\nInteractive Elements: All interactive elements will be provided in format as [index]<type>text</type> where\n- index: Numeric identifier for interaction\n- type: HTML element type (button, input, etc.)\n- text: Element description\n\nExamples:\n[33]<div>User form</div>\n\\t*[35]<button aria-label='Submit form'>Submit</button>\n\nNote that:\n- Only elements with numeric indexes in [] are interactive\n- (stacked) indentation (with \\t) is important and means that the element is a (html) child of the element above (with a lower index)\n- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.\n- Pure text elements without [] are not interactive.\n</browser_state>\n\n<browser_rules>\nStrictly follow these rules while using the browser and navigating the web:\n- Only interact with elements that have a numeric [index] assigned.\n- Only use indexes that are explicitly provided.\n- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.\n- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.\n- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).\n- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.\n- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat one action for more than 3 times unless some conditions changed.\n- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.\n- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.\n- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.\n- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.\n- Don't login into a page if you don't have to. Don't login if you don't have the credentials. \n- There are 2 types of tasks always first think which type of request you are dealing with:\n1. Very specific step by step instructions:\n- Follow them as very precise and don't skip steps. Try to complete everything as requested.\n2. Open ended tasks. Plan yourself, be creative in achieving them.\n- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.\n</browser_rules>\n\n<capability>\n- You can only handle single page app. Do not jump out of current page.\n- Do not click on link if it will open in a new page (e.g., <a target=\"_blank\">)\n- It is ok to fail the task.\n - User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.\n - Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.\n - Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.\n- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.\n</capability>\n\n<task_completion_rules>\nYou must call the `done` action in one of three cases:\n- When you have fully completed the USER REQUEST.\n- When you reach the final allowed step (`max_steps`), even if the task is incomplete.\n- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.\n- If it is ABSOLUTELY IMPOSSIBLE to continue.\n\nThe `done` action is your opportunity to terminate and share your findings with the user.\n- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.\n- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.\n- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.\n- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.\n- If the user asks for specified format, such as \"return JSON with following structure\", \"return a list of format...\", MAKE sure to use the right format in your answer.\n- If the user asks for a structured output, your `done` action's schema may be modified. Take this schema into account when solving the task!\n</task_completion_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns to successfully achieve the <user_request>:\n\n- Reason about <agent_history> to track progress and context toward <user_request>.\n- Analyze the most recent \"Next Goal\" and \"Action Result\" in <agent_history> and clearly state what you previously tried to achieve.\n- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.\n- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.\n- Ask user for help if you have any difficulty. Keep user in the loop.\n- If you see information relevant to <user_request>, plan saving the information to memory.\n- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns. Use them as reference but never copy them directly.\n\n<evaluation_examples>\n\"evaluation_previous_goal\": \"Successfully navigated to the product page and found the target information. Verdict: Success\"\n\"evaluation_previous_goal\": \"Clicked the login button and user authentication form appeared. Verdict: Success\"\n</evaluation_examples>\n\n<memory_examples>\n\"memory\": \"Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports.\"\n</memory_examples>\n\n<next_goal_examples>\n\"next_goal\": \"Click on the 'Add to Cart' button to proceed with the purchase flow.\"\n</next_goal_examples>\n</examples>\n\n<output>\n{\n \"evaluation_previous_goal\": \"Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.\",\n \"memory\": \"1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.\",\n \"next_goal\": \"State the next immediate goal and action to achieve it, in one clear sentence.\",\n \"action\":{\n \"Action name\": {// Action parameters}\n }\n}\n</output>\n";
|
|
3566
|
+
//#endregion
|
|
3567
|
+
//#region src/agent/utils/autoFixer.ts
|
|
3568
|
+
var log = console.log.bind(console, chalk.yellow("[autoFixer]"));
|
|
3569
|
+
/**
|
|
3570
|
+
* Normalize LLM response and fix common format issues.
|
|
3571
|
+
*
|
|
3572
|
+
* Handles:
|
|
3573
|
+
* - No tool_calls but JSON in message.content (fallback)
|
|
3574
|
+
* - Model returns action name as tool call instead of AgentOutput
|
|
3575
|
+
* - Arguments wrapped as double JSON string
|
|
3576
|
+
* - Nested function call format
|
|
3577
|
+
* - Missing action field (fallback to wait)
|
|
3578
|
+
* - Primitive action input for single-field tools (e.g. `{"click_element_by_index": 2}`)
|
|
3579
|
+
* - etc.
|
|
3580
|
+
*/
|
|
3581
|
+
function normalizeResponse(response, tools) {
|
|
3582
|
+
let resolvedArguments;
|
|
3583
|
+
const choice = response.choices?.[0];
|
|
3584
|
+
if (!choice) throw new Error("No choices in response");
|
|
3585
|
+
const message = choice.message;
|
|
3586
|
+
if (!message) throw new Error("No message in choice");
|
|
3587
|
+
const toolCall = message.tool_calls?.[0];
|
|
3588
|
+
if (toolCall?.function?.arguments) {
|
|
3589
|
+
resolvedArguments = safeJsonParse(toolCall.function.arguments);
|
|
3590
|
+
if (toolCall.function.name && toolCall.function.name !== "AgentOutput") {
|
|
3591
|
+
log(`#1: fixing tool_call`);
|
|
3592
|
+
resolvedArguments = { action: safeJsonParse(resolvedArguments) };
|
|
3593
|
+
}
|
|
3594
|
+
} else if (message.content) {
|
|
3595
|
+
const jsonInContent = retrieveJsonFromString(message.content.trim());
|
|
3596
|
+
if (jsonInContent) {
|
|
3597
|
+
resolvedArguments = safeJsonParse(jsonInContent);
|
|
3598
|
+
if (resolvedArguments?.name === "AgentOutput") {
|
|
3599
|
+
log(`#2: fixing tool_call`);
|
|
3600
|
+
resolvedArguments = safeJsonParse(resolvedArguments.arguments);
|
|
3601
|
+
}
|
|
3602
|
+
if (resolvedArguments?.type === "function") {
|
|
3603
|
+
log(`#3: fixing tool_call`);
|
|
3604
|
+
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments);
|
|
3605
|
+
}
|
|
3606
|
+
if (!resolvedArguments?.action && !resolvedArguments?.evaluation_previous_goal && !resolvedArguments?.memory && !resolvedArguments?.next_goal && !resolvedArguments?.thinking) {
|
|
3607
|
+
log(`#4: fixing tool_call`);
|
|
3608
|
+
resolvedArguments = { action: safeJsonParse(resolvedArguments) };
|
|
3609
|
+
}
|
|
3610
|
+
} else throw new Error("No tool_call and the message content does not contain valid JSON");
|
|
3611
|
+
} else throw new Error("No tool_call nor message content is present");
|
|
3612
|
+
resolvedArguments = safeJsonParse(resolvedArguments);
|
|
3613
|
+
if (resolvedArguments.action) resolvedArguments.action = safeJsonParse(resolvedArguments.action);
|
|
3614
|
+
if (resolvedArguments.action && tools) resolvedArguments.action = validateAction(resolvedArguments.action, tools);
|
|
3615
|
+
if (!resolvedArguments.action) {
|
|
3616
|
+
log(`#5: fixing tool_call`);
|
|
3617
|
+
resolvedArguments.action = { wait: { seconds: 1 } };
|
|
3618
|
+
}
|
|
3619
|
+
return {
|
|
3620
|
+
...response,
|
|
3621
|
+
choices: [{
|
|
3622
|
+
...choice,
|
|
3623
|
+
message: {
|
|
3624
|
+
...message,
|
|
3625
|
+
tool_calls: [{
|
|
3626
|
+
...toolCall || {},
|
|
3627
|
+
function: {
|
|
3628
|
+
...toolCall?.function || {},
|
|
3629
|
+
name: "AgentOutput",
|
|
3630
|
+
arguments: JSON.stringify(resolvedArguments)
|
|
3631
|
+
}
|
|
3632
|
+
}]
|
|
3633
|
+
}
|
|
3634
|
+
}]
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Validate action against tool schemas. Provides clear error messages
|
|
3639
|
+
* instead of letting the union schema produce unreadable errors.
|
|
3640
|
+
*
|
|
3641
|
+
* Also coerces primitive inputs for single-field tools:
|
|
3642
|
+
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
|
|
3643
|
+
*/
|
|
3644
|
+
function validateAction(action, tools) {
|
|
3645
|
+
if (typeof action !== "object" || action === null) return action;
|
|
3646
|
+
const toolName = Object.keys(action)[0];
|
|
3647
|
+
if (!toolName) return action;
|
|
3648
|
+
const tool = tools.get(toolName);
|
|
3649
|
+
if (!tool) {
|
|
3650
|
+
const available = Array.from(tools.keys()).join(", ");
|
|
3651
|
+
throw new InvokeError(InvokeErrorTypes.INVALID_TOOL_ARGS, `Unknown action "${toolName}". Available: ${available}`);
|
|
3652
|
+
}
|
|
3653
|
+
let value = action[toolName];
|
|
3654
|
+
const schema = tool.inputSchema;
|
|
3655
|
+
if (schema instanceof z.ZodObject && value !== null && typeof value !== "object") {
|
|
3656
|
+
const requiredKey = Object.keys(schema.shape).find((k) => !schema.shape[k].safeParse(void 0).success);
|
|
3657
|
+
if (requiredKey) {
|
|
3658
|
+
log(`coercing primitive action input for "${toolName}"`);
|
|
3659
|
+
value = { [requiredKey]: value };
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
const result = schema.safeParse(value);
|
|
3663
|
+
if (!result.success) throw new InvokeError(InvokeErrorTypes.INVALID_TOOL_ARGS, `Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`);
|
|
3664
|
+
return { [toolName]: result.data };
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Safely parse JSON, return original input if not json.
|
|
3668
|
+
*/
|
|
3669
|
+
function safeJsonParse(input) {
|
|
3670
|
+
if (typeof input === "string") try {
|
|
3671
|
+
return JSON.parse(input.trim());
|
|
3672
|
+
} catch {
|
|
3673
|
+
return input;
|
|
3674
|
+
}
|
|
3675
|
+
return input;
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Extract and parse JSON from a string.
|
|
3679
|
+
* - Treat content between the first `{` and the last `}` as JSON.
|
|
3680
|
+
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
|
|
3681
|
+
*/
|
|
3682
|
+
function retrieveJsonFromString(str) {
|
|
3683
|
+
try {
|
|
3684
|
+
const json = /({[\s\S]*})/.exec(str) ?? [];
|
|
3685
|
+
if (json.length === 0) return null;
|
|
3686
|
+
return JSON.parse(json[0]);
|
|
3687
|
+
} catch {
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region src/agent/utils/index.ts
|
|
3693
|
+
/**
|
|
3694
|
+
* Wait for `seconds`. If a `signal` is provided, the wait is cancellable:
|
|
3695
|
+
* aborting rejects with the signal's reason (an `AbortError`).
|
|
3696
|
+
*/
|
|
3697
|
+
async function waitFor(seconds, signal) {
|
|
3698
|
+
if (!signal) {
|
|
3699
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
signal.throwIfAborted();
|
|
3703
|
+
await new Promise((resolve, reject) => {
|
|
3704
|
+
const timer = setTimeout(() => {
|
|
3705
|
+
signal.removeEventListener("abort", onAbort);
|
|
3706
|
+
resolve();
|
|
3707
|
+
}, seconds * 1e3);
|
|
3708
|
+
const onAbort = () => {
|
|
3709
|
+
clearTimeout(timer);
|
|
3710
|
+
reject(signal.reason);
|
|
3711
|
+
};
|
|
3712
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
function truncate(text, maxLength) {
|
|
3716
|
+
if (text.length > maxLength) return text.substring(0, maxLength) + "...";
|
|
3717
|
+
return text;
|
|
3718
|
+
}
|
|
3719
|
+
function randomID(existingIDs) {
|
|
3720
|
+
let id = Math.random().toString(36).substring(2, 11);
|
|
3721
|
+
if (!existingIDs) return id;
|
|
3722
|
+
const MAX_TRY = 1e3;
|
|
3723
|
+
let tryCount = 0;
|
|
3724
|
+
while (existingIDs.includes(id)) {
|
|
3725
|
+
id = Math.random().toString(36).substring(2, 11);
|
|
3726
|
+
tryCount++;
|
|
3727
|
+
if (tryCount > MAX_TRY) throw new Error("randomID: too many tries");
|
|
3728
|
+
}
|
|
3729
|
+
return id;
|
|
3730
|
+
}
|
|
3731
|
+
var _global = globalThis;
|
|
3732
|
+
if (!_global.__PAGE_AGENT_IDS__) _global.__PAGE_AGENT_IDS__ = [];
|
|
3733
|
+
var ids = _global.__PAGE_AGENT_IDS__;
|
|
3734
|
+
/**
|
|
3735
|
+
* Generate a random ID.
|
|
3736
|
+
* @note Unique within this window.
|
|
3737
|
+
*/
|
|
3738
|
+
function uid() {
|
|
3739
|
+
const id = randomID(ids);
|
|
3740
|
+
ids.push(id);
|
|
3741
|
+
return id;
|
|
3742
|
+
}
|
|
3743
|
+
var llmsTxtCache = /* @__PURE__ */ new Map();
|
|
3744
|
+
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
|
|
3745
|
+
async function fetchLlmsTxt(url) {
|
|
3746
|
+
let origin;
|
|
3747
|
+
try {
|
|
3748
|
+
origin = new URL(url).origin;
|
|
3749
|
+
} catch {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
if (origin === "null") return null;
|
|
3753
|
+
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin);
|
|
3754
|
+
const endpoint = `${origin}/llms.txt`;
|
|
3755
|
+
let result = null;
|
|
3756
|
+
try {
|
|
3757
|
+
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`));
|
|
3758
|
+
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3e3) });
|
|
3759
|
+
if (res.ok) {
|
|
3760
|
+
result = await res.text();
|
|
3761
|
+
console.log(chalk.green(`[llms.txt] Found (${result.length} chars)`));
|
|
3762
|
+
if (result.length > 1e3) {
|
|
3763
|
+
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`));
|
|
3764
|
+
result = truncate(result, 1e3);
|
|
3765
|
+
}
|
|
3766
|
+
} else console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`));
|
|
3767
|
+
} catch (e) {
|
|
3768
|
+
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e);
|
|
3769
|
+
}
|
|
3770
|
+
llmsTxtCache.set(origin, result);
|
|
3771
|
+
return result;
|
|
3772
|
+
}
|
|
3773
|
+
/**
|
|
3774
|
+
* Simple assertion function that throws an error if the condition is falsy
|
|
3775
|
+
* @param condition - The condition to assert
|
|
3776
|
+
* @param message - Optional error message
|
|
3777
|
+
* @throws Error if condition is falsy
|
|
3778
|
+
*/
|
|
3779
|
+
function assert(condition, message, silent) {
|
|
3780
|
+
if (!condition) {
|
|
3781
|
+
const errorMessage = message ?? "Assertion failed";
|
|
3782
|
+
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`));
|
|
3783
|
+
throw new Error(errorMessage);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Suppress errors from a function.
|
|
3788
|
+
*/
|
|
3789
|
+
async function suppress(fn) {
|
|
3790
|
+
try {
|
|
3791
|
+
return await fn();
|
|
3792
|
+
} catch (error) {
|
|
3793
|
+
console.error(error);
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
//#endregion
|
|
3798
|
+
//#region src/agent/tools/index.ts
|
|
3799
|
+
/**
|
|
3800
|
+
* Internal tools for PageAgent.
|
|
3801
|
+
* @note Adapted from browser-use
|
|
3802
|
+
*/
|
|
3803
|
+
function tool(options) {
|
|
3804
|
+
return options;
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Internal tools for PageAgent.
|
|
3808
|
+
* Note: Using any to allow different parameter types for each tool
|
|
3809
|
+
*/
|
|
3810
|
+
var tools = /* @__PURE__ */ new Map();
|
|
3811
|
+
tools.set("done", tool({
|
|
3812
|
+
description: "Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.",
|
|
3813
|
+
inputSchema: z.object({
|
|
3814
|
+
text: z.string(),
|
|
3815
|
+
success: z.boolean().default(true)
|
|
3816
|
+
}),
|
|
3817
|
+
execute: async function(_input) {
|
|
3818
|
+
return Promise.resolve("Task completed");
|
|
3819
|
+
}
|
|
3820
|
+
}));
|
|
3821
|
+
tools.set("wait", tool({
|
|
3822
|
+
description: "Wait for x seconds. Can be used to wait until the page or data is fully loaded.",
|
|
3823
|
+
inputSchema: z.object({ seconds: z.number().min(1).max(10).default(1) }),
|
|
3824
|
+
execute: async function(input, { signal }) {
|
|
3825
|
+
const lastTimeUpdate = await this.browserController.getLastUpdateTime();
|
|
3826
|
+
const secondsSinceLastUpdate = (Date.now() - lastTimeUpdate) / 1e3;
|
|
3827
|
+
const actualWaitTime = Math.max(0, input.seconds - secondsSinceLastUpdate);
|
|
3828
|
+
console.log(`actualWaitTime: ${actualWaitTime} seconds`);
|
|
3829
|
+
await waitFor(actualWaitTime, signal);
|
|
3830
|
+
return `✅ Waited for ${(secondsSinceLastUpdate + actualWaitTime).toFixed(2)} seconds.`;
|
|
3831
|
+
}
|
|
3832
|
+
}));
|
|
3833
|
+
tools.set("ask_user", tool({
|
|
3834
|
+
description: "Ask the user a question and wait for their answer. Use this if you need more information or clarification.",
|
|
3835
|
+
inputSchema: z.object({ question: z.string() }),
|
|
3836
|
+
execute: async function(input, { signal }) {
|
|
3837
|
+
if (!this.onAskUser) throw new Error("ask_user tool requires onAskUser callback to be set");
|
|
3838
|
+
return `User answered: ${await this.onAskUser(input.question, { signal })}`;
|
|
3839
|
+
}
|
|
3840
|
+
}));
|
|
3841
|
+
tools.set("click_element_by_index", tool({
|
|
3842
|
+
description: "Click element by index",
|
|
3843
|
+
inputSchema: z.object({ index: z.int().min(0) }),
|
|
3844
|
+
execute: async function(input) {
|
|
3845
|
+
return (await this.browserController.clickElement(input.index)).message;
|
|
3846
|
+
}
|
|
3847
|
+
}));
|
|
3848
|
+
tools.set("input_text", tool({
|
|
3849
|
+
description: "Click and type text into an interactive input element",
|
|
3850
|
+
inputSchema: z.object({
|
|
3851
|
+
index: z.int().min(0),
|
|
3852
|
+
text: z.string()
|
|
3853
|
+
}),
|
|
3854
|
+
execute: async function(input) {
|
|
3855
|
+
return (await this.browserController.inputText(input.index, input.text)).message;
|
|
3856
|
+
}
|
|
3857
|
+
}));
|
|
3858
|
+
tools.set("select_dropdown_option", tool({
|
|
3859
|
+
description: "Select dropdown option for interactive element index by the text of the option you want to select",
|
|
3860
|
+
inputSchema: z.object({
|
|
3861
|
+
index: z.int().min(0),
|
|
3862
|
+
text: z.string()
|
|
3863
|
+
}),
|
|
3864
|
+
execute: async function(input) {
|
|
3865
|
+
return (await this.browserController.selectOption(input.index, input.text)).message;
|
|
3866
|
+
}
|
|
3867
|
+
}));
|
|
3868
|
+
/**
|
|
3869
|
+
* @note Reference from browser-use
|
|
3870
|
+
*/
|
|
3871
|
+
tools.set("scroll", tool({
|
|
3872
|
+
description: "Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
|
|
3873
|
+
inputSchema: z.object({
|
|
3874
|
+
down: z.boolean().default(true),
|
|
3875
|
+
num_pages: z.number().min(0).max(10).optional().default(.1),
|
|
3876
|
+
pixels: z.number().int().min(0).optional(),
|
|
3877
|
+
index: z.number().int().min(0).optional()
|
|
3878
|
+
}),
|
|
3879
|
+
execute: async function(input) {
|
|
3880
|
+
return (await this.browserController.scroll({
|
|
3881
|
+
...input,
|
|
3882
|
+
numPages: input.num_pages
|
|
3883
|
+
})).message;
|
|
3884
|
+
}
|
|
3885
|
+
}));
|
|
3886
|
+
/**
|
|
3887
|
+
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
|
|
3888
|
+
*/
|
|
3889
|
+
tools.set("scroll_horizontally", tool({
|
|
3890
|
+
description: "Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
|
|
3891
|
+
inputSchema: z.object({
|
|
3892
|
+
right: z.boolean().default(true),
|
|
3893
|
+
pixels: z.number().int().min(0),
|
|
3894
|
+
index: z.number().int().min(0).optional()
|
|
3895
|
+
}),
|
|
3896
|
+
execute: async function(input) {
|
|
3897
|
+
return (await this.browserController.scrollHorizontally(input)).message;
|
|
3898
|
+
}
|
|
3899
|
+
}));
|
|
3900
|
+
tools.set("execute_javascript", tool({
|
|
3901
|
+
description: "Execute JavaScript code on the current page. Supports async/await syntax. Use with caution! An `AbortSignal` named `signal` is available in scope: long-running async code MUST honor it (e.g. `await fetch(url, { signal })`, or `signal.throwIfAborted()` in loops)",
|
|
3902
|
+
inputSchema: z.object({ script: z.string() }),
|
|
3903
|
+
execute: async function(input, { signal }) {
|
|
3904
|
+
const result = await this.browserController.executeJavascript(input.script, signal);
|
|
3905
|
+
signal.throwIfAborted();
|
|
3906
|
+
return result.message;
|
|
3907
|
+
}
|
|
3908
|
+
}));
|
|
3909
|
+
//#endregion
|
|
3910
|
+
//#region src/agent/AgentRuntime.ts
|
|
3911
|
+
/**
|
|
3912
|
+
* AI agent for browser automation.
|
|
3913
|
+
*
|
|
3914
|
+
* @remarks
|
|
3915
|
+
* ## Re-act Agent Loop
|
|
3916
|
+
* - step
|
|
3917
|
+
* - observe (gather information about current environment and context)
|
|
3918
|
+
* - think (LLM calling)
|
|
3919
|
+
* - reflection (evaluate history, generate memory, short-term planning)
|
|
3920
|
+
* - action (give the action to approach the next goal)
|
|
3921
|
+
* - act (execute the action)
|
|
3922
|
+
* - loop
|
|
3923
|
+
*
|
|
3924
|
+
* ## Event System
|
|
3925
|
+
* - `statuschange` - Agent status transitions (idle → running → completed/error/stopped)
|
|
3926
|
+
* - `historychange` - History events updated (persistent, part of agent memory)
|
|
3927
|
+
* - `activity` - Real-time activity feedback (transient, for UI only)
|
|
3928
|
+
* - `dispose` - Agent cleanup triggered
|
|
3929
|
+
*
|
|
3930
|
+
* ## Information Streams
|
|
3931
|
+
* 1. **History Events** (`history` array)
|
|
3932
|
+
* - Persistent event stream that forms agent's memory
|
|
3933
|
+
* - Included in LLM context across steps
|
|
3934
|
+
* - Types: steps, observations, user takeovers, llm errors
|
|
3935
|
+
*
|
|
3936
|
+
* 2. **Activity Events** (via `activity` event)
|
|
3937
|
+
* - Transient UI feedback during task execution
|
|
3938
|
+
* - NOT included in LLM context
|
|
3939
|
+
* - Types: thinking, executing, executed, retrying, error
|
|
3940
|
+
*/
|
|
3941
|
+
var AgentRuntime = class extends EventTarget {
|
|
3942
|
+
id = uid();
|
|
3943
|
+
config;
|
|
3944
|
+
tools;
|
|
3945
|
+
/** BrowserController for DOM operations */
|
|
3946
|
+
browserController;
|
|
3947
|
+
task = "";
|
|
3948
|
+
taskId = "";
|
|
3949
|
+
/** History events */
|
|
3950
|
+
history = [];
|
|
3951
|
+
/** Whether this agent has been disposed */
|
|
3952
|
+
disposed = false;
|
|
3953
|
+
/**
|
|
3954
|
+
* Called when the agent needs to ask the user questions.
|
|
3955
|
+
* If unset, the `ask_user` tool will be disabled.
|
|
3956
|
+
* Implementations should reject the promise when `signal` aborts.
|
|
3957
|
+
* @example onAskUser: (q) => window.prompt(q) || ''
|
|
3958
|
+
*/
|
|
3959
|
+
onAskUser;
|
|
3960
|
+
#status = "idle";
|
|
3961
|
+
#llm;
|
|
3962
|
+
/**
|
|
3963
|
+
* Task cancellation primitive: its signal reaches the LLM fetch, tools
|
|
3964
|
+
* (via `ctx.signal`) and async callbacks. Aborted only by `stop`/`dispose`
|
|
3965
|
+
* (during a task) or task setup, always WITHOUT a reason so `signal.reason`
|
|
3966
|
+
* stays a standard `AbortError`.
|
|
3967
|
+
*/
|
|
3968
|
+
#abortController = new AbortController();
|
|
3969
|
+
#observations = [];
|
|
3970
|
+
/** Resolves when the current run has fully settled. Awaited by `stop()`. */
|
|
3971
|
+
#running = Promise.resolve();
|
|
3972
|
+
#lastResult = null;
|
|
3973
|
+
/** internal states during a single task execution */
|
|
3974
|
+
#states = {
|
|
3975
|
+
/** Accumulated wait time in seconds */
|
|
3976
|
+
totalWaitTime: 0,
|
|
3977
|
+
/** For detecting navigation */
|
|
3978
|
+
lastURL: "",
|
|
3979
|
+
/** Browser state */
|
|
3980
|
+
browserState: null
|
|
3981
|
+
};
|
|
3982
|
+
constructor(config) {
|
|
3983
|
+
super();
|
|
3984
|
+
this.config = {
|
|
3985
|
+
...config,
|
|
3986
|
+
maxSteps: config.maxSteps ?? 40
|
|
3987
|
+
};
|
|
3988
|
+
this.#llm = new LLM(this.config);
|
|
3989
|
+
this.tools = new Map(tools);
|
|
3990
|
+
this.browserController = config.browserController;
|
|
3991
|
+
this.#llm.addEventListener("retry", (e) => {
|
|
3992
|
+
const { attempt, maxAttempts, lastError } = e.detail;
|
|
3993
|
+
this.#emitActivity({
|
|
3994
|
+
type: "retrying",
|
|
3995
|
+
attempt,
|
|
3996
|
+
maxAttempts
|
|
3997
|
+
});
|
|
3998
|
+
this.history.push({
|
|
3999
|
+
type: "error",
|
|
4000
|
+
message: String(lastError),
|
|
4001
|
+
rawResponse: lastError.rawResponse
|
|
4002
|
+
});
|
|
4003
|
+
this.history.push({
|
|
4004
|
+
type: "retry",
|
|
4005
|
+
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
|
|
4006
|
+
attempt,
|
|
4007
|
+
maxAttempts
|
|
4008
|
+
});
|
|
4009
|
+
this.#emitHistoryChange();
|
|
4010
|
+
});
|
|
4011
|
+
if (this.config.customTools) for (const [name, tool] of Object.entries(this.config.customTools)) {
|
|
4012
|
+
if (tool === null) {
|
|
4013
|
+
this.tools.delete(name);
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
4016
|
+
this.tools.set(name, tool);
|
|
4017
|
+
}
|
|
4018
|
+
if (!this.config.experimentalScriptExecutionTool) this.tools.delete("execute_javascript");
|
|
4019
|
+
}
|
|
4020
|
+
/** Get current agent status */
|
|
4021
|
+
get status() {
|
|
4022
|
+
return this.#status;
|
|
4023
|
+
}
|
|
4024
|
+
/** Result of the most recent run, or `null` before the first run completes. */
|
|
4025
|
+
get lastResult() {
|
|
4026
|
+
return this.#lastResult;
|
|
4027
|
+
}
|
|
4028
|
+
/** Emit statuschange event */
|
|
4029
|
+
#emitStatusChange() {
|
|
4030
|
+
this.dispatchEvent(new Event("statuschange"));
|
|
4031
|
+
}
|
|
4032
|
+
/** Emit historychange event */
|
|
4033
|
+
#emitHistoryChange(pushHistoricalEvent) {
|
|
4034
|
+
if (pushHistoricalEvent) this.history.push(pushHistoricalEvent);
|
|
4035
|
+
this.dispatchEvent(new Event("historychange"));
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Emit activity event - for transient UI feedback
|
|
4039
|
+
* @param activity - Current agent activity
|
|
4040
|
+
*/
|
|
4041
|
+
#emitActivity(activity) {
|
|
4042
|
+
this.dispatchEvent(new CustomEvent("activity", { detail: activity }));
|
|
4043
|
+
}
|
|
4044
|
+
/** Update status and emit event */
|
|
4045
|
+
#setStatus(status) {
|
|
4046
|
+
if (this.#status !== status) {
|
|
4047
|
+
this.#status = status;
|
|
4048
|
+
this.#emitStatusChange();
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Push an observation message to the history event stream.
|
|
4053
|
+
* This will be visible in <agent_history> and remain persistent in memory across steps.
|
|
4054
|
+
* @experimental @internal
|
|
4055
|
+
* @note history change will be emitted before next step starts
|
|
4056
|
+
*/
|
|
4057
|
+
pushObservation(content) {
|
|
4058
|
+
this.#observations.push(content);
|
|
4059
|
+
}
|
|
4060
|
+
/**
|
|
4061
|
+
* Stop the current task and wait until the run has fully settled (including lifecycle hooks).
|
|
4062
|
+
* @note never await .stop() in a lifecycle hook.
|
|
4063
|
+
*/
|
|
4064
|
+
async stop() {
|
|
4065
|
+
if (this.#status !== "running") return;
|
|
4066
|
+
this.#abortController.abort();
|
|
4067
|
+
await this.#running;
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* external errors (pre-checks/config/hooks) will threw;
|
|
4071
|
+
* agent errors will be caught and added to history, and return a failed result
|
|
4072
|
+
*/
|
|
4073
|
+
async execute(task) {
|
|
4074
|
+
if (this.disposed) throw new Error("PageAgent has been disposed. Create a new instance.");
|
|
4075
|
+
if (this.#status === "running") throw new Error("A task is already running.");
|
|
4076
|
+
if (!task) throw new Error("Task is required");
|
|
4077
|
+
this.task = task;
|
|
4078
|
+
this.taskId = uid();
|
|
4079
|
+
this.history = [];
|
|
4080
|
+
this.#observations = [];
|
|
4081
|
+
this.#states = {
|
|
4082
|
+
totalWaitTime: 0,
|
|
4083
|
+
lastURL: "",
|
|
4084
|
+
browserState: null
|
|
4085
|
+
};
|
|
4086
|
+
this.#abortController = new AbortController();
|
|
4087
|
+
const signal = this.#abortController.signal;
|
|
4088
|
+
let resolveRunning;
|
|
4089
|
+
this.#running = new Promise((r) => resolveRunning = r);
|
|
4090
|
+
this.#setStatus("running");
|
|
4091
|
+
this.#emitHistoryChange();
|
|
4092
|
+
if (!this.onAskUser) this.tools.delete("ask_user");
|
|
4093
|
+
const onBeforeStep = this.config.onBeforeStep;
|
|
4094
|
+
const onAfterStep = this.config.onAfterStep;
|
|
4095
|
+
const onBeforeTask = this.config.onBeforeTask;
|
|
4096
|
+
const onAfterTask = this.config.onAfterTask;
|
|
4097
|
+
const stepDelay = this.config.stepDelay ?? .4;
|
|
4098
|
+
const maxSteps = this.config.maxSteps;
|
|
4099
|
+
let step = 0;
|
|
4100
|
+
let taskResult;
|
|
4101
|
+
let finalStatus = "error";
|
|
4102
|
+
await suppress(() => this.browserController.showMask());
|
|
4103
|
+
try {
|
|
4104
|
+
await onBeforeTask?.(this);
|
|
4105
|
+
while (true) {
|
|
4106
|
+
if (step >= maxSteps) {
|
|
4107
|
+
const message = "Step count exceeded maximum limit";
|
|
4108
|
+
console.error(message);
|
|
4109
|
+
this.#emitActivity({
|
|
4110
|
+
type: "error",
|
|
4111
|
+
message
|
|
4112
|
+
});
|
|
4113
|
+
this.#emitHistoryChange({
|
|
4114
|
+
type: "error",
|
|
4115
|
+
message
|
|
4116
|
+
});
|
|
4117
|
+
taskResult = {
|
|
4118
|
+
success: false,
|
|
4119
|
+
data: message,
|
|
4120
|
+
history: this.history
|
|
4121
|
+
};
|
|
4122
|
+
this.#lastResult = taskResult;
|
|
4123
|
+
finalStatus = "error";
|
|
4124
|
+
break;
|
|
4125
|
+
}
|
|
4126
|
+
await onBeforeStep?.(this, step);
|
|
4127
|
+
try {
|
|
4128
|
+
console.group(`step: ${step}`);
|
|
4129
|
+
if (step > 0) await waitFor(stepDelay, signal);
|
|
4130
|
+
signal.throwIfAborted();
|
|
4131
|
+
console.log(chalk.blue.bold("👀 Observing..."));
|
|
4132
|
+
this.#states.browserState = await this.browserController.getBrowserState();
|
|
4133
|
+
await this.#handleObservations(step);
|
|
4134
|
+
const messages = [{
|
|
4135
|
+
role: "system",
|
|
4136
|
+
content: this.#getSystemPrompt()
|
|
4137
|
+
}, {
|
|
4138
|
+
role: "user",
|
|
4139
|
+
content: await this.#assembleUserPrompt()
|
|
4140
|
+
}];
|
|
4141
|
+
const macroTool = { AgentOutput: this.#packMacroTool() };
|
|
4142
|
+
console.log(chalk.blue.bold("🧠 Thinking..."));
|
|
4143
|
+
this.#emitActivity({ type: "thinking" });
|
|
4144
|
+
const result = await this.#llm.invoke(messages, macroTool, signal, {
|
|
4145
|
+
toolChoiceName: "AgentOutput",
|
|
4146
|
+
normalizeResponse: (res) => normalizeResponse(res, this.tools)
|
|
4147
|
+
});
|
|
4148
|
+
const macroResult = result.toolResult;
|
|
4149
|
+
const input = macroResult.input;
|
|
4150
|
+
const output = macroResult.output;
|
|
4151
|
+
const reflection = {
|
|
4152
|
+
evaluation_previous_goal: input.evaluation_previous_goal,
|
|
4153
|
+
memory: input.memory,
|
|
4154
|
+
next_goal: input.next_goal
|
|
4155
|
+
};
|
|
4156
|
+
const actionName = Object.keys(input.action)[0];
|
|
4157
|
+
const action = {
|
|
4158
|
+
name: actionName,
|
|
4159
|
+
input: input.action[actionName],
|
|
4160
|
+
output
|
|
4161
|
+
};
|
|
4162
|
+
this.#emitHistoryChange({
|
|
4163
|
+
type: "step",
|
|
4164
|
+
stepIndex: step,
|
|
4165
|
+
reflection,
|
|
4166
|
+
action,
|
|
4167
|
+
usage: result.usage,
|
|
4168
|
+
rawResponse: result.rawResponse,
|
|
4169
|
+
rawRequest: result.rawRequest
|
|
4170
|
+
});
|
|
4171
|
+
if (actionName === "done") {
|
|
4172
|
+
const success = action.input?.success ?? false;
|
|
4173
|
+
const data = action.input?.text || "no text provided";
|
|
4174
|
+
console.log(chalk.green.bold("Task completed"), success, data);
|
|
4175
|
+
taskResult = {
|
|
4176
|
+
success,
|
|
4177
|
+
data,
|
|
4178
|
+
history: this.history
|
|
4179
|
+
};
|
|
4180
|
+
this.#lastResult = taskResult;
|
|
4181
|
+
finalStatus = "completed";
|
|
4182
|
+
break;
|
|
4183
|
+
}
|
|
4184
|
+
} catch (error) {
|
|
4185
|
+
const isAbortError = error?.name === "AbortError";
|
|
4186
|
+
if (!isAbortError) console.error("Task failed", error);
|
|
4187
|
+
const message = isAbortError ? "Task aborted" : String(error);
|
|
4188
|
+
this.#emitActivity({
|
|
4189
|
+
type: "error",
|
|
4190
|
+
message
|
|
4191
|
+
});
|
|
4192
|
+
this.#emitHistoryChange({
|
|
4193
|
+
type: "error",
|
|
4194
|
+
message,
|
|
4195
|
+
rawResponse: error
|
|
4196
|
+
});
|
|
4197
|
+
taskResult = {
|
|
4198
|
+
success: false,
|
|
4199
|
+
data: message,
|
|
4200
|
+
history: this.history
|
|
4201
|
+
};
|
|
4202
|
+
this.#lastResult = taskResult;
|
|
4203
|
+
finalStatus = isAbortError ? "stopped" : "error";
|
|
4204
|
+
break;
|
|
4205
|
+
} finally {
|
|
4206
|
+
console.groupEnd();
|
|
4207
|
+
await onAfterStep?.(this, this.history);
|
|
4208
|
+
}
|
|
4209
|
+
step++;
|
|
4210
|
+
}
|
|
4211
|
+
await onAfterTask?.(this, taskResult);
|
|
4212
|
+
return taskResult;
|
|
4213
|
+
} catch (error) {
|
|
4214
|
+
this.#emitActivity({
|
|
4215
|
+
type: "error",
|
|
4216
|
+
message: String(error)
|
|
4217
|
+
});
|
|
4218
|
+
finalStatus = "error";
|
|
4219
|
+
throw error;
|
|
4220
|
+
} finally {
|
|
4221
|
+
await suppress(() => this.browserController.cleanUpHighlights());
|
|
4222
|
+
await suppress(() => this.browserController.hideMask());
|
|
4223
|
+
this.#abortController.abort();
|
|
4224
|
+
resolveRunning();
|
|
4225
|
+
this.#setStatus(finalStatus);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Merge all tools into a single MacroTool with the following input:
|
|
4230
|
+
* - thinking: string
|
|
4231
|
+
* - evaluation_previous_goal: string
|
|
4232
|
+
* - memory: string
|
|
4233
|
+
* - next_goal: string
|
|
4234
|
+
* - action: { toolName: toolInput }
|
|
4235
|
+
* where action must be selected from tools defined in this.tools
|
|
4236
|
+
*/
|
|
4237
|
+
#packMacroTool() {
|
|
4238
|
+
const tools = this.tools;
|
|
4239
|
+
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
|
4240
|
+
return z.object({ [toolName]: tool.inputSchema }).describe(tool.description);
|
|
4241
|
+
});
|
|
4242
|
+
const actionSchema = z.union(actionSchemas);
|
|
4243
|
+
return {
|
|
4244
|
+
description: "You MUST call this tool every step!",
|
|
4245
|
+
inputSchema: z.object({
|
|
4246
|
+
evaluation_previous_goal: z.string().optional(),
|
|
4247
|
+
memory: z.string().optional(),
|
|
4248
|
+
next_goal: z.string().optional(),
|
|
4249
|
+
action: actionSchema
|
|
4250
|
+
}),
|
|
4251
|
+
execute: async (input) => {
|
|
4252
|
+
const signal = this.#abortController.signal;
|
|
4253
|
+
signal.throwIfAborted();
|
|
4254
|
+
console.log(chalk.blue.bold("MacroTool input"), input);
|
|
4255
|
+
const action = input.action;
|
|
4256
|
+
const toolName = Object.keys(action)[0];
|
|
4257
|
+
const toolInput = action[toolName];
|
|
4258
|
+
const reflectionLines = [];
|
|
4259
|
+
if (input.evaluation_previous_goal) reflectionLines.push(`✅: ${input.evaluation_previous_goal}`);
|
|
4260
|
+
if (input.memory) reflectionLines.push(`💾: ${input.memory}`);
|
|
4261
|
+
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`);
|
|
4262
|
+
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join("\n") : "";
|
|
4263
|
+
if (reflectionText) console.log(reflectionText);
|
|
4264
|
+
const tool = tools.get(toolName);
|
|
4265
|
+
assert(tool, `Tool ${toolName} not found`);
|
|
4266
|
+
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput);
|
|
4267
|
+
this.#emitActivity({
|
|
4268
|
+
type: "executing",
|
|
4269
|
+
tool: toolName,
|
|
4270
|
+
input: toolInput
|
|
4271
|
+
});
|
|
4272
|
+
const startTime = Date.now();
|
|
4273
|
+
const result = await tool.execute.bind(this)(toolInput, { signal });
|
|
4274
|
+
signal.throwIfAborted();
|
|
4275
|
+
const duration = Date.now() - startTime;
|
|
4276
|
+
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result);
|
|
4277
|
+
this.#emitActivity({
|
|
4278
|
+
type: "executed",
|
|
4279
|
+
tool: toolName,
|
|
4280
|
+
input: toolInput,
|
|
4281
|
+
output: result,
|
|
4282
|
+
duration
|
|
4283
|
+
});
|
|
4284
|
+
if (toolName === "wait") this.#states.totalWaitTime += toolInput?.seconds || 0;
|
|
4285
|
+
else this.#states.totalWaitTime = 0;
|
|
4286
|
+
return {
|
|
4287
|
+
input,
|
|
4288
|
+
output: result
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* Get system prompt, dynamically replace language settings based on configured language
|
|
4295
|
+
*/
|
|
4296
|
+
#getSystemPrompt() {
|
|
4297
|
+
if (this.config.customSystemPrompt) return this.config.customSystemPrompt;
|
|
4298
|
+
const targetLanguage = this.config.language === "zh-CN" ? "中文" : "English";
|
|
4299
|
+
return system_prompt_default.replace(/Default working language: \*\*.*?\*\*/, `Default working language: **${targetLanguage}**`);
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Get instructions from config
|
|
4303
|
+
*/
|
|
4304
|
+
async #getInstructions() {
|
|
4305
|
+
const { instructions, experimentalLlmsTxt } = this.config;
|
|
4306
|
+
const systemInstructions = instructions?.system?.trim();
|
|
4307
|
+
let pageInstructions;
|
|
4308
|
+
const url = this.#states.browserState?.url || "";
|
|
4309
|
+
if (instructions?.getPageInstructions && url) try {
|
|
4310
|
+
pageInstructions = instructions.getPageInstructions(url)?.trim();
|
|
4311
|
+
} catch (error) {
|
|
4312
|
+
console.error(chalk.red("[PageAgent] Failed to execute getPageInstructions callback:"), error);
|
|
4313
|
+
}
|
|
4314
|
+
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : void 0;
|
|
4315
|
+
if (!systemInstructions && !pageInstructions && !llmsTxt) return "";
|
|
4316
|
+
let result = "<instructions>\n";
|
|
4317
|
+
if (systemInstructions) result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`;
|
|
4318
|
+
if (pageInstructions) result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`;
|
|
4319
|
+
if (llmsTxt) result += `<llms_txt>\n${llmsTxt}\n</llms_txt>\n`;
|
|
4320
|
+
result += "</instructions>\n\n";
|
|
4321
|
+
return result;
|
|
4322
|
+
}
|
|
4323
|
+
/**
|
|
4324
|
+
* Generate system observations before each step
|
|
4325
|
+
* @todo loop detection
|
|
4326
|
+
* @todo console error
|
|
4327
|
+
*/
|
|
4328
|
+
async #handleObservations(step) {
|
|
4329
|
+
if (this.#states.totalWaitTime >= 3) this.pushObservation(`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`);
|
|
4330
|
+
const currentURL = this.#states.browserState?.url || "";
|
|
4331
|
+
if (currentURL !== this.#states.lastURL) {
|
|
4332
|
+
this.pushObservation(`Page navigated to → ${currentURL}`);
|
|
4333
|
+
this.#states.lastURL = currentURL;
|
|
4334
|
+
await waitFor(.5);
|
|
4335
|
+
}
|
|
4336
|
+
const remaining = this.config.maxSteps - step;
|
|
4337
|
+
if (remaining === 5) this.pushObservation(`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`);
|
|
4338
|
+
else if (remaining === 2) this.pushObservation(`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`);
|
|
4339
|
+
if (this.#observations.length > 0) {
|
|
4340
|
+
for (const content of this.#observations) {
|
|
4341
|
+
this.history.push({
|
|
4342
|
+
type: "observation",
|
|
4343
|
+
content
|
|
4344
|
+
});
|
|
4345
|
+
console.log(chalk.cyan("Observation:"), content);
|
|
4346
|
+
}
|
|
4347
|
+
this.#observations = [];
|
|
4348
|
+
this.#emitHistoryChange();
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
async #assembleUserPrompt() {
|
|
4352
|
+
const browserState = this.#states.browserState;
|
|
4353
|
+
let prompt = "";
|
|
4354
|
+
prompt += await this.#getInstructions();
|
|
4355
|
+
const stepCount = this.history.filter((e) => e.type === "step").length;
|
|
4356
|
+
prompt += "<agent_state>\n";
|
|
4357
|
+
prompt += "<user_request>\n";
|
|
4358
|
+
prompt += `${this.task}\n`;
|
|
4359
|
+
prompt += "</user_request>\n";
|
|
4360
|
+
prompt += "<step_info>\n";
|
|
4361
|
+
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`;
|
|
4362
|
+
prompt += `Current time: ${(/* @__PURE__ */ new Date()).toLocaleString()}\n`;
|
|
4363
|
+
prompt += "</step_info>\n";
|
|
4364
|
+
prompt += "</agent_state>\n\n";
|
|
4365
|
+
prompt += "<agent_history>\n";
|
|
4366
|
+
let stepIndex = 0;
|
|
4367
|
+
for (const event of this.history) if (event.type === "step") {
|
|
4368
|
+
stepIndex++;
|
|
4369
|
+
prompt += `<step_${stepIndex}>\n`;
|
|
4370
|
+
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`;
|
|
4371
|
+
prompt += `Memory: ${event.reflection.memory}\n`;
|
|
4372
|
+
prompt += `Next Goal: ${event.reflection.next_goal}\n`;
|
|
4373
|
+
prompt += `Action Results: ${event.action.output}\n`;
|
|
4374
|
+
prompt += `</step_${stepIndex}>\n`;
|
|
4375
|
+
} else if (event.type === "observation") prompt += `<sys>${event.content}</sys>\n`;
|
|
4376
|
+
else if (event.type === "user_takeover") prompt += `<sys>User took over control and made changes to the page</sys>\n`;
|
|
4377
|
+
else if (event.type === "error") {}
|
|
4378
|
+
prompt += "</agent_history>\n\n";
|
|
4379
|
+
let pageContent = browserState.content;
|
|
4380
|
+
if (this.config.transformPageContent) pageContent = await this.config.transformPageContent(pageContent);
|
|
4381
|
+
prompt += "<browser_state>\n";
|
|
4382
|
+
prompt += browserState.header + "\n";
|
|
4383
|
+
prompt += pageContent + "\n";
|
|
4384
|
+
prompt += browserState.footer + "\n\n";
|
|
4385
|
+
prompt += "</browser_state>\n\n";
|
|
4386
|
+
return prompt;
|
|
4387
|
+
}
|
|
4388
|
+
dispose() {
|
|
4389
|
+
if (this.disposed) return;
|
|
4390
|
+
this.disposed = true;
|
|
4391
|
+
console.log("Disposing PageAgent...");
|
|
4392
|
+
const errors = [];
|
|
4393
|
+
const cleanUp = (callback) => {
|
|
4394
|
+
try {
|
|
4395
|
+
callback();
|
|
4396
|
+
} catch (error) {
|
|
4397
|
+
errors.push(error);
|
|
4398
|
+
}
|
|
4399
|
+
};
|
|
4400
|
+
cleanUp(() => this.#abortController.abort());
|
|
4401
|
+
cleanUp(() => this.browserController.dispose());
|
|
4402
|
+
cleanUp(() => this.dispatchEvent(new Event("dispose")));
|
|
4403
|
+
cleanUp(() => this.config.onDispose?.(this));
|
|
4404
|
+
if (errors.length === 1) throw errors[0];
|
|
4405
|
+
if (errors.length > 1) throw new AggregateError(errors, "PageAgent disposal failed");
|
|
4406
|
+
}
|
|
4407
|
+
};
|
|
4408
|
+
//#endregion
|
|
4409
|
+
//#region src/agent/MyPageAgent.ts
|
|
4410
|
+
var MyPageAgent = class extends AgentRuntime {
|
|
4411
|
+
ui;
|
|
4412
|
+
constructor(config) {
|
|
4413
|
+
const browserController = new BrowserController({
|
|
4414
|
+
...config,
|
|
4415
|
+
enableMask: config.enableMask ?? true
|
|
4416
|
+
});
|
|
4417
|
+
super({
|
|
4418
|
+
...config,
|
|
4419
|
+
experimentalScriptExecutionTool: false,
|
|
4420
|
+
browserController
|
|
4421
|
+
});
|
|
4422
|
+
this.ui = new UI(this, {
|
|
4423
|
+
language: config.language,
|
|
4424
|
+
promptForNextTask: config.promptForNextTask
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4428
|
+
//#endregion
|
|
4429
|
+
//#region src/index.ts
|
|
4430
|
+
function createAgent(config) {
|
|
4431
|
+
return new MyPageAgent(config);
|
|
4432
|
+
}
|
|
4433
|
+
//#endregion
|
|
4434
|
+
export { MyPageAgent, createAgent, tool };
|
|
4435
|
+
|
|
4436
|
+
//# sourceMappingURL=index.js.map
|