@mhfire/dsh-im-bridge 0.4.2 → 0.4.3
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.en.md +220 -217
- package/README.md +220 -217
- package/cordis.patch.yml +89 -89
- package/lib/client.js +2 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +32 -21
- package/package.json +94 -92
- package/src/client/card-controller.ts +2 -1
- package/src/client/card-form.ts +2 -6
- package/src/client/index.ts +6 -2
- package/src/index.ts +1099 -1058
- package/tsdown.client.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -1,1058 +1,1099 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-im-bridge — WeCom AI bot ⇄ DSH Agent host plugin.
|
|
3
|
-
*
|
|
4
|
-
* Function-plugin shape (`name` / `inject` / `Config` / `apply`, no default
|
|
5
|
-
* export). Messages create in-process Agents so per-chat-window sessions stay on
|
|
6
|
-
* the same Loader tree as the Web GUI. Settings register through
|
|
7
|
-
* `
|
|
8
|
-
* require a process restart to open the WebSocket.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from 'node:fs'
|
|
12
|
-
import { dirname, join } from 'node:path'
|
|
13
|
-
import { fileURLToPath } from 'node:url'
|
|
14
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
-
import z from '@deepseek-ai/schemastery'
|
|
16
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
17
|
-
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
18
|
-
import { installModelSelection } from '@deepseek-ai/dsh-agent'
|
|
19
|
-
|
|
20
|
-
import {
|
|
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
|
-
export const
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
export const
|
|
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
|
-
|
|
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
|
-
interface
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface
|
|
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
|
-
interface
|
|
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
|
-
interface
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
interface
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
interface
|
|
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
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
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
|
-
return
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
function
|
|
436
|
-
if (
|
|
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
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
console.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
(
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
ws.
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-im-bridge — WeCom AI bot ⇄ DSH Agent host plugin.
|
|
3
|
+
*
|
|
4
|
+
* Function-plugin shape (`name` / `inject` / `Config` / `apply`, no default
|
|
5
|
+
* export). Messages create in-process Agents so per-chat-window sessions stay on
|
|
6
|
+
* the same Loader tree as the Web GUI. Settings register through
|
|
7
|
+
* `ctx.settings.installSection`; live fields read `source()`, credentials still
|
|
8
|
+
* require a process restart to open the WebSocket.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
+
import z from '@deepseek-ai/schemastery'
|
|
16
|
+
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
|
17
|
+
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
18
|
+
import { installModelSelection, type ModelSelection } from '@deepseek-ai/dsh-agent'
|
|
19
|
+
// Type-only: the ctx.settings Context merge; the section installs through the service.
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-settings'
|
|
21
|
+
// Type-only: the 'session/title' SessionEventMap merge this bridge reads and appends.
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-session-title'
|
|
23
|
+
import {
|
|
24
|
+
collectReplyPngs,
|
|
25
|
+
resolveChatId,
|
|
26
|
+
sendCollectedPngs,
|
|
27
|
+
} from './reply-images.ts'
|
|
28
|
+
import {
|
|
29
|
+
isLegacyPinnedWecomTitle,
|
|
30
|
+
planWecomBind,
|
|
31
|
+
resolveWecomSession,
|
|
32
|
+
stripBotMention,
|
|
33
|
+
wecomDisplayTitle,
|
|
34
|
+
WecomSessionReject,
|
|
35
|
+
type WecomSessionRef,
|
|
36
|
+
} from './session-key.ts'
|
|
37
|
+
import {
|
|
38
|
+
ALLOW_FROM_REQUIRED_MESSAGE,
|
|
39
|
+
IM_BRIDGE_RPC_CHANNEL,
|
|
40
|
+
INSTALL_SKILLS_ENDPOINT,
|
|
41
|
+
SKILLS_RPC_UNAVAILABLE_MESSAGE,
|
|
42
|
+
WECOM_CLI_NO_OFFICE_PROMPT,
|
|
43
|
+
WECOM_CLI_PROMPT,
|
|
44
|
+
WECOM_CLI_TOOL_NAME,
|
|
45
|
+
WORKSPACE_WECOMCLI_LEAK_MESSAGE,
|
|
46
|
+
authInitHint,
|
|
47
|
+
countWecomcliSkills,
|
|
48
|
+
countWorkspaceWecomcliLeaks,
|
|
49
|
+
ensureConfigDir,
|
|
50
|
+
ensureOnPath,
|
|
51
|
+
installOfficialWecomSkills,
|
|
52
|
+
loadWecomSkills,
|
|
53
|
+
probeAuth,
|
|
54
|
+
registerWecomCliTool,
|
|
55
|
+
registerWecomOfficeSkills,
|
|
56
|
+
resolveConfigDir,
|
|
57
|
+
resolveSkillsDir,
|
|
58
|
+
resolveWecomBin,
|
|
59
|
+
senderHasOfficeAccess,
|
|
60
|
+
shouldEnableWecomCli,
|
|
61
|
+
shouldInjectWecomOfficeSkills,
|
|
62
|
+
skillsInstallHint,
|
|
63
|
+
trySeedAuth,
|
|
64
|
+
type InstallWecomSkillsResult,
|
|
65
|
+
type WecomSkill,
|
|
66
|
+
} from './wecom-cli.ts'
|
|
67
|
+
import {
|
|
68
|
+
DEFAULT_THINKING,
|
|
69
|
+
footerOf,
|
|
70
|
+
fmtDuration,
|
|
71
|
+
labelTool,
|
|
72
|
+
pickStatusLine,
|
|
73
|
+
sendFinal,
|
|
74
|
+
startThinking,
|
|
75
|
+
streamPhaseFromChunk,
|
|
76
|
+
truncate,
|
|
77
|
+
type ThinkingConfig,
|
|
78
|
+
} from './wecom.ts'
|
|
79
|
+
|
|
80
|
+
/** Package root (persona files live beside package.json). */
|
|
81
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
82
|
+
/** Built-in Chinese persona. */
|
|
83
|
+
const DEFAULT_PERSONA_ZH = join(PACKAGE_ROOT, 'persona.default.md')
|
|
84
|
+
/** Built-in English persona. */
|
|
85
|
+
const DEFAULT_PERSONA_EN = join(PACKAGE_ROOT, 'persona.default.en.md')
|
|
86
|
+
/** Host locale settings namespace (`dsh-client-locale`). */
|
|
87
|
+
const LOCALE_SETTINGS_NS = 'locale'
|
|
88
|
+
|
|
89
|
+
/** Settings namespace paired with the browser card. */
|
|
90
|
+
export const IM_BRIDGE_NS = 'im-bridge'
|
|
91
|
+
|
|
92
|
+
/** Cordis diagnostic name. */
|
|
93
|
+
export const name = 'im-bridge'
|
|
94
|
+
|
|
95
|
+
/** Required host services. */
|
|
96
|
+
export const inject = ['agents', 'sessions', 'agentDefaultModel']
|
|
97
|
+
|
|
98
|
+
const ThinkingPhase = z.object({
|
|
99
|
+
atSec: z.number(),
|
|
100
|
+
text: z.string(),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const ThinkingSchema = z.object({
|
|
104
|
+
phases: z.array(ThinkingPhase).default(DEFAULT_THINKING.phases),
|
|
105
|
+
spin: z.array(String).default(DEFAULT_THINKING.spin),
|
|
106
|
+
eggs: z.array(String).default(DEFAULT_THINKING.eggs),
|
|
107
|
+
eggAfterSec: z.number().default(DEFAULT_THINKING.eggAfterSec),
|
|
108
|
+
intervalMs: z.number().default(DEFAULT_THINKING.intervalMs),
|
|
109
|
+
activityPrefix: z.string().default(DEFAULT_THINKING.activityPrefix),
|
|
110
|
+
toolLabels: z.dict(String).default(DEFAULT_THINKING.toolLabels),
|
|
111
|
+
reasoningStatus: z.array(String).default(DEFAULT_THINKING.reasoningStatus),
|
|
112
|
+
outputStatus: z.array(String).default(DEFAULT_THINKING.outputStatus),
|
|
113
|
+
reasoningSpin: z.array(String).default(DEFAULT_THINKING.reasoningSpin),
|
|
114
|
+
outputSpin: z.array(String).default(DEFAULT_THINKING.outputSpin),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const WecomCliSchema = z.object({
|
|
118
|
+
enabled: z.boolean().default(false),
|
|
119
|
+
skillsDir: z.string().default(''),
|
|
120
|
+
configDir: z.string().default(''),
|
|
121
|
+
allowFrom: z.array(String).default([]),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Optional wecom-cli office skills (mail, calendar, docs, …). */
|
|
125
|
+
export interface WecomCliConfig {
|
|
126
|
+
/** Explicit opt-in; off by default because it shares the authorized identity. */
|
|
127
|
+
enabled: boolean
|
|
128
|
+
/** Skills root; empty = `$DSH_HOME/wecom-cli-skills`. */
|
|
129
|
+
skillsDir: string
|
|
130
|
+
/** Credential directory; empty = `<workspace>/.dsh/wecom-cli`. */
|
|
131
|
+
configDir: string
|
|
132
|
+
/** Office-command userid list; empty skips PATH / auth. Independent of chat `allowFrom`. */
|
|
133
|
+
allowFrom: string[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Plugin config: secrets come from the profile patch or Settings. */
|
|
137
|
+
export interface Config {
|
|
138
|
+
botId: string
|
|
139
|
+
secret: string
|
|
140
|
+
workspace: string
|
|
141
|
+
allowFrom: string[]
|
|
142
|
+
startHint: string
|
|
143
|
+
agentTimeoutSec: number
|
|
144
|
+
agentPreset: string
|
|
145
|
+
provider: string
|
|
146
|
+
model: string
|
|
147
|
+
reasoningEffort: string
|
|
148
|
+
persona: string
|
|
149
|
+
personaFile: string
|
|
150
|
+
maxReplyBytes: number
|
|
151
|
+
thinking: ThinkingConfig
|
|
152
|
+
deniedMessage: string
|
|
153
|
+
welcomeMessage: string
|
|
154
|
+
wecomCli: WecomCliConfig
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Schemastery schema for the composition entry and settings namespace. */
|
|
158
|
+
export const Config: z<Config> = z.object({
|
|
159
|
+
botId: z.string().default('').role('secret'),
|
|
160
|
+
secret: z.string().default('').role('secret'),
|
|
161
|
+
workspace: z.string().default(process.cwd()),
|
|
162
|
+
allowFrom: z.array(String).default([]),
|
|
163
|
+
startHint: z.string().default('🧠 正在思考...'),
|
|
164
|
+
agentTimeoutSec: z.number().default(600),
|
|
165
|
+
agentPreset: z.string().default('standard'),
|
|
166
|
+
provider: z.string().default(''),
|
|
167
|
+
model: z.string().default(''),
|
|
168
|
+
reasoningEffort: z.string().default(''),
|
|
169
|
+
persona: z.string().default(''),
|
|
170
|
+
personaFile: z.string().default(''),
|
|
171
|
+
maxReplyBytes: z.number().default(20000),
|
|
172
|
+
thinking: ThinkingSchema.default(DEFAULT_THINKING),
|
|
173
|
+
deniedMessage: z.string().default('无权访问本服务'),
|
|
174
|
+
welcomeMessage: z.string().default('👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'),
|
|
175
|
+
wecomCli: WecomCliSchema.default({ enabled: false, skillsDir: '', configDir: '', allowFrom: [] }),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
interface LoggedEvent {
|
|
179
|
+
seq: number
|
|
180
|
+
type: string
|
|
181
|
+
/** Per-type payload; each reader narrows it to the event it handles. */
|
|
182
|
+
data: unknown
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface TextBlock {
|
|
186
|
+
type: string
|
|
187
|
+
text?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface AssistantMessageData {
|
|
191
|
+
message?: { content?: TextBlock[] }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface ToolCallData {
|
|
195
|
+
name?: string
|
|
196
|
+
callId?: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface ToolResultData {
|
|
200
|
+
error?: unknown
|
|
201
|
+
message?: { source?: { callId?: string } }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface ChunkData {
|
|
205
|
+
chunk?: { type?: string; blockType?: string }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface LiveAgent {
|
|
209
|
+
whenIdle(): Promise<void>
|
|
210
|
+
followup(message: unknown): void
|
|
211
|
+
ctx: Context
|
|
212
|
+
session: {
|
|
213
|
+
seq: number
|
|
214
|
+
snapshotEvents(): readonly LoggedEvent[]
|
|
215
|
+
header?: { cwd?: string; agentPreset?: string }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
interface ChatState {
|
|
220
|
+
agent?: LiveAgent
|
|
221
|
+
sessionId?: string
|
|
222
|
+
kind?: 'single' | 'group'
|
|
223
|
+
/** True when the current inbound sender is on `wecomCli.allowFrom`. */
|
|
224
|
+
office: boolean
|
|
225
|
+
/** Prompt sections already registered on this Agent. */
|
|
226
|
+
wecomPromptInstalled: boolean
|
|
227
|
+
/** wecomcli-* already registered on this Agent. */
|
|
228
|
+
officeSkillsRegistered: boolean
|
|
229
|
+
/** The gated `wecom_cli` tool already registered on this Agent. */
|
|
230
|
+
officeToolRegistered: boolean
|
|
231
|
+
queue: Promise<unknown>
|
|
232
|
+
lastActivity: string
|
|
233
|
+
activityClearAt: number
|
|
234
|
+
lastToolByCallId: Map<string, string>
|
|
235
|
+
modelStreamPhase: 'idle' | 'reasoning' | 'outputting'
|
|
236
|
+
streamStatusTick: number
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Placeholder Map value so later messages on the same key share one queue. */
|
|
240
|
+
function emptyChatState(): ChatState {
|
|
241
|
+
return {
|
|
242
|
+
office: false,
|
|
243
|
+
wecomPromptInstalled: false,
|
|
244
|
+
officeSkillsRegistered: false,
|
|
245
|
+
officeToolRegistered: false,
|
|
246
|
+
queue: Promise.resolve(),
|
|
247
|
+
lastActivity: '',
|
|
248
|
+
activityClearAt: 0,
|
|
249
|
+
lastToolByCallId: new Map(),
|
|
250
|
+
modelStreamPhase: 'idle',
|
|
251
|
+
streamStatusTick: 0,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Duck-typed Connection RPC result (no apiproxy import). */
|
|
256
|
+
type SkillsRpcResult =
|
|
257
|
+
| { ok: true; value: InstallWecomSkillsResult }
|
|
258
|
+
| { ok: false; error: { code: 'internal' | 'cancelled'; message: string; details: Record<string, never> } }
|
|
259
|
+
|
|
260
|
+
/** Optional Host Connection used by the Settings install button. */
|
|
261
|
+
interface HostConnectionRpc {
|
|
262
|
+
handle(
|
|
263
|
+
channel: string,
|
|
264
|
+
handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<SkillsRpcResult>,
|
|
265
|
+
options: { authority: 'loopback' },
|
|
266
|
+
): () => Promise<void>
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface DefaultModel {
|
|
270
|
+
currentSelection(): ModelSelection
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface AgentRegistry {
|
|
274
|
+
get(id: ReturnType<typeof SessionId>): LiveAgent | undefined
|
|
275
|
+
create(options: {
|
|
276
|
+
sessionId: ReturnType<typeof SessionId>
|
|
277
|
+
meta?: { cwd?: string; agentPreset?: string }
|
|
278
|
+
agentOptions?: { provider: string; model: string }
|
|
279
|
+
setup?: (agentCtx: Context) => void | Promise<void>
|
|
280
|
+
}): Promise<{ agent: LiveAgent }>
|
|
281
|
+
resume(options: {
|
|
282
|
+
resumeSessionId: ReturnType<typeof SessionId>
|
|
283
|
+
agentOptions?: { provider: string; model: string }
|
|
284
|
+
setup?: (agentCtx: Context) => void | Promise<void>
|
|
285
|
+
}): Promise<{ agent: LiveAgent }>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface SessionPersistenceHeader {
|
|
289
|
+
id: string
|
|
290
|
+
cwd?: string
|
|
291
|
+
agentPreset?: string
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface SessionPersistence {
|
|
295
|
+
list(): Promise<SessionPersistenceHeader[]>
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface WorkspaceRegistry {
|
|
299
|
+
readonly archivedSessionIds: readonly string[]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface SessionStore {
|
|
303
|
+
flush(session: LiveAgent['session']): Promise<void>
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface SessionTitleSnapshot {
|
|
307
|
+
title: string
|
|
308
|
+
source: { kind: string }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface SessionTitleService {
|
|
312
|
+
get(session: LiveAgent['session']): SessionTitleSnapshot | undefined
|
|
313
|
+
refresh(session: LiveAgent['session']): Promise<unknown>
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
interface TitledSession {
|
|
317
|
+
id: string
|
|
318
|
+
snapshotEvents(): readonly LoggedEvent[]
|
|
319
|
+
append(type: 'session/title', data: {
|
|
320
|
+
title: string
|
|
321
|
+
messageSeqs: number[]
|
|
322
|
+
source: unknown
|
|
323
|
+
}): void
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface SessionTitleEventData {
|
|
327
|
+
title?: string
|
|
328
|
+
messageSeqs?: number[]
|
|
329
|
+
source?: { kind?: string }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
interface AgentPresets {
|
|
333
|
+
resolve(id: string): Promise<{ id: string }>
|
|
334
|
+
mount(agentCtx: Context, id: string): Promise<unknown>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface SystemPromptService {
|
|
338
|
+
section(entry: { name: string; order: number; text: () => string }): unknown
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Caller-bound prompt registry on an Agent context (not a raw `get()` result). */
|
|
342
|
+
interface PromptHost {
|
|
343
|
+
get(name: string): unknown
|
|
344
|
+
systemPrompt?: SystemPromptService
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The read face persona / locale resolution needs. */
|
|
348
|
+
interface SettingsReader {
|
|
349
|
+
get(ns: string): unknown
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Hooks `installSection` drives at attach, at each committed change, and at detach. */
|
|
353
|
+
interface SettingsSectionHooks<T> {
|
|
354
|
+
setSource(current: () => T): void
|
|
355
|
+
onChange(): void
|
|
356
|
+
validate?(value: T): void
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** `ctx.settings` members this plugin uses, duck-typed like the other host services. */
|
|
360
|
+
interface SettingsService extends SettingsReader {
|
|
361
|
+
installSection<T>(
|
|
362
|
+
owner: Context,
|
|
363
|
+
ns: string,
|
|
364
|
+
schema: z<T>,
|
|
365
|
+
entry: T,
|
|
366
|
+
hooks: SettingsSectionHooks<T>,
|
|
367
|
+
): void
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface LoaderTree {
|
|
371
|
+
await(): Promise<void>
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface WecomFrame {
|
|
375
|
+
body?: {
|
|
376
|
+
text?: { content?: string }
|
|
377
|
+
sender?: { userid?: string }
|
|
378
|
+
from?: { userid?: string }
|
|
379
|
+
userid?: string
|
|
380
|
+
chatid?: string
|
|
381
|
+
chattype?: string | number
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface WecomClient {
|
|
386
|
+
replyStream(frame: unknown, streamId: string, content: string, finish: boolean): Promise<unknown>
|
|
387
|
+
replyWelcome(frame: unknown, payload: { msgtype: string; text: { content: string } }): Promise<unknown>
|
|
388
|
+
uploadMedia(
|
|
389
|
+
fileBuffer: Buffer,
|
|
390
|
+
options: { type: string; filename: string },
|
|
391
|
+
): Promise<{ media_id?: string; mediaId?: string }>
|
|
392
|
+
sendMediaMessage(chatid: string, mediaType: string, mediaId: string): Promise<unknown>
|
|
393
|
+
connect(): void
|
|
394
|
+
close?(): void
|
|
395
|
+
on(event: string, handler: (...args: never[]) => void): void
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Join assistant text from one turn starting at `firstSeq`. */
|
|
399
|
+
function summarize(events: readonly LoggedEvent[], firstSeq: number): { text: string; reason: unknown } {
|
|
400
|
+
let started = false
|
|
401
|
+
let text = ''
|
|
402
|
+
let reason: unknown
|
|
403
|
+
for (const event of events) {
|
|
404
|
+
if (event.seq < firstSeq) continue
|
|
405
|
+
if (event.type === 'turn/start') { started = true; continue }
|
|
406
|
+
if (!started) continue
|
|
407
|
+
if (event.type === 'assistant/message') {
|
|
408
|
+
const message = (event.data as AssistantMessageData).message
|
|
409
|
+
const joined = (message?.content ?? [])
|
|
410
|
+
.filter((block) => block.type === 'text')
|
|
411
|
+
.map((block) => block.text ?? '')
|
|
412
|
+
.join('')
|
|
413
|
+
if (joined !== '') text = joined
|
|
414
|
+
}
|
|
415
|
+
if (event.type === 'turn/end') reason = (event.data as { reason?: unknown }).reason
|
|
416
|
+
}
|
|
417
|
+
return { text, reason }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Payload of the log's last `session/title` event — the title in force now.
|
|
422
|
+
* @param session - live session whose log to fold.
|
|
423
|
+
* @returns the payload, or undefined when the session has no title event.
|
|
424
|
+
*/
|
|
425
|
+
function latestTitleData(session: TitledSession): SessionTitleEventData | undefined {
|
|
426
|
+
const events = session.snapshotEvents()
|
|
427
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
428
|
+
const event = events[i]
|
|
429
|
+
if (event.type === 'session/title') return event.data as SessionTitleEventData
|
|
430
|
+
}
|
|
431
|
+
return undefined
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Read Host `locale.preference`; missing or unknown falls back to `zh`. */
|
|
435
|
+
function readLocalePreference(settings: SettingsReader | undefined): 'zh' | 'en' {
|
|
436
|
+
if (settings === undefined) return 'zh'
|
|
437
|
+
try {
|
|
438
|
+
const section = settings.get(LOCALE_SETTINGS_NS)
|
|
439
|
+
const pref = section && typeof section === 'object' && 'preference' in section
|
|
440
|
+
? (section as { preference?: unknown }).preference
|
|
441
|
+
: undefined
|
|
442
|
+
return pref === 'en' ? 'en' : 'zh'
|
|
443
|
+
} catch {
|
|
444
|
+
return 'zh'
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Strip leading `#` comment lines from a built-in persona file. */
|
|
449
|
+
function stripLeadingHashComments(text: string): string {
|
|
450
|
+
const lines = text.split(/\r?\n/)
|
|
451
|
+
let i = 0
|
|
452
|
+
while (i < lines.length && /^\s*#/.test(lines[i] ?? '')) i++
|
|
453
|
+
while (i < lines.length && (lines[i] ?? '').trim() === '') i++
|
|
454
|
+
return lines.slice(i).join('\n')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Resolve persona: personaFile → persona string → built-in locale file. */
|
|
458
|
+
function resolvePersona(config: Config, settings: SettingsReader | undefined): string {
|
|
459
|
+
if (config.personaFile) {
|
|
460
|
+
try {
|
|
461
|
+
return readFileSync(config.personaFile, 'utf8')
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
464
|
+
console.error(`[im-bridge] 读取 personaFile 失败: ${message}`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (config.persona !== '') return config.persona
|
|
468
|
+
const file = readLocalePreference(settings) === 'en' ? DEFAULT_PERSONA_EN : DEFAULT_PERSONA_ZH
|
|
469
|
+
try {
|
|
470
|
+
return stripLeadingHashComments(readFileSync(file, 'utf8'))
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
473
|
+
console.error(`[im-bridge] 读取默认人设失败: ${message}`)
|
|
474
|
+
return ''
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Resolve the model for a new WeCom chat session. Both provider and model must be
|
|
480
|
+
* non-empty to override; otherwise fall back to agent-default-model.
|
|
481
|
+
*/
|
|
482
|
+
function resolveSelection(
|
|
483
|
+
config: Config,
|
|
484
|
+
defaultModel: DefaultModel,
|
|
485
|
+
): ModelSelection {
|
|
486
|
+
const provider = config.provider.trim()
|
|
487
|
+
const model = config.model.trim()
|
|
488
|
+
if (provider !== '' && model !== '') {
|
|
489
|
+
const effort = config.reasoningEffort.trim()
|
|
490
|
+
return effort === ''
|
|
491
|
+
? { provider, model }
|
|
492
|
+
: { provider, model, reasoningEffort: ReasoningEffortId(effort) }
|
|
493
|
+
}
|
|
494
|
+
if (provider !== '' || model !== '') {
|
|
495
|
+
console.warn('[im-bridge] provider/model 需同时填写才覆盖企微模型, 已回退 agent-default-model。')
|
|
496
|
+
}
|
|
497
|
+
return defaultModel.currentSelection()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Read a Host service this bridge cannot run without.
|
|
502
|
+
* @param ctx - host plugin context.
|
|
503
|
+
* @param name - service name in the global service store.
|
|
504
|
+
* @returns the service value.
|
|
505
|
+
* @throws when the composition does not provide the service.
|
|
506
|
+
*/
|
|
507
|
+
function requireService<T>(ctx: Context, name: string): T {
|
|
508
|
+
const service = ctx.get(name) as T | undefined
|
|
509
|
+
if (service === undefined) throw new Error(`im-bridge: 需要 ${name} 服务`)
|
|
510
|
+
return service
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Mount the WeCom bridge: settings namespace, then a deferred WebSocket after Loader settle.
|
|
515
|
+
* @param ctx - host plugin context.
|
|
516
|
+
* @param config - composition entry used as the settings `base` layer.
|
|
517
|
+
*/
|
|
518
|
+
export function apply(ctx: Context, config: Config): void {
|
|
519
|
+
const agents = requireService<AgentRegistry>(ctx, 'agents')
|
|
520
|
+
const sessions = requireService<SessionStore>(ctx, 'sessions')
|
|
521
|
+
const defaultModel = requireService<DefaultModel>(ctx, 'agentDefaultModel')
|
|
522
|
+
|
|
523
|
+
let source = (): Config => config
|
|
524
|
+
let settings: SettingsReader | undefined
|
|
525
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
526
|
+
const provider = settingsCtx.settings as SettingsService
|
|
527
|
+
// The owner is this plugin's own ctx, not the injected one: the provider
|
|
528
|
+
// reads it to tell its own detach from this plugin unloading.
|
|
529
|
+
provider.installSection(ctx, IM_BRIDGE_NS, Config, config, {
|
|
530
|
+
setSource: (current) => { source = current },
|
|
531
|
+
onChange: () => {
|
|
532
|
+
// Live fields are read through source() on the next handle/ensureAgent.
|
|
533
|
+
// botId/secret still require a process restart to open the WebSocket.
|
|
534
|
+
},
|
|
535
|
+
})
|
|
536
|
+
settings = provider
|
|
537
|
+
settingsCtx.effect(() => () => { settings = undefined }, 'im-bridge: settings reader')
|
|
538
|
+
})
|
|
539
|
+
const cfg = (): Config => source()
|
|
540
|
+
|
|
541
|
+
const chats = new Map<string, ChatState>()
|
|
542
|
+
let wecomCliReady = false
|
|
543
|
+
let officeSkills: WecomSkill[] = []
|
|
544
|
+
/** Launcher and credential directory the gated tool spawns with; set once wecom-cli is usable. */
|
|
545
|
+
let officeCli: { binJs: string; configDir: string } | undefined
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Install the office layer on one Agent: wecomcli-* skills and the gated
|
|
549
|
+
* `wecom_cli` tool. Both register through the Agent's own context, so a group
|
|
550
|
+
* chat or the GUI never sees them. Idempotent per Agent, and retried on each
|
|
551
|
+
* inbound message because skills can arrive from the Settings button later.
|
|
552
|
+
*/
|
|
553
|
+
function installOfficeLayer(agentCtx: Context, st: ChatState): void {
|
|
554
|
+
if (!wecomCliReady) return
|
|
555
|
+
if (!shouldInjectWecomOfficeSkills(st.kind, st.office)) return
|
|
556
|
+
if (!st.officeSkillsRegistered) {
|
|
557
|
+
const count = registerWecomOfficeSkills(agentCtx, officeSkills)
|
|
558
|
+
if (count > 0) {
|
|
559
|
+
st.officeSkillsRegistered = true
|
|
560
|
+
console.log(`[im-bridge] 已在该 Agent 注册 ${String(count)} 个 wecomcli-*`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (st.officeToolRegistered || officeCli === undefined) return
|
|
564
|
+
try {
|
|
565
|
+
st.officeToolRegistered = registerWecomCliTool(agentCtx, officeCli.binJs, officeCli.configDir)
|
|
566
|
+
if (st.officeToolRegistered) {
|
|
567
|
+
console.log(`[im-bridge] 已在该 Agent 注册 ${WECOM_CLI_TOOL_NAME} 工具`)
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
571
|
+
console.error(`[im-bridge] 注册 ${WECOM_CLI_TOOL_NAME} 失败: ${message}`)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
ctx.inject(['connection'], (bound) => {
|
|
576
|
+
const rpc = (bound.get('connection') as { rpc?: HostConnectionRpc } | undefined)?.rpc
|
|
577
|
+
if (rpc === undefined) {
|
|
578
|
+
console.warn(`[im-bridge] ${SKILLS_RPC_UNAVAILABLE_MESSAGE}`)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
bound.effect(
|
|
582
|
+
() => rpc.handle(IM_BRIDGE_RPC_CHANNEL, async (endpoint, _payload, signal) => {
|
|
583
|
+
if (endpoint !== INSTALL_SKILLS_ENDPOINT) {
|
|
584
|
+
return {
|
|
585
|
+
ok: false,
|
|
586
|
+
error: { code: 'internal', message: `unknown endpoint ${endpoint}`, details: {} },
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (signal.aborted) {
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
error: { code: 'cancelled', message: '安装已取消', details: {} },
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const dest = resolveSkillsDir(cfg().wecomCli.skillsDir, cfg().workspace)
|
|
597
|
+
const result = await installOfficialWecomSkills(dest, { signal })
|
|
598
|
+
officeSkills = loadWecomSkills(result.dest).filter(skill => skill.name.startsWith('wecomcli-'))
|
|
599
|
+
for (const st of chats.values()) {
|
|
600
|
+
if (st.agent === undefined) continue
|
|
601
|
+
installOfficeLayer(st.agent.ctx, st)
|
|
602
|
+
}
|
|
603
|
+
console.log(`[im-bridge] 已安装 ${String(result.count)} 个 wecomcli-* 到 ${result.dest}`)
|
|
604
|
+
return { ok: true, value: result }
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
607
|
+
return { ok: false, error: { code: 'internal', message, details: {} } }
|
|
608
|
+
}
|
|
609
|
+
}, { authority: 'loopback' }),
|
|
610
|
+
'im-bridge: wecomcli.installSkills',
|
|
611
|
+
)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
void (async () => {
|
|
615
|
+
const loader = ctx.get('loader') as LoaderTree | undefined
|
|
616
|
+
await loader?.await()
|
|
617
|
+
const { botId, secret } = cfg()
|
|
618
|
+
if (!botId || !secret) {
|
|
619
|
+
console.warn(
|
|
620
|
+
'[im-bridge] 跳过启动: 缺少 botId/secret。请在 profile cordis.patch.yml 或 Settings → 插件配置中填写后重启。',
|
|
621
|
+
)
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const wecomCli = cfg().wecomCli
|
|
626
|
+
if (wecomCli.enabled) {
|
|
627
|
+
const skillsDir = resolveSkillsDir(wecomCli.skillsDir, cfg().workspace)
|
|
628
|
+
officeSkills = loadWecomSkills(skillsDir).filter(skill => skill.name.startsWith('wecomcli-'))
|
|
629
|
+
const wecomcliCount = countWecomcliSkills(officeSkills)
|
|
630
|
+
if (wecomcliCount === 0) {
|
|
631
|
+
console.warn(
|
|
632
|
+
`[im-bridge] 未找到 wecomcli-* skills(${skillsDir})。${skillsInstallHint(skillsDir)}`,
|
|
633
|
+
)
|
|
634
|
+
} else {
|
|
635
|
+
console.log(
|
|
636
|
+
`[im-bridge] 已从 ${skillsDir} 加载 ${String(wecomcliCount)} 个 wecomcli-*,将在办公单聊 Agent 上注册`,
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
const leakCount = countWorkspaceWecomcliLeaks(cfg().workspace)
|
|
640
|
+
if (leakCount > 0) {
|
|
641
|
+
console.warn(`[im-bridge] ${WORKSPACE_WECOMCLI_LEAK_MESSAGE}(${String(leakCount)})`)
|
|
642
|
+
}
|
|
643
|
+
if (!shouldEnableWecomCli(true, wecomCli.allowFrom)) {
|
|
644
|
+
console.warn(`[im-bridge] ${ALLOW_FROM_REQUIRED_MESSAGE}`)
|
|
645
|
+
} else {
|
|
646
|
+
wecomCliReady = true
|
|
647
|
+
const binJs = resolveWecomBin()
|
|
648
|
+
if (binJs === undefined) {
|
|
649
|
+
console.warn('[im-bridge] 未找到 @wecom/cli 二进制,办公命令不可用。请确认插件依赖已安装。')
|
|
650
|
+
} else {
|
|
651
|
+
const configDir = ensureConfigDir(resolveConfigDir(wecomCli.configDir, cfg().workspace))
|
|
652
|
+
officeCli = { binJs, configDir }
|
|
653
|
+
console.log(`[im-bridge] wecom-cli 凭证目录: ${configDir}`)
|
|
654
|
+
const pathResult = ensureOnPath()
|
|
655
|
+
console.log(
|
|
656
|
+
`[im-bridge] PATH 上的 wecom-cli 已改为拒绝执行: ${pathResult.shimDir}${pathResult.shadowed ? '(已遮蔽另一个 wecom-cli)' : ''}`,
|
|
657
|
+
)
|
|
658
|
+
let status = await probeAuth(binJs, configDir)
|
|
659
|
+
if (status === 'unauthorized') {
|
|
660
|
+
if ((await trySeedAuth(binJs, cfg().botId, cfg().secret, configDir)) === undefined) {
|
|
661
|
+
status = await probeAuth(binJs, configDir)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (status === 'authorized') {
|
|
665
|
+
console.log('[im-bridge] wecom-cli 已授权')
|
|
666
|
+
} else if (status === 'unauthorized') {
|
|
667
|
+
console.warn(
|
|
668
|
+
`[im-bridge] wecom-cli 未能用 botId/secret 完成授权。请在 host 上执行 ${authInitHint(configDir)}(输入同一套密钥,不要全局安装)。`,
|
|
669
|
+
)
|
|
670
|
+
} else {
|
|
671
|
+
console.warn('[im-bridge] wecom-cli auth show 失败。')
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Add the channel prefix to a title the Host generated. `session/event`
|
|
679
|
+
* runs inside the append publication window, which refuses a reentrant
|
|
680
|
+
* append, so the prefixed title goes out in a microtask and re-reads the
|
|
681
|
+
* log first: an already prefixed tail (including the one this appends)
|
|
682
|
+
* stops the chain.
|
|
683
|
+
*/
|
|
684
|
+
function prefixWecomTitle(session: TitledSession, st: ChatState): void {
|
|
685
|
+
const kind = st.kind
|
|
686
|
+
if (kind === undefined) return
|
|
687
|
+
queueMicrotask(() => {
|
|
688
|
+
const data = latestTitleData(session)
|
|
689
|
+
if (data === undefined) return
|
|
690
|
+
// An explicit GUI rename is pinned on purpose; only automatic titles get labelled.
|
|
691
|
+
if (data.source?.kind === 'user') return
|
|
692
|
+
const raw = typeof data.title === 'string' ? data.title : ''
|
|
693
|
+
const next = wecomDisplayTitle(kind, raw)
|
|
694
|
+
if (next === raw) return
|
|
695
|
+
const messageSeqs = Array.isArray(data.messageSeqs)
|
|
696
|
+
? data.messageSeqs.filter((seq) => typeof seq === 'number')
|
|
697
|
+
: []
|
|
698
|
+
// A non-user title must cite at least one user/message seq, or the
|
|
699
|
+
// session-title invariant rejects the append.
|
|
700
|
+
if (messageSeqs.length === 0) return
|
|
701
|
+
try {
|
|
702
|
+
session.append('session/title', {
|
|
703
|
+
title: next,
|
|
704
|
+
messageSeqs,
|
|
705
|
+
source: data.source ?? { kind: 'fallback' },
|
|
706
|
+
})
|
|
707
|
+
} catch (error) {
|
|
708
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
709
|
+
console.error(`[im-bridge] 加标题前缀失败: ${message}`)
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Sessions the GUI archived. Archiving is the workspace registry's global
|
|
716
|
+
* set, not session state, and it has no inverse: an archived session is
|
|
717
|
+
* invisible in every list, so this plugin must stop writing to it.
|
|
718
|
+
*/
|
|
719
|
+
function archivedSessions(): ReadonlySet<string> {
|
|
720
|
+
const registry = ctx.get('workspaceRegistry') as WorkspaceRegistry | undefined
|
|
721
|
+
if (registry === undefined) return new Set()
|
|
722
|
+
try {
|
|
723
|
+
return new Set(registry.archivedSessionIds)
|
|
724
|
+
} catch (error) {
|
|
725
|
+
// The getter throws until the registry finishes its own startup.
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
727
|
+
console.warn(`[im-bridge] 读归档会话失败: ${message}`)
|
|
728
|
+
return new Set()
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function unpinLegacyWecomTitle(agent: LiveAgent): Promise<void> {
|
|
733
|
+
const titles = ctx.get('sessionTitle') as SessionTitleService | undefined
|
|
734
|
+
if (titles === undefined) return
|
|
735
|
+
try {
|
|
736
|
+
const snapshot = titles.get(agent.session)
|
|
737
|
+
if (snapshot?.source?.kind !== 'user') return
|
|
738
|
+
if (!isLegacyPinnedWecomTitle(snapshot.title)) return
|
|
739
|
+
await titles.refresh(agent.session)
|
|
740
|
+
} catch (error) {
|
|
741
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
742
|
+
console.error(`[im-bridge] 解开旧标题失败: ${message}`)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Register persona + wecom prompt on this Agent's layer, synchronously.
|
|
748
|
+
* Must use `agentCtx.systemPrompt` (caller-bound). `inject()` without await
|
|
749
|
+
* yields a microtask and can publish the Agent before the section exists;
|
|
750
|
+
* `adopt` never re-runs setup.
|
|
751
|
+
*/
|
|
752
|
+
function installWecomChannel(agentCtx: Context, st: ChatState): void {
|
|
753
|
+
if (st.wecomPromptInstalled) return
|
|
754
|
+
const host = agentCtx as Context & PromptHost
|
|
755
|
+
if (host.get('systemPrompt') === undefined || host.systemPrompt === undefined) {
|
|
756
|
+
console.warn('[im-bridge] 当前 Agent 没有 systemPrompt,企微提示词未注入')
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
const prompt = host.systemPrompt
|
|
760
|
+
try {
|
|
761
|
+
prompt.section({
|
|
762
|
+
name: 'deployment:persona',
|
|
763
|
+
order: 0,
|
|
764
|
+
text: () => resolvePersona(cfg(), settings),
|
|
765
|
+
})
|
|
766
|
+
} catch (error) {
|
|
767
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
768
|
+
console.warn(`[im-bridge] 人设段未覆盖(可能已由 preset 注册): ${message}`)
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
prompt.section({
|
|
772
|
+
name: 'channel:wecom-cli',
|
|
773
|
+
order: 1,
|
|
774
|
+
// Re-read per request: a group Agent is shared, and its sender —
|
|
775
|
+
// hence `st.office` — changes message to message. Only an office
|
|
776
|
+
// 1:1 gets the tool, so only it may get the office prompt.
|
|
777
|
+
// Always inject: WeCom has no GUI even when wecomCli is off.
|
|
778
|
+
text: () => wecomCliReady && shouldInjectWecomOfficeSkills(st.kind, st.office)
|
|
779
|
+
? WECOM_CLI_PROMPT
|
|
780
|
+
: WECOM_CLI_NO_OFFICE_PROMPT,
|
|
781
|
+
})
|
|
782
|
+
console.log(
|
|
783
|
+
`[im-bridge] 已注入企微提示词 office=${String(st.office)} kind=${st.kind ?? '?'}`,
|
|
784
|
+
)
|
|
785
|
+
} catch (error) {
|
|
786
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
787
|
+
console.error(`[im-bridge] 注入企微提示词失败: ${message}`)
|
|
788
|
+
}
|
|
789
|
+
st.wecomPromptInstalled = true
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function ensureAgent(ref: WecomSessionRef): Promise<ChatState> {
|
|
793
|
+
let st = chats.get(ref.key)
|
|
794
|
+
if (st === undefined) {
|
|
795
|
+
st = emptyChatState()
|
|
796
|
+
chats.set(ref.key, st)
|
|
797
|
+
}
|
|
798
|
+
st.kind = ref.kind
|
|
799
|
+
if (st.agent !== undefined) {
|
|
800
|
+
if (st.sessionId === undefined || !archivedSessions().has(st.sessionId)) {
|
|
801
|
+
// Office access is decided per inbound sender, and skills can arrive
|
|
802
|
+
// from the Settings button after this Agent was created.
|
|
803
|
+
installOfficeLayer(st.agent.ctx, st)
|
|
804
|
+
return st
|
|
805
|
+
}
|
|
806
|
+
console.log(`[im-bridge] 会话 ${st.sessionId} 已归档,改开新会话`)
|
|
807
|
+
st.agent = undefined
|
|
808
|
+
st.sessionId = undefined
|
|
809
|
+
st.wecomPromptInstalled = false
|
|
810
|
+
st.officeSkillsRegistered = false
|
|
811
|
+
st.officeToolRegistered = false
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const persistence = ctx.get('sessionPersistence') as SessionPersistence | undefined
|
|
815
|
+
const headers = persistence === undefined ? [] : await persistence.list()
|
|
816
|
+
const plan = planWecomBind(ref.key, {
|
|
817
|
+
live: (id) => agents.get(SessionId(id)) !== undefined,
|
|
818
|
+
stored: new Set(headers.map((header) => header.id)),
|
|
819
|
+
archived: archivedSessions(),
|
|
820
|
+
})
|
|
821
|
+
const sessionId = SessionId(plan.sessionId)
|
|
822
|
+
const stored = headers.find((header) => header.id === sessionId)
|
|
823
|
+
|
|
824
|
+
const attach = (agent: LiveAgent, how: 'adopt' | 'resume' | 'create'): void => {
|
|
825
|
+
st.agent = agent
|
|
826
|
+
st.sessionId = sessionId
|
|
827
|
+
st.kind = ref.kind
|
|
828
|
+
if (agent.ctx === undefined) {
|
|
829
|
+
console.warn('[im-bridge] Agent 没有 ctx,无法注入企微提示词')
|
|
830
|
+
} else {
|
|
831
|
+
installWecomChannel(agent.ctx, st)
|
|
832
|
+
installOfficeLayer(agent.ctx, st)
|
|
833
|
+
}
|
|
834
|
+
void unpinLegacyWecomTitle(agent)
|
|
835
|
+
const cwd = agent.session.header?.cwd ?? stored?.cwd
|
|
836
|
+
if (cwd !== undefined && cwd !== cfg().workspace) {
|
|
837
|
+
console.warn(
|
|
838
|
+
`[im-bridge] 会话 ${sessionId} 仍使用存档目录 ${cwd},当前 workspace=${cfg().workspace}`,
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
const epoch = plan.epoch > 1 ? ` 第${String(plan.epoch)}段` : ''
|
|
842
|
+
console.log(
|
|
843
|
+
`[im-bridge] 为 ${ref.key} ${how}会话 ${sessionId}${epoch} userid=${ref.sender} chattype=${ref.kind} chatid=${ref.chatid ?? ''}`,
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const live = agents.get(sessionId)
|
|
848
|
+
if (plan.bind === 'adopt' && live !== undefined) {
|
|
849
|
+
attach(live, 'adopt')
|
|
850
|
+
return st
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const selection = resolveSelection(cfg(), defaultModel)
|
|
854
|
+
const presets = ctx.get('agentPresets') as AgentPresets | undefined
|
|
855
|
+
const presetId = (plan.bind === 'resume' && stored?.agentPreset) ? stored.agentPreset : cfg().agentPreset
|
|
856
|
+
let resolvedId = presetId
|
|
857
|
+
if (presets !== undefined) {
|
|
858
|
+
resolvedId = (await presets.resolve(presetId)).id
|
|
859
|
+
}
|
|
860
|
+
const setup = async (agentCtx: Context): Promise<void> => {
|
|
861
|
+
const selected = { current: selection, assembled: undefined }
|
|
862
|
+
installModelSelection(agentCtx, selected)
|
|
863
|
+
if (presets !== undefined) await presets.mount(agentCtx, resolvedId)
|
|
864
|
+
installWecomChannel(agentCtx, st)
|
|
865
|
+
}
|
|
866
|
+
const agentOptions = { provider: selection.provider, model: selection.model }
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
if (plan.bind === 'resume') {
|
|
870
|
+
const { agent } = await agents.resume({
|
|
871
|
+
resumeSessionId: sessionId,
|
|
872
|
+
agentOptions,
|
|
873
|
+
setup,
|
|
874
|
+
})
|
|
875
|
+
attach(agent, 'resume')
|
|
876
|
+
return st
|
|
877
|
+
}
|
|
878
|
+
const { agent } = await agents.create({
|
|
879
|
+
sessionId,
|
|
880
|
+
meta: { cwd: cfg().workspace, agentPreset: resolvedId },
|
|
881
|
+
agentOptions,
|
|
882
|
+
setup,
|
|
883
|
+
})
|
|
884
|
+
attach(agent, 'create')
|
|
885
|
+
return st
|
|
886
|
+
} catch (error) {
|
|
887
|
+
const raced = agents.get(sessionId)
|
|
888
|
+
if (raced !== undefined) {
|
|
889
|
+
attach(raced, 'adopt')
|
|
890
|
+
return st
|
|
891
|
+
}
|
|
892
|
+
throw error
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
ctx.on('session/event', (session: TitledSession, event: LoggedEvent) => {
|
|
897
|
+
const thinking = cfg().thinking
|
|
898
|
+
const prefix = thinking?.activityPrefix ?? DEFAULT_THINKING.activityPrefix
|
|
899
|
+
const flashMs = Number.isFinite(thinking?.intervalMs) && thinking.intervalMs > 0
|
|
900
|
+
? thinking.intervalMs
|
|
901
|
+
: DEFAULT_THINKING.intervalMs
|
|
902
|
+
for (const st of chats.values()) {
|
|
903
|
+
if (st.sessionId !== session.id) continue
|
|
904
|
+
if (event.type === 'session/title') {
|
|
905
|
+
prefixWecomTitle(session, st)
|
|
906
|
+
continue
|
|
907
|
+
}
|
|
908
|
+
if (event.type === 'assistant/chunk') {
|
|
909
|
+
const next = streamPhaseFromChunk((event.data as ChunkData).chunk)
|
|
910
|
+
if (next !== null) st.modelStreamPhase = next
|
|
911
|
+
continue
|
|
912
|
+
}
|
|
913
|
+
if (event.type === 'tool/call') {
|
|
914
|
+
const toolName = (event.data as ToolCallData).name ?? ''
|
|
915
|
+
const callId = (event.data as ToolCallData).callId
|
|
916
|
+
if (callId !== undefined) st.lastToolByCallId.set(callId, toolName)
|
|
917
|
+
st.activityClearAt = 0
|
|
918
|
+
st.lastActivity = `${prefix}${labelTool(toolName, thinking)}`
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
if (event.type === 'tool/result') {
|
|
922
|
+
const data = event.data as ToolResultData
|
|
923
|
+
const callId = data.message?.source?.callId
|
|
924
|
+
const rawName = (callId !== undefined && st.lastToolByCallId.get(callId))
|
|
925
|
+
|| [...st.lastToolByCallId.values()].at(-1)
|
|
926
|
+
|| ''
|
|
927
|
+
if (callId !== undefined) st.lastToolByCallId.delete(callId)
|
|
928
|
+
const label = labelTool(rawName || '工具', thinking)
|
|
929
|
+
const failed = data.error !== undefined
|
|
930
|
+
st.lastActivity = failed ? `❌ ${label} 失败` : `✅ ${label} 完成`
|
|
931
|
+
st.activityClearAt = Date.now() + flashMs
|
|
932
|
+
st.modelStreamPhase = 'idle'
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
// The SDK's own WSClient types its listeners per event name; this bridge
|
|
938
|
+
// reads frames as JSON and needs one permissive `on`, so the narrowing goes
|
|
939
|
+
// through unknown rather than widening every handler to the SDK's maps.
|
|
940
|
+
const { default: AiBot, generateReqId } = await import('@wecom/aibot-node-sdk') as unknown as {
|
|
941
|
+
default: { WSClient: new (options: { botId: string; secret: string }) => WecomClient }
|
|
942
|
+
generateReqId: (kind: string) => string
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function handle(frame: WecomFrame, ref: WecomSessionRef, content: string): Promise<void> {
|
|
946
|
+
let st = chats.get(ref.key)
|
|
947
|
+
if (st === undefined) {
|
|
948
|
+
st = emptyChatState()
|
|
949
|
+
chats.set(ref.key, st)
|
|
950
|
+
}
|
|
951
|
+
st.office = senderHasOfficeAccess(cfg().wecomCli.allowFrom, ref.sender)
|
|
952
|
+
st = await ensureAgent(ref)
|
|
953
|
+
const startedAt = Date.now()
|
|
954
|
+
const streamId = generateReqId('stream')
|
|
955
|
+
let stopThinking: (() => void) | null = null
|
|
956
|
+
st.lastActivity = ''
|
|
957
|
+
st.activityClearAt = 0
|
|
958
|
+
st.lastToolByCallId.clear()
|
|
959
|
+
st.modelStreamPhase = 'idle'
|
|
960
|
+
st.streamStatusTick = 0
|
|
961
|
+
try {
|
|
962
|
+
await ws.replyStream(frame, streamId, cfg().startHint, false)
|
|
963
|
+
stopThinking = startThinking(
|
|
964
|
+
ws, frame, streamId, startedAt, cfg().agentTimeoutSec,
|
|
965
|
+
() => {
|
|
966
|
+
if (st.activityClearAt > 0 && Date.now() >= st.activityClearAt) {
|
|
967
|
+
st.lastActivity = ''
|
|
968
|
+
st.activityClearAt = 0
|
|
969
|
+
}
|
|
970
|
+
if (st.lastActivity) return st.lastActivity
|
|
971
|
+
const thinking = cfg().thinking
|
|
972
|
+
const tick = st.streamStatusTick++
|
|
973
|
+
if (st.modelStreamPhase === 'reasoning') {
|
|
974
|
+
return pickStatusLine(
|
|
975
|
+
thinking?.reasoningStatus,
|
|
976
|
+
DEFAULT_THINKING.reasoningStatus,
|
|
977
|
+
tick,
|
|
978
|
+
)
|
|
979
|
+
}
|
|
980
|
+
if (st.modelStreamPhase === 'outputting') {
|
|
981
|
+
return pickStatusLine(
|
|
982
|
+
thinking?.outputStatus,
|
|
983
|
+
DEFAULT_THINKING.outputStatus,
|
|
984
|
+
tick,
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
return ''
|
|
988
|
+
},
|
|
989
|
+
cfg().thinking,
|
|
990
|
+
() => (st.lastActivity ? 'idle' : st.modelStreamPhase),
|
|
991
|
+
)
|
|
992
|
+
} catch (error) {
|
|
993
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
994
|
+
console.error(`[im-bridge] 占位回复失败: ${message}`)
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
if (st.agent === undefined) throw new Error('im-bridge: chat agent missing')
|
|
998
|
+
await st.agent.whenIdle()
|
|
999
|
+
const firstSeq = st.agent.session.seq
|
|
1000
|
+
st.agent.followup(createUserMessage({
|
|
1001
|
+
content: [{ type: 'text', text: content }],
|
|
1002
|
+
source: { kind: 'user' },
|
|
1003
|
+
}))
|
|
1004
|
+
await st.agent.whenIdle()
|
|
1005
|
+
await sessions.flush(st.agent.session)
|
|
1006
|
+
const outcome = summarize(st.agent.session.snapshotEvents(), firstSeq)
|
|
1007
|
+
if (stopThinking) stopThinking()
|
|
1008
|
+
const ms = Date.now() - startedAt
|
|
1009
|
+
const body = outcome.text || '(agent 无输出)'
|
|
1010
|
+
const collected = collectReplyPngs(body, cfg().workspace)
|
|
1011
|
+
for (const reason of collected.skipped) {
|
|
1012
|
+
console.warn(`[im-bridge] 跳过图片: ${reason}`)
|
|
1013
|
+
}
|
|
1014
|
+
let reply = truncate(body, (cfg().maxReplyBytes || 20000) - 200) + footerOf(ms)
|
|
1015
|
+
if (collected.skipped.length > 0) {
|
|
1016
|
+
reply = truncate(
|
|
1017
|
+
`${reply}\n⚠️ ${collected.skipped.length} 张图片未发送(过大、越权或不存在)`,
|
|
1018
|
+
cfg().maxReplyBytes || 20000,
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
console.log(`[im-bridge] ${ref.key} 完成 (${Buffer.byteLength(reply, 'utf8')}B, ${fmtDuration(ms)})`)
|
|
1022
|
+
await sendFinal(ws, frame, streamId, reply)
|
|
1023
|
+
if (collected.images.length > 0) {
|
|
1024
|
+
await sendCollectedPngs(ws, resolveChatId(frame, ref.sender), collected.images)
|
|
1025
|
+
}
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (stopThinking) stopThinking()
|
|
1028
|
+
const ms = Date.now() - startedAt
|
|
1029
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1030
|
+
console.error(`[im-bridge] agent 失败: ${message}`)
|
|
1031
|
+
try {
|
|
1032
|
+
await sendFinal(ws, frame, streamId, `处理失败: ${truncate(message, 400)}\n\n---\n❌ 耗时 ${fmtDuration(ms)}`)
|
|
1033
|
+
} catch (retryError) {
|
|
1034
|
+
const retryMessage = retryError instanceof Error ? retryError.message : String(retryError)
|
|
1035
|
+
console.error(`[im-bridge] 错误回复也失败: ${retryMessage}`)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const ws = new AiBot.WSClient({ botId, secret })
|
|
1041
|
+
|
|
1042
|
+
ws.on('connected', (() => console.log('[im-bridge] WebSocket 已连接')) as (...args: never[]) => void)
|
|
1043
|
+
ws.on('authenticated', (() => console.log('[im-bridge] 认证成功, 等待消息...')) as (...args: never[]) => void)
|
|
1044
|
+
ws.on('disconnected', ((reason: string) => console.log(`[im-bridge] 断开: ${reason}`)) as (...args: never[]) => void)
|
|
1045
|
+
ws.on('reconnecting', ((n: number) => console.log(`[im-bridge] 第 ${n} 次重连...`)) as (...args: never[]) => void)
|
|
1046
|
+
ws.on('error', ((error: Error) => console.error(`[im-bridge] 错误: ${error.message}`)) as (...args: never[]) => void)
|
|
1047
|
+
|
|
1048
|
+
ws.on('message.text', ((frame: WecomFrame) => {
|
|
1049
|
+
const inbound = (frame.body?.text?.content || '').trim()
|
|
1050
|
+
if (!inbound) return
|
|
1051
|
+
const content = stripBotMention(inbound)
|
|
1052
|
+
let ref: WecomSessionRef
|
|
1053
|
+
try {
|
|
1054
|
+
ref = resolveWecomSession(frame)
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
if (error instanceof WecomSessionReject) {
|
|
1057
|
+
console.error(`[im-bridge] ${error.reply}`)
|
|
1058
|
+
void ws.replyStream(frame, generateReqId('stream'), error.reply, true).catch(() => {})
|
|
1059
|
+
return
|
|
1060
|
+
}
|
|
1061
|
+
throw error
|
|
1062
|
+
}
|
|
1063
|
+
if (cfg().allowFrom.length > 0 && !cfg().allowFrom.includes(ref.sender)) {
|
|
1064
|
+
void ws.replyStream(frame, generateReqId('stream'), cfg().deniedMessage, true).catch(() => {})
|
|
1065
|
+
return
|
|
1066
|
+
}
|
|
1067
|
+
console.log(
|
|
1068
|
+
`[im-bridge] 收到 key=${ref.key} userid=${ref.sender} chattype=${String(frame.body?.chattype ?? '')} chatid=${ref.chatid ?? ''}: ${content.slice(0, 100)}`,
|
|
1069
|
+
)
|
|
1070
|
+
const st = chats.get(ref.key) ?? emptyChatState()
|
|
1071
|
+
st.kind = ref.kind
|
|
1072
|
+
chats.set(ref.key, st)
|
|
1073
|
+
st.queue = st.queue
|
|
1074
|
+
.then(() => handle(frame, ref, content))
|
|
1075
|
+
.catch((error: unknown) => {
|
|
1076
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1077
|
+
console.error(`[im-bridge] 任务异常: ${message}`)
|
|
1078
|
+
})
|
|
1079
|
+
}) as (...args: never[]) => void)
|
|
1080
|
+
|
|
1081
|
+
ws.on('event.enter_chat', ((frame: WecomFrame) => {
|
|
1082
|
+
const sender = frame.body?.from?.userid || 'unknown'
|
|
1083
|
+
console.log(`[im-bridge] 用户 ${sender} 进入会话`)
|
|
1084
|
+
void ws.replyWelcome(frame, {
|
|
1085
|
+
msgtype: 'text',
|
|
1086
|
+
text: { content: cfg().welcomeMessage },
|
|
1087
|
+
}).catch((error: unknown) => {
|
|
1088
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1089
|
+
console.error(`[im-bridge] 欢迎语失败: ${message}`)
|
|
1090
|
+
})
|
|
1091
|
+
}) as (...args: never[]) => void)
|
|
1092
|
+
|
|
1093
|
+
ws.connect()
|
|
1094
|
+
|
|
1095
|
+
ctx.effect(() => () => {
|
|
1096
|
+
try { ws.close?.() } catch { /* already closed */ }
|
|
1097
|
+
}, 'im-bridge: wecom websocket')
|
|
1098
|
+
})()
|
|
1099
|
+
}
|