dsh-browser-view 0.7.4 → 0.8.0
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/README.md +35 -3
- package/lib/client.js +1 -1
- package/lib/index.js +256 -175
- package/lib/mcp.js +9731 -0
- package/lib/types/browser/session/collaboration.d.ts +3 -0
- package/lib/types/browser/session/screencast.d.ts +23 -12
- package/lib/types/client/components/LiveImage.d.ts +21 -0
- package/lib/types/lib/prompts.d.ts +7 -0
- package/lib/types/lib/schema.d.ts +34 -0
- package/lib/types/mcp.d.ts +29 -0
- package/package.json +17 -2
package/lib/index.js
CHANGED
|
@@ -47,6 +47,150 @@ function resolveConfig(moduleName, schema, defaults, raw) {
|
|
|
47
47
|
return config;
|
|
48
48
|
}
|
|
49
49
|
//#endregion
|
|
50
|
+
//#region lib/types/lib/schema.js
|
|
51
|
+
const BROWSER_CONFIG_SCHEMA = {
|
|
52
|
+
enabled: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
optional: true
|
|
55
|
+
},
|
|
56
|
+
channel: { type: "string" },
|
|
57
|
+
executablePath: { type: "string" },
|
|
58
|
+
headless: { type: "boolean" },
|
|
59
|
+
userDataDir: { type: "string" },
|
|
60
|
+
profiles: { type: "any" },
|
|
61
|
+
screenshotDir: { type: "string" },
|
|
62
|
+
downloadDir: { type: "string" },
|
|
63
|
+
workspaceDir: { type: "string" },
|
|
64
|
+
screenshotMaxAgeDays: {
|
|
65
|
+
type: "number",
|
|
66
|
+
min: 0
|
|
67
|
+
},
|
|
68
|
+
screenshotMaxCount: {
|
|
69
|
+
type: "number",
|
|
70
|
+
min: 0
|
|
71
|
+
},
|
|
72
|
+
basePath: { type: "string" },
|
|
73
|
+
recordBodies: { type: "boolean" },
|
|
74
|
+
maxTextChars: {
|
|
75
|
+
type: "number",
|
|
76
|
+
min: 1
|
|
77
|
+
},
|
|
78
|
+
maxLinks: {
|
|
79
|
+
type: "number",
|
|
80
|
+
min: 1
|
|
81
|
+
},
|
|
82
|
+
timeoutMs: {
|
|
83
|
+
type: "number",
|
|
84
|
+
min: 1
|
|
85
|
+
},
|
|
86
|
+
baselineDir: { type: "string" },
|
|
87
|
+
selectorHealing: { type: "boolean" },
|
|
88
|
+
healTimeoutMs: {
|
|
89
|
+
type: "number",
|
|
90
|
+
min: 100
|
|
91
|
+
},
|
|
92
|
+
maxRefs: {
|
|
93
|
+
type: "number",
|
|
94
|
+
min: 1
|
|
95
|
+
},
|
|
96
|
+
maxRows: {
|
|
97
|
+
type: "number",
|
|
98
|
+
min: 1
|
|
99
|
+
},
|
|
100
|
+
snapshotHistory: {
|
|
101
|
+
type: "number",
|
|
102
|
+
min: 1,
|
|
103
|
+
max: 20
|
|
104
|
+
},
|
|
105
|
+
confirmMode: {
|
|
106
|
+
type: "string",
|
|
107
|
+
enum: [
|
|
108
|
+
"off",
|
|
109
|
+
"manual",
|
|
110
|
+
"auto"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
confirmTimeoutMs: {
|
|
114
|
+
type: "number",
|
|
115
|
+
min: 1e3
|
|
116
|
+
},
|
|
117
|
+
sensitivePatterns: { type: "string[]" },
|
|
118
|
+
sensitiveTools: { type: "string[]" },
|
|
119
|
+
confirmPolicies: {
|
|
120
|
+
type: "any",
|
|
121
|
+
optional: true
|
|
122
|
+
},
|
|
123
|
+
humanize: {
|
|
124
|
+
type: "any",
|
|
125
|
+
optional: true
|
|
126
|
+
},
|
|
127
|
+
humanizeSpeed: {
|
|
128
|
+
type: "string",
|
|
129
|
+
enum: [
|
|
130
|
+
"slow",
|
|
131
|
+
"normal",
|
|
132
|
+
"fast"
|
|
133
|
+
],
|
|
134
|
+
optional: true
|
|
135
|
+
},
|
|
136
|
+
confirmInReplay: { type: "boolean" },
|
|
137
|
+
somFallback: { type: "boolean" },
|
|
138
|
+
refReanchor: { type: "boolean" },
|
|
139
|
+
toolset: {
|
|
140
|
+
type: "string",
|
|
141
|
+
enum: ["core", "all"]
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const BROWSER_DEFAULT_CONFIG = {
|
|
145
|
+
channel: "auto",
|
|
146
|
+
executablePath: "",
|
|
147
|
+
headless: false,
|
|
148
|
+
userDataDir: "",
|
|
149
|
+
profiles: {},
|
|
150
|
+
screenshotDir: ".dsh-browser/screenshots",
|
|
151
|
+
downloadDir: ".dsh-browser/downloads",
|
|
152
|
+
workspaceDir: "",
|
|
153
|
+
screenshotMaxAgeDays: 7,
|
|
154
|
+
screenshotMaxCount: 200,
|
|
155
|
+
basePath: "/browser",
|
|
156
|
+
recordBodies: true,
|
|
157
|
+
maxTextChars: 2e4,
|
|
158
|
+
maxLinks: 50,
|
|
159
|
+
timeoutMs: 3e4,
|
|
160
|
+
baselineDir: ".dsh-browser/baselines",
|
|
161
|
+
selectorHealing: true,
|
|
162
|
+
healTimeoutMs: 3e3,
|
|
163
|
+
maxRefs: 200,
|
|
164
|
+
maxRows: 1e3,
|
|
165
|
+
snapshotHistory: 5,
|
|
166
|
+
confirmMode: "off",
|
|
167
|
+
confirmTimeoutMs: 6e4,
|
|
168
|
+
sensitivePatterns: [
|
|
169
|
+
"提交",
|
|
170
|
+
"删除",
|
|
171
|
+
"支付"
|
|
172
|
+
],
|
|
173
|
+
sensitiveTools: ["browser_click"],
|
|
174
|
+
confirmInReplay: true,
|
|
175
|
+
somFallback: true,
|
|
176
|
+
refReanchor: true,
|
|
177
|
+
toolset: "all"
|
|
178
|
+
};
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region lib/types/lib/prompts.js
|
|
181
|
+
const FULL_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),页内打分匹配并自动注册 ref,一次调用替代 快照→查找→点击 三轮;零命中报错会附最相近候选。需要精确控制时再用 selector(CSS)/ref(aria 快照)。打开页面后可先 browser_snapshot 读文本与链接、browser_elements 读表单结构(含关联 label 与展开的下拉选项);browser_find(query 传名称,只返回命中 ref+区域上下文,大页面最省 token;零命中会给相近候选)适合多目标排查,需要全页结构时再用 browser_snapshot { mode: \"aria\" }(可交互元素带 ref=eN,click/type/select/press 直接传 ref 代替 CSS 选择器);下拉选择(原生 select 与 antd/element 组件库)用 browser_select 传 selector/ref/what + option 文本一步完成;固定小序列(点击→等待→输入→提交)用 browser_batch 一次执行减少往返;标签管理用 browser_tabs;下载用 browser_download(wait=true 可等新下载);登录态用 browser_cookies。做自动化测试时:用 browser_wait_for 显式等待页面稳定/元素出现,用 browser_assert 断言(失败自动截图;visual={baseline} 可做视觉回归,先用 browser_screenshot { baseline } 建基线),用 browser_network 检查接口调用与 4xx/5xx,跑完一轮可用 browser_record save 保存操作序列、browser_replay 回放做回归(选择器失效自动按指纹自愈;vars/data 支持参数化与数据驱动)。每次 replay/batch 都记入运行历史,browser_report list/export 可查看与导出回归报告。抓取结构化数据用 browser_extract(fields 定义 CSS 字段,可翻页,可导出 CSV/JSONL)。遇到需要登录的网站:先 browser_open 打开登录页,告知用户请在浏览器窗口中完成登录,再调用 browser_wait_for_login 等待(可给 successSelector/successUrl);验证码/二次确认等更广的「人补一步」场景用 browser_pause_for_human(提示语会显示在实时画面面板);配置 userDataDir 时登录态会持久化。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
182
|
+
const CORE_TOOL_PROMPT = "浏览器工具操作一个共享浏览器会话(toolset=core:已精简工具集)。定位操作元素最省事的方式是意图语义定位:browser_click/browser_type/browser_select 直接传 what(元素名称/label,如 what:'登录'),一次调用替代快照→查找→点击;零命中报错附最相近候选。需要精确控制时再用 selector/ref:打开页面后用 browser_snapshot 读内容(scope 限定区域、since 增量重读省 token),browser_find(query 传名称,返回 ref),点击/输入/按键/下拉用 browser_click/browser_type/browser_press/browser_select(selector 或 ref);固定小序列用 browser_batch;等待用 browser_wait/browser_wait_for,断言用 browser_assert;接口检查用 browser_network;标签 browser_tabs;下载/上传 browser_download/browser_upload。需要人工:登录用 browser_wait_for_login,验证码/二次确认用 browser_pause_for_human,敏感动作(提交/删除/支付)前用 browser_confirm。环境异常先跑 browser_doctor 诊断。录制/回放/报告/抽取/profile/隔离 context 等工具当前未注册(toolset=core),需要时把配置改为 toolset: all 并重启 DSH。用户可通过界面右下角面板的实时画面查看浏览器。";
|
|
183
|
+
/** confirmMode 附加指引(与 DSH 入口同款语义,manual 面向宿主审批、auto 说明硬闸)。 */
|
|
184
|
+
function confirmGuidanceOf(confirmMode) {
|
|
185
|
+
if (confirmMode === "manual") return "本环境 confirmMode 为 manual:凡提交/删除/支付等不可逆或影响线上数据的敏感动作,执行前必须先调用 browser_confirm(reason 说明操作与影响,等待用户在实时画面面板点允许/拒绝;拒绝或超时即放弃该动作,不得换路径绕过)。";
|
|
186
|
+
if (confirmMode === "auto") return "本环境 confirmMode 为 auto:宿主层已在敏感动作(命中敏感模式的目标元素)前强制弹审批卡,agent 无法绕过;审批被拒绝或超时的动作不可换路径重试,应向用户说明并等待指示。";
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
/** 组装完整的工具使用指引文本(DSH systemPrompt 段落与 MCP instructions 共用)。 */
|
|
190
|
+
function browserSystemPrompt(config) {
|
|
191
|
+
return (config.toolset === "core" ? CORE_TOOL_PROMPT : FULL_TOOL_PROMPT) + confirmGuidanceOf(config.confirmMode);
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
50
194
|
//#region lib/types/lib/http.js
|
|
51
195
|
/** Parse the request URL pathname (query strings are ignored). */
|
|
52
196
|
function urlPath(req) {
|
|
@@ -397,6 +541,10 @@ function snapshotVersionEntry(cache, version) {
|
|
|
397
541
|
* 供实时画面面板展示「等待人工操作」提示。
|
|
398
542
|
*/
|
|
399
543
|
let pauseState = null;
|
|
544
|
+
let interactivePanelPresent = true;
|
|
545
|
+
function isInteractivePanelPresent() {
|
|
546
|
+
return interactivePanelPresent;
|
|
547
|
+
}
|
|
400
548
|
function setPauseInstruction(instruction) {
|
|
401
549
|
pauseState = {
|
|
402
550
|
instruction,
|
|
@@ -2533,24 +2681,43 @@ var ScreencastManager = class {
|
|
|
2533
2681
|
cdp = null;
|
|
2534
2682
|
currentPage = null;
|
|
2535
2683
|
listeners = /* @__PURE__ */ new Set();
|
|
2536
|
-
isStarting = false;
|
|
2537
2684
|
lastFrame = null;
|
|
2685
|
+
/** 期望绑定的页面(syncTo/subscribe 写入);null 表示停流。 */
|
|
2686
|
+
desiredPage = null;
|
|
2687
|
+
/** ensure 单飞标记:在途收敛循环会在每个 await 后重读 desiredPage,吞并并发 syncTo。 */
|
|
2688
|
+
switching = false;
|
|
2538
2689
|
/**
|
|
2539
2690
|
* 注册帧监听器。返回注销函数。
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2691
|
+
* page 为订阅时的初始目标页(由调用方解析)。首个订阅者加入时激活 CDP
|
|
2692
|
+
* Screencast;全部离开时注销并停止 CDP,保持零空载开销。
|
|
2542
2693
|
*/
|
|
2543
|
-
subscribe(listener) {
|
|
2694
|
+
subscribe(listener, page) {
|
|
2544
2695
|
this.listeners.add(listener);
|
|
2696
|
+
if (page !== void 0) this.desiredPage = page;
|
|
2545
2697
|
if (this.lastFrame) try {
|
|
2546
2698
|
listener(this.lastFrame);
|
|
2547
2699
|
} catch {}
|
|
2548
|
-
if (this.listeners.size === 1) this.
|
|
2700
|
+
if (this.listeners.size === 1) this.ensure();
|
|
2549
2701
|
return () => {
|
|
2550
2702
|
this.listeners.delete(listener);
|
|
2551
|
-
if (this.listeners.size === 0)
|
|
2703
|
+
if (this.listeners.size === 0) {
|
|
2704
|
+
this.desiredPage = null;
|
|
2705
|
+
this.ensure();
|
|
2706
|
+
}
|
|
2552
2707
|
};
|
|
2553
2708
|
}
|
|
2709
|
+
/**
|
|
2710
|
+
* 通知目标页变化(换页重绑 / 置 null 停流)。无订阅者时仅记录意图,
|
|
2711
|
+
* 不建 CDP 会话;快速连续换页由收敛循环合并,最终绑定最后一页。
|
|
2712
|
+
*/
|
|
2713
|
+
async syncTo(page) {
|
|
2714
|
+
this.desiredPage = page;
|
|
2715
|
+
await this.ensure();
|
|
2716
|
+
}
|
|
2717
|
+
/** 停止推流并释放 CDP(closeBrowser 等收尾路径)。 */
|
|
2718
|
+
async stop() {
|
|
2719
|
+
await this.syncTo(null);
|
|
2720
|
+
}
|
|
2554
2721
|
get subscriberCount() {
|
|
2555
2722
|
return this.listeners.size;
|
|
2556
2723
|
}
|
|
@@ -2558,26 +2725,54 @@ var ScreencastManager = class {
|
|
|
2558
2725
|
return this.lastFrame;
|
|
2559
2726
|
}
|
|
2560
2727
|
/**
|
|
2561
|
-
*
|
|
2728
|
+
* 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
|
|
2729
|
+
* 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
|
|
2730
|
+
* 不会因启动中而丢掉换页请求。
|
|
2562
2731
|
*/
|
|
2563
|
-
async
|
|
2564
|
-
if (this.
|
|
2565
|
-
|
|
2566
|
-
if (!page || page.isClosed()) return false;
|
|
2567
|
-
this.isStarting = true;
|
|
2732
|
+
async ensure() {
|
|
2733
|
+
if (this.switching) return;
|
|
2734
|
+
this.switching = true;
|
|
2568
2735
|
try {
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
if (this.
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2736
|
+
for (let round = 0; round < 16; round++) {
|
|
2737
|
+
const want = this.desiredPage;
|
|
2738
|
+
if (this.listeners.size === 0 || want === null || want.isClosed()) {
|
|
2739
|
+
if (this.cdp === null) return;
|
|
2740
|
+
await this.teardown();
|
|
2741
|
+
if (this.desiredPage === want) return;
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
if (this.currentPage === want && this.cdp !== null) return;
|
|
2745
|
+
await this.teardown();
|
|
2746
|
+
if (this.desiredPage !== want) continue;
|
|
2747
|
+
if (!await this.attach(want)) return;
|
|
2748
|
+
if (this.desiredPage !== want) continue;
|
|
2749
|
+
return;
|
|
2577
2750
|
}
|
|
2751
|
+
} finally {
|
|
2752
|
+
this.switching = false;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/** 停掉当前 CDP 会话并清空绑定;lastFrame 一并清空,防新订阅者回放到旧页帧。 */
|
|
2756
|
+
async teardown() {
|
|
2757
|
+
const cdp = this.cdp;
|
|
2758
|
+
this.cdp = null;
|
|
2759
|
+
this.currentPage = null;
|
|
2760
|
+
this.lastFrame = null;
|
|
2761
|
+
if (cdp !== null) try {
|
|
2762
|
+
await cdp.send("Page.stopScreencast").catch(() => {});
|
|
2763
|
+
await cdp.detach().catch(() => {});
|
|
2764
|
+
} catch {}
|
|
2765
|
+
}
|
|
2766
|
+
/** 为页面建立 CDP Screencast 会话;失败(非 Chromium 等)静默降级返回 false。 */
|
|
2767
|
+
async attach(page) {
|
|
2768
|
+
try {
|
|
2769
|
+
const context = page.context();
|
|
2770
|
+
if (typeof context.newCDPSession !== "function") return false;
|
|
2578
2771
|
const cdp = await context.newCDPSession(page);
|
|
2579
2772
|
this.cdp = cdp;
|
|
2773
|
+
this.currentPage = page;
|
|
2580
2774
|
cdp.on("Page.screencastFrame", async (payload) => {
|
|
2775
|
+
if (this.cdp !== cdp) return;
|
|
2581
2776
|
try {
|
|
2582
2777
|
const frame = {
|
|
2583
2778
|
buffer: Buffer.from(payload.data, "base64"),
|
|
@@ -2592,34 +2787,29 @@ var ScreencastManager = class {
|
|
|
2592
2787
|
await cdp.send("Page.screencastFrameAck", { sessionId: payload.sessionId }).catch(() => {});
|
|
2593
2788
|
} catch {}
|
|
2594
2789
|
});
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2790
|
+
try {
|
|
2791
|
+
await cdp.send("Page.startScreencast", {
|
|
2792
|
+
format: "jpeg",
|
|
2793
|
+
quality: 60,
|
|
2794
|
+
maxWidth: 1280,
|
|
2795
|
+
maxHeight: 800,
|
|
2796
|
+
everyNthFrame: 1
|
|
2797
|
+
});
|
|
2798
|
+
} catch (err) {
|
|
2799
|
+
if (this.cdp === cdp) await this.teardown();
|
|
2800
|
+
throw err;
|
|
2801
|
+
}
|
|
2802
|
+
page.once("close", () => {
|
|
2803
|
+
if (this.currentPage === page) {
|
|
2804
|
+
this.desiredPage = null;
|
|
2805
|
+
this.ensure();
|
|
2806
|
+
}
|
|
2601
2807
|
});
|
|
2602
2808
|
return true;
|
|
2603
2809
|
} catch {
|
|
2604
|
-
await this.stop();
|
|
2605
2810
|
return false;
|
|
2606
|
-
} finally {
|
|
2607
|
-
this.isStarting = false;
|
|
2608
2811
|
}
|
|
2609
2812
|
}
|
|
2610
|
-
/**
|
|
2611
|
-
* 停止 CDP Screencast,静默释放资源
|
|
2612
|
-
*/
|
|
2613
|
-
async stop() {
|
|
2614
|
-
const cdp = this.cdp;
|
|
2615
|
-
this.cdp = null;
|
|
2616
|
-
this.currentPage = null;
|
|
2617
|
-
this.isStarting = false;
|
|
2618
|
-
if (cdp !== null) try {
|
|
2619
|
-
await cdp.send("Page.stopScreencast").catch(() => {});
|
|
2620
|
-
await cdp.detach().catch(() => {});
|
|
2621
|
-
} catch {}
|
|
2622
|
-
}
|
|
2623
2813
|
};
|
|
2624
2814
|
const screencastManager = new ScreencastManager();
|
|
2625
2815
|
//#endregion
|
|
@@ -2964,6 +3154,12 @@ function pruneClosedPages(state) {
|
|
|
2964
3154
|
if (state.activeId === id) state.activeId = null;
|
|
2965
3155
|
}
|
|
2966
3156
|
}
|
|
3157
|
+
/** 实时画面流跟随活动页(推模型:session 层在 activeId 变化点主动通知,
|
|
3158
|
+
* screencast 模块不反查本 facade,避免循环依赖)。 */
|
|
3159
|
+
function syncScreencast(state) {
|
|
3160
|
+
if (state !== stateOf()) return;
|
|
3161
|
+
screencastManager.syncTo(state.pages.get(state.activeId ?? -1) ?? null);
|
|
3162
|
+
}
|
|
2967
3163
|
/** 把一个已 attach 的页面注册进 profile 标签表并激活,返回分配的 tab id。
|
|
2968
3164
|
* 唯一注册路径:同时挂 close 监听幂等摘除外部关闭的页(否则僵尸句柄会永久占用
|
|
2969
3165
|
* activeId,所有工具连报 "Target page...closed" 而 tabs list 仍显示存活)。 */
|
|
@@ -2972,11 +3168,13 @@ function registerTabPage(state, page) {
|
|
|
2972
3168
|
state.pages.set(id, page);
|
|
2973
3169
|
state.activeId = id;
|
|
2974
3170
|
(state.tabMeta ??= /* @__PURE__ */ new Map()).set(id, { createdAt: Date.now() });
|
|
3171
|
+
syncScreencast(state);
|
|
2975
3172
|
page.on("close", () => {
|
|
2976
3173
|
if (!state.pages.has(id)) return;
|
|
2977
3174
|
state.pages.delete(id);
|
|
2978
3175
|
state.tabMeta?.delete(id);
|
|
2979
3176
|
if (state.activeId === id) state.activeId = null;
|
|
3177
|
+
syncScreencast(state);
|
|
2980
3178
|
});
|
|
2981
3179
|
return id;
|
|
2982
3180
|
}
|
|
@@ -3087,6 +3285,8 @@ function setLastFormFields(fields) {
|
|
|
3087
3285
|
/** 切换当前 profile。 */
|
|
3088
3286
|
function setCurrentProfile(name) {
|
|
3089
3287
|
currentProfile = name;
|
|
3288
|
+
const state = stateOf();
|
|
3289
|
+
screencastManager.syncTo(state.pages.get(state.activeId ?? -1) ?? null);
|
|
3090
3290
|
}
|
|
3091
3291
|
//#endregion
|
|
3092
3292
|
//#region lib/types/browser/tools/navigation.js
|
|
@@ -5647,6 +5847,7 @@ async function pauseForHumanTool(config, args) {
|
|
|
5647
5847
|
async function confirmTool(config, args) {
|
|
5648
5848
|
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
5649
5849
|
if (!reason) throw new Error("缺少 reason: 请说明要执行什么敏感操作、为什么需要审批(如「提交白名单表单,会产生线上数据变更」)");
|
|
5850
|
+
if (!isInteractivePanelPresent()) throw new Error("敏感操作审批失败: 当前环境无前端面板支持人工审批(已按安全策略 fail-closed 拒绝)");
|
|
5650
5851
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? config.confirmTimeoutMs ?? 6e4) || 6e4, 1e3), 6e5);
|
|
5651
5852
|
const id = `cfm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
5652
5853
|
setPendingConfirm({
|
|
@@ -5716,6 +5917,14 @@ const describePoint = injectWalker((payload, w) => {
|
|
|
5716
5917
|
*/
|
|
5717
5918
|
async function pickToolOnPage(page, config, args) {
|
|
5718
5919
|
const instruction = typeof args.instruction === "string" && args.instruction.trim() !== "" ? args.instruction.trim() : "请在实时画面大图上点击要教学的目标元素";
|
|
5920
|
+
if (!isInteractivePanelPresent()) return {
|
|
5921
|
+
picked: false,
|
|
5922
|
+
status: "missed",
|
|
5923
|
+
instruction,
|
|
5924
|
+
detail: "面板点选教学失败: 当前环境无前端面板供用户交互式点选",
|
|
5925
|
+
waitedMs: 0,
|
|
5926
|
+
...await pageIdentity(page)
|
|
5927
|
+
};
|
|
5719
5928
|
const timeoutMs = Math.min(Math.max(Number(args.timeoutMs ?? 3e5) || 3e5, 1e3), 6e5);
|
|
5720
5929
|
const id = `pick-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
5721
5930
|
const meta = screencastManager.getLastFrame()?.metadata ?? void 0;
|
|
@@ -9659,7 +9868,7 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
9659
9868
|
timestamp: frame.timestamp
|
|
9660
9869
|
})}\n\n`);
|
|
9661
9870
|
} catch {}
|
|
9662
|
-
});
|
|
9871
|
+
}, page);
|
|
9663
9872
|
req.on("close", () => {
|
|
9664
9873
|
unsubscribe();
|
|
9665
9874
|
});
|
|
@@ -9677,7 +9886,7 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
9677
9886
|
res.write(frame.buffer);
|
|
9678
9887
|
res.write("\r\n");
|
|
9679
9888
|
} catch {}
|
|
9680
|
-
});
|
|
9889
|
+
}, page);
|
|
9681
9890
|
req.on("close", () => {
|
|
9682
9891
|
unsubscribe();
|
|
9683
9892
|
});
|
|
@@ -9715,136 +9924,8 @@ function registerBrowserApi(ctx, config, base) {
|
|
|
9715
9924
|
//#region lib/types/index.js
|
|
9716
9925
|
const name = "dsh-browser-view";
|
|
9717
9926
|
const inject = [];
|
|
9718
|
-
const SCHEMA = {
|
|
9719
|
-
enabled: {
|
|
9720
|
-
type: "boolean",
|
|
9721
|
-
optional: true
|
|
9722
|
-
},
|
|
9723
|
-
channel: { type: "string" },
|
|
9724
|
-
executablePath: { type: "string" },
|
|
9725
|
-
headless: { type: "boolean" },
|
|
9726
|
-
userDataDir: { type: "string" },
|
|
9727
|
-
profiles: { type: "any" },
|
|
9728
|
-
screenshotDir: { type: "string" },
|
|
9729
|
-
downloadDir: { type: "string" },
|
|
9730
|
-
workspaceDir: { type: "string" },
|
|
9731
|
-
screenshotMaxAgeDays: {
|
|
9732
|
-
type: "number",
|
|
9733
|
-
min: 0
|
|
9734
|
-
},
|
|
9735
|
-
screenshotMaxCount: {
|
|
9736
|
-
type: "number",
|
|
9737
|
-
min: 0
|
|
9738
|
-
},
|
|
9739
|
-
basePath: { type: "string" },
|
|
9740
|
-
recordBodies: { type: "boolean" },
|
|
9741
|
-
maxTextChars: {
|
|
9742
|
-
type: "number",
|
|
9743
|
-
min: 1
|
|
9744
|
-
},
|
|
9745
|
-
maxLinks: {
|
|
9746
|
-
type: "number",
|
|
9747
|
-
min: 1
|
|
9748
|
-
},
|
|
9749
|
-
timeoutMs: {
|
|
9750
|
-
type: "number",
|
|
9751
|
-
min: 1
|
|
9752
|
-
},
|
|
9753
|
-
baselineDir: { type: "string" },
|
|
9754
|
-
selectorHealing: { type: "boolean" },
|
|
9755
|
-
healTimeoutMs: {
|
|
9756
|
-
type: "number",
|
|
9757
|
-
min: 100
|
|
9758
|
-
},
|
|
9759
|
-
maxRefs: {
|
|
9760
|
-
type: "number",
|
|
9761
|
-
min: 1
|
|
9762
|
-
},
|
|
9763
|
-
maxRows: {
|
|
9764
|
-
type: "number",
|
|
9765
|
-
min: 1
|
|
9766
|
-
},
|
|
9767
|
-
snapshotHistory: {
|
|
9768
|
-
type: "number",
|
|
9769
|
-
min: 1,
|
|
9770
|
-
max: 20
|
|
9771
|
-
},
|
|
9772
|
-
confirmMode: {
|
|
9773
|
-
type: "string",
|
|
9774
|
-
enum: [
|
|
9775
|
-
"off",
|
|
9776
|
-
"manual",
|
|
9777
|
-
"auto"
|
|
9778
|
-
]
|
|
9779
|
-
},
|
|
9780
|
-
confirmTimeoutMs: {
|
|
9781
|
-
type: "number",
|
|
9782
|
-
min: 1e3
|
|
9783
|
-
},
|
|
9784
|
-
sensitivePatterns: { type: "string[]" },
|
|
9785
|
-
sensitiveTools: { type: "string[]" },
|
|
9786
|
-
confirmPolicies: {
|
|
9787
|
-
type: "any",
|
|
9788
|
-
optional: true
|
|
9789
|
-
},
|
|
9790
|
-
humanize: {
|
|
9791
|
-
type: "any",
|
|
9792
|
-
optional: true
|
|
9793
|
-
},
|
|
9794
|
-
humanizeSpeed: {
|
|
9795
|
-
type: "string",
|
|
9796
|
-
enum: [
|
|
9797
|
-
"slow",
|
|
9798
|
-
"normal",
|
|
9799
|
-
"fast"
|
|
9800
|
-
],
|
|
9801
|
-
optional: true
|
|
9802
|
-
},
|
|
9803
|
-
confirmInReplay: { type: "boolean" },
|
|
9804
|
-
somFallback: { type: "boolean" },
|
|
9805
|
-
refReanchor: { type: "boolean" },
|
|
9806
|
-
toolset: {
|
|
9807
|
-
type: "string",
|
|
9808
|
-
enum: ["core", "all"]
|
|
9809
|
-
}
|
|
9810
|
-
};
|
|
9811
|
-
const DEFAULT_CONFIG = {
|
|
9812
|
-
channel: "auto",
|
|
9813
|
-
executablePath: "",
|
|
9814
|
-
headless: false,
|
|
9815
|
-
userDataDir: "",
|
|
9816
|
-
profiles: {},
|
|
9817
|
-
screenshotDir: ".dsh-browser/screenshots",
|
|
9818
|
-
downloadDir: ".dsh-browser/downloads",
|
|
9819
|
-
workspaceDir: "",
|
|
9820
|
-
screenshotMaxAgeDays: 7,
|
|
9821
|
-
screenshotMaxCount: 200,
|
|
9822
|
-
basePath: "/browser",
|
|
9823
|
-
recordBodies: true,
|
|
9824
|
-
maxTextChars: 2e4,
|
|
9825
|
-
maxLinks: 50,
|
|
9826
|
-
timeoutMs: 3e4,
|
|
9827
|
-
baselineDir: ".dsh-browser/baselines",
|
|
9828
|
-
selectorHealing: true,
|
|
9829
|
-
healTimeoutMs: 3e3,
|
|
9830
|
-
maxRefs: 200,
|
|
9831
|
-
maxRows: 1e3,
|
|
9832
|
-
snapshotHistory: 5,
|
|
9833
|
-
confirmMode: "off",
|
|
9834
|
-
confirmTimeoutMs: 6e4,
|
|
9835
|
-
sensitivePatterns: [
|
|
9836
|
-
"提交",
|
|
9837
|
-
"删除",
|
|
9838
|
-
"支付"
|
|
9839
|
-
],
|
|
9840
|
-
sensitiveTools: ["browser_click"],
|
|
9841
|
-
confirmInReplay: true,
|
|
9842
|
-
somFallback: true,
|
|
9843
|
-
refReanchor: true,
|
|
9844
|
-
toolset: "all"
|
|
9845
|
-
};
|
|
9846
9927
|
function apply(ctx, rawConfig) {
|
|
9847
|
-
const resolved = resolveConfig("dsh-browser-view",
|
|
9928
|
+
const resolved = resolveConfig("dsh-browser-view", BROWSER_CONFIG_SCHEMA, BROWSER_DEFAULT_CONFIG, rawConfig);
|
|
9848
9929
|
if (resolved.enabled === false) return;
|
|
9849
9930
|
const config = resolved;
|
|
9850
9931
|
const base = (config.basePath ?? "/browser").replace(/\/+$/, "");
|
|
@@ -9860,11 +9941,11 @@ function apply(ctx, rawConfig) {
|
|
|
9860
9941
|
const bindPrompt = (promptCtx) => {
|
|
9861
9942
|
const systemPrompt = promptCtx.get("systemPrompt") ?? promptCtx.systemPrompt;
|
|
9862
9943
|
if (systemPrompt === void 0) return;
|
|
9863
|
-
const
|
|
9944
|
+
const toolPrompt = browserSystemPrompt(config);
|
|
9864
9945
|
const sectionDispose = systemPrompt?.section?.({
|
|
9865
9946
|
name: "tool:browser",
|
|
9866
9947
|
order: 200,
|
|
9867
|
-
text:
|
|
9948
|
+
text: toolPrompt
|
|
9868
9949
|
});
|
|
9869
9950
|
if (sectionDispose !== void 0) promptCtx.effect(() => sectionDispose);
|
|
9870
9951
|
};
|