dsh-mail-sender 0.1.1 → 0.1.3

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/cordis.patch.yml CHANGED
@@ -1,17 +1,3 @@
1
1
  - insert:
2
- - id: mail-bot
2
+ - id: dsh-mail-sender
3
3
  name: dsh-mail-sender
4
- config:
5
- imap:
6
- host: imap.example.com
7
- port: 993
8
- secure: true
9
- user: agent@example.com
10
- password: ""
11
- smtp:
12
- host: smtp.example.com
13
- port: 465
14
- secure: true
15
- user: agent@example.com
16
- password: ""
17
- allowedAddresses: []
package/lib/index.d.ts CHANGED
@@ -2,23 +2,84 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import Schema from '@deepseek-ai/schemastery';
3
3
  export declare const name = "dsh-mail-sender";
4
4
  export declare const inject: string[];
5
+ /** schemastery `.volatile()` 字段解析后的稳定引用 */
6
+ interface Ref<T> {
7
+ get(): T;
8
+ }
5
9
  export interface Config {
6
10
  imap: {
7
- host: string;
8
- port: number;
9
- secure: boolean;
10
- user: string;
11
- password: string;
11
+ host: Ref<string>;
12
+ port: Ref<number>;
13
+ secure: Ref<boolean>;
14
+ user: Ref<string>;
15
+ password: Ref<string>;
12
16
  };
13
17
  smtp: {
14
- host: string;
15
- port: number;
16
- secure: boolean;
17
- user: string;
18
- password: string;
18
+ host: Ref<string>;
19
+ port: Ref<number>;
20
+ secure: Ref<boolean>;
21
+ user: Ref<string>;
22
+ password: Ref<string>;
19
23
  };
20
- /** 入站+出站合并白名单:只有名单内的地址可以给 agent 发信,agent 也只能回复这些地址 */
21
- allowedAddresses: string[];
24
+ /** 入站+出站合并白名单 */
25
+ allowedAddresses: Ref<string[]>;
22
26
  }
23
- export declare const Config: Schema<Config>;
27
+ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
28
+ imap: Schema<Schemastery.ObjectS<NoInfer<{
29
+ host: Schema<string, string, "volatile-defined">;
30
+ port: Schema<number, number, "volatile-defined">;
31
+ secure: Schema<boolean, boolean, "volatile-defined">;
32
+ user: Schema<string, string, "volatile-defined">;
33
+ password: Schema<string, string, "volatile-defined">;
34
+ }>>, Schemastery.ObjectT<NoInfer<{
35
+ host: Schema<string, string, "volatile-defined">;
36
+ port: Schema<number, number, "volatile-defined">;
37
+ secure: Schema<boolean, boolean, "volatile-defined">;
38
+ user: Schema<string, string, "volatile-defined">;
39
+ password: Schema<string, string, "volatile-defined">;
40
+ }>>, "plain">;
41
+ smtp: Schema<Schemastery.ObjectS<NoInfer<{
42
+ host: Schema<string, string, "volatile-defined">;
43
+ port: Schema<number, number, "volatile-defined">;
44
+ secure: Schema<boolean, boolean, "volatile-defined">;
45
+ user: Schema<string, string, "volatile-defined">;
46
+ password: Schema<string, string, "volatile-defined">;
47
+ }>>, Schemastery.ObjectT<NoInfer<{
48
+ host: Schema<string, string, "volatile-defined">;
49
+ port: Schema<number, number, "volatile-defined">;
50
+ secure: Schema<boolean, boolean, "volatile-defined">;
51
+ user: Schema<string, string, "volatile-defined">;
52
+ password: Schema<string, string, "volatile-defined">;
53
+ }>>, "plain">;
54
+ allowedAddresses: Schema<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
55
+ }>>, Schemastery.ObjectT<NoInfer<{
56
+ imap: Schema<Schemastery.ObjectS<NoInfer<{
57
+ host: Schema<string, string, "volatile-defined">;
58
+ port: Schema<number, number, "volatile-defined">;
59
+ secure: Schema<boolean, boolean, "volatile-defined">;
60
+ user: Schema<string, string, "volatile-defined">;
61
+ password: Schema<string, string, "volatile-defined">;
62
+ }>>, Schemastery.ObjectT<NoInfer<{
63
+ host: Schema<string, string, "volatile-defined">;
64
+ port: Schema<number, number, "volatile-defined">;
65
+ secure: Schema<boolean, boolean, "volatile-defined">;
66
+ user: Schema<string, string, "volatile-defined">;
67
+ password: Schema<string, string, "volatile-defined">;
68
+ }>>, "plain">;
69
+ smtp: Schema<Schemastery.ObjectS<NoInfer<{
70
+ host: Schema<string, string, "volatile-defined">;
71
+ port: Schema<number, number, "volatile-defined">;
72
+ secure: Schema<boolean, boolean, "volatile-defined">;
73
+ user: Schema<string, string, "volatile-defined">;
74
+ password: Schema<string, string, "volatile-defined">;
75
+ }>>, Schemastery.ObjectT<NoInfer<{
76
+ host: Schema<string, string, "volatile-defined">;
77
+ port: Schema<number, number, "volatile-defined">;
78
+ secure: Schema<boolean, boolean, "volatile-defined">;
79
+ user: Schema<string, string, "volatile-defined">;
80
+ password: Schema<string, string, "volatile-defined">;
81
+ }>>, "plain">;
82
+ allowedAddresses: Schema<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
83
+ }>>, "plain">;
24
84
  export declare function apply(ctx: Context, config: Config): void;
85
+ export {};
package/lib/index.js CHANGED
@@ -7,56 +7,64 @@ export const name = 'dsh-mail-sender';
7
7
  export const inject = ['agents'];
8
8
  export const Config = Schema.object({
9
9
  imap: Schema.object({
10
- host: Schema.string().required().description('IMAP 服务器地址'),
11
- port: Schema.number().default(993),
12
- secure: Schema.boolean().default(true),
13
- user: Schema.string().required().description('IMAP 登录账号'),
14
- password: Schema.string().required().description('IMAP 密码/授权码'),
15
- }),
10
+ host: Schema.string().volatile().default('').description('IMAP 服务器地址'),
11
+ port: Schema.number().volatile().default(993),
12
+ secure: Schema.boolean().volatile().default(true),
13
+ user: Schema.string().volatile().default('').description('IMAP 登录账号'),
14
+ password: Schema.string().volatile().default('').description('IMAP 密码/授权码'),
15
+ }).description('IMAP 收信'),
16
16
  smtp: Schema.object({
17
- host: Schema.string().required().description('SMTP 服务器地址'),
18
- port: Schema.number().default(465),
19
- secure: Schema.boolean().default(true),
20
- user: Schema.string().required().description('SMTP 登录账号'),
21
- password: Schema.string().required().description('SMTP 密码/授权码'),
22
- }),
17
+ host: Schema.string().volatile().default('').description('SMTP 服务器地址'),
18
+ port: Schema.number().volatile().default(465),
19
+ secure: Schema.boolean().volatile().default(true),
20
+ user: Schema.string().volatile().default('').description('SMTP 登录账号'),
21
+ password: Schema.string().volatile().default('').description('SMTP 密码/授权码'),
22
+ }).description('SMTP 发信'),
23
23
  allowedAddresses: Schema.array(Schema.string())
24
+ .volatile()
24
25
  .default([])
25
26
  .description('允许与 agent 通信的邮箱地址'),
26
27
  });
27
28
  export function apply(ctx, config) {
28
- const allowed = new Set(config.allowedAddresses.map(a => a.toLowerCase().trim()));
29
- const smtp = nodemailer.createTransport({
30
- host: config.smtp.host,
31
- port: config.smtp.port,
32
- secure: config.smtp.secure,
33
- auth: { user: config.smtp.user, pass: config.smtp.password },
34
- });
35
- const imap = new ImapFlow({
36
- host: config.imap.host,
37
- port: config.imap.port,
38
- secure: config.imap.secure,
39
- auth: { user: config.imap.user, pass: config.imap.password },
40
- logger: false,
29
+ const imapConfig = () => ({
30
+ host: config.imap.host.get(),
31
+ port: config.imap.port.get(),
32
+ secure: config.imap.secure.get(),
33
+ auth: { user: config.imap.user.get(), pass: config.imap.password.get() },
41
34
  });
35
+ const imap = new ImapFlow({ ...imapConfig(), logger: false });
36
+ // SMTP 传输按当前配置缓存,设置变更后自动重建
37
+ let smtp;
38
+ let smtpKey = '';
39
+ const getSmtp = () => {
40
+ const conf = {
41
+ host: config.smtp.host.get(),
42
+ port: config.smtp.port.get(),
43
+ secure: config.smtp.secure.get(),
44
+ auth: { user: config.smtp.user.get(), pass: config.smtp.password.get() },
45
+ };
46
+ const key = JSON.stringify(conf);
47
+ if (smtpKey !== key) {
48
+ smtp = nodemailer.createTransport(conf);
49
+ smtpKey = key;
50
+ }
51
+ return smtp;
52
+ };
42
53
  // sessionId -> agent handle
43
54
  const handles = new Map();
44
55
  // sessionId -> 最近一封来信的信息(用于回复)
45
56
  const lastMail = new Map();
46
57
  // `${sessionId}:${turn}` -> 该 turn 内累积的 assistant 文本
47
58
  const turnBuffers = new Map();
48
- ctx.effect(async () => {
49
- await imap.connect();
50
- const lock = await imap.getMailboxLock('INBOX');
59
+ ctx.effect(() => {
60
+ let closed = false;
61
+ let lock;
51
62
  imap.on('exists', async () => {
52
63
  try {
53
64
  const mb = imap.mailbox;
54
65
  if (mb === false)
55
66
  return;
56
- const seq = mb.exists;
57
- if (!seq)
58
- return;
59
- const fetched = await imap.fetchOne(seq, { source: true });
67
+ const fetched = await imap.fetchOne(mb.exists, { source: true });
60
68
  if (fetched === false || !fetched.source)
61
69
  return;
62
70
  const parsed = await simpleParser(fetched.source);
@@ -64,13 +72,16 @@ export function apply(ctx, config) {
64
72
  if (!from)
65
73
  return;
66
74
  // 白名单:不在名单内直接丢弃
75
+ const allowed = new Set(config.allowedAddresses.get().map(a => a.toLowerCase().trim()));
67
76
  if (!allowed.has(from))
68
77
  return;
69
78
  const sessionId = `mail-${from}`;
70
- const subject = parsed.subject || '(no subject)';
71
- const body = parsed.text || '';
72
- lastMail.set(sessionId, { from, subject, messageId: parsed.messageId });
73
- // 获取或创建 agent(同一发件人同一会话)
79
+ lastMail.set(sessionId, {
80
+ from,
81
+ subject: parsed.subject || '(no subject)',
82
+ messageId: parsed.messageId,
83
+ });
84
+ // 同一发件人同一会话
74
85
  let handle = handles.get(sessionId);
75
86
  if (!handle) {
76
87
  handle = await getOrCreateAgent(ctx, sessionId);
@@ -81,19 +92,25 @@ export function apply(ctx, config) {
81
92
  handleSessionEvent(sessionId, event);
82
93
  });
83
94
  }
84
- const msg = createUserMessage({
85
- content: [{ type: 'text', text: body }],
95
+ handle.agent.followup(createUserMessage({
96
+ content: [{ type: 'text', text: parsed.text || '' }],
86
97
  source: { kind: 'plugin', plugin: name },
87
- });
88
- handle.agent.followup(msg);
98
+ }));
89
99
  }
90
100
  catch (err) {
91
- ctx.logger?.error('[mail-bot] fetch/parse failed', err);
101
+ console.error('[mail-bot] 收信失败', err);
92
102
  }
93
103
  });
94
- return async () => {
104
+ void (async () => {
105
+ await imap.connect();
106
+ if (closed)
107
+ return void imap.logout();
108
+ lock = await imap.getMailboxLock('INBOX');
109
+ })().catch(err => console.error('[mail-bot] IMAP 连接失败', err));
110
+ return () => {
111
+ closed = true;
95
112
  try {
96
- lock.release();
113
+ lock?.release();
97
114
  }
98
115
  catch { }
99
116
  for (const h of handles.values()) {
@@ -105,12 +122,13 @@ export function apply(ctx, config) {
105
122
  handles.clear();
106
123
  turnBuffers.clear();
107
124
  lastMail.clear();
108
- await imap.logout();
125
+ void imap.logout().catch(() => { });
109
126
  };
110
127
  });
111
128
  function handleSessionEvent(sessionId, event) {
112
129
  const data = event?.data ?? {};
113
130
  const turn = data.turn;
131
+ const key = `${sessionId}:${turn}`;
114
132
  if (event.type === 'assistant/message') {
115
133
  const text = (data.message?.content ?? [])
116
134
  .filter((b) => b.type === 'text')
@@ -118,31 +136,27 @@ export function apply(ctx, config) {
118
136
  .join('');
119
137
  if (!text)
120
138
  return;
121
- const key = `${sessionId}:${turn}`;
122
- const buf = turnBuffers.get(key) ?? [];
123
- buf.push(text);
124
- turnBuffers.set(key, buf);
139
+ turnBuffers.set(key, [...(turnBuffers.get(key) ?? []), text]);
125
140
  return;
126
141
  }
142
+ // 一轮完整结束后才回复;一个 turn 可能含多个 step
127
143
  if (event.type === 'turn/end' && data.reason?.kind === 'completed') {
128
- const key = `${sessionId}:${turn}`;
129
- const parts = turnBuffers.get(key) ?? [];
144
+ const replyText = (turnBuffers.get(key) ?? []).join('\n\n').trim();
130
145
  turnBuffers.delete(key);
131
- const replyText = parts.join('\n\n').trim();
132
146
  if (!replyText)
133
147
  return;
134
148
  const mail = lastMail.get(sessionId);
135
149
  if (!mail)
136
150
  return;
137
151
  const subject = mail.subject.startsWith('Re:') ? mail.subject : `Re: ${mail.subject}`;
138
- smtp.sendMail({
139
- from: config.smtp.user,
152
+ getSmtp().sendMail({
153
+ from: config.smtp.user.get(),
140
154
  to: mail.from,
141
155
  subject,
142
156
  text: replyText,
143
157
  inReplyTo: mail.messageId,
144
158
  references: mail.messageId,
145
- }).catch(err => ctx.logger?.error('[mail-bot] send reply failed', err));
159
+ }).catch(err => console.error('[mail-bot] 回信失败', err));
146
160
  }
147
161
  }
148
162
  }
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "dsh-mail-sender",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
+ "description": "DSH mail channel: inbound IMAP mail is delivered to an agent, whose reply is sent back over SMTP.",
4
5
  "type": "module",
5
6
  "main": "lib/index.js",
6
7
  "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
7
15
  "files": ["lib", "cordis.patch.yml"],
16
+ "keywords": ["dsh-plugin", "deepseek-harness", "mail", "imap", "smtp"],
8
17
  "dsh": {
9
18
  "bundle": { "patch": "./cordis.patch.yml" }
10
19
  },
@@ -33,4 +42,4 @@
33
42
  "@types/nodemailer": "^6.4.17",
34
43
  "typescript": "^5.6.0"
35
44
  }
36
- }
45
+ }
package/readme.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # dsh-mail-sender
2
2
 
3
- 一个 DeepSeek Harness (DSH) 邮件通道插件:用户发邮件 → DSH 接收并交给 agent → agent 的回复自动用邮件发回去。
3
+ DeepSeek Harness (DSH) 邮件通道插件:用户发邮件 → DSH 接收并交给 agent → agent 的回复自动用邮件发回去。
4
4
 
5
5
  - **入站**:IMAP IDLE 实时监听收件箱
6
6
  - **会话**:同一发件人 → 同一个 agent 会话(`sessionId = mail-<地址>`)
7
- - **出站**:订阅 `session/event`,一轮 turn 结束后自动 SMTP 回复(`Re:` 主题 + `In-Reply-To`)
8
- - **白名单**:`allowedAddresses` 同时控制入站和出站
7
+ - **出站**:订阅 `session/event`,一轮 turn 正常结束后自动 SMTP 回信(`Re:` 主题 + `In-Reply-To`)
8
+ - **白名单**:`allowedAddresses` 同时控制入站与出站
9
+ - **配置界面**:Host 从导出的 `Config` schema 自动渲染,无需自写前端
9
10
 
10
11
  ## 工作原理
11
12
 
@@ -24,4 +25,94 @@ ctx.agents.create / resume (sessionId = `mail-${from}`)
24
25
  │ turn/end (reason.kind === 'completed') → 取出本轮文本
25
26
  ▼
26
27
  SMTP 回复 (Re: 原主题, In-Reply-To: 原 Message-ID)
27
- ```
28
+ ```
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ # npm
34
+ dsh plugin --profile web add dsh-mail-sender
35
+
36
+ # GitHub(需仓库内已提交 lib/)
37
+ dsh plugin --profile web add github:<你的用户名>/dsh-mail-sender
38
+
39
+ # 本地源码
40
+ npm install && npm run build
41
+ dsh plugin --profile web add link:/path/to/dsh-mail-sender
42
+ ```
43
+
44
+ 安装后重启 `dsh web`,用 `dsh --profile web --dump-config` 确认插件行已进入配置树。
45
+
46
+ ## 在设置页填写配置
47
+
48
+ 打开 **Settings(设置)→ Plugins(插件)→ dsh-mail-sender**,表单由 Host 根据插件导出的 `Config` schema 自动生成。
49
+
50
+ > 该表单依赖两点,本插件已满足:
51
+ > 1. patch 行 `id` 与包名一致(`dsh-mail-sender`),设置表单按行 id 定位;
52
+ > 2. 每个可编辑字段声明为 `.volatile()`,DSH 才会把它投影成可编辑项并把改动写回 profile 的 `cordis.patch.yml`。
53
+
54
+ | 字段 | 说明 |
55
+ |---|---|
56
+ | `imap.host` | IMAP 服务器地址(如 `imap.qq.com`) |
57
+ | `imap.port` | 端口,默认 `993` |
58
+ | `imap.secure` | 是否 TLS,默认 `true` |
59
+ | `imap.user` | IMAP 登录账号 |
60
+ | `imap.password` | IMAP 密码或授权码 |
61
+ | `smtp.host` | SMTP 服务器地址(如 `smtp.qq.com`) |
62
+ | `smtp.port` | 端口,默认 `465` |
63
+ | `smtp.secure` | 是否 TLS,默认 `true` |
64
+ | `smtp.user` | SMTP 登录账号(也是回信 From) |
65
+ | `smtp.password` | SMTP 密码或授权码 |
66
+ | `allowedAddresses` | 允许与 agent 通信的邮箱地址列表 |
67
+
68
+ 白名单同时控制两个方向:非名单内发件人的来信直接丢弃;agent 的回复目标就是来信人,因此也天然限定在名单内。
69
+
70
+ 配置为 `.volatile()`:白名单与 SMTP 设置在保存后即时生效;IMAP 连接参数变更后需重载插件(重启 `dsh web`)才重连。
71
+
72
+ ## 包结构
73
+
74
+ ```
75
+ .
76
+ ├── package.json # dsh.bundle 声明(无 dsh.client,纯 Host 插件)
77
+ ├── cordis.patch.yml # 顶层 YAML 数组:插入插件行
78
+ ├── src/index.ts
79
+ ├── tsconfig.json
80
+ └── .npmrc # legacy-peer-deps=true(本地开发)
81
+ ```
82
+
83
+ `cordis.patch.yml` 必须是**顶层数组**的 loader patch 条目:
84
+
85
+ ```yaml
86
+ - insert:
87
+ - id: dsh-mail-sender
88
+ name: dsh-mail-sender
89
+ ```
90
+
91
+ ## 发布
92
+
93
+ ### npm
94
+
95
+ ```bash
96
+ npm run build # 产出 lib/
97
+ npm publish # scoped 包用 --access public
98
+ ```
99
+
100
+ `files` 已包含 `lib` 与 `cordis.patch.yml`——漏掉 patch 文件是最常见的“装上了但没进配置树”原因。
101
+
102
+ ### GitHub
103
+
104
+ ```bash
105
+ git init && git add . && git commit -m "feat: dsh-mail-sender"
106
+ git branch -M main
107
+ git remote add origin https://github.com/<用户名>/dsh-mail-sender.git
108
+ git push -u origin main
109
+ git tag v0.1.3 && git push origin v0.1.3
110
+ ```
111
+
112
+ 从 git 安装时 pnpm 需要执行构建,请在 profile 的 `pnpm-workspace.yaml` 里加 `allowBuilds: { dsh-mail-sender: true }`;更省事的方式是发布到 npm(发布产物已构建好)。
113
+
114
+ ## 依赖
115
+
116
+ 运行时:`imapflow`、`mailparser`、`nodemailer`。
117
+
118
+ Peer(由 DSH host 提供):`@deepseek-ai/cordis`、`@deepseek-ai/dsh-agent`、`@deepseek-ai/dsh-llm`、`@deepseek-ai/schemastery`。