dsh-completion-reminder 1.1.0 → 1.2.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 +65 -62
- package/install.md +16 -20
- package/lib/client.d.ts +24 -16
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +460 -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,343 @@ 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
|
+
// Try to register a settings section. If the host doesn't expose
|
|
781
|
+
// `ctx.slots` (older / different host), we silently fall back to the
|
|
782
|
+
// detection-only behaviour and surface a one-time hint.
|
|
783
|
+
try {
|
|
784
|
+
if (ctx && ctx.inject && ctx.slots) {
|
|
785
|
+
const slots = ctx.slots;
|
|
786
|
+
const inject = ctx.inject;
|
|
787
|
+
// The `slots.inject(name, factory)` API registers a slot
|
|
788
|
+
// contributor. The factory returns a teardown function (often
|
|
789
|
+
// `ctx.slots.register(...)`) that adds the entry. We keep the
|
|
790
|
+
// teardown around for symmetry but it is not currently used.
|
|
791
|
+
slots.inject('settings.section', () => {
|
|
792
|
+
// Register the section. The third arg is the React component
|
|
793
|
+
// factory; we provide a component that yields a ref-bound host.
|
|
794
|
+
const sectionFactory = makeSectionComponent((el) => {
|
|
795
|
+
state.panelHostEl = el;
|
|
796
|
+
injectPanelInto(el);
|
|
797
|
+
});
|
|
798
|
+
const off = slots.register({
|
|
799
|
+
name: 'settings.section',
|
|
800
|
+
id: 'reminder',
|
|
801
|
+
order: 50,
|
|
802
|
+
label: () => '🔔 提醒',
|
|
803
|
+
locale: '@dsh-completion-reminder',
|
|
804
|
+
inject: () => ({}),
|
|
805
|
+
}, sectionFactory);
|
|
806
|
+
return off;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
showFirstRunHint();
|
|
811
|
+
}
|
|
778
812
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
813
|
+
catch (err) {
|
|
814
|
+
try {
|
|
815
|
+
console.warn('[dsh-completion-reminder] failed to register settings section:', err);
|
|
816
|
+
}
|
|
817
|
+
catch { /* noop */ }
|
|
818
|
+
showFirstRunHint();
|
|
783
819
|
}
|
|
784
820
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
821
|
+
/**
|
|
822
|
+
* Show a single, dismissable hint that points the user to the DSH
|
|
823
|
+
* settings dialog. We only show it once and only when the section
|
|
824
|
+
* registration failed (older host, no slots service, etc.).
|
|
825
|
+
*/
|
|
826
|
+
function showFirstRunHint() {
|
|
827
|
+
if (state.hintEl || hasPersistedConfig())
|
|
828
|
+
return;
|
|
829
|
+
injectHintStyles();
|
|
830
|
+
const el = document.createElement('div');
|
|
831
|
+
el.className = 'dsh-reminder-hint';
|
|
832
|
+
el.innerHTML = `
|
|
833
|
+
<span>🔔 DSH Completion Reminder 已激活。在「设置 → 🔔 提醒」中配置通知渠道。</span>
|
|
834
|
+
<button type="button" data-reminder-hint-dismiss>知道了</button>
|
|
835
|
+
`;
|
|
836
|
+
el.querySelector('[data-reminder-hint-dismiss]')?.addEventListener('click', () => {
|
|
837
|
+
el.remove();
|
|
838
|
+
state.hintEl = null;
|
|
839
|
+
});
|
|
840
|
+
document.body.appendChild(el);
|
|
841
|
+
state.hintEl = el;
|
|
842
|
+
setTimeout(() => {
|
|
843
|
+
el.classList.add('dsh-reminder-hint-leave');
|
|
844
|
+
setTimeout(() => { el.remove(); state.hintEl = null; }, 600);
|
|
845
|
+
}, 12000);
|
|
846
|
+
}
|
|
847
|
+
function removeHint() {
|
|
848
|
+
if (state.hintEl) {
|
|
849
|
+
state.hintEl.remove();
|
|
850
|
+
state.hintEl = null;
|
|
790
851
|
}
|
|
791
852
|
}
|
|
792
|
-
function
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
853
|
+
function hasPersistedConfig() {
|
|
854
|
+
if (typeof localStorage === 'undefined')
|
|
855
|
+
return false;
|
|
856
|
+
try {
|
|
857
|
+
return !!localStorage.getItem(STORAGE_KEY);
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
797
862
|
}
|
|
798
|
-
|
|
799
|
-
|
|
863
|
+
const HINT_STYLE_ID = 'dsh-completion-reminder-hint-style';
|
|
864
|
+
function injectHintStyles() {
|
|
865
|
+
if (document.getElementById(HINT_STYLE_ID))
|
|
800
866
|
return;
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
867
|
+
const style = document.createElement('style');
|
|
868
|
+
style.id = HINT_STYLE_ID;
|
|
869
|
+
style.textContent = `
|
|
870
|
+
.dsh-reminder-hint {
|
|
871
|
+
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
|
|
872
|
+
z-index: 2147483600;
|
|
873
|
+
display: flex; align-items: center; gap: 10px;
|
|
874
|
+
background: ${DSH_CSS_VARS.bgModule}; color: ${DSH_CSS_VARS.labelPrimary};
|
|
875
|
+
border: 1px solid ${DSH_CSS_VARS.borderL3}; border-radius: 999px;
|
|
876
|
+
box-shadow: ${DSH_CSS_VARS.shadowLv3};
|
|
877
|
+
padding: 8px 8px 8px 14px;
|
|
878
|
+
font-size: 13px; line-height: 1.2;
|
|
879
|
+
transition: opacity .4s, transform .4s;
|
|
880
|
+
}
|
|
881
|
+
.dsh-reminder-hint button {
|
|
882
|
+
background: ${DSH_CSS_VARS.buttonInfoFill}; color: #fff;
|
|
883
|
+
border: none; border-radius: 999px; padding: 4px 10px;
|
|
884
|
+
font-size: 12px; cursor: pointer; font-family: inherit;
|
|
885
|
+
}
|
|
886
|
+
.dsh-reminder-hint-leave { opacity: 0; transform: translate(-50%, 12px); }
|
|
887
|
+
`;
|
|
888
|
+
document.head.appendChild(style);
|
|
889
|
+
}
|
|
890
|
+
// ──── Panel rendering (vanilla DOM inside the DSH settings dialog) ─────────
|
|
891
|
+
/**
|
|
892
|
+
* Provider descriptions shown next to the picker.
|
|
893
|
+
*/
|
|
894
|
+
const PROVIDER_DESCRIPTIONS = {
|
|
895
|
+
browser: '使用浏览器原生通知 API,首次使用需用户授权。',
|
|
896
|
+
telegram: '通过 Telegram Bot 推送到你的聊天 / 频道 / 群组。',
|
|
897
|
+
bark: '通过 Bark HTTP API 推送到 iPhone。',
|
|
898
|
+
pushover: '通过 Pushover 推送到 Android / iOS / 桌面。',
|
|
899
|
+
serverchan: '推送到微信(sct.ftqq.com,SendKey)。',
|
|
900
|
+
discord: '通过 Discord Webhook 推送到频道。',
|
|
901
|
+
slack: '通过 Slack Incoming Webhook 推送到频道。',
|
|
902
|
+
webhook: 'POST JSON 到你提供的 URL。',
|
|
903
|
+
custom: '通过 customSend(payload) 函数自己实现。',
|
|
904
|
+
};
|
|
905
|
+
function injectPanelInto(host) {
|
|
906
|
+
state.panelHostEl = host;
|
|
907
|
+
renderPanelInto(host);
|
|
908
|
+
}
|
|
909
|
+
function rerenderPanel() {
|
|
910
|
+
if (state.panelHostEl) {
|
|
911
|
+
renderPanelInto(state.panelHostEl);
|
|
816
912
|
}
|
|
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>`)
|
|
913
|
+
}
|
|
914
|
+
function renderPanelInto(host) {
|
|
915
|
+
injectPanelStyles();
|
|
916
|
+
host.innerHTML = buildPanelHtml();
|
|
917
|
+
wirePanelEvents(host);
|
|
918
|
+
}
|
|
919
|
+
function buildPanelHtml() {
|
|
920
|
+
const providerOptions = PROVIDER_LABELS
|
|
921
|
+
.map(([id, label]) => `<option value="${id}" ${config.provider === id ? 'selected' : ''}>${escapeHtml(label)}</option>`)
|
|
833
922
|
.join('');
|
|
834
923
|
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;
|
|
924
|
+
const fields = PROVIDER_FIELDS[config.provider] ?? [];
|
|
925
|
+
const fieldsHtml = fields.length === 0
|
|
926
|
+
? `<p class="dsh-reminder-panel-hint">此渠道无需凭证,保存即可使用。</p>`
|
|
927
|
+
: fields.map((f) => {
|
|
928
|
+
const value = p[f.key] ?? '';
|
|
929
|
+
return `
|
|
930
|
+
<label class="dsh-reminder-panel-field">
|
|
931
|
+
<span class="dsh-reminder-panel-label">${escapeHtml(f.label)}</span>
|
|
932
|
+
<input type="text" data-reminder-field="${escapeAttr(f.key)}" value="${escapeAttr(value)}" placeholder="${escapeAttr(f.placeholder)}" autocomplete="off" spellcheck="false" />
|
|
933
|
+
</label>
|
|
934
|
+
`;
|
|
935
|
+
}).join('');
|
|
936
|
+
const desc = PROVIDER_DESCRIPTIONS[config.provider] ?? '';
|
|
856
937
|
return `
|
|
857
|
-
<
|
|
858
|
-
<
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
<
|
|
865
|
-
|
|
938
|
+
<div class="dsh-reminder-panel">
|
|
939
|
+
<header class="dsh-reminder-panel-header">
|
|
940
|
+
<strong>Agent 完成提醒</strong>
|
|
941
|
+
<span class="dsh-reminder-panel-sub">配置通知渠道,agent 完成后通知你</span>
|
|
942
|
+
</header>
|
|
943
|
+
|
|
944
|
+
<section class="dsh-reminder-panel-section">
|
|
945
|
+
<label class="dsh-reminder-panel-field">
|
|
946
|
+
<span class="dsh-reminder-panel-label">通知渠道</span>
|
|
947
|
+
<select data-reminder-input="provider">${providerOptions}</select>
|
|
948
|
+
</label>
|
|
949
|
+
<p class="dsh-reminder-panel-hint">${escapeHtml(desc)}</p>
|
|
950
|
+
</section>
|
|
866
951
|
|
|
867
|
-
<
|
|
868
|
-
<
|
|
869
|
-
${
|
|
870
|
-
|
|
871
|
-
配置只保存在本浏览器的 localStorage,
|
|
872
|
-
<strong>不会</strong> 上传到任何服务器。
|
|
873
|
-
</p>
|
|
874
|
-
</fieldset>
|
|
952
|
+
<section class="dsh-reminder-panel-section">
|
|
953
|
+
<header class="dsh-reminder-panel-section-title">凭证(仅当前渠道需要)</header>
|
|
954
|
+
${fieldsHtml}
|
|
955
|
+
</section>
|
|
875
956
|
|
|
876
|
-
<
|
|
877
|
-
<
|
|
878
|
-
<label class="dsh-reminder-row">
|
|
957
|
+
<section class="dsh-reminder-panel-section">
|
|
958
|
+
<header class="dsh-reminder-panel-section-title">行为</header>
|
|
959
|
+
<label class="dsh-reminder-panel-row">
|
|
879
960
|
<input type="checkbox" data-reminder-input="notifyOnSuccess" ${config.notifyOnSuccess ? 'checked' : ''} />
|
|
880
961
|
<span>成功完成时通知</span>
|
|
881
962
|
</label>
|
|
882
|
-
<label class="dsh-reminder-row">
|
|
963
|
+
<label class="dsh-reminder-panel-row">
|
|
883
964
|
<input type="checkbox" data-reminder-input="notifyOnStopped" ${config.notifyOnStopped ? 'checked' : ''} />
|
|
884
965
|
<span>用户主动停止时通知</span>
|
|
885
966
|
</label>
|
|
886
|
-
<label class="dsh-reminder-row">
|
|
967
|
+
<label class="dsh-reminder-panel-row">
|
|
887
968
|
<input type="checkbox" data-reminder-input="notifyOnError" ${config.notifyOnError ? 'checked' : ''} />
|
|
888
969
|
<span>Agent 出错时通知</span>
|
|
889
970
|
</label>
|
|
890
|
-
<label class="dsh-reminder-row">
|
|
971
|
+
<label class="dsh-reminder-panel-row">
|
|
891
972
|
<input type="checkbox" data-reminder-input="suppressWhenFocused" ${config.suppressWhenFocused ? 'checked' : ''} />
|
|
892
973
|
<span>DSH 标签页可见时静默(推荐)</span>
|
|
893
974
|
</label>
|
|
894
|
-
<label class="dsh-reminder-row">
|
|
975
|
+
<label class="dsh-reminder-panel-row">
|
|
895
976
|
<input type="checkbox" data-reminder-input="autoRequestPermission" ${config.autoRequestPermission ? 'checked' : ''} />
|
|
896
977
|
<span>自动请求浏览器通知权限</span>
|
|
897
978
|
</label>
|
|
898
|
-
<label class="dsh-reminder-field">
|
|
899
|
-
<span>冷却(ms)— 防止连续完成时刷屏</span>
|
|
979
|
+
<label class="dsh-reminder-panel-field">
|
|
980
|
+
<span class="dsh-reminder-panel-label">冷却(ms)— 防止连续完成时刷屏</span>
|
|
900
981
|
<input type="number" min="0" step="500" data-reminder-input="cooldownMs" value="${config.cooldownMs}" />
|
|
901
982
|
</label>
|
|
902
|
-
</
|
|
983
|
+
</section>
|
|
903
984
|
|
|
904
|
-
<
|
|
905
|
-
<span>当前权限:<strong data-reminder-perm>${permissionLabel(
|
|
985
|
+
<section class="dsh-reminder-panel-section dsh-reminder-panel-perm">
|
|
986
|
+
<span>当前权限:<strong data-reminder-perm>${permissionLabel(state.permission)}</strong></span>
|
|
906
987
|
<button type="button" data-reminder-action="request-permission">请求权限</button>
|
|
907
|
-
</
|
|
988
|
+
</section>
|
|
908
989
|
|
|
909
|
-
<
|
|
990
|
+
<footer class="dsh-reminder-panel-actions">
|
|
910
991
|
<button type="button" class="primary" data-reminder-action="test">发送测试通知</button>
|
|
911
|
-
<button type="button" data-reminder-action="save">保存</button>
|
|
912
992
|
<button type="button" data-reminder-action="reset">重置</button>
|
|
913
|
-
|
|
914
|
-
|
|
993
|
+
<span class="dsh-reminder-panel-status" data-reminder-status></span>
|
|
994
|
+
</footer>
|
|
995
|
+
<p class="dsh-reminder-panel-hint">配置只保存在本浏览器的 localStorage,不会上传任何服务器。</p>
|
|
915
996
|
</div>
|
|
916
997
|
`;
|
|
917
998
|
}
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
999
|
+
function wirePanelEvents(host) {
|
|
1000
|
+
const root = host.querySelector('.dsh-reminder-panel');
|
|
1001
|
+
if (!root)
|
|
1002
|
+
return;
|
|
1003
|
+
root.addEventListener('change', (ev) => {
|
|
922
1004
|
const target = ev.target;
|
|
923
1005
|
if (!target)
|
|
924
1006
|
return;
|
|
@@ -926,10 +1008,15 @@ window.__ModuleLoader__.load({
|
|
|
926
1008
|
const name = target.dataset.reminderInput;
|
|
927
1009
|
if (name === 'provider') {
|
|
928
1010
|
config.provider = target.value;
|
|
1011
|
+
savePersisted();
|
|
1012
|
+
// Re-render so the credentials section updates to the new channel.
|
|
1013
|
+
renderPanelInto(host);
|
|
929
1014
|
}
|
|
930
1015
|
}
|
|
931
1016
|
else if (target instanceof HTMLInputElement) {
|
|
932
1017
|
const name = target.dataset.reminderInput;
|
|
1018
|
+
if (!name)
|
|
1019
|
+
return;
|
|
933
1020
|
const value = target.type === 'checkbox' ? target.checked
|
|
934
1021
|
: target.type === 'number' ? Number(target.value) || 0
|
|
935
1022
|
: target.value;
|
|
@@ -945,26 +1032,20 @@ window.__ModuleLoader__.load({
|
|
|
945
1032
|
config.autoRequestPermission = !!value;
|
|
946
1033
|
else if (name === 'cooldownMs')
|
|
947
1034
|
config.cooldownMs = Number(value) || 0;
|
|
1035
|
+
savePersisted();
|
|
948
1036
|
}
|
|
949
|
-
savePersisted();
|
|
950
1037
|
});
|
|
951
|
-
|
|
1038
|
+
root.addEventListener('input', (ev) => {
|
|
952
1039
|
const target = ev.target;
|
|
953
1040
|
if (!target)
|
|
954
1041
|
return;
|
|
955
|
-
const
|
|
956
|
-
if (
|
|
957
|
-
config.providers[
|
|
1042
|
+
const key = target.dataset.reminderField;
|
|
1043
|
+
if (key) {
|
|
1044
|
+
config.providers[key] = target.value;
|
|
958
1045
|
savePersisted();
|
|
959
1046
|
}
|
|
960
1047
|
});
|
|
961
|
-
|
|
962
|
-
?.addEventListener('click', () => {
|
|
963
|
-
savePersisted();
|
|
964
|
-
setStatus(panel, '已保存 ✓');
|
|
965
|
-
});
|
|
966
|
-
panel.querySelector('[data-reminder-action="reset"]')
|
|
967
|
-
?.addEventListener('click', () => {
|
|
1048
|
+
root.querySelector('[data-reminder-action="reset"]')?.addEventListener('click', () => {
|
|
968
1049
|
if (typeof localStorage !== 'undefined') {
|
|
969
1050
|
try {
|
|
970
1051
|
localStorage.removeItem(STORAGE_KEY);
|
|
@@ -972,18 +1053,18 @@ window.__ModuleLoader__.load({
|
|
|
972
1053
|
catch { /* noop */ }
|
|
973
1054
|
}
|
|
974
1055
|
configure();
|
|
975
|
-
|
|
976
|
-
|
|
1056
|
+
renderPanelInto(host);
|
|
1057
|
+
setPanelStatus(root, '已重置 ✓');
|
|
977
1058
|
});
|
|
978
|
-
|
|
1059
|
+
root.querySelector('[data-reminder-action="request-permission"]')
|
|
979
1060
|
?.addEventListener('click', async () => {
|
|
980
1061
|
const result = await requestBrowserPermission();
|
|
981
|
-
const permEl =
|
|
1062
|
+
const permEl = root.querySelector('[data-reminder-perm]');
|
|
982
1063
|
if (permEl)
|
|
983
1064
|
permEl.textContent = permissionLabel(result);
|
|
984
|
-
|
|
1065
|
+
setPanelStatus(root, result === 'granted' ? '权限已授予' : '权限状态:' + permissionLabel(result));
|
|
985
1066
|
});
|
|
986
|
-
|
|
1067
|
+
root.querySelector('[data-reminder-action="test"]')
|
|
987
1068
|
?.addEventListener('click', async () => {
|
|
988
1069
|
try {
|
|
989
1070
|
const ctx = {
|
|
@@ -1002,16 +1083,16 @@ window.__ModuleLoader__.load({
|
|
|
1002
1083
|
};
|
|
1003
1084
|
await dispatch(payload);
|
|
1004
1085
|
config.onNotify(payload, config.provider);
|
|
1005
|
-
|
|
1086
|
+
setPanelStatus(root, '测试通知已发送,请查收。');
|
|
1006
1087
|
}
|
|
1007
1088
|
catch (err) {
|
|
1008
1089
|
const e = toError(err);
|
|
1009
1090
|
config.onError(e, config.provider);
|
|
1010
|
-
|
|
1091
|
+
setPanelStatus(root, '测试失败:' + e.message, true);
|
|
1011
1092
|
}
|
|
1012
1093
|
});
|
|
1013
1094
|
}
|
|
1014
|
-
function
|
|
1095
|
+
function setPanelStatus(panel, text, isError = false) {
|
|
1015
1096
|
const el = panel.querySelector('[data-reminder-status]');
|
|
1016
1097
|
if (!el)
|
|
1017
1098
|
return;
|
|
@@ -1033,169 +1114,187 @@ window.__ModuleLoader__.load({
|
|
|
1033
1114
|
return '不支持';
|
|
1034
1115
|
return '未询问';
|
|
1035
1116
|
}
|
|
1036
|
-
function
|
|
1117
|
+
function escapeHtml(s) {
|
|
1037
1118
|
return s
|
|
1038
1119
|
.replace(/&/g, '&')
|
|
1039
|
-
.replace(/"/g, '"')
|
|
1040
1120
|
.replace(/</g, '<')
|
|
1041
1121
|
.replace(/>/g, '>');
|
|
1042
1122
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1123
|
+
function escapeAttr(s) {
|
|
1124
|
+
return escapeHtml(s).replace(/"/g, '"');
|
|
1125
|
+
}
|
|
1126
|
+
function injectPanelStyles() {
|
|
1127
|
+
if (document.getElementById(PANEL_STYLE_ID))
|
|
1045
1128
|
return;
|
|
1046
1129
|
const style = document.createElement('style');
|
|
1047
|
-
style.id =
|
|
1130
|
+
style.id = PANEL_STYLE_ID;
|
|
1131
|
+
// The host's parent (DSH settings dialog) sets `color-scheme: dark`
|
|
1132
|
+
// for the dark theme, so we explicitly hint the dark UA scheme on
|
|
1133
|
+
// selects + add explicit colors that work in both themes. Native
|
|
1134
|
+
// form controls otherwise default to UA colors (white-on-white in
|
|
1135
|
+
// dark mode).
|
|
1048
1136
|
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
|
-
|
|
1137
|
+
.dsh-reminder-host { color: inherit; }
|
|
1138
|
+
.dsh-reminder-host,
|
|
1139
|
+
.dsh-reminder-host * { box-sizing: border-box; }
|
|
1069
1140
|
.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};
|
|
1141
|
+
display: flex;
|
|
1142
|
+
flex-direction: column;
|
|
1143
|
+
gap: 18px;
|
|
1078
1144
|
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
1145
|
font-size: 13px;
|
|
1083
|
-
line-height: 1.
|
|
1084
|
-
|
|
1146
|
+
line-height: 1.5;
|
|
1147
|
+
padding: 4px 4px 24px;
|
|
1085
1148
|
}
|
|
1086
1149
|
.dsh-reminder-panel-header {
|
|
1087
1150
|
display: flex;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
padding: 10px 12px;
|
|
1151
|
+
flex-direction: column;
|
|
1152
|
+
gap: 2px;
|
|
1091
1153
|
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};
|
|
1154
|
+
padding-bottom: 12px;
|
|
1097
1155
|
}
|
|
1098
|
-
.dsh-reminder-
|
|
1099
|
-
|
|
1100
|
-
|
|
1156
|
+
.dsh-reminder-panel-header strong { font-size: 15px; font-weight: 600; }
|
|
1157
|
+
.dsh-reminder-panel-sub { color: ${DSH_CSS_VARS.labelTertiary}; font-size: 12px; }
|
|
1158
|
+
|
|
1159
|
+
.dsh-reminder-panel-section {
|
|
1160
|
+
display: flex;
|
|
1161
|
+
flex-direction: column;
|
|
1162
|
+
gap: 10px;
|
|
1163
|
+
}
|
|
1164
|
+
.dsh-reminder-panel-section-title {
|
|
1165
|
+
font-size: 12px;
|
|
1101
1166
|
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
line-height: 1;
|
|
1105
|
-
padding: 2px 6px;
|
|
1106
|
-
border-radius: 4px;
|
|
1167
|
+
text-transform: uppercase;
|
|
1168
|
+
letter-spacing: 0.04em;
|
|
1107
1169
|
}
|
|
1108
|
-
.dsh-reminder-close:hover { background: ${DSH_CSS_VARS.borderL1}; color: ${DSH_CSS_VARS.labelPrimary}; }
|
|
1109
1170
|
|
|
1110
|
-
.dsh-reminder-panel-
|
|
1111
|
-
|
|
1112
|
-
.dsh-reminder-group {
|
|
1113
|
-
border: 1px solid ${DSH_CSS_VARS.borderL1};
|
|
1114
|
-
border-radius: 8px;
|
|
1115
|
-
padding: 8px 10px 10px;
|
|
1116
|
-
margin: 0;
|
|
1171
|
+
.dsh-reminder-panel-field {
|
|
1117
1172
|
display: flex;
|
|
1118
1173
|
flex-direction: column;
|
|
1119
|
-
gap:
|
|
1174
|
+
gap: 4px;
|
|
1120
1175
|
}
|
|
1121
|
-
.dsh-reminder-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
.dsh-reminder-field input,
|
|
1126
|
-
.dsh-reminder-field
|
|
1176
|
+
.dsh-reminder-panel-label {
|
|
1177
|
+
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1178
|
+
font-size: 12px;
|
|
1179
|
+
}
|
|
1180
|
+
.dsh-reminder-panel-field input[type="text"],
|
|
1181
|
+
.dsh-reminder-panel-field input[type="number"],
|
|
1182
|
+
.dsh-reminder-panel-field select {
|
|
1183
|
+
color-scheme: light dark;
|
|
1184
|
+
width: 100%;
|
|
1127
1185
|
background: transparent;
|
|
1128
1186
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1129
1187
|
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1130
1188
|
border-radius: 6px;
|
|
1131
|
-
padding: 6px
|
|
1189
|
+
padding: 6px 10px;
|
|
1132
1190
|
font-size: 13px;
|
|
1133
|
-
outline: none;
|
|
1134
1191
|
font-family: inherit;
|
|
1192
|
+
outline: none;
|
|
1135
1193
|
transition: border-color .15s, box-shadow .15s;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1194
|
+
-webkit-appearance: none;
|
|
1195
|
+
appearance: none;
|
|
1196
|
+
}
|
|
1197
|
+
.dsh-reminder-panel-field select {
|
|
1198
|
+
/* Provide a small caret since we stripped the native chrome. */
|
|
1199
|
+
background-image: linear-gradient(45deg, transparent 50%, ${DSH_CSS_VARS.labelSecondary} 50%),
|
|
1200
|
+
linear-gradient(135deg, ${DSH_CSS_VARS.labelSecondary} 50%, transparent 50%);
|
|
1201
|
+
background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
|
|
1202
|
+
background-size: 5px 5px, 5px 5px;
|
|
1203
|
+
background-repeat: no-repeat;
|
|
1204
|
+
padding-right: 26px;
|
|
1205
|
+
}
|
|
1206
|
+
.dsh-reminder-panel-field input[type="text"]:focus,
|
|
1207
|
+
.dsh-reminder-panel-field input[type="number"]:focus,
|
|
1208
|
+
.dsh-reminder-panel-field select:focus {
|
|
1139
1209
|
border-color: ${DSH_CSS_VARS.stateBusinessPrimary};
|
|
1140
1210
|
box-shadow: 0 0 0 2px color-mix(in srgb, ${DSH_CSS_VARS.stateBusinessPrimary} 25%, transparent);
|
|
1141
1211
|
}
|
|
1142
1212
|
|
|
1143
|
-
.dsh-reminder-row {
|
|
1144
|
-
|
|
1213
|
+
.dsh-reminder-panel-row {
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
gap: 8px;
|
|
1217
|
+
cursor: pointer;
|
|
1218
|
+
user-select: none;
|
|
1219
|
+
}
|
|
1220
|
+
.dsh-reminder-panel-row > input[type="checkbox"] {
|
|
1221
|
+
accent-color: ${DSH_CSS_VARS.buttonInfoFill};
|
|
1222
|
+
}
|
|
1145
1223
|
|
|
1146
|
-
.dsh-reminder-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1224
|
+
.dsh-reminder-panel-hint {
|
|
1225
|
+
color: ${DSH_CSS_VARS.labelTertiary};
|
|
1226
|
+
font-size: 12px;
|
|
1227
|
+
line-height: 1.5;
|
|
1228
|
+
margin: 0;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.dsh-reminder-panel-perm {
|
|
1232
|
+
flex-direction: row;
|
|
1233
|
+
align-items: center;
|
|
1234
|
+
justify-content: space-between;
|
|
1235
|
+
flex-wrap: wrap;
|
|
1236
|
+
gap: 8px;
|
|
1237
|
+
padding: 8px 10px;
|
|
1238
|
+
border: 1px dashed ${DSH_CSS_VARS.borderL2};
|
|
1239
|
+
border-radius: 6px;
|
|
1240
|
+
color: ${DSH_CSS_VARS.labelSecondary};
|
|
1241
|
+
font-size: 12px;
|
|
1242
|
+
}
|
|
1243
|
+
.dsh-reminder-panel-perm strong { color: ${DSH_CSS_VARS.labelPrimary}; }
|
|
1244
|
+
.dsh-reminder-panel-perm button {
|
|
1151
1245
|
background: transparent;
|
|
1152
1246
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1247
|
+
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1153
1248
|
border-radius: 6px;
|
|
1249
|
+
padding: 4px 10px;
|
|
1154
1250
|
cursor: pointer;
|
|
1155
|
-
font-size:
|
|
1251
|
+
font-size: 12px;
|
|
1156
1252
|
font-family: inherit;
|
|
1157
1253
|
}
|
|
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); }
|
|
1254
|
+
.dsh-reminder-panel-perm button:hover { background: ${DSH_CSS_VARS.borderL1}; }
|
|
1161
1255
|
|
|
1162
|
-
.dsh-reminder-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
justify-content: space-between;
|
|
1256
|
+
.dsh-reminder-panel-actions {
|
|
1257
|
+
display: flex;
|
|
1258
|
+
align-items: center;
|
|
1259
|
+
gap: 8px;
|
|
1167
1260
|
flex-wrap: wrap;
|
|
1168
1261
|
}
|
|
1169
|
-
.dsh-reminder-
|
|
1262
|
+
.dsh-reminder-panel-actions button {
|
|
1263
|
+
padding: 6px 14px;
|
|
1264
|
+
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1170
1265
|
background: transparent;
|
|
1171
1266
|
color: ${DSH_CSS_VARS.labelPrimary};
|
|
1172
|
-
border: 1px solid ${DSH_CSS_VARS.borderL2};
|
|
1173
1267
|
border-radius: 6px;
|
|
1174
|
-
padding: 4px 8px;
|
|
1175
1268
|
cursor: pointer;
|
|
1176
|
-
font-size:
|
|
1269
|
+
font-size: 13px;
|
|
1177
1270
|
font-family: inherit;
|
|
1178
1271
|
}
|
|
1179
|
-
.dsh-reminder-
|
|
1272
|
+
.dsh-reminder-panel-actions button:hover { background: ${DSH_CSS_VARS.borderL1}; }
|
|
1273
|
+
.dsh-reminder-panel-actions button.primary {
|
|
1274
|
+
background: ${DSH_CSS_VARS.buttonInfoFill};
|
|
1275
|
+
color: #fff;
|
|
1276
|
+
border-color: transparent;
|
|
1277
|
+
}
|
|
1278
|
+
.dsh-reminder-panel-actions button.primary:hover { filter: brightness(1.05); }
|
|
1180
1279
|
|
|
1181
|
-
.dsh-reminder-
|
|
1280
|
+
.dsh-reminder-panel-status {
|
|
1281
|
+
margin-left: auto;
|
|
1282
|
+
color: ${DSH_CSS_VARS.stateSuccessPrimary};
|
|
1283
|
+
font-size: 12px;
|
|
1284
|
+
min-width: 0;
|
|
1285
|
+
flex: 1 1 0;
|
|
1286
|
+
text-align: right;
|
|
1287
|
+
}
|
|
1182
1288
|
`;
|
|
1183
1289
|
document.head.appendChild(style);
|
|
1184
1290
|
}
|
|
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
1291
|
exports.configure = configure;
|
|
1194
1292
|
exports.activate = activate;
|
|
1195
1293
|
exports.deactivate = deactivate;
|
|
1196
1294
|
exports.apply = apply;
|
|
1197
1295
|
exports.requestBrowserPermission = requestBrowserPermission;
|
|
1198
1296
|
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
1297
|
+
exports.renderPanelInto = renderPanelInto;
|
|
1199
1298
|
//# sourceMappingURL=client.js.map
|
|
1200
1299
|
|
|
1201
1300
|
// ── export fallback (guard against stripped 'export { ... }' lists) ──
|