dsh-messager 0.1.4
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.
- package/LICENSE +21 -0
- package/README.en.md +86 -0
- package/README.md +241 -0
- package/assets/icon.png +0 -0
- package/cordis.patch.yml +11 -0
- package/lib/channels/feishu.d.ts +58 -0
- package/lib/channels/feishu.d.ts.map +1 -0
- package/lib/channels/feishu.js +77 -0
- package/lib/channels/feishu.js.map +1 -0
- package/lib/channels/system.d.ts +26 -0
- package/lib/channels/system.d.ts.map +1 -0
- package/lib/channels/system.js +73 -0
- package/lib/channels/system.js.map +1 -0
- package/lib/client.js +2001 -0
- package/lib/client.js.map +1 -0
- package/lib/config-route.d.ts +55 -0
- package/lib/config-route.d.ts.map +1 -0
- package/lib/config-route.js +170 -0
- package/lib/config-route.js.map +1 -0
- package/lib/config-shared.d.ts +28 -0
- package/lib/config-shared.d.ts.map +1 -0
- package/lib/config-shared.js +7 -0
- package/lib/config-shared.js.map +1 -0
- package/lib/config.d.ts +82 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +59 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +152 -0
- package/lib/index.js.map +1 -0
- package/lib/notify.d.ts +75 -0
- package/lib/notify.d.ts.map +1 -0
- package/lib/notify.js +157 -0
- package/lib/notify.js.map +1 -0
- package/lib/settings.d.ts +23 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +23 -0
- package/lib/settings.js.map +1 -0
- package/lib/signals.d.ts +57 -0
- package/lib/signals.d.ts.map +1 -0
- package/lib/signals.js +52 -0
- package/lib/signals.js.map +1 -0
- package/lib/templates.d.ts +40 -0
- package/lib/templates.d.ts.map +1 -0
- package/lib/templates.js +107 -0
- package/lib/templates.js.map +1 -0
- package/lib/types/client/card-controller.d.ts +171 -0
- package/lib/types/client/card-controller.d.ts.map +1 -0
- package/lib/types/client/config.d.ts +20 -0
- package/lib/types/client/config.d.ts.map +1 -0
- package/lib/types/client/diff.d.ts +26 -0
- package/lib/types/client/diff.d.ts.map +1 -0
- package/lib/types/client/fetch-scope.d.ts +29 -0
- package/lib/types/client/fetch-scope.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +92 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/section.d.ts +16 -0
- package/lib/types/client/section.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +22 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/config-shared.d.ts +28 -0
- package/lib/types/config-shared.d.ts.map +1 -0
- package/lib/types/config.d.ts +82 -0
- package/lib/types/config.d.ts.map +1 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ly6170
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# dsh-messager
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-messager)
|
|
5
|
+
[](package.json)
|
|
6
|
+
|
|
7
|
+
> **Task-status notification plugin for DeepSeek Harness (DSH).** Get notified via **system notifications** (OS toast), **browser notifications**, and a **Feishu (Lark) bot** (webhook) whenever a session needs attention, a task completes, or a task errors.
|
|
8
|
+
|
|
9
|
+
A single-package, dual-runtime design: the **host side** (Node) handles system notifications and the Feishu webhook; the **client side** (browser) handles Web Notification. Both share one config source (namespace `messager`), editable live from the "Notifications" settings section — config flows through the plugin's own webserver route (`/dsh-messager/config`), so it works on all environments including release/npx/npm installs (no DSH settings allowlist dependency).
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🟢 Three triggers — needs-interaction / completed / errored, each toggleable
|
|
14
|
+
- 🔔 Three channels — system (node-notifier), browser (Notification API), Feishu bot (webhook + HMAC-SHA256 signature)
|
|
15
|
+
- ⚙️ Hot-reloadable config — applies instantly, no restart
|
|
16
|
+
- 🌐 Internationalized settings section (简体中文 / English) via `ctx.locale`
|
|
17
|
+
- ⌨️ Browser shortcut `Ctrl+Shift+M` opens the settings panel (no apiproxy allowlist needed)
|
|
18
|
+
- 🔌 Extensible channels via the `NotifyChannel` interface
|
|
19
|
+
|
|
20
|
+
Trigger semantics align with the Web UI status dots: orange = needs interaction, green = completed (non-current session), blue = running (not notified).
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install from npm (recommended, prebuilt `lib/` — no build approval needed):
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add dsh-messager
|
|
28
|
+
dsh web
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If you don't have the `dsh` CLI preinstalled, run it on the fly via npx:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-messager
|
|
35
|
+
npx -p @deepseek-ai/dsh dsh web
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
From source (for development / customizing):
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
git clone https://github.com/ly6170/dsh-messager.git
|
|
42
|
+
cd dsh-messager
|
|
43
|
+
pnpm install
|
|
44
|
+
pnpm build
|
|
45
|
+
npx -p @deepseek-ai/dsh dsh plugin --profile web add ./
|
|
46
|
+
npx -p @deepseek-ai/dsh dsh web
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Precedence: **schema defaults → base (that row's `config:`) → user layer (settings section/document/RPC)**. Any change applies instantly.
|
|
52
|
+
|
|
53
|
+
| Field | Type | Default | Description |
|
|
54
|
+
| --- | --- | --- | --- |
|
|
55
|
+
| `triggers.interaction` | boolean | `true` | Notify on needs-interaction |
|
|
56
|
+
| `triggers.completed` | boolean | `true` | Notify on task completed |
|
|
57
|
+
| `triggers.error` | boolean | `true` | Notify on task error |
|
|
58
|
+
| `system.enabled` | boolean | `true` | System notification channel |
|
|
59
|
+
| `system.icon` | string | - | Absolute icon path (must exist) |
|
|
60
|
+
| `system.verbosity` | `minimal\|normal\|detailed` | `normal` | System content verbosity |
|
|
61
|
+
| `browser.enabled` | boolean | `true` | Browser notification channel |
|
|
62
|
+
| `browser.onlyWhenHidden` | boolean | `true` | Notify only when page hidden/unfocused |
|
|
63
|
+
| `browser.verbosity` | `minimal\|normal\|detailed` | `normal` | Browser content verbosity |
|
|
64
|
+
| `feishu.enabled` | boolean | `false` | Feishu bot (webhook) channel |
|
|
65
|
+
| `feishu.webhookUrl` | string | - | Custom bot webhook URL |
|
|
66
|
+
| `feishu.secret` | string (secret) | - | Signing secret |
|
|
67
|
+
| `feishu.timeoutMs` | number | `5000` | Per-request timeout |
|
|
68
|
+
| `feishu.verbosity` | `minimal\|normal\|detailed` | `normal` | Card content verbosity |
|
|
69
|
+
| `dedup.interactionCooldownMs` | number | `10000` | Cooldown for same session/trigger |
|
|
70
|
+
| `dedup.completedDebounceMs` | number | `1000` | Completion debounce |
|
|
71
|
+
| `dedup.perChannelPerMinute` | number | `20` | Per-channel per-minute cap |
|
|
72
|
+
| `message.titlePrefix` | string | - | Title prefix |
|
|
73
|
+
| `message.includeSessionTitle` | boolean | `true` | Include session title |
|
|
74
|
+
| `message.guiUrl` | string | `http://127.0.0.1:3080` | "Open" link target |
|
|
75
|
+
|
|
76
|
+
## Known limitations
|
|
77
|
+
|
|
78
|
+
- Browser notifications require site permission; with `onlyWhenHidden=false` they also pop when visible.
|
|
79
|
+
- Multi-tab dedup uses localStorage cooldown; each browser notifies separately.
|
|
80
|
+
- Subagent completions are not notified (root sessions only) — forked sessions count as top-level and do notify.
|
|
81
|
+
- Channel failures are only logged; they don't affect other channels or the plugin.
|
|
82
|
+
- Completion/interaction dedup state is in-memory and resets after a DSH restart.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
[MIT](LICENSE) © [ly6170](https://github.com/ly6170)
|
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# dsh-messager
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-messager)
|
|
5
|
+
[](package.json)
|
|
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)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 特性 / Features
|
|
32
|
+
|
|
33
|
+
| 特性 | 说明 |
|
|
34
|
+
| --- | --- |
|
|
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
|
+
---
|
|
45
|
+
|
|
46
|
+
## 安装 / Installation
|
|
47
|
+
|
|
48
|
+
> 支持 **npm / npx / git / 源码**四种安装方式。推荐的正式安装只需一步(host + client 都会生效)。
|
|
49
|
+
|
|
50
|
+
### 方式一:npm(推荐)<sup>已发布 npm</sup> / *Via npm (recommended, published)*
|
|
51
|
+
|
|
52
|
+
已发布到 npm(`dsh-messager`)。DSH 环境已装的用户直接执行:
|
|
53
|
+
|
|
54
|
+
```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
|
+
|
|
73
|
+
pnpm install
|
|
74
|
+
pnpm build
|
|
75
|
+
|
|
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
|
|
87
|
+
```
|
|
88
|
+
|
|
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 侧可用)。
|
|
110
|
+
|
|
111
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
112
|
+
| --- | --- | --- | --- |
|
|
113
|
+
| `triggers.interaction` | boolean | `true` | 需要交互时通知 |
|
|
114
|
+
| `triggers.completed` | boolean | `true` | 任务完成时通知 |
|
|
115
|
+
| `triggers.error` | boolean | `true` | 任务出错时通知 |
|
|
116
|
+
| `system.enabled` | boolean | `true` | 系统通知通道 |
|
|
117
|
+
| `system.icon` | string | - | 图标绝对路径(node-notifier 需要文件路径,且该文件必须存在) |
|
|
118
|
+
| `system.verbosity` | `minimal\|normal\|detailed` | `normal` | 系统通知内容繁复度 |
|
|
119
|
+
| `browser.enabled` | boolean | `true` | 浏览器通知通道 |
|
|
120
|
+
| `browser.onlyWhenHidden` | boolean | `true` | 仅页面隐藏/未聚焦时弹 |
|
|
121
|
+
| `browser.verbosity` | `minimal\|normal\|detailed` | `normal` | 浏览器通知内容繁复度 |
|
|
122
|
+
| `feishu.enabled` | boolean | `false` | 飞书机器人(webhook)通道 |
|
|
123
|
+
| `feishu.webhookUrl` | string | - | 自定义机器人 webhook 地址 |
|
|
124
|
+
| `feishu.secret` | string(secret) | - | 签名密钥(机器人「安全设置-签名校验」) |
|
|
125
|
+
| `feishu.timeoutMs` | number | `5000` | 单次请求超时 |
|
|
126
|
+
| `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 | - | 标题前缀 |
|
|
131
|
+
| `message.includeSessionTitle` | boolean | `true` | 正文附带会话标题 |
|
|
132
|
+
| `message.guiUrl` | string | `http://127.0.0.1:3080` | 通知「打开」链接目标 |
|
|
133
|
+
|
|
134
|
+
内容繁复度:`minimal` 只有标题;`normal` 增加会话标题 / 工具名 / 结束原因 / 错误摘要;`detailed` 再增加 turn/step、审批原因与 GUI 链接。
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 设置分区「通知&信使」/ *Settings section*
|
|
139
|
+
|
|
140
|
+
安装后 DSH 设置页左侧菜单出现 **「通知&信使」** 分区,内含完整配置表单。读写经插件自身的 webServer 路由(`/dsh-messager/config`,同源校验 + 脱敏视图)直达 host 端 `settings` 服务——**不依赖 DSH 设置白名单,发行版(npx/npm 安装)开箱即用,无需任何补丁**。
|
|
141
|
+
|
|
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) |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| 审批 | `session/event` `approval/asked` | 摘要 `pendingInteraction==='approval'` 出现 |
|
|
152
|
+
| 提问/计划待审 | `session/event` `tool/call`(`ask_user_question`) | `pendingInteraction==='question'/'plan-review'` 出现 |
|
|
153
|
+
| 任务完成 | `agent/status` running→idle(仅根会话)+`turn/end` 原因 | 摘要 `running:true→false` 且非当前会话 |
|
|
154
|
+
| 任务出错 | `agent/error` | -(host 端覆盖) |
|
|
155
|
+
|
|
156
|
+
> **分叉会话(fork)处理**:完成通知只对真正的子代理(`origin === 'subagent'`)排除,分叉会话(`sessions.fork`,其 `parentSession` 指向源会话但 `origin` 为空)视为顶层会话,照常通知。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 通道扩展 / Channel extension
|
|
161
|
+
|
|
162
|
+
实现 `NotifyChannel` 接口并在 `src/index.ts` 的 `buildChannels()` 注册即可接入新通道:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
export interface NotifyChannel {
|
|
166
|
+
readonly id: string
|
|
167
|
+
send(payload: NotificationPayload): Promise<void>
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 项目结构 / Project structure
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
dsh-messager/
|
|
177
|
+
├── package.json # dsh.bundle + dsh.client 双声明;exports["./client"];publishConfig/prepublishOnly
|
|
178
|
+
├── tsconfig.json # host 端(Node)
|
|
179
|
+
├── tsconfig.client.json # client 端声明输出(lib/types/client)
|
|
180
|
+
├── tsdown.config.ts # client bundle(__ModuleLoader__.load 契约)
|
|
181
|
+
├── cordis.yml # 本地开发覆盖层(host 端)
|
|
182
|
+
├── cordis.patch.yml # 分发包配置层(安装后生效)
|
|
183
|
+
├── assets/icon.png # 默认通知图标
|
|
184
|
+
├── src/
|
|
185
|
+
│ ├── index.ts # host apply:事件接线 + settings 注册 + 通道构建 + 路由挂载
|
|
186
|
+
│ ├── config.ts # Config schema(Loader config 与 settings 共用)
|
|
187
|
+
│ ├── config-shared.ts # 配置路由的跨端共享类型(host/client 共用)
|
|
188
|
+
│ ├── config-route.ts # webServer 配置路由(GET 视图 / POST ops,同源校验)
|
|
189
|
+
│ ├── signals.ts # 事件 → Signal 提取(纯函数)
|
|
190
|
+
│ ├── notify.ts # 调度:过滤/冷却/防抖/限流 + NotifyChannel 接口
|
|
191
|
+
│ ├── templates.ts # verbosity 模板渲染(纯函数)
|
|
192
|
+
│ ├── settings.ts # settings 命名空间注册(base = Loader config)
|
|
193
|
+
│ ├── channels/ # system(node-notifier)、feishu(webhook+签名)
|
|
194
|
+
│ └── client/ # 浏览器端:sessions diff、Notification、设置分区、配置同步
|
|
195
|
+
│ ├── index.ts # 分区注册(动态 order)+ 浏览器通知 + 配置路由访问器
|
|
196
|
+
│ ├── section.tsx # 设置页「通知&信使」分区组件
|
|
197
|
+
│ ├── settings-form.tsx # 共享表单体(分组 + FieldRow + 操作栏)
|
|
198
|
+
│ ├── card-controller.ts # 表单控制器(纯逻辑,可单测)
|
|
199
|
+
│ ├── fetch-scope.ts # ScopeLike 的 fetch 适配层(配置路由)
|
|
200
|
+
│ ├── locales.ts # zh/en 字典(ctx.locale 注册)
|
|
201
|
+
│ ├── config.ts # 浏览器通知的配置句柄(走配置路由)
|
|
202
|
+
│ └── diff.ts # 会话摘要 diff(纯函数)
|
|
203
|
+
└── tests/ # vitest 单元测试
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 开发与测试 / Development
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
pnpm install
|
|
212
|
+
pnpm test # 单元测试:信号/模板/调度/飞书签名/配置解析/client diff/配置路由/fetch scope/字典一致性
|
|
213
|
+
pnpm typecheck # host 端类型检查
|
|
214
|
+
pnpm build # host tsc + client 声明 + client bundle(lib/)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 已知边界 / Known limitations
|
|
220
|
+
|
|
221
|
+
- 浏览器通知需站点权限;`onlyWhenHidden=false` 时页面可见也会弹;
|
|
222
|
+
- 多标签页经 localStorage 冷却去重,不同浏览器各自通知;
|
|
223
|
+
- 子代理结束不触发完成通知(仅根会话)——分叉会话视为顶层且会通知;
|
|
224
|
+
- 通道失败(webhook 超时、toast 不可用)只记日志,不影响其他通道与插件运行;
|
|
225
|
+
- 完成/交互去重状态为内存态,DSH 重启后重置。
|
|
226
|
+
|
|
227
|
+
### 系统通知(node-notifier)跨平台前提
|
|
228
|
+
|
|
229
|
+
`node-notifier` 在三个平台调用完全不同的底层程序:
|
|
230
|
+
|
|
231
|
+
| 平台 | 底层 | 前提条件 / 差异 |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| Windows | PowerShell ToastNotification | 内建,无需额外安装;`sound` 仅在 Windows 有可靠映射 |
|
|
234
|
+
| macOS | terminal-notifier | 首次使用需联网下载第三方二进制,且需登录图形会话(Dock 存在);`sound` 不生效 |
|
|
235
|
+
| Linux | notify-send(libnotify) | 需安装 `libnotify-bin`,并有运行中的通知守护进程(GNOME Shell / Plasma / mako / dunst 等);`sound` 不生效 |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 许可 / License
|
|
240
|
+
|
|
241
|
+
[MIT](LICENSE) © [ly6170](https://github.com/ly6170)
|
package/assets/icon.png
ADDED
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 分发包(bundle)的配置层:用户通过 `dsh plugin add ./dsh-messager` 安装后,
|
|
2
|
+
# 本层把插件行插入运行组合。name 使用包名解析(需要先 `pnpm build` 产出 lib/)。
|
|
3
|
+
# host 端由此行加载;client 端由 clientModules 扫描本包的 dsh.client 声明自动编入 Web bundle。
|
|
4
|
+
- insert:
|
|
5
|
+
- id: messager
|
|
6
|
+
name: dsh-messager
|
|
7
|
+
# 配置示例(可省略,schema 有默认值;设置页可覆盖用户层):
|
|
8
|
+
# config:
|
|
9
|
+
# feishu:
|
|
10
|
+
# enabled: true
|
|
11
|
+
# webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/<token>'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书机器人(webhook)通道:自定义机器人 webhook,interactive 卡片。
|
|
3
|
+
* 配置 secret 时按飞书规范签名(HMAC-SHA256,timestamp + sign 字段)。
|
|
4
|
+
* 纯函数(buildCardPayload / signFeishuPayload)可单测。
|
|
5
|
+
*/
|
|
6
|
+
import type { NotifyChannel, NotificationPayload } from '../notify.js';
|
|
7
|
+
export interface FeishuChannelOptions {
|
|
8
|
+
webhookUrl: string;
|
|
9
|
+
/** 签名密钥(飞书“安全设置-签名校验”)。 */
|
|
10
|
+
secret?: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
/** 飞书 interactive 卡片载荷(未签名)。 */
|
|
14
|
+
export interface FeishuCardPayload {
|
|
15
|
+
msg_type: 'interactive';
|
|
16
|
+
card: {
|
|
17
|
+
header: {
|
|
18
|
+
title: {
|
|
19
|
+
tag: 'plain_text';
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
template: 'green' | 'orange' | 'red';
|
|
23
|
+
};
|
|
24
|
+
elements: ({
|
|
25
|
+
tag: 'div';
|
|
26
|
+
text: {
|
|
27
|
+
tag: 'lark_md';
|
|
28
|
+
content: string;
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
tag: 'action';
|
|
32
|
+
actions: Array<{
|
|
33
|
+
tag: 'button';
|
|
34
|
+
text: {
|
|
35
|
+
tag: 'plain_text';
|
|
36
|
+
content: string;
|
|
37
|
+
};
|
|
38
|
+
url: string;
|
|
39
|
+
type: 'primary';
|
|
40
|
+
}>;
|
|
41
|
+
})[];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** 构建飞书卡片(纯函数)。 */
|
|
45
|
+
export declare function buildCardPayload(payload: NotificationPayload): FeishuCardPayload;
|
|
46
|
+
/**
|
|
47
|
+
* 按飞书签名规范签名:
|
|
48
|
+
* string_to_sign = `${timestamp}\n${secret}`,sign = base64(HMAC-SHA256(string_to_sign, ""))
|
|
49
|
+
* @param payload - 原始请求体。
|
|
50
|
+
* @param secret - 机器人签名密钥。
|
|
51
|
+
* @param timestamp - 秒级时间戳。
|
|
52
|
+
*/
|
|
53
|
+
export declare function signFeishuPayload<T extends object>(payload: T, secret: string, timestamp: string): T & {
|
|
54
|
+
timestamp: string;
|
|
55
|
+
sign: string;
|
|
56
|
+
};
|
|
57
|
+
export declare function createFeishuChannel(options: FeishuChannelOptions): NotifyChannel;
|
|
58
|
+
//# sourceMappingURL=feishu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feishu.d.ts","sourceRoot":"","sources":["../../src/channels/feishu.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEtE,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAWD,gCAAgC;AAChC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,aAAa,CAAA;IACvB,IAAI,EAAE;QACJ,MAAM,EAAE;YAAE,KAAK,EAAE;gBAAE,GAAG,EAAE,YAAY,CAAC;gBAAC,OAAO,EAAE,MAAM,CAAA;aAAE,CAAC;YAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;SAAE,CAAA;QAC/F,QAAQ,EAAE,CACN;YAAE,GAAG,EAAE,KAAK,CAAC;YAAC,IAAI,EAAE;gBAAE,GAAG,EAAE,SAAS,CAAC;gBAAC,OAAO,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,GACzD;YAAE,GAAG,EAAE,QAAQ,CAAC;YAAC,OAAO,EAAE,KAAK,CAAC;gBAAE,GAAG,EAAE,QAAQ,CAAC;gBAAC,IAAI,EAAE;oBAAE,GAAG,EAAE,YAAY,CAAC;oBAAC,OAAO,EAAE,MAAM,CAAA;iBAAE,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,SAAS,CAAA;aAAE,CAAC,CAAA;SAAE,CACnI,EAAE,CAAA;KACJ,CAAA;CACF;AAED,mBAAmB;AACnB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAwBhF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAChD,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAIzC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAuBhF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书机器人(webhook)通道:自定义机器人 webhook,interactive 卡片。
|
|
3
|
+
* 配置 secret 时按飞书规范签名(HMAC-SHA256,timestamp + sign 字段)。
|
|
4
|
+
* 纯函数(buildCardPayload / signFeishuPayload)可单测。
|
|
5
|
+
*/
|
|
6
|
+
import { createHmac } from 'node:crypto';
|
|
7
|
+
/** 卡片模板色:completed 绿 / interaction 橙 / error 红。 */
|
|
8
|
+
function cardTemplateOf(kind) {
|
|
9
|
+
switch (kind) {
|
|
10
|
+
case 'interaction': return 'orange';
|
|
11
|
+
case 'completed': return 'green';
|
|
12
|
+
case 'error': return 'red';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** 构建飞书卡片(纯函数)。 */
|
|
16
|
+
export function buildCardPayload(payload) {
|
|
17
|
+
return {
|
|
18
|
+
msg_type: 'interactive',
|
|
19
|
+
card: {
|
|
20
|
+
header: {
|
|
21
|
+
title: { tag: 'plain_text', content: payload.title },
|
|
22
|
+
template: cardTemplateOf(payload.kind),
|
|
23
|
+
},
|
|
24
|
+
elements: [
|
|
25
|
+
...(payload.body === ''
|
|
26
|
+
? []
|
|
27
|
+
: [{ tag: 'div', text: { tag: 'lark_md', content: payload.body } }]),
|
|
28
|
+
{
|
|
29
|
+
tag: 'action',
|
|
30
|
+
actions: [{
|
|
31
|
+
tag: 'button',
|
|
32
|
+
text: { tag: 'plain_text', content: '打开 DSH' },
|
|
33
|
+
url: payload.url,
|
|
34
|
+
type: 'primary',
|
|
35
|
+
}],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 按飞书签名规范签名:
|
|
43
|
+
* string_to_sign = `${timestamp}\n${secret}`,sign = base64(HMAC-SHA256(string_to_sign, ""))
|
|
44
|
+
* @param payload - 原始请求体。
|
|
45
|
+
* @param secret - 机器人签名密钥。
|
|
46
|
+
* @param timestamp - 秒级时间戳。
|
|
47
|
+
*/
|
|
48
|
+
export function signFeishuPayload(payload, secret, timestamp) {
|
|
49
|
+
const stringToSign = `${timestamp}\n${secret}`;
|
|
50
|
+
const sign = createHmac('sha256', stringToSign).update('').digest('base64');
|
|
51
|
+
return { ...payload, timestamp, sign };
|
|
52
|
+
}
|
|
53
|
+
export function createFeishuChannel(options) {
|
|
54
|
+
return {
|
|
55
|
+
id: 'feishu',
|
|
56
|
+
async send(payload) {
|
|
57
|
+
const card = buildCardPayload(payload);
|
|
58
|
+
const body = options.secret === undefined
|
|
59
|
+
? card
|
|
60
|
+
: signFeishuPayload(card, options.secret, String(Math.floor(Date.now() / 1000)));
|
|
61
|
+
const response = await fetch(options.webhookUrl, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'content-type': 'application/json' },
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`feishu webhook responded ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
const result = (await response.json().catch(() => undefined));
|
|
71
|
+
if (result !== undefined && result.code !== 0) {
|
|
72
|
+
throw new Error(`feishu webhook rejected: code=${String(result.code)} msg=${String(result.msg)}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=feishu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feishu.js","sourceRoot":"","sources":["../../src/channels/feishu.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统通知通道:OS 级 toast(Windows / macOS / Linux),经 node-notifier 投递。
|
|
3
|
+
*
|
|
4
|
+
* node-notifier 在三个平台调用的是完全不同的底层程序:
|
|
5
|
+
* - Windows:PowerShell ToastNotification(内建,无需额外安装);
|
|
6
|
+
* - macOS:terminal-notifier(第三方二进制,首次使用需联网下载,且需登录图形会话);
|
|
7
|
+
* - Linux:notify-send(libnotify,需 libnotify-bin 与一个运行中的通知守护进程)。
|
|
8
|
+
*
|
|
9
|
+
* 因此同一份载荷落在不同平台上语义不一致,这里按平台分流:
|
|
10
|
+
* - sound 只在 Windows 有可靠映射(macOS/Linux 的底层不支持内建声音,强制传可能
|
|
11
|
+
* 引起差异,故仅在 win32 传递);
|
|
12
|
+
* - icon 必须是存在的文件路径:Linux 的 notify-send 与 macOS 的 terminal-notifier
|
|
13
|
+
* 对缺失/非法路径的处理与 Windows 不同,可能直接失败而非降级,故先做存在性校验,
|
|
14
|
+
* 无效时降级为不带图标;
|
|
15
|
+
* - 发送失败时尝试一次「去掉图标」的重试(很多 Linux/macOS 失败由图标引起),
|
|
16
|
+
* 再交给调度层记录。
|
|
17
|
+
*
|
|
18
|
+
* 失败最终由调度层兜底记录(notify.ts 的 logWarn),不抛出到事件监听器。
|
|
19
|
+
*/
|
|
20
|
+
import type { NotifyChannel } from '../notify.js';
|
|
21
|
+
export interface SystemChannelOptions {
|
|
22
|
+
/** 图标绝对路径(node-notifier 需要文件路径且该文件必须存在)。 */
|
|
23
|
+
icon?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createSystemChannel(options?: SystemChannelOptions): NotifyChannel;
|
|
26
|
+
//# sourceMappingURL=system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/channels/system.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAuB,MAAM,cAAc,CAAA;AAEtE,MAAM,WAAW,oBAAoB;IACnC,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAiDD,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAiBrF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统通知通道:OS 级 toast(Windows / macOS / Linux),经 node-notifier 投递。
|
|
3
|
+
*
|
|
4
|
+
* node-notifier 在三个平台调用的是完全不同的底层程序:
|
|
5
|
+
* - Windows:PowerShell ToastNotification(内建,无需额外安装);
|
|
6
|
+
* - macOS:terminal-notifier(第三方二进制,首次使用需联网下载,且需登录图形会话);
|
|
7
|
+
* - Linux:notify-send(libnotify,需 libnotify-bin 与一个运行中的通知守护进程)。
|
|
8
|
+
*
|
|
9
|
+
* 因此同一份载荷落在不同平台上语义不一致,这里按平台分流:
|
|
10
|
+
* - sound 只在 Windows 有可靠映射(macOS/Linux 的底层不支持内建声音,强制传可能
|
|
11
|
+
* 引起差异,故仅在 win32 传递);
|
|
12
|
+
* - icon 必须是存在的文件路径:Linux 的 notify-send 与 macOS 的 terminal-notifier
|
|
13
|
+
* 对缺失/非法路径的处理与 Windows 不同,可能直接失败而非降级,故先做存在性校验,
|
|
14
|
+
* 无效时降级为不带图标;
|
|
15
|
+
* - 发送失败时尝试一次「去掉图标」的重试(很多 Linux/macOS 失败由图标引起),
|
|
16
|
+
* 再交给调度层记录。
|
|
17
|
+
*
|
|
18
|
+
* 失败最终由调度层兜底记录(notify.ts 的 logWarn),不抛出到事件监听器。
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import notifier from 'node-notifier';
|
|
22
|
+
/** 平台分立的载荷拼接:sound / icon 语义随平台、底层程序不同。 */
|
|
23
|
+
function buildOptions(payload, rawIcon) {
|
|
24
|
+
// 图标只在存在时传入;不存在时整段省略,避免 Linux/macOS 直接报错。
|
|
25
|
+
const icon = rawIcon !== undefined && rawIcon !== '' && existsSync(rawIcon) ? rawIcon : undefined;
|
|
26
|
+
const common = {
|
|
27
|
+
title: payload.title,
|
|
28
|
+
...(payload.body === '' ? {} : { message: payload.body }),
|
|
29
|
+
...(icon === undefined ? {} : { icon }),
|
|
30
|
+
wait: false,
|
|
31
|
+
};
|
|
32
|
+
switch (process.platform) {
|
|
33
|
+
case 'win32':
|
|
34
|
+
// Windows Toast 对 sound 有完整支持。
|
|
35
|
+
return { ...common, sound: true };
|
|
36
|
+
case 'darwin':
|
|
37
|
+
case 'linux':
|
|
38
|
+
default:
|
|
39
|
+
// macOS 用 terminal-notifier,Linux 用 notify-send:均无内建可靠声音参数。
|
|
40
|
+
return common;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function deliver(options) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
notifier.notify(options, (error) => {
|
|
46
|
+
if (error !== null)
|
|
47
|
+
reject(error);
|
|
48
|
+
else
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function createSystemChannel(options = {}) {
|
|
54
|
+
return {
|
|
55
|
+
id: 'system',
|
|
56
|
+
async send(payload) {
|
|
57
|
+
try {
|
|
58
|
+
await deliver(buildOptions(payload, options.icon));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// 降级重试:去掉图标再发一次,兜住由 icon 路径引发的 Linux/macOS 失败。
|
|
62
|
+
// 若仍失败则抛出,由调度层记录;不在此处吞掉也不抛出到事件监听器。
|
|
63
|
+
try {
|
|
64
|
+
await deliver(buildOptions(payload, undefined));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.js","sourceRoot":"","sources":["../../src/channels/system.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,QAAQ,MAAM,eAAe,CAAA;AAqBpC,2CAA2C;AAC3C,SAAS,YAAY,CACnB,OAA4B,EAC5B,OAA2B;IAE3B,0CAA0C;IAC1C,MAAM,IAAI,GAAG,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;IACjG,MAAM,MAAM,GAAuC;QACjD,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;QACzD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACvC,IAAI,EAAE,KAAK;KACZ,CAAA;IACD,QAAQ,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,KAAK,OAAO;YACV,+BAA+B;YAC/B,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;QACnC,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO,CAAC;QACb;YACE,4DAA4D;YAC5D,OAAO,MAAM,CAAA;IACjB,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,OAA4B;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjC,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,CAAC,KAAK,CAAC,CAAA;;gBAC5B,OAAO,EAAE,CAAA;QAChB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAgC,EAAE;IACpE,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,KAAK,CAAC,IAAI,CAAC,OAA4B;YACrC,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,+CAA+C;gBAC/C,mCAAmC;gBACnC,IAAI,CAAC;oBACH,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;gBACjD,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,KAAK,CAAA;gBACb,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|