@angelky7/dsh-browser-control 1.6.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 +5 -5
- package/README.md +5 -5
- package/lib/index.js +67 -10
- package/lib/types/errors.js +3 -0
- package/lib/types/index.js +60 -8
- 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,9 +256,9 @@ 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+) |
|
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,9 +250,9 @@ 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+) |
|
package/lib/index.js
CHANGED
|
@@ -117,6 +117,8 @@ 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
123
|
"hold",
|
|
122
124
|
"windows.open"
|
|
@@ -1200,6 +1202,17 @@ function closeTabIds(args) {
|
|
|
1200
1202
|
if (ids.length === 0) throw new Error("close requires tabId or tabIds");
|
|
1201
1203
|
return ids;
|
|
1202
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
|
+
}
|
|
1203
1216
|
/** Write one screenshot payload to the shots directory and return its durable location. */
|
|
1204
1217
|
async function saveScreenshot(controller, payload) {
|
|
1205
1218
|
const dir = controller.shotsDir;
|
|
@@ -1210,12 +1223,15 @@ async function saveScreenshot(controller, payload) {
|
|
|
1210
1223
|
const file = path.join(dir, `${stamp}-${random}.${payload.format === "jpeg" ? "jpg" : "png"}`);
|
|
1211
1224
|
const buffer = Buffer.from(payload.base64, "base64");
|
|
1212
1225
|
await writeFile(file, buffer);
|
|
1226
|
+
const dims = payload.format === "png" ? pngDimensions(buffer) : {};
|
|
1213
1227
|
return {
|
|
1214
1228
|
file,
|
|
1215
1229
|
bytes: buffer.length,
|
|
1216
1230
|
tabId: payload.tabId,
|
|
1217
1231
|
title: payload.tabTitle,
|
|
1218
|
-
url: payload.tabUrl
|
|
1232
|
+
url: payload.tabUrl,
|
|
1233
|
+
...dims,
|
|
1234
|
+
...payload.elementRect === void 0 ? {} : { elementRect: payload.elementRect }
|
|
1219
1235
|
};
|
|
1220
1236
|
}
|
|
1221
1237
|
/** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
|
|
@@ -2041,12 +2057,12 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2041
2057
|
}));
|
|
2042
2058
|
ctx.tools.register(defineTool({
|
|
2043
2059
|
name: "browser_tabs",
|
|
2044
|
-
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.",
|
|
2045
2061
|
parameters: {
|
|
2046
2062
|
action: {
|
|
2047
2063
|
type: "string",
|
|
2048
2064
|
required: true,
|
|
2049
|
-
description: "One of: list, open, close, activate, group, ungroup."
|
|
2065
|
+
description: "One of: list, open, close, activate, group, ungroup, back, forward."
|
|
2050
2066
|
},
|
|
2051
2067
|
url: {
|
|
2052
2068
|
type: "string",
|
|
@@ -2054,7 +2070,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2054
2070
|
},
|
|
2055
2071
|
tabId: {
|
|
2056
2072
|
type: "number",
|
|
2057
|
-
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."
|
|
2058
2074
|
},
|
|
2059
2075
|
tabIds: {
|
|
2060
2076
|
type: "array",
|
|
@@ -2071,11 +2087,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2071
2087
|
},
|
|
2072
2088
|
waitUntil: {
|
|
2073
2089
|
type: "string",
|
|
2074
|
-
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."
|
|
2075
2091
|
},
|
|
2076
2092
|
timeoutMs: {
|
|
2077
2093
|
type: "number",
|
|
2078
|
-
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)."
|
|
2079
2095
|
},
|
|
2080
2096
|
windowId: {
|
|
2081
2097
|
type: "number",
|
|
@@ -2121,6 +2137,18 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2121
2137
|
return ids.length === 1 ? await controller.execute("tabs.close", { tabId: ids[0] }, exec.signal) : await controller.execute("tabs.closeMany", { tabIds: ids }, exec.signal);
|
|
2122
2138
|
}
|
|
2123
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
|
+
}
|
|
2124
2152
|
case "group": return await controller.execute("tabs.group", { tabId: requireTabId(args, "group") }, exec.signal);
|
|
2125
2153
|
case "ungroup": {
|
|
2126
2154
|
const params = {};
|
|
@@ -2326,7 +2354,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2326
2354
|
}));
|
|
2327
2355
|
ctx.tools.register(defineTool({
|
|
2328
2356
|
name: "browser_screenshot",
|
|
2329
|
-
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.",
|
|
2330
2358
|
parameters: {
|
|
2331
2359
|
tabId: {
|
|
2332
2360
|
type: "number",
|
|
@@ -2334,7 +2362,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2334
2362
|
},
|
|
2335
2363
|
fullPage: {
|
|
2336
2364
|
type: "boolean",
|
|
2337
|
-
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."
|
|
2338
2370
|
},
|
|
2339
2371
|
format: {
|
|
2340
2372
|
type: "string",
|
|
@@ -2359,12 +2391,36 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2359
2391
|
required: true
|
|
2360
2392
|
},
|
|
2361
2393
|
title: { type: "string" },
|
|
2362
|
-
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
|
+
}
|
|
2363
2419
|
}
|
|
2364
2420
|
},
|
|
2365
2421
|
render: (_args, value) => [{
|
|
2366
2422
|
type: "text",
|
|
2367
|
-
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}` : ""})`
|
|
2368
2424
|
}]
|
|
2369
2425
|
},
|
|
2370
2426
|
presentCall: () => ({
|
|
@@ -2376,6 +2432,7 @@ function applyBrowserTools(ctx, controller) {
|
|
|
2376
2432
|
const params = { format: args.format === "jpeg" ? "jpeg" : "png" };
|
|
2377
2433
|
if (args.tabId !== void 0) params.tabId = args.tabId;
|
|
2378
2434
|
if (args.fullPage !== void 0) params.fullPage = args.fullPage;
|
|
2435
|
+
if (args.selector !== void 0) params.selector = args.selector;
|
|
2379
2436
|
return saveScreenshot(controller, await controller.execute("screenshot", params, exec.signal));
|
|
2380
2437
|
}
|
|
2381
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
|
// 就是告诉模型「什么都没发生,重试安全」,而那正好是假的。
|
package/lib/types/index.js
CHANGED
|
@@ -309,6 +309,15 @@ function closeTabIds(args) {
|
|
|
309
309
|
throw new Error('close requires tabId or tabIds');
|
|
310
310
|
return ids;
|
|
311
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
|
+
}
|
|
312
321
|
/** Write one screenshot payload to the shots directory and return its durable location. */
|
|
313
322
|
async function saveScreenshot(controller, payload) {
|
|
314
323
|
const dir = controller.shotsDir;
|
|
@@ -320,12 +329,15 @@ async function saveScreenshot(controller, payload) {
|
|
|
320
329
|
const file = path.join(dir, `${stamp}-${random}.${payload.format === 'jpeg' ? 'jpg' : 'png'}`);
|
|
321
330
|
const buffer = Buffer.from(payload.base64, 'base64');
|
|
322
331
|
await writeFile(file, buffer);
|
|
332
|
+
const dims = payload.format === 'png' ? pngDimensions(buffer) : {};
|
|
323
333
|
return {
|
|
324
334
|
file,
|
|
325
335
|
bytes: buffer.length,
|
|
326
336
|
tabId: payload.tabId,
|
|
327
337
|
title: payload.tabTitle,
|
|
328
338
|
url: payload.tabUrl,
|
|
339
|
+
...dims,
|
|
340
|
+
...(payload.elementRect === undefined ? {} : { elementRect: payload.elementRect }),
|
|
329
341
|
};
|
|
330
342
|
}
|
|
331
343
|
/** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
|
|
@@ -881,16 +893,16 @@ function applyBrowserTools(ctx, controller) {
|
|
|
881
893
|
}));
|
|
882
894
|
ctx.tools.register(defineTool({
|
|
883
895
|
name: 'browser_tabs',
|
|
884
|
-
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.',
|
|
885
897
|
parameters: {
|
|
886
|
-
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.' },
|
|
887
899
|
url: { type: 'string', description: 'URL for the open action.' },
|
|
888
|
-
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.' },
|
|
889
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.' },
|
|
890
902
|
active: { type: 'boolean', description: 'Whether a newly opened tab becomes active; defaults to true.' },
|
|
891
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.' },
|
|
892
|
-
waitUntil: { type: 'string', description: 'For open: when the
|
|
893
|
-
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).' },
|
|
894
906
|
windowId: { type: 'number', description: 'For ungroup: only dissolve that window’s DSH group; omit to clear every window.' },
|
|
895
907
|
},
|
|
896
908
|
output: {
|
|
@@ -934,6 +946,28 @@ function applyBrowserTools(ctx, controller) {
|
|
|
934
946
|
}
|
|
935
947
|
case 'activate':
|
|
936
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
|
+
}
|
|
937
971
|
case 'group':
|
|
938
972
|
return await controller.execute('tabs.group', { tabId: requireTabId(args, 'group') }, exec.signal);
|
|
939
973
|
case 'ungroup': {
|
|
@@ -1078,10 +1112,11 @@ function applyBrowserTools(ctx, controller) {
|
|
|
1078
1112
|
}));
|
|
1079
1113
|
ctx.tools.register(defineTool({
|
|
1080
1114
|
name: 'browser_screenshot',
|
|
1081
|
-
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.',
|
|
1082
1116
|
parameters: {
|
|
1083
1117
|
tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
|
|
1084
|
-
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.' },
|
|
1085
1120
|
format: { type: 'string', description: '"png" (default) or "jpeg".' },
|
|
1086
1121
|
},
|
|
1087
1122
|
output: {
|
|
@@ -1094,9 +1129,24 @@ function applyBrowserTools(ctx, controller) {
|
|
|
1094
1129
|
tabId: { type: 'number', required: true },
|
|
1095
1130
|
title: { type: 'string' },
|
|
1096
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
|
+
},
|
|
1097
1144
|
},
|
|
1098
1145
|
},
|
|
1099
|
-
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
|
+
}],
|
|
1100
1150
|
},
|
|
1101
1151
|
presentCall: () => ({ card: 'generic', title: 'Browser screenshot', kind: 'other' }),
|
|
1102
1152
|
async execute(args, exec) {
|
|
@@ -1106,6 +1156,8 @@ function applyBrowserTools(ctx, controller) {
|
|
|
1106
1156
|
params.tabId = args.tabId;
|
|
1107
1157
|
if (args.fullPage !== undefined)
|
|
1108
1158
|
params.fullPage = args.fullPage;
|
|
1159
|
+
if (args.selector !== undefined)
|
|
1160
|
+
params.selector = args.selector;
|
|
1109
1161
|
const payload = await controller.execute('screenshot', params, exec.signal);
|
|
1110
1162
|
return saveScreenshot(controller, payload);
|
|
1111
1163
|
},
|
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 && node verify/verify_windows.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",
|