@spzhongwin/skill-logger-plugin 1.0.16 → 1.0.18
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/dist/index.js +2902 -281
- package/openclaw.plugin.json +50 -50
- package/package.json +34 -34
- package/src/active-skills.test.ts +32 -32
- package/src/active-skills.ts +77 -77
- package/src/config-sync.test.ts +165 -165
- package/src/config-sync.ts +544 -544
- package/src/expert-skill-layout.test.ts +196 -196
- package/src/expert-skill-layout.ts +233 -233
- package/src/hooks.test.ts +228 -228
- package/src/hooks.ts +494 -494
- package/src/http.ts +61 -61
- package/src/identity.ts +88 -88
- package/src/index.test.ts +53 -53
- package/src/index.ts +218 -218
- package/src/integration.test.ts +119 -119
- package/src/matcher.test.ts +170 -170
- package/src/matcher.ts +393 -393
- package/src/paths.test.ts +57 -57
- package/src/paths.ts +84 -84
- package/src/reporter.test.ts +139 -139
- package/src/reporter.ts +303 -303
- package/src/sample-config.json +72 -72
- package/src/semver.test.ts +33 -33
- package/src/semver.ts +65 -65
- package/src/skill-version.ts +53 -53
- package/src/types.ts +202 -202
- package/src/updater.test.ts +431 -431
- package/src/updater.ts +584 -584
- package/src/ws-client.test.ts +263 -158
- package/src/ws-client.ts +936 -805
- package/test-ws.ts +17 -17
- package/tsconfig.json +18 -18
package/src/ws-client.ts
CHANGED
|
@@ -1,805 +1,936 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
import { SkillUpdater } from "./updater.ts";
|
|
6
|
-
import { openclawHome } from "./paths.ts";
|
|
7
|
-
import { readSkillVersion } from "./skill-version.ts";
|
|
8
|
-
|
|
9
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
10
|
-
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
11
|
-
const AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1000;
|
|
12
|
-
const ASSISTANT_WORKSPACE_PREFIX = "workspace-assistant-";
|
|
13
|
-
const ASSISTANT_AGENT_PREFIX = "assistant-";
|
|
14
|
-
const ASSISTANT_WORKSPACE_ID_RE = /^\d{5,}$/;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
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
|
-
|
|
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
|
-
private
|
|
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
|
-
if (
|
|
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
|
-
this.
|
|
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
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { SkillUpdater } from "./updater.ts";
|
|
6
|
+
import { openclawHome } from "./paths.ts";
|
|
7
|
+
import { readSkillVersion } from "./skill-version.ts";
|
|
8
|
+
|
|
9
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
10
|
+
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
11
|
+
const AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1000;
|
|
12
|
+
const ASSISTANT_WORKSPACE_PREFIX = "workspace-assistant-";
|
|
13
|
+
const ASSISTANT_AGENT_PREFIX = "assistant-";
|
|
14
|
+
const ASSISTANT_WORKSPACE_ID_RE = /^\d{5,}$/;
|
|
15
|
+
|
|
16
|
+
type OpenClawAccount = {
|
|
17
|
+
agentId?: unknown;
|
|
18
|
+
enabled?: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SkillInstallTarget = {
|
|
22
|
+
/** 普通 Skill 的安装根目录。 */
|
|
23
|
+
skillsDir: string;
|
|
24
|
+
/** 解析出的本地 OpenClaw agent,便于日志与后续专家链路复用。 */
|
|
25
|
+
localAgentId: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从 `channels.xg_cwork_im.accounts` 提取应上报给中控的 agent ID。
|
|
30
|
+
*
|
|
31
|
+
* `enabled: false` 是唯一的禁用标识;旧配置未填写 enabled 时维持原有可用语义。
|
|
32
|
+
* 没有有效 agentId 的配置项(例如 default)不会被上报。
|
|
33
|
+
*/
|
|
34
|
+
export function enabledAgentIdsFromAccounts(config: unknown): string[] {
|
|
35
|
+
if (!config || typeof config !== "object") return [];
|
|
36
|
+
|
|
37
|
+
const channels = (config as { channels?: unknown }).channels;
|
|
38
|
+
if (!channels || typeof channels !== "object") return [];
|
|
39
|
+
const cworkConfig = (channels as { xg_cwork_im?: unknown }).xg_cwork_im;
|
|
40
|
+
if (!cworkConfig || typeof cworkConfig !== "object") return [];
|
|
41
|
+
const accounts = (cworkConfig as { accounts?: unknown }).accounts;
|
|
42
|
+
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return [];
|
|
43
|
+
|
|
44
|
+
const agentIds = new Set<string>();
|
|
45
|
+
for (const account of Object.values(accounts as Record<string, OpenClawAccount>)) {
|
|
46
|
+
if (!account || typeof account !== "object" || account.enabled === false) continue;
|
|
47
|
+
if (typeof account.agentId !== "string") continue;
|
|
48
|
+
const agentId = account.agentId.trim();
|
|
49
|
+
if (agentId) agentIds.add(agentId);
|
|
50
|
+
}
|
|
51
|
+
return [...agentIds];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 根据 OpenClaw 配置解析普通 Skill 的安装目录。
|
|
56
|
+
*
|
|
57
|
+
* 只有同时满足以下条件才返回目标目录:
|
|
58
|
+
* 1. xg_cwork_im 中存在 `agentId === userId` 的 account;
|
|
59
|
+
* 2. account 没有显式 disabled;
|
|
60
|
+
* 3. account(或它的 binding)映射到一个配置了 workspace 的本地 agent。
|
|
61
|
+
*
|
|
62
|
+
* 这里故意不回退到 `workspace-assistant-<id>` 命名规则。配置缺失时拒绝执行,
|
|
63
|
+
* 防止禁用或已迁移 workspace 的用户仍被写入历史目录。
|
|
64
|
+
*/
|
|
65
|
+
export function resolveSkillInstallTarget(config: unknown, userId: string): SkillInstallTarget {
|
|
66
|
+
if (!config || typeof config !== "object") throw new Error("openclaw.json 配置无效");
|
|
67
|
+
const root = config as {
|
|
68
|
+
channels?: { xg_cwork_im?: { accounts?: unknown } };
|
|
69
|
+
bindings?: unknown;
|
|
70
|
+
agents?: { defaults?: { workspace?: unknown }; list?: unknown };
|
|
71
|
+
};
|
|
72
|
+
const accounts = root.channels?.xg_cwork_im?.accounts;
|
|
73
|
+
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) {
|
|
74
|
+
throw new Error(`未找到 userId=${userId} 的 xg_cwork_im account`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const matched = Object.entries(accounts as Record<string, OpenClawAccount>)
|
|
78
|
+
.filter(([, account]) => account && typeof account === "object" && account.agentId === userId);
|
|
79
|
+
if (matched.length !== 1) {
|
|
80
|
+
throw new Error(matched.length > 1
|
|
81
|
+
? `userId=${userId} 存在多个 xg_cwork_im account,无法确定安装目录`
|
|
82
|
+
: `未找到 userId=${userId} 的 xg_cwork_im account`);
|
|
83
|
+
}
|
|
84
|
+
const [accountId, account] = matched[0];
|
|
85
|
+
if (account.enabled === false) throw new Error(`userId=${userId} 对应 Agent 已禁用`);
|
|
86
|
+
|
|
87
|
+
// 规范配置以 binding 决定本地 agent;兼容历史配置中 account key/agentId 作为 accountId 的写法。
|
|
88
|
+
const bindingAccountIds = new Set([accountId, userId]);
|
|
89
|
+
const bindings = Array.isArray(root.bindings) ? root.bindings : [];
|
|
90
|
+
const binding = bindings.find((item): item is { agentId?: unknown; match?: { channel?: unknown; accountId?: unknown } } => {
|
|
91
|
+
if (!item || typeof item !== "object") return false;
|
|
92
|
+
const candidate = item as { agentId?: unknown; match?: { channel?: unknown; accountId?: unknown } };
|
|
93
|
+
return candidate.match?.channel === "xg_cwork_im"
|
|
94
|
+
&& typeof candidate.match.accountId === "string"
|
|
95
|
+
&& bindingAccountIds.has(candidate.match.accountId)
|
|
96
|
+
&& typeof candidate.agentId === "string"
|
|
97
|
+
&& candidate.agentId.length > 0;
|
|
98
|
+
});
|
|
99
|
+
const localAgentId = typeof binding?.agentId === "string" ? binding.agentId : userId;
|
|
100
|
+
|
|
101
|
+
const list = Array.isArray(root.agents?.list) ? root.agents.list : [];
|
|
102
|
+
const agent = list.find((item): item is { id?: unknown; workspace?: unknown } =>
|
|
103
|
+
Boolean(item) && typeof item === "object" && (item as { id?: unknown }).id === localAgentId,
|
|
104
|
+
);
|
|
105
|
+
if (!agent) throw new Error(`未找到本地 Agent 配置: ${localAgentId}`);
|
|
106
|
+
const workspace = typeof agent.workspace === "string"
|
|
107
|
+
? agent.workspace
|
|
108
|
+
: root.agents?.defaults?.workspace;
|
|
109
|
+
if (typeof workspace !== "string" || !workspace.trim()) {
|
|
110
|
+
throw new Error(`本地 Agent ${localAgentId} 未配置 workspace`);
|
|
111
|
+
}
|
|
112
|
+
return { skillsDir: path.join(workspace, "skills"), localAgentId };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseAssistantWorkspaceAgentId(entryName: string): string | undefined {
|
|
116
|
+
if (!entryName.startsWith(ASSISTANT_WORKSPACE_PREFIX)) return undefined;
|
|
117
|
+
const suffix = entryName.slice(ASSISTANT_WORKSPACE_PREFIX.length);
|
|
118
|
+
if (!ASSISTANT_WORKSPACE_ID_RE.test(suffix)) return undefined;
|
|
119
|
+
return `${ASSISTANT_AGENT_PREFIX}${suffix}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeAssistantUserId(userId: string): string | undefined {
|
|
123
|
+
const safeUserId = path.basename(userId);
|
|
124
|
+
if (safeUserId !== userId) return undefined;
|
|
125
|
+
const pureId = safeUserId.startsWith(ASSISTANT_AGENT_PREFIX)
|
|
126
|
+
? safeUserId.slice(ASSISTANT_AGENT_PREFIX.length)
|
|
127
|
+
: safeUserId;
|
|
128
|
+
if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return undefined;
|
|
129
|
+
return pureId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function normalizeCommandCode(code: unknown): string | undefined {
|
|
133
|
+
if (typeof code !== "string" || code.length === 0 || code === "." || code === "..") return undefined;
|
|
134
|
+
if (code.includes("/") || code.includes("\\") || code.includes("\0")) return undefined;
|
|
135
|
+
if (path.basename(code) !== code) return undefined;
|
|
136
|
+
return code;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function shouldSyncBuiltInTemplate(action: string, isBuiltIn: unknown): boolean {
|
|
140
|
+
return action === "UPDATE_SKILL" && isBuiltIn === true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveGatewaySkillTarget(home = openclawHome()): string {
|
|
144
|
+
return path.join(home, "skills");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isGatewaySkillCommand(action: unknown, installScope: unknown): boolean {
|
|
148
|
+
return installScope === "gateway"
|
|
149
|
+
&& ["INSTALL_SKILL", "UPDATE_SKILL", "UNINSTALL_SKILL"].includes(String(action || ""));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function findInstalledExpertSkillsRoot(
|
|
153
|
+
rootPath: string,
|
|
154
|
+
pureId: string,
|
|
155
|
+
code: string
|
|
156
|
+
): Promise<string | undefined> {
|
|
157
|
+
const skillsRoot = path.join(rootPath, `workspace-assistant-${pureId}`, ".user", "skills");
|
|
158
|
+
try {
|
|
159
|
+
const stat = await fs.stat(path.join(skillsRoot, code));
|
|
160
|
+
return stat.isDirectory() ? skillsRoot : undefined;
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function defaultOpenclawSqlitePath(): string {
|
|
167
|
+
return path.join(openclawHome(), "state", "openclaw.sqlite");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readCronJobsByAgentId(
|
|
171
|
+
agentId: string,
|
|
172
|
+
sqlitePath = defaultOpenclawSqlitePath(),
|
|
173
|
+
onError?: (err: unknown) => void
|
|
174
|
+
): any[] {
|
|
175
|
+
let db: DatabaseSync | undefined;
|
|
176
|
+
try {
|
|
177
|
+
db = new DatabaseSync(sqlitePath, { readOnly: true });
|
|
178
|
+
return db.prepare("SELECT * FROM cron_jobs WHERE agent_id = ?").all(agentId);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
onError?.(err);
|
|
181
|
+
return [];
|
|
182
|
+
} finally {
|
|
183
|
+
db?.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface WsClientOptions {
|
|
188
|
+
serverUrl: string; // 例如: wss://api.aishuo.co/gateway/ws
|
|
189
|
+
authToken?: string; // 用于网关鉴权
|
|
190
|
+
gatewayId: string; // 当前网关宿主的标识,方便中控做集群分发
|
|
191
|
+
updater: SkillUpdater; // 传入原来已有的 updater 实例
|
|
192
|
+
enableFileLog?: boolean; // 文件日志开关
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export class GatewayWsClient {
|
|
196
|
+
private ws: WebSocket | null = null;
|
|
197
|
+
private options: WsClientOptions;
|
|
198
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
199
|
+
private agentScanTimer: NodeJS.Timeout | null = null;
|
|
200
|
+
private pingTimer: NodeJS.Timeout | null = null;
|
|
201
|
+
private connectTimeoutTimer: NodeJS.Timeout | null = null;
|
|
202
|
+
private lastServerAckAt = 0;
|
|
203
|
+
private reconnectAttempts = 0;
|
|
204
|
+
private isDestroyed = false;
|
|
205
|
+
|
|
206
|
+
// 本地缓存的 agent ID 列表
|
|
207
|
+
private currentAgentIds = new Set<string>();
|
|
208
|
+
|
|
209
|
+
private appendLogToFile(level: string, category: string, message: string, payload?: any) {
|
|
210
|
+
if (!this.options.enableFileLog) return;
|
|
211
|
+
try {
|
|
212
|
+
const ts = new Date().toISOString();
|
|
213
|
+
let logLine = `[${ts}] [${level}] [${category}] ${message}`;
|
|
214
|
+
if (payload !== undefined && payload !== null) {
|
|
215
|
+
// 如果是 Error 对象,主动提取 stack
|
|
216
|
+
if (payload instanceof Error) {
|
|
217
|
+
logLine += `\n Stack: ${payload.stack || payload.message}`;
|
|
218
|
+
} else {
|
|
219
|
+
logLine += ` | Data: ${typeof payload === 'object' ? JSON.stringify(payload) : payload}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
logLine += '\n';
|
|
223
|
+
const logsDir = path.join(openclawHome(), "logs");
|
|
224
|
+
fs.mkdir(logsDir, { recursive: true }).then(() => {
|
|
225
|
+
const logPath = path.join(logsDir, "skill-logger.err");
|
|
226
|
+
fs.appendFile(logPath, logLine).catch(()=>{});
|
|
227
|
+
}).catch(()=>{});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private createInstallTrace(context: Record<string, unknown>) {
|
|
234
|
+
return (stage: string, data?: Record<string, unknown>) => {
|
|
235
|
+
this.appendLogToFile(stage === "install.failed" ? "ERROR" : "INFO", "Install", stage, {
|
|
236
|
+
...context,
|
|
237
|
+
...data,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 普通 Skill 的所有读写操作共用这一个配置驱动的寻址入口。 */
|
|
243
|
+
private async resolveRegularSkillTarget(userId: string): Promise<string> {
|
|
244
|
+
const configPath = path.join(openclawHome(), "openclaw.json");
|
|
245
|
+
let config: unknown;
|
|
246
|
+
try {
|
|
247
|
+
config = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
throw new Error(`无法读取 openclaw.json: ${err?.message || String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
return resolveSkillInstallTarget(config, userId).skillsDir;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
constructor(options: WsClientOptions) {
|
|
255
|
+
this.options = options;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public connect() {
|
|
259
|
+
if (this.isDestroyed) return;
|
|
260
|
+
if (this.ws && (
|
|
261
|
+
this.ws.readyState === WebSocket.OPEN ||
|
|
262
|
+
this.ws.readyState === WebSocket.CONNECTING
|
|
263
|
+
)) {
|
|
264
|
+
this.appendLogToFile("INFO", "Connection", "Connect skipped: websocket already active", { readyState: this.ws.readyState });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.clearConnectTimeout();
|
|
269
|
+
|
|
270
|
+
const msgConnect = `Connecting to Central Service: ${this.options.serverUrl}`;
|
|
271
|
+
console.log(`[skill-logger-plugin][WS] ${msgConnect}`);
|
|
272
|
+
this.appendLogToFile("INFO", "Connection", msgConnect);
|
|
273
|
+
|
|
274
|
+
const headers: Record<string, string> = {
|
|
275
|
+
"X-Gateway-Id": this.options.gatewayId,
|
|
276
|
+
};
|
|
277
|
+
if (this.options.authToken) {
|
|
278
|
+
headers["Authorization"] = this.options.authToken;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const ws = new WebSocket(this.options.serverUrl, { headers });
|
|
283
|
+
this.ws = ws;
|
|
284
|
+
this.connectTimeoutTimer = setTimeout(() => {
|
|
285
|
+
if (this.ws === ws && ws.readyState === WebSocket.CONNECTING) {
|
|
286
|
+
this.appendLogToFile("WARN", "Connection", "Connection timed out before open; terminating socket.");
|
|
287
|
+
ws.terminate();
|
|
288
|
+
}
|
|
289
|
+
}, 15000);
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
console.error(`[skill-logger-plugin][WS] Sync init error:`, err);
|
|
292
|
+
this.appendLogToFile("ERROR", "Connection", "Sync initialization error", err);
|
|
293
|
+
this.scheduleReconnect();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const ws = this.ws;
|
|
298
|
+
|
|
299
|
+
ws.on("open", async () => {
|
|
300
|
+
if (this.ws !== ws) return;
|
|
301
|
+
console.log(`[skill-logger-plugin][WS] Connected successfully!`);
|
|
302
|
+
this.appendLogToFile("INFO", "Connection", "Connected successfully!");
|
|
303
|
+
this.clearConnectTimeout();
|
|
304
|
+
this.reconnectAttempts = 0;
|
|
305
|
+
this.lastServerAckAt = Date.now();
|
|
306
|
+
this.clearReconnectTimer();
|
|
307
|
+
|
|
308
|
+
// 首次连接,全量扫描并上报,同时进行应用层握手,确保服务端能把 DB 在线态刷新成真实状态。
|
|
309
|
+
await this.scanAndReportAgents(true);
|
|
310
|
+
this.sendGatewayHello();
|
|
311
|
+
this.startHeartbeat();
|
|
312
|
+
|
|
313
|
+
// 开启 3 分钟定期的自动扫码增量同步
|
|
314
|
+
if (!this.agentScanTimer) {
|
|
315
|
+
this.agentScanTimer = setInterval(() => {
|
|
316
|
+
this.scanAndReportAgents(true);
|
|
317
|
+
}, AGENT_SCAN_INTERVAL_MS);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
ws.on("pong", () => {
|
|
322
|
+
this.lastServerAckAt = Date.now();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
ws.on("message", async (data) => {
|
|
326
|
+
if (this.ws !== ws) return;
|
|
327
|
+
try {
|
|
328
|
+
const msg = JSON.parse(data.toString());
|
|
329
|
+
if (msg?.type === "GATEWAY_HELLO_ACK" || msg?.type === "HEARTBEAT_ACK") {
|
|
330
|
+
this.lastServerAckAt = Date.now();
|
|
331
|
+
this.appendLogToFile("INFO", "Heartbeat", "Server heartbeat acknowledged", { type: msg.type, connectionId: msg.connectionId });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (msg?.type === "BATCH_COMMAND" && Array.isArray(msg.commands)) {
|
|
335
|
+
const { commands, type, ...shared } = msg;
|
|
336
|
+
for (const cmd of commands) {
|
|
337
|
+
await this.handleMessage({ ...shared, userId: cmd.userId, replyId: cmd.replyId });
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await this.handleMessage(msg);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error(`[skill-logger-plugin][WS] Failed to parse/handle message:`, err);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
ws.on("close", () => {
|
|
348
|
+
console.warn(`[skill-logger-plugin][WS] Connection closed.`);
|
|
349
|
+
this.appendLogToFile("WARN", "Connection", "Connection closed. Starting reconnect timer.");
|
|
350
|
+
if (this.ws === ws) {
|
|
351
|
+
this.ws = null;
|
|
352
|
+
this.clearConnectTimeout();
|
|
353
|
+
this.clearAgentScanTimer();
|
|
354
|
+
this.clearHeartbeat();
|
|
355
|
+
this.scheduleReconnect();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
ws.on("error", (err) => {
|
|
360
|
+
console.error(`[skill-logger-plugin][WS] Connection error:`, err);
|
|
361
|
+
this.appendLogToFile("ERROR", "Connection", "Connection error", err);
|
|
362
|
+
if (this.ws === ws) {
|
|
363
|
+
ws.close(); // 触发 close 事件进行重连
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** 从 openclaw.json 的 xg_cwork_im accounts 读取当前启用的 agent。 */
|
|
369
|
+
private async scanAndReportAgents(isInitialReport: boolean) {
|
|
370
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const configPath = path.join(openclawHome(), "openclaw.json");
|
|
374
|
+
let rawConfig: string;
|
|
375
|
+
try {
|
|
376
|
+
rawConfig = await fs.readFile(configPath, "utf-8");
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// 配置文件临时不可读时,保留上次成功读取的列表,避免把所有用户误报为离线。
|
|
379
|
+
this.appendLogToFile("WARN", "AgentScan", "OpenClaw config is not readable; skipping this scan cycle", e);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let config: unknown;
|
|
384
|
+
try {
|
|
385
|
+
config = JSON.parse(rawConfig);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
this.appendLogToFile("WARN", "AgentScan", "OpenClaw config is invalid JSON; skipping this scan cycle", e);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const newAgentIds = new Set(enabledAgentIdsFromAccounts(config));
|
|
391
|
+
|
|
392
|
+
let changed = false;
|
|
393
|
+
if (newAgentIds.size !== this.currentAgentIds.size) {
|
|
394
|
+
changed = true;
|
|
395
|
+
} else {
|
|
396
|
+
for (const id of newAgentIds) {
|
|
397
|
+
if (!this.currentAgentIds.has(id)) {
|
|
398
|
+
changed = true;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.currentAgentIds = newAgentIds;
|
|
405
|
+
|
|
406
|
+
if (isInitialReport) {
|
|
407
|
+
this.appendLogToFile("INFO", "AgentScan", `Reporting full agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
408
|
+
this.sendAgentListReport("AGENT_LIST_REPORT");
|
|
409
|
+
} else if (changed) {
|
|
410
|
+
this.appendLogToFile("INFO", "AgentScan", `Syncing changed agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
411
|
+
this.sendAgentListReport("AGENT_LIST_SYNC");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
} catch (err: any) {
|
|
415
|
+
console.error(`[skill-logger-plugin][WS] Failed to scan agents:`, err);
|
|
416
|
+
this.appendLogToFile("ERROR", "AgentScan", `Failed to scan agents`, err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private startHeartbeat() {
|
|
421
|
+
this.clearHeartbeat();
|
|
422
|
+
this.pingTimer = setInterval(() => {
|
|
423
|
+
const ws = this.ws;
|
|
424
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
425
|
+
|
|
426
|
+
const ackAge = Date.now() - this.lastServerAckAt;
|
|
427
|
+
if (ackAge > HEARTBEAT_ACK_TIMEOUT_MS) {
|
|
428
|
+
this.appendLogToFile("WARN", "Heartbeat", "Server heartbeat ACK timed out; terminating socket for reconnect", { ackAge });
|
|
429
|
+
ws.terminate();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
ws.ping();
|
|
434
|
+
this.sendClientHeartbeat();
|
|
435
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
436
|
+
this.sendClientHeartbeat();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private clearHeartbeat() {
|
|
440
|
+
if (this.pingTimer) {
|
|
441
|
+
clearInterval(this.pingTimer);
|
|
442
|
+
this.pingTimer = null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private scheduleReconnect() {
|
|
447
|
+
if (this.isDestroyed || this.reconnectTimer) return;
|
|
448
|
+
|
|
449
|
+
// 闭环完善:引入随机 Jitter 抖动,打散服务端重启时可能引发的瞬间重连风暴
|
|
450
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
451
|
+
const baseDelay = Math.min(30000, 2000 * Math.max(1, 2 ** this.reconnectAttempts));
|
|
452
|
+
const delay = baseDelay + jitter;
|
|
453
|
+
this.reconnectAttempts += 1;
|
|
454
|
+
|
|
455
|
+
console.log(`[skill-logger-plugin][WS] Reconnecting in ${delay}ms...`);
|
|
456
|
+
this.appendLogToFile("INFO", "Connection", "Reconnect scheduled", { delay, attempt: this.reconnectAttempts });
|
|
457
|
+
this.reconnectTimer = setTimeout(() => {
|
|
458
|
+
this.reconnectTimer = null;
|
|
459
|
+
this.connect();
|
|
460
|
+
}, delay);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private clearReconnectTimer() {
|
|
464
|
+
if (this.reconnectTimer) {
|
|
465
|
+
clearTimeout(this.reconnectTimer);
|
|
466
|
+
this.reconnectTimer = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private clearConnectTimeout() {
|
|
471
|
+
if (this.connectTimeoutTimer) {
|
|
472
|
+
clearTimeout(this.connectTimeoutTimer);
|
|
473
|
+
this.connectTimeoutTimer = null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private terminateCurrentSocket(reason: string, payload?: any) {
|
|
478
|
+
const ws = this.ws;
|
|
479
|
+
if (!ws) return;
|
|
480
|
+
this.appendLogToFile("WARN", "Connection", `Terminating websocket: ${reason}`, payload);
|
|
481
|
+
try {
|
|
482
|
+
ws.terminate();
|
|
483
|
+
} catch (err) {
|
|
484
|
+
this.appendLogToFile("WARN", "Connection", "Terminate websocket failed", err);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private sendJson(payload: any, category: string) {
|
|
489
|
+
const ws = this.ws;
|
|
490
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
ws.send(JSON.stringify(payload), (err) => {
|
|
494
|
+
if (!err) return;
|
|
495
|
+
this.appendLogToFile("WARN", category, "WebSocket send failed", err);
|
|
496
|
+
if (this.ws === ws) {
|
|
497
|
+
this.terminateCurrentSocket("send_failed", { category, message: err.message });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return true;
|
|
501
|
+
} catch (err) {
|
|
502
|
+
this.appendLogToFile("WARN", category, "WebSocket send threw", err);
|
|
503
|
+
if (this.ws === ws) {
|
|
504
|
+
this.terminateCurrentSocket("send_threw", err);
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private sendGatewayHello() {
|
|
511
|
+
this.sendJson({
|
|
512
|
+
type: "GATEWAY_HELLO",
|
|
513
|
+
gatewayId: this.options.gatewayId,
|
|
514
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
515
|
+
clientTime: Date.now(),
|
|
516
|
+
supportsBatch: true,
|
|
517
|
+
supportsGatewaySkillScope: true,
|
|
518
|
+
}, "Heartbeat");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private sendClientHeartbeat() {
|
|
522
|
+
this.sendJson({
|
|
523
|
+
type: "CLIENT_HEARTBEAT",
|
|
524
|
+
gatewayId: this.options.gatewayId,
|
|
525
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
526
|
+
clientTime: Date.now(),
|
|
527
|
+
}, "Heartbeat");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private sendAgentListReport(type: "AGENT_LIST_REPORT" | "AGENT_LIST_SYNC") {
|
|
531
|
+
this.sendJson({
|
|
532
|
+
type,
|
|
533
|
+
gatewayId: this.options.gatewayId,
|
|
534
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
535
|
+
clientTime: Date.now(),
|
|
536
|
+
}, "AgentScan");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private clearAgentScanTimer() {
|
|
540
|
+
if (this.agentScanTimer) {
|
|
541
|
+
clearInterval(this.agentScanTimer);
|
|
542
|
+
this.agentScanTimer = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
548
|
+
*/
|
|
549
|
+
private async handleMessage(msg: any) {
|
|
550
|
+
const { action, userId, code, url, force, version, replyId, isBuiltIn, installScope } = msg;
|
|
551
|
+
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
552
|
+
action,
|
|
553
|
+
userId,
|
|
554
|
+
code,
|
|
555
|
+
version,
|
|
556
|
+
replyId,
|
|
557
|
+
isBuiltIn,
|
|
558
|
+
installScope,
|
|
559
|
+
hasDirectUrl: Boolean(url),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (!action) {
|
|
563
|
+
this.appendLogToFile("WARN", "Command", `Message dropped: missing action`, msg);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (action === "GET_CRON_JOBS_BY_AGENT_ID") {
|
|
568
|
+
const agentId = typeof msg.agent_id === "string"
|
|
569
|
+
? msg.agent_id
|
|
570
|
+
: typeof msg.agentId === "string"
|
|
571
|
+
? msg.agentId
|
|
572
|
+
: userId;
|
|
573
|
+
if (!agentId) {
|
|
574
|
+
this.reply(replyId, { success: false, message: "Missing agent_id parameter", action });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const data = readCronJobsByAgentId(agentId, defaultOpenclawSqlitePath(), (err) => {
|
|
579
|
+
this.appendLogToFile("WARN", "Command", `GET_CRON_JOBS_BY_AGENT_ID sqlite lookup skipped`, err);
|
|
580
|
+
});
|
|
581
|
+
this.reply(replyId, { success: true, data, action });
|
|
582
|
+
} catch (err: any) {
|
|
583
|
+
this.appendLogToFile("ERROR", "Command", `GET_CRON_JOBS_BY_AGENT_ID threw`, err);
|
|
584
|
+
this.reply(replyId, { success: false, message: err.message, action });
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!userId) {
|
|
590
|
+
this.appendLogToFile("WARN", "Command", `Message dropped: missing userId`, msg);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// code 必须是单一路径段;不允许通过 basename 静默改写恶意输入。
|
|
595
|
+
const safeCode = normalizeCommandCode(code);
|
|
596
|
+
if (code !== undefined && !safeCode) {
|
|
597
|
+
this.reply(replyId, { success: false, message: `Invalid code: ${String(code)}`, action });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const gatewaySkillCommand = isGatewaySkillCommand(action, installScope);
|
|
602
|
+
// Gateway 公共 Skill 命令不依赖 Agent。其余命令继续执行严格的用户目录寻址。
|
|
603
|
+
const pureId = gatewaySkillCommand ? undefined : normalizeAssistantUserId(userId);
|
|
604
|
+
if (!gatewaySkillCommand && !pureId) {
|
|
605
|
+
this.reply(replyId, { success: false, message: `Invalid userId: ${userId}`, action });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
if (action === "INSTALL_SKILL") {
|
|
610
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
611
|
+
const targetDir = gatewaySkillCommand
|
|
612
|
+
? resolveGatewaySkillTarget()
|
|
613
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
614
|
+
console.log(`[skill-logger-plugin][WS] Executing INSTALL for user ${userId}, code: ${safeCode}`);
|
|
615
|
+
this.appendLogToFile("INFO", "Command", `INSTALL_SKILL received`, { userId, code: safeCode, version });
|
|
616
|
+
const result = await this.options.updater.manualInstall({
|
|
617
|
+
code: safeCode,
|
|
618
|
+
url,
|
|
619
|
+
version,
|
|
620
|
+
force: force !== false,
|
|
621
|
+
targetDir,
|
|
622
|
+
trace: this.createInstallTrace({ action, replyId, userId, code: safeCode }),
|
|
623
|
+
});
|
|
624
|
+
this.reply(replyId, {
|
|
625
|
+
success: result.success, message: result.message, action,
|
|
626
|
+
data: {code: safeCode, version, installScope: gatewaySkillCommand ? "gateway" : "agent"},
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
} else if (action === "UNINSTALL_SKILL") {
|
|
630
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
631
|
+
const targetDir = gatewaySkillCommand
|
|
632
|
+
? resolveGatewaySkillTarget()
|
|
633
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
634
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL for user ${userId}, code: ${safeCode}`);
|
|
635
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_SKILL received`, { userId, code: safeCode });
|
|
636
|
+
const skillPath = path.join(targetDir, safeCode);
|
|
637
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
638
|
+
this.reply(replyId, {
|
|
639
|
+
success: true, message: `Skill ${safeCode} removed`, action,
|
|
640
|
+
data: {code: safeCode, installScope: gatewaySkillCommand ? "gateway" : "agent", removed: true},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
} else if (action === "LIST_SKILLS") {
|
|
644
|
+
const regularTargetDir = await this.resolveRegularSkillTarget(userId);
|
|
645
|
+
|
|
646
|
+
// 用户视角的有效 Skill = Agent workspace Skill + Gateway 顶层公共 Skill。
|
|
647
|
+
// 同 code 时公共内置版本后写覆盖,确保不会被快照差分误判为缺失。
|
|
648
|
+
const skillsByCode = new Map<string, any>();
|
|
649
|
+
const sources = [
|
|
650
|
+
{dir: regularTargetDir, gatewayBuiltIn: false},
|
|
651
|
+
{dir: resolveGatewaySkillTarget(), gatewayBuiltIn: true},
|
|
652
|
+
];
|
|
653
|
+
for (const source of sources) {
|
|
654
|
+
let entries;
|
|
655
|
+
try {
|
|
656
|
+
entries = await fs.readdir(source.dir, {withFileTypes: true});
|
|
657
|
+
} catch (err: any) {
|
|
658
|
+
// 新用户的 Agent 私有目录尚未创建时,仍必须返回 Gateway 公共内置 Skill。
|
|
659
|
+
if (err?.code === "ENOENT") continue;
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
const dirs = entries.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
663
|
+
for (const e of dirs) {
|
|
664
|
+
const skillDir = path.join(source.dir, e.name);
|
|
665
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
666
|
+
try {
|
|
667
|
+
const stat = await fs.stat(skillMdPath);
|
|
668
|
+
if (!stat.isFile()) continue;
|
|
669
|
+
} catch {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const metaPath = path.join(skillDir, ".meta.json");
|
|
674
|
+
let isPlatform = source.gatewayBuiltIn;
|
|
675
|
+
let isBuiltIn = source.gatewayBuiltIn || e.isSymbolicLink();
|
|
676
|
+
let metaData: any = null;
|
|
677
|
+
let name = e.name;
|
|
678
|
+
let description = "";
|
|
679
|
+
let skillVersion = "";
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const mdContent = await fs.readFile(skillMdPath, "utf8");
|
|
683
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
684
|
+
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
685
|
+
if (parsedName) name = parsedName;
|
|
686
|
+
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
687
|
+
if (descMatch?.[2]) description = descMatch[2].replace(/\n\s+/g, " ").trim();
|
|
688
|
+
} catch {}
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const parsed = JSON.parse(await fs.readFile(metaPath, "utf8"));
|
|
692
|
+
if (parsed) {
|
|
693
|
+
if (parsed.ownerId === "CMS" || parsed.ownerId === "CMS_COMPAT") isPlatform = true;
|
|
694
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === "built-in") isBuiltIn = true;
|
|
695
|
+
metaData = parsed;
|
|
696
|
+
}
|
|
697
|
+
} catch {}
|
|
698
|
+
|
|
699
|
+
const resolvedVersion = await readSkillVersion(skillDir);
|
|
700
|
+
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
701
|
+
|
|
702
|
+
if (isPlatform) {
|
|
703
|
+
skillsByCode.set(e.name, {
|
|
704
|
+
code: e.name, isPlatform: true, isBuiltIn, version: skillVersion,
|
|
705
|
+
name, description, publishedAt: metaData?.publishedAt,
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
skillsByCode.set(e.name, {
|
|
709
|
+
code: e.name, isPlatform: false, isBuiltIn, version: skillVersion,
|
|
710
|
+
name, description,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
this.reply(replyId, { success: true, data: [...skillsByCode.values()], action });
|
|
716
|
+
|
|
717
|
+
} else if (action === "UPDATE_SKILL") {
|
|
718
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
719
|
+
// 更新可能是大批量广播,增加防阻塞的随机延时 (0~5秒)
|
|
720
|
+
const delayMs = Math.random() * 5000;
|
|
721
|
+
const syncBuiltInTemplate = !gatewaySkillCommand && shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
722
|
+
console.log(`[skill-logger-plugin][WS] Scheduled UPDATE for user ${userId}, code: ${safeCode} in ${Math.round(delayMs)}ms`);
|
|
723
|
+
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
724
|
+
userId,
|
|
725
|
+
code: safeCode,
|
|
726
|
+
version,
|
|
727
|
+
isBuiltIn,
|
|
728
|
+
installScope,
|
|
729
|
+
syncBuiltInTemplate,
|
|
730
|
+
delayMs: Math.round(delayMs),
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
setTimeout(async () => {
|
|
734
|
+
try {
|
|
735
|
+
// 延时期间配置可能变化;在真正写盘前重新校验 enabled 与 workspace。
|
|
736
|
+
const targetDir = gatewaySkillCommand
|
|
737
|
+
? resolveGatewaySkillTarget()
|
|
738
|
+
: await this.resolveRegularSkillTarget(userId);
|
|
739
|
+
const additionalTargetDirs: string[] = syncBuiltInTemplate
|
|
740
|
+
? [path.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")]
|
|
741
|
+
: [];
|
|
742
|
+
|
|
743
|
+
// 同步更新用户的 expert skill 目录
|
|
744
|
+
const userSkillRoot = pureId
|
|
745
|
+
? await findInstalledExpertSkillsRoot(openclawHome(), pureId, safeCode)
|
|
746
|
+
: undefined;
|
|
747
|
+
if (userSkillRoot) additionalTargetDirs.push(userSkillRoot);
|
|
748
|
+
const result = await this.options.updater.manualInstall({
|
|
749
|
+
code: safeCode,
|
|
750
|
+
url,
|
|
751
|
+
version,
|
|
752
|
+
force: true,
|
|
753
|
+
targetDir,
|
|
754
|
+
additionalTargetDirs,
|
|
755
|
+
trace: this.createInstallTrace({
|
|
756
|
+
action,
|
|
757
|
+
replyId,
|
|
758
|
+
userId,
|
|
759
|
+
code: safeCode,
|
|
760
|
+
isBuiltIn,
|
|
761
|
+
syncBuiltInTemplate,
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
this.appendLogToFile(result.success ? "INFO" : "ERROR", "Command", `UPDATE_SKILL completed`, {
|
|
765
|
+
userId,
|
|
766
|
+
code: safeCode,
|
|
767
|
+
replyId,
|
|
768
|
+
success: result.success,
|
|
769
|
+
message: result.message,
|
|
770
|
+
syncBuiltInTemplate,
|
|
771
|
+
});
|
|
772
|
+
if (replyId) {
|
|
773
|
+
this.reply(replyId, {
|
|
774
|
+
success: result.success, message: result.message, action,
|
|
775
|
+
data: {code: safeCode, version, installScope: gatewaySkillCommand ? "gateway" : "agent"},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
} catch (e: any) {
|
|
779
|
+
this.appendLogToFile("ERROR", "Command", `UPDATE_SKILL threw`, {
|
|
780
|
+
userId,
|
|
781
|
+
code: safeCode,
|
|
782
|
+
replyId,
|
|
783
|
+
message: e?.message || String(e),
|
|
784
|
+
stack: e?.stack,
|
|
785
|
+
});
|
|
786
|
+
if (replyId) this.reply(replyId, { success: false, message: e.message, action });
|
|
787
|
+
}
|
|
788
|
+
}, delayMs);
|
|
789
|
+
|
|
790
|
+
} else if (action === "INSTALL_EXPERT") {
|
|
791
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
792
|
+
const { name, version, downloadUrl, skills } = msg;
|
|
793
|
+
console.log(`[skill-logger-plugin][WS] Executing INSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
794
|
+
this.appendLogToFile("INFO", "Command", `INSTALL_EXPERT received`, { userId, code: safeCode, version, downloadUrl });
|
|
795
|
+
|
|
796
|
+
const userSkillRoot = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user");
|
|
797
|
+
|
|
798
|
+
// 1. 检查当前版本,同版本跳过
|
|
799
|
+
const expertTarget = path.join(userSkillRoot, "experts", safeCode);
|
|
800
|
+
let skipInstall = false;
|
|
801
|
+
const metaPath = path.join(expertTarget, ".meta.json");
|
|
802
|
+
try {
|
|
803
|
+
const raw = await fs.readFile(metaPath, "utf-8");
|
|
804
|
+
const existing = JSON.parse(raw);
|
|
805
|
+
if (existing.version && existing.version === (version || '1.0.0')) {
|
|
806
|
+
skipInstall = true;
|
|
807
|
+
}
|
|
808
|
+
} catch {}
|
|
809
|
+
|
|
810
|
+
if (!skipInstall) {
|
|
811
|
+
await fs.mkdir(path.dirname(expertTarget), { recursive: true });
|
|
812
|
+
const expertResult = await this.options.updater.installExpertZipFromUrl(downloadUrl, expertTarget);
|
|
813
|
+
if (!expertResult.success) {
|
|
814
|
+
throw new Error(`专家安装失败: ${expertResult.message}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 写入/更新版本信息
|
|
819
|
+
const meta = { code: safeCode, name, version: version || '1.0.0', installedAt: Date.now() };
|
|
820
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
821
|
+
|
|
822
|
+
// 2. 安装依赖的 skills
|
|
823
|
+
const skillTargetRoot = path.join(userSkillRoot, "skills");
|
|
824
|
+
await fs.mkdir(skillTargetRoot, { recursive: true });
|
|
825
|
+
const skillResults: string[] = [];
|
|
826
|
+
if (Array.isArray(skills)) {
|
|
827
|
+
for (const sk of skills) {
|
|
828
|
+
const skillCode = normalizeCommandCode(sk?.code);
|
|
829
|
+
if (!skillCode) {
|
|
830
|
+
skillResults.push(`${String(sk?.code || 'unknown')}: 失败 - 非法的 Skill code`);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (!sk.downloadUrl) {
|
|
834
|
+
skillResults.push(`${skillCode}: 失败 - 缺少下载地址`);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const result = await this.options.updater.manualInstall({
|
|
839
|
+
code: skillCode,
|
|
840
|
+
url: sk.downloadUrl,
|
|
841
|
+
version: sk.version,
|
|
842
|
+
force: true,
|
|
843
|
+
targetDir: skillTargetRoot,
|
|
844
|
+
});
|
|
845
|
+
skillResults.push(`${skillCode}: ${result.success ? '成功' : '失败 - ' + result.message}`);
|
|
846
|
+
} catch (e: any) {
|
|
847
|
+
skillResults.push(`${skillCode}: 失败 - ${e.message}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
this.reply(replyId, {
|
|
853
|
+
success: true,
|
|
854
|
+
message: `专家 ${safeCode} 安装完成`,
|
|
855
|
+
action,
|
|
856
|
+
data: { expertCode: safeCode, skills: skillResults },
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
} else if (action === "UNINSTALL_EXPERT") {
|
|
860
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
861
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
862
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_EXPERT received`, { userId, code: safeCode });
|
|
863
|
+
|
|
864
|
+
const expertPath = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts", safeCode);
|
|
865
|
+
await fs.rm(expertPath, { recursive: true, force: true });
|
|
866
|
+
|
|
867
|
+
this.reply(replyId, { success: true, message: `专家 ${safeCode} 已卸载`, action });
|
|
868
|
+
|
|
869
|
+
} else if (action === "LIST_EXPERTS") {
|
|
870
|
+
console.log(`[skill-logger-plugin][WS] Executing LIST_EXPERTS for user ${userId}`);
|
|
871
|
+
this.appendLogToFile("INFO", "Command", `LIST_EXPERTS received`, { userId });
|
|
872
|
+
|
|
873
|
+
const expertsDir = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts");
|
|
874
|
+
const list: any[] = [];
|
|
875
|
+
try {
|
|
876
|
+
const stat = await fs.stat(expertsDir);
|
|
877
|
+
if (stat.isDirectory()) {
|
|
878
|
+
const entries = await fs.readdir(expertsDir, { withFileTypes: true });
|
|
879
|
+
for (const e of entries) {
|
|
880
|
+
if (!e.isDirectory()) continue;
|
|
881
|
+
const metaPath = path.join(expertsDir, e.name, ".meta.json");
|
|
882
|
+
try {
|
|
883
|
+
const raw = await fs.readFile(metaPath, "utf-8");
|
|
884
|
+
const meta = JSON.parse(raw);
|
|
885
|
+
list.push({
|
|
886
|
+
code: meta.code || e.name,
|
|
887
|
+
name: meta.name || e.name,
|
|
888
|
+
version: meta.version || '',
|
|
889
|
+
installedAt: meta.installedAt,
|
|
890
|
+
});
|
|
891
|
+
} catch {
|
|
892
|
+
list.push({ code: e.name, name: e.name, version: '' });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch {}
|
|
897
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
898
|
+
|
|
899
|
+
} else {
|
|
900
|
+
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
901
|
+
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
902
|
+
this.reply(replyId, { success: false, message: `Unknown action: ${action}`, action });
|
|
903
|
+
}
|
|
904
|
+
} catch (err: any) {
|
|
905
|
+
this.appendLogToFile("ERROR", "Command", `Error executing action ${action}`, err);
|
|
906
|
+
this.reply(replyId, { success: false, message: err.message, action });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private reply(replyId: string, payload: any) {
|
|
911
|
+
this.appendLogToFile("INFO", "Command", `Replying to command`, { replyId, payload });
|
|
912
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !replyId) {
|
|
913
|
+
this.appendLogToFile("WARN", "Command", `Cannot reply: WS closed or missing replyId`, { replyId });
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }), (err) => {
|
|
917
|
+
if (err) {
|
|
918
|
+
this.appendLogToFile("ERROR", "Command", `Reply send failed`, { replyId, message: err.message });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
this.appendLogToFile("INFO", "Command", `Reply sent`, { replyId });
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
public destroy() {
|
|
926
|
+
this.isDestroyed = true;
|
|
927
|
+
this.clearReconnectTimer();
|
|
928
|
+
this.clearAgentScanTimer();
|
|
929
|
+
this.clearHeartbeat();
|
|
930
|
+
this.clearConnectTimeout();
|
|
931
|
+
if (this.ws) {
|
|
932
|
+
this.ws.terminate(); // 强行销毁,斩断半开连接残留
|
|
933
|
+
this.ws = null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|