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/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
- * 当第一个监听器加入时自动激活 CDP Screencast;
2541
- * 当所有监听器离开时自动注销并停止 CDP,实现零空载开销。
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.start();
2700
+ if (this.listeners.size === 1) this.ensure();
2549
2701
  return () => {
2550
2702
  this.listeners.delete(listener);
2551
- if (this.listeners.size === 0) this.stop();
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
- * 启动 CDP Screencast
2728
+ * 收敛循环:让 (cdp, currentPage) 与 (desiredPage, listeners) 达成一致。
2729
+ * 每轮重读 desiredPage —— await 期间到来的新 syncTo 在下一轮被吞并,
2730
+ * 不会因启动中而丢掉换页请求。
2562
2731
  */
2563
- async start(targetPage) {
2564
- if (this.cdp !== null || this.isStarting) return true;
2565
- const page = targetPage ?? activePage();
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
- this.currentPage = page;
2570
- page.once("close", () => {
2571
- if (this.currentPage === page) this.stop();
2572
- });
2573
- const context = page.context();
2574
- if (typeof context.newCDPSession !== "function") {
2575
- this.isStarting = false;
2576
- return false;
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
- await cdp.send("Page.startScreencast", {
2596
- format: "jpeg",
2597
- quality: 60,
2598
- maxWidth: 1280,
2599
- maxHeight: 800,
2600
- everyNthFrame: 2
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", SCHEMA, DEFAULT_CONFIG, rawConfig);
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 confirmGuidance = config.confirmMode === "manual" ? "本环境 confirmMode 为 manual:凡提交/删除/支付等不可逆或影响线上数据的敏感动作,执行前必须先调用 browser_confirm(reason 说明操作与影响,等待用户在实时画面面板点允许/拒绝;拒绝或超时即放弃该动作,不得换路径绕过)。" : config.confirmMode === "auto" ? "本环境 confirmMode 为 auto:宿主层已在敏感动作(命中敏感模式的目标元素)前强制弹审批卡,agent 无法绕过;审批被拒绝或超时的动作不可换路径重试,应向用户说明并等待指示。" : "";
9944
+ const toolPrompt = browserSystemPrompt(config);
9864
9945
  const sectionDispose = systemPrompt?.section?.({
9865
9946
  name: "tool:browser",
9866
9947
  order: 200,
9867
- text: (config.toolset === "core" ? "浏览器工具操作一个共享浏览器会话(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。用户可通过界面右下角面板的实时画面查看浏览器。" : "浏览器工具操作一个共享浏览器会话,支持多标签页与多配置文件。定位操作元素最省事的方式是意图语义定位: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 时登录态会持久化。用户可通过界面右下角面板的实时画面查看浏览器。") + confirmGuidance
9948
+ text: toolPrompt
9868
9949
  });
9869
9950
  if (sectionDispose !== void 0) promptCtx.effect(() => sectionDispose);
9870
9951
  };