@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/client.js +626 -650
- package/lib/index.d.ts +0 -3
- package/lib/index.js +43 -29
- package/package.json +8 -8
- package/src/watchers/decision.ts +16 -3
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
46
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
47
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
48
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
51
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
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",
|
package/src/watchers/decision.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|