@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 +626 -650
- package/lib/index.d.ts +0 -3
- package/lib/index.js +70 -66
- package/package.json +8 -8
- package/src/client/card.tsx +169 -52
- package/src/client/client.ts +20 -8
- package/src/client/scope.ts +25 -3
- package/src/config.ts +15 -3
- package/src/index.ts +8 -3
- package/src/mailer.ts +2 -2
- package/src/service.ts +23 -17
- package/src/triggers/builtin.ts +5 -2
- package/src/watchers/turn-end.ts +4 -7
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
})
|
|
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
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
text: `${t("testFailed")}${why}`
|
|
177
|
+
const edit = (key, editValue) => {
|
|
178
|
+
setDraft({
|
|
179
|
+
...draft,
|
|
180
|
+
[key]: editValue
|
|
245
181
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
405
|
-
className:
|
|
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:
|
|
269
|
+
children: t("readOnly")
|
|
408
270
|
}) : null,
|
|
409
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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)(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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)("
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
});
|