dsh
OpenPetsHQ/openpets将 DeepSeek Harness 的生命周期状态、错误与审批请求,桥接到本地运行的 OpenPets 桌面伙伴。
会话轮次结束时推送浏览器系统通知与页内提示,内置 5 种语言模板与自定义预设。
简体中文 · English · 繁體中文 · 日本語 · 한국어
DSH(DeepSeek Harness)会话完成提醒插件 —— 每一轮结束,让完成状态主动找你,而不是你盯着屏幕等。
每轮对话结束时,把「已完成 / 出错 / 被阻塞 / 达到上限」连同用时、token 消耗写入会话日志,并推送浏览器系统通知与页内 toast(窗口失焦 / 聚焦可分别指定通道,两路都含「不通知」);AI 向你提问、请求审批时同样立即弹窗提醒,不必守着会话页面。内置 5 种语言、4 套风格预设(颜文字 / 艾露猫 / 猫娘 / DeepSeek 娘)、可视化文案模板编辑器、自定义预设库,缓存命中率与生成速度取自官方投影,与状态栏同口径。
| 通道 | 形式 | 说明 |
|---|---|---|
| 会话内系统消息 | 可折叠提示行 | 每轮结束把结束原因与用时、消耗作为插件来源的系统消息追加进会话日志,随 JSONL 落盘,恢复或回放会话后依然可见。 |
| 浏览器系统通知 | Web Notification | 原生弹窗。每次完成事件使用独立 tag(dsh-session-notify:<timestamp>),不与前一次互相替换,也不被折叠成一个分组条目;点击通知聚焦回窗口。 |
| 页内 toast | 右下角浮动弹窗 | 永远展示的保底通道:系统通知被平台静默、权限拒绝或环境不支持时仍有可见反馈。同屏最多 3 条(超出移除最旧),10 秒自动消失,点击关闭。 |
[!NOTE] 后两条通道按窗口聚焦状态分流:设置面板中「失焦时」「聚焦时」各有一个下拉,可选
双通道/仅系统通知/仅页内提示/不通知。判定失焦的条件是document.visibilityState === 'hidden'或!document.hasFocus(),也就是标签页被切走、窗口被最小化或点到别处时,走「失焦时」那一路。
session-complete-notify),推送正文跨会话一致,不依赖你恰好开着那个窗口。running 位,true → false 边沿即触发推送,与官方 sidebar 提醒同策略(首次观测只记录基线,已在 idle 的会话不补发)。ask_user_question 向你提问时,宿主立刻把「提问标题 + 正文」写入独立投影单元(key = session-complete-notify-question),客户端实时轮询并弹窗提醒——即使你正看着别的页面,也不会错过提问。{question} 占位符(注入 AI 的实际提问),媒体开关 {image} / {icon} / {audio} 同样生效。approval/asked)立刻提醒,approval/decided 后失效——切到别的标签页也不会漏掉审批。pendingInteractions(宿主提供时最准)→ 宿主审批投影(key = session-complete-notify-approval,标题与正文由宿主按当前语言渲染)→ 会话列表快照的 pendingInteraction === 'approval'。localStorage 持久化),支持自动编号的未命名预设(未命名、未命名 2…)、「来自:xxx · 已修改」来源指示、删除预设。{title} 引用会话标题。tokenUsage 投影:缓存读 /(未缓存输入 + 缓存读 + 缓存写)。sessionStats 投影:输出 token ÷ 解码耗时。[!NOTE] 缓存命中率与速度只在自定义模板中通过
{cache}、{tps}占位符插入时才显示。使用内置默认文案时,正文不含用时与消耗(要显示数据需在自定义模板中插入对应占位符)。
user/message)与自身监听目标(turn/*)不相交。dsh-llm 的 createUserMessage 契约手工构造;纯逻辑层(lib/core.js)零依赖,可独立测试。ctx.effect() 中并返回 clearTimeout disposer,注册随 fiber 卸载自动撤销,HMR 热重载安全。dsh.bundle manifest,dsh plugin add 一条命令装完即用,无需手写 patch。npm run selftest 跑 scripts/selftest.mjs——宿主 schema 与 sanitize、客户端纯逻辑与接线、设置面板渲染冒烟、提示音播放链路、README × 5 一致性,全部为可执行断言,发布前必跑。| 依赖 | 要求 |
|---|---|
| DSH(DeepSeek Harness) | 0.1.x 与 0.2.0-rc.2 起(当前实测 0.2.0-rc.2)。Web profile 部署;官方 base bundle 默认包含 @deepseek-ai/dsh-settings 与会话投影,两代设置契约由插件自动适配,无需配置 |
| cordis | >=4.0.0-rc <5(peer dependency,由宿主提供) |
| Node.js | >=22(宿主侧) |
| 浏览器 | 支持 Web Notification 则有系统通知;不支持、权限拒绝或被静默时由 toast 兜底 |
两代宿主的设置契约不同,插件自动适配,无需任何配置:
| 环境 | 设置命名空间 | 设置面板入口 |
|---|---|---|
| dsh 0.1.x | 插件自选:session-complete-notify(ctx.settings.register) | 宿主 settingsScope → settings.plugin.item 槽位 |
| dsh 0.2.0-rc.2 起 | profile entry id:dsh-session-notify | 宿主 configForms → plugins.bundle.config / plugins.row.config / settings.plugins.tab 槽位 |
0.1.x 的代码路径保持不变,但最近一轮改动只在 0.2.0-rc.2 上做过端到端回归;旧宿主上
schemastery< 3.18.3 时自动跳过volatile()(0.1.x 不需要它)。
[!WARNING] 裸
npm install只会把包装进依赖树,不会注册插件 —— 这是 DSH 官方设计(npm install only adds the dependency; it does not register the plugin)。自动挂载的唯一官方途径是dsh plugin add:它读取包内dsh.bundlemanifest(本插件自 0.1.3 起声明,指向仓库根cordis.patch.yml)并自动应用。
安装包的同时自动应用 cordis.patch.yml,把插件挂载进 profile 装配(host 事件订阅 + client 启动图注入)。
dsh plugin --profile web add @telosmaylx/dsh-session-notify
dsh plugin add github:TelosmaYLX/dsh-session-notify
也可以在 DSH Web GUI 会话内执行:
dev_install_package github=TelosmaYLX/dsh-session-notify
把路径换成你的克隆目录,在 DSH Web GUI 会话内执行:
dev_install_package dir=/你的/克隆目录/dsh-session-notify
先打包:
npm pack @telosmaylx/dsh-session-notify
解压后指定目录安装(在 DSH Web GUI 会话内执行):
dev_install_package dir=/解压/目录/package
在 ~/.dsh/profiles/web/cordis.patch.yml 追加:
- insert:
- id: dsh-session-notify
name: '@telosmaylx/dsh-session-notify'
config: {}
[!IMPORTANT] 无论用哪种方式,装完都需要刷新一次浏览器页面 —— 客户端 bundle 通过
__DSH_BOOT__启动图注入。
一条命令移除插件及其挂载(自动从 cordis.patch.yml 移除 insert 条目):
dsh plugin --profile web remove @telosmaylx/dsh-session-notify
[!NOTE] 手动安装(方式四/五)的用户,需同步从
~/.dsh/profiles/web/cordis.patch.yml删除对应 insert 条目,再刷新页面。
插件实现了完整的生命周期收尾(Cordis effect 纪律),卸载/禁用/HMR 热重载时:
| 平面 | 自动释放的资源 |
|---|---|
| host | session/event 事件订阅、settings 命名空间、会话投影单元、设置注册重试定时器(ctx.effect 包装);置卸载标志抑制已调度的微任务追加 |
| client | 会话列表订阅、完成推送正文的轮询定时器、window.__dsch_notify_debug 调试钩子(按引用删除,防闭包泄漏)、页内 toast 容器 DOM |
localStorage(dsh-scn-custom-presets),重装后仍在;刚装好时,会话日志里会出现这样一行可折叠提示:
会话「重构登录模块」已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。
默认文案在「会话」后内嵌会话标题标签(
{title});会话无标题时自动退回「会话已完成」。
每轮对话结束(turn/end)时按结束原因判断,命中白名单即提醒:
| 结束原因 | 含义 | 默认 |
|---|---|---|
completed | 会话正常完成 | 提醒 |
aborted | 会话中止 | 提醒 |
blocked | 会话被阻塞 | 提醒 |
error | 会话出错(附错误详情,超长截断) | 提醒 |
max-tokens | 达到输出 token 上限 | 提醒 |
interrupted | 中断(崩溃恢复后由持久化后端补写的孤儿轮次关闭标记) | 不提醒(可配置加入) |
子代理会话默认跳过(header.origin === 'subagent' 或 delegationDepth > 0)—— 子代理由父会话编排,逐轮提醒是噪音;可在宿主配置关闭跳过。
提问是独立通道,不走上面的白名单:AI 调用 ask_user_question 等待你回答时(tool/call 事件)立即提醒,tool/result 返回后提醒失效。提问不写会话日志,只弹通知。
审批也是独立通道:会话请求权限审批时(approval/asked)立即提醒,approval/decided 后失效。同样不写会话日志,只弹通知;标题与正文只含工具名与可选原因,不含命令参数。
客户端在会话列表观测到 running: true → false 边沿时推送,正文按以下优先级获取(最长轮询 6 秒,400ms 间隔):
session-complete-notify)—— 每个会话都有,后台会话同样拿到全文;kind=context + form=notice)—— 正在查看的会话,落盘后立即可用;cwd 最后一段)。提问提醒的正文同样优先取宿主投影(key = session-complete-notify-question,宿主已渲染好标题与正文),老宿主无该投影时客户端自行拼接标题与 {question} 文本兜底。
审批提醒按可用性三路取源:harness 原生 pendingInteractions → 宿主审批投影(key = session-complete-notify-approval)→ 会话列表快照的 pendingInteraction 字段;取到即提醒,同一审批只推一次。
以下均由 lib/core.js 的 buildNotice 实际生成。默认文案统一为「会话「{title}」已xx,请点击查看。」句式(按结束原因差异用词;不含用时与消耗):
简体中文默认文案:
会话「重构登录模块」已完成,请点击查看。 ← 完成
会话「重构登录模块」已中止,请点击查看。 ← 中止
会话「重构登录模块」被阻塞,请点击查看。 ← 阻塞
会话「重构登录模块」达到上限,请点击查看。 ← 上限
会话「重构登录模块」出错,请点击查看。 ← 出错
会话无标题(
titleValue为空)时自动回退「会话已完成,请点击查看。」;用时/消耗/缓存命中/速度等数据只在自定义模板中通过{duration}{usage}{cache}{tps}占位符插入时显示。
自定义模板(在设置面板编辑,本例用到全部信息位):
{title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}
渲染结果:
重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s
五种语言的同一事件:
会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
會話「重構登入模組」已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
セッション「重构登录模块」完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
세션「重构登录模块」 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。
| 权限状态 | 行为 |
|---|---|
default(未决定) | 完成事件只发 toast;设置面板「通知权限」区提供「请求授权」按钮(用户手势内请求——Chromium 会忽略非手势的自动请求,因此插件不再自动请求) |
granted | 按「失焦时」「聚焦时」各自所选通道发系统通知(独立 tag,互不覆盖;选「不通知」则不弹) |
denied(被浏览器屏蔽) | 仅 toast;设置面板显示地址栏操作指引(权限图标 → 网站设置 → 通知 → 允许) |
undefined(非安全上下文 / 不支持) | 仅 toast;建议改用「仅页内提示」 |
绝大多数配置在 DSH Web UI → 设置 → 插件 → 会话完成提醒 面板完成(保存后点「点击刷新」生效)。仅「触发原因白名单」在宿主 cordis.patch.yml 的 config 中配置(跳过子代理在面板中以复选框控制)。
面板在官方「设置 → 插件」面板中注册(settings.plugin.item keyed slot,key = session-complete-notify),样式逐值复刻原生插件卡片(12px 圆角、展开收起、旋转 chevron、footer 状态位 + 弃置 ghost + 主色保存按钮):
| 区域 | 内容 |
|---|---|
| 预设 | 下拉选择内置或自定义预设;「新增」把当前配置另存为自定义预设;当前预设可「删除」 |
| 语言 | 5 种语言单选,切换即时重渲染整个面板 |
| 失焦时 | 四选一:不通知(该时机完全静默)/ 双通道(系统通知 + 页内提示,默认)/ 仅系统通知 / 仅页内提示 —— 窗口失焦(切走标签页、最小化、点到别处)时生效 |
| 聚焦时 | 四选一:同上(默认双通道)—— 窗口聚焦时生效。两路互相独立,可自由组合成「失焦系统通知 + 聚焦不通知」等搭配 |
| 音频 | 折叠区(默认展开,点折叠头的 ▾ 可收起):提示音的音量与时长。展开后是**「音量」+ 滑块同一行**(「音量」二字在滑块前面,滑块右侧显示百分比与「试听 / 停止」按钮:0~100%,内存 0~1,默认 60%;试听按当前音量播放——拖完滑块点一下就能听出效果,播放中按钮变「停止」,再点一次即停,未设置任何提示音时按钮禁用)与**「时长」行**(同一行顺序:标签 → 滑块 → 数值框 → 秒;滑块量程 0~15 秒、步长 0.1 秒,拖动即改,也可直接在数值框里输入任意秒数;默认 0 = 不限制,设为 N 秒后音频到点仍未播完 → 0.5 秒音量淡出后停止,音频本身比上限短时自然播完;只影响播放,上传的音频原样保存、不截断)。两项写入设置文档的 volume / maxDuration;第三行是**「全局提示音」**(未设置显示「添加音频」按钮,已设置显示 WAV · 5 KB 这样的类型/体积摘要 + 「替换 / 试听 / 清除」),写入 globalAudio——内容没为该状态指定音频时播放它(优先级:该状态指定的音频 > 全局提示音;都没有则不播放) |
| 通知图片 | 大图两种来源:按原因上传——在模板中通过「+ 插入信息 → 图片」插入 {image} 标签并选择本地图片(编辑器内显示为带缩略图的标签,自动压缩至 512px 宽、按通知显示比例 16:9 居中裁切,随各原因独立保存);全局大图/图标——两张上传卡片并排一行(图标在前,空态 = 圆角矩形 + 号,点击上传;大图 512×288(16:9 居中裁切)、图标 128×128(1:1 方形居中裁切);已上传则卡片显示缩略图,点击缩略图可全屏查看完整原图(等比未裁切),右上角 × 删除)。图标留空用站点默认图标,也可在模板中插入 {icon} 标签按原因指定图标(优先于全局)。仅系统通知通道生效(页内 toast 为文字卡片),「发送」测试按钮同样生效 |
| 标题 | 折叠区(默认收起,点击展开):全局推送标题(所有原因共用,Chip 编辑器——点「+ 插入信息」插入的信息以胶囊标签形式显示,点击胶囊移除;占位提示「通用推送标题,留空时则使用默认标题,优先级低于下方自定义标题」(不可选中/删除);通知发送时标题里的信息位(用时/消耗/错误/缓存命中/速度)会替换为实际值,不再显示代码;留空时各原因用默认标题——完成=任务已完成、出错=任务出错、中止=任务已中止、阻塞=任务被阻塞、上限=任务达到上限、提问=AI 正在向你提问)+ 按原因定制标题(6 条原因各自输入,每行带「+」插入按钮——可插入信息标签(含「提问」,不含图片/图标),插入到光标处;优先于全局标题,留空 = 用全局或语言默认标题) |
| 内容 | 折叠区(默认收起,点击展开);展开后每条结束原因(完成、出错、中止、阻塞、上限、提问)一行式布局(原因标签 + Chip 编辑器 + 「+」插入按钮——菜单展开时变「−」+ 发送箭头按钮,按钮为矩形、垂直居中):空模板(默认预设)时编辑器显示默认文案「会话「{title}」已xx,请点击查看。」,文字 + 内联信息胶囊,光标处插入;{image}/{icon} 标签点击缩略图可预览大图、点 × 才删除(防误删),{audio} 标签点播放图标试听、播放中变成暂停图标、再点即停,点 × 才删除(同样防误删;删除标签只清掉该原因的音频数据,正文原样保留),其他标签点击移除;编辑后删空则显示「留空则使用默认文案」占位(不可选中/删除);提问行的默认文案为「AI 向你提问:{question}」,{question} 会在发送时替换为 AI 的实际提问(插入菜单同样提供「提问」标签,与其他标签同款交互) |
| 跳过子代理会话 | 复选框(保存时一并写入设置文档) |
| 通知权限 | 状态实时显示:已授权(绿)/ 尚未授权(附「请求授权」按钮)/ 已被浏览器屏蔽(附地址栏操作指引)/ 环境不支持 |
| 按原因定制标题 | 折叠区(默认收起):每个结束原因一个独立标题输入框,留空 = 用全局模板或语言默认标题 |
| 保存 | 写入宿主设置文档(language / templates / titleTemplate / titleTemplates / pushModeBlur / pushModeFocus / skipSubagents / audios / globalAudio / volume / maxDuration);只写真正变化的字段(其余一律不动),保存期间按钮禁用;保存后显示「点击刷新」链接 |
| 重置 | 一键还原默认值(语言保留当前选择,标题/模板/失焦聚焦通道/提示音/时长恢复默认)并立即保存 |
[!NOTE] 「失焦时」「聚焦时」的通道取舍:
dual(默认)同时弹 Windows 系统通知与页内 toast,toast 是保底通道,防止系统通知被平台静默(专注助手、通知横幅关闭)。两路互相独立,可组合成「失焦时系统通知、聚焦时不打扰」。老配置不受影响——设置文档里没有pushModeBlur/pushModeFocus时,两路都沿用旧版单一pushMode的值。但 QQ 浏览器等国产 Chromium 壳浏览器会把Notification渲染成「浏览器内置的页内推送弹窗」(页面顶部/角落的横幅,不经 Windows 通知中心)——此时dual会造成页内两个提示(浏览器内置弹窗 + 插件 toast)。这类浏览器请选「仅页内提示」(不再调用Notification,浏览器内置弹窗不会出现,页内只有插件自己的小 toast);「仅系统通知」模式在 QQ 浏览器无效(它永远渲染为页内弹窗)。设置面板每个原因的「发送」测试按钮同样受此影响——测试通知优先走「聚焦时」那一路,该路为「不通知」时改用「失焦时」那一路,两路都关则回落dual,保证预览总有反馈。
[!NOTE] 系统通知(
NotificationAPI)能否弹出由浏览器与站点访问方式共同决定:Edge/Chrome 对"不熟悉"的站点会自动屏蔽通知(地址栏出现「通知已屏蔽」)——点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许即可恢复;http://IP这类非安全上下文访问时Notification根本不存在,请改用「仅页内提示」。设置面板「通知权限」区域会实时显示当前状态并给出对应操作指引(可一键请求授权)。Firefox 窗口聚焦时通知显示为页内横幅、失焦才进系统通知中心。
[!NOTE] 面板中「跳过子代理会话」保存的是设置文档里的布尔值;宿主
cordis.patch.yml的config.skipSubagents是其启动默认值,两者任一为真即跳过。
每条结束原因独立一个模板输入框,标签即开关 —— 在模板里插入对应信息标签,该项数据才会显示:
| 占位符 | 含义 | 示例值 |
|---|---|---|
{title} | 会话标题(推送标题模板也可用) | 重构登录模块 |
{duration} | 本轮用时(turn/start 起表 → turn/end 结束) | 3 分 25 秒 / 3m25s |
{usage} | token 消耗(输入 = 未缓存 + 缓存读 + 缓存写) | 1,240 输入 / 3,560 输出 |
{error} | 错误信息(无错误时显示 none;单行化,80 字符截断) | connection timeout |
{cache} | 缓存命中率(官方投影口径,无数据为空) | 96.5% |
{tps} | 生成速度(官方投影口径,无数据为空) | 92 tok/s |
{image} | 自定义通知大图开关:从「+ 插入信息」插入并选择本地图片(自动压缩至 512px),按原因独立;正文渲染时剥除,不进会话日志;删除标签时该原因图片数据一并清除 | — |
{icon} | 自定义通知图标开关:从「+ 插入信息」插入并选择本地图片(自动压缩至 128×128 方形),按原因独立;正文渲染时剥除,不进会话日志;优先于全局「通知图标」;删除标签时该原因图标数据一并清除 | — |
{audio} | 自定义提示音开关:从「+ 插入信息 → 音频」插入并选择本地音频文件(mp3/wav/ogg… 原样读取,不做压缩、不做截断;>512 KB 会先确认),按原因独立;默认为空(不设置就没有提示音),音量与「时长」由面板「音频」折叠区里的「音量」滑块 / 「时长」行(滑块 0~15 秒、步长 0.1 + 数值框,默认 0 = 不限制,超时 0.5 秒淡出)统一控制,试听即按当前音量;点标签上的播放图标试听、播放中变成暂停图标再点即停,点标签上的 × 才删除(删除只清掉该原因的音频数据与标签,正文不受影响);正文渲染时剥除,不进会话日志;同个状态只允许一个(已插入时菜单按钮置灰,重复标签在保存时自动剥除)。是否播放由「已保存」的模板决定(点保存后刷新生效,与面板其他项一致)——插了标签且为该状态选了音频才用它;否则播放「音频」区里的全局提示音(两者都没有才静音) | — |
{question} | 提问行的专属占位符:发送时替换为 AI 的实际提问文本;与「+ 插入信息」菜单联动(可直接选「提问」标签,手输 {question} 同样识别为胶囊),仅提问通道可用,其他原因行插了也会被替换为空(防字面量泄漏) | 要继续生成报告吗? |
{label} | 已废弃 —— 渲染时自动剥除,旧模板仍兼容(插入菜单已移除该选项) | — |
模板留空即使用内置默认文案(「会话「{title}」已xx,请点击查看。」句式,不含用时与消耗)。折叠行 summary 与正文同源(渲染结果截断至 120 字符)—— 只看折叠行的用户也能看到真实标题与用时、消耗。
localStorage(key = dsh-scn-custom-presets):
未命名、未命名 2、未命名 3…(编号取当前最大值 + 1);- insert:
- id: dsh-session-notify
name: '@telosmaylx/dsh-session-notify'
config:
reasons: [completed, aborted, blocked, error, max-tokens]
skipSubagents: true
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
reasons | string[] | [completed, aborted, blocked, error, max-tokens] | 触发提醒的 turn/end 原因白名单 |
skipSubagents | boolean | true | 跳过子代理会话(origin=subagent 或 delegationDepth>0) |
插件分宿主平面(Node)与客户端平面(浏览器),中间靠会话日志(JSONL)与官方会话投影衔接:
┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
│ │
│ session/event 火线 │
│ ├─ turn/start → tracker 起表(key: sessionId:turn) │
│ ├─ assistant/message → 累加该轮 token 用量 │
│ ├─ tool/call → ask_user_question?写提问投影(标题+正文) │
│ └─ turn/end → reason.kind ∈ reasons ? │
│ ├─ 子代理会话?跳过 │
│ ├─ 读官方投影:cache / tps / title │
│ ├─ 按语言+模板构建通知(summary ≤120 字) │
│ └─ queueMicrotask 追加系统消息 │
│ (避开 append 重入窗口) │
│ │
│ settings.register → 官方「设置 → 插件」命名空间(失败退避重试) │
│ sessionProjections → 注册投影单元(key=session-complete-notify) │
│ + 提问投影(key=session-complete-notify- │
│ question,等待回答期间持续推送) │
└──────────────────────────────┬──────────────────────────────────────┘
│ user/message (source: plugin, form: notice)
▼ JSONL 持久化 + 投影推送
┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
│ │
│ 会话列表订阅:running true → false 边沿 → pushCompletion │
│ ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s) │
│ ├─ Web Notification(独立 tag,点击聚焦) │
│ └─ 页内 toast(永远展示,≤3 条,10s 自动消失) │
│ 提问投影轮询(key=session-complete-notify-question): │
│ 有值 → 立即弹提醒(标题+正文),无值清空 │
│ │
│ slots.inject('settings.plugin.item') → 设置卡片(预设/语言/模板) │
└─────────────────────────────────────────────────────────────────────┘
user/message,自身只监听 turn/*,事件类型不相交。@deepseek-ai/* 无法裸解析 —— 宿主平面用 createRequire 锚定 profile 共享依赖枢纽(.dsh/profiles/node_modules)取 schemastery(设置 schema)与 zod(投影 schema);UserMessage 按 dsh-llm 契约手工构造(id = crypto.randomUUID(),deep-freeze 由 session.append 的 adopt 快照阶段完成)。session/event 观察者回调运行在 turn/end 那次 append 的发布边界之内(dsh-session 在 dispatch 前置 entry.appending、finally 复位),同步 append 会被拒绝 —— 因此推迟到 queueMicrotask(微任务在本次同步栈含 finally 复位之后才执行)。ctx.effect() 中并返回 clearTimeout disposer —— 插件在重试窗口内被卸载或热重载时定时器随 fiber 拆除,不会对已释放的 ctx 触发注册(极老环境无 ctx.effect API 时退化为裸定时器 + ctx 已拆除兜底捕获)。core.js 导入带 ?v=1 缓存破坏(HMR 重载按 URL 键控);设置注册遇到热重载竞态(duplicate)时自动退避重试(最多 8 次,间隔 400ms × attempts)。ctx.root.get('sessionProjections')(最靠近宿主根的一份),拿不到时回退注入实例;只注册进注入实例时客户端可能读不到投影单元,推送正文走降级路径 —— 属尽力而为,不影响会话内系统消息。dsh-session-notify/
├── lib/
│ ├── index.js # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
│ │ # settings 命名空间注册(schemastery schema,退避重试);
│ │ # sessionProjections 投影单元(后台会话推送正文)
│ ├── core.js # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
│ │ # 5 语言文案表、时长/用量/缓存/速度格式化、
│ │ # 模板渲染({title}{duration}{usage}{error}{cache}{tps})、
│ │ # 提问正文构建(buildQuestionBody,{question} + 媒体剥除)
│ └── client.js # 浏览器平面:完成推送(系统通知 + toast)、
│ # 设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
├── scripts/
│ ├── build.sh # 零构建:仅 node --check 语法校验
│ ├── selftest.mjs # 零依赖自检(发布前必跑):schema / 纯逻辑 / 渲染冒烟 / 播放链路 / README 一致性
│ ├── verify-notice.mjs # 校验会话日志落盘证据(zstd 多帧逐帧解压)
│ ├── probe-client.mjs # 探针:客户端装配
│ ├── probe-client-e2e.mjs # 探针:客户端端到端
│ ├── probe-card-render.mjs # 探针:设置卡片渲染
│ ├── probe-settings-card.mjs # 探针:设置面板卡片
│ ├── probe-settings-check.mjs# 探针:设置面板检查
│ └── probe-diag-settings.mjs # 探针:settings 诊断
├── cordis.patch.yml # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
├── package.json # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
│ # exports: "." / "./client" / "./core"
├── LICENSE # MIT
└── README.md # 本文档
语法校验(零构建,prepublishOnly 同款检查):
npm run build
自检(发布前必跑):宿主 schema / 客户端纯逻辑与接线 / 设置面板渲染冒烟 / 提示音播放链路 / README × 5 一致性:
npm run selftest
发布(发布前自动执行 prepublishOnly 语法校验):
npm publish --registry=https://registry.npmjs.org --access public
离线校验:解出会话日志中所有 plugin-source 事件与 turn/end 尾部序列(不传路径则自动选 ~/.dsh/sessions 下最新会话):
node scripts/verify-notice.mjs <session.jsonl.zstd>
| 入口 | 内容 |
|---|---|
~/.dsh/session-complete-notify.log | 宿主诊断日志:设置注册、重试与失败、投影注册、追加失败堆栈 |
浏览器 console [dsh-session-notify-client] | 客户端日志:权限状态、通知展示、设置保存 |
window.__dsch_notify_debug.readNotice(id) | 手动读取指定会话的最新通知正文 |
window.__dsch_notify_debug.snapshotDebug(id) | 会话尾部节点类型 + notice 数量 + 最近正文(前 200 字) |
window.__dsch_notify_debug.alertAudio(action, src, volume, maxSeconds) | 提示音探针:play / stop / toggle / state,返回当前播放音源('' = 未播放) |
这是 DSH 官方设计:npm install 只把包装进依赖树,不注册插件。自动挂载的唯一途径是 dsh plugin add —— 它读取包内 dsh.bundle manifest(本插件自 0.1.3 起声明)并自动应用 cordis.patch.yml。参见安装。
会。AI 调用 ask_user_question 等待你回答时,宿主立刻把「提问标题 + 正文」写入独立投影(key = session-complete-notify-question),客户端轮询到后立即弹提醒——即使你正看着别的页面也不会错过。提问文案与完成通知一样完全可定制:设置面板的「标题 / 内容」折叠区各有「提问」一行,正文支持 {question} 占位符(注入 AI 的实际提问),{image} / {icon} / {audio} 媒体开关同样生效。回答后(tool/result)提醒失效,不会残留。
interrupted 是崩溃恢复后由持久化后端补写的孤儿轮次关闭标记,用户视角的「完成」不包含它(否则恢复会话会刷一屏误报)。确有需要可在宿主配置的 reasons 中加入。
会。客户端从会话列表快照观测所有会话的 running 边沿;正文优先取宿主投影 —— 宿主为所有会话(含后台)维护投影单元,因此推送正文跨会话一致。投影不可用时降级为事件窗口或工作区信息。
宿主在注册命名空间时读取一次设置,客户端 bundle 在页面加载时装配。保存后点「点击刷新」让两侧重新读取,新语言、模板即生效。
来自官方 sessionProjections(tokenUsage、sessionStats),与 dsh-web-ui 状态栏同口径。宿主读取投影快照失败或数据尚未就绪时,退回本地用量聚合估算,仍无数据则该项留空(标签插了也不显示)。另外,这两项只在自定义模板中通过 {cache}、{tps} 插入时才出现,默认文案不含。
摘要行(折叠行)与错误详情都会单行化并截断:摘要 120 字符、模板 {error} 80 字符、默认文案的错误详情 40 字符,超长以省略号结尾。
图标可以自定义:设置面板「通知图片」区可上传通知大图与通知图标(全局),也可在各原因模板中插入 {icon} 标签为该原因单独指定图标(优先于全局)。声音也可以自定义:在某个状态的「内容」行点「+ 插入信息 → 音频」,插入 {audio} 标签并选择本地音频文件(每个状态各一个,默认为空——不设置就没有提示音),音量与「时长」在面板「音频」折叠区里统一设置(「音量」标签就在滑块前,拖完可直接点「试听」——试听即按当前音量;「时长」= 滑块 0~15 秒、步长 0.1 + 数值框,默认 0 = 不限制,超时 0.5 秒淡出;上传的音频原样保存、不截断),通知与页内提示发出时按该音量播放一次;音频标签上的播放图标可随时试听(播放中变成暂停图标,再点即停)。toast 为固定深色卡片。如有其他需求欢迎提 Issue 或 PR。
两者都是浏览器行为,插件无法强制:
Notification 固定渲染为浏览器内置的页内推送弹窗(页面顶部/角落横幅,不经 Windows 通知中心),且无系统通知选项。「失焦时」「聚焦时」两个下拉在这类浏览器上表现一致:
双通道 → 浏览器内置弹窗 + 插件 toast,页内两个提示;仅系统通知 → 无效(QQ 浏览器永远渲染为页内弹窗);仅页内提示 → 浏览器内置弹窗不出现,页内只有插件自带的小 toast(推荐);不通知 → 该时机完全静默。
设置面板每个原因的「发送」测试按钮同样按此规则渲染。http://IP 访问(非安全上下文)时 Notification 不存在,任何浏览器都弹不了系统通知。设置面板「通知权限」区域会实时显示当前状态与对应操作指引。
| 版本 | 日期 | 变更 |
|---|---|---|
| 0.1.23 | 2026-10-04 |