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 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="${CSS.escape(el.id)}"]`)?.textContent?.trim();
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 `#${CSS.escape(el.id)}`;
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
- lines.push(`${" ".repeat(depth)}- ${role}${name !== "" ? ` "${name.replace(/"/g, "'")}"` : ""}${ref !== "" ? ` [ref=${ref}]` : ""}${expanded !== null ? ` [expanded=${expanded}]` : ""}${disabled ? " [disabled]" : ""}`);
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 hit = false;
1629
- if (name !== "") {
1630
- const n = name.toLowerCase();
1631
- hit = payload.exact ? n === q : n.includes(q);
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 (!hit && idOrTestid !== "" && idOrTestid.toLowerCase() === q) hit = true;
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
- if (!query) throw new Error("缺少 query: 要找的元素名称(如「提交」「登录」,或 id/data-testid)");
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 语义定位页面元素,只返回命中的 ref + 区域上下文,不输出全量 aria 树 —— 在大页面定位单个操作目标时比 browser_snapshot { mode: \"aria\" } 省 95% 以上 token。返回的 ref 可直接传给 browser_click / browser_type / browser_select / browser_press。默认包含匹配(query 是名称子串,忽略大小写;exact:true 精确匹配);scope 限定查找容器;多命中全部返回并附 ancestors 区域链供二次定位;零命中不抛错,返回最相近的 suggestions 候选。",
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.0",
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",