## 版本与变更记录 - **v0.14.0**:**收起成一条 = 暂停画面流**(用户 2026-09-14 的省资源项)。 原先 `setMin()` 只切 CSS 类,画面流照旧在跑(仍在拉 JPEG 帧、仍在设 `img.src`,解码也照做), 只有整块面板被 ✕ 收走才关流。现在加 `applyStreamState()`:`want = S.open && !S.min`, 收起就断流、展开立即恢复;因为宿主 `/bl/stream` 是"无查看者零开销",断开即真的停掉截图 (改在 client 侧即可,不动 host、不用重启)。健康判定新增 `paused`:收起态是**灰实心灯** + 「已暂停(收起中)」,不会被误报成黄灯"正在重连"。实测:收起 4.5 秒后帧字节一字未变, 展开后帧字节变化、灯回绿。回归 `verify-panel-health` 21 ⇒ **30 项**。 - **v0.13.0**:**画面健康 —— 绿灯只代表"画面在更新",断流亮黄灯**(用户 2026-09-14 反馈)。 原先收到画面就亮绿灯,而断流时**状态不更新**:`es.onerror` 是纯注释、`pollState` 的校灯只在 `if (!S.es)` 分支里跑 ⇒ 流对象还在时那个绿点永远不会被纠正,用户会把最后一帧当成实时画面。 现在把"浏览器在运行"(宿主 `/bl/state.alive`)与"画面在更新"(SSE 最近一帧时刻,阈值 3.5s) 分成两个事实:判定是纯函数 `computeHealth()`,绿灯**只由帧处理器点亮**, 黄灯时顶部写「正在重连」(从没收到过帧则写「还没收到画面」)、画面中上盖 「⏸ 画面已停 · 最后更新于 X 秒前」角标、收起态标题行也带「· ⏸ 画面已停 Ns」。 回归:新增 `tools/verify-panel-health.mjs`(**21 项**:纯函数八种组合 + 两种文案 + 9 条接线断言, 含"没有退回 `classList.toggle('on', !!st.alive)` 旧口径"),离线套件 12 ⇒ **13 套**。 - **v0.12.0**:**把"尺度"从写死的像素改成百分比,并清掉剩下的写死项** —— ①**观察窗面板**:宽/高/宽屏宽改成视口百分比(`panelWidthPct` 等,设置页可调、即时生效)。 旧写法 `clamp(300px→560px)` 看着像百分比,但**上限 560px 是硬上限** —— 窗口再宽也不变大, 宽屏上反而相对变小,用起来就是"固定尺寸";高度更是完全由截图宽高比决定,窄而高的窗口会顶穿视口。 现在舞台 `flex:1 + min-height:0`、图片 `contain`(**照搬独立页 `/bl/view` 已验证的写法**,两处一致), 实测 1418×775 视口下面板 597×405 = **42.1%×52.2%**(597px 已超过旧的 560px 上限)。 ②**浏览器窗口尺寸**支持百分比(`windowSize:"80%,85%"` = 屏幕工作区比例):启动后按页面里的 `screen.avail*` 换算并用 `Browser.setWindowBounds` 调整(量不到屏幕就不动,不猜)—— 换显示器/换机器不用改设置。③**其余写死项**:搜索冷却与总预算、面板百分比都进了设置; 设置导出/导入改为**直接复用插件本体的 DEFAULT_SETTINGS**(原来是手抄一份 + 注释约束,迟早漂移)。 ④顺手修一个真 bug:`sanitizeSettings` 从默认值出发,导致 PUT 里的**非法值会把该字段静默重置成默认** (例如 PUT `windowSize:"八百块"` 会冲掉用户设好的 80%)—— 现在 PUT 以**当前设置**为底,非法值只是被忽略。 README 新增「配置:哪些可配/是百分比,哪些刻意固定」一节,把"该配的都配上、写死的给出理由"写清楚。 - **v0.11.0**:**把搜索从"菜单"改回"机制",并把"写镜像"这一类 bug 修到根上** —— ①`browser_search` 不再定死引擎:`SEARCH_ENGINES` 降级为**预设**,新增 `engineSpec`(调用时自带 `{url:'…%s', item, link, title, text}`,任何搜索页/站内搜索都能接,**免改代码、免配置、免重启**, 有端到端测试)与 `settings.search={engines,order}` 长期配置;报错也改成教你怎么自带引擎。 ②`viewOf()` 是单向拷贝,导致所有 `browser.xxx = v` 都是写空气 —— 全库排查出的受害面包括 `browser_tabs{action:'select'}` 与观察窗点标签页(**报 ok 但页面没换**)、拟人鼠标"笔尖"与视口尺寸 每次调用被重置;现在这些字段是**写透访问器**,该类错误语法上不可能再犯。 ③空结果分诊补上第四态 `filtered-out`(命中 item 但字段选择器不匹配),并修掉"页面偏短就先判 没加载完"的优先级 bug(命中了条目本身就证明加载完了);这同时**更正**了 v0.10.1 里一句我写了却 没实现的声明。 ④`browser_read` 不再被徽章淹没:图片 URL >120 字符只留 alt 并记账(`imagesOmitted`)。 - **v0.10.1**:**把 v0.10.0 拉到真网站上跑,两个修复** —— ①「**新开标签页后当前页还在旧页**」这个既有 bug(`browser_open {newTab}`/`browser_tabs new` 都中招; `browser_search` 因此在旧页上抽取并误报"页面还没加载完"):`Target.createTarget` 异步生效 + `refreshTabs` 会把"列表里找不到的 selected"重置回 `tabs[0]` + `browser.selected` 只是会话字段的镜像, 三件事叠加;统一改成 `openTabAndSelect()`(等它真的出现在列表里再切),并有回归测试钉住。 ②「**百度那条链从来就没成功过**」:标题链接是 `baidu.com/link?url=<加密串>`,被"自家内链不算结果" 的守卫(要求以 `/link` **结尾**)全部误杀 —— 8 条结果 0 条返回,还报成"引擎改版"; 现在跳转链保留并标 `viaEngineRedirect`,摘要选择器按实测补上 `[class*=summary]`。 两条教训:**夹具要照抄实测到的 DOM,不能照抄我的推测**(第一版百度夹具是猜的,于是测试全绿、 线上 0 条);选择器表 `SEARCH_ENGINES` 改为 export 供测试直接引用,防止"测试里另抄一份"。 - **v0.10.0**:**「读得懂、抓得到、搜得了」+ 把调用方案做成技能** —— 从 7 个同类 MIT 插件(生态索引里 找的真实等价物;用户给的那 4 个 URL 六条路径全 404)蒸馏出 `browser_read`(正文四级降级 + 噪音剥离 + Markdown + **段落感知截断**)、`browser_scrape`(`item`+`fields` 结构化抓取)、`browser_search` (真实浏览器里搜 Bing/百度/DDG,跳转链解包 + 三态空结果判定 + 引擎冷却),**零新增依赖**; 同时修三处观察/动作层短板:**ref 跨快照稳定**(原来每次快照重排编号,"上次的 ref=7" 在页面重排后 会点到**另一个元素**)、快照内联状态并做**交互恢复**(React 的 onClick 是事件委托,DOM 上没有 onclick 属性,只按选择器收元素会漏掉一大片真按钮)、点/输入前做**可操作性检查**(含 `elementFromPoint` 遮挡检测 —— 参考实现两家都没有)与输入后**回读校验**;`browser_snapshot` 里内联人机验证识别 (Cloudflare/hCaptcha/reCAPTCHA/Turnstile/文本兜底),命中就停下问人。 调用方案落成 `skills/browser-automation/SKILL.md`(DSH 不会自动发现插件包里的 skills/, 由 `index.js` 调 `skills.register` 注册 + 免重启重扫路由)。新增 96 条真浏览器断言; 另修两处与本功能无关、但本机一直红的测试环境假设(改动前 HEAD 同样失败,已用 worktree 取信)。 - **v0.8.3**:**「不动你正在看的页面」+「Chrome 起不来不是提权,是安全软件拦了 GPU 沙箱」** —— ① 接管你的浏览器时,"当前页未授权、目标站已授权"不再**就地导航**(那会把你正在读的页面换掉), 改成 **`chrome.tabs.create` 新开一个标签页**、附加到新标签页,你原来的页面一个字都不动; 扩展把真实 `tabId` 报回 host,host 据此纠正映射(否则列表显示"旧标签页已附加"、操作却落在新标签页上)。 ② **更正 v0.8.2 的错误归因**:Chrome 静默退出不是"DSH 以管理员运行"—— 本机用计划任务 (干净父进程、中完整性、脱离 DSH 作业对象)复测照样起不来,而加 `--in-process-gpu` 就正常; 真凶是**火绒 HipsDaemon 拦掉 Chrome GPU 进程的沙箱初始化**(Edge 因微软签名被放行)。 启动链因此改成"先常规试 → 失败补试 `--in-process-gpu` → 成功就记进 `state.json` 下次直接用", 并刻意**不**用 `--no-sandbox`(渲染器沙箱要留着),`verify-launcher` 有断言钉死这条底线。 - **v0.8.2**:**「Chrome 起不来 = 整个插件不可用」+「中文用户名下浏览器根本拉不起来」** —— 启动链上两个只在真实机器暴露的坑。① VBS 独立启动器原来用 UTF-8 写、wscript 按 ANSI 读, 中文用户名(`C:\Users\陈道云\…`)被读成乱码路径 → 浏览器一个进程都不起,对外只报「未响应 CDP 端口」, 还在 `C:\Users` 下留下乱码目录;现在写 **UTF-16LE + BOM**。 ② 首选浏览器起不来时不再全盘失败:`findChromiumExes()` 返回候选列表 (`chromePath` → 环境变量 → Chrome → Edge → Brave)逐个尝试并**自动降级**。 这条是实测逼出来的:**DSH Desktop 以管理员身份运行时,Chrome 会因无法在提权父进程下初始化沙箱 而静默退出(加 `--no-sandbox` 才起,但默认不关沙箱),Edge 不受影响** —— 也就是说"装了个 Chrome" 反而可能让插件彻底不可用。同时:端口改成跳过"已有别的 CDP 在听"的(避免附加到错误的浏览器), 每个浏览器各自一个 profile 目录(`chrome-profile-chrome` / `-msedge` / …)。 新增回归测试 `tools/verify-launcher.mjs`(10 条)。 - **v0.8.1**:**「我允许了目标站点,你却还在报未授权」** —— 用户浏览器档的判权用的是标签页**当前** URL, 于是"允许了 github.com、前台却开着别的页"这种最常见用法必然被闸死。 现在 `browser_open {use:"edge", url}` / `browser_navigate {url}` 会把目标 URL 传进 `Target.attachToTarget` 的 `intendedUrl`,扩展在**当前页未授权、目标页已授权**时**先导航、再附加**(最多等 5s 落在已授权 origin)。 隐私底线不变:attach 永远在导航之后,agent 拿不到未授权页面的调试器。- **v0.8.0**:**装扩展不再靠“看文档”** —— 新增 `browser_ext_setup`,并把“桥没开”和“扩展没装”分开报。 - 背景:接管你的浏览器要三件事同时成立(桥开着 / 扩展装着 / token 粘对),而 v0.7.x 只把它们写成文字说明: `use:"edge"` 在**桥没开**时也报“扩展还没连接”,把人引去装一个装了也连不上的扩展; 报错不给 `extension/` 绝对路径、不说 token 在哪;面板在桥没开时显示空 token,步骤却照旧摆着。 - 新工具 `browser_ext_setup`:开桥(`userBridge=true`,即时生效)→ token 进剪贴板 → 打开目标浏览器的扩展页 → 资源管理器里打开本包 `extension/` 目录,并把 5 步操作原样列出。 `kind:'edge'|'chrome'`(默认 `settings.userDefault`),`open:false` 只返回路径与 token、不动 UI。 - `browser_open {use:"edge"}` 的失败信息分两种情况:**桥没开** → 指向 `browser_ext_setup`; **扩展没装** → 给出扩展页地址 + `extension/` 绝对路径 + `bridge.json` 位置(token 在那)。 - 面板:桥没开时出现 **「① 启用用户浏览器桥」** 按钮(一键 `PUT {userBridge:true}` 并刷新状态), 安装步骤加序号,并注明“也可以直接交给 agent:调 `browser_ext_setup`”。 - `/bl/bridge` 的 hint 带上 `extension/` 绝对路径;`browser_open` 在“一台都没接入”时提示该调谁。 - 边界(没变):「加载已解压缩的扩展程序」是**原生文件选择框,无法自动化** —— 那一下永远得人点; 本版做的是把“点哪儿、选哪个目录、粘什么”全部摆到眼前,而不是绕过它。- **v0.7.1(2026-09-11 修)**:真圆的方圆角豁免。DSH 主题自带全局规则 `*,:before,:after{corner-shape:var(--dsw-corner-shape)}`,而默认值是 `superellipse(1.5)`; 新版 Chromium 支持 `corner-shape` 后,凡 `border-radius:50%` 的元素都会被画成方圆块 (用户报的是底图工坊的宝珠,本插件的 `.bl-fab` hover 底衬、`.bl-dot`、 独立网页 `/bl/view` 里的 `.dot` 同理)。宿主自己的圆形控件都写了 `corner-shape:round` 豁免, 本插件已照做(`client.js` 三处 + `index.js` 的 `/bl/view` 一处)。旧内核自动忽略该属性。 注意 `index.js` 是宿主侧文件:改完要重启桌面端,`/bl/view` 页面才会换新。 - v0.7.0:**Chrome / Edge 同时可控,每次调用指定用哪台(`use`)** - 需求:日常两个浏览器(Chrome 153 / Edge 152)各有登录态,agent 要能操作 **Edge 里已登录的页面**, 而且要能在同一个会话里来回切,不必断开重连。 - 桥(`bridge.js`):单个连接槽(`this.sock/this.ext`)→ `conns: Map`, **每个 kind 一条连接、各自的 pending/seq/心跳**(同 kind 重复连接保留最新,旧的以 `4002 superseded` 关掉)。 回包按 `sessionId` 的 `:` 前缀找连接再配对,找不到就忽略(不抛)。 `kindOfBrowser` 现在**纯 kind 与完整 UA 两种形状都认**(原先只认 UA,"opera" 这种族名会静默落进 unknown)。 - 扩展(`extension/`,v0.3.0):装载时按 UA 嗅探身份(`Edg/`→edge…)并在 hello 里上报; **所有跨 WS 边界的 sessionId 一律带 `:` 前缀**(出口加、入口剥,内部仍用裸 id); 弹窗新增身份行 + **「允许当前所有标签页」**(批量并入 origin,不打开 allowAll)+「撤销全部授权」。 - host(`index.js`):新增**每浏览器一份会话**(tabs / selected / cdp 分开存), `browser` 单例退化为"当前活跃会话的视图",于是 19 个工具里上百处 `browser.xxx` 读法一行没改; 每个工具注入 `use` 参数(`plugin`/`chrome`/`edge`/`user`/`auto`,缺省**沿用上一次**); `/bl/state` 与 `/bl/bridge` 新增 `browsers[]`,观察窗红标带上浏览器名;`settings.userDefault` 决定 `use:"user"` 指哪台。 - 隐私/安全边界不变:仍是逐站点授权 + 「允许操作」开关 + Chrome/Edge 自带且无法隐藏的调试横幅。 多浏览器只扩大"能连几台",不放松任何一层。 - 行为修正(都是这次改造暴露出来的真问题): · `browser_close` 之后 `browser_open` 接不回来(外层守卫抢在 `browser_open` 自己的 `useSession/launch` 之前判死); · 没写 `use` 时若"上一次那台"已掉线,会一路报错 → 现在退回插件实例并记录一行提示; 显式写了 `use:"edge"` 而它掉线时**仍然报错**(明示意图不该被静默改写); · `use` 取值校验用错了判定(`isUserKind` 把 `firefox` 当成"未接入的浏览器")→ 新增 `isKnownKind`; · `onBridgeStatus` 的兜底分支曾把完整 UA 当 kind 塞进 Set(子代理 review 抓到)→ 改为读 `browsers[].kind`; · `backendMode='user'` 时桥从 0→N 接入会把活跃会话切到你的浏览器(恢复 v0.6 的档位语义)。 - 验证:新增 `tools/verify-browsers.mjs`(**35 项**)——真 `index.js` + 真桥 + 真扩展, **两台"浏览器"各跑在一个 Worker 里**(独立 realm = 独立 `globalThis`),断言"发给 Edge 的命令只打在 Edge 的标签页上""两台各自一份调试器会话""同 kind 重连只占一个槽""掉线不影响另一台"等串台类故障。 另有 `tools/verify-bridge-v2.mjs`(74 项)、`tools/verify-extension-v2.mjs`(146 项)。 五个套件合计 **23 + 74 + 66 + 146 + 59 + 35 = 403 项全绿**(v1 那两个套件也一并保留通过)。 - 记一笔踩过的坑(替身层面,但东西是真的):在**同一个 realm** 里加载两份扩展副本时, 模块级自由变量 `chrome` 指向同一个 `globalThis`,靠"副本开头重新赋值"隔离不了 —— 表现是 Chrome 扩展读到 Edge 的标签页,断言全绿但全是假的。两个 Worker 才是与"两个浏览器进程"同构的替身。 - 提醒:改 `index.js` / `client.js` 后要**重启 DSH Desktop** 才加载新代码与新的设置页控件。 - v0.6.2:**把"免登录页走自带实例、授权后才切你的浏览器"这条默认路做成看得见的一档** - 背景:v0.6.1 已经有 `backendMode='auto'`,但设置页只有「自带实例(推荐)/ 我的浏览器」两档, 文案也没写清"免登录页 vs 要登录的页"各走谁 —— 于是很容易被设成 `user`, 之后**每一个免登录页面都撞逐站点授权**(表现为"连 example.com 都开不了")。 - 设置页三档,标签直接写明用途:**免登录用自带实例(推荐,=auto)/ 只用自带实例(=plugin)/ 只用我的浏览器(=user)**; 下面那行说明按档位分别讲清"免登录页去哪、要登录的页怎么办"。 - `browser_open` 的返回值新增 `mode`(当前档位)与 `hint`(下一步该 `use:"plugin"` 还是 `use:"user"`), 描述里也写明:`auto`/`plugin` 免授权、可 `newTab`;`user` 档逐站点授权、默认只读, 新开标签页自扩展 v0.3.2 起也支持(弹窗可控,仅 http/https)。 - `PUT /bl/settings.json` 改 `backendMode` **当场换后端**(原来要等下一次工具调用才切); 桥/扩展还没连上时切 `user` 只存设置并回 `backendWarn`,等扩展接入后由 `onBridgeStatus` 自愈。 - `tools/verify-host.mjs` 跟着修:**原来的用例假设"桥一连上就切 user"(v0.6.0 旧行为),在 `auto` 档下必然红**; 改成先显式设 `backendMode:'user'` 再验用户浏览器那条链路,并新增两条用例 (`auto` 档下桥连着也不抢后端、`PUT` 切档当场生效)。47 项全绿。 - 提醒:改 `index.js` / `client.js` 后要**重启 DSH Desktop** 才加载新代码与新的设置页按钮(工具描述在插件装载时定型)。 - v0.6.4:**能在插件实例里打开 DSH 自己的 GUI(`browser_open {gui:true}`)** - 症状:在插件自带实例(独立 profile)里访问 `http://127.0.0.1:<端口>/` 只会得到 `dsh web authentication required; reopen the URL printed by dsh web.`。 - 机制(`@deepseek-ai/dsh-client-connection` 的 `authorizeIndex`):GUI 根路径只认两样东西 —— ① URL 查询串里的**一次性 launch token**(`?token=…`,仅 `GET /`)会 303 落地并种一个**按 Host 绑定的签名 cookie**; ② 之后靠那个 cookie。插件 Chrome 是干净 profile,两样都没有 → 必然 401。 token 由进程生成、只在"DSH 启动时打印的 URL"里下发,凭据库(`.credentials.yaml`)只存 cookie 签名密钥,不存 URL。 - 改法:**不绕过门禁**,改用宿主自己的接口 —— 与 `dsh-web-app` 打印 dsh web URL 用的是同一个: `ctx.inject(['connection'], c => c.connection.authenticatedUrl('http://127.0.0.1:'))`。 - 启动时取一次并缓存(拿不到只警告,不影响任何既有功能); - 新增路由 `POST /bl/gui` `{need:'gui-url'}` → `{ok,url}`;可带 `base` 换成 LAN 地址(同一套 token 规则重签); - `browser_open {gui:true}` 用它打开 GUI; - 取向:**只认 POST + JSON**(裸 GET 会被 `/