@angelky7/dsh-browser-control 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -64,8 +64,8 @@ In one line: **for "use MY browser" tasks (logged-in Bilibili, Zhihu, admin pane
64
64
 
65
65
  | File | Purpose |
66
66
  |---|---|
67
- | [DSH-Browser-Control-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/DSH-Browser-Control-v1.7.0.zip) | Chrome extension (unzip and load; this release adds the history navigation commands — reinstall required) |
68
- | [dsh-browser-control-plugin-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/dsh-browser-control-plugin-v1.7.0.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
67
+ | [DSH-Browser-Control-v1.7.1.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.1/DSH-Browser-Control-v1.7.1.zip) | Chrome extension (unzip and load; this release adds the metrics command and emulate throttling — reinstall required) |
68
+ | [dsh-browser-control-plugin-v1.7.1.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.1/dsh-browser-control-plugin-v1.7.1.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
69
69
 
70
70
  ## Install the Chrome extension (30 seconds)
71
71
 
@@ -104,7 +104,7 @@ npm install @angelky7/dsh-browser-control
104
104
 
105
105
  ```bash
106
106
  # Straight from GitHub
107
- dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.1"
108
108
 
109
109
  # Local checkout for debugging (note: the explicit file: prefix is required)
110
110
  dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
@@ -265,11 +265,12 @@ curl -X POST http://127.0.0.1:9777/api/command \
265
265
  | `browser_pdf` | Export the current page as PDF (v1.0.7+) |
266
266
  | `browser_archive` | Save the whole page as a single-file MHTML (text/images/styles inline; a greppable offline evidence snapshot — embedded scripts do not run, v1.5.0+) |
267
267
  | `browser_windows` | Window control: list / open / update state & geometry / close a whole window / move tabs across windows (the only tool that may touch window geometry, v1.6.0+) |
268
- | `browser_emulate` | Switch to a device viewport (mobile / desktop / custom, v1.0.7+) |
268
+ | `browser_emulate` | Switch to a device viewport (mobile / desktop / custom, v1.0.7+); v1.7.1+ adds throttling: `network` slow3g/fast3g/offline/reset + `cpuThrottlingRate` (1–20), the receipt states the applied numbers, `device:"reset"` clears them too |
269
+ | `browser_metrics` | Read performance counters (DOM nodes / JS heap / layout & script durations, Performance.getMetrics, read-only; puts numbers behind "this page is heavy", v1.7.1+) |
269
270
  | `browser_cleanup` | Clean up temp files |
270
271
  | `browser_info` | Which browser and extension build is attached (run it first when debugging, v1.0.9+) |
271
272
 
272
- 24 tools in total. `browser_wait`, `browser_find`, `browser_dialog` and `browser_info` existed inside the extension up to v1.0.8 but were never registered as tools, so the agent could not reach them; `browser_network_clear` existed in code and was simply missing from this table. `verify/verify_surface.mjs` now reconciles the extension command table, the commands the plugin calls, this table and the built bundle, so any drift fails `npm run verify`.
273
+ 25 tools in total. `browser_wait`, `browser_find`, `browser_dialog` and `browser_info` existed inside the extension up to v1.0.8 but were never registered as tools, so the agent could not reach them; `browser_network_clear` existed in code and was simply missing from this table. `verify/verify_surface.mjs` now reconciles the extension command table, the commands the plugin calls, this table and the built bundle, so any drift fails `npm run verify`.
273
274
 
274
275
  ## Architecture
275
276
 
package/README.md CHANGED
@@ -64,8 +64,8 @@ Chrome 扩展(CDP 驱动)
64
64
 
65
65
  | 文件 | 说明 |
66
66
  |---|---|
67
- | [DSH-Browser-Control-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/DSH-Browser-Control-v1.7.0.zip) | Chrome 扩展(解压后加载;这一版扩展新增 tabs.back/forward 历史导航命令,**必须重装**) |
68
- | [dsh-browser-control-plugin-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/dsh-browser-control-plugin-v1.7.0.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
67
+ | [DSH-Browser-Control-v1.7.1.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.1/DSH-Browser-Control-v1.7.1.zip) | Chrome 扩展(解压后加载;这一版扩展新增 metrics 命令与 emulate 节流参数,**必须重装**) |
68
+ | [dsh-browser-control-plugin-v1.7.1.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.1/dsh-browser-control-plugin-v1.7.1.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
69
69
 
70
70
  ## 安装 Chrome 扩展(30 秒)
71
71
 
@@ -104,7 +104,7 @@ npm install @angelky7/dsh-browser-control
104
104
 
105
105
  ```bash
106
106
  # 直接从 GitHub 安装
107
- dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.1"
108
108
 
109
109
  # 本地目录调试(注意:必须显式 file: 前缀)
110
110
  dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
@@ -258,12 +258,13 @@ curl -X POST http://127.0.0.1:9777/api/command \
258
258
  | `browser_network_clear` | 单独清空网络日志缓冲(v1.0.7+) |
259
259
  | `browser_pdf` | 当前页导出 PDF(v1.0.7+) |
260
260
  | `browser_archive` | 整页存成单文件 MHTML(正文/图片/样式全内联,可当文本 grep 的离线证据;是快照,内嵌脚本不会跑,v1.5.0+) |
261
- | `browser_emulate` | 切设备视口(移动 / 桌面 / 自定义,v1.0.7+) |
261
+ | `browser_emulate` | 切设备视口(移动 / 桌面 / 自定义,v1.0.7+);v1.7.1 起还能限速:`network` slow3g/fast3g/offline/reset + `cpuThrottlingRate`(1–20),回执写明生效数值,`device:"reset"` 一并还原 |
262
+ | `browser_metrics` | 读性能计数器(DOM 节点数 / JS 堆 / 布局与脚本耗时,Performance.getMetrics,只读;让「这页面很重」有数字说话,v1.7.1+) |
262
263
  | `browser_windows` | 窗口控制:清单/开窗/改状态与几何/关整窗/跨窗挪标签(唯一能碰窗口几何的工具,v1.6.0+) |
263
264
  | `browser_cleanup` | 清理临时文件 |
264
265
  | `browser_info` | 报告实际连上的浏览器与扩展版本(排障先跑它,v1.0.9+) |
265
266
 
266
- 共 24 个工具。`browser_wait` / `browser_find` / `browser_dialog` / `browser_info` 在 v1.0.8 及以前是「扩展里已实现、但没注册成工具」,agent 调不到;`browser_network_clear` 则是代码里有、这张表漏了。这些现在由 `verify/verify_surface.mjs` 做三方对账(扩展命令表 ↔ 插件调用 ↔ 文档表 ↔ 构建产物),漂移会直接让 `npm run verify` 失败。
267
+ 共 25 个工具。`browser_wait` / `browser_find` / `browser_dialog` / `browser_info` 在 v1.0.8 及以前是「扩展里已实现、但没注册成工具」,agent 调不到;`browser_network_clear` 则是代码里有、这张表漏了。这些现在由 `verify/verify_surface.mjs` 做三方对账(扩展命令表 ↔ 插件调用 ↔ 文档表 ↔ 构建产物),漂移会直接让 `npm run verify` 失败。
267
268
 
268
269
  ## 架构
269
270
 
package/lib/index.js CHANGED
@@ -878,7 +878,8 @@ const READ_ONLY_ALLOWED = /* @__PURE__ */ new Set([
878
878
  "console.log",
879
879
  "network.log",
880
880
  "windows.list",
881
- "cookies.get"
881
+ "cookies.get",
882
+ "metrics"
882
883
  ]);
883
884
  /**
884
885
  * 会执行**任意页面脚本**的扩展命令。这条与只读档分开管,因为用户可能想「可以读、
@@ -2853,7 +2854,7 @@ function applyBrowserTools(ctx, controller) {
2853
2854
  }));
2854
2855
  ctx.tools.register(defineTool({
2855
2856
  name: "browser_emulate",
2856
- description: "Switch the tab into a device viewport (mobile / tablet / desktop) for responsive-UI testing. `device:\"reset\"` restores the user's actual viewport. Custom `width`/`height`/`deviceScaleFactor`/`isMobile`/`hasTouch` override any preset field.",
2857
+ description: "Switch the tab into a device viewport (mobile / tablet / desktop) for responsive-UI testing, and throttle the network or CPU to test slow conditions. `device:\"reset\"` restores the user's actual viewport and clears all throttling. `network` presets (this tool's own numbers): slow3g = 400ms RTT + ~50KB/s both ways, fast3g = 150ms + ~200KB/s down / ~94KB/s up, offline, reset to clear. `cpuThrottlingRate` multiplies CPU cost (1 = off, up to 20). Throttling lives in the debugger session — it is gone when the extension detaches, and device:\"reset\" clears it too. Custom `width`/`height`/`deviceScaleFactor`/`isMobile`/`hasTouch` override any preset field.",
2857
2858
  parameters: {
2858
2859
  tabId: {
2859
2860
  type: "number",
@@ -2863,6 +2864,14 @@ function applyBrowserTools(ctx, controller) {
2863
2864
  type: "string",
2864
2865
  description: "Preset: desktop | mobile-iphone-13 | mobile-pixel-7 | tablet-ipad | reset. Or pass custom width/height below."
2865
2866
  },
2867
+ network: {
2868
+ type: "string",
2869
+ description: "slow3g | fast3g | offline | reset. The receipt reports the applied latency/throughput in bytes per second."
2870
+ },
2871
+ cpuThrottlingRate: {
2872
+ type: "number",
2873
+ description: "CPU slowdown multiplier: 1 = none (default), 4 = mid-range phone, up to 20."
2874
+ },
2866
2875
  width: {
2867
2876
  type: "number",
2868
2877
  description: "Custom viewport width in CSS px."
@@ -2903,30 +2912,39 @@ function applyBrowserTools(ctx, controller) {
2903
2912
  deviceScaleFactor: { type: "number" },
2904
2913
  isMobile: { type: "boolean" },
2905
2914
  hasTouch: { type: "boolean" },
2906
- userAgent: { type: "string" }
2915
+ userAgent: { type: "string" },
2916
+ network: { type: "string" },
2917
+ latency: { type: "number" },
2918
+ downloadThroughput: { type: "number" },
2919
+ uploadThroughput: { type: "number" },
2920
+ cpuThrottlingRate: { type: "number" }
2907
2921
  }
2908
2922
  },
2909
2923
  render: (_args, value) => {
2910
2924
  if (value.reset) return [{
2911
2925
  type: "text",
2912
- text: `Tab ${value.tabId}: emulation reset to default`
2926
+ text: `Tab ${value.tabId}: emulation reset to default (network unthrottled, CPU rate 1)`
2913
2927
  }];
2914
2928
  const ua = value.userAgent === void 0 ? "" : ` UA="${flattenOneLine(value.userAgent, 80)}"`;
2929
+ const net = value.network === void 0 ? "" : ` network=${value.network}(${value.latency}ms↓${value.downloadThroughput}B/s↑${value.uploadThroughput}B/s)`;
2930
+ const cpu = value.cpuThrottlingRate === void 0 ? "" : ` cpu×${value.cpuThrottlingRate}`;
2915
2931
  return [{
2916
2932
  type: "text",
2917
- text: `Tab ${value.tabId}: ${value.width || "?"}×${value.height || "?"} DPR=${value.deviceScaleFactor ?? "?"} mobile=${value.isMobile ?? false} touch=${value.hasTouch ?? false}${ua}`
2933
+ text: `Tab ${value.tabId}: ${value.width || "?"}×${value.height || "?"} DPR=${value.deviceScaleFactor ?? "?"} mobile=${value.isMobile ?? false} touch=${value.hasTouch ?? false}${ua}${net}${cpu}`
2918
2934
  }];
2919
2935
  }
2920
2936
  },
2921
2937
  presentCall: (args) => ({
2922
2938
  card: "generic",
2923
- title: `Emulate${args.device ? " " + args.device : " device"}`,
2939
+ title: `Emulate${args.device ? " " + args.device : ""}${args.network ? " " + args.network : ""}${args.cpuThrottlingRate ? " cpu×" + args.cpuThrottlingRate : ""}`,
2924
2940
  kind: "other"
2925
2941
  }),
2926
2942
  async execute(args, exec) {
2927
2943
  const params = {};
2928
2944
  if (args.tabId !== void 0) params.tabId = args.tabId;
2929
2945
  if (typeof args.device === "string") params.device = args.device;
2946
+ if (typeof args.network === "string") params.network = args.network;
2947
+ if (typeof args.cpuThrottlingRate === "number") params.cpuThrottlingRate = args.cpuThrottlingRate;
2930
2948
  if (typeof args.width === "number") params.width = args.width;
2931
2949
  if (typeof args.height === "number") params.height = args.height;
2932
2950
  if (typeof args.deviceScaleFactor === "number") params.deviceScaleFactor = args.deviceScaleFactor;
@@ -2942,10 +2960,81 @@ function applyBrowserTools(ctx, controller) {
2942
2960
  ...typeof raw.deviceScaleFactor === "number" ? { deviceScaleFactor: raw.deviceScaleFactor } : {},
2943
2961
  ...typeof raw.isMobile === "boolean" ? { isMobile: raw.isMobile } : {},
2944
2962
  ...typeof raw.hasTouch === "boolean" ? { hasTouch: raw.hasTouch } : {},
2945
- ...typeof raw.userAgent === "string" && raw.userAgent.length > 0 ? { userAgent: raw.userAgent } : {}
2963
+ ...typeof raw.userAgent === "string" && raw.userAgent.length > 0 ? { userAgent: raw.userAgent } : {},
2964
+ ...typeof raw.network === "string" ? { network: raw.network } : {},
2965
+ ...typeof raw.latency === "number" ? { latency: raw.latency } : {},
2966
+ ...typeof raw.downloadThroughput === "number" ? { downloadThroughput: raw.downloadThroughput } : {},
2967
+ ...typeof raw.uploadThroughput === "number" ? { uploadThroughput: raw.uploadThroughput } : {},
2968
+ ...typeof raw.cpuThrottlingRate === "number" ? { cpuThrottlingRate: raw.cpuThrottlingRate } : {}
2946
2969
  };
2947
2970
  }
2948
2971
  }));
2972
+ ctx.tools.register(defineTool({
2973
+ name: "browser_metrics",
2974
+ description: "Read the tab's performance counters (Performance.getMetrics): DOM node count, JS heap usage, layout/style recalc counts, script/task durations. Use it to back \"this page is heavy\" with numbers, before and after an action. Read-only.",
2975
+ parameters: { tabId: {
2976
+ type: "number",
2977
+ description: "Target tab; defaults to the active tab."
2978
+ } },
2979
+ output: {
2980
+ schema: {
2981
+ type: "object",
2982
+ additionalProperties: false,
2983
+ properties: {
2984
+ tabId: {
2985
+ type: "number",
2986
+ required: true
2987
+ },
2988
+ metrics: {
2989
+ type: "object",
2990
+ required: true,
2991
+ additionalProperties: false,
2992
+ properties: {
2993
+ Timestamp: { type: "number" },
2994
+ Documents: { type: "number" },
2995
+ Frames: { type: "number" },
2996
+ JSEventListeners: { type: "number" },
2997
+ Nodes: { type: "number" },
2998
+ LayoutCount: { type: "number" },
2999
+ RecalcStyleCount: { type: "number" },
3000
+ LayoutDuration: { type: "number" },
3001
+ RecalcStyleDuration: { type: "number" },
3002
+ ScriptDuration: { type: "number" },
3003
+ TaskDuration: { type: "number" },
3004
+ JSHeapUsedSize: { type: "number" },
3005
+ JSHeapTotalSize: { type: "number" }
3006
+ }
3007
+ }
3008
+ }
3009
+ },
3010
+ render: (_args, value) => {
3011
+ const m = value.metrics ?? {};
3012
+ const parts = [];
3013
+ if (m.Nodes !== void 0) parts.push(`nodes=${m.Nodes}`);
3014
+ if (m.JSHeapUsedSize !== void 0) {
3015
+ const total = m.JSHeapTotalSize !== void 0 ? `/${(m.JSHeapTotalSize / 1048576).toFixed(1)}MB` : "";
3016
+ parts.push(`heap=${(m.JSHeapUsedSize / 1048576).toFixed(1)}${total}`);
3017
+ }
3018
+ if (m.LayoutCount !== void 0) parts.push(`layouts=${m.LayoutCount}`);
3019
+ if (m.ScriptDuration !== void 0) parts.push(`script=${Number(m.ScriptDuration).toFixed(2)}s`);
3020
+ if (m.TaskDuration !== void 0) parts.push(`tasks=${Number(m.TaskDuration).toFixed(2)}s`);
3021
+ return [{
3022
+ type: "text",
3023
+ text: `Tab ${value.tabId}: ${parts.join(" ") || JSON.stringify(value.metrics)}`
3024
+ }];
3025
+ }
3026
+ },
3027
+ presentCall: () => ({
3028
+ card: "generic",
3029
+ title: "Browser metrics",
3030
+ kind: "other"
3031
+ }),
3032
+ async execute(args, exec) {
3033
+ const params = {};
3034
+ if (args.tabId !== void 0) params.tabId = args.tabId;
3035
+ return await controller.execute("metrics", params, exec.signal);
3036
+ }
3037
+ }));
2949
3038
  }
2950
3039
  /** Cordis plugin entry: wire the settings-driven lifecycle plus the model-facing tools. */
2951
3040
  function apply(ctx, config) {
@@ -1446,10 +1446,12 @@ function applyBrowserTools(ctx, controller) {
1446
1446
  }));
1447
1447
  ctx.tools.register(defineTool({
1448
1448
  name: 'browser_emulate',
1449
- description: 'Switch the tab into a device viewport (mobile / tablet / desktop) for responsive-UI testing. `device:"reset"` restores the user\'s actual viewport. Custom `width`/`height`/`deviceScaleFactor`/`isMobile`/`hasTouch` override any preset field.',
1449
+ description: 'Switch the tab into a device viewport (mobile / tablet / desktop) for responsive-UI testing, and throttle the network or CPU to test slow conditions. `device:"reset"` restores the user\'s actual viewport and clears all throttling. `network` presets (this tool\'s own numbers): slow3g = 400ms RTT + ~50KB/s both ways, fast3g = 150ms + ~200KB/s down / ~94KB/s up, offline, reset to clear. `cpuThrottlingRate` multiplies CPU cost (1 = off, up to 20). Throttling lives in the debugger session — it is gone when the extension detaches, and device:"reset" clears it too. Custom `width`/`height`/`deviceScaleFactor`/`isMobile`/`hasTouch` override any preset field.',
1450
1450
  parameters: {
1451
1451
  tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
1452
1452
  device: { type: 'string', description: 'Preset: desktop | mobile-iphone-13 | mobile-pixel-7 | tablet-ipad | reset. Or pass custom width/height below.' },
1453
+ network: { type: 'string', description: 'slow3g | fast3g | offline | reset. The receipt reports the applied latency/throughput in bytes per second.' },
1454
+ cpuThrottlingRate: { type: 'number', description: 'CPU slowdown multiplier: 1 = none (default), 4 = mid-range phone, up to 20.' },
1453
1455
  width: { type: 'number', description: 'Custom viewport width in CSS px.' },
1454
1456
  height: { type: 'number', description: 'Custom viewport height in CSS px.' },
1455
1457
  deviceScaleFactor: { type: 'number', description: 'Custom DPR (1 = standard, 2 = retina, 3 = super-retina).' },
@@ -1470,26 +1472,37 @@ function applyBrowserTools(ctx, controller) {
1470
1472
  isMobile: { type: 'boolean' },
1471
1473
  hasTouch: { type: 'boolean' },
1472
1474
  userAgent: { type: 'string' },
1475
+ network: { type: 'string' },
1476
+ latency: { type: 'number' },
1477
+ downloadThroughput: { type: 'number' },
1478
+ uploadThroughput: { type: 'number' },
1479
+ cpuThrottlingRate: { type: 'number' },
1473
1480
  },
1474
1481
  },
1475
1482
  render: (_args, value) => {
1476
1483
  if (value.reset)
1477
- return [{ type: 'text', text: `Tab ${value.tabId}: emulation reset to default` }];
1478
- // 覆盖生效的是哪个 UA 必须写出来:模型只看这句,看不见 value 里的字段。
1484
+ return [{ type: 'text', text: `Tab ${value.tabId}: emulation reset to default (network unthrottled, CPU rate 1)` }];
1485
+ // 覆盖生效的是哪个 UA / 哪档节流必须写出来:模型只看这句,看不见 value 里的字段。
1479
1486
  const ua = value.userAgent === undefined ? '' : ` UA="${flattenOneLine(value.userAgent, 80)}"`;
1487
+ const net = value.network === undefined ? '' : ` network=${value.network}(${value.latency}ms↓${value.downloadThroughput}B/s↑${value.uploadThroughput}B/s)`;
1488
+ const cpu = value.cpuThrottlingRate === undefined ? '' : ` cpu×${value.cpuThrottlingRate}`;
1480
1489
  return [{
1481
1490
  type: 'text',
1482
- text: `Tab ${value.tabId}: ${value.width || '?'}×${value.height || '?'} DPR=${value.deviceScaleFactor ?? '?'} mobile=${value.isMobile ?? false} touch=${value.hasTouch ?? false}${ua}`,
1491
+ text: `Tab ${value.tabId}: ${value.width || '?'}×${value.height || '?'} DPR=${value.deviceScaleFactor ?? '?'} mobile=${value.isMobile ?? false} touch=${value.hasTouch ?? false}${ua}${net}${cpu}`,
1483
1492
  }];
1484
1493
  },
1485
1494
  },
1486
- presentCall: args => ({ card: 'generic', title: `Emulate${args.device ? ' ' + args.device : ' device'}`, kind: 'other' }),
1495
+ presentCall: args => ({ card: 'generic', title: `Emulate${args.device ? ' ' + args.device : ''}${args.network ? ' ' + args.network : ''}${args.cpuThrottlingRate ? ' cpu×' + args.cpuThrottlingRate : ''}`, kind: 'other' }),
1487
1496
  async execute(args, exec) {
1488
1497
  const params = {};
1489
1498
  if (args.tabId !== undefined)
1490
1499
  params.tabId = args.tabId;
1491
1500
  if (typeof args.device === 'string')
1492
1501
  params.device = args.device;
1502
+ if (typeof args.network === 'string')
1503
+ params.network = args.network;
1504
+ if (typeof args.cpuThrottlingRate === 'number')
1505
+ params.cpuThrottlingRate = args.cpuThrottlingRate;
1493
1506
  if (typeof args.width === 'number')
1494
1507
  params.width = args.width;
1495
1508
  if (typeof args.height === 'number')
@@ -1515,9 +1528,77 @@ function applyBrowserTools(ctx, controller) {
1515
1528
  ...(typeof raw.isMobile === 'boolean' ? { isMobile: raw.isMobile } : {}),
1516
1529
  ...(typeof raw.hasTouch === 'boolean' ? { hasTouch: raw.hasTouch } : {}),
1517
1530
  ...(typeof raw.userAgent === 'string' && raw.userAgent.length > 0 ? { userAgent: raw.userAgent } : {}),
1531
+ ...(typeof raw.network === 'string' ? { network: raw.network } : {}),
1532
+ ...(typeof raw.latency === 'number' ? { latency: raw.latency } : {}),
1533
+ ...(typeof raw.downloadThroughput === 'number' ? { downloadThroughput: raw.downloadThroughput } : {}),
1534
+ ...(typeof raw.uploadThroughput === 'number' ? { uploadThroughput: raw.uploadThroughput } : {}),
1535
+ ...(typeof raw.cpuThrottlingRate === 'number' ? { cpuThrottlingRate: raw.cpuThrottlingRate } : {}),
1518
1536
  };
1519
1537
  },
1520
1538
  }));
1539
+ // Performance metrics: a pure read of CDP counters — whitelisted under
1540
+ // readOnly, no effect on the page, and the numbers ARE the content.
1541
+ ctx.tools.register(defineTool({
1542
+ name: 'browser_metrics',
1543
+ description: 'Read the tab\'s performance counters (Performance.getMetrics): DOM node count, JS heap usage, layout/style recalc counts, script/task durations. Use it to back "this page is heavy" with numbers, before and after an action. Read-only.',
1544
+ parameters: {
1545
+ tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
1546
+ },
1547
+ output: {
1548
+ schema: {
1549
+ type: 'object',
1550
+ additionalProperties: false,
1551
+ properties: {
1552
+ tabId: { type: 'number', required: true },
1553
+ metrics: {
1554
+ type: 'object',
1555
+ required: true,
1556
+ additionalProperties: false,
1557
+ properties: {
1558
+ Timestamp: { type: 'number' },
1559
+ Documents: { type: 'number' },
1560
+ Frames: { type: 'number' },
1561
+ JSEventListeners: { type: 'number' },
1562
+ Nodes: { type: 'number' },
1563
+ LayoutCount: { type: 'number' },
1564
+ RecalcStyleCount: { type: 'number' },
1565
+ LayoutDuration: { type: 'number' },
1566
+ RecalcStyleDuration: { type: 'number' },
1567
+ ScriptDuration: { type: 'number' },
1568
+ TaskDuration: { type: 'number' },
1569
+ JSHeapUsedSize: { type: 'number' },
1570
+ JSHeapTotalSize: { type: 'number' },
1571
+ },
1572
+ },
1573
+ },
1574
+ },
1575
+ render: (_args, value) => {
1576
+ const m = (value.metrics ?? {});
1577
+ // 数字本身就是产出:关键几项原样进模型可见文本,heap 顺手换算成 MB。
1578
+ const parts = [];
1579
+ if (m.Nodes !== undefined)
1580
+ parts.push(`nodes=${m.Nodes}`);
1581
+ if (m.JSHeapUsedSize !== undefined) {
1582
+ const total = m.JSHeapTotalSize !== undefined ? `/${(m.JSHeapTotalSize / 1_048_576).toFixed(1)}MB` : '';
1583
+ parts.push(`heap=${(m.JSHeapUsedSize / 1_048_576).toFixed(1)}${total}`);
1584
+ }
1585
+ if (m.LayoutCount !== undefined)
1586
+ parts.push(`layouts=${m.LayoutCount}`);
1587
+ if (m.ScriptDuration !== undefined)
1588
+ parts.push(`script=${Number(m.ScriptDuration).toFixed(2)}s`);
1589
+ if (m.TaskDuration !== undefined)
1590
+ parts.push(`tasks=${Number(m.TaskDuration).toFixed(2)}s`);
1591
+ return [{ type: 'text', text: `Tab ${value.tabId}: ${parts.join(' ') || JSON.stringify(value.metrics)}` }];
1592
+ },
1593
+ },
1594
+ presentCall: () => ({ card: 'generic', title: 'Browser metrics', kind: 'other' }),
1595
+ async execute(args, exec) {
1596
+ const params = {};
1597
+ if (args.tabId !== undefined)
1598
+ params.tabId = args.tabId;
1599
+ return await controller.execute('metrics', params, exec.signal);
1600
+ },
1601
+ }));
1521
1602
  }
1522
1603
  /** Cordis plugin entry: wire the settings-driven lifecycle plus the model-facing tools. */
1523
1604
  export function apply(ctx, config) {
@@ -28,6 +28,9 @@ export const READ_ONLY_ALLOWED = new Set([
28
28
  'windows.list',
29
29
  // 读浏览器自己的状态(cookie 是凭据,但 get 不改任何东西;set/remove 不在此列)
30
30
  'cookies.get',
31
+ // 读性能计数器(Performance.getMetrics):纯粹读,重试无损害;节流(emulate
32
+ // 的 network/cpu 参数)改页面加载行为,emulate 整体仍是写命令,不在此列。
33
+ 'metrics',
31
34
  ]);
32
35
  /**
33
36
  * 会执行**任意页面脚本**的扩展命令。这条与只读档分开管,因为用户可能想「可以读、
package/llms.txt CHANGED
@@ -35,7 +35,7 @@ Chrome 浏览器扩展 + DeepSeek Harness 插件,让 AI Agent 通过 CDP 驱
35
35
 
36
36
  ## 关键文件
37
37
 
38
- - `src/index.ts` - 24 个 `browser_*` 工具注册 + settings 接线的版本兼容分支
38
+ - `src/index.ts` - 25 个 `browser_*` 工具注册 + settings 接线的版本兼容分支
39
39
  - `src/policy.ts` - 三张互不相同的表:只读白名单、脚本命令、插件本地破坏性命令(`browser_cleanup` 不经过 `execute()`,必须自己站闸门)
40
40
  - `src/server.ts` - WebSocket 服务 + HTTP 面(鉴权、来源校验、状态页)
41
41
  - `src/ws.ts` - RFC 6455 帧编解码
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@angelky7/dsh-browser-control",
3
3
  "description": "Local WebSocket bridge between dsh and the DSH Browser Control browser extension, with model-facing browser_* tools and a Settings-managed on/off switch",
4
- "version": "1.7.0",
4
+ "version": "1.7.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "build": "tsc -p tsconfig.json && tsdown",
38
38
  "typecheck": "tsc -p tsconfig.json --noEmit",
39
39
  "check": "npm run typecheck && npm run build && publint",
40
- "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs"
40
+ "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs && node verify/verify_metrics_throttle.mjs"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",