agentpage 0.0.20 → 0.0.22

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
@@ -90,7 +90,8 @@ function hasToolError(result) {
90
90
  * 压缩/剪枝是怎么做的(中)/ How compression & pruning works in practice (EN):
91
91
  * - `viewportOnly=true` 时:仅保留与视口相交元素(根层容器保留),完全视口外元素跳过。
92
92
  * - `pruneLayout=true` 时:无 id/无语义/无交互/无直接文本的布局容器会被“折叠”,
93
- * 子节点直接提升输出,减少无意义层级。
93
+ * 子节点直接提升输出,减少无意义层级;当同一折叠容器提升出多个相邻节点时,
94
+ * 快照会用括号分组块标记其关联来源(collapsed-group)。
94
95
  * - `maxNodes`:全局节点预算,超限后停止继续遍历并追加 truncation 提示。
95
96
  * - `maxChildren`:每个父节点只保留前 N 个子元素,其余用 `... (n children omitted)` 汇总。
96
97
  * - `maxTextLength`:节点文本按长度截断,避免长段文案占满上下文。
@@ -215,7 +216,7 @@ function buildCompactMessages(userMessage, trace, latestSnapshot, currentUrl, hi
215
216
  activeInstruction
216
217
  ];
217
218
  if (currentUrl) parts.push("", `URL: ${currentUrl}`);
218
- if (latestSnapshot) parts.push("", "## Current page snapshot", "Apply task-reduction model directly from this snapshot. Do NOT restate the task.", "Use hash IDs (e.g. #a1b2c) from the snapshot as selector params.", "Do NOT call page_info (get_url/get_title/query_all/snapshot).", "Batch independent visible actions in one round.", "If action changes DOM (open modal/navigate), stop that batch and continue next round.", "For dropdown/select fields, use dom with action=select_option (or fill on a select).", 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.", "Output one line: REMAINING: <new remaining task after this round> or REMAINING: DONE", wrapSnapshot(latestSnapshot));
219
+ if (latestSnapshot) parts.push("", "## Current page snapshot", "Apply task-reduction model directly from this snapshot. Do NOT restate the task.", "Use hash IDs (e.g. #a1b2c) from the snapshot as selector params.", "Do NOT call page_info (get_url/get_title/query_all/snapshot).", "Batch independent visible actions in one round.", "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.", "If action changes DOM (open modal/navigate), stop that batch and continue next round.", "For dropdown/select fields, use dom with action=select_option (or fill on a select).", "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.", "Output one line: REMAINING: <new remaining task after this round> or REMAINING: DONE", wrapSnapshot(latestSnapshot));
219
220
  if (protocolViolationHint) parts.push("", protocolViolationHint);
220
221
  messages.push({
221
222
  role: "user",
@@ -251,6 +252,9 @@ function buildCompactMessages(userMessage, trace, latestSnapshot, currentUrl, hi
251
252
  "If action changes DOM (open modal/navigate), stop after that batch and continue next round.",
252
253
  "Do NOT call page_info (get_url/get_title/query_all/snapshot).",
253
254
  "For dropdown/select fields, use dom with action=select_option (or fill on a select).",
255
+ "Build the minimal action array from current snapshot to finish this remaining instruction in one round whenever possible.",
256
+ "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.",
257
+ "Stop rule: once requested state is reached, stop tool calls. If verification is needed, verify once and then output REMAINING: DONE.",
254
258
  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."
255
259
  ];
256
260
  if (hasErrors) contextParts.push("", "The last step failed. Retry with a different approach, or skip and continue with other visible targets.");
@@ -372,15 +376,21 @@ async function handleNavigationUrlChange(toolName, toolInput, result, registry,
372
376
  pageContext.latestSnapshot = await readPageSnapshot(registry);
373
377
  }
374
378
  }
375
- /** 只读工具集合(中)/ Read-only tool set (EN). */
379
+ /** 只读工具集合。 */
376
380
  const READ_ONLY_TOOLS = new Set(["page_info"]);
381
+ /** DOM 只读动作集合。 */
382
+ const READ_ONLY_DOM_ACTIONS = new Set(["get_text", "get_attr"]);
377
383
  /**
378
- * 空转检测(中)/ Detect idle loops dominated by read-only actions (EN).
384
+ * 空转检测:识别连续只读轮次并终止。
379
385
  * 返回 -1 表示应终止循环。
380
- * Returns -1 when loop should terminate.
381
386
  */
382
- function detectIdleLoop(toolCallNames, consecutiveReadOnlyRounds) {
383
- if (toolCallNames.every((name) => READ_ONLY_TOOLS.has(name))) {
387
+ function detectIdleLoop(toolCalls, consecutiveReadOnlyRounds) {
388
+ if (toolCalls.length > 0 && toolCalls.every(({ name, input }) => {
389
+ if (READ_ONLY_TOOLS.has(name)) return true;
390
+ if (name !== "dom") return false;
391
+ const action = getToolAction(input);
392
+ return Boolean(action && READ_ONLY_DOM_ACTIONS.has(action));
393
+ })) {
384
394
  const newCount = consecutiveReadOnlyRounds + 1;
385
395
  return newCount >= 2 ? -1 : newCount;
386
396
  }
@@ -390,10 +400,10 @@ function detectIdleLoop(toolCallNames, consecutiveReadOnlyRounds) {
390
400
  //#endregion
391
401
  //#region src/core/agent-loop/index.ts
392
402
  /**
393
- * Agent Loop 主流程(中)/ Core environment-agnostic agent loop (EN).
403
+ * Agent Loop 主流程
394
404
  *
395
405
  * 负责消息构建、AI 决策、工具执行、恢复保护与指标汇总。
396
- * Orchestrates message build, AI decisions, tool execution, recovery, and metrics.
406
+ *
397
407
  *
398
408
  * 流程图(文本):
399
409
  *
@@ -510,15 +520,18 @@ async function executeAgentLoop(params) {
510
520
  return (trimmed.split(/\n\s*\n/)[0]?.trim() ?? trimmed).slice(0, 220);
511
521
  };
512
522
  /**
513
- * 判定动作是否会触发 DOM 结构变化(中)/ Whether action may cause DOM-shape change (EN).
523
+ * 判定动作是否会触发 DOM 结构变化(
514
524
  *
515
525
  * 触发后应强制断轮,等待下一轮新快照继续。
516
- * Force round break after such action and continue with refreshed snapshot next round.
526
+ *
517
527
  */
518
528
  const shouldForceRoundBreak = (toolName, toolInput) => {
519
529
  const action = getToolAction(toolInput);
520
530
  if (toolName === "navigate") return action === "goto" || action === "back" || action === "forward" || action === "reload";
521
- if (toolName === "dom") return action === "click" || action === "press";
531
+ if (toolName === "dom") {
532
+ if (action === "press") return (typeof toolInput === "object" && toolInput !== null ? String(toolInput.key ?? toolInput.value ?? "") : "") === "Enter";
533
+ return false;
534
+ }
522
535
  if (toolName === "evaluate") return true;
523
536
  return false;
524
537
  };
@@ -720,9 +733,12 @@ async function executeAgentLoop(params) {
720
733
  lastRoundHadError = roundHasError;
721
734
  previousRoundTasks = buildTaskArray(executedTaskCalls);
722
735
  previousRoundPlannedTasks = plannedTasksCurrentRound;
723
- const idleResult = detectIdleLoop(executedTaskCalls.map((tc) => tc.name), consecutiveReadOnlyRounds);
736
+ const idleResult = detectIdleLoop(response.toolCalls.map((tc) => ({
737
+ name: tc.name,
738
+ input: tc.input
739
+ })), consecutiveReadOnlyRounds);
724
740
  if (idleResult === -1) {
725
- finalReply = response.text || "任务已完成。";
741
+ finalReply = response.text?.trim() || "任务已完成。";
726
742
  if (finalReply) callbacks?.onText?.(finalReply);
727
743
  break;
728
744
  }
@@ -1304,6 +1320,14 @@ var ToolRegistry = class {
1304
1320
  getDefinitions() {
1305
1321
  return Array.from(this.tools.values());
1306
1322
  }
1323
+ /** 按名称检查工具是否已注册。 */
1324
+ has(name) {
1325
+ return this.tools.has(name);
1326
+ }
1327
+ /** 按名称注销工具,返回是否删除成功。 */
1328
+ unregister(name) {
1329
+ return this.tools.delete(name);
1330
+ }
1307
1331
  /**
1308
1332
  * 根据工具名分发并执行工具调用。
1309
1333
  * - 找到工具 → 执行 execute() → 返回结果
@@ -1338,23 +1362,17 @@ var ToolRegistry = class {
1338
1362
 
1339
1363
  //#endregion
1340
1364
  //#region src/core/system-prompt.ts
1341
- /**
1342
- * 规范化额外指令(中)/ Normalize additional instructions (EN).
1343
- */
1365
+ /** 规范化额外指令。 */
1344
1366
  function normalizeExtraInstructions(input) {
1345
1367
  if (!input) return [];
1346
1368
  return (Array.isArray(input) ? input : [input]).map((s) => s.trim()).filter(Boolean);
1347
1369
  }
1348
1370
  /**
1349
- * 构建系统提示词(中)/ Build system prompt (EN).
1371
+ * 构建系统提示词。
1350
1372
  *
1351
1373
  * 约束:
1352
1374
  * - 输出给模型的提示词正文统一为英文。
1353
- * - 中文仅用于代码注释,便于团队维护。
1354
- *
1355
- * Constraints:
1356
- * - Prompt text sent to model stays English-only.
1357
- * - Chinese content is used in code comments only for maintainability.
1375
+ * - 中文仅用于源码注释,便于团队维护。
1358
1376
  */
1359
1377
  function buildSystemPrompt(params = {}) {
1360
1378
  const sections = [];
@@ -1368,13 +1386,19 @@ function buildSystemPrompt(params = {}) {
1368
1386
  " Output: new remaining task after removing this-round actions.",
1369
1387
  "- Use only visible targets from snapshot. Use #hashID as selector. Do not guess CSS selectors.",
1370
1388
  "- Batch independent visible actions in one round. Do not split one form into many rounds unnecessarily.",
1371
- "- Strict input order: before every fill/type/select_option, first click or focus the same target in the SAME round.",
1372
- "- Fixed sequence examples: dom.click(#field) -> dom.fill(#field, \"text\"); dom.click(#select) -> dom.select_option(#select, ...).",
1389
+ "- Strict input order (MANDATORY): before every fill/type/select_option, click or focus the SAME target immediately in the SAME round.",
1390
+ "- Multi-field rule (MANDATORY): execute alternating pairs in one batch: focus/click field A -> fill/type A -> focus/click field B -> fill/type B.",
1391
+ "- Build the minimal action array from CURRENT snapshot to satisfy the target in one round whenever possible.",
1392
+ "- Do NOT run focus-only batches (e.g., focus A -> focus B). Each focused input/select target must be followed by its input/select action right away.",
1393
+ "- Fixed sequence examples: dom.focus(#name) -> dom.fill(#name, \"new-name\") -> dom.focus(#desc) -> dom.fill(#desc, \"new-desc\"); dom.click(#select) -> dom.select_option(#select, ...).",
1394
+ "- Deterministic delta rule: for increase/decrease steppers, compute target delta from visible current value and emit exactly |delta| clicks in one round (e.g., +2 => click increase twice). Never overshoot then undo.",
1395
+ "- For check/uncheck, target the real input control (checkbox/radio), not nearby text/container nodes.",
1373
1396
  "- Form batch rule: for one visible form, complete all independent fields in one round; do not fill one field then verify repeatedly.",
1374
1397
  "- If an action will change DOM (open modal, navigate), stop after that action batch and continue next round with new snapshot.",
1375
1398
  "- Do NOT call page_info (snapshot/query/get_url/get_title). Snapshot is already provided every round.",
1376
1399
  "- For dropdown/select, use dom action=select_option (or fill on select).",
1377
1400
  "- Verification whitelist: do NOT use get_text/get_attr to verify input/select values unless the user explicitly asks for verification.",
1401
+ "- 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).",
1378
1402
  "- Do NOT interact with AutoPilot UI unless user explicitly asks.",
1379
1403
  "",
1380
1404
  "## Output Contract",
@@ -1406,41 +1430,93 @@ function buildSystemPrompt(params = {}) {
1406
1430
  //#endregion
1407
1431
  //#region src/web/tools/dom-tool.ts
1408
1432
  /**
1409
- * DOM Tool — 基于 Web API DOM 操作工具。
1433
+ * DOM Tool — 浏览器 DOM 操作工具(结合 Playwright 核心交互模式增强)。
1410
1434
  *
1411
- * 替代 Playwright 的 click/fill/type 等操作,直接在页面上下文中执行。
1412
- * 运行环境:浏览器 Content Script。
1435
+ * 关键改进(参考 Playwright):
1436
+ * 1. retarget — 点击时自动重定向到 button/link/label.control
1437
+ * 2. scrollIntoView 多策略 — 4 种 block 对齐轮换,解决 sticky 遮挡
1438
+ * 3. stable 检查 — rAF 逐帧检测元素位置稳定后再操作
1439
+ * 4. hit-target 验证 — elementsFromPoint 检查是否被遮挡
1440
+ * 5. 完整点击事件链 — pointermove→pointerdown→mousedown→pointerup→mouseup→click
1441
+ * 6. check/uncheck 通过 click — 先检查→click 切换→验证状态
1442
+ * 7. press 组合键 — 支持 Control+a, Shift+Enter 等修饰键
1443
+ * 8. fill 分类型 — date/color/range 走 setValue,text 类走 selectAll+原生写入
1444
+ * 9. 自定义下拉增强 — 更广泛的 option 选择器 + 等待弹出
1445
+ * 10. ARIA disabled — 检查祖先链 aria-disabled
1413
1446
  *
1414
- * 支持 15 种动作:
1415
- * click — 点击元素
1416
- * fill — 填写可编辑控件(input/textarea/select/contenteditable)
1417
- * select_option — 选择下拉框选项(value/label)
1418
- * clear — 清空输入控件
1419
- * check — 勾选 checkbox/radio
1420
- * uncheck — 取消勾选 checkbox
1421
- * type — 逐字符模拟键入
1422
- * focus — 聚焦元素
1423
- * hover — 鼠标悬停(触发 mouseenter/mouseover)
1424
- * press — 按下键盘按键(Enter/Escape/Tab/ArrowDown 等)
1425
- * get_text — 获取元素文本内容
1426
- * get_attr — 获取元素属性值
1427
- * set_attr — 设置元素属性
1428
- * add_class — 添加 CSS 类名
1429
- * remove_class — 移除 CSS 类名
1447
+ * 运行环境:浏览器 Content Script(直接访问 DOM,无 CDP)。
1430
1448
  */
1431
- const DEFAULT_WAIT_MS = 1e3;
1432
- /** 当前活跃的 RefStore 实例(由 WebAgent 在 chat() 时设置) */
1449
+ const DEFAULT_WAIT_MS = 2e3;
1450
+ /** scrollIntoView 轮换策略(参考 Playwright dom.ts) */
1451
+ const SCROLL_OPTIONS = [
1452
+ void 0,
1453
+ {
1454
+ block: "end",
1455
+ inline: "end"
1456
+ },
1457
+ {
1458
+ block: "center",
1459
+ inline: "center"
1460
+ },
1461
+ {
1462
+ block: "start",
1463
+ inline: "start"
1464
+ }
1465
+ ];
1466
+ /** fill 时直接 setValue 的 input 类型(参考 Playwright kInputTypesToSetValue) */
1467
+ const INPUT_SET_VALUE_TYPES = new Set([
1468
+ "color",
1469
+ "date",
1470
+ "time",
1471
+ "datetime-local",
1472
+ "month",
1473
+ "range",
1474
+ "week"
1475
+ ]);
1476
+ /** 不可 fill 的 input 类型 */
1477
+ const INPUT_BLOCKED_TYPES = new Set([
1478
+ "checkbox",
1479
+ "radio",
1480
+ "file",
1481
+ "button",
1482
+ "submit",
1483
+ "reset",
1484
+ "image"
1485
+ ]);
1486
+ /** 键名→code 映射 */
1487
+ const KEY_CODE_MAP = {
1488
+ Enter: "Enter",
1489
+ Escape: "Escape",
1490
+ Esc: "Escape",
1491
+ Tab: "Tab",
1492
+ Space: "Space",
1493
+ " ": "Space",
1494
+ Backspace: "Backspace",
1495
+ Delete: "Delete",
1496
+ ArrowUp: "ArrowUp",
1497
+ ArrowDown: "ArrowDown",
1498
+ ArrowLeft: "ArrowLeft",
1499
+ ArrowRight: "ArrowRight",
1500
+ Home: "Home",
1501
+ End: "End",
1502
+ PageUp: "PageUp",
1503
+ PageDown: "PageDown",
1504
+ Control: "ControlLeft",
1505
+ Shift: "ShiftLeft",
1506
+ Alt: "AltLeft",
1507
+ Meta: "MetaLeft"
1508
+ };
1433
1509
  let activeRefStore;
1510
+ function setActiveRefStore(store) {
1511
+ activeRefStore = store;
1512
+ }
1513
+ function getActiveRefStore() {
1514
+ return activeRefStore;
1515
+ }
1434
1516
  function sleep(ms) {
1435
- return new Promise((resolve) => setTimeout(resolve, ms));
1517
+ return new Promise((r) => setTimeout(r, ms));
1436
1518
  }
1437
- /**
1438
- * 安全地查询 DOM 元素。
1439
- *
1440
- * 支持两种定位方式(优先级从高到低):
1441
- * - hash ID(以 "#" 开头且在 RefStore 中存在):确定性 hash 查找(最高效)
1442
- * - CSS 选择器(其他):传统 querySelector
1443
- */
1519
+ /** 查询元素:优先 RefStore hash,回退 CSS 选择器 */
1444
1520
  function queryElement(selector) {
1445
1521
  try {
1446
1522
  if (selector.startsWith("#") && activeRefStore) {
@@ -1458,28 +1534,13 @@ function queryElement(selector) {
1458
1534
  return `选择器语法错误: ${selector}`;
1459
1535
  }
1460
1536
  }
1461
- /**
1462
- * 设置当前活跃的 RefStore(由 WebAgent 在 chat 开始时调用)。
1463
- */
1464
- function setActiveRefStore(store) {
1465
- activeRefStore = store;
1466
- }
1467
- /** 获取当前活跃的 RefStore(供其他工具复用) */
1468
- function getActiveRefStore() {
1469
- return activeRefStore;
1470
- }
1471
- /**
1472
- * 在给定超时时间内轮询查找元素。
1473
- * - 返回 Element:找到元素
1474
- * - 返回 string:选择器语法错误
1475
- * - 返回 null:超时未找到
1476
- */
1537
+ /** 轮询等待元素出现 */
1477
1538
  async function waitForElement(selector, timeoutMs) {
1478
1539
  const start = Date.now();
1479
1540
  while (Date.now() - start <= timeoutMs) {
1480
- const elOrError = queryElement(selector);
1481
- if (typeof elOrError !== "string") return elOrError;
1482
- if (elOrError.startsWith("选择器语法错误")) return elOrError;
1541
+ const r = queryElement(selector);
1542
+ if (typeof r !== "string") return r;
1543
+ if (r.startsWith("选择器语法错误")) return r;
1483
1544
  await sleep(100);
1484
1545
  }
1485
1546
  return null;
@@ -1491,119 +1552,128 @@ function resolveWaitMs(params) {
1491
1552
  if (typeof waitSeconds === "number" && Number.isFinite(waitSeconds)) return Math.max(0, Math.floor(waitSeconds * 1e3));
1492
1553
  return DEFAULT_WAIT_MS;
1493
1554
  }
1494
- /**
1495
- * 模拟真实用户输入:触发 input、change 事件,兼容 React/Vue 等框架。
1496
- */
1497
- function dispatchInputEvents(el) {
1498
- try {
1499
- el.dispatchEvent(new InputEvent("input", {
1500
- bubbles: true,
1501
- cancelable: true,
1502
- inputType: "insertText",
1503
- data: null
1504
- }));
1505
- } catch {
1506
- el.dispatchEvent(new Event("input", {
1507
- bubbles: true,
1508
- cancelable: true
1509
- }));
1510
- }
1511
- el.dispatchEvent(new Event("change", {
1512
- bubbles: true,
1513
- cancelable: true
1514
- }));
1515
- }
1516
- /**
1517
- * 使用原生 setter 写入表单值,提升对受控组件(React/Vue 等)的兼容性。
1518
- */
1519
- function setNativeEditableValue(el, value) {
1520
- const proto = el instanceof HTMLInputElement ? HTMLInputElement.prototype : el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLSelectElement.prototype;
1521
- const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
1522
- if (descriptor?.set) {
1523
- descriptor.set.call(el, value);
1524
- return;
1555
+ /** 检查元素样式可见性(处理 checkVisibility / details 折叠 / visibility) */
1556
+ function isStyleVisible(el, style) {
1557
+ style = style ?? window.getComputedStyle(el);
1558
+ if (typeof el.checkVisibility === "function") {
1559
+ if (!el.checkVisibility()) return false;
1560
+ } else {
1561
+ const det = el.closest("details,summary");
1562
+ if (det !== el && det?.nodeName === "DETAILS" && !det.open) return false;
1525
1563
  }
1526
- el.value = value;
1527
- }
1528
- /**
1529
- * 读取可编辑元素当前值。
1530
- */
1531
- function getEditableValue(el) {
1532
- return el.value ?? "";
1533
- }
1534
- /**
1535
- * 将常见 key 映射为更接近浏览器语义的 KeyboardEvent.code。
1536
- */
1537
- function resolveKeyboardCode(key) {
1538
- return {
1539
- Enter: "Enter",
1540
- Escape: "Escape",
1541
- Esc: "Escape",
1542
- Tab: "Tab",
1543
- Space: "Space",
1544
- " ": "Space",
1545
- Backspace: "Backspace",
1546
- Delete: "Delete",
1547
- ArrowUp: "ArrowUp",
1548
- ArrowDown: "ArrowDown",
1549
- ArrowLeft: "ArrowLeft",
1550
- ArrowRight: "ArrowRight"
1551
- }[key] ?? key;
1564
+ return style.visibility === "visible";
1552
1565
  }
1553
1566
  /**
1554
- * 生成元素的可读描述,用于在操作结果中展示实际命中的 DOM 节点。
1555
- * 格式:<tag#id.class> "文本" [attr=val, ...]
1567
+ * 元素可见性检查(参考 Playwright isElementVisible+computeBox)。
1568
+ * 处理 display:contents / display:none / visibility / opacity / 尺寸为 0。
1556
1569
  */
1557
- function describeElement(el) {
1558
- const tag = el.tagName.toLowerCase();
1559
- const id = el.id ? `#${el.id}` : "";
1560
- const cls = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean).slice(0, 3).map((c) => `.${c}`).join("") : "";
1561
- const text = el instanceof HTMLSelectElement ? el.selectedOptions[0]?.textContent?.trim().slice(0, 40) ?? "" : el.textContent?.trim().slice(0, 40) ?? "";
1562
- const textHint = text ? ` "${text}"` : "";
1563
- const hints = [];
1564
- for (const attr of [
1565
- "type",
1566
- "name",
1567
- "placeholder",
1568
- "href",
1569
- "role"
1570
- ]) {
1571
- const val = el.getAttribute(attr);
1572
- if (val) hints.push(`${attr}=${val}`);
1573
- }
1574
- if (el instanceof HTMLSelectElement && el.value) hints.push(`val=${el.value}`);
1575
- return `<${tag}${id}${cls}>${textHint}${hints.length > 0 ? ` [${hints.join(", ")}]` : ""}`;
1576
- }
1577
1570
  function isElementVisible(el) {
1578
1571
  if (!(el instanceof HTMLElement || el instanceof SVGElement)) return false;
1579
1572
  if (!el.isConnected) return false;
1580
1573
  const style = window.getComputedStyle(el);
1581
- if (style.display === "none" || style.visibility === "hidden") return false;
1574
+ if (style.display === "contents") {
1575
+ for (let child = el.firstChild; child; child = child.nextSibling) {
1576
+ if (child.nodeType === Node.ELEMENT_NODE && isElementVisible(child)) return true;
1577
+ if (child.nodeType === Node.TEXT_NODE) {
1578
+ const range = document.createRange();
1579
+ range.selectNodeContents(child);
1580
+ const rects = range.getClientRects();
1581
+ for (let i = 0; i < rects.length; i++) if (rects[i].width > 0 && rects[i].height > 0) return true;
1582
+ }
1583
+ }
1584
+ return false;
1585
+ }
1586
+ if (style.display === "none") return false;
1587
+ if (!isStyleVisible(el, style)) return false;
1582
1588
  if (style.opacity === "0") return false;
1583
1589
  const rect = el.getBoundingClientRect();
1584
1590
  return rect.width > 0 && rect.height > 0;
1585
1591
  }
1592
+ /** ARIA disabled:检查元素自身 + 祖先链 aria-disabled(参考 Playwright getAriaDisabled) */
1586
1593
  function isElementDisabled(el) {
1587
- if (!(el instanceof HTMLElement)) return false;
1588
- if (el.hasAttribute("disabled")) return true;
1589
- if (el.getAttribute("aria-disabled") === "true") return true;
1590
- if ("disabled" in el && typeof el.disabled === "boolean") return Boolean(el.disabled);
1594
+ if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) {
1595
+ if (el.disabled) return true;
1596
+ }
1597
+ let cursor = el;
1598
+ while (cursor) {
1599
+ if (cursor.getAttribute("aria-disabled") === "true") return true;
1600
+ cursor = cursor.parentElement;
1601
+ }
1591
1602
  return false;
1592
1603
  }
1593
1604
  function isEditableElement(el) {
1594
1605
  if (el instanceof HTMLTextAreaElement) return !el.readOnly;
1595
- if (el instanceof HTMLInputElement) return !new Set([
1596
- "checkbox",
1597
- "radio",
1598
- "file",
1599
- "button",
1600
- "submit",
1601
- "reset"
1602
- ]).has(el.type) && !el.readOnly;
1606
+ if (el instanceof HTMLInputElement) return !INPUT_BLOCKED_TYPES.has(el.type) && !el.readOnly;
1603
1607
  if (el instanceof HTMLSelectElement) return true;
1604
1608
  return el instanceof HTMLElement && el.isContentEditable;
1605
1609
  }
1606
- function ensureActionable(el, action, selector) {
1610
+ /** rAF 逐帧检查元素位置是否连续 3 帧不变 */
1611
+ function checkElementStable(el, timeoutMs = 800) {
1612
+ return new Promise((resolve) => {
1613
+ let lastRect;
1614
+ let stableCount = 0;
1615
+ const start = performance.now();
1616
+ function check() {
1617
+ if (performance.now() - start > timeoutMs || !el.isConnected) {
1618
+ resolve(false);
1619
+ return;
1620
+ }
1621
+ const rect = el.getBoundingClientRect();
1622
+ if (lastRect) {
1623
+ if (!(rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height)) stableCount = 0;
1624
+ else if (++stableCount >= 3) {
1625
+ resolve(true);
1626
+ return;
1627
+ }
1628
+ }
1629
+ lastRect = rect;
1630
+ requestAnimationFrame(check);
1631
+ }
1632
+ requestAnimationFrame(check);
1633
+ });
1634
+ }
1635
+ /**
1636
+ * 将目标重定向到关联的交互控件。
1637
+ * - button-link:非交互元素→最近 button/[role=button]/a/[role=link]
1638
+ * - follow-label:label→control + 非交互→button/[role=button]/[role=checkbox]/[role=radio]
1639
+ */
1640
+ function retarget(el, mode) {
1641
+ if (mode === "none") return el;
1642
+ if (!el.matches("input, textarea, select") && !el.isContentEditable) if (mode === "button-link") el = el.closest("button, [role=button], a, [role=link]") || el;
1643
+ else el = el.closest("button, [role=button], [role=checkbox], [role=radio]") || el;
1644
+ if (mode === "follow-label") {
1645
+ if (!el.matches("a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]") && !el.isContentEditable) {
1646
+ const label = el.closest("label");
1647
+ if (label?.control) el = label.control;
1648
+ }
1649
+ }
1650
+ return el;
1651
+ }
1652
+ function scrollIntoViewIfNeeded(el, retry = 0) {
1653
+ if (retry === 0 && "scrollIntoViewIfNeeded" in el) {
1654
+ el.scrollIntoViewIfNeeded(true);
1655
+ return;
1656
+ }
1657
+ const opts = SCROLL_OPTIONS[retry % SCROLL_OPTIONS.length];
1658
+ el.scrollIntoView(opts ?? {
1659
+ block: "center",
1660
+ inline: "nearest"
1661
+ });
1662
+ }
1663
+ /** 检查元素中心点是否被遮挡,返回遮挡元素描述或 null */
1664
+ function checkHitTarget(el) {
1665
+ const rect = el.getBoundingClientRect();
1666
+ const x = rect.left + rect.width / 2;
1667
+ const y = rect.top + rect.height / 2;
1668
+ const topEl = document.elementFromPoint(x, y);
1669
+ if (!topEl) return null;
1670
+ if (topEl === el || el.contains(topEl) || topEl.contains(el)) return null;
1671
+ const sharedLabel = topEl.closest("label");
1672
+ if (sharedLabel && sharedLabel.contains(el)) return null;
1673
+ return describeElement(topEl);
1674
+ }
1675
+ function ensureActionable(el, action, selector, force) {
1676
+ if (force) return null;
1607
1677
  if (!el.isConnected) return {
1608
1678
  content: `"${selector}" 元素已脱离文档,无法执行 ${action}`,
1609
1679
  details: {
@@ -1632,7 +1702,7 @@ function ensureActionable(el, action, selector) {
1632
1702
  "check",
1633
1703
  "uncheck"
1634
1704
  ]).has(action) && isElementDisabled(el)) return {
1635
- content: `"${selector}" 元素已禁用,无法执行 ${action}`,
1705
+ content: `"${selector}" 元素已禁用(disabled/aria-disabled),无法执行 ${action}`,
1636
1706
  details: {
1637
1707
  error: true,
1638
1708
  code: "ELEMENT_DISABLED",
@@ -1655,45 +1725,318 @@ function ensureActionable(el, action, selector) {
1655
1725
  };
1656
1726
  return null;
1657
1727
  }
1658
- function isOptionCandidateVisible(el) {
1659
- if (!(el instanceof HTMLElement)) return false;
1660
- if (!isElementVisible(el)) return false;
1661
- return (el.textContent?.trim() ?? "").length > 0;
1728
+ function getClickPoint(el) {
1729
+ const r = el.getBoundingClientRect();
1730
+ return {
1731
+ x: r.left + r.width / 2,
1732
+ y: r.top + r.height / 2
1733
+ };
1734
+ }
1735
+ /**
1736
+ * 完整点击事件链(参考 Playwright Mouse.click):
1737
+ * pointermove → mousemove → (per clickCount) pointerdown → mousedown → focus → pointerup → mouseup → click
1738
+ */
1739
+ function dispatchClickEvents(el, clickCount = 1) {
1740
+ const { x, y } = getClickPoint(el);
1741
+ const base = {
1742
+ bubbles: true,
1743
+ cancelable: true,
1744
+ view: window,
1745
+ clientX: x,
1746
+ clientY: y,
1747
+ button: 0
1748
+ };
1749
+ el.dispatchEvent(new PointerEvent("pointermove", {
1750
+ ...base,
1751
+ pointerId: 1
1752
+ }));
1753
+ el.dispatchEvent(new MouseEvent("mousemove", base));
1754
+ for (let cc = 1; cc <= clickCount; cc++) {
1755
+ el.dispatchEvent(new PointerEvent("pointerdown", {
1756
+ ...base,
1757
+ detail: cc,
1758
+ buttons: 1,
1759
+ pointerId: 1
1760
+ }));
1761
+ el.dispatchEvent(new MouseEvent("mousedown", {
1762
+ ...base,
1763
+ detail: cc,
1764
+ buttons: 1
1765
+ }));
1766
+ if (cc === 1 && el !== document.activeElement) el.focus({ preventScroll: true });
1767
+ el.dispatchEvent(new PointerEvent("pointerup", {
1768
+ ...base,
1769
+ detail: cc,
1770
+ pointerId: 1
1771
+ }));
1772
+ el.dispatchEvent(new MouseEvent("mouseup", {
1773
+ ...base,
1774
+ detail: cc
1775
+ }));
1776
+ el.dispatchEvent(new MouseEvent("click", {
1777
+ ...base,
1778
+ detail: cc
1779
+ }));
1780
+ }
1781
+ }
1782
+ /** hover 事件链 */
1783
+ function dispatchHoverEvents(el) {
1784
+ const { x, y } = getClickPoint(el);
1785
+ const base = {
1786
+ bubbles: true,
1787
+ cancelable: true,
1788
+ view: window,
1789
+ clientX: x,
1790
+ clientY: y
1791
+ };
1792
+ el.dispatchEvent(new PointerEvent("pointerenter", {
1793
+ ...base,
1794
+ bubbles: false
1795
+ }));
1796
+ el.dispatchEvent(new MouseEvent("mouseenter", {
1797
+ ...base,
1798
+ bubbles: false
1799
+ }));
1800
+ el.dispatchEvent(new PointerEvent("pointermove", {
1801
+ ...base,
1802
+ pointerId: 1
1803
+ }));
1804
+ el.dispatchEvent(new MouseEvent("mousemove", base));
1805
+ el.dispatchEvent(new MouseEvent("mouseover", base));
1806
+ }
1807
+ /** 派发 input + change 事件(兼容 React/Vue 受控组件) */
1808
+ function dispatchInputEvents(el) {
1809
+ el.dispatchEvent(new Event("input", {
1810
+ bubbles: true,
1811
+ composed: true
1812
+ }));
1813
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1814
+ }
1815
+ /** 原生 setter 写入表单值(绕过 React/Vue getter/setter 拦截) */
1816
+ function setNativeValue(el, value) {
1817
+ const proto = el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
1818
+ const desc = Object.getOwnPropertyDescriptor(proto, "value");
1819
+ if (desc?.set) desc.set.call(el, value);
1820
+ else el.value = value;
1821
+ }
1822
+ function selectText(el) {
1823
+ if (el instanceof HTMLInputElement) {
1824
+ el.select();
1825
+ el.focus();
1826
+ return;
1827
+ }
1828
+ if (el instanceof HTMLTextAreaElement) {
1829
+ el.selectionStart = 0;
1830
+ el.selectionEnd = el.value.length;
1831
+ el.focus();
1832
+ return;
1833
+ }
1834
+ const range = document.createRange();
1835
+ range.selectNodeContents(el);
1836
+ const sel = window.getSelection();
1837
+ if (sel) {
1838
+ sel.removeAllRanges();
1839
+ sel.addRange(range);
1840
+ }
1841
+ if (el instanceof HTMLElement) el.focus();
1842
+ }
1843
+ function splitKeyCombo(key) {
1844
+ const tokens = key.split("+");
1845
+ for (let i = 0; i < tokens.length; i++) if (tokens[i] === "" && i + 1 < tokens.length) {
1846
+ tokens[i + 1] = "+" + tokens[i + 1];
1847
+ tokens.splice(i, 1);
1848
+ }
1849
+ return tokens.filter(Boolean);
1850
+ }
1851
+ function resolveKeyCode(key) {
1852
+ return KEY_CODE_MAP[key] ?? (key.length === 1 ? `Key${key.toUpperCase()}` : key);
1853
+ }
1854
+ /**
1855
+ * 执行 press:修饰键按正序 down → 主键 down/up → 修饰键逆序 up(参考 Playwright)。
1856
+ * 修饰键按下时抑制文本输入(只发 keydown/keyup,不发 keypress)。
1857
+ */
1858
+ function executePress(el, key) {
1859
+ const tokens = splitKeyCombo(key);
1860
+ const mainKey = tokens[tokens.length - 1];
1861
+ const mods = tokens.slice(0, -1);
1862
+ const modState = {
1863
+ ctrlKey: mods.includes("Control"),
1864
+ shiftKey: mods.includes("Shift"),
1865
+ altKey: mods.includes("Alt"),
1866
+ metaKey: mods.includes("Meta")
1867
+ };
1868
+ const hasNonShiftMod = modState.ctrlKey || modState.altKey || modState.metaKey;
1869
+ for (const m of mods) el.dispatchEvent(new KeyboardEvent("keydown", {
1870
+ key: m,
1871
+ code: resolveKeyCode(m),
1872
+ bubbles: true,
1873
+ cancelable: true,
1874
+ ...modState
1875
+ }));
1876
+ if (el.dispatchEvent(new KeyboardEvent("keydown", {
1877
+ key: mainKey,
1878
+ code: resolveKeyCode(mainKey),
1879
+ bubbles: true,
1880
+ cancelable: true,
1881
+ ...modState
1882
+ })) && mainKey.length === 1 && !hasNonShiftMod) el.dispatchEvent(new KeyboardEvent("keypress", {
1883
+ key: mainKey,
1884
+ code: resolveKeyCode(mainKey),
1885
+ bubbles: true,
1886
+ cancelable: true,
1887
+ ...modState
1888
+ }));
1889
+ el.dispatchEvent(new KeyboardEvent("keyup", {
1890
+ key: mainKey,
1891
+ code: resolveKeyCode(mainKey),
1892
+ bubbles: true,
1893
+ cancelable: true,
1894
+ ...modState
1895
+ }));
1896
+ for (let i = mods.length - 1; i >= 0; i--) el.dispatchEvent(new KeyboardEvent("keyup", {
1897
+ key: mods[i],
1898
+ code: resolveKeyCode(mods[i]),
1899
+ bubbles: true,
1900
+ cancelable: true,
1901
+ ...modState
1902
+ }));
1903
+ }
1904
+ function describeElement(el) {
1905
+ const tag = el.tagName.toLowerCase();
1906
+ const id = el.id ? `#${el.id}` : "";
1907
+ const cls = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean).slice(0, 3).map((c) => `.${c}`).join("") : "";
1908
+ const text = el instanceof HTMLSelectElement ? el.selectedOptions[0]?.textContent?.trim().slice(0, 40) ?? "" : el.textContent?.trim().slice(0, 40) ?? "";
1909
+ const textHint = text ? ` "${text}"` : "";
1910
+ const hints = [];
1911
+ for (const attr of [
1912
+ "type",
1913
+ "name",
1914
+ "placeholder",
1915
+ "href",
1916
+ "role"
1917
+ ]) {
1918
+ const v = el.getAttribute(attr);
1919
+ if (v) hints.push(`${attr}=${v}`);
1920
+ }
1921
+ if (el instanceof HTMLSelectElement && el.value) hints.push(`val=${el.value}`);
1922
+ return `<${tag}${id}${cls}>${textHint}${hints.length > 0 ? ` [${hints.join(", ")}]` : ""}`;
1923
+ }
1924
+ function getChecked(el) {
1925
+ if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) return el.checked;
1926
+ const role = el.getAttribute("role");
1927
+ if (role === "checkbox" || role === "radio" || role === "switch") return el.getAttribute("aria-checked") === "true";
1928
+ return "error";
1929
+ }
1930
+ /**
1931
+ * 归一化 check/uncheck 目标:允许命中文本容器/label/div,回溯到关联 checkbox/radio。
1932
+ */
1933
+ function resolveCheckableTarget(el) {
1934
+ if (getChecked(el) !== "error") return el;
1935
+ if (el instanceof HTMLLabelElement && el.control && getChecked(el.control) !== "error") return el.control;
1936
+ const ownerLabel = el.closest("label");
1937
+ if (ownerLabel?.control && getChecked(ownerLabel.control) !== "error") return ownerLabel.control;
1938
+ const inner = el.querySelector("input[type=\"checkbox\"], input[type=\"radio\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"]");
1939
+ if (inner && getChecked(inner) !== "error") return inner;
1940
+ const prev = el.previousElementSibling;
1941
+ if (prev && getChecked(prev) !== "error") return prev;
1942
+ const next = el.nextElementSibling;
1943
+ if (next && getChecked(next) !== "error") return next;
1944
+ const parent = el.parentElement;
1945
+ if (parent) {
1946
+ const inP = parent.querySelector("input[type=\"checkbox\"], input[type=\"radio\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"]");
1947
+ if (inP && getChecked(inP) !== "error") return inP;
1948
+ }
1949
+ return el;
1950
+ }
1951
+ /**
1952
+ * 为 pointer 类动作(click/check/uncheck)解析可点击代理目标:
1953
+ * 当命中隐藏的原生 checkbox/radio/switch input 时,优先改点其可见 label/容器。
1954
+ */
1955
+ function resolvePointerActionTarget(el) {
1956
+ if (!(el instanceof HTMLInputElement)) return el;
1957
+ const inputType = el.type?.toLowerCase() ?? "";
1958
+ if (!(inputType === "checkbox" || inputType === "radio") && el.getAttribute("role") !== "switch") return el;
1959
+ if (isElementVisible(el)) return el;
1960
+ const label = el.labels?.[0] ?? el.closest("label");
1961
+ if (label && isElementVisible(label)) return label;
1962
+ const proxy = el.closest(".el-switch, .el-checkbox, .el-radio, [role='switch'], [role='checkbox'], [role='radio']");
1963
+ if (proxy && isElementVisible(proxy)) return proxy;
1964
+ const siblingProxy = el.parentElement?.querySelector(".el-switch__core, .el-checkbox__inner, .el-radio__inner, [role='switch'], [role='checkbox'], [role='radio']");
1965
+ if (siblingProxy && isElementVisible(siblingProxy)) return siblingProxy;
1966
+ return el;
1967
+ }
1968
+ /**
1969
+ * 当命中表单项说明 label(如 Element Plus el-form-item__label)时,
1970
+ * 自动重定向到同一表单项中的首个可交互控件。
1971
+ */
1972
+ function resolveFormItemControlTarget(el) {
1973
+ if (!(el instanceof HTMLElement)) return el;
1974
+ if (!(el.tagName === "LABEL" || el.classList.contains("el-form-item__label"))) return el;
1975
+ const htmlLabel = el;
1976
+ if (htmlLabel.control && isElementVisible(htmlLabel.control)) return htmlLabel.control;
1977
+ const formItem = el.closest(".el-form-item");
1978
+ if (!formItem) return el;
1979
+ const control = (formItem.querySelector(".el-form-item__content") ?? formItem).querySelector("input:not([type='hidden']), textarea, select, button, [role='switch'], [role='checkbox'], [role='radio'], [role='button'], .el-switch, .el-checkbox, .el-radio, [tabindex]:not([tabindex='-1'])");
1980
+ if (control && isElementVisible(control)) return control;
1981
+ return el;
1662
1982
  }
1663
1983
  function findVisibleOptionByText(text) {
1664
1984
  const target = text.trim().toLowerCase();
1665
1985
  if (!target) return null;
1666
- const nodes = Array.from(document.querySelectorAll("[role=\"option\"], .bk-select-option, .bk-option, [data-option], li, option"));
1667
- for (const node of nodes) {
1668
- if (!isOptionCandidateVisible(node)) continue;
1669
- if ((node.textContent?.trim().toLowerCase() ?? "") === target) return node;
1670
- }
1671
- for (const node of nodes) {
1672
- if (!isOptionCandidateVisible(node)) continue;
1673
- if ((node.textContent?.trim().toLowerCase() ?? "").includes(target)) return node;
1674
- }
1986
+ const selectors = [
1987
+ "[role=\"option\"]",
1988
+ "[role=\"listbox\"] li",
1989
+ ".el-select-dropdown__item",
1990
+ ".el-option",
1991
+ ".ant-select-item-option",
1992
+ ".el-cascader-node",
1993
+ ".el-dropdown-menu__item",
1994
+ "[class*=\"option\"]",
1995
+ "li[data-value]",
1996
+ "option"
1997
+ ].join(", ");
1998
+ const visible = Array.from(document.querySelectorAll(selectors)).filter((n) => n instanceof HTMLElement && isElementVisible(n));
1999
+ for (const n of visible) if (n.textContent?.trim().toLowerCase() === target) return n;
2000
+ for (const n of visible) if (n.textContent?.trim().toLowerCase().includes(target)) return n;
1675
2001
  return null;
1676
2002
  }
2003
+ async function waitForDropdownPopup(maxWait = 500) {
2004
+ const start = Date.now();
2005
+ while (Date.now() - start < maxWait) {
2006
+ const popup = document.querySelector("[role=\"listbox\"], .el-select-dropdown, .el-popper, .ant-select-dropdown, [class*=\"dropdown\"]");
2007
+ if (popup && isElementVisible(popup)) return;
2008
+ await sleep(50);
2009
+ }
2010
+ }
1677
2011
  function createDomTool() {
1678
2012
  return {
1679
2013
  name: "dom",
1680
2014
  description: [
1681
2015
  "Perform DOM operations on the current page.",
1682
2016
  "Actions: click, fill, select_option, clear, check, uncheck, type, focus, hover, press, get_text, get_attr, set_attr, add_class, remove_class.",
1683
- "Use the hash ID from DOM snapshot (e.g. #a1b2c) as selector."
2017
+ "Input/Select rule: before each fill/type/select_option, click or focus the same target immediately in the same round.",
2018
+ "For multiple fields, use alternating pairs in one batch: focus/click A -> fill/type A -> focus/click B -> fill/type B.",
2019
+ "Use the hash ID from DOM snapshot (e.g. #a1b2c) as selector.",
2020
+ "press supports combo keys like 'Control+a', 'Shift+Enter'.",
2021
+ "check/uncheck is done via click — state change is verified after action.",
2022
+ "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.",
2023
+ "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.",
2024
+ "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.",
2025
+ "fill supports role=slider elements: use fill with a numeric value on a role=slider container (rating/slider) to set its value programmatically."
1684
2026
  ].join(" "),
1685
2027
  schema: Type.Object({
1686
- action: Type.String({ description: "DOM action: click | fill | select_option | clear | check | uncheck | type | focus | hover | press | get_text | get_attr | set_attr | add_class | remove_class" }),
2028
+ action: Type.String({ description: "DOM action: click | fill | select_option | clear | check | uncheck | type | focus | hover | press | get_text | get_attr | set_attr | add_class | remove_class." }),
1687
2029
  selector: Type.String({ description: "Element ref ID from snapshot (e.g. #r0, #r5) or CSS selector" }),
1688
- value: Type.Optional(Type.String({ description: "Value for fill/type/set_attr actions" })),
1689
- key: Type.Optional(Type.String({ description: "Key name for press action (e.g. Enter, Escape, Tab, ArrowDown, ArrowUp, Backspace, Delete, Space)" })),
1690
- label: Type.Optional(Type.String({ description: "Label text for select_option action (fallback when value is not provided)" })),
2030
+ value: Type.Optional(Type.String({ description: "Value for fill/type/set_attr actions." })),
2031
+ key: Type.Optional(Type.String({ description: "Key for press action. Supports combo: 'Enter', 'Control+a', 'Shift+Enter', 'Meta+c'" })),
2032
+ label: Type.Optional(Type.String({ description: "Label text for select_option action." })),
1691
2033
  index: Type.Optional(Type.Number({ description: "0-based option index for select_option action" })),
1692
- attribute: Type.Optional(Type.String({ description: "Attribute name for get_attr/set_attr actions" })),
2034
+ attribute: Type.Optional(Type.String({ description: "Attribute name for get_attr/set_attr" })),
1693
2035
  className: Type.Optional(Type.String({ description: "CSS class name for add_class/remove_class" })),
1694
- waitMs: Type.Optional(Type.Number({ description: "Optional wait timeout in ms before action (default: 1000). Use 0 to disable waiting." })),
1695
- waitSeconds: Type.Optional(Type.Number({ description: "Optional wait timeout in seconds before action. Used when waitMs is not provided." })),
1696
- force: Type.Optional(Type.Boolean({ description: "Skip actionability checks for interaction actions (default false)." }))
2036
+ clickCount: Type.Optional(Type.Number({ description: "Click count (default 1). 2 = double-click, 3 = triple-click." })),
2037
+ waitMs: Type.Optional(Type.Number({ description: "Wait timeout in ms before action (default: 2000)." })),
2038
+ waitSeconds: Type.Optional(Type.Number({ description: "Wait timeout in seconds (fallback for waitMs)." })),
2039
+ force: Type.Optional(Type.Boolean({ description: "Skip actionability checks (default false)." }))
1697
2040
  }),
1698
2041
  execute: async (params) => {
1699
2042
  const action = params.action;
@@ -1725,180 +2068,149 @@ function createDomTool() {
1725
2068
  };
1726
2069
  el = found;
1727
2070
  } else {
1728
- const elOrError = queryElement(selector);
1729
- if (typeof elOrError === "string") return {
1730
- content: elOrError,
2071
+ const r = queryElement(selector);
2072
+ if (typeof r === "string") return {
2073
+ content: r,
1731
2074
  details: {
1732
2075
  error: true,
1733
- code: elOrError.startsWith("未找到") ? "ELEMENT_NOT_FOUND" : "INVALID_SELECTOR",
2076
+ code: r.startsWith("未找到") ? "ELEMENT_NOT_FOUND" : "INVALID_SELECTOR",
1734
2077
  action,
1735
2078
  selector,
1736
2079
  waitMs
1737
2080
  }
1738
2081
  };
1739
- el = elOrError;
2082
+ el = r;
1740
2083
  }
2084
+ if (action === "check" || action === "uncheck") el = resolveCheckableTarget(el);
2085
+ const actionabilityTarget = action === "click" || action === "check" || action === "uncheck" ? resolvePointerActionTarget(resolveFormItemControlTarget(el)) : el;
1741
2086
  try {
1742
- if (!force) {
1743
- const checkResult = ensureActionable(el, action, selector);
1744
- if (checkResult) return checkResult;
1745
- }
2087
+ const checkResult = ensureActionable(actionabilityTarget, action, selector, force);
2088
+ if (checkResult) return checkResult;
1746
2089
  switch (action) {
1747
- case "click":
1748
- if (el instanceof HTMLOptionElement) {
1749
- const parent = el.parentElement;
2090
+ case "click": {
2091
+ const target = resolvePointerActionTarget(resolveFormItemControlTarget(retarget(el, force ? "none" : "button-link")));
2092
+ const clickCount = typeof params.clickCount === "number" ? params.clickCount : 1;
2093
+ if (target instanceof HTMLOptionElement) {
2094
+ const parent = target.parentElement;
1750
2095
  if (parent instanceof HTMLSelectElement) {
1751
2096
  parent.focus();
1752
- parent.value = el.value;
2097
+ parent.value = target.value;
1753
2098
  dispatchInputEvents(parent);
1754
- return { content: `已选择 ${describeElement(parent)} 的选项 "${el.value}"` };
2099
+ return { content: `已选择 ${describeElement(parent)} 的选项 "${target.value}"` };
1755
2100
  }
1756
2101
  }
1757
- if (el instanceof HTMLElement) {
1758
- el.focus();
1759
- el.dispatchEvent(new PointerEvent("pointerdown", {
1760
- bubbles: true,
1761
- cancelable: true
1762
- }));
1763
- el.dispatchEvent(new MouseEvent("mousedown", {
1764
- bubbles: true,
1765
- cancelable: true
1766
- }));
1767
- el.dispatchEvent(new PointerEvent("pointerup", {
1768
- bubbles: true,
1769
- cancelable: true
1770
- }));
1771
- el.dispatchEvent(new MouseEvent("mouseup", {
1772
- bubbles: true,
1773
- cancelable: true
1774
- }));
1775
- el.click();
1776
- } else el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
1777
- return { content: `已点击 ${describeElement(el)}` };
1778
- case "focus":
1779
- if (el instanceof HTMLElement) el.focus();
1780
- else el.dispatchEvent(new FocusEvent("focus", { bubbles: true }));
1781
- return { content: `已聚焦 ${describeElement(el)}` };
1782
- case "hover":
1783
- el.dispatchEvent(new MouseEvent("mouseenter", {
1784
- bubbles: false,
1785
- cancelable: true
1786
- }));
1787
- el.dispatchEvent(new MouseEvent("mouseover", {
1788
- bubbles: true,
1789
- cancelable: true
1790
- }));
1791
- el.dispatchEvent(new MouseEvent("mousemove", {
1792
- bubbles: true,
1793
- cancelable: true
1794
- }));
1795
- return { content: `已悬停 ${describeElement(el)}` };
1796
- case "press": {
1797
- const key = params.key || params.value;
1798
- if (!key) return { content: "缺少 key 参数(如 Enter, Escape, Tab)" };
1799
- if (el instanceof HTMLElement) el.focus();
1800
- const eventInit = {
1801
- key,
1802
- code: resolveKeyboardCode(key),
1803
- bubbles: true,
1804
- cancelable: true
1805
- };
1806
- const keydownAllowed = el.dispatchEvent(new KeyboardEvent("keydown", eventInit));
1807
- el.dispatchEvent(new KeyboardEvent("keypress", eventInit));
1808
- el.dispatchEvent(new KeyboardEvent("keyup", eventInit));
1809
- if (keydownAllowed && key === "Enter") {
1810
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) (el.form ?? el.closest("form"))?.dispatchEvent(new Event("submit", {
1811
- bubbles: true,
1812
- cancelable: true
1813
- }));
1814
- }
1815
- return { content: `已在 ${describeElement(el)} 上按下 ${key}` };
2102
+ if (target instanceof HTMLElement) {
2103
+ scrollIntoViewIfNeeded(target);
2104
+ if (!force) await checkElementStable(target, 500);
2105
+ if (!force) {
2106
+ if (checkHitTarget(target)) {
2107
+ scrollIntoViewIfNeeded(target, 1);
2108
+ await sleep(100);
2109
+ }
2110
+ }
2111
+ dispatchClickEvents(target, clickCount);
2112
+ } else target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
2113
+ return { content: `已点击 ${describeElement(target)}` };
1816
2114
  }
1817
2115
  case "fill": {
1818
2116
  const value = params.value;
1819
2117
  if (value === void 0) return { content: "缺少 value 参数" };
1820
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1821
- if (el instanceof HTMLInputElement) {
1822
- if (new Set([
1823
- "checkbox",
1824
- "radio",
1825
- "file",
1826
- "button",
1827
- "submit",
1828
- "reset"
1829
- ]).has(el.type)) return {
1830
- content: `"${selector}" 为 input[type=${el.type}],不支持 fill;请使用 click/press/select_option 等动作。`,
2118
+ const target = retarget(el, "follow-label");
2119
+ if (target instanceof HTMLInputElement) {
2120
+ const type = target.type.toLowerCase();
2121
+ if (INPUT_BLOCKED_TYPES.has(type)) return {
2122
+ content: `"${selector}" 为 input[type=${type}],不支持 fill;请使用 click/check 等动作。`,
2123
+ details: {
2124
+ error: true,
2125
+ code: "UNSUPPORTED_FILL_TARGET",
2126
+ action,
2127
+ selector
2128
+ }
2129
+ };
2130
+ if (INPUT_SET_VALUE_TYPES.has(type)) {
2131
+ const finalVal = type === "color" ? value.toLowerCase().trim() : value.trim();
2132
+ target.focus();
2133
+ target.value = finalVal;
2134
+ if (target.value !== finalVal) return {
2135
+ content: `"${selector}" 填写格式不匹配(type=${type})`,
1831
2136
  details: {
1832
2137
  error: true,
1833
- code: "UNSUPPORTED_FILL_TARGET",
2138
+ code: "MALFORMED_VALUE",
1834
2139
  action,
1835
2140
  selector
1836
2141
  }
1837
2142
  };
2143
+ dispatchInputEvents(target);
2144
+ return { content: `已填写 ${describeElement(target)}: "${finalVal}"` };
1838
2145
  }
1839
- el.focus();
1840
- setNativeEditableValue(el, value);
1841
- dispatchInputEvents(el);
1842
- const actualValue = getEditableValue(el);
1843
- if (actualValue !== value) return {
1844
- content: `"${selector}" 填写后值不一致:期望 "${value}",实际 "${actualValue}"`,
2146
+ if (type === "number" && isNaN(Number(value.trim()))) return {
2147
+ content: `"${selector}" 为 input[type=number],无法填写非数字 "${value}"`,
1845
2148
  details: {
1846
2149
  error: true,
1847
- code: "FILL_NOT_APPLIED",
2150
+ code: "INVALID_NUMBER",
1848
2151
  action,
1849
- selector,
1850
- expected: value,
1851
- actual: actualValue
2152
+ selector
1852
2153
  }
1853
2154
  };
1854
- } else if (el instanceof HTMLSelectElement) {
1855
- el.focus();
1856
- let matched = false;
1857
- for (const option of Array.from(el.options)) if (option.value === value) {
1858
- el.value = option.value;
1859
- matched = true;
1860
- break;
1861
- }
1862
- if (!matched) {
1863
- const normalized = value.trim().toLowerCase();
1864
- for (const option of Array.from(el.options)) if (option.text.trim().toLowerCase() === normalized) {
1865
- el.value = option.value;
1866
- matched = true;
1867
- break;
1868
- }
1869
- }
1870
- if (!matched) return { content: `"${selector}" 下拉框中不存在选项 "${value}"` };
1871
- dispatchInputEvents(el);
1872
- const actualValue = getEditableValue(el);
1873
- if (actualValue !== el.value) return {
1874
- content: `"${selector}" 下拉框状态异常,未确认写入`,
2155
+ scrollIntoViewIfNeeded(target);
2156
+ target.focus();
2157
+ selectText(target);
2158
+ setNativeValue(target, value);
2159
+ dispatchInputEvents(target);
2160
+ if (target.value !== value) return {
2161
+ content: `"${selector}" 填写后值不一致:期望 "${value}",实际 "${target.value}"`,
1875
2162
  details: {
1876
2163
  error: true,
1877
2164
  code: "FILL_NOT_APPLIED",
1878
2165
  action,
1879
- selector,
1880
- expected: value,
1881
- actual: actualValue
2166
+ selector
1882
2167
  }
1883
2168
  };
1884
- } else if (el instanceof HTMLElement && el.isContentEditable) {
1885
- el.focus();
1886
- el.textContent = value;
1887
- el.dispatchEvent(new Event("input", { bubbles: true }));
1888
- } else return { content: `"${selector}" 不是可编辑元素` };
1889
- return { content: `已填写 ${describeElement(el)}: "${value}"` };
2169
+ return { content: `已填写 ${describeElement(target)}: "${value}"` };
2170
+ }
2171
+ if (target instanceof HTMLTextAreaElement) {
2172
+ scrollIntoViewIfNeeded(target);
2173
+ target.focus();
2174
+ selectText(target);
2175
+ setNativeValue(target, value);
2176
+ dispatchInputEvents(target);
2177
+ return { content: `已填写 ${describeElement(target)}: "${value}"` };
2178
+ }
2179
+ if (target instanceof HTMLSelectElement) {
2180
+ target.focus();
2181
+ const options = Array.from(target.options);
2182
+ let matched = options.find((o) => o.value === value);
2183
+ if (!matched) {
2184
+ const n = value.trim().toLowerCase();
2185
+ matched = options.find((o) => o.text.trim().toLowerCase() === n);
2186
+ }
2187
+ if (!matched) return { content: `"${selector}" 下拉框中不存在选项 "${value}"` };
2188
+ target.value = matched.value;
2189
+ dispatchInputEvents(target);
2190
+ return { content: `已填写 ${describeElement(target)}: "${value}"` };
2191
+ }
2192
+ if (target instanceof HTMLElement && target.isContentEditable) {
2193
+ target.focus();
2194
+ selectText(target);
2195
+ if (value) document.execCommand("insertText", false, value);
2196
+ else document.execCommand("delete", false, void 0);
2197
+ return { content: `已填写 ${describeElement(target)}: "${value}"` };
2198
+ }
2199
+ return { content: `"${selector}" 不是可编辑元素` };
1890
2200
  }
1891
2201
  case "select_option": {
1892
2202
  const value = params.value;
1893
2203
  const label = params.label;
1894
2204
  const index = typeof params.index === "number" ? Math.floor(params.index) : void 0;
1895
2205
  if (value === void 0 && label === void 0 && index === void 0) return { content: "缺少可选参数:value 或 label 或 index" };
1896
- if (!(el instanceof HTMLSelectElement)) {
1897
- if (!(el instanceof HTMLElement)) return { content: `"${selector}" 不是下拉框元素` };
1898
- el.focus();
1899
- el.click();
2206
+ const target = retarget(el, "follow-label");
2207
+ if (!(target instanceof HTMLSelectElement)) {
2208
+ if (!(target instanceof HTMLElement)) return { content: `"${selector}" 不是下拉框元素` };
2209
+ scrollIntoViewIfNeeded(target);
1900
2210
  const wanted = (label ?? value ?? "").trim();
1901
2211
  if (!wanted) return { content: `"${selector}" 为自定义下拉时,需提供 value 或 label` };
2212
+ dispatchClickEvents(target);
2213
+ await waitForDropdownPopup(800);
1902
2214
  const option = findVisibleOptionByText(wanted);
1903
2215
  if (!option) return {
1904
2216
  content: `未找到与 "${wanted}" 匹配的可见下拉选项(自定义下拉)`,
@@ -1910,84 +2222,156 @@ function createDomTool() {
1910
2222
  wanted
1911
2223
  }
1912
2224
  };
1913
- option.click();
2225
+ dispatchClickEvents(option);
1914
2226
  return { content: `已在自定义下拉中选择 "${wanted}"` };
1915
2227
  }
1916
- el.focus();
1917
- const options = Array.from(el.options);
1918
- let selectedOption;
1919
- if (value !== void 0) selectedOption = options.find((option) => option.value === value);
1920
- if (!selectedOption && label !== void 0) {
1921
- const normalizedLabel = label.trim().toLowerCase();
1922
- selectedOption = options.find((option) => option.text.trim().toLowerCase() === normalizedLabel);
2228
+ target.focus();
2229
+ const options = Array.from(target.options);
2230
+ let selected;
2231
+ if (value !== void 0) selected = options.find((o) => o.value === value);
2232
+ if (!selected && label !== void 0) {
2233
+ const nl = label.trim().toLowerCase();
2234
+ selected = options.find((o) => o.text.trim().toLowerCase() === nl);
1923
2235
  }
1924
- if (!selectedOption && value !== void 0) {
1925
- const normalizedValueAsLabel = value.trim().toLowerCase();
1926
- selectedOption = options.find((option) => option.text.trim().toLowerCase() === normalizedValueAsLabel);
2236
+ if (!selected && value !== void 0) {
2237
+ const nv = value.trim().toLowerCase();
2238
+ selected = options.find((o) => o.text.trim().toLowerCase() === nv);
1927
2239
  }
1928
- if (!selectedOption && index !== void 0) {
2240
+ if (!selected && index !== void 0) {
1929
2241
  if (index < 0 || index >= options.length) return { content: `"${selector}" 下拉框不存在 index=${index} 的选项` };
1930
- selectedOption = options[index];
2242
+ selected = options[index];
1931
2243
  }
1932
- if (!selectedOption) return { content: `"${selector}" 下拉框中不存在选项 "${value ?? label ?? `index=${index}`}"` };
1933
- if (selectedOption.disabled) return { content: `"${selector}" 目标选项已禁用:${selectedOption.value}` };
1934
- if (!el.multiple) for (const option of options) option.selected = false;
1935
- selectedOption.selected = true;
1936
- el.value = selectedOption.value;
1937
- dispatchInputEvents(el);
1938
- return { content: `已选择 ${describeElement(el)}: value="${selectedOption.value}", label="${selectedOption.text.trim()}"` };
2244
+ if (!selected) return { content: `"${selector}" 下拉框中不存在选项 "${value ?? label ?? `index=${index}`}"` };
2245
+ if (selected.disabled) return {
2246
+ content: `"${selector}" 目标选项已禁用:${selected.value}`,
2247
+ details: {
2248
+ error: true,
2249
+ code: "OPTION_DISABLED",
2250
+ action,
2251
+ selector
2252
+ }
2253
+ };
2254
+ if (!target.multiple) for (const o of options) o.selected = false;
2255
+ selected.selected = true;
2256
+ target.value = selected.value;
2257
+ dispatchInputEvents(target);
2258
+ return { content: `已选择 ${describeElement(target)}: value="${selected.value}", label="${selected.text.trim()}"` };
1939
2259
  }
1940
- case "clear":
1941
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1942
- el.focus();
1943
- setNativeEditableValue(el, "");
1944
- dispatchInputEvents(el);
1945
- return { content: `已清空 ${describeElement(el)}` };
2260
+ case "clear": {
2261
+ const target = retarget(el, "follow-label");
2262
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
2263
+ scrollIntoViewIfNeeded(target);
2264
+ target.focus();
2265
+ selectText(target);
2266
+ setNativeValue(target, "");
2267
+ dispatchInputEvents(target);
2268
+ return { content: `已清空 ${describeElement(target)}` };
2269
+ }
2270
+ if (target instanceof HTMLSelectElement) {
2271
+ target.focus();
2272
+ target.value = "";
2273
+ dispatchInputEvents(target);
2274
+ return { content: `已清空 ${describeElement(target)}` };
1946
2275
  }
1947
- if (el instanceof HTMLElement && el.isContentEditable) {
1948
- el.focus();
1949
- el.textContent = "";
1950
- el.dispatchEvent(new Event("input", { bubbles: true }));
1951
- return { content: `已清空 ${describeElement(el)}` };
2276
+ if (target instanceof HTMLElement && target.isContentEditable) {
2277
+ target.focus();
2278
+ selectText(target);
2279
+ document.execCommand("delete", false, void 0);
2280
+ return { content: `已清空 ${describeElement(target)}` };
1952
2281
  }
1953
2282
  return { content: `"${selector}" 不是可清空元素` };
2283
+ }
1954
2284
  case "check":
1955
- if (!(el instanceof HTMLInputElement) || el.type !== "checkbox" && el.type !== "radio") return { content: `"${selector}" 不是 checkbox/radio` };
1956
- el.focus();
1957
- if (!el.checked) {
1958
- el.checked = true;
1959
- dispatchInputEvents(el);
1960
- }
1961
- return { content: `已勾选 ${describeElement(el)}` };
1962
- case "uncheck":
1963
- if (!(el instanceof HTMLInputElement) || el.type !== "checkbox") return { content: `"${selector}" 不是 checkbox` };
1964
- el.focus();
1965
- if (el.checked) {
1966
- el.checked = false;
2285
+ case "uncheck": {
2286
+ const wantChecked = action === "check";
2287
+ const current = getChecked(el);
2288
+ if (current === "error") return {
2289
+ content: `"${selector}" 不是 checkbox/radio/[role=checkbox]/[role=radio],无法 ${action}`,
2290
+ details: {
2291
+ error: true,
2292
+ code: "NOT_CHECKABLE",
2293
+ action,
2294
+ selector
2295
+ }
2296
+ };
2297
+ if (current === wantChecked) return { content: `${describeElement(el)} 已经是${wantChecked ? "选中" : "未选中"}状态` };
2298
+ if (!wantChecked && el instanceof HTMLInputElement && el.type === "radio") return {
2299
+ content: `无法取消 radio 按钮的选中状态`,
2300
+ details: {
2301
+ error: true,
2302
+ code: "CANNOT_UNCHECK_RADIO",
2303
+ action,
2304
+ selector
2305
+ }
2306
+ };
2307
+ const pointerTarget = resolvePointerActionTarget(el);
2308
+ scrollIntoViewIfNeeded(pointerTarget);
2309
+ if (pointerTarget instanceof HTMLElement) dispatchClickEvents(pointerTarget);
2310
+ else pointerTarget.dispatchEvent(new MouseEvent("click", { bubbles: true }));
2311
+ await sleep(50);
2312
+ if (getChecked(el) !== wantChecked && el instanceof HTMLInputElement) {
2313
+ el.checked = wantChecked;
1967
2314
  dispatchInputEvents(el);
1968
2315
  }
1969
- return { content: `已取消勾选 ${describeElement(el)}` };
2316
+ return { content: `已${wantChecked ? "勾选" : "取消勾选"} ${describeElement(el)}` };
2317
+ }
1970
2318
  case "type": {
1971
2319
  const value = params.value;
1972
2320
  if (value === void 0) return { content: "缺少 value 参数" };
1973
- if (el instanceof HTMLElement) el.focus();
2321
+ const target = retarget(el, "follow-label");
2322
+ scrollIntoViewIfNeeded(target);
2323
+ if (target instanceof HTMLElement) target.focus();
1974
2324
  for (const char of value) {
1975
- el.dispatchEvent(new KeyboardEvent("keydown", {
1976
- key: char,
1977
- bubbles: true
1978
- }));
1979
- el.dispatchEvent(new KeyboardEvent("keypress", {
1980
- key: char,
1981
- bubbles: true
1982
- }));
1983
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) el.value += char;
1984
- el.dispatchEvent(new Event("input", { bubbles: true }));
1985
- el.dispatchEvent(new KeyboardEvent("keyup", {
2325
+ const init = {
1986
2326
  key: char,
1987
- bubbles: true
2327
+ code: resolveKeyCode(char),
2328
+ bubbles: true,
2329
+ cancelable: true
2330
+ };
2331
+ target.dispatchEvent(new KeyboardEvent("keydown", init));
2332
+ target.dispatchEvent(new KeyboardEvent("keypress", init));
2333
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
2334
+ const proto = target instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
2335
+ const nativeSet = Object.getOwnPropertyDescriptor(proto, "value")?.set;
2336
+ if (nativeSet) nativeSet.call(target, target.value + char);
2337
+ else target.value += char;
2338
+ } else if (target instanceof HTMLElement && target.isContentEditable) document.execCommand("insertText", false, char);
2339
+ target.dispatchEvent(new Event("input", {
2340
+ bubbles: true,
2341
+ composed: true
1988
2342
  }));
2343
+ target.dispatchEvent(new KeyboardEvent("keyup", init));
1989
2344
  }
1990
- return { content: `已逐字输入到 ${describeElement(el)}: "${value}"` };
2345
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) target.dispatchEvent(new Event("change", { bubbles: true }));
2346
+ return { content: `已逐字输入到 ${describeElement(target)}: "${value}"` };
2347
+ }
2348
+ case "focus": {
2349
+ const target = retarget(el, "follow-label");
2350
+ if (target instanceof HTMLElement || target instanceof SVGElement) {
2351
+ target.focus();
2352
+ target.focus();
2353
+ }
2354
+ return { content: `已聚焦 ${describeElement(target)}` };
2355
+ }
2356
+ case "hover": {
2357
+ const target = retarget(el, "none");
2358
+ scrollIntoViewIfNeeded(target);
2359
+ if (!force) await checkElementStable(target, 500);
2360
+ if (target instanceof HTMLElement) dispatchHoverEvents(target);
2361
+ return { content: `已悬停 ${describeElement(target)}` };
2362
+ }
2363
+ case "press": {
2364
+ const key = params.key || params.value;
2365
+ if (!key) return { content: "缺少 key 参数(如 Enter, Escape, Tab, Control+a)" };
2366
+ const target = retarget(el, "none");
2367
+ scrollIntoViewIfNeeded(target);
2368
+ if (target instanceof HTMLElement) target.focus();
2369
+ executePress(target, key);
2370
+ if (splitKeyCombo(key).pop() === "Enter") (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement ? target.form ?? target.closest("form") : target.closest("form"))?.dispatchEvent(new Event("submit", {
2371
+ bubbles: true,
2372
+ cancelable: true
2373
+ }));
2374
+ return { content: `已在 ${describeElement(target)} 上按下 ${key}` };
1991
2375
  }
1992
2376
  case "get_text": {
1993
2377
  const text = el.textContent?.trim() ?? "";
@@ -1996,8 +2380,21 @@ function createDomTool() {
1996
2380
  case "get_attr": {
1997
2381
  const attribute = params.attribute;
1998
2382
  if (!attribute) return { content: "缺少 attribute 参数" };
1999
- const attrValue = el.getAttribute(attribute);
2000
- return { content: `${describeElement(el)} ${attribute} = ${attrValue ?? "(不存在)"}` };
2383
+ const attrName = attribute.toLowerCase();
2384
+ if (attrName === "checked") {
2385
+ if (el instanceof HTMLInputElement) return { content: `${describeElement(el)} 的 checked = ${String(el.checked)}` };
2386
+ return { content: `${describeElement(el)} 的 checked = ${el.getAttribute("aria-checked") ?? "(不存在)"}` };
2387
+ }
2388
+ if (attrName === "selected") {
2389
+ if (el instanceof HTMLOptionElement) return { content: `${describeElement(el)} 的 selected = ${String(el.selected)}` };
2390
+ return { content: `${describeElement(el)} 的 selected = ${el.getAttribute("aria-selected") ?? "(不存在)"}` };
2391
+ }
2392
+ if (attrName === "disabled") {
2393
+ if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) return { content: `${describeElement(el)} 的 disabled = ${String(el.disabled)}` };
2394
+ }
2395
+ if (attrName === "readonly" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return { content: `${describeElement(el)} 的 readonly = ${String(el.readOnly)}` };
2396
+ if (attrName === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) return { content: `${describeElement(el)} 的 value = ${el.value || "(空)"}` };
2397
+ return { content: `${describeElement(el)} 的 ${attribute} = ${el.getAttribute(attribute) ?? "(不存在)"}` };
2001
2398
  }
2002
2399
  case "set_attr": {
2003
2400
  const attribute = params.attribute;
@@ -2255,13 +2652,18 @@ function generateSnapshot(root = document.body, options = {}) {
2255
2652
  const orderedChildren = [...interactiveChildren, ...nonInteractiveChildren];
2256
2653
  const selectedChildren = orderedChildren.slice(0, maxChildren);
2257
2654
  const omittedChildren = orderedChildren.length - selectedChildren.length;
2258
- const childLines = [];
2655
+ const childBlocks = [];
2259
2656
  for (let i = 0; i < selectedChildren.length; i++) {
2260
2657
  const childResult = walk(selectedChildren[i], depth, currentPath);
2261
- if (childResult) childLines.push(childResult);
2658
+ if (childResult) childBlocks.push(childResult);
2262
2659
  }
2263
- if (omittedChildren > 0) childLines.push(`${" ".repeat(depth)}... (${omittedChildren} children omitted)`);
2264
- return childLines.join("\n");
2660
+ if (childBlocks.length === 0 && omittedChildren <= 0) return "";
2661
+ if (!(childBlocks.length >= 2 || omittedChildren > 0)) return childBlocks.join("\n");
2662
+ const groupLines = [`${" ".repeat(depth)}([${tag}] collapsed-group`];
2663
+ for (const block of childBlocks) groupLines.push(indentMultiline(block, 1));
2664
+ if (omittedChildren > 0) groupLines.push(`${" ".repeat(depth + 1)}... (${omittedChildren} children omitted)`);
2665
+ groupLines.push(`${" ".repeat(depth)})`);
2666
+ return groupLines.join("\n");
2265
2667
  }
2266
2668
  let line = `${indent}[${tag}]`;
2267
2669
  if (directText) line += ` "${directText.slice(0, maxTextLength)}"`;
@@ -2312,6 +2714,13 @@ function queryAllElements(selector, limit = 20) {
2312
2714
  return `选择器语法错误: ${selector}`;
2313
2715
  }
2314
2716
  }
2717
+ /**
2718
+ * 多行文本块缩进(中)/ Indent each line of a multiline block (EN).
2719
+ */
2720
+ function indentMultiline(block, indentLevel) {
2721
+ const prefix = " ".repeat(indentLevel);
2722
+ return block.split("\n").map((line) => `${prefix}${line}`).join("\n");
2723
+ }
2315
2724
  function createPageInfoTool() {
2316
2725
  return {
2317
2726
  name: "page_info",
@@ -2388,26 +2797,42 @@ function createPageInfoTool() {
2388
2797
  //#endregion
2389
2798
  //#region src/web/tools/navigate-tool.ts
2390
2799
  /**
2391
- * Navigate Tool — 基于 Web API 的页面导航工具。
2392
- *
2393
- * 替代 Playwright 的 goto/goBack/goForward/reload。
2394
- * 运行环境:浏览器 Content Script。
2800
+ * Navigate Tool — 页面导航工具(增强版)。
2395
2801
  *
2396
2802
  * 支持 5 种动作:
2397
2803
  * goto — 跳转到指定 URL
2398
2804
  * back — 浏览器后退
2399
2805
  * forward — 浏览器前进
2400
2806
  * reload — 刷新当前页面
2401
- * scroll — 滚动页面到指定位置或元素
2807
+ * scroll — 滚动页面到指定位置或元素(支持 RefStore hash ID + 多策略对齐)
2402
2808
  */
2809
+ /** 解析 selector(支持 RefStore hash ID 和 CSS 选择器) */
2810
+ function resolveElement(selector) {
2811
+ if (selector.startsWith("#")) {
2812
+ const store = getActiveRefStore();
2813
+ if (store) {
2814
+ const id = selector.slice(1);
2815
+ if (store.has(id)) return store.get(id) ?? null;
2816
+ }
2817
+ }
2818
+ try {
2819
+ return document.querySelector(selector);
2820
+ } catch {
2821
+ return null;
2822
+ }
2823
+ }
2403
2824
  function createNavigateTool() {
2404
2825
  return {
2405
2826
  name: "navigate",
2406
- description: ["Navigate the current page.", "Actions: goto (open URL), back, forward, reload, scroll (to position or element)."].join(" "),
2827
+ description: [
2828
+ "Navigate the current page.",
2829
+ "Actions: goto (open URL), back, forward, reload, scroll (to position or element).",
2830
+ "scroll supports hash ID from snapshot (e.g. #r0) or CSS selector."
2831
+ ].join(" "),
2407
2832
  schema: Type.Object({
2408
2833
  action: Type.String({ description: "Navigation action: goto | back | forward | reload | scroll" }),
2409
2834
  url: Type.Optional(Type.String({ description: "URL for goto action" })),
2410
- selector: Type.Optional(Type.String({ description: "CSS selector for scroll action (scrolls element into view)" })),
2835
+ selector: Type.Optional(Type.String({ description: "Element ref ID from snapshot (e.g. #r0) or CSS selector for scroll action" })),
2411
2836
  x: Type.Optional(Type.Number({ description: "Horizontal scroll position (pixels)" })),
2412
2837
  y: Type.Optional(Type.Number({ description: "Vertical scroll position (pixels)" }))
2413
2838
  }),
@@ -2433,9 +2858,10 @@ function createNavigateTool() {
2433
2858
  case "scroll": {
2434
2859
  const selector = params.selector;
2435
2860
  if (selector) {
2436
- const el = document.querySelector(selector);
2861
+ const el = resolveElement(selector);
2437
2862
  if (!el) return { content: `未找到元素 "${selector}"` };
2438
- el.scrollIntoView({
2863
+ if ("scrollIntoViewIfNeeded" in el) el.scrollIntoViewIfNeeded(true);
2864
+ else el.scrollIntoView({
2439
2865
  behavior: "smooth",
2440
2866
  block: "center"
2441
2867
  });
@@ -2468,36 +2894,88 @@ function createNavigateTool() {
2468
2894
  //#endregion
2469
2895
  //#region src/web/tools/wait-tool.ts
2470
2896
  /**
2471
- * Wait Tool 基于 MutationObserver 的元素等待工具。
2897
+ * Wait Tool 等待工具 / Wait utility for DOM conditions.
2472
2898
  *
2473
- * 替代 Playwright waitForSelector/waitForNavigation。
2474
- * 运行环境:浏览器 Content Script。
2899
+ * 支持动作 / Supported actions:
2900
+ * - wait_for_selector: 等待选择器达到状态 / wait selector state
2901
+ * - wait_for_hidden: 等待元素隐藏或移除 / wait element hidden or detached
2902
+ * - wait_for_text: 等待页面出现文本 / wait text appears in page
2903
+ * - wait_for_stable: 等待 DOM 进入静默窗口 / wait DOM quiet window
2475
2904
  *
2476
- * 支持 4 种动作:
2477
- * wait_for_selector — 等待匹配选择器的元素出现
2478
- * wait_for_hidden — 等待元素消失或隐藏
2479
- * wait_for_text — 等待页面中出现指定文本
2480
- * wait_for_stable — 等待 DOM 在一段时间内无变化
2905
+ * 说明 / Notes:
2906
+ * - hash selector(如 #abc123)优先通过 RefStore 解析。
2907
+ * - 可见性语义与 dom-tool 保持一致(参考 Playwright 风格)。
2481
2908
  */
2482
- /** 默认超时时间(毫秒) */
2483
2909
  const DEFAULT_TIMEOUT = 1e4;
2910
+ const POLL_INTERVAL_MS = 80;
2911
+ const STABLE_TICK_MS = 50;
2912
+ const OBSERVER_OPTIONS = {
2913
+ childList: true,
2914
+ subtree: true,
2915
+ attributes: true,
2916
+ characterData: true
2917
+ };
2918
+ const TEXT_OBSERVER_OPTIONS = {
2919
+ childList: true,
2920
+ subtree: true,
2921
+ characterData: true
2922
+ };
2484
2923
  /**
2485
- * Playwright 风格可见性判定(近似)。
2924
+ * 可见性判定 / Visibility check.
2925
+ *
2926
+ * 与 dom-tool 保持一致,处理 display:contents、visibility、opacity、零尺寸等场景。
2486
2927
  */
2487
2928
  function isVisible(el) {
2488
2929
  if (!(el instanceof HTMLElement || el instanceof SVGElement)) return false;
2489
2930
  if (!el.isConnected) return false;
2490
2931
  const style = window.getComputedStyle(el);
2491
- if (style.display === "none" || style.visibility === "hidden") return false;
2932
+ if (style.display === "contents") {
2933
+ for (let child = el.firstChild; child; child = child.nextSibling) {
2934
+ if (child.nodeType === Node.ELEMENT_NODE && isVisible(child)) return true;
2935
+ if (child.nodeType === Node.TEXT_NODE) {
2936
+ const range = document.createRange();
2937
+ range.selectNodeContents(child);
2938
+ const rects = range.getClientRects();
2939
+ for (let i = 0; i < rects.length; i++) if (rects[i].width > 0 && rects[i].height > 0) return true;
2940
+ }
2941
+ }
2942
+ return false;
2943
+ }
2944
+ if (style.display === "none") return false;
2945
+ if (typeof el.checkVisibility === "function") {
2946
+ if (!el.checkVisibility()) return false;
2947
+ }
2948
+ if (style.visibility !== "visible") return false;
2492
2949
  if (style.opacity === "0") return false;
2493
2950
  const rect = el.getBoundingClientRect();
2494
2951
  return rect.width > 0 && rect.height > 0;
2495
2952
  }
2496
2953
  /**
2497
- * 读取 selector 当前状态。
2954
+ * 解析选择器 / Resolve selector.
2955
+ *
2956
+ * 先尝试 RefStore hash,再回退到 document.querySelector。
2957
+ */
2958
+ function resolveSelector(selector) {
2959
+ if (selector.startsWith("#")) {
2960
+ const store = getActiveRefStore();
2961
+ if (store) {
2962
+ const id = selector.slice(1);
2963
+ if (store.has(id)) return store.get(id) ?? null;
2964
+ }
2965
+ }
2966
+ try {
2967
+ return document.querySelector(selector);
2968
+ } catch {
2969
+ return null;
2970
+ }
2971
+ }
2972
+ /**
2973
+ * 计算选择器状态 / Evaluate selector state.
2974
+ *
2975
+ * @returns matched 表示是否达到目标状态;element 为当前命中的元素(如果存在)。
2498
2976
  */
2499
2977
  function evaluateSelectorState(selector, state) {
2500
- const el = document.querySelector(selector) ?? void 0;
2978
+ const el = resolveSelector(selector) ?? void 0;
2501
2979
  switch (state) {
2502
2980
  case "attached": return {
2503
2981
  matched: Boolean(el),
@@ -2519,7 +2997,9 @@ function evaluateSelectorState(selector, state) {
2519
2997
  }
2520
2998
  }
2521
2999
  /**
2522
- * 等待 selector 达到指定状态(近似 Playwright state 语义)。
3000
+ * 等待选择器达到指定状态 / Wait selector reaches state.
3001
+ *
3002
+ * 策略:轮询 + MutationObserver 双通道,既保证及时性也降低漏检概率。
2523
3003
  */
2524
3004
  function waitForSelectorState(selector, state, timeoutMs) {
2525
3005
  return new Promise((resolve, reject) => {
@@ -2545,19 +3025,16 @@ function waitForSelectorState(selector, state, timeoutMs) {
2545
3025
  const timer = setTimeout(() => {
2546
3026
  finish(() => reject(/* @__PURE__ */ new Error(`等待 "${selector}" 达到状态 "${state}" 超时 (${timeoutMs}ms)`)));
2547
3027
  }, timeoutMs);
2548
- const interval = setInterval(check, 80);
3028
+ const interval = setInterval(check, POLL_INTERVAL_MS);
2549
3029
  const observer = new MutationObserver(check);
2550
- observer.observe(document.body, {
2551
- childList: true,
2552
- subtree: true,
2553
- attributes: true,
2554
- characterData: true
2555
- });
3030
+ observer.observe(document.body, OBSERVER_OPTIONS);
2556
3031
  check();
2557
3032
  });
2558
3033
  }
2559
3034
  /**
2560
- * 等待页面中出现指定文本。
3035
+ * 等待文本出现 / Wait text appears.
3036
+ *
3037
+ * 先做一次即时检查,再监听 DOM 变化。
2561
3038
  */
2562
3039
  function waitForText(text, timeoutMs) {
2563
3040
  return new Promise((resolve, reject) => {
@@ -2576,15 +3053,13 @@ function waitForText(text, timeoutMs) {
2576
3053
  resolve();
2577
3054
  }
2578
3055
  });
2579
- observer.observe(document.body, {
2580
- childList: true,
2581
- subtree: true,
2582
- characterData: true
2583
- });
3056
+ observer.observe(document.body, TEXT_OBSERVER_OPTIONS);
2584
3057
  });
2585
3058
  }
2586
3059
  /**
2587
- * 等待页面进入稳定状态:在 quietMs 时间窗口内没有 DOM 变化。
3060
+ * 等待 DOM 稳定 / Wait DOM stable.
3061
+ *
3062
+ * 定义:quietMs 窗口内没有任何 MutationObserver 事件。
2588
3063
  */
2589
3064
  function waitForDomStable(timeoutMs, quietMs) {
2590
3065
  return new Promise((resolve, reject) => {
@@ -2599,12 +3074,7 @@ function waitForDomStable(timeoutMs, quietMs) {
2599
3074
  const observer = new MutationObserver(() => {
2600
3075
  lastMutationAt = Date.now();
2601
3076
  });
2602
- observer.observe(document.body, {
2603
- childList: true,
2604
- subtree: true,
2605
- attributes: true,
2606
- characterData: true
2607
- });
3077
+ observer.observe(document.body, OBSERVER_OPTIONS);
2608
3078
  const tick = setInterval(() => {
2609
3079
  const now = Date.now();
2610
3080
  if (now - startedAt > timeoutMs) {
@@ -2612,7 +3082,7 @@ function waitForDomStable(timeoutMs, quietMs) {
2612
3082
  return;
2613
3083
  }
2614
3084
  if (now - lastMutationAt >= quietMs) finish(true);
2615
- }, 50);
3085
+ }, STABLE_TICK_MS);
2616
3086
  });
2617
3087
  }
2618
3088
  function createWaitTool() {
@@ -2965,7 +3435,17 @@ function registerToolHandler(executors) {
2965
3435
  * │ └──────────┘ └────────────┘ └──────────────┘ │
2966
3436
  * └──────────────────────────────────────────────────┘
2967
3437
  */
2968
- var WebAgent = class {
3438
+ var WebAgent = class WebAgent {
3439
+ /** 默认系统提示词 key(兼容旧版 setSystemPrompt(prompt))。 */
3440
+ static DEFAULT_SYSTEM_PROMPT_KEY = "default";
3441
+ /** 默认内置工具名(注册后受保护,不允许删除)。 */
3442
+ static DEFAULT_TOOL_NAMES = [
3443
+ "dom",
3444
+ "navigate",
3445
+ "page_info",
3446
+ "wait",
3447
+ "evaluate"
3448
+ ];
2969
3449
  /** 用户传入的自定义 AI 客户端实例(优先级高于 token/provider) */
2970
3450
  client;
2971
3451
  token;
@@ -2975,7 +3455,10 @@ var WebAgent = class {
2975
3455
  stream;
2976
3456
  dryRun;
2977
3457
  maxRounds;
2978
- customSystemPrompt;
3458
+ /** system prompt 注册表(key -> prompt 文本)。 */
3459
+ systemPromptRegistry = /* @__PURE__ */ new Map();
3460
+ /** 受保护工具集合(默认工具)。 */
3461
+ protectedToolNames = /* @__PURE__ */ new Set();
2979
3462
  /** 多轮对话记忆开关 */
2980
3463
  memory;
2981
3464
  /** 对话历史(memory 开启时自动累积) */
@@ -2997,10 +3480,11 @@ var WebAgent = class {
2997
3480
  this.stream = options.stream ?? true;
2998
3481
  this.dryRun = options.dryRun ?? false;
2999
3482
  this.maxRounds = options.maxRounds ?? 40;
3000
- this.customSystemPrompt = options.systemPrompt;
3001
3483
  this.memory = options.memory ?? false;
3002
3484
  this.autoSnapshot = options.autoSnapshot ?? true;
3003
3485
  this.snapshotOptions = options.snapshotOptions ?? {};
3486
+ if (typeof options.systemPrompt === "string") this.setSystemPrompt(options.systemPrompt);
3487
+ else if (options.systemPrompt && typeof options.systemPrompt === "object") this.setSystemPrompts(options.systemPrompt);
3004
3488
  }
3005
3489
  /** 注册所有内置 Web 工具(dom, navigate, page_info, wait, evaluate) */
3006
3490
  registerTools() {
@@ -3009,11 +3493,41 @@ var WebAgent = class {
3009
3493
  this.registry.register(createPageInfoTool());
3010
3494
  this.registry.register(createWaitTool());
3011
3495
  this.registry.register(createEvaluateTool());
3496
+ for (const name of WebAgent.DEFAULT_TOOL_NAMES) this.protectedToolNames.add(name);
3012
3497
  }
3013
3498
  /** 注册一个自定义工具 */
3014
3499
  registerTool(tool) {
3015
3500
  this.registry.register(tool);
3016
3501
  }
3502
+ /**
3503
+ * 删除一个已注册工具。
3504
+ * - 默认内置工具(registerTools 注册)不允许删除
3505
+ * - 返回 true 表示删除成功,false 表示不存在或受保护
3506
+ */
3507
+ removeTool(name) {
3508
+ if (this.protectedToolNames.has(name)) return false;
3509
+ return this.registry.unregister(name);
3510
+ }
3511
+ /** 检查工具是否已注册。 */
3512
+ hasTool(name) {
3513
+ return this.registry.has(name);
3514
+ }
3515
+ /** 获取当前所有已注册工具名。 */
3516
+ getToolNames() {
3517
+ return this.registry.getDefinitions().map((tool) => tool.name);
3518
+ }
3519
+ /**
3520
+ * 删除所有“非默认”工具。
3521
+ * 返回值为本次被删除的工具名数组。
3522
+ */
3523
+ clearCustomTools() {
3524
+ const removed = [];
3525
+ for (const tool of this.registry.getDefinitions()) {
3526
+ if (this.protectedToolNames.has(tool.name)) continue;
3527
+ if (this.registry.unregister(tool.name)) removed.push(tool.name);
3528
+ }
3529
+ return removed;
3530
+ }
3017
3531
  /** 获取所有已注册的工具定义列表 */
3018
3532
  getTools() {
3019
3533
  return this.registry.getDefinitions();
@@ -3051,9 +3565,37 @@ var WebAgent = class {
3051
3565
  setDryRun(enabled) {
3052
3566
  this.dryRun = enabled;
3053
3567
  }
3054
- /** 设置自定义系统提示词 */
3055
- setSystemPrompt(prompt) {
3056
- this.customSystemPrompt = prompt;
3568
+ setSystemPrompt(keyOrPrompt, maybePrompt) {
3569
+ const key = maybePrompt === void 0 ? WebAgent.DEFAULT_SYSTEM_PROMPT_KEY : keyOrPrompt.trim();
3570
+ const prompt = maybePrompt === void 0 ? keyOrPrompt : maybePrompt;
3571
+ if (!key) throw new Error("system prompt 的 key 不能为空");
3572
+ const value = prompt.trim();
3573
+ if (!value) throw new Error("system prompt 不能为空");
3574
+ this.systemPromptRegistry.set(key, value);
3575
+ }
3576
+ /** 批量注册系统提示词(key -> prompt)。 */
3577
+ setSystemPrompts(prompts) {
3578
+ for (const [key, prompt] of Object.entries(prompts)) this.setSystemPrompt(key, prompt);
3579
+ }
3580
+ /** 注销指定 key 的系统提示词。 */
3581
+ removeSystemPrompt(key) {
3582
+ return this.systemPromptRegistry.delete(key);
3583
+ }
3584
+ /** 只保留指定 key 的系统提示词,其余全部删除。 */
3585
+ keepOnlySystemPrompt(key) {
3586
+ if (!this.systemPromptRegistry.has(key)) return false;
3587
+ const value = this.systemPromptRegistry.get(key);
3588
+ this.systemPromptRegistry.clear();
3589
+ this.systemPromptRegistry.set(key, value);
3590
+ return true;
3591
+ }
3592
+ /** 获取当前已注册的全部系统提示词(浅拷贝)。 */
3593
+ getSystemPrompts() {
3594
+ return Object.fromEntries(this.systemPromptRegistry.entries());
3595
+ }
3596
+ /** 删除全部系统提示词。 */
3597
+ clearSystemPrompts() {
3598
+ this.systemPromptRegistry.clear();
3057
3599
  }
3058
3600
  /** 开启或关闭多轮对话记忆 */
3059
3601
  setMemory(enabled) {
@@ -3095,7 +3637,11 @@ var WebAgent = class {
3095
3637
  */
3096
3638
  async chat(message) {
3097
3639
  const client = this.client ?? this.createBuiltinClient();
3098
- let systemPrompt = this.customSystemPrompt ?? buildSystemPrompt({ tools: this.registry.getDefinitions() });
3640
+ let systemPrompt = buildSystemPrompt({ tools: this.registry.getDefinitions() });
3641
+ if (this.systemPromptRegistry.size > 0) {
3642
+ const extensionText = Array.from(this.systemPromptRegistry.entries()).map(([key, prompt]) => `- [${key}]\n${prompt}`).join("\n\n");
3643
+ systemPrompt += `\n\n## Registered System Prompt Extensions\n${extensionText}`;
3644
+ }
3099
3645
  const refStore = new RefStore(globalThis.location?.href);
3100
3646
  setActiveRefStore(refStore);
3101
3647
  let initialSnapshot;