@dsh-plus/notify-email 0.1.10 → 0.1.12
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 -40
- package/package.json +12 -13
- package/src/client/api.ts +2 -2
- package/src/client/card.tsx +11 -17
- package/src/client/client.ts +16 -14
- package/src/config-api.ts +2 -2
- package/src/ns.ts +1 -1
package/lib/client.js
CHANGED
|
@@ -6,6 +6,25 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
8
|
let react = require("react");
|
|
9
|
+
//#region ../shared/src/client/icons.tsx
|
|
10
|
+
/** 官方 IconChevronDownOutline14 的同构实现(path 数据逐字一致)。 */
|
|
11
|
+
function ChevronDownIcon(props) {
|
|
12
|
+
const size = props.size ?? 14;
|
|
13
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
14
|
+
width: size,
|
|
15
|
+
height: size,
|
|
16
|
+
className: props.className,
|
|
17
|
+
viewBox: "0 0 14 14",
|
|
18
|
+
fill: "none",
|
|
19
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
20
|
+
"aria-hidden": "true",
|
|
21
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
22
|
+
d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
|
|
23
|
+
fill: "currentColor"
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
9
28
|
//#region ../shared/src/client/card.tsx
|
|
10
29
|
const IDLE_STATUS = {
|
|
11
30
|
kind: "idle",
|
|
@@ -41,10 +60,7 @@ window.__ModuleLoader__.load({
|
|
|
41
60
|
className: `${p}-pending`,
|
|
42
61
|
children: props.dirtyLabel
|
|
43
62
|
}) : null,
|
|
44
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
45
|
-
className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}`,
|
|
46
|
-
children: "▾"
|
|
47
|
-
})
|
|
63
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
|
|
48
64
|
]
|
|
49
65
|
}), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
50
66
|
className: `${p}-body`,
|
|
@@ -110,6 +126,11 @@ window.__ModuleLoader__.load({
|
|
|
110
126
|
]
|
|
111
127
|
});
|
|
112
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
|
|
131
|
+
* 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
|
|
132
|
+
* 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
|
|
133
|
+
*/
|
|
113
134
|
function CheckRow(props) {
|
|
114
135
|
const p = props.prefix ?? "dshp";
|
|
115
136
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -117,6 +138,8 @@ window.__ModuleLoader__.load({
|
|
|
117
138
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
118
139
|
id: props.id,
|
|
119
140
|
type: "checkbox",
|
|
141
|
+
role: "switch",
|
|
142
|
+
"aria-checked": props.checked,
|
|
120
143
|
checked: props.checked,
|
|
121
144
|
disabled: props.disabled === true,
|
|
122
145
|
onChange: (event) => props.onEdit(event.target.checked)
|
|
@@ -173,14 +196,15 @@ window.__ModuleLoader__.load({
|
|
|
173
196
|
//#endregion
|
|
174
197
|
//#region ../shared/src/client/scope.ts
|
|
175
198
|
/**
|
|
176
|
-
*
|
|
199
|
+
* remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
177
200
|
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
178
201
|
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
179
202
|
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
180
203
|
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
181
204
|
* effect 归属调用方 fiber(label 需含插件名以便诊断)。
|
|
182
205
|
*/
|
|
183
|
-
function
|
|
206
|
+
function createSettingsScope(c, ns, label) {
|
|
207
|
+
const remote = c.get("remote");
|
|
184
208
|
let state = {
|
|
185
209
|
status: "loading",
|
|
186
210
|
value: void 0,
|
|
@@ -195,36 +219,37 @@ window.__ModuleLoader__.load({
|
|
|
195
219
|
};
|
|
196
220
|
const load = async () => {
|
|
197
221
|
const gen = ++generation;
|
|
198
|
-
let
|
|
222
|
+
let view;
|
|
199
223
|
try {
|
|
200
|
-
response = await
|
|
224
|
+
const response = await remote.settings.describe();
|
|
225
|
+
if (!response.ok) return;
|
|
226
|
+
view = response.value;
|
|
201
227
|
} catch {
|
|
202
228
|
return;
|
|
203
229
|
}
|
|
204
|
-
if (gen !== generation
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
if (view === void 0) {
|
|
230
|
+
if (gen !== generation) return;
|
|
231
|
+
const namespace = view.namespaces.find((candidate) => candidate.ns === ns);
|
|
232
|
+
if (namespace === void 0) {
|
|
208
233
|
publish({
|
|
209
234
|
status: "unavailable",
|
|
210
235
|
value: void 0,
|
|
211
236
|
revision: void 0,
|
|
212
|
-
writable
|
|
237
|
+
writable: view.writable
|
|
213
238
|
});
|
|
214
239
|
return;
|
|
215
240
|
}
|
|
216
241
|
publish({
|
|
217
242
|
status: "ready",
|
|
218
|
-
value:
|
|
219
|
-
revision:
|
|
220
|
-
writable
|
|
243
|
+
value: namespace.value,
|
|
244
|
+
revision: namespace.revision,
|
|
245
|
+
writable: view.writable
|
|
221
246
|
});
|
|
222
247
|
};
|
|
223
248
|
const refresh = (namespace) => {
|
|
224
249
|
if (namespace !== void 0 && namespace !== ns) return;
|
|
225
250
|
load();
|
|
226
251
|
};
|
|
227
|
-
const disposers = [
|
|
252
|
+
const disposers = [remote.$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
228
253
|
load();
|
|
229
254
|
})];
|
|
230
255
|
c.effect(() => () => {
|
|
@@ -242,6 +267,20 @@ window.__ModuleLoader__.load({
|
|
|
242
267
|
load
|
|
243
268
|
};
|
|
244
269
|
}
|
|
270
|
+
/** 以 remote.settings 构造命名空间绑定的 api 面。 */
|
|
271
|
+
function createNamespaceApi(remote, ns) {
|
|
272
|
+
return {
|
|
273
|
+
update: async (patch, expectedRevision) => {
|
|
274
|
+
const response = await remote.update(ns, patch, expectedRevision);
|
|
275
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
276
|
+
},
|
|
277
|
+
describe: async () => {
|
|
278
|
+
const response = await remote.describe();
|
|
279
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
280
|
+
return response.value;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
245
284
|
//#endregion
|
|
246
285
|
//#region ../shared/src/client/styles.ts
|
|
247
286
|
/**
|
|
@@ -265,7 +304,7 @@ window.__ModuleLoader__.load({
|
|
|
265
304
|
.${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
266
305
|
.${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
267
306
|
.${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
268
|
-
.${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;
|
|
307
|
+
.${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
|
|
269
308
|
.${prefix}-chevronOpen{transform:rotate(180deg)}
|
|
270
309
|
.${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
271
310
|
.${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
@@ -279,16 +318,24 @@ window.__ModuleLoader__.load({
|
|
|
279
318
|
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
280
319
|
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
281
320
|
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
282
|
-
.${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
|
|
321
|
+
.${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;transition:border-color .18s,background .18s}
|
|
322
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
283
323
|
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
284
324
|
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
285
325
|
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
286
|
-
.${prefix}-select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-
|
|
326
|
+
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
327
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
287
328
|
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
288
329
|
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
289
|
-
.${prefix}-checkRow{align-items:center;gap:
|
|
290
|
-
.${prefix}-checkRow
|
|
291
|
-
|
|
330
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
331
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
332
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
333
|
+
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
334
|
+
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
335
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
336
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
337
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
338
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
292
339
|
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
293
340
|
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
294
341
|
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
@@ -306,7 +353,6 @@ window.__ModuleLoader__.load({
|
|
|
306
353
|
.${prefix}-header{padding:12px 12px}
|
|
307
354
|
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
308
355
|
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
309
|
-
.${prefix}-checkRow input{width:18px;height:18px;flex:none}
|
|
310
356
|
.${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
|
|
311
357
|
.${prefix}-status{flex-basis:100%;flex:none}
|
|
312
358
|
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
@@ -341,7 +387,7 @@ ${extra}
|
|
|
341
387
|
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
342
388
|
* 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
|
|
343
389
|
* settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
|
|
344
|
-
*
|
|
390
|
+
* 契约:该槽位按「卡片编辑的 settings 命名空间」作 key 分发(0.1.2-alpha.1 基线),
|
|
345
391
|
* 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
|
|
346
392
|
* @module notify-email/ns
|
|
347
393
|
*/
|
|
@@ -367,7 +413,8 @@ ${extra}
|
|
|
367
413
|
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
368
414
|
* 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
|
|
369
415
|
* staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
|
|
370
|
-
*
|
|
416
|
+
* 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
|
|
417
|
+
* 已移除):value 为 schema 解析后的脱敏视图
|
|
371
418
|
* (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
|
|
372
419
|
* 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
|
|
373
420
|
* @module notify-email/client/card
|
|
@@ -432,10 +479,10 @@ ${extra}
|
|
|
432
479
|
}, [value, draft]);
|
|
433
480
|
(0, react.useEffect)(() => {
|
|
434
481
|
let alive = true;
|
|
435
|
-
api.describe(
|
|
436
|
-
if (!alive
|
|
437
|
-
const
|
|
438
|
-
setPassConfigured(
|
|
482
|
+
api.describe().then((view) => {
|
|
483
|
+
if (!alive) return;
|
|
484
|
+
const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS);
|
|
485
|
+
setPassConfigured(ns?.secrets.some((secret) => secret.path.join(".") === "smtp.pass" && secret.set) ?? false);
|
|
439
486
|
}).catch(() => {});
|
|
440
487
|
return () => {
|
|
441
488
|
alive = false;
|
|
@@ -468,12 +515,7 @@ ${extra}
|
|
|
468
515
|
const patch = toPatch(draft);
|
|
469
516
|
if (patch["smtp"]["pass"] === "") delete patch["smtp"]["pass"];
|
|
470
517
|
const revision = scope.getSnapshot().revision;
|
|
471
|
-
api.update({
|
|
472
|
-
ns: SETTINGS_NS,
|
|
473
|
-
patch,
|
|
474
|
-
...revision !== void 0 ? { expectedRevision: revision } : {}
|
|
475
|
-
}).then(async (response) => {
|
|
476
|
-
if (!response.result.ok) throw new Error(response.result.error?.message ?? t("saveFailed"));
|
|
518
|
+
api.update(patch, revision).then(async () => {
|
|
477
519
|
await scope.load();
|
|
478
520
|
const next = scope.getSnapshot().value;
|
|
479
521
|
if (next !== void 0) setDraft(draftFromValue(next));
|
|
@@ -763,8 +805,8 @@ ${extra}
|
|
|
763
805
|
const inject = [
|
|
764
806
|
"slots",
|
|
765
807
|
"locale",
|
|
766
|
-
"
|
|
767
|
-
"remote"
|
|
808
|
+
"remote",
|
|
809
|
+
"remote.settings"
|
|
768
810
|
];
|
|
769
811
|
function apply(ctx) {
|
|
770
812
|
const c = ctx;
|
|
@@ -776,8 +818,8 @@ ${extra}
|
|
|
776
818
|
zh,
|
|
777
819
|
en
|
|
778
820
|
}), "notify-email: locale");
|
|
779
|
-
const
|
|
780
|
-
const
|
|
821
|
+
const scope = createSettingsScope(c, SETTINGS_NS, "notify-email: settings scope");
|
|
822
|
+
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
781
823
|
c.slots.inject("settings.plugin.item", () => c.slots.register({
|
|
782
824
|
name: "settings.plugin.item",
|
|
783
825
|
key: SETTINGS_NS,
|
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.12",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 任务结束/等待决策/出错时向指定邮箱发送邮件通知",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
"dsh": {
|
|
24
24
|
"client": {
|
|
25
25
|
"inject": [
|
|
26
|
-
"@deepseek-ai/dsh-client-
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
27
27
|
"@deepseek-ai/dsh-client-locale",
|
|
28
|
-
"@deepseek-ai/dsh-client-connection",
|
|
29
28
|
"@deepseek-ai/dsh-api-remotes"
|
|
30
29
|
],
|
|
31
30
|
"platform": "web"
|
|
@@ -33,23 +32,23 @@
|
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
34
|
"nodemailer": "9.0.5",
|
|
36
|
-
"@dsh-plus/shared": "0.1.
|
|
35
|
+
"@dsh-plus/shared": "0.1.6"
|
|
37
36
|
},
|
|
38
37
|
"peerDependencies": {
|
|
39
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
39
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.2-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.1",
|
|
42
41
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
44
|
"@deepseek-ai/cordis": "4.0.1",
|
|
46
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
47
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
48
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
51
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
52
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
45
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.1",
|
|
50
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.1",
|
|
53
52
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
54
53
|
"@types/nodemailer": "8.0.1",
|
|
55
54
|
"@types/react": "~18.3.1",
|
package/src/client/api.ts
CHANGED
package/src/client/card.tsx
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
4
4
|
* 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
|
|
5
5
|
* staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
|
|
6
|
-
*
|
|
6
|
+
* 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
|
|
7
|
+
* 已移除):value 为 schema 解析后的脱敏视图
|
|
7
8
|
* (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
|
|
8
9
|
* 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
|
|
9
10
|
* @module notify-email/client/card
|
|
@@ -14,8 +15,8 @@ import {
|
|
|
14
15
|
type CardStatusState,
|
|
15
16
|
CheckRow,
|
|
16
17
|
IDLE_STATUS,
|
|
18
|
+
type NamespaceSettingsApi,
|
|
17
19
|
type Scope,
|
|
18
|
-
type SettingsApi,
|
|
19
20
|
TextField,
|
|
20
21
|
} from '@dsh-plus/shared/client'
|
|
21
22
|
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
@@ -25,7 +26,7 @@ import { sendTest } from './api.ts'
|
|
|
25
26
|
export interface CardProps {
|
|
26
27
|
t(key: string): string
|
|
27
28
|
scope: Scope
|
|
28
|
-
api:
|
|
29
|
+
api: NamespaceSettingsApi
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/** settings 命名空间的脱敏解析值(smtp.pass 被脱敏剥除)。 */
|
|
@@ -136,12 +137,12 @@ export function NotifyEmailCard(props: CardProps): ReactElement | null {
|
|
|
136
137
|
useEffect(() => {
|
|
137
138
|
let alive = true
|
|
138
139
|
api
|
|
139
|
-
.describe(
|
|
140
|
-
.then((
|
|
141
|
-
if (!alive
|
|
142
|
-
const
|
|
140
|
+
.describe()
|
|
141
|
+
.then((view) => {
|
|
142
|
+
if (!alive) return
|
|
143
|
+
const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS)
|
|
143
144
|
setPassConfigured(
|
|
144
|
-
|
|
145
|
+
ns?.secrets.some((secret) => secret.path.join('.') === 'smtp.pass' && secret.set) ??
|
|
145
146
|
false,
|
|
146
147
|
)
|
|
147
148
|
})
|
|
@@ -190,15 +191,8 @@ export function NotifyEmailCard(props: CardProps): ReactElement | null {
|
|
|
190
191
|
}
|
|
191
192
|
const revision = scope.getSnapshot().revision
|
|
192
193
|
api
|
|
193
|
-
.update(
|
|
194
|
-
|
|
195
|
-
patch,
|
|
196
|
-
...(revision !== undefined ? { expectedRevision: revision } : {}),
|
|
197
|
-
})
|
|
198
|
-
.then(async (response) => {
|
|
199
|
-
if (!response.result.ok) {
|
|
200
|
-
throw new Error(response.result.error?.message ?? t('saveFailed'))
|
|
201
|
-
}
|
|
194
|
+
.update(patch, revision)
|
|
195
|
+
.then(async () => {
|
|
202
196
|
await scope.load()
|
|
203
197
|
const next = scope.getSnapshot().value as ConfigValue | undefined
|
|
204
198
|
if (next !== undefined) setDraft(draftFromValue(next))
|
package/src/client/client.ts
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
* 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
|
|
4
4
|
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
5
5
|
*
|
|
6
|
-
* 类型说明:浏览器半只用到 slots/locale/
|
|
6
|
+
* 类型说明:浏览器半只用到 slots/locale/remote 的很窄一面,
|
|
7
7
|
* scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 该插槽为 keyed 槽位(0.1.2-alpha.1 基线):key 必须是卡片编辑的 settings
|
|
9
|
+
* 命名空间(即服务端 settingsNamespace() 注册的同一字面量,见 ../ns.ts),
|
|
10
10
|
* 官方配置页按此 key 与 Host 已注册命名空间配对分发。
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
|
|
12
|
+
* 已移除;不复用 settingsScope 服务——非 loopback 页面下它固定 memory 模式无数据);
|
|
13
13
|
* 自定义端点仅剩「发送测试邮件」(api.ts)。
|
|
14
14
|
* @module @dsh-plus/notify-email/client
|
|
15
15
|
*/
|
|
16
16
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
cardCss,
|
|
19
|
+
createNamespaceApi,
|
|
20
|
+
createSettingsScope,
|
|
21
|
+
injectCardStyle,
|
|
22
|
+
type SettingsRemoteFace,
|
|
23
|
+
} from '@dsh-plus/shared/client'
|
|
18
24
|
import { SETTINGS_NS } from '../ns.ts'
|
|
19
25
|
import { NotifyEmailCard } from './card.tsx'
|
|
20
26
|
import { en, NS, zh } from './i18n.ts'
|
|
@@ -22,7 +28,7 @@ import { en, NS, zh } from './i18n.ts'
|
|
|
22
28
|
export const name = 'dsh-plus-notify-email'
|
|
23
29
|
|
|
24
30
|
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
25
|
-
export const inject = ['slots', 'locale', '
|
|
31
|
+
export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
|
|
26
32
|
|
|
27
33
|
interface SlotsLike {
|
|
28
34
|
inject(key: string, callback: () => unknown): unknown
|
|
@@ -35,17 +41,13 @@ interface LocaleLike {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
interface RemoteLike {
|
|
44
|
+
settings: SettingsRemoteFace
|
|
38
45
|
$on(event: string, listener: (payload?: unknown) => void): () => void
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
interface ConnectionLike {
|
|
42
|
-
api: { settings: Parameters<typeof createApiScope>[0] }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
48
|
interface ClientContext {
|
|
46
49
|
slots: SlotsLike
|
|
47
50
|
locale: LocaleLike
|
|
48
|
-
get(key: 'connection'): ConnectionLike
|
|
49
51
|
get(key: 'remote'): RemoteLike
|
|
50
52
|
on(event: string, listener: () => void): () => void
|
|
51
53
|
effect(execute: () => () => void, label?: string): unknown
|
|
@@ -61,8 +63,8 @@ export function apply(ctx: Context): void {
|
|
|
61
63
|
'notify-email: style',
|
|
62
64
|
)
|
|
63
65
|
c.effect(() => c.locale.register(NS, { zh, en }), 'notify-email: locale')
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
+
const scope = createSettingsScope(c, SETTINGS_NS, 'notify-email: settings scope')
|
|
67
|
+
const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
|
|
66
68
|
c.slots.inject('settings.plugin.item', () =>
|
|
67
69
|
c.slots.register(
|
|
68
70
|
{
|
package/src/config-api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 自定义端点 HTTP
|
|
3
|
-
*
|
|
2
|
+
* 自定义端点 HTTP 通道:配置读写走官方 settings 命名空间(0.1.2-alpha.1 起
|
|
3
|
+
* 第三方命名空间全量开放),这里只保留官方传输没有的「发送测试邮件」端点。
|
|
4
4
|
* 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
|
|
5
5
|
* @module notify-email/config-api
|
|
6
6
|
*/
|
package/src/ns.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
3
3
|
* 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
|
|
4
4
|
* settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
|
|
5
|
-
*
|
|
5
|
+
* 契约:该槽位按「卡片编辑的 settings 命名空间」作 key 分发(0.1.2-alpha.1 基线),
|
|
6
6
|
* 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
|
|
7
7
|
* @module notify-email/ns
|
|
8
8
|
*/
|