dsh-notify-me 1.1.4 → 1.1.5
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/CHANGELOG.md +16 -0
- package/lib/client.js +224 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ All notable changes to **dsh-notify-me** are documented here.
|
|
|
6
6
|
|
|
7
7
|
### 文档
|
|
8
8
|
- 新增 `docs/listing.md`:记录各插件目录/市场的收录方式(提交物、合并方式),以及本插件当前的收录状态;并记下 `awesome-dsh-plugin` 站点构建失败的排查入口(`build-site.yml` 最近一次运行 / issue #4731)。
|
|
9
|
+
- issue #1 / #2 报告的「等待审批 / 提问 / 方案确认不提醒」查清并修复(见 1.1.5)。
|
|
10
|
+
|
|
11
|
+
## [1.1.5] — 2026-09-14
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
- **「模型在等你操作」这一路提醒在 DSH `0.1.2-alpha.2` 及以后的运行时上完全不触发**(现象:审批卡片出现、模型停下等待时,没有系统通知、没有提示音、标签页标题也不变;而「回复完成」提醒一直正常)。根因是两个待办数据源同时被上游搬走:
|
|
15
|
+
- 选中会话的快照 —— `SessionSnapshot.pending` 已不存在(0.1.2-alpha.2 起只剩 `queue / pendingSubmissions / running / …`);
|
|
16
|
+
- 会话列表行摘要 —— `SessionSummary.pendingInteraction` 已不存在(只剩 `running / completed`)。
|
|
17
|
+
|
|
18
|
+
待办交互改为由 `dsh-client-ui-approval` / `dsh-client-ui-user-questions` 经 `ctx.uiSession.registerPendingInteraction()` 发布到**独立 store** `ctx.uiSession.pendingInteractions`(`Map<sessionId, interaction>`,`interaction.kind` 为 `approval | question | plan-review`)。本版改为订阅该 store:新出现的交互弹一次提醒(`PendingApproval` 取 `toolName` / `reason`,`PendingQuestion` 取 `questions[0].question`),交互消失时释放标签页标记,挂载时已存在的等待也提醒一次。
|
|
19
|
+
- **为什么不把 `uiSession` 写进 `inject`**:客户端条目的 `inject` 是**硬门控**——服务不存在时条目会永远停在 `pending`,而 web 启动断言会因「有条目未激活」直接抛错(这正是 1.1.3/1.1.4 两次踩过的坑)。`uiSession` 只存在于 `0.1.2-alpha.2` 及以后,写进 `inject` 会让本插件在 `0.1.1-rc.2` 上彻底失效。因此改为**惰性查找**:拿得到就订阅交互 store,拿不到就静默走旧的快照路径,两代宿主都保留完整功能。
|
|
20
|
+
- 保留 `@deepseek-ai/dsh-client-ui-session` 在 `dsh.client.inject` 中(它是客户端加载图里的 bundle 条目,与服务门控无关);实测该条目在加载图顺序上位于 `@deepseek-ai/dsh-client-ui-session` 之后(54 个条目中第 44 位 vs 第 10 位),且官方 `dsh-client-ui-approval` / `dsh-client-ui-user-questions` / `dsh-client-ui-open-in-app` / `dsh-client-ui-layout` 等同样以 `"uiSession"` 为服务依赖。
|
|
21
|
+
|
|
22
|
+
### 变更
|
|
23
|
+
- 两条来源同时可用时按交互 key 去重:同一次等待只提醒一次、也只有一个标签页标记;标题标记的归属固定给交互 store(可用时),避免两路各记一个标记。
|
|
24
|
+
- 测试:`smoke/smoke-test.cjs` 拆成**两代宿主**各跑一遍——旧宿主(无 `uiSession`,快照带 `pending`)与新宿主(`uiSession.pendingInteractions`),新增「重复通知不重复提醒」「挂载前已存在的等待仍提醒一次」「多会话并发等待各提醒一次并逐个释放标记」「两路去重」「主开关关闭时交互通道同样静音」等回归用例。
|
|
9
25
|
|
|
10
26
|
## [1.1.4] — 2026-09-13
|
|
11
27
|
|
package/lib/client.js
CHANGED
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
// slots/locale services are absent, keeping the reminder core self-contained.
|
|
11
11
|
//
|
|
12
12
|
// What this plugin does (all client-side, no settings transport needed):
|
|
13
|
-
// - watches
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// - watches two services, because pending interactions and the running bit
|
|
14
|
+
// have different owners on current hosts:
|
|
15
|
+
// * "uiSession" -> pendingInteractions (a Map<sessionId, interaction>
|
|
16
|
+
// snapshot whose values carry kind 'approval' | 'plan-review' |
|
|
17
|
+
// 'question'). Present since DSH 0.1.2-alpha.2; the controller
|
|
18
|
+
// snapshots stopped carrying `pending` in the same release, so this is
|
|
19
|
+
// the only live source of "the agent is waiting on you" on 0.1.2+;
|
|
20
|
+
// * "sessions" -> the selected session's ConversationSnapshot (running)
|
|
21
|
+
// and the session-list summaries of every other session (running /
|
|
22
|
+
// completed), which carry the "reply finished" edge on every version;
|
|
16
23
|
// - fires a Windows desktop Notification (+ optional sound) when the agent
|
|
17
24
|
// needs the user's input — pending interaction kinds 'approval',
|
|
18
25
|
// 'plan-review' or 'question' — and when a reply/turn finishes;
|
|
@@ -227,14 +234,32 @@ window.__ModuleLoader__.load({
|
|
|
227
234
|
window.addEventListener("keydown", onFirstGesture, true);
|
|
228
235
|
|
|
229
236
|
// ────────────────────────── title marker ─────────────────────────
|
|
230
|
-
|
|
237
|
+
// The host owns document.title (it tracks the session/workspace name), so
|
|
238
|
+
// the base title is re-read on every apply instead of being frozen once —
|
|
239
|
+
// and the exact string we last wrote is remembered, so the host taking the
|
|
240
|
+
// title over (or an earlier load leaving a marker behind) can never make us
|
|
241
|
+
// mistake our own marker for the base title.
|
|
231
242
|
var attentionActive = 0;
|
|
243
|
+
var hostBase = null; // document.title as the host last left it
|
|
244
|
+
var writtenTitle = null; // exact string this plugin last put into document.title
|
|
245
|
+
// Our own marker prefix never belongs to the base title — strip a leftover
|
|
246
|
+
// one (an earlier load that was closed while a wait was pending, or a title
|
|
247
|
+
// the host tracked while a marker was up) before storing the base.
|
|
248
|
+
function cleanBase(t) {
|
|
249
|
+
var d = texts();
|
|
250
|
+
var prefixes = [d.marker, TXT.zh.marker, TXT.en.marker];
|
|
251
|
+
for (var i = 0; i < prefixes.length; i++) {
|
|
252
|
+
while (t.indexOf(prefixes[i]) === 0) t = t.slice(prefixes[i].length);
|
|
253
|
+
}
|
|
254
|
+
return t;
|
|
255
|
+
}
|
|
232
256
|
function applyTitle() {
|
|
233
257
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
258
|
+
var current = String(document.title || "");
|
|
259
|
+
if (current !== writtenTitle) hostBase = cleanBase(current);
|
|
260
|
+
var next = (config.enabled && attentionActive > 0) ? texts().marker + hostBase : hostBase;
|
|
261
|
+
writtenTitle = next;
|
|
262
|
+
if (current !== next) document.title = next;
|
|
238
263
|
} catch (e) {}
|
|
239
264
|
}
|
|
240
265
|
function acquireTitleMark() {
|
|
@@ -245,21 +270,39 @@ window.__ModuleLoader__.load({
|
|
|
245
270
|
if (attentionActive > 0) attentionActive -= 1;
|
|
246
271
|
applyTitle();
|
|
247
272
|
}
|
|
273
|
+
// Title marker ownership: on hosts that publish pending interactions
|
|
274
|
+
// (uiSession) that source owns the marker, but whichever source saw the wait
|
|
275
|
+
// first must still mark it (the other one may never report that key at all),
|
|
276
|
+
// so the mark is acquired by the first reporter and released only by its
|
|
277
|
+
// owner. Without this, the two sources would either double-mark or never
|
|
278
|
+
// clear the mark.
|
|
279
|
+
function markAttention(key) {
|
|
280
|
+
if (attentionKeysMarked[key]) return;
|
|
281
|
+
attentionKeysMarked[key] = true;
|
|
282
|
+
acquireTitleMark();
|
|
283
|
+
}
|
|
284
|
+
function unmarkAttention(key) {
|
|
285
|
+
if (!attentionKeysMarked[key]) return;
|
|
286
|
+
delete attentionKeysMarked[key];
|
|
287
|
+
releaseTitleMark();
|
|
288
|
+
}
|
|
248
289
|
|
|
249
290
|
// ─────────────────────── shared alert core ───────────────────────
|
|
250
291
|
var attentionKeysFired = Object.create(null); // dedupe per pending key
|
|
292
|
+
var attentionKeysMarked = Object.create(null); // keys currently holding a title mark
|
|
251
293
|
var lastAttentionAt = 0;
|
|
252
294
|
|
|
253
295
|
function hidden() { return document.hidden || document.visibilityState === "hidden"; }
|
|
254
296
|
|
|
255
297
|
// Deliver one alert regardless of visibility rules (used by the test
|
|
256
|
-
// buttons); respects the master switch and permission state.
|
|
298
|
+
// buttons); respects the master switch and permission state. The title
|
|
299
|
+
// marker is NOT touched here — its ownership belongs to the watchers
|
|
300
|
+
// (markAttention / unmarkAttention), which keeps the counter balanced.
|
|
257
301
|
function deliver(kind, title, body) {
|
|
258
302
|
if (!config.enabled) return;
|
|
259
303
|
showToast(kind, title, body);
|
|
260
304
|
if (kind === "attention") {
|
|
261
305
|
playAttentionSound();
|
|
262
|
-
acquireTitleMark();
|
|
263
306
|
} else {
|
|
264
307
|
playDoneSound();
|
|
265
308
|
}
|
|
@@ -282,6 +325,20 @@ window.__ModuleLoader__.load({
|
|
|
282
325
|
if (!raw || raw === "current") return texts().current;
|
|
283
326
|
return raw;
|
|
284
327
|
}
|
|
328
|
+
// Display label for a session id we only know from the interaction map:
|
|
329
|
+
// the list row's title, the localized "current" placeholder for the selected
|
|
330
|
+
// session, the raw id as a last resort.
|
|
331
|
+
function labelForSessionId(sid) {
|
|
332
|
+
try {
|
|
333
|
+
var st = listStore && listStore.getSnapshot();
|
|
334
|
+
if (st) {
|
|
335
|
+
var sum = st.byId && st.byId[sid];
|
|
336
|
+
if (sum) return sessionLabelText({ label: sum.displayTitle || sum.title || "" });
|
|
337
|
+
if (st.current === sid) return texts().current;
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {}
|
|
340
|
+
return sid;
|
|
341
|
+
}
|
|
285
342
|
function fireAttention(kind, sessionLabel, detail) {
|
|
286
343
|
var d = texts();
|
|
287
344
|
var label = d.kind[kind] || d.kindDefault;
|
|
@@ -302,11 +359,13 @@ window.__ModuleLoader__.load({
|
|
|
302
359
|
// also drops its title marker again shortly after, so a test never leaves a
|
|
303
360
|
// standing "waiting on you" mark in the tab.
|
|
304
361
|
var TEST_MARK_RELEASE_MS = 6000;
|
|
362
|
+
var TEST_MARK_KEY = "test:attention";
|
|
305
363
|
function testAlert(kind) {
|
|
306
364
|
var d = texts();
|
|
307
365
|
if (kind === "attention") {
|
|
308
366
|
deliver("attention", "DSH · " + d.kind.question, d.testAttention);
|
|
309
|
-
|
|
367
|
+
markAttention(TEST_MARK_KEY);
|
|
368
|
+
setTimeout(function () { unmarkAttention(TEST_MARK_KEY); }, TEST_MARK_RELEASE_MS);
|
|
310
369
|
} else {
|
|
311
370
|
deliver("done", d.doneTitle, d.testDone);
|
|
312
371
|
}
|
|
@@ -320,16 +379,52 @@ window.__ModuleLoader__.load({
|
|
|
320
379
|
var currentFaceId = null;
|
|
321
380
|
var listStore = null;
|
|
322
381
|
var rootCtx = null; // client cordis context, captured at apply()
|
|
382
|
+
// uiSession pending-interaction watcher (DSH >= 0.1.2-alpha.2)
|
|
383
|
+
var uiPending = false; // true once the interaction snapshot is readable
|
|
384
|
+
var uiSub = null;
|
|
385
|
+
var uiBaseline = Object.create(null); // sessionId -> { key: string }
|
|
323
386
|
|
|
387
|
+
// One-line detail for a pending interaction. Two shapes reach this:
|
|
388
|
+
// * the pre-0.1.2 controller payload (approval/question requested frames)
|
|
389
|
+
// — flat, its text under `text`/`question`/…;
|
|
390
|
+
// * the 0.1.2+ uiSession interaction objects — class instances whose own
|
|
391
|
+
// fields are the detail (PendingApproval: toolName/reason/callId,
|
|
392
|
+
// PendingQuestion: questions[]).
|
|
324
393
|
function textFromPayload(payload) {
|
|
325
394
|
if (!payload || typeof payload !== "object") return "";
|
|
326
|
-
|
|
395
|
+
function clip(s) {
|
|
396
|
+
s = String(s).replace(/\s+/g, " ").trim();
|
|
397
|
+
if (!s) return "";
|
|
398
|
+
return s.length > 160 ? s.slice(0, 160) + "…" : s;
|
|
399
|
+
}
|
|
400
|
+
// scalar fields (covers the flat pre-0.1.2 payloads and PendingApproval)
|
|
401
|
+
var keys = ["text", "question", "prompt", "title", "summary", "description", "message", "operation", "toolName", "reason", "name", "tool"];
|
|
327
402
|
for (var i = 0; i < keys.length; i++) {
|
|
328
403
|
var v = payload[keys[i]];
|
|
329
|
-
if (typeof v === "string"
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
404
|
+
if (typeof v === "string") {
|
|
405
|
+
var s = clip(v);
|
|
406
|
+
if (s) {
|
|
407
|
+
// PendingApproval carries both the tool and the human-readable
|
|
408
|
+
// reason — "pwsh" alone says nothing useful in a toast.
|
|
409
|
+
if (keys[i] === "toolName" && typeof payload.reason === "string" && clip(payload.reason)) {
|
|
410
|
+
return s + " · " + clip(payload.reason);
|
|
411
|
+
}
|
|
412
|
+
return s;
|
|
413
|
+
}
|
|
414
|
+
} else if (v && typeof v === "object" && typeof v.text === "string" && clip(v.text)) {
|
|
415
|
+
return clip(v.text);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// nested batches (PendingQuestion.questions -> { question, detail, options })
|
|
419
|
+
var qs = payload.questions;
|
|
420
|
+
if (Array.isArray(qs) && qs.length > 0) {
|
|
421
|
+
var q = qs[0];
|
|
422
|
+
if (typeof q === "string") return clip(q);
|
|
423
|
+
if (q && typeof q === "object") {
|
|
424
|
+
var qk = ["question", "title", "text", "detail"];
|
|
425
|
+
for (var j = 0; j < qk.length; j++) {
|
|
426
|
+
if (typeof q[qk[j]] === "string" && clip(q[qk[j]])) return clip(q[qk[j]]);
|
|
427
|
+
}
|
|
333
428
|
}
|
|
334
429
|
}
|
|
335
430
|
return "";
|
|
@@ -372,12 +467,14 @@ window.__ModuleLoader__.load({
|
|
|
372
467
|
// First evaluation of a session also lands here (prevPendingKeys empty),
|
|
373
468
|
// so a wait that already existed when the plugin (or page) mounted still
|
|
374
469
|
// alerts once.
|
|
470
|
+
// On hosts that expose uiSession this is the fallback path only: the
|
|
471
|
+
// interaction snapshot there is the authoritative source, and it drives
|
|
472
|
+
// the title marker itself (see the uiSession watcher below).
|
|
375
473
|
if (newKeys.length > 0) {
|
|
376
474
|
var it = newKeys[0];
|
|
377
|
-
|
|
378
|
-
if (!detail) detail = "";
|
|
379
|
-
fireAttention(it.kind, sessionLabelText(row), detail);
|
|
475
|
+
fireAttention(it.kind, sessionLabelText(row), textFromPayload(it.payload));
|
|
380
476
|
lastAttentionAt = Date.now();
|
|
477
|
+
markAttention(it.key);
|
|
381
478
|
}
|
|
382
479
|
|
|
383
480
|
// 2) a non-selected session finished while we were away (sidebar "done")
|
|
@@ -406,8 +503,8 @@ window.__ModuleLoader__.load({
|
|
|
406
503
|
};
|
|
407
504
|
|
|
408
505
|
// a wait we alerted about has been resolved -> drop the title marker
|
|
409
|
-
if (prev && prevPendingKeys.length > 0 && pendingNow.length === 0) {
|
|
410
|
-
|
|
506
|
+
if (!uiPending && prev && prevPendingKeys.length > 0 && pendingNow.length === 0) {
|
|
507
|
+
for (var r = 0; r < prevPendingKeys.length; r++) unmarkAttention(prevPendingKeys[r]);
|
|
411
508
|
}
|
|
412
509
|
}
|
|
413
510
|
|
|
@@ -476,6 +573,7 @@ window.__ModuleLoader__.load({
|
|
|
476
573
|
var st = listStore.getSnapshot();
|
|
477
574
|
var cur = st && st.current;
|
|
478
575
|
bindCurrentFace();
|
|
576
|
+
bindUiSession();
|
|
479
577
|
var byId = (st && st.byId) || {};
|
|
480
578
|
for (var sid in byId) {
|
|
481
579
|
if (sid === cur) continue; // current handled by the face watcher
|
|
@@ -487,25 +585,98 @@ window.__ModuleLoader__.load({
|
|
|
487
585
|
} catch (e) {}
|
|
488
586
|
}
|
|
489
587
|
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
588
|
+
// ─────────────── uiSession pending-interaction watcher ───────────
|
|
589
|
+
// DSH >= 0.1.2-alpha.2 moved pending interactions out of the Controller
|
|
590
|
+
// snapshots into their own store (uiSession.pendingInteractions), a
|
|
591
|
+
// Map<sessionId, interaction> whose values carry kind + key + sessionId and
|
|
592
|
+
// the domain's own detail fields. It is the only live "the agent is waiting
|
|
593
|
+
// on you" source on those hosts.
|
|
594
|
+
//
|
|
595
|
+
// The service is looked up lazily rather than declared in `inject`:
|
|
596
|
+
// a cordis `inject` entry is a hard gate (the entry never activates while
|
|
597
|
+
// the service is missing, and a never-activated entry makes the whole web
|
|
598
|
+
// boot audit fail), while hosts older than 0.1.2-alpha.2 have no uiSession
|
|
599
|
+
// at all and must keep working through the controller path above.
|
|
600
|
+
function bindUiSession() {
|
|
601
|
+
if (uiSub) return;
|
|
602
|
+
var svc = null;
|
|
603
|
+
try { svc = rootCtx && rootCtx.uiSession; } catch (e) { svc = null; }
|
|
604
|
+
var store = svc && svc.pendingInteractions;
|
|
605
|
+
if (!store || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") return;
|
|
606
|
+
uiPending = true; // this watcher now owns the title marker
|
|
607
|
+
uiSub = store.subscribe(function () { try { evalUiPending(); } catch (e) {} });
|
|
608
|
+
evalUiPending(); // seed: a wait that predates this plugin still alerts once
|
|
609
|
+
}
|
|
610
|
+
function evalUiPending() {
|
|
611
|
+
var svc = null;
|
|
612
|
+
try { svc = rootCtx && rootCtx.uiSession; } catch (e) { svc = null; }
|
|
613
|
+
var store = svc && svc.pendingInteractions;
|
|
614
|
+
if (!store || typeof store.getSnapshot !== "function") return;
|
|
615
|
+
var snap = store.getSnapshot(); // Map<sessionId, interaction>
|
|
616
|
+
if (!snap || typeof snap.forEach !== "function") return;
|
|
617
|
+
var seen = Object.create(null);
|
|
618
|
+
snap.forEach(function (it, sid) {
|
|
619
|
+
if (!it) return;
|
|
620
|
+
seen[sid] = true;
|
|
621
|
+
var key = it.key || (it.kind + ":" + sid);
|
|
622
|
+
var prev = uiBaseline[sid];
|
|
623
|
+
// new interactions alert once; a wait that already existed on bind
|
|
624
|
+
// (no baseline yet for that session) alerts here too.
|
|
625
|
+
if (prev && prev.key === key) return;
|
|
626
|
+
uiBaseline[sid] = { key: key };
|
|
627
|
+
// a wait replaced by a newer one in the same session: the old mark goes
|
|
628
|
+
// with it, otherwise the tab would keep one stale marker per replacement
|
|
629
|
+
if (prev) unmarkAttention(prev.key);
|
|
630
|
+
if (attentionKeysFired[key]) { markAttention(key); return; } // already alerted, still needs its mark
|
|
631
|
+
attentionKeysFired[key] = true;
|
|
632
|
+
fireAttention(it.kind, labelForSessionId(sid), textFromPayload(it));
|
|
633
|
+
lastAttentionAt = Date.now();
|
|
634
|
+
markAttention(key);
|
|
635
|
+
});
|
|
636
|
+
for (var sid2 in uiBaseline) {
|
|
637
|
+
if (seen[sid2]) continue;
|
|
638
|
+
// interaction answered, cancelled, or its session went away
|
|
639
|
+
unmarkAttention(uiBaseline[sid2].key);
|
|
640
|
+
delete uiBaseline[sid2];
|
|
508
641
|
}
|
|
642
|
+
}
|
|
643
|
+
function detachUiSession() {
|
|
644
|
+
if (uiSub) { try { uiSub(); } catch (e) {} uiSub = null; }
|
|
645
|
+
// drop every mark this watcher holds; hosts without the interaction store
|
|
646
|
+
// go back to the controller marker path
|
|
647
|
+
for (var s in uiBaseline) unmarkAttention(uiBaseline[s].key);
|
|
648
|
+
uiBaseline = Object.create(null);
|
|
649
|
+
uiPending = false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ──────────────────────── global control API ─────────────────────
|
|
653
|
+
// Expose the control API. Merge instead of replace: a consumer (or a test
|
|
654
|
+
// harness) may have registered onEvent before the plugin applied, and that
|
|
655
|
+
// hook must survive — binding the interaction store can deliver an alert
|
|
656
|
+
// synchronously, in the middle of apply().
|
|
657
|
+
var api = window.__dshNotifyMe;
|
|
658
|
+
if (!api || typeof api !== "object") api = {};
|
|
659
|
+
try {
|
|
660
|
+
Object.defineProperty(api, "config", {
|
|
661
|
+
get: function () { return readConfig(); },
|
|
662
|
+
configurable: true
|
|
663
|
+
});
|
|
664
|
+
} catch (e) {}
|
|
665
|
+
// keep in sync with package.json "version" on every release
|
|
666
|
+
api.version = "1.1.5";
|
|
667
|
+
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
668
|
+
api.resetConfig = function () {
|
|
669
|
+
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
670
|
+
config = readConfig();
|
|
671
|
+
return config;
|
|
672
|
+
};
|
|
673
|
+
// manual test; run it after clicking anywhere so permission can be granted.
|
|
674
|
+
// Test alerts fire regardless of the page-open visibility rules.
|
|
675
|
+
api.test = function (kind) {
|
|
676
|
+
kind = kind === "attention" ? "attention" : "done";
|
|
677
|
+
unlockOnce();
|
|
678
|
+
testAlert(kind);
|
|
679
|
+
return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ")";
|
|
509
680
|
};
|
|
510
681
|
window.__dshNotifyMe = api;
|
|
511
682
|
|
|
@@ -739,6 +910,14 @@ window.__ModuleLoader__.load({
|
|
|
739
910
|
}
|
|
740
911
|
|
|
741
912
|
// ────────────────────────── plugin body ──────────────────────────
|
|
913
|
+
// Hard service requirements only. `uiSession` is deliberately NOT listed:
|
|
914
|
+
// a cordis inject entry is a hard gate (the entry stays pending while the
|
|
915
|
+
// service is missing, and the web boot audit fails on entries that never
|
|
916
|
+
// activate), while hosts older than 0.1.2-alpha.2 have no uiSession at all.
|
|
917
|
+
// It is therefore looked up lazily in bindUiSession(), with the controller
|
|
918
|
+
// path above as the fallback. The bundle entry
|
|
919
|
+
// "@deepseek-ai/dsh-client-ui-session" stays in package.json's
|
|
920
|
+
// dsh.client.inject so the load order keeps the bundle in the graph.
|
|
742
921
|
var inject = ["sessions", "locale", "slots"];
|
|
743
922
|
|
|
744
923
|
/**
|
|
@@ -788,12 +967,16 @@ window.__ModuleLoader__.load({
|
|
|
788
967
|
}
|
|
789
968
|
listStore = sessionsSvc.list;
|
|
790
969
|
unsubs.push(sessionsSvc.list.subscribe(onListChanged));
|
|
791
|
-
//
|
|
970
|
+
// Interaction source first: it owns the title marker once present, so
|
|
971
|
+
// binding it before the controller scan keeps a single mark per wait.
|
|
972
|
+
bindUiSession();
|
|
973
|
+
// initial scan (baseline only; no alerts for pre-existing sessions)
|
|
792
974
|
onListChanged();
|
|
793
975
|
// binding may resolve a tick later; re-check periodically while idle
|
|
794
976
|
retryTimer = setInterval(function () {
|
|
795
977
|
if (disposed) return;
|
|
796
978
|
try { bindCurrentFace(); } catch (e) {}
|
|
979
|
+
try { bindUiSession(); } catch (e) {}
|
|
797
980
|
}, 1500);
|
|
798
981
|
try { console.log("[dsh-notify-me] active — window.__dshNotifyMe 可配置提醒 (enabled=" + config.enabled + ", lang=" + effectiveLang() + ", toast=" + config.toast + ", sound=" + config.sound + ")"); } catch (e) {}
|
|
799
982
|
} catch (e) {
|
|
@@ -804,6 +987,7 @@ window.__ModuleLoader__.load({
|
|
|
804
987
|
if (retryTimer) clearInterval(retryTimer);
|
|
805
988
|
for (var i = 0; i < unsubs.length; i++) { try { unsubs[i](); } catch (e) {} }
|
|
806
989
|
detachFace();
|
|
990
|
+
detachUiSession();
|
|
807
991
|
};
|
|
808
992
|
}, "dsh-notify-me: watchers");
|
|
809
993
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-me",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "DSH 桌面消息提醒插件:模型需要你操作(审批/方案确认/提问的可操作提醒)或回复在后台完成时,系统通知+提示音+标签页标题提醒你;可在设置页开关并切换通知语言。Desktop notification & message alerts for DeepSeek Harness web — toasts, sounds and a tab-title marker when the agent needs your input (approval / plan review / question) or a reply finishes in the background; toggleable from Settings with zh/en notification language.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|