@dsh-plus/notify-email 0.1.16 → 0.1.18
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 +82 -38
- package/lib/index.js +39 -13
- package/lib/style.css +72 -72
- package/package.json +16 -14
- package/src/audit.ts +2 -1
- package/src/config.ts +10 -2
- package/src/mailer.ts +34 -6
- package/src/service.ts +11 -5
package/lib/client.js
CHANGED
|
@@ -18000,16 +18000,16 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
18000
18000
|
init_shellscript();
|
|
18001
18001
|
init_json();
|
|
18002
18002
|
var DiffBlock_module_default = {
|
|
18003
|
-
"add": "
|
|
18004
|
-
"block": "
|
|
18005
|
-
"body": "
|
|
18006
|
-
"copyButton": "
|
|
18007
|
-
"del": "
|
|
18008
|
-
"expand": "
|
|
18009
|
-
"footer": "
|
|
18010
|
-
"gap": "
|
|
18011
|
-
"line": "
|
|
18012
|
-
"path": "
|
|
18003
|
+
"add": "nVxsMW_add",
|
|
18004
|
+
"block": "nVxsMW_block",
|
|
18005
|
+
"body": "nVxsMW_body",
|
|
18006
|
+
"copyButton": "nVxsMW_copyButton",
|
|
18007
|
+
"del": "nVxsMW_del",
|
|
18008
|
+
"expand": "nVxsMW_expand",
|
|
18009
|
+
"footer": "nVxsMW_footer",
|
|
18010
|
+
"gap": "nVxsMW_gap",
|
|
18011
|
+
"line": "nVxsMW_line",
|
|
18012
|
+
"path": "nVxsMW_path"
|
|
18013
18013
|
};
|
|
18014
18014
|
//#endregion
|
|
18015
18015
|
//#region ../../node_modules/.pnpm/mdast-util-to-string@4.0.0/node_modules/mdast-util-to-string/lib/index.js
|
|
@@ -59244,27 +59244,27 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59244
59244
|
chunkString: "chunkString"
|
|
59245
59245
|
};
|
|
59246
59246
|
//#endregion
|
|
59247
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
59247
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/CodeBlock.module.css
|
|
59248
59248
|
var CodeBlock_module_default = {
|
|
59249
|
-
"action": "
|
|
59250
|
-
"banner": "
|
|
59251
|
-
"bannerWrap": "
|
|
59252
|
-
"block": "
|
|
59253
|
-
"copyButton": "
|
|
59254
|
-
"infostring": "
|
|
59255
|
-
"plain": "
|
|
59256
|
-
"shiki": "
|
|
59249
|
+
"action": "Hbqh2W_action",
|
|
59250
|
+
"banner": "Hbqh2W_banner",
|
|
59251
|
+
"bannerWrap": "Hbqh2W_bannerWrap",
|
|
59252
|
+
"block": "Hbqh2W_block",
|
|
59253
|
+
"copyButton": "Hbqh2W_copyButton",
|
|
59254
|
+
"infostring": "Hbqh2W_infostring",
|
|
59255
|
+
"plain": "Hbqh2W_plain",
|
|
59256
|
+
"shiki": "Hbqh2W_shiki"
|
|
59257
59257
|
};
|
|
59258
59258
|
//#endregion
|
|
59259
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
59259
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css
|
|
59260
59260
|
var MarkdownText_module_default = {
|
|
59261
|
-
"fileMention": "
|
|
59262
|
-
"image": "
|
|
59263
|
-
"imageAlt": "
|
|
59264
|
-
"linkIcon": "
|
|
59265
|
-
"markdown": "
|
|
59266
|
-
"tableFill": "
|
|
59267
|
-
"tableScroll": "
|
|
59261
|
+
"fileMention": "BL43XG_fileMention",
|
|
59262
|
+
"image": "BL43XG_image",
|
|
59263
|
+
"imageAlt": "BL43XG_imageAlt",
|
|
59264
|
+
"linkIcon": "BL43XG_linkIcon",
|
|
59265
|
+
"markdown": "BL43XG_markdown",
|
|
59266
|
+
"tableFill": "BL43XG_tableFill",
|
|
59267
|
+
"tableScroll": "BL43XG_tableScroll"
|
|
59268
59268
|
};
|
|
59269
59269
|
//#endregion
|
|
59270
59270
|
//#region ../../node_modules/.pnpm/@shikijs+langs@4.4.3/node_modules/@shikijs/langs/dist/python.mjs
|
|
@@ -59584,7 +59584,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
59584
59584
|
less_default = [lang];
|
|
59585
59585
|
}));
|
|
59586
59586
|
//#endregion
|
|
59587
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.
|
|
59587
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-ui-primitives@0.1.5-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/index.js
|
|
59588
59588
|
init_typescript();
|
|
59589
59589
|
init_shellscript();
|
|
59590
59590
|
init_json();
|
|
@@ -61284,6 +61284,30 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61284
61284
|
return;
|
|
61285
61285
|
}
|
|
61286
61286
|
}
|
|
61287
|
+
/** Protocols a vocabulary-rewritten image destination may carry. */
|
|
61288
|
+
function vocabularyImageUrl(url) {
|
|
61289
|
+
try {
|
|
61290
|
+
const protocol = new URL(url).protocol;
|
|
61291
|
+
return protocol === "http:" || protocol === "https:" || protocol === "blob:" || protocol === "data:" ? url : void 0;
|
|
61292
|
+
} catch {
|
|
61293
|
+
return;
|
|
61294
|
+
}
|
|
61295
|
+
}
|
|
61296
|
+
/**
|
|
61297
|
+
* The displayable source for one image destination: absolute HTTP(S) as
|
|
61298
|
+
* authored, otherwise the context's local-path vocabulary when it vouches for
|
|
61299
|
+
* the destination. Either miss leaves the authored fallback (alt text) to the
|
|
61300
|
+
* caller.
|
|
61301
|
+
* @param url - The authored markdown destination.
|
|
61302
|
+
* @param pathImages - Rewriting vocabulary, when the render pass has one.
|
|
61303
|
+
* @returns The displayable image URL, or undefined.
|
|
61304
|
+
*/
|
|
61305
|
+
function imageSource(url, pathImages) {
|
|
61306
|
+
const remote = remoteImageUrl(sanitizeUrl(normalizeUri(url)));
|
|
61307
|
+
if (remote !== void 0) return remote;
|
|
61308
|
+
const rewritten = pathImages?.resolve(url);
|
|
61309
|
+
return rewritten === void 0 ? void 0 : vocabularyImageUrl(rewritten);
|
|
61310
|
+
}
|
|
61287
61311
|
/**
|
|
61288
61312
|
* Create an empty {@link ReferenceTargets}.
|
|
61289
61313
|
* @returns Fresh empty maps.
|
|
@@ -61398,7 +61422,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61398
61422
|
inLink: true
|
|
61399
61423
|
}), key, !anchorWrapsOnlyImages(node.children));
|
|
61400
61424
|
case "linkReference": return renderLinkReference(node, key, context);
|
|
61401
|
-
case "image": return renderImage(node.url, node.alt ?? "", key);
|
|
61425
|
+
case "image": return renderImage(node.url, node.alt ?? "", key, context);
|
|
61402
61426
|
case "imageReference": return renderImageReference(node, key, context);
|
|
61403
61427
|
case "footnoteReference": return renderFootnoteReference(node, key, context);
|
|
61404
61428
|
case "definition":
|
|
@@ -61532,20 +61556,36 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61532
61556
|
return;
|
|
61533
61557
|
}
|
|
61534
61558
|
}
|
|
61535
|
-
function renderImage(url, alt, key) {
|
|
61536
|
-
const imageSrc =
|
|
61559
|
+
function renderImage(url, alt, key, context) {
|
|
61560
|
+
const imageSrc = imageSource(url, context.pathImages);
|
|
61537
61561
|
if (imageSrc === void 0) return (0, react_jsx_runtime.jsx)("span", {
|
|
61538
61562
|
className: MarkdownText_module_default.imageAlt,
|
|
61539
61563
|
children: alt
|
|
61540
61564
|
}, key);
|
|
61565
|
+
return (0, react_jsx_runtime.jsx)(MarkdownImage, {
|
|
61566
|
+
src: imageSrc,
|
|
61567
|
+
alt,
|
|
61568
|
+
destination: url
|
|
61569
|
+
}, `${key}:${imageSrc}`);
|
|
61570
|
+
}
|
|
61571
|
+
/** Failed loads retain the authored alt or destination; a new source remounts the image. */
|
|
61572
|
+
function MarkdownImage({ src, alt, destination }) {
|
|
61573
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
61574
|
+
if (failed) return (0, react_jsx_runtime.jsx)("span", {
|
|
61575
|
+
className: MarkdownText_module_default.imageAlt,
|
|
61576
|
+
children: alt || destination
|
|
61577
|
+
});
|
|
61541
61578
|
return (0, react_jsx_runtime.jsx)("img", {
|
|
61542
61579
|
className: MarkdownText_module_default.image,
|
|
61543
|
-
src
|
|
61580
|
+
src,
|
|
61544
61581
|
alt,
|
|
61582
|
+
onError: () => {
|
|
61583
|
+
setFailed(true);
|
|
61584
|
+
},
|
|
61545
61585
|
loading: "lazy",
|
|
61546
61586
|
decoding: "async",
|
|
61547
61587
|
referrerPolicy: "no-referrer"
|
|
61548
|
-
}
|
|
61588
|
+
});
|
|
61549
61589
|
}
|
|
61550
61590
|
/** The bracketed source text a reference reverts to when its definition is missing. */
|
|
61551
61591
|
function referenceSuffix(node) {
|
|
@@ -61569,7 +61609,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61569
61609
|
function renderImageReference(node, key, context) {
|
|
61570
61610
|
const definition = context.targets.definitions.get(node.identifier.toUpperCase());
|
|
61571
61611
|
if (definition === void 0) return `![${node.alt ?? ""}${referenceSuffix(node)}`;
|
|
61572
|
-
return renderImage(definition.url, node.alt ?? "", key);
|
|
61612
|
+
return renderImage(definition.url, node.alt ?? "", key, context);
|
|
61573
61613
|
}
|
|
61574
61614
|
function renderFootnoteReference(node, key, context) {
|
|
61575
61615
|
const id = node.identifier.toUpperCase();
|
|
@@ -61630,7 +61670,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61630
61670
|
* full parse self-heals it.
|
|
61631
61671
|
*/
|
|
61632
61672
|
/** One settled full render: parse with math, resolve references, append the footnote section. */
|
|
61633
|
-
function renderSettled(text, labels, fileMentions) {
|
|
61673
|
+
function renderSettled(text, labels, fileMentions, pathImages) {
|
|
61634
61674
|
const root = parseGfmWithMath(text);
|
|
61635
61675
|
const targets = createReferenceTargets();
|
|
61636
61676
|
collectReferenceTargets(root.children, targets);
|
|
@@ -61638,6 +61678,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61638
61678
|
streaming: false,
|
|
61639
61679
|
labels,
|
|
61640
61680
|
fileMentions,
|
|
61681
|
+
pathImages,
|
|
61641
61682
|
targets,
|
|
61642
61683
|
footnoteOrder: [],
|
|
61643
61684
|
footnoteCounts: /* @__PURE__ */ new Map()
|
|
@@ -61704,6 +61745,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61704
61745
|
streaming: true,
|
|
61705
61746
|
labels: this.labels,
|
|
61706
61747
|
fileMentions: void 0,
|
|
61748
|
+
pathImages: void 0,
|
|
61707
61749
|
targets: frameTargets,
|
|
61708
61750
|
footnoteOrder: this.frozenFootnoteOrder,
|
|
61709
61751
|
footnoteCounts: this.frozenFootnoteCounts
|
|
@@ -61720,6 +61762,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61720
61762
|
streaming: true,
|
|
61721
61763
|
labels: this.labels,
|
|
61722
61764
|
fileMentions: void 0,
|
|
61765
|
+
pathImages: void 0,
|
|
61723
61766
|
targets: frameTargets,
|
|
61724
61767
|
footnoteOrder: [...this.frozenFootnoteOrder],
|
|
61725
61768
|
footnoteCounts: new Map(this.frozenFootnoteCounts)
|
|
@@ -61736,13 +61779,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61736
61779
|
return this.lastRendered;
|
|
61737
61780
|
}
|
|
61738
61781
|
};
|
|
61739
|
-
(0, react.memo)(function MarkdownText({ text, streaming = false, labels, fileMentions }) {
|
|
61782
|
+
(0, react.memo)(function MarkdownText({ text, streaming = false, labels, fileMentions, pathImages }) {
|
|
61740
61783
|
const streamRef = (0, react.useRef)(null);
|
|
61741
61784
|
const streamLabelsRef = (0, react.useRef)(labels);
|
|
61742
61785
|
const children = (0, react.useMemo)(() => {
|
|
61743
61786
|
if (!streaming) {
|
|
61744
61787
|
streamRef.current = null;
|
|
61745
|
-
return renderSettled(text, labels, fileMentions);
|
|
61788
|
+
return renderSettled(text, labels, fileMentions, pathImages);
|
|
61746
61789
|
}
|
|
61747
61790
|
if (streamRef.current === null || streamLabelsRef.current !== labels) {
|
|
61748
61791
|
streamRef.current = new StreamingRenderer(labels);
|
|
@@ -61753,7 +61796,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
61753
61796
|
text,
|
|
61754
61797
|
streaming,
|
|
61755
61798
|
labels,
|
|
61756
|
-
fileMentions
|
|
61799
|
+
fileMentions,
|
|
61800
|
+
pathImages
|
|
61757
61801
|
]);
|
|
61758
61802
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
61759
61803
|
className: MarkdownText_module_default.markdown,
|
package/lib/index.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
|
-
import {
|
|
3
|
+
import { pluginDataPath } from "@dsh-plus/shared";
|
|
4
4
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
6
7
|
import nodemailer from "nodemailer";
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region src/config.ts
|
|
9
10
|
/**
|
|
10
11
|
* 配置单一事实源:cordis 行级 Config(组合默认值,dev/prod patch 层可覆盖)
|
|
11
12
|
* 与 settings namespace(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)
|
|
12
|
-
* 共用同一 schemastery schema。SMTP
|
|
13
|
+
* 共用同一 schemastery schema。SMTP 密码走官方 credentials seam
|
|
14
|
+
* (refs/NOTIFY_EMAIL_SMTP_PASS),不进 settings.yaml(存储规范)。
|
|
13
15
|
* @module notify-email/config
|
|
14
16
|
*/
|
|
15
17
|
/** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
|
|
16
18
|
const SETTINGS_NS = "dsh-plus-notify-email";
|
|
19
|
+
/** SMTP 密码在官方 credentials seam 中的引用名。 */
|
|
20
|
+
const SMTP_PASS_REF = "NOTIFY_EMAIL_SMTP_PASS";
|
|
17
21
|
const SmtpSchema = z.object({
|
|
18
22
|
host: z.string().description("SMTP 服务器主机名").default(""),
|
|
19
23
|
port: z.natural().max(65535).description("SMTP 端口").default(465),
|
|
20
24
|
secure: z.boolean().description("使用 TLS 直连(465 端口通常为 true)").default(true),
|
|
21
25
|
user: z.string().description("SMTP 登录用户名(通常为发件邮箱)").default(""),
|
|
22
|
-
pass: z.string().role("secret").description("SMTP
|
|
26
|
+
pass: z.string().role("secret").description("SMTP 密码/授权码(仅兼容行级注入;用户层请配置 credentials 引用)").default(""),
|
|
23
27
|
from: z.string().description("发件地址(From 头)").default("")
|
|
24
28
|
});
|
|
25
29
|
const TriggerSchema = z.object({
|
|
@@ -54,7 +58,8 @@ const Config = z.object({
|
|
|
54
58
|
//#endregion
|
|
55
59
|
//#region src/audit.ts
|
|
56
60
|
/**
|
|
57
|
-
* 通知投递审计:每次发送尝试追加一行 JSONL 到
|
|
61
|
+
* 通知投递审计:每次发送尝试追加一行 JSONL 到
|
|
62
|
+
* $DSH_HOME/dsh-plus/notify-email/audit.jsonl。
|
|
58
63
|
* 让 dry-run 可观测(dev 实例 logger 不落盘),也为生产排查「为什么没收到邮件」留痕。
|
|
59
64
|
* 记录正文截断 2000 字符;永远不含 SMTP 凭据。写失败只告警不阻断。
|
|
60
65
|
* @module notify-email/audit
|
|
@@ -127,15 +132,17 @@ function registerTestApi(ctx, service) {
|
|
|
127
132
|
const CONNECTION_TIMEOUT_MS = 1e4;
|
|
128
133
|
const GREETING_TIMEOUT_MS = 1e4;
|
|
129
134
|
const SOCKET_TIMEOUT_MS = 3e4;
|
|
130
|
-
/** 真实 SMTP 通道:每次发送按当前配置新建 transport(配置热改即生效,发送低频无池化需求)。
|
|
131
|
-
|
|
135
|
+
/** 真实 SMTP 通道:每次发送按当前配置新建 transport(配置热改即生效,发送低频无池化需求)。
|
|
136
|
+
* 密码优先 credentials seam(NOTIFY_EMAIL_SMTP_PASS),缺席回落行级 smtp.pass(兼容注入场景)。 */
|
|
137
|
+
const smtpTransport = async (cfg, msg, credentials) => {
|
|
138
|
+
const pass = await resolvePass(cfg, credentials);
|
|
132
139
|
await nodemailer.createTransport({
|
|
133
140
|
host: cfg.smtp.host,
|
|
134
141
|
port: cfg.smtp.port,
|
|
135
142
|
secure: cfg.smtp.secure,
|
|
136
|
-
auth: cfg.smtp.user.length > 0 ? {
|
|
143
|
+
auth: cfg.smtp.user.length > 0 && pass !== null ? {
|
|
137
144
|
user: cfg.smtp.user,
|
|
138
|
-
pass
|
|
145
|
+
pass
|
|
139
146
|
} : void 0,
|
|
140
147
|
connectionTimeout: CONNECTION_TIMEOUT_MS,
|
|
141
148
|
greetingTimeout: GREETING_TIMEOUT_MS,
|
|
@@ -148,16 +155,26 @@ const smtpTransport = async (cfg, msg) => {
|
|
|
148
155
|
...msg.html !== void 0 ? { html: msg.html } : {}
|
|
149
156
|
});
|
|
150
157
|
};
|
|
158
|
+
/** 解析生效密码:credentials 引用优先,行级 secret 兜底;两处皆空返回 null。 */
|
|
159
|
+
async function resolvePass(cfg, credentials) {
|
|
160
|
+
if (credentials !== void 0) {
|
|
161
|
+
const resolved = await credentials.resolve(credentialRef(SMTP_PASS_REF)).catch(() => void 0);
|
|
162
|
+
if (resolved !== void 0 && resolved.value.length > 0) return resolved.value;
|
|
163
|
+
}
|
|
164
|
+
return cfg.smtp.pass.length > 0 ? cfg.smtp.pass : null;
|
|
165
|
+
}
|
|
151
166
|
var Mailer = class {
|
|
152
167
|
resolveConfig;
|
|
153
168
|
logger;
|
|
154
169
|
transport;
|
|
155
170
|
audit;
|
|
156
|
-
|
|
171
|
+
credentials;
|
|
172
|
+
constructor(resolveConfig, logger, transport = smtpTransport, audit, credentials) {
|
|
157
173
|
this.resolveConfig = resolveConfig;
|
|
158
174
|
this.logger = logger;
|
|
159
175
|
this.transport = transport;
|
|
160
176
|
this.audit = audit;
|
|
177
|
+
this.credentials = credentials;
|
|
161
178
|
}
|
|
162
179
|
/**
|
|
163
180
|
* 常规通知发送:受 enabled 与配置完整性门禁。
|
|
@@ -181,7 +198,7 @@ var Mailer = class {
|
|
|
181
198
|
});
|
|
182
199
|
}
|
|
183
200
|
try {
|
|
184
|
-
await this.transport(cfg, msg);
|
|
201
|
+
await this.transport(cfg, msg, this.credentials);
|
|
185
202
|
return this.record(msg, cfg, {
|
|
186
203
|
ok: true,
|
|
187
204
|
detail: "sent"
|
|
@@ -414,8 +431,17 @@ function installTurnEndWatcher(ctx, service, resolveConfig) {
|
|
|
414
431
|
* - 投递:首个非空 EmailNotice 经 Mailer 发送;单个触发器抛错不影响其余。
|
|
415
432
|
* @module @dsh-plus/notify-email
|
|
416
433
|
*/
|
|
434
|
+
/** credentials seam 最小面:inject 硬声明保证时序(credentials-local init 为异步,
|
|
435
|
+
* 构造期探测 get() 可能早于其激活而拿到 undefined,导致密码解析静默降级)。 */
|
|
436
|
+
function credentialsOf(ctx) {
|
|
437
|
+
return ctx.credentials;
|
|
438
|
+
}
|
|
417
439
|
var NotifyEmailService = class extends Service {
|
|
418
|
-
static [Context.inject] = [
|
|
440
|
+
static [Context.inject] = [
|
|
441
|
+
"agents",
|
|
442
|
+
"tools",
|
|
443
|
+
"credentials"
|
|
444
|
+
];
|
|
419
445
|
triggers = [];
|
|
420
446
|
mailer;
|
|
421
447
|
current;
|
|
@@ -431,8 +457,8 @@ var NotifyEmailService = class extends Service {
|
|
|
431
457
|
});
|
|
432
458
|
});
|
|
433
459
|
const logger = ctx.logger("notify-email");
|
|
434
|
-
const audit = createJsonlAuditSink(
|
|
435
|
-
this.mailer = new Mailer(() => this.current(), logger, void 0, audit);
|
|
460
|
+
const audit = createJsonlAuditSink(pluginDataPath("notify-email", "audit.jsonl"), (message) => logger.warn(`audit write failed: ${message}`));
|
|
461
|
+
this.mailer = new Mailer(() => this.current(), logger, void 0, audit, credentialsOf(ctx));
|
|
436
462
|
this.registerTrigger(createDecisionTrigger(() => this.current()));
|
|
437
463
|
this.registerTrigger(createTurnEndTrigger(() => this.current()));
|
|
438
464
|
installDecisionWatcher(ctx, this);
|
package/lib/style.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.nVxsMW_block {
|
|
2
2
|
--dsl-diff-radius: 12px;
|
|
3
3
|
--dsl-diff-line-height: 22px;
|
|
4
4
|
color: var(--dsw-alias-label-primary);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
position: relative;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.
|
|
11
|
+
.nVxsMW_copyButton {
|
|
12
12
|
z-index: 1;
|
|
13
13
|
color: var(--dsw-alias-label-secondary);
|
|
14
14
|
cursor: pointer;
|
|
@@ -22,46 +22,46 @@
|
|
|
22
22
|
right: 12px;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.
|
|
25
|
+
.nVxsMW_body {
|
|
26
26
|
font: var(--dsw-font-markdown-code-block);
|
|
27
27
|
padding: 12px 14px;
|
|
28
28
|
overflow: auto hidden;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
.
|
|
31
|
+
.nVxsMW_line {
|
|
32
32
|
min-height: var(--dsl-diff-line-height);
|
|
33
33
|
white-space: pre;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.
|
|
36
|
+
.nVxsMW_path {
|
|
37
37
|
color: var(--dsw-alias-label-primary);
|
|
38
38
|
padding-right: 56px;
|
|
39
39
|
font-weight: 600;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.
|
|
42
|
+
.nVxsMW_gap {
|
|
43
43
|
color: var(--dsw-alias-label-tertiary);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
.
|
|
46
|
+
.nVxsMW_del:before {
|
|
47
47
|
content: "- ";
|
|
48
48
|
color: var(--dsw-alias-state-error-primary);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
.
|
|
51
|
+
.nVxsMW_del {
|
|
52
52
|
color: var(--dsw-alias-state-error-primary);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
.
|
|
55
|
+
.nVxsMW_add:before {
|
|
56
56
|
content: "+ ";
|
|
57
57
|
color: var(--dsw-alias-state-success-primary);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
.
|
|
60
|
+
.nVxsMW_add {
|
|
61
61
|
color: var(--dsw-alias-state-success-primary);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
.
|
|
64
|
+
.nVxsMW_expand {
|
|
65
65
|
width: 100%;
|
|
66
66
|
color: var(--dsw-alias-label-tertiary);
|
|
67
67
|
cursor: pointer;
|
|
@@ -73,16 +73,16 @@
|
|
|
73
73
|
display: block;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
.
|
|
76
|
+
.nVxsMW_expand:hover {
|
|
77
77
|
color: var(--dsw-alias-label-secondary);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
.
|
|
80
|
+
.nVxsMW_footer {
|
|
81
81
|
font: var(--dsw-font-markdown-code-block);
|
|
82
82
|
color: var(--dsw-alias-label-tertiary);
|
|
83
83
|
padding: 0 14px 12px;
|
|
84
84
|
}
|
|
85
|
-
.
|
|
85
|
+
.Hbqh2W_block {
|
|
86
86
|
--dsl-code-block-banner-background-color: var(--dsw-alias-markdown-code-block-banner);
|
|
87
87
|
--dsl-code-block-border-radius: 12px;
|
|
88
88
|
--dsl-code-block-banner-font: 11px/18px var(--dsw-font-family);
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
position: relative;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.
|
|
97
|
+
.Hbqh2W_block:not(:last-child) {
|
|
98
98
|
margin-bottom: 11px;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
.
|
|
101
|
+
.Hbqh2W_bannerWrap {
|
|
102
102
|
z-index: 6;
|
|
103
103
|
background-color: var(--dsw-alias-bg-base);
|
|
104
104
|
border-top-left-radius: var(--dsl-code-block-border-radius);
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
top: 0;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
.
|
|
110
|
+
.Hbqh2W_banner {
|
|
111
111
|
background: var(--dsl-code-block-banner-background-color);
|
|
112
112
|
font: var(--dsl-code-block-banner-font);
|
|
113
113
|
border-top-left-radius: var(--dsl-code-block-border-radius);
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
display: flex;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.
|
|
122
|
+
.Hbqh2W_infostring {
|
|
123
123
|
color: var(--dsw-alias-label-primary);
|
|
124
124
|
font-family: var(--ds-font-family-code);
|
|
125
125
|
text-overflow: ellipsis;
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
overflow: hidden;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.
|
|
133
|
+
.Hbqh2W_action {
|
|
134
134
|
flex-shrink: 0;
|
|
135
135
|
align-items: center;
|
|
136
136
|
display: flex;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
.
|
|
139
|
+
.Hbqh2W_copyButton {
|
|
140
140
|
color: inherit;
|
|
141
141
|
cursor: pointer;
|
|
142
142
|
font: inherit;
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
padding: 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
.
|
|
149
|
+
.Hbqh2W_block :where(pre) {
|
|
150
150
|
font: var(--dsl-code-block-content-font);
|
|
151
151
|
white-space: pre-wrap;
|
|
152
152
|
word-break: break-all;
|
|
@@ -158,72 +158,72 @@
|
|
|
158
158
|
margin: 0 !important;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
.
|
|
161
|
+
.Hbqh2W_block :where(pre.Hbqh2W_shiki) {
|
|
162
162
|
background: var(--dsw-alias-markdown-code-block) !important;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
.
|
|
165
|
+
.Hbqh2W_block :where(pre) code {
|
|
166
166
|
font: inherit;
|
|
167
167
|
background: none;
|
|
168
168
|
padding: 0;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
.
|
|
171
|
+
.Hbqh2W_plain {
|
|
172
172
|
color: var(--dsw-alias-label-primary);
|
|
173
173
|
}
|
|
174
|
-
.
|
|
174
|
+
.BL43XG_markdown {
|
|
175
175
|
overflow-wrap: anywhere;
|
|
176
176
|
min-width: 0;
|
|
177
177
|
font: var(--dsw-font-markdown-base);
|
|
178
178
|
color: var(--dsw-alias-label-primary);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
.
|
|
181
|
+
.BL43XG_markdown strong {
|
|
182
182
|
font-weight: 600;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
.
|
|
185
|
+
.BL43XG_markdown h1 {
|
|
186
186
|
font: var(--dsw-font-markdown-h1);
|
|
187
187
|
margin: 32px 0 16px;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
.
|
|
190
|
+
.BL43XG_markdown h2 {
|
|
191
191
|
font: var(--dsw-font-markdown-h2);
|
|
192
192
|
margin: 32px 0 16px;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
.
|
|
195
|
+
.BL43XG_markdown h3 {
|
|
196
196
|
font: var(--dsw-font-markdown-h3);
|
|
197
197
|
margin: 32px 0 16px;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
.
|
|
200
|
+
.BL43XG_markdown h4 {
|
|
201
201
|
font: var(--dsw-font-markdown-h4);
|
|
202
202
|
margin: 16px 0;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
.
|
|
205
|
+
.BL43XG_markdown :where(h5, h6) {
|
|
206
206
|
font: var(--dsw-font-markdown-base-strong);
|
|
207
207
|
margin: 16px 0;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
.
|
|
210
|
+
.BL43XG_markdown :where(h1, h2, h3, h4, h5, h6) strong {
|
|
211
211
|
font-weight: inherit;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
.
|
|
214
|
+
.BL43XG_markdown p {
|
|
215
215
|
margin: 16px 0;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
.
|
|
218
|
+
.BL43XG_markdown :where(h4, h5, h6) + :where(ul, ol) {
|
|
219
219
|
margin-top: 8px;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
.
|
|
222
|
+
.BL43XG_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
|
|
223
223
|
margin-bottom: 8px;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
.
|
|
226
|
+
.BL43XG_markdown a {
|
|
227
227
|
color: var(--dsw-alias-link);
|
|
228
228
|
transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
229
229
|
border: 2px solid #fff0;
|
|
@@ -235,17 +235,17 @@
|
|
|
235
235
|
position: relative;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
.
|
|
238
|
+
.BL43XG_markdown a:hover, .BL43XG_markdown a:focus {
|
|
239
239
|
text-decoration: underline dotted var(--dsw-alias-link);
|
|
240
240
|
text-underline-offset: 3px;
|
|
241
241
|
outline: none;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
.
|
|
244
|
+
.BL43XG_markdown a:focus-visible {
|
|
245
245
|
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
.
|
|
248
|
+
.BL43XG_linkIcon {
|
|
249
249
|
vertical-align: -.25em;
|
|
250
250
|
flex: none;
|
|
251
251
|
width: 1.1em;
|
|
@@ -253,46 +253,46 @@
|
|
|
253
253
|
margin-right: 5px;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
.
|
|
256
|
+
.BL43XG_markdown :where(ul, ol) {
|
|
257
257
|
margin: 16px 0;
|
|
258
258
|
padding-left: 18px;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
.
|
|
261
|
+
.BL43XG_markdown li:not(:first-child) {
|
|
262
262
|
margin-top: 6px;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
.
|
|
265
|
+
.BL43XG_markdown li > :where(ul, ol) {
|
|
266
266
|
margin-top: 4px;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
.
|
|
269
|
+
.BL43XG_markdown li::marker {
|
|
270
270
|
color: var(--dsw-alias-label-secondary);
|
|
271
271
|
line-height: 24px;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
.
|
|
274
|
+
.BL43XG_markdown :where(ul, ol) ol {
|
|
275
275
|
padding-left: 0;
|
|
276
276
|
list-style-position: inside;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
.
|
|
279
|
+
.BL43XG_markdown :where(ul, ol) ol li p {
|
|
280
280
|
display: inline;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
.
|
|
283
|
+
.BL43XG_markdown li > p {
|
|
284
284
|
margin: 8px 0;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
.
|
|
287
|
+
.BL43XG_markdown li > :first-child {
|
|
288
288
|
margin-top: 0;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
.
|
|
291
|
+
.BL43XG_markdown li > :last-child:not(.md-code-block) {
|
|
292
292
|
margin-bottom: 0;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
.
|
|
295
|
+
.BL43XG_markdown hr {
|
|
296
296
|
background: var(--dsw-alias-border-l2);
|
|
297
297
|
border: none;
|
|
298
298
|
height: .5px;
|
|
@@ -300,19 +300,19 @@
|
|
|
300
300
|
display: block;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
.
|
|
303
|
+
.BL43XG_markdown blockquote {
|
|
304
304
|
border-left: 2px solid var(--dsw-alias-label-caption);
|
|
305
305
|
margin: 16px 0 0;
|
|
306
306
|
padding-left: 14px;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
.
|
|
309
|
+
.BL43XG_markdown pre {
|
|
310
310
|
font-family: var(--ds-font-family-code);
|
|
311
311
|
margin: 16px 0;
|
|
312
312
|
overflow: auto;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
.
|
|
315
|
+
.BL43XG_markdown :not(pre) > code {
|
|
316
316
|
box-sizing: border-box;
|
|
317
317
|
font: var(--dsw-font-markdown-code);
|
|
318
318
|
font-family: var(--ds-font-family-code);
|
|
@@ -325,54 +325,54 @@
|
|
|
325
325
|
font-size: .875em !important;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
.
|
|
328
|
+
.BL43XG_markdown :where(h1, h2, h3, h4, h5, h6) code {
|
|
329
329
|
font: inherit;
|
|
330
330
|
font-family: var(--ds-font-family-code);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
.
|
|
333
|
+
.BL43XG_markdown .katex-display {
|
|
334
334
|
max-width: 100%;
|
|
335
335
|
overflow: auto hidden;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
.
|
|
338
|
+
.BL43XG_markdown input[type="checkbox"] {
|
|
339
339
|
accent-color: var(--dsw-alias-label-secondary);
|
|
340
340
|
margin: 0 8px 0 0;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
.
|
|
343
|
+
.BL43XG_tableScroll {
|
|
344
344
|
overscroll-behavior-x: contain;
|
|
345
345
|
max-width: 100%;
|
|
346
346
|
overflow-x: auto;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
.
|
|
349
|
+
.BL43XG_tableScroll.md-table-wide {
|
|
350
350
|
padding-bottom: var(--dsh-scrollbar-width, 8px);
|
|
351
351
|
overflow-x: hidden;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
.
|
|
354
|
+
.BL43XG_tableScroll.md-table-wide:hover, .BL43XG_tableScroll.md-table-wide:focus-visible {
|
|
355
355
|
padding-bottom: 0;
|
|
356
356
|
overflow-x: auto;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
.
|
|
359
|
+
.BL43XG_tableScroll:focus-visible {
|
|
360
360
|
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
|
|
361
361
|
outline: none;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
.
|
|
364
|
+
.BL43XG_tableScroll table {
|
|
365
365
|
border-collapse: collapse;
|
|
366
366
|
width: max-content;
|
|
367
367
|
max-width: max-content;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
.
|
|
370
|
+
.BL43XG_tableFill table {
|
|
371
371
|
width: 100%;
|
|
372
372
|
max-width: none;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
.
|
|
375
|
+
.BL43XG_tableScroll th {
|
|
376
376
|
text-align: start;
|
|
377
377
|
border-bottom: .5px solid var(--dsw-alias-border-l3);
|
|
378
378
|
font: var(--dsw-font-markdown-table-head);
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
padding: 10px 16px;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
.
|
|
385
|
+
.BL43XG_tableScroll td {
|
|
386
386
|
border-bottom: .5px solid var(--dsw-alias-border-l2);
|
|
387
387
|
font: var(--dsw-font-markdown-table);
|
|
388
388
|
min-width: 100px;
|
|
@@ -390,32 +390,32 @@
|
|
|
390
390
|
padding: 10px 16px;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
.
|
|
393
|
+
.BL43XG_tableScroll th:first-child, .BL43XG_tableScroll td:first-child {
|
|
394
394
|
padding-left: 0;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
.
|
|
397
|
+
.BL43XG_tableScroll td:last-child {
|
|
398
398
|
padding-right: 0;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
.
|
|
401
|
+
.BL43XG_tableScroll table code {
|
|
402
402
|
font-size: 11px;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
.
|
|
405
|
+
.BL43XG_markdown > :first-child, .BL43XG_markdown p:first-child {
|
|
406
406
|
margin-top: 0 !important;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
.
|
|
409
|
+
.BL43XG_markdown > :last-child, .BL43XG_markdown p:last-child {
|
|
410
410
|
margin-bottom: 0 !important;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
.
|
|
413
|
+
.BL43XG_imageAlt {
|
|
414
414
|
color: var(--dsw-alias-label-tertiary);
|
|
415
415
|
font-style: italic;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
.
|
|
418
|
+
.BL43XG_image {
|
|
419
419
|
background: var(--dsw-alias-bg-base);
|
|
420
420
|
object-fit: contain;
|
|
421
421
|
border-radius: 8px;
|
|
@@ -426,7 +426,7 @@
|
|
|
426
426
|
display: block;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
.
|
|
429
|
+
.BL43XG_fileMention {
|
|
430
430
|
font: inherit;
|
|
431
431
|
color: var(--dsw-alias-link);
|
|
432
432
|
cursor: pointer;
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
text-decoration: none;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
.
|
|
441
|
+
.BL43XG_fileMention:hover, .BL43XG_fileMention:focus {
|
|
442
442
|
text-decoration: underline dotted var(--dsw-alias-link);
|
|
443
443
|
text-underline-offset: 3px;
|
|
444
444
|
outline: none;
|
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.18",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 任务结束/等待决策/出错时向指定邮箱发送邮件通知",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -31,28 +31,30 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@dsh-plus/shared": "0.1.
|
|
34
|
+
"@dsh-plus/shared": "0.1.12",
|
|
35
35
|
"nodemailer": "9.0.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
39
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
40
|
-
"@deepseek-ai/dsh-settings": "
|
|
41
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
39
|
+
"@deepseek-ai/dsh-home-paths": "*",
|
|
40
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
42
|
+
"@deepseek-ai/dsh-credentials": "*"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@deepseek-ai/cordis": "4.0.2",
|
|
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.
|
|
46
|
+
"@deepseek-ai/dsh-agent": "0.1.5-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
|
|
50
|
+
"@deepseek-ai/dsh-session": "0.1.5-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "0.1.5-alpha.1",
|
|
52
53
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
53
54
|
"@types/nodemailer": "8.0.1",
|
|
54
55
|
"@types/react": "~18.3.31",
|
|
55
|
-
"react": "^18.2.0"
|
|
56
|
+
"react": "^18.2.0",
|
|
57
|
+
"@deepseek-ai/dsh-credentials": "0.1.5-alpha.1"
|
|
56
58
|
},
|
|
57
59
|
"publishConfig": {
|
|
58
60
|
"access": "public"
|
package/src/audit.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 通知投递审计:每次发送尝试追加一行 JSONL 到
|
|
2
|
+
* 通知投递审计:每次发送尝试追加一行 JSONL 到
|
|
3
|
+
* $DSH_HOME/dsh-plus/notify-email/audit.jsonl。
|
|
3
4
|
* 让 dry-run 可观测(dev 实例 logger 不落盘),也为生产排查「为什么没收到邮件」留痕。
|
|
4
5
|
* 记录正文截断 2000 字符;永远不含 SMTP 凭据。写失败只告警不阻断。
|
|
5
6
|
* @module notify-email/audit
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置单一事实源:cordis 行级 Config(组合默认值,dev/prod patch 层可覆盖)
|
|
3
3
|
* 与 settings namespace(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)
|
|
4
|
-
* 共用同一 schemastery schema。SMTP
|
|
4
|
+
* 共用同一 schemastery schema。SMTP 密码走官方 credentials seam
|
|
5
|
+
* (refs/NOTIFY_EMAIL_SMTP_PASS),不进 settings.yaml(存储规范)。
|
|
5
6
|
* @module notify-email/config
|
|
6
7
|
*/
|
|
7
8
|
|
|
@@ -12,12 +13,19 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
|
12
13
|
/** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
|
|
13
14
|
export const SETTINGS_NS = NS_LITERAL
|
|
14
15
|
|
|
16
|
+
/** SMTP 密码在官方 credentials seam 中的引用名。 */
|
|
17
|
+
export const SMTP_PASS_REF = 'NOTIFY_EMAIL_SMTP_PASS'
|
|
18
|
+
|
|
15
19
|
const SmtpSchema = z.object({
|
|
16
20
|
host: z.string().description('SMTP 服务器主机名').default(''),
|
|
17
21
|
port: z.natural().max(65535).description('SMTP 端口').default(465),
|
|
18
22
|
secure: z.boolean().description('使用 TLS 直连(465 端口通常为 true)').default(true),
|
|
19
23
|
user: z.string().description('SMTP 登录用户名(通常为发件邮箱)').default(''),
|
|
20
|
-
pass: z
|
|
24
|
+
pass: z
|
|
25
|
+
.string()
|
|
26
|
+
.role('secret')
|
|
27
|
+
.description('SMTP 密码/授权码(仅兼容行级注入;用户层请配置 credentials 引用)')
|
|
28
|
+
.default(''),
|
|
21
29
|
from: z.string().description('发件地址(From 头)').default(''),
|
|
22
30
|
})
|
|
23
31
|
|
package/src/mailer.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* Transport 抽象为函数注入,单元测试以假 transport 断言参数,零网络。
|
|
4
4
|
* @module notify-email/mailer
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
6
8
|
import nodemailer from 'nodemailer'
|
|
7
9
|
|
|
8
10
|
import { type AuditSink, buildAuditRecord } from './audit.ts'
|
|
9
|
-
import type
|
|
11
|
+
import { type NotifyEmailConfig, SMTP_PASS_REF } from './config.ts'
|
|
10
12
|
|
|
11
13
|
/** 一封待发邮件(触发器产物的最小契约)。 */
|
|
12
14
|
export interface MailMessage {
|
|
@@ -22,7 +24,16 @@ export interface SendResult {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** 发送通道:按生效配置投递一封邮件;失败抛错由调用方归一化。 */
|
|
25
|
-
export type Transport = (
|
|
27
|
+
export type Transport = (
|
|
28
|
+
cfg: NotifyEmailConfig,
|
|
29
|
+
msg: MailMessage,
|
|
30
|
+
credentials?: CredentialsSeamLike,
|
|
31
|
+
) => Promise<void>
|
|
32
|
+
|
|
33
|
+
/** credentials seam 最小面(Mailer 不感知完整 seam 类型)。 */
|
|
34
|
+
export interface CredentialsSeamLike {
|
|
35
|
+
resolve(ref: ReturnType<typeof credentialRef>): Promise<{ value: string } | undefined>
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
export interface MailLogger {
|
|
28
39
|
info(msg: string): void
|
|
@@ -34,13 +45,15 @@ const CONNECTION_TIMEOUT_MS = 10_000
|
|
|
34
45
|
const GREETING_TIMEOUT_MS = 10_000
|
|
35
46
|
const SOCKET_TIMEOUT_MS = 30_000
|
|
36
47
|
|
|
37
|
-
/** 真实 SMTP 通道:每次发送按当前配置新建 transport(配置热改即生效,发送低频无池化需求)。
|
|
38
|
-
|
|
48
|
+
/** 真实 SMTP 通道:每次发送按当前配置新建 transport(配置热改即生效,发送低频无池化需求)。
|
|
49
|
+
* 密码优先 credentials seam(NOTIFY_EMAIL_SMTP_PASS),缺席回落行级 smtp.pass(兼容注入场景)。 */
|
|
50
|
+
export const smtpTransport: Transport = async (cfg, msg, credentials) => {
|
|
51
|
+
const pass = await resolvePass(cfg, credentials)
|
|
39
52
|
const transporter = nodemailer.createTransport({
|
|
40
53
|
host: cfg.smtp.host,
|
|
41
54
|
port: cfg.smtp.port,
|
|
42
55
|
secure: cfg.smtp.secure,
|
|
43
|
-
auth: cfg.smtp.user.length > 0 ? { user: cfg.smtp.user, pass
|
|
56
|
+
auth: cfg.smtp.user.length > 0 && pass !== null ? { user: cfg.smtp.user, pass } : undefined,
|
|
44
57
|
connectionTimeout: CONNECTION_TIMEOUT_MS,
|
|
45
58
|
greetingTimeout: GREETING_TIMEOUT_MS,
|
|
46
59
|
socketTimeout: SOCKET_TIMEOUT_MS,
|
|
@@ -54,22 +67,37 @@ export const smtpTransport: Transport = async (cfg, msg) => {
|
|
|
54
67
|
})
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
/** 解析生效密码:credentials 引用优先,行级 secret 兜底;两处皆空返回 null。 */
|
|
71
|
+
async function resolvePass(
|
|
72
|
+
cfg: NotifyEmailConfig,
|
|
73
|
+
credentials: CredentialsSeamLike | undefined,
|
|
74
|
+
): Promise<string | null> {
|
|
75
|
+
if (credentials !== undefined) {
|
|
76
|
+
const resolved = await credentials.resolve(credentialRef(SMTP_PASS_REF)).catch(() => undefined)
|
|
77
|
+
if (resolved !== undefined && resolved.value.length > 0) return resolved.value
|
|
78
|
+
}
|
|
79
|
+
return cfg.smtp.pass.length > 0 ? cfg.smtp.pass : null
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
export class Mailer {
|
|
58
83
|
private readonly resolveConfig: () => NotifyEmailConfig
|
|
59
84
|
private readonly logger: MailLogger
|
|
60
85
|
private readonly transport: Transport
|
|
61
86
|
private readonly audit?: AuditSink
|
|
87
|
+
private readonly credentials?: CredentialsSeamLike
|
|
62
88
|
|
|
63
89
|
constructor(
|
|
64
90
|
resolveConfig: () => NotifyEmailConfig,
|
|
65
91
|
logger: MailLogger,
|
|
66
92
|
transport: Transport = smtpTransport,
|
|
67
93
|
audit?: AuditSink,
|
|
94
|
+
credentials?: CredentialsSeamLike,
|
|
68
95
|
) {
|
|
69
96
|
this.resolveConfig = resolveConfig
|
|
70
97
|
this.logger = logger
|
|
71
98
|
this.transport = transport
|
|
72
99
|
this.audit = audit
|
|
100
|
+
this.credentials = credentials
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
/**
|
|
@@ -87,7 +115,7 @@ export class Mailer {
|
|
|
87
115
|
return this.record(msg, cfg, { ok: true, detail: 'dry-run' })
|
|
88
116
|
}
|
|
89
117
|
try {
|
|
90
|
-
await this.transport(cfg, msg)
|
|
118
|
+
await this.transport(cfg, msg, this.credentials)
|
|
91
119
|
return this.record(msg, cfg, { ok: true, detail: 'sent' })
|
|
92
120
|
} catch (error) {
|
|
93
121
|
const message = error instanceof Error ? error.message : String(error)
|
package/src/service.ts
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* @module @dsh-plus/notify-email
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
10
|
-
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
11
10
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
11
|
+
import { pluginDataPath } from '@dsh-plus/shared'
|
|
12
12
|
import { createJsonlAuditSink } from './audit.ts'
|
|
13
13
|
import { Config, type NotifyEmailConfig, SETTINGS_NS } from './config.ts'
|
|
14
14
|
import { registerTestApi } from './config-api.ts'
|
|
15
|
-
import { Mailer } from './mailer.ts'
|
|
15
|
+
import { type CredentialsSeamLike, Mailer } from './mailer.ts'
|
|
16
16
|
import { createDecisionTrigger, createTurnEndTrigger } from './triggers/builtin.ts'
|
|
17
17
|
import type { DecisionCall, NotifyTrigger, TurnEndInfo } from './triggers/types.ts'
|
|
18
18
|
import { installDecisionWatcher } from './watchers/decision.ts'
|
|
@@ -24,8 +24,14 @@ declare module '@deepseek-ai/cordis' {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** credentials seam 最小面:inject 硬声明保证时序(credentials-local init 为异步,
|
|
28
|
+
* 构造期探测 get() 可能早于其激活而拿到 undefined,导致密码解析静默降级)。 */
|
|
29
|
+
function credentialsOf(ctx: Context): CredentialsSeamLike {
|
|
30
|
+
return (ctx as unknown as { credentials: CredentialsSeamLike }).credentials
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export class NotifyEmailService extends Service {
|
|
28
|
-
static [Context.inject] = ['agents', 'tools']
|
|
34
|
+
static [Context.inject] = ['agents', 'tools', 'credentials']
|
|
29
35
|
|
|
30
36
|
private readonly triggers: NotifyTrigger[] = []
|
|
31
37
|
private readonly mailer: Mailer
|
|
@@ -45,10 +51,10 @@ export class NotifyEmailService extends Service {
|
|
|
45
51
|
})
|
|
46
52
|
})
|
|
47
53
|
const logger = ctx.logger('notify-email')
|
|
48
|
-
const audit = createJsonlAuditSink(
|
|
54
|
+
const audit = createJsonlAuditSink(pluginDataPath('notify-email', 'audit.jsonl'), (message) =>
|
|
49
55
|
logger.warn(`audit write failed: ${message}`),
|
|
50
56
|
)
|
|
51
|
-
this.mailer = new Mailer(() => this.current(), logger, undefined, audit)
|
|
57
|
+
this.mailer = new Mailer(() => this.current(), logger, undefined, audit, credentialsOf(ctx))
|
|
52
58
|
this.registerTrigger(createDecisionTrigger(() => this.current()))
|
|
53
59
|
this.registerTrigger(createTurnEndTrigger(() => this.current()))
|
|
54
60
|
installDecisionWatcher(ctx, this)
|