# 设计与架构 这份文档回答两类问题:**为什么这么设计**,以及**代码长什么样**。 - [为什么需要一个插件来做这件事](#为什么需要一个插件来做这件事) - [代码结构](#代码结构) - [宿主端 HTTP 接口](#宿主端-http-接口) - [设计取舍](#设计取舍) ## 为什么需要一个插件来做这件事 DSH 桌面外壳本来就有"任务完成"通知,但它是靠**扫描左侧会话列表的 DOM 文本**猜出来的: 会话标题在运行中一变,旧的那条就被当成"运行中的任务消失了",于是误报成"已完成"。 把外壳日志和真实会话日志做时间对齐后可以看到:**190 次通知里有 161 次是在回合还在运行时弹出的**, 很多发生在回合开始后 0.1~3 秒(标题刚生成的那一刻),只有 26 次接近真正的 `turn/end`。 所以本插件不猜 DOM,直接订阅权威事实: | 通知什么 | 依据 | | --- | --- | | 任务完成 | 会话事件 `turn/end`(带 `reason.kind`:completed / error / stopped …) | | 需要授权 | 会话事件 `approval/asked`(ApprovalService 在等决策前写入日志的审计事实) | | 等我回答 | `tools/pre-execute` 里看到 `ask_user_question`(**只观察,原样 `next()` 委托**,绝不抢夺门禁决策) | | 是否在看界面 | 浏览器半侧上报 `document.hasFocus()` + `visibilityState`(窗口有焦点且可见 = 在看) | 外壳误报本身的修法(独立于本插件可用)见 [`shell-patch/`](../shell-patch/README.md)。 ## 代码结构 ``` lib/smtp.js 零依赖 SMTP 客户端(465 隐式 TLS / 587 STARTTLS / AUTH LOGIN·PLAIN, RFC 2047 中文主题、base64 正文、可读的中文错误提示、可选协议追踪) lib/config.js 配置读写:白名单字段、口令不回显、原子写入、空模板 lib/template.js 邮件主题/正文模板:默认精简模板、占位符表、渲染与空行整理(纯函数) lib/session-title.js 侧栏会话名:读 /storages/session_projcache.json(带 mtime 缓存) lib/index.js 宿主端:订阅 turn/end、approval/asked、ask_user_question, 按"是否在看界面"决定发邮件还是排界面提示;提供本地 HTTP 接口 client/client.js 浏览器半侧:上报窗口焦点/可见性、轮询提示队列并弹系统通知 (退化方案:界面内浮层卡片)、渲染「设置 → 邮件通知」面板(两级页面) scripts/install.mjs 安装/卸载(复制 + 目录联接 + 改 profile 配置,可 --dry) scripts/send-test.mjs 命令行发信自检 shell-patch/ 修桌面外壳"任务一开始就弹完成通知"的误报(自带 asar 读写、校验与行为自检) test/ 假 SMTP 服务器 + 假 cordis 上下文 + DOM/React 桩的自检用例 tools/ 诊断工具(外壳误报通知的时间对齐分析) ``` 插件分两半:`lib/` 跑在宿主端(Node,能直接发信、订阅会话事件),`client/` 跑在浏览器页面里 (能知道焦点与可见性、渲染设置面板)。两边通过下面这组只绑在 `127.0.0.1` 的 HTTP 接口通信。 ## 宿主端 HTTP 接口 | 方法 | 路径 | 用途 | | --- | --- | --- | | POST | `/dsh-email-notify/presence` | 客户端上报 `{clientId, focused, visible}` | | GET | `/dsh-email-notify/inbox?since=N` | 取"看着界面时"的提示队列(带游标) | | GET | `/dsh-email-notify/config` | 读配置(口令只报是否已设置),并下发默认模板与占位符清单 | | POST | `/dsh-email-notify/config` | 写配置(白名单字段,口令留空 = 不改;模板里有认不出的占位符时回 `warnings`) | | GET | `/dsh-email-notify/status` | 诊断信息(含当前是否判定为"在看界面"、上次发信结果) | | POST | `/dsh-email-notify/test` | 发测试邮件,可传入界面上的草稿配置 | ## 设计取舍 - **为什么"在看界面"由浏览器半侧判定**:只有页面知道 `hasFocus()` 与 `visibilityState`。 心跳 20 秒一次(不监听鼠标键盘,免得每次敲键都刷接口),焦点/可见性变化即时上报; 宿主端 90 秒收不到心跳就当客户端不在了(关窗口、崩溃、断线都覆盖)。 没有任何客户端时按"不在看"处理——宁可多发一封,也不要漏掉完成通知。 - **为什么不订阅 `approval/request` 瀑布事件**:那是个 waterfall,监听器必须正确地 `next()` 委托, 一不小心就会抢走别人的授权决策。而 `approval/asked` 是服务自己写入会话日志的审计事实, 既权威又零风险。同理,`ask_user_question` 只从 `tools/pre-execute` **旁听**并原样委托。 - **为什么自己写 SMTP**:DSH 运行时的 `node_modules` 里没有 nodemailer, 而这个插件要以"复制 + 目录联接"的方式装配,不能依赖需要联网安装的包。 - **去重与节流**:同一回合的 `turn/end`、同一次授权的 `approval/asked`、同一次提问的 `callId` 各只处理一次(30 分钟窗口);同类通知按会话节流,另有全局每分钟上限防死循环。 - **默认拒绝明文发信**(`allowInsecure: false`):没有 STARTTLS 的服务器上继续发信等于把授权码明文丢出去, 宁可失败也得说清楚;确实要发(本机中继)才在设置里打开。