Cangjier/dsh-mail-notify

dsh-mail-notify

仓库尚未提供可用的公开描述。

未分類

dsh-mail-notify

DeepSeek Harness 插件,把 agent 的每一轮回答和你的邮箱接起来,双向:

  • 发:agent 结束一轮回答就发一封通知邮件,正文是该轮的回答文本,标题带上这轮回答的摘要。

一个任务常常要连跑好几轮(turn/end 之后还会继续下一轮),所以标题分成两种: 中途那几轮是 DSH 任务进行中|…,整段任务真正跑完(agent 停稳)那一封是 DSH 任务完成|…, 收件箱里一眼看得出哪封才是结束了。父会话停下来不等于整段任务结束:名下还有子代理 (Agent Teams 成员等)在跑时,收尾那封不会报「任务完成」——中途通知开着就改发 DSH 任务进行中(N 个子代理仍在跑)|…,关着就等它们跑完再发。详见两种邮件。

  • 收:两种玩法。回复那封通知邮件(主题里带关键字,默认 DSH),插件会把邮件正文**作为一条用户消息

追加到那封通知所属的会话里,接着原来的上下文继续干;或者新写一封邮件、标题正好是 创建对话 (默认也认 创建新对话、新建对话), 插件会新建一条会话,并把邮件正文原样作为这条对话的第一句话,工作区则由默认模型**看着邮件内容 从你已登记的项目里挑(挑不出来才沿用上一次的目录)。答案都按上面的规则回信给你 (见下文「收信触发」一节)。

  • 提问也提醒:agent 调 ask_user_question 向你要答案时,这一轮会停在原地等你,

所以插件不等轮次结束,提问一发生就发一封 DSH 等待你的回答|问题摘要,正文列出每个问题、 可选项和补充说明,提醒你回 DSH 界面回答(见提问时也会立刻发一封)。

  • 来源过滤:定时提醒(dsh-schedule)那一轮整轮不发信,也不会把你正跑着的任务的收尾邮件

降级成「进行中」;要跳别的自动化来源(目标续跑、webhook、邮件触发……)往 skipSources 里加 kind 即可。

  • 零依赖:SMTP 客户端(smtp.mjs)、IMAP 客户端(imap.mjs)、邮件解析(mail.mjs)

和邮件组装(mime.mjs)只用 Node 内置模块,不需要 pnpm add 任何东西。

  • 零构建:纯 ESM .mjs,直接就是运行时代码。
  • 默认全部关闭:装好、重启之后也是纯惰性的——不注册监听器、不连网络、不发信、不收信。
  • 自带配置引导:注册了一个模型可见的 mail_notify 工具,可以直接让 Agent 帮你配好、试发、查为什么没触发。
  • 收件人任意域名:发信服务器只由发件账号决定;收件人写 qq.com、163.com、gmail.com 都一样能收。

让 Agent 帮你配

插件注册了 mail_notify 工具,三个 action:

action · 作用 status · 报告当前到底会不会发信,以及收信触发的状态:SMTP 端点、发件账号、收件人、授权码从哪儿读、关键字、白名单、UID 水位 guide · 给出配置文件的确切路径,以及一段可直接粘贴的 YAML test · 立刻真发一封测试邮件(可用 to 临时指定别的收件人),验证网络/TLS/授权码/中文编码 reply_status · 只说收信触发:IMAP 端点、关键字规则、创建对话短语、工作目录策略、白名单、投递方式、已登记的通知与所属会话、UID 水位、最近一次轮询与投递(含逐封判定结果与原因——被跳过的邮件也列出来,所以「刚才那封为什么没触发」事后也查得到) reply_check · 立刻只读检查一次邮箱,逐封打印判定结果、原因,以及会落到哪条会话;不改状态、不投递。问「为什么我回复了却没反应」时用它

所以装好之后你只要说「帮我把邮件通知配好」,或者「为什么没收到邮件」,Agent 就能查到原因。 配置写错时这个工具照样注册——那时它正是把错因讲清楚的唯一入口。

装之前先看:它为什么不会拖垮 dsh

五条硬保证,都有测试兜着(node --test,175 个用例):

  1. 没启用就不碰任何东西。 enabled: false 且 reply.enabled: false(出厂默认)时,apply 在注册

session/event 监听器和定时器之前就返回,不响应任何会话事件,也没有网络或文件动作。

  1. 配置写错也不抛回加载器。 缺 to、地址非法、发件域名认不出、reply 字段类型错、config 整个是

undefined,这些情况都只写 error 日志然后保持惰性。

  1. 发信与收信是两个独立开关。 enabled 只管发通知,reply.enabled 只管收信触发;

可以只发、只收,或都开。

  1. 引导工具注册失败不影响发信。 它包在单独的 try 里——附加能力不能连累主功能。
  2. 来源判定不靠猜正文。 只看宿主给的输入来源(user/message.source.kind,辅以 turn/start

那一刻收件箱里即将被领取的那批消息);读不到就照常发信,绝不因为猜错而漏发。

为什么用 bundle,而不是散装文件

第一版把 .mjs 直接放在 profiles/desktop/plugins/ 下,再往 profile 的 cordis.patch.yml 里 insert 一行,结果运行中的 dsh 卡死,只能重启恢复。 profile 内的相对路径插件要靠 HMR 的运行时 reconcile 去 import 一个新模块; 而 bundle 方式是启动时解析的(dsh.profile.bundles),是 xl 插件已经验证过的路径。 所以现在代码放在 profile 目录之外,走 bundle。

安装(分三步,每步都可回退)

第 1 步:只加依赖,不加 bundle —— 风险为零

在 %USERPROFILE%.dsh\profiles\desktop\package.json 的 dependencies 里加一行:

"dependencies": {
  "dsh-mail-notify": "link:C:/Users/you/Documents/GitHub/dsh-mail-notify"   // 换成你自己的绝对路径
}
cd $env:USERPROFILE\.dsh\profiles\desktop
pnpm install

这一步不要动 dsh.profile.bundles。没有任何 bundle 或 patch 引用这个包, 加载器就不会 import 它,启动树和现在完全一样。确认 dsh 照常启动,再进第 2 步。

第 2 步:加进 bundles

"dsh": {
  "profile": {
    "bundles": [
      "@deepseek-ai/dsh-base",
      "@deepseek-ai/dsh-web-app",
      "xl",
      "dsh-mail-notify"                                                         // 新增
    ]
  }
}

完全退出再启动 dsh(bundle 解析变更本来就要重启,这是 dsh 的设计)。 此时插件已挂载,但 cordis.patch.yml 里 enabled: false,所以仍然不发信。 可以先用 mail_notify 的 guide / test 把配置弄好再来这一步之后的启用。

第 3 步:填个人配置并启用

本包的 cordis.patch.yml 里只有占位地址(you@example.com),真实邮箱写在 profile 自己的覆盖层 %USERPROFILE%.dsh\profiles\desktop\cordis.patch.yml 里, 这样公开仓库不会留下你的地址:

- id: mail-notify
  name: 'dsh-mail-notify'
  config:
    enabled: true
    smtp:
      user: 你的账号@qq.com
      from: 你的账号@qq.com
    to:
      - 收件人@qq.com

patch 是整体替换 config,不是深合并,所以这段会覆盖本包的默认值——从 enabled 开始写全。 host / port / secure 会按发件域名自动推断,不必手写;其余字段都有代码默认值。

example.com 故意不在内置端点表里:如果只把 enabled 改成 true 而不填真实地址,
会得到一条明确的配置错误日志,而不是静默失败。

回退

把 dsh-mail-notify 从 dsh.profile.bundles 里删掉再重启,就回到装之前的状态。 或者只把 enabled 改回 false。

授权码

不写进任何配置文件。按 内联 → 环境变量 → 文件 的顺序找:

  1. config.authCode(不推荐)
  2. config.authCodeEnv 指的环境变量,默认 QQ_SMTP_AUTH_CODE
  3. config.authCodeFile 指的文件,默认就是本目录下的 secret.txt

secret.txt 每次发信都重新读,所以补进去之后不用重启。它已在 .gitignore 里,不要提交。 QQ 邮箱要用「设置 → 账户 → POP3/IMAP/SMTP 服务」里生成的授权码,不是登录密码。

两种邮件:进行中与完成

驱动循环是 while (await turn()):一个任务的很多轮之间不会回到 idle,只有整段跑完 agent.status 才变成 idle。所以判定链是这样的:

某一轮 turn/end  →  先不发,等「安静窗口」(默认 5 秒)
      ├─ 期间收到下一轮 turn/start ⇒ 上一轮只是中途 → 发「DSH 任务进行中|摘要」
      ├─ 窗口到期、agent idle,但名下还有子代理在跑
      │     ├─ 中途通知开着 ⇒ 发「DSH 任务进行中(N 个子代理仍在跑)|摘要」
      │     └─ 中途通知关着 ⇒ 继续等,子代理收口后才发收尾那封
      └─ 窗口到期、agent idle,且没有子代理在跑 ⇒ 整段任务结束 → 发「DSH 任务完成|摘要」

父会话 idle 不等于整段任务结束:Agent Teams 成员、子代理都开自己的会话,父会话在等它们时 本来就是 idle;只看父会话就会把「还有成员在干活」报成「任务完成」。所以收尾那一封会再查一次 agents 实时注册表,只统计本会话的后代(沿会话血缘 parentSession 跨多层),有正在跑的 子代理就绝不出现「任务完成」:

情况 · 邮件 一轮就干完(最常见) · 只有一封:DSH 任务完成|…(正文带这一轮的回答) 连跑 3 轮 · 3 封:进行中、进行中、完成(都各自带那一轮的回答) 父会话停下时还有子代理在跑 · 中途通知开着:一封 进行中(N 个子代理仍在跑)|…,子代理收口后接一封 完成;notifyProgress: false 时只有子代理跑完那一封 完成 notifyProgress: false · 只有最后那封 完成,中途不打扰;正文只带最后一轮的回答

「完成」那一封正文开头会写清 本次任务已全部结束(N 轮),所以即使和「进行中」的邮件混在 一个会话里也能一眼确认收尾。两种邮件都带会话 token,回复任意一封都能接回同一条会话。

配置:

字段 · 默认 · 说明 subject · DSH 任务完成 · 全部结束那一封的前缀(非正常结束时自动补后缀) subjectProgress · DSH 任务进行中 · 中途每轮的前缀 notifyProgress · true · 中途那些轮是否各发一封;false = 一个任务只发一封 finalQuietSeconds · 5 · 安静多久算这一波结束;0 = 不等,立刻算结束(等于不管续跑)

安静窗口只影响「哪一封算完成」,不影响内容;把它调大一些可以更稳妥地吸收紧接着的续跑
(例如目标循环)。窗口内进程被关掉的话,这一封会丢——不放心就设 finalQuietSeconds: 0
或直接关掉 notifyProgress 之外的顾虑。

提问时也会立刻发一封

ask_user_question(以及任何走 DSH 问答服务的提问,比如计划审阅)和「一轮结束」不是一回事: 这一轮会停在原地等人回答,你不在界面(或界面根本关着)时它会一直等下去。所以这一封不等 turn/end,提问一发生就发:

智能体正在等你回答下面的问题:

1. 【确认】是否删除旧文件?
   - 删除(推荐):永久删除,不能恢复
   - 保留

请回到 DSH 界面里回答(这封邮件只是提醒,问题要在界面里答)。

情况 · 邮件 一次提问(ask_user_question) · 一封 DSH 等待你的回答|第一条问题的摘要 答完、这一轮继续跑完 · 照常再收到「进行中 / 完成」那一封 notifyQuestions: false · 提问时不发,只保留轮次通知

要点:

  • 发得早:挂的是 DSH 的 user-questions/request 瀑布,并且注册在宿主转发器之前——

正常答题(result)时宿主转发器直接返回、不再往下传,晚注册的监听器在真机上根本轮不到。

  • 不挡路:监听器只把发信排进队列就立刻放行,SMTP 再慢也不影响界面把问题画出来;

发信失败只记 error 日志,绝不让提问本身出错。

  • 列全:多个问题按编号全部列出;选项带上说明;计划审阅的 detail(标注「待审阅的计划」)、

多选标记也会带上。正文超长按 maxBodyChars 截断,但「回界面回答」这句提醒不会被截掉。

  • 和 `skipSources` 无关:定时提醒发起的任务如果反过来向你提问,仍然会发这一封——

它是「有人在等你」的行动提醒,不是任务结束的噪音。

  • 回信不能代替回答:回这封邮件只会作为那条会话的后续消息排队(要等你在界面里把这次提问

答完才会被读到),问题本身还得在界面回答。所以正文里的 token 页脚也是专门的措辞,不是 轮次通知那句「回复可以继续对话」。

  • token 的前提:只有「有会话 + 开了收信触发 + reply.token 未关 + 邮件真的发出」才会附

[DSH-XXXXXX] 并登记;没有 agent 的编程式提问(没有会话)不带 token,正文也不会承诺回信能排队。

  • 无条件触发:不判断你是否马上就会在界面里回答——提问一发生就发。嫌吵就写

notifyQuestions: false;目前没有「N 秒没答才发」的延迟选项。