@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 +6 -5
- package/README.md +6 -5
- package/lib/index.js +96 -7
- package/lib/types/index.js +86 -5
- package/lib/types/policy.js +3 -0
- package/llms.txt +1 -1
- package/package.json +2 -2
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.
|
|
68
|
-
| [dsh-browser-control-plugin-v1.7.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
68
|
-
| [dsh-browser-control-plugin-v1.7.
|
|
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.
|
|
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
|
-
共
|
|
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 : "
|
|
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) {
|
package/lib/types/index.js
CHANGED
|
@@ -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
|
|
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 : '
|
|
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) {
|
package/lib/types/policy.js
CHANGED
|
@@ -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` -
|
|
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.
|
|
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",
|