dsh-completion-reminder 1.1.0 → 1.3.0
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/README.md +71 -61
- package/install.md +17 -20
- package/lib/client.d.ts +24 -16
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +517 -361
- package/lib/client.js.map +1 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -64,25 +64,30 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
|
|
65
65
|
// ── compiled client code ────────────────────────────────────────
|
|
66
66
|
/**
|
|
67
|
-
* DSH Completion Reminder — client half (v1.
|
|
67
|
+
* DSH Completion Reminder — client half (v1.2).
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* "
|
|
73
|
-
*
|
|
69
|
+
* Strategy:
|
|
70
|
+
* 1. Detect agent-completion lifecycle by watching the DSH composer
|
|
71
|
+
* primary button's `aria-label` (which flips between "Stop
|
|
72
|
+
* generating" / "停止生成" and "Send message" / "发送消息") inside
|
|
73
|
+
* `[data-composer-card]`. The `data-phase` attribute on the
|
|
74
|
+
* conversation root helps disambiguate active / settling / hero.
|
|
74
75
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
76
|
+
* 2. Register a settings section in DSH's own settings dialog via
|
|
77
|
+
* `ctx.slots.inject('settings.section', ...)` so the user finds the
|
|
78
|
+
* configuration alongside General / Models / Plugins rather than as
|
|
79
|
+
* an extra floating widget.
|
|
80
|
+
*
|
|
81
|
+
* 3. Filter credential fields by the currently selected provider so the
|
|
82
|
+
* panel stays compact (Telegram shows 2 fields, Bark shows 1, etc.).
|
|
83
|
+
*
|
|
84
|
+
* 4. Persist configuration to `localStorage` and re-load on activation.
|
|
82
85
|
*
|
|
83
86
|
* The plugin is packaged as a DSH client plugin (`dsh.client` in
|
|
84
87
|
* package.json) and loaded through `window.__ModuleLoader__`.
|
|
85
88
|
*/
|
|
89
|
+
var React = require("react");
|
|
90
|
+
var { forwardRef, useEffect, useImperativeHandle, useRef } = require("react");
|
|
86
91
|
|
|
87
92
|
let config = { ...DEFAULT_OPTIONS };
|
|
88
93
|
const state = {
|
|
@@ -95,49 +100,22 @@ window.__ModuleLoader__.load({
|
|
|
95
100
|
isActive: false,
|
|
96
101
|
permission: detectPermission(),
|
|
97
102
|
unbinder: [],
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
panelHostEl: null,
|
|
104
|
+
panelRendered: false,
|
|
105
|
+
hintEl: null,
|
|
101
106
|
};
|
|
102
107
|
// ──── DOM signals we look at ───────────────────────────────────────────────
|
|
103
|
-
/**
|
|
104
|
-
* The composer card root in DSH: `<div data-composer-card="true">…</div>`.
|
|
105
|
-
* It contains the textarea, the `data-input-scroll` scrollport, and the
|
|
106
|
-
* primary send/stop button.
|
|
107
|
-
*/
|
|
108
108
|
const COMPOSER_CARD_SELECTOR = '[data-composer-card]';
|
|
109
|
-
/**
|
|
110
|
-
* The composer root containing all sessions. Has `data-phase` attribute
|
|
111
|
-
* ('hero' | 'active' | 'settling'). We use this to disambiguate "first
|
|
112
|
-
* paint idle" from "completed and went idle".
|
|
113
|
-
*/
|
|
114
109
|
const CONVERSATION_ROOT_SELECTOR = '[data-conversation-scroll], [data-composer-seat]';
|
|
115
|
-
/**
|
|
116
|
-
* The primary send/stop button. DSH flips its `aria-label` between
|
|
117
|
-
* "Stop generating" / "停止生成" (while running) and "Send message" /
|
|
118
|
-
* "发送消息" (when idle). In addition, DSH adds `disabled` to the
|
|
119
|
-
* button when the draft is empty.
|
|
120
|
-
*
|
|
121
|
-
* We do NOT match by class name (CSS-modules hashes change between
|
|
122
|
-
* builds) — only by stable attributes.
|
|
123
|
-
*/
|
|
124
|
-
const PRIMARY_BUTTON_SELECTOR = 'button[type="button"]';
|
|
125
|
-
/**
|
|
126
|
-
* Tokens we look for in the primary button's `aria-label` to detect
|
|
127
|
-
* a *running* agent.
|
|
128
|
-
*/
|
|
129
110
|
const RUNNING_TOKENS = [
|
|
130
111
|
'stop generating', '停止生成', '停止', 'stop',
|
|
131
112
|
'abort', 'cancel generating', 'cancel',
|
|
132
113
|
];
|
|
133
|
-
/** Tokens for an *idle* / send button. */
|
|
134
114
|
const IDLE_TOKENS = [
|
|
135
115
|
'send message', '发送消息', 'send', '发送',
|
|
136
116
|
];
|
|
137
117
|
// ──── Public API ───────────────────────────────────────────────────────────
|
|
138
118
|
function configure(opts) {
|
|
139
|
-
// First merge in any persisted config so the panel + programmatic
|
|
140
|
-
// configure() play nicely together.
|
|
141
119
|
const persisted = loadPersisted();
|
|
142
120
|
const merged = { ...persisted, ...(opts ?? {}) };
|
|
143
121
|
if (!opts || opts.providers === undefined) {
|
|
@@ -149,7 +127,8 @@ window.__ModuleLoader__.load({
|
|
|
149
127
|
providers: { ...DEFAULT_OPTIONS.providers, ...(merged.providers ?? {}) },
|
|
150
128
|
};
|
|
151
129
|
if (state.isActive) {
|
|
152
|
-
|
|
130
|
+
// Re-render the open panel with new values.
|
|
131
|
+
rerenderPanel();
|
|
153
132
|
}
|
|
154
133
|
}
|
|
155
134
|
function activate() {
|
|
@@ -163,9 +142,7 @@ window.__ModuleLoader__.load({
|
|
|
163
142
|
}
|
|
164
143
|
startObserver();
|
|
165
144
|
bindVisibilityEvents();
|
|
166
|
-
|
|
167
|
-
ensureSettingsButton();
|
|
168
|
-
}
|
|
145
|
+
// Don't auto-show a hint — the user now finds us via DSH Settings.
|
|
169
146
|
}
|
|
170
147
|
function deactivate() {
|
|
171
148
|
state.isActive = false;
|
|
@@ -176,9 +153,15 @@ window.__ModuleLoader__.load({
|
|
|
176
153
|
}
|
|
177
154
|
catch { /* noop */ }
|
|
178
155
|
}
|
|
179
|
-
|
|
156
|
+
removeHint();
|
|
180
157
|
state.runStartedAt = null;
|
|
181
158
|
state.inFlight = false;
|
|
159
|
+
// Clear the panel host so the next activation rebuilds it.
|
|
160
|
+
if (state.panelHostEl) {
|
|
161
|
+
state.panelHostEl.innerHTML = '';
|
|
162
|
+
state.panelHostEl = null;
|
|
163
|
+
}
|
|
164
|
+
state.panelRendered = false;
|
|
182
165
|
}
|
|
183
166
|
// ──── Persistence ──────────────────────────────────────────────────────────
|
|
184
167
|
function loadPersisted() {
|
|
@@ -207,7 +190,6 @@ window.__ModuleLoader__.load({
|
|
|
207
190
|
notifyOnError: config.notifyOnError,
|
|
208
191
|
suppressWhenFocused: config.suppressWhenFocused,
|
|
209
192
|
cooldownMs: config.cooldownMs,
|
|
210
|
-
showSettingsPanel: config.showSettingsPanel,
|
|
211
193
|
providers: config.providers,
|
|
212
194
|
};
|
|
213
195
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(out));
|
|
@@ -245,9 +227,6 @@ window.__ModuleLoader__.load({
|
|
|
245
227
|
if (state.observer)
|
|
246
228
|
state.observer.disconnect();
|
|
247
229
|
state.observer = new MutationObserver(handleMutations);
|
|
248
|
-
// Watch the whole document — DSH may swap the conversation root on
|
|
249
|
-
// session change. We only act on changes inside the composer card or
|
|
250
|
-
// the conversation root, which is cheap to filter in the callback.
|
|
251
230
|
state.observer.observe(document.body, {
|
|
252
231
|
childList: true,
|
|
253
232
|
subtree: true,
|
|
@@ -255,7 +234,6 @@ window.__ModuleLoader__.load({
|
|
|
255
234
|
attributes: true,
|
|
256
235
|
attributeFilter: ['aria-label', 'aria-disabled', 'data-phase', 'data-state', 'class', 'disabled'],
|
|
257
236
|
});
|
|
258
|
-
// Take an initial reading.
|
|
259
237
|
evaluateNow();
|
|
260
238
|
}
|
|
261
239
|
function stopObserver() {
|
|
@@ -268,8 +246,6 @@ window.__ModuleLoader__.load({
|
|
|
268
246
|
if (!state.isActive)
|
|
269
247
|
return;
|
|
270
248
|
for (const m of mutations) {
|
|
271
|
-
// Attribute changes on the primary button or its `data-composer-card`
|
|
272
|
-
// ancestor are the most precise signal.
|
|
273
249
|
if (m.type === 'attributes') {
|
|
274
250
|
const target = m.target;
|
|
275
251
|
if (target && isInsideComposer(target)) {
|
|
@@ -277,7 +253,6 @@ window.__ModuleLoader__.load({
|
|
|
277
253
|
continue;
|
|
278
254
|
}
|
|
279
255
|
}
|
|
280
|
-
// Newly added composer cards / buttons.
|
|
281
256
|
for (const node of m.addedNodes) {
|
|
282
257
|
if (!(node instanceof HTMLElement))
|
|
283
258
|
continue;
|
|
@@ -296,13 +271,11 @@ window.__ModuleLoader__.load({
|
|
|
296
271
|
captureRunMetadata();
|
|
297
272
|
const isRunning = !!primary && isRunningButton(primary);
|
|
298
273
|
if (isRunning && !state.inFlight) {
|
|
299
|
-
// idle → running
|
|
300
274
|
state.inFlight = true;
|
|
301
275
|
state.runStartedAt = Date.now();
|
|
302
276
|
return;
|
|
303
277
|
}
|
|
304
278
|
if (!isRunning && state.inFlight) {
|
|
305
|
-
// running → idle: completion event
|
|
306
279
|
state.inFlight = false;
|
|
307
280
|
const startedAt = state.runStartedAt ?? Date.now();
|
|
308
281
|
const durationMs = Date.now() - startedAt;
|
|
@@ -310,12 +283,6 @@ window.__ModuleLoader__.load({
|
|
|
310
283
|
void completeRun(determineStatus(), durationMs);
|
|
311
284
|
}
|
|
312
285
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Find the primary send/stop button by walking up from the composer card.
|
|
315
|
-
* The button is the only `<button type="button">` inside the composer that
|
|
316
|
-
* is NOT the leading add-command button, so we look for buttons with an
|
|
317
|
-
* aria-label that includes one of the well-known tokens.
|
|
318
|
-
*/
|
|
319
286
|
function findPrimaryButton() {
|
|
320
287
|
const card = document.querySelector(COMPOSER_CARD_SELECTOR);
|
|
321
288
|
if (!card)
|
|
@@ -328,7 +295,6 @@ window.__ModuleLoader__.load({
|
|
|
328
295
|
return b;
|
|
329
296
|
}
|
|
330
297
|
}
|
|
331
|
-
// Last-resort: if the composer has exactly one button, use it.
|
|
332
298
|
if (buttons.length === 1)
|
|
333
299
|
return buttons[0];
|
|
334
300
|
return null;
|
|
@@ -337,22 +303,11 @@ window.__ModuleLoader__.load({
|
|
|
337
303
|
const aria = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
338
304
|
return RUNNING_TOKENS.some((t) => aria.includes(t));
|
|
339
305
|
}
|
|
340
|
-
/**
|
|
341
|
-
* Look at the conversation list to figure out success / error / stopped.
|
|
342
|
-
* If we cannot decide, default to 'success' (the common case).
|
|
343
|
-
*/
|
|
344
306
|
function determineStatus() {
|
|
345
|
-
// Strongest signal: the conversation root's `data-phase` is `settling`
|
|
346
|
-
// while a turn is being finalised. We treat the moment it leaves
|
|
347
|
-
// `settling` as completion.
|
|
348
307
|
const root = document.querySelector(CONVERSATION_ROOT_SELECTOR);
|
|
349
308
|
const phase = root?.getAttribute('data-phase');
|
|
350
|
-
if (phase === 'settling')
|
|
309
|
+
if (phase === 'settling')
|
|
351
310
|
return 'success';
|
|
352
|
-
}
|
|
353
|
-
// Next, look at the latest assistant turn. If its last child has
|
|
354
|
-
// [data-state="interrupted"] / [data-state="error"] / class includes
|
|
355
|
-
// "error" / "stop", we have a more specific status.
|
|
356
311
|
const lastAssistant = findLastAssistantTurn();
|
|
357
312
|
if (lastAssistant) {
|
|
358
313
|
const cls = (lastAssistant.getAttribute('class') ?? '').toLowerCase();
|
|
@@ -491,7 +446,6 @@ window.__ModuleLoader__.load({
|
|
|
491
446
|
}
|
|
492
447
|
}
|
|
493
448
|
}
|
|
494
|
-
// ──── browser ──────────────────────────────────────────────────────────────
|
|
495
449
|
async function deliverBrowser(payload) {
|
|
496
450
|
if (state.permission === 'unsupported') {
|
|
497
451
|
showInPageToast(payload);
|
|
@@ -585,27 +539,15 @@ window.__ModuleLoader__.load({
|
|
|
585
539
|
style.id = TOAST_STYLE_ID;
|
|
586
540
|
style.textContent = `
|
|
587
541
|
.dsh-reminder-toast-root {
|
|
588
|
-
position: fixed;
|
|
589
|
-
|
|
590
|
-
right: 20px;
|
|
591
|
-
z-index: 2147483647;
|
|
592
|
-
display: flex;
|
|
593
|
-
flex-direction: column;
|
|
594
|
-
gap: 8px;
|
|
595
|
-
pointer-events: none;
|
|
596
|
-
max-width: 360px;
|
|
542
|
+
position: fixed; top: 20px; right: 20px; z-index: 2147483647;
|
|
543
|
+
display: flex; flex-direction: column; gap: 8px; pointer-events: none; max-width: 360px;
|
|
597
544
|
}
|
|
598
545
|
.dsh-reminder-toast {
|
|
599
546
|
pointer-events: auto;
|
|
600
|
-
background: ${DSH_CSS_VARS.bgModule};
|
|
601
|
-
|
|
602
|
-
border: 1px solid ${DSH_CSS_VARS.borderL3};
|
|
603
|
-
border-radius: 10px;
|
|
547
|
+
background: ${DSH_CSS_VARS.bgModule}; color: ${DSH_CSS_VARS.labelPrimary};
|
|
548
|
+
border: 1px solid ${DSH_CSS_VARS.borderL3}; border-radius: 10px;
|
|
604
549
|
box-shadow: ${DSH_CSS_VARS.shadowLv3};
|
|
605
|
-
padding: 12px 14px;
|
|
606
|
-
font-size: 13px;
|
|
607
|
-
line-height: 1.4;
|
|
608
|
-
cursor: pointer;
|
|
550
|
+
padding: 12px 14px; font-size: 13px; line-height: 1.4; cursor: pointer;
|
|
609
551
|
transition: opacity .25s ease, transform .25s ease;
|
|
610
552
|
}
|
|
611
553
|
.dsh-reminder-toast-title { font-weight: 600; margin-bottom: 4px; color: ${DSH_CSS_VARS.labelPrimary}; }
|
|
@@ -615,7 +557,6 @@ window.__ModuleLoader__.load({
|
|
|
615
557
|
`;
|
|
616
558
|
document.head.appendChild(style);
|
|
617
559
|
}
|
|
618
|
-
// ──── Generic fetch helpers ────────────────────────────────────────────────
|
|
619
560
|
async function postJson(url, body) {
|
|
620
561
|
const res = await fetch(url, {
|
|
621
562
|
method: 'POST',
|
|
@@ -651,7 +592,6 @@ window.__ModuleLoader__.load({
|
|
|
651
592
|
return '';
|
|
652
593
|
}
|
|
653
594
|
}
|
|
654
|
-
// ──── telegram ─────────────────────────────────────────────────────────────
|
|
655
595
|
async function deliverTelegram(payload) {
|
|
656
596
|
const cfg = config.providers;
|
|
657
597
|
if (!cfg.telegramBotToken || !cfg.telegramChatId) {
|
|
@@ -669,7 +609,6 @@ window.__ModuleLoader__.load({
|
|
|
669
609
|
function escapeMd(s) {
|
|
670
610
|
return s.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, (m) => `\\${m}`);
|
|
671
611
|
}
|
|
672
|
-
// ──── bark ─────────────────────────────────────────────────────────────────
|
|
673
612
|
async function deliverBark(payload) {
|
|
674
613
|
const cfg = config.providers;
|
|
675
614
|
if (!cfg.barkKey)
|
|
@@ -682,7 +621,6 @@ window.__ModuleLoader__.load({
|
|
|
682
621
|
throw new Error(`Bark ${res.status}: ${text || res.statusText}`);
|
|
683
622
|
}
|
|
684
623
|
}
|
|
685
|
-
// ──── pushover ─────────────────────────────────────────────────────────────
|
|
686
624
|
async function deliverPushover(payload) {
|
|
687
625
|
const cfg = config.providers;
|
|
688
626
|
if (!cfg.pushoverToken || !cfg.pushoverUserKey) {
|
|
@@ -700,7 +638,6 @@ window.__ModuleLoader__.load({
|
|
|
700
638
|
fields.device = cfg.pushoverDevice;
|
|
701
639
|
await postForm('https://api.pushover.net/1/messages.json', fields);
|
|
702
640
|
}
|
|
703
|
-
// ──── serverchan ───────────────────────────────────────────────────────────
|
|
704
641
|
async function deliverServerChan(payload) {
|
|
705
642
|
const cfg = config.providers;
|
|
706
643
|
if (!cfg.serverchanSendKey)
|
|
@@ -710,7 +647,6 @@ window.__ModuleLoader__.load({
|
|
|
710
647
|
desp: payload.body + (payload.url ? `\n\n[打开 DSH](${payload.url})` : ''),
|
|
711
648
|
});
|
|
712
649
|
}
|
|
713
|
-
// ──── discord ──────────────────────────────────────────────────────────────
|
|
714
650
|
async function deliverDiscord(payload) {
|
|
715
651
|
const cfg = config.providers;
|
|
716
652
|
if (!cfg.discordWebhookUrl)
|
|
@@ -720,7 +656,6 @@ window.__ModuleLoader__.load({
|
|
|
720
656
|
username: 'DSH Reminder',
|
|
721
657
|
});
|
|
722
658
|
}
|
|
723
|
-
// ──── slack ────────────────────────────────────────────────────────────────
|
|
724
659
|
async function deliverSlack(payload) {
|
|
725
660
|
const cfg = config.providers;
|
|
726
661
|
if (!cfg.slackWebhookUrl)
|
|
@@ -729,196 +664,400 @@ window.__ModuleLoader__.load({
|
|
|
729
664
|
text: `*${payload.title}*\n${payload.body}${payload.url ? `\n<${payload.url}|Open DSH>` : ''}`,
|
|
730
665
|
});
|
|
731
666
|
}
|
|
732
|
-
// ──── generic webhook ──────────────────────────────────────────────────────
|
|
733
667
|
async function deliverWebhook(payload) {
|
|
734
668
|
const cfg = config.providers;
|
|
735
669
|
if (!cfg.webhookUrl)
|
|
736
670
|
throw new Error('Webhook provider requires webhookUrl');
|
|
737
|
-
const body = cfg.webhookPayload
|
|
738
|
-
? cfg.webhookPayload(payload)
|
|
739
|
-
: payload;
|
|
671
|
+
const body = cfg.webhookPayload ? cfg.webhookPayload(payload) : payload;
|
|
740
672
|
await postJson(cfg.webhookUrl, body);
|
|
741
673
|
}
|
|
742
|
-
// ──── custom ───────────────────────────────────────────────────────────────
|
|
743
674
|
async function deliverCustom(payload) {
|
|
744
675
|
const fn = config.providers.customSend;
|
|
745
676
|
if (!fn)
|
|
746
677
|
throw new Error('Custom provider requires providers.customSend');
|
|
747
678
|
await fn(payload);
|
|
748
679
|
}
|
|
749
|
-
// ──── Helpers ──────────────────────────────────────────────────────────────
|
|
750
680
|
function toError(value) {
|
|
751
681
|
if (value instanceof Error)
|
|
752
682
|
return value;
|
|
753
683
|
return new Error(typeof value === 'string' ? value : JSON.stringify(value));
|
|
754
684
|
}
|
|
755
|
-
// ──── Settings
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
685
|
+
// ──── Settings dialog integration ──────────────────────────────────────────
|
|
686
|
+
/**
|
|
687
|
+
* Map a provider id to the credential fields it needs. Used to filter
|
|
688
|
+
* the panel so users only see the fields relevant to their choice.
|
|
689
|
+
*/
|
|
690
|
+
const PROVIDER_FIELDS = {
|
|
691
|
+
browser: [],
|
|
692
|
+
telegram: [
|
|
693
|
+
{ key: 'telegramBotToken', label: 'Bot Token', placeholder: '123456:ABC…' },
|
|
694
|
+
{ key: 'telegramChatId', label: 'Chat ID', placeholder: '123456789' },
|
|
695
|
+
],
|
|
696
|
+
bark: [
|
|
697
|
+
{ key: 'barkKey', label: 'Bark Key', placeholder: 'iPhone Bark 设备 Key' },
|
|
698
|
+
{ key: 'barkServer', label: 'Bark Server(可选)', placeholder: 'https://api.day.app' },
|
|
699
|
+
],
|
|
700
|
+
pushover: [
|
|
701
|
+
{ key: 'pushoverToken', label: 'App Token', placeholder: 'a…' },
|
|
702
|
+
{ key: 'pushoverUserKey', label: 'User Key', placeholder: 'u…' },
|
|
703
|
+
{ key: 'pushoverDevice', label: 'Device(可选)', placeholder: '留空推送到所有设备' },
|
|
704
|
+
],
|
|
705
|
+
serverchan: [
|
|
706
|
+
{ key: 'serverchanSendKey', label: 'Server酱 SendKey', placeholder: 'SCT…' },
|
|
707
|
+
],
|
|
708
|
+
discord: [
|
|
709
|
+
{ key: 'discordWebhookUrl', label: 'Discord Webhook URL', placeholder: 'https://discord.com/api/webhooks/…' },
|
|
710
|
+
],
|
|
711
|
+
slack: [
|
|
712
|
+
{ key: 'slackWebhookUrl', label: 'Slack Webhook URL', placeholder: 'https://hooks.slack.com/services/…' },
|
|
713
|
+
],
|
|
714
|
+
webhook: [
|
|
715
|
+
{ key: 'webhookUrl', label: 'Webhook URL', placeholder: 'https://your-service.example/notify' },
|
|
716
|
+
],
|
|
717
|
+
custom: [
|
|
718
|
+
{ key: 'webhookUrl', label: 'Custom provider 占位字段', placeholder: '可填任意值' },
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
const PROVIDER_LABELS = [
|
|
722
|
+
['browser', '🌐 浏览器通知'],
|
|
723
|
+
['telegram', '✈️ Telegram'],
|
|
724
|
+
['bark', '🍎 Bark (iOS)'],
|
|
725
|
+
['pushover', '📲 Pushover'],
|
|
726
|
+
['serverchan', '🐦 Server酱'],
|
|
727
|
+
['discord', '🎮 Discord'],
|
|
728
|
+
['slack', '💼 Slack'],
|
|
729
|
+
['webhook', '🔗 通用 Webhook'],
|
|
730
|
+
['custom', '🛠 自定义'],
|
|
731
|
+
];
|
|
732
|
+
const PANEL_STYLE_ID = 'dsh-completion-reminder-panel-style';
|
|
733
|
+
/**
|
|
734
|
+
* The Host React component. It renders a stable <div> that the
|
|
735
|
+
* vanilla JS portion of the plugin fills in. This way we get free
|
|
736
|
+
* DSH theming (the host div inherits the dialog's CSS variables) and
|
|
737
|
+
* we don't have to bundle React/JSX-runtime.
|
|
738
|
+
*/
|
|
739
|
+
const Host = forwardRef((_props, ref) => {
|
|
740
|
+
const innerRef = useRef(null);
|
|
741
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
742
|
+
return React.createElement('div', {
|
|
743
|
+
ref: innerRef,
|
|
744
|
+
className: 'dsh-reminder-host',
|
|
745
|
+
'data-reminder-host': '',
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
Host.displayName = 'DSHCompletionReminderHost';
|
|
749
|
+
/**
|
|
750
|
+
* Component registered with DSH's `settings.section` slot. Renders a
|
|
751
|
+
* ref-bound div into the dialog; the ref is captured by the parent
|
|
752
|
+
* closure and populated with the actual settings UI.
|
|
753
|
+
*/
|
|
754
|
+
function makeSectionComponent(registerHost) {
|
|
755
|
+
return function ReminderSection() {
|
|
756
|
+
const localRef = useRef(null);
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (localRef.current) {
|
|
759
|
+
registerHost(localRef.current);
|
|
760
|
+
}
|
|
761
|
+
return () => {
|
|
762
|
+
if (localRef.current) {
|
|
763
|
+
localRef.current.innerHTML = '';
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}, []);
|
|
767
|
+
return React.createElement(Host, { ref: localRef });
|
|
768
|
+
};
|
|
774
769
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Plugin entry point invoked by the DSH Cordis Loader.
|
|
772
|
+
*
|
|
773
|
+
* In DSH v1.x the host invokes the plugin's exported `apply(ctx, opts)`
|
|
774
|
+
* after the module system has been bootstrapped. We use the context
|
|
775
|
+
* to register a settings section.
|
|
776
|
+
*/
|
|
777
|
+
function apply(ctx, opts) {
|
|
778
|
+
configure(opts);
|
|
779
|
+
activate();
|
|
780
|
+
// Register the settings entry point.
|
|
781
|
+
//
|
|
782
|
+
// The user finds this under "DSH 设置 → 插件 → 完成提醒" — a sibling
|
|
783
|
+
// tab to the existing "可配置" (configurable) tab. We register:
|
|
784
|
+
// 1. `settings.plugins.tab` — a new tab inside the Plugins section
|
|
785
|
+
// 2. `settings.section` — fallback top-level section in case
|
|
786
|
+
// the host is too old to expose the plugins tab slot
|
|
787
|
+
// The tab registration is the primary one and shows up in the right
|
|
788
|
+
// place; the section is registered too so the user can still find
|
|
789
|
+
// the configuration if the host has a non-standard Plugins UI.
|
|
790
|
+
try {
|
|
791
|
+
if (ctx && ctx.slots) {
|
|
792
|
+
const slots = ctx.slots;
|
|
793
|
+
const hostRef = { current: null };
|
|
794
|
+
// ──── primary: a tab inside the Plugins section ────────────────
|
|
795
|
+
// DSH v1.2+ uses `settings.plugins.tab` to render tab buttons in
|
|
796
|
+
// the Plugins section. The component factory is invoked with DI
|
|
797
|
+
// props; we accept them and ignore everything except the empty
|
|
798
|
+
// `renderSlot`, since our body is a vanilla-DOM host.
|
|
799
|
+
try {
|
|
800
|
+
slots.inject('settings.plugins.tab', () => {
|
|
801
|
+
return slots.register({
|
|
802
|
+
name: 'settings.plugins.tab',
|
|
803
|
+
id: 'reminder',
|
|
804
|
+
order: 100,
|
|
805
|
+
label: () => '🔔 完成提醒',
|
|
806
|
+
locale: '@dsh-completion-reminder',
|
|
807
|
+
inject: () => ({}),
|
|
808
|
+
}, function ReminderPluginsTab() {
|
|
809
|
+
const localRef = useRef(null);
|
|
810
|
+
useEffect(() => {
|
|
811
|
+
if (localRef.current) {
|
|
812
|
+
hostRef.current = localRef.current;
|
|
813
|
+
injectPanelInto(localRef.current);
|
|
814
|
+
}
|
|
815
|
+
return () => {
|
|
816
|
+
if (localRef.current)
|
|
817
|
+
localRef.current.innerHTML = '';
|
|
818
|
+
};
|
|
819
|
+
}, []);
|
|
820
|
+
return React.createElement(Host, { ref: localRef });
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
try {
|
|
826
|
+
console.warn('[dsh-completion-reminder] settings.plugins.tab registration failed:', err);
|
|
827
|
+
}
|
|
828
|
+
catch { /* noop */ }
|
|
829
|
+
}
|
|
830
|
+
// ──── fallback: top-level section ───────────────────────────────
|
|
831
|
+
// Older hosts that don't know about the plugins-tab slot will
|
|
832
|
+
// accept a top-level section registration so the user can still
|
|
833
|
+
// find the configuration.
|
|
834
|
+
try {
|
|
835
|
+
slots.inject('settings.section', () => {
|
|
836
|
+
const sectionFactory = makeSectionComponent((el) => {
|
|
837
|
+
hostRef.current = el;
|
|
838
|
+
injectPanelInto(el);
|
|
839
|
+
});
|
|
840
|
+
return slots.register({
|
|
841
|
+
name: 'settings.section',
|
|
842
|
+
id: 'reminder',
|
|
843
|
+
order: 50,
|
|
844
|
+
label: () => '🔔 提醒',
|
|
845
|
+
locale: '@dsh-completion-reminder',
|
|
846
|
+
inject: () => ({}),
|
|
847
|
+
}, sectionFactory);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
catch (err) {
|
|
851
|
+
try {
|
|
852
|
+
console.warn('[dsh-completion-reminder] settings.section registration failed:', err);
|
|
853
|
+
}
|
|
854
|
+
catch { /* noop */ }
|
|
855
|
+
}
|
|
856
|
+
// If neither slot was registered, the host is too old. The
|
|
857
|
+
// detection still works, but the user has nowhere to configure;
|
|
858
|
+
// surface a hint.
|
|
859
|
+
if (!hostRef.current) {
|
|
860
|
+
// Defer: hostRef gets populated on tab/section mount, but
|
|
861
|
+
// that happens after DSH opens the dialog. Show a hint now.
|
|
862
|
+
showFirstRunHint('DSH 主机不支持 settings.plugins.tab 槽位。');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
showFirstRunHint();
|
|
867
|
+
}
|
|
778
868
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
869
|
+
catch (err) {
|
|
870
|
+
try {
|
|
871
|
+
console.warn('[dsh-completion-reminder] failed to register settings UI:', err);
|
|
872
|
+
}
|
|
873
|
+
catch { /* noop */ }
|
|
874
|
+
showFirstRunHint();
|
|
783
875
|
}
|
|
784
876
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
877
|
+
/**
|
|
878
|
+
* Show a single, dismissable hint that points the user to the DSH
|
|
879
|
+
* settings dialog. We only show it once and only when the section
|
|
880
|
+
* registration failed (older host, no slots service, etc.).
|
|
881
|
+
*/
|
|
882
|
+
function showFirstRunHint(suffix) {
|
|
883
|
+
if (state.hintEl || hasPersistedConfig())
|
|
884
|
+
return;
|
|
885
|
+
injectHintStyles();
|
|
886
|
+
const el = document.createElement('div');
|
|
887
|
+
el.className = 'dsh-reminder-hint';
|
|
888
|
+
const msg = suffix || '已激活。请在 DSH 设置里配置通知渠道。';
|
|
889
|
+
el.innerHTML = `
|
|
890
|
+
<span>🔔 DSH Completion Reminder ${escapeHtml(msg)}</span>
|
|
891
|
+
<button type="button" data-reminder-hint-dismiss>知道了</button>
|
|
892
|
+
`;
|
|
893
|
+
el.querySelector('[data-reminder-hint-dismiss]')?.addEventListener('click', () => {
|
|
894
|
+
el.remove();
|
|
895
|
+
state.hintEl = null;
|
|
896
|
+
});
|
|
897
|
+
document.body.appendChild(el);
|
|
898
|
+
state.hintEl = el;
|
|
899
|
+
setTimeout(() => {
|
|
900
|
+
el.classList.add('dsh-reminder-hint-leave');
|
|
901
|
+
setTimeout(() => { el.remove(); state.hintEl = null; }, 600);
|
|
902
|
+
}, 12000);
|
|
903
|
+
}
|
|
904
|
+
function removeHint() {
|
|
905
|
+
if (state.hintEl) {
|
|
906
|
+
state.hintEl.remove();
|
|
907
|
+
state.hintEl = null;
|
|
790
908
|
}
|
|
791
909
|
}
|
|
792
|
-
function
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
910
|
+
function hasPersistedConfig() {
|
|
911
|
+
if (typeof localStorage === 'undefined')
|
|
912
|
+
return false;
|
|
913
|
+
try {
|
|
914
|
+
return !!localStorage.getItem(STORAGE_KEY);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
797
919
|
}
|
|
798
|
-
|
|
799
|
-
|
|
920
|
+
const HINT_STYLE_ID = 'dsh-completion-reminder-hint-style';
|
|
921
|
+
function injectHintStyles() {
|
|
922
|
+
if (document.getElementById(HINT_STYLE_ID))
|
|
800
923
|
return;
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
924
|
+
const style = document.createElement('style');
|
|
925
|
+
style.id = HINT_STYLE_ID;
|
|
926
|
+
style.textContent = `
|
|
927
|
+
.dsh-reminder-hint {
|
|
928
|
+
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
|
|
929
|
+
z-index: 2147483600;
|
|
930
|
+
display: flex; align-items: center; gap: 10px;
|
|
931
|
+
background: ${DSH_CSS_VARS.bgModule}; color: ${DSH_CSS_VARS.labelPrimary};
|
|
932
|
+
border: 1px solid ${DSH_CSS_VARS.borderL3}; border-radius: 999px;
|
|
933
|
+
box-shadow: ${DSH_CSS_VARS.shadowLv3};
|
|
934
|
+
padding: 8px 8px 8px 14px;
|
|
935
|
+
font-size: 13px; line-height: 1.2;
|
|
936
|
+
transition: opacity .4s, transform .4s;
|
|
937
|
+
}
|
|
938
|
+
.dsh-reminder-hint button {
|
|
939
|
+
background: ${DSH_CSS_VARS.buttonInfoFill}; color: #fff;
|
|
940
|
+
border: none; border-radius: 999px; padding: 4px 10px;
|
|
941
|
+
font-size: 12px; cursor: pointer; font-family: inherit;
|
|
942
|
+
}
|
|
943
|
+
.dsh-reminder-hint-leave { opacity: 0; transform: translate(-50%, 12px); }
|
|
944
|
+
`;
|
|
945
|
+
document.head.appendChild(style);
|
|
946
|
+
}
|
|
947
|
+
// ──── Panel rendering (vanilla DOM inside the DSH settings dialog) ─────────
|
|
948
|
+
/**
|
|
949
|
+
* Provider descriptions shown next to the picker.
|
|
950
|
+
*/
|
|
951
|
+
const PROVIDER_DESCRIPTIONS = {
|
|
952
|
+
browser: '使用浏览器原生通知 API,首次使用需用户授权。',
|
|
953
|
+
telegram: '通过 Telegram Bot 推送到你的聊天 / 频道 / 群组。',
|
|
954
|
+
bark: '通过 Bark HTTP API 推送到 iPhone。',
|
|
955
|
+
pushover: '通过 Pushover 推送到 Android / iOS / 桌面。',
|
|
956
|
+
serverchan: '推送到微信(sct.ftqq.com,SendKey)。',
|
|
957
|
+
discord: '通过 Discord Webhook 推送到频道。',
|
|
958
|
+
slack: '通过 Slack Incoming Webhook 推送到频道。',
|
|
959
|
+
webhook: 'POST JSON 到你提供的 URL。',
|
|
960
|
+
custom: '通过 customSend(payload) 函数自己实现。',
|
|
961
|
+
};
|
|
962
|
+
function injectPanelInto(host) {
|
|
963
|
+
state.panelHostEl = host;
|
|
964
|
+
renderPanelInto(host);
|
|
965
|
+
}
|
|
966
|
+
function rerenderPanel() {
|
|
967
|
+
if (state.panelHostEl) {
|
|
968
|
+
renderPanelInto(state.panelHostEl);
|
|
816
969
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
[
|
|
826
|
-
['discord', '🎮 Discord'],
|
|
827
|
-
['slack', '💼 Slack'],
|
|
828
|
-
['webhook', '🔗 通用 Webhook'],
|
|
829
|
-
['custom', '🛠 自定义'],
|
|
830
|
-
];
|
|
831
|
-
const providerOptions = providers
|
|
832
|
-
.map(([id, name]) => `<option value="${id}" ${config.provider === id ? 'selected' : ''}>${name}</option>`)
|
|
970
|
+
}
|
|
971
|
+
function renderPanelInto(host) {
|
|
972
|
+
injectPanelStyles();
|
|
973
|
+
host.innerHTML = buildPanelHtml();
|
|
974
|
+
wirePanelEvents(host);
|
|
975
|
+
}
|
|
976
|
+
function buildPanelHtml() {
|
|
977
|
+
const providerOptions = PROVIDER_LABELS
|
|
978
|
+
.map(([id, label]) => `<option value="${id}" ${config.provider === id ? 'selected' : ''}>${escapeHtml(label)}</option>`)
|
|
833
979
|
.join('');
|
|
834
980
|
const p = config.providers;
|
|
835
|
-
const fields = [
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
];
|
|
848
|
-
const providerFieldsHtml = fields
|
|
849
|
-
.map(([name, label, placeholder, value]) => `
|
|
850
|
-
<label class="dsh-reminder-field" data-provider-field>
|
|
851
|
-
<span>${label}</span>
|
|
852
|
-
<input type="text" name="${name}" data-provider-input="${name}" placeholder="${escapeAttr(placeholder)}" value="${escapeAttr(value)}" autocomplete="off" spellcheck="false" />
|
|
853
|
-
</label>`)
|
|
854
|
-
.join('');
|
|
855
|
-
const perm = state.permission;
|
|
981
|
+
const fields = PROVIDER_FIELDS[config.provider] ?? [];
|
|
982
|
+
const fieldsHtml = fields.length === 0
|
|
983
|
+
? `<p class="dsh-reminder-panel-hint">此渠道无需凭证,保存即可使用。</p>`
|
|
984
|
+
: fields.map((f) => {
|
|
985
|
+
const value = p[f.key] ?? '';
|
|
986
|
+
return `
|
|
987
|
+
<label class="dsh-reminder-panel-field">
|
|
988
|
+
<span class="dsh-reminder-panel-label">${escapeHtml(f.label)}</span>
|
|
989
|
+
<input type="text" data-reminder-field="${escapeAttr(f.key)}" value="${escapeAttr(value)}" placeholder="${escapeAttr(f.placeholder)}" autocomplete="off" spellcheck="false" />
|
|
990
|
+
</label>
|
|
991
|
+
`;
|
|
992
|
+
}).join('');
|
|
993
|
+
const desc = PROVIDER_DESCRIPTIONS[config.provider] ?? '';
|
|
856
994
|
return `
|
|
857
|
-
<
|
|
858
|
-
<
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
<label class="dsh-reminder-field">
|
|
863
|
-
<span>通知渠道</span>
|
|
864
|
-
<select data-reminder-input="provider">${providerOptions}</select>
|
|
865
|
-
</label>
|
|
995
|
+
<div class="dsh-reminder-panel">
|
|
996
|
+
<header class="dsh-reminder-panel-header">
|
|
997
|
+
<strong>Agent 完成提醒</strong>
|
|
998
|
+
<span class="dsh-reminder-panel-sub">配置通知渠道,agent 完成后通知你</span>
|
|
999
|
+
</header>
|
|
866
1000
|
|
|
867
|
-
<
|
|
868
|
-
<
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1001
|
+
<section class="dsh-reminder-panel-section">
|
|
1002
|
+
<label class="dsh-reminder-panel-field">
|
|
1003
|
+
<span class="dsh-reminder-panel-label">通知渠道</span>
|
|
1004
|
+
<select data-reminder-input="provider">${providerOptions}</select>
|
|
1005
|
+
</label>
|
|
1006
|
+
<p class="dsh-reminder-panel-hint">${escapeHtml(desc)}</p>
|
|
1007
|
+
</section>
|
|
1008
|
+
|
|
1009
|
+
<section class="dsh-reminder-panel-section">
|
|
1010
|
+
<header class="dsh-reminder-panel-section-title">凭证(仅当前渠道需要)</header>
|
|
1011
|
+
${fieldsHtml}
|
|
1012
|
+
</section>
|
|
875
1013
|
|
|
876
|
-
<
|
|
877
|
-
<
|
|
878
|
-
<label class="dsh-reminder-row">
|
|
1014
|
+
<section class="dsh-reminder-panel-section">
|
|
1015
|
+
<header class="dsh-reminder-panel-section-title">行为</header>
|
|
1016
|
+
<label class="dsh-reminder-panel-row">
|
|
879
1017
|
<input type="checkbox" data-reminder-input="notifyOnSuccess" ${config.notifyOnSuccess ? 'checked' : ''} />
|
|
880
1018
|
<span>成功完成时通知</span>
|
|
881
1019
|
</label>
|
|
882
|
-
<label class="dsh-reminder-row">
|
|
1020
|
+
<label class="dsh-reminder-panel-row">
|
|
883
1021
|
<input type="checkbox" data-reminder-input="notifyOnStopped" ${config.notifyOnStopped ? 'checked' : ''} />
|
|
884
1022
|
<span>用户主动停止时通知</span>
|
|
885
1023
|
</label>
|
|
886
|
-
<label class="dsh-reminder-row">
|
|
1024
|
+
<label class="dsh-reminder-panel-row">
|
|
887
1025
|
<input type="checkbox" data-reminder-input="notifyOnError" ${config.notifyOnError ? 'checked' : ''} />
|
|
888
1026
|
<span>Agent 出错时通知</span>
|
|
889
1027
|
</label>
|
|
890
|
-
<label class="dsh-reminder-row">
|
|
1028
|
+
<label class="dsh-reminder-panel-row">
|
|
891
1029
|
<input type="checkbox" data-reminder-input="suppressWhenFocused" ${config.suppressWhenFocused ? 'checked' : ''} />
|
|
892
1030
|
<span>DSH 标签页可见时静默(推荐)</span>
|
|
893
1031
|
</label>
|
|
894
|
-
<label class="dsh-reminder-row">
|
|
1032
|
+
<label class="dsh-reminder-panel-row">
|
|
895
1033
|
<input type="checkbox" data-reminder-input="autoRequestPermission" ${config.autoRequestPermission ? 'checked' : ''} />
|
|
896
1034
|
<span>自动请求浏览器通知权限</span>
|
|
897
1035
|
</label>
|
|
898
|
-
<label class="dsh-reminder-field">
|
|
899
|
-
<span>冷却(ms)— 防止连续完成时刷屏</span>
|
|
1036
|
+
<label class="dsh-reminder-panel-field">
|
|
1037
|
+
<span class="dsh-reminder-panel-label">冷却(ms)— 防止连续完成时刷屏</span>
|
|
900
1038
|
<input type="number" min="0" step="500" data-reminder-input="cooldownMs" value="${config.cooldownMs}" />
|
|
901
1039
|
</label>
|
|
902
|
-
</
|
|
1040
|
+
</section>
|
|
903
1041
|
|
|
904
|
-
<
|
|
905
|
-
<span>当前权限:<strong data-reminder-perm>${permissionLabel(
|
|
1042
|
+
<section class="dsh-reminder-panel-section dsh-reminder-panel-perm">
|
|
1043
|
+
<span>当前权限:<strong data-reminder-perm>${permissionLabel(state.permission)}</strong></span>
|
|
906
1044
|
<button type="button" data-reminder-action="request-permission">请求权限</button>
|
|
907
|
-
</
|
|
1045
|
+
</section>
|
|
908
1046
|
|
|
909
|
-
<
|
|
1047
|
+
<footer class="dsh-reminder-panel-actions">
|
|
910
1048
|
<button type="button" class="primary" data-reminder-action="test">发送测试通知</button>
|
|
911
|
-
<button type="button" data-reminder-action="save">保存</button>
|
|
912
1049
|
<button type="button" data-reminder-action="reset">重置</button>
|
|
913
|
-
|
|
914
|
-
|
|
1050
|
+
<span class="dsh-reminder-panel-status" data-reminder-status></span>
|
|
1051
|
+
</footer>
|
|
1052
|
+
<p class="dsh-reminder-panel-hint">配置只保存在本浏览器的 localStorage,不会上传任何服务器。</p>
|
|
915
1053
|
</div>
|
|
916
1054
|
`;
|
|
917
1055
|
}
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1056
|
+
function wirePanelEvents(host) {
|
|
1057
|
+
const root = host.querySelector('.dsh-reminder-panel');
|
|
1058
|
+
if (!root)
|
|
1059
|
+
return;
|
|
1060
|
+
root.addEventListener('change', (ev) => {
|
|
922
1061
|
const target = ev.target;
|
|
923
1062
|
if (!target)
|
|
924
1063
|
return;
|
|
@@ -926,10 +1065,15 @@ window.__ModuleLoader__.load({
|
|
|
926
1065
|
const name = target.dataset.reminderInput;
|
|
927
1066
|
if (name === 'provider') {
|
|
928
1067
|
config.provider = target.value;
|
|
1068
|
+
savePersisted();
|
|
1069
|
+
// Re-render so the credentials section updates to the new channel.
|
|
1070
|
+
renderPanelInto(host);
|
|
929
1071
|
}
|
|
930
1072
|
}
|
|
931
1073
|
else if (target instanceof HTMLInputElement) {
|
|
932
1074
|
const name = target.dataset.reminderInput;
|
|
1075
|
+
if (!name)
|
|
1076
|
+
return;
|
|
933
1077
|
const value = target.type === 'checkbox' ? target.checked
|
|
934
1078
|
: target.type === 'number' ? Number(target.value) || 0
|
|
935
1079
|
: target.value;
|
|
@@ -945,26 +1089,20 @@ window.__ModuleLoader__.load({
|
|
|
945
1089
|
config.autoRequestPermission = !!value;
|
|
946
1090
|
else if (name === 'cooldownMs')
|
|
947
1091
|
config.cooldownMs = Number(value) || 0;
|
|
1092
|
+
savePersisted();
|
|
948
1093
|
}
|
|
949
|
-
savePersisted();
|
|
950
1094
|
});
|
|
951
|
-
|
|
1095
|
+
root.addEventListener('input', (ev) => {
|
|
952
1096
|
const target = ev.target;
|
|
953
1097
|
if (!target)
|
|
954
1098
|
return;
|
|
955
|
-
const
|
|
956
|
-
if (
|
|
957
|
-
config.providers[
|
|
1099
|
+
const key = target.dataset.reminderField;
|
|
1100
|
+
if (key) {
|
|
1101
|
+
config.providers[key] = target.value;
|
|
958
1102
|
savePersisted();
|
|
959
1103
|
}
|
|
960
1104
|
});
|
|
961
|
-
|
|
962
|
-
?.addEventListener('click', () => {
|
|
963
|
-
savePersisted();
|
|
964
|
-
setStatus(panel, '已保存 ✓');
|
|
965
|
-
});
|
|
966
|
-
panel.querySelector('[data-reminder-action="reset"]')
|
|
967
|
-
?.addEventListener('click', () => {
|
|
1105
|
+
root.querySelector('[data-reminder-action="reset"]')?.addEventListener('click', () => {
|
|
968
1106
|
if (typeof localStorage !== 'undefined') {
|
|
969
1107
|
try {
|
|
970
1108
|
localStorage.removeItem(STORAGE_KEY);
|
|
@@ -972,18 +1110,18 @@ window.__ModuleLoader__.load({
|
|
|
972
1110
|
catch { /* noop */ }
|
|
973
1111
|
}
|
|
974
1112
|
configure();
|
|
975
|
-
|
|
976
|
-
|
|
1113
|
+
renderPanelInto(host);
|
|
1114
|
+
setPanelStatus(root, '已重置 ✓');
|
|
977
1115
|
});
|
|
978
|
-
|
|
1116
|
+
root.querySelector('[data-reminder-action="request-permission"]')
|
|
979
1117
|
?.addEventListener('click', async () => {
|
|
980
1118
|
const result = await requestBrowserPermission();
|
|
981
|
-
const permEl =
|
|
1119
|
+
const permEl = root.querySelector('[data-reminder-perm]');
|
|
982
1120
|
if (permEl)
|
|
983
1121
|
permEl.textContent = permissionLabel(result);
|
|
984
|
-
|
|
1122
|
+
setPanelStatus(root, result === 'granted' ? '权限已授予' : '权限状态:' + permissionLabel(result));
|
|
985
1123
|
});
|
|
986
|
-
|
|
1124
|
+
root.querySelector('[data-reminder-action="test"]')
|
|
987
1125
|
?.addEventListener('click', async () => {
|
|
988
1126
|
try {
|
|
989
1127
|
const ctx = {
|
|
@@ -1002,16 +1140,16 @@ window.__ModuleLoader__.load({
|
|
|
1002
1140
|
};
|
|
1003
1141
|
await dispatch(payload);
|
|
1004
1142
|
config.onNotify(payload, config.provider);
|
|
1005
|
-
|
|
1143
|
+
setPanelStatus(root, '测试通知已发送,请查收。');
|
|
1006
1144
|
}
|
|
1007
1145
|
catch (err) {
|
|
1008
1146
|
const e = toError(err);
|
|
1009
1147
|
config.onError(e, config.provider);
|
|
1010
|
-
|
|
1148
|
+
setPanelStatus(root, '测试失败:' + e.message, true);
|
|
1011
1149
|
}
|
|
1012
1150
|
});
|
|
1013
1151
|
}
|
|
1014
|
-
function
|
|
1152
|
+
function setPanelStatus(panel, text, isError = false) {
|
|
1015
1153
|
const el = panel.querySelector('[data-reminder-status]');
|
|
1016
1154
|
if (!el)
|
|
1017
1155
|
return;
|
|
@@ -1033,169 +1171,187 @@ window.__ModuleLoader__.load({
|
|
|
1033
1171
|
return '不支持';
|
|
1034
1172
|
return '未询问';
|
|
1035
1173
|
}
|
|
1036
|
-
function
|
|
1174
|
+
function escapeHtml(s) {
|
|
1037
1175
|
return s
|
|
1038
1176
|
.replace(/&/g, '&')
|
|
1039
|
-
.replace(/"/g, '"')
|
|
1040
1177
|
.replace(/</g, '<')
|
|
1041
1178
|
.replace(/>/g, '>');
|
|
1042
1179
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1180
|
+
function escapeAttr(s) {
|
|
1181
|
+
return escapeHtml(s).replace(/"/g, '"');
|
|
1182
|
+
}
|
|
1183
|
+
function injectPanelStyles() {
|
|
1184
|
+
if (document.getElementById(PANEL_STYLE_ID))
|
|
1045
1185
|
return;
|
|
1046
1186
|
const style = document.createElement('style');
|
|
1047
|
-
style.id =
|
|
1187
|
+
style.id = PANEL_STYLE_ID;
|
|
1188
|
+
// The host's parent (DSH settings dialog) sets `color-scheme: dark`
|
|
1189
|
+
// for the dark theme, so we explicitly hint the dark UA scheme on
|
|
1190
|
+
// selects + add explicit colors that work in both themes. Native
|
|
1191
|
+
// form controls otherwise default to UA colors (white-on-white in
|
|
1192
|
+
// dark mode).
|
|
1048
1193
|
style.textContent = `
|
|
1049
|
-
.dsh-reminder-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
bottom: 18px;
|
|
1053
|
-
z-index: 2147483600;
|
|
1054
|
-
width: 40px;
|
|
1055
|
-
height: 40px;
|
|
1056
|
-
border-radius: 50%;
|
|
1057
|
-
background: ${DSH_CSS_VARS.buttonInfoFill};
|
|
1058
|
-
color: #fff;
|
|
1059
|
-
border: none;
|
|
1060
|
-
box-shadow: ${DSH_CSS_VARS.shadowLv3};
|
|
1061
|
-
cursor: pointer;
|
|
1062
|
-
display: grid;
|
|
1063
|
-
place-items: center;
|
|
1064
|
-
opacity: 0.85;
|
|
1065
|
-
transition: opacity .15s, transform .15s;
|
|
1066
|
-
}
|
|
1067
|
-
.dsh-reminder-fab:hover { opacity: 1; transform: scale(1.05); }
|
|
1068
|
-
|
|
1194
|
+
.dsh-reminder-host { color: inherit; }
|
|
1195
|
+
.dsh-reminder-host,
|
|
1196
|
+
.dsh-reminder-host * { box-sizing: border-box; }
|
|
1069
1197
|
.dsh-reminder-panel {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
z-index: 2147483601;
|
|
1074
|
-
width: 360px;
|
|
1075
|
-
max-height: min(80vh, 640px);
|
|
1076
|
-
overflow: auto;
|
|
1077
|
-
background: ${DSH_CSS_VARS.bgModule};
|
|
1198
|
+
display: flex;
|
|
1199
|
+
flex-direction: column;
|
|
1200
|
+
gap: 18px;
|
|
1078
1201
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1079
|
-
border: 1px solid ${DSH_CSS_VARS.borderL3};
|
|
1080
|
-
border-radius: 12px;
|
|
1081
|
-
box-shadow: ${DSH_CSS_VARS.shadowLv3};
|
|
1082
1202
|
font-size: 13px;
|
|
1083
|
-
line-height: 1.
|
|
1084
|
-
|
|
1203
|
+
line-height: 1.5;
|
|
1204
|
+
padding: 4px 4px 24px;
|
|
1085
1205
|
}
|
|
1086
1206
|
.dsh-reminder-panel-header {
|
|
1087
1207
|
display: flex;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
padding: 10px 12px;
|
|
1208
|
+
flex-direction: column;
|
|
1209
|
+
gap: 2px;
|
|
1091
1210
|
border-bottom: 1px solid ${DSH_CSS_VARS.borderL1};
|
|
1092
|
-
|
|
1093
|
-
font-weight: 600;
|
|
1094
|
-
position: sticky;
|
|
1095
|
-
top: 0;
|
|
1096
|
-
background: ${DSH_CSS_VARS.bgModule};
|
|
1211
|
+
padding-bottom: 12px;
|
|
1097
1212
|
}
|
|
1098
|
-
.dsh-reminder-
|
|
1099
|
-
|
|
1100
|
-
|
|
1213
|
+
.dsh-reminder-panel-header strong { font-size: 15px; font-weight: 600; }
|
|
1214
|
+
.dsh-reminder-panel-sub { color: ${DSH_CSS_VARS.labelTertiary}; font-size: 12px; }
|
|
1215
|
+
|
|
1216
|
+
.dsh-reminder-panel-section {
|
|
1217
|
+
display: flex;
|
|
1218
|
+
flex-direction: column;
|
|
1219
|
+
gap: 10px;
|
|
1220
|
+
}
|
|
1221
|
+
.dsh-reminder-panel-section-title {
|
|
1222
|
+
font-size: 12px;
|
|
1101
1223
|
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
line-height: 1;
|
|
1105
|
-
padding: 2px 6px;
|
|
1106
|
-
border-radius: 4px;
|
|
1224
|
+
text-transform: uppercase;
|
|
1225
|
+
letter-spacing: 0.04em;
|
|
1107
1226
|
}
|
|
1108
|
-
.dsh-reminder-close:hover { background: ${DSH_CSS_VARS.borderL1}; color: ${DSH_CSS_VARS.labelPrimary}; }
|
|
1109
|
-
|
|
1110
|
-
.dsh-reminder-panel-body { padding: 12px 14px 16px; display: flex; flex-direction: column; gap: 14px; }
|
|
1111
1227
|
|
|
1112
|
-
.dsh-reminder-
|
|
1113
|
-
border: 1px solid ${DSH_CSS_VARS.borderL1};
|
|
1114
|
-
border-radius: 8px;
|
|
1115
|
-
padding: 8px 10px 10px;
|
|
1116
|
-
margin: 0;
|
|
1228
|
+
.dsh-reminder-panel-field {
|
|
1117
1229
|
display: flex;
|
|
1118
1230
|
flex-direction: column;
|
|
1119
|
-
gap:
|
|
1231
|
+
gap: 4px;
|
|
1120
1232
|
}
|
|
1121
|
-
.dsh-reminder-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
.dsh-reminder-field input,
|
|
1126
|
-
.dsh-reminder-field
|
|
1233
|
+
.dsh-reminder-panel-label {
|
|
1234
|
+
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1235
|
+
font-size: 12px;
|
|
1236
|
+
}
|
|
1237
|
+
.dsh-reminder-panel-field input[type="text"],
|
|
1238
|
+
.dsh-reminder-panel-field input[type="number"],
|
|
1239
|
+
.dsh-reminder-panel-field select {
|
|
1240
|
+
color-scheme: light dark;
|
|
1241
|
+
width: 100%;
|
|
1127
1242
|
background: transparent;
|
|
1128
1243
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1129
1244
|
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1130
1245
|
border-radius: 6px;
|
|
1131
|
-
padding: 6px
|
|
1246
|
+
padding: 6px 10px;
|
|
1132
1247
|
font-size: 13px;
|
|
1133
|
-
outline: none;
|
|
1134
1248
|
font-family: inherit;
|
|
1249
|
+
outline: none;
|
|
1135
1250
|
transition: border-color .15s, box-shadow .15s;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1251
|
+
-webkit-appearance: none;
|
|
1252
|
+
appearance: none;
|
|
1253
|
+
}
|
|
1254
|
+
.dsh-reminder-panel-field select {
|
|
1255
|
+
/* Provide a small caret since we stripped the native chrome. */
|
|
1256
|
+
background-image: linear-gradient(45deg, transparent 50%, ${DSH_CSS_VARS.labelSecondary} 50%),
|
|
1257
|
+
linear-gradient(135deg, ${DSH_CSS_VARS.labelSecondary} 50%, transparent 50%);
|
|
1258
|
+
background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
|
|
1259
|
+
background-size: 5px 5px, 5px 5px;
|
|
1260
|
+
background-repeat: no-repeat;
|
|
1261
|
+
padding-right: 26px;
|
|
1262
|
+
}
|
|
1263
|
+
.dsh-reminder-panel-field input[type="text"]:focus,
|
|
1264
|
+
.dsh-reminder-panel-field input[type="number"]:focus,
|
|
1265
|
+
.dsh-reminder-panel-field select:focus {
|
|
1139
1266
|
border-color: ${DSH_CSS_VARS.stateBusinessPrimary};
|
|
1140
1267
|
box-shadow: 0 0 0 2px color-mix(in srgb, ${DSH_CSS_VARS.stateBusinessPrimary} 25%, transparent);
|
|
1141
1268
|
}
|
|
1142
1269
|
|
|
1143
|
-
.dsh-reminder-row {
|
|
1144
|
-
|
|
1270
|
+
.dsh-reminder-panel-row {
|
|
1271
|
+
display: flex;
|
|
1272
|
+
align-items: center;
|
|
1273
|
+
gap: 8px;
|
|
1274
|
+
cursor: pointer;
|
|
1275
|
+
user-select: none;
|
|
1276
|
+
}
|
|
1277
|
+
.dsh-reminder-panel-row > input[type="checkbox"] {
|
|
1278
|
+
accent-color: ${DSH_CSS_VARS.buttonInfoFill};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.dsh-reminder-panel-hint {
|
|
1282
|
+
color: ${DSH_CSS_VARS.labelTertiary};
|
|
1283
|
+
font-size: 12px;
|
|
1284
|
+
line-height: 1.5;
|
|
1285
|
+
margin: 0;
|
|
1286
|
+
}
|
|
1145
1287
|
|
|
1146
|
-
.dsh-reminder-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1288
|
+
.dsh-reminder-panel-perm {
|
|
1289
|
+
flex-direction: row;
|
|
1290
|
+
align-items: center;
|
|
1291
|
+
justify-content: space-between;
|
|
1292
|
+
flex-wrap: wrap;
|
|
1293
|
+
gap: 8px;
|
|
1294
|
+
padding: 8px 10px;
|
|
1295
|
+
border: 1px dashed ${DSH_CSS_VARS.borderL2};
|
|
1296
|
+
border-radius: 6px;
|
|
1297
|
+
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1298
|
+
font-size: 12px;
|
|
1299
|
+
}
|
|
1300
|
+
.dsh-reminder-panel-perm strong { color: ${DSH_CSS_VARS.labelPrimary}; }
|
|
1301
|
+
.dsh-reminder-panel-perm button {
|
|
1151
1302
|
background: transparent;
|
|
1152
1303
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1304
|
+
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1153
1305
|
border-radius: 6px;
|
|
1306
|
+
padding: 4px 10px;
|
|
1154
1307
|
cursor: pointer;
|
|
1155
|
-
font-size:
|
|
1308
|
+
font-size: 12px;
|
|
1156
1309
|
font-family: inherit;
|
|
1157
1310
|
}
|
|
1158
|
-
.dsh-reminder-
|
|
1159
|
-
.dsh-reminder-actions button.primary { background: ${DSH_CSS_VARS.buttonInfoFill}; color: #fff; border-color: transparent; }
|
|
1160
|
-
.dsh-reminder-actions button.primary:hover { filter: brightness(1.05); }
|
|
1311
|
+
.dsh-reminder-panel-perm button:hover { background: ${DSH_CSS_VARS.borderL1}; }
|
|
1161
1312
|
|
|
1162
|
-
.dsh-reminder-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
justify-content: space-between;
|
|
1313
|
+
.dsh-reminder-panel-actions {
|
|
1314
|
+
display: flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
gap: 8px;
|
|
1167
1317
|
flex-wrap: wrap;
|
|
1168
1318
|
}
|
|
1169
|
-
.dsh-reminder-
|
|
1319
|
+
.dsh-reminder-panel-actions button {
|
|
1320
|
+
padding: 6px 14px;
|
|
1321
|
+
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1170
1322
|
background: transparent;
|
|
1171
1323
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1172
|
-
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1173
1324
|
border-radius: 6px;
|
|
1174
|
-
padding: 4px 8px;
|
|
1175
1325
|
cursor: pointer;
|
|
1176
|
-
font-size:
|
|
1326
|
+
font-size: 13px;
|
|
1177
1327
|
font-family: inherit;
|
|
1178
1328
|
}
|
|
1179
|
-
.dsh-reminder-
|
|
1329
|
+
.dsh-reminder-panel-actions button:hover { background: ${DSH_CSS_VARS.borderL1}; }
|
|
1330
|
+
.dsh-reminder-panel-actions button.primary {
|
|
1331
|
+
background: ${DSH_CSS_VARS.buttonInfoFill};
|
|
1332
|
+
color: #fff;
|
|
1333
|
+
border-color: transparent;
|
|
1334
|
+
}
|
|
1335
|
+
.dsh-reminder-panel-actions button.primary:hover { filter: brightness(1.05); }
|
|
1180
1336
|
|
|
1181
|
-
.dsh-reminder-
|
|
1337
|
+
.dsh-reminder-panel-status {
|
|
1338
|
+
margin-left: auto;
|
|
1339
|
+
color: ${DSH_CSS_VARS.stateSuccessPrimary};
|
|
1340
|
+
font-size: 12px;
|
|
1341
|
+
min-width: 0;
|
|
1342
|
+
flex: 1 1 0;
|
|
1343
|
+
text-align: right;
|
|
1344
|
+
}
|
|
1182
1345
|
`;
|
|
1183
1346
|
document.head.appendChild(style);
|
|
1184
1347
|
}
|
|
1185
|
-
// ──── DSH client plugin entry ──────────────────────────────────────────────
|
|
1186
|
-
/**
|
|
1187
|
-
* DSH client plugin entry — called by the Cordis Loader.
|
|
1188
|
-
*/
|
|
1189
|
-
function apply(ctx, opts) {
|
|
1190
|
-
configure(opts);
|
|
1191
|
-
activate();
|
|
1192
|
-
}
|
|
1193
1348
|
exports.configure = configure;
|
|
1194
1349
|
exports.activate = activate;
|
|
1195
1350
|
exports.deactivate = deactivate;
|
|
1196
1351
|
exports.apply = apply;
|
|
1197
1352
|
exports.requestBrowserPermission = requestBrowserPermission;
|
|
1198
1353
|
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
1354
|
+
exports.renderPanelInto = renderPanelInto;
|
|
1199
1355
|
//# sourceMappingURL=client.js.map
|
|
1200
1356
|
|
|
1201
1357
|
// ── export fallback (guard against stripped 'export { ... }' lists) ──
|