agentpage 0.0.31 → 0.0.33

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/dist/index.mjs CHANGED
@@ -10,25 +10,7 @@ const DEFAULT_MAX_ROUNDS = 40;
10
10
  const DEFAULT_RECOVERY_WAIT_MS = 100;
11
11
  const DEFAULT_ACTION_RECOVERY_ROUNDS = 2;
12
12
  const DEFAULT_NOT_FOUND_RETRY_ROUNDS = 2;
13
- const DEFAULT_NOT_FOUND_RETRY_WAIT_MS = 1e3;
14
- const DEFAULT_ROUND_STABILITY_WAIT_TIMEOUT_MS = 4e3;
15
- const DEFAULT_ROUND_STABILITY_WAIT_QUIET_MS = 200;
16
- const DEFAULT_ROUND_STABILITY_WAIT_LOADING_SELECTORS = [
17
- ".ant-spin",
18
- ".ant-spin-spinning",
19
- ".ant-skeleton",
20
- ".el-loading-mask",
21
- ".bk-loading",
22
- ".bk-spin-loading",
23
- ".bk-skeleton",
24
- ".bk-sideslider-loading",
25
- ".t-loading",
26
- ".t-skeleton",
27
- ".t-skeleton__row",
28
- "[aria-busy=\"true\"]",
29
- ".skeleton",
30
- ".loading"
31
- ];
13
+ const DEFAULT_NOT_FOUND_RETRY_WAIT_MS = 2e3;
32
14
  /** 快照起始标记 — 用于在消息中识别快照边界 */
33
15
  const SNAPSHOT_START = "<!-- SNAPSHOT_START -->";
34
16
  /** 快照结束标记 */
@@ -154,13 +136,11 @@ function deriveNextInstruction(text, currentInstruction) {
154
136
  */
155
137
  function reduceRemainingHeuristically(currentInstruction, executedCount) {
156
138
  if (!currentInstruction.trim() || executedCount <= 0) return currentInstruction;
157
- if (!/(->|=>|→|\bthen\b|\band then\b|\bnext\b|\bafter that\b|然后|接着|随后|之后|再)/i.test(currentInstruction)) return currentInstruction;
158
- const parts = currentInstruction.replace(/\s+/g, " ").replace(/(->|=>|→)/g, " 然后 ").split(/\s*(?:then|and then|next|after that|然后|接着|随后|之后|再)\s*/gi).map((part) => part.trim()).filter(Boolean);
139
+ const parts = currentInstruction.replace(/\s+/g, " ").replace(/(->|=>|→)/g, " 然后 ").replace(/[,,。;;]/g, " 然后 ").split(/\s*(?:然后|再|并且|并|接着|随后|之后)\s*/g).map((part) => part.trim()).filter(Boolean);
159
140
  if (parts.length <= 1) return currentInstruction;
160
- const consumedSteps = Math.min(Math.max(1, Math.floor(executedCount)), 1);
161
- const nextParts = parts.slice(Math.min(consumedSteps, parts.length));
141
+ const nextParts = parts.slice(Math.min(executedCount, parts.length));
162
142
  if (nextParts.length === 0) return "";
163
- return nextParts.join(" 然后 ");
143
+ return nextParts.join(" -> ");
164
144
  }
165
145
  /**
166
146
  * 判定是否强制断轮。
@@ -183,36 +163,6 @@ function shouldForceRoundBreak(toolName, toolInput) {
183
163
  return toolName === "evaluate";
184
164
  }
185
165
  /**
186
- * 判定动作是否可能引发页面结构或状态变化。
187
- *
188
- * 用于“轮次后稳定等待”触发条件:
189
- * - 命中 true:本轮结束后执行加载态 + DOM 静默双重等待
190
- * - 命中 false:跳过等待,直接进入下一轮
191
- */
192
- function isPotentialDomMutation(toolName, toolInput) {
193
- const action = getToolAction(toolInput);
194
- if (toolName === "navigate") return true;
195
- if (toolName === "evaluate") return true;
196
- if (toolName !== "dom") return false;
197
- if (!action) return false;
198
- return [
199
- "click",
200
- "fill",
201
- "select_option",
202
- "clear",
203
- "check",
204
- "uncheck",
205
- "type",
206
- "focus",
207
- "hover",
208
- "scroll",
209
- "press",
210
- "set_attr",
211
- "add_class",
212
- "remove_class"
213
- ].includes(action);
214
- }
215
- /**
216
166
  * 采集找不到元素任务。
217
167
  *
218
168
  * 返回 null 表示当前结果不属于“元素未找到”,
@@ -479,10 +429,26 @@ function buildCompactMessages(userMessage, trace, latestSnapshot, currentUrl, hi
479
429
  content: `Done steps (do NOT repeat):\n${traceParts.join("\n")}`
480
430
  });
481
431
  const hasErrors = trace.some((e) => hasToolError(e.result));
482
- const needsMasterGoalAnchor = activeInstruction.trim().toLowerCase() !== userMessage.trim().toLowerCase();
483
- const contextParts = ["## Execution context"];
484
- if (needsMasterGoalAnchor) contextParts.push(`Master goal (reference only — do NOT restart from scratch):`, userMessage, "");
485
- contextParts.push("Current remaining instruction:", activeInstruction, "", "Task-reduction model:", "Input: current remaining instruction + previous round executed actions + this-round actions.", "Output: new remaining instruction after removing this-round actions.", "Start from visible page state directly. Do NOT restate task. Do NOT output planning text.", "Execute all independent visible sub-tasks in one round.", "Do NOT act on elements not present in this snapshot yet.", "If action changes DOM (open modal/navigate), stop after that batch and continue next round.", "Do NOT call page_info (get_url/get_title/query_all/snapshot).", "For dropdown/select fields, use dom with action=select_option (or fill on a select).", "If a needed list shows `... (N children omitted)` under a specific container, output `SNAPSHOT_HINT: EXPAND_CHILDREN #<containerRef>` and wait for next round snapshot.", "Build the minimal action array from current snapshot to finish this remaining instruction in one round whenever possible.", "For deterministic increase/decrease controls, compute delta from current visible value and issue exactly that many clicks in one round (e.g., +2 => two increase clicks). Do not overshoot then undo.", "Stop rule: once requested state is reached, stop tool calls. If verification is needed, verify once and then output REMAINING: DONE.", allowAgentUiInteraction ? "User explicitly asked to operate AutoPilot UI. You may interact with chat input/send/dock only as requested." : "Do NOT interact with any AI chat UI elements (chat input, send button, dock). Only operate on the actual page content.");
432
+ const contextParts = [
433
+ "## Execution context",
434
+ "Current remaining instruction:",
435
+ activeInstruction,
436
+ "",
437
+ "Task-reduction model:",
438
+ "Input: current remaining instruction + previous round executed actions + this-round actions.",
439
+ "Output: new remaining instruction after removing this-round actions.",
440
+ "Start from visible page state directly. Do NOT restate task. Do NOT output planning text.",
441
+ "Execute all independent visible sub-tasks in one round.",
442
+ "Do NOT act on elements not present in this snapshot yet.",
443
+ "If action changes DOM (open modal/navigate), stop after that batch and continue next round.",
444
+ "Do NOT call page_info (get_url/get_title/query_all/snapshot).",
445
+ "For dropdown/select fields, use dom with action=select_option (or fill on a select).",
446
+ "If a needed list shows `... (N children omitted)` under a specific container, output `SNAPSHOT_HINT: EXPAND_CHILDREN #<containerRef>` and wait for next round snapshot.",
447
+ "Build the minimal action array from current snapshot to finish this remaining instruction in one round whenever possible.",
448
+ "For deterministic increase/decrease controls, compute delta from current visible value and issue exactly that many clicks in one round (e.g., +2 => two increase clicks). Do not overshoot then undo.",
449
+ "Stop rule: once requested state is reached, stop tool calls. If verification is needed, verify once and then output REMAINING: DONE.",
450
+ allowAgentUiInteraction ? "User explicitly asked to operate AutoPilot UI. You may interact with chat input/send/dock only as requested." : "Do NOT interact with any AI chat UI elements (chat input, send button, dock). Only operate on the actual page content."
451
+ ];
486
452
  if (hasErrors) contextParts.push("", "The last step failed. Retry with a different approach, or skip and continue with other visible targets.");
487
453
  else contextParts.push("", "If the goal is fully done, reply with a short summary (no tool calls).");
488
454
  if (previousRoundTasks && previousRoundTasks.length > 0) contextParts.push("", "Previous round planned task array (already executed):", ...previousRoundTasks.map((task, index) => `${index + 1}. ${task}`));
@@ -730,7 +696,7 @@ function detectIdleLoop(toolCalls, consecutiveReadOnlyRounds) {
730
696
  * - 达到 `maxRounds`
731
697
  */
732
698
  async function executeAgentLoop(params) {
733
- const { client, registry, systemPrompt, message, initialSnapshot, history, dryRun = false, maxRounds = DEFAULT_MAX_ROUNDS, roundStabilityWait, callbacks } = params;
699
+ const { client, registry, systemPrompt, message, initialSnapshot, history, dryRun = false, maxRounds = DEFAULT_MAX_ROUNDS, callbacks } = params;
734
700
  const tools = registry.getDefinitions();
735
701
  const allToolCalls = [];
736
702
  const fullToolTrace = [];
@@ -751,12 +717,6 @@ async function executeAgentLoop(params) {
751
717
  let lastRoundHadError = false;
752
718
  let protocolViolationHint;
753
719
  const snapshotExpandRefIds = /* @__PURE__ */ new Set();
754
- const effectiveRoundStabilityWait = {
755
- enabled: roundStabilityWait?.enabled ?? true,
756
- timeoutMs: Math.max(200, Math.floor(roundStabilityWait?.timeoutMs ?? DEFAULT_ROUND_STABILITY_WAIT_TIMEOUT_MS)),
757
- quietMs: Math.max(50, Math.floor(roundStabilityWait?.quietMs ?? DEFAULT_ROUND_STABILITY_WAIT_QUIET_MS)),
758
- loadingSelectors: [...new Set([...DEFAULT_ROUND_STABILITY_WAIT_LOADING_SELECTORS, ...roundStabilityWait?.loadingSelectors ?? []].map((selector) => selector.trim()).filter(Boolean))]
759
- };
760
720
  let recoveryCount = 0;
761
721
  let redundantInterceptCount = 0;
762
722
  let pendingNotFoundRetry;
@@ -788,30 +748,6 @@ async function executeAgentLoop(params) {
788
748
  } : void 0);
789
749
  recordSnapshotStats(pageContext.latestSnapshot);
790
750
  };
791
- /**
792
- * 轮次后稳定等待(双重等待)。
793
- *
794
- * 顺序固定为:
795
- * 1) 等待 loading 指示器隐藏
796
- * 2) 等待 DOM quiet window
797
- */
798
- const runRoundStabilityBarrier = async () => {
799
- if (!effectiveRoundStabilityWait.enabled) return;
800
- if (!registry.has("wait")) return;
801
- const timeout = effectiveRoundStabilityWait.timeoutMs;
802
- const loadingSelector = effectiveRoundStabilityWait.loadingSelectors.join(", ");
803
- if (loadingSelector) await registry.dispatch("wait", {
804
- action: "wait_for_selector",
805
- selector: loadingSelector,
806
- state: "hidden",
807
- timeout
808
- });
809
- await registry.dispatch("wait", {
810
- action: "wait_for_stable",
811
- timeout,
812
- quietMs: effectiveRoundStabilityWait.quietMs
813
- });
814
- };
815
751
  if (pageContext.latestSnapshot) recordSnapshotStats(pageContext.latestSnapshot);
816
752
  /**
817
753
  * 追加工具轨迹。
@@ -927,7 +863,6 @@ async function executeAgentLoop(params) {
927
863
  break;
928
864
  }
929
865
  let roundHasError = false;
930
- let roundHasPotentialDomMutation = false;
931
866
  const executedTaskCalls = [];
932
867
  const roundMissingTasks = [];
933
868
  for (const tc of response.toolCalls) {
@@ -958,7 +893,6 @@ async function executeAgentLoop(params) {
958
893
  const missingTask = collectMissingTask(tc.name, tc.input, result);
959
894
  if (missingTask) roundMissingTasks.push(missingTask);
960
895
  if (result.details && typeof result.details === "object") roundHasError = roundHasError || Boolean(result.details.error);
961
- if (!hasToolError(result) && isPotentialDomMutation(tc.name, tc.input)) roundHasPotentialDomMutation = true;
962
896
  if (tc.name === "page_info" && getToolAction(tc.input) === "snapshot") {
963
897
  pageContext.latestSnapshot = toContentString(result.content);
964
898
  recordSnapshotStats(pageContext.latestSnapshot);
@@ -974,8 +908,7 @@ async function executeAgentLoop(params) {
974
908
  else pendingNotFoundRetry = void 0;
975
909
  if (parsedInstructionState.hasRemainingProtocol) remainingInstruction = parsedInstructionState.nextInstruction;
976
910
  else {
977
- const heuristicProgressUnits = executedTaskCalls.length > 0 ? 1 : 0;
978
- const nextByHeuristic = reduceRemainingHeuristically(remainingInstruction, heuristicProgressUnits);
911
+ const nextByHeuristic = reduceRemainingHeuristically(remainingInstruction, executedTaskCalls.length);
979
912
  if (nextByHeuristic !== remainingInstruction) remainingInstruction = nextByHeuristic;
980
913
  else roundHasError = true;
981
914
  }
@@ -993,7 +926,6 @@ async function executeAgentLoop(params) {
993
926
  break;
994
927
  }
995
928
  consecutiveReadOnlyRounds = idleResult;
996
- if (roundHasPotentialDomMutation) await runRoundStabilityBarrier();
997
929
  await refreshSnapshot();
998
930
  }
999
931
  const resultMessages = [...history ?? [], {
@@ -1678,16 +1610,12 @@ function buildSystemPrompt(params = {}) {
1678
1610
  "- If an action will change DOM (open modal, navigate), stop after that action batch and continue next round with new snapshot.",
1679
1611
  "- Do NOT call page_info (snapshot/query/get_url/get_title). Snapshot is already provided every round.",
1680
1612
  "- For dropdown/select, use dom action=select_option (or fill on select).",
1681
- "- Always cross-check planned actions against the original goal to avoid task drift (e.g., do not confuse create issue vs create repository).",
1682
1613
  "- If a required list shows `... (N children omitted)` under a specific container, request focused expansion by outputting `SNAPSHOT_HINT: EXPAND_CHILDREN #<containerRef>`.",
1683
1614
  "- After outputting snapshot expansion hint, wait for the next refreshed snapshot before further scrolling/clicking on that list.",
1684
1615
  "- Verification whitelist: do NOT use get_text/get_attr to verify input/select values unless the user explicitly asks for verification.",
1685
1616
  "- Stop rule: when the requested state is achieved, stop calling tools. If verification is requested, verify once and then return REMAINING: DONE (no repeated get_text/get_attr on the same target).",
1686
1617
  "- Do NOT interact with AutoPilot UI unless user explicitly asks.",
1687
1618
  "",
1688
- "## Listener Abbrevs",
1689
- "clk=click dbl=dblclick mdn=mousedown mup=mouseup mmv=mousemove mov=mouseover mot=mouseout men=mouseenter mlv=mouseleave pdn=pointerdown pup=pointerup pmv=pointermove tst=touchstart ted=touchend kdn=keydown kup=keyup inp=input chg=change sub=submit fcs=focus blr=blur scl=scroll whl=wheel drg=drag drs=dragstart dre=dragend drp=drop ctx=contextmenu",
1690
- "",
1691
1619
  "## Output Contract",
1692
1620
  "- Return tool calls for this round.",
1693
1621
  "- Also include one plain text line:",
@@ -1715,89 +1643,25 @@ function buildSystemPrompt(params = {}) {
1715
1643
  }
1716
1644
 
1717
1645
  //#endregion
1718
- //#region src/web/event-listener-tracker.ts
1719
- const elementEventMap = /* @__PURE__ */ new WeakMap();
1720
- let installed = false;
1721
- let originalAddEventListener;
1722
- let originalRemoveEventListener;
1723
- function normalizeEventType(type) {
1724
- if (typeof type !== "string") return null;
1725
- return type.trim().toLowerCase() || null;
1726
- }
1727
- function canTrackElementTarget(target) {
1728
- if (typeof Element === "undefined") return false;
1729
- return target instanceof Element;
1730
- }
1731
- function trackElementEvent(target, type) {
1732
- if (!canTrackElementTarget(target)) return;
1733
- const prev = elementEventMap.get(target);
1734
- if (prev) {
1735
- prev.add(type);
1736
- return;
1737
- }
1738
- elementEventMap.set(target, new Set([type]));
1739
- }
1740
- function untrackElementEvent(target, type) {
1741
- if (!canTrackElementTarget(target)) return;
1742
- const prev = elementEventMap.get(target);
1743
- if (!prev) return;
1744
- prev.delete(type);
1745
- if (prev.size === 0) elementEventMap.delete(target);
1746
- }
1747
- /**
1748
- * 安装全局监听追踪补丁(幂等)。
1749
- */
1750
- function installEventListenerTracking() {
1751
- if (installed) return;
1752
- if (typeof EventTarget === "undefined") return;
1753
- const proto = EventTarget.prototype;
1754
- const nativeAdd = proto.addEventListener;
1755
- const nativeRemove = proto.removeEventListener;
1756
- if (typeof nativeAdd !== "function" || typeof nativeRemove !== "function") return;
1757
- originalAddEventListener = nativeAdd;
1758
- originalRemoveEventListener = nativeRemove;
1759
- proto.addEventListener = function patchedAddEventListener(type, listener, options) {
1760
- originalAddEventListener?.call(this, type, listener, options);
1761
- try {
1762
- const normalizedType = normalizeEventType(type);
1763
- if (!normalizedType || listener == null) return;
1764
- trackElementEvent(this, normalizedType);
1765
- } catch {}
1766
- };
1767
- proto.removeEventListener = function patchedRemoveEventListener(type, listener, options) {
1768
- originalRemoveEventListener?.call(this, type, listener, options);
1769
- try {
1770
- const normalizedType = normalizeEventType(type);
1771
- if (!normalizedType || listener == null) return;
1772
- untrackElementEvent(this, normalizedType);
1773
- } catch {}
1774
- };
1775
- installed = true;
1776
- }
1777
- /**
1778
- * 读取元素已记录的事件名(排序后返回,便于稳定输出)。
1779
- */
1780
- function getTrackedElementEvents(el) {
1781
- const set = elementEventMap.get(el);
1782
- if (!set || set.size === 0) return [];
1783
- return Array.from(set).sort();
1784
- }
1785
- /**
1786
- * 判断元素是否存在至少一个被追踪到的事件绑定。
1787
- */
1788
- function hasTrackedElementEvents(el) {
1789
- return (elementEventMap.get(el)?.size ?? 0) > 0;
1790
- }
1791
-
1792
- //#endregion
1793
- //#region src/web/tools/dom-tool/constants.ts
1646
+ //#region src/web/tools/dom-tool.ts
1794
1647
  /**
1795
- * DOM Tool 常量定义。
1648
+ * DOM Tool — 浏览器 DOM 操作工具(结合 Playwright 核心交互模式增强)。
1649
+ *
1650
+ * 关键改进(参考 Playwright):
1651
+ * 1. retarget — 点击时自动重定向到 button/link/label.control
1652
+ * 2. scrollIntoView 多策略 — 4 种 block 对齐轮换,解决 sticky 遮挡
1653
+ * 3. stable 检查 — rAF 逐帧检测元素位置稳定后再操作
1654
+ * 4. hit-target 验证 — elementsFromPoint 检查是否被遮挡
1655
+ * 5. 完整点击事件链 — pointermove→pointerdown→mousedown→pointerup→mouseup→click
1656
+ * 6. check/uncheck 通过 click — 先检查→click 切换→验证状态
1657
+ * 7. press 组合键 — 支持 Control+a, Shift+Enter 等修饰键
1658
+ * 8. fill 分类型 — date/color/range 走 setValue,text 类走 selectAll+原生写入
1659
+ * 9. 自定义下拉增强 — 更广泛的 option 选择器 + 等待弹出
1660
+ * 10. ARIA disabled — 检查祖先链 aria-disabled
1796
1661
  *
1797
- * 包含:input 类型分类、修饰键集合、键码映射、滚动策略。
1662
+ * 运行环境:浏览器 Content Script(直接访问 DOM,无 CDP)。
1798
1663
  */
1799
- /** 默认等待超时(ms) */
1800
- const DEFAULT_WAIT_MS = 1200;
1664
+ const DEFAULT_WAIT_MS = 2e3;
1801
1665
  /** scrollIntoView 轮换策略(参考 Playwright dom.ts) */
1802
1666
  const SCROLL_OPTIONS = [
1803
1667
  void 0,
@@ -1857,9 +1721,6 @@ const KEY_CODE_MAP = {
1857
1721
  Alt: "AltLeft",
1858
1722
  Meta: "MetaLeft"
1859
1723
  };
1860
-
1861
- //#endregion
1862
- //#region src/web/tools/dom-tool/query.ts
1863
1724
  let activeRefStore;
1864
1725
  function setActiveRefStore(store) {
1865
1726
  activeRefStore = store;
@@ -1870,26 +1731,15 @@ function getActiveRefStore() {
1870
1731
  function sleep(ms) {
1871
1732
  return new Promise((r) => setTimeout(r, ms));
1872
1733
  }
1873
- /**
1874
- * 查询元素:优先 RefStore hash,回退 CSS 选择器。
1875
- * 支持复合 hash 选择器(如 "#hashID .child-class")——先解析 hash 根,再在其子树内 querySelector。
1876
- */
1734
+ /** 查询元素:优先 RefStore hash,回退 CSS 选择器 */
1877
1735
  function queryElement(selector) {
1878
1736
  try {
1879
1737
  if (selector.startsWith("#") && activeRefStore) {
1880
- const spaceIdx = selector.indexOf(" ");
1881
- const hashPart = spaceIdx > 0 ? selector.slice(1, spaceIdx) : selector.slice(1);
1882
- const rest = spaceIdx > 0 ? selector.slice(spaceIdx + 1).trim() : "";
1883
- if (activeRefStore.has(hashPart)) {
1884
- const root = activeRefStore.get(hashPart);
1885
- if (!root || !root.isConnected) {
1886
- activeRefStore.delete(hashPart);
1887
- return `未找到 ref "#${hashPart}" 对应的元素(可能已被移除或快照已过期)`;
1888
- }
1889
- if (!rest) return root;
1890
- const child = root.querySelector(rest);
1891
- if (!child) return `在 #${hashPart} 内未找到匹配 "${rest}" 的子元素`;
1892
- return child;
1738
+ const id = selector.slice(1);
1739
+ if (activeRefStore.has(id)) {
1740
+ const el = activeRefStore.get(id);
1741
+ if (!el) return `未找到 ref "${selector}" 对应的元素(可能已被移除或快照已过期)`;
1742
+ return el;
1893
1743
  }
1894
1744
  }
1895
1745
  const el = document.querySelector(selector);
@@ -1917,30 +1767,6 @@ function resolveWaitMs(params) {
1917
1767
  if (typeof waitSeconds === "number" && Number.isFinite(waitSeconds)) return Math.max(0, Math.floor(waitSeconds * 1e3));
1918
1768
  return DEFAULT_WAIT_MS;
1919
1769
  }
1920
- /** 生成元素的简洁描述字符串,用于工具调用结果的可读输出。 */
1921
- function describeElement(el) {
1922
- const tag = el.tagName.toLowerCase();
1923
- const id = el.id ? `#${el.id}` : "";
1924
- const cls = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean).slice(0, 3).map((c) => `.${c}`).join("") : "";
1925
- const text = el instanceof HTMLSelectElement ? el.selectedOptions[0]?.textContent?.trim().slice(0, 40) ?? "" : el.textContent?.trim().slice(0, 40) ?? "";
1926
- const textHint = text ? ` "${text}"` : "";
1927
- const hints = [];
1928
- for (const attr of [
1929
- "type",
1930
- "name",
1931
- "placeholder",
1932
- "href",
1933
- "role"
1934
- ]) {
1935
- const v = el.getAttribute(attr);
1936
- if (v) hints.push(`${attr}=${v}`);
1937
- }
1938
- if (el instanceof HTMLSelectElement && el.value) hints.push(`val=${el.value}`);
1939
- return `<${tag}${id}${cls}>${textHint}${hints.length > 0 ? ` [${hints.join(", ")}]` : ""}`;
1940
- }
1941
-
1942
- //#endregion
1943
- //#region src/web/tools/dom-tool/actionability.ts
1944
1770
  /** 检查元素样式可见性(处理 checkVisibility / details 折叠 / visibility) */
1945
1771
  function isStyleVisible(el, style) {
1946
1772
  style = style ?? window.getComputedStyle(el);
@@ -2021,6 +1847,23 @@ function checkElementStable(el, timeoutMs = 800) {
2021
1847
  requestAnimationFrame(check);
2022
1848
  });
2023
1849
  }
1850
+ /**
1851
+ * 将目标重定向到关联的交互控件。
1852
+ * - button-link:非交互元素→最近 button/[role=button]/a/[role=link]
1853
+ * - follow-label:label→control + 非交互→button/[role=button]/[role=checkbox]/[role=radio]
1854
+ */
1855
+ function retarget(el, mode) {
1856
+ if (mode === "none") return el;
1857
+ if (!el.matches("input, textarea, select") && !el.isContentEditable) if (mode === "button-link") el = el.closest("button, [role=button], a, [role=link]") || el;
1858
+ else el = el.closest("button, [role=button], [role=checkbox], [role=radio]") || el;
1859
+ if (mode === "follow-label") {
1860
+ if (!el.matches("a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]") && !el.isContentEditable) {
1861
+ const label = el.closest("label");
1862
+ if (label?.control) el = label.control;
1863
+ }
1864
+ }
1865
+ return el;
1866
+ }
2024
1867
  function scrollIntoViewIfNeeded(el, retry = 0) {
2025
1868
  if (retry === 0 && "scrollIntoViewIfNeeded" in el) {
2026
1869
  el.scrollIntoViewIfNeeded(true);
@@ -2042,7 +1885,7 @@ function checkHitTarget(el) {
2042
1885
  if (topEl === el || el.contains(topEl) || topEl.contains(el)) return null;
2043
1886
  const sharedLabel = topEl.closest("label");
2044
1887
  if (sharedLabel && sharedLabel.contains(el)) return null;
2045
- return `<${topEl.tagName.toLowerCase()}${topEl.id ? `#${topEl.id}` : ""}>`;
1888
+ return describeElement(topEl);
2046
1889
  }
2047
1890
  function ensureActionable(el, action, selector, force) {
2048
1891
  if (force) return null;
@@ -2097,15 +1940,6 @@ function ensureActionable(el, action, selector, force) {
2097
1940
  };
2098
1941
  return null;
2099
1942
  }
2100
-
2101
- //#endregion
2102
- //#region src/web/tools/dom-tool/events.ts
2103
- /**
2104
- * DOM Tool — 事件派发与键盘操作。
2105
- *
2106
- * 包含:完整点击事件链、hover 事件链、input/change 派发、
2107
- * 原生 setter 写入、selectText、组合键 press。
2108
- */
2109
1943
  function getClickPoint(el) {
2110
1944
  const r = el.getBoundingClientRect();
2111
1945
  return {
@@ -2114,7 +1948,7 @@ function getClickPoint(el) {
2114
1948
  };
2115
1949
  }
2116
1950
  /**
2117
- * 完整点击事件链:
1951
+ * 完整点击事件链(参考 Playwright Mouse.click):
2118
1952
  * pointermove → mousemove → (per clickCount) pointerdown → mousedown → focus → pointerup → mouseup → click
2119
1953
  */
2120
1954
  function dispatchClickEvents(el, clickCount = 1) {
@@ -2282,31 +2116,25 @@ function executePress(el, key) {
2282
2116
  ...modState
2283
2117
  }));
2284
2118
  }
2285
-
2286
- //#endregion
2287
- //#region src/web/tools/dom-tool/resolve.ts
2288
- /**
2289
- * DOM Tool 目标解析与归一化。
2290
- *
2291
- * 包含:retarget、checkable 目标归一化、pointer action 代理、
2292
- * 表单项控件重定向、editable 穿透。
2293
- */
2294
- /**
2295
- * 将目标重定向到关联的交互控件。
2296
- * - button-link:非交互元素→最近 button/[role=button]/a/[role=link]
2297
- * - follow-label:label→control + 非交互→button/[role=button]/[role=checkbox]/[role=radio]
2298
- */
2299
- function retarget(el, mode) {
2300
- if (mode === "none") return el;
2301
- if (!el.matches("input, textarea, select") && !el.isContentEditable) if (mode === "button-link") el = el.closest("button, [role=button], a, [role=link]") || el;
2302
- else el = el.closest("button, [role=button], [role=checkbox], [role=radio]") || el;
2303
- if (mode === "follow-label") {
2304
- if (!el.matches("a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]") && !el.isContentEditable) {
2305
- const label = el.closest("label");
2306
- if (label?.control) el = label.control;
2307
- }
2119
+ function describeElement(el) {
2120
+ const tag = el.tagName.toLowerCase();
2121
+ const id = el.id ? `#${el.id}` : "";
2122
+ const cls = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean).slice(0, 3).map((c) => `.${c}`).join("") : "";
2123
+ const text = el instanceof HTMLSelectElement ? el.selectedOptions[0]?.textContent?.trim().slice(0, 40) ?? "" : el.textContent?.trim().slice(0, 40) ?? "";
2124
+ const textHint = text ? ` "${text}"` : "";
2125
+ const hints = [];
2126
+ for (const attr of [
2127
+ "type",
2128
+ "name",
2129
+ "placeholder",
2130
+ "href",
2131
+ "role"
2132
+ ]) {
2133
+ const v = el.getAttribute(attr);
2134
+ if (v) hints.push(`${attr}=${v}`);
2308
2135
  }
2309
- return el;
2136
+ if (el instanceof HTMLSelectElement && el.value) hints.push(`val=${el.value}`);
2137
+ return `<${tag}${id}${cls}>${textHint}${hints.length > 0 ? ` [${hints.join(", ")}]` : ""}`;
2310
2138
  }
2311
2139
  function getChecked(el) {
2312
2140
  if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) return el.checked;
@@ -2353,30 +2181,6 @@ function resolvePointerActionTarget(el) {
2353
2181
  return el;
2354
2182
  }
2355
2183
  /**
2356
- * 点击目标上卷:当命中文本/装饰子节点时,优先上卷到最近可点击祖先。
2357
- *
2358
- * 典型场景:
2359
- * - 列表项文本本身无 click,但父级容器(如 .g-pointer)有点击语义
2360
- * - 事件委托绑定在祖先,子节点点击命中不稳定
2361
- */
2362
- function resolveClickableAncestorTarget(el) {
2363
- const isSelfClickable = (node) => {
2364
- if (node.matches("a, button, input, textarea, select, summary, [role=button], [role=link], [role=menuitem]")) return true;
2365
- if (node.hasAttribute("onclick")) return true;
2366
- const tabIndexAttr = node.getAttribute("tabindex");
2367
- if (tabIndexAttr !== null && tabIndexAttr !== "-1") return true;
2368
- if (getTrackedElementEvents(node).some((name) => name === "click" || name === "pointerdown" || name === "mousedown")) return true;
2369
- return false;
2370
- };
2371
- if (isSelfClickable(el)) return el;
2372
- let ancestor = el.parentElement;
2373
- for (let depth = 0; ancestor && depth < 6; depth++, ancestor = ancestor.parentElement) {
2374
- if (!isElementVisible(ancestor)) continue;
2375
- if (isSelfClickable(ancestor)) return ancestor;
2376
- }
2377
- return el;
2378
- }
2379
- /**
2380
2184
  * 当命中表单项说明 label(如 Element Plus el-form-item__label)时,
2381
2185
  * 自动重定向到同一表单项中的首个可交互控件。
2382
2186
  */
@@ -2391,35 +2195,6 @@ function resolveFormItemControlTarget(el) {
2391
2195
  if (control && isElementVisible(control)) return control;
2392
2196
  return el;
2393
2197
  }
2394
- /**
2395
- * 穿透包裹容器,查找内部可编辑子元素。
2396
- * 覆盖 UI 框架常见模式:wrapper div 包裹真实 input/textarea。
2397
- * 若自身已可编辑则直接返回;否则在子树中搜索第一个可编辑且可见的控件。
2398
- * 对 role=slider/spinbutton 等 ARIA widget:向上逐级查找最近容器中的关联 input。
2399
- */
2400
- function resolveEditableTarget(el) {
2401
- if (isEditableElement(el)) return el;
2402
- const inner = el.querySelector("input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]");
2403
- if (inner && isEditableElement(inner) && isElementVisible(inner)) return inner;
2404
- const role = el.getAttribute("role");
2405
- if (role === "slider" || role === "spinbutton") {
2406
- let ancestor = el.parentElement;
2407
- for (let depth = 0; ancestor && depth < 5; depth++, ancestor = ancestor.parentElement) {
2408
- const input = ancestor.querySelector("input[type=\"number\"], input[role=\"spinbutton\"], input:not([type=\"hidden\"])");
2409
- if (input instanceof HTMLInputElement && isEditableElement(input) && isElementVisible(input)) return input;
2410
- }
2411
- }
2412
- return el;
2413
- }
2414
-
2415
- //#endregion
2416
- //#region src/web/tools/dom-tool/dropdown.ts
2417
- /**
2418
- * DOM Tool — 自定义下拉增强。
2419
- *
2420
- * 包含:全局可见 option 查找、下拉弹出等待。
2421
- */
2422
- /** 在全局可见 option 节点中按文本匹配(精确 → 包含) */
2423
2198
  function findVisibleOptionByText(text) {
2424
2199
  const target = text.trim().toLowerCase();
2425
2200
  if (!target) return null;
@@ -2440,7 +2215,6 @@ function findVisibleOptionByText(text) {
2440
2215
  for (const n of visible) if (n.textContent?.trim().toLowerCase().includes(target)) return n;
2441
2216
  return null;
2442
2217
  }
2443
- /** 轮询等待下拉弹出层出现 */
2444
2218
  async function waitForDropdownPopup(maxWait = 500) {
2445
2219
  const start = Date.now();
2446
2220
  while (Date.now() - start < maxWait) {
@@ -2449,33 +2223,22 @@ async function waitForDropdownPopup(maxWait = 500) {
2449
2223
  await sleep(50);
2450
2224
  }
2451
2225
  }
2452
-
2453
- //#endregion
2454
- //#region src/web/tools/dom-tool/index.ts
2455
- /**
2456
- * DOM Tool — 浏览器 DOM 操作工具入口(结合 Playwright 核心交互模式增强)。
2457
- *
2458
- * 关键能力:
2459
- * 1. retarget — 点击时自动重定向到 button/link/label.control
2460
- * 2. scrollIntoView 多策略 — 4 种 block 对齐轮换,解决 sticky 遮挡
2461
- * 3. stable 检查 — rAF 逐帧检测元素位置稳定后再操作
2462
- * 4. hit-target 验证 — elementsFromPoint 检查是否被遮挡
2463
- * 5. 完整点击事件链 — pointermove→pointerdown→mousedown→pointerup→mouseup→click
2464
- * 6. check/uncheck 通过 click — 先检查→click 切换→验证状态
2465
- * 7. press 组合键 — 支持 Control+a, Shift+Enter 等修饰键
2466
- * 8. fill 分类型 — date/color/range 走 setValue,text 类走 selectAll+原生写入
2467
- * 9. 自定义下拉增强 — 更广泛的 option 选择器 + 等待弹出
2468
- * 10. ARIA disabled — 检查祖先链 aria-disabled
2469
- *
2470
- * 运行环境:浏览器 Content Script(直接访问 DOM,无 CDP)。
2471
- */
2472
2226
  function createDomTool() {
2473
2227
  return {
2474
2228
  name: "dom",
2475
2229
  description: [
2476
2230
  "Perform DOM operations on the current page.",
2477
2231
  "Actions: click, fill, select_option, clear, check, uncheck, type, focus, hover, scroll, press, get_text, get_attr, set_attr, add_class, remove_class.",
2478
- "fill auto-resolves wrapper inner input. check/uncheck toggles via click. press supports combos (Control+a). scroll supports steps for repeated scrolling."
2232
+ "Input/Select rule: before each fill/type/select_option, click or focus the same target immediately in the same round.",
2233
+ "For multiple fields, use alternating pairs in one batch: focus/click A -> fill/type A -> focus/click B -> fill/type B.",
2234
+ "Use the hash ID from DOM snapshot (e.g. #a1b2c) as selector.",
2235
+ "press supports combo keys like 'Control+a', 'Shift+Enter'.",
2236
+ "check/uncheck is done via click — state change is verified after action.",
2237
+ "Ordinal/index rule: treat visual order as 1-based when the instruction says 'the Nth item' (e.g. 4th star = 4th visible icon from left to right), and avoid off-by-one mistakes.",
2238
+ "Disambiguation rule: distinguish descriptive text/labels from actionable options. Do not click nearby label/help text; click the actual interactive option/control item (icon/button/option) that changes state.",
2239
+ "Unknown/complex components: if a container element (e.g. role=slider, rating, custom widget) has multiple child icons/items in the snapshot but you don't know how to operate it directly, try clicking the appropriate child element instead. For example, a rating component with 5 star icon children — click the 4th icon child to set 4 stars. A slider with a runway — clicking the runway at the right position may work. Always prefer interacting with visible children when the parent container doesn't respond to fill/click as expected.",
2240
+ "fill supports role=slider elements: use fill with a numeric value on a role=slider container (rating/slider) to set its value programmatically.",
2241
+ "For wheel/virtualized pickers where target option is not visible yet, use scroll on the picker column first, then click/select the newly visible option. scroll supports steps for repeated scrolling in one call."
2479
2242
  ].join(" "),
2480
2243
  schema: Type.Object({
2481
2244
  action: Type.String({ description: "DOM action: click | fill | select_option | clear | check | uncheck | type | focus | hover | scroll | press | get_text | get_attr | set_attr | add_class | remove_class." }),
@@ -2490,7 +2253,7 @@ function createDomTool() {
2490
2253
  deltaY: Type.Optional(Type.Number({ description: "Vertical scroll delta for scroll action. Positive = down, negative = up." })),
2491
2254
  deltaX: Type.Optional(Type.Number({ description: "Horizontal scroll delta for scroll action." })),
2492
2255
  steps: Type.Optional(Type.Number({ description: "Repeat count for scroll action (default 1, max 20)." })),
2493
- waitMs: Type.Optional(Type.Number({ description: "Wait timeout in ms before action (default: 1200)." })),
2256
+ waitMs: Type.Optional(Type.Number({ description: "Wait timeout in ms before action (default: 2000)." })),
2494
2257
  waitSeconds: Type.Optional(Type.Number({ description: "Wait timeout in seconds (fallback for waitMs)." })),
2495
2258
  force: Type.Optional(Type.Boolean({ description: "Skip actionability checks (default false)." }))
2496
2259
  }),
@@ -2538,18 +2301,13 @@ function createDomTool() {
2538
2301
  el = r;
2539
2302
  }
2540
2303
  if (action === "check" || action === "uncheck") el = resolveCheckableTarget(el);
2541
- if ([
2542
- "fill",
2543
- "type",
2544
- "clear"
2545
- ].includes(action)) el = resolveEditableTarget(retarget(el, "follow-label"));
2546
- const actionabilityTarget = action === "click" || action === "check" || action === "uncheck" ? resolvePointerActionTarget(resolveClickableAncestorTarget(resolveFormItemControlTarget(el))) : el;
2304
+ const actionabilityTarget = action === "click" || action === "check" || action === "uncheck" ? resolvePointerActionTarget(resolveFormItemControlTarget(el)) : el;
2547
2305
  try {
2548
2306
  const checkResult = ensureActionable(actionabilityTarget, action, selector, force);
2549
2307
  if (checkResult) return checkResult;
2550
2308
  switch (action) {
2551
2309
  case "click": {
2552
- const target = resolvePointerActionTarget(resolveClickableAncestorTarget(resolveFormItemControlTarget(retarget(el, force ? "none" : "button-link"))));
2310
+ const target = resolvePointerActionTarget(resolveFormItemControlTarget(retarget(el, force ? "none" : "button-link")));
2553
2311
  const clickCount = typeof params.clickCount === "number" ? params.clickCount : 1;
2554
2312
  if (target instanceof HTMLOptionElement) {
2555
2313
  const parent = target.parentElement;
@@ -2576,7 +2334,7 @@ function createDomTool() {
2576
2334
  case "fill": {
2577
2335
  const value = params.value;
2578
2336
  if (value === void 0) return { content: "缺少 value 参数" };
2579
- const target = el;
2337
+ const target = retarget(el, "follow-label");
2580
2338
  if (target instanceof HTMLInputElement) {
2581
2339
  const type = target.type.toLowerCase();
2582
2340
  if (INPUT_BLOCKED_TYPES.has(type)) return {
@@ -2719,7 +2477,7 @@ function createDomTool() {
2719
2477
  return { content: `已选择 ${describeElement(target)}: value="${selected.value}", label="${selected.text.trim()}"` };
2720
2478
  }
2721
2479
  case "clear": {
2722
- const target = el;
2480
+ const target = retarget(el, "follow-label");
2723
2481
  if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
2724
2482
  scrollIntoViewIfNeeded(target);
2725
2483
  target.focus();
@@ -2779,7 +2537,7 @@ function createDomTool() {
2779
2537
  case "type": {
2780
2538
  const value = params.value;
2781
2539
  if (value === void 0) return { content: "缺少 value 参数" };
2782
- const target = el;
2540
+ const target = retarget(el, "follow-label");
2783
2541
  scrollIntoViewIfNeeded(target);
2784
2542
  if (target instanceof HTMLElement) target.focus();
2785
2543
  for (const char of value) {
@@ -2946,50 +2704,6 @@ const MAX_EXPANDED_LIST_CHILDREN = 120;
2946
2704
  /** 定向放宽 children 的硬上限。 */
2947
2705
  const MAX_EXPANDED_CHILDREN_LIMIT = 300;
2948
2706
  /**
2949
- * 事件名 → 快照简写映射。
2950
- * 目的:大幅压缩 listeners="..." 占用的 token,同时保留可读性。
2951
- * 简写规则在 system-prompt 中向模型说明。
2952
- */
2953
- const EVENT_ABBREV = {
2954
- click: "clk",
2955
- dblclick: "dbl",
2956
- mousedown: "mdn",
2957
- mouseup: "mup",
2958
- mousemove: "mmv",
2959
- mouseover: "mov",
2960
- mouseout: "mot",
2961
- mouseenter: "men",
2962
- mouseleave: "mlv",
2963
- pointerdown: "pdn",
2964
- pointerup: "pup",
2965
- pointermove: "pmv",
2966
- pointerenter: "pen",
2967
- pointerleave: "plv",
2968
- touchstart: "tst",
2969
- touchend: "ted",
2970
- touchmove: "tmv",
2971
- keydown: "kdn",
2972
- keyup: "kup",
2973
- keypress: "kpr",
2974
- input: "inp",
2975
- change: "chg",
2976
- submit: "sub",
2977
- focus: "fcs",
2978
- blur: "blr",
2979
- scroll: "scl",
2980
- wheel: "whl",
2981
- drag: "drg",
2982
- dragstart: "drs",
2983
- dragend: "dre",
2984
- drop: "drp",
2985
- contextmenu: "ctx",
2986
- resize: "rsz"
2987
- };
2988
- /** 将完整事件名转为快照简写(未收录的取前 3 字符)。 */
2989
- function abbrevEvent(name) {
2990
- return EVENT_ABBREV[name] ?? name.slice(0, 3);
2991
- }
2992
- /**
2993
2707
  * 规整快照属性值,避免把长 base64/data URL 原样注入快照。
2994
2708
  */
2995
2709
  function sanitizeSnapshotAttrValue(value) {
@@ -3041,7 +2755,6 @@ function generateSnapshot(root = document.body, options = {}) {
3041
2755
  const expandChildrenRefSet = new Set((opts.expandChildrenRefs ?? []).map((ref) => ref.trim().replace(/^#/, "")).filter(Boolean));
3042
2756
  let emittedNodes = 0;
3043
2757
  let truncatedByNodeBudget = false;
3044
- const emittedRefIds = /* @__PURE__ */ new Set();
3045
2758
  const refStore = opts.refStore;
3046
2759
  const SKIP_TAGS = new Set([
3047
2760
  "SCRIPT",
@@ -3077,11 +2790,7 @@ function generateSnapshot(root = document.body, options = {}) {
3077
2790
  "value",
3078
2791
  "name",
3079
2792
  "role",
3080
- "tabindex",
3081
2793
  "aria-label",
3082
- "aria-valuenow",
3083
- "aria-valuemin",
3084
- "aria-valuemax",
3085
2794
  "src",
3086
2795
  "alt",
3087
2796
  "title",
@@ -3099,25 +2808,6 @@ function generateSnapshot(root = document.body, options = {}) {
3099
2808
  "LABEL",
3100
2809
  "SUMMARY"
3101
2810
  ]);
3102
- /** 常见可交互事件(用于提升元素交互优先级)。 */
3103
- const INTERACTIVE_EVENTS = new Set([
3104
- "click",
3105
- "dblclick",
3106
- "mousedown",
3107
- "mouseup",
3108
- "pointerdown",
3109
- "pointerup",
3110
- "touchstart",
3111
- "touchend",
3112
- "input",
3113
- "change",
3114
- "keydown",
3115
- "keyup",
3116
- "keypress",
3117
- "submit",
3118
- "focus",
3119
- "blur"
3120
- ]);
3121
2811
  /** 布尔状态属性 — 只在存在时输出(无值),如 disabled、checked */
3122
2812
  const BOOLEAN_ATTRS = [
3123
2813
  "disabled",
@@ -3152,37 +2842,6 @@ function generateSnapshot(root = document.body, options = {}) {
3152
2842
  if (rect.width === 0 && rect.height === 0) return false;
3153
2843
  return true;
3154
2844
  }
3155
- /** 统一标签名键值(HTML/SVG 在不同环境可能大小写不一致)。 */
3156
- function getTagKey(el) {
3157
- return (el.tagName || "").toUpperCase();
3158
- }
3159
- /** 判断元素是否存在绑定事件(inline 或 addEventListener 追踪)。 */
3160
- function hasBoundEvents(el) {
3161
- if (hasTrackedElementEvents(el)) return true;
3162
- for (const attr of Array.from(el.attributes)) if (attr.name.startsWith("on")) return true;
3163
- return false;
3164
- }
3165
- /**
3166
- * 判断子树内是否存在绑定事件元素。
3167
- *
3168
- * 说明:
3169
- * - 该判定只用于“是否允许剪枝布局容器”。
3170
- * - 命中扫描预算上限时保守返回 true,避免误剪导致交互目标丢失。
3171
- */
3172
- function hasBoundEventsInSubtree(el, scanBudget = 180) {
3173
- const stack = Array.from(el.children);
3174
- let scanned = 0;
3175
- while (stack.length > 0) {
3176
- const current = stack.pop();
3177
- if (!current) continue;
3178
- if (hasBoundEvents(current)) return true;
3179
- scanned += 1;
3180
- if (scanned >= scanBudget) return true;
3181
- const children = Array.from(current.children);
3182
- for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
3183
- }
3184
- return false;
3185
- }
3186
2845
  /**
3187
2846
  * 判断元素是否为「无意义布局容器」(智能剪枝候选)。
3188
2847
  * 满足所有条件时返回 true:
@@ -3193,26 +2852,19 @@ function generateSnapshot(root = document.body, options = {}) {
3193
2852
  */
3194
2853
  function isEmptyLayoutContainer(el, directText) {
3195
2854
  if (!pruneLayout) return false;
3196
- if (!LAYOUT_TAGS.has(getTagKey(el))) return false;
2855
+ if (!LAYOUT_TAGS.has(el.tagName)) return false;
3197
2856
  if (el.getAttribute("id")) return false;
3198
2857
  if (el.getAttribute("role") || el.getAttribute("aria-label")) return false;
3199
- if (hasBoundEvents(el)) return false;
2858
+ for (const attr of Array.from(el.attributes)) if (attr.name.startsWith("on")) return false;
3200
2859
  if (directText) return false;
3201
- if (hasBoundEventsInSubtree(el)) return false;
3202
2860
  return true;
3203
2861
  }
3204
- function hasInteractiveTrackedEvents(el) {
3205
- const trackedEvents = getTrackedElementEvents(el);
3206
- if (trackedEvents.length === 0) return false;
3207
- return trackedEvents.some((eventName) => INTERACTIVE_EVENTS.has(eventName));
3208
- }
3209
2862
  function isInteractiveElement(el) {
3210
- if (INTERACTIVE_TAGS.has(getTagKey(el))) return true;
2863
+ if (INTERACTIVE_TAGS.has(el.tagName)) return true;
3211
2864
  if (el.hasAttribute("onclick")) return true;
3212
2865
  if (el.hasAttribute("role")) return true;
3213
2866
  if (el.hasAttribute("tabindex")) return true;
3214
2867
  if (el.hasAttribute("aria-label")) return true;
3215
- if (hasInteractiveTrackedEvents(el)) return true;
3216
2868
  return false;
3217
2869
  }
3218
2870
  /** 判断是否为“选项列表”容器(时间/下拉/listbox 等)。 */
@@ -3241,9 +2893,7 @@ function generateSnapshot(root = document.body, options = {}) {
3241
2893
  return "";
3242
2894
  }
3243
2895
  if (depth > maxDepth) return "";
3244
- const tagKey = getTagKey(el);
3245
- if (SKIP_TAGS.has(tagKey)) return "";
3246
- if (el.getAttribute("id") === "__SVG_SPRITE_NODE__") return "";
2896
+ if (SKIP_TAGS.has(el.tagName)) return "";
3247
2897
  if (el.hasAttribute("data-autopilot-ignore")) return "";
3248
2898
  const style = window.getComputedStyle(el);
3249
2899
  if (style.display === "none" || style.visibility === "hidden") return "";
@@ -3275,12 +2925,6 @@ function generateSnapshot(root = document.body, options = {}) {
3275
2925
  if (!attrs.includes("readonly")) attrs.push("readonly");
3276
2926
  }
3277
2927
  if (el.hasAttribute("onclick")) attrs.push("onclick");
3278
- const trackedEvents = getTrackedElementEvents(el);
3279
- if (trackedEvents.length > 0) {
3280
- const preview = trackedEvents.slice(0, 6).map(abbrevEvent).join(",");
3281
- const suffix = trackedEvents.length > 6 ? ",..." : "";
3282
- attrs.push(`listeners="${preview}${suffix}"`);
3283
- }
3284
2928
  const testId = el.getAttribute("data-testid") || el.getAttribute("data-test-id");
3285
2929
  if (testId) {
3286
2930
  const safeTestId = sanitizeSnapshotAttrValue(testId).slice(0, 25);
@@ -3330,10 +2974,8 @@ function generateSnapshot(root = document.body, options = {}) {
3330
2974
  let line = `${indent}[${tag}]`;
3331
2975
  if (directText) line += ` "${directText.slice(0, maxTextLength)}"`;
3332
2976
  if (attrs.length) line += ` ${attrs.join(" ")}`;
3333
- if (hashId) {
3334
- line += ` #${hashId}`;
3335
- emittedRefIds.add(hashId);
3336
- } else line += ` ref="${currentPath}"`;
2977
+ if (hashId) line += ` #${hashId}`;
2978
+ else line += ` ref="${currentPath}"`;
3337
2979
  const lines = [line];
3338
2980
  emittedNodes++;
3339
2981
  const allChildren = Array.from(el.children);
@@ -3351,7 +2993,6 @@ function generateSnapshot(root = document.body, options = {}) {
3351
2993
  return lines.join("\n");
3352
2994
  }
3353
2995
  const output = walk(root, 0, "") || "(空页面)";
3354
- refStore?.prune(emittedRefIds);
3355
2996
  if (!truncatedByNodeBudget) return output;
3356
2997
  return `${output}\n... (snapshot truncated: maxNodes=${maxNodes})`;
3357
2998
  }
@@ -3579,7 +3220,7 @@ function createNavigateTool() {
3579
3220
  * - hash selector(如 #abc123)优先通过 RefStore 解析。
3580
3221
  * - 可见性语义与 dom-tool 保持一致(参考 Playwright 风格)。
3581
3222
  */
3582
- const DEFAULT_TIMEOUT = 6e3;
3223
+ const DEFAULT_TIMEOUT = 1e4;
3583
3224
  const POLL_INTERVAL_MS = 80;
3584
3225
  const STABLE_TICK_MS = 50;
3585
3226
  const OBSERVER_OPTIONS = {
@@ -3633,14 +3274,7 @@ function resolveSelector(selector) {
3633
3274
  const store = getActiveRefStore();
3634
3275
  if (store) {
3635
3276
  const id = selector.slice(1);
3636
- if (store.has(id)) {
3637
- const el = store.get(id);
3638
- if (!el || !el.isConnected) {
3639
- store.delete(id);
3640
- return null;
3641
- }
3642
- return el;
3643
- }
3277
+ if (store.has(id)) return store.get(id) ?? null;
3644
3278
  }
3645
3279
  }
3646
3280
  try {
@@ -3778,7 +3412,7 @@ function createWaitTool() {
3778
3412
  selector: Type.Optional(Type.String({ description: "CSS selector for wait_for_selector/wait_for_hidden" })),
3779
3413
  state: Type.Optional(Type.String({ description: "Selector state for wait_for_selector: attached | visible | hidden | detached (default: attached)" })),
3780
3414
  text: Type.Optional(Type.String({ description: "Text to wait for in wait_for_text" })),
3781
- timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 6000)" })),
3415
+ timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 10000)" })),
3782
3416
  quietMs: Type.Optional(Type.Number({ description: "Quiet window for wait_for_stable in milliseconds (default: 300)" }))
3783
3417
  }),
3784
3418
  execute: async (params) => {
@@ -3984,29 +3618,6 @@ var RefStore = class {
3984
3618
  has(id) {
3985
3619
  return this.map.has(id);
3986
3620
  }
3987
- /** 删除指定 hash ID 映射,返回是否删除成功。 */
3988
- delete(id) {
3989
- return this.map.delete(id);
3990
- }
3991
- /**
3992
- * 清理失效引用:
3993
- * - 仅保留 keepIds 中的映射(若提供)
3994
- * - 自动移除已脱离文档(isConnected=false)的元素
3995
- *
3996
- * @returns 被移除的映射数量
3997
- */
3998
- prune(keepIds) {
3999
- let removed = 0;
4000
- for (const [id, el] of this.map.entries()) {
4001
- const shouldKeepById = keepIds ? keepIds.has(id) : true;
4002
- const isConnected = el.isConnected;
4003
- if (!shouldKeepById || !isConnected) {
4004
- this.map.delete(id);
4005
- removed++;
4006
- }
4007
- }
4008
- return removed;
4009
- }
4010
3621
  /** 清空所有映射 */
4011
3622
  clear() {
4012
3623
  this.map.clear();
@@ -4138,7 +3749,6 @@ function registerToolHandler(executors) {
4138
3749
  * │ └──────────┘ └────────────┘ └──────────────┘ │
4139
3750
  * └──────────────────────────────────────────────────┘
4140
3751
  */
4141
- installEventListenerTracking();
4142
3752
  var WebAgent = class WebAgent {
4143
3753
  /** 默认系统提示词 key(兼容旧版 setSystemPrompt(prompt))。 */
4144
3754
  static DEFAULT_SYSTEM_PROMPT_KEY = "default";
@@ -4171,8 +3781,6 @@ var WebAgent = class WebAgent {
4171
3781
  autoSnapshot;
4172
3782
  /** 快照选项 */
4173
3783
  snapshotOptions;
4174
- /** 轮次后稳定等待配置 */
4175
- roundStabilityWait;
4176
3784
  /** 工具注册表实例 — 每个 WebAgent 拥有独立的工具集 */
4177
3785
  registry = new ToolRegistry();
4178
3786
  /** 事件回调 — 绑定后可实时获取 Agent 进度,用于 UI 展示 */
@@ -4189,7 +3797,6 @@ var WebAgent = class WebAgent {
4189
3797
  this.memory = options.memory ?? false;
4190
3798
  this.autoSnapshot = options.autoSnapshot ?? true;
4191
3799
  this.snapshotOptions = options.snapshotOptions ?? {};
4192
- this.roundStabilityWait = options.roundStabilityWait;
4193
3800
  if (typeof options.systemPrompt === "string") this.setSystemPrompt(options.systemPrompt);
4194
3801
  else if (options.systemPrompt && typeof options.systemPrompt === "object") this.setSystemPrompts(options.systemPrompt);
4195
3802
  }
@@ -4382,7 +3989,6 @@ var WebAgent = class WebAgent {
4382
3989
  history: this.memory ? this.history : void 0,
4383
3990
  dryRun: this.dryRun,
4384
3991
  maxRounds: this.maxRounds,
4385
- roundStabilityWait: this.roundStabilityWait,
4386
3992
  callbacks: wrappedCallbacks
4387
3993
  });
4388
3994
  if (this.memory) this.history = result.messages;