@angelky7/dsh-browser-control 1.5.0 → 1.7.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.en.md +7 -6
- package/README.md +7 -6
- package/lib/index.js +200 -11
- package/lib/types/errors.js +7 -0
- package/lib/types/index.js +148 -8
- package/lib/types/policy.js +2 -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.
|
|
68
|
-
| [dsh-browser-control-plugin-v1.
|
|
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) |
|
|
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.
|
|
107
|
+
dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
|
|
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"
|
|
@@ -256,19 +256,20 @@ curl -X POST http://127.0.0.1:9777/api/command \
|
|
|
256
256
|
| `browser_dialog` | Read or set the native-dialog policy accept/dismiss/manual, plus the recent dialog log (v1.0.9+) |
|
|
257
257
|
| `browser_hold` | Put or remove a cancellable take-over overlay on a tab (tells the user, stops mis-clicks); `covered:false` says why it could not cover the page. A soft layer, not a security boundary |
|
|
258
258
|
| `browser_cookies` | Read/set/remove cookies visible to a tab (`action: get/set/remove`; get takes a `domain` filter, set/remove need a `url` or `domain` anchor, remove re-checks and reports `remaining`). **Cookie values are credentials** — a get puts them fully into the model-visible text; see SECURITY.md |
|
|
259
|
-
| `browser_tabs` | Tab management (list/open/close/activate/group/ungroup;
|
|
259
|
+
| `browser_tabs` | Tab management (list/open/close/activate/group/ungroup; v1.7.0+ history back/forward — same arrival judging as navigate, reports unchanged at the history edge) |
|
|
260
260
|
| `browser_evaluate` | Run arbitrary JS |
|
|
261
|
-
| `browser_screenshot` | Capture page screenshot |
|
|
261
|
+
| `browser_screenshot` | Capture page screenshot (v1.7.0+ receipt carries the real pixel size; fullPage captures the whole scrollable page, selector captures one element) |
|
|
262
262
|
| `browser_console_log` | Captured console entries, listed with their text (v1.0.7+) |
|
|
263
263
|
| `browser_network_log` | Captured HTTP requests, listed with method/status/URL (v1.0.7+) |
|
|
264
264
|
| `browser_network_clear` | Clear the network log buffer on its own (v1.0.7+) |
|
|
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
|
+
| `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+) |
|
|
267
268
|
| `browser_emulate` | Switch to a device viewport (mobile / desktop / custom, v1.0.7+) |
|
|
268
269
|
| `browser_cleanup` | Clean up temp files |
|
|
269
270
|
| `browser_info` | Which browser and extension build is attached (run it first when debugging, v1.0.9+) |
|
|
270
271
|
|
|
271
|
-
|
|
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`.
|
|
272
273
|
|
|
273
274
|
## Architecture
|
|
274
275
|
|
package/README.md
CHANGED
|
@@ -64,8 +64,8 @@ Chrome 扩展(CDP 驱动)
|
|
|
64
64
|
|
|
65
65
|
| 文件 | 说明 |
|
|
66
66
|
|---|---|
|
|
67
|
-
| [DSH-Browser-Control-v1.
|
|
68
|
-
| [dsh-browser-control-plugin-v1.
|
|
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) |
|
|
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.
|
|
107
|
+
dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
|
|
108
108
|
|
|
109
109
|
# 本地目录调试(注意:必须显式 file: 前缀)
|
|
110
110
|
dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
|
|
@@ -250,19 +250,20 @@ curl -X POST http://127.0.0.1:9777/api/command \
|
|
|
250
250
|
| `browser_dialog` | 读改原生弹窗应答策略 accept/dismiss/manual,并回最近弹窗记录(v1.0.9+) |
|
|
251
251
|
| `browser_hold` | 在某个标签页上加/撤一层可取消的接管浮层(告知用户 + 防误触);`covered:false` 时说明盖不住的原因。软层,不是安全边界 |
|
|
252
252
|
| `browser_cookies` | 读/设/删某标签页可见的 cookie(`action: get/set/remove`;get 带 `domain` 过滤,set/remove 需要 `url` 或 `domain` 锚点,remove 复查并回报 `remaining`)。**cookie 的值是凭据**,get 会把值完整放进模型可见文本——见 SECURITY.md |
|
|
253
|
-
| `browser_tabs` |
|
|
253
|
+
| `browser_tabs` | 标签页管理(列表/新建/关闭/切换/归组/拆组;v1.7.0 起支持历史 back/forward——落地判据与 navigate 同一套,历史尽头如实报 unchanged) |
|
|
254
254
|
| `browser_evaluate` | 执行任意 JS |
|
|
255
|
-
| `browser_screenshot` |
|
|
255
|
+
| `browser_screenshot` | 截取页面截图(v1.7.0 起回执带**真实像素尺寸**;fullPage 一张截全页、selector 只截一个元素) |
|
|
256
256
|
| `browser_console_log` | 抓取页面 console 日志并逐条给出正文(v1.0.7+) |
|
|
257
257
|
| `browser_network_log` | 抓取 HTTP 请求/响应并逐条给出方法/状态/URL(v1.0.7+) |
|
|
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
261
|
| `browser_emulate` | 切设备视口(移动 / 桌面 / 自定义,v1.0.7+) |
|
|
262
|
+
| `browser_windows` | 窗口控制:清单/开窗/改状态与几何/关整窗/跨窗挪标签(唯一能碰窗口几何的工具,v1.6.0+) |
|
|
262
263
|
| `browser_cleanup` | 清理临时文件 |
|
|
263
264
|
| `browser_info` | 报告实际连上的浏览器与扩展版本(排障先跑它,v1.0.9+) |
|
|
264
265
|
|
|
265
|
-
共
|
|
266
|
+
共 24 个工具。`browser_wait` / `browser_find` / `browser_dialog` / `browser_info` 在 v1.0.8 及以前是「扩展里已实现、但没注册成工具」,agent 调不到;`browser_network_clear` 则是代码里有、这张表漏了。这些现在由 `verify/verify_surface.mjs` 做三方对账(扩展命令表 ↔ 插件调用 ↔ 文档表 ↔ 构建产物),漂移会直接让 `npm run verify` 失败。
|
|
266
267
|
|
|
267
268
|
## 架构
|
|
268
269
|
|
package/lib/index.js
CHANGED
|
@@ -117,8 +117,11 @@ const WRITE_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
117
117
|
"tabs.closeMany",
|
|
118
118
|
"tabs.activate",
|
|
119
119
|
"dialog",
|
|
120
|
+
"tabs.back",
|
|
121
|
+
"tabs.forward",
|
|
120
122
|
"cleanup",
|
|
121
|
-
"hold"
|
|
123
|
+
"hold",
|
|
124
|
+
"windows.open"
|
|
122
125
|
]);
|
|
123
126
|
/** 该命令是否可能留下副作用。 */
|
|
124
127
|
function isWriteCommand(command) {
|
|
@@ -874,6 +877,7 @@ const READ_ONLY_ALLOWED = /* @__PURE__ */ new Set([
|
|
|
874
877
|
"tabs.list",
|
|
875
878
|
"console.log",
|
|
876
879
|
"network.log",
|
|
880
|
+
"windows.list",
|
|
877
881
|
"cookies.get"
|
|
878
882
|
]);
|
|
879
883
|
/**
|
|
@@ -1183,6 +1187,10 @@ function requireTabId(args, action) {
|
|
|
1183
1187
|
if (typeof args.tabId !== "number") throw new Error(`${action} requires tabId`);
|
|
1184
1188
|
return args.tabId;
|
|
1185
1189
|
}
|
|
1190
|
+
function requireWindowId(args, action) {
|
|
1191
|
+
if (typeof args.windowId !== "number") throw new Error(`${action} requires windowId`);
|
|
1192
|
+
return args.windowId;
|
|
1193
|
+
}
|
|
1186
1194
|
/**
|
|
1187
1195
|
* Every tab the caller asked to close. `tabId` and `tabIds` are both accepted
|
|
1188
1196
|
* (a batch call that repeats one id, or passes both, must not close it twice),
|
|
@@ -1194,6 +1202,17 @@ function closeTabIds(args) {
|
|
|
1194
1202
|
if (ids.length === 0) throw new Error("close requires tabId or tabIds");
|
|
1195
1203
|
return ids;
|
|
1196
1204
|
}
|
|
1205
|
+
/** Actual pixel dimensions straight from the PNG's IHDR chunk (width/height are
|
|
1206
|
+
* big-endian at offsets 16/20 after the 8-byte signature + 4-byte length + 4-byte
|
|
1207
|
+
* type). The receipt should say how big the picture IS — fullPage captures are
|
|
1208
|
+
* often 3-5 viewports tall and the model cannot tell from the byte count. */
|
|
1209
|
+
function pngDimensions(buffer) {
|
|
1210
|
+
if (buffer.length < 24 || buffer.toString("latin1", 12, 16) !== "IHDR") return {};
|
|
1211
|
+
return {
|
|
1212
|
+
width: buffer.readUInt32BE(16),
|
|
1213
|
+
height: buffer.readUInt32BE(20)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1197
1216
|
/** Write one screenshot payload to the shots directory and return its durable location. */
|
|
1198
1217
|
async function saveScreenshot(controller, payload) {
|
|
1199
1218
|
const dir = controller.shotsDir;
|
|
@@ -1204,12 +1223,15 @@ async function saveScreenshot(controller, payload) {
|
|
|
1204
1223
|
const file = path.join(dir, `${stamp}-${random}.${payload.format === "jpeg" ? "jpg" : "png"}`);
|
|
1205
1224
|
const buffer = Buffer.from(payload.base64, "base64");
|
|
1206
1225
|
await writeFile(file, buffer);
|
|
1226
|
+
const dims = payload.format === "png" ? pngDimensions(buffer) : {};
|
|
1207
1227
|
return {
|
|
1208
1228
|
file,
|
|
1209
1229
|
bytes: buffer.length,
|
|
1210
1230
|
tabId: payload.tabId,
|
|
1211
1231
|
title: payload.tabTitle,
|
|
1212
|
-
url: payload.tabUrl
|
|
1232
|
+
url: payload.tabUrl,
|
|
1233
|
+
...dims,
|
|
1234
|
+
...payload.elementRect === void 0 ? {} : { elementRect: payload.elementRect }
|
|
1213
1235
|
};
|
|
1214
1236
|
}
|
|
1215
1237
|
/** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
|
|
@@ -2035,12 +2057,12 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2035
2057
|
}));
|
|
2036
2058
|
ctx.tools.register(defineTool({
|
|
2037
2059
|
name: "browser_tabs",
|
|
2038
|
-
description: "List tabs, or open/close/activate/group/ungroup. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.",
|
|
2060
|
+
description: "List tabs, or open/close/activate/group/ungroup, or go back/forward in the tab's history. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.",
|
|
2039
2061
|
parameters: {
|
|
2040
2062
|
action: {
|
|
2041
2063
|
type: "string",
|
|
2042
2064
|
required: true,
|
|
2043
|
-
description: "One of: list, open, close, activate, group, ungroup."
|
|
2065
|
+
description: "One of: list, open, close, activate, group, ungroup, back, forward."
|
|
2044
2066
|
},
|
|
2045
2067
|
url: {
|
|
2046
2068
|
type: "string",
|
|
@@ -2048,7 +2070,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2048
2070
|
},
|
|
2049
2071
|
tabId: {
|
|
2050
2072
|
type: "number",
|
|
2051
|
-
description: "Target tab for close/activate/group."
|
|
2073
|
+
description: "Target tab for close/activate/group/back/forward; defaults to the active tab for back/forward."
|
|
2052
2074
|
},
|
|
2053
2075
|
tabIds: {
|
|
2054
2076
|
type: "array",
|
|
@@ -2065,11 +2087,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2065
2087
|
},
|
|
2066
2088
|
waitUntil: {
|
|
2067
2089
|
type: "string",
|
|
2068
|
-
description: "For open: when the
|
|
2090
|
+
description: "For open/back/forward: when the navigation counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none). back/forward report unchanged:true + a reason when the tab is already at the oldest/newest history entry."
|
|
2069
2091
|
},
|
|
2070
2092
|
timeoutMs: {
|
|
2071
2093
|
type: "number",
|
|
2072
|
-
description: "For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000)."
|
|
2094
|
+
description: "For open/back/forward: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000)."
|
|
2073
2095
|
},
|
|
2074
2096
|
windowId: {
|
|
2075
2097
|
type: "number",
|
|
@@ -2115,6 +2137,18 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2115
2137
|
return ids.length === 1 ? await controller.execute("tabs.close", { tabId: ids[0] }, exec.signal) : await controller.execute("tabs.closeMany", { tabIds: ids }, exec.signal);
|
|
2116
2138
|
}
|
|
2117
2139
|
case "activate": return await controller.execute("tabs.activate", { tabId: requireTabId(args, "activate") }, exec.signal);
|
|
2140
|
+
case "back":
|
|
2141
|
+
case "forward": {
|
|
2142
|
+
const waitUntil = args.waitUntil === void 0 ? "load" : String(args.waitUntil);
|
|
2143
|
+
if (!(waitUntil in NAV_TIERS)) throw new Error(`browser_tabs ${args.action}: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
|
|
2144
|
+
const params = { waitUntil };
|
|
2145
|
+
if (args.tabId !== void 0) params.tabId = args.tabId;
|
|
2146
|
+
let budgetMs = NAV_TIERS[waitUntil];
|
|
2147
|
+
if (typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs)) budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
|
|
2148
|
+
params.timeoutMs = budgetMs;
|
|
2149
|
+
if (args.action === "back") return await controller.execute("tabs.back", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
|
|
2150
|
+
return await controller.execute("tabs.forward", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
|
|
2151
|
+
}
|
|
2118
2152
|
case "group": return await controller.execute("tabs.group", { tabId: requireTabId(args, "group") }, exec.signal);
|
|
2119
2153
|
case "ungroup": {
|
|
2120
2154
|
const params = {};
|
|
@@ -2125,6 +2159,132 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2125
2159
|
}
|
|
2126
2160
|
}
|
|
2127
2161
|
}));
|
|
2162
|
+
ctx.tools.register(defineTool({
|
|
2163
|
+
name: "browser_windows",
|
|
2164
|
+
description: "Inspect and control browser windows: list windows with geometry and state, open a new window (optionally positioned/sized), update state (minimize / maximize / fullscreen / focus) or geometry, close a whole window (all its tabs go too), or move tabs between windows. Geometry and state changes happen only through this tool. open is the only non-idempotent action: if it times out, run action:\"list\" first — the window may already exist.",
|
|
2165
|
+
parameters: {
|
|
2166
|
+
action: {
|
|
2167
|
+
type: "string",
|
|
2168
|
+
required: true,
|
|
2169
|
+
description: "One of: list, open, update, close, moveTab."
|
|
2170
|
+
},
|
|
2171
|
+
windowId: {
|
|
2172
|
+
type: "number",
|
|
2173
|
+
description: "Target window for update/close/moveTab."
|
|
2174
|
+
},
|
|
2175
|
+
url: {
|
|
2176
|
+
type: "string",
|
|
2177
|
+
description: "For open: a URL or an array of URLs (one tab each); defaults to about:blank."
|
|
2178
|
+
},
|
|
2179
|
+
state: {
|
|
2180
|
+
type: "string",
|
|
2181
|
+
description: "For open/update: normal | minimized | maximized | fullscreen."
|
|
2182
|
+
},
|
|
2183
|
+
left: {
|
|
2184
|
+
type: "number",
|
|
2185
|
+
description: "For open/update: window left edge in screen pixels."
|
|
2186
|
+
},
|
|
2187
|
+
top: {
|
|
2188
|
+
type: "number",
|
|
2189
|
+
description: "For open/update: window top edge in screen pixels."
|
|
2190
|
+
},
|
|
2191
|
+
width: {
|
|
2192
|
+
type: "number",
|
|
2193
|
+
description: "For open/update: window width in screen pixels."
|
|
2194
|
+
},
|
|
2195
|
+
height: {
|
|
2196
|
+
type: "number",
|
|
2197
|
+
description: "For open/update: window height in screen pixels."
|
|
2198
|
+
},
|
|
2199
|
+
focused: {
|
|
2200
|
+
type: "boolean",
|
|
2201
|
+
description: "For open/update: focus the window. focused:true on a minimized window restores it WITHOUT un-maximizing (state:\"normal\" would un-maximize)."
|
|
2202
|
+
},
|
|
2203
|
+
alwaysOnTop: {
|
|
2204
|
+
type: "boolean",
|
|
2205
|
+
description: "For update: keep the window above others."
|
|
2206
|
+
},
|
|
2207
|
+
drawAttention: {
|
|
2208
|
+
type: "boolean",
|
|
2209
|
+
description: "For update: flash the taskbar entry to get the user's attention without stealing focus."
|
|
2210
|
+
},
|
|
2211
|
+
incognito: {
|
|
2212
|
+
type: "boolean",
|
|
2213
|
+
description: "For open: open in incognito. Fails honestly when the extension is not allowed in incognito."
|
|
2214
|
+
},
|
|
2215
|
+
tabId: {
|
|
2216
|
+
type: "number",
|
|
2217
|
+
description: "For moveTab: one tab to move."
|
|
2218
|
+
},
|
|
2219
|
+
tabIds: {
|
|
2220
|
+
type: "array",
|
|
2221
|
+
items: { type: "number" },
|
|
2222
|
+
description: "For moveTab: several tabs to move in order; each is reported individually."
|
|
2223
|
+
},
|
|
2224
|
+
index: {
|
|
2225
|
+
type: "number",
|
|
2226
|
+
description: "For moveTab: target position in the destination window; omit for the end."
|
|
2227
|
+
}
|
|
2228
|
+
},
|
|
2229
|
+
output: {
|
|
2230
|
+
schema: {
|
|
2231
|
+
type: "object",
|
|
2232
|
+
additionalProperties: true
|
|
2233
|
+
},
|
|
2234
|
+
render: (_args, value) => [{
|
|
2235
|
+
type: "text",
|
|
2236
|
+
text: clampRendered(JSON.stringify(value))
|
|
2237
|
+
}]
|
|
2238
|
+
},
|
|
2239
|
+
presentCall: (args) => ({
|
|
2240
|
+
card: "generic",
|
|
2241
|
+
title: `Browser windows: ${args.action}`,
|
|
2242
|
+
kind: "other"
|
|
2243
|
+
}),
|
|
2244
|
+
async execute(args, exec) {
|
|
2245
|
+
switch (args.action) {
|
|
2246
|
+
case "list": return await controller.execute("windows.list", {}, exec.signal);
|
|
2247
|
+
case "open": {
|
|
2248
|
+
const params = {};
|
|
2249
|
+
if (args.url !== void 0) params.url = args.url;
|
|
2250
|
+
if (args.state !== void 0) params.state = args.state;
|
|
2251
|
+
if (args.incognito === true) params.incognito = true;
|
|
2252
|
+
for (const k of [
|
|
2253
|
+
"left",
|
|
2254
|
+
"top",
|
|
2255
|
+
"width",
|
|
2256
|
+
"height"
|
|
2257
|
+
]) if (typeof args[k] === "number") params[k] = args[k];
|
|
2258
|
+
if (args.focused !== void 0) params.focused = args.focused;
|
|
2259
|
+
return await controller.execute("windows.open", params, exec.signal);
|
|
2260
|
+
}
|
|
2261
|
+
case "update": {
|
|
2262
|
+
const params = { windowId: requireWindowId(args, "update") };
|
|
2263
|
+
if (args.state !== void 0) params.state = args.state;
|
|
2264
|
+
for (const k of [
|
|
2265
|
+
"left",
|
|
2266
|
+
"top",
|
|
2267
|
+
"width",
|
|
2268
|
+
"height"
|
|
2269
|
+
]) if (typeof args[k] === "number") params[k] = args[k];
|
|
2270
|
+
if (args.focused !== void 0) params.focused = args.focused;
|
|
2271
|
+
if (args.alwaysOnTop !== void 0) params.alwaysOnTop = args.alwaysOnTop;
|
|
2272
|
+
if (args.drawAttention === true) params.drawAttention = true;
|
|
2273
|
+
return await controller.execute("windows.update", params, exec.signal);
|
|
2274
|
+
}
|
|
2275
|
+
case "close": return await controller.execute("windows.close", { windowId: requireWindowId(args, "close") }, exec.signal);
|
|
2276
|
+
case "moveTab": {
|
|
2277
|
+
if (args.tabId === void 0 && args.tabIds === void 0) throw new Error("moveTab requires tabId or tabIds");
|
|
2278
|
+
const params = { windowId: requireWindowId(args, "moveTab") };
|
|
2279
|
+
if (args.tabId !== void 0) params.tabId = args.tabId;
|
|
2280
|
+
if (args.tabIds !== void 0) params.tabIds = args.tabIds;
|
|
2281
|
+
if (args.index !== void 0) params.index = args.index;
|
|
2282
|
+
return await controller.execute("windows.moveTab", params, exec.signal);
|
|
2283
|
+
}
|
|
2284
|
+
default: throw new Error(`unknown windows action: ${String(args.action)} (use list|open|update|close|moveTab)`);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}));
|
|
2128
2288
|
/** Page-side ceiling of the eval race in the extension; the bridge budget adds slack on top. */
|
|
2129
2289
|
const EVAL_MAX_MS = 12e4;
|
|
2130
2290
|
ctx.tools.register(defineTool({
|
|
@@ -2194,7 +2354,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2194
2354
|
}));
|
|
2195
2355
|
ctx.tools.register(defineTool({
|
|
2196
2356
|
name: "browser_screenshot",
|
|
2197
|
-
description: "Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path.",
|
|
2357
|
+
description: "Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path plus the image's real pixel size. fullPage captures the whole scrollable page in one shot (sticky/fixed headers may repeat); selector captures one element.",
|
|
2198
2358
|
parameters: {
|
|
2199
2359
|
tabId: {
|
|
2200
2360
|
type: "number",
|
|
@@ -2202,7 +2362,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2202
2362
|
},
|
|
2203
2363
|
fullPage: {
|
|
2204
2364
|
type: "boolean",
|
|
2205
|
-
description: "Capture
|
|
2365
|
+
description: "Capture the entire scrollable page instead of just the viewport. The receipt's width/height show the real size."
|
|
2366
|
+
},
|
|
2367
|
+
selector: {
|
|
2368
|
+
type: "string",
|
|
2369
|
+
description: "CSS selector: capture just that element (scrolled into view); the receipt carries its rect."
|
|
2206
2370
|
},
|
|
2207
2371
|
format: {
|
|
2208
2372
|
type: "string",
|
|
@@ -2227,12 +2391,36 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2227
2391
|
required: true
|
|
2228
2392
|
},
|
|
2229
2393
|
title: { type: "string" },
|
|
2230
|
-
url: { type: "string" }
|
|
2394
|
+
url: { type: "string" },
|
|
2395
|
+
width: { type: "number" },
|
|
2396
|
+
height: { type: "number" },
|
|
2397
|
+
elementRect: {
|
|
2398
|
+
type: "object",
|
|
2399
|
+
additionalProperties: false,
|
|
2400
|
+
properties: {
|
|
2401
|
+
x: {
|
|
2402
|
+
type: "number",
|
|
2403
|
+
required: true
|
|
2404
|
+
},
|
|
2405
|
+
y: {
|
|
2406
|
+
type: "number",
|
|
2407
|
+
required: true
|
|
2408
|
+
},
|
|
2409
|
+
w: {
|
|
2410
|
+
type: "number",
|
|
2411
|
+
required: true
|
|
2412
|
+
},
|
|
2413
|
+
h: {
|
|
2414
|
+
type: "number",
|
|
2415
|
+
required: true
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2231
2419
|
}
|
|
2232
2420
|
},
|
|
2233
2421
|
render: (_args, value) => [{
|
|
2234
2422
|
type: "text",
|
|
2235
|
-
text: `Saved ${value.file} (${value.bytes} bytes)`
|
|
2423
|
+
text: `Saved ${value.file} (${value.bytes} bytes${value.width !== void 0 && value.height !== void 0 ? `, ${value.width}×${value.height}` : ""}${value.elementRect !== void 0 ? `, element at ${value.elementRect.x},${value.elementRect.y} size ${value.elementRect.w}×${value.elementRect.h}` : ""})`
|
|
2236
2424
|
}]
|
|
2237
2425
|
},
|
|
2238
2426
|
presentCall: () => ({
|
|
@@ -2244,6 +2432,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2244
2432
|
const params = { format: args.format === "jpeg" ? "jpeg" : "png" };
|
|
2245
2433
|
if (args.tabId !== void 0) params.tabId = args.tabId;
|
|
2246
2434
|
if (args.fullPage !== void 0) params.fullPage = args.fullPage;
|
|
2435
|
+
if (args.selector !== void 0) params.selector = args.selector;
|
|
2247
2436
|
return saveScreenshot(controller, await controller.execute("screenshot", params, exec.signal));
|
|
2248
2437
|
}
|
|
2249
2438
|
}));
|
package/lib/types/errors.js
CHANGED
|
@@ -41,6 +41,9 @@ export const HOLD_ABORTED_MARKER = 'browser-hold-aborted:';
|
|
|
41
41
|
const WRITE_COMMANDS = new Set([
|
|
42
42
|
'nav', 'click', 'input', 'press', 'scroll', 'eval', 'emulate',
|
|
43
43
|
'tabs.open', 'tabs.close', 'tabs.closeMany', 'tabs.activate', 'dialog',
|
|
44
|
+
// 历史导航与 nav 同族:超时/掉线后重试可能**多跳一条**历史(第一次 back 其实
|
|
45
|
+
// 已经落了地),报 unknown 才诚实。
|
|
46
|
+
'tabs.back', 'tabs.forward',
|
|
44
47
|
// 这条不是扩展命令,而是插件本地命令 `browser_cleanup`:它逐条 rm,中途失败
|
|
45
48
|
// (Windows 上被占用的截图会 EPERM)时前面的文件已经没了,报 not_started
|
|
46
49
|
// 就是告诉模型「什么都没发生,重试安全」,而那正好是假的。
|
|
@@ -48,6 +51,10 @@ const WRITE_COMMANDS = new Set([
|
|
|
48
51
|
// 往页面里插一个宿主元素 = 改页面 DOM。超时/掉线之后重试,可能多留一层浮层
|
|
49
52
|
//(旧的那层还挂着),所以报 unknown 而不是「没动过页面」。
|
|
50
53
|
'hold',
|
|
54
|
+
// 开窗不是幂等的:超时/掉线后重试可能真的开出第二个窗口——必须报 unknown。
|
|
55
|
+
// update/close/moveTab 是幂等的(重设同一几何、关已关的窗、挪到同一位置),
|
|
56
|
+
// 按判据算读,登记在 verify_errors 的 classifiedRead 里。
|
|
57
|
+
'windows.open',
|
|
51
58
|
]);
|
|
52
59
|
/** 该命令是否可能留下副作用。 */
|
|
53
60
|
export function isWriteCommand(command) {
|
package/lib/types/index.js
CHANGED
|
@@ -289,6 +289,11 @@ function requireTabId(args, action) {
|
|
|
289
289
|
throw new Error(`${action} requires tabId`);
|
|
290
290
|
return args.tabId;
|
|
291
291
|
}
|
|
292
|
+
function requireWindowId(args, action) {
|
|
293
|
+
if (typeof args.windowId !== 'number')
|
|
294
|
+
throw new Error(`${action} requires windowId`);
|
|
295
|
+
return args.windowId;
|
|
296
|
+
}
|
|
292
297
|
/**
|
|
293
298
|
* Every tab the caller asked to close. `tabId` and `tabIds` are both accepted
|
|
294
299
|
* (a batch call that repeats one id, or passes both, must not close it twice),
|
|
@@ -304,6 +309,15 @@ function closeTabIds(args) {
|
|
|
304
309
|
throw new Error('close requires tabId or tabIds');
|
|
305
310
|
return ids;
|
|
306
311
|
}
|
|
312
|
+
/** Actual pixel dimensions straight from the PNG's IHDR chunk (width/height are
|
|
313
|
+
* big-endian at offsets 16/20 after the 8-byte signature + 4-byte length + 4-byte
|
|
314
|
+
* type). The receipt should say how big the picture IS — fullPage captures are
|
|
315
|
+
* often 3-5 viewports tall and the model cannot tell from the byte count. */
|
|
316
|
+
function pngDimensions(buffer) {
|
|
317
|
+
if (buffer.length < 24 || buffer.toString('latin1', 12, 16) !== 'IHDR')
|
|
318
|
+
return {};
|
|
319
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
320
|
+
}
|
|
307
321
|
/** Write one screenshot payload to the shots directory and return its durable location. */
|
|
308
322
|
async function saveScreenshot(controller, payload) {
|
|
309
323
|
const dir = controller.shotsDir;
|
|
@@ -315,12 +329,15 @@ async function saveScreenshot(controller, payload) {
|
|
|
315
329
|
const file = path.join(dir, `${stamp}-${random}.${payload.format === 'jpeg' ? 'jpg' : 'png'}`);
|
|
316
330
|
const buffer = Buffer.from(payload.base64, 'base64');
|
|
317
331
|
await writeFile(file, buffer);
|
|
332
|
+
const dims = payload.format === 'png' ? pngDimensions(buffer) : {};
|
|
318
333
|
return {
|
|
319
334
|
file,
|
|
320
335
|
bytes: buffer.length,
|
|
321
336
|
tabId: payload.tabId,
|
|
322
337
|
title: payload.tabTitle,
|
|
323
338
|
url: payload.tabUrl,
|
|
339
|
+
...dims,
|
|
340
|
+
...(payload.elementRect === undefined ? {} : { elementRect: payload.elementRect }),
|
|
324
341
|
};
|
|
325
342
|
}
|
|
326
343
|
/** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
|
|
@@ -876,16 +893,16 @@ function applyBrowserTools(ctx, controller) {
|
|
|
876
893
|
}));
|
|
877
894
|
ctx.tools.register(defineTool({
|
|
878
895
|
name: 'browser_tabs',
|
|
879
|
-
description: 'List tabs, or open/close/activate/group/ungroup. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.',
|
|
896
|
+
description: 'List tabs, or open/close/activate/group/ungroup, or go back/forward in the tab\'s history. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.',
|
|
880
897
|
parameters: {
|
|
881
|
-
action: { type: 'string', required: true, description: 'One of: list, open, close, activate, group, ungroup.' },
|
|
898
|
+
action: { type: 'string', required: true, description: 'One of: list, open, close, activate, group, ungroup, back, forward.' },
|
|
882
899
|
url: { type: 'string', description: 'URL for the open action.' },
|
|
883
|
-
tabId: { type: 'number', description: 'Target tab for close/activate/group.' },
|
|
900
|
+
tabId: { type: 'number', description: 'Target tab for close/activate/group/back/forward; defaults to the active tab for back/forward.' },
|
|
884
901
|
tabIds: { type: 'array', items: { type: 'number' }, description: 'For close: every tab to close in one call. The result reports each id (closed / alreadyGone / failed) instead of stopping at the first failure.' },
|
|
885
902
|
active: { type: 'boolean', description: 'Whether a newly opened tab becomes active; defaults to true.' },
|
|
886
903
|
group: { type: 'boolean', description: 'For open: put the new tab in the DSH tab group. Defaults to true; pass false to keep it loose.' },
|
|
887
|
-
waitUntil: { type: 'string', description: 'For open: when the
|
|
888
|
-
timeoutMs: { type: 'number', description: 'For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000).' },
|
|
904
|
+
waitUntil: { type: 'string', description: 'For open/back/forward: when the navigation counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none). back/forward report unchanged:true + a reason when the tab is already at the oldest/newest history entry.' },
|
|
905
|
+
timeoutMs: { type: 'number', description: 'For open/back/forward: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000).' },
|
|
889
906
|
windowId: { type: 'number', description: 'For ungroup: only dissolve that window’s DSH group; omit to clear every window.' },
|
|
890
907
|
},
|
|
891
908
|
output: {
|
|
@@ -929,6 +946,28 @@ function applyBrowserTools(ctx, controller) {
|
|
|
929
946
|
}
|
|
930
947
|
case 'activate':
|
|
931
948
|
return await controller.execute('tabs.activate', { tabId: requireTabId(args, 'activate') }, exec.signal);
|
|
949
|
+
case 'back':
|
|
950
|
+
case 'forward': {
|
|
951
|
+
// Same wait-shape as open: tier default load, explicit budget, bridge
|
|
952
|
+
// margin so the reply can outlive the tier without the bridge timing out.
|
|
953
|
+
// 两条各写一个字面量调用点(surface 台对账 execute 字面量,拼名字会瞎)。
|
|
954
|
+
const waitUntil = args.waitUntil === undefined ? 'load' : String(args.waitUntil);
|
|
955
|
+
if (!(waitUntil in NAV_TIERS)) {
|
|
956
|
+
throw new Error(`browser_tabs ${args.action}: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
|
|
957
|
+
}
|
|
958
|
+
const params = { waitUntil };
|
|
959
|
+
if (args.tabId !== undefined)
|
|
960
|
+
params.tabId = args.tabId;
|
|
961
|
+
let budgetMs = NAV_TIERS[waitUntil];
|
|
962
|
+
if (typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)) {
|
|
963
|
+
budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
|
|
964
|
+
}
|
|
965
|
+
params.timeoutMs = budgetMs;
|
|
966
|
+
if (args.action === 'back') {
|
|
967
|
+
return await controller.execute('tabs.back', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
|
|
968
|
+
}
|
|
969
|
+
return await controller.execute('tabs.forward', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
|
|
970
|
+
}
|
|
932
971
|
case 'group':
|
|
933
972
|
return await controller.execute('tabs.group', { tabId: requireTabId(args, 'group') }, exec.signal);
|
|
934
973
|
case 'ungroup': {
|
|
@@ -942,6 +981,89 @@ function applyBrowserTools(ctx, controller) {
|
|
|
942
981
|
}
|
|
943
982
|
},
|
|
944
983
|
}));
|
|
984
|
+
// Window control is an explicit, opt-in surface: no other tool ever touches
|
|
985
|
+
// window geometry or state (the input path only focuses), so everything the
|
|
986
|
+
// model does to the user's windows is visible as a browser_windows call.
|
|
987
|
+
ctx.tools.register(defineTool({
|
|
988
|
+
name: 'browser_windows',
|
|
989
|
+
description: 'Inspect and control browser windows: list windows with geometry and state, open a new window (optionally positioned/sized), update state (minimize / maximize / fullscreen / focus) or geometry, close a whole window (all its tabs go too), or move tabs between windows. Geometry and state changes happen only through this tool. open is the only non-idempotent action: if it times out, run action:"list" first — the window may already exist.',
|
|
990
|
+
parameters: {
|
|
991
|
+
action: { type: 'string', required: true, description: 'One of: list, open, update, close, moveTab.' },
|
|
992
|
+
windowId: { type: 'number', description: 'Target window for update/close/moveTab.' },
|
|
993
|
+
url: { type: 'string', description: 'For open: a URL or an array of URLs (one tab each); defaults to about:blank.' },
|
|
994
|
+
state: { type: 'string', description: 'For open/update: normal | minimized | maximized | fullscreen.' },
|
|
995
|
+
left: { type: 'number', description: 'For open/update: window left edge in screen pixels.' },
|
|
996
|
+
top: { type: 'number', description: 'For open/update: window top edge in screen pixels.' },
|
|
997
|
+
width: { type: 'number', description: 'For open/update: window width in screen pixels.' },
|
|
998
|
+
height: { type: 'number', description: 'For open/update: window height in screen pixels.' },
|
|
999
|
+
focused: { type: 'boolean', description: 'For open/update: focus the window. focused:true on a minimized window restores it WITHOUT un-maximizing (state:"normal" would un-maximize).' },
|
|
1000
|
+
alwaysOnTop: { type: 'boolean', description: 'For update: keep the window above others.' },
|
|
1001
|
+
drawAttention: { type: 'boolean', description: 'For update: flash the taskbar entry to get the user\'s attention without stealing focus.' },
|
|
1002
|
+
incognito: { type: 'boolean', description: 'For open: open in incognito. Fails honestly when the extension is not allowed in incognito.' },
|
|
1003
|
+
tabId: { type: 'number', description: 'For moveTab: one tab to move.' },
|
|
1004
|
+
tabIds: { type: 'array', items: { type: 'number' }, description: 'For moveTab: several tabs to move in order; each is reported individually.' },
|
|
1005
|
+
index: { type: 'number', description: 'For moveTab: target position in the destination window; omit for the end.' },
|
|
1006
|
+
},
|
|
1007
|
+
output: {
|
|
1008
|
+
schema: { type: 'object', additionalProperties: true },
|
|
1009
|
+
render: (_args, value) => [{ type: 'text', text: clampRendered(JSON.stringify(value)) }],
|
|
1010
|
+
},
|
|
1011
|
+
presentCall: args => ({ card: 'generic', title: `Browser windows: ${args.action}`, kind: 'other' }),
|
|
1012
|
+
async execute(args, exec) {
|
|
1013
|
+
switch (args.action) {
|
|
1014
|
+
case 'list':
|
|
1015
|
+
return await controller.execute('windows.list', {}, exec.signal);
|
|
1016
|
+
case 'open': {
|
|
1017
|
+
const params = {};
|
|
1018
|
+
if (args.url !== undefined)
|
|
1019
|
+
params.url = args.url;
|
|
1020
|
+
if (args.state !== undefined)
|
|
1021
|
+
params.state = args.state;
|
|
1022
|
+
if (args.incognito === true)
|
|
1023
|
+
params.incognito = true;
|
|
1024
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
1025
|
+
if (typeof args[k] === 'number')
|
|
1026
|
+
params[k] = args[k];
|
|
1027
|
+
}
|
|
1028
|
+
if (args.focused !== undefined)
|
|
1029
|
+
params.focused = args.focused;
|
|
1030
|
+
return await controller.execute('windows.open', params, exec.signal);
|
|
1031
|
+
}
|
|
1032
|
+
case 'update': {
|
|
1033
|
+
const params = { windowId: requireWindowId(args, 'update') };
|
|
1034
|
+
if (args.state !== undefined)
|
|
1035
|
+
params.state = args.state;
|
|
1036
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
1037
|
+
if (typeof args[k] === 'number')
|
|
1038
|
+
params[k] = args[k];
|
|
1039
|
+
}
|
|
1040
|
+
if (args.focused !== undefined)
|
|
1041
|
+
params.focused = args.focused;
|
|
1042
|
+
if (args.alwaysOnTop !== undefined)
|
|
1043
|
+
params.alwaysOnTop = args.alwaysOnTop;
|
|
1044
|
+
if (args.drawAttention === true)
|
|
1045
|
+
params.drawAttention = true;
|
|
1046
|
+
return await controller.execute('windows.update', params, exec.signal);
|
|
1047
|
+
}
|
|
1048
|
+
case 'close':
|
|
1049
|
+
return await controller.execute('windows.close', { windowId: requireWindowId(args, 'close') }, exec.signal);
|
|
1050
|
+
case 'moveTab': {
|
|
1051
|
+
if (args.tabId === undefined && args.tabIds === undefined)
|
|
1052
|
+
throw new Error('moveTab requires tabId or tabIds');
|
|
1053
|
+
const params = { windowId: requireWindowId(args, 'moveTab') };
|
|
1054
|
+
if (args.tabId !== undefined)
|
|
1055
|
+
params.tabId = args.tabId;
|
|
1056
|
+
if (args.tabIds !== undefined)
|
|
1057
|
+
params.tabIds = args.tabIds;
|
|
1058
|
+
if (args.index !== undefined)
|
|
1059
|
+
params.index = args.index;
|
|
1060
|
+
return await controller.execute('windows.moveTab', params, exec.signal);
|
|
1061
|
+
}
|
|
1062
|
+
default:
|
|
1063
|
+
throw new Error(`unknown windows action: ${String(args.action)} (use list|open|update|close|moveTab)`);
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
}));
|
|
945
1067
|
/** Page-side ceiling of the eval race in the extension; the bridge budget adds slack on top. */
|
|
946
1068
|
const EVAL_MAX_MS = 120_000;
|
|
947
1069
|
ctx.tools.register(defineTool({
|
|
@@ -990,10 +1112,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
990
1112
|
}));
|
|
991
1113
|
ctx.tools.register(defineTool({
|
|
992
1114
|
name: 'browser_screenshot',
|
|
993
|
-
description: 'Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path.',
|
|
1115
|
+
description: 'Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path plus the image\'s real pixel size. fullPage captures the whole scrollable page in one shot (sticky/fixed headers may repeat); selector captures one element.',
|
|
994
1116
|
parameters: {
|
|
995
1117
|
tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
|
|
996
|
-
fullPage: { type: 'boolean', description: 'Capture
|
|
1118
|
+
fullPage: { type: 'boolean', description: 'Capture the entire scrollable page instead of just the viewport. The receipt\'s width/height show the real size.' },
|
|
1119
|
+
selector: { type: 'string', description: 'CSS selector: capture just that element (scrolled into view); the receipt carries its rect.' },
|
|
997
1120
|
format: { type: 'string', description: '"png" (default) or "jpeg".' },
|
|
998
1121
|
},
|
|
999
1122
|
output: {
|
|
@@ -1006,9 +1129,24 @@ function applyBrowserTools(ctx, controller) {
|
|
|
1006
1129
|
tabId: { type: 'number', required: true },
|
|
1007
1130
|
title: { type: 'string' },
|
|
1008
1131
|
url: { type: 'string' },
|
|
1132
|
+
width: { type: 'number' },
|
|
1133
|
+
height: { type: 'number' },
|
|
1134
|
+
elementRect: {
|
|
1135
|
+
type: 'object',
|
|
1136
|
+
additionalProperties: false,
|
|
1137
|
+
properties: {
|
|
1138
|
+
x: { type: 'number', required: true },
|
|
1139
|
+
y: { type: 'number', required: true },
|
|
1140
|
+
w: { type: 'number', required: true },
|
|
1141
|
+
h: { type: 'number', required: true },
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1009
1144
|
},
|
|
1010
1145
|
},
|
|
1011
|
-
render: (_args, value) => [{
|
|
1146
|
+
render: (_args, value) => [{
|
|
1147
|
+
type: 'text',
|
|
1148
|
+
text: `Saved ${value.file} (${value.bytes} bytes${value.width !== undefined && value.height !== undefined ? `, ${value.width}×${value.height}` : ''}${value.elementRect !== undefined ? `, element at ${value.elementRect.x},${value.elementRect.y} size ${value.elementRect.w}×${value.elementRect.h}` : ''})`,
|
|
1149
|
+
}],
|
|
1012
1150
|
},
|
|
1013
1151
|
presentCall: () => ({ card: 'generic', title: 'Browser screenshot', kind: 'other' }),
|
|
1014
1152
|
async execute(args, exec) {
|
|
@@ -1018,6 +1156,8 @@ function applyBrowserTools(ctx, controller) {
|
|
|
1018
1156
|
params.tabId = args.tabId;
|
|
1019
1157
|
if (args.fullPage !== undefined)
|
|
1020
1158
|
params.fullPage = args.fullPage;
|
|
1159
|
+
if (args.selector !== undefined)
|
|
1160
|
+
params.selector = args.selector;
|
|
1021
1161
|
const payload = await controller.execute('screenshot', params, exec.signal);
|
|
1022
1162
|
return saveScreenshot(controller, payload);
|
|
1023
1163
|
},
|
package/lib/types/policy.js
CHANGED
|
@@ -24,6 +24,8 @@ export const READ_ONLY_ALLOWED = new Set([
|
|
|
24
24
|
'content', 'find', 'snapshot', 'screenshot', 'pdf', 'wait',
|
|
25
25
|
// 读扩展采集到的缓冲
|
|
26
26
|
'tabs.list', 'console.log', 'network.log',
|
|
27
|
+
// 读窗口清单(几何/状态/tabIds);open/update/close/moveTab 改窗口,不在此列
|
|
28
|
+
'windows.list',
|
|
27
29
|
// 读浏览器自己的状态(cookie 是凭据,但 get 不改任何东西;set/remove 不在此列)
|
|
28
30
|
'cookies.get',
|
|
29
31
|
]);
|
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` - 24 个 `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.
|
|
4
|
+
"version": "1.7.0",
|
|
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"
|
|
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"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public",
|