dsh-notify-yimit 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/README.en.md +97 -97
- package/README.md +96 -94
- package/cordis.patch.yml +6 -6
- package/lib/client.js +1369 -790
- package/lib/index.js +1098 -716
- package/lib/toast-host.ps1 +496 -329
- package/lib/typert.host.js +227 -158
- package/package.json +56 -50
package/lib/index.js
CHANGED
|
@@ -1,716 +1,1098 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-notify-yimit-yimit 宿主插件。
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-notify-yimit-yimit 宿主插件。
|
|
3
|
+
* 优化点:内存LRU控制、队列头指针优化、浏览器探测缓存、宿主崩溃退避、全面多语言支持。
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
z
|
|
7
|
+
} from 'zod'
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import {
|
|
11
|
+
spawn
|
|
12
|
+
} from 'node:child_process'
|
|
13
|
+
import {
|
|
14
|
+
fileURLToPath
|
|
15
|
+
} from 'node:url'
|
|
16
|
+
import {
|
|
17
|
+
resolveDshHome
|
|
18
|
+
} from '@deepseek-ai/dsh-home-paths'
|
|
19
|
+
|
|
20
|
+
export const name = 'dsh-notify-yimit'
|
|
21
|
+
|
|
22
|
+
const KIND_KEYS = Object.freeze(['completed', 'error', 'running', 'approval', 'question'])
|
|
23
|
+
|
|
24
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
25
|
+
enabled: false,
|
|
26
|
+
mode: 'off',
|
|
27
|
+
maxVisible: 3,
|
|
28
|
+
durationMs: 8000,
|
|
29
|
+
browser: '',
|
|
30
|
+
bgColor: '#20272f',
|
|
31
|
+
textColor: '#e6edf3',
|
|
32
|
+
locale: 'en',
|
|
33
|
+
// 通知窗口样式:width 窗口宽度(编辑模式拖拽边缘调整);gap 通知间距(px);
|
|
34
|
+
// position = null 表示默认右下贴边,{left,top} 为任意屏幕位置(工作区相对坐标)。
|
|
35
|
+
toast: {
|
|
36
|
+
width: 340,
|
|
37
|
+
gap: 12,
|
|
38
|
+
position: null,
|
|
39
|
+
},
|
|
40
|
+
colors: {
|
|
41
|
+
completed: {
|
|
42
|
+
bg: '#1f5138',
|
|
43
|
+
fg: '#e6f4ec',
|
|
44
|
+
enabled: true
|
|
45
|
+
},
|
|
46
|
+
error: {
|
|
47
|
+
bg: '#5c1f24',
|
|
48
|
+
fg: '#fbe9ea',
|
|
49
|
+
enabled: true
|
|
50
|
+
},
|
|
51
|
+
running: {
|
|
52
|
+
bg: '#203a5c',
|
|
53
|
+
fg: '#e8f0fb',
|
|
54
|
+
enabled: true
|
|
55
|
+
},
|
|
56
|
+
approval: {
|
|
57
|
+
bg: '#4a3a12',
|
|
58
|
+
fg: '#f7efd8',
|
|
59
|
+
enabled: true
|
|
60
|
+
},
|
|
61
|
+
question: {
|
|
62
|
+
bg: '#3d2a55',
|
|
63
|
+
fg: '#f0e8fa',
|
|
64
|
+
enabled: true
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const ACTIVITY_LABELS = {
|
|
70
|
+
zh: {
|
|
71
|
+
start: '开始处理',
|
|
72
|
+
thinking: '正在思考…',
|
|
73
|
+
generating: '正在生成回复…',
|
|
74
|
+
executing: '正在执行',
|
|
75
|
+
continuing: '继续处理…',
|
|
76
|
+
processing: '正在处理…',
|
|
77
|
+
completed: '任务已完成',
|
|
78
|
+
errorPrefix: '任务出错:',
|
|
79
|
+
errorFallback: '任务出错',
|
|
80
|
+
approvalPrefix: '等待批准执行',
|
|
81
|
+
approvalFallback: '等待批准',
|
|
82
|
+
},
|
|
83
|
+
en: {
|
|
84
|
+
start: 'Started processing',
|
|
85
|
+
thinking: 'Thinking…',
|
|
86
|
+
generating: 'Generating response…',
|
|
87
|
+
executing: 'Executing',
|
|
88
|
+
continuing: 'Continuing…',
|
|
89
|
+
processing: 'Processing...',
|
|
90
|
+
completed: 'Task completed',
|
|
91
|
+
errorPrefix: 'Task failed: ',
|
|
92
|
+
errorFallback: 'Task failed',
|
|
93
|
+
approvalPrefix: 'Awaiting approval to execute',
|
|
94
|
+
approvalFallback: 'Awaiting approval',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const TOAST_LABELS = {
|
|
99
|
+
zh: {
|
|
100
|
+
ignore: '忽略',
|
|
101
|
+
jump: '跳转会话',
|
|
102
|
+
unnamed: '(未命名会话)'
|
|
103
|
+
},
|
|
104
|
+
en: {
|
|
105
|
+
ignore: 'Ignore',
|
|
106
|
+
jump: 'Open',
|
|
107
|
+
unnamed: '(Unnamed session)'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 标题未生成时的占位文本(中/英):session/title 到达后原地替换。 */
|
|
112
|
+
const UNTITLED_NAMES = new Set([TOAST_LABELS.zh.unnamed, TOAST_LABELS.en.unnamed])
|
|
113
|
+
|
|
114
|
+
/** 编辑模式样板窗标题(中/英)。 */
|
|
115
|
+
const EDIT_TITLES = {
|
|
116
|
+
zh: '通知样式预览',
|
|
117
|
+
en: 'Notification preview'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const HEX_RE = /^#[0-9a-fA-F]{3,8}$/
|
|
121
|
+
const isHex = (v) => typeof v === 'string' && HEX_RE.test(v)
|
|
122
|
+
|
|
123
|
+
function normalizeConfig(input) {
|
|
124
|
+
const src = input !== null && typeof input === 'object' && !Array.isArray(input) ? input : {}
|
|
125
|
+
const out = {
|
|
126
|
+
...DEFAULT_CONFIG
|
|
127
|
+
}
|
|
128
|
+
if (typeof src.enabled === 'boolean') out.enabled = src.enabled
|
|
129
|
+
if (src.mode === 'system' || src.mode === 'custom' || src.mode === 'off') out.mode = src.mode
|
|
130
|
+
if (typeof src.maxVisible === 'number' && Number.isFinite(src.maxVisible)) out.maxVisible = Math.max(1, Math.min(20,
|
|
131
|
+
Math.floor(src.maxVisible)))
|
|
132
|
+
if (typeof src.durationMs === 'number' && Number.isFinite(src.durationMs)) out.durationMs = Math.max(1000, Math.min(
|
|
133
|
+
300000, Math.floor(src.durationMs)))
|
|
134
|
+
if (typeof src.browser === 'string') out.browser = src.browser
|
|
135
|
+
if (isHex(src.bgColor)) out.bgColor = src.bgColor
|
|
136
|
+
if (isHex(src.textColor)) out.textColor = src.textColor
|
|
137
|
+
if (src.locale === 'zh' || src.locale === 'en') out.locale = src.locale
|
|
138
|
+
|
|
139
|
+
// 通知窗口样式:width 260–520;gap 0–60;position 缺省 null(默认右下贴边),合法 {left,top} 才采纳。
|
|
140
|
+
out.toast = { width: 340, gap: 12, position: null }
|
|
141
|
+
const srcToast = src.toast !== null && typeof src.toast === 'object' && !Array.isArray(src.toast) ? src.toast : {}
|
|
142
|
+
if (typeof srcToast.width === 'number' && Number.isFinite(srcToast.width)) {
|
|
143
|
+
out.toast.width = Math.max(260, Math.min(520, Math.round(srcToast.width)))
|
|
144
|
+
}
|
|
145
|
+
if (typeof srcToast.gap === 'number' && Number.isFinite(srcToast.gap)) {
|
|
146
|
+
out.toast.gap = Math.max(0, Math.min(60, Math.round(srcToast.gap)))
|
|
147
|
+
}
|
|
148
|
+
const pos = srcToast.position
|
|
149
|
+
if (pos !== null && typeof pos === 'object' && !Array.isArray(pos) &&
|
|
150
|
+
typeof pos.left === 'number' && Number.isFinite(pos.left) &&
|
|
151
|
+
typeof pos.top === 'number' && Number.isFinite(pos.top)) {
|
|
152
|
+
out.toast.position = { left: Math.round(pos.left), top: Math.round(pos.top) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const srcColors = src.colors !== null && typeof src.colors === 'object' && !Array.isArray(src.colors) ? src.colors :
|
|
156
|
+
{}
|
|
157
|
+
out.colors = {}
|
|
158
|
+
for (const kind of KIND_KEYS) {
|
|
159
|
+
const entry = srcColors[kind]
|
|
160
|
+
const def = DEFAULT_CONFIG.colors[kind]
|
|
161
|
+
out.colors[kind] = {
|
|
162
|
+
bg: isHex(entry?.bg) ? entry.bg : def.bg,
|
|
163
|
+
fg: isHex(entry?.fg) ? entry.fg : def.fg,
|
|
164
|
+
enabled: entry?.enabled !== false,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function migrateLegacyColors(input) {
|
|
171
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) return input
|
|
172
|
+
if (input.colors !== null && typeof input.colors === 'object' && !Array.isArray(input.colors)) return input
|
|
173
|
+
if (!isHex(input.bgColor) && !isHex(input.textColor)) return input
|
|
174
|
+
const colors = {}
|
|
175
|
+
for (const kind of KIND_KEYS) colors[kind] = {
|
|
176
|
+
bg: input.bgColor,
|
|
177
|
+
fg: input.textColor,
|
|
178
|
+
enabled: true
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...input,
|
|
182
|
+
colors
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function mergeConfigPatch(base, patch) {
|
|
187
|
+
const out = {
|
|
188
|
+
...base,
|
|
189
|
+
...patch
|
|
190
|
+
}
|
|
191
|
+
if (patch !== null && typeof patch === 'object' && patch.colors !== null && typeof patch.colors === 'object' && !
|
|
192
|
+
Array.isArray(patch.colors)) {
|
|
193
|
+
const baseColors = base.colors !== null && typeof base.colors === 'object' ? base.colors : {}
|
|
194
|
+
const next = {}
|
|
195
|
+
for (const kind of KIND_KEYS) next[kind] = {
|
|
196
|
+
...(baseColors[kind] ?? {}),
|
|
197
|
+
...(patch.colors[kind] ?? {})
|
|
198
|
+
}
|
|
199
|
+
out.colors = next
|
|
200
|
+
}
|
|
201
|
+
if (patch !== null && typeof patch === 'object' && patch.toast !== null && typeof patch.toast === 'object' && !
|
|
202
|
+
Array.isArray(patch.toast)) {
|
|
203
|
+
out.toast = {
|
|
204
|
+
...(base.toast !== null && typeof base.toast === 'object' && !Array.isArray(base.toast) ? base.toast : {}),
|
|
205
|
+
...patch.toast
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function openConfig() {
|
|
212
|
+
const home = resolveDshHome()
|
|
213
|
+
const dir = path.join(home, 'storages', 'dsh-notify-yimit')
|
|
214
|
+
const file = path.join(dir, 'config.json')
|
|
215
|
+
let loaded = null
|
|
216
|
+
try {
|
|
217
|
+
loaded = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
218
|
+
} catch {
|
|
219
|
+
loaded = null
|
|
220
|
+
}
|
|
221
|
+
let config = normalizeConfig(migrateLegacyColors(loaded))
|
|
222
|
+
let timer = null
|
|
223
|
+
|
|
224
|
+
const persist = () => {
|
|
225
|
+
if (timer !== null) return
|
|
226
|
+
timer = setTimeout(() => {
|
|
227
|
+
timer = null
|
|
228
|
+
try {
|
|
229
|
+
fs.mkdirSync(dir, {
|
|
230
|
+
recursive: true
|
|
231
|
+
})
|
|
232
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
233
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), 'utf8')
|
|
234
|
+
fs.renameSync(tmp, file)
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.warn(`[dsh-notify-yimit] Failed to persist config: ${String(error)}`)
|
|
237
|
+
}
|
|
238
|
+
}, 200)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
get: () => config,
|
|
243
|
+
set(patch) {
|
|
244
|
+
config = normalizeConfig(mergeConfigPatch(config, patch))
|
|
245
|
+
persist()
|
|
246
|
+
return config
|
|
247
|
+
},
|
|
248
|
+
close() {
|
|
249
|
+
if (timer !== null) {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
timer = null
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
fs.mkdirSync(dir, {
|
|
255
|
+
recursive: true
|
|
256
|
+
})
|
|
257
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
258
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), 'utf8')
|
|
259
|
+
fs.renameSync(tmp, file)
|
|
260
|
+
} catch {
|
|
261
|
+
/* 静默 */ }
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const MAX_QUEUE = 500
|
|
267
|
+
const RUNNING_THROTTLE_MS = 400
|
|
268
|
+
const KIND_DEBOUNCE_MS = 2000
|
|
269
|
+
/** 通知间距默认值(px);实际使用 config.toast.gap(0–60,可调)。 */
|
|
270
|
+
const TOAST_GAP = 12
|
|
271
|
+
const TEXT_MAX = 300
|
|
272
|
+
|
|
273
|
+
function clip(text) {
|
|
274
|
+
const s = String(text ?? '')
|
|
275
|
+
return s.length > TEXT_MAX ? `${s.slice(0, TEXT_MAX)}…` : s
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function titleOf(session) {
|
|
279
|
+
const events = session?.events
|
|
280
|
+
if (Array.isArray(events)) {
|
|
281
|
+
// 全量扫描:标题事件通常在会话早期(seq 小)生成,长会话/恢复会话里它可能在
|
|
282
|
+
// 最后 50 条之外——只扫尾部会漏掉,导致回退到占位/目录名(恢复会话不会重放
|
|
283
|
+
// 种子事件,event 流也补不上)。
|
|
284
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
285
|
+
const event = events[i]
|
|
286
|
+
if (event?.type === 'session/title' && typeof event.data?.title === 'string' && event.data.title.length > 0)
|
|
287
|
+
return event.data.title
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// 标题尚未生成(异步 LLM/摘录,首轮事件都先于 session/title 到达):
|
|
291
|
+
// 不回退到 cwd 目录名(工作区名),返回空由调用方按语言显示占位;
|
|
292
|
+
// session/title 事件到达后,已弹浮窗标题经 title: 命令原地更新。
|
|
293
|
+
return ''
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let cachedBrowsers = null
|
|
297
|
+
|
|
298
|
+
function browserCandidates() {
|
|
299
|
+
const pf = process.env.ProgramFiles ?? 'C:\\Program Files'
|
|
300
|
+
const pfx86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)'
|
|
301
|
+
const local = process.env.LOCALAPPDATA ?? ''
|
|
302
|
+
return [{
|
|
303
|
+
id: 'edge',
|
|
304
|
+
name: 'Microsoft Edge',
|
|
305
|
+
paths: [`${pfx86}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
306
|
+
`${pf}\\Microsoft\\Edge\\Application\\msedge.exe`
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: 'chrome',
|
|
311
|
+
name: 'Google Chrome',
|
|
312
|
+
paths: [`${pf}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
313
|
+
`${pfx86}\\Google\\Chrome\\Application\\chrome.exe`, local ?
|
|
314
|
+
`${local}\\Google\\Chrome\\Application\\chrome.exe` : ''
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'firefox',
|
|
319
|
+
name: 'Mozilla Firefox',
|
|
320
|
+
paths: [`${pf}\\Mozilla Firefox\\firefox.exe`, `${pfx86}\\Mozilla Firefox\\firefox.exe`]
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'brave',
|
|
324
|
+
name: 'Brave',
|
|
325
|
+
paths: [`${pf}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, local ?
|
|
326
|
+
`${local}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe` : ''
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'opera',
|
|
331
|
+
name: 'Opera',
|
|
332
|
+
paths: [local ? `${local}\\Programs\\Opera\\opera.exe` : '', `${pf}\\Opera\\launcher.exe`]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: 'vivaldi',
|
|
336
|
+
name: 'Vivaldi',
|
|
337
|
+
paths: [local ? `${local}\\Vivaldi\\Application\\vivaldi.exe` : '',
|
|
338
|
+
`${pf}\\Vivaldi\\Application\\vivaldi.exe`
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: '360se',
|
|
343
|
+
name: '360 Safe Browser',
|
|
344
|
+
paths: [`${pfx86}\\360\\360se6\\Application\\360se.exe`]
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: '360chrome',
|
|
348
|
+
name: 'Chrome Browser',
|
|
349
|
+
paths: [`${pfx86}\\360\\360Chrome\\Chrome\\Application\\360chrome.exe`]
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'qqbrowser',
|
|
353
|
+
name: 'QQ Browser',
|
|
354
|
+
paths: [`${pfx86}\\Tencent\\QQBrowser\\QQBrowser.exe`, `${pf}\\Tencent\\QQBrowser\\QQBrowser.exe`]
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: 'sogou',
|
|
358
|
+
name: 'Sogou Browser',
|
|
359
|
+
paths: [`${pfx86}\\SogouExplorer\\SogouExplorer.exe`]
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'liebao',
|
|
363
|
+
name: 'Cheetah Browser',
|
|
364
|
+
paths: [`${pfx86}\\liebao\\liebao.exe`]
|
|
365
|
+
},
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function detectBrowsers() {
|
|
370
|
+
if (cachedBrowsers !== null) return cachedBrowsers
|
|
371
|
+
const found = []
|
|
372
|
+
for (const candidate of browserCandidates()) {
|
|
373
|
+
const path = (candidate.paths ?? []).find((p) => typeof p === 'string' && p.length > 0 && fs.existsSync(p))
|
|
374
|
+
if (path !== undefined) found.push({
|
|
375
|
+
id: candidate.id,
|
|
376
|
+
name: candidate.name,
|
|
377
|
+
path
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
cachedBrowsers = found
|
|
381
|
+
return found
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function apply(ctx) {
|
|
385
|
+
const store = openConfig()
|
|
386
|
+
ctx.effect(() => () => store.close(), 'dsh-notify-yimit: config close')
|
|
387
|
+
|
|
388
|
+
const MAX_SESSIONS = 200
|
|
389
|
+
const states = new Map()
|
|
390
|
+
const stateOf = (sessionId) => {
|
|
391
|
+
let st = states.get(sessionId)
|
|
392
|
+
if (st === undefined) {
|
|
393
|
+
st = {
|
|
394
|
+
title: '',
|
|
395
|
+
status: 'idle',
|
|
396
|
+
lastRunningId: null,
|
|
397
|
+
lastRunningAt: 0,
|
|
398
|
+
lastRunningText: null,
|
|
399
|
+
waitingKind: null,
|
|
400
|
+
lastFired: {}
|
|
401
|
+
}
|
|
402
|
+
states.set(sessionId, st)
|
|
403
|
+
if (states.size > MAX_SESSIONS) states.delete(states.keys().next().value)
|
|
404
|
+
}
|
|
405
|
+
return st
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let queue = []
|
|
409
|
+
let queueHead = 0
|
|
410
|
+
|
|
411
|
+
function trim() {
|
|
412
|
+
while (queue.length - queueHead > MAX_QUEUE) queueHead++
|
|
413
|
+
if (queueHead > MAX_QUEUE) {
|
|
414
|
+
queue = queue.slice(queueHead);
|
|
415
|
+
queueHead = 0
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const TOAST_HOST_SCRIPT = fileURLToPath(new URL('./toast-host.ps1',
|
|
420
|
+
import.meta.url))
|
|
421
|
+
const activeToasts = new Map()
|
|
422
|
+
const toastOrder = []
|
|
423
|
+
const pendingClose = new Set()
|
|
424
|
+
|
|
425
|
+
let globalWorkBottom = null
|
|
426
|
+
let toastSeq = 1
|
|
427
|
+
let host = null
|
|
428
|
+
let hostAlive = false
|
|
429
|
+
let hostRestartAttempts = 0
|
|
430
|
+
let hostRestartUntil = 0
|
|
431
|
+
|
|
432
|
+
function spawnHost() {
|
|
433
|
+
try {
|
|
434
|
+
const spawnFn = typeof globalThis.__DSH_NOTIFY_TEST_SPAWN__ === 'function' ? globalThis
|
|
435
|
+
.__DSH_NOTIFY_TEST_SPAWN__ : spawn
|
|
436
|
+
host = spawnFn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', TOAST_HOST_SCRIPT], {
|
|
437
|
+
windowsHide: true,
|
|
438
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
439
|
+
})
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.warn(`[dsh-notify-yimit] Toast host spawn failed: ${String(error)}`)
|
|
442
|
+
host = null;
|
|
443
|
+
hostAlive = false;
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
hostAlive = true
|
|
447
|
+
host.stdin.on('error', (err) => {
|
|
448
|
+
if (process.env.DSH_DEBUG) console.warn(`[dsh-notify-yimit] stdin error: ${err}`)
|
|
449
|
+
})
|
|
450
|
+
host.stdout.on('error', () => {})
|
|
451
|
+
host.on('error', () => {
|
|
452
|
+
hostAlive = false
|
|
453
|
+
})
|
|
454
|
+
host.stdout.setEncoding('utf8')
|
|
455
|
+
|
|
456
|
+
let buf = ''
|
|
457
|
+
host.stdout.on('data', (d) => {
|
|
458
|
+
buf += String(d)
|
|
459
|
+
let nl
|
|
460
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
461
|
+
const line = buf.slice(0, nl).trim()
|
|
462
|
+
buf = buf.slice(nl + 1)
|
|
463
|
+
if (line.length === 0) continue
|
|
464
|
+
let msg
|
|
465
|
+
try {
|
|
466
|
+
msg = JSON.parse(line)
|
|
467
|
+
} catch {
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
if (msg === null || typeof msg !== 'object') continue
|
|
471
|
+
if (msg.type === 'pos' && typeof msg.key === 'string') {
|
|
472
|
+
const rec = activeToasts.get(msg.key)
|
|
473
|
+
if (rec !== undefined && rec.instance === msg.instance) {
|
|
474
|
+
setToastMeta(rec, {
|
|
475
|
+
left: msg.left,
|
|
476
|
+
top: msg.top,
|
|
477
|
+
height: msg.height,
|
|
478
|
+
hwnd: msg.hwnd,
|
|
479
|
+
workBottom: msg.workBottom
|
|
480
|
+
})
|
|
481
|
+
reflowToasts()
|
|
482
|
+
}
|
|
483
|
+
} else if (msg.type === 'exit' && typeof msg.key === 'string') {
|
|
484
|
+
const rec = activeToasts.get(msg.key)
|
|
485
|
+
if (rec !== undefined && rec.instance === msg.instance) {
|
|
486
|
+
pendingClose.delete(msg.key)
|
|
487
|
+
activeToasts.delete(msg.key)
|
|
488
|
+
const idx = toastOrder.indexOf(msg.key)
|
|
489
|
+
if (idx !== -1) toastOrder.splice(idx, 1)
|
|
490
|
+
reflowToasts()
|
|
491
|
+
}
|
|
492
|
+
} else if (msg.type === 'geometry' && typeof msg.left === 'number' && typeof msg.top === 'number') {
|
|
493
|
+
// 编辑模式:样板窗拖拽/缩放结束 → 写回配置,并应用到在屏浮窗。
|
|
494
|
+
const width = Math.max(260, Math.min(520, Math.round(Number(msg.width) || 340)))
|
|
495
|
+
const left = Math.round(Number(msg.left))
|
|
496
|
+
const top = Math.round(Number(msg.top))
|
|
497
|
+
store.set({
|
|
498
|
+
toast: {
|
|
499
|
+
width,
|
|
500
|
+
position: { left, top }
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
for (const rec of activeToasts.values()) {
|
|
504
|
+
sendCmd({ cmd: 'size', key: rec.key, width })
|
|
505
|
+
}
|
|
506
|
+
reflowToasts()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
host.on('exit', () => {
|
|
512
|
+
hostAlive = false
|
|
513
|
+
activeToasts.clear()
|
|
514
|
+
toastOrder.length = 0
|
|
515
|
+
pendingClose.clear()
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function sendCmd(payload) {
|
|
520
|
+
if (host === null || !hostAlive) return false
|
|
521
|
+
try {
|
|
522
|
+
host.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
523
|
+
return true
|
|
524
|
+
} catch {
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function ensureHost() {
|
|
530
|
+
if (host !== null && hostAlive) return true
|
|
531
|
+
if (Date.now() < hostRestartUntil) return false
|
|
532
|
+
spawnHost()
|
|
533
|
+
if (!hostAlive) {
|
|
534
|
+
hostRestartAttempts++
|
|
535
|
+
hostRestartUntil = Date.now() + Math.min(30000, 1000 * (2 ** hostRestartAttempts))
|
|
536
|
+
} else {
|
|
537
|
+
hostRestartAttempts = 0
|
|
538
|
+
}
|
|
539
|
+
return host !== null && hostAlive
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function dismissToast(key) {
|
|
543
|
+
if (pendingClose.has(key)) return
|
|
544
|
+
const rec = activeToasts.get(key)
|
|
545
|
+
if (rec === undefined) return
|
|
546
|
+
pendingClose.add(key)
|
|
547
|
+
sendCmd({
|
|
548
|
+
cmd: 'close',
|
|
549
|
+
key
|
|
550
|
+
})
|
|
551
|
+
setTimeout(() => {
|
|
552
|
+
if (activeToasts.get(key) === rec) {
|
|
553
|
+
sendCmd({
|
|
554
|
+
cmd: 'close',
|
|
555
|
+
key
|
|
556
|
+
})
|
|
557
|
+
pendingClose.delete(key)
|
|
558
|
+
activeToasts.delete(key)
|
|
559
|
+
const idx = toastOrder.indexOf(key)
|
|
560
|
+
if (idx !== -1) toastOrder.splice(idx, 1)
|
|
561
|
+
reflowToasts()
|
|
562
|
+
} else {
|
|
563
|
+
pendingClose.delete(key)
|
|
564
|
+
}
|
|
565
|
+
}, 1500).unref?.()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function reflowToasts() {
|
|
569
|
+
const cfg = store.get()
|
|
570
|
+
const pos = cfg.toast?.position ?? null
|
|
571
|
+
const gap = typeof cfg.toast?.gap === 'number' ? cfg.toast.gap : TOAST_GAP
|
|
572
|
+
if (pos !== null) {
|
|
573
|
+
// 自定义位置:position.top 是"第一个(最新,最底部)浮窗的顶边",向上堆叠。
|
|
574
|
+
// 与宿主 Loaded 的 targetTop = pos.top - offsetY 语义一致,否则重排会把
|
|
575
|
+
// 浮窗再上移一个自身高度(先正确、随即上移的 bug)。横向同步 position.left,
|
|
576
|
+
// 编辑调整位置后,在屏浮窗立即左右移动到位。
|
|
577
|
+
let prevTop = pos.top
|
|
578
|
+
let first = true
|
|
579
|
+
for (let i = 0; i < toastOrder.length; i += 1) {
|
|
580
|
+
const rec = activeToasts.get(toastOrder[i])
|
|
581
|
+
if (rec === undefined) continue
|
|
582
|
+
const height = rec.meta?.height ?? 130
|
|
583
|
+
const newTop = first ? prevTop : Math.round(prevTop - gap - height)
|
|
584
|
+
first = false
|
|
585
|
+
const needsTop = rec.meta !== null && newTop !== rec.meta.top
|
|
586
|
+
const needsLeft = rec.meta !== null && rec.meta.left !== pos.left
|
|
587
|
+
if (needsTop || needsLeft) {
|
|
588
|
+
const move = { cmd: 'move', key: rec.key, top: newTop }
|
|
589
|
+
if (needsTop) rec.meta.top = newTop
|
|
590
|
+
if (needsLeft) { move.left = pos.left; rec.meta.left = pos.left }
|
|
591
|
+
sendCmd(move)
|
|
592
|
+
}
|
|
593
|
+
prevTop = newTop
|
|
594
|
+
}
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
// 默认右下:底边对齐(工作区底边 - 20),向上堆叠;横向回默认贴边(left 'default'),
|
|
598
|
+
// 覆盖"恢复默认位置/改宽度后需回到右下角"的场景。
|
|
599
|
+
if (globalWorkBottom === null) return
|
|
600
|
+
let cursor = globalWorkBottom - 20
|
|
601
|
+
for (let i = 0; i < toastOrder.length; i += 1) {
|
|
602
|
+
const rec = activeToasts.get(toastOrder[i])
|
|
603
|
+
if (rec === undefined) continue
|
|
604
|
+
const height = rec.meta?.height ?? 130
|
|
605
|
+
const newTop = Math.round(cursor - height)
|
|
606
|
+
if (rec.meta !== null && newTop !== rec.meta.top) {
|
|
607
|
+
rec.meta.top = newTop
|
|
608
|
+
sendCmd({
|
|
609
|
+
cmd: 'move',
|
|
610
|
+
key: rec.key,
|
|
611
|
+
top: newTop,
|
|
612
|
+
left: 'default'
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
cursor = newTop - gap
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function setToastMeta(rec, meta) {
|
|
620
|
+
rec.meta = meta
|
|
621
|
+
if (Number.isFinite(meta.workBottom)) globalWorkBottom = meta.workBottom
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const baseUrlOf = () => {
|
|
625
|
+
const ws = ctx.get('webServer')
|
|
626
|
+
return `http://127.0.0.1:${ws !== null && ws !== undefined && typeof ws.port === 'number' ? ws.port : 3080}`
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function showDesktopToast(cfg, ev) {
|
|
630
|
+
const maxVisible = Math.max(1, Math.min(20, Math.floor(Number(cfg.maxVisible) || 3)))
|
|
631
|
+
const stickyKind = ev.kind === 'running' || ev.kind === 'approval' || ev.kind === 'question'
|
|
632
|
+
const key = stickyKind ? `${ev.kind}:${ev.sessionId}` : `${ev.kind}:${ev.sessionId}:${ev.at}`
|
|
633
|
+
|
|
634
|
+
let replaceIndex = -1
|
|
635
|
+
if (stickyKind) {
|
|
636
|
+
const prev = activeToasts.get(key)
|
|
637
|
+
if (prev !== undefined && !pendingClose.has(key)) {
|
|
638
|
+
sendCmd({
|
|
639
|
+
cmd: 'text',
|
|
640
|
+
key,
|
|
641
|
+
text: clip(ev.text)
|
|
642
|
+
})
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
if (prev !== undefined) {
|
|
646
|
+
replaceIndex = toastOrder.indexOf(key)
|
|
647
|
+
if (replaceIndex !== -1) toastOrder.splice(replaceIndex, 1)
|
|
648
|
+
activeToasts.delete(key)
|
|
649
|
+
pendingClose.delete(key)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let guard = 0
|
|
654
|
+
while (activeToasts.size - pendingClose.size >= maxVisible && guard++ < 64) {
|
|
655
|
+
let first = null
|
|
656
|
+
for (const k of activeToasts.keys()) {
|
|
657
|
+
if (pendingClose.has(k)) continue
|
|
658
|
+
if (stickyKind && !/^(running|approval|question):/.test(k)) {
|
|
659
|
+
first = k;
|
|
660
|
+
break
|
|
661
|
+
}
|
|
662
|
+
if (first === null) first = k
|
|
663
|
+
}
|
|
664
|
+
if (first === null) break
|
|
665
|
+
dismissToast(first)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const colors = cfg.colors !== null && typeof cfg.colors === 'object' ? cfg.colors[ev.kind] : undefined
|
|
669
|
+
if (replaceIndex === -1) {
|
|
670
|
+
toastOrder.unshift(key)
|
|
671
|
+
} else {
|
|
672
|
+
toastOrder.splice(replaceIndex, 0, key)
|
|
673
|
+
}
|
|
674
|
+
const insertIndex = replaceIndex !== -1 ? replaceIndex : 0
|
|
675
|
+
const gap = typeof cfg.toast?.gap === 'number' ? cfg.toast.gap : TOAST_GAP
|
|
676
|
+
let offsetY = 0
|
|
677
|
+
for (let i = 0; i < insertIndex; i += 1) {
|
|
678
|
+
const r = activeToasts.get(toastOrder[i])
|
|
679
|
+
offsetY += (r?.meta?.height ?? 130) + gap
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const lang = cfg.locale === 'zh' ? 'zh' : 'en'
|
|
683
|
+
const l = TOAST_LABELS[lang]
|
|
684
|
+
|
|
685
|
+
const payload = {
|
|
686
|
+
cmd: 'show',
|
|
687
|
+
key,
|
|
688
|
+
instance: toastSeq++,
|
|
689
|
+
title: typeof ev.title === 'string' && ev.title.length > 0 ? ev.title : l.unnamed,
|
|
690
|
+
text: clip(ev.text),
|
|
691
|
+
bg: colors?.bg ?? cfg.bgColor ?? '#20272f',
|
|
692
|
+
fg: colors?.fg ?? cfg.textColor ?? '#e6edf3',
|
|
693
|
+
durationSec: Math.max(1, Math.round((Number(cfg.durationMs) || 8000) / 1000)),
|
|
694
|
+
sticky: stickyKind,
|
|
695
|
+
offsetY,
|
|
696
|
+
sessionId: String(ev.sessionId),
|
|
697
|
+
baseUrl: baseUrlOf(),
|
|
698
|
+
winTitle: `dsh-notify-yimit-${key}`,
|
|
699
|
+
ignoreLabel: l.ignore,
|
|
700
|
+
jumpLabel: l.jump,
|
|
701
|
+
unnamedLabel: l.unnamed,
|
|
702
|
+
// 通知窗口样式:宽度 + 自定义位置(null = 默认右下贴边)
|
|
703
|
+
width: cfg.toast?.width ?? 340,
|
|
704
|
+
position: cfg.toast?.position ?? null,
|
|
705
|
+
}
|
|
706
|
+
if (typeof cfg.browser === 'string' && cfg.browser.length > 0) payload.browserPath = cfg.browser
|
|
707
|
+
|
|
708
|
+
if (!ensureHost() || !sendCmd(payload)) {
|
|
709
|
+
const entry = {
|
|
710
|
+
id: String(nextId++),
|
|
711
|
+
sessionId: String(ev.sessionId),
|
|
712
|
+
title: typeof ev.title === 'string' && ev.title.length > 0 ? ev.title : l.unnamed,
|
|
713
|
+
kind: ev.kind,
|
|
714
|
+
text: clip(ev.text),
|
|
715
|
+
at: ev.at
|
|
716
|
+
}
|
|
717
|
+
queue.push(entry)
|
|
718
|
+
trim()
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
activeToasts.set(key, {
|
|
722
|
+
key,
|
|
723
|
+
instance: payload.instance,
|
|
724
|
+
meta: null,
|
|
725
|
+
offsetY,
|
|
726
|
+
sessionId: String(ev.sessionId)
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function closeToast(kind, sessionId) {
|
|
731
|
+
dismissToast(`${kind}:${sessionId}`)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** 会话标题到达后原地更新已弹浮窗的标题(宿主 title: 命令,不重弹、不闪烁)。 */
|
|
735
|
+
function updateToastTitle(sessionId, title) {
|
|
736
|
+
if (typeof title !== 'string' || title.length === 0) return
|
|
737
|
+
for (const rec of activeToasts.values()) {
|
|
738
|
+
if (rec.sessionId === sessionId) {
|
|
739
|
+
sendCmd({ cmd: 'title', key: rec.key, title })
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
ctx.effect(() => () => {
|
|
745
|
+
if (host !== null) {
|
|
746
|
+
try {
|
|
747
|
+
sendCmd({
|
|
748
|
+
cmd: 'shutdown'
|
|
749
|
+
})
|
|
750
|
+
} catch {
|
|
751
|
+
/* 忽略 */ }
|
|
752
|
+
try {
|
|
753
|
+
host.kill()
|
|
754
|
+
} catch {
|
|
755
|
+
/* 已退出 */ }
|
|
756
|
+
}
|
|
757
|
+
activeToasts.clear()
|
|
758
|
+
toastOrder.length = 0
|
|
759
|
+
pendingClose.clear()
|
|
760
|
+
}, 'dsh-notify-yimit: toast cleanup')
|
|
761
|
+
|
|
762
|
+
spawnHost()
|
|
763
|
+
let nextId = 1
|
|
764
|
+
|
|
765
|
+
const push = (sessionId, kind, text) => {
|
|
766
|
+
const st = states.get(sessionId)
|
|
767
|
+
if (st === undefined) return
|
|
768
|
+
const at = Date.now()
|
|
769
|
+
const cfg = store.get()
|
|
770
|
+
// 每类型开关:该类型被关闭时完全不产生通知(桌面浮窗与系统通知都不发)。
|
|
771
|
+
if (cfg.colors?.[kind]?.enabled === false) return
|
|
772
|
+
const desktop = cfg.enabled === true && cfg.mode === 'custom'
|
|
773
|
+
const lang = cfg.locale === 'zh' ? 'zh' : 'en'
|
|
774
|
+
const l = TOAST_LABELS[lang]
|
|
775
|
+
const entryTitle = typeof st.title === 'string' && st.title.length > 0 ? st.title : l.unnamed
|
|
776
|
+
|
|
777
|
+
if (kind === 'running') {
|
|
778
|
+
if (st.lastRunningAt !== 0 && at - st.lastRunningAt < RUNNING_THROTTLE_MS && st.lastRunningId !==
|
|
779
|
+
null) {
|
|
780
|
+
if (desktop) {
|
|
781
|
+
if (st.lastRunningText !== clip(text)) {
|
|
782
|
+
st.lastRunningText = clip(text)
|
|
783
|
+
showDesktopToast(cfg, {
|
|
784
|
+
sessionId,
|
|
785
|
+
title: st.title,
|
|
786
|
+
kind,
|
|
787
|
+
text: clip(text),
|
|
788
|
+
at
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
for (let i = queueHead; i < queue.length; i++) {
|
|
794
|
+
if (queue[i].id === st.lastRunningId) {
|
|
795
|
+
queue[i].text = clip(text);
|
|
796
|
+
queue[i].at = at;
|
|
797
|
+
break
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
st.lastRunningAt = at
|
|
803
|
+
st.lastRunningText = clip(text)
|
|
804
|
+
if (desktop) {
|
|
805
|
+
showDesktopToast(cfg, {
|
|
806
|
+
sessionId,
|
|
807
|
+
title: st.title,
|
|
808
|
+
kind,
|
|
809
|
+
text: clip(text),
|
|
810
|
+
at
|
|
811
|
+
})
|
|
812
|
+
st.lastRunningId = `desktop:${at}`
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
const entry = {
|
|
816
|
+
id: String(nextId++),
|
|
817
|
+
sessionId,
|
|
818
|
+
title: entryTitle,
|
|
819
|
+
kind,
|
|
820
|
+
text: clip(text),
|
|
821
|
+
activity: clip(text),
|
|
822
|
+
at
|
|
823
|
+
}
|
|
824
|
+
st.lastRunningId = entry.id
|
|
825
|
+
queue.push(entry)
|
|
826
|
+
trim()
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (st.lastFired === undefined) st.lastFired = {}
|
|
831
|
+
const isSticky = kind === 'approval' || kind === 'question'
|
|
832
|
+
if (!isSticky && at - (st.lastFired[kind] ?? 0) < KIND_DEBOUNCE_MS) return
|
|
833
|
+
st.lastFired[kind] = at
|
|
834
|
+
|
|
835
|
+
if (desktop) {
|
|
836
|
+
showDesktopToast(cfg, {
|
|
837
|
+
sessionId,
|
|
838
|
+
title: st.title,
|
|
839
|
+
kind,
|
|
840
|
+
text: clip(text),
|
|
841
|
+
at
|
|
842
|
+
})
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
let stale = -1
|
|
847
|
+
for (let i = queueHead; i < queue.length; i++) {
|
|
848
|
+
if (queue[i].sessionId === sessionId && queue[i].kind === kind) {
|
|
849
|
+
stale = i;
|
|
850
|
+
break
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (stale !== -1) queue.splice(stale, 1)
|
|
854
|
+
|
|
855
|
+
const entry = {
|
|
856
|
+
id: String(nextId++),
|
|
857
|
+
sessionId,
|
|
858
|
+
title: entryTitle,
|
|
859
|
+
kind,
|
|
860
|
+
text: clip(text),
|
|
861
|
+
at
|
|
862
|
+
}
|
|
863
|
+
queue.push(entry)
|
|
864
|
+
trim()
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
ctx.on('session/event', (session, event) => {
|
|
868
|
+
if (session === undefined || session === null || event === undefined || event === null) return
|
|
869
|
+
const id = session.id
|
|
870
|
+
const st = stateOf(id)
|
|
871
|
+
if (st.title.length === 0) st.title = titleOf(session)
|
|
872
|
+
|
|
873
|
+
const lang = store.get().locale === 'zh' ? 'zh' : 'en'
|
|
874
|
+
const L = ACTIVITY_LABELS[lang]
|
|
875
|
+
|
|
876
|
+
switch (event.type) {
|
|
877
|
+
case 'session/title': {
|
|
878
|
+
if (typeof event.data?.title === 'string' && event.data.title.length > 0) {
|
|
879
|
+
st.title = event.data.title
|
|
880
|
+
for (let i = queueHead; i < queue.length; i++) {
|
|
881
|
+
if (queue[i].sessionId === id && (queue[i].title.length === 0 || UNTITLED_NAMES.has(queue[i].title)))
|
|
882
|
+
queue[i].title = st.title
|
|
883
|
+
}
|
|
884
|
+
// 已弹浮窗(运行中/待审批/待回答等)标题原地更新,不再一直顶着占位/旧标题。
|
|
885
|
+
updateToastTitle(id, st.title)
|
|
886
|
+
}
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
case 'turn/start': {
|
|
890
|
+
st.status = 'running'
|
|
891
|
+
st.activity = L.start
|
|
892
|
+
push(id, 'running', st.activity)
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
case 'step/start': {
|
|
896
|
+
st.status = 'running'
|
|
897
|
+
st.activity = L.thinking
|
|
898
|
+
push(id, 'running', st.activity)
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
case 'assistant/chunk': {
|
|
902
|
+
const chunk = event.data?.chunk
|
|
903
|
+
if (chunk === null || typeof chunk !== 'object') return
|
|
904
|
+
if (chunk.type === 'reasoning' && typeof chunk.text === 'string' && chunk.text.length > 0) {
|
|
905
|
+
st.activity = L.thinking
|
|
906
|
+
push(id, 'running', st.activity)
|
|
907
|
+
} else if (chunk.type === 'text' && typeof chunk.text === 'string' && chunk.text.length > 0) {
|
|
908
|
+
st.activity = L.generating
|
|
909
|
+
push(id, 'running', st.activity)
|
|
910
|
+
}
|
|
911
|
+
return
|
|
912
|
+
}
|
|
913
|
+
case 'tool/call': {
|
|
914
|
+
const data = event.data
|
|
915
|
+
if (data === null || typeof data !== 'object') return
|
|
916
|
+
const toolName = typeof data.name === 'string' ? data.name : ''
|
|
917
|
+
st.activity = `${L.executing} ${toolName}`
|
|
918
|
+
push(id, 'running', st.activity)
|
|
919
|
+
if (toolName === 'ask_user_question' && typeof data.arguments === 'string') {
|
|
920
|
+
try {
|
|
921
|
+
const args = JSON.parse(data.arguments)
|
|
922
|
+
const questions = Array.isArray(args?.questions) ? args.questions : []
|
|
923
|
+
const first = questions.find((q) => q !== null && typeof q === 'object' && typeof q
|
|
924
|
+
.question === 'string')
|
|
925
|
+
if (first !== undefined) {
|
|
926
|
+
st.status = 'waiting'
|
|
927
|
+
st.waitingKind = 'question'
|
|
928
|
+
push(id, 'question', first.question)
|
|
929
|
+
}
|
|
930
|
+
} catch {
|
|
931
|
+
/* arguments 非 JSON */ }
|
|
932
|
+
}
|
|
933
|
+
return
|
|
934
|
+
}
|
|
935
|
+
case 'tool/result': {
|
|
936
|
+
if (st.waitingKind === 'question') {
|
|
937
|
+
closeToast('question', id)
|
|
938
|
+
st.waitingKind = null
|
|
939
|
+
st.status = 'running'
|
|
940
|
+
st.activity = L.continuing
|
|
941
|
+
push(id, 'running', st.activity)
|
|
942
|
+
} else if (st.status === 'waiting') {
|
|
943
|
+
st.status = 'running'
|
|
944
|
+
st.activity = L.continuing
|
|
945
|
+
push(id, 'running', st.activity)
|
|
946
|
+
}
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
case 'approval/decided': {
|
|
950
|
+
closeToast('approval', id)
|
|
951
|
+
if (st.waitingKind === 'approval') st.waitingKind = null
|
|
952
|
+
return
|
|
953
|
+
}
|
|
954
|
+
case 'turn/end': {
|
|
955
|
+
const reason = event.data?.reason
|
|
956
|
+
const kind = reason !== null && typeof reason === 'object' ? reason.kind : undefined
|
|
957
|
+
st.status = 'idle'
|
|
958
|
+
st.activity = ''
|
|
959
|
+
st.lastRunningAt = 0
|
|
960
|
+
st.lastRunningId = null
|
|
961
|
+
st.lastRunningText = null
|
|
962
|
+
st.waitingKind = null
|
|
963
|
+
closeToast('running', id)
|
|
964
|
+
if (kind === 'completed') {
|
|
965
|
+
push(id, 'completed', L.completed)
|
|
966
|
+
} else if (kind === 'error') {
|
|
967
|
+
const message = reason !== null && typeof reason === 'object' && reason.error !== null &&
|
|
968
|
+
typeof reason.error === 'object' ? (typeof reason.error.message === 'string' ? reason
|
|
969
|
+
.error.message : '') : ''
|
|
970
|
+
push(id, 'error', message.length > 0 ? `${L.errorPrefix}${message}` : L.errorFallback)
|
|
971
|
+
}
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
default:
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
ctx.on('agent/status', ({
|
|
980
|
+
agent,
|
|
981
|
+
status
|
|
982
|
+
}) => {
|
|
983
|
+
if (agent === undefined || agent === null) return
|
|
984
|
+
const sessionId = agent.session?.id ?? agent.id
|
|
985
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return
|
|
986
|
+
const st = stateOf(sessionId)
|
|
987
|
+
const lang = store.get().locale === 'zh' ? 'zh' : 'en'
|
|
988
|
+
const L = ACTIVITY_LABELS[lang]
|
|
989
|
+
if (status === 'running' && st.status !== 'running') {
|
|
990
|
+
st.status = 'running'
|
|
991
|
+
st.activity = L.processing
|
|
992
|
+
push(sessionId, 'running', st.activity)
|
|
993
|
+
} else if (status === 'idle' && st.status === 'running') {
|
|
994
|
+
st.status = 'idle'
|
|
995
|
+
st.lastRunningAt = 0
|
|
996
|
+
st.lastRunningId = null
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
ctx.on('approval/request', (req, next) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const sid = req?.agent?.session?.id
|
|
1003
|
+
const sessionId = (typeof sid === 'string' && sid.length > 0) ? sid : req?.agent?.id
|
|
1004
|
+
if (typeof sessionId === 'string' && sessionId.length > 0 && req !== null && typeof req ===
|
|
1005
|
+
'object') {
|
|
1006
|
+
const st = stateOf(sessionId)
|
|
1007
|
+
st.status = 'waiting'
|
|
1008
|
+
st.waitingKind = 'approval'
|
|
1009
|
+
const toolName = typeof req.toolName === 'string' ? req.toolName : ''
|
|
1010
|
+
const reason = typeof req.reason === 'string' && req.reason.length > 0 ? req.reason : ''
|
|
1011
|
+
|
|
1012
|
+
const lang = store.get().locale === 'zh' ? 'zh' : 'en'
|
|
1013
|
+
const L = ACTIVITY_LABELS[lang]
|
|
1014
|
+
push(sessionId, 'approval', reason.length > 0 ? reason : (toolName.length > 0 ?
|
|
1015
|
+
`${L.approvalPrefix} ${toolName}` : L.approvalFallback))
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
/* 观察失败不影响审批链路 */ }
|
|
1019
|
+
return next()
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
const service = {
|
|
1023
|
+
getState() {
|
|
1024
|
+
return {
|
|
1025
|
+
config: store.get(),
|
|
1026
|
+
events: queue.slice(queueHead).map((item) => ({
|
|
1027
|
+
...item
|
|
1028
|
+
}))
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
updateConfig(patch) {
|
|
1032
|
+
const prev = store.get()
|
|
1033
|
+
const next = store.set(patch)
|
|
1034
|
+
// toast 几何(位置/宽度/间距)变更:立即应用到在屏浮窗,无需等待下一个浮窗。
|
|
1035
|
+
const pToast = patch !== null && typeof patch === 'object' && !Array.isArray(patch) ? patch.toast : undefined
|
|
1036
|
+
if (pToast !== null && typeof pToast === 'object' && !Array.isArray(pToast)) {
|
|
1037
|
+
if (typeof pToast.width === 'number' && pToast.width !== prev.toast?.width) {
|
|
1038
|
+
for (const rec of activeToasts.values()) {
|
|
1039
|
+
sendCmd({ cmd: 'size', key: rec.key, width: next.toast.width })
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
reflowToasts()
|
|
1043
|
+
}
|
|
1044
|
+
return next
|
|
1045
|
+
},
|
|
1046
|
+
ackEvents(ids) {
|
|
1047
|
+
if (Array.isArray(ids)) {
|
|
1048
|
+
const acked = new Set(ids.map((x) => String(x)))
|
|
1049
|
+
for (let i = queue.length - 1; i >= queueHead; i -= 1) {
|
|
1050
|
+
if (acked.has(queue[i].id)) queue.splice(i, 1)
|
|
1051
|
+
}
|
|
1052
|
+
if (queueHead > MAX_QUEUE / 2) {
|
|
1053
|
+
queue = queue.slice(queueHead);
|
|
1054
|
+
queueHead = 0
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
ok: true
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
listBrowsers() {
|
|
1062
|
+
return {
|
|
1063
|
+
browsers: detectBrowsers()
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
/** 进入通知窗口编辑模式:宿主弹样板窗(可拖拽/缩放),几何回传后写回配置。 */
|
|
1067
|
+
startEditMode() {
|
|
1068
|
+
const cfg = store.get()
|
|
1069
|
+
const lang = cfg.locale === 'zh' ? 'zh' : 'en'
|
|
1070
|
+
sendCmd({
|
|
1071
|
+
cmd: 'edit',
|
|
1072
|
+
title: EDIT_TITLES[lang],
|
|
1073
|
+
width: cfg.toast?.width ?? 340,
|
|
1074
|
+
position: cfg.toast?.position ?? null,
|
|
1075
|
+
})
|
|
1076
|
+
return { ok: true }
|
|
1077
|
+
},
|
|
1078
|
+
/** 退出编辑模式:样板窗淡出消失。 */
|
|
1079
|
+
endEditMode() {
|
|
1080
|
+
sendCmd({ cmd: 'edit-end' })
|
|
1081
|
+
return { ok: true }
|
|
1082
|
+
},
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
Object.defineProperty(service, 'typertRemote', {
|
|
1086
|
+
configurable: false,
|
|
1087
|
+
enumerable: false,
|
|
1088
|
+
writable: false,
|
|
1089
|
+
value: {
|
|
1090
|
+
service,
|
|
1091
|
+
serviceKey: 'notify',
|
|
1092
|
+
namespace: 'notify'
|
|
1093
|
+
},
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
ctx.provide('notify', service)
|
|
1097
|
+
console.log('[dsh-notify-yimit] Loaded (disabled by default; please enable in Settings -> Notifications)')
|
|
1098
|
+
}
|