dsh-completion-reminder 1.0.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 +173 -0
- package/cordis.patch.yml +7 -0
- package/dist/dsh-completion-reminder.js +590 -0
- package/install.md +97 -0
- package/lib/client.d.ts +34 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +779 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +15 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +208 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +72 -0
- package/lib/types.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Completion Reminder — standalone bundle
|
|
3
|
+
*
|
|
4
|
+
* Load this file as a regular <script> tag in the DSH Web GUI page
|
|
5
|
+
* to receive a browser/system notification when the agent finishes.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <script src="dsh-completion-reminder.js"></script>
|
|
9
|
+
* <script>
|
|
10
|
+
* DSHCompletionReminder.configure({
|
|
11
|
+
* provider: 'telegram',
|
|
12
|
+
* providers: { telegramBotToken: '…', telegramChatId: '…' },
|
|
13
|
+
* });
|
|
14
|
+
* DSHCompletionReminder.activate();
|
|
15
|
+
* </script>
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// ── options ────────────────────────────────────────────────────────
|
|
21
|
+
var DEFAULTS = {
|
|
22
|
+
provider: 'browser',
|
|
23
|
+
autoRequestPermission: true,
|
|
24
|
+
notifyOnSuccess: true,
|
|
25
|
+
notifyOnStopped: true,
|
|
26
|
+
notifyOnError: true,
|
|
27
|
+
suppressWhenFocused: true,
|
|
28
|
+
cooldownMs: 5000,
|
|
29
|
+
providers: {},
|
|
30
|
+
clickUrl: '',
|
|
31
|
+
iconUrl: '',
|
|
32
|
+
onNotify: function () { return undefined; },
|
|
33
|
+
onError: function (err) { try { console.warn('[dsh-completion-reminder]', err); } catch (_e) {} },
|
|
34
|
+
titleTemplate: function (ctx) {
|
|
35
|
+
if (ctx.status === 'success') return '✅ DSH Agent 已完成';
|
|
36
|
+
if (ctx.status === 'stopped') return '⏹ DSH Agent 已停止';
|
|
37
|
+
return '⚠️ DSH Agent 出错';
|
|
38
|
+
},
|
|
39
|
+
bodyTemplate: function (ctx) {
|
|
40
|
+
var parts = [];
|
|
41
|
+
if (ctx.agent) parts.push('Agent: ' + ctx.agent);
|
|
42
|
+
if (ctx.model) parts.push('Model: ' + ctx.model);
|
|
43
|
+
if (typeof ctx.durationMs === 'number') {
|
|
44
|
+
if (ctx.durationMs < 1000) parts.push('用时: ' + Math.round(ctx.durationMs) + 'ms');
|
|
45
|
+
else {
|
|
46
|
+
var total = Math.round(ctx.durationMs / 1000);
|
|
47
|
+
if (total >= 3600) parts.push('用时: ' + Math.floor(total / 3600) + 'h ' + Math.floor((total % 3600) / 60) + 'm');
|
|
48
|
+
else if (total >= 60) parts.push('用时: ' + Math.floor(total / 60) + 'm ' + (total % 60) + 's');
|
|
49
|
+
else parts.push('用时: ' + total + 's');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!parts.length) return '代理任务已结束,点击查看详情。';
|
|
53
|
+
return parts.join(' · ');
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
var CSS_VARS = {
|
|
58
|
+
bgModule: 'var(--dsw-alias-bg-module-platform)',
|
|
59
|
+
borderL3: 'var(--dsw-alias-border-l3)',
|
|
60
|
+
labelPrimary: 'var(--dsw-alias-label-primary)',
|
|
61
|
+
labelSecondary: 'var(--dsw-alias-label-secondary)',
|
|
62
|
+
labelTertiary: 'var(--dsw-alias-label-tertiary)',
|
|
63
|
+
shadowLv3: 'var(--dsw-shadow-lv3)',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
var TOOLBAR_BUTTON_SELECTORS = [
|
|
67
|
+
'button[aria-label*="send" i]',
|
|
68
|
+
'button[aria-label*="stop" i]',
|
|
69
|
+
'button[aria-label*="停止" i]',
|
|
70
|
+
'button[aria-label*="发送" i]',
|
|
71
|
+
'button[aria-label*="取消" i]',
|
|
72
|
+
'button[aria-label*="中止" i]',
|
|
73
|
+
'button[aria-label*="abort" i]',
|
|
74
|
+
'button[aria-label*="cancel" i]',
|
|
75
|
+
'form button[type="submit"]',
|
|
76
|
+
'textarea + * button',
|
|
77
|
+
'textarea ~ button',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
var RUNNING_TOKENS = ['stop', 'stop generating', '停止', '中止', '取消生成', 'abort', 'cancel', 'pause', '暂停', 'interrupt'];
|
|
81
|
+
var SUCCESS_TOKENS = ['send', '发送', 'submit', '提交'];
|
|
82
|
+
|
|
83
|
+
var CONVERSATION_SELECTORS = [
|
|
84
|
+
'[data-conversation-id]',
|
|
85
|
+
'[data-conversation]',
|
|
86
|
+
'main [class*="conversation"]',
|
|
87
|
+
'main [class*="chat"]',
|
|
88
|
+
'main [class*="message"]',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
var opts = Object.assign({}, DEFAULTS);
|
|
92
|
+
var state = {
|
|
93
|
+
observer: null,
|
|
94
|
+
runStartedAt: null,
|
|
95
|
+
lastModel: null,
|
|
96
|
+
lastAgent: null,
|
|
97
|
+
inFlight: false,
|
|
98
|
+
lastNotifiedAt: 0,
|
|
99
|
+
isActive: false,
|
|
100
|
+
permission: detectPermission(),
|
|
101
|
+
unbinder: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function detectPermission() {
|
|
105
|
+
if (typeof window === 'undefined' || !('Notification' in window)) return 'unsupported';
|
|
106
|
+
return Notification.permission;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toError(value) {
|
|
110
|
+
if (value instanceof Error) return value;
|
|
111
|
+
return new Error(typeof value === 'string' ? value : JSON.stringify(value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeText(res) {
|
|
115
|
+
return res.text().then(function (t) { return t.slice(0, 500); }).catch(function () { return ''; });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function postJson(url, body) {
|
|
119
|
+
return fetch(url, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
}).then(function (res) {
|
|
124
|
+
if (!res.ok) return safeText(res).then(function (t) {
|
|
125
|
+
throw new Error('POST ' + url + ' → ' + res.status + ': ' + (t || res.statusText));
|
|
126
|
+
});
|
|
127
|
+
return res;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function postForm(url, fields) {
|
|
132
|
+
var form = new URLSearchParams();
|
|
133
|
+
for (var k in fields) if (Object.prototype.hasOwnProperty.call(fields, k)) form.set(k, fields[k]);
|
|
134
|
+
return fetch(url, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
137
|
+
body: form.toString(),
|
|
138
|
+
}).then(function (res) {
|
|
139
|
+
if (!res.ok) return safeText(res).then(function (t) {
|
|
140
|
+
throw new Error('POST ' + url + ' → ' + res.status + ': ' + (t || res.statusText));
|
|
141
|
+
});
|
|
142
|
+
return res;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function escapeMd(s) {
|
|
147
|
+
return s.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, function (m) { return '\\' + m; });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isInsideToolbar(el) {
|
|
151
|
+
return !!el.closest('form, [class*="composer" i], [class*="toolbar" i], [class*="input" i], [class*="chat-input" i]');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function matchesAny(btn, tokens) {
|
|
155
|
+
var text = (btn.textContent || '').trim().toLowerCase();
|
|
156
|
+
var aria = (btn.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
157
|
+
var title = (btn.getAttribute('title') || '').trim().toLowerCase();
|
|
158
|
+
var cls = (btn.getAttribute('class') || '').toLowerCase();
|
|
159
|
+
var hay = text + ' ' + aria + ' ' + title + ' ' + cls;
|
|
160
|
+
return tokens.some(function (tok) { return hay.indexOf(tok) !== -1; });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectToolbarButtons() {
|
|
164
|
+
var out = [];
|
|
165
|
+
var forms = document.querySelectorAll('form, [class*="composer" i], [class*="toolbar" i]');
|
|
166
|
+
forms.forEach(function (f) {
|
|
167
|
+
f.querySelectorAll('button').forEach(function (b) {
|
|
168
|
+
if (b.offsetParent !== null || b.getClientRects().length) out.push(b);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
TOOLBAR_BUTTON_SELECTORS.forEach(function (sel) {
|
|
172
|
+
document.querySelectorAll(sel).forEach(function (b) {
|
|
173
|
+
if ((b.offsetParent !== null || b.getClientRects().length) && out.indexOf(b) === -1) {
|
|
174
|
+
out.push(b);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findLastAssistantMessage() {
|
|
182
|
+
for (var i = 0; i < CONVERSATION_SELECTORS.length; i++) {
|
|
183
|
+
var all = document.querySelectorAll(CONVERSATION_SELECTORS[i]);
|
|
184
|
+
for (var j = all.length - 1; j >= 0; j--) {
|
|
185
|
+
var el = all[j];
|
|
186
|
+
var role = (el.getAttribute('data-role') || el.getAttribute('data-author') || '').toLowerCase();
|
|
187
|
+
if (role.indexOf('assistant') !== -1 || role.indexOf('agent') !== -1 || role.indexOf('model') !== -1) {
|
|
188
|
+
return el;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
var main = document.querySelector('main');
|
|
193
|
+
var last = main && main.lastElementChild;
|
|
194
|
+
return last || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function determineStatus() {
|
|
198
|
+
var last = findLastAssistantMessage();
|
|
199
|
+
if (!last) return 'success';
|
|
200
|
+
var text = (last.textContent || '').toLowerCase();
|
|
201
|
+
if (/(error|exception|failed|traceback|错误|失败|异常)/.test(text) &&
|
|
202
|
+
!/no error|没有错误|successfully|成功/.test(text)) {
|
|
203
|
+
return 'error';
|
|
204
|
+
}
|
|
205
|
+
if (/(stopped by user|user stopped|手动停止|已停止|已取消)/.test(text)) return 'stopped';
|
|
206
|
+
return 'success';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readModelFromHeader() {
|
|
210
|
+
var candidates = document.querySelectorAll('[data-model], [data-testid*="model" i], [class*="model" i]');
|
|
211
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
212
|
+
var t = (candidates[i].textContent || '').trim();
|
|
213
|
+
if (t && t.length < 80) return t;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readAgentFromHeader() {
|
|
219
|
+
var candidates = document.querySelectorAll('[data-agent], [data-testid*="agent" i]');
|
|
220
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
221
|
+
var t = (candidates[i].textContent || '').trim();
|
|
222
|
+
if (t && t.length < 80) return t;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function captureRunMetadata() {
|
|
228
|
+
var m = readModelFromHeader();
|
|
229
|
+
if (m) state.lastModel = m;
|
|
230
|
+
var a = readAgentFromHeader();
|
|
231
|
+
if (a) state.lastAgent = a;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function pageIsFocused() {
|
|
235
|
+
if (typeof document === 'undefined') return true;
|
|
236
|
+
if (document.visibilityState === 'hidden') return false;
|
|
237
|
+
if (document.hasFocus && !document.hasFocus()) return false;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function evaluateToolbar() {
|
|
242
|
+
var buttons = collectToolbarButtons();
|
|
243
|
+
if (!buttons.length) return;
|
|
244
|
+
var running = buttons.find(function (b) { return matchesAny(b, RUNNING_TOKENS); });
|
|
245
|
+
captureRunMetadata();
|
|
246
|
+
if (running && !state.inFlight) {
|
|
247
|
+
state.inFlight = true;
|
|
248
|
+
state.runStartedAt = Date.now();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!running && state.inFlight) {
|
|
252
|
+
state.inFlight = false;
|
|
253
|
+
var startedAt = state.runStartedAt || Date.now();
|
|
254
|
+
var durationMs = Date.now() - startedAt;
|
|
255
|
+
state.runStartedAt = null;
|
|
256
|
+
completeRun(determineStatus(), durationMs);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handleMutations(mutations) {
|
|
261
|
+
if (!state.isActive) return;
|
|
262
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
263
|
+
var m = mutations[i];
|
|
264
|
+
if (m.type === 'attributes' || m.type === 'characterData') {
|
|
265
|
+
var target = m.target;
|
|
266
|
+
if (target && isInsideToolbar(target)) evaluateToolbar();
|
|
267
|
+
}
|
|
268
|
+
m.addedNodes.forEach(function (node) {
|
|
269
|
+
if (!(node instanceof HTMLElement)) return;
|
|
270
|
+
var selList = TOOLBAR_BUTTON_SELECTORS.join(',');
|
|
271
|
+
if (node.matches && node.matches(selList)) {
|
|
272
|
+
evaluateToolbar();
|
|
273
|
+
} else if (node.querySelector && node.querySelector(selList)) {
|
|
274
|
+
evaluateToolbar();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function startObserver() {
|
|
281
|
+
if (state.observer) state.observer.disconnect();
|
|
282
|
+
state.observer = new MutationObserver(handleMutations);
|
|
283
|
+
state.observer.observe(document.body, {
|
|
284
|
+
childList: true,
|
|
285
|
+
subtree: true,
|
|
286
|
+
characterData: true,
|
|
287
|
+
attributes: true,
|
|
288
|
+
attributeFilter: ['aria-label', 'aria-pressed', 'data-state', 'data-status', 'class', 'disabled'],
|
|
289
|
+
});
|
|
290
|
+
evaluateToolbar();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function stopObserver() {
|
|
294
|
+
if (state.observer) { state.observer.disconnect(); state.observer = null; }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function bindVisibilityEvents() {
|
|
298
|
+
if (typeof document === 'undefined') return;
|
|
299
|
+
var onVis = function () {};
|
|
300
|
+
document.addEventListener('visibilitychange', onVis);
|
|
301
|
+
state.unbinder.push(function () { document.removeEventListener('visibilitychange', onVis); });
|
|
302
|
+
var onFocus = function () {};
|
|
303
|
+
window.addEventListener('focus', onFocus);
|
|
304
|
+
state.unbinder.push(function () { window.removeEventListener('focus', onFocus); });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function requestBrowserPermission() {
|
|
308
|
+
if (state.permission === 'unsupported') return Promise.resolve('unsupported');
|
|
309
|
+
if (Notification.permission !== 'default') {
|
|
310
|
+
state.permission = Notification.permission;
|
|
311
|
+
return Promise.resolve(Notification.permission);
|
|
312
|
+
}
|
|
313
|
+
return Notification.requestPermission().then(function (result) {
|
|
314
|
+
state.permission = result;
|
|
315
|
+
return result;
|
|
316
|
+
}).catch(function (err) {
|
|
317
|
+
opts.onError(toError(err), 'browser');
|
|
318
|
+
return state.permission;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function shouldNotify(status) {
|
|
323
|
+
if (status === 'success') return opts.notifyOnSuccess;
|
|
324
|
+
if (status === 'stopped') return opts.notifyOnStopped;
|
|
325
|
+
if (status === 'error') return opts.notifyOnError;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function showInPageToast(payload, hint) {
|
|
330
|
+
if (typeof document === 'undefined') return;
|
|
331
|
+
var root = ensureToastRoot();
|
|
332
|
+
var card = document.createElement('div');
|
|
333
|
+
card.className = 'dsh-reminder-toast';
|
|
334
|
+
card.setAttribute('role', 'status');
|
|
335
|
+
card.innerHTML = '<div class="dsh-reminder-toast-title"></div><div class="dsh-reminder-toast-body"></div>' + (hint ? '<div class="dsh-reminder-toast-hint"></div>' : '');
|
|
336
|
+
card.querySelector('.dsh-reminder-toast-title').textContent = payload.title;
|
|
337
|
+
card.querySelector('.dsh-reminder-toast-body').textContent = payload.body;
|
|
338
|
+
if (hint) card.querySelector('.dsh-reminder-toast-hint').textContent = hint;
|
|
339
|
+
card.addEventListener('click', function () {
|
|
340
|
+
if (payload.url) { try { window.open(payload.url, '_self'); } catch (_e) {} }
|
|
341
|
+
card.remove();
|
|
342
|
+
});
|
|
343
|
+
root.appendChild(card);
|
|
344
|
+
setTimeout(function () { card.classList.add('dsh-reminder-toast-leave'); }, 4500);
|
|
345
|
+
setTimeout(function () { card.remove(); }, 5200);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
var toastRoot = null;
|
|
349
|
+
function ensureToastRoot() {
|
|
350
|
+
if (toastRoot && document.body.contains(toastRoot)) return toastRoot;
|
|
351
|
+
if (document.getElementById('dsh-completion-reminder-toast-style')) {
|
|
352
|
+
// styles already injected
|
|
353
|
+
} else {
|
|
354
|
+
var style = document.createElement('style');
|
|
355
|
+
style.id = 'dsh-completion-reminder-toast-style';
|
|
356
|
+
style.textContent =
|
|
357
|
+
'.dsh-reminder-toast-root{position:fixed;top:20px;right:20px;z-index:2147483647;display:flex;flex-direction:column;gap:8px;pointer-events:none;max-width:360px;}' +
|
|
358
|
+
'.dsh-reminder-toast{pointer-events:auto;background:' + CSS_VARS.bgModule + ';color:' + CSS_VARS.labelPrimary + ';border:1px solid ' + CSS_VARS.borderL3 + ';border-radius:10px;box-shadow:' + CSS_VARS.shadowLv3 + ';padding:12px 14px;font-size:13px;line-height:1.4;cursor:pointer;transition:opacity .25s ease,transform .25s ease;}' +
|
|
359
|
+
'.dsh-reminder-toast-title{font-weight:600;margin-bottom:4px;}' +
|
|
360
|
+
'.dsh-reminder-toast-body{color:' + CSS_VARS.labelSecondary + ';white-space:pre-wrap;word-break:break-word;}' +
|
|
361
|
+
'.dsh-reminder-toast-hint{color:' + CSS_VARS.labelTertiary + ';font-size:12px;margin-top:6px;}' +
|
|
362
|
+
'.dsh-reminder-toast-leave{opacity:0;transform:translateY(-4px);}';
|
|
363
|
+
document.head.appendChild(style);
|
|
364
|
+
}
|
|
365
|
+
var root = document.createElement('div');
|
|
366
|
+
root.id = 'dsh-completion-reminder-toasts';
|
|
367
|
+
root.className = 'dsh-reminder-toast-root';
|
|
368
|
+
document.body.appendChild(root);
|
|
369
|
+
toastRoot = root;
|
|
370
|
+
return root;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function deliverBrowser(payload) {
|
|
374
|
+
return Promise.resolve().then(function () {
|
|
375
|
+
if (state.permission === 'unsupported') {
|
|
376
|
+
showInPageToast(payload);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (state.permission !== 'granted') {
|
|
380
|
+
if (opts.autoRequestPermission) {
|
|
381
|
+
return requestBrowserPermission().then(function (next) {
|
|
382
|
+
if (next !== 'granted') {
|
|
383
|
+
showInPageToast(payload, '未授予通知权限,已改为页面内提示。');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
fire();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
showInPageToast(payload, '未授予通知权限,已改为页面内提示。');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
fire();
|
|
393
|
+
|
|
394
|
+
function fire() {
|
|
395
|
+
try {
|
|
396
|
+
var n = new Notification(payload.title, {
|
|
397
|
+
body: payload.body,
|
|
398
|
+
icon: payload.iconUrl,
|
|
399
|
+
tag: 'dsh-completion-reminder',
|
|
400
|
+
requireInteraction: false,
|
|
401
|
+
});
|
|
402
|
+
n.onclick = function () {
|
|
403
|
+
try {
|
|
404
|
+
if (typeof window !== 'undefined' && payload.url) {
|
|
405
|
+
window.focus();
|
|
406
|
+
window.open(payload.url, '_self');
|
|
407
|
+
}
|
|
408
|
+
} catch (_e) {}
|
|
409
|
+
n.close();
|
|
410
|
+
};
|
|
411
|
+
} catch (err) {
|
|
412
|
+
showInPageToast(payload, '系统通知失败,已改为页面内提示。');
|
|
413
|
+
opts.onError(toError(err), 'browser');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function deliverTelegram(payload) {
|
|
420
|
+
var p = opts.providers || {};
|
|
421
|
+
if (!p.telegramBotToken || !p.telegramChatId) {
|
|
422
|
+
return Promise.reject(new Error('Telegram provider requires telegramBotToken and telegramChatId'));
|
|
423
|
+
}
|
|
424
|
+
var url = 'https://api.telegram.org/bot' + encodeURIComponent(p.telegramBotToken) + '/sendMessage';
|
|
425
|
+
return postJson(url, {
|
|
426
|
+
chat_id: p.telegramChatId,
|
|
427
|
+
text: '*' + escapeMd(payload.title) + '*\n' + escapeMd(payload.body),
|
|
428
|
+
parse_mode: 'Markdown',
|
|
429
|
+
disable_web_page_preview: true,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function deliverBark(payload) {
|
|
434
|
+
var p = opts.providers || {};
|
|
435
|
+
if (!p.barkKey) return Promise.reject(new Error('Bark provider requires barkKey'));
|
|
436
|
+
var server = (p.barkServer || 'https://api.day.app').replace(/\/$/, '');
|
|
437
|
+
var url = server + '/' + encodeURIComponent(p.barkKey) + '/' +
|
|
438
|
+
encodeURIComponent(payload.title) + '/' + encodeURIComponent(payload.body) +
|
|
439
|
+
(payload.url ? '?url=' + encodeURIComponent(payload.url) : '') +
|
|
440
|
+
(payload.iconUrl ? (payload.url ? '&' : '?') + 'icon=' + encodeURIComponent(payload.iconUrl) : '');
|
|
441
|
+
return fetch(url, { method: 'GET' }).then(function (res) {
|
|
442
|
+
if (!res.ok) return safeText(res).then(function (t) {
|
|
443
|
+
throw new Error('Bark ' + res.status + ': ' + (t || res.statusText));
|
|
444
|
+
});
|
|
445
|
+
return res;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function deliverPushover(payload) {
|
|
450
|
+
var p = opts.providers || {};
|
|
451
|
+
if (!p.pushoverToken || !p.pushoverUserKey) {
|
|
452
|
+
return Promise.reject(new Error('Pushover provider requires pushoverToken and pushoverUserKey'));
|
|
453
|
+
}
|
|
454
|
+
var fields = {
|
|
455
|
+
token: p.pushoverToken,
|
|
456
|
+
user: p.pushoverUserKey,
|
|
457
|
+
title: payload.title,
|
|
458
|
+
message: payload.body,
|
|
459
|
+
url: payload.url,
|
|
460
|
+
url_title: 'Open DSH',
|
|
461
|
+
};
|
|
462
|
+
if (p.pushoverDevice) fields.device = p.pushoverDevice;
|
|
463
|
+
return postForm('https://api.pushover.net/1/messages.json', fields);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function deliverServerChan(payload) {
|
|
467
|
+
var p = opts.providers || {};
|
|
468
|
+
if (!p.serverchanSendKey) return Promise.reject(new Error('Server酱 provider requires serverchanSendKey'));
|
|
469
|
+
return postForm('https://sctapi.ftqq.com/' + encodeURIComponent(p.serverchanSendKey) + '.send', {
|
|
470
|
+
title: payload.title,
|
|
471
|
+
desp: payload.body + (payload.url ? '\n\n[打开 DSH](' + payload.url + ')' : ''),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function deliverDiscord(payload) {
|
|
476
|
+
var p = opts.providers || {};
|
|
477
|
+
if (!p.discordWebhookUrl) return Promise.reject(new Error('Discord provider requires discordWebhookUrl'));
|
|
478
|
+
return postJson(p.discordWebhookUrl, {
|
|
479
|
+
content: '**' + payload.title + '**\n' + payload.body + (payload.url ? '\n' + payload.url : ''),
|
|
480
|
+
username: 'DSH Reminder',
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function deliverSlack(payload) {
|
|
485
|
+
var p = opts.providers || {};
|
|
486
|
+
if (!p.slackWebhookUrl) return Promise.reject(new Error('Slack provider requires slackWebhookUrl'));
|
|
487
|
+
return postJson(p.slackWebhookUrl, {
|
|
488
|
+
text: '*' + payload.title + '*\n' + payload.body + (payload.url ? '\n<' + payload.url + '|Open DSH>' : ''),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function deliverWebhook(payload) {
|
|
493
|
+
var p = opts.providers || {};
|
|
494
|
+
if (!p.webhookUrl) return Promise.reject(new Error('Webhook provider requires webhookUrl'));
|
|
495
|
+
var body = p.webhookPayload ? p.webhookPayload(payload) : payload;
|
|
496
|
+
return postJson(p.webhookUrl, body);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function deliverCustom(payload) {
|
|
500
|
+
var p = opts.providers || {};
|
|
501
|
+
if (!p.customSend) return Promise.reject(new Error('Custom provider requires providers.customSend'));
|
|
502
|
+
return Promise.resolve(p.customSend(payload));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function dispatch(payload) {
|
|
506
|
+
var provider = opts.provider;
|
|
507
|
+
if (provider === 'browser') return deliverBrowser(payload);
|
|
508
|
+
if (provider === 'telegram') return deliverTelegram(payload);
|
|
509
|
+
if (provider === 'bark') return deliverBark(payload);
|
|
510
|
+
if (provider === 'pushover') return deliverPushover(payload);
|
|
511
|
+
if (provider === 'serverchan') return deliverServerChan(payload);
|
|
512
|
+
if (provider === 'discord') return deliverDiscord(payload);
|
|
513
|
+
if (provider === 'slack') return deliverSlack(payload);
|
|
514
|
+
if (provider === 'webhook') return deliverWebhook(payload);
|
|
515
|
+
if (provider === 'custom') return deliverCustom(payload);
|
|
516
|
+
return Promise.reject(new Error('Unknown provider: ' + provider));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function completeRun(status, durationMs) {
|
|
520
|
+
if (!shouldNotify(status)) return Promise.resolve();
|
|
521
|
+
if (opts.suppressWhenFocused && pageIsFocused()) return Promise.resolve();
|
|
522
|
+
var now = Date.now();
|
|
523
|
+
if (now - state.lastNotifiedAt < opts.cooldownMs) return Promise.resolve();
|
|
524
|
+
state.lastNotifiedAt = now;
|
|
525
|
+
var ctx = {
|
|
526
|
+
status: status,
|
|
527
|
+
model: state.lastModel || undefined,
|
|
528
|
+
agent: state.lastAgent || undefined,
|
|
529
|
+
durationMs: durationMs,
|
|
530
|
+
completedAt: new Date().toISOString(),
|
|
531
|
+
url: opts.clickUrl || (typeof location !== 'undefined' ? location.href : ''),
|
|
532
|
+
};
|
|
533
|
+
var payload = {
|
|
534
|
+
title: opts.titleTemplate(ctx),
|
|
535
|
+
body: opts.bodyTemplate(ctx),
|
|
536
|
+
url: ctx.url,
|
|
537
|
+
iconUrl: opts.iconUrl || undefined,
|
|
538
|
+
status: status,
|
|
539
|
+
model: ctx.model,
|
|
540
|
+
agent: ctx.agent,
|
|
541
|
+
durationMs: durationMs,
|
|
542
|
+
completedAt: ctx.completedAt,
|
|
543
|
+
};
|
|
544
|
+
return dispatch(payload).then(function () {
|
|
545
|
+
opts.onNotify(payload, opts.provider);
|
|
546
|
+
}).catch(function (err) {
|
|
547
|
+
opts.onError(toError(err), opts.provider);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
var api = {
|
|
552
|
+
DEFAULTS: DEFAULTS,
|
|
553
|
+
configure: function (options) {
|
|
554
|
+
if (options) {
|
|
555
|
+
for (var k in options) {
|
|
556
|
+
if (Object.prototype.hasOwnProperty.call(options, k) && Object.prototype.hasOwnProperty.call(DEFAULTS, k)) {
|
|
557
|
+
opts[k] = options[k];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (options.providers) opts.providers = Object.assign({}, DEFAULTS.providers, options.providers);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
activate: function () {
|
|
564
|
+
if (state.isActive) return;
|
|
565
|
+
state.isActive = true;
|
|
566
|
+
if (opts.provider === 'browser' && opts.autoRequestPermission && state.permission === 'default') {
|
|
567
|
+
requestBrowserPermission();
|
|
568
|
+
}
|
|
569
|
+
startObserver();
|
|
570
|
+
bindVisibilityEvents();
|
|
571
|
+
},
|
|
572
|
+
deactivate: function () {
|
|
573
|
+
state.isActive = false;
|
|
574
|
+
stopObserver();
|
|
575
|
+
for (var i = 0; i < state.unbinder.length; i++) {
|
|
576
|
+
try { state.unbinder[i](); } catch (_e) {}
|
|
577
|
+
}
|
|
578
|
+
state.unbinder = [];
|
|
579
|
+
state.runStartedAt = null;
|
|
580
|
+
state.inFlight = false;
|
|
581
|
+
},
|
|
582
|
+
requestBrowserPermission: requestBrowserPermission,
|
|
583
|
+
apply: function (ctx, options) {
|
|
584
|
+
if (options) api.configure(options);
|
|
585
|
+
api.activate();
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
window.DSHCompletionReminder = api;
|
|
590
|
+
})();
|
package/install.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# DSH 完成提醒插件 - 安装指南
|
|
2
|
+
|
|
3
|
+
## 前提条件
|
|
4
|
+
|
|
5
|
+
- DeepSeek Harness (DSH) 已安装
|
|
6
|
+
- 浏览器允许 `http://127.0.0.1:3080` 发送通知(首次使用时会弹权限请求)
|
|
7
|
+
|
|
8
|
+
## 安装(npm,推荐)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
dsh plugin --profile web add dsh-completion-reminder
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> 如果报 `ERR_PNPM_ADDING_TO_ROOT`,说明 profile 目录是 pnpm workspace root,补一个 `-w`:
|
|
15
|
+
>
|
|
16
|
+
> ```bash
|
|
17
|
+
> dsh plugin --profile web add -w dsh-completion-reminder
|
|
18
|
+
> ```
|
|
19
|
+
|
|
20
|
+
插件包内自带 `cordis.patch.yml` 并声明 `dsh.bundle.patch`,
|
|
21
|
+
`dsh plugin add` 时 DSH 会**自动**把插件加入 profile 的 bundles 层——无需手动编辑任何配置文件。
|
|
22
|
+
|
|
23
|
+
然后重启:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh web
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
启动后,浏览器地址栏左侧应该会出现「🔔 想显示通知吗?」的权限询问。
|
|
30
|
+
点击「允许」即可启用浏览器原生通知。
|
|
31
|
+
|
|
32
|
+
### 升级
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dsh plugin --profile web update dsh-completion-reminder
|
|
36
|
+
dsh web # 重启生效
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 验证安装
|
|
40
|
+
|
|
41
|
+
1. 打开或强制刷新 DSH Web GUI(http://127.0.0.1:3080,Ctrl+Shift+R)
|
|
42
|
+
2. 给 agent 发任意一条消息
|
|
43
|
+
3. 等任务结束 → 系统右下角(或顶部)出现通知:「✅ DSH Agent 已完成 · 用时 Xs」
|
|
44
|
+
|
|
45
|
+
如果浏览器拒绝了通知权限,插件会自动改用页面内浮动 toast,**不会**完全失效。
|
|
46
|
+
|
|
47
|
+
## 切换到第三方通知
|
|
48
|
+
|
|
49
|
+
在 DSH 页面打开 DevTools 控制台,运行:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
DSHCompletionReminder.configure({
|
|
53
|
+
provider: 'telegram',
|
|
54
|
+
providers: {
|
|
55
|
+
telegramBotToken: '123456:ABC...',
|
|
56
|
+
telegramChatId: '987654321',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
随后任意一次 agent 完成都会通过 Telegram 推送到你的手机。
|
|
62
|
+
|
|
63
|
+
可用的 provider:
|
|
64
|
+
|
|
65
|
+
- `browser` — 浏览器原生通知(默认)
|
|
66
|
+
- `telegram` — Telegram Bot
|
|
67
|
+
- `bark` — iPhone Bark
|
|
68
|
+
- `pushover` — Pushover
|
|
69
|
+
- `serverchan` — Server酱 (sct.ftqq.com)
|
|
70
|
+
- `discord` — Discord Webhook
|
|
71
|
+
- `slack` — Slack Webhook
|
|
72
|
+
- `webhook` — 通用 JSON POST Webhook
|
|
73
|
+
- `custom` — 自定义 `customSend(payload)` 函数
|
|
74
|
+
|
|
75
|
+
## 故障排除
|
|
76
|
+
|
|
77
|
+
| 问题 | 原因 | 解决方法 |
|
|
78
|
+
|------|------|----------|
|
|
79
|
+
| 安装报 `ERR_PNPM_ADDING_TO_ROOT` | profile 是 pnpm workspace root | 加 `-w` 标志重新执行 |
|
|
80
|
+
| 装了但没有通知 | 未重启 `dsh web` | 重启后强刷页面 |
|
|
81
|
+
| 浏览器没弹权限询问 | 之前已「阻止」该网站 | 浏览器地址栏左侧锁形图标 → 通知 → 允许 |
|
|
82
|
+
| Telegram 报 401/400 | `telegramBotToken` / `telegramChatId` 配错 | 用 `@BotFather` 重新获取 token;用 `getUpdates` 找 chat id |
|
|
83
|
+
| 通知频率太高 | 多个 agent 接连完成 | 调大 `cooldownMs`(默认 5000) |
|
|
84
|
+
| 切回浏览器通知 | 想撤销 Telegram 配置 | `DSHCompletionReminder.configure({ provider: 'browser' })` |
|
|
85
|
+
| 完全停用 | — | `DSHCompletionReminder.deactivate()` |
|
|
86
|
+
|
|
87
|
+
## 卸载
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
dsh plugin --profile web remove dsh-completion-reminder
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
然后重启:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
dsh web
|
|
97
|
+
```
|