dsh-browser-view 0.7.0 → 0.7.2
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 +431 -68
- package/lib/types/browser/session.d.ts +5 -2
- package/lib/types/browser/tools/interaction.d.ts +2 -0
- package/lib/types/browser/tools/navigation.d.ts +4 -1
- package/lib/types/browser/tools/testing.d.ts +23 -1
- package/lib/types/browser/types.d.ts +6 -0
- package/lib/types/browser/walker.d.ts +1 -0
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -683,6 +683,25 @@ function __walkerCore() {
|
|
|
683
683
|
visible: isVisible(el)
|
|
684
684
|
};
|
|
685
685
|
}
|
|
686
|
+
/** 疑似可点击 generic 元素(自定义组件的 span+click 等):无 ARIA 角色或显式
|
|
687
|
+
* presentational(presentation/none)但具备 cursor:pointer / onclick 属性 /
|
|
688
|
+
* tabindex≥0 特征且有非空文本(≤60 字符)。仅供扫描语义消费(what 语义扫描与
|
|
689
|
+
* SoM 标徽共用,防判定分叉);aria 快照不纳入(避免噪音)——这是记录在案的
|
|
690
|
+
* 设计分叉,见产品文档 03。 */
|
|
691
|
+
function isProbablyClickable(el) {
|
|
692
|
+
const role = roleOf(el);
|
|
693
|
+
if (role !== "" && role !== "presentation" && role !== "none") return false;
|
|
694
|
+
const text = (el.innerText || el.textContent || "").trim();
|
|
695
|
+
if (text === "" || text.length > 60) return false;
|
|
696
|
+
if (el.hasAttribute("onclick")) return true;
|
|
697
|
+
const ti = el.tabIndex;
|
|
698
|
+
if (typeof ti === "number" && ti >= 0) return true;
|
|
699
|
+
try {
|
|
700
|
+
return getComputedStyle(el).cursor === "pointer";
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
686
705
|
return {
|
|
687
706
|
INTERACTIVE,
|
|
688
707
|
STRUCTURAL,
|
|
@@ -694,7 +713,8 @@ function __walkerCore() {
|
|
|
694
713
|
isElementInViewport,
|
|
695
714
|
describeElement,
|
|
696
715
|
composedParent,
|
|
697
|
-
deepChildren
|
|
716
|
+
deepChildren,
|
|
717
|
+
isProbablyClickable
|
|
698
718
|
};
|
|
699
719
|
}
|
|
700
720
|
/** walker 核心的序列化源码(模块加载期取一次;tsdown/vitest 均原样保留)。 */
|
|
@@ -1650,7 +1670,7 @@ async function fillFormFieldHelper(page, field, scope) {
|
|
|
1650
1670
|
* 多 token(空格/中英混排拆分)按 AND 命中 —— 单 token 行为与旧子串匹配等价。
|
|
1651
1671
|
*/
|
|
1652
1672
|
const scanInteractive = injectWalker((payload, w) => {
|
|
1653
|
-
const { INTERACTIVE, STRUCTURAL, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible, deepChildren, composedParent } = w;
|
|
1673
|
+
const { INTERACTIVE, STRUCTURAL, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible, deepChildren, composedParent, isProbablyClickable } = w;
|
|
1654
1674
|
function resolveScopeRoot(scopeStr) {
|
|
1655
1675
|
if (!scopeStr) return document.body;
|
|
1656
1676
|
const wantsVisible = /:visible\b/.test(scopeStr);
|
|
@@ -1759,6 +1779,39 @@ const scanInteractive = injectWalker((payload, w) => {
|
|
|
1759
1779
|
});
|
|
1760
1780
|
continue;
|
|
1761
1781
|
}
|
|
1782
|
+
if (isVisible(el) && isProbablyClickable(el)) {
|
|
1783
|
+
const text = (el.innerText || el.textContent || "").trim().slice(0, 60);
|
|
1784
|
+
const n = text.toLowerCase();
|
|
1785
|
+
const nStr = n.replace(/\s+/g, "");
|
|
1786
|
+
const legacyHit = payload.exact ? n === q || nStr === qStripped : n.includes(q) || qStripped !== "" && nStr.includes(qStripped);
|
|
1787
|
+
let tokenScore = 0;
|
|
1788
|
+
let tokenHit = true;
|
|
1789
|
+
for (const t of tokens) {
|
|
1790
|
+
const tStr = t.replace(/\s+/g, "");
|
|
1791
|
+
if (n.includes(t) || tStr !== "" && nStr.includes(tStr)) tokenScore += 3;
|
|
1792
|
+
else {
|
|
1793
|
+
tokenHit = false;
|
|
1794
|
+
break;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (legacyHit && n === q) tokenScore += 50;
|
|
1798
|
+
const hit = payload.exact ? legacyHit : legacyHit || tokens.length > 0 && tokenHit;
|
|
1799
|
+
if (candidates.length < 50) candidates.push({
|
|
1800
|
+
role: "clickable",
|
|
1801
|
+
name: text,
|
|
1802
|
+
selector: cssPath(el)
|
|
1803
|
+
});
|
|
1804
|
+
if (hit && hits.length < 100) hits.push({
|
|
1805
|
+
role: "clickable",
|
|
1806
|
+
name: text,
|
|
1807
|
+
selector: cssPath(el),
|
|
1808
|
+
text,
|
|
1809
|
+
visible: true,
|
|
1810
|
+
inDialog: dialogSel !== "" ? Boolean(el.closest(dialogSel)) : false,
|
|
1811
|
+
score: (legacyHit ? 100 : 0) + tokenScore - 200,
|
|
1812
|
+
ancestors: ancestorsOf(el)
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1762
1815
|
for (const c of deepChildren(el)) if (!SKIP_TAGS.has(c.tagName.toLowerCase())) stack.push(c);
|
|
1763
1816
|
}
|
|
1764
1817
|
hits.sort((a, b) => Number(b.inDialog) - Number(a.inDialog) || b.score - a.score);
|
|
@@ -1838,7 +1891,7 @@ async function resolveSemanticTarget(page, what, opts = {}) {
|
|
|
1838
1891
|
* 同一 walker 语义。上限 300 条,超出截断(超大页面重锚定按命中部分处理)。
|
|
1839
1892
|
*/
|
|
1840
1893
|
const scanAllInteractive = injectWalker((payload, w) => {
|
|
1841
|
-
const { INTERACTIVE, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible, isElementInViewport, deepChildren } = w;
|
|
1894
|
+
const { INTERACTIVE, SKIP_TAGS, roleOf, nameOf, cssPath, isVisible, isElementInViewport, deepChildren, isProbablyClickable } = w;
|
|
1842
1895
|
const out = [];
|
|
1843
1896
|
let truncated = false;
|
|
1844
1897
|
const snapScroll = {
|
|
@@ -1882,6 +1935,24 @@ const scanAllInteractive = injectWalker((payload, w) => {
|
|
|
1882
1935
|
});
|
|
1883
1936
|
continue;
|
|
1884
1937
|
}
|
|
1938
|
+
if (out.length < 300 && isVisible(el) && isProbablyClickable(el)) {
|
|
1939
|
+
const text = (el.innerText || el.textContent || "").trim();
|
|
1940
|
+
const rect = el.getBoundingClientRect();
|
|
1941
|
+
out.push({
|
|
1942
|
+
role: "clickable",
|
|
1943
|
+
name: text.slice(0, 60),
|
|
1944
|
+
selector: cssPath(el),
|
|
1945
|
+
...isElementInViewport(el, 0) ? {
|
|
1946
|
+
rect: {
|
|
1947
|
+
x: Math.round(rect.left),
|
|
1948
|
+
y: Math.round(rect.top),
|
|
1949
|
+
w: Math.round(rect.width),
|
|
1950
|
+
h: Math.round(rect.height)
|
|
1951
|
+
},
|
|
1952
|
+
scroll: snapScroll
|
|
1953
|
+
} : {}
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1885
1956
|
for (const c of deepChildren(el)) if (!SKIP_TAGS.has(c.tagName.toLowerCase())) stack.push(c);
|
|
1886
1957
|
}
|
|
1887
1958
|
return {
|
|
@@ -1964,6 +2035,18 @@ function nameSimilar(a, b) {
|
|
|
1964
2035
|
* 具备路径防漂移校验与 getByRole 自愈降级能力。
|
|
1965
2036
|
*/
|
|
1966
2037
|
async function resolveTargetLocator(page, args) {
|
|
2038
|
+
{
|
|
2039
|
+
const present = [];
|
|
2040
|
+
if (args.ref !== void 0 && args.ref !== "") present.push("ref");
|
|
2041
|
+
if (args.mark !== void 0) present.push("mark");
|
|
2042
|
+
if (typeof args.what === "string" && args.what.trim() !== "") present.push("what");
|
|
2043
|
+
if (args.selector !== void 0 && args.selector !== "") present.push("selector");
|
|
2044
|
+
if ((args.rowText !== void 0 || args.row !== void 0) && present.some((k) => k !== "selector")) {
|
|
2045
|
+
const offending = present.filter((k) => k !== "selector");
|
|
2046
|
+
throw new Error(`定位参数冲突: row/rowText 与 ${offending.join("、")} 不能同时使用(行级定位仅可组合 selector 限定行内目标);在行内按意图找按钮请用 row + action/actionText`);
|
|
2047
|
+
}
|
|
2048
|
+
if (present.length >= 2) throw new Error(`定位参数冲突: 同时传了 ${present.join(" 与 ")} —— ref/mark/what/selector 四选一;若要在容器内语义定位,请用 what + scope(容器选择器)`);
|
|
2049
|
+
}
|
|
1967
2050
|
if (args.ref !== void 0 && args.ref !== "") {
|
|
1968
2051
|
const ref = String(args.ref);
|
|
1969
2052
|
const entry = pageRef(page, ref);
|
|
@@ -2159,6 +2242,29 @@ async function resolveTargetLocator(page, args) {
|
|
|
2159
2242
|
visibleCount: 1
|
|
2160
2243
|
};
|
|
2161
2244
|
}
|
|
2245
|
+
{
|
|
2246
|
+
const bridgeHit = await withTimeout(page.evaluate(async (q) => {
|
|
2247
|
+
const b = window.__DSH_BRIDGE__;
|
|
2248
|
+
if (b === void 0 || typeof b.resolveActionTarget !== "function") return null;
|
|
2249
|
+
const targets = await b.resolveActionTarget({ query: q });
|
|
2250
|
+
if (!Array.isArray(targets) || targets.length === 0) return null;
|
|
2251
|
+
const t = targets[0];
|
|
2252
|
+
if (t === null || typeof t !== "object" || typeof t.selector !== "string" || t.selector === "") return null;
|
|
2253
|
+
return { selector: t.selector };
|
|
2254
|
+
}, what), 2500, "bridge 兜底").catch(() => null);
|
|
2255
|
+
const bridgeSelector = bridgeHit !== null && typeof bridgeHit === "object" && typeof bridgeHit.selector === "string" && bridgeHit.selector !== "" ? bridgeHit.selector : null;
|
|
2256
|
+
if (bridgeSelector !== null) {
|
|
2257
|
+
const loc = page.locator(bridgeSelector).first();
|
|
2258
|
+
if (await withTimeout(loc.count(), 1500, "bridge 兜底定位").catch(() => 0) > 0) return {
|
|
2259
|
+
locator: loc,
|
|
2260
|
+
container: page,
|
|
2261
|
+
isIFrame: false,
|
|
2262
|
+
matchedCount: 1,
|
|
2263
|
+
visibleCount: 1,
|
|
2264
|
+
viaBridge: true
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2162
2268
|
const err = new Error(semanticMissMessage(res));
|
|
2163
2269
|
err.semanticMiss = res;
|
|
2164
2270
|
err.locationFailure = {
|
|
@@ -2756,7 +2862,7 @@ async function launchProfile(name, config) {
|
|
|
2756
2862
|
...options,
|
|
2757
2863
|
headless: resolved.headless ?? false
|
|
2758
2864
|
});
|
|
2759
|
-
|
|
2865
|
+
await Promise.race([Promise.all(context.pages().map((p) => p.close().catch(() => {}))), new Promise((r) => setTimeout(r, 3e3))]);
|
|
2760
2866
|
state.browser = null;
|
|
2761
2867
|
state.context = context;
|
|
2762
2868
|
state.persistent = true;
|
|
@@ -2781,11 +2887,30 @@ const namedContexts = /* @__PURE__ */ new Map();
|
|
|
2781
2887
|
function namedContextsOf() {
|
|
2782
2888
|
return namedContexts;
|
|
2783
2889
|
}
|
|
2784
|
-
/**
|
|
2890
|
+
/** 同步摘除已关闭页面(手动关标签/渲染崩溃的僵尸句柄)。
|
|
2891
|
+
* activeId 指向被摘页时置 null 而非自动迁移——静默换页对 click/type 是错页执行,
|
|
2892
|
+
* 由 getPage 报错引导显式恢复(switch / browser_open 自愈)。 */
|
|
2893
|
+
function pruneClosedPages(state) {
|
|
2894
|
+
for (const [id, page] of state.pages) if (page.isClosed()) {
|
|
2895
|
+
state.pages.delete(id);
|
|
2896
|
+
state.tabMeta?.delete(id);
|
|
2897
|
+
if (state.activeId === id) state.activeId = null;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/** 把一个已 attach 的页面注册进 profile 标签表并激活,返回分配的 tab id。
|
|
2901
|
+
* 唯一注册路径:同时挂 close 监听幂等摘除外部关闭的页(否则僵尸句柄会永久占用
|
|
2902
|
+
* activeId,所有工具连报 "Target page...closed" 而 tabs list 仍显示存活)。 */
|
|
2785
2903
|
function registerTabPage(state, page) {
|
|
2786
2904
|
const id = state.counter++;
|
|
2787
2905
|
state.pages.set(id, page);
|
|
2788
2906
|
state.activeId = id;
|
|
2907
|
+
(state.tabMeta ??= /* @__PURE__ */ new Map()).set(id, { createdAt: Date.now() });
|
|
2908
|
+
page.on("close", () => {
|
|
2909
|
+
if (!state.pages.has(id)) return;
|
|
2910
|
+
state.pages.delete(id);
|
|
2911
|
+
state.tabMeta?.delete(id);
|
|
2912
|
+
if (state.activeId === id) state.activeId = null;
|
|
2913
|
+
});
|
|
2789
2914
|
return id;
|
|
2790
2915
|
}
|
|
2791
2916
|
/** 新建一个页面(当前 profile 会话未启动时先启动)。 */
|
|
@@ -2811,34 +2936,42 @@ async function newPage(config) {
|
|
|
2811
2936
|
async function getPage(config) {
|
|
2812
2937
|
const state = stateOf();
|
|
2813
2938
|
if (state.browser === null && state.context === null) await launchProfile(currentProfile, config);
|
|
2939
|
+
pruneClosedPages(state);
|
|
2814
2940
|
if (state.pages.size === 0) {
|
|
2815
2941
|
const page = await newPage(config);
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
state.activeId = id;
|
|
2942
|
+
registerTabPage(state, page);
|
|
2943
|
+
return page;
|
|
2819
2944
|
}
|
|
2820
2945
|
const page = state.pages.get(state.activeId ?? -1);
|
|
2821
|
-
if (page === void 0)
|
|
2946
|
+
if (page === void 0) {
|
|
2947
|
+
const err = /* @__PURE__ */ new Error("dsh-browser: 活动标签已关闭或崩溃;请 browser_tabs { action: \"list\" } 查看存活标签并 switch,或 browser_open 打开目标 URL(将自动新建标签)");
|
|
2948
|
+
err.code = "activeTabClosed";
|
|
2949
|
+
throw err;
|
|
2950
|
+
}
|
|
2822
2951
|
return page;
|
|
2823
2952
|
}
|
|
2824
2953
|
/** 活动页面(可能为 null,不触发启动)。 */
|
|
2825
2954
|
function activePage() {
|
|
2826
2955
|
const state = stateOf();
|
|
2956
|
+
pruneClosedPages(state);
|
|
2827
2957
|
return state.pages.get(state.activeId ?? -1) ?? null;
|
|
2828
2958
|
}
|
|
2829
|
-
/** 投影当前 profile
|
|
2959
|
+
/** 投影当前 profile 的标签列表(先摘除僵尸页,附注册时间)。 */
|
|
2830
2960
|
async function tabList() {
|
|
2831
2961
|
const state = stateOf();
|
|
2962
|
+
pruneClosedPages(state);
|
|
2832
2963
|
const tabs = [];
|
|
2833
2964
|
for (const [id, page] of state.pages) {
|
|
2834
2965
|
let title = "";
|
|
2835
2966
|
try {
|
|
2836
2967
|
title = await page.title();
|
|
2837
2968
|
} catch {}
|
|
2969
|
+
const createdAt = state.tabMeta?.get(id)?.createdAt;
|
|
2838
2970
|
tabs.push({
|
|
2839
2971
|
id,
|
|
2840
2972
|
url: page.url(),
|
|
2841
|
-
title
|
|
2973
|
+
title,
|
|
2974
|
+
...createdAt !== void 0 ? { createdAt } : {}
|
|
2842
2975
|
});
|
|
2843
2976
|
}
|
|
2844
2977
|
return tabs;
|
|
@@ -2851,6 +2984,7 @@ async function closeBrowser() {
|
|
|
2851
2984
|
state.browser = null;
|
|
2852
2985
|
state.context = null;
|
|
2853
2986
|
state.pages = /* @__PURE__ */ new Map();
|
|
2987
|
+
state.tabMeta?.clear();
|
|
2854
2988
|
state.activeId = null;
|
|
2855
2989
|
state.counter = 0;
|
|
2856
2990
|
try {
|
|
@@ -2893,15 +3027,38 @@ async function openTool(configOrArgs, maybeArgs) {
|
|
|
2893
3027
|
const isSingleArg = maybeArgs === void 0;
|
|
2894
3028
|
const config = isSingleArg && typeof configOrArgs?.url === "string" ? {} : configOrArgs;
|
|
2895
3029
|
const args = isSingleArg ? configOrArgs : maybeArgs;
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3030
|
+
const state = stateOf();
|
|
3031
|
+
let recovered;
|
|
3032
|
+
const freshPage = async () => {
|
|
3033
|
+
const p = await newPage(config ?? {});
|
|
3034
|
+
registerTabPage(state, p);
|
|
3035
|
+
recovered = "new-tab";
|
|
3036
|
+
return p;
|
|
3037
|
+
};
|
|
3038
|
+
let page = await getPage(config ?? {}).catch(async (e) => {
|
|
3039
|
+
if (e?.code === "activeTabClosed") return freshPage();
|
|
3040
|
+
throw e;
|
|
2900
3041
|
});
|
|
3042
|
+
if (page.isClosed()) page = await freshPage();
|
|
3043
|
+
try {
|
|
3044
|
+
await page.goto(String(args.url), {
|
|
3045
|
+
waitUntil: args.waitUntil ?? "domcontentloaded",
|
|
3046
|
+
timeout: args.timeoutMs ?? config?.timeoutMs
|
|
3047
|
+
});
|
|
3048
|
+
} catch (e) {
|
|
3049
|
+
if (recovered === void 0 && /has been closed/.test(String(e?.message ?? e))) {
|
|
3050
|
+
page = await freshPage();
|
|
3051
|
+
await page.goto(String(args.url), {
|
|
3052
|
+
waitUntil: args.waitUntil ?? "domcontentloaded",
|
|
3053
|
+
timeout: args.timeoutMs ?? config?.timeoutMs
|
|
3054
|
+
});
|
|
3055
|
+
} else throw e;
|
|
3056
|
+
}
|
|
2901
3057
|
return {
|
|
2902
3058
|
url: page.url(),
|
|
2903
3059
|
title: await page.title(),
|
|
2904
|
-
status: "ok"
|
|
3060
|
+
status: "ok",
|
|
3061
|
+
...recovered !== void 0 ? { recovered } : {}
|
|
2905
3062
|
};
|
|
2906
3063
|
}
|
|
2907
3064
|
async function backTool(config) {
|
|
@@ -3057,9 +3214,7 @@ async function tabsTool(config, args) {
|
|
|
3057
3214
|
};
|
|
3058
3215
|
case "new": {
|
|
3059
3216
|
const page = await newPage(config);
|
|
3060
|
-
const id = state
|
|
3061
|
-
state.pages.set(id, page);
|
|
3062
|
-
state.activeId = id;
|
|
3217
|
+
const id = registerTabPage(state, page);
|
|
3063
3218
|
if (args.url) await page.goto(String(args.url), {
|
|
3064
3219
|
waitUntil: args.waitUntil ?? "domcontentloaded",
|
|
3065
3220
|
timeout: args.timeoutMs ?? config.timeoutMs
|
|
@@ -3089,12 +3244,10 @@ async function tabsTool(config, args) {
|
|
|
3089
3244
|
const id = resolveTabId(args);
|
|
3090
3245
|
const page = state.pages.get(id);
|
|
3091
3246
|
if (page === void 0) throw new Error(`no tab with id ${id}`);
|
|
3092
|
-
await page.close().catch(() => {});
|
|
3093
3247
|
state.pages.delete(id);
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
}
|
|
3248
|
+
state.tabMeta?.delete(id);
|
|
3249
|
+
if (state.activeId === id) state.activeId = null;
|
|
3250
|
+
await page.close().catch(() => {});
|
|
3098
3251
|
return {
|
|
3099
3252
|
action,
|
|
3100
3253
|
activeId: state.activeId ?? -1,
|
|
@@ -3609,7 +3762,7 @@ const removeSomOverlay = () => {
|
|
|
3609
3762
|
document.getElementById("__dsh_som_overlay__")?.remove();
|
|
3610
3763
|
};
|
|
3611
3764
|
const injectSomOverlay = injectWalker((payload, w) => {
|
|
3612
|
-
const { INTERACTIVE, roleOf, nameOf, cssPath, isVisible, isElementInViewport, deepChildren } = w;
|
|
3765
|
+
const { INTERACTIVE, roleOf, nameOf, cssPath, isVisible, isElementInViewport, deepChildren, isProbablyClickable } = w;
|
|
3613
3766
|
const marks = [];
|
|
3614
3767
|
const snapScroll = {
|
|
3615
3768
|
x: window.scrollX,
|
|
@@ -3625,6 +3778,7 @@ const injectSomOverlay = injectWalker((payload, w) => {
|
|
|
3625
3778
|
candidates.push(el);
|
|
3626
3779
|
return;
|
|
3627
3780
|
}
|
|
3781
|
+
if (isVisible(el) && isElementInViewport(el, 0) && isProbablyClickable(el)) candidates.push(el);
|
|
3628
3782
|
for (const c of deepChildren(el)) if (c.tagName !== "SCRIPT" && c.tagName !== "STYLE") collect(c);
|
|
3629
3783
|
}
|
|
3630
3784
|
if (document.body) collect(document.body);
|
|
@@ -3635,8 +3789,9 @@ const injectSomOverlay = injectWalker((payload, w) => {
|
|
|
3635
3789
|
const rect = el.getBoundingClientRect();
|
|
3636
3790
|
if (rect.width < 8 || rect.height < 8) continue;
|
|
3637
3791
|
count += 1;
|
|
3638
|
-
const
|
|
3639
|
-
const
|
|
3792
|
+
const realRole = roleOf(el);
|
|
3793
|
+
const role = realRole === "" ? "clickable" : realRole;
|
|
3794
|
+
const name = realRole === "" ? (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60) : nameOf(el, realRole);
|
|
3640
3795
|
const x = Math.max(0, rect.left);
|
|
3641
3796
|
const y = Math.max(0, rect.top);
|
|
3642
3797
|
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
@@ -3953,6 +4108,22 @@ async function humanType(page, text, options = {}) {
|
|
|
3953
4108
|
}
|
|
3954
4109
|
//#endregion
|
|
3955
4110
|
//#region lib/types/browser/tools/interaction.js
|
|
4111
|
+
/** 多命中可见化:解析层 matchedCount/visibleCount 透传到成功返回,>1 附操作指引。
|
|
4112
|
+
* 防同名字段静默打进被遮罩的背景元素(背景表单 + Modal 同名字段场景)。 */
|
|
4113
|
+
function ambiguityFields(res) {
|
|
4114
|
+
const matched = res.matchedCount;
|
|
4115
|
+
if (matched === void 0) return {};
|
|
4116
|
+
return {
|
|
4117
|
+
matchedCount: matched,
|
|
4118
|
+
...res.visibleCount !== void 0 ? { visibleCount: res.visibleCount } : {},
|
|
4119
|
+
...matched > 1 ? { hint: `命中 ${matched} 个元素(可见 ${res.visibleCount ?? 0}),已操作第 1 个;建议用 scope 限定或 browser_snapshot 取 ref 消歧` } : {}
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
/** strict 模式(对齐 Playwright strict):多命中直接拒绝,要求定位唯一。 */
|
|
4123
|
+
function strictReject(args, res) {
|
|
4124
|
+
if (args.strict !== true || (res.matchedCount ?? 0) <= 1) return null;
|
|
4125
|
+
return /* @__PURE__ */ new Error(`strict 模式拒绝多命中: 命中 ${res.matchedCount} 个元素(可见 ${res.visibleCount ?? 0}),要求定位唯一;请用 scope 限定、更特异的 selector 或 browser_snapshot 取 ref`);
|
|
4126
|
+
}
|
|
3956
4127
|
function isBotSensitiveUrl(url) {
|
|
3957
4128
|
if (!url) return false;
|
|
3958
4129
|
return /login|signin|auth|captcha|challenge|turnstile|security|waf/i.test(url);
|
|
@@ -3982,6 +4153,8 @@ async function clickTool(config, args, exec) {
|
|
|
3982
4153
|
}
|
|
3983
4154
|
let target = targetRes.locator;
|
|
3984
4155
|
const label = String(args.ref ?? args.selector ?? args.mark ?? args.what ?? "");
|
|
4156
|
+
const strictErr = strictReject(args, targetRes);
|
|
4157
|
+
if (strictErr !== null) throw strictErr;
|
|
3985
4158
|
target = await liftTargetIfHiddenInput(target);
|
|
3986
4159
|
const wantEffect = args.effect === true;
|
|
3987
4160
|
let urlBefore = "";
|
|
@@ -4074,17 +4247,24 @@ async function clickTool(config, args, exec) {
|
|
|
4074
4247
|
clicked: String(args.ref ?? args.selector ?? args.mark ?? args.what ?? (args.row ? `row: ${args.row}` : "target")),
|
|
4075
4248
|
effect,
|
|
4076
4249
|
...quiet ? { effectHint: "点击后未观察到任何 DOM/URL/网络变化,可能未生效(被遮挡/事件被吞/前置条件未满足);建议 browser_snapshot 核实或换目标" } : {},
|
|
4250
|
+
...ambiguityFields(targetRes),
|
|
4251
|
+
...targetRes.viaBridge === true ? { viaBridge: true } : {},
|
|
4077
4252
|
...await pageIdentity(page)
|
|
4078
4253
|
};
|
|
4079
4254
|
}
|
|
4080
4255
|
return {
|
|
4081
4256
|
clicked: String(args.ref ?? args.selector ?? args.mark ?? args.what ?? (args.row ? `row: ${args.row}` : "target")),
|
|
4257
|
+
...ambiguityFields(targetRes),
|
|
4258
|
+
...targetRes.viaBridge === true ? { viaBridge: true } : {},
|
|
4082
4259
|
...await pageIdentity(page)
|
|
4083
4260
|
};
|
|
4084
4261
|
}
|
|
4085
4262
|
async function typeTool(config, args) {
|
|
4086
4263
|
const page = await getPage(config);
|
|
4087
|
-
const
|
|
4264
|
+
const targetRes = await resolveTargetLocator(page, args);
|
|
4265
|
+
const target = targetRes.locator;
|
|
4266
|
+
const strictErr = strictReject(args, targetRes);
|
|
4267
|
+
if (strictErr !== null) throw strictErr;
|
|
4088
4268
|
const text = String(args.text ?? "");
|
|
4089
4269
|
const isHuman = args.humanize === true || args.humanize === void 0 && (config.humanize === true || config.humanize === "auto" && isBotSensitiveUrl(page.url()));
|
|
4090
4270
|
if (args.editor === true) {
|
|
@@ -4093,6 +4273,7 @@ async function typeTool(config, args) {
|
|
|
4093
4273
|
typed: true,
|
|
4094
4274
|
editor: true,
|
|
4095
4275
|
selector,
|
|
4276
|
+
...ambiguityFields(targetRes),
|
|
4096
4277
|
...await pageIdentity(page)
|
|
4097
4278
|
};
|
|
4098
4279
|
}
|
|
@@ -4102,9 +4283,11 @@ async function typeTool(config, args) {
|
|
|
4102
4283
|
await humanType(page, text, { speed: config.humanizeSpeed });
|
|
4103
4284
|
} else await target.fill(text, { timeout: args.timeoutMs ?? config.timeoutMs });
|
|
4104
4285
|
} else {
|
|
4286
|
+
const ambiguous = args.strict === true || (targetRes.matchedCount ?? 0) > 1;
|
|
4105
4287
|
try {
|
|
4106
4288
|
await target.click({ timeout: Math.min(args.timeoutMs ?? config.timeoutMs, 5e3) });
|
|
4107
|
-
} catch {
|
|
4289
|
+
} catch (e) {
|
|
4290
|
+
if (ambiguous) throw new Error(`type 定位目标存在歧义(命中 ${targetRes.matchedCount ?? "?"} 个元素)且首个不可交互,已中止;请用 scope 限定或 browser_snapshot 取 ref 消歧;原始错误: ${e instanceof Error ? e.message : String(e)}`);
|
|
4108
4291
|
await target.click({
|
|
4109
4292
|
timeout: 2e3,
|
|
4110
4293
|
force: true
|
|
@@ -4118,6 +4301,7 @@ async function typeTool(config, args) {
|
|
|
4118
4301
|
typed: true,
|
|
4119
4302
|
selector: String(args.ref ?? args.selector),
|
|
4120
4303
|
submit: args.submit === true,
|
|
4304
|
+
...ambiguityFields(targetRes),
|
|
4121
4305
|
...await pageIdentity(page)
|
|
4122
4306
|
};
|
|
4123
4307
|
}
|
|
@@ -4144,8 +4328,18 @@ async function pressTool(config, args) {
|
|
|
4144
4328
|
} else warning = "检测到页面存在活动弹窗 [modal: closeOnEsc],按全局 Escape 可能导致弹窗及其表单被关闭。若需关闭指定元素请传 selector/ref。";
|
|
4145
4329
|
}
|
|
4146
4330
|
}
|
|
4147
|
-
if (args.selector !== void 0 || args.ref !== void 0 || typeof args.what === "string" && args.what.trim() !== "")
|
|
4148
|
-
|
|
4331
|
+
if (args.selector !== void 0 || args.ref !== void 0 || typeof args.what === "string" && args.what.trim() !== "") {
|
|
4332
|
+
const targetRes = await resolveTargetLocator(page, args);
|
|
4333
|
+
const strictErr = strictReject(args, targetRes);
|
|
4334
|
+
if (strictErr !== null) throw strictErr;
|
|
4335
|
+
await targetRes.locator.press(key, { timeout: args.timeoutMs ?? config.timeoutMs });
|
|
4336
|
+
return {
|
|
4337
|
+
pressed: key,
|
|
4338
|
+
...warning ? { warning } : {},
|
|
4339
|
+
...ambiguityFields(targetRes),
|
|
4340
|
+
...await pageIdentity(page)
|
|
4341
|
+
};
|
|
4342
|
+
} else await page.keyboard.press(key);
|
|
4149
4343
|
return {
|
|
4150
4344
|
pressed: key,
|
|
4151
4345
|
...warning ? { warning } : {},
|
|
@@ -4158,8 +4352,11 @@ async function pressTool(config, args) {
|
|
|
4158
4352
|
*/
|
|
4159
4353
|
async function selectTool(config, args) {
|
|
4160
4354
|
const page = await getPage(config);
|
|
4161
|
-
const
|
|
4162
|
-
const
|
|
4355
|
+
const targetRes = await resolveTargetLocator(page, args);
|
|
4356
|
+
const target = targetRes.locator;
|
|
4357
|
+
const strictErr = strictReject(args, targetRes);
|
|
4358
|
+
if (strictErr !== null) throw strictErr;
|
|
4359
|
+
const label = String(args.ref ?? args.selector ?? args.what ?? "");
|
|
4163
4360
|
const want = String(args.option ?? "");
|
|
4164
4361
|
if (!want) throw new Error("select 需要 option(选项文本)");
|
|
4165
4362
|
return {
|
|
@@ -4173,6 +4370,7 @@ async function selectTool(config, args) {
|
|
|
4173
4370
|
clear: args.clear === true
|
|
4174
4371
|
})).selected,
|
|
4175
4372
|
selector: label,
|
|
4373
|
+
...ambiguityFields(targetRes),
|
|
4176
4374
|
...await pageIdentity(page)
|
|
4177
4375
|
};
|
|
4178
4376
|
}
|
|
@@ -4482,7 +4680,53 @@ async function checkOnce(page, cond) {
|
|
|
4482
4680
|
detail: `eval 执行失败: ${e instanceof Error ? e.message : String(e)}`
|
|
4483
4681
|
};
|
|
4484
4682
|
}
|
|
4485
|
-
|
|
4683
|
+
if (cond.bridge !== void 0 && typeof cond.bridge === "object") {
|
|
4684
|
+
const spec = cond.bridge;
|
|
4685
|
+
const method = String(spec.method ?? "");
|
|
4686
|
+
const trunc = (v) => {
|
|
4687
|
+
const s = JSON.stringify(v) ?? String(v);
|
|
4688
|
+
return s.length > 500 ? `${s.slice(0, 500)}…(截断)` : s;
|
|
4689
|
+
};
|
|
4690
|
+
if (method === "") return {
|
|
4691
|
+
ok: false,
|
|
4692
|
+
detail: "bridge 条件需要 method(__DSH_BRIDGE__ 方法名)"
|
|
4693
|
+
};
|
|
4694
|
+
try {
|
|
4695
|
+
const value = await page.evaluate(async (payload) => {
|
|
4696
|
+
const bridge = window.__DSH_BRIDGE__;
|
|
4697
|
+
if (bridge === void 0 || bridge === null) throw new Error("NO_BRIDGE:页面无 __DSH_BRIDGE__(未接入 vite-plugin-dsh-bridge?)");
|
|
4698
|
+
if (typeof bridge[payload.method] !== "function") throw new Error(`NO_BRIDGE:__DSH_BRIDGE__ 无方法 ${payload.method}`);
|
|
4699
|
+
return await bridge[payload.method](...payload.args);
|
|
4700
|
+
}, {
|
|
4701
|
+
method,
|
|
4702
|
+
args: Array.isArray(spec.args) ? spec.args : []
|
|
4703
|
+
});
|
|
4704
|
+
const actual = JSON.parse(JSON.stringify(value ?? null));
|
|
4705
|
+
if (spec.expectContains !== void 0) return (typeof actual === "string" ? actual : JSON.stringify(actual)).includes(String(spec.expectContains)) ? { ok: true } : {
|
|
4706
|
+
ok: false,
|
|
4707
|
+
detail: `[bridge] ${method}() 结果不含 "${spec.expectContains}"(实际: ${trunc(actual)})`
|
|
4708
|
+
};
|
|
4709
|
+
if (spec.expect !== void 0) return JSON.stringify(actual) === JSON.stringify(spec.expect) ? { ok: true } : {
|
|
4710
|
+
ok: false,
|
|
4711
|
+
detail: `[bridge] ${method}() = ${trunc(actual)} ≠ 期望 ${trunc(spec.expect)}`
|
|
4712
|
+
};
|
|
4713
|
+
return actual ? { ok: true } : {
|
|
4714
|
+
ok: false,
|
|
4715
|
+
detail: `[bridge] ${method}() 返回假值: ${trunc(actual)}`
|
|
4716
|
+
};
|
|
4717
|
+
} catch (e) {
|
|
4718
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4719
|
+
if (msg.startsWith("NO_BRIDGE:")) return {
|
|
4720
|
+
ok: false,
|
|
4721
|
+
detail: `[bridge] ${msg.slice(10)}`
|
|
4722
|
+
};
|
|
4723
|
+
return {
|
|
4724
|
+
ok: false,
|
|
4725
|
+
detail: `[bridge] ${method}() 执行失败: ${msg}`
|
|
4726
|
+
};
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
throw new Error("条件无效: 需要 selector/url/text/count/eval/bridge 之一");
|
|
4486
4730
|
}
|
|
4487
4731
|
/** 轮询等待条件成立(intervalMs 步进),返回最后一次检查结果。 */
|
|
4488
4732
|
async function pollCondition(page, cond, timeoutMs, intervalMs = 500) {
|
|
@@ -4668,12 +4912,28 @@ async function visualAssert(config, visual, page, exec) {
|
|
|
4668
4912
|
};
|
|
4669
4913
|
}
|
|
4670
4914
|
function matchNetwork(entry, f) {
|
|
4671
|
-
if (f.urlSubstr && !entry.url.includes(String(f.urlSubstr))) return false;
|
|
4915
|
+
if (f.urlSubstr && !entry.url.toLowerCase().includes(String(f.urlSubstr).toLowerCase())) return false;
|
|
4672
4916
|
if (f.method && entry.method.toUpperCase() !== String(f.method).toUpperCase()) return false;
|
|
4673
4917
|
if (f.status !== void 0 && Number(f.status) !== entry.status) return false;
|
|
4674
4918
|
if (f.failedOnly === true && !entry.failed && (entry.status === void 0 || entry.status < 400)) return false;
|
|
4675
4919
|
return true;
|
|
4676
4920
|
}
|
|
4921
|
+
/** urlSubstr 零命中时的相近 URL 候选(top5,去重,LCS 打分),与 what 未命中的
|
|
4922
|
+
* 候选体验对齐——把"子串写错"与"接口没调"区分开。 */
|
|
4923
|
+
function urlSuggestions(entries, substr) {
|
|
4924
|
+
const min = substr.trim().length <= 3 ? 1 : 2;
|
|
4925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4926
|
+
const scored = [];
|
|
4927
|
+
for (const e of entries) {
|
|
4928
|
+
if (seen.has(e.url)) continue;
|
|
4929
|
+
seen.add(e.url);
|
|
4930
|
+
scored.push({
|
|
4931
|
+
url: e.url,
|
|
4932
|
+
score: lcsScore(substr, e.url)
|
|
4933
|
+
});
|
|
4934
|
+
}
|
|
4935
|
+
return scored.filter((s) => s.score >= min).sort((a, b) => b.score - a.score).slice(0, 5).map((s) => s.url);
|
|
4936
|
+
}
|
|
4677
4937
|
/** 网络记录查询:list / failed / wait / clear。兼容 (args) 与 (config, args) 两种调用签名。 */
|
|
4678
4938
|
async function networkTool(configOrArgs, maybeArgs) {
|
|
4679
4939
|
const args = (maybeArgs !== void 0 ? maybeArgs : configOrArgs) ?? {};
|
|
@@ -4748,8 +5008,20 @@ async function networkTool(configOrArgs, maybeArgs) {
|
|
|
4748
5008
|
byStatus: b.byStatus
|
|
4749
5009
|
})).sort((a, b) => b.total - a.total) };
|
|
4750
5010
|
}
|
|
4751
|
-
if (action === "failed")
|
|
4752
|
-
|
|
5011
|
+
if (action === "failed") {
|
|
5012
|
+
const suggestions = list.length === 0 && args.urlSubstr ? urlSuggestions(entries, String(args.urlSubstr)) : [];
|
|
5013
|
+
return suggestions.length > 0 ? {
|
|
5014
|
+
failed: list,
|
|
5015
|
+
suggestions
|
|
5016
|
+
} : { failed: list };
|
|
5017
|
+
}
|
|
5018
|
+
if (action === "list") {
|
|
5019
|
+
const suggestions = list.length === 0 && args.urlSubstr ? urlSuggestions(entries, String(args.urlSubstr)) : [];
|
|
5020
|
+
return suggestions.length > 0 ? {
|
|
5021
|
+
entries: list,
|
|
5022
|
+
suggestions
|
|
5023
|
+
} : { entries: list };
|
|
5024
|
+
}
|
|
4753
5025
|
throw new Error(`unknown network action: ${action}`);
|
|
4754
5026
|
}
|
|
4755
5027
|
//#endregion
|
|
@@ -5996,7 +6268,7 @@ function compileToPlaywrightSpec(steps, options = {}) {
|
|
|
5996
6268
|
else if (args.selector) {
|
|
5997
6269
|
const state = args.state || "visible";
|
|
5998
6270
|
lines.push(` await page.locator(${JSON.stringify(args.selector)}).waitFor({ state: ${JSON.stringify(state)} });`);
|
|
5999
|
-
}
|
|
6271
|
+
} else if (args.condition?.bridge !== void 0 || args.bridge !== void 0) lines.push(` // bridge 条件请在接入 vite-plugin-dsh-bridge 的环境手动断言: await __DSH_BRIDGE__.<method>(...)`);
|
|
6000
6272
|
break;
|
|
6001
6273
|
case "browser_assert": {
|
|
6002
6274
|
const sel = args.selector || "body";
|
|
@@ -6004,6 +6276,9 @@ function compileToPlaywrightSpec(steps, options = {}) {
|
|
|
6004
6276
|
else if (args.visible !== void 0) {
|
|
6005
6277
|
if (args.visible) lines.push(` await expect(page.locator(${JSON.stringify(sel)})).toBeVisible();`);
|
|
6006
6278
|
else lines.push(` await expect(page.locator(${JSON.stringify(sel)})).toBeHidden();`);
|
|
6279
|
+
} else if (args.condition?.bridge !== void 0 || args.bridge !== void 0) {
|
|
6280
|
+
const bridge = args.condition?.bridge ?? args.bridge;
|
|
6281
|
+
lines.push(` // bridge 断言请在接入 vite-plugin-dsh-bridge 的环境手动执行: const v = await __DSH_BRIDGE__.${String(bridge?.method ?? "method")}(); expect(v).toBeTruthy();`);
|
|
6007
6282
|
}
|
|
6008
6283
|
break;
|
|
6009
6284
|
}
|
|
@@ -6658,7 +6933,7 @@ function getNavigationTools(config, timeout, stepImpls) {
|
|
|
6658
6933
|
return [
|
|
6659
6934
|
definePlainTool({
|
|
6660
6935
|
name: "browser_open",
|
|
6661
|
-
description: "在共享浏览器中打开一个 URL。之后可用 browser_snapshot 读取页面内容,用 browser_click / browser_type / browser_eval 操作页面,用 browser_screenshot 截图。",
|
|
6936
|
+
description: "在共享浏览器中打开一个 URL。活动标签已关闭或崩溃时自动新建标签重试(返回标 recovered: 'new-tab')。之后可用 browser_snapshot 读取页面内容,用 browser_click / browser_type / browser_eval 操作页面,用 browser_screenshot 截图。",
|
|
6662
6937
|
parameters: {
|
|
6663
6938
|
type: "object",
|
|
6664
6939
|
properties: {
|
|
@@ -6689,7 +6964,11 @@ function getNavigationTools(config, timeout, stepImpls) {
|
|
|
6689
6964
|
properties: {
|
|
6690
6965
|
url: { type: "string" },
|
|
6691
6966
|
title: { type: "string" },
|
|
6692
|
-
status: { type: "string" }
|
|
6967
|
+
status: { type: "string" },
|
|
6968
|
+
recovered: {
|
|
6969
|
+
type: "string",
|
|
6970
|
+
description: "活动标签已死自动换新页时为 'new-tab'。"
|
|
6971
|
+
}
|
|
6693
6972
|
},
|
|
6694
6973
|
required: [
|
|
6695
6974
|
"url",
|
|
@@ -6798,7 +7077,7 @@ function getNavigationTools(config, timeout, stepImpls) {
|
|
|
6798
7077
|
}),
|
|
6799
7078
|
definePlainTool({
|
|
6800
7079
|
name: "browser_tabs",
|
|
6801
|
-
description: "管理浏览器多标签页:list
|
|
7080
|
+
description: "管理浏览器多标签页:list 列出所有标签(每项含 createdAt 注册时间;已关闭的僵尸页自动摘除),new 新建(可选带 url),switch 按 id 或 index 切换,close 关闭指定标签。id 来自 list 结果。",
|
|
6802
7081
|
parameters: {
|
|
6803
7082
|
type: "object",
|
|
6804
7083
|
properties: {
|
|
@@ -6975,21 +7254,21 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
6975
7254
|
return [
|
|
6976
7255
|
definePlainTool({
|
|
6977
7256
|
name: "browser_click",
|
|
6978
|
-
description: "点击页面元素:selector(CSS)、ref(来自 browser_snapshot mode:'aria')、what(意图描述,如 '登录'/'提交订单',页内打分匹配并自动注册 ref)或 mark(SoM 标徽编号,看图定位)
|
|
7257
|
+
description: "点击页面元素:selector(CSS)、ref(来自 browser_snapshot mode:'aria')、what(意图描述,如 '登录'/'提交订单',页内打分匹配并自动注册 ref)或 mark(SoM 标徽编号,看图定位)四选一——同时传多个会直接报参数错误;容器内语义定位用 what + scope。也可通过 row(如 'TASK-1002') 与 action(如 '编辑') 直接定位表格中某行操作按钮(row 可组合 selector 限定行内目标)。what 零命中时报错并附最相近候选与 SoM 标徽截图。",
|
|
6979
7258
|
parameters: {
|
|
6980
7259
|
type: "object",
|
|
6981
7260
|
properties: {
|
|
6982
7261
|
selector: {
|
|
6983
7262
|
type: "string",
|
|
6984
|
-
description: "CSS
|
|
7263
|
+
description: "CSS 选择器(与 ref/mark/what 互斥)。"
|
|
6985
7264
|
},
|
|
6986
7265
|
ref: {
|
|
6987
7266
|
type: "string",
|
|
6988
|
-
description: "aria 快照的 ref(如 'e3')
|
|
7267
|
+
description: "aria 快照的 ref(如 'e3');导航后失效。与 selector/mark/what 互斥。"
|
|
6989
7268
|
},
|
|
6990
7269
|
what: {
|
|
6991
7270
|
type: "string",
|
|
6992
|
-
description: "意图描述(元素名称/文本,如 '登录')
|
|
7271
|
+
description: "意图描述(元素名称/文本,如 '登录'):页内打分语义定位,免快照直达;零命中返回最相近候选。与 selector/ref/mark 互斥;容器内定位配合 scope。"
|
|
6993
7272
|
},
|
|
6994
7273
|
scope: {
|
|
6995
7274
|
type: "string",
|
|
@@ -6997,11 +7276,11 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
6997
7276
|
},
|
|
6998
7277
|
mark: {
|
|
6999
7278
|
type: "integer",
|
|
7000
|
-
description: "SoM 标徽编号(browser_screenshot { som: true } 返回 somMarks 的 id)
|
|
7279
|
+
description: "SoM 标徽编号(browser_screenshot { som: true } 返回 somMarks 的 id);多模态看图定位的落点,导航后失效。与 selector/ref/what 互斥。"
|
|
7001
7280
|
},
|
|
7002
7281
|
row: {
|
|
7003
7282
|
type: "string",
|
|
7004
|
-
description: "表格行文本标识,如 'TASK-1002'
|
|
7283
|
+
description: "表格行文本标识,如 'TASK-1002',用于行级定向操作(可组合 selector 限定行内目标,不与 what/ref/mark 同用)。"
|
|
7005
7284
|
},
|
|
7006
7285
|
action: {
|
|
7007
7286
|
type: "string",
|
|
@@ -7019,6 +7298,10 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7019
7298
|
type: "boolean",
|
|
7020
7299
|
description: "点击效果验证:收集点击后的 DOM 变化摘要/URL 变化/新增网络请求;what 多候选首击无动静时自动换次候选。"
|
|
7021
7300
|
},
|
|
7301
|
+
strict: {
|
|
7302
|
+
type: "boolean",
|
|
7303
|
+
description: "多命中即抛错(对齐 Playwright strict),要求定位唯一;默认 false 操作第 1 个并在返回中提示命中数。"
|
|
7304
|
+
},
|
|
7022
7305
|
timeoutMs: { type: "integer" }
|
|
7023
7306
|
},
|
|
7024
7307
|
additionalProperties: false
|
|
@@ -7031,7 +7314,23 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7031
7314
|
url: { type: "string" },
|
|
7032
7315
|
title: { type: "string" },
|
|
7033
7316
|
effect: { type: "object" },
|
|
7034
|
-
effectHint: { type: "string" }
|
|
7317
|
+
effectHint: { type: "string" },
|
|
7318
|
+
matchedCount: {
|
|
7319
|
+
type: "integer",
|
|
7320
|
+
description: "选择器命中的元素总数(多命中歧义可见化)。"
|
|
7321
|
+
},
|
|
7322
|
+
visibleCount: {
|
|
7323
|
+
type: "integer",
|
|
7324
|
+
description: "命中元素中的可见数量。"
|
|
7325
|
+
},
|
|
7326
|
+
hint: {
|
|
7327
|
+
type: "string",
|
|
7328
|
+
description: "matchedCount>1 时的消歧指引。"
|
|
7329
|
+
},
|
|
7330
|
+
viaBridge: {
|
|
7331
|
+
type: "boolean",
|
|
7332
|
+
description: "what 未命中时经页面 __DSH_BRIDGE__(vite-plugin-dsh-bridge)兜底命中。"
|
|
7333
|
+
}
|
|
7035
7334
|
},
|
|
7036
7335
|
required: [
|
|
7037
7336
|
"clicked",
|
|
@@ -7045,18 +7344,21 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7045
7344
|
}),
|
|
7046
7345
|
definePlainTool({
|
|
7047
7346
|
name: "browser_type",
|
|
7048
|
-
description: "向输入框输入文本:selector(CSS)、ref 或 what(意图描述,如 '用户名'/'搜索')
|
|
7347
|
+
description: "向输入框输入文本:selector(CSS)、ref 或 what(意图描述,如 '用户名'/'搜索')三选一定位目标(同时传多个会报参数错误;容器内定位用 what + scope)。clear=true 时先清空再输入(推荐用于表单);submit=true 时输入后按回车;editor=true 时适配 Monaco/CodeMirror/富文本编辑器。",
|
|
7049
7348
|
parameters: {
|
|
7050
7349
|
type: "object",
|
|
7051
7350
|
properties: {
|
|
7052
|
-
selector: {
|
|
7351
|
+
selector: {
|
|
7352
|
+
type: "string",
|
|
7353
|
+
description: "CSS 选择器(与 ref/what 互斥)。"
|
|
7354
|
+
},
|
|
7053
7355
|
ref: {
|
|
7054
7356
|
type: "string",
|
|
7055
|
-
description: "aria 快照的 ref(如 'e2')
|
|
7357
|
+
description: "aria 快照的 ref(如 'e2')。与 selector/what 互斥。"
|
|
7056
7358
|
},
|
|
7057
7359
|
what: {
|
|
7058
7360
|
type: "string",
|
|
7059
|
-
description: "意图描述(元素名称/label/placeholder,如 '用户名')
|
|
7361
|
+
description: "意图描述(元素名称/label/placeholder,如 '用户名'):页内打分语义定位。与 selector/ref 互斥;容器内定位配合 scope。"
|
|
7060
7362
|
},
|
|
7061
7363
|
scope: {
|
|
7062
7364
|
type: "string",
|
|
@@ -7073,6 +7375,10 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7073
7375
|
type: "boolean",
|
|
7074
7376
|
description: "使用高斯间隔拟人化逐字输入。"
|
|
7075
7377
|
},
|
|
7378
|
+
strict: {
|
|
7379
|
+
type: "boolean",
|
|
7380
|
+
description: "多命中即抛错(对齐 Playwright strict);多命中或 strict 时不再 force 降级点击,遮罩拦截会直接暴露。"
|
|
7381
|
+
},
|
|
7076
7382
|
delayMs: { type: "integer" },
|
|
7077
7383
|
timeoutMs: { type: "integer" }
|
|
7078
7384
|
},
|
|
@@ -7087,7 +7393,19 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7087
7393
|
selector: { type: "string" },
|
|
7088
7394
|
submit: { type: "boolean" },
|
|
7089
7395
|
url: { type: "string" },
|
|
7090
|
-
title: { type: "string" }
|
|
7396
|
+
title: { type: "string" },
|
|
7397
|
+
matchedCount: {
|
|
7398
|
+
type: "integer",
|
|
7399
|
+
description: "选择器命中的元素总数(多命中歧义可见化)。"
|
|
7400
|
+
},
|
|
7401
|
+
visibleCount: {
|
|
7402
|
+
type: "integer",
|
|
7403
|
+
description: "命中元素中的可见数量。"
|
|
7404
|
+
},
|
|
7405
|
+
hint: {
|
|
7406
|
+
type: "string",
|
|
7407
|
+
description: "matchedCount>1 时的消歧指引。"
|
|
7408
|
+
}
|
|
7091
7409
|
},
|
|
7092
7410
|
required: [
|
|
7093
7411
|
"typed",
|
|
@@ -7103,7 +7421,7 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7103
7421
|
}),
|
|
7104
7422
|
definePlainTool({
|
|
7105
7423
|
name: "browser_press",
|
|
7106
|
-
description: "按键:有 selector 或
|
|
7424
|
+
description: "按键:有 selector、ref 或 what(三选一,互斥)时先聚焦该元素再按键(如 'Enter'、'Tab'、'Escape'、'Control+a'),否则在页面级按键。",
|
|
7107
7425
|
parameters: {
|
|
7108
7426
|
type: "object",
|
|
7109
7427
|
properties: {
|
|
@@ -7111,14 +7429,21 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7111
7429
|
type: "string",
|
|
7112
7430
|
description: "按键名(Playwright 键盘键名)。"
|
|
7113
7431
|
},
|
|
7114
|
-
selector: {
|
|
7432
|
+
selector: {
|
|
7433
|
+
type: "string",
|
|
7434
|
+
description: "CSS 选择器(与 ref/what 互斥)。"
|
|
7435
|
+
},
|
|
7115
7436
|
ref: {
|
|
7116
7437
|
type: "string",
|
|
7117
|
-
description: "aria 快照的 ref
|
|
7438
|
+
description: "aria 快照的 ref。与 selector/what 互斥。"
|
|
7118
7439
|
},
|
|
7119
7440
|
what: {
|
|
7120
7441
|
type: "string",
|
|
7121
|
-
description: "意图描述:先聚焦该元素再按键(页内打分语义定位)
|
|
7442
|
+
description: "意图描述:先聚焦该元素再按键(页内打分语义定位)。与 selector/ref 互斥。"
|
|
7443
|
+
},
|
|
7444
|
+
strict: {
|
|
7445
|
+
type: "boolean",
|
|
7446
|
+
description: "多命中即抛错(对齐 Playwright strict)。"
|
|
7122
7447
|
},
|
|
7123
7448
|
timeoutMs: { type: "integer" }
|
|
7124
7449
|
},
|
|
@@ -7131,7 +7456,20 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7131
7456
|
properties: {
|
|
7132
7457
|
pressed: { type: "string" },
|
|
7133
7458
|
url: { type: "string" },
|
|
7134
|
-
title: { type: "string" }
|
|
7459
|
+
title: { type: "string" },
|
|
7460
|
+
warning: { type: "string" },
|
|
7461
|
+
matchedCount: {
|
|
7462
|
+
type: "integer",
|
|
7463
|
+
description: "选择器命中的元素总数(多命中歧义可见化)。"
|
|
7464
|
+
},
|
|
7465
|
+
visibleCount: {
|
|
7466
|
+
type: "integer",
|
|
7467
|
+
description: "命中元素中的可见数量。"
|
|
7468
|
+
},
|
|
7469
|
+
hint: {
|
|
7470
|
+
type: "string",
|
|
7471
|
+
description: "matchedCount>1 时的消歧指引。"
|
|
7472
|
+
}
|
|
7135
7473
|
},
|
|
7136
7474
|
required: [
|
|
7137
7475
|
"pressed",
|
|
@@ -7145,21 +7483,25 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7145
7483
|
}),
|
|
7146
7484
|
definePlainTool({
|
|
7147
7485
|
name: "browser_select",
|
|
7148
|
-
description: "下拉选择原语:原生 <select> 与组件库下拉(antd / element-plus / ARIA listbox)通用。selector 或
|
|
7486
|
+
description: "下拉选择原语:原生 <select> 与组件库下拉(antd / element-plus / ARIA listbox)通用。selector、ref 或 what(三选一,互斥)指向下拉触发框,option 为选项文本(默认包含匹配,exact=true 精确)。多选下拉传 clear:true 可在选择前先清空旧标签实现替换语义。找不到选项时报错并列出可见选项,无需再 browser_eval 探查。",
|
|
7149
7487
|
parameters: {
|
|
7150
7488
|
type: "object",
|
|
7151
7489
|
properties: {
|
|
7152
7490
|
selector: {
|
|
7153
7491
|
type: "string",
|
|
7154
|
-
description: "下拉触发框的 CSS 选择器(如 #rc_select_1)
|
|
7492
|
+
description: "下拉触发框的 CSS 选择器(如 #rc_select_1);与 ref/what 互斥。"
|
|
7155
7493
|
},
|
|
7156
7494
|
ref: {
|
|
7157
7495
|
type: "string",
|
|
7158
|
-
description: "aria 快照的 ref
|
|
7496
|
+
description: "aria 快照的 ref。与 selector/what 互斥。"
|
|
7159
7497
|
},
|
|
7160
7498
|
what: {
|
|
7161
7499
|
type: "string",
|
|
7162
|
-
description: "意图描述(下拉的 label/名称,如 '所属部门')
|
|
7500
|
+
description: "意图描述(下拉的 label/名称,如 '所属部门'):页内打分语义定位触发框。与 selector/ref 互斥;容器内定位配合 scope。"
|
|
7501
|
+
},
|
|
7502
|
+
scope: {
|
|
7503
|
+
type: "string",
|
|
7504
|
+
description: "配合 what 限定扫描范围的容器选择器。"
|
|
7163
7505
|
},
|
|
7164
7506
|
option: {
|
|
7165
7507
|
type: "string",
|
|
@@ -7173,6 +7515,10 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7173
7515
|
type: "boolean",
|
|
7174
7516
|
description: "是否在选择前先清空已有选中项(对于多选下拉,替换而非叠加旧选项)。"
|
|
7175
7517
|
},
|
|
7518
|
+
strict: {
|
|
7519
|
+
type: "boolean",
|
|
7520
|
+
description: "多命中即抛错(对齐 Playwright strict)。"
|
|
7521
|
+
},
|
|
7176
7522
|
timeoutMs: { type: "integer" }
|
|
7177
7523
|
},
|
|
7178
7524
|
required: ["option"],
|
|
@@ -7185,7 +7531,19 @@ function getInteractionTools(config, timeout, stepImpls) {
|
|
|
7185
7531
|
selected: { type: "string" },
|
|
7186
7532
|
selector: { type: "string" },
|
|
7187
7533
|
url: { type: "string" },
|
|
7188
|
-
title: { type: "string" }
|
|
7534
|
+
title: { type: "string" },
|
|
7535
|
+
matchedCount: {
|
|
7536
|
+
type: "integer",
|
|
7537
|
+
description: "选择器命中的元素总数(多命中歧义可见化)。"
|
|
7538
|
+
},
|
|
7539
|
+
visibleCount: {
|
|
7540
|
+
type: "integer",
|
|
7541
|
+
description: "命中元素中的可见数量。"
|
|
7542
|
+
},
|
|
7543
|
+
hint: {
|
|
7544
|
+
type: "string",
|
|
7545
|
+
description: "matchedCount>1 时的消歧指引。"
|
|
7546
|
+
}
|
|
7189
7547
|
},
|
|
7190
7548
|
required: [
|
|
7191
7549
|
"selected",
|
|
@@ -7841,7 +8199,7 @@ function getTestingTools(config, timeout, stepImpls) {
|
|
|
7841
8199
|
}),
|
|
7842
8200
|
definePlainTool({
|
|
7843
8201
|
name: "browser_wait_for",
|
|
7844
|
-
description: "通用显式等待(测试同步原语):轮询直到条件成立或超时抛错。condition 格式(可放在 condition 对象内,也可直接平铺于顶层参数): 1) {selector:'#btn', state:'visible|hidden|attached|detached'}; 2) {ref:'e1', state}; 3) {row:'TASK-1002', action:'编辑', state}; 4) {url:'正则'}; 5) {text:'子串'}; 6) {count:{selector,op,value}}; 7) {eval:'JS 表达式' (支持纯表达式或 return 语句)}
|
|
8202
|
+
description: "通用显式等待(测试同步原语):轮询直到条件成立或超时抛错。condition 格式(可放在 condition 对象内,也可直接平铺于顶层参数): 1) {selector:'#btn', state:'visible|hidden|attached|detached'}; 2) {ref:'e1', state}; 3) {row:'TASK-1002', action:'编辑', state}; 4) {url:'正则'}; 5) {text:'子串'}; 6) {count:{selector,op,value}}; 7) {eval:'JS 表达式' (支持纯表达式或 return 语句)}; 8) {bridge:{method:'桥方法名', args?:[], expect?:期望值 / expectContains:'子串'}} —— 页内调用 __DSH_BRIDGE__(vite-plugin-dsh-bridge)方法并断言返回(缺省判 truthy;expect 深比较),让桥取数与宿主等待无缝衔接。用于等页面渲染、等请求完成、等元素出现。",
|
|
7845
8203
|
parameters: {
|
|
7846
8204
|
type: "object",
|
|
7847
8205
|
properties: {
|
|
@@ -7881,13 +8239,13 @@ function getTestingTools(config, timeout, stepImpls) {
|
|
|
7881
8239
|
}),
|
|
7882
8240
|
definePlainTool({
|
|
7883
8241
|
name: "browser_assert",
|
|
7884
|
-
description: "结构化断言(测试核心):condition 在 timeoutMs 内成立则通过(条件格式同 browser_wait_for,支持 selector/ref/row+action/url/text/count/eval,亦可直接平铺在顶层参数)。失败时自动截图存证据目录(assert-fail-*.png)并抛错,错误消息含原因与截图路径。另支持视觉回归:传 visual={baseline, threshold?, maxDiffRatio?, mask?} 与基线截图做像素比对(browser_screenshot 的 baseline 建立基线;mask 为忽略的选择器数组,如时钟/广告位),失败时产出当前截图与红标 diff 图。适合断言登录成功、关键元素出现、接口返回、文本正确、页面无视觉回归。",
|
|
8242
|
+
description: "结构化断言(测试核心):condition 在 timeoutMs 内成立则通过(条件格式同 browser_wait_for,支持 selector/ref/row+action/url/text/count/eval/bridge,亦可直接平铺在顶层参数)。bridge 条件 {bridge:{method,args?,expect?/expectContains?}} 页内调用 __DSH_BRIDGE__(vite-plugin-dsh-bridge)方法断言返回——桥取数、宿主断言留证(失败自动截图)。失败时自动截图存证据目录(assert-fail-*.png)并抛错,错误消息含原因与截图路径。另支持视觉回归:传 visual={baseline, threshold?, maxDiffRatio?, mask?} 与基线截图做像素比对(browser_screenshot 的 baseline 建立基线;mask 为忽略的选择器数组,如时钟/广告位),失败时产出当前截图与红标 diff 图。适合断言登录成功、关键元素出现、接口返回、文本正确、页面无视觉回归。",
|
|
7885
8243
|
parameters: {
|
|
7886
8244
|
type: "object",
|
|
7887
8245
|
properties: {
|
|
7888
8246
|
condition: {
|
|
7889
8247
|
type: "object",
|
|
7890
|
-
description: "断言条件(同 browser_wait_for,另支持 {ref, state})。"
|
|
8248
|
+
description: "断言条件(同 browser_wait_for,另支持 {ref, state} 与 {bridge:{method,args?,expect?}})。"
|
|
7891
8249
|
},
|
|
7892
8250
|
visual: {
|
|
7893
8251
|
type: "object",
|
|
@@ -7942,7 +8300,7 @@ function getTestingTools(config, timeout, stepImpls) {
|
|
|
7942
8300
|
},
|
|
7943
8301
|
urlSubstr: {
|
|
7944
8302
|
type: "string",
|
|
7945
|
-
description: "URL 子串过滤(list/failed)。"
|
|
8303
|
+
description: "URL 子串过滤(list/failed,大小写不敏感;零命中时附 suggestions 相近 URL 候选,防误判接口未调用)。"
|
|
7946
8304
|
},
|
|
7947
8305
|
method: {
|
|
7948
8306
|
type: "string",
|
|
@@ -7985,7 +8343,12 @@ function getTestingTools(config, timeout, stepImpls) {
|
|
|
7985
8343
|
},
|
|
7986
8344
|
matched: { type: "object" },
|
|
7987
8345
|
waitMs: { type: "integer" },
|
|
7988
|
-
cleared: { type: "boolean" }
|
|
8346
|
+
cleared: { type: "boolean" },
|
|
8347
|
+
suggestions: {
|
|
8348
|
+
type: "array",
|
|
8349
|
+
items: { type: "string" },
|
|
8350
|
+
description: "urlSubstr 零命中时的 top5 相近 URL 候选。"
|
|
8351
|
+
}
|
|
7989
8352
|
},
|
|
7990
8353
|
required: []
|
|
7991
8354
|
},
|
|
@@ -49,7 +49,9 @@ export interface NamedContext {
|
|
|
49
49
|
createdAt: number;
|
|
50
50
|
}
|
|
51
51
|
export declare function namedContextsOf(): Map<string, NamedContext>;
|
|
52
|
-
/** 把一个已 attach 的页面注册进 profile 标签表并激活,返回分配的 tab id。
|
|
52
|
+
/** 把一个已 attach 的页面注册进 profile 标签表并激活,返回分配的 tab id。
|
|
53
|
+
* 唯一注册路径:同时挂 close 监听幂等摘除外部关闭的页(否则僵尸句柄会永久占用
|
|
54
|
+
* activeId,所有工具连报 "Target page...closed" 而 tabs list 仍显示存活)。 */
|
|
53
55
|
export declare function registerTabPage(state: ProfileState, page: Page): number;
|
|
54
56
|
/** 新建一个页面(当前 profile 会话未启动时先启动)。 */
|
|
55
57
|
export declare function newPage(config: BrowserConfig): Promise<Page>;
|
|
@@ -57,8 +59,9 @@ export declare function newPage(config: BrowserConfig): Promise<Page>;
|
|
|
57
59
|
export declare function getPage(config: BrowserConfig): Promise<Page>;
|
|
58
60
|
/** 活动页面(可能为 null,不触发启动)。 */
|
|
59
61
|
export declare function activePage(): Page | null;
|
|
60
|
-
/** 投影当前 profile
|
|
62
|
+
/** 投影当前 profile 的标签列表(先摘除僵尸页,附注册时间)。 */
|
|
61
63
|
export declare function tabList(): Promise<{
|
|
64
|
+
createdAt?: number | undefined;
|
|
62
65
|
id: number;
|
|
63
66
|
url: string;
|
|
64
67
|
title: string;
|
|
@@ -7,6 +7,7 @@ export declare function isBotSensitiveUrl(url: string): boolean;
|
|
|
7
7
|
export declare function clickTool(config: BrowserConfig, args: Record<string, any>, exec?: ToolRunContext): Promise<{
|
|
8
8
|
url: string;
|
|
9
9
|
title: string;
|
|
10
|
+
viaBridge?: boolean | undefined;
|
|
10
11
|
effectHint?: string | undefined;
|
|
11
12
|
clicked: string;
|
|
12
13
|
effect: {
|
|
@@ -23,6 +24,7 @@ export declare function clickTool(config: BrowserConfig, args: Record<string, an
|
|
|
23
24
|
} | {
|
|
24
25
|
url: string;
|
|
25
26
|
title: string;
|
|
27
|
+
viaBridge?: boolean | undefined;
|
|
26
28
|
clicked: string;
|
|
27
29
|
}>;
|
|
28
30
|
export declare function typeTool(config: BrowserConfig, args: Record<string, any>): Promise<{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BrowserConfig } from "../types.js";
|
|
2
2
|
export declare function openTool(configOrArgs: BrowserConfig | Record<string, any>, maybeArgs?: Record<string, any>): Promise<{
|
|
3
|
+
recovered?: string | undefined;
|
|
3
4
|
url: string;
|
|
4
5
|
title: string;
|
|
5
6
|
status: string;
|
|
@@ -90,6 +91,7 @@ export declare function statusTool(): Promise<{
|
|
|
90
91
|
export declare function tabsTool(config: BrowserConfig, args: Record<string, any>): Promise<{
|
|
91
92
|
action: any;
|
|
92
93
|
tabs: {
|
|
94
|
+
createdAt?: number | undefined;
|
|
93
95
|
id: number;
|
|
94
96
|
url: string;
|
|
95
97
|
title: string;
|
|
@@ -104,11 +106,12 @@ export declare function tabsTool(config: BrowserConfig, args: Record<string, any
|
|
|
104
106
|
title: string;
|
|
105
107
|
};
|
|
106
108
|
tabs: {
|
|
109
|
+
createdAt?: number | undefined;
|
|
107
110
|
id: number;
|
|
108
111
|
url: string;
|
|
109
112
|
title: string;
|
|
110
113
|
}[];
|
|
111
|
-
activeId: number;
|
|
114
|
+
activeId: number | null;
|
|
112
115
|
}>;
|
|
113
116
|
export declare function profileTool(config: BrowserConfig, args: Record<string, any>): Promise<{
|
|
114
117
|
current: string;
|
|
@@ -9,12 +9,13 @@ export declare function waitTool(config: BrowserConfig, args: Record<string, any
|
|
|
9
9
|
/**
|
|
10
10
|
* 条件检查引擎(测试原语共用)。
|
|
11
11
|
*
|
|
12
|
-
* cond
|
|
12
|
+
* cond 支持六类(每类内部互斥,取第一个存在的字段):
|
|
13
13
|
* { selector: "#btn", state: "visible|hidden|attached|detached" } 元素状态(默认 visible)
|
|
14
14
|
* { url: "正则字符串" } 当前 URL 匹配
|
|
15
15
|
* { text: "子串" } body 可见文本包含
|
|
16
16
|
* { count: { selector, op: "eq|gt|gte|lt|lte", value: n } } 元素数量比较
|
|
17
17
|
* { eval: "JS 表达式" } 表达式求值 truthy
|
|
18
|
+
* { bridge: { method, args?, expect?/expectContains? } } 页内调用 __DSH_BRIDGE__ 方法并断言返回
|
|
18
19
|
*/
|
|
19
20
|
export type TestCondition = Record<string, any>;
|
|
20
21
|
/** 单次检查:返回 { ok, detail }。不抛错,失败时 detail 给出原因。 */
|
|
@@ -56,6 +57,7 @@ export declare function networkTool(configOrArgs?: Record<string, any>, maybeArg
|
|
|
56
57
|
waitMs?: undefined;
|
|
57
58
|
stats?: undefined;
|
|
58
59
|
failed?: undefined;
|
|
60
|
+
suggestions?: undefined;
|
|
59
61
|
} | {
|
|
60
62
|
matched: import("../session.js").NetworkEntry;
|
|
61
63
|
waitMs: number;
|
|
@@ -63,6 +65,7 @@ export declare function networkTool(configOrArgs?: Record<string, any>, maybeArg
|
|
|
63
65
|
entries?: undefined;
|
|
64
66
|
stats?: undefined;
|
|
65
67
|
failed?: undefined;
|
|
68
|
+
suggestions?: undefined;
|
|
66
69
|
} | {
|
|
67
70
|
stats: {
|
|
68
71
|
host: string;
|
|
@@ -76,13 +79,31 @@ export declare function networkTool(configOrArgs?: Record<string, any>, maybeArg
|
|
|
76
79
|
matched?: undefined;
|
|
77
80
|
waitMs?: undefined;
|
|
78
81
|
failed?: undefined;
|
|
82
|
+
suggestions?: undefined;
|
|
79
83
|
} | {
|
|
80
84
|
failed: import("../session.js").NetworkEntry[];
|
|
85
|
+
suggestions: string[];
|
|
81
86
|
cleared?: undefined;
|
|
82
87
|
entries?: undefined;
|
|
83
88
|
matched?: undefined;
|
|
84
89
|
waitMs?: undefined;
|
|
85
90
|
stats?: undefined;
|
|
91
|
+
} | {
|
|
92
|
+
failed: import("../session.js").NetworkEntry[];
|
|
93
|
+
cleared?: undefined;
|
|
94
|
+
entries?: undefined;
|
|
95
|
+
matched?: undefined;
|
|
96
|
+
waitMs?: undefined;
|
|
97
|
+
stats?: undefined;
|
|
98
|
+
suggestions?: undefined;
|
|
99
|
+
} | {
|
|
100
|
+
entries: import("../session.js").NetworkEntry[];
|
|
101
|
+
suggestions: string[];
|
|
102
|
+
cleared?: undefined;
|
|
103
|
+
matched?: undefined;
|
|
104
|
+
waitMs?: undefined;
|
|
105
|
+
stats?: undefined;
|
|
106
|
+
failed?: undefined;
|
|
86
107
|
} | {
|
|
87
108
|
entries: import("../session.js").NetworkEntry[];
|
|
88
109
|
cleared?: undefined;
|
|
@@ -90,5 +111,6 @@ export declare function networkTool(configOrArgs?: Record<string, any>, maybeArg
|
|
|
90
111
|
waitMs?: undefined;
|
|
91
112
|
stats?: undefined;
|
|
92
113
|
failed?: undefined;
|
|
114
|
+
suggestions?: undefined;
|
|
93
115
|
}>;
|
|
94
116
|
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -92,6 +92,10 @@ export interface ProfileState {
|
|
|
92
92
|
activeId: number | null;
|
|
93
93
|
counter: number;
|
|
94
94
|
channel?: string;
|
|
95
|
+
/** 标签元数据(id → createdAt);随 pages 同步增删,僵尸标签治理用。 */
|
|
96
|
+
tabMeta?: Map<number, {
|
|
97
|
+
createdAt: number;
|
|
98
|
+
}>;
|
|
95
99
|
}
|
|
96
100
|
/** 一次页面下载的记录。 */
|
|
97
101
|
export interface DownloadEntry {
|
|
@@ -173,6 +177,8 @@ export interface TargetResolution {
|
|
|
173
177
|
visibleCount?: number;
|
|
174
178
|
/** 语义定位的备选目标(按打分次序,最多 2 个);点击效果验证无变化时自动换选。 */
|
|
175
179
|
alternates?: Locator[];
|
|
180
|
+
/** 经 window.__DSH_BRIDGE__.resolveActionTarget 兜底命中(vite-plugin-dsh-bridge 反哺)。 */
|
|
181
|
+
viaBridge?: boolean;
|
|
176
182
|
}
|
|
177
183
|
/** 录制时采集的选择器指纹(回放自愈的降级候选来源)。 */
|
|
178
184
|
export interface SelectorHint {
|
|
@@ -20,6 +20,7 @@ declare function __walkerCore(): {
|
|
|
20
20
|
};
|
|
21
21
|
composedParent: (el: Element) => Element | null;
|
|
22
22
|
deepChildren: (el: Element) => Element[];
|
|
23
|
+
isProbablyClickable: (el: Element) => boolean;
|
|
23
24
|
};
|
|
24
25
|
/** walker 核心注入后暴露给 entry 的 API(类型层)。 */
|
|
25
26
|
export type WalkerApi = ReturnType<typeof __walkerCore>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-view",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test:enterprise": "tsx scripts/test-dev-server-enterprise.ts",
|
|
35
35
|
"test:energy": "tsx scripts/test-dev-server-energy.ts",
|
|
36
36
|
"test:shadow": "tsx scripts/test-dev-server-shadow.ts",
|
|
37
|
+
"test:span": "tsx scripts/test-dev-server-clickable-span.ts",
|
|
37
38
|
"test:pick": "tsx scripts/test-panel-pick.ts",
|
|
38
39
|
"dev:server": "tsx scripts/dev-server.ts",
|
|
39
40
|
"verify": "npm run typecheck && npm run lint && npm run test && npm run build",
|