npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-zen-browser

v0.3.0

Published

DeepSeek Harness plugin that drives Zen Browser (Firefox) over the native WebDriver BiDi endpoint: sessions, navigation, screenshots, page text.

Readme

dsh-zen-browser

DeepSeek Harness 插件:用浏览器自带的 WebDriver BiDi 驱动 Zen Browser(Firefox 内核)。

不需要 geckodriver、不需要 CDP、不需要任何扩展——zen-browser --remote-debugging-port <port> 会直接开一个 BiDi 端点,插件连上去就能导航、读正文、截图、产生可信输入。

安装

dsh plugin --profile web add dsh-zen-browser     # 从 npm 装;或:dsh plugin --profile web add /path/to/dsh-zen-browser
dsh restart                                      # 插件在宿主启动时加载

包内除了插件本体,还带 scripts/(不进 PATH,按需手动装):

P="$(dirname "$(readlink -f ~/.dsh/profiles/web/node_modules/dsh-zen-browser)")/scripts"
"$P/install-zen-agent.sh"        # zen-agent → ~/.local/bin + 应用菜单项
"$P/install-daily-entry.sh"      # 可选:把日常入口也变成带端口的入口(见下文)

安装后本插件会自带 zen-browser 技能;默认 lazyTools: true,也就是技能被调用后才把 zen_* 工具放开。

在对话里试:

/zen-browser 打开 example.com,把正文读给我,再截个图

工具

| 工具 | action | 说明 | | --- | --- | --- | | zen_session | start / stop / list / attach | start 拉起一个会话;attach 连到你自己带 --remote-debugging-port 启动的 Zen(不新起进程,stop 也不会关闭它) | | zen_page | navigate | 打开 URL 并等待 complete / interactive / none | | zen_inspect | observe / text / html / screenshot / pdf / console / network | observe 产出 @eN 元素引用;screenshot 能内联时直接作为图片回给模型;console/network 是带游标的只读诊断 | | zen_interact | click / hover / fill / type / press / select / scroll-to / wheel / focus / blur | 与页面交互。点击/键入/按键/滚轮走原生输入(isTrusted=true);select/focus/blur/scroll-to 走脚本,结果里显式标注 trusted=false | | zen_tabs | list / create / select / close / containers | 标签页与容器(Zen 的容器/工作区) | | zen_assist | resize / emulate / request-help | 改窗口形态,或把登录/验证码这类步骤交给人类 | | zen_ext | status / install / diagnose / downloads / bookmarks / history / containers / tabs | 伴生扩展桥:补 BiDi 没有的浏览器级能力 |

@eN 元素引用

zen_inspect({action:"observe"}) 会给可交互元素分配 @e1、@e2… 的引用(DOM + ARIA 启发式:角色、可访问名、值、选项、禁用态,外加 cursor:pointer 的普通元素)。 引用在导航后失效,再次 observe 也会整体替换——这时 zen_interact 会返回明确的“引用已失效,请重新 observe”。

配置

装好后编辑 profile 的 cordis.patch.yml(web profile 默认在 ~/.dsh/profiles/web/cordis.patch.yml):

- id: zenbrowser
  config:
    profileMode: daily
    zenPath: zen-browser
    headless: false
    maxSessions: 3
    lazyTools: true

| 选项 | 默认 | 说明 | | --- | --- | --- | | zenPath | zen-browser | Zen 启动器路径(PATH 上的名字或绝对路径) | | profileMode | daily | daily=用你日常的 profile(登录态立即可用);agent=插件专用 profile | | dailyProfilePath | 空 | 显式指定日常 profile 目录;空则从 profiles.ini 解析 | | agentProfileDir | $XDG_DATA_HOME/dsh-zen-browser/profiles | 插件专用 profile 的根目录 | | headless | false | 无窗口启动(那样就没法人工登录了) | | port | 空 | 固定端口;空则每次会话取一个空闲端口 | | launchTimeoutMs | 30000 | 等待浏览器打印 BiDi 端点的时间 | | defaultTimeoutMs | 30000 | BiDi 命令默认超时 | | maxSessions | 3 | 本插件并发会话上限 | | maxTextChars | 40000 | text / html / observe 的截断上限 | | screenshotOrigin | document | 截图范围:document(整页,视口测不出时也能用)或 viewport(仅可见区),另一个会自动回退 | | lazyTools | true | 技能被调用后才注册 zen_* 工具 | | extraArgs | [] | 追加到 Zen 命令行的额外参数 |

关于 profile(重要)

Firefox/Zen 的 profile 是协作式独占的:lock 符号链接指向持有它的进程,第二个实例会被拒绝启动。

所以 profileMode: daily 有个前提:你自己的 Zen 必须先关掉。插件永远不会去结束一个不是它启动的浏览器—— 这种情况下 zen_session start 会返回:

{ "status": "profile-locked", "runningPid": 12345, "profile": "…" }

此时让模型把情况告诉你,你关掉 Zen 再重试;或者改用 mode: "agent"(插件专用 profile,可以和你的日常浏览器同时运行, 代价是每个站点要各自登录一次)。

为什么不直接删掉锁文件强行双开?实测过:两个实例共用一个 profile 时,第二个实例的日志会出现 PlacesUtils.sys.mjs … db is undefined 与 UnknownError —— 它打不开 places.sqlite(书签/历史)。 详见仓库里的 计划书.md §5.4。

安全边界

--remote-debugging-port 起开的端口没有鉴权,只绑定 127.0.0.1:本机上任何进程连上去都能控制这个浏览器、读走 Cookie。

  • 插件自己拉起的会话用每次随机挑的空闲端口,只随那个实例存活;
  • 如果你装了「日常入口」(见上一节),端口就会随你的日常浏览器常驻——那是一个显式的取舍,撤销脚本也写在那一节;
  • profileMode: daily 时那个实例里装着你的真实身份,因此 agent 默认只操作自己创建的标签,不去动你已有的窗口。

开发

node test/plugin-wiring.mjs      # 假 ctx 跑插件接线(技能注册、lazyTools 门控、schema 合法性、所有权边界)
node test/smoke.mjs              # 脱离 DSH 的驱动冒烟:拉起 Zen → 导航 → 读正文 → 截图 → 按 pid 回收
node test/smoke.mjs --headed     # 带窗口跑
node test/smoke.mjs --profile ~/.config/zen/<你的profile>   # 用指定 profile(会先检查锁)

冒烟测试永远只操作它自己启动的那个子进程,并按 pid 结束它。

让 agent 接管你现在用的浏览器(zen-agent)

Firefox 的 Remote Agent 只能在启动那一刻开:命令行 --remote-debugging-port 有效,而写进 user.js 的 remote.enabled / remote.port 一个端口都不开(2026-09-20 用临时 profile 实测,见 计划书.md §15)。 所以「需要时直接控制」的唯一办法是:让日常那个入口带上参数。

./scripts/install-zen-agent.sh     # 命令 → ~/.local/bin/zen-agent;外加一个「Zen Browser (可被 DSH 接管)」菜单项
./scripts/install-daily-entry.sh   # 把「日常 Zen 入口」也指到 zen-agent(用户级 zen.desktop 覆盖)

install-daily-entry.sh 会在 ~/.local/share/applications/zen.desktop 放一份同名用户级覆盖 (同名即优先于 /usr/share/applications/zen.desktop),只把 Exec 换成 zen-agent,图标 / MimeType / 动作 / 多语言名字全部保留。 于是应用菜单、xdg-open 点链接、默认浏览器全都走带端口的入口。撤销:install-daily-entry.sh --uninstall。

想让 Hyprland 快捷键也走同一个入口,注意 uwsm app -- 继承的是会话 PATH(不含 ~/.local/bin),要写绝对路径:

-- ~/.config/hypr/config/variables.lua
BROWSER = "/home/jianzian/.local/bin/zen-agent"

(hyprctl reload 或重新登录后生效。)

之后怎么用

  1. 照常从菜单 / 快捷键 / 点链接打开 Zen —— 它本身就是带端口的日常浏览器;
  2. 让 agent 执行 zen_session({ action: "attach" }) —— 不用给 port,插件会读启动器写的 ~/.local/state/dsh-zen-browser/attach.json;
  3. 浏览器已经在跑时再点链接 / 再按快捷键:zen-agent 会把请求交接给那个实例(实测 0.16s 返回、 不会产生第二个实例,URL 真的落在已运行的窗口里);如果那个实例没带端口,会弹一次桌面通知提醒你。

随时可以问它现在能不能被接管:

zen-agent --status
# profile    : /home/jianzian/.config/zen/wq8es50q.Default (release)
# running pid: 215452
# state port : 60713  pid: 215452
# attachable : yes(zen_session action=attach 可直接接管,不用给 port)

这个实例用的是你日常的 profile:登录态、书签、扩展全在;agent 拿到的是可信输入(真实事件,不是合成事件)。

安全代价(要接受才把它当日常入口):调试端口没有鉴权、只绑 127.0.0.1,但本机任何进程连上就能控制这个浏览器、 读走所有已登录 Cookie;当日常入口 = 这个暴露面一直在,而且界面上没有任何提示。端口随该浏览器进程存活,关掉浏览器即消失。 不接受就撤销:install-daily-entry.sh --uninstall,并把 BROWSER 改回 zen-browser。

zen-agent 支持 --profile <dir>(显式指定时不做交接,锁被占就直接报错退出)/ --port <n>(固定端口,被占用时明确报错而不是静默无端口)/ --status / --dry-run。

多进程共用一条连接(zen-call)

Firefox 的 Remote Agent 一个浏览器只允许一个 BiDi 会话:第二个连接会拿到 Session already started。 所以插件连着的时候,你自己的脚本、另一个 DSH 实例都连不上——除非先把插件的会话 stop 掉。

插件的做法是把连接复用出去:它在 loopback 上开一个小桥(src/rpc.js),把 zen_* 工具面暴露给本机其它进程, 端口与 token 写进 $XDG_STATE_HOME/dsh-zen-browser/rpc.json(0600);scripts/zen-call 是配套客户端:

./scripts/zen-call status                              # 看桥和当前会话
./scripts/zen-call zen_session '{"action":"attach"}'    # reused:true —— 复用插件那条连接
./scripts/zen-call zen_tabs '{"action":"list"}'
./scripts/zen-call zen_page '{"action":"navigate","url":"https://example.com"}'
./scripts/zen-call zen_inspect '{"action":"text","maxChars":500}'
  • 不会开第二条 BiDi 会话:attach 直接返回插件已有的会话(reused: true),实测 zen_session list 始终只有 1 个会话;
  • 走的是同一份工具实现,结果与对话里调用完全一致(需要内联图片的能力——比如截图——会退化成「返回文件路径」);
  • 鉴权:请求带 x-zen-token 头或 ?token=,不对就 403;
  • 关掉:cordis.patch.yml 里 rpcBridge: false(想固定端口用 rpcPort)。

安全边界:桥只绑 127.0.0.1,但它把「控制这个浏览器」的入口开放给本机任意进程(能读 rpc.json 就能拿到 token)。 和常驻端口属于同一类取舍,区别是这里有 token、而且可以随时关掉。

伴生扩展(zen_ext)

插件自带一个 MV2 扩展,用 zen_ext({action:"install"}) 通过 BiDi 自动安装(未签名也能装,不需要手动 about:debugging)。 扩展通过 loopback 的 HTTP + SSE 桥与插件通信,补上 BiDi 完全没有的能力:下载记录、书签、历史、具名容器。

但它控制不了当前页面,也给不了可信输入——实测(Zen 1.22.1b,有头无头一致):扩展派发的 click 是 isTrusted: false (Firefox 没有 chrome.debugger 等价物),运行时安装的临时扩展还拿不到初始标签页枚举, tabs.query 任何过滤条件都返回 0,只有装完之后新建的标签页可见;省略 tabId 走活动标签页又被 activeTab 权限挡住(需要用户手势)。 想控制当前页,要么把扩展签名后永久安装(AMO unlisted),要么给它加个工具栏按钮让用户点一下授权。

已知限制

  • zen_assist 的 resize 依赖 browser.setWindowBounds,Firefox 的 BiDi 支持不稳定(实测同一台机器上时好时坏);不可用时返回 status: "unsupported" 而不是报错。
  • emulate(browsingContext.setViewport)在有头窗口上可能挂住不返回(实测),因此只给 6 秒超时并在失败时提示改用截图。
  • Zen 不支持通过 BiDi 把标签切到前台:browsingContext.activate 抛 selectedBrowser is null,前台建标签还抛 TabManager.getBrowserForTab(...) is null。所以 zen_tabs create 建出来的是后台标签(点一下标签栏里的它才看得到), zen_tabs select 只切「本插件的目标标签」,不会把浏览器可见标签切到前面。
  • Zen 不支持通过 BiDi 建窗口(openWindow() not supported in Zen):zen_tabs create type=window 直接给出可读报错。
  • 没有前端可视化面板(P4)。
  • 伴生扩展桥能补浏览器级数据,但控制不了当前页面(见上一节的实测),webRequest 全量抓包仍未接。

测试

node test/plugin-wiring.mjs      # 假 ctx:技能注册、lazyTools 门控、schema、所有权边界
node test/smoke.mjs              # 驱动冒烟:拉起 Zen → 导航 → 读正文 → 截图 → 按 pid 回收
node test/interact-exec.mjs      # observe/@eN + 可信点击/填写/按键/悬停/滚动 + 失效引用契约
node test/p2b-exec.mjs           # 标签页、PDF 导出、console/network 游标、assist
node test/zen-agent-exec.mjs     # 启动器 e2e:zen-agent 拉起 → 不传 port 直接 attach → stop 后浏览器仍活着
bash test/zen-agent-branches.sh  # 启动器分支:start / handoff / attachable 判定 / 固定端口占用 / dry-run 无副作用
node test/json-output.mjs        # 工具输出必须是 lossless JSON(undefined / NaN 会被严格调用方判成失败)
node test/rpc-exec.mjs           # 连接复用:外部进程经工具桥复用同一条 BiDi 连接(reused / 403 / 未知工具)