@dsh-plus/notify-email 0.1.7 → 0.1.9

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/lib/index.d.ts CHANGED
@@ -1,9 +1,6 @@
1
- import "@deepseek-ai/dsh-settings";
2
1
  import z from "@deepseek-ai/schemastery";
3
2
  import { Context, Service } from "@deepseek-ai/cordis";
4
-
5
3
  //#region src/config.d.ts
6
-
7
4
  declare const Config: z<Schemastery.ObjectS<{
8
5
  enabled: z<boolean, boolean>;
9
6
  smtp: z<Schemastery.ObjectS<{
package/lib/index.js CHANGED
@@ -5,22 +5,16 @@ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
5
5
  import { appendFile, mkdir } from "node:fs/promises";
6
6
  import { dirname } from "node:path";
7
7
  import nodemailer from "nodemailer";
8
-
9
- //#region src/ns.ts
10
- /**
11
- * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
12
- * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
13
- * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
14
- * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
15
- * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
16
- * @module notify-email/ns
17
- */
18
- const SETTINGS_NS$1 = "dsh-plus-notify-email";
19
-
20
8
  //#endregion
21
9
  //#region src/config.ts
10
+ /**
11
+ * 配置单一事实源:cordis 行级 Config(组合默认值,dev/prod patch 层可覆盖)
12
+ * 与 settings namespace(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)
13
+ * 共用同一 schemastery schema。SMTP 密码标记 secret 角色,任何读取通道不得回传。
14
+ * @module notify-email/config
15
+ */
22
16
  /** settings 命名空间;webui 配置卡片与插件运行期读取同一份(字面量见 ./ns.ts)。 */
23
- const SETTINGS_NS = settingsNamespace(SETTINGS_NS$1);
17
+ const SETTINGS_NS = settingsNamespace("dsh-plus-notify-email");
24
18
  const SmtpSchema = z.object({
25
19
  host: z.string().description("SMTP 服务器主机名").default(""),
26
20
  port: z.natural().max(65535).description("SMTP 端口").default(465),
@@ -58,9 +52,14 @@ const Config = z.object({
58
52
  maxBodyChars: z.natural().min(200).description("邮件正文内容截断长度").default(4e3),
59
53
  dryRun: z.boolean().description("仅记录日志不真实发送(开发调试用)").default(false)
60
54
  });
61
-
62
55
  //#endregion
63
56
  //#region src/audit.ts
57
+ /**
58
+ * 通知投递审计:每次发送尝试追加一行 JSONL 到 $DSH_HOME/logs/notify-email.jsonl。
59
+ * 让 dry-run 可观测(dev 实例 logger 不落盘),也为生产排查「为什么没收到邮件」留痕。
60
+ * 记录正文截断 2000 字符;永远不含 SMTP 凭据。写失败只告警不阻断。
61
+ * @module notify-email/audit
62
+ */
64
63
  const BODY_EXCERPT_CHARS = 2e3;
65
64
  function buildAuditRecord(subject, text, to, result) {
66
65
  return {
@@ -80,7 +79,6 @@ function createJsonlAuditSink(path, onError) {
80
79
  });
81
80
  };
82
81
  }
83
-
84
82
  //#endregion
85
83
  //#region src/config-api.ts
86
84
  const ROUTE_TEST = "/dsh-plus/notify-email/test";
@@ -119,9 +117,13 @@ function registerTestApi(ctx, service) {
119
117
  })
120
118
  });
121
119
  }
122
-
123
120
  //#endregion
124
121
  //#region src/mailer.ts
122
+ /**
123
+ * 邮件发送:nodemailer 封装 + dry-run 兜底。
124
+ * Transport 抽象为函数注入,单元测试以假 transport 断言参数,零网络。
125
+ * @module notify-email/mailer
126
+ */
125
127
  /** nodemailer 连接/问候/套接字超时(毫秒),防 SMTP 不可达时挂住通知链路。 */
126
128
  const CONNECTION_TIMEOUT_MS = 1e4;
127
129
  const GREETING_TIMEOUT_MS = 1e4;
@@ -199,7 +201,6 @@ var Mailer = class {
199
201
  return result;
200
202
  }
201
203
  };
202
-
203
204
  //#endregion
204
205
  //#region src/triggers/render.ts
205
206
  function truncate(text, maxChars) {
@@ -264,7 +265,6 @@ function renderAbortedNotice(info) {
264
265
  text: header("任务被用户取消", info)
265
266
  };
266
267
  }
267
-
268
268
  //#endregion
269
269
  //#region src/triggers/builtin.ts
270
270
  /** 官方决策工具名 → 渲染函数 + 对应 toggle。 */
@@ -289,16 +289,22 @@ function createTurnEndTrigger(resolveConfig) {
289
289
  }
290
290
  };
291
291
  }
292
-
293
292
  //#endregion
294
293
  //#region src/watchers/decision.ts
295
294
  function installDecisionWatcher(ctx, service) {
296
- const seen = /* @__PURE__ */ new Set();
295
+ const seen = /* @__PURE__ */ new Map();
296
+ const UNKNOWN_BUCKET_CAP = 1e3;
297
297
  const logger = ctx.logger("notify-email");
298
298
  ctx.on("tools/pre-execute", (exec, next) => {
299
- if (seen.has(exec.callId)) return next();
300
- seen.add(exec.callId);
301
299
  const sessionId = exec.agent?.id ?? "unknown";
300
+ let bucket = seen.get(sessionId);
301
+ if (bucket === void 0) {
302
+ bucket = /* @__PURE__ */ new Set();
303
+ seen.set(sessionId, bucket);
304
+ }
305
+ if (bucket.has(exec.callId)) return next();
306
+ if (sessionId === "unknown" && bucket.size >= UNKNOWN_BUCKET_CAP) bucket.clear();
307
+ bucket.add(exec.callId);
302
308
  service.dispatchDecision({
303
309
  callId: exec.callId,
304
310
  sessionId,
@@ -309,12 +315,14 @@ function installDecisionWatcher(ctx, service) {
309
315
  });
310
316
  return next();
311
317
  });
318
+ ctx.on("session/disposed", (session) => {
319
+ seen.delete(session.id);
320
+ });
312
321
  }
313
-
314
322
  //#endregion
315
323
  //#region src/watchers/turn-end.ts
316
324
  /** 只有这三类 turn 结束对用户有可决策价值;blocked/max-tokens/interrupted 不通知。 */
317
- const NOTIFY_KINDS = new Set([
325
+ const NOTIFY_KINDS = /* @__PURE__ */ new Set([
318
326
  "completed",
319
327
  "error",
320
328
  "aborted"
@@ -397,9 +405,16 @@ function installTurnEndWatcher(ctx, service, resolveConfig) {
397
405
  watches.clear();
398
406
  }, "notify-email: turn-end timers");
399
407
  }
400
-
401
408
  //#endregion
402
409
  //#region src/service.ts
410
+ /**
411
+ * NotifyEmailService:通知编排中枢。
412
+ * - 配置:installSettingsSection 接入 settings 用户层($DSH_HOME/settings.yaml,热生效),
413
+ * 无 settings provider 时退化为 cordis 行级 config。
414
+ * - 触发器:registerTrigger 是第三方插件的扩展接口;内置官方适配器同路径注册。
415
+ * - 投递:首个非空 EmailNotice 经 Mailer 发送;单个触发器抛错不影响其余。
416
+ * @module @dsh-plus/notify-email
417
+ */
403
418
  var NotifyEmailService = class extends Service {
404
419
  static [Context.inject] = ["agents", "tools"];
405
420
  triggers = [];
@@ -415,7 +430,8 @@ var NotifyEmailService = class extends Service {
415
430
  onChange: () => {}
416
431
  });
417
432
  const logger = ctx.logger("notify-email");
418
- this.mailer = new Mailer(() => this.current(), logger, void 0, createJsonlAuditSink(dshHomePath("logs", "notify-email.jsonl"), (message) => logger.warn(`audit write failed: ${message}`)));
433
+ const audit = createJsonlAuditSink(dshHomePath("logs", "notify-email.jsonl"), (message) => logger.warn(`audit write failed: ${message}`));
434
+ this.mailer = new Mailer(() => this.current(), logger, void 0, audit);
419
435
  this.registerTrigger(createDecisionTrigger(() => this.current()));
420
436
  this.registerTrigger(createTurnEndTrigger(() => this.current()));
421
437
  installDecisionWatcher(ctx, this);
@@ -472,7 +488,6 @@ var NotifyEmailService = class extends Service {
472
488
  }
473
489
  }
474
490
  };
475
-
476
491
  //#endregion
477
492
  //#region src/index.ts
478
493
  const name = "dsh-plus-notify-email";
@@ -480,6 +495,5 @@ const inject = ["agents", "tools"];
480
495
  function apply(ctx, config) {
481
496
  ctx.plugin(NotifyEmailService, config);
482
497
  }
483
-
484
498
  //#endregion
485
- export { Config, NotifyEmailService, apply, inject, name };
499
+ export { Config, NotifyEmailService, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/notify-email",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "dsh-plus service+ui plugin: 任务结束/等待决策/出错时向指定邮箱发送邮件通知",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -42,13 +42,13 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/cordis": "4.0.1",
45
- "@deepseek-ai/dsh-agent": "0.1.0-rc.8",
46
- "@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
47
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
48
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
49
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
50
- "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
51
- "@deepseek-ai/dsh-tools": "0.1.0-rc.8",
45
+ "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
47
+ "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
48
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
49
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
50
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
51
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
52
52
  "@deepseek-ai/schemastery": "3.18.1",
53
53
  "@types/nodemailer": "8.0.1",
54
54
  "@types/react": "~18.3.1",
@@ -9,12 +9,22 @@ import type { Context } from '@deepseek-ai/cordis'
9
9
  import type { NotifyEmailService } from '../service.ts'
10
10
 
11
11
  export function installDecisionWatcher(ctx: Context, service: NotifyEmailService): void {
12
- const seen = new Set<string>()
12
+ // 按 sessionId 分桶去重:callId 粒度的全局集合会随每个工具调用无限增长;
13
+ // 会话销毁即整桶释放。无 agent 归属的调用落到 unknown 桶(无生命周期可挂),
14
+ // 以有界环形覆盖兜底,保证任何路径都不产生无界内存。
15
+ const seen = new Map<string, Set<string>>()
16
+ const UNKNOWN_BUCKET_CAP = 1000
13
17
  const logger = ctx.logger('notify-email')
14
18
  ctx.on('tools/pre-execute', (exec, next) => {
15
- if (seen.has(exec.callId)) return next()
16
- seen.add(exec.callId)
17
19
  const sessionId = exec.agent?.id ?? 'unknown'
20
+ let bucket = seen.get(sessionId)
21
+ if (bucket === undefined) {
22
+ bucket = new Set()
23
+ seen.set(sessionId, bucket)
24
+ }
25
+ if (bucket.has(exec.callId)) return next()
26
+ if (sessionId === 'unknown' && bucket.size >= UNKNOWN_BUCKET_CAP) bucket.clear()
27
+ bucket.add(exec.callId)
18
28
  service
19
29
  .dispatchDecision({
20
30
  callId: exec.callId,
@@ -30,4 +40,7 @@ export function installDecisionWatcher(ctx: Context, service: NotifyEmailService
30
40
  })
31
41
  return next()
32
42
  })
43
+ ctx.on('session/disposed', (session) => {
44
+ seen.delete(session.id)
45
+ })
33
46
  }