dsh-builtin-browser 0.1.20 → 0.1.21
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 -2
- package/README.md +5 -2
- package/lib/browser-electron/host-main.js +30 -7
- package/package.json +1 -1
- package/src/browser-electron/host-main.ts +27 -7
package/README.en.md
CHANGED
|
@@ -238,7 +238,7 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
238
238
|
| DeepSeek Harness (dsh) | `0.1.1-rc.2` (peer range `^0.1.1-rc.2`) |
|
|
239
239
|
| Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
|
|
240
240
|
| Node.js | `22.20.0` |
|
|
241
|
-
| dsh-builtin-browser | `0.1.
|
|
241
|
+
| dsh-builtin-browser | `0.1.21` |
|
|
242
242
|
| OS | Windows 10 (10.0.26200) |
|
|
243
243
|
|
|
244
244
|
> The plugin declares `electron >= 30`; it has **only been verified on Windows** (macOS/Linux untested, not yet promised).
|
|
@@ -253,7 +253,7 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
253
253
|
- `browser_download` fetches in the page context (keeps logins) and is subject to same-origin/CORS constraints; HTTP(S) targets only; `savePath` must be absolute (default confined to `~/Downloads`, override with `downloadDir`); single files are capped at 256 MB (streamed with a Content-Length early reject) and are written by the browser child itself (temp file + atomic rename).
|
|
254
254
|
- The self-hosted browser's cookies are stored in plaintext on disk (Electron default); deployments that need encrypted-at-rest should integrate a system keychain / DPAPI at the host layer.
|
|
255
255
|
- `browser_restrict` is a **soft guardrail** against accidental actions, not a security boundary: the model can lift it itself.
|
|
256
|
-
- Popups (`window.open` / `target=_blank`)
|
|
256
|
+
- Popups (`window.open` / `target=_blank`) open as a **new tab** in the same session window: the original page and its opener context survive and the jump is recorded in the session history. Non-HTTP(S) popups (OAuth handoff pages, `mailto:`, custom schemes) are still handed to the system, and untracked native windows are never created.
|
|
257
257
|
- The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies); it is available on the self-hosted browser only.
|
|
258
258
|
- After a self-hosted child crash (or a DSH restart that kills it) the browser host restarts automatically, and sessions opened before the crash **rebuild on their next use** — only page state is lost, no manual `browser_reset_session` needed (it still works for an explicit reset).
|
|
259
259
|
- Electron 44+ downloads its binary (~100 MB) on the first window open and needs network; it is never needed again afterwards. If detection runs without network, pre-install a binary and point `ELECTRON_PATH` at it.
|
|
@@ -301,6 +301,9 @@ Code layout:
|
|
|
301
301
|
| 12 | 2026-08-27 | **resolveElectronPath excludes packaged apps** (issue #6): added `isBareElectron` (a sibling `app.asar` means packaged — never reused, all platforms incl. macOS bundle layout); bundled-electron filesystem probe goes first; `ELECTRON_PATH` override wins first; missing dist errors clearly (`npx install-electron` hint) |
|
|
302
302
|
| Hardening | 2026-08-27 | **Three review passes hardened**: concurrent-recovery double-rebuild race (child `createView` made idempotent), macOS bundle-path detection, `dispose()` vs `start()` zombie-child race triple-guard, pendingSocket leak, fully unit-tested lookup order (24→25 tests) |
|
|
303
303
|
| **0.1.20** | 2026-08-27 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
|
|
304
|
+
| 13 | 2026-08-28 | **macOS/Linux untypeable inputs fix** (issue #7): the Windows focus routing from 0.1.16 (mousedown force-focus + window-refocus restore) shipped without a platform guard and fought macOS native click-to-focus, leaving login inputs untypeable → both handlers are now gated to `win32`; non-Windows restores native behavior |
|
|
305
|
+
| 14 | 2026-08-28 | **window.open/target=_blank opens a new tab** (issue #8): HTTP(S) popups no longer loadURL over the current view; they are handed to the parent and open as a **new tab in the same session window** — the opener page and its context survive (portal "workspace" jumps no longer 403) and the jump lands in session history; ungrouped views keep the fallback; non-HTTP popups still go to the system |
|
|
306
|
+
| **0.1.21** | 2026-08-28 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
|
|
304
307
|
|
|
305
308
|
## Acknowledgements
|
|
306
309
|
|
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ agent (browser_* 工具)
|
|
|
238
238
|
| DeepSeek Harness(dsh) | `0.1.1-rc.2`(peer 声明 `^0.1.1-rc.2`) |
|
|
239
239
|
| Electron | `44.0.0`(推荐 ≥ 40;33.x 存在合成器缺陷) |
|
|
240
240
|
| Node.js | `22.20.0` |
|
|
241
|
-
| dsh-builtin-browser | `0.1.
|
|
241
|
+
| dsh-builtin-browser | `0.1.21` |
|
|
242
242
|
| 操作系统 | Windows 10 (10.0.26200) |
|
|
243
243
|
|
|
244
244
|
> 插件声明 `electron >= 30`;**当前仅在 Windows 环境实测**(macOS/Linux 未验证,暂不承诺)。
|
|
@@ -253,7 +253,7 @@ agent (browser_* 工具)
|
|
|
253
253
|
- `browser_download` 在页面上下文内 `fetch`(带登录态),受同源/CORS 约束;仅允许 HTTP(S) 目标;`savePath` 必须为绝对路径(默认限定在 `~/Downloads`,可用 `downloadDir` 覆盖);单文件上限 256MB(流式限流,按 Content-Length 提前拒绝),文件由浏览器子进程直接落盘(临时文件 + 原子改名)。
|
|
254
254
|
- 自托管浏览器的 cookie 在磁盘上以明文存储(Electron 默认行为);需要加密落盘的部署应在宿主层接入系统钥匙串 / DPAPI。
|
|
255
255
|
- `browser_restrict` 是防误操作的**软护栏**,不是安全边界:模型可以自行解除白名单。
|
|
256
|
-
- 页面弹窗(`window.open` / `target=_blank`)
|
|
256
|
+
- 页面弹窗(`window.open` / `target=_blank`)会在同一会话窗口中**新开一个标签页**打开,保留原页面与 opener 上下文,跳转也计入会话历史;非 HTTP(S) 弹窗(OAuth 承接页、`mailto:`、自定义协议)仍交给系统处理,不创建未跟踪的独立窗口。
|
|
257
257
|
- `browser_auth` 的 cookie 往返不保留 `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie);仅自托管浏览器可用。
|
|
258
258
|
- 自托管浏览器子进程崩溃(或宿主 DSH 重启)后会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `browser_reset_session`。`browser_reset_session` 仍可用于主动重置。
|
|
259
259
|
- electron 随插件安装;Electron 44+ 首次打开浏览器窗口时自动下载二进制(约 100MB,需网络),之后不再需要。若探测时网络不可用,可预装 `ELECTRON_PATH` 指定的二进制。
|
|
@@ -301,6 +301,9 @@ npm run build
|
|
|
301
301
|
| 第十二轮 | 2026-08-27 | **resolveElectronPath 排除打包应用**(issue #6):新增 `isBareElectron`(旁有 `app.asar` 即打包应用,一律不复用,全平台含 macOS bundle 布局);bundled electron 纯文件系统探测置最优先;`ELECTRON_PATH` 显式覆盖最优先;dist 缺失时明确报错(`npx install-electron` 指引) |
|
|
302
302
|
| 全面复审加固 | 2026-08-27 | **三轮审查加固**:并发恢复双重建竞态(child `createView` 幂等化)、macOS bundle 路径判定、`dispose()` vs `start()` 僵尸 child 竞态三道闸、pendingSocket 泄漏、选路顺序全量单测(24→25 项测试) |
|
|
303
303
|
| **0.1.20** | 2026-08-27 | **发布**:第十一/十二轮 + 全面复审加固随 **0.1.20** 发布(构建零错误、25 项测试全绿) |
|
|
304
|
+
| 第十三轮 | 2026-08-28 | **macOS/Linux 输入框无法键入修复**(issue #7):0.1.16 引入的 Windows 焦点路由(mousedown 强制 focus + 窗口 refocus 恢复)未做平台判断,与 macOS 原生 click-to-focus 冲突导致登录框收不到键入 → 两处焦点逻辑加 `win32` 平台门,非 Windows 恢复原生行为 |
|
|
305
|
+
| 第十四轮 | 2026-08-28 | **window.open/target=_blank 新开标签**(issue #8):HTTP(S) 弹窗不再 loadURL 覆盖当前视图,转交父进程在**同一会话窗口新开标签**——原页面与 opener 上下文保留(门户「工作台」类跳转不再 403),跳转计入会话历史;未分组视图保留回退;非 HTTP 弹窗仍放行系统 |
|
|
306
|
+
| **0.1.21** | 2026-08-28 | **发布**:第十三/十四轮随 **0.1.21** 发布(构建零错误、25 项测试全绿) |
|
|
304
307
|
|
|
305
308
|
## 特别感谢
|
|
306
309
|
|
|
@@ -292,8 +292,12 @@ function windowFor(windowId) {
|
|
|
292
292
|
// regaining focus (alt-tab, click) does not automatically refocus any view
|
|
293
293
|
// (electron#28163) — the last addChildView keeps the focus. Restore the view
|
|
294
294
|
// the human was last using (click steering via wireFocusRouting), falling
|
|
295
|
-
// back to the visible page view.
|
|
295
|
+
// back to the visible page view. Windows-only: on macOS/Linux the OS routes
|
|
296
|
+
// keyboard input to the clicked view natively, and forcing focus() on window
|
|
297
|
+
// focus fights the platform — it can leave page inputs untypeable (issue #7).
|
|
296
298
|
w.on('focus', () => {
|
|
299
|
+
if (process.platform !== 'win32')
|
|
300
|
+
return;
|
|
297
301
|
const last = win.lastFocusedViewId;
|
|
298
302
|
const wc = last === TOOLBAR_FOCUS ? win.toolbarView?.webContents
|
|
299
303
|
: last !== undefined ? win.views.get(last)?.webContentsView.webContents
|
|
@@ -334,8 +338,15 @@ function layoutWindow(win) {
|
|
|
334
338
|
* regaining focus does not refocus any view, electron#28163). Focusing the
|
|
335
339
|
* clicked view on mousedown makes the toolbar and pages behave like a real
|
|
336
340
|
* browser: click the URL bar and type, click the page and type/scroll.
|
|
341
|
+
*
|
|
342
|
+
* Windows-only by design (issue #7): macOS/Linux already route keyboard input
|
|
343
|
+
* to the clicked view natively; calling webContents.focus() from inside the
|
|
344
|
+
* mousedown input-event there interferes with native click-to-focus and can
|
|
345
|
+
* leave login/page inputs unable to receive typed characters.
|
|
337
346
|
*/
|
|
338
347
|
function wireFocusRouting(win, view, focusKey) {
|
|
348
|
+
if (process.platform !== 'win32')
|
|
349
|
+
return;
|
|
339
350
|
view.webContents.on('input-event', (_event, input) => {
|
|
340
351
|
if (input.type !== 'mouseDown')
|
|
341
352
|
return;
|
|
@@ -508,14 +519,26 @@ async function handle(op, msg) {
|
|
|
508
519
|
const assignment = assignments.get(viewId);
|
|
509
520
|
const win = windowFor(assignment?.windowId);
|
|
510
521
|
const view = new WebContentsView();
|
|
511
|
-
// Route popups (window.open / target=_blank)
|
|
512
|
-
// instead of letting Electron open untracked native windows
|
|
513
|
-
// diverge from the session/tab model.
|
|
522
|
+
// Route popups (window.open / target=_blank) into the session/tab
|
|
523
|
+
// model instead of letting Electron open untracked native windows.
|
|
514
524
|
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
515
|
-
// HTTP(S) popups
|
|
516
|
-
//
|
|
525
|
+
// HTTP(S) popups open as a NEW TAB of this view's session: the
|
|
526
|
+
// parent (provider) owns the session model and its newTab action
|
|
527
|
+
// creates the tab in the same window, navigates it, and records it
|
|
528
|
+
// in the session history. Overriding THIS view with loadURL()
|
|
529
|
+
// destroyed the opener page — pages that need their opener context
|
|
530
|
+
// (portal dashboards carrying token/referer to the new window)
|
|
531
|
+
// lost it and the target page then 403'd (issue #8); the tab strip
|
|
532
|
+
// now keeps the original page alive like a real browser.
|
|
517
533
|
if (/^https?:/i.test(url)) {
|
|
518
|
-
|
|
534
|
+
if (win.windowId !== SHARED_WINDOW_ID) {
|
|
535
|
+
sendUserAction({ type: 'newTab', windowId: win.windowId, url });
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
// Ungrouped view (no live session to attach a tab to,
|
|
539
|
+
// defensive only): fall back to navigating this view.
|
|
540
|
+
void view.webContents.loadURL(url).catch(() => { });
|
|
541
|
+
}
|
|
519
542
|
return { action: 'deny' };
|
|
520
543
|
}
|
|
521
544
|
// Non-HTTP popups (OAuth redirects, mailto:, custom schemes):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-builtin-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (browser_auth), CAPTCHA detection, batch form fill (browser_fill), authenticated download, history/replay, action restriction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -327,8 +327,11 @@ function windowFor(windowId: string | undefined): HostWindow {
|
|
|
327
327
|
// regaining focus (alt-tab, click) does not automatically refocus any view
|
|
328
328
|
// (electron#28163) — the last addChildView keeps the focus. Restore the view
|
|
329
329
|
// the human was last using (click steering via wireFocusRouting), falling
|
|
330
|
-
// back to the visible page view.
|
|
330
|
+
// back to the visible page view. Windows-only: on macOS/Linux the OS routes
|
|
331
|
+
// keyboard input to the clicked view natively, and forcing focus() on window
|
|
332
|
+
// focus fights the platform — it can leave page inputs untypeable (issue #7).
|
|
331
333
|
w.on('focus', () => {
|
|
334
|
+
if (process.platform !== 'win32') return
|
|
332
335
|
const last = win.lastFocusedViewId
|
|
333
336
|
const wc = last === TOOLBAR_FOCUS ? win.toolbarView?.webContents
|
|
334
337
|
: last !== undefined ? win.views.get(last)?.webContentsView.webContents
|
|
@@ -361,8 +364,14 @@ function layoutWindow(win: HostWindow): void {
|
|
|
361
364
|
* regaining focus does not refocus any view, electron#28163). Focusing the
|
|
362
365
|
* clicked view on mousedown makes the toolbar and pages behave like a real
|
|
363
366
|
* browser: click the URL bar and type, click the page and type/scroll.
|
|
367
|
+
*
|
|
368
|
+
* Windows-only by design (issue #7): macOS/Linux already route keyboard input
|
|
369
|
+
* to the clicked view natively; calling webContents.focus() from inside the
|
|
370
|
+
* mousedown input-event there interferes with native click-to-focus and can
|
|
371
|
+
* leave login/page inputs unable to receive typed characters.
|
|
364
372
|
*/
|
|
365
373
|
function wireFocusRouting(win: HostWindow, view: WebContentsView, focusKey: string): void {
|
|
374
|
+
if (process.platform !== 'win32') return
|
|
366
375
|
view.webContents.on('input-event', (_event, input) => {
|
|
367
376
|
if (input.type !== 'mouseDown') return
|
|
368
377
|
win.lastFocusedViewId = focusKey
|
|
@@ -509,14 +518,25 @@ async function handle(op: string, msg: { id: number; viewId?: string; windowId?:
|
|
|
509
518
|
const assignment = assignments.get(viewId)
|
|
510
519
|
const win = windowFor(assignment?.windowId)
|
|
511
520
|
const view = new WebContentsView()
|
|
512
|
-
// Route popups (window.open / target=_blank)
|
|
513
|
-
// instead of letting Electron open untracked native windows
|
|
514
|
-
// diverge from the session/tab model.
|
|
521
|
+
// Route popups (window.open / target=_blank) into the session/tab
|
|
522
|
+
// model instead of letting Electron open untracked native windows.
|
|
515
523
|
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
516
|
-
// HTTP(S) popups
|
|
517
|
-
//
|
|
524
|
+
// HTTP(S) popups open as a NEW TAB of this view's session: the
|
|
525
|
+
// parent (provider) owns the session model and its newTab action
|
|
526
|
+
// creates the tab in the same window, navigates it, and records it
|
|
527
|
+
// in the session history. Overriding THIS view with loadURL()
|
|
528
|
+
// destroyed the opener page — pages that need their opener context
|
|
529
|
+
// (portal dashboards carrying token/referer to the new window)
|
|
530
|
+
// lost it and the target page then 403'd (issue #8); the tab strip
|
|
531
|
+
// now keeps the original page alive like a real browser.
|
|
518
532
|
if (/^https?:/i.test(url)) {
|
|
519
|
-
|
|
533
|
+
if (win.windowId !== SHARED_WINDOW_ID) {
|
|
534
|
+
sendUserAction({ type: 'newTab', windowId: win.windowId, url })
|
|
535
|
+
} else {
|
|
536
|
+
// Ungrouped view (no live session to attach a tab to,
|
|
537
|
+
// defensive only): fall back to navigating this view.
|
|
538
|
+
void view.webContents.loadURL(url).catch(() => { /* navigation failures surface on the next snapshot */ })
|
|
539
|
+
}
|
|
520
540
|
return { action: 'deny' }
|
|
521
541
|
}
|
|
522
542
|
// Non-HTTP popups (OAuth redirects, mailto:, custom schemes):
|