# dsh-update-notifier [English](README.md) | 中文 每小时——或用 `/check-updates` 随时手动触发——检查已安装的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh) 插件有无新版本,并用一个**一键审批气泡**让你决定是否升级。 dsh 本身不会告诉你某个已安装插件已经出了新版,只能靠自己记得去 `npm view`。 本插件补上这一环:每小时把每个已安装插件和注册表比对一次,发现更新时直接在会话里 弹出一个真正的提问气泡,问你要升级哪些。勾选之后由智能体当着你的面执行升级。 它**绝不会擅自升级任何东西**。 ## 行为 每小时(可配置)对每个支撑已加载插件的 npm 包: - **查询注册表的 `latest`**,与 profile 的 `node_modules` 中已安装的版本比对。 版本比较严格遵循 semver——预发布版本排序正确,绝不建议降级;不符合严格 `X.Y.Z` 格式的版本会被跳过,而不是猜测其含义。 - **用一个气泡询问。** 所有更新的版本会汇总成一个多选提问,由 dsh 自己渲染 (`dsh-chrome 0.1.2 -> 0.1.3`,每个插件一个选项),挂在你最近活跃的会话上。 它不占用模型轮次、不消耗 token,断线重连后还会重放。 - **把勾选的交给智能体**:以一条后续消息指示它执行 ```sh pnpm --dir add @ ... ``` 智能体会立即被唤醒,你可以直接看到升级过程。搭配 [`dsh-hot-reload`](https://github.com/stuarthu/dsh-hot-reload) 使用,新版本无需重启 dsh 即可生效。 ## `/check-updates` 不想等下一个整点?在会话里输入 `/check-updates`: ``` /check-updates check-updates · 2 updates — see the question above (7 plugins checked) ``` 它会立即针对你输入命令的那个会话跑一轮完整检查,并在注册表查询结束的那一刻就 结算这条命令——它弹出的气泡就是一个普通气泡,你可以随时回答,刷新页面也不会丢。 如果当前根本无法弹出气泡,结算文本里会直接给出 `pnpm ... add` 命令,而不是只 告诉你"做不到"。 查询不到的包(网络故障、超时、5xx、限流,或响应里根本没有版本号)会在结算文本中 单独计数,而不会被当成"已是最新";如果**所有**包都查不到,这条命令会以错误结算, 而不是报告"没有更新"。两个数字相加即为本轮总数:`5 plugins checked, 2 unreachable` 表示一共查了 7 个。 取消这条命令会同时中止检查,因此被你取消的检查不会过一会儿又弹出气泡。 手动检查会**忽略下面的拒绝记忆**:既然是你主动问的,就把完整列表给你,包括你此前 未勾选的版本。它不会打乱定时检查的节奏;如果已有气泡未回答、或已有检查正在进行, 它会带着原因拒绝执行。 只要 dsh 组合了命令注册表,该命令就会出现——`@deepseek-ai/dsh-base` 就会组合它, 因此基于它的 profile 都能用 `/check-updates`。命令通过 `ctx.inject` 注册,所以 没有命令注册表的 headless profile 依然能加载本插件并照常执行定时检查。 ## 只问一次 同一个版本只会问你**一次**(指定时检查;上面的命令可以越过这条规则): - **未勾选的视为拒绝**,记录在 `/.dsh-update-notifier.json` 中, **跨重启**永不再提,直到注册表的 `latest` 变成另一个仍然高于你已安装版本的 版本为止。删除该文件即可重新开始询问。 - **已批准的不会记录**:升级成功后它自然就是当前版本;升级失败的话,重启后重新 提醒才是合理的。 - **你从未回答过的气泡**(会话结束、dsh 停止)不算拒绝——下次仍会询问。 尚无活跃会话时,提醒会挂起:在你下一条消息之后立即重试,下一轮定时检查也会重试。 如果根本没有任何界面能渲染气泡——headless profile,或者虽然组合了 `userQuestions` 服务但没有任何 UI 向它注册 provider——提醒同样会挂起,并且会把可用更新和确切的 升级命令一并写入日志,因为这种情况下你再发消息也不会有气泡出现。 ## 安装 ```sh dsh plugin --profile web add dsh-update-notifier ``` 然后重启一次 dsh(bundle patch 层在启动时加载)。适用于**任何 profile**——把 `web` 换成你实际使用的 profile 即可,它检查的就是自己被加载进的那个 profile。 需要 Node 18+(依赖全局 `fetch`),任何 dsh 宿主都已满足。 ## 兼容性 基于 **dsh `0.1.0-rc.6`**(Node 22 / 24)构建并测试。它只使用宿主的公开服务, 因此某项缺失时只会降级而不会崩溃: | 宿主接口 | 用途 | |---|---| | `loader.entries()` | 找出哪些包支撑着已加载的插件 | | `userQuestions.ask()` | 渲染审批气泡 | | `userQuestions.provider` | 没有 UI 注册渲染时,不承诺会有气泡 | | `agents.get(sessionId)` | 解析会话对应的活跃智能体 | | `agent.followup()` | 唤醒该智能体执行升级 | | `session/event`(`user/message`) | 追踪最近活跃的会话 | | `agent/disposed` | 撤回会话已销毁的气泡 | | `commands.register()` | 注册 `/check-updates` 命令 | | `agents.roots()` | 避免承诺一个 `ask()` 会拒绝的气泡 | `userQuestions` 和 `commands` 都是用到时才解析的,而不是声明为依赖,因此在两者 都不存在的 headless profile 中,插件依然能正常加载。 ## 退出检查 不希望被提示升级的插件,可以在自己的 `package.json` 中声明: ```json { "dsh": { "updateNotifier": false } } ``` 对于不由你掌控的插件,用下面的 `exclude` 让它闭嘴。 ## 配置 设置在 profile 的 `cordis.patch.yml` 里 `update-notifier` 那一行上: | 键 | 默认值 | 含义 | |---|---|---| | `interval` | `3600000` | 检查间隔毫秒数(下限钳制为 60000) | | `initialDelay` | `10000` | 启动后首次检查前的延迟毫秒数 | | `registry` | `https://registry.npmjs.org` | 要查询的 npm 兼容注册表 | | `exclude` | `[]` | 永不检查/提及的包名列表 | | `fetchTimeout` | `10000` | 单个注册表请求超时,毫秒(下限 1000) | | `profileDir` | 自动 | profile 目录的绝对路径(省略时从 loader base URL 自动探测) | ## 安全 - **未经点击不会有任何升级。** 本插件从不自行启动包管理器,只负责组装气泡, 以及在你批准后生成那条由智能体当着你的面执行的指令。 - **注册表的自由文本不会进入 UI 或模型。** 进入气泡和智能体消息的,只有来自你 本地 profile 的包名,以及通过 `/^[0-9A-Za-z.+-]{1,64}$/` 校验的版本号——绝不 包含从注册表取回的描述、README 或变更日志。 - 后续消息的 `source.kind` 为 `"plugin"`,明确列出已批准的包,并指示智能体不得 改动其他任何内容。 ## 局限 —— 请务必阅读 - **它只负责询问,不负责验证。** 你批准之后,升级就交给智能体执行了。失败会显示 在会话里,但本插件既不会重试,也不会事后确认新版本是否真的装上了。 - **只能看到从注册表安装的插件。** 从本地检出 link 过来的插件,或 dsh 内置的 插件,在 profile 的 `node_modules` 下没有对应的包,会被静默跳过——没有气泡, 也没有日志。 - **只查询 `latest` dist-tag。** 不支持固定版本范围、`next`/`beta` 通道,也无法 把某个包压在某个大版本上;对于永远不想升级的插件,请使用 `exclude`。 - **同一时间只有一个气泡。** 在它等待回答期间,定时检查会跳过,`/check-updates` 也会以"已有升级提问未回答"为由拒绝执行;这期间发布的新版本,会在你回答之后的 第一轮检查中被发现。 - **拒绝是针对那个确切版本的**,而不是"到此为止的所有版本":如果注册表的 `latest` 变成另一个仍然高于你当前版本的版本——包括 unpublish 之后降下来的 版本——你还是会被再次询问。 - 气泡需要一个 dsh 能挂载提问的会话。注册表故障、loader 状态异常、状态文件不可 写,都只会降级为日志;只有两种情况会让插件直接停用——上下文里没有插件 loader, 以及定位不到 profile 目录——此时日志中会明确说明是哪一种。 ## 许可 [MIT](LICENSE) © Stuart Hu