dsh-email 0.5.0 → 0.6.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.
- package/README.en.md +128 -0
- package/README.md +140 -128
- package/lib/client.js +13 -0
- package/lib/config.d.ts +10 -0
- package/lib/config.js +35 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/settings.d.ts +3 -0
- package/lib/settings.js +3 -0
- package/lib/web.js +8 -0
- package/package.json +4 -3
package/README.en.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# dsh-email
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Email tools for DeepSeek Harness: let the agent **check the inbox, read mail, search mail, send mail on your behalf, and send/receive attachments** through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud. Pure plugin implementation, zero core changes, works out of the box.
|
|
6
|
+
|
|
7
|
+
Pure Node, **cross-platform** (one codebase for Windows / macOS / Linux), no shell, no native binaries.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `email_list` | List the newest mail in a folder (unread filter, pagination, summaries only, no body) |
|
|
14
|
+
| `email_read` | Read one message's full text by uid (HTML auto-converted to plain text, oversized bodies truncated) |
|
|
15
|
+
| `email_search` | Search sender/recipient/subject by keyword (server-side); with no results, falls back to a body scan of the most recent 30 messages by default |
|
|
16
|
+
| `email_send` | Send mail on your behalf (attachments supported). **Prompts for confirmation before sending by default**, showing recipients, subject and attachment count; only sends after you approve |
|
|
17
|
+
| `email_folders` | List the mailbox folders (INBOX/Sent/Junk/custom…); feed the `path` to other tools |
|
|
18
|
+
| `email_attachment` | Download an attachment by index (saved to the session workspace by default so the model can read it directly; size capped by `maxAttachmentBytes`) |
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
|
|
22
|
+
> Check the 10 newest unread messages in my QQ mailbox and list the ones that need a reply.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add dsh-email
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
(Or install from GitHub: `dsh plugin --profile web add github:your-account/dsh-email#<commit>`, then follow the prompt to authorize the `prepare` build in the profile's `pnpm-workspace.yaml`.)
|
|
31
|
+
|
|
32
|
+
After installing, restart `dsh web`. The plugin ships with an empty config and **won't crash startup**; calling any email tool before configuration returns a clear configuration hint.
|
|
33
|
+
|
|
34
|
+
**Two configuration methods (pick one):**
|
|
35
|
+
|
|
36
|
+
1. **Web settings (recommended)**: after restart, open **Settings → Mail (dsh-email)**, fill in the email address and authorization code, and click "Save & Apply"; a "Test connection" button is also provided. Zero YAML, zero restart.
|
|
37
|
+
2. **YAML**: hand-write the cordis.patch.yml template below; the settings page's "Multiple accounts (advanced, YAML)" textbox can also hold the account map (overriding `accounts` in YAML).
|
|
38
|
+
|
|
39
|
+
Values saved in the settings page live in the `dsh-email` namespace of `settings.yaml` and override the YAML default-account config; password fields are marked secret (they never appear in exports or diagnostics).
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
In your profile's `cordis.patch.yml` (under `$DSH_HOME/profiles/<name>/`), override the `tool-email` line, then restart:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
- id: tool-email
|
|
47
|
+
config:
|
|
48
|
+
provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
|
|
49
|
+
user: you@qq.com
|
|
50
|
+
password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
No preset needed? Hand-write any IMAP/SMTP server:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- id: tool-email
|
|
57
|
+
config:
|
|
58
|
+
user: you@corp.example
|
|
59
|
+
password: 你的授权码
|
|
60
|
+
imap: { host: imap.corp.example, port: 993, secure: true }
|
|
61
|
+
smtp: { host: smtp.corp.example, port: 465, secure: true }
|
|
62
|
+
inboxFolder: INBOX
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Multiple accounts: one `tool-email` line can hold several mailboxes; select one with the `account` argument when calling a tool:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
- id: tool-email
|
|
69
|
+
config:
|
|
70
|
+
accounts:
|
|
71
|
+
work: { provider: qq, user: work@qq.com, password: 授权码1 }
|
|
72
|
+
home: { provider: '163', user: home@163.com, password: 授权码2 }
|
|
73
|
+
defaultAccount: work # 省略 account 参数时用这个
|
|
74
|
+
downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Top-level `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` remain available as shared defaults for all accounts (the v0.1 single-account style stays fully compatible).
|
|
78
|
+
|
|
79
|
+
## Presets
|
|
80
|
+
|
|
81
|
+
| provider | IMAP | SMTP |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
|
|
84
|
+
| `163` | imap.163.com:993 | smtp.163.com:465 |
|
|
85
|
+
| `126` | imap.126.com:993 | smtp.126.com:465 |
|
|
86
|
+
| `sina` | imap.sina.com:993 | smtp.sina.com:465 |
|
|
87
|
+
| `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
|
|
88
|
+
| `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
|
|
89
|
+
| `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
|
|
90
|
+
| `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
|
|
91
|
+
|
|
92
|
+
## Getting an authorization code
|
|
93
|
+
|
|
94
|
+
Every provider requires an authorization code / app-specific password instead of your login password:
|
|
95
|
+
|
|
96
|
+
- **QQ Mail**: Settings → Account → enable IMAP/SMTP → generate authorization code
|
|
97
|
+
- **163/126**: Settings → POP3/SMTP/IMAP → enable → add authorization code
|
|
98
|
+
- **Gmail**: enable 2-Step Verification → Security → App passwords
|
|
99
|
+
- **Outlook**: Microsoft account security → App password (some accounts need 2-step verification first)
|
|
100
|
+
|
|
101
|
+
## Security
|
|
102
|
+
|
|
103
|
+
- **The authorization code is the key to your mailbox.** It lives on this machine (`cordis.patch.yml` or `settings.yaml` in the profile); never commit it to any Git repo; prefer the `DSH_EMAIL_PASSWORD` env var.
|
|
104
|
+
- `email_send` goes through the DSH approval channel by default: every send shows "send mail to xx, subject "xx"" and only sends after you approve. Environments without an approval channel (e.g. headless with no UI) **refuse to send outright** — that is the secure default.
|
|
105
|
+
- When the session is in **Full Access** mode, the harness approval policy is never (no confirmation dialogs) — `email_send` is **blocked with a clear hint**. Two ways out: ① switch the access mode back to Read Only / Write; ② turn off `sendApproval` (uncheck "Confirm before sending" in the settings page), explicitly declaring you accept the risk.
|
|
106
|
+
- This plugin performs no outbound telemetry; credentials are used in memory only to connect to your mail servers.
|
|
107
|
+
|
|
108
|
+
## Known limitations (v0.6)
|
|
109
|
+
|
|
110
|
+
- **Connection reuse**: IMAP is pooled per account (idle connections auto-recycled); SMTP uses nodemailer's connection pool. Concurrent calls for the same account queue serially (one connection serves one operation at a time — intentional).
|
|
111
|
+
- **Multiple accounts**: each account has its own connection pool; one `tool-email` line can hold any number of accounts. The settings page's "Multiple accounts (advanced, YAML)" textbox can directly edit the map (including a `defaultAccount` key), overriding `accounts` in cordis.patch.yml.
|
|
112
|
+
- **Attachment download**: `email_attachment` locates attachments from `email_read`'s attachment list (matching by filename first, then type+size to the IMAP part; a failed match errors instead of downloading the wrong file); inline images aren't downloadable yet; filenames are sanitized against path traversal, existing names get a suffix, and size is capped by `maxAttachmentBytes`.
|
|
113
|
+
- **No OAuth2**: enterprise environments that force OAuth (some M365/Google Workspace) aren't usable yet.
|
|
114
|
+
- **Body search uses client-side fallback**: most servers (e.g. QQ) have unreliable IMAP `TEXT`/`HEADER` search, so the server side only searches subject/sender/recipient; with no results it falls back to a body scan of the most recent `bodySearchLimit` messages (slower; disable with `bodySearchFallback`).
|
|
115
|
+
- **Password storage form**: the authorization code saved in the settings page is written in plaintext to the local `settings.yaml` (the schema's secret mark only keeps it out of logs/exports/diagnostics; no disk encryption). Don't hand settings.yaml to untrusted people.
|
|
116
|
+
- **Settings page vs. plugin-set changes**: saving in the settings page takes effect **immediately** (live), no restart needed; but upgrading or adding/removing plugins (composition-tree changes) still requires restarting `dsh web`.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
pnpm install
|
|
122
|
+
pnpm run build # tsc → lib/
|
|
123
|
+
pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
|
package/README.md
CHANGED
|
@@ -1,148 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
[English](README.en.md)
|
|
2
|
+
|
|
3
|
+
# dsh-email
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DeepSeek Harness 邮件工具插件:让 agent 能**查收件箱、读邮件、搜邮件、代发邮件、收发附件**。纯插件实现,零核心改动,安装即可用。
|
|
9
|
+
|
|
5
10
|
Email tools for DeepSeek Harness: list, read, search and send mail through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud.
|
|
6
11
|
|
|
7
|
-
纯 Node 实现,**全平台通用**(Windows / macOS / Linux 同一份代码),不依赖 shell、无原生二进制。
|
|
8
|
-
|
|
9
|
-
## 工具一览
|
|
10
|
-
|
|
11
|
-
| 工具 | 作用 |
|
|
12
|
-
|---|---|
|
|
13
|
-
| `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
|
|
14
|
-
| `email_read` | 按 uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
|
|
15
|
-
| `email_search` | 按关键词搜索发件人/收件人/主题(服务器端);无结果时默认回退到最近 30 封的正文扫描 |
|
|
12
|
+
纯 Node 实现,**全平台通用**(Windows / macOS / Linux 同一份代码),不依赖 shell、无原生二进制。
|
|
13
|
+
|
|
14
|
+
## 工具一览
|
|
15
|
+
|
|
16
|
+
| 工具 | 作用 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
|
|
19
|
+
| `email_read` | 按 uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
|
|
20
|
+
| `email_search` | 按关键词搜索发件人/收件人/主题(服务器端);无结果时默认回退到最近 30 封的正文扫描 |
|
|
16
21
|
| `email_send` | 代发邮件(支持带附件)。**默认发信前会弹确认**,显示收件人、主题和附件数,由你批准后才发出 |
|
|
17
22
|
| `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
|
|
18
|
-
| `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
|
|
19
|
-
|
|
20
|
-
示例对话:
|
|
21
|
-
|
|
22
|
-
> 帮我看下 QQ 邮箱最新的 10 封未读,把要回复的列出来。
|
|
23
|
-
|
|
24
|
-
## 安装
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
dsh plugin --profile web add dsh-email
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
(或从 GitHub 安装:`dsh plugin --profile web add github:你的账号/dsh-email#<commit>`,随后按提示在 profile 的 `pnpm-workspace.yaml` 里授权 `prepare` 构建。)
|
|
31
|
-
|
|
23
|
+
| `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
|
|
24
|
+
|
|
25
|
+
示例对话:
|
|
26
|
+
|
|
27
|
+
> 帮我看下 QQ 邮箱最新的 10 封未读,把要回复的列出来。
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile web add dsh-email
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(或从 GitHub 安装:`dsh plugin --profile web add github:你的账号/dsh-email#<commit>`,随后按提示在 profile 的 `pnpm-workspace.yaml` 里授权 `prepare` 构建。)
|
|
36
|
+
|
|
32
37
|
装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
|
|
33
38
|
|
|
34
39
|
**配置方式有两种(任选其一):**
|
|
35
40
|
|
|
36
41
|
1. **网页设置(推荐)**:重启后打开 **设置 → 邮件 (dsh-email)**,表单里填邮箱地址和授权码,点「保存并应用」,还带「测试连接」按钮。零 YAML、零重启。
|
|
37
|
-
2. **YAML**:按下面的 cordis.patch.yml
|
|
38
|
-
|
|
39
|
-
设置页保存的值存在 `settings.yaml` 的 `dsh-email` 命名空间里,覆盖 YAML 的默认账号配置;密码字段标记为 secret(不会出现在任何导出/诊断里)。
|
|
40
|
-
|
|
41
|
-
## 配置
|
|
42
|
-
|
|
43
|
-
在你 profile 的 `cordis.patch.yml` 里覆盖 `tool-email` 行(在 `$DSH_HOME/profiles/<name>/` 下),然后重启:
|
|
44
|
-
|
|
45
|
-
```yaml
|
|
46
|
-
- id: tool-email
|
|
47
|
-
config:
|
|
48
|
-
provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
|
|
49
|
-
user: you@qq.com
|
|
50
|
-
password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
不需要预设?手填任意 IMAP/SMTP 服务器即可:
|
|
54
|
-
|
|
55
|
-
```yaml
|
|
56
|
-
- id: tool-email
|
|
57
|
-
config:
|
|
58
|
-
user: you@corp.example
|
|
59
|
-
password: 你的授权码
|
|
60
|
-
imap: { host: imap.corp.example, port: 993, secure: true }
|
|
61
|
-
smtp: { host: smtp.corp.example, port: 465, secure: true }
|
|
62
|
-
inboxFolder: INBOX
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
|
|
66
|
-
|
|
67
|
-
```yaml
|
|
68
|
-
- id: tool-email
|
|
69
|
-
config:
|
|
70
|
-
accounts:
|
|
71
|
-
work: { provider: qq, user: work@qq.com, password: 授权码1 }
|
|
72
|
-
home: { provider: '163', user: home@163.com, password: 授权码2 }
|
|
73
|
-
defaultAccount: work # 省略 account 参数时用这个
|
|
74
|
-
downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍然可用,作为各账号的共享默认值(v0.1 单账号写法完全兼容)。
|
|
78
|
-
|
|
79
|
-
### 常用邮箱预设
|
|
80
|
-
|
|
81
|
-
| provider | IMAP | SMTP |
|
|
82
|
-
|---|---|---|
|
|
83
|
-
| `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
|
|
84
|
-
| `163` | imap.163.com:993 | smtp.163.com:465 |
|
|
85
|
-
| `126` | imap.126.com:993 | smtp.126.com:465 |
|
|
86
|
-
| `sina` | imap.sina.com:993 | smtp.sina.com:465 |
|
|
87
|
-
| `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
|
|
88
|
-
| `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
|
|
89
|
-
| `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
|
|
90
|
-
| `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
|
|
91
|
-
|
|
92
|
-
### 完整配置项
|
|
93
|
-
|
|
94
|
-
| 字段 | 默认 | 说明 |
|
|
95
|
-
|---|---|---|
|
|
96
|
-
| `provider` | 无 | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
|
|
97
|
-
| `user` | 必填 | 登录邮箱地址 |
|
|
98
|
-
| `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
|
|
99
|
-
| `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
|
|
100
|
-
| `smtp.host/port/secure` | 按预设 | 发信服务器 |
|
|
101
|
-
| `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
|
|
102
|
-
| `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
|
|
42
|
+
2. **YAML**:按下面的 cordis.patch.yml 模板手写;设置页的「多账号(高级,YAML)」文本框也能填账号映射(覆盖 YAML 里的 accounts)。
|
|
43
|
+
|
|
44
|
+
设置页保存的值存在 `settings.yaml` 的 `dsh-email` 命名空间里,覆盖 YAML 的默认账号配置;密码字段标记为 secret(不会出现在任何导出/诊断里)。
|
|
45
|
+
|
|
46
|
+
## 配置
|
|
47
|
+
|
|
48
|
+
在你 profile 的 `cordis.patch.yml` 里覆盖 `tool-email` 行(在 `$DSH_HOME/profiles/<name>/` 下),然后重启:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
- id: tool-email
|
|
52
|
+
config:
|
|
53
|
+
provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
|
|
54
|
+
user: you@qq.com
|
|
55
|
+
password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
不需要预设?手填任意 IMAP/SMTP 服务器即可:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
- id: tool-email
|
|
62
|
+
config:
|
|
63
|
+
user: you@corp.example
|
|
64
|
+
password: 你的授权码
|
|
65
|
+
imap: { host: imap.corp.example, port: 993, secure: true }
|
|
66
|
+
smtp: { host: smtp.corp.example, port: 465, secure: true }
|
|
67
|
+
inboxFolder: INBOX
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
- id: tool-email
|
|
74
|
+
config:
|
|
75
|
+
accounts:
|
|
76
|
+
work: { provider: qq, user: work@qq.com, password: 授权码1 }
|
|
77
|
+
home: { provider: '163', user: home@163.com, password: 授权码2 }
|
|
78
|
+
defaultAccount: work # 省略 account 参数时用这个
|
|
79
|
+
downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍然可用,作为各账号的共享默认值(v0.1 单账号写法完全兼容)。
|
|
83
|
+
|
|
84
|
+
### 常用邮箱预设
|
|
85
|
+
|
|
86
|
+
| provider | IMAP | SMTP |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
|
|
89
|
+
| `163` | imap.163.com:993 | smtp.163.com:465 |
|
|
90
|
+
| `126` | imap.126.com:993 | smtp.126.com:465 |
|
|
91
|
+
| `sina` | imap.sina.com:993 | smtp.sina.com:465 |
|
|
92
|
+
| `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
|
|
93
|
+
| `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
|
|
94
|
+
| `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
|
|
95
|
+
| `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
|
|
96
|
+
|
|
97
|
+
### 完整配置项
|
|
98
|
+
|
|
99
|
+
| 字段 | 默认 | 说明 |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `provider` | 无 | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
|
|
102
|
+
| `user` | 必填 | 登录邮箱地址 |
|
|
103
|
+
| `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
|
|
104
|
+
| `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
|
|
105
|
+
| `smtp.host/port/secure` | 按预设 | 发信服务器 |
|
|
106
|
+
| `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
|
|
107
|
+
| `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
|
|
103
108
|
| `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
|
|
104
109
|
| `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
|
|
110
|
+
| `accountsYaml` | 无 | 设置页「多账号(高级)」文本框的 YAML 文本;非空时覆盖 accounts |
|
|
105
111
|
| `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
|
|
106
112
|
| `downloadDir` | 会话工作区下 .dsh-email-downloads(回退 $DSH_HOME/email-downloads) | email_attachment 的落盘目录;显式设置后固定 |
|
|
107
113
|
| `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
|
|
108
114
|
| `idleTimeoutMs` | `60000` | IMAP 空闲连接回收时间(连接复用,连续操作更快) |
|
|
109
115
|
| `bodySearchFallback` | `true` | 服务器搜索无结果时,回退到客户端扫描最近邮件的正文 |
|
|
110
|
-
| `bodySearchLimit` | `30` | 正文回退扫描的邮件数量(5-200) |
|
|
111
|
-
|
|
112
|
-
## 第一步:拿到授权码
|
|
113
|
-
|
|
114
|
-
各邮箱都要求用「授权码/应用专用密码」而不是登录密码:
|
|
115
|
-
|
|
116
|
-
- **QQ 邮箱**:设置 → 账户 → 开启 IMAP/SMTP 服务 → 生成授权码
|
|
117
|
-
- **163/126**:设置 → POP3/SMTP/IMAP → 开启 → 新增授权码
|
|
118
|
-
- **Gmail**:开启两步验证 → 安全 → 应用专用密码
|
|
119
|
-
- **Outlook**:Microsoft 账户安全 → 应用密码(部分账号需先开两步验证)
|
|
120
|
-
|
|
121
|
-
## 安全须知
|
|
122
|
-
|
|
123
|
-
- **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` 或 `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
|
|
116
|
+
| `bodySearchLimit` | `30` | 正文回退扫描的邮件数量(5-200) |
|
|
117
|
+
|
|
118
|
+
## 第一步:拿到授权码
|
|
119
|
+
|
|
120
|
+
各邮箱都要求用「授权码/应用专用密码」而不是登录密码:
|
|
121
|
+
|
|
122
|
+
- **QQ 邮箱**:设置 → 账户 → 开启 IMAP/SMTP 服务 → 生成授权码
|
|
123
|
+
- **163/126**:设置 → POP3/SMTP/IMAP → 开启 → 新增授权码
|
|
124
|
+
- **Gmail**:开启两步验证 → 安全 → 应用专用密码
|
|
125
|
+
- **Outlook**:Microsoft 账户安全 → 应用密码(部分账号需先开两步验证)
|
|
126
|
+
|
|
127
|
+
## 安全须知
|
|
128
|
+
|
|
129
|
+
- **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` 或 `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
|
|
124
130
|
- `email_send` 默认走 DSH 审批通道:每次发信都显示「发送邮件给 xx,主题「xx」」,你批准才发出。没有审批通道的环境(如无 UI 的 headless)会**直接拒绝发信**,这是安全默认。
|
|
125
|
-
- 会话处于 **Full Access(完全访问)** 模式时,harness 的审批策略是 never(不弹任何确认框)——`email_send` 会**被拦截并给出明确提示**。两条出路:① 把访问模式切回 Read Only / Write;② 关闭 `sendApproval`(设置页勾掉「发信前确认」),即显式声明自行承担风险。
|
|
126
|
-
- 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
|
|
127
|
-
|
|
128
|
-
## 已知限制(v0.3)
|
|
129
|
-
|
|
130
|
-
- **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
|
|
131
|
-
- **多账号**:每个账号独立连接池;一个 `tool-email`
|
|
132
|
-
- **附件下载**:email_attachment 按 email_read 的附件列表定位(先按文件名、再按类型+大小匹配到 IMAP 部件,定位失败会报错而不是下载错文件);内嵌图片暂不支持下载;文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
|
|
133
|
-
- **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
|
|
131
|
+
- 会话处于 **Full Access(完全访问)** 模式时,harness 的审批策略是 never(不弹任何确认框)——`email_send` 会**被拦截并给出明确提示**。两条出路:① 把访问模式切回 Read Only / Write;② 关闭 `sendApproval`(设置页勾掉「发信前确认」),即显式声明自行承担风险。
|
|
132
|
+
- 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
|
|
133
|
+
|
|
134
|
+
## 已知限制(v0.3)
|
|
135
|
+
|
|
136
|
+
- **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
|
|
137
|
+
- **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页的「多账号(高级,YAML)」文本框可直接编辑映射(可含 defaultAccount 键),覆盖 cordis.patch.yml 的 accounts。
|
|
138
|
+
- **附件下载**:email_attachment 按 email_read 的附件列表定位(先按文件名、再按类型+大小匹配到 IMAP 部件,定位失败会报错而不是下载错文件);内嵌图片暂不支持下载;文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
|
|
139
|
+
- **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
|
|
134
140
|
- 正文搜索走客户端回退:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索不可靠,所以服务器端只搜主题/发件人/收件人;无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可关 `bodySearchFallback`)。
|
|
135
141
|
- **密码落盘形式**:设置页保存的授权码以明文写在本机 `settings.yaml`(schema 标记 secret 只是保证它不进日志/导出/诊断,不做磁盘加密)。请勿把 settings.yaml 交给不信任的人。
|
|
136
|
-
- **设置页与插件集变更**:设置页保存后工具**立即生效**(live),无需重启;但升级/增删插件(组合树变化)仍需重启 `dsh web`。
|
|
137
|
-
|
|
138
|
-
## 开发
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
pnpm install
|
|
142
|
-
pnpm run build # tsc → lib/
|
|
143
|
-
pnpm test # 构建 + node --test(配置/解析/注册与审批门,
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## 协议
|
|
147
|
-
|
|
142
|
+
- **设置页与插件集变更**:设置页保存后工具**立即生效**(live),无需重启;但升级/增删插件(组合树变化)仍需重启 `dsh web`。
|
|
143
|
+
|
|
144
|
+
## 开发
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
pnpm install
|
|
148
|
+
pnpm run build # tsc → lib/
|
|
149
|
+
pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 协议
|
|
153
|
+
|
|
148
154
|
MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
|
|
155
|
+
|
|
156
|
+
## 相关插件
|
|
157
|
+
|
|
158
|
+
- [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
|
|
159
|
+
- [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
|
|
160
|
+
- [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件六件套 + Web 设置页
|
package/lib/client.js
CHANGED
|
@@ -45,6 +45,7 @@ const EMPTY = {
|
|
|
45
45
|
inboxFolder: "INBOX",
|
|
46
46
|
sendApproval: true,
|
|
47
47
|
downloadDir: "",
|
|
48
|
+
accountsYaml: "",
|
|
48
49
|
imap: { host: "", port: 993, secure: true },
|
|
49
50
|
smtp: { host: "", port: 465, secure: true },
|
|
50
51
|
};
|
|
@@ -227,6 +228,18 @@ function EmailSettingsSection() {
|
|
|
227
228
|
h("label", null, "附件下载目录(默认 $DSH_HOME/email-downloads)"),
|
|
228
229
|
fieldInput("text", draft.downloadDir, (v) => update({ downloadDir: v }), "留空使用默认"),
|
|
229
230
|
]),
|
|
231
|
+
h("details", { className: "dshe-details" }, [
|
|
232
|
+
h("summary", null, "多账号(高级,YAML)"),
|
|
233
|
+
h("div", { className: "dshe-field", style: { marginTop: 10 } }, [
|
|
234
|
+
h("textarea", {
|
|
235
|
+
rows: 7,
|
|
236
|
+
value: draft.accountsYaml || "",
|
|
237
|
+
placeholder: "work: { provider: qq, user: work@qq.com, password: 授权码1 }\nhome: { provider: '163', user: home@163.com, password: 授权码2 }\ndefaultAccount: work",
|
|
238
|
+
onChange: (e) => update({ accountsYaml: e.target.value }),
|
|
239
|
+
}),
|
|
240
|
+
h("div", { className: "dshe-hint" }, "账号名 -> 配置的映射,可含 defaultAccount 键。填了这里会覆盖 cordis.patch.yml 里的 accounts 映射;工具调用时用 account 参数选账号。"),
|
|
241
|
+
]),
|
|
242
|
+
]),
|
|
230
243
|
h("details", { className: "dshe-details" }, [
|
|
231
244
|
h("summary", null, "高级:自定义服务器(选了预设可留空)"),
|
|
232
245
|
h("div", { className: "dshe-grid", style: { marginTop: 10 } }, [
|
package/lib/config.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface EmailConfig extends AccountConfig {
|
|
|
27
27
|
maxBodyChars?: number;
|
|
28
28
|
/** Named accounts. Account-level fields override the top-level shorthand. */
|
|
29
29
|
accounts?: Record<string, AccountConfig>;
|
|
30
|
+
/** YAML text of the accounts map, editable from the settings page. Wins over accounts when non-empty. */
|
|
31
|
+
accountsYaml?: string;
|
|
30
32
|
/** Which account tools use when the call omits account. Required with 2+ accounts. */
|
|
31
33
|
defaultAccount?: string;
|
|
32
34
|
/** Directory email_attachment writes into. Default: the session workspace's .dsh-email-downloads (falls back to $DSH_HOME/email-downloads). */
|
|
@@ -86,6 +88,14 @@ export interface ResolvedEmailSettings {
|
|
|
86
88
|
bodySearchLimit: number;
|
|
87
89
|
}
|
|
88
90
|
export declare function defaultDownloadDir(): string;
|
|
91
|
+
/**
|
|
92
|
+
* Parse the settings-page accounts YAML: an object map (name -> account),
|
|
93
|
+
* optionally with a reserved string key defaultAccount that is extracted.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseAccountsYaml(text: string): {
|
|
96
|
+
map: Record<string, AccountConfig>;
|
|
97
|
+
defaultAccount?: string;
|
|
98
|
+
};
|
|
89
99
|
/**
|
|
90
100
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
91
101
|
* (in Chinese, since it is what the user and the model both read) when the
|
package/lib/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
export const PROVIDER_PRESETS = {
|
|
4
5
|
qq: { imap: { host: 'imap.qq.com', port: 993, secure: true }, smtp: { host: 'smtp.qq.com', port: 465, secure: true } },
|
|
@@ -18,6 +19,33 @@ export function defaultDownloadDir() {
|
|
|
18
19
|
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
19
20
|
return join(home, 'email-downloads');
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse the settings-page accounts YAML: an object map (name -> account),
|
|
24
|
+
* optionally with a reserved string key defaultAccount that is extracted.
|
|
25
|
+
*/
|
|
26
|
+
export function parseAccountsYaml(text) {
|
|
27
|
+
let doc;
|
|
28
|
+
try {
|
|
29
|
+
doc = parseYaml(text);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error('dsh-email:accountsYaml 不是合法的 YAML:' + (error instanceof Error ? error.message : String(error)));
|
|
33
|
+
}
|
|
34
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
35
|
+
throw new Error('dsh-email:accountsYaml 必须是一个对象映射(账号名 -> 账号配置),例如 work: { provider: qq, user: a@b.c, password: xxx }');
|
|
36
|
+
}
|
|
37
|
+
const map = {};
|
|
38
|
+
let defaultAccount;
|
|
39
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
40
|
+
if (key === 'defaultAccount') {
|
|
41
|
+
if (typeof value === 'string' && value !== '')
|
|
42
|
+
defaultAccount = value;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
map[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return { map, defaultAccount };
|
|
48
|
+
}
|
|
21
49
|
/**
|
|
22
50
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
23
51
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
@@ -33,9 +61,10 @@ export function resolveEmailSettings(config) {
|
|
|
33
61
|
smtp: raw.smtp,
|
|
34
62
|
inboxFolder: raw.inboxFolder,
|
|
35
63
|
};
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
const parsedYaml = raw.accountsYaml?.trim() ? parseAccountsYaml(raw.accountsYaml) : undefined;
|
|
65
|
+
const entries = parsedYaml !== undefined
|
|
66
|
+
? parsedYaml.map
|
|
67
|
+
: (raw.accounts === undefined || Object.keys(raw.accounts).length === 0 ? undefined : raw.accounts);
|
|
39
68
|
const accounts = new Map();
|
|
40
69
|
if (entries === undefined) {
|
|
41
70
|
accounts.set('default', resolveAccount('default', common, {}, true));
|
|
@@ -55,6 +84,9 @@ export function resolveEmailSettings(config) {
|
|
|
55
84
|
else if (accounts.size === 1) {
|
|
56
85
|
defaultName = [...accounts.keys()][0];
|
|
57
86
|
}
|
|
87
|
+
else if (parsedYaml?.defaultAccount !== undefined && accounts.has(parsedYaml.defaultAccount)) {
|
|
88
|
+
defaultName = parsedYaml.defaultAccount;
|
|
89
|
+
}
|
|
58
90
|
else if (accounts.has('default')) {
|
|
59
91
|
defaultName = 'default';
|
|
60
92
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const inject: string[];
|
|
|
4
4
|
export type Config = EmailConfig;
|
|
5
5
|
export declare function apply(ctx: any, config?: Config): void;
|
|
6
6
|
export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
|
|
7
|
-
export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
|
|
7
|
+
export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
|
|
8
8
|
export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
|
|
9
9
|
export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart, messageMatchesQuery } from './mail-client.js';
|
|
10
10
|
export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
|
package/lib/index.js
CHANGED
|
@@ -393,7 +393,7 @@ export function apply(ctx, config = {}) {
|
|
|
393
393
|
}, { prepend: true });
|
|
394
394
|
}
|
|
395
395
|
export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
|
|
396
|
-
export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
|
|
396
|
+
export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
|
|
397
397
|
export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
|
|
398
398
|
export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart, messageMatchesQuery } from './mail-client.js';
|
|
399
399
|
export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
|
package/lib/settings.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const EmailSettingsSchema: z<Schemastery.ObjectS<{
|
|
|
15
15
|
sendApproval: z<boolean, boolean>;
|
|
16
16
|
maxBodyChars: z<number, number>;
|
|
17
17
|
downloadDir: z<string, string>;
|
|
18
|
+
accountsYaml: z<string, string>;
|
|
18
19
|
imap: z<Schemastery.ObjectS<{
|
|
19
20
|
host: z<string, string>;
|
|
20
21
|
port: z<number, number>;
|
|
@@ -41,6 +42,7 @@ export declare const EmailSettingsSchema: z<Schemastery.ObjectS<{
|
|
|
41
42
|
sendApproval: z<boolean, boolean>;
|
|
42
43
|
maxBodyChars: z<number, number>;
|
|
43
44
|
downloadDir: z<string, string>;
|
|
45
|
+
accountsYaml: z<string, string>;
|
|
44
46
|
imap: z<Schemastery.ObjectS<{
|
|
45
47
|
host: z<string, string>;
|
|
46
48
|
port: z<number, number>;
|
|
@@ -68,6 +70,7 @@ export interface EmailSettingsValue {
|
|
|
68
70
|
sendApproval: boolean;
|
|
69
71
|
maxBodyChars: number;
|
|
70
72
|
downloadDir: string;
|
|
73
|
+
accountsYaml: string;
|
|
71
74
|
imap: {
|
|
72
75
|
host: string;
|
|
73
76
|
port: number;
|
package/lib/settings.js
CHANGED
|
@@ -15,6 +15,7 @@ export const EmailSettingsSchema = z.object({
|
|
|
15
15
|
sendApproval: z.boolean().default(true),
|
|
16
16
|
maxBodyChars: z.number().default(20000),
|
|
17
17
|
downloadDir: z.string().default(''),
|
|
18
|
+
accountsYaml: z.string().role('secret').default(''),
|
|
18
19
|
imap: z.object({
|
|
19
20
|
host: z.string().default(''),
|
|
20
21
|
port: z.number().default(993),
|
|
@@ -77,6 +78,8 @@ export function toEmailConfig(value, user) {
|
|
|
77
78
|
out.maxBodyChars = value.maxBodyChars;
|
|
78
79
|
if (has('downloadDir'))
|
|
79
80
|
out.downloadDir = value.downloadDir;
|
|
81
|
+
if (has('accountsYaml'))
|
|
82
|
+
out.accountsYaml = value.accountsYaml;
|
|
80
83
|
if (user === null || user?.imap !== undefined) {
|
|
81
84
|
const fields = user === null ? value.imap : (user.imap ?? {});
|
|
82
85
|
const imap = {};
|
package/lib/web.js
CHANGED
|
@@ -75,6 +75,14 @@ export class EmailSettingsBackend {
|
|
|
75
75
|
res.end(bytes);
|
|
76
76
|
}
|
|
77
77
|
async handle(req, res) {
|
|
78
|
+
// Localhost-only: the snapshot carries the stored authorization code. If a
|
|
79
|
+
// deployment binds the webserver to 0.0.0.0, this route must never leak it
|
|
80
|
+
// to the LAN.
|
|
81
|
+
const remote = String(req.socket?.remoteAddress ?? '');
|
|
82
|
+
if (remote !== '127.0.0.1' && remote !== '::1' && remote !== '::ffff:127.0.0.1') {
|
|
83
|
+
this.responseJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'dsh-email settings route is localhost-only' } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
78
86
|
if (req.method === 'GET') {
|
|
79
87
|
try {
|
|
80
88
|
this.responseJson(res, 200, { ok: true, value: await this.snapshot() });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -60,10 +60,11 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"schemastery": "^3.18.0",
|
|
64
63
|
"imapflow": "^1.7.0",
|
|
65
64
|
"mailparser": "^3.9.15",
|
|
66
|
-
"nodemailer": "^9.0.5"
|
|
65
|
+
"nodemailer": "^9.0.5",
|
|
66
|
+
"schemastery": "^3.18.0",
|
|
67
|
+
"yaml": "^2.9.0"
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
69
70
|
"@types/node": "^26.2.0",
|