@angelky7/dsh-browser-control 1.3.0 → 1.4.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 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.3.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.3.0/DSH-Browser-Control-v1.3.0.zip) | Chrome extension (unzip and load; extension code is unchanged since 1.2.0 — no reinstall needed) |
68
- | [dsh-browser-control-plugin-v1.3.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.3.0/dsh-browser-control-plugin-v1.3.0.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
67
+ | [DSH-Browser-Control-v1.4.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.4.0/DSH-Browser-Control-v1.4.0.zip) | Chrome extension (unzip and load; navigation waiting is now event-driven — reinstall required) |
68
+ | [dsh-browser-control-plugin-v1.4.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.4.0/dsh-browser-control-plugin-v1.4.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.3.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.4.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"
package/README.md CHANGED
@@ -64,8 +64,8 @@ Chrome 扩展(CDP 驱动)
64
64
 
65
65
  | 文件 | 说明 |
66
66
  |---|---|
67
- | [DSH-Browser-Control-v1.3.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.3.0/DSH-Browser-Control-v1.3.0.zip) | Chrome 扩展(解压后加载;与 1.2.0 相比扩展代码无改动,已装 1.2.0 不必重装) |
68
- | [dsh-browser-control-plugin-v1.3.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.3.0/dsh-browser-control-plugin-v1.3.0.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
67
+ | [DSH-Browser-Control-v1.4.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.4.0/DSH-Browser-Control-v1.4.0.zip) | Chrome 扩展(解压后加载;这一版扩展的导航等待换成事件驱动,**必须重装**) |
68
+ | [dsh-browser-control-plugin-v1.4.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.4.0/dsh-browser-control-plugin-v1.4.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.3.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.4.0"
108
108
 
109
109
  # 本地目录调试(注意:必须显式 file: 前缀)
110
110
  dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
package/lib/index.js CHANGED
@@ -942,6 +942,18 @@ function policyRefusal(command, flags, params) {
942
942
  /** Navigation wait ceiling (same as browser_wait); the bridge gets this plus BRIDGE_MARGIN_MS. */
943
943
  const NAV_MAX_MS = 12e4;
944
944
  /**
945
+ * Same tier table as the extension's NAV_TIERS (extension/background.js). Both
946
+ * literals must stay in lockstep — verify_nav_waituntil.mjs extracts the
947
+ * extension's by regex and asserts the two agree (cross-side literals are wire
948
+ * protocol: a value only one side knows is a lie on the other).
949
+ */
950
+ const NAV_TIERS = {
951
+ none: 0,
952
+ commit: 4e3,
953
+ domcontentloaded: 4e3,
954
+ load: 15e3
955
+ };
956
+ /**
945
957
  * Extra time handed to the bridge on top of the navigation budget. Without it a
946
958
  * caller-requested budget longer than the bridge default (60s) would be beaten by
947
959
  * the bridge, and the model would get `command_timeout` (effect unknown) instead
@@ -1246,9 +1258,13 @@ function applyBrowserTools(ctx, controller) {
1246
1258
  type: "number",
1247
1259
  description: "Target tab; defaults to the active tab."
1248
1260
  },
1261
+ waitUntil: {
1262
+ type: "string",
1263
+ description: "When the navigation counts as arrived: \"load\" (default) waits for the page load event (15s budget), \"domcontentloaded\" returns once the DOM is parsed (async components may still be mounting — follow with browser_wait if an element is missing), \"commit\" once the new document's address lands, \"none\" returns immediately with no arrival check at all."
1264
+ },
1249
1265
  timeoutMs: {
1250
1266
  type: "number",
1251
- description: `How long to wait for the page load to settle: defaults to 15000, capped at ${NAV_MAX_MS}. Raise it for slow pages instead of retrying; drop it to 3000-5000 when you do not need the page to finish loading and will read/wait afterwards.`
1267
+ description: `How long to wait for the page load to settle: defaults to the waitUntil tier (load 15000, commit/domcontentloaded 4000), capped at ${NAV_MAX_MS}. An explicit value always wins over the tier.`
1252
1268
  }
1253
1269
  },
1254
1270
  output: {
@@ -1264,7 +1280,10 @@ function applyBrowserTools(ctx, controller) {
1264
1280
  title: { type: "string" },
1265
1281
  pendingUrl: { type: "string" },
1266
1282
  loadPending: { type: "boolean" },
1267
- siteUnreachable: { type: "boolean" }
1283
+ siteUnreachable: { type: "boolean" },
1284
+ waitUntil: { type: "string" },
1285
+ waitedMs: { type: "number" },
1286
+ readyState: { type: "string" }
1268
1287
  }
1269
1288
  },
1270
1289
  render: (_args, value) => {
@@ -1298,17 +1317,26 @@ function applyBrowserTools(ctx, controller) {
1298
1317
  kind: "other"
1299
1318
  }),
1300
1319
  async execute(args, exec) {
1301
- const params = { url: args.url };
1320
+ const waitUntil = args.waitUntil === void 0 ? "load" : String(args.waitUntil);
1321
+ if (!(waitUntil in NAV_TIERS)) throw new Error(`browser_navigate: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
1322
+ const params = {
1323
+ url: args.url,
1324
+ waitUntil
1325
+ };
1302
1326
  if (args.tabId !== void 0) params.tabId = args.tabId;
1303
- const budgetMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs))) : 15e3;
1304
- params.timeoutMs = budgetMs;
1305
- const raw = await controller.execute("nav", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
1327
+ let budgetMs = NAV_TIERS[waitUntil];
1328
+ if (typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs)) budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
1329
+ if (waitUntil !== "none") params.timeoutMs = budgetMs;
1330
+ const raw = await controller.execute("nav", params, exec.signal, waitUntil === "none" ? void 0 : budgetMs + BRIDGE_MARGIN_MS);
1306
1331
  const out = { tabId: raw.tabId };
1307
1332
  if (typeof raw.url === "string") out.url = raw.url;
1308
1333
  if (typeof raw.title === "string") out.title = raw.title;
1309
1334
  if (typeof raw.pendingUrl === "string") out.pendingUrl = raw.pendingUrl;
1310
1335
  if (raw.loadPending === true) out.loadPending = true;
1311
1336
  if (raw.siteUnreachable === true || typeof raw.siteUnreachable === "object" && raw.siteUnreachable !== null) out.siteUnreachable = true;
1337
+ if (typeof raw.waitUntil === "string") out.waitUntil = raw.waitUntil;
1338
+ if (typeof raw.waitedMs === "number") out.waitedMs = raw.waitedMs;
1339
+ if (typeof raw.readyState === "string" && raw.readyState.length > 0) out.readyState = raw.readyState;
1312
1340
  return out;
1313
1341
  }
1314
1342
  }));
@@ -2011,6 +2039,14 @@ function applyBrowserTools(ctx, controller) {
2011
2039
  type: "boolean",
2012
2040
  description: "For open: put the new tab in the DSH tab group. Defaults to true; pass false to keep it loose."
2013
2041
  },
2042
+ waitUntil: {
2043
+ type: "string",
2044
+ description: "For open: when the new tab counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none)."
2045
+ },
2046
+ timeoutMs: {
2047
+ type: "number",
2048
+ description: "For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000)."
2049
+ },
2014
2050
  windowId: {
2015
2051
  type: "number",
2016
2052
  description: "For ungroup: only dissolve that window’s DSH group; omit to clear every window."
@@ -2036,10 +2072,19 @@ function applyBrowserTools(ctx, controller) {
2036
2072
  case "list": return await controller.execute("tabs.list", {}, exec.signal);
2037
2073
  case "open": {
2038
2074
  if (typeof args.url !== "string" || args.url.length === 0) throw new Error("open requires url");
2039
- const params = { url: args.url };
2075
+ const waitUntil = args.waitUntil === void 0 ? "load" : String(args.waitUntil);
2076
+ if (!(waitUntil in NAV_TIERS)) throw new Error(`browser_tabs open: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
2077
+ const params = {
2078
+ url: args.url,
2079
+ waitUntil
2080
+ };
2040
2081
  if (args.active !== void 0) params.active = args.active;
2041
2082
  if (args.group !== void 0) params.group = args.group;
2042
- return await controller.execute("tabs.open", params, exec.signal);
2083
+ if (waitUntil === "none") return await controller.execute("tabs.open", params, exec.signal);
2084
+ let budgetMs = NAV_TIERS[waitUntil];
2085
+ if (typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs)) budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
2086
+ params.timeoutMs = budgetMs;
2087
+ return await controller.execute("tabs.open", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
2043
2088
  }
2044
2089
  case "close": {
2045
2090
  const ids = closeTabIds(args);
@@ -26,6 +26,13 @@ import { BridgeError, classify } from "./errors.js";
26
26
  import { policyRefusal } from "./policy.js";
27
27
  /** Navigation wait ceiling (same as browser_wait); the bridge gets this plus BRIDGE_MARGIN_MS. */
28
28
  const NAV_MAX_MS = 120_000;
29
+ /**
30
+ * Same tier table as the extension's NAV_TIERS (extension/background.js). Both
31
+ * literals must stay in lockstep — verify_nav_waituntil.mjs extracts the
32
+ * extension's by regex and asserts the two agree (cross-side literals are wire
33
+ * protocol: a value only one side knows is a lie on the other).
34
+ */
35
+ const NAV_TIERS = { none: 0, commit: 4_000, domcontentloaded: 4_000, load: 15_000 };
29
36
  /**
30
37
  * Extra time handed to the bridge on top of the navigation budget. Without it a
31
38
  * caller-requested budget longer than the bridge default (60s) would be beaten by
@@ -367,7 +374,8 @@ function applyBrowserTools(ctx, controller) {
367
374
  parameters: {
368
375
  url: { type: 'string', required: true, description: 'Absolute URL to open in the tab.' },
369
376
  tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
370
- timeoutMs: { type: 'number', description: `How long to wait for the page load to settle: defaults to 15000, capped at ${NAV_MAX_MS}. Raise it for slow pages instead of retrying; drop it to 3000-5000 when you do not need the page to finish loading and will read/wait afterwards.` },
377
+ waitUntil: { type: 'string', description: 'When the navigation counts as arrived: "load" (default) waits for the page load event (15s budget), "domcontentloaded" returns once the DOM is parsed (async components may still be mounting — follow with browser_wait if an element is missing), "commit" once the new document\'s address lands, "none" returns immediately with no arrival check at all.' },
378
+ timeoutMs: { type: 'number', description: `How long to wait for the page load to settle: defaults to the waitUntil tier (load 15000, commit/domcontentloaded 4000), capped at ${NAV_MAX_MS}. An explicit value always wins over the tier.` },
371
379
  },
372
380
  output: {
373
381
  schema: {
@@ -380,6 +388,9 @@ function applyBrowserTools(ctx, controller) {
380
388
  pendingUrl: { type: 'string' },
381
389
  loadPending: { type: 'boolean' },
382
390
  siteUnreachable: { type: 'boolean' },
391
+ waitUntil: { type: 'string' },
392
+ waitedMs: { type: 'number' },
393
+ readyState: { type: 'string' },
383
394
  },
384
395
  },
385
396
  render: (_args, value) => {
@@ -404,14 +415,23 @@ function applyBrowserTools(ctx, controller) {
404
415
  isConcurrencySafe: () => false,
405
416
  presentCall: args => ({ card: 'generic', title: `Open ${args.url}`, kind: 'other' }),
406
417
  async execute(args, exec) {
407
- const params = { url: args.url };
418
+ const waitUntil = args.waitUntil === undefined ? 'load' : String(args.waitUntil);
419
+ if (!(waitUntil in NAV_TIERS)) {
420
+ throw new Error(`browser_navigate: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
421
+ }
422
+ const params = { url: args.url, waitUntil };
408
423
  if (args.tabId !== undefined)
409
424
  params.tabId = args.tabId;
410
- const budgetMs = typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)
411
- ? Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)))
412
- : 15_000;
413
- params.timeoutMs = budgetMs;
414
- const raw = await controller.execute('nav', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
425
+ // Explicit timeoutMs always wins over the tier; 'none' waits 0 and sends
426
+ // no budget at all. The bridge gets budget+2s so the extension's own
427
+ // "navigation did not arrive" always reports before the bridge's ceiling.
428
+ let budgetMs = NAV_TIERS[waitUntil];
429
+ if (typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)) {
430
+ budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
431
+ }
432
+ if (waitUntil !== 'none')
433
+ params.timeoutMs = budgetMs;
434
+ const raw = await controller.execute('nav', params, exec.signal, waitUntil === 'none' ? undefined : budgetMs + BRIDGE_MARGIN_MS);
415
435
  // The declared schema is closed, so the fields describing a navigation that did
416
436
  // not arrive have to be carried over on purpose. The old shape check
417
437
  // (`siteUnreachable === true`) also missed the object the extension really
@@ -429,6 +449,12 @@ function applyBrowserTools(ctx, controller) {
429
449
  || (typeof raw.siteUnreachable === 'object' && raw.siteUnreachable !== null);
430
450
  if (unreachable)
431
451
  out.siteUnreachable = true;
452
+ if (typeof raw.waitUntil === 'string')
453
+ out.waitUntil = raw.waitUntil;
454
+ if (typeof raw.waitedMs === 'number')
455
+ out.waitedMs = raw.waitedMs;
456
+ if (typeof raw.readyState === 'string' && raw.readyState.length > 0)
457
+ out.readyState = raw.readyState;
432
458
  return out;
433
459
  },
434
460
  }));
@@ -836,6 +862,8 @@ function applyBrowserTools(ctx, controller) {
836
862
  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.' },
837
863
  active: { type: 'boolean', description: 'Whether a newly opened tab becomes active; defaults to true.' },
838
864
  group: { type: 'boolean', description: 'For open: put the new tab in the DSH tab group. Defaults to true; pass false to keep it loose.' },
865
+ waitUntil: { type: 'string', description: 'For open: when the new tab counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none).' },
866
+ timeoutMs: { type: 'number', description: 'For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000).' },
839
867
  windowId: { type: 'number', description: 'For ungroup: only dissolve that window’s DSH group; omit to clear every window.' },
840
868
  },
841
869
  output: {
@@ -850,12 +878,24 @@ function applyBrowserTools(ctx, controller) {
850
878
  case 'open': {
851
879
  if (typeof args.url !== 'string' || args.url.length === 0)
852
880
  throw new Error('open requires url');
853
- const params = { url: args.url };
881
+ const waitUntil = args.waitUntil === undefined ? 'load' : String(args.waitUntil);
882
+ if (!(waitUntil in NAV_TIERS)) {
883
+ throw new Error(`browser_tabs open: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
884
+ }
885
+ const params = { url: args.url, waitUntil };
854
886
  if (args.active !== undefined)
855
887
  params.active = args.active;
856
888
  if (args.group !== undefined)
857
889
  params.group = args.group;
858
- return await controller.execute('tabs.open', params, exec.signal);
890
+ if (waitUntil === 'none') {
891
+ return await controller.execute('tabs.open', params, exec.signal);
892
+ }
893
+ let budgetMs = NAV_TIERS[waitUntil];
894
+ if (typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)) {
895
+ budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
896
+ }
897
+ params.timeoutMs = budgetMs;
898
+ return await controller.execute('tabs.open', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
859
899
  }
860
900
  case 'close': {
861
901
  const ids = closeTabIds(args);
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.3.0",
4
+ "version": "1.4.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"
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"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",