dsh-browser-view 0.4.0 → 0.4.1
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 +239 -26
- package/lib/types/browser/tools.d.ts +13 -1
- package/lib/types/browser/types.d.ts +6 -0
- package/lib/types/browser/walker.d.ts +12 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -243,6 +243,150 @@ function __walkerCore() {
|
|
|
243
243
|
"meta",
|
|
244
244
|
"br"
|
|
245
245
|
]);
|
|
246
|
+
function safeEscape(str) {
|
|
247
|
+
return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(str) : str;
|
|
248
|
+
}
|
|
249
|
+
function matches(el, sel) {
|
|
250
|
+
try {
|
|
251
|
+
return typeof el.matches === "function" && el.matches(sel);
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function closest(el, sel) {
|
|
257
|
+
try {
|
|
258
|
+
return typeof el.closest === "function" ? el.closest(sel) : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 运行时组件元数据与源码坐标反射:
|
|
265
|
+
* 支持 @xiaou66/vite-plugin-vue-mcp-next 与 vite-plugin-vue-inspector 的 data-v-inspector,
|
|
266
|
+
* 以及 Vue 3 挂载在 DOM 上的 __vueParentComponent 实例。
|
|
267
|
+
*/
|
|
268
|
+
function resolveVueAndInspector(el) {
|
|
269
|
+
let inspector = el.getAttribute("data-v-inspector") || el.getAttribute("data-v-inspect") || el.getAttribute("data-inspector-file") || "";
|
|
270
|
+
let cur = el.parentElement;
|
|
271
|
+
let depth = 0;
|
|
272
|
+
while (!inspector && cur && depth < 2) {
|
|
273
|
+
inspector = cur.getAttribute("data-v-inspector") || cur.getAttribute("data-v-inspect") || cur.getAttribute("data-inspector-file") || "";
|
|
274
|
+
cur = cur.parentElement;
|
|
275
|
+
depth += 1;
|
|
276
|
+
}
|
|
277
|
+
let componentName = "";
|
|
278
|
+
let props = {};
|
|
279
|
+
const vcomp = el.__vueParentComponent;
|
|
280
|
+
if (vcomp && typeof vcomp === "object") {
|
|
281
|
+
componentName = vcomp.type?.name || vcomp.type?.__name || "";
|
|
282
|
+
props = vcomp.props || {};
|
|
283
|
+
}
|
|
284
|
+
if (!componentName && el.parentElement) {
|
|
285
|
+
const pvcomp = el.parentElement.__vueParentComponent;
|
|
286
|
+
if (pvcomp && typeof pvcomp === "object") {
|
|
287
|
+
componentName = pvcomp.type?.name || pvcomp.type?.__name || "";
|
|
288
|
+
if (Object.keys(props).length === 0) props = pvcomp.props || {};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
componentName,
|
|
293
|
+
inspector,
|
|
294
|
+
props
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* 可扩展组件库适配体系:
|
|
299
|
+
* 抹平 Ant Design Vue、Element Plus、Naive UI、Arco Design 等组件库的外层包装与非原生标签。
|
|
300
|
+
*/
|
|
301
|
+
function resolveComponentSemantics(el) {
|
|
302
|
+
const { componentName, inspector, props } = resolveVueAndInspector(el);
|
|
303
|
+
const className = typeof el.className === "string" ? el.className : "";
|
|
304
|
+
if (className.includes("ant-") || /^A[A-Z]/.test(componentName) || !!closest(el, ".ant-form-item")) {
|
|
305
|
+
const formLabel = closest(el, ".ant-form-item")?.querySelector(".ant-form-item-label")?.textContent?.trim() || "";
|
|
306
|
+
let role = "";
|
|
307
|
+
if (matches(el, ".ant-select, .ant-cascader, .ant-tree-select") || componentName === "ASelect" || componentName === "ACascader") role = "combobox";
|
|
308
|
+
else if (matches(el, ".ant-picker") || componentName === "ADatePicker" || componentName === "ATimePicker") role = "textbox";
|
|
309
|
+
else if (matches(el, ".ant-input-number") || componentName === "AInputNumber") role = "spinbutton";
|
|
310
|
+
else if (matches(el, ".ant-switch") || componentName === "ASwitch") role = "switch";
|
|
311
|
+
else if (matches(el, ".ant-btn") || componentName === "AButton") role = "button";
|
|
312
|
+
const ph = String(props.placeholder || el.querySelector(".ant-select-selection-placeholder")?.textContent?.trim() || el.getAttribute("placeholder") || "");
|
|
313
|
+
return {
|
|
314
|
+
library: "ant-design-vue",
|
|
315
|
+
component: componentName || (role === "combobox" ? "ASelect" : ""),
|
|
316
|
+
sourceFile: inspector,
|
|
317
|
+
role,
|
|
318
|
+
formLabel,
|
|
319
|
+
placeholder: ph
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (className.includes("el-") || /^El[A-Z]/.test(componentName) || !!closest(el, ".el-form-item")) {
|
|
323
|
+
const formLabel = closest(el, ".el-form-item")?.querySelector(".el-form-item__label")?.textContent?.trim() || "";
|
|
324
|
+
let role = "";
|
|
325
|
+
if (matches(el, ".el-select, .el-cascader") || componentName === "ElSelect" || componentName === "ElCascader") role = "combobox";
|
|
326
|
+
else if (matches(el, ".el-date-editor") || componentName === "ElDatePicker") role = "textbox";
|
|
327
|
+
else if (matches(el, ".el-input-number") || componentName === "ElInputNumber") role = "spinbutton";
|
|
328
|
+
else if (matches(el, ".el-switch") || componentName === "ElSwitch") role = "switch";
|
|
329
|
+
else if (matches(el, ".el-button") || componentName === "ElButton") role = "button";
|
|
330
|
+
const ph = String(props.placeholder || el.querySelector(".el-select__placeholder")?.textContent?.trim() || el.getAttribute("placeholder") || "");
|
|
331
|
+
return {
|
|
332
|
+
library: "element-plus",
|
|
333
|
+
component: componentName || (role === "combobox" ? "ElSelect" : ""),
|
|
334
|
+
sourceFile: inspector,
|
|
335
|
+
role,
|
|
336
|
+
formLabel,
|
|
337
|
+
placeholder: ph
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (className.includes("n-") || /^N[A-Z]/.test(componentName) || !!closest(el, ".n-form-item")) {
|
|
341
|
+
const formLabel = closest(el, ".n-form-item")?.querySelector(".n-form-item-label")?.textContent?.trim() || "";
|
|
342
|
+
let role = "";
|
|
343
|
+
if (matches(el, ".n-select") || componentName === "NSelect") role = "combobox";
|
|
344
|
+
else if (matches(el, ".n-date-picker") || componentName === "NDatePicker") role = "textbox";
|
|
345
|
+
else if (matches(el, ".n-switch") || componentName === "NSwitch") role = "switch";
|
|
346
|
+
else if (matches(el, ".n-button") || componentName === "NButton") role = "button";
|
|
347
|
+
const ph = String(props.placeholder || el.getAttribute("placeholder") || "");
|
|
348
|
+
return {
|
|
349
|
+
library: "naive-ui",
|
|
350
|
+
component: componentName || "",
|
|
351
|
+
sourceFile: inspector,
|
|
352
|
+
role,
|
|
353
|
+
formLabel,
|
|
354
|
+
placeholder: ph
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (className.includes("arco-") || !!closest(el, ".arco-form-item")) {
|
|
358
|
+
const formLabel = closest(el, ".arco-form-item")?.querySelector(".arco-form-item-label")?.textContent?.trim() || "";
|
|
359
|
+
let role = "";
|
|
360
|
+
if (matches(el, ".arco-select") || componentName.includes("Select")) role = "combobox";
|
|
361
|
+
else if (matches(el, ".arco-picker")) role = "textbox";
|
|
362
|
+
else if (matches(el, ".arco-btn")) role = "button";
|
|
363
|
+
const ph = String(props.placeholder || el.getAttribute("placeholder") || "");
|
|
364
|
+
return {
|
|
365
|
+
library: "arco-design",
|
|
366
|
+
component: componentName || "",
|
|
367
|
+
sourceFile: inspector,
|
|
368
|
+
role,
|
|
369
|
+
formLabel,
|
|
370
|
+
placeholder: ph
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (componentName || inspector) return {
|
|
374
|
+
library: "custom-vue",
|
|
375
|
+
component: componentName,
|
|
376
|
+
sourceFile: inspector,
|
|
377
|
+
role: "",
|
|
378
|
+
formLabel: "",
|
|
379
|
+
placeholder: String(props.placeholder || "")
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
library: "",
|
|
383
|
+
component: "",
|
|
384
|
+
sourceFile: "",
|
|
385
|
+
role: "",
|
|
386
|
+
formLabel: "",
|
|
387
|
+
placeholder: ""
|
|
388
|
+
};
|
|
389
|
+
}
|
|
246
390
|
function roleOf(el) {
|
|
247
391
|
const explicit = el.getAttribute("role");
|
|
248
392
|
if (explicit) return explicit.trim().split(/\s+/)[0] ?? "";
|
|
@@ -284,10 +428,11 @@ function __walkerCore() {
|
|
|
284
428
|
case "table": return "table";
|
|
285
429
|
case "dialog": return "dialog";
|
|
286
430
|
case "summary": return "button";
|
|
287
|
-
default: return
|
|
431
|
+
default: return resolveComponentSemantics(el).role;
|
|
288
432
|
}
|
|
289
433
|
}
|
|
290
434
|
function nameOf(el, role) {
|
|
435
|
+
const sem = resolveComponentSemantics(el);
|
|
291
436
|
const aria = el.getAttribute("aria-label");
|
|
292
437
|
if (aria && aria.trim()) return aria.trim().slice(0, 60);
|
|
293
438
|
const labelledby = el.getAttribute("aria-labelledby");
|
|
@@ -296,16 +441,17 @@ function __walkerCore() {
|
|
|
296
441
|
if (t) return t.slice(0, 60);
|
|
297
442
|
}
|
|
298
443
|
if (el.id) {
|
|
299
|
-
const t = document.querySelector(`label[for="${
|
|
444
|
+
const t = document.querySelector(`label[for="${safeEscape(el.id)}"]`)?.textContent?.trim();
|
|
300
445
|
if (t) return t.slice(0, 60);
|
|
301
446
|
}
|
|
447
|
+
if (sem.formLabel) return sem.formLabel.slice(0, 60);
|
|
302
448
|
if ([
|
|
303
449
|
"textbox",
|
|
304
450
|
"searchbox",
|
|
305
451
|
"combobox",
|
|
306
452
|
"spinbutton"
|
|
307
453
|
].includes(role)) {
|
|
308
|
-
const ph = el.getAttribute("placeholder");
|
|
454
|
+
const ph = el.getAttribute("placeholder") || sem.placeholder;
|
|
309
455
|
if (ph && ph.trim()) return ph.trim().slice(0, 60);
|
|
310
456
|
}
|
|
311
457
|
const text = (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ");
|
|
@@ -316,7 +462,7 @@ function __walkerCore() {
|
|
|
316
462
|
}
|
|
317
463
|
/** 稳定 CSS 路径:#id → [data-testid] → nth-of-type 链(≤6 层)。 */
|
|
318
464
|
function cssPath(el) {
|
|
319
|
-
if (el.id) return `#${
|
|
465
|
+
if (el.id) return `#${safeEscape(el.id)}`;
|
|
320
466
|
const testid = el.getAttribute("data-testid") ?? el.getAttribute("data-test-id");
|
|
321
467
|
if (testid) return `[data-testid="${testid.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"]`;
|
|
322
468
|
const parts = [];
|
|
@@ -347,6 +493,7 @@ function __walkerCore() {
|
|
|
347
493
|
* 拾取指纹(pick hint)、录制回声(recorder payload)共用同一份字段语义。
|
|
348
494
|
*/
|
|
349
495
|
function describeElement(el) {
|
|
496
|
+
const sem = resolveComponentSemantics(el);
|
|
350
497
|
const role = roleOf(el);
|
|
351
498
|
const testid = el.getAttribute("data-testid") ?? el.getAttribute("data-test-id") ?? "";
|
|
352
499
|
return {
|
|
@@ -357,7 +504,11 @@ function __walkerCore() {
|
|
|
357
504
|
id: el.id ?? "",
|
|
358
505
|
tag: el.tagName.toLowerCase(),
|
|
359
506
|
text: (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60),
|
|
360
|
-
visible: isVisible(el)
|
|
507
|
+
visible: isVisible(el),
|
|
508
|
+
...sem.library ? { library: sem.library } : {},
|
|
509
|
+
...sem.component ? { component: sem.component } : {},
|
|
510
|
+
...sem.sourceFile ? { sourceFile: sem.sourceFile } : {},
|
|
511
|
+
...sem.formLabel ? { formLabel: sem.formLabel } : {}
|
|
361
512
|
};
|
|
362
513
|
}
|
|
363
514
|
return {
|
|
@@ -368,7 +519,8 @@ function __walkerCore() {
|
|
|
368
519
|
nameOf,
|
|
369
520
|
cssPath,
|
|
370
521
|
isVisible,
|
|
371
|
-
describeElement
|
|
522
|
+
describeElement,
|
|
523
|
+
resolveComponentSemantics
|
|
372
524
|
};
|
|
373
525
|
}
|
|
374
526
|
/** walker 核心的序列化源码(模块加载期取一次;tsdown/vitest 均原样保留)。 */
|
|
@@ -1543,16 +1695,22 @@ const buildAriaTree = injectWalker((payload, w) => {
|
|
|
1543
1695
|
if (INTERACTIVE.has(role)) {
|
|
1544
1696
|
if (!isVisible(el)) return;
|
|
1545
1697
|
const name = nameOf(el, role);
|
|
1698
|
+
const sem = w.resolveComponentSemantics(el);
|
|
1546
1699
|
const ref = refs.length < payload.maxRefs ? `e${refs.length + 1}` : "";
|
|
1547
1700
|
if (ref !== "") refs.push({
|
|
1548
1701
|
ref,
|
|
1549
1702
|
role,
|
|
1550
1703
|
name,
|
|
1551
|
-
selector: cssPath(el)
|
|
1704
|
+
selector: cssPath(el),
|
|
1705
|
+
...sem.library ? { library: sem.library } : {},
|
|
1706
|
+
...sem.component ? { component: sem.component } : {},
|
|
1707
|
+
...sem.sourceFile ? { sourceFile: sem.sourceFile } : {}
|
|
1552
1708
|
});
|
|
1553
1709
|
const expanded = el.getAttribute("aria-expanded");
|
|
1554
1710
|
const disabled = el.disabled === true || el.getAttribute("aria-disabled") === "true";
|
|
1555
|
-
|
|
1711
|
+
const compLabel = sem.component ? ` [comp=${sem.component}]` : "";
|
|
1712
|
+
const srcLabel = sem.sourceFile ? ` [src=${sem.sourceFile.split("/").pop()}]` : "";
|
|
1713
|
+
lines.push(`${" ".repeat(depth)}- ${role}${name !== "" ? ` "${name.replace(/"/g, "'")}"` : ""}${ref !== "" ? ` [ref=${ref}]` : ""}${compLabel}${srcLabel}${expanded !== null ? ` [expanded=${expanded}]` : ""}${disabled ? " [disabled]" : ""}`);
|
|
1556
1714
|
return;
|
|
1557
1715
|
}
|
|
1558
1716
|
if (STRUCTURAL.has(role)) {
|
|
@@ -1592,7 +1750,7 @@ const buildAriaTree = injectWalker((payload, w) => {
|
|
|
1592
1750
|
* 构建期拼装注入),语义天然一致,不再需要手工同步副本。
|
|
1593
1751
|
*/
|
|
1594
1752
|
const findInteractive = injectWalker((payload, w) => {
|
|
1595
|
-
const { INTERACTIVE, STRUCTURAL, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible } = w;
|
|
1753
|
+
const { INTERACTIVE, STRUCTURAL, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible, resolveComponentSemantics } = w;
|
|
1596
1754
|
/** 最近的结构化祖先(区域上下文,帮助区分多命中)。 */
|
|
1597
1755
|
function ancestorsOf(el) {
|
|
1598
1756
|
const out = [];
|
|
@@ -1607,7 +1765,10 @@ const findInteractive = injectWalker((payload, w) => {
|
|
|
1607
1765
|
}
|
|
1608
1766
|
return out;
|
|
1609
1767
|
}
|
|
1610
|
-
const q = payload.query.trim().toLowerCase();
|
|
1768
|
+
const q = (payload.query || "").trim().toLowerCase();
|
|
1769
|
+
const filterComp = (payload.component || "").trim().toLowerCase();
|
|
1770
|
+
const filterLib = (payload.library || "").trim().toLowerCase();
|
|
1771
|
+
const filterFile = (payload.file || "").trim().toLowerCase();
|
|
1611
1772
|
const matches = [];
|
|
1612
1773
|
const candidates = [];
|
|
1613
1774
|
let scanned = 0;
|
|
@@ -1624,13 +1785,30 @@ const findInteractive = injectWalker((payload, w) => {
|
|
|
1624
1785
|
if (INTERACTIVE.has(role)) {
|
|
1625
1786
|
if (!isVisible(el)) continue;
|
|
1626
1787
|
const name = nameOf(el, role);
|
|
1788
|
+
const sem = resolveComponentSemantics(el);
|
|
1627
1789
|
const idOrTestid = el.id || el.getAttribute("data-testid") || "";
|
|
1628
|
-
let
|
|
1629
|
-
if (
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1790
|
+
let filterPass = true;
|
|
1791
|
+
if (filterComp) {
|
|
1792
|
+
const comp = (sem.component || "").toLowerCase();
|
|
1793
|
+
if (filterComp.startsWith("*")) filterPass = comp.endsWith(filterComp.slice(1));
|
|
1794
|
+
else if (filterComp.endsWith("*")) filterPass = comp.startsWith(filterComp.slice(0, -1));
|
|
1795
|
+
else filterPass = comp === filterComp;
|
|
1632
1796
|
}
|
|
1633
|
-
if (
|
|
1797
|
+
if (filterPass && filterLib) filterPass = (sem.library || "").toLowerCase() === filterLib;
|
|
1798
|
+
if (filterPass && filterFile) filterPass = (sem.sourceFile || "").toLowerCase().includes(filterFile);
|
|
1799
|
+
if (!filterPass) continue;
|
|
1800
|
+
let hit = false;
|
|
1801
|
+
if (q) {
|
|
1802
|
+
if (name !== "") {
|
|
1803
|
+
const n = name.toLowerCase();
|
|
1804
|
+
hit = payload.exact ? n === q : n.includes(q);
|
|
1805
|
+
}
|
|
1806
|
+
if (!hit && idOrTestid !== "" && idOrTestid.toLowerCase() === q) hit = true;
|
|
1807
|
+
if (!hit && sem.formLabel && sem.formLabel.toLowerCase().includes(q)) hit = true;
|
|
1808
|
+
if (!hit && sem.placeholder && sem.placeholder.toLowerCase().includes(q)) hit = true;
|
|
1809
|
+
if (!hit && sem.component && sem.component.toLowerCase() === q) hit = true;
|
|
1810
|
+
if (!hit && sem.sourceFile && sem.sourceFile.toLowerCase().includes(q)) hit = true;
|
|
1811
|
+
} else hit = true;
|
|
1634
1812
|
scanned += 1;
|
|
1635
1813
|
if (name !== "" && candidates.length < 50) candidates.push({
|
|
1636
1814
|
role,
|
|
@@ -1640,7 +1818,11 @@ const findInteractive = injectWalker((payload, w) => {
|
|
|
1640
1818
|
role,
|
|
1641
1819
|
name,
|
|
1642
1820
|
selector: cssPath(el),
|
|
1643
|
-
ancestors: ancestorsOf(el)
|
|
1821
|
+
ancestors: ancestorsOf(el),
|
|
1822
|
+
...sem.library ? { library: sem.library } : {},
|
|
1823
|
+
...sem.component ? { component: sem.component } : {},
|
|
1824
|
+
...sem.sourceFile ? { sourceFile: sem.sourceFile } : {},
|
|
1825
|
+
...sem.formLabel ? { formLabel: sem.formLabel } : {}
|
|
1644
1826
|
});
|
|
1645
1827
|
continue;
|
|
1646
1828
|
}
|
|
@@ -1706,7 +1888,10 @@ function assignRefs(existing, matches, maxRefs) {
|
|
|
1706
1888
|
merged.set(ref, {
|
|
1707
1889
|
role: m.role,
|
|
1708
1890
|
name: m.name,
|
|
1709
|
-
selector: m.selector
|
|
1891
|
+
selector: m.selector,
|
|
1892
|
+
...m.library ? { library: m.library } : {},
|
|
1893
|
+
...m.component ? { component: m.component } : {},
|
|
1894
|
+
...m.sourceFile ? { sourceFile: m.sourceFile } : {}
|
|
1710
1895
|
});
|
|
1711
1896
|
withRefs.push({
|
|
1712
1897
|
ref,
|
|
@@ -1720,32 +1905,42 @@ function assignRefs(existing, matches, maxRefs) {
|
|
|
1720
1905
|
};
|
|
1721
1906
|
}
|
|
1722
1907
|
/**
|
|
1723
|
-
* 语义元素定位:按名称(或 id/data-testid)在可交互元素中查找,只返回命中的
|
|
1908
|
+
* 语义元素定位:按名称(或 id/data-testid/组件名/源码文件)在可交互元素中查找,只返回命中的
|
|
1724
1909
|
* ref + 区域上下文,不输出全量 aria 树 —— 大页面定位单个目标时 token 开销
|
|
1725
1910
|
* 远小于 browser_snapshot { mode: "aria" }。返回的 ref 可直接用于
|
|
1726
1911
|
* click/type/select/press。零命中不抛错,附最相近候选供二次定位。
|
|
1727
1912
|
*/
|
|
1728
1913
|
async function findTool(config, args) {
|
|
1729
1914
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
1730
|
-
|
|
1915
|
+
const component = typeof args.component === "string" ? args.component.trim() : "";
|
|
1916
|
+
const library = typeof args.library === "string" ? args.library.trim() : "";
|
|
1917
|
+
const file = typeof args.file === "string" ? args.file.trim() : "";
|
|
1918
|
+
if (!query && !component && !file) throw new Error("缺少定位参数: 请提供 query(元素名称/id/data-testid), 或 component(如 ASelect/ElSelect), 或 file(源码文件名)");
|
|
1731
1919
|
const page = await getPage(config);
|
|
1732
1920
|
const maxRefs = Math.min(Math.max(Number(args.maxRefs ?? config.maxRefs ?? 200) || 200, 1), 500);
|
|
1733
1921
|
const data = await page.evaluate(findInteractive, {
|
|
1734
1922
|
query,
|
|
1735
1923
|
exact: args.exact === true,
|
|
1736
1924
|
scope: typeof args.scope === "string" && args.scope ? args.scope : "",
|
|
1737
|
-
limit: 20
|
|
1925
|
+
limit: 20,
|
|
1926
|
+
component,
|
|
1927
|
+
library,
|
|
1928
|
+
file
|
|
1738
1929
|
});
|
|
1739
1930
|
const { withRefs, merged, truncated } = assignRefs(pageRefsOf(page), data.matches, maxRefs);
|
|
1740
1931
|
setPageRefs(page, merged);
|
|
1741
|
-
const suggestions = withRefs.length === 0 ? topSuggestions(query, data.candidates) : [];
|
|
1932
|
+
const suggestions = withRefs.length === 0 ? topSuggestions(query || component || file, data.candidates) : [];
|
|
1742
1933
|
return {
|
|
1743
1934
|
found: withRefs.length,
|
|
1744
|
-
matches: withRefs.slice(0, 10).map(({ ref, role, name, ancestors }) => ({
|
|
1935
|
+
matches: withRefs.slice(0, 10).map(({ ref, role, name, ancestors, library, component, sourceFile, formLabel }) => ({
|
|
1745
1936
|
ref,
|
|
1746
1937
|
role,
|
|
1747
1938
|
name,
|
|
1748
|
-
ancestors
|
|
1939
|
+
ancestors,
|
|
1940
|
+
...library ? { library } : {},
|
|
1941
|
+
...component ? { component } : {},
|
|
1942
|
+
...sourceFile ? { sourceFile } : {},
|
|
1943
|
+
...formLabel ? { formLabel } : {}
|
|
1749
1944
|
})),
|
|
1750
1945
|
...suggestions.length > 0 ? { suggestions } : {},
|
|
1751
1946
|
...truncated ? { truncated } : {},
|
|
@@ -1975,10 +2170,12 @@ async function pressTool(config, args) {
|
|
|
1975
2170
|
...await pageIdentity(page)
|
|
1976
2171
|
};
|
|
1977
2172
|
}
|
|
1978
|
-
/** 组件库下拉框的可见选项选择器并集(antd / element-plus / 通用 ARIA listbox)。 */
|
|
2173
|
+
/** 组件库下拉框的可见选项选择器并集(antd / element-plus / naive-ui / arco-design / 通用 ARIA listbox)。 */
|
|
1979
2174
|
const DROPDOWN_OPTION_SELECTOR = [
|
|
1980
2175
|
".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option",
|
|
1981
2176
|
".el-select-dropdown__item",
|
|
2177
|
+
".n-base-select-option",
|
|
2178
|
+
".arco-select-option",
|
|
1982
2179
|
"[role='listbox'] [role='option']"
|
|
1983
2180
|
].join(", ");
|
|
1984
2181
|
/** 给本身无超时的 Playwright 调用(evaluate/allTextContents)加有界保护,防页面主线程忙时无限挂起。 */
|
|
@@ -5340,7 +5537,7 @@ function registerTools(ctx, config) {
|
|
|
5340
5537
|
}));
|
|
5341
5538
|
register(definePlainTool({
|
|
5342
5539
|
name: "browser_find",
|
|
5343
|
-
description: "按名称/id/data-testid
|
|
5540
|
+
description: "按名称/id/data-testid/组件名/源码文件语义定位页面元素,只返回命中的 ref + 区域上下文,不输出全量 aria 树 —— 在大页面定位单个操作目标时比 browser_snapshot { mode: \"aria\" } 省 95% 以上 token。原生支持 Ant Design Vue / Element Plus / Naive UI 等组件库与 @xiaou66/vite-plugin-vue-mcp-next 源码映射。返回的 ref 可直接传给 browser_click / browser_type / browser_select / browser_press。默认包含匹配(query 是名称子串,忽略大小写;exact:true 精确匹配);scope 限定查找容器;多命中全部返回并附 ancestors 区域链供二次定位;零命中不抛错,返回最相近的 suggestions 候选。",
|
|
5344
5541
|
parameters: {
|
|
5345
5542
|
type: "object",
|
|
5346
5543
|
properties: {
|
|
@@ -5355,6 +5552,18 @@ function registerTools(ctx, config) {
|
|
|
5355
5552
|
scope: {
|
|
5356
5553
|
type: "string",
|
|
5357
5554
|
description: "可选:限定查找的容器 CSS 选择器(如 '#login-form')。"
|
|
5555
|
+
},
|
|
5556
|
+
component: {
|
|
5557
|
+
type: "string",
|
|
5558
|
+
description: "可选:按组件名查找(如 'ASelect', 'ElSelect', '*Select')。"
|
|
5559
|
+
},
|
|
5560
|
+
library: {
|
|
5561
|
+
type: "string",
|
|
5562
|
+
description: "可选:按组件库筛选(如 'ant-design-vue', 'element-plus')。"
|
|
5563
|
+
},
|
|
5564
|
+
file: {
|
|
5565
|
+
type: "string",
|
|
5566
|
+
description: "可选:按 Vue SFC 源码文件名筛选(如 'Login.vue')。"
|
|
5358
5567
|
}
|
|
5359
5568
|
},
|
|
5360
5569
|
required: ["query"],
|
|
@@ -5376,7 +5585,11 @@ function registerTools(ctx, config) {
|
|
|
5376
5585
|
ancestors: {
|
|
5377
5586
|
type: "array",
|
|
5378
5587
|
items: { type: "string" }
|
|
5379
|
-
}
|
|
5588
|
+
},
|
|
5589
|
+
library: { type: "string" },
|
|
5590
|
+
component: { type: "string" },
|
|
5591
|
+
sourceFile: { type: "string" },
|
|
5592
|
+
formLabel: { type: "string" }
|
|
5380
5593
|
},
|
|
5381
5594
|
required: [
|
|
5382
5595
|
"ref",
|
|
@@ -53,6 +53,10 @@ export declare function assignRefs(existing: Map<string, RefEntry>, matches: {
|
|
|
53
53
|
name: string;
|
|
54
54
|
selector: string;
|
|
55
55
|
ancestors: string[];
|
|
56
|
+
library?: string;
|
|
57
|
+
component?: string;
|
|
58
|
+
sourceFile?: string;
|
|
59
|
+
formLabel?: string;
|
|
56
60
|
}[], maxRefs: number): {
|
|
57
61
|
withRefs: {
|
|
58
62
|
ref: string;
|
|
@@ -60,12 +64,16 @@ export declare function assignRefs(existing: Map<string, RefEntry>, matches: {
|
|
|
60
64
|
name: string;
|
|
61
65
|
selector: string;
|
|
62
66
|
ancestors: string[];
|
|
67
|
+
library?: string;
|
|
68
|
+
component?: string;
|
|
69
|
+
sourceFile?: string;
|
|
70
|
+
formLabel?: string;
|
|
63
71
|
}[];
|
|
64
72
|
merged: Map<string, RefEntry>;
|
|
65
73
|
truncated: boolean;
|
|
66
74
|
};
|
|
67
75
|
/**
|
|
68
|
-
* 语义元素定位:按名称(或 id/data-testid)在可交互元素中查找,只返回命中的
|
|
76
|
+
* 语义元素定位:按名称(或 id/data-testid/组件名/源码文件)在可交互元素中查找,只返回命中的
|
|
69
77
|
* ref + 区域上下文,不输出全量 aria 树 —— 大页面定位单个目标时 token 开销
|
|
70
78
|
* 远小于 browser_snapshot { mode: "aria" }。返回的 ref 可直接用于
|
|
71
79
|
* click/type/select/press。零命中不抛错,附最相近候选供二次定位。
|
|
@@ -82,6 +90,10 @@ export declare function findTool(config: BrowserConfig, args: Record<string, any
|
|
|
82
90
|
}[] | undefined;
|
|
83
91
|
found: number;
|
|
84
92
|
matches: {
|
|
93
|
+
formLabel?: string | undefined;
|
|
94
|
+
sourceFile?: string | undefined;
|
|
95
|
+
component?: string | undefined;
|
|
96
|
+
library?: string | undefined;
|
|
85
97
|
ref: string;
|
|
86
98
|
role: string;
|
|
87
99
|
name: string;
|
|
@@ -104,6 +104,12 @@ export interface RefEntry {
|
|
|
104
104
|
name: string;
|
|
105
105
|
/** 生成快照时计算的稳定 CSS 路径(优先 #id / [data-testid] / nth-of-type 链)。 */
|
|
106
106
|
selector: string;
|
|
107
|
+
/** 组件库标识(如 ant-design-vue, element-plus) */
|
|
108
|
+
library?: string;
|
|
109
|
+
/** 组件名(如 ASelect, ElSelect) */
|
|
110
|
+
component?: string;
|
|
111
|
+
/** 源码位置坐标(@xiaou66/vite-plugin-vue-mcp-next 或 inspector 注入,如 src/views/Login.vue:24:5) */
|
|
112
|
+
sourceFile?: string;
|
|
107
113
|
}
|
|
108
114
|
/** 录制时采集的选择器指纹(回放自愈的降级候选来源)。 */
|
|
109
115
|
export interface SelectorHint {
|
|
@@ -16,6 +16,18 @@ declare function __walkerCore(): {
|
|
|
16
16
|
tag: string;
|
|
17
17
|
text: string;
|
|
18
18
|
visible: boolean;
|
|
19
|
+
library?: string;
|
|
20
|
+
component?: string;
|
|
21
|
+
sourceFile?: string;
|
|
22
|
+
formLabel?: string;
|
|
23
|
+
};
|
|
24
|
+
resolveComponentSemantics: (el: Element) => {
|
|
25
|
+
library: string;
|
|
26
|
+
component: string;
|
|
27
|
+
sourceFile: string;
|
|
28
|
+
role: string;
|
|
29
|
+
formLabel: string;
|
|
30
|
+
placeholder: string;
|
|
19
31
|
};
|
|
20
32
|
};
|
|
21
33
|
/** walker 核心注入后暴露给 entry 的 API(类型层)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-view",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "DSH 插件:共享 Playwright 浏览器自动化(36 个 browser_* 工具,含语义元素查找、批处理、视觉回归、选择器自愈、参数化/数据驱动回放、失败自动重试、结构化抽取与字段建议、增量快照历史版本、回归报告与隔离 context)+ 人机协作(人工接管等待、敏感操作审批(confirmMode:auto 宿主层硬拦截)、人机混合录制、面板一键重跑、面板双向化:操作预告高亮/内嵌报告)+ 实时画面面板与录制管理。仅依赖 playwright-core,不依赖任何 @deepseek-ai 运行时包。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|