dsh-messager 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.en.md +169 -45
  2. package/README.md +145 -146
  3. package/cordis.patch.yml +13 -0
  4. package/lib/channels/dingtalk.d.ts +38 -0
  5. package/lib/channels/dingtalk.d.ts.map +1 -0
  6. package/lib/channels/dingtalk.js +60 -0
  7. package/lib/channels/dingtalk.js.map +1 -0
  8. package/lib/channels/discord.d.ts +28 -0
  9. package/lib/channels/discord.d.ts.map +1 -0
  10. package/lib/channels/discord.js +44 -0
  11. package/lib/channels/discord.js.map +1 -0
  12. package/lib/channels/telegram.d.ts +31 -0
  13. package/lib/channels/telegram.d.ts.map +1 -0
  14. package/lib/channels/telegram.js +52 -0
  15. package/lib/channels/telegram.js.map +1 -0
  16. package/lib/channels/wecom.d.ts +34 -0
  17. package/lib/channels/wecom.d.ts.map +1 -0
  18. package/lib/channels/wecom.js +53 -0
  19. package/lib/channels/wecom.js.map +1 -0
  20. package/lib/client.js +391 -54
  21. package/lib/client.js.map +1 -1
  22. package/lib/config.d.ts +46 -0
  23. package/lib/config.d.ts.map +1 -1
  24. package/lib/config.js +27 -0
  25. package/lib/config.js.map +1 -1
  26. package/lib/index.d.ts +2 -2
  27. package/lib/index.d.ts.map +1 -1
  28. package/lib/index.js +33 -2
  29. package/lib/index.js.map +1 -1
  30. package/lib/notify.d.ts +1 -1
  31. package/lib/notify.d.ts.map +1 -1
  32. package/lib/notify.js +10 -6
  33. package/lib/notify.js.map +1 -1
  34. package/lib/types/client/card-controller.d.ts.map +1 -1
  35. package/lib/types/client/locales.d.ts +48 -2
  36. package/lib/types/client/locales.d.ts.map +1 -1
  37. package/lib/types/client/settings-form.d.ts.map +1 -1
  38. package/lib/types/config.d.ts +46 -0
  39. package/lib/types/config.d.ts.map +1 -1
  40. package/package.json +28 -5
package/README.md CHANGED
@@ -1,165 +1,165 @@
1
1
  # dsh-messager
2
2
 
3
- [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
4
- [![npm version](https://img.shields.io/npm/v/dsh-messager)](https://www.npmjs.com/package/dsh-messager)
5
- [![Node](https://img.shields.io/badge/node-%3E%3D20-blue.svg)](package.json)
3
+ DeepSeek Harness(DSH)**任务状态通知插件**:会话需要交互、任务完成、任务出错时,通过
4
+ **系统通知**(OS toast)、**浏览器通知**、**飞书 / 企业微信 / Discord / 钉钉 / Telegram** 推送提醒,
5
+ 不再依赖盯着会话列表的圆点。
6
6
 
7
- > **DeepSeek Harness(DSH)任务状态通知插件** / *Task-status notification plugin for DeepSeek Harness (DSH)*
8
- >
9
- > 会话需要交互、任务完成、任务出错时,通过**系统通知**(OS toast)、**浏览器通知**、**飞书机器人(webhook)**推送提醒——不再依赖盯着会话列表的圆点。
10
- > *Get notified via system notifications, browser notifications, and a Feishu (Lark) bot whenever a session needs attention, a task completes, or a task errors.*
11
-
12
- 单包双运行端(dual-runtime)结构:**host 端**(Node)负责系统通知与飞书 webhook,**client 端**(浏览器)负责 Web Notification。两者配置同源(命名空间 `messager`),设置页「通知&信使」分区可编辑、实时生效——配置经插件自身的 webServer 路由(`/dsh-messager/config`),**不受 DSH 设置白名单限制,发行版(npx/npm 安装)同样可用**。
13
-
14
- ---
15
-
16
- ## 目录 / Table of Contents
17
-
18
- - [特性 / Features](#特性--features)
19
- - [安装 / Installation](#安装--installation)
20
- - [配置 / Configuration](#配置--configuration)
21
- - [设置分区 / Settings section](#设置分区通知信使--settings-section)
22
- - [触发信号 / Trigger signals](#触发信号--trigger-signals)
23
- - [通道扩展 / Channel extension](#通道扩展--channel-extension)
24
- - [项目结构 / Project structure](#项目结构--project-structure)
25
- - [开发与测试 / Development](#开发与测试--development)
26
- - [已知边界 / Known limitations](#已知边界--known-limitations)
27
- - [许可 / License](#许可--license)
7
+ 单包双运行端(dual-runtime)结构:**host 端**(Node,服务端)负责系统通知与全部第三方通道,
8
+ **client 端**(浏览器)负责 Web Notification;两者配置同源(settings 命名空间 `messager`),
9
+ 设置页「通知&信使」分区可编辑、实时生效 —— 配置通道经插件自身的 webServer 路由
10
+ (`/dsh-messager/config`),**不受 DSH 设置白名单限制,发行版(npx 安装)同样可用**。
28
11
 
29
- ---
12
+ ## 能力一览
30
13
 
31
- ## 特性 / Features
32
-
33
- | 特性 | 说明 |
14
+ | 需求 | 实现 |
34
15
  | --- | --- |
35
- | **触发时机** | 需要交互(审批 / 提问 / 计划待审)、任务完成、任务出错,见[触发信号](#触发信号--trigger-signals) |
36
- | **推送路径** | 系统通知(node-notifier toast)、浏览器通知(Notification API)、飞书机器人(webhook:interactive 卡片 + HMAC-SHA256 签名) |
37
- | **可配置** | 各通道启停 / 内容繁复度 / icon、触发开关、去重冷却、标题前缀等 |
38
- | **国际化** | 设置分区菜单与表单随 DSH 语言切换(中文 / English),经 `ctx.locale` 注册 zh/en 字典 |
39
- | **快捷入口** | 浏览器可经快捷键 `Ctrl+Shift+M` 打开设置面板(不依赖 apiproxy 白名单) |
40
- | **易接入** | `NotifyChannel` 接口可扩展第三方通道(钉钉 / 企业微信 / Telegram 等) |
41
-
42
- 触发语义与 Web UI 状态圆点完全对齐:**橙点 = 需要交互**(`pendingInteraction`),**绿点 = 任务完成**(`running→idle` 且非当前会话),**蓝点 = 运行中**(不通知)。
43
-
44
- ---
16
+ | 触发时机 | 需要交互(审批 `approval/asked`、提问/计划待审 `ask_user_question`、客户端 pendingInteraction)、任务完成(`agent/status` running→idle 且仅根会话 + `turn/end` 原因)、任务出错(`agent/error`) |
17
+ | 推送路径 | 系统通知(node-notifier toast)、浏览器通知(Notification API)、飞书(interactive 卡片 + HMAC-SHA256 签名)、企业微信(markdown + 可选加签)、Discord(embed 卡片)、钉钉(actionCard + 可选加签)、Telegram(Bot API HTML 消息);`NotifyChannel` 接口可扩展 |
18
+ | 可配置 | 触发开关、各通道启停/verbosity/icon、去重冷却、标题前缀等,见[配置](#配置) |
45
19
 
46
- ## 安装 / Installation
20
+ 触发语义与 Web UI 状态圆点完全对齐:**橙点 = 需要交互**(`pendingInteraction`),**绿点 = 任务完成**
21
+ (`running→idle` 且非当前会话),**蓝点 = 运行中**(不通知)。
47
22
 
48
- > 支持 **npm / npx / git / 源码**四种安装方式。推荐的正式安装只需一步(host + client 都会生效)。
23
+ ## 安装
49
24
 
50
- ### 方式一:npm(推荐)<sup>已发布 npm</sup> / *Via npm (recommended, published)*
25
+ > 📖 面向**使用方**的完整分步指南见 [doc/用户安装指南.md](doc/用户安装指南.md) —— 覆盖
26
+ > **源码方式**(clone + 本地构建)与 **pnpm 方式**(本地 checkout / tarball / git / npm)两类安装。
51
27
 
52
- 已发布到 npm(`dsh-messager`)。DSH 环境已装的用户直接执行:
28
+ **正式安装只需一步**(host 端 + 浏览器 client 端都会生效,之后直接 `dsh web` 启动即可,
29
+ **不需要** `--patch`):
53
30
 
54
31
  ```sh
55
- dsh plugin --profile web add dsh-messager
56
- dsh web # 或 dsh --profile web
57
- ```
58
-
59
- > 尚未预装 `dsh` CLI 时,用 npx 现拉官方发行版执行(无需先全局装 dsh):
60
- > ```sh
61
- > npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-messager
62
- > npx -p @deepseek-ai/dsh dsh web
63
- > ```
64
- > npm 包已含构建产物 `lib/`,安装无需构建授权。
65
- > *The npm package ships prebuilt `lib/`, so no build-approval is needed.*
66
-
67
- ### 方式二:源码(开发 / 修改代码)/ *From source*
68
-
69
- ```sh
70
- git clone https://github.com/ly6170/dsh-messager.git
71
- cd dsh-messager
72
-
32
+ # 在插件仓库构建产物
73
33
  pnpm install
74
34
  pnpm build
75
35
 
76
- npx -p @deepseek-ai/dsh dsh plugin --profile web add ./
77
- npx -p @deepseek-ai/dsh dsh web
78
- ```
79
-
80
- > 使用方分步指南(源码 / pnpm 方式)见 [doc/用户安装指南.md](doc/用户安装指南.md)。
81
- > *Step-by-step guide: [doc/用户安装指南.md](doc/用户安装指南.md).*
82
-
83
- ### 方式三:从 git 直接安装 / *Directly from git (needs build approval)*
84
-
85
- ```sh
86
- npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ly6170/dsh-messager
36
+ dsh plugin --profile web add <插件路径>
37
+ dsh web # dsh --profile web
87
38
  ```
88
39
 
89
- > ⚠️ git 安装拉取的是源码,需跑 `prepare` 构建。pnpm ≥ 10 默认拒绝 git 依赖的构建脚本,首次 `add` 会提示把确切的包键加入该 profile `pnpm-workspace.yaml` `allowBuilds`,然后重新 `add`:
90
- > ```yaml
91
- > allowBuilds:
92
- > dsh-messager: true
93
- > ```
94
-
95
- ### 验证 / Verify
96
-
97
- 打开 `http://127.0.0.1:3080`,设置 → 侧边菜单出现「通知&信使」分区即安装成功;首次加载会请求浏览器通知权限。
98
-
99
- ---
100
-
101
- ## 配置 / Configuration
102
-
103
- 配置优先级:**schema 默认值 → base(该插件行的 `config:`)→ 用户层(Web 设置页/分区)**。
104
-
105
- 用户层入口(同源不冲突、任一变更实时生效):
106
-
107
- 1. **设置页「通知&信使」分区**:完整字段表单,所有环境(含发行版)可用;
108
- 2. **设置文档**:编辑 `$DSH_HOME/settings.yaml` 的 `messager:` 段(完整字段,含 dedup 节流等);
109
- 3. **RPC**:`settings.describe` / `settings.mutate`(host 侧可用)。
40
+ > `pnpm install` `pnpm build` 在你的插件仓库目录内执行;`<插件路径>` 替换为该目录
41
+ > (绝对或相对路径均可)。完整的分步安装指南见 [doc/用户安装指南.md](doc/用户安装指南.md)。
42
+
43
+ > - `--patch` 不是安装步骤,而是可选的**开发调试**手段(见下节「本地开发」):
44
+ > 它只加载 host 端、不写 profile、仅对本次启动生效。装了 bundle 之后**请勿再同时
45
+ > 带 `--patch` 启动同一插件**(host 端会加载两份,settings 命名空间重复注册报错)。
46
+ > - 以**源码方式运行 DSH**(从 deepseek-harness 仓库根目录)时,把上述 `dsh` 换成
47
+ > `pnpm dsh` 即可,命令与行为完全一致:`pnpm dsh plugin --profile web add …`、
48
+ > `pnpm dsh web`。profile 目录仍为 `$DSH_HOME/profiles/web`(`dsh web`
49
+ > `--profile web` 别名)。
50
+ > - 从 git 安装时 pnpm ≥ 10 需要放行构建脚本:把 pnpm 提示的包名加入 profile 的
51
+ > `pnpm-workspace.yaml` 的 `allowBuilds`(见 DSH 官方 publish 教程)。
52
+
53
+ ### 设置分区「通知&信使」(所有环境可用)
54
+
55
+ 安装后 DSH 设置页左侧菜单会出现 **「通知&信使」** 分区(排在「Agent预设」下方,位置
56
+ 随已有分区动态计算,不写死)。分区内是完整的配置表单,读写经插件自身的
57
+ webServer 路由(`/dsh-messager/config`,同源校验 + 脱敏视图)直达 host 端
58
+ `settings` 服务 —— **不依赖 DSH 的设置白名单,发行版(npx 安装)开箱即用**,
59
+ 无需任何补丁。
60
+
61
+ > 配置与 `settings.yaml` 同源(同一命名空间):任一处变更均实时生效。
62
+
63
+ ## 本地开发
64
+
65
+ - **host 端(快速)**:从 DSH 仓库根目录运行
66
+ `pnpm dsh web --patch <插件路径>/cordis.yml`,直接加载 TS 源码(HMR 生效)。
67
+ 源码模式下的 host 经 tsx 运行,该路径无需构建即可加载。
68
+ - **完整双运行端**:浏览器(client)端要求插件以包身份进入 Loader 才会被 clientModules
69
+ 扫描编入 Web bundle(`--patch` 的文件路径入口不会被扫描),因此完整开发请安装到 profile:
70
+ ```sh
71
+ dsh plugin --profile web add <插件路径> # 源码模式:pnpm dsh plugin ...
72
+ pnpm dsh web # 从 DSH 源码仓库运行
73
+ ```
74
+ `plugin add` 后需要**重启** `pnpm dsh web`(clientModules 启动时扫描,运行中的实例
75
+ 不会热加入新 bundle)。修改 client 端代码后在自己的仓库重新 `pnpm run build:client`
76
+ 并刷新页面即可(bundle 带 rev hash 会重新拉取;DSH 仓库的 `dev:web` watcher 只盯
77
+ workspace 内的 client 插件,不盯外部插件)。
78
+
79
+ 浏览器通知需要用户授予权限:首次加载插件时若权限为 `default` 会自动请求一次;
80
+ 被拒绝时浏览器通道静默降级(其余通道不受影响),可在浏览器站点设置中重新授权。
81
+
82
+ ## 配置
83
+
84
+ 配置优先级:**schema 默认值 → base(该插件行的 `config:`)→ 用户层(Web 设置页)**。
85
+ host 端把 Loader config 注册为 settings 命名空间 `messager` 的 base 层,因此:
86
+ - base 的写法按使用方式不同:dev 调试写在 `cordis.yml`(patch 覆盖层)里该行的
87
+ `config:`;正式安装写在 **profile 的 `cordis.patch.yml`** 里按 `id: messager`
88
+ 覆盖该行,或直接改 bundle 包内的 `cordis.patch.yml`;
89
+ - 用户层三处入口,**同源不冲突、任一处变更均实时生效**(host 端 `watch` 重建通道;
90
+ client 端经 `settings/document-updated` 失效重拉):
91
+ 1. **设置页分区**:设置 →「通知&信使」分区(完整字段表单,所有环境可用);
92
+ 2. **设置文档**:直接编辑 `$DSH_HOME/settings.yaml` 的 `messager:` 段(完整字段,
93
+ 含 dedup 节流等表单未展示的项);
94
+ 3. **RPC**:settings.describe / settings.mutate(host 侧可用;Web 端白名单不影响本插件
95
+ 的分区,因为分区走插件自己的配置路由)。
96
+
97
+ > 配置读写链路:设置分区 → `GET/POST /dsh-messager/config`(webServer 路由,同源校验)
98
+ > → host 端 `settings` 服务(describe 脱敏视图 / mutate 逐字段 ops)→ settings.yaml。
99
+ > 写后 `settings/document-updated` 事件(DSH 内置转发)驱动前端刷新。
100
+ >
101
+ > 🌐 **国际化**:分区菜单与表单文案随 DSH 设置的语言切换(中文 / English),
102
+ > 字典注册在 `ctx.locale`(zh/en 键集一致,缺失键 fail loud 显示键名)。
110
103
 
111
104
  | 字段 | 类型 | 默认值 | 说明 |
112
105
  | --- | --- | --- | --- |
113
- | `triggers.interaction` | boolean | `true` | 需要交互时通知 |
106
+ | `triggers.interaction` | boolean | `true` | 需要交互时通知(审批/提问/计划待审) |
114
107
  | `triggers.completed` | boolean | `true` | 任务完成时通知 |
115
108
  | `triggers.error` | boolean | `true` | 任务出错时通知 |
116
109
  | `system.enabled` | boolean | `true` | 系统通知通道 |
117
- | `system.icon` | string | - | 图标绝对路径(node-notifier 需要文件路径,且该文件必须存在) |
110
+ | `system.icon` | string | - | 图标绝对路径(node-notifier 需要文件路径,**且该文件必须存在**) |
118
111
  | `system.verbosity` | `minimal\|normal\|detailed` | `normal` | 系统通知内容繁复度 |
119
112
  | `browser.enabled` | boolean | `true` | 浏览器通知通道 |
120
- | `browser.onlyWhenHidden` | boolean | `true` | 仅页面隐藏/未聚焦时弹 |
113
+ | `browser.icon` | string | - | 图标 URL 或 data URL |
114
+ | `browser.onlyWhenHidden` | boolean | `true` | 仅页面隐藏/未聚焦时弹(看着界面不打扰) |
121
115
  | `browser.verbosity` | `minimal\|normal\|detailed` | `normal` | 浏览器通知内容繁复度 |
122
116
  | `feishu.enabled` | boolean | `false` | 飞书机器人(webhook)通道 |
123
117
  | `feishu.webhookUrl` | string | - | 自定义机器人 webhook 地址 |
124
118
  | `feishu.secret` | string(secret) | - | 签名密钥(机器人「安全设置-签名校验」) |
125
119
  | `feishu.timeoutMs` | number | `5000` | 单次请求超时 |
126
120
  | `feishu.verbosity` | `minimal\|normal\|detailed` | `normal` | 卡片内容繁复度 |
127
- | `dedup.interactionCooldownMs` | number | `10000` | 同会话同触发冷却 |
128
- | `dedup.completedDebounceMs` | number | `1000` | 完成通知防抖 |
129
- | `dedup.perChannelPerMinute` | number | `20` | 每通道每分钟上限 |
130
- | `message.titlePrefix` | string | - | 标题前缀 |
121
+ | `wecom.enabled` | boolean | `false` | 企业微信群机器人(webhook)通道 |
122
+ | `wecom.webhookUrl` | string | - | 群机器人 webhook 地址(含 `?key=`) |
123
+ | `wecom.secret` | string(secret) | - | 加签密钥(「安全设置-加签」,HMAC-SHA256,无需 URL 编码) |
124
+ | `wecom.timeoutMs` | number | `5000` | 单次请求超时 |
125
+ | `wecom.verbosity` | `minimal\|normal\|detailed` | `normal` | 消息内容繁复度 |
126
+ | `discord.enabled` | boolean | `false` | Discord 通道(webhook) |
127
+ | `discord.webhookUrl` | string | - | Discord webhook 地址(`.../api/webhooks/<id>/<token>`) |
128
+ | `discord.timeoutMs` | number | `5000` | 单次请求超时 |
129
+ | `discord.verbosity` | `minimal\|normal\|detailed` | `normal` | embed 内容繁复度 |
130
+ | `dingtalk.enabled` | boolean | `false` | 钉钉自定义机器人(webhook)通道 |
131
+ | `dingtalk.webhookUrl` | string | - | 自定义机器人 webhook 地址(含 `?access_token=`) |
132
+ | `dingtalk.secret` | string(secret) | - | 加签密钥(「安全设置-加签」,HMAC-SHA256 + URL 编码) |
133
+ | `dingtalk.timeoutMs` | number | `5000` | 单次请求超时 |
134
+ | `dingtalk.verbosity` | `minimal\|normal\|detailed` | `normal` | 卡片内容繁复度 |
135
+ | `telegram.enabled` | boolean | `false` | Telegram 通道(Bot API) |
136
+ | `telegram.botToken` | string(secret) | - | Bot Token(@BotFather 获取) |
137
+ | `telegram.chatId` | string | - | 接收 chat_id(数字 ID 或 `@频道用户名`) |
138
+ | `telegram.timeoutMs` | number | `5000` | 单次请求超时 |
139
+ | `telegram.verbosity` | `minimal\|normal\|detailed` | `normal` | 消息内容繁复度 |
140
+ | `dedup.interactionCooldownMs` | number | `10000` | 同会话同触发冷却(也用于跨标签去重窗口) |
141
+ | `dedup.completedDebounceMs` | number | `1000` | 完成通知防抖(等待 turn/end 原因、合并边界) |
142
+ | `dedup.perChannelPerMinute` | number | `20` | 每通道每分钟上限(防第三方限流/刷屏) |
143
+ | `message.titlePrefix` | string | - | 标题前缀,如 `[DSH]` |
131
144
  | `message.includeSessionTitle` | boolean | `true` | 正文附带会话标题 |
132
- | `message.guiUrl` | string | `http://127.0.0.1:3080` | 通知「打开」链接目标 |
133
-
134
- 内容繁复度:`minimal` 只有标题;`normal` 增加会话标题 / 工具名 / 结束原因 / 错误摘要;`detailed` 再增加 turn/step、审批原因与 GUI 链接。
135
-
136
- ---
145
+ | `message.guiUrl` | string | `http://127.0.0.1:3080` | 通知「打开」链接/按钮目标 |
137
146
 
138
- ## 设置分区「通知&信使」/ *Settings section*
147
+ 内容繁复度:`minimal` 只有标题;`normal` 增加会话标题/工具名/结束原因/错误摘要;
148
+ `detailed` 再增加 turn/step、审批原因与 GUI 链接。
139
149
 
140
- 安装后 DSH 设置页左侧菜单出现 **「通知&信使」** 分区,内含完整配置表单。读写经插件自身的 webServer 路由(`/dsh-messager/config`,同源校验 + 脱敏视图)直达 host 端 `settings` 服务——**不依赖 DSH 设置白名单,发行版(npx/npm 安装)开箱即用,无需任何补丁**。
150
+ ## 触发信号(事件 通知映射)
141
151
 
142
- > 配置与 `settings.yaml` 同源(同一命名空间),任一处变更均实时生效。
143
- > *Internationalized: the section menu and form follow the DSH display language (简体中文 / English).*
144
-
145
- ---
146
-
147
- ## 触发信号 / Trigger signals
148
-
149
- | 触发 | host 端(system/feishu) | client 端(browser) |
152
+ | 触发 | host 端(system/feishu/wecom/discord/dingtalk/telegram) | client 端(browser) |
150
153
  | --- | --- | --- |
151
154
  | 审批 | `session/event` `approval/asked` | 摘要 `pendingInteraction==='approval'` 出现 |
152
155
  | 提问/计划待审 | `session/event` `tool/call`(`ask_user_question`) | `pendingInteraction==='question'/'plan-review'` 出现 |
153
156
  | 任务完成 | `agent/status` running→idle(仅根会话)+`turn/end` 原因 | 摘要 `running:true→false` 且非当前会话 |
154
157
  | 任务出错 | `agent/error` | -(host 端覆盖) |
155
158
 
156
- > **分叉会话(fork)处理**:完成通知只对真正的子代理(`origin === 'subagent'`)排除,分叉会话(`sessions.fork`,其 `parentSession` 指向源会话但 `origin` 为空)视为顶层会话,照常通知。
157
-
158
- ---
159
-
160
- ## 通道扩展 / Channel extension
159
+ ## 通道扩展
161
160
 
162
- 实现 `NotifyChannel` 接口并在 `src/index.ts` 的 `buildChannels()` 注册即可接入新通道:
161
+ 新增第三方通道(钉钉/企业微信/Telegram…)实现 `NotifyChannel` 接口并在
162
+ `src/index.ts` 的 `buildChannels()` 注册即可:
163
163
 
164
164
  ```ts
165
165
  export interface NotifyChannel {
@@ -168,13 +168,11 @@ export interface NotifyChannel {
168
168
  }
169
169
  ```
170
170
 
171
- ---
172
-
173
- ## 项目结构 / Project structure
171
+ ## 项目结构
174
172
 
175
173
  ```
176
174
  dsh-messager/
177
- ├── package.json # dsh.bundle + dsh.client 双声明;exports["./client"];publishConfig/prepublishOnly
175
+ ├── package.json # dsh.bundle + dsh.client 双声明;exports["./client"]
178
176
  ├── tsconfig.json # host 端(Node)
179
177
  ├── tsconfig.client.json # client 端声明输出(lib/types/client)
180
178
  ├── tsdown.config.ts # client bundle(__ModuleLoader__.load 契约)
@@ -190,7 +188,7 @@ dsh-messager/
190
188
  │ ├── notify.ts # 调度:过滤/冷却/防抖/限流 + NotifyChannel 接口
191
189
  │ ├── templates.ts # verbosity 模板渲染(纯函数)
192
190
  │ ├── settings.ts # settings 命名空间注册(base = Loader config)
193
- │ ├── channels/ # system(node-notifier)、feishu(webhook+签名)
191
+ │ ├── channels/ # system(node-notifier)、feishu/wecom/discord/dingtalk/telegram(webhook/Bot API+签名)
194
192
  │ └── client/ # 浏览器端:sessions diff、Notification、设置分区、配置同步
195
193
  │ ├── index.ts # 分区注册(动态 order)+ 浏览器通知 + 配置路由访问器
196
194
  │ ├── section.tsx # 设置页「通知&信使」分区组件
@@ -200,42 +198,43 @@ dsh-messager/
200
198
  │ ├── locales.ts # zh/en 字典(ctx.locale 注册)
201
199
  │ ├── config.ts # 浏览器通知的配置句柄(走配置路由)
202
200
  │ └── diff.ts # 会话摘要 diff(纯函数)
203
- └── tests/ # vitest 单元测试
201
+ └── tests/ # vitest 单元测试(126 个)
204
202
  ```
205
203
 
206
- ---
207
-
208
- ## 开发与测试 / Development
204
+ ## 测试
209
205
 
210
206
  ```sh
211
- pnpm install
212
- pnpm test # 单元测试:信号/模板/调度/飞书签名/配置解析/client diff/配置路由/fetch scope/字典一致性
213
- pnpm typecheck # host 端类型检查
207
+ pnpm test # 126 个单元测试:信号提取/模板/调度/各通道签名与载荷/配置解析/client diff/配置路由/fetch scope/字典一致性/表单门控
208
+ pnpm typecheck # host
214
209
  pnpm build # host tsc + client 声明 + client bundle(lib/)
215
210
  ```
216
211
 
217
- ---
218
-
219
- ## 已知边界 / Known limitations
212
+ ## 已知边界
220
213
 
221
- - 浏览器通知需站点权限;`onlyWhenHidden=false` 时页面可见也会弹;
222
- - 多标签页经 localStorage 冷却去重,不同浏览器各自通知;
223
- - 子代理结束不触发完成通知(仅根会话)——分叉会话视为顶层且会通知;
224
- - 通道失败(webhook 超时、toast 不可用)只记日志,不影响其他通道与插件运行;
225
- - 完成/交互去重状态为内存态,DSH 重启后重置。
214
+ - 浏览器通知需站点权限;`onlyWhenHidden=false` 时页面可见也会弹。
215
+ - 多标签页经 localStorage 冷却去重;不同浏览器各自通知。
216
+ - 子代理结束不触发完成通知(仅根会话),避免噪音。
217
+ - 通道失败(webhook 超时、toast 不可用)只记日志,不影响其他通道与插件运行。
218
+ - 完成/交互的去重状态为内存态,DSH 重启后重置(可接受)。
226
219
 
227
220
  ### 系统通知(node-notifier)跨平台前提
228
221
 
229
- `node-notifier` 在三个平台调用完全不同的底层程序:
222
+ `node-notifier` 在三个平台调用**完全不同的底层程序**,平台差异如下:
230
223
 
231
224
  | 平台 | 底层 | 前提条件 / 差异 |
232
225
  | --- | --- | --- |
233
226
  | Windows | PowerShell ToastNotification | 内建,无需额外安装;`sound` 仅在 Windows 有可靠映射 |
234
- | macOS | terminal-notifier | 首次使用需联网下载第三方二进制,且需登录图形会话(Dock 存在);`sound` 不生效 |
235
- | Linux | notify-send(libnotify) | 需安装 `libnotify-bin`,并有运行中的通知守护进程(GNOME Shell / Plasma / mako / dunst 等);`sound` 不生效 |
227
+ | macOS | terminal-notifier | 首次使用需**联网下载**第三方二进制,且需登录图形会话(Dock 存在);`sound` 不生效 |
228
+ | Linux | notify-send(libnotify) | 需安装 `libnotify-bin`,并有一个**运行中的通知守护进程**(GNOME Shell / Plasma / mako / dunst 等);`sound` 不生效 |
236
229
 
237
- ---
230
+ - **图标**:`system.icon` 需是**存在的文件路径**。Windows 对缺失路径多会静默降级,但
231
+ Linux/macOS 可能直接报错,故通道层已做存在性校验,无效时降级为不带图标。
232
+ - **环境差异不是插件 bug**:Linux 若缺通知守护进程、macOS 若无法联网下载
233
+ terminal-notifier 或不在图形会话中,通知可能不弹出或静默失败——此时请先排查上述前提,
234
+ 而非插件;失败时调度层会 `logWarn` 记录具体错误。
238
235
 
239
- ## 许可 / License
236
+ ## 后续规划
240
237
 
241
- [MIT](LICENSE) © [ly6170](https://github.com/ly6170)
238
+ - 第三方通道扩展:邮件
239
+ - 触发扩展:后台 job 完成、goal 轮次完成
240
+ - 通知历史、按会话静音、勿扰时段
package/cordis.patch.yml CHANGED
@@ -9,3 +9,16 @@
9
9
  # feishu:
10
10
  # enabled: true
11
11
  # webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/<token>'
12
+ # wecom:
13
+ # enabled: false
14
+ # webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<key>'
15
+ # discord:
16
+ # enabled: false
17
+ # webhookUrl: 'https://discord.com/api/webhooks/<id>/<token>'
18
+ # dingtalk:
19
+ # enabled: false
20
+ # webhookUrl: 'https://oapi.dingtalk.com/robot/send?access_token=<token>'
21
+ # telegram:
22
+ # enabled: false
23
+ # botToken: ''
24
+ # chatId: ''
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 钉钉自定义机器人通道(webhook):actionCard 卡片(标题 + markdown 正文 + 打开按钮),host 端投递。
3
+ *
4
+ * 可选加签(机器人「安全设置-加签」):
5
+ * string_to_sign = `${timestamp}\n${secret}`
6
+ * sign = urlEncode(base64(HmacSHA256(string_to_sign, key=secret))) // 必须 URL 编码
7
+ * 追加为查询参数 &timestamp=<ts>&sign=<sign>(与企微不同:企微不需要 urlEncode)。
8
+ * 成功判定:HTTP 2xx 且响应 errcode === 0。
9
+ * 注意:actionCard 的 title 限 20 字符,超出会被平台拒绝,需截断。
10
+ */
11
+ import type { NotifyChannel, NotificationPayload } from '../notify.js';
12
+ export interface DingtalkChannelOptions {
13
+ webhookUrl: string;
14
+ /** 加签密钥(机器人「安全设置-加签」),配置后按钉钉规范签名。 */
15
+ secret?: string;
16
+ timeoutMs: number;
17
+ }
18
+ /** 钉钉 actionCard 载荷。 */
19
+ export interface DingtalkActionCardPayload {
20
+ msgtype: 'actionCard';
21
+ actionCard: {
22
+ title: string;
23
+ text: string;
24
+ btnOrientation: '0';
25
+ singleTitle: string;
26
+ singleURL: string;
27
+ };
28
+ }
29
+ /** 构建钉钉 actionCard 载荷(纯函数;title 限 20 字符;正文空时退化为链接)。 */
30
+ export declare function buildDingtalkPayload(payload: NotificationPayload): DingtalkActionCardPayload;
31
+ /**
32
+ * 钉钉加签:
33
+ * sign = urlEncode(base64(HmacSHA256(key=secret, msg=`${timestamp}\n${secret}`)))
34
+ * @returns 追加了 timestamp/sign(URL 编码后)查询参数的完整 URL。
35
+ */
36
+ export declare function signDingtalkUrl(webhookUrl: string, secret: string, timestamp: string): string;
37
+ export declare function createDingtalkChannel(options: DingtalkChannelOptions): NotifyChannel;
38
+ //# sourceMappingURL=dingtalk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dingtalk.d.ts","sourceRoot":"","sources":["../../src/channels/dingtalk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEtE,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAwB;AACxB,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,GAAG,CAAA;QACnB,WAAW,EAAE,MAAM,CAAA;QACnB,SAAS,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAED,uDAAuD;AACvD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,yBAAyB,CAY5F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAM7F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,aAAa,CAoBpF"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 钉钉自定义机器人通道(webhook):actionCard 卡片(标题 + markdown 正文 + 打开按钮),host 端投递。
3
+ *
4
+ * 可选加签(机器人「安全设置-加签」):
5
+ * string_to_sign = `${timestamp}\n${secret}`
6
+ * sign = urlEncode(base64(HmacSHA256(string_to_sign, key=secret))) // 必须 URL 编码
7
+ * 追加为查询参数 &timestamp=<ts>&sign=<sign>(与企微不同:企微不需要 urlEncode)。
8
+ * 成功判定:HTTP 2xx 且响应 errcode === 0。
9
+ * 注意:actionCard 的 title 限 20 字符,超出会被平台拒绝,需截断。
10
+ */
11
+ import { createHmac } from 'node:crypto';
12
+ /** 构建钉钉 actionCard 载荷(纯函数;title 限 20 字符;正文空时退化为链接)。 */
13
+ export function buildDingtalkPayload(payload) {
14
+ const link = `[打开 DSH](${payload.url})`;
15
+ return {
16
+ msgtype: 'actionCard',
17
+ actionCard: {
18
+ title: payload.title.slice(0, 20),
19
+ text: payload.body === '' ? link : `${payload.body}\n\n${link}`,
20
+ btnOrientation: '0',
21
+ singleTitle: '打开 DSH',
22
+ singleURL: payload.url,
23
+ },
24
+ };
25
+ }
26
+ /**
27
+ * 钉钉加签:
28
+ * sign = urlEncode(base64(HmacSHA256(key=secret, msg=`${timestamp}\n${secret}`)))
29
+ * @returns 追加了 timestamp/sign(URL 编码后)查询参数的完整 URL。
30
+ */
31
+ export function signDingtalkUrl(webhookUrl, secret, timestamp) {
32
+ const stringToSign = `${timestamp}\n${secret}`;
33
+ const sign = createHmac('sha256', secret).update(stringToSign).digest('base64');
34
+ const encoded = encodeURIComponent(sign);
35
+ const separator = webhookUrl.includes('?') ? '&' : '?';
36
+ return `${webhookUrl}${separator}timestamp=${timestamp}&sign=${encoded}`;
37
+ }
38
+ export function createDingtalkChannel(options) {
39
+ return {
40
+ id: 'dingtalk',
41
+ async send(payload) {
42
+ const url = options.secret === undefined
43
+ ? options.webhookUrl
44
+ : signDingtalkUrl(options.webhookUrl, options.secret, String(Math.floor(Date.now() / 1000)));
45
+ const response = await fetch(url, {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/json' },
48
+ body: JSON.stringify(buildDingtalkPayload(payload)),
49
+ signal: AbortSignal.timeout(options.timeoutMs),
50
+ });
51
+ if (!response.ok)
52
+ throw new Error(`dingtalk webhook responded ${response.status}`);
53
+ const result = (await response.json().catch(() => undefined));
54
+ if (result !== undefined && result.errcode !== 0) {
55
+ throw new Error(`dingtalk webhook rejected: errcode=${String(result.errcode)} errmsg=${String(result.errmsg)}`);
56
+ }
57
+ },
58
+ };
59
+ }
60
+ //# sourceMappingURL=dingtalk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dingtalk.js","sourceRoot":"","sources":["../../src/channels/dingtalk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAsBxC,uDAAuD;AACvD,MAAM,UAAU,oBAAoB,CAAC,OAA4B;IAC/D,MAAM,IAAI,GAAG,YAAY,OAAO,CAAC,GAAG,GAAG,CAAA;IACvC,OAAO;QACL,OAAO,EAAE,YAAY;QACrB,UAAU,EAAE;YACV,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,OAAO,IAAI,EAAE;YAC/D,cAAc,EAAE,GAAG;YACnB,WAAW,EAAE,QAAQ;YACrB,SAAS,EAAE,OAAO,CAAC,GAAG;SACvB;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,MAAc,EAAE,SAAiB;IACnF,MAAM,YAAY,GAAG,GAAG,SAAS,KAAK,MAAM,EAAE,CAAA;IAC9C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC/E,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IACtD,OAAO,GAAG,UAAU,GAAG,SAAS,aAAa,SAAS,SAAS,OAAO,EAAE,CAAA;AAC1E,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAA+B;IACnE,OAAO;QACL,EAAE,EAAE,UAAU;QACd,KAAK,CAAC,IAAI,CAAC,OAA4B;YACrC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS;gBACtC,CAAC,CAAC,OAAO,CAAC,UAAU;gBACpB,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YAC9F,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;gBACnD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;aAC/C,CAAC,CAAA;YACF,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;YAClF,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAsD,CAAA;YAClH,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;gBACjD,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YACjH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Discord 通道(webhook):embed 卡片(标题/正文/链接 + kind 颜色),host 端投递。
3
+ *
4
+ * 成功判定:任意 2xx(Discord 通常返回 204 No Content,无 body 可解析,
5
+ * 因此与飞书/企微/钉钉的 errcode 判定不同,不解析响应 JSON)。
6
+ * 限制守卫:title ≤ 256、description ≤ 4096(超限会被 Discord 400 拒绝)。
7
+ */
8
+ import type { NotifyChannel, NotificationPayload } from '../notify.js';
9
+ export interface DiscordChannelOptions {
10
+ webhookUrl: string;
11
+ timeoutMs: number;
12
+ }
13
+ /** kind → embed 颜色(interaction 橙 / completed 绿 / error 红)。 */
14
+ export declare function embedColorOf(kind: NotificationPayload['kind']): number;
15
+ /** Discord webhook 载荷(embed 单卡片)。 */
16
+ export interface DiscordEmbedPayload {
17
+ username: string;
18
+ embeds: Array<{
19
+ title: string;
20
+ description?: string;
21
+ url: string;
22
+ color: number;
23
+ }>;
24
+ }
25
+ /** 构建 Discord embed 载荷(纯函数;title ≤ 256、description ≤ 4096)。 */
26
+ export declare function buildDiscordPayload(payload: NotificationPayload): DiscordEmbedPayload;
27
+ export declare function createDiscordChannel(options: DiscordChannelOptions): NotifyChannel;
28
+ //# sourceMappingURL=discord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../src/channels/discord.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEtE,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,GAAG,MAAM,CAMtE;AAED,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,KAAK,CAAC;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;CACH;AAED,+DAA+D;AAC/D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,mBAAmB,CAUrF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAclF"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Discord 通道(webhook):embed 卡片(标题/正文/链接 + kind 颜色),host 端投递。
3
+ *
4
+ * 成功判定:任意 2xx(Discord 通常返回 204 No Content,无 body 可解析,
5
+ * 因此与飞书/企微/钉钉的 errcode 判定不同,不解析响应 JSON)。
6
+ * 限制守卫:title ≤ 256、description ≤ 4096(超限会被 Discord 400 拒绝)。
7
+ */
8
+ /** kind → embed 颜色(interaction 橙 / completed 绿 / error 红)。 */
9
+ export function embedColorOf(kind) {
10
+ switch (kind) {
11
+ case 'interaction': return 0xE67E22;
12
+ case 'completed': return 0x2ECC71;
13
+ case 'error': return 0xE74C3C;
14
+ }
15
+ }
16
+ /** 构建 Discord embed 载荷(纯函数;title ≤ 256、description ≤ 4096)。 */
17
+ export function buildDiscordPayload(payload) {
18
+ return {
19
+ username: 'DSH',
20
+ embeds: [{
21
+ title: payload.title.slice(0, 256),
22
+ ...(payload.body === '' ? {} : { description: payload.body.slice(0, 4096) }),
23
+ url: payload.url,
24
+ color: embedColorOf(payload.kind),
25
+ }],
26
+ };
27
+ }
28
+ export function createDiscordChannel(options) {
29
+ return {
30
+ id: 'discord',
31
+ async send(payload) {
32
+ const response = await fetch(options.webhookUrl, {
33
+ method: 'POST',
34
+ headers: { 'content-type': 'application/json' },
35
+ body: JSON.stringify(buildDiscordPayload(payload)),
36
+ signal: AbortSignal.timeout(options.timeoutMs),
37
+ });
38
+ if (!response.ok)
39
+ throw new Error(`discord webhook responded ${response.status}`);
40
+ // 204 No Content:成功即返回,不读取 body
41
+ },
42
+ };
43
+ }
44
+ //# sourceMappingURL=discord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.js","sourceRoot":"","sources":["../../src/channels/discord.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,8DAA8D;AAC9D,MAAM,UAAU,YAAY,CAAC,IAAiC;IAC5D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,aAAa,CAAC,CAAC,OAAO,QAAQ,CAAA;QACnC,KAAK,WAAW,CAAC,CAAC,OAAO,QAAQ,CAAA;QACjC,KAAK,OAAO,CAAC,CAAC,OAAO,QAAQ,CAAA;IAC/B,CAAC;AACH,CAAC;AAaD,+DAA+D;AAC/D,MAAM,UAAU,mBAAmB,CAAC,OAA4B;IAC9D,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,CAAC;gBACP,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;gBAClC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC5E,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC;KACH,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,OAAO;QACL,EAAE,EAAE,SAAS;QACb,KAAK,CAAC,IAAI,CAAC,OAA4B;YACrC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE;gBAC/C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;gBAClD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;aAC/C,CAAC,CAAA;YACF,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;YACjF,gCAAgC;QAClC,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Telegram 通道(Bot API):sendMessage(HTML parse_mode),host 端投递。
3
+ *
4
+ * 端点 = https://api.telegram.org/bot<token>/sendMessage(token 即凭证,走 URL 路径)。
5
+ * 成功判定:HTTP 2xx 且响应 ok === true。
6
+ * HTML parse_mode 必须转义 & < > " ',否则非法标签会导致 API 400。
7
+ * 限制守卫:text ≤ 4096 字符。
8
+ */
9
+ import type { NotifyChannel, NotificationPayload } from '../notify.js';
10
+ export interface TelegramChannelOptions {
11
+ botToken: string;
12
+ /** 接收 chat_id(数字 ID 或 @频道用户名)。 */
13
+ chatId: string;
14
+ timeoutMs: number;
15
+ }
16
+ export declare const TELEGRAM_API_BASE = "https://api.telegram.org";
17
+ /** HTML 转义(纯函数):& < > " '。 */
18
+ export declare function escapeHtml(text: string): string;
19
+ /** sendMessage 载荷。 */
20
+ export interface TelegramSendPayload {
21
+ chat_id: string;
22
+ text: string;
23
+ parse_mode: 'HTML';
24
+ link_preview_options: {
25
+ is_disabled: boolean;
26
+ };
27
+ }
28
+ /** 构建 sendMessage 载荷(纯函数;text ≤ 4096 兜底)。 */
29
+ export declare function buildTelegramPayload(payload: NotificationPayload, chatId: string): TelegramSendPayload;
30
+ export declare function createTelegramChannel(options: TelegramChannelOptions): NotifyChannel;
31
+ //# sourceMappingURL=telegram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/channels/telegram.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEtE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,iBAAiB,6BAA6B,CAAA;AAE3D,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED,sBAAsB;AACtB,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,oBAAoB,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C;AAED,6CAA6C;AAC7C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAUtG;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,aAAa,CAkBpF"}