Back to home@qgx1992

dsh-notify

DSH 通知显示层插件:全局 toast 栈,订阅桌面壳 __dshExo 桥事件,点击经官方 sessions runtime 程序化激活会话;无壳时降级订阅 sessions store 自绘。可插拔通知显示层的 web 侧。

Stars
0
Language
JavaScript
Created
Aug 28, 2026
Updated
Aug 28, 2026
GitHub repo

Introduction

dsh-notify

DSH 通知显示层插件(可插拔通知显示层的 web 侧) 全局右上角 toast 栈,订阅桌面壳 __dshExo 桥事件(对话完成 / 服务事件 / 更新就绪),点击经官方 dsh-client-runtimesessions store 程序化激活会话(替代壳侧 __reactFiber$ DOM hack);无壳时自动降级订阅 sessions store 自绘轮次完成 toast。

面向 DSH-Exoskeleton 设计文档 docs/NOTIFICATION-PLUGIN-DESIGN.md §6 实现。壳核分离不破坏:检测逻辑(zstd watcher / 服务健康 / 更新)留在主进程,本插件只负责显示与交互


1. 解决什么问题

用户问题本插件对应的解法
通知不弹 / 漏报桌面壳把事件经 webview 预加载桥(__dshExo)投递给页面内插件 → auto 决策「webview 在线优先」,dev/portable 无 System Toast 时通知仍然在页面内可见
点击跳错会话 / 跳不过去插件在页面内直接用官方 @deepseek-ai/dsh-client-runtime/clientsessions store 程序化选中/打开会话(会话 ID 精确),不再从壳外猜 DOM、不依赖 __reactFiber$
每轮对话都通知,太吵显示层策略化:聚合窗口、每轮/聚合粒度由壳侧 hub 决定后下发(本插件只渲染);无壳降级时同会话连发原位刷新而非堆积,避免覆盖层通知风暴

2. 架构(两种数据源,运行时互斥)

┌─ 壳桥模式(默认,覆盖式首选)────────────────────────────────────┐
│  DSH-Exoskeleton 主进程 notification-hub(壳侧,P1)              │
│   └─ webviewProvider(P2:WebContentsView 预加载桥 __dshExo)      │
│        │ onEvent(ev)                              send()           │
│        ▼                                                ▲          │
│  ┌─ dsh web 页面 ─────────────────────────────────────────────┐   │
│  │  dsh-notify 插件(本包)                                   │   │
│  │   · 检测到 window.__dshExo → ready() 握手 → 订阅 onEvent    │   │
│  │   · 渲染 toast 栈(按 kind 分级)                          │   │
│  │   · 点击 → ctx.sessions.open(id) 程序化激活                 │   │
│  │          → __dshExo.send('notify:click', …)                │   │
│  └────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘

┌─ 无壳降级(浏览器直开 dsh web,无 __dshExo)───────────────────┐
│  dsh-notify 插件                                                │
│   · 订阅 ctx.sessions.list,检测 completed 0→1 边缘(= 侧边栏    │
│     绿色 "done":运行中→完成且未选中)                            │
│   · 自绘「对话完成」toast;点击 → ctx.sessions.open(id)          │
│   · 页面内闭环,无 OS 通知                                        │
└────────────────────────────────────────────────────────────────┘
  • 事件是"事实",显示是"策略":本插件只渲染壳/列表喂给它的完成事件,聚合/渠道等策略由壳侧 hub 决策。
  • 桥与降级互斥(有桥绝不再开 sessions 降级),天然无双通道重复。

3. 桥契约(window.__dshExo

壳侧 P2 已落地(src/preload/dsh-view.ts + window-manager.attachDshView 挂 preload + notification-hub webview 通道),本契约即当前实现,供壳侧后续维护对齐。

插件只消费最小白名单 API,不依赖任何任意 IPC:

// 壳侧 → 页面(预加载桥 window.__dshExo,经 contextBridge 暴露)
interface DshExoBridge {
  /** 订阅壳推送的通知事件,返回取消函数 */
  onEvent(cb: (ev: NotificationEvent) => void): () => void
  /** 页面 → 壳:点击 / 已读回执 */
  send(channel: 'notify:click' | 'notify:seen', payload: { id: string; sessionId?: string }): void
  /** 页面 → 壳:握手(插件就绪后才投递,防事件丢失) */
  ready(): void
  /** 壳版本探针(仅用于日志) */
  appInfo(): { version: string }
}

// 通知事件(与设计 §3.1 一致)
type NotificationEventKind =
  | 'session-done' | 'service-ready' | 'service-error' | 'service-restarting' | 'update-ready'

interface NotificationEvent {
  id: string
  kind: NotificationEventKind
  title: string
  body: string
  ts: number
  session?: { sessionDir: string; workspace: string; uuid: string; file: string; turn?: number; project?: string; sessionTitle?: string; firstUserText?: string }
  service?: { port?: number; error?: string; restartCount?: number }
  update?: { version?: string }
  actions?: { onClick?: () => void }   // 原生 provider 专用;webview 通道序列化后无回调
}

桥侧交互时序(壳实现参考):

壳 webviewProvider 建桥(preload:sandbox+contextIsolation 暴露 __dshExo)
  → 等页面 __dshExo.ready() 握手
  → 握手后「webview 在线」,auto 决策投向 webview provider
  → view.webContents.send('dsh-notify:event', ev) → preload 转发给 onEvent 订阅者
  → 收到 notify:click → windowManager.show()(会话激活由插件侧 ctx.sessions.open 完成)
  → 收到 notify:seen → hub 记录已读回执(R-26)
未握手不投递(防漏报);握手丢失 → native 兜底。

4. 安装

从包源(npm / GitHub,推荐 —— 任意机器可装)

# npm 源(发布后)
dsh plugin --profile web add dsh-notify
# GitHub 源
dsh plugin --profile web add github:qgx1992/dsh-notify

桌面壳内也可用「插件」面板安装:目录会按 dsh-plugin topic / npm keywords 自动发现,一键安装。

已知坑(AGENT.md §7.1):新发布 <24h 时 pnpm 供应链策略可能报 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION,把该版本加进 profile 的 minimumReleaseAgeExclude 白名单即可。

本地开发装法(改源码调试用)

# 因项目路径含空格+中文,link: 直连会在 dsh→pnpm 转发链里被拆坏;
# 用无空格 junction 指向真实目录(改动实时生效,路径移动后需重建 junction)
New-Item -ItemType Junction -Path "$env:USERPROFILE\dsh-notify-link" `
  -Target "D:/A-my project/研究agent桌面端/DSH-Exoskeleton/plugins/dsh-notify"
dsh plugin --profile web add link:C:/Users/QIU/dsh-notify-link

装完后:dsh web 重启生效(客户端 bundle 在启动时组合,仅刷页面不生效), 页面刷新即加载 /plugins/dsh-notify/client.js


5. 行为细节

  • toast 栈:右上角,全局。按 kind 分级:对话完成=信息、服务就绪=成功、服务异常=错误、服务重启=警告、更新就绪=信息(左侧竖色条 + DSW 主题变量,自动适配亮/暗色)。
  • 自动关闭:信息 8s / 成功 6s / 错误与更新 15s / 警告 12s;× 手动关闭;同屏上限 5 条(超出淘汰最旧)。
  • 对话完成 toast项目「X」· 标题(壳下发 body 原样渲染;无壳降级由插件按 cwd 项目名 + displayTitle 组装)。点击 → ctx.sessions.open(sessionId) 程序化激活 + notify:click 回传。
  • 激活可靠性与防御open() 前先校验会话 id 是否在列表(unknown id 会 fail loud,避免炸进插件);列表瞬态(phase=pending)下每 500ms 重试至多 4 次;再点可按需复用 byId 精确 id。
  • 已读回执:事件渲染即 notify:seen(壳侧 hub 用以上报投递成功/去重)。
  • i18n:独立 locale NS dsh-notify(zh/en)。data 前缀data-dsh-notify-*
  • DOM 纪律:toast 栈是插件自建的 fixed 覆盖容器,绝不搬动 DSH slot 渲染出的节点(搬节点 ↔ 框架重渲染互相触发会导致渲染进程 100% CPU 卡死,见 AGENT.md 已知坑);卸载插件(fiber dispose)整体拆除样式 + 根容器 + 全部 toast。
  • 静默降级:所有异常 try/catch,不向壳/页面抛。

6. 设计验证结论(对应设计文档 §11「待验证清单」)

#待验证项结论(本插件取证)
1dsh-client-runtimesessions store 是否暴露程序化选中/打开会话的 API存在ISessions.open(id: SessionId): void("Select a session as current"),实现 SessionRuntime.openctx.sessions 可见(inject "sessions" 后)。§6.3 首选路径可行
2页面是否容忍注入 __dshExo 全局插件侧只消费,不注入;__dshExo 命名冲突检测由壳侧 preload 负责(R-27,页面无同名全局时才注入)
4WebContentsView + sandbox + preload + contextBridge 兼容性壳侧 P2 阶段验证;本插件按标准 contextBridge 契约实现,无特殊前提
覆盖式 toast 渲染安全性✅ 与生态先例(dsh-pet)同型:自建 position: fixed 容器 + <style>,不搬 slot 节点

无壳降级的"轮次完成"信号:SessionSummary.completed(侧边栏绿色 done 标记, SessionManager 在 running→idle 且未选中时 arming)。勘察结果见 @deepseek-ai/dsh-client-runtime/lib/types/client/contract/sessions.d.tssessions/service.d.ts


7. 与 DSH-Exoskeleton 壳侧协作(P0–P2 已落地,P3/P4 约定)

壳侧已完成(本次同步交付,npm run typecheck / npm test(113 项)通过):

  • P0 ✅:AUMID 对齐(index.ts setAppUserModelId('io.dsh.exoskeleton'),与 electron-builder.yml appId 一致)。
  • P1 ✅src/main/notification-hub.tsdispatch / 聚合策略 / Provider 路由 / 投递回执日志 R-26),原 notify.ts 降为 native 内部实现。
  • P2 ✅src/preload/dsh-view.ts 预加载桥(window.__dshExo 白名单,R-27)+ window-manager.attachDshView 挂 preload + hub webview 通道(在线判定 = view attach && 收到 ready() 握手)+ 三处事件源(session-watcher / index 服务事件 / updater)改走 hub。

约定:webview 通道下,壳收到 notify:click 只需 windowManager.show(),会话激活由插件 ctx.sessions.open() 完成 —— 这是替换 activateSessionInWebUi DOM hack 的关键收益;壳侧 activateSessionInWebUi 保留为 native 通道 / 页面异常时的兜底(actions.onClick 携带)。

P3 / P4(未做,按需后续):把 dsh-notify 加入 RECOMMENDED_PLUGINS 默认预置/推荐;管理面板「设置」页渠道自检 + 通知渠道/粒度/聚合窗口 UI(现状:notifyChannel 默认 auto 已生效,粒度默认 per-turn 保持现状零回归,聚合窗口默认 20000ms,均可在 config.json 手动调整)。


8. 目录结构

plugins/dsh-notify/
├── package.json          # dsh.client.platform=web;dsh.client.inject 留空(不静态注入源码上下文)
├── cordis.patch.yml      # bundle 挂载声明(insert 进 DSH 配置树)
├── lib/
│   ├── index.js          # 宿主(node 侧)入口:纯浏览器插件,无宿主行为
│   └── client.js         # 浏览器 bundle:toast 栈 + 桥订阅 + 程序化激活 + 无壳降级 + i18n
├── README.md             # 本文档
└── LICENSE               # MIT

9. 开发 / 发布

  • lib/client.js 后:node --check lib/client.js 语法校验 → node test/smoke.cjs 冒烟测试(在无浏览器环境用最小 DOM shim 真实执行 apply(),覆盖:bundle 导出形状 / 无壳降级 completed 边缘弹 toast / 程序化激活 / 更新就绪 notify:install / 同事件去重 / 卸载拆除)→ 本地 link: 重装 → 重启 dsh web 实测。
  • 当前默认本地分发:不推 GitHub、不 npm publish。版本语义化(改代码后 bump),README/description 维护好即可。
  • 平台标记:dsh.client.platform = "web"exports['./client'] 供模块系统组合 /plugins/dsh-notify/client.js