Repository navigation
Releases: angelky7/dsh-browser-control
Release list
v1.1.0 — npm 包名迁移到 @angelky7/dsh-browser-control(无功能变更)
v1.1.0
这一版只做一件事:npm 包名随账号改名迁移。插件代码、扩展代码与 v1.0.19 完全相同,没有功能变更。
包名变更
GitHub 账号已由 caob23 改名为 angelky7,而 npm 的用户名无法修改,因此新建同名组织 angelky7 承接 scope。
| 旧 | 新 | |
|---|---|---|
| npm 包名 | @caob23/dsh-browser-control |
@angelky7/dsh-browser-control |
| 安装命令 | dsh plugin --profile web add @caob23/dsh-browser-control |
dsh plugin --profile web add @angelky7/dsh-browser-control |
- 旧包在 npm 上保留,但已
npm deprecate,安装时会提示去向 - 也评估过去掉 scope 的
dsh-browser-control、dsh-browser-bridge等名字,均已被他人占用,最终回到带 scope 的方案
升级步骤(装过旧版的用户)
dsh plugin --profile web remove @caob23/dsh-browser-control
dsh plugin --profile web add @angelky7/dsh-browser-controlprofile 覆盖层里手写过 name: '@caob23/dsh-browser-control' 的配置需一并更新;只写 id 的不受影响(宿主合并按 id 认)。
扩展不用重装
extension/ 未改动,DSH-Browser-Control-v1.0.19.zip 仍是当前版本,继续用它即可。
安装
dsh plugin --profile web add @angelky7/dsh-browser-control@1.1.0
# 撞上 ERR_PNPM_NO_MATCHING_VERSION(镜像滞后)时,这一次显式走官方源:
dsh plugin --profile web add @angelky7/dsh-browser-control@1.1.0 --registry=https://registry.npmjs.org/ERR_PNPM_NO_MATURE_MATCHING_VERSION 是你机器上的 24 小时新包门槛,不是包坏了——判别与三条绕法见 README。
资产
| 资产 | 说明 |
|---|---|
dsh-browser-control-plugin-v1.1.0.zip |
插件离线包;解压后 dsh plugin --profile web add file:<解压目录> |
扩展资产不重复发布,仍见 v1.0.19。
资产校验和
| 资产 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
dsh-browser-control-plugin-v1.1.0.zip |
113 341 B | 2af8aadf04071f399c00ccf5dfb1356aeea9849baf6cdb4c2938a9c4046c789d |
2c2d4fd7b83ee48e1f38ccd71820f519 |
发布前本地解包复验:zip 头 504b0304、CRC 全过、无反斜杠条目、含 package.json 与 cordis.patch.yml、package.json 的 name 为 @angelky7/dsh-browser-control。
v1.0.19 — 接管浮层 browser_hold(防误触的软层)+ 四处复审修补
v1.0.19
一个外部 PR 带来新工具 browser_hold(接管浮层),加上合并后复审抓到的四处问题。这一版改了 extension/,扩展必须重装。
1. 接管浮层:browser_hold(合并 PR #18,@long-arm-ape)
给某个标签页加一层可撤销的提示层:页面变暗,中间一张卡片写明模型在做什么,挡住误触。定位是告知 + 防误触的软层,不是安全边界——桥的 token 模型读得到也改得动;真正能冻住浏览器的是 v1.0.18 的弹窗「锁定」。
- 标题由扩展写死(
DSH Browser Control),调用方只能给正文note(压平换行、截到 400 字符)。否则模型能写一句系统口吻的话冒充提示。 - 关掉 = 用户接管,不拦模型:双击任意处 / 连按两下 Esc / 按住 Esc ≈0.6 秒。关掉之后模型照常操作这一页。
- 卡片上的「中止模型操作」才拦:两次点击确认(第二次 3 秒内),之后
click/input/press/nav在扩展的runCommand入口被拒,零 CDP 派发。模型要恢复必须先与用户确认,再调browser_hold action=off——那一步等于「确认继续」。 - 状态在
chrome.storage.session、按tabId记:活过 service worker 回收(内存标志 30 秒就丢,那正是浮层「以为挡着、其实没挡」的失败方式),不落盘,浏览器关掉即清。 - 盖不住就如实说:
covered:false+reason(error-page/browser-page/devtools/inject-failed)。已知盖不住:严格 CSP、全屏、跨源 iframe、PDF 阅读器、原生弹窗。 - 导航即消失:页面换了浮层就没了,此时命令放行、存储里那条顺手清掉、
status如实报active:false。 - 模型的错误面也接上了同一套线协议:中止拒绝的前缀
browser-hold-aborted:→code=held_by_user、effectState=not_started,提示指向browser_hold action=off,而不是「换个选择器重试」。
2. 合并后复审抓到的四处
- 模型自己的点击被自己的浮层吃掉。 浮层全屏、
z-index最大,click按坐标派发先命中的是它——页面什么都没发生,结果只轻描淡写一句hitVerified:false;更糟的是同一坐标连点两下会被浮层的「双击接管」逻辑当成用户操作,以用户的名义把浮层关掉。现在命中到自己那层时临时把宿主设成pointer-events:none、重测命中点、派发完在finally里恢复,结果带bypassedOwnOverlay:true。浮层挡的是人,不是模型。 - AX 快照把浮层自己编进去了。 卡片上的「中止模型操作」是标准
button,会进无障碍树:模型读到的快照里就有自己挂的提示语,甚至能按 ref 点到它。现在 stamping 函数遇到[data-dsh-hold]子树直接跳过:不占 ref、不计入total(ax.ownOverlay单独计数)。DOM 模式原本就排除了,这次补齐 AX 模式。 hold on会把「中止」静默清掉。 模型被拦一次之后只要再on一次就绕过去了,既不用问用户也不必说「继续」。现在中止期间hold on被拒,只有off能解。- 只读档把
hold status也拒了。status只读(读 session 条目 + 探一下页面那层还在不在),不是写操作;「只读」不该连看一眼浮层状态都不让。闸门改为按动作判:policyRefusal(command, flags, params),on/off仍拒、status放行。
3. 验证
npm run verify548 项断言,exit 0(535 来自 PR #18 与合并,+13 是本轮的台子):浮层台 44→52、AX 台 18→22、策略台 41→42。- 点击旁路那条不是断言假件返回值:台子里有一台真的执行定位表达式的假 DOM,浮层在场时
elementFromPoint先返回宿主,宿主pointer-events:none之后才返回目标。 - 四处修法各自做过变异测试:摘掉 AX 排除 → 4 项红(快照里真的出现「中止模型操作」);去掉 pointer-events 旁路 → 2 项红(
hitVerified:false, hitInstead:"div");去掉中止检查 → 2 项红(hold on成功且注入次数变 2);只读档那条第一次只改了一半分支,新断言当场把它抓红。 - PR #18 自带的证据:页面侧三个函数在 Edge 156 + 真 CDP 上跑过 13 条断言(含几何探测:
position:fixed、满视口、z-index:2147483647、elementFromPoint命中浮层);lib/与src/重建一致。 - 仍未覆盖(如实写):vm 台子里的
chrome.debugger是假的,真 MV3 worker 走真扩展安装的注入链没有被端到端断言;严格 CSP / 全屏 / 跨源 iframe 只保证如实回报covered:false,没有逐项做真机断言。
4. 安装
dsh plugin --profile web add @caob23/dsh-browser-control@1.0.19
# 撞上 ERR_PNPM_NO_MATCHING_VERSION(镜像滞后)时,这一次显式走官方源:
dsh plugin --profile web add @caob23/dsh-browser-control@1.0.19 --registry=https://registry.npmjs.org/ERR_PNPM_NO_MATURE_MATCHING_VERSION 是你机器上的 24 小时新包门槛,不是包坏了——判别与三条绕法见 README。
扩展:解压 DSH-Browser-Control-v1.0.19.zip → chrome://extensions → 开发者模式 → 移除旧版(或点扩展卡片上的刷新)→「加载已解压的扩展程序」。不重装就还是旧代码(browser_hold 与上面的修法都在扩展侧)。
5. 资产校验和
| 资产 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.19.zip |
61 267 B | 1ee9f08b36a22bf47ff8716a3981c5c1ea8c4c3309af6fddf54d069d468a2c76 |
9ff23c7dfdd81fb1ea2a62c34a5982bd |
dsh-browser-control-plugin-v1.0.19.zip |
203 264 B | 19126d0c472a538ee145aae08b4efd48ee3c3e1cd03fbae2368722423dc2c8d4 |
c17e0111f1d0d7a66e23997722bb3158 |
发布前本地解包复验(zip 头 504b0304、CRC 全过、无反斜杠条目、插件包含 cordis.patch.yml、扩展 manifest version 为 1.0.19、权限仍是 debugger / tabs / tabGroups / storage / alarms),发布后匿名重下逐字节比对。按惯例不放 .sha256 附件。
v1.0.18 — 弹窗一键冻结(只有用户能解)+ 只读档与脚本开关
v1.0.18
两件事是从外部评审来的:扩展弹窗里的一键冻结(PR #15)与插件侧的只读档 / 脚本开关(PR #16);第三件事是我合并后复审抓到的四个缺口,都补在这版里。
这一版改了
extension/——扩展必须重装,只更新插件不够。装完扩展之后弹窗里会多一个「锁定」按钮。
1. 本地硬锁:用户一键冻结,模型解不开(合并 PR #15,@long-arm-ape)
桥的唯一实际边界是回环地址 + token,而 token 明写在 profile 的 cordis.patch.yml 里——模型读得到、改得动,还能重启桥。所以「模型自己把浏览器控制关掉」在当前架构里本来就是可行的,缺的是反过来那一句:用户想要一个「别碰我的浏览器,而且只有我能解除」的开关。
- 点扩展图标 → 按钮行中间的「锁定」,单击即生效(打开弹窗本身已经是人的动作,panic switch 不该再加一次确认)。
- 冻结期间 WebSocket 保持连接:调用方看到「连得上但被冻结」,不会误判成掉线去重试。
- 拒绝发生在扩展
runCommand的入口,任何 handler 都不执行 ⇒ 零 CDP 派发、零标签页改动,只读命令也一样(冻结的意思是什么都别做,不是「只别写」)。 - 徽标从绿色
ON换成红色LOCK。 - 解锁只有一条路:同一个弹窗里点「已锁定」。这是结构性而非承诺——锁通过
chrome.runtime消息设置,只有扩展页面能发;桥那边没有任何解锁命令,lockSet也不在COMMANDS表里,模型塞一条同名命令帧只会被当作普通命令拦下。 - 状态落
chrome.storage.local,PROTOCOL_VERSION仍是 1。
2. 只读档与脚本开关:readOnly / allowScript(合并 PR #16,@long-arm-ape)
profile 覆盖层里给 browser-bridge 加两个字段(默认值就是老行为,装上不会突然变只读):
config:
readOnly: true # 只读:点击/输入/按键/导航/滚动/标签页操作/视口模拟/清缓冲全拒
allowScript: false # 再把 browser_evaluate(在页面里跑任意 JS)单独关掉- 闸门只有一个位置:
BridgeController.execute(),在命令发给扩展之前。被拒的命令零副作用,错误码blocked_by_policy、effectState=not_started,文案写明改哪个字段并劝退原地重试。 - 为什么要两项而不是一项:只让读工具可用不够——
browser_evaluate能在页面里跑任意 JS,等于把点击、输入、改页面全绕过去。 - 只读白名单是「读页面 + 读扩展缓冲」那 11 条,默认拒绝:扩展以后新增命令不会自动放行,
verify/verify_policy.mjs会把扩展COMMANDS表 26 条逐条对账。 - 与
WRITE_COMMANDS是两张故意不同的表(那张判「重试会不会多造成一次副作用」,所以tabs.group算只读;这张判「会不会改变状态」,所以不算)。理由写在src/policy.ts顶部。 - 边界如实写着:这两个开关约束的是模型那条工具面。HTTP 面(
/api/command,要你的 token)是你的通道、不受影响;真要冻结整个扩展请用上面那个「锁定」。
3. 合并后复审抓到的四处
browser_cleanup整个绕过了只读档。 它不调execute(),而是走controller.cleanup()在插件进程里逐条rm——所以readOnly: true开着时,模型仍能一条命令删掉用户整个截图目录,而删除恰恰是「只读」最该拦住的动作。现在src/policy.ts里有第三张表LOCAL_DESTRUCTIVE_COMMANDS,cleanup()自己站闸门,拒绝文案说的是「会删除本机保存的截图与临时文件……没有任何文件被删除」。台子不只断言文案:先往临时目录写一个文件,被拒之后再读一次目录确认文件还在。cleanup从没进过写/读分类表,它的失败也没有错误契约。 它是循环里逐个await rm、没有 per-entry catch,半路失败(Windows 上被占用的截图 EPERM)时前面已经删掉了;而原来只有「尚未加载配置」那一句调了classify(),其余异常是裸 fs 错误直接冒出来——模型既拿不到code/effectState/hint,默认含义还是not_started「没动过,重试安全」。现在classify('cleanup', error)+ 登记为写操作。对账口径同时从「扩展 COMMANDS 表」扩到「src/index.ts里每个classify('…')用到的名字」——这是 v1.0.17 那次tabs.closeMany漏登记的同一族问题,只是发生在插件本地这一侧,原来那条对账永远看不见它。- 硬锁读不出锁状态时原来是 fail-open。
catch里只记一句lastError、标志保持「没锁」,而且照样把「已加载」置真——一次瞬时读盘失败之后不再重试,锁永久没了,徽标还是绿色ON,没人会知道。现在读不到就拒绝,且不置「已加载」,下一条命令重读、存储恢复后自愈;用户仍解得开(弹窗那条通道走内存里的setHardLock,不依赖这次失败的读)。 - 只有
runCommand会触发重读,所以 service worker 被回收再唤醒之后、第一条命令到来之前,图标显示的是ON——用户以为锁着,桥那边一切正常。现在init()先读锁再连接。
另外把锁的拒绝做成了模型读得到的形状。结果帧只有一个 error 字符串能带信息,扩展抛裸 Error 会被分类成 command_failed,模型于是读到 effectState=unknown:动作可能已经落到页面上 + hint「通常是选择器没命中」——拦在 handler 之前,这两句都指向错误的下一步。现在扩展在文本最前面写 browser-locked-by-user: / browser-lock-unreadable: 前缀,classify() 认它 ⇒ code=locked_by_user、effectState=not_started(写操作也是),标记本身从给模型的那句话里剥掉;两个字符串是跨 extension/ 与 src/ 的线协议,verify_errors 对账两侧必须逐字一致。两种来源的文案也分开了:读不到状态时不说「已被用户锁定」,免得模型转告用户去点一个他根本没按过的按钮。
4. 验证
npm run verify478 项断言,exit 0:385(v1.0.17 基线)→ 411(硬锁台 26)→ 440(策略台 29)→ 451(cleanup 过闸门 + classify 对账)→ 478(锁的 fail-closed / 开机即 LOCK / 端到端错误形状)。- 端到端那条是真桥 + 真
background.js+ 真 WebSocket:锁着的click在 HTTP 面拿到locked_by_user/not_started,且Input.dispatch*的派发次数增量为 0。 - 每条修法都做过变异测试(改回旧行为确认台子会红):fail-open → 4 项红(其中一条报
no tab available,证明命令真的执行到了 handler);去掉init()的预读 → 3 项红(徽标OFF);扩展侧前缀改名 → 对账红;classify()去掉锁分支 → 5 项红;摘掉policyGate('cleanup')→ 3 项红;cleanup从写清单摘掉 → 3 项红。 - PR #16 提交的
lib/与重新构建的结果逐字节一致(npm run build之后git diff lib/为空)。 - 还没做的一项如实写着:
readOnly/allowScript出现在 dsh 0.1.7+ 的设置页里,是按宿主投影Configschema 这个已实测机制推出来的,这两个新字段本身还没在真宿主设置页里看过。 - CI:ubuntu 与
windows-latest两个 job 都在 main 上跑通。
5. 安装
在线装(写这一节时官方源 latest 已经是 1.0.18;npmmirror 的同步有延迟,latest 还停在 1.0.17,所以下面两种失败都可能出现,判别见 README 的安装一节):
dsh plugin --profile web add @caob23/dsh-browser-control@1.0.18
# 撞上 ERR_PNPM_NO_MATCHING_VERSION(镜像滞后)时,这一次显式走官方源:
dsh plugin --profile web add @caob23/dsh-browser-control@1.0.18 --registry=https://registry.npmjs.org/如果撞 ERR_PNPM_NO_MATURE_MATCHING_VERSION 那是你机器上的 24 小时新包门槛,不是包坏了;两条的判别与三条绕法见 README。离线包就是下面这两个 zip。
扩展:解压 DSH-Browser-Control-v1.0.18.zip → chrome://extensions → 右上角开发者模式 → 「加载已解压的扩展程序」。旧版本必须先移除或点扩展卡片上的刷新,否则跑的还是 1.0.17 的代码(没有锁,键盘修复也一样带着)。
6. 资产校验和
| 资产 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.18.zip |
53 789 B | 8991fdf364d3939fe2952f01f015c2ce91cbeed603301b89f43dac6918332bc8 |
d25c5f415de2a767c55a4277fc07e08d |
dsh-browser-control-plugin-v1.0.18.zip |
189 500 B | 3203b2369dc106afa309d528aabfaab402e3464e01b9663116b54cfb90b3b30e |
7c5754224c2ee638d4be26a7ab66f64c |
两份包都在发布前本地解包复验过(zip 头 504b0304、CRC 全过、无反斜杠条目、插件包含 cordis.patch.yml、扩展 manifest 里 version 为 1.0.18、权限仍是 debugger / tabs / tabGroups / storage / alarms),发布后又匿名重新下载逐字节比过一次。按本仓库惯例不放 .sha256 附件,校验和只写在这里。
v1.0.17 — 键盘双倍输入修复 + 批量关标签(每个 id 都有交代)
v1.0.17
三件事:browser_press 一次按键插入两次(表单被提交两遍)修好了、browser_tabs close 现在能一次关一批且每个 id 都有交代(合并 PR #11)、以及批量关闭的超时分类修正——合并进来之后我自己复审时发现的缺口。
这一版改了
extension/——扩展必须重装,只更新插件不够。
1. 键盘:一次按键只许插入一次(issue #14)
cmdPress 在 Input.dispatchKeyEvent 的 keyDown 已经带 text(Chrome 据此就已插入字符)之后,又补发一个独立的 char 事件——两处各插一次:
- 输入框里按一下
x→ 得到xx - 按一下
Enter→ 表单提交两次 browser_type的submit:true走同一条通道,所以提交类操作会做两遍
同一个仓库里这个坑早就在 cmdInput 的 type 模式修过,注释就写着「an explicit char event duplicates the input」,CHANGELOG v1.0.2 也记了一笔——只有 cmdPress 没跟上,从 v0.2.0(be6c302)一路留到现在。取报告方(@wangyuncai2024)的方案 B:删掉 char 分支,与 cmdInput 一致(他的方案 A 改 rawKeyDown 保留 char,实测等价,但仓库里会同时存在两种写法)。
同一函数里另外两处也一并修了:
- 组合键现在真能用:
"Ctrl+A"/"Shift+ArrowRight",或modifiers: ["ctrl"]。以前扩展层支持modifiers而工具层没暴露,写"Shift+ArrowRight"只是静默无效(不报错也不选中文本)。shift单独存在时照常打字,ctrl/meta/alt抑制插入。 - 认不出的键名改为抛错,不再带着
undefined的 keyCode/code 照样派发、再回一句pressed:true让调用方以为成功。
为什么以前测不出来:报告方的判断成立——verify/ 对 browser_press 只测过模型可见文本,整个仓库没有任何一条按键事件计数。新增 verify/verify_press_events.mjs(19 项)第一次按派发次数测:单字符 / Enter / 数字键 / type 模式逐字符都必须「插入型事件恰好一个、不得有 char」,外加组合键解析与未知键名零派发。这个台子可以传入另一份 background.js:对修复前的版本跑同一批断言红 12 项并如实报出 keyDown,char,keyUp,所以它不是跟着新代码写的空断言。
2. 批量关标签页(合并 PR #11,@long-arm-ape)
browser_tabs action:"close" 收 tabId(一个)或 tabIds(一批)。一批在链路上只发一条命令(扩展侧 tabs.closeMany → 一次 chrome.tabs.remove([ids])),不再 N 次往返中间掉线。结果统一为 {requested, closed, alreadyGone, failed:[{tabId,error}]},单数与批量同一个形状(cmdTabsClose 直接委托给 cmdTabsCloseMany)。以前的 for (id of ids) await tabs.close(id) 第一个失败就抛出,会把已经真的关掉的标签藏起来,模型以为什么都没发生于是重放整批。alreadyGone(早就没了)算幂等成功;并且不猜——remove 之后逐个复查,「还在」的才进 failed,被拒但其实已关掉的如实进 closed。
他们顺手挖出了我们自己的对账缺陷:verify_surface.mjs 的命令名正则 [a-z_.]+ 会把 tabs.closeMany 截成 tabs.close,新命令名进不了对账——已放宽。
3. 合并后的复审抓到的缺口:tabs.closeMany 没登记为写操作
effectState 是按命令名判写/读的。批量关闭是新命令名,没进写清单就会静默落到 not_started,于是超时报文会写着「effectState=not_started:这次调用没有动到页面,重试安全」——而实际可能已经关掉了一半标签。这正是本仓库这一路在修的那类不诚实,只不过这次出现在我刚合并进来的代码上。
修法:登记为写操作,并加一条对账护栏——从 extension/background.js 的 COMMANDS 表解析出全部 26 条命令,每条要么在写清单里,要么在测试侧显式登记为读/幂等并写理由;新增命令不做判断就直接红。护栏做过变异测试:临时摘掉 tabs.closeMany 的登记,两条断言立刻点名该命令并 FAIL。
关于 1.0.16
1.0.16 只存在于 npm(没有 tag、没有 Release、没有在任何地方推荐安装)。原因是发布流程走到一半——npm publish 之后、建 Release 之前——上面第 3 条缺口被发现,而 npm 的版本不可改写。为避免有人按确切版本号装到那个半修好的版本,已对 1.0.16 下 npm 弃用提示指向本版。请直接装 1.0.17。
测试台
npm run verify 331 → 350(键盘台)→ 381(PR #11 自带 31 项)→ 385 项断言,exit 0。CI 在合并提交上 ubuntu 与 windows-latest 两个 job 都 success。
还没验证的部分(说实话)
- 键盘修复没有在真实 Chrome 上跑过。 断言来自「有状态假
chrome.debugger+ 真实扩展代码」,测的是我们能控制的那一层(派发序列);「Chrome 收到带text的 keyDown 之后到底插几个字符」是浏览器的行为——报告方给了裸 CDP 三组对照支持方案 B,但那是他的环境。你重装扩展后请按一次x看是否只出一个字符、按一次Enter是否只提交一次;若仍翻倍,请在 #14 直接回我,我改按方案 A(rawKeyDown+char)。 - 批量关闭的
closed/alreadyGone/failed划分在假chrome.tabs上验过,真实 Chrome 里「remove 之后复查」的时序仍需一次实机确认。 - 前面几版留下的:归组的视觉效果、AX 快照在真实页面上的表现与耗时(扩展升到本版后即可测 AX)。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.17.zip |
50969 B | bb117b0f0f8c597540eb21e64916062d8db5f4d8e9a40dbae011b19efeb708e9 |
bcd59b1f2bdf7ff40d06e8191f72782a |
dsh-browser-control-plugin-v1.0.17.zip |
172353 B | b774702ee1e4cb8c0ccf0a24bb198ff7e6b2830cce1910c8c66d504165faf64f |
6dc197519825d8ea04a68d40fedfcbac |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.17、权限 debugger, tabs, tabGroups, storage, alarms。
安装(两步都要做)
- 扩展:下载
DSH-Browser-Control-v1.0.17.zip→ 解压 →chrome://extensions→ 开发者模式 → 先移除旧版再「加载未打包的扩展程序」,确认版本显示 1.0.17。不重装扩展,键盘修复不会生效。 - 插件:
dsh plugin --profile <你的profile> add @caob23/dsh-browser-control@1.0.17。刚发布 24 小时内可能撞 dsh 自带的包龄闸门,报ERR_PNPM_NO_MATURE_MATCHING_VERSION等满即可;报ERR_PNPM_NO_MATCHING_VERSION才是源没同步,那一次加--registry=https://registry.npmjs.org/,或用上面的离线插件包。 - 自检:
http://127.0.0.1:9777/状态页应显示已连接;在输入框让 agent 按一次字母,页面里只应多出一个字符;browser_tabs close传tabIds:[a,b]时结果会把每个 id 的去向写出来。
v1.0.15 — 真实宿主×真实浏览器接入验证;旧扩展静默忽略 mode:ax 现在如实回报
v1.0.15
这一版的价值主要在验证方式上:第一次把真实宿主和真实浏览器接进验证链,而它当场挖出一个假扩展台子永远发现不了的缺陷——旧扩展会静默忽略 mode:"ax"。extension/ 代码没有改动(manifest 只是版本戳),老扩展不用升级就能受益于这次的修复。
1. 真实宿主 × 真实浏览器,第一次跑通
用本机已安装的 dsh(@deepseek-ai/dsh 全家 0.2.0-rc.2,版本号从 app.asar 里直接读出)在隔离的 DSH_HOME 里建 profile、装上插件、让真实 Chrome 扩展连上桥,然后把只读工具跑在真实代码路径上:
- 宿主组装出的层:
- id: browser-bridge / name: '@caob23/dsh-browser-control' / config: {enabled: true, port: 9777, token: dsh-local}—— issue #4 那条「装上即监听」第一次被真实宿主证实,而不是靠假 cordis 推断。 - 顺带实测回答了一条含糊配置:profile 里只写
- id: browser-bridge+disabled: true(不带name)就足以关掉本插件——宿主的合并按id认。 - 真实 Chrome 153 + 扩展 1.0.10 下 9 条只读工具全过:
browser_read交出 1341 字符真实正文、browser_snapshot交出 14 条带[e1] a "首页" -> /apply/main.do的清单、每条输出都零 schema 违规。这同时证明 1.0.12 那两个修复在真实链路上确实生效。
2. 挖出来的缺陷:旧扩展静默忽略 mode:"ax"
扩展 1.0.10(早于 1.0.11 引入的 AX 快照)收到 mode:"ax" 时不报错,回一份 DOM 结果,而且既不带 mode 也不带 axFallback——本机实测两次请求(ax 与默认)的响应完全一样。模型于是只看到 (mode dom),读不出「我要的 ax 被旧扩展降级了」,会把标签名当成可访问名,在按名字找元素时判断就错了。
修法全在插件侧:比对请求的模式 vs 回报的模式(mode === 'ax' 或带 ax 计数才算真走了 AX),不一致就自己写 axFallback:
(mode ax fell back to dom: this extension ignored mode:"ax" and returned CSS-DOM matches
(AX mode needs DSH Browser Control extension v1.0.11 or newer) — the names below are tag names, not accessible names)
老扩展不升级也立刻受益;想要真的 AX 语义,把扩展升到 ≥ 1.0.11(本版 zip 即可)。补了 4 项断言:静默降级 / 真走了 AX 不许凭空报降级 / 扩展自带降级原因时以它为准 / 降级原因必须进得了模型可见文本。
3. 顺带纠正一条我自己的说法
ERR_PNPM_NO_MATURE_MATCHING_VERSION 与 #13 里的 ERR_PNPM_NO_MATCHING_VERSION 是两个不同的原因,本机实测区分开了:dsh 的 profile 自带 24 小时「包龄」供应链闸门(node_modules/.pnpm-workspace-state-v1.json 里 minimumReleaseAge: 1440),刚发布的版本被它拦下时报前者,报文里直接写 was published at …, within the minimumReleaseAge cutoff (…);而 NO_MATCHING_VERSION 是「你配置的源里没有这个版本」(镜像滞后)。README 与 CONTRIBUTING 现在把两条判据都写上了——两种都只能等,但只有后者换 registry 或离线包有用。
测试台
npm run verify 331 项断言 exit 0(v1.0.14 是 327),ubuntu 与 windows-latest 两个 job 都跑。
还没验证的部分(说实话)
- 真实浏览器这一跑只覆盖只读命令;
click/type/navigate/tabs.open(含归组)与写操作的effectState路径仍未在真实 Chrome 上验过——不是不想,而是不该在你的浏览器里留下副作用。 browser_emulate这次仍然只有假chrome.debugger的证据(1.0.14 修的是输出契约层)。- 归组的视觉效果(颜色、分组条)与 AX 快照在真实页面上的耗时仍未实测:本机扩展是 1.0.10,压根没有 AX 通路,要看 AX 得先装 ≥1.0.11 的扩展 zip。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.15.zip |
49210 B | 2c56a99cc64a5e6424ee3207d880febe86224922848021b429f09c6a38dfe915 |
0bb24bbf4de285fbbd10ea3d36b12c05 |
dsh-browser-control-plugin-v1.0.15.zip |
166836 B | 6420f1ed8922b0747d480df42a549e6c5d49c9f5c20bf13751bc21e7b0bb0f58 |
5497142b78c2c6168d012803973dd286 |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.15、权限 debugger, tabs, tabGroups, storage, alarms。
发布时的源状态(实测,别照着报错猜)
| 时间(UTC) | 官方源 registry.npmjs.org |
镜像 registry.npmmirror.com |
|---|---|---|
| 2026-10-12 15:08 | latest = 1.0.15(发布时间 14:53:49Z) |
latest = 1.0.14,还没有 1.0.15 |
也就是说到本 Release 建立时为止,镜像已经滞后约 15 分钟(历史上从 1 分钟到几小时都出现过)。如果你的 npm config get registry 指向 npmmirror,按下面安装方式第 1 条加 --registry=https://registry.npmjs.org/,或用离线包,或直接 git 通道(#v1.0.15,不经过 npm)。这一步我会在镜像跟上后回来更新这一节。
安装
- 插件:
dsh plugin --profile <你的profile> add @caob23/dsh-browser-control@1.0.15。刚发 24 小时内可能撞上包龄闸门(NO_MATURE_MATCHING_VERSION),等满 cutoff 即可;报NO_MATCHING_VERSION才是源没同步,那一次加--registry=https://registry.npmjs.org/。 - 离线兜底:解压
dsh-browser-control-plugin-v1.0.15.zip后dsh plugin --profile desktop add file:<解压目录>(完全不过 registry,也不受包龄闸门影响)。 - 扩展:只在想要真 AX 快照时才需要升级到 ≥1.0.11(本版 zip 即可);不升也不会再被静默降级误导。
- profile 里想关掉插件:
- id: browser-bridge+disabled: true就够(实测按 id 合并)。
v1.0.14 — emulate 的 null 覆盖不再打穿输出契约;Windows 进 CI
v1.0.14
browser_emulate 的一个必然失败修好了,CI 多了一道 Windows 门。extension/ 代码没有改动(manifest 只是版本戳),已装 1.0.12/1.0.13 扩展的人不用重装。
1. browser_emulate:不覆盖 UA 的调用以前每次必失败
扩展在无覆盖时回的是显式 userAgent: null(extension/background.js:719,userAgent: userAgent || null,而 desktop 预设的 UA 本来就是空串),插件这边的 output.schema 声明的是 userAgent: {type:'string'}。宿主的 checkValue 只对 undefined 放行(!hasOwn(value,key) || value[key] === void 0 才跳过),对 null 会下钻判类型,于是:
device:"desktop"→ 每次抛ToolOutputError- 只给
width/height的自定义视口 → 每次抛ToolOutputError device:"mobile-iphone-13"/mobile-pixel-7/tablet-ipad(预设里带真 UA)→ 正常,所以一直没暴露
用宿主自己的校验器实测这条:旧行为 ["\"value.userAgent\" must be a string"],归一化后 []。这条从 v1.0.7 坏到 v1.0.13,和之前那两条(browser_navigate 少声明 siteUnreachable、两条日志工具的数组项)是同一个缺陷家族,只是这次藏在 null 上。
改法全在插件侧(扩展不用重装):null 与空串一起剥掉,其余字段按声明类型逐条归一。另外 UA 真的被覆盖时,UA="…" 现在会写进给模型的那句话——以前它只待在进不了模型的 value 里,模型只知道视口、不知道浏览器身份被改了。
2. 审计台补了 null 的通用护栏
verify/verify_render_contract.mjs(110 → 126 项)现在对全部 11 条封闭 schema 工具递归断言 value 里不得出现任何 null——出现即判失败,不必等下一个真实载荷踩到。加上 desktop / 自定义宽高 / 带 UA / reset 四种形状各一条用例。npm run verify 311 → 327 项,exit 0。
3. Windows 进 CI 了
这个插件的实际宿主平台是 Windows(用户和开发者都在 Windows 上跑 dsh),但 CI 以前只有 ubuntu:换行符、path.join/resolve、临时目录、回环绑定这些差异一个都拦不住。新增的 windows-latest job 跑:安装 → 类型检查 → 重新构建 → 「lib/ 必须与源码同步」→ publint → 327 项实测台。首跑十步全 success(56 秒)。
配套加了 .gitattributes 把仓库锁成 LF:不换行的话那道同步门在 Windows runner 上是必然假红(本机 core.autocrlf=true 一直刷的「LF will be replaced by CRLF」是同一个根因)。加完 git status 只多一个新文件,说明索引里本来就是 LF,这次不产生 renormalize 噪声。
还没验证的部分(说实话)
- 「
browser_emulate在真 Chrome 上真的把视口/UA 切过去了」仍然只有 node:vm + 假chrome.debugger的证据;这版修的是输出契约层的必然失败,不是 CDP 行为本身。 - Windows CI 跑通不等于所有 Windows 用户机器跑通:runner 的 Node 24 / Git / 路径长度限制与你本机环境不同。
- 前几版留下的两项照旧:归组的视觉效果、AX 快照在真实页面上的表现与耗时。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.14.zip |
49209 B | 87a84dbe4d5dabfd059c1172511a4545741887f7f620d7b6f88c59cd0913a557 |
981af910f5b532cf449bcb6491f57c71 |
dsh-browser-control-plugin-v1.0.14.zip |
164151 B | 03f4974693127b814086263e99d4aab95c7d78a58f4eda33b17184a13d951ade |
3521a9167ede29ec961e54a34aebb21c |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.14、权限 debugger, tabs, tabGroups, storage, alarms。
安装
- 插件(在线):
dsh plugin --profile <你的profile> add @caob23/dsh-browser-control@1.0.14。若你的 npm 源是滞后镜像(registry.npmmirror.com),加--registry=https://registry.npmjs.org/;这个「解析与安装吃不同源」的问题我已在上游开单:dshplugin/dsh-plugin-hub#175。 - 插件(离线兜底):解压
dsh-browser-control-plugin-v1.0.14.zip后dsh plugin --profile desktop add file:<解压目录>。 - 从仓库装:
dsh plugin --profile web add "github:caob23/dsh-browser-control#v1.0.14"。 - 扩展:不用动。
- 想验证这版修好了:让 agent 跑
browser_emulate device:"desktop"——v1.0.13 及以前会报输出校验错误,这版应返回Tab N: 1280×800 …。
v1.0.13 — 失败文案把「该不该重试」交给模型
v1.0.13
一件事:错误契约里「该不该重试」那半句,以前模型一个字都看不到。 另外把「模型可见内容」这台审计铺满全部 20 条工具。extension/ 代码没有改动(manifest 只是版本戳),已经装着 1.0.12 扩展的人不用重装。
1. 失败文案现在自带决策依据
宿主把异常渲染成模型可见内容时只取一句 Error: ${message}(@deepseek-ai/dsh-tools lib/index.js:3616-3623),而 code / effectState / hint 只进 error.info 与 HTTP 面。也就是说 v1.0.9 设计「写操作超时不许盲目重试」这个信号时,唯一的接收方(模型)拿不到它,只能凭一行 browser command timed out after 800ms: click 猜该不该重试。上一版修的「render 只写摘要」是同一个缺陷,只是发生在失败路径上。
现在同一个失败,模型看到的是:
Error: [command_timeout] browser command timed out after 800ms: click — effectState=unknown:
动作可能已经落到页面上,先用只读工具看当前状态,再决定要不要重做,不要直接重试。
下一步:扩展超时未回应:先别重试。用 browser_find / browser_snapshot 看页面当前状态,判断动作是否其实已经生效。
- 写操作(
click/input/nav/tabs.open/tabs.close/eval…)超时或在途掉线 →effectState=unknown+ 明确的「先观察再重做」。 - 只读操作(
screenshot/content/find/ 日志…)任何失败 →effectState=not_started(重试安全)。 - 标签归组仍按可重复操作处理(再归一次是空操作),所以它落在
not_started一侧。 - HTTP 命令面的形状没变:新增
detail字段承载原始失败描述,toJSON().error仍是那句原话,hint不会在error里重复出现。
2. 模型可见内容审计铺满 20 条工具
新增一张地面真相表:每条工具都用 extension/background.js 真实回传的形状当载荷,同时过两关——过宿主自己的 validateJsonSchemaValue,并且模型可见文本必须命中这次调用真正要的片段([e41]、页面正文、DIALOG-GT、412×915、PDF 路径…)。截图 / PDF / browser_cleanup 会真写文件,所以 shotsDir 指到系统临时目录,跑完删掉。
这一轮没有再挖出新缺陷——剩下 11 条本来就合格。价值在广度:抽查 9 条意味着漏掉的 11 条里下一个人还会踩,而这类缺陷上一次是「活了 12 个版本没人发现」。
测试台
npm run verify 311 项断言 exit 0(v1.0.12 是 242)。CI 在 f31421e 上:类型检查、lib/ 与源码同步、publint、311 项实测台全绿;只有「文档里的 release 下载链接不得为死链」这一步在建成本页 Release 之前必然红(它检查的就是这些链接),建好 Release 后重跑为绿。
还没验证的部分(说实话)
- 新的失败文案还没在真实 dsh 宿主里跑过一次:
Error: ${message}这条渲染路径是从@deepseek-ai/dsh-tools源码实测出来的(含行号),但「模型读了这句话之后真的改变行为(先去browser_find而不是直接重试)」只能靠实机观察。 - 20 条工具的地面真相表用的是从扩展源码抄下来的载荷形状,不是真 Chrome 吐的字节。字段名与可选性以
extension/background.js当前实现为准;真页面上多出来的字段仍可能触发封闭 schema 违规——这正是browser_console_log那次的教训,所以这条限制写在这儿而不是藏起来。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.13.zip |
49210 B | 4a68e45acea1105dce2fe35d681a756d0b5d5cbcd2b14d2f91be157ffd11be8d |
eba0497dc8dc24ecf117ec4552220587 |
dsh-browser-control-plugin-v1.0.13.zip |
162759 B | 8b8f79fa36c88fa87d92e925c0b75e0ee9d679b2f7abe657d9b83eefb04fbfc1 |
90f59c6f2f6beccef2e954045ab1906f |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.13、权限 debugger, tabs, tabGroups, storage, alarms。
安装
- 插件(在线):
dsh plugin --profile <你的profile> add @caob23/dsh-browser-control@1.0.13;若你的 npm 源是滞后镜像(registry.npmmirror.com),加--registry=https://registry.npmjs.org/。 - 插件(离线兜底):解压
dsh-browser-control-plugin-v1.0.13.zip后dsh plugin --profile desktop add file:<解压目录>。 - 从仓库装:
dsh plugin --profile web add "github:caob23/dsh-browser-control#v1.0.13"。 - 扩展:不用动。要新装才下载
DSH-Browser-Control-v1.0.13.zip(解压后在chrome://extensions开发者模式加载)。 - 装完打开
http://127.0.0.1:9777/看链路状态;验证是否装到 1.0.13:故意让一次browser_click超时,返回文本里应出现effectState=unknown。
v1.0.12 — 模型可见内容契约(正文 / ref / 日志条目真的给到模型)
v1.0.12
这一版只修一件事:模型到底看到了什么。结果是四个工具自 v1.0.0 起从来没把自己要取的数据交给过模型,另外两条日志工具只要有条目就必然调用失败。无破坏性变更:命令名、帧契约、配置形状都没改,扩展代码一个字没动。
1. 根因:render() 才是模型能看到的全部内容
宿主实测(@deepseek-ai/dsh-tools):成功结果里 content = snapshotProjection(tool.output.render(args, value))(lib/index.js:3548-3551),而 canonical value 是 execution-local 的,注释明写 "deliberately omitted from durable events"(lib/types/index.d.ts:415),presentationMeta 只喂 UI。render 少写一个字段,模型就永远看不到那个字段。
以前的 browser_read 只交出一句 Title (url) — 4321 chars,browser_snapshot 只交出 12 interactive elements …; click or fill them by ref.(一个 ref 都没给,而系统提示词教的正是「先快照、再按 ref 点」),两条日志工具只给计数。数据一直躺在模型看不见的 value 里。
2. browser_read 现在是窗口式读取
正文进模型可见文本,同时加 offset / limit:单次默认与上限 20 000 字符,头部直接写 chars 0-20000 of 43210; call again with offset=20000,value 带 offset / totalChars。一次调用不会把整站灌进上下文,长页面靠 offset 走 —— 也顺手消掉了旧设计里「value 留到 120 000 字符但模型一个字也看不到」这种两头不靠的状态。
3. browser_snapshot 把清单本身给出去
[e13] button "Save" -> /profile 逐条列出(AX 模式给 role + 可访问名,DOM 模式给 tag),8 000 字符预算,超出时截断;mode:"ax" 降级回 DOM 的原因(axFallback)也一起写进文本,agent 据此知道这次的名字来自哪里。ref 用法完全照旧。
4. 两条日志工具:有条目就失败,且正文从没给过模型
browser_console_log / browser_network_log 的 items 声明是 {type:'object', additionalProperties:false, properties:{}}。宿主 checkValue(lib/index.js:468-470)对 additionalProperties:false 是逐键判违规,properties 为空 = 任何键都违规;用宿主自己的 valueSchemaSpecToJsonSchema 实测编译后这个 items 原样保留。所以 count>=1 的每一次调用都会抛 ToolOutputError —— 它活了五个版本没被发现,因为「日志正好是空的」恰好是最常测到的路径,而空数组合法。
现在按扩展真实回传的字段声明行形状,execute 归一化(console:level / text / time / location;network:method / url / status / resourceType / bytes / time / failed / errorText 等),render 逐条列出,时间统一 UTC 秒,count < total 时明说「只给了最新的 N 条」。headers / responseHeaders / postData 不再进结果:它们到不了模型,只会把载荷变大。
5. 截断信号改成机器可读(+ 三处漏网的硬切片)
超出预算时,切片之后追加一行稳定标记:
… [renderedTruncated omittedChars=8036 limitChars=4000] narrow the query and call again to see the rest
标记在切片之后拼上,所以它必然出现在模型看到的文本里;数字实测对齐。看到它的正确反应是缩小查询(limit / pattern / urlPattern / offset),不是假设页面就这么长。同时补掉 v1.0.11 那次清理漏掉的三处 JSON.stringify(value).slice(0, 200|600|1200)(browser_wait / browser_find / browser_dialog)—— 它们此前仍会把 JSON 从 token 中间切断。
6. 扩展不用重装
extension/ 的代码一行没改(manifest.json 只是版本戳跟着走),协议仍是 protocol: 1,老扩展配新插件照常工作。要更新的只有插件侧:dsh plugin add @caob23/dsh-browser-control@1.0.12,或下面那份离线插件包。
还没验证的部分(说实话)
- 这批改动第一次让真实数据进模型,所以「模型拿到页面正文 / ref 清单 / 日志条目」这件事本身还没有在真实 dsh 宿主 + 真实 Chrome 里跑过端到端。242 项断言是在真桥 + 假扩展上跑的,载荷形状取自
extension/background.js的真实返回。 - 20 000 字符的读取窗口、8 000 / 6 000 的列表预算是我按 token 成本定的经验值,没有实测过 DeepSeek 侧的实际表现;如果一次读下来上下文吃紧,把
limit调小即可。 - 上一版仍未验证的两项照旧:归组的视觉效果、AX 快照在真实页面上的表现与耗时。
测试台
新增 verify/verify_render_contract.mjs:真 BridgeServer + 假扩展 + 宿主自己的 validateJsonSchemaValue / valueSchemaSpecToJsonSchema,把每条工具的真实返回值同时喂给它的输出 schema 和它的 render,逐条问「schema 过不过」「模型可见文本里有没有这次调用真正要的数据」「超预算时 [renderedTruncated] 的数字对不对」。这一层以前没有任何测试覆盖,所以两类缺陷分别活了 12 个(v1.0.0–v1.0.11)与 5 个(v1.0.7–v1.0.11)已发布版本。npm run verify:191 → 242 项断言,exit 0;CI run#16、run#17 十步全 success(顺带把 CI 里匿名调 GitHub API 的死链审计改成带令牌 —— 那条红是配额 403,不是死链)。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.12.zip |
49210 B | 64255a1e82616b4b03b63fefd299029fe39e1713efee12e55d24c37dffa39d5d |
628400eccf3f26e415ef3ed2be52e483 |
dsh-browser-control-plugin-v1.0.12.zip |
161066 B | cf3f8103c7c21a0fae9c09bfeb1fbc69d5006529d38a62b57553b08a27368a64 |
f76a5f28228a91591fbd966d6d7589f5 |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.12、权限 debugger, tabs, tabGroups, storage, alarms。
安装
- 插件(在线):
dsh plugin add @caob23/dsh-browser-control@1.0.12;若你的 npm 源是滞后镜像(registry.npmmirror.com),加--registry=https://registry.npmjs.org/。 - 插件(离线兜底):解压
dsh-browser-control-plugin-v1.0.12.zip后dsh plugin --profile desktop add file:<解压目录>。 - 从仓库装:
dsh plugin --profile web add "github:caob23/dsh-browser-control#v1.0.12"。 - 扩展:已经装着 v1.0.11 的不用动;新装才需要
DSH-Browser-Control-v1.0.12.zip(解压后在chrome://extensions开发者模式加载)。
v1.0.11
v1.0.11
四个方向一起进:agent 开的标签看得见、快照换成语义定位、导航不再假报成功、装机元数据修好。外加合了三个社区 PR。无破坏性变更:命令名、帧契约、配置形状都没改。
1. agent 新建的标签自动归组
Agent 用 browser_tabs open 打开的标签会收进当前窗口的 DSH 蓝色分组,一眼能分辨哪些页面是机器开的。
browser_tabs新增group/ungroup:ungroup带windowId只拆那个窗口,不带则所有窗口都拆;只解除归属,不关任何标签页。open支持group: false跳过归组;group能把早就存在的散标签拉进来。- 三条实测出来的约束:刚
tabs.create立刻tabs.group会跟 Chrome 的标签簿记打架,所以 50/150/400ms 退避、最多 4 枪封顶;groupId只在内存里,service worker 重启或用户手动拆组后按标题反查重建,绝不往已失效的 id 上塞标签;归组失败绝不让「打开标签」失败,只在结果里附grouped:false与原因。 - manifest 加
tabGroups(Chrome ≥ 89 具名权限,不读网页内容)。没有加host_permissions:「控制期间地址栏显示『正在被调试』」这条边界保持原样。
2. browser_snapshot 的 AX 语义模式
mode:"ax" 走 Chrome 的 Accessibility.getFullAXTree,role 与 name 是浏览器算好的可访问名(aria-label、<label>、alt、可见文本合成),比让 agent 猜 CSS 选择器抗改版。ref 契约一点没变:AX 节点经 DOM.resolveNode + Runtime.callFunctionOn 找到真实元素后写的还是同一个 data-dsh-ref,browser_click / browser_type 的 ref 用法照旧,默认仍是 DOM 模式。四条硬规则都有断言:只收可操作角色白名单(GenericContainer/StaticText/InlineTextBox 一律滤掉,否则 limit 会被文本节点吃光);隐形元素不占 ref 号也不写属性;AX 报了节点而 DOM 已没有 → 记 ax.unresolved;取树之前先清旧 ref。AX 域拿不到时降级回 DOM 并回报 axFallback,不让整条命令失败。
3. 导航不再假报成功(含一个发布版里的线上缺陷)
- 等待器改在触发加载之前挂上,并坚持看到新文档:此前一次还没开始的导航会 0ms 返回、把上一个文档的 URL 当成功报出去(实测)。
- 两个 CDP 探针都上了预算:导航未 commit 时 CDP 不答话,此前
browser_navigate会挂死远超自己的timeoutMs(实测 >8s,作者实测 40s)。 output.schema是封闭的却没声明siteUnreachable,扩展发的又是对象{reason}—— 拿宿主自己的校验器实测:"value.siteUnreachable" is not a declared property (additionalProperties: false),也就是说装 1.0.10 的人导航到不可达站点时,模型收到的是输出校验错误而不是「站点不可达」。现在字段声明齐了、{reason}归一成siteUnreachable:true+ 原因,loadPending/pendingUrl也能到模型眼前。- 再补一刀:上面那版仍会把「导航压根没发生」(链接变成下载、目标被 Chrome 拒载)在 1.2 秒宽限后报成成功;现在宽限期后比对落到的 URL vs 要去的那个(忽略 hash 与末尾斜杠)。两个不误报方向都测了:导航到已经开着的同一地址仍算成功,真重定向走不到这条分支。
4. 装机元数据
- peer 范围两个读者语义相反(宿主门禁拿宿主版本 +
includePrerelease;pnpm 拿包的版本列表、不带),此前>=0.1.0-rc.1 <0.3.0-0这种写法过得了门禁却只能解析到 0.1.0-rc.8 那代。现在0.2.0-rc.2 || >=0.1.0-rc.1 <0.3.0-0:精确支当锚点、宽支保兼容。 - 删掉
engines.node(官方 dsh 插件一个都没写,宿主门禁也不读它)。 @deepseek-ai/schemastery按官方约定放回dependencies: ~3.18.4;JsonValue改本地声明(0.2 的dsh-tools不再导出它)。
5. 合并的社区改动
#7 CDP 会话失效缓存(DevTools 抢会话后能重连)、#10 模型可见内容不再被 300/400 定长切片砍断、#12 导航诚实性。谢谢 @BeiWay1145 与 @long-arm-ape。#9、#11 还在评审中(理由写在 PR 里)。
还没验证的部分(说实话)
- 归组的视觉效果(颜色、标题、折叠态)与 AX 快照在真实页面上的表现/耗时 都只在
node:vm+ 有状态假chrome.*上测过,没进真实 Chrome 复验。装上新扩展后如果发现分组不出现,先确认chrome://extensions里版本号是 1.0.11、popup 里权限含tabGroups。 - CI 全绿是本机之外的第二双眼睛,不等于真实浏览器验证。
测试台
npm run verify 191 项断言 exit 0:新增 verify_tab_groups(23)、verify_extension_e2e(真桥 + 真扩展代码 + 真 WebSocket,10)、verify_peers(34)、verify_cdp_retry(7)、verify_snapshot_ax(18)、verify_nav_report(PR #12 带来)、verify_nav_landing(10)。CI 的「重新构建后 lib/ 必须与提交一致」这道门也在 runner 上真跑过了。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.11.zip |
49210 B | 8fe9c27434d0c6dfc66045791bc63f6ebd241bca4fe31b9f6e273e9c0ca8cd01 |
551fb18bfeb7d5021a80f8e2397dd030 |
dsh-browser-control-plugin-v1.0.11.zip |
155170 B | c02215e4ad8fc97ca672a7dc72121e519e8e0cc18b7292703acf6ee102bc7f00 |
7214b90a57934ddd1d1e2b89fd602d7b |
两份都验过:文件头 504b0304、条目全用正斜杠、CRC 全过;插件包含 cordis.patch.yml(dsh.bundle.patch 指向它),扩展 manifest 版本 1.0.11、权限 debugger, tabs, tabGroups, storage, alarms。
安装
- 扩展:下载
DSH-Browser-Control-v1.0.11.zip,解压 →chrome://extensions→ 开发者模式 → 先移除旧版再「加载未打包的扩展程序」。这次改了extension/,旧扩展必须重装。 - 插件:
dsh plugin --profile <你的profile> add @caob23/dsh-browser-control@1.0.11。若你的 npm 源是镜像(国内默认常见registry.npmmirror.com),新版可能还没同步过去,会报ERR_PNPM_NO_MATCHING_VERSION——那就这一次显式走官方源:加--registry=https://registry.npmjs.org/,或直接用上面的离线插件包add file:<解压目录>。 - 装完打开
http://127.0.0.1:9777/看链路状态(扩展图标应为绿色 ON)。
v1.0.10
v1.0.10
两件事:agent 开的标签页从此看得见,以及插件市场装不上时所暴露的插件侧元数据病根修掉了(#13)。无破坏性变更——命令名、帧格式、配置形状都没改,老扩展、老配置照常工作。
1. agent 新建的标签自动归组
Agent 用 browser_tabs open 打开的标签页,现在会收进当前窗口的一个 DSH 蓝色标签分组,你扫一眼就知道哪些页面是机器在开、哪些是自己开的,事后也能一把拆掉。
browser_tabs新增group/ungroup两个动作:ungroup带windowId只拆那一个窗口,不带就所有窗口都拆;只解除归属,不关任何标签页。open加group: false可以让新标签留在组外;group能把早就存在的散标签拉进来。- 三条实测出来的硬约束:刚
tabs.create立刻tabs.group会跟 Chrome 自己的标签簿记打架,所以按 50/150/400ms 退避、最多 4 枪封顶;groupId只存内存,service worker 重启或用户手动拆组后按标题DSH反查重建,绝不往已失效的 id 上塞标签;归组失败绝不让「打开标签」这件事失败,只在结果里附grouped:false与原因。 extension/manifest.json增加tabGroups(Chrome ≥ 89 的具名权限,不涉及读取网页内容)。没有顺手加host_permissions——「控制期间地址栏显示『正在被调试』、所有操作实时可见」这条边界保持原样。
node:vm + 一台有状态的假 chrome.tabs/chrome.tabGroups 上。建组、复用、退避、脏缓存、拆组、无权限降级这些行为是有凭据的,颜色和分组条长什么样要装上新扩展才看得见。
2. 安装元数据修复(issue #13 的插件侧部分)
同一条 peerDependencies 范围其实有两个读法完全相反的读者:
| 读者 | 拿什么去匹配 | 带 includePrerelease 吗 |
|---|---|---|
宿主兼容门禁 @deepseek-ai/dsh-app-boot(lib/index.js:294-300) |
宿主版本号 | 带;不过就 throw,整个插件 bundle 被跳过 |
| pnpm / npm | 包自己的已发布版本列表 | 不带 |
而 @deepseek-ai/dsh-* 这一族没有任何稳定版(latest 停在 0.0.1-rc.1)。v1.0.8 写的 ">=0.1.0-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.3.0-0" 在门禁那一关全过,但包管理器最大只能解析到 0.1.0-rc.8(实测:npm view "@deepseek-ai/dsh-settings@<该范围>" version 只吐 0.1.0-rc.*)。后果是仓库自己的锁文件把宿主 API 钉在半年前那代,历次 typecheck 校的都是旧类型;pnpm 默认 auto-install-peers=true,干净目录安装会真拉一整棵 0.1.0 时代的依赖树。
反过来只钉精确版 "0.2.0-rc.2" 会死在另一头:dsh 0.1.7 / 0.2.1 宿主被门禁判不兼容。所以最终形态是两条并列:
"@deepseek-ai/dsh-tools": "0.2.0-rc.2 || >=0.1.0-rc.1 <0.3.0-0"
精确支给包管理器当锚点(实测 pnpm why 现在拿到 0.2.0-rc.2 / cordis 4.0.4),宽支让四代宿主都过门禁。另外:@deepseek-ai/schemastery 按官方插件的约定放回 dependencies: ~3.18.4;删掉了 engines.node(宿主门禁根本不读它,官方四个插件一个都没写);JsonValue 改为本地声明——钉回真宿主版本后 tsc 才第一次报出 dsh-tools@0.2 不再导出这个类型。
没说谎的部分:这一条不等于 #13 已经修好。旧范围并不会让 pnpm 报错(它能解析到 0.1.0-rc.8,本机安装已发布的 1.0.8 实测 exit 0),而报告标题里那句 “Cause: plugin-side install failure” 是 dsh-plugin-hub classifyFailure() 对任何没匹配上的失败的兜底标签;dsh plugin add 又是继承 stdio 跑的 pnpm,pnpm.log 里并不会包含 pnpm 自己的输出。定案还需要报告者的日志与他的 registry 配置。
3. 测试台又补了一层
verify_extension_e2e.mjs:真的BridgeServer+ 真的extension/background.js+ 真的 WebSocket(只有chrome.*是假的)。断言hello的protocol被认下、HTTP 命令面的tabs.open/group/list/ungroup确实落到扩展的COMMANDS表并原样回来、扩展抛的错变 502 +command_failed+effectState。verify_peers.mjs:直接用宿主同款 semver 把上面两张表都跑一遍(34 项)。注入旧的宽范围报 6 项 FAIL,注入纯精确钉版报 9 项 FAIL——这个台就是拦下上面那次回退的。verify_surface.mjs开始对账llms.txt里的工具数量(它写着 16,实际 20,同一类漂移的第三个藏身处)。npm run verify共 144 项断言,exit 0;CI 里“重新构建后lib/与提交不一致就 fail”那道门在 runner 上真跑过了。
资产校验和
不附 .sha256 文件,直接列在这里:
| 文件 | 大小 | SHA-256 | MD5 |
|---|---|---|---|
DSH-Browser-Control-v1.0.10.zip |
44705 B | 8cc17010fa73af7a83d8669f905c114950904ff3c4b588e39ead550362ae4037 |
4b4cc2c63d3cd80d788ebac509f99871 |
dsh-browser-control-plugin-v1.0.10.zip |
143139 B | 3beba6a8e7dc226444539c6cc8b6288d9e4c8274ff73b7fcfe245720e09fb83b |
8472119c91df827b1a2b9489b64d136d |
两份都验过:文件头 504b0304(PK\x03\x04,资源管理器可直接展开)、条目全用正斜杠、CRC 全过;插件包内含 cordis.patch.yml(package.json 的 dsh.bundle.patch 就指向它)。
安装
- Chrome 扩展:下载
DSH-Browser-Control-v1.0.10.zip,解压后在chrome://extensions开开发者模式→「加载已解压的扩展程序」。已经装过旧版的请重新加载一次,本次改了extension/。 - dsh 插件:
dsh plugin add @caob23/dsh-browser-control(npm 同步发布 1.0.10)。 - 装完打开
http://127.0.0.1:9777/看链路状态;扩展图标应为绿色 ON。