@dsh-plus/notify-email 0.1.6 → 0.1.8

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
@@ -3,554 +3,532 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
-
7
- //#region rolldown:runtime
8
- var __create = Object.create;
9
- var __defProp = Object.defineProperty;
10
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
- var __getOwnPropNames = Object.getOwnPropertyNames;
12
- var __getProtoOf = Object.getPrototypeOf;
13
- var __hasOwnProp = Object.prototype.hasOwnProperty;
14
- var __copyProps = (to, from, except, desc) => {
15
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
- key = keys[i];
17
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
- get: ((k) => from[k]).bind(null, key),
19
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
- });
21
- }
22
- return to;
23
- };
24
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
25
- value: mod,
26
- enumerable: true
27
- }) : target, mod));
28
-
29
- //#endregion
30
- let react = require("react");
31
- react = __toESM(react);
32
- let react_jsx_runtime = require("react/jsx-runtime");
33
- react_jsx_runtime = __toESM(react_jsx_runtime);
34
-
35
- //#region src/ns.ts
36
- /**
37
- * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
38
- * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
39
- * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
40
- * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
41
- * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
42
- * @module notify-email/ns
43
- */
44
- const SETTINGS_NS = "dsh-plus-notify-email";
45
-
46
- //#endregion
47
- //#region src/client/api.ts
48
- const ROUTE_TEST = "/dsh-plus/notify-email/test";
49
- async function parse(res) {
50
- const body = await res.json();
51
- if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
52
- return body;
53
- }
54
- async function sendTest() {
55
- return parse(await fetch(ROUTE_TEST, {
56
- method: "POST",
57
- credentials: "same-origin"
58
- }));
59
- }
60
-
61
- //#endregion
62
- //#region src/client/fields.tsx
63
- function TextField(props) {
64
- const invalid = props.invalid === true;
65
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
66
- className: "dne-field",
67
- children: [
68
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
69
- className: "dne-head",
70
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
71
- className: "dne-label",
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/ns.ts
10
+ /**
11
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
12
+ * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
13
+ * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
14
+ * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
15
+ * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
16
+ * @module notify-email/ns
17
+ */
18
+ const SETTINGS_NS = "dsh-plus-notify-email";
19
+ //#endregion
20
+ //#region src/client/api.ts
21
+ const ROUTE_TEST = "/dsh-plus/notify-email/test";
22
+ async function parse(res) {
23
+ const body = await res.json();
24
+ if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
25
+ return body;
26
+ }
27
+ async function sendTest() {
28
+ return parse(await fetch(ROUTE_TEST, {
29
+ method: "POST",
30
+ credentials: "same-origin"
31
+ }));
32
+ }
33
+ //#endregion
34
+ //#region src/client/fields.tsx
35
+ function TextField(props) {
36
+ const invalid = props.invalid === true;
37
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
38
+ className: "dne-field",
39
+ children: [
40
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
41
+ className: "dne-head",
42
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
43
+ className: "dne-label",
44
+ htmlFor: props.id,
45
+ children: props.label
46
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47
+ className: `dne-badge ${props.badge.set ? "dne-badgeSet" : "dne-badgeUnset"}`,
48
+ children: props.badge.text
49
+ }) : null]
50
+ }),
51
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
52
+ id: props.id,
53
+ className: `dne-input${invalid ? " dne-inputInvalid" : ""}`,
54
+ type: props.password === true ? "password" : "text",
55
+ inputMode: props.numeric === true ? "numeric" : void 0,
56
+ autoComplete: props.password === true ? "off" : void 0,
57
+ "aria-invalid": invalid || void 0,
58
+ value: props.value,
59
+ disabled: props.disabled === true,
60
+ onChange: (event) => props.onEdit(event.target.value)
61
+ }),
62
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
63
+ className: invalid ? "dne-invalid" : "dne-hint",
64
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
65
+ })
66
+ ]
67
+ });
68
+ }
69
+ function CheckRow(props) {
70
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
71
+ className: "dne-checkRow",
72
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
73
+ id: props.id,
74
+ type: "checkbox",
75
+ checked: props.checked,
76
+ disabled: props.disabled === true,
77
+ onChange: (event) => props.onEdit(event.target.checked)
78
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
72
79
  htmlFor: props.id,
73
80
  children: props.label
74
- }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
75
- className: `dne-badge ${props.badge.set ? "dne-badgeSet" : "dne-badgeUnset"}`,
76
- children: props.badge.text
77
- }) : null]
78
- }),
79
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
80
- id: props.id,
81
- className: `dne-input${invalid ? " dne-inputInvalid" : ""}`,
82
- type: props.password === true ? "password" : "text",
83
- inputMode: props.numeric === true ? "numeric" : void 0,
84
- autoComplete: props.password === true ? "off" : void 0,
85
- "aria-invalid": invalid || void 0,
86
- value: props.value,
87
- disabled: props.disabled === true,
88
- onChange: (event) => props.onEdit(event.target.value)
89
- }),
90
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
91
- className: invalid ? "dne-invalid" : "dne-hint",
92
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
93
- })
94
- ]
95
- });
96
- }
97
- function CheckRow(props) {
98
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
99
- className: "dne-checkRow",
100
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
101
- id: props.id,
102
- type: "checkbox",
103
- checked: props.checked,
104
- disabled: props.disabled === true,
105
- onChange: (event) => props.onEdit(event.target.checked)
106
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
107
- htmlFor: props.id,
108
- children: props.label
109
- })]
110
- });
111
- }
112
-
113
- //#endregion
114
- //#region src/client/card.tsx
115
- const IDLE_STATUS = {
116
- kind: "idle",
117
- text: ""
118
- };
119
- function draftFromValue(value) {
120
- return {
121
- enabled: value.enabled,
122
- host: value.smtp.host,
123
- port: String(value.smtp.port),
124
- secure: value.smtp.secure,
125
- user: value.smtp.user,
126
- pass: "",
127
- from: value.smtp.from,
128
- toText: value.to.join(", "),
129
- triggers: { ...value.triggers },
130
- idleDebounceMs: String(value.idleDebounceMs),
131
- maxBodyChars: String(value.maxBodyChars),
132
- dryRun: value.dryRun
133
- };
134
- }
135
- function isPositiveInt(text) {
136
- return /^[0-9]+$/.test(text) && Number(text) > 0;
137
- }
138
- function toPatch(draft) {
139
- return {
140
- enabled: draft.enabled,
141
- smtp: {
142
- host: draft.host.trim(),
143
- port: Number(draft.port),
144
- secure: draft.secure,
145
- user: draft.user.trim(),
146
- pass: draft.pass,
147
- from: draft.from.trim()
148
- },
149
- to: draft.toText.split(",").map((s) => s.trim()).filter((s) => s.length > 0),
150
- triggers: { ...draft.triggers },
151
- idleDebounceMs: Number(draft.idleDebounceMs),
152
- maxBodyChars: Number(draft.maxBodyChars),
153
- dryRun: draft.dryRun
154
- };
155
- }
156
- const TRIGGER_KEYS = [
157
- "onComplete",
158
- "onError",
159
- "onAborted",
160
- "onQuestion",
161
- "onPlanReview"
162
- ];
163
- function NotifyEmailCard(props) {
164
- const { t, scope, api } = props;
165
- const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
166
- const value = snapshot.value;
167
- const [open, setOpen] = (0, react.useState)(false);
168
- const [draft, setDraft] = (0, react.useState)(null);
169
- const [passConfigured, setPassConfigured] = (0, react.useState)(false);
170
- const [saving, setSaving] = (0, react.useState)(false);
171
- const [testing, setTesting] = (0, react.useState)(false);
172
- const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
173
- (0, react.useEffect)(() => {
174
- if (value === void 0 || draft !== null) return;
175
- setDraft(draftFromValue(value));
176
- }, [value, draft]);
177
- (0, react.useEffect)(() => {
178
- let alive = true;
179
- api.describe({}).then((response) => {
180
- if (!alive || !response.result.ok) return;
181
- const view = response.result.value?.namespaces.find((ns) => ns.ns === SETTINGS_NS);
182
- setPassConfigured(view?.secrets.some((secret) => secret.path.join(".") === "smtp.pass" && secret.set) ?? false);
183
- }).catch(() => {});
184
- return () => {
185
- alive = false;
186
- };
187
- }, [api, snapshot]);
188
- const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
189
- const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.port) || !isPositiveInt(draft.idleDebounceMs) || !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200), [draft]);
190
- if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
191
- className: "dne-card",
192
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
193
- className: "dne-readOnly",
194
- children: t("loading")
195
- })
196
- });
197
- const edit = (key, editValue) => {
198
- setDraft({
199
- ...draft,
200
- [key]: editValue
201
- });
202
- setStatus(IDLE_STATUS);
203
- };
204
- const editTrigger = (key, checked) => {
205
- edit("triggers", {
206
- ...draft.triggers,
207
- [key]: checked
208
- });
209
- };
210
- const onSave = () => {
211
- setSaving(true);
212
- const patch = toPatch(draft);
213
- if (patch["smtp"]["pass"] === "") delete patch["smtp"]["pass"];
214
- const revision = scope.getSnapshot().revision;
215
- api.update({
216
- ns: SETTINGS_NS,
217
- patch,
218
- ...revision !== void 0 ? { expectedRevision: revision } : {}
219
- }).then(async (response) => {
220
- if (!response.result.ok) throw new Error(response.result.error?.message ?? t("saveFailed"));
221
- await scope.load();
222
- const next = scope.getSnapshot().value;
223
- if (next !== void 0) setDraft(draftFromValue(next));
224
- setStatus(IDLE_STATUS);
225
- }).catch((error) => {
226
- const message = error instanceof Error ? error.message : String(error);
227
- setStatus({
228
- kind: "error",
229
- text: `${t("saveFailed")}${message}`
81
+ })]
230
82
  });
231
- }).finally(() => setSaving(false));
232
- };
233
- const onTest = () => {
234
- setTesting(true);
235
- sendTest().then((result) => {
236
- if (result.ok) setStatus({
237
- kind: "ok",
238
- text: result.detail === "dry-run" ? t("testDryRun") : t("testOk")
83
+ }
84
+ //#endregion
85
+ //#region src/client/card.tsx
86
+ /**
87
+ * 「邮件通知」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
88
+ * 交互对齐官方卡片:折叠/展开、staged draft、未保存标记、保存/放弃;
89
+ * 另加「发送测试邮件」(唯一保留的自定义端点)。
90
+ * 配置读写走官方 settingsScope 传输(scope.ts):value 为 schema 解析后的
91
+ * 脱敏视图(smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
92
+ * 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
93
+ * @module notify-email/client/card
94
+ */
95
+ const IDLE_STATUS = {
96
+ kind: "idle",
97
+ text: ""
98
+ };
99
+ function draftFromValue(value) {
100
+ return {
101
+ enabled: value.enabled,
102
+ host: value.smtp.host,
103
+ port: String(value.smtp.port),
104
+ secure: value.smtp.secure,
105
+ user: value.smtp.user,
106
+ pass: "",
107
+ from: value.smtp.from,
108
+ toText: value.to.join(", "),
109
+ triggers: { ...value.triggers },
110
+ idleDebounceMs: String(value.idleDebounceMs),
111
+ maxBodyChars: String(value.maxBodyChars),
112
+ dryRun: value.dryRun
113
+ };
114
+ }
115
+ function isPositiveInt(text) {
116
+ return /^[0-9]+$/.test(text) && Number(text) > 0;
117
+ }
118
+ function toPatch(draft) {
119
+ return {
120
+ enabled: draft.enabled,
121
+ smtp: {
122
+ host: draft.host.trim(),
123
+ port: Number(draft.port),
124
+ secure: draft.secure,
125
+ user: draft.user.trim(),
126
+ pass: draft.pass,
127
+ from: draft.from.trim()
128
+ },
129
+ to: draft.toText.split(",").map((s) => s.trim()).filter((s) => s.length > 0),
130
+ triggers: { ...draft.triggers },
131
+ idleDebounceMs: Number(draft.idleDebounceMs),
132
+ maxBodyChars: Number(draft.maxBodyChars),
133
+ dryRun: draft.dryRun
134
+ };
135
+ }
136
+ const TRIGGER_KEYS = [
137
+ "onComplete",
138
+ "onError",
139
+ "onAborted",
140
+ "onQuestion",
141
+ "onPlanReview"
142
+ ];
143
+ function NotifyEmailCard(props) {
144
+ const { t, scope, api } = props;
145
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
146
+ const value = snapshot.value;
147
+ const [open, setOpen] = (0, react.useState)(false);
148
+ const [draft, setDraft] = (0, react.useState)(null);
149
+ const [passConfigured, setPassConfigured] = (0, react.useState)(false);
150
+ const [saving, setSaving] = (0, react.useState)(false);
151
+ const [testing, setTesting] = (0, react.useState)(false);
152
+ const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
153
+ (0, react.useEffect)(() => {
154
+ if (value === void 0 || draft !== null) return;
155
+ setDraft(draftFromValue(value));
156
+ }, [value, draft]);
157
+ (0, react.useEffect)(() => {
158
+ let alive = true;
159
+ api.describe({}).then((response) => {
160
+ if (!alive || !response.result.ok) return;
161
+ const view = response.result.value?.namespaces.find((ns) => ns.ns === SETTINGS_NS);
162
+ setPassConfigured(view?.secrets.some((secret) => secret.path.join(".") === "smtp.pass" && secret.set) ?? false);
163
+ }).catch(() => {});
164
+ return () => {
165
+ alive = false;
166
+ };
167
+ }, [api, snapshot]);
168
+ const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
169
+ const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.port) || !isPositiveInt(draft.idleDebounceMs) || !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200), [draft]);
170
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
171
+ className: "dne-card",
172
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
173
+ className: "dne-readOnly",
174
+ children: t("loading")
175
+ })
239
176
  });
240
- else {
241
- const why = result.detail === "incomplete" ? t("incomplete") : result.detail;
242
- setStatus({
243
- kind: "error",
244
- text: `${t("testFailed")}${why}`
177
+ const edit = (key, editValue) => {
178
+ setDraft({
179
+ ...draft,
180
+ [key]: editValue
245
181
  });
246
- }
247
- }).catch((error) => {
248
- setStatus({
249
- kind: "error",
250
- text: `${t("testFailed")}${error instanceof Error ? error.message : ""}`
251
- });
252
- }).finally(() => setTesting(false));
253
- };
254
- const disabled = !snapshot.writable;
255
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
256
- className: `dne-card${open ? " dne-cardOpen" : ""}`,
257
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
258
- type: "button",
259
- className: "dne-header",
260
- "aria-expanded": open,
261
- "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
262
- onClick: () => setOpen(!open),
263
- children: [
264
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
265
- className: "dne-headText",
266
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
267
- className: "dne-name",
268
- children: t("title")
269
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
270
- className: "dne-description",
271
- children: t("description")
272
- })]
273
- }),
274
- dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
275
- className: "dne-pending",
276
- children: t("unsaved")
277
- }) : null,
278
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
279
- className: `dne-chevron${open ? " dne-chevronOpen" : ""}`,
280
- children: "▾"
281
- })
282
- ]
283
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
284
- className: "dne-body",
285
- children: [
286
- disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
287
- className: "dne-readOnly",
288
- role: "status",
289
- children: t("readOnly")
290
- }) : null,
291
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
292
- id: "dne-enabled",
293
- label: t("enabled"),
294
- checked: draft.enabled,
295
- disabled,
296
- onEdit: (v) => edit("enabled", v)
297
- }),
298
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
299
- id: "dne-host",
300
- label: t("host"),
301
- hint: t("hostHint"),
302
- value: draft.host,
303
- disabled,
304
- onEdit: (v) => edit("host", v)
305
- }),
306
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
307
- id: "dne-port",
308
- label: t("port"),
309
- hint: t("portHint"),
310
- value: draft.port,
311
- numeric: true,
312
- disabled,
313
- invalid: !isPositiveInt(draft.port),
314
- invalidLabel: t("invalidNumber"),
315
- onEdit: (v) => edit("port", v)
316
- }),
317
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
318
- id: "dne-secure",
319
- label: t("secure"),
320
- checked: draft.secure,
321
- disabled,
322
- onEdit: (v) => edit("secure", v)
323
- }),
324
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
325
- id: "dne-user",
326
- label: t("user"),
327
- hint: t("userHint"),
328
- value: draft.user,
329
- disabled,
330
- onEdit: (v) => edit("user", v)
331
- }),
332
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
333
- id: "dne-pass",
334
- label: t("pass"),
335
- hint: t("passHint"),
336
- value: draft.pass,
337
- password: true,
338
- disabled,
339
- badge: {
340
- text: passConfigured ? t("passSet") : t("passUnset"),
341
- set: passConfigured
342
- },
343
- onEdit: (v) => edit("pass", v)
344
- }),
345
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
346
- id: "dne-from",
347
- label: t("from"),
348
- hint: t("fromHint"),
349
- value: draft.from,
350
- disabled,
351
- onEdit: (v) => edit("from", v)
352
- }),
353
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
354
- id: "dne-to",
355
- label: t("to"),
356
- hint: t("toHint"),
357
- value: draft.toText,
358
- disabled,
359
- onEdit: (v) => edit("toText", v)
360
- }),
361
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
362
- className: "dne-groupLabel",
363
- children: t("triggerGroup")
364
- }),
365
- TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
366
- id: `dne-${key}`,
367
- label: t(key),
368
- checked: draft.triggers[key],
369
- disabled,
370
- onEdit: (v) => editTrigger(key, v)
371
- }, key)),
372
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
373
- id: "dne-debounce",
374
- label: t("idleDebounceMs"),
375
- hint: t("idleDebounceMsHint"),
376
- value: draft.idleDebounceMs,
377
- numeric: true,
378
- disabled,
379
- invalid: !isPositiveInt(draft.idleDebounceMs),
380
- invalidLabel: t("invalidNumber"),
381
- onEdit: (v) => edit("idleDebounceMs", v)
382
- }),
383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
384
- id: "dne-maxchars",
385
- label: t("maxBodyChars"),
386
- hint: t("maxBodyCharsHint"),
387
- value: draft.maxBodyChars,
388
- numeric: true,
389
- disabled,
390
- invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
391
- invalidLabel: t("invalidNumber"),
392
- onEdit: (v) => edit("maxBodyChars", v)
393
- }),
394
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
395
- id: "dne-dryrun",
396
- label: t("dryRun"),
397
- checked: draft.dryRun,
398
- disabled,
399
- onEdit: (v) => edit("dryRun", v)
400
- }),
401
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
402
- className: "dne-footer",
182
+ setStatus(IDLE_STATUS);
183
+ };
184
+ const editTrigger = (key, checked) => {
185
+ edit("triggers", {
186
+ ...draft.triggers,
187
+ [key]: checked
188
+ });
189
+ };
190
+ const onSave = () => {
191
+ setSaving(true);
192
+ const patch = toPatch(draft);
193
+ if (patch["smtp"]["pass"] === "") delete patch["smtp"]["pass"];
194
+ const revision = scope.getSnapshot().revision;
195
+ api.update({
196
+ ns: SETTINGS_NS,
197
+ patch,
198
+ ...revision !== void 0 ? { expectedRevision: revision } : {}
199
+ }).then(async (response) => {
200
+ if (!response.result.ok) throw new Error(response.result.error?.message ?? t("saveFailed"));
201
+ await scope.load();
202
+ const next = scope.getSnapshot().value;
203
+ if (next !== void 0) setDraft(draftFromValue(next));
204
+ setStatus(IDLE_STATUS);
205
+ }).catch((error) => {
206
+ const message = error instanceof Error ? error.message : String(error);
207
+ setStatus({
208
+ kind: "error",
209
+ text: `${t("saveFailed")}${message}`
210
+ });
211
+ }).finally(() => setSaving(false));
212
+ };
213
+ const onTest = () => {
214
+ setTesting(true);
215
+ sendTest().then((result) => {
216
+ if (result.ok) setStatus({
217
+ kind: "ok",
218
+ text: result.detail === "dry-run" ? t("testDryRun") : t("testOk")
219
+ });
220
+ else {
221
+ const why = result.detail === "incomplete" ? t("incomplete") : result.detail;
222
+ setStatus({
223
+ kind: "error",
224
+ text: `${t("testFailed")}${why}`
225
+ });
226
+ }
227
+ }).catch((error) => {
228
+ setStatus({
229
+ kind: "error",
230
+ text: `${t("testFailed")}${error instanceof Error ? error.message : ""}`
231
+ });
232
+ }).finally(() => setTesting(false));
233
+ };
234
+ const disabled = !snapshot.writable;
235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
236
+ className: `dne-card${open ? " dne-cardOpen" : ""}`,
237
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
238
+ type: "button",
239
+ className: "dne-header",
240
+ "aria-expanded": open,
241
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
242
+ onClick: () => setOpen(!open),
403
243
  children: [
404
- status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
405
- className: `dne-status${status.kind === "error" ? " dne-statusError" : ""}`,
244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
245
+ className: "dne-headText",
246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
247
+ className: "dne-name",
248
+ children: t("title")
249
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
250
+ className: "dne-description",
251
+ children: t("description")
252
+ })]
253
+ }),
254
+ dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
255
+ className: "dne-pending",
256
+ children: t("unsaved")
257
+ }) : null,
258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
259
+ className: `dne-chevron${open ? " dne-chevronOpen" : ""}`,
260
+ children: "▾"
261
+ })
262
+ ]
263
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
264
+ className: "dne-body",
265
+ children: [
266
+ disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
267
+ className: "dne-readOnly",
406
268
  role: "status",
407
- children: status.text
269
+ children: t("readOnly")
408
270
  }) : null,
409
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
410
- type: "button",
411
- className: "dne-btn dne-btnGhost",
412
- disabled: testing,
413
- onClick: onTest,
414
- children: t(testing ? "testing" : "test")
271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
272
+ id: "dne-enabled",
273
+ label: t("enabled"),
274
+ checked: draft.enabled,
275
+ disabled,
276
+ onEdit: (v) => edit("enabled", v)
277
+ }),
278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
279
+ id: "dne-host",
280
+ label: t("host"),
281
+ hint: t("hostHint"),
282
+ value: draft.host,
283
+ disabled,
284
+ onEdit: (v) => edit("host", v)
285
+ }),
286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
287
+ id: "dne-port",
288
+ label: t("port"),
289
+ hint: t("portHint"),
290
+ value: draft.port,
291
+ numeric: true,
292
+ disabled,
293
+ invalid: !isPositiveInt(draft.port),
294
+ invalidLabel: t("invalidNumber"),
295
+ onEdit: (v) => edit("port", v)
296
+ }),
297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
298
+ id: "dne-secure",
299
+ label: t("secure"),
300
+ checked: draft.secure,
301
+ disabled,
302
+ onEdit: (v) => edit("secure", v)
415
303
  }),
416
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
417
- type: "button",
418
- className: "dne-btn dne-btnGhost",
419
- disabled: !dirty || saving,
420
- onClick: () => {
421
- setDraft(draftFromValue(value));
422
- setStatus(IDLE_STATUS);
304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
305
+ id: "dne-user",
306
+ label: t("user"),
307
+ hint: t("userHint"),
308
+ value: draft.user,
309
+ disabled,
310
+ onEdit: (v) => edit("user", v)
311
+ }),
312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
313
+ id: "dne-pass",
314
+ label: t("pass"),
315
+ hint: t("passHint"),
316
+ value: draft.pass,
317
+ password: true,
318
+ disabled,
319
+ badge: {
320
+ text: passConfigured ? t("passSet") : t("passUnset"),
321
+ set: passConfigured
423
322
  },
424
- children: t("discard")
323
+ onEdit: (v) => edit("pass", v)
324
+ }),
325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
326
+ id: "dne-from",
327
+ label: t("from"),
328
+ hint: t("fromHint"),
329
+ value: draft.from,
330
+ disabled,
331
+ onEdit: (v) => edit("from", v)
332
+ }),
333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
334
+ id: "dne-to",
335
+ label: t("to"),
336
+ hint: t("toHint"),
337
+ value: draft.toText,
338
+ disabled,
339
+ onEdit: (v) => edit("toText", v)
425
340
  }),
426
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
427
- type: "button",
428
- className: "dne-btn dne-btnPrimary",
429
- disabled: !dirty || invalid || saving || disabled,
430
- onClick: onSave,
431
- children: t(saving ? "saving" : "save")
341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
342
+ className: "dne-groupLabel",
343
+ children: t("triggerGroup")
344
+ }),
345
+ TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
346
+ id: `dne-${key}`,
347
+ label: t(key),
348
+ checked: draft.triggers[key],
349
+ disabled,
350
+ onEdit: (v) => editTrigger(key, v)
351
+ }, key)),
352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
353
+ id: "dne-debounce",
354
+ label: t("idleDebounceMs"),
355
+ hint: t("idleDebounceMsHint"),
356
+ value: draft.idleDebounceMs,
357
+ numeric: true,
358
+ disabled,
359
+ invalid: !isPositiveInt(draft.idleDebounceMs),
360
+ invalidLabel: t("invalidNumber"),
361
+ onEdit: (v) => edit("idleDebounceMs", v)
362
+ }),
363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
364
+ id: "dne-maxchars",
365
+ label: t("maxBodyChars"),
366
+ hint: t("maxBodyCharsHint"),
367
+ value: draft.maxBodyChars,
368
+ numeric: true,
369
+ disabled,
370
+ invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
371
+ invalidLabel: t("invalidNumber"),
372
+ onEdit: (v) => edit("maxBodyChars", v)
373
+ }),
374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
375
+ id: "dne-dryrun",
376
+ label: t("dryRun"),
377
+ checked: draft.dryRun,
378
+ disabled,
379
+ onEdit: (v) => edit("dryRun", v)
380
+ }),
381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
+ className: "dne-footer",
383
+ children: [
384
+ status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
385
+ className: `dne-status${status.kind === "error" ? " dne-statusError" : ""}`,
386
+ role: "status",
387
+ children: status.text
388
+ }) : null,
389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
390
+ type: "button",
391
+ className: "dne-btn dne-btnGhost",
392
+ disabled: testing,
393
+ onClick: onTest,
394
+ children: t(testing ? "testing" : "test")
395
+ }),
396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
397
+ type: "button",
398
+ className: "dne-btn dne-btnGhost",
399
+ disabled: !dirty || saving,
400
+ onClick: () => {
401
+ setDraft(draftFromValue(value));
402
+ setStatus(IDLE_STATUS);
403
+ },
404
+ children: t("discard")
405
+ }),
406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
407
+ type: "button",
408
+ className: "dne-btn dne-btnPrimary",
409
+ disabled: !dirty || invalid || saving || disabled,
410
+ onClick: onSave,
411
+ children: t(saving ? "saving" : "save")
412
+ })
413
+ ]
432
414
  })
433
415
  ]
434
- })
435
- ]
436
- }) : null]
437
- });
438
- }
439
-
440
- //#endregion
441
- //#region src/client/i18n.ts
442
- /**
443
- * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
444
- * @module notify-email/client/i18n
445
- */
446
- const NS = "dsh-plus-notify-email";
447
- const zh = {
448
- title: "邮件通知",
449
- description: "任务结束、等待决策或出错时向指定邮箱发送邮件。",
450
- enabled: "启用通知",
451
- host: "SMTP 服务器",
452
- hostHint: "如 smtp.qq.com、smtp.163.com。",
453
- port: "端口",
454
- portHint: "465(TLS)或 587(STARTTLS)。",
455
- secure: "TLS 直连",
456
- user: "登录用户名",
457
- userHint: "通常为发件邮箱地址;免认证服务器可留空。",
458
- pass: "密码 / 授权码",
459
- passHint: "留空表示保持已存密码不变。",
460
- passSet: "已配置",
461
- passUnset: "未配置",
462
- from: "发件地址",
463
- fromHint: "邮件 From 头,通常与登录用户名一致。",
464
- to: "收件邮箱",
465
- toHint: "多个收件人用英文逗号分隔。",
466
- triggerGroup: "通知时机",
467
- onComplete: "任务执行完毕",
468
- onError: "报错 / 失败停止",
469
- onAborted: "被用户取消",
470
- onQuestion: "提问等待回答",
471
- onPlanReview: "Plan 待审批",
472
- idleDebounceMs: "空闲确认(毫秒)",
473
- idleDebounceMsHint: "任务结束后等待该时长确认无后续工作再发信。",
474
- maxBodyChars: "正文截断长度",
475
- maxBodyCharsHint: "邮件正文内容超过该长度时截断。",
476
- dryRun: "仅记录日志(不真实发送)",
477
- save: "保存",
478
- saving: "保存中…",
479
- discard: "放弃",
480
- unsaved: "未保存",
481
- test: "发送测试邮件",
482
- testing: "发送中…",
483
- testOk: "测试邮件已发送。",
484
- testDryRun: "已按 dry-run 记录日志(未真实发送)。",
485
- testFailed: "测试发送失败:",
486
- saveFailed: "保存失败,请检查填写内容。",
487
- loading: "加载中…",
488
- unavailable: "配置服务不可用。",
489
- readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
490
- expand: "展开",
491
- collapse: "收起",
492
- invalidNumber: "请输入有效数字",
493
- incomplete: "SMTP 配置不完整(服务器/发件地址/收件人必填)。"
494
- };
495
- const en = {
496
- title: "Email notifications",
497
- description: "Email a mailbox when a task finishes, awaits a decision, or fails.",
498
- enabled: "Enable notifications",
499
- host: "SMTP host",
500
- hostHint: "e.g. smtp.qq.com, smtp.gmail.com.",
501
- port: "Port",
502
- portHint: "465 (TLS) or 587 (STARTTLS).",
503
- secure: "Direct TLS",
504
- user: "Username",
505
- userHint: "Usually the sender address; leave empty for auth-less servers.",
506
- pass: "Password / app token",
507
- passHint: "Leave blank to keep the stored password.",
508
- passSet: "Configured",
509
- passUnset: "Not configured",
510
- from: "From address",
511
- fromHint: "The From header, usually same as the username.",
512
- to: "Recipients",
513
- toHint: "Separate multiple recipients with commas.",
514
- triggerGroup: "Notify when",
515
- onComplete: "Task completed",
516
- onError: "Task failed",
517
- onAborted: "Cancelled by user",
518
- onQuestion: "Waiting for an answer",
519
- onPlanReview: "Plan pending approval",
520
- idleDebounceMs: "Idle confirmation (ms)",
521
- idleDebounceMsHint: "Wait this long after completion to confirm no follow-up work.",
522
- maxBodyChars: "Body truncation",
523
- maxBodyCharsHint: "Truncate mail body content beyond this length.",
524
- dryRun: "Log only (do not send)",
525
- save: "Save",
526
- saving: "Saving…",
527
- discard: "Discard",
528
- unsaved: "Unsaved",
529
- test: "Send test email",
530
- testing: "Sending…",
531
- testOk: "Test email sent.",
532
- testDryRun: "Recorded as dry-run (not actually sent).",
533
- testFailed: "Test failed: ",
534
- saveFailed: "Save failed; please check the fields.",
535
- loading: "Loading…",
536
- unavailable: "Configuration service unavailable.",
537
- readOnly: "No settings provider in this deployment; edit settings.yaml instead.",
538
- expand: "Expand",
539
- collapse: "Collapse",
540
- invalidNumber: "Enter a valid number",
541
- incomplete: "SMTP config incomplete (host/from/recipients required)."
542
- };
543
-
544
- //#endregion
545
- //#region src/client/styles.ts
546
- /**
547
- * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
548
- * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
549
- * @module notify-email/client/styles
550
- */
551
- const PLUGIN_ID = "@dsh-plus/notify-email";
552
- const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
553
- const cardCss = `
416
+ }) : null]
417
+ });
418
+ }
419
+ //#endregion
420
+ //#region src/client/i18n.ts
421
+ /**
422
+ * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
423
+ * @module notify-email/client/i18n
424
+ */
425
+ const NS = "dsh-plus-notify-email";
426
+ const zh = {
427
+ title: "邮件通知",
428
+ description: "任务结束、等待决策或出错时向指定邮箱发送邮件。",
429
+ enabled: "启用通知",
430
+ host: "SMTP 服务器",
431
+ hostHint: "如 smtp.qq.com、smtp.163.com。",
432
+ port: "端口",
433
+ portHint: "465(TLS)或 587(STARTTLS)。",
434
+ secure: "TLS 直连",
435
+ user: "登录用户名",
436
+ userHint: "通常为发件邮箱地址;免认证服务器可留空。",
437
+ pass: "密码 / 授权码",
438
+ passHint: "留空表示保持已存密码不变。",
439
+ passSet: "已配置",
440
+ passUnset: "未配置",
441
+ from: "发件地址",
442
+ fromHint: "邮件 From 头,通常与登录用户名一致。",
443
+ to: "收件邮箱",
444
+ toHint: "多个收件人用英文逗号分隔。",
445
+ triggerGroup: "通知时机",
446
+ onComplete: "任务执行完毕",
447
+ onError: "报错 / 失败停止",
448
+ onAborted: "被用户取消",
449
+ onQuestion: "提问等待回答",
450
+ onPlanReview: "Plan 待审批",
451
+ idleDebounceMs: "空闲确认(毫秒)",
452
+ idleDebounceMsHint: "任务结束后等待该时长确认无后续工作再发信。",
453
+ maxBodyChars: "正文截断长度",
454
+ maxBodyCharsHint: "邮件正文内容超过该长度时截断。",
455
+ dryRun: "仅记录日志(不真实发送)",
456
+ save: "保存",
457
+ saving: "保存中…",
458
+ discard: "放弃",
459
+ unsaved: "未保存",
460
+ test: "发送测试邮件",
461
+ testing: "发送中…",
462
+ testOk: "测试邮件已发送。",
463
+ testDryRun: "已按 dry-run 记录日志(未真实发送)。",
464
+ testFailed: "测试发送失败:",
465
+ saveFailed: "保存失败,请检查填写内容。",
466
+ loading: "加载中…",
467
+ unavailable: "配置服务不可用。",
468
+ readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
469
+ expand: "展开",
470
+ collapse: "收起",
471
+ invalidNumber: "请输入有效数字",
472
+ incomplete: "SMTP 配置不完整(服务器/发件地址/收件人必填)。"
473
+ };
474
+ const en = {
475
+ title: "Email notifications",
476
+ description: "Email a mailbox when a task finishes, awaits a decision, or fails.",
477
+ enabled: "Enable notifications",
478
+ host: "SMTP host",
479
+ hostHint: "e.g. smtp.qq.com, smtp.gmail.com.",
480
+ port: "Port",
481
+ portHint: "465 (TLS) or 587 (STARTTLS).",
482
+ secure: "Direct TLS",
483
+ user: "Username",
484
+ userHint: "Usually the sender address; leave empty for auth-less servers.",
485
+ pass: "Password / app token",
486
+ passHint: "Leave blank to keep the stored password.",
487
+ passSet: "Configured",
488
+ passUnset: "Not configured",
489
+ from: "From address",
490
+ fromHint: "The From header, usually same as the username.",
491
+ to: "Recipients",
492
+ toHint: "Separate multiple recipients with commas.",
493
+ triggerGroup: "Notify when",
494
+ onComplete: "Task completed",
495
+ onError: "Task failed",
496
+ onAborted: "Cancelled by user",
497
+ onQuestion: "Waiting for an answer",
498
+ onPlanReview: "Plan pending approval",
499
+ idleDebounceMs: "Idle confirmation (ms)",
500
+ idleDebounceMsHint: "Wait this long after completion to confirm no follow-up work.",
501
+ maxBodyChars: "Body truncation",
502
+ maxBodyCharsHint: "Truncate mail body content beyond this length.",
503
+ dryRun: "Log only (do not send)",
504
+ save: "Save",
505
+ saving: "Saving…",
506
+ discard: "Discard",
507
+ unsaved: "Unsaved",
508
+ test: "Send test email",
509
+ testing: "Sending…",
510
+ testOk: "Test email sent.",
511
+ testDryRun: "Recorded as dry-run (not actually sent).",
512
+ testFailed: "Test failed: ",
513
+ saveFailed: "Save failed; please check the fields.",
514
+ loading: "Loading…",
515
+ unavailable: "Configuration service unavailable.",
516
+ readOnly: "No settings provider in this deployment; edit settings.yaml instead.",
517
+ expand: "Expand",
518
+ collapse: "Collapse",
519
+ invalidNumber: "Enter a valid number",
520
+ incomplete: "SMTP config incomplete (host/from/recipients required)."
521
+ };
522
+ //#endregion
523
+ //#region src/client/styles.ts
524
+ /**
525
+ * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
526
+ * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
527
+ * @module notify-email/client/styles
528
+ */
529
+ const PLUGIN_ID = "@dsh-plus/notify-email";
530
+ const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
531
+ const cardCss = `
554
532
  .dne-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
555
533
  .dne-card:hover{border-color:var(--dsw-alias-label-dimmed)}
556
534
  .dne-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
@@ -591,125 +569,123 @@ const cardCss = `
591
569
  .dne-btn:disabled{opacity:.4;cursor:default}
592
570
  .dne-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
593
571
  `;
594
- /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
595
- function injectStyle() {
596
- if (typeof document === "undefined") return null;
597
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
598
- const tag = document.createElement("style");
599
- tag.dataset.plugin = PLUGIN_ID;
600
- tag.dataset.pluginCss = STYLE_TAG_ID;
601
- tag.textContent = cardCss;
602
- document.head.appendChild(tag);
603
- return tag;
604
- }
605
-
606
- //#endregion
607
- //#region src/client/client.ts
608
- const name = "dsh-plus-notify-email";
609
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
610
- const inject = [
611
- "slots",
612
- "locale",
613
- "connection",
614
- "remote"
615
- ];
616
- /**
617
- * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
618
- * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
619
- * 刷新:remote `settings/document-updated`(按命名空间过滤)与
620
- * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
621
- * (loopback 或 tailnet 信任域名)下行为一致。
622
- */
623
- function createApiScope(api, ns, c) {
624
- let state = {
625
- status: "loading",
626
- value: void 0,
627
- revision: void 0,
628
- writable: false
629
- };
630
- const listeners = /* @__PURE__ */ new Set();
631
- let generation = 0;
632
- const publish = (next) => {
633
- state = next;
634
- for (const listener of [...listeners]) listener();
635
- };
636
- const load = async () => {
637
- const gen = ++generation;
638
- let response;
639
- try {
640
- response = await api.describe({});
641
- } catch {
642
- return;
572
+ /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
573
+ function injectStyle() {
574
+ if (typeof document === "undefined") return null;
575
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
576
+ const tag = document.createElement("style");
577
+ tag.dataset.plugin = PLUGIN_ID;
578
+ tag.dataset.pluginCss = STYLE_TAG_ID;
579
+ tag.textContent = cardCss;
580
+ document.head.appendChild(tag);
581
+ return tag;
643
582
  }
644
- if (gen !== generation || !response.result.ok) return;
645
- const writable = response.result.value?.writable ?? false;
646
- const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
647
- if (view === void 0) {
648
- publish({
649
- status: "unavailable",
583
+ //#endregion
584
+ //#region src/client/client.ts
585
+ const name = "dsh-plus-notify-email";
586
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
587
+ const inject = [
588
+ "slots",
589
+ "locale",
590
+ "connection",
591
+ "remote"
592
+ ];
593
+ /**
594
+ * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
595
+ * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
596
+ * 刷新:remote `settings/document-updated`(按命名空间过滤)与
597
+ * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
598
+ * (loopback 或 tailnet 信任域名)下行为一致。
599
+ */
600
+ function createApiScope(api, ns, c) {
601
+ let state = {
602
+ status: "loading",
650
603
  value: void 0,
651
604
  revision: void 0,
652
- writable
653
- });
654
- return;
655
- }
656
- publish({
657
- status: "ready",
658
- value: view.value,
659
- revision: view.revision,
660
- writable
661
- });
662
- };
663
- const refresh = (namespace) => {
664
- if (namespace !== void 0 && namespace !== ns) return;
665
- load();
666
- };
667
- const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
668
- load();
669
- })];
670
- c.effect(() => () => {
671
- for (const dispose of disposers) dispose();
672
- }, "notify-email: settings scope");
673
- load();
674
- return {
675
- getSnapshot: () => state,
676
- subscribe: (listener) => {
677
- listeners.add(listener);
678
- return () => {
679
- listeners.delete(listener);
605
+ writable: false
606
+ };
607
+ const listeners = /* @__PURE__ */ new Set();
608
+ let generation = 0;
609
+ const publish = (next) => {
610
+ state = next;
611
+ for (const listener of [...listeners]) listener();
680
612
  };
681
- },
682
- load
683
- };
684
- }
685
- function apply(ctx) {
686
- const c = ctx;
687
- const tag = injectStyle();
688
- c.effect(() => () => {
689
- tag?.remove();
690
- }, "notify-email: style");
691
- c.effect(() => c.locale.register(NS, {
692
- zh,
693
- en
694
- }), "notify-email: locale");
695
- const api = c.get("connection").api.settings;
696
- const scope = createApiScope(api, SETTINGS_NS, c);
697
- c.slots.inject("settings.plugin.item", () => c.slots.register({
698
- name: "settings.plugin.item",
699
- key: SETTINGS_NS,
700
- locale: NS,
701
- inject: () => ({
702
- t: c.locale.bind(NS),
703
- scope,
704
- api
705
- })
706
- }, NotifyEmailCard));
707
- }
708
-
709
- //#endregion
710
- exports.apply = apply;
711
- exports.inject = inject;
712
- exports.name = name;
613
+ const load = async () => {
614
+ const gen = ++generation;
615
+ let response;
616
+ try {
617
+ response = await api.describe({});
618
+ } catch {
619
+ return;
620
+ }
621
+ if (gen !== generation || !response.result.ok) return;
622
+ const writable = response.result.value?.writable ?? false;
623
+ const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
624
+ if (view === void 0) {
625
+ publish({
626
+ status: "unavailable",
627
+ value: void 0,
628
+ revision: void 0,
629
+ writable
630
+ });
631
+ return;
632
+ }
633
+ publish({
634
+ status: "ready",
635
+ value: view.value,
636
+ revision: view.revision,
637
+ writable
638
+ });
639
+ };
640
+ const refresh = (namespace) => {
641
+ if (namespace !== void 0 && namespace !== ns) return;
642
+ load();
643
+ };
644
+ const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
645
+ load();
646
+ })];
647
+ c.effect(() => () => {
648
+ for (const dispose of disposers) dispose();
649
+ }, "notify-email: settings scope");
650
+ load();
651
+ return {
652
+ getSnapshot: () => state,
653
+ subscribe: (listener) => {
654
+ listeners.add(listener);
655
+ return () => {
656
+ listeners.delete(listener);
657
+ };
658
+ },
659
+ load
660
+ };
661
+ }
662
+ function apply(ctx) {
663
+ const c = ctx;
664
+ const tag = injectStyle();
665
+ c.effect(() => () => {
666
+ tag?.remove();
667
+ }, "notify-email: style");
668
+ c.effect(() => c.locale.register(NS, {
669
+ zh,
670
+ en
671
+ }), "notify-email: locale");
672
+ const api = c.get("connection").api.settings;
673
+ const scope = createApiScope(api, SETTINGS_NS, c);
674
+ c.slots.inject("settings.plugin.item", () => c.slots.register({
675
+ name: "settings.plugin.item",
676
+ key: SETTINGS_NS,
677
+ locale: NS,
678
+ inject: () => ({
679
+ t: c.locale.bind(NS),
680
+ scope,
681
+ api
682
+ })
683
+ }, NotifyEmailCard));
684
+ }
685
+ //#endregion
686
+ exports.apply = apply;
687
+ exports.inject = inject;
688
+ exports.name = name;
713
689
  return module.exports;
714
690
  }
715
691
  });