@dsh-plus/notify-email 0.1.16 → 0.1.17

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 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": "obLtqa_add",
18004
- "block": "obLtqa_block",
18005
- "body": "obLtqa_body",
18006
- "copyButton": "obLtqa_copyButton",
18007
- "del": "obLtqa_del",
18008
- "expand": "obLtqa_expand",
18009
- "footer": "obLtqa_footer",
18010
- "gap": "obLtqa_gap",
18011
- "line": "obLtqa_line",
18012
- "path": "obLtqa_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.3-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/CodeBlock.module.css
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": "folzqq_action",
59250
- "banner": "folzqq_banner",
59251
- "bannerWrap": "folzqq_bannerWrap",
59252
- "block": "folzqq_block",
59253
- "copyButton": "folzqq_copyButton",
59254
- "infostring": "folzqq_infostring",
59255
- "plain": "folzqq_plain",
59256
- "shiki": "folzqq_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.3-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css
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": "vrxa6G_fileMention",
59262
- "image": "vrxa6G_image",
59263
- "imageAlt": "vrxa6G_imageAlt",
59264
- "linkIcon": "vrxa6G_linkIcon",
59265
- "markdown": "vrxa6G_markdown",
59266
- "tableFill": "vrxa6G_tableFill",
59267
- "tableScroll": "vrxa6G_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.3-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/index.js
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 = remoteImageUrl(sanitizeUrl(normalizeUri(url)));
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: imageSrc,
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
- }, key);
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 { dshHomePath } from "@deepseek-ai/dsh-home-paths";
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 密码标记 secret 角色,任何读取通道不得回传。
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 密码/授权码").default(""),
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 到 $DSH_HOME/logs/notify-email.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
- const smtpTransport = async (cfg, msg) => {
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: cfg.smtp.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
- constructor(resolveConfig, logger, transport = smtpTransport, audit) {
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,6 +431,11 @@ function installTurnEndWatcher(ctx, service, resolveConfig) {
414
431
  * - 投递:首个非空 EmailNotice 经 Mailer 发送;单个触发器抛错不影响其余。
415
432
  * @module @dsh-plus/notify-email
416
433
  */
434
+ /** credentials seam 最小面提取(缺席返回 undefined;Mailer 自行降级行级密码)。 */
435
+ function credentialsOf(ctx) {
436
+ const found = ctx.get?.("credentials");
437
+ return found !== null && typeof found === "object" ? found : void 0;
438
+ }
417
439
  var NotifyEmailService = class extends Service {
418
440
  static [Context.inject] = ["agents", "tools"];
419
441
  triggers = [];
@@ -431,8 +453,8 @@ var NotifyEmailService = class extends Service {
431
453
  });
432
454
  });
433
455
  const logger = ctx.logger("notify-email");
434
- const audit = createJsonlAuditSink(dshHomePath("logs", "notify-email.jsonl"), (message) => logger.warn(`audit write failed: ${message}`));
435
- this.mailer = new Mailer(() => this.current(), logger, void 0, audit);
456
+ const audit = createJsonlAuditSink(pluginDataPath("notify-email", "audit.jsonl"), (message) => logger.warn(`audit write failed: ${message}`));
457
+ this.mailer = new Mailer(() => this.current(), logger, void 0, audit, credentialsOf(ctx));
436
458
  this.registerTrigger(createDecisionTrigger(() => this.current()));
437
459
  this.registerTrigger(createTurnEndTrigger(() => this.current()));
438
460
  installDecisionWatcher(ctx, this);
package/lib/style.css CHANGED
@@ -1,4 +1,4 @@
1
- .obLtqa_block {
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
- .obLtqa_copyButton {
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
- .obLtqa_body {
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
- .obLtqa_line {
31
+ .nVxsMW_line {
32
32
  min-height: var(--dsl-diff-line-height);
33
33
  white-space: pre;
34
34
  }
35
35
 
36
- .obLtqa_path {
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
- .obLtqa_gap {
42
+ .nVxsMW_gap {
43
43
  color: var(--dsw-alias-label-tertiary);
44
44
  }
45
45
 
46
- .obLtqa_del:before {
46
+ .nVxsMW_del:before {
47
47
  content: "- ";
48
48
  color: var(--dsw-alias-state-error-primary);
49
49
  }
50
50
 
51
- .obLtqa_del {
51
+ .nVxsMW_del {
52
52
  color: var(--dsw-alias-state-error-primary);
53
53
  }
54
54
 
55
- .obLtqa_add:before {
55
+ .nVxsMW_add:before {
56
56
  content: "+ ";
57
57
  color: var(--dsw-alias-state-success-primary);
58
58
  }
59
59
 
60
- .obLtqa_add {
60
+ .nVxsMW_add {
61
61
  color: var(--dsw-alias-state-success-primary);
62
62
  }
63
63
 
64
- .obLtqa_expand {
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
- .obLtqa_expand:hover {
76
+ .nVxsMW_expand:hover {
77
77
  color: var(--dsw-alias-label-secondary);
78
78
  }
79
79
 
80
- .obLtqa_footer {
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
- .folzqq_block {
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
- .folzqq_block:not(:last-child) {
97
+ .Hbqh2W_block:not(:last-child) {
98
98
  margin-bottom: 11px;
99
99
  }
100
100
 
101
- .folzqq_bannerWrap {
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
- .folzqq_banner {
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
- .folzqq_infostring {
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
- .folzqq_action {
133
+ .Hbqh2W_action {
134
134
  flex-shrink: 0;
135
135
  align-items: center;
136
136
  display: flex;
137
137
  }
138
138
 
139
- .folzqq_copyButton {
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
- .folzqq_block :where(pre) {
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
- .folzqq_block :where(pre.folzqq_shiki) {
161
+ .Hbqh2W_block :where(pre.Hbqh2W_shiki) {
162
162
  background: var(--dsw-alias-markdown-code-block) !important;
163
163
  }
164
164
 
165
- .folzqq_block :where(pre) code {
165
+ .Hbqh2W_block :where(pre) code {
166
166
  font: inherit;
167
167
  background: none;
168
168
  padding: 0;
169
169
  }
170
170
 
171
- .folzqq_plain {
171
+ .Hbqh2W_plain {
172
172
  color: var(--dsw-alias-label-primary);
173
173
  }
174
- .vrxa6G_markdown {
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
- .vrxa6G_markdown strong {
181
+ .BL43XG_markdown strong {
182
182
  font-weight: 600;
183
183
  }
184
184
 
185
- .vrxa6G_markdown h1 {
185
+ .BL43XG_markdown h1 {
186
186
  font: var(--dsw-font-markdown-h1);
187
187
  margin: 32px 0 16px;
188
188
  }
189
189
 
190
- .vrxa6G_markdown h2 {
190
+ .BL43XG_markdown h2 {
191
191
  font: var(--dsw-font-markdown-h2);
192
192
  margin: 32px 0 16px;
193
193
  }
194
194
 
195
- .vrxa6G_markdown h3 {
195
+ .BL43XG_markdown h3 {
196
196
  font: var(--dsw-font-markdown-h3);
197
197
  margin: 32px 0 16px;
198
198
  }
199
199
 
200
- .vrxa6G_markdown h4 {
200
+ .BL43XG_markdown h4 {
201
201
  font: var(--dsw-font-markdown-h4);
202
202
  margin: 16px 0;
203
203
  }
204
204
 
205
- .vrxa6G_markdown :where(h5, h6) {
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
- .vrxa6G_markdown :where(h1, h2, h3, h4, h5, h6) strong {
210
+ .BL43XG_markdown :where(h1, h2, h3, h4, h5, h6) strong {
211
211
  font-weight: inherit;
212
212
  }
213
213
 
214
- .vrxa6G_markdown p {
214
+ .BL43XG_markdown p {
215
215
  margin: 16px 0;
216
216
  }
217
217
 
218
- .vrxa6G_markdown :where(h4, h5, h6) + :where(ul, ol) {
218
+ .BL43XG_markdown :where(h4, h5, h6) + :where(ul, ol) {
219
219
  margin-top: 8px;
220
220
  }
221
221
 
222
- .vrxa6G_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
222
+ .BL43XG_markdown :where(h4, h5, h6):has( + :where(ul, ol)) {
223
223
  margin-bottom: 8px;
224
224
  }
225
225
 
226
- .vrxa6G_markdown a {
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
- .vrxa6G_markdown a:hover, .vrxa6G_markdown a:focus {
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
- .vrxa6G_markdown a:focus-visible {
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
- .vrxa6G_linkIcon {
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
- .vrxa6G_markdown :where(ul, ol) {
256
+ .BL43XG_markdown :where(ul, ol) {
257
257
  margin: 16px 0;
258
258
  padding-left: 18px;
259
259
  }
260
260
 
261
- .vrxa6G_markdown li:not(:first-child) {
261
+ .BL43XG_markdown li:not(:first-child) {
262
262
  margin-top: 6px;
263
263
  }
264
264
 
265
- .vrxa6G_markdown li > :where(ul, ol) {
265
+ .BL43XG_markdown li > :where(ul, ol) {
266
266
  margin-top: 4px;
267
267
  }
268
268
 
269
- .vrxa6G_markdown li::marker {
269
+ .BL43XG_markdown li::marker {
270
270
  color: var(--dsw-alias-label-secondary);
271
271
  line-height: 24px;
272
272
  }
273
273
 
274
- .vrxa6G_markdown :where(ul, ol) ol {
274
+ .BL43XG_markdown :where(ul, ol) ol {
275
275
  padding-left: 0;
276
276
  list-style-position: inside;
277
277
  }
278
278
 
279
- .vrxa6G_markdown :where(ul, ol) ol li p {
279
+ .BL43XG_markdown :where(ul, ol) ol li p {
280
280
  display: inline;
281
281
  }
282
282
 
283
- .vrxa6G_markdown li > p {
283
+ .BL43XG_markdown li > p {
284
284
  margin: 8px 0;
285
285
  }
286
286
 
287
- .vrxa6G_markdown li > :first-child {
287
+ .BL43XG_markdown li > :first-child {
288
288
  margin-top: 0;
289
289
  }
290
290
 
291
- .vrxa6G_markdown li > :last-child:not(.md-code-block) {
291
+ .BL43XG_markdown li > :last-child:not(.md-code-block) {
292
292
  margin-bottom: 0;
293
293
  }
294
294
 
295
- .vrxa6G_markdown hr {
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
- .vrxa6G_markdown blockquote {
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
- .vrxa6G_markdown pre {
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
- .vrxa6G_markdown :not(pre) > code {
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
- .vrxa6G_markdown :where(h1, h2, h3, h4, h5, h6) code {
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
- .vrxa6G_markdown .katex-display {
333
+ .BL43XG_markdown .katex-display {
334
334
  max-width: 100%;
335
335
  overflow: auto hidden;
336
336
  }
337
337
 
338
- .vrxa6G_markdown input[type="checkbox"] {
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
- .vrxa6G_tableScroll {
343
+ .BL43XG_tableScroll {
344
344
  overscroll-behavior-x: contain;
345
345
  max-width: 100%;
346
346
  overflow-x: auto;
347
347
  }
348
348
 
349
- .vrxa6G_tableScroll.md-table-wide {
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
- .vrxa6G_tableScroll.md-table-wide:hover, .vrxa6G_tableScroll.md-table-wide:focus-visible {
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
- .vrxa6G_tableScroll:focus-visible {
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
- .vrxa6G_tableScroll table {
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
- .vrxa6G_tableFill table {
370
+ .BL43XG_tableFill table {
371
371
  width: 100%;
372
372
  max-width: none;
373
373
  }
374
374
 
375
- .vrxa6G_tableScroll th {
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
- .vrxa6G_tableScroll td {
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
- .vrxa6G_tableScroll th:first-child, .vrxa6G_tableScroll td:first-child {
393
+ .BL43XG_tableScroll th:first-child, .BL43XG_tableScroll td:first-child {
394
394
  padding-left: 0;
395
395
  }
396
396
 
397
- .vrxa6G_tableScroll td:last-child {
397
+ .BL43XG_tableScroll td:last-child {
398
398
  padding-right: 0;
399
399
  }
400
400
 
401
- .vrxa6G_tableScroll table code {
401
+ .BL43XG_tableScroll table code {
402
402
  font-size: 11px;
403
403
  }
404
404
 
405
- .vrxa6G_markdown > :first-child, .vrxa6G_markdown p:first-child {
405
+ .BL43XG_markdown > :first-child, .BL43XG_markdown p:first-child {
406
406
  margin-top: 0 !important;
407
407
  }
408
408
 
409
- .vrxa6G_markdown > :last-child, .vrxa6G_markdown p:last-child {
409
+ .BL43XG_markdown > :last-child, .BL43XG_markdown p:last-child {
410
410
  margin-bottom: 0 !important;
411
411
  }
412
412
 
413
- .vrxa6G_imageAlt {
413
+ .BL43XG_imageAlt {
414
414
  color: var(--dsw-alias-label-tertiary);
415
415
  font-style: italic;
416
416
  }
417
417
 
418
- .vrxa6G_image {
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
- .vrxa6G_fileMention {
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
- .vrxa6G_fileMention:hover, .vrxa6G_fileMention:focus {
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.16",
3
+ "version": "0.1.17",
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.10",
35
- "nodemailer": "9.0.6"
34
+ "nodemailer": "9.0.6",
35
+ "@dsh-plus/shared": "0.1.11"
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-home-paths": "^0.1.3-alpha.2",
40
- "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
41
- "@deepseek-ai/schemastery": "^3.18.1"
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.3-alpha.2",
46
- "@deepseek-ai/dsh-home-paths": "0.1.3-alpha.2",
47
- "@deepseek-ai/dsh-host-webserver": "0.1.3-alpha.2",
48
- "@deepseek-ai/dsh-llm": "0.1.3-alpha.2",
49
- "@deepseek-ai/dsh-session": "0.1.3-alpha.2",
50
- "@deepseek-ai/dsh-settings": "0.1.3-alpha.2",
51
- "@deepseek-ai/dsh-tools": "0.1.3-alpha.2",
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 到 $DSH_HOME/logs/notify-email.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 密码标记 secret 角色,任何读取通道不得回传。
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.string().role('secret').description('SMTP 密码/授权码').default(''),
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 { NotifyEmailConfig } from './config.ts'
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 = (cfg: NotifyEmailConfig, msg: MailMessage) => Promise<void>
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
- export const smtpTransport: Transport = async (cfg, msg) => {
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: cfg.smtp.pass } : undefined,
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,6 +24,12 @@ declare module '@deepseek-ai/cordis' {
24
24
  }
25
25
  }
26
26
 
27
+ /** credentials seam 最小面提取(缺席返回 undefined;Mailer 自行降级行级密码)。 */
28
+ function credentialsOf(ctx: Context): CredentialsSeamLike | undefined {
29
+ const found = (ctx as unknown as { get?(key: string): unknown }).get?.('credentials')
30
+ return found !== null && typeof found === 'object' ? (found as CredentialsSeamLike) : undefined
31
+ }
32
+
27
33
  export class NotifyEmailService extends Service {
28
34
  static [Context.inject] = ['agents', 'tools']
29
35
 
@@ -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(dshHomePath('logs', 'notify-email.jsonl'), (message) =>
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)