跳到主要内容

dsh-browser-notify

已验证

dsh-browser-notify · v0.1.5 · MIT · Web 界面

DSH 浏览器通知插件:agent 回合结束、向你提问或请求授权时,用浏览器系统通知提醒你(仅当页面不在前台时)

安装

dsh plugin add dsh-browser-notify

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

发布到 npm 但没有公开仓库。安装前请检查包内容。

标签

说明文档

dsh-browser-notify

把 DeepSeek Harness Web GUI 的「需要你处理」变成浏览器系统通知的 dsh 插件。

当 agent 结束回合、向你提问或请求工具授权时,如果你没有盯着那个标签页,就完全无法感知——dsh-browser-notify 在后台利用浏览器的 Notification API 弹出系统通知,点一下通知即可回到 GUI。

触发场景

事件 通知内容 默认
user-questions/request(agent 提问) 问题文本(截断 110 字)+ 前三个选项 + 题数 ✅
approval/request(工具授权) 工具名 + 授权原因 ✅
api-session/status(回合结束 → 等待输入) “回合已完成,等待你的输入 · 项目 / 会话” ✅

细节:

  • 只在页面不在前台时通知——document.hidden 为真(切走了标签页/最小化)、窗口失去焦点(你切到了别的应用,浏览器窗口还在后面)、或事件属于你没在看的那个对话(你在同页面的另一个会话里)时才算「没在看」,这三种情况都会弹。
  • 提问/授权优先:同一会话刚通知过提问/授权,紧接着的「回合结束」会静默,避免重复轰炸。
  • 提问重提醒:提问通知如果你正看着别的会话、或窗口不在前台,每 20 秒重弹一次(问题未答且 5 分钟内有效)——弹窗几秒就消失也不怕漏;你回到那个会话后自动停止。
  • 回合结束带项目/会话:正文会追加 项目:<cwd 基名> · 会话:<会话标题或 id 前 8 位>,一眼知道是哪个工程的哪个对话跑完了。
  • 静默窗口:同一会话、同一类型的通知默认 15 秒内只弹一次。
  • 重连不漏:api-session/status 只推送状态变化,页面刷新/重连时正在跑的回合收不到「开始」边沿;插件会从会话列表补齐运行状态,回合结束时照样通知。
  • 水瀑事件不拦路:user-questions/request 与 approval/request 是 waterfall,插件只负责通知,随后原样 next(),问题卡片照常渲染。
  • 通知的 tag 按 类型+会话 分组,浏览器会替换仍在展示的同组通知;点击通知会跳到对应的那个会话(走 ctx.uiWorkspace.openSession,ui-workspace 才是 DSH 客户端导航的归属方)。

dsh-desktop(Electron 外壳)里的跳转

在系统浏览器里,点通知由浏览器自己把窗口/标签页抬到前面,插件再切到对应会话,所以「跳转」看起来是完整的。

在 dsh-desktop 里(Electron,页面跑在 dsh-app://app/)只有「切到对应会话」这一步生效:Electron 对 renderer 通知的点击只把事件派发回页面(shell/browser/notifications/platform_notification_service.cc 的 NotificationClick 仅做 DispatchNonPersistentClickEvent),并不会激活窗口,而 renderer 里的 window.focus() 也抬不动原生窗口(WebContents::ActivateContents 只通知观察者)。所以窗口隐藏在托盘、或压在别的窗口后面时,点通知不会把 dsh-desktop 窗口叫到前台——请用托盘/Dock/任务栏把它叫出来;插件已经把该会话切好,窗口一出来就是那个对话。要让点击通知本身就把窗口抬到前台,得由 DSH Desktop 主进程在处理通知点击时 show() + focus() 它自己的窗口。

安装

dsh plugin --profile <你的 profile> add dsh-browser-notify

重启 dsh web 后即生效。

通知权限必须由一次点击触发

浏览器普遍要求用户手势才弹通知权限框:Firefox ≥ 72 会直接静默拒绝没有手势的 Notification.requestPermission();Chromium 系(Chrome / Edge)的「防滥用通知」策略同样经常不弹、只留一个地址栏图标。旧写法只在「该发通知的那一刻」申请权限,那时没有任何手势,权限就永远停在 default,于是什么通知都不会出现。所以插件会在页面加载后的第一次点击 / 按键时申请权限,允许后稍等片刻会弹一条「通知已开启」确认。

如果当时点了「阻止」,或错过了那次询问,可以点地址栏左侧的权限图标手动改成「允许」,之后无需重启页面即生效。

配置

Host 半声明了 schemastery Config,可用 profile 的 cordis.patch.yml 覆盖(profile 默认 patchReload: live,改完即时生效):

# ~/.dsh/profiles/<profile>/cordis.patch.yml
- config:
    id: dsh-browser-notify
    $data:
      question: true      # agent 提问时通知
      approval: true      # 工具请求授权时通知
      roundEnd: true      # 回合结束等待输入时通知
      onlyWhenHidden: true # 仅当页面不在前台(隐藏标签页/窗口失焦/在别的对话)时通知
      quietMs: 15000      # 同一会话同类通知的静默窗口(毫秒)

不配置时全部使用默认值;浏览器半会从 GET /plugins/dsh-browser-notify/api/config 读取解析后的配置(拿不到就用内置默认值,功能不中断)。

实现

分 host / client 两半:

  • Host 半(src/host.js,cordis 插件):声明 Config 并向宿主 webserver 注册 GET /plugins/dsh-browser-notify/api/config。
  • Client 半(src/client.js,纯 JS,无 React):经 dsh.client.inject 注入 @deepseek-ai/dsh-api-gateway,挂上 ctx.remote,订阅转发过来的 Remote 事件(user-questions/request、approval/request、api-session/status),按配置弹浏览器通知。
浏览器 (client bundle)
  ├─ remote.$on('user-questions/request' | 'approval/request' | 'api-session/status')
  │    └─ 标签页不可见 / 在看别的会话 → new Notification(...)
  │         └─ onclick → ctx.get('uiWorkspace').openSession(sessionId)
  ├─ localStorage['dsh.sessions.current']  ← 读「当前在看哪个会话」(ui-workspace 写入)
  └─ fetch /plugins/dsh-browser-notify/api/config
       └─ host plugin (ctx.webServer)  → 解析后的 Config

客户端契约上有两处容易踩空,插件都按 DSH 真实形状读取:

  • 导航在 uiWorkspace:ctx.sessions(session-controller)没有 open(),它的注释明确写着导航属于 view owner;ctx.get('uiWorkspace').openSession(id) 才是跳转入口(旧版 sessions.open() 仍作为兜底尝试)。
  • 当前会话不在会话列表里:SessionListState 只有 ids / byId / phase 等,没有 current;「正在看哪个对话」由 ui-workspace 以 { sessionId } 持久化在 localStorage['dsh.sessions.current'],插件优先读快照里可能出现的 current,读不到再读这个键。

数据面完全在浏览器内(通知不经过 host),host 只是配置的声明与出口,遵循 plugin-dashboard / model-select-plus 的 webserver+fetch 模式。

开发

pnpm install          # workspace 安装
pnpm run build        # esbuild 打包 src/client.js → lib/client.cjs,拷贝 host 到 lib/
pnpm run smoke        # 用 fake 驱动 host 配置路由与 client 事件→通知主流程

lib/ 为构建产物、已被 .gitignore 忽略;npm publish 会在 prepublishOnly 阶段自动构建。

License

MIT