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
|
-
|
|
1380
|
-
|
|
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
|
|
1439
|
-
const
|
|
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
|
|
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, {
|
|
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
|
-
*
|
|
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
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
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:
|
|
5329
|
-
|
|
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 登录态会持久化)
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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",
|