@spzhongwin/skill-logger-plugin 1.0.11 → 1.0.13
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/active-skills.js +67 -0
- package/dist/active-skills.test.js +29 -0
- package/dist/config-sync.js +439 -0
- package/dist/config-sync.test.js +145 -0
- package/dist/hooks.js +337 -0
- package/dist/hooks.test.js +123 -0
- package/dist/http.js +54 -0
- package/dist/identity.js +56 -0
- package/dist/index.js +240 -78
- package/dist/index.test.js +39 -0
- package/dist/integration.test.js +102 -0
- package/dist/matcher.js +362 -0
- package/dist/matcher.test.js +139 -0
- package/dist/paths.js +62 -0
- package/dist/paths.test.js +49 -0
- package/dist/reporter.js +267 -0
- package/dist/reporter.test.js +128 -0
- package/dist/semver.js +64 -0
- package/dist/semver.test.js +21 -0
- package/dist/skill-version.js +23 -0
- package/dist/types.js +9 -0
- package/dist/updater.js +352 -0
- package/dist/updater.test.js +212 -0
- package/dist/ws-client.js +484 -0
- package/openclaw.plugin.json +50 -50
- package/package.json +37 -37
- 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/hooks.test.ts +251 -251
- package/src/hooks.ts +517 -517
- package/src/http.ts +61 -61
- package/src/identity.ts +64 -64
- package/src/index.test.ts +53 -53
- package/src/index.ts +226 -226
- 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 +298 -298
- package/src/sample-config.json +72 -72
- package/src/semver.test.ts +23 -23
- package/src/semver.ts +60 -60
- package/src/skill-version.ts +53 -53
- package/src/types.ts +198 -198
- package/src/updater.test.ts +325 -237
- package/src/updater.ts +549 -433
- package/src/ws-client.test.ts +48 -37
- package/src/ws-client.ts +717 -642
- package/test-ws.ts +17 -17
- package/tsconfig.json +14 -14
package/src/ws-client.ts
CHANGED
|
@@ -1,642 +1,717 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import { SkillUpdater } from "./updater.ts";
|
|
5
|
-
import { openclawHome } from "./paths.ts";
|
|
6
|
-
import { readSkillVersion } from "./skill-version.ts";
|
|
7
|
-
|
|
8
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
|
-
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
10
|
-
const AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1000;
|
|
11
|
-
const ASSISTANT_WORKSPACE_PREFIX = "workspace-assistant-";
|
|
12
|
-
const ASSISTANT_AGENT_PREFIX = "assistant-";
|
|
13
|
-
const ASSISTANT_WORKSPACE_ID_RE = /^\d{5,}$/;
|
|
14
|
-
|
|
15
|
-
export function parseAssistantWorkspaceAgentId(entryName: string): string | undefined {
|
|
16
|
-
if (!entryName.startsWith(ASSISTANT_WORKSPACE_PREFIX)) return undefined;
|
|
17
|
-
const suffix = entryName.slice(ASSISTANT_WORKSPACE_PREFIX.length);
|
|
18
|
-
if (!ASSISTANT_WORKSPACE_ID_RE.test(suffix)) return undefined;
|
|
19
|
-
return `${ASSISTANT_AGENT_PREFIX}${suffix}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function normalizeAssistantUserId(userId: string): string | undefined {
|
|
23
|
-
const safeUserId = path.basename(userId);
|
|
24
|
-
if (safeUserId !== userId) return undefined;
|
|
25
|
-
const pureId = safeUserId.startsWith(ASSISTANT_AGENT_PREFIX)
|
|
26
|
-
? safeUserId.slice(ASSISTANT_AGENT_PREFIX.length)
|
|
27
|
-
: safeUserId;
|
|
28
|
-
if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return undefined;
|
|
29
|
-
return pureId;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
private
|
|
47
|
-
private
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
private
|
|
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
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
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
|
-
this.
|
|
130
|
-
this.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
if (
|
|
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
|
-
this.
|
|
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
|
-
if (
|
|
379
|
-
this.
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
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
|
-
}
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { SkillUpdater } from "./updater.ts";
|
|
5
|
+
import { openclawHome } from "./paths.ts";
|
|
6
|
+
import { readSkillVersion } from "./skill-version.ts";
|
|
7
|
+
|
|
8
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
|
+
const HEARTBEAT_ACK_TIMEOUT_MS = 75_000;
|
|
10
|
+
const AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1000;
|
|
11
|
+
const ASSISTANT_WORKSPACE_PREFIX = "workspace-assistant-";
|
|
12
|
+
const ASSISTANT_AGENT_PREFIX = "assistant-";
|
|
13
|
+
const ASSISTANT_WORKSPACE_ID_RE = /^\d{5,}$/;
|
|
14
|
+
|
|
15
|
+
export function parseAssistantWorkspaceAgentId(entryName: string): string | undefined {
|
|
16
|
+
if (!entryName.startsWith(ASSISTANT_WORKSPACE_PREFIX)) return undefined;
|
|
17
|
+
const suffix = entryName.slice(ASSISTANT_WORKSPACE_PREFIX.length);
|
|
18
|
+
if (!ASSISTANT_WORKSPACE_ID_RE.test(suffix)) return undefined;
|
|
19
|
+
return `${ASSISTANT_AGENT_PREFIX}${suffix}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeAssistantUserId(userId: string): string | undefined {
|
|
23
|
+
const safeUserId = path.basename(userId);
|
|
24
|
+
if (safeUserId !== userId) return undefined;
|
|
25
|
+
const pureId = safeUserId.startsWith(ASSISTANT_AGENT_PREFIX)
|
|
26
|
+
? safeUserId.slice(ASSISTANT_AGENT_PREFIX.length)
|
|
27
|
+
: safeUserId;
|
|
28
|
+
if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return undefined;
|
|
29
|
+
return pureId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldSyncBuiltInTemplate(action: string, isBuiltIn: unknown): boolean {
|
|
33
|
+
return action === "UPDATE_SKILL" && isBuiltIn === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WsClientOptions {
|
|
37
|
+
serverUrl: string; // 例如: wss://api.aishuo.co/gateway/ws
|
|
38
|
+
authToken?: string; // 用于网关鉴权
|
|
39
|
+
gatewayId: string; // 当前网关宿主的标识,方便中控做集群分发
|
|
40
|
+
updater: SkillUpdater; // 传入原来已有的 updater 实例
|
|
41
|
+
enableFileLog?: boolean; // 文件日志开关
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class GatewayWsClient {
|
|
45
|
+
private ws: WebSocket | null = null;
|
|
46
|
+
private options: WsClientOptions;
|
|
47
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
48
|
+
private agentScanTimer: NodeJS.Timeout | null = null;
|
|
49
|
+
private pingTimer: NodeJS.Timeout | null = null;
|
|
50
|
+
private connectTimeoutTimer: NodeJS.Timeout | null = null;
|
|
51
|
+
private lastServerAckAt = 0;
|
|
52
|
+
private reconnectAttempts = 0;
|
|
53
|
+
private isDestroyed = false;
|
|
54
|
+
|
|
55
|
+
// 本地缓存的 agent ID 列表
|
|
56
|
+
private currentAgentIds = new Set<string>();
|
|
57
|
+
|
|
58
|
+
private appendLogToFile(level: string, category: string, message: string, payload?: any) {
|
|
59
|
+
if (!this.options.enableFileLog) return;
|
|
60
|
+
try {
|
|
61
|
+
const ts = new Date().toISOString();
|
|
62
|
+
let logLine = `[${ts}] [${level}] [${category}] ${message}`;
|
|
63
|
+
if (payload !== undefined && payload !== null) {
|
|
64
|
+
// 如果是 Error 对象,主动提取 stack
|
|
65
|
+
if (payload instanceof Error) {
|
|
66
|
+
logLine += `\n Stack: ${payload.stack || payload.message}`;
|
|
67
|
+
} else {
|
|
68
|
+
logLine += ` | Data: ${typeof payload === 'object' ? JSON.stringify(payload) : payload}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
logLine += '\n';
|
|
72
|
+
const logsDir = path.join(openclawHome(), "logs");
|
|
73
|
+
fs.mkdir(logsDir, { recursive: true }).then(() => {
|
|
74
|
+
const logPath = path.join(logsDir, "skill-logger.err");
|
|
75
|
+
fs.appendFile(logPath, logLine).catch(()=>{});
|
|
76
|
+
}).catch(()=>{});
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private createInstallTrace(context: Record<string, unknown>) {
|
|
83
|
+
return (stage: string, data?: Record<string, unknown>) => {
|
|
84
|
+
this.appendLogToFile(stage === "install.failed" ? "ERROR" : "INFO", "Install", stage, {
|
|
85
|
+
...context,
|
|
86
|
+
...data,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
constructor(options: WsClientOptions) {
|
|
92
|
+
this.options = options;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public connect() {
|
|
96
|
+
if (this.isDestroyed) return;
|
|
97
|
+
if (this.ws && (
|
|
98
|
+
this.ws.readyState === WebSocket.OPEN ||
|
|
99
|
+
this.ws.readyState === WebSocket.CONNECTING
|
|
100
|
+
)) {
|
|
101
|
+
this.appendLogToFile("INFO", "Connection", "Connect skipped: websocket already active", { readyState: this.ws.readyState });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.clearConnectTimeout();
|
|
106
|
+
|
|
107
|
+
const msgConnect = `Connecting to Central Service: ${this.options.serverUrl}`;
|
|
108
|
+
console.log(`[skill-logger-plugin][WS] ${msgConnect}`);
|
|
109
|
+
this.appendLogToFile("INFO", "Connection", msgConnect);
|
|
110
|
+
|
|
111
|
+
const headers: Record<string, string> = {
|
|
112
|
+
"X-Gateway-Id": this.options.gatewayId,
|
|
113
|
+
};
|
|
114
|
+
if (this.options.authToken) {
|
|
115
|
+
headers["Authorization"] = this.options.authToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const ws = new WebSocket(this.options.serverUrl, { headers });
|
|
120
|
+
this.ws = ws;
|
|
121
|
+
this.connectTimeoutTimer = setTimeout(() => {
|
|
122
|
+
if (this.ws === ws && ws.readyState === WebSocket.CONNECTING) {
|
|
123
|
+
this.appendLogToFile("WARN", "Connection", "Connection timed out before open; terminating socket.");
|
|
124
|
+
ws.terminate();
|
|
125
|
+
}
|
|
126
|
+
}, 15000);
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
console.error(`[skill-logger-plugin][WS] Sync init error:`, err);
|
|
129
|
+
this.appendLogToFile("ERROR", "Connection", "Sync initialization error", err);
|
|
130
|
+
this.scheduleReconnect();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ws = this.ws;
|
|
135
|
+
|
|
136
|
+
ws.on("open", async () => {
|
|
137
|
+
if (this.ws !== ws) return;
|
|
138
|
+
console.log(`[skill-logger-plugin][WS] Connected successfully!`);
|
|
139
|
+
this.appendLogToFile("INFO", "Connection", "Connected successfully!");
|
|
140
|
+
this.clearConnectTimeout();
|
|
141
|
+
this.reconnectAttempts = 0;
|
|
142
|
+
this.lastServerAckAt = Date.now();
|
|
143
|
+
this.clearReconnectTimer();
|
|
144
|
+
|
|
145
|
+
// 首次连接,全量扫描并上报,同时进行应用层握手,确保服务端能把 DB 在线态刷新成真实状态。
|
|
146
|
+
await this.scanAndReportAgents(true);
|
|
147
|
+
this.sendGatewayHello();
|
|
148
|
+
this.startHeartbeat();
|
|
149
|
+
|
|
150
|
+
// 开启 3 分钟定期的自动扫码增量同步
|
|
151
|
+
if (!this.agentScanTimer) {
|
|
152
|
+
this.agentScanTimer = setInterval(() => {
|
|
153
|
+
this.scanAndReportAgents(true);
|
|
154
|
+
}, AGENT_SCAN_INTERVAL_MS);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
ws.on("pong", () => {
|
|
159
|
+
this.lastServerAckAt = Date.now();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
ws.on("message", async (data) => {
|
|
163
|
+
if (this.ws !== ws) return;
|
|
164
|
+
try {
|
|
165
|
+
const msg = JSON.parse(data.toString());
|
|
166
|
+
if (msg?.type === "GATEWAY_HELLO_ACK" || msg?.type === "HEARTBEAT_ACK") {
|
|
167
|
+
this.lastServerAckAt = Date.now();
|
|
168
|
+
this.appendLogToFile("INFO", "Heartbeat", "Server heartbeat acknowledged", { type: msg.type, connectionId: msg.connectionId });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (msg?.type === "BATCH_COMMAND" && Array.isArray(msg.commands)) {
|
|
172
|
+
const { commands, type, ...shared } = msg;
|
|
173
|
+
for (const cmd of commands) {
|
|
174
|
+
await this.handleMessage({ ...shared, userId: cmd.userId, replyId: cmd.replyId });
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await this.handleMessage(msg);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(`[skill-logger-plugin][WS] Failed to parse/handle message:`, err);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
ws.on("close", () => {
|
|
185
|
+
console.warn(`[skill-logger-plugin][WS] Connection closed.`);
|
|
186
|
+
this.appendLogToFile("WARN", "Connection", "Connection closed. Starting reconnect timer.");
|
|
187
|
+
if (this.ws === ws) {
|
|
188
|
+
this.ws = null;
|
|
189
|
+
this.clearConnectTimeout();
|
|
190
|
+
this.clearAgentScanTimer();
|
|
191
|
+
this.clearHeartbeat();
|
|
192
|
+
this.scheduleReconnect();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
ws.on("error", (err) => {
|
|
197
|
+
console.error(`[skill-logger-plugin][WS] Connection error:`, err);
|
|
198
|
+
this.appendLogToFile("ERROR", "Connection", "Connection error", err);
|
|
199
|
+
if (this.ws === ws) {
|
|
200
|
+
ws.close(); // 触发 close 事件进行重连
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 扫描 OpenClaw 根目录下的 workspace-assistant-{userId} 目录。
|
|
207
|
+
* userId 必须是至少 5 位数字。
|
|
208
|
+
*/
|
|
209
|
+
private async scanAndReportAgents(isInitialReport: boolean) {
|
|
210
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const rootPath = openclawHome();
|
|
214
|
+
let entries: string[] = [];
|
|
215
|
+
try {
|
|
216
|
+
entries = await fs.readdir(rootPath);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// 读目录失败通常是瞬时性的(磁盘抖动/权限/句柄耗尽等),不代表这台机器真的没有用户了。
|
|
219
|
+
// 跳过本轮上报、保留上次已知状态,等下一次扫描自然重试,避免把瞬时故障放大成
|
|
220
|
+
// "全部用户离线"(服务端收到空列表会把这台网关下所有用户标记 OFFLINE)。
|
|
221
|
+
this.appendLogToFile("WARN", "AgentScan", "OpenClaw home is not readable; skipping this scan cycle", e);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const newAgentIds = new Set<string>();
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const agentId = parseAssistantWorkspaceAgentId(entry);
|
|
228
|
+
if (agentId) newAgentIds.add(agentId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let changed = false;
|
|
232
|
+
if (newAgentIds.size !== this.currentAgentIds.size) {
|
|
233
|
+
changed = true;
|
|
234
|
+
} else {
|
|
235
|
+
for (const id of newAgentIds) {
|
|
236
|
+
if (!this.currentAgentIds.has(id)) {
|
|
237
|
+
changed = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.currentAgentIds = newAgentIds;
|
|
244
|
+
|
|
245
|
+
if (isInitialReport) {
|
|
246
|
+
this.appendLogToFile("INFO", "AgentScan", `Reporting full agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
247
|
+
this.sendAgentListReport("AGENT_LIST_REPORT");
|
|
248
|
+
} else if (changed) {
|
|
249
|
+
this.appendLogToFile("INFO", "AgentScan", `Syncing changed agent list`, { count: this.currentAgentIds.size, agents: Array.from(this.currentAgentIds) });
|
|
250
|
+
this.sendAgentListReport("AGENT_LIST_SYNC");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
console.error(`[skill-logger-plugin][WS] Failed to scan agents:`, err);
|
|
255
|
+
this.appendLogToFile("ERROR", "AgentScan", `Failed to scan agents`, err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private startHeartbeat() {
|
|
260
|
+
this.clearHeartbeat();
|
|
261
|
+
this.pingTimer = setInterval(() => {
|
|
262
|
+
const ws = this.ws;
|
|
263
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
264
|
+
|
|
265
|
+
const ackAge = Date.now() - this.lastServerAckAt;
|
|
266
|
+
if (ackAge > HEARTBEAT_ACK_TIMEOUT_MS) {
|
|
267
|
+
this.appendLogToFile("WARN", "Heartbeat", "Server heartbeat ACK timed out; terminating socket for reconnect", { ackAge });
|
|
268
|
+
ws.terminate();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
ws.ping();
|
|
273
|
+
this.sendClientHeartbeat();
|
|
274
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
275
|
+
this.sendClientHeartbeat();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private clearHeartbeat() {
|
|
279
|
+
if (this.pingTimer) {
|
|
280
|
+
clearInterval(this.pingTimer);
|
|
281
|
+
this.pingTimer = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private scheduleReconnect() {
|
|
286
|
+
if (this.isDestroyed || this.reconnectTimer) return;
|
|
287
|
+
|
|
288
|
+
// 闭环完善:引入随机 Jitter 抖动,打散服务端重启时可能引发的瞬间重连风暴
|
|
289
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
290
|
+
const baseDelay = Math.min(30000, 2000 * Math.max(1, 2 ** this.reconnectAttempts));
|
|
291
|
+
const delay = baseDelay + jitter;
|
|
292
|
+
this.reconnectAttempts += 1;
|
|
293
|
+
|
|
294
|
+
console.log(`[skill-logger-plugin][WS] Reconnecting in ${delay}ms...`);
|
|
295
|
+
this.appendLogToFile("INFO", "Connection", "Reconnect scheduled", { delay, attempt: this.reconnectAttempts });
|
|
296
|
+
this.reconnectTimer = setTimeout(() => {
|
|
297
|
+
this.reconnectTimer = null;
|
|
298
|
+
this.connect();
|
|
299
|
+
}, delay);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private clearReconnectTimer() {
|
|
303
|
+
if (this.reconnectTimer) {
|
|
304
|
+
clearTimeout(this.reconnectTimer);
|
|
305
|
+
this.reconnectTimer = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private clearConnectTimeout() {
|
|
310
|
+
if (this.connectTimeoutTimer) {
|
|
311
|
+
clearTimeout(this.connectTimeoutTimer);
|
|
312
|
+
this.connectTimeoutTimer = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private terminateCurrentSocket(reason: string, payload?: any) {
|
|
317
|
+
const ws = this.ws;
|
|
318
|
+
if (!ws) return;
|
|
319
|
+
this.appendLogToFile("WARN", "Connection", `Terminating websocket: ${reason}`, payload);
|
|
320
|
+
try {
|
|
321
|
+
ws.terminate();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
this.appendLogToFile("WARN", "Connection", "Terminate websocket failed", err);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private sendJson(payload: any, category: string) {
|
|
328
|
+
const ws = this.ws;
|
|
329
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
ws.send(JSON.stringify(payload), (err) => {
|
|
333
|
+
if (!err) return;
|
|
334
|
+
this.appendLogToFile("WARN", category, "WebSocket send failed", err);
|
|
335
|
+
if (this.ws === ws) {
|
|
336
|
+
this.terminateCurrentSocket("send_failed", { category, message: err.message });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
return true;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
this.appendLogToFile("WARN", category, "WebSocket send threw", err);
|
|
342
|
+
if (this.ws === ws) {
|
|
343
|
+
this.terminateCurrentSocket("send_threw", err);
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private sendGatewayHello() {
|
|
350
|
+
this.sendJson({
|
|
351
|
+
type: "GATEWAY_HELLO",
|
|
352
|
+
gatewayId: this.options.gatewayId,
|
|
353
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
354
|
+
clientTime: Date.now(),
|
|
355
|
+
supportsBatch: true,
|
|
356
|
+
}, "Heartbeat");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private sendClientHeartbeat() {
|
|
360
|
+
this.sendJson({
|
|
361
|
+
type: "CLIENT_HEARTBEAT",
|
|
362
|
+
gatewayId: this.options.gatewayId,
|
|
363
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
364
|
+
clientTime: Date.now(),
|
|
365
|
+
}, "Heartbeat");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private sendAgentListReport(type: "AGENT_LIST_REPORT" | "AGENT_LIST_SYNC") {
|
|
369
|
+
this.sendJson({
|
|
370
|
+
type,
|
|
371
|
+
gatewayId: this.options.gatewayId,
|
|
372
|
+
agentIds: Array.from(this.currentAgentIds),
|
|
373
|
+
clientTime: Date.now(),
|
|
374
|
+
}, "AgentScan");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private clearAgentScanTimer() {
|
|
378
|
+
if (this.agentScanTimer) {
|
|
379
|
+
clearInterval(this.agentScanTimer);
|
|
380
|
+
this.agentScanTimer = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 核心指令分发中心:完全跳过沙盒,基于 userId 直接进行底层物理文件操作
|
|
386
|
+
*/
|
|
387
|
+
private async handleMessage(msg: any) {
|
|
388
|
+
const { action, userId, code, url, force, version, replyId, isBuiltIn } = msg;
|
|
389
|
+
this.appendLogToFile("INFO", "Command", `Received WS message`, {
|
|
390
|
+
action,
|
|
391
|
+
userId,
|
|
392
|
+
code,
|
|
393
|
+
version,
|
|
394
|
+
replyId,
|
|
395
|
+
isBuiltIn,
|
|
396
|
+
hasDirectUrl: Boolean(url),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (!action || !userId) {
|
|
400
|
+
this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 闭环完善:清理 code,并严格校验 userId,防止恶意指令通过 '../' 引发宿主机目录穿越攻击
|
|
405
|
+
const safeCode = code ? path.basename(code) : undefined;
|
|
406
|
+
|
|
407
|
+
// 100% 确定性安全寻址:userId 只接受纯数字或 assistant-数字,且数字至少 5 位。
|
|
408
|
+
const pureId = normalizeAssistantUserId(userId);
|
|
409
|
+
if (!pureId) {
|
|
410
|
+
this.reply(replyId, { success: false, message: `Invalid userId: ${userId}`, action });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const targetDir = path.join(openclawHome(), `workspace-assistant-${pureId}`, "skills");
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
if (action === "INSTALL_SKILL") {
|
|
417
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
418
|
+
console.log(`[skill-logger-plugin][WS] Executing INSTALL for user ${userId}, code: ${safeCode}`);
|
|
419
|
+
this.appendLogToFile("INFO", "Command", `INSTALL_SKILL received`, { userId, code: safeCode, version });
|
|
420
|
+
const result = await this.options.updater.manualInstall({
|
|
421
|
+
code: safeCode,
|
|
422
|
+
url,
|
|
423
|
+
version,
|
|
424
|
+
force: force !== false,
|
|
425
|
+
targetDir,
|
|
426
|
+
trace: this.createInstallTrace({ action, replyId, userId, code: safeCode }),
|
|
427
|
+
});
|
|
428
|
+
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
429
|
+
|
|
430
|
+
} else if (action === "UNINSTALL_SKILL") {
|
|
431
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
432
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL for user ${userId}, code: ${safeCode}`);
|
|
433
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_SKILL received`, { userId, code: safeCode });
|
|
434
|
+
const skillPath = path.join(targetDir, safeCode);
|
|
435
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
436
|
+
this.reply(replyId, { success: true, message: `Skill ${safeCode} removed`, action });
|
|
437
|
+
|
|
438
|
+
} else if (action === "LIST_SKILLS") {
|
|
439
|
+
let list: any[] = [];
|
|
440
|
+
let targetDirExists = false;
|
|
441
|
+
try {
|
|
442
|
+
const targetStat = await fs.stat(targetDir);
|
|
443
|
+
targetDirExists = targetStat.isDirectory();
|
|
444
|
+
} catch (err: any) {
|
|
445
|
+
if (err?.code !== "ENOENT") throw err;
|
|
446
|
+
}
|
|
447
|
+
if (!targetDirExists) {
|
|
448
|
+
throw new Error(`Target skills directory does not exist: ${targetDir}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
452
|
+
|
|
453
|
+
const dirs = entries.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith("."));
|
|
454
|
+
|
|
455
|
+
for (const e of dirs) {
|
|
456
|
+
const skillDir = path.join(targetDir, e.name);
|
|
457
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const stat = await fs.stat(skillMdPath);
|
|
461
|
+
if (!stat.isFile()) continue;
|
|
462
|
+
} catch (err) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const metaPath = path.join(skillDir, '.meta.json');
|
|
467
|
+
let isPlatform = false;
|
|
468
|
+
let isBuiltIn = e.isSymbolicLink();
|
|
469
|
+
let metaData: any = null;
|
|
470
|
+
let name = e.name;
|
|
471
|
+
let description = "";
|
|
472
|
+
let skillVersion = "";
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const mdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
476
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdContent)?.[1] ?? "";
|
|
477
|
+
const parsedName = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim();
|
|
478
|
+
if (parsedName) name = parsedName;
|
|
479
|
+
|
|
480
|
+
const descMatch = /(^|\n)description:\s*(?:>\s*\n\s*)?(.*?)(?=\n[a-z]+:|\n---|$)/is.exec(fm);
|
|
481
|
+
if (descMatch && descMatch[2]) {
|
|
482
|
+
description = descMatch[2].replace(/\n\s+/g, ' ').trim();
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const metaContent = await fs.readFile(metaPath, 'utf8');
|
|
488
|
+
const parsed = JSON.parse(metaContent);
|
|
489
|
+
if (parsed) {
|
|
490
|
+
if (parsed.ownerId === 'CMS' || parsed.ownerId === 'CMS_COMPAT') isPlatform = true;
|
|
491
|
+
if (parsed.isBuiltIn === true || parsed.ownerId === 'built-in') isBuiltIn = true;
|
|
492
|
+
metaData = parsed;
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {}
|
|
495
|
+
|
|
496
|
+
const resolvedVersion = await readSkillVersion(skillDir);
|
|
497
|
+
if (resolvedVersion) skillVersion = resolvedVersion;
|
|
498
|
+
|
|
499
|
+
if (isPlatform) {
|
|
500
|
+
list.push({
|
|
501
|
+
code: e.name,
|
|
502
|
+
isPlatform: true,
|
|
503
|
+
isBuiltIn: isBuiltIn,
|
|
504
|
+
version: skillVersion,
|
|
505
|
+
name,
|
|
506
|
+
description,
|
|
507
|
+
publishedAt: metaData?.publishedAt
|
|
508
|
+
});
|
|
509
|
+
} else {
|
|
510
|
+
list.push({
|
|
511
|
+
code: e.name,
|
|
512
|
+
isPlatform: false,
|
|
513
|
+
isBuiltIn: isBuiltIn,
|
|
514
|
+
version: skillVersion,
|
|
515
|
+
name: name,
|
|
516
|
+
description: description
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
521
|
+
|
|
522
|
+
} else if (action === "UPDATE_SKILL") {
|
|
523
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
524
|
+
// 更新可能是大批量广播,增加防阻塞的随机延时 (0~5秒)
|
|
525
|
+
const delayMs = Math.random() * 5000;
|
|
526
|
+
const syncBuiltInTemplate = shouldSyncBuiltInTemplate(action, isBuiltIn);
|
|
527
|
+
console.log(`[skill-logger-plugin][WS] Scheduled UPDATE for user ${userId}, code: ${safeCode} in ${Math.round(delayMs)}ms`);
|
|
528
|
+
this.appendLogToFile("INFO", "Command", `Scheduled UPDATE_SKILL`, {
|
|
529
|
+
userId,
|
|
530
|
+
code: safeCode,
|
|
531
|
+
version,
|
|
532
|
+
isBuiltIn,
|
|
533
|
+
syncBuiltInTemplate,
|
|
534
|
+
delayMs: Math.round(delayMs),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
setTimeout(async () => {
|
|
538
|
+
try {
|
|
539
|
+
const additionalTargetDirs = syncBuiltInTemplate
|
|
540
|
+
? [path.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")]
|
|
541
|
+
: [];
|
|
542
|
+
const result = await this.options.updater.manualInstall({
|
|
543
|
+
code: safeCode,
|
|
544
|
+
url,
|
|
545
|
+
version,
|
|
546
|
+
force: true,
|
|
547
|
+
targetDir,
|
|
548
|
+
additionalTargetDirs,
|
|
549
|
+
trace: this.createInstallTrace({
|
|
550
|
+
action,
|
|
551
|
+
replyId,
|
|
552
|
+
userId,
|
|
553
|
+
code: safeCode,
|
|
554
|
+
isBuiltIn,
|
|
555
|
+
syncBuiltInTemplate,
|
|
556
|
+
}),
|
|
557
|
+
});
|
|
558
|
+
this.appendLogToFile(result.success ? "INFO" : "ERROR", "Command", `UPDATE_SKILL completed`, {
|
|
559
|
+
userId,
|
|
560
|
+
code: safeCode,
|
|
561
|
+
replyId,
|
|
562
|
+
success: result.success,
|
|
563
|
+
message: result.message,
|
|
564
|
+
syncBuiltInTemplate,
|
|
565
|
+
});
|
|
566
|
+
if (replyId) {
|
|
567
|
+
this.reply(replyId, { success: result.success, message: result.message, action });
|
|
568
|
+
}
|
|
569
|
+
} catch (e: any) {
|
|
570
|
+
this.appendLogToFile("ERROR", "Command", `UPDATE_SKILL threw`, {
|
|
571
|
+
userId,
|
|
572
|
+
code: safeCode,
|
|
573
|
+
replyId,
|
|
574
|
+
message: e?.message || String(e),
|
|
575
|
+
stack: e?.stack,
|
|
576
|
+
});
|
|
577
|
+
if (replyId) this.reply(replyId, { success: false, message: e.message, action });
|
|
578
|
+
}
|
|
579
|
+
}, delayMs);
|
|
580
|
+
|
|
581
|
+
} else if (action === "INSTALL_EXPERT") {
|
|
582
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
583
|
+
const { name, version, downloadUrl, skills } = msg;
|
|
584
|
+
console.log(`[skill-logger-plugin][WS] Executing INSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
585
|
+
this.appendLogToFile("INFO", "Command", `INSTALL_EXPERT received`, { userId, code: safeCode, version, downloadUrl });
|
|
586
|
+
|
|
587
|
+
const userSkillRoot = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user");
|
|
588
|
+
|
|
589
|
+
// 1. 检查当前版本,同版本跳过
|
|
590
|
+
const expertTarget = path.join(userSkillRoot, "experts", safeCode);
|
|
591
|
+
let skipInstall = false;
|
|
592
|
+
const metaPath = path.join(expertTarget, ".meta.json");
|
|
593
|
+
try {
|
|
594
|
+
const raw = await fs.readFile(metaPath, "utf-8");
|
|
595
|
+
const existing = JSON.parse(raw);
|
|
596
|
+
if (existing.version && existing.version === (version || '1.0.0')) {
|
|
597
|
+
skipInstall = true;
|
|
598
|
+
}
|
|
599
|
+
} catch {}
|
|
600
|
+
|
|
601
|
+
if (!skipInstall) {
|
|
602
|
+
await fs.mkdir(path.dirname(expertTarget), { recursive: true });
|
|
603
|
+
const expertResult = await this.options.updater.installZipFromUrl(downloadUrl, expertTarget);
|
|
604
|
+
if (!expertResult.success) {
|
|
605
|
+
throw new Error(`专家安装失败: ${expertResult.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 写入/更新版本信息
|
|
610
|
+
const meta = { code: safeCode, name, version: version || '1.0.0', installedAt: Date.now() };
|
|
611
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
612
|
+
|
|
613
|
+
// 2. 安装依赖的 skills
|
|
614
|
+
const skillTargetRoot = path.join(userSkillRoot, "skills");
|
|
615
|
+
await fs.mkdir(skillTargetRoot, { recursive: true });
|
|
616
|
+
const skillResults: string[] = [];
|
|
617
|
+
if (Array.isArray(skills)) {
|
|
618
|
+
for (const sk of skills) {
|
|
619
|
+
if (!sk.code || !sk.downloadUrl) {
|
|
620
|
+
skillResults.push(`${sk.code || 'unknown'}: 缺少下载地址`);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const skTarget = path.join(skillTargetRoot, sk.code);
|
|
625
|
+
const result = await this.options.updater.installZipFromUrl(sk.downloadUrl, skTarget);
|
|
626
|
+
skillResults.push(`${sk.code}: ${result.success ? '成功' : '失败 - ' + result.message}`);
|
|
627
|
+
} catch (e: any) {
|
|
628
|
+
skillResults.push(`${sk.code}: 失败 - ${e.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this.reply(replyId, {
|
|
634
|
+
success: true,
|
|
635
|
+
message: `专家 ${safeCode} 安装完成`,
|
|
636
|
+
action,
|
|
637
|
+
data: { expertCode: safeCode, skills: skillResults },
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
} else if (action === "UNINSTALL_EXPERT") {
|
|
641
|
+
if (!safeCode) throw new Error("Missing code parameter");
|
|
642
|
+
console.log(`[skill-logger-plugin][WS] Executing UNINSTALL_EXPERT for user ${userId}, code: ${safeCode}`);
|
|
643
|
+
this.appendLogToFile("INFO", "Command", `UNINSTALL_EXPERT received`, { userId, code: safeCode });
|
|
644
|
+
|
|
645
|
+
const expertPath = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts", safeCode);
|
|
646
|
+
await fs.rm(expertPath, { recursive: true, force: true });
|
|
647
|
+
|
|
648
|
+
this.reply(replyId, { success: true, message: `专家 ${safeCode} 已卸载`, action });
|
|
649
|
+
|
|
650
|
+
} else if (action === "LIST_EXPERTS") {
|
|
651
|
+
console.log(`[skill-logger-plugin][WS] Executing LIST_EXPERTS for user ${userId}`);
|
|
652
|
+
this.appendLogToFile("INFO", "Command", `LIST_EXPERTS received`, { userId });
|
|
653
|
+
|
|
654
|
+
const expertsDir = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "experts");
|
|
655
|
+
const list: any[] = [];
|
|
656
|
+
try {
|
|
657
|
+
const stat = await fs.stat(expertsDir);
|
|
658
|
+
if (stat.isDirectory()) {
|
|
659
|
+
const entries = await fs.readdir(expertsDir, { withFileTypes: true });
|
|
660
|
+
for (const e of entries) {
|
|
661
|
+
if (!e.isDirectory()) continue;
|
|
662
|
+
const metaPath = path.join(expertsDir, e.name, ".meta.json");
|
|
663
|
+
try {
|
|
664
|
+
const raw = await fs.readFile(metaPath, "utf-8");
|
|
665
|
+
const meta = JSON.parse(raw);
|
|
666
|
+
list.push({
|
|
667
|
+
code: meta.code || e.name,
|
|
668
|
+
name: meta.name || e.name,
|
|
669
|
+
version: meta.version || '',
|
|
670
|
+
installedAt: meta.installedAt,
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
list.push({ code: e.name, name: e.name, version: '' });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch {}
|
|
678
|
+
this.reply(replyId, { success: true, data: list, action });
|
|
679
|
+
|
|
680
|
+
} else {
|
|
681
|
+
console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
|
|
682
|
+
this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
|
|
683
|
+
this.reply(replyId, { success: false, message: `Unknown action: ${action}`, action });
|
|
684
|
+
}
|
|
685
|
+
} catch (err: any) {
|
|
686
|
+
this.appendLogToFile("ERROR", "Command", `Error executing action ${action}`, err);
|
|
687
|
+
this.reply(replyId, { success: false, message: err.message, action });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private reply(replyId: string, payload: any) {
|
|
692
|
+
this.appendLogToFile("INFO", "Command", `Replying to command`, { replyId, payload });
|
|
693
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !replyId) {
|
|
694
|
+
this.appendLogToFile("WARN", "Command", `Cannot reply: WS closed or missing replyId`, { replyId });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.ws.send(JSON.stringify({ type: "REPLY", replyId, ...payload }), (err) => {
|
|
698
|
+
if (err) {
|
|
699
|
+
this.appendLogToFile("ERROR", "Command", `Reply send failed`, { replyId, message: err.message });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
this.appendLogToFile("INFO", "Command", `Reply sent`, { replyId });
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
public destroy() {
|
|
707
|
+
this.isDestroyed = true;
|
|
708
|
+
this.clearReconnectTimer();
|
|
709
|
+
this.clearAgentScanTimer();
|
|
710
|
+
this.clearHeartbeat();
|
|
711
|
+
this.clearConnectTimeout();
|
|
712
|
+
if (this.ws) {
|
|
713
|
+
this.ws.terminate(); // 强行销毁,斩断半开连接残留
|
|
714
|
+
this.ws = null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|