dsh-browser-view 0.7.2 → 0.7.4

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/lib/index.js CHANGED
@@ -572,12 +572,12 @@ function __walkerCore() {
572
572
  }
573
573
  if (el.id) {
574
574
  const t = document.querySelector(`label[for="${CSS.escape(el.id)}"]`)?.textContent?.trim();
575
- if (t) return t.slice(0, 60);
575
+ if (t) return t.replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
576
576
  }
577
577
  const wrapLabel = el.closest("label");
578
578
  if (wrapLabel) {
579
579
  const t = wrapLabel.textContent?.trim();
580
- if (t) return t.slice(0, 60);
580
+ if (t) return t.replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
581
581
  }
582
582
  if ([
583
583
  "checkbox",
@@ -587,17 +587,45 @@ function __walkerCore() {
587
587
  const container = el.closest("label, [class*='wrapper'], [class*='item'], li");
588
588
  if (container) {
589
589
  const t = container.textContent?.trim();
590
- if (t) return t.replace(/\s+/g, " ").slice(0, 60);
590
+ if (t) return t.replace(/\s+/g, " ").replace(/^[*::\s]+|[*::\s]+$/g, "").slice(0, 60);
591
591
  }
592
592
  }
593
593
  if ([
594
594
  "textbox",
595
595
  "searchbox",
596
596
  "combobox",
597
- "spinbutton"
597
+ "spinbutton",
598
+ "slider",
599
+ "listbox"
598
600
  ].includes(role)) {
601
+ const formItem = el.closest("[class*='form-item'], [class*='form__item'], [class*='form-group'], [role='group']");
602
+ if (formItem) {
603
+ const lab = formItem.querySelector("label, [class*='form-item-label'], [class*='form-item__label'], [class*='label']");
604
+ if (lab) {
605
+ const t = (lab.getAttribute("title") || lab.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
606
+ if (t) return t.slice(0, 60);
607
+ }
608
+ }
609
+ const selectHost = el.closest("[class*='select'], [class*='cascader'], [class*='picker']");
610
+ if (selectHost) {
611
+ const ph = selectHost.querySelector("[class*='placeholder'], [class*='selection-placeholder']")?.textContent?.trim();
612
+ if (ph) return ph.slice(0, 60);
613
+ const selVal = selectHost.querySelector("[class*='selection-item'], [class*='selected-value']")?.textContent?.trim();
614
+ if (selVal) return selVal.slice(0, 60);
615
+ }
599
616
  const ph = el.getAttribute("placeholder");
600
617
  if (ph && ph.trim()) return ph.trim().slice(0, 60);
618
+ const host = selectHost || el;
619
+ const prevEl = host.previousElementSibling;
620
+ if (prevEl) {
621
+ const prevText = (prevEl.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
622
+ if (prevText && prevText.length <= 30) return prevText.slice(0, 60);
623
+ }
624
+ const td = host.closest("td");
625
+ if (td && td.previousElementSibling) {
626
+ const tdText = (td.previousElementSibling.textContent || "").trim().replace(/^[*::\s]+|[*::\s]+$/g, "");
627
+ if (tdText && tdText.length <= 30) return tdText.slice(0, 60);
628
+ }
601
629
  }
602
630
  const text = (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ");
603
631
  if (text) return text.slice(0, 60);
@@ -1376,8 +1404,38 @@ async function executeUniversalSelect(ctx) {
1376
1404
  const options = await withTimeout(target.locator("option").allTextContents(), probe, "读取原生下拉选项").catch(() => []);
1377
1405
  throw new Error(`原生下拉无选项 "${want}"; 可用选项: ${options.map((t) => t.trim()).join(" / ")}`);
1378
1406
  }
1379
- const hostLocator = target.locator("xpath=ancestor-or-self::*[@role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][last()]");
1380
- const actualTarget = await hostLocator.count().catch(() => 0) > 0 ? hostLocator.first() : target;
1407
+ let resolvedTarget = target;
1408
+ if (await withTimeout(target.evaluate((el) => {
1409
+ if (el.tagName.toLowerCase() === "label") return true;
1410
+ const cls = typeof el.className === "string" ? el.className : "";
1411
+ if (/(?:form-item|form__item|form-group)/i.test(cls)) return true;
1412
+ if (el.closest("label, [class*='form-item'], [class*='form__item'], [class*='form-group']")) return !el.matches("[role='combobox'], select, [class*='select-selector']");
1413
+ if (!el.matches("[role='combobox'], select, [class*='select'], [class*='selector']")) return true;
1414
+ return false;
1415
+ }), 1500, "探测 label 或非标文本容器").catch(() => false)) {
1416
+ const descendantSelect = target.locator("xpath=.//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)]").first();
1417
+ if (await descendantSelect.count().catch(() => 0) > 0) resolvedTarget = descendantSelect;
1418
+ else {
1419
+ const forId = await target.getAttribute("for").catch(() => null);
1420
+ if (forId) {
1421
+ const forTarget = page.locator(`[id="${forId.replace(/"/g, "\\\"")}"]`).first();
1422
+ if (await forTarget.count().catch(() => 0) > 0) resolvedTarget = forTarget;
1423
+ } else {
1424
+ const nextSiblingSelect = target.locator("xpath=following-sibling::*[self::select or @role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][1] | following-sibling::*[1]//*[@role='combobox' or self::select or contains(@class, 'select')]").first();
1425
+ if (await nextSiblingSelect.count().catch(() => 0) > 0) resolvedTarget = nextSiblingSelect;
1426
+ else {
1427
+ const tdSiblingSelect = target.locator("xpath=ancestor-or-self::td[1]/following-sibling::td[1]//*[@role='combobox' or self::select or contains(@class, 'select')]").first();
1428
+ if (await tdSiblingSelect.count().catch(() => 0) > 0) resolvedTarget = tdSiblingSelect;
1429
+ else {
1430
+ const siblingSelect = target.locator("xpath=ancestor-or-self::*[contains(@class, 'form-item') or contains(@class, 'form__item') or contains(@class, 'form-group')][1]//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)] | ..//*[@role='combobox' or self::select or contains(@class, 'select-selector') or contains(@class, 'select')][not(self::label)]").first();
1431
+ if (await siblingSelect.count().catch(() => 0) > 0) resolvedTarget = siblingSelect;
1432
+ }
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ const hostLocator = resolvedTarget.locator("xpath=ancestor-or-self::*[@role='combobox' or contains(@class, 'select') or contains(@class, 'selector')][last()]");
1438
+ const actualTarget = await hostLocator.count().catch(() => 0) > 0 ? hostLocator.first() : resolvedTarget;
1381
1439
  if (clear) try {
1382
1440
  const clearBtn = actualTarget.locator("[class*='clear'], [aria-label*='clear']").first();
1383
1441
  if (await clearBtn.count().catch(() => 0) > 0 && await clearBtn.isVisible().catch(() => false)) await clearBtn.click({ timeout: 1e3 }).catch(() => {});
@@ -1430,13 +1488,17 @@ async function executeUniversalSelect(ctx) {
1430
1488
  const optionSelector = "[role='option']:not([style*='height: 0px']):not([class*='empty']), [class*='item-option']:not([class*='empty']), [class*='select-dropdown'] li:not([class*='empty'])";
1431
1489
  const dropdownSelector = "[role='listbox']:not([style*='display: none']), [class*='select-dropdown']:not([style*='display: none']):not([class*='hidden']), [class*='popper']:not([style*='display: none'])";
1432
1490
  let options;
1491
+ let targetDropdown = null;
1433
1492
  if (controlsId) {
1434
1493
  const parentDropdown = page.locator(`xpath=//*[@id='${controlsId}']/ancestor-or-self::*[contains(@class, 'dropdown') or contains(@class, 'popper') or @role='listbox'][1] | //*[contains(@class, 'dropdown') or @role='listbox'][.//*[@id='${controlsId}']]`);
1494
+ targetDropdown = parentDropdown.first();
1435
1495
  const visual = parentDropdown.locator(optionSelector).locator("visible=true");
1436
1496
  options = await visual.count().catch(() => 0) > 0 ? visual : parentDropdown.locator(optionSelector);
1437
1497
  } else {
1438
- const activeDropdowns = page.locator(dropdownSelector);
1439
- const dropdown = await activeDropdowns.count().catch(() => 0) > 1 ? activeDropdowns.last() : activeDropdowns.first();
1498
+ const activeVisualDropdowns = page.locator(dropdownSelector).locator("visible=true");
1499
+ const visualCount = await activeVisualDropdowns.count().catch(() => 0);
1500
+ const dropdown = visualCount > 1 ? activeVisualDropdowns.last() : visualCount === 1 ? activeVisualDropdowns.first() : page.locator(dropdownSelector).last();
1501
+ targetDropdown = dropdown;
1440
1502
  const visual = dropdown.locator(optionSelector).locator("visible=true");
1441
1503
  options = await visual.count().catch(() => 0) > 0 ? visual : dropdown.locator(optionSelector);
1442
1504
  }
@@ -1452,7 +1514,7 @@ async function executeUniversalSelect(ctx) {
1452
1514
  };
1453
1515
  })).catch(() => [])).map((e) => e.text || e.title || e.aria).filter(Boolean);
1454
1516
  if (visibleTexts.length === 0) {
1455
- const isEmpty = await page.locator("[class*='empty'], [class*='no-data'], [role='status'], [class*='item-empty']").first().isVisible().catch(() => false);
1517
+ const isEmpty = targetDropdown !== null ? await targetDropdown.locator("[class*='empty'], [class*='no-data'], [role='status'], [class*='item-empty']").first().isVisible().catch(() => false) : false;
1456
1518
  await actualTarget.click({ timeout: 1e3 }).catch(() => {});
1457
1519
  if (isEmpty) throw new Error(`下拉浮层当前无选项(数据为空态/无可选数据): ${label}`);
1458
1520
  throw new Error(`点击 ${label} 后未出现下拉选项(选项列表为空态)`);
@@ -1761,6 +1823,7 @@ const scanInteractive = injectWalker((payload, w) => {
1761
1823
  }
1762
1824
  const hit = payload.exact ? legacyHit : legacyHit || tokens.length > 0 && tokenHit;
1763
1825
  if (legacyHit && name !== "" && name.toLowerCase() === q) score += 50;
1826
+ if (payload.preferredRoles && payload.preferredRoles.includes(role)) score += 40;
1764
1827
  scanned += 1;
1765
1828
  if (name !== "" && candidates.length < 50) candidates.push({
1766
1829
  role,
@@ -1857,7 +1920,8 @@ async function resolveSemanticTarget(page, what, opts = {}) {
1857
1920
  exact: opts.exact === true,
1858
1921
  scope: typeof opts.scope === "string" && opts.scope !== "" ? opts.scope : "",
1859
1922
  limit: Math.min(Math.max(opts.limit ?? 5, 1), 20),
1860
- dialogSelector: ACTIVE_DIALOG_SELECTORS
1923
+ dialogSelector: ACTIVE_DIALOG_SELECTORS,
1924
+ preferredRoles: opts.preferredRoles
1861
1925
  });
1862
1926
  } catch {
1863
1927
  return {
@@ -2034,7 +2098,7 @@ function nameSimilar(a, b) {
2034
2098
  * 自动识别 selector 或 ref;若 ref 标记位于子 Frame,自动绑定到对应子 Frame 上下文。
2035
2099
  * 具备路径防漂移校验与 getByRole 自愈降级能力。
2036
2100
  */
2037
- async function resolveTargetLocator(page, args) {
2101
+ async function resolveTargetLocator(page, args, opts = {}) {
2038
2102
  {
2039
2103
  const present = [];
2040
2104
  if (args.ref !== void 0 && args.ref !== "") present.push("ref");
@@ -2224,7 +2288,10 @@ async function resolveTargetLocator(page, args) {
2224
2288
  }
2225
2289
  const what = args.what;
2226
2290
  if (typeof what === "string" && what.trim() !== "" && args.selector === void 0 && args.ref === void 0 && args.mark === void 0 && rowPattern === void 0) {
2227
- const res = await resolveSemanticTarget(page, what, { scope: typeof args.scope === "string" && args.scope !== "" ? args.scope : void 0 });
2291
+ const res = await resolveSemanticTarget(page, what, {
2292
+ scope: typeof args.scope === "string" && args.scope !== "" ? args.scope : void 0,
2293
+ preferredRoles: opts.preferredRoles
2294
+ });
2228
2295
  if (!("candidates" in res)) return res;
2229
2296
  for (const role of [
2230
2297
  "button",
@@ -4261,7 +4328,11 @@ async function clickTool(config, args, exec) {
4261
4328
  }
4262
4329
  async function typeTool(config, args) {
4263
4330
  const page = await getPage(config);
4264
- const targetRes = await resolveTargetLocator(page, args);
4331
+ const targetRes = await resolveTargetLocator(page, args, { preferredRoles: [
4332
+ "textbox",
4333
+ "searchbox",
4334
+ "spinbutton"
4335
+ ] });
4265
4336
  const target = targetRes.locator;
4266
4337
  const strictErr = strictReject(args, targetRes);
4267
4338
  if (strictErr !== null) throw strictErr;
@@ -4352,7 +4423,11 @@ async function pressTool(config, args) {
4352
4423
  */
4353
4424
  async function selectTool(config, args) {
4354
4425
  const page = await getPage(config);
4355
- const targetRes = await resolveTargetLocator(page, args);
4426
+ const targetRes = await resolveTargetLocator(page, args, { preferredRoles: [
4427
+ "combobox",
4428
+ "select",
4429
+ "listbox"
4430
+ ] });
4356
4431
  const target = targetRes.locator;
4357
4432
  const strictErr = strictReject(args, targetRes);
4358
4433
  if (strictErr !== null) throw strictErr;
@@ -5308,25 +5383,187 @@ function previewHighlight(page, selector, label) {
5308
5383
  }).catch(() => {});
5309
5384
  }
5310
5385
  /**
5386
+ * 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
5387
+ */
5388
+ const AUTH_URL_PATTERN = /login|signin|sign-in|oauth|passport|sso|cas|challenge|captcha|verify|auth\b/i;
5389
+ /**
5390
+ * 等待人工登录底层执行(独立导出供测试注入 fake page)。
5391
+ * 协同机制:
5392
+ * 1. 挂起等待期间置入 instruction 至实时画面面板(setPauseInstruction),结束后统一清理;
5393
+ * 2. 存活标签感知:感知 context 内所有存活页面,原标签关闭时自动迁移至存活页,新标签出现时通知外层注册;
5394
+ * 3. 稳态判定:
5395
+ * - 显式 successSelector / successUrl 优先;
5396
+ * - 缺省时,检测 URL 离开登录页(非 AUTH_URL_PATTERN)、登录密码框消除、或 SPA 内容签名稳态变化,防范跳板页误判与 SPA 弹窗漏判。
5397
+ */
5398
+ async function runWaitForLoginOnPage(initialPage, args, onNewPageDetected) {
5399
+ const instruction = typeof args.instruction === "string" && args.instruction.trim() ? args.instruction.trim() : "请在浏览器窗口中完成登录";
5400
+ const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 1e3), 6e5);
5401
+ const pollIntervalMs = Math.min(Math.max(Number(args.pollIntervalMs ?? 2e3) || 2e3, 100), 5e3);
5402
+ let currentPage = initialPage;
5403
+ let startUrl = "";
5404
+ try {
5405
+ startUrl = initialPage.url();
5406
+ } catch {}
5407
+ const checkHasPassword = async (p) => {
5408
+ try {
5409
+ return await p.evaluate(() => !!document.querySelector("input[type=\"password\"]"));
5410
+ } catch {
5411
+ return false;
5412
+ }
5413
+ };
5414
+ const getPageSig = async (p) => {
5415
+ try {
5416
+ return await p.evaluate(() => {
5417
+ const body = document.body;
5418
+ return `${location.href}|${body ? body.innerText.length : 0}|${document.querySelectorAll("input,button,a").length}`;
5419
+ });
5420
+ } catch {
5421
+ try {
5422
+ return p.url();
5423
+ } catch {
5424
+ return "";
5425
+ }
5426
+ }
5427
+ };
5428
+ const startHasPassword = await checkHasPassword(initialPage);
5429
+ const startSig = args.successSelector || args.successUrl ? "" : await getPageSig(initialPage);
5430
+ const startedAt = Date.now();
5431
+ let loggedIn = false;
5432
+ let detail = `等待登录超时(${timeoutMs}ms)`;
5433
+ setPauseInstruction(instruction);
5434
+ try {
5435
+ const deadline = startedAt + timeoutMs;
5436
+ while (Date.now() < deadline) {
5437
+ let candidatePages = [];
5438
+ try {
5439
+ const context = typeof currentPage.context === "function" ? currentPage.context() : null;
5440
+ if (context && typeof context.pages === "function") candidatePages = context.pages().filter((p) => {
5441
+ try {
5442
+ return !p.isClosed();
5443
+ } catch {
5444
+ return false;
5445
+ }
5446
+ });
5447
+ } catch {}
5448
+ let isCurrentClosed = false;
5449
+ try {
5450
+ isCurrentClosed = currentPage.isClosed();
5451
+ } catch {
5452
+ isCurrentClosed = true;
5453
+ }
5454
+ if (candidatePages.length > 0) {
5455
+ for (const p of candidatePages) if (p !== currentPage && onNewPageDetected) onNewPageDetected(p);
5456
+ if (isCurrentClosed) {
5457
+ currentPage = candidatePages[candidatePages.length - 1];
5458
+ isCurrentClosed = false;
5459
+ }
5460
+ }
5461
+ try {
5462
+ if (args.successSelector) {
5463
+ const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
5464
+ for (const p of targets) try {
5465
+ if (await p.locator(String(args.successSelector)).first().isVisible()) {
5466
+ loggedIn = true;
5467
+ currentPage = p;
5468
+ detail = `命中 successSelector: ${args.successSelector}`;
5469
+ break;
5470
+ }
5471
+ } catch {}
5472
+ } else if (args.successUrl) {
5473
+ const re = new RegExp(String(args.successUrl));
5474
+ const targets = candidatePages.length > 0 ? candidatePages : isCurrentClosed ? [] : [currentPage];
5475
+ for (const p of targets) try {
5476
+ const u = p.url();
5477
+ if (re.test(u)) {
5478
+ loggedIn = true;
5479
+ currentPage = p;
5480
+ detail = `命中 successUrl: ${args.successUrl}`;
5481
+ break;
5482
+ }
5483
+ } catch {}
5484
+ } else {
5485
+ const target = !isCurrentClosed ? currentPage : candidatePages[0];
5486
+ if (target) {
5487
+ let curUrl = "";
5488
+ try {
5489
+ curUrl = target.url();
5490
+ } catch {}
5491
+ if (curUrl && curUrl !== "about:blank") {
5492
+ if (curUrl !== startUrl) {
5493
+ if (!AUTH_URL_PATTERN.test(curUrl)) {
5494
+ loggedIn = true;
5495
+ currentPage = target;
5496
+ detail = `URL 离开登录页跳转至 ${curUrl}`;
5497
+ } else {
5498
+ const hasPwd = await checkHasPassword(target);
5499
+ if (startHasPassword && !hasPwd) {
5500
+ loggedIn = true;
5501
+ currentPage = target;
5502
+ detail = `登录密码输入框已消除,页面跳转至 ${curUrl}`;
5503
+ }
5504
+ }
5505
+ } else {
5506
+ const hasPwd = await checkHasPassword(target);
5507
+ if (startHasPassword && !hasPwd) {
5508
+ loggedIn = true;
5509
+ currentPage = target;
5510
+ detail = "登录密码输入框已消除(SPA 登录完成)";
5511
+ } else if (await getPageSig(target) !== startSig && (!startHasPassword || !hasPwd)) {
5512
+ loggedIn = true;
5513
+ currentPage = target;
5514
+ detail = "页面内容签名变更(SPA 登录完成)";
5515
+ }
5516
+ }
5517
+ }
5518
+ }
5519
+ }
5520
+ } catch (e) {
5521
+ detail = `检查登录状态出错: ${e instanceof Error ? e.message : String(e)}`;
5522
+ }
5523
+ if (loggedIn) break;
5524
+ try {
5525
+ await currentPage.waitForTimeout(pollIntervalMs);
5526
+ } catch {
5527
+ await sleep$1(pollIntervalMs);
5528
+ }
5529
+ }
5530
+ } finally {
5531
+ clearPauseInstruction();
5532
+ }
5533
+ return {
5534
+ loggedIn,
5535
+ waitedMs: Date.now() - startedAt,
5536
+ detail,
5537
+ targetPage: currentPage
5538
+ };
5539
+ }
5540
+ /**
5311
5541
  * 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
5312
- * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。基于通用条件引擎:
5313
- * successSelector / successUrl 指定完成条件,两者都不给则以 URL 跳转视为完成。
5314
- * 超时返回 loggedIn:false(不抛错)。
5542
+ * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。支持 instruction 提示语
5543
+ * 与右下角实时画面面板联动显示横幅,支持多标签感知与 SPA 签名自适应判定。
5544
+ * 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
5315
5545
  */
5316
5546
  async function waitForLoginTool(config, args) {
5317
5547
  const page = await getPage(config);
5318
- const startUrl = page.url();
5319
- let condition;
5320
- if (args.successSelector) condition = {
5321
- selector: String(args.successSelector),
5322
- state: "visible"
5323
- };
5324
- else if (args.successUrl) condition = { url: String(args.successUrl) };
5325
- else condition = { eval: `location.href !== ${JSON.stringify(startUrl)} && location.href !== "about:blank"` };
5326
- const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 12e4) || 12e4, 5e3), 6e5);
5548
+ const state = stateOf();
5549
+ const result = await runWaitForLoginOnPage(page, args, (newPage) => {
5550
+ if (![...state.pages.values()].includes(newPage)) {
5551
+ attachPage(newPage, profileConfig(config, currentProfile));
5552
+ registerTabPage(state, newPage);
5553
+ }
5554
+ });
5555
+ if (result.targetPage !== page) {
5556
+ for (const [id, p] of state.pages) if (p === result.targetPage) {
5557
+ state.activeId = id;
5558
+ break;
5559
+ }
5560
+ }
5561
+ const identity = await pageIdentity(result.targetPage);
5327
5562
  return {
5328
- loggedIn: (await pollCondition(page, condition, timeoutMs, 2e3)).ok,
5329
- ...await pageIdentity(page)
5563
+ loggedIn: result.loggedIn,
5564
+ waitedMs: result.waitedMs,
5565
+ detail: result.detail,
5566
+ ...identity
5330
5567
  };
5331
5568
  }
5332
5569
  /**
@@ -8373,10 +8610,14 @@ function getCollaborationTools(config, timeout, stepImpls) {
8373
8610
  return [
8374
8611
  definePlainTool({
8375
8612
  name: "browser_wait_for_login",
8376
- description: "等待人工登录完成。agent 在登录页调用本工具后,提示用户在浏览器窗口中完成登录(有头模式 headless:false 时用户可直接操作;配 userDataDir 登录态会持久化)。成功条件三选一:successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则,如 'https://\\\\.app\\\\.example\\\\.com')都不给则 URL 发生变化即视为完成。轮询间隔 2 秒,超时返回 loggedIn:false。",
8613
+ description: "等待人工登录完成。agent 在登录页调用本工具后,提示用户在浏览器窗口中完成登录(有头模式 headless:false 时用户可直接操作;配 userDataDir 登录态会持久化)。支持 instruction 提示语(将显示在右下角实时画面面板);成功条件:优先取 successSelector(登录后出现的元素,如 '#avatar')或 successUrl(登录后 URL 正则);两者未指定时,结合 URL 变化、登录特征消除(密码框等)与页面签名稳态综合判定。轮询间隔 2 秒,超时返回 loggedIn:false(附 waitedMs 与 detail)。",
8377
8614
  parameters: {
8378
8615
  type: "object",
8379
8616
  properties: {
8617
+ instruction: {
8618
+ type: "string",
8619
+ description: "可选:给用户看的提示说明(默认「请在浏览器窗口中完成登录」)。"
8620
+ },
8380
8621
  timeoutMs: {
8381
8622
  type: "integer",
8382
8623
  description: "等待上限(毫秒,默认 120000,上限 600000)。"
@@ -8397,11 +8638,15 @@ function getCollaborationTools(config, timeout, stepImpls) {
8397
8638
  additionalProperties: false,
8398
8639
  properties: {
8399
8640
  loggedIn: { type: "boolean" },
8641
+ waitedMs: { type: "integer" },
8642
+ detail: { type: "string" },
8400
8643
  url: { type: "string" },
8401
8644
  title: { type: "string" }
8402
8645
  },
8403
8646
  required: [
8404
8647
  "loggedIn",
8648
+ "waitedMs",
8649
+ "detail",
8405
8650
  "url",
8406
8651
  "title"
8407
8652
  ]
@@ -35,6 +35,7 @@ export declare const scanInteractive: (payload: {
35
35
  scope: string;
36
36
  limit: number;
37
37
  dialogSelector?: string;
38
+ preferredRoles?: string[];
38
39
  }) => {
39
40
  matches: SemanticMatch[];
40
41
  candidates: {
@@ -65,6 +66,7 @@ export declare function resolveSemanticTarget(page: Page, what: string, opts?: {
65
66
  scope?: string;
66
67
  limit?: number;
67
68
  maxRefs?: number;
69
+ preferredRoles?: string[];
68
70
  }): Promise<TargetResolution | SemanticMiss>;
69
71
  /**
70
72
  * 全量可交互元素扫描(SPA ref 重锚定的数据源):返回页面(或 scope 内)全部
@@ -10,7 +10,9 @@ export declare function buildFlexibleTextPattern(text: string): RegExp;
10
10
  * 自动识别 selector 或 ref;若 ref 标记位于子 Frame,自动绑定到对应子 Frame 上下文。
11
11
  * 具备路径防漂移校验与 getByRole 自愈降级能力。
12
12
  */
13
- export declare function resolveTargetLocator(page: Page, args: Record<string, any>): Promise<TargetResolution>;
13
+ export declare function resolveTargetLocator(page: Page, args: Record<string, any>, opts?: {
14
+ preferredRoles?: string[];
15
+ }): Promise<TargetResolution>;
14
16
  /** 兼容旧版签名的目标定位快捷方式 */
15
17
  export declare function targetLocator(page: Page, args: Record<string, any>): Promise<Locator>;
16
18
  //# sourceMappingURL=target.d.ts.map
@@ -5,16 +5,38 @@ import type { BrowserConfig } from "../types.js";
5
5
  * ~1.5 秒。异步不阻塞(fire-and-forget,任何失败静默),不影响操作时序。
6
6
  */
7
7
  export declare function previewHighlight(page: Page | null, selector: string, label: string): void;
8
+ /**
9
+ * 常见认证/登录相关的 URL 特征正则,用于防止跳板页提前误判。
10
+ */
11
+ export declare const AUTH_URL_PATTERN: RegExp;
12
+ export interface WaitForLoginResult {
13
+ loggedIn: boolean;
14
+ waitedMs: number;
15
+ detail: string;
16
+ targetPage: Page;
17
+ }
18
+ /**
19
+ * 等待人工登录底层执行(独立导出供测试注入 fake page)。
20
+ * 协同机制:
21
+ * 1. 挂起等待期间置入 instruction 至实时画面面板(setPauseInstruction),结束后统一清理;
22
+ * 2. 存活标签感知:感知 context 内所有存活页面,原标签关闭时自动迁移至存活页,新标签出现时通知外层注册;
23
+ * 3. 稳态判定:
24
+ * - 显式 successSelector / successUrl 优先;
25
+ * - 缺省时,检测 URL 离开登录页(非 AUTH_URL_PATTERN)、登录密码框消除、或 SPA 内容签名稳态变化,防范跳板页误判与 SPA 弹窗漏判。
26
+ */
27
+ export declare function runWaitForLoginOnPage(initialPage: Page, args: Record<string, any>, onNewPageDetected?: (page: Page) => void): Promise<WaitForLoginResult>;
8
28
  /**
9
29
  * 等待人工登录完成(配合 headless:false 使用):agent 打开登录页后调用本工具,
10
- * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。基于通用条件引擎:
11
- * successSelector / successUrl 指定完成条件,两者都不给则以 URL 跳转视为完成。
12
- * 超时返回 loggedIn:false(不抛错)。
30
+ * 提示用户在浏览器窗口中完成登录(输入/扫码/双因素)。支持 instruction 提示语
31
+ * 与右下角实时画面面板联动显示横幅,支持多标签感知与 SPA 签名自适应判定。
32
+ * 超时返回 loggedIn:false(附 waitedMs 与 detail,不抛错)。
13
33
  */
14
34
  export declare function waitForLoginTool(config: BrowserConfig, args: Record<string, any>): Promise<{
15
35
  url: string;
16
36
  title: string;
17
37
  loggedIn: boolean;
38
+ waitedMs: number;
39
+ detail: string;
18
40
  }>;
19
41
  /**
20
42
  * 人工接管等待(通用版 wait_for_login):agent 把控制权交还给用户(验证码 /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-view",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "DSH 插件:共享 Playwright 浏览器自动化(38 个 browser_* 工具,含 what 意图语义定位(一次调用替代快照→查找→点击)、SoM 标徽看图点击与零命中截图兜底、SPA ref 软失效重锚定、open Shadow DOM 组合树穿透、跨 Frame/IFrame 定位、视口感知动态快照、批量表单智能填充、点击效果验证与定位失败遥测、Playwright 规范代码导出、批处理、视觉回归、选择器自愈、增量快照)+ 人机协作(面板点选教学 browser_pick_element、人工接管等待、敏感操作审批硬拦截、人机混合录制、面板一键重跑)+ CDP 流式推流、拟人化轨迹与高斯击键、全景时光机、企业富控件矩阵。仅依赖 playwright-core。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",